@axiomatic-labs/claudeflow 2.33.0 → 2.33.2
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/package.json +8 -3
- package/lib/doctor.js +0 -376
- package/lib/panel.js +0 -2628
package/package.json
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiomatic-labs/claudeflow",
|
|
3
|
-
"version": "2.33.
|
|
3
|
+
"version": "2.33.2",
|
|
4
4
|
"description": "Claudeflow — AI-powered development toolkit for Claude Code. Skills, agents, hooks, and quality gates that ship production apps.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"claudeflow": "./bin/cli.js"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
|
-
"bin/",
|
|
10
|
-
"lib/",
|
|
9
|
+
"bin/cli.js",
|
|
10
|
+
"lib/auth.js",
|
|
11
|
+
"lib/claude-passthrough.js",
|
|
12
|
+
"lib/download.js",
|
|
13
|
+
"lib/install.js",
|
|
14
|
+
"lib/ui.js",
|
|
15
|
+
"lib/version.js",
|
|
11
16
|
"README.md"
|
|
12
17
|
],
|
|
13
18
|
"keywords": [
|
package/lib/doctor.js
DELETED
|
@@ -1,376 +0,0 @@
|
|
|
1
|
-
// `claudeflow doctor` — diagnose and optionally repair common local-machine
|
|
2
|
-
// issues that don't surface until something silently fails.
|
|
3
|
-
//
|
|
4
|
-
// Scope (intentionally narrow):
|
|
5
|
-
// 1. CDP port mismatch — `.mcp.json` ships a `--cdp-endpoint` derived from
|
|
6
|
-
// the path of whoever ran `claudeflow init`. After a clone, the local
|
|
7
|
-
// observer derives a different port from the new path, so the Playwright
|
|
8
|
-
// MCP can't reach the observer's Chrome.
|
|
9
|
-
// 2. Stale browser PID lockfile — `openBrowser()` writes
|
|
10
|
-
// `.claudeflow/tmp/.browser-cdp-<port>.pid` and skips relaunch if the PID
|
|
11
|
-
// is alive. If Chrome is killed externally (pkill) the lockfile lingers
|
|
12
|
-
// and blocks subsequent launches.
|
|
13
|
-
//
|
|
14
|
-
// Read-only by default. `--fix` applies repairs after reporting them.
|
|
15
|
-
|
|
16
|
-
const fs = require('fs');
|
|
17
|
-
const path = require('path');
|
|
18
|
-
const crypto = require('crypto');
|
|
19
|
-
|
|
20
|
-
const ui = require('./ui.js');
|
|
21
|
-
|
|
22
|
-
function deriveCdpPort(projectPath) {
|
|
23
|
-
const hash = crypto.createHash('sha1').update(projectPath).digest();
|
|
24
|
-
return 9200 + (hash.readUInt16BE(2) % 100);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function readPlaywrightCdpEndpoint(mcpPath) {
|
|
28
|
-
let raw;
|
|
29
|
-
try {
|
|
30
|
-
raw = fs.readFileSync(mcpPath, 'utf8');
|
|
31
|
-
} catch {
|
|
32
|
-
return { state: 'missing-file' };
|
|
33
|
-
}
|
|
34
|
-
let parsed;
|
|
35
|
-
try {
|
|
36
|
-
parsed = JSON.parse(raw);
|
|
37
|
-
} catch (err) {
|
|
38
|
-
return { state: 'invalid-json', error: err.message };
|
|
39
|
-
}
|
|
40
|
-
const entry = parsed && parsed.mcpServers && parsed.mcpServers.playwright;
|
|
41
|
-
if (!entry || !Array.isArray(entry.args)) return { state: 'no-playwright' };
|
|
42
|
-
const flagIdx = entry.args.indexOf('--cdp-endpoint');
|
|
43
|
-
if (flagIdx === -1) return { state: 'no-flag', parsed };
|
|
44
|
-
const value = entry.args[flagIdx + 1];
|
|
45
|
-
// Accept localhost OR 127.0.0.1 (v2.13.72+ writes the latter explicitly to
|
|
46
|
-
// dodge the IPv4/IPv6 dual-daemon trap — see observer-loopback lint test).
|
|
47
|
-
const match = /(?:localhost|127\.0\.0\.1):(\d+)/.exec(value || '');
|
|
48
|
-
if (!match) return { state: 'unparseable', value, parsed };
|
|
49
|
-
return { state: 'ok', port: Number(match[1]), flagIdx, parsed };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function checkCdpPortMismatch(cwd) {
|
|
53
|
-
const mcpPath = path.join(cwd, '.mcp.json');
|
|
54
|
-
const expected = deriveCdpPort(cwd);
|
|
55
|
-
const reading = readPlaywrightCdpEndpoint(mcpPath);
|
|
56
|
-
|
|
57
|
-
switch (reading.state) {
|
|
58
|
-
case 'missing-file':
|
|
59
|
-
return { id: 'cdp-port', severity: 'info', message: 'No .mcp.json in cwd — skipping.' };
|
|
60
|
-
case 'invalid-json':
|
|
61
|
-
return { id: 'cdp-port', severity: 'error', message: `.mcp.json is invalid JSON: ${reading.error}` };
|
|
62
|
-
case 'no-playwright':
|
|
63
|
-
return { id: 'cdp-port', severity: 'info', message: '.mcp.json has no `playwright` entry — skipping.' };
|
|
64
|
-
case 'no-flag':
|
|
65
|
-
return { id: 'cdp-port', severity: 'info', message: 'Playwright MCP has no `--cdp-endpoint` flag — observer-MCP wiring not in use.' };
|
|
66
|
-
case 'unparseable':
|
|
67
|
-
return { id: 'cdp-port', severity: 'error', message: `Unparseable --cdp-endpoint value: ${reading.value}` };
|
|
68
|
-
case 'ok':
|
|
69
|
-
if (reading.port === expected) {
|
|
70
|
-
return { id: 'cdp-port', severity: 'ok', message: `CDP port matches (${expected}).` };
|
|
71
|
-
}
|
|
72
|
-
return {
|
|
73
|
-
id: 'cdp-port',
|
|
74
|
-
severity: 'mismatch',
|
|
75
|
-
message: `CDP port mismatch — .mcp.json has ${reading.port}, this machine derives ${expected} from ${cwd}.`,
|
|
76
|
-
fix: { mcpPath, expected, parsed: reading.parsed, flagIdx: reading.flagIdx },
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function applyCdpPortFix(check) {
|
|
82
|
-
const { mcpPath, expected, parsed, flagIdx } = check.fix;
|
|
83
|
-
parsed.mcpServers.playwright.args[flagIdx + 1] = `http://127.0.0.1:${expected}`;
|
|
84
|
-
fs.writeFileSync(mcpPath, JSON.stringify(parsed, null, 2) + '\n');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function listStaleLockfiles(cwd) {
|
|
88
|
-
const dir = path.join(cwd, '.claudeflow', 'tmp');
|
|
89
|
-
let entries;
|
|
90
|
-
try {
|
|
91
|
-
entries = fs.readdirSync(dir);
|
|
92
|
-
} catch {
|
|
93
|
-
return [];
|
|
94
|
-
}
|
|
95
|
-
const stale = [];
|
|
96
|
-
for (const name of entries) {
|
|
97
|
-
if (!/^\.browser-cdp-\d+\.pid$/.test(name)) continue;
|
|
98
|
-
const full = path.join(dir, name);
|
|
99
|
-
let pid;
|
|
100
|
-
try {
|
|
101
|
-
pid = parseInt(fs.readFileSync(full, 'utf8').trim(), 10);
|
|
102
|
-
} catch {
|
|
103
|
-
stale.push({ file: full, reason: 'unreadable' });
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
if (!Number.isFinite(pid) || pid <= 0) {
|
|
107
|
-
stale.push({ file: full, reason: 'invalid-pid', pid });
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
try {
|
|
111
|
-
process.kill(pid, 0);
|
|
112
|
-
} catch (err) {
|
|
113
|
-
if (err.code === 'ESRCH') stale.push({ file: full, reason: 'dead-pid', pid });
|
|
114
|
-
// EPERM means the PID exists but we can't signal it — treat as alive.
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return stale;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function checkStaleLockfiles(cwd) {
|
|
121
|
-
const stale = listStaleLockfiles(cwd);
|
|
122
|
-
if (stale.length === 0) {
|
|
123
|
-
return { id: 'stale-lockfile', severity: 'ok', message: 'No stale browser lockfiles.' };
|
|
124
|
-
}
|
|
125
|
-
return {
|
|
126
|
-
id: 'stale-lockfile',
|
|
127
|
-
severity: 'mismatch',
|
|
128
|
-
message: `${stale.length} stale browser lockfile(s) — Chrome PIDs no longer alive.`,
|
|
129
|
-
detail: stale,
|
|
130
|
-
fix: { stale },
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Read the browser-error-daemon observer state. The daemon is the Node
|
|
135
|
-
// process that hosts the HTTP server at error-observer.json's `port` and
|
|
136
|
-
// receives error/network reports from Chrome.
|
|
137
|
-
//
|
|
138
|
-
// CAVEAT — this is NOT the same as Chrome's CDP pidfile:
|
|
139
|
-
// - `.browser-cdp-{cdpPort}.pid` → Chrome browser process (CDP port)
|
|
140
|
-
// - `error-observer.pid` / json → Node daemon process (observer port)
|
|
141
|
-
// These are two distinct processes. Previous versions of this function
|
|
142
|
-
// looked at the Chrome pidfile and reported the observer "stopped" even
|
|
143
|
-
// when the daemon was alive, because the cdp-port pidfile may not be
|
|
144
|
-
// present when Chrome was started outside of claudeflow's openBrowser().
|
|
145
|
-
//
|
|
146
|
-
// Detection strategy (in order of preference):
|
|
147
|
-
// 1. error-observer.json exists → read its `port` + `pid_path`.
|
|
148
|
-
// 2. If pidfile exists and PID is alive → running=true.
|
|
149
|
-
// 3. Else, TCP probe the observer port via `lsof -i :PORT -sTCP:LISTEN -t`.
|
|
150
|
-
// The pidfile may be missing (race) but if SOMETHING is listening on
|
|
151
|
-
// the configured port, the daemon is up.
|
|
152
|
-
function readObserverState(cwd) {
|
|
153
|
-
const observerJsonPath = path.join(cwd, '.claudeflow', 'tmp', 'error-observer.json');
|
|
154
|
-
// Defense-in-depth retry: even though the daemon now writes atomically
|
|
155
|
-
// (rename-after-write), older daemon versions and any future non-atomic
|
|
156
|
-
// writer would expose a 0-byte window during overwrite. We retry up to
|
|
157
|
-
// 3 times with a tiny pause between attempts so a transient empty read
|
|
158
|
-
// doesn't flip the panel UI between running/stopped every refresh.
|
|
159
|
-
let meta = null;
|
|
160
|
-
let lastErr = null;
|
|
161
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
162
|
-
try {
|
|
163
|
-
const raw = fs.readFileSync(observerJsonPath, 'utf8');
|
|
164
|
-
if (raw.trim()) {
|
|
165
|
-
meta = JSON.parse(raw);
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
lastErr = new Error('empty file');
|
|
169
|
-
} catch (e) {
|
|
170
|
-
lastErr = e;
|
|
171
|
-
if (e.code === 'ENOENT') break; // file genuinely missing — no retry helps
|
|
172
|
-
}
|
|
173
|
-
if (attempt < 2) {
|
|
174
|
-
// Sync sleep (~30ms) via spawnSync — sufficient to outlast a writeFile race.
|
|
175
|
-
const { spawnSync } = require('child_process');
|
|
176
|
-
spawnSync('sleep', ['0.03']);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
if (!meta) {
|
|
180
|
-
const source = (lastErr && lastErr.code === 'ENOENT') ? 'no-observer-json' : 'observer-json-malformed';
|
|
181
|
-
return { running: false, pid: null, port: null, source, observerJsonPath };
|
|
182
|
-
}
|
|
183
|
-
if (typeof meta.port !== 'number') {
|
|
184
|
-
return { running: false, pid: null, port: null, source: 'observer-json-malformed', observerJsonPath };
|
|
185
|
-
}
|
|
186
|
-
const port = meta.port;
|
|
187
|
-
const pidPath = typeof meta.pid_path === 'string' ? meta.pid_path : null;
|
|
188
|
-
|
|
189
|
-
// Try the pidfile first — fastest path.
|
|
190
|
-
let pid = null;
|
|
191
|
-
if (pidPath) {
|
|
192
|
-
try {
|
|
193
|
-
const p = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
|
|
194
|
-
if (Number.isFinite(p) && p > 0) pid = p;
|
|
195
|
-
} catch {}
|
|
196
|
-
}
|
|
197
|
-
if (pid !== null) {
|
|
198
|
-
let alive = false;
|
|
199
|
-
try { process.kill(pid, 0); alive = true; } catch (e) { alive = e.code === 'EPERM'; }
|
|
200
|
-
if (alive) return { running: true, pid, port, source: 'pidfile', observerJsonPath };
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Fallback: TCP probe via lsof. Catches the case where pidfile is missing
|
|
204
|
-
// but the daemon process is still listening on the port.
|
|
205
|
-
try {
|
|
206
|
-
const { spawnSync } = require('child_process');
|
|
207
|
-
const r = spawnSync('lsof', ['-i', `:${port}`, '-sTCP:LISTEN', '-t'], { encoding: 'utf8', timeout: 1500 });
|
|
208
|
-
if (r.status === 0 && r.stdout && r.stdout.trim()) {
|
|
209
|
-
const tcpPid = parseInt(r.stdout.trim().split('\n')[0], 10);
|
|
210
|
-
if (Number.isFinite(tcpPid) && tcpPid > 0) {
|
|
211
|
-
return { running: true, pid: tcpPid, port, source: 'tcp-probe', observerJsonPath };
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
} catch {}
|
|
215
|
-
|
|
216
|
-
return { running: false, pid: pid, port, source: 'no-listener', observerJsonPath };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Surface as a doctor/panel issue when the browser-error-daemon observer
|
|
220
|
-
// is stopped while Playwright is configured. The observer collects console
|
|
221
|
-
// errors + network failures during headed browser verification — when it's
|
|
222
|
-
// down, those signals are silently lost, so the agent may verify a flow
|
|
223
|
-
// that has console errors and not know it.
|
|
224
|
-
function checkObserverState(cwd) {
|
|
225
|
-
const mcpPath = path.join(cwd, '.mcp.json');
|
|
226
|
-
let cfg;
|
|
227
|
-
try { cfg = JSON.parse(fs.readFileSync(mcpPath, 'utf8')); }
|
|
228
|
-
catch { return { id: 'observer-state', severity: 'info', message: 'No .mcp.json — observer not applicable.' }; }
|
|
229
|
-
const hasPlaywright = cfg.mcpServers
|
|
230
|
-
&& Object.keys(cfg.mcpServers).some((s) => s.toLowerCase().includes('playwright'));
|
|
231
|
-
if (!hasPlaywright) {
|
|
232
|
-
return { id: 'observer-state', severity: 'info', message: 'Playwright not configured — observer not applicable.' };
|
|
233
|
-
}
|
|
234
|
-
const observer = readObserverState(cwd);
|
|
235
|
-
if (observer.running) {
|
|
236
|
-
return {
|
|
237
|
-
id: 'observer-state',
|
|
238
|
-
severity: 'ok',
|
|
239
|
-
message: `browser-error-daemon running on port ${observer.port} (pid ${observer.pid}, detected via ${observer.source}).`,
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
// Distinguish "never started" (no observer JSON) from "started then died".
|
|
243
|
-
const detailMsg = observer.source === 'no-observer-json'
|
|
244
|
-
? 'no `.claudeflow/tmp/error-observer.json` — the daemon never started this session. Check `.claude/hooks/SessionStart/browser-error-daemon.js` for failures.'
|
|
245
|
-
: observer.source === 'observer-json-malformed'
|
|
246
|
-
? '`.claudeflow/tmp/error-observer.json` is malformed.'
|
|
247
|
-
: 'observer JSON exists (port ' + observer.port + ') but no process is listening — the daemon crashed after startup. Tail `.claude/tmp/hooks.log` for the most recent SessionStart entry.';
|
|
248
|
-
return {
|
|
249
|
-
id: 'observer-state',
|
|
250
|
-
severity: 'mismatch',
|
|
251
|
-
message: `browser-error-daemon is STOPPED while Playwright is configured. Console errors / network failures from headed sessions are NOT being collected — the agent may verify a flow with console errors and miss them. Diagnosis: ${detailMsg}`,
|
|
252
|
-
detail: [{ port: observer.port, source: observer.source, observerJsonPath: observer.observerJsonPath }],
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Detects more than one process listening on the observer port. macOS allows
|
|
257
|
-
// distinct sockets on the same port for IPv4 and IPv6, so a stale daemon
|
|
258
|
-
// bound to "localhost" (which old code resolved to ::1) could coexist with a
|
|
259
|
-
// fresh daemon bound to 127.0.0.1. Snapshots routed via `localhost` then
|
|
260
|
-
// landed on the stale daemon, which carried the pre-v2.13.71 auto-resolve
|
|
261
|
-
// bug — panel saw the new daemon's empty state, hook saw 1 incident. This
|
|
262
|
-
// check surfaces that condition explicitly.
|
|
263
|
-
function checkObserverDualBind(cwd) {
|
|
264
|
-
const observer = readObserverState(cwd);
|
|
265
|
-
if (!observer.port) {
|
|
266
|
-
return { id: 'observer-dual-bind', severity: 'info', message: 'No observer port known — skipping dual-bind check.' };
|
|
267
|
-
}
|
|
268
|
-
const port = observer.port;
|
|
269
|
-
let listeners = [];
|
|
270
|
-
try {
|
|
271
|
-
const { spawnSync } = require('child_process');
|
|
272
|
-
const r = spawnSync('lsof', [`-iTCP:${port}`, '-sTCP:LISTEN', '-P', '-n', '-F', 'pn'], { encoding: 'utf8', timeout: 1500 });
|
|
273
|
-
if (r.status === 0 && r.stdout) {
|
|
274
|
-
let currentPid = null;
|
|
275
|
-
for (const line of r.stdout.split('\n')) {
|
|
276
|
-
if (line.startsWith('p')) currentPid = Number(line.slice(1));
|
|
277
|
-
else if (line.startsWith('n') && currentPid) listeners.push({ pid: currentPid, name: line.slice(1) });
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
} catch {
|
|
281
|
-
return { id: 'observer-dual-bind', severity: 'info', message: 'lsof unavailable — cannot verify single-bind on observer port.' };
|
|
282
|
-
}
|
|
283
|
-
const uniquePids = new Set(listeners.map((l) => l.pid));
|
|
284
|
-
if (uniquePids.size <= 1) {
|
|
285
|
-
return { id: 'observer-dual-bind', severity: 'ok', message: `Observer port ${port} has a single listener.` };
|
|
286
|
-
}
|
|
287
|
-
return {
|
|
288
|
-
id: 'observer-dual-bind',
|
|
289
|
-
severity: 'error',
|
|
290
|
-
message: `Observer port ${port} has ${uniquePids.size} distinct processes listening (PIDs: ${[...uniquePids].join(', ')}). One is likely a stale daemon from a previous version. Browser snapshots may route to the wrong instance, causing the panel and the Stop hook to disagree. Fix: stop the older PID (\`kill <pid>\`) and let the SessionStart hook respawn a single daemon.`,
|
|
291
|
-
detail: listeners.map((l) => ({ file: `pid ${l.pid}`, reason: l.name })),
|
|
292
|
-
fix: { kind: 'dual-bind', listeners },
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function applyStaleLockfileFix(check) {
|
|
297
|
-
for (const { file } of check.fix.stale) {
|
|
298
|
-
try { fs.unlinkSync(file); } catch {}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const SEVERITY_TAG = {
|
|
303
|
-
ok: `${ui.GREEN}✓${ui.RESET}`,
|
|
304
|
-
info: `${ui.DIM}·${ui.RESET}`,
|
|
305
|
-
mismatch: `${ui.YELLOW}!${ui.RESET}`,
|
|
306
|
-
error: `${ui.RED}✗${ui.RESET}`,
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
function printCheck(check) {
|
|
310
|
-
const tag = SEVERITY_TAG[check.severity] || '?';
|
|
311
|
-
console.log(` ${tag} [${check.id}] ${check.message}`);
|
|
312
|
-
if (check.detail && Array.isArray(check.detail)) {
|
|
313
|
-
for (const d of check.detail) {
|
|
314
|
-
console.log(` ${ui.DIM}- ${d.file} (${d.reason}${d.pid ? `, pid=${d.pid}` : ''})${ui.RESET}`);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
async function run(argv = []) {
|
|
320
|
-
const fixMode = argv.includes('--fix');
|
|
321
|
-
const cwd = process.cwd();
|
|
322
|
-
|
|
323
|
-
ui.banner();
|
|
324
|
-
console.log(` Diagnosing ${ui.CYAN}${cwd}${ui.RESET}${fixMode ? ` ${ui.YELLOW}(--fix)${ui.RESET}` : ''}`);
|
|
325
|
-
console.log('');
|
|
326
|
-
|
|
327
|
-
const checks = [
|
|
328
|
-
checkCdpPortMismatch(cwd),
|
|
329
|
-
checkStaleLockfiles(cwd),
|
|
330
|
-
checkObserverState(cwd),
|
|
331
|
-
checkObserverDualBind(cwd),
|
|
332
|
-
];
|
|
333
|
-
|
|
334
|
-
for (const check of checks) printCheck(check);
|
|
335
|
-
|
|
336
|
-
const fixable = checks.filter(c => c.fix);
|
|
337
|
-
if (fixable.length === 0) {
|
|
338
|
-
console.log('');
|
|
339
|
-
console.log(` ${ui.GREEN}All checks passed.${ui.RESET}`);
|
|
340
|
-
return 0;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
if (!fixMode) {
|
|
344
|
-
console.log('');
|
|
345
|
-
console.log(` ${ui.YELLOW}${fixable.length} issue(s) detected.${ui.RESET} Re-run with ${ui.CYAN}claudeflow doctor --fix${ui.RESET} to repair.`);
|
|
346
|
-
return 1;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
console.log('');
|
|
350
|
-
console.log(` ${ui.CYAN}Applying fixes...${ui.RESET}`);
|
|
351
|
-
for (const check of fixable) {
|
|
352
|
-
if (check.id === 'cdp-port') {
|
|
353
|
-
applyCdpPortFix(check);
|
|
354
|
-
console.log(` ${ui.GREEN}✓${ui.RESET} Rewrote .mcp.json playwright --cdp-endpoint → ${check.fix.expected}`);
|
|
355
|
-
console.log(` ${ui.YELLOW}Do NOT commit this change${ui.RESET} — the port is local to this machine's path.`);
|
|
356
|
-
console.log(` Consider: ${ui.CYAN}git update-index --skip-worktree .mcp.json${ui.RESET}`);
|
|
357
|
-
} else if (check.id === 'stale-lockfile') {
|
|
358
|
-
applyStaleLockfileFix(check);
|
|
359
|
-
console.log(` ${ui.GREEN}✓${ui.RESET} Removed ${check.fix.stale.length} stale lockfile(s).`);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
console.log('');
|
|
363
|
-
console.log(` ${ui.GREEN}Done.${ui.RESET} Reconnect MCP (${ui.CYAN}/mcp${ui.RESET} in Claude Code) to pick up changes.`);
|
|
364
|
-
return 0;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
module.exports = run;
|
|
368
|
-
module.exports.deriveCdpPort = deriveCdpPort;
|
|
369
|
-
module.exports.checkCdpPortMismatch = checkCdpPortMismatch;
|
|
370
|
-
module.exports.checkStaleLockfiles = checkStaleLockfiles;
|
|
371
|
-
module.exports.checkObserverState = checkObserverState;
|
|
372
|
-
module.exports.checkObserverDualBind = checkObserverDualBind;
|
|
373
|
-
module.exports.readObserverState = readObserverState;
|
|
374
|
-
module.exports.readPlaywrightCdpEndpoint = readPlaywrightCdpEndpoint;
|
|
375
|
-
module.exports.applyCdpPortFix = applyCdpPortFix;
|
|
376
|
-
module.exports.applyStaleLockfileFix = applyStaleLockfileFix;
|