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`); + } }