diff --git a/AGENTS.md b/AGENTS.md
index a05fab61..4f1d1ccb 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -194,9 +194,11 @@ Create provider file exporting:
```bash
npm run release:local -- --out /tmp/pi-local-release --force
cd /tmp
- /tmp/pi-local-release/bin/pi --help
- /tmp/pi-local-release/bin/pi --version
- /tmp/pi-local-release/bin/pi
+ /tmp/pi-local-release/node/pi --help
+ /tmp/pi-local-release/node/pi --version
+ /tmp/pi-local-release/node/pi
+ /tmp/pi-local-release/bun/pi --help
+ /tmp/pi-local-release/bun/pi --version
```
In the interactive smoke test, verify startup, model/account listing, and at least one real prompt with the intended default provider. Treat failures as release blockers unless the user explicitly accepts the risk.
diff --git a/README.md b/README.md
index d7e73cf0..750b3a61 100644
--- a/README.md
+++ b/README.md
@@ -79,7 +79,7 @@ We treat npm dependency changes as reviewed code changes.
- `package-lock.json` is the dependency ground truth. Pre-commit blocks accidental lockfile commits unless `PI_ALLOW_LOCKFILE_CHANGE=1` is set.
- `npm run check` verifies pinned direct deps, native TypeScript import compatibility, and the generated coding-agent shrinkwrap.
- The published CLI package includes `packages/coding-agent/npm-shrinkwrap.json`, generated from the root lockfile, to pin transitive deps for npm users.
-- Release smoke tests use `npm run release:local` to build, pack, and install outside the repo before publishing.
+- Release smoke tests use `npm run release:local` to build, pack, and create isolated npm and Bun installs outside the repo before publishing.
- Local release installs, documented npm installs, and `pi update --self` use `--ignore-scripts` where supported.
- CI installs with `npm ci --ignore-scripts`, and a scheduled GitHub workflow runs `npm audit --omit=dev` plus `npm audit signatures --omit=dev`.
- Shrinkwrap generation has an explicit allowlist for dependency lifecycle scripts; new lifecycle-script deps fail checks until reviewed.
diff --git a/scripts/local-release.mjs b/scripts/local-release.mjs
index d5b4b416..5cdc67bc 100644
--- a/scripts/local-release.mjs
+++ b/scripts/local-release.mjs
@@ -19,16 +19,17 @@ Builds and packs the publishable packages, then installs the tarballs into an
isolated directory outside the repository for local release testing.
Options:
- --out
Output directory. Defaults to a new directory under ${tmpdir()}
- --force Remove --out first if it already exists
- --skip-check Do not run npm run check before building
- --skip-install Only create tarballs; do not create the isolated install
- --help Show this help
+ --out Output directory. Defaults to a new directory under ${tmpdir()}
+ --force Remove --out first if it already exists
+ --skip-check Do not run npm run check before building
+ --skip-install Only create tarballs; do not create isolated installs
+ --skip-bun-install Do not create the isolated Bun install
+ --help Show this help
`);
}
function parseArgs() {
- const options = { force: false, outDir: undefined, skipCheck: false, skipInstall: false };
+ const options = { force: false, outDir: undefined, skipBunInstall: false, skipCheck: false, skipInstall: false };
const args = process.argv.slice(2);
for (let i = 0; i < args.length; i++) {
@@ -49,6 +50,10 @@ function parseArgs() {
options.skipInstall = true;
continue;
}
+ if (arg === "--skip-bun-install") {
+ options.skipBunInstall = true;
+ continue;
+ }
if (arg === "--out") {
const value = args[++i];
if (!value) {
@@ -82,6 +87,10 @@ function readPackageJson(directory) {
return JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
}
+function commandExists(command) {
+ return spawnSync(command, ["--version"], { stdio: "ignore" }).status === 0;
+}
+
function isInsidePath(child, parent) {
const relativePath = relative(parent, child);
return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
@@ -138,8 +147,8 @@ if (rootPackageJson.name !== "pi-monorepo") {
const outDir = prepareOutputDirectory(options, repoRoot);
const tarballDirectory = join(outDir, "tarballs");
-const installDirectory = join(outDir, "install");
-const binDirectory = join(outDir, "bin");
+const nodeInstallDirectory = join(outDir, "node");
+const bunInstallDirectory = join(outDir, "bun");
mkdirSync(tarballDirectory, { recursive: true });
if (!options.skipCheck) {
@@ -158,18 +167,28 @@ for (const pkg of packages) {
}
if (!options.skipInstall) {
- mkdirSync(installDirectory, { recursive: true });
+ mkdirSync(nodeInstallDirectory, { recursive: true });
const dependencies = Object.fromEntries(
- packages.map((pkg) => [pkg.name, fileSpecifier(installDirectory, tarballs.get(pkg.name))]),
- );
- writeFileSync(
- join(installDirectory, "package.json"),
- `${JSON.stringify({ private: true, dependencies }, undefined, "\t")}\n`,
+ packages.map((pkg) => [pkg.name, fileSpecifier(nodeInstallDirectory, tarballs.get(pkg.name))]),
);
+ const installPackageJson = `${JSON.stringify({ private: true, dependencies }, undefined, "\t")}\n`;
+ writeFileSync(join(nodeInstallDirectory, "package.json"), installPackageJson);
- run("npm", ["install", "--omit=dev", "--ignore-scripts"], { cwd: installDirectory });
- mkdirSync(binDirectory, { recursive: true });
- symlinkSync(join(installDirectory, "node_modules", ".bin", "pi"), join(binDirectory, "pi"));
+ run("npm", ["install", "--omit=dev", "--ignore-scripts"], { cwd: nodeInstallDirectory });
+ symlinkSync(join("node_modules", ".bin", "pi"), join(nodeInstallDirectory, "pi"));
+
+ if (!options.skipBunInstall) {
+ if (!commandExists("bun")) {
+ throw new Error("Bun is required for the isolated Bun install. Use --skip-bun-install to skip it.");
+ }
+ mkdirSync(bunInstallDirectory, { recursive: true });
+ const bunDependencies = Object.fromEntries(
+ packages.map((pkg) => [pkg.name, fileSpecifier(bunInstallDirectory, tarballs.get(pkg.name))]),
+ );
+ writeFileSync(join(bunInstallDirectory, "package.json"), `${JSON.stringify({ private: true, dependencies: bunDependencies }, undefined, "\t")}\n`);
+ run("bun", ["install", "--production", "--ignore-scripts"], { cwd: bunInstallDirectory });
+ symlinkSync(join("node_modules", ".bin", "pi"), join(bunInstallDirectory, "pi"));
+ }
}
console.log("\nLocal release artifacts created:");
@@ -180,8 +199,15 @@ for (const tarball of tarballs.values()) {
}
if (!options.skipInstall) {
- console.log("\nIsolated install:");
- console.log(` ${installDirectory}`);
- console.log("\nRun the locally packed CLI from outside the repository:");
- console.log(` ${join(binDirectory, "pi")} --help`);
+ console.log("\nIsolated npm install:");
+ console.log(` ${nodeInstallDirectory}`);
+ console.log("\nRun the locally packed npm CLI from outside the repository:");
+ console.log(` ${join(nodeInstallDirectory, "pi")} --help`);
+
+ if (!options.skipBunInstall) {
+ console.log("\nIsolated Bun install:");
+ console.log(` ${bunInstallDirectory}`);
+ console.log("\nRun the locally packed Bun CLI from outside the repository:");
+ console.log(` ${join(bunInstallDirectory, "pi")} --help`);
+ }
}