@linzumi/cli 1.0.116 → 1.0.118

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.
@@ -0,0 +1,484 @@
1
+ // Spec: kandan/plans/2026-07-10-compute-nodes-rescript-commander-zero-ts-program.md
2
+ // (M3 cluster 13, onboarding - the SUPPORT slice: the commander-side
3
+ // starter-task suggestion engine and the hello-demo project materializer).
4
+ // Relationship: Builds the TS-side PARITY ORACLE for the onboarding
5
+ // support modules of the ReScript commander port, the sibling of
6
+ // build-worker-entry-parity-oracle.mjs (cluster 12). The oracle bundles
7
+ // the REAL signupTaskSuggestions.ts (suggestSignupTasksWithCodex /
8
+ // signupTaskSuggestionProcessForTest + the tagged oracle exports of the
9
+ // model/prompt/schema/parse internals), signupTaskSuggestionsProgress.ts
10
+ // (progressLabelForCodexEvent / createTaskSuggestionProgressParser), and
11
+ // helloLinzumiProject.ts (createHelloLinzumiProject + the default
12
+ // constants) behind the cluster 1/2 one-shot BATCH driver (a JSON array
13
+ // of cases on stdin, a JSON array of results on stdout).
14
+ //
15
+ // Determinism + normalization: fs cases run in realpath'd scratch dirs (or
16
+ // a caller-provided externalRoot for the hello byte-tree law, so both
17
+ // impls can build at the SAME root and compare without normalization);
18
+ // paths normalize through the "$DIR" placeholder and the random
19
+ // linzumi-signup-codex-tasks-* scratch roots through the "$TASKS"
20
+ // placeholder. The suggestion flow's codex binary is a case-scripted
21
+ // executable planted from the case tree, so both impls drive identical
22
+ // bytes.
23
+ //
24
+ // helloLinzumiProject.ts reads its logo asset relative to the bundled
25
+ // module, so the build copies src/assets/linzumi-logo.svg next to the
26
+ // bundle (.differential/assets/).
27
+ //
28
+ // SECRETS: the oracle only ever sees the conformance suite's synthetic
29
+ // trees and synthetic credentials. The output is a local test artifact
30
+ // (.differential/ is gitignored, never published) and stays unminified
31
+ // for debuggability.
32
+ import { copyFile, mkdir } from 'node:fs/promises';
33
+ import { dirname, join } from 'node:path';
34
+ import { fileURLToPath } from 'node:url';
35
+ import { build } from 'esbuild';
36
+
37
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
38
+
39
+ const driverSource = `
40
+ import {
41
+ chmodSync,
42
+ existsSync,
43
+ mkdirSync,
44
+ mkdtempSync,
45
+ readdirSync,
46
+ readFileSync,
47
+ realpathSync,
48
+ rmSync,
49
+ statSync,
50
+ writeFileSync,
51
+ } from 'node:fs';
52
+ import { tmpdir } from 'node:os';
53
+ import { dirname as pathDirname, join, relative } from 'node:path';
54
+ import {
55
+ parseTaskSuggestionResponseForTest,
56
+ resolveTaskSuggestionModelForTest,
57
+ signupTaskSuggestionProcessForTest,
58
+ suggestSignupTasksWithCodex,
59
+ taskSuggestionJsonSchemaForTest,
60
+ taskSuggestionPromptForTest,
61
+ } from './src/signupTaskSuggestions';
62
+ import {
63
+ createTaskSuggestionProgressParser,
64
+ progressLabelForCodexEvent,
65
+ } from './src/signupTaskSuggestionsProgress';
66
+ import {
67
+ createHelloLinzumiProject,
68
+ defaultHelloLinzumiHost,
69
+ defaultHelloLinzumiParentDir,
70
+ defaultHelloLinzumiPort,
71
+ defaultHelloLinzumiProjectDir,
72
+ defaultHelloLinzumiProjectName,
73
+ } from './src/helloLinzumiProject';
74
+
75
+ // --- Placeholder + snapshot plumbing --------------------------------------------
76
+
77
+ const dirPlaceholder = '$DIR';
78
+
79
+ function scratch() {
80
+ // realpath so macOS /var -> /private/var never splits the two impls'
81
+ // placeholder substitution.
82
+ return realpathSync(mkdtempSync(join(tmpdir(), 'linzumi-onboarding-oracle-')));
83
+ }
84
+
85
+ function substitute(value, from, to) {
86
+ if (typeof value === 'string') {
87
+ return value.split(from).join(to);
88
+ }
89
+ if (Array.isArray(value)) {
90
+ return value.map((entry) => substitute(entry, from, to));
91
+ }
92
+ if (typeof value === 'object' && value !== null) {
93
+ return Object.fromEntries(
94
+ Object.entries(value).map(([key, entry]) => [
95
+ substitute(key, from, to),
96
+ substitute(entry, from, to),
97
+ ])
98
+ );
99
+ }
100
+ return value;
101
+ }
102
+
103
+ // The suggestion flow's per-attempt scratch roots are random mkdtemp dirs
104
+ // under the OS tmpdir; normalize the WHOLE path through one placeholder.
105
+ function normalizeTasksTemp(value) {
106
+ if (typeof value === 'string') {
107
+ return value.replace(/\\S*linzumi-signup-codex-tasks-[^/"'\\s]+/g, '$TASKS');
108
+ }
109
+ if (Array.isArray(value)) {
110
+ return value.map((entry) => normalizeTasksTemp(entry));
111
+ }
112
+ if (typeof value === 'object' && value !== null) {
113
+ return Object.fromEntries(
114
+ Object.entries(value).map(([key, entry]) => [
115
+ normalizeTasksTemp(key),
116
+ normalizeTasksTemp(entry),
117
+ ])
118
+ );
119
+ }
120
+ return value;
121
+ }
122
+
123
+ function buildTree(root, files) {
124
+ for (const [relPath, spec] of Object.entries(files ?? {})) {
125
+ const full = join(root, relPath);
126
+ if (relPath.endsWith('/')) {
127
+ mkdirSync(full, { recursive: true });
128
+ } else {
129
+ mkdirSync(pathDirname(full), { recursive: true });
130
+ if (spec !== null && typeof spec === 'object') {
131
+ writeFileSync(full, String(spec.content), 'utf8');
132
+ if (spec.mode !== undefined) {
133
+ chmodSync(full, spec.mode);
134
+ }
135
+ } else {
136
+ writeFileSync(full, String(spec), 'utf8');
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ function snapshotFiles(dir) {
143
+ const files = {};
144
+ const walk = (current) => {
145
+ let names;
146
+ try {
147
+ names = readdirSync(current).sort();
148
+ } catch {
149
+ return;
150
+ }
151
+ for (const name of names) {
152
+ const full = join(current, name);
153
+ const stats = statSync(full);
154
+ if (stats.isDirectory()) {
155
+ files[relative(dir, full) + '/'] = '<dir>';
156
+ walk(full);
157
+ } else {
158
+ files[relative(dir, full)] = {
159
+ content: readFileSync(full, 'utf8'),
160
+ mode: stats.mode & 0o777,
161
+ };
162
+ }
163
+ }
164
+ };
165
+ walk(dir);
166
+ return files;
167
+ }
168
+
169
+ // Env manipulation is per-case and restored afterwards (the batch runs
170
+ // sequentially in one process).
171
+ function withEnv(overrides, run) {
172
+ const saved = {};
173
+ for (const [key, value] of Object.entries(overrides)) {
174
+ saved[key] = process.env[key];
175
+ if (value === null) {
176
+ delete process.env[key];
177
+ } else {
178
+ process.env[key] = value;
179
+ }
180
+ }
181
+ const restore = () => {
182
+ for (const [key, value] of Object.entries(saved)) {
183
+ if (value === undefined) {
184
+ delete process.env[key];
185
+ } else {
186
+ process.env[key] = value;
187
+ }
188
+ }
189
+ };
190
+ return { run, restore };
191
+ }
192
+
193
+ async function runWithEnv(overrides, run) {
194
+ const { restore } = withEnv(overrides, run);
195
+ try {
196
+ return await run();
197
+ } finally {
198
+ restore();
199
+ }
200
+ }
201
+
202
+ // --- Suggestion transforms ---------------------------------------------------------
203
+
204
+ function handleResolveModel(testCase) {
205
+ return runWithEnv(
206
+ { LINZUMI_SIGNUP_TASK_MODEL: testCase.envModel ?? null },
207
+ () => ({
208
+ value: resolveTaskSuggestionModelForTest(
209
+ testCase.explicitModel ?? undefined,
210
+ testCase.wafer === true
211
+ ? {
212
+ config: {
213
+ id: 'linzumi',
214
+ name: 'Linzumi',
215
+ baseUrl: 'https://example.invalid/llm',
216
+ envKey: 'LINZUMI_LLM_PROXY_TOKEN',
217
+ wireApi: 'responses',
218
+ },
219
+ env: {},
220
+ }
221
+ : undefined
222
+ ),
223
+ })
224
+ );
225
+ }
226
+
227
+ function providerOfCase(spec) {
228
+ if (spec === undefined || spec === null) {
229
+ return undefined;
230
+ }
231
+ return {
232
+ config: {
233
+ id: spec.config.id,
234
+ name: spec.config.name,
235
+ baseUrl: spec.config.baseUrl,
236
+ envKey: spec.config.envKey,
237
+ wireApi: spec.config.wireApi,
238
+ ...(spec.config.httpHeaders === undefined ? {} : { httpHeaders: spec.config.httpHeaders }),
239
+ ...(spec.config.envHttpHeaders === undefined
240
+ ? {}
241
+ : { envHttpHeaders: spec.config.envHttpHeaders }),
242
+ },
243
+ env: spec.env ?? {},
244
+ };
245
+ }
246
+
247
+ function handleBuildProcess(testCase) {
248
+ const built = signupTaskSuggestionProcessForTest({
249
+ command: testCase.command,
250
+ model: testCase.model,
251
+ ...(testCase.modelProvider === undefined
252
+ ? {}
253
+ : { modelProvider: providerOfCase(testCase.modelProvider) }),
254
+ projectPath: testCase.projectPath,
255
+ schemaPath: testCase.schemaPath,
256
+ outputPath: testCase.outputPath,
257
+ prompt: testCase.prompt,
258
+ ...(testCase.timeoutMs === undefined ? {} : { timeoutMs: testCase.timeoutMs }),
259
+ codexHome: testCase.codexHome,
260
+ ...(testCase.forwardableOpenAiApiKey === undefined
261
+ ? {}
262
+ : { forwardableOpenAiApiKey: testCase.forwardableOpenAiApiKey }),
263
+ });
264
+ return {
265
+ value: {
266
+ command: built.command,
267
+ args: [...built.args],
268
+ cwd: built.cwd,
269
+ stdin: built.stdin,
270
+ env: built.env ?? null,
271
+ timeoutMs: built.timeoutMs,
272
+ },
273
+ };
274
+ }
275
+
276
+ function handleProgressChunks(testCase) {
277
+ const labels = [];
278
+ const throwOn = new Set(testCase.throwOnLabels ?? []);
279
+ const parser = createTaskSuggestionProgressParser((label) => {
280
+ labels.push(label);
281
+ if (throwOn.has(label)) {
282
+ throw new Error('scripted consumer failure');
283
+ }
284
+ });
285
+ for (const chunk of testCase.chunks ?? []) {
286
+ parser.push(chunk);
287
+ }
288
+ if (testCase.flush !== false) {
289
+ parser.flush();
290
+ }
291
+ return { labels };
292
+ }
293
+
294
+ // --- The suggestion flow -------------------------------------------------------------
295
+
296
+ async function handleSuggestFlow(testCase) {
297
+ const dir = scratch();
298
+ const sub = (value) => substitute(value, dirPlaceholder, dir);
299
+ buildTree(dir, sub(testCase.files ?? {}));
300
+
301
+ const labels = [];
302
+ const onProgress =
303
+ testCase.withProgress === true ? (label) => labels.push(label) : undefined;
304
+
305
+ const outcome = await runWithEnv(
306
+ {
307
+ LINZUMI_CODEX_HOME: join(dir, 'isolated-home'),
308
+ CODEX_HOME: join(dir, 'user-codex'),
309
+ OPENAI_API_KEY: null,
310
+ LINZUMI_SIGNUP_TASK_MODEL: null,
311
+ },
312
+ async () => {
313
+ try {
314
+ const tasks = await suggestSignupTasksWithCodex({
315
+ projectPath: join(dir, testCase.projectRelDir ?? 'project'),
316
+ codexBin: sub(testCase.codexBin),
317
+ ...(testCase.model === undefined ? {} : { model: testCase.model }),
318
+ ...(testCase.modelProvider === undefined
319
+ ? {}
320
+ : { modelProvider: providerOfCase(sub(testCase.modelProvider)) }),
321
+ ...(onProgress === undefined ? {} : { onProgress }),
322
+ });
323
+ return { ok: true, tasks };
324
+ } catch (error) {
325
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
326
+ }
327
+ }
328
+ );
329
+
330
+ // Give any straggling stdout events a beat before reading labels (the
331
+ // scripted binary sleeps before exiting, so labels are ordinarily
332
+ // delivered pre-exit in both impls).
333
+ await new Promise((resolve) => setTimeout(resolve, 50));
334
+
335
+ const invocationsPath = join(dir, 'invocations.jsonl');
336
+ const invocations = existsSync(invocationsPath)
337
+ ? readFileSync(invocationsPath, 'utf8')
338
+ .split('\\n')
339
+ .filter((line) => line !== '')
340
+ .map((line) => JSON.parse(line))
341
+ : [];
342
+
343
+ // The per-attempt scratch roots must be gone after the flow (the TS
344
+ // finally); derive them from the recorded argv.
345
+ const tempRoots = new Set();
346
+ for (const invocation of invocations) {
347
+ const index = invocation.argv.indexOf('--output-schema');
348
+ if (index !== -1) {
349
+ tempRoots.add(pathDirname(invocation.argv[index + 1]));
350
+ }
351
+ }
352
+ const tempRootsGone = [...tempRoots].every((root) => !existsSync(root));
353
+
354
+ const result = {
355
+ outcome,
356
+ labels,
357
+ invocations,
358
+ isolatedHome: snapshotFiles(join(dir, 'isolated-home')),
359
+ tempRootsGone,
360
+ };
361
+ return normalizeTasksTemp(substitute(result, dir, dirPlaceholder));
362
+ }
363
+
364
+ // --- The hello-demo project ----------------------------------------------------------
365
+
366
+ function handleHelloCreate(testCase) {
367
+ const externalRoot = testCase.externalRoot;
368
+ const dir = externalRoot ?? scratch();
369
+ const sub = (value) => substitute(value, dirPlaceholder, dir);
370
+ buildTree(dir, sub(testCase.files ?? {}));
371
+
372
+ let result;
373
+ try {
374
+ const input = sub(testCase.input);
375
+ const project = createHelloLinzumiProject(
376
+ typeof input === 'string'
377
+ ? input
378
+ : {
379
+ ...(input.rootPath === undefined ? {} : { rootPath: input.rootPath }),
380
+ ...(input.parentDir === undefined ? {} : { parentDir: input.parentDir }),
381
+ ...(input.name === undefined ? {} : { name: input.name }),
382
+ ...(input.port === undefined ? {} : { port: input.port }),
383
+ ...(input.host === undefined ? {} : { host: input.host }),
384
+ ...(input.reset === undefined ? {} : { reset: input.reset }),
385
+ }
386
+ );
387
+ result = { ok: project };
388
+ } catch (error) {
389
+ result = { threw: error instanceof Error ? error.message : String(error) };
390
+ }
391
+
392
+ return substitute(
393
+ {
394
+ result,
395
+ ...(externalRoot === undefined ? { tree: snapshotFiles(dir) } : {}),
396
+ },
397
+ dir,
398
+ dirPlaceholder
399
+ );
400
+ }
401
+
402
+ // --- Dispatch -----------------------------------------------------------------------
403
+
404
+ async function handle(testCase) {
405
+ switch (testCase.op) {
406
+ case 'resolveModel':
407
+ return handleResolveModel(testCase);
408
+ case 'prompt':
409
+ return { value: taskSuggestionPromptForTest(testCase.previousResponse ?? undefined) };
410
+ case 'schemaFile':
411
+ return {
412
+ value: JSON.stringify(taskSuggestionJsonSchemaForTest(), null, 2) + '\\n',
413
+ };
414
+ case 'parseResponse': {
415
+ const parsed = parseTaskSuggestionResponseForTest(testCase.response);
416
+ return { value: parsed === undefined ? null : parsed };
417
+ }
418
+ case 'buildProcess':
419
+ return handleBuildProcess(testCase);
420
+ case 'progressEvent': {
421
+ const label = progressLabelForCodexEvent(testCase.event);
422
+ return { value: label === undefined ? null : label };
423
+ }
424
+ case 'progressChunks':
425
+ return handleProgressChunks(testCase);
426
+ case 'suggestFlow':
427
+ return handleSuggestFlow(testCase);
428
+ case 'helloCreate':
429
+ return handleHelloCreate(testCase);
430
+ case 'helloDefaults':
431
+ return {
432
+ value: {
433
+ projectDir: defaultHelloLinzumiProjectDir,
434
+ projectName: defaultHelloLinzumiProjectName,
435
+ parentDir: defaultHelloLinzumiParentDir,
436
+ port: defaultHelloLinzumiPort,
437
+ host: defaultHelloLinzumiHost,
438
+ },
439
+ };
440
+ default:
441
+ return { threw: true, message: 'unknown op ' + String(testCase.op) };
442
+ }
443
+ }
444
+
445
+ let stdin = '';
446
+ process.stdin.setEncoding('utf8');
447
+ for await (const chunk of process.stdin) {
448
+ stdin += chunk;
449
+ }
450
+ const cases = JSON.parse(stdin);
451
+ const results = [];
452
+ for (const testCase of cases) {
453
+ results.push(await handle(testCase));
454
+ }
455
+ process.stdout.write(JSON.stringify(results));
456
+ `;
457
+
458
+ await mkdir(join(packageRoot, '.differential'), { recursive: true });
459
+
460
+ await build({
461
+ stdin: {
462
+ contents: driverSource,
463
+ resolveDir: packageRoot,
464
+ sourcefile: 'onboarding-support-parity-driver.ts',
465
+ loader: 'ts',
466
+ },
467
+ bundle: true,
468
+ platform: 'node',
469
+ target: 'node20',
470
+ format: 'esm',
471
+ outfile: join(packageRoot, '.differential/onboarding-support-parity-oracle.mjs'),
472
+ minify: false,
473
+ sourcemap: false,
474
+ legalComments: 'none',
475
+ external: ['ws', 'undici', 'blessed', '@anthropic-ai/claude-agent-sdk'],
476
+ });
477
+
478
+ // helloLinzumiProject.ts reads its logo relative to the executing module;
479
+ // the bundle executes from .differential/, so mirror the asset beside it.
480
+ await mkdir(join(packageRoot, '.differential/assets'), { recursive: true });
481
+ await copyFile(
482
+ join(packageRoot, 'src/assets/linzumi-logo.svg'),
483
+ join(packageRoot, '.differential/assets/linzumi-logo.svg')
484
+ );