@nick848/fet 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2,15 +2,15 @@
2
2
  import {
3
3
  FetError,
4
4
  toFetError
5
- } from "../chunk-FZOVNHE7.js";
5
+ } from "../chunk-V4ZRBF5L.js";
6
6
 
7
7
  // src/cli/index.ts
8
8
  import { createInterface } from "readline/promises";
9
9
  import { Command } from "commander";
10
10
 
11
11
  // src/commands/init.ts
12
- import { readFile as readFile6, stat as stat2 } from "fs/promises";
13
- import { join as join7 } from "path";
12
+ import { readFile as readFile6, stat as stat3 } from "fs/promises";
13
+ import { join as join8 } from "path";
14
14
 
15
15
  // src/fs/atomic-write.ts
16
16
  import { dirname } from "path";
@@ -118,23 +118,26 @@ async function writeInitJournal(projectRoot, journal) {
118
118
 
119
119
  // src/gitnexus.ts
120
120
  import { execFile } from "child_process";
121
+ import { stat as stat2 } from "fs/promises";
122
+ import { join as join4 } from "path";
121
123
  import { promisify } from "util";
122
124
  var execFileAsync = promisify(execFile);
125
+ var DEFAULT_GRAPH_PATH = ".gitnexus";
123
126
  async function detectGitNexus(env = process.env) {
124
127
  const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
125
- const executable = env.FET_GITNEXUS_EXECUTABLE?.trim() || "gitnexus";
128
+ const command = resolveGitNexusCommand(env);
126
129
  try {
127
- const { stdout, stderr } = await execFileAsync(executable, ["--version"], { shell: process.platform === "win32" });
130
+ const { stdout, stderr } = await execFileAsync(command.file, [...command.args, "--version"], { shell: process.platform === "win32" });
128
131
  return {
129
132
  installed: true,
130
- executablePath: executable,
133
+ executablePath: command.label,
131
134
  version: (stdout.trim() || stderr.trim() || "unknown").split(/\r?\n/)[0] ?? "unknown",
132
135
  checkedAt
133
136
  };
134
137
  } catch (error) {
135
138
  return {
136
139
  installed: false,
137
- executablePath: executable,
140
+ executablePath: command.label,
138
141
  version: null,
139
142
  checkedAt,
140
143
  error: error instanceof Error ? error.message : String(error)
@@ -148,7 +151,66 @@ function toGitNexusState(detection, previous) {
148
151
  executablePath: detection.installed ? detection.executablePath : null,
149
152
  version: detection.version,
150
153
  checkedAt: detection.checkedAt,
151
- recommendationShownAt: previous?.recommendationShownAt ?? null
154
+ recommendationShownAt: previous?.recommendationShownAt ?? null,
155
+ graphPath: previous?.graphPath ?? null,
156
+ graphExists: previous?.graphExists ?? false,
157
+ lastIndexedAt: previous?.lastIndexedAt ?? null,
158
+ lastRefreshAt: previous?.lastRefreshAt ?? null,
159
+ lastStatus: previous?.lastStatus ?? null,
160
+ setupHandoffPath: previous?.setupHandoffPath ?? null,
161
+ setupHandoffUpdatedAt: previous?.setupHandoffUpdatedAt ?? null,
162
+ handoffPath: previous?.handoffPath ?? null,
163
+ handoffUpdatedAt: previous?.handoffUpdatedAt ?? null
164
+ };
165
+ }
166
+ async function inspectGitNexusGraph(projectRoot, env = process.env) {
167
+ const relative2 = env.FET_GITNEXUS_GRAPH_PATH?.trim() || DEFAULT_GRAPH_PATH;
168
+ const graphPath = join4(projectRoot, relative2);
169
+ try {
170
+ const info = await stat2(graphPath);
171
+ return {
172
+ graphPath: relative2,
173
+ graphExists: true,
174
+ lastIndexedAt: info.mtime.toISOString()
175
+ };
176
+ } catch {
177
+ return {
178
+ graphPath: relative2,
179
+ graphExists: false,
180
+ lastIndexedAt: null
181
+ };
182
+ }
183
+ }
184
+ async function runGitNexus(args, options) {
185
+ const command = resolveGitNexusCommand(options.env ?? process.env);
186
+ const fullCommand = [command.file, ...command.args, ...args];
187
+ try {
188
+ const { stdout, stderr } = await execFileAsync(command.file, [...command.args, ...args], {
189
+ cwd: options.cwd,
190
+ shell: process.platform === "win32"
191
+ });
192
+ return {
193
+ exitCode: 0,
194
+ stdout,
195
+ stderr,
196
+ command: fullCommand
197
+ };
198
+ } catch (error) {
199
+ const maybe = error;
200
+ return {
201
+ exitCode: typeof maybe.code === "number" ? maybe.code : 1,
202
+ stdout: maybe.stdout ?? "",
203
+ stderr: maybe.stderr ?? (error instanceof Error ? error.message : String(error)),
204
+ command: fullCommand
205
+ };
206
+ }
207
+ }
208
+ function mergeGitNexusGraphInfo(state, graph2) {
209
+ return {
210
+ ...state,
211
+ graphPath: graph2.graphPath,
212
+ graphExists: graph2.graphExists,
213
+ lastIndexedAt: graph2.lastIndexedAt
152
214
  };
153
215
  }
154
216
  function renderGitNexusRecommendation(state) {
@@ -157,17 +219,27 @@ function renderGitNexusRecommendation(state) {
157
219
  }
158
220
  return "Optional GitNexus code graph support is not installed. Consider installing GitNexus later to speed up OpenSpec artifact generation and improve code insertion context.";
159
221
  }
222
+ function resolveGitNexusCommand(env) {
223
+ const raw = env.FET_GITNEXUS_COMMAND?.trim() || env.FET_GITNEXUS_EXECUTABLE?.trim() || "gitnexus";
224
+ const parts = splitCommand(raw);
225
+ const [file = "gitnexus", ...args] = parts;
226
+ return { file, args, label: raw };
227
+ }
228
+ function splitCommand(value) {
229
+ const matches = value.match(/"[^"]+"|'[^']+'|\S+/g);
230
+ return (matches?.length ? matches : [value]).map((part) => part.replace(/^["']|["']$/g, ""));
231
+ }
160
232
 
161
233
  // src/version.ts
162
234
  import { existsSync, readFileSync } from "fs";
163
- import { dirname as dirname4, join as join4, parse } from "path";
235
+ import { dirname as dirname4, join as join5, parse } from "path";
164
236
  import { fileURLToPath } from "url";
165
237
  var FET_VERSION = readPackageVersion();
166
238
  function readPackageVersion() {
167
239
  let currentDir = dirname4(fileURLToPath(import.meta.url));
168
240
  const root = parse(currentDir).root;
169
241
  while (true) {
170
- const packageJsonPath = join4(currentDir, "package.json");
242
+ const packageJsonPath = join5(currentDir, "package.json");
171
243
  if (existsSync(packageJsonPath)) {
172
244
  const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
173
245
  if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
@@ -377,7 +449,8 @@ var RULES = [
377
449
  "openspec/.fet.lock",
378
450
  "openspec/.fet-init-journal.json",
379
451
  "openspec/changes/*/fet-state.json",
380
- "openspec/changes/*/.fet/"
452
+ "openspec/changes/*/.fet/",
453
+ ".gitnexus/"
381
454
  ];
382
455
  function mergeGitignore(existing) {
383
456
  const block = `${BEGIN}
@@ -400,7 +473,7 @@ ${block}
400
473
 
401
474
  // src/commands/update-context.ts
402
475
  import { readFile as readFile5 } from "fs/promises";
403
- import { join as join6 } from "path";
476
+ import { join as join7 } from "path";
404
477
 
405
478
  // src/config/yaml.ts
406
479
  import { readFile as readFile3 } from "fs/promises";
@@ -421,11 +494,11 @@ async function mergeFetConfig(configPath, renderedFetYaml) {
421
494
 
422
495
  // src/context-placeholders.ts
423
496
  import { readFile as readFile4 } from "fs/promises";
424
- import { join as join5 } from "path";
497
+ import { join as join6 } from "path";
425
498
  var AGENTS_LLM_PLACEHOLDER_PATTERN = /\[NEEDS? LLM INPUT\]/g;
426
499
  async function countAgentsLlmPlaceholders(projectRoot) {
427
500
  try {
428
- const content = await readFile4(join5(projectRoot, "AGENTS.md"), "utf8");
501
+ const content = await readFile4(join6(projectRoot, "AGENTS.md"), "utf8");
429
502
  return [...content.matchAll(AGENTS_LLM_PLACEHOLDER_PATTERN)].length;
430
503
  } catch {
431
504
  return 0;
@@ -454,8 +527,8 @@ async function updateContextCommand(ctx) {
454
527
  }
455
528
  async function updateContextFiles(ctx) {
456
529
  const scan = await ctx.scanner.scan(ctx.projectRoot, {});
457
- const agentsPath = join6(ctx.projectRoot, "AGENTS.md");
458
- const configPath = join6(ctx.projectRoot, "openspec", "config.yaml");
530
+ const agentsPath = join7(ctx.projectRoot, "AGENTS.md");
531
+ const configPath = join7(ctx.projectRoot, "openspec", "config.yaml");
459
532
  const existingAgents = await readOptional(agentsPath);
460
533
  const warnings = [...scan.warnings];
461
534
  if (existingAgents && hasInvalidManagedAutoRegion(existingAgents)) {
@@ -505,7 +578,7 @@ async function readOptional(path) {
505
578
 
506
579
  // src/commands/init.ts
507
580
  async function initCommand(ctx) {
508
- const alreadyInitialized = await exists(join7(ctx.projectRoot, "openspec", "config.yaml"));
581
+ const alreadyInitialized = await exists(join8(ctx.projectRoot, "openspec", "config.yaml"));
509
582
  let warnings = [];
510
583
  await withProjectLock(
511
584
  ctx.projectRoot,
@@ -527,7 +600,10 @@ async function initCommand(ctx) {
527
600
  const state = await ctx.stateStore.getOrCreateGlobal();
528
601
  state.openspec = identity;
529
602
  state.graph ??= {};
530
- const gitnexus = toGitNexusState(await detectGitNexus(), state.graph.gitnexus);
603
+ const gitnexus = mergeGitNexusGraphInfo(
604
+ toGitNexusState(await detectGitNexus(), state.graph.gitnexus),
605
+ await inspectGitNexusGraph(ctx.projectRoot)
606
+ );
531
607
  if (!gitnexus.installed && !gitnexus.recommendationShownAt) {
532
608
  warnings.push(renderGitNexusRecommendation(gitnexus));
533
609
  gitnexus.recommendationShownAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -557,7 +633,7 @@ async function initCommand(ctx) {
557
633
  });
558
634
  }
559
635
  async function ensureGitignore(ctx) {
560
- const gitignorePath = join7(ctx.projectRoot, ".gitignore");
636
+ const gitignorePath = join8(ctx.projectRoot, ".gitignore");
561
637
  const existing = await readOptional2(gitignorePath);
562
638
  await atomicWrite(gitignorePath, mergeGitignore(existing));
563
639
  }
@@ -570,7 +646,7 @@ async function readOptional2(path) {
570
646
  }
571
647
  async function exists(path) {
572
648
  try {
573
- await stat2(path);
649
+ await stat3(path);
574
650
  return true;
575
651
  } catch {
576
652
  return false;
@@ -578,20 +654,20 @@ async function exists(path) {
578
654
  }
579
655
 
580
656
  // src/commands/doctor.ts
581
- import { readFile as readFile7, stat as stat3 } from "fs/promises";
582
- import { join as join8 } from "path";
657
+ import { readFile as readFile7, stat as stat4 } from "fs/promises";
658
+ import { join as join9 } from "path";
583
659
  async function doctorCommand(ctx, options = {}) {
584
660
  const checks = [];
585
661
  checks.push(await checkOpenSpec(ctx));
586
662
  checks.push(await checkState(ctx));
587
- checks.push(await checkFile("agents", join8(ctx.projectRoot, "AGENTS.md"), "AGENTS.md \u7F3A\u5931", "fet update-context"));
588
- checks.push(await checkFile("config", join8(ctx.projectRoot, "openspec", "config.yaml"), "openspec/config.yaml \u7F3A\u5931", "fet init"));
663
+ checks.push(await checkFile("agents", join9(ctx.projectRoot, "AGENTS.md"), "AGENTS.md \u7F3A\u5931", "fet update-context"));
664
+ checks.push(await checkFile("config", join9(ctx.projectRoot, "openspec", "config.yaml"), "openspec/config.yaml \u7F3A\u5931", "fet init"));
589
665
  checks.push(await checkPlaceholders(ctx.projectRoot));
590
666
  checks.push(await checkGitNexus(ctx));
591
667
  for (const adapter of ctx.toolAdapters) {
592
668
  checks.push(...await adapter.doctor(ctx.projectRoot));
593
669
  }
594
- const lockPath = join8(ctx.projectRoot, "openspec", ".fet.lock");
670
+ const lockPath = join9(ctx.projectRoot, "openspec", ".fet.lock");
595
671
  if (await exists2(lockPath)) {
596
672
  if (options.fixLock) {
597
673
  await clearLock(ctx.projectRoot);
@@ -611,7 +687,10 @@ async function doctorCommand(ctx, options = {}) {
611
687
  }
612
688
  async function checkGitNexus(ctx) {
613
689
  const global = await ctx.stateStore.readGlobal();
614
- const state = toGitNexusState(await detectGitNexus(), global?.graph?.gitnexus);
690
+ const state = mergeGitNexusGraphInfo(
691
+ toGitNexusState(await detectGitNexus(), global?.graph?.gitnexus),
692
+ await inspectGitNexusGraph(ctx.projectRoot)
693
+ );
615
694
  if (global) {
616
695
  global.graph ??= {};
617
696
  global.graph.gitnexus = state;
@@ -620,7 +699,7 @@ async function checkGitNexus(ctx) {
620
699
  return state.installed ? {
621
700
  id: "gitnexus",
622
701
  status: "pass",
623
- message: `GitNexus detected: ${state.executablePath ?? "gitnexus"} (${state.version ?? "unknown"})`
702
+ message: `GitNexus detected: ${state.executablePath ?? "gitnexus"} (${state.version ?? "unknown"}), graph ${state.graphExists ? "found" : "not found"}`
624
703
  } : {
625
704
  id: "gitnexus",
626
705
  status: "warn",
@@ -649,7 +728,7 @@ async function checkFile(id, path, missing, suggestedCommand) {
649
728
  }
650
729
  async function checkPlaceholders(projectRoot) {
651
730
  try {
652
- await readFile7(join8(projectRoot, "AGENTS.md"), "utf8");
731
+ await readFile7(join9(projectRoot, "AGENTS.md"), "utf8");
653
732
  const count2 = await countAgentsLlmPlaceholders(projectRoot);
654
733
  return count2 ? {
655
734
  id: "context-placeholders",
@@ -663,7 +742,7 @@ async function checkPlaceholders(projectRoot) {
663
742
  }
664
743
  async function exists2(path) {
665
744
  try {
666
- await stat3(path);
745
+ await stat4(path);
667
746
  return true;
668
747
  } catch {
669
748
  return false;
@@ -672,13 +751,13 @@ async function exists2(path) {
672
751
 
673
752
  // src/commands/fill-context.ts
674
753
  import { mkdir as mkdir3 } from "fs/promises";
675
- import { dirname as dirname5, join as join9 } from "path";
754
+ import { dirname as dirname5, join as join10 } from "path";
676
755
  async function fillContextCommand(ctx) {
677
756
  await withProjectLock(
678
757
  ctx.projectRoot,
679
758
  { command: "fill-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
680
759
  async () => {
681
- const handoffPath = join9(ctx.projectRoot, ".fet", "fill-context.md");
760
+ const handoffPath = join10(ctx.projectRoot, ".fet", "fill-context.md");
682
761
  await mkdir3(dirname5(handoffPath), { recursive: true });
683
762
  await atomicWrite(handoffPath, renderGenericHandoff());
684
763
  for (const adapter of ctx.toolAdapters) {
@@ -733,9 +812,259 @@ Use the IDE AI to complete FET-generated placeholders.
733
812
  `;
734
813
  }
735
814
 
815
+ // src/commands/graph.ts
816
+ import { mkdir as mkdir4 } from "fs/promises";
817
+ import { dirname as dirname6, join as join11 } from "path";
818
+ async function graphCommand(ctx, action, args = []) {
819
+ switch (action) {
820
+ case "status":
821
+ await graphStatusCommand(ctx);
822
+ return;
823
+ case "doctor":
824
+ await graphDoctorCommand(ctx);
825
+ return;
826
+ case "setup":
827
+ await graphSetupCommand(ctx);
828
+ return;
829
+ case "handoff":
830
+ await graphHandoffCommand(ctx);
831
+ return;
832
+ case "init":
833
+ await graphAnalyzeCommand(ctx, "init", args);
834
+ return;
835
+ case "refresh":
836
+ await graphAnalyzeCommand(ctx, "refresh", args);
837
+ return;
838
+ }
839
+ }
840
+ async function graphStatusCommand(ctx) {
841
+ const result = await refreshGraphState(ctx, { runStatus: true });
842
+ const warnings = result.state.installed ? [] : ["GitNexus is not installed. Run fet graph setup for installation handoff instructions."];
843
+ ctx.output.result({
844
+ ok: true,
845
+ command: "graph status",
846
+ 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.",
847
+ warnings,
848
+ nextSteps: result.state.installed && !result.state.graphExists ? ["Run fet graph init to build the first GitNexus graph"] : void 0,
849
+ data: result
850
+ });
851
+ }
852
+ async function graphDoctorCommand(ctx) {
853
+ const result = await refreshGraphState(ctx, { runStatus: true });
854
+ const warnings = [
855
+ ...!result.state.installed ? ["GitNexus is not installed."] : [],
856
+ ...result.state.installed && !result.state.graphExists ? ["GitNexus is installed but no graph directory was found."] : [],
857
+ ...!result.state.handoffPath ? ["Graph handoff instructions have not been generated."] : []
858
+ ];
859
+ ctx.output.result({
860
+ ok: true,
861
+ command: "graph doctor",
862
+ summary: warnings.length ? `Graph doctor completed with ${warnings.length} warning(s).` : "Graph doctor completed without warnings.",
863
+ warnings,
864
+ nextSteps: warnings.length ? ["Run fet graph setup", "Run fet graph handoff", "Run fet graph init when GitNexus is installed"] : void 0,
865
+ data: result
866
+ });
867
+ }
868
+ async function graphSetupCommand(ctx) {
869
+ let result;
870
+ const handoffPath = join11(ctx.projectRoot, ".fet", "graph-setup.md");
871
+ await withProjectLock(ctx.projectRoot, { command: "graph setup", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
872
+ result = await refreshGraphState(ctx, { write: false });
873
+ await writeHandoffFile(handoffPath, renderGraphSetupHandoff(result.state));
874
+ const global = await ctx.stateStore.getOrCreateGlobal();
875
+ global.graph ??= {};
876
+ global.graph.gitnexus = {
877
+ ...result.state,
878
+ setupHandoffPath: ".fet/graph-setup.md",
879
+ setupHandoffUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
880
+ };
881
+ await ctx.stateStore.writeGlobal(global);
882
+ });
883
+ ctx.output.result({
884
+ ok: true,
885
+ command: "graph setup",
886
+ summary: "GitNexus setup handoff generated.",
887
+ warnings: result.state.installed ? [] : ["GitNexus is not installed. The handoff explains installation and IDE-assisted setup options."],
888
+ nextSteps: result.state.installed ? ["Run gitnexus setup if you want to configure IDE/MCP integrations", "Run fet graph init"] : ["Open .fet/graph-setup.md in your IDE AI"],
889
+ data: {
890
+ path: ".fet/graph-setup.md",
891
+ gitnexus: result.state
892
+ }
893
+ });
894
+ }
895
+ async function graphHandoffCommand(ctx) {
896
+ let result;
897
+ const handoffPath = join11(ctx.projectRoot, ".fet", "graph-handoff.md");
898
+ await withProjectLock(ctx.projectRoot, { command: "graph handoff", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
899
+ result = await refreshGraphState(ctx, { runStatus: true, write: false });
900
+ await writeHandoffFile(handoffPath, renderGraphUsageHandoff(result.state));
901
+ const global = await ctx.stateStore.getOrCreateGlobal();
902
+ global.graph ??= {};
903
+ global.graph.gitnexus = {
904
+ ...result.state,
905
+ handoffPath: ".fet/graph-handoff.md",
906
+ handoffUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
907
+ };
908
+ await ctx.stateStore.writeGlobal(global);
909
+ });
910
+ ctx.output.result({
911
+ ok: true,
912
+ command: "graph handoff",
913
+ summary: "GitNexus graph usage handoff generated.",
914
+ warnings: result.state.installed ? [] : ["GitNexus is not installed. The handoff still documents the fallback behavior."],
915
+ nextSteps: ["Cursor/Codex/OpenCode: read .fet/graph-handoff.md before broad repository scans"],
916
+ data: {
917
+ path: ".fet/graph-handoff.md",
918
+ gitnexus: result.state
919
+ }
920
+ });
921
+ }
922
+ async function graphAnalyzeCommand(ctx, mode, args) {
923
+ const detection = await detectGitNexus();
924
+ if (!detection.installed) {
925
+ throw new FetError({
926
+ code: "GRAPH_PROVIDER_NOT_FOUND" /* GraphProviderNotFound */,
927
+ message: "GitNexus is not installed or is not available on PATH.",
928
+ details: { executable: detection.executablePath, error: detection.error },
929
+ suggestedCommand: "fet graph setup"
930
+ });
931
+ }
932
+ const run = await runGitNexus(["analyze", ...args], { cwd: ctx.projectRoot });
933
+ if (run.exitCode !== 0) {
934
+ throw new FetError({
935
+ code: "GRAPH_COMMAND_FAILED" /* GraphCommandFailed */,
936
+ message: "GitNexus analyze failed.",
937
+ details: { command: run.command.join(" "), exitCode: run.exitCode, stdout: run.stdout, stderr: run.stderr },
938
+ suggestedCommand: "fet graph doctor"
939
+ });
940
+ }
941
+ const result = await refreshGraphState(ctx, { write: false });
942
+ const global = await ctx.stateStore.getOrCreateGlobal();
943
+ global.graph ??= {};
944
+ global.graph.gitnexus = {
945
+ ...result.state,
946
+ lastRefreshAt: (/* @__PURE__ */ new Date()).toISOString()
947
+ };
948
+ await ctx.stateStore.writeGlobal(global);
949
+ ctx.output.result({
950
+ ok: true,
951
+ command: `graph ${mode}`,
952
+ summary: mode === "init" ? "GitNexus graph initialized." : "GitNexus graph refreshed.",
953
+ warnings: result.state.graphExists ? [] : ["GitNexus analyze completed, but the configured graph directory was not found."],
954
+ nextSteps: ["Run fet graph status", "Use .fet/graph-handoff.md or generated IDE prompts to prefer graph context"],
955
+ data: {
956
+ gitnexus: global.graph.gitnexus,
957
+ run: {
958
+ command: run.command,
959
+ stdout: run.stdout.trim(),
960
+ stderr: run.stderr.trim()
961
+ }
962
+ }
963
+ });
964
+ }
965
+ async function refreshGraphState(ctx, options = {}) {
966
+ const global = await ctx.stateStore.getOrCreateGlobal();
967
+ global.graph ??= {};
968
+ const detection = await detectGitNexus();
969
+ const graph2 = await inspectGitNexusGraph(ctx.projectRoot);
970
+ let state = mergeGitNexusGraphInfo(toGitNexusState(detection, global.graph.gitnexus), graph2);
971
+ let gitnexusStatus = null;
972
+ if (options.runStatus && detection.installed) {
973
+ gitnexusStatus = await runGitNexus(["status"], { cwd: ctx.projectRoot });
974
+ state = {
975
+ ...state,
976
+ lastStatus: firstLine(gitnexusStatus.stdout) || firstLine(gitnexusStatus.stderr) || `exit ${gitnexusStatus.exitCode}`
977
+ };
978
+ }
979
+ if (options.write ?? true) {
980
+ global.graph.gitnexus = state;
981
+ await ctx.stateStore.writeGlobal(global);
982
+ }
983
+ return {
984
+ state,
985
+ gitnexusStatus: gitnexusStatus ? {
986
+ exitCode: gitnexusStatus.exitCode,
987
+ command: gitnexusStatus.command,
988
+ stdout: gitnexusStatus.stdout.trim(),
989
+ stderr: gitnexusStatus.stderr.trim()
990
+ } : null
991
+ };
992
+ }
993
+ async function writeHandoffFile(path, content) {
994
+ await mkdir4(dirname6(path), { recursive: true });
995
+ await atomicWrite(path, content);
996
+ }
997
+ function renderGraphSetupHandoff(state) {
998
+ return `<!-- FET:MANAGED
999
+ schemaVersion: 1
1000
+ generator: graph-setup
1001
+ FET:END -->
1002
+
1003
+ # FET Graph Setup
1004
+
1005
+ GitNexus graph support is optional. FET does not install GitNexus automatically and does not require graph support for OpenSpec workflows.
1006
+
1007
+ Current status:
1008
+
1009
+ - Installed: ${state.installed ? "yes" : "no"}
1010
+ - Executable: ${state.executablePath ?? "gitnexus"}
1011
+ - Version: ${state.version ?? "unknown"}
1012
+ - Graph path: ${state.graphPath ?? ".gitnexus"}
1013
+ - Graph exists: ${state.graphExists ? "yes" : "no"}
1014
+
1015
+ Suggested setup flow:
1016
+
1017
+ 1. If GitNexus is not installed, install it using the method recommended by the GitNexus project.
1018
+ 2. If you want GitNexus MCP or IDE integration, run \`gitnexus setup\` yourself after reviewing what it changes.
1019
+ 3. Return to this project and run \`fet graph init\` to build the first graph.
1020
+ 4. Run \`fet graph handoff\` so IDE AI can prefer graph context before broad repository scans.
1021
+
1022
+ Guardrails:
1023
+
1024
+ - Do not block FET/OpenSpec commands when GitNexus is unavailable.
1025
+ - Do not generate or modify application code during setup.
1026
+ - Do not run global IDE configuration commands unless the user explicitly approves them.
1027
+ `;
1028
+ }
1029
+ function renderGraphUsageHandoff(state) {
1030
+ return `<!-- FET:MANAGED
1031
+ schemaVersion: 1
1032
+ generator: graph-handoff
1033
+ FET:END -->
1034
+
1035
+ # FET Graph Handoff
1036
+
1037
+ Use GitNexus graph context as an optional first pass before broad repository scans.
1038
+
1039
+ Current status:
1040
+
1041
+ - Installed: ${state.installed ? "yes" : "no"}
1042
+ - Graph path: ${state.graphPath ?? ".gitnexus"}
1043
+ - Graph exists: ${state.graphExists ? "yes" : "no"}
1044
+ - Last indexed at: ${state.lastIndexedAt ?? "unknown"}
1045
+ - Last status: ${state.lastStatus ?? "unknown"}
1046
+
1047
+ When graph context is available:
1048
+
1049
+ 1. Use the graph to identify likely modules, dependencies, and insertion points.
1050
+ 2. Read only the concrete source files needed to confirm behavior.
1051
+ 3. Prefer OpenSpec artifacts and AGENTS.md over graph guesses when they conflict.
1052
+ 4. Fall back to normal repository inspection if the graph is missing, stale, or incomplete.
1053
+
1054
+ When producing OpenSpec artifacts:
1055
+
1056
+ - Use graph context to make proposal, design, specs, and tasks more precise.
1057
+ - Avoid large repository scans when the graph already narrows the relevant area.
1058
+ - Keep all generated artifacts in the normal OpenSpec change directory.
1059
+ `;
1060
+ }
1061
+ function firstLine(value) {
1062
+ return value.trim().split(/\r?\n/)[0]?.trim() || null;
1063
+ }
1064
+
736
1065
  // src/commands/proxy.ts
737
1066
  import { readFile as readFile10 } from "fs/promises";
738
- import { join as join11 } from "path";
1067
+ import { join as join13 } from "path";
739
1068
 
740
1069
  // src/state/project.ts
741
1070
  import { execFile as execFile2 } from "child_process";
@@ -764,8 +1093,8 @@ async function git(cwd, args) {
764
1093
  }
765
1094
 
766
1095
  // src/state/store.ts
767
- import { mkdir as mkdir4, readFile as readFile8 } from "fs/promises";
768
- import { join as join10 } from "path";
1096
+ import { mkdir as mkdir5, readFile as readFile8 } from "fs/promises";
1097
+ import { join as join12 } from "path";
769
1098
 
770
1099
  // src/state/schema.ts
771
1100
  var phases = ["explore", "propose", "implement", "verify", "sync", "archive"];
@@ -874,7 +1203,7 @@ var StateStore = class {
874
1203
  }
875
1204
  async writeGlobal(state) {
876
1205
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
877
- await mkdir4(join10(this.projectRoot, "openspec"), { recursive: true });
1206
+ await mkdir5(join12(this.projectRoot, "openspec"), { recursive: true });
878
1207
  await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
879
1208
  `);
880
1209
  }
@@ -895,15 +1224,15 @@ var StateStore = class {
895
1224
  }
896
1225
  async writeChange(state) {
897
1226
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
898
- await mkdir4(join10(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
1227
+ await mkdir5(join12(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
899
1228
  await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
900
1229
  `);
901
1230
  }
902
1231
  globalPath() {
903
- return join10(this.projectRoot, "openspec", "fet-state.json");
1232
+ return join12(this.projectRoot, "openspec", "fet-state.json");
904
1233
  }
905
1234
  changePath(changeId) {
906
- return join10(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
1235
+ return join12(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
907
1236
  }
908
1237
  };
909
1238
  function isNotFound(error) {
@@ -1020,7 +1349,7 @@ async function createChangelogEntry(projectRoot, changeId) {
1020
1349
  };
1021
1350
  }
1022
1351
  async function appendChangelog(projectRoot, entry) {
1023
- const changelogPath = join11(projectRoot, "CHANGELOG.md");
1352
+ const changelogPath = join13(projectRoot, "CHANGELOG.md");
1024
1353
  const existing = await readOptional3(changelogPath);
1025
1354
  const block = `updateTime: ${entry.updateTime}
1026
1355
  \u66F4\u65B0\u5185\u5BB9:${entry.content}
@@ -1031,12 +1360,12 @@ ${block}` : block;
1031
1360
  await atomicWrite(changelogPath, next);
1032
1361
  }
1033
1362
  async function readChangeRequirement(projectRoot, changeId) {
1034
- const changeRoot = join11(projectRoot, "openspec", "changes", changeId);
1035
- const proposal = await readOptional3(join11(changeRoot, "proposal.md"));
1363
+ const changeRoot = join13(projectRoot, "openspec", "changes", changeId);
1364
+ const proposal = await readOptional3(join13(changeRoot, "proposal.md"));
1036
1365
  if (proposal) {
1037
1366
  return summarizeMarkdown(proposal);
1038
1367
  }
1039
- const readme = await readOptional3(join11(changeRoot, "README.md"));
1368
+ const readme = await readOptional3(join13(changeRoot, "README.md"));
1040
1369
  if (readme) {
1041
1370
  return summarizeMarkdown(readme);
1042
1371
  }
@@ -1180,8 +1509,8 @@ async function assertVerified(ctx) {
1180
1509
 
1181
1510
  // src/commands/verify.ts
1182
1511
  import { createHash } from "crypto";
1183
- import { mkdir as mkdir5, readFile as readFile11, stat as stat4 } from "fs/promises";
1184
- import { join as join12 } from "path";
1512
+ import { mkdir as mkdir6, readFile as readFile11, stat as stat5 } from "fs/promises";
1513
+ import { join as join14 } from "path";
1185
1514
  async function verifyCommand(ctx, options) {
1186
1515
  if (options.auto) {
1187
1516
  const scan = await ctx.scanner.scan(ctx.projectRoot, {});
@@ -1248,9 +1577,9 @@ async function verifyCommand(ctx, options) {
1248
1577
  async function writeInstructions(ctx, changeId) {
1249
1578
  await assertChangeExists(ctx, changeId);
1250
1579
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
1251
- const dir = join12(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
1252
- const instructionsPath = join12(dir, "verify-instructions.md");
1253
- await mkdir5(dir, { recursive: true });
1580
+ const dir = join14(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
1581
+ const instructionsPath = join14(dir, "verify-instructions.md");
1582
+ await mkdir6(dir, { recursive: true });
1254
1583
  await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
1255
1584
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
1256
1585
  state.currentPhase = "verify";
@@ -1266,7 +1595,7 @@ async function writeInstructions(ctx, changeId) {
1266
1595
  async function markDone(ctx, changeId) {
1267
1596
  await assertChangeExists(ctx, changeId);
1268
1597
  const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
1269
- const instructionsPath = join12(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
1598
+ const instructionsPath = join14(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
1270
1599
  const instructions = await readInstructions(instructionsPath, changeId);
1271
1600
  const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
1272
1601
  const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
@@ -1301,7 +1630,7 @@ async function assertChangeExists(ctx, changeId) {
1301
1630
  }
1302
1631
  async function readInstructions(path, changeId) {
1303
1632
  try {
1304
- await stat4(path);
1633
+ await stat5(path);
1305
1634
  const content = await readFile11(path, "utf8");
1306
1635
  const fileChangeId = readFrontMatterValue(content, "changeId");
1307
1636
  if (fileChangeId !== changeId) {
@@ -1419,9 +1748,9 @@ function renderIdeModelPolicy(command) {
1419
1748
  import { resolve } from "path";
1420
1749
 
1421
1750
  // src/adapters/codex/index.ts
1422
- import { mkdir as mkdir6, readFile as readFile12, stat as stat5 } from "fs/promises";
1751
+ import { mkdir as mkdir7, readFile as readFile12, stat as stat6 } from "fs/promises";
1423
1752
  import { homedir } from "os";
1424
- import { dirname as dirname6, join as join13 } from "path";
1753
+ import { dirname as dirname7, join as join15 } from "path";
1425
1754
 
1426
1755
  // src/adapters/commands.ts
1427
1756
  var FET_WORKFLOW_COMMANDS = [
@@ -1437,7 +1766,18 @@ var FET_WORKFLOW_COMMANDS = [
1437
1766
  "bulk-archive",
1438
1767
  "onboard"
1439
1768
  ];
1440
- var FET_ADAPTER_COMMANDS = [...FET_WORKFLOW_COMMANDS, "fill-context", "passthrough"];
1769
+ var FET_GRAPH_COMMANDS = ["graph-status", "graph-setup", "graph-init", "graph-refresh", "graph-doctor", "graph-handoff"];
1770
+ var FET_ADAPTER_COMMANDS = [...FET_WORKFLOW_COMMANDS, "fill-context", "passthrough", ...FET_GRAPH_COMMANDS];
1771
+ function renderFetAdapterUsage(command, args = "[...args]") {
1772
+ if (command.startsWith("graph-")) {
1773
+ const subcommand = command.slice("graph-".length);
1774
+ return `fet graph ${subcommand}${args ? ` ${args}` : ""}`;
1775
+ }
1776
+ if (command === "passthrough") {
1777
+ return `fet passthrough <openspec-command>${args ? ` ${args}` : ""}`;
1778
+ }
1779
+ return `fet ${command}${args ? ` ${args}` : ""}`;
1780
+ }
1441
1781
 
1442
1782
  // src/adapters/codex/templates.ts
1443
1783
  function codexGuideFile() {
@@ -1485,15 +1825,19 @@ function renderCommand(command) {
1485
1825
  if (command === "passthrough") {
1486
1826
  return renderPassthroughCommand();
1487
1827
  }
1828
+ if (command.startsWith("graph-")) {
1829
+ return renderGraphCommand(command);
1830
+ }
1831
+ const usage = renderFetAdapterUsage(command, "");
1488
1832
  return `<!-- FET:MANAGED
1489
1833
  schemaVersion: 1
1490
1834
  fetVersion: ${FET_VERSION}
1491
1835
  generator: codex-adapter
1492
1836
  adapterVersion: 1
1493
- command: fet ${command}
1837
+ command: ${usage}
1494
1838
  FET:END -->
1495
1839
 
1496
- # fet ${command}
1840
+ # ${usage}
1497
1841
 
1498
1842
  ${renderIdeModelPolicy(command)}
1499
1843
 
@@ -1504,7 +1848,7 @@ If GitNexus graph context is available, consult it before broad source scans and
1504
1848
  Then run:
1505
1849
 
1506
1850
  \`\`\`sh
1507
- fet ${command}
1851
+ ${usage}
1508
1852
  \`\`\`
1509
1853
 
1510
1854
  If the command needs a change id, pass it with \`--change <change-id>\` or use the active OpenSpec change from the user's request.
@@ -1538,6 +1882,36 @@ fet passthrough <openspec-command> [...args]
1538
1882
  This preserves the FET entry point while allowing access to unmanaged or newly added OpenSpec commands. Passthrough does not update FET lifecycle state.
1539
1883
  `;
1540
1884
  }
1885
+ function renderGraphCommand(command) {
1886
+ const usage = renderFetAdapterUsage(command, "");
1887
+ const subcommand = command.slice("graph-".length);
1888
+ return `<!-- FET:MANAGED
1889
+ schemaVersion: 1
1890
+ fetVersion: ${FET_VERSION}
1891
+ generator: codex-adapter
1892
+ adapterVersion: 1
1893
+ command: ${usage}
1894
+ FET:END -->
1895
+
1896
+ # ${usage}
1897
+
1898
+ ${renderIdeModelPolicy(command)}
1899
+
1900
+ When the user asks Codex to work with optional GitNexus graph support, use FET as the entry point.
1901
+
1902
+ 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.
1903
+
1904
+ Run:
1905
+
1906
+ \`\`\`sh
1907
+ ${usage}
1908
+ \`\`\`
1909
+
1910
+ For graph init or refresh, pass extra GitNexus analyze arguments only when the user provides them.
1911
+
1912
+ After the command completes, report the GitNexus state, generated handoff files, and next steps.
1913
+ `;
1914
+ }
1541
1915
  function renderSlashPrompt(command) {
1542
1916
  if (command === "continue") {
1543
1917
  return renderContinueSlashPrompt();
@@ -1575,9 +1949,10 @@ function renderSlashPrompt(command) {
1575
1949
  if (command === "passthrough") {
1576
1950
  return renderPassthroughSlashPrompt();
1577
1951
  }
1578
- const usage = command === "passthrough" ? "fet passthrough <openspec-command> [...args]" : `fet ${command} [...args]`;
1579
- const shellCommand = command === "passthrough" ? "fet passthrough $ARGUMENTS" : `fet ${command} $ARGUMENTS`;
1580
- const description = command === "passthrough" ? "Run an unmanaged OpenSpec command through FET passthrough" : `Run the FET-managed OpenSpec ${command} workflow`;
1952
+ const usage = renderFetAdapterUsage(command);
1953
+ const isGraph = command.startsWith("graph-");
1954
+ const shellCommand = isGraph ? `${renderFetAdapterUsage(command, "")} $ARGUMENTS` : `fet ${command} $ARGUMENTS`;
1955
+ const description = isGraph ? `Run optional GitNexus graph ${command.slice("graph-".length)} through FET` : `Run the FET-managed OpenSpec ${command} workflow`;
1581
1956
  return `<!-- FET:MANAGED
1582
1957
  schemaVersion: 1
1583
1958
  fetVersion: ${FET_VERSION}
@@ -2033,7 +2408,7 @@ var CodexAdapter = class {
2033
2408
  adapterVersion = 1;
2034
2409
  async detect(projectRoot) {
2035
2410
  return {
2036
- detected: await exists3(join13(projectRoot, ".codex")) || await exists3(join13(projectRoot, "AGENTS.md")),
2411
+ detected: await exists3(join15(projectRoot, ".codex")) || await exists3(join15(projectRoot, "AGENTS.md")),
2037
2412
  reason: "Codex adapter is available for projects that use AGENTS.md"
2038
2413
  };
2039
2414
  }
@@ -2072,7 +2447,7 @@ var CodexAdapter = class {
2072
2447
  if (existing && !existing.includes("FET:MANAGED") && force) {
2073
2448
  await createBackup(target);
2074
2449
  }
2075
- await mkdir6(dirname6(target), { recursive: true });
2450
+ await mkdir7(dirname7(target), { recursive: true });
2076
2451
  await atomicWrite(target, file.content);
2077
2452
  written.push(displayPath);
2078
2453
  }
@@ -2099,9 +2474,9 @@ var CodexAdapter = class {
2099
2474
  };
2100
2475
  function resolveTarget(projectRoot, file) {
2101
2476
  if (file.root === "codex-home") {
2102
- return join13(resolveCodexHome(), file.path);
2477
+ return join15(resolveCodexHome(), file.path);
2103
2478
  }
2104
- return join13(projectRoot, file.path);
2479
+ return join15(projectRoot, file.path);
2105
2480
  }
2106
2481
  function displayPathFor(file) {
2107
2482
  if (file.root === "codex-home") {
@@ -2110,7 +2485,7 @@ function displayPathFor(file) {
2110
2485
  return file.path;
2111
2486
  }
2112
2487
  function resolveCodexHome() {
2113
- return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join13(homedir(), ".codex");
2488
+ return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join15(homedir(), ".codex");
2114
2489
  }
2115
2490
  async function readExisting(path) {
2116
2491
  try {
@@ -2121,7 +2496,7 @@ async function readExisting(path) {
2121
2496
  }
2122
2497
  async function exists3(path) {
2123
2498
  try {
2124
- await stat5(path);
2499
+ await stat6(path);
2125
2500
  return true;
2126
2501
  } catch {
2127
2502
  return false;
@@ -2129,8 +2504,8 @@ async function exists3(path) {
2129
2504
  }
2130
2505
 
2131
2506
  // src/adapters/cursor/index.ts
2132
- import { mkdir as mkdir7, readFile as readFile13, stat as stat6 } from "fs/promises";
2133
- import { dirname as dirname7, join as join14 } from "path";
2507
+ import { mkdir as mkdir8, readFile as readFile13, stat as stat7 } from "fs/promises";
2508
+ import { dirname as dirname8, join as join16 } from "path";
2134
2509
 
2135
2510
  // src/adapters/cursor/templates.ts
2136
2511
  function cursorSkillFiles() {
@@ -2166,7 +2541,7 @@ alwaysApply: false
2166
2541
  };
2167
2542
  }
2168
2543
  function renderSkill(command) {
2169
- const usage = command === "passthrough" ? "fet passthrough <openspec-command> [...args]" : `fet ${command}`;
2544
+ const usage = renderFetAdapterUsage(command, command === "passthrough" ? "[...args]" : "");
2170
2545
  if (command === "fill-context") {
2171
2546
  return `<!-- FET:MANAGED
2172
2547
  schemaVersion: 1
@@ -2232,7 +2607,7 @@ var CursorAdapter = class {
2232
2607
  adapterVersion = 1;
2233
2608
  async detect(projectRoot) {
2234
2609
  return {
2235
- detected: await exists4(join14(projectRoot, ".cursor")),
2610
+ detected: await exists4(join16(projectRoot, ".cursor")),
2236
2611
  reason: "Cursor adapter is available for any project"
2237
2612
  };
2238
2613
  }
@@ -2249,7 +2624,7 @@ var CursorAdapter = class {
2249
2624
  const written = [];
2250
2625
  const skipped = [];
2251
2626
  for (const file of plan.files) {
2252
- const target = join14(projectRoot, file.path);
2627
+ const target = join16(projectRoot, file.path);
2253
2628
  const existing = await readExisting2(target);
2254
2629
  if (existing && !existing.includes("FET:MANAGED") && !force) {
2255
2630
  throw new FetError({
@@ -2262,7 +2637,7 @@ var CursorAdapter = class {
2262
2637
  if (existing && !existing.includes("FET:MANAGED") && force) {
2263
2638
  await createBackup(target);
2264
2639
  }
2265
- await mkdir7(dirname7(target), { recursive: true });
2640
+ await mkdir8(dirname8(target), { recursive: true });
2266
2641
  await atomicWrite(target, file.content);
2267
2642
  written.push(file.path);
2268
2643
  }
@@ -2272,7 +2647,7 @@ var CursorAdapter = class {
2272
2647
  const plan = await this.planInstall(projectRoot);
2273
2648
  const checks = [];
2274
2649
  for (const file of plan.files) {
2275
- const target = join14(projectRoot, file.path);
2650
+ const target = join16(projectRoot, file.path);
2276
2651
  const content = await readExisting2(target);
2277
2652
  const managed = Boolean(content?.includes("FET:MANAGED"));
2278
2653
  const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
@@ -2295,7 +2670,7 @@ async function readExisting2(path) {
2295
2670
  }
2296
2671
  async function exists4(path) {
2297
2672
  try {
2298
- await stat6(path);
2673
+ await stat7(path);
2299
2674
  return true;
2300
2675
  } catch {
2301
2676
  return false;
@@ -2307,13 +2682,13 @@ import { execFile as execFile4 } from "child_process";
2307
2682
  import { promisify as promisify4 } from "util";
2308
2683
 
2309
2684
  // src/openspec/inspector.ts
2310
- import { readdir, stat as stat7 } from "fs/promises";
2311
- import { join as join15 } from "path";
2685
+ import { readdir, stat as stat8 } from "fs/promises";
2686
+ import { join as join17 } from "path";
2312
2687
  async function inspectOpenSpecProject(projectRoot) {
2313
- const openspecPath = join15(projectRoot, "openspec");
2314
- const changesPath = join15(openspecPath, "changes");
2315
- const legacyArchivePath = join15(openspecPath, "archive");
2316
- const changesArchivePath = join15(changesPath, "archive");
2688
+ const openspecPath = join17(projectRoot, "openspec");
2689
+ const changesPath = join17(openspecPath, "changes");
2690
+ const legacyArchivePath = join17(openspecPath, "archive");
2691
+ const changesArchivePath = join17(changesPath, "archive");
2317
2692
  return {
2318
2693
  exists: await exists5(openspecPath),
2319
2694
  changes: await listDirectories(changesPath, { exclude: ["archive"] }),
@@ -2321,13 +2696,13 @@ async function inspectOpenSpecProject(projectRoot) {
2321
2696
  };
2322
2697
  }
2323
2698
  async function inspectOpenSpecChange(projectRoot, changeId) {
2324
- const changePath = join15(projectRoot, "openspec", "changes", changeId);
2325
- const tasksPath = join15(changePath, "tasks.md");
2326
- const specsPath = join15(changePath, "specs");
2699
+ const changePath = join17(projectRoot, "openspec", "changes", changeId);
2700
+ const tasksPath = join17(changePath, "tasks.md");
2701
+ const specsPath = join17(changePath, "specs");
2327
2702
  return {
2328
2703
  changeId,
2329
2704
  exists: await exists5(changePath),
2330
- hasProposal: await exists5(join15(changePath, "proposal.md")),
2705
+ hasProposal: await exists5(join17(changePath, "proposal.md")),
2331
2706
  hasTasks: await exists5(tasksPath),
2332
2707
  hasSpecs: await exists5(specsPath),
2333
2708
  tasksPath,
@@ -2345,7 +2720,7 @@ async function listDirectories(path, options = {}) {
2345
2720
  }
2346
2721
  async function exists5(path) {
2347
2722
  try {
2348
- await stat7(path);
2723
+ await stat8(path);
2349
2724
  return true;
2350
2725
  } catch {
2351
2726
  return false;
@@ -2504,12 +2879,12 @@ function parseCommands(help) {
2504
2879
  }
2505
2880
 
2506
2881
  // src/scanner/package.ts
2507
- import { readFile as readFile14, stat as stat8 } from "fs/promises";
2508
- import { join as join16 } from "path";
2882
+ import { readFile as readFile14, stat as stat9 } from "fs/promises";
2883
+ import { join as join18 } from "path";
2509
2884
  import { parse as parse2 } from "yaml";
2510
2885
  async function readPackageJson(projectRoot) {
2511
2886
  try {
2512
- return JSON.parse(await readFile14(join16(projectRoot, "package.json"), "utf8"));
2887
+ return JSON.parse(await readFile14(join18(projectRoot, "package.json"), "utf8"));
2513
2888
  } catch {
2514
2889
  return null;
2515
2890
  }
@@ -2575,7 +2950,7 @@ function detectFramework(pkg) {
2575
2950
  }
2576
2951
  async function detectLanguage(projectRoot, pkg) {
2577
2952
  const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
2578
- if (deps.typescript || await exists6(join16(projectRoot, "tsconfig.json"))) {
2953
+ if (deps.typescript || await exists6(join18(projectRoot, "tsconfig.json"))) {
2579
2954
  return "typescript";
2580
2955
  }
2581
2956
  return "javascript";
@@ -2590,7 +2965,7 @@ async function detectWorkspaces(projectRoot, pkg) {
2590
2965
  return packageWorkspaces;
2591
2966
  }
2592
2967
  try {
2593
- const workspace = parse2(await readFile14(join16(projectRoot, "pnpm-workspace.yaml"), "utf8"));
2968
+ const workspace = parse2(await readFile14(join18(projectRoot, "pnpm-workspace.yaml"), "utf8"));
2594
2969
  return (workspace?.packages ?? []).map((path) => ({
2595
2970
  name: path,
2596
2971
  path,
@@ -2610,7 +2985,7 @@ async function detectLockManagers(projectRoot) {
2610
2985
  ];
2611
2986
  const found = [];
2612
2987
  for (const [file, manager] of lockFiles) {
2613
- if (await exists6(join16(projectRoot, file))) {
2988
+ if (await exists6(join18(projectRoot, file))) {
2614
2989
  found.push(manager);
2615
2990
  }
2616
2991
  }
@@ -2627,7 +3002,7 @@ function scriptCommand(packageManager, name) {
2627
3002
  }
2628
3003
  async function exists6(path) {
2629
3004
  try {
2630
- await stat8(path);
3005
+ await stat9(path);
2631
3006
  return true;
2632
3007
  } catch {
2633
3008
  return false;
@@ -2635,13 +3010,13 @@ async function exists6(path) {
2635
3010
  }
2636
3011
 
2637
3012
  // src/scanner/routes.ts
2638
- import { readdir as readdir2, stat as stat9 } from "fs/promises";
2639
- import { join as join17, relative, sep } from "path";
3013
+ import { readdir as readdir2, stat as stat10 } from "fs/promises";
3014
+ import { join as join19, relative, sep } from "path";
2640
3015
  async function scanRoutes(projectRoot) {
2641
3016
  const candidates = ["src/routes", "src/pages", "app", "pages"];
2642
3017
  const routes = [];
2643
3018
  for (const candidate of candidates) {
2644
- const root = join17(projectRoot, candidate);
3019
+ const root = join19(projectRoot, candidate);
2645
3020
  if (!await exists7(root)) {
2646
3021
  continue;
2647
3022
  }
@@ -2669,7 +3044,7 @@ async function listFiles(root) {
2669
3044
  const entries = await readdir2(root, { withFileTypes: true });
2670
3045
  const files = [];
2671
3046
  for (const entry of entries) {
2672
- const path = join17(root, entry.name);
3047
+ const path = join19(root, entry.name);
2673
3048
  if (entry.isDirectory()) {
2674
3049
  files.push(...await listFiles(path));
2675
3050
  } else {
@@ -2680,7 +3055,7 @@ async function listFiles(root) {
2680
3055
  }
2681
3056
  async function exists7(path) {
2682
3057
  try {
2683
- await stat9(path);
3058
+ await stat10(path);
2684
3059
  return true;
2685
3060
  } catch {
2686
3061
  return false;
@@ -2827,6 +3202,13 @@ program.name("fet").description("Frontend workflow orchestration tool built arou
2827
3202
  addGlobalOptions(program.command("init")).description("\u521D\u59CB\u5316 FET + OpenSpec").action(wrap("init", initCommand));
2828
3203
  addGlobalOptions(program.command("update-context")).description("\u66F4\u65B0\u9879\u76EE\u4E0A\u4E0B\u6587").action(wrap("update-context", updateContextCommand));
2829
3204
  addGlobalOptions(program.command("fill-context")).description("Refresh IDE prompts for filling AGENTS.md placeholders").action(wrap("fill-context", fillContextCommand));
3205
+ var graph = addGlobalOptions(program.command("graph").description("Manage optional GitNexus code graph support"));
3206
+ for (const action of ["status", "setup", "doctor", "handoff"]) {
3207
+ addGlobalOptions(graph.command(action).description(`Run fet graph ${action}`)).action(wrap("graph", (ctx) => graphCommand(ctx, action)));
3208
+ }
3209
+ for (const action of ["init", "refresh"]) {
3210
+ addGlobalOptions(graph.command(`${action} [args...]`).description(`Run GitNexus analyze for graph ${action}`).allowUnknownOption(true).passThroughOptions()).action(wrap("graph", (ctx, args = []) => graphCommand(ctx, action, args)));
3211
+ }
2830
3212
  addGlobalOptions(program.command("doctor").description("\u8BCA\u65AD\u72B6\u6001\u3001\u914D\u7F6E\u4E0E\u4E00\u81F4\u6027").option("--fix-lock", "\u6E05\u7406 FET \u9501\u6587\u4EF6")).action(
2831
3213
  wrap("doctor", (ctx, options) => doctorCommand(ctx, { fixLock: Boolean(options.fixLock) }))
2832
3214
  );