@nick848/fet 1.1.6 → 1.1.7
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 +4 -0
- package/README_en.md +4 -0
- package/dist/cli/index.js +372 -88
- 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 relative4 = env.FET_GITNEXUS_GRAPH_PATH?.trim() || DEFAULT_GRAPH_PATH;
|
|
192
|
+
const graphPath = join5(projectRoot, relative4);
|
|
193
193
|
try {
|
|
194
194
|
const info = await stat2(graphPath);
|
|
195
195
|
return {
|
|
196
|
-
graphPath:
|
|
196
|
+
graphPath: relative4,
|
|
197
197
|
graphExists: true,
|
|
198
198
|
lastIndexedAt: info.mtime.toISOString()
|
|
199
199
|
};
|
|
200
200
|
} catch {
|
|
201
201
|
return {
|
|
202
|
-
graphPath:
|
|
202
|
+
graphPath: relative4,
|
|
203
203
|
graphExists: false,
|
|
204
204
|
lastIndexedAt: null
|
|
205
205
|
};
|
|
@@ -2079,6 +2079,10 @@ 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
|
+
onUncertainty: "stop_and_ask"
|
|
2082
2086
|
}
|
|
2083
2087
|
}
|
|
2084
2088
|
});
|
|
@@ -2239,6 +2243,118 @@ fet verify --done --change ${changeId}
|
|
|
2239
2243
|
`;
|
|
2240
2244
|
}
|
|
2241
2245
|
|
|
2246
|
+
// src/templates/figma-guard.ts
|
|
2247
|
+
var FIGMA_URL_PATTERN = /https?:\/\/(?:www\.)?figma\.com\/(?:file|design|proto)\/[^\s)\]"'<>]+/gi;
|
|
2248
|
+
function figmaStopHandoffRelativePath(changeId) {
|
|
2249
|
+
return `openspec/changes/${changeId}/.fet/figma-stop.md`;
|
|
2250
|
+
}
|
|
2251
|
+
function renderFigmaStopProtocolBody(language) {
|
|
2252
|
+
if (language === "en") {
|
|
2253
|
+
return `## Stop immediately (do not write or change UI code) when
|
|
2254
|
+
|
|
2255
|
+
- Figma MCP/API errors, 403, timeout, or empty node/selection
|
|
2256
|
+
- You cannot resolve the frame or node referenced in the change
|
|
2257
|
+
- Color, typography, spacing, radius, shadow, or layout cannot be determined from the design input
|
|
2258
|
+
- Component instances do not map to an agreed code component and the user has not chosen one
|
|
2259
|
+
- Interaction states (hover, disabled, loading, empty) are missing from the design
|
|
2260
|
+
|
|
2261
|
+
## After stopping, ask the user
|
|
2262
|
+
|
|
2263
|
+
1. What failed (permission, node, frame, token type)
|
|
2264
|
+
2. What you need: **viewable link**, **screenshot + short notes**, or **explicit permission to infer** (which rule)
|
|
2265
|
+
3. Do **not** continue UI implementation until the user clearly says to continue or answers the question
|
|
2266
|
+
|
|
2267
|
+
## While uncertain
|
|
2268
|
+
|
|
2269
|
+
- Do not fill gaps with "common UI patterns" or guessed pixel values
|
|
2270
|
+
- Prefer showing the blocking question over partial implementation`;
|
|
2271
|
+
}
|
|
2272
|
+
return `## \u5FC5\u987B\u7ACB\u5373\u505C\u6B62\uFF08\u4E0D\u5F97\u7EE7\u7EED\u7F16\u5199\u6216\u4FEE\u6539 UI \u4EE3\u7801\uFF09\u5F53
|
|
2273
|
+
|
|
2274
|
+
- Figma MCP/API \u62A5\u9519\u3001403\u3001\u8D85\u65F6\uFF0C\u6216\u8282\u70B9/\u9009\u533A\u4E3A\u7A7A
|
|
2275
|
+
- \u65E0\u6CD5\u89E3\u6790 change \u4E2D\u5F15\u7528\u7684\u753B\u677F\u6216\u8282\u70B9
|
|
2276
|
+
- \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
|
|
2277
|
+
- \u7EC4\u4EF6\u5B9E\u4F8B\u65E0\u6CD5\u5BF9\u5E94\u5230\u5DF2\u7EA6\u5B9A\u7684\u4EE3\u7801\u7EC4\u4EF6\uFF0C\u4E14\u7528\u6237\u672A\u6307\u5B9A
|
|
2278
|
+
- \u4EA4\u4E92\u72B6\u6001\uFF08hover\u3001disabled\u3001loading\u3001\u7A7A\u6001\u7B49\uFF09\u5728\u8BBE\u8BA1\u7A3F\u4E2D\u7F3A\u5931
|
|
2279
|
+
|
|
2280
|
+
## \u505C\u6B62\u540E\u5FC5\u987B\u8BE2\u95EE\u7528\u6237
|
|
2281
|
+
|
|
2282
|
+
1. \u5361\u5728\u54EA\u4E00\u6B65\uFF08\u6743\u9650\u3001\u8282\u70B9\u3001\u753B\u677F\u3001\u54EA\u7C7B token\uFF09
|
|
2283
|
+
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**
|
|
2284
|
+
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
|
|
2285
|
+
|
|
2286
|
+
## \u5B58\u5728\u4E0D\u786E\u5B9A\u6027\u65F6
|
|
2287
|
+
|
|
2288
|
+
- \u4E0D\u8981\u7528\u300C\u5E38\u89C1 UI \u505A\u6CD5\u300D\u6216\u731C\u6D4B\u7684\u50CF\u7D20\u503C\u586B\u8865\u7A7A\u767D
|
|
2289
|
+
- \u4F18\u5148\u5411\u7528\u6237\u63D0\u95EE\uFF0C\u800C\u4E0D\u662F\u5148\u5199\u4E00\u7248\u6837\u5F0F\u518D\u6539`;
|
|
2290
|
+
}
|
|
2291
|
+
function renderCursorFigmaStopRule(language) {
|
|
2292
|
+
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";
|
|
2293
|
+
return `<!-- FET:MANAGED
|
|
2294
|
+
schemaVersion: 1
|
|
2295
|
+
fetVersion: ${FET_VERSION}
|
|
2296
|
+
generator: cursor-adapter
|
|
2297
|
+
adapterVersion: 1
|
|
2298
|
+
FET:END -->
|
|
2299
|
+
|
|
2300
|
+
---
|
|
2301
|
+
description: ${description}
|
|
2302
|
+
alwaysApply: false
|
|
2303
|
+
---
|
|
2304
|
+
|
|
2305
|
+
${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"}
|
|
2306
|
+
|
|
2307
|
+
${renderFigmaStopProtocolBody(language)}
|
|
2308
|
+
|
|
2309
|
+
${language === "en" ? "If this change has `openspec/changes/<change-id>/.fet/figma-stop.md`, read it for detected links and repeat the same stop rules." : "\u82E5\u5F53\u524D change \u5B58\u5728 `openspec/changes/<change-id>/.fet/figma-stop.md`\uFF0C\u8BF7\u5148\u9605\u8BFB\u5176\u4E2D\u7684\u94FE\u63A5\u5217\u8868\uFF0C\u5E76\u9075\u5B88\u76F8\u540C\u7684\u505C\u6B62\u89C4\u5219\u3002"}
|
|
2310
|
+
`;
|
|
2311
|
+
}
|
|
2312
|
+
function renderCodexFigmaStopGuide(language) {
|
|
2313
|
+
return `<!-- FET:MANAGED
|
|
2314
|
+
schemaVersion: 1
|
|
2315
|
+
fetVersion: ${FET_VERSION}
|
|
2316
|
+
generator: codex-adapter
|
|
2317
|
+
adapterVersion: 1
|
|
2318
|
+
FET:END -->
|
|
2319
|
+
|
|
2320
|
+
# Figma stop protocol (Codex)
|
|
2321
|
+
|
|
2322
|
+
${renderFigmaStopProtocolBody(language)}
|
|
2323
|
+
`;
|
|
2324
|
+
}
|
|
2325
|
+
function renderChangeFigmaStopHandoff(options) {
|
|
2326
|
+
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";
|
|
2327
|
+
const sourceList = options.sources.length ? options.sources.map((source) => `- ${source}`).join("\n") : options.language === "en" ? "- n/a" : "- \u65E0";
|
|
2328
|
+
const title = options.language === "en" ? "Figma guard (this change)" : "Figma \u5B88\u536B\uFF08\u672C change\uFF09";
|
|
2329
|
+
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";
|
|
2330
|
+
return `---
|
|
2331
|
+
schemaVersion: 1
|
|
2332
|
+
fetVersion: ${FET_VERSION}
|
|
2333
|
+
generatedAt: ${options.generatedAt}
|
|
2334
|
+
changeId: ${options.changeId}
|
|
2335
|
+
purpose: figma-stop
|
|
2336
|
+
---
|
|
2337
|
+
|
|
2338
|
+
# ${title}
|
|
2339
|
+
|
|
2340
|
+
${intro}
|
|
2341
|
+
|
|
2342
|
+
## Detected Figma links
|
|
2343
|
+
|
|
2344
|
+
${linkList}
|
|
2345
|
+
|
|
2346
|
+
## Sources
|
|
2347
|
+
|
|
2348
|
+
${sourceList}
|
|
2349
|
+
|
|
2350
|
+
${renderFigmaStopProtocolBody(options.language)}
|
|
2351
|
+
`;
|
|
2352
|
+
}
|
|
2353
|
+
function renderFigmaStopNextStep(changeId, language) {
|
|
2354
|
+
const path = figmaStopHandoffRelativePath(changeId);
|
|
2355
|
+
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`;
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2242
2358
|
// src/commands/update-context.ts
|
|
2243
2359
|
async function updateContextCommand(ctx) {
|
|
2244
2360
|
let contextResult = { warnings: [] };
|
|
@@ -2400,8 +2516,128 @@ async function exists4(path) {
|
|
|
2400
2516
|
}
|
|
2401
2517
|
|
|
2402
2518
|
// src/commands/proxy.ts
|
|
2403
|
-
import { readFile as
|
|
2404
|
-
import { join as
|
|
2519
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
2520
|
+
import { join as join18 } from "path";
|
|
2521
|
+
|
|
2522
|
+
// src/figma-guard.ts
|
|
2523
|
+
import { readdir as readdir3, readFile as readFile11, stat as stat7 } from "fs/promises";
|
|
2524
|
+
import { join as join16, relative as relative2 } from "path";
|
|
2525
|
+
import { parseDocument as parseDocument2 } from "yaml";
|
|
2526
|
+
var DEFAULT_CONFIG = {
|
|
2527
|
+
enabled: true,
|
|
2528
|
+
onUncertainty: "stop_and_ask"
|
|
2529
|
+
};
|
|
2530
|
+
async function loadFigmaGuardConfig(projectRoot) {
|
|
2531
|
+
try {
|
|
2532
|
+
const raw = await readFile11(join16(projectRoot, "openspec", "config.yaml"), "utf8");
|
|
2533
|
+
const doc = parseDocument2(raw);
|
|
2534
|
+
const fetNode = doc.get("fet", true);
|
|
2535
|
+
const node = fetNode?.get?.("figmaGuard");
|
|
2536
|
+
if (!node || typeof node.get !== "function") {
|
|
2537
|
+
return DEFAULT_CONFIG;
|
|
2538
|
+
}
|
|
2539
|
+
const enabled = node.get("enabled");
|
|
2540
|
+
return {
|
|
2541
|
+
enabled: enabled === void 0 ? true : Boolean(enabled),
|
|
2542
|
+
onUncertainty: "stop_and_ask"
|
|
2543
|
+
};
|
|
2544
|
+
} catch {
|
|
2545
|
+
return DEFAULT_CONFIG;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
function extractFigmaUrls(content) {
|
|
2549
|
+
const matches = content.match(FIGMA_URL_PATTERN) ?? [];
|
|
2550
|
+
return [...new Set(matches.map((url) => url.replace(/[.,;]+$/, "")))];
|
|
2551
|
+
}
|
|
2552
|
+
async function collectFigmaUrlsFromChange(projectRoot, changeId) {
|
|
2553
|
+
const changePath = join16(projectRoot, "openspec", "changes", changeId);
|
|
2554
|
+
const urls = /* @__PURE__ */ new Set();
|
|
2555
|
+
const sources = [];
|
|
2556
|
+
const candidates = ["proposal.md", "tasks.md", "design.md"];
|
|
2557
|
+
for (const name of candidates) {
|
|
2558
|
+
const filePath = join16(changePath, name);
|
|
2559
|
+
const content = await readOptional3(filePath);
|
|
2560
|
+
if (!content) {
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2563
|
+
const found = extractFigmaUrls(content);
|
|
2564
|
+
if (found.length) {
|
|
2565
|
+
sources.push(`openspec/changes/${changeId}/${name}`);
|
|
2566
|
+
for (const url of found) {
|
|
2567
|
+
urls.add(url);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
const specsPath = join16(changePath, "specs");
|
|
2572
|
+
for (const filePath of await listMarkdownFiles(specsPath)) {
|
|
2573
|
+
const content = await readOptional3(filePath);
|
|
2574
|
+
if (!content) {
|
|
2575
|
+
continue;
|
|
2576
|
+
}
|
|
2577
|
+
const found = extractFigmaUrls(content);
|
|
2578
|
+
if (found.length) {
|
|
2579
|
+
sources.push(relative2(projectRoot, filePath).replaceAll("\\", "/"));
|
|
2580
|
+
for (const url of found) {
|
|
2581
|
+
urls.add(url);
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
return { urls: [...urls], sources };
|
|
2586
|
+
}
|
|
2587
|
+
async function ensureChangeFigmaStopHandoff(options) {
|
|
2588
|
+
const config = options.enabled === void 0 ? await loadFigmaGuardConfig(options.projectRoot) : { enabled: options.enabled, onUncertainty: "stop_and_ask" };
|
|
2589
|
+
if (!config.enabled) {
|
|
2590
|
+
return null;
|
|
2591
|
+
}
|
|
2592
|
+
const { urls, sources } = await collectFigmaUrlsFromChange(options.projectRoot, options.changeId);
|
|
2593
|
+
if (!urls.length) {
|
|
2594
|
+
return null;
|
|
2595
|
+
}
|
|
2596
|
+
const relativePath = figmaStopHandoffRelativePath(options.changeId);
|
|
2597
|
+
const absolutePath = join16(options.projectRoot, relativePath);
|
|
2598
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2599
|
+
const content = renderChangeFigmaStopHandoff({
|
|
2600
|
+
changeId: options.changeId,
|
|
2601
|
+
generatedAt,
|
|
2602
|
+
urls,
|
|
2603
|
+
sources,
|
|
2604
|
+
language: options.language
|
|
2605
|
+
});
|
|
2606
|
+
const existing = await readOptional3(absolutePath);
|
|
2607
|
+
const written = existing !== content;
|
|
2608
|
+
if (written) {
|
|
2609
|
+
await atomicWrite(absolutePath, content);
|
|
2610
|
+
}
|
|
2611
|
+
return { path: relativePath, written, urls, sources };
|
|
2612
|
+
}
|
|
2613
|
+
async function listMarkdownFiles(root) {
|
|
2614
|
+
const files = [];
|
|
2615
|
+
await walk(root, files);
|
|
2616
|
+
return files;
|
|
2617
|
+
}
|
|
2618
|
+
async function walk(dir, files) {
|
|
2619
|
+
try {
|
|
2620
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
2621
|
+
for (const entry of entries) {
|
|
2622
|
+
const fullPath = join16(dir, entry.name);
|
|
2623
|
+
if (entry.isDirectory()) {
|
|
2624
|
+
await walk(fullPath, files);
|
|
2625
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2626
|
+
files.push(fullPath);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
} catch {
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
async function readOptional3(path) {
|
|
2634
|
+
try {
|
|
2635
|
+
await stat7(path);
|
|
2636
|
+
return await readFile11(path, "utf8");
|
|
2637
|
+
} catch {
|
|
2638
|
+
return null;
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2405
2641
|
|
|
2406
2642
|
// src/state/project.ts
|
|
2407
2643
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -2430,8 +2666,8 @@ async function git(cwd, args) {
|
|
|
2430
2666
|
}
|
|
2431
2667
|
|
|
2432
2668
|
// src/state/store.ts
|
|
2433
|
-
import { mkdir as mkdir6, readFile as
|
|
2434
|
-
import { join as
|
|
2669
|
+
import { mkdir as mkdir6, readFile as readFile12 } from "fs/promises";
|
|
2670
|
+
import { join as join17 } from "path";
|
|
2435
2671
|
|
|
2436
2672
|
// src/language.ts
|
|
2437
2673
|
var DEFAULT_LANGUAGE = "zh-CN";
|
|
@@ -2549,7 +2785,7 @@ var StateStore = class {
|
|
|
2549
2785
|
project;
|
|
2550
2786
|
async readGlobal() {
|
|
2551
2787
|
try {
|
|
2552
|
-
const value = JSON.parse(await
|
|
2788
|
+
const value = JSON.parse(await readFile12(this.globalPath(), "utf8"));
|
|
2553
2789
|
assertGlobalState(value);
|
|
2554
2790
|
return value;
|
|
2555
2791
|
} catch (error) {
|
|
@@ -2564,13 +2800,13 @@ var StateStore = class {
|
|
|
2564
2800
|
}
|
|
2565
2801
|
async writeGlobal(state) {
|
|
2566
2802
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2567
|
-
await mkdir6(
|
|
2803
|
+
await mkdir6(join17(this.projectRoot, "openspec"), { recursive: true });
|
|
2568
2804
|
await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
|
|
2569
2805
|
`);
|
|
2570
2806
|
}
|
|
2571
2807
|
async readChange(changeId) {
|
|
2572
2808
|
try {
|
|
2573
|
-
const value = JSON.parse(await
|
|
2809
|
+
const value = JSON.parse(await readFile12(this.changePath(changeId), "utf8"));
|
|
2574
2810
|
assertChangeState(value);
|
|
2575
2811
|
return value;
|
|
2576
2812
|
} catch (error) {
|
|
@@ -2585,15 +2821,15 @@ var StateStore = class {
|
|
|
2585
2821
|
}
|
|
2586
2822
|
async writeChange(state) {
|
|
2587
2823
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2588
|
-
await mkdir6(
|
|
2824
|
+
await mkdir6(join17(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
|
|
2589
2825
|
await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
|
|
2590
2826
|
`);
|
|
2591
2827
|
}
|
|
2592
2828
|
globalPath() {
|
|
2593
|
-
return
|
|
2829
|
+
return join17(this.projectRoot, "openspec", "fet-state.json");
|
|
2594
2830
|
}
|
|
2595
2831
|
changePath(changeId) {
|
|
2596
|
-
return
|
|
2832
|
+
return join17(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
|
|
2597
2833
|
}
|
|
2598
2834
|
};
|
|
2599
2835
|
function isNotFound(error) {
|
|
@@ -2601,11 +2837,11 @@ function isNotFound(error) {
|
|
|
2601
2837
|
}
|
|
2602
2838
|
|
|
2603
2839
|
// src/state/tasks.ts
|
|
2604
|
-
import { readFile as
|
|
2840
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
2605
2841
|
async function readCompletedTaskIds(tasksPath) {
|
|
2606
2842
|
let content;
|
|
2607
2843
|
try {
|
|
2608
|
-
content = await
|
|
2844
|
+
content = await readFile13(tasksPath, "utf8");
|
|
2609
2845
|
} catch {
|
|
2610
2846
|
return [];
|
|
2611
2847
|
}
|
|
@@ -2754,21 +2990,31 @@ async function applyWorkflowCommand(ctx, args) {
|
|
|
2754
2990
|
exitCode: instructions.exitCode,
|
|
2755
2991
|
phaseStatus: "in_progress"
|
|
2756
2992
|
});
|
|
2993
|
+
const figmaGuard = await ensureChangeFigmaStopHandoff({
|
|
2994
|
+
projectRoot: ctx.projectRoot,
|
|
2995
|
+
changeId,
|
|
2996
|
+
language: ctx.language
|
|
2997
|
+
});
|
|
2998
|
+
const applyNextSteps = [
|
|
2999
|
+
`Read openspec/changes/${changeId}/tasks.md and the instructions output.`,
|
|
3000
|
+
"Implement pending tasks and update task checkboxes only after the work is done.",
|
|
3001
|
+
`Run fet verify --change ${changeId}`
|
|
3002
|
+
];
|
|
3003
|
+
if (figmaGuard) {
|
|
3004
|
+
applyNextSteps.unshift(renderFigmaStopNextStep(changeId, ctx.language));
|
|
3005
|
+
}
|
|
2757
3006
|
ctx.output.result({
|
|
2758
3007
|
ok: true,
|
|
2759
3008
|
command: "apply",
|
|
2760
3009
|
summary: `fet apply prepared implementation instructions for change "${changeId}".`,
|
|
2761
3010
|
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
|
-
],
|
|
3011
|
+
nextSteps: applyNextSteps,
|
|
2767
3012
|
data: {
|
|
2768
3013
|
changeId,
|
|
2769
3014
|
instructions: instructions.data,
|
|
2770
3015
|
status,
|
|
2771
|
-
graphContext: runState.graphContext
|
|
3016
|
+
graphContext: runState.graphContext,
|
|
3017
|
+
figmaGuard: figmaGuard ?? void 0
|
|
2772
3018
|
}
|
|
2773
3019
|
});
|
|
2774
3020
|
});
|
|
@@ -2781,16 +3027,25 @@ async function exploreWorkflowCommand(ctx, args) {
|
|
|
2781
3027
|
args: openSpecArgs,
|
|
2782
3028
|
changeId
|
|
2783
3029
|
});
|
|
3030
|
+
const figmaGuard = changeId ? await ensureChangeFigmaStopHandoff({
|
|
3031
|
+
projectRoot: ctx.projectRoot,
|
|
3032
|
+
changeId,
|
|
3033
|
+
language: ctx.language
|
|
3034
|
+
}) : null;
|
|
3035
|
+
const exploreNextSteps = [
|
|
3036
|
+
"Discuss the requirement, constraints, and acceptance criteria with the user.",
|
|
3037
|
+
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."
|
|
3038
|
+
];
|
|
3039
|
+
if (figmaGuard) {
|
|
3040
|
+
exploreNextSteps.unshift(renderFigmaStopNextStep(changeId, ctx.language));
|
|
3041
|
+
}
|
|
2784
3042
|
ctx.output.result({
|
|
2785
3043
|
ok: true,
|
|
2786
3044
|
command: "explore",
|
|
2787
3045
|
summary: "fet explore is an IDE-guided workflow for shaping OpenSpec changes.",
|
|
2788
3046
|
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 }
|
|
3047
|
+
nextSteps: exploreNextSteps,
|
|
3048
|
+
data: { changeId, args: openSpecArgs, graphContext, figmaGuard: figmaGuard ?? void 0 }
|
|
2794
3049
|
});
|
|
2795
3050
|
}
|
|
2796
3051
|
async function syncWorkflowCommand(ctx, args) {
|
|
@@ -2911,23 +3166,33 @@ async function artifactWorkflowCommand(ctx, command, args) {
|
|
|
2911
3166
|
exitCode: instructions.exitCode
|
|
2912
3167
|
});
|
|
2913
3168
|
const status = await readOpenSpecStatus(ctx, changeId);
|
|
3169
|
+
const figmaGuard = await ensureChangeFigmaStopHandoff({
|
|
3170
|
+
projectRoot: ctx.projectRoot,
|
|
3171
|
+
changeId,
|
|
3172
|
+
language: ctx.language
|
|
3173
|
+
});
|
|
3174
|
+
const planningNextSteps = [
|
|
3175
|
+
`Create or update openspec/changes/${changeId}/${resolveOutputPath(status, artifactId)}`,
|
|
3176
|
+
"Review the artifact with the user before generating the next planning file.",
|
|
3177
|
+
`Run fet passthrough status --change ${changeId}`,
|
|
3178
|
+
status.isComplete ? `Run fet apply --change ${changeId}` : `Run fet continue --change ${changeId} when ready for the next artifact`
|
|
3179
|
+
];
|
|
3180
|
+
if (figmaGuard) {
|
|
3181
|
+
planningNextSteps.unshift(renderFigmaStopNextStep(changeId, ctx.language));
|
|
3182
|
+
}
|
|
2914
3183
|
ctx.output.result({
|
|
2915
3184
|
ok: true,
|
|
2916
3185
|
command,
|
|
2917
3186
|
summary: `fet ${command} prepared OpenSpec artifact "${artifactId}" for change "${changeId}".`,
|
|
2918
3187
|
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
|
-
],
|
|
3188
|
+
nextSteps: planningNextSteps,
|
|
2925
3189
|
data: {
|
|
2926
3190
|
changeId,
|
|
2927
3191
|
artifactId,
|
|
2928
3192
|
instructions: instructions.data,
|
|
2929
3193
|
status,
|
|
2930
|
-
graphContext: runState.graphContext
|
|
3194
|
+
graphContext: runState.graphContext,
|
|
3195
|
+
figmaGuard: figmaGuard ?? void 0
|
|
2931
3196
|
}
|
|
2932
3197
|
});
|
|
2933
3198
|
});
|
|
@@ -3133,8 +3398,8 @@ async function createChangelogEntry(projectRoot, changeId) {
|
|
|
3133
3398
|
};
|
|
3134
3399
|
}
|
|
3135
3400
|
async function appendChangelog(projectRoot, entry) {
|
|
3136
|
-
const changelogPath =
|
|
3137
|
-
const existing = await
|
|
3401
|
+
const changelogPath = join18(projectRoot, "CHANGELOG.md");
|
|
3402
|
+
const existing = await readOptional4(changelogPath);
|
|
3138
3403
|
const legacyContentLabel = "\u66F4\u65B0\u5185\u5BB9";
|
|
3139
3404
|
const block = `updateTime: ${entry.updateTime}
|
|
3140
3405
|
changeRequirement:${entry.content}
|
|
@@ -3146,12 +3411,12 @@ ${block}` : block;
|
|
|
3146
3411
|
await atomicWrite(changelogPath, next);
|
|
3147
3412
|
}
|
|
3148
3413
|
async function readChangeRequirement(projectRoot, changeId) {
|
|
3149
|
-
const changeRoot =
|
|
3150
|
-
const proposal = await
|
|
3414
|
+
const changeRoot = join18(projectRoot, "openspec", "changes", changeId);
|
|
3415
|
+
const proposal = await readOptional4(join18(changeRoot, "proposal.md"));
|
|
3151
3416
|
if (proposal) {
|
|
3152
3417
|
return summarizeMarkdown(proposal);
|
|
3153
3418
|
}
|
|
3154
|
-
const readme = await
|
|
3419
|
+
const readme = await readOptional4(join18(changeRoot, "README.md"));
|
|
3155
3420
|
if (readme) {
|
|
3156
3421
|
return summarizeMarkdown(readme);
|
|
3157
3422
|
}
|
|
@@ -3161,9 +3426,9 @@ function summarizeMarkdown(content) {
|
|
|
3161
3426
|
const normalized = content.replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("<!--") && !line.startsWith("---")).join(" ");
|
|
3162
3427
|
return normalized || "No change requirement found.";
|
|
3163
3428
|
}
|
|
3164
|
-
async function
|
|
3429
|
+
async function readOptional4(path) {
|
|
3165
3430
|
try {
|
|
3166
|
-
return await
|
|
3431
|
+
return await readFile14(path, "utf8");
|
|
3167
3432
|
} catch {
|
|
3168
3433
|
return null;
|
|
3169
3434
|
}
|
|
@@ -3519,8 +3784,8 @@ async function updateCommand(ctx) {
|
|
|
3519
3784
|
|
|
3520
3785
|
// src/commands/verify.ts
|
|
3521
3786
|
import { createHash } from "crypto";
|
|
3522
|
-
import { mkdir as mkdir7, readFile as
|
|
3523
|
-
import { join as
|
|
3787
|
+
import { mkdir as mkdir7, readFile as readFile15, stat as stat8 } from "fs/promises";
|
|
3788
|
+
import { join as join19 } from "path";
|
|
3524
3789
|
async function verifyCommand(ctx, options) {
|
|
3525
3790
|
if (options.auto) {
|
|
3526
3791
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
@@ -3587,8 +3852,8 @@ async function verifyCommand(ctx, options) {
|
|
|
3587
3852
|
async function writeInstructions(ctx, changeId) {
|
|
3588
3853
|
await assertChangeExists(ctx, changeId);
|
|
3589
3854
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3590
|
-
const dir =
|
|
3591
|
-
const instructionsPath =
|
|
3855
|
+
const dir = join19(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
3856
|
+
const instructionsPath = join19(dir, "verify-instructions.md");
|
|
3592
3857
|
await mkdir7(dir, { recursive: true });
|
|
3593
3858
|
await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
|
|
3594
3859
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
@@ -3605,7 +3870,7 @@ async function writeInstructions(ctx, changeId) {
|
|
|
3605
3870
|
async function markDone(ctx, changeId) {
|
|
3606
3871
|
await assertChangeExists(ctx, changeId);
|
|
3607
3872
|
const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3608
|
-
const instructionsPath =
|
|
3873
|
+
const instructionsPath = join19(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
|
|
3609
3874
|
const instructions = await readInstructions(instructionsPath, changeId);
|
|
3610
3875
|
const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
|
|
3611
3876
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
@@ -3640,8 +3905,8 @@ async function assertChangeExists(ctx, changeId) {
|
|
|
3640
3905
|
}
|
|
3641
3906
|
async function readInstructions(path, changeId) {
|
|
3642
3907
|
try {
|
|
3643
|
-
await
|
|
3644
|
-
const content = await
|
|
3908
|
+
await stat8(path);
|
|
3909
|
+
const content = await readFile15(path, "utf8");
|
|
3645
3910
|
const fileChangeId = readFrontMatterValue(content, "changeId");
|
|
3646
3911
|
if (fileChangeId !== changeId) {
|
|
3647
3912
|
throw new FetError({
|
|
@@ -3779,9 +4044,9 @@ function renderIdeModelPolicy(command, language = "zh-CN") {
|
|
|
3779
4044
|
import { resolve } from "path";
|
|
3780
4045
|
|
|
3781
4046
|
// src/adapters/codex/index.ts
|
|
3782
|
-
import { mkdir as mkdir8, readFile as
|
|
4047
|
+
import { mkdir as mkdir8, readFile as readFile16, stat as stat9 } from "fs/promises";
|
|
3783
4048
|
import { homedir } from "os";
|
|
3784
|
-
import { dirname as dirname8, join as
|
|
4049
|
+
import { dirname as dirname8, join as join20 } from "path";
|
|
3785
4050
|
|
|
3786
4051
|
// src/adapters/commands.ts
|
|
3787
4052
|
var FET_WORKFLOW_COMMANDS = [
|
|
@@ -3823,6 +4088,7 @@ Before doing FET or OpenSpec work in Codex, read:
|
|
|
3823
4088
|
- AGENTS.md
|
|
3824
4089
|
- openspec/config.yaml
|
|
3825
4090
|
- .codex/fet/karpathy-guidelines.md
|
|
4091
|
+
- .codex/fet/figma-stop.md when implementing UI from Figma
|
|
3826
4092
|
- the active change files under openspec/changes/<change-id>/, when a change is selected
|
|
3827
4093
|
|
|
3828
4094
|
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 +4107,7 @@ ${languageInstruction(language)}
|
|
|
3841
4107
|
- AGENTS.md
|
|
3842
4108
|
- openspec/config.yaml
|
|
3843
4109
|
- .codex/fet/karpathy-guidelines.md
|
|
4110
|
+
- \u6309 Figma \u5B9E\u73B0 UI \u65F6\u9605\u8BFB .codex/fet/figma-stop.md
|
|
3844
4111
|
- \u5982\u679C\u5DF2\u9009\u62E9 change\uFF0C\u9605\u8BFB openspec/changes/<change-id>/ \u4E0B\u7684\u5F53\u524D\u4EA7\u7269
|
|
3845
4112
|
|
|
3846
4113
|
\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 +4128,16 @@ FET:END -->
|
|
|
3861
4128
|
${body}`
|
|
3862
4129
|
};
|
|
3863
4130
|
}
|
|
4131
|
+
function codexFigmaStopFile(language = DEFAULT_LANGUAGE) {
|
|
4132
|
+
return {
|
|
4133
|
+
path: ".codex/fet/figma-stop.md",
|
|
4134
|
+
content: renderCodexFigmaStopGuide(language)
|
|
4135
|
+
};
|
|
4136
|
+
}
|
|
3864
4137
|
function codexCommandFiles(language = DEFAULT_LANGUAGE) {
|
|
3865
4138
|
return [
|
|
3866
4139
|
codexKarpathyGuidelinesFile(language),
|
|
4140
|
+
codexFigmaStopFile(language),
|
|
3867
4141
|
...FET_ADAPTER_COMMANDS.map((command) => ({
|
|
3868
4142
|
path: `.codex/fet/commands/${command}.md`,
|
|
3869
4143
|
content: renderCommand(command, language)
|
|
@@ -4822,7 +5096,7 @@ var CodexAdapter = class {
|
|
|
4822
5096
|
adapterVersion = 1;
|
|
4823
5097
|
async detect(projectRoot) {
|
|
4824
5098
|
return {
|
|
4825
|
-
detected: await exists5(
|
|
5099
|
+
detected: await exists5(join20(projectRoot, ".codex")) || await exists5(join20(projectRoot, "AGENTS.md")),
|
|
4826
5100
|
reason: "Codex adapter is available for projects that use AGENTS.md"
|
|
4827
5101
|
};
|
|
4828
5102
|
}
|
|
@@ -4888,9 +5162,9 @@ var CodexAdapter = class {
|
|
|
4888
5162
|
};
|
|
4889
5163
|
function resolveTarget(projectRoot, file) {
|
|
4890
5164
|
if (file.root === "codex-home") {
|
|
4891
|
-
return
|
|
5165
|
+
return join20(resolveCodexHome(), file.path);
|
|
4892
5166
|
}
|
|
4893
|
-
return
|
|
5167
|
+
return join20(projectRoot, file.path);
|
|
4894
5168
|
}
|
|
4895
5169
|
function displayPathFor(file) {
|
|
4896
5170
|
if (file.root === "codex-home") {
|
|
@@ -4899,18 +5173,18 @@ function displayPathFor(file) {
|
|
|
4899
5173
|
return file.path;
|
|
4900
5174
|
}
|
|
4901
5175
|
function resolveCodexHome() {
|
|
4902
|
-
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ??
|
|
5176
|
+
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join20(homedir(), ".codex");
|
|
4903
5177
|
}
|
|
4904
5178
|
async function readExisting(path) {
|
|
4905
5179
|
try {
|
|
4906
|
-
return await
|
|
5180
|
+
return await readFile16(path, "utf8");
|
|
4907
5181
|
} catch {
|
|
4908
5182
|
return null;
|
|
4909
5183
|
}
|
|
4910
5184
|
}
|
|
4911
5185
|
async function exists5(path) {
|
|
4912
5186
|
try {
|
|
4913
|
-
await
|
|
5187
|
+
await stat9(path);
|
|
4914
5188
|
return true;
|
|
4915
5189
|
} catch {
|
|
4916
5190
|
return false;
|
|
@@ -4918,10 +5192,19 @@ async function exists5(path) {
|
|
|
4918
5192
|
}
|
|
4919
5193
|
|
|
4920
5194
|
// src/adapters/cursor/index.ts
|
|
4921
|
-
import { mkdir as mkdir9, readFile as
|
|
4922
|
-
import { dirname as dirname9, join as
|
|
5195
|
+
import { mkdir as mkdir9, readFile as readFile17, stat as stat10 } from "fs/promises";
|
|
5196
|
+
import { dirname as dirname9, join as join21 } from "path";
|
|
4923
5197
|
|
|
4924
5198
|
// src/adapters/cursor/templates.ts
|
|
5199
|
+
function cursorFigmaStopRuleFile(language = DEFAULT_LANGUAGE) {
|
|
5200
|
+
return {
|
|
5201
|
+
path: ".cursor/rules/fet-figma-stop.mdc",
|
|
5202
|
+
content: renderCursorFigmaStopRule(language)
|
|
5203
|
+
};
|
|
5204
|
+
}
|
|
5205
|
+
function cursorRuleFiles(language = DEFAULT_LANGUAGE) {
|
|
5206
|
+
return [cursorRuleFile(language), cursorFigmaStopRuleFile(language)];
|
|
5207
|
+
}
|
|
4925
5208
|
function cursorSkillFiles(language = DEFAULT_LANGUAGE) {
|
|
4926
5209
|
return FET_ADAPTER_COMMANDS.map((command) => ({
|
|
4927
5210
|
path: `.cursor/skills/fet-${command}/SKILL.md`,
|
|
@@ -4951,6 +5234,7 @@ ${languageInstruction(language)}
|
|
|
4951
5234
|
- openspec/config.yaml
|
|
4952
5235
|
- \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
5236
|
- \u5F53\u524D change \u76EE\u5F55\u4E0B\u7684 OpenSpec \u89C4\u5212\u4EA7\u7269\u3002
|
|
5237
|
+
- \u6309 Figma \u5B9E\u73B0 UI \u65F6\u9075\u5B88 \`.cursor/rules/fet-figma-stop.mdc\`\uFF1B\u82E5\u5B58\u5728 \`openspec/changes/<change-id>/.fet/figma-stop.md\` \u987B\u5148\u9605\u8BFB\u3002
|
|
4954
5238
|
|
|
4955
5239
|
\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
5240
|
`
|
|
@@ -5055,14 +5339,14 @@ var CursorAdapter = class {
|
|
|
5055
5339
|
adapterVersion = 1;
|
|
5056
5340
|
async detect(projectRoot) {
|
|
5057
5341
|
return {
|
|
5058
|
-
detected: await exists6(
|
|
5342
|
+
detected: await exists6(join21(projectRoot, ".cursor")),
|
|
5059
5343
|
reason: "Cursor adapter is available for any project"
|
|
5060
5344
|
};
|
|
5061
5345
|
}
|
|
5062
5346
|
async planInstall(_projectRoot, language) {
|
|
5063
5347
|
return {
|
|
5064
5348
|
tool: this.tool,
|
|
5065
|
-
files: [...cursorSkillFiles(language),
|
|
5349
|
+
files: [...cursorSkillFiles(language), ...cursorRuleFiles(language)].map((file) => ({
|
|
5066
5350
|
...file,
|
|
5067
5351
|
managed: true
|
|
5068
5352
|
}))
|
|
@@ -5072,7 +5356,7 @@ var CursorAdapter = class {
|
|
|
5072
5356
|
const written = [];
|
|
5073
5357
|
const skipped = [];
|
|
5074
5358
|
for (const file of plan.files) {
|
|
5075
|
-
const target =
|
|
5359
|
+
const target = join21(projectRoot, file.path);
|
|
5076
5360
|
const existing = await readExisting2(target);
|
|
5077
5361
|
if (existing && !existing.includes("FET:MANAGED") && !force) {
|
|
5078
5362
|
throw new FetError({
|
|
@@ -5095,7 +5379,7 @@ var CursorAdapter = class {
|
|
|
5095
5379
|
const plan = await this.planInstall(projectRoot);
|
|
5096
5380
|
const checks = [];
|
|
5097
5381
|
for (const file of plan.files) {
|
|
5098
|
-
const target =
|
|
5382
|
+
const target = join21(projectRoot, file.path);
|
|
5099
5383
|
const content = await readExisting2(target);
|
|
5100
5384
|
const managed = Boolean(content?.includes("FET:MANAGED"));
|
|
5101
5385
|
const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
|
|
@@ -5111,14 +5395,14 @@ var CursorAdapter = class {
|
|
|
5111
5395
|
};
|
|
5112
5396
|
async function readExisting2(path) {
|
|
5113
5397
|
try {
|
|
5114
|
-
return await
|
|
5398
|
+
return await readFile17(path, "utf8");
|
|
5115
5399
|
} catch {
|
|
5116
5400
|
return null;
|
|
5117
5401
|
}
|
|
5118
5402
|
}
|
|
5119
5403
|
async function exists6(path) {
|
|
5120
5404
|
try {
|
|
5121
|
-
await
|
|
5405
|
+
await stat10(path);
|
|
5122
5406
|
return true;
|
|
5123
5407
|
} catch {
|
|
5124
5408
|
return false;
|
|
@@ -5130,13 +5414,13 @@ import { execFile as execFile4 } from "child_process";
|
|
|
5130
5414
|
import { promisify as promisify4 } from "util";
|
|
5131
5415
|
|
|
5132
5416
|
// src/openspec/inspector.ts
|
|
5133
|
-
import { readdir as
|
|
5134
|
-
import { join as
|
|
5417
|
+
import { readdir as readdir4, stat as stat11 } from "fs/promises";
|
|
5418
|
+
import { join as join22 } from "path";
|
|
5135
5419
|
async function inspectOpenSpecProject(projectRoot) {
|
|
5136
|
-
const openspecPath =
|
|
5137
|
-
const changesPath =
|
|
5138
|
-
const legacyArchivePath =
|
|
5139
|
-
const changesArchivePath =
|
|
5420
|
+
const openspecPath = join22(projectRoot, "openspec");
|
|
5421
|
+
const changesPath = join22(openspecPath, "changes");
|
|
5422
|
+
const legacyArchivePath = join22(openspecPath, "archive");
|
|
5423
|
+
const changesArchivePath = join22(changesPath, "archive");
|
|
5140
5424
|
return {
|
|
5141
5425
|
exists: await exists7(openspecPath),
|
|
5142
5426
|
changes: await listDirectories(changesPath, { exclude: ["archive"] }),
|
|
@@ -5144,13 +5428,13 @@ async function inspectOpenSpecProject(projectRoot) {
|
|
|
5144
5428
|
};
|
|
5145
5429
|
}
|
|
5146
5430
|
async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
5147
|
-
const changePath =
|
|
5148
|
-
const tasksPath =
|
|
5149
|
-
const specsPath =
|
|
5431
|
+
const changePath = join22(projectRoot, "openspec", "changes", changeId);
|
|
5432
|
+
const tasksPath = join22(changePath, "tasks.md");
|
|
5433
|
+
const specsPath = join22(changePath, "specs");
|
|
5150
5434
|
return {
|
|
5151
5435
|
changeId,
|
|
5152
5436
|
exists: await exists7(changePath),
|
|
5153
|
-
hasProposal: await exists7(
|
|
5437
|
+
hasProposal: await exists7(join22(changePath, "proposal.md")),
|
|
5154
5438
|
hasTasks: await exists7(tasksPath),
|
|
5155
5439
|
hasSpecs: await exists7(specsPath),
|
|
5156
5440
|
tasksPath,
|
|
@@ -5159,7 +5443,7 @@ async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
|
5159
5443
|
}
|
|
5160
5444
|
async function listDirectories(path, options = {}) {
|
|
5161
5445
|
try {
|
|
5162
|
-
const entries = await
|
|
5446
|
+
const entries = await readdir4(path, { withFileTypes: true });
|
|
5163
5447
|
const excluded = new Set(options.exclude ?? []);
|
|
5164
5448
|
return entries.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).map((entry) => entry.name);
|
|
5165
5449
|
} catch {
|
|
@@ -5168,7 +5452,7 @@ async function listDirectories(path, options = {}) {
|
|
|
5168
5452
|
}
|
|
5169
5453
|
async function exists7(path) {
|
|
5170
5454
|
try {
|
|
5171
|
-
await
|
|
5455
|
+
await stat11(path);
|
|
5172
5456
|
return true;
|
|
5173
5457
|
} catch {
|
|
5174
5458
|
return false;
|
|
@@ -5351,13 +5635,13 @@ function escapeRegExp(value) {
|
|
|
5351
5635
|
}
|
|
5352
5636
|
|
|
5353
5637
|
// src/scanner/routes.ts
|
|
5354
|
-
import { readdir as
|
|
5355
|
-
import { join as
|
|
5638
|
+
import { readdir as readdir5, stat as stat12 } from "fs/promises";
|
|
5639
|
+
import { join as join23, relative as relative3, sep } from "path";
|
|
5356
5640
|
async function scanRoutes(projectRoot) {
|
|
5357
5641
|
const candidates = ["src/routes", "src/pages", "app", "pages"];
|
|
5358
5642
|
const routes = [];
|
|
5359
5643
|
for (const candidate of candidates) {
|
|
5360
|
-
const root =
|
|
5644
|
+
const root = join23(projectRoot, candidate);
|
|
5361
5645
|
if (!await exists8(root)) {
|
|
5362
5646
|
continue;
|
|
5363
5647
|
}
|
|
@@ -5366,8 +5650,8 @@ async function scanRoutes(projectRoot) {
|
|
|
5366
5650
|
continue;
|
|
5367
5651
|
}
|
|
5368
5652
|
routes.push({
|
|
5369
|
-
path: inferRoutePath(
|
|
5370
|
-
source:
|
|
5653
|
+
path: inferRoutePath(relative3(root, file)),
|
|
5654
|
+
source: relative3(projectRoot, file).split(sep).join("/"),
|
|
5371
5655
|
inferred: true,
|
|
5372
5656
|
confidence: "medium"
|
|
5373
5657
|
});
|
|
@@ -5382,10 +5666,10 @@ function inferRoutePath(relativePath) {
|
|
|
5382
5666
|
return `/${withoutIndex}`.replace(/\/+/g, "/");
|
|
5383
5667
|
}
|
|
5384
5668
|
async function listFiles(root) {
|
|
5385
|
-
const entries = await
|
|
5669
|
+
const entries = await readdir5(root, { withFileTypes: true });
|
|
5386
5670
|
const files = [];
|
|
5387
5671
|
for (const entry of entries) {
|
|
5388
|
-
const path =
|
|
5672
|
+
const path = join23(root, entry.name);
|
|
5389
5673
|
if (entry.isDirectory()) {
|
|
5390
5674
|
files.push(...await listFiles(path));
|
|
5391
5675
|
} else {
|
|
@@ -5396,7 +5680,7 @@ async function listFiles(root) {
|
|
|
5396
5680
|
}
|
|
5397
5681
|
async function exists8(path) {
|
|
5398
5682
|
try {
|
|
5399
|
-
await
|
|
5683
|
+
await stat12(path);
|
|
5400
5684
|
return true;
|
|
5401
5685
|
} catch {
|
|
5402
5686
|
return false;
|
|
@@ -5553,9 +5837,9 @@ async function createCommandContext(command, options) {
|
|
|
5553
5837
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
5554
5838
|
|
|
5555
5839
|
// src/update/check.ts
|
|
5556
|
-
import { mkdir as mkdir10, readFile as
|
|
5840
|
+
import { mkdir as mkdir10, readFile as readFile18, writeFile } from "fs/promises";
|
|
5557
5841
|
import { homedir as homedir2 } from "os";
|
|
5558
|
-
import { dirname as dirname10, join as
|
|
5842
|
+
import { dirname as dirname10, join as join24 } from "path";
|
|
5559
5843
|
var DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
5560
5844
|
function getFetUpdateCheckMode(env = process.env) {
|
|
5561
5845
|
const value = env.FET_UPDATE_CHECK?.trim().toLowerCase();
|
|
@@ -5628,11 +5912,11 @@ function formatFetUpdateWarning(availability, language) {
|
|
|
5628
5912
|
}
|
|
5629
5913
|
function cachePath() {
|
|
5630
5914
|
const home = process.env.FET_UPDATE_CHECK_CACHE_HOME?.trim() || homedir2();
|
|
5631
|
-
return
|
|
5915
|
+
return join24(home, ".fet", "update-check-cache.json");
|
|
5632
5916
|
}
|
|
5633
5917
|
async function readUpdateCheckCache() {
|
|
5634
5918
|
try {
|
|
5635
|
-
const raw = await
|
|
5919
|
+
const raw = await readFile18(cachePath(), "utf8");
|
|
5636
5920
|
const parsed = JSON.parse(raw);
|
|
5637
5921
|
if (typeof parsed.latestVersion !== "string" || typeof parsed.checkedAt !== "string") {
|
|
5638
5922
|
return null;
|