@nick848/fet 1.1.4 → 1.1.6

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
@@ -5,7 +5,7 @@ import {
5
5
  } from "../chunk-J5WB4KAL.js";
6
6
 
7
7
  // src/cli/index.ts
8
- import { createInterface as createInterface2 } from "readline/promises";
8
+ import { createInterface as createInterface3 } from "readline/promises";
9
9
  import { Command } from "commander";
10
10
 
11
11
  // src/commands/doctor.ts
@@ -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 relative3 = env.FET_GITNEXUS_GRAPH_PATH?.trim() || DEFAULT_GRAPH_PATH;
192
+ const graphPath = join5(projectRoot, relative3);
193
193
  try {
194
194
  const info = await stat2(graphPath);
195
195
  return {
196
- graphPath: relative2,
196
+ graphPath: relative3,
197
197
  graphExists: true,
198
198
  lastIndexedAt: info.mtime.toISOString()
199
199
  };
200
200
  } catch {
201
201
  return {
202
- graphPath: relative2,
202
+ graphPath: relative3,
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.
@@ -1543,58 +2239,6 @@ fet verify --done --change ${changeId}
1543
2239
  `;
1544
2240
  }
1545
2241
 
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*$/, "")}
1571
-
1572
- ${block}
1573
- `;
1574
- }
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();
1596
- }
1597
-
1598
2242
  // src/commands/update-context.ts
