@nick848/fet 1.1.6 → 1.1.8
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 +9 -0
- package/README_en.md +9 -0
- package/dist/cli/index.js +1253 -93
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -188,18 +188,18 @@ function toGitNexusState(detection, previous) {
|
|
|
188
188
|
};
|
|
189
189
|
}
|
|
190
190
|
async function inspectGitNexusGraph(projectRoot, env = process.env) {
|
|
191
|
-
const
|
|
192
|
-
const graphPath = join5(projectRoot,
|
|
191
|
+
const relative5 = env.FET_GITNEXUS_GRAPH_PATH?.trim() || DEFAULT_GRAPH_PATH;
|
|
192
|
+
const graphPath = join5(projectRoot, relative5);
|
|
193
193
|
try {
|
|
194
194
|
const info = await stat2(graphPath);
|
|
195
195
|
return {
|
|
196
|
-
graphPath:
|
|
196
|
+
graphPath: relative5,
|
|
197
197
|
graphExists: true,
|
|
198
198
|
lastIndexedAt: info.mtime.toISOString()
|
|
199
199
|
};
|
|
200
200
|
} catch {
|
|
201
201
|
return {
|
|
202
|
-
graphPath:
|
|
202
|
+
graphPath: relative5,
|
|
203
203
|
graphExists: false,
|
|
204
204
|
lastIndexedAt: null
|
|
205
205
|
};
|
|
@@ -2079,6 +2079,20 @@ function renderFetConfig(scan, language = "zh-CN") {
|
|
|
2079
2079
|
test: "warn"
|
|
2080
2080
|
},
|
|
2081
2081
|
workspaces: scan.project.workspaces
|
|
2082
|
+
},
|
|
2083
|
+
figmaGuard: {
|
|
2084
|
+
enabled: true,
|
|
2085
|
+
mode: "require_before_ui",
|
|
2086
|
+
onUncertainty: "stop_and_ask"
|
|
2087
|
+
},
|
|
2088
|
+
uiDisplayContract: {
|
|
2089
|
+
enabled: true
|
|
2090
|
+
},
|
|
2091
|
+
specLanguage: {
|
|
2092
|
+
style: "layered_bilingual",
|
|
2093
|
+
canonical: "en",
|
|
2094
|
+
notesLocale: "zh-CN",
|
|
2095
|
+
maintainZhNotesOnUpdate: true
|
|
2082
2096
|
}
|
|
2083
2097
|
}
|
|
2084
2098
|
});
|
|
@@ -2239,6 +2253,484 @@ fet verify --done --change ${changeId}
|
|
|
2239
2253
|
`;
|
|
2240
2254
|
}
|
|
2241
2255
|
|
|
2256
|
+
// src/templates/figma-guard.ts
|
|
2257
|
+
var FIGMA_URL_PATTERN = /https?:\/\/(?:www\.)?figma\.com\/(?:file|design|proto)\/[^\s)\]"'<>]+/gi;
|
|
2258
|
+
function figmaStopHandoffRelativePath(changeId) {
|
|
2259
|
+
return `openspec/changes/${changeId}/.fet/figma-stop.md`;
|
|
2260
|
+
}
|
|
2261
|
+
function figmaApplyInstructionsRelativePath(changeId) {
|
|
2262
|
+
return `openspec/changes/${changeId}/.fet/figma-apply-instructions.md`;
|
|
2263
|
+
}
|
|
2264
|
+
function renderFigmaRequireBeforeUiBody(language, changeId) {
|
|
2265
|
+
const stopPath = figmaStopHandoffRelativePath(changeId);
|
|
2266
|
+
if (language === "en") {
|
|
2267
|
+
return `## Mandatory before any UI implementation
|
|
2268
|
+
|
|
2269
|
+
Complete these steps **before** writing or editing UI code (components, pages, styles, layout):
|
|
2270
|
+
|
|
2271
|
+
1. Read \`${stopPath}\` for detected Figma links and stop rules.
|
|
2272
|
+
2. Use **Figma MCP/API** (or an approved Figma tool) to read every linked frame/node referenced by this change.
|
|
2273
|
+
3. In your reply, briefly list design facts you confirmed from Figma (frames, colors, typography, spacing, components, states).
|
|
2274
|
+
4. Only then implement UI tasks from \`tasks.md\`.
|
|
2275
|
+
|
|
2276
|
+
## Forbidden
|
|
2277
|
+
|
|
2278
|
+
- Implementing or restyling UI without reading Figma when this change references design links
|
|
2279
|
+
- Filling gaps with "common UI patterns", guessed pixel values, or invented tokens
|
|
2280
|
+
- Marking UI tasks complete while design input is still unclear
|
|
2281
|
+
|
|
2282
|
+
## When to stop and talk to the user
|
|
2283
|
+
|
|
2284
|
+
Follow the stop rules in \`${stopPath}\`. Pause implementation, explain what failed, and ask for a viewable link, screenshots, or explicit permission to infer\u2014do not continue UI work until the user answers or says to continue.`;
|
|
2285
|
+
}
|
|
2286
|
+
return `## \u5B9E\u65BD\u4EFB\u4F55 UI \u4EE3\u7801\u4E4B\u524D\u5FC5\u987B\u5B8C\u6210
|
|
2287
|
+
|
|
2288
|
+
\u5728\u7F16\u5199\u6216\u4FEE\u6539 UI \u4EE3\u7801\uFF08\u7EC4\u4EF6\u3001\u9875\u9762\u3001\u6837\u5F0F\u3001\u5E03\u5C40\uFF09**\u4E4B\u524D**\uFF0C\u6309\u987A\u5E8F\u5B8C\u6210\uFF1A
|
|
2289
|
+
|
|
2290
|
+
1. \u9605\u8BFB \`${stopPath}\` \u4E2D\u7684 Figma \u94FE\u63A5\u4E0E\u505C\u6B62\u89C4\u5219\u3002
|
|
2291
|
+
2. \u4F7F\u7528 **Figma MCP/API**\uFF08\u6216\u5DF2\u914D\u7F6E\u7684 Figma \u5DE5\u5177\uFF09\u8BFB\u53D6\u672C change \u5F15\u7528\u7684\u6BCF\u4E2A\u753B\u677F/\u8282\u70B9\u3002
|
|
2292
|
+
3. \u5728\u56DE\u590D\u4E2D\u7B80\u8981\u5217\u51FA\u5DF2\u4ECE Figma \u786E\u8BA4\u7684\u8BBE\u8BA1\u4E8B\u5B9E\uFF08\u753B\u677F\u3001\u989C\u8272\u3001\u5B57\u53F7\u3001\u95F4\u8DDD\u3001\u7EC4\u4EF6\u3001\u72B6\u6001\u7B49\uFF09\u3002
|
|
2293
|
+
4. \u5B8C\u6210\u4EE5\u4E0A\u6B65\u9AA4\u540E\uFF0C\u518D\u6309 \`tasks.md\` \u5B9E\u65BD UI \u4EFB\u52A1\u3002
|
|
2294
|
+
|
|
2295
|
+
## \u7981\u6B62
|
|
2296
|
+
|
|
2297
|
+
- \u672C change \u5DF2\u5F15\u7528\u8BBE\u8BA1\u7A3F\u65F6\uFF0C\u672A\u8BFB Figma \u5C31\u5B9E\u73B0\u6216\u6539 UI \u6837\u5F0F
|
|
2298
|
+
- \u7528\u300C\u5E38\u89C1 UI \u505A\u6CD5\u300D\u3001\u731C\u6D4B\u7684\u50CF\u7D20\u503C\u6216\u81EA\u521B token \u586B\u8865\u7A7A\u767D
|
|
2299
|
+
- \u8BBE\u8BA1\u8F93\u5165\u4ECD\u4E0D\u6E05\u6670\u5C31\u628A UI \u4EFB\u52A1\u6807\u4E3A\u5B8C\u6210
|
|
2300
|
+
|
|
2301
|
+
## \u4F55\u65F6\u5FC5\u987B\u505C\u4E0B\u5E76\u4E0E\u7528\u6237\u6C9F\u901A
|
|
2302
|
+
|
|
2303
|
+
\u9075\u5B88 \`${stopPath}\` \u4E2D\u7684\u505C\u6B62\u89C4\u5219\u3002\u6682\u505C\u5B9E\u65BD\uFF0C\u8BF4\u660E\u5361\u5728\u54EA\u4E00\u6B65\uFF0C\u5411\u7528\u6237\u7D22\u8981\u53EF\u67E5\u770B\u7684\u94FE\u63A5\u3001\u622A\u56FE\uFF0C\u6216\u660E\u786E\u5141\u8BB8\u6309\u67D0\u89C4\u5219\u63A8\u65AD\uFF1B\u5728\u7528\u6237\u56DE\u7B54\u6216\u660E\u786E\u8868\u793A\u7EE7\u7EED\u4E4B\u524D\uFF0C\u4E0D\u8981\u7EE7\u7EED UI \u5B9E\u73B0\u3002`;
|
|
2304
|
+
}
|
|
2305
|
+
function renderChangeFigmaApplyInstructions(options) {
|
|
2306
|
+
const linkList = options.urls.map((url) => `- ${url}`).join("\n");
|
|
2307
|
+
const title = options.language === "en" ? "Figma apply gate (this change)" : "Figma \u5B9E\u65BD\u95E8\u7981\uFF08\u672C change\uFF09";
|
|
2308
|
+
const intro = options.language === "en" ? "FET detected Figma links in this change. **UI work must follow the design**\u2014read Figma first, do not invent styles. If access or design details fail, stop and coordinate with the user." : "FET \u5728\u672C change \u4E2D\u68C0\u6D4B\u5230 Figma \u94FE\u63A5\u3002**UI \u5FC5\u987B\u6309\u8BBE\u8BA1\u7A3F\u5B9E\u73B0**\u2014\u5148\u8BFB Figma\uFF0C\u7981\u6B62\u81EA\u521B\u6837\u5F0F\uFF1B\u8BBF\u95EE\u5931\u8D25\u6216\u7EC6\u8282\u4E0D\u6E05\u65F6\u505C\u4E0B\u5E76\u4E0E\u7528\u6237\u6C9F\u901A\u3002";
|
|
2309
|
+
return `---
|
|
2310
|
+
schemaVersion: 1
|
|
2311
|
+
fetVersion: ${FET_VERSION}
|
|
2312
|
+
generatedAt: ${options.generatedAt}
|
|
2313
|
+
changeId: ${options.changeId}
|
|
2314
|
+
purpose: figma-apply
|
|
2315
|
+
---
|
|
2316
|
+
|
|
2317
|
+
# ${title}
|
|
2318
|
+
|
|
2319
|
+
${intro}
|
|
2320
|
+
|
|
2321
|
+
## Detected Figma links
|
|
2322
|
+
|
|
2323
|
+
${linkList}
|
|
2324
|
+
|
|
2325
|
+
${renderFigmaRequireBeforeUiBody(options.language, options.changeId)}
|
|
2326
|
+
|
|
2327
|
+
${renderFigmaStopProtocolBody(options.language)}
|
|
2328
|
+
`;
|
|
2329
|
+
}
|
|
2330
|
+
function renderFigmaStopProtocolBody(language) {
|
|
2331
|
+
if (language === "en") {
|
|
2332
|
+
return `## Stop immediately (do not write or change UI code) when
|
|
2333
|
+
|
|
2334
|
+
- Figma MCP/API errors, 403, timeout, or empty node/selection
|
|
2335
|
+
- You cannot resolve the frame or node referenced in the change
|
|
2336
|
+
- Color, typography, spacing, radius, shadow, or layout cannot be determined from the design input
|
|
2337
|
+
- Component instances do not map to an agreed code component and the user has not chosen one
|
|
2338
|
+
- Interaction states (hover, disabled, loading, empty) are missing from the design
|
|
2339
|
+
|
|
2340
|
+
## After stopping, ask the user
|
|
2341
|
+
|
|
2342
|
+
1. What failed (permission, node, frame, token type)
|
|
2343
|
+
2. What you need: **viewable link**, **screenshot + short notes**, or **explicit permission to infer** (which rule)
|
|
2344
|
+
3. Do **not** continue UI implementation until the user clearly says to continue or answers the question
|
|
2345
|
+
|
|
2346
|
+
## While uncertain
|
|
2347
|
+
|
|
2348
|
+
- Do not fill gaps with "common UI patterns" or guessed pixel values
|
|
2349
|
+
- Prefer showing the blocking question over partial implementation`;
|
|
2350
|
+
}
|
|
2351
|
+
return `## \u5FC5\u987B\u7ACB\u5373\u505C\u6B62\uFF08\u4E0D\u5F97\u7EE7\u7EED\u7F16\u5199\u6216\u4FEE\u6539 UI \u4EE3\u7801\uFF09\u5F53
|
|
2352
|
+
|
|
2353
|
+
- Figma MCP/API \u62A5\u9519\u3001403\u3001\u8D85\u65F6\uFF0C\u6216\u8282\u70B9/\u9009\u533A\u4E3A\u7A7A
|
|
2354
|
+
- \u65E0\u6CD5\u89E3\u6790 change \u4E2D\u5F15\u7528\u7684\u753B\u677F\u6216\u8282\u70B9
|
|
2355
|
+
- \u989C\u8272\u3001\u5B57\u53F7\u3001\u95F4\u8DDD\u3001\u5706\u89D2\u3001\u9634\u5F71\u3001\u5E03\u5C40\u65E0\u6CD5\u4ECE\u8BBE\u8BA1\u8F93\u5165\u4E2D\u786E\u5B9A
|
|
2356
|
+
- \u7EC4\u4EF6\u5B9E\u4F8B\u65E0\u6CD5\u5BF9\u5E94\u5230\u5DF2\u7EA6\u5B9A\u7684\u4EE3\u7801\u7EC4\u4EF6\uFF0C\u4E14\u7528\u6237\u672A\u6307\u5B9A
|
|
2357
|
+
- \u4EA4\u4E92\u72B6\u6001\uFF08hover\u3001disabled\u3001loading\u3001\u7A7A\u6001\u7B49\uFF09\u5728\u8BBE\u8BA1\u7A3F\u4E2D\u7F3A\u5931
|
|
2358
|
+
|
|
2359
|
+
## \u505C\u6B62\u540E\u5FC5\u987B\u8BE2\u95EE\u7528\u6237
|
|
2360
|
+
|
|
2361
|
+
1. \u5361\u5728\u54EA\u4E00\u6B65\uFF08\u6743\u9650\u3001\u8282\u70B9\u3001\u753B\u677F\u3001\u54EA\u7C7B token\uFF09
|
|
2362
|
+
2. \u9700\u8981\u7528\u6237\u8865\u5145\uFF1A**\u53EF\u67E5\u770B\u7684\u94FE\u63A5**\u3001**\u622A\u56FE + \u6587\u5B57\u8BF4\u660E**\uFF0C\u6216 **\u660E\u786E\u5141\u8BB8\u6309\u67D0\u89C4\u5219\u63A8\u65AD**
|
|
2363
|
+
3. \u5728\u7528\u6237\u660E\u786E\u8868\u793A\u300C\u7EE7\u7EED\u300D\u6216\u56DE\u7B54\u95EE\u9898\u4E4B\u524D\uFF0C**\u4E0D\u8981**\u7EE7\u7EED\u5B9E\u73B0 UI
|
|
2364
|
+
|
|
2365
|
+
## \u5B58\u5728\u4E0D\u786E\u5B9A\u6027\u65F6
|
|
2366
|
+
|
|
2367
|
+
- \u4E0D\u8981\u7528\u300C\u5E38\u89C1 UI \u505A\u6CD5\u300D\u6216\u731C\u6D4B\u7684\u50CF\u7D20\u503C\u586B\u8865\u7A7A\u767D
|
|
2368
|
+
- \u4F18\u5148\u5411\u7528\u6237\u63D0\u95EE\uFF0C\u800C\u4E0D\u662F\u5148\u5199\u4E00\u7248\u6837\u5F0F\u518D\u6539`;
|
|
2369
|
+
}
|
|
2370
|
+
function renderCursorFigmaStopRule(language) {
|
|
2371
|
+
const description = language === "en" ? "Stop UI work when Figma cannot be read reliably; ask the user before continuing" : "Figma \u7406\u89E3\u5F02\u5E38\u65F6\u505C\u6B62 UI \u5B9E\u73B0\u5E76\u5411\u7528\u6237\u786E\u8BA4\u540E\u518D\u7EE7\u7EED";
|
|
2372
|
+
return `<!-- FET:MANAGED
|
|
2373
|
+
schemaVersion: 1
|
|
2374
|
+
fetVersion: ${FET_VERSION}
|
|
2375
|
+
generator: cursor-adapter
|
|
2376
|
+
adapterVersion: 1
|
|
2377
|
+
FET:END -->
|
|
2378
|
+
|
|
2379
|
+
---
|
|
2380
|
+
description: ${description}
|
|
2381
|
+
alwaysApply: false
|
|
2382
|
+
---
|
|
2383
|
+
|
|
2384
|
+
${language === "en" ? "Apply when the user shares a Figma link, asks to implement from a design file, or uses Figma MCP/tools for UI work." : "\u5728\u7528\u6237\u5206\u4EAB Figma \u94FE\u63A5\u3001\u8981\u6C42\u6309\u8BBE\u8BA1\u7A3F\u5B9E\u73B0 UI\uFF0C\u6216\u4F7F\u7528 Figma MCP/\u5DE5\u5177\u65F6\u9002\u7528\u3002"}
|
|
2385
|
+
|
|
2386
|
+
${renderFigmaStopProtocolBody(language)}
|
|
2387
|
+
|
|
2388
|
+
${language === "en" ? "If this change has `openspec/changes/<change-id>/.fet/figma-apply-instructions.md`, follow it before UI work. Also read `figma-stop.md` in the same folder for links and stop rules." : "\u82E5\u5F53\u524D change \u5B58\u5728 `openspec/changes/<change-id>/.fet/figma-apply-instructions.md`\uFF0C\u5B9E\u65BD UI \u524D\u5FC5\u987B\u9075\u5B88\uFF1B\u540C\u76EE\u5F55\u7684 `figma-stop.md` \u542B\u94FE\u63A5\u5217\u8868\u4E0E\u505C\u6B62\u89C4\u5219\u3002"}
|
|
2389
|
+
`;
|
|
2390
|
+
}
|
|
2391
|
+
function renderCodexFigmaStopGuide(language) {
|
|
2392
|
+
return `<!-- FET:MANAGED
|
|
2393
|
+
schemaVersion: 1
|
|
2394
|
+
fetVersion: ${FET_VERSION}
|
|
2395
|
+
generator: codex-adapter
|
|
2396
|
+
adapterVersion: 1
|
|
2397
|
+
FET:END -->
|
|
2398
|
+
|
|
2399
|
+
# Figma stop protocol (Codex)
|
|
2400
|
+
|
|
2401
|
+
${renderFigmaStopProtocolBody(language)}
|
|
2402
|
+
`;
|
|
2403
|
+
}
|
|
2404
|
+
function renderChangeFigmaStopHandoff(options) {
|
|
2405
|
+
const linkList = options.urls.length ? options.urls.map((url) => `- ${url}`).join("\n") : options.language === "en" ? "- (none detected in change artifacts; user may still reference Figma in chat)" : "- \uFF08change \u4EA7\u7269\u4E2D\u672A\u68C0\u6D4B\u5230\uFF1B\u7528\u6237\u4ECD\u53EF\u80FD\u5728\u5BF9\u8BDD\u4E2D\u63D0\u4F9B Figma\uFF09";
|
|
2406
|
+
const sourceList = options.sources.length ? options.sources.map((source) => `- ${source}`).join("\n") : options.language === "en" ? "- n/a" : "- \u65E0";
|
|
2407
|
+
const title = options.language === "en" ? "Figma guard (this change)" : "Figma \u5B88\u536B\uFF08\u672C change\uFF09";
|
|
2408
|
+
const intro = options.language === "en" ? "FET detected Figma links in this change. When design input is unclear, **stop** and let the user decide whether to continue or clarify." : "FET \u5728\u672C change \u4E2D\u68C0\u6D4B\u5230 Figma \u94FE\u63A5\u3002\u8BBE\u8BA1\u8F93\u5165\u4E0D\u6E05\u6670\u65F6\uFF0C**\u505C\u6B62**\u5F53\u524D\u64CD\u4F5C\uFF0C\u7531\u7528\u6237\u51B3\u5B9A\u662F\u7EE7\u7EED\u8FD8\u662F\u8865\u5145\u8BF4\u660E\u3002";
|
|
2409
|
+
return `---
|
|
2410
|
+
schemaVersion: 1
|
|
2411
|
+
fetVersion: ${FET_VERSION}
|
|
2412
|
+
generatedAt: ${options.generatedAt}
|
|
2413
|
+
changeId: ${options.changeId}
|
|
2414
|
+
purpose: figma-stop
|
|
2415
|
+
---
|
|
2416
|
+
|
|
2417
|
+
# ${title}
|
|
2418
|
+
|
|
2419
|
+
${intro}
|
|
2420
|
+
|
|
2421
|
+
## Detected Figma links
|
|
2422
|
+
|
|
2423
|
+
${linkList}
|
|
2424
|
+
|
|
2425
|
+
## Sources
|
|
2426
|
+
|
|
2427
|
+
${sourceList}
|
|
2428
|
+
|
|
2429
|
+
${renderFigmaStopProtocolBody(options.language)}
|
|
2430
|
+
`;
|
|
2431
|
+
}
|
|
2432
|
+
function renderFigmaStopNextStep(changeId, language) {
|
|
2433
|
+
const path = figmaStopHandoffRelativePath(changeId);
|
|
2434
|
+
return language === "en" ? `Before UI implementation, read ${path}. If Figma access fails or design details are unclear, stop and ask the user\u2014do not guess styles.` : `\u5B9E\u65BD UI \u524D\u9605\u8BFB ${path}\u3002Figma \u8BBF\u95EE\u5931\u8D25\u6216\u8BBE\u8BA1\u7EC6\u8282\u4E0D\u660E\u786E\u65F6\u7ACB\u5373\u505C\u6B62\u5E76\u5411\u7528\u6237\u63D0\u95EE\uFF0C\u4E0D\u8981\u731C\u6D4B\u6837\u5F0F\u3002`;
|
|
2435
|
+
}
|
|
2436
|
+
function renderFigmaApplyNextSteps(changeId, language, mode) {
|
|
2437
|
+
const applyPath = figmaApplyInstructionsRelativePath(changeId);
|
|
2438
|
+
const stopPath = figmaStopHandoffRelativePath(changeId);
|
|
2439
|
+
if (mode === "require_before_ui") {
|
|
2440
|
+
return language === "en" ? [
|
|
2441
|
+
`Before any UI task: read and follow ${applyPath} (mandatory). Use Figma MCP/API for every linked frame\u2014do not invent styles.`,
|
|
2442
|
+
`If Figma access fails or design details are unclear, stop per ${stopPath} and ask the user before continuing.`,
|
|
2443
|
+
`After Figma is confirmed, read openspec/changes/${changeId}/tasks.md and implement pending tasks.`
|
|
2444
|
+
] : [
|
|
2445
|
+
`\u5B9E\u65BD\u4EFB\u4F55 UI \u4EFB\u52A1\u524D\uFF1A\u5FC5\u987B\u9605\u8BFB\u5E76\u9075\u5B88 ${applyPath}\uFF1B\u7528 Figma MCP/API \u8BFB\u53D6\u6BCF\u4E2A\u94FE\u63A5\u7684\u753B\u677F\uFF0C\u7981\u6B62\u81EA\u521B\u6837\u5F0F\u3002`,
|
|
2446
|
+
`Figma \u8BBF\u95EE\u5931\u8D25\u6216\u8BBE\u8BA1\u7EC6\u8282\u4E0D\u6E05\uFF1A\u6309 ${stopPath} \u7ACB\u5373\u505C\u6B62\u5E76\u5411\u7528\u6237\u63D0\u95EE\uFF0C\u786E\u8BA4\u540E\u518D\u7EE7\u7EED\u3002`,
|
|
2447
|
+
`\u8BBE\u8BA1\u786E\u8BA4\u540E\uFF0C\u518D\u9605\u8BFB openspec/changes/${changeId}/tasks.md \u5E76\u5B9E\u65BD\u5F85\u529E\u4EFB\u52A1\u3002`
|
|
2448
|
+
];
|
|
2449
|
+
}
|
|
2450
|
+
return [renderFigmaStopNextStep(changeId, language)];
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
// src/templates/ui-display-contract.ts
|
|
2454
|
+
import { stringify as stringify2 } from "yaml";
|
|
2455
|
+
function uiDisplayContractRelativePath(changeId) {
|
|
2456
|
+
return `openspec/changes/${changeId}/.fet/ui-display-contract.yaml`;
|
|
2457
|
+
}
|
|
2458
|
+
function uiFieldApplyInstructionsRelativePath(changeId) {
|
|
2459
|
+
return `openspec/changes/${changeId}/.fet/ui-field-apply-instructions.md`;
|
|
2460
|
+
}
|
|
2461
|
+
function renderUiDisplayContractYaml(doc) {
|
|
2462
|
+
return stringify2(doc);
|
|
2463
|
+
}
|
|
2464
|
+
function renderUiFieldApplyInstructions(options) {
|
|
2465
|
+
const title = options.language === "en" ? "UI field apply gate (this change)" : "UI \u5B57\u6BB5\u5B9E\u65BD\u95E8\u7981\uFF08\u672C change\uFF09";
|
|
2466
|
+
const intro = options.language === "en" ? "FET generated a **UI display contract** draft. API docs are a superset; **only fields listed under `displayFields` may appear in the UI**. Figma defines what is visible; the contract records the binding." : "FET \u5DF2\u751F\u6210 **UI \u5C55\u793A\u5951\u7EA6** \u8349\u6848\u3002\u63A5\u53E3\u6587\u6863\u662F\u8D85\u96C6\uFF1B**\u53EA\u6709 `displayFields` \u4E2D\u7684\u5B57\u6BB5\u5141\u8BB8\u51FA\u73B0\u5728\u754C\u9762\u4E0A**\u3002Figma \u51B3\u5B9A\u53EF\u89C1\u6027\uFF1B\u5951\u7EA6\u8BB0\u5F55\u7ED1\u5B9A\u5173\u7CFB\u3002";
|
|
2467
|
+
const steps = options.language === "en" ? `## Before binding API data to UI
|
|
2468
|
+
|
|
2469
|
+
1. Read and update \`${options.contractPath}\` (confirm or fill \`displayFields\` per screen).
|
|
2470
|
+
2. ${options.hasFigma ? "Use Figma MCP/API to list visible labels, columns, and form fields; map them to API field names in `displayFields`." : "Infer visible fields from design artifacts in this change; document them in `displayFields`."}
|
|
2471
|
+
3. Move API fields that exist in docs but are **not** on the design into \`omittedFromUi\` (or \`hiddenButUsed\` if needed for logic without rendering).
|
|
2472
|
+
4. Resolve every entry in \`needsReview\` with the user before implementation.
|
|
2473
|
+
|
|
2474
|
+
## Forbidden
|
|
2475
|
+
|
|
2476
|
+
- Rendering every property from an API response type because it appears in OpenAPI/Swagger
|
|
2477
|
+
- Using \`Object.keys(data)\`, schema iteration, or generic "show all fields" table/form generators for user-facing UI
|
|
2478
|
+
- Adding columns or form inputs for fields not in \`displayFields\` unless the user explicitly expands the contract
|
|
2479
|
+
|
|
2480
|
+
## Data vs presentation
|
|
2481
|
+
|
|
2482
|
+
- **Data layer**: types, fetch, normalize may use the full API model.
|
|
2483
|
+
- **Presentation layer**: bind only \`displayFields\`; never surface \`omittedFromUi\` fields in lists, forms, cards, or detail views.` : `## \u5C06 API \u6570\u636E\u7ED1\u5B9A\u5230 UI \u4E4B\u524D
|
|
2484
|
+
|
|
2485
|
+
1. \u9605\u8BFB\u5E76\u66F4\u65B0 \`${options.contractPath}\`\uFF08\u6309\u5C4F\u786E\u8BA4\u6216\u586B\u5199 \`displayFields\`\uFF09\u3002
|
|
2486
|
+
2. ${options.hasFigma ? "\u7528 Figma MCP/API \u5217\u51FA\u53EF\u89C1\u6587\u6848\u3001\u8868\u683C\u5217\u3001\u8868\u5355\u9879\uFF1B\u6620\u5C04\u5230 API \u5B57\u6BB5\u540D\u5E76\u5199\u5165 `displayFields`\u3002" : "\u4ECE\u672C change \u7684\u8BBE\u8BA1\u4EA7\u7269\u63A8\u65AD\u53EF\u89C1\u5B57\u6BB5\uFF0C\u5199\u5165 `displayFields`\u3002"}
|
|
2487
|
+
3. \u63A5\u53E3\u6587\u6863\u6709\u3001\u8BBE\u8BA1\u7A3F**\u6CA1\u6709**\u7684\u5B57\u6BB5\u653E\u5165 \`omittedFromUi\`\uFF08\u4EC5\u903B\u8F91\u4F7F\u7528\u4E14\u4E0D\u53EF\u89C1\u5219\u653E\u5165 \`hiddenButUsed\`\uFF09\u3002
|
|
2488
|
+
4. \`needsReview\` \u4E2D\u6BCF\u4E00\u9879\u987B\u5728\u5B9E\u65BD\u524D\u4E0E\u7528\u6237\u786E\u8BA4\u3002
|
|
2489
|
+
|
|
2490
|
+
## \u7981\u6B62
|
|
2491
|
+
|
|
2492
|
+
- \u56E0 OpenAPI/Swagger \u91CC\u6709\u5B57\u6BB5\u5C31\u5728\u9875\u9762\u4E0A\u5168\u90E8\u5C55\u793A
|
|
2493
|
+
- \u5BF9\u7528\u6237\u53EF\u89C1 UI \u4F7F\u7528 \`Object.keys(data)\`\u3001\u904D\u5386 schema \u6216\u300C\u81EA\u52A8\u5C55\u793A\u5168\u90E8\u5B57\u6BB5\u300D\u7684\u8868\u683C/\u8868\u5355
|
|
2494
|
+
- \u5728 \`displayFields\` \u672A\u5217\u5165\u65F6\u65B0\u589E\u5217\u3001\u8868\u5355\u9879\uFF08\u9664\u975E\u7528\u6237\u660E\u786E\u6269\u5C55\u5951\u7EA6\uFF09
|
|
2495
|
+
|
|
2496
|
+
## \u6570\u636E\u5C42\u4E0E\u5C55\u793A\u5C42
|
|
2497
|
+
|
|
2498
|
+
- **\u6570\u636E\u5C42**\uFF1A\u7C7B\u578B\u3001\u8BF7\u6C42\u3001normalize \u53EF\u4F7F\u7528\u5B8C\u6574 API \u6A21\u578B\u3002
|
|
2499
|
+
- **\u5C55\u793A\u5C42**\uFF1A\u53EA\u7ED1\u5B9A \`displayFields\`\uFF1B\`omittedFromUi\` \u4E2D\u7684\u5B57\u6BB5\u4E0D\u5F97\u51FA\u73B0\u5728\u5217\u8868\u3001\u8868\u5355\u3001\u5361\u7247\u3001\u8BE6\u60C5\u7B49\u53EF\u89C1\u533A\u57DF\u3002`;
|
|
2500
|
+
return `---
|
|
2501
|
+
schemaVersion: 1
|
|
2502
|
+
fetVersion: ${FET_VERSION}
|
|
2503
|
+
generatedAt: ${options.generatedAt}
|
|
2504
|
+
changeId: ${options.changeId}
|
|
2505
|
+
purpose: ui-field-apply
|
|
2506
|
+
---
|
|
2507
|
+
|
|
2508
|
+
# ${title}
|
|
2509
|
+
|
|
2510
|
+
${intro}
|
|
2511
|
+
|
|
2512
|
+
${steps}
|
|
2513
|
+
`;
|
|
2514
|
+
}
|
|
2515
|
+
function renderUiDisplayContractApplyNextSteps(changeId, language) {
|
|
2516
|
+
const contractPath = uiDisplayContractRelativePath(changeId);
|
|
2517
|
+
const applyPath = uiFieldApplyInstructionsRelativePath(changeId);
|
|
2518
|
+
if (language === "en") {
|
|
2519
|
+
return [
|
|
2520
|
+
`Read and confirm ${contractPath} before UI that binds API data (update displayFields from Figma; move extras to omittedFromUi).`,
|
|
2521
|
+
`Follow ${applyPath}: API schema is not a UI checklist\u2014do not render undocumented fields.`,
|
|
2522
|
+
`After the contract is confirmed, implement tasks from openspec/changes/${changeId}/tasks.md.`
|
|
2523
|
+
];
|
|
2524
|
+
}
|
|
2525
|
+
return [
|
|
2526
|
+
`\u5728\u7ED1\u5B9A API \u7684 UI \u5B9E\u65BD\u524D\uFF1A\u9605\u8BFB\u5E76\u786E\u8BA4 ${contractPath}\uFF08\u6309 Figma \u586B\u5199 displayFields\uFF1B\u591A\u4F59\u5B57\u6BB5\u653E\u5165 omittedFromUi\uFF09\u3002`,
|
|
2527
|
+
`\u9075\u5B88 ${applyPath}\uFF1A\u63A5\u53E3\u6587\u6863\u4E0D\u662F UI \u6E05\u5355\uFF0C\u7981\u6B62\u5C55\u793A\u5951\u7EA6\u672A\u5217\u51FA\u7684\u5B57\u6BB5\u3002`,
|
|
2528
|
+
`\u5951\u7EA6\u786E\u8BA4\u540E\uFF0C\u518D\u5B9E\u65BD openspec/changes/${changeId}/tasks.md \u4E2D\u7684\u4EFB\u52A1\u3002`
|
|
2529
|
+
];
|
|
2530
|
+
}
|
|
2531
|
+
function renderUiDisplayContractGuardrail(language) {
|
|
2532
|
+
if (language === "en") {
|
|
2533
|
+
return "- When a change has API docs and UI (especially with Figma), read `openspec/changes/<change-id>/.fet/ui-display-contract.yaml`. In `specs/**/spec.md`, add the UI display contract Requirement (see planning guide); API fields not in `displayFields` MUST NOT be rendered.";
|
|
2534
|
+
}
|
|
2535
|
+
return "- change \u542B\u63A5\u53E3\u6587\u6863\u4E14\u6D89\u53CA UI\uFF08\u5C24\u5176\u6709 Figma\uFF09\u65F6\uFF0C\u9605\u8BFB `openspec/changes/<change-id>/.fet/ui-display-contract.yaml`\uFF1B\u5728 `specs/**/spec.md` \u4E2D\u5199\u5165 UI \u5C55\u793A\u5951\u7EA6 Requirement\uFF08\u89C1\u89C4\u5212\u6307\u5F15\uFF09\uFF1B\u672A\u5217\u5165 `displayFields` \u7684\u63A5\u53E3\u5B57\u6BB5\u7981\u6B62\u51FA\u73B0\u5728\u754C\u9762\u4E0A\u3002";
|
|
2536
|
+
}
|
|
2537
|
+
function renderPlanningArtifactUiContractBlock(language, changeId) {
|
|
2538
|
+
const contractPath = uiDisplayContractRelativePath(changeId);
|
|
2539
|
+
if (language === "en") {
|
|
2540
|
+
return `## UI display contract (when API + UI)
|
|
2541
|
+
|
|
2542
|
+
If this change renders API-backed UI (especially with Figma links), \`${contractPath}\` may exist or will be generated on \`fet apply\`. When writing \`specs/<capability>/spec.md\`, **add** this requirement (adjust screen ids as needed):
|
|
2543
|
+
|
|
2544
|
+
\`\`\`markdown
|
|
2545
|
+
### Requirement: UI displays only contracted fields
|
|
2546
|
+
<!-- \u4E2D\u6587\uFF1A\u754C\u9762\u53EA\u5C55\u793A ui-display-contract.yaml \u4E2D displayFields \u5217\u51FA\u7684\u5B57\u6BB5\uFF1B\u63A5\u53E3\u6587\u6863\u5176\u4F59\u5B57\u6BB5\u4EC5\u7528\u4E8E\u7C7B\u578B/\u6570\u636E\u5C42\uFF0C\u4E0D\u5F97\u51FA\u73B0\u5728\u5217\u8868\u3001\u8868\u5355\u3001\u8BE6\u60C5\u7B49\u53EF\u89C1 UI -->
|
|
2547
|
+
|
|
2548
|
+
The UI SHALL only render fields listed under \`displayFields\` for each screen in \`${contractPath}\`.
|
|
2549
|
+
Fields documented in API/OpenAPI but absent from the design MUST be listed under \`omittedFromUi\` and MUST NOT appear in user-visible UI.
|
|
2550
|
+
The system MAY use \`hiddenButUsed\` fields in logic without displaying them.
|
|
2551
|
+
On conflict between API schema completeness and Figma/contract, **Figma + ui-display-contract win** for presentation.
|
|
2552
|
+
\`\`\`
|
|
2553
|
+
|
|
2554
|
+
Update \`displayFields\` / \`omittedFromUi\` in the contract in the **same edit** when requirements change.`;
|
|
2555
|
+
}
|
|
2556
|
+
return `## UI \u5C55\u793A\u5951\u7EA6\uFF08\u63A5\u53E3 + UI \u65F6\uFF09
|
|
2557
|
+
|
|
2558
|
+
\u82E5\u672C change \u5B9E\u65BD\u7ED1\u5B9A\u63A5\u53E3\u7684 UI\uFF08\u5C24\u5176\u542B Figma \u94FE\u63A5\uFF09\uFF0C\`${contractPath}\` \u53EF\u80FD\u5DF2\u5B58\u5728\u6216\u5728 \`fet apply\` \u65F6\u751F\u6210\u3002\u7F16\u5199 \`specs/<capability>/spec.md\` \u65F6**\u5FC5\u987B\u52A0\u5165**\u5982\u4E0B Requirement\uFF08\u6309\u5B9E\u9645 screen \u8C03\u6574\uFF09\uFF1A
|
|
2559
|
+
|
|
2560
|
+
\`\`\`markdown
|
|
2561
|
+
### Requirement: UI displays only contracted fields
|
|
2562
|
+
<!-- \u4E2D\u6587\uFF1A\u754C\u9762\u53EA\u5C55\u793A ui-display-contract.yaml \u4E2D displayFields \u5217\u51FA\u7684\u5B57\u6BB5\uFF1B\u63A5\u53E3\u6587\u6863\u5176\u4F59\u5B57\u6BB5\u4EC5\u7528\u4E8E\u7C7B\u578B/\u6570\u636E\u5C42\uFF0C\u4E0D\u5F97\u51FA\u73B0\u5728\u5217\u8868\u3001\u8868\u5355\u3001\u8BE6\u60C5\u7B49\u53EF\u89C1 UI -->
|
|
2563
|
+
|
|
2564
|
+
The UI SHALL only render fields listed under \`displayFields\` for each screen in \`${contractPath}\`.
|
|
2565
|
+
Fields documented in API/OpenAPI but absent from the design MUST be listed under \`omittedFromUi\` and MUST NOT appear in user-visible UI.
|
|
2566
|
+
The system MAY use \`hiddenButUsed\` fields in logic without displaying them.
|
|
2567
|
+
On conflict between API schema completeness and Figma/contract, **Figma + ui-display-contract win** for presentation.
|
|
2568
|
+
\`\`\`
|
|
2569
|
+
|
|
2570
|
+
\u4FEE\u6539 Requirement \u65F6\uFF0C**\u540C\u4E00\u6B21\u7F16\u8F91**\u987B\u540C\u6B65\u66F4\u65B0\u5951\u7EA6\u4E2D\u7684 \`displayFields\` / \`omittedFromUi\`\u3002`;
|
|
2571
|
+
}
|
|
2572
|
+
function renderCursorUiDisplayContractRule(language) {
|
|
2573
|
+
const description = language === "en" ? "UI display contract: API schemas are not UI checklists; only displayFields may render" : "UI \u5C55\u793A\u5951\u7EA6\uFF1A\u63A5\u53E3\u6587\u6863\u4E0D\u662F UI \u6E05\u5355\uFF0C\u4EC5 displayFields \u53EF\u5C55\u793A";
|
|
2574
|
+
return `<!-- FET:MANAGED
|
|
2575
|
+
schemaVersion: 1
|
|
2576
|
+
fetVersion: ${FET_VERSION}
|
|
2577
|
+
generator: cursor-adapter
|
|
2578
|
+
adapterVersion: 1
|
|
2579
|
+
FET:END -->
|
|
2580
|
+
|
|
2581
|
+
---
|
|
2582
|
+
description: ${description}
|
|
2583
|
+
alwaysApply: false
|
|
2584
|
+
---
|
|
2585
|
+
|
|
2586
|
+
${language === "en" ? "Apply when implementing UI that binds API responses, or when `openspec/changes/<change-id>/.fet/ui-display-contract.yaml` exists." : "\u5728\u5B9E\u65BD\u7ED1\u5B9A\u63A5\u53E3\u54CD\u5E94\u7684 UI \u65F6\uFF0C\u6216\u5B58\u5728 `openspec/changes/<change-id>/.fet/ui-display-contract.yaml` \u65F6\u9002\u7528\u3002"}
|
|
2587
|
+
|
|
2588
|
+
${renderUiDisplayContractGuardrail(language)}
|
|
2589
|
+
|
|
2590
|
+
${language === "en" ? "Also read `ui-field-apply-instructions.md` in the same `.fet/` folder before UI work." : "\u5B9E\u65BD UI \u524D\u540C\u65F6\u9605\u8BFB\u540C\u76EE\u5F55 `.fet/ui-field-apply-instructions.md`\u3002"}
|
|
2591
|
+
`;
|
|
2592
|
+
}
|
|
2593
|
+
function renderCodexUiDisplayContractGuide(language) {
|
|
2594
|
+
return `<!-- FET:MANAGED
|
|
2595
|
+
schemaVersion: 1
|
|
2596
|
+
fetVersion: ${FET_VERSION}
|
|
2597
|
+
generator: codex-adapter
|
|
2598
|
+
adapterVersion: 1
|
|
2599
|
+
FET:END -->
|
|
2600
|
+
|
|
2601
|
+
# UI display contract (Codex)
|
|
2602
|
+
|
|
2603
|
+
${renderUiDisplayContractGuardrail(language)}
|
|
2604
|
+
|
|
2605
|
+
${language === "en" ? "Read `openspec/changes/<change-id>/.fet/ui-display-contract.yaml` and `ui-field-apply-instructions.md` when applying UI tasks that use API data." : "\u5BF9\u4F7F\u7528\u63A5\u53E3\u6570\u636E\u7684 UI \u4EFB\u52A1\uFF0C\u9605\u8BFB `openspec/changes/<change-id>/.fet/ui-display-contract.yaml` \u4E0E `ui-field-apply-instructions.md`\u3002"}
|
|
2606
|
+
`;
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
// src/templates/spec-language.ts
|
|
2610
|
+
function renderSpecArtifactGuardrail(language) {
|
|
2611
|
+
if (language === "en") {
|
|
2612
|
+
return "- When creating or editing `openspec/changes/<change-id>/specs/**/spec.md` (or merging into `openspec/specs/**/spec.md`), keep English Requirements/Scenario headings and normative text; add or update the paired `<!-- \u4E2D\u6587\uFF1A... -->` note on the same edit\u2014never leave stale Chinese notes.";
|
|
2613
|
+
}
|
|
2614
|
+
return "- \u521B\u5EFA\u6216\u4FEE\u6539 `openspec/changes/<change-id>/specs/**/spec.md`\uFF08\u6216\u5408\u5E76\u5230 `openspec/specs/**/spec.md`\uFF09\u65F6\uFF1ARequirements/Scenario \u6807\u9898\u4E0E normative \u53E5\u4FDD\u6301\u82F1\u6587\uFF1B**\u540C\u4E00\u6B21\u7F16\u8F91**\u5FC5\u987B\u65B0\u589E\u6216\u66F4\u65B0\u5BF9\u5E94\u7684 `<!-- \u4E2D\u6587\uFF1A... -->` \u8BF4\u660E\uFF0C\u7981\u6B62\u7559\u4E0B\u8FC7\u65F6\u4E2D\u6587\u6CE8\u91CA\u3002";
|
|
2615
|
+
}
|
|
2616
|
+
function renderSpecLanguagePolicyBody(language) {
|
|
2617
|
+
if (language === "en") {
|
|
2618
|
+
return `## Layered bilingual spec (canonical English + Chinese notes)
|
|
2619
|
+
|
|
2620
|
+
This project uses **layered_bilingual** OpenSpec specs (see \`fet.specLanguage\` in \`openspec/config.yaml\`).
|
|
2621
|
+
|
|
2622
|
+
### Structure
|
|
2623
|
+
|
|
2624
|
+
- **English (canonical)**: Keep OpenSpec section titles (\`## Requirements\`, \`### Requirement:\`, \`#### Scenario:\`) and normative sentences (SHALL/MUST, acceptance criteria) in English.
|
|
2625
|
+
- **Chinese (human notes)**: Immediately after each \`### Requirement:\` line (before the English body), add one HTML comment:
|
|
2626
|
+
|
|
2627
|
+
\`\`\`markdown
|
|
2628
|
+
### Requirement: User can export report
|
|
2629
|
+
<!-- \u4E2D\u6587\uFF1A\u767B\u5F55\u7528\u6237\u53EF\u5728\u62A5\u8868\u9875\u5BFC\u51FA PDF\uFF1B\u9700\u5177\u5907 export \u6743\u9650\uFF1B\u5931\u8D25\u65F6\u63D0\u793A\u539F\u56E0 -->
|
|
2630
|
+
|
|
2631
|
+
The system SHALL ...
|
|
2632
|
+
\`\`\`
|
|
2633
|
+
|
|
2634
|
+
Optionally add \`<!-- \u4E2D\u6587\uFF1A... -->\` after \`#### Scenario:\` when the scenario needs extra business context.
|
|
2635
|
+
|
|
2636
|
+
### When creating or updating specs
|
|
2637
|
+
|
|
2638
|
+
- **Same edit rule**: Any change to English normative text MUST update the paired Chinese comment in the **same** commit/edit session.
|
|
2639
|
+
- **Do not** remove Chinese notes when refactoring English unless the requirement was removed.
|
|
2640
|
+
- **Do not** let Chinese notes contradict English; if they disagree, fix both or ask the user. **On conflict, English Requirements win.**
|
|
2641
|
+
- **proposal.md** / **design.md** may be Chinese; **tasks.md** may be Chinese with English identifiers.
|
|
2642
|
+
|
|
2643
|
+
### Applies to
|
|
2644
|
+
|
|
2645
|
+
- \`fet propose\`, \`fet continue\`, \`fet ff\` artifact writes
|
|
2646
|
+
- Manual edits to spec files in chat
|
|
2647
|
+
- \`fet sync\` merges into \`openspec/specs/**/spec.md\` (preserve or refresh Chinese notes for touched requirements)`;
|
|
2648
|
+
}
|
|
2649
|
+
return `## \u5206\u5C42\u53CC\u8BED spec\uFF08\u82F1\u6587\u89C4\u8303 + \u4E2D\u6587\u8BF4\u660E\uFF09
|
|
2650
|
+
|
|
2651
|
+
\u672C\u9879\u76EE OpenSpec spec \u91C7\u7528 **layered_bilingual**\uFF08\u89C1 \`openspec/config.yaml\` \u7684 \`fet.specLanguage\`\uFF09\u3002
|
|
2652
|
+
|
|
2653
|
+
### \u7ED3\u6784
|
|
2654
|
+
|
|
2655
|
+
- **\u82F1\u6587\uFF08\u6743\u5A01\uFF09**\uFF1A\u4FDD\u7559 OpenSpec \u7AE0\u8282\u6807\u9898\uFF08\`## Requirements\`\u3001\`### Requirement:\`\u3001\`#### Scenario:\`\uFF09\u4E0E normative \u8BED\u53E5\uFF08SHALL/MUST\u3001\u9A8C\u6536\u6761\u4EF6\uFF09\u3002
|
|
2656
|
+
- **\u4E2D\u6587\uFF08\u7ED9\u4EBA\u8BFB\uFF09**\uFF1A\u5728\u6BCF\u4E2A \`### Requirement:\` \u6807\u9898\u884C\u4E4B\u540E\u3001\u82F1\u6587\u6B63\u6587\u4E4B\u524D\uFF0C\u6DFB\u52A0\u4E00\u6761 HTML \u6CE8\u91CA\uFF1A
|
|
2657
|
+
|
|
2658
|
+
\`\`\`markdown
|
|
2659
|
+
### Requirement: User can export report
|
|
2660
|
+
<!-- \u4E2D\u6587\uFF1A\u767B\u5F55\u7528\u6237\u53EF\u5728\u62A5\u8868\u9875\u5BFC\u51FA PDF\uFF1B\u9700\u5177\u5907 export \u6743\u9650\uFF1B\u5931\u8D25\u65F6\u63D0\u793A\u539F\u56E0 -->
|
|
2661
|
+
|
|
2662
|
+
The system SHALL ...
|
|
2663
|
+
\`\`\`
|
|
2664
|
+
|
|
2665
|
+
\u82E5\u573A\u666F\u9700\u8981\u8865\u5145\u4E1A\u52A1\u8BED\u5883\uFF0C\u53EF\u5728 \`#### Scenario:\` \u540E\u540C\u6837\u6DFB\u52A0 \`<!-- \u4E2D\u6587\uFF1A... -->\`\u3002
|
|
2666
|
+
|
|
2667
|
+
### \u521B\u5EFA\u6216\u66F4\u65B0 spec \u65F6\uFF08\u542B LLM \u6539\u6587\u6863\uFF09
|
|
2668
|
+
|
|
2669
|
+
- **\u540C\u6B21\u7F16\u8F91\u539F\u5219**\uFF1A\u4FEE\u6539\u82F1\u6587\u89C4\u8303\u53E5\u65F6\uFF0C**\u5FC5\u987B\u5728\u540C\u4E00\u6B21\u7F16\u8F91\u4E2D**\u540C\u6B65\u66F4\u65B0\u5BF9\u5E94\u7684\u4E2D\u6587\u6CE8\u91CA\uFF0C\u4E0D\u5F97\u9057\u7559\u8FC7\u65F6\u8BF4\u660E\u3002
|
|
2670
|
+
- \u91CD\u6784\u82F1\u6587\u65F6\u4E0D\u8981\u65E0\u6545\u5220\u9664\u4E2D\u6587\u6CE8\u91CA\uFF1B\u82E5\u5220\u9664 Requirement\uFF0C\u4E00\u5E76\u5220\u9664\u5176\u6CE8\u91CA\u3002
|
|
2671
|
+
- \u4E2D\u6587\u8BF4\u660E\u4E0D\u5F97\u4E0E\u82F1\u6587\u77DB\u76FE\uFF1B\u82E5\u6709\u51B2\u7A81\uFF0C\u540C\u65F6\u4FEE\u6B63\u6216\u8BE2\u95EE\u7528\u6237\u3002**\u51B2\u7A81\u65F6\u4EE5\u82F1\u6587 Requirements \u4E3A\u51C6\u3002**
|
|
2672
|
+
- **proposal.md** / **design.md** \u53EF\u7528\u4E2D\u6587\uFF1B**tasks.md** \u53EF\u7528\u4E2D\u6587\u5E76\u4FDD\u7559\u82F1\u6587\u6807\u8BC6\u7B26\u3002
|
|
2673
|
+
|
|
2674
|
+
### \u9002\u7528\u573A\u666F
|
|
2675
|
+
|
|
2676
|
+
- \`fet propose\`\u3001\`fet continue\`\u3001\`fet ff\` \u5199\u5165\u7684\u4EA7\u7269
|
|
2677
|
+
- \u5BF9\u8BDD\u4E2D\u624B\u52A8\u4FEE\u6539 change \u4E0B spec
|
|
2678
|
+
- \`fet sync\` \u5408\u5E76\u5230 \`openspec/specs/**/spec.md\` \u65F6\uFF0C\u5BF9\u89E6\u53CA\u7684 Requirement \u4FDD\u7559\u6216\u5237\u65B0\u4E2D\u6587\u6CE8\u91CA`;
|
|
2679
|
+
}
|
|
2680
|
+
function renderCursorSpecLanguageRule(language) {
|
|
2681
|
+
const description = language === "en" ? "Layered bilingual OpenSpec specs: English canonical text with synced Chinese HTML comments" : "OpenSpec \u5206\u5C42\u53CC\u8BED\uFF1A\u82F1\u6587\u89C4\u8303 + \u540C\u6B65\u7EF4\u62A4\u7684\u4E2D\u6587\u6CE8\u91CA";
|
|
2682
|
+
return `<!-- FET:MANAGED
|
|
2683
|
+
schemaVersion: 1
|
|
2684
|
+
fetVersion: ${FET_VERSION}
|
|
2685
|
+
generator: cursor-adapter
|
|
2686
|
+
adapterVersion: 1
|
|
2687
|
+
FET:END -->
|
|
2688
|
+
|
|
2689
|
+
---
|
|
2690
|
+
description: ${description}
|
|
2691
|
+
alwaysApply: false
|
|
2692
|
+
---
|
|
2693
|
+
|
|
2694
|
+
${language === "en" ? "Apply when creating or editing OpenSpec spec files under `openspec/changes/` or `openspec/specs/`, or when running `fet propose` / `fet continue` / `fet ff` / `fet sync`." : "\u5728\u521B\u5EFA\u6216\u7F16\u8F91 `openspec/changes/`\u3001`openspec/specs/` \u4E0B\u7684 spec\uFF0C\u6216\u8FD0\u884C `fet propose` / `fet continue` / `fet ff` / `fet sync` \u65F6\u9002\u7528\u3002"}
|
|
2695
|
+
|
|
2696
|
+
${renderSpecLanguagePolicyBody(language)}
|
|
2697
|
+
`;
|
|
2698
|
+
}
|
|
2699
|
+
function renderCodexSpecLanguageGuide(language) {
|
|
2700
|
+
return `<!-- FET:MANAGED
|
|
2701
|
+
schemaVersion: 1
|
|
2702
|
+
fetVersion: ${FET_VERSION}
|
|
2703
|
+
generator: codex-adapter
|
|
2704
|
+
adapterVersion: 1
|
|
2705
|
+
FET:END -->
|
|
2706
|
+
|
|
2707
|
+
# OpenSpec spec language (layered bilingual)
|
|
2708
|
+
|
|
2709
|
+
${renderSpecLanguagePolicyBody(language)}
|
|
2710
|
+
`;
|
|
2711
|
+
}
|
|
2712
|
+
function renderPlanningArtifactSpecBlock(language, changeId) {
|
|
2713
|
+
const uiBlock = changeId !== void 0 ? `
|
|
2714
|
+
|
|
2715
|
+
${renderPlanningArtifactUiContractBlock(language, changeId)}` : "";
|
|
2716
|
+
if (language === "en") {
|
|
2717
|
+
return `## OpenSpec spec artifacts
|
|
2718
|
+
|
|
2719
|
+
When the artifact is \`specs/<capability>/spec.md\` (or you edit spec files in this change):
|
|
2720
|
+
|
|
2721
|
+
1. Follow \`.cursor/rules/fet-spec-language.mdc\` or \`.codex/fet/spec-language.md\`.
|
|
2722
|
+
2. English Requirements/Scenario + \`<!-- \u4E2D\u6587\uFF1A... -->\` after each Requirement title.
|
|
2723
|
+
3. Update Chinese notes in the **same edit** whenever English normative text changes.${uiBlock}`;
|
|
2724
|
+
}
|
|
2725
|
+
return `## OpenSpec spec \u4EA7\u7269
|
|
2726
|
+
|
|
2727
|
+
\u5F53\u4EA7\u7269\u4E3A \`specs/<capability>/spec.md\`\uFF08\u6216\u4F60\u5728\u672C change \u4E2D\u4FEE\u6539 spec\uFF09\u65F6\uFF1A
|
|
2728
|
+
|
|
2729
|
+
1. \u9075\u5B88 \`.cursor/rules/fet-spec-language.mdc\` \u6216 \`.codex/fet/spec-language.md\`\u3002
|
|
2730
|
+
2. \u82F1\u6587 Requirements/Scenario + \u6BCF\u4E2A Requirement \u6807\u9898\u540E\u52A0 \`<!-- \u4E2D\u6587\uFF1A... -->\`\u3002
|
|
2731
|
+
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}`;
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2242
2734
|
// src/commands/update-context.ts
|
|
2243
2735
|
async function updateContextCommand(ctx) {
|
|
2244
2736
|
let contextResult = { warnings: [] };
|
|
@@ -2400,8 +2892,455 @@ async function exists4(path) {
|
|
|
2400
2892
|
}
|
|
2401
2893
|
|
|
2402
2894
|
// src/commands/proxy.ts
|
|
2403
|
-
import { readFile as
|
|
2404
|
-
import { join as
|
|
2895
|
+
import { readFile as readFile15 } from "fs/promises";
|
|
2896
|
+
import { join as join19 } from "path";
|
|
2897
|
+
|
|
2898
|
+
// src/figma-guard.ts
|
|
2899
|
+
import { readdir as readdir3, readFile as readFile11, stat as stat7 } from "fs/promises";
|
|
2900
|
+
import { join as join16, relative as relative2 } from "path";
|
|
2901
|
+
import { parseDocument as parseDocument2 } from "yaml";
|
|
2902
|
+
var DEFAULT_CONFIG = {
|
|
2903
|
+
enabled: true,
|
|
2904
|
+
mode: "require_before_ui",
|
|
2905
|
+
onUncertainty: "stop_and_ask"
|
|
2906
|
+
};
|
|
2907
|
+
async function loadFigmaGuardConfig(projectRoot) {
|
|
2908
|
+
try {
|
|
2909
|
+
const raw = await readFile11(join16(projectRoot, "openspec", "config.yaml"), "utf8");
|
|
2910
|
+
const doc = parseDocument2(raw);
|
|
2911
|
+
const fetNode = doc.get("fet", true);
|
|
2912
|
+
const node = fetNode?.get?.("figmaGuard");
|
|
2913
|
+
if (!node || typeof node.get !== "function") {
|
|
2914
|
+
return DEFAULT_CONFIG;
|
|
2915
|
+
}
|
|
2916
|
+
const enabled = node.get("enabled");
|
|
2917
|
+
const modeRaw = node.get("mode");
|
|
2918
|
+
const mode = modeRaw === "stop_and_ask" ? "stop_and_ask" : "require_before_ui";
|
|
2919
|
+
return {
|
|
2920
|
+
enabled: enabled === void 0 ? true : Boolean(enabled),
|
|
2921
|
+
mode,
|
|
2922
|
+
onUncertainty: "stop_and_ask"
|
|
2923
|
+
};
|
|
2924
|
+
} catch {
|
|
2925
|
+
return DEFAULT_CONFIG;
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
function extractFigmaUrls(content) {
|
|
2929
|
+
const matches = content.match(FIGMA_URL_PATTERN) ?? [];
|
|
2930
|
+
return [...new Set(matches.map((url) => url.replace(/[.,;]+$/, "")))];
|
|
2931
|
+
}
|
|
2932
|
+
async function collectFigmaUrlsFromChange(projectRoot, changeId) {
|
|
2933
|
+
const changePath = join16(projectRoot, "openspec", "changes", changeId);
|
|
2934
|
+
const urls = /* @__PURE__ */ new Set();
|
|
2935
|
+
const sources = [];
|
|
2936
|
+
const candidates = ["proposal.md", "tasks.md", "design.md"];
|
|
2937
|
+
for (const name of candidates) {
|
|
2938
|
+
const filePath = join16(changePath, name);
|
|
2939
|
+
const content = await readOptional3(filePath);
|
|
2940
|
+
if (!content) {
|
|
2941
|
+
continue;
|
|
2942
|
+
}
|
|
2943
|
+
const found = extractFigmaUrls(content);
|
|
2944
|
+
if (found.length) {
|
|
2945
|
+
sources.push(`openspec/changes/${changeId}/${name}`);
|
|
2946
|
+
for (const url of found) {
|
|
2947
|
+
urls.add(url);
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
const specsPath = join16(changePath, "specs");
|
|
2952
|
+
for (const filePath of await listMarkdownFiles(specsPath)) {
|
|
2953
|
+
const content = await readOptional3(filePath);
|
|
2954
|
+
if (!content) {
|
|
2955
|
+
continue;
|
|
2956
|
+
}
|
|
2957
|
+
const found = extractFigmaUrls(content);
|
|
2958
|
+
if (found.length) {
|
|
2959
|
+
sources.push(relative2(projectRoot, filePath).replaceAll("\\", "/"));
|
|
2960
|
+
for (const url of found) {
|
|
2961
|
+
urls.add(url);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
return { urls: [...urls], sources };
|
|
2966
|
+
}
|
|
2967
|
+
async function ensureChangeFigmaStopHandoff(options) {
|
|
2968
|
+
const config = options.enabled === void 0 ? await loadFigmaGuardConfig(options.projectRoot) : {
|
|
2969
|
+
enabled: options.enabled,
|
|
2970
|
+
mode: options.mode ?? "require_before_ui",
|
|
2971
|
+
onUncertainty: "stop_and_ask"
|
|
2972
|
+
};
|
|
2973
|
+
if (!config.enabled) {
|
|
2974
|
+
return null;
|
|
2975
|
+
}
|
|
2976
|
+
const { urls, sources } = await collectFigmaUrlsFromChange(options.projectRoot, options.changeId);
|
|
2977
|
+
if (!urls.length) {
|
|
2978
|
+
return null;
|
|
2979
|
+
}
|
|
2980
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2981
|
+
let written = false;
|
|
2982
|
+
const stopRelativePath = figmaStopHandoffRelativePath(options.changeId);
|
|
2983
|
+
const stopAbsolutePath = join16(options.projectRoot, stopRelativePath);
|
|
2984
|
+
const stopContent = renderChangeFigmaStopHandoff({
|
|
2985
|
+
changeId: options.changeId,
|
|
2986
|
+
generatedAt,
|
|
2987
|
+
urls,
|
|
2988
|
+
sources,
|
|
2989
|
+
language: options.language
|
|
2990
|
+
});
|
|
2991
|
+
const existingStop = await readOptional3(stopAbsolutePath);
|
|
2992
|
+
if (existingStop !== stopContent) {
|
|
2993
|
+
await atomicWrite(stopAbsolutePath, stopContent);
|
|
2994
|
+
written = true;
|
|
2995
|
+
}
|
|
2996
|
+
let applyInstructionsPath;
|
|
2997
|
+
if (config.mode === "require_before_ui") {
|
|
2998
|
+
applyInstructionsPath = figmaApplyInstructionsRelativePath(options.changeId);
|
|
2999
|
+
const applyAbsolutePath = join16(options.projectRoot, applyInstructionsPath);
|
|
3000
|
+
const applyContent = renderChangeFigmaApplyInstructions({
|
|
3001
|
+
changeId: options.changeId,
|
|
3002
|
+
generatedAt,
|
|
3003
|
+
urls,
|
|
3004
|
+
sources,
|
|
3005
|
+
language: options.language
|
|
3006
|
+
});
|
|
3007
|
+
const existingApply = await readOptional3(applyAbsolutePath);
|
|
3008
|
+
if (existingApply !== applyContent) {
|
|
3009
|
+
await atomicWrite(applyAbsolutePath, applyContent);
|
|
3010
|
+
written = true;
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
return {
|
|
3014
|
+
path: stopRelativePath,
|
|
3015
|
+
applyInstructionsPath,
|
|
3016
|
+
written,
|
|
3017
|
+
urls,
|
|
3018
|
+
sources,
|
|
3019
|
+
mode: config.mode
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
3022
|
+
async function listMarkdownFiles(root) {
|
|
3023
|
+
const files = [];
|
|
3024
|
+
await walk(root, files);
|
|
3025
|
+
return files;
|
|
3026
|
+
}
|
|
3027
|
+
async function walk(dir, files) {
|
|
3028
|
+
try {
|
|
3029
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
3030
|
+
for (const entry of entries) {
|
|
3031
|
+
const fullPath = join16(dir, entry.name);
|
|
3032
|
+
if (entry.isDirectory()) {
|
|
3033
|
+
await walk(fullPath, files);
|
|
3034
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
3035
|
+
files.push(fullPath);
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
} catch {
|
|
3039
|
+
return;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
async function readOptional3(path) {
|
|
3043
|
+
try {
|
|
3044
|
+
await stat7(path);
|
|
3045
|
+
return await readFile11(path, "utf8");
|
|
3046
|
+
} catch {
|
|
3047
|
+
return null;
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
// src/ui-display-contract.ts
|
|
3052
|
+
import { existsSync as existsSync2 } from "fs";
|
|
3053
|
+
import { readdir as readdir4, readFile as readFile12, stat as stat8 } from "fs/promises";
|
|
3054
|
+
import { join as join17, relative as relative3 } from "path";
|
|
3055
|
+
import { parse as parse3, parseDocument as parseDocument3 } from "yaml";
|
|
3056
|
+
var DEFAULT_CONFIG2 = {
|
|
3057
|
+
enabled: true
|
|
3058
|
+
};
|
|
3059
|
+
var API_DOC_PATH_PATTERN = /(?:^|[\s(`])([\w./-]*(?:openapi|swagger|api-doc|api\/docs)[\w./-]*\.(?:ya?ml|json))(?:[\s)`.,;]|$)/gi;
|
|
3060
|
+
var MARKDOWN_LINK_PATH_PATTERN = /\[[^\]]*\]\(([^)]+\.(?:ya?ml|json))\)/gi;
|
|
3061
|
+
var BACKTICK_PATH_PATTERN = /`([^`]+\.(?:ya?ml|json))`/gi;
|
|
3062
|
+
var OPENAPI_BARE_PATTERN = /\b(openapi\.ya?ml|swagger\.ya?ml|swagger\.json)\b/gi;
|
|
3063
|
+
async function loadUiDisplayContractConfig(projectRoot) {
|
|
3064
|
+
try {
|
|
3065
|
+
const raw = await readFile12(join17(projectRoot, "openspec", "config.yaml"), "utf8");
|
|
3066
|
+
const doc = parseDocument3(raw);
|
|
3067
|
+
const fetNode = doc.get("fet", true);
|
|
3068
|
+
const node = fetNode?.get?.("uiDisplayContract");
|
|
3069
|
+
if (!node || typeof node.get !== "function") {
|
|
3070
|
+
return DEFAULT_CONFIG2;
|
|
3071
|
+
}
|
|
3072
|
+
const enabled = node.get("enabled");
|
|
3073
|
+
return {
|
|
3074
|
+
enabled: enabled === void 0 ? true : Boolean(enabled)
|
|
3075
|
+
};
|
|
3076
|
+
} catch {
|
|
3077
|
+
return DEFAULT_CONFIG2;
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
async function collectApiSourcesFromChange(projectRoot, changeId) {
|
|
3081
|
+
const changePath = join17(projectRoot, "openspec", "changes", changeId);
|
|
3082
|
+
const paths = /* @__PURE__ */ new Set();
|
|
3083
|
+
const sources = [];
|
|
3084
|
+
const candidates = ["proposal.md", "tasks.md", "design.md"];
|
|
3085
|
+
for (const name of candidates) {
|
|
3086
|
+
const filePath = join17(changePath, name);
|
|
3087
|
+
const content = await readOptional4(filePath);
|
|
3088
|
+
if (!content) {
|
|
3089
|
+
continue;
|
|
3090
|
+
}
|
|
3091
|
+
const found = extractApiDocPaths(content, projectRoot);
|
|
3092
|
+
if (found.length) {
|
|
3093
|
+
sources.push(`openspec/changes/${changeId}/${name}`);
|
|
3094
|
+
for (const path of found) {
|
|
3095
|
+
paths.add(path);
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
const specsPath = join17(changePath, "specs");
|
|
3100
|
+
for (const filePath of await listMarkdownFiles2(specsPath)) {
|
|
3101
|
+
const content = await readOptional4(filePath);
|
|
3102
|
+
if (!content) {
|
|
3103
|
+
continue;
|
|
3104
|
+
}
|
|
3105
|
+
const found = extractApiDocPaths(content, projectRoot);
|
|
3106
|
+
if (found.length) {
|
|
3107
|
+
sources.push(relative3(projectRoot, filePath).replaceAll("\\", "/"));
|
|
3108
|
+
for (const path of found) {
|
|
3109
|
+
paths.add(path);
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
return { paths: [...paths], sources };
|
|
3114
|
+
}
|
|
3115
|
+
function extractApiDocPaths(content, projectRoot) {
|
|
3116
|
+
const found = /* @__PURE__ */ new Set();
|
|
3117
|
+
const patterns = [API_DOC_PATH_PATTERN, MARKDOWN_LINK_PATH_PATTERN, BACKTICK_PATH_PATTERN, OPENAPI_BARE_PATTERN];
|
|
3118
|
+
for (const pattern of patterns) {
|
|
3119
|
+
pattern.lastIndex = 0;
|
|
3120
|
+
let match;
|
|
3121
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
3122
|
+
const raw = (match[1] ?? match[0]).trim().replace(/^["']|["']$/g, "");
|
|
3123
|
+
const normalized = normalizeRepoPath(raw, projectRoot);
|
|
3124
|
+
if (normalized) {
|
|
3125
|
+
found.add(normalized);
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
if (/\b(openapi|swagger|接口文档|API\s*文档)\b/i.test(content)) {
|
|
3130
|
+
const common = ["openapi.yaml", "openapi.yml", "docs/openapi.yaml", "swagger.yaml", "swagger.json"];
|
|
3131
|
+
for (const candidate of common) {
|
|
3132
|
+
const normalized = normalizeRepoPath(candidate, projectRoot);
|
|
3133
|
+
if (normalized && existsSync2(join17(projectRoot, normalized))) {
|
|
3134
|
+
found.add(normalized);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
return [...found];
|
|
3139
|
+
}
|
|
3140
|
+
async function extractOpenApiSchemas(projectRoot, relativePaths) {
|
|
3141
|
+
const schemas = [];
|
|
3142
|
+
for (const rel of relativePaths) {
|
|
3143
|
+
const absolute = join17(projectRoot, rel);
|
|
3144
|
+
const content = await readOptional4(absolute);
|
|
3145
|
+
if (!content) {
|
|
3146
|
+
continue;
|
|
3147
|
+
}
|
|
3148
|
+
let doc;
|
|
3149
|
+
try {
|
|
3150
|
+
doc = rel.endsWith(".json") ? JSON.parse(content) : parse3(content);
|
|
3151
|
+
} catch {
|
|
3152
|
+
continue;
|
|
3153
|
+
}
|
|
3154
|
+
const names = collectOpenApiSchemaPropertyNames(doc);
|
|
3155
|
+
for (const [name, fields] of names) {
|
|
3156
|
+
if (fields.length) {
|
|
3157
|
+
schemas.push({ name, fields: [...new Set(fields)].sort(), sourcePath: rel });
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
return schemas;
|
|
3162
|
+
}
|
|
3163
|
+
function buildUiDisplayContractDocument(options) {
|
|
3164
|
+
const allApiFields = [...new Set(options.apiSchemas.flatMap((schema) => schema.fields))].sort();
|
|
3165
|
+
const screenId = options.figmaUrls.length ? "primary" : "default";
|
|
3166
|
+
const omittedCandidates = allApiFields.map((field) => {
|
|
3167
|
+
const schema = options.apiSchemas.find((item) => item.fields.includes(field))?.name;
|
|
3168
|
+
return {
|
|
3169
|
+
field,
|
|
3170
|
+
schema,
|
|
3171
|
+
reason: "Listed in API/OpenAPI schema; not yet in displayFields\u2014confirm with Figma before rendering in UI."
|
|
3172
|
+
};
|
|
3173
|
+
});
|
|
3174
|
+
return {
|
|
3175
|
+
schemaVersion: 1,
|
|
3176
|
+
fetVersion: options.fetVersion,
|
|
3177
|
+
generatedAt: options.generatedAt,
|
|
3178
|
+
changeId: options.changeId,
|
|
3179
|
+
purpose: "ui-display-contract",
|
|
3180
|
+
status: "draft",
|
|
3181
|
+
precedence: [
|
|
3182
|
+
"ui-display-contract.yaml displayFields",
|
|
3183
|
+
"Figma visible elements",
|
|
3184
|
+
"API/OpenAPI schema (data layer and types only)"
|
|
3185
|
+
],
|
|
3186
|
+
sources: {
|
|
3187
|
+
figma: {
|
|
3188
|
+
urls: options.figmaUrls,
|
|
3189
|
+
artifactPaths: options.figmaSources
|
|
3190
|
+
},
|
|
3191
|
+
api: options.apiPaths.map((path) => ({
|
|
3192
|
+
path,
|
|
3193
|
+
artifactPaths: options.apiSources,
|
|
3194
|
+
schemas: options.apiSchemas.filter((schema) => schema.sourcePath === path)
|
|
3195
|
+
}))
|
|
3196
|
+
},
|
|
3197
|
+
screens: [
|
|
3198
|
+
{
|
|
3199
|
+
id: screenId,
|
|
3200
|
+
title: options.figmaUrls.length ? "Primary screen (confirm id with design)" : "Default screen",
|
|
3201
|
+
figmaUrls: options.figmaUrls,
|
|
3202
|
+
displayFields: [],
|
|
3203
|
+
hiddenButUsed: [],
|
|
3204
|
+
omittedFromUi: [],
|
|
3205
|
+
needsReview: allApiFields.length ? [
|
|
3206
|
+
`Fill displayFields for screen "${screenId}" from Figma before UI implementation.`,
|
|
3207
|
+
...omittedCandidates.slice(0, 12).map((item) => `API field "${item.field}"\u2014omit from UI or add to displayFields?`)
|
|
3208
|
+
] : options.figmaUrls.length ? [`Fill displayFields for screen "${screenId}" from Figma MCP/API.`] : ["Add API doc paths to change artifacts or confirm this change has no API-backed UI."]
|
|
3209
|
+
}
|
|
3210
|
+
],
|
|
3211
|
+
omittedCandidates
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
async function ensureChangeUiDisplayContract(options) {
|
|
3215
|
+
const config = options.enabled === void 0 ? await loadUiDisplayContractConfig(options.projectRoot) : { enabled: options.enabled };
|
|
3216
|
+
if (!config.enabled) {
|
|
3217
|
+
return null;
|
|
3218
|
+
}
|
|
3219
|
+
const figma = await collectFigmaUrlsFromChange(options.projectRoot, options.changeId);
|
|
3220
|
+
const api = await collectApiSourcesFromChange(options.projectRoot, options.changeId);
|
|
3221
|
+
const hasFigma = figma.urls.length > 0;
|
|
3222
|
+
const hasApi = api.paths.length > 0;
|
|
3223
|
+
if (!hasFigma && !hasApi) {
|
|
3224
|
+
return null;
|
|
3225
|
+
}
|
|
3226
|
+
const apiSchemas = hasApi ? await extractOpenApiSchemas(options.projectRoot, api.paths) : [];
|
|
3227
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3228
|
+
const contractRelativePath = uiDisplayContractRelativePath(options.changeId);
|
|
3229
|
+
const applyRelativePath = uiFieldApplyInstructionsRelativePath(options.changeId);
|
|
3230
|
+
const doc = buildUiDisplayContractDocument({
|
|
3231
|
+
changeId: options.changeId,
|
|
3232
|
+
generatedAt,
|
|
3233
|
+
fetVersion: options.fetVersion,
|
|
3234
|
+
figmaUrls: figma.urls,
|
|
3235
|
+
figmaSources: figma.sources,
|
|
3236
|
+
apiPaths: api.paths,
|
|
3237
|
+
apiSources: api.sources,
|
|
3238
|
+
apiSchemas
|
|
3239
|
+
});
|
|
3240
|
+
const contractContent = renderUiDisplayContractYaml(doc);
|
|
3241
|
+
const contractAbsolutePath = join17(options.projectRoot, contractRelativePath);
|
|
3242
|
+
const existingContract = await readOptional4(contractAbsolutePath);
|
|
3243
|
+
let written = false;
|
|
3244
|
+
if (existingContract !== contractContent) {
|
|
3245
|
+
await atomicWrite(contractAbsolutePath, contractContent);
|
|
3246
|
+
written = true;
|
|
3247
|
+
}
|
|
3248
|
+
const applyContent = renderUiFieldApplyInstructions({
|
|
3249
|
+
changeId: options.changeId,
|
|
3250
|
+
generatedAt,
|
|
3251
|
+
contractPath: contractRelativePath,
|
|
3252
|
+
language: options.language,
|
|
3253
|
+
hasFigma,
|
|
3254
|
+
hasApi
|
|
3255
|
+
});
|
|
3256
|
+
const applyAbsolutePath = join17(options.projectRoot, applyRelativePath);
|
|
3257
|
+
const existingApply = await readOptional4(applyAbsolutePath);
|
|
3258
|
+
if (existingApply !== applyContent) {
|
|
3259
|
+
await atomicWrite(applyAbsolutePath, applyContent);
|
|
3260
|
+
written = true;
|
|
3261
|
+
}
|
|
3262
|
+
return {
|
|
3263
|
+
contractPath: contractRelativePath,
|
|
3264
|
+
applyInstructionsPath: applyRelativePath,
|
|
3265
|
+
written,
|
|
3266
|
+
hasFigma,
|
|
3267
|
+
hasApi,
|
|
3268
|
+
apiFieldCount: apiSchemas.reduce((sum, schema) => sum + schema.fields.length, 0)
|
|
3269
|
+
};
|
|
3270
|
+
}
|
|
3271
|
+
function collectOpenApiSchemaPropertyNames(doc) {
|
|
3272
|
+
const result = /* @__PURE__ */ new Map();
|
|
3273
|
+
if (!doc || typeof doc !== "object") {
|
|
3274
|
+
return result;
|
|
3275
|
+
}
|
|
3276
|
+
const root = doc;
|
|
3277
|
+
const components = root.components;
|
|
3278
|
+
const schemas = components?.schemas;
|
|
3279
|
+
if (schemas && typeof schemas === "object") {
|
|
3280
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
3281
|
+
const fields = propertyNamesFromSchema(schema);
|
|
3282
|
+
if (fields.length) {
|
|
3283
|
+
result.set(name, fields);
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
return result;
|
|
3288
|
+
}
|
|
3289
|
+
function propertyNamesFromSchema(schema, depth = 0) {
|
|
3290
|
+
if (!schema || typeof schema !== "object" || depth > 4) {
|
|
3291
|
+
return [];
|
|
3292
|
+
}
|
|
3293
|
+
const node = schema;
|
|
3294
|
+
if (node.$ref && typeof node.$ref === "string") {
|
|
3295
|
+
return [];
|
|
3296
|
+
}
|
|
3297
|
+
const properties = node.properties;
|
|
3298
|
+
if (properties && typeof properties === "object") {
|
|
3299
|
+
return Object.keys(properties).sort();
|
|
3300
|
+
}
|
|
3301
|
+
if (node.items) {
|
|
3302
|
+
return propertyNamesFromSchema(node.items, depth + 1);
|
|
3303
|
+
}
|
|
3304
|
+
if (node.allOf && Array.isArray(node.allOf)) {
|
|
3305
|
+
return node.allOf.flatMap((part) => propertyNamesFromSchema(part, depth + 1));
|
|
3306
|
+
}
|
|
3307
|
+
return [];
|
|
3308
|
+
}
|
|
3309
|
+
function normalizeRepoPath(raw, projectRoot) {
|
|
3310
|
+
const cleaned = raw.replace(/^\.\//, "").replaceAll("\\", "/");
|
|
3311
|
+
if (!cleaned || cleaned.includes("..")) {
|
|
3312
|
+
return null;
|
|
3313
|
+
}
|
|
3314
|
+
return cleaned;
|
|
3315
|
+
}
|
|
3316
|
+
async function listMarkdownFiles2(root) {
|
|
3317
|
+
const files = [];
|
|
3318
|
+
await walk2(root, files);
|
|
3319
|
+
return files;
|
|
3320
|
+
}
|
|
3321
|
+
async function walk2(dir, files) {
|
|
3322
|
+
try {
|
|
3323
|
+
const entries = await readdir4(dir, { withFileTypes: true });
|
|
3324
|
+
for (const entry of entries) {
|
|
3325
|
+
const fullPath = join17(dir, entry.name);
|
|
3326
|
+
if (entry.isDirectory()) {
|
|
3327
|
+
await walk2(fullPath, files);
|
|
3328
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
3329
|
+
files.push(fullPath);
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
} catch {
|
|
3333
|
+
return;
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
async function readOptional4(path) {
|
|
3337
|
+
try {
|
|
3338
|
+
await stat8(path);
|
|
3339
|
+
return await readFile12(path, "utf8");
|
|
3340
|
+
} catch {
|
|
3341
|
+
return null;
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
2405
3344
|
|
|
2406
3345
|
// src/state/project.ts
|
|
2407
3346
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -2430,8 +3369,8 @@ async function git(cwd, args) {
|
|
|
2430
3369
|
}
|
|
2431
3370
|
|
|
2432
3371
|
// src/state/store.ts
|
|
2433
|
-
import { mkdir as mkdir6, readFile as
|
|
2434
|
-
import { join as
|
|
3372
|
+
import { mkdir as mkdir6, readFile as readFile13 } from "fs/promises";
|
|
3373
|
+
import { join as join18 } from "path";
|
|
2435
3374
|
|
|
2436
3375
|
// src/language.ts
|
|
2437
3376
|
var DEFAULT_LANGUAGE = "zh-CN";
|
|
@@ -2549,7 +3488,7 @@ var StateStore = class {
|
|
|
2549
3488
|
project;
|
|
2550
3489
|
async readGlobal() {
|
|
2551
3490
|
try {
|
|
2552
|
-
const value = JSON.parse(await
|
|
3491
|
+
const value = JSON.parse(await readFile13(this.globalPath(), "utf8"));
|
|
2553
3492
|
assertGlobalState(value);
|
|
2554
3493
|
return value;
|
|
2555
3494
|
} catch (error) {
|
|
@@ -2564,13 +3503,13 @@ var StateStore = class {
|
|
|
2564
3503
|
}
|
|
2565
3504
|
async writeGlobal(state) {
|
|
2566
3505
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2567
|
-
await mkdir6(
|
|
3506
|
+
await mkdir6(join18(this.projectRoot, "openspec"), { recursive: true });
|
|
2568
3507
|
await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
|
|
2569
3508
|
`);
|
|
2570
3509
|
}
|
|
2571
3510
|
async readChange(changeId) {
|
|
2572
3511
|
try {
|
|
2573
|
-
const value = JSON.parse(await
|
|
3512
|
+
const value = JSON.parse(await readFile13(this.changePath(changeId), "utf8"));
|
|
2574
3513
|
assertChangeState(value);
|
|
2575
3514
|
return value;
|
|
2576
3515
|
} catch (error) {
|
|
@@ -2585,15 +3524,15 @@ var StateStore = class {
|
|
|
2585
3524
|
}
|
|
2586
3525
|
async writeChange(state) {
|
|
2587
3526
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2588
|
-
await mkdir6(
|
|
3527
|
+
await mkdir6(join18(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
|
|
2589
3528
|
await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
|
|
2590
3529
|
`);
|
|
2591
3530
|
}
|
|
2592
3531
|
globalPath() {
|
|
2593
|
-
return
|
|
3532
|
+
return join18(this.projectRoot, "openspec", "fet-state.json");
|
|
2594
3533
|
}
|
|
2595
3534
|
changePath(changeId) {
|
|
2596
|
-
return
|
|
3535
|
+
return join18(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
|
|
2597
3536
|
}
|
|
2598
3537
|
};
|
|
2599
3538
|
function isNotFound(error) {
|
|
@@ -2601,11 +3540,11 @@ function isNotFound(error) {
|
|
|
2601
3540
|
}
|
|
2602
3541
|
|
|
2603
3542
|
// src/state/tasks.ts
|
|
2604
|
-
import { readFile as
|
|
3543
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
2605
3544
|
async function readCompletedTaskIds(tasksPath) {
|
|
2606
3545
|
let content;
|
|
2607
3546
|
try {
|
|
2608
|
-
content = await
|
|
3547
|
+
content = await readFile14(tasksPath, "utf8");
|
|
2609
3548
|
} catch {
|
|
2610
3549
|
return [];
|
|
2611
3550
|
}
|
|
@@ -2754,21 +3693,43 @@ async function applyWorkflowCommand(ctx, args) {
|
|
|
2754
3693
|
exitCode: instructions.exitCode,
|
|
2755
3694
|
phaseStatus: "in_progress"
|
|
2756
3695
|
});
|
|
3696
|
+
const [figmaGuard, uiContract] = await Promise.all([
|
|
3697
|
+
ensureChangeFigmaStopHandoff({
|
|
3698
|
+
projectRoot: ctx.projectRoot,
|
|
3699
|
+
changeId,
|
|
3700
|
+
language: ctx.language
|
|
3701
|
+
}),
|
|
3702
|
+
ensureChangeUiDisplayContract({
|
|
3703
|
+
projectRoot: ctx.projectRoot,
|
|
3704
|
+
changeId,
|
|
3705
|
+
language: ctx.language,
|
|
3706
|
+
fetVersion: ctx.fetVersion
|
|
3707
|
+
})
|
|
3708
|
+
]);
|
|
3709
|
+
const applyNextSteps = [
|
|
3710
|
+
`Read openspec/changes/${changeId}/tasks.md and the instructions output.`,
|
|
3711
|
+
"Implement pending tasks and update task checkboxes only after the work is done.",
|
|
3712
|
+
`Run fet verify --change ${changeId}`
|
|
3713
|
+
];
|
|
3714
|
+
if (uiContract) {
|
|
3715
|
+
applyNextSteps.unshift(...renderUiDisplayContractApplyNextSteps(changeId, ctx.language));
|
|
3716
|
+
}
|
|
3717
|
+
if (figmaGuard) {
|
|
3718
|
+
applyNextSteps.unshift(...renderFigmaApplyNextSteps(changeId, ctx.language, figmaGuard.mode));
|
|
3719
|
+
}
|
|
2757
3720
|
ctx.output.result({
|
|
2758
3721
|
ok: true,
|
|
2759
3722
|
command: "apply",
|
|
2760
3723
|
summary: `fet apply prepared implementation instructions for change "${changeId}".`,
|
|
2761
3724
|
warnings: [...runState.graphContext?.warnings ?? [], ...warnings ?? []],
|
|
2762
|
-
nextSteps:
|
|
2763
|
-
`Read openspec/changes/${changeId}/tasks.md and the instructions output.`,
|
|
2764
|
-
"Implement pending tasks and update task checkboxes only after the work is done.",
|
|
2765
|
-
`Run fet verify --change ${changeId}`
|
|
2766
|
-
],
|
|
3725
|
+
nextSteps: applyNextSteps,
|
|
2767
3726
|
data: {
|
|
2768
3727
|
changeId,
|
|
2769
3728
|
instructions: instructions.data,
|
|
2770
3729
|
status,
|
|
2771
|
-
graphContext: runState.graphContext
|
|
3730
|
+
graphContext: runState.graphContext,
|
|
3731
|
+
figmaGuard: figmaGuard ?? void 0,
|
|
3732
|
+
uiDisplayContract: uiContract ?? void 0
|
|
2772
3733
|
}
|
|
2773
3734
|
});
|
|
2774
3735
|
});
|
|
@@ -2781,16 +3742,25 @@ async function exploreWorkflowCommand(ctx, args) {
|
|
|
2781
3742
|
args: openSpecArgs,
|
|
2782
3743
|
changeId
|
|
2783
3744
|
});
|
|
3745
|
+
const figmaGuard = changeId ? await ensureChangeFigmaStopHandoff({
|
|
3746
|
+
projectRoot: ctx.projectRoot,
|
|
3747
|
+
changeId,
|
|
3748
|
+
language: ctx.language
|
|
3749
|
+
}) : null;
|
|
3750
|
+
const exploreNextSteps = [
|
|
3751
|
+
"Discuss the requirement, constraints, and acceptance criteria with the user.",
|
|
3752
|
+
changeId ? `Run fet continue --change ${changeId} when ready to create the next artifact.` : "Run fet propose <change-id-or-description> when ready to create a change."
|
|
3753
|
+
];
|
|
3754
|
+
if (figmaGuard) {
|
|
3755
|
+
exploreNextSteps.unshift(renderFigmaStopNextStep(changeId, ctx.language));
|
|
3756
|
+
}
|
|
2784
3757
|
ctx.output.result({
|
|
2785
3758
|
ok: true,
|
|
2786
3759
|
command: "explore",
|
|
2787
3760
|
summary: "fet explore is an IDE-guided workflow for shaping OpenSpec changes.",
|
|
2788
3761
|
warnings: graphContext.warnings,
|
|
2789
|
-
nextSteps:
|
|
2790
|
-
|
|
2791
|
-
changeId ? `Run fet continue --change ${changeId} when ready to create the next artifact.` : "Run fet propose <change-id-or-description> when ready to create a change."
|
|
2792
|
-
],
|
|
2793
|
-
data: { changeId, args: openSpecArgs, graphContext }
|
|
3762
|
+
nextSteps: exploreNextSteps,
|
|
3763
|
+
data: { changeId, args: openSpecArgs, graphContext, figmaGuard: figmaGuard ?? void 0 }
|
|
2794
3764
|
});
|
|
2795
3765
|
}
|
|
2796
3766
|
async function syncWorkflowCommand(ctx, args) {
|
|
@@ -2911,23 +3881,47 @@ async function artifactWorkflowCommand(ctx, command, args) {
|
|
|
2911
3881
|
exitCode: instructions.exitCode
|
|
2912
3882
|
});
|
|
2913
3883
|
const status = await readOpenSpecStatus(ctx, changeId);
|
|
3884
|
+
const [figmaGuard, uiContract] = await Promise.all([
|
|
3885
|
+
ensureChangeFigmaStopHandoff({
|
|
3886
|
+
projectRoot: ctx.projectRoot,
|
|
3887
|
+
changeId,
|
|
3888
|
+
language: ctx.language
|
|
3889
|
+
}),
|
|
3890
|
+
ensureChangeUiDisplayContract({
|
|
3891
|
+
projectRoot: ctx.projectRoot,
|
|
3892
|
+
changeId,
|
|
3893
|
+
language: ctx.language,
|
|
3894
|
+
fetVersion: ctx.fetVersion
|
|
3895
|
+
})
|
|
3896
|
+
]);
|
|
3897
|
+
const planningNextSteps = [
|
|
3898
|
+
`Create or update openspec/changes/${changeId}/${resolveOutputPath(status, artifactId)}`,
|
|
3899
|
+
"Review the artifact with the user before generating the next planning file.",
|
|
3900
|
+
`Run fet passthrough status --change ${changeId}`,
|
|
3901
|
+
status.isComplete ? `Run fet apply --change ${changeId}` : `Run fet continue --change ${changeId} when ready for the next artifact`
|
|
3902
|
+
];
|
|
3903
|
+
if (uiContract) {
|
|
3904
|
+
planningNextSteps.unshift(
|
|
3905
|
+
ctx.language === "en" ? `If writing specs for API-backed UI, add the UI display contract Requirement; draft at ${uiContract.contractPath}.` : `\u82E5\u7F16\u5199\u7ED1\u5B9A\u63A5\u53E3\u7684 UI spec\uFF0C\u987B\u52A0\u5165 UI \u5C55\u793A\u5951\u7EA6 Requirement\uFF1B\u8349\u6848\u89C1 ${uiContract.contractPath}\u3002`
|
|
3906
|
+
);
|
|
3907
|
+
}
|
|
3908
|
+
if (figmaGuard) {
|
|
3909
|
+
planningNextSteps.unshift(renderFigmaStopNextStep(changeId, ctx.language));
|
|
3910
|
+
}
|
|
2914
3911
|
ctx.output.result({
|
|
2915
3912
|
ok: true,
|
|
2916
3913
|
command,
|
|
2917
3914
|
summary: `fet ${command} prepared OpenSpec artifact "${artifactId}" for change "${changeId}".`,
|
|
2918
3915
|
warnings: runState.graphContext?.warnings,
|
|
2919
|
-
nextSteps:
|
|
2920
|
-
`Create or update openspec/changes/${changeId}/${resolveOutputPath(status, artifactId)}`,
|
|
2921
|
-
"Review the artifact with the user before generating the next planning file.",
|
|
2922
|
-
`Run fet passthrough status --change ${changeId}`,
|
|
2923
|
-
status.isComplete ? `Run fet apply --change ${changeId}` : `Run fet continue --change ${changeId} when ready for the next artifact`
|
|
2924
|
-
],
|
|
3916
|
+
nextSteps: planningNextSteps,
|
|
2925
3917
|
data: {
|
|
2926
3918
|
changeId,
|
|
2927
3919
|
artifactId,
|
|
2928
3920
|
instructions: instructions.data,
|
|
2929
3921
|
status,
|
|
2930
|
-
graphContext: runState.graphContext
|
|
3922
|
+
graphContext: runState.graphContext,
|
|
3923
|
+
figmaGuard: figmaGuard ?? void 0,
|
|
3924
|
+
uiDisplayContract: uiContract ?? void 0
|
|
2931
3925
|
}
|
|
2932
3926
|
});
|
|
2933
3927
|
});
|
|
@@ -3133,8 +4127,8 @@ async function createChangelogEntry(projectRoot, changeId) {
|
|
|
3133
4127
|
};
|
|
3134
4128
|
}
|
|
3135
4129
|
async function appendChangelog(projectRoot, entry) {
|
|
3136
|
-
const changelogPath =
|
|
3137
|
-
const existing = await
|
|
4130
|
+
const changelogPath = join19(projectRoot, "CHANGELOG.md");
|
|
4131
|
+
const existing = await readOptional5(changelogPath);
|
|
3138
4132
|
const legacyContentLabel = "\u66F4\u65B0\u5185\u5BB9";
|
|
3139
4133
|
const block = `updateTime: ${entry.updateTime}
|
|
3140
4134
|
changeRequirement:${entry.content}
|
|
@@ -3146,12 +4140,12 @@ ${block}` : block;
|
|
|
3146
4140
|
await atomicWrite(changelogPath, next);
|
|
3147
4141
|
}
|
|
3148
4142
|
async function readChangeRequirement(projectRoot, changeId) {
|
|
3149
|
-
const changeRoot =
|
|
3150
|
-
const proposal = await
|
|
4143
|
+
const changeRoot = join19(projectRoot, "openspec", "changes", changeId);
|
|
4144
|
+
const proposal = await readOptional5(join19(changeRoot, "proposal.md"));
|
|
3151
4145
|
if (proposal) {
|
|
3152
4146
|
return summarizeMarkdown(proposal);
|
|
3153
4147
|
}
|
|
3154
|
-
const readme = await
|
|
4148
|
+
const readme = await readOptional5(join19(changeRoot, "README.md"));
|
|
3155
4149
|
if (readme) {
|
|
3156
4150
|
return summarizeMarkdown(readme);
|
|
3157
4151
|
}
|
|
@@ -3161,9 +4155,9 @@ function summarizeMarkdown(content) {
|
|
|
3161
4155
|
const normalized = content.replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("<!--") && !line.startsWith("---")).join(" ");
|
|
3162
4156
|
return normalized || "No change requirement found.";
|
|
3163
4157
|
}
|
|
3164
|
-
async function
|
|
4158
|
+
async function readOptional5(path) {
|
|
3165
4159
|
try {
|
|
3166
|
-
return await
|
|
4160
|
+
return await readFile15(path, "utf8");
|
|
3167
4161
|
} catch {
|
|
3168
4162
|
return null;
|
|
3169
4163
|
}
|
|
@@ -3519,8 +4513,8 @@ async function updateCommand(ctx) {
|
|
|
3519
4513
|
|
|
3520
4514
|
// src/commands/verify.ts
|
|
3521
4515
|
import { createHash } from "crypto";
|
|
3522
|
-
import { mkdir as mkdir7, readFile as
|
|
3523
|
-
import { join as
|
|
4516
|
+
import { mkdir as mkdir7, readFile as readFile16, stat as stat9 } from "fs/promises";
|
|
4517
|
+
import { join as join20 } from "path";
|
|
3524
4518
|
async function verifyCommand(ctx, options) {
|
|
3525
4519
|
if (options.auto) {
|
|
3526
4520
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
@@ -3587,8 +4581,8 @@ async function verifyCommand(ctx, options) {
|
|
|
3587
4581
|
async function writeInstructions(ctx, changeId) {
|
|
3588
4582
|
await assertChangeExists(ctx, changeId);
|
|
3589
4583
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3590
|
-
const dir =
|
|
3591
|
-
const instructionsPath =
|
|
4584
|
+
const dir = join20(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
4585
|
+
const instructionsPath = join20(dir, "verify-instructions.md");
|
|
3592
4586
|
await mkdir7(dir, { recursive: true });
|
|
3593
4587
|
await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
|
|
3594
4588
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
@@ -3605,7 +4599,7 @@ async function writeInstructions(ctx, changeId) {
|
|
|
3605
4599
|
async function markDone(ctx, changeId) {
|
|
3606
4600
|
await assertChangeExists(ctx, changeId);
|
|
3607
4601
|
const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3608
|
-
const instructionsPath =
|
|
4602
|
+
const instructionsPath = join20(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
|
|
3609
4603
|
const instructions = await readInstructions(instructionsPath, changeId);
|
|
3610
4604
|
const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
|
|
3611
4605
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
@@ -3640,8 +4634,8 @@ async function assertChangeExists(ctx, changeId) {
|
|
|
3640
4634
|
}
|
|
3641
4635
|
async function readInstructions(path, changeId) {
|
|
3642
4636
|
try {
|
|
3643
|
-
await
|
|
3644
|
-
const content = await
|
|
4637
|
+
await stat9(path);
|
|
4638
|
+
const content = await readFile16(path, "utf8");
|
|
3645
4639
|
const fileChangeId = readFrontMatterValue(content, "changeId");
|
|
3646
4640
|
if (fileChangeId !== changeId) {
|
|
3647
4641
|
throw new FetError({
|
|
@@ -3779,9 +4773,9 @@ function renderIdeModelPolicy(command, language = "zh-CN") {
|
|
|
3779
4773
|
import { resolve } from "path";
|
|
3780
4774
|
|
|
3781
4775
|
// src/adapters/codex/index.ts
|
|
3782
|
-
import { mkdir as mkdir8, readFile as
|
|
4776
|
+
import { mkdir as mkdir8, readFile as readFile17, stat as stat10 } from "fs/promises";
|
|
3783
4777
|
import { homedir } from "os";
|
|
3784
|
-
import { dirname as dirname8, join as
|
|
4778
|
+
import { dirname as dirname8, join as join21 } from "path";
|
|
3785
4779
|
|
|
3786
4780
|
// src/adapters/commands.ts
|
|
3787
4781
|
var FET_WORKFLOW_COMMANDS = [
|
|
@@ -3823,6 +4817,10 @@ Before doing FET or OpenSpec work in Codex, read:
|
|
|
3823
4817
|
- AGENTS.md
|
|
3824
4818
|
- openspec/config.yaml
|
|
3825
4819
|
- .codex/fet/karpathy-guidelines.md
|
|
4820
|
+
- .codex/fet/figma-stop.md when implementing UI from Figma
|
|
4821
|
+
- .codex/fet/spec-language.md when writing or updating OpenSpec specs
|
|
4822
|
+
- openspec/changes/<change-id>/.fet/figma-apply-instructions.md before UI work when FET apply reports Figma links
|
|
4823
|
+
- .codex/fet/ui-display-contract.md when UI binds API data; openspec/changes/<change-id>/.fet/ui-display-contract.yaml when present
|
|
3826
4824
|
- the active change files under openspec/changes/<change-id>/, when a change is selected
|
|
3827
4825
|
|
|
3828
4826
|
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.
|
|
@@ -3841,6 +4839,9 @@ ${languageInstruction(language)}
|
|
|
3841
4839
|
- AGENTS.md
|
|
3842
4840
|
- openspec/config.yaml
|
|
3843
4841
|
- .codex/fet/karpathy-guidelines.md
|
|
4842
|
+
- \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
|
|
4843
|
+
- \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
|
|
4844
|
+
- \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
|
|
3844
4845
|
- \u5982\u679C\u5DF2\u9009\u62E9 change\uFF0C\u9605\u8BFB openspec/changes/<change-id>/ \u4E0B\u7684\u5F53\u524D\u4EA7\u7269
|
|
3845
4846
|
|
|
3846
4847
|
\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
|
|
@@ -3861,9 +4862,30 @@ FET:END -->
|
|
|
3861
4862
|
${body}`
|
|
3862
4863
|
};
|
|
3863
4864
|
}
|
|
4865
|
+
function codexFigmaStopFile(language = DEFAULT_LANGUAGE) {
|
|
4866
|
+
return {
|
|
4867
|
+
path: ".codex/fet/figma-stop.md",
|
|
4868
|
+
content: renderCodexFigmaStopGuide(language)
|
|
4869
|
+
};
|
|
4870
|
+
}
|
|
4871
|
+
function codexSpecLanguageFile(language = DEFAULT_LANGUAGE) {
|
|
4872
|
+
return {
|
|
4873
|
+
path: ".codex/fet/spec-language.md",
|
|
4874
|
+
content: renderCodexSpecLanguageGuide(language)
|
|
4875
|
+
};
|
|
4876
|
+
}
|
|
4877
|
+
function codexUiDisplayContractFile(language = DEFAULT_LANGUAGE) {
|
|
4878
|
+
return {
|
|
4879
|
+
path: ".codex/fet/ui-display-contract.md",
|
|
4880
|
+
content: renderCodexUiDisplayContractGuide(language)
|
|
4881
|
+
};
|
|
4882
|
+
}
|
|
3864
4883
|
function codexCommandFiles(language = DEFAULT_LANGUAGE) {
|
|
3865
4884
|
return [
|
|
3866
4885
|
codexKarpathyGuidelinesFile(language),
|
|
4886
|
+
codexFigmaStopFile(language),
|
|
4887
|
+
codexUiDisplayContractFile(language),
|
|
4888
|
+
codexSpecLanguageFile(language),
|
|
3867
4889
|
...FET_ADAPTER_COMMANDS.map((command) => ({
|
|
3868
4890
|
path: `.codex/fet/commands/${command}.md`,
|
|
3869
4891
|
content: renderCommand(command, language)
|
|
@@ -4214,7 +5236,9 @@ ${commandGoalZh(command)}
|
|
|
4214
5236
|
- \u9ED8\u8BA4\u4F7F\u7528\u4E2D\u6587\u4EA7\u51FA\u3002
|
|
4215
5237
|
- \u4E0D\u8981\u7ED5\u8FC7 FET \u76F4\u63A5\u8C03\u7528 openspec\uFF0C\u9664\u975E FET \u547D\u4EE4\u672C\u8EAB\u4E0D\u53EF\u7528\u3002
|
|
4216
5238
|
- change \u4E0D\u660E\u786E\u65F6\u5148\u8BE2\u95EE\u7528\u6237\u3002
|
|
4217
|
-
${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\u3002\n" : ""}${command === "propose" || command === "continue" ? "- \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" ? "- \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 === "
|
|
5239
|
+
${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\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")}
|
|
5240
|
+
` : ""}${command === "propose" || command === "continue" || command === "ff" ? `${renderUiDisplayContractGuardrail("zh-CN")}
|
|
5241
|
+
` : ""}${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" : ""}`;
|
|
4218
5242
|
}
|
|
4219
5243
|
function commandTitleZh(command) {
|
|
4220
5244
|
const titles = {
|
|
@@ -4254,7 +5278,7 @@ function commandGoalZh(command) {
|
|
|
4254
5278
|
return "\u5728\u7528\u6237\u660E\u786E\u8981\u6C42\u5FEB\u8FDB\u65F6\uFF0C\u4E00\u6B21\u6027\u751F\u6210 change \u6240\u9700\u7684\u5168\u90E8\u89C4\u5212\u4EA7\u7269\u3002";
|
|
4255
5279
|
}
|
|
4256
5280
|
if (command === "apply") {
|
|
4257
|
-
return "\u8BFB\u53D6 OpenSpec \u4EA7\u7269\
|
|
5281
|
+
return "\u8BFB\u53D6 OpenSpec \u4EA7\u7269\uFF1B\u6709 Figma \u65F6\u5148\u8BFB\u7A3F\u518D\u5B9E\u65BD UI\uFF1B\u6709 ui-display-contract.yaml \u65F6\u53EA\u5C55\u793A displayFields\uFF0C\u7981\u6B62\u6309 OpenAPI \u5168\u91CF\u6E32\u67D3\u3002";
|
|
4258
5282
|
}
|
|
4259
5283
|
if (command.startsWith("graph-")) {
|
|
4260
5284
|
return "\u7BA1\u7406\u53EF\u9009\u7684 GitNexus \u4EE3\u7801\u56FE\uFF0C\u8BA9 AI \u5728\u5927\u8303\u56F4\u626B\u63CF\u524D\u4F18\u5148\u83B7\u5F97\u7ED3\u6784\u5316\u4EE3\u7801\u4E0A\u4E0B\u6587\u3002";
|
|
@@ -4379,16 +5403,26 @@ Steps:
|
|
|
4379
5403
|
fet apply --change <change-id> --json
|
|
4380
5404
|
\`\`\`
|
|
4381
5405
|
3. Follow the native apply output. If JSON output is unavailable, read the files referenced by the terminal output and the artifacts under openspec/changes/<change-id>/.
|
|
4382
|
-
4. If apply
|
|
4383
|
-
|
|
5406
|
+
4. If \`openspec/changes/<change-id>/.fet/figma-apply-instructions.md\` exists (or apply nextSteps mention Figma):
|
|
5407
|
+
- Read it and \`figma-stop.md\` in the same folder before any UI code.
|
|
5408
|
+
- Use Figma MCP/API to read every linked frame; briefly confirm design facts in your reply.
|
|
5409
|
+
- If Figma access fails or design details are unclear, stop and ask the user\u2014do not guess styles or implement UI.
|
|
5410
|
+
5. If \`openspec/changes/<change-id>/.fet/ui-display-contract.yaml\` exists (or apply nextSteps mention UI display contract):
|
|
5411
|
+
- Read it and \`ui-field-apply-instructions.md\` in the same folder before UI that binds API data.
|
|
5412
|
+
- Confirm \`displayFields\` from Figma; list API-only fields under \`omittedFromUi\`.
|
|
5413
|
+
- Do not render every OpenAPI property\u2014only fields in \`displayFields\`.
|
|
5414
|
+
6. If apply is blocked because required artifacts are missing, stop and suggest /prompts:fet-continue <change-id> or /prompts:fet-ff <change-id>.
|
|
5415
|
+
7. Implement pending tasks one by one:
|
|
4384
5416
|
- Keep code changes minimal and scoped to the task.
|
|
4385
5417
|
- Follow proposal, specs, design, and tasks.
|
|
4386
5418
|
- Mark each completed task checkbox in tasks.md from \`- [ ]\` to \`- [x]\`.
|
|
4387
5419
|
- Pause and ask if a task is ambiguous or reveals a design conflict.
|
|
4388
|
-
|
|
5420
|
+
8. After completing or pausing, summarize completed tasks, remaining tasks, and blockers.
|
|
4389
5421
|
|
|
4390
5422
|
Guardrails:
|
|
4391
5423
|
- Never skip reading OpenSpec artifacts before implementation.
|
|
5424
|
+
- When Figma links exist for this change, never implement or restyle UI without reading Figma first.
|
|
5425
|
+
- When ui-display-contract.yaml exists, API schemas are not UI checklists\u2014only displayFields may render.
|
|
4392
5426
|
- Do not mark a task complete until the code change is actually done.
|
|
4393
5427
|
- Do not run sync or archive from apply.`,
|
|
4394
5428
|
void 0,
|
|
@@ -4449,6 +5483,7 @@ Steps:
|
|
|
4449
5483
|
- Remove REMOVED requirements.
|
|
4450
5484
|
- Apply RENAMED requirements.
|
|
4451
5485
|
- Preserve main-spec content not mentioned in the delta.
|
|
5486
|
+
- Follow .codex/fet/spec-language.md: keep English normative text; carry over or refresh \`<!-- \u4E2D\u6587\uFF1A... -->\` for every touched Requirement in the same edit.
|
|
4452
5487
|
5. If no delta specs exist, state that there is nothing to merge.
|
|
4453
5488
|
6. Run the FET sync gate and strict OpenSpec validation:
|
|
4454
5489
|
\`\`\`sh
|
|
@@ -4686,6 +5721,7 @@ Steps:
|
|
|
4686
5721
|
4. Follow the native output. When it provides template, instruction, dependencies, and outputPath, use those fields.
|
|
4687
5722
|
5. Read dependency files before writing.
|
|
4688
5723
|
6. Create only that artifact file at outputPath. Do not copy context/rules wrapper text into the artifact.
|
|
5724
|
+
- If the artifact is specs/<capability>/spec.md, follow .codex/fet/spec-language.md: English Requirements/Scenario plus \`<!-- \u4E2D\u6587\uFF1A... -->\` after each Requirement title; update Chinese notes in the same edit when English changes.
|
|
4689
5725
|
7. Verify the file exists, then run:
|
|
4690
5726
|
\`\`\`sh
|
|
4691
5727
|
fet passthrough status --change <change-id>
|
|
@@ -4730,6 +5766,7 @@ Steps:
|
|
|
4730
5766
|
6. Follow the native continue output. When it provides template, instruction, dependencies, and outputPath, use those fields.
|
|
4731
5767
|
7. Read dependency files before writing.
|
|
4732
5768
|
8. Create the artifact file at outputPath. Do not copy context/rules wrapper text into the artifact; use those fields only as constraints.
|
|
5769
|
+
- If the artifact is specs/<capability>/spec.md, follow .codex/fet/spec-language.md: English Requirements/Scenario plus \`<!-- \u4E2D\u6587\uFF1A... -->\` after each Requirement title; update Chinese notes in the same edit when English changes.
|
|
4733
5770
|
9. Verify the file exists, then run:
|
|
4734
5771
|
\`\`\`sh
|
|
4735
5772
|
fet passthrough status --change <change-id>
|
|
@@ -4774,6 +5811,7 @@ Artifact rules:
|
|
|
4774
5811
|
- Follow the instruction field from OpenSpec/FET for each artifact.
|
|
4775
5812
|
- Use template as structure, filling it with concrete project-specific content.
|
|
4776
5813
|
- Do not copy context/rules wrapper text into artifact files.
|
|
5814
|
+
- For specs/<capability>/spec.md, follow .codex/fet/spec-language.md (English canonical + \`<!-- \u4E2D\u6587\uFF1A... -->\`; update Chinese notes in the same edit when English changes).
|
|
4777
5815
|
- Verify each file exists after writing.
|
|
4778
5816
|
|
|
4779
5817
|
Output:
|
|
@@ -4822,7 +5860,7 @@ var CodexAdapter = class {
|
|
|
4822
5860
|
adapterVersion = 1;
|
|
4823
5861
|
async detect(projectRoot) {
|
|
4824
5862
|
return {
|
|
4825
|
-
detected: await exists5(
|
|
5863
|
+
detected: await exists5(join21(projectRoot, ".codex")) || await exists5(join21(projectRoot, "AGENTS.md")),
|
|
4826
5864
|
reason: "Codex adapter is available for projects that use AGENTS.md"
|
|
4827
5865
|
};
|
|
4828
5866
|
}
|
|
@@ -4888,9 +5926,9 @@ var CodexAdapter = class {
|
|
|
4888
5926
|
};
|
|
4889
5927
|
function resolveTarget(projectRoot, file) {
|
|
4890
5928
|
if (file.root === "codex-home") {
|
|
4891
|
-
return
|
|
5929
|
+
return join21(resolveCodexHome(), file.path);
|
|
4892
5930
|
}
|
|
4893
|
-
return
|
|
5931
|
+
return join21(projectRoot, file.path);
|
|
4894
5932
|
}
|
|
4895
5933
|
function displayPathFor(file) {
|
|
4896
5934
|
if (file.root === "codex-home") {
|
|
@@ -4899,18 +5937,18 @@ function displayPathFor(file) {
|
|
|
4899
5937
|
return file.path;
|
|
4900
5938
|
}
|
|
4901
5939
|
function resolveCodexHome() {
|
|
4902
|
-
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ??
|
|
5940
|
+
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join21(homedir(), ".codex");
|
|
4903
5941
|
}
|
|
4904
5942
|
async function readExisting(path) {
|
|
4905
5943
|
try {
|
|
4906
|
-
return await
|
|
5944
|
+
return await readFile17(path, "utf8");
|
|
4907
5945
|
} catch {
|
|
4908
5946
|
return null;
|
|
4909
5947
|
}
|
|
4910
5948
|
}
|
|
4911
5949
|
async function exists5(path) {
|
|
4912
5950
|
try {
|
|
4913
|
-
await
|
|
5951
|
+
await stat10(path);
|
|
4914
5952
|
return true;
|
|
4915
5953
|
} catch {
|
|
4916
5954
|
return false;
|
|
@@ -4918,10 +5956,36 @@ async function exists5(path) {
|
|
|
4918
5956
|
}
|
|
4919
5957
|
|
|
4920
5958
|
// src/adapters/cursor/index.ts
|
|
4921
|
-
import { mkdir as mkdir9, readFile as
|
|
4922
|
-
import { dirname as dirname9, join as
|
|
5959
|
+
import { mkdir as mkdir9, readFile as readFile18, stat as stat11 } from "fs/promises";
|
|
5960
|
+
import { dirname as dirname9, join as join22 } from "path";
|
|
4923
5961
|
|
|
4924
5962
|
// src/adapters/cursor/templates.ts
|
|
5963
|
+
function cursorFigmaStopRuleFile(language = DEFAULT_LANGUAGE) {
|
|
5964
|
+
return {
|
|
5965
|
+
path: ".cursor/rules/fet-figma-stop.mdc",
|
|
5966
|
+
content: renderCursorFigmaStopRule(language)
|
|
5967
|
+
};
|
|
5968
|
+
}
|
|
5969
|
+
function cursorSpecLanguageRuleFile(language = DEFAULT_LANGUAGE) {
|
|
5970
|
+
return {
|
|
5971
|
+
path: ".cursor/rules/fet-spec-language.mdc",
|
|
5972
|
+
content: renderCursorSpecLanguageRule(language)
|
|
5973
|
+
};
|
|
5974
|
+
}
|
|
5975
|
+
function cursorUiDisplayContractRuleFile(language = DEFAULT_LANGUAGE) {
|
|
5976
|
+
return {
|
|
5977
|
+
path: ".cursor/rules/fet-ui-display-contract.mdc",
|
|
5978
|
+
content: renderCursorUiDisplayContractRule(language)
|
|
5979
|
+
};
|
|
5980
|
+
}
|
|
5981
|
+
function cursorRuleFiles(language = DEFAULT_LANGUAGE) {
|
|
5982
|
+
return [
|
|
5983
|
+
cursorRuleFile(language),
|
|
5984
|
+
cursorFigmaStopRuleFile(language),
|
|
5985
|
+
cursorUiDisplayContractRuleFile(language),
|
|
5986
|
+
cursorSpecLanguageRuleFile(language)
|
|
5987
|
+
];
|
|
5988
|
+
}
|
|
4925
5989
|
function cursorSkillFiles(language = DEFAULT_LANGUAGE) {
|
|
4926
5990
|
return FET_ADAPTER_COMMANDS.map((command) => ({
|
|
4927
5991
|
path: `.cursor/skills/fet-${command}/SKILL.md`,
|
|
@@ -4951,6 +6015,9 @@ ${languageInstruction(language)}
|
|
|
4951
6015
|
- openspec/config.yaml
|
|
4952
6016
|
- \u53EF\u7528\u65F6\u7684 GitNexus \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\u3002\u4F18\u5148\u7528\u5B83\u7F29\u5C0F\u8303\u56F4\uFF1B\u4E0D\u53EF\u7528\u65F6\u6309\u666E\u901A FET/OpenSpec \u5DE5\u4F5C\u6D41\u7EE7\u7EED\u3002
|
|
4953
6017
|
- \u5F53\u524D change \u76EE\u5F55\u4E0B\u7684 OpenSpec \u89C4\u5212\u4EA7\u7269\u3002
|
|
6018
|
+
- \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
|
|
6019
|
+
- \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
|
|
6020
|
+
- \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
|
|
4954
6021
|
|
|
4955
6022
|
\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
|
|
4956
6023
|
`
|
|
@@ -5019,6 +6086,12 @@ After installation, verify with \`gitnexus --version\`, then run \`fet graph ini
|
|
|
5019
6086
|
\u5B89\u88C5\u540E\u7528 \`gitnexus --version\` \u9A8C\u8BC1\uFF1B\u5408\u9002\u65F6\u7EE7\u7EED\u8FD0\u884C \`fet graph init\` \u548C \`fet graph handoff\`\u3002\u5982\u679C\u5B89\u88C5\u5931\u8D25\uFF0C\u6C47\u603B\u5931\u8D25\u547D\u4EE4\u548C\u4E0B\u4E00\u6B65\u4EBA\u5DE5\u5904\u7406\u5EFA\u8BAE\u3002`}
|
|
5020
6087
|
`;
|
|
5021
6088
|
}
|
|
6089
|
+
if (command === "apply") {
|
|
6090
|
+
return renderApplySkill(usage, language);
|
|
6091
|
+
}
|
|
6092
|
+
if (command === "propose" || command === "continue" || command === "ff") {
|
|
6093
|
+
return renderPlanningSkill(command, usage, language);
|
|
6094
|
+
}
|
|
5022
6095
|
return `<!-- FET:MANAGED
|
|
5023
6096
|
schemaVersion: 1
|
|
5024
6097
|
fetVersion: ${FET_VERSION}
|
|
@@ -5048,6 +6121,93 @@ ${usage}
|
|
|
5048
6121
|
\u6267\u884C\u524D\u8BF7\u786E\u8BA4\u5DF2\u9605\u8BFB AGENTS.md \u548C openspec/config.yaml\u3002
|
|
5049
6122
|
`;
|
|
5050
6123
|
}
|
|
6124
|
+
function renderPlanningSkill(command, usage, language) {
|
|
6125
|
+
const title = command === "propose" ? language === "en" ? "Create OpenSpec change and first artifact" : "\u521B\u5EFA OpenSpec change \u5E76\u751F\u6210\u9996\u4E2A\u89C4\u5212\u4EA7\u7269" : command === "ff" ? language === "en" ? "Fast-forward OpenSpec artifacts" : "\u5FEB\u8FDB\u751F\u6210 OpenSpec \u89C4\u5212\u4EA7\u7269" : language === "en" ? "Create next OpenSpec artifact" : "\u751F\u6210\u4E0B\u4E00\u4E2A OpenSpec \u89C4\u5212\u4EA7\u7269";
|
|
6126
|
+
return `<!-- FET:MANAGED
|
|
6127
|
+
schemaVersion: 1
|
|
6128
|
+
fetVersion: ${FET_VERSION}
|
|
6129
|
+
generator: cursor-adapter
|
|
6130
|
+
adapterVersion: 1
|
|
6131
|
+
command: ${usage}
|
|
6132
|
+
FET:END -->
|
|
6133
|
+
|
|
6134
|
+
---
|
|
6135
|
+
name: fet-${command}
|
|
6136
|
+
description: ${title}
|
|
6137
|
+
disable-model-invocation: true
|
|
6138
|
+
---
|
|
6139
|
+
|
|
6140
|
+
${renderIdeModelPolicy(command, language)}
|
|
6141
|
+
|
|
6142
|
+
${languageInstruction(language)}
|
|
6143
|
+
|
|
6144
|
+
\u8BF7\u5728\u7EC8\u7AEF\u4E2D\u6267\u884C\uFF1A
|
|
6145
|
+
|
|
6146
|
+
\`\`\`sh
|
|
6147
|
+
${usage}
|
|
6148
|
+
\`\`\`
|
|
6149
|
+
|
|
6150
|
+
${renderPlanningArtifactSpecBlock(language)}
|
|
6151
|
+
|
|
6152
|
+
${renderSpecArtifactGuardrail(language)}
|
|
6153
|
+
|
|
6154
|
+
${renderUiDisplayContractGuardrail(language)}
|
|
6155
|
+
|
|
6156
|
+
\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
|
|
6157
|
+
`;
|
|
6158
|
+
}
|
|
6159
|
+
function renderApplySkill(usage, language) {
|
|
6160
|
+
const uiContractBlock = language === "en" ? `If \`openspec/changes/<change-id>/.fet/ui-display-contract.yaml\` exists:
|
|
6161
|
+
|
|
6162
|
+
1. Read it and \`ui-field-apply-instructions.md\` in the same folder before UI that binds API data.
|
|
6163
|
+
2. Confirm \`displayFields\` from Figma; put API-only fields in \`omittedFromUi\`.
|
|
6164
|
+
3. Do not render every OpenAPI property\u2014only contracted display fields.` : `\u82E5\u5B58\u5728 \`openspec/changes/<change-id>/.fet/ui-display-contract.yaml\`\uFF1A
|
|
6165
|
+
|
|
6166
|
+
1. \u5728\u7ED1\u5B9A\u63A5\u53E3\u7684 UI \u5B9E\u65BD\u524D\u9605\u8BFB\u8BE5\u6587\u4EF6\u4E0E\u540C\u76EE\u5F55 \`ui-field-apply-instructions.md\`\u3002
|
|
6167
|
+
2. \u6309 Figma \u786E\u8BA4 \`displayFields\`\uFF1B\u4EC5\u63A5\u53E3\u6709\u7684\u5B57\u6BB5\u653E\u5165 \`omittedFromUi\`\u3002
|
|
6168
|
+
3. \u7981\u6B62\u6309 OpenAPI \u5168\u91CF\u5C55\u793A\u2014\u53EA\u6E32\u67D3\u5951\u7EA6\u4E2D\u7684 displayFields\u3002`;
|
|
6169
|
+
const figmaBlock = language === "en" ? `If \`fet apply\` output or the change has \`openspec/changes/<change-id>/.fet/figma-apply-instructions.md\`:
|
|
6170
|
+
|
|
6171
|
+
1. Read that file and \`figma-stop.md\` in the same folder before any UI code.
|
|
6172
|
+
2. Use Figma MCP/API to read every linked frame; do not invent colors, spacing, or layout.
|
|
6173
|
+
3. If Figma fails or design is unclear, stop and ask the user\u2014do not guess styles or mark UI tasks done.
|
|
6174
|
+
4. After design is confirmed, implement tasks from \`tasks.md\`.` : `\u82E5 \`fet apply\` \u8F93\u51FA\u6216 change \u4E2D\u5B58\u5728 \`openspec/changes/<change-id>/.fet/figma-apply-instructions.md\`\uFF1A
|
|
6175
|
+
|
|
6176
|
+
1. \u5728\u6539\u4EFB\u4F55 UI \u4EE3\u7801\u524D\uFF0C\u5148\u9605\u8BFB\u8BE5\u6587\u4EF6\u4E0E\u540C\u76EE\u5F55 \`figma-stop.md\`\u3002
|
|
6177
|
+
2. \u7528 Figma MCP/API \u8BFB\u53D6\u6BCF\u4E2A\u94FE\u63A5\u7684\u753B\u677F\uFF1B\u7981\u6B62\u81EA\u521B\u989C\u8272\u3001\u95F4\u8DDD\u3001\u5E03\u5C40\u3002
|
|
6178
|
+
3. Figma \u5931\u8D25\u6216\u8BBE\u8BA1\u4E0D\u6E05\u65F6\u7ACB\u5373\u505C\u6B62\u5E76\u5411\u7528\u6237\u63D0\u95EE\uFF0C\u4E0D\u8981\u731C\u6837\u5F0F\uFF0C\u4E0D\u8981\u628A UI \u4EFB\u52A1\u6807\u4E3A\u5B8C\u6210\u3002
|
|
6179
|
+
4. \u8BBE\u8BA1\u786E\u8BA4\u540E\uFF0C\u518D\u6309 \`tasks.md\` \u5B9E\u65BD\u4EFB\u52A1\u3002`;
|
|
6180
|
+
return `<!-- FET:MANAGED
|
|
6181
|
+
schemaVersion: 1
|
|
6182
|
+
fetVersion: ${FET_VERSION}
|
|
6183
|
+
generator: cursor-adapter
|
|
6184
|
+
adapterVersion: 1
|
|
6185
|
+
command: ${usage}
|
|
6186
|
+
FET:END -->
|
|
6187
|
+
|
|
6188
|
+
---
|
|
6189
|
+
name: fet-apply
|
|
6190
|
+
description: ${language === "en" ? "Implement OpenSpec tasks; require Figma when design links exist" : "\u5B9E\u65BD OpenSpec \u4EFB\u52A1\uFF1B\u6709 Figma \u94FE\u63A5\u65F6\u5FC5\u987B\u5148\u6309\u7A3F\u5B9E\u73B0"}
|
|
6191
|
+
disable-model-invocation: true
|
|
6192
|
+
---
|
|
6193
|
+
|
|
6194
|
+
${renderIdeModelPolicy("apply", language)}
|
|
6195
|
+
|
|
6196
|
+
${languageInstruction(language)}
|
|
6197
|
+
|
|
6198
|
+
\u8BF7\u5728\u7EC8\u7AEF\u4E2D\u6267\u884C\uFF1A
|
|
6199
|
+
|
|
6200
|
+
\`\`\`sh
|
|
6201
|
+
${usage}
|
|
6202
|
+
\`\`\`
|
|
6203
|
+
|
|
6204
|
+
${figmaBlock}
|
|
6205
|
+
|
|
6206
|
+
${uiContractBlock}
|
|
6207
|
+
|
|
6208
|
+
\u6267\u884C\u524D\u8BF7\u9605\u8BFB AGENTS.md\u3001openspec/config.yaml \u4E0E\u5F53\u524D change \u4E0B\u7684 OpenSpec \u4EA7\u7269\u3002
|
|
6209
|
+
`;
|
|
6210
|
+
}
|
|
5051
6211
|
|
|
5052
6212
|
// src/adapters/cursor/index.ts
|
|
5053
6213
|
var CursorAdapter = class {
|
|
@@ -5055,14 +6215,14 @@ var CursorAdapter = class {
|
|
|
5055
6215
|
adapterVersion = 1;
|
|
5056
6216
|
async detect(projectRoot) {
|
|
5057
6217
|
return {
|
|
5058
|
-
detected: await exists6(
|
|
6218
|
+
detected: await exists6(join22(projectRoot, ".cursor")),
|
|
5059
6219
|
reason: "Cursor adapter is available for any project"
|
|
5060
6220
|
};
|
|
5061
6221
|
}
|
|
5062
6222
|
async planInstall(_projectRoot, language) {
|
|
5063
6223
|
return {
|
|
5064
6224
|
tool: this.tool,
|
|
5065
|
-
files: [...cursorSkillFiles(language),
|
|
6225
|
+
files: [...cursorSkillFiles(language), ...cursorRuleFiles(language)].map((file) => ({
|
|
5066
6226
|
...file,
|
|
5067
6227
|
managed: true
|
|
5068
6228
|
}))
|
|
@@ -5072,7 +6232,7 @@ var CursorAdapter = class {
|
|
|
5072
6232
|
const written = [];
|
|
5073
6233
|
const skipped = [];
|
|
5074
6234
|
for (const file of plan.files) {
|
|
5075
|
-
const target =
|
|
6235
|
+
const target = join22(projectRoot, file.path);
|
|
5076
6236
|
const existing = await readExisting2(target);
|
|
5077
6237
|
if (existing && !existing.includes("FET:MANAGED") && !force) {
|
|
5078
6238
|
throw new FetError({
|
|
@@ -5095,7 +6255,7 @@ var CursorAdapter = class {
|
|
|
5095
6255
|
const plan = await this.planInstall(projectRoot);
|
|
5096
6256
|
const checks = [];
|
|
5097
6257
|
for (const file of plan.files) {
|
|
5098
|
-
const target =
|
|
6258
|
+
const target = join22(projectRoot, file.path);
|
|
5099
6259
|
const content = await readExisting2(target);
|
|
5100
6260
|
const managed = Boolean(content?.includes("FET:MANAGED"));
|
|
5101
6261
|
const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
|
|
@@ -5111,14 +6271,14 @@ var CursorAdapter = class {
|
|
|
5111
6271
|
};
|
|
5112
6272
|
async function readExisting2(path) {
|
|
5113
6273
|
try {
|
|
5114
|
-
return await
|
|
6274
|
+
return await readFile18(path, "utf8");
|
|
5115
6275
|
} catch {
|
|
5116
6276
|
return null;
|
|
5117
6277
|
}
|
|
5118
6278
|
}
|
|
5119
6279
|
async function exists6(path) {
|
|
5120
6280
|
try {
|
|
5121
|
-
await
|
|
6281
|
+
await stat11(path);
|
|
5122
6282
|
return true;
|
|
5123
6283
|
} catch {
|
|
5124
6284
|
return false;
|
|
@@ -5130,13 +6290,13 @@ import { execFile as execFile4 } from "child_process";
|
|
|
5130
6290
|
import { promisify as promisify4 } from "util";
|
|
5131
6291
|
|
|
5132
6292
|
// src/openspec/inspector.ts
|
|
5133
|
-
import { readdir as
|
|
5134
|
-
import { join as
|
|
6293
|
+
import { readdir as readdir5, stat as stat12 } from "fs/promises";
|
|
6294
|
+
import { join as join23 } from "path";
|
|
5135
6295
|
async function inspectOpenSpecProject(projectRoot) {
|
|
5136
|
-
const openspecPath =
|
|
5137
|
-
const changesPath =
|
|
5138
|
-
const legacyArchivePath =
|
|
5139
|
-
const changesArchivePath =
|
|
6296
|
+
const openspecPath = join23(projectRoot, "openspec");
|
|
6297
|
+
const changesPath = join23(openspecPath, "changes");
|
|
6298
|
+
const legacyArchivePath = join23(openspecPath, "archive");
|
|
6299
|
+
const changesArchivePath = join23(changesPath, "archive");
|
|
5140
6300
|
return {
|
|
5141
6301
|
exists: await exists7(openspecPath),
|
|
5142
6302
|
changes: await listDirectories(changesPath, { exclude: ["archive"] }),
|
|
@@ -5144,13 +6304,13 @@ async function inspectOpenSpecProject(projectRoot) {
|
|
|
5144
6304
|
};
|
|
5145
6305
|
}
|
|
5146
6306
|
async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
5147
|
-
const changePath =
|
|
5148
|
-
const tasksPath =
|
|
5149
|
-
const specsPath =
|
|
6307
|
+
const changePath = join23(projectRoot, "openspec", "changes", changeId);
|
|
6308
|
+
const tasksPath = join23(changePath, "tasks.md");
|
|
6309
|
+
const specsPath = join23(changePath, "specs");
|
|
5150
6310
|
return {
|
|
5151
6311
|
changeId,
|
|
5152
6312
|
exists: await exists7(changePath),
|
|
5153
|
-
hasProposal: await exists7(
|
|
6313
|
+
hasProposal: await exists7(join23(changePath, "proposal.md")),
|
|
5154
6314
|
hasTasks: await exists7(tasksPath),
|
|
5155
6315
|
hasSpecs: await exists7(specsPath),
|
|
5156
6316
|
tasksPath,
|
|
@@ -5159,7 +6319,7 @@ async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
|
5159
6319
|
}
|
|
5160
6320
|
async function listDirectories(path, options = {}) {
|
|
5161
6321
|
try {
|
|
5162
|
-
const entries = await
|
|
6322
|
+
const entries = await readdir5(path, { withFileTypes: true });
|
|
5163
6323
|
const excluded = new Set(options.exclude ?? []);
|
|
5164
6324
|
return entries.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).map((entry) => entry.name);
|
|
5165
6325
|
} catch {
|
|
@@ -5168,7 +6328,7 @@ async function listDirectories(path, options = {}) {
|
|
|
5168
6328
|
}
|
|
5169
6329
|
async function exists7(path) {
|
|
5170
6330
|
try {
|
|
5171
|
-
await
|
|
6331
|
+
await stat12(path);
|
|
5172
6332
|
return true;
|
|
5173
6333
|
} catch {
|
|
5174
6334
|
return false;
|
|
@@ -5351,13 +6511,13 @@ function escapeRegExp(value) {
|
|
|
5351
6511
|
}
|
|
5352
6512
|
|
|
5353
6513
|
// src/scanner/routes.ts
|
|
5354
|
-
import { readdir as
|
|
5355
|
-
import { join as
|
|
6514
|
+
import { readdir as readdir6, stat as stat13 } from "fs/promises";
|
|
6515
|
+
import { join as join24, relative as relative4, sep } from "path";
|
|
5356
6516
|
async function scanRoutes(projectRoot) {
|
|
5357
6517
|
const candidates = ["src/routes", "src/pages", "app", "pages"];
|
|
5358
6518
|
const routes = [];
|
|
5359
6519
|
for (const candidate of candidates) {
|
|
5360
|
-
const root =
|
|
6520
|
+
const root = join24(projectRoot, candidate);
|
|
5361
6521
|
if (!await exists8(root)) {
|
|
5362
6522
|
continue;
|
|
5363
6523
|
}
|
|
@@ -5366,8 +6526,8 @@ async function scanRoutes(projectRoot) {
|
|
|
5366
6526
|
continue;
|
|
5367
6527
|
}
|
|
5368
6528
|
routes.push({
|
|
5369
|
-
path: inferRoutePath(
|
|
5370
|
-
source:
|
|
6529
|
+
path: inferRoutePath(relative4(root, file)),
|
|
6530
|
+
source: relative4(projectRoot, file).split(sep).join("/"),
|
|
5371
6531
|
inferred: true,
|
|
5372
6532
|
confidence: "medium"
|
|
5373
6533
|
});
|
|
@@ -5382,10 +6542,10 @@ function inferRoutePath(relativePath) {
|
|
|
5382
6542
|
return `/${withoutIndex}`.replace(/\/+/g, "/");
|
|
5383
6543
|
}
|
|
5384
6544
|
async function listFiles(root) {
|
|
5385
|
-
const entries = await
|
|
6545
|
+
const entries = await readdir6(root, { withFileTypes: true });
|
|
5386
6546
|
const files = [];
|
|
5387
6547
|
for (const entry of entries) {
|
|
5388
|
-
const path =
|
|
6548
|
+
const path = join24(root, entry.name);
|
|
5389
6549
|
if (entry.isDirectory()) {
|
|
5390
6550
|
files.push(...await listFiles(path));
|
|
5391
6551
|
} else {
|
|
@@ -5396,7 +6556,7 @@ async function listFiles(root) {
|
|
|
5396
6556
|
}
|
|
5397
6557
|
async function exists8(path) {
|
|
5398
6558
|
try {
|
|
5399
|
-
await
|
|
6559
|
+
await stat13(path);
|
|
5400
6560
|
return true;
|
|
5401
6561
|
} catch {
|
|
5402
6562
|
return false;
|
|
@@ -5553,9 +6713,9 @@ async function createCommandContext(command, options) {
|
|
|
5553
6713
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
5554
6714
|
|
|
5555
6715
|
// src/update/check.ts
|
|
5556
|
-
import { mkdir as mkdir10, readFile as
|
|
6716
|
+
import { mkdir as mkdir10, readFile as readFile19, writeFile } from "fs/promises";
|
|
5557
6717
|
import { homedir as homedir2 } from "os";
|
|
5558
|
-
import { dirname as dirname10, join as
|
|
6718
|
+
import { dirname as dirname10, join as join25 } from "path";
|
|
5559
6719
|
var DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
5560
6720
|
function getFetUpdateCheckMode(env = process.env) {
|
|
5561
6721
|
const value = env.FET_UPDATE_CHECK?.trim().toLowerCase();
|
|
@@ -5628,11 +6788,11 @@ function formatFetUpdateWarning(availability, language) {
|
|
|
5628
6788
|
}
|
|
5629
6789
|
function cachePath() {
|
|
5630
6790
|
const home = process.env.FET_UPDATE_CHECK_CACHE_HOME?.trim() || homedir2();
|
|
5631
|
-
return
|
|
6791
|
+
return join25(home, ".fet", "update-check-cache.json");
|
|
5632
6792
|
}
|
|
5633
6793
|
async function readUpdateCheckCache() {
|
|
5634
6794
|
try {
|
|
5635
|
-
const raw = await
|
|
6795
|
+
const raw = await readFile19(cachePath(), "utf8");
|
|
5636
6796
|
const parsed = JSON.parse(raw);
|
|
5637
6797
|
if (typeof parsed.latestVersion !== "string" || typeof parsed.checkedAt !== "string") {
|
|
5638
6798
|
return null;
|