rcf-lite 0.16.0 → 0.17.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/CHANGELOG.md +14 -0
- package/blueprints/application-spa/assets/tc-templates/e2e.md +85 -0
- package/blueprints/application-spa/blueprint.json +24 -2
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1134.json +24 -0
- package/blueprints/application-spa/contributions/user-stories/application-spa-us-1135.json +24 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/github-actions/pull-request-checks.yml +69 -0
- package/blueprints/delivery-ci-workflows/assets/ci-provider-examples/notes.md +18 -0
- package/blueprints/delivery-ci-workflows/blueprint.json +301 -60
- package/blueprints/delivery-ci-workflows/contributions/user-stories/delivery-ci-workflows-us-6124.json +28 -0
- package/fixtures/canary-manifest.json +9 -9
- package/package.json +13 -1
- package/rcf/code-nodes/cn-070.json +12 -0
- package/rcf/code-nodes/cn-071.json +12 -0
- package/rcf/code-nodes/cn-072.json +12 -0
- package/rcf/code-nodes/cn-073.json +12 -0
- package/rcf/fbs/fbs-020.json +18 -0
- package/rcf/fbs/fbs-021.json +18 -0
- package/rcf/fbs/fbs-022.json +18 -0
- package/rcf/fbs/fbs-023.json +18 -0
- package/rcf/prd.json +3 -2
- package/rcf/requirements/req-011.json +22 -0
- package/rcf/test-suites/ts-030.json +66 -0
- package/rcf/test-suites/ts-031.json +59 -0
- package/rcf/test-suites/ts-032.json +50 -0
- package/rcf/test-suites/ts-033.json +83 -0
- package/rcf/user-stories/us-1101.json +51 -0
- package/rcf/user-stories/us-1102.json +60 -0
- package/rcf/user-stories/us-1103.json +51 -0
- package/rcf/user-stories/us-1104.json +60 -0
- package/releases/releases.yaml +11 -1
- package/src/blueprint/supersede.js +2 -2
- package/src/cli/doctor.js +182 -5
- package/src/cli/init.js +166 -0
- package/src/setup/playwright-checks.js +426 -0
- package/src/verify/cli/run.js +28 -0
- package/src/verify/engine/index.js +24 -4
- package/src/verify/engine/launcher.js +41 -10
package/src/cli/doctor.js
CHANGED
|
@@ -47,6 +47,16 @@ import {
|
|
|
47
47
|
} from '../setup/managed-gitignore.js';
|
|
48
48
|
import { identityProfilePath } from '../setup/identity-seed.js';
|
|
49
49
|
import { knowledgePaths } from '../setup/knowledge-seed.js';
|
|
50
|
+
import {
|
|
51
|
+
checkBrowserPresent,
|
|
52
|
+
checkPlaywrightMcpReachable,
|
|
53
|
+
checkPlaywrightPresent,
|
|
54
|
+
findProjectPlaywrightKey,
|
|
55
|
+
FIX_LINES,
|
|
56
|
+
loadBrowserFacingSources,
|
|
57
|
+
probeClaudeCodeMcp,
|
|
58
|
+
SKIP_LINE_NON_BROWSER_FACING,
|
|
59
|
+
} from '../setup/playwright-checks.js';
|
|
50
60
|
|
|
51
61
|
const OPTION_SPEC = {
|
|
52
62
|
fix: { type: 'boolean' },
|
|
@@ -57,7 +67,24 @@ const OPTION_SPEC = {
|
|
|
57
67
|
help: { type: 'boolean' },
|
|
58
68
|
};
|
|
59
69
|
|
|
60
|
-
const KNOWN_CHECKS = /** @type {const} */ ([
|
|
70
|
+
const KNOWN_CHECKS = /** @type {const} */ ([
|
|
71
|
+
'agent-instructions',
|
|
72
|
+
'gitignore',
|
|
73
|
+
'knowledge',
|
|
74
|
+
'identity',
|
|
75
|
+
'playwright-present',
|
|
76
|
+
'browser-present',
|
|
77
|
+
'playwright-mcp-reachable',
|
|
78
|
+
'playwright-mcp-redundant',
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
/** The four Playwright-related checks doctor runs conditionally for
|
|
82
|
+
* browser-facing projects (spec 2026-09-03, section 3). */
|
|
83
|
+
const PLAYWRIGHT_CHECKS = /** @type {const} */ ([
|
|
84
|
+
'playwright-present',
|
|
85
|
+
'browser-present',
|
|
86
|
+
'playwright-mcp-reachable',
|
|
87
|
+
]);
|
|
61
88
|
|
|
62
89
|
export const HELP = `Usage: rcf doctor [--fix] [--check <check>[,check]] [--json] [--quiet] [--help]
|
|
63
90
|
|
|
@@ -73,8 +100,10 @@ Options:
|
|
|
73
100
|
removing the corrupted region.
|
|
74
101
|
--check <check>[,check] Run only the named checks. Default: all.
|
|
75
102
|
Values: agent-instructions, gitignore,
|
|
76
|
-
knowledge, identity
|
|
77
|
-
|
|
103
|
+
knowledge, identity, playwright-present,
|
|
104
|
+
browser-present, playwright-mcp-reachable,
|
|
105
|
+
playwright-mcp-redundant.
|
|
106
|
+
--json Emit machine-readable envelope: { ok, drift, writes, notices }.
|
|
78
107
|
--quiet Only summary line + first 3 drift items.
|
|
79
108
|
--force Accept a legacy-markers --fix on hand-edited
|
|
80
109
|
content that a non-interactive run would
|
|
@@ -155,12 +184,48 @@ export async function main(argv, deps = {}) {
|
|
|
155
184
|
force: Boolean(flags.force),
|
|
156
185
|
// isTty is deps-injectable for tests; falls back to real stdout.
|
|
157
186
|
isTty: deps.isTty ?? Boolean(stdout.isTTY),
|
|
187
|
+
// Doctor's Playwright-check seams (spec 2026-09-03, section 3). Every
|
|
188
|
+
// probe is injectable so the unit suite runs with none of these tools
|
|
189
|
+
// installed on the runner.
|
|
190
|
+
checkPlaywrightPresentImpl: deps.checkPlaywrightPresent ?? checkPlaywrightPresent,
|
|
191
|
+
checkBrowserPresentImpl: deps.checkBrowserPresent ?? checkBrowserPresent,
|
|
192
|
+
checkPlaywrightMcpReachableImpl:
|
|
193
|
+
deps.checkPlaywrightMcpReachable ?? checkPlaywrightMcpReachable,
|
|
194
|
+
probeClaudeCodeMcpImpl: deps.probeClaudeCodeMcp ?? probeClaudeCodeMcp,
|
|
195
|
+
loadBrowserFacingSourcesImpl:
|
|
196
|
+
deps.loadBrowserFacingSources ?? loadBrowserFacingSources,
|
|
197
|
+
readMcpJsonImpl: deps.readMcpJson ?? defaultReadMcpJson,
|
|
158
198
|
};
|
|
159
199
|
|
|
200
|
+
// Section 3.1: browser-facing projection. Computed once; every Playwright
|
|
201
|
+
// check reads the result from ctx rather than re-walking the manifest.
|
|
202
|
+
const browserFacingResult = await ctx.loadBrowserFacingSourcesImpl(cwd);
|
|
203
|
+
ctx.browserFacing = Boolean(browserFacingResult.browserFacing);
|
|
204
|
+
ctx.browserFacingSources = browserFacingResult.sources ?? [];
|
|
205
|
+
|
|
160
206
|
/** @type {Array<{check: string, item: string, file: string, message: string, refusedByFix: boolean}>} */
|
|
161
207
|
const drift = [];
|
|
162
208
|
/** @type {Array<{file: string, action: string}>} */
|
|
163
209
|
const writes = [];
|
|
210
|
+
/** @type {string[]} */
|
|
211
|
+
const notices = [];
|
|
212
|
+
|
|
213
|
+
// Section 3.4: skip line for the three Playwright checks on non-browser-
|
|
214
|
+
// facing projects. Emitted when at least one of the three is enabled but
|
|
215
|
+
// the project is not browser-facing AND the operator did not explicitly ask
|
|
216
|
+
// for that check by name (spec 3.5). We honour the --check filter by
|
|
217
|
+
// detecting whether the operator explicitly named any playwright check.
|
|
218
|
+
const explicitlyPickedPlaywrightChecks = new Set(
|
|
219
|
+
enabled.filter((c) => PLAYWRIGHT_CHECKS.includes(c)),
|
|
220
|
+
);
|
|
221
|
+
const anyPlaywrightCheckEnabled = explicitlyPickedPlaywrightChecks.size > 0;
|
|
222
|
+
const operatorAskedByName =
|
|
223
|
+
typeof flags.check === 'string'
|
|
224
|
+
&& flags.check.length > 0
|
|
225
|
+
&& explicitlyPickedPlaywrightChecks.size > 0;
|
|
226
|
+
if (anyPlaywrightCheckEnabled && !ctx.browserFacing && !operatorAskedByName) {
|
|
227
|
+
notices.push(SKIP_LINE_NON_BROWSER_FACING);
|
|
228
|
+
}
|
|
164
229
|
|
|
165
230
|
for (const check of enabled) {
|
|
166
231
|
let result;
|
|
@@ -168,7 +233,21 @@ export async function main(argv, deps = {}) {
|
|
|
168
233
|
else if (check === 'gitignore') result = await runGitignoreCheck(ctx);
|
|
169
234
|
else if (check === 'knowledge') result = await runKnowledgeCheck(ctx);
|
|
170
235
|
else if (check === 'identity') result = await runIdentityCheck(ctx);
|
|
171
|
-
else
|
|
236
|
+
else if (PLAYWRIGHT_CHECKS.includes(check)) {
|
|
237
|
+
// Skip the check on non-browser-facing projects unless the operator
|
|
238
|
+
// explicitly asked for this check by name (--check filter): the skip
|
|
239
|
+
// line above is the ground-truth diagnostic in that case (spec 3.4/3.5).
|
|
240
|
+
if (!ctx.browserFacing && !operatorAskedByName) continue;
|
|
241
|
+
if (check === 'playwright-present') result = await runPlaywrightPresentCheck(ctx);
|
|
242
|
+
else if (check === 'browser-present') result = await runBrowserPresentCheck(ctx);
|
|
243
|
+
else if (check === 'playwright-mcp-reachable') result = await runPlaywrightMcpReachableCheck(ctx);
|
|
244
|
+
else continue;
|
|
245
|
+
} else if (check === 'playwright-mcp-redundant') {
|
|
246
|
+
// Fires only on browser-facing projects (spec 4.5). Never runs on an
|
|
247
|
+
// API-only project even under an explicit --check ask.
|
|
248
|
+
if (!ctx.browserFacing) continue;
|
|
249
|
+
result = await runPlaywrightMcpRedundantCheck(ctx);
|
|
250
|
+
} else continue;
|
|
172
251
|
for (const d of result.drift) drift.push({ check, ...d });
|
|
173
252
|
for (const w of result.writes) writes.push(w);
|
|
174
253
|
}
|
|
@@ -183,10 +262,15 @@ export async function main(argv, deps = {}) {
|
|
|
183
262
|
const exitCode = ok ? 0 : 3;
|
|
184
263
|
|
|
185
264
|
if (flags.json) {
|
|
186
|
-
stdout.write(`${JSON.stringify({ ok, drift, writes }, null, 2)}\n`);
|
|
265
|
+
stdout.write(`${JSON.stringify({ ok, drift, writes, notices }, null, 2)}\n`);
|
|
187
266
|
return exitCode;
|
|
188
267
|
}
|
|
189
268
|
|
|
269
|
+
// Notices (spec 3.4 skip line) are diagnostic ground truth. Emitted BEFORE
|
|
270
|
+
// the summary so an operator scanning the top of the output sees why the
|
|
271
|
+
// three checks did not fire on a non-browser-facing project. Not
|
|
272
|
+
// suppressed by --quiet.
|
|
273
|
+
for (const notice of notices) stdout.write(`${notice}\n`);
|
|
190
274
|
writeHumanSummary({ stdout, ok, drift, writes, fixed: ctx.fix, quiet: Boolean(flags.quiet) });
|
|
191
275
|
return exitCode;
|
|
192
276
|
}
|
|
@@ -546,3 +630,96 @@ async function isPathIgnored(projectRoot) {
|
|
|
546
630
|
|
|
547
631
|
// Silence unused-import warnings for values consumed only via names.
|
|
548
632
|
void composeGitignoreInner;
|
|
633
|
+
|
|
634
|
+
/* ------------------------------------------------------------------ */
|
|
635
|
+
/* Check: playwright-present (spec 3.3) */
|
|
636
|
+
/* ------------------------------------------------------------------ */
|
|
637
|
+
|
|
638
|
+
async function runPlaywrightPresentCheck(ctx) {
|
|
639
|
+
const drift = [];
|
|
640
|
+
const result = ctx.checkPlaywrightPresentImpl(ctx.projectRoot);
|
|
641
|
+
if (!result.ok) {
|
|
642
|
+
drift.push({
|
|
643
|
+
item: 'missing-peer',
|
|
644
|
+
file: 'package.json',
|
|
645
|
+
message: FIX_LINES['playwright-present'],
|
|
646
|
+
refusedByFix: true,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
return { drift, writes: [] };
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/* ------------------------------------------------------------------ */
|
|
653
|
+
/* Check: browser-present (spec 3.3) */
|
|
654
|
+
/* ------------------------------------------------------------------ */
|
|
655
|
+
|
|
656
|
+
async function runBrowserPresentCheck(ctx) {
|
|
657
|
+
const drift = [];
|
|
658
|
+
const result = await ctx.checkBrowserPresentImpl();
|
|
659
|
+
if (!result.ok) {
|
|
660
|
+
drift.push({
|
|
661
|
+
item: 'no-browser',
|
|
662
|
+
file: '(system)',
|
|
663
|
+
message: FIX_LINES['browser-present'],
|
|
664
|
+
refusedByFix: true,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
return { drift, writes: [] };
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/* ------------------------------------------------------------------ */
|
|
671
|
+
/* Check: playwright-mcp-reachable (spec 3.3) */
|
|
672
|
+
/* ------------------------------------------------------------------ */
|
|
673
|
+
|
|
674
|
+
async function runPlaywrightMcpReachableCheck(ctx) {
|
|
675
|
+
const drift = [];
|
|
676
|
+
const result = await ctx.checkPlaywrightMcpReachableImpl();
|
|
677
|
+
if (!result.ok) {
|
|
678
|
+
drift.push({
|
|
679
|
+
item: result.timedOut ? 'unreachable-timeout' : 'unreachable',
|
|
680
|
+
file: '(npx @playwright/mcp)',
|
|
681
|
+
message: FIX_LINES['playwright-mcp-reachable'],
|
|
682
|
+
refusedByFix: true,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
return { drift, writes: [] };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/* ------------------------------------------------------------------ */
|
|
689
|
+
/* Check: playwright-mcp-redundant (spec 4.5) */
|
|
690
|
+
/* ------------------------------------------------------------------ */
|
|
691
|
+
|
|
692
|
+
async function runPlaywrightMcpRedundantCheck(ctx) {
|
|
693
|
+
const drift = [];
|
|
694
|
+
const mcpJson = await ctx.readMcpJsonImpl(ctx.projectRoot);
|
|
695
|
+
const projectKey = mcpJson ? findProjectPlaywrightKey(mcpJson) : null;
|
|
696
|
+
if (!projectKey) return { drift, writes: [] };
|
|
697
|
+
const probeResult = await ctx.probeClaudeCodeMcpImpl();
|
|
698
|
+
if (probeResult.kind !== 'found') return { drift, writes: [] };
|
|
699
|
+
drift.push({
|
|
700
|
+
item: 'redundant-entry',
|
|
701
|
+
file: '.mcp.json',
|
|
702
|
+
message: `project-scope .mcp.json carries a Playwright MCP entry ('${projectKey}') that is also declared at ${probeResult.scope} scope. The project entry shadows the user entry. Remove the project entry with \`rcf init --no-playwright-mcp\` (which re-runs init without writing it), or delete the '${projectKey}' entry from .mcp.json by hand.`,
|
|
703
|
+
refusedByFix: true,
|
|
704
|
+
});
|
|
705
|
+
return { drift, writes: [] };
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Default reader for the project-root .mcp.json body. Returns the parsed
|
|
710
|
+
* object, or null on missing / unparseable (doctor treats an unparseable
|
|
711
|
+
* .mcp.json as "no signature findable" rather than a hard refusal here; the
|
|
712
|
+
* merge path in agent-setup already refuses unparseable with exit 2 on write).
|
|
713
|
+
*
|
|
714
|
+
* @param {string} projectRoot
|
|
715
|
+
* @returns {Promise<object|null>}
|
|
716
|
+
*/
|
|
717
|
+
async function defaultReadMcpJson(projectRoot) {
|
|
718
|
+
const file = join(projectRoot, '.mcp.json');
|
|
719
|
+
try {
|
|
720
|
+
const raw = await readFile(file, 'utf8');
|
|
721
|
+
return JSON.parse(raw);
|
|
722
|
+
} catch {
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
}
|
package/src/cli/init.js
CHANGED
|
@@ -31,6 +31,11 @@ import {
|
|
|
31
31
|
writeAgentInstructions,
|
|
32
32
|
writeMcpConfig,
|
|
33
33
|
} from '../setup/agent-setup.js';
|
|
34
|
+
import {
|
|
35
|
+
findProjectPlaywrightKey,
|
|
36
|
+
probeClaudeCodeMcp,
|
|
37
|
+
} from '../setup/playwright-checks.js';
|
|
38
|
+
import { PLAYWRIGHT_MCP_VERSION } from '../verify/engine/launcher.js';
|
|
34
39
|
import { writeKnowledgeSeed } from '../setup/knowledge-seed.js';
|
|
35
40
|
import { writeIdentityTemplate } from '../setup/identity-seed.js';
|
|
36
41
|
import {
|
|
@@ -46,6 +51,7 @@ const OPTION_SPEC = {
|
|
|
46
51
|
'project-name': { type: 'string' },
|
|
47
52
|
'non-interactive': { type: 'boolean' },
|
|
48
53
|
'no-agent-setup': { type: 'boolean' },
|
|
54
|
+
'no-playwright-mcp': { type: 'boolean' },
|
|
49
55
|
quiet: { type: 'boolean' },
|
|
50
56
|
help: { type: 'boolean' },
|
|
51
57
|
};
|
|
@@ -70,6 +76,11 @@ Options:
|
|
|
70
76
|
not on a TTY or when piped)
|
|
71
77
|
--no-agent-setup Scaffold the tree only; print the manual
|
|
72
78
|
harness-wiring instructions instead
|
|
79
|
+
--no-playwright-mcp Skip the Playwright MCP entry step. The probe
|
|
80
|
+
still runs so the print-out remains honest, but
|
|
81
|
+
init writes no Playwright entry and touches no
|
|
82
|
+
existing one. Use when a user-scope Playwright
|
|
83
|
+
entry is declared in a harness init cannot probe.
|
|
73
84
|
--quiet Suppress non-error stdout
|
|
74
85
|
--help Print this help
|
|
75
86
|
`;
|
|
@@ -172,6 +183,26 @@ export async function main(argv, deps = {}) {
|
|
|
172
183
|
return 2;
|
|
173
184
|
}
|
|
174
185
|
|
|
186
|
+
// Step 1a: Playwright MCP entry (spec 2026-09-03, section 4). Writes a
|
|
187
|
+
// distinctly named 'playwright-rcf' entry ONLY when init can prove no
|
|
188
|
+
// Playwright entry exists at any scope the harness can report. Detection
|
|
189
|
+
// is by command tail (spec 4.1). Cross-scope probe of the Claude Code
|
|
190
|
+
// harness via `claude mcp list` (spec 4.2). Where the harness cannot be
|
|
191
|
+
// probed non-interactively, init falls back to the distinctly-named entry
|
|
192
|
+
// with a notice (spec 4.3). --no-playwright-mcp suppresses the write
|
|
193
|
+
// entirely; the probe still runs so the print-out remains honest.
|
|
194
|
+
const playwrightPass = await runPlaywrightMcpPass({
|
|
195
|
+
projectRoot: cwd,
|
|
196
|
+
stdout,
|
|
197
|
+
quiet: Boolean(flags.quiet),
|
|
198
|
+
optOut: Boolean(flags['no-playwright-mcp']),
|
|
199
|
+
probeClaudeCodeMcp: deps.probeClaudeCodeMcp ?? probeClaudeCodeMcp,
|
|
200
|
+
});
|
|
201
|
+
if (playwrightPass && 'kind' in playwrightPass && 'message' in playwrightPass) {
|
|
202
|
+
stderr.write(`[error] ${playwrightPass.kind} ${playwrightPass.message}\n`);
|
|
203
|
+
return 2;
|
|
204
|
+
}
|
|
205
|
+
|
|
175
206
|
// Step 2: agent-instructions managed block inside rcf markers (idempotent).
|
|
176
207
|
// Source is the 0.6.0 canonical asset, not the harness-template fenced
|
|
177
208
|
// fragment (which is now regenerated from the same canonical text at
|
|
@@ -297,3 +328,138 @@ function countLocal(haystack, needle) {
|
|
|
297
328
|
i = at + needle.length;
|
|
298
329
|
}
|
|
299
330
|
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Init's Playwright MCP pass (spec 2026-09-03, section 4). Decision tree:
|
|
334
|
+
*
|
|
335
|
+
* 1. If --no-playwright-mcp: probe still runs so the print-out is honest,
|
|
336
|
+
* but no .mcp.json entry is written or touched.
|
|
337
|
+
* 2. If the project-scope .mcp.json carries a Playwright signature under
|
|
338
|
+
* any key (spec 4.1): print the 'left alone' line and return.
|
|
339
|
+
* 3. Probe the Claude Code harness (spec 4.2):
|
|
340
|
+
* - 'found': print the 'already registered at <scope> scope' line and
|
|
341
|
+
* return (no shadowing).
|
|
342
|
+
* - 'none': write the distinctly-named 'playwright-rcf' entry.
|
|
343
|
+
* - 'inconclusive' (claude absent, non-zero, or unparseable): write the
|
|
344
|
+
* distinctly-named 'playwright-rcf' entry with the 'could not probe'
|
|
345
|
+
* notice.
|
|
346
|
+
*
|
|
347
|
+
* A distinct project-scope entry keeps init's "never write outside the
|
|
348
|
+
* project root" discipline; a coexisting user-scope entry the operator has
|
|
349
|
+
* that init could not see is not shadowed by naming convention.
|
|
350
|
+
*
|
|
351
|
+
* @param {object} args
|
|
352
|
+
* @param {string} args.projectRoot
|
|
353
|
+
* @param {NodeJS.WritableStream} args.stdout
|
|
354
|
+
* @param {boolean} args.quiet
|
|
355
|
+
* @param {boolean} args.optOut
|
|
356
|
+
* @param {import('../setup/playwright-checks.js').probeClaudeCodeMcp} args.probeClaudeCodeMcp
|
|
357
|
+
* @returns {Promise<{ action: 'left-alone-project'|'left-alone-harness'|'written'|'skipped-opt-out', name?: string, scope?: string } | import('../core/errors/index.js').RcfError>}
|
|
358
|
+
*/
|
|
359
|
+
async function runPlaywrightMcpPass({ projectRoot, stdout, quiet, optOut, probeClaudeCodeMcp: probeImpl }) {
|
|
360
|
+
const mcpPath = join(projectRoot, '.mcp.json');
|
|
361
|
+
let mcpJson = null;
|
|
362
|
+
try {
|
|
363
|
+
const raw = await readFile(mcpPath, 'utf8');
|
|
364
|
+
try {
|
|
365
|
+
mcpJson = JSON.parse(raw);
|
|
366
|
+
} catch {
|
|
367
|
+
// writeMcpConfig already refused unparseable with a distinct error and
|
|
368
|
+
// returned before us; if we somehow reach here with a corrupt file,
|
|
369
|
+
// stay out and let doctor surface it. Return silently.
|
|
370
|
+
return { action: 'skipped-opt-out' };
|
|
371
|
+
}
|
|
372
|
+
} catch (err) {
|
|
373
|
+
if (err.code !== 'ENOENT') throw err;
|
|
374
|
+
// .mcp.json does not exist yet (rcf mcp write above would have created
|
|
375
|
+
// it; if that step ran we should not hit ENOENT). Treat as no project
|
|
376
|
+
// entry and continue to the probe.
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const projectKey = mcpJson ? findProjectPlaywrightKey(mcpJson) : null;
|
|
380
|
+
// Gate finding 8 (2026-09-03): when --no-playwright-mcp is set, print the
|
|
381
|
+
// opt-out notice on EVERY terminal branch of this pass, in addition to the
|
|
382
|
+
// honest probe line. The probe still runs (spec 4.4) so the print-out is
|
|
383
|
+
// always honest about what init could see; the notice makes the reason a
|
|
384
|
+
// write did not happen unambiguous on the branches that would otherwise
|
|
385
|
+
// have written.
|
|
386
|
+
const optOutLine = 'Playwright MCP: --no-playwright-mcp set; nothing written.\n';
|
|
387
|
+
|
|
388
|
+
if (projectKey) {
|
|
389
|
+
if (!quiet) {
|
|
390
|
+
stdout.write(
|
|
391
|
+
`Playwright MCP: already registered in .mcp.json under key '${projectKey}' at project scope, left alone.\n`,
|
|
392
|
+
);
|
|
393
|
+
if (optOut) stdout.write(optOutLine);
|
|
394
|
+
}
|
|
395
|
+
return { action: 'left-alone-project', name: projectKey };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Probe the harness even when --no-playwright-mcp so the print-out remains
|
|
399
|
+
// honest about what init can see.
|
|
400
|
+
const probeResult = await probeImpl();
|
|
401
|
+
if (probeResult.kind === 'found') {
|
|
402
|
+
if (!quiet) {
|
|
403
|
+
stdout.write(
|
|
404
|
+
`Playwright MCP: already registered under key '${probeResult.name}' at ${probeResult.scope} scope in Claude Code, left alone.\n`,
|
|
405
|
+
);
|
|
406
|
+
if (optOut) stdout.write(optOutLine);
|
|
407
|
+
}
|
|
408
|
+
return { action: 'left-alone-harness', name: probeResult.name, scope: probeResult.scope };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (optOut) {
|
|
412
|
+
if (!quiet) {
|
|
413
|
+
// The honest probe line first, then the opt-out notice.
|
|
414
|
+
if (probeResult.kind === 'inconclusive') {
|
|
415
|
+
stdout.write(
|
|
416
|
+
`Playwright MCP: could not probe user-scope entries (${probeResult.reason}).\n`,
|
|
417
|
+
);
|
|
418
|
+
} else {
|
|
419
|
+
stdout.write(
|
|
420
|
+
'Playwright MCP: no ambient Playwright entry found at any scope this init could probe.\n',
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
stdout.write(optOutLine);
|
|
424
|
+
}
|
|
425
|
+
return { action: 'skipped-opt-out' };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Compose the distinctly-named entry. Distinct from 'playwright' (the
|
|
429
|
+
// common name a user-scope entry uses) and from any name Claude Code
|
|
430
|
+
// writes by default. If init could not see a user-scope entry that
|
|
431
|
+
// actually exists, both coexist and no shadowing has occurred; verify
|
|
432
|
+
// provisions its OWN MCP config anyway.
|
|
433
|
+
const distinctEntry = {
|
|
434
|
+
type: 'stdio',
|
|
435
|
+
command: 'npx',
|
|
436
|
+
args: ['-y', `@playwright/mcp@${PLAYWRIGHT_MCP_VERSION}`],
|
|
437
|
+
env: {},
|
|
438
|
+
};
|
|
439
|
+
const nextBody = mcpJson ?? {};
|
|
440
|
+
const servers = (nextBody.mcpServers && typeof nextBody.mcpServers === 'object' && !Array.isArray(nextBody.mcpServers))
|
|
441
|
+
? nextBody.mcpServers
|
|
442
|
+
: {};
|
|
443
|
+
const nextConfig = {
|
|
444
|
+
...nextBody,
|
|
445
|
+
mcpServers: {
|
|
446
|
+
...servers,
|
|
447
|
+
'playwright-rcf': distinctEntry,
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
await writeFile(mcpPath, `${JSON.stringify(nextConfig, null, 2)}\n`, 'utf8');
|
|
451
|
+
|
|
452
|
+
if (!quiet) {
|
|
453
|
+
if (probeResult.kind === 'inconclusive') {
|
|
454
|
+
stdout.write(
|
|
455
|
+
`Playwright MCP: could not probe user-scope entries (${probeResult.reason}). Wrote 'playwright-rcf' at project scope; remove with \`rcf init --no-playwright-mcp\` or delete the entry by hand.\n`,
|
|
456
|
+
);
|
|
457
|
+
} else {
|
|
458
|
+
stdout.write(
|
|
459
|
+
"Playwright MCP: wrote 'playwright-rcf' at project scope (no ambient Playwright entry found at any scope this init could probe). Remove with `rcf init --no-playwright-mcp` or delete the entry by hand.\n",
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return { action: 'written' };
|
|
464
|
+
}
|
|
465
|
+
|