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