@nick848/fet 1.0.4 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/dist/{chunk-FZOVNHE7.js → chunk-V4ZRBF5L.js} +5 -1
- package/dist/chunk-V4ZRBF5L.js.map +1 -0
- package/dist/cli/index.js +639 -116
- 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) {
|
|
@@ -296,6 +368,12 @@ ${routes || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | low |"}
|
|
|
296
368
|
|
|
297
369
|
[NEEDS LLM INPUT]
|
|
298
370
|
|
|
371
|
+
## AI Work Guidelines
|
|
372
|
+
|
|
373
|
+
- Prefer the project-level Andrej Karpathy inspired guidelines in .fet/karpathy-guidelines.md when using FET-managed IDE workflows.
|
|
374
|
+
- For Codex, also read .codex/fet/karpathy-guidelines.md when present.
|
|
375
|
+
- Treat those guidelines as secondary to the user's latest request and explicit OpenSpec artifacts.
|
|
376
|
+
|
|
299
377
|
## Scanner Metadata
|
|
300
378
|
|
|
301
379
|
- Generated At: ${scan.generatedAt}
|
|
@@ -343,6 +421,97 @@ function renderFetConfig(scan) {
|
|
|
343
421
|
});
|
|
344
422
|
}
|
|
345
423
|
|
|
424
|
+
// src/templates/karpathy-skills.ts
|
|
425
|
+
var KARPATHY_SKILLS_SOURCE = "https://github.com/forrestchang/andrej-karpathy-skills";
|
|
426
|
+
var BEGIN = "<!-- FET:BEGIN ANDREJ-KARPATHY-SKILLS -->";
|
|
427
|
+
var END = "<!-- FET:END ANDREJ-KARPATHY-SKILLS -->";
|
|
428
|
+
function mergeKarpathyClaudeMd(existing) {
|
|
429
|
+
const block = renderManagedBlock(renderKarpathyClaudeGuidelines());
|
|
430
|
+
if (!existing || !existing.trim()) {
|
|
431
|
+
return `${block}
|
|
432
|
+
`;
|
|
433
|
+
}
|
|
434
|
+
const start = existing.indexOf(BEGIN);
|
|
435
|
+
const end = existing.indexOf(END);
|
|
436
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
437
|
+
return `${existing.slice(0, start)}${block}${existing.slice(end + END.length)}`;
|
|
438
|
+
}
|
|
439
|
+
return `${existing.replace(/\s*$/, "")}
|
|
440
|
+
|
|
441
|
+
${block}
|
|
442
|
+
`;
|
|
443
|
+
}
|
|
444
|
+
function renderKarpathyCursorRule() {
|
|
445
|
+
return `<!-- FET:MANAGED
|
|
446
|
+
schemaVersion: 1
|
|
447
|
+
generator: karpathy-skills
|
|
448
|
+
FET:END -->
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
description: Andrej Karpathy inspired coding guidelines
|
|
452
|
+
alwaysApply: true
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
${renderKarpathyGuidelinesBody()}
|
|
456
|
+
`;
|
|
457
|
+
}
|
|
458
|
+
function renderKarpathyFetHandoff() {
|
|
459
|
+
return `<!-- FET:MANAGED
|
|
460
|
+
schemaVersion: 1
|
|
461
|
+
generator: karpathy-skills
|
|
462
|
+
FET:END -->
|
|
463
|
+
|
|
464
|
+
# Andrej Karpathy Inspired Coding Guidelines
|
|
465
|
+
|
|
466
|
+
${renderKarpathyGuidelinesBody()}
|
|
467
|
+
`;
|
|
468
|
+
}
|
|
469
|
+
function renderManagedBlock(content) {
|
|
470
|
+
return `${BEGIN}
|
|
471
|
+
${content}
|
|
472
|
+
${END}`;
|
|
473
|
+
}
|
|
474
|
+
function renderKarpathyClaudeGuidelines() {
|
|
475
|
+
return `# Andrej Karpathy Inspired Coding Guidelines
|
|
476
|
+
|
|
477
|
+
${renderKarpathyGuidelinesBody()}`;
|
|
478
|
+
}
|
|
479
|
+
function renderKarpathyGuidelinesBody() {
|
|
480
|
+
return `Source: ${KARPATHY_SKILLS_SOURCE}
|
|
481
|
+
|
|
482
|
+
Use these project-level guidelines together with AGENTS.md, OpenSpec artifacts, and the user's latest request.
|
|
483
|
+
|
|
484
|
+
## Think Before Coding
|
|
485
|
+
|
|
486
|
+
- State important assumptions before editing.
|
|
487
|
+
- Ask for clarification when ambiguity would change the implementation.
|
|
488
|
+
- Surface tradeoffs instead of silently choosing a risky path.
|
|
489
|
+
- Push back when a simpler approach better fits the request.
|
|
490
|
+
|
|
491
|
+
## Simplicity First
|
|
492
|
+
|
|
493
|
+
- Solve the requested problem with the smallest clear change.
|
|
494
|
+
- Avoid speculative features, configuration, or abstraction.
|
|
495
|
+
- Do not create abstractions for one-off code.
|
|
496
|
+
- Prefer deleting complexity introduced by your own change over adding more structure.
|
|
497
|
+
|
|
498
|
+
## Precise Edits
|
|
499
|
+
|
|
500
|
+
- Touch only files and lines that directly serve the task.
|
|
501
|
+
- Preserve existing style even when you personally prefer another pattern.
|
|
502
|
+
- Do not refactor nearby code, comments, or formatting unless the task requires it.
|
|
503
|
+
- Remove only dead imports, variables, or helpers made obsolete by your own change.
|
|
504
|
+
|
|
505
|
+
## Goal-Driven Execution
|
|
506
|
+
|
|
507
|
+
- Convert vague work into concrete success criteria.
|
|
508
|
+
- For bugs, prefer a reproducing test or clear verification before the fix.
|
|
509
|
+
- For multi-step work, keep a short plan and verify each meaningful step.
|
|
510
|
+
- Continue iterating until the success criteria are met or a blocker is explicit.
|
|
511
|
+
|
|
512
|
+
These guidelines intentionally favor caution over speed for non-trivial work. For obvious one-line fixes, use judgment and stay lightweight.`;
|
|
513
|
+
}
|
|
514
|
+
|
|
346
515
|
// src/templates/verify-instructions.ts
|
|
347
516
|
function renderVerifyInstructions(changeId, generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
348
517
|
return `---
|
|
@@ -370,27 +539,28 @@ fet verify --done --change ${changeId}
|
|
|
370
539
|
}
|
|
371
540
|
|
|
372
541
|
// src/templates/gitignore.ts
|
|
373
|
-
var
|
|
374
|
-
var
|
|
542
|
+
var BEGIN2 = "# FET:BEGIN LOCAL STATE";
|
|
543
|
+
var END2 = "# FET:END LOCAL STATE";
|
|
375
544
|
var RULES = [
|
|
376
545
|
"openspec/fet-state.json",
|
|
377
546
|
"openspec/.fet.lock",
|
|
378
547
|
"openspec/.fet-init-journal.json",
|
|
379
548
|
"openspec/changes/*/fet-state.json",
|
|
380
|
-
"openspec/changes/*/.fet/"
|
|
549
|
+
"openspec/changes/*/.fet/",
|
|
550
|
+
".gitnexus/"
|
|
381
551
|
];
|
|
382
552
|
function mergeGitignore(existing) {
|
|
383
|
-
const block = `${
|
|
553
|
+
const block = `${BEGIN2}
|
|
384
554
|
${RULES.join("\n")}
|
|
385
|
-
${
|
|
555
|
+
${END2}`;
|
|
386
556
|
if (!existing || !existing.trim()) {
|
|
387
557
|
return `${block}
|
|
388
558
|
`;
|
|
389
559
|
}
|
|
390
|
-
const start = existing.indexOf(
|
|
391
|
-
const end = existing.indexOf(
|
|
560
|
+
const start = existing.indexOf(BEGIN2);
|
|
561
|
+
const end = existing.indexOf(END2);
|
|
392
562
|
if (start !== -1 && end !== -1 && end > start) {
|
|
393
|
-
return `${existing.slice(0, start)}${block}${existing.slice(end +
|
|
563
|
+
return `${existing.slice(0, start)}${block}${existing.slice(end + END2.length)}`;
|
|
394
564
|
}
|
|
395
565
|
return `${existing.replace(/\s*$/, "")}
|
|
396
566
|
|
|
@@ -400,7 +570,7 @@ ${block}
|
|
|
400
570
|
|
|
401
571
|
// src/commands/update-context.ts
|
|
402
572
|
import { readFile as readFile5 } from "fs/promises";
|
|
403
|
-
import { join as
|
|
573
|
+
import { join as join7 } from "path";
|
|
404
574
|
|
|
405
575
|
// src/config/yaml.ts
|
|
406
576
|
import { readFile as readFile3 } from "fs/promises";
|
|
@@ -421,11 +591,11 @@ async function mergeFetConfig(configPath, renderedFetYaml) {
|
|
|
421
591
|
|
|
422
592
|
// src/context-placeholders.ts
|
|
423
593
|
import { readFile as readFile4 } from "fs/promises";
|
|
424
|
-
import { join as
|
|
594
|
+
import { join as join6 } from "path";
|
|
425
595
|
var AGENTS_LLM_PLACEHOLDER_PATTERN = /\[NEEDS? LLM INPUT\]/g;
|
|
426
596
|
async function countAgentsLlmPlaceholders(projectRoot) {
|
|
427
597
|
try {
|
|
428
|
-
const content = await readFile4(
|
|
598
|
+
const content = await readFile4(join6(projectRoot, "AGENTS.md"), "utf8");
|
|
429
599
|
return [...content.matchAll(AGENTS_LLM_PLACEHOLDER_PATTERN)].length;
|
|
430
600
|
} catch {
|
|
431
601
|
return 0;
|
|
@@ -454,9 +624,14 @@ async function updateContextCommand(ctx) {
|
|
|
454
624
|
}
|
|
455
625
|
async function updateContextFiles(ctx) {
|
|
456
626
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
457
|
-
const agentsPath =
|
|
458
|
-
const configPath =
|
|
627
|
+
const agentsPath = join7(ctx.projectRoot, "AGENTS.md");
|
|
628
|
+
const configPath = join7(ctx.projectRoot, "openspec", "config.yaml");
|
|
629
|
+
const claudePath = join7(ctx.projectRoot, "CLAUDE.md");
|
|
630
|
+
const karpathyHandoffPath = join7(ctx.projectRoot, ".fet", "karpathy-guidelines.md");
|
|
631
|
+
const karpathyCursorPath = join7(ctx.projectRoot, ".cursor", "rules", "karpathy-guidelines.mdc");
|
|
459
632
|
const existingAgents = await readOptional(agentsPath);
|
|
633
|
+
const existingClaude = await readOptional(claudePath);
|
|
634
|
+
const existingKarpathyCursor = await readOptional(karpathyCursorPath);
|
|
460
635
|
const warnings = [...scan.warnings];
|
|
461
636
|
if (existingAgents && hasInvalidManagedAutoRegion(existingAgents)) {
|
|
462
637
|
throw new FetError({
|
|
@@ -482,6 +657,13 @@ async function updateContextFiles(ctx) {
|
|
|
482
657
|
}
|
|
483
658
|
await atomicWrite(agentsPath, replaceManagedRegion(existingAgents, renderAgentsMd(scan)));
|
|
484
659
|
await atomicWrite(configPath, await mergeFetConfig(configPath, renderFetConfig(scan)));
|
|
660
|
+
await atomicWrite(claudePath, mergeKarpathyClaudeMd(existingClaude));
|
|
661
|
+
await atomicWrite(karpathyHandoffPath, renderKarpathyFetHandoff());
|
|
662
|
+
if (!existingKarpathyCursor || existingKarpathyCursor.includes("FET:MANAGED")) {
|
|
663
|
+
await atomicWrite(karpathyCursorPath, renderKarpathyCursorRule());
|
|
664
|
+
} else {
|
|
665
|
+
warnings.push(".cursor/rules/karpathy-guidelines.mdc exists and is not managed by FET; leaving it unchanged.");
|
|
666
|
+
}
|
|
485
667
|
const placeholderCount = await countAgentsLlmPlaceholders(ctx.projectRoot);
|
|
486
668
|
if (placeholderCount > 0) {
|
|
487
669
|
warnings.push(renderAgentsPlaceholderWarning(placeholderCount));
|
|
@@ -505,7 +687,7 @@ async function readOptional(path) {
|
|
|
505
687
|
|
|
506
688
|
// src/commands/init.ts
|
|
507
689
|
async function initCommand(ctx) {
|
|
508
|
-
const alreadyInitialized = await exists(
|
|
690
|
+
const alreadyInitialized = await exists(join8(ctx.projectRoot, "openspec", "config.yaml"));
|
|
509
691
|
let warnings = [];
|
|
510
692
|
await withProjectLock(
|
|
511
693
|
ctx.projectRoot,
|
|
@@ -527,7 +709,10 @@ async function initCommand(ctx) {
|
|
|
527
709
|
const state = await ctx.stateStore.getOrCreateGlobal();
|
|
528
710
|
state.openspec = identity;
|
|
529
711
|
state.graph ??= {};
|
|
530
|
-
const gitnexus =
|
|
712
|
+
const gitnexus = mergeGitNexusGraphInfo(
|
|
713
|
+
toGitNexusState(await detectGitNexus(), state.graph.gitnexus),
|
|
714
|
+
await inspectGitNexusGraph(ctx.projectRoot)
|
|
715
|
+
);
|
|
531
716
|
if (!gitnexus.installed && !gitnexus.recommendationShownAt) {
|
|
532
717
|
warnings.push(renderGitNexusRecommendation(gitnexus));
|
|
533
718
|
gitnexus.recommendationShownAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -557,7 +742,7 @@ async function initCommand(ctx) {
|
|
|
557
742
|
});
|
|
558
743
|
}
|
|
559
744
|
async function ensureGitignore(ctx) {
|
|
560
|
-
const gitignorePath =
|
|
745
|
+
const gitignorePath = join8(ctx.projectRoot, ".gitignore");
|
|
561
746
|
const existing = await readOptional2(gitignorePath);
|
|
562
747
|
await atomicWrite(gitignorePath, mergeGitignore(existing));
|
|
563
748
|
}
|
|
@@ -570,7 +755,7 @@ async function readOptional2(path) {
|
|
|
570
755
|
}
|
|
571
756
|
async function exists(path) {
|
|
572
757
|
try {
|
|
573
|
-
await
|
|
758
|
+
await stat3(path);
|
|
574
759
|
return true;
|
|
575
760
|
} catch {
|
|
576
761
|
return false;
|
|
@@ -578,20 +763,20 @@ async function exists(path) {
|
|
|
578
763
|
}
|
|
579
764
|
|
|
580
765
|
// src/commands/doctor.ts
|
|
581
|
-
import { readFile as readFile7, stat as
|
|
582
|
-
import { join as
|
|
766
|
+
import { readFile as readFile7, stat as stat4 } from "fs/promises";
|
|
767
|
+
import { join as join9 } from "path";
|
|
583
768
|
async function doctorCommand(ctx, options = {}) {
|
|
584
769
|
const checks = [];
|
|
585
770
|
checks.push(await checkOpenSpec(ctx));
|
|
586
771
|
checks.push(await checkState(ctx));
|
|
587
|
-
checks.push(await checkFile("agents",
|
|
588
|
-
checks.push(await checkFile("config",
|
|
772
|
+
checks.push(await checkFile("agents", join9(ctx.projectRoot, "AGENTS.md"), "AGENTS.md \u7F3A\u5931", "fet update-context"));
|
|
773
|
+
checks.push(await checkFile("config", join9(ctx.projectRoot, "openspec", "config.yaml"), "openspec/config.yaml \u7F3A\u5931", "fet init"));
|
|
589
774
|
checks.push(await checkPlaceholders(ctx.projectRoot));
|
|
590
775
|
checks.push(await checkGitNexus(ctx));
|
|
591
776
|
for (const adapter of ctx.toolAdapters) {
|
|
592
777
|
checks.push(...await adapter.doctor(ctx.projectRoot));
|
|
593
778
|
}
|
|
594
|
-
const lockPath =
|
|
779
|
+
const lockPath = join9(ctx.projectRoot, "openspec", ".fet.lock");
|
|
595
780
|
if (await exists2(lockPath)) {
|
|
596
781
|
if (options.fixLock) {
|
|
597
782
|
await clearLock(ctx.projectRoot);
|
|
@@ -611,7 +796,10 @@ async function doctorCommand(ctx, options = {}) {
|
|
|
611
796
|
}
|
|
612
797
|
async function checkGitNexus(ctx) {
|
|
613
798
|
const global = await ctx.stateStore.readGlobal();
|
|
614
|
-
const state =
|
|
799
|
+
const state = mergeGitNexusGraphInfo(
|
|
800
|
+
toGitNexusState(await detectGitNexus(), global?.graph?.gitnexus),
|
|
801
|
+
await inspectGitNexusGraph(ctx.projectRoot)
|
|
802
|
+
);
|
|
615
803
|
if (global) {
|
|
616
804
|
global.graph ??= {};
|
|
617
805
|
global.graph.gitnexus = state;
|
|
@@ -620,7 +808,7 @@ async function checkGitNexus(ctx) {
|
|
|
620
808
|
return state.installed ? {
|
|
621
809
|
id: "gitnexus",
|
|
622
810
|
status: "pass",
|
|
623
|
-
message: `GitNexus detected: ${state.executablePath ?? "gitnexus"} (${state.version ?? "unknown"})`
|
|
811
|
+
message: `GitNexus detected: ${state.executablePath ?? "gitnexus"} (${state.version ?? "unknown"}), graph ${state.graphExists ? "found" : "not found"}`
|
|
624
812
|
} : {
|
|
625
813
|
id: "gitnexus",
|
|
626
814
|
status: "warn",
|
|
@@ -649,7 +837,7 @@ async function checkFile(id, path, missing, suggestedCommand) {
|
|
|
649
837
|
}
|
|
650
838
|
async function checkPlaceholders(projectRoot) {
|
|
651
839
|
try {
|
|
652
|
-
await readFile7(
|
|
840
|
+
await readFile7(join9(projectRoot, "AGENTS.md"), "utf8");
|
|
653
841
|
const count2 = await countAgentsLlmPlaceholders(projectRoot);
|
|
654
842
|
return count2 ? {
|
|
655
843
|
id: "context-placeholders",
|
|
@@ -663,7 +851,7 @@ async function checkPlaceholders(projectRoot) {
|
|
|
663
851
|
}
|
|
664
852
|
async function exists2(path) {
|
|
665
853
|
try {
|
|
666
|
-
await
|
|
854
|
+
await stat4(path);
|
|
667
855
|
return true;
|
|
668
856
|
} catch {
|
|
669
857
|
return false;
|
|
@@ -672,13 +860,13 @@ async function exists2(path) {
|
|
|
672
860
|
|
|
673
861
|
// src/commands/fill-context.ts
|
|
674
862
|
import { mkdir as mkdir3 } from "fs/promises";
|
|
675
|
-
import { dirname as dirname5, join as
|
|
863
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
676
864
|
async function fillContextCommand(ctx) {
|
|
677
865
|
await withProjectLock(
|
|
678
866
|
ctx.projectRoot,
|
|
679
867
|
{ command: "fill-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
|
|
680
868
|
async () => {
|
|
681
|
-
const handoffPath =
|
|
869
|
+
const handoffPath = join10(ctx.projectRoot, ".fet", "fill-context.md");
|
|
682
870
|
await mkdir3(dirname5(handoffPath), { recursive: true });
|
|
683
871
|
await atomicWrite(handoffPath, renderGenericHandoff());
|
|
684
872
|
for (const adapter of ctx.toolAdapters) {
|
|
@@ -725,17 +913,268 @@ FET:END -->
|
|
|
725
913
|
Use the IDE AI to complete FET-generated placeholders.
|
|
726
914
|
|
|
727
915
|
1. Read AGENTS.md and openspec/config.yaml.
|
|
728
|
-
2.
|
|
729
|
-
3.
|
|
730
|
-
4.
|
|
731
|
-
5.
|
|
732
|
-
6.
|
|
916
|
+
2. Read .fet/karpathy-guidelines.md when it exists. For Codex, also read .codex/fet/karpathy-guidelines.md when it exists.
|
|
917
|
+
3. Inspect README files, package scripts, routes, tests, source layout, and project conventions.
|
|
918
|
+
4. Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete project-specific content.
|
|
919
|
+
5. Preserve FET managed markers.
|
|
920
|
+
6. Do not modify business code.
|
|
921
|
+
7. Run \`fet doctor\` and confirm no AGENTS.md placeholder warning remains.
|
|
922
|
+
`;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// src/commands/graph.ts
|
|
926
|
+
import { mkdir as mkdir4 } from "fs/promises";
|
|
927
|
+
import { dirname as dirname6, join as join11 } from "path";
|
|
928
|
+
async function graphCommand(ctx, action, args = []) {
|
|
929
|
+
switch (action) {
|
|
930
|
+
case "status":
|
|
931
|
+
await graphStatusCommand(ctx);
|
|
932
|
+
return;
|
|
933
|
+
case "doctor":
|
|
934
|
+
await graphDoctorCommand(ctx);
|
|
935
|
+
return;
|
|
936
|
+
case "setup":
|
|
937
|
+
await graphSetupCommand(ctx);
|
|
938
|
+
return;
|
|
939
|
+
case "handoff":
|
|
940
|
+
await graphHandoffCommand(ctx);
|
|
941
|
+
return;
|
|
942
|
+
case "init":
|
|
943
|
+
await graphAnalyzeCommand(ctx, "init", args);
|
|
944
|
+
return;
|
|
945
|
+
case "refresh":
|
|
946
|
+
await graphAnalyzeCommand(ctx, "refresh", args);
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
async function graphStatusCommand(ctx) {
|
|
951
|
+
const result = await refreshGraphState(ctx, { runStatus: true });
|
|
952
|
+
const warnings = result.state.installed ? [] : ["GitNexus is not installed. Run fet graph setup for installation handoff instructions."];
|
|
953
|
+
ctx.output.result({
|
|
954
|
+
ok: true,
|
|
955
|
+
command: "graph status",
|
|
956
|
+
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.",
|
|
957
|
+
warnings,
|
|
958
|
+
nextSteps: result.state.installed && !result.state.graphExists ? ["Run fet graph init to build the first GitNexus graph"] : void 0,
|
|
959
|
+
data: result
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
async function graphDoctorCommand(ctx) {
|
|
963
|
+
const result = await refreshGraphState(ctx, { runStatus: true });
|
|
964
|
+
const warnings = [
|
|
965
|
+
...!result.state.installed ? ["GitNexus is not installed."] : [],
|
|
966
|
+
...result.state.installed && !result.state.graphExists ? ["GitNexus is installed but no graph directory was found."] : [],
|
|
967
|
+
...!result.state.handoffPath ? ["Graph handoff instructions have not been generated."] : []
|
|
968
|
+
];
|
|
969
|
+
ctx.output.result({
|
|
970
|
+
ok: true,
|
|
971
|
+
command: "graph doctor",
|
|
972
|
+
summary: warnings.length ? `Graph doctor completed with ${warnings.length} warning(s).` : "Graph doctor completed without warnings.",
|
|
973
|
+
warnings,
|
|
974
|
+
nextSteps: warnings.length ? ["Run fet graph setup", "Run fet graph handoff", "Run fet graph init when GitNexus is installed"] : void 0,
|
|
975
|
+
data: result
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
async function graphSetupCommand(ctx) {
|
|
979
|
+
let result;
|
|
980
|
+
const handoffPath = join11(ctx.projectRoot, ".fet", "graph-setup.md");
|
|
981
|
+
await withProjectLock(ctx.projectRoot, { command: "graph setup", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
982
|
+
result = await refreshGraphState(ctx, { write: false });
|
|
983
|
+
await writeHandoffFile(handoffPath, renderGraphSetupHandoff(result.state));
|
|
984
|
+
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
985
|
+
global.graph ??= {};
|
|
986
|
+
global.graph.gitnexus = {
|
|
987
|
+
...result.state,
|
|
988
|
+
setupHandoffPath: ".fet/graph-setup.md",
|
|
989
|
+
setupHandoffUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
990
|
+
};
|
|
991
|
+
await ctx.stateStore.writeGlobal(global);
|
|
992
|
+
});
|
|
993
|
+
ctx.output.result({
|
|
994
|
+
ok: true,
|
|
995
|
+
command: "graph setup",
|
|
996
|
+
summary: "GitNexus setup handoff generated.",
|
|
997
|
+
warnings: result.state.installed ? [] : ["GitNexus is not installed. The handoff explains installation and IDE-assisted setup options."],
|
|
998
|
+
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"],
|
|
999
|
+
data: {
|
|
1000
|
+
path: ".fet/graph-setup.md",
|
|
1001
|
+
gitnexus: result.state
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
async function graphHandoffCommand(ctx) {
|
|
1006
|
+
let result;
|
|
1007
|
+
const handoffPath = join11(ctx.projectRoot, ".fet", "graph-handoff.md");
|
|
1008
|
+
await withProjectLock(ctx.projectRoot, { command: "graph handoff", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
1009
|
+
result = await refreshGraphState(ctx, { runStatus: true, write: false });
|
|
1010
|
+
await writeHandoffFile(handoffPath, renderGraphUsageHandoff(result.state));
|
|
1011
|
+
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
1012
|
+
global.graph ??= {};
|
|
1013
|
+
global.graph.gitnexus = {
|
|
1014
|
+
...result.state,
|
|
1015
|
+
handoffPath: ".fet/graph-handoff.md",
|
|
1016
|
+
handoffUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1017
|
+
};
|
|
1018
|
+
await ctx.stateStore.writeGlobal(global);
|
|
1019
|
+
});
|
|
1020
|
+
ctx.output.result({
|
|
1021
|
+
ok: true,
|
|
1022
|
+
command: "graph handoff",
|
|
1023
|
+
summary: "GitNexus graph usage handoff generated.",
|
|
1024
|
+
warnings: result.state.installed ? [] : ["GitNexus is not installed. The handoff still documents the fallback behavior."],
|
|
1025
|
+
nextSteps: ["Cursor/Codex/OpenCode: read .fet/graph-handoff.md before broad repository scans"],
|
|
1026
|
+
data: {
|
|
1027
|
+
path: ".fet/graph-handoff.md",
|
|
1028
|
+
gitnexus: result.state
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
async function graphAnalyzeCommand(ctx, mode, args) {
|
|
1033
|
+
const detection = await detectGitNexus();
|
|
1034
|
+
if (!detection.installed) {
|
|
1035
|
+
throw new FetError({
|
|
1036
|
+
code: "GRAPH_PROVIDER_NOT_FOUND" /* GraphProviderNotFound */,
|
|
1037
|
+
message: "GitNexus is not installed or is not available on PATH.",
|
|
1038
|
+
details: { executable: detection.executablePath, error: detection.error },
|
|
1039
|
+
suggestedCommand: "fet graph setup"
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
const run = await runGitNexus(["analyze", ...args], { cwd: ctx.projectRoot });
|
|
1043
|
+
if (run.exitCode !== 0) {
|
|
1044
|
+
throw new FetError({
|
|
1045
|
+
code: "GRAPH_COMMAND_FAILED" /* GraphCommandFailed */,
|
|
1046
|
+
message: "GitNexus analyze failed.",
|
|
1047
|
+
details: { command: run.command.join(" "), exitCode: run.exitCode, stdout: run.stdout, stderr: run.stderr },
|
|
1048
|
+
suggestedCommand: "fet graph doctor"
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
const result = await refreshGraphState(ctx, { write: false });
|
|
1052
|
+
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
1053
|
+
global.graph ??= {};
|
|
1054
|
+
global.graph.gitnexus = {
|
|
1055
|
+
...result.state,
|
|
1056
|
+
lastRefreshAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1057
|
+
};
|
|
1058
|
+
await ctx.stateStore.writeGlobal(global);
|
|
1059
|
+
ctx.output.result({
|
|
1060
|
+
ok: true,
|
|
1061
|
+
command: `graph ${mode}`,
|
|
1062
|
+
summary: mode === "init" ? "GitNexus graph initialized." : "GitNexus graph refreshed.",
|
|
1063
|
+
warnings: result.state.graphExists ? [] : ["GitNexus analyze completed, but the configured graph directory was not found."],
|
|
1064
|
+
nextSteps: ["Run fet graph status", "Use .fet/graph-handoff.md or generated IDE prompts to prefer graph context"],
|
|
1065
|
+
data: {
|
|
1066
|
+
gitnexus: global.graph.gitnexus,
|
|
1067
|
+
run: {
|
|
1068
|
+
command: run.command,
|
|
1069
|
+
stdout: run.stdout.trim(),
|
|
1070
|
+
stderr: run.stderr.trim()
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
async function refreshGraphState(ctx, options = {}) {
|
|
1076
|
+
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
1077
|
+
global.graph ??= {};
|
|
1078
|
+
const detection = await detectGitNexus();
|
|
1079
|
+
const graph2 = await inspectGitNexusGraph(ctx.projectRoot);
|
|
1080
|
+
let state = mergeGitNexusGraphInfo(toGitNexusState(detection, global.graph.gitnexus), graph2);
|
|
1081
|
+
let gitnexusStatus = null;
|
|
1082
|
+
if (options.runStatus && detection.installed) {
|
|
1083
|
+
gitnexusStatus = await runGitNexus(["status"], { cwd: ctx.projectRoot });
|
|
1084
|
+
state = {
|
|
1085
|
+
...state,
|
|
1086
|
+
lastStatus: firstLine(gitnexusStatus.stdout) || firstLine(gitnexusStatus.stderr) || `exit ${gitnexusStatus.exitCode}`
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
if (options.write ?? true) {
|
|
1090
|
+
global.graph.gitnexus = state;
|
|
1091
|
+
await ctx.stateStore.writeGlobal(global);
|
|
1092
|
+
}
|
|
1093
|
+
return {
|
|
1094
|
+
state,
|
|
1095
|
+
gitnexusStatus: gitnexusStatus ? {
|
|
1096
|
+
exitCode: gitnexusStatus.exitCode,
|
|
1097
|
+
command: gitnexusStatus.command,
|
|
1098
|
+
stdout: gitnexusStatus.stdout.trim(),
|
|
1099
|
+
stderr: gitnexusStatus.stderr.trim()
|
|
1100
|
+
} : null
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
async function writeHandoffFile(path, content) {
|
|
1104
|
+
await mkdir4(dirname6(path), { recursive: true });
|
|
1105
|
+
await atomicWrite(path, content);
|
|
1106
|
+
}
|
|
1107
|
+
function renderGraphSetupHandoff(state) {
|
|
1108
|
+
return `<!-- FET:MANAGED
|
|
1109
|
+
schemaVersion: 1
|
|
1110
|
+
generator: graph-setup
|
|
1111
|
+
FET:END -->
|
|
1112
|
+
|
|
1113
|
+
# FET Graph Setup
|
|
1114
|
+
|
|
1115
|
+
GitNexus graph support is optional. FET does not install GitNexus automatically and does not require graph support for OpenSpec workflows.
|
|
1116
|
+
|
|
1117
|
+
Current status:
|
|
1118
|
+
|
|
1119
|
+
- Installed: ${state.installed ? "yes" : "no"}
|
|
1120
|
+
- Executable: ${state.executablePath ?? "gitnexus"}
|
|
1121
|
+
- Version: ${state.version ?? "unknown"}
|
|
1122
|
+
- Graph path: ${state.graphPath ?? ".gitnexus"}
|
|
1123
|
+
- Graph exists: ${state.graphExists ? "yes" : "no"}
|
|
1124
|
+
|
|
1125
|
+
Suggested setup flow:
|
|
1126
|
+
|
|
1127
|
+
1. If GitNexus is not installed, install it using the method recommended by the GitNexus project.
|
|
1128
|
+
2. If you want GitNexus MCP or IDE integration, run \`gitnexus setup\` yourself after reviewing what it changes.
|
|
1129
|
+
3. Return to this project and run \`fet graph init\` to build the first graph.
|
|
1130
|
+
4. Run \`fet graph handoff\` so IDE AI can prefer graph context before broad repository scans.
|
|
1131
|
+
|
|
1132
|
+
Guardrails:
|
|
1133
|
+
|
|
1134
|
+
- Do not block FET/OpenSpec commands when GitNexus is unavailable.
|
|
1135
|
+
- Do not generate or modify application code during setup.
|
|
1136
|
+
- Do not run global IDE configuration commands unless the user explicitly approves them.
|
|
733
1137
|
`;
|
|
734
1138
|
}
|
|
1139
|
+
function renderGraphUsageHandoff(state) {
|
|
1140
|
+
return `<!-- FET:MANAGED
|
|
1141
|
+
schemaVersion: 1
|
|
1142
|
+
generator: graph-handoff
|
|
1143
|
+
FET:END -->
|
|
1144
|
+
|
|
1145
|
+
# FET Graph Handoff
|
|
1146
|
+
|
|
1147
|
+
Use GitNexus graph context as an optional first pass before broad repository scans.
|
|
1148
|
+
|
|
1149
|
+
Current status:
|
|
1150
|
+
|
|
1151
|
+
- Installed: ${state.installed ? "yes" : "no"}
|
|
1152
|
+
- Graph path: ${state.graphPath ?? ".gitnexus"}
|
|
1153
|
+
- Graph exists: ${state.graphExists ? "yes" : "no"}
|
|
1154
|
+
- Last indexed at: ${state.lastIndexedAt ?? "unknown"}
|
|
1155
|
+
- Last status: ${state.lastStatus ?? "unknown"}
|
|
1156
|
+
|
|
1157
|
+
When graph context is available:
|
|
1158
|
+
|
|
1159
|
+
1. Use the graph to identify likely modules, dependencies, and insertion points.
|
|
1160
|
+
2. Read only the concrete source files needed to confirm behavior.
|
|
1161
|
+
3. Prefer OpenSpec artifacts and AGENTS.md over graph guesses when they conflict.
|
|
1162
|
+
4. Fall back to normal repository inspection if the graph is missing, stale, or incomplete.
|
|
1163
|
+
|
|
1164
|
+
When producing OpenSpec artifacts:
|
|
1165
|
+
|
|
1166
|
+
- Use graph context to make proposal, design, specs, and tasks more precise.
|
|
1167
|
+
- Avoid large repository scans when the graph already narrows the relevant area.
|
|
1168
|
+
- Keep all generated artifacts in the normal OpenSpec change directory.
|
|
1169
|
+
`;
|
|
1170
|
+
}
|
|
1171
|
+
function firstLine(value) {
|
|
1172
|
+
return value.trim().split(/\r?\n/)[0]?.trim() || null;
|
|
1173
|
+
}
|
|
735
1174
|
|
|
736
1175
|
// src/commands/proxy.ts
|
|
737
1176
|
import { readFile as readFile10 } from "fs/promises";
|
|
738
|
-
import { join as
|
|
1177
|
+
import { join as join13 } from "path";
|
|
739
1178
|
|
|
740
1179
|
// src/state/project.ts
|
|
741
1180
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -764,8 +1203,8 @@ async function git(cwd, args) {
|
|
|
764
1203
|
}
|
|
765
1204
|
|
|
766
1205
|
// src/state/store.ts
|
|
767
|
-
import { mkdir as
|
|
768
|
-
import { join as
|
|
1206
|
+
import { mkdir as mkdir5, readFile as readFile8 } from "fs/promises";
|
|
1207
|
+
import { join as join12 } from "path";
|
|
769
1208
|
|
|
770
1209
|
// src/state/schema.ts
|
|
771
1210
|
var phases = ["explore", "propose", "implement", "verify", "sync", "archive"];
|
|
@@ -874,7 +1313,7 @@ var StateStore = class {
|
|
|
874
1313
|
}
|
|
875
1314
|
async writeGlobal(state) {
|
|
876
1315
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
877
|
-
await
|
|
1316
|
+
await mkdir5(join12(this.projectRoot, "openspec"), { recursive: true });
|
|
878
1317
|
await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
|
|
879
1318
|
`);
|
|
880
1319
|
}
|
|
@@ -895,15 +1334,15 @@ var StateStore = class {
|
|
|
895
1334
|
}
|
|
896
1335
|
async writeChange(state) {
|
|
897
1336
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
898
|
-
await
|
|
1337
|
+
await mkdir5(join12(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
|
|
899
1338
|
await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
|
|
900
1339
|
`);
|
|
901
1340
|
}
|
|
902
1341
|
globalPath() {
|
|
903
|
-
return
|
|
1342
|
+
return join12(this.projectRoot, "openspec", "fet-state.json");
|
|
904
1343
|
}
|
|
905
1344
|
changePath(changeId) {
|
|
906
|
-
return
|
|
1345
|
+
return join12(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
|
|
907
1346
|
}
|
|
908
1347
|
};
|
|
909
1348
|
function isNotFound(error) {
|
|
@@ -1020,7 +1459,7 @@ async function createChangelogEntry(projectRoot, changeId) {
|
|
|
1020
1459
|
};
|
|
1021
1460
|
}
|
|
1022
1461
|
async function appendChangelog(projectRoot, entry) {
|
|
1023
|
-
const changelogPath =
|
|
1462
|
+
const changelogPath = join13(projectRoot, "CHANGELOG.md");
|
|
1024
1463
|
const existing = await readOptional3(changelogPath);
|
|
1025
1464
|
const block = `updateTime: ${entry.updateTime}
|
|
1026
1465
|
\u66F4\u65B0\u5185\u5BB9:${entry.content}
|
|
@@ -1031,12 +1470,12 @@ ${block}` : block;
|
|
|
1031
1470
|
await atomicWrite(changelogPath, next);
|
|
1032
1471
|
}
|
|
1033
1472
|
async function readChangeRequirement(projectRoot, changeId) {
|
|
1034
|
-
const changeRoot =
|
|
1035
|
-
const proposal = await readOptional3(
|
|
1473
|
+
const changeRoot = join13(projectRoot, "openspec", "changes", changeId);
|
|
1474
|
+
const proposal = await readOptional3(join13(changeRoot, "proposal.md"));
|
|
1036
1475
|
if (proposal) {
|
|
1037
1476
|
return summarizeMarkdown(proposal);
|
|
1038
1477
|
}
|
|
1039
|
-
const readme = await readOptional3(
|
|
1478
|
+
const readme = await readOptional3(join13(changeRoot, "README.md"));
|
|
1040
1479
|
if (readme) {
|
|
1041
1480
|
return summarizeMarkdown(readme);
|
|
1042
1481
|
}
|
|
@@ -1180,8 +1619,8 @@ async function assertVerified(ctx) {
|
|
|
1180
1619
|
|
|
1181
1620
|
// src/commands/verify.ts
|
|
1182
1621
|
import { createHash } from "crypto";
|
|
1183
|
-
import { mkdir as
|
|
1184
|
-
import { join as
|
|
1622
|
+
import { mkdir as mkdir6, readFile as readFile11, stat as stat5 } from "fs/promises";
|
|
1623
|
+
import { join as join14 } from "path";
|
|
1185
1624
|
async function verifyCommand(ctx, options) {
|
|
1186
1625
|
if (options.auto) {
|
|
1187
1626
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
@@ -1248,9 +1687,9 @@ async function verifyCommand(ctx, options) {
|
|
|
1248
1687
|
async function writeInstructions(ctx, changeId) {
|
|
1249
1688
|
await assertChangeExists(ctx, changeId);
|
|
1250
1689
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1251
|
-
const dir =
|
|
1252
|
-
const instructionsPath =
|
|
1253
|
-
await
|
|
1690
|
+
const dir = join14(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
1691
|
+
const instructionsPath = join14(dir, "verify-instructions.md");
|
|
1692
|
+
await mkdir6(dir, { recursive: true });
|
|
1254
1693
|
await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
|
|
1255
1694
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
1256
1695
|
state.currentPhase = "verify";
|
|
@@ -1266,7 +1705,7 @@ async function writeInstructions(ctx, changeId) {
|
|
|
1266
1705
|
async function markDone(ctx, changeId) {
|
|
1267
1706
|
await assertChangeExists(ctx, changeId);
|
|
1268
1707
|
const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1269
|
-
const instructionsPath =
|
|
1708
|
+
const instructionsPath = join14(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
|
|
1270
1709
|
const instructions = await readInstructions(instructionsPath, changeId);
|
|
1271
1710
|
const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
|
|
1272
1711
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
@@ -1301,7 +1740,7 @@ async function assertChangeExists(ctx, changeId) {
|
|
|
1301
1740
|
}
|
|
1302
1741
|
async function readInstructions(path, changeId) {
|
|
1303
1742
|
try {
|
|
1304
|
-
await
|
|
1743
|
+
await stat5(path);
|
|
1305
1744
|
const content = await readFile11(path, "utf8");
|
|
1306
1745
|
const fileChangeId = readFrontMatterValue(content, "changeId");
|
|
1307
1746
|
if (fileChangeId !== changeId) {
|
|
@@ -1419,9 +1858,9 @@ function renderIdeModelPolicy(command) {
|
|
|
1419
1858
|
import { resolve } from "path";
|
|
1420
1859
|
|
|
1421
1860
|
// src/adapters/codex/index.ts
|
|
1422
|
-
import { mkdir as
|
|
1861
|
+
import { mkdir as mkdir7, readFile as readFile12, stat as stat6 } from "fs/promises";
|
|
1423
1862
|
import { homedir } from "os";
|
|
1424
|
-
import { dirname as
|
|
1863
|
+
import { dirname as dirname7, join as join15 } from "path";
|
|
1425
1864
|
|
|
1426
1865
|
// src/adapters/commands.ts
|
|
1427
1866
|
var FET_WORKFLOW_COMMANDS = [
|
|
@@ -1437,7 +1876,18 @@ var FET_WORKFLOW_COMMANDS = [
|
|
|
1437
1876
|
"bulk-archive",
|
|
1438
1877
|
"onboard"
|
|
1439
1878
|
];
|
|
1440
|
-
var
|
|
1879
|
+
var FET_GRAPH_COMMANDS = ["graph-status", "graph-setup", "graph-init", "graph-refresh", "graph-doctor", "graph-handoff"];
|
|
1880
|
+
var FET_ADAPTER_COMMANDS = [...FET_WORKFLOW_COMMANDS, "fill-context", "passthrough", ...FET_GRAPH_COMMANDS];
|
|
1881
|
+
function renderFetAdapterUsage(command, args = "[...args]") {
|
|
1882
|
+
if (command.startsWith("graph-")) {
|
|
1883
|
+
const subcommand = command.slice("graph-".length);
|
|
1884
|
+
return `fet graph ${subcommand}${args ? ` ${args}` : ""}`;
|
|
1885
|
+
}
|
|
1886
|
+
if (command === "passthrough") {
|
|
1887
|
+
return `fet passthrough <openspec-command>${args ? ` ${args}` : ""}`;
|
|
1888
|
+
}
|
|
1889
|
+
return `fet ${command}${args ? ` ${args}` : ""}`;
|
|
1890
|
+
}
|
|
1441
1891
|
|
|
1442
1892
|
// src/adapters/codex/templates.ts
|
|
1443
1893
|
function codexGuideFile() {
|
|
@@ -1456,6 +1906,7 @@ Before doing FET or OpenSpec work in Codex, read:
|
|
|
1456
1906
|
|
|
1457
1907
|
- AGENTS.md
|
|
1458
1908
|
- openspec/config.yaml
|
|
1909
|
+
- .codex/fet/karpathy-guidelines.md
|
|
1459
1910
|
- the active change files under openspec/changes/<change-id>/, when a change is selected
|
|
1460
1911
|
|
|
1461
1912
|
If GitNexus code graph context is available in the IDE or MCP tools, prefer it before broad repository scans. Use it to identify relevant modules, dependencies, and insertion points, then read only the concrete source files needed. If GitNexus is unavailable, continue with the normal FET/OpenSpec workflow.
|
|
@@ -1467,10 +1918,13 @@ Command guides live in .codex/fet/commands/.
|
|
|
1467
1918
|
};
|
|
1468
1919
|
}
|
|
1469
1920
|
function codexCommandFiles() {
|
|
1470
|
-
return
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1921
|
+
return [
|
|
1922
|
+
codexKarpathyGuidelinesFile(),
|
|
1923
|
+
...FET_ADAPTER_COMMANDS.map((command) => ({
|
|
1924
|
+
path: `.codex/fet/commands/${command}.md`,
|
|
1925
|
+
content: renderCommand(command)
|
|
1926
|
+
}))
|
|
1927
|
+
];
|
|
1474
1928
|
}
|
|
1475
1929
|
function codexSlashPromptFiles() {
|
|
1476
1930
|
return FET_ADAPTER_COMMANDS.map((command) => ({
|
|
@@ -1478,6 +1932,22 @@ function codexSlashPromptFiles() {
|
|
|
1478
1932
|
content: renderSlashPrompt(command)
|
|
1479
1933
|
}));
|
|
1480
1934
|
}
|
|
1935
|
+
function codexKarpathyGuidelinesFile() {
|
|
1936
|
+
return {
|
|
1937
|
+
path: ".codex/fet/karpathy-guidelines.md",
|
|
1938
|
+
content: `<!-- FET:MANAGED
|
|
1939
|
+
schemaVersion: 1
|
|
1940
|
+
fetVersion: ${FET_VERSION}
|
|
1941
|
+
generator: codex-adapter
|
|
1942
|
+
adapterVersion: 1
|
|
1943
|
+
FET:END -->
|
|
1944
|
+
|
|
1945
|
+
# Andrej Karpathy Inspired Coding Guidelines
|
|
1946
|
+
|
|
1947
|
+
${renderKarpathyGuidelinesBody()}
|
|
1948
|
+
`
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1481
1951
|
function renderCommand(command) {
|
|
1482
1952
|
if (command === "fill-context") {
|
|
1483
1953
|
return renderFillContextCommand();
|
|
@@ -1485,26 +1955,32 @@ function renderCommand(command) {
|
|
|
1485
1955
|
if (command === "passthrough") {
|
|
1486
1956
|
return renderPassthroughCommand();
|
|
1487
1957
|
}
|
|
1958
|
+
if (command.startsWith("graph-")) {
|
|
1959
|
+
return renderGraphCommand(command);
|
|
1960
|
+
}
|
|
1961
|
+
const usage = renderFetAdapterUsage(command, "");
|
|
1488
1962
|
return `<!-- FET:MANAGED
|
|
1489
1963
|
schemaVersion: 1
|
|
1490
1964
|
fetVersion: ${FET_VERSION}
|
|
1491
1965
|
generator: codex-adapter
|
|
1492
1966
|
adapterVersion: 1
|
|
1493
|
-
command:
|
|
1967
|
+
command: ${usage}
|
|
1494
1968
|
FET:END -->
|
|
1495
1969
|
|
|
1496
|
-
#
|
|
1970
|
+
# ${usage}
|
|
1497
1971
|
|
|
1498
1972
|
${renderIdeModelPolicy(command)}
|
|
1499
1973
|
|
|
1500
1974
|
When the user asks Codex to run the FET ${command} workflow, first make sure the project context is loaded from AGENTS.md and openspec/config.yaml.
|
|
1501
1975
|
|
|
1976
|
+
Also read .codex/fet/karpathy-guidelines.md and follow it unless it conflicts with the user's latest request or OpenSpec artifacts.
|
|
1977
|
+
|
|
1502
1978
|
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.
|
|
1503
1979
|
|
|
1504
1980
|
Then run:
|
|
1505
1981
|
|
|
1506
1982
|
\`\`\`sh
|
|
1507
|
-
|
|
1983
|
+
${usage}
|
|
1508
1984
|
\`\`\`
|
|
1509
1985
|
|
|
1510
1986
|
If the command needs a change id, pass it with \`--change <change-id>\` or use the active OpenSpec change from the user's request.
|
|
@@ -1527,6 +2003,8 @@ ${renderIdeModelPolicy("passthrough")}
|
|
|
1527
2003
|
|
|
1528
2004
|
When the user asks Codex to run an OpenSpec command that FET does not manage as a first-class workflow command, use FET passthrough instead of calling OpenSpec directly.
|
|
1529
2005
|
|
|
2006
|
+
Also read .codex/fet/karpathy-guidelines.md and follow it unless it conflicts with the user's latest request or OpenSpec artifacts.
|
|
2007
|
+
|
|
1530
2008
|
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.
|
|
1531
2009
|
|
|
1532
2010
|
Then run:
|
|
@@ -1538,6 +2016,38 @@ fet passthrough <openspec-command> [...args]
|
|
|
1538
2016
|
This preserves the FET entry point while allowing access to unmanaged or newly added OpenSpec commands. Passthrough does not update FET lifecycle state.
|
|
1539
2017
|
`;
|
|
1540
2018
|
}
|
|
2019
|
+
function renderGraphCommand(command) {
|
|
2020
|
+
const usage = renderFetAdapterUsage(command, "");
|
|
2021
|
+
const subcommand = command.slice("graph-".length);
|
|
2022
|
+
return `<!-- FET:MANAGED
|
|
2023
|
+
schemaVersion: 1
|
|
2024
|
+
fetVersion: ${FET_VERSION}
|
|
2025
|
+
generator: codex-adapter
|
|
2026
|
+
adapterVersion: 1
|
|
2027
|
+
command: ${usage}
|
|
2028
|
+
FET:END -->
|
|
2029
|
+
|
|
2030
|
+
# ${usage}
|
|
2031
|
+
|
|
2032
|
+
${renderIdeModelPolicy(command)}
|
|
2033
|
+
|
|
2034
|
+
When the user asks Codex to work with optional GitNexus graph support, use FET as the entry point.
|
|
2035
|
+
|
|
2036
|
+
Also read .codex/fet/karpathy-guidelines.md and follow it unless it conflicts with the user's latest request or OpenSpec artifacts.
|
|
2037
|
+
|
|
2038
|
+
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.
|
|
2039
|
+
|
|
2040
|
+
Run:
|
|
2041
|
+
|
|
2042
|
+
\`\`\`sh
|
|
2043
|
+
${usage}
|
|
2044
|
+
\`\`\`
|
|
2045
|
+
|
|
2046
|
+
For graph init or refresh, pass extra GitNexus analyze arguments only when the user provides them.
|
|
2047
|
+
|
|
2048
|
+
After the command completes, report the GitNexus state, generated handoff files, and next steps.
|
|
2049
|
+
`;
|
|
2050
|
+
}
|
|
1541
2051
|
function renderSlashPrompt(command) {
|
|
1542
2052
|
if (command === "continue") {
|
|
1543
2053
|
return renderContinueSlashPrompt();
|
|
@@ -1575,9 +2085,10 @@ function renderSlashPrompt(command) {
|
|
|
1575
2085
|
if (command === "passthrough") {
|
|
1576
2086
|
return renderPassthroughSlashPrompt();
|
|
1577
2087
|
}
|
|
1578
|
-
const usage = command
|
|
1579
|
-
const
|
|
1580
|
-
const
|
|
2088
|
+
const usage = renderFetAdapterUsage(command);
|
|
2089
|
+
const isGraph = command.startsWith("graph-");
|
|
2090
|
+
const shellCommand = isGraph ? `${renderFetAdapterUsage(command, "")} $ARGUMENTS` : `fet ${command} $ARGUMENTS`;
|
|
2091
|
+
const description = isGraph ? `Run optional GitNexus graph ${command.slice("graph-".length)} through FET` : `Run the FET-managed OpenSpec ${command} workflow`;
|
|
1581
2092
|
return `<!-- FET:MANAGED
|
|
1582
2093
|
schemaVersion: 1
|
|
1583
2094
|
fetVersion: ${FET_VERSION}
|
|
@@ -1595,6 +2106,8 @@ Use FET as the entry point for this OpenSpec workflow.
|
|
|
1595
2106
|
|
|
1596
2107
|
Before running the command, make sure the relevant project context is loaded from AGENTS.md and openspec/config.yaml. If a change id is needed and was not provided, infer it from the active FET/OpenSpec state when unambiguous; otherwise ask the user for the change id.
|
|
1597
2108
|
|
|
2109
|
+
Also read .codex/fet/karpathy-guidelines.md and follow it unless it conflicts with the user's latest request or OpenSpec artifacts.
|
|
2110
|
+
|
|
1598
2111
|
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.
|
|
1599
2112
|
|
|
1600
2113
|
Run:
|
|
@@ -1621,6 +2134,8 @@ ${renderIdeModelPolicy("fill-context")}
|
|
|
1621
2134
|
|
|
1622
2135
|
Use this command to complete FET-generated project context placeholders with Codex.
|
|
1623
2136
|
|
|
2137
|
+
Also read .codex/fet/karpathy-guidelines.md and follow it unless it conflicts with the user's latest request or OpenSpec artifacts.
|
|
2138
|
+
|
|
1624
2139
|
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.
|
|
1625
2140
|
|
|
1626
2141
|
First run:
|
|
@@ -1645,16 +2160,17 @@ Steps:
|
|
|
1645
2160
|
fet fill-context
|
|
1646
2161
|
\`\`\`
|
|
1647
2162
|
2. Read AGENTS.md and openspec/config.yaml.
|
|
1648
|
-
3.
|
|
2163
|
+
3. Read .codex/fet/karpathy-guidelines.md.
|
|
2164
|
+
4. Inspect the project to understand:
|
|
1649
2165
|
- source structure and major modules
|
|
1650
2166
|
- framework and routing conventions
|
|
1651
2167
|
- scripts, test commands, and build commands
|
|
1652
2168
|
- coding conventions and project-specific patterns
|
|
1653
2169
|
- important docs such as README files
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
2170
|
+
5. Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete, concise project-specific content.
|
|
2171
|
+
6. Preserve FET managed markers such as \`FET:BEGIN AUTO\`, \`FET:END AUTO\`, \`FET:BEGIN USER\`, and \`FET:END USER\`.
|
|
2172
|
+
7. Do not modify business code.
|
|
2173
|
+
8. Run:
|
|
1658
2174
|
\`\`\`sh
|
|
1659
2175
|
fet doctor
|
|
1660
2176
|
\`\`\`
|
|
@@ -2033,7 +2549,7 @@ var CodexAdapter = class {
|
|
|
2033
2549
|
adapterVersion = 1;
|
|
2034
2550
|
async detect(projectRoot) {
|
|
2035
2551
|
return {
|
|
2036
|
-
detected: await exists3(
|
|
2552
|
+
detected: await exists3(join15(projectRoot, ".codex")) || await exists3(join15(projectRoot, "AGENTS.md")),
|
|
2037
2553
|
reason: "Codex adapter is available for projects that use AGENTS.md"
|
|
2038
2554
|
};
|
|
2039
2555
|
}
|
|
@@ -2072,7 +2588,7 @@ var CodexAdapter = class {
|
|
|
2072
2588
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
2073
2589
|
await createBackup(target);
|
|
2074
2590
|
}
|
|
2075
|
-
await
|
|
2591
|
+
await mkdir7(dirname7(target), { recursive: true });
|
|
2076
2592
|
await atomicWrite(target, file.content);
|
|
2077
2593
|
written.push(displayPath);
|
|
2078
2594
|
}
|
|
@@ -2099,9 +2615,9 @@ var CodexAdapter = class {
|
|
|
2099
2615
|
};
|
|
2100
2616
|
function resolveTarget(projectRoot, file) {
|
|
2101
2617
|
if (file.root === "codex-home") {
|
|
2102
|
-
return
|
|
2618
|
+
return join15(resolveCodexHome(), file.path);
|
|
2103
2619
|
}
|
|
2104
|
-
return
|
|
2620
|
+
return join15(projectRoot, file.path);
|
|
2105
2621
|
}
|
|
2106
2622
|
function displayPathFor(file) {
|
|
2107
2623
|
if (file.root === "codex-home") {
|
|
@@ -2110,7 +2626,7 @@ function displayPathFor(file) {
|
|
|
2110
2626
|
return file.path;
|
|
2111
2627
|
}
|
|
2112
2628
|
function resolveCodexHome() {
|
|
2113
|
-
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ??
|
|
2629
|
+
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join15(homedir(), ".codex");
|
|
2114
2630
|
}
|
|
2115
2631
|
async function readExisting(path) {
|
|
2116
2632
|
try {
|
|
@@ -2121,7 +2637,7 @@ async function readExisting(path) {
|
|
|
2121
2637
|
}
|
|
2122
2638
|
async function exists3(path) {
|
|
2123
2639
|
try {
|
|
2124
|
-
await
|
|
2640
|
+
await stat6(path);
|
|
2125
2641
|
return true;
|
|
2126
2642
|
} catch {
|
|
2127
2643
|
return false;
|
|
@@ -2129,8 +2645,8 @@ async function exists3(path) {
|
|
|
2129
2645
|
}
|
|
2130
2646
|
|
|
2131
2647
|
// src/adapters/cursor/index.ts
|
|
2132
|
-
import { mkdir as
|
|
2133
|
-
import { dirname as
|
|
2648
|
+
import { mkdir as mkdir8, readFile as readFile13, stat as stat7 } from "fs/promises";
|
|
2649
|
+
import { dirname as dirname8, join as join16 } from "path";
|
|
2134
2650
|
|
|
2135
2651
|
// src/adapters/cursor/templates.ts
|
|
2136
2652
|
function cursorSkillFiles() {
|
|
@@ -2166,7 +2682,7 @@ alwaysApply: false
|
|
|
2166
2682
|
};
|
|
2167
2683
|
}
|
|
2168
2684
|
function renderSkill(command) {
|
|
2169
|
-
const usage = command === "passthrough" ? "
|
|
2685
|
+
const usage = renderFetAdapterUsage(command, command === "passthrough" ? "[...args]" : "");
|
|
2170
2686
|
if (command === "fill-context") {
|
|
2171
2687
|
return `<!-- FET:MANAGED
|
|
2172
2688
|
schemaVersion: 1
|
|
@@ -2232,7 +2748,7 @@ var CursorAdapter = class {
|
|
|
2232
2748
|
adapterVersion = 1;
|
|
2233
2749
|
async detect(projectRoot) {
|
|
2234
2750
|
return {
|
|
2235
|
-
detected: await exists4(
|
|
2751
|
+
detected: await exists4(join16(projectRoot, ".cursor")),
|
|
2236
2752
|
reason: "Cursor adapter is available for any project"
|
|
2237
2753
|
};
|
|
2238
2754
|
}
|
|
@@ -2249,7 +2765,7 @@ var CursorAdapter = class {
|
|
|
2249
2765
|
const written = [];
|
|
2250
2766
|
const skipped = [];
|
|
2251
2767
|
for (const file of plan.files) {
|
|
2252
|
-
const target =
|
|
2768
|
+
const target = join16(projectRoot, file.path);
|
|
2253
2769
|
const existing = await readExisting2(target);
|
|
2254
2770
|
if (existing && !existing.includes("FET:MANAGED") && !force) {
|
|
2255
2771
|
throw new FetError({
|
|
@@ -2262,7 +2778,7 @@ var CursorAdapter = class {
|
|
|
2262
2778
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
2263
2779
|
await createBackup(target);
|
|
2264
2780
|
}
|
|
2265
|
-
await
|
|
2781
|
+
await mkdir8(dirname8(target), { recursive: true });
|
|
2266
2782
|
await atomicWrite(target, file.content);
|
|
2267
2783
|
written.push(file.path);
|
|
2268
2784
|
}
|
|
@@ -2272,7 +2788,7 @@ var CursorAdapter = class {
|
|
|
2272
2788
|
const plan = await this.planInstall(projectRoot);
|
|
2273
2789
|
const checks = [];
|
|
2274
2790
|
for (const file of plan.files) {
|
|
2275
|
-
const target =
|
|
2791
|
+
const target = join16(projectRoot, file.path);
|
|
2276
2792
|
const content = await readExisting2(target);
|
|
2277
2793
|
const managed = Boolean(content?.includes("FET:MANAGED"));
|
|
2278
2794
|
const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
|
|
@@ -2295,7 +2811,7 @@ async function readExisting2(path) {
|
|
|
2295
2811
|
}
|
|
2296
2812
|
async function exists4(path) {
|
|
2297
2813
|
try {
|
|
2298
|
-
await
|
|
2814
|
+
await stat7(path);
|
|
2299
2815
|
return true;
|
|
2300
2816
|
} catch {
|
|
2301
2817
|
return false;
|
|
@@ -2307,13 +2823,13 @@ import { execFile as execFile4 } from "child_process";
|
|
|
2307
2823
|
import { promisify as promisify4 } from "util";
|
|
2308
2824
|
|
|
2309
2825
|
// src/openspec/inspector.ts
|
|
2310
|
-
import { readdir, stat as
|
|
2311
|
-
import { join as
|
|
2826
|
+
import { readdir, stat as stat8 } from "fs/promises";
|
|
2827
|
+
import { join as join17 } from "path";
|
|
2312
2828
|
async function inspectOpenSpecProject(projectRoot) {
|
|
2313
|
-
const openspecPath =
|
|
2314
|
-
const changesPath =
|
|
2315
|
-
const legacyArchivePath =
|
|
2316
|
-
const changesArchivePath =
|
|
2829
|
+
const openspecPath = join17(projectRoot, "openspec");
|
|
2830
|
+
const changesPath = join17(openspecPath, "changes");
|
|
2831
|
+
const legacyArchivePath = join17(openspecPath, "archive");
|
|
2832
|
+
const changesArchivePath = join17(changesPath, "archive");
|
|
2317
2833
|
return {
|
|
2318
2834
|
exists: await exists5(openspecPath),
|
|
2319
2835
|
changes: await listDirectories(changesPath, { exclude: ["archive"] }),
|
|
@@ -2321,13 +2837,13 @@ async function inspectOpenSpecProject(projectRoot) {
|
|
|
2321
2837
|
};
|
|
2322
2838
|
}
|
|
2323
2839
|
async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
2324
|
-
const changePath =
|
|
2325
|
-
const tasksPath =
|
|
2326
|
-
const specsPath =
|
|
2840
|
+
const changePath = join17(projectRoot, "openspec", "changes", changeId);
|
|
2841
|
+
const tasksPath = join17(changePath, "tasks.md");
|
|
2842
|
+
const specsPath = join17(changePath, "specs");
|
|
2327
2843
|
return {
|
|
2328
2844
|
changeId,
|
|
2329
2845
|
exists: await exists5(changePath),
|
|
2330
|
-
hasProposal: await exists5(
|
|
2846
|
+
hasProposal: await exists5(join17(changePath, "proposal.md")),
|
|
2331
2847
|
hasTasks: await exists5(tasksPath),
|
|
2332
2848
|
hasSpecs: await exists5(specsPath),
|
|
2333
2849
|
tasksPath,
|
|
@@ -2345,7 +2861,7 @@ async function listDirectories(path, options = {}) {
|
|
|
2345
2861
|
}
|
|
2346
2862
|
async function exists5(path) {
|
|
2347
2863
|
try {
|
|
2348
|
-
await
|
|
2864
|
+
await stat8(path);
|
|
2349
2865
|
return true;
|
|
2350
2866
|
} catch {
|
|
2351
2867
|
return false;
|
|
@@ -2504,12 +3020,12 @@ function parseCommands(help) {
|
|
|
2504
3020
|
}
|
|
2505
3021
|
|
|
2506
3022
|
// src/scanner/package.ts
|
|
2507
|
-
import { readFile as readFile14, stat as
|
|
2508
|
-
import { join as
|
|
3023
|
+
import { readFile as readFile14, stat as stat9 } from "fs/promises";
|
|
3024
|
+
import { join as join18 } from "path";
|
|
2509
3025
|
import { parse as parse2 } from "yaml";
|
|
2510
3026
|
async function readPackageJson(projectRoot) {
|
|
2511
3027
|
try {
|
|
2512
|
-
return JSON.parse(await readFile14(
|
|
3028
|
+
return JSON.parse(await readFile14(join18(projectRoot, "package.json"), "utf8"));
|
|
2513
3029
|
} catch {
|
|
2514
3030
|
return null;
|
|
2515
3031
|
}
|
|
@@ -2575,7 +3091,7 @@ function detectFramework(pkg) {
|
|
|
2575
3091
|
}
|
|
2576
3092
|
async function detectLanguage(projectRoot, pkg) {
|
|
2577
3093
|
const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
|
|
2578
|
-
if (deps.typescript || await exists6(
|
|
3094
|
+
if (deps.typescript || await exists6(join18(projectRoot, "tsconfig.json"))) {
|
|
2579
3095
|
return "typescript";
|
|
2580
3096
|
}
|
|
2581
3097
|
return "javascript";
|
|
@@ -2590,7 +3106,7 @@ async function detectWorkspaces(projectRoot, pkg) {
|
|
|
2590
3106
|
return packageWorkspaces;
|
|
2591
3107
|
}
|
|
2592
3108
|
try {
|
|
2593
|
-
const workspace = parse2(await readFile14(
|
|
3109
|
+
const workspace = parse2(await readFile14(join18(projectRoot, "pnpm-workspace.yaml"), "utf8"));
|
|
2594
3110
|
return (workspace?.packages ?? []).map((path) => ({
|
|
2595
3111
|
name: path,
|
|
2596
3112
|
path,
|
|
@@ -2610,7 +3126,7 @@ async function detectLockManagers(projectRoot) {
|
|
|
2610
3126
|
];
|
|
2611
3127
|
const found = [];
|
|
2612
3128
|
for (const [file, manager] of lockFiles) {
|
|
2613
|
-
if (await exists6(
|
|
3129
|
+
if (await exists6(join18(projectRoot, file))) {
|
|
2614
3130
|
found.push(manager);
|
|
2615
3131
|
}
|
|
2616
3132
|
}
|
|
@@ -2627,7 +3143,7 @@ function scriptCommand(packageManager, name) {
|
|
|
2627
3143
|
}
|
|
2628
3144
|
async function exists6(path) {
|
|
2629
3145
|
try {
|
|
2630
|
-
await
|
|
3146
|
+
await stat9(path);
|
|
2631
3147
|
return true;
|
|
2632
3148
|
} catch {
|
|
2633
3149
|
return false;
|
|
@@ -2635,13 +3151,13 @@ async function exists6(path) {
|
|
|
2635
3151
|
}
|
|
2636
3152
|
|
|
2637
3153
|
// src/scanner/routes.ts
|
|
2638
|
-
import { readdir as readdir2, stat as
|
|
2639
|
-
import { join as
|
|
3154
|
+
import { readdir as readdir2, stat as stat10 } from "fs/promises";
|
|
3155
|
+
import { join as join19, relative, sep } from "path";
|
|
2640
3156
|
async function scanRoutes(projectRoot) {
|
|
2641
3157
|
const candidates = ["src/routes", "src/pages", "app", "pages"];
|
|
2642
3158
|
const routes = [];
|
|
2643
3159
|
for (const candidate of candidates) {
|
|
2644
|
-
const root =
|
|
3160
|
+
const root = join19(projectRoot, candidate);
|
|
2645
3161
|
if (!await exists7(root)) {
|
|
2646
3162
|
continue;
|
|
2647
3163
|
}
|
|
@@ -2669,7 +3185,7 @@ async function listFiles(root) {
|
|
|
2669
3185
|
const entries = await readdir2(root, { withFileTypes: true });
|
|
2670
3186
|
const files = [];
|
|
2671
3187
|
for (const entry of entries) {
|
|
2672
|
-
const path =
|
|
3188
|
+
const path = join19(root, entry.name);
|
|
2673
3189
|
if (entry.isDirectory()) {
|
|
2674
3190
|
files.push(...await listFiles(path));
|
|
2675
3191
|
} else {
|
|
@@ -2680,7 +3196,7 @@ async function listFiles(root) {
|
|
|
2680
3196
|
}
|
|
2681
3197
|
async function exists7(path) {
|
|
2682
3198
|
try {
|
|
2683
|
-
await
|
|
3199
|
+
await stat10(path);
|
|
2684
3200
|
return true;
|
|
2685
3201
|
} catch {
|
|
2686
3202
|
return false;
|
|
@@ -2827,6 +3343,13 @@ program.name("fet").description("Frontend workflow orchestration tool built arou
|
|
|
2827
3343
|
addGlobalOptions(program.command("init")).description("\u521D\u59CB\u5316 FET + OpenSpec").action(wrap("init", initCommand));
|
|
2828
3344
|
addGlobalOptions(program.command("update-context")).description("\u66F4\u65B0\u9879\u76EE\u4E0A\u4E0B\u6587").action(wrap("update-context", updateContextCommand));
|
|
2829
3345
|
addGlobalOptions(program.command("fill-context")).description("Refresh IDE prompts for filling AGENTS.md placeholders").action(wrap("fill-context", fillContextCommand));
|
|
3346
|
+
var graph = addGlobalOptions(program.command("graph").description("Manage optional GitNexus code graph support"));
|
|
3347
|
+
for (const action of ["status", "setup", "doctor", "handoff"]) {
|
|
3348
|
+
addGlobalOptions(graph.command(action).description(`Run fet graph ${action}`)).action(wrap("graph", (ctx) => graphCommand(ctx, action)));
|
|
3349
|
+
}
|
|
3350
|
+
for (const action of ["init", "refresh"]) {
|
|
3351
|
+
addGlobalOptions(graph.command(`${action} [args...]`).description(`Run GitNexus analyze for graph ${action}`).allowUnknownOption(true).passThroughOptions()).action(wrap("graph", (ctx, args = []) => graphCommand(ctx, action, args)));
|
|
3352
|
+
}
|
|
2830
3353
|
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
3354
|
wrap("doctor", (ctx, options) => doctorCommand(ctx, { fixLock: Boolean(options.fixLock) }))
|
|
2832
3355
|
);
|