8b683f8ccb
gen:bundle previously ran gen:docs and gen:docs:reset around the build. Under prepack that reset ran before bun pm pack assembled the tarball, so the published src/ copy of docs-index.generated.txt was empty and consumers importing via subpath (e.g. @oh-my-pi/pi-coding-agent/internal-urls/docs-index) fell through to the missing repo docs/ tree. Restored gen:docs in prepack so postpack owns the reset (matching the original lifecycle), and made gen:bundle assert the embed is populated and fresh instead of touching it, so a direct gen:bundle outside prepack fails with an actionable message. Fixes #3934
130 lines
4.8 KiB
TypeScript
Executable File
130 lines
4.8 KiB
TypeScript
Executable File
#!/usr/bin/env bun
|
|
|
|
import * as fs from "node:fs/promises";
|
|
import * as path from "node:path";
|
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
import { assertDocsIndexFresh, buildDocsIndexPayload } from "./generate-docs-index";
|
|
|
|
const packageDir = path.join(import.meta.dir, "..");
|
|
const outDir = path.join(packageDir, "dist");
|
|
const cliPath = path.join(outDir, "cli.js");
|
|
const shebang = "#!/usr/bin/env bun\n";
|
|
|
|
// Native / optional / platform-specific deps that are never bundled — installed on
|
|
// demand (transformers/fastembed/onnxruntime) or shipped as their own artifact
|
|
// (native addon, mupdf).
|
|
const ALWAYS_EXTERNAL = ["mupdf", "@oh-my-pi/pi-natives", "@huggingface/transformers", "fastembed", "onnxruntime-node"];
|
|
|
|
// Heavy, lazily-used third-party leaf deps. Each is a declared `dependency`, so the
|
|
// published package resolves it from node_modules at runtime; bundling only embeds a
|
|
// redundant copy that bloats dist/cli.js. NEVER add a patched dependency here — the
|
|
// bundle is where a root `patchedDependencies` patch is baked in, so an externalized
|
|
// import would load the unpatched npm package in users' installs (currently
|
|
// @ark/schema is patched, so it — and arktype, which pulls @ark/schema — stay
|
|
// bundled).
|
|
const RUNTIME_EXTERNAL = [
|
|
"puppeteer-core",
|
|
"@puppeteer/browsers",
|
|
"@babel/parser",
|
|
"@xterm/headless",
|
|
"turndown",
|
|
"turndown-plugin-gfm",
|
|
"@mozilla/readability",
|
|
"linkedom",
|
|
"@agentclientprotocol/sdk",
|
|
];
|
|
|
|
async function runCommand(command: string[]): Promise<void> {
|
|
const proc = Bun.spawn(command, {
|
|
cwd: packageDir,
|
|
stdout: "inherit",
|
|
stderr: "inherit",
|
|
});
|
|
const exitCode = await proc.exited;
|
|
if (exitCode !== 0) throw new Error(`Command failed with exit code ${exitCode}: ${command.join(" ")}`);
|
|
}
|
|
|
|
async function ensureShebang(): Promise<void> {
|
|
const text = await Bun.file(cliPath).text();
|
|
if (text.startsWith(shebang)) return;
|
|
const withoutExisting = text.startsWith("#!") ? text.slice(text.indexOf("\n") + 1) : text;
|
|
await Bun.write(cliPath, shebang + withoutExisting);
|
|
}
|
|
|
|
function formatBytes(bytes: number): string {
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
|
|
}
|
|
|
|
async function cleanBundleOutputs(): Promise<void> {
|
|
// dist/ is shared with the dev binary (dist/omp); only remove this
|
|
// script's own outputs (entry bundle + copied native assets).
|
|
let entries: string[];
|
|
try {
|
|
entries = await fs.readdir(outDir);
|
|
} catch (err) {
|
|
if (isEnoent(err)) return;
|
|
throw err;
|
|
}
|
|
await Promise.all(
|
|
entries
|
|
.filter(entry => entry === "cli.js" || entry.endsWith(".node") || entry.endsWith(".js.map"))
|
|
.map(entry => fs.rm(path.join(outDir, entry), { force: true })),
|
|
);
|
|
}
|
|
|
|
async function assertDocsEmbedPopulated(): Promise<void> {
|
|
// bundle-dist runs from prepack (which calls `gen:docs` first) or directly.
|
|
// Direct invocations must fail — the tarball ships src/, and an empty embed
|
|
// would make src/internal-urls/docs-index.ts fall through to the missing
|
|
// repo `docs/` tree at runtime in published packages (codex review, PR #3941).
|
|
const embedPath = path.join(packageDir, "src/internal-urls/docs-index.generated.txt");
|
|
const embed = await Bun.file(embedPath).text();
|
|
if (embed.length === 0) {
|
|
throw new Error(
|
|
"docs-index embed is empty. Run `bun run gen:docs` before `bun run gen:bundle`, or use `bun pm pack` which runs the prepack chain.",
|
|
);
|
|
}
|
|
const expected = await buildDocsIndexPayload();
|
|
assertDocsIndexFresh(embed, expected);
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const start = Bun.nanoseconds();
|
|
await cleanBundleOutputs();
|
|
await assertDocsEmbedPopulated();
|
|
// The npm bundle ships no stats dashboard sources, so embed the dashboard
|
|
// archive the same way compiled binaries do (scripts/build-binary.ts). Reset
|
|
// afterwards to keep the checked-in placeholder empty. The docs embed stays
|
|
// populated on disk — postpack owns its reset so `bun pm pack` can pack a
|
|
// tarball whose src copy is still valid for subpath imports.
|
|
await runCommand(["bun", "--cwd=../stats", "run", "gen:stats"]);
|
|
try {
|
|
await runCommand([
|
|
"bun",
|
|
"build",
|
|
"--target=bun",
|
|
"--outdir",
|
|
"dist",
|
|
// Full minify (whitespace + syntax + identifiers); --keep-names retains
|
|
// fn/class .name where code depends on it.
|
|
"--minify",
|
|
"--keep-names",
|
|
...[...ALWAYS_EXTERNAL, ...RUNTIME_EXTERNAL].flatMap(dep => ["--external", dep]),
|
|
"--define",
|
|
'process.env.PI_BUNDLED="true"',
|
|
"./src/cli.ts",
|
|
]);
|
|
} finally {
|
|
await runCommand(["bun", "--cwd=../stats", "run", "gen:stats:reset"]);
|
|
}
|
|
await ensureShebang();
|
|
const stat = await fs.stat(cliPath);
|
|
const elapsedMs = (Bun.nanoseconds() - start) / 1_000_000;
|
|
process.stdout.write(
|
|
`Bundled coding-agent CLI to dist/cli.js (${formatBytes(stat.size)}) in ${elapsedMs.toFixed(0)}ms\n`,
|
|
);
|
|
}
|
|
|
|
await main();
|