@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/README.md +19 -0
- package/dist/{chunk-FZOVNHE7.js → chunk-V4ZRBF5L.js} +5 -1
- package/dist/chunk-V4ZRBF5L.js.map +1 -0
- package/dist/cli/index.js +477 -95
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-FZOVNHE7.js.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
import {
|
|
3
3
|
FetError,
|
|
4
4
|
toFetError
|
|
5
|
-
} from "../chunk-
|
|
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
|
|
13
|
-
import { join as
|
|
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
|
|
128
|
+
const command = resolveGitNexusCommand(env);
|
|
126
129
|
try {
|
|
127
|
-
const { stdout, stderr } = await execFileAsync(
|
|
130
|
+
const { stdout, stderr } = await execFileAsync(command.file, [...command.args, "--version"], { shell: process.platform === "win32" });
|
|
128
131
|
return {
|
|
129
132
|
installed: true,
|
|
130
|
-
executablePath:
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
458
|
-
const configPath =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
582
|
-
import { join as
|
|
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",
|
|
588
|
-
checks.push(await checkFile("config",
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
768
|
-
import { join as
|
|
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
|
|
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
|
|
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
|
|
1232
|
+
return join12(this.projectRoot, "openspec", "fet-state.json");
|
|
904
1233
|
}
|
|
905
1234
|
changePath(changeId) {
|
|
906
|
-
return
|
|
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 =
|
|
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 =
|
|
1035
|
-
const proposal = await readOptional3(
|
|
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(
|
|
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
|
|
1184
|
-
import { join as
|
|
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 =
|
|
1252
|
-
const instructionsPath =
|
|
1253
|
-
await
|
|
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 =
|
|
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
|
|
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
|
|
1751
|
+
import { mkdir as mkdir7, readFile as readFile12, stat as stat6 } from "fs/promises";
|
|
1423
1752
|
import { homedir } from "os";
|
|
1424
|
-
import { dirname as
|
|
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
|
|
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:
|
|
1837
|
+
command: ${usage}
|
|
1494
1838
|
FET:END -->
|
|
1495
1839
|
|
|
1496
|
-
#
|
|
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
|
-
|
|
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
|
|
1579
|
-
const
|
|
1580
|
-
const
|
|
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(
|
|
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
|
|
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
|
|
2477
|
+
return join15(resolveCodexHome(), file.path);
|
|
2103
2478
|
}
|
|
2104
|
-
return
|
|
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 ??
|
|
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
|
|
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
|
|
2133
|
-
import { dirname as
|
|
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" ? "
|
|
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(
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
2311
|
-
import { join as
|
|
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 =
|
|
2314
|
-
const changesPath =
|
|
2315
|
-
const legacyArchivePath =
|
|
2316
|
-
const changesArchivePath =
|
|
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 =
|
|
2325
|
-
const tasksPath =
|
|
2326
|
-
const specsPath =
|
|
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(
|
|
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
|
|
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
|
|
2508
|
-
import { join as
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
2639
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
);
|