Skip to content

Commit 644d2bc

Browse files
mcp-tool-shopclaude
andcommitted
build(cli): bundle loadout-os self-contained via esbuild (no external @mcptoolshop deps)
Dep strategy = bundle (director call): esbuild (esbuild.config.mjs) bundles kernel + memories + rules into a single self-contained dist/loadout-os.js, so npm i -g @mcptoolshop/loadout-os installs ONE package with zero @mcptoolshop runtime deps. package.json: bin -> the bundle, files = [bundle, README, CHANGELOG, LICENSE], dependencies removed (the 3 stay as devDeps for the workspace build), prepublishOnly = build + bundle. rules split reworked to run IN-PROCESS (rules' exported generators + readline; no longer shells out to the absent claude-rules bin) — proven standalone. hook test degrades gracefully when apps/hook is absent (global install). Added resolveClaudeMd/resolveMemoryMd to the rules barrel for the in-process split. Verified: 374 tests green; bundle runs from a node_modules-free dir; pack ships 5 files, no @mcptoolshop deps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e974511 commit 644d2bc

5 files changed

Lines changed: 322 additions & 42 deletions

File tree

packages/cli/esbuild.config.mjs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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)`);

packages/cli/package.json

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"author": "mcp-tool-shop",
77
"type": "module",
88
"bin": {
9-
"loadout-os": "./dist/cli.js"
9+
"loadout-os": "./dist/loadout-os.js"
1010
},
1111
"main": "./dist/index.js",
1212
"types": "./dist/index.d.ts",
@@ -17,24 +17,25 @@
1717
}
1818
},
1919
"files": [
20-
"dist",
20+
"dist/loadout-os.js",
2121
"README.md",
2222
"CHANGELOG.md",
2323
"LICENSE"
2424
],
2525
"scripts": {
2626
"build": "tsc",
27+
"bundle": "node esbuild.config.mjs",
2728
"test": "node --test dist/tests/*.test.js",
28-
"verify": "tsc --noEmit && node --test dist/tests/*.test.js"
29+
"verify": "tsc --noEmit && node --test dist/tests/*.test.js",
30+
"prepublishOnly": "npm run build && npm run bundle"
2931
},
30-
"dependencies": {
32+
"devDependencies": {
3133
"@mcptoolshop/ai-loadout": "^1.4.3",
3234
"@mcptoolshop/claude-memories": "^1.0.3",
33-
"@mcptoolshop/claude-rules": "^1.2.2"
34-
},
35-
"devDependencies": {
35+
"@mcptoolshop/claude-rules": "^1.2.2",
3636
"@types/node": "^22",
3737
"typescript": "^5.7",
38+
"esbuild": "^0.28.1",
3839
"c8": "^11"
3940
},
4041
"engines": {

0 commit comments

Comments
 (0)