@nick848/fet 1.1.5 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/README_en.md +7 -1
- package/dist/cli/index.js +1129 -337
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -188,18 +188,18 @@ function toGitNexusState(detection, previous) {
|
|
|
188
188
|
};
|
|
189
189
|
}
|
|
190
190
|
async function inspectGitNexusGraph(projectRoot, env = process.env) {
|
|
191
|
-
const
|
|
192
|
-
const graphPath = join5(projectRoot,
|
|
191
|
+
const relative4 = env.FET_GITNEXUS_GRAPH_PATH?.trim() || DEFAULT_GRAPH_PATH;
|
|
192
|
+
const graphPath = join5(projectRoot, relative4);
|
|
193
193
|
try {
|
|
194
194
|
const info = await stat2(graphPath);
|
|
195
195
|
return {
|
|
196
|
-
graphPath:
|
|
196
|
+
graphPath: relative4,
|
|
197
197
|
graphExists: true,
|
|
198
198
|
lastIndexedAt: info.mtime.toISOString()
|
|
199
199
|
};
|
|
200
200
|
} catch {
|
|
201
201
|
return {
|
|
202
|
-
graphPath:
|
|
202
|
+
graphPath: relative4,
|
|
203
203
|
graphExists: false,
|
|
204
204
|
lastIndexedAt: null
|
|
205
205
|
};
|
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
426
|
-
5.
|
|
427
|
-
6.
|
|
428
|
-
7.
|
|
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.
|
|
444
|
-
5. \
|
|
445
|
-
6. \
|
|
446
|
-
7. \
|
|
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
|
|
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
|
|
456
|
-
import { dirname as dirname5, join as
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
1354
|
+
const capabilities = await readdir2(specsRoot, { withFileTypes: true });
|
|
691
1355
|
return capabilities.filter((entry) => entry.isDirectory()).map((entry) => ({
|
|
692
|
-
path:
|
|
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
|
|
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 =
|
|
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 =
|
|
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 {
|
|
1104
|
-
import { join as
|
|
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
|
|
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 =
|
|
1799
|
+
const root = parse2(currentDir).root;
|
|
1114
1800
|
while (true) {
|
|
1115
|
-
const packageJsonPath =
|
|
1801
|
+
const packageJsonPath = join13(currentDir, "package.json");
|
|
1116
1802
|
if (existsSync(packageJsonPath)) {
|
|
1117
1803
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
1118
1804
|
if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
|
|
@@ -1191,6 +1877,12 @@ function sectionKey(heading) {
|
|
|
1191
1877
|
"ai \u5DE5\u4F5C\u6307\u5357": "ai-guidelines",
|
|
1192
1878
|
"scanner metadata": "metadata",
|
|
1193
1879
|
"\u626B\u63CF\u5143\u6570\u636E": "metadata",
|
|
1880
|
+
"mini program": "miniprogram",
|
|
1881
|
+
"\u5C0F\u7A0B\u5E8F": "miniprogram",
|
|
1882
|
+
"package size (filled by `fet fill-context`)": "miniprogram-size",
|
|
1883
|
+
"\u5305\u4F53\u79EF\uFF08\u7531 `fet fill-context` \u626B\u63CF\u8865\u5145\uFF09": "miniprogram-size",
|
|
1884
|
+
"development constraints": "miniprogram-rules",
|
|
1885
|
+
"\u5F00\u53D1\u7EA6\u675F": "miniprogram-rules",
|
|
1194
1886
|
"notes for ai": "notes",
|
|
1195
1887
|
"\u7ED9 ai \u7684\u5907\u6CE8": "notes"
|
|
1196
1888
|
};
|
|
@@ -1271,6 +1963,8 @@ ${routes || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | low |"}
|
|
|
1271
1963
|
|
|
1272
1964
|
[NEEDS LLM INPUT]
|
|
1273
1965
|
|
|
1966
|
+
${renderMiniprogramPlaceholderSection("zh-CN")}
|
|
1967
|
+
|
|
1274
1968
|
## AI \u5DE5\u4F5C\u6307\u5357
|
|
1275
1969
|
|
|
1276
1970
|
- \u4F7F\u7528 FET \u6258\u7BA1\u7684 IDE \u5DE5\u4F5C\u6D41\u65F6\uFF0C\u4F18\u5148\u53C2\u8003 .fet/karpathy-guidelines.md \u4E2D\u7684\u9879\u76EE\u7EA7\u6307\u5357\u3002
|
|
@@ -1334,6 +2028,8 @@ ${routes || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | low |"}
|
|
|
1334
2028
|
|
|
1335
2029
|
[NEEDS LLM INPUT]
|
|
1336
2030
|
|
|
2031
|
+
${renderMiniprogramPlaceholderSection("en")}
|
|
2032
|
+
|
|
1337
2033
|
## AI Work Guidelines
|
|
1338
2034
|
|
|
1339
2035
|
- Prefer the project-level Andrej Karpathy inspired guidelines in .fet/karpathy-guidelines.md when using FET-managed IDE workflows.
|
|
@@ -1383,6 +2079,10 @@ function renderFetConfig(scan, language = "zh-CN") {
|
|
|
1383
2079
|
test: "warn"
|
|
1384
2080
|
},
|
|
1385
2081
|
workspaces: scan.project.workspaces
|
|
2082
|
+
},
|
|
2083
|
+
figmaGuard: {
|
|
2084
|
+
enabled: true,
|
|
2085
|
+
onUncertainty: "stop_and_ask"
|
|
1386
2086
|
}
|
|
1387
2087
|
}
|
|
1388
2088
|
});
|
|
@@ -1535,64 +2235,124 @@ purpose: manual-verify
|
|
|
1535
2235
|
2. \u6309\u9879\u76EE\u7EA6\u5B9A\u8FD0\u884C lint\u3001typecheck\u3001test\u3002
|
|
1536
2236
|
3. \u68C0\u67E5\u672C\u6B21 change \u7684 \`tasks.md\` \u662F\u5426\u4E0E\u5B9E\u73B0\u72B6\u6001\u4E00\u81F4\u3002
|
|
1537
2237
|
|
|
1538
|
-
\u5B8C\u6210\u540E\u8FD0\u884C\uFF1A
|
|
2238
|
+
\u5B8C\u6210\u540E\u8FD0\u884C\uFF1A
|
|
2239
|
+
|
|
2240
|
+
\`\`\`sh
|
|
2241
|
+
fet verify --done --change ${changeId}
|
|
2242
|
+
\`\`\`
|
|
2243
|
+
`;
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
// src/templates/figma-guard.ts
|
|
2247
|
+
var FIGMA_URL_PATTERN = /https?:\/\/(?:www\.)?figma\.com\/(?:file|design|proto)\/[^\s)\]"'<>]+/gi;
|
|
2248
|
+
function figmaStopHandoffRelativePath(changeId) {
|
|
2249
|
+
return `openspec/changes/${changeId}/.fet/figma-stop.md`;
|
|
2250
|
+
}
|
|
2251
|
+
function renderFigmaStopProtocolBody(language) {
|
|
2252
|
+
if (language === "en") {
|
|
2253
|
+
return `## Stop immediately (do not write or change UI code) when
|
|
2254
|
+
|
|
2255
|
+
- Figma MCP/API errors, 403, timeout, or empty node/selection
|
|
2256
|
+
- You cannot resolve the frame or node referenced in the change
|
|
2257
|
+
- Color, typography, spacing, radius, shadow, or layout cannot be determined from the design input
|
|
2258
|
+
- Component instances do not map to an agreed code component and the user has not chosen one
|
|
2259
|
+
- Interaction states (hover, disabled, loading, empty) are missing from the design
|
|
2260
|
+
|
|
2261
|
+
## After stopping, ask the user
|
|
2262
|
+
|
|
2263
|
+
1. What failed (permission, node, frame, token type)
|
|
2264
|
+
2. What you need: **viewable link**, **screenshot + short notes**, or **explicit permission to infer** (which rule)
|
|
2265
|
+
3. Do **not** continue UI implementation until the user clearly says to continue or answers the question
|
|
2266
|
+
|
|
2267
|
+
## While uncertain
|
|
2268
|
+
|
|
2269
|
+
- Do not fill gaps with "common UI patterns" or guessed pixel values
|
|
2270
|
+
- Prefer showing the blocking question over partial implementation`;
|
|
2271
|
+
}
|
|
2272
|
+
return `## \u5FC5\u987B\u7ACB\u5373\u505C\u6B62\uFF08\u4E0D\u5F97\u7EE7\u7EED\u7F16\u5199\u6216\u4FEE\u6539 UI \u4EE3\u7801\uFF09\u5F53
|
|
2273
|
+
|
|
2274
|
+
- Figma MCP/API \u62A5\u9519\u3001403\u3001\u8D85\u65F6\uFF0C\u6216\u8282\u70B9/\u9009\u533A\u4E3A\u7A7A
|
|
2275
|
+
- \u65E0\u6CD5\u89E3\u6790 change \u4E2D\u5F15\u7528\u7684\u753B\u677F\u6216\u8282\u70B9
|
|
2276
|
+
- \u989C\u8272\u3001\u5B57\u53F7\u3001\u95F4\u8DDD\u3001\u5706\u89D2\u3001\u9634\u5F71\u3001\u5E03\u5C40\u65E0\u6CD5\u4ECE\u8BBE\u8BA1\u8F93\u5165\u4E2D\u786E\u5B9A
|
|
2277
|
+
- \u7EC4\u4EF6\u5B9E\u4F8B\u65E0\u6CD5\u5BF9\u5E94\u5230\u5DF2\u7EA6\u5B9A\u7684\u4EE3\u7801\u7EC4\u4EF6\uFF0C\u4E14\u7528\u6237\u672A\u6307\u5B9A
|
|
2278
|
+
- \u4EA4\u4E92\u72B6\u6001\uFF08hover\u3001disabled\u3001loading\u3001\u7A7A\u6001\u7B49\uFF09\u5728\u8BBE\u8BA1\u7A3F\u4E2D\u7F3A\u5931
|
|
2279
|
+
|
|
2280
|
+
## \u505C\u6B62\u540E\u5FC5\u987B\u8BE2\u95EE\u7528\u6237
|
|
2281
|
+
|
|
2282
|
+
1. \u5361\u5728\u54EA\u4E00\u6B65\uFF08\u6743\u9650\u3001\u8282\u70B9\u3001\u753B\u677F\u3001\u54EA\u7C7B token\uFF09
|
|
2283
|
+
2. \u9700\u8981\u7528\u6237\u8865\u5145\uFF1A**\u53EF\u67E5\u770B\u7684\u94FE\u63A5**\u3001**\u622A\u56FE + \u6587\u5B57\u8BF4\u660E**\uFF0C\u6216 **\u660E\u786E\u5141\u8BB8\u6309\u67D0\u89C4\u5219\u63A8\u65AD**
|
|
2284
|
+
3. \u5728\u7528\u6237\u660E\u786E\u8868\u793A\u300C\u7EE7\u7EED\u300D\u6216\u56DE\u7B54\u95EE\u9898\u4E4B\u524D\uFF0C**\u4E0D\u8981**\u7EE7\u7EED\u5B9E\u73B0 UI
|
|
2285
|
+
|
|
2286
|
+
## \u5B58\u5728\u4E0D\u786E\u5B9A\u6027\u65F6
|
|
2287
|
+
|
|
2288
|
+
- \u4E0D\u8981\u7528\u300C\u5E38\u89C1 UI \u505A\u6CD5\u300D\u6216\u731C\u6D4B\u7684\u50CF\u7D20\u503C\u586B\u8865\u7A7A\u767D
|
|
2289
|
+
- \u4F18\u5148\u5411\u7528\u6237\u63D0\u95EE\uFF0C\u800C\u4E0D\u662F\u5148\u5199\u4E00\u7248\u6837\u5F0F\u518D\u6539`;
|
|
2290
|
+
}
|
|
2291
|
+
function renderCursorFigmaStopRule(language) {
|
|
2292
|
+
const description = language === "en" ? "Stop UI work when Figma cannot be read reliably; ask the user before continuing" : "Figma \u7406\u89E3\u5F02\u5E38\u65F6\u505C\u6B62 UI \u5B9E\u73B0\u5E76\u5411\u7528\u6237\u786E\u8BA4\u540E\u518D\u7EE7\u7EED";
|
|
2293
|
+
return `<!-- FET:MANAGED
|
|
2294
|
+
schemaVersion: 1
|
|
2295
|
+
fetVersion: ${FET_VERSION}
|
|
2296
|
+
generator: cursor-adapter
|
|
2297
|
+
adapterVersion: 1
|
|
2298
|
+
FET:END -->
|
|
2299
|
+
|
|
2300
|
+
---
|
|
2301
|
+
description: ${description}
|
|
2302
|
+
alwaysApply: false
|
|
2303
|
+
---
|
|
2304
|
+
|
|
2305
|
+
${language === "en" ? "Apply when the user shares a Figma link, asks to implement from a design file, or uses Figma MCP/tools for UI work." : "\u5728\u7528\u6237\u5206\u4EAB Figma \u94FE\u63A5\u3001\u8981\u6C42\u6309\u8BBE\u8BA1\u7A3F\u5B9E\u73B0 UI\uFF0C\u6216\u4F7F\u7528 Figma MCP/\u5DE5\u5177\u65F6\u9002\u7528\u3002"}
|
|
2306
|
+
|
|
2307
|
+
${renderFigmaStopProtocolBody(language)}
|
|
2308
|
+
|
|
2309
|
+
${language === "en" ? "If this change has `openspec/changes/<change-id>/.fet/figma-stop.md`, read it for detected links and repeat the same stop rules." : "\u82E5\u5F53\u524D change \u5B58\u5728 `openspec/changes/<change-id>/.fet/figma-stop.md`\uFF0C\u8BF7\u5148\u9605\u8BFB\u5176\u4E2D\u7684\u94FE\u63A5\u5217\u8868\uFF0C\u5E76\u9075\u5B88\u76F8\u540C\u7684\u505C\u6B62\u89C4\u5219\u3002"}
|
|
2310
|
+
`;
|
|
2311
|
+
}
|
|
2312
|
+
function renderCodexFigmaStopGuide(language) {
|
|
2313
|
+
return `<!-- FET:MANAGED
|
|
2314
|
+
schemaVersion: 1
|
|
2315
|
+
fetVersion: ${FET_VERSION}
|
|
2316
|
+
generator: codex-adapter
|
|
2317
|
+
adapterVersion: 1
|
|
2318
|
+
FET:END -->
|
|
2319
|
+
|
|
2320
|
+
# Figma stop protocol (Codex)
|
|
2321
|
+
|
|
2322
|
+
${renderFigmaStopProtocolBody(language)}
|
|
2323
|
+
`;
|
|
2324
|
+
}
|
|
2325
|
+
function renderChangeFigmaStopHandoff(options) {
|
|
2326
|
+
const linkList = options.urls.length ? options.urls.map((url) => `- ${url}`).join("\n") : options.language === "en" ? "- (none detected in change artifacts; user may still reference Figma in chat)" : "- \uFF08change \u4EA7\u7269\u4E2D\u672A\u68C0\u6D4B\u5230\uFF1B\u7528\u6237\u4ECD\u53EF\u80FD\u5728\u5BF9\u8BDD\u4E2D\u63D0\u4F9B Figma\uFF09";
|
|
2327
|
+
const sourceList = options.sources.length ? options.sources.map((source) => `- ${source}`).join("\n") : options.language === "en" ? "- n/a" : "- \u65E0";
|
|
2328
|
+
const title = options.language === "en" ? "Figma guard (this change)" : "Figma \u5B88\u536B\uFF08\u672C change\uFF09";
|
|
2329
|
+
const intro = options.language === "en" ? "FET detected Figma links in this change. When design input is unclear, **stop** and let the user decide whether to continue or clarify." : "FET \u5728\u672C change \u4E2D\u68C0\u6D4B\u5230 Figma \u94FE\u63A5\u3002\u8BBE\u8BA1\u8F93\u5165\u4E0D\u6E05\u6670\u65F6\uFF0C**\u505C\u6B62**\u5F53\u524D\u64CD\u4F5C\uFF0C\u7531\u7528\u6237\u51B3\u5B9A\u662F\u7EE7\u7EED\u8FD8\u662F\u8865\u5145\u8BF4\u660E\u3002";
|
|
2330
|
+
return `---
|
|
2331
|
+
schemaVersion: 1
|
|
2332
|
+
fetVersion: ${FET_VERSION}
|
|
2333
|
+
generatedAt: ${options.generatedAt}
|
|
2334
|
+
changeId: ${options.changeId}
|
|
2335
|
+
purpose: figma-stop
|
|
2336
|
+
---
|
|
2337
|
+
|
|
2338
|
+
# ${title}
|
|
2339
|
+
|
|
2340
|
+
${intro}
|
|
2341
|
+
|
|
2342
|
+
## Detected Figma links
|
|
2343
|
+
|
|
2344
|
+
${linkList}
|
|
1539
2345
|
|
|
1540
|
-
|
|
1541
|
-
fet verify --done --change ${changeId}
|
|
1542
|
-
\`\`\`
|
|
1543
|
-
`;
|
|
1544
|
-
}
|
|
2346
|
+
## Sources
|
|
1545
2347
|
|
|
1546
|
-
|
|
1547
|
-
var BEGIN2 = "# FET:BEGIN LOCAL STATE";
|
|
1548
|
-
var END2 = "# FET:END LOCAL STATE";
|
|
1549
|
-
var RULES = [
|
|
1550
|
-
"openspec/fet-state.json",
|
|
1551
|
-
"openspec/.fet.lock",
|
|
1552
|
-
"openspec/.fet-init-journal.json",
|
|
1553
|
-
"openspec/changes/*/fet-state.json",
|
|
1554
|
-
"openspec/changes/*/.fet/",
|
|
1555
|
-
".gitnexus/"
|
|
1556
|
-
];
|
|
1557
|
-
function mergeGitignore(existing) {
|
|
1558
|
-
const block = `${BEGIN2}
|
|
1559
|
-
${RULES.join("\n")}
|
|
1560
|
-
${END2}`;
|
|
1561
|
-
if (!existing || !existing.trim()) {
|
|
1562
|
-
return `${block}
|
|
1563
|
-
`;
|
|
1564
|
-
}
|
|
1565
|
-
const start = existing.indexOf(BEGIN2);
|
|
1566
|
-
const end = existing.indexOf(END2);
|
|
1567
|
-
if (start !== -1 && end !== -1 && end > start) {
|
|
1568
|
-
return `${existing.slice(0, start)}${block}${existing.slice(end + END2.length)}`;
|
|
1569
|
-
}
|
|
1570
|
-
return `${existing.replace(/\s*$/, "")}
|
|
2348
|
+
${sourceList}
|
|
1571
2349
|
|
|
1572
|
-
${
|
|
2350
|
+
${renderFigmaStopProtocolBody(options.language)}
|
|
1573
2351
|
`;
|
|
1574
2352
|
}
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
import { createInterface } from "readline/promises";
|
|
1579
|
-
import { join as join11 } from "path";
|
|
1580
|
-
|
|
1581
|
-
// src/config/yaml.ts
|
|
1582
|
-
import { readFile as readFile6 } from "fs/promises";
|
|
1583
|
-
import { parseDocument } from "yaml";
|
|
1584
|
-
async function mergeFetConfig(configPath, renderedFetYaml) {
|
|
1585
|
-
const fetDoc = parseDocument(renderedFetYaml);
|
|
1586
|
-
const nextFet = fetDoc.get("fet", true);
|
|
1587
|
-
let existing = "";
|
|
1588
|
-
try {
|
|
1589
|
-
existing = await readFile6(configPath, "utf8");
|
|
1590
|
-
} catch {
|
|
1591
|
-
return renderedFetYaml;
|
|
1592
|
-
}
|
|
1593
|
-
const doc = parseDocument(existing || "{}");
|
|
1594
|
-
doc.set("fet", nextFet);
|
|
1595
|
-
return doc.toString();
|
|
2353
|
+
function renderFigmaStopNextStep(changeId, language) {
|
|
2354
|
+
const path = figmaStopHandoffRelativePath(changeId);
|
|
2355
|
+
return language === "en" ? `Before UI implementation, read ${path}. If Figma access fails or design details are unclear, stop and ask the user\u2014do not guess styles.` : `\u5B9E\u65BD UI \u524D\u9605\u8BFB ${path}\u3002Figma \u8BBF\u95EE\u5931\u8D25\u6216\u8BBE\u8BA1\u7EC6\u8282\u4E0D\u660E\u786E\u65F6\u7ACB\u5373\u505C\u6B62\u5E76\u5411\u7528\u6237\u63D0\u95EE\uFF0C\u4E0D\u8981\u731C\u6D4B\u6837\u5F0F\u3002`;
|
|
1596
2356
|
}
|
|
1597
2357
|
|
|
1598
2358
|
// src/commands/update-context.ts
|
|
@@ -1610,11 +2370,11 @@ async function updateContextCommand(ctx) {
|
|
|
1610
2370
|
}
|
|
1611
2371
|
async function updateContextFiles(ctx) {
|
|
1612
2372
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
1613
|
-
const agentsPath =
|
|
1614
|
-
const configPath =
|
|
1615
|
-
const claudePath =
|
|
1616
|
-
const karpathyHandoffPath =
|
|
1617
|
-
const karpathyCursorPath =
|
|
2373
|
+
const agentsPath = join14(ctx.projectRoot, "AGENTS.md");
|
|
2374
|
+
const configPath = join14(ctx.projectRoot, "openspec", "config.yaml");
|
|
2375
|
+
const claudePath = join14(ctx.projectRoot, "CLAUDE.md");
|
|
2376
|
+
const karpathyHandoffPath = join14(ctx.projectRoot, ".fet", "karpathy-guidelines.md");
|
|
2377
|
+
const karpathyCursorPath = join14(ctx.projectRoot, ".cursor", "rules", "karpathy-guidelines.mdc");
|
|
1618
2378
|
const existingAgents = await readOptional2(agentsPath);
|
|
1619
2379
|
const existingClaude = await readOptional2(claudePath);
|
|
1620
2380
|
const existingKarpathyCursor = await readOptional2(karpathyCursorPath);
|
|
@@ -1688,7 +2448,7 @@ async function confirmInitCanReplaceUnmanagedAgents(ctx) {
|
|
|
1688
2448
|
}
|
|
1689
2449
|
async function readOptional2(path) {
|
|
1690
2450
|
try {
|
|
1691
|
-
return await
|
|
2451
|
+
return await readFile10(path, "utf8");
|
|
1692
2452
|
} catch {
|
|
1693
2453
|
return null;
|
|
1694
2454
|
}
|
|
@@ -1696,7 +2456,7 @@ async function readOptional2(path) {
|
|
|
1696
2456
|
|
|
1697
2457
|
// src/commands/init.ts
|
|
1698
2458
|
async function initCommand(ctx) {
|
|
1699
|
-
const alreadyInitialized = await
|
|
2459
|
+
const alreadyInitialized = await exists4(join15(ctx.projectRoot, "openspec", "config.yaml"));
|
|
1700
2460
|
let warnings = [];
|
|
1701
2461
|
await withProjectLock(ctx.projectRoot, { command: "init", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
1702
2462
|
const journal = createInitJournal(ctx.fetVersion);
|
|
@@ -1711,7 +2471,6 @@ async function initCommand(ctx) {
|
|
|
1711
2471
|
}
|
|
1712
2472
|
const contextResult = await updateContextFiles(ctx);
|
|
1713
2473
|
warnings = contextResult.warnings;
|
|
1714
|
-
await ensureGitignore(ctx);
|
|
1715
2474
|
const state = await ctx.stateStore.getOrCreateGlobal();
|
|
1716
2475
|
state.openspec = identity;
|
|
1717
2476
|
state.language = {
|
|
@@ -1747,30 +2506,138 @@ async function initCommand(ctx) {
|
|
|
1747
2506
|
nextSteps: ctx.language === "en" ? ["Use fet propose/new to create an OpenSpec change", "Use fet doctor to check project health"] : ["\u4F7F\u7528 fet propose/new \u521B\u5EFA OpenSpec change", "\u4F7F\u7528 fet doctor \u68C0\u67E5\u9879\u76EE\u72B6\u6001"]
|
|
1748
2507
|
});
|
|
1749
2508
|
}
|
|
1750
|
-
async function
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
2509
|
+
async function exists4(path) {
|
|
2510
|
+
try {
|
|
2511
|
+
await stat6(path);
|
|
2512
|
+
return true;
|
|
2513
|
+
} catch {
|
|
2514
|
+
return false;
|
|
2515
|
+
}
|
|
1754
2516
|
}
|
|
1755
|
-
|
|
2517
|
+
|
|
2518
|
+
// src/commands/proxy.ts
|
|
2519
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
2520
|
+
import { join as join18 } from "path";
|
|
2521
|
+
|
|
2522
|
+
// src/figma-guard.ts
|
|
2523
|
+
import { readdir as readdir3, readFile as readFile11, stat as stat7 } from "fs/promises";
|
|
2524
|
+
import { join as join16, relative as relative2 } from "path";
|
|
2525
|
+
import { parseDocument as parseDocument2 } from "yaml";
|
|
2526
|
+
var DEFAULT_CONFIG = {
|
|
2527
|
+
enabled: true,
|
|
2528
|
+
onUncertainty: "stop_and_ask"
|
|
2529
|
+
};
|
|
2530
|
+
async function loadFigmaGuardConfig(projectRoot) {
|
|
1756
2531
|
try {
|
|
1757
|
-
|
|
2532
|
+
const raw = await readFile11(join16(projectRoot, "openspec", "config.yaml"), "utf8");
|
|
2533
|
+
const doc = parseDocument2(raw);
|
|
2534
|
+
const fetNode = doc.get("fet", true);
|
|
2535
|
+
const node = fetNode?.get?.("figmaGuard");
|
|
2536
|
+
if (!node || typeof node.get !== "function") {
|
|
2537
|
+
return DEFAULT_CONFIG;
|
|
2538
|
+
}
|
|
2539
|
+
const enabled = node.get("enabled");
|
|
2540
|
+
return {
|
|
2541
|
+
enabled: enabled === void 0 ? true : Boolean(enabled),
|
|
2542
|
+
onUncertainty: "stop_and_ask"
|
|
2543
|
+
};
|
|
1758
2544
|
} catch {
|
|
2545
|
+
return DEFAULT_CONFIG;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
function extractFigmaUrls(content) {
|
|
2549
|
+
const matches = content.match(FIGMA_URL_PATTERN) ?? [];
|
|
2550
|
+
return [...new Set(matches.map((url) => url.replace(/[.,;]+$/, "")))];
|
|
2551
|
+
}
|
|
2552
|
+
async function collectFigmaUrlsFromChange(projectRoot, changeId) {
|
|
2553
|
+
const changePath = join16(projectRoot, "openspec", "changes", changeId);
|
|
2554
|
+
const urls = /* @__PURE__ */ new Set();
|
|
2555
|
+
const sources = [];
|
|
2556
|
+
const candidates = ["proposal.md", "tasks.md", "design.md"];
|
|
2557
|
+
for (const name of candidates) {
|
|
2558
|
+
const filePath = join16(changePath, name);
|
|
2559
|
+
const content = await readOptional3(filePath);
|
|
2560
|
+
if (!content) {
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2563
|
+
const found = extractFigmaUrls(content);
|
|
2564
|
+
if (found.length) {
|
|
2565
|
+
sources.push(`openspec/changes/${changeId}/${name}`);
|
|
2566
|
+
for (const url of found) {
|
|
2567
|
+
urls.add(url);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
const specsPath = join16(changePath, "specs");
|
|
2572
|
+
for (const filePath of await listMarkdownFiles(specsPath)) {
|
|
2573
|
+
const content = await readOptional3(filePath);
|
|
2574
|
+
if (!content) {
|
|
2575
|
+
continue;
|
|
2576
|
+
}
|
|
2577
|
+
const found = extractFigmaUrls(content);
|
|
2578
|
+
if (found.length) {
|
|
2579
|
+
sources.push(relative2(projectRoot, filePath).replaceAll("\\", "/"));
|
|
2580
|
+
for (const url of found) {
|
|
2581
|
+
urls.add(url);
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
return { urls: [...urls], sources };
|
|
2586
|
+
}
|
|
2587
|
+
async function ensureChangeFigmaStopHandoff(options) {
|
|
2588
|
+
const config = options.enabled === void 0 ? await loadFigmaGuardConfig(options.projectRoot) : { enabled: options.enabled, onUncertainty: "stop_and_ask" };
|
|
2589
|
+
if (!config.enabled) {
|
|
2590
|
+
return null;
|
|
2591
|
+
}
|
|
2592
|
+
const { urls, sources } = await collectFigmaUrlsFromChange(options.projectRoot, options.changeId);
|
|
2593
|
+
if (!urls.length) {
|
|
1759
2594
|
return null;
|
|
1760
2595
|
}
|
|
2596
|
+
const relativePath = figmaStopHandoffRelativePath(options.changeId);
|
|
2597
|
+
const absolutePath = join16(options.projectRoot, relativePath);
|
|
2598
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2599
|
+
const content = renderChangeFigmaStopHandoff({
|
|
2600
|
+
changeId: options.changeId,
|
|
2601
|
+
generatedAt,
|
|
2602
|
+
urls,
|
|
2603
|
+
sources,
|
|
2604
|
+
language: options.language
|
|
2605
|
+
});
|
|
2606
|
+
const existing = await readOptional3(absolutePath);
|
|
2607
|
+
const written = existing !== content;
|
|
2608
|
+
if (written) {
|
|
2609
|
+
await atomicWrite(absolutePath, content);
|
|
2610
|
+
}
|
|
2611
|
+
return { path: relativePath, written, urls, sources };
|
|
1761
2612
|
}
|
|
1762
|
-
async function
|
|
2613
|
+
async function listMarkdownFiles(root) {
|
|
2614
|
+
const files = [];
|
|
2615
|
+
await walk(root, files);
|
|
2616
|
+
return files;
|
|
2617
|
+
}
|
|
2618
|
+
async function walk(dir, files) {
|
|
1763
2619
|
try {
|
|
1764
|
-
await
|
|
1765
|
-
|
|
2620
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
2621
|
+
for (const entry of entries) {
|
|
2622
|
+
const fullPath = join16(dir, entry.name);
|
|
2623
|
+
if (entry.isDirectory()) {
|
|
2624
|
+
await walk(fullPath, files);
|
|
2625
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
2626
|
+
files.push(fullPath);
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
1766
2629
|
} catch {
|
|
1767
|
-
return
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
async function readOptional3(path) {
|
|
2634
|
+
try {
|
|
2635
|
+
await stat7(path);
|
|
2636
|
+
return await readFile11(path, "utf8");
|
|
2637
|
+
} catch {
|
|
2638
|
+
return null;
|
|
1768
2639
|
}
|
|
1769
2640
|
}
|
|
1770
|
-
|
|
1771
|
-
// src/commands/proxy.ts
|
|
1772
|
-
import { readFile as readFile11 } from "fs/promises";
|
|
1773
|
-
import { join as join14 } from "path";
|
|
1774
2641
|
|
|
1775
2642
|
// src/state/project.ts
|
|
1776
2643
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -1799,8 +2666,8 @@ async function git(cwd, args) {
|
|
|
1799
2666
|
}
|
|
1800
2667
|
|
|
1801
2668
|
// src/state/store.ts
|
|
1802
|
-
import { mkdir as mkdir6, readFile as
|
|
1803
|
-
import { join as
|
|
2669
|
+
import { mkdir as mkdir6, readFile as readFile12 } from "fs/promises";
|
|
2670
|
+
import { join as join17 } from "path";
|
|
1804
2671
|
|
|
1805
2672
|
// src/language.ts
|
|
1806
2673
|
var DEFAULT_LANGUAGE = "zh-CN";
|
|
@@ -1918,7 +2785,7 @@ var StateStore = class {
|
|
|
1918
2785
|
project;
|
|
1919
2786
|
async readGlobal() {
|
|
1920
2787
|
try {
|
|
1921
|
-
const value = JSON.parse(await
|
|
2788
|
+
const value = JSON.parse(await readFile12(this.globalPath(), "utf8"));
|
|
1922
2789
|
assertGlobalState(value);
|
|
1923
2790
|
return value;
|
|
1924
2791
|
} catch (error) {
|
|
@@ -1933,13 +2800,13 @@ var StateStore = class {
|
|
|
1933
2800
|
}
|
|
1934
2801
|
async writeGlobal(state) {
|
|
1935
2802
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1936
|
-
await mkdir6(
|
|
2803
|
+
await mkdir6(join17(this.projectRoot, "openspec"), { recursive: true });
|
|
1937
2804
|
await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
|
|
1938
2805
|
`);
|
|
1939
2806
|
}
|
|
1940
2807
|
async readChange(changeId) {
|
|
1941
2808
|
try {
|
|
1942
|
-
const value = JSON.parse(await
|
|
2809
|
+
const value = JSON.parse(await readFile12(this.changePath(changeId), "utf8"));
|
|
1943
2810
|
assertChangeState(value);
|
|
1944
2811
|
return value;
|
|
1945
2812
|
} catch (error) {
|
|
@@ -1954,15 +2821,15 @@ var StateStore = class {
|
|
|
1954
2821
|
}
|
|
1955
2822
|
async writeChange(state) {
|
|
1956
2823
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1957
|
-
await mkdir6(
|
|
2824
|
+
await mkdir6(join17(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
|
|
1958
2825
|
await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
|
|
1959
2826
|
`);
|
|
1960
2827
|
}
|
|
1961
2828
|
globalPath() {
|
|
1962
|
-
return
|
|
2829
|
+
return join17(this.projectRoot, "openspec", "fet-state.json");
|
|
1963
2830
|
}
|
|
1964
2831
|
changePath(changeId) {
|
|
1965
|
-
return
|
|
2832
|
+
return join17(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
|
|
1966
2833
|
}
|
|
1967
2834
|
};
|
|
1968
2835
|
function isNotFound(error) {
|
|
@@ -1970,11 +2837,11 @@ function isNotFound(error) {
|
|
|
1970
2837
|
}
|
|
1971
2838
|
|
|
1972
2839
|
// src/state/tasks.ts
|
|
1973
|
-
import { readFile as
|
|
2840
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
1974
2841
|
async function readCompletedTaskIds(tasksPath) {
|
|
1975
2842
|
let content;
|
|
1976
2843
|
try {
|
|
1977
|
-
content = await
|
|
2844
|
+
content = await readFile13(tasksPath, "utf8");
|
|
1978
2845
|
} catch {
|
|
1979
2846
|
return [];
|
|
1980
2847
|
}
|
|
@@ -2123,21 +2990,31 @@ async function applyWorkflowCommand(ctx, args) {
|
|
|
2123
2990
|
exitCode: instructions.exitCode,
|
|
2124
2991
|
phaseStatus: "in_progress"
|
|
2125
2992
|
});
|
|
2993
|
+
const figmaGuard = await ensureChangeFigmaStopHandoff({
|
|
2994
|
+
projectRoot: ctx.projectRoot,
|
|
2995
|
+
changeId,
|
|
2996
|
+
language: ctx.language
|
|
2997
|
+
});
|
|
2998
|
+
const applyNextSteps = [
|
|
2999
|
+
`Read openspec/changes/${changeId}/tasks.md and the instructions output.`,
|
|
3000
|
+
"Implement pending tasks and update task checkboxes only after the work is done.",
|
|
3001
|
+
`Run fet verify --change ${changeId}`
|
|
3002
|
+
];
|
|
3003
|
+
if (figmaGuard) {
|
|
3004
|
+
applyNextSteps.unshift(renderFigmaStopNextStep(changeId, ctx.language));
|
|
3005
|
+
}
|
|
2126
3006
|
ctx.output.result({
|
|
2127
3007
|
ok: true,
|
|
2128
3008
|
command: "apply",
|
|
2129
3009
|
summary: `fet apply prepared implementation instructions for change "${changeId}".`,
|
|
2130
3010
|
warnings: [...runState.graphContext?.warnings ?? [], ...warnings ?? []],
|
|
2131
|
-
nextSteps:
|
|
2132
|
-
`Read openspec/changes/${changeId}/tasks.md and the instructions output.`,
|
|
2133
|
-
"Implement pending tasks and update task checkboxes only after the work is done.",
|
|
2134
|
-
`Run fet verify --change ${changeId}`
|
|
2135
|
-
],
|
|
3011
|
+
nextSteps: applyNextSteps,
|
|
2136
3012
|
data: {
|
|
2137
3013
|
changeId,
|
|
2138
3014
|
instructions: instructions.data,
|
|
2139
3015
|
status,
|
|
2140
|
-
graphContext: runState.graphContext
|
|
3016
|
+
graphContext: runState.graphContext,
|
|
3017
|
+
figmaGuard: figmaGuard ?? void 0
|
|
2141
3018
|
}
|
|
2142
3019
|
});
|
|
2143
3020
|
});
|
|
@@ -2150,16 +3027,25 @@ async function exploreWorkflowCommand(ctx, args) {
|
|
|
2150
3027
|
args: openSpecArgs,
|
|
2151
3028
|
changeId
|
|
2152
3029
|
});
|
|
3030
|
+
const figmaGuard = changeId ? await ensureChangeFigmaStopHandoff({
|
|
3031
|
+
projectRoot: ctx.projectRoot,
|
|
3032
|
+
changeId,
|
|
3033
|
+
language: ctx.language
|
|
3034
|
+
}) : null;
|
|
3035
|
+
const exploreNextSteps = [
|
|
3036
|
+
"Discuss the requirement, constraints, and acceptance criteria with the user.",
|
|
3037
|
+
changeId ? `Run fet continue --change ${changeId} when ready to create the next artifact.` : "Run fet propose <change-id-or-description> when ready to create a change."
|
|
3038
|
+
];
|
|
3039
|
+
if (figmaGuard) {
|
|
3040
|
+
exploreNextSteps.unshift(renderFigmaStopNextStep(changeId, ctx.language));
|
|
3041
|
+
}
|
|
2153
3042
|
ctx.output.result({
|
|
2154
3043
|
ok: true,
|
|
2155
3044
|
command: "explore",
|
|
2156
3045
|
summary: "fet explore is an IDE-guided workflow for shaping OpenSpec changes.",
|
|
2157
3046
|
warnings: graphContext.warnings,
|
|
2158
|
-
nextSteps:
|
|
2159
|
-
|
|
2160
|
-
changeId ? `Run fet continue --change ${changeId} when ready to create the next artifact.` : "Run fet propose <change-id-or-description> when ready to create a change."
|
|
2161
|
-
],
|
|
2162
|
-
data: { changeId, args: openSpecArgs, graphContext }
|
|
3047
|
+
nextSteps: exploreNextSteps,
|
|
3048
|
+
data: { changeId, args: openSpecArgs, graphContext, figmaGuard: figmaGuard ?? void 0 }
|
|
2163
3049
|
});
|
|
2164
3050
|
}
|
|
2165
3051
|
async function syncWorkflowCommand(ctx, args) {
|
|
@@ -2280,23 +3166,33 @@ async function artifactWorkflowCommand(ctx, command, args) {
|
|
|
2280
3166
|
exitCode: instructions.exitCode
|
|
2281
3167
|
});
|
|
2282
3168
|
const status = await readOpenSpecStatus(ctx, changeId);
|
|
3169
|
+
const figmaGuard = await ensureChangeFigmaStopHandoff({
|
|
3170
|
+
projectRoot: ctx.projectRoot,
|
|
3171
|
+
changeId,
|
|
3172
|
+
language: ctx.language
|
|
3173
|
+
});
|
|
3174
|
+
const planningNextSteps = [
|
|
3175
|
+
`Create or update openspec/changes/${changeId}/${resolveOutputPath(status, artifactId)}`,
|
|
3176
|
+
"Review the artifact with the user before generating the next planning file.",
|
|
3177
|
+
`Run fet passthrough status --change ${changeId}`,
|
|
3178
|
+
status.isComplete ? `Run fet apply --change ${changeId}` : `Run fet continue --change ${changeId} when ready for the next artifact`
|
|
3179
|
+
];
|
|
3180
|
+
if (figmaGuard) {
|
|
3181
|
+
planningNextSteps.unshift(renderFigmaStopNextStep(changeId, ctx.language));
|
|
3182
|
+
}
|
|
2283
3183
|
ctx.output.result({
|
|
2284
3184
|
ok: true,
|
|
2285
3185
|
command,
|
|
2286
3186
|
summary: `fet ${command} prepared OpenSpec artifact "${artifactId}" for change "${changeId}".`,
|
|
2287
3187
|
warnings: runState.graphContext?.warnings,
|
|
2288
|
-
nextSteps:
|
|
2289
|
-
`Create or update openspec/changes/${changeId}/${resolveOutputPath(status, artifactId)}`,
|
|
2290
|
-
"Review the artifact with the user before generating the next planning file.",
|
|
2291
|
-
`Run fet passthrough status --change ${changeId}`,
|
|
2292
|
-
status.isComplete ? `Run fet apply --change ${changeId}` : `Run fet continue --change ${changeId} when ready for the next artifact`
|
|
2293
|
-
],
|
|
3188
|
+
nextSteps: planningNextSteps,
|
|
2294
3189
|
data: {
|
|
2295
3190
|
changeId,
|
|
2296
3191
|
artifactId,
|
|
2297
3192
|
instructions: instructions.data,
|
|
2298
3193
|
status,
|
|
2299
|
-
graphContext: runState.graphContext
|
|
3194
|
+
graphContext: runState.graphContext,
|
|
3195
|
+
figmaGuard: figmaGuard ?? void 0
|
|
2300
3196
|
}
|
|
2301
3197
|
});
|
|
2302
3198
|
});
|
|
@@ -2502,7 +3398,7 @@ async function createChangelogEntry(projectRoot, changeId) {
|
|
|
2502
3398
|
};
|
|
2503
3399
|
}
|
|
2504
3400
|
async function appendChangelog(projectRoot, entry) {
|
|
2505
|
-
const changelogPath =
|
|
3401
|
+
const changelogPath = join18(projectRoot, "CHANGELOG.md");
|
|
2506
3402
|
const existing = await readOptional4(changelogPath);
|
|
2507
3403
|
const legacyContentLabel = "\u66F4\u65B0\u5185\u5BB9";
|
|
2508
3404
|
const block = `updateTime: ${entry.updateTime}
|
|
@@ -2515,12 +3411,12 @@ ${block}` : block;
|
|
|
2515
3411
|
await atomicWrite(changelogPath, next);
|
|
2516
3412
|
}
|
|
2517
3413
|
async function readChangeRequirement(projectRoot, changeId) {
|
|
2518
|
-
const changeRoot =
|
|
2519
|
-
const proposal = await readOptional4(
|
|
3414
|
+
const changeRoot = join18(projectRoot, "openspec", "changes", changeId);
|
|
3415
|
+
const proposal = await readOptional4(join18(changeRoot, "proposal.md"));
|
|
2520
3416
|
if (proposal) {
|
|
2521
3417
|
return summarizeMarkdown(proposal);
|
|
2522
3418
|
}
|
|
2523
|
-
const readme = await readOptional4(
|
|
3419
|
+
const readme = await readOptional4(join18(changeRoot, "README.md"));
|
|
2524
3420
|
if (readme) {
|
|
2525
3421
|
return summarizeMarkdown(readme);
|
|
2526
3422
|
}
|
|
@@ -2532,7 +3428,7 @@ function summarizeMarkdown(content) {
|
|
|
2532
3428
|
}
|
|
2533
3429
|
async function readOptional4(path) {
|
|
2534
3430
|
try {
|
|
2535
|
-
return await
|
|
3431
|
+
return await readFile14(path, "utf8");
|
|
2536
3432
|
} catch {
|
|
2537
3433
|
return null;
|
|
2538
3434
|
}
|
|
@@ -2888,8 +3784,8 @@ async function updateCommand(ctx) {
|
|
|
2888
3784
|
|
|
2889
3785
|
// src/commands/verify.ts
|
|
2890
3786
|
import { createHash } from "crypto";
|
|
2891
|
-
import { mkdir as mkdir7, readFile as
|
|
2892
|
-
import { join as
|
|
3787
|
+
import { mkdir as mkdir7, readFile as readFile15, stat as stat8 } from "fs/promises";
|
|
3788
|
+
import { join as join19 } from "path";
|
|
2893
3789
|
async function verifyCommand(ctx, options) {
|
|
2894
3790
|
if (options.auto) {
|
|
2895
3791
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
@@ -2956,8 +3852,8 @@ async function verifyCommand(ctx, options) {
|
|
|
2956
3852
|
async function writeInstructions(ctx, changeId) {
|
|
2957
3853
|
await assertChangeExists(ctx, changeId);
|
|
2958
3854
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2959
|
-
const dir =
|
|
2960
|
-
const instructionsPath =
|
|
3855
|
+
const dir = join19(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
3856
|
+
const instructionsPath = join19(dir, "verify-instructions.md");
|
|
2961
3857
|
await mkdir7(dir, { recursive: true });
|
|
2962
3858
|
await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
|
|
2963
3859
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
@@ -2974,7 +3870,7 @@ async function writeInstructions(ctx, changeId) {
|
|
|
2974
3870
|
async function markDone(ctx, changeId) {
|
|
2975
3871
|
await assertChangeExists(ctx, changeId);
|
|
2976
3872
|
const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2977
|
-
const instructionsPath =
|
|
3873
|
+
const instructionsPath = join19(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
|
|
2978
3874
|
const instructions = await readInstructions(instructionsPath, changeId);
|
|
2979
3875
|
const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
|
|
2980
3876
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
@@ -3009,8 +3905,8 @@ async function assertChangeExists(ctx, changeId) {
|
|
|
3009
3905
|
}
|
|
3010
3906
|
async function readInstructions(path, changeId) {
|
|
3011
3907
|
try {
|
|
3012
|
-
await
|
|
3013
|
-
const content = await
|
|
3908
|
+
await stat8(path);
|
|
3909
|
+
const content = await readFile15(path, "utf8");
|
|
3014
3910
|
const fileChangeId = readFrontMatterValue(content, "changeId");
|
|
3015
3911
|
if (fileChangeId !== changeId) {
|
|
3016
3912
|
throw new FetError({
|
|
@@ -3148,9 +4044,9 @@ function renderIdeModelPolicy(command, language = "zh-CN") {
|
|
|
3148
4044
|
import { resolve } from "path";
|
|
3149
4045
|
|
|
3150
4046
|
// src/adapters/codex/index.ts
|
|
3151
|
-
import { mkdir as mkdir8, readFile as
|
|
4047
|
+
import { mkdir as mkdir8, readFile as readFile16, stat as stat9 } from "fs/promises";
|
|
3152
4048
|
import { homedir } from "os";
|
|
3153
|
-
import { dirname as dirname8, join as
|
|
4049
|
+
import { dirname as dirname8, join as join20 } from "path";
|
|
3154
4050
|
|
|
3155
4051
|
// src/adapters/commands.ts
|
|
3156
4052
|
var FET_WORKFLOW_COMMANDS = [
|
|
@@ -3192,6 +4088,7 @@ Before doing FET or OpenSpec work in Codex, read:
|
|
|
3192
4088
|
- AGENTS.md
|
|
3193
4089
|
- openspec/config.yaml
|
|
3194
4090
|
- .codex/fet/karpathy-guidelines.md
|
|
4091
|
+
- .codex/fet/figma-stop.md when implementing UI from Figma
|
|
3195
4092
|
- the active change files under openspec/changes/<change-id>/, when a change is selected
|
|
3196
4093
|
|
|
3197
4094
|
If GitNexus code graph context is available in the IDE or MCP tools, prefer it before broad repository scans. Use it to identify relevant modules, dependencies, and insertion points, then read only the concrete source files needed. If GitNexus is unavailable, continue with the normal FET/OpenSpec workflow.
|
|
@@ -3210,6 +4107,7 @@ ${languageInstruction(language)}
|
|
|
3210
4107
|
- AGENTS.md
|
|
3211
4108
|
- openspec/config.yaml
|
|
3212
4109
|
- .codex/fet/karpathy-guidelines.md
|
|
4110
|
+
- \u6309 Figma \u5B9E\u73B0 UI \u65F6\u9605\u8BFB .codex/fet/figma-stop.md
|
|
3213
4111
|
- \u5982\u679C\u5DF2\u9009\u62E9 change\uFF0C\u9605\u8BFB openspec/changes/<change-id>/ \u4E0B\u7684\u5F53\u524D\u4EA7\u7269
|
|
3214
4112
|
|
|
3215
4113
|
\u5982\u679C IDE \u6216 MCP \u5DE5\u5177\u4E2D\u53EF\u7528 GitNexus \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\uFF0C\u5148\u7528\u5B83\u7F29\u5C0F\u4ED3\u5E93\u626B\u63CF\u8303\u56F4\uFF1B\u7528\u56FE\u8BC6\u522B\u76F8\u5173\u6A21\u5757\u3001\u4F9D\u8D56\u548C\u63D2\u5165\u70B9\uFF0C\u518D\u53EA\u8BFB\u53D6\u9700\u8981\u786E\u8BA4\u884C\u4E3A\u7684\u5177\u4F53\u6E90\u7801\u6587\u4EF6\u3002GitNexus \u4E0D\u53EF\u7528\u65F6\uFF0C\u6309\u666E\u901A FET/OpenSpec \u5DE5\u4F5C\u6D41\u7EE7\u7EED\u3002
|
|
@@ -3230,9 +4128,16 @@ FET:END -->
|
|
|
3230
4128
|
${body}`
|
|
3231
4129
|
};
|
|
3232
4130
|
}
|
|
4131
|
+
function codexFigmaStopFile(language = DEFAULT_LANGUAGE) {
|
|
4132
|
+
return {
|
|
4133
|
+
path: ".codex/fet/figma-stop.md",
|
|
4134
|
+
content: renderCodexFigmaStopGuide(language)
|
|
4135
|
+
};
|
|
4136
|
+
}
|
|
3233
4137
|
function codexCommandFiles(language = DEFAULT_LANGUAGE) {
|
|
3234
4138
|
return [
|
|
3235
4139
|
codexKarpathyGuidelinesFile(language),
|
|
4140
|
+
codexFigmaStopFile(language),
|
|
3236
4141
|
...FET_ADAPTER_COMMANDS.map((command) => ({
|
|
3237
4142
|
path: `.codex/fet/commands/${command}.md`,
|
|
3238
4143
|
content: renderCommand(command, language)
|
|
@@ -3583,7 +4488,7 @@ ${commandGoalZh(command)}
|
|
|
3583
4488
|
- \u9ED8\u8BA4\u4F7F\u7528\u4E2D\u6587\u4EA7\u51FA\u3002
|
|
3584
4489
|
- \u4E0D\u8981\u7ED5\u8FC7 FET \u76F4\u63A5\u8C03\u7528 openspec\uFF0C\u9664\u975E FET \u547D\u4EE4\u672C\u8EAB\u4E0D\u53EF\u7528\u3002
|
|
3585
4490
|
- change \u4E0D\u660E\u786E\u65F6\u5148\u8BE2\u95EE\u7528\u6237\u3002
|
|
3586
|
-
${command === "fill-context" ? "- \u66FF\u6362 AGENTS.md \u4E2D\
|
|
4491
|
+
${command === "fill-context" ? "- fet fill-context \u53EF\u80FD\u5DF2\u5199\u5165\u5C0F\u7A0B\u5E8F\u5305\u4F53\u79EF\u4E0E 2MB \u7EA6\u675F\uFF0C\u4E0D\u8981\u8986\u76D6\u8BE5\u8282\u4E2D\u7684\u626B\u63CF\u7ED3\u679C\uFF0C\u9664\u975E\u4ED3\u5E93\u7ED3\u6784\u5DF2\u53D8\u3002\n- \u66FF\u6362 AGENTS.md \u4E2D\u5176\u4F59 [NEEDS LLM INPUT] \u5360\u4F4D\u7B26\uFF0C\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002\n" : ""}${command === "propose" || command === "continue" ? "- \u4E00\u6B21\u53EA\u521B\u5EFA\u4E00\u4E2A ready artifact\uFF0C\u5E76\u5728\u5199\u5165\u524D\u9605\u8BFB\u4F9D\u8D56\u6587\u4EF6\u3002\n" : ""}${command === "propose" || command === "continue" ? "- \u4E0D\u8981\u5728\u7528\u6237\u5BA1\u9605\u5F53\u524D\u4EA7\u7269\u524D\u81EA\u52A8\u8FD0\u884C fet continue\u3001fet ff \u6216\u5FAA\u73AF\u751F\u6210\u540E\u7EED\u4EA7\u7269\uFF1B\u9700\u8981\u7528\u6237\u660E\u786E\u786E\u8BA4\u540E\u518D\u63A8\u8FDB\u3002\n" : ""}${command === "apply" ? "- \u4E0D\u8981\u5728\u672A\u5B8C\u6210\u771F\u5B9E\u5B9E\u73B0\u524D\u52FE\u9009 tasks.md\uFF1B\u4E0D\u8981\u4ECE apply \u9636\u6BB5\u76F4\u63A5 sync \u6216 archive\u3002\n" : ""}`;
|
|
3587
4492
|
}
|
|
3588
4493
|
function commandTitleZh(command) {
|
|
3589
4494
|
const titles = {
|
|
@@ -3680,10 +4585,11 @@ Steps:
|
|
|
3680
4585
|
- scripts, test commands, and build commands
|
|
3681
4586
|
- coding conventions and project-specific patterns
|
|
3682
4587
|
- important docs such as README files
|
|
3683
|
-
5.
|
|
3684
|
-
6.
|
|
3685
|
-
7.
|
|
3686
|
-
8.
|
|
4588
|
+
5. \`fet fill-context\` may already fill the mini program package-size table and 2MB constraints. Do not overwrite those subsections unless the repo changed.
|
|
4589
|
+
6. Replace every remaining \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete, concise project-specific content.
|
|
4590
|
+
7. Preserve FET managed markers such as \`FET:BEGIN AUTO\`, \`FET:END AUTO\`, \`FET:BEGIN USER\`, and \`FET:END USER\`.
|
|
4591
|
+
8. Do not modify business code.
|
|
4592
|
+
9. Run:
|
|
3687
4593
|
\`\`\`sh
|
|
3688
4594
|
fet doctor
|
|
3689
4595
|
\`\`\`
|
|
@@ -4190,7 +5096,7 @@ var CodexAdapter = class {
|
|
|
4190
5096
|
adapterVersion = 1;
|
|
4191
5097
|
async detect(projectRoot) {
|
|
4192
5098
|
return {
|
|
4193
|
-
detected: await
|
|
5099
|
+
detected: await exists5(join20(projectRoot, ".codex")) || await exists5(join20(projectRoot, "AGENTS.md")),
|
|
4194
5100
|
reason: "Codex adapter is available for projects that use AGENTS.md"
|
|
4195
5101
|
};
|
|
4196
5102
|
}
|
|
@@ -4256,9 +5162,9 @@ var CodexAdapter = class {
|
|
|
4256
5162
|
};
|
|
4257
5163
|
function resolveTarget(projectRoot, file) {
|
|
4258
5164
|
if (file.root === "codex-home") {
|
|
4259
|
-
return
|
|
5165
|
+
return join20(resolveCodexHome(), file.path);
|
|
4260
5166
|
}
|
|
4261
|
-
return
|
|
5167
|
+
return join20(projectRoot, file.path);
|
|
4262
5168
|
}
|
|
4263
5169
|
function displayPathFor(file) {
|
|
4264
5170
|
if (file.root === "codex-home") {
|
|
@@ -4267,18 +5173,18 @@ function displayPathFor(file) {
|
|
|
4267
5173
|
return file.path;
|
|
4268
5174
|
}
|
|
4269
5175
|
function resolveCodexHome() {
|
|
4270
|
-
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ??
|
|
5176
|
+
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join20(homedir(), ".codex");
|
|
4271
5177
|
}
|
|
4272
5178
|
async function readExisting(path) {
|
|
4273
5179
|
try {
|
|
4274
|
-
return await
|
|
5180
|
+
return await readFile16(path, "utf8");
|
|
4275
5181
|
} catch {
|
|
4276
5182
|
return null;
|
|
4277
5183
|
}
|
|
4278
5184
|
}
|
|
4279
|
-
async function
|
|
5185
|
+
async function exists5(path) {
|
|
4280
5186
|
try {
|
|
4281
|
-
await
|
|
5187
|
+
await stat9(path);
|
|
4282
5188
|
return true;
|
|
4283
5189
|
} catch {
|
|
4284
5190
|
return false;
|
|
@@ -4286,10 +5192,19 @@ async function exists3(path) {
|
|
|
4286
5192
|
}
|
|
4287
5193
|
|
|
4288
5194
|
// src/adapters/cursor/index.ts
|
|
4289
|
-
import { mkdir as mkdir9, readFile as
|
|
4290
|
-
import { dirname as dirname9, join as
|
|
5195
|
+
import { mkdir as mkdir9, readFile as readFile17, stat as stat10 } from "fs/promises";
|
|
5196
|
+
import { dirname as dirname9, join as join21 } from "path";
|
|
4291
5197
|
|
|
4292
5198
|
// src/adapters/cursor/templates.ts
|
|
5199
|
+
function cursorFigmaStopRuleFile(language = DEFAULT_LANGUAGE) {
|
|
5200
|
+
return {
|
|
5201
|
+
path: ".cursor/rules/fet-figma-stop.mdc",
|
|
5202
|
+
content: renderCursorFigmaStopRule(language)
|
|
5203
|
+
};
|
|
5204
|
+
}
|
|
5205
|
+
function cursorRuleFiles(language = DEFAULT_LANGUAGE) {
|
|
5206
|
+
return [cursorRuleFile(language), cursorFigmaStopRuleFile(language)];
|
|
5207
|
+
}
|
|
4293
5208
|
function cursorSkillFiles(language = DEFAULT_LANGUAGE) {
|
|
4294
5209
|
return FET_ADAPTER_COMMANDS.map((command) => ({
|
|
4295
5210
|
path: `.cursor/skills/fet-${command}/SKILL.md`,
|
|
@@ -4319,6 +5234,7 @@ ${languageInstruction(language)}
|
|
|
4319
5234
|
- openspec/config.yaml
|
|
4320
5235
|
- \u53EF\u7528\u65F6\u7684 GitNexus \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\u3002\u4F18\u5148\u7528\u5B83\u7F29\u5C0F\u8303\u56F4\uFF1B\u4E0D\u53EF\u7528\u65F6\u6309\u666E\u901A FET/OpenSpec \u5DE5\u4F5C\u6D41\u7EE7\u7EED\u3002
|
|
4321
5236
|
- \u5F53\u524D change \u76EE\u5F55\u4E0B\u7684 OpenSpec \u89C4\u5212\u4EA7\u7269\u3002
|
|
5237
|
+
- \u6309 Figma \u5B9E\u73B0 UI \u65F6\u9075\u5B88 \`.cursor/rules/fet-figma-stop.mdc\`\uFF1B\u82E5\u5B58\u5728 \`openspec/changes/<change-id>/.fet/figma-stop.md\` \u987B\u5148\u9605\u8BFB\u3002
|
|
4322
5238
|
|
|
4323
5239
|
\u5982\u679C\u7528\u6237\u8F93\u5165\u7C7B\u4F3C \`/fet apply\` \u7684\u8BF7\u6C42\uFF0C\u8BF7\u628A\u5B83\u89C6\u4E3A\u5DE5\u4F5C\u6D41\u610F\u56FE\uFF0C\u5E76\u63D0\u793A\u6216\u6267\u884C\u5BF9\u5E94\u7684\u7EC8\u7AEF\u547D\u4EE4 \`fet <cmd>\`\u3002
|
|
4324
5240
|
`
|
|
@@ -4352,7 +5268,9 @@ ${languageInstruction(language)}
|
|
|
4352
5268
|
- AGENTS.md
|
|
4353
5269
|
- openspec/config.yaml
|
|
4354
5270
|
|
|
4355
|
-
|
|
5271
|
+
\`fet fill-context\` \u53EF\u80FD\u5DF2\u5199\u5165\u5C0F\u7A0B\u5E8F\u5305\u4F53\u79EF\u8868\u4E0E 2MB \u5F00\u53D1\u7EA6\u675F\uFF0C\u4E0D\u8981\u8986\u76D6 AGENTS.md\u300C\u5C0F\u7A0B\u5E8F\u300D\u8282\u4E2D\u7684\u626B\u63CF\u7ED3\u679C\uFF0C\u9664\u975E\u4ED3\u5E93\u7ED3\u6784\u5DF2\u53D8\u3002
|
|
5272
|
+
|
|
5273
|
+
\u68C0\u67E5 README\u3001package scripts\u3001\u8DEF\u7531\u3001\u6D4B\u8BD5\u3001\u6E90\u7801\u7ED3\u6784\u548C\u73B0\u6709\u7EA6\u5B9A\u540E\uFF0C\u628A AGENTS.md \u4E2D**\u5176\u4F59** \`[NEEDS LLM INPUT]\` \u6216 \`[NEED LLM INPUT]\` \u5360\u4F4D\u7B26\u66FF\u6362\u4E3A\u5177\u4F53\u3001\u7B80\u6D01\u3001\u9879\u76EE\u76F8\u5173\u7684\u5185\u5BB9\u3002\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002
|
|
4356
5274
|
`;
|
|
4357
5275
|
}
|
|
4358
5276
|
if (command === "graph-setup") {
|
|
@@ -4421,14 +5339,14 @@ var CursorAdapter = class {
|
|
|
4421
5339
|
adapterVersion = 1;
|
|
4422
5340
|
async detect(projectRoot) {
|
|
4423
5341
|
return {
|
|
4424
|
-
detected: await
|
|
5342
|
+
detected: await exists6(join21(projectRoot, ".cursor")),
|
|
4425
5343
|
reason: "Cursor adapter is available for any project"
|
|
4426
5344
|
};
|
|
4427
5345
|
}
|
|
4428
5346
|
async planInstall(_projectRoot, language) {
|
|
4429
5347
|
return {
|
|
4430
5348
|
tool: this.tool,
|
|
4431
|
-
files: [...cursorSkillFiles(language),
|
|
5349
|
+
files: [...cursorSkillFiles(language), ...cursorRuleFiles(language)].map((file) => ({
|
|
4432
5350
|
...file,
|
|
4433
5351
|
managed: true
|
|
4434
5352
|
}))
|
|
@@ -4438,7 +5356,7 @@ var CursorAdapter = class {
|
|
|
4438
5356
|
const written = [];
|
|
4439
5357
|
const skipped = [];
|
|
4440
5358
|
for (const file of plan.files) {
|
|
4441
|
-
const target =
|
|
5359
|
+
const target = join21(projectRoot, file.path);
|
|
4442
5360
|
const existing = await readExisting2(target);
|
|
4443
5361
|
if (existing && !existing.includes("FET:MANAGED") && !force) {
|
|
4444
5362
|
throw new FetError({
|
|
@@ -4461,7 +5379,7 @@ var CursorAdapter = class {
|
|
|
4461
5379
|
const plan = await this.planInstall(projectRoot);
|
|
4462
5380
|
const checks = [];
|
|
4463
5381
|
for (const file of plan.files) {
|
|
4464
|
-
const target =
|
|
5382
|
+
const target = join21(projectRoot, file.path);
|
|
4465
5383
|
const content = await readExisting2(target);
|
|
4466
5384
|
const managed = Boolean(content?.includes("FET:MANAGED"));
|
|
4467
5385
|
const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
|
|
@@ -4477,14 +5395,14 @@ var CursorAdapter = class {
|
|
|
4477
5395
|
};
|
|
4478
5396
|
async function readExisting2(path) {
|
|
4479
5397
|
try {
|
|
4480
|
-
return await
|
|
5398
|
+
return await readFile17(path, "utf8");
|
|
4481
5399
|
} catch {
|
|
4482
5400
|
return null;
|
|
4483
5401
|
}
|
|
4484
5402
|
}
|
|
4485
|
-
async function
|
|
5403
|
+
async function exists6(path) {
|
|
4486
5404
|
try {
|
|
4487
|
-
await
|
|
5405
|
+
await stat10(path);
|
|
4488
5406
|
return true;
|
|
4489
5407
|
} catch {
|
|
4490
5408
|
return false;
|
|
@@ -4496,45 +5414,45 @@ import { execFile as execFile4 } from "child_process";
|
|
|
4496
5414
|
import { promisify as promisify4 } from "util";
|
|
4497
5415
|
|
|
4498
5416
|
// src/openspec/inspector.ts
|
|
4499
|
-
import { readdir as
|
|
4500
|
-
import { join as
|
|
5417
|
+
import { readdir as readdir4, stat as stat11 } from "fs/promises";
|
|
5418
|
+
import { join as join22 } from "path";
|
|
4501
5419
|
async function inspectOpenSpecProject(projectRoot) {
|
|
4502
|
-
const openspecPath =
|
|
4503
|
-
const changesPath =
|
|
4504
|
-
const legacyArchivePath =
|
|
4505
|
-
const changesArchivePath =
|
|
5420
|
+
const openspecPath = join22(projectRoot, "openspec");
|
|
5421
|
+
const changesPath = join22(openspecPath, "changes");
|
|
5422
|
+
const legacyArchivePath = join22(openspecPath, "archive");
|
|
5423
|
+
const changesArchivePath = join22(changesPath, "archive");
|
|
4506
5424
|
return {
|
|
4507
|
-
exists: await
|
|
5425
|
+
exists: await exists7(openspecPath),
|
|
4508
5426
|
changes: await listDirectories(changesPath, { exclude: ["archive"] }),
|
|
4509
5427
|
archived: [.../* @__PURE__ */ new Set([...await listDirectories(legacyArchivePath), ...await listDirectories(changesArchivePath)])]
|
|
4510
5428
|
};
|
|
4511
5429
|
}
|
|
4512
5430
|
async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
4513
|
-
const changePath =
|
|
4514
|
-
const tasksPath =
|
|
4515
|
-
const specsPath =
|
|
5431
|
+
const changePath = join22(projectRoot, "openspec", "changes", changeId);
|
|
5432
|
+
const tasksPath = join22(changePath, "tasks.md");
|
|
5433
|
+
const specsPath = join22(changePath, "specs");
|
|
4516
5434
|
return {
|
|
4517
5435
|
changeId,
|
|
4518
|
-
exists: await
|
|
4519
|
-
hasProposal: await
|
|
4520
|
-
hasTasks: await
|
|
4521
|
-
hasSpecs: await
|
|
5436
|
+
exists: await exists7(changePath),
|
|
5437
|
+
hasProposal: await exists7(join22(changePath, "proposal.md")),
|
|
5438
|
+
hasTasks: await exists7(tasksPath),
|
|
5439
|
+
hasSpecs: await exists7(specsPath),
|
|
4522
5440
|
tasksPath,
|
|
4523
5441
|
changePath
|
|
4524
5442
|
};
|
|
4525
5443
|
}
|
|
4526
5444
|
async function listDirectories(path, options = {}) {
|
|
4527
5445
|
try {
|
|
4528
|
-
const entries = await
|
|
5446
|
+
const entries = await readdir4(path, { withFileTypes: true });
|
|
4529
5447
|
const excluded = new Set(options.exclude ?? []);
|
|
4530
5448
|
return entries.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).map((entry) => entry.name);
|
|
4531
5449
|
} catch {
|
|
4532
5450
|
return [];
|
|
4533
5451
|
}
|
|
4534
5452
|
}
|
|
4535
|
-
async function
|
|
5453
|
+
async function exists7(path) {
|
|
4536
5454
|
try {
|
|
4537
|
-
await
|
|
5455
|
+
await stat11(path);
|
|
4538
5456
|
return true;
|
|
4539
5457
|
} catch {
|
|
4540
5458
|
return false;
|
|
@@ -4716,146 +5634,15 @@ function escapeRegExp(value) {
|
|
|
4716
5634
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4717
5635
|
}
|
|
4718
5636
|
|
|
4719
|
-
// src/scanner/package.ts
|
|
4720
|
-
import { readFile as readFile15, stat as stat9 } from "fs/promises";
|
|
4721
|
-
import { join as join19 } from "path";
|
|
4722
|
-
import { parse as parse2 } from "yaml";
|
|
4723
|
-
async function readPackageJson(projectRoot) {
|
|
4724
|
-
try {
|
|
4725
|
-
return JSON.parse(await readFile15(join19(projectRoot, "package.json"), "utf8"));
|
|
4726
|
-
} catch {
|
|
4727
|
-
return null;
|
|
4728
|
-
}
|
|
4729
|
-
}
|
|
4730
|
-
async function detectPackageManager(projectRoot, pkg) {
|
|
4731
|
-
const warnings = [];
|
|
4732
|
-
if (pkg?.packageManager) {
|
|
4733
|
-
const declared = pkg.packageManager.split("@")[0] ?? "unknown";
|
|
4734
|
-
const locks2 = await detectLockManagers(projectRoot);
|
|
4735
|
-
const conflicting = locks2.filter((item) => item !== declared);
|
|
4736
|
-
if (conflicting.length) {
|
|
4737
|
-
warnings.push(`packageManager \u58F0\u660E\u4E3A ${declared}\uFF0C\u4F46\u540C\u65F6\u53D1\u73B0\u9501\u6587\u4EF6\uFF1A${conflicting.join(", ")}`);
|
|
4738
|
-
}
|
|
4739
|
-
return { manager: declared, confidence: "high", warnings };
|
|
4740
|
-
}
|
|
4741
|
-
const locks = await detectLockManagers(projectRoot);
|
|
4742
|
-
if (locks.length > 1) {
|
|
4743
|
-
warnings.push(`\u53D1\u73B0\u591A\u4E2A\u5305\u7BA1\u7406\u5668\u9501\u6587\u4EF6\uFF1A${locks.join(", ")}\uFF0C\u9ED8\u8BA4\u4F7F\u7528 ${locks[0]}`);
|
|
4744
|
-
return { manager: locks[0] ?? "npm", confidence: "medium", warnings };
|
|
4745
|
-
}
|
|
4746
|
-
if (locks[0]) {
|
|
4747
|
-
return { manager: locks[0], confidence: "high", warnings };
|
|
4748
|
-
}
|
|
4749
|
-
return { manager: "npm", confidence: "low", warnings };
|
|
4750
|
-
}
|
|
4751
|
-
function extractCommands(pkg, packageManager) {
|
|
4752
|
-
const scripts = pkg?.scripts ?? {};
|
|
4753
|
-
const result = {};
|
|
4754
|
-
const scriptNames = ["dev", "build", "lint", "typecheck", "check", "test", "test:unit"];
|
|
4755
|
-
for (const name of scriptNames) {
|
|
4756
|
-
if (scripts[name]) {
|
|
4757
|
-
const dimension = name === "check" ? "typecheck" : name === "test:unit" ? "test" : name;
|
|
4758
|
-
if (result[dimension]) {
|
|
4759
|
-
continue;
|
|
4760
|
-
}
|
|
4761
|
-
result[dimension] = {
|
|
4762
|
-
command: scriptCommand(packageManager, name),
|
|
4763
|
-
source: `package.json:scripts.${name}`,
|
|
4764
|
-
required: name === "build"
|
|
4765
|
-
};
|
|
4766
|
-
}
|
|
4767
|
-
}
|
|
4768
|
-
return result;
|
|
4769
|
-
}
|
|
4770
|
-
function detectFramework(pkg) {
|
|
4771
|
-
const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
|
|
4772
|
-
const candidates = [
|
|
4773
|
-
["next", ["next"]],
|
|
4774
|
-
["nuxt", ["nuxt"]],
|
|
4775
|
-
["vite", ["vite"]],
|
|
4776
|
-
["sveltekit", ["@sveltejs/kit"]],
|
|
4777
|
-
["angular", ["@angular/core", "@angular/cli"]],
|
|
4778
|
-
["react", ["react"]],
|
|
4779
|
-
["vue", ["vue"]],
|
|
4780
|
-
["svelte", ["svelte"]]
|
|
4781
|
-
];
|
|
4782
|
-
for (const [candidate, packages] of candidates) {
|
|
4783
|
-
if (packages.some((name) => deps[name])) {
|
|
4784
|
-
return { name: candidate, confidence: "high", sources: ["package.json"] };
|
|
4785
|
-
}
|
|
4786
|
-
}
|
|
4787
|
-
return { name: "unknown", confidence: "low", sources: [] };
|
|
4788
|
-
}
|
|
4789
|
-
async function detectLanguage(projectRoot, pkg) {
|
|
4790
|
-
const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
|
|
4791
|
-
if (deps.typescript || await exists6(join19(projectRoot, "tsconfig.json"))) {
|
|
4792
|
-
return "typescript";
|
|
4793
|
-
}
|
|
4794
|
-
return "javascript";
|
|
4795
|
-
}
|
|
4796
|
-
async function detectWorkspaces(projectRoot, pkg) {
|
|
4797
|
-
const packageWorkspaces = normalizeWorkspaces(pkg?.workspaces).map((path) => ({
|
|
4798
|
-
name: path,
|
|
4799
|
-
path,
|
|
4800
|
-
source: "package.json:workspaces"
|
|
4801
|
-
}));
|
|
4802
|
-
if (packageWorkspaces.length) {
|
|
4803
|
-
return packageWorkspaces;
|
|
4804
|
-
}
|
|
4805
|
-
try {
|
|
4806
|
-
const workspace = parse2(await readFile15(join19(projectRoot, "pnpm-workspace.yaml"), "utf8"));
|
|
4807
|
-
return (workspace?.packages ?? []).map((path) => ({
|
|
4808
|
-
name: path,
|
|
4809
|
-
path,
|
|
4810
|
-
source: "pnpm-workspace.yaml:packages"
|
|
4811
|
-
}));
|
|
4812
|
-
} catch {
|
|
4813
|
-
return [];
|
|
4814
|
-
}
|
|
4815
|
-
}
|
|
4816
|
-
async function detectLockManagers(projectRoot) {
|
|
4817
|
-
const lockFiles = [
|
|
4818
|
-
["pnpm-lock.yaml", "pnpm"],
|
|
4819
|
-
["yarn.lock", "yarn"],
|
|
4820
|
-
["bun.lockb", "bun"],
|
|
4821
|
-
["bun.lock", "bun"],
|
|
4822
|
-
["package-lock.json", "npm"]
|
|
4823
|
-
];
|
|
4824
|
-
const found = [];
|
|
4825
|
-
for (const [file, manager] of lockFiles) {
|
|
4826
|
-
if (await exists6(join19(projectRoot, file))) {
|
|
4827
|
-
found.push(manager);
|
|
4828
|
-
}
|
|
4829
|
-
}
|
|
4830
|
-
return found;
|
|
4831
|
-
}
|
|
4832
|
-
function normalizeWorkspaces(workspaces) {
|
|
4833
|
-
if (Array.isArray(workspaces)) {
|
|
4834
|
-
return workspaces;
|
|
4835
|
-
}
|
|
4836
|
-
return workspaces?.packages ?? [];
|
|
4837
|
-
}
|
|
4838
|
-
function scriptCommand(packageManager, name) {
|
|
4839
|
-
return packageManager === "npm" ? `npm run ${name}` : `${packageManager} ${name}`;
|
|
4840
|
-
}
|
|
4841
|
-
async function exists6(path) {
|
|
4842
|
-
try {
|
|
4843
|
-
await stat9(path);
|
|
4844
|
-
return true;
|
|
4845
|
-
} catch {
|
|
4846
|
-
return false;
|
|
4847
|
-
}
|
|
4848
|
-
}
|
|
4849
|
-
|
|
4850
5637
|
// src/scanner/routes.ts
|
|
4851
|
-
import { readdir as
|
|
4852
|
-
import { join as
|
|
5638
|
+
import { readdir as readdir5, stat as stat12 } from "fs/promises";
|
|
5639
|
+
import { join as join23, relative as relative3, sep } from "path";
|
|
4853
5640
|
async function scanRoutes(projectRoot) {
|
|
4854
5641
|
const candidates = ["src/routes", "src/pages", "app", "pages"];
|
|
4855
5642
|
const routes = [];
|
|
4856
5643
|
for (const candidate of candidates) {
|
|
4857
|
-
const root =
|
|
4858
|
-
if (!await
|
|
5644
|
+
const root = join23(projectRoot, candidate);
|
|
5645
|
+
if (!await exists8(root)) {
|
|
4859
5646
|
continue;
|
|
4860
5647
|
}
|
|
4861
5648
|
for (const file of await listFiles(root)) {
|
|
@@ -4863,8 +5650,8 @@ async function scanRoutes(projectRoot) {
|
|
|
4863
5650
|
continue;
|
|
4864
5651
|
}
|
|
4865
5652
|
routes.push({
|
|
4866
|
-
path: inferRoutePath(
|
|
4867
|
-
source:
|
|
5653
|
+
path: inferRoutePath(relative3(root, file)),
|
|
5654
|
+
source: relative3(projectRoot, file).split(sep).join("/"),
|
|
4868
5655
|
inferred: true,
|
|
4869
5656
|
confidence: "medium"
|
|
4870
5657
|
});
|
|
@@ -4879,10 +5666,10 @@ function inferRoutePath(relativePath) {
|
|
|
4879
5666
|
return `/${withoutIndex}`.replace(/\/+/g, "/");
|
|
4880
5667
|
}
|
|
4881
5668
|
async function listFiles(root) {
|
|
4882
|
-
const entries = await
|
|
5669
|
+
const entries = await readdir5(root, { withFileTypes: true });
|
|
4883
5670
|
const files = [];
|
|
4884
5671
|
for (const entry of entries) {
|
|
4885
|
-
const path =
|
|
5672
|
+
const path = join23(root, entry.name);
|
|
4886
5673
|
if (entry.isDirectory()) {
|
|
4887
5674
|
files.push(...await listFiles(path));
|
|
4888
5675
|
} else {
|
|
@@ -4891,9 +5678,9 @@ async function listFiles(root) {
|
|
|
4891
5678
|
}
|
|
4892
5679
|
return files;
|
|
4893
5680
|
}
|
|
4894
|
-
async function
|
|
5681
|
+
async function exists8(path) {
|
|
4895
5682
|
try {
|
|
4896
|
-
await
|
|
5683
|
+
await stat12(path);
|
|
4897
5684
|
return true;
|
|
4898
5685
|
} catch {
|
|
4899
5686
|
return false;
|
|
@@ -4908,10 +5695,14 @@ var ProjectScanner = class {
|
|
|
4908
5695
|
const framework = detectFramework(pkg);
|
|
4909
5696
|
const workspaces = await detectWorkspaces(projectRoot, pkg);
|
|
4910
5697
|
const language = await detectLanguage(projectRoot, pkg);
|
|
5698
|
+
const miniprogram = await detectMiniprogramProject(projectRoot);
|
|
4911
5699
|
const warnings = [...packageManager.warnings];
|
|
4912
5700
|
if (framework.name === "unknown") {
|
|
4913
5701
|
warnings.push("\u672A\u80FD\u4ECE package.json \u8BC6\u522B\u4E3B\u8981\u6846\u67B6");
|
|
4914
5702
|
}
|
|
5703
|
+
if (miniprogram.supported) {
|
|
5704
|
+
warnings.push(...miniprogram.warnings);
|
|
5705
|
+
}
|
|
4915
5706
|
return {
|
|
4916
5707
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4917
5708
|
scannerVersion: 1,
|
|
@@ -4928,7 +5719,8 @@ var ProjectScanner = class {
|
|
|
4928
5719
|
routes: await scanRoutes(projectRoot),
|
|
4929
5720
|
conventions: [],
|
|
4930
5721
|
skippedFiles: [],
|
|
4931
|
-
warnings
|
|
5722
|
+
warnings,
|
|
5723
|
+
miniprogram
|
|
4932
5724
|
};
|
|
4933
5725
|
}
|
|
4934
5726
|
};
|
|
@@ -5045,9 +5837,9 @@ async function createCommandContext(command, options) {
|
|
|
5045
5837
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
5046
5838
|
|
|
5047
5839
|
// src/update/check.ts
|
|
5048
|
-
import { mkdir as mkdir10, readFile as
|
|
5840
|
+
import { mkdir as mkdir10, readFile as readFile18, writeFile } from "fs/promises";
|
|
5049
5841
|
import { homedir as homedir2 } from "os";
|
|
5050
|
-
import { dirname as dirname10, join as
|
|
5842
|
+
import { dirname as dirname10, join as join24 } from "path";
|
|
5051
5843
|
var DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
5052
5844
|
function getFetUpdateCheckMode(env = process.env) {
|
|
5053
5845
|
const value = env.FET_UPDATE_CHECK?.trim().toLowerCase();
|
|
@@ -5120,11 +5912,11 @@ function formatFetUpdateWarning(availability, language) {
|
|
|
5120
5912
|
}
|
|
5121
5913
|
function cachePath() {
|
|
5122
5914
|
const home = process.env.FET_UPDATE_CHECK_CACHE_HOME?.trim() || homedir2();
|
|
5123
|
-
return
|
|
5915
|
+
return join24(home, ".fet", "update-check-cache.json");
|
|
5124
5916
|
}
|
|
5125
5917
|
async function readUpdateCheckCache() {
|
|
5126
5918
|
try {
|
|
5127
|
-
const raw = await
|
|
5919
|
+
const raw = await readFile18(cachePath(), "utf8");
|
|
5128
5920
|
const parsed = JSON.parse(raw);
|
|
5129
5921
|
if (typeof parsed.latestVersion !== "string" || typeof parsed.checkedAt !== "string") {
|
|
5130
5922
|
return null;
|