@nick848/fet 1.0.8 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +500 -178
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -179,7 +179,12 @@ function toGitNexusState(detection, previous) {
|
|
|
179
179
|
setupHandoffPath: previous?.setupHandoffPath ?? null,
|
|
180
180
|
setupHandoffUpdatedAt: previous?.setupHandoffUpdatedAt ?? null,
|
|
181
181
|
handoffPath: previous?.handoffPath ?? null,
|
|
182
|
-
handoffUpdatedAt: previous?.handoffUpdatedAt ?? null
|
|
182
|
+
handoffUpdatedAt: previous?.handoffUpdatedAt ?? null,
|
|
183
|
+
projectContextPath: previous?.projectContextPath ?? null,
|
|
184
|
+
projectContextUpdatedAt: previous?.projectContextUpdatedAt ?? null,
|
|
185
|
+
workflowContextPath: previous?.workflowContextPath ?? null,
|
|
186
|
+
workflowContextUpdatedAt: previous?.workflowContextUpdatedAt ?? null,
|
|
187
|
+
lastWorkflowGraphQuery: previous?.lastWorkflowGraphQuery ?? null
|
|
183
188
|
};
|
|
184
189
|
}
|
|
185
190
|
async function inspectGitNexusGraph(projectRoot, env = process.env) {
|
|
@@ -248,7 +253,8 @@ function resolveGitNexusCommand(env) {
|
|
|
248
253
|
const raw = env.FET_GITNEXUS_COMMAND?.trim() || env.FET_GITNEXUS_EXECUTABLE?.trim() || "gitnexus";
|
|
249
254
|
const parts = splitCommand(raw);
|
|
250
255
|
const [file = "gitnexus", ...args] = parts;
|
|
251
|
-
|
|
256
|
+
const resolvedFile = process.platform === "win32" && raw === "gitnexus" ? "gitnexus.cmd" : file;
|
|
257
|
+
return { file: resolvedFile, args, label: raw };
|
|
252
258
|
}
|
|
253
259
|
function splitCommand(value) {
|
|
254
260
|
const matches = value.match(/"[^"]+"|'[^']+'|\S+/g);
|
|
@@ -442,8 +448,313 @@ FET:END -->
|
|
|
442
448
|
}
|
|
443
449
|
|
|
444
450
|
// src/commands/graph.ts
|
|
445
|
-
import { mkdir as
|
|
451
|
+
import { mkdir as mkdir5 } from "fs/promises";
|
|
452
|
+
import { dirname as dirname6, join as join9 } from "path";
|
|
453
|
+
|
|
454
|
+
// src/graph-context.ts
|
|
455
|
+
import { mkdir as mkdir4, readdir, readFile as readFile5 } from "fs/promises";
|
|
446
456
|
import { dirname as dirname5, join as join8 } from "path";
|
|
457
|
+
var MAX_SOURCE_CONTEXT = 8e3;
|
|
458
|
+
var MAX_GRAPH_OUTPUT = 2e4;
|
|
459
|
+
async function buildProjectGraphContext(ctx, state, trigger) {
|
|
460
|
+
if (!isGraphReadable(state)) {
|
|
461
|
+
return {
|
|
462
|
+
generated: false,
|
|
463
|
+
path: null,
|
|
464
|
+
query: null,
|
|
465
|
+
warnings: ["GitNexus graph exists check did not pass; project graph context was not generated."]
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
const query = "FET OpenSpec workflow architecture commands adapters graph integration project structure";
|
|
469
|
+
const goal = "Summarize the repository modules, workflow entry points, and likely insertion points for future FET/OpenSpec work.";
|
|
470
|
+
const graphQuery = await runGitNexus(["query", query, "--goal", goal, "--limit", "8"], { cwd: ctx.projectRoot });
|
|
471
|
+
const status = await runGitNexus(["status"], { cwd: ctx.projectRoot });
|
|
472
|
+
const warnings = commandWarnings([["gitnexus query", graphQuery], ["gitnexus status", status]]);
|
|
473
|
+
const relativePath = ".fet/graph-context/project.md";
|
|
474
|
+
await writeGraphContext(
|
|
475
|
+
join8(ctx.projectRoot, relativePath),
|
|
476
|
+
renderProjectContext({
|
|
477
|
+
trigger,
|
|
478
|
+
state,
|
|
479
|
+
query,
|
|
480
|
+
goal,
|
|
481
|
+
status: commandText(status),
|
|
482
|
+
graphOutput: commandText(graphQuery),
|
|
483
|
+
warnings
|
|
484
|
+
})
|
|
485
|
+
);
|
|
486
|
+
return {
|
|
487
|
+
generated: true,
|
|
488
|
+
path: relativePath,
|
|
489
|
+
query,
|
|
490
|
+
warnings
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
async function buildWorkflowGraphContext(ctx, options) {
|
|
494
|
+
const existing = await ctx.stateStore.getOrCreateGlobal();
|
|
495
|
+
if (!existing.graph?.gitnexus?.graphExists) {
|
|
496
|
+
return {
|
|
497
|
+
generated: false,
|
|
498
|
+
path: null,
|
|
499
|
+
query: null,
|
|
500
|
+
warnings: []
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const state = await refreshGitNexusState(ctx);
|
|
504
|
+
if (!isGraphReadable(state)) {
|
|
505
|
+
return {
|
|
506
|
+
generated: false,
|
|
507
|
+
path: null,
|
|
508
|
+
query: null,
|
|
509
|
+
warnings: []
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
const sourceContext = await collectOpenSpecContext(ctx.projectRoot, options.changeId);
|
|
513
|
+
const query = buildWorkflowQuery(options, sourceContext);
|
|
514
|
+
const goal = workflowGoal(options.command);
|
|
515
|
+
const graphQuery = await runGitNexus(["query", query, "--goal", goal, "--limit", "8"], { cwd: ctx.projectRoot });
|
|
516
|
+
const detectChanges = shouldDetectChanges(options.command) ? await runGitNexus(["detect-changes", "--scope", "all"], { cwd: ctx.projectRoot }) : null;
|
|
517
|
+
const warnings = commandWarnings([
|
|
518
|
+
["gitnexus query", graphQuery],
|
|
519
|
+
...detectChanges ? [["gitnexus detect-changes", detectChanges]] : []
|
|
520
|
+
]);
|
|
521
|
+
const relativePath = `.fet/graph-context/${sanitizePathPart(options.changeId ?? options.command)}.md`;
|
|
522
|
+
await writeGraphContext(
|
|
523
|
+
join8(ctx.projectRoot, relativePath),
|
|
524
|
+
renderWorkflowContext({
|
|
525
|
+
state,
|
|
526
|
+
command: options.command,
|
|
527
|
+
args: options.args,
|
|
528
|
+
changeId: options.changeId,
|
|
529
|
+
query,
|
|
530
|
+
goal,
|
|
531
|
+
sourceContext,
|
|
532
|
+
graphOutput: commandText(graphQuery),
|
|
533
|
+
detectChanges: detectChanges ? commandText(detectChanges) : null,
|
|
534
|
+
warnings
|
|
535
|
+
})
|
|
536
|
+
);
|
|
537
|
+
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
538
|
+
global.graph ??= {};
|
|
539
|
+
global.graph.gitnexus = {
|
|
540
|
+
...state,
|
|
541
|
+
workflowContextPath: relativePath,
|
|
542
|
+
workflowContextUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
543
|
+
lastWorkflowGraphQuery: query
|
|
544
|
+
};
|
|
545
|
+
await ctx.stateStore.writeGlobal(global);
|
|
546
|
+
return {
|
|
547
|
+
generated: true,
|
|
548
|
+
path: relativePath,
|
|
549
|
+
query,
|
|
550
|
+
warnings
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
async function refreshGitNexusState(ctx) {
|
|
554
|
+
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
555
|
+
global.graph ??= {};
|
|
556
|
+
const detection = await detectGitNexus();
|
|
557
|
+
const graph2 = await inspectGitNexusGraph(ctx.projectRoot);
|
|
558
|
+
const state = mergeGitNexusGraphInfo(toGitNexusState(detection, global.graph.gitnexus), graph2);
|
|
559
|
+
global.graph.gitnexus = state;
|
|
560
|
+
await ctx.stateStore.writeGlobal(global);
|
|
561
|
+
return state;
|
|
562
|
+
}
|
|
563
|
+
function isGraphReadable(state) {
|
|
564
|
+
return Boolean(state.installed && state.graphExists);
|
|
565
|
+
}
|
|
566
|
+
async function writeGraphContext(path, content) {
|
|
567
|
+
await mkdir4(dirname5(path), { recursive: true });
|
|
568
|
+
await atomicWrite(path, content);
|
|
569
|
+
}
|
|
570
|
+
function renderProjectContext(options) {
|
|
571
|
+
return `<!-- FET:MANAGED
|
|
572
|
+
schemaVersion: 1
|
|
573
|
+
generator: graph-context
|
|
574
|
+
scope: project
|
|
575
|
+
FET:END -->
|
|
576
|
+
|
|
577
|
+
# FET GitNexus Project Graph Context
|
|
578
|
+
|
|
579
|
+
Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
580
|
+
Trigger: ${options.trigger}
|
|
581
|
+
|
|
582
|
+
Use this file before broad repository scans in FET/OpenSpec work. Treat it as graph-derived context: confirm concrete behavior by reading the source files it points to.
|
|
583
|
+
|
|
584
|
+
## Graph State
|
|
585
|
+
|
|
586
|
+
- Provider: GitNexus
|
|
587
|
+
- Installed: ${options.state.installed ? "yes" : "no"}
|
|
588
|
+
- Version: ${options.state.version ?? "unknown"}
|
|
589
|
+
- Graph path: ${options.state.graphPath ?? ".gitnexus"}
|
|
590
|
+
- Graph exists: ${options.state.graphExists ? "yes" : "no"}
|
|
591
|
+
- Last indexed at: ${options.state.lastIndexedAt ?? "unknown"}
|
|
592
|
+
|
|
593
|
+
## GitNexus Status
|
|
594
|
+
|
|
595
|
+
\`\`\`text
|
|
596
|
+
${clip(options.status, MAX_GRAPH_OUTPUT)}
|
|
597
|
+
\`\`\`
|
|
598
|
+
|
|
599
|
+
## Project Query
|
|
600
|
+
|
|
601
|
+
- Query: ${options.query}
|
|
602
|
+
- Goal: ${options.goal}
|
|
603
|
+
|
|
604
|
+
\`\`\`text
|
|
605
|
+
${clip(options.graphOutput, MAX_GRAPH_OUTPUT)}
|
|
606
|
+
\`\`\`
|
|
607
|
+
${renderWarnings(options.warnings)}
|
|
608
|
+
`;
|
|
609
|
+
}
|
|
610
|
+
function renderWorkflowContext(options) {
|
|
611
|
+
return `<!-- FET:MANAGED
|
|
612
|
+
schemaVersion: 1
|
|
613
|
+
generator: graph-context
|
|
614
|
+
scope: workflow
|
|
615
|
+
FET:END -->
|
|
616
|
+
|
|
617
|
+
# FET GitNexus Workflow Graph Context
|
|
618
|
+
|
|
619
|
+
Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
620
|
+
|
|
621
|
+
Read this before editing or generating OpenSpec artifacts for this workflow. Use the graph output to narrow likely files, symbols, dependencies, and impact areas. If it conflicts with OpenSpec artifacts or source code, OpenSpec artifacts and source code win.
|
|
622
|
+
|
|
623
|
+
## Workflow
|
|
624
|
+
|
|
625
|
+
- FET command: ${options.command}
|
|
626
|
+
- Args: ${options.args.length ? options.args.join(" ") : "(none)"}
|
|
627
|
+
- Change: ${options.changeId ?? "(none)"}
|
|
628
|
+
- Graph path: ${options.state.graphPath ?? ".gitnexus"}
|
|
629
|
+
- Last indexed at: ${options.state.lastIndexedAt ?? "unknown"}
|
|
630
|
+
|
|
631
|
+
## OpenSpec Context Used For Query
|
|
632
|
+
|
|
633
|
+
\`\`\`text
|
|
634
|
+
${clip(options.sourceContext || "(no change artifacts found yet)", MAX_SOURCE_CONTEXT)}
|
|
635
|
+
\`\`\`
|
|
636
|
+
|
|
637
|
+
## GitNexus Query
|
|
638
|
+
|
|
639
|
+
- Query: ${options.query}
|
|
640
|
+
- Goal: ${options.goal}
|
|
641
|
+
|
|
642
|
+
\`\`\`text
|
|
643
|
+
${clip(options.graphOutput, MAX_GRAPH_OUTPUT)}
|
|
644
|
+
\`\`\`
|
|
645
|
+
${options.detectChanges ? `
|
|
646
|
+
## GitNexus Change Impact
|
|
647
|
+
|
|
648
|
+
\`\`\`text
|
|
649
|
+
${clip(options.detectChanges, MAX_GRAPH_OUTPUT)}
|
|
650
|
+
\`\`\`
|
|
651
|
+
` : ""}
|
|
652
|
+
${renderWarnings(options.warnings)}
|
|
653
|
+
`;
|
|
654
|
+
}
|
|
655
|
+
function renderWarnings(warnings) {
|
|
656
|
+
if (!warnings.length) {
|
|
657
|
+
return "";
|
|
658
|
+
}
|
|
659
|
+
return `
|
|
660
|
+
## Warnings
|
|
661
|
+
|
|
662
|
+
${warnings.map((warning) => `- ${warning}`).join("\n")}
|
|
663
|
+
`;
|
|
664
|
+
}
|
|
665
|
+
async function collectOpenSpecContext(projectRoot, changeId) {
|
|
666
|
+
if (!changeId) {
|
|
667
|
+
return "";
|
|
668
|
+
}
|
|
669
|
+
const changeRoot = join8(projectRoot, "openspec", "changes", changeId);
|
|
670
|
+
const chunks = [];
|
|
671
|
+
for (const file of ["proposal.md", "design.md", "tasks.md", "README.md"]) {
|
|
672
|
+
const content = await readOptional(join8(changeRoot, file));
|
|
673
|
+
if (content) {
|
|
674
|
+
chunks.push(`## ${file}
|
|
675
|
+
${content}`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
const specsRoot = join8(changeRoot, "specs");
|
|
679
|
+
for (const spec of await listSpecFiles(specsRoot)) {
|
|
680
|
+
const content = await readOptional(spec.path);
|
|
681
|
+
if (content) {
|
|
682
|
+
chunks.push(`## ${spec.label}
|
|
683
|
+
${content}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return clip(chunks.join("\n\n"), MAX_SOURCE_CONTEXT);
|
|
687
|
+
}
|
|
688
|
+
async function listSpecFiles(specsRoot) {
|
|
689
|
+
try {
|
|
690
|
+
const capabilities = await readdir(specsRoot, { withFileTypes: true });
|
|
691
|
+
return capabilities.filter((entry) => entry.isDirectory()).map((entry) => ({
|
|
692
|
+
path: join8(specsRoot, entry.name, "spec.md"),
|
|
693
|
+
label: `specs/${entry.name}/spec.md`
|
|
694
|
+
}));
|
|
695
|
+
} catch {
|
|
696
|
+
return [];
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
async function readOptional(path) {
|
|
700
|
+
try {
|
|
701
|
+
return await readFile5(path, "utf8");
|
|
702
|
+
} catch {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
function buildWorkflowQuery(options, sourceContext) {
|
|
707
|
+
const artifactTerms = normalizeWhitespace(sourceContext).slice(0, 1200);
|
|
708
|
+
return normalizeWhitespace(
|
|
709
|
+
[
|
|
710
|
+
`FET OpenSpec ${options.command}`,
|
|
711
|
+
options.changeId ? `change ${options.changeId}` : "",
|
|
712
|
+
options.args.join(" "),
|
|
713
|
+
artifactTerms
|
|
714
|
+
].join(" ")
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
function workflowGoal(command) {
|
|
718
|
+
if (command === "apply") {
|
|
719
|
+
return "Find implementation files, symbols, dependencies, and likely blast radius for the current OpenSpec tasks.";
|
|
720
|
+
}
|
|
721
|
+
if (command === "verify") {
|
|
722
|
+
return "Find affected flows and source areas that should be checked while verifying this OpenSpec change.";
|
|
723
|
+
}
|
|
724
|
+
if (["explore", "propose", "new", "continue", "ff"].includes(command)) {
|
|
725
|
+
return "Find relevant modules, entry points, and existing behavior to make OpenSpec artifacts precise.";
|
|
726
|
+
}
|
|
727
|
+
return "Find relevant repository context for this FET/OpenSpec workflow command.";
|
|
728
|
+
}
|
|
729
|
+
function shouldDetectChanges(command) {
|
|
730
|
+
return ["apply", "verify", "sync"].includes(command);
|
|
731
|
+
}
|
|
732
|
+
function commandText(result) {
|
|
733
|
+
const output = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
|
|
734
|
+
return output || `exit ${result.exitCode}`;
|
|
735
|
+
}
|
|
736
|
+
function commandWarnings(results) {
|
|
737
|
+
return results.filter(([, result]) => result.exitCode !== 0).map(([label, result]) => `${label} exited with ${result.exitCode}: ${firstLine(commandText(result))}`);
|
|
738
|
+
}
|
|
739
|
+
function firstLine(value) {
|
|
740
|
+
return value.trim().split(/\r?\n/)[0]?.trim() || "no output";
|
|
741
|
+
}
|
|
742
|
+
function clip(value, max) {
|
|
743
|
+
if (value.length <= max) {
|
|
744
|
+
return value;
|
|
745
|
+
}
|
|
746
|
+
return `${value.slice(0, max)}
|
|
747
|
+
|
|
748
|
+
[truncated ${value.length - max} characters]`;
|
|
749
|
+
}
|
|
750
|
+
function normalizeWhitespace(value) {
|
|
751
|
+
return value.replace(/\s+/g, " ").trim();
|
|
752
|
+
}
|
|
753
|
+
function sanitizePathPart(value) {
|
|
754
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "workflow";
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/commands/graph.ts
|
|
447
758
|
async function graphCommand(ctx, action, args = []) {
|
|
448
759
|
switch (action) {
|
|
449
760
|
case "status":
|
|
@@ -498,7 +809,7 @@ async function graphDoctorCommand(ctx) {
|
|
|
498
809
|
}
|
|
499
810
|
async function graphSetupCommand(ctx) {
|
|
500
811
|
let result;
|
|
501
|
-
const handoffPath =
|
|
812
|
+
const handoffPath = join9(ctx.projectRoot, ".fet", "graph-setup.md");
|
|
502
813
|
const installCommand = process.env.FET_GITNEXUS_INSTALL_COMMAND?.trim() || null;
|
|
503
814
|
await withProjectLock(ctx.projectRoot, { command: "graph setup", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
504
815
|
result = await refreshGraphState(ctx, { write: false });
|
|
@@ -534,7 +845,7 @@ async function graphSetupCommand(ctx) {
|
|
|
534
845
|
}
|
|
535
846
|
async function graphHandoffCommand(ctx) {
|
|
536
847
|
let result;
|
|
537
|
-
const handoffPath =
|
|
848
|
+
const handoffPath = join9(ctx.projectRoot, ".fet", "graph-handoff.md");
|
|
538
849
|
await withProjectLock(ctx.projectRoot, { command: "graph handoff", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
539
850
|
result = await refreshGraphState(ctx, { runStatus: true, write: false });
|
|
540
851
|
await writeHandoffFile(handoffPath, renderGraphUsageHandoff(result.state, ctx.language));
|
|
@@ -581,11 +892,14 @@ async function graphAnalyzeCommand(ctx, mode, args) {
|
|
|
581
892
|
});
|
|
582
893
|
}
|
|
583
894
|
const result = await refreshGraphState(ctx, { write: false });
|
|
895
|
+
const graphContext = await buildProjectGraphContext(ctx, result.state, `fet graph ${mode}`);
|
|
584
896
|
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
585
897
|
global.graph ??= {};
|
|
586
898
|
global.graph.gitnexus = {
|
|
587
899
|
...result.state,
|
|
588
|
-
lastRefreshAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
900
|
+
lastRefreshAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
901
|
+
projectContextPath: graphContext.path,
|
|
902
|
+
projectContextUpdatedAt: graphContext.generated ? (/* @__PURE__ */ new Date()).toISOString() : result.state.projectContextUpdatedAt ?? null
|
|
589
903
|
};
|
|
590
904
|
await ctx.stateStore.writeGlobal(global);
|
|
591
905
|
ctx.output.result({
|
|
@@ -595,9 +909,10 @@ async function graphAnalyzeCommand(ctx, mode, args) {
|
|
|
595
909
|
warnings: result.state.graphExists ? [] : [
|
|
596
910
|
ctx.language === "en" ? "GitNexus analyze completed, but the configured graph directory was not found." : "GitNexus analyze \u5DF2\u5B8C\u6210\uFF0C\u4F46\u672A\u53D1\u73B0\u914D\u7F6E\u7684\u4EE3\u7801\u56FE\u76EE\u5F55\u3002"
|
|
597
911
|
],
|
|
598
|
-
nextSteps: ctx.language === "en" ? ["Run fet graph status", "Use .fet/graph-handoff.md or generated IDE prompts to prefer graph context"] : ["\u8FD0\u884C fet graph status", "\u4F7F\u7528 .fet/graph-handoff.md \u6216\u751F\u6210\u7684 IDE \u63D0\u793A\uFF0C\u4F18\u5148\u53C2\u8003\u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587"],
|
|
912
|
+
nextSteps: ctx.language === "en" ? ["Run fet graph status", "Read .fet/graph-context/project.md before broad scans", "Use .fet/graph-handoff.md or generated IDE prompts to prefer graph context"] : ["\u8FD0\u884C fet graph status", "\u4F7F\u7528 .fet/graph-handoff.md \u6216\u751F\u6210\u7684 IDE \u63D0\u793A\uFF0C\u4F18\u5148\u53C2\u8003\u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587"],
|
|
599
913
|
data: {
|
|
600
914
|
gitnexus: global.graph.gitnexus,
|
|
915
|
+
graphContext,
|
|
601
916
|
run: {
|
|
602
917
|
command: run.command,
|
|
603
918
|
stdout: run.stdout.trim(),
|
|
@@ -617,7 +932,7 @@ async function refreshGraphState(ctx, options = {}) {
|
|
|
617
932
|
gitnexusStatus = await runGitNexus(["status"], { cwd: ctx.projectRoot });
|
|
618
933
|
state = {
|
|
619
934
|
...state,
|
|
620
|
-
lastStatus:
|
|
935
|
+
lastStatus: firstLine2(gitnexusStatus.stdout) || firstLine2(gitnexusStatus.stderr) || `exit ${gitnexusStatus.exitCode}`
|
|
621
936
|
};
|
|
622
937
|
}
|
|
623
938
|
if (options.write ?? true) {
|
|
@@ -635,7 +950,7 @@ async function refreshGraphState(ctx, options = {}) {
|
|
|
635
950
|
};
|
|
636
951
|
}
|
|
637
952
|
async function writeHandoffFile(path, content) {
|
|
638
|
-
await
|
|
953
|
+
await mkdir5(dirname6(path), { recursive: true });
|
|
639
954
|
await atomicWrite(path, content);
|
|
640
955
|
}
|
|
641
956
|
function renderGraphSetupHandoff(state, options) {
|
|
@@ -780,24 +1095,24 @@ FET:END -->
|
|
|
780
1095
|
- \u6240\u6709\u751F\u6210\u4EA7\u7269\u4ECD\u5199\u5165\u6B63\u5E38\u7684 OpenSpec change \u76EE\u5F55\u3002
|
|
781
1096
|
`;
|
|
782
1097
|
}
|
|
783
|
-
function
|
|
1098
|
+
function firstLine2(value) {
|
|
784
1099
|
return value.trim().split(/\r?\n/)[0]?.trim() || null;
|
|
785
1100
|
}
|
|
786
1101
|
|
|
787
1102
|
// src/commands/init.ts
|
|
788
|
-
import { readFile as
|
|
789
|
-
import { join as
|
|
1103
|
+
import { readFile as readFile8, stat as stat4 } from "fs/promises";
|
|
1104
|
+
import { join as join12 } from "path";
|
|
790
1105
|
|
|
791
1106
|
// src/version.ts
|
|
792
1107
|
import { existsSync, readFileSync } from "fs";
|
|
793
|
-
import { dirname as
|
|
1108
|
+
import { dirname as dirname7, join as join10, parse } from "path";
|
|
794
1109
|
import { fileURLToPath } from "url";
|
|
795
1110
|
var FET_VERSION = readPackageVersion();
|
|
796
1111
|
function readPackageVersion() {
|
|
797
|
-
let currentDir =
|
|
1112
|
+
let currentDir = dirname7(fileURLToPath(import.meta.url));
|
|
798
1113
|
const root = parse(currentDir).root;
|
|
799
1114
|
while (true) {
|
|
800
|
-
const packageJsonPath =
|
|
1115
|
+
const packageJsonPath = join10(currentDir, "package.json");
|
|
801
1116
|
if (existsSync(packageJsonPath)) {
|
|
802
1117
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
803
1118
|
if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
|
|
@@ -808,7 +1123,7 @@ function readPackageVersion() {
|
|
|
808
1123
|
if (currentDir === root) {
|
|
809
1124
|
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D FET package.json");
|
|
810
1125
|
}
|
|
811
|
-
currentDir =
|
|
1126
|
+
currentDir = dirname7(currentDir);
|
|
812
1127
|
}
|
|
813
1128
|
}
|
|
814
1129
|
|
|
@@ -1259,18 +1574,18 @@ ${block}
|
|
|
1259
1574
|
}
|
|
1260
1575
|
|
|
1261
1576
|
// src/commands/update-context.ts
|
|
1262
|
-
import { readFile as
|
|
1263
|
-
import { join as
|
|
1577
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1578
|
+
import { join as join11 } from "path";
|
|
1264
1579
|
|
|
1265
1580
|
// src/config/yaml.ts
|
|
1266
|
-
import { readFile as
|
|
1581
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1267
1582
|
import { parseDocument } from "yaml";
|
|
1268
1583
|
async function mergeFetConfig(configPath, renderedFetYaml) {
|
|
1269
1584
|
const fetDoc = parseDocument(renderedFetYaml);
|
|
1270
1585
|
const nextFet = fetDoc.get("fet", true);
|
|
1271
1586
|
let existing = "";
|
|
1272
1587
|
try {
|
|
1273
|
-
existing = await
|
|
1588
|
+
existing = await readFile6(configPath, "utf8");
|
|
1274
1589
|
} catch {
|
|
1275
1590
|
return renderedFetYaml;
|
|
1276
1591
|
}
|
|
@@ -1294,14 +1609,14 @@ async function updateContextCommand(ctx) {
|
|
|
1294
1609
|
}
|
|
1295
1610
|
async function updateContextFiles(ctx) {
|
|
1296
1611
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
1297
|
-
const agentsPath =
|
|
1298
|
-
const configPath =
|
|
1299
|
-
const claudePath =
|
|
1300
|
-
const karpathyHandoffPath =
|
|
1301
|
-
const karpathyCursorPath =
|
|
1302
|
-
const existingAgents = await
|
|
1303
|
-
const existingClaude = await
|
|
1304
|
-
const existingKarpathyCursor = await
|
|
1612
|
+
const agentsPath = join11(ctx.projectRoot, "AGENTS.md");
|
|
1613
|
+
const configPath = join11(ctx.projectRoot, "openspec", "config.yaml");
|
|
1614
|
+
const claudePath = join11(ctx.projectRoot, "CLAUDE.md");
|
|
1615
|
+
const karpathyHandoffPath = join11(ctx.projectRoot, ".fet", "karpathy-guidelines.md");
|
|
1616
|
+
const karpathyCursorPath = join11(ctx.projectRoot, ".cursor", "rules", "karpathy-guidelines.mdc");
|
|
1617
|
+
const existingAgents = await readOptional2(agentsPath);
|
|
1618
|
+
const existingClaude = await readOptional2(claudePath);
|
|
1619
|
+
const existingKarpathyCursor = await readOptional2(karpathyCursorPath);
|
|
1305
1620
|
const warnings = [...scan.warnings];
|
|
1306
1621
|
if (existingAgents && hasInvalidManagedAutoRegion(existingAgents)) {
|
|
1307
1622
|
throw new FetError({
|
|
@@ -1349,9 +1664,9 @@ async function updateContextFiles(ctx) {
|
|
|
1349
1664
|
await ctx.stateStore.writeGlobal(state);
|
|
1350
1665
|
return { warnings };
|
|
1351
1666
|
}
|
|
1352
|
-
async function
|
|
1667
|
+
async function readOptional2(path) {
|
|
1353
1668
|
try {
|
|
1354
|
-
return await
|
|
1669
|
+
return await readFile7(path, "utf8");
|
|
1355
1670
|
} catch {
|
|
1356
1671
|
return null;
|
|
1357
1672
|
}
|
|
@@ -1359,7 +1674,7 @@ async function readOptional(path) {
|
|
|
1359
1674
|
|
|
1360
1675
|
// src/commands/init.ts
|
|
1361
1676
|
async function initCommand(ctx) {
|
|
1362
|
-
const alreadyInitialized = await exists2(
|
|
1677
|
+
const alreadyInitialized = await exists2(join12(ctx.projectRoot, "openspec", "config.yaml"));
|
|
1363
1678
|
let warnings = [];
|
|
1364
1679
|
await withProjectLock(ctx.projectRoot, { command: "init", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
1365
1680
|
const journal = createInitJournal(ctx.fetVersion);
|
|
@@ -1411,13 +1726,13 @@ async function initCommand(ctx) {
|
|
|
1411
1726
|
});
|
|
1412
1727
|
}
|
|
1413
1728
|
async function ensureGitignore(ctx) {
|
|
1414
|
-
const gitignorePath =
|
|
1415
|
-
const existing = await
|
|
1729
|
+
const gitignorePath = join12(ctx.projectRoot, ".gitignore");
|
|
1730
|
+
const existing = await readOptional3(gitignorePath);
|
|
1416
1731
|
await atomicWrite(gitignorePath, mergeGitignore(existing));
|
|
1417
1732
|
}
|
|
1418
|
-
async function
|
|
1733
|
+
async function readOptional3(path) {
|
|
1419
1734
|
try {
|
|
1420
|
-
return await
|
|
1735
|
+
return await readFile8(path, "utf8");
|
|
1421
1736
|
} catch {
|
|
1422
1737
|
return null;
|
|
1423
1738
|
}
|
|
@@ -1432,8 +1747,8 @@ async function exists2(path) {
|
|
|
1432
1747
|
}
|
|
1433
1748
|
|
|
1434
1749
|
// src/commands/proxy.ts
|
|
1435
|
-
import { readFile as
|
|
1436
|
-
import { join as
|
|
1750
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
1751
|
+
import { join as join14 } from "path";
|
|
1437
1752
|
|
|
1438
1753
|
// src/state/project.ts
|
|
1439
1754
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -1462,8 +1777,8 @@ async function git(cwd, args) {
|
|
|
1462
1777
|
}
|
|
1463
1778
|
|
|
1464
1779
|
// src/state/store.ts
|
|
1465
|
-
import { mkdir as
|
|
1466
|
-
import { join as
|
|
1780
|
+
import { mkdir as mkdir6, readFile as readFile9 } from "fs/promises";
|
|
1781
|
+
import { join as join13 } from "path";
|
|
1467
1782
|
|
|
1468
1783
|
// src/language.ts
|
|
1469
1784
|
var DEFAULT_LANGUAGE = "zh-CN";
|
|
@@ -1581,7 +1896,7 @@ var StateStore = class {
|
|
|
1581
1896
|
project;
|
|
1582
1897
|
async readGlobal() {
|
|
1583
1898
|
try {
|
|
1584
|
-
const value = JSON.parse(await
|
|
1899
|
+
const value = JSON.parse(await readFile9(this.globalPath(), "utf8"));
|
|
1585
1900
|
assertGlobalState(value);
|
|
1586
1901
|
return value;
|
|
1587
1902
|
} catch (error) {
|
|
@@ -1596,13 +1911,13 @@ var StateStore = class {
|
|
|
1596
1911
|
}
|
|
1597
1912
|
async writeGlobal(state) {
|
|
1598
1913
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1599
|
-
await
|
|
1914
|
+
await mkdir6(join13(this.projectRoot, "openspec"), { recursive: true });
|
|
1600
1915
|
await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
|
|
1601
1916
|
`);
|
|
1602
1917
|
}
|
|
1603
1918
|
async readChange(changeId) {
|
|
1604
1919
|
try {
|
|
1605
|
-
const value = JSON.parse(await
|
|
1920
|
+
const value = JSON.parse(await readFile9(this.changePath(changeId), "utf8"));
|
|
1606
1921
|
assertChangeState(value);
|
|
1607
1922
|
return value;
|
|
1608
1923
|
} catch (error) {
|
|
@@ -1617,15 +1932,15 @@ var StateStore = class {
|
|
|
1617
1932
|
}
|
|
1618
1933
|
async writeChange(state) {
|
|
1619
1934
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1620
|
-
await
|
|
1935
|
+
await mkdir6(join13(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
|
|
1621
1936
|
await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
|
|
1622
1937
|
`);
|
|
1623
1938
|
}
|
|
1624
1939
|
globalPath() {
|
|
1625
|
-
return
|
|
1940
|
+
return join13(this.projectRoot, "openspec", "fet-state.json");
|
|
1626
1941
|
}
|
|
1627
1942
|
changePath(changeId) {
|
|
1628
|
-
return
|
|
1943
|
+
return join13(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
|
|
1629
1944
|
}
|
|
1630
1945
|
};
|
|
1631
1946
|
function isNotFound(error) {
|
|
@@ -1633,11 +1948,11 @@ function isNotFound(error) {
|
|
|
1633
1948
|
}
|
|
1634
1949
|
|
|
1635
1950
|
// src/state/tasks.ts
|
|
1636
|
-
import { readFile as
|
|
1951
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
1637
1952
|
async function readCompletedTaskIds(tasksPath) {
|
|
1638
1953
|
let content;
|
|
1639
1954
|
try {
|
|
1640
|
-
content = await
|
|
1955
|
+
content = await readFile10(tasksPath, "utf8");
|
|
1641
1956
|
} catch {
|
|
1642
1957
|
return [];
|
|
1643
1958
|
}
|
|
@@ -1668,71 +1983,76 @@ var phaseByCommand = {
|
|
|
1668
1983
|
};
|
|
1669
1984
|
async function proxyCommand(ctx, command, args) {
|
|
1670
1985
|
const openSpecArgs = stripFetOptions(args);
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
state.activeChangeId = ctx.changeId;
|
|
1704
|
-
} else if (state.activeChangeId && !inspection.changes.includes(state.activeChangeId)) {
|
|
1705
|
-
state.activeChangeId = inspection.changes.length === 1 ? inspection.changes[0] ?? null : null;
|
|
1706
|
-
} else if (!state.activeChangeId && inspection.changes.length === 1) {
|
|
1707
|
-
state.activeChangeId = inspection.changes[0] ?? null;
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
await ctx.stateStore.writeGlobal(state);
|
|
1711
|
-
const changeId = ctx.changeId ?? state.activeChangeId;
|
|
1712
|
-
if (changeId && inspection.changes.includes(changeId)) {
|
|
1713
|
-
const changeInspection = await ctx.openSpec.inspectChange(ctx.projectRoot, changeId);
|
|
1714
|
-
const changeState = await ctx.stateStore.getOrCreateChange(changeId, phaseByCommand[command] ?? "propose");
|
|
1715
|
-
changeState.currentPhase = phaseByCommand[command] ?? changeState.currentPhase;
|
|
1716
|
-
changeState.phases[changeState.currentPhase] = {
|
|
1717
|
-
status: "done",
|
|
1718
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1719
|
-
};
|
|
1720
|
-
changeState.lastOpenSpecCommand = {
|
|
1721
|
-
command: mapped.command,
|
|
1722
|
-
args: mapped.args,
|
|
1723
|
-
exitCode: result.exitCode,
|
|
1724
|
-
ranAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1725
|
-
};
|
|
1726
|
-
changeState.tasks.completedIds = await readCompletedTaskIds(changeInspection.tasksPath);
|
|
1727
|
-
changeState.tasks.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1728
|
-
await ctx.stateStore.writeChange(changeState);
|
|
1986
|
+
const runState = {};
|
|
1987
|
+
await withProjectLock(ctx.projectRoot, { command, cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
1988
|
+
if (["sync", "archive", "bulk-archive"].includes(command)) {
|
|
1989
|
+
await assertVerified(ctx);
|
|
1990
|
+
}
|
|
1991
|
+
const mapped = await mapOpenSpecCommand(ctx, command, openSpecArgs);
|
|
1992
|
+
const mappedChangeId = extractChangeId(mapped.args);
|
|
1993
|
+
const targetChangeId = command === "archive" ? mapped.args[0] ?? ctx.changeId ?? mappedChangeId : ctx.changeId ?? mappedChangeId;
|
|
1994
|
+
runState.graphContext = await buildWorkflowGraphContext(ctx, {
|
|
1995
|
+
command,
|
|
1996
|
+
args: mapped.args,
|
|
1997
|
+
changeId: targetChangeId
|
|
1998
|
+
});
|
|
1999
|
+
const changelogEntry = command === "archive" && targetChangeId ? await createChangelogEntry(ctx.projectRoot, targetChangeId) : null;
|
|
2000
|
+
const result = await ctx.openSpec.run(mapped.command, mapped.args, { cwd: ctx.projectRoot, stdio: ctx.json ? "pipe" : "inherit" });
|
|
2001
|
+
if (result.exitCode !== 0) {
|
|
2002
|
+
throw new FetError({
|
|
2003
|
+
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
2004
|
+
message: `OpenSpec ${command} failed.`,
|
|
2005
|
+
details: result,
|
|
2006
|
+
recoverable: true
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
if (changelogEntry) {
|
|
2010
|
+
await appendChangelog(ctx.projectRoot, changelogEntry);
|
|
2011
|
+
}
|
|
2012
|
+
const inspection = await ctx.openSpec.inspectProject(ctx.projectRoot);
|
|
2013
|
+
const state = await ctx.stateStore.getOrCreateGlobal();
|
|
2014
|
+
state.openChangeIds = inspection.changes;
|
|
2015
|
+
if (command === "archive") {
|
|
2016
|
+
if (!state.activeChangeId || state.activeChangeId === targetChangeId || !inspection.changes.includes(state.activeChangeId)) {
|
|
2017
|
+
state.activeChangeId = null;
|
|
1729
2018
|
}
|
|
2019
|
+
state.verifyAuthorization = null;
|
|
2020
|
+
} else if (ctx.changeId && inspection.changes.includes(ctx.changeId)) {
|
|
2021
|
+
state.activeChangeId = ctx.changeId;
|
|
2022
|
+
} else if (state.activeChangeId && !inspection.changes.includes(state.activeChangeId)) {
|
|
2023
|
+
state.activeChangeId = inspection.changes.length === 1 ? inspection.changes[0] ?? null : null;
|
|
2024
|
+
} else if (!state.activeChangeId && inspection.changes.length === 1) {
|
|
2025
|
+
state.activeChangeId = inspection.changes[0] ?? null;
|
|
1730
2026
|
}
|
|
1731
|
-
|
|
2027
|
+
await ctx.stateStore.writeGlobal(state);
|
|
2028
|
+
const changeId = ctx.changeId ?? state.activeChangeId;
|
|
2029
|
+
if (changeId && inspection.changes.includes(changeId)) {
|
|
2030
|
+
const changeInspection = await ctx.openSpec.inspectChange(ctx.projectRoot, changeId);
|
|
2031
|
+
const changeState = await ctx.stateStore.getOrCreateChange(changeId, phaseByCommand[command] ?? "propose");
|
|
2032
|
+
changeState.currentPhase = phaseByCommand[command] ?? changeState.currentPhase;
|
|
2033
|
+
changeState.phases[changeState.currentPhase] = {
|
|
2034
|
+
status: "done",
|
|
2035
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2036
|
+
};
|
|
2037
|
+
changeState.lastOpenSpecCommand = {
|
|
2038
|
+
command: mapped.command,
|
|
2039
|
+
args: mapped.args,
|
|
2040
|
+
exitCode: result.exitCode,
|
|
2041
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2042
|
+
};
|
|
2043
|
+
changeState.tasks.completedIds = await readCompletedTaskIds(changeInspection.tasksPath);
|
|
2044
|
+
changeState.tasks.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2045
|
+
await ctx.stateStore.writeChange(changeState);
|
|
2046
|
+
}
|
|
2047
|
+
});
|
|
2048
|
+
const graphContext = runState.graphContext;
|
|
1732
2049
|
ctx.output.result({
|
|
1733
2050
|
ok: true,
|
|
1734
2051
|
command,
|
|
1735
|
-
summary: `fet ${command}
|
|
2052
|
+
summary: `fet ${command} completed.`,
|
|
2053
|
+
warnings: graphContext?.warnings,
|
|
2054
|
+
nextSteps: graphContext?.generated && graphContext.path ? [`Read ${graphContext.path} before broad source scans or implementation decisions.`] : void 0,
|
|
2055
|
+
data: graphContext ? { graphContext } : void 0
|
|
1736
2056
|
});
|
|
1737
2057
|
}
|
|
1738
2058
|
async function createChangelogEntry(projectRoot, changeId) {
|
|
@@ -1742,10 +2062,12 @@ async function createChangelogEntry(projectRoot, changeId) {
|
|
|
1742
2062
|
};
|
|
1743
2063
|
}
|
|
1744
2064
|
async function appendChangelog(projectRoot, entry) {
|
|
1745
|
-
const changelogPath =
|
|
1746
|
-
const existing = await
|
|
2065
|
+
const changelogPath = join14(projectRoot, "CHANGELOG.md");
|
|
2066
|
+
const existing = await readOptional4(changelogPath);
|
|
2067
|
+
const legacyContentLabel = "\u66F4\u65B0\u5185\u5BB9";
|
|
1747
2068
|
const block = `updateTime: ${entry.updateTime}
|
|
1748
|
-
|
|
2069
|
+
changeRequirement:${entry.content}
|
|
2070
|
+
${legacyContentLabel}:${entry.content}
|
|
1749
2071
|
`;
|
|
1750
2072
|
const next = existing?.trimEnd() ? `${existing.trimEnd()}
|
|
1751
2073
|
|
|
@@ -1753,12 +2075,12 @@ ${block}` : block;
|
|
|
1753
2075
|
await atomicWrite(changelogPath, next);
|
|
1754
2076
|
}
|
|
1755
2077
|
async function readChangeRequirement(projectRoot, changeId) {
|
|
1756
|
-
const changeRoot =
|
|
1757
|
-
const proposal = await
|
|
2078
|
+
const changeRoot = join14(projectRoot, "openspec", "changes", changeId);
|
|
2079
|
+
const proposal = await readOptional4(join14(changeRoot, "proposal.md"));
|
|
1758
2080
|
if (proposal) {
|
|
1759
2081
|
return summarizeMarkdown(proposal);
|
|
1760
2082
|
}
|
|
1761
|
-
const readme = await
|
|
2083
|
+
const readme = await readOptional4(join14(changeRoot, "README.md"));
|
|
1762
2084
|
if (readme) {
|
|
1763
2085
|
return summarizeMarkdown(readme);
|
|
1764
2086
|
}
|
|
@@ -1766,11 +2088,11 @@ async function readChangeRequirement(projectRoot, changeId) {
|
|
|
1766
2088
|
}
|
|
1767
2089
|
function summarizeMarkdown(content) {
|
|
1768
2090
|
const normalized = content.replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("<!--") && !line.startsWith("---")).join(" ");
|
|
1769
|
-
return normalized || "
|
|
2091
|
+
return normalized || "No change requirement found.";
|
|
1770
2092
|
}
|
|
1771
|
-
async function
|
|
2093
|
+
async function readOptional4(path) {
|
|
1772
2094
|
try {
|
|
1773
|
-
return await
|
|
2095
|
+
return await readFile11(path, "utf8");
|
|
1774
2096
|
} catch {
|
|
1775
2097
|
return null;
|
|
1776
2098
|
}
|
|
@@ -1792,7 +2114,7 @@ async function passthroughCommand(ctx, command, args) {
|
|
|
1792
2114
|
if (result.exitCode !== 0) {
|
|
1793
2115
|
throw new FetError({
|
|
1794
2116
|
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
1795
|
-
message: `OpenSpec ${command}
|
|
2117
|
+
message: `OpenSpec ${command} failed.`,
|
|
1796
2118
|
details: result
|
|
1797
2119
|
});
|
|
1798
2120
|
}
|
|
@@ -1834,18 +2156,6 @@ async function mapOpenSpecCommand(ctx, command, args) {
|
|
|
1834
2156
|
return { command: "new", args: args[0] === "change" ? args : ["change", ...args] };
|
|
1835
2157
|
case "archive":
|
|
1836
2158
|
return { command: "archive", args: [ctx.changeId ?? args[0] ?? await requireChangeId(ctx), ...args.slice(ctx.changeId ? 0 : 1)] };
|
|
1837
|
-
/*
|
|
1838
|
-
case "bulk-archive":
|
|
1839
|
-
throw new FetError({
|
|
1840
|
-
code: ErrorCode.InvalidArguments,
|
|
1841
|
-
message: "OpenSpec 1.2.0 不提供 bulk-archive 顶层命令",
|
|
1842
|
-
suggestedCommand: "逐个执行 fet archive --change <change-id>"
|
|
1843
|
-
});
|
|
1844
|
-
case "explore":
|
|
1845
|
-
return { command: "explore", args: ctx.changeId ? ["--change", ctx.changeId, ...args] : args };
|
|
1846
|
-
case "onboard":
|
|
1847
|
-
return { command: "instructions", args: [] };
|
|
1848
|
-
*/
|
|
1849
2159
|
default:
|
|
1850
2160
|
return { command, args };
|
|
1851
2161
|
}
|
|
@@ -1865,6 +2175,18 @@ async function withDefaultChange(ctx, args, allowWithArgs = false) {
|
|
|
1865
2175
|
}
|
|
1866
2176
|
return ["--change", await requireChangeId(ctx), ...args];
|
|
1867
2177
|
}
|
|
2178
|
+
function extractChangeId(args) {
|
|
2179
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2180
|
+
const arg = args[index];
|
|
2181
|
+
if (arg === "--change") {
|
|
2182
|
+
return args[index + 1] ?? null;
|
|
2183
|
+
}
|
|
2184
|
+
if (arg?.startsWith("--change=")) {
|
|
2185
|
+
return arg.slice("--change=".length) || null;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
return null;
|
|
2189
|
+
}
|
|
1868
2190
|
async function requireChangeId(ctx) {
|
|
1869
2191
|
if (ctx.changeId) {
|
|
1870
2192
|
return ctx.changeId;
|
|
@@ -1879,9 +2201,9 @@ async function requireChangeId(ctx) {
|
|
|
1879
2201
|
}
|
|
1880
2202
|
throw new FetError({
|
|
1881
2203
|
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
1882
|
-
message: "
|
|
2204
|
+
message: "No unambiguous OpenSpec change id was found.",
|
|
1883
2205
|
details: { openChangeIds: inspection.changes },
|
|
1884
|
-
suggestedCommand: "
|
|
2206
|
+
suggestedCommand: "Pass --change <change-id>."
|
|
1885
2207
|
});
|
|
1886
2208
|
}
|
|
1887
2209
|
async function assertVerified(ctx) {
|
|
@@ -1890,7 +2212,7 @@ async function assertVerified(ctx) {
|
|
|
1890
2212
|
if (!changeId) {
|
|
1891
2213
|
throw new FetError({
|
|
1892
2214
|
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
1893
|
-
message: "
|
|
2215
|
+
message: "A change id is required before this command can check FET verification.",
|
|
1894
2216
|
suggestedCommand: "fet verify --done --change <change-id>"
|
|
1895
2217
|
});
|
|
1896
2218
|
}
|
|
@@ -1899,7 +2221,7 @@ async function assertVerified(ctx) {
|
|
|
1899
2221
|
if (!inspection.changes.includes(changeId)) {
|
|
1900
2222
|
throw new FetError({
|
|
1901
2223
|
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
1902
|
-
message: "
|
|
2224
|
+
message: "The selected change does not exist in openspec/changes.",
|
|
1903
2225
|
details: { changeId, openChangeIds: inspection.changes },
|
|
1904
2226
|
suggestedCommand: "fet doctor"
|
|
1905
2227
|
});
|
|
@@ -1907,7 +2229,7 @@ async function assertVerified(ctx) {
|
|
|
1907
2229
|
if (change?.manualVerify?.status !== "declared_done") {
|
|
1908
2230
|
throw new FetError({
|
|
1909
2231
|
code: "STATE_CORRUPTED" /* StateCorrupted */,
|
|
1910
|
-
message: "
|
|
2232
|
+
message: "This change has not been marked verified by FET.",
|
|
1911
2233
|
details: { changeId },
|
|
1912
2234
|
suggestedCommand: `fet verify --change ${changeId}`
|
|
1913
2235
|
});
|
|
@@ -1916,8 +2238,8 @@ async function assertVerified(ctx) {
|
|
|
1916
2238
|
|
|
1917
2239
|
// src/commands/verify.ts
|
|
1918
2240
|
import { createHash } from "crypto";
|
|
1919
|
-
import { mkdir as
|
|
1920
|
-
import { join as
|
|
2241
|
+
import { mkdir as mkdir7, readFile as readFile12, stat as stat5 } from "fs/promises";
|
|
2242
|
+
import { join as join15 } from "path";
|
|
1921
2243
|
async function verifyCommand(ctx, options) {
|
|
1922
2244
|
if (options.auto) {
|
|
1923
2245
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
@@ -1984,9 +2306,9 @@ async function verifyCommand(ctx, options) {
|
|
|
1984
2306
|
async function writeInstructions(ctx, changeId) {
|
|
1985
2307
|
await assertChangeExists(ctx, changeId);
|
|
1986
2308
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1987
|
-
const dir =
|
|
1988
|
-
const instructionsPath =
|
|
1989
|
-
await
|
|
2309
|
+
const dir = join15(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
2310
|
+
const instructionsPath = join15(dir, "verify-instructions.md");
|
|
2311
|
+
await mkdir7(dir, { recursive: true });
|
|
1990
2312
|
await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
|
|
1991
2313
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
1992
2314
|
state.currentPhase = "verify";
|
|
@@ -2002,7 +2324,7 @@ async function writeInstructions(ctx, changeId) {
|
|
|
2002
2324
|
async function markDone(ctx, changeId) {
|
|
2003
2325
|
await assertChangeExists(ctx, changeId);
|
|
2004
2326
|
const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2005
|
-
const instructionsPath =
|
|
2327
|
+
const instructionsPath = join15(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
|
|
2006
2328
|
const instructions = await readInstructions(instructionsPath, changeId);
|
|
2007
2329
|
const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
|
|
2008
2330
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
@@ -2038,7 +2360,7 @@ async function assertChangeExists(ctx, changeId) {
|
|
|
2038
2360
|
async function readInstructions(path, changeId) {
|
|
2039
2361
|
try {
|
|
2040
2362
|
await stat5(path);
|
|
2041
|
-
const content = await
|
|
2363
|
+
const content = await readFile12(path, "utf8");
|
|
2042
2364
|
const fileChangeId = readFrontMatterValue(content, "changeId");
|
|
2043
2365
|
if (fileChangeId !== changeId) {
|
|
2044
2366
|
throw new FetError({
|
|
@@ -2176,9 +2498,9 @@ function renderIdeModelPolicy(command, language = "zh-CN") {
|
|
|
2176
2498
|
import { resolve } from "path";
|
|
2177
2499
|
|
|
2178
2500
|
// src/adapters/codex/index.ts
|
|
2179
|
-
import { mkdir as
|
|
2501
|
+
import { mkdir as mkdir8, readFile as readFile13, stat as stat6 } from "fs/promises";
|
|
2180
2502
|
import { homedir } from "os";
|
|
2181
|
-
import { dirname as
|
|
2503
|
+
import { dirname as dirname8, join as join16 } from "path";
|
|
2182
2504
|
|
|
2183
2505
|
// src/adapters/commands.ts
|
|
2184
2506
|
var FET_WORKFLOW_COMMANDS = [
|
|
@@ -3165,7 +3487,7 @@ var CodexAdapter = class {
|
|
|
3165
3487
|
adapterVersion = 1;
|
|
3166
3488
|
async detect(projectRoot) {
|
|
3167
3489
|
return {
|
|
3168
|
-
detected: await exists3(
|
|
3490
|
+
detected: await exists3(join16(projectRoot, ".codex")) || await exists3(join16(projectRoot, "AGENTS.md")),
|
|
3169
3491
|
reason: "Codex adapter is available for projects that use AGENTS.md"
|
|
3170
3492
|
};
|
|
3171
3493
|
}
|
|
@@ -3204,7 +3526,7 @@ var CodexAdapter = class {
|
|
|
3204
3526
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
3205
3527
|
await createBackup(target);
|
|
3206
3528
|
}
|
|
3207
|
-
await
|
|
3529
|
+
await mkdir8(dirname8(target), { recursive: true });
|
|
3208
3530
|
await atomicWrite(target, file.content);
|
|
3209
3531
|
written.push(displayPath);
|
|
3210
3532
|
}
|
|
@@ -3231,9 +3553,9 @@ var CodexAdapter = class {
|
|
|
3231
3553
|
};
|
|
3232
3554
|
function resolveTarget(projectRoot, file) {
|
|
3233
3555
|
if (file.root === "codex-home") {
|
|
3234
|
-
return
|
|
3556
|
+
return join16(resolveCodexHome(), file.path);
|
|
3235
3557
|
}
|
|
3236
|
-
return
|
|
3558
|
+
return join16(projectRoot, file.path);
|
|
3237
3559
|
}
|
|
3238
3560
|
function displayPathFor(file) {
|
|
3239
3561
|
if (file.root === "codex-home") {
|
|
@@ -3242,11 +3564,11 @@ function displayPathFor(file) {
|
|
|
3242
3564
|
return file.path;
|
|
3243
3565
|
}
|
|
3244
3566
|
function resolveCodexHome() {
|
|
3245
|
-
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ??
|
|
3567
|
+
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join16(homedir(), ".codex");
|
|
3246
3568
|
}
|
|
3247
3569
|
async function readExisting(path) {
|
|
3248
3570
|
try {
|
|
3249
|
-
return await
|
|
3571
|
+
return await readFile13(path, "utf8");
|
|
3250
3572
|
} catch {
|
|
3251
3573
|
return null;
|
|
3252
3574
|
}
|
|
@@ -3261,8 +3583,8 @@ async function exists3(path) {
|
|
|
3261
3583
|
}
|
|
3262
3584
|
|
|
3263
3585
|
// src/adapters/cursor/index.ts
|
|
3264
|
-
import { mkdir as
|
|
3265
|
-
import { dirname as
|
|
3586
|
+
import { mkdir as mkdir9, readFile as readFile14, stat as stat7 } from "fs/promises";
|
|
3587
|
+
import { dirname as dirname9, join as join17 } from "path";
|
|
3266
3588
|
|
|
3267
3589
|
// src/adapters/cursor/templates.ts
|
|
3268
3590
|
function cursorSkillFiles(language = DEFAULT_LANGUAGE) {
|
|
@@ -3396,7 +3718,7 @@ var CursorAdapter = class {
|
|
|
3396
3718
|
adapterVersion = 1;
|
|
3397
3719
|
async detect(projectRoot) {
|
|
3398
3720
|
return {
|
|
3399
|
-
detected: await exists4(
|
|
3721
|
+
detected: await exists4(join17(projectRoot, ".cursor")),
|
|
3400
3722
|
reason: "Cursor adapter is available for any project"
|
|
3401
3723
|
};
|
|
3402
3724
|
}
|
|
@@ -3413,7 +3735,7 @@ var CursorAdapter = class {
|
|
|
3413
3735
|
const written = [];
|
|
3414
3736
|
const skipped = [];
|
|
3415
3737
|
for (const file of plan.files) {
|
|
3416
|
-
const target =
|
|
3738
|
+
const target = join17(projectRoot, file.path);
|
|
3417
3739
|
const existing = await readExisting2(target);
|
|
3418
3740
|
if (existing && !existing.includes("FET:MANAGED") && !force) {
|
|
3419
3741
|
throw new FetError({
|
|
@@ -3426,7 +3748,7 @@ var CursorAdapter = class {
|
|
|
3426
3748
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
3427
3749
|
await createBackup(target);
|
|
3428
3750
|
}
|
|
3429
|
-
await
|
|
3751
|
+
await mkdir9(dirname9(target), { recursive: true });
|
|
3430
3752
|
await atomicWrite(target, file.content);
|
|
3431
3753
|
written.push(file.path);
|
|
3432
3754
|
}
|
|
@@ -3436,7 +3758,7 @@ var CursorAdapter = class {
|
|
|
3436
3758
|
const plan = await this.planInstall(projectRoot);
|
|
3437
3759
|
const checks = [];
|
|
3438
3760
|
for (const file of plan.files) {
|
|
3439
|
-
const target =
|
|
3761
|
+
const target = join17(projectRoot, file.path);
|
|
3440
3762
|
const content = await readExisting2(target);
|
|
3441
3763
|
const managed = Boolean(content?.includes("FET:MANAGED"));
|
|
3442
3764
|
const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
|
|
@@ -3452,7 +3774,7 @@ var CursorAdapter = class {
|
|
|
3452
3774
|
};
|
|
3453
3775
|
async function readExisting2(path) {
|
|
3454
3776
|
try {
|
|
3455
|
-
return await
|
|
3777
|
+
return await readFile14(path, "utf8");
|
|
3456
3778
|
} catch {
|
|
3457
3779
|
return null;
|
|
3458
3780
|
}
|
|
@@ -3471,13 +3793,13 @@ import { execFile as execFile4 } from "child_process";
|
|
|
3471
3793
|
import { promisify as promisify4 } from "util";
|
|
3472
3794
|
|
|
3473
3795
|
// src/openspec/inspector.ts
|
|
3474
|
-
import { readdir, stat as stat8 } from "fs/promises";
|
|
3475
|
-
import { join as
|
|
3796
|
+
import { readdir as readdir2, stat as stat8 } from "fs/promises";
|
|
3797
|
+
import { join as join18 } from "path";
|
|
3476
3798
|
async function inspectOpenSpecProject(projectRoot) {
|
|
3477
|
-
const openspecPath =
|
|
3478
|
-
const changesPath =
|
|
3479
|
-
const legacyArchivePath =
|
|
3480
|
-
const changesArchivePath =
|
|
3799
|
+
const openspecPath = join18(projectRoot, "openspec");
|
|
3800
|
+
const changesPath = join18(openspecPath, "changes");
|
|
3801
|
+
const legacyArchivePath = join18(openspecPath, "archive");
|
|
3802
|
+
const changesArchivePath = join18(changesPath, "archive");
|
|
3481
3803
|
return {
|
|
3482
3804
|
exists: await exists5(openspecPath),
|
|
3483
3805
|
changes: await listDirectories(changesPath, { exclude: ["archive"] }),
|
|
@@ -3485,13 +3807,13 @@ async function inspectOpenSpecProject(projectRoot) {
|
|
|
3485
3807
|
};
|
|
3486
3808
|
}
|
|
3487
3809
|
async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
3488
|
-
const changePath =
|
|
3489
|
-
const tasksPath =
|
|
3490
|
-
const specsPath =
|
|
3810
|
+
const changePath = join18(projectRoot, "openspec", "changes", changeId);
|
|
3811
|
+
const tasksPath = join18(changePath, "tasks.md");
|
|
3812
|
+
const specsPath = join18(changePath, "specs");
|
|
3491
3813
|
return {
|
|
3492
3814
|
changeId,
|
|
3493
3815
|
exists: await exists5(changePath),
|
|
3494
|
-
hasProposal: await exists5(
|
|
3816
|
+
hasProposal: await exists5(join18(changePath, "proposal.md")),
|
|
3495
3817
|
hasTasks: await exists5(tasksPath),
|
|
3496
3818
|
hasSpecs: await exists5(specsPath),
|
|
3497
3819
|
tasksPath,
|
|
@@ -3500,7 +3822,7 @@ async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
|
3500
3822
|
}
|
|
3501
3823
|
async function listDirectories(path, options = {}) {
|
|
3502
3824
|
try {
|
|
3503
|
-
const entries = await
|
|
3825
|
+
const entries = await readdir2(path, { withFileTypes: true });
|
|
3504
3826
|
const excluded = new Set(options.exclude ?? []);
|
|
3505
3827
|
return entries.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).map((entry) => entry.name);
|
|
3506
3828
|
} catch {
|
|
@@ -3668,12 +3990,12 @@ function parseCommands(help) {
|
|
|
3668
3990
|
}
|
|
3669
3991
|
|
|
3670
3992
|
// src/scanner/package.ts
|
|
3671
|
-
import { readFile as
|
|
3672
|
-
import { join as
|
|
3993
|
+
import { readFile as readFile15, stat as stat9 } from "fs/promises";
|
|
3994
|
+
import { join as join19 } from "path";
|
|
3673
3995
|
import { parse as parse2 } from "yaml";
|
|
3674
3996
|
async function readPackageJson(projectRoot) {
|
|
3675
3997
|
try {
|
|
3676
|
-
return JSON.parse(await
|
|
3998
|
+
return JSON.parse(await readFile15(join19(projectRoot, "package.json"), "utf8"));
|
|
3677
3999
|
} catch {
|
|
3678
4000
|
return null;
|
|
3679
4001
|
}
|
|
@@ -3739,7 +4061,7 @@ function detectFramework(pkg) {
|
|
|
3739
4061
|
}
|
|
3740
4062
|
async function detectLanguage(projectRoot, pkg) {
|
|
3741
4063
|
const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
|
|
3742
|
-
if (deps.typescript || await exists6(
|
|
4064
|
+
if (deps.typescript || await exists6(join19(projectRoot, "tsconfig.json"))) {
|
|
3743
4065
|
return "typescript";
|
|
3744
4066
|
}
|
|
3745
4067
|
return "javascript";
|
|
@@ -3754,7 +4076,7 @@ async function detectWorkspaces(projectRoot, pkg) {
|
|
|
3754
4076
|
return packageWorkspaces;
|
|
3755
4077
|
}
|
|
3756
4078
|
try {
|
|
3757
|
-
const workspace = parse2(await
|
|
4079
|
+
const workspace = parse2(await readFile15(join19(projectRoot, "pnpm-workspace.yaml"), "utf8"));
|
|
3758
4080
|
return (workspace?.packages ?? []).map((path) => ({
|
|
3759
4081
|
name: path,
|
|
3760
4082
|
path,
|
|
@@ -3774,7 +4096,7 @@ async function detectLockManagers(projectRoot) {
|
|
|
3774
4096
|
];
|
|
3775
4097
|
const found = [];
|
|
3776
4098
|
for (const [file, manager] of lockFiles) {
|
|
3777
|
-
if (await exists6(
|
|
4099
|
+
if (await exists6(join19(projectRoot, file))) {
|
|
3778
4100
|
found.push(manager);
|
|
3779
4101
|
}
|
|
3780
4102
|
}
|
|
@@ -3799,13 +4121,13 @@ async function exists6(path) {
|
|
|
3799
4121
|
}
|
|
3800
4122
|
|
|
3801
4123
|
// src/scanner/routes.ts
|
|
3802
|
-
import { readdir as
|
|
3803
|
-
import { join as
|
|
4124
|
+
import { readdir as readdir3, stat as stat10 } from "fs/promises";
|
|
4125
|
+
import { join as join20, relative, sep } from "path";
|
|
3804
4126
|
async function scanRoutes(projectRoot) {
|
|
3805
4127
|
const candidates = ["src/routes", "src/pages", "app", "pages"];
|
|
3806
4128
|
const routes = [];
|
|
3807
4129
|
for (const candidate of candidates) {
|
|
3808
|
-
const root =
|
|
4130
|
+
const root = join20(projectRoot, candidate);
|
|
3809
4131
|
if (!await exists7(root)) {
|
|
3810
4132
|
continue;
|
|
3811
4133
|
}
|
|
@@ -3830,10 +4152,10 @@ function inferRoutePath(relativePath) {
|
|
|
3830
4152
|
return `/${withoutIndex}`.replace(/\/+/g, "/");
|
|
3831
4153
|
}
|
|
3832
4154
|
async function listFiles(root) {
|
|
3833
|
-
const entries = await
|
|
4155
|
+
const entries = await readdir3(root, { withFileTypes: true });
|
|
3834
4156
|
const files = [];
|
|
3835
4157
|
for (const entry of entries) {
|
|
3836
|
-
const path =
|
|
4158
|
+
const path = join20(root, entry.name);
|
|
3837
4159
|
if (entry.isDirectory()) {
|
|
3838
4160
|
files.push(...await listFiles(path));
|
|
3839
4161
|
} else {
|