From 02b2a2fd34113a9877df7ccea8372d4e8d37fb7b Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 7 Sep 2026 07:23:26 +0000 Subject: [PATCH 1/4] fix(create): pair compiler and runtime releases --- .changeset/README.md | 16 +-- packages/create-agent-bundle/README.md | 19 +-- packages/create-agent-bundle/package.json | 12 ++ packages/create-agent-bundle/src/framework.ts | 113 +++++++++++------ .../create-agent-bundle/src/scaffold-cli.ts | 38 ++++-- packages/create-agent-bundle/src/scaffold.ts | 13 +- .../tests/framework.test.ts | 119 +++++++++++++----- .../tests/scaffold-noop.test.ts | 1 + .../tests/scaffold-packed.e2e.test.ts | 56 ++++++++- .../tests/scaffold.test.ts | 8 +- .../tests/support/package-tarball.ts | 21 ++-- .../tests/support/scaffold-fixture.ts | 57 +++++++-- pnpm-lock.yaml | 7 ++ website/docs/en/guide/start/installation.mdx | 8 ++ website/docs/en/guide/start/quick-start.mdx | 7 +- website/docs/zh/guide/start/installation.mdx | 6 + website/docs/zh/guide/start/quick-start.mdx | 6 +- 17 files changed, 388 insertions(+), 119 deletions(-) diff --git a/.changeset/README.md b/.changeset/README.md index 981b9d3d2..1e344f583 100644 --- a/.changeset/README.md +++ b/.changeset/README.md @@ -97,15 +97,17 @@ because they change no publishable package. package needs to move when the other does. Preview tarballs pin the peer to the same commit (`docs/preview-packages.md`), which is a preview concern, not a version-coupling one. Revisit if the peer range ever becomes exact. -- `create-agent-bundle` versions independently; its templates pin - `agent-bundle` explicitly rather than through a workspace range. +- `create-agent-bundle` versions independently, but its optional + `workspace:*` peers on `agent-bundle` and `@agent-bundle/runtime` are the + release pairing record. `pnpm pack` rewrites them to the two exact published + versions; the installed scaffolder reads that record instead of assuming + the packages share a version. - `updateInternalDependencies: "patch"` with `bumpVersionsWithWorkspaceProtocolOnly: true`: only `workspace:` ranges - between publishable packages trigger dependent patch bumps. The one such - range is `@agent-bundle/runtime`'s `rsc-markdown-stream: workspace:^`, so - a `rsc-markdown-stream` release also patch-bumps the runtime and - re-publishes it with the new caret (`pnpm publish` rewrites `workspace:^` - to `^` in the shipped manifest). + between publishable packages trigger dependent patch bumps. The runtime's + `rsc-markdown-stream: workspace:^` edge republishes it with the renderer's + new caret. The scaffolder's two optional `workspace:*` peers patch-bump and + republish it whenever either member of its exact release pair moves. - `access` stays `"restricted"` at the repository level until the release owner decides the npm package names and access policy (`docs/preview-packages.md`). `@agent-bundle/runtime` and diff --git a/packages/create-agent-bundle/README.md b/packages/create-agent-bundle/README.md index d52e115c4..af410462d 100644 --- a/packages/create-agent-bundle/README.md +++ b/packages/create-agent-bundle/README.md @@ -31,7 +31,7 @@ scripted and asks nothing — the remaining values fall back to their defaults. | `--targets ` | Comma-separated host targets: `portable`, `claude`, `codex`, `cursor`. Default: `portable,codex,claude`. | | `--package-manager ` | `npm`, `pnpm`, `yarn`, or `bun`. Default: detected from the invoking client. | | `--no-install` | Skip installing dependencies after scaffolding. | -| `--framework-version ` | Pin the project's `agent-bundle` dependency to this spec (a version, a tarball path, or a URL). | +| `--framework-version ` | Pin the project's `agent-bundle` dependency to this spec (a version, a tarball path, or a URL). Runtime templates require the compiler version recorded by this scaffolder release. | | `-h, --help` | Show usage. | ## Templates @@ -59,14 +59,15 @@ anything; its README documents the wiring to add with the first route. ## The framework dependency -Scaffolded projects pin `agent-bundle` to an exact -[pkg.pr.new](https://pkg.pr.new) preview tarball. Without -`--framework-version`, the pin is derived from this scaffolder's own preview -version: pkg.pr.new publishes every workspace package of one commit under the -same `-preview-` suffix, so the scaffolder and the framework it pins -always come from the same commit. A non-preview build of the scaffolder has -no derivable default (the `agent-bundle` name on npm currently belongs to an -unrelated project) and requires `--framework-version` explicitly. +Preview scaffolders pin `agent-bundle` and `@agent-bundle/runtime` to exact +[pkg.pr.new](https://pkg.pr.new) tarballs from one commit SHA. An npm release +instead records its compatible compiler and runtime versions as optional +peers in the packed `create-agent-bundle` manifest. The scaffolder pins those +two recorded versions independently — it never derives the runtime version +from the compiler version — and rejects a runtime-bearing scaffold whose +`--framework-version` does not match the recorded compiler. A local compiler +tarball selects the sibling runtime tarball with the recorded runtime version +and validates both package names and versions before writing the project. ## License diff --git a/packages/create-agent-bundle/package.json b/packages/create-agent-bundle/package.json index 942f5b1b5..84b2148ba 100644 --- a/packages/create-agent-bundle/package.json +++ b/packages/create-agent-bundle/package.json @@ -52,5 +52,17 @@ "@types/node": "26.4.1", "effect": "4.0.0-rc.112", "effect-rstest": "https://pkg.pr.new/ScriptedAlchemy/effect-rstest@e5f8d5f" + }, + "peerDependencies": { + "@agent-bundle/runtime": "workspace:*", + "agent-bundle": "workspace:*" + }, + "peerDependenciesMeta": { + "@agent-bundle/runtime": { + "optional": true + }, + "agent-bundle": { + "optional": true + } } } diff --git a/packages/create-agent-bundle/src/framework.ts b/packages/create-agent-bundle/src/framework.ts index 47b5e00e4..c30d0eccb 100644 --- a/packages/create-agent-bundle/src/framework.ts +++ b/packages/create-agent-bundle/src/framework.ts @@ -13,22 +13,35 @@ const unzip = promisify(gunzip); export type PreviewPackageName = 'agent-bundle' | '@agent-bundle/runtime' | 'create-agent-bundle'; +export interface FrameworkRuntimePairing { + readonly framework: string; + readonly runtime: string; +} + export const previewPackageSpec = (packageName: PreviewPackageName, sha: string): string => `https://pkg.pr.new/ScriptedAlchemy/agent-bundle/${packageName}@${sha}`; export const previewFrameworkSpec = (sha: string): string => previewPackageSpec('agent-bundle', sha); /** - * Derives a paired runtime package from an exact preview/local framework - * build, or reuses an npm version, range, or tag that resolves independently - * under each package name. + * Exact previews pair by commit. Released scaffolders carry the compiler and + * runtime versions their packed manifest selected from the workspace. */ -export const runtimeSpecForFramework = (frameworkSpec: string): string => { +export const runtimeSpecForFramework = ( + frameworkSpec: string, + pairing?: FrameworkRuntimePairing, +): string => { const preview = /^(https:\/\/pkg\.pr\.new\/ScriptedAlchemy\/agent-bundle\/)agent-bundle@([0-9a-f]{7,40})$/u.exec(frameworkSpec); if (preview !== null) return `${preview[1]}@agent-bundle/runtime@${preview[2]}`; const localTarball = /^(file:(?:.*[/\\])?)agent-bundle(-[^/\\]+)?\.tgz$/u.exec(frameworkSpec); if (localTarball !== null) { - return `${localTarball[1]}agent-bundle-runtime${localTarball[2] ?? ''}.tgz`; + if (pairing === undefined) { + throw new UsageError( + `Cannot select @agent-bundle/runtime for agent-bundle spec "${frameworkSpec}": ` + + 'this create-agent-bundle package has no release pairing metadata.', + ); + } + return `${localTarball[1]}agent-bundle-runtime${localTarball[2] === undefined ? '' : `-${pairing.runtime}`}.tgz`; } if ( frameworkSpec !== '' @@ -37,12 +50,25 @@ export const runtimeSpecForFramework = (frameworkSpec: string): string => { && !frameworkSpec.endsWith('.tgz') && !frameworkSpec.endsWith('.tar.gz') ) { - return frameworkSpec; + if (pairing === undefined) { + throw new UsageError( + `Cannot select @agent-bundle/runtime for agent-bundle spec "${frameworkSpec}": ` + + 'this create-agent-bundle package has no release pairing metadata.', + ); + } + if (frameworkSpec !== pairing.framework) { + throw new UsageError( + `This create-agent-bundle release is paired with agent-bundle ${pairing.framework} ` + + `and @agent-bundle/runtime ${pairing.runtime}; agent-bundle spec "${frameworkSpec}" may resolve ` + + 'an incompatible compiler. Omit --framework-version or install the matching create-agent-bundle release.', + ); + } + return pairing.runtime; } throw new UsageError( `Cannot derive a paired @agent-bundle/runtime package from agent-bundle spec "${frameworkSpec}". ` - + 'This package spec cannot be reused for @agent-bundle/runtime. Use an npm registry version, range, or tag; ' - + 'an exact pkg.pr.new preview URL; or a file: tarball ' + + 'This package spec cannot be reused for @agent-bundle/runtime. Use the npm registry version paired ' + + 'with this create-agent-bundle release, an exact pkg.pr.new preview URL, or a file: tarball ' + 'named agent-bundle.tgz or agent-bundle-.tgz.', ); }; @@ -88,13 +114,18 @@ const tarHeaderChecksumMatches = (header: Buffer): boolean => { * spec is written verbatim into that project's `package.json` and npm resolves * it from there — never from this CLI's working directory. */ -const packedPackageName = Effect.fnUntraced(function* ( +interface PackedPackageManifest { + readonly name: string; + readonly version: string; +} + +const packedPackageManifest = Effect.fnUntraced(function* ( tarballPath: string, -): Effect.fn.Return { +): Effect.fn.Return { const fs = yield* FileSystem.FileSystem; const compressed = yield* fs.readFile(tarballPath); const archive = yield* liftPromise(() => unzip(compressed)); - let packageName: string | undefined; + let packageManifest: PackedPackageManifest | undefined; for (let offset = 0; offset + tarBlockSize <= archive.length;) { const header = archive.subarray(offset, offset + tarBlockSize); if (isEndOfArchiveBlock(header)) break; @@ -112,26 +143,26 @@ const packedPackageName = Effect.fnUntraced(function* ( if (name === 'package/package.json') { const manifest = yield* liftTry(() => JSON.parse( archive.subarray(contentsOffset, contentsOffset + size).toString('utf8'), - ) as { readonly name?: unknown }); - if (typeof manifest.name !== 'string') { - return yield* Effect.fail(new Error('Packed package manifest has no string name.')); + ) as { readonly name?: unknown; readonly version?: unknown }); + if (typeof manifest.name !== 'string' || typeof manifest.version !== 'string') { + return yield* Effect.fail(new Error('Packed package manifest has no string name and version.')); } - packageName = manifest.name; + packageManifest = { name: manifest.name, version: manifest.version }; } offset = contentsOffset + Math.ceil(size / tarBlockSize) * tarBlockSize; } - if (packageName === undefined) { + if (packageManifest === undefined) { return yield* Effect.fail(new Error('Packed package manifest was not found.')); } - return packageName; + return packageManifest; }); -const localTarballPackageName = Effect.fnUntraced(function* ( +const localTarballPackageManifest = Effect.fnUntraced(function* ( packageSpec: string, baseDirectory: string, -): Effect.fn.Return { +): Effect.fn.Return { const path = yield* Path.Path; - return yield* packedPackageName(path.resolve(baseDirectory, packageSpec.slice('file:'.length))).pipe( + return yield* packedPackageManifest(path.resolve(baseDirectory, packageSpec.slice('file:'.length))).pipe( Effect.catch((error) => Effect.fail( new UsageError(`Cannot inspect local package tarball "${packageSpec}": ${describeError(error)}`), )), @@ -153,11 +184,11 @@ export const assertLocalFrameworkTarball = Effect.fnUntraced(function* ( baseDirectory: string, ): Effect.fn.Return { if (!frameworkSpec.startsWith('file:')) return; - const frameworkName = yield* localTarballPackageName(frameworkSpec, baseDirectory); - if (frameworkName !== 'agent-bundle') { + const framework = yield* localTarballPackageManifest(frameworkSpec, baseDirectory); + if (framework.name !== 'agent-bundle') { return yield* Effect.fail(new UsageError( `Local package tarball "${frameworkSpec}" is not the agent-bundle package: expected agent-bundle, ` - + `received ${JSON.stringify(frameworkName)}.`, + + `received ${JSON.stringify(framework.name)}.`, )); } }); @@ -169,19 +200,27 @@ export const assertLocalFrameworkTarball = Effect.fnUntraced(function* ( export const validatedRuntimeSpecForFramework = Effect.fnUntraced(function* ( frameworkSpec: string, baseDirectory: string, + pairing?: FrameworkRuntimePairing, ): Effect.fn.Return { - const runtimeSpec = yield* liftTry(() => runtimeSpecForFramework(frameworkSpec)).pipe( + const runtimeSpec = yield* liftTry(() => runtimeSpecForFramework(frameworkSpec, pairing)).pipe( Effect.catch((error) => (error instanceof UsageError ? Effect.fail(error) : Effect.die(error))), ); if (!frameworkSpec.startsWith('file:')) return runtimeSpec; - const [frameworkName, runtimeName] = yield* Effect.all([ - localTarballPackageName(frameworkSpec, baseDirectory), - localTarballPackageName(runtimeSpec, baseDirectory), + const [framework, runtime] = yield* Effect.all([ + localTarballPackageManifest(frameworkSpec, baseDirectory), + localTarballPackageManifest(runtimeSpec, baseDirectory), ], { concurrency: 'unbounded' }); - if (frameworkName !== 'agent-bundle' || runtimeName !== '@agent-bundle/runtime') { + if (framework.name !== 'agent-bundle' || runtime.name !== '@agent-bundle/runtime') { return yield* Effect.fail(new UsageError( `Local package tarballs are not a valid agent-bundle/runtime pair: expected agent-bundle and ` - + `@agent-bundle/runtime, received ${JSON.stringify(frameworkName)} and ${JSON.stringify(runtimeName)}.`, + + `@agent-bundle/runtime, received ${JSON.stringify(framework.name)} and ${JSON.stringify(runtime.name)}.`, + )); + } + if (pairing !== undefined && (framework.version !== pairing.framework || runtime.version !== pairing.runtime)) { + return yield* Effect.fail(new UsageError( + `Local package tarballs do not match this create-agent-bundle release: expected agent-bundle ` + + `${pairing.framework} and @agent-bundle/runtime ${pairing.runtime}, received agent-bundle ` + + `${framework.version} and @agent-bundle/runtime ${runtime.version}.`, )); } return runtimeSpec; @@ -195,17 +234,21 @@ export const validatedRuntimeSpecForFramework = Effect.fnUntraced(function* ( * preview version: pkg.pr.new publishes every workspace package of one * commit under the same `-preview-` string, so the paired * `agent-bundle` preview of the very build that shipped this scaffolder is - * always the right default. There is no derivable default outside a preview - * build — the `agent-bundle` name on npm belongs to an unrelated project, so - * falling back to a semver range would install the wrong package. + * always the right default. A registry release instead uses the compiler + * version recorded in its packed pairing metadata. */ -export const resolveFrameworkSpec = (ownVersion: string, flag: string | undefined): string => { +export const resolveFrameworkSpec = ( + ownVersion: string, + flag: string | undefined, + pairing?: FrameworkRuntimePairing, +): string => { if (flag !== undefined && flag.trim() !== '') return flag.trim(); const preview = previewPattern.exec(ownVersion); if (preview !== null) return previewFrameworkSpec(preview[1]!); + if (pairing !== undefined) return pairing.framework; throw new UsageError( - `This build of create-agent-bundle (${ownVersion}) is not a pkg.pr.new preview, so it cannot derive ` - + 'a default agent-bundle version. Pass --framework-version — for example ' + `This build of create-agent-bundle (${ownVersion}) is not a pkg.pr.new preview and has no release ` + + 'pairing metadata, so it cannot derive a default agent-bundle version. Pass --framework-version — for example ' + '--framework-version https://pkg.pr.new/ScriptedAlchemy/agent-bundle/agent-bundle@.', ); }; diff --git a/packages/create-agent-bundle/src/scaffold-cli.ts b/packages/create-agent-bundle/src/scaffold-cli.ts index 20980d6f4..ec2df864b 100644 --- a/packages/create-agent-bundle/src/scaffold-cli.ts +++ b/packages/create-agent-bundle/src/scaffold-cli.ts @@ -9,7 +9,7 @@ import type { PlatformError } from 'effect/PlatformError'; import { mapCause, runPromise } from './effect/boundary.ts'; import { liftPromise, liftTry } from './effect/lift.ts'; -import { resolveFrameworkSpec } from './framework.ts'; +import { type FrameworkRuntimePairing, resolveFrameworkSpec } from './framework.ts'; import { UsageError, resolveOptions, @@ -65,19 +65,29 @@ const clackPrompter: Prompter = { const ownDirectory = dirname(fileURLToPath(import.meta.url)); /** - * The version must be read from disk at run time, not inlined at build time: - * pkg.pr.new rewrites the manifest version to `-preview-` when - * it packs the preview tarball, and that suffix is what pairs the scaffolded - * project with the matching agent-bundle preview. + * The version and release pair must be read from disk at run time, not inlined: + * preview and npm packers rewrite this manifest after the build. */ -const ownVersion = Effect.gen(function* () { +interface OwnManifest { + readonly peerDependencies?: Readonly>; + readonly version: string; +} + +const ownManifest = Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const manifestPath = path.join(ownDirectory, '..', 'package.json'); - const manifest = JSON.parse(yield* fs.readFileString(manifestPath)) as { readonly version: string }; - return manifest.version; + return JSON.parse(yield* fs.readFileString(manifestPath)) as OwnManifest; }); +const releasePairing = (manifest: OwnManifest): FrameworkRuntimePairing | undefined => { + const framework = manifest.peerDependencies?.['agent-bundle']; + const runtime = manifest.peerDependencies?.['@agent-bundle/runtime']; + if (typeof framework !== 'string' || typeof runtime !== 'string') return undefined; + if (framework.startsWith('workspace:') || runtime.startsWith('workspace:')) return undefined; + return { framework, runtime }; +}; + const runInstall = (options: ResolvedOptions, targetDirectory: string): Effect.Effect => liftPromise(() => { log.step(`Installing dependencies with ${options.packageManager}...`); @@ -105,8 +115,9 @@ const scaffoldProgram = Effect.fnUntraced(function* ( ): Effect.fn.Return<0 | 1 | 2, PlatformError, FileSystem.FileSystem | Path.Path> { // Reading this package's own manifest fails before the intro, exactly as // it did as a rejected Promise: no cancel banner, the error leaves runCli. - const version = yield* ownVersion; - intro(`create-agent-bundle ${version}`); + const manifest = yield* ownManifest; + const pairing = releasePairing(manifest); + intro(`create-agent-bundle ${manifest.version}`); const run = Effect.gen(function* () { const path = yield* Path.Path; const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true; @@ -115,7 +126,11 @@ const scaffoldProgram = Effect.fnUntraced(function* ( prompter: clackPrompter, userAgent: process.env['npm_config_user_agent'], })); - const frameworkSpec = yield* liftTry(() => resolveFrameworkSpec(version, options.frameworkVersion)); + const frameworkSpec = yield* liftTry(() => resolveFrameworkSpec( + manifest.version, + options.frameworkVersion, + pairing, + )); const targetDirectory = path.resolve(process.cwd(), options.targetDir); yield* assertScaffoldTarget(targetDirectory, options.targetDir); @@ -123,6 +138,7 @@ const scaffoldProgram = Effect.fnUntraced(function* ( const files = yield* scaffold({ frameworkSpec, packageName: options.packageName, + pairing, pluginName: options.pluginName, targetDirectory, targets: options.targets, diff --git a/packages/create-agent-bundle/src/scaffold.ts b/packages/create-agent-bundle/src/scaffold.ts index 9b925857b..c9009771d 100644 --- a/packages/create-agent-bundle/src/scaffold.ts +++ b/packages/create-agent-bundle/src/scaffold.ts @@ -3,7 +3,11 @@ import type { PlatformError } from 'effect/PlatformError'; import { liftTry } from './effect/lift.ts'; import { defaultTargets, UsageError, type TargetName } from './options.ts'; -import { assertLocalFrameworkTarball, validatedRuntimeSpecForFramework } from './framework.ts'; +import { + assertLocalFrameworkTarball, + type FrameworkRuntimePairing, + validatedRuntimeSpecForFramework, +} from './framework.ts'; /** * The literal project name every template is written under. Templates stay @@ -35,6 +39,7 @@ const installableHostNames: readonly TargetName[] = ['claude', 'codex', 'cursor' export interface ScaffoldRequest { readonly frameworkSpec: string; readonly packageName: string; + readonly pairing?: FrameworkRuntimePairing; readonly pluginName: string; readonly targetDirectory: string; readonly targets: readonly TargetName[]; @@ -176,7 +181,11 @@ export const scaffold = Effect.fnUntraced(function* ( .some((section) => section?.['@agent-bundle/runtime'] === 'workspace:*'); let runtimeSpec: string | undefined; if (usesWorkspaceRuntime) { - runtimeSpec = yield* validatedRuntimeSpecForFramework(request.frameworkSpec, request.targetDirectory); + runtimeSpec = yield* validatedRuntimeSpecForFramework( + request.frameworkSpec, + request.targetDirectory, + request.pairing, + ); } else { yield* assertLocalFrameworkTarball(request.frameworkSpec, request.targetDirectory); } diff --git a/packages/create-agent-bundle/tests/framework.test.ts b/packages/create-agent-bundle/tests/framework.test.ts index ad2477029..73dfc6890 100644 --- a/packages/create-agent-bundle/tests/framework.test.ts +++ b/packages/create-agent-bundle/tests/framework.test.ts @@ -1,4 +1,6 @@ +import { readFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import * as NodeServices from '@effect/platform-node/NodeServices'; import { Effect, FileSystem, Path } from 'effect'; @@ -34,6 +36,25 @@ const expectUsageError = (error: unknown, message?: string): void => { if (message !== undefined) expect((error as Error).message).toContain(message); }; +const releasePairing = { framework: '0.2.0', runtime: '0.1.0' } as const; + +it('declares the compiler/runtime release pair as optional workspace peers', async () => { + const manifest = JSON.parse( + await readFile(join(process.cwd(), 'packages/create-agent-bundle/package.json'), 'utf8'), + ) as { + readonly peerDependencies: Record; + readonly peerDependenciesMeta: Record; + }; + expect(manifest.peerDependencies).toMatchObject({ + '@agent-bundle/runtime': 'workspace:*', + 'agent-bundle': 'workspace:*', + }); + expect(manifest.peerDependenciesMeta).toMatchObject({ + '@agent-bundle/runtime': { optional: true }, + 'agent-bundle': { optional: true }, + }); +}); + describe('previewPackageSpec', () => { it('derives the renamed runtime pkg.pr.new URL', () => { expect(previewPackageSpec('@agent-bundle/runtime', 'da5df1d')) @@ -50,9 +71,13 @@ describe('resolveFrameworkSpec', () => { }); it('lets --framework-version win verbatim', () => { - expect(resolveFrameworkSpec('0.0.0-preview-da5df1d', 'file:/tmp/agent-bundle.tgz')) + expect(resolveFrameworkSpec('0.0.0-preview-da5df1d', 'file:/tmp/agent-bundle.tgz', releasePairing)) .toBe('file:/tmp/agent-bundle.tgz'); - expect(resolveFrameworkSpec('0.0.0', ' 0.2.0 ')).toBe('0.2.0'); + expect(resolveFrameworkSpec('0.1.0', ' 0.2.0 ', releasePairing)).toBe('0.2.0'); + }); + + it('pins the compiler selected by an installed registry scaffolder', () => { + expect(resolveFrameworkSpec('0.1.0', undefined, releasePairing)).toBe('0.2.0'); }); it('refuses to guess outside a preview build (the npm agent-bundle name is unrelated)', () => { @@ -62,21 +87,31 @@ describe('resolveFrameworkSpec', () => { }); describe('runtimeSpecForFramework', () => { - it('pairs preview and local tarball framework specs with the runtime package', () => { - expect(runtimeSpecForFramework('https://pkg.pr.new/ScriptedAlchemy/agent-bundle/agent-bundle@da5df1d')) + it('keeps previews on their exact SHA and uses the recorded runtime version for local tarballs', () => { + expect(runtimeSpecForFramework( + 'https://pkg.pr.new/ScriptedAlchemy/agent-bundle/agent-bundle@da5df1d', + releasePairing, + )) .toBe('https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@da5df1d'); - expect(runtimeSpecForFramework('file:/tmp/agent-bundle-0.1.0.tgz')) + expect(runtimeSpecForFramework('file:/tmp/agent-bundle-0.2.0.tgz', releasePairing)) .toBe('file:/tmp/agent-bundle-runtime-0.1.0.tgz'); - expect(runtimeSpecForFramework('file:/tmp/agent-bundle.tgz')) + expect(runtimeSpecForFramework('file:/tmp/agent-bundle.tgz', releasePairing)) .toBe('file:/tmp/agent-bundle-runtime.tgz'); }); - it('mirrors npm registry framework specs onto the runtime package', () => { - expect(runtimeSpecForFramework('0.1.0')).toBe('0.1.0'); - expect(runtimeSpecForFramework('^0.1.0')).toBe('^0.1.0'); - expect(runtimeSpecForFramework('>=0.1.0 <1')).toBe('>=0.1.0 <1'); - expect(runtimeSpecForFramework('1.x')).toBe('1.x'); - expect(runtimeSpecForFramework('next')).toBe('next'); + it('selects the recorded runtime version for the paired registry compiler', () => { + expect(runtimeSpecForFramework('0.2.0', releasePairing)).toBe('0.1.0'); + }); + + it('rejects a registry compiler outside the installed scaffolder pairing', () => { + expect(() => runtimeSpecForFramework('0.1.0', releasePairing)).toThrow(UsageError); + expect(() => runtimeSpecForFramework('0.1.0', releasePairing)) + .toThrow('paired with agent-bundle 0.2.0 and @agent-bundle/runtime 0.1.0'); + }); + + it('never copies a registry compiler selector when pairing metadata is absent', () => { + expect(() => runtimeSpecForFramework('0.2.0')).toThrow(UsageError); + expect(() => runtimeSpecForFramework('0.2.0')).toThrow('pairing metadata'); }); it('rejects package specs that cannot resolve independently under the runtime name', () => { @@ -91,14 +126,14 @@ describe('runtimeSpecForFramework', () => { 'agent-bundle.tar.gz', ]; for (const spec of unsupportedSpecs) { - expect(() => runtimeSpecForFramework(spec)).toThrow(UsageError); - expect(() => runtimeSpecForFramework(spec)).toThrow('cannot be reused for @agent-bundle/runtime'); + expect(() => runtimeSpecForFramework(spec, releasePairing)).toThrow(UsageError); + expect(() => runtimeSpecForFramework(spec, releasePairing)).toThrow('cannot be reused for @agent-bundle/runtime'); } }); it('fails closed when a paired runtime spec cannot be derived', () => { - expect(() => runtimeSpecForFramework('file:/tmp/framework.tgz')).toThrow(UsageError); - expect(() => runtimeSpecForFramework('file:/tmp/framework.tgz')).toThrow('npm registry version, range, or tag'); + expect(() => runtimeSpecForFramework('file:/tmp/framework.tgz', releasePairing)).toThrow(UsageError); + expect(() => runtimeSpecForFramework('file:/tmp/framework.tgz', releasePairing)).toThrow('npm registry version'); }); }); @@ -151,37 +186,65 @@ layer(NodeServices.layer, { excludeTestServices: true })('assertLocalFrameworkTa }); layer(NodeServices.layer, { excludeTestServices: true })('validatedRuntimeSpecForFramework', (it) => { - it.effect('leaves registry and preview specs to npm', () => Effect.gen(function* () { - expect(yield* validatedRuntimeSpecForFramework('0.1.0', tmpdir())).toBe('0.1.0'); + it.effect('selects the installed scaffolder runtime for its registry compiler', () => Effect.gen(function* () { + expect(yield* validatedRuntimeSpecForFramework('0.2.0', tmpdir(), releasePairing)).toBe('0.1.0'); + })); + + it.effect('rejects a registry compiler outside the installed scaffolder pairing', () => Effect.gen(function* () { + expectUsageError( + yield* Effect.flip(validatedRuntimeSpecForFramework('0.1.0', tmpdir(), releasePairing)), + 'paired with agent-bundle 0.2.0 and @agent-bundle/runtime 0.1.0', + ); })); it.effect('fails closed on the typed usage error when no runtime spec can be derived', () => Effect.gen(function* () { expectUsageError( yield* Effect.flip(validatedRuntimeSpecForFramework('file:/tmp/framework.tgz', tmpdir())), - 'npm registry version, range, or tag', + 'npm registry version paired with this create-agent-bundle release', ); })); it.effect('resolves a relative file: pair against the base directory', () => Effect.gen(function* () { const path = yield* Path.Path; const directory = yield* tarballDirectory({ - 'agent-bundle-0.0.0.tgz': packageTarball('agent-bundle'), - 'agent-bundle-runtime-0.0.0.tgz': packageTarball('@agent-bundle/runtime'), + 'agent-bundle-0.2.0.tgz': packageTarball('agent-bundle', '0.2.0'), + 'agent-bundle-runtime-0.1.0.tgz': packageTarball('@agent-bundle/runtime', '0.1.0'), }); - const spec = 'file:../agent-bundle-0.0.0.tgz'; - expect(yield* validatedRuntimeSpecForFramework(spec, path.join(directory, 'project'))) - .toBe('file:../agent-bundle-runtime-0.0.0.tgz'); - expectUsageError(yield* Effect.flip(validatedRuntimeSpecForFramework(spec, process.cwd()))); + const spec = 'file:../agent-bundle-0.2.0.tgz'; + expect(yield* validatedRuntimeSpecForFramework(spec, path.join(directory, 'project'), releasePairing)) + .toBe('file:../agent-bundle-runtime-0.1.0.tgz'); + expectUsageError(yield* Effect.flip(validatedRuntimeSpecForFramework(spec, process.cwd(), releasePairing))); + })); + + it.effect('rejects a runtime tarball whose package version violates the recorded pairing', () => Effect.gen(function* () { + const path = yield* Path.Path; + const directory = yield* tarballDirectory({ + 'agent-bundle-0.2.0.tgz': packageTarball('agent-bundle', '0.2.0'), + 'agent-bundle-runtime-0.1.0.tgz': packageTarball('@agent-bundle/runtime', '0.2.0'), + }); + expectUsageError( + yield* Effect.flip(validatedRuntimeSpecForFramework( + `file:${path.join(directory, 'agent-bundle-0.2.0.tgz')}`, + directory, + releasePairing, + )), + 'expected agent-bundle 0.2.0 and @agent-bundle/runtime 0.1.0, ' + + 'received agent-bundle 0.2.0 and @agent-bundle/runtime 0.2.0', + ); })); it.effect('rejects a pair whose runtime tarball has a tampered tar header', () => Effect.gen(function* () { const path = yield* Path.Path; const directory = yield* tarballDirectory({ - 'agent-bundle-0.0.0.tgz': packageTarball('agent-bundle'), - 'agent-bundle-runtime-0.0.0.tgz': tamperedPackageTarball('@agent-bundle/runtime'), + 'agent-bundle-0.2.0.tgz': packageTarball('agent-bundle', '0.2.0'), + 'agent-bundle-runtime-0.1.0.tgz': tamperedPackageTarball('@agent-bundle/runtime', '0.1.0'), }); expectUsageError( - yield* Effect.flip(validatedRuntimeSpecForFramework(`file:${path.join(directory, 'agent-bundle-0.0.0.tgz')}`, directory)), + yield* Effect.flip(validatedRuntimeSpecForFramework( + `file:${path.join(directory, 'agent-bundle-0.2.0.tgz')}`, + directory, + releasePairing, + )), 'Invalid tar header checksum', ); })); diff --git a/packages/create-agent-bundle/tests/scaffold-noop.test.ts b/packages/create-agent-bundle/tests/scaffold-noop.test.ts index 4a81534c6..90ded852c 100644 --- a/packages/create-agent-bundle/tests/scaffold-noop.test.ts +++ b/packages/create-agent-bundle/tests/scaffold-noop.test.ts @@ -146,6 +146,7 @@ describe('scaffold over FileSystem.layerNoop', () => { const error = yield* Effect.flip(scaffold({ frameworkSpec: 'file:../agent-bundle-0.4.0.tgz', packageName: 'status-plugin', + pairing: { framework: '0.4.0', runtime: '0.3.0' }, pluginName: 'status-plugin', targetDirectory: '/project', targets: ['portable'], diff --git a/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts b/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts index 9ea9a4159..4825ac9ea 100644 --- a/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts +++ b/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts @@ -3,18 +3,70 @@ import { join } from 'node:path'; import { afterAll, expect, it } from '@rstest/core'; +import { openPackedMcpServer, removeProjectSource } from '../../agent-bundle/src/test/packed.ts'; +import { installedEnvironment } from '../../agent-bundle/tests/support/shared-pack.ts'; import { cleanupScaffoldFixture, expectCleanValidate, + installScaffoldedProject, npmRun, scaffoldProject, scaffoldProjectWithMismatchedRuntime, + scaffoldReleasePairing, } from './support/scaffold-fixture.ts'; afterAll(cleanupScaffoldFixture); -it('rejects a local framework tarball paired with the wrong runtime package', async () => { - await expect(scaffoldProjectWithMismatchedRuntime('mismatched-runtime-project')).rejects.toMatchObject({ code: 2 }); +it('rejects a local framework tarball paired with an incompatible runtime version', async () => { + const pairing = await scaffoldReleasePairing(); + await expect(scaffoldProjectWithMismatchedRuntime('mismatched-runtime-project')).rejects.toMatchObject({ + code: 2, + stdout: expect.stringContaining( + `expected agent-bundle ${pairing.framework} and @agent-bundle/runtime ${pairing.runtime}, ` + + `received agent-bundle ${pairing.framework} and @agent-bundle/runtime 999.0.0`, + ), + }); +}, 600_000); + +it('scaffolds with differently versioned release tarballs and runs after source deletion', async () => { + const pairing = await scaffoldReleasePairing(); + expect(pairing.framework).not.toBe(pairing.runtime); + + const projectRoot = await scaffoldProject('mcp-server', 'paired-runtime-project', ['--no-install']); + const manifest = JSON.parse(await readFile(join(projectRoot, 'package.json'), 'utf8')) as { + readonly devDependencies: Record; + }; + expect(manifest.devDependencies['agent-bundle']?.endsWith(`agent-bundle-${pairing.framework}.tgz`)).toBe(true); + expect(manifest.devDependencies['@agent-bundle/runtime'] + ?.endsWith(`agent-bundle-runtime-${pairing.runtime}.tgz`)).toBe(true); + + await installScaffoldedProject(projectRoot); + await npmRun(projectRoot, 'build'); + + const artifact = join(projectRoot, 'artifact'); + const mcp = JSON.parse(await readFile(join(artifact, 'mcp.json'), 'utf8')) as { + readonly mcpServers: { readonly status: { readonly args: readonly [string, ...string[]] } }; + }; + const deletedSource = await removeProjectSource({ projectRoot }); + expect(deletedSource.removed).toEqual(['agent-bundle.config.ts', 'src']); + const session = await openPackedMcpServer({ + cwd: projectRoot, + deletedSource, + entry: join(artifact, mcp.mcpServers.status.args[0]), + env: Object.fromEntries( + Object.entries(installedEnvironment()).filter((entry): entry is [string, string] => entry[1] !== undefined), + ), + }); + try { + await expect(session.client.callTool({ + arguments: { service: 'docs' }, + name: 'report-status', + })).resolves.toMatchObject({ + structuredContent: { service: 'docs', status: 'healthy' }, + }); + } finally { + await session.close(); + } }, 600_000); /** diff --git a/packages/create-agent-bundle/tests/scaffold.test.ts b/packages/create-agent-bundle/tests/scaffold.test.ts index b51b721b3..9adc5270d 100644 --- a/packages/create-agent-bundle/tests/scaffold.test.ts +++ b/packages/create-agent-bundle/tests/scaffold.test.ts @@ -8,6 +8,7 @@ import { assertScaffoldTarget, placeholderName, scaffold } from '../src/scaffold import { packageTarball, tamperedPackageTarball } from './support/package-tarball.ts'; const workspaceRoot = process.cwd(); +const testPairing = { framework: '0.0.0', runtime: '0.0.0' } as const; const templateRoot = (path: Path.Path, template: string): string => path.join(workspaceRoot, 'packages', 'create-agent-bundle', 'templates', template); @@ -44,6 +45,7 @@ const scaffoldTemplate = Effect.fnUntraced(function* ( const files = yield* scaffold({ frameworkSpec, packageName: overrides.packageName ?? 'status-plugin', + pairing: testPairing, pluginName: overrides.pluginName ?? 'status-plugin', targetDirectory: path.join(root, 'project'), targets: overrides.targets ?? ['portable', 'codex', 'claude'], @@ -247,7 +249,7 @@ layer(NodeServices.layer, { excludeTestServices: true })('scaffold (real filesys readonly devDependencies: Record; }>(path.join(root, 'package.json')); expect(manifest.dependencies).toBeUndefined(); - expect(manifest.devDependencies['@agent-bundle/runtime']).toBe(runtimeSpecForFramework(frameworkSpec)); + expect(manifest.devDependencies['@agent-bundle/runtime']).toBe(runtimeSpecForFramework(frameworkSpec, testPairing)); expect(manifest.devDependencies['zod']).toBeDefined(); })); @@ -272,7 +274,7 @@ layer(NodeServices.layer, { excludeTestServices: true })('scaffold (real filesys expect(manifest.devDependencies['agent-bundle']).toBe(frameworkSpec); expect(files).toContain('src/cli/greet.ts'); expect(manifest.dependencies).toBeUndefined(); - expect(manifest.devDependencies['@agent-bundle/runtime']).toBe(runtimeSpecForFramework(frameworkSpec)); + expect(manifest.devDependencies['@agent-bundle/runtime']).toBe(runtimeSpecForFramework(frameworkSpec, testPairing)); expect(manifest.bin).toBeUndefined(); const config = yield* readText(path.join(root, 'agent-bundle.config.ts')); expect(config).toContain("name: 'status-plugin'"); @@ -329,6 +331,7 @@ layer(NodeServices.layer, { excludeTestServices: true })('scaffold (real filesys const error = yield* Effect.flip(scaffold({ frameworkSpec: `file:${frameworkTarball}`, packageName: 'status-plugin', + pairing: testPairing, pluginName: 'status-plugin', targetDirectory, targets: ['portable', 'codex', 'claude'], @@ -440,6 +443,7 @@ layer(NodeServices.layer, { excludeTestServices: true })('scaffold (real filesys const files = yield* scaffold({ frameworkSpec: 'file:../agent-bundle-0.0.0.tgz', packageName: 'status-plugin', + pairing: testPairing, pluginName: 'status-plugin', targetDirectory, targets: ['portable'], diff --git a/packages/create-agent-bundle/tests/support/package-tarball.ts b/packages/create-agent-bundle/tests/support/package-tarball.ts index d984d151d..82d006367 100644 --- a/packages/create-agent-bundle/tests/support/package-tarball.ts +++ b/packages/create-agent-bundle/tests/support/package-tarball.ts @@ -20,8 +20,8 @@ const applyHeaderChecksum = (header: Buffer): void => { * The smallest archive `localTarballPackageName` accepts: one ustar entry for * `package/package.json` naming the package, then the end-of-archive blocks. */ -export const packageTarArchive = (name: string): Buffer => { - const manifest = Buffer.from(JSON.stringify({ name })); +export const packageTarArchive = (name: string, version = '0.0.0'): Buffer => { + const manifest = Buffer.from(JSON.stringify({ name, version })); const archive = Buffer.alloc( tarBlockSize + Math.ceil(manifest.length / tarBlockSize) * tarBlockSize + tarBlockSize * 2, ); @@ -40,15 +40,16 @@ export const packageTarArchive = (name: string): Buffer => { return archive; }; -export const packageTarball = (name: string): Buffer => gzipSync(packageTarArchive(name)); +export const packageTarball = (name: string, version = '0.0.0'): Buffer => + gzipSync(packageTarArchive(name, version)); /** * A gzip stream that still inflates cleanly, carrying a header whose mode was * rewritten without refreshing the checksum. The parser never reads the mode, * so only checksum verification can tell this archive from a sound one. */ -export const tamperedPackageTarball = (name: string): Buffer => { - const archive = packageTarArchive(name); +export const tamperedPackageTarball = (name: string, version = '0.0.0'): Buffer => { + const archive = packageTarArchive(name, version); writeOctalField(archive, 0o777, tarModeOffset, 8); return gzipSync(archive); }; @@ -80,8 +81,8 @@ const writeTarEntry = ( * Like `packageTarArchive`, but adds a trailing entry after `package/package.json` * so callers can tamper with a later header without touching the manifest block. */ -export const packageTarArchiveWithTrailingEntry = (name: string): Buffer => { - const manifest = Buffer.from(JSON.stringify({ name })); +export const packageTarArchiveWithTrailingEntry = (name: string, version = '0.0.0'): Buffer => { + const manifest = Buffer.from(JSON.stringify({ name, version })); const trailing = Buffer.from('trailing payload'); const archive = Buffer.alloc( tarBlockSize @@ -96,9 +97,9 @@ export const packageTarArchiveWithTrailingEntry = (name: string): Buffer => { }; /** Manifest checksum is valid; a later tar header checksum is not. */ -export const tamperedTrailingHeaderPackageTarball = (name: string): Buffer => { - const archive = packageTarArchiveWithTrailingEntry(name); - const manifestBlocks = Math.ceil(Buffer.from(JSON.stringify({ name })).length / tarBlockSize) * tarBlockSize; +export const tamperedTrailingHeaderPackageTarball = (name: string, version = '0.0.0'): Buffer => { + const archive = packageTarArchiveWithTrailingEntry(name, version); + const manifestBlocks = Math.ceil(Buffer.from(JSON.stringify({ name, version })).length / tarBlockSize) * tarBlockSize; const trailingHeaderOffset = tarBlockSize + manifestBlocks; writeOctalField(archive, 0o777, trailingHeaderOffset + tarModeOffset, 8); return gzipSync(archive); diff --git a/packages/create-agent-bundle/tests/support/scaffold-fixture.ts b/packages/create-agent-bundle/tests/support/scaffold-fixture.ts index 7b0977706..b23fcc083 100644 --- a/packages/create-agent-bundle/tests/support/scaffold-fixture.ts +++ b/packages/create-agent-bundle/tests/support/scaffold-fixture.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { copyFile, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { basename, dirname, join } from 'node:path'; import { promisify } from 'node:util'; @@ -10,6 +10,7 @@ import { cachedNpmInstallArguments, installedEnvironment, npmInstallArguments, + packOutputFromJson, sharedPackedTarball, } from '../../../agent-bundle/tests/support/shared-pack.ts'; @@ -17,9 +18,12 @@ const execFile = promisify(executeFile); interface PackedFixture { readonly frameworkTarball: string; + readonly frameworkVersion: string; readonly markdownStreamTarball: string; readonly root: string; readonly runnerRoot: string; + readonly runtimeTarball: string; + readonly runtimeVersion: string; readonly scaffolderBin: string; } @@ -35,9 +39,9 @@ interface PackedFixture { const packFixture = async (): Promise => { const root = await mkdtemp(join(tmpdir(), 'create-agent-bundle-e2e-')); const [ - { tarball: frameworkTarball }, + { packOutput: frameworkPack, tarball: frameworkTarball }, { tarball: scaffolderTarball }, - { tarball: runtimeTarball }, + { packOutput: runtimePack, tarball: runtimeTarball }, { tarball: markdownStreamTarball }, ] = await Promise.all([ sharedPackedTarball('agent-bundle'), @@ -47,9 +51,9 @@ const packFixture = async (): Promise => { ]); const pairedRuntimeTarball = join( dirname(frameworkTarball), - basename(frameworkTarball).replace(/^agent-bundle-/u, 'agent-bundle-runtime-'), + basename(runtimeTarball), ); - await copyFile(runtimeTarball, pairedRuntimeTarball); + if (pairedRuntimeTarball !== runtimeTarball) await copyFile(runtimeTarball, pairedRuntimeTarball); const runnerRoot = join(root, 'runner'); await mkdir(runnerRoot, { recursive: true }); @@ -60,9 +64,12 @@ const packFixture = async (): Promise => { }); return { frameworkTarball, + frameworkVersion: frameworkPack.version, markdownStreamTarball, root, runnerRoot, + runtimeTarball, + runtimeVersion: runtimePack.version, scaffolderBin: join(runnerRoot, 'node_modules', '.bin', 'create-agent-bundle'), }; }; @@ -113,15 +120,47 @@ export const scaffoldProject = async ( return join(runnerRoot, projectName); }; +export const scaffoldReleasePairing = async (): Promise> => { + const { frameworkVersion: framework, runtimeVersion: runtime } = await fixture(); + return { framework, runtime }; +}; + export const scaffoldProjectWithMismatchedRuntime = async (projectName: string): Promise => { - const { frameworkTarball, root, runnerRoot, scaffolderBin } = await fixture(); + const { + frameworkTarball, + frameworkVersion, + root, + runnerRoot, + runtimeTarball, + runtimeVersion, + scaffolderBin, + } = await fixture(); const mismatchedDirectory = join(root, 'mismatched-pair'); - const mismatchedFramework = join(mismatchedDirectory, 'agent-bundle-mismatched.tgz'); - const mismatchedRuntime = join(mismatchedDirectory, 'agent-bundle-runtime-mismatched.tgz'); + const mismatchedFramework = join(mismatchedDirectory, `agent-bundle-${frameworkVersion}.tgz`); + const mismatchedRuntime = join(mismatchedDirectory, `agent-bundle-runtime-${runtimeVersion}.tgz`); await mkdir(mismatchedDirectory, { recursive: true }); + const extracted = join(mismatchedDirectory, 'runtime'); + await mkdir(extracted); + await execFile('tar', ['-xzf', runtimeTarball, '-C', extracted]); + const runtimeRoot = join(extracted, 'package'); + const manifestPath = join(runtimeRoot, 'package.json'); + const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as { version: string }; + manifest.version = '999.0.0'; + await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); + const { stdout } = await execFile('npm', [ + 'pack', + '--json', + '--ignore-scripts', + '--pack-destination', + mismatchedDirectory, + ], { cwd: runtimeRoot, env: installedEnvironment() }); + const repackedRuntime = join(mismatchedDirectory, packOutputFromJson(stdout).filename); await Promise.all([ copyFile(frameworkTarball, mismatchedFramework), - copyFile(frameworkTarball, mismatchedRuntime), + copyFile(repackedRuntime, mismatchedRuntime), ]); await execFile(scaffolderBin, [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2b684f647..89cb2cbad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -346,6 +346,13 @@ importers: version: 4.5.4 packages/create-agent-bundle: + dependencies: + '@agent-bundle/runtime': + specifier: workspace:* + version: link:../rsc-runtime + agent-bundle: + specifier: workspace:* + version: link:../agent-bundle devDependencies: '@clack/prompts': specifier: 1.7.0 diff --git a/website/docs/en/guide/start/installation.mdx b/website/docs/en/guide/start/installation.mdx index 47a4408f2..e6f479a86 100644 --- a/website/docs/en/guide/start/installation.mdx +++ b/website/docs/en/guide/start/installation.mdx @@ -72,6 +72,14 @@ npm install --save-dev agent-bundle npm create agent-bundle@latest my-plugin ``` +The packed `create-agent-bundle` release records its compatible `agent-bundle` and +`@agent-bundle/runtime` versions as optional peers. `npm create` pins those recorded versions +independently instead of copying the compiler version onto the runtime package. For example, the +compiler may be `0.2.0` while its runtime is `0.1.0`. A runtime-bearing template rejects a +`--framework-version` outside that release pair; install the matching scaffolder release instead. +Local compiler tarballs use the recorded runtime version in the sibling tarball name and verify +both packed manifests before writing the project. + The first npm release will use npm package provenance. Until the release owner picks the final package name and license, pkg.pr.new previews are the release channel. diff --git a/website/docs/en/guide/start/quick-start.mdx b/website/docs/en/guide/start/quick-start.mdx index 5ab964af2..55be5353c 100644 --- a/website/docs/en/guide/start/quick-start.mdx +++ b/website/docs/en/guide/start/quick-start.mdx @@ -43,8 +43,11 @@ zero diagnostics — including the `AB473x` migration nudges, because the templa against the entry conventions from the start. The `mcp-server` template also starts with the consumer test harness, each pool labeled with the proof level it carries. -Scaffolded projects pin `agent-bundle` to an exact preview tarball from the same commit the -scaffolder came from, so the scaffolder and the framework it pins never disagree. See the +Preview scaffolders pin `agent-bundle` and `@agent-bundle/runtime` to one commit SHA. An +npm-installed scaffolder instead reads the exact compiler/runtime pair from its packed optional +peer metadata; those versions are independent, so a compiler `0.2.0` release can correctly select +runtime `0.1.0`. A `--framework-version` used with a runtime template must match that recorded +compiler version, or scaffolding stops before writing files. See the [create-agent-bundle README](https://github.com/ScriptedAlchemy/agent-bundle/blob/main/packages/create-agent-bundle/README.md) for every flag. diff --git a/website/docs/zh/guide/start/installation.mdx b/website/docs/zh/guide/start/installation.mdx index 01552ce0a..959d0b5cf 100644 --- a/website/docs/zh/guide/start/installation.mdx +++ b/website/docs/zh/guide/start/installation.mdx @@ -65,6 +65,12 @@ npm install --save-dev agent-bundle npm create agent-bundle@latest my-plugin ``` +打包后的 `create-agent-bundle` 版本会把兼容的 `agent-bundle` 与 `@agent-bundle/runtime` 版本记录为可选 +peer。`npm create` 会分别固定这两个记录值,而不是把 compiler 版本复制给 runtime 包。例如 compiler +可以是 `0.2.0`,而对应 runtime 是 `0.1.0`。带 runtime 的模板会拒绝超出该发布配对的 +`--framework-version`;此时应安装与 compiler 匹配的脚手架版本。本地 compiler tarball 会在相邻 +runtime tarball 的文件名中使用记录的 runtime 版本,并在写入项目之前校验两个包的清单。 + 首个 npm 版本将使用 npm package provenance。在发布负责人确定最终包名与许可证之前,pkg.pr.new 预览 就是发布通道。 diff --git a/website/docs/zh/guide/start/quick-start.mdx b/website/docs/zh/guide/start/quick-start.mdx index 6b446a28e..6f0042ee1 100644 --- a/website/docs/zh/guide/start/quick-start.mdx +++ b/website/docs/zh/guide/start/quick-start.mdx @@ -39,8 +39,10 @@ npx https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@ Date: Mon, 7 Sep 2026 07:34:16 +0000 Subject: [PATCH 2/4] test(create): prove versioned release pairing --- .changeset/create-runtime-release-pairing.md | 5 +++++ packages/create-agent-bundle/src/framework.ts | 10 +++++----- .../tests/scaffold-packed.e2e.test.ts | 8 +++----- .../tests/support/package-tarball.ts | 4 ++-- 4 files changed, 15 insertions(+), 12 deletions(-) create mode 100644 .changeset/create-runtime-release-pairing.md diff --git a/.changeset/create-runtime-release-pairing.md b/.changeset/create-runtime-release-pairing.md new file mode 100644 index 000000000..2ccbe58c9 --- /dev/null +++ b/.changeset/create-runtime-release-pairing.md @@ -0,0 +1,5 @@ +--- +"create-agent-bundle": patch +--- + +Make `npm create agent-bundle` select the compiler/runtime pair recorded by `create-agent-bundle`, preserve same-SHA previews, and reject incompatible `--framework-version` pairings before writing files. (#739) diff --git a/packages/create-agent-bundle/src/framework.ts b/packages/create-agent-bundle/src/framework.ts index c30d0eccb..f94038a9b 100644 --- a/packages/create-agent-bundle/src/framework.ts +++ b/packages/create-agent-bundle/src/framework.ts @@ -109,11 +109,6 @@ const tarHeaderChecksumMatches = (header: Buffer): boolean => { return stored === unsigned || stored === signed; }; -/** - * `baseDirectory` is the scaffolded project root, because a relative `file:` - * spec is written verbatim into that project's `package.json` and npm resolves - * it from there — never from this CLI's working directory. - */ interface PackedPackageManifest { readonly name: string; readonly version: string; @@ -157,6 +152,11 @@ const packedPackageManifest = Effect.fnUntraced(function* ( return packageManifest; }); +/** + * `baseDirectory` is the scaffolded project root, because a relative `file:` + * spec is written verbatim into that project's `package.json` and npm resolves + * it from there — never from this CLI's working directory. + */ const localTarballPackageManifest = Effect.fnUntraced(function* ( packageSpec: string, baseDirectory: string, diff --git a/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts b/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts index 4825ac9ea..d34221546 100644 --- a/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts +++ b/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts @@ -70,11 +70,9 @@ it('scaffolds with differently versioned release tarballs and runs after source }, 600_000); /** - * Per-PR scaffolder smoke: one template through the full consumer journey — - * installed scaffolder bin, template scaffold, scaffolder-driven npm install, - * project check, clean validate. The mcp-server and cli-tool templates run in - * the release-boundary matrix (scaffold-packed-matrix.e2e.test.ts) via - * `test:packed:release` and the nightly schedule. + * The minimal-template smoke covers the scaffolder-driven install and full + * project check. The release-boundary matrix runs every check for the routed + * templates through `test:packed:release` and the nightly schedule. */ it('scaffolds the minimal template, auto-installs, and passes its own check', async () => { // No --no-install: this run covers the scaffolder-driven `npm install` path. diff --git a/packages/create-agent-bundle/tests/support/package-tarball.ts b/packages/create-agent-bundle/tests/support/package-tarball.ts index 82d006367..07465b007 100644 --- a/packages/create-agent-bundle/tests/support/package-tarball.ts +++ b/packages/create-agent-bundle/tests/support/package-tarball.ts @@ -17,8 +17,8 @@ const applyHeaderChecksum = (header: Buffer): void => { }; /** - * The smallest archive `localTarballPackageName` accepts: one ustar entry for - * `package/package.json` naming the package, then the end-of-archive blocks. + * The smallest archive `localTarballPackageManifest` accepts: one ustar entry + * for `package/package.json`, then the end-of-archive blocks. */ export const packageTarArchive = (name: string, version = '0.0.0'): Buffer => { const manifest = Buffer.from(JSON.stringify({ name, version })); From 5f348600175e56723990b09e56e12c10ef87784b Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 7 Sep 2026 07:51:38 +0000 Subject: [PATCH 3/4] fix(create): preserve preview pairing semantics --- docs/preview-packages.md | 12 ++++----- packages/create-agent-bundle/README.md | 3 +++ packages/create-agent-bundle/src/framework.ts | 15 ++++++++++- .../create-agent-bundle/src/scaffold-cli.ts | 12 ++------- .../tests/framework.test.ts | 27 +++++++++++++++++++ .../tests/scaffold-packed.e2e.test.ts | 15 +++++++++-- .../tests/support/scaffold-fixture.ts | 15 +++++++++++ 7 files changed, 80 insertions(+), 19 deletions(-) diff --git a/docs/preview-packages.md b/docs/preview-packages.md index 916977d1a..2eee62dbf 100644 --- a/docs/preview-packages.md +++ b/docs/preview-packages.md @@ -65,12 +65,12 @@ pnpm 11 additionally needs the `blockExoticSubdeps` setting described below. Previews carry the version string `0.0.0-preview-`, and the publish (`--peerDeps`) rewrites every peer range that points at a sibling workspace -package to that exact preview version inside the preview tarballs. Today that -is the optional `@agent-bundle/runtime` peer declared by `agent-bundle` -(`@agent-bundle/runtime` itself no longer declares an `agent-bundle` peer; -its peers are `react`, `react-dom`, and the optional `@rspack/core`). A regular -`dependencies` entry that names a sibling workspace package is rewritten to -that sibling's same-sha tarball URL: `@agent-bundle/runtime`'s +package to that exact preview version inside the preview tarballs. Those peers +are the optional `@agent-bundle/runtime` peer declared by `agent-bundle` and +the optional compiler/runtime release-pair record declared by +`create-agent-bundle` (`@agent-bundle/runtime` itself has no `agent-bundle` +peer). A regular `dependencies` entry that names a sibling workspace package +is rewritten to that sibling's same-sha tarball URL: `@agent-bundle/runtime`'s `rsc-markdown-stream` dependency resolves to the renderer preview of the same commit. Installing both packages from the same sha therefore works with stock npm — no `--legacy-peer-deps` needed. Mixing two different shas fails with `ERESOLVE` diff --git a/packages/create-agent-bundle/README.md b/packages/create-agent-bundle/README.md index af410462d..8218ec832 100644 --- a/packages/create-agent-bundle/README.md +++ b/packages/create-agent-bundle/README.md @@ -68,6 +68,9 @@ from the compiler version — and rejects a runtime-bearing scaffold whose `--framework-version` does not match the recorded compiler. A local compiler tarball selects the sibling runtime tarball with the recorded runtime version and validates both package names and versions before writing the project. +Until this project owns the npm package names, use the preview command above; +a locally packed release record does not make the unrelated registry package +safe to install. ## License diff --git a/packages/create-agent-bundle/src/framework.ts b/packages/create-agent-bundle/src/framework.ts index f94038a9b..b8f49bd46 100644 --- a/packages/create-agent-bundle/src/framework.ts +++ b/packages/create-agent-bundle/src/framework.ts @@ -18,6 +18,18 @@ export interface FrameworkRuntimePairing { readonly runtime: string; } +export const runtimePairingFromManifest = (manifest: { + readonly peerDependencies?: Readonly>; + readonly version: string; +}): FrameworkRuntimePairing | undefined => { + if (previewPattern.test(manifest.version)) return undefined; + const framework = manifest.peerDependencies?.['agent-bundle']; + const runtime = manifest.peerDependencies?.['@agent-bundle/runtime']; + if (typeof framework !== 'string' || typeof runtime !== 'string') return undefined; + if (framework.startsWith('workspace:') || runtime.startsWith('workspace:')) return undefined; + return { framework, runtime }; +}; + export const previewPackageSpec = (packageName: PreviewPackageName, sha: string): string => `https://pkg.pr.new/ScriptedAlchemy/agent-bundle/${packageName}@${sha}`; @@ -35,13 +47,14 @@ export const runtimeSpecForFramework = ( if (preview !== null) return `${preview[1]}@agent-bundle/runtime@${preview[2]}`; const localTarball = /^(file:(?:.*[/\\])?)agent-bundle(-[^/\\]+)?\.tgz$/u.exec(frameworkSpec); if (localTarball !== null) { + if (localTarball[2] === undefined) return `${localTarball[1]}agent-bundle-runtime.tgz`; if (pairing === undefined) { throw new UsageError( `Cannot select @agent-bundle/runtime for agent-bundle spec "${frameworkSpec}": ` + 'this create-agent-bundle package has no release pairing metadata.', ); } - return `${localTarball[1]}agent-bundle-runtime${localTarball[2] === undefined ? '' : `-${pairing.runtime}`}.tgz`; + return `${localTarball[1]}agent-bundle-runtime-${pairing.runtime}.tgz`; } if ( frameworkSpec !== '' diff --git a/packages/create-agent-bundle/src/scaffold-cli.ts b/packages/create-agent-bundle/src/scaffold-cli.ts index ec2df864b..46ad3ba8a 100644 --- a/packages/create-agent-bundle/src/scaffold-cli.ts +++ b/packages/create-agent-bundle/src/scaffold-cli.ts @@ -9,7 +9,7 @@ import type { PlatformError } from 'effect/PlatformError'; import { mapCause, runPromise } from './effect/boundary.ts'; import { liftPromise, liftTry } from './effect/lift.ts'; -import { type FrameworkRuntimePairing, resolveFrameworkSpec } from './framework.ts'; +import { resolveFrameworkSpec, runtimePairingFromManifest } from './framework.ts'; import { UsageError, resolveOptions, @@ -80,14 +80,6 @@ const ownManifest = Effect.gen(function* () { return JSON.parse(yield* fs.readFileString(manifestPath)) as OwnManifest; }); -const releasePairing = (manifest: OwnManifest): FrameworkRuntimePairing | undefined => { - const framework = manifest.peerDependencies?.['agent-bundle']; - const runtime = manifest.peerDependencies?.['@agent-bundle/runtime']; - if (typeof framework !== 'string' || typeof runtime !== 'string') return undefined; - if (framework.startsWith('workspace:') || runtime.startsWith('workspace:')) return undefined; - return { framework, runtime }; -}; - const runInstall = (options: ResolvedOptions, targetDirectory: string): Effect.Effect => liftPromise(() => { log.step(`Installing dependencies with ${options.packageManager}...`); @@ -116,7 +108,7 @@ const scaffoldProgram = Effect.fnUntraced(function* ( // Reading this package's own manifest fails before the intro, exactly as // it did as a rejected Promise: no cancel banner, the error leaves runCli. const manifest = yield* ownManifest; - const pairing = releasePairing(manifest); + const pairing = runtimePairingFromManifest(manifest); intro(`create-agent-bundle ${manifest.version}`); const run = Effect.gen(function* () { const path = yield* Path.Path; diff --git a/packages/create-agent-bundle/tests/framework.test.ts b/packages/create-agent-bundle/tests/framework.test.ts index 73dfc6890..af89163bd 100644 --- a/packages/create-agent-bundle/tests/framework.test.ts +++ b/packages/create-agent-bundle/tests/framework.test.ts @@ -10,6 +10,7 @@ import { assertLocalFrameworkTarball, previewPackageSpec, resolveFrameworkSpec, + runtimePairingFromManifest, runtimeSpecForFramework, validatedRuntimeSpecForFramework, } from '../src/framework.ts'; @@ -38,6 +39,30 @@ const expectUsageError = (error: unknown, message?: string): void => { const releasePairing = { framework: '0.2.0', runtime: '0.1.0' } as const; +it('reads release pairing metadata but ignores source and preview peer rewrites', () => { + expect(runtimePairingFromManifest({ + peerDependencies: { + '@agent-bundle/runtime': '0.1.0', + 'agent-bundle': '0.2.0', + }, + version: '0.1.0', + })).toEqual(releasePairing); + expect(runtimePairingFromManifest({ + peerDependencies: { + '@agent-bundle/runtime': '0.0.0-preview-da5df1d', + 'agent-bundle': '0.0.0-preview-da5df1d', + }, + version: '0.0.0-preview-da5df1d', + })).toBeUndefined(); + expect(runtimePairingFromManifest({ + peerDependencies: { + '@agent-bundle/runtime': 'workspace:*', + 'agent-bundle': 'workspace:*', + }, + version: '0.0.0', + })).toBeUndefined(); +}); + it('declares the compiler/runtime release pair as optional workspace peers', async () => { const manifest = JSON.parse( await readFile(join(process.cwd(), 'packages/create-agent-bundle/package.json'), 'utf8'), @@ -97,6 +122,8 @@ describe('runtimeSpecForFramework', () => { .toBe('file:/tmp/agent-bundle-runtime-0.1.0.tgz'); expect(runtimeSpecForFramework('file:/tmp/agent-bundle.tgz', releasePairing)) .toBe('file:/tmp/agent-bundle-runtime.tgz'); + expect(runtimeSpecForFramework('file:/tmp/agent-bundle.tgz')) + .toBe('file:/tmp/agent-bundle-runtime.tgz'); }); it('selects the recorded runtime version for the paired registry compiler', () => { diff --git a/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts b/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts index d34221546..d507fdbc6 100644 --- a/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts +++ b/packages/create-agent-bundle/tests/scaffold-packed.e2e.test.ts @@ -11,6 +11,7 @@ import { installScaffoldedProject, npmRun, scaffoldProject, + scaffoldProjectFromReleasePairing, scaffoldProjectWithMismatchedRuntime, scaffoldReleasePairing, } from './support/scaffold-fixture.ts'; @@ -28,10 +29,20 @@ it('rejects a local framework tarball paired with an incompatible runtime versio }); }, 600_000); -it('scaffolds with differently versioned release tarballs and runs after source deletion', async () => { +it('uses the packed release pair when no framework override is passed', async () => { const pairing = await scaffoldReleasePairing(); - expect(pairing.framework).not.toBe(pairing.runtime); + const projectRoot = await scaffoldProjectFromReleasePairing('mcp-server', 'registry-pair-project'); + const manifest = JSON.parse(await readFile(join(projectRoot, 'package.json'), 'utf8')) as { + readonly devDependencies: Record; + }; + expect(manifest.devDependencies).toMatchObject({ + '@agent-bundle/runtime': pairing.runtime, + 'agent-bundle': pairing.framework, + }); +}, 600_000); +it('scaffolds with independently versioned release tarballs and runs after source deletion', async () => { + const pairing = await scaffoldReleasePairing(); const projectRoot = await scaffoldProject('mcp-server', 'paired-runtime-project', ['--no-install']); const manifest = JSON.parse(await readFile(join(projectRoot, 'package.json'), 'utf8')) as { readonly devDependencies: Record; diff --git a/packages/create-agent-bundle/tests/support/scaffold-fixture.ts b/packages/create-agent-bundle/tests/support/scaffold-fixture.ts index b23fcc083..4f514b393 100644 --- a/packages/create-agent-bundle/tests/support/scaffold-fixture.ts +++ b/packages/create-agent-bundle/tests/support/scaffold-fixture.ts @@ -120,6 +120,21 @@ export const scaffoldProject = async ( return join(runnerRoot, projectName); }; +export const scaffoldProjectFromReleasePairing = async ( + template: string, + projectName: string, +): Promise => { + const { runnerRoot, scaffolderBin } = await fixture(); + await execFile(scaffolderBin, [ + projectName, + '--template', template, + '--targets', 'portable', + '--package-manager', 'npm', + '--no-install', + ], { cwd: runnerRoot, env: installedEnvironment() }); + return join(runnerRoot, projectName); +}; + export const scaffoldReleasePairing = async (): Promise Date: Mon, 7 Sep 2026 07:55:32 +0000 Subject: [PATCH 4/4] docs(create): clarify preview pairing overrides --- packages/create-agent-bundle/README.md | 22 ++++++++++++------- packages/create-agent-bundle/src/framework.ts | 21 ++++++++---------- .../tests/framework.test.ts | 4 +++- website/docs/en/guide/start/installation.mdx | 5 +++++ website/docs/zh/guide/start/installation.mdx | 4 ++++ 5 files changed, 35 insertions(+), 21 deletions(-) diff --git a/packages/create-agent-bundle/README.md b/packages/create-agent-bundle/README.md index 8218ec832..4db30385c 100644 --- a/packages/create-agent-bundle/README.md +++ b/packages/create-agent-bundle/README.md @@ -60,14 +60,20 @@ anything; its README documents the wiring to add with the first route. ## The framework dependency Preview scaffolders pin `agent-bundle` and `@agent-bundle/runtime` to exact -[pkg.pr.new](https://pkg.pr.new) tarballs from one commit SHA. An npm release -instead records its compatible compiler and runtime versions as optional -peers in the packed `create-agent-bundle` manifest. The scaffolder pins those -two recorded versions independently — it never derives the runtime version -from the compiler version — and rejects a runtime-bearing scaffold whose -`--framework-version` does not match the recorded compiler. A local compiler -tarball selects the sibling runtime tarball with the recorded runtime version -and validates both package names and versions before writing the project. +[pkg.pr.new](https://pkg.pr.new) tarballs from one commit SHA. A runtime +template overridden from a preview scaffolder accepts another exact +pkg.pr.new URL or the unversioned local names `agent-bundle.tgz` and +`agent-bundle-runtime.tgz`; versioned registry or local overrides require the +matching npm scaffolder release. + +An npm release records its compatible compiler and runtime versions as +optional peers in the packed `create-agent-bundle` manifest. The scaffolder +pins those two recorded versions independently — it never derives the runtime +version from the compiler version — and rejects a runtime-bearing scaffold +whose `--framework-version` does not match the recorded compiler. A local +compiler tarball selects the sibling runtime tarball with the recorded runtime +version and validates both package names and versions before writing the +project. Until this project owns the npm package names, use the preview command above; a locally packed release record does not make the unrelated registry package safe to install. diff --git a/packages/create-agent-bundle/src/framework.ts b/packages/create-agent-bundle/src/framework.ts index b8f49bd46..c297a0bd7 100644 --- a/packages/create-agent-bundle/src/framework.ts +++ b/packages/create-agent-bundle/src/framework.ts @@ -35,6 +35,13 @@ export const previewPackageSpec = (packageName: PreviewPackageName, sha: string) export const previewFrameworkSpec = (sha: string): string => previewPackageSpec('agent-bundle', sha); +const missingPairingError = (frameworkSpec: string): UsageError => new UsageError( + `Cannot select @agent-bundle/runtime for agent-bundle spec "${frameworkSpec}": ` + + 'this create-agent-bundle package has no release pairing metadata. Use a same-SHA pkg.pr.new URL, ' + + 'the unversioned local pair agent-bundle.tgz and agent-bundle-runtime.tgz, or a create-agent-bundle ' + + 'release paired with that registry compiler.', +); + /** * Exact previews pair by commit. Released scaffolders carry the compiler and * runtime versions their packed manifest selected from the workspace. @@ -48,12 +55,7 @@ export const runtimeSpecForFramework = ( const localTarball = /^(file:(?:.*[/\\])?)agent-bundle(-[^/\\]+)?\.tgz$/u.exec(frameworkSpec); if (localTarball !== null) { if (localTarball[2] === undefined) return `${localTarball[1]}agent-bundle-runtime.tgz`; - if (pairing === undefined) { - throw new UsageError( - `Cannot select @agent-bundle/runtime for agent-bundle spec "${frameworkSpec}": ` - + 'this create-agent-bundle package has no release pairing metadata.', - ); - } + if (pairing === undefined) throw missingPairingError(frameworkSpec); return `${localTarball[1]}agent-bundle-runtime-${pairing.runtime}.tgz`; } if ( @@ -63,12 +65,7 @@ export const runtimeSpecForFramework = ( && !frameworkSpec.endsWith('.tgz') && !frameworkSpec.endsWith('.tar.gz') ) { - if (pairing === undefined) { - throw new UsageError( - `Cannot select @agent-bundle/runtime for agent-bundle spec "${frameworkSpec}": ` - + 'this create-agent-bundle package has no release pairing metadata.', - ); - } + if (pairing === undefined) throw missingPairingError(frameworkSpec); if (frameworkSpec !== pairing.framework) { throw new UsageError( `This create-agent-bundle release is paired with agent-bundle ${pairing.framework} ` diff --git a/packages/create-agent-bundle/tests/framework.test.ts b/packages/create-agent-bundle/tests/framework.test.ts index af89163bd..597554045 100644 --- a/packages/create-agent-bundle/tests/framework.test.ts +++ b/packages/create-agent-bundle/tests/framework.test.ts @@ -138,7 +138,9 @@ describe('runtimeSpecForFramework', () => { it('never copies a registry compiler selector when pairing metadata is absent', () => { expect(() => runtimeSpecForFramework('0.2.0')).toThrow(UsageError); - expect(() => runtimeSpecForFramework('0.2.0')).toThrow('pairing metadata'); + expect(() => runtimeSpecForFramework('0.2.0')).toThrow('same-SHA pkg.pr.new URL'); + expect(() => runtimeSpecForFramework('file:/tmp/agent-bundle-0.2.0.tgz')) + .toThrow('agent-bundle.tgz and agent-bundle-runtime.tgz'); }); it('rejects package specs that cannot resolve independently under the runtime name', () => { diff --git a/website/docs/en/guide/start/installation.mdx b/website/docs/en/guide/start/installation.mdx index e6f479a86..be8c173f2 100644 --- a/website/docs/en/guide/start/installation.mdx +++ b/website/docs/en/guide/start/installation.mdx @@ -44,6 +44,11 @@ preview version of the runtime built from the same commit, so a matched pair ins stock npm. Mixing two different SHAs fails with `ERESOLVE` by design — the range to inspect is the one in the installed `agent-bundle` package. +A preview scaffolder has no npm release-pair record. For a runtime template, an explicit +`--framework-version` must therefore be an exact pkg.pr.new compiler URL or the local +`agent-bundle.tgz` beside `agent-bundle-runtime.tgz`. Versioned registry or local tarball +overrides require the matching released scaffolder. + pnpm and yarn accept the same URLs (`pnpm add `, `yarn add agent-bundle@`). Preview versions are spelled `0.0.0-preview-`. They are built from the same output the diff --git a/website/docs/zh/guide/start/installation.mdx b/website/docs/zh/guide/start/installation.mdx index 959d0b5cf..5b2bfb91e 100644 --- a/website/docs/zh/guide/start/installation.mdx +++ b/website/docs/zh/guide/start/installation.mdx @@ -39,6 +39,10 @@ npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@5685 内部的这个 peer 范围改写为同一提交构建出的 runtime 精确预览版本,因此配对安装用原生 npm 即可完成。 混用两个不同的 SHA 会按设计以 `ERESOLVE` 失败——需要检查的范围位于已安装的 `agent-bundle` 包中。 +预览脚手架没有 npm 发布配对记录。因此,对需要 runtime 的模板显式传入 `--framework-version` 时,必须使用 +精确的 pkg.pr.new compiler URL,或使用相邻的本地 `agent-bundle.tgz` 与 +`agent-bundle-runtime.tgz`。带版本号的 registry 或本地 tarball 覆盖需要匹配的正式发布脚手架。 + pnpm 与 yarn 接受同样的 URL(`pnpm add `、`yarn add agent-bundle@`)。 预览版本号形如 `0.0.0-preview-`。它们由发布门禁校验的同一份产物构建而来,但并不是 npm 正式版本。