@nick848/fet 1.0.7 → 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 +840 -248
- 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);
|
|
@@ -262,7 +268,7 @@ async function doctorCommand(ctx, options = {}) {
|
|
|
262
268
|
checks.push(await checkState(ctx));
|
|
263
269
|
checks.push(await checkFile("agents", join6(ctx.projectRoot, "AGENTS.md"), "AGENTS.md \u7F3A\u5931", "fet update-context"));
|
|
264
270
|
checks.push(await checkFile("config", join6(ctx.projectRoot, "openspec", "config.yaml"), "openspec/config.yaml \u7F3A\u5931", "fet init"));
|
|
265
|
-
checks.push(await checkPlaceholders(ctx
|
|
271
|
+
checks.push(await checkPlaceholders(ctx));
|
|
266
272
|
checks.push(await checkGitNexus(ctx));
|
|
267
273
|
for (const adapter of ctx.toolAdapters) {
|
|
268
274
|
checks.push(...await adapter.doctor(ctx.projectRoot));
|
|
@@ -299,12 +305,12 @@ async function checkGitNexus(ctx) {
|
|
|
299
305
|
return state.installed ? {
|
|
300
306
|
id: "gitnexus",
|
|
301
307
|
status: "pass",
|
|
302
|
-
message: `GitNexus detected: ${state.executablePath ?? "gitnexus"} (${state.version ?? "unknown"}), graph ${state.graphExists ? "found" : "not found"}`
|
|
308
|
+
message: ctx.language === "en" ? `GitNexus detected: ${state.executablePath ?? "gitnexus"} (${state.version ?? "unknown"}), graph ${state.graphExists ? "found" : "not found"}` : `\u68C0\u6D4B\u5230 GitNexus\uFF1A${state.executablePath ?? "gitnexus"}\uFF08${state.version ?? "unknown"}\uFF09\uFF0C\u4EE3\u7801\u56FE${state.graphExists ? "\u5DF2\u627E\u5230" : "\u672A\u627E\u5230"}`
|
|
303
309
|
} : {
|
|
304
310
|
id: "gitnexus",
|
|
305
311
|
status: "warn",
|
|
306
|
-
message: "Optional GitNexus code graph support is not installed",
|
|
307
|
-
suggestedCommand: "Install GitNexus later if you want OpenSpec artifacts to prefer a repository graph"
|
|
312
|
+
message: ctx.language === "en" ? "Optional GitNexus code graph support is not installed" : "\u5C1A\u672A\u5B89\u88C5\u53EF\u9009\u7684 GitNexus \u4EE3\u7801\u56FE\u652F\u6301",
|
|
313
|
+
suggestedCommand: ctx.language === "en" ? "Install GitNexus later if you want OpenSpec artifacts to prefer a repository graph" : "\u5982\u679C\u5E0C\u671B OpenSpec \u4EA7\u7269\u4F18\u5148\u53C2\u8003\u4ED3\u5E93\u4EE3\u7801\u56FE\uFF0C\u53EF\u4EE5\u7A0D\u540E\u5B89\u88C5 GitNexus"
|
|
308
314
|
};
|
|
309
315
|
}
|
|
310
316
|
async function checkOpenSpec(ctx) {
|
|
@@ -326,18 +332,27 @@ async function checkState(ctx) {
|
|
|
326
332
|
async function checkFile(id, path, missing, suggestedCommand) {
|
|
327
333
|
return await exists(path) ? { id, status: "pass", message: `${id} \u5B58\u5728` } : { id, status: "warn", message: missing, suggestedCommand };
|
|
328
334
|
}
|
|
329
|
-
async function checkPlaceholders(
|
|
335
|
+
async function checkPlaceholders(ctx) {
|
|
330
336
|
try {
|
|
331
|
-
await readFile4(join6(projectRoot, "AGENTS.md"), "utf8");
|
|
332
|
-
const count2 = await countAgentsLlmPlaceholders(projectRoot);
|
|
337
|
+
await readFile4(join6(ctx.projectRoot, "AGENTS.md"), "utf8");
|
|
338
|
+
const count2 = await countAgentsLlmPlaceholders(ctx.projectRoot);
|
|
333
339
|
return count2 ? {
|
|
334
340
|
id: "context-placeholders",
|
|
335
341
|
status: "warn",
|
|
336
|
-
message: `AGENTS.md has ${count2} LLM placeholder(s)`,
|
|
342
|
+
message: ctx.language === "en" ? `AGENTS.md has ${count2} LLM placeholder(s)` : `AGENTS.md \u4ECD\u6709 ${count2} \u4E2A LLM \u5360\u4F4D\u7B26`,
|
|
337
343
|
suggestedCommand: "fet fill-context"
|
|
338
|
-
} : {
|
|
344
|
+
} : {
|
|
345
|
+
id: "context-placeholders",
|
|
346
|
+
status: "pass",
|
|
347
|
+
message: ctx.language === "en" ? "AGENTS.md placeholders resolved" : "AGENTS.md \u5360\u4F4D\u7B26\u5DF2\u5904\u7406"
|
|
348
|
+
};
|
|
339
349
|
} catch {
|
|
340
|
-
return {
|
|
350
|
+
return {
|
|
351
|
+
id: "context-placeholders",
|
|
352
|
+
status: "warn",
|
|
353
|
+
message: ctx.language === "en" ? "AGENTS.md missing" : "AGENTS.md \u7F3A\u5931",
|
|
354
|
+
suggestedCommand: "fet update-context"
|
|
355
|
+
};
|
|
341
356
|
}
|
|
342
357
|
}
|
|
343
358
|
async function exists(path) {
|
|
@@ -433,8 +448,313 @@ FET:END -->
|
|
|
433
448
|
}
|
|
434
449
|
|
|
435
450
|
// src/commands/graph.ts
|
|
436
|
-
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";
|
|
437
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
|
|
438
758
|
async function graphCommand(ctx, action, args = []) {
|
|
439
759
|
switch (action) {
|
|
440
760
|
case "status":
|
|
@@ -459,38 +779,41 @@ async function graphCommand(ctx, action, args = []) {
|
|
|
459
779
|
}
|
|
460
780
|
async function graphStatusCommand(ctx) {
|
|
461
781
|
const result = await refreshGraphState(ctx, { runStatus: true });
|
|
462
|
-
const warnings = result.state.installed ? [] : [
|
|
782
|
+
const warnings = result.state.installed ? [] : [
|
|
783
|
+
ctx.language === "en" ? "GitNexus is not installed. Run fet graph setup for installation handoff instructions." : "\u5C1A\u672A\u5B89\u88C5 GitNexus\u3002\u8FD0\u884C fet graph setup \u83B7\u53D6\u5B89\u88C5\u4EA4\u63A5\u8BF4\u660E\u3002"
|
|
784
|
+
];
|
|
463
785
|
ctx.output.result({
|
|
464
786
|
ok: true,
|
|
465
787
|
command: "graph status",
|
|
466
|
-
summary: result.state.installed ? `GitNexus graph status checked. Graph ${result.state.graphExists ? "exists" : "does not exist"} at ${result.state.graphPath ?? ".gitnexus"}.` : "GitNexus is not installed. Graph support remains optional.",
|
|
788
|
+
summary: ctx.language === "en" ? result.state.installed ? `GitNexus graph status checked. Graph ${result.state.graphExists ? "exists" : "does not exist"} at ${result.state.graphPath ?? ".gitnexus"}.` : "GitNexus is not installed. Graph support remains optional." : result.state.installed ? `\u5DF2\u68C0\u67E5 GitNexus \u4EE3\u7801\u56FE\u72B6\u6001\u3002\u4EE3\u7801\u56FE${result.state.graphExists ? "\u5B58\u5728" : "\u4E0D\u5B58\u5728"}\uFF0C\u8DEF\u5F84\u4E3A ${result.state.graphPath ?? ".gitnexus"}\u3002` : "\u5C1A\u672A\u5B89\u88C5 GitNexus\u3002\u4EE3\u7801\u56FE\u80FD\u529B\u4FDD\u6301\u53EF\u9009\u3002",
|
|
467
789
|
warnings,
|
|
468
|
-
nextSteps: result.state.installed && !result.state.graphExists ? ["Run fet graph init to build the first GitNexus graph"] : void 0,
|
|
790
|
+
nextSteps: result.state.installed && !result.state.graphExists ? [ctx.language === "en" ? "Run fet graph init to build the first GitNexus graph" : "\u8FD0\u884C fet graph init \u751F\u6210\u7B2C\u4E00\u4EFD GitNexus \u4EE3\u7801\u56FE"] : void 0,
|
|
469
791
|
data: result
|
|
470
792
|
});
|
|
471
793
|
}
|
|
472
794
|
async function graphDoctorCommand(ctx) {
|
|
473
795
|
const result = await refreshGraphState(ctx, { runStatus: true });
|
|
474
796
|
const warnings = [
|
|
475
|
-
...!result.state.installed ? ["GitNexus is not installed."] : [],
|
|
476
|
-
...result.state.installed && !result.state.graphExists ? ["GitNexus is installed but no graph directory was found."] : [],
|
|
477
|
-
...!result.state.handoffPath ? ["Graph handoff instructions have not been generated."] : []
|
|
797
|
+
...!result.state.installed ? [ctx.language === "en" ? "GitNexus is not installed." : "\u5C1A\u672A\u5B89\u88C5 GitNexus\u3002"] : [],
|
|
798
|
+
...result.state.installed && !result.state.graphExists ? [ctx.language === "en" ? "GitNexus is installed but no graph directory was found." : "\u5DF2\u5B89\u88C5 GitNexus\uFF0C\u4F46\u672A\u53D1\u73B0\u4EE3\u7801\u56FE\u76EE\u5F55\u3002"] : [],
|
|
799
|
+
...!result.state.handoffPath ? [ctx.language === "en" ? "Graph handoff instructions have not been generated." : "\u5C1A\u672A\u751F\u6210\u4EE3\u7801\u56FE\u4F7F\u7528\u4EA4\u63A5\u8BF4\u660E\u3002"] : []
|
|
478
800
|
];
|
|
479
801
|
ctx.output.result({
|
|
480
802
|
ok: true,
|
|
481
803
|
command: "graph doctor",
|
|
482
|
-
summary: warnings.length ? `Graph doctor completed with ${warnings.length} warning(s).` : "Graph doctor completed without warnings.",
|
|
804
|
+
summary: ctx.language === "en" ? warnings.length ? `Graph doctor completed with ${warnings.length} warning(s).` : "Graph doctor completed without warnings." : warnings.length ? `\u4EE3\u7801\u56FE\u8BCA\u65AD\u5B8C\u6210\uFF0C\u53D1\u73B0 ${warnings.length} \u4E2A\u8B66\u544A\u3002` : "\u4EE3\u7801\u56FE\u8BCA\u65AD\u5B8C\u6210\uFF0C\u672A\u53D1\u73B0\u8B66\u544A\u3002",
|
|
483
805
|
warnings,
|
|
484
|
-
nextSteps: warnings.length ? ["Run fet graph setup", "Run fet graph handoff", "Run fet graph init when GitNexus is installed"] : void 0,
|
|
806
|
+
nextSteps: warnings.length ? ctx.language === "en" ? ["Run fet graph setup", "Run fet graph handoff", "Run fet graph init when GitNexus is installed"] : ["\u8FD0\u884C fet graph setup", "\u8FD0\u884C fet graph handoff", "\u5B89\u88C5 GitNexus \u540E\u8FD0\u884C fet graph init"] : void 0,
|
|
485
807
|
data: result
|
|
486
808
|
});
|
|
487
809
|
}
|
|
488
810
|
async function graphSetupCommand(ctx) {
|
|
489
811
|
let result;
|
|
490
|
-
const handoffPath =
|
|
812
|
+
const handoffPath = join9(ctx.projectRoot, ".fet", "graph-setup.md");
|
|
813
|
+
const installCommand = process.env.FET_GITNEXUS_INSTALL_COMMAND?.trim() || null;
|
|
491
814
|
await withProjectLock(ctx.projectRoot, { command: "graph setup", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
492
815
|
result = await refreshGraphState(ctx, { write: false });
|
|
493
|
-
await writeHandoffFile(handoffPath, renderGraphSetupHandoff(result.state));
|
|
816
|
+
await writeHandoffFile(handoffPath, renderGraphSetupHandoff(result.state, { installCommand, language: ctx.language }));
|
|
494
817
|
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
495
818
|
global.graph ??= {};
|
|
496
819
|
global.graph.gitnexus = {
|
|
@@ -503,21 +826,29 @@ async function graphSetupCommand(ctx) {
|
|
|
503
826
|
ctx.output.result({
|
|
504
827
|
ok: true,
|
|
505
828
|
command: "graph setup",
|
|
506
|
-
summary: "GitNexus setup
|
|
507
|
-
warnings: result.state.installed ? [] : [
|
|
508
|
-
|
|
829
|
+
summary: ctx.language === "en" ? "GitNexus IDE-assisted setup playbook generated." : "\u5DF2\u751F\u6210 GitNexus IDE LLM \u8F85\u52A9\u5B89\u88C5\u4EFB\u52A1\u4E66\u3002",
|
|
830
|
+
warnings: result.state.installed ? [] : [
|
|
831
|
+
ctx.language === "en" ? "GitNexus is not installed. The playbook lets the current IDE LLM guide installation with user confirmation before risky commands." : "\u5C1A\u672A\u5B89\u88C5 GitNexus\u3002\u4EFB\u52A1\u4E66\u4F1A\u6307\u5BFC\u5F53\u524D IDE LLM \u63A8\u8FDB\u5B89\u88C5\uFF0C\u5E76\u5728\u9AD8\u98CE\u9669\u547D\u4EE4\u524D\u8BF7\u6C42\u7528\u6237\u786E\u8BA4\u3002"
|
|
832
|
+
],
|
|
833
|
+
nextSteps: result.state.installed ? [
|
|
834
|
+
ctx.language === "en" ? "Ask your IDE AI to read .fet/graph-setup.md and run the optional gitnexus setup flow if you want IDE/MCP integration" : "\u8BA9\u5F53\u524D IDE AI \u9605\u8BFB .fet/graph-setup.md\uFF0C\u5E76\u5728\u9700\u8981 IDE/MCP \u96C6\u6210\u65F6\u6309\u786E\u8BA4\u6D41\u7A0B\u8FD0\u884C gitnexus setup",
|
|
835
|
+
"fet graph init"
|
|
836
|
+
] : [
|
|
837
|
+
ctx.language === "en" ? "Ask your current IDE AI to read .fet/graph-setup.md and follow the installation playbook" : "\u8BA9\u5F53\u524D IDE AI \u9605\u8BFB .fet/graph-setup.md\uFF0C\u5E76\u6309\u5B89\u88C5\u4EFB\u52A1\u4E66\u63A8\u8FDB"
|
|
838
|
+
],
|
|
509
839
|
data: {
|
|
510
840
|
path: ".fet/graph-setup.md",
|
|
841
|
+
installCommandConfigured: Boolean(installCommand),
|
|
511
842
|
gitnexus: result.state
|
|
512
843
|
}
|
|
513
844
|
});
|
|
514
845
|
}
|
|
515
846
|
async function graphHandoffCommand(ctx) {
|
|
516
847
|
let result;
|
|
517
|
-
const handoffPath =
|
|
848
|
+
const handoffPath = join9(ctx.projectRoot, ".fet", "graph-handoff.md");
|
|
518
849
|
await withProjectLock(ctx.projectRoot, { command: "graph handoff", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
519
850
|
result = await refreshGraphState(ctx, { runStatus: true, write: false });
|
|
520
|
-
await writeHandoffFile(handoffPath, renderGraphUsageHandoff(result.state));
|
|
851
|
+
await writeHandoffFile(handoffPath, renderGraphUsageHandoff(result.state, ctx.language));
|
|
521
852
|
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
522
853
|
global.graph ??= {};
|
|
523
854
|
global.graph.gitnexus = {
|
|
@@ -530,9 +861,11 @@ async function graphHandoffCommand(ctx) {
|
|
|
530
861
|
ctx.output.result({
|
|
531
862
|
ok: true,
|
|
532
863
|
command: "graph handoff",
|
|
533
|
-
summary: "GitNexus graph usage handoff generated.",
|
|
534
|
-
warnings: result.state.installed ? [] : [
|
|
535
|
-
|
|
864
|
+
summary: ctx.language === "en" ? "GitNexus graph usage handoff generated." : "\u5DF2\u751F\u6210 GitNexus \u4EE3\u7801\u56FE\u4F7F\u7528\u4EA4\u63A5\u8BF4\u660E\u3002",
|
|
865
|
+
warnings: result.state.installed ? [] : [
|
|
866
|
+
ctx.language === "en" ? "GitNexus is not installed. The handoff still documents the fallback behavior." : "\u5C1A\u672A\u5B89\u88C5 GitNexus\u3002\u4EA4\u63A5\u8BF4\u660E\u4ECD\u4F1A\u8BB0\u5F55\u56DE\u9000\u884C\u4E3A\u3002"
|
|
867
|
+
],
|
|
868
|
+
nextSteps: ctx.language === "en" ? ["Cursor/Codex/OpenCode: read .fet/graph-handoff.md before broad repository scans"] : ["Cursor/Codex/OpenCode\uFF1A\u5927\u8303\u56F4\u626B\u63CF\u4ED3\u5E93\u524D\u5148\u9605\u8BFB .fet/graph-handoff.md"],
|
|
536
869
|
data: {
|
|
537
870
|
path: ".fet/graph-handoff.md",
|
|
538
871
|
gitnexus: result.state
|
|
@@ -544,7 +877,7 @@ async function graphAnalyzeCommand(ctx, mode, args) {
|
|
|
544
877
|
if (!detection.installed) {
|
|
545
878
|
throw new FetError({
|
|
546
879
|
code: "GRAPH_PROVIDER_NOT_FOUND" /* GraphProviderNotFound */,
|
|
547
|
-
message: "GitNexus is not installed or is not available on PATH.",
|
|
880
|
+
message: ctx.language === "en" ? "GitNexus is not installed or is not available on PATH." : "\u5C1A\u672A\u5B89\u88C5 GitNexus\uFF0C\u6216 GitNexus \u4E0D\u5728 PATH \u4E2D\u3002",
|
|
548
881
|
details: { executable: detection.executablePath, error: detection.error },
|
|
549
882
|
suggestedCommand: "fet graph setup"
|
|
550
883
|
});
|
|
@@ -553,27 +886,33 @@ async function graphAnalyzeCommand(ctx, mode, args) {
|
|
|
553
886
|
if (run.exitCode !== 0) {
|
|
554
887
|
throw new FetError({
|
|
555
888
|
code: "GRAPH_COMMAND_FAILED" /* GraphCommandFailed */,
|
|
556
|
-
message: "GitNexus analyze failed.",
|
|
889
|
+
message: ctx.language === "en" ? "GitNexus analyze failed." : "GitNexus analyze \u6267\u884C\u5931\u8D25\u3002",
|
|
557
890
|
details: { command: run.command.join(" "), exitCode: run.exitCode, stdout: run.stdout, stderr: run.stderr },
|
|
558
891
|
suggestedCommand: "fet graph doctor"
|
|
559
892
|
});
|
|
560
893
|
}
|
|
561
894
|
const result = await refreshGraphState(ctx, { write: false });
|
|
895
|
+
const graphContext = await buildProjectGraphContext(ctx, result.state, `fet graph ${mode}`);
|
|
562
896
|
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
563
897
|
global.graph ??= {};
|
|
564
898
|
global.graph.gitnexus = {
|
|
565
899
|
...result.state,
|
|
566
|
-
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
|
|
567
903
|
};
|
|
568
904
|
await ctx.stateStore.writeGlobal(global);
|
|
569
905
|
ctx.output.result({
|
|
570
906
|
ok: true,
|
|
571
907
|
command: `graph ${mode}`,
|
|
572
|
-
summary: mode === "init" ? "GitNexus graph initialized." : "GitNexus graph refreshed.",
|
|
573
|
-
warnings: result.state.graphExists ? [] : [
|
|
574
|
-
|
|
908
|
+
summary: ctx.language === "en" ? mode === "init" ? "GitNexus graph initialized." : "GitNexus graph refreshed." : mode === "init" ? "\u5DF2\u521D\u59CB\u5316 GitNexus \u4EE3\u7801\u56FE\u3002" : "\u5DF2\u5237\u65B0 GitNexus \u4EE3\u7801\u56FE\u3002",
|
|
909
|
+
warnings: result.state.graphExists ? [] : [
|
|
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"
|
|
911
|
+
],
|
|
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"],
|
|
575
913
|
data: {
|
|
576
914
|
gitnexus: global.graph.gitnexus,
|
|
915
|
+
graphContext,
|
|
577
916
|
run: {
|
|
578
917
|
command: run.command,
|
|
579
918
|
stdout: run.stdout.trim(),
|
|
@@ -593,7 +932,7 @@ async function refreshGraphState(ctx, options = {}) {
|
|
|
593
932
|
gitnexusStatus = await runGitNexus(["status"], { cwd: ctx.projectRoot });
|
|
594
933
|
state = {
|
|
595
934
|
...state,
|
|
596
|
-
lastStatus:
|
|
935
|
+
lastStatus: firstLine2(gitnexusStatus.stdout) || firstLine2(gitnexusStatus.stderr) || `exit ${gitnexusStatus.exitCode}`
|
|
597
936
|
};
|
|
598
937
|
}
|
|
599
938
|
if (options.write ?? true) {
|
|
@@ -611,18 +950,19 @@ async function refreshGraphState(ctx, options = {}) {
|
|
|
611
950
|
};
|
|
612
951
|
}
|
|
613
952
|
async function writeHandoffFile(path, content) {
|
|
614
|
-
await
|
|
953
|
+
await mkdir5(dirname6(path), { recursive: true });
|
|
615
954
|
await atomicWrite(path, content);
|
|
616
955
|
}
|
|
617
|
-
function renderGraphSetupHandoff(state) {
|
|
618
|
-
|
|
956
|
+
function renderGraphSetupHandoff(state, options) {
|
|
957
|
+
if (options.language === "en") {
|
|
958
|
+
return `<!-- FET:MANAGED
|
|
619
959
|
schemaVersion: 1
|
|
620
960
|
generator: graph-setup
|
|
621
961
|
FET:END -->
|
|
622
962
|
|
|
623
|
-
# FET
|
|
963
|
+
# FET GitNexus IDE Setup Playbook
|
|
624
964
|
|
|
625
|
-
|
|
965
|
+
This file is written for the current IDE LLM. Use it to help the user install and verify optional GitNexus graph support. GitNexus is optional; FET/OpenSpec workflows must continue to work when it is unavailable.
|
|
626
966
|
|
|
627
967
|
Current status:
|
|
628
968
|
|
|
@@ -631,24 +971,70 @@ Current status:
|
|
|
631
971
|
- Version: ${state.version ?? "unknown"}
|
|
632
972
|
- Graph path: ${state.graphPath ?? ".gitnexus"}
|
|
633
973
|
- Graph exists: ${state.graphExists ? "yes" : "no"}
|
|
974
|
+
- Configured install command: ${options.installCommand ?? "none"}
|
|
634
975
|
|
|
635
|
-
|
|
976
|
+
IDE LLM setup flow:
|
|
636
977
|
|
|
637
|
-
1.
|
|
638
|
-
2.
|
|
639
|
-
3.
|
|
640
|
-
4.
|
|
978
|
+
1. Check the shell, operating system, package managers, and PATH. Run only read-only detection commands at first.
|
|
979
|
+
2. Run \`gitnexus --version\` or the executable shown above. If it succeeds, skip installation and continue to verification.
|
|
980
|
+
3. If GitNexus is missing and \`FET_GITNEXUS_INSTALL_COMMAND\` is configured, explain that command to the user and ask for approval before running it.
|
|
981
|
+
4. If no install command is configured, find the official GitNexus installation instructions or ask the user for the preferred install method. Do not invent an installer.
|
|
982
|
+
5. Before running any command that downloads software, installs globally, changes PATH, modifies user home files, or changes IDE/MCP configuration, show the exact command and wait for user approval.
|
|
983
|
+
6. After installation, verify with \`gitnexus --version\`.
|
|
984
|
+
7. If the user wants IDE/MCP integration, run \`gitnexus setup\` only after explaining what it may change and receiving approval.
|
|
985
|
+
8. Return to this project and run \`fet graph init\` to build the first graph.
|
|
986
|
+
9. Run \`fet graph handoff\` so future IDE AI work can prefer graph context before broad repository scans.
|
|
641
987
|
|
|
642
988
|
Guardrails:
|
|
643
989
|
|
|
644
990
|
- Do not block FET/OpenSpec commands when GitNexus is unavailable.
|
|
645
991
|
- Do not generate or modify application code during setup.
|
|
646
|
-
- Do not
|
|
992
|
+
- Do not silently install software or modify global/user-level configuration.
|
|
993
|
+
- If installation fails, summarize the failing command, stderr, and the next manual step.
|
|
647
994
|
`;
|
|
648
|
-
}
|
|
649
|
-
function renderGraphUsageHandoff(state) {
|
|
995
|
+
}
|
|
650
996
|
return `<!-- FET:MANAGED
|
|
651
997
|
schemaVersion: 1
|
|
998
|
+
generator: graph-setup
|
|
999
|
+
FET:END -->
|
|
1000
|
+
|
|
1001
|
+
# FET GitNexus IDE \u5B89\u88C5\u4EFB\u52A1\u4E66
|
|
1002
|
+
|
|
1003
|
+
\u672C\u6587\u6863\u5199\u7ED9\u5F53\u524D IDE LLM\u3002\u8BF7\u7528\u5B83\u5E2E\u52A9\u7528\u6237\u5B89\u88C5\u5E76\u9A8C\u8BC1\u53EF\u9009\u7684 GitNexus \u4EE3\u7801\u56FE\u80FD\u529B\u3002GitNexus \u662F\u53EF\u9009\u80FD\u529B\uFF1B\u5373\u4F7F\u4E0D\u53EF\u7528\uFF0CFET/OpenSpec \u4E3B\u6D41\u7A0B\u4E5F\u5FC5\u987B\u7EE7\u7EED\u53EF\u7528\u3002
|
|
1004
|
+
|
|
1005
|
+
\u5F53\u524D\u72B6\u6001\uFF1A
|
|
1006
|
+
|
|
1007
|
+
- \u5DF2\u5B89\u88C5\uFF1A${state.installed ? "\u662F" : "\u5426"}
|
|
1008
|
+
- \u53EF\u6267\u884C\u6587\u4EF6\uFF1A${state.executablePath ?? "gitnexus"}
|
|
1009
|
+
- \u7248\u672C\uFF1A${state.version ?? "unknown"}
|
|
1010
|
+
- \u56FE\u8DEF\u5F84\uFF1A${state.graphPath ?? ".gitnexus"}
|
|
1011
|
+
- \u56FE\u5DF2\u5B58\u5728\uFF1A${state.graphExists ? "\u662F" : "\u5426"}
|
|
1012
|
+
- \u5DF2\u914D\u7F6E\u5B89\u88C5\u547D\u4EE4\uFF1A${options.installCommand ?? "\u65E0"}
|
|
1013
|
+
|
|
1014
|
+
IDE LLM \u5B89\u88C5\u6D41\u7A0B\uFF1A
|
|
1015
|
+
|
|
1016
|
+
1. \u5148\u68C0\u6D4B shell\u3001\u64CD\u4F5C\u7CFB\u7EDF\u3001\u5305\u7BA1\u7406\u5668\u548C PATH\u3002\u8D77\u6B65\u9636\u6BB5\u53EA\u8FD0\u884C\u53EA\u8BFB\u68C0\u6D4B\u547D\u4EE4\u3002
|
|
1017
|
+
2. \u8FD0\u884C \`gitnexus --version\` \u6216\u4E0A\u65B9\u663E\u793A\u7684\u53EF\u6267\u884C\u6587\u4EF6\u3002\u5982\u679C\u6210\u529F\uFF0C\u8DF3\u8FC7\u5B89\u88C5\u5E76\u8FDB\u5165\u9A8C\u8BC1\u3002
|
|
1018
|
+
3. \u5982\u679C GitNexus \u7F3A\u5931\u4E14\u5DF2\u914D\u7F6E \`FET_GITNEXUS_INSTALL_COMMAND\`\uFF0C\u5148\u5411\u7528\u6237\u89E3\u91CA\u8BE5\u547D\u4EE4\uFF0C\u518D\u7B49\u5F85\u7528\u6237\u6279\u51C6\u540E\u6267\u884C\u3002
|
|
1019
|
+
4. \u5982\u679C\u6CA1\u6709\u914D\u7F6E\u5B89\u88C5\u547D\u4EE4\uFF0C\u67E5\u627E GitNexus \u5B98\u65B9\u5B89\u88C5\u8BF4\u660E\uFF0C\u6216\u8BE2\u95EE\u7528\u6237\u5E0C\u671B\u4F7F\u7528\u7684\u5B89\u88C5\u65B9\u5F0F\u3002\u4E0D\u8981\u81C6\u9020\u5B89\u88C5\u547D\u4EE4\u3002
|
|
1020
|
+
5. \u4EFB\u4F55\u4F1A\u4E0B\u8F7D\u8F6F\u4EF6\u3001\u5168\u5C40\u5B89\u88C5\u3001\u4FEE\u6539 PATH\u3001\u5199\u5165\u7528\u6237\u76EE\u5F55\u6216\u4FEE\u6539 IDE/MCP \u914D\u7F6E\u7684\u547D\u4EE4\uFF0C\u6267\u884C\u524D\u90FD\u8981\u5C55\u793A\u5B8C\u6574\u547D\u4EE4\u5E76\u7B49\u5F85\u7528\u6237\u786E\u8BA4\u3002
|
|
1021
|
+
6. \u5B89\u88C5\u540E\u8FD0\u884C \`gitnexus --version\` \u9A8C\u8BC1\u3002
|
|
1022
|
+
7. \u5982\u679C\u7528\u6237\u9700\u8981 IDE/MCP \u96C6\u6210\uFF0C\u5148\u8BF4\u660E \`gitnexus setup\` \u53EF\u80FD\u4FEE\u6539\u7684\u5185\u5BB9\uFF0C\u83B7\u5F97\u786E\u8BA4\u540E\u518D\u8FD0\u884C\u3002
|
|
1023
|
+
8. \u56DE\u5230\u672C\u9879\u76EE\u8FD0\u884C \`fet graph init\`\uFF0C\u751F\u6210\u7B2C\u4E00\u4EFD\u4EE3\u7801\u56FE\u3002
|
|
1024
|
+
9. \u8FD0\u884C \`fet graph handoff\`\uFF0C\u8BA9\u540E\u7EED IDE AI \u5728\u5927\u8303\u56F4\u626B\u63CF\u524D\u4F18\u5148\u4F7F\u7528\u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\u3002
|
|
1025
|
+
|
|
1026
|
+
\u7EA6\u675F\uFF1A
|
|
1027
|
+
|
|
1028
|
+
- GitNexus \u4E0D\u53EF\u7528\u65F6\uFF0C\u4E0D\u8981\u963B\u585E FET/OpenSpec \u547D\u4EE4\u3002
|
|
1029
|
+
- \u5B89\u88C5\u8FC7\u7A0B\u4E2D\u4E0D\u8981\u751F\u6210\u6216\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002
|
|
1030
|
+
- \u4E0D\u8981\u9759\u9ED8\u5B89\u88C5\u8F6F\u4EF6\uFF0C\u4E5F\u4E0D\u8981\u9759\u9ED8\u4FEE\u6539\u5168\u5C40\u6216\u7528\u6237\u7EA7\u914D\u7F6E\u3002
|
|
1031
|
+
- \u5982\u679C\u5B89\u88C5\u5931\u8D25\uFF0C\u6C47\u603B\u5931\u8D25\u547D\u4EE4\u3001stderr \u548C\u4E0B\u4E00\u6B65\u4EBA\u5DE5\u5904\u7406\u5EFA\u8BAE\u3002
|
|
1032
|
+
`;
|
|
1033
|
+
}
|
|
1034
|
+
function renderGraphUsageHandoff(state, language) {
|
|
1035
|
+
if (language === "en") {
|
|
1036
|
+
return `<!-- FET:MANAGED
|
|
1037
|
+
schemaVersion: 1
|
|
652
1038
|
generator: graph-handoff
|
|
653
1039
|
FET:END -->
|
|
654
1040
|
|
|
@@ -676,26 +1062,57 @@ When producing OpenSpec artifacts:
|
|
|
676
1062
|
- Use graph context to make proposal, design, specs, and tasks more precise.
|
|
677
1063
|
- Avoid large repository scans when the graph already narrows the relevant area.
|
|
678
1064
|
- Keep all generated artifacts in the normal OpenSpec change directory.
|
|
1065
|
+
`;
|
|
1066
|
+
}
|
|
1067
|
+
return `<!-- FET:MANAGED
|
|
1068
|
+
schemaVersion: 1
|
|
1069
|
+
generator: graph-handoff
|
|
1070
|
+
FET:END -->
|
|
1071
|
+
|
|
1072
|
+
# FET \u4EE3\u7801\u56FE\u4EA4\u63A5\u8BF4\u660E
|
|
1073
|
+
|
|
1074
|
+
\u5728\u5927\u8303\u56F4\u626B\u63CF\u4ED3\u5E93\u524D\uFF0C\u4F18\u5148\u628A GitNexus \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\u4F5C\u4E3A\u53EF\u9009\u7684\u7B2C\u4E00\u8F6E\u7EBF\u7D22\u3002
|
|
1075
|
+
|
|
1076
|
+
\u5F53\u524D\u72B6\u6001\uFF1A
|
|
1077
|
+
|
|
1078
|
+
- \u5DF2\u5B89\u88C5\uFF1A${state.installed ? "\u662F" : "\u5426"}
|
|
1079
|
+
- \u56FE\u8DEF\u5F84\uFF1A${state.graphPath ?? ".gitnexus"}
|
|
1080
|
+
- \u56FE\u5DF2\u5B58\u5728\uFF1A${state.graphExists ? "\u662F" : "\u5426"}
|
|
1081
|
+
- \u6700\u540E\u7D22\u5F15\u65F6\u95F4\uFF1A${state.lastIndexedAt ?? "unknown"}
|
|
1082
|
+
- \u6700\u540E\u72B6\u6001\uFF1A${state.lastStatus ?? "unknown"}
|
|
1083
|
+
|
|
1084
|
+
\u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\u53EF\u7528\u65F6\uFF1A
|
|
1085
|
+
|
|
1086
|
+
1. \u7528\u4EE3\u7801\u56FE\u8BC6\u522B\u53EF\u80FD\u76F8\u5173\u7684\u6A21\u5757\u3001\u4F9D\u8D56\u548C\u63D2\u5165\u70B9\u3002
|
|
1087
|
+
2. \u53EA\u8BFB\u53D6\u9700\u8981\u786E\u8BA4\u884C\u4E3A\u7684\u5177\u4F53\u6E90\u7801\u6587\u4EF6\u3002
|
|
1088
|
+
3. \u5F53\u4EE3\u7801\u56FE\u63A8\u65AD\u4E0E OpenSpec \u4EA7\u7269\u6216 AGENTS.md \u51B2\u7A81\u65F6\uFF0C\u4F18\u5148\u76F8\u4FE1 OpenSpec \u4EA7\u7269\u548C AGENTS.md\u3002
|
|
1089
|
+
4. \u5982\u679C\u4EE3\u7801\u56FE\u7F3A\u5931\u3001\u8FC7\u671F\u6216\u4E0D\u5B8C\u6574\uFF0C\u56DE\u9000\u5230\u666E\u901A\u4ED3\u5E93\u68C0\u67E5\u3002
|
|
1090
|
+
|
|
1091
|
+
\u751F\u6210 OpenSpec \u4EA7\u7269\u65F6\uFF1A
|
|
1092
|
+
|
|
1093
|
+
- \u7528\u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\u8BA9 proposal\u3001design\u3001specs \u548C tasks \u66F4\u7CBE\u786E\u3002
|
|
1094
|
+
- \u5F53\u4EE3\u7801\u56FE\u5DF2\u7ECF\u7F29\u5C0F\u76F8\u5173\u8303\u56F4\u65F6\uFF0C\u907F\u514D\u5927\u8303\u56F4\u4ED3\u5E93\u626B\u63CF\u3002
|
|
1095
|
+
- \u6240\u6709\u751F\u6210\u4EA7\u7269\u4ECD\u5199\u5165\u6B63\u5E38\u7684 OpenSpec change \u76EE\u5F55\u3002
|
|
679
1096
|
`;
|
|
680
1097
|
}
|
|
681
|
-
function
|
|
1098
|
+
function firstLine2(value) {
|
|
682
1099
|
return value.trim().split(/\r?\n/)[0]?.trim() || null;
|
|
683
1100
|
}
|
|
684
1101
|
|
|
685
1102
|
// src/commands/init.ts
|
|
686
|
-
import { readFile as
|
|
687
|
-
import { join as
|
|
1103
|
+
import { readFile as readFile8, stat as stat4 } from "fs/promises";
|
|
1104
|
+
import { join as join12 } from "path";
|
|
688
1105
|
|
|
689
1106
|
// src/version.ts
|
|
690
1107
|
import { existsSync, readFileSync } from "fs";
|
|
691
|
-
import { dirname as
|
|
1108
|
+
import { dirname as dirname7, join as join10, parse } from "path";
|
|
692
1109
|
import { fileURLToPath } from "url";
|
|
693
1110
|
var FET_VERSION = readPackageVersion();
|
|
694
1111
|
function readPackageVersion() {
|
|
695
|
-
let currentDir =
|
|
1112
|
+
let currentDir = dirname7(fileURLToPath(import.meta.url));
|
|
696
1113
|
const root = parse(currentDir).root;
|
|
697
1114
|
while (true) {
|
|
698
|
-
const packageJsonPath =
|
|
1115
|
+
const packageJsonPath = join10(currentDir, "package.json");
|
|
699
1116
|
if (existsSync(packageJsonPath)) {
|
|
700
1117
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
701
1118
|
if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
|
|
@@ -706,7 +1123,7 @@ function readPackageVersion() {
|
|
|
706
1123
|
if (currentDir === root) {
|
|
707
1124
|
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D FET package.json");
|
|
708
1125
|
}
|
|
709
|
-
currentDir =
|
|
1126
|
+
currentDir = dirname7(currentDir);
|
|
710
1127
|
}
|
|
711
1128
|
}
|
|
712
1129
|
|
|
@@ -975,8 +1392,8 @@ function renderFetConfig(scan, language = "zh-CN") {
|
|
|
975
1392
|
var KARPATHY_SKILLS_SOURCE = "https://github.com/forrestchang/andrej-karpathy-skills";
|
|
976
1393
|
var BEGIN = "<!-- FET:BEGIN ANDREJ-KARPATHY-SKILLS -->";
|
|
977
1394
|
var END = "<!-- FET:END ANDREJ-KARPATHY-SKILLS -->";
|
|
978
|
-
function mergeKarpathyClaudeMd(existing) {
|
|
979
|
-
const block = renderManagedBlock(renderKarpathyClaudeGuidelines());
|
|
1395
|
+
function mergeKarpathyClaudeMd(existing, language = "zh-CN") {
|
|
1396
|
+
const block = renderManagedBlock(renderKarpathyClaudeGuidelines(language));
|
|
980
1397
|
if (!existing || !existing.trim()) {
|
|
981
1398
|
return `${block}
|
|
982
1399
|
`;
|
|
@@ -1021,10 +1438,10 @@ function renderManagedBlock(content) {
|
|
|
1021
1438
|
${content}
|
|
1022
1439
|
${END}`;
|
|
1023
1440
|
}
|
|
1024
|
-
function renderKarpathyClaudeGuidelines() {
|
|
1025
|
-
return `# Andrej Karpathy Inspired Coding Guidelines
|
|
1441
|
+
function renderKarpathyClaudeGuidelines(language) {
|
|
1442
|
+
return `# ${language === "en" ? "Andrej Karpathy Inspired Coding Guidelines" : "\u53D7 Andrej Karpathy \u542F\u53D1\u7684\u7F16\u7801\u6307\u5357"}
|
|
1026
1443
|
|
|
1027
|
-
${renderKarpathyGuidelinesBody()}`;
|
|
1444
|
+
${renderKarpathyGuidelinesBody(language)}`;
|
|
1028
1445
|
}
|
|
1029
1446
|
function renderKarpathyGuidelinesBody(language = "zh-CN") {
|
|
1030
1447
|
if (language === "en") {
|
|
@@ -1157,18 +1574,18 @@ ${block}
|
|
|
1157
1574
|
}
|
|
1158
1575
|
|
|
1159
1576
|
// src/commands/update-context.ts
|
|
1160
|
-
import { readFile as
|
|
1161
|
-
import { join as
|
|
1577
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1578
|
+
import { join as join11 } from "path";
|
|
1162
1579
|
|
|
1163
1580
|
// src/config/yaml.ts
|
|
1164
|
-
import { readFile as
|
|
1581
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1165
1582
|
import { parseDocument } from "yaml";
|
|
1166
1583
|
async function mergeFetConfig(configPath, renderedFetYaml) {
|
|
1167
1584
|
const fetDoc = parseDocument(renderedFetYaml);
|
|
1168
1585
|
const nextFet = fetDoc.get("fet", true);
|
|
1169
1586
|
let existing = "";
|
|
1170
1587
|
try {
|
|
1171
|
-
existing = await
|
|
1588
|
+
existing = await readFile6(configPath, "utf8");
|
|
1172
1589
|
} catch {
|
|
1173
1590
|
return renderedFetYaml;
|
|
1174
1591
|
}
|
|
@@ -1192,14 +1609,14 @@ async function updateContextCommand(ctx) {
|
|
|
1192
1609
|
}
|
|
1193
1610
|
async function updateContextFiles(ctx) {
|
|
1194
1611
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
1195
|
-
const agentsPath =
|
|
1196
|
-
const configPath =
|
|
1197
|
-
const claudePath =
|
|
1198
|
-
const karpathyHandoffPath =
|
|
1199
|
-
const karpathyCursorPath =
|
|
1200
|
-
const existingAgents = await
|
|
1201
|
-
const existingClaude = await
|
|
1202
|
-
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);
|
|
1203
1620
|
const warnings = [...scan.warnings];
|
|
1204
1621
|
if (existingAgents && hasInvalidManagedAutoRegion(existingAgents)) {
|
|
1205
1622
|
throw new FetError({
|
|
@@ -1225,7 +1642,7 @@ async function updateContextFiles(ctx) {
|
|
|
1225
1642
|
}
|
|
1226
1643
|
await atomicWrite(agentsPath, replaceManagedRegion(existingAgents, renderAgentsMd(scan, ctx.language)));
|
|
1227
1644
|
await atomicWrite(configPath, await mergeFetConfig(configPath, renderFetConfig(scan, ctx.language)));
|
|
1228
|
-
await atomicWrite(claudePath, mergeKarpathyClaudeMd(existingClaude));
|
|
1645
|
+
await atomicWrite(claudePath, mergeKarpathyClaudeMd(existingClaude, ctx.language));
|
|
1229
1646
|
await atomicWrite(karpathyHandoffPath, renderKarpathyFetHandoff(ctx.language));
|
|
1230
1647
|
if (!existingKarpathyCursor || existingKarpathyCursor.includes("FET:MANAGED")) {
|
|
1231
1648
|
await atomicWrite(karpathyCursorPath, renderKarpathyCursorRule(ctx.language));
|
|
@@ -1247,9 +1664,9 @@ async function updateContextFiles(ctx) {
|
|
|
1247
1664
|
await ctx.stateStore.writeGlobal(state);
|
|
1248
1665
|
return { warnings };
|
|
1249
1666
|
}
|
|
1250
|
-
async function
|
|
1667
|
+
async function readOptional2(path) {
|
|
1251
1668
|
try {
|
|
1252
|
-
return await
|
|
1669
|
+
return await readFile7(path, "utf8");
|
|
1253
1670
|
} catch {
|
|
1254
1671
|
return null;
|
|
1255
1672
|
}
|
|
@@ -1257,7 +1674,7 @@ async function readOptional(path) {
|
|
|
1257
1674
|
|
|
1258
1675
|
// src/commands/init.ts
|
|
1259
1676
|
async function initCommand(ctx) {
|
|
1260
|
-
const alreadyInitialized = await exists2(
|
|
1677
|
+
const alreadyInitialized = await exists2(join12(ctx.projectRoot, "openspec", "config.yaml"));
|
|
1261
1678
|
let warnings = [];
|
|
1262
1679
|
await withProjectLock(ctx.projectRoot, { command: "init", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
1263
1680
|
const journal = createInitJournal(ctx.fetVersion);
|
|
@@ -1309,13 +1726,13 @@ async function initCommand(ctx) {
|
|
|
1309
1726
|
});
|
|
1310
1727
|
}
|
|
1311
1728
|
async function ensureGitignore(ctx) {
|
|
1312
|
-
const gitignorePath =
|
|
1313
|
-
const existing = await
|
|
1729
|
+
const gitignorePath = join12(ctx.projectRoot, ".gitignore");
|
|
1730
|
+
const existing = await readOptional3(gitignorePath);
|
|
1314
1731
|
await atomicWrite(gitignorePath, mergeGitignore(existing));
|
|
1315
1732
|
}
|
|
1316
|
-
async function
|
|
1733
|
+
async function readOptional3(path) {
|
|
1317
1734
|
try {
|
|
1318
|
-
return await
|
|
1735
|
+
return await readFile8(path, "utf8");
|
|
1319
1736
|
} catch {
|
|
1320
1737
|
return null;
|
|
1321
1738
|
}
|
|
@@ -1330,8 +1747,8 @@ async function exists2(path) {
|
|
|
1330
1747
|
}
|
|
1331
1748
|
|
|
1332
1749
|
// src/commands/proxy.ts
|
|
1333
|
-
import { readFile as
|
|
1334
|
-
import { join as
|
|
1750
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
1751
|
+
import { join as join14 } from "path";
|
|
1335
1752
|
|
|
1336
1753
|
// src/state/project.ts
|
|
1337
1754
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -1360,8 +1777,8 @@ async function git(cwd, args) {
|
|
|
1360
1777
|
}
|
|
1361
1778
|
|
|
1362
1779
|
// src/state/store.ts
|
|
1363
|
-
import { mkdir as
|
|
1364
|
-
import { join as
|
|
1780
|
+
import { mkdir as mkdir6, readFile as readFile9 } from "fs/promises";
|
|
1781
|
+
import { join as join13 } from "path";
|
|
1365
1782
|
|
|
1366
1783
|
// src/language.ts
|
|
1367
1784
|
var DEFAULT_LANGUAGE = "zh-CN";
|
|
@@ -1479,7 +1896,7 @@ var StateStore = class {
|
|
|
1479
1896
|
project;
|
|
1480
1897
|
async readGlobal() {
|
|
1481
1898
|
try {
|
|
1482
|
-
const value = JSON.parse(await
|
|
1899
|
+
const value = JSON.parse(await readFile9(this.globalPath(), "utf8"));
|
|
1483
1900
|
assertGlobalState(value);
|
|
1484
1901
|
return value;
|
|
1485
1902
|
} catch (error) {
|
|
@@ -1494,13 +1911,13 @@ var StateStore = class {
|
|
|
1494
1911
|
}
|
|
1495
1912
|
async writeGlobal(state) {
|
|
1496
1913
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1497
|
-
await
|
|
1914
|
+
await mkdir6(join13(this.projectRoot, "openspec"), { recursive: true });
|
|
1498
1915
|
await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
|
|
1499
1916
|
`);
|
|
1500
1917
|
}
|
|
1501
1918
|
async readChange(changeId) {
|
|
1502
1919
|
try {
|
|
1503
|
-
const value = JSON.parse(await
|
|
1920
|
+
const value = JSON.parse(await readFile9(this.changePath(changeId), "utf8"));
|
|
1504
1921
|
assertChangeState(value);
|
|
1505
1922
|
return value;
|
|
1506
1923
|
} catch (error) {
|
|
@@ -1515,15 +1932,15 @@ var StateStore = class {
|
|
|
1515
1932
|
}
|
|
1516
1933
|
async writeChange(state) {
|
|
1517
1934
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1518
|
-
await
|
|
1935
|
+
await mkdir6(join13(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
|
|
1519
1936
|
await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
|
|
1520
1937
|
`);
|
|
1521
1938
|
}
|
|
1522
1939
|
globalPath() {
|
|
1523
|
-
return
|
|
1940
|
+
return join13(this.projectRoot, "openspec", "fet-state.json");
|
|
1524
1941
|
}
|
|
1525
1942
|
changePath(changeId) {
|
|
1526
|
-
return
|
|
1943
|
+
return join13(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
|
|
1527
1944
|
}
|
|
1528
1945
|
};
|
|
1529
1946
|
function isNotFound(error) {
|
|
@@ -1531,11 +1948,11 @@ function isNotFound(error) {
|
|
|
1531
1948
|
}
|
|
1532
1949
|
|
|
1533
1950
|
// src/state/tasks.ts
|
|
1534
|
-
import { readFile as
|
|
1951
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
1535
1952
|
async function readCompletedTaskIds(tasksPath) {
|
|
1536
1953
|
let content;
|
|
1537
1954
|
try {
|
|
1538
|
-
content = await
|
|
1955
|
+
content = await readFile10(tasksPath, "utf8");
|
|
1539
1956
|
} catch {
|
|
1540
1957
|
return [];
|
|
1541
1958
|
}
|
|
@@ -1566,71 +1983,76 @@ var phaseByCommand = {
|
|
|
1566
1983
|
};
|
|
1567
1984
|
async function proxyCommand(ctx, command, args) {
|
|
1568
1985
|
const openSpecArgs = stripFetOptions(args);
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
state.activeChangeId = ctx.changeId;
|
|
1602
|
-
} else if (state.activeChangeId && !inspection.changes.includes(state.activeChangeId)) {
|
|
1603
|
-
state.activeChangeId = inspection.changes.length === 1 ? inspection.changes[0] ?? null : null;
|
|
1604
|
-
} else if (!state.activeChangeId && inspection.changes.length === 1) {
|
|
1605
|
-
state.activeChangeId = inspection.changes[0] ?? null;
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
await ctx.stateStore.writeGlobal(state);
|
|
1609
|
-
const changeId = ctx.changeId ?? state.activeChangeId;
|
|
1610
|
-
if (changeId && inspection.changes.includes(changeId)) {
|
|
1611
|
-
const changeInspection = await ctx.openSpec.inspectChange(ctx.projectRoot, changeId);
|
|
1612
|
-
const changeState = await ctx.stateStore.getOrCreateChange(changeId, phaseByCommand[command] ?? "propose");
|
|
1613
|
-
changeState.currentPhase = phaseByCommand[command] ?? changeState.currentPhase;
|
|
1614
|
-
changeState.phases[changeState.currentPhase] = {
|
|
1615
|
-
status: "done",
|
|
1616
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1617
|
-
};
|
|
1618
|
-
changeState.lastOpenSpecCommand = {
|
|
1619
|
-
command: mapped.command,
|
|
1620
|
-
args: mapped.args,
|
|
1621
|
-
exitCode: result.exitCode,
|
|
1622
|
-
ranAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1623
|
-
};
|
|
1624
|
-
changeState.tasks.completedIds = await readCompletedTaskIds(changeInspection.tasksPath);
|
|
1625
|
-
changeState.tasks.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1626
|
-
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;
|
|
1627
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;
|
|
1628
2026
|
}
|
|
1629
|
-
|
|
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;
|
|
1630
2049
|
ctx.output.result({
|
|
1631
2050
|
ok: true,
|
|
1632
2051
|
command,
|
|
1633
|
-
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
|
|
1634
2056
|
});
|
|
1635
2057
|
}
|
|
1636
2058
|
async function createChangelogEntry(projectRoot, changeId) {
|
|
@@ -1640,10 +2062,12 @@ async function createChangelogEntry(projectRoot, changeId) {
|
|
|
1640
2062
|
};
|
|
1641
2063
|
}
|
|
1642
2064
|
async function appendChangelog(projectRoot, entry) {
|
|
1643
|
-
const changelogPath =
|
|
1644
|
-
const existing = await
|
|
2065
|
+
const changelogPath = join14(projectRoot, "CHANGELOG.md");
|
|
2066
|
+
const existing = await readOptional4(changelogPath);
|
|
2067
|
+
const legacyContentLabel = "\u66F4\u65B0\u5185\u5BB9";
|
|
1645
2068
|
const block = `updateTime: ${entry.updateTime}
|
|
1646
|
-
|
|
2069
|
+
changeRequirement:${entry.content}
|
|
2070
|
+
${legacyContentLabel}:${entry.content}
|
|
1647
2071
|
`;
|
|
1648
2072
|
const next = existing?.trimEnd() ? `${existing.trimEnd()}
|
|
1649
2073
|
|
|
@@ -1651,12 +2075,12 @@ ${block}` : block;
|
|
|
1651
2075
|
await atomicWrite(changelogPath, next);
|
|
1652
2076
|
}
|
|
1653
2077
|
async function readChangeRequirement(projectRoot, changeId) {
|
|
1654
|
-
const changeRoot =
|
|
1655
|
-
const proposal = await
|
|
2078
|
+
const changeRoot = join14(projectRoot, "openspec", "changes", changeId);
|
|
2079
|
+
const proposal = await readOptional4(join14(changeRoot, "proposal.md"));
|
|
1656
2080
|
if (proposal) {
|
|
1657
2081
|
return summarizeMarkdown(proposal);
|
|
1658
2082
|
}
|
|
1659
|
-
const readme = await
|
|
2083
|
+
const readme = await readOptional4(join14(changeRoot, "README.md"));
|
|
1660
2084
|
if (readme) {
|
|
1661
2085
|
return summarizeMarkdown(readme);
|
|
1662
2086
|
}
|
|
@@ -1664,11 +2088,11 @@ async function readChangeRequirement(projectRoot, changeId) {
|
|
|
1664
2088
|
}
|
|
1665
2089
|
function summarizeMarkdown(content) {
|
|
1666
2090
|
const normalized = content.replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("<!--") && !line.startsWith("---")).join(" ");
|
|
1667
|
-
return normalized || "
|
|
2091
|
+
return normalized || "No change requirement found.";
|
|
1668
2092
|
}
|
|
1669
|
-
async function
|
|
2093
|
+
async function readOptional4(path) {
|
|
1670
2094
|
try {
|
|
1671
|
-
return await
|
|
2095
|
+
return await readFile11(path, "utf8");
|
|
1672
2096
|
} catch {
|
|
1673
2097
|
return null;
|
|
1674
2098
|
}
|
|
@@ -1690,7 +2114,7 @@ async function passthroughCommand(ctx, command, args) {
|
|
|
1690
2114
|
if (result.exitCode !== 0) {
|
|
1691
2115
|
throw new FetError({
|
|
1692
2116
|
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
1693
|
-
message: `OpenSpec ${command}
|
|
2117
|
+
message: `OpenSpec ${command} failed.`,
|
|
1694
2118
|
details: result
|
|
1695
2119
|
});
|
|
1696
2120
|
}
|
|
@@ -1732,18 +2156,6 @@ async function mapOpenSpecCommand(ctx, command, args) {
|
|
|
1732
2156
|
return { command: "new", args: args[0] === "change" ? args : ["change", ...args] };
|
|
1733
2157
|
case "archive":
|
|
1734
2158
|
return { command: "archive", args: [ctx.changeId ?? args[0] ?? await requireChangeId(ctx), ...args.slice(ctx.changeId ? 0 : 1)] };
|
|
1735
|
-
/*
|
|
1736
|
-
case "bulk-archive":
|
|
1737
|
-
throw new FetError({
|
|
1738
|
-
code: ErrorCode.InvalidArguments,
|
|
1739
|
-
message: "OpenSpec 1.2.0 不提供 bulk-archive 顶层命令",
|
|
1740
|
-
suggestedCommand: "逐个执行 fet archive --change <change-id>"
|
|
1741
|
-
});
|
|
1742
|
-
case "explore":
|
|
1743
|
-
return { command: "explore", args: ctx.changeId ? ["--change", ctx.changeId, ...args] : args };
|
|
1744
|
-
case "onboard":
|
|
1745
|
-
return { command: "instructions", args: [] };
|
|
1746
|
-
*/
|
|
1747
2159
|
default:
|
|
1748
2160
|
return { command, args };
|
|
1749
2161
|
}
|
|
@@ -1763,6 +2175,18 @@ async function withDefaultChange(ctx, args, allowWithArgs = false) {
|
|
|
1763
2175
|
}
|
|
1764
2176
|
return ["--change", await requireChangeId(ctx), ...args];
|
|
1765
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
|
+
}
|
|
1766
2190
|
async function requireChangeId(ctx) {
|
|
1767
2191
|
if (ctx.changeId) {
|
|
1768
2192
|
return ctx.changeId;
|
|
@@ -1777,9 +2201,9 @@ async function requireChangeId(ctx) {
|
|
|
1777
2201
|
}
|
|
1778
2202
|
throw new FetError({
|
|
1779
2203
|
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
1780
|
-
message: "
|
|
2204
|
+
message: "No unambiguous OpenSpec change id was found.",
|
|
1781
2205
|
details: { openChangeIds: inspection.changes },
|
|
1782
|
-
suggestedCommand: "
|
|
2206
|
+
suggestedCommand: "Pass --change <change-id>."
|
|
1783
2207
|
});
|
|
1784
2208
|
}
|
|
1785
2209
|
async function assertVerified(ctx) {
|
|
@@ -1788,7 +2212,7 @@ async function assertVerified(ctx) {
|
|
|
1788
2212
|
if (!changeId) {
|
|
1789
2213
|
throw new FetError({
|
|
1790
2214
|
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
1791
|
-
message: "
|
|
2215
|
+
message: "A change id is required before this command can check FET verification.",
|
|
1792
2216
|
suggestedCommand: "fet verify --done --change <change-id>"
|
|
1793
2217
|
});
|
|
1794
2218
|
}
|
|
@@ -1797,7 +2221,7 @@ async function assertVerified(ctx) {
|
|
|
1797
2221
|
if (!inspection.changes.includes(changeId)) {
|
|
1798
2222
|
throw new FetError({
|
|
1799
2223
|
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
1800
|
-
message: "
|
|
2224
|
+
message: "The selected change does not exist in openspec/changes.",
|
|
1801
2225
|
details: { changeId, openChangeIds: inspection.changes },
|
|
1802
2226
|
suggestedCommand: "fet doctor"
|
|
1803
2227
|
});
|
|
@@ -1805,7 +2229,7 @@ async function assertVerified(ctx) {
|
|
|
1805
2229
|
if (change?.manualVerify?.status !== "declared_done") {
|
|
1806
2230
|
throw new FetError({
|
|
1807
2231
|
code: "STATE_CORRUPTED" /* StateCorrupted */,
|
|
1808
|
-
message: "
|
|
2232
|
+
message: "This change has not been marked verified by FET.",
|
|
1809
2233
|
details: { changeId },
|
|
1810
2234
|
suggestedCommand: `fet verify --change ${changeId}`
|
|
1811
2235
|
});
|
|
@@ -1814,8 +2238,8 @@ async function assertVerified(ctx) {
|
|
|
1814
2238
|
|
|
1815
2239
|
// src/commands/verify.ts
|
|
1816
2240
|
import { createHash } from "crypto";
|
|
1817
|
-
import { mkdir as
|
|
1818
|
-
import { join as
|
|
2241
|
+
import { mkdir as mkdir7, readFile as readFile12, stat as stat5 } from "fs/promises";
|
|
2242
|
+
import { join as join15 } from "path";
|
|
1819
2243
|
async function verifyCommand(ctx, options) {
|
|
1820
2244
|
if (options.auto) {
|
|
1821
2245
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
@@ -1882,9 +2306,9 @@ async function verifyCommand(ctx, options) {
|
|
|
1882
2306
|
async function writeInstructions(ctx, changeId) {
|
|
1883
2307
|
await assertChangeExists(ctx, changeId);
|
|
1884
2308
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1885
|
-
const dir =
|
|
1886
|
-
const instructionsPath =
|
|
1887
|
-
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 });
|
|
1888
2312
|
await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
|
|
1889
2313
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
1890
2314
|
state.currentPhase = "verify";
|
|
@@ -1900,7 +2324,7 @@ async function writeInstructions(ctx, changeId) {
|
|
|
1900
2324
|
async function markDone(ctx, changeId) {
|
|
1901
2325
|
await assertChangeExists(ctx, changeId);
|
|
1902
2326
|
const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1903
|
-
const instructionsPath =
|
|
2327
|
+
const instructionsPath = join15(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
|
|
1904
2328
|
const instructions = await readInstructions(instructionsPath, changeId);
|
|
1905
2329
|
const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
|
|
1906
2330
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
@@ -1936,7 +2360,7 @@ async function assertChangeExists(ctx, changeId) {
|
|
|
1936
2360
|
async function readInstructions(path, changeId) {
|
|
1937
2361
|
try {
|
|
1938
2362
|
await stat5(path);
|
|
1939
|
-
const content = await
|
|
2363
|
+
const content = await readFile12(path, "utf8");
|
|
1940
2364
|
const fileChangeId = readFrontMatterValue(content, "changeId");
|
|
1941
2365
|
if (fileChangeId !== changeId) {
|
|
1942
2366
|
throw new FetError({
|
|
@@ -2013,10 +2437,10 @@ function getModelPolicyMode(env = process.env) {
|
|
|
2013
2437
|
if (value === "off" || env.FET_SKIP_MODEL_POLICY === "1") {
|
|
2014
2438
|
return "off";
|
|
2015
2439
|
}
|
|
2016
|
-
if (value === "
|
|
2017
|
-
return "
|
|
2440
|
+
if (value === "warn") {
|
|
2441
|
+
return "warn";
|
|
2018
2442
|
}
|
|
2019
|
-
return "
|
|
2443
|
+
return "confirm";
|
|
2020
2444
|
}
|
|
2021
2445
|
function getCommandModelPolicyMismatch(command, env = process.env) {
|
|
2022
2446
|
if (getModelPolicyMode(env) === "off") {
|
|
@@ -2060,23 +2484,23 @@ function formatModelPolicyMismatch(mismatch, language = "zh-CN") {
|
|
|
2060
2484
|
function renderIdeModelPolicy(command, language = "zh-CN") {
|
|
2061
2485
|
if (language === "en") {
|
|
2062
2486
|
if (command === "apply") {
|
|
2063
|
-
return "Model policy: this command is recommended to run with a high-capability/high-cost model such as GPT-5.5, GLM-5.1, GLM-5, Claude Opus, or Claude Sonnet.
|
|
2487
|
+
return "Model policy: this command is recommended to run with a high-capability/high-cost model such as GPT-5.5, GLM-5.1, GLM-5, Claude Opus, or Claude Sonnet. If the current IDE model is lower-cost, tell the user and ask whether to stop for a model switch or continue anyway. Continue only after the user chooses.";
|
|
2064
2488
|
}
|
|
2065
|
-
return "Model policy: this command is recommended to run with a low-cost model. If the current IDE model is GPT-5.5, GLM-5.1, GLM-5, Claude Opus, Claude Sonnet, or another high-cost model,
|
|
2489
|
+
return "Model policy: this command is recommended to run with a low-cost model. If the current IDE model is GPT-5.5, GLM-5.1, GLM-5, Claude Opus, Claude Sonnet, or another high-cost model, tell the user and ask whether to stop for a model switch or continue anyway. Continue only after the user chooses.";
|
|
2066
2490
|
}
|
|
2067
2491
|
if (command === "apply") {
|
|
2068
|
-
return "\u6A21\u578B\u7B56\u7565\uFF1A\u8BE5\u547D\u4EE4\u5EFA\u8BAE\u4F7F\u7528\u9AD8\u80FD\u529B/\u9AD8\u6210\u672C\u6A21\u578B\u8FD0\u884C\uFF0C\u4F8B\u5982 GPT-5.5\u3001GLM-5.1\u3001GLM-5\u3001Claude Opus \u6216 Claude Sonnet\u3002\
|
|
2492
|
+
return "\u6A21\u578B\u7B56\u7565\uFF1A\u8BE5\u547D\u4EE4\u5EFA\u8BAE\u4F7F\u7528\u9AD8\u80FD\u529B/\u9AD8\u6210\u672C\u6A21\u578B\u8FD0\u884C\uFF0C\u4F8B\u5982 GPT-5.5\u3001GLM-5.1\u3001GLM-5\u3001Claude Opus \u6216 Claude Sonnet\u3002\u82E5\u5F53\u524D IDE \u6A21\u578B\u80FD\u529B\u8F83\u4F4E\uFF0C\u8BF7\u544A\u77E5\u7528\u6237\u5E76\u8BE2\u95EE\u662F\u505C\u6B62\u540E\u5207\u6362\u6A21\u578B\uFF0C\u8FD8\u662F\u7EE7\u7EED\u6267\u884C\u5F53\u524D\u547D\u4EE4\uFF1B\u53EA\u6709\u5728\u7528\u6237\u9009\u62E9\u540E\u624D\u7EE7\u7EED\u3002";
|
|
2069
2493
|
}
|
|
2070
|
-
return "\u6A21\u578B\u7B56\u7565\uFF1A\u8BE5\u547D\u4EE4\u5EFA\u8BAE\u4F7F\u7528\u4F4E\u6210\u672C\u6A21\u578B\u8FD0\u884C\u3002\u82E5\u5F53\u524D IDE \u6A21\u578B\u662F GPT-5.5\u3001GLM-5.1\u3001GLM-5\u3001Claude Opus\u3001Claude Sonnet \u6216\u5176\u4ED6\u9AD8\u6210\u672C\u6A21\u578B\uFF0C\
|
|
2494
|
+
return "\u6A21\u578B\u7B56\u7565\uFF1A\u8BE5\u547D\u4EE4\u5EFA\u8BAE\u4F7F\u7528\u4F4E\u6210\u672C\u6A21\u578B\u8FD0\u884C\u3002\u82E5\u5F53\u524D IDE \u6A21\u578B\u662F GPT-5.5\u3001GLM-5.1\u3001GLM-5\u3001Claude Opus\u3001Claude Sonnet \u6216\u5176\u4ED6\u9AD8\u6210\u672C\u6A21\u578B\uFF0C\u8BF7\u544A\u77E5\u7528\u6237\u5E76\u8BE2\u95EE\u662F\u505C\u6B62\u540E\u5207\u6362\u6A21\u578B\uFF0C\u8FD8\u662F\u7EE7\u7EED\u6267\u884C\u5F53\u524D\u547D\u4EE4\uFF1B\u53EA\u6709\u5728\u7528\u6237\u9009\u62E9\u540E\u624D\u7EE7\u7EED\u3002";
|
|
2071
2495
|
}
|
|
2072
2496
|
|
|
2073
2497
|
// src/cli/context.ts
|
|
2074
2498
|
import { resolve } from "path";
|
|
2075
2499
|
|
|
2076
2500
|
// src/adapters/codex/index.ts
|
|
2077
|
-
import { mkdir as
|
|
2501
|
+
import { mkdir as mkdir8, readFile as readFile13, stat as stat6 } from "fs/promises";
|
|
2078
2502
|
import { homedir } from "os";
|
|
2079
|
-
import { dirname as
|
|
2503
|
+
import { dirname as dirname8, join as join16 } from "path";
|
|
2080
2504
|
|
|
2081
2505
|
// src/adapters/commands.ts
|
|
2082
2506
|
var FET_WORKFLOW_COMMANDS = [
|
|
@@ -2107,18 +2531,9 @@ function renderFetAdapterUsage(command, args = "[...args]") {
|
|
|
2107
2531
|
|
|
2108
2532
|
// src/adapters/codex/templates.ts
|
|
2109
2533
|
function codexGuideFile(language = DEFAULT_LANGUAGE) {
|
|
2110
|
-
|
|
2111
|
-
path: ".codex/fet/context.md",
|
|
2112
|
-
content: `<!-- FET:MANAGED
|
|
2113
|
-
schemaVersion: 1
|
|
2114
|
-
fetVersion: ${FET_VERSION}
|
|
2115
|
-
generator: codex-adapter
|
|
2116
|
-
adapterVersion: 1
|
|
2117
|
-
FET:END -->
|
|
2534
|
+
const body = language === "en" ? `# FET For Codex
|
|
2118
2535
|
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
## \u8BED\u8A00
|
|
2536
|
+
## Language
|
|
2122
2537
|
|
|
2123
2538
|
${languageInstruction(language)}
|
|
2124
2539
|
|
|
@@ -2134,7 +2549,35 @@ If GitNexus code graph context is available in the IDE or MCP tools, prefer it b
|
|
|
2134
2549
|
Use the terminal command \`fet <command>\` as the source of truth for workflow transitions. These files are Codex-readable guidance; they do not register native slash commands.
|
|
2135
2550
|
|
|
2136
2551
|
Command guides live in .codex/fet/commands/.
|
|
2137
|
-
`
|
|
2552
|
+
` : `# Codex \u7684 FET \u4F7F\u7528\u6307\u5357
|
|
2553
|
+
|
|
2554
|
+
## \u8BED\u8A00
|
|
2555
|
+
|
|
2556
|
+
${languageInstruction(language)}
|
|
2557
|
+
|
|
2558
|
+
\u5728 Codex \u4E2D\u6267\u884C FET \u6216 OpenSpec \u5DE5\u4F5C\u524D\uFF0C\u5148\u9605\u8BFB\uFF1A
|
|
2559
|
+
|
|
2560
|
+
- AGENTS.md
|
|
2561
|
+
- openspec/config.yaml
|
|
2562
|
+
- .codex/fet/karpathy-guidelines.md
|
|
2563
|
+
- \u5982\u679C\u5DF2\u9009\u62E9 change\uFF0C\u9605\u8BFB openspec/changes/<change-id>/ \u4E0B\u7684\u5F53\u524D\u4EA7\u7269
|
|
2564
|
+
|
|
2565
|
+
\u5982\u679C IDE \u6216 MCP \u5DE5\u5177\u4E2D\u53EF\u7528 GitNexus \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\uFF0C\u5148\u7528\u5B83\u7F29\u5C0F\u4ED3\u5E93\u626B\u63CF\u8303\u56F4\uFF1B\u7528\u56FE\u8BC6\u522B\u76F8\u5173\u6A21\u5757\u3001\u4F9D\u8D56\u548C\u63D2\u5165\u70B9\uFF0C\u518D\u53EA\u8BFB\u53D6\u9700\u8981\u786E\u8BA4\u884C\u4E3A\u7684\u5177\u4F53\u6E90\u7801\u6587\u4EF6\u3002GitNexus \u4E0D\u53EF\u7528\u65F6\uFF0C\u6309\u666E\u901A FET/OpenSpec \u5DE5\u4F5C\u6D41\u7EE7\u7EED\u3002
|
|
2566
|
+
|
|
2567
|
+
\u5DE5\u4F5C\u6D41\u6D41\u8F6C\u4EE5\u7EC8\u7AEF\u547D\u4EE4 \`fet <command>\` \u4E3A\u51C6\u3002\u8FD9\u4E9B\u6587\u4EF6\u662F\u7ED9 Codex \u9605\u8BFB\u7684\u6307\u5BFC\uFF0C\u4E0D\u6CE8\u518C\u539F\u751F slash command\u3002
|
|
2568
|
+
|
|
2569
|
+
\u547D\u4EE4\u6307\u5357\u4F4D\u4E8E .codex/fet/commands/\u3002
|
|
2570
|
+
`;
|
|
2571
|
+
return {
|
|
2572
|
+
path: ".codex/fet/context.md",
|
|
2573
|
+
content: `<!-- FET:MANAGED
|
|
2574
|
+
schemaVersion: 1
|
|
2575
|
+
fetVersion: ${FET_VERSION}
|
|
2576
|
+
generator: codex-adapter
|
|
2577
|
+
adapterVersion: 1
|
|
2578
|
+
FET:END -->
|
|
2579
|
+
|
|
2580
|
+
${body}`
|
|
2138
2581
|
};
|
|
2139
2582
|
}
|
|
2140
2583
|
function codexCommandFiles(language = DEFAULT_LANGUAGE) {
|
|
@@ -2162,7 +2605,7 @@ generator: codex-adapter
|
|
|
2162
2605
|
adapterVersion: 1
|
|
2163
2606
|
FET:END -->
|
|
2164
2607
|
|
|
2165
|
-
# Andrej Karpathy Inspired Coding Guidelines
|
|
2608
|
+
# ${language === "en" ? "Andrej Karpathy Inspired Coding Guidelines" : "\u53D7 Andrej Karpathy \u542F\u53D1\u7684\u7F16\u7801\u6307\u5357"}
|
|
2166
2609
|
|
|
2167
2610
|
${renderKarpathyGuidelinesBody(language)}
|
|
2168
2611
|
`
|
|
@@ -2216,6 +2659,34 @@ After the command completes, report the important next steps from the FET output
|
|
|
2216
2659
|
function renderCommandZh(command) {
|
|
2217
2660
|
const usage = renderFetAdapterUsage(command, command === "fill-context" ? "" : command === "passthrough" ? "<openspec-command> [...args]" : "");
|
|
2218
2661
|
const title = commandTitleZh(command);
|
|
2662
|
+
if (command === "graph-setup") {
|
|
2663
|
+
return `<!-- FET:MANAGED
|
|
2664
|
+
schemaVersion: 1
|
|
2665
|
+
fetVersion: ${FET_VERSION}
|
|
2666
|
+
generator: codex-adapter
|
|
2667
|
+
adapterVersion: 1
|
|
2668
|
+
command: ${usage}
|
|
2669
|
+
FET:END -->
|
|
2670
|
+
|
|
2671
|
+
# ${usage}
|
|
2672
|
+
|
|
2673
|
+
${renderIdeModelPolicy(command, "zh-CN")}
|
|
2674
|
+
|
|
2675
|
+
${languageInstruction("zh-CN")}
|
|
2676
|
+
|
|
2677
|
+
\u7528\u4E8E\u6307\u5BFC\u5F53\u524D IDE LLM \u5728\u7528\u6237\u786E\u8BA4\u4E0B\u5B89\u88C5 GitNexus\u3002
|
|
2678
|
+
|
|
2679
|
+
\u5148\u8FD0\u884C\uFF1A
|
|
2680
|
+
|
|
2681
|
+
\`\`\`sh
|
|
2682
|
+
${usage}
|
|
2683
|
+
\`\`\`
|
|
2684
|
+
|
|
2685
|
+
\u7136\u540E\u9605\u8BFB .fet/graph-setup.md\uFF0C\u5E76\u4EE5\u5B83\u4F5C\u4E3A\u5B89\u88C5\u4EFB\u52A1\u4E66\u3002\u53EA\u8BFB\u68C0\u6D4B\u547D\u4EE4\u53EF\u4EE5\u76F4\u63A5\u8FD0\u884C\uFF1B\u4EFB\u4F55\u4F1A\u4E0B\u8F7D\u8F6F\u4EF6\u3001\u5168\u5C40\u5B89\u88C5\u3001\u4FEE\u6539 PATH\u3001\u5199\u5165\u7528\u6237\u7EA7\u6587\u4EF6\u6216\u4FEE\u6539 IDE/MCP \u914D\u7F6E\u7684\u547D\u4EE4\uFF0C\u6267\u884C\u524D\u90FD\u8981\u5C55\u793A\u5B8C\u6574\u547D\u4EE4\u5E76\u7B49\u5F85\u7528\u6237\u786E\u8BA4\u3002
|
|
2686
|
+
|
|
2687
|
+
\u5B89\u88C5\u540E\u7528 \`gitnexus --version\` \u9A8C\u8BC1\u3002\u5408\u9002\u65F6\u7EE7\u7EED\u8FD0\u884C \`fet graph init\` \u548C \`fet graph handoff\`\u3002\u5982\u679C\u5B89\u88C5\u5931\u8D25\uFF0C\u62A5\u544A\u5931\u8D25\u547D\u4EE4\u3001stderr \u548C\u4E0B\u4E00\u6B65\u4EBA\u5DE5\u5904\u7406\u5EFA\u8BAE\u3002
|
|
2688
|
+
`;
|
|
2689
|
+
}
|
|
2219
2690
|
return `<!-- FET:MANAGED
|
|
2220
2691
|
schemaVersion: 1
|
|
2221
2692
|
fetVersion: ${FET_VERSION}
|
|
@@ -2279,6 +2750,34 @@ This preserves the FET entry point while allowing access to unmanaged or newly a
|
|
|
2279
2750
|
}
|
|
2280
2751
|
function renderGraphCommand(command, language) {
|
|
2281
2752
|
const usage = renderFetAdapterUsage(command, "");
|
|
2753
|
+
if (command === "graph-setup") {
|
|
2754
|
+
return `<!-- FET:MANAGED
|
|
2755
|
+
schemaVersion: 1
|
|
2756
|
+
fetVersion: ${FET_VERSION}
|
|
2757
|
+
generator: codex-adapter
|
|
2758
|
+
adapterVersion: 1
|
|
2759
|
+
command: ${usage}
|
|
2760
|
+
FET:END -->
|
|
2761
|
+
|
|
2762
|
+
# ${usage}
|
|
2763
|
+
|
|
2764
|
+
${renderIdeModelPolicy(command, language)}
|
|
2765
|
+
|
|
2766
|
+
${languageInstruction(language)}
|
|
2767
|
+
|
|
2768
|
+
Use this command to guide IDE-assisted GitNexus installation with user approval.
|
|
2769
|
+
|
|
2770
|
+
Run:
|
|
2771
|
+
|
|
2772
|
+
\`\`\`sh
|
|
2773
|
+
${usage}
|
|
2774
|
+
\`\`\`
|
|
2775
|
+
|
|
2776
|
+
Then read .fet/graph-setup.md and follow it as the source of truth. You may run read-only detection commands directly. Before downloading software, installing globally, changing PATH, writing user-level files, or modifying IDE/MCP configuration, show the exact command and wait for user approval.
|
|
2777
|
+
|
|
2778
|
+
After installation, verify \`gitnexus --version\`. If appropriate, continue with \`fet graph init\` and \`fet graph handoff\`. If installation fails, report the failing command, stderr, and next manual step.
|
|
2779
|
+
`;
|
|
2780
|
+
}
|
|
2282
2781
|
const subcommand = command.slice("graph-".length);
|
|
2283
2782
|
return `<!-- FET:MANAGED
|
|
2284
2783
|
schemaVersion: 1
|
|
@@ -2351,6 +2850,9 @@ function renderSlashPrompt(command, language) {
|
|
|
2351
2850
|
if (command === "passthrough") {
|
|
2352
2851
|
return renderPassthroughSlashPrompt(language);
|
|
2353
2852
|
}
|
|
2853
|
+
if (command === "graph-setup") {
|
|
2854
|
+
return renderGraphSetupSlashPrompt(language);
|
|
2855
|
+
}
|
|
2354
2856
|
const usage = renderFetAdapterUsage(command);
|
|
2355
2857
|
const isGraph = command.startsWith("graph-");
|
|
2356
2858
|
const shellCommand = isGraph ? `${renderFetAdapterUsage(command, "")} $ARGUMENTS` : `fet ${command} $ARGUMENTS`;
|
|
@@ -2385,6 +2887,9 @@ After it completes, summarize the important FET output and next steps.
|
|
|
2385
2887
|
`;
|
|
2386
2888
|
}
|
|
2387
2889
|
function renderSlashPromptZh(command) {
|
|
2890
|
+
if (command === "graph-setup") {
|
|
2891
|
+
return renderGraphSetupSlashPrompt("zh-CN");
|
|
2892
|
+
}
|
|
2388
2893
|
const usage = renderFetAdapterUsage(command, command === "fill-context" ? "" : command === "passthrough" ? "<openspec-command> [...args]" : "[...args]");
|
|
2389
2894
|
const argumentHint = command === "passthrough" ? "openspec-command [...args]" : void 0;
|
|
2390
2895
|
const argumentHintLine = argumentHint ? `argument-hint: ${argumentHint}
|
|
@@ -2778,6 +3283,62 @@ Guardrails:
|
|
|
2778
3283
|
language
|
|
2779
3284
|
);
|
|
2780
3285
|
}
|
|
3286
|
+
function renderGraphSetupSlashPrompt(language) {
|
|
3287
|
+
if (language === "en") {
|
|
3288
|
+
return renderManagedSlashPrompt(
|
|
3289
|
+
"fet graph setup",
|
|
3290
|
+
"Guide IDE-assisted GitNexus installation with user approval",
|
|
3291
|
+
`Guide optional GitNexus installation for this project.
|
|
3292
|
+
|
|
3293
|
+
Steps:
|
|
3294
|
+
|
|
3295
|
+
1. Run:
|
|
3296
|
+
\`\`\`sh
|
|
3297
|
+
fet graph setup
|
|
3298
|
+
\`\`\`
|
|
3299
|
+
2. Read .fet/graph-setup.md and follow it as the source of truth.
|
|
3300
|
+
3. Run read-only detection commands as needed, such as checking the shell, PATH, package managers, and \`gitnexus --version\`.
|
|
3301
|
+
4. If GitNexus is missing, use the configured \`FET_GITNEXUS_INSTALL_COMMAND\` when present; otherwise find the official GitNexus installation instructions or ask the user for the preferred method.
|
|
3302
|
+
5. Before any command that downloads software, installs globally, changes PATH, writes user-level files, or modifies IDE/MCP settings, show the exact command and wait for user approval.
|
|
3303
|
+
6. Verify installation with \`gitnexus --version\`.
|
|
3304
|
+
7. If the user wants IDE/MCP integration, explain \`gitnexus setup\` and run it only after approval.
|
|
3305
|
+
8. Run \`fet graph init\` and then \`fet graph handoff\` when GitNexus is available.
|
|
3306
|
+
|
|
3307
|
+
Guardrails:
|
|
3308
|
+
- GitNexus is optional; do not block FET/OpenSpec workflows if installation fails.
|
|
3309
|
+
- Do not modify application code during setup.
|
|
3310
|
+
- Report the failing command, stderr, and next manual step if blocked.`,
|
|
3311
|
+
void 0,
|
|
3312
|
+
language
|
|
3313
|
+
);
|
|
3314
|
+
}
|
|
3315
|
+
return renderManagedSlashPrompt(
|
|
3316
|
+
"fet graph setup",
|
|
3317
|
+
"\u6307\u5BFC IDE LLM \u5728\u7528\u6237\u786E\u8BA4\u4E0B\u5B89\u88C5 GitNexus",
|
|
3318
|
+
`\u4E3A\u5F53\u524D\u9879\u76EE\u5F15\u5BFC\u53EF\u9009\u7684 GitNexus \u5B89\u88C5\u3002
|
|
3319
|
+
|
|
3320
|
+
\u6B65\u9AA4\uFF1A
|
|
3321
|
+
|
|
3322
|
+
1. \u8FD0\u884C\uFF1A
|
|
3323
|
+
\`\`\`sh
|
|
3324
|
+
fet graph setup
|
|
3325
|
+
\`\`\`
|
|
3326
|
+
2. \u9605\u8BFB .fet/graph-setup.md\uFF0C\u5E76\u4EE5\u5B83\u4F5C\u4E3A\u5B89\u88C5\u4EFB\u52A1\u4E66\u3002
|
|
3327
|
+
3. \u6309\u9700\u8FD0\u884C\u53EA\u8BFB\u68C0\u6D4B\u547D\u4EE4\uFF0C\u4F8B\u5982\u68C0\u67E5 shell\u3001PATH\u3001\u5305\u7BA1\u7406\u5668\u548C \`gitnexus --version\`\u3002
|
|
3328
|
+
4. \u5982\u679C GitNexus \u7F3A\u5931\uFF0C\u4F18\u5148\u4F7F\u7528\u5DF2\u914D\u7F6E\u7684 \`FET_GITNEXUS_INSTALL_COMMAND\`\uFF1B\u6CA1\u6709\u914D\u7F6E\u65F6\uFF0C\u67E5\u627E GitNexus \u5B98\u65B9\u5B89\u88C5\u8BF4\u660E\uFF0C\u6216\u8BE2\u95EE\u7528\u6237\u5E0C\u671B\u4F7F\u7528\u7684\u5B89\u88C5\u65B9\u5F0F\u3002
|
|
3329
|
+
5. \u4EFB\u4F55\u4F1A\u4E0B\u8F7D\u8F6F\u4EF6\u3001\u5168\u5C40\u5B89\u88C5\u3001\u4FEE\u6539 PATH\u3001\u5199\u5165\u7528\u6237\u7EA7\u6587\u4EF6\u6216\u4FEE\u6539 IDE/MCP \u8BBE\u7F6E\u7684\u547D\u4EE4\uFF0C\u6267\u884C\u524D\u90FD\u8981\u5C55\u793A\u5B8C\u6574\u547D\u4EE4\u5E76\u7B49\u5F85\u7528\u6237\u786E\u8BA4\u3002
|
|
3330
|
+
6. \u7528 \`gitnexus --version\` \u9A8C\u8BC1\u5B89\u88C5\u3002
|
|
3331
|
+
7. \u5982\u679C\u7528\u6237\u9700\u8981 IDE/MCP \u96C6\u6210\uFF0C\u5148\u8BF4\u660E \`gitnexus setup\`\uFF0C\u83B7\u5F97\u786E\u8BA4\u540E\u518D\u8FD0\u884C\u3002
|
|
3332
|
+
8. GitNexus \u53EF\u7528\u540E\u8FD0\u884C \`fet graph init\`\uFF0C\u518D\u8FD0\u884C \`fet graph handoff\`\u3002
|
|
3333
|
+
|
|
3334
|
+
\u7EA6\u675F\uFF1A
|
|
3335
|
+
- GitNexus \u662F\u53EF\u9009\u80FD\u529B\uFF1B\u5B89\u88C5\u5931\u8D25\u65F6\u4E0D\u8981\u963B\u585E FET/OpenSpec \u4E3B\u6D41\u7A0B\u3002
|
|
3336
|
+
- \u5B89\u88C5\u8FC7\u7A0B\u4E2D\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002
|
|
3337
|
+
- \u5982\u679C\u53D7\u963B\uFF0C\u62A5\u544A\u5931\u8D25\u547D\u4EE4\u3001stderr \u548C\u4E0B\u4E00\u6B65\u4EBA\u5DE5\u5904\u7406\u5EFA\u8BAE\u3002`,
|
|
3338
|
+
void 0,
|
|
3339
|
+
language
|
|
3340
|
+
);
|
|
3341
|
+
}
|
|
2781
3342
|
function renderExploreSlashPrompt(language) {
|
|
2782
3343
|
return renderManagedSlashPrompt(
|
|
2783
3344
|
"fet explore [...args]",
|
|
@@ -2897,6 +3458,7 @@ function renderManagedSlashPrompt(command, description, body, argumentHint, lang
|
|
|
2897
3458
|
const policyCommand = command.split(/\s+/)[1] ?? command;
|
|
2898
3459
|
const argumentHintLine = argumentHint ? `argument-hint: ${argumentHint}
|
|
2899
3460
|
` : "";
|
|
3461
|
+
const graphContextInstruction = language === "en" ? "If GitNexus graph context is available, consult it before broad source scans and use it to narrow the files you read. If it is unavailable, continue normally." : "\u5982\u679C GitNexus \u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587\u53EF\u7528\uFF0C\u5148\u7528\u5B83\u7F29\u5C0F\u6E90\u7801\u626B\u63CF\u8303\u56F4\uFF1B\u4E0D\u53EF\u7528\u65F6\u6309\u666E\u901A\u6D41\u7A0B\u7EE7\u7EED\u3002";
|
|
2900
3462
|
return `<!-- FET:MANAGED
|
|
2901
3463
|
schemaVersion: 1
|
|
2902
3464
|
fetVersion: ${FET_VERSION}
|
|
@@ -2913,7 +3475,7 @@ ${renderIdeModelPolicy(policyCommand, language)}
|
|
|
2913
3475
|
|
|
2914
3476
|
${languageInstruction(language)}
|
|
2915
3477
|
|
|
2916
|
-
|
|
3478
|
+
${graphContextInstruction}
|
|
2917
3479
|
|
|
2918
3480
|
${body}
|
|
2919
3481
|
`;
|
|
@@ -2925,7 +3487,7 @@ var CodexAdapter = class {
|
|
|
2925
3487
|
adapterVersion = 1;
|
|
2926
3488
|
async detect(projectRoot) {
|
|
2927
3489
|
return {
|
|
2928
|
-
detected: await exists3(
|
|
3490
|
+
detected: await exists3(join16(projectRoot, ".codex")) || await exists3(join16(projectRoot, "AGENTS.md")),
|
|
2929
3491
|
reason: "Codex adapter is available for projects that use AGENTS.md"
|
|
2930
3492
|
};
|
|
2931
3493
|
}
|
|
@@ -2964,7 +3526,7 @@ var CodexAdapter = class {
|
|
|
2964
3526
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
2965
3527
|
await createBackup(target);
|
|
2966
3528
|
}
|
|
2967
|
-
await
|
|
3529
|
+
await mkdir8(dirname8(target), { recursive: true });
|
|
2968
3530
|
await atomicWrite(target, file.content);
|
|
2969
3531
|
written.push(displayPath);
|
|
2970
3532
|
}
|
|
@@ -2991,9 +3553,9 @@ var CodexAdapter = class {
|
|
|
2991
3553
|
};
|
|
2992
3554
|
function resolveTarget(projectRoot, file) {
|
|
2993
3555
|
if (file.root === "codex-home") {
|
|
2994
|
-
return
|
|
3556
|
+
return join16(resolveCodexHome(), file.path);
|
|
2995
3557
|
}
|
|
2996
|
-
return
|
|
3558
|
+
return join16(projectRoot, file.path);
|
|
2997
3559
|
}
|
|
2998
3560
|
function displayPathFor(file) {
|
|
2999
3561
|
if (file.root === "codex-home") {
|
|
@@ -3002,11 +3564,11 @@ function displayPathFor(file) {
|
|
|
3002
3564
|
return file.path;
|
|
3003
3565
|
}
|
|
3004
3566
|
function resolveCodexHome() {
|
|
3005
|
-
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ??
|
|
3567
|
+
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join16(homedir(), ".codex");
|
|
3006
3568
|
}
|
|
3007
3569
|
async function readExisting(path) {
|
|
3008
3570
|
try {
|
|
3009
|
-
return await
|
|
3571
|
+
return await readFile13(path, "utf8");
|
|
3010
3572
|
} catch {
|
|
3011
3573
|
return null;
|
|
3012
3574
|
}
|
|
@@ -3021,8 +3583,8 @@ async function exists3(path) {
|
|
|
3021
3583
|
}
|
|
3022
3584
|
|
|
3023
3585
|
// src/adapters/cursor/index.ts
|
|
3024
|
-
import { mkdir as
|
|
3025
|
-
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";
|
|
3026
3588
|
|
|
3027
3589
|
// src/adapters/cursor/templates.ts
|
|
3028
3590
|
function cursorSkillFiles(language = DEFAULT_LANGUAGE) {
|
|
@@ -3088,6 +3650,36 @@ ${languageInstruction(language)}
|
|
|
3088
3650
|
- openspec/config.yaml
|
|
3089
3651
|
|
|
3090
3652
|
\u68C0\u67E5 README\u3001package scripts\u3001\u8DEF\u7531\u3001\u6D4B\u8BD5\u3001\u6E90\u7801\u7ED3\u6784\u548C\u73B0\u6709\u7EA6\u5B9A\u540E\uFF0C\u628A AGENTS.md \u4E2D\u6BCF\u4E2A \`[NEEDS LLM INPUT]\` \u6216 \`[NEED LLM INPUT]\` \u5360\u4F4D\u7B26\u66FF\u6362\u4E3A\u5177\u4F53\u3001\u7B80\u6D01\u3001\u9879\u76EE\u76F8\u5173\u7684\u5185\u5BB9\u3002\u4FDD\u7559 FET \u6258\u7BA1\u6807\u8BB0\uFF0C\u4E0D\u8981\u4FEE\u6539\u4E1A\u52A1\u4EE3\u7801\u3002
|
|
3653
|
+
`;
|
|
3654
|
+
}
|
|
3655
|
+
if (command === "graph-setup") {
|
|
3656
|
+
return `<!-- FET:MANAGED
|
|
3657
|
+
schemaVersion: 1
|
|
3658
|
+
fetVersion: ${FET_VERSION}
|
|
3659
|
+
generator: cursor-adapter
|
|
3660
|
+
adapterVersion: 1
|
|
3661
|
+
command: fet graph setup
|
|
3662
|
+
FET:END -->
|
|
3663
|
+
|
|
3664
|
+
---
|
|
3665
|
+
name: fet-graph-setup
|
|
3666
|
+
description: ${language === "en" ? "Guide IDE-assisted GitNexus installation with user approval" : "\u6307\u5BFC IDE LLM \u5728\u7528\u6237\u786E\u8BA4\u4E0B\u5B89\u88C5 GitNexus"}
|
|
3667
|
+
disable-model-invocation: false
|
|
3668
|
+
---
|
|
3669
|
+
|
|
3670
|
+
${renderIdeModelPolicy(command, language)}
|
|
3671
|
+
|
|
3672
|
+
${languageInstruction(language)}
|
|
3673
|
+
|
|
3674
|
+
${language === "en" ? `Run \`fet graph setup\`, then read .fet/graph-setup.md and follow it as an installation playbook.
|
|
3675
|
+
|
|
3676
|
+
You may run read-only detection commands without extra confirmation. Before any command that downloads software, installs globally, changes PATH, writes user-level config, or modifies IDE/MCP settings, show the exact command and wait for user approval.
|
|
3677
|
+
|
|
3678
|
+
After installation, verify with \`gitnexus --version\`, then run \`fet graph init\` and \`fet graph handoff\` when appropriate. If installation fails, summarize the failing command and next manual step.` : `\u5148\u8FD0\u884C \`fet graph setup\`\uFF0C\u518D\u9605\u8BFB .fet/graph-setup.md\uFF0C\u5E76\u628A\u5B83\u4F5C\u4E3A\u5B89\u88C5\u4EFB\u52A1\u4E66\u6267\u884C\u3002
|
|
3679
|
+
|
|
3680
|
+
\u53EA\u8BFB\u68C0\u6D4B\u547D\u4EE4\u53EF\u4EE5\u76F4\u63A5\u8FD0\u884C\u3002\u4EFB\u4F55\u4F1A\u4E0B\u8F7D\u8F6F\u4EF6\u3001\u5168\u5C40\u5B89\u88C5\u3001\u4FEE\u6539 PATH\u3001\u5199\u5165\u7528\u6237\u7EA7\u914D\u7F6E\u6216\u4FEE\u6539 IDE/MCP \u8BBE\u7F6E\u7684\u547D\u4EE4\uFF0C\u6267\u884C\u524D\u90FD\u8981\u5C55\u793A\u5B8C\u6574\u547D\u4EE4\u5E76\u7B49\u5F85\u7528\u6237\u786E\u8BA4\u3002
|
|
3681
|
+
|
|
3682
|
+
\u5B89\u88C5\u540E\u7528 \`gitnexus --version\` \u9A8C\u8BC1\uFF1B\u5408\u9002\u65F6\u7EE7\u7EED\u8FD0\u884C \`fet graph init\` \u548C \`fet graph handoff\`\u3002\u5982\u679C\u5B89\u88C5\u5931\u8D25\uFF0C\u6C47\u603B\u5931\u8D25\u547D\u4EE4\u548C\u4E0B\u4E00\u6B65\u4EBA\u5DE5\u5904\u7406\u5EFA\u8BAE\u3002`}
|
|
3091
3683
|
`;
|
|
3092
3684
|
}
|
|
3093
3685
|
return `<!-- FET:MANAGED
|
|
@@ -3126,7 +3718,7 @@ var CursorAdapter = class {
|
|
|
3126
3718
|
adapterVersion = 1;
|
|
3127
3719
|
async detect(projectRoot) {
|
|
3128
3720
|
return {
|
|
3129
|
-
detected: await exists4(
|
|
3721
|
+
detected: await exists4(join17(projectRoot, ".cursor")),
|
|
3130
3722
|
reason: "Cursor adapter is available for any project"
|
|
3131
3723
|
};
|
|
3132
3724
|
}
|
|
@@ -3143,7 +3735,7 @@ var CursorAdapter = class {
|
|
|
3143
3735
|
const written = [];
|
|
3144
3736
|
const skipped = [];
|
|
3145
3737
|
for (const file of plan.files) {
|
|
3146
|
-
const target =
|
|
3738
|
+
const target = join17(projectRoot, file.path);
|
|
3147
3739
|
const existing = await readExisting2(target);
|
|
3148
3740
|
if (existing && !existing.includes("FET:MANAGED") && !force) {
|
|
3149
3741
|
throw new FetError({
|
|
@@ -3156,7 +3748,7 @@ var CursorAdapter = class {
|
|
|
3156
3748
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
3157
3749
|
await createBackup(target);
|
|
3158
3750
|
}
|
|
3159
|
-
await
|
|
3751
|
+
await mkdir9(dirname9(target), { recursive: true });
|
|
3160
3752
|
await atomicWrite(target, file.content);
|
|
3161
3753
|
written.push(file.path);
|
|
3162
3754
|
}
|
|
@@ -3166,7 +3758,7 @@ var CursorAdapter = class {
|
|
|
3166
3758
|
const plan = await this.planInstall(projectRoot);
|
|
3167
3759
|
const checks = [];
|
|
3168
3760
|
for (const file of plan.files) {
|
|
3169
|
-
const target =
|
|
3761
|
+
const target = join17(projectRoot, file.path);
|
|
3170
3762
|
const content = await readExisting2(target);
|
|
3171
3763
|
const managed = Boolean(content?.includes("FET:MANAGED"));
|
|
3172
3764
|
const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
|
|
@@ -3182,7 +3774,7 @@ var CursorAdapter = class {
|
|
|
3182
3774
|
};
|
|
3183
3775
|
async function readExisting2(path) {
|
|
3184
3776
|
try {
|
|
3185
|
-
return await
|
|
3777
|
+
return await readFile14(path, "utf8");
|
|
3186
3778
|
} catch {
|
|
3187
3779
|
return null;
|
|
3188
3780
|
}
|
|
@@ -3201,13 +3793,13 @@ import { execFile as execFile4 } from "child_process";
|
|
|
3201
3793
|
import { promisify as promisify4 } from "util";
|
|
3202
3794
|
|
|
3203
3795
|
// src/openspec/inspector.ts
|
|
3204
|
-
import { readdir, stat as stat8 } from "fs/promises";
|
|
3205
|
-
import { join as
|
|
3796
|
+
import { readdir as readdir2, stat as stat8 } from "fs/promises";
|
|
3797
|
+
import { join as join18 } from "path";
|
|
3206
3798
|
async function inspectOpenSpecProject(projectRoot) {
|
|
3207
|
-
const openspecPath =
|
|
3208
|
-
const changesPath =
|
|
3209
|
-
const legacyArchivePath =
|
|
3210
|
-
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");
|
|
3211
3803
|
return {
|
|
3212
3804
|
exists: await exists5(openspecPath),
|
|
3213
3805
|
changes: await listDirectories(changesPath, { exclude: ["archive"] }),
|
|
@@ -3215,13 +3807,13 @@ async function inspectOpenSpecProject(projectRoot) {
|
|
|
3215
3807
|
};
|
|
3216
3808
|
}
|
|
3217
3809
|
async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
3218
|
-
const changePath =
|
|
3219
|
-
const tasksPath =
|
|
3220
|
-
const specsPath =
|
|
3810
|
+
const changePath = join18(projectRoot, "openspec", "changes", changeId);
|
|
3811
|
+
const tasksPath = join18(changePath, "tasks.md");
|
|
3812
|
+
const specsPath = join18(changePath, "specs");
|
|
3221
3813
|
return {
|
|
3222
3814
|
changeId,
|
|
3223
3815
|
exists: await exists5(changePath),
|
|
3224
|
-
hasProposal: await exists5(
|
|
3816
|
+
hasProposal: await exists5(join18(changePath, "proposal.md")),
|
|
3225
3817
|
hasTasks: await exists5(tasksPath),
|
|
3226
3818
|
hasSpecs: await exists5(specsPath),
|
|
3227
3819
|
tasksPath,
|
|
@@ -3230,7 +3822,7 @@ async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
|
3230
3822
|
}
|
|
3231
3823
|
async function listDirectories(path, options = {}) {
|
|
3232
3824
|
try {
|
|
3233
|
-
const entries = await
|
|
3825
|
+
const entries = await readdir2(path, { withFileTypes: true });
|
|
3234
3826
|
const excluded = new Set(options.exclude ?? []);
|
|
3235
3827
|
return entries.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).map((entry) => entry.name);
|
|
3236
3828
|
} catch {
|
|
@@ -3398,12 +3990,12 @@ function parseCommands(help) {
|
|
|
3398
3990
|
}
|
|
3399
3991
|
|
|
3400
3992
|
// src/scanner/package.ts
|
|
3401
|
-
import { readFile as
|
|
3402
|
-
import { join as
|
|
3993
|
+
import { readFile as readFile15, stat as stat9 } from "fs/promises";
|
|
3994
|
+
import { join as join19 } from "path";
|
|
3403
3995
|
import { parse as parse2 } from "yaml";
|
|
3404
3996
|
async function readPackageJson(projectRoot) {
|
|
3405
3997
|
try {
|
|
3406
|
-
return JSON.parse(await
|
|
3998
|
+
return JSON.parse(await readFile15(join19(projectRoot, "package.json"), "utf8"));
|
|
3407
3999
|
} catch {
|
|
3408
4000
|
return null;
|
|
3409
4001
|
}
|
|
@@ -3469,7 +4061,7 @@ function detectFramework(pkg) {
|
|
|
3469
4061
|
}
|
|
3470
4062
|
async function detectLanguage(projectRoot, pkg) {
|
|
3471
4063
|
const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
|
|
3472
|
-
if (deps.typescript || await exists6(
|
|
4064
|
+
if (deps.typescript || await exists6(join19(projectRoot, "tsconfig.json"))) {
|
|
3473
4065
|
return "typescript";
|
|
3474
4066
|
}
|
|
3475
4067
|
return "javascript";
|
|
@@ -3484,7 +4076,7 @@ async function detectWorkspaces(projectRoot, pkg) {
|
|
|
3484
4076
|
return packageWorkspaces;
|
|
3485
4077
|
}
|
|
3486
4078
|
try {
|
|
3487
|
-
const workspace = parse2(await
|
|
4079
|
+
const workspace = parse2(await readFile15(join19(projectRoot, "pnpm-workspace.yaml"), "utf8"));
|
|
3488
4080
|
return (workspace?.packages ?? []).map((path) => ({
|
|
3489
4081
|
name: path,
|
|
3490
4082
|
path,
|
|
@@ -3504,7 +4096,7 @@ async function detectLockManagers(projectRoot) {
|
|
|
3504
4096
|
];
|
|
3505
4097
|
const found = [];
|
|
3506
4098
|
for (const [file, manager] of lockFiles) {
|
|
3507
|
-
if (await exists6(
|
|
4099
|
+
if (await exists6(join19(projectRoot, file))) {
|
|
3508
4100
|
found.push(manager);
|
|
3509
4101
|
}
|
|
3510
4102
|
}
|
|
@@ -3529,13 +4121,13 @@ async function exists6(path) {
|
|
|
3529
4121
|
}
|
|
3530
4122
|
|
|
3531
4123
|
// src/scanner/routes.ts
|
|
3532
|
-
import { readdir as
|
|
3533
|
-
import { join as
|
|
4124
|
+
import { readdir as readdir3, stat as stat10 } from "fs/promises";
|
|
4125
|
+
import { join as join20, relative, sep } from "path";
|
|
3534
4126
|
async function scanRoutes(projectRoot) {
|
|
3535
4127
|
const candidates = ["src/routes", "src/pages", "app", "pages"];
|
|
3536
4128
|
const routes = [];
|
|
3537
4129
|
for (const candidate of candidates) {
|
|
3538
|
-
const root =
|
|
4130
|
+
const root = join20(projectRoot, candidate);
|
|
3539
4131
|
if (!await exists7(root)) {
|
|
3540
4132
|
continue;
|
|
3541
4133
|
}
|
|
@@ -3560,10 +4152,10 @@ function inferRoutePath(relativePath) {
|
|
|
3560
4152
|
return `/${withoutIndex}`.replace(/\/+/g, "/");
|
|
3561
4153
|
}
|
|
3562
4154
|
async function listFiles(root) {
|
|
3563
|
-
const entries = await
|
|
4155
|
+
const entries = await readdir3(root, { withFileTypes: true });
|
|
3564
4156
|
const files = [];
|
|
3565
4157
|
for (const entry of entries) {
|
|
3566
|
-
const path =
|
|
4158
|
+
const path = join20(root, entry.name);
|
|
3567
4159
|
if (entry.isDirectory()) {
|
|
3568
4160
|
files.push(...await listFiles(path));
|
|
3569
4161
|
} else {
|
|
@@ -3805,7 +4397,7 @@ function renderModelPolicyActionHint(policyMode, language) {
|
|
|
3805
4397
|
if (policyMode === "confirm") {
|
|
3806
4398
|
return language === "en" ? "Choose whether to continue this command or stop and switch models." : "\u8BF7\u9009\u62E9\u7EE7\u7EED\u6267\u884C\u672C\u547D\u4EE4\uFF0C\u6216\u505C\u6B62\u540E\u624B\u52A8\u5207\u6362\u6A21\u578B\u3002";
|
|
3807
4399
|
}
|
|
3808
|
-
return language === "en" ? "This is advisory; the command will continue.
|
|
4400
|
+
return language === "en" ? "This is advisory because FET_MODEL_POLICY=warn; the command will continue." : "\u5F53\u524D\u8BBE\u7F6E FET_MODEL_POLICY=warn\uFF0C\u8BE5\u63D0\u9192\u4EC5\u4F5C\u4E3A\u5EFA\u8BAE\uFF0C\u547D\u4EE4\u4F1A\u7EE7\u7EED\u6267\u884C\u3002";
|
|
3809
4401
|
}
|
|
3810
4402
|
async function warnIfContextPlaceholdersRemain(ctx) {
|
|
3811
4403
|
if (["init", "update-context", "fill-context", "doctor"].includes(ctx.command)) {
|