@linzumi/cli 1.0.120 → 1.0.122
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/impl-res/crossHarnessFailover.mjs +14 -0
- package/dist/impl-res/index.js +366 -0
- package/dist/impl-res/mcp-server.mjs +325 -0
- package/dist/impl-ts/assets/linzumi-logo.svg +1 -0
- package/dist/impl-ts/cloud-supervisor.mjs +18 -0
- package/dist/impl-ts/crossHarnessFailover.mjs +14 -0
- package/dist/impl-ts/index.js +2394 -0
- package/dist/impl-ts/mcp-server.mjs +424 -0
- package/dist/index.js +3 -2393
- package/package.json +1 -1
- package/scripts/build-differential-entry-impl-res.mjs +64 -0
- package/scripts/build-differential-entry.mjs +2 -2
- package/scripts/build.mjs +120 -10
- package/scripts/check-package-size-budget.mjs +186 -0
- package/dist/crossHarnessFailover.mjs +0 -6
- /package/dist/{assets → impl-res/assets}/linzumi-logo.svg +0 -0
- /package/dist/{cloud-supervisor.mjs → impl-res/cloud-supervisor.mjs} +0 -0
package/package.json
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Spec: kandan/plans/2026-07-10-compute-nodes-rescript-commander-zero-ts-program.md (M3 cluster 15)
|
|
2
|
+
// Relationship: Builds the ReScript-impl differential entry
|
|
3
|
+
// (packages/linzumi-cli-rescript/src/entry/CommanderDifferentialMain.res,
|
|
4
|
+
// compiled by `rescript` to .res.mjs) into a single runnable bundle at
|
|
5
|
+
// .differential/impl-res.mjs - the peer of scripts/build-differential-entry.mjs's
|
|
6
|
+
// impl-ts bundle, with the SAME platform/target/format/externals/banner so the
|
|
7
|
+
// differential harness compares the two impls under one bundling reality.
|
|
8
|
+
//
|
|
9
|
+
// Version injection: the ReScript package ships no version literal (repo
|
|
10
|
+
// rule); the CLI version is esbuild-`define`d into the bundle as
|
|
11
|
+
// process.env.LINZUMI_CLI_VERSION (read via CommanderEntryVersion.res), so the
|
|
12
|
+
// hello `version` equals the TS impl's linzumiCliVersion (== package.json
|
|
13
|
+
// version) and the min-version refusal transcripts stay byte-identical.
|
|
14
|
+
//
|
|
15
|
+
// FAIL LOUDLY when the compiled ReScript artifacts are missing: the
|
|
16
|
+
// differential harness must never silently fall back to a stub.
|
|
17
|
+
import { existsSync } from 'node:fs';
|
|
18
|
+
import { readFile } from 'node:fs/promises';
|
|
19
|
+
import { dirname, join } from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { build } from 'esbuild';
|
|
22
|
+
|
|
23
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
24
|
+
|
|
25
|
+
const entryPoint = join(
|
|
26
|
+
packageRoot,
|
|
27
|
+
'../linzumi-cli-rescript/src/entry/CommanderDifferentialMain.res.mjs'
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (!existsSync(entryPoint)) {
|
|
31
|
+
console.error(
|
|
32
|
+
`impl-res differential entry artifact missing: ${entryPoint}\n` +
|
|
33
|
+
'run `pnpm build` in packages/linzumi-cli-rescript first (rescript compiles src/**.res -> .res.mjs in-source)'
|
|
34
|
+
);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const packageJson = JSON.parse(
|
|
39
|
+
await readFile(join(packageRoot, 'package.json'), 'utf8')
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Renamed banner import, mirroring scripts/build-differential-entry.mjs: this
|
|
43
|
+
// bundle keeps source-level identifiers and the sources may import
|
|
44
|
+
// createRequire themselves - the published banner's bare name would collide.
|
|
45
|
+
const banner = {
|
|
46
|
+
js: "import { createRequire as __differentialCreateRequire } from 'node:module';\nconst require = __differentialCreateRequire(import.meta.url);",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
await build({
|
|
50
|
+
entryPoints: [entryPoint],
|
|
51
|
+
bundle: true,
|
|
52
|
+
platform: 'node',
|
|
53
|
+
target: 'node20',
|
|
54
|
+
format: 'esm',
|
|
55
|
+
outfile: join(packageRoot, '.differential/impl-res.mjs'),
|
|
56
|
+
minify: false,
|
|
57
|
+
sourcemap: false,
|
|
58
|
+
legalComments: 'none',
|
|
59
|
+
external: ['ws', 'undici', 'blessed', '@anthropic-ai/claude-agent-sdk'],
|
|
60
|
+
banner,
|
|
61
|
+
define: {
|
|
62
|
+
'process.env.LINZUMI_CLI_VERSION': JSON.stringify(packageJson.version),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// differential harness exercises the SAME bundling reality the shipped CLI
|
|
7
7
|
// has - this is the M3 packaging seam's dist/impl-ts precursor: the M3
|
|
8
8
|
// chooser design ships dist/impl-ts/ + dist/impl-res/ side by side, and the
|
|
9
|
-
// M2.5 differential runner's impl parameter selects between them (
|
|
10
|
-
//
|
|
9
|
+
// M2.5 differential runner's impl parameter selects between them (the
|
|
10
|
+
// impl-res twin is scripts/build-differential-entry-impl-res.mjs).
|
|
11
11
|
//
|
|
12
12
|
// M2.6 mutation mode (`--mutation <name>`): applies ONE seeded behavior
|
|
13
13
|
// delta from scripts/differentialMutations.mjs as a build-time source patch
|
package/scripts/build.mjs
CHANGED
|
@@ -1,27 +1,61 @@
|
|
|
1
1
|
// Spec: plans/2026-05-17-linzumi-cli-node-toolchain-note.md
|
|
2
2
|
// Relationship: Builds the published CLI package with Node-hosted tooling.
|
|
3
|
-
import { copyFile, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { build } from 'esbuild';
|
|
7
7
|
|
|
8
8
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
9
9
|
|
|
10
|
+
// M3 cluster 15: the ReScript package ships no version literal (repo rule);
|
|
11
|
+
// the CLI version is esbuild-`define`d into the impl-res bundle as
|
|
12
|
+
// process.env.LINZUMI_CLI_VERSION (read via CommanderEntryVersion.res), the
|
|
13
|
+
// same source of truth as src/version.ts linzumiCliVersion.
|
|
14
|
+
const packageVersion = JSON.parse(
|
|
15
|
+
await readFile(join(packageRoot, 'package.json'), 'utf8')
|
|
16
|
+
).version;
|
|
17
|
+
|
|
10
18
|
await rm(join(packageRoot, 'dist'), { recursive: true, force: true });
|
|
11
19
|
|
|
12
20
|
const banner = {
|
|
13
21
|
js: "import { createRequire } from 'node:module';\nconst require = createRequire(import.meta.url);",
|
|
14
22
|
};
|
|
15
23
|
|
|
24
|
+
// M3 cluster 15 (entry/packaging/selector): ONE package ships BOTH commander
|
|
25
|
+
// implementations - dist/impl-ts/ (this package's esbuild output, relocated
|
|
26
|
+
// from the historical flat dist/index.js) and dist/impl-res/ (the ReScript
|
|
27
|
+
// commander bundled from ../linzumi-cli-rescript) - behind the tiny plain-JS
|
|
28
|
+
// chooser bundled to dist/index.js. The byte-pinned bin/linzumi.js entry
|
|
29
|
+
// (I153) keeps importing ../dist/index.js and calling main(argv), so the bin
|
|
30
|
+
// bytes stay EXACTLY as pinned and the chooser lives behind them.
|
|
31
|
+
//
|
|
32
|
+
// OWNER GATE (2026-07-15): the chooser resolves an absent/unknown
|
|
33
|
+
// ~/.linzumi/commanderImpl cache to "ts"; nothing in the package selects
|
|
34
|
+
// impl-res by default.
|
|
16
35
|
await build({
|
|
17
|
-
entryPoints: [join(packageRoot, 'src/
|
|
36
|
+
entryPoints: [join(packageRoot, 'src/commanderImplChooser.mjs')],
|
|
18
37
|
bundle: true,
|
|
19
38
|
platform: 'node',
|
|
20
39
|
target: 'node20',
|
|
21
40
|
format: 'esm',
|
|
22
41
|
outfile: join(packageRoot, 'dist/index.js'),
|
|
42
|
+
minify: true,
|
|
43
|
+
sourcemap: false,
|
|
44
|
+
legalComments: 'none',
|
|
45
|
+
// The impl bundles are runtime dynamic imports the chooser selects between;
|
|
46
|
+
// they must never be inlined into the chooser bundle.
|
|
47
|
+
external: ['./impl-ts/*', './impl-res/*'],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await build({
|
|
51
|
+
entryPoints: [join(packageRoot, 'src/index.ts')],
|
|
52
|
+
bundle: true,
|
|
53
|
+
platform: 'node',
|
|
54
|
+
target: 'node20',
|
|
55
|
+
format: 'esm',
|
|
56
|
+
outfile: join(packageRoot, 'dist/impl-ts/index.js'),
|
|
23
57
|
// Production hardening: minify the published CLI bundle and ship no source
|
|
24
|
-
// map. We already bundle to a single
|
|
58
|
+
// map. We already bundle to a single impl bundle (no loose src/); minifying
|
|
25
59
|
// reduces accidental source/IP leakage in the npm artifact. This is NOT a
|
|
26
60
|
// security boundary -- the published package is public by definition, so no
|
|
27
61
|
// secret may live in this code and all authz stays server-side.
|
|
@@ -61,6 +95,16 @@ await build({
|
|
|
61
95
|
banner,
|
|
62
96
|
});
|
|
63
97
|
|
|
98
|
+
// M3 cluster 15: the TS impl's guest MCP bridge also ships CO-LOCATED at
|
|
99
|
+
// dist/impl-ts/mcp-server.mjs (the 'ts' candidate ladder's impl-specific
|
|
100
|
+
// head; the flat dist/mcp-server.mjs above stays as the rollback backstop
|
|
101
|
+
// and the guest path contract). Same bytes, copied - never a second build.
|
|
102
|
+
await mkdir(join(packageRoot, 'dist/impl-ts'), { recursive: true });
|
|
103
|
+
await copyFile(
|
|
104
|
+
join(packageRoot, 'dist/mcp-server.mjs'),
|
|
105
|
+
join(packageRoot, 'dist/impl-ts/mcp-server.mjs')
|
|
106
|
+
);
|
|
107
|
+
|
|
64
108
|
// M3 cluster 14 selector story (compute-nodes program, M3 packaging): the
|
|
65
109
|
// ReScript impl's guest MCP bridge, bundled from the compiled
|
|
66
110
|
// CommanderMcpEntry artifact into dist/impl-res/mcp-server.mjs - the head
|
|
@@ -118,13 +162,23 @@ await build({
|
|
|
118
162
|
platform: 'node',
|
|
119
163
|
target: 'node20',
|
|
120
164
|
format: 'esm',
|
|
121
|
-
outfile: join(packageRoot, 'dist/cloud-supervisor.mjs'),
|
|
165
|
+
outfile: join(packageRoot, 'dist/impl-ts/cloud-supervisor.mjs'),
|
|
122
166
|
minify: true,
|
|
123
167
|
sourcemap: false,
|
|
124
168
|
legalComments: 'none',
|
|
125
169
|
banner,
|
|
126
170
|
});
|
|
127
171
|
|
|
172
|
+
// The in-guest bundles are impl-AGNOSTIC guest artifacts (they run inside the
|
|
173
|
+
// VM, not inside either commander), so the impl-res dist carries the same
|
|
174
|
+
// bytes: its vmCloudSupervisor twin resolves siblings of ITS bundle exactly
|
|
175
|
+
// like the TS one does (resolveDistBundle / dirname(import.meta.url)).
|
|
176
|
+
await mkdir(join(packageRoot, 'dist/impl-res'), { recursive: true });
|
|
177
|
+
await copyFile(
|
|
178
|
+
join(packageRoot, 'dist/impl-ts/cloud-supervisor.mjs'),
|
|
179
|
+
join(packageRoot, 'dist/impl-res/cloud-supervisor.mjs')
|
|
180
|
+
);
|
|
181
|
+
|
|
128
182
|
// F5 cross-harness failover bridge (Workstream F task F5): the runner
|
|
129
183
|
// dynamic-imports ./crossHarnessFailover.mjs relative to its own bundle, so
|
|
130
184
|
// dist ships a self-contained sibling bundle with the compiled ReScript
|
|
@@ -141,7 +195,7 @@ try {
|
|
|
141
195
|
platform: 'node',
|
|
142
196
|
target: 'node20',
|
|
143
197
|
format: 'esm',
|
|
144
|
-
outfile: join(packageRoot, 'dist/crossHarnessFailover.mjs'),
|
|
198
|
+
outfile: join(packageRoot, 'dist/impl-ts/crossHarnessFailover.mjs'),
|
|
145
199
|
minify: true,
|
|
146
200
|
sourcemap: false,
|
|
147
201
|
legalComments: 'none',
|
|
@@ -153,8 +207,9 @@ try {
|
|
|
153
207
|
'linzumi-cli-rescript artifacts built?); shipping the honest refusal ' +
|
|
154
208
|
`stub instead: ${error instanceof Error ? error.message : String(error)}`
|
|
155
209
|
);
|
|
210
|
+
await mkdir(join(packageRoot, 'dist/impl-ts'), { recursive: true });
|
|
156
211
|
await writeFile(
|
|
157
|
-
join(packageRoot, 'dist/crossHarnessFailover.mjs'),
|
|
212
|
+
join(packageRoot, 'dist/impl-ts/crossHarnessFailover.mjs'),
|
|
158
213
|
'export async function transpileAndSeedClaudeSession() {\n' +
|
|
159
214
|
' return {\n' +
|
|
160
215
|
' ok: false,\n' +
|
|
@@ -163,9 +218,64 @@ try {
|
|
|
163
218
|
'}\n'
|
|
164
219
|
);
|
|
165
220
|
}
|
|
166
|
-
|
|
167
|
-
|
|
221
|
+
// The F5 bridge is self-contained; the impl-res commander resolves the same
|
|
222
|
+
// sibling name next to ITS bundle.
|
|
168
223
|
await copyFile(
|
|
169
|
-
join(packageRoot, '
|
|
170
|
-
join(packageRoot, 'dist/
|
|
224
|
+
join(packageRoot, 'dist/impl-ts/crossHarnessFailover.mjs'),
|
|
225
|
+
join(packageRoot, 'dist/impl-res/crossHarnessFailover.mjs')
|
|
171
226
|
);
|
|
227
|
+
|
|
228
|
+
for (const implDir of ['dist/impl-ts', 'dist/impl-res']) {
|
|
229
|
+
await mkdir(join(packageRoot, implDir, 'assets'), { recursive: true });
|
|
230
|
+
await copyFile(
|
|
231
|
+
join(packageRoot, 'src/assets/linzumi-logo.svg'),
|
|
232
|
+
join(packageRoot, implDir, 'assets/linzumi-logo.svg')
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// M3 cluster 15: the ReScript commander itself - dist/impl-res/index.js,
|
|
237
|
+
// bundled from the compiled composition-root entry
|
|
238
|
+
// (packages/linzumi-cli-rescript/src/entry/CommanderEntryMain.res). Same
|
|
239
|
+
// externals/banner posture as the TS impl bundle: the runtime deps stay
|
|
240
|
+
// external (they ship in the package's dependency closure), everything else
|
|
241
|
+
// is inlined. When the ReScript artifacts are not compiled in this checkout
|
|
242
|
+
// the build ships an HONEST refusal stub whose import succeeds but whose
|
|
243
|
+
// main() refuses loudly - the chooser's short-lived-exit breaker then falls
|
|
244
|
+
// back to TS (never a silent wrong impl). Publish CI compiles the ReScript
|
|
245
|
+
// package first and the package-size gate asserts the REAL bundle shipped,
|
|
246
|
+
// so the stub can never reach npm.
|
|
247
|
+
try {
|
|
248
|
+
await build({
|
|
249
|
+
entryPoints: [
|
|
250
|
+
join(
|
|
251
|
+
packageRoot,
|
|
252
|
+
'../linzumi-cli-rescript/src/entry/CommanderEntryMain.res.mjs'
|
|
253
|
+
),
|
|
254
|
+
],
|
|
255
|
+
bundle: true,
|
|
256
|
+
platform: 'node',
|
|
257
|
+
target: 'node20',
|
|
258
|
+
format: 'esm',
|
|
259
|
+
outfile: join(packageRoot, 'dist/impl-res/index.js'),
|
|
260
|
+
minify: true,
|
|
261
|
+
sourcemap: false,
|
|
262
|
+
legalComments: 'none',
|
|
263
|
+
external: ['ws', 'undici', 'blessed', '@anthropic-ai/claude-agent-sdk'],
|
|
264
|
+
banner,
|
|
265
|
+
define: {
|
|
266
|
+
'process.env.LINZUMI_CLI_VERSION': JSON.stringify(packageVersion),
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
} catch (error) {
|
|
270
|
+
console.warn(
|
|
271
|
+
'[build] impl-res commander bundle could not be built (are the ' +
|
|
272
|
+
'linzumi-cli-rescript artifacts compiled?); shipping the honest ' +
|
|
273
|
+
`refusal stub instead: ${error instanceof Error ? error.message : String(error)}`
|
|
274
|
+
);
|
|
275
|
+
await writeFile(
|
|
276
|
+
join(packageRoot, 'dist/impl-res/index.js'),
|
|
277
|
+
'export async function main() {\n' +
|
|
278
|
+
" throw new Error('the rescript commander implementation was not bundled into this build');\n" +
|
|
279
|
+
'}\n'
|
|
280
|
+
);
|
|
281
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/*
|
|
2
|
+
- Date: 2026-07-15
|
|
3
|
+
Spec: kandan/plans/2026-07-10-compute-nodes-rescript-commander-zero-ts-program.md
|
|
4
|
+
(M3 packaging paragraph: "Package-size budget in publish CI")
|
|
5
|
+
Relationship: The M3 cluster 15 publish-CI package-size gate. Runs
|
|
6
|
+
`npm pack --dry-run --json`, enforces the dual-impl package budget with the
|
|
7
|
+
warn-band shape of test/ciBudget.ts (warn at 85% of the fail limit,
|
|
8
|
+
actionable per-file top-offender breakdown - never a bare at-limit
|
|
9
|
+
assertion; see the 2026-07-12 "expected 1000 to be less than 1000" staging
|
|
10
|
+
red), and asserts the dual-impl packaging PARITY facts: both impl bundles
|
|
11
|
+
ship for real (the impl-res honest-refusal stub that dev builds emit when
|
|
12
|
+
the ReScript artifacts are not compiled must never reach npm), the chooser
|
|
13
|
+
stays tiny, and the guest-bundle ladder files exist.
|
|
14
|
+
|
|
15
|
+
Budget rationale (measured 2026-07-15 on this branch): the dual-impl
|
|
16
|
+
package measures ~4.96 MB unpacked with the impl-res stub; the real
|
|
17
|
+
impl-res commander bundle lands well under 3 MB (it bundles the same code
|
|
18
|
+
the ~2.26 MB impl-ts bundle carries, compiled from ReScript). 12 MB
|
|
19
|
+
unpacked caps runaway growth at roughly 1.5x the projected dual-impl size
|
|
20
|
+
while leaving the dual-impl period real headroom; the 85% warn band
|
|
21
|
+
(10.2 MB) makes erosion loud before it reds a publish.
|
|
22
|
+
*/
|
|
23
|
+
import { execFileSync } from 'node:child_process';
|
|
24
|
+
import { dirname } from 'node:path';
|
|
25
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
26
|
+
|
|
27
|
+
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
28
|
+
|
|
29
|
+
export const UNPACKED_SIZE_LIMIT_BYTES = 12 * 1024 * 1024;
|
|
30
|
+
export const WARN_RATIO = 0.85;
|
|
31
|
+
// The chooser must stay a TINY plain-JS shim (it runs before either impl
|
|
32
|
+
// bundle loads); a fat dist/index.js means impl code leaked into it.
|
|
33
|
+
export const CHOOSER_SIZE_LIMIT_BYTES = 64 * 1024;
|
|
34
|
+
// A real commander bundle is megabytes; the honest-refusal stub is ~125
|
|
35
|
+
// bytes. Anything under this floor is not a real implementation.
|
|
36
|
+
export const REAL_IMPL_BUNDLE_FLOOR_BYTES = 200 * 1024;
|
|
37
|
+
|
|
38
|
+
const requiredFiles = [
|
|
39
|
+
'bin/linzumi.js',
|
|
40
|
+
'bin/remote-codex-harness-worker.js',
|
|
41
|
+
'dist/index.js',
|
|
42
|
+
'dist/impl-ts/index.js',
|
|
43
|
+
'dist/impl-res/index.js',
|
|
44
|
+
'dist/mcp-server.mjs',
|
|
45
|
+
'dist/impl-ts/mcp-server.mjs',
|
|
46
|
+
'dist/impl-res/mcp-server.mjs',
|
|
47
|
+
'dist/impl-ts/crossHarnessFailover.mjs',
|
|
48
|
+
'dist/impl-res/crossHarnessFailover.mjs',
|
|
49
|
+
'dist/impl-ts/cloud-supervisor.mjs',
|
|
50
|
+
'dist/impl-res/cloud-supervisor.mjs',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// Per-file real-bundle floors (Greptile P2): the co-located guest bundles
|
|
54
|
+
// have honest-stub fallbacks in the build too (the F5 refusal stub is ~200
|
|
55
|
+
// bytes; the real bridge is ~8 KB; the real cloud supervisor is ~93 KB), so
|
|
56
|
+
// each gets a floor that distinguishes stub from real - a botched build must
|
|
57
|
+
// red the publish, not ship a refusal stub to the fleet.
|
|
58
|
+
export const GUEST_BUNDLE_FLOOR_BYTES = {
|
|
59
|
+
'dist/impl-ts/crossHarnessFailover.mjs': 2 * 1024,
|
|
60
|
+
'dist/impl-res/crossHarnessFailover.mjs': 2 * 1024,
|
|
61
|
+
'dist/impl-ts/cloud-supervisor.mjs': 20 * 1024,
|
|
62
|
+
'dist/impl-res/cloud-supervisor.mjs': 20 * 1024,
|
|
63
|
+
'dist/mcp-server.mjs': 100 * 1024,
|
|
64
|
+
'dist/impl-ts/mcp-server.mjs': 100 * 1024,
|
|
65
|
+
'dist/impl-res/mcp-server.mjs': 100 * 1024,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function formatMb(bytes) {
|
|
69
|
+
return `${Math.round((bytes / (1024 * 1024)) * 100) / 100} MB`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function evaluatePackedPackage(packReport) {
|
|
73
|
+
const failures = [];
|
|
74
|
+
const warnings = [];
|
|
75
|
+
const files = new Map(packReport.files.map((file) => [file.path, file.size]));
|
|
76
|
+
|
|
77
|
+
for (const path of requiredFiles) {
|
|
78
|
+
if (!files.has(path)) {
|
|
79
|
+
failures.push(`required package file missing from the pack: ${path}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const chooserSize = files.get('dist/index.js') ?? 0;
|
|
84
|
+
if (chooserSize > CHOOSER_SIZE_LIMIT_BYTES) {
|
|
85
|
+
failures.push(
|
|
86
|
+
`dist/index.js (the chooser) is ${chooserSize} bytes > ${CHOOSER_SIZE_LIMIT_BYTES}: impl code must not leak into the chooser shim`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const implBundle of [
|
|
91
|
+
'dist/impl-ts/index.js',
|
|
92
|
+
'dist/impl-res/index.js',
|
|
93
|
+
]) {
|
|
94
|
+
const size = files.get(implBundle) ?? 0;
|
|
95
|
+
if (size > 0 && size < REAL_IMPL_BUNDLE_FLOOR_BYTES) {
|
|
96
|
+
failures.push(
|
|
97
|
+
`${implBundle} is ${size} bytes < ${REAL_IMPL_BUNDLE_FLOOR_BYTES}: this is the honest-refusal stub, not a real commander bundle - the publish build must compile the ReScript package first (pnpm --filter @linzumi/cli-rescript build)`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const [path, floor] of Object.entries(GUEST_BUNDLE_FLOOR_BYTES)) {
|
|
103
|
+
const size = files.get(path) ?? 0;
|
|
104
|
+
if (size > 0 && size < floor) {
|
|
105
|
+
failures.push(
|
|
106
|
+
`${path} is ${size} bytes < ${floor}: this looks like a build stub, not the real guest bundle`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// npm pack --json has carried unpackedSize since npm v7; `size` is the
|
|
112
|
+
// COMPRESSED tarball (2-4x smaller), so falling back to it would make the
|
|
113
|
+
// budget silently un-enforceable (Greptile P2). An absent unpackedSize is
|
|
114
|
+
// a gate error, never a pass.
|
|
115
|
+
const unpackedSize = packReport.unpackedSize;
|
|
116
|
+
if (typeof unpackedSize !== 'number' || !Number.isFinite(unpackedSize)) {
|
|
117
|
+
failures.push(
|
|
118
|
+
'npm pack --json did not report unpackedSize; the budget cannot be evaluated (refusing to fall back to the compressed tarball size)'
|
|
119
|
+
);
|
|
120
|
+
return { failures, warnings, unpackedSize: 0, usedRatio: 0 };
|
|
121
|
+
}
|
|
122
|
+
const usedRatio = unpackedSize / UNPACKED_SIZE_LIMIT_BYTES;
|
|
123
|
+
const topOffenders = [...packReport.files]
|
|
124
|
+
.sort((a, b) => b.size - a.size)
|
|
125
|
+
.slice(0, 8)
|
|
126
|
+
.map((file) => ` - ${file.path}: ${formatMb(file.size)}`);
|
|
127
|
+
|
|
128
|
+
if (unpackedSize >= UNPACKED_SIZE_LIMIT_BYTES) {
|
|
129
|
+
failures.push(
|
|
130
|
+
[
|
|
131
|
+
`package unpacked size ${formatMb(unpackedSize)} >= the ${formatMb(UNPACKED_SIZE_LIMIT_BYTES)} budget (${Math.round(usedRatio * 1000) / 10}% used)`,
|
|
132
|
+
'top offenders:',
|
|
133
|
+
...topOffenders,
|
|
134
|
+
].join('\n')
|
|
135
|
+
);
|
|
136
|
+
} else if (unpackedSize >= UNPACKED_SIZE_LIMIT_BYTES * WARN_RATIO) {
|
|
137
|
+
warnings.push(
|
|
138
|
+
[
|
|
139
|
+
`WARNING BAND: package unpacked size ${formatMb(unpackedSize)} is ${Math.round(usedRatio * 1000) / 10}% of the ${formatMb(UNPACKED_SIZE_LIMIT_BYTES)} budget (band starts at ${formatMb(UNPACKED_SIZE_LIMIT_BYTES * WARN_RATIO)})`,
|
|
140
|
+
'top offenders:',
|
|
141
|
+
...topOffenders,
|
|
142
|
+
].join('\n')
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { failures, warnings, unpackedSize, usedRatio };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function main() {
|
|
150
|
+
const raw = execFileSync('npm', ['pack', '--dry-run', '--json'], {
|
|
151
|
+
cwd: packageRoot,
|
|
152
|
+
encoding: 'utf8',
|
|
153
|
+
stdio: ['ignore', 'pipe', 'inherit'],
|
|
154
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
155
|
+
});
|
|
156
|
+
const parsed = JSON.parse(raw);
|
|
157
|
+
const report = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
158
|
+
|
|
159
|
+
const verdict = evaluatePackedPackage(report);
|
|
160
|
+
|
|
161
|
+
console.log(
|
|
162
|
+
`package size: ${formatMb(verdict.unpackedSize)} unpacked of the ${formatMb(UNPACKED_SIZE_LIMIT_BYTES)} budget (${Math.round(verdict.usedRatio * 1000) / 10}% used), ${report.files.length} files`
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
for (const warning of verdict.warnings) {
|
|
166
|
+
console.warn(warning);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (verdict.failures.length > 0) {
|
|
170
|
+
for (const failure of verdict.failures) {
|
|
171
|
+
console.error(`FAIL: ${failure}`);
|
|
172
|
+
}
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log('package-size budget gate: OK');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Auto-run only when invoked directly (the evaluate function is imported by
|
|
180
|
+
// the vitest teeth).
|
|
181
|
+
if (
|
|
182
|
+
process.argv[1] !== undefined &&
|
|
183
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
184
|
+
) {
|
|
185
|
+
main();
|
|
186
|
+
}
|
|
File without changes
|
|
File without changes
|