Skip to content

feat(package): package-relative host installer bins and the agent-bundle prepack inventory gate (#252) - #281

Merged
ScriptedAlchemy merged 1 commit into
mainfrom
feat/252-npm-host-packs
Sep 2, 2026
Merged

feat(package): package-relative host installer bins and the agent-bundle prepack inventory gate (#252)#281
ScriptedAlchemy merged 1 commit into
mainfrom
feat/252-npm-host-packs

Conversation

@ScriptedAlchemy

Copy link
Copy Markdown
Owner

Summary

First slice of #252 — the npm distribution contract for generated host packs:

  • Generated package-relative installer bin. When package outputs are built alongside at least one Claude/Codex/Cursor host pack (the plugin target implies all three; portable-only emits nothing), the package build synthesizes one additional self-contained executable, dist/bin/<plugin-name>.js when that name is free, otherwise dist/bin/<plugin-name>-install.js. Grammar: install <host> [--scope <scope>] [--json]; help lists only built hosts. The bin resolves the shipped artifact directory from import.meta.url (a build-time-baked relative URL — never process.cwd()) and delegates to the exact installBundle implementation behind agent-bundle install, bundled in via a new install-entry runtime shell aliased like the routed-CLI shell. npm lifecycle never mutates hosts; installation is the explicit documented command (npx <plugin> install <host> when the plugin name is the bin).
  • agent-bundle prepack — the packed-tarball inventory gate: runs the release build (with feat(agent-bundle): optional plugin.version with fail-closed releases and an agent-bundle/meta build-time constant #270's fail-closed version semantics), then npm pack --dry-run --json --ignore-scripts (no prepack recursion), parsed by the one shared packOutputFromJson (promoted from tests/support/shared-pack.ts into src/build/pack-inventory.ts; the test harness now imports it from src). Diagnostics: AB7010 missing packed files (dist outputs, artifact manifest + every manifest file, per-target INSTALL.md/install.mjs surfaces, README), AB7011 artifact files stale against manifest SHA-256s, AB7012 package.json bins escaping the packed dist output (e.g. src/ paths), AB7013 package.json / normalized plugin / host-manifest / artifact-provenance version disagreement.
  • installBundle additionally resolves the plugin target directory (all three host manifests in one bundle dir), matching the pack gate's inventory.
  • Docs (entry-conventions, diagnostics, README) + minor changeset.

Proof

prepack.test.ts ends with the real consumer journey: npm pack the fixture, npm install the exact tarball into an empty consumer, delete the source project, run the installed node_modules/.bin/<plugin> installer for Cursor into an isolated HOME, and observe registration from package-local assets only. installer-entry.test.ts pins naming fallback, portable exclusion, built-host argv validation, missing-artifact errors, and executes emitted bins for cursor/plugin targets.

The full empty-dir proof matrix at the host-install level (real claude/codex binaries, binary-gated) plus scaffold adoption and release wiring land in the follow-up slices of #252.

Test plan

  • pnpm build, pnpm typecheck, pnpm lint (all green on the tree rebased over 9a43616a5)
  • pnpm lint:package (publint) green
  • Scoped unit: install.test.ts + install-surface.test.ts — 20/20
  • Scoped integration: installer-entry, prepack, package-build, cli, host-install-proof — 36 passed / 1 failed
  • Known pre-existing failure (NOT this PR): cli.test.ts "runs MCP and hook operations from a packed consumer" fails identically on clean origin/main (9a43616a5) in this environment — dist/api.js statically imports @agent-bundle/runtime/state while the runtime is an optional peer the packed consumer never installs. Reproduced before and after this diff; needs its own fix in the routed-CLI/state lane.

…dle prepack inventory gate

Generated package builds now emit one self-contained installer bin that
resolves the shipped artifact directory from import.meta.url and delegates
to installBundle; agent-bundle prepack runs the release build plus an
npm pack --dry-run inventory gate (AB7010-AB7013) sharing one
packOutputFromJson with the test harness.
@changeset-bot

changeset-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 835b241

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agent-bundle Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T05:19:41.947313Z 835b241 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ScriptedAlchemy
ScriptedAlchemy merged commit 2655ca5 into main Sep 2, 2026
4 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 835b241a2a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const execFile = promisify(executeFile);

export const prepack = async (options: BuildOptions): Promise<PrepackResult> => {
const result = await build({ ...options, packageOutputs: true });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Choose a non-overlapping default for prepack

When agent-bundle prepack is invoked without --output, this forwards no output override, so build() selects dist; every project eligible for prepack also has a package build whose output is dist, causing the existing overlap check to throw AB4706 before packing. This makes the documented bare command and typical "prepack": "agent-bundle prepack" lifecycle unusable unless users discover and supply another output path, so prepack should provide a non-overlapping artifact default or make the option required.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5317ce619: package-output builds now use artifact/ as their non-overlapping fallback while ProjectService still honors an explicitly configured output.distPath; the documented bare agent-bundle prepack lifecycle is covered by regression tests and docs. Merged via #319.

Comment on lines +192 to +194
const name = packageBuild.bins.some((bin) => bin.name === model.metadata.name)
? `${model.metadata.name}-install`
: model.metadata.name;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject a second installer-name collision

When plugin foo already declares bins named both foo and foo-install, this fallback still chooses foo-install, producing the same entry name and dist/bin/foo-install.js output as the authored bin. The package build can therefore fail on duplicate entries or replace the configured executable instead of adding an installer; check both candidate names and report a diagnostic or choose a guaranteed-free name.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5317ce619: installer planning now checks the full authored-bin name set and advances through -install-2, -install-3, etc. until it finds a guaranteed-free output. The foo + foo-install collision has an integration regression. Merged via #319.

...manifest.targets.flatMap((target) =>
installSurfaceRequirements(target.name).map((path) => `${artifactPrefix}/${target.name}/${path}`)),
]);
if (await exists(join(projectRoot, 'README.md'))) expected.add('README.md');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require the README even when it is absent

If a publishable project has no README.md, this conditional omits it from the expected inventory, allowing prepack to succeed without the README that AB7010 and the documented inventory gate say is required. Add README.md unconditionally so a missing source README is diagnosed just like one omitted from the packed tarball.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 5317ce619: README.md is now unconditionally part of the expected packed inventory, so a missing source README produces AB7010. The stricter gate and packed fixtures are covered by regression tests. Merged via #319.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant