@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,562 @@
1
+ // Spec: kandan/plans/2026-07-10-compute-nodes-rescript-commander-zero-ts-program.md
2
+ // (M3 cluster 13, onboarding).
3
+ // Relationship: Builds the TS-side PARITY ORACLE for the signup-flow port -
4
+ // the interactive first-run flow. ONE esbuild bundle imports the REAL
5
+ // signup-flow module exports behind the cluster 1/2 stdin/stdout batch
6
+ // driver (a JSON array of cases in, a JSON array of results out). The
7
+ // ReScript package's OnboardingFlowParityConformance drives the SAME
8
+ // inputs through src/onboarding/CommanderOnboarding* and asserts
9
+ // byte-equal terminal transcripts, deep-equal recorded side effects, and
10
+ // byte-equal debug launch payloads.
11
+ //
12
+ // The crown op is `flow_walk`: run the REAL runSignupFlow end-to-end with
13
+ // fully scripted deps (prompt answer queues, scripted preflight tools,
14
+ // scripted discovery/task/server/launcher seams, an in-memory draft
15
+ // store, a fixed-width fake terminal) and return every byte the flow
16
+ // wrote plus every recorded side effect. `flow_walk_tty` goes one layer
17
+ // deeper: NO scripted prompts - the flow renders through the REAL
18
+ // @inquirer/prompts + the raw-keypress project picker over scripted
19
+ // PassThrough TTYs, driven by a seeded key schedule (the cluster 11
20
+ // interactive-parity pattern). `picker_walk` drives the raw project
21
+ // picker alone through a key schedule.
22
+ //
23
+ // Determinism: clocks never reach rendered output (the draft store
24
+ // normalizes updatedAt; auth writes use pinned timestamps); scripted
25
+ // scenarios use absolute scratch paths so the fs/git probes the flow runs
26
+ // inline (project resolution, git emails) behave identically for both
27
+ // impls over the same trees; each case runs in a fresh module-cache
28
+ // process, and walk cases clear the git-head cache via the exported hook.
29
+ //
30
+ // SECRETS: scripted tokens are synthetic; the launch payload op exists
31
+ // precisely to pin the [redacted] behavior byte-for-byte.
32
+ //
33
+ // The output is a local test artifact (.differential/ is gitignored, never
34
+ // published) and stays unminified for debuggability.
35
+ import { mkdir } from 'node:fs/promises';
36
+ import { dirname, join } from 'node:path';
37
+ import { fileURLToPath } from 'node:url';
38
+ import { build } from 'esbuild';
39
+
40
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
41
+
42
+ const driverSource = `
43
+ import { PassThrough } from 'node:stream';
44
+ import {
45
+ runSignupFlow,
46
+ signupHelpText,
47
+ signupPromptCancelMessage,
48
+ parseSignupTuiOptions,
49
+ signupAuthMatchesServiceUrl,
50
+ comparableServiceUrl,
51
+ autocompletePathSuggestions,
52
+ signupProjectPickerFrameForTest,
53
+ projectPickerKeyIsPrintableForTest,
54
+ toggleProjectPickerSelectionForTest,
55
+ signupEmailPickerChoiceNamesForTest,
56
+ signupSuggestedEmailIsUsefulForTest,
57
+ signupSpinnerLinesForTest,
58
+ signupTaskSuggestionWaitMessageForTest,
59
+ signupLaunchPayloadForTest,
60
+ clearSignupGitHeadSummaryCacheForTest,
61
+ signupProjectGitHeadForPathForTest,
62
+ signupDiscoverProjectsForStartForTest,
63
+ signupReadTaskCacheForTest,
64
+ resolveSignupCodexCommandForTest,
65
+ signupCodexTaskSuggestionProcessForTest,
66
+ signupSuggestTasksForProjectForTest,
67
+ runProjectPickerPromptForTest,
68
+ } from './src/signupFlow';
69
+ import { SignupServerError } from './src/signupServerClient';
70
+
71
+ const orNull = (value) => (value === undefined ? null : value);
72
+
73
+ // --- Scripted terminal ------------------------------------------------------------------
74
+
75
+ function makeFakeWriter(columns) {
76
+ let bytes = '';
77
+ return {
78
+ writer: {
79
+ columns: columns === null ? undefined : columns,
80
+ write: (chunk) => {
81
+ bytes += chunk;
82
+ return true;
83
+ },
84
+ },
85
+ transcript: () => bytes,
86
+ };
87
+ }
88
+
89
+ // --- Scripted prompts ---------------------------------------------------------------------
90
+
91
+ function makeScriptedPrompts(script, log) {
92
+ let index = 0;
93
+ const next = (kind, detail) => {
94
+ const entry = script[index];
95
+ index += 1;
96
+ if (entry === undefined) {
97
+ throw new Error('prompt script exhausted at ' + kind);
98
+ }
99
+ if (entry.kind !== kind) {
100
+ throw new Error('prompt script expected ' + entry.kind + ' got ' + kind);
101
+ }
102
+ log.push({ kind, detail });
103
+ if (entry.reject !== undefined) {
104
+ throw new Error(entry.reject);
105
+ }
106
+ return entry.value;
107
+ };
108
+ return {
109
+ input: async (args) => next('input', { message: args.message, defaultValue: args.defaultValue }),
110
+ projectPicker: async (args) =>
111
+ next('projectPicker', {
112
+ message: args.message,
113
+ projectPaths: args.projects.map((project) => project.path),
114
+ defaultSelectedPaths: args.defaultSelectedPaths,
115
+ }),
116
+ starterProjectPicker: async (args) =>
117
+ next('starterProjectPicker', {
118
+ message: args.message,
119
+ projectPaths: args.projects.map((project) => project.path),
120
+ defaultValue: args.defaultValue,
121
+ }),
122
+ emailPicker: async (args) =>
123
+ next('emailPicker', {
124
+ suggestedEmails: args.suggestedEmails,
125
+ defaultValue: args.defaultValue,
126
+ }),
127
+ checkbox: async (args) =>
128
+ next('checkbox', {
129
+ message: args.message,
130
+ choices: args.choices,
131
+ required: args.required,
132
+ minSelected: args.minSelected,
133
+ maxSelected: orNull(args.maxSelected),
134
+ }),
135
+ confirm: async (args) => next('confirm', { message: args.message, defaultValue: args.defaultValue }),
136
+ };
137
+ }
138
+
139
+ // --- Scripted server client -----------------------------------------------------------------
140
+
141
+ function scriptedFailure(spec) {
142
+ if (spec.serverCode !== undefined) {
143
+ // The real client's structured-code error carries the code AS the
144
+ // message (signupServerClient's error split).
145
+ return new SignupServerError({
146
+ status: spec.status ?? 422,
147
+ code: spec.serverCode,
148
+ message: spec.serverCode,
149
+ });
150
+ }
151
+ return new Error(spec.message ?? 'scripted failure');
152
+ }
153
+
154
+ function makeScriptedClient(script, log) {
155
+ const take = (leg, detail) => {
156
+ const queue = script[leg] ?? [];
157
+ const entry = queue.shift();
158
+ log.push({ leg, detail });
159
+ if (entry === undefined) {
160
+ throw new Error('server script exhausted at ' + leg);
161
+ }
162
+ if (entry.failure !== undefined) {
163
+ throw scriptedFailure(entry.failure);
164
+ }
165
+ return entry.value;
166
+ };
167
+ return {
168
+ startEmailCode: async (args) => take('startEmailCode', args),
169
+ verifyEmailCode: async (args) => take('verifyEmailCode', args),
170
+ validateAuth: async (args) => take('validateAuth', { accessToken: '[recorded]' }),
171
+ completeMissionControl: async (args) => take('completeMissionControl', args),
172
+ startMissionControlTasks: async (args) => take('startMissionControlTasks', args),
173
+ };
174
+ }
175
+
176
+ // --- Scripted stores + seams -------------------------------------------------------------------
177
+
178
+ function makeScriptedDraftStore(initial, writes) {
179
+ let draft = initial ?? undefined;
180
+ return {
181
+ read: () => draft,
182
+ write: (next) => {
183
+ draft = next;
184
+ writes.push({ ...next, updatedAt: '[normalized]' });
185
+ },
186
+ clear: () => {
187
+ draft = undefined;
188
+ writes.push('cleared');
189
+ },
190
+ };
191
+ }
192
+
193
+ function makePreflightRuntime(spec) {
194
+ const tools = spec.tools ?? {};
195
+ return {
196
+ cwd: spec.cwd,
197
+ homeDir: spec.homeDir,
198
+ probeTool: async (command) => {
199
+ const tool = tools[command];
200
+ if (tool === undefined) {
201
+ return { command, available: false };
202
+ }
203
+ return { command, available: tool.available, version: tool.version ?? undefined };
204
+ },
205
+ readGitEmail: async () => (spec.gitEmail === undefined ? undefined : spec.gitEmail),
206
+ discoverCodeRoots: async () => spec.codeRoots ?? [],
207
+ checkBrowserHandoff: async () => spec.browserHandoff !== false,
208
+ ...(spec.bundledCodexBin === undefined
209
+ ? {}
210
+ : { resolveBundledCodexBin: () => spec.bundledCodexBin }),
211
+ };
212
+ }
213
+
214
+ function reviveProjects(projects) {
215
+ return (projects ?? []).map((project) => ({
216
+ path: project.path,
217
+ language: project.language,
218
+ source: project.source,
219
+ ...(project.gitHead === undefined || project.gitHead === null
220
+ ? {}
221
+ : { gitHead: project.gitHead }),
222
+ }));
223
+ }
224
+
225
+ // --- flow_walk -------------------------------------------------------------------------------------
226
+
227
+ async function handleFlowWalk(testCase) {
228
+ clearSignupGitHeadSummaryCacheForTest();
229
+ const { writer, transcript } = makeFakeWriter(testCase.columns ?? 80);
230
+ const promptLog = [];
231
+ const serverLog = [];
232
+ const draftWrites = [];
233
+ const authWrites = [];
234
+ const completionWrites = [];
235
+ const launcherCalls = [];
236
+ const openedUrls = [];
237
+ const scriptedClient =
238
+ testCase.server === undefined ? false : makeScriptedClient(testCase.server, serverLog);
239
+ const pinnedNow = '2026-07-14T00:00:00.000Z';
240
+ let outcome = { ok: true };
241
+ try {
242
+ await runSignupFlow({
243
+ input: { isTTY: testCase.inputIsTty === true },
244
+ output: writer,
245
+ prompts: makeScriptedPrompts(testCase.prompts ?? [], promptLog),
246
+ preflight: makePreflightRuntime(testCase.preflight),
247
+ projectDiscovery: {
248
+ discoverProjects: async () => reviveProjects(testCase.projects),
249
+ },
250
+ taskSuggestion: {
251
+ suggestTasks: async () => testCase.suggestedTasks ?? [],
252
+ },
253
+ forceSignup: testCase.forceSignup === true,
254
+ serviceUrl: testCase.serviceUrl,
255
+ ...(testCase.codeServerBin === undefined ? {} : { codeServerBin: testCase.codeServerBin }),
256
+ openBrowser: testCase.openBrowser !== false,
257
+ debugLaunchPayload: testCase.debugLaunchPayload !== false,
258
+ openUrl: async (url) => {
259
+ openedUrls.push(url);
260
+ if (testCase.openUrlFails === true) {
261
+ throw new Error('scripted open failure');
262
+ }
263
+ },
264
+ readSignupAuth: () => (testCase.existingAuth === undefined ? undefined : testCase.existingAuth),
265
+ readConfiguredProjectPaths: () => testCase.configuredPaths ?? [],
266
+ signupServerClient: scriptedClient,
267
+ commanderLauncher: async (args) => {
268
+ launcherCalls.push(args);
269
+ const launch = testCase.launch;
270
+ if (launch === undefined) {
271
+ throw new Error('scripted launcher without a launch result');
272
+ }
273
+ if (launch.throws !== undefined) {
274
+ throw new Error(launch.throws);
275
+ }
276
+ return launch;
277
+ },
278
+ signupDraftStore:
279
+ testCase.draft === undefined && testCase.useDraftStore !== true
280
+ ? false
281
+ : makeScriptedDraftStore(testCase.draft, draftWrites),
282
+ signupTaskCacheStore: false,
283
+ retryPolicy: false,
284
+ writeSignupAuth: (args) => {
285
+ authWrites.push(args.email);
286
+ return {
287
+ email: args.email,
288
+ serviceUrl: testCase.serviceUrl,
289
+ createdAt: pinnedNow,
290
+ updatedAt: pinnedNow,
291
+ };
292
+ },
293
+ writeSignupCompletion: (args) => {
294
+ completionWrites.push({
295
+ ...args,
296
+ accessToken: '[recorded]',
297
+ refreshToken: '[recorded]',
298
+ });
299
+ return {
300
+ email: args.email,
301
+ serviceUrl: args.serviceUrl,
302
+ accessToken: args.accessToken,
303
+ refreshToken: args.refreshToken,
304
+ ...(args.expiresInSeconds === undefined
305
+ ? {}
306
+ : { expiresInSeconds: args.expiresInSeconds }),
307
+ ...(args.userId === undefined ? {} : { userId: args.userId }),
308
+ ...(args.workspaceSlug === undefined ? {} : { workspaceSlug: args.workspaceSlug }),
309
+ ...(args.launchUrl === undefined ? {} : { launchUrl: args.launchUrl }),
310
+ createdAt: pinnedNow,
311
+ updatedAt: pinnedNow,
312
+ };
313
+ },
314
+ });
315
+ } catch (error) {
316
+ outcome = { ok: false, message: error instanceof Error ? error.message : String(error) };
317
+ }
318
+ return {
319
+ outcome,
320
+ transcript: transcript(),
321
+ promptLog,
322
+ serverLog,
323
+ draftWrites,
324
+ authWrites,
325
+ completionWrites,
326
+ launcherCalls,
327
+ openedUrls,
328
+ };
329
+ }
330
+
331
+ // --- Key-schedule plumbing (flow_walk_tty + picker_walk) ------------------------------------------
332
+
333
+ function makeFakeTtyInput() {
334
+ const stream = new PassThrough();
335
+ stream.isTTY = true;
336
+ stream.setRawMode = () => stream;
337
+ return stream;
338
+ }
339
+
340
+ const NAMED_KEYS = {
341
+ up: '\\u001b[A',
342
+ down: '\\u001b[B',
343
+ left: '\\u001b[D',
344
+ right: '\\u001b[C',
345
+ tab: '\\t',
346
+ enter: '\\r',
347
+ space: ' ',
348
+ backspace: '\\u007f',
349
+ 'ctrl-c': '\\u0003',
350
+ };
351
+
352
+ function keyBytes(key) {
353
+ if (key.startsWith('text:')) {
354
+ return key.slice(5);
355
+ }
356
+ const bytes = NAMED_KEYS[key];
357
+ if (bytes === undefined) {
358
+ throw new Error('unknown scheduled key ' + key);
359
+ }
360
+ return bytes;
361
+ }
362
+
363
+ const settle = () => new Promise((resolve) => setTimeout(resolve, 15));
364
+
365
+ async function driveSchedule(input, schedule) {
366
+ for (const key of schedule) {
367
+ await settle();
368
+ input.write(keyBytes(key));
369
+ }
370
+ await settle();
371
+ }
372
+
373
+ async function handlePickerWalk(testCase) {
374
+ clearSignupGitHeadSummaryCacheForTest();
375
+ const input = makeFakeTtyInput();
376
+ const { writer, transcript } = makeFakeWriter(testCase.columns ?? 80);
377
+ // The outcome handler attaches BEFORE the schedule drives: a ctrl-c
378
+ // cancellation rejects mid-schedule, and an unhandled rejection would
379
+ // take the whole oracle process down.
380
+ const guarded = runProjectPickerPromptForTest(input, writer, {
381
+ message: testCase.message,
382
+ projects: reviveProjects(testCase.projects),
383
+ defaultSelectedPaths: testCase.defaultSelectedPaths ?? [],
384
+ }).then(
385
+ (selected) => ({ ok: true, selected }),
386
+ (error) => ({ ok: false, message: error instanceof Error ? error.message : String(error) })
387
+ );
388
+ await driveSchedule(input, testCase.schedule ?? []);
389
+ return { outcome: await guarded, transcript: transcript() };
390
+ }
391
+
392
+ // --- Unit ops ----------------------------------------------------------------------------------------
393
+
394
+ async function handle(testCase) {
395
+ switch (testCase.op) {
396
+ case 'help_text':
397
+ return signupHelpText();
398
+ case 'cancel_message':
399
+ return orNull(signupPromptCancelMessage(new Error(testCase.message)));
400
+ case 'parse_options':
401
+ try {
402
+ const options = parseSignupTuiOptions(testCase.args);
403
+ return {
404
+ ok: true,
405
+ serviceUrl: options.serviceUrl,
406
+ forceSignup: options.forceSignup,
407
+ codeServerBin: orNull(options.codeServerBin),
408
+ openBrowser: options.openBrowser,
409
+ debugLaunchPayload: options.debugLaunchPayload,
410
+ };
411
+ } catch (error) {
412
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
413
+ }
414
+ case 'auth_matches':
415
+ return signupAuthMatchesServiceUrl(testCase.auth, testCase.serviceUrl);
416
+ case 'comparable_url':
417
+ return comparableServiceUrl(testCase.value);
418
+ case 'autocomplete':
419
+ return autocompletePathSuggestions(testCase.pathInput);
420
+ case 'picker_frame':
421
+ return signupProjectPickerFrameForTest({
422
+ columns: testCase.columns,
423
+ projects: reviveProjects(testCase.projects),
424
+ selectedPaths: testCase.selectedPaths,
425
+ });
426
+ case 'printable_key':
427
+ return projectPickerKeyIsPrintableForTest(
428
+ testCase.value === null ? undefined : testCase.value
429
+ );
430
+ case 'toggle_selection':
431
+ return toggleProjectPickerSelectionForTest({
432
+ pathInput: testCase.state.pathInput,
433
+ cursor: testCase.state.cursor,
434
+ highlightIndex: testCase.state.highlightIndex,
435
+ selectedPaths: testCase.state.selectedPaths,
436
+ highlightedPath:
437
+ testCase.state.highlightedPath === null ? undefined : testCase.state.highlightedPath,
438
+ });
439
+ case 'email_picker_choices':
440
+ return signupEmailPickerChoiceNamesForTest(testCase.suggestedEmails);
441
+ case 'useful_email':
442
+ return signupSuggestedEmailIsUsefulForTest(testCase.email);
443
+ case 'spinner_lines':
444
+ return signupSpinnerLinesForTest(testCase.mark, testCase.message, testCase.columns);
445
+ case 'wait_message':
446
+ return signupTaskSuggestionWaitMessageForTest(testCase.projectPath);
447
+ case 'launch_payload':
448
+ return JSON.stringify(signupLaunchPayloadForTest(testCase.state), null, 2);
449
+ case 'project_git_head': {
450
+ clearSignupGitHeadSummaryCacheForTest();
451
+ return orNull(signupProjectGitHeadForPathForTest(testCase.path));
452
+ }
453
+ case 'discover_projects': {
454
+ clearSignupGitHeadSummaryCacheForTest();
455
+ return signupDiscoverProjectsForStartForTest({
456
+ homeDir: testCase.homeDir,
457
+ cwd: testCase.cwd,
458
+ }).map((project) => ({
459
+ path: project.path,
460
+ language: project.language,
461
+ source: project.source,
462
+ gitHead: orNull(project.gitHead),
463
+ }));
464
+ }
465
+ case 'read_task_cache':
466
+ return signupReadTaskCacheForTest(testCase.path);
467
+ case 'resolve_codex_command':
468
+ try {
469
+ return {
470
+ ok: true,
471
+ command: await resolveSignupCodexCommandForTest({
472
+ env: testCase.env,
473
+ homeDir: testCase.homeDir,
474
+ executablePaths: testCase.executablePaths,
475
+ }),
476
+ };
477
+ } catch (error) {
478
+ return { ok: false, message: error instanceof Error ? error.message : String(error) };
479
+ }
480
+ case 'codex_process': {
481
+ const spec = signupCodexTaskSuggestionProcessForTest(testCase.args);
482
+ return {
483
+ command: spec.command,
484
+ args: spec.args,
485
+ cwd: spec.cwd,
486
+ stdin: spec.stdin,
487
+ timeoutMs: spec.timeoutMs,
488
+ env: orNull(spec.env),
489
+ };
490
+ }
491
+ case 'suggest_tasks_cached': {
492
+ const { writer, transcript } = makeFakeWriter(testCase.columns ?? 80);
493
+ const cacheWrites = [];
494
+ const runnerCalls = [];
495
+ const store = {
496
+ read: (key) => {
497
+ runnerCalls.push({ read: key });
498
+ const entry = (testCase.cache ?? {})[key];
499
+ return entry === undefined ? undefined : entry;
500
+ },
501
+ write: (key, tasks) => {
502
+ cacheWrites.push({ key, tasks });
503
+ },
504
+ };
505
+ const tasks = await signupSuggestTasksForProjectForTest({
506
+ project: reviveProjects([testCase.project])[0],
507
+ output: writer,
508
+ taskCacheStore: store,
509
+ codexTaskRunner: async () => testCase.runnerTasks ?? [],
510
+ });
511
+ return { tasks, transcript: transcript(), cacheWrites, runnerCalls };
512
+ }
513
+ case 'flow_walk':
514
+ return handleFlowWalk(testCase);
515
+ case 'picker_walk':
516
+ return handlePickerWalk(testCase);
517
+ default:
518
+ return { threw: true, message: 'unknown op ' + String(testCase.op) };
519
+ }
520
+ }
521
+
522
+ let stdin = '';
523
+ process.stdin.setEncoding('utf8');
524
+ for await (const chunk of process.stdin) {
525
+ stdin += chunk;
526
+ }
527
+ const cases = JSON.parse(stdin);
528
+ const results = [];
529
+ for (const testCase of cases) {
530
+ results.push(await handle(testCase));
531
+ }
532
+ process.stdout.write(JSON.stringify(results));
533
+ `;
534
+
535
+ await mkdir(join(packageRoot, '.differential'), { recursive: true });
536
+
537
+ // Renamed banner import (the build-auth-parity-oracle.mjs convention): CJS
538
+ // transitive deps (@inquirer's yoctocolors) require() node builtins at
539
+ // module scope, and a bare `createRequire` name would collide with modules
540
+ // that import it themselves.
541
+ const banner = {
542
+ js: "import { createRequire as __onboardingFlowOracleCreateRequire } from 'node:module';\nconst require = __onboardingFlowOracleCreateRequire(import.meta.url);",
543
+ };
544
+
545
+ await build({
546
+ banner,
547
+ stdin: {
548
+ contents: driverSource,
549
+ resolveDir: packageRoot,
550
+ sourcefile: 'onboarding-flow-parity-driver.ts',
551
+ loader: 'ts',
552
+ },
553
+ bundle: true,
554
+ platform: 'node',
555
+ target: 'node20',
556
+ format: 'esm',
557
+ outfile: join(packageRoot, '.differential/onboarding-flow-parity-oracle.mjs'),
558
+ minify: false,
559
+ sourcemap: false,
560
+ legalComments: 'none',
561
+ external: ['ws', 'undici', 'blessed', '@anthropic-ai/claude-agent-sdk'],
562
+ });