@nick848/fet 1.1.10 → 1.1.12
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/README.md +5 -1
- package/README_en.md +4 -1
- package/dist/cli/index.js +1530 -135
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2090,6 +2090,12 @@ function renderFetConfig(scan, language = "zh-CN") {
|
|
|
2090
2090
|
mode: "require_before_apply",
|
|
2091
2091
|
whenNoTestScript: "block"
|
|
2092
2092
|
},
|
|
2093
|
+
visual: {
|
|
2094
|
+
enabled: true,
|
|
2095
|
+
compareMode: "layout-only",
|
|
2096
|
+
requireBeforeVerify: "when_figma",
|
|
2097
|
+
whenNoCapture: "warn"
|
|
2098
|
+
},
|
|
2093
2099
|
specLanguage: {
|
|
2094
2100
|
style: "layered_bilingual",
|
|
2095
2101
|
canonical: "en",
|
|
@@ -2274,7 +2280,7 @@ function tddResultsRelativePath(changeId) {
|
|
|
2274
2280
|
|
|
2275
2281
|
// src/templates/tdd.ts
|
|
2276
2282
|
function renderTddInstructions(changeId, manifest, language) {
|
|
2277
|
-
const
|
|
2283
|
+
const manifestPath3 = tddManifestRelativePath(changeId);
|
|
2278
2284
|
const specPath = tddSpecRelativePath(changeId);
|
|
2279
2285
|
const caseList = manifest.cases.map((item) => `- \`${item.id}\`: ${item.title} \u2192 \`${item.testFile}\` (${item.testIds.join(", ")})`).join("\n");
|
|
2280
2286
|
if (language === "en") {
|
|
@@ -2293,13 +2299,13 @@ Create or update unit tests **before** marking implementation tasks done in \`ta
|
|
|
2293
2299
|
## Sources
|
|
2294
2300
|
${manifest.sources.map((s) => `- ${s}`).join("\n")}
|
|
2295
2301
|
|
|
2296
|
-
## Cases (from ${
|
|
2302
|
+
## Cases (from ${manifestPath3})
|
|
2297
2303
|
${caseList}
|
|
2298
2304
|
|
|
2299
2305
|
## Rules
|
|
2300
2306
|
1. Each case must map to a real test file under the repo test tree.
|
|
2301
2307
|
2. Tests should fail until implementation lands (red \u2192 green).
|
|
2302
|
-
3. Do not edit \`${
|
|
2308
|
+
3. Do not edit \`${manifestPath3}\` by hand unless fixing IDs; re-run \`fet tdd --change ${changeId}\` after planning changes.
|
|
2303
2309
|
4. When tests exist, run \`fet test --change ${changeId}\` before \`fet verify\`.
|
|
2304
2310
|
|
|
2305
2311
|
Human-readable matrix: \`${specPath}\`
|
|
@@ -2320,13 +2326,13 @@ generatedAt: ${manifest.generatedAt}
|
|
|
2320
2326
|
## \u6765\u6E90
|
|
2321
2327
|
${manifest.sources.map((s) => `- ${s}`).join("\n")}
|
|
2322
2328
|
|
|
2323
|
-
## \u7528\u4F8B\uFF08\u89C1 ${
|
|
2329
|
+
## \u7528\u4F8B\uFF08\u89C1 ${manifestPath3}\uFF09
|
|
2324
2330
|
${caseList}
|
|
2325
2331
|
|
|
2326
2332
|
## \u89C4\u5219
|
|
2327
2333
|
1. \u6BCF\u4E2A\u7528\u4F8B\u5FC5\u987B\u5BF9\u5E94\u4ED3\u5E93\u5185\u771F\u5B9E\u6D4B\u8BD5\u6587\u4EF6\u3002
|
|
2328
2334
|
2. \u5B9E\u73B0\u843D\u5730\u524D\u6D4B\u8BD5\u5E94\u5904\u4E8E\u5931\u8D25\uFF08\u7EA2\uFF09\u72B6\u6001\uFF0C\u843D\u5730\u540E\u5E94\u53D8\u7EFF\u3002
|
|
2329
|
-
3. \u9664\u975E\u4FEE\u6B63 ID\uFF0C\u4E0D\u8981\u624B\u6539 \`${
|
|
2335
|
+
3. \u9664\u975E\u4FEE\u6B63 ID\uFF0C\u4E0D\u8981\u624B\u6539 \`${manifestPath3}\`\uFF1B\u89C4\u5212\u53D8\u66F4\u540E\u8BF7\u91CD\u65B0\u6267\u884C \`fet tdd --change ${changeId}\`\u3002
|
|
2330
2336
|
4. \u6D4B\u8BD5\u5C31\u7EEA\u540E\u5148 \`fet test --change ${changeId}\`\uFF0C\u518D \`fet verify\`\u3002
|
|
2331
2337
|
|
|
2332
2338
|
\u53EF\u8BFB\u77E9\u9635\u89C1 \`${specPath}\`
|
|
@@ -2371,19 +2377,151 @@ function escapeTable(value) {
|
|
|
2371
2377
|
return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
2372
2378
|
}
|
|
2373
2379
|
function renderTddApplyNextSteps(changeId, language) {
|
|
2374
|
-
const
|
|
2380
|
+
const manifestPath3 = tddManifestRelativePath(changeId);
|
|
2375
2381
|
if (language === "en") {
|
|
2376
2382
|
return [
|
|
2377
|
-
`Read ${
|
|
2383
|
+
`Read ${manifestPath3} and tdd-instructions.md; implement code until fet test passes for this change.`,
|
|
2378
2384
|
`Run fet test --change ${changeId} before fet verify.`
|
|
2379
2385
|
];
|
|
2380
2386
|
}
|
|
2381
2387
|
return [
|
|
2382
|
-
`\u9605\u8BFB ${
|
|
2388
|
+
`\u9605\u8BFB ${manifestPath3} \u4E0E tdd-instructions.md\uFF1B\u5B9E\u73B0\u4EE3\u7801\u76F4\u81F3\u672C change \u7684 fet test \u901A\u8FC7\u3002`,
|
|
2383
2389
|
`\u5728 fet verify \u4E4B\u524D\u5148\u6267\u884C fet test --change ${changeId}\u3002`
|
|
2384
2390
|
];
|
|
2385
2391
|
}
|
|
2386
2392
|
|
|
2393
|
+
// src/visual/paths.ts
|
|
2394
|
+
function visualFetDirRelative(changeId) {
|
|
2395
|
+
return `openspec/changes/${changeId}/.fet`;
|
|
2396
|
+
}
|
|
2397
|
+
function visualManifestRelativePath(changeId) {
|
|
2398
|
+
return `${visualFetDirRelative(changeId)}/visual-manifest.yaml`;
|
|
2399
|
+
}
|
|
2400
|
+
function visualSpecRelativePath(changeId) {
|
|
2401
|
+
return `${visualFetDirRelative(changeId)}/visual-spec.md`;
|
|
2402
|
+
}
|
|
2403
|
+
function visualInstructionsRelativePath(changeId) {
|
|
2404
|
+
return `${visualFetDirRelative(changeId)}/visual-instructions.md`;
|
|
2405
|
+
}
|
|
2406
|
+
function visualCaptureRelativePath(changeId) {
|
|
2407
|
+
return `${visualFetDirRelative(changeId)}/visual-capture.json`;
|
|
2408
|
+
}
|
|
2409
|
+
function visualResultsRelativePath(changeId) {
|
|
2410
|
+
return `${visualFetDirRelative(changeId)}/visual-results.json`;
|
|
2411
|
+
}
|
|
2412
|
+
function visualBaselinesDirRelative(changeId) {
|
|
2413
|
+
return `${visualFetDirRelative(changeId)}/visual-baselines`;
|
|
2414
|
+
}
|
|
2415
|
+
function visualPageScreenshotRelative(changeId, pageId) {
|
|
2416
|
+
return `${visualBaselinesDirRelative(changeId)}/${pageId}/implementation.png`;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
// src/templates/visual.ts
|
|
2420
|
+
function renderVisualInstructions(changeId, manifest, language) {
|
|
2421
|
+
const pages = manifest.pages.map(
|
|
2422
|
+
(page) => `- \`${page.id}\`: ${page.title} \u2192 \`${page.route}\` (ignore: ${page.ignoreSelectors.join(", ") || "none"})`
|
|
2423
|
+
).join("\n");
|
|
2424
|
+
if (language === "en") {
|
|
2425
|
+
return `---
|
|
2426
|
+
schemaVersion: 1
|
|
2427
|
+
fetVersion: ${FET_VERSION}
|
|
2428
|
+
changeId: ${changeId}
|
|
2429
|
+
purpose: visual-instructions
|
|
2430
|
+
generatedAt: ${manifest.generatedAt}
|
|
2431
|
+
compareMode: layout-only
|
|
2432
|
+
---
|
|
2433
|
+
|
|
2434
|
+
# Visual verification (layout-only)
|
|
2435
|
+
|
|
2436
|
+
Compare **layout / spacing / shell regions** only. Do **not** pixel-match dynamic API text, list rows, or images.
|
|
2437
|
+
|
|
2438
|
+
## Figma references
|
|
2439
|
+
${manifest.figmaUrls.map((url) => `- ${url}`).join("\n") || "- (none detected)"}
|
|
2440
|
+
|
|
2441
|
+
## Pages
|
|
2442
|
+
${pages}
|
|
2443
|
+
|
|
2444
|
+
## Default command
|
|
2445
|
+
|
|
2446
|
+
\`\`\`sh
|
|
2447
|
+
fet visual --change ${changeId}
|
|
2448
|
+
\`\`\`
|
|
2449
|
+
|
|
2450
|
+
Runs manifest refresh, capture (Playwright + \`--base-url\`), and layout checks in one step.
|
|
2451
|
+
|
|
2452
|
+
See \`${visualSpecRelativePath(changeId)}\` for the human-readable matrix.
|
|
2453
|
+
`;
|
|
2454
|
+
}
|
|
2455
|
+
return `---
|
|
2456
|
+
schemaVersion: 1
|
|
2457
|
+
fetVersion: ${FET_VERSION}
|
|
2458
|
+
changeId: ${changeId}
|
|
2459
|
+
purpose: visual-instructions
|
|
2460
|
+
generatedAt: ${manifest.generatedAt}
|
|
2461
|
+
compareMode: layout-only
|
|
2462
|
+
---
|
|
2463
|
+
|
|
2464
|
+
# \u89C6\u89C9\u9A8C\u6536\uFF08\u4EC5 layout-only\uFF09
|
|
2465
|
+
|
|
2466
|
+
\u53EA\u9A8C\u6536 **\u6392\u7248 / \u95F4\u8DDD / \u58F3\u5C42\u533A\u57DF**\uFF0C**\u4E0D\u8981**\u5BF9\u52A8\u6001\u63A5\u53E3\u6587\u6848\u3001\u5217\u8868\u884C\u3001\u56FE\u7247\u505A\u50CF\u7D20\u7EA7\u5BF9\u6BD4\u3002
|
|
2467
|
+
|
|
2468
|
+
## Figma \u5F15\u7528
|
|
2469
|
+
${manifest.figmaUrls.map((url) => `- ${url}`).join("\n") || "- \uFF08\u672A\u68C0\u6D4B\u5230\uFF09"}
|
|
2470
|
+
|
|
2471
|
+
## \u9875\u9762
|
|
2472
|
+
${pages}
|
|
2473
|
+
|
|
2474
|
+
## \u9ED8\u8BA4\u547D\u4EE4
|
|
2475
|
+
|
|
2476
|
+
\`\`\`sh
|
|
2477
|
+
fet visual --change ${changeId}
|
|
2478
|
+
\`\`\`
|
|
2479
|
+
|
|
2480
|
+
\u4E00\u6761\u547D\u4EE4\u5B8C\u6210\u6E05\u5355\u66F4\u65B0\u3001\u622A\u56FE\uFF08Playwright + \`--base-url\`\uFF09\u4E0E\u5E03\u5C40\u68C0\u67E5\u3002
|
|
2481
|
+
|
|
2482
|
+
\u8BE6\u89C1 \`${visualSpecRelativePath(changeId)}\`\u3002
|
|
2483
|
+
`;
|
|
2484
|
+
}
|
|
2485
|
+
function renderVisualSpec(changeId, manifest, language) {
|
|
2486
|
+
const rows = manifest.pages.flatMap(
|
|
2487
|
+
(page) => page.checkRegions.map(
|
|
2488
|
+
(region) => `| ${page.id} | ${page.route} | \`${region.selector}\` | ${region.checks.join(", ")} | ${page.ignoreSelectors.join(", ") || "-"} |`
|
|
2489
|
+
)
|
|
2490
|
+
).join("\n");
|
|
2491
|
+
if (language === "en") {
|
|
2492
|
+
return `---
|
|
2493
|
+
changeId: ${changeId}
|
|
2494
|
+
compareMode: layout-only
|
|
2495
|
+
planningFingerprint: ${manifest.planningFingerprint}
|
|
2496
|
+
---
|
|
2497
|
+
|
|
2498
|
+
# Visual case matrix (layout-only)
|
|
2499
|
+
|
|
2500
|
+
| Page | Route | Region | Checks | Ignored dynamic |
|
|
2501
|
+
|------|-------|--------|--------|-----------------|
|
|
2502
|
+
${rows}
|
|
2503
|
+
`;
|
|
2504
|
+
}
|
|
2505
|
+
return `---
|
|
2506
|
+
changeId: ${changeId}
|
|
2507
|
+
compareMode: layout-only
|
|
2508
|
+
planningFingerprint: ${manifest.planningFingerprint}
|
|
2509
|
+
---
|
|
2510
|
+
|
|
2511
|
+
# \u89C6\u89C9\u7528\u4F8B\u77E9\u9635\uFF08\u4EC5 layout-only\uFF09
|
|
2512
|
+
|
|
2513
|
+
| \u9875\u9762 | \u8DEF\u7531 | \u533A\u57DF | \u68C0\u67E5\u9879 | \u5FFD\u7565\u7684\u52A8\u6001\u533A |
|
|
2514
|
+
|------|------|------|--------|--------------|
|
|
2515
|
+
${rows}
|
|
2516
|
+
`;
|
|
2517
|
+
}
|
|
2518
|
+
function renderVisualVerifyNextSteps(changeId, language) {
|
|
2519
|
+
if (language === "en") {
|
|
2520
|
+
return [`Run fet visual --change ${changeId} before fet verify when this change references Figma.`];
|
|
2521
|
+
}
|
|
2522
|
+
return [`\u82E5\u672C change \u5F15\u7528 Figma\uFF0C\u8BF7\u5728 fet verify \u4E4B\u524D\u6267\u884C fet visual --change ${changeId}\u3002`];
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2387
2525
|
// src/templates/figma-guard.ts
|
|
2388
2526
|
var FIGMA_URL_PATTERN = /https?:\/\/(?:www\.)?figma\.com\/(?:file|design|proto)\/[^\s)\]"'<>]+/gi;
|
|
2389
2527
|
function figmaStopHandoffRelativePath(changeId) {
|
|
@@ -2862,6 +3000,390 @@ When the artifact is \`specs/<capability>/spec.md\` (or you edit spec files in t
|
|
|
2862
3000
|
3. \u82F1\u6587\u89C4\u8303\u53E5\u6709\u4EFB\u4F55\u53D8\u52A8\u65F6\uFF0C**\u540C\u4E00\u6B21\u7F16\u8F91**\u5FC5\u987B\u540C\u6B65\u66F4\u65B0\u5BF9\u5E94\u4E2D\u6587\u6CE8\u91CA\u3002${uiBlock}`;
|
|
2863
3001
|
}
|
|
2864
3002
|
|
|
3003
|
+
// src/templates/write-boundary.ts
|
|
3004
|
+
var WRITE_BOUNDARY_ALLOW_PREFIXES = ["src/", "openspec/"];
|
|
3005
|
+
var WRITE_BOUNDARY_ROOT_CONFIG_EXACT = [
|
|
3006
|
+
".gitignore",
|
|
3007
|
+
".gitattributes",
|
|
3008
|
+
".stylelintignore",
|
|
3009
|
+
".eslintignore",
|
|
3010
|
+
".prettierignore",
|
|
3011
|
+
".editorconfig",
|
|
3012
|
+
".npmrc",
|
|
3013
|
+
".nvmrc",
|
|
3014
|
+
".node-version",
|
|
3015
|
+
".checkrc.js",
|
|
3016
|
+
"package.json",
|
|
3017
|
+
"package-lock.json",
|
|
3018
|
+
"pnpm-lock.yaml",
|
|
3019
|
+
"yarn.lock",
|
|
3020
|
+
"bun.lockb",
|
|
3021
|
+
"npm-shrinkwrap.json"
|
|
3022
|
+
];
|
|
3023
|
+
var WRITE_BOUNDARY_ROOT_CONFIG_BASENAME_PREFIXES = [
|
|
3024
|
+
".eslintrc",
|
|
3025
|
+
".stylelintrc",
|
|
3026
|
+
".prettierrc",
|
|
3027
|
+
".stylelint",
|
|
3028
|
+
"eslint.config",
|
|
3029
|
+
"stylelint.config",
|
|
3030
|
+
"prettier.config",
|
|
3031
|
+
"tsconfig",
|
|
3032
|
+
"vitest.config",
|
|
3033
|
+
"vite.config",
|
|
3034
|
+
"tsup.config",
|
|
3035
|
+
"jest.config",
|
|
3036
|
+
"rollup.config",
|
|
3037
|
+
"webpack.config",
|
|
3038
|
+
"babel.config",
|
|
3039
|
+
"biome.json",
|
|
3040
|
+
"deno.json",
|
|
3041
|
+
"deno.jsonc"
|
|
3042
|
+
];
|
|
3043
|
+
function renderRootConfigPathList(language) {
|
|
3044
|
+
const samples = language === "en" ? "`.gitignore`, `.stylelintignore`, `.stylelintrc*`, `.eslintrc*`, `eslint.config.*`, `.checkrc.js`, `package.json`, `package-lock.json`, `tsconfig*.json`, `.prettierrc*`, tool configs at repo root (`vitest.config.*`, `vite.config.*`, \u2026)" : "`.gitignore`\u3001`.stylelintignore`\u3001`.stylelintrc*`\u3001`.eslintrc*`\u3001`eslint.config.*`\u3001`.checkrc.js`\u3001`package.json`\u3001`package-lock.json`\u3001`tsconfig*.json`\u3001`.prettierrc*`\u3001\u4EE5\u53CA\u4ED3\u5E93\u6839\u76EE\u5F55\u4E0B\u7684 `vitest.config.*`\u3001`vite.config.*` \u7B49\u5DE5\u5177\u914D\u7F6E";
|
|
3045
|
+
return samples;
|
|
3046
|
+
}
|
|
3047
|
+
function renderWriteBoundaryGuardrail(language) {
|
|
3048
|
+
const rootList = renderRootConfigPathList(language);
|
|
3049
|
+
if (language === "en") {
|
|
3050
|
+
return `- Default write scope: \`src/**\`, \`openspec/**\`, and any \`**/.fet/**\`. All other paths need explicit user approval first.
|
|
3051
|
+
- **Repo-root config is high risk**: ${rootList} \u2014 list each file and why before editing; never change these silently.`;
|
|
3052
|
+
}
|
|
3053
|
+
return `- \u9ED8\u8BA4\u53EA\u5141\u8BB8\u4FEE\u6539 \`src/**\`\u3001\`openspec/**\` \u53CA\u4EFB\u610F \`**/.fet/**\`\uFF1B\u5176\u4F59\u8DEF\u5F84\u987B\u5148\u83B7\u7528\u6237\u660E\u786E\u540C\u610F\u3002
|
|
3054
|
+
- **\u4ED3\u5E93\u6839\u76EE\u5F55\u914D\u7F6E\u6587\u4EF6\u9AD8\u98CE\u9669**\uFF1A${rootList} \u2014 \u4FEE\u6539\u524D\u987B\u5217\u51FA\u6587\u4EF6\u4E0E\u539F\u56E0\uFF0C\u7981\u6B62\u64C5\u81EA\u6539\u52A8\u3002`;
|
|
3055
|
+
}
|
|
3056
|
+
function renderWriteBoundaryPolicyBody(language) {
|
|
3057
|
+
if (language === "en") {
|
|
3058
|
+
return `## Default allowed write scope
|
|
3059
|
+
|
|
3060
|
+
- \`src/**\` \u2014 application/library source
|
|
3061
|
+
- \`openspec/**\` \u2014 OpenSpec specs and change artifacts
|
|
3062
|
+
- \`**/.fet/**\` \u2014 per-change FET handoff files (including \`openspec/changes/<id>/.fet/\`)
|
|
3063
|
+
|
|
3064
|
+
## Ask the user first
|
|
3065
|
+
|
|
3066
|
+
Before creating, editing, or deleting files **outside** the allowed scope:
|
|
3067
|
+
|
|
3068
|
+
1. List every path you need to touch and why.
|
|
3069
|
+
2. Wait for explicit user approval (do not assume silence means yes).
|
|
3070
|
+
3. Prefer minimal diffs.
|
|
3071
|
+
|
|
3072
|
+
Common paths that need approval: \`tests/**\`, \`.github/**\`, \`.workflow/**\`, \`.cursor/**\`, \`.codex/**\`, \`AGENTS.md\`, \`README*\`, \`.env*\`.
|
|
3073
|
+
|
|
3074
|
+
## Repo-root config (always ask \u2014 never silent edit)
|
|
3075
|
+
|
|
3076
|
+
These live at the **repository root** (no subdirectory). Treat every change as infrastructure impact:
|
|
3077
|
+
|
|
3078
|
+
${renderRootConfigPathList(language)}
|
|
3079
|
+
|
|
3080
|
+
Workflow:
|
|
3081
|
+
|
|
3082
|
+
1. State **exact filenames** (e.g. \`package.json\`, \`.eslintrc.js\`, \`.gitignore\`).
|
|
3083
|
+
2. Explain **why** each file must change.
|
|
3084
|
+
3. Wait for **explicit user approval** before writing.
|
|
3085
|
+
4. Do not \u201Cfix lint/format\u201D by editing root configs unless the user requested that scope.
|
|
3086
|
+
|
|
3087
|
+
## Forbidden without approval
|
|
3088
|
+
|
|
3089
|
+
- Dependency or lockfile changes (\`package.json\`, \`package-lock.json\`, etc.) unless the user asked
|
|
3090
|
+
- Lint/format/tooling config at repo root (\`.eslintrc*\`, \`.stylelint*\`, \`.checkrc.js\`, \`.gitignore\`, \u2026) unless the user asked
|
|
3091
|
+
- Secrets or credential files
|
|
3092
|
+
- Using shell redirects or destructive git commands to bypass this policy
|
|
3093
|
+
|
|
3094
|
+
## Cursor enforcement
|
|
3095
|
+
|
|
3096
|
+
When \`.cursor/hooks/fet-guard-write-paths.mjs\` is installed, out-of-scope **write tools** trigger an approval prompt (\`permission: ask\`). Shell writes are gated by \`fet-guard-shell-writes.mjs\`. Rules alone are not sufficient\u2014keep hooks enabled after \`fet init\`.`;
|
|
3097
|
+
}
|
|
3098
|
+
return `## \u9ED8\u8BA4\u5141\u8BB8\u4FEE\u6539\u7684\u8303\u56F4
|
|
3099
|
+
|
|
3100
|
+
- \`src/**\` \u2014 \u4E1A\u52A1/\u5E93\u6E90\u7801
|
|
3101
|
+
- \`openspec/**\` \u2014 OpenSpec \u89C4\u8303\u4E0E change \u4EA7\u7269
|
|
3102
|
+
- \`**/.fet/**\` \u2014 \u5404 change \u7684 FET \u4EA4\u63A5\u6587\u4EF6\uFF08\u542B \`openspec/changes/<id>/.fet/\`\uFF09
|
|
3103
|
+
|
|
3104
|
+
## \u987B\u5148\u5F81\u5F97\u7528\u6237\u540C\u610F
|
|
3105
|
+
|
|
3106
|
+
\u5728**\u5141\u8BB8\u8303\u56F4\u5916**\u521B\u5EFA\u3001\u4FEE\u6539\u6216\u5220\u9664\u6587\u4EF6\u4E4B\u524D\uFF1A
|
|
3107
|
+
|
|
3108
|
+
1. \u5217\u51FA\u5C06\u8981\u4FEE\u6539\u7684\u8DEF\u5F84\u53CA\u539F\u56E0\u3002
|
|
3109
|
+
2. \u7B49\u5F85\u7528\u6237\u660E\u786E\u540C\u610F\uFF08\u4E0D\u8981\u9ED8\u8BA4\u6C89\u9ED8\u5373\u540C\u610F\uFF09\u3002
|
|
3110
|
+
3. \u4FDD\u6301\u6700\u5C0F\u6539\u52A8\u3002
|
|
3111
|
+
|
|
3112
|
+
\u5E38\u89C1\u9700\u5BA1\u6279\u8DEF\u5F84\uFF1A\`tests/**\`\u3001\`.github/**\`\u3001\`.workflow/**\`\u3001\`.cursor/**\`\u3001\`.codex/**\`\u3001\`AGENTS.md\`\u3001\`README*\`\u3001\`.env*\`\u3002
|
|
3113
|
+
|
|
3114
|
+
## \u4ED3\u5E93\u6839\u76EE\u5F55\u914D\u7F6E\uFF08\u59CB\u7EC8\u987B\u8BE2\u95EE\uFF0C\u7981\u6B62\u9759\u9ED8\u4FEE\u6539\uFF09
|
|
3115
|
+
|
|
3116
|
+
\u4EE5\u4E0B\u6587\u4EF6\u4F4D\u4E8E**\u9879\u76EE\u6839\u76EE\u5F55**\uFF08\u8DEF\u5F84\u4E2D\u65E0\u5B50\u76EE\u5F55\uFF09\uFF0C\u4E00\u5F8B\u89C6\u4E3A\u57FA\u7840\u8BBE\u65BD\u7EA7\u6539\u52A8\uFF1A
|
|
3117
|
+
|
|
3118
|
+
${renderRootConfigPathList(language)}
|
|
3119
|
+
|
|
3120
|
+
\u6D41\u7A0B\uFF1A
|
|
3121
|
+
|
|
3122
|
+
1. \u660E\u786E\u5217\u51FA**\u5B8C\u6574\u6587\u4EF6\u540D**\uFF08\u5982 \`package.json\`\u3001\`.eslintrc.js\`\u3001\`.gitignore\`\uFF09\u3002
|
|
3123
|
+
2. \u8BF4\u660E**\u6BCF\u9879\u4FEE\u6539\u7684\u539F\u56E0**\u3002
|
|
3124
|
+
3. \u83B7\u5F97\u7528\u6237**\u660E\u786E\u540C\u610F**\u540E\u518D\u5199\u5165\u3002
|
|
3125
|
+
4. \u4E0D\u8981\u4EE5\u300C\u987A\u4FBF\u4FEE lint/\u683C\u5F0F\u300D\u4E3A\u7531\u64C5\u81EA\u6539\u6839\u76EE\u5F55\u914D\u7F6E\u3002
|
|
3126
|
+
|
|
3127
|
+
## \u672A\u7ECF\u540C\u610F\u7981\u6B62
|
|
3128
|
+
|
|
3129
|
+
- \u64C5\u81EA\u6539\u4F9D\u8D56\u6216\u9501\u6587\u4EF6\uFF08\`package.json\`\u3001\`package-lock.json\` \u7B49\uFF09\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42
|
|
3130
|
+
- \u64C5\u81EA\u6539\u6839\u76EE\u5F55 lint/\u683C\u5F0F\u5316/\u5DE5\u5177\u94FE\u914D\u7F6E\uFF08\`.eslintrc*\`\u3001\`.stylelint*\`\u3001\`.checkrc.js\`\u3001\`.gitignore\` \u7B49\uFF09\uFF0C\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42
|
|
3131
|
+
- \u4FEE\u6539\u5BC6\u94A5\u6216\u51ED\u8BC1\u6587\u4EF6
|
|
3132
|
+
- \u7528 shell \u91CD\u5B9A\u5411\u6216\u7834\u574F\u6027 git \u547D\u4EE4\u7ED5\u8FC7\u672C\u7B56\u7565
|
|
3133
|
+
|
|
3134
|
+
## Cursor \u786C\u95E8\u7981
|
|
3135
|
+
|
|
3136
|
+
\u5B89\u88C5 \`.cursor/hooks/fet-guard-write-paths.mjs\` \u540E\uFF0C\u8D85\u51FA\u8303\u56F4\u7684**\u5199\u5DE5\u5177**\u4F1A\u5F39\u51FA\u5BA1\u6279\uFF08\`permission: ask\`\uFF09\uFF1B\`fet-guard-shell-writes.mjs\` \u7EA6\u675F\u53EF\u80FD\u5199\u6587\u4EF6\u7684 shell\u3002\u4EC5\u89C4\u5219\u4E0D\u591F\u2014\u2014\`fet init\` \u540E\u8BF7\u4FDD\u6301 hooks \u542F\u7528\u3002`;
|
|
3137
|
+
}
|
|
3138
|
+
function renderCursorWriteBoundaryRule(language) {
|
|
3139
|
+
const description = language === "en" ? "FET write boundary: default edits only under src/, openspec/, and .fet/" : "FET \u5199\u8DEF\u5F84\u8FB9\u754C\uFF1A\u9ED8\u8BA4\u4EC5\u53EF\u6539 src/\u3001openspec/\u3001.fet/";
|
|
3140
|
+
return `<!-- FET:MANAGED
|
|
3141
|
+
schemaVersion: 1
|
|
3142
|
+
fetVersion: ${FET_VERSION}
|
|
3143
|
+
generator: cursor-adapter
|
|
3144
|
+
adapterVersion: 1
|
|
3145
|
+
FET:END -->
|
|
3146
|
+
|
|
3147
|
+
---
|
|
3148
|
+
description: ${description}
|
|
3149
|
+
alwaysApply: true
|
|
3150
|
+
---
|
|
3151
|
+
|
|
3152
|
+
${renderWriteBoundaryPolicyBody(language)}
|
|
3153
|
+
|
|
3154
|
+
${renderWriteBoundaryGuardrail(language)}
|
|
3155
|
+
`;
|
|
3156
|
+
}
|
|
3157
|
+
function renderCodexWriteBoundaryGuide(language) {
|
|
3158
|
+
return `<!-- FET:MANAGED
|
|
3159
|
+
schemaVersion: 1
|
|
3160
|
+
fetVersion: ${FET_VERSION}
|
|
3161
|
+
generator: codex-adapter
|
|
3162
|
+
adapterVersion: 1
|
|
3163
|
+
FET:END -->
|
|
3164
|
+
|
|
3165
|
+
# ${language === "en" ? "Write path boundary (Codex)" : "\u5199\u8DEF\u5F84\u8FB9\u754C\uFF08Codex\uFF09"}
|
|
3166
|
+
|
|
3167
|
+
${renderWriteBoundaryPolicyBody(language)}
|
|
3168
|
+
|
|
3169
|
+
${renderWriteBoundaryGuardrail(language)}
|
|
3170
|
+
`;
|
|
3171
|
+
}
|
|
3172
|
+
function renderCursorWritePathsHookMjs() {
|
|
3173
|
+
const allowPrefixes = [...WRITE_BOUNDARY_ALLOW_PREFIXES];
|
|
3174
|
+
const rootExact = [...WRITE_BOUNDARY_ROOT_CONFIG_EXACT];
|
|
3175
|
+
const rootPrefixes = [...WRITE_BOUNDARY_ROOT_CONFIG_BASENAME_PREFIXES];
|
|
3176
|
+
return `#!/usr/bin/env node
|
|
3177
|
+
/**
|
|
3178
|
+
* FET:MANAGED
|
|
3179
|
+
* adapterVersion: 1
|
|
3180
|
+
* write-path guard for Cursor preToolUse (Write/StrReplace/EditNotebook/Delete).
|
|
3181
|
+
*/
|
|
3182
|
+
import { readFileSync } from "node:fs";
|
|
3183
|
+
|
|
3184
|
+
const ALLOW_PREFIXES = ${JSON.stringify(allowPrefixes)};
|
|
3185
|
+
const ROOT_CONFIG_EXACT = ${JSON.stringify(rootExact)};
|
|
3186
|
+
const ROOT_CONFIG_PREFIXES = ${JSON.stringify(rootPrefixes)};
|
|
3187
|
+
|
|
3188
|
+
function normalizePath(path) {
|
|
3189
|
+
return String(path ?? "")
|
|
3190
|
+
.replaceAll("\\\\", "/")
|
|
3191
|
+
.replace(/^\\.\\/+/, "")
|
|
3192
|
+
.replace(/^\\/+/u, "");
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
function isRootConfigPath(path) {
|
|
3196
|
+
const normalized = normalizePath(path);
|
|
3197
|
+
if (!normalized || normalized.includes("/")) return false;
|
|
3198
|
+
const base = normalized.toLowerCase();
|
|
3199
|
+
if (ROOT_CONFIG_EXACT.includes(base)) return true;
|
|
3200
|
+
return ROOT_CONFIG_PREFIXES.some((prefix) => base === prefix || base.startsWith(prefix));
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
function classifyPath(path) {
|
|
3204
|
+
const normalized = normalizePath(path);
|
|
3205
|
+
if (!normalized) return "ask";
|
|
3206
|
+
for (const prefix of ALLOW_PREFIXES) {
|
|
3207
|
+
const bare = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
3208
|
+
if (normalized === bare || normalized.startsWith(prefix)) return "allow";
|
|
3209
|
+
}
|
|
3210
|
+
if (normalized.startsWith(".fet/") || normalized.includes("/.fet/")) return "allow";
|
|
3211
|
+
if (isRootConfigPath(normalized)) return "root_config";
|
|
3212
|
+
return "ask";
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
function extractPaths(payload) {
|
|
3216
|
+
const paths = [];
|
|
3217
|
+
const tool = payload?.tool_name ?? payload?.toolName ?? "";
|
|
3218
|
+
const input = payload?.tool_input ?? payload?.toolInput ?? payload?.input ?? {};
|
|
3219
|
+
if (tool === "Write" || tool === "StrReplace" || tool === "Delete") {
|
|
3220
|
+
if (input.path) paths.push(input.path);
|
|
3221
|
+
}
|
|
3222
|
+
if (tool === "EditNotebook" && input.target_notebook) {
|
|
3223
|
+
paths.push(input.target_notebook);
|
|
3224
|
+
}
|
|
3225
|
+
return paths;
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
function respond(decision, paths) {
|
|
3229
|
+
if (decision === "allow") {
|
|
3230
|
+
process.stdout.write(JSON.stringify({ permission: "allow" }));
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
const list = paths.map((p) => normalizePath(p)).filter(Boolean).join(", ") || "(unknown path)";
|
|
3234
|
+
const hasRootConfig = paths.some((p) => classifyPath(p) === "root_config");
|
|
3235
|
+
const msg = hasRootConfig
|
|
3236
|
+
? "FET: repo-root config file(s) require your explicit approval (.gitignore, package.json, .eslintrc*, .stylelint*, .checkrc.js, lockfiles, etc.). Approve only if you requested this tooling change."
|
|
3237
|
+
: "FET write boundary: this edit is outside src/, openspec/, or **/.fet/. Approve only if you intend to modify protected paths.";
|
|
3238
|
+
process.stdout.write(
|
|
3239
|
+
JSON.stringify({
|
|
3240
|
+
permission: "ask",
|
|
3241
|
+
user_message: msg + " Paths: " + list,
|
|
3242
|
+
agent_message: hasRootConfig
|
|
3243
|
+
? "Repo-root config edit blocked. Explain why each root config file must change and wait for user approval."
|
|
3244
|
+
: "Out-of-scope write blocked pending user approval. List why each path is needed, then retry after approval."
|
|
3245
|
+
})
|
|
3246
|
+
);
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
const raw = readFileSync(0, "utf8");
|
|
3250
|
+
let payload = {};
|
|
3251
|
+
try {
|
|
3252
|
+
payload = JSON.parse(raw || "{}");
|
|
3253
|
+
} catch {
|
|
3254
|
+
process.stdout.write(JSON.stringify({ permission: "ask", user_message: "FET write guard: invalid hook payload." }));
|
|
3255
|
+
process.exit(0);
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
const paths = extractPaths(payload);
|
|
3259
|
+
if (paths.length === 0) {
|
|
3260
|
+
respond("allow", paths);
|
|
3261
|
+
process.exit(0);
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
const decisions = paths.map((p) => classifyPath(p));
|
|
3265
|
+
if (decisions.every((d) => d === "allow")) {
|
|
3266
|
+
respond("allow", paths);
|
|
3267
|
+
} else {
|
|
3268
|
+
respond("ask", paths);
|
|
3269
|
+
}
|
|
3270
|
+
process.exit(0);
|
|
3271
|
+
`;
|
|
3272
|
+
}
|
|
3273
|
+
function renderCursorShellWriteHookMjs() {
|
|
3274
|
+
return `#!/usr/bin/env node
|
|
3275
|
+
/**
|
|
3276
|
+
* FET:MANAGED
|
|
3277
|
+
* adapterVersion: 1
|
|
3278
|
+
* shell guard for commands that may write outside the FET write boundary.
|
|
3279
|
+
*/
|
|
3280
|
+
import { readFileSync } from "node:fs";
|
|
3281
|
+
|
|
3282
|
+
const REDIRECT = />\\s*[^\\s|&;]+/;
|
|
3283
|
+
const GIT_WRITE = /\\bgit\\s+(checkout|restore|reset|clean|apply)\\b/i;
|
|
3284
|
+
const PKG_WRITE = /\\b(npm|pnpm|yarn|bun)\\s+(install|ci|add|remove|update)\\b/i;
|
|
3285
|
+
const STREAM_EDIT = /\\b(sed|perl)\\s+[^\\n]*-i\\b/i;
|
|
3286
|
+
const TEE = /\\btee\\s+[^|;&\\n]+/i;
|
|
3287
|
+
|
|
3288
|
+
function respondAsk(command, reason) {
|
|
3289
|
+
process.stdout.write(
|
|
3290
|
+
JSON.stringify({
|
|
3291
|
+
permission: "ask",
|
|
3292
|
+
user_message: "FET shell guard: " + reason,
|
|
3293
|
+
agent_message: "Shell command may modify files outside src/openspec/.fet. Command: " + command
|
|
3294
|
+
})
|
|
3295
|
+
);
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
const raw = readFileSync(0, "utf8");
|
|
3299
|
+
let payload = {};
|
|
3300
|
+
try {
|
|
3301
|
+
payload = JSON.parse(raw || "{}");
|
|
3302
|
+
} catch {
|
|
3303
|
+
respondAsk("", "invalid hook payload");
|
|
3304
|
+
process.exit(0);
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
const command = String(payload?.command ?? "");
|
|
3308
|
+
if (!command.trim()) {
|
|
3309
|
+
process.stdout.write(JSON.stringify({ permission: "allow" }));
|
|
3310
|
+
process.exit(0);
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
if (
|
|
3314
|
+
REDIRECT.test(command) ||
|
|
3315
|
+
GIT_WRITE.test(command) ||
|
|
3316
|
+
PKG_WRITE.test(command) ||
|
|
3317
|
+
STREAM_EDIT.test(command) ||
|
|
3318
|
+
TEE.test(command)
|
|
3319
|
+
) {
|
|
3320
|
+
respondAsk(command, "command may write or replace project files outside the default FET scope");
|
|
3321
|
+
process.exit(0);
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
process.stdout.write(JSON.stringify({ permission: "allow" }));
|
|
3325
|
+
process.exit(0);
|
|
3326
|
+
`;
|
|
3327
|
+
}
|
|
3328
|
+
function renderCursorHooksJson() {
|
|
3329
|
+
return JSON.stringify(
|
|
3330
|
+
{
|
|
3331
|
+
version: 1,
|
|
3332
|
+
_fet: {
|
|
3333
|
+
writeBoundary: true,
|
|
3334
|
+
fetVersion: FET_VERSION
|
|
3335
|
+
},
|
|
3336
|
+
hooks: {
|
|
3337
|
+
preToolUse: [
|
|
3338
|
+
{
|
|
3339
|
+
command: ".cursor/hooks/fet-guard-write-paths.mjs",
|
|
3340
|
+
matcher: "Write|StrReplace|EditNotebook|Delete",
|
|
3341
|
+
failClosed: true
|
|
3342
|
+
}
|
|
3343
|
+
],
|
|
3344
|
+
beforeShellExecution: [
|
|
3345
|
+
{
|
|
3346
|
+
command: ".cursor/hooks/fet-guard-shell-writes.mjs",
|
|
3347
|
+
failClosed: true
|
|
3348
|
+
}
|
|
3349
|
+
]
|
|
3350
|
+
}
|
|
3351
|
+
},
|
|
3352
|
+
null,
|
|
3353
|
+
2
|
|
3354
|
+
);
|
|
3355
|
+
}
|
|
3356
|
+
function mergeCursorHooksJson(existingContent, fetContent) {
|
|
3357
|
+
const fet = JSON.parse(fetContent);
|
|
3358
|
+
if (!existingContent?.trim()) {
|
|
3359
|
+
return fetContent;
|
|
3360
|
+
}
|
|
3361
|
+
let existing;
|
|
3362
|
+
try {
|
|
3363
|
+
existing = JSON.parse(existingContent);
|
|
3364
|
+
} catch {
|
|
3365
|
+
return fetContent;
|
|
3366
|
+
}
|
|
3367
|
+
const merged = {
|
|
3368
|
+
version: existing.version ?? fet.version ?? 1,
|
|
3369
|
+
_fet: { ...existing._fet, ...fet._fet, writeBoundary: true },
|
|
3370
|
+
hooks: { ...existing.hooks }
|
|
3371
|
+
};
|
|
3372
|
+
for (const [event, fetEntries] of Object.entries(fet.hooks ?? {})) {
|
|
3373
|
+
const current = [...merged.hooks?.[event] ?? []];
|
|
3374
|
+
for (const entry of fetEntries) {
|
|
3375
|
+
const duplicate = current.some((item) => item.command === entry.command);
|
|
3376
|
+
if (!duplicate) {
|
|
3377
|
+
current.push(entry);
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
merged.hooks = merged.hooks ?? {};
|
|
3381
|
+
merged.hooks[event] = current;
|
|
3382
|
+
}
|
|
3383
|
+
return `${JSON.stringify(merged, null, 2)}
|
|
3384
|
+
`;
|
|
3385
|
+
}
|
|
3386
|
+
|
|
2865
3387
|
// src/commands/update-context.ts
|
|
2866
3388
|
async function updateContextCommand(ctx) {
|
|
2867
3389
|
let contextResult = { warnings: [] };
|
|
@@ -3818,6 +4340,8 @@ function createChangeState(fetVersion, changeId, phase) {
|
|
|
3818
4340
|
manualVerify: null,
|
|
3819
4341
|
tdd: null,
|
|
3820
4342
|
testRun: null,
|
|
4343
|
+
visual: null,
|
|
4344
|
+
visualRun: null,
|
|
3821
4345
|
lastOpenSpecCommand: null,
|
|
3822
4346
|
warnings: []
|
|
3823
4347
|
};
|
|
@@ -4104,6 +4628,7 @@ async function applyWorkflowCommand(ctx, args) {
|
|
|
4104
4628
|
}
|
|
4105
4629
|
if (figmaGuard) {
|
|
4106
4630
|
applyNextSteps.unshift(...renderFigmaApplyNextSteps(changeId, ctx.language, figmaGuard.mode));
|
|
4631
|
+
applyNextSteps.splice(applyNextSteps.length - 1, 0, ...renderVisualVerifyNextSteps(changeId, ctx.language));
|
|
4107
4632
|
}
|
|
4108
4633
|
ctx.output.result({
|
|
4109
4634
|
ok: true,
|
|
@@ -5043,7 +5568,7 @@ async function tddCommand(ctx) {
|
|
|
5043
5568
|
cases,
|
|
5044
5569
|
testCommand: testCommand2
|
|
5045
5570
|
});
|
|
5046
|
-
const
|
|
5571
|
+
const manifestPath3 = await writeTddManifest(ctx.projectRoot, manifest);
|
|
5047
5572
|
const fetDir = join24(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
5048
5573
|
await mkdir8(fetDir, { recursive: true });
|
|
5049
5574
|
const instructionsPath = tddInstructionsRelativePath(changeId);
|
|
@@ -5055,7 +5580,7 @@ async function tddCommand(ctx) {
|
|
|
5055
5580
|
status: "ready",
|
|
5056
5581
|
generatedAt: manifest.generatedAt,
|
|
5057
5582
|
planningFingerprint,
|
|
5058
|
-
manifestPath:
|
|
5583
|
+
manifestPath: manifestPath3
|
|
5059
5584
|
};
|
|
5060
5585
|
invalidateTestRun(changeState);
|
|
5061
5586
|
changeState.currentPhase = "implement";
|
|
@@ -5086,7 +5611,7 @@ async function tddCommand(ctx) {
|
|
|
5086
5611
|
],
|
|
5087
5612
|
data: {
|
|
5088
5613
|
changeId,
|
|
5089
|
-
manifestPath:
|
|
5614
|
+
manifestPath: manifestPath3,
|
|
5090
5615
|
specPath,
|
|
5091
5616
|
instructionsPath,
|
|
5092
5617
|
caseCount: cases.length,
|
|
@@ -5291,81 +5816,746 @@ function truncate(value, max = 2e3) {
|
|
|
5291
5816
|
return value.length > max ? `${value.slice(0, max)}\u2026` : value;
|
|
5292
5817
|
}
|
|
5293
5818
|
|
|
5294
|
-
// src/commands/
|
|
5295
|
-
import {
|
|
5296
|
-
import {
|
|
5819
|
+
// src/commands/visual.ts
|
|
5820
|
+
import { mkdir as mkdir10 } from "fs/promises";
|
|
5821
|
+
import { join as join28 } from "path";
|
|
5822
|
+
|
|
5823
|
+
// src/visual/config.ts
|
|
5824
|
+
import { readFile as readFile19 } from "fs/promises";
|
|
5297
5825
|
import { join as join25 } from "path";
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
statusIfMissing: command.required ? "fail" : "warn"
|
|
5314
|
-
}))
|
|
5315
|
-
}
|
|
5316
|
-
],
|
|
5317
|
-
missing: ["lint", "typecheck", "test"].filter((name) => !scan.commands[name])
|
|
5318
|
-
};
|
|
5319
|
-
if (ctx.yes) {
|
|
5320
|
-
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
5321
|
-
global.verifyAuthorization = {
|
|
5322
|
-
schemaVersion: 1,
|
|
5323
|
-
approvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5324
|
-
commandFingerprint: fingerprint(plan),
|
|
5325
|
-
packageManager: plan.packageManager,
|
|
5326
|
-
plan: plan.workspaces.flatMap(
|
|
5327
|
-
(workspace) => workspace.commands.map((command) => ({
|
|
5328
|
-
cwd: workspace.cwd,
|
|
5329
|
-
dimension: command.dimension,
|
|
5330
|
-
command: command.command,
|
|
5331
|
-
source: command.source,
|
|
5332
|
-
required: command.required
|
|
5333
|
-
}))
|
|
5334
|
-
)
|
|
5335
|
-
};
|
|
5336
|
-
await ctx.stateStore.writeGlobal(global);
|
|
5826
|
+
import { parseDocument as parseDocument5 } from "yaml";
|
|
5827
|
+
var DEFAULT_CONFIG4 = {
|
|
5828
|
+
enabled: true,
|
|
5829
|
+
compareMode: "layout-only",
|
|
5830
|
+
requireBeforeVerify: "when_figma",
|
|
5831
|
+
whenNoCapture: "warn"
|
|
5832
|
+
};
|
|
5833
|
+
async function loadVisualConfig(projectRoot) {
|
|
5834
|
+
try {
|
|
5835
|
+
const raw = await readFile19(join25(projectRoot, "openspec", "config.yaml"), "utf8");
|
|
5836
|
+
const doc = parseDocument5(raw);
|
|
5837
|
+
const fetNode = doc.get("fet", true);
|
|
5838
|
+
const node = fetNode?.get?.("visual");
|
|
5839
|
+
if (!node || typeof node.get !== "function") {
|
|
5840
|
+
return DEFAULT_CONFIG4;
|
|
5337
5841
|
}
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5842
|
+
const enabled = node.get("enabled");
|
|
5843
|
+
const requireBeforeVerify = node.get("requireBeforeVerify");
|
|
5844
|
+
const whenNoCapture = node.get("whenNoCapture");
|
|
5845
|
+
return {
|
|
5846
|
+
enabled: enabled === void 0 ? true : Boolean(enabled),
|
|
5847
|
+
compareMode: "layout-only",
|
|
5848
|
+
requireBeforeVerify: parseRequireBeforeVerify(requireBeforeVerify),
|
|
5849
|
+
whenNoCapture: parseWhenNoCapture(whenNoCapture)
|
|
5850
|
+
};
|
|
5851
|
+
} catch {
|
|
5852
|
+
return DEFAULT_CONFIG4;
|
|
5347
5853
|
}
|
|
5348
|
-
await withProjectLock(
|
|
5349
|
-
ctx.projectRoot,
|
|
5350
|
-
{ command: "verify", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
|
|
5351
|
-
async () => {
|
|
5352
|
-
const changeId = await resolveChangeId(ctx);
|
|
5353
|
-
if (options.done) {
|
|
5354
|
-
await markDone(ctx, changeId);
|
|
5355
|
-
} else {
|
|
5356
|
-
await writeInstructions(ctx, changeId);
|
|
5357
|
-
}
|
|
5358
|
-
}
|
|
5359
|
-
);
|
|
5360
5854
|
}
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5855
|
+
function parseRequireBeforeVerify(value) {
|
|
5856
|
+
if (value === "off" || value === "always" || value === "when_figma") {
|
|
5857
|
+
return value;
|
|
5858
|
+
}
|
|
5859
|
+
return DEFAULT_CONFIG4.requireBeforeVerify;
|
|
5860
|
+
}
|
|
5861
|
+
function parseWhenNoCapture(value) {
|
|
5862
|
+
if (value === "block" || value === "warn" || value === "skip") {
|
|
5863
|
+
return value;
|
|
5864
|
+
}
|
|
5865
|
+
return DEFAULT_CONFIG4.whenNoCapture;
|
|
5866
|
+
}
|
|
5867
|
+
function isVisualRequiredForVerify(config, hasFigma) {
|
|
5868
|
+
if (!config.enabled) {
|
|
5869
|
+
return false;
|
|
5870
|
+
}
|
|
5871
|
+
if (config.requireBeforeVerify === "off") {
|
|
5872
|
+
return false;
|
|
5873
|
+
}
|
|
5874
|
+
if (config.requireBeforeVerify === "always") {
|
|
5875
|
+
return true;
|
|
5876
|
+
}
|
|
5877
|
+
return hasFigma;
|
|
5878
|
+
}
|
|
5879
|
+
|
|
5880
|
+
// src/visual/playwright.ts
|
|
5881
|
+
import { createRequire } from "module";
|
|
5882
|
+
import { dirname as dirname9, join as join26 } from "path";
|
|
5883
|
+
import { pathToFileURL } from "url";
|
|
5884
|
+
async function resolvePlaywright(projectRoot) {
|
|
5885
|
+
const require2 = createRequire(join26(projectRoot, "package.json"));
|
|
5886
|
+
const candidates = ["playwright", "@playwright/test"];
|
|
5887
|
+
for (const name of candidates) {
|
|
5888
|
+
try {
|
|
5889
|
+
const resolved = require2.resolve(name);
|
|
5890
|
+
const mod = await import(pathToFileURL(resolved).href);
|
|
5891
|
+
if (mod?.chromium) {
|
|
5892
|
+
return mod;
|
|
5893
|
+
}
|
|
5894
|
+
} catch {
|
|
5895
|
+
continue;
|
|
5896
|
+
}
|
|
5897
|
+
}
|
|
5898
|
+
return null;
|
|
5899
|
+
}
|
|
5900
|
+
async function capturePagesWithPlaywright(projectRoot, manifest, baseUrl) {
|
|
5901
|
+
const playwright = await resolvePlaywright(projectRoot);
|
|
5902
|
+
if (!playwright) {
|
|
5903
|
+
return [];
|
|
5904
|
+
}
|
|
5905
|
+
const browser = await playwright.chromium.launch({ headless: true });
|
|
5906
|
+
const results = [];
|
|
5907
|
+
try {
|
|
5908
|
+
for (const page of manifest.pages) {
|
|
5909
|
+
results.push(await captureSinglePage(projectRoot, browser, manifest.changeId, page, baseUrl));
|
|
5910
|
+
}
|
|
5911
|
+
} finally {
|
|
5912
|
+
await browser.close();
|
|
5913
|
+
}
|
|
5914
|
+
return results;
|
|
5915
|
+
}
|
|
5916
|
+
async function captureSinglePage(projectRoot, browser, changeId, pageDef, baseUrl) {
|
|
5917
|
+
const { mkdir: mkdir15 } = await import("fs/promises");
|
|
5918
|
+
const { join: joinPath } = await import("path");
|
|
5919
|
+
const screenshotRelative = visualPageScreenshotRelative(changeId, pageDef.id);
|
|
5920
|
+
const screenshotAbsolute = joinPath(projectRoot, screenshotRelative);
|
|
5921
|
+
await mkdir15(dirname9(screenshotAbsolute), { recursive: true });
|
|
5922
|
+
const url = new URL(pageDef.route, baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`).toString();
|
|
5923
|
+
const pwPage = await browser.newPage();
|
|
5924
|
+
try {
|
|
5925
|
+
await pwPage.setViewportSize(pageDef.viewport);
|
|
5926
|
+
await pwPage.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
5927
|
+
await pwPage.screenshot({ path: screenshotAbsolute, fullPage: true });
|
|
5928
|
+
const regions = await extractRegions(pwPage, pageDef);
|
|
5929
|
+
return {
|
|
5930
|
+
pageId: pageDef.id,
|
|
5931
|
+
route: pageDef.route,
|
|
5932
|
+
screenshotPath: screenshotRelative,
|
|
5933
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5934
|
+
regions
|
|
5935
|
+
};
|
|
5936
|
+
} catch {
|
|
5937
|
+
return {
|
|
5938
|
+
pageId: pageDef.id,
|
|
5939
|
+
route: pageDef.route,
|
|
5940
|
+
screenshotPath: null,
|
|
5941
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5942
|
+
regions: []
|
|
5943
|
+
};
|
|
5944
|
+
} finally {
|
|
5945
|
+
await pwPage.close();
|
|
5946
|
+
}
|
|
5947
|
+
}
|
|
5948
|
+
var EXTRACT_REGIONS_SCRIPT = `
|
|
5949
|
+
(inputSelectors) => {
|
|
5950
|
+
const layouts = [];
|
|
5951
|
+
for (const selector of inputSelectors) {
|
|
5952
|
+
const element = document.querySelector(selector);
|
|
5953
|
+
if (!element) {
|
|
5954
|
+
layouts.push({
|
|
5955
|
+
selector,
|
|
5956
|
+
exists: false,
|
|
5957
|
+
visible: false,
|
|
5958
|
+
width: 0,
|
|
5959
|
+
height: 0,
|
|
5960
|
+
display: "none",
|
|
5961
|
+
position: "static"
|
|
5962
|
+
});
|
|
5963
|
+
continue;
|
|
5964
|
+
}
|
|
5965
|
+
const style = window.getComputedStyle(element);
|
|
5966
|
+
const rect = element.getBoundingClientRect();
|
|
5967
|
+
layouts.push({
|
|
5968
|
+
selector,
|
|
5969
|
+
exists: true,
|
|
5970
|
+
visible: style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0,
|
|
5971
|
+
width: rect.width,
|
|
5972
|
+
height: rect.height,
|
|
5973
|
+
display: style.display,
|
|
5974
|
+
position: style.position
|
|
5975
|
+
});
|
|
5976
|
+
}
|
|
5977
|
+
return layouts;
|
|
5978
|
+
}
|
|
5979
|
+
`;
|
|
5980
|
+
async function extractRegions(page, pageDef) {
|
|
5981
|
+
const selectors = pageDef.checkRegions.map((region) => region.selector);
|
|
5982
|
+
return page.evaluate(EXTRACT_REGIONS_SCRIPT, selectors);
|
|
5983
|
+
}
|
|
5984
|
+
|
|
5985
|
+
// src/visual/manifest.ts
|
|
5986
|
+
import { mkdir as mkdir9, readFile as readFile20, stat as stat11 } from "fs/promises";
|
|
5987
|
+
import { dirname as dirname10, join as join27 } from "path";
|
|
5988
|
+
import { parse as parse5, stringify as stringify4 } from "yaml";
|
|
5989
|
+
function visualManifestPath(projectRoot, changeId) {
|
|
5990
|
+
return join27(projectRoot, visualManifestRelativePath(changeId));
|
|
5991
|
+
}
|
|
5992
|
+
async function readVisualManifest(projectRoot, changeId) {
|
|
5993
|
+
const path = visualManifestPath(projectRoot, changeId);
|
|
5994
|
+
try {
|
|
5995
|
+
await stat11(path);
|
|
5996
|
+
} catch {
|
|
5997
|
+
return null;
|
|
5998
|
+
}
|
|
5999
|
+
const doc = parse5(await readFile20(path, "utf8"));
|
|
6000
|
+
if (!doc || doc.schemaVersion !== 1 || doc.changeId !== changeId) {
|
|
6001
|
+
return null;
|
|
6002
|
+
}
|
|
6003
|
+
return doc;
|
|
6004
|
+
}
|
|
6005
|
+
async function writeVisualManifest(projectRoot, manifest) {
|
|
6006
|
+
const relative6 = visualManifestRelativePath(manifest.changeId);
|
|
6007
|
+
const path = join27(projectRoot, relative6);
|
|
6008
|
+
await mkdir9(dirname10(path), { recursive: true });
|
|
6009
|
+
await atomicWrite(path, stringify4(manifest));
|
|
6010
|
+
return relative6;
|
|
6011
|
+
}
|
|
6012
|
+
async function readVisualCapture(projectRoot, changeId) {
|
|
6013
|
+
const path = join27(projectRoot, visualCaptureRelativePath(changeId));
|
|
6014
|
+
try {
|
|
6015
|
+
const doc = JSON.parse(await readFile20(path, "utf8"));
|
|
6016
|
+
if (doc?.schemaVersion === 1 && doc.changeId === changeId) {
|
|
6017
|
+
return doc;
|
|
6018
|
+
}
|
|
6019
|
+
} catch {
|
|
6020
|
+
return null;
|
|
6021
|
+
}
|
|
6022
|
+
return null;
|
|
6023
|
+
}
|
|
6024
|
+
async function writeVisualCapture(projectRoot, capture) {
|
|
6025
|
+
const relative6 = visualCaptureRelativePath(capture.changeId);
|
|
6026
|
+
const path = join27(projectRoot, relative6);
|
|
6027
|
+
await mkdir9(dirname10(path), { recursive: true });
|
|
6028
|
+
await atomicWrite(path, `${JSON.stringify(capture, null, 2)}
|
|
6029
|
+
`);
|
|
6030
|
+
return relative6;
|
|
6031
|
+
}
|
|
6032
|
+
async function writeVisualResults(projectRoot, results) {
|
|
6033
|
+
const relative6 = visualResultsRelativePath(results.changeId);
|
|
6034
|
+
const path = join27(projectRoot, relative6);
|
|
6035
|
+
await mkdir9(dirname10(path), { recursive: true });
|
|
6036
|
+
await atomicWrite(path, `${JSON.stringify(results, null, 2)}
|
|
6037
|
+
`);
|
|
6038
|
+
return relative6;
|
|
6039
|
+
}
|
|
6040
|
+
function createVisualManifest(input) {
|
|
6041
|
+
return {
|
|
6042
|
+
schemaVersion: 1,
|
|
6043
|
+
changeId: input.changeId,
|
|
6044
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6045
|
+
fetVersion: FET_VERSION,
|
|
6046
|
+
planningFingerprint: input.planningFingerprint,
|
|
6047
|
+
compareMode: "layout-only",
|
|
6048
|
+
figmaUrls: input.figmaUrls,
|
|
6049
|
+
figmaSources: input.figmaSources,
|
|
6050
|
+
run: { baseUrl: input.baseUrl },
|
|
6051
|
+
pages: input.pages
|
|
6052
|
+
};
|
|
6053
|
+
}
|
|
6054
|
+
|
|
6055
|
+
// src/visual/capture.ts
|
|
6056
|
+
async function runVisualCapture(options) {
|
|
6057
|
+
const warnings = [];
|
|
6058
|
+
const baseUrl = options.baseUrl ?? options.manifest.run.baseUrl;
|
|
6059
|
+
if (!baseUrl) {
|
|
6060
|
+
return handleMissingBaseUrl(options.config, options.language, warnings);
|
|
6061
|
+
}
|
|
6062
|
+
const playwright = await resolvePlaywright(options.projectRoot);
|
|
6063
|
+
if (!playwright) {
|
|
6064
|
+
return handleMissingPlaywright(options.config, options.language, warnings);
|
|
6065
|
+
}
|
|
6066
|
+
const pages = await capturePagesWithPlaywright(options.projectRoot, options.manifest, baseUrl);
|
|
6067
|
+
const failedPages = pages.filter((page) => !page.screenshotPath);
|
|
6068
|
+
const capture = {
|
|
6069
|
+
schemaVersion: 1,
|
|
6070
|
+
changeId: options.changeId,
|
|
6071
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6072
|
+
baseUrl,
|
|
6073
|
+
pages
|
|
6074
|
+
};
|
|
6075
|
+
await writeVisualCapture(options.projectRoot, capture);
|
|
6076
|
+
if (failedPages.length) {
|
|
6077
|
+
return {
|
|
6078
|
+
status: "failed",
|
|
6079
|
+
capture,
|
|
6080
|
+
warnings,
|
|
6081
|
+
message: options.language === "en" ? `Capture failed for ${failedPages.length} page(s). Check dev server and routes.` : `${failedPages.length} \u4E2A\u9875\u9762\u622A\u56FE\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5 dev \u670D\u52A1\u4E0E\u8DEF\u7531\u3002`
|
|
6082
|
+
};
|
|
6083
|
+
}
|
|
6084
|
+
return { status: "passed", capture, warnings };
|
|
6085
|
+
}
|
|
6086
|
+
function handleMissingBaseUrl(config, language, warnings) {
|
|
6087
|
+
const message = language === "en" ? "No base URL for capture. Pass --base-url or set manifest.run.baseUrl." : "\u672A\u914D\u7F6E base URL\uFF0C\u65E0\u6CD5\u622A\u56FE\u3002\u8BF7\u4F7F\u7528 --base-url \u6216\u5728 manifest \u4E2D\u8BBE\u7F6E run.baseUrl\u3002";
|
|
6088
|
+
warnings.push(message);
|
|
6089
|
+
if (config.whenNoCapture === "block") {
|
|
6090
|
+
return { status: "failed", capture: null, warnings, message };
|
|
6091
|
+
}
|
|
6092
|
+
return { status: "skipped", capture: null, warnings, message };
|
|
6093
|
+
}
|
|
6094
|
+
function handleMissingPlaywright(config, language, warnings) {
|
|
6095
|
+
const message = language === "en" ? "Playwright not found in project. Install playwright or use --check-layout-only after manual capture." : "\u9879\u76EE\u672A\u5B89\u88C5 Playwright\u3002\u8BF7\u5B89\u88C5 playwright\uFF0C\u6216\u5728\u624B\u52A8\u622A\u56FE\u540E\u4F7F\u7528 --check-layout-only\u3002";
|
|
6096
|
+
warnings.push(message);
|
|
6097
|
+
if (config.whenNoCapture === "block") {
|
|
6098
|
+
return { status: "failed", capture: null, warnings, message };
|
|
6099
|
+
}
|
|
6100
|
+
return { status: "skipped", capture: null, warnings, message };
|
|
6101
|
+
}
|
|
6102
|
+
|
|
6103
|
+
// src/visual/check-layout.ts
|
|
6104
|
+
function runLayoutCheck(manifest, capture, language, options) {
|
|
6105
|
+
const regions = [];
|
|
6106
|
+
if (!capture && options?.allowManifestOnly) {
|
|
6107
|
+
return runManifestOnlyValidation(manifest, language);
|
|
6108
|
+
}
|
|
6109
|
+
if (capture?.pages.length) {
|
|
6110
|
+
for (const page of manifest.pages) {
|
|
6111
|
+
const captured = capture.pages.find((item) => item.pageId === page.id);
|
|
6112
|
+
for (const region of page.checkRegions) {
|
|
6113
|
+
const layout = captured?.regions.find((item) => item.selector === region.selector);
|
|
6114
|
+
const result = evaluateRegion(page.id, region.selector, region.checks, layout, language);
|
|
6115
|
+
regions.push(result);
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
6118
|
+
} else {
|
|
6119
|
+
for (const page of manifest.pages) {
|
|
6120
|
+
if (!page.checkRegions.length) {
|
|
6121
|
+
regions.push({
|
|
6122
|
+
pageId: page.id,
|
|
6123
|
+
selector: "(page)",
|
|
6124
|
+
status: "failed",
|
|
6125
|
+
message: language === "en" ? "No checkRegions defined" : "\u672A\u5B9A\u4E49 checkRegions"
|
|
6126
|
+
});
|
|
6127
|
+
continue;
|
|
6128
|
+
}
|
|
6129
|
+
for (const region of page.checkRegions) {
|
|
6130
|
+
regions.push({
|
|
6131
|
+
pageId: page.id,
|
|
6132
|
+
selector: region.selector,
|
|
6133
|
+
status: capture ? "failed" : "skipped",
|
|
6134
|
+
message: language === "en" ? "No capture data; run capture with --base-url or install Playwright" : "\u65E0\u622A\u56FE\u6570\u636E\uFF1B\u8BF7\u5E26 --base-url \u8FD0\u884C capture \u6216\u5B89\u88C5 Playwright"
|
|
6135
|
+
});
|
|
6136
|
+
}
|
|
6137
|
+
}
|
|
6138
|
+
}
|
|
6139
|
+
const failed = regions.filter((item) => item.status === "failed");
|
|
6140
|
+
if (failed.length) {
|
|
6141
|
+
return {
|
|
6142
|
+
status: "failed",
|
|
6143
|
+
regions,
|
|
6144
|
+
message: language === "en" ? `Layout check failed for ${failed.length} region(s).` : `${failed.length} \u4E2A\u533A\u57DF\u7684\u5E03\u5C40\u68C0\u67E5\u672A\u901A\u8FC7\u3002`
|
|
6145
|
+
};
|
|
6146
|
+
}
|
|
6147
|
+
const skipped = regions.filter((item) => item.status === "skipped");
|
|
6148
|
+
if (skipped.length && !capture) {
|
|
6149
|
+
return {
|
|
6150
|
+
status: "failed",
|
|
6151
|
+
regions,
|
|
6152
|
+
message: language === "en" ? "Layout check requires capture data when regions are defined." : "\u5DF2\u5B9A\u4E49\u68C0\u67E5\u533A\u57DF\u65F6\u9700\u8981 capture \u6570\u636E\u624D\u80FD\u5B8C\u6210\u5E03\u5C40\u9A8C\u6536\u3002"
|
|
6153
|
+
};
|
|
6154
|
+
}
|
|
6155
|
+
return { status: "passed", regions };
|
|
6156
|
+
}
|
|
6157
|
+
function runManifestOnlyValidation(manifest, language) {
|
|
6158
|
+
const regions = [];
|
|
6159
|
+
if (!manifest.pages.length) {
|
|
6160
|
+
return {
|
|
6161
|
+
status: "failed",
|
|
6162
|
+
regions,
|
|
6163
|
+
message: language === "en" ? "Visual manifest has no pages." : "\u89C6\u89C9\u6E05\u5355\u4E2D\u6CA1\u6709\u4EFB\u4F55\u9875\u9762\u3002"
|
|
6164
|
+
};
|
|
6165
|
+
}
|
|
6166
|
+
for (const page of manifest.pages) {
|
|
6167
|
+
for (const region of page.checkRegions) {
|
|
6168
|
+
regions.push({
|
|
6169
|
+
pageId: page.id,
|
|
6170
|
+
selector: region.selector,
|
|
6171
|
+
status: "skipped",
|
|
6172
|
+
message: language === "en" ? "Manifest-only mode (layout-only, no pixel/content compare)" : "\u4EC5 manifest \u6821\u9A8C\uFF08layout-only\uFF0C\u4E0D\u5BF9\u6BD4\u50CF\u7D20/\u52A8\u6001\u5185\u5BB9\uFF09"
|
|
6173
|
+
});
|
|
6174
|
+
}
|
|
6175
|
+
}
|
|
6176
|
+
return {
|
|
6177
|
+
status: "passed",
|
|
6178
|
+
regions,
|
|
6179
|
+
message: language === "en" ? "Manifest validated; capture skipped \u2014 layout-only checklist recorded." : "\u6E05\u5355\u6821\u9A8C\u901A\u8FC7\uFF1B\u5DF2\u8DF3\u8FC7\u622A\u56FE \u2014 \u5DF2\u8BB0\u5F55 layout-only \u68C0\u67E5\u9879\u3002"
|
|
6180
|
+
};
|
|
6181
|
+
}
|
|
6182
|
+
function evaluateRegion(pageId, selector, checks, layout, language) {
|
|
6183
|
+
if (!layout) {
|
|
6184
|
+
return {
|
|
6185
|
+
pageId,
|
|
6186
|
+
selector,
|
|
6187
|
+
status: "failed",
|
|
6188
|
+
message: language === "en" ? "Region not found in capture" : "capture \u4E2D\u672A\u627E\u5230\u8BE5\u533A\u57DF"
|
|
6189
|
+
};
|
|
6190
|
+
}
|
|
6191
|
+
for (const check of checks) {
|
|
6192
|
+
if (check === "exists" && !layout.exists) {
|
|
6193
|
+
return { pageId, selector, status: "failed", message: language === "en" ? "Missing element" : "\u5143\u7D20\u4E0D\u5B58\u5728" };
|
|
6194
|
+
}
|
|
6195
|
+
if (check === "visible" && !layout.visible) {
|
|
6196
|
+
return { pageId, selector, status: "failed", message: language === "en" ? "Not visible" : "\u4E0D\u53EF\u89C1" };
|
|
6197
|
+
}
|
|
6198
|
+
if (check === "has-box" && (layout.width <= 0 || layout.height <= 0)) {
|
|
6199
|
+
return { pageId, selector, status: "failed", message: language === "en" ? "Zero layout box" : "\u5E03\u5C40\u5C3A\u5BF8\u4E3A 0" };
|
|
6200
|
+
}
|
|
6201
|
+
}
|
|
6202
|
+
return { pageId, selector, status: "passed" };
|
|
6203
|
+
}
|
|
6204
|
+
function buildVisualResults(input) {
|
|
6205
|
+
return {
|
|
6206
|
+
schemaVersion: 1,
|
|
6207
|
+
changeId: input.changeId,
|
|
6208
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6209
|
+
compareMode: "layout-only",
|
|
6210
|
+
planningFingerprint: input.planningFingerprint,
|
|
6211
|
+
steps: input.steps,
|
|
6212
|
+
captureStatus: input.captureStatus,
|
|
6213
|
+
layoutStatus: input.layoutStatus,
|
|
6214
|
+
regions: input.regions
|
|
6215
|
+
};
|
|
6216
|
+
}
|
|
6217
|
+
|
|
6218
|
+
// src/visual/generate.ts
|
|
6219
|
+
var DEFAULT_VIEWPORT = { width: 1280, height: 720 };
|
|
6220
|
+
async function buildVisualManifestPages(projectRoot, changeId, figmaUrls) {
|
|
6221
|
+
if (figmaUrls.length) {
|
|
6222
|
+
return [
|
|
6223
|
+
{
|
|
6224
|
+
id: `${changeId}-main`,
|
|
6225
|
+
title: "Main UI screen",
|
|
6226
|
+
route: "/",
|
|
6227
|
+
viewport: DEFAULT_VIEWPORT,
|
|
6228
|
+
dataMode: "mock",
|
|
6229
|
+
mockFixture: null,
|
|
6230
|
+
ignoreSelectors: [".dynamic-list", "[data-dynamic]", "[data-testid='dynamic-content']"],
|
|
6231
|
+
checkRegions: [
|
|
6232
|
+
{ selector: "header, [role='banner'], .app-header", checks: ["exists", "visible", "has-box"] },
|
|
6233
|
+
{ selector: "main, [role='main'], .app-main", checks: ["exists", "visible", "has-box"] },
|
|
6234
|
+
{ selector: "nav, [role='navigation'], .app-nav", checks: ["exists", "has-box"] }
|
|
6235
|
+
]
|
|
6236
|
+
}
|
|
6237
|
+
];
|
|
6238
|
+
}
|
|
6239
|
+
return [
|
|
6240
|
+
{
|
|
6241
|
+
id: `${changeId}-shell`,
|
|
6242
|
+
title: "Application shell",
|
|
6243
|
+
route: "/",
|
|
6244
|
+
viewport: DEFAULT_VIEWPORT,
|
|
6245
|
+
dataMode: "mock",
|
|
6246
|
+
mockFixture: null,
|
|
6247
|
+
ignoreSelectors: [".dynamic-list", "[data-dynamic]"],
|
|
6248
|
+
checkRegions: [{ selector: "body", checks: ["exists", "visible", "has-box"] }]
|
|
6249
|
+
}
|
|
6250
|
+
];
|
|
6251
|
+
}
|
|
6252
|
+
async function generateVisualManifestInput(projectRoot, changeId, baseUrl) {
|
|
6253
|
+
const { urls, sources } = await collectFigmaUrlsFromChange(projectRoot, changeId);
|
|
6254
|
+
const planningFingerprint = await computePlanningFingerprint(projectRoot, changeId);
|
|
6255
|
+
const pages = await buildVisualManifestPages(projectRoot, changeId, urls);
|
|
6256
|
+
return {
|
|
6257
|
+
planningFingerprint,
|
|
6258
|
+
figmaUrls: urls,
|
|
6259
|
+
figmaSources: sources,
|
|
6260
|
+
pages
|
|
6261
|
+
};
|
|
6262
|
+
}
|
|
6263
|
+
function mergeBaseUrl(manifest, baseUrl) {
|
|
6264
|
+
if (!baseUrl) {
|
|
6265
|
+
return manifest;
|
|
6266
|
+
}
|
|
6267
|
+
return {
|
|
6268
|
+
...manifest,
|
|
6269
|
+
run: { baseUrl }
|
|
6270
|
+
};
|
|
6271
|
+
}
|
|
6272
|
+
|
|
6273
|
+
// src/visual/gates.ts
|
|
6274
|
+
async function assertVisualPassed(ctx, changeId) {
|
|
6275
|
+
const config = await loadVisualConfig(ctx.projectRoot);
|
|
6276
|
+
const { urls } = await collectFigmaUrlsFromChange(ctx.projectRoot, changeId);
|
|
6277
|
+
if (!isVisualRequiredForVerify(config, urls.length > 0)) {
|
|
6278
|
+
return;
|
|
6279
|
+
}
|
|
6280
|
+
const change = await ctx.stateStore.readChange(changeId);
|
|
6281
|
+
const visualRun = change?.visualRun;
|
|
6282
|
+
if (visualRun?.status === "skipped") {
|
|
6283
|
+
return;
|
|
6284
|
+
}
|
|
6285
|
+
if (visualRun?.status === "passed" && await fingerprintMatches2(ctx, changeId, visualRun)) {
|
|
6286
|
+
return;
|
|
6287
|
+
}
|
|
6288
|
+
throw new FetError({
|
|
6289
|
+
code: "STATE_CORRUPTED" /* StateCorrupted */,
|
|
6290
|
+
message: msg(ctx.language, "\u672C change \u5C1A\u672A\u901A\u8FC7 fet visual\u3002", "This change has not passed fet visual yet."),
|
|
6291
|
+
details: { changeId, visualRun: visualRun ?? null, figmaUrlCount: urls.length },
|
|
6292
|
+
suggestedCommand: `fet visual --change ${changeId}`,
|
|
6293
|
+
recoverable: true
|
|
6294
|
+
});
|
|
6295
|
+
}
|
|
6296
|
+
async function fingerprintMatches2(ctx, changeId, visualRun) {
|
|
6297
|
+
const current = await computePlanningFingerprint(ctx.projectRoot, changeId);
|
|
6298
|
+
return visualRun.planningFingerprint === current;
|
|
6299
|
+
}
|
|
6300
|
+
function invalidateVisualRun(state) {
|
|
6301
|
+
state.visualRun = null;
|
|
6302
|
+
}
|
|
6303
|
+
function manifestPath2(changeId) {
|
|
6304
|
+
return visualManifestRelativePath(changeId);
|
|
6305
|
+
}
|
|
6306
|
+
|
|
6307
|
+
// src/commands/visual.ts
|
|
6308
|
+
async function visualCommand(ctx, options) {
|
|
6309
|
+
await withProjectLock(ctx.projectRoot, { command: "visual", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
6310
|
+
const changeId = await resolveChangeId(ctx);
|
|
6311
|
+
await assertChangeExists(ctx, changeId);
|
|
6312
|
+
const config = await loadVisualConfig(ctx.projectRoot);
|
|
6313
|
+
const steps = [];
|
|
6314
|
+
const warnings = [];
|
|
6315
|
+
const manifest = await ensureManifest(ctx, changeId, options.baseUrl ?? null);
|
|
6316
|
+
steps.push("manifest");
|
|
6317
|
+
if (options.plan) {
|
|
6318
|
+
ctx.output.result({
|
|
6319
|
+
ok: true,
|
|
6320
|
+
command: "visual",
|
|
6321
|
+
summary: msg(ctx.language, "\u5DF2\u751F\u6210 fet visual \u6267\u884C\u8BA1\u5212\u3002", "Generated fet visual execution plan."),
|
|
6322
|
+
data: {
|
|
6323
|
+
changeId,
|
|
6324
|
+
compareMode: manifest.compareMode,
|
|
6325
|
+
steps: options.checkLayoutOnly ? ["manifest", "check-layout"] : options.captureOnly ? ["manifest", "capture"] : ["manifest", "capture", "check-layout"],
|
|
6326
|
+
baseUrl: options.baseUrl ?? manifest.run.baseUrl,
|
|
6327
|
+
pages: manifest.pages.map((page) => ({ id: page.id, route: page.route }))
|
|
6328
|
+
},
|
|
6329
|
+
nextSteps: [`fet visual --change ${changeId}${options.baseUrl ? ` --base-url ${options.baseUrl}` : ""}`]
|
|
6330
|
+
});
|
|
6331
|
+
return;
|
|
6332
|
+
}
|
|
6333
|
+
let captureStatus = "skipped";
|
|
6334
|
+
let captureDoc = await readVisualCapture(ctx.projectRoot, changeId);
|
|
6335
|
+
if (!options.checkLayoutOnly) {
|
|
6336
|
+
const captureResult = await runVisualCapture({
|
|
6337
|
+
projectRoot: ctx.projectRoot,
|
|
6338
|
+
changeId,
|
|
6339
|
+
manifest,
|
|
6340
|
+
baseUrl: options.baseUrl ?? null,
|
|
6341
|
+
config,
|
|
6342
|
+
language: ctx.language
|
|
6343
|
+
});
|
|
6344
|
+
warnings.push(...captureResult.warnings);
|
|
6345
|
+
captureStatus = captureResult.status;
|
|
6346
|
+
captureDoc = captureResult.capture;
|
|
6347
|
+
steps.push("capture");
|
|
6348
|
+
if (captureResult.status === "failed") {
|
|
6349
|
+
await recordVisualFailure(ctx, changeId, manifest.planningFingerprint, steps, captureStatus, "failed", [], captureResult.message);
|
|
6350
|
+
throw new FetError({
|
|
6351
|
+
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
6352
|
+
message: captureResult.message ?? msg(ctx.language, "fet visual \u622A\u56FE\u5931\u8D25\u3002", "fet visual capture failed."),
|
|
6353
|
+
details: { changeId },
|
|
6354
|
+
suggestedCommand: `fet visual --change ${changeId} --base-url http://localhost:3000`,
|
|
6355
|
+
recoverable: true
|
|
6356
|
+
});
|
|
6357
|
+
}
|
|
6358
|
+
} else {
|
|
6359
|
+
steps.push("capture");
|
|
6360
|
+
captureStatus = captureDoc ? "passed" : "skipped";
|
|
6361
|
+
}
|
|
6362
|
+
if (!options.captureOnly) {
|
|
6363
|
+
const layout = runLayoutCheck(manifest, captureDoc, ctx.language, {
|
|
6364
|
+
allowManifestOnly: captureStatus === "skipped" && config.whenNoCapture !== "block"
|
|
6365
|
+
});
|
|
6366
|
+
steps.push("check-layout");
|
|
6367
|
+
const results = buildVisualResults({
|
|
6368
|
+
changeId,
|
|
6369
|
+
planningFingerprint: manifest.planningFingerprint,
|
|
6370
|
+
steps,
|
|
6371
|
+
captureStatus,
|
|
6372
|
+
layoutStatus: layout.status,
|
|
6373
|
+
regions: layout.regions
|
|
6374
|
+
});
|
|
6375
|
+
const resultsPath = await writeVisualResults(ctx.projectRoot, results);
|
|
6376
|
+
if (layout.status === "failed") {
|
|
6377
|
+
await recordVisualFailure(ctx, changeId, manifest.planningFingerprint, steps, captureStatus, layout.status, layout.regions, layout.message);
|
|
6378
|
+
throw new FetError({
|
|
6379
|
+
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
6380
|
+
message: layout.message ?? msg(ctx.language, "fet visual \u5E03\u5C40\u68C0\u67E5\u672A\u901A\u8FC7\u3002", "fet visual layout check failed."),
|
|
6381
|
+
details: { changeId, resultsPath: visualResultsRelativePath(changeId) },
|
|
6382
|
+
suggestedCommand: `fet visual --change ${changeId}`,
|
|
6383
|
+
recoverable: true
|
|
6384
|
+
});
|
|
6385
|
+
}
|
|
6386
|
+
await recordVisualSuccess(ctx, changeId, manifest.planningFingerprint, resultsPath, captureStatus);
|
|
6387
|
+
ctx.output.result({
|
|
6388
|
+
ok: true,
|
|
6389
|
+
command: "visual",
|
|
6390
|
+
summary: msg(
|
|
6391
|
+
ctx.language,
|
|
6392
|
+
`change "${changeId}" \u5DF2\u901A\u8FC7 layout-only \u89C6\u89C9\u9A8C\u6536\u3002`,
|
|
6393
|
+
`Layout-only visual verification passed for change "${changeId}".`
|
|
6394
|
+
),
|
|
6395
|
+
warnings: warnings.length ? warnings : void 0,
|
|
6396
|
+
nextSteps: [`fet verify --change ${changeId}`],
|
|
6397
|
+
data: { changeId, results, capturePath: captureDoc ? visualCaptureRelativePath(changeId) : null }
|
|
6398
|
+
});
|
|
6399
|
+
return;
|
|
6400
|
+
}
|
|
6401
|
+
ctx.output.result({
|
|
6402
|
+
ok: true,
|
|
6403
|
+
command: "visual",
|
|
6404
|
+
summary: msg(ctx.language, "fet visual \u622A\u56FE\u6B65\u9AA4\u5DF2\u5B8C\u6210\u3002", "fet visual capture step completed."),
|
|
6405
|
+
warnings: warnings.length ? warnings : void 0,
|
|
6406
|
+
data: { changeId, captureStatus, capturePath: captureDoc ? visualCaptureRelativePath(changeId) : null }
|
|
6407
|
+
});
|
|
6408
|
+
});
|
|
6409
|
+
}
|
|
6410
|
+
async function ensureManifest(ctx, changeId, baseUrl) {
|
|
6411
|
+
const fingerprint2 = await computePlanningFingerprint(ctx.projectRoot, changeId);
|
|
6412
|
+
const existing = await readVisualManifest(ctx.projectRoot, changeId);
|
|
6413
|
+
if (existing && existing.planningFingerprint === fingerprint2 && (!baseUrl || existing.run.baseUrl === baseUrl)) {
|
|
6414
|
+
return existing;
|
|
6415
|
+
}
|
|
6416
|
+
const input = await generateVisualManifestInput(ctx.projectRoot, changeId, baseUrl);
|
|
6417
|
+
const manifest = mergeBaseUrl(
|
|
6418
|
+
createVisualManifest({
|
|
6419
|
+
changeId,
|
|
6420
|
+
planningFingerprint: input.planningFingerprint,
|
|
6421
|
+
figmaUrls: input.figmaUrls,
|
|
6422
|
+
figmaSources: input.figmaSources,
|
|
6423
|
+
baseUrl,
|
|
6424
|
+
pages: input.pages
|
|
6425
|
+
}),
|
|
6426
|
+
baseUrl
|
|
6427
|
+
);
|
|
6428
|
+
await writeVisualManifest(ctx.projectRoot, manifest);
|
|
6429
|
+
const fetDir = join28(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
6430
|
+
await mkdir10(fetDir, { recursive: true });
|
|
6431
|
+
await atomicWrite(join28(ctx.projectRoot, visualInstructionsRelativePath(changeId)), renderVisualInstructions(changeId, manifest, ctx.language));
|
|
6432
|
+
await atomicWrite(join28(ctx.projectRoot, visualSpecRelativePath(changeId)), renderVisualSpec(changeId, manifest, ctx.language));
|
|
6433
|
+
const changeState = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
6434
|
+
changeState.visual = {
|
|
6435
|
+
status: "ready",
|
|
6436
|
+
generatedAt: manifest.generatedAt,
|
|
6437
|
+
planningFingerprint: fingerprint2,
|
|
6438
|
+
manifestPath: manifestPath2(changeId)
|
|
6439
|
+
};
|
|
6440
|
+
invalidateVisualRun(changeState);
|
|
6441
|
+
await ctx.stateStore.writeChange(changeState);
|
|
6442
|
+
return manifest;
|
|
6443
|
+
}
|
|
6444
|
+
async function recordVisualSuccess(ctx, changeId, planningFingerprint, resultsPath, captureStatus) {
|
|
6445
|
+
const changeState = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
6446
|
+
changeState.visualRun = {
|
|
6447
|
+
status: "passed",
|
|
6448
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6449
|
+
compareMode: "layout-only",
|
|
6450
|
+
planningFingerprint,
|
|
6451
|
+
manifestPath: manifestPath2(changeId),
|
|
6452
|
+
resultsPath,
|
|
6453
|
+
captureStatus
|
|
6454
|
+
};
|
|
6455
|
+
await ctx.stateStore.writeChange(changeState);
|
|
6456
|
+
}
|
|
6457
|
+
async function recordVisualFailure(ctx, changeId, planningFingerprint, steps, captureStatus, layoutStatus, regions, message) {
|
|
6458
|
+
const results = buildVisualResults({
|
|
6459
|
+
changeId,
|
|
6460
|
+
planningFingerprint,
|
|
6461
|
+
steps,
|
|
6462
|
+
captureStatus,
|
|
6463
|
+
layoutStatus,
|
|
6464
|
+
regions
|
|
6465
|
+
});
|
|
6466
|
+
await writeVisualResults(ctx.projectRoot, results);
|
|
6467
|
+
const changeState = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
6468
|
+
changeState.visualRun = {
|
|
6469
|
+
status: "failed",
|
|
6470
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6471
|
+
compareMode: "layout-only",
|
|
6472
|
+
planningFingerprint,
|
|
6473
|
+
manifestPath: manifestPath2(changeId),
|
|
6474
|
+
resultsPath: visualResultsRelativePath(changeId),
|
|
6475
|
+
captureStatus
|
|
6476
|
+
};
|
|
6477
|
+
await ctx.stateStore.writeChange(changeState);
|
|
6478
|
+
if (message) {
|
|
6479
|
+
void message;
|
|
6480
|
+
}
|
|
6481
|
+
}
|
|
6482
|
+
|
|
6483
|
+
// src/commands/verify.ts
|
|
6484
|
+
import { createHash as createHash2 } from "crypto";
|
|
6485
|
+
import { mkdir as mkdir11, readFile as readFile21, stat as stat12 } from "fs/promises";
|
|
6486
|
+
import { join as join29 } from "path";
|
|
6487
|
+
async function verifyCommand(ctx, options) {
|
|
6488
|
+
if (options.auto) {
|
|
6489
|
+
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
6490
|
+
const plan = {
|
|
6491
|
+
schemaVersion: 1,
|
|
6492
|
+
packageManager: scan.project.packageManager,
|
|
6493
|
+
workspaces: [
|
|
6494
|
+
{
|
|
6495
|
+
name: "root",
|
|
6496
|
+
cwd: ".",
|
|
6497
|
+
commands: Object.entries(scan.commands).filter(([name]) => ["lint", "typecheck", "test"].includes(name)).map(([dimension, command]) => ({
|
|
6498
|
+
dimension,
|
|
6499
|
+
command: command.command,
|
|
6500
|
+
source: command.source,
|
|
6501
|
+
required: command.required,
|
|
6502
|
+
statusIfMissing: command.required ? "fail" : "warn"
|
|
6503
|
+
}))
|
|
6504
|
+
}
|
|
6505
|
+
],
|
|
6506
|
+
missing: ["lint", "typecheck", "test"].filter((name) => !scan.commands[name])
|
|
6507
|
+
};
|
|
6508
|
+
if (ctx.yes) {
|
|
6509
|
+
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
6510
|
+
global.verifyAuthorization = {
|
|
6511
|
+
schemaVersion: 1,
|
|
6512
|
+
approvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6513
|
+
commandFingerprint: fingerprint(plan),
|
|
6514
|
+
packageManager: plan.packageManager,
|
|
6515
|
+
plan: plan.workspaces.flatMap(
|
|
6516
|
+
(workspace) => workspace.commands.map((command) => ({
|
|
6517
|
+
cwd: workspace.cwd,
|
|
6518
|
+
dimension: command.dimension,
|
|
6519
|
+
command: command.command,
|
|
6520
|
+
source: command.source,
|
|
6521
|
+
required: command.required
|
|
6522
|
+
}))
|
|
6523
|
+
)
|
|
6524
|
+
};
|
|
6525
|
+
await ctx.stateStore.writeGlobal(global);
|
|
6526
|
+
}
|
|
6527
|
+
ctx.output.result({
|
|
6528
|
+
ok: true,
|
|
6529
|
+
command: "verify",
|
|
6530
|
+
summary: ctx.yes ? "\u5DF2\u786E\u8BA4 verify --auto \u6267\u884C\u8BA1\u5212\uFF1BMVP \u6682\u4E0D\u6267\u884C\u4ED3\u5E93\u811A\u672C\u3002" : "\u5DF2\u751F\u6210 verify --auto \u6267\u884C\u8BA1\u5212\uFF1BMVP \u6682\u4E0D\u6267\u884C\u4ED3\u5E93\u811A\u672C\u3002",
|
|
6531
|
+
warnings: plan.missing.map((name) => `\u672A\u53D1\u73B0 ${name} \u811A\u672C\uFF0C\u5C06\u5728\u81EA\u52A8\u6267\u884C\u7248\u672C\u4E2D\u6309\u914D\u7F6E\u5904\u7406\u3002`),
|
|
6532
|
+
data: plan,
|
|
6533
|
+
nextSteps: ctx.yes ? ["\u5F53\u524D\u7248\u672C\u8BF7\u8FD0\u884C fet verify \u8FDB\u5165\u624B\u52A8\u9A8C\u8BC1\u6A21\u5F0F"] : ["\u5BA1\u6838\u6267\u884C\u8BA1\u5212", "\u786E\u8BA4\u8BA1\u5212\u540E\u53EF\u8FD0\u884C fet verify --auto --yes", "\u5F53\u524D\u7248\u672C\u8BF7\u8FD0\u884C fet verify \u8FDB\u5165\u624B\u52A8\u9A8C\u8BC1\u6A21\u5F0F"]
|
|
6534
|
+
});
|
|
6535
|
+
return;
|
|
6536
|
+
}
|
|
6537
|
+
await withProjectLock(
|
|
6538
|
+
ctx.projectRoot,
|
|
6539
|
+
{ command: "verify", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
|
|
6540
|
+
async () => {
|
|
6541
|
+
const changeId = await resolveChangeId(ctx);
|
|
6542
|
+
if (options.done) {
|
|
6543
|
+
await markDone(ctx, changeId);
|
|
6544
|
+
} else {
|
|
6545
|
+
await writeInstructions(ctx, changeId);
|
|
6546
|
+
}
|
|
6547
|
+
}
|
|
6548
|
+
);
|
|
6549
|
+
}
|
|
6550
|
+
async function writeInstructions(ctx, changeId) {
|
|
6551
|
+
await assertChangeExists(ctx, changeId);
|
|
6552
|
+
await assertTestPassed(ctx, changeId);
|
|
6553
|
+
await assertVisualPassed(ctx, changeId);
|
|
6554
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6555
|
+
const dir = join29(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
6556
|
+
const instructionsPath = join29(dir, "verify-instructions.md");
|
|
6557
|
+
await mkdir11(dir, { recursive: true });
|
|
6558
|
+
await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
|
|
5369
6559
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
5370
6560
|
state.currentPhase = "verify";
|
|
5371
6561
|
state.phases.verify = { status: "in_progress", updatedAt: generatedAt };
|
|
@@ -5380,8 +6570,9 @@ async function writeInstructions(ctx, changeId) {
|
|
|
5380
6570
|
async function markDone(ctx, changeId) {
|
|
5381
6571
|
await assertChangeExists(ctx, changeId);
|
|
5382
6572
|
await assertTestPassed(ctx, changeId);
|
|
6573
|
+
await assertVisualPassed(ctx, changeId);
|
|
5383
6574
|
const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5384
|
-
const instructionsPath =
|
|
6575
|
+
const instructionsPath = join29(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
|
|
5385
6576
|
const instructions = await readInstructions(ctx, instructionsPath, changeId);
|
|
5386
6577
|
const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
|
|
5387
6578
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
@@ -5405,8 +6596,8 @@ async function markDone(ctx, changeId) {
|
|
|
5405
6596
|
}
|
|
5406
6597
|
async function readInstructions(ctx, path, changeId) {
|
|
5407
6598
|
try {
|
|
5408
|
-
await
|
|
5409
|
-
const content = await
|
|
6599
|
+
await stat12(path);
|
|
6600
|
+
const content = await readFile21(path, "utf8");
|
|
5410
6601
|
const fileChangeId = readFrontMatterValue(content, "changeId");
|
|
5411
6602
|
if (fileChangeId !== changeId) {
|
|
5412
6603
|
throw new FetError({
|
|
@@ -5529,12 +6720,12 @@ function renderIdeModelPolicy(command, language = "zh-CN") {
|
|
|
5529
6720
|
import { resolve } from "path";
|
|
5530
6721
|
|
|
5531
6722
|
// src/adapters/codex/index.ts
|
|
5532
|
-
import { mkdir as
|
|
6723
|
+
import { mkdir as mkdir12, readFile as readFile22, stat as stat13 } from "fs/promises";
|
|
5533
6724
|
import { homedir } from "os";
|
|
5534
|
-
import { dirname as
|
|
6725
|
+
import { dirname as dirname11, join as join30 } from "path";
|
|
5535
6726
|
|
|
5536
6727
|
// src/adapters/commands.ts
|
|
5537
|
-
var FET_STANDALONE_COMMANDS = ["tdd", "test"];
|
|
6728
|
+
var FET_STANDALONE_COMMANDS = ["tdd", "test", "visual"];
|
|
5538
6729
|
var FET_WORKFLOW_COMMANDS = [
|
|
5539
6730
|
"explore",
|
|
5540
6731
|
"propose",
|
|
@@ -5578,6 +6769,7 @@ Before doing FET or OpenSpec work in Codex, read:
|
|
|
5578
6769
|
- .codex/fet/spec-language.md when writing or updating OpenSpec specs
|
|
5579
6770
|
- openspec/changes/<change-id>/.fet/figma-apply-instructions.md before UI work when FET apply reports Figma links
|
|
5580
6771
|
- .codex/fet/ui-display-contract.md when UI binds API data; openspec/changes/<change-id>/.fet/ui-display-contract.yaml when present
|
|
6772
|
+
- .codex/fet/write-boundary.md for default edit scope (src/, openspec/, .fet/ only; ask before other paths)
|
|
5581
6773
|
- the active change files under openspec/changes/<change-id>/, when a change is selected
|
|
5582
6774
|
|
|
5583
6775
|
If GitNexus code graph context is available in the IDE or MCP tools, prefer it before broad repository scans. Use it to identify relevant modules, dependencies, and insertion points, then read only the concrete source files needed. If GitNexus is unavailable, continue with the normal FET/OpenSpec workflow.
|
|
@@ -5599,6 +6791,7 @@ ${languageInstruction(language)}
|
|
|
5599
6791
|
- \u6309 Figma \u5B9E\u73B0 UI \u65F6\u9605\u8BFB .codex/fet/figma-stop.md\uFF1B\u6709 figma-apply-instructions.md \u65F6\u5FC5\u987B\u5728\u6539 UI \u524D\u5B8C\u6574\u6267\u884C
|
|
5600
6792
|
- \u7F16\u5199\u6216\u66F4\u65B0 spec \u65F6\u9605\u8BFB .codex/fet/spec-language.md\uFF08\u82F1\u6587\u89C4\u8303 + \u540C\u6B21\u7F16\u8F91\u7EF4\u62A4\u4E2D\u6587\u6CE8\u91CA\uFF09
|
|
5601
6793
|
- \u7ED1\u5B9A\u63A5\u53E3\u7684 UI \u9605\u8BFB .codex/fet/ui-display-contract.md\uFF1B\u5B58\u5728 ui-display-contract.yaml \u65F6\u5B9E\u65BD\u524D\u987B\u786E\u8BA4 displayFields
|
|
6794
|
+
- \u4FEE\u6539\u6587\u4EF6\u524D\u9605\u8BFB .codex/fet/write-boundary.md\uFF08\u9ED8\u8BA4\u4EC5 src/\u3001openspec/\u3001.fet/\uFF1B\u5176\u5B83\u8DEF\u5F84\u987B\u7528\u6237\u660E\u786E\u540C\u610F\uFF09
|
|
5602
6795
|
- \u5982\u679C\u5DF2\u9009\u62E9 change\uFF0C\u9605\u8BFB openspec/changes/<change-id>/ \u4E0B\u7684\u5F53\u524D\u4EA7\u7269
|
|
5603
6796
|
|
|
5604
6797
|
\u5982\u679C IDE \u6216 MCP \u5DE5\u5177\u4E2D\u53EF\u7528 GitNexus \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\uFF0C\u5148\u7528\u5B83\u7F29\u5C0F\u4ED3\u5E93\u626B\u63CF\u8303\u56F4\uFF1B\u7528\u56FE\u8BC6\u522B\u76F8\u5173\u6A21\u5757\u3001\u4F9D\u8D56\u548C\u63D2\u5165\u70B9\uFF0C\u518D\u53EA\u8BFB\u53D6\u9700\u8981\u786E\u8BA4\u884C\u4E3A\u7684\u5177\u4F53\u6E90\u7801\u6587\u4EF6\u3002GitNexus \u4E0D\u53EF\u7528\u65F6\uFF0C\u6309\u666E\u901A FET/OpenSpec \u5DE5\u4F5C\u6D41\u7EE7\u7EED\u3002
|
|
@@ -5637,12 +6830,19 @@ function codexUiDisplayContractFile(language = DEFAULT_LANGUAGE) {
|
|
|
5637
6830
|
content: renderCodexUiDisplayContractGuide(language)
|
|
5638
6831
|
};
|
|
5639
6832
|
}
|
|
6833
|
+
function codexWriteBoundaryFile(language = DEFAULT_LANGUAGE) {
|
|
6834
|
+
return {
|
|
6835
|
+
path: ".codex/fet/write-boundary.md",
|
|
6836
|
+
content: renderCodexWriteBoundaryGuide(language)
|
|
6837
|
+
};
|
|
6838
|
+
}
|
|
5640
6839
|
function codexCommandFiles(language = DEFAULT_LANGUAGE) {
|
|
5641
6840
|
return [
|
|
5642
6841
|
codexKarpathyGuidelinesFile(language),
|
|
5643
6842
|
codexFigmaStopFile(language),
|
|
5644
6843
|
codexUiDisplayContractFile(language),
|
|
5645
6844
|
codexSpecLanguageFile(language),
|
|
6845
|
+
codexWriteBoundaryFile(language),
|
|
5646
6846
|
...FET_ADAPTER_COMMANDS.map((command) => ({
|
|
5647
6847
|
path: `.codex/fet/commands/${command}.md`,
|
|
5648
6848
|
content: renderCommand(command, language)
|
|
@@ -5684,6 +6884,9 @@ function renderCommand(command, language) {
|
|
|
5684
6884
|
if (command.startsWith("graph-")) {
|
|
5685
6885
|
return renderGraphCommand(command, language);
|
|
5686
6886
|
}
|
|
6887
|
+
if (command === "tdd" || command === "test" || command === "visual") {
|
|
6888
|
+
return renderStandaloneWorkflowCommand(command, language);
|
|
6889
|
+
}
|
|
5687
6890
|
const usage = renderFetAdapterUsage(command, "");
|
|
5688
6891
|
return `<!-- FET:MANAGED
|
|
5689
6892
|
schemaVersion: 1
|
|
@@ -5714,11 +6917,16 @@ ${usage}
|
|
|
5714
6917
|
If the command needs a change id, pass it with \`--change <change-id>\` or use the active OpenSpec change from the user's request.
|
|
5715
6918
|
|
|
5716
6919
|
After the command completes, report the important next steps from the FET output and keep any generated OpenSpec artifacts in the normal project workflow.
|
|
6920
|
+
|
|
6921
|
+
${renderWriteBoundaryGuardrail(language)}
|
|
5717
6922
|
`;
|
|
5718
6923
|
}
|
|
5719
6924
|
function renderCommandZh(command) {
|
|
5720
6925
|
const usage = renderFetAdapterUsage(command, command === "fill-context" ? "" : command === "passthrough" ? "<openspec-command> [...args]" : "");
|
|
5721
6926
|
const title = commandTitleZh(command);
|
|
6927
|
+
if (command === "tdd" || command === "test" || command === "visual") {
|
|
6928
|
+
return renderStandaloneWorkflowCommand(command, "zh-CN");
|
|
6929
|
+
}
|
|
5722
6930
|
if (command === "graph-setup") {
|
|
5723
6931
|
return `<!-- FET:MANAGED
|
|
5724
6932
|
schemaVersion: 1
|
|
@@ -5776,6 +6984,8 @@ ${usage}
|
|
|
5776
6984
|
\u5982\u679C\u547D\u4EE4\u9700\u8981 change id\uFF0C\u4F18\u5148\u4F7F\u7528\u7528\u6237\u8F93\u5165\u3001\`--change <change-id>\`\u3001FET active change \u6216\u552F\u4E00\u6253\u5F00\u7684 change\u3002\u5B58\u5728\u6B67\u4E49\u65F6\u5148\u8BE2\u95EE\u7528\u6237\u3002
|
|
5777
6985
|
|
|
5778
6986
|
\u6267\u884C\u5B8C\u6210\u540E\uFF0C\u7528\u4E2D\u6587\u603B\u7ED3\u5173\u952E\u8F93\u51FA\u3001\u751F\u6210\u6216\u66F4\u65B0\u7684\u6587\u4EF6\uFF0C\u4EE5\u53CA\u4E0B\u4E00\u6B65\u5EFA\u8BAE\u3002
|
|
6987
|
+
|
|
6988
|
+
${renderWriteBoundaryGuardrail("zh-CN")}
|
|
5779
6989
|
`;
|
|
5780
6990
|
}
|
|
5781
6991
|
function renderPassthroughCommand(language) {
|
|
@@ -5892,6 +7102,9 @@ function renderSlashPrompt(command, language) {
|
|
|
5892
7102
|
if (command === "apply") {
|
|
5893
7103
|
return renderApplySlashPrompt(language);
|
|
5894
7104
|
}
|
|
7105
|
+
if (command === "tdd" || command === "test" || command === "visual") {
|
|
7106
|
+
return renderStandaloneWorkflowSlashPrompt(command, language);
|
|
7107
|
+
}
|
|
5895
7108
|
if (command === "verify") {
|
|
5896
7109
|
return renderVerifySlashPrompt(language);
|
|
5897
7110
|
}
|
|
@@ -5953,6 +7166,9 @@ function renderSlashPromptZh(command) {
|
|
|
5953
7166
|
if (command === "graph-setup") {
|
|
5954
7167
|
return renderGraphSetupSlashPrompt("zh-CN");
|
|
5955
7168
|
}
|
|
7169
|
+
if (command === "tdd" || command === "test" || command === "visual") {
|
|
7170
|
+
return renderStandaloneWorkflowSlashPrompt(command, "zh-CN");
|
|
7171
|
+
}
|
|
5956
7172
|
const usage = renderFetAdapterUsage(command, command === "fill-context" ? "" : command === "passthrough" ? "<openspec-command> [...args]" : "[...args]");
|
|
5957
7173
|
const argumentHint = command === "passthrough" ? "openspec-command [...args]" : void 0;
|
|
5958
7174
|
const argumentHintLine = argumentHint ? `argument-hint: ${argumentHint}
|
|
@@ -5993,9 +7209,10 @@ ${commandGoalZh(command)}
|
|
|
5993
7209
|
- \u9ED8\u8BA4\u4F7F\u7528\u4E2D\u6587\u4EA7\u51FA\u3002
|
|
5994
7210
|
- \u4E0D\u8981\u7ED5\u8FC7 FET \u76F4\u63A5\u8C03\u7528 openspec\uFF0C\u9664\u975E FET \u547D\u4EE4\u672C\u8EAB\u4E0D\u53EF\u7528\u3002
|
|
5995
7211
|
- change \u4E0D\u660E\u786E\u65F6\u5148\u8BE2\u95EE\u7528\u6237\u3002
|
|
5996
|
-
${
|
|
7212
|
+
${renderWriteBoundaryGuardrail("zh-CN")}
|
|
7213
|
+
${command === "fill-context" ? "- fet fill-context \u53EF\u80FD\u5DF2\u5199\u5165\u5C0F\u7A0B\u5E8F\u5305\u4F53\u79EF\u4E0E 2MB \u7EA6\u675F\uFF0C\u4E0D\u8981\u8986\u76D6\u8BE5\u8282\u4E2D\u7684\u626B\u63CF\u7ED3\u679C\uFF0C\u9664\u975E\u4ED3\u5E93\u7ED3\u6784\u5DF2\u53D8\u3002\n- \u66FF\u6362 AGENTS.md \u4E2D\u5176\u4F59 [NEEDS LLM INPUT] \u5360\u4F4D\u7B26\uFF0C\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\uFF1B\u4FEE\u6539 AGENTS.md \u5C5E\u4E8E\u5141\u8BB8\u8303\u56F4\u5916\u8DEF\u5F84\uFF0C\u987B\u5728\u672C\u8F6E\u5DF2\u83B7\u5F97\u7528\u6237\u540C\u610F\u3002\n" : ""}${command === "propose" || command === "continue" || command === "ff" ? "- \u4E00\u6B21\u53EA\u521B\u5EFA\u4E00\u4E2A ready artifact\uFF0C\u5E76\u5728\u5199\u5165\u524D\u9605\u8BFB\u4F9D\u8D56\u6587\u4EF6\u3002\n" : ""}${command === "propose" || command === "continue" || command === "ff" ? "- \u4E0D\u8981\u5728\u7528\u6237\u5BA1\u9605\u5F53\u524D\u4EA7\u7269\u524D\u81EA\u52A8\u8FD0\u884C fet continue\u3001fet ff \u6216\u5FAA\u73AF\u751F\u6210\u540E\u7EED\u4EA7\u7269\uFF1B\u9700\u8981\u7528\u6237\u660E\u786E\u786E\u8BA4\u540E\u518D\u63A8\u8FDB\u3002\n" : ""}${command === "propose" || command === "continue" || command === "ff" || command === "sync" ? `${renderSpecArtifactGuardrail("zh-CN")}
|
|
5997
7214
|
` : ""}${command === "propose" || command === "continue" || command === "ff" ? `${renderUiDisplayContractGuardrail("zh-CN")}
|
|
5998
|
-
` : ""}${command === "apply" ? "- \u4E0D\u8981\u5728\u672A\u5B8C\u6210\u771F\u5B9E\u5B9E\u73B0\u524D\u52FE\u9009 tasks.md\uFF1B\u4E0D\u8981\u4ECE apply \u9636\u6BB5\u76F4\u63A5 sync \u6216 archive\u3002\n- \u82E5\u5B58\u5728 openspec/changes/<change-id>/.fet/figma-apply-instructions.md\uFF0C\u5FC5\u987B\u5148\u8BFB Figma\uFF08MCP/API\uFF09\u518D\u6539 UI\uFF1B\u5931\u8D25\u5219\u505C\u4E0B\u95EE\u7528\u6237\uFF0C\u7981\u6B62\u731C\u6837\u5F0F\u3002\n- \u82E5\u5B58\u5728 openspec/changes/<change-id>/.fet/ui-display-contract.yaml\uFF0C\u5B9E\u65BD\u7ED1\u5B9A\u63A5\u53E3\u7684 UI \u524D\u987B\u786E\u8BA4 displayFields\uFF0C\u7981\u6B62\u6309 OpenAPI \u5168\u91CF\u5C55\u793A\u3002\n" : ""}`;
|
|
7215
|
+
` : ""}${command === "apply" ? "- \u4E0D\u8981\u5728\u672A\u5B8C\u6210\u771F\u5B9E\u5B9E\u73B0\u524D\u52FE\u9009 tasks.md\uFF1B\u4E0D\u8981\u4ECE apply \u9636\u6BB5\u76F4\u63A5 sync \u6216 archive\u3002\n- \u5B9E\u65BD\u524D\u987B\u5DF2\u8FD0\u884C fet tdd\uFF1B\u5B58\u5728 tdd-manifest.yaml \u65F6\u6309\u6E05\u5355\u7F16\u5199\u6D4B\u8BD5\u4E0E\u5B9E\u73B0\uFF0Cfet verify \u524D\u5148 fet test\u3002\n- \u82E5\u5B58\u5728 openspec/changes/<change-id>/.fet/figma-apply-instructions.md\uFF0C\u5FC5\u987B\u5148\u8BFB Figma\uFF08MCP/API\uFF09\u518D\u6539 UI\uFF1B\u5931\u8D25\u5219\u505C\u4E0B\u95EE\u7528\u6237\uFF0C\u7981\u6B62\u731C\u6837\u5F0F\u3002\n- \u82E5\u5B58\u5728 openspec/changes/<change-id>/.fet/ui-display-contract.yaml\uFF0C\u5B9E\u65BD\u7ED1\u5B9A\u63A5\u53E3\u7684 UI \u524D\u987B\u786E\u8BA4 displayFields\uFF0C\u7981\u6B62\u6309 OpenAPI \u5168\u91CF\u5C55\u793A\u3002\n" : ""}`;
|
|
5999
7216
|
}
|
|
6000
7217
|
function commandTitleZh(command) {
|
|
6001
7218
|
const titles = {
|
|
@@ -6069,7 +7286,9 @@ First run:
|
|
|
6069
7286
|
fet fill-context
|
|
6070
7287
|
\`\`\`
|
|
6071
7288
|
|
|
6072
|
-
Then read AGENTS.md and openspec/config.yaml, inspect the project, and replace every [NEEDS LLM INPUT] or [NEED LLM INPUT] placeholder in AGENTS.md with concrete project-specific content. Preserve FET managed markers and do not modify business code.
|
|
7289
|
+
Then read AGENTS.md and openspec/config.yaml, inspect the project, and replace every [NEEDS LLM INPUT] or [NEED LLM INPUT] placeholder in AGENTS.md with concrete project-specific content. Preserve FET managed markers and do not modify business code. Editing AGENTS.md is outside the default write scope\u2014confirm user approval first.
|
|
7290
|
+
|
|
7291
|
+
${renderWriteBoundaryGuardrail(language)}
|
|
6073
7292
|
`;
|
|
6074
7293
|
}
|
|
6075
7294
|
function renderFillContextSlashPrompt(language) {
|
|
@@ -6105,7 +7324,9 @@ Steps:
|
|
|
6105
7324
|
Guardrails:
|
|
6106
7325
|
- Do not invent facts that cannot be inferred from the repo.
|
|
6107
7326
|
- Use [UNKNOWN] only when the repository does not contain enough evidence.
|
|
6108
|
-
- Keep generated context stable and useful for future AI coding sessions
|
|
7327
|
+
- Keep generated context stable and useful for future AI coding sessions.
|
|
7328
|
+
- Editing AGENTS.md is outside the default write scope; confirm user approval first (see .codex/fet/write-boundary.md).
|
|
7329
|
+
${renderWriteBoundaryGuardrail(language)}`,
|
|
6109
7330
|
void 0,
|
|
6110
7331
|
language
|
|
6111
7332
|
);
|
|
@@ -6174,14 +7395,16 @@ Steps:
|
|
|
6174
7395
|
- Follow proposal, specs, design, and tasks.
|
|
6175
7396
|
- Mark each completed task checkbox in tasks.md from \`- [ ]\` to \`- [x]\`.
|
|
6176
7397
|
- Pause and ask if a task is ambiguous or reveals a design conflict.
|
|
6177
|
-
8.
|
|
7398
|
+
8. Run \`fet tdd --change <change-id>\` before implementation when no \`tdd-manifest.yaml\` exists yet; after coding run \`fet test --change <change-id>\` before \`fet verify\`.
|
|
7399
|
+
9. After completing or pausing, summarize completed tasks, remaining tasks, and blockers.
|
|
6178
7400
|
|
|
6179
7401
|
Guardrails:
|
|
6180
7402
|
- Never skip reading OpenSpec artifacts before implementation.
|
|
6181
7403
|
- When Figma links exist for this change, never implement or restyle UI without reading Figma first.
|
|
6182
7404
|
- When ui-display-contract.yaml exists, API schemas are not UI checklists\u2014only displayFields may render.
|
|
6183
7405
|
- Do not mark a task complete until the code change is actually done.
|
|
6184
|
-
- Do not run sync or archive from apply
|
|
7406
|
+
- Do not run sync or archive from apply.
|
|
7407
|
+
${renderWriteBoundaryGuardrail(language)}`,
|
|
6185
7408
|
void 0,
|
|
6186
7409
|
language
|
|
6187
7410
|
);
|
|
@@ -6608,8 +7831,97 @@ ${languageInstruction(language)}
|
|
|
6608
7831
|
${graphContextInstruction}
|
|
6609
7832
|
|
|
6610
7833
|
${body}
|
|
7834
|
+
|
|
7835
|
+
${renderWriteBoundaryGuardrail(language)}
|
|
6611
7836
|
`;
|
|
6612
7837
|
}
|
|
7838
|
+
function renderStandaloneWorkflowCommand(command, language) {
|
|
7839
|
+
const usage = renderFetAdapterUsage(command, "");
|
|
7840
|
+
const { title, body } = standaloneWorkflowCopy(command, language);
|
|
7841
|
+
return `<!-- FET:MANAGED
|
|
7842
|
+
schemaVersion: 1
|
|
7843
|
+
fetVersion: ${FET_VERSION}
|
|
7844
|
+
generator: codex-adapter
|
|
7845
|
+
adapterVersion: 1
|
|
7846
|
+
command: ${usage}
|
|
7847
|
+
FET:END -->
|
|
7848
|
+
|
|
7849
|
+
# ${usage}
|
|
7850
|
+
|
|
7851
|
+
${renderIdeModelPolicy(command, language)}
|
|
7852
|
+
|
|
7853
|
+
${languageInstruction(language)}
|
|
7854
|
+
|
|
7855
|
+
## ${language === "en" ? "Purpose" : "\u7528\u9014"}
|
|
7856
|
+
|
|
7857
|
+
${title}
|
|
7858
|
+
|
|
7859
|
+
## ${language === "en" ? "Workflow" : "\u5DE5\u4F5C\u6D41"}
|
|
7860
|
+
|
|
7861
|
+
${body}
|
|
7862
|
+
|
|
7863
|
+
${renderWriteBoundaryGuardrail(language)}
|
|
7864
|
+
`;
|
|
7865
|
+
}
|
|
7866
|
+
function renderStandaloneWorkflowSlashPrompt(command, language) {
|
|
7867
|
+
const usage = renderFetAdapterUsage(command, "[...args]");
|
|
7868
|
+
const { title, body, description } = standaloneWorkflowCopy(command, language);
|
|
7869
|
+
return renderManagedSlashPrompt(
|
|
7870
|
+
usage,
|
|
7871
|
+
description,
|
|
7872
|
+
`${title}
|
|
7873
|
+
|
|
7874
|
+
Steps:
|
|
7875
|
+
|
|
7876
|
+
1. Read AGENTS.md, openspec/config.yaml, .codex/fet/karpathy-guidelines.md, and .codex/fet/write-boundary.md.
|
|
7877
|
+
2. Resolve the change id when needed (\`--change <change-id>\` or active change).
|
|
7878
|
+
3. Run:
|
|
7879
|
+
\`\`\`sh
|
|
7880
|
+
${renderFetAdapterUsage(command, "")}
|
|
7881
|
+
\`\`\`
|
|
7882
|
+
4. ${body}
|
|
7883
|
+
5. Summarize outputs, paths written under openspec/changes/<change-id>/.fet/, and next steps.
|
|
7884
|
+
|
|
7885
|
+
Guardrails:
|
|
7886
|
+
- ${body.split("\n")[0]}
|
|
7887
|
+
${renderWriteBoundaryGuardrail(language)}`,
|
|
7888
|
+
void 0,
|
|
7889
|
+
language
|
|
7890
|
+
);
|
|
7891
|
+
}
|
|
7892
|
+
function standaloneWorkflowCopy(command, language) {
|
|
7893
|
+
if (command === "tdd") {
|
|
7894
|
+
return language === "en" ? {
|
|
7895
|
+
description: "Generate per-change TDD manifest and test instructions",
|
|
7896
|
+
title: "Generate the TDD manifest before implementation.",
|
|
7897
|
+
body: "After planning artifacts exist, run this before `fet apply`. It writes `openspec/changes/<change-id>/.fet/tdd-manifest.yaml`, `tdd-spec.md`, and `tdd-instructions.md`. Then add failing tests in the repo before implementation (tests/ edits need user approval per write-boundary)."
|
|
7898
|
+
} : {
|
|
7899
|
+
description: "\u751F\u6210\u672C change \u7684 TDD \u6E05\u5355\u4E0E\u6D4B\u8BD5\u6307\u5F15",
|
|
7900
|
+
title: "\u5728\u5B9E\u65BD\u524D\u751F\u6210 TDD \u6E05\u5355\u3002",
|
|
7901
|
+
body: "\u89C4\u5212\u4EA7\u7269\u5C31\u7EEA\u540E\u3001\u6267\u884C `fet apply` \u4E4B\u524D\u8FD0\u884C\u3002\u4F1A\u5199\u5165 `openspec/changes/<change-id>/.fet/tdd-manifest.yaml`\u3001`tdd-spec.md`\u3001`tdd-instructions.md`\uFF0C\u518D\u5728\u4ED3\u5E93\u4E2D\u7F16\u5199\u9884\u671F\u5931\u8D25\u7684\u6D4B\u8BD5\uFF08\u4FEE\u6539 tests/ \u987B\u7B26\u5408 write-boundary\uFF0C\u5148\u83B7\u7528\u6237\u540C\u610F\uFF09\u3002"
|
|
7902
|
+
};
|
|
7903
|
+
}
|
|
7904
|
+
if (command === "test") {
|
|
7905
|
+
return language === "en" ? {
|
|
7906
|
+
description: "Run unit tests scoped to the change TDD manifest",
|
|
7907
|
+
title: "Run change-scoped tests after implementation.",
|
|
7908
|
+
body: "Requires `tdd-manifest.yaml`. Records pass/fail in FET state; `fet verify` is blocked until this passes (unless configured to skip)."
|
|
7909
|
+
} : {
|
|
7910
|
+
description: "\u6309 change TDD \u6E05\u5355\u8FD0\u884C\u5355\u6D4B",
|
|
7911
|
+
title: "\u5B9E\u73B0\u5B8C\u6210\u540E\u6309 change \u8FD0\u884C\u6D4B\u8BD5\u3002",
|
|
7912
|
+
body: "\u9700\u8981\u6709\u6548\u7684 `tdd-manifest.yaml`\u3002\u7ED3\u679C\u5199\u5165 FET \u72B6\u6001\uFF1B\u672A\u901A\u8FC7\u524D `fet verify` \u4F1A\u88AB\u62E6\u622A\uFF08\u9664\u975E\u914D\u7F6E\u4E3A skip\uFF09\u3002"
|
|
7913
|
+
};
|
|
7914
|
+
}
|
|
7915
|
+
return language === "en" ? {
|
|
7916
|
+
description: "Layout-only visual verification for a change",
|
|
7917
|
+
title: "Run layout-only visual checks for the change.",
|
|
7918
|
+
body: "Default `fet visual` refreshes the manifest, captures with Playwright (`--base-url` required), and runs layout-only checks (no pixel match on dynamic API content). Use `--plan`, `--capture-only`, or `--check-layout-only` only when debugging."
|
|
7919
|
+
} : {
|
|
7920
|
+
description: "change \u7EA7 layout-only \u89C6\u89C9\u9A8C\u6536",
|
|
7921
|
+
title: "\u5BF9 change \u505A layout-only \u89C6\u89C9\u9A8C\u6536\u3002",
|
|
7922
|
+
body: "\u9ED8\u8BA4 `fet visual` \u4F1A\u66F4\u65B0\u6E05\u5355\u3001Playwright \u622A\u56FE\uFF08\u9700 `--base-url`\uFF09\u5E76\u505A layout-only \u68C0\u67E5\uFF08\u4E0D\u5BF9\u52A8\u6001\u63A5\u53E3\u5185\u5BB9\u505A\u50CF\u7D20\u5BF9\u6BD4\uFF09\u3002\u4EC5\u5728\u8C03\u8BD5\u65F6\u4F7F\u7528 `--plan`\u3001`--capture-only`\u3001`--check-layout-only`\u3002"
|
|
7923
|
+
};
|
|
7924
|
+
}
|
|
6613
7925
|
|
|
6614
7926
|
// src/adapters/codex/index.ts
|
|
6615
7927
|
var CodexAdapter = class {
|
|
@@ -6617,7 +7929,7 @@ var CodexAdapter = class {
|
|
|
6617
7929
|
adapterVersion = 1;
|
|
6618
7930
|
async detect(projectRoot) {
|
|
6619
7931
|
return {
|
|
6620
|
-
detected: await exists6(
|
|
7932
|
+
detected: await exists6(join30(projectRoot, ".codex")) || await exists6(join30(projectRoot, "AGENTS.md")),
|
|
6621
7933
|
reason: "Codex adapter is available for projects that use AGENTS.md"
|
|
6622
7934
|
};
|
|
6623
7935
|
}
|
|
@@ -6656,7 +7968,7 @@ var CodexAdapter = class {
|
|
|
6656
7968
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
6657
7969
|
await createBackup(target);
|
|
6658
7970
|
}
|
|
6659
|
-
await
|
|
7971
|
+
await mkdir12(dirname11(target), { recursive: true });
|
|
6660
7972
|
await atomicWrite(target, file.content);
|
|
6661
7973
|
written.push(displayPath);
|
|
6662
7974
|
}
|
|
@@ -6683,9 +7995,9 @@ var CodexAdapter = class {
|
|
|
6683
7995
|
};
|
|
6684
7996
|
function resolveTarget(projectRoot, file) {
|
|
6685
7997
|
if (file.root === "codex-home") {
|
|
6686
|
-
return
|
|
7998
|
+
return join30(resolveCodexHome(), file.path);
|
|
6687
7999
|
}
|
|
6688
|
-
return
|
|
8000
|
+
return join30(projectRoot, file.path);
|
|
6689
8001
|
}
|
|
6690
8002
|
function displayPathFor(file) {
|
|
6691
8003
|
if (file.root === "codex-home") {
|
|
@@ -6694,18 +8006,18 @@ function displayPathFor(file) {
|
|
|
6694
8006
|
return file.path;
|
|
6695
8007
|
}
|
|
6696
8008
|
function resolveCodexHome() {
|
|
6697
|
-
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ??
|
|
8009
|
+
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join30(homedir(), ".codex");
|
|
6698
8010
|
}
|
|
6699
8011
|
async function readExisting(path) {
|
|
6700
8012
|
try {
|
|
6701
|
-
return await
|
|
8013
|
+
return await readFile22(path, "utf8");
|
|
6702
8014
|
} catch {
|
|
6703
8015
|
return null;
|
|
6704
8016
|
}
|
|
6705
8017
|
}
|
|
6706
8018
|
async function exists6(path) {
|
|
6707
8019
|
try {
|
|
6708
|
-
await
|
|
8020
|
+
await stat13(path);
|
|
6709
8021
|
return true;
|
|
6710
8022
|
} catch {
|
|
6711
8023
|
return false;
|
|
@@ -6713,8 +8025,8 @@ async function exists6(path) {
|
|
|
6713
8025
|
}
|
|
6714
8026
|
|
|
6715
8027
|
// src/adapters/cursor/index.ts
|
|
6716
|
-
import { mkdir as
|
|
6717
|
-
import { dirname as
|
|
8028
|
+
import { mkdir as mkdir13, readFile as readFile23, stat as stat14 } from "fs/promises";
|
|
8029
|
+
import { dirname as dirname12, join as join31 } from "path";
|
|
6718
8030
|
|
|
6719
8031
|
// src/adapters/cursor/templates.ts
|
|
6720
8032
|
function cursorFigmaStopRuleFile(language = DEFAULT_LANGUAGE) {
|
|
@@ -6729,6 +8041,28 @@ function cursorSpecLanguageRuleFile(language = DEFAULT_LANGUAGE) {
|
|
|
6729
8041
|
content: renderCursorSpecLanguageRule(language)
|
|
6730
8042
|
};
|
|
6731
8043
|
}
|
|
8044
|
+
function cursorWriteBoundaryRuleFile(language = DEFAULT_LANGUAGE) {
|
|
8045
|
+
return {
|
|
8046
|
+
path: ".cursor/rules/fet-write-boundary.mdc",
|
|
8047
|
+
content: renderCursorWriteBoundaryRule(language)
|
|
8048
|
+
};
|
|
8049
|
+
}
|
|
8050
|
+
function cursorHookFiles() {
|
|
8051
|
+
return [
|
|
8052
|
+
{
|
|
8053
|
+
path: ".cursor/hooks/fet-guard-write-paths.mjs",
|
|
8054
|
+
content: renderCursorWritePathsHookMjs()
|
|
8055
|
+
},
|
|
8056
|
+
{
|
|
8057
|
+
path: ".cursor/hooks/fet-guard-shell-writes.mjs",
|
|
8058
|
+
content: renderCursorShellWriteHookMjs()
|
|
8059
|
+
},
|
|
8060
|
+
{
|
|
8061
|
+
path: ".cursor/hooks.json",
|
|
8062
|
+
content: renderCursorHooksJson()
|
|
8063
|
+
}
|
|
8064
|
+
];
|
|
8065
|
+
}
|
|
6732
8066
|
function cursorUiDisplayContractRuleFile(language = DEFAULT_LANGUAGE) {
|
|
6733
8067
|
return {
|
|
6734
8068
|
path: ".cursor/rules/fet-ui-display-contract.mdc",
|
|
@@ -6738,6 +8072,7 @@ function cursorUiDisplayContractRuleFile(language = DEFAULT_LANGUAGE) {
|
|
|
6738
8072
|
function cursorRuleFiles(language = DEFAULT_LANGUAGE) {
|
|
6739
8073
|
return [
|
|
6740
8074
|
cursorRuleFile(language),
|
|
8075
|
+
cursorWriteBoundaryRuleFile(language),
|
|
6741
8076
|
cursorFigmaStopRuleFile(language),
|
|
6742
8077
|
cursorUiDisplayContractRuleFile(language),
|
|
6743
8078
|
cursorSpecLanguageRuleFile(language)
|
|
@@ -6775,6 +8110,7 @@ ${languageInstruction(language)}
|
|
|
6775
8110
|
- \u6309 Figma \u5B9E\u73B0 UI \u65F6\u9075\u5B88 \`.cursor/rules/fet-figma-stop.mdc\`\uFF1B\u82E5\u5B58\u5728 \`openspec/changes/<change-id>/.fet/figma-apply-instructions.md\` \u5FC5\u987B\u5728\u6539 UI \u524D\u5B8C\u6574\u6267\u884C\uFF1B\u540C\u76EE\u5F55 \`figma-stop.md\` \u542B\u94FE\u63A5\u4E0E\u505C\u6B62\u89C4\u5219\u3002
|
|
6776
8111
|
- \u7F16\u5199\u6216\u4FEE\u6539 OpenSpec \`specs/**/spec.md\` \u65F6\u9075\u5B88 \`.cursor/rules/fet-spec-language.mdc\`\uFF08\u82F1\u6587\u89C4\u8303 + \`<!-- \u4E2D\u6587\uFF1A... -->\`\uFF0C\u540C\u6B21\u7F16\u8F91\u540C\u6B65\u66F4\u65B0\u4E2D\u6587\u8BF4\u660E\uFF09\u3002
|
|
6777
8112
|
- \u7ED1\u5B9A\u63A5\u53E3\u7684 UI \u9075\u5B88 \`.cursor/rules/fet-ui-display-contract.mdc\`\uFF1B\u5B58\u5728 \`openspec/changes/<change-id>/.fet/ui-display-contract.yaml\` \u65F6\u5B9E\u65BD\u524D\u987B\u786E\u8BA4 displayFields\u3002
|
|
8113
|
+
- \u4FEE\u6539\u6587\u4EF6\u65F6\u9075\u5B88 \`.cursor/rules/fet-write-boundary.mdc\`\uFF08\u9ED8\u8BA4\u4EC5 \`src/**\`\u3001\`openspec/**\`\u3001\`**/.fet/**\`\uFF1B\u5176\u5B83\u8DEF\u5F84\u987B\u7528\u6237\u660E\u786E\u540C\u610F\uFF09\u3002Cursor hooks \u4F1A\u5BF9\u8D8A\u754C\u5199\u5165\u5F39\u51FA\u5BA1\u6279\u3002
|
|
6778
8114
|
|
|
6779
8115
|
\u5982\u679C\u7528\u6237\u8F93\u5165\u7C7B\u4F3C \`/fet apply\` \u7684\u8BF7\u6C42\uFF0C\u8BF7\u628A\u5B83\u89C6\u4E3A\u5DE5\u4F5C\u6D41\u610F\u56FE\uFF0C\u5E76\u63D0\u793A\u6216\u6267\u884C\u5BF9\u5E94\u7684\u7EC8\u7AEF\u547D\u4EE4 \`fet <cmd>\`\u3002
|
|
6780
8116
|
`
|
|
@@ -6810,7 +8146,9 @@ ${languageInstruction(language)}
|
|
|
6810
8146
|
|
|
6811
8147
|
\`fet fill-context\` \u53EF\u80FD\u5DF2\u5199\u5165\u5C0F\u7A0B\u5E8F\u5305\u4F53\u79EF\u8868\u4E0E 2MB \u5F00\u53D1\u7EA6\u675F\uFF0C\u4E0D\u8981\u8986\u76D6 AGENTS.md\u300C\u5C0F\u7A0B\u5E8F\u300D\u8282\u4E2D\u7684\u626B\u63CF\u7ED3\u679C\uFF0C\u9664\u975E\u4ED3\u5E93\u7ED3\u6784\u5DF2\u53D8\u3002
|
|
6812
8148
|
|
|
6813
|
-
\u68C0\u67E5 README\u3001package scripts\u3001\u8DEF\u7531\u3001\u6D4B\u8BD5\u3001\u6E90\u7801\u7ED3\u6784\u548C\u73B0\u6709\u7EA6\u5B9A\u540E\uFF0C\u628A AGENTS.md \u4E2D**\u5176\u4F59** \`[NEEDS LLM INPUT]\` \u6216 \`[NEED LLM INPUT]\` \u5360\u4F4D\u7B26\u66FF\u6362\u4E3A\u5177\u4F53\u3001\u7B80\u6D01\u3001\u9879\u76EE\u76F8\u5173\u7684\u5185\u5BB9\u3002\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002
|
|
8149
|
+
\u68C0\u67E5 README\u3001package scripts\u3001\u8DEF\u7531\u3001\u6D4B\u8BD5\u3001\u6E90\u7801\u7ED3\u6784\u548C\u73B0\u6709\u7EA6\u5B9A\u540E\uFF0C\u628A AGENTS.md \u4E2D**\u5176\u4F59** \`[NEEDS LLM INPUT]\` \u6216 \`[NEED LLM INPUT]\` \u5360\u4F4D\u7B26\u66FF\u6362\u4E3A\u5177\u4F53\u3001\u7B80\u6D01\u3001\u9879\u76EE\u76F8\u5173\u7684\u5185\u5BB9\u3002\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002\u4FEE\u6539 \`AGENTS.md\` \u4E0D\u5728\u9ED8\u8BA4\u5199\u8DEF\u5F84\u5185\uFF0C\u987B\u786E\u8BA4\u7528\u6237\u5DF2\u540C\u610F\uFF08\u6216\u7531 hooks \u5BA1\u6279\uFF09\u3002
|
|
8150
|
+
|
|
8151
|
+
${renderWriteBoundaryGuardrail(language)}
|
|
6814
8152
|
`;
|
|
6815
8153
|
}
|
|
6816
8154
|
if (command === "graph-setup") {
|
|
@@ -6849,6 +8187,9 @@ After installation, verify with \`gitnexus --version\`, then run \`fet graph ini
|
|
|
6849
8187
|
if (command === "tdd" || command === "test") {
|
|
6850
8188
|
return renderTddTestSkill(command, usage, language);
|
|
6851
8189
|
}
|
|
8190
|
+
if (command === "visual") {
|
|
8191
|
+
return renderVisualSkill(usage, language);
|
|
8192
|
+
}
|
|
6852
8193
|
if (command === "propose" || command === "continue" || command === "ff") {
|
|
6853
8194
|
return renderPlanningSkill(command, usage, language);
|
|
6854
8195
|
}
|
|
@@ -6913,6 +8254,8 @@ ${renderSpecArtifactGuardrail(language)}
|
|
|
6913
8254
|
|
|
6914
8255
|
${renderUiDisplayContractGuardrail(language)}
|
|
6915
8256
|
|
|
8257
|
+
${renderWriteBoundaryGuardrail(language)}
|
|
8258
|
+
|
|
6916
8259
|
\u6267\u884C\u524D\u8BF7\u9605\u8BFB AGENTS.md\u3001openspec/config.yaml\uFF08\u542B \`fet.specLanguage\`\uFF09\u4E0E\u5F53\u524D change \u5DF2\u6709\u4EA7\u7269\u3002
|
|
6917
8260
|
`;
|
|
6918
8261
|
}
|
|
@@ -6968,6 +8311,37 @@ ${uiContractBlock}
|
|
|
6968
8311
|
\u6267\u884C\u524D\u8BF7\u9605\u8BFB AGENTS.md\u3001openspec/config.yaml \u4E0E\u5F53\u524D change \u4E0B\u7684 OpenSpec \u4EA7\u7269\u3002
|
|
6969
8312
|
|
|
6970
8313
|
\u5B9E\u65BD\u524D\u987B\u5DF2\u8FD0\u884C \`fet tdd\`\uFF1B\u5B58\u5728 \`openspec/changes/<change-id>/.fet/tdd-manifest.yaml\` \u65F6\u6309\u6E05\u5355\u7F16\u5199\u6D4B\u8BD5\u4E0E\u5B9E\u73B0\uFF0C\u5E76\u5728 \`fet verify\` \u524D\u5148 \`fet test\`\u3002
|
|
8314
|
+
|
|
8315
|
+
${renderWriteBoundaryGuardrail(language)}
|
|
8316
|
+
`;
|
|
8317
|
+
}
|
|
8318
|
+
function renderVisualSkill(usage, language) {
|
|
8319
|
+
const body = language === "en" ? `Default \`fet visual\` runs manifest refresh, Playwright capture (needs \`--base-url\`), and **layout-only** checks (no pixel match on dynamic API content). Use \`--plan\`, \`--capture-only\`, or \`--check-layout-only\` only when debugging.` : `\u9ED8\u8BA4 \`fet visual\` \u4F1A\u66F4\u65B0\u6E05\u5355\u3001Playwright \u622A\u56FE\uFF08\u9700 \`--base-url\`\uFF09\u5E76\u505A **layout-only** \u68C0\u67E5\uFF08\u4E0D\u5BF9\u52A8\u6001\u63A5\u53E3\u5185\u5BB9\u505A\u50CF\u7D20\u5BF9\u6BD4\uFF09\u3002\u4EC5\u5728\u8C03\u8BD5\u65F6\u4F7F\u7528 \`--plan\`\u3001\`--capture-only\`\u3001\`--check-layout-only\`\u3002`;
|
|
8320
|
+
return `<!-- FET:MANAGED
|
|
8321
|
+
schemaVersion: 1
|
|
8322
|
+
fetVersion: ${FET_VERSION}
|
|
8323
|
+
generator: cursor-adapter
|
|
8324
|
+
adapterVersion: 1
|
|
8325
|
+
command: ${usage}
|
|
8326
|
+
FET:END -->
|
|
8327
|
+
|
|
8328
|
+
---
|
|
8329
|
+
name: fet-visual
|
|
8330
|
+
description: ${language === "en" ? "Layout-only visual verification for a change" : "change \u7EA7 layout-only \u89C6\u89C9\u9A8C\u6536"}
|
|
8331
|
+
disable-model-invocation: true
|
|
8332
|
+
---
|
|
8333
|
+
|
|
8334
|
+
${renderIdeModelPolicy("visual", language)}
|
|
8335
|
+
|
|
8336
|
+
${languageInstruction(language)}
|
|
8337
|
+
|
|
8338
|
+
\u8BF7\u5728\u7EC8\u7AEF\u4E2D\u6267\u884C\uFF1A
|
|
8339
|
+
|
|
8340
|
+
\`\`\`sh
|
|
8341
|
+
${usage}
|
|
8342
|
+
\`\`\`
|
|
8343
|
+
|
|
8344
|
+
${body}
|
|
6971
8345
|
`;
|
|
6972
8346
|
}
|
|
6973
8347
|
function renderTddTestSkill(command, usage, language) {
|
|
@@ -7007,14 +8381,14 @@ var CursorAdapter = class {
|
|
|
7007
8381
|
adapterVersion = 1;
|
|
7008
8382
|
async detect(projectRoot) {
|
|
7009
8383
|
return {
|
|
7010
|
-
detected: await exists7(
|
|
8384
|
+
detected: await exists7(join31(projectRoot, ".cursor")),
|
|
7011
8385
|
reason: "Cursor adapter is available for any project"
|
|
7012
8386
|
};
|
|
7013
8387
|
}
|
|
7014
8388
|
async planInstall(_projectRoot, language) {
|
|
7015
8389
|
return {
|
|
7016
8390
|
tool: this.tool,
|
|
7017
|
-
files: [...cursorSkillFiles(language), ...cursorRuleFiles(language)].map((file) => ({
|
|
8391
|
+
files: [...cursorSkillFiles(language), ...cursorRuleFiles(language), ...cursorHookFiles()].map((file) => ({
|
|
7018
8392
|
...file,
|
|
7019
8393
|
managed: true
|
|
7020
8394
|
}))
|
|
@@ -7024,8 +8398,14 @@ var CursorAdapter = class {
|
|
|
7024
8398
|
const written = [];
|
|
7025
8399
|
const skipped = [];
|
|
7026
8400
|
for (const file of plan.files) {
|
|
7027
|
-
const target =
|
|
8401
|
+
const target = join31(projectRoot, file.path);
|
|
7028
8402
|
const existing = await readExisting2(target);
|
|
8403
|
+
if (file.path === ".cursor/hooks.json") {
|
|
8404
|
+
await mkdir13(dirname12(target), { recursive: true });
|
|
8405
|
+
await atomicWrite(target, mergeCursorHooksJson(existing, file.content));
|
|
8406
|
+
written.push(file.path);
|
|
8407
|
+
continue;
|
|
8408
|
+
}
|
|
7029
8409
|
if (existing && !existing.includes("FET:MANAGED") && !force) {
|
|
7030
8410
|
throw new FetError({
|
|
7031
8411
|
code: "TOOL_ADAPTER_CONFLICT" /* ToolAdapterConflict */,
|
|
@@ -7037,7 +8417,7 @@ var CursorAdapter = class {
|
|
|
7037
8417
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
7038
8418
|
await createBackup(target);
|
|
7039
8419
|
}
|
|
7040
|
-
await
|
|
8420
|
+
await mkdir13(dirname12(target), { recursive: true });
|
|
7041
8421
|
await atomicWrite(target, file.content);
|
|
7042
8422
|
written.push(file.path);
|
|
7043
8423
|
}
|
|
@@ -7047,14 +8427,16 @@ var CursorAdapter = class {
|
|
|
7047
8427
|
const plan = await this.planInstall(projectRoot);
|
|
7048
8428
|
const checks = [];
|
|
7049
8429
|
for (const file of plan.files) {
|
|
7050
|
-
const target =
|
|
8430
|
+
const target = join31(projectRoot, file.path);
|
|
7051
8431
|
const content = await readExisting2(target);
|
|
7052
|
-
const
|
|
7053
|
-
const
|
|
8432
|
+
const hooksManaged = file.path === ".cursor/hooks.json" && Boolean(content?.includes('"writeBoundary"'));
|
|
8433
|
+
const hookScript = file.path.startsWith(".cursor/hooks/fet-guard-") && file.path.endsWith(".mjs");
|
|
8434
|
+
const managed = hooksManaged || hookScript || Boolean(content?.includes("FET:MANAGED"));
|
|
8435
|
+
const versionMatches = file.path === ".cursor/hooks.json" ? Boolean(content?.includes('"writeBoundary"')) : hookScript ? Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`)) : Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
|
|
7054
8436
|
checks.push({
|
|
7055
8437
|
id: `cursor:${file.path}`,
|
|
7056
8438
|
status: !content ? "warn" : managed && versionMatches ? "pass" : "warn",
|
|
7057
|
-
message: !content ? `${file.path} \u7F3A\u5931` : !managed ? `${file.path} \u5DF2\u5B58\u5728\uFF0C\u4F46\u4E0D\u7531 FET \u7BA1\u7406` : !versionMatches ? `${file.path}
|
|
8439
|
+
message: !content ? `${file.path} \u7F3A\u5931` : !managed ? `${file.path} \u5DF2\u5B58\u5728\uFF0C\u4F46\u4E0D\u7531 FET \u7BA1\u7406` : !versionMatches ? `${file.path} \u7248\u672C\u5DF2\u8FC7\u671F\uFF08\u8BF7\u8FD0\u884C fet init \u66F4\u65B0\uFF09` : `${file.path} \u5DF2\u5B58\u5728\u4E14\u7248\u672C\u5339\u914D`,
|
|
7058
8440
|
suggestedCommand: !content || !managed || !versionMatches ? "fet init" : void 0
|
|
7059
8441
|
});
|
|
7060
8442
|
}
|
|
@@ -7063,14 +8445,14 @@ var CursorAdapter = class {
|
|
|
7063
8445
|
};
|
|
7064
8446
|
async function readExisting2(path) {
|
|
7065
8447
|
try {
|
|
7066
|
-
return await
|
|
8448
|
+
return await readFile23(path, "utf8");
|
|
7067
8449
|
} catch {
|
|
7068
8450
|
return null;
|
|
7069
8451
|
}
|
|
7070
8452
|
}
|
|
7071
8453
|
async function exists7(path) {
|
|
7072
8454
|
try {
|
|
7073
|
-
await
|
|
8455
|
+
await stat14(path);
|
|
7074
8456
|
return true;
|
|
7075
8457
|
} catch {
|
|
7076
8458
|
return false;
|
|
@@ -7082,13 +8464,13 @@ import { execFile as execFile4 } from "child_process";
|
|
|
7082
8464
|
import { promisify as promisify4 } from "util";
|
|
7083
8465
|
|
|
7084
8466
|
// src/openspec/inspector.ts
|
|
7085
|
-
import { readdir as readdir6, stat as
|
|
7086
|
-
import { join as
|
|
8467
|
+
import { readdir as readdir6, stat as stat15 } from "fs/promises";
|
|
8468
|
+
import { join as join32 } from "path";
|
|
7087
8469
|
async function inspectOpenSpecProject(projectRoot) {
|
|
7088
|
-
const openspecPath =
|
|
7089
|
-
const changesPath =
|
|
7090
|
-
const legacyArchivePath =
|
|
7091
|
-
const changesArchivePath =
|
|
8470
|
+
const openspecPath = join32(projectRoot, "openspec");
|
|
8471
|
+
const changesPath = join32(openspecPath, "changes");
|
|
8472
|
+
const legacyArchivePath = join32(openspecPath, "archive");
|
|
8473
|
+
const changesArchivePath = join32(changesPath, "archive");
|
|
7092
8474
|
return {
|
|
7093
8475
|
exists: await exists8(openspecPath),
|
|
7094
8476
|
changes: await listDirectories(changesPath, { exclude: ["archive"] }),
|
|
@@ -7096,13 +8478,13 @@ async function inspectOpenSpecProject(projectRoot) {
|
|
|
7096
8478
|
};
|
|
7097
8479
|
}
|
|
7098
8480
|
async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
7099
|
-
const changePath =
|
|
7100
|
-
const tasksPath =
|
|
7101
|
-
const specsPath =
|
|
8481
|
+
const changePath = join32(projectRoot, "openspec", "changes", changeId);
|
|
8482
|
+
const tasksPath = join32(changePath, "tasks.md");
|
|
8483
|
+
const specsPath = join32(changePath, "specs");
|
|
7102
8484
|
return {
|
|
7103
8485
|
changeId,
|
|
7104
8486
|
exists: await exists8(changePath),
|
|
7105
|
-
hasProposal: await exists8(
|
|
8487
|
+
hasProposal: await exists8(join32(changePath, "proposal.md")),
|
|
7106
8488
|
hasTasks: await exists8(tasksPath),
|
|
7107
8489
|
hasSpecs: await exists8(specsPath),
|
|
7108
8490
|
tasksPath,
|
|
@@ -7120,7 +8502,7 @@ async function listDirectories(path, options = {}) {
|
|
|
7120
8502
|
}
|
|
7121
8503
|
async function exists8(path) {
|
|
7122
8504
|
try {
|
|
7123
|
-
await
|
|
8505
|
+
await stat15(path);
|
|
7124
8506
|
return true;
|
|
7125
8507
|
} catch {
|
|
7126
8508
|
return false;
|
|
@@ -7303,13 +8685,13 @@ function escapeRegExp(value) {
|
|
|
7303
8685
|
}
|
|
7304
8686
|
|
|
7305
8687
|
// src/scanner/routes.ts
|
|
7306
|
-
import { readdir as readdir7, stat as
|
|
7307
|
-
import { join as
|
|
8688
|
+
import { readdir as readdir7, stat as stat16 } from "fs/promises";
|
|
8689
|
+
import { join as join33, relative as relative5, sep } from "path";
|
|
7308
8690
|
async function scanRoutes(projectRoot) {
|
|
7309
8691
|
const candidates = ["src/routes", "src/pages", "app", "pages"];
|
|
7310
8692
|
const routes = [];
|
|
7311
8693
|
for (const candidate of candidates) {
|
|
7312
|
-
const root =
|
|
8694
|
+
const root = join33(projectRoot, candidate);
|
|
7313
8695
|
if (!await exists9(root)) {
|
|
7314
8696
|
continue;
|
|
7315
8697
|
}
|
|
@@ -7337,7 +8719,7 @@ async function listFiles(root) {
|
|
|
7337
8719
|
const entries = await readdir7(root, { withFileTypes: true });
|
|
7338
8720
|
const files = [];
|
|
7339
8721
|
for (const entry of entries) {
|
|
7340
|
-
const path =
|
|
8722
|
+
const path = join33(root, entry.name);
|
|
7341
8723
|
if (entry.isDirectory()) {
|
|
7342
8724
|
files.push(...await listFiles(path));
|
|
7343
8725
|
} else {
|
|
@@ -7348,7 +8730,7 @@ async function listFiles(root) {
|
|
|
7348
8730
|
}
|
|
7349
8731
|
async function exists9(path) {
|
|
7350
8732
|
try {
|
|
7351
|
-
await
|
|
8733
|
+
await stat16(path);
|
|
7352
8734
|
return true;
|
|
7353
8735
|
} catch {
|
|
7354
8736
|
return false;
|
|
@@ -7505,9 +8887,9 @@ async function createCommandContext(command, options) {
|
|
|
7505
8887
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
7506
8888
|
|
|
7507
8889
|
// src/update/check.ts
|
|
7508
|
-
import { mkdir as
|
|
8890
|
+
import { mkdir as mkdir14, readFile as readFile24, writeFile } from "fs/promises";
|
|
7509
8891
|
import { homedir as homedir2 } from "os";
|
|
7510
|
-
import { dirname as
|
|
8892
|
+
import { dirname as dirname13, join as join34 } from "path";
|
|
7511
8893
|
var DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
7512
8894
|
function getFetUpdateCheckMode(env = process.env) {
|
|
7513
8895
|
const value = env.FET_UPDATE_CHECK?.trim().toLowerCase();
|
|
@@ -7580,11 +8962,11 @@ function formatFetUpdateWarning(availability, language) {
|
|
|
7580
8962
|
}
|
|
7581
8963
|
function cachePath() {
|
|
7582
8964
|
const home = process.env.FET_UPDATE_CHECK_CACHE_HOME?.trim() || homedir2();
|
|
7583
|
-
return
|
|
8965
|
+
return join34(home, ".fet", "update-check-cache.json");
|
|
7584
8966
|
}
|
|
7585
8967
|
async function readUpdateCheckCache() {
|
|
7586
8968
|
try {
|
|
7587
|
-
const raw = await
|
|
8969
|
+
const raw = await readFile24(cachePath(), "utf8");
|
|
7588
8970
|
const parsed = JSON.parse(raw);
|
|
7589
8971
|
if (typeof parsed.latestVersion !== "string" || typeof parsed.checkedAt !== "string") {
|
|
7590
8972
|
return null;
|
|
@@ -7600,7 +8982,7 @@ async function readUpdateCheckCache() {
|
|
|
7600
8982
|
}
|
|
7601
8983
|
async function writeUpdateCheckCache(cache) {
|
|
7602
8984
|
const path = cachePath();
|
|
7603
|
-
await
|
|
8985
|
+
await mkdir14(dirname13(path), { recursive: true });
|
|
7604
8986
|
await writeFile(path, `${JSON.stringify(cache, null, 2)}
|
|
7605
8987
|
`, "utf8");
|
|
7606
8988
|
}
|
|
@@ -7691,6 +9073,19 @@ addGlobalOptions(program.command("tdd").description("\u6839\u636E\u89C4\u5212\u4
|
|
|
7691
9073
|
addGlobalOptions(program.command("test").description("\u6309 change TDD \u6E05\u5355\u8FD0\u884C\u5355\u6D4B\u5E76\u8BB0\u5F55\u7EFF\u706F\u72B6\u6001").option("--plan", "\u4EC5\u8F93\u51FA\u5C06\u6267\u884C\u7684\u6D4B\u8BD5\u547D\u4EE4\uFF0C\u4E0D\u8FD0\u884C")).action(
|
|
7692
9074
|
wrap("test", (ctx, options) => testCommand(ctx, { plan: Boolean(options.plan) }))
|
|
7693
9075
|
);
|
|
9076
|
+
addGlobalOptions(
|
|
9077
|
+
program.command("visual").description("layout-only \u89C6\u89C9\u9A8C\u6536\uFF08\u9ED8\u8BA4\uFF1A\u66F4\u65B0\u6E05\u5355 + \u622A\u56FE + \u5E03\u5C40\u68C0\u67E5\uFF09").option("--plan", "\u4EC5\u8F93\u51FA\u6267\u884C\u8BA1\u5212").option("--capture-only", "\u4EC5\u6267\u884C\u622A\u56FE").option("--check-layout-only", "\u4EC5\u6267\u884C\u5E03\u5C40\u68C0\u67E5\uFF08\u4F9D\u8D56\u5DF2\u6709 capture\uFF09").option("--base-url <url>", "\u5E94\u7528 base URL\uFF0C\u7528\u4E8E Playwright \u622A\u56FE")
|
|
9078
|
+
).action(
|
|
9079
|
+
wrap(
|
|
9080
|
+
"visual",
|
|
9081
|
+
(ctx, options) => visualCommand(ctx, {
|
|
9082
|
+
plan: Boolean(options.plan),
|
|
9083
|
+
captureOnly: Boolean(options.captureOnly),
|
|
9084
|
+
checkLayoutOnly: Boolean(options.checkLayoutOnly),
|
|
9085
|
+
baseUrl: options.baseUrl
|
|
9086
|
+
})
|
|
9087
|
+
)
|
|
9088
|
+
);
|
|
7694
9089
|
addGlobalOptions(program.command("verify").description("\u6700\u7EC8\u8D28\u91CF\u9A8C\u8BC1").option("--done", "\u58F0\u660E\u624B\u52A8\u9A8C\u8BC1\u5DF2\u5B8C\u6210").option("--auto", "\u751F\u6210\u6216\u6267\u884C\u81EA\u52A8\u9A8C\u8BC1\u8BA1\u5212")).action(
|
|
7695
9090
|
wrap("verify", verifyCommand)
|
|
7696
9091
|
);
|