@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.
- package/AGENTS.example.md +18 -0
- package/CHANGELOG.md +311 -0
- package/CHATGPT_PROMPT.md +12 -0
- package/CODEX_PROMPT.md +12 -0
- package/CONTRIBUTING.md +51 -0
- package/DOMAIN_SETUP.md +241 -0
- package/FAQ.md +327 -0
- package/FAQ_ZH.md +279 -0
- package/LICENSE +21 -0
- package/NOTICE +9 -0
- package/PUBLIC_LAUNCH_CHECKLIST.md +111 -0
- package/README.md +287 -0
- package/README_ZH.md +461 -0
- package/SECURITY.md +145 -0
- package/config.example.env +31 -0
- package/dist/analysis/cache.js +27 -0
- package/dist/analysis/cache.js.map +1 -0
- package/dist/analysis/classify.js +102 -0
- package/dist/analysis/classify.js.map +1 -0
- package/dist/analysis/extract.js +139 -0
- package/dist/analysis/extract.js.map +1 -0
- package/dist/analysis/graph.js +22 -0
- package/dist/analysis/graph.js.map +1 -0
- package/dist/analysis/impact.js +167 -0
- package/dist/analysis/impact.js.map +1 -0
- package/dist/analysis/index.js +215 -0
- package/dist/analysis/index.js.map +1 -0
- package/dist/analysis/inventory.js +51 -0
- package/dist/analysis/inventory.js.map +1 -0
- package/dist/analysis/providers.js +24 -0
- package/dist/analysis/providers.js.map +1 -0
- package/dist/analysis/rank.js +27 -0
- package/dist/analysis/rank.js.map +1 -0
- package/dist/analysis/types.js +8 -0
- package/dist/analysis/types.js.map +1 -0
- package/dist/bashOps.js +233 -0
- package/dist/bashOps.js.map +1 -0
- package/dist/capabilitiesOps.js +365 -0
- package/dist/capabilitiesOps.js.map +1 -0
- package/dist/codexSessions.js +379 -0
- package/dist/codexSessions.js.map +1 -0
- package/dist/config.js +289 -0
- package/dist/config.js.map +1 -0
- package/dist/fsOps.js +286 -0
- package/dist/fsOps.js.map +1 -0
- package/dist/gitOps.js +79 -0
- package/dist/gitOps.js.map +1 -0
- package/dist/guard.js +198 -0
- package/dist/guard.js.map +1 -0
- package/dist/http.js +1671 -0
- package/dist/http.js.map +1 -0
- package/dist/proContext.js +274 -0
- package/dist/proContext.js.map +1 -0
- package/dist/profileStore.js +89 -0
- package/dist/profileStore.js.map +1 -0
- package/dist/projectCatalog.js +134 -0
- package/dist/projectCatalog.js.map +1 -0
- package/dist/redact.js +73 -0
- package/dist/redact.js.map +1 -0
- package/dist/searchOps.js +186 -0
- package/dist/searchOps.js.map +1 -0
- package/dist/server.js +2502 -0
- package/dist/server.js.map +1 -0
- package/dist/stdio.js +36 -0
- package/dist/stdio.js.map +1 -0
- package/dist/toolCardWidget.js +1155 -0
- package/dist/toolCardWidget.js.map +1 -0
- package/dist/workspaceOps.js +229 -0
- package/dist/workspaceOps.js.map +1 -0
- package/docs/.nojekyll +1 -0
- package/docs/favicon.svg +5 -0
- package/docs/index.html +638 -0
- package/docs/og.png +0 -0
- package/docs/script.js +80 -0
- package/docs/star.svg +11 -0
- package/docs/styles.css +1229 -0
- package/docs/zh.html +436 -0
- package/package.json +94 -0
- package/scripts/analysis-cli-smoke.mjs +81 -0
- package/scripts/analysis-smoke.mjs +179 -0
- package/scripts/clean.mjs +6 -0
- package/scripts/cli-smoke.mjs +168 -0
- package/scripts/codexflow.mjs +4375 -0
- package/scripts/doctor-smoke.mjs +90 -0
- package/scripts/execute-handoff-smoke.mjs +1110 -0
- package/scripts/http-smoke.mjs +812 -0
- package/scripts/pro-apply.mjs +141 -0
- package/scripts/pro-bundle.mjs +121 -0
- package/scripts/pro-smoke.mjs +95 -0
- package/scripts/settings-smoke.mjs +756 -0
- package/scripts/smoke.mjs +1194 -0
- package/scripts/stress.mjs +835 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
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
|
+
function run(args, env) {
|
|
9
|
+
const result = spawnSync(process.execPath, ['scripts/codexflow.mjs', ...args], {
|
|
10
|
+
cwd: path.resolve('.'),
|
|
11
|
+
env,
|
|
12
|
+
encoding: 'utf8'
|
|
13
|
+
});
|
|
14
|
+
if (result.status !== 0) {
|
|
15
|
+
throw new Error(`codexflow ${args.join(' ')} failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
|
|
16
|
+
}
|
|
17
|
+
return `${result.stdout}\n${result.stderr}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function runFail(args, env, pattern) {
|
|
21
|
+
const result = spawnSync(process.execPath, ['scripts/codexflow.mjs', ...args], {
|
|
22
|
+
cwd: path.resolve('.'),
|
|
23
|
+
env,
|
|
24
|
+
encoding: 'utf8'
|
|
25
|
+
});
|
|
26
|
+
if (result.status === 0) {
|
|
27
|
+
throw new Error(`codexflow ${args.join(' ')} unexpectedly succeeded\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
|
|
28
|
+
}
|
|
29
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
30
|
+
if (pattern && !pattern.test(output)) {
|
|
31
|
+
throw new Error(`codexflow ${args.join(' ')} failed for the wrong reason\n${output}`);
|
|
32
|
+
}
|
|
33
|
+
return output;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function readProfile(root, home) {
|
|
37
|
+
const realRoot = await fs.realpath(root);
|
|
38
|
+
const id = createHash('sha256').update(realRoot).digest('hex').slice(0, 24);
|
|
39
|
+
return JSON.parse(await fs.readFile(path.join(home, 'profiles', `${id}.json`), 'utf8'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function runtimeStatusPath(root, home) {
|
|
43
|
+
const realRoot = await fs.realpath(root);
|
|
44
|
+
const id = createHash('sha256').update(realRoot).digest('hex').slice(0, 24);
|
|
45
|
+
return path.join(home, 'runtime', `${id}.json`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function getFreePort() {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const server = net.createServer();
|
|
51
|
+
server.listen(0, '127.0.0.1', () => {
|
|
52
|
+
const address = server.address();
|
|
53
|
+
const port = typeof address === 'object' && address ? address.port : undefined;
|
|
54
|
+
server.close(() => (port ? resolve(port) : reject(new Error('no free port'))));
|
|
55
|
+
});
|
|
56
|
+
server.on('error', reject);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function waitForJson(filePath, predicate, label) {
|
|
61
|
+
const deadline = Date.now() + 10_000;
|
|
62
|
+
let lastError;
|
|
63
|
+
while (Date.now() < deadline) {
|
|
64
|
+
try {
|
|
65
|
+
const data = JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
66
|
+
if (predicate(data)) return data;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
lastError = error;
|
|
69
|
+
}
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`timed out waiting for ${label}: ${lastError?.message ?? 'predicate not met'}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function withStartedCodexFlow(args, env, fn) {
|
|
76
|
+
const child = spawn(process.execPath, ['scripts/codexflow.mjs', 'start', ...args], {
|
|
77
|
+
cwd: path.resolve('.'),
|
|
78
|
+
env,
|
|
79
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
80
|
+
});
|
|
81
|
+
let output = '';
|
|
82
|
+
let closed = false;
|
|
83
|
+
const closedPromise = new Promise((resolve) => child.once('close', (code, signal) => {
|
|
84
|
+
closed = true;
|
|
85
|
+
resolve({ code, signal });
|
|
86
|
+
}));
|
|
87
|
+
child.stdout.on('data', (chunk) => { output += chunk; });
|
|
88
|
+
child.stderr.on('data', (chunk) => { output += chunk; });
|
|
89
|
+
try {
|
|
90
|
+
await fn(child);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw new Error(`${error.message}\nstart output:\n${output}`);
|
|
93
|
+
} finally {
|
|
94
|
+
if (!closed) child.kill('SIGTERM');
|
|
95
|
+
await closedPromise;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function findPythonForPty() {
|
|
100
|
+
if (process.platform === 'win32') return '';
|
|
101
|
+
for (const command of ['python3', 'python']) {
|
|
102
|
+
const result = spawnSync(command, ['-c', 'import pty, select, subprocess'], { stdio: 'ignore' });
|
|
103
|
+
if (result.status === 0) return command;
|
|
104
|
+
}
|
|
105
|
+
return '';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function runInteractiveQuit(args, env) {
|
|
109
|
+
const python = findPythonForPty();
|
|
110
|
+
if (!python) return false;
|
|
111
|
+
const payload = JSON.stringify({
|
|
112
|
+
cmd: process.execPath,
|
|
113
|
+
args: ['scripts/codexflow.mjs', 'start', ...args],
|
|
114
|
+
cwd: path.resolve('.')
|
|
115
|
+
});
|
|
116
|
+
const code = `
|
|
117
|
+
import json, os, pty, select, subprocess, sys, time
|
|
118
|
+
payload = json.loads(sys.argv[1])
|
|
119
|
+
master, slave = pty.openpty()
|
|
120
|
+
proc = subprocess.Popen([payload["cmd"]] + payload["args"], cwd=payload["cwd"], env=os.environ.copy(), stdin=slave, stdout=slave, stderr=slave, close_fds=True)
|
|
121
|
+
os.close(slave)
|
|
122
|
+
out = bytearray()
|
|
123
|
+
sent = False
|
|
124
|
+
deadline = time.time() + 20
|
|
125
|
+
while time.time() < deadline:
|
|
126
|
+
if proc.poll() is not None:
|
|
127
|
+
break
|
|
128
|
+
ready, _, _ = select.select([master], [], [], 0.1)
|
|
129
|
+
if not ready:
|
|
130
|
+
continue
|
|
131
|
+
try:
|
|
132
|
+
chunk = os.read(master, 4096)
|
|
133
|
+
except OSError:
|
|
134
|
+
break
|
|
135
|
+
if not chunk:
|
|
136
|
+
break
|
|
137
|
+
out.extend(chunk)
|
|
138
|
+
if not sent and b"codexflow> " in out:
|
|
139
|
+
os.write(master, b"q")
|
|
140
|
+
sent = True
|
|
141
|
+
if proc.poll() is None:
|
|
142
|
+
try:
|
|
143
|
+
proc.wait(timeout=2)
|
|
144
|
+
except subprocess.TimeoutExpired:
|
|
145
|
+
pass
|
|
146
|
+
if proc.poll() is None:
|
|
147
|
+
proc.terminate()
|
|
148
|
+
try:
|
|
149
|
+
proc.wait(timeout=2)
|
|
150
|
+
except subprocess.TimeoutExpired:
|
|
151
|
+
proc.kill()
|
|
152
|
+
proc.wait()
|
|
153
|
+
sys.stderr.write(out.decode(errors="replace"))
|
|
154
|
+
raise SystemExit(124)
|
|
155
|
+
while True:
|
|
156
|
+
ready, _, _ = select.select([master], [], [], 0)
|
|
157
|
+
if not ready:
|
|
158
|
+
break
|
|
159
|
+
try:
|
|
160
|
+
chunk = os.read(master, 4096)
|
|
161
|
+
except OSError:
|
|
162
|
+
break
|
|
163
|
+
if not chunk:
|
|
164
|
+
break
|
|
165
|
+
out.extend(chunk)
|
|
166
|
+
os.close(master)
|
|
167
|
+
sys.stdout.write(out.decode(errors="replace"))
|
|
168
|
+
if not sent:
|
|
169
|
+
sys.stderr.write("control prompt was not reached\\n")
|
|
170
|
+
raise SystemExit(125)
|
|
171
|
+
raise SystemExit(proc.returncode or 0)
|
|
172
|
+
`;
|
|
173
|
+
const result = spawnSync(python, ['-c', code, payload], {
|
|
174
|
+
cwd: path.resolve('.'),
|
|
175
|
+
env: { ...env, NO_COLOR: '1' },
|
|
176
|
+
encoding: 'utf8',
|
|
177
|
+
maxBuffer: 1024 * 1024
|
|
178
|
+
});
|
|
179
|
+
if (result.status !== 0) {
|
|
180
|
+
throw new Error(`interactive quit failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-settings-root-'));
|
|
186
|
+
const reuseRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-settings-reuse-'));
|
|
187
|
+
const policyRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-settings-policy-'));
|
|
188
|
+
const runtimeRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-settings-runtime-'));
|
|
189
|
+
const staleRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-settings-stale-'));
|
|
190
|
+
const ngrokRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-settings-ngrok-'));
|
|
191
|
+
const home = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-settings-home-'));
|
|
192
|
+
const env = { ...process.env, CODEXFLOW_HOME: home };
|
|
193
|
+
function withoutProxyEnv(input) {
|
|
194
|
+
const next = { ...input };
|
|
195
|
+
for (const key of ['HTTPS_PROXY', 'https_proxy', 'ALL_PROXY', 'all_proxy', 'HTTP_PROXY', 'http_proxy']) delete next[key];
|
|
196
|
+
return next;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const empty = run(['settings', 'show', '--root', root], env);
|
|
200
|
+
if (!empty.includes('No saved settings')) {
|
|
201
|
+
throw new Error(`expected empty settings output, got:\n${empty}`);
|
|
202
|
+
}
|
|
203
|
+
const emptyEquals = run([`settings`, `show`, `--root=${root}`], env);
|
|
204
|
+
if (!emptyEquals.includes('No saved settings')) {
|
|
205
|
+
throw new Error(`expected --root= settings output, got:\n${emptyEquals}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const saved = run([
|
|
209
|
+
'settings',
|
|
210
|
+
'set',
|
|
211
|
+
'--root',
|
|
212
|
+
root,
|
|
213
|
+
'--allow-root',
|
|
214
|
+
reuseRoot,
|
|
215
|
+
'--tunnel',
|
|
216
|
+
'ngrok',
|
|
217
|
+
'--hostname',
|
|
218
|
+
'codexflow-test.ngrok-free.app',
|
|
219
|
+
'--port',
|
|
220
|
+
'19087',
|
|
221
|
+
'--mode',
|
|
222
|
+
'agent',
|
|
223
|
+
'--tool-mode',
|
|
224
|
+
'full',
|
|
225
|
+
'--bash-transcript',
|
|
226
|
+
'full',
|
|
227
|
+
'--widget-domain',
|
|
228
|
+
'https://widgets.codexflow.test',
|
|
229
|
+
'--tool-cards',
|
|
230
|
+
'on',
|
|
231
|
+
'--token',
|
|
232
|
+
'codexflow-settings-token'
|
|
233
|
+
], env);
|
|
234
|
+
if (!saved.includes('Saved workspace settings')) {
|
|
235
|
+
throw new Error(`expected settings save output, got:\n${saved}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const shown = run(['settings', 'show', '--root', root], env);
|
|
239
|
+
for (const expected of ['Tunnel', 'ngrok', 'codexflow-test.ngrok-free.app', '19087', 'Project roots', reuseRoot, 'Tool cards', 'on', 'Bash transcript', 'full', '<saved>']) {
|
|
240
|
+
if (!shown.includes(expected)) {
|
|
241
|
+
throw new Error(`settings show missing ${expected}\n${shown}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (shown.includes('codexflow-settings-token')) {
|
|
245
|
+
throw new Error(`settings show leaked token\n${shown}`);
|
|
246
|
+
}
|
|
247
|
+
const profile = await readProfile(root, home);
|
|
248
|
+
if (profile.toolMode !== 'full' || profile.toolCards !== true || profile.bashTranscript !== 'full' || profile.widgetDomain !== 'https://widgets.codexflow.test' || !profile.allowRoots?.includes(await fs.realpath(reuseRoot))) {
|
|
249
|
+
throw new Error(`settings profile did not persist tool/widget options: ${JSON.stringify(profile)}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
runFail([
|
|
253
|
+
'settings',
|
|
254
|
+
'set',
|
|
255
|
+
'--root',
|
|
256
|
+
policyRoot,
|
|
257
|
+
'--tunnel',
|
|
258
|
+
'cloudflare-named',
|
|
259
|
+
'--hostname',
|
|
260
|
+
'codexflow.example.com',
|
|
261
|
+
'--cloudflare-token',
|
|
262
|
+
'raw-cloudflare-token'
|
|
263
|
+
], env, /does not save raw --cloudflare-token/i);
|
|
264
|
+
|
|
265
|
+
runFail([
|
|
266
|
+
'settings',
|
|
267
|
+
'set',
|
|
268
|
+
'--root',
|
|
269
|
+
policyRoot,
|
|
270
|
+
'--tunnel',
|
|
271
|
+
'ngrok',
|
|
272
|
+
'--hostname',
|
|
273
|
+
'http://policy.ngrok-free.app'
|
|
274
|
+
], env, /hostname must use https/i);
|
|
275
|
+
|
|
276
|
+
run([
|
|
277
|
+
'settings',
|
|
278
|
+
'set',
|
|
279
|
+
'--root',
|
|
280
|
+
policyRoot,
|
|
281
|
+
'--tunnel',
|
|
282
|
+
'ngrok',
|
|
283
|
+
'--hostname',
|
|
284
|
+
'https://policy.ngrok-free.app/mcp',
|
|
285
|
+
'--mode',
|
|
286
|
+
'handoff',
|
|
287
|
+
'--write',
|
|
288
|
+
'workspace',
|
|
289
|
+
'--ngrok-config',
|
|
290
|
+
'ngrok.yml'
|
|
291
|
+
], env);
|
|
292
|
+
const policyProfile = await readProfile(policyRoot, home);
|
|
293
|
+
const realPolicyRoot = await fs.realpath(policyRoot);
|
|
294
|
+
if (policyProfile.write !== 'handoff' || policyProfile.hostname !== 'policy.ngrok-free.app' || policyProfile.ngrokConfig !== path.join(realPolicyRoot, 'ngrok.yml')) {
|
|
295
|
+
throw new Error(`settings policy profile did not normalize write/path values: ${JSON.stringify(policyProfile)}`);
|
|
296
|
+
}
|
|
297
|
+
run([
|
|
298
|
+
'settings',
|
|
299
|
+
'set',
|
|
300
|
+
'--root',
|
|
301
|
+
policyRoot,
|
|
302
|
+
'--tunnel',
|
|
303
|
+
'none'
|
|
304
|
+
], env);
|
|
305
|
+
const localPolicyProfile = await readProfile(policyRoot, home);
|
|
306
|
+
if (localPolicyProfile.tunnel !== 'none' || localPolicyProfile.hostname || localPolicyProfile.ngrokConfig) {
|
|
307
|
+
throw new Error(`settings local-only profile kept stale ngrok values: ${JSON.stringify(localPolicyProfile)}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
run([
|
|
311
|
+
'settings',
|
|
312
|
+
'set',
|
|
313
|
+
'--root',
|
|
314
|
+
policyRoot,
|
|
315
|
+
'--tunnel',
|
|
316
|
+
'tailscale',
|
|
317
|
+
'--hostname',
|
|
318
|
+
'https://codexflow-test.tailnet.ts.net/mcp'
|
|
319
|
+
], env);
|
|
320
|
+
const tailscalePolicyProfile = await readProfile(policyRoot, home);
|
|
321
|
+
if (tailscalePolicyProfile.tunnel !== 'tailscale' || tailscalePolicyProfile.hostname !== 'codexflow-test.tailnet.ts.net' || tailscalePolicyProfile.ngrokConfig) {
|
|
322
|
+
throw new Error(`settings tailscale profile did not normalize/clear stale tunnel values: ${JSON.stringify(tailscalePolicyProfile)}`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
run([
|
|
326
|
+
'settings',
|
|
327
|
+
'set',
|
|
328
|
+
'--root',
|
|
329
|
+
staleRoot,
|
|
330
|
+
'--tunnel',
|
|
331
|
+
'cloudflare-named',
|
|
332
|
+
'--hostname',
|
|
333
|
+
'codexflow-stale.example.com',
|
|
334
|
+
'--tunnel-name',
|
|
335
|
+
'stale-tunnel',
|
|
336
|
+
'--cloudflare-config',
|
|
337
|
+
'cloudflared.yml',
|
|
338
|
+
'--cloudflare-token-file',
|
|
339
|
+
'cloudflare-token'
|
|
340
|
+
], env);
|
|
341
|
+
run([
|
|
342
|
+
'settings',
|
|
343
|
+
'set',
|
|
344
|
+
'--root',
|
|
345
|
+
staleRoot,
|
|
346
|
+
'--tunnel',
|
|
347
|
+
'cloudflare'
|
|
348
|
+
], env);
|
|
349
|
+
const quickProfile = await readProfile(staleRoot, home);
|
|
350
|
+
if (quickProfile.tunnel !== 'cloudflare' || quickProfile.hostname || quickProfile.tunnelName || quickProfile.cloudflareConfig || quickProfile.cloudflareTokenFile) {
|
|
351
|
+
throw new Error(`settings quick tunnel profile kept stale named-tunnel values: ${JSON.stringify(quickProfile)}`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
runFail([
|
|
355
|
+
'settings',
|
|
356
|
+
'set',
|
|
357
|
+
'--root',
|
|
358
|
+
root,
|
|
359
|
+
'--tunnel',
|
|
360
|
+
'ngrok',
|
|
361
|
+
'--hostname',
|
|
362
|
+
'codexflow-test.ngrok-free.app',
|
|
363
|
+
'--require-bash-session'
|
|
364
|
+
], env, /requires --bash-session/i);
|
|
365
|
+
|
|
366
|
+
const guarded = run([
|
|
367
|
+
'settings',
|
|
368
|
+
'set',
|
|
369
|
+
'--root',
|
|
370
|
+
root,
|
|
371
|
+
'--tunnel',
|
|
372
|
+
'ngrok',
|
|
373
|
+
'--hostname',
|
|
374
|
+
'codexflow-test.ngrok-free.app',
|
|
375
|
+
'--bash-session',
|
|
376
|
+
'guarded-main',
|
|
377
|
+
'--require-bash-session'
|
|
378
|
+
], env);
|
|
379
|
+
if (!guarded.includes('Bash session') || !guarded.includes('guarded-main required')) {
|
|
380
|
+
throw new Error(`settings save did not display guarded bash session\n${guarded}`);
|
|
381
|
+
}
|
|
382
|
+
const guardedProfile = await readProfile(root, home);
|
|
383
|
+
if (guardedProfile.bashSession !== 'guarded-main' || guardedProfile.requireBashSession !== true) {
|
|
384
|
+
throw new Error(`settings profile did not persist bash session guard: ${JSON.stringify(guardedProfile)}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
runFail([
|
|
388
|
+
'settings',
|
|
389
|
+
'set',
|
|
390
|
+
'--root',
|
|
391
|
+
policyRoot,
|
|
392
|
+
'--tunnel',
|
|
393
|
+
'none',
|
|
394
|
+
'--bash',
|
|
395
|
+
'banana'
|
|
396
|
+
], env, /--bash must be off, safe, or full/i);
|
|
397
|
+
|
|
398
|
+
runFail([
|
|
399
|
+
'settings',
|
|
400
|
+
'set',
|
|
401
|
+
'--root',
|
|
402
|
+
policyRoot,
|
|
403
|
+
'--tunnel',
|
|
404
|
+
'none',
|
|
405
|
+
'--tool-mode',
|
|
406
|
+
'banana'
|
|
407
|
+
], env, /--tool-mode must be minimal, standard, or full/i);
|
|
408
|
+
|
|
409
|
+
runFail([
|
|
410
|
+
'settings',
|
|
411
|
+
'set',
|
|
412
|
+
'--root',
|
|
413
|
+
policyRoot,
|
|
414
|
+
'--tunnel',
|
|
415
|
+
'none',
|
|
416
|
+
'--port',
|
|
417
|
+
'abc'
|
|
418
|
+
], env, /Invalid port: abc/i);
|
|
419
|
+
|
|
420
|
+
const runtimePort = await getFreePort();
|
|
421
|
+
const runtimePath = await runtimeStatusPath(runtimeRoot, home);
|
|
422
|
+
run([
|
|
423
|
+
'settings',
|
|
424
|
+
'set',
|
|
425
|
+
'--root',
|
|
426
|
+
runtimeRoot,
|
|
427
|
+
'--tunnel',
|
|
428
|
+
'none',
|
|
429
|
+
'--port',
|
|
430
|
+
String(runtimePort),
|
|
431
|
+
'--tool-cards',
|
|
432
|
+
'on'
|
|
433
|
+
], env);
|
|
434
|
+
await withStartedCodexFlow([
|
|
435
|
+
'--root',
|
|
436
|
+
runtimeRoot
|
|
437
|
+
], env, async (child) => {
|
|
438
|
+
const runtime = await waitForJson(runtimePath, (data) => data.toolCards === true && data.pid === child.pid, 'tool-cards runtime status');
|
|
439
|
+
if (runtime.toolCards !== true || runtime.pid !== child.pid) {
|
|
440
|
+
throw new Error(`runtime status did not persist toolCards: ${JSON.stringify(runtime)}`);
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
try {
|
|
444
|
+
await fs.access(runtimePath);
|
|
445
|
+
throw new Error('runtime status was not cleared after launcher SIGTERM');
|
|
446
|
+
} catch (error) {
|
|
447
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const quitRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-settings-quit-'));
|
|
451
|
+
const quitPort = await getFreePort();
|
|
452
|
+
const quitRuntimePath = await runtimeStatusPath(quitRoot, home);
|
|
453
|
+
if (runInteractiveQuit([
|
|
454
|
+
'--root',
|
|
455
|
+
quitRoot,
|
|
456
|
+
'--tunnel',
|
|
457
|
+
'none',
|
|
458
|
+
'--port',
|
|
459
|
+
String(quitPort),
|
|
460
|
+
'--no-copy-url'
|
|
461
|
+
], env)) {
|
|
462
|
+
try {
|
|
463
|
+
await fs.access(quitRuntimePath);
|
|
464
|
+
throw new Error('runtime status was not cleared after interactive q exit');
|
|
465
|
+
} catch (error) {
|
|
466
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const cloudflareRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-settings-cloudflare-'));
|
|
471
|
+
const cloudflarePort = await getFreePort();
|
|
472
|
+
const cloudflarePath = await runtimeStatusPath(cloudflareRoot, home);
|
|
473
|
+
const fakeCloudflared = path.join(home, 'fake-cloudflared.mjs');
|
|
474
|
+
await fs.writeFile(fakeCloudflared, [
|
|
475
|
+
'#!/usr/bin/env node',
|
|
476
|
+
"if (process.argv.includes('--version')) { console.log('cloudflared version 2026.6.0'); process.exit(0); }",
|
|
477
|
+
"console.error('https://api.trycloudflare.com/tunnel');",
|
|
478
|
+
"setTimeout(() => console.error('https://real-codexflow.trycloudflare.com'), 100);",
|
|
479
|
+
'setInterval(() => {}, 1000);',
|
|
480
|
+
''
|
|
481
|
+
].join('\n'), { mode: 0o700 });
|
|
482
|
+
await withStartedCodexFlow([
|
|
483
|
+
'--root',
|
|
484
|
+
cloudflareRoot,
|
|
485
|
+
'--tunnel',
|
|
486
|
+
'cloudflare',
|
|
487
|
+
'--cloudflared',
|
|
488
|
+
fakeCloudflared,
|
|
489
|
+
'--port',
|
|
490
|
+
String(cloudflarePort),
|
|
491
|
+
'--token',
|
|
492
|
+
'codexflow-cloudflare-token',
|
|
493
|
+
'--no-copy-url'
|
|
494
|
+
], withoutProxyEnv(env), async () => {
|
|
495
|
+
const runtime = await waitForJson(cloudflarePath, (data) => data.endpoint?.includes('trycloudflare.com'), 'cloudflare runtime status');
|
|
496
|
+
if (runtime.endpoint.includes('api.trycloudflare.com') || !runtime.endpoint.startsWith('https://real-codexflow.trycloudflare.com/mcp')) {
|
|
497
|
+
throw new Error(`quick tunnel saved the wrong endpoint: ${JSON.stringify(runtime)}`);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
const proxyCloudflareRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-settings-cloudflare-proxy-'));
|
|
502
|
+
const proxyCloudflarePort = await getFreePort();
|
|
503
|
+
const proxyCloudflarePath = await runtimeStatusPath(proxyCloudflareRoot, home);
|
|
504
|
+
const fakeBin = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-settings-fake-bin-'));
|
|
505
|
+
const fakeCurl = path.join(fakeBin, 'curl');
|
|
506
|
+
const curlArgsPath = path.join(home, 'fake-curl-args.json');
|
|
507
|
+
const cloudflaredArgsPath = path.join(home, 'fake-cloudflared-proxy-args.json');
|
|
508
|
+
const fakeProxyCloudflared = path.join(home, 'fake-cloudflared-proxy.mjs');
|
|
509
|
+
await fs.writeFile(fakeCurl, [
|
|
510
|
+
'#!/usr/bin/env node',
|
|
511
|
+
"const fs = require('node:fs');",
|
|
512
|
+
"fs.writeFileSync(process.env.CODEXFLOW_FAKE_CURL_ARGS, JSON.stringify(process.argv.slice(2)));",
|
|
513
|
+
"console.log(JSON.stringify({ success: true, result: { id: 'proxy-tunnel-id', hostname: 'proxy-codexflow.trycloudflare.com', account_tag: 'account-tag', secret: 'proxy-secret-1234567890' } }));",
|
|
514
|
+
''
|
|
515
|
+
].join('\n'), { mode: 0o700 });
|
|
516
|
+
await fs.writeFile(fakeProxyCloudflared, [
|
|
517
|
+
'#!/usr/bin/env node',
|
|
518
|
+
"import fs from 'node:fs';",
|
|
519
|
+
"if (process.argv.includes('--version')) { console.log('cloudflared version 2026.6.0'); process.exit(0); }",
|
|
520
|
+
"const args = process.argv.slice(2);",
|
|
521
|
+
"fs.writeFileSync(process.env.CODEXFLOW_FAKE_CLOUDFLARED_ARGS, JSON.stringify(args));",
|
|
522
|
+
"const credentialsPath = args[args.indexOf('--credentials-file') + 1];",
|
|
523
|
+
"const credentials = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'));",
|
|
524
|
+
"if (credentials.TunnelID !== 'proxy-tunnel-id' || credentials.AccountTag !== 'account-tag' || credentials.TunnelSecret !== 'proxy-secret-1234567890') process.exit(4);",
|
|
525
|
+
"setInterval(() => {}, 1000);",
|
|
526
|
+
''
|
|
527
|
+
].join('\n'), { mode: 0o700 });
|
|
528
|
+
await withStartedCodexFlow([
|
|
529
|
+
'--root',
|
|
530
|
+
proxyCloudflareRoot,
|
|
531
|
+
'--tunnel',
|
|
532
|
+
'cloudflare',
|
|
533
|
+
'--cloudflared',
|
|
534
|
+
fakeProxyCloudflared,
|
|
535
|
+
'--port',
|
|
536
|
+
String(proxyCloudflarePort),
|
|
537
|
+
'--token',
|
|
538
|
+
'codexflow-cloudflare-proxy-token',
|
|
539
|
+
'--no-copy-url'
|
|
540
|
+
], {
|
|
541
|
+
...env,
|
|
542
|
+
PATH: `${fakeBin}${path.delimiter}${process.env.PATH ?? ''}`,
|
|
543
|
+
HTTPS_PROXY: 'http://proxy.example.test:8080',
|
|
544
|
+
CODEXFLOW_FAKE_CURL_ARGS: curlArgsPath,
|
|
545
|
+
CODEXFLOW_FAKE_CLOUDFLARED_ARGS: cloudflaredArgsPath
|
|
546
|
+
}, async () => {
|
|
547
|
+
const runtime = await waitForJson(proxyCloudflarePath, (data) => data.endpoint?.includes('proxy-codexflow.trycloudflare.com'), 'proxy cloudflare runtime status');
|
|
548
|
+
if (!runtime.endpoint.startsWith('https://proxy-codexflow.trycloudflare.com/mcp')) {
|
|
549
|
+
throw new Error(`proxy quick tunnel saved the wrong endpoint: ${JSON.stringify(runtime)}`);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
const curlArgs = JSON.parse(await fs.readFile(curlArgsPath, 'utf8'));
|
|
553
|
+
if (!curlArgs.includes('--proxy') || !curlArgs.includes('http://proxy.example.test:8080') || !curlArgs.includes('https://api.trycloudflare.com/tunnel')) {
|
|
554
|
+
throw new Error(`proxy quick tunnel did not call curl through proxy: ${JSON.stringify(curlArgs)}`);
|
|
555
|
+
}
|
|
556
|
+
const cloudflaredArgs = JSON.parse(await fs.readFile(cloudflaredArgsPath, 'utf8'));
|
|
557
|
+
if (!cloudflaredArgs.includes('--credentials-file') || !cloudflaredArgs.includes('run') || !cloudflaredArgs.includes('proxy-tunnel-id')) {
|
|
558
|
+
throw new Error(`proxy quick tunnel did not run cloudflared with credentials: ${JSON.stringify(cloudflaredArgs)}`);
|
|
559
|
+
}
|
|
560
|
+
const credentialsPath = cloudflaredArgs[cloudflaredArgs.indexOf('--credentials-file') + 1];
|
|
561
|
+
try {
|
|
562
|
+
await fs.access(credentialsPath);
|
|
563
|
+
throw new Error(`proxy quick tunnel credentials were not cleaned up: ${credentialsPath}`);
|
|
564
|
+
} catch (error) {
|
|
565
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const proxyCloudflareFailRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-settings-cloudflare-proxy-fail-'));
|
|
569
|
+
const proxyCloudflareFailPort = await getFreePort();
|
|
570
|
+
const fakeFailingProxyCloudflared = path.join(home, 'fake-cloudflared-proxy-fail.mjs');
|
|
571
|
+
await fs.writeFile(fakeFailingProxyCloudflared, [
|
|
572
|
+
'#!/usr/bin/env node',
|
|
573
|
+
"if (process.argv.includes('--version')) { console.log('cloudflared version 2026.6.0'); process.exit(0); }",
|
|
574
|
+
"console.error('proxy tunnel startup failed');",
|
|
575
|
+
'process.exit(7);',
|
|
576
|
+
''
|
|
577
|
+
].join('\n'), { mode: 0o700 });
|
|
578
|
+
const proxyFailure = runFail([
|
|
579
|
+
'start',
|
|
580
|
+
'--root',
|
|
581
|
+
proxyCloudflareFailRoot,
|
|
582
|
+
'--tunnel',
|
|
583
|
+
'cloudflare',
|
|
584
|
+
'--cloudflared',
|
|
585
|
+
fakeFailingProxyCloudflared,
|
|
586
|
+
'--port',
|
|
587
|
+
String(proxyCloudflareFailPort),
|
|
588
|
+
'--token',
|
|
589
|
+
'codexflow-cloudflare-proxy-token',
|
|
590
|
+
'--no-copy-url'
|
|
591
|
+
], {
|
|
592
|
+
...env,
|
|
593
|
+
PATH: `${fakeBin}${path.delimiter}${process.env.PATH ?? ''}`,
|
|
594
|
+
HTTPS_PROXY: 'http://proxy.example.test:8080',
|
|
595
|
+
CODEXFLOW_FAKE_CURL_ARGS: path.join(home, 'fake-curl-fail-args.json')
|
|
596
|
+
}, /exited before startup completed/);
|
|
597
|
+
if (!proxyFailure.includes('proxy tunnel startup failed')) {
|
|
598
|
+
throw new Error(`proxy quick tunnel did not include cloudflared startup failure output\n${proxyFailure}`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const namedCloudflareRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-settings-cloudflare-named-'));
|
|
602
|
+
const namedCloudflarePort = await getFreePort();
|
|
603
|
+
const fakeNamedCloudflared = path.join(home, 'fake-cloudflared-named.mjs');
|
|
604
|
+
const cloudflareRawToken = 'cf_audit_secret_1234567890TOKEN';
|
|
605
|
+
await fs.writeFile(fakeNamedCloudflared, [
|
|
606
|
+
'#!/usr/bin/env node',
|
|
607
|
+
"if (process.argv.includes('--version')) { console.log('cloudflared version 2026.6.0'); process.exit(0); }",
|
|
608
|
+
"console.error('fake tunnel saw TUNNEL_TOKEN=' + process.env.TUNNEL_TOKEN);",
|
|
609
|
+
'process.exit(2);',
|
|
610
|
+
''
|
|
611
|
+
].join('\n'), { mode: 0o700 });
|
|
612
|
+
const namedFailure = runFail([
|
|
613
|
+
'start',
|
|
614
|
+
'--root',
|
|
615
|
+
namedCloudflareRoot,
|
|
616
|
+
'--tunnel',
|
|
617
|
+
'cloudflare-named',
|
|
618
|
+
'--hostname',
|
|
619
|
+
'codexflow-audit.example.com',
|
|
620
|
+
'--cloudflare-token',
|
|
621
|
+
cloudflareRawToken,
|
|
622
|
+
'--cloudflared',
|
|
623
|
+
fakeNamedCloudflared,
|
|
624
|
+
'--port',
|
|
625
|
+
String(namedCloudflarePort),
|
|
626
|
+
'--token',
|
|
627
|
+
'codexflow-named-http-token',
|
|
628
|
+
'--no-copy-url'
|
|
629
|
+
], env, /Recent cloudflared output/);
|
|
630
|
+
if (namedFailure.includes(cloudflareRawToken) || !namedFailure.includes('TUNNEL_TOKEN= [REDACTED_SECRET]')) {
|
|
631
|
+
throw new Error(`named tunnel failure leaked or failed to redact Cloudflare token\n${namedFailure}`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const fakeNgrok = path.join(home, 'fake-ngrok.mjs');
|
|
635
|
+
await fs.writeFile(fakeNgrok, [
|
|
636
|
+
'#!/usr/bin/env node',
|
|
637
|
+
"if (process.argv.includes('version')) { console.log('ngrok version 3.0.0'); process.exit(0); }",
|
|
638
|
+
"console.error('NGROK_ARGS=' + process.argv.slice(2).join('|'));",
|
|
639
|
+
'process.exit(2);',
|
|
640
|
+
''
|
|
641
|
+
].join('\n'), { mode: 0o700 });
|
|
642
|
+
run([
|
|
643
|
+
'settings',
|
|
644
|
+
'set',
|
|
645
|
+
'--root',
|
|
646
|
+
ngrokRoot,
|
|
647
|
+
'--tunnel',
|
|
648
|
+
'ngrok',
|
|
649
|
+
'--hostname',
|
|
650
|
+
'codexflow-env.ngrok-free.app',
|
|
651
|
+
'--ngrok-config',
|
|
652
|
+
'old-ngrok.yml'
|
|
653
|
+
], env);
|
|
654
|
+
const ngrokPort = await getFreePort();
|
|
655
|
+
const ngrokFailure = runFail([
|
|
656
|
+
'start',
|
|
657
|
+
'--root',
|
|
658
|
+
ngrokRoot,
|
|
659
|
+
'--tunnel',
|
|
660
|
+
'ngrok',
|
|
661
|
+
'--hostname',
|
|
662
|
+
'codexflow-env.ngrok-free.app',
|
|
663
|
+
'--ngrok',
|
|
664
|
+
fakeNgrok,
|
|
665
|
+
'--port',
|
|
666
|
+
String(ngrokPort),
|
|
667
|
+
'--token',
|
|
668
|
+
'codexflow-ngrok-env-token',
|
|
669
|
+
'--no-copy-url'
|
|
670
|
+
], { ...env, NGROK_CONFIG: 'new-ngrok.yml' }, /Recent ngrok output/);
|
|
671
|
+
const realNgrokRoot = await fs.realpath(ngrokRoot);
|
|
672
|
+
if (!ngrokFailure.includes(`--config|${path.join(realNgrokRoot, 'new-ngrok.yml')}`) || ngrokFailure.includes('old-ngrok.yml')) {
|
|
673
|
+
throw new Error(`ngrok start did not let env config override saved profile\n${ngrokFailure}`);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const fakeTailscale = path.join(home, 'fake-tailscale.mjs');
|
|
677
|
+
await fs.writeFile(fakeTailscale, [
|
|
678
|
+
'#!/usr/bin/env node',
|
|
679
|
+
"if (process.argv.includes('version')) { console.log('1.80.0'); process.exit(0); }",
|
|
680
|
+
"console.error('TAILSCALE_ARGS=' + process.argv.slice(2).join('|'));",
|
|
681
|
+
'process.exit(2);',
|
|
682
|
+
''
|
|
683
|
+
].join('\n'), { mode: 0o700 });
|
|
684
|
+
const tailscaleRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-settings-tailscale-'));
|
|
685
|
+
const tailscalePort = await getFreePort();
|
|
686
|
+
const tailscaleFailure = runFail([
|
|
687
|
+
'start',
|
|
688
|
+
'--root',
|
|
689
|
+
tailscaleRoot,
|
|
690
|
+
'--tunnel',
|
|
691
|
+
'tailscale',
|
|
692
|
+
'--hostname',
|
|
693
|
+
'codexflow-env.tailnet.ts.net',
|
|
694
|
+
'--tailscale',
|
|
695
|
+
fakeTailscale,
|
|
696
|
+
'--port',
|
|
697
|
+
String(tailscalePort),
|
|
698
|
+
'--token',
|
|
699
|
+
'codexflow-tailscale-token',
|
|
700
|
+
'--no-copy-url'
|
|
701
|
+
], env, /Recent tailscale output/);
|
|
702
|
+
if (!tailscaleFailure.includes(`funnel|http://127.0.0.1:${tailscalePort}`)) {
|
|
703
|
+
throw new Error(`tailscale start did not invoke Funnel against the local server\n${tailscaleFailure}`);
|
|
704
|
+
}
|
|
705
|
+
const tailscalePortRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'codexflow-settings-tailscale-port-'));
|
|
706
|
+
const tailscalePort8443 = await getFreePort();
|
|
707
|
+
const tailscalePortFailure = runFail([
|
|
708
|
+
'start',
|
|
709
|
+
'--root',
|
|
710
|
+
tailscalePortRoot,
|
|
711
|
+
'--tunnel',
|
|
712
|
+
'tailscale',
|
|
713
|
+
'--hostname',
|
|
714
|
+
'codexflow-env.tailnet.ts.net:8443',
|
|
715
|
+
'--tailscale',
|
|
716
|
+
fakeTailscale,
|
|
717
|
+
'--port',
|
|
718
|
+
String(tailscalePort8443),
|
|
719
|
+
'--token',
|
|
720
|
+
'codexflow-tailscale-token',
|
|
721
|
+
'--no-copy-url'
|
|
722
|
+
], env, /Recent tailscale output/);
|
|
723
|
+
if (!tailscalePortFailure.includes(`funnel|--https=8443|http://127.0.0.1:${tailscalePort8443}`)) {
|
|
724
|
+
throw new Error(`tailscale start did not map hostname port to Funnel HTTPS port\n${tailscalePortFailure}`);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const listed = run(['settings', 'list'], env);
|
|
728
|
+
if (!listed.includes(root) || !listed.includes('codexflow-test.ngrok-free.app') || !listed.includes('codexflow-test.tailnet.ts.net')) {
|
|
729
|
+
throw new Error(`settings list missing saved profile\n${listed}`);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const reused = run(['settings', 'use', '--root', reuseRoot, '--from-root', root], env);
|
|
733
|
+
if (!reused.includes('Saved workspace settings from')) {
|
|
734
|
+
throw new Error(`settings use did not save profile\n${reused}`);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const reusedShown = run(['settings', 'show', '--root', reuseRoot], env);
|
|
738
|
+
for (const expected of ['ngrok', 'codexflow-test.ngrok-free.app', '<saved>']) {
|
|
739
|
+
if (!reusedShown.includes(expected)) {
|
|
740
|
+
throw new Error(`reused settings show missing ${expected}\n${reusedShown}`);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const deleted = run(['settings', 'delete', '--root', root, '--yes'], env);
|
|
745
|
+
if (!deleted.includes('Deleted saved settings')) {
|
|
746
|
+
throw new Error(`expected settings delete output, got:\n${deleted}`);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
run(['settings', 'delete', '--root', reuseRoot, '--yes'], env);
|
|
750
|
+
|
|
751
|
+
const afterDelete = run(['settings', 'show', '--root', root], env);
|
|
752
|
+
if (!afterDelete.includes('No saved settings')) {
|
|
753
|
+
throw new Error(`expected empty settings after delete, got:\n${afterDelete}`);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
console.log('✓ settings smoke test passed');
|