From a8a4fc5c9d242d9493f45c39271d57cdfe29a490 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 2 Sep 2026 06:10:10 +0000 Subject: [PATCH] fix(package): keep the generated installer's artifact URL statically analyzable in packed consumers (#252) --- .changeset/static-packed-installer-url.md | 5 +++++ packages/agent-bundle/src/build/entry-shell.ts | 3 ++- packages/agent-bundle/src/install-entry.ts | 10 ++++------ packages/agent-bundle/tests/packed-consumer.test.ts | 4 +++- 4 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 .changeset/static-packed-installer-url.md diff --git a/.changeset/static-packed-installer-url.md b/.changeset/static-packed-installer-url.md new file mode 100644 index 000000000..35e93c21e --- /dev/null +++ b/.changeset/static-packed-installer-url.md @@ -0,0 +1,5 @@ +--- +'agent-bundle': patch +--- + +Keep generated package-relative installer paths statically analyzable when plugin projects build through a packed `agent-bundle` dependency. diff --git a/packages/agent-bundle/src/build/entry-shell.ts b/packages/agent-bundle/src/build/entry-shell.ts index 80d474760..f0ed7aa9a 100644 --- a/packages/agent-bundle/src/build/entry-shell.ts +++ b/packages/agent-bundle/src/build/entry-shell.ts @@ -117,10 +117,11 @@ export const generatedInstallBinEntrySource = (options: { readonly hosts: readonly ('claude' | 'codex' | 'cursor')[]; readonly name: string; }): string => [ + "import { fileURLToPath } from 'node:url';", `import { runGeneratedInstallProcess } from ${JSON.stringify(installEntryRuntimeSpecifier)};`, '', 'process.exitCode = await runGeneratedInstallProcess(process.argv.slice(2), Object.freeze({', - ` artifactRelativeUrl: ${JSON.stringify(options.artifactRelativeUrl)},`, + ` artifactRoot: fileURLToPath(new URL(${JSON.stringify(options.artifactRelativeUrl)}, import.meta.url)),`, ` hosts: Object.freeze(${stableJson(options.hosts)}),`, ` name: ${JSON.stringify(options.name)},`, '}));', diff --git a/packages/agent-bundle/src/install-entry.ts b/packages/agent-bundle/src/install-entry.ts index 50911c3c2..d33c2d6c8 100644 --- a/packages/agent-bundle/src/install-entry.ts +++ b/packages/agent-bundle/src/install-entry.ts @@ -1,5 +1,4 @@ import { lstat } from 'node:fs/promises'; -import { fileURLToPath } from 'node:url'; import { stableJson } from './core/digest.ts'; import { DiagnosticError, type Diagnostic } from './core/diagnostics.ts'; @@ -11,7 +10,7 @@ import { } from './install/install.ts'; export interface GeneratedInstallProcessOptions { - readonly artifactRelativeUrl: string; + readonly artifactRoot: string; readonly hosts: readonly InstallHost[]; readonly name: string; } @@ -98,16 +97,15 @@ export const runGeneratedInstallProcess = async ( let parsed: ParsedInstallArguments | undefined; try { parsed = parseArguments(argv, options); - const artifactRoot = fileURLToPath(new URL(options.artifactRelativeUrl, import.meta.url)); - const metadata = await lstat(artifactRoot).catch(() => undefined); + const metadata = await lstat(options.artifactRoot).catch(() => undefined); if (metadata === undefined || !metadata.isDirectory()) { throw new Error( - `Package artifact root is missing at ${JSON.stringify(artifactRoot)}; ` + + `Package artifact root is missing at ${JSON.stringify(options.artifactRoot)}; ` + 'the package must ship its generated artifact directory.', ); } const result = await installBundle({ - from: artifactRoot, + from: options.artifactRoot, host: parsed.host, scope: parsed.scope, }); diff --git a/packages/agent-bundle/tests/packed-consumer.test.ts b/packages/agent-bundle/tests/packed-consumer.test.ts index aeb11b0a1..07e573b61 100644 --- a/packages/agent-bundle/tests/packed-consumer.test.ts +++ b/packages/agent-bundle/tests/packed-consumer.test.ts @@ -327,7 +327,7 @@ it('uses only an installed tarball after source deletion', async () => { 'export default {', ' mcp: { servers: { greeter: {} } },', " plugin: { name: 'framework-build-fixture', version: '1.0.0' },", - " targets: ['portable'],", + " targets: ['claude', 'codex', 'portable'],", '};', '', ].join('\n')), @@ -371,7 +371,9 @@ it('uses only an installed tarball after source deletion', async () => { await runInstalled(frameworkCli, frameworkRoot, ['build', '--root', frameworkRoot, '--output', frameworkArtifact]); const packedBin = join(frameworkRoot, 'dist', 'bin', 'framework-build-fixture.js'); + const packedInstallerBin = join(frameworkRoot, 'dist', 'bin', 'framework-build-fixture-install.js'); expect((await stat(packedBin)).mode & 0o111).not.toBe(0); + expect((await stat(packedInstallerBin)).mode & 0o111).not.toBe(0); expect((await readFile(packedBin, 'utf8')).startsWith('#!/usr/bin/env node\n')).toBe(true); await expect(execFile(packedBin, ['alpha'], { cwd: frameworkRoot, env: installedEnvironment() })) .resolves.toMatchObject({ stdout: 'packed bin ran:alpha\n' });