1599
2243
  async function updateContextCommand(ctx) {
1600
2244
  let contextResult = { warnings: [] };
@@ -1610,11 +2254,11 @@ async function updateContextCommand(ctx) {
1610
2254
  }
1611
2255
  async function updateContextFiles(ctx) {
1612
2256
  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");
2257
+ const agentsPath = join14(ctx.projectRoot, "AGENTS.md");
2258
+ const configPath = join14(ctx.projectRoot, "openspec", "config.yaml");
2259
+ const claudePath = join14(ctx.projectRoot, "CLAUDE.md");
2260
+ const karpathyHandoffPath = join14(ctx.projectRoot, ".fet", "karpathy-guidelines.md");
2261
+ const karpathyCursorPath = join14(ctx.projectRoot, ".cursor", "rules", "karpathy-guidelines.mdc");
1618
2262
  const existingAgents = await readOptional2(agentsPath);
1619
2263
  const existingClaude = await readOptional2(claudePath);
1620
2264
  const existingKarpathyCursor = await readOptional2(karpathyCursorPath);
@@ -1688,7 +2332,7 @@ async function confirmInitCanReplaceUnmanagedAgents(ctx) {
1688
2332
  }
1689
2333
  async function readOptional2(path) {
1690
2334
  try {
1691
- return await readFile7(path, "utf8");
2335
+ return await readFile10(path, "utf8");
1692
2336
  } catch {
1693
2337
  return null;
1694
2338
  }
@@ -1696,7 +2340,7 @@ async function readOptional2(path) {
1696
2340
 
1697
2341
  // src/commands/init.ts
1698
2342
  async function initCommand(ctx) {
1699
- const alreadyInitialized = await exists2(join12(ctx.projectRoot, "openspec", "config.yaml"));
2343
+ const alreadyInitialized = await exists4(join15(ctx.projectRoot, "openspec", "config.yaml"));
1700
2344
  let warnings = [];
1701
2345
  await withProjectLock(ctx.projectRoot, { command: "init", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
1702
2346
  const journal = createInitJournal(ctx.fetVersion);
@@ -1711,7 +2355,6 @@ async function initCommand(ctx) {
1711
2355
  }
1712
2356
  const contextResult = await updateContextFiles(ctx);
1713
2357
  warnings = contextResult.warnings;
1714
- await ensureGitignore(ctx);
1715
2358
  const state = await ctx.stateStore.getOrCreateGlobal();
1716
2359
  state.openspec = identity;
1717
2360
  state.language = {
@@ -1747,21 +2390,9 @@ async function initCommand(ctx) {
1747
2390
  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
2391
  });
1749
2392
  }
1750
- async function ensureGitignore(ctx) {
1751
- const gitignorePath = join12(ctx.projectRoot, ".gitignore");
1752
- const existing = await readOptional3(gitignorePath);
1753
- await atomicWrite(gitignorePath, mergeGitignore(existing));
1754
- }
1755
- async function readOptional3(path) {
1756
- try {
1757
- return await readFile8(path, "utf8");
1758
- } catch {
1759
- return null;
1760
- }
1761
- }
1762
- async function exists2(path) {
2393
+ async function exists4(path) {
1763
2394
  try {
1764
- await stat4(path);
2395
+ await stat6(path);
1765
2396
  return true;
1766
2397
  } catch {
1767
2398
  return false;
@@ -1769,8 +2400,8 @@ async function exists2(path) {
1769
2400
  }
1770
2401
 
1771
2402
  // src/commands/proxy.ts
1772
- import { readFile as readFile11 } from "fs/promises";
1773
- import { join as join14 } from "path";
2403
+ import { readFile as readFile13 } from "fs/promises";
2404
+ import { join as join17 } from "path";
1774
2405
 
1775
2406
  // src/state/project.ts
1776
2407
  import { execFile as execFile2 } from "child_process";
@@ -1799,8 +2430,8 @@ async function git(cwd, args) {
1799
2430
  }
1800
2431
 
1801
2432
  // src/state/store.ts
1802
- import { mkdir as mkdir6, readFile as readFile9 } from "fs/promises";
1803
- import { join as join13 } from "path";
2433
+ import { mkdir as mkdir6, readFile as readFile11 } from "fs/promises";
2434
+ import { join as join16 } from "path";
1804
2435
 
1805
2436
  // src/language.ts
1806
2437
  var DEFAULT_LANGUAGE = "zh-CN";
@@ -1918,7 +2549,7 @@ var StateStore = class {
1918
2549
  project;
1919
2550
  async readGlobal() {
1920
2551
  try {
1921
- const value = JSON.parse(await readFile9(this.globalPath(), "utf8"));
2552
+ const value = JSON.parse(await readFile11(this.globalPath(), "utf8"));
1922
2553
  assertGlobalState(value);
1923
2554
  return value;
1924
2555
  } catch (error) {
@@ -1933,13 +2564,13 @@ var StateStore = class {
1933
2564
  }
1934
2565
  async writeGlobal(state) {
1935
2566
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1936
- await mkdir6(join13(this.projectRoot, "openspec"), { recursive: true });
2567
+ await mkdir6(join16(this.projectRoot, "openspec"), { recursive: true });
1937
2568
  await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
1938
2569
  `);
1939
2570
  }
1940
2571
  async readChange(changeId) {
1941
2572
  try {
1942
- const value = JSON.parse(await readFile9(this.changePath(changeId), "utf8"));
2573
+ const value = JSON.parse(await readFile11(this.changePath(changeId), "utf8"));
1943
2574
  assertChangeState(value);
1944
2575
  return value;
1945
2576
  } catch (error) {
@@ -1954,15 +2585,15 @@ var StateStore = class {
1954
2585
  }
1955
2586
  async writeChange(state) {
1956
2587
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1957
- await mkdir6(join13(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
2588
+ await mkdir6(join16(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
1958
2589
  await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
1959
2590
  `);
1960
2591
  }
1961
2592
  globalPath() {
1962
- return join13(this.projectRoot, "openspec", "fet-state.json");
2593
+ return join16(this.projectRoot, "openspec", "fet-state.json");
1963
2594
  }
1964
2595
  changePath(changeId) {
1965
- return join13(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
2596
+ return join16(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
1966
2597
  }
1967
2598
  };
1968
2599
  function isNotFound(error) {
@@ -1970,11 +2601,11 @@ function isNotFound(error) {
1970
2601
  }
1971
2602
 
1972
2603
  // src/state/tasks.ts
1973
- import { readFile as readFile10 } from "fs/promises";
2604
+ import { readFile as readFile12 } from "fs/promises";
1974
2605
  async function readCompletedTaskIds(tasksPath) {
1975
2606
  let content;
1976
2607
  try {
1977
- content = await readFile10(tasksPath, "utf8");
2608
+ content = await readFile12(tasksPath, "utf8");
1978
2609
  } catch {
1979
2610
  return [];
1980
2611
  }
@@ -2502,8 +3133,8 @@ async function createChangelogEntry(projectRoot, changeId) {
2502
3133
  };
2503
3134
  }
2504
3135
  async function appendChangelog(projectRoot, entry) {
2505
- const changelogPath = join14(projectRoot, "CHANGELOG.md");
2506
- const existing = await readOptional4(changelogPath);
3136
+ const changelogPath = join17(projectRoot, "CHANGELOG.md");
3137
+ const existing = await readOptional3(changelogPath);
2507
3138
  const legacyContentLabel = "\u66F4\u65B0\u5185\u5BB9";
2508
3139
  const block = `updateTime: ${entry.updateTime}
2509
3140
  changeRequirement:${entry.content}
@@ -2515,12 +3146,12 @@ ${block}` : block;
2515
3146
  await atomicWrite(changelogPath, next);
2516
3147
  }
2517
3148
  async function readChangeRequirement(projectRoot, changeId) {
2518
- const changeRoot = join14(projectRoot, "openspec", "changes", changeId);
2519
- const proposal = await readOptional4(join14(changeRoot, "proposal.md"));
3149
+ const changeRoot = join17(projectRoot, "openspec", "changes", changeId);
3150
+ const proposal = await readOptional3(join17(changeRoot, "proposal.md"));
2520
3151
  if (proposal) {
2521
3152
  return summarizeMarkdown(proposal);
2522
3153
  }
2523
- const readme = await readOptional4(join14(changeRoot, "README.md"));
3154
+ const readme = await readOptional3(join17(changeRoot, "README.md"));
2524
3155
  if (readme) {
2525
3156
  return summarizeMarkdown(readme);
2526
3157
  }
@@ -2530,9 +3161,9 @@ function summarizeMarkdown(content) {
2530
3161
  const normalized = content.replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("<!--") && !line.startsWith("---")).join(" ");
2531
3162
  return normalized || "No change requirement found.";
2532
3163
  }
2533
- async function readOptional4(path) {
3164
+ async function readOptional3(path) {
2534
3165
  try {
2535
- return await readFile11(path, "utf8");
3166
+ return await readFile13(path, "utf8");
2536
3167
  } catch {
2537
3168
  return null;
2538
3169
  }
@@ -2679,60 +3310,16 @@ async function assertVerifiedChange(ctx, changeId) {
2679
3310
  }
2680
3311
  }
2681
3312
 
2682
- // src/commands/update.ts
3313
+ // src/update/npm.ts
2683
3314
  import { spawn } from "child_process";
2684
- var DEFAULT_PACKAGE_NAME = "@nick848/fet";
2685
- async function updateCommand(ctx) {
2686
- const packageName = process.env.FET_UPDATE_PACKAGE_NAME ?? DEFAULT_PACKAGE_NAME;
2687
- const npmExecutable = process.env.FET_UPDATE_NPM_EXECUTABLE ?? defaultNpmExecutable();
2688
- const latestVersion = await resolveLatestVersion(packageName, npmExecutable);
2689
- const currentVersion = ctx.fetVersion;
2690
- if (compareVersions(currentVersion, latestVersion) >= 0) {
2691
- ctx.output.result({
2692
- ok: true,
2693
- command: "update",
2694
- summary: ctx.language === "en" ? `FET is already up to date (${currentVersion}).` : `FET \u5DF2\u662F\u6700\u65B0\u7248 (${currentVersion})\u3002`,
2695
- data: {
2696
- packageName,
2697
- currentVersion,
2698
- latestVersion,
2699
- updated: false
2700
- }
2701
- });
2702
- return;
2703
- }
2704
- if (!ctx.json) {
2705
- ctx.output.info(
2706
- ctx.language === "en" ? `Updating FET from ${currentVersion} to ${latestVersion}...` : `\u6B63\u5728\u5C06 FET \u4ECE ${currentVersion} \u5347\u7EA7\u5230 ${latestVersion}...`
2707
- );
2708
- }
2709
- const installArgs = ["install", "-g", `${packageName}@latest`];
2710
- const result = await runNpm(npmExecutable, installArgs, {
2711
- cwd: ctx.cwd,
2712
- stdio: ctx.json ? "pipe" : "inherit"
2713
- });
2714
- if (result.exitCode !== 0) {
2715
- throw new FetError({
2716
- code: "UPDATE_FAILED" /* UpdateFailed */,
2717
- message: ctx.language === "en" ? "FET update failed." : "FET \u5347\u7EA7\u5931\u8D25\u3002",
2718
- details: result,
2719
- suggestedCommand: `${npmExecutable} ${installArgs.join(" ")}`
2720
- });
2721
- }
2722
- ctx.output.result({
2723
- ok: true,
2724
- command: "update",
2725
- summary: ctx.language === "en" ? `FET updated from ${currentVersion} to ${latestVersion}.` : `FET \u5DF2\u4ECE ${currentVersion} \u5347\u7EA7\u5230 ${latestVersion}\u3002`,
2726
- data: {
2727
- packageName,
2728
- currentVersion,
2729
- latestVersion,
2730
- updated: true,
2731
- installCommand: `${npmExecutable} ${installArgs.join(" ")}`
2732
- }
2733
- });
3315
+ var DEFAULT_FET_PACKAGE_NAME = "@nick848/fet";
3316
+ function getFetPackageName(env = process.env) {
3317
+ return env.FET_UPDATE_PACKAGE_NAME?.trim() || DEFAULT_FET_PACKAGE_NAME;
3318
+ }
3319
+ function getNpmExecutable(env = process.env) {
3320
+ return env.FET_UPDATE_NPM_EXECUTABLE?.trim() || (process.platform === "win32" ? "npm.cmd" : "npm");
2734
3321
  }
2735
- async function resolveLatestVersion(packageName, npmExecutable) {
3322
+ async function resolveLatestFetVersion(packageName = getFetPackageName(), npmExecutable = getNpmExecutable()) {
2736
3323
  const override = process.env.FET_UPDATE_LATEST_VERSION?.trim();
2737
3324
  if (override) {
2738
3325
  return override;
@@ -2759,6 +3346,32 @@ async function resolveLatestVersion(packageName, npmExecutable) {
2759
3346
  }
2760
3347
  return version;
2761
3348
  }
3349
+ async function performFetUpdate(currentVersion, latestVersion, options) {
3350
+ const packageName = getFetPackageName();
3351
+ const npmExecutable = getNpmExecutable();
3352
+ const installArgs = ["install", "-g", `${packageName}@latest`];
3353
+ if (!options.json) {
3354
+ options.info(
3355
+ options.language === "en" ? `Updating FET from ${currentVersion} to ${latestVersion}...` : `\u6B63\u5728\u5C06 FET \u4ECE ${currentVersion} \u5347\u7EA7\u5230 ${latestVersion}...`
3356
+ );
3357
+ }
3358
+ const result = await runNpm(npmExecutable, installArgs, {
3359
+ cwd: options.cwd,
3360
+ stdio: options.json ? "pipe" : "inherit"
3361
+ });
3362
+ if (result.exitCode !== 0) {
3363
+ throw new FetError({
3364
+ code: "UPDATE_FAILED" /* UpdateFailed */,
3365
+ message: options.language === "en" ? "FET update failed." : "FET \u5347\u7EA7\u5931\u8D25\u3002",
3366
+ details: result,
3367
+ suggestedCommand: `${npmExecutable} ${installArgs.join(" ")}`
3368
+ });
3369
+ }
3370
+ return {
3371
+ packageName,
3372
+ installCommand: `${npmExecutable} ${installArgs.join(" ")}`
3373
+ };
3374
+ }
2762
3375
  function parseNpmVersion(stdout) {
2763
3376
  const trimmed = stdout.trim();
2764
3377
  if (!trimmed) {
@@ -2805,9 +3418,6 @@ function runNpm(command, args, options) {
2805
3418
  });
2806
3419
  });
2807
3420
  }
2808
- function defaultNpmExecutable() {
2809
- return process.platform === "win32" ? "npm.cmd" : "npm";
2810
- }
2811
3421
  function compareVersions(left, right) {
2812
3422
  const leftVersion = parseVersion(left);
2813
3423
  const rightVersion = parseVersion(right);
@@ -2868,10 +3478,49 @@ function comparePrereleasePart(left, right) {
2868
3478
  return left.localeCompare(right);
2869
3479
  }
2870
3480
 
3481
+ // src/commands/update.ts
3482
+ async function updateCommand(ctx) {
3483
+ const packageName = getFetPackageName();
3484
+ const latestVersion = await resolveLatestFetVersion(packageName);
3485
+ const currentVersion = ctx.fetVersion;
3486
+ if (compareVersions(currentVersion, latestVersion) >= 0) {
3487
+ ctx.output.result({
3488
+ ok: true,
3489
+ command: "update",
3490
+ summary: ctx.language === "en" ? `FET is already up to date (${currentVersion}).` : `FET \u5DF2\u662F\u6700\u65B0\u7248 (${currentVersion})\u3002`,
3491
+ data: {
3492
+ packageName,
3493
+ currentVersion,
3494
+ latestVersion,
3495
+ updated: false
3496
+ }
3497
+ });
3498
+ return;
3499
+ }
3500
+ const install = await performFetUpdate(currentVersion, latestVersion, {
3501
+ cwd: ctx.cwd,
3502
+ json: ctx.json,
3503
+ language: ctx.language,
3504
+ info: (message) => ctx.output.info(message)
3505
+ });
3506
+ ctx.output.result({
3507
+ ok: true,
3508
+ command: "update",
3509
+ summary: ctx.language === "en" ? `FET updated from ${currentVersion} to ${latestVersion}.` : `FET \u5DF2\u4ECE ${currentVersion} \u5347\u7EA7\u5230 ${latestVersion}\u3002`,
3510
+ data: {
3511
+ packageName,
3512
+ currentVersion,
3513
+ latestVersion,
3514
+ updated: true,
3515
+ installCommand: install.installCommand
3516
+ }
3517
+ });
3518
+ }
3519
+
2871
3520
  // src/commands/verify.ts
2872
3521
  import { createHash } from "crypto";
2873
- import { mkdir as mkdir7, readFile as readFile12, stat as stat5 } from "fs/promises";
2874
- import { join as join15 } from "path";
3522
+ import { mkdir as mkdir7, readFile as readFile14, stat as stat7 } from "fs/promises";
3523
+ import { join as join18 } from "path";
2875
3524
  async function verifyCommand(ctx, options) {
2876
3525
  if (options.auto) {
2877
3526
  const scan = await ctx.scanner.scan(ctx.projectRoot, {});
@@ -2938,8 +3587,8 @@ async function verifyCommand(ctx, options) {
2938
3587
  async function writeInstructions(ctx, changeId) {
2939
3588
  await assertChangeExists(ctx, changeId);
2940
3589
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
2941
- const dir = join15(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
2942
- const instructionsPath = join15(dir, "verify-instructions.md");
3590
+ const dir = join18(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
3591
+ const instructionsPath = join18(dir, "verify-instructions.md");
2943
3592
  await mkdir7(dir, { recursive: true });
2944
3593
  await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
2945
3594
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
@@ -2956,7 +3605,7 @@ async function writeInstructions(ctx, changeId) {
2956
3605
  async function markDone(ctx, changeId) {
2957
3606
  await assertChangeExists(ctx, changeId);
2958
3607
  const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
2959
- const instructionsPath = join15(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
3608
+ const instructionsPath = join18(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
2960
3609
  const instructions = await readInstructions(instructionsPath, changeId);
2961
3610
  const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
2962
3611
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
@@ -2991,8 +3640,8 @@ async function assertChangeExists(ctx, changeId) {
2991
3640
  }
2992
3641
  async function readInstructions(path, changeId) {
2993
3642
  try {
2994
- await stat5(path);
2995
- const content = await readFile12(path, "utf8");
3643
+ await stat7(path);
3644
+ const content = await readFile14(path, "utf8");
2996
3645
  const fileChangeId = readFrontMatterValue(content, "changeId");
2997
3646
  if (fileChangeId !== changeId) {
2998
3647
  throw new FetError({
@@ -3130,9 +3779,9 @@ function renderIdeModelPolicy(command, language = "zh-CN") {
3130
3779
  import { resolve } from "path";
3131
3780
 
3132
3781
  // src/adapters/codex/index.ts
3133
- import { mkdir as mkdir8, readFile as readFile13, stat as stat6 } from "fs/promises";
3782
+ import { mkdir as mkdir8, readFile as readFile15, stat as stat8 } from "fs/promises";
3134
3783
  import { homedir } from "os";
3135
- import { dirname as dirname8, join as join16 } from "path";
3784
+ import { dirname as dirname8, join as join19 } from "path";
3136
3785
 
3137
3786
  // src/adapters/commands.ts
3138
3787
  var FET_WORKFLOW_COMMANDS = [
@@ -3565,7 +4214,7 @@ ${commandGoalZh(command)}
3565
4214
  - \u9ED8\u8BA4\u4F7F\u7528\u4E2D\u6587\u4EA7\u51FA\u3002
3566
4215
  - \u4E0D\u8981\u7ED5\u8FC7 FET \u76F4\u63A5\u8C03\u7528 openspec\uFF0C\u9664\u975E FET \u547D\u4EE4\u672C\u8EAB\u4E0D\u53EF\u7528\u3002
3567
4216
  - change \u4E0D\u660E\u786E\u65F6\u5148\u8BE2\u95EE\u7528\u6237\u3002
3568
- ${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" : ""}`;
4217
+ ${command === "fill-context" ? "- fet fill-context \u53EF\u80FD\u5DF2\u5199\u5165\u5C0F\u7A0B\u5E8F\u5305\u4F53\u79EF\u4E0E 2MB \u7EA6\u675F\uFF0C\u4E0D\u8981\u8986\u76D6\u8BE5\u8282\u4E2D\u7684\u626B\u63CF\u7ED3\u679C\uFF0C\u9664\u975E\u4ED3\u5E93\u7ED3\u6784\u5DF2\u53D8\u3002\n- \u66FF\u6362 AGENTS.md \u4E2D\u5176\u4F59 [NEEDS LLM INPUT] \u5360\u4F4D\u7B26\uFF0C\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002\n" : ""}${command === "propose" || command === "continue" ? "- \u4E00\u6B21\u53EA\u521B\u5EFA\u4E00\u4E2A ready artifact\uFF0C\u5E76\u5728\u5199\u5165\u524D\u9605\u8BFB\u4F9D\u8D56\u6587\u4EF6\u3002\n" : ""}${command === "propose" || command === "continue" ? "- \u4E0D\u8981\u5728\u7528\u6237\u5BA1\u9605\u5F53\u524D\u4EA7\u7269\u524D\u81EA\u52A8\u8FD0\u884C fet continue\u3001fet ff \u6216\u5FAA\u73AF\u751F\u6210\u540E\u7EED\u4EA7\u7269\uFF1B\u9700\u8981\u7528\u6237\u660E\u786E\u786E\u8BA4\u540E\u518D\u63A8\u8FDB\u3002\n" : ""}${command === "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" : ""}`;
3569
4218
  }
3570
4219
  function commandTitleZh(command) {
3571
4220
  const titles = {
@@ -3662,10 +4311,11 @@ Steps:
3662
4311
  - scripts, test commands, and build commands
3663
4312
  - coding conventions and project-specific patterns
3664
4313
  - important docs such as README files
3665
- 5. Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete, concise project-specific content.
3666
- 6. Preserve FET managed markers such as \`FET:BEGIN AUTO\`, \`FET:END AUTO\`, \`FET:BEGIN USER\`, and \`FET:END USER\`.
3667
- 7. Do not modify business code.
3668
- 8. Run:
4314
+ 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.
4315
+ 6. Replace every remaining \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete, concise project-specific content.
4316
+ 7. Preserve FET managed markers such as \`FET:BEGIN AUTO\`, \`FET:END AUTO\`, \`FET:BEGIN USER\`, and \`FET:END USER\`.
4317
+ 8. Do not modify business code.
4318
+ 9. Run:
3669
4319
  \`\`\`sh
3670
4320
  fet doctor
3671
4321
  \`\`\`
@@ -4172,7 +4822,7 @@ var CodexAdapter = class {
4172
4822
  adapterVersion = 1;
4173
4823
  async detect(projectRoot) {
4174
4824
  return {
4175
- detected: await exists3(join16(projectRoot, ".codex")) || await exists3(join16(projectRoot, "AGENTS.md")),
4825
+ detected: await exists5(join19(projectRoot, ".codex")) || await exists5(join19(projectRoot, "AGENTS.md")),
4176
4826
  reason: "Codex adapter is available for projects that use AGENTS.md"
4177
4827
  };
4178
4828
  }
@@ -4238,9 +4888,9 @@ var CodexAdapter = class {
4238
4888
  };
4239
4889
  function resolveTarget(projectRoot, file) {
4240
4890
  if (file.root === "codex-home") {
4241
- return join16(resolveCodexHome(), file.path);
4891
+ return join19(resolveCodexHome(), file.path);
4242
4892
  }
4243
- return join16(projectRoot, file.path);
4893
+ return join19(projectRoot, file.path);
4244
4894
  }
4245
4895
  function displayPathFor(file) {
4246
4896
  if (file.root === "codex-home") {
@@ -4249,18 +4899,18 @@ function displayPathFor(file) {
4249
4899
  return file.path;
4250
4900
  }
4251
4901
  function resolveCodexHome() {
4252
- return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join16(homedir(), ".codex");
4902
+ return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join19(homedir(), ".codex");
4253
4903
  }
4254
4904
  async function readExisting(path) {
4255
4905
  try {
4256
- return await readFile13(path, "utf8");
4906
+ return await readFile15(path, "utf8");
4257
4907
  } catch {
4258
4908
  return null;
4259
4909
  }
4260
4910
  }
4261
- async function exists3(path) {
4911
+ async function exists5(path) {
4262
4912
  try {
4263
- await stat6(path);
4913
+ await stat8(path);
4264
4914
  return true;
4265
4915
  } catch {
4266
4916
  return false;
@@ -4268,8 +4918,8 @@ async function exists3(path) {
4268
4918
  }
4269
4919
 
4270
4920
  // src/adapters/cursor/index.ts
4271
- import { mkdir as mkdir9, readFile as readFile14, stat as stat7 } from "fs/promises";
4272
- import { dirname as dirname9, join as join17 } from "path";
4921
+ import { mkdir as mkdir9, readFile as readFile16, stat as stat9 } from "fs/promises";
4922
+ import { dirname as dirname9, join as join20 } from "path";
4273
4923
 
4274
4924
  // src/adapters/cursor/templates.ts
4275
4925
  function cursorSkillFiles(language = DEFAULT_LANGUAGE) {
@@ -4334,7 +4984,9 @@ ${languageInstruction(language)}
4334
4984
  - AGENTS.md
4335
4985
  - openspec/config.yaml
4336
4986
 
4337
- \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
4987
+ \`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
4988
+
4989
+ \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
4338
4990
  `;
4339
4991
  }
4340
4992
  if (command === "graph-setup") {
@@ -4403,7 +5055,7 @@ var CursorAdapter = class {
4403
5055
  adapterVersion = 1;
4404
5056
  async detect(projectRoot) {
4405
5057
  return {
4406
- detected: await exists4(join17(projectRoot, ".cursor")),
5058
+ detected: await exists6(join20(projectRoot, ".cursor")),
4407
5059
  reason: "Cursor adapter is available for any project"
4408
5060
  };
4409
5061
  }
@@ -4420,7 +5072,7 @@ var CursorAdapter = class {
4420
5072
  const written = [];
4421
5073
  const skipped = [];
4422
5074
  for (const file of plan.files) {
4423
- const target = join17(projectRoot, file.path);
5075
+ const target = join20(projectRoot, file.path);
4424
5076
  const existing = await readExisting2(target);
4425
5077
  if (existing && !existing.includes("FET:MANAGED") && !force) {
4426
5078
  throw new FetError({
@@ -4443,7 +5095,7 @@ var CursorAdapter = class {
4443
5095
  const plan = await this.planInstall(projectRoot);
4444
5096
  const checks = [];
4445
5097
  for (const file of plan.files) {
4446
- const target = join17(projectRoot, file.path);
5098
+ const target = join20(projectRoot, file.path);
4447
5099
  const content = await readExisting2(target);
4448
5100
  const managed = Boolean(content?.includes("FET:MANAGED"));
4449
5101
  const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
@@ -4459,14 +5111,14 @@ var CursorAdapter = class {
4459
5111
  };
4460
5112
  async function readExisting2(path) {
4461
5113
  try {
4462
- return await readFile14(path, "utf8");
5114
+ return await readFile16(path, "utf8");
4463
5115
  } catch {
4464
5116
  return null;
4465
5117
  }
4466
5118
  }
4467
- async function exists4(path) {
5119
+ async function exists6(path) {
4468
5120
  try {
4469
- await stat7(path);
5121
+ await stat9(path);
4470
5122
  return true;
4471
5123
  } catch {
4472
5124
  return false;
@@ -4478,45 +5130,45 @@ import { execFile as execFile4 } from "child_process";
4478
5130
  import { promisify as promisify4 } from "util";
4479
5131
 
4480
5132
  // src/openspec/inspector.ts
4481
- import { readdir as readdir2, stat as stat8 } from "fs/promises";
4482
- import { join as join18 } from "path";
5133
+ import { readdir as readdir3, stat as stat10 } from "fs/promises";
5134
+ import { join as join21 } from "path";
4483
5135
  async function inspectOpenSpecProject(projectRoot) {
4484
- const openspecPath = join18(projectRoot, "openspec");
4485
- const changesPath = join18(openspecPath, "changes");
4486
- const legacyArchivePath = join18(openspecPath, "archive");
4487
- const changesArchivePath = join18(changesPath, "archive");
5136
+ const openspecPath = join21(projectRoot, "openspec");
5137
+ const changesPath = join21(openspecPath, "changes");
5138
+ const legacyArchivePath = join21(openspecPath, "archive");
5139
+ const changesArchivePath = join21(changesPath, "archive");
4488
5140
  return {
4489
- exists: await exists5(openspecPath),
5141
+ exists: await exists7(openspecPath),
4490
5142
  changes: await listDirectories(changesPath, { exclude: ["archive"] }),
4491
5143
  archived: [.../* @__PURE__ */ new Set([...await listDirectories(legacyArchivePath), ...await listDirectories(changesArchivePath)])]
4492
5144
  };
4493
5145
  }
4494
5146
  async function inspectOpenSpecChange(projectRoot, changeId) {
4495
- const changePath = join18(projectRoot, "openspec", "changes", changeId);
4496
- const tasksPath = join18(changePath, "tasks.md");
4497
- const specsPath = join18(changePath, "specs");
5147
+ const changePath = join21(projectRoot, "openspec", "changes", changeId);
5148
+ const tasksPath = join21(changePath, "tasks.md");
5149
+ const specsPath = join21(changePath, "specs");
4498
5150
  return {
4499
5151
  changeId,
4500
- exists: await exists5(changePath),
4501
- hasProposal: await exists5(join18(changePath, "proposal.md")),
4502
- hasTasks: await exists5(tasksPath),
4503
- hasSpecs: await exists5(specsPath),
5152
+ exists: await exists7(changePath),
5153
+ hasProposal: await exists7(join21(changePath, "proposal.md")),
5154
+ hasTasks: await exists7(tasksPath),
5155
+ hasSpecs: await exists7(specsPath),
4504
5156
  tasksPath,
4505
5157
  changePath
4506
5158
  };
4507
5159
  }
4508
5160
  async function listDirectories(path, options = {}) {
4509
5161
  try {
4510
- const entries = await readdir2(path, { withFileTypes: true });
5162
+ const entries = await readdir3(path, { withFileTypes: true });
4511
5163
  const excluded = new Set(options.exclude ?? []);
4512
5164
  return entries.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).map((entry) => entry.name);
4513
5165
  } catch {
4514
5166
  return [];
4515
5167
  }
4516
5168
  }
4517
- async function exists5(path) {
5169
+ async function exists7(path) {
4518
5170
  try {
4519
- await stat8(path);
5171
+ await stat10(path);
4520
5172
  return true;
4521
5173
  } catch {
4522
5174
  return false;
@@ -4698,146 +5350,15 @@ function escapeRegExp(value) {
4698
5350
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4699
5351
  }
4700
5352
 
4701
- // src/scanner/package.ts
4702
- import { readFile as readFile15, stat as stat9 } from "fs/promises";
4703
- import { join as join19 } from "path";
4704
- import { parse as parse2 } from "yaml";
4705
- async function readPackageJson(projectRoot) {
4706
- try {
4707
- return JSON.parse(await readFile15(join19(projectRoot, "package.json"), "utf8"));
4708
- } catch {
4709
- return null;
4710
- }
4711
- }
4712
- async function detectPackageManager(projectRoot, pkg) {
4713
- const warnings = [];
4714
- if (pkg?.packageManager) {
4715
- const declared = pkg.packageManager.split("@")[0] ?? "unknown";
4716
- const locks2 = await detectLockManagers(projectRoot);
4717
- const conflicting = locks2.filter((item) => item !== declared);
4718
- if (conflicting.length) {
4719
- warnings.push(`packageManager \u58F0\u660E\u4E3A ${declared}\uFF0C\u4F46\u540C\u65F6\u53D1\u73B0\u9501\u6587\u4EF6\uFF1A${conflicting.join(", ")}`);
4720
- }
4721
- return { manager: declared, confidence: "high", warnings };
4722
- }
4723
- const locks = await detectLockManagers(projectRoot);
4724
- if (locks.length > 1) {
4725
- warnings.push(`\u53D1\u73B0\u591A\u4E2A\u5305\u7BA1\u7406\u5668\u9501\u6587\u4EF6\uFF1A${locks.join(", ")}\uFF0C\u9ED8\u8BA4\u4F7F\u7528 ${locks[0]}`);
4726
- return { manager: locks[0] ?? "npm", confidence: "medium", warnings };
4727
- }
4728
- if (locks[0]) {
4729
- return { manager: locks[0], confidence: "high", warnings };
4730
- }
4731
- return { manager: "npm", confidence: "low", warnings };
4732
- }
4733
- function extractCommands(pkg, packageManager) {
4734
- const scripts = pkg?.scripts ?? {};
4735
- const result = {};
4736
- const scriptNames = ["dev", "build", "lint", "typecheck", "check", "test", "test:unit"];
4737
- for (const name of scriptNames) {
4738
- if (scripts[name]) {
4739
- const dimension = name === "check" ? "typecheck" : name === "test:unit" ? "test" : name;
4740
- if (result[dimension]) {
4741
- continue;
4742
- }
4743
- result[dimension] = {
4744
- command: scriptCommand(packageManager, name),
4745
- source: `package.json:scripts.${name}`,
4746
- required: name === "build"
4747
- };
4748
- }
4749
- }
4750
- return result;
4751
- }
4752
- function detectFramework(pkg) {
4753
- const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
4754
- const candidates = [
4755
- ["next", ["next"]],
4756
- ["nuxt", ["nuxt"]],
4757
- ["vite", ["vite"]],
4758
- ["sveltekit", ["@sveltejs/kit"]],
4759
- ["angular", ["@angular/core", "@angular/cli"]],
4760
- ["react", ["react"]],
4761
- ["vue", ["vue"]],
4762
- ["svelte", ["svelte"]]
4763
- ];
4764
- for (const [candidate, packages] of candidates) {
4765
- if (packages.some((name) => deps[name])) {
4766
- return { name: candidate, confidence: "high", sources: ["package.json"] };
4767
- }
4768
- }
4769
- return { name: "unknown", confidence: "low", sources: [] };
4770
- }
4771
- async function detectLanguage(projectRoot, pkg) {
4772
- const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
4773
- if (deps.typescript || await exists6(join19(projectRoot, "tsconfig.json"))) {
4774
- return "typescript";
4775
- }
4776
- return "javascript";
4777
- }
4778
- async function detectWorkspaces(projectRoot, pkg) {
4779
- const packageWorkspaces = normalizeWorkspaces(pkg?.workspaces).map((path) => ({
4780
- name: path,
4781
- path,
4782
- source: "package.json:workspaces"
4783
- }));
4784
- if (packageWorkspaces.length) {
4785
- return packageWorkspaces;
4786
- }
4787
- try {
4788
- const workspace = parse2(await readFile15(join19(projectRoot, "pnpm-workspace.yaml"), "utf8"));
4789
- return (workspace?.packages ?? []).map((path) => ({
4790
- name: path,
4791
- path,
4792
- source: "pnpm-workspace.yaml:packages"
4793
- }));
4794
- } catch {
4795
- return [];
4796
- }
4797
- }
4798
- async function detectLockManagers(projectRoot) {
4799
- const lockFiles = [
4800
- ["pnpm-lock.yaml", "pnpm"],
4801
- ["yarn.lock", "yarn"],
4802
- ["bun.lockb", "bun"],
4803
- ["bun.lock", "bun"],
4804
- ["package-lock.json", "npm"]
4805
- ];
4806
- const found = [];
4807
- for (const [file, manager] of lockFiles) {
4808
- if (await exists6(join19(projectRoot, file))) {
4809
- found.push(manager);
4810
- }
4811
- }
4812
- return found;
4813
- }
4814
- function normalizeWorkspaces(workspaces) {
4815
- if (Array.isArray(workspaces)) {
4816
- return workspaces;
4817
- }
4818
- return workspaces?.packages ?? [];
4819
- }
4820
- function scriptCommand(packageManager, name) {
4821
- return packageManager === "npm" ? `npm run ${name}` : `${packageManager} ${name}`;
4822
- }
4823
- async function exists6(path) {
4824
- try {
4825
- await stat9(path);
4826
- return true;
4827
- } catch {
4828
- return false;
4829
- }
4830
- }
4831
-
4832
5353
  // src/scanner/routes.ts
4833
- import { readdir as readdir3, stat as stat10 } from "fs/promises";
4834
- import { join as join20, relative, sep } from "path";
5354
+ import { readdir as readdir4, stat as stat11 } from "fs/promises";
5355
+ import { join as join22, relative as relative2, sep } from "path";
4835
5356
  async function scanRoutes(projectRoot) {
4836
5357
  const candidates = ["src/routes", "src/pages", "app", "pages"];
4837
5358
  const routes = [];
4838
5359
  for (const candidate of candidates) {
4839
- const root = join20(projectRoot, candidate);
4840
- if (!await exists7(root)) {
5360
+ const root = join22(projectRoot, candidate);
5361
+ if (!await exists8(root)) {
4841
5362
  continue;
4842
5363
  }
4843
5364
  for (const file of await listFiles(root)) {
@@ -4845,8 +5366,8 @@ async function scanRoutes(projectRoot) {
4845
5366
  continue;
4846
5367
  }
4847
5368
  routes.push({
4848
- path: inferRoutePath(relative(root, file)),
4849
- source: relative(projectRoot, file).split(sep).join("/"),
5369
+ path: inferRoutePath(relative2(root, file)),
5370
+ source: relative2(projectRoot, file).split(sep).join("/"),
4850
5371
  inferred: true,
4851
5372
  confidence: "medium"
4852
5373
  });
@@ -4861,10 +5382,10 @@ function inferRoutePath(relativePath) {
4861
5382
  return `/${withoutIndex}`.replace(/\/+/g, "/");
4862
5383
  }
4863
5384
  async function listFiles(root) {
4864
- const entries = await readdir3(root, { withFileTypes: true });
5385
+ const entries = await readdir4(root, { withFileTypes: true });
4865
5386
  const files = [];
4866
5387
  for (const entry of entries) {
4867
- const path = join20(root, entry.name);
5388
+ const path = join22(root, entry.name);
4868
5389
  if (entry.isDirectory()) {
4869
5390
  files.push(...await listFiles(path));
4870
5391
  } else {
@@ -4873,9 +5394,9 @@ async function listFiles(root) {
4873
5394
  }
4874
5395
  return files;
4875
5396
  }
4876
- async function exists7(path) {
5397
+ async function exists8(path) {
4877
5398
  try {
4878
- await stat10(path);
5399
+ await stat11(path);
4879
5400
  return true;
4880
5401
  } catch {
4881
5402
  return false;
@@ -4890,10 +5411,14 @@ var ProjectScanner = class {
4890
5411
  const framework = detectFramework(pkg);
4891
5412
  const workspaces = await detectWorkspaces(projectRoot, pkg);
4892
5413
  const language = await detectLanguage(projectRoot, pkg);
5414
+ const miniprogram = await detectMiniprogramProject(projectRoot);
4893
5415
  const warnings = [...packageManager.warnings];
4894
5416
  if (framework.name === "unknown") {
4895
5417
  warnings.push("\u672A\u80FD\u4ECE package.json \u8BC6\u522B\u4E3B\u8981\u6846\u67B6");
4896
5418
  }
5419
+ if (miniprogram.supported) {
5420
+ warnings.push(...miniprogram.warnings);
5421
+ }
4897
5422
  return {
4898
5423
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4899
5424
  scannerVersion: 1,
@@ -4910,7 +5435,8 @@ var ProjectScanner = class {
4910
5435
  routes: await scanRoutes(projectRoot),
4911
5436
  conventions: [],
4912
5437
  skippedFiles: [],
4913
- warnings
5438
+ warnings,
5439
+ miniprogram
4914
5440
  };
4915
5441
  }
4916
5442
  };
@@ -5023,6 +5549,173 @@ async function createCommandContext(command, options) {
5023
5549
  };
5024
5550
  }
5025
5551
 
5552
+ // src/cli/update-check.ts
5553
+ import { createInterface as createInterface2 } from "readline/promises";
5554
+
5555
+ // src/update/check.ts
5556
+ import { mkdir as mkdir10, readFile as readFile17, writeFile } from "fs/promises";
5557
+ import { homedir as homedir2 } from "os";
5558
+ import { dirname as dirname10, join as join23 } from "path";
5559
+ var DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
5560
+ function getFetUpdateCheckMode(env = process.env) {
5561
+ const value = env.FET_UPDATE_CHECK?.trim().toLowerCase();
5562
+ if (value === "off" || env.FET_SKIP_UPDATE_CHECK === "1") {
5563
+ return "off";
5564
+ }
5565
+ if (value === "warn") {
5566
+ return "warn";
5567
+ }
5568
+ if (value === "confirm") {
5569
+ return "confirm";
5570
+ }
5571
+ return "confirm";
5572
+ }
5573
+ function shouldCheckFetUpdateForCommand(command) {
5574
+ return command !== "update";
5575
+ }
5576
+ async function resolveFetUpdateAvailability(currentVersion) {
5577
+ const packageName = getFetPackageName();
5578
+ const cache = await readUpdateCheckCache();
5579
+ const ttlMs = Number.parseInt(process.env.FET_UPDATE_CHECK_TTL_MS ?? "", 10);
5580
+ const cacheTtlMs = Number.isFinite(ttlMs) && ttlMs > 0 ? ttlMs : DEFAULT_CACHE_TTL_MS;
5581
+ let latestVersion = cache?.latestVersion;
5582
+ const cacheFresh = cache?.checkedAt ? Date.now() - Date.parse(cache.checkedAt) < cacheTtlMs : false;
5583
+ if (!latestVersion || !cacheFresh) {
5584
+ try {
5585
+ latestVersion = await resolveLatestFetVersion(packageName);
5586
+ await writeUpdateCheckCache({
5587
+ latestVersion,
5588
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
5589
+ dismissedLatestVersion: cache?.dismissedLatestVersion
5590
+ });
5591
+ } catch {
5592
+ if (cache?.latestVersion) {
5593
+ latestVersion = cache.latestVersion;
5594
+ } else {
5595
+ return null;
5596
+ }
5597
+ }
5598
+ }
5599
+ if (compareVersions(currentVersion, latestVersion) >= 0) {
5600
+ return null;
5601
+ }
5602
+ if (cache?.dismissedLatestVersion === latestVersion) {
5603
+ return null;
5604
+ }
5605
+ return {
5606
+ packageName,
5607
+ currentVersion,
5608
+ latestVersion
5609
+ };
5610
+ }
5611
+ async function rememberDismissedFetUpdate(latestVersion) {
5612
+ const cache = await readUpdateCheckCache() ?? {
5613
+ latestVersion,
5614
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
5615
+ };
5616
+ await writeUpdateCheckCache({
5617
+ ...cache,
5618
+ latestVersion,
5619
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
5620
+ dismissedLatestVersion: latestVersion
5621
+ });
5622
+ }
5623
+ function formatFetUpdateWarning(availability, language) {
5624
+ if (language === "en") {
5625
+ return `A newer FET release is available (${availability.currentVersion} -> ${availability.latestVersion}). Run \`fet update\` or choose update when prompted.`;
5626
+ }
5627
+ return `\u68C0\u6D4B\u5230 FET \u6709\u65B0\u7248\u672C\uFF08\u5F53\u524D ${availability.currentVersion}\uFF0C\u6700\u65B0 ${availability.latestVersion}\uFF09\u3002\u53EF\u8FD0\u884C \`fet update\`\uFF0C\u6216\u5728\u63D0\u793A\u65F6\u9009\u62E9\u5347\u7EA7\u3002`;
5628
+ }
5629
+ function cachePath() {
5630
+ const home = process.env.FET_UPDATE_CHECK_CACHE_HOME?.trim() || homedir2();
5631
+ return join23(home, ".fet", "update-check-cache.json");
5632
+ }
5633
+ async function readUpdateCheckCache() {
5634
+ try {
5635
+ const raw = await readFile17(cachePath(), "utf8");
5636
+ const parsed = JSON.parse(raw);
5637
+ if (typeof parsed.latestVersion !== "string" || typeof parsed.checkedAt !== "string") {
5638
+ return null;
5639
+ }
5640
+ return {
5641
+ latestVersion: parsed.latestVersion,
5642
+ checkedAt: parsed.checkedAt,
5643
+ dismissedLatestVersion: typeof parsed.dismissedLatestVersion === "string" ? parsed.dismissedLatestVersion : void 0
5644
+ };
5645
+ } catch {
5646
+ return null;
5647
+ }
5648
+ }
5649
+ async function writeUpdateCheckCache(cache) {
5650
+ const path = cachePath();
5651
+ await mkdir10(dirname10(path), { recursive: true });
5652
+ await writeFile(path, `${JSON.stringify(cache, null, 2)}
5653
+ `, "utf8");
5654
+ }
5655
+
5656
+ // src/cli/update-check.ts
5657
+ async function handleFetUpdateCheck(ctx) {
5658
+ if (!shouldCheckFetUpdateForCommand(ctx.command)) {
5659
+ return;
5660
+ }
5661
+ const mode = getFetUpdateCheckMode();
5662
+ if (mode === "off") {
5663
+ return;
5664
+ }
5665
+ const availability = await resolveFetUpdateAvailability(ctx.fetVersion);
5666
+ if (!availability) {
5667
+ return;
5668
+ }
5669
+ const warning = formatFetUpdateWarning(availability, ctx.language);
5670
+ ctx.output.warn(warning);
5671
+ const forceConfirm = process.env.FET_UPDATE_CHECK_FORCE_CONFIRM === "1";
5672
+ const interactive = mode === "confirm" && !ctx.json && !ctx.yes && (forceConfirm || process.stdin.isTTY && process.stderr.isTTY);
5673
+ if (!interactive) {
5674
+ return;
5675
+ }
5676
+ const choice = await promptFetUpdateChoice(availability, ctx.language);
5677
+ if (choice === "skip") {
5678
+ await rememberDismissedFetUpdate(availability.latestVersion);
5679
+ return;
5680
+ }
5681
+ if (choice === "cancel") {
5682
+ throw new FetError({
5683
+ code: "USER_CANCELLED" /* UserCancelled */,
5684
+ message: ctx.language === "en" ? "Command cancelled before FET update." : "\u547D\u4EE4\u5DF2\u53D6\u6D88\uFF0C\u672A\u6267\u884C FET \u5347\u7EA7\u3002",
5685
+ details: availability,
5686
+ suggestedCommand: ctx.language === "en" ? `fet update` : "fet update"
5687
+ });
5688
+ }
5689
+ await performFetUpdate(availability.currentVersion, availability.latestVersion, {
5690
+ cwd: ctx.cwd,
5691
+ json: ctx.json,
5692
+ language: ctx.language,
5693
+ info: (message) => ctx.output.info(message)
5694
+ });
5695
+ throw new FetError({
5696
+ code: "USER_CANCELLED" /* UserCancelled */,
5697
+ message: ctx.language === "en" ? `FET updated to ${availability.latestVersion}. Rerun this command to use the new version.` : `FET \u5DF2\u5347\u7EA7\u5230 ${availability.latestVersion}\u3002\u8BF7\u91CD\u65B0\u8FD0\u884C\u5F53\u524D\u547D\u4EE4\u4EE5\u4F7F\u7528\u65B0\u7248\u672C\u3002`,
5698
+ details: availability,
5699
+ suggestedCommand: `fet ${ctx.command}`
5700
+ });
5701
+ }
5702
+ async function promptFetUpdateChoice(availability, language) {
5703
+ const rl = createInterface2({ input: process.stdin, output: process.stderr });
5704
+ try {
5705
+ const question = language === "en" ? `Update FET now (${availability.currentVersion} -> ${availability.latestVersion})? [U]pdate / [S]kip / [C]ancel [s]: ` : `\u662F\u5426\u73B0\u5728\u5347\u7EA7 FET\uFF08${availability.currentVersion} -> ${availability.latestVersion}\uFF09\uFF1F[U]\u5347\u7EA7 / [S]\u8DF3\u8FC7 / [C]\u53D6\u6D88 [s]: `;
5706
+ const answer = (await rl.question(question)).trim().toLowerCase();
5707
+ if (answer === "u" || answer === "update" || answer === "\u5347\u7EA7") {
5708
+ return "update";
5709
+ }
5710
+ if (answer === "c" || answer === "cancel" || answer === "\u53D6\u6D88") {
5711
+ return "cancel";
5712
+ }
5713
+ return "skip";
5714
+ } finally {
5715
+ rl.close();
5716
+ }
5717
+ }
5718
+
5026
5719
  // src/cli/index.ts
5027
5720
  var program = new Command();
5028
5721
  program.name("fet").description("\u56F4\u7ED5 OpenSpec \u7684\u524D\u7AEF\u5F00\u53D1\u5DE5\u4F5C\u6D41\u7F16\u6392\u5DE5\u5177\u3002").enablePositionalOptions().version(FET_VERSION).option("--cwd <path>", "\u6307\u5B9A\u9879\u76EE\u6839\u76EE\u5F55").option("--change <id>", "\u6307\u5B9A OpenSpec change").option("--lang <language>", "\u6307\u5B9A FET \u4EA4\u4E92\u4FE1\u606F\u548C\u751F\u6210\u4EA7\u7269\u8BED\u8A00\uFF0C\u9ED8\u8BA4 zh-CN").option("--yes", "\u5BF9\u4F4E\u98CE\u9669\u786E\u8BA4\u4F7F\u7528\u9ED8\u8BA4\u540C\u610F").option("--json", "\u8F93\u51FA\u673A\u5668\u53EF\u8BFB JSON").option("--verbose", "\u8F93\u51FA\u8BCA\u65AD\u7EC6\u8282").option("--no-color", "\u7981\u7528\u7EC8\u7AEF\u989C\u8272");
@@ -5066,6 +5759,7 @@ function wrap(command, handler) {
5066
5759
  const opts = isCommandLike(maybeCommand) ? { ...maybeCommand.parent?.opts(), ...maybeCommand.opts() } : program.opts();
5067
5760
  const ctx = await createCommandContext(command, { ...opts, ...extractGlobalOptions(args) });
5068
5761
  try {
5762
+ await handleFetUpdateCheck(ctx);
5069
5763
  await handleModelPolicyRecommendation(ctx);
5070
5764
  await warnIfContextPlaceholdersRemain(ctx);
5071
5765
  await handler(ctx, ...args);
@@ -5087,7 +5781,7 @@ async function handleModelPolicyRecommendation(ctx) {
5087
5781
  if (policyMode !== "confirm" || ctx.yes || ctx.json || !process.stdin.isTTY || !process.stderr.isTTY) {
5088
5782
  return;
5089
5783
  }
5090
- const rl = createInterface2({ input: process.stdin, output: process.stderr });
5784
+ const rl = createInterface3({ input: process.stdin, output: process.stderr });
5091
5785
  try {
5092
5786
  const question = ctx.language === "en" ? "Continue anyway? [y/N] " : "\u4ECD\u7136\u7EE7\u7EED\uFF1F[y/N] ";
5093
5787
  const answer = (await rl.question(question)).trim().toLowerCase();