@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/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 relative3 = env.FET_GITNEXUS_GRAPH_PATH?.trim() || DEFAULT_GRAPH_PATH;
192
- const graphPath = join5(projectRoot, relative3);
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: relative3,
196
+ graphPath: relative4,
197
197
  graphExists: true,
198
198
  lastIndexedAt: info.mtime.toISOString()
199
199
  };
200
200
  } catch {
201
201
  return {
202
- graphPath: relative3,
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 readFile13 } from "fs/promises";
2404
- import { join as join17 } from "path";
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 readFile11 } from "fs/promises";
2434
- import { join as join16 } from "path";
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 readFile11(this.globalPath(), "utf8"));
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(join16(this.projectRoot, "openspec"), { recursive: true });
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 readFile11(this.changePath(changeId), "utf8"));
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(join16(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
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 join16(this.projectRoot, "openspec", "fet-state.json");
2829
+ return join17(this.projectRoot, "openspec", "fet-state.json");
2594
2830
  }
2595
2831
  changePath(changeId) {
2596
- return join16(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
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 readFile12 } from "fs/promises";
2840
+ import { readFile as readFile13 } from "fs/promises";
2605
2841
  async function readCompletedTaskIds(tasksPath) {
2606
2842
  let content;
2607
2843
  try {
2608
- content = await readFile12(tasksPath, "utf8");
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
- "Discuss the requirement, constraints, and acceptance criteria with the user.",
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 = join17(projectRoot, "CHANGELOG.md");
3137
- const existing = await readOptional3(changelogPath);
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 = join17(projectRoot, "openspec", "changes", changeId);
3150
- const proposal = await readOptional3(join17(changeRoot, "proposal.md"));
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 readOptional3(join17(changeRoot, "README.md"));
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 readOptional3(path) {
3429
+ async function readOptional4(path) {
3165
3430
  try {
3166
- return await readFile13(path, "utf8");
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 readFile14, stat as stat7 } from "fs/promises";
3523
- import { join as join18 } from "path";
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 = join18(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
3591
- const instructionsPath = join18(dir, "verify-instructions.md");
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 = join18(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
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 stat7(path);
3644
- const content = await readFile14(path, "utf8");
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 readFile15, stat as stat8 } from "fs/promises";
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 join19 } from "path";
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(join19(projectRoot, ".codex")) || await exists5(join19(projectRoot, "AGENTS.md")),
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 join19(resolveCodexHome(), file.path);
5165
+ return join20(resolveCodexHome(), file.path);
4892
5166
  }
4893
- return join19(projectRoot, file.path);
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 ?? join19(homedir(), ".codex");
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 readFile15(path, "utf8");
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 stat8(path);
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 readFile16, stat as stat9 } from "fs/promises";
4922
- import { dirname as dirname9, join as join20 } from "path";
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(join20(projectRoot, ".cursor")),
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), cursorRuleFile(language)].map((file) => ({
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 = join20(projectRoot, file.path);
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 = join20(projectRoot, file.path);
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 readFile16(path, "utf8");
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 stat9(path);
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 readdir3, stat as stat10 } from "fs/promises";
5134
- import { join as join21 } from "path";
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 = join21(projectRoot, "openspec");
5137
- const changesPath = join21(openspecPath, "changes");
5138
- const legacyArchivePath = join21(openspecPath, "archive");
5139
- const changesArchivePath = join21(changesPath, "archive");
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 = join21(projectRoot, "openspec", "changes", changeId);
5148
- const tasksPath = join21(changePath, "tasks.md");
5149
- const specsPath = join21(changePath, "specs");
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(join21(changePath, "proposal.md")),
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 readdir3(path, { withFileTypes: true });
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 stat10(path);
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 readdir4, stat as stat11 } from "fs/promises";
5355
- import { join as join22, relative as relative2, sep } from "path";
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 = join22(projectRoot, candidate);
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(relative2(root, file)),
5370
- source: relative2(projectRoot, file).split(sep).join("/"),
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 readdir4(root, { withFileTypes: true });
5669
+ const entries = await readdir5(root, { withFileTypes: true });
5386
5670
  const files = [];
5387
5671
  for (const entry of entries) {
5388
- const path = join22(root, entry.name);
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 stat11(path);
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 readFile17, writeFile } from "fs/promises";
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 join23 } from "path";
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 join23(home, ".fet", "update-check-cache.json");
5915
+ return join24(home, ".fet", "update-check-cache.json");
5632
5916
  }
5633
5917
  async function readUpdateCheckCache() {
5634
5918
  try {
5635
- const raw = await readFile17(cachePath(), "utf8");
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;