@nick848/fet 1.1.5 → 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 relative2 = env.FET_GITNEXUS_GRAPH_PATH?.trim() || DEFAULT_GRAPH_PATH;
192
- const graphPath = join5(projectRoot, relative2);
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: relative2,
196
+ graphPath: relative4,
197
197
  graphExists: true,
198
198
  lastIndexedAt: info.mtime.toISOString()
199
199
  };
200
200
  } catch {
201
201
  return {
202
- graphPath: relative2,
202
+ graphPath: relative4,
203
203
  graphExists: false,
204
204
  lastIndexedAt: null
205
205
  };
@@ -366,12 +366,670 @@ async function exists(path) {
366
366
 
367
367
  // src/commands/fill-context.ts
368
368
  import { mkdir as mkdir3 } from "fs/promises";
369
- import { dirname as dirname4, join as join7 } from "path";
369
+ import { dirname as dirname4, join as join10 } from "path";
370
+
371
+ // src/agents-miniprogram.ts
372
+ import { readFile as readFile7 } from "fs/promises";
373
+ import { join as join9 } from "path";
374
+
375
+ // src/scanner/miniprogram.ts
376
+ import { readdir, readFile as readFile6, stat as stat5 } from "fs/promises";
377
+ import { join as join8, relative } from "path";
378
+
379
+ // src/scanner/package.ts
380
+ import { readFile as readFile5, stat as stat4 } from "fs/promises";
381
+ import { join as join7 } from "path";
382
+ import { parse } from "yaml";
383
+ async function readPackageJson(projectRoot) {
384
+ try {
385
+ return JSON.parse(await readFile5(join7(projectRoot, "package.json"), "utf8"));
386
+ } catch {
387
+ return null;
388
+ }
389
+ }
390
+ async function detectPackageManager(projectRoot, pkg) {
391
+ const warnings = [];
392
+ if (pkg?.packageManager) {
393
+ const declared = pkg.packageManager.split("@")[0] ?? "unknown";
394
+ const locks2 = await detectLockManagers(projectRoot);
395
+ const conflicting = locks2.filter((item) => item !== declared);
396
+ if (conflicting.length) {
397
+ warnings.push(`packageManager \u58F0\u660E\u4E3A ${declared}\uFF0C\u4F46\u540C\u65F6\u53D1\u73B0\u9501\u6587\u4EF6\uFF1A${conflicting.join(", ")}`);
398
+ }
399
+ return { manager: declared, confidence: "high", warnings };
400
+ }
401
+ const locks = await detectLockManagers(projectRoot);
402
+ if (locks.length > 1) {
403
+ warnings.push(`\u53D1\u73B0\u591A\u4E2A\u5305\u7BA1\u7406\u5668\u9501\u6587\u4EF6\uFF1A${locks.join(", ")}\uFF0C\u9ED8\u8BA4\u4F7F\u7528 ${locks[0]}`);
404
+ return { manager: locks[0] ?? "npm", confidence: "medium", warnings };
405
+ }
406
+ if (locks[0]) {
407
+ return { manager: locks[0], confidence: "high", warnings };
408
+ }
409
+ return { manager: "npm", confidence: "low", warnings };
410
+ }
411
+ function extractCommands(pkg, packageManager) {
412
+ const scripts = pkg?.scripts ?? {};
413
+ const result = {};
414
+ const scriptNames = ["dev", "build", "lint", "typecheck", "check", "test", "test:unit"];
415
+ for (const name of scriptNames) {
416
+ if (scripts[name]) {
417
+ const dimension = name === "check" ? "typecheck" : name === "test:unit" ? "test" : name;
418
+ if (result[dimension]) {
419
+ continue;
420
+ }
421
+ result[dimension] = {
422
+ command: scriptCommand(packageManager, name),
423
+ source: `package.json:scripts.${name}`,
424
+ required: name === "build"
425
+ };
426
+ }
427
+ }
428
+ return result;
429
+ }
430
+ function detectFramework(pkg) {
431
+ const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
432
+ const candidates = [
433
+ ["next", ["next"]],
434
+ ["nuxt", ["nuxt"]],
435
+ ["vite", ["vite"]],
436
+ ["sveltekit", ["@sveltejs/kit"]],
437
+ ["angular", ["@angular/core", "@angular/cli"]],
438
+ ["react", ["react"]],
439
+ ["vue", ["vue"]],
440
+ ["svelte", ["svelte"]]
441
+ ];
442
+ for (const [candidate, packages] of candidates) {
443
+ if (packages.some((name) => deps[name])) {
444
+ return { name: candidate, confidence: "high", sources: ["package.json"] };
445
+ }
446
+ }
447
+ return { name: "unknown", confidence: "low", sources: [] };
448
+ }
449
+ async function detectLanguage(projectRoot, pkg) {
450
+ const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
451
+ if (deps.typescript || await exists2(join7(projectRoot, "tsconfig.json"))) {
452
+ return "typescript";
453
+ }
454
+ return "javascript";
455
+ }
456
+ async function detectWorkspaces(projectRoot, pkg) {
457
+ const packageWorkspaces = normalizeWorkspaces(pkg?.workspaces).map((path) => ({
458
+ name: path,
459
+ path,
460
+ source: "package.json:workspaces"
461
+ }));
462
+ if (packageWorkspaces.length) {
463
+ return packageWorkspaces;
464
+ }
465
+ try {
466
+ const workspace = parse(await readFile5(join7(projectRoot, "pnpm-workspace.yaml"), "utf8"));
467
+ return (workspace?.packages ?? []).map((path) => ({
468
+ name: path,
469
+ path,
470
+ source: "pnpm-workspace.yaml:packages"
471
+ }));
472
+ } catch {
473
+ return [];
474
+ }
475
+ }
476
+ async function detectLockManagers(projectRoot) {
477
+ const lockFiles = [
478
+ ["pnpm-lock.yaml", "pnpm"],
479
+ ["yarn.lock", "yarn"],
480
+ ["bun.lockb", "bun"],
481
+ ["bun.lock", "bun"],
482
+ ["package-lock.json", "npm"]
483
+ ];
484
+ const found = [];
485
+ for (const [file, manager] of lockFiles) {
486
+ if (await exists2(join7(projectRoot, file))) {
487
+ found.push(manager);
488
+ }
489
+ }
490
+ return found;
491
+ }
492
+ function normalizeWorkspaces(workspaces) {
493
+ if (Array.isArray(workspaces)) {
494
+ return workspaces;
495
+ }
496
+ return workspaces?.packages ?? [];
497
+ }
498
+ function scriptCommand(packageManager, name) {
499
+ return packageManager === "npm" ? `npm run ${name}` : `${packageManager} ${name}`;
500
+ }
501
+ async function exists2(path) {
502
+ try {
503
+ await stat4(path);
504
+ return true;
505
+ } catch {
506
+ return false;
507
+ }
508
+ }
509
+
510
+ // src/scanner/miniprogram.ts
511
+ var MAIN_PACKAGE_LIMIT_BYTES = 2 * 1024 * 1024;
512
+ var SUBPACKAGE_LIMIT_BYTES = 2 * 1024 * 1024;
513
+ var TOTAL_LIMIT_BYTES = 20 * 1024 * 1024;
514
+ var NEAR_LIMIT_BYTES = Math.floor(1.7 * 1024 * 1024);
515
+ async function detectMiniprogramProject(projectRoot) {
516
+ const pkg = await readPackageJson(projectRoot);
517
+ const platform = await resolvePlatform(projectRoot, pkg);
518
+ if (!platform) {
519
+ return { supported: false };
520
+ }
521
+ const appJsonPath = await resolveAppJsonPath(projectRoot, platform);
522
+ if (!appJsonPath) {
523
+ return { supported: false };
524
+ }
525
+ const miniprogramRoot = dirnameNormalized(appJsonPath);
526
+ const appJson = await readAppJson(appJsonPath);
527
+ if (!appJson) {
528
+ return { supported: false };
529
+ }
530
+ const subPackages = appJson.subPackages ?? appJson.subpackages ?? [];
531
+ const subRoots = subPackages.map((item) => normalizeRelative(item.root ?? "")).filter(Boolean);
532
+ const mainPagePaths = appJson.pages ?? [];
533
+ const mainDirs = uniquePaths(mainPagePaths.map((page) => pageDir(page)).filter(Boolean));
534
+ const mainSize = await sumPaths(miniprogramRoot, mainDirs, subRoots);
535
+ const mainPackage = {
536
+ name: "main",
537
+ root: miniprogramRoot,
538
+ sizeBytes: mainSize,
539
+ sizeLabel: formatBytes(mainSize),
540
+ limitBytes: MAIN_PACKAGE_LIMIT_BYTES,
541
+ status: sizeStatus(mainSize, MAIN_PACKAGE_LIMIT_BYTES),
542
+ pagePaths: mainPagePaths
543
+ };
544
+ const subpackageReports = [];
545
+ for (const sub of subPackages) {
546
+ const root = normalizeRelative(sub.root ?? "");
547
+ if (!root) {
548
+ continue;
549
+ }
550
+ const size = await directorySize(join8(miniprogramRoot, root));
551
+ subpackageReports.push({
552
+ name: sub.name ?? root.replace(/\/$/, ""),
553
+ root,
554
+ sizeBytes: size,
555
+ sizeLabel: formatBytes(size),
556
+ limitBytes: SUBPACKAGE_LIMIT_BYTES,
557
+ status: sizeStatus(size, SUBPACKAGE_LIMIT_BYTES),
558
+ pagePaths: sub.pages ?? []
559
+ });
560
+ }
561
+ const totalSizeBytes = mainPackage.sizeBytes + subpackageReports.reduce((sum, item) => sum + item.sizeBytes, 0);
562
+ const warnings = [];
563
+ if (totalSizeBytes >= NEAR_LIMIT_BYTES) {
564
+ warnings.push(
565
+ totalSizeBytes >= TOTAL_LIMIT_BYTES ? "\u6E90\u7801\u76EE\u5F55\u5408\u8BA1\u4F53\u79EF\u5DF2\u8FBE\u5230\u6216\u8D85\u8FC7 20MB \u53C2\u8003\u4E0A\u9650\uFF0C\u9700\u6574\u4F53\u7626\u8EAB\u3002" : "\u6E90\u7801\u76EE\u5F55\u5408\u8BA1\u4F53\u79EF\u5DF2\u63A5\u8FD1 20MB \u53C2\u8003\u4E0A\u9650\u3002"
566
+ );
567
+ }
568
+ return {
569
+ supported: true,
570
+ platform: platform.id,
571
+ platformLabel: platform.label,
572
+ projectType: platform.projectType,
573
+ appJsonPath: relative(projectRoot, appJsonPath).replace(/\\/g, "/"),
574
+ miniprogramRoot: relative(projectRoot, miniprogramRoot).replace(/\\/g, "/") || ".",
575
+ mainPackage,
576
+ subpackages: subpackageReports,
577
+ totalSizeBytes,
578
+ warnings
579
+ };
580
+ }
581
+ async function resolvePlatform(projectRoot, pkg) {
582
+ const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
583
+ if (await exists3(join8(projectRoot, "project.config.json"))) {
584
+ return { id: "wechat", label: "\u5FAE\u4FE1\u5C0F\u7A0B\u5E8F", projectType: "\u5FAE\u4FE1\u539F\u751F / \u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177" };
585
+ }
586
+ if (deps["@tarojs/taro"] || deps["@tarojs/cli"] || await exists3(join8(projectRoot, "config", "index.ts"))) {
587
+ return { id: "taro", label: "Taro\uFF08\u53EF\u53D1\u5E03\u5FAE\u4FE1\u5C0F\u7A0B\u5E8F\uFF09", projectType: "Taro \u8DE8\u7AEF\u5C0F\u7A0B\u5E8F" };
588
+ }
589
+ if (await exists3(join8(projectRoot, "manifest.json")) && await exists3(join8(projectRoot, "pages.json"))) {
590
+ return { id: "uni-app", label: "uni-app\uFF08\u53EF\u53D1\u5E03\u5FAE\u4FE1\u5C0F\u7A0B\u5E8F\uFF09", projectType: "uni-app \u8DE8\u7AEF\u5C0F\u7A0B\u5E8F" };
591
+ }
592
+ if (await exists3(join8(projectRoot, "app.json"))) {
593
+ return { id: "wechat", label: "\u5FAE\u4FE1\u5C0F\u7A0B\u5E8F", projectType: "\u5FAE\u4FE1\u539F\u751F\uFF08app.json\uFF09" };
594
+ }
595
+ if (await exists3(join8(projectRoot, "miniprogram", "app.json"))) {
596
+ return { id: "wechat", label: "\u5FAE\u4FE1\u5C0F\u7A0B\u5E8F", projectType: "\u5FAE\u4FE1\u539F\u751F\uFF08miniprogram \u76EE\u5F55\uFF09" };
597
+ }
598
+ if (deps["miniprogram-api-typings"]) {
599
+ return { id: "wechat", label: "\u5FAE\u4FE1\u5C0F\u7A0B\u5E8F", projectType: "\u5FAE\u4FE1 API \u7C7B\u578B\u5B9A\u4E49\u9879\u76EE" };
600
+ }
601
+ return null;
602
+ }
603
+ async function resolveAppJsonPath(projectRoot, platform) {
604
+ const candidates = [];
605
+ if (platform.id === "wechat") {
606
+ try {
607
+ const config = JSON.parse(await readFile6(join8(projectRoot, "project.config.json"), "utf8"));
608
+ if (config.miniprogramRoot) {
609
+ candidates.push(join8(projectRoot, config.miniprogramRoot, "app.json"));
610
+ }
611
+ } catch {
612
+ }
613
+ candidates.push(join8(projectRoot, "app.json"), join8(projectRoot, "miniprogram", "app.json"));
614
+ } else {
615
+ candidates.push(
616
+ join8(projectRoot, "src", "app.json"),
617
+ join8(projectRoot, "app.json"),
618
+ join8(projectRoot, "miniprogram", "app.json"),
619
+ join8(projectRoot, "dist", "app.json")
620
+ );
621
+ }
622
+ for (const candidate of candidates) {
623
+ if (await exists3(candidate)) {
624
+ return candidate;
625
+ }
626
+ }
627
+ return null;
628
+ }
629
+ async function readAppJson(path) {
630
+ try {
631
+ return JSON.parse(await readFile6(path, "utf8"));
632
+ } catch {
633
+ return null;
634
+ }
635
+ }
636
+ async function sumPaths(baseDir, dirs, excludedRoots) {
637
+ let total = 0;
638
+ const visited = /* @__PURE__ */ new Set();
639
+ for (const dir of dirs) {
640
+ const abs = join8(baseDir, dir);
641
+ if (visited.has(abs)) {
642
+ continue;
643
+ }
644
+ visited.add(abs);
645
+ total += await directorySize(abs);
646
+ }
647
+ const rootFiles = await readdir(baseDir, { withFileTypes: true });
648
+ for (const entry of rootFiles) {
649
+ if (!entry.isFile()) {
650
+ continue;
651
+ }
652
+ if (shouldSkipName(entry.name)) {
653
+ continue;
654
+ }
655
+ total += (await stat5(join8(baseDir, entry.name))).size;
656
+ }
657
+ for (const entry of rootFiles) {
658
+ if (!entry.isDirectory() || shouldSkipName(entry.name)) {
659
+ continue;
660
+ }
661
+ const rel = `${entry.name}/`;
662
+ if (dirs.some((dir) => dir === entry.name || dir.startsWith(`${entry.name}/`))) {
663
+ continue;
664
+ }
665
+ if (excludedRoots.some((root) => root === entry.name || root.startsWith(`${entry.name}/`) || rel.startsWith(root))) {
666
+ continue;
667
+ }
668
+ total += await directorySize(join8(baseDir, entry.name));
669
+ }
670
+ return total;
671
+ }
672
+ async function directorySize(targetPath) {
673
+ try {
674
+ const info = await stat5(targetPath);
675
+ if (!info.isDirectory()) {
676
+ return info.size;
677
+ }
678
+ } catch {
679
+ return 0;
680
+ }
681
+ let total = 0;
682
+ const queue = [targetPath];
683
+ while (queue.length) {
684
+ const current = queue.pop();
685
+ if (!current) {
686
+ break;
687
+ }
688
+ let entries;
689
+ try {
690
+ entries = await readdir(current, { withFileTypes: true });
691
+ } catch {
692
+ continue;
693
+ }
694
+ for (const entry of entries) {
695
+ if (shouldSkipName(entry.name)) {
696
+ continue;
697
+ }
698
+ const next = join8(current, entry.name);
699
+ if (entry.isDirectory()) {
700
+ queue.push(next);
701
+ } else if (entry.isFile()) {
702
+ try {
703
+ total += (await stat5(next)).size;
704
+ } catch {
705
+ }
706
+ }
707
+ }
708
+ }
709
+ return total;
710
+ }
711
+ function pageDir(pagePath) {
712
+ const normalized = pagePath.replace(/^\//, "");
713
+ const parts = normalized.split("/");
714
+ if (parts.length <= 1) {
715
+ return parts[0] ?? "";
716
+ }
717
+ return parts.slice(0, -1).join("/");
718
+ }
719
+ function sizeStatus(sizeBytes, limitBytes) {
720
+ if (sizeBytes >= limitBytes) {
721
+ return "over_limit";
722
+ }
723
+ if (sizeBytes >= NEAR_LIMIT_BYTES) {
724
+ return "near_limit";
725
+ }
726
+ return "ok";
727
+ }
728
+ function formatBytes(bytes) {
729
+ if (bytes >= 1024 * 1024) {
730
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
731
+ }
732
+ if (bytes >= 1024) {
733
+ return `${(bytes / 1024).toFixed(1)} KB`;
734
+ }
735
+ return `${bytes} B`;
736
+ }
737
+ function normalizeRelative(value) {
738
+ return value.replace(/^\//, "").replace(/\\/g, "/");
739
+ }
740
+ function uniquePaths(paths) {
741
+ return [...new Set(paths.filter(Boolean))];
742
+ }
743
+ function shouldSkipName(name) {
744
+ return name === "node_modules" || name === ".git" || name === "miniprogram_npm" || name.startsWith(".");
745
+ }
746
+ function dirnameNormalized(filePath) {
747
+ const parts = filePath.replace(/\\/g, "/").split("/");
748
+ parts.pop();
749
+ return parts.join("/") || ".";
750
+ }
751
+ async function exists3(path) {
752
+ try {
753
+ await stat5(path);
754
+ return true;
755
+ } catch {
756
+ return false;
757
+ }
758
+ }
759
+
760
+ // src/templates/miniprogram-agents.ts
761
+ var MAIN_LIMIT_MB = 2;
762
+ var SUB_LIMIT_MB = 2;
763
+ var TOTAL_LIMIT_MB = 20;
764
+ var NEAR_LIMIT_MB = 1.7;
765
+ function renderMiniprogramPlaceholderSection(language) {
766
+ if (language === "en") {
767
+ return `## Mini Program
768
+
769
+ - Platform: [NEEDS LLM INPUT]
770
+ - Project type: [NEEDS LLM INPUT]
771
+
772
+ ### Package size (filled by \`fet fill-context\`)
773
+
774
+ [NEEDS LLM INPUT]
775
+
776
+ ### Development constraints
777
+
778
+ [NEEDS LLM INPUT]`;
779
+ }
780
+ return `## \u5C0F\u7A0B\u5E8F
781
+
782
+ - \u5E73\u53F0\uFF1A[NEEDS LLM INPUT]
783
+ - \u5DE5\u7A0B\u7C7B\u578B\uFF1A[NEEDS LLM INPUT]
784
+
785
+ ### \u5305\u4F53\u79EF\uFF08\u7531 \`fet fill-context\` \u626B\u63CF\u8865\u5145\uFF09
786
+
787
+ [NEEDS LLM INPUT]
788
+
789
+ ### \u5F00\u53D1\u7EA6\u675F
790
+
791
+ [NEEDS LLM INPUT]`;
792
+ }
793
+ function renderMiniprogramFilledSection(scan, language) {
794
+ if (language === "en") {
795
+ return renderMiniprogramFilledSectionEn(scan);
796
+ }
797
+ return renderMiniprogramFilledSectionZh(scan);
798
+ }
799
+ function renderMiniprogramFilledSectionZh(scan) {
800
+ const sizeTable = renderSizeTableZh(scan);
801
+ const rules = renderConstraintRulesZh(scan);
802
+ const warnings = scan.warnings.length ? `
803
+
804
+ \u626B\u63CF\u63D0\u793A\uFF1A${scan.warnings.join("\uFF1B")}` : "";
805
+ return `## \u5C0F\u7A0B\u5E8F
806
+
807
+ - \u5E73\u53F0\uFF1A${scan.platformLabel}
808
+ - \u5DE5\u7A0B\u7C7B\u578B\uFF1A${scan.projectType}
809
+ - \u914D\u7F6E\uFF1A\`${scan.appJsonPath}\`\uFF08\u6839\u76EE\u5F55 \`${scan.miniprogramRoot}/\`\uFF09
810
+
811
+ ### \u5305\u4F53\u79EF\uFF08\u6E90\u7801\u76EE\u5F55\u4F30\u7B97\uFF0C\u4E0A\u4F20\u524D\u4EE5\u5FAE\u4FE1\u5F00\u53D1\u8005\u5DE5\u5177 / CI \u6784\u5EFA\u4EA7\u7269\u4E3A\u51C6\uFF09
812
+
813
+ ${sizeTable}
814
+
815
+ > \u8BF4\u660E\uFF1A\u4E0B\u8868\u6309\u4ED3\u5E93\u5185\u9875\u9762/\u5206\u5305\u76EE\u5F55\u6E90\u7801\u4F53\u79EF\u7D2F\u8BA1\uFF0C\u7528\u4E8E\u89C4\u5212\u5F00\u53D1\uFF1B**\u5B9E\u9645\u4E0A\u4F20\u4F53\u79EF\u4EE5\u7F16\u8BD1\u540E\u4E3A\u51C6**\uFF0C\u901A\u5E38\u4E0E\u6E90\u7801\u8D8B\u52BF\u4E00\u81F4\u3002
816
+
817
+ ### \u5F00\u53D1\u7EA6\u675F
818
+
819
+ ${rules}${warnings}`;
820
+ }
821
+ function renderMiniprogramFilledSectionEn(scan) {
822
+ const sizeTable = renderSizeTableEn(scan);
823
+ const rules = renderConstraintRulesEn(scan);
824
+ const warnings = scan.warnings.length ? `
825
+
826
+ Scan notes: ${scan.warnings.join("; ")}` : "";
827
+ return `## Mini Program
828
+
829
+ - Platform: ${scan.platformLabel}
830
+ - Project type: ${scan.projectType}
831
+ - Config: \`${scan.appJsonPath}\` (root \`${scan.miniprogramRoot}/\`)
832
+
833
+ ### Package size (source tree estimate; verify with WeChat DevTools / CI build output)
834
+
835
+ ${sizeTable}
836
+
837
+ > These numbers sum source directories for planning. **Uploaded package size is determined by the build output**, but source trends usually match.
838
+
839
+ ### Development constraints
840
+
841
+ ${rules}${warnings}`;
842
+ }
843
+ function renderSizeTableZh(scan) {
844
+ const rows = [
845
+ renderPackageRowZh("\u4E3B\u5305", scan.mainPackage),
846
+ ...scan.subpackages.map((item) => renderPackageRowZh(`\u5206\u5305 ${item.name}`, item))
847
+ ];
848
+ rows.push(`| \u5408\u8BA1\uFF08\u6E90\u7801\u4F30\u7B97\uFF09 | \u2014 | ${formatBytes2(scan.totalSizeBytes)} | \u2264 ${TOTAL_LIMIT_MB} MB | \u53C2\u8003 |`);
849
+ return `| \u5305 | \u6839\u76EE\u5F55 | \u6E90\u7801\u4F53\u79EF\uFF08\u4F30\u7B97\uFF09 | \u4E0A\u9650 | \u72B6\u6001 |
850
+ |----|--------|------------------|------|------|
851
+ ${rows.join("\n")}`;
852
+ }
853
+ function renderSizeTableEn(scan) {
854
+ const rows = [
855
+ renderPackageRowEn("main", scan.mainPackage),
856
+ ...scan.subpackages.map((item) => renderPackageRowEn(`subpackage ${item.name}`, item))
857
+ ];
858
+ rows.push(`| total (source estimate) | \u2014 | ${formatBytes2(scan.totalSizeBytes)} | \u2264 ${TOTAL_LIMIT_MB} MB | reference |`);
859
+ return `| package | root | estimated source size | limit | status |
860
+ |---------|------|------------------------|-------|--------|
861
+ ${rows.join("\n")}`;
862
+ }
863
+ function renderPackageRowZh(label, pkg) {
864
+ return `| ${label} | \`${pkg.root}\` | ${pkg.sizeLabel} | \u2264 ${pkg.limitBytes / (1024 * 1024)} MB | ${statusLabelZh(pkg.status)} |`;
865
+ }
866
+ function renderPackageRowEn(label, pkg) {
867
+ return `| ${label} | \`${pkg.root}\` | ${pkg.sizeLabel} | \u2264 ${pkg.limitBytes / (1024 * 1024)} MB | ${statusLabelEn(pkg.status)} |`;
868
+ }
869
+ function renderConstraintRulesZh(scan) {
870
+ const blocked = collectBlockedPackages(scan);
871
+ const lines = [
872
+ `- \u5FAE\u4FE1\u5C0F\u7A0B\u5E8F\u4F53\u79EF\u4E0A\u9650\uFF08\u53D1\u5E03\u5230\u5FAE\u4FE1\u65F6\uFF09\uFF1A**\u4E3B\u5305 \u2264 ${MAIN_LIMIT_MB}MB**\uFF0C**\u5355\u4E2A\u5206\u5305 \u2264 ${SUB_LIMIT_MB}MB**\uFF0C**\u6574\u5305 \u2264 ${TOTAL_LIMIT_MB}MB**\uFF08\u4EE5[\u5FAE\u4FE1\u5B98\u65B9\u6587\u6863](https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages.html)\u4E3A\u51C6\uFF09\u3002`,
873
+ `- **\u63A5\u8FD1\u4E0A\u9650\u9608\u503C\uFF1A\u6E90\u7801\u76EE\u5F55 \u2265 ${NEAR_LIMIT_MB}MB** \u5373\u89C6\u4E3A\u300C\u63A5\u8FD1 2MB\u300D\uFF1A\u7981\u6B62\u5728\u8BE5\u5305\u5185**\u65B0\u589E\u9875\u9762**\u3001\u5927\u56FE\u3001\u97F3\u89C6\u9891\u3001\u91CD\u590D\u9759\u6001\u8D44\u6E90\uFF1B\u4F18\u5148\u62C6\u5230\u5176\u4ED6\u5206\u5305\u3001\u61D2\u52A0\u8F7D\u6216\u5148\u505A\u4F53\u79EF\u4F18\u5316\u3002`,
874
+ `- \u4F53\u79EF **\u2265 ${MAIN_LIMIT_MB}MB\uFF08\u4E3B\u5305\uFF09\u6216 \u2265 ${SUB_LIMIT_MB}MB\uFF08\u5206\u5305\uFF09** \u65F6\uFF0C\u5FC5\u987B\u5148\u7626\u8EAB\u518D\u65B0\u589E\u529F\u80FD\u6216\u9875\u9762\u3002`,
875
+ `- \u65B0\u589E\u9875\u9762\u524D\uFF1A\u786E\u8BA4\u76EE\u6807\u5305\u4E0D\u5728\u300C\u63A5\u8FD1\u4E0A\u9650\u300D\u6216\u300C\u8D85\u9650\u300D\u5217\u8868\uFF1B\u8DE8\u5305\u8FC1\u79FB\u9875\u9762\u65F6\u540C\u6B65\u66F4\u65B0 \`app.json\` / \u5206\u5305\u914D\u7F6E\u4E0E\u8DEF\u7531\u5F15\u7528\u3002`,
876
+ `- Taro / uni-app \u53D1\u5E03\u5230\u5FAE\u4FE1\u65F6\u540C\u6837\u53D7\u4E0A\u8FF0\u4E0A\u4F20\u4F53\u79EF\u7EA6\u675F\uFF1B\u5176\u4ED6\u7AEF\u89C4\u5219\u89C1\u5BF9\u5E94\u5E73\u53F0\u6587\u6863\u3002`
877
+ ];
878
+ if (blocked.near.length) {
879
+ lines.push(
880
+ `- **\u5F53\u524D\u63A5\u8FD1\u4E0A\u9650\uFF08\u2265${NEAR_LIMIT_MB}MB\uFF0C\u7981\u6B62\u5728\u672C\u5305\u5185\u65B0\u589E\u9875\u9762\uFF09**\uFF1A${blocked.near.map((item) => `\`${item}\``).join("\u3001")}\u3002`
881
+ );
882
+ }
883
+ if (blocked.over.length) {
884
+ lines.push(`- **\u5F53\u524D\u5DF2\u8D85\u9650\uFF08\u5FC5\u987B\u5148\u7626\u8EAB\uFF09**\uFF1A${blocked.over.map((item) => `\`${item}\``).join("\u3001")}\u3002`);
885
+ }
886
+ if (!blocked.near.length && !blocked.over.length) {
887
+ lines.push("- \u5F53\u524D\u626B\u63CF\u672A\u53D1\u73B0\u63A5\u8FD1 2MB \u7684\u4E3B\u5305/\u5206\u5305\u76EE\u5F55\uFF0C\u4F46\u4ECD\u5E94\u5728\u6BCF\u6B21\u8F83\u5927\u6539\u52A8\u540E\u590D\u6838\u6784\u5EFA\u4EA7\u7269\u4F53\u79EF\u3002");
888
+ }
889
+ return lines.join("\n");
890
+ }
891
+ function renderConstraintRulesEn(scan) {
892
+ const blocked = collectBlockedPackages(scan);
893
+ const lines = [
894
+ `- WeChat upload limits: **main package \u2264 ${MAIN_LIMIT_MB}MB**, **each subpackage \u2264 ${SUB_LIMIT_MB}MB**, **whole mini program \u2264 ${TOTAL_LIMIT_MB}MB** (see WeChat official docs).`,
895
+ `- Treat **source tree \u2265 ${NEAR_LIMIT_MB}MB** as near the 2MB cap: do **not** add new pages, large media, or redundant static assets in that package; split work to another subpackage or optimize first.`,
896
+ `- If a package is **\u2265 ${MAIN_LIMIT_MB}MB (main) or \u2265 ${SUB_LIMIT_MB}MB (sub)** , optimize before adding pages or features.`,
897
+ `- Before adding a page, confirm the target package is not listed below; update \`app.json\` / subpackage config when moving pages.`,
898
+ `- Taro / uni-app builds for WeChat follow the same upload limits.`
899
+ ];
900
+ if (blocked.near.length) {
901
+ lines.push(`- **Near limit (\u2265${NEAR_LIMIT_MB}MB, no new pages in these packages):** ${blocked.near.map((item) => `\`${item}\``).join(", ")}.`);
902
+ }
903
+ if (blocked.over.length) {
904
+ lines.push(`- **Over limit (optimize before more work):** ${blocked.over.map((item) => `\`${item}\``).join(", ")}.`);
905
+ }
906
+ if (!blocked.near.length && !blocked.over.length) {
907
+ lines.push("- No package is near 2MB in this scan; still verify build output after large changes.");
908
+ }
909
+ return lines.join("\n");
910
+ }
911
+ function collectBlockedPackages(scan) {
912
+ const near = [];
913
+ const over = [];
914
+ const all = [scan.mainPackage, ...scan.subpackages];
915
+ for (const pkg of all) {
916
+ const label = pkg.name === "main" ? `\u4E3B\u5305(${pkg.root})` : `${pkg.name}(${pkg.root})`;
917
+ if (pkg.status === "over_limit") {
918
+ over.push(label);
919
+ } else if (pkg.status === "near_limit") {
920
+ near.push(label);
921
+ }
922
+ }
923
+ return { near, over };
924
+ }
925
+ function statusLabelZh(status) {
926
+ if (status === "over_limit") {
927
+ return "\u8D85\u9650";
928
+ }
929
+ if (status === "near_limit") {
930
+ return "\u63A5\u8FD1\u4E0A\u9650";
931
+ }
932
+ return "\u6B63\u5E38";
933
+ }
934
+ function statusLabelEn(status) {
935
+ if (status === "over_limit") {
936
+ return "over limit";
937
+ }
938
+ if (status === "near_limit") {
939
+ return "near limit";
940
+ }
941
+ return "ok";
942
+ }
943
+ function formatBytes2(bytes) {
944
+ if (bytes >= 1024 * 1024) {
945
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
946
+ }
947
+ if (bytes >= 1024) {
948
+ return `${(bytes / 1024).toFixed(1)} KB`;
949
+ }
950
+ return `${bytes} B`;
951
+ }
952
+ function renderMiniprogramNotApplicableSection(language) {
953
+ if (language === "en") {
954
+ return `## Mini Program
955
+
956
+ Not detected as a mini program project. Leave this section as N/A or remove it if irrelevant.`;
957
+ }
958
+ return `## \u5C0F\u7A0B\u5E8F
959
+
960
+ \u672A\u8BC6\u522B\u4E3A\u5C0F\u7A0B\u5E8F\u5DE5\u7A0B\uFF1B\u5982\u65E0\u5C0F\u7A0B\u5E8F\u573A\u666F\u53EF\u586B\u5199\u300C\u4E0D\u9002\u7528\u300D\u6216\u5220\u9664\u672C\u8282\u3002`;
961
+ }
962
+ function patchAgentsMiniprogramSection(content, sectionMarkdown, language) {
963
+ const heading = language === "en" ? "## Mini Program" : "## \u5C0F\u7A0B\u5E8F";
964
+ const autoBegin = "<!-- FET:BEGIN AUTO -->";
965
+ const autoEnd = "<!-- FET:END AUTO -->";
966
+ const begin = content.indexOf(autoBegin);
967
+ const end = content.indexOf(autoEnd);
968
+ if (begin === -1 || end === -1 || end < begin) {
969
+ return content;
970
+ }
971
+ const auto = content.slice(begin, end);
972
+ const headingIndex = auto.indexOf(heading);
973
+ if (headingIndex === -1) {
974
+ const insertion = `
975
+
976
+ ${sectionMarkdown}
977
+ `;
978
+ return `${content.slice(0, end)}${insertion}${content.slice(end)}`;
979
+ }
980
+ const afterHeading = auto.slice(headingIndex + heading.length);
981
+ const nextHeading = afterHeading.search(/\n## /);
982
+ const sectionEnd = nextHeading === -1 ? auto.length : headingIndex + heading.length + nextHeading;
983
+ const absoluteStart = begin + headingIndex;
984
+ const absoluteEnd = begin + sectionEnd;
985
+ return `${content.slice(0, absoluteStart)}${sectionMarkdown}${content.slice(absoluteEnd)}`;
986
+ }
987
+
988
+ // src/agents-miniprogram.ts
989
+ async function applyMiniprogramAgentsContext(projectRoot, language) {
990
+ const agentsPath = join9(projectRoot, "AGENTS.md");
991
+ const detection = await detectMiniprogramProject(projectRoot);
992
+ let existing;
993
+ try {
994
+ existing = await readFile7(agentsPath, "utf8");
995
+ } catch {
996
+ return {
997
+ applied: false,
998
+ detection,
999
+ summary: language === "en" ? "AGENTS.md was not found." : "\u672A\u627E\u5230 AGENTS.md\u3002"
1000
+ };
1001
+ }
1002
+ const section = detection.supported ? renderMiniprogramFilledSection(detection, language) : renderMiniprogramNotApplicableSection(language);
1003
+ const next = patchAgentsMiniprogramSection(existing, section, language);
1004
+ if (next === existing) {
1005
+ return {
1006
+ applied: false,
1007
+ detection,
1008
+ summary: language === "en" ? "AGENTS.md mini program section was unchanged." : "AGENTS.md \u5C0F\u7A0B\u5E8F\u8282\u672A\u53D8\u66F4\u3002"
1009
+ };
1010
+ }
1011
+ await atomicWrite(agentsPath, next);
1012
+ if (!detection.supported) {
1013
+ return {
1014
+ applied: true,
1015
+ detection,
1016
+ summary: language === "en" ? "Marked mini program section as not applicable." : "\u5DF2\u5C06\u5C0F\u7A0B\u5E8F\u8282\u6807\u8BB0\u4E3A\u4E0D\u9002\u7528\u3002"
1017
+ };
1018
+ }
1019
+ const near = [detection.mainPackage, ...detection.subpackages].filter((pkg) => pkg.status !== "ok");
1020
+ const summary = language === "en" ? near.length ? `Updated AGENTS.md mini program constraints (${near.length} package(s) near or over limit).` : "Updated AGENTS.md mini program constraints and package size table." : near.length ? `\u5DF2\u66F4\u65B0 AGENTS.md \u5C0F\u7A0B\u5E8F\u7EA6\u675F\uFF08${near.length} \u4E2A\u5305\u63A5\u8FD1\u6216\u8D85\u8FC7\u4F53\u79EF\u4E0A\u9650\uFF09\u3002` : "\u5DF2\u66F4\u65B0 AGENTS.md \u5C0F\u7A0B\u5E8F\u5305\u4F53\u79EF\u8868\u4E0E\u5F00\u53D1\u7EA6\u675F\u3002";
1021
+ return { applied: true, detection, summary };
1022
+ }
1023
+
1024
+ // src/commands/fill-context.ts
370
1025
  async function fillContextCommand(ctx) {
1026
+ let miniprogramSummary;
371
1027
  await withProjectLock(ctx.projectRoot, { command: "fill-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
372
- const handoffPath = join7(ctx.projectRoot, ".fet", "fill-context.md");
1028
+ const miniprogramResult = await applyMiniprogramAgentsContext(ctx.projectRoot, ctx.language);
1029
+ miniprogramSummary = miniprogramResult.summary;
1030
+ const handoffPath = join10(ctx.projectRoot, ".fet", "fill-context.md");
373
1031
  await mkdir3(dirname4(handoffPath), { recursive: true });
374
- await atomicWrite(handoffPath, renderGenericHandoff(ctx.language));
1032
+ await atomicWrite(handoffPath, renderGenericHandoff(ctx.language, miniprogramResult.detection.supported));
375
1033
  for (const adapter of ctx.toolAdapters) {
376
1034
  const plan = await adapter.planInstall(ctx.projectRoot, ctx.language);
377
1035
  const result = await adapter.install(ctx.projectRoot, plan, ctx.yes);
@@ -388,10 +1046,12 @@ async function fillContextCommand(ctx) {
388
1046
  }
389
1047
  });
390
1048
  const placeholders = await countAgentsLlmPlaceholders(ctx.projectRoot);
1049
+ const warnings = miniprogramSummary ? [miniprogramSummary] : void 0;
391
1050
  ctx.output.result({
392
1051
  ok: true,
393
1052
  command: "fill-context",
394
- summary: ctx.language === "en" ? placeholders ? `Found ${placeholders} AGENTS.md placeholder(s). Use your IDE AI to fill them.` : "No AGENTS.md placeholders found. IDE fill-context commands were refreshed." : placeholders ? `\u53D1\u73B0 ${placeholders} \u4E2A AGENTS.md \u5360\u4F4D\u7B26\u3002\u8BF7\u4F7F\u7528 IDE AI \u8865\u9F50\u3002` : "\u672A\u53D1\u73B0 AGENTS.md \u5360\u4F4D\u7B26\uFF0C\u5DF2\u5237\u65B0 IDE fill-context \u547D\u4EE4\u3002",
1053
+ summary: ctx.language === "en" ? placeholders ? `Found ${placeholders} AGENTS.md placeholder(s). Use your IDE AI to fill the rest.` : "AGENTS.md placeholders are complete. IDE fill-context commands were refreshed." : placeholders ? `\u53D1\u73B0 ${placeholders} \u4E2A AGENTS.md \u5360\u4F4D\u7B26\u3002\u8BF7\u4F7F\u7528 IDE AI \u8865\u9F50\u5176\u4F59\u90E8\u5206\u3002` : "AGENTS.md \u5360\u4F4D\u7B26\u5DF2\u8865\u9F50\uFF0C\u5DF2\u5237\u65B0 IDE fill-context \u547D\u4EE4\u3002",
1054
+ warnings,
395
1055
  nextSteps: ctx.language === "en" ? placeholders ? [
396
1056
  "Cursor: run /fet-fill-context",
397
1057
  "Codex: run /prompts:fet-fill-context",
@@ -408,7 +1068,9 @@ async function fillContextCommand(ctx) {
408
1068
  }
409
1069
  });
410
1070
  }
411
- function renderGenericHandoff(language) {
1071
+ function renderGenericHandoff(language, miniprogramDetected) {
1072
+ const miniprogramNoteEn = miniprogramDetected ? "FET already scanned the mini program layout and wrote package-size constraints into AGENTS.md. Do not overwrite the Mini Program / package-size / development-constraint subsections unless the repo changed." : "If AGENTS.md has a Mini Program section marked not applicable, keep it unless the project is actually a mini program.";
1073
+ const miniprogramNoteZh = miniprogramDetected ? "FET \u5DF2\u626B\u63CF\u5C0F\u7A0B\u5E8F\u76EE\u5F55\u5E76\u628A\u5305\u4F53\u79EF/\u5F00\u53D1\u7EA6\u675F\u5199\u5165 AGENTS.md\u3002\u9664\u975E\u4ED3\u5E93\u7ED3\u6784\u5DF2\u53D8\uFF0C\u4E0D\u8981\u8986\u76D6\u300C\u5C0F\u7A0B\u5E8F\u300D\u8282\u4E2D\u7684\u5305\u4F53\u79EF\u8868\u4E0E\u5F00\u53D1\u7EA6\u675F\u3002" : "\u82E5 AGENTS.md \u5C0F\u7A0B\u5E8F\u8282\u5DF2\u6807\u8BB0\u4E3A\u4E0D\u9002\u7528\uFF0C\u4E14\u9879\u76EE\u786E\u5B9E\u4E0D\u662F\u5C0F\u7A0B\u5E8F\uFF0C\u53EF\u4FDD\u7559\u8BE5\u8BF4\u660E\u3002";
412
1074
  if (language === "en") {
413
1075
  return `<!-- FET:MANAGED
414
1076
  schemaVersion: 1
@@ -422,10 +1084,11 @@ Use the IDE AI to complete FET-generated placeholders.
422
1084
  1. Read AGENTS.md and openspec/config.yaml.
423
1085
  2. Read .fet/karpathy-guidelines.md when it exists. For Codex, also read .codex/fet/karpathy-guidelines.md when it exists.
424
1086
  3. Inspect README files, package scripts, routes, tests, source layout, and project conventions.
425
- 4. Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete project-specific content.
426
- 5. Preserve FET managed markers.
427
- 6. Do not modify business code.
428
- 7. Run \`fet doctor\` and confirm no AGENTS.md placeholder warning remains.
1087
+ 4. ${miniprogramNoteEn}
1088
+ 5. Replace every remaining \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete project-specific content.
1089
+ 6. Preserve FET managed markers.
1090
+ 7. Do not modify business code.
1091
+ 8. Run \`fet doctor\` and confirm no AGENTS.md placeholder warning remains.
429
1092
  `;
430
1093
  }
431
1094
  return `<!-- FET:MANAGED
@@ -440,20 +1103,21 @@ FET:END -->
440
1103
  1. \u9605\u8BFB AGENTS.md \u548C openspec/config.yaml\u3002
441
1104
  2. \u5982\u679C\u5B58\u5728 .fet/karpathy-guidelines.md\uFF0C\u8BF7\u4E00\u5E76\u9605\u8BFB\u3002\u5BF9 Codex\uFF0C\u5982\u679C\u5B58\u5728 .codex/fet/karpathy-guidelines.md\uFF0C\u4E5F\u8981\u9605\u8BFB\u3002
442
1105
  3. \u68C0\u67E5 README\u3001package scripts\u3001\u8DEF\u7531\u3001\u6D4B\u8BD5\u3001\u6E90\u7801\u7ED3\u6784\u548C\u9879\u76EE\u7EA6\u5B9A\u3002
443
- 4. \u5C06 AGENTS.md \u4E2D\u6BCF\u4E2A \`[NEEDS LLM INPUT]\` \u6216 \`[NEED LLM INPUT]\` \u5360\u4F4D\u7B26\u66FF\u6362\u4E3A\u5177\u4F53\u3001\u9879\u76EE\u76F8\u5173\u7684\u5185\u5BB9\u3002
444
- 5. \u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\u3002
445
- 6. \u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002
446
- 7. \u8FD0\u884C \`fet doctor\`\uFF0C\u786E\u8BA4\u4E0D\u518D\u6709 AGENTS.md \u5360\u4F4D\u7B26\u8B66\u544A\u3002
1106
+ 4. ${miniprogramNoteZh}
1107
+ 5. \u5C06\u5176\u4F59 \`[NEEDS LLM INPUT]\` \u6216 \`[NEED LLM INPUT]\` \u5360\u4F4D\u7B26\u66FF\u6362\u4E3A\u5177\u4F53\u3001\u9879\u76EE\u76F8\u5173\u7684\u5185\u5BB9\u3002
1108
+ 6. \u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\u3002
1109
+ 7. \u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002
1110
+ 8. \u8FD0\u884C \`fet doctor\`\uFF0C\u786E\u8BA4\u4E0D\u518D\u6709 AGENTS.md \u5360\u4F4D\u7B26\u8B66\u544A\u3002
447
1111
  `;
448
1112
  }
449
1113
 
450
1114
  // src/commands/graph.ts
451
1115
  import { mkdir as mkdir5 } from "fs/promises";
452
- import { dirname as dirname6, join as join9 } from "path";
1116
+ import { dirname as dirname6, join as join12 } from "path";
453
1117
 
454
1118
  // src/graph-context.ts
455
- import { mkdir as mkdir4, readdir, readFile as readFile5 } from "fs/promises";
456
- import { dirname as dirname5, join as join8 } from "path";
1119
+ import { mkdir as mkdir4, readdir as readdir2, readFile as readFile8 } from "fs/promises";
1120
+ import { dirname as dirname5, join as join11 } from "path";
457
1121
  var MAX_SOURCE_CONTEXT = 8e3;
458
1122
  var MAX_GRAPH_OUTPUT = 2e4;
459
1123
  async function buildProjectGraphContext(ctx, state, trigger) {
@@ -472,7 +1136,7 @@ async function buildProjectGraphContext(ctx, state, trigger) {
472
1136
  const warnings = commandWarnings([["gitnexus query", graphQuery], ["gitnexus status", status]]);
473
1137
  const relativePath = ".fet/graph-context/project.md";
474
1138
  await writeGraphContext(
475
- join8(ctx.projectRoot, relativePath),
1139
+ join11(ctx.projectRoot, relativePath),
476
1140
  renderProjectContext({
477
1141
  trigger,
478
1142
  state,
@@ -520,7 +1184,7 @@ async function buildWorkflowGraphContext(ctx, options) {
520
1184
  ]);
521
1185
  const relativePath = `.fet/graph-context/${sanitizePathPart(options.changeId ?? options.command)}.md`;
522
1186
  await writeGraphContext(
523
- join8(ctx.projectRoot, relativePath),
1187
+ join11(ctx.projectRoot, relativePath),
524
1188
  renderWorkflowContext({
525
1189
  state,
526
1190
  command: options.command,
@@ -666,16 +1330,16 @@ async function collectOpenSpecContext(projectRoot, changeId) {
666
1330
  if (!changeId) {
667
1331
  return "";
668
1332
  }
669
- const changeRoot = join8(projectRoot, "openspec", "changes", changeId);
1333
+ const changeRoot = join11(projectRoot, "openspec", "changes", changeId);
670
1334
  const chunks = [];
671
1335
  for (const file of ["proposal.md", "design.md", "tasks.md", "README.md"]) {
672
- const content = await readOptional(join8(changeRoot, file));
1336
+ const content = await readOptional(join11(changeRoot, file));
673
1337
  if (content) {
674
1338
  chunks.push(`## ${file}
675
1339
  ${content}`);
676
1340
  }
677
1341
  }
678
- const specsRoot = join8(changeRoot, "specs");
1342
+ const specsRoot = join11(changeRoot, "specs");
679
1343
  for (const spec of await listSpecFiles(specsRoot)) {
680
1344
  const content = await readOptional(spec.path);
681
1345
  if (content) {
@@ -687,9 +1351,9 @@ ${content}`);
687
1351
  }
688
1352
  async function listSpecFiles(specsRoot) {
689
1353
  try {
690
- const capabilities = await readdir(specsRoot, { withFileTypes: true });
1354
+ const capabilities = await readdir2(specsRoot, { withFileTypes: true });
691
1355
  return capabilities.filter((entry) => entry.isDirectory()).map((entry) => ({
692
- path: join8(specsRoot, entry.name, "spec.md"),
1356
+ path: join11(specsRoot, entry.name, "spec.md"),
693
1357
  label: `specs/${entry.name}/spec.md`
694
1358
  }));
695
1359
  } catch {
@@ -698,7 +1362,7 @@ async function listSpecFiles(specsRoot) {
698
1362
  }
699
1363
  async function readOptional(path) {
700
1364
  try {
701
- return await readFile5(path, "utf8");
1365
+ return await readFile8(path, "utf8");
702
1366
  } catch {
703
1367
  return null;
704
1368
  }
@@ -809,7 +1473,7 @@ async function graphDoctorCommand(ctx) {
809
1473
  }
810
1474
  async function graphSetupCommand(ctx) {
811
1475
  let result;
812
- const handoffPath = join9(ctx.projectRoot, ".fet", "graph-setup.md");
1476
+ const handoffPath = join12(ctx.projectRoot, ".fet", "graph-setup.md");
813
1477
  const installCommand = process.env.FET_GITNEXUS_INSTALL_COMMAND?.trim() || null;
814
1478
  await withProjectLock(ctx.projectRoot, { command: "graph setup", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
815
1479
  result = await refreshGraphState(ctx, { write: false });
@@ -845,7 +1509,7 @@ async function graphSetupCommand(ctx) {
845
1509
  }
846
1510
  async function graphHandoffCommand(ctx) {
847
1511
  let result;
848
- const handoffPath = join9(ctx.projectRoot, ".fet", "graph-handoff.md");
1512
+ const handoffPath = join12(ctx.projectRoot, ".fet", "graph-handoff.md");
849
1513
  await withProjectLock(ctx.projectRoot, { command: "graph handoff", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
850
1514
  result = await refreshGraphState(ctx, { runStatus: true, write: false });
851
1515
  await writeHandoffFile(handoffPath, renderGraphUsageHandoff(result.state, ctx.language));
@@ -1100,19 +1764,41 @@ function firstLine2(value) {
1100
1764
  }
1101
1765
 
1102
1766
  // src/commands/init.ts
1103
- import { readFile as readFile8, stat as stat4 } from "fs/promises";
1104
- import { join as join12 } from "path";
1767
+ import { stat as stat6 } from "fs/promises";
1768
+ import { join as join15 } from "path";
1769
+
1770
+ // src/commands/update-context.ts
1771
+ import { readFile as readFile10 } from "fs/promises";
1772
+ import { createInterface } from "readline/promises";
1773
+ import { join as join14 } from "path";
1774
+
1775
+ // src/config/yaml.ts
1776
+ import { readFile as readFile9 } from "fs/promises";
1777
+ import { parseDocument } from "yaml";
1778
+ async function mergeFetConfig(configPath, renderedFetYaml) {
1779
+ const fetDoc = parseDocument(renderedFetYaml);
1780
+ const nextFet = fetDoc.get("fet", true);
1781
+ let existing = "";
1782
+ try {
1783
+ existing = await readFile9(configPath, "utf8");
1784
+ } catch {
1785
+ return renderedFetYaml;
1786
+ }
1787
+ const doc = parseDocument(existing || "{}");
1788
+ doc.set("fet", nextFet);
1789
+ return doc.toString();
1790
+ }
1105
1791
 
1106
1792
  // src/version.ts
1107
1793
  import { existsSync, readFileSync } from "fs";
1108
- import { dirname as dirname7, join as join10, parse } from "path";
1794
+ import { dirname as dirname7, join as join13, parse as parse2 } from "path";
1109
1795
  import { fileURLToPath } from "url";
1110
1796
  var FET_VERSION = readPackageVersion();
1111
1797
  function readPackageVersion() {
1112
1798
  let currentDir = dirname7(fileURLToPath(import.meta.url));
1113
- const root = parse(currentDir).root;
1799
+ const root = parse2(currentDir).root;
1114
1800
  while (true) {
1115
- const packageJsonPath = join10(currentDir, "package.json");
1801
+ const packageJsonPath = join13(currentDir, "package.json");
1116
1802
  if (existsSync(packageJsonPath)) {
1117
1803
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
1118
1804
  if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
@@ -1191,6 +1877,12 @@ function sectionKey(heading) {
1191
1877
  "ai \u5DE5\u4F5C\u6307\u5357": "ai-guidelines",
1192
1878
  "scanner metadata": "metadata",
1193
1879
  "\u626B\u63CF\u5143\u6570\u636E": "metadata",
1880
+ "mini program": "miniprogram",
1881
+ "\u5C0F\u7A0B\u5E8F": "miniprogram",
1882
+ "package size (filled by `fet fill-context`)": "miniprogram-size",
1883
+ "\u5305\u4F53\u79EF\uFF08\u7531 `fet fill-context` \u626B\u63CF\u8865\u5145\uFF09": "miniprogram-size",
1884
+ "development constraints": "miniprogram-rules",
1885
+ "\u5F00\u53D1\u7EA6\u675F": "miniprogram-rules",
1194
1886
  "notes for ai": "notes",
1195
1887
  "\u7ED9 ai \u7684\u5907\u6CE8": "notes"
1196
1888
  };
@@ -1271,6 +1963,8 @@ ${routes || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | low |"}
1271
1963
 
1272
1964
  [NEEDS LLM INPUT]
1273
1965
 
1966
+ ${renderMiniprogramPlaceholderSection("zh-CN")}
1967
+
1274
1968
  ## AI \u5DE5\u4F5C\u6307\u5357
1275
1969
 
1276
1970
  - \u4F7F\u7528 FET \u6258\u7BA1\u7684 IDE \u5DE5\u4F5C\u6D41\u65F6\uFF0C\u4F18\u5148\u53C2\u8003 .fet/karpathy-guidelines.md \u4E2D\u7684\u9879\u76EE\u7EA7\u6307\u5357\u3002
@@ -1334,6 +2028,8 @@ ${routes || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | low |"}
1334
2028
 
1335
2029
  [NEEDS LLM INPUT]
1336
2030
 
2031
+ ${renderMiniprogramPlaceholderSection("en")}
2032
+
1337
2033
  ## AI Work Guidelines
1338
2034
 
1339
2035
  - Prefer the project-level Andrej Karpathy inspired guidelines in .fet/karpathy-guidelines.md when using FET-managed IDE workflows.
@@ -1383,6 +2079,10 @@ function renderFetConfig(scan, language = "zh-CN") {
1383
2079
  test: "warn"
1384
2080
  },
1385
2081
  workspaces: scan.project.workspaces
2082
+ },
2083
+ figmaGuard: {
2084
+ enabled: true,
2085
+ onUncertainty: "stop_and_ask"
1386
2086
  }
1387
2087
  }
1388
2088
  });
@@ -1535,64 +2235,124 @@ purpose: manual-verify
1535
2235
  2. \u6309\u9879\u76EE\u7EA6\u5B9A\u8FD0\u884C lint\u3001typecheck\u3001test\u3002
1536
2236
  3. \u68C0\u67E5\u672C\u6B21 change \u7684 \`tasks.md\` \u662F\u5426\u4E0E\u5B9E\u73B0\u72B6\u6001\u4E00\u81F4\u3002
1537
2237
 
1538
- \u5B8C\u6210\u540E\u8FD0\u884C\uFF1A
2238
+ \u5B8C\u6210\u540E\u8FD0\u884C\uFF1A
2239
+
2240
+ \`\`\`sh
2241
+ fet verify --done --change ${changeId}
2242
+ \`\`\`
2243
+ `;
2244
+ }
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}
1539
2345
 
1540
- \`\`\`sh
1541
- fet verify --done --change ${changeId}
1542
- \`\`\`
1543
- `;
1544
- }
2346
+ ## Sources
1545
2347
 
1546
- // src/templates/gitignore.ts
1547
- var BEGIN2 = "# FET:BEGIN LOCAL STATE";
1548
- var END2 = "# FET:END LOCAL STATE";
1549
- var RULES = [
1550
- "openspec/fet-state.json",
1551
- "openspec/.fet.lock",
1552
- "openspec/.fet-init-journal.json",
1553
- "openspec/changes/*/fet-state.json",
1554
- "openspec/changes/*/.fet/",
1555
- ".gitnexus/"
1556
- ];
1557
- function mergeGitignore(existing) {
1558
- const block = `${BEGIN2}
1559
- ${RULES.join("\n")}
1560
- ${END2}`;
1561
- if (!existing || !existing.trim()) {
1562
- return `${block}
1563
- `;
1564
- }
1565
- const start = existing.indexOf(BEGIN2);
1566
- const end = existing.indexOf(END2);
1567
- if (start !== -1 && end !== -1 && end > start) {
1568
- return `${existing.slice(0, start)}${block}${existing.slice(end + END2.length)}`;
1569
- }
1570
- return `${existing.replace(/\s*$/, "")}
2348
+ ${sourceList}
1571
2349
 
1572
- ${block}
2350
+ ${renderFigmaStopProtocolBody(options.language)}
1573
2351
  `;
1574
2352
  }
1575
-
1576
- // src/commands/update-context.ts
1577
- import { readFile as readFile7 } from "fs/promises";
1578
- import { createInterface } from "readline/promises";
1579
- import { join as join11 } from "path";
1580
-
1581
- // src/config/yaml.ts
1582
- import { readFile as readFile6 } from "fs/promises";
1583
- import { parseDocument } from "yaml";
1584
- async function mergeFetConfig(configPath, renderedFetYaml) {
1585
- const fetDoc = parseDocument(renderedFetYaml);
1586
- const nextFet = fetDoc.get("fet", true);
1587
- let existing = "";
1588
- try {
1589
- existing = await readFile6(configPath, "utf8");
1590
- } catch {
1591
- return renderedFetYaml;
1592
- }
1593
- const doc = parseDocument(existing || "{}");
1594
- doc.set("fet", nextFet);
1595
- return doc.toString();
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`;
1596
2356
  }
1597
2357
 
1598
2358
  // src/commands/update-context.ts
@@ -1610,11 +2370,11 @@ async function updateContextCommand(ctx) {
1610
2370
  }
1611
2371
  async function updateContextFiles(ctx) {
1612
2372
  const scan = await ctx.scanner.scan(ctx.projectRoot, {});
1613
- const agentsPath = join11(ctx.projectRoot, "AGENTS.md");
1614
- const configPath = join11(ctx.projectRoot, "openspec", "config.yaml");
1615
- const claudePath = join11(ctx.projectRoot, "CLAUDE.md");
1616
- const karpathyHandoffPath = join11(ctx.projectRoot, ".fet", "karpathy-guidelines.md");
1617
- const karpathyCursorPath = join11(ctx.projectRoot, ".cursor", "rules", "karpathy-guidelines.mdc");
2373
+ const agentsPath = join14(ctx.projectRoot, "AGENTS.md");
2374
+ const configPath = join14(ctx.projectRoot, "openspec", "config.yaml");
2375
+ const claudePath = join14(ctx.projectRoot, "CLAUDE.md");
2376
+ const karpathyHandoffPath = join14(ctx.projectRoot, ".fet", "karpathy-guidelines.md");
2377
+ const karpathyCursorPath = join14(ctx.projectRoot, ".cursor", "rules", "karpathy-guidelines.mdc");
1618
2378
  const existingAgents = await readOptional2(agentsPath);
1619
2379
  const existingClaude = await readOptional2(claudePath);
1620
2380
  const existingKarpathyCursor = await readOptional2(karpathyCursorPath);
@@ -1688,7 +2448,7 @@ async function confirmInitCanReplaceUnmanagedAgents(ctx) {
1688
2448
  }
1689
2449
  async function readOptional2(path) {
1690
2450
  try {
1691
- return await readFile7(path, "utf8");
2451
+ return await readFile10(path, "utf8");
1692
2452
  } catch {
1693
2453
  return null;
1694
2454
  }
@@ -1696,7 +2456,7 @@ async function readOptional2(path) {
1696
2456
 
1697
2457
  // src/commands/init.ts
1698
2458
  async function initCommand(ctx) {
1699
- const alreadyInitialized = await exists2(join12(ctx.projectRoot, "openspec", "config.yaml"));
2459
+ const alreadyInitialized = await exists4(join15(ctx.projectRoot, "openspec", "config.yaml"));
1700
2460
  let warnings = [];
1701
2461
  await withProjectLock(ctx.projectRoot, { command: "init", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
1702
2462
  const journal = createInitJournal(ctx.fetVersion);
@@ -1711,7 +2471,6 @@ async function initCommand(ctx) {
1711
2471
  }
1712
2472
  const contextResult = await updateContextFiles(ctx);
1713
2473
  warnings = contextResult.warnings;
1714
- await ensureGitignore(ctx);
1715
2474
  const state = await ctx.stateStore.getOrCreateGlobal();
1716
2475
  state.openspec = identity;
1717
2476
  state.language = {
@@ -1747,30 +2506,138 @@ async function initCommand(ctx) {
1747
2506
  nextSteps: ctx.language === "en" ? ["Use fet propose/new to create an OpenSpec change", "Use fet doctor to check project health"] : ["\u4F7F\u7528 fet propose/new \u521B\u5EFA OpenSpec change", "\u4F7F\u7528 fet doctor \u68C0\u67E5\u9879\u76EE\u72B6\u6001"]
1748
2507
  });
1749
2508
  }
1750
- async function ensureGitignore(ctx) {
1751
- const gitignorePath = join12(ctx.projectRoot, ".gitignore");
1752
- const existing = await readOptional3(gitignorePath);
1753
- await atomicWrite(gitignorePath, mergeGitignore(existing));
2509
+ async function exists4(path) {
2510
+ try {
2511
+ await stat6(path);
2512
+ return true;
2513
+ } catch {
2514
+ return false;
2515
+ }
1754
2516
  }
1755
- async function readOptional3(path) {
2517
+
2518
+ // src/commands/proxy.ts
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) {
1756
2531
  try {
1757
- return await readFile8(path, "utf8");
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
+ };
1758
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) {
1759
2594
  return null;
1760
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 };
1761
2612
  }
1762
- async function exists2(path) {
2613
+ async function listMarkdownFiles(root) {
2614
+ const files = [];
2615
+ await walk(root, files);
2616
+ return files;
2617
+ }
2618
+ async function walk(dir, files) {
1763
2619
  try {
1764
- await stat4(path);
1765
- return true;
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
+ }
1766
2629
  } catch {
1767
- return false;
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;
1768
2639
  }
1769
2640
  }
1770
-
1771
- // src/commands/proxy.ts
1772
- import { readFile as readFile11 } from "fs/promises";
1773
- import { join as join14 } from "path";
1774
2641
 
1775
2642
  // src/state/project.ts
1776
2643
  import { execFile as execFile2 } from "child_process";
@@ -1799,8 +2666,8 @@ async function git(cwd, args) {
1799
2666
  }
1800
2667
 
1801
2668
  // src/state/store.ts
1802
- import { mkdir as mkdir6, readFile as readFile9 } from "fs/promises";
1803
- import { join as join13 } from "path";
2669
+ import { mkdir as mkdir6, readFile as readFile12 } from "fs/promises";
2670
+ import { join as join17 } from "path";
1804
2671
 
1805
2672
  // src/language.ts
1806
2673
  var DEFAULT_LANGUAGE = "zh-CN";
@@ -1918,7 +2785,7 @@ var StateStore = class {
1918
2785
  project;
1919
2786
  async readGlobal() {
1920
2787
  try {
1921
- const value = JSON.parse(await readFile9(this.globalPath(), "utf8"));
2788
+ const value = JSON.parse(await readFile12(this.globalPath(), "utf8"));
1922
2789
  assertGlobalState(value);
1923
2790
  return value;
1924
2791
  } catch (error) {
@@ -1933,13 +2800,13 @@ var StateStore = class {
1933
2800
  }
1934
2801
  async writeGlobal(state) {
1935
2802
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1936
- await mkdir6(join13(this.projectRoot, "openspec"), { recursive: true });
2803
+ await mkdir6(join17(this.projectRoot, "openspec"), { recursive: true });
1937
2804
  await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
1938
2805
  `);
1939
2806
  }
1940
2807
  async readChange(changeId) {
1941
2808
  try {
1942
- const value = JSON.parse(await readFile9(this.changePath(changeId), "utf8"));
2809
+ const value = JSON.parse(await readFile12(this.changePath(changeId), "utf8"));
1943
2810
  assertChangeState(value);
1944
2811
  return value;
1945
2812
  } catch (error) {
@@ -1954,15 +2821,15 @@ var StateStore = class {
1954
2821
  }
1955
2822
  async writeChange(state) {
1956
2823
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1957
- await mkdir6(join13(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
2824
+ await mkdir6(join17(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
1958
2825
  await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
1959
2826
  `);
1960
2827
  }
1961
2828
  globalPath() {
1962
- return join13(this.projectRoot, "openspec", "fet-state.json");
2829
+ return join17(this.projectRoot, "openspec", "fet-state.json");
1963
2830
  }
1964
2831
  changePath(changeId) {
1965
- return join13(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
2832
+ return join17(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
1966
2833
  }
1967
2834
  };
1968
2835
  function isNotFound(error) {
@@ -1970,11 +2837,11 @@ function isNotFound(error) {
1970
2837
  }
1971
2838
 
1972
2839
  // src/state/tasks.ts
1973
- import { readFile as readFile10 } from "fs/promises";
2840
+ import { readFile as readFile13 } from "fs/promises";
1974
2841
  async function readCompletedTaskIds(tasksPath) {
1975
2842
  let content;
1976
2843
  try {
1977
- content = await readFile10(tasksPath, "utf8");
2844
+ content = await readFile13(tasksPath, "utf8");
1978
2845
  } catch {
1979
2846
  return [];
1980
2847
  }
@@ -2123,21 +2990,31 @@ async function applyWorkflowCommand(ctx, args) {
2123
2990
  exitCode: instructions.exitCode,
2124
2991
  phaseStatus: "in_progress"
2125
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
+ }
2126
3006
  ctx.output.result({
2127
3007
  ok: true,
2128
3008
  command: "apply",
2129
3009
  summary: `fet apply prepared implementation instructions for change "${changeId}".`,
2130
3010
  warnings: [...runState.graphContext?.warnings ?? [], ...warnings ?? []],
2131
- nextSteps: [
2132
- `Read openspec/changes/${changeId}/tasks.md and the instructions output.`,
2133
- "Implement pending tasks and update task checkboxes only after the work is done.",
2134
- `Run fet verify --change ${changeId}`
2135
- ],
3011
+ nextSteps: applyNextSteps,
2136
3012
  data: {
2137
3013
  changeId,
2138
3014
  instructions: instructions.data,
2139
3015
  status,
2140
- graphContext: runState.graphContext
3016
+ graphContext: runState.graphContext,
3017
+ figmaGuard: figmaGuard ?? void 0
2141
3018
  }
2142
3019
  });
2143
3020
  });
@@ -2150,16 +3027,25 @@ async function exploreWorkflowCommand(ctx, args) {
2150
3027
  args: openSpecArgs,
2151
3028
  changeId
2152
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
+ }
2153
3042
  ctx.output.result({
2154
3043
  ok: true,
2155
3044
  command: "explore",
2156
3045
  summary: "fet explore is an IDE-guided workflow for shaping OpenSpec changes.",
2157
3046
  warnings: graphContext.warnings,
2158
- nextSteps: [
2159
- "Discuss the requirement, constraints, and acceptance criteria with the user.",
2160
- 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."
2161
- ],
2162
- data: { changeId, args: openSpecArgs, graphContext }
3047
+ nextSteps: exploreNextSteps,
3048
+ data: { changeId, args: openSpecArgs, graphContext, figmaGuard: figmaGuard ?? void 0 }
2163
3049
  });
2164
3050
  }
2165
3051
  async function syncWorkflowCommand(ctx, args) {
@@ -2280,23 +3166,33 @@ async function artifactWorkflowCommand(ctx, command, args) {
2280
3166
  exitCode: instructions.exitCode
2281
3167
  });
2282
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
+ }
2283
3183
  ctx.output.result({
2284
3184
  ok: true,
2285
3185
  command,
2286
3186
  summary: `fet ${command} prepared OpenSpec artifact "${artifactId}" for change "${changeId}".`,
2287
3187
  warnings: runState.graphContext?.warnings,
2288
- nextSteps: [
2289
- `Create or update openspec/changes/${changeId}/${resolveOutputPath(status, artifactId)}`,
2290
- "Review the artifact with the user before generating the next planning file.",
2291
- `Run fet passthrough status --change ${changeId}`,
2292
- status.isComplete ? `Run fet apply --change ${changeId}` : `Run fet continue --change ${changeId} when ready for the next artifact`
2293
- ],
3188
+ nextSteps: planningNextSteps,
2294
3189
  data: {
2295
3190
  changeId,
2296
3191
  artifactId,
2297
3192
  instructions: instructions.data,
2298
3193
  status,
2299
- graphContext: runState.graphContext
3194
+ graphContext: runState.graphContext,
3195
+ figmaGuard: figmaGuard ?? void 0
2300
3196
  }
2301
3197
  });
2302
3198
  });
@@ -2502,7 +3398,7 @@ async function createChangelogEntry(projectRoot, changeId) {
2502
3398
  };
2503
3399
  }
2504
3400
  async function appendChangelog(projectRoot, entry) {
2505
- const changelogPath = join14(projectRoot, "CHANGELOG.md");
3401
+ const changelogPath = join18(projectRoot, "CHANGELOG.md");
2506
3402
  const existing = await readOptional4(changelogPath);
2507
3403
  const legacyContentLabel = "\u66F4\u65B0\u5185\u5BB9";
2508
3404
  const block = `updateTime: ${entry.updateTime}
@@ -2515,12 +3411,12 @@ ${block}` : block;
2515
3411
  await atomicWrite(changelogPath, next);
2516
3412
  }
2517
3413
  async function readChangeRequirement(projectRoot, changeId) {
2518
- const changeRoot = join14(projectRoot, "openspec", "changes", changeId);
2519
- const proposal = await readOptional4(join14(changeRoot, "proposal.md"));
3414
+ const changeRoot = join18(projectRoot, "openspec", "changes", changeId);
3415
+ const proposal = await readOptional4(join18(changeRoot, "proposal.md"));
2520
3416
  if (proposal) {
2521
3417
  return summarizeMarkdown(proposal);
2522
3418
  }
2523
- const readme = await readOptional4(join14(changeRoot, "README.md"));
3419
+ const readme = await readOptional4(join18(changeRoot, "README.md"));
2524
3420
  if (readme) {
2525
3421
  return summarizeMarkdown(readme);
2526
3422
  }
@@ -2532,7 +3428,7 @@ function summarizeMarkdown(content) {
2532
3428
  }
2533
3429
  async function readOptional4(path) {
2534
3430
  try {
2535
- return await readFile11(path, "utf8");
3431
+ return await readFile14(path, "utf8");
2536
3432
  } catch {
2537
3433
  return null;
2538
3434
  }
@@ -2888,8 +3784,8 @@ async function updateCommand(ctx) {
2888
3784
 
2889
3785
  // src/commands/verify.ts
2890
3786
  import { createHash } from "crypto";
2891
- import { mkdir as mkdir7, readFile as readFile12, stat as stat5 } from "fs/promises";
2892
- import { join as join15 } from "path";
3787
+ import { mkdir as mkdir7, readFile as readFile15, stat as stat8 } from "fs/promises";
3788
+ import { join as join19 } from "path";
2893
3789
  async function verifyCommand(ctx, options) {
2894
3790
  if (options.auto) {
2895
3791
  const scan = await ctx.scanner.scan(ctx.projectRoot, {});
@@ -2956,8 +3852,8 @@ async function verifyCommand(ctx, options) {
2956
3852
  async function writeInstructions(ctx, changeId) {
2957
3853
  await assertChangeExists(ctx, changeId);
2958
3854
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
2959
- const dir = join15(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
2960
- const instructionsPath = join15(dir, "verify-instructions.md");
3855
+ const dir = join19(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
3856
+ const instructionsPath = join19(dir, "verify-instructions.md");
2961
3857
  await mkdir7(dir, { recursive: true });
2962
3858
  await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
2963
3859
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
@@ -2974,7 +3870,7 @@ async function writeInstructions(ctx, changeId) {
2974
3870
  async function markDone(ctx, changeId) {
2975
3871
  await assertChangeExists(ctx, changeId);
2976
3872
  const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
2977
- const instructionsPath = join15(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
3873
+ const instructionsPath = join19(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
2978
3874
  const instructions = await readInstructions(instructionsPath, changeId);
2979
3875
  const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
2980
3876
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
@@ -3009,8 +3905,8 @@ async function assertChangeExists(ctx, changeId) {
3009
3905
  }
3010
3906
  async function readInstructions(path, changeId) {
3011
3907
  try {
3012
- await stat5(path);
3013
- const content = await readFile12(path, "utf8");
3908
+ await stat8(path);
3909
+ const content = await readFile15(path, "utf8");
3014
3910
  const fileChangeId = readFrontMatterValue(content, "changeId");
3015
3911
  if (fileChangeId !== changeId) {
3016
3912
  throw new FetError({
@@ -3148,9 +4044,9 @@ function renderIdeModelPolicy(command, language = "zh-CN") {
3148
4044
  import { resolve } from "path";
3149
4045
 
3150
4046
  // src/adapters/codex/index.ts
3151
- import { mkdir as mkdir8, readFile as readFile13, stat as stat6 } from "fs/promises";
4047
+ import { mkdir as mkdir8, readFile as readFile16, stat as stat9 } from "fs/promises";
3152
4048
  import { homedir } from "os";
3153
- import { dirname as dirname8, join as join16 } from "path";
4049
+ import { dirname as dirname8, join as join20 } from "path";
3154
4050
 
3155
4051
  // src/adapters/commands.ts
3156
4052
  var FET_WORKFLOW_COMMANDS = [
@@ -3192,6 +4088,7 @@ Before doing FET or OpenSpec work in Codex, read:
3192
4088
  - AGENTS.md
3193
4089
  - openspec/config.yaml
3194
4090
  - .codex/fet/karpathy-guidelines.md
4091
+ - .codex/fet/figma-stop.md when implementing UI from Figma
3195
4092
  - the active change files under openspec/changes/<change-id>/, when a change is selected
3196
4093
 
3197
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.
@@ -3210,6 +4107,7 @@ ${languageInstruction(language)}
3210
4107
  - AGENTS.md
3211
4108
  - openspec/config.yaml
3212
4109
  - .codex/fet/karpathy-guidelines.md
4110
+ - \u6309 Figma \u5B9E\u73B0 UI \u65F6\u9605\u8BFB .codex/fet/figma-stop.md
3213
4111
  - \u5982\u679C\u5DF2\u9009\u62E9 change\uFF0C\u9605\u8BFB openspec/changes/<change-id>/ \u4E0B\u7684\u5F53\u524D\u4EA7\u7269
3214
4112
 
3215
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
@@ -3230,9 +4128,16 @@ FET:END -->
3230
4128
  ${body}`
3231
4129
  };
3232
4130
  }
4131
+ function codexFigmaStopFile(language = DEFAULT_LANGUAGE) {
4132
+ return {
4133
+ path: ".codex/fet/figma-stop.md",
4134
+ content: renderCodexFigmaStopGuide(language)
4135
+ };
4136
+ }
3233
4137
  function codexCommandFiles(language = DEFAULT_LANGUAGE) {
3234
4138
  return [
3235
4139
  codexKarpathyGuidelinesFile(language),
4140
+ codexFigmaStopFile(language),
3236
4141
  ...FET_ADAPTER_COMMANDS.map((command) => ({
3237
4142
  path: `.codex/fet/commands/${command}.md`,
3238
4143
  content: renderCommand(command, language)
@@ -3583,7 +4488,7 @@ ${commandGoalZh(command)}
3583
4488
  - \u9ED8\u8BA4\u4F7F\u7528\u4E2D\u6587\u4EA7\u51FA\u3002
3584
4489
  - \u4E0D\u8981\u7ED5\u8FC7 FET \u76F4\u63A5\u8C03\u7528 openspec\uFF0C\u9664\u975E FET \u547D\u4EE4\u672C\u8EAB\u4E0D\u53EF\u7528\u3002
3585
4490
  - change \u4E0D\u660E\u786E\u65F6\u5148\u8BE2\u95EE\u7528\u6237\u3002
3586
- ${command === "fill-context" ? "- \u66FF\u6362 AGENTS.md \u4E2D\u6BCF\u4E2A `[NEEDS LLM INPUT]` \u6216 `[NEED 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 === "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" : ""}`;
4491
+ ${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 === "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" : ""}`;
3587
4492
  }
3588
4493
  function commandTitleZh(command) {
3589
4494
  const titles = {
@@ -3680,10 +4585,11 @@ Steps:
3680
4585
  - scripts, test commands, and build commands
3681
4586
  - coding conventions and project-specific patterns
3682
4587
  - important docs such as README files
3683
- 5. Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete, concise project-specific content.
3684
- 6. Preserve FET managed markers such as \`FET:BEGIN AUTO\`, \`FET:END AUTO\`, \`FET:BEGIN USER\`, and \`FET:END USER\`.
3685
- 7. Do not modify business code.
3686
- 8. Run:
4588
+ 5. \`fet fill-context\` may already fill the mini program package-size table and 2MB constraints. Do not overwrite those subsections unless the repo changed.
4589
+ 6. Replace every remaining \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete, concise project-specific content.
4590
+ 7. Preserve FET managed markers such as \`FET:BEGIN AUTO\`, \`FET:END AUTO\`, \`FET:BEGIN USER\`, and \`FET:END USER\`.
4591
+ 8. Do not modify business code.
4592
+ 9. Run:
3687
4593
  \`\`\`sh
3688
4594
  fet doctor
3689
4595
  \`\`\`
@@ -4190,7 +5096,7 @@ var CodexAdapter = class {
4190
5096
  adapterVersion = 1;
4191
5097
  async detect(projectRoot) {
4192
5098
  return {
4193
- detected: await exists3(join16(projectRoot, ".codex")) || await exists3(join16(projectRoot, "AGENTS.md")),
5099
+ detected: await exists5(join20(projectRoot, ".codex")) || await exists5(join20(projectRoot, "AGENTS.md")),
4194
5100
  reason: "Codex adapter is available for projects that use AGENTS.md"
4195
5101
  };
4196
5102
  }
@@ -4256,9 +5162,9 @@ var CodexAdapter = class {
4256
5162
  };
4257
5163
  function resolveTarget(projectRoot, file) {
4258
5164
  if (file.root === "codex-home") {
4259
- return join16(resolveCodexHome(), file.path);
5165
+ return join20(resolveCodexHome(), file.path);
4260
5166
  }
4261
- return join16(projectRoot, file.path);
5167
+ return join20(projectRoot, file.path);
4262
5168
  }
4263
5169
  function displayPathFor(file) {
4264
5170
  if (file.root === "codex-home") {
@@ -4267,18 +5173,18 @@ function displayPathFor(file) {
4267
5173
  return file.path;
4268
5174
  }
4269
5175
  function resolveCodexHome() {
4270
- return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join16(homedir(), ".codex");
5176
+ return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join20(homedir(), ".codex");
4271
5177
  }
4272
5178
  async function readExisting(path) {
4273
5179
  try {
4274
- return await readFile13(path, "utf8");
5180
+ return await readFile16(path, "utf8");
4275
5181
  } catch {
4276
5182
  return null;
4277
5183
  }
4278
5184
  }
4279
- async function exists3(path) {
5185
+ async function exists5(path) {
4280
5186
  try {
4281
- await stat6(path);
5187
+ await stat9(path);
4282
5188
  return true;
4283
5189
  } catch {
4284
5190
  return false;
@@ -4286,10 +5192,19 @@ async function exists3(path) {
4286
5192
  }
4287
5193
 
4288
5194
  // src/adapters/cursor/index.ts
4289
- import { mkdir as mkdir9, readFile as readFile14, stat as stat7 } from "fs/promises";
4290
- import { dirname as dirname9, join as join17 } 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";
4291
5197
 
4292
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
+ }
4293
5208
  function cursorSkillFiles(language = DEFAULT_LANGUAGE) {
4294
5209
  return FET_ADAPTER_COMMANDS.map((command) => ({
4295
5210
  path: `.cursor/skills/fet-${command}/SKILL.md`,
@@ -4319,6 +5234,7 @@ ${languageInstruction(language)}
4319
5234
  - openspec/config.yaml
4320
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
4321
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
4322
5238
 
4323
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
4324
5240
  `
@@ -4352,7 +5268,9 @@ ${languageInstruction(language)}
4352
5268
  - AGENTS.md
4353
5269
  - openspec/config.yaml
4354
5270
 
4355
- \u68C0\u67E5 README\u3001package scripts\u3001\u8DEF\u7531\u3001\u6D4B\u8BD5\u3001\u6E90\u7801\u7ED3\u6784\u548C\u73B0\u6709\u7EA6\u5B9A\u540E\uFF0C\u628A AGENTS.md \u4E2D\u6BCF\u4E2A \`[NEEDS LLM INPUT]\` \u6216 \`[NEED LLM INPUT]\` \u5360\u4F4D\u7B26\u66FF\u6362\u4E3A\u5177\u4F53\u3001\u7B80\u6D01\u3001\u9879\u76EE\u76F8\u5173\u7684\u5185\u5BB9\u3002\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002
5271
+ \`fet fill-context\` \u53EF\u80FD\u5DF2\u5199\u5165\u5C0F\u7A0B\u5E8F\u5305\u4F53\u79EF\u8868\u4E0E 2MB \u5F00\u53D1\u7EA6\u675F\uFF0C\u4E0D\u8981\u8986\u76D6 AGENTS.md\u300C\u5C0F\u7A0B\u5E8F\u300D\u8282\u4E2D\u7684\u626B\u63CF\u7ED3\u679C\uFF0C\u9664\u975E\u4ED3\u5E93\u7ED3\u6784\u5DF2\u53D8\u3002
5272
+
5273
+ \u68C0\u67E5 README\u3001package scripts\u3001\u8DEF\u7531\u3001\u6D4B\u8BD5\u3001\u6E90\u7801\u7ED3\u6784\u548C\u73B0\u6709\u7EA6\u5B9A\u540E\uFF0C\u628A AGENTS.md \u4E2D**\u5176\u4F59** \`[NEEDS LLM INPUT]\` \u6216 \`[NEED LLM INPUT]\` \u5360\u4F4D\u7B26\u66FF\u6362\u4E3A\u5177\u4F53\u3001\u7B80\u6D01\u3001\u9879\u76EE\u76F8\u5173\u7684\u5185\u5BB9\u3002\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002
4356
5274
  `;
4357
5275
  }
4358
5276
  if (command === "graph-setup") {
@@ -4421,14 +5339,14 @@ var CursorAdapter = class {
4421
5339
  adapterVersion = 1;
4422
5340
  async detect(projectRoot) {
4423
5341
  return {
4424
- detected: await exists4(join17(projectRoot, ".cursor")),
5342
+ detected: await exists6(join21(projectRoot, ".cursor")),
4425
5343
  reason: "Cursor adapter is available for any project"
4426
5344
  };
4427
5345
  }
4428
5346
  async planInstall(_projectRoot, language) {
4429
5347
  return {
4430
5348
  tool: this.tool,
4431
- files: [...cursorSkillFiles(language), cursorRuleFile(language)].map((file) => ({
5349
+ files: [...cursorSkillFiles(language), ...cursorRuleFiles(language)].map((file) => ({
4432
5350
  ...file,
4433
5351
  managed: true
4434
5352
  }))
@@ -4438,7 +5356,7 @@ var CursorAdapter = class {
4438
5356
  const written = [];
4439
5357
  const skipped = [];
4440
5358
  for (const file of plan.files) {
4441
- const target = join17(projectRoot, file.path);
5359
+ const target = join21(projectRoot, file.path);
4442
5360
  const existing = await readExisting2(target);
4443
5361
  if (existing && !existing.includes("FET:MANAGED") && !force) {
4444
5362
  throw new FetError({
@@ -4461,7 +5379,7 @@ var CursorAdapter = class {
4461
5379
  const plan = await this.planInstall(projectRoot);
4462
5380
  const checks = [];
4463
5381
  for (const file of plan.files) {
4464
- const target = join17(projectRoot, file.path);
5382
+ const target = join21(projectRoot, file.path);
4465
5383
  const content = await readExisting2(target);
4466
5384
  const managed = Boolean(content?.includes("FET:MANAGED"));
4467
5385
  const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
@@ -4477,14 +5395,14 @@ var CursorAdapter = class {
4477
5395
  };
4478
5396
  async function readExisting2(path) {
4479
5397
  try {
4480
- return await readFile14(path, "utf8");
5398
+ return await readFile17(path, "utf8");
4481
5399
  } catch {
4482
5400
  return null;
4483
5401
  }
4484
5402
  }
4485
- async function exists4(path) {
5403
+ async function exists6(path) {
4486
5404
  try {
4487
- await stat7(path);
5405
+ await stat10(path);
4488
5406
  return true;
4489
5407
  } catch {
4490
5408
  return false;
@@ -4496,45 +5414,45 @@ import { execFile as execFile4 } from "child_process";
4496
5414
  import { promisify as promisify4 } from "util";
4497
5415
 
4498
5416
  // src/openspec/inspector.ts
4499
- import { readdir as readdir2, stat as stat8 } from "fs/promises";
4500
- import { join as join18 } from "path";
5417
+ import { readdir as readdir4, stat as stat11 } from "fs/promises";
5418
+ import { join as join22 } from "path";
4501
5419
  async function inspectOpenSpecProject(projectRoot) {
4502
- const openspecPath = join18(projectRoot, "openspec");
4503
- const changesPath = join18(openspecPath, "changes");
4504
- const legacyArchivePath = join18(openspecPath, "archive");
4505
- const changesArchivePath = join18(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");
4506
5424
  return {
4507
- exists: await exists5(openspecPath),
5425
+ exists: await exists7(openspecPath),
4508
5426
  changes: await listDirectories(changesPath, { exclude: ["archive"] }),
4509
5427
  archived: [.../* @__PURE__ */ new Set([...await listDirectories(legacyArchivePath), ...await listDirectories(changesArchivePath)])]
4510
5428
  };
4511
5429
  }
4512
5430
  async function inspectOpenSpecChange(projectRoot, changeId) {
4513
- const changePath = join18(projectRoot, "openspec", "changes", changeId);
4514
- const tasksPath = join18(changePath, "tasks.md");
4515
- const specsPath = join18(changePath, "specs");
5431
+ const changePath = join22(projectRoot, "openspec", "changes", changeId);
5432
+ const tasksPath = join22(changePath, "tasks.md");
5433
+ const specsPath = join22(changePath, "specs");
4516
5434
  return {
4517
5435
  changeId,
4518
- exists: await exists5(changePath),
4519
- hasProposal: await exists5(join18(changePath, "proposal.md")),
4520
- hasTasks: await exists5(tasksPath),
4521
- hasSpecs: await exists5(specsPath),
5436
+ exists: await exists7(changePath),
5437
+ hasProposal: await exists7(join22(changePath, "proposal.md")),
5438
+ hasTasks: await exists7(tasksPath),
5439
+ hasSpecs: await exists7(specsPath),
4522
5440
  tasksPath,
4523
5441
  changePath
4524
5442
  };
4525
5443
  }
4526
5444
  async function listDirectories(path, options = {}) {
4527
5445
  try {
4528
- const entries = await readdir2(path, { withFileTypes: true });
5446
+ const entries = await readdir4(path, { withFileTypes: true });
4529
5447
  const excluded = new Set(options.exclude ?? []);
4530
5448
  return entries.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).map((entry) => entry.name);
4531
5449
  } catch {
4532
5450
  return [];
4533
5451
  }
4534
5452
  }
4535
- async function exists5(path) {
5453
+ async function exists7(path) {
4536
5454
  try {
4537
- await stat8(path);
5455
+ await stat11(path);
4538
5456
  return true;
4539
5457
  } catch {
4540
5458
  return false;
@@ -4716,146 +5634,15 @@ function escapeRegExp(value) {
4716
5634
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4717
5635
  }
4718
5636
 
4719
- // src/scanner/package.ts
4720
- import { readFile as readFile15, stat as stat9 } from "fs/promises";
4721
- import { join as join19 } from "path";
4722
- import { parse as parse2 } from "yaml";
4723
- async function readPackageJson(projectRoot) {
4724
- try {
4725
- return JSON.parse(await readFile15(join19(projectRoot, "package.json"), "utf8"));
4726
- } catch {
4727
- return null;
4728
- }
4729
- }
4730
- async function detectPackageManager(projectRoot, pkg) {
4731
- const warnings = [];
4732
- if (pkg?.packageManager) {
4733
- const declared = pkg.packageManager.split("@")[0] ?? "unknown";
4734
- const locks2 = await detectLockManagers(projectRoot);
4735
- const conflicting = locks2.filter((item) => item !== declared);
4736
- if (conflicting.length) {
4737
- warnings.push(`packageManager \u58F0\u660E\u4E3A ${declared}\uFF0C\u4F46\u540C\u65F6\u53D1\u73B0\u9501\u6587\u4EF6\uFF1A${conflicting.join(", ")}`);
4738
- }
4739
- return { manager: declared, confidence: "high", warnings };
4740
- }
4741
- const locks = await detectLockManagers(projectRoot);
4742
- if (locks.length > 1) {
4743
- warnings.push(`\u53D1\u73B0\u591A\u4E2A\u5305\u7BA1\u7406\u5668\u9501\u6587\u4EF6\uFF1A${locks.join(", ")}\uFF0C\u9ED8\u8BA4\u4F7F\u7528 ${locks[0]}`);
4744
- return { manager: locks[0] ?? "npm", confidence: "medium", warnings };
4745
- }
4746
- if (locks[0]) {
4747
- return { manager: locks[0], confidence: "high", warnings };
4748
- }
4749
- return { manager: "npm", confidence: "low", warnings };
4750
- }
4751
- function extractCommands(pkg, packageManager) {
4752
- const scripts = pkg?.scripts ?? {};
4753
- const result = {};
4754
- const scriptNames = ["dev", "build", "lint", "typecheck", "check", "test", "test:unit"];
4755
- for (const name of scriptNames) {
4756
- if (scripts[name]) {
4757
- const dimension = name === "check" ? "typecheck" : name === "test:unit" ? "test" : name;
4758
- if (result[dimension]) {
4759
- continue;
4760
- }
4761
- result[dimension] = {
4762
- command: scriptCommand(packageManager, name),
4763
- source: `package.json:scripts.${name}`,
4764
- required: name === "build"
4765
- };
4766
- }
4767
- }
4768
- return result;
4769
- }
4770
- function detectFramework(pkg) {
4771
- const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
4772
- const candidates = [
4773
- ["next", ["next"]],
4774
- ["nuxt", ["nuxt"]],
4775
- ["vite", ["vite"]],
4776
- ["sveltekit", ["@sveltejs/kit"]],
4777
- ["angular", ["@angular/core", "@angular/cli"]],
4778
- ["react", ["react"]],
4779
- ["vue", ["vue"]],
4780
- ["svelte", ["svelte"]]
4781
- ];
4782
- for (const [candidate, packages] of candidates) {
4783
- if (packages.some((name) => deps[name])) {
4784
- return { name: candidate, confidence: "high", sources: ["package.json"] };
4785
- }
4786
- }
4787
- return { name: "unknown", confidence: "low", sources: [] };
4788
- }
4789
- async function detectLanguage(projectRoot, pkg) {
4790
- const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
4791
- if (deps.typescript || await exists6(join19(projectRoot, "tsconfig.json"))) {
4792
- return "typescript";
4793
- }
4794
- return "javascript";
4795
- }
4796
- async function detectWorkspaces(projectRoot, pkg) {
4797
- const packageWorkspaces = normalizeWorkspaces(pkg?.workspaces).map((path) => ({
4798
- name: path,
4799
- path,
4800
- source: "package.json:workspaces"
4801
- }));
4802
- if (packageWorkspaces.length) {
4803
- return packageWorkspaces;
4804
- }
4805
- try {
4806
- const workspace = parse2(await readFile15(join19(projectRoot, "pnpm-workspace.yaml"), "utf8"));
4807
- return (workspace?.packages ?? []).map((path) => ({
4808
- name: path,
4809
- path,
4810
- source: "pnpm-workspace.yaml:packages"
4811
- }));
4812
- } catch {
4813
- return [];
4814
- }
4815
- }
4816
- async function detectLockManagers(projectRoot) {
4817
- const lockFiles = [
4818
- ["pnpm-lock.yaml", "pnpm"],
4819
- ["yarn.lock", "yarn"],
4820
- ["bun.lockb", "bun"],
4821
- ["bun.lock", "bun"],
4822
- ["package-lock.json", "npm"]
4823
- ];
4824
- const found = [];
4825
- for (const [file, manager] of lockFiles) {
4826
- if (await exists6(join19(projectRoot, file))) {
4827
- found.push(manager);
4828
- }
4829
- }
4830
- return found;
4831
- }
4832
- function normalizeWorkspaces(workspaces) {
4833
- if (Array.isArray(workspaces)) {
4834
- return workspaces;
4835
- }
4836
- return workspaces?.packages ?? [];
4837
- }
4838
- function scriptCommand(packageManager, name) {
4839
- return packageManager === "npm" ? `npm run ${name}` : `${packageManager} ${name}`;
4840
- }
4841
- async function exists6(path) {
4842
- try {
4843
- await stat9(path);
4844
- return true;
4845
- } catch {
4846
- return false;
4847
- }
4848
- }
4849
-
4850
5637
  // src/scanner/routes.ts
4851
- import { readdir as readdir3, stat as stat10 } from "fs/promises";
4852
- import { join as join20, relative, 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";
4853
5640
  async function scanRoutes(projectRoot) {
4854
5641
  const candidates = ["src/routes", "src/pages", "app", "pages"];
4855
5642
  const routes = [];
4856
5643
  for (const candidate of candidates) {
4857
- const root = join20(projectRoot, candidate);
4858
- if (!await exists7(root)) {
5644
+ const root = join23(projectRoot, candidate);
5645
+ if (!await exists8(root)) {
4859
5646
  continue;
4860
5647
  }
4861
5648
  for (const file of await listFiles(root)) {
@@ -4863,8 +5650,8 @@ async function scanRoutes(projectRoot) {
4863
5650
  continue;
4864
5651
  }
4865
5652
  routes.push({
4866
- path: inferRoutePath(relative(root, file)),
4867
- source: relative(projectRoot, file).split(sep).join("/"),
5653
+ path: inferRoutePath(relative3(root, file)),
5654
+ source: relative3(projectRoot, file).split(sep).join("/"),
4868
5655
  inferred: true,
4869
5656
  confidence: "medium"
4870
5657
  });
@@ -4879,10 +5666,10 @@ function inferRoutePath(relativePath) {
4879
5666
  return `/${withoutIndex}`.replace(/\/+/g, "/");
4880
5667
  }
4881
5668
  async function listFiles(root) {
4882
- const entries = await readdir3(root, { withFileTypes: true });
5669
+ const entries = await readdir5(root, { withFileTypes: true });
4883
5670
  const files = [];
4884
5671
  for (const entry of entries) {
4885
- const path = join20(root, entry.name);
5672
+ const path = join23(root, entry.name);
4886
5673
  if (entry.isDirectory()) {
4887
5674
  files.push(...await listFiles(path));
4888
5675
  } else {
@@ -4891,9 +5678,9 @@ async function listFiles(root) {
4891
5678
  }
4892
5679
  return files;
4893
5680
  }
4894
- async function exists7(path) {
5681
+ async function exists8(path) {
4895
5682
  try {
4896
- await stat10(path);
5683
+ await stat12(path);
4897
5684
  return true;
4898
5685
  } catch {
4899
5686
  return false;
@@ -4908,10 +5695,14 @@ var ProjectScanner = class {
4908
5695
  const framework = detectFramework(pkg);
4909
5696
  const workspaces = await detectWorkspaces(projectRoot, pkg);
4910
5697
  const language = await detectLanguage(projectRoot, pkg);
5698
+ const miniprogram = await detectMiniprogramProject(projectRoot);
4911
5699
  const warnings = [...packageManager.warnings];
4912
5700
  if (framework.name === "unknown") {
4913
5701
  warnings.push("\u672A\u80FD\u4ECE package.json \u8BC6\u522B\u4E3B\u8981\u6846\u67B6");
4914
5702
  }
5703
+ if (miniprogram.supported) {
5704
+ warnings.push(...miniprogram.warnings);
5705
+ }
4915
5706
  return {
4916
5707
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4917
5708
  scannerVersion: 1,
@@ -4928,7 +5719,8 @@ var ProjectScanner = class {
4928
5719
  routes: await scanRoutes(projectRoot),
4929
5720
  conventions: [],
4930
5721
  skippedFiles: [],
4931
- warnings
5722
+ warnings,
5723
+ miniprogram
4932
5724
  };
4933
5725
  }
4934
5726
  };
@@ -5045,9 +5837,9 @@ async function createCommandContext(command, options) {
5045
5837
  import { createInterface as createInterface2 } from "readline/promises";
5046
5838
 
5047
5839
  // src/update/check.ts
5048
- import { mkdir as mkdir10, readFile as readFile16, writeFile } from "fs/promises";
5840
+ import { mkdir as mkdir10, readFile as readFile18, writeFile } from "fs/promises";
5049
5841
  import { homedir as homedir2 } from "os";
5050
- import { dirname as dirname10, join as join21 } from "path";
5842
+ import { dirname as dirname10, join as join24 } from "path";
5051
5843
  var DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
5052
5844
  function getFetUpdateCheckMode(env = process.env) {
5053
5845
  const value = env.FET_UPDATE_CHECK?.trim().toLowerCase();
@@ -5120,11 +5912,11 @@ function formatFetUpdateWarning(availability, language) {
5120
5912
  }
5121
5913
  function cachePath() {
5122
5914
  const home = process.env.FET_UPDATE_CHECK_CACHE_HOME?.trim() || homedir2();
5123
- return join21(home, ".fet", "update-check-cache.json");
5915
+ return join24(home, ".fet", "update-check-cache.json");
5124
5916
  }
5125
5917
  async function readUpdateCheckCache() {
5126
5918
  try {
5127
- const raw = await readFile16(cachePath(), "utf8");
5919
+ const raw = await readFile18(cachePath(), "utf8");
5128
5920
  const parsed = JSON.parse(raw);
5129
5921
  if (typeof parsed.latestVersion !== "string" || typeof parsed.checkedAt !== "string") {
5130
5922
  return null;