@tarunspandit/codexflow 0.29.0

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.
Files changed (92) hide show
  1. package/AGENTS.example.md +18 -0
  2. package/CHANGELOG.md +311 -0
  3. package/CHATGPT_PROMPT.md +12 -0
  4. package/CODEX_PROMPT.md +12 -0
  5. package/CONTRIBUTING.md +51 -0
  6. package/DOMAIN_SETUP.md +241 -0
  7. package/FAQ.md +327 -0
  8. package/FAQ_ZH.md +279 -0
  9. package/LICENSE +21 -0
  10. package/NOTICE +9 -0
  11. package/PUBLIC_LAUNCH_CHECKLIST.md +111 -0
  12. package/README.md +287 -0
  13. package/README_ZH.md +461 -0
  14. package/SECURITY.md +145 -0
  15. package/config.example.env +31 -0
  16. package/dist/analysis/cache.js +27 -0
  17. package/dist/analysis/cache.js.map +1 -0
  18. package/dist/analysis/classify.js +102 -0
  19. package/dist/analysis/classify.js.map +1 -0
  20. package/dist/analysis/extract.js +139 -0
  21. package/dist/analysis/extract.js.map +1 -0
  22. package/dist/analysis/graph.js +22 -0
  23. package/dist/analysis/graph.js.map +1 -0
  24. package/dist/analysis/impact.js +167 -0
  25. package/dist/analysis/impact.js.map +1 -0
  26. package/dist/analysis/index.js +215 -0
  27. package/dist/analysis/index.js.map +1 -0
  28. package/dist/analysis/inventory.js +51 -0
  29. package/dist/analysis/inventory.js.map +1 -0
  30. package/dist/analysis/providers.js +24 -0
  31. package/dist/analysis/providers.js.map +1 -0
  32. package/dist/analysis/rank.js +27 -0
  33. package/dist/analysis/rank.js.map +1 -0
  34. package/dist/analysis/types.js +8 -0
  35. package/dist/analysis/types.js.map +1 -0
  36. package/dist/bashOps.js +233 -0
  37. package/dist/bashOps.js.map +1 -0
  38. package/dist/capabilitiesOps.js +365 -0
  39. package/dist/capabilitiesOps.js.map +1 -0
  40. package/dist/codexSessions.js +379 -0
  41. package/dist/codexSessions.js.map +1 -0
  42. package/dist/config.js +289 -0
  43. package/dist/config.js.map +1 -0
  44. package/dist/fsOps.js +286 -0
  45. package/dist/fsOps.js.map +1 -0
  46. package/dist/gitOps.js +79 -0
  47. package/dist/gitOps.js.map +1 -0
  48. package/dist/guard.js +198 -0
  49. package/dist/guard.js.map +1 -0
  50. package/dist/http.js +1671 -0
  51. package/dist/http.js.map +1 -0
  52. package/dist/proContext.js +274 -0
  53. package/dist/proContext.js.map +1 -0
  54. package/dist/profileStore.js +89 -0
  55. package/dist/profileStore.js.map +1 -0
  56. package/dist/projectCatalog.js +134 -0
  57. package/dist/projectCatalog.js.map +1 -0
  58. package/dist/redact.js +73 -0
  59. package/dist/redact.js.map +1 -0
  60. package/dist/searchOps.js +186 -0
  61. package/dist/searchOps.js.map +1 -0
  62. package/dist/server.js +2502 -0
  63. package/dist/server.js.map +1 -0
  64. package/dist/stdio.js +36 -0
  65. package/dist/stdio.js.map +1 -0
  66. package/dist/toolCardWidget.js +1155 -0
  67. package/dist/toolCardWidget.js.map +1 -0
  68. package/dist/workspaceOps.js +229 -0
  69. package/dist/workspaceOps.js.map +1 -0
  70. package/docs/.nojekyll +1 -0
  71. package/docs/favicon.svg +5 -0
  72. package/docs/index.html +638 -0
  73. package/docs/og.png +0 -0
  74. package/docs/script.js +80 -0
  75. package/docs/star.svg +11 -0
  76. package/docs/styles.css +1229 -0
  77. package/docs/zh.html +436 -0
  78. package/package.json +94 -0
  79. package/scripts/analysis-cli-smoke.mjs +81 -0
  80. package/scripts/analysis-smoke.mjs +179 -0
  81. package/scripts/clean.mjs +6 -0
  82. package/scripts/cli-smoke.mjs +168 -0
  83. package/scripts/codexflow.mjs +4375 -0
  84. package/scripts/doctor-smoke.mjs +90 -0
  85. package/scripts/execute-handoff-smoke.mjs +1110 -0
  86. package/scripts/http-smoke.mjs +812 -0
  87. package/scripts/pro-apply.mjs +141 -0
  88. package/scripts/pro-bundle.mjs +121 -0
  89. package/scripts/pro-smoke.mjs +95 -0
  90. package/scripts/settings-smoke.mjs +756 -0
  91. package/scripts/smoke.mjs +1194 -0
  92. package/scripts/stress.mjs +835 -0
