|
| 1 | +/** |
| 2 | + * Bundle the loadout-os CLI into ONE self-contained ESM bin. |
| 3 | + * |
| 4 | + * Why this exists: the published @mcptoolshop/loadout-os must `npm install -g` |
| 5 | + * as a single package with NO `@mcptoolshop/*` runtime dependencies. The three |
| 6 | + * workspace libraries (kernel = ai-loadout, claude-memories, claude-rules) are |
| 7 | + * INLINED here so the global bin runs standalone — no node_modules resolution of |
| 8 | + * the (unpublished) workspace packages at runtime. |
| 9 | + * |
| 10 | + * Output: dist/loadout-os.js — the file named in package.json `bin` + `files`. |
| 11 | + * The modular tsc dist/ (dist/cli.js, dist/commands.js, …) is kept for the test |
| 12 | + * suite + dev, but is NOT shipped (see `files`). esbuild reads the TS source |
| 13 | + * directly (so a bundle can be built even before/independent of tsc). |
| 14 | + * |
| 15 | + * Format choice: ESM. The workspace deps are `"type": "module"` ESM, and the |
| 16 | + * source uses `import.meta.url` (getVersion / isEntrypoint). Bundling to ESM |
| 17 | + * preserves those semantics with zero shims. We keep the `#!/usr/bin/env node` |
| 18 | + * shebang via `banner` so the artifact is directly executable. |
| 19 | + * |
| 20 | + * Standards compliance (workflow-standards.md): |
| 21 | + * PIN_PER_STEP 2 — the bundle is a deterministic, replayable artifact of the |
| 22 | + * pinned source + pinned esbuild version (devDependencies) at a fixed target |
| 23 | + * (node20); no network, no nondeterministic input. |
| 24 | + * ANDON_AUTHORITY 2 — esbuild fails the build (non-zero) on any unresolved |
| 25 | + * import; a broken bundle never reaches `prepublishOnly`'s publish step. |
| 26 | + * EXTERNAL_VERIFIER 1 — the post-bundle smoke check (`--version` on the |
| 27 | + * emitted bin from a clean temp dir) is run by the operator/coordinator, not |
| 28 | + * baked in here. skip: a build script is not a multi-model pipeline. |
| 29 | + * Remaining standards (NAMED_COMPENSATORS / DECOMPOSE_BY_SECRETS / |
| 30 | + * UNCERTAINTY_GATED_HUMANS) skip: this step performs no irreversible tool |
| 31 | + * call (it only writes dist/loadout-os.js, overwritten on every run) and has |
| 32 | + * no human checkpoint. |
| 33 | + */ |
| 34 | + |
| 35 | +import { build } from "esbuild"; |
| 36 | +import { readFileSync } from "node:fs"; |
| 37 | +import { fileURLToPath } from "node:url"; |
| 38 | +import { dirname, join } from "node:path"; |
| 39 | + |
| 40 | +const here = dirname(fileURLToPath(import.meta.url)); |
| 41 | +const entry = join(here, "src", "cli.ts"); |
| 42 | +const outfile = join(here, "dist", "loadout-os.js"); |
| 43 | + |
| 44 | +await build({ |
| 45 | + entryPoints: [entry], |
| 46 | + outfile, |
| 47 | + bundle: true, |
| 48 | + platform: "node", |
| 49 | + format: "esm", |
| 50 | + target: "node20", |
| 51 | + // The executable shebang is preserved from the entry: src/cli.ts opens with |
| 52 | + // `#!/usr/bin/env node`, and esbuild keeps a leading hashbang verbatim in the |
| 53 | + // output. We deliberately do NOT add a `banner` shebang — that would duplicate |
| 54 | + // it (esbuild's preserved one + the banner). A post-bundle assertion below |
| 55 | + // verifies exactly one shebang on line 1. |
| 56 | + // Inline EVERYTHING (the three @mcptoolshop/* workspace deps included). Only |
| 57 | + // Node's own builtins stay external — they're always present at runtime. |
| 58 | + packages: undefined, |
| 59 | + external: [], |
| 60 | + logLevel: "info", |
| 61 | + sourcemap: false, |
| 62 | + legalComments: "none", |
| 63 | +}); |
| 64 | + |
| 65 | +// ANDON: a malformed shebang silently breaks `loadout-os` as a global bin. |
| 66 | +// Assert exactly one `#!/usr/bin/env node` and that it is line 1. |
| 67 | +const out = readFileSync(outfile, "utf8"); |
| 68 | +const lines = out.split("\n"); |
| 69 | +const shebangCount = lines.filter((l) => l.startsWith("#!")).length; |
| 70 | +if (lines[0] !== "#!/usr/bin/env node" || shebangCount !== 1) { |
| 71 | + console.error( |
| 72 | + `bundle shebang check failed: expected exactly one '#!/usr/bin/env node' on line 1, ` + |
| 73 | + `got ${shebangCount} hashbang line(s); line 1 = ${JSON.stringify(lines[0])}`, |
| 74 | + ); |
| 75 | + process.exit(1); |
| 76 | +} |
| 77 | + |
| 78 | +console.log(`bundled → ${outfile} (${(out.length / 1024).toFixed(1)} kb, shebang OK)`); |
0 commit comments