@@ -0,0 +1,179 @@
1
+ import assert from 'node:assert/strict';
2
+ import fs from 'node:fs/promises';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+
7
+ const projectRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
8
+ const importBuilt = (relativePath) => import(pathToFileURL(path.join(projectRoot, 'dist', relativePath)).href);
9
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-analysis-'));
10
+ const outside = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-analysis-outside-'));
11
+
12
+ async function write(relativePath, content) {
13
+ const target = path.join(tmp, relativePath);
14
+ await fs.mkdir(path.dirname(target), { recursive: true });
15
+ await fs.writeFile(target, content, 'utf8');
16
+ }
17
+
18
+ try {
19
+ await write('package.json', JSON.stringify({ name: 'fixture', packageManager: 'pnpm@9.15.0', scripts: { test: 'node --test' } }, null, 2));
20
+ await write('src/index.ts', "import { authenticate } from './auth.js';\nexport const ready = authenticate('demo');\n");
21
+ await write('src/auth.ts', 'export function authenticate(user: string) { return Boolean(user); }\n');
22
+ await write('src/race.ts', 'export function temporaryFile() { return true; }\n');
23
+ await write('test/auth.test.ts', "import { authenticate } from '../src/auth.js';\nvoid authenticate('test');\n");
24
+ await write('README.md', '# Fixture\n');
25
+ await write('.env', 'PRIVATE_TOKEN=never-visible\n');
26
+ await write('python/service.py', 'def load_user(user_id):\n return user_id\n');
27
+ await write('go/service.go', 'package service\nfunc LoadUser(id string) string { return id }\n');
28
+ await write('go.mod', 'module example.com/fixture\n\ngo 1.24\n');
29
+ await write('rust/service.rs', 'pub fn load_user(id: &str) -> &str { id }\n');
30
+ await write('swift/Service.swift', 'public func loadUser(_ id: String) -> String { id }\n');
31
+ await write('java/Service.java', 'public class Service { }\n');
32
+ await write('csharp/Service.cs', 'public class Service { }\n');
33
+ await write('c/service.c', 'int load_user(int id) { return id; }\n');
34
+ await write('cpp/service.cpp', 'class Service { };\n');
35
+ await write('notes/many.txt', Array.from({ length: 20 }, (_, index) => `common marker ${index}`).join('\n') + '\n');
36
+ await write('unknown/service.zig', 'pub fn loadUser() void {}\n');
37
+ await write('packages/core/package.json', JSON.stringify({ name: '@fixture/core' }, null, 2));
38
+ await write('packages/core/src/index.ts', 'export function coreValue() { return 1; }\n');
39
+ await write('packages/web/package.json', JSON.stringify({ name: '@fixture/web', dependencies: { '@fixture/core': 'workspace:*' } }, null, 2));
40
+ await write('packages/web/src/index.ts', "import { coreValue } from '../../core/src/index.js';\nexport const webValue = coreValue();\n");
41
+ await fs.writeFile(path.join(outside, 'outside.ts'), 'export const outside = true;\n', 'utf8');
42
+ let symlinkCreated = false;
43
+ try {
44
+ await fs.symlink(outside, path.join(tmp, 'outside-link'), 'dir');
45
+ symlinkCreated = true;
46
+ } catch (error) {
47
+ if (process.platform !== 'win32' || error?.code !== 'EPERM') throw error;
48
+ }
49
+
50
+ const [{ loadConfig }, { PathGuard, WorkspaceManager }, { inventoryWorkspace }, { extractWorkspaceFiles }, classification, analysisApi] = await Promise.all([
51
+ importBuilt('config.js'),
52
+ importBuilt('guard.js'),
53
+ importBuilt('analysis/inventory.js'),
54
+ importBuilt('analysis/extract.js'),
55
+ importBuilt('analysis/classify.js'),
56
+ importBuilt('analysis/index.js')
57
+ ]);
58
+ const config = loadConfig(['--root', tmp, '--bash', 'off', '--write', 'off']);
59
+ const guard = new PathGuard(config);
60
+ const workspace = new WorkspaceManager(config).defaultWorkspace();
61
+ const result = await inventoryWorkspace(config, guard, workspace);
62
+
63
+ assert(result.files.some((file) => file.path === 'src/index.ts' && file.language === 'typescript' && file.role === 'source'));
64
+ assert(result.files.some((file) => file.path === 'test/auth.test.ts' && file.role === 'test'));
65
+ assert(!result.files.some((file) => file.path === '.env'));
66
+ assert(classification.detectProjectTypes(result.files).includes('node'));
67
+ assert(result.files.some((file) => file.path === 'src/index.ts' && file.entrypoint === true));
68
+ assert.equal(result.coverage.inventoryFiles, result.files.length);
69
+ assert.match(result.fingerprint, /^[a-f0-9]{64}$/);
70
+ assert(result.files.some((file) => file.path === 'unknown/service.zig' && file.language === 'unknown'));
71
+ if (symlinkCreated) assert(!result.files.some((file) => file.path.startsWith('outside-link/')));
72
+ await fs.rm(path.join(tmp, 'src', 'race.ts'));
73
+ const changedDuringScan = await extractWorkspaceFiles(config, guard, workspace, result.files);
74
+ assert.equal(changedDuringScan.truncated, true);
75
+ assert(changedDuringScan.warnings.some((warning) => warning.includes('changed or became unreadable')));
76
+
77
+ const analysis = await analysisApi.inspectWorkspace(config, guard, workspace);
78
+ assert(analysis.symbols.some((symbol) => symbol.name === 'authenticate' && symbol.kind === 'function' && symbol.path === 'src/auth.ts'));
79
+ assert(analysis.relationships.some((edge) => edge.from === 'src/index.ts' && edge.to === 'src/auth.ts' && edge.kind === 'imports'));
80
+ assert(analysis.relationships.some((edge) => edge.from === 'packages/web/src/index.ts' && edge.to === 'packages/core/src/index.ts' && edge.kind === 'imports'));
81
+
82
+ const expectedSymbols = [
83
+ ['python/service.py', 'load_user'],
84
+ ['go/service.go', 'LoadUser'],
85
+ ['rust/service.rs', 'load_user'],
86
+ ['swift/Service.swift', 'loadUser'],
87
+ ['java/Service.java', 'Service'],
88
+ ['csharp/Service.cs', 'Service'],
89
+ ['c/service.c', 'load_user'],
90
+ ['cpp/service.cpp', 'Service']
91
+ ];
92
+ for (const [file, name] of expectedSymbols) {
93
+ assert(analysis.symbols.some((symbol) => symbol.path === file && symbol.name === name), `missing ${name} in ${file}`);
94
+ }
95
+
96
+ const structured = await analysisApi.searchWorkspaceStructured(config, guard, workspace, {
97
+ query: 'authenticate',
98
+ intent: 'symbol',
99
+ includeTests: true
100
+ });
101
+ assert.equal(structured.groups.definitions[0]?.path, 'src/auth.ts');
102
+ assert(structured.groups.tests.some((match) => match.path === 'test/auth.test.ts'));
103
+ assert(structured.groups.definitions[0].reasons.includes('symbol definition'));
104
+ const impactSearch = await analysisApi.searchWorkspaceStructured(config, guard, workspace, {
105
+ query: 'authenticate',
106
+ intent: 'impact',
107
+ includeTests: true
108
+ });
109
+ assert(impactSearch.groups.references.some((match) => match.path === 'src/index.ts' && match.reasons.includes('dependent module')));
110
+ assert(impactSearch.groups.tests.some((match) => match.path === 'test/auth.test.ts' && match.reasons.includes('dependent test')));
111
+
112
+ const cached = await analysisApi.inspectWorkspace(config, guard, workspace);
113
+ assert.equal(cached.cache.hit, true);
114
+ await fs.appendFile(path.join(tmp, 'src/auth.ts'), 'export function authorize() { return true; }\n', 'utf8');
115
+ analysisApi.invalidateWorkspaceAnalysis(workspace.id);
116
+ const refreshed = await analysisApi.inspectWorkspace(config, guard, workspace);
117
+ assert.equal(refreshed.cache.hit, false);
118
+ assert(refreshed.symbols.some((symbol) => symbol.name === 'authorize'));
119
+
120
+ const review = await analysisApi.reviewWorkspaceChanges(config, guard, workspace, { changedPaths: ['src/auth.ts'] });
121
+ assert(review.affectedAreas.includes('src'));
122
+ assert(review.dependentFiles.some((file) => file.path === 'src/index.ts'));
123
+ assert(review.relatedTests.some((file) => file.path === 'test/auth.test.ts'));
124
+ assert(review.riskSignals.some((risk) => risk.id === 'authentication'));
125
+ assert(review.recommendedCommands.some((item) => item.command === 'pnpm test' && item.source === 'package.json'));
126
+ assert(review.recommendedCommands.some((item) => item.command === 'go test ./...' && item.source === 'go.mod'));
127
+
128
+ const symbolLimitedConfig = {
129
+ ...config,
130
+ analysisLimits: { ...config.analysisLimits, maxSymbols: 2 }
131
+ };
132
+ analysisApi.invalidateWorkspaceAnalysis(workspace.id);
133
+ const symbolLimited = await analysisApi.inspectWorkspace(symbolLimitedConfig, guard, workspace);
134
+ assert.equal(symbolLimited.symbols.length, 2);
135
+ assert.equal(symbolLimited.coverage.truncated, true);
136
+ assert(symbolLimited.warnings.some((warning) => warning.includes('Symbol extraction reached')));
137
+
138
+ const searchLimitedConfig = {
139
+ ...config,
140
+ analysisLimits: { ...config.analysisLimits, maxAnalyzedFiles: 1 }
141
+ };
142
+ analysisApi.invalidateWorkspaceAnalysis(workspace.id);
143
+ const searchLimited = await analysisApi.searchWorkspaceStructured(searchLimitedConfig, guard, workspace, {
144
+ query: 'authenticate',
145
+ intent: 'symbol',
146
+ includeTests: true
147
+ });
148
+ assert.equal(searchLimited.coverage.truncated, true);
149
+ assert(searchLimited.warnings.some((warning) => warning.includes('Grouped search reached')));
150
+
151
+ const scoped = await analysisApi.searchWorkspaceStructured(config, guard, workspace, {
152
+ query: 'coreValue',
153
+ intent: 'symbol',
154
+ root: 'src'
155
+ });
156
+ assert.equal(scoped.matches.length, 0);
157
+
158
+ const unsupportedRegex = await analysisApi.searchWorkspaceStructured(config, guard, workspace, {
159
+ query: '(?i)authenticate',
160
+ intent: 'text',
161
+ regex: true
162
+ });
163
+ assert.equal(unsupportedRegex.matches.length, 0);
164
+ assert(unsupportedRegex.warnings.some((warning) => warning.includes('regular expression')));
165
+
166
+ const candidateLimited = await analysisApi.searchWorkspaceStructured(config, guard, workspace, {
167
+ query: 'common marker',
168
+ intent: 'text',
169
+ maxResults: 2
170
+ });
171
+ assert.equal(candidateLimited.matches.length, 2);
172
+ assert.equal(candidateLimited.coverage.truncated, true);
173
+ assert(candidateLimited.warnings.some((warning) => warning.includes('retained the first 8 candidates')));
174
+
175
+ console.log('✓ analysis smoke test passed');
176
+ } finally {
177
+ await fs.rm(tmp, { recursive: true, force: true });
178
+ await fs.rm(outside, { recursive: true, force: true });
179
+ }
@@ -0,0 +1,6 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
6
+ fs.rmSync(path.join(root, "dist"), { recursive: true, force: true });
@@ -0,0 +1,168 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawn, spawnSync } from 'node:child_process';
3
+ import fs from 'node:fs/promises';
4
+ import net from 'node:net';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+
8
+ const projectRoot = path.resolve('.');
9
+
10
+ function run(args, env) {
11
+ return spawnSync(process.execPath, ['scripts/codexflow.mjs', ...args], {
12
+ cwd: projectRoot,
13
+ env,
14
+ encoding: 'utf8',
15
+ maxBuffer: 2 * 1024 * 1024
16
+ });
17
+ }
18
+
19
+ async function getFreePort() {
20
+ return new Promise((resolve, reject) => {
21
+ const server = net.createServer();
22
+ server.once('error', reject);
23
+ server.listen(0, '127.0.0.1', () => {
24
+ const address = server.address();
25
+ const port = typeof address === 'object' && address ? address.port : undefined;
26
+ server.close(() => (port ? resolve(port) : reject(new Error('no free port'))));
27
+ });
28
+ });
29
+ }
30
+
31
+ async function waitForExit(child, timeoutMs = 5000) {
32
+ if (child.exitCode !== null) return;
33
+ await new Promise((resolve, reject) => {
34
+ const timer = setTimeout(() => reject(new Error('timed out waiting for launcher exit')), timeoutMs);
35
+ child.once('exit', () => {
36
+ clearTimeout(timer);
37
+ resolve();
38
+ });
39
+ });
40
+ }
41
+
42
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-cli-smoke-root-'));
43
+ const home = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-cli-smoke-home-'));
44
+ const env = {
45
+ ...process.env,
46
+ CODEXFLOW_HOME: home
47
+ };
48
+
49
+ const codexDir = path.join(home, 'codex');
50
+ const sessionDir = path.join(codexDir, 'sessions', '2026', '07', '12');
51
+ const autoProjectA = path.join(home, 'project-a');
52
+ const autoProjectB = path.join(home, 'project-b');
53
+ await Promise.all([
54
+ fs.mkdir(sessionDir, { recursive: true }),
55
+ fs.mkdir(autoProjectA, { recursive: true }),
56
+ fs.mkdir(autoProjectB, { recursive: true })
57
+ ]);
58
+ await fs.writeFile(path.join(sessionDir, 'rollout-2026-07-12T01-00-00-11111111-1111-4111-8111-111111111111.jsonl'),
59
+ `${JSON.stringify({ timestamp: '2026-07-12T01:00:00Z', type: 'session_meta', payload: { id: '11111111-1111-4111-8111-111111111111', cwd: autoProjectA } })}\n`);
60
+ await fs.writeFile(path.join(sessionDir, 'rollout-2026-07-12T02-00-00-22222222-2222-4222-8222-222222222222.jsonl'),
61
+ `${JSON.stringify({ timestamp: '2026-07-12T02:00:00Z', type: 'session_meta', payload: { id: '22222222-2222-4222-8222-222222222222', cwd: autoProjectB } })}\n`);
62
+ env.CODEXFLOW_CODEX_DIR = codexDir;
63
+
64
+ const autoPort = await getFreePort();
65
+ const autoChild = spawn(process.execPath, [
66
+ 'scripts/codexflow.mjs',
67
+ '--port', String(autoPort),
68
+ '--tunnel', 'none',
69
+ '--no-auth',
70
+ '--no-copy-url',
71
+ '--non-interactive'
72
+ ], { cwd: projectRoot, env, stdio: ['ignore', 'pipe', 'pipe'] });
73
+ let autoOutput = '';
74
+ autoChild.stdout.on('data', (chunk) => { autoOutput += String(chunk); });
75
+ autoChild.stderr.on('data', (chunk) => { autoOutput += String(chunk); });
76
+ let autoHealth;
77
+ const autoDeadline = Date.now() + 15000;
78
+ while (Date.now() < autoDeadline) {
79
+ try {
80
+ const response = await fetch(`http://127.0.0.1:${autoPort}/healthz`);
81
+ if (response.ok) {
82
+ autoHealth = await response.json();
83
+ break;
84
+ }
85
+ } catch {
86
+ // The zero-setup launcher may still be starting.
87
+ }
88
+ await new Promise((resolve) => setTimeout(resolve, 100));
89
+ }
90
+ try {
91
+ assert.equal(autoHealth?.defaultRoot, await fs.realpath(autoProjectB), `${autoOutput}\nzero-setup launch did not choose the most recent Codex project`);
92
+ assert.ok(autoHealth.allowedRoots.includes(await fs.realpath(autoProjectA)), 'zero-setup launch did not include project A');
93
+ assert.ok(autoHealth.allowedRoots.includes(await fs.realpath(autoProjectB)), 'zero-setup launch did not include project B');
94
+ assert.match(autoOutput, /Projects found\s+2/);
95
+ assert.doesNotMatch(autoOutput, /Where is your project located|First run setup|Save this setup/);
96
+ } finally {
97
+ if (autoChild.exitCode === null) autoChild.kill('SIGTERM');
98
+ await waitForExit(autoChild);
99
+ }
100
+
101
+ const statusBefore = run(['status', '--root', root, '--json'], env);
102
+ assert.equal(statusBefore.status, 1, statusBefore.stderr || statusBefore.stdout);
103
+ const beforePayload = JSON.parse(statusBefore.stdout);
104
+ assert.equal(beforePayload.state, 'not_running');
105
+ assert.equal(beforePayload.active, false);
106
+
107
+ const port = await getFreePort();
108
+ const child = spawn(process.execPath, [
109
+ 'scripts/codexflow.mjs',
110
+ 'start',
111
+ '--root', root,
112
+ '--port', String(port),
113
+ '--tunnel', 'none',
114
+ '--no-auth',
115
+ '--no-copy-url',
116
+ '--non-interactive'
117
+ ], {
118
+ cwd: projectRoot,
119
+ env,
120
+ stdio: ['ignore', 'pipe', 'pipe']
121
+ });
122
+ let output = '';
123
+ child.stdout.on('data', (chunk) => { output += String(chunk); });
124
+ child.stderr.on('data', (chunk) => { output += String(chunk); });
125
+
126
+ const runtimePath = path.join(home, 'runtime');
127
+ let runtimeFile = '';
128
+ const runtimeDeadline = Date.now() + 15000;
129
+ let runtime;
130
+ while (Date.now() < runtimeDeadline) {
131
+ try {
132
+ const files = (await fs.readdir(runtimePath)).filter((name) => name.endsWith('.json'));
133
+ if (files.length) {
134
+ runtimeFile = path.join(runtimePath, files[0]);
135
+ runtime = JSON.parse(await fs.readFile(runtimeFile, 'utf8'));
136
+ if (runtime.pid === child.pid) break;
137
+ }
138
+ } catch {
139
+ // The launcher may not have created its runtime directory yet.
140
+ }
141
+ await new Promise((resolve) => setTimeout(resolve, 100));
142
+ }
143
+
144
+ try {
145
+ assert.equal(runtime?.pid, child.pid, `${output}\nlauncher did not write an active runtime record`);
146
+ assert.match(output, /Non-interactive mode/);
147
+
148
+ const statusDuring = run(['status', '--root', root, '--json'], env);
149
+ assert.equal(statusDuring.status, 0, statusDuring.stderr || statusDuring.stdout);
150
+ const duringPayload = JSON.parse(statusDuring.stdout);
151
+ assert.equal(duringPayload.state, 'active');
152
+ assert.equal(duringPayload.active, true);
153
+ assert.equal(duringPayload.health.status, 'ok');
154
+ assert.equal(duringPayload.runtime.pid, child.pid);
155
+ assert.match(duringPayload.runtime.local_base, /^http:\/\/127\.0\.0\.1:/);
156
+ } finally {
157
+ if (child.exitCode === null) child.kill('SIGTERM');
158
+ try {
159
+ await waitForExit(child);
160
+ } catch {
161
+ child.kill('SIGKILL');
162
+ }
163
+ if (runtimeFile) await fs.rm(runtimeFile, { force: true });
164
+ await fs.rm(root, { recursive: true, force: true });
165
+ await fs.rm(home, { recursive: true, force: true });
166
+ }
167
+
168
+ console.log('✓ CLI status and non-interactive smoke test passed');