@nick848/fet 1.0.2 → 1.0.4
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 +26 -17
- package/dist/cli/index.js +616 -126
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -5,11 +5,12 @@ import {
|
|
|
5
5
|
} from "../chunk-FZOVNHE7.js";
|
|
6
6
|
|
|
7
7
|
// src/cli/index.ts
|
|
8
|
+
import { createInterface } from "readline/promises";
|
|
8
9
|
import { Command } from "commander";
|
|
9
10
|
|
|
10
11
|
// src/commands/init.ts
|
|
11
|
-
import { readFile as
|
|
12
|
-
import { join as
|
|
12
|
+
import { readFile as readFile6, stat as stat2 } from "fs/promises";
|
|
13
|
+
import { join as join7 } from "path";
|
|
13
14
|
|
|
14
15
|
// src/fs/atomic-write.ts
|
|
15
16
|
import { dirname } from "path";
|
|
@@ -115,6 +116,48 @@ async function writeInitJournal(projectRoot, journal) {
|
|
|
115
116
|
);
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
// src/gitnexus.ts
|
|
120
|
+
import { execFile } from "child_process";
|
|
121
|
+
import { promisify } from "util";
|
|
122
|
+
var execFileAsync = promisify(execFile);
|
|
123
|
+
async function detectGitNexus(env = process.env) {
|
|
124
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
125
|
+
const executable = env.FET_GITNEXUS_EXECUTABLE?.trim() || "gitnexus";
|
|
126
|
+
try {
|
|
127
|
+
const { stdout, stderr } = await execFileAsync(executable, ["--version"], { shell: process.platform === "win32" });
|
|
128
|
+
return {
|
|
129
|
+
installed: true,
|
|
130
|
+
executablePath: executable,
|
|
131
|
+
version: (stdout.trim() || stderr.trim() || "unknown").split(/\r?\n/)[0] ?? "unknown",
|
|
132
|
+
checkedAt
|
|
133
|
+
};
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return {
|
|
136
|
+
installed: false,
|
|
137
|
+
executablePath: executable,
|
|
138
|
+
version: null,
|
|
139
|
+
checkedAt,
|
|
140
|
+
error: error instanceof Error ? error.message : String(error)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function toGitNexusState(detection, previous) {
|
|
145
|
+
return {
|
|
146
|
+
provider: "gitnexus",
|
|
147
|
+
installed: detection.installed,
|
|
148
|
+
executablePath: detection.installed ? detection.executablePath : null,
|
|
149
|
+
version: detection.version,
|
|
150
|
+
checkedAt: detection.checkedAt,
|
|
151
|
+
recommendationShownAt: previous?.recommendationShownAt ?? null
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function renderGitNexusRecommendation(state) {
|
|
155
|
+
if (state.installed) {
|
|
156
|
+
return `Optional GitNexus detected (${state.version ?? "unknown"}). You can generate a code graph after init; future OpenSpec artifacts should prefer the graph when it is available.`;
|
|
157
|
+
}
|
|
158
|
+
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
|
+
}
|
|
160
|
+
|
|
118
161
|
// src/version.ts
|
|
119
162
|
import { existsSync, readFileSync } from "fs";
|
|
120
163
|
import { dirname as dirname4, join as join4, parse } from "path";
|
|
@@ -144,6 +187,7 @@ var AUTO_BEGIN = "<!-- FET:BEGIN AUTO -->";
|
|
|
144
187
|
var AUTO_END = "<!-- FET:END AUTO -->";
|
|
145
188
|
var USER_BEGIN = "<!-- FET:BEGIN USER -->";
|
|
146
189
|
var USER_END = "<!-- FET:END USER -->";
|
|
190
|
+
var LLM_PLACEHOLDER_PATTERN = /\[NEEDS? LLM INPUT\]/;
|
|
147
191
|
function hasManagedAutoRegion(content) {
|
|
148
192
|
return count(content, AUTO_BEGIN) === 1 && count(content, AUTO_END) === 1 && content.indexOf(AUTO_BEGIN) < content.indexOf(AUTO_END);
|
|
149
193
|
}
|
|
@@ -163,11 +207,41 @@ function replaceManagedRegion(existing, generated) {
|
|
|
163
207
|
}
|
|
164
208
|
const before = existing.slice(0, start);
|
|
165
209
|
const after = existing.slice(end + AUTO_END.length);
|
|
210
|
+
const existingAuto = extractAuto(existing);
|
|
166
211
|
const generatedAuto = extractAuto(generated);
|
|
167
212
|
return `${before}${AUTO_BEGIN}
|
|
168
|
-
${generatedAuto}
|
|
213
|
+
${mergeAutoRegion(existingAuto, generatedAuto)}
|
|
169
214
|
${AUTO_END}${after}`;
|
|
170
215
|
}
|
|
216
|
+
function mergeAutoRegion(existingAuto, generatedAuto) {
|
|
217
|
+
const generatedSections = splitMarkdownSections(generatedAuto);
|
|
218
|
+
const existingSections = new Map(splitMarkdownSections(existingAuto).map((section) => [section.heading, section]));
|
|
219
|
+
if (!generatedSections.length || !existingSections.size) {
|
|
220
|
+
return generatedAuto;
|
|
221
|
+
}
|
|
222
|
+
return generatedSections.map((section) => {
|
|
223
|
+
const existing = existingSections.get(section.heading);
|
|
224
|
+
if (existing && LLM_PLACEHOLDER_PATTERN.test(section.body) && !LLM_PLACEHOLDER_PATTERN.test(existing.body)) {
|
|
225
|
+
return existing.raw.trim();
|
|
226
|
+
}
|
|
227
|
+
return section.raw.trim();
|
|
228
|
+
}).join("\n\n");
|
|
229
|
+
}
|
|
230
|
+
function splitMarkdownSections(content) {
|
|
231
|
+
const matches = [...content.matchAll(/^## .+$/gm)];
|
|
232
|
+
if (!matches.length) {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
return matches.map((match, index) => {
|
|
236
|
+
const start = match.index ?? 0;
|
|
237
|
+
const end = matches[index + 1]?.index ?? content.length;
|
|
238
|
+
const raw = content.slice(start, end).trim();
|
|
239
|
+
const newline = raw.indexOf("\n");
|
|
240
|
+
const heading = newline === -1 ? raw.trim() : raw.slice(0, newline).trim();
|
|
241
|
+
const body = newline === -1 ? "" : raw.slice(newline + 1).trim();
|
|
242
|
+
return { heading, body, raw };
|
|
243
|
+
});
|
|
244
|
+
}
|
|
171
245
|
function extractAuto(content) {
|
|
172
246
|
const start = content.indexOf(AUTO_BEGIN);
|
|
173
247
|
const end = content.indexOf(AUTO_END);
|
|
@@ -325,8 +399,8 @@ ${block}
|
|
|
325
399
|
}
|
|
326
400
|
|
|
327
401
|
// src/commands/update-context.ts
|
|
328
|
-
import { readFile as
|
|
329
|
-
import { join as
|
|
402
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
403
|
+
import { join as join6 } from "path";
|
|
330
404
|
|
|
331
405
|
// src/config/yaml.ts
|
|
332
406
|
import { readFile as readFile3 } from "fs/promises";
|
|
@@ -345,23 +419,43 @@ async function mergeFetConfig(configPath, renderedFetYaml) {
|
|
|
345
419
|
return doc.toString();
|
|
346
420
|
}
|
|
347
421
|
|
|
422
|
+
// src/context-placeholders.ts
|
|
423
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
424
|
+
import { join as join5 } from "path";
|
|
425
|
+
var AGENTS_LLM_PLACEHOLDER_PATTERN = /\[NEEDS? LLM INPUT\]/g;
|
|
426
|
+
async function countAgentsLlmPlaceholders(projectRoot) {
|
|
427
|
+
try {
|
|
428
|
+
const content = await readFile4(join5(projectRoot, "AGENTS.md"), "utf8");
|
|
429
|
+
return [...content.matchAll(AGENTS_LLM_PLACEHOLDER_PATTERN)].length;
|
|
430
|
+
} catch {
|
|
431
|
+
return 0;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function renderAgentsPlaceholderWarning(count2) {
|
|
435
|
+
return `AGENTS.md still contains ${count2} LLM placeholder(s). Run fet fill-context first so your IDE AI can replace them. Continuing current command.`;
|
|
436
|
+
}
|
|
437
|
+
|
|
348
438
|
// src/commands/update-context.ts
|
|
349
439
|
async function updateContextCommand(ctx) {
|
|
440
|
+
let contextResult = { warnings: [] };
|
|
350
441
|
await withProjectLock(
|
|
351
442
|
ctx.projectRoot,
|
|
352
443
|
{ command: "update-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
|
|
353
|
-
async () =>
|
|
444
|
+
async () => {
|
|
445
|
+
contextResult = await updateContextFiles(ctx);
|
|
446
|
+
}
|
|
354
447
|
);
|
|
355
448
|
ctx.output.result({
|
|
356
449
|
ok: true,
|
|
357
450
|
command: "update-context",
|
|
358
|
-
summary: "\u5DF2\u66F4\u65B0 AGENTS.md \u4E0E openspec/config.yaml \u7684 FET \u6258\u7BA1\u533A\u57DF\u3002"
|
|
451
|
+
summary: "\u5DF2\u66F4\u65B0 AGENTS.md \u4E0E openspec/config.yaml \u7684 FET \u6258\u7BA1\u533A\u57DF\u3002",
|
|
452
|
+
warnings: contextResult.warnings
|
|
359
453
|
});
|
|
360
454
|
}
|
|
361
455
|
async function updateContextFiles(ctx) {
|
|
362
456
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
363
|
-
const agentsPath =
|
|
364
|
-
const configPath =
|
|
457
|
+
const agentsPath = join6(ctx.projectRoot, "AGENTS.md");
|
|
458
|
+
const configPath = join6(ctx.projectRoot, "openspec", "config.yaml");
|
|
365
459
|
const existingAgents = await readOptional(agentsPath);
|
|
366
460
|
const warnings = [...scan.warnings];
|
|
367
461
|
if (existingAgents && hasInvalidManagedAutoRegion(existingAgents)) {
|
|
@@ -388,6 +482,10 @@ async function updateContextFiles(ctx) {
|
|
|
388
482
|
}
|
|
389
483
|
await atomicWrite(agentsPath, replaceManagedRegion(existingAgents, renderAgentsMd(scan)));
|
|
390
484
|
await atomicWrite(configPath, await mergeFetConfig(configPath, renderFetConfig(scan)));
|
|
485
|
+
const placeholderCount = await countAgentsLlmPlaceholders(ctx.projectRoot);
|
|
486
|
+
if (placeholderCount > 0) {
|
|
487
|
+
warnings.push(renderAgentsPlaceholderWarning(placeholderCount));
|
|
488
|
+
}
|
|
391
489
|
const state = await ctx.stateStore.getOrCreateGlobal();
|
|
392
490
|
state.context = {
|
|
393
491
|
agentsMdUpdatedAt: scan.generatedAt,
|
|
@@ -399,7 +497,7 @@ async function updateContextFiles(ctx) {
|
|
|
399
497
|
}
|
|
400
498
|
async function readOptional(path) {
|
|
401
499
|
try {
|
|
402
|
-
return await
|
|
500
|
+
return await readFile5(path, "utf8");
|
|
403
501
|
} catch {
|
|
404
502
|
return null;
|
|
405
503
|
}
|
|
@@ -407,7 +505,8 @@ async function readOptional(path) {
|
|
|
407
505
|
|
|
408
506
|
// src/commands/init.ts
|
|
409
507
|
async function initCommand(ctx) {
|
|
410
|
-
const alreadyInitialized = await exists(
|
|
508
|
+
const alreadyInitialized = await exists(join7(ctx.projectRoot, "openspec", "config.yaml"));
|
|
509
|
+
let warnings = [];
|
|
411
510
|
await withProjectLock(
|
|
412
511
|
ctx.projectRoot,
|
|
413
512
|
{ command: "init", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
|
|
@@ -423,9 +522,17 @@ async function initCommand(ctx) {
|
|
|
423
522
|
}
|
|
424
523
|
}
|
|
425
524
|
const contextResult = await updateContextFiles(ctx);
|
|
525
|
+
warnings = contextResult.warnings;
|
|
426
526
|
await ensureGitignore(ctx);
|
|
427
527
|
const state = await ctx.stateStore.getOrCreateGlobal();
|
|
428
528
|
state.openspec = identity;
|
|
529
|
+
state.graph ??= {};
|
|
530
|
+
const gitnexus = toGitNexusState(await detectGitNexus(), state.graph.gitnexus);
|
|
531
|
+
if (!gitnexus.installed && !gitnexus.recommendationShownAt) {
|
|
532
|
+
warnings.push(renderGitNexusRecommendation(gitnexus));
|
|
533
|
+
gitnexus.recommendationShownAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
534
|
+
}
|
|
535
|
+
state.graph.gitnexus = gitnexus;
|
|
429
536
|
for (const adapter of ctx.toolAdapters) {
|
|
430
537
|
const plan = await adapter.planInstall(ctx.projectRoot);
|
|
431
538
|
const result = await adapter.install(ctx.projectRoot, plan, ctx.yes);
|
|
@@ -439,26 +546,24 @@ async function initCommand(ctx) {
|
|
|
439
546
|
journal.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
440
547
|
await writeInitJournal(ctx.projectRoot, journal);
|
|
441
548
|
await ctx.stateStore.writeGlobal(state);
|
|
442
|
-
for (const warning of contextResult.warnings) {
|
|
443
|
-
ctx.output.warn(warning);
|
|
444
|
-
}
|
|
445
549
|
}
|
|
446
550
|
);
|
|
447
551
|
ctx.output.result({
|
|
448
552
|
ok: true,
|
|
449
553
|
command: "init",
|
|
450
554
|
summary: "FET \u521D\u59CB\u5316\u5B8C\u6210\u3002",
|
|
555
|
+
warnings,
|
|
451
556
|
nextSteps: ["\u4F7F\u7528 fet propose/new \u521B\u5EFA OpenSpec change", "\u4F7F\u7528 fet doctor \u68C0\u67E5\u9879\u76EE\u72B6\u6001"]
|
|
452
557
|
});
|
|
453
558
|
}
|
|
454
559
|
async function ensureGitignore(ctx) {
|
|
455
|
-
const gitignorePath =
|
|
560
|
+
const gitignorePath = join7(ctx.projectRoot, ".gitignore");
|
|
456
561
|
const existing = await readOptional2(gitignorePath);
|
|
457
562
|
await atomicWrite(gitignorePath, mergeGitignore(existing));
|
|
458
563
|
}
|
|
459
564
|
async function readOptional2(path) {
|
|
460
565
|
try {
|
|
461
|
-
return await
|
|
566
|
+
return await readFile6(path, "utf8");
|
|
462
567
|
} catch {
|
|
463
568
|
return null;
|
|
464
569
|
}
|
|
@@ -473,18 +578,20 @@ async function exists(path) {
|
|
|
473
578
|
}
|
|
474
579
|
|
|
475
580
|
// src/commands/doctor.ts
|
|
476
|
-
import { stat as stat3 } from "fs/promises";
|
|
477
|
-
import { join as
|
|
581
|
+
import { readFile as readFile7, stat as stat3 } from "fs/promises";
|
|
582
|
+
import { join as join8 } from "path";
|
|
478
583
|
async function doctorCommand(ctx, options = {}) {
|
|
479
584
|
const checks = [];
|
|
480
585
|
checks.push(await checkOpenSpec(ctx));
|
|
481
586
|
checks.push(await checkState(ctx));
|
|
482
|
-
checks.push(await checkFile("agents",
|
|
483
|
-
checks.push(await checkFile("config",
|
|
587
|
+
checks.push(await checkFile("agents", join8(ctx.projectRoot, "AGENTS.md"), "AGENTS.md \u7F3A\u5931", "fet update-context"));
|
|
588
|
+
checks.push(await checkFile("config", join8(ctx.projectRoot, "openspec", "config.yaml"), "openspec/config.yaml \u7F3A\u5931", "fet init"));
|
|
589
|
+
checks.push(await checkPlaceholders(ctx.projectRoot));
|
|
590
|
+
checks.push(await checkGitNexus(ctx));
|
|
484
591
|
for (const adapter of ctx.toolAdapters) {
|
|
485
592
|
checks.push(...await adapter.doctor(ctx.projectRoot));
|
|
486
593
|
}
|
|
487
|
-
const lockPath =
|
|
594
|
+
const lockPath = join8(ctx.projectRoot, "openspec", ".fet.lock");
|
|
488
595
|
if (await exists2(lockPath)) {
|
|
489
596
|
if (options.fixLock) {
|
|
490
597
|
await clearLock(ctx.projectRoot);
|
|
@@ -502,6 +609,25 @@ async function doctorCommand(ctx, options = {}) {
|
|
|
502
609
|
data: checks
|
|
503
610
|
});
|
|
504
611
|
}
|
|
612
|
+
async function checkGitNexus(ctx) {
|
|
613
|
+
const global = await ctx.stateStore.readGlobal();
|
|
614
|
+
const state = toGitNexusState(await detectGitNexus(), global?.graph?.gitnexus);
|
|
615
|
+
if (global) {
|
|
616
|
+
global.graph ??= {};
|
|
617
|
+
global.graph.gitnexus = state;
|
|
618
|
+
await ctx.stateStore.writeGlobal(global);
|
|
619
|
+
}
|
|
620
|
+
return state.installed ? {
|
|
621
|
+
id: "gitnexus",
|
|
622
|
+
status: "pass",
|
|
623
|
+
message: `GitNexus detected: ${state.executablePath ?? "gitnexus"} (${state.version ?? "unknown"})`
|
|
624
|
+
} : {
|
|
625
|
+
id: "gitnexus",
|
|
626
|
+
status: "warn",
|
|
627
|
+
message: "Optional GitNexus code graph support is not installed",
|
|
628
|
+
suggestedCommand: "Install GitNexus later if you want OpenSpec artifacts to prefer a repository graph"
|
|
629
|
+
};
|
|
630
|
+
}
|
|
505
631
|
async function checkOpenSpec(ctx) {
|
|
506
632
|
try {
|
|
507
633
|
const identity = await ctx.openSpec.resolveExecutable();
|
|
@@ -521,6 +647,20 @@ async function checkState(ctx) {
|
|
|
521
647
|
async function checkFile(id, path, missing, suggestedCommand) {
|
|
522
648
|
return await exists2(path) ? { id, status: "pass", message: `${id} \u5B58\u5728` } : { id, status: "warn", message: missing, suggestedCommand };
|
|
523
649
|
}
|
|
650
|
+
async function checkPlaceholders(projectRoot) {
|
|
651
|
+
try {
|
|
652
|
+
await readFile7(join8(projectRoot, "AGENTS.md"), "utf8");
|
|
653
|
+
const count2 = await countAgentsLlmPlaceholders(projectRoot);
|
|
654
|
+
return count2 ? {
|
|
655
|
+
id: "context-placeholders",
|
|
656
|
+
status: "warn",
|
|
657
|
+
message: `AGENTS.md has ${count2} LLM placeholder(s)`,
|
|
658
|
+
suggestedCommand: "fet fill-context"
|
|
659
|
+
} : { id: "context-placeholders", status: "pass", message: "AGENTS.md placeholders resolved" };
|
|
660
|
+
} catch {
|
|
661
|
+
return { id: "context-placeholders", status: "warn", message: "AGENTS.md missing", suggestedCommand: "fet update-context" };
|
|
662
|
+
}
|
|
663
|
+
}
|
|
524
664
|
async function exists2(path) {
|
|
525
665
|
try {
|
|
526
666
|
await stat3(path);
|
|
@@ -530,10 +670,77 @@ async function exists2(path) {
|
|
|
530
670
|
}
|
|
531
671
|
}
|
|
532
672
|
|
|
673
|
+
// src/commands/fill-context.ts
|
|
674
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
675
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
676
|
+
async function fillContextCommand(ctx) {
|
|
677
|
+
await withProjectLock(
|
|
678
|
+
ctx.projectRoot,
|
|
679
|
+
{ command: "fill-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
|
|
680
|
+
async () => {
|
|
681
|
+
const handoffPath = join9(ctx.projectRoot, ".fet", "fill-context.md");
|
|
682
|
+
await mkdir3(dirname5(handoffPath), { recursive: true });
|
|
683
|
+
await atomicWrite(handoffPath, renderGenericHandoff());
|
|
684
|
+
for (const adapter of ctx.toolAdapters) {
|
|
685
|
+
const plan = await adapter.planInstall(ctx.projectRoot);
|
|
686
|
+
const result = await adapter.install(ctx.projectRoot, plan, ctx.yes);
|
|
687
|
+
const state = await ctx.stateStore.getOrCreateGlobal();
|
|
688
|
+
state.toolAdapters[adapter.tool] = {
|
|
689
|
+
adapterVersion: adapter.adapterVersion,
|
|
690
|
+
installed: true,
|
|
691
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
692
|
+
};
|
|
693
|
+
await ctx.stateStore.writeGlobal(state);
|
|
694
|
+
if (ctx.verbose) {
|
|
695
|
+
ctx.output.info(`Updated ${adapter.tool} adapter`, { written: result.written });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
);
|
|
700
|
+
const placeholders = await countAgentsLlmPlaceholders(ctx.projectRoot);
|
|
701
|
+
ctx.output.result({
|
|
702
|
+
ok: true,
|
|
703
|
+
command: "fill-context",
|
|
704
|
+
summary: placeholders ? `Found ${placeholders} AGENTS.md placeholder(s). Use your IDE AI to fill them.` : "No AGENTS.md placeholders found. IDE fill-context commands were refreshed.",
|
|
705
|
+
nextSteps: placeholders ? [
|
|
706
|
+
"Cursor: run /fet-fill-context",
|
|
707
|
+
"Codex: run /prompts:fet-fill-context",
|
|
708
|
+
"OpenCode or other IDEs: open .fet/fill-context.md or run fet fill-context for handoff instructions"
|
|
709
|
+
] : ["Run fet doctor to confirm project context health"],
|
|
710
|
+
data: {
|
|
711
|
+
placeholders,
|
|
712
|
+
cursorCommand: "/fet-fill-context",
|
|
713
|
+
codexCommand: "/prompts:fet-fill-context"
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
function renderGenericHandoff() {
|
|
718
|
+
return `<!-- FET:MANAGED
|
|
719
|
+
schemaVersion: 1
|
|
720
|
+
generator: fill-context
|
|
721
|
+
FET:END -->
|
|
722
|
+
|
|
723
|
+
# FET Fill Context
|
|
724
|
+
|
|
725
|
+
Use the IDE AI to complete FET-generated placeholders.
|
|
726
|
+
|
|
727
|
+
1. Read AGENTS.md and openspec/config.yaml.
|
|
728
|
+
2. Inspect README files, package scripts, routes, tests, source layout, and project conventions.
|
|
729
|
+
3. Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete project-specific content.
|
|
730
|
+
4. Preserve FET managed markers.
|
|
731
|
+
5. Do not modify business code.
|
|
732
|
+
6. Run \`fet doctor\` and confirm no AGENTS.md placeholder warning remains.
|
|
733
|
+
`;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/commands/proxy.ts
|
|
737
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
738
|
+
import { join as join11 } from "path";
|
|
739
|
+
|
|
533
740
|
// src/state/project.ts
|
|
534
|
-
import { execFile } from "child_process";
|
|
535
|
-
import { promisify } from "util";
|
|
536
|
-
var
|
|
741
|
+
import { execFile as execFile2 } from "child_process";
|
|
742
|
+
import { promisify as promisify2 } from "util";
|
|
743
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
537
744
|
async function detectProjectIdentity(projectRoot) {
|
|
538
745
|
const [gitRoot, branch, headCommit] = await Promise.all([
|
|
539
746
|
git(projectRoot, ["rev-parse", "--show-toplevel"]),
|
|
@@ -549,7 +756,7 @@ async function detectProjectIdentity(projectRoot) {
|
|
|
549
756
|
}
|
|
550
757
|
async function git(cwd, args) {
|
|
551
758
|
try {
|
|
552
|
-
const { stdout } = await
|
|
759
|
+
const { stdout } = await execFileAsync2("git", args, { cwd });
|
|
553
760
|
return stdout.trim() || null;
|
|
554
761
|
} catch {
|
|
555
762
|
return null;
|
|
@@ -557,8 +764,8 @@ async function git(cwd, args) {
|
|
|
557
764
|
}
|
|
558
765
|
|
|
559
766
|
// src/state/store.ts
|
|
560
|
-
import { mkdir as
|
|
561
|
-
import { join as
|
|
767
|
+
import { mkdir as mkdir4, readFile as readFile8 } from "fs/promises";
|
|
768
|
+
import { join as join10 } from "path";
|
|
562
769
|
|
|
563
770
|
// src/state/schema.ts
|
|
564
771
|
var phases = ["explore", "propose", "implement", "verify", "sync", "archive"];
|
|
@@ -579,6 +786,7 @@ function createGlobalState(fetVersion, project) {
|
|
|
579
786
|
scannerVersion: 1
|
|
580
787
|
},
|
|
581
788
|
toolAdapters: {},
|
|
789
|
+
graph: {},
|
|
582
790
|
verifyAuthorization: null,
|
|
583
791
|
lastDoctor: null
|
|
584
792
|
};
|
|
@@ -651,7 +859,7 @@ var StateStore = class {
|
|
|
651
859
|
project;
|
|
652
860
|
async readGlobal() {
|
|
653
861
|
try {
|
|
654
|
-
const value = JSON.parse(await
|
|
862
|
+
const value = JSON.parse(await readFile8(this.globalPath(), "utf8"));
|
|
655
863
|
assertGlobalState(value);
|
|
656
864
|
return value;
|
|
657
865
|
} catch (error) {
|
|
@@ -666,13 +874,13 @@ var StateStore = class {
|
|
|
666
874
|
}
|
|
667
875
|
async writeGlobal(state) {
|
|
668
876
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
669
|
-
await
|
|
877
|
+
await mkdir4(join10(this.projectRoot, "openspec"), { recursive: true });
|
|
670
878
|
await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
|
|
671
879
|
`);
|
|
672
880
|
}
|
|
673
881
|
async readChange(changeId) {
|
|
674
882
|
try {
|
|
675
|
-
const value = JSON.parse(await
|
|
883
|
+
const value = JSON.parse(await readFile8(this.changePath(changeId), "utf8"));
|
|
676
884
|
assertChangeState(value);
|
|
677
885
|
return value;
|
|
678
886
|
} catch (error) {
|
|
@@ -687,15 +895,15 @@ var StateStore = class {
|
|
|
687
895
|
}
|
|
688
896
|
async writeChange(state) {
|
|
689
897
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
690
|
-
await
|
|
898
|
+
await mkdir4(join10(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
|
|
691
899
|
await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
|
|
692
900
|
`);
|
|
693
901
|
}
|
|
694
902
|
globalPath() {
|
|
695
|
-
return
|
|
903
|
+
return join10(this.projectRoot, "openspec", "fet-state.json");
|
|
696
904
|
}
|
|
697
905
|
changePath(changeId) {
|
|
698
|
-
return
|
|
906
|
+
return join10(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
|
|
699
907
|
}
|
|
700
908
|
};
|
|
701
909
|
function isNotFound(error) {
|
|
@@ -703,11 +911,11 @@ function isNotFound(error) {
|
|
|
703
911
|
}
|
|
704
912
|
|
|
705
913
|
// src/state/tasks.ts
|
|
706
|
-
import { readFile as
|
|
914
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
707
915
|
async function readCompletedTaskIds(tasksPath) {
|
|
708
916
|
let content;
|
|
709
917
|
try {
|
|
710
|
-
content = await
|
|
918
|
+
content = await readFile9(tasksPath, "utf8");
|
|
711
919
|
} catch {
|
|
712
920
|
return [];
|
|
713
921
|
}
|
|
@@ -746,6 +954,8 @@ async function proxyCommand(ctx, command, args) {
|
|
|
746
954
|
await assertVerified(ctx);
|
|
747
955
|
}
|
|
748
956
|
const mapped = await mapOpenSpecCommand(ctx, command, openSpecArgs);
|
|
957
|
+
const targetChangeId = command === "archive" ? mapped.args[0] ?? ctx.changeId ?? null : ctx.changeId ?? null;
|
|
958
|
+
const changelogEntry = command === "archive" && targetChangeId ? await createChangelogEntry(ctx.projectRoot, targetChangeId) : null;
|
|
749
959
|
const result = await ctx.openSpec.run(mapped.command, mapped.args, { cwd: ctx.projectRoot, stdio: ctx.json ? "pipe" : "inherit" });
|
|
750
960
|
if (result.exitCode !== 0) {
|
|
751
961
|
throw new FetError({
|
|
@@ -755,15 +965,25 @@ async function proxyCommand(ctx, command, args) {
|
|
|
755
965
|
recoverable: true
|
|
756
966
|
});
|
|
757
967
|
}
|
|
968
|
+
if (changelogEntry) {
|
|
969
|
+
await appendChangelog(ctx.projectRoot, changelogEntry);
|
|
970
|
+
}
|
|
758
971
|
const inspection = await ctx.openSpec.inspectProject(ctx.projectRoot);
|
|
759
972
|
const state = await ctx.stateStore.getOrCreateGlobal();
|
|
760
973
|
state.openChangeIds = inspection.changes;
|
|
761
|
-
if (
|
|
762
|
-
state.activeChangeId
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
974
|
+
if (command === "archive") {
|
|
975
|
+
if (!state.activeChangeId || state.activeChangeId === targetChangeId || !inspection.changes.includes(state.activeChangeId)) {
|
|
976
|
+
state.activeChangeId = null;
|
|
977
|
+
}
|
|
978
|
+
state.verifyAuthorization = null;
|
|
979
|
+
} else {
|
|
980
|
+
if (ctx.changeId && inspection.changes.includes(ctx.changeId)) {
|
|
981
|
+
state.activeChangeId = ctx.changeId;
|
|
982
|
+
} else if (state.activeChangeId && !inspection.changes.includes(state.activeChangeId)) {
|
|
983
|
+
state.activeChangeId = inspection.changes.length === 1 ? inspection.changes[0] ?? null : null;
|
|
984
|
+
} else if (!state.activeChangeId && inspection.changes.length === 1) {
|
|
985
|
+
state.activeChangeId = inspection.changes[0] ?? null;
|
|
986
|
+
}
|
|
767
987
|
}
|
|
768
988
|
await ctx.stateStore.writeGlobal(state);
|
|
769
989
|
const changeId = ctx.changeId ?? state.activeChangeId;
|
|
@@ -793,6 +1013,58 @@ async function proxyCommand(ctx, command, args) {
|
|
|
793
1013
|
summary: `fet ${command} \u5B8C\u6210\u3002`
|
|
794
1014
|
});
|
|
795
1015
|
}
|
|
1016
|
+
async function createChangelogEntry(projectRoot, changeId) {
|
|
1017
|
+
return {
|
|
1018
|
+
updateTime: formatLocalTimestamp(/* @__PURE__ */ new Date()),
|
|
1019
|
+
content: await readChangeRequirement(projectRoot, changeId)
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
async function appendChangelog(projectRoot, entry) {
|
|
1023
|
+
const changelogPath = join11(projectRoot, "CHANGELOG.md");
|
|
1024
|
+
const existing = await readOptional3(changelogPath);
|
|
1025
|
+
const block = `updateTime: ${entry.updateTime}
|
|
1026
|
+
\u66F4\u65B0\u5185\u5BB9:${entry.content}
|
|
1027
|
+
`;
|
|
1028
|
+
const next = existing?.trimEnd() ? `${existing.trimEnd()}
|
|
1029
|
+
|
|
1030
|
+
${block}` : block;
|
|
1031
|
+
await atomicWrite(changelogPath, next);
|
|
1032
|
+
}
|
|
1033
|
+
async function readChangeRequirement(projectRoot, changeId) {
|
|
1034
|
+
const changeRoot = join11(projectRoot, "openspec", "changes", changeId);
|
|
1035
|
+
const proposal = await readOptional3(join11(changeRoot, "proposal.md"));
|
|
1036
|
+
if (proposal) {
|
|
1037
|
+
return summarizeMarkdown(proposal);
|
|
1038
|
+
}
|
|
1039
|
+
const readme = await readOptional3(join11(changeRoot, "README.md"));
|
|
1040
|
+
if (readme) {
|
|
1041
|
+
return summarizeMarkdown(readme);
|
|
1042
|
+
}
|
|
1043
|
+
return changeId;
|
|
1044
|
+
}
|
|
1045
|
+
function summarizeMarkdown(content) {
|
|
1046
|
+
const normalized = content.replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("<!--") && !line.startsWith("---")).join(" ");
|
|
1047
|
+
return normalized || "\u672A\u63D0\u4F9B\u53D8\u66F4\u9700\u6C42";
|
|
1048
|
+
}
|
|
1049
|
+
async function readOptional3(path) {
|
|
1050
|
+
try {
|
|
1051
|
+
return await readFile10(path, "utf8");
|
|
1052
|
+
} catch {
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
function formatLocalTimestamp(date) {
|
|
1057
|
+
const year = date.getFullYear();
|
|
1058
|
+
const month = pad(date.getMonth() + 1);
|
|
1059
|
+
const day = pad(date.getDate());
|
|
1060
|
+
const hours = pad(date.getHours());
|
|
1061
|
+
const minutes = pad(date.getMinutes());
|
|
1062
|
+
const seconds = pad(date.getSeconds());
|
|
1063
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
1064
|
+
}
|
|
1065
|
+
function pad(value) {
|
|
1066
|
+
return String(value).padStart(2, "0");
|
|
1067
|
+
}
|
|
796
1068
|
async function passthroughCommand(ctx, command, args) {
|
|
797
1069
|
const result = await ctx.openSpec.run(command, stripFetOptions(args), { cwd: ctx.projectRoot, stdio: ctx.json ? "pipe" : "inherit" });
|
|
798
1070
|
if (result.exitCode !== 0) {
|
|
@@ -826,32 +1098,37 @@ function stripFetOptions(args) {
|
|
|
826
1098
|
async function mapOpenSpecCommand(ctx, command, args) {
|
|
827
1099
|
switch (command) {
|
|
828
1100
|
case "propose":
|
|
829
|
-
case "new":
|
|
830
|
-
return { command: "new", args: args[0] === "change" ? args : ["change", ...args] };
|
|
831
1101
|
case "continue":
|
|
832
|
-
return { command: "instructions", args: [...withoutUndefined(args[0] ? [args[0]] : ["proposal"]), "--change", await requireChangeId(ctx)] };
|
|
833
1102
|
case "ff":
|
|
834
|
-
return { command: "status", args: ["--change", await requireChangeId(ctx)] };
|
|
835
1103
|
case "apply":
|
|
836
|
-
return { command: "instructions", args: ["apply", "--change", await requireChangeId(ctx)] };
|
|
837
1104
|
case "sync":
|
|
838
|
-
|
|
1105
|
+
case "bulk-archive":
|
|
1106
|
+
case "explore":
|
|
1107
|
+
case "onboard":
|
|
1108
|
+
return { command, args: withGlobalChange(ctx, args) };
|
|
1109
|
+
case "new":
|
|
1110
|
+
return { command: "new", args: args[0] === "change" ? args : ["change", ...args] };
|
|
839
1111
|
case "archive":
|
|
840
1112
|
return { command: "archive", args: [ctx.changeId ?? args[0] ?? await requireChangeId(ctx), ...args.slice(ctx.changeId ? 0 : 1)] };
|
|
1113
|
+
/*
|
|
841
1114
|
case "bulk-archive":
|
|
842
1115
|
throw new FetError({
|
|
843
|
-
code:
|
|
844
|
-
message: "OpenSpec 1.2.0
|
|
845
|
-
suggestedCommand: "
|
|
1116
|
+
code: ErrorCode.InvalidArguments,
|
|
1117
|
+
message: "OpenSpec 1.2.0 不提供 bulk-archive 顶层命令",
|
|
1118
|
+
suggestedCommand: "逐个执行 fet archive --change <change-id>"
|
|
846
1119
|
});
|
|
847
1120
|
case "explore":
|
|
848
|
-
return { command: "
|
|
1121
|
+
return { command: "explore", args: ctx.changeId ? ["--change", ctx.changeId, ...args] : args };
|
|
849
1122
|
case "onboard":
|
|
850
1123
|
return { command: "instructions", args: [] };
|
|
1124
|
+
*/
|
|
851
1125
|
default:
|
|
852
1126
|
return { command, args };
|
|
853
1127
|
}
|
|
854
1128
|
}
|
|
1129
|
+
function withGlobalChange(ctx, args) {
|
|
1130
|
+
return ctx.changeId ? ["--change", ctx.changeId, ...args] : args;
|
|
1131
|
+
}
|
|
855
1132
|
async function requireChangeId(ctx) {
|
|
856
1133
|
if (ctx.changeId) {
|
|
857
1134
|
return ctx.changeId;
|
|
@@ -871,9 +1148,6 @@ async function requireChangeId(ctx) {
|
|
|
871
1148
|
suggestedCommand: "\u6DFB\u52A0 --change <change-id>"
|
|
872
1149
|
});
|
|
873
1150
|
}
|
|
874
|
-
function withoutUndefined(values) {
|
|
875
|
-
return values.filter(Boolean);
|
|
876
|
-
}
|
|
877
1151
|
async function assertVerified(ctx) {
|
|
878
1152
|
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
879
1153
|
const changeId = ctx.changeId ?? global.activeChangeId;
|
|
@@ -906,8 +1180,8 @@ async function assertVerified(ctx) {
|
|
|
906
1180
|
|
|
907
1181
|
// src/commands/verify.ts
|
|
908
1182
|
import { createHash } from "crypto";
|
|
909
|
-
import { mkdir as
|
|
910
|
-
import { join as
|
|
1183
|
+
import { mkdir as mkdir5, readFile as readFile11, stat as stat4 } from "fs/promises";
|
|
1184
|
+
import { join as join12 } from "path";
|
|
911
1185
|
async function verifyCommand(ctx, options) {
|
|
912
1186
|
if (options.auto) {
|
|
913
1187
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
@@ -974,9 +1248,9 @@ async function verifyCommand(ctx, options) {
|
|
|
974
1248
|
async function writeInstructions(ctx, changeId) {
|
|
975
1249
|
await assertChangeExists(ctx, changeId);
|
|
976
1250
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
977
|
-
const dir =
|
|
978
|
-
const instructionsPath =
|
|
979
|
-
await
|
|
1251
|
+
const dir = join12(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
1252
|
+
const instructionsPath = join12(dir, "verify-instructions.md");
|
|
1253
|
+
await mkdir5(dir, { recursive: true });
|
|
980
1254
|
await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
|
|
981
1255
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
982
1256
|
state.currentPhase = "verify";
|
|
@@ -992,7 +1266,7 @@ async function writeInstructions(ctx, changeId) {
|
|
|
992
1266
|
async function markDone(ctx, changeId) {
|
|
993
1267
|
await assertChangeExists(ctx, changeId);
|
|
994
1268
|
const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
995
|
-
const instructionsPath =
|
|
1269
|
+
const instructionsPath = join12(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
|
|
996
1270
|
const instructions = await readInstructions(instructionsPath, changeId);
|
|
997
1271
|
const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
|
|
998
1272
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
@@ -1028,7 +1302,7 @@ async function assertChangeExists(ctx, changeId) {
|
|
|
1028
1302
|
async function readInstructions(path, changeId) {
|
|
1029
1303
|
try {
|
|
1030
1304
|
await stat4(path);
|
|
1031
|
-
const content = await
|
|
1305
|
+
const content = await readFile11(path, "utf8");
|
|
1032
1306
|
const fileChangeId = readFrontMatterValue(content, "changeId");
|
|
1033
1307
|
if (fileChangeId !== changeId) {
|
|
1034
1308
|
throw new FetError({
|
|
@@ -1078,13 +1352,76 @@ async function resolveChangeId(ctx) {
|
|
|
1078
1352
|
});
|
|
1079
1353
|
}
|
|
1080
1354
|
|
|
1355
|
+
// src/model-policy.ts
|
|
1356
|
+
var HIGH_COST_MODEL_PATTERNS = [
|
|
1357
|
+
/gpt[-_ ]?5\.5/i,
|
|
1358
|
+
/glm[-_ ]?5(?:\.1)?/i,
|
|
1359
|
+
/claude.*opus/i,
|
|
1360
|
+
/opus/i,
|
|
1361
|
+
/claude.*sonnet/i,
|
|
1362
|
+
/sonnet/i
|
|
1363
|
+
];
|
|
1364
|
+
var MODEL_ENV_KEYS = ["FET_IDE_MODEL", "FET_MODEL", "CODEX_MODEL", "CURSOR_MODEL", "OPENCODE_MODEL", "OPENAI_MODEL", "ANTHROPIC_MODEL"];
|
|
1365
|
+
function detectCurrentModel(env = process.env) {
|
|
1366
|
+
for (const key of MODEL_ENV_KEYS) {
|
|
1367
|
+
const value = env[key]?.trim();
|
|
1368
|
+
if (value) {
|
|
1369
|
+
return { source: key, name: value };
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
function isHighCostModel(model) {
|
|
1375
|
+
return HIGH_COST_MODEL_PATTERNS.some((pattern) => pattern.test(model));
|
|
1376
|
+
}
|
|
1377
|
+
function getCommandModelPolicyMismatch(command, env = process.env) {
|
|
1378
|
+
if (env.FET_MODEL_POLICY === "off" || env.FET_SKIP_MODEL_POLICY === "1") {
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
const detected = detectCurrentModel(env);
|
|
1382
|
+
if (!detected) {
|
|
1383
|
+
return null;
|
|
1384
|
+
}
|
|
1385
|
+
const highCost = isHighCostModel(detected.name);
|
|
1386
|
+
if (command === "apply") {
|
|
1387
|
+
if (!highCost) {
|
|
1388
|
+
return {
|
|
1389
|
+
command,
|
|
1390
|
+
detected,
|
|
1391
|
+
recommended: "high-cost",
|
|
1392
|
+
reason: "fet apply is the implementation phase and is recommended to use a high-capability/high-cost model."
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
return null;
|
|
1396
|
+
}
|
|
1397
|
+
if (highCost) {
|
|
1398
|
+
return {
|
|
1399
|
+
command,
|
|
1400
|
+
detected,
|
|
1401
|
+
recommended: "low-cost",
|
|
1402
|
+
reason: `fet ${command} is not the implementation phase and is recommended to use a lower-cost model.`
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1407
|
+
function formatModelPolicyMismatch(mismatch) {
|
|
1408
|
+
const switchHint = mismatch.recommended === "high-cost" ? "Recommended models include GPT-5.5, GLM-5.1, GLM-5, Claude Opus, or Claude Sonnet." : "Recommended action: switch to a lower-cost model and reserve high-cost models for fet apply.";
|
|
1409
|
+
return `${mismatch.reason} Detected ${mismatch.detected.source}="${mismatch.detected.name}". ${switchHint}`;
|
|
1410
|
+
}
|
|
1411
|
+
function renderIdeModelPolicy(command) {
|
|
1412
|
+
if (command === "apply") {
|
|
1413
|
+
return "Model policy: this command is recommended to run with a high-capability/high-cost model such as GPT-5.5, GLM-5.1, GLM-5, Claude Opus, or Claude Sonnet. If the current IDE model is lower-cost, tell the user and ask whether to stop for a model switch or continue anyway.";
|
|
1414
|
+
}
|
|
1415
|
+
return "Model policy: this command is recommended to run with a low-cost model. If the current IDE model is GPT-5.5, GLM-5.1, GLM-5, Claude Opus, Claude Sonnet, or another high-cost model, tell the user and ask whether to stop for a model switch or continue anyway.";
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1081
1418
|
// src/cli/context.ts
|
|
1082
1419
|
import { resolve } from "path";
|
|
1083
1420
|
|
|
1084
1421
|
// src/adapters/codex/index.ts
|
|
1085
|
-
import { mkdir as
|
|
1422
|
+
import { mkdir as mkdir6, readFile as readFile12, stat as stat5 } from "fs/promises";
|
|
1086
1423
|
import { homedir } from "os";
|
|
1087
|
-
import { dirname as
|
|
1424
|
+
import { dirname as dirname6, join as join13 } from "path";
|
|
1088
1425
|
|
|
1089
1426
|
// src/adapters/commands.ts
|
|
1090
1427
|
var FET_WORKFLOW_COMMANDS = [
|
|
@@ -1100,7 +1437,7 @@ var FET_WORKFLOW_COMMANDS = [
|
|
|
1100
1437
|
"bulk-archive",
|
|
1101
1438
|
"onboard"
|
|
1102
1439
|
];
|
|
1103
|
-
var FET_ADAPTER_COMMANDS = [...FET_WORKFLOW_COMMANDS, "passthrough"];
|
|
1440
|
+
var FET_ADAPTER_COMMANDS = [...FET_WORKFLOW_COMMANDS, "fill-context", "passthrough"];
|
|
1104
1441
|
|
|
1105
1442
|
// src/adapters/codex/templates.ts
|
|
1106
1443
|
function codexGuideFile() {
|
|
@@ -1121,6 +1458,8 @@ Before doing FET or OpenSpec work in Codex, read:
|
|
|
1121
1458
|
- openspec/config.yaml
|
|
1122
1459
|
- the active change files under openspec/changes/<change-id>/, when a change is selected
|
|
1123
1460
|
|
|
1461
|
+
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.
|
|
1462
|
+
|
|
1124
1463
|
Use the terminal command \`fet <command>\` as the source of truth for workflow transitions. These files are Codex-readable guidance; they do not register native slash commands.
|
|
1125
1464
|
|
|
1126
1465
|
Command guides live in .codex/fet/commands/.
|
|
@@ -1140,6 +1479,9 @@ function codexSlashPromptFiles() {
|
|
|
1140
1479
|
}));
|
|
1141
1480
|
}
|
|
1142
1481
|
function renderCommand(command) {
|
|
1482
|
+
if (command === "fill-context") {
|
|
1483
|
+
return renderFillContextCommand();
|
|
1484
|
+
}
|
|
1143
1485
|
if (command === "passthrough") {
|
|
1144
1486
|
return renderPassthroughCommand();
|
|
1145
1487
|
}
|
|
@@ -1153,8 +1495,12 @@ FET:END -->
|
|
|
1153
1495
|
|
|
1154
1496
|
# fet ${command}
|
|
1155
1497
|
|
|
1498
|
+
${renderIdeModelPolicy(command)}
|
|
1499
|
+
|
|
1156
1500
|
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.
|
|
1157
1501
|
|
|
1502
|
+
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
|
+
|
|
1158
1504
|
Then run:
|
|
1159
1505
|
|
|
1160
1506
|
\`\`\`sh
|
|
@@ -1177,8 +1523,12 @@ FET:END -->
|
|
|
1177
1523
|
|
|
1178
1524
|
# fet passthrough
|
|
1179
1525
|
|
|
1526
|
+
${renderIdeModelPolicy("passthrough")}
|
|
1527
|
+
|
|
1180
1528
|
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.
|
|
1181
1529
|
|
|
1530
|
+
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
|
+
|
|
1182
1532
|
Then run:
|
|
1183
1533
|
|
|
1184
1534
|
\`\`\`sh
|
|
@@ -1219,6 +1569,9 @@ function renderSlashPrompt(command) {
|
|
|
1219
1569
|
if (command === "onboard") {
|
|
1220
1570
|
return renderOnboardSlashPrompt();
|
|
1221
1571
|
}
|
|
1572
|
+
if (command === "fill-context") {
|
|
1573
|
+
return renderFillContextSlashPrompt();
|
|
1574
|
+
}
|
|
1222
1575
|
if (command === "passthrough") {
|
|
1223
1576
|
return renderPassthroughSlashPrompt();
|
|
1224
1577
|
}
|
|
@@ -1242,6 +1595,8 @@ Use FET as the entry point for this OpenSpec workflow.
|
|
|
1242
1595
|
|
|
1243
1596
|
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.
|
|
1244
1597
|
|
|
1598
|
+
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
|
+
|
|
1245
1600
|
Run:
|
|
1246
1601
|
|
|
1247
1602
|
\`\`\`sh
|
|
@@ -1251,6 +1606,66 @@ ${shellCommand}
|
|
|
1251
1606
|
After it completes, summarize the important FET output and next steps.
|
|
1252
1607
|
`;
|
|
1253
1608
|
}
|
|
1609
|
+
function renderFillContextCommand() {
|
|
1610
|
+
return `<!-- FET:MANAGED
|
|
1611
|
+
schemaVersion: 1
|
|
1612
|
+
fetVersion: ${FET_VERSION}
|
|
1613
|
+
generator: codex-adapter
|
|
1614
|
+
adapterVersion: 1
|
|
1615
|
+
command: fet fill-context
|
|
1616
|
+
FET:END -->
|
|
1617
|
+
|
|
1618
|
+
# fet fill-context
|
|
1619
|
+
|
|
1620
|
+
${renderIdeModelPolicy("fill-context")}
|
|
1621
|
+
|
|
1622
|
+
Use this command to complete FET-generated project context placeholders with Codex.
|
|
1623
|
+
|
|
1624
|
+
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
|
+
|
|
1626
|
+
First run:
|
|
1627
|
+
|
|
1628
|
+
\`\`\`sh
|
|
1629
|
+
fet fill-context
|
|
1630
|
+
\`\`\`
|
|
1631
|
+
|
|
1632
|
+
Then read AGENTS.md and openspec/config.yaml, inspect the project, and replace every [NEEDS LLM INPUT] or [NEED LLM INPUT] placeholder in AGENTS.md with concrete project-specific content. Preserve FET managed markers and do not modify business code.
|
|
1633
|
+
`;
|
|
1634
|
+
}
|
|
1635
|
+
function renderFillContextSlashPrompt() {
|
|
1636
|
+
return renderManagedSlashPrompt(
|
|
1637
|
+
"fet fill-context",
|
|
1638
|
+
"Fill FET AGENTS.md placeholders with Codex",
|
|
1639
|
+
`Complete FET-generated project context placeholders.
|
|
1640
|
+
|
|
1641
|
+
Steps:
|
|
1642
|
+
|
|
1643
|
+
1. Refresh FET IDE handoff files:
|
|
1644
|
+
\`\`\`sh
|
|
1645
|
+
fet fill-context
|
|
1646
|
+
\`\`\`
|
|
1647
|
+
2. Read AGENTS.md and openspec/config.yaml.
|
|
1648
|
+
3. Inspect the project to understand:
|
|
1649
|
+
- source structure and major modules
|
|
1650
|
+
- framework and routing conventions
|
|
1651
|
+
- scripts, test commands, and build commands
|
|
1652
|
+
- coding conventions and project-specific patterns
|
|
1653
|
+
- important docs such as README files
|
|
1654
|
+
4. Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete, concise project-specific content.
|
|
1655
|
+
5. Preserve FET managed markers such as \`FET:BEGIN AUTO\`, \`FET:END AUTO\`, \`FET:BEGIN USER\`, and \`FET:END USER\`.
|
|
1656
|
+
6. Do not modify business code.
|
|
1657
|
+
7. Run:
|
|
1658
|
+
\`\`\`sh
|
|
1659
|
+
fet doctor
|
|
1660
|
+
\`\`\`
|
|
1661
|
+
Confirm that no AGENTS.md placeholder warning remains.
|
|
1662
|
+
|
|
1663
|
+
Guardrails:
|
|
1664
|
+
- Do not invent facts that cannot be inferred from the repo.
|
|
1665
|
+
- Use [UNKNOWN] only when the repository does not contain enough evidence.
|
|
1666
|
+
- Keep generated context stable and useful for future AI coding sessions.`
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1254
1669
|
function renderNewSlashPrompt() {
|
|
1255
1670
|
return renderManagedSlashPrompt(
|
|
1256
1671
|
"fet new [...args]",
|
|
@@ -1294,11 +1709,11 @@ Input after the slash command should identify the change, for example a change i
|
|
|
1294
1709
|
Steps:
|
|
1295
1710
|
|
|
1296
1711
|
1. Resolve the change id. If ambiguous, ask the user.
|
|
1297
|
-
2.
|
|
1712
|
+
2. Run the native OpenSpec apply flow through FET:
|
|
1298
1713
|
\`\`\`sh
|
|
1299
1714
|
fet apply --change <change-id> --json
|
|
1300
1715
|
\`\`\`
|
|
1301
|
-
3.
|
|
1716
|
+
3. Follow the native apply output. If JSON output is unavailable, read the files referenced by the terminal output and the artifacts under openspec/changes/<change-id>/.
|
|
1302
1717
|
4. If apply is blocked because required artifacts are missing, stop and suggest /prompts:fet-continue <change-id> or /prompts:fet-ff <change-id>.
|
|
1303
1718
|
5. Implement pending tasks one by one:
|
|
1304
1719
|
- Keep code changes minimal and scoped to the task.
|
|
@@ -1502,11 +1917,11 @@ Steps:
|
|
|
1502
1917
|
\`\`\`sh
|
|
1503
1918
|
fet new <change-id>
|
|
1504
1919
|
\`\`\`
|
|
1505
|
-
5.
|
|
1920
|
+
5. Run the native OpenSpec exploration flow through FET so clarification prompts stay interactive:
|
|
1506
1921
|
\`\`\`sh
|
|
1507
1922
|
fet explore --change <change-id>
|
|
1508
1923
|
\`\`\`
|
|
1509
|
-
6. If the user asks to generate or capture the proposal, create openspec/changes/<change-id>/proposal.md using the
|
|
1924
|
+
6. If OpenSpec or the user asks to generate or capture the proposal, create openspec/changes/<change-id>/proposal.md using the interactive answers, conversation, and project context.
|
|
1510
1925
|
|
|
1511
1926
|
Guardrails:
|
|
1512
1927
|
- Do not write application code in explore mode.
|
|
@@ -1532,11 +1947,11 @@ Steps:
|
|
|
1532
1947
|
fet passthrough status --change <change-id> --json
|
|
1533
1948
|
\`\`\`
|
|
1534
1949
|
4. Pick the first artifact whose status is ready, unless the user specified an artifact id.
|
|
1535
|
-
5.
|
|
1950
|
+
5. Run the native OpenSpec continue flow through FET:
|
|
1536
1951
|
\`\`\`sh
|
|
1537
1952
|
fet continue <artifact-id> --change <change-id> --json
|
|
1538
1953
|
\`\`\`
|
|
1539
|
-
6.
|
|
1954
|
+
6. Follow the native continue output. When it provides template, instruction, dependencies, and outputPath, use those fields.
|
|
1540
1955
|
7. Read dependency files before writing.
|
|
1541
1956
|
8. Create the artifact file at outputPath. Do not copy context/rules wrapper text into the artifact; use those fields only as constraints.
|
|
1542
1957
|
9. Verify the file exists, then run:
|
|
@@ -1557,6 +1972,7 @@ Guardrails:
|
|
|
1557
1972
|
}
|
|
1558
1973
|
function renderFastForwardSlashPrompt(command) {
|
|
1559
1974
|
const title = command === "propose" ? "Propose a new FET/OpenSpec change" : "Fast-forward FET/OpenSpec artifact creation";
|
|
1975
|
+
const commandLine = command === "propose" ? "fet propose <change-id-or-description>" : "fet ff --change <change-id>";
|
|
1560
1976
|
return renderManagedSlashPrompt(
|
|
1561
1977
|
`fet ${command} [...args]`,
|
|
1562
1978
|
command === "propose" ? "Create a change and generate required OpenSpec artifacts" : "Generate required OpenSpec artifacts for a change",
|
|
@@ -1567,22 +1983,13 @@ Input after the slash command may be a change id or a description of what the us
|
|
|
1567
1983
|
Steps:
|
|
1568
1984
|
|
|
1569
1985
|
1. Load project context from AGENTS.md and openspec/config.yaml.
|
|
1570
|
-
2. Resolve or
|
|
1571
|
-
|
|
1572
|
-
- If the change already exists, continue it instead of recreating it.
|
|
1573
|
-
3. Check artifact status:
|
|
1986
|
+
2. Resolve the change id or description. If ambiguous, ask the user.
|
|
1987
|
+
3. Run the native OpenSpec ${command} flow through FET:
|
|
1574
1988
|
\`\`\`sh
|
|
1575
|
-
|
|
1989
|
+
${commandLine}
|
|
1576
1990
|
\`\`\`
|
|
1577
|
-
4.
|
|
1578
|
-
|
|
1579
|
-
- Run \`fet continue <artifact-id> --change <change-id> --json\`.
|
|
1580
|
-
- Parse template, instruction, dependencies, and outputPath.
|
|
1581
|
-
- Read dependency files.
|
|
1582
|
-
- Write the artifact file at outputPath.
|
|
1583
|
-
- Re-run \`fet passthrough status --change <change-id> --json\`.
|
|
1584
|
-
- Stop when all apply-required artifacts are done, or when no artifact is ready.
|
|
1585
|
-
5. If context is unclear, ask one concise question, then continue.
|
|
1991
|
+
4. Follow the native output. If it asks for clarification, ask the user rather than inventing details.
|
|
1992
|
+
5. If the native output includes artifact paths or templates to write, create only those files and preserve OpenSpec structure.
|
|
1586
1993
|
|
|
1587
1994
|
Artifact rules:
|
|
1588
1995
|
- Follow the instruction field from OpenSpec/FET for each artifact.
|
|
@@ -1598,6 +2005,7 @@ Output:
|
|
|
1598
2005
|
);
|
|
1599
2006
|
}
|
|
1600
2007
|
function renderManagedSlashPrompt(command, description, body) {
|
|
2008
|
+
const policyCommand = command.split(/\s+/)[1] ?? command;
|
|
1601
2009
|
return `<!-- FET:MANAGED
|
|
1602
2010
|
schemaVersion: 1
|
|
1603
2011
|
fetVersion: ${FET_VERSION}
|
|
@@ -1611,6 +2019,10 @@ description: ${description}
|
|
|
1611
2019
|
argument-hint: command arguments
|
|
1612
2020
|
---
|
|
1613
2021
|
|
|
2022
|
+
${renderIdeModelPolicy(policyCommand)}
|
|
2023
|
+
|
|
2024
|
+
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.
|
|
2025
|
+
|
|
1614
2026
|
${body}
|
|
1615
2027
|
`;
|
|
1616
2028
|
}
|
|
@@ -1621,7 +2033,7 @@ var CodexAdapter = class {
|
|
|
1621
2033
|
adapterVersion = 1;
|
|
1622
2034
|
async detect(projectRoot) {
|
|
1623
2035
|
return {
|
|
1624
|
-
detected: await exists3(
|
|
2036
|
+
detected: await exists3(join13(projectRoot, ".codex")) || await exists3(join13(projectRoot, "AGENTS.md")),
|
|
1625
2037
|
reason: "Codex adapter is available for projects that use AGENTS.md"
|
|
1626
2038
|
};
|
|
1627
2039
|
}
|
|
@@ -1660,7 +2072,7 @@ var CodexAdapter = class {
|
|
|
1660
2072
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
1661
2073
|
await createBackup(target);
|
|
1662
2074
|
}
|
|
1663
|
-
await
|
|
2075
|
+
await mkdir6(dirname6(target), { recursive: true });
|
|
1664
2076
|
await atomicWrite(target, file.content);
|
|
1665
2077
|
written.push(displayPath);
|
|
1666
2078
|
}
|
|
@@ -1687,9 +2099,9 @@ var CodexAdapter = class {
|
|
|
1687
2099
|
};
|
|
1688
2100
|
function resolveTarget(projectRoot, file) {
|
|
1689
2101
|
if (file.root === "codex-home") {
|
|
1690
|
-
return
|
|
2102
|
+
return join13(resolveCodexHome(), file.path);
|
|
1691
2103
|
}
|
|
1692
|
-
return
|
|
2104
|
+
return join13(projectRoot, file.path);
|
|
1693
2105
|
}
|
|
1694
2106
|
function displayPathFor(file) {
|
|
1695
2107
|
if (file.root === "codex-home") {
|
|
@@ -1698,11 +2110,11 @@ function displayPathFor(file) {
|
|
|
1698
2110
|
return file.path;
|
|
1699
2111
|
}
|
|
1700
2112
|
function resolveCodexHome() {
|
|
1701
|
-
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ??
|
|
2113
|
+
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join13(homedir(), ".codex");
|
|
1702
2114
|
}
|
|
1703
2115
|
async function readExisting(path) {
|
|
1704
2116
|
try {
|
|
1705
|
-
return await
|
|
2117
|
+
return await readFile12(path, "utf8");
|
|
1706
2118
|
} catch {
|
|
1707
2119
|
return null;
|
|
1708
2120
|
}
|
|
@@ -1717,8 +2129,8 @@ async function exists3(path) {
|
|
|
1717
2129
|
}
|
|
1718
2130
|
|
|
1719
2131
|
// src/adapters/cursor/index.ts
|
|
1720
|
-
import { mkdir as
|
|
1721
|
-
import { dirname as
|
|
2132
|
+
import { mkdir as mkdir7, readFile as readFile13, stat as stat6 } from "fs/promises";
|
|
2133
|
+
import { dirname as dirname7, join as join14 } from "path";
|
|
1722
2134
|
|
|
1723
2135
|
// src/adapters/cursor/templates.ts
|
|
1724
2136
|
function cursorSkillFiles() {
|
|
@@ -1746,6 +2158,7 @@ alwaysApply: false
|
|
|
1746
2158
|
|
|
1747
2159
|
- AGENTS.md
|
|
1748
2160
|
- openspec/config.yaml
|
|
2161
|
+
- GitNexus code graph context, when available. Prefer it before broad repository scans; if it is unavailable, continue normally.
|
|
1749
2162
|
- \u5F53\u524D change \u76EE\u5F55\u4E0B\u7684 OpenSpec \u89C4\u5212\u4EA7\u7269
|
|
1750
2163
|
|
|
1751
2164
|
\u5982\u679C\u7528\u6237\u8F93\u5165\u7C7B\u4F3C \`/fet apply\` \u7684\u8BF7\u6C42\uFF0CCursor \u5F53\u524D\u7248\u672C\u672A\u5FC5\u4F1A\u628A\u672C\u6587\u4EF6\u6CE8\u518C\u4E3A\u539F\u751F slash command\u3002\u6B64\u65F6\u8BF7\u628A\u5B83\u5F53\u4F5C\u5DE5\u4F5C\u6D41\u610F\u56FE\uFF0C\u5E76\u63D0\u793A\u7528\u6237\u5728\u7EC8\u7AEF\u6267\u884C\u5BF9\u5E94\u7684 \`fet <cmd>\` \u547D\u4EE4\u3002
|
|
@@ -1754,6 +2167,35 @@ alwaysApply: false
|
|
|
1754
2167
|
}
|
|
1755
2168
|
function renderSkill(command) {
|
|
1756
2169
|
const usage = command === "passthrough" ? "fet passthrough <openspec-command> [...args]" : `fet ${command}`;
|
|
2170
|
+
if (command === "fill-context") {
|
|
2171
|
+
return `<!-- FET:MANAGED
|
|
2172
|
+
schemaVersion: 1
|
|
2173
|
+
fetVersion: ${FET_VERSION}
|
|
2174
|
+
generator: cursor-adapter
|
|
2175
|
+
adapterVersion: 1
|
|
2176
|
+
command: fet fill-context
|
|
2177
|
+
FET:END -->
|
|
2178
|
+
|
|
2179
|
+
---
|
|
2180
|
+
name: fet-fill-context
|
|
2181
|
+
description: Fill FET AGENTS.md placeholders with Cursor AI
|
|
2182
|
+
disable-model-invocation: false
|
|
2183
|
+
---
|
|
2184
|
+
|
|
2185
|
+
Run \`fet fill-context\` first if the IDE commands need refreshing.
|
|
2186
|
+
|
|
2187
|
+
${renderIdeModelPolicy(command)}
|
|
2188
|
+
|
|
2189
|
+
If GitNexus code graph context is available in Cursor, prefer it before broad repository scans. If it is unavailable, continue normally.
|
|
2190
|
+
|
|
2191
|
+
Then read:
|
|
2192
|
+
|
|
2193
|
+
- AGENTS.md
|
|
2194
|
+
- openspec/config.yaml
|
|
2195
|
+
|
|
2196
|
+
Replace every \`[NEEDS LLM INPUT]\` or \`[NEED LLM INPUT]\` placeholder in AGENTS.md with concrete project-specific content. Inspect README files, package scripts, routes, tests, source layout, and existing conventions before writing. Preserve FET managed markers and do not modify business code.
|
|
2197
|
+
`;
|
|
2198
|
+
}
|
|
1757
2199
|
return `<!-- FET:MANAGED
|
|
1758
2200
|
schemaVersion: 1
|
|
1759
2201
|
fetVersion: ${FET_VERSION}
|
|
@@ -1768,6 +2210,10 @@ description: Run FET-managed OpenSpec ${command} workflow from the terminal
|
|
|
1768
2210
|
disable-model-invocation: true
|
|
1769
2211
|
---
|
|
1770
2212
|
|
|
2213
|
+
${renderIdeModelPolicy(command)}
|
|
2214
|
+
|
|
2215
|
+
If GitNexus code graph context is available in Cursor, prefer it before broad repository scans. If it is unavailable, continue normally.
|
|
2216
|
+
|
|
1771
2217
|
\u6CE8\u610F\uFF1A\u6B64\u6587\u4EF6\u91C7\u7528 Cursor Skill \u76EE\u5F55\u7ED3\u6784\u3002\u5B83\u63D0\u4F9B \`/fet-${command}\` \u98CE\u683C\u7684\u5DE5\u4F5C\u6D41\u8BF4\u660E\uFF0C\u4E0D\u627F\u8BFA\u6CE8\u518C \`/fet ${command}\` \u8FD9\u79CD\u5E26\u7A7A\u683C\u7684\u539F\u751F slash command\u3002
|
|
1772
2218
|
|
|
1773
2219
|
\u8BF7\u5728\u7EC8\u7AEF\u4E2D\u6267\u884C\uFF1A
|
|
@@ -1786,7 +2232,7 @@ var CursorAdapter = class {
|
|
|
1786
2232
|
adapterVersion = 1;
|
|
1787
2233
|
async detect(projectRoot) {
|
|
1788
2234
|
return {
|
|
1789
|
-
detected: await exists4(
|
|
2235
|
+
detected: await exists4(join14(projectRoot, ".cursor")),
|
|
1790
2236
|
reason: "Cursor adapter is available for any project"
|
|
1791
2237
|
};
|
|
1792
2238
|
}
|
|
@@ -1803,7 +2249,7 @@ var CursorAdapter = class {
|
|
|
1803
2249
|
const written = [];
|
|
1804
2250
|
const skipped = [];
|
|
1805
2251
|
for (const file of plan.files) {
|
|
1806
|
-
const target =
|
|
2252
|
+
const target = join14(projectRoot, file.path);
|
|
1807
2253
|
const existing = await readExisting2(target);
|
|
1808
2254
|
if (existing && !existing.includes("FET:MANAGED") && !force) {
|
|
1809
2255
|
throw new FetError({
|
|
@@ -1816,7 +2262,7 @@ var CursorAdapter = class {
|
|
|
1816
2262
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
1817
2263
|
await createBackup(target);
|
|
1818
2264
|
}
|
|
1819
|
-
await
|
|
2265
|
+
await mkdir7(dirname7(target), { recursive: true });
|
|
1820
2266
|
await atomicWrite(target, file.content);
|
|
1821
2267
|
written.push(file.path);
|
|
1822
2268
|
}
|
|
@@ -1826,7 +2272,7 @@ var CursorAdapter = class {
|
|
|
1826
2272
|
const plan = await this.planInstall(projectRoot);
|
|
1827
2273
|
const checks = [];
|
|
1828
2274
|
for (const file of plan.files) {
|
|
1829
|
-
const target =
|
|
2275
|
+
const target = join14(projectRoot, file.path);
|
|
1830
2276
|
const content = await readExisting2(target);
|
|
1831
2277
|
const managed = Boolean(content?.includes("FET:MANAGED"));
|
|
1832
2278
|
const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
|
|
@@ -1842,7 +2288,7 @@ var CursorAdapter = class {
|
|
|
1842
2288
|
};
|
|
1843
2289
|
async function readExisting2(path) {
|
|
1844
2290
|
try {
|
|
1845
|
-
return await
|
|
2291
|
+
return await readFile13(path, "utf8");
|
|
1846
2292
|
} catch {
|
|
1847
2293
|
return null;
|
|
1848
2294
|
}
|
|
@@ -1857,40 +2303,42 @@ async function exists4(path) {
|
|
|
1857
2303
|
}
|
|
1858
2304
|
|
|
1859
2305
|
// src/openspec/adapter.ts
|
|
1860
|
-
import { execFile as
|
|
1861
|
-
import { promisify as
|
|
2306
|
+
import { execFile as execFile4 } from "child_process";
|
|
2307
|
+
import { promisify as promisify4 } from "util";
|
|
1862
2308
|
|
|
1863
2309
|
// src/openspec/inspector.ts
|
|
1864
2310
|
import { readdir, stat as stat7 } from "fs/promises";
|
|
1865
|
-
import { join as
|
|
2311
|
+
import { join as join15 } from "path";
|
|
1866
2312
|
async function inspectOpenSpecProject(projectRoot) {
|
|
1867
|
-
const openspecPath =
|
|
1868
|
-
const changesPath =
|
|
1869
|
-
const
|
|
2313
|
+
const openspecPath = join15(projectRoot, "openspec");
|
|
2314
|
+
const changesPath = join15(openspecPath, "changes");
|
|
2315
|
+
const legacyArchivePath = join15(openspecPath, "archive");
|
|
2316
|
+
const changesArchivePath = join15(changesPath, "archive");
|
|
1870
2317
|
return {
|
|
1871
2318
|
exists: await exists5(openspecPath),
|
|
1872
|
-
changes: await listDirectories(changesPath),
|
|
1873
|
-
archived: await listDirectories(
|
|
2319
|
+
changes: await listDirectories(changesPath, { exclude: ["archive"] }),
|
|
2320
|
+
archived: [.../* @__PURE__ */ new Set([...await listDirectories(legacyArchivePath), ...await listDirectories(changesArchivePath)])]
|
|
1874
2321
|
};
|
|
1875
2322
|
}
|
|
1876
2323
|
async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
1877
|
-
const changePath =
|
|
1878
|
-
const tasksPath =
|
|
1879
|
-
const specsPath =
|
|
2324
|
+
const changePath = join15(projectRoot, "openspec", "changes", changeId);
|
|
2325
|
+
const tasksPath = join15(changePath, "tasks.md");
|
|
2326
|
+
const specsPath = join15(changePath, "specs");
|
|
1880
2327
|
return {
|
|
1881
2328
|
changeId,
|
|
1882
2329
|
exists: await exists5(changePath),
|
|
1883
|
-
hasProposal: await exists5(
|
|
2330
|
+
hasProposal: await exists5(join15(changePath, "proposal.md")),
|
|
1884
2331
|
hasTasks: await exists5(tasksPath),
|
|
1885
2332
|
hasSpecs: await exists5(specsPath),
|
|
1886
2333
|
tasksPath,
|
|
1887
2334
|
changePath
|
|
1888
2335
|
};
|
|
1889
2336
|
}
|
|
1890
|
-
async function listDirectories(path) {
|
|
2337
|
+
async function listDirectories(path, options = {}) {
|
|
1891
2338
|
try {
|
|
1892
2339
|
const entries = await readdir(path, { withFileTypes: true });
|
|
1893
|
-
|
|
2340
|
+
const excluded = new Set(options.exclude ?? []);
|
|
2341
|
+
return entries.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).map((entry) => entry.name);
|
|
1894
2342
|
} catch {
|
|
1895
2343
|
return [];
|
|
1896
2344
|
}
|
|
@@ -1905,9 +2353,9 @@ async function exists5(path) {
|
|
|
1905
2353
|
}
|
|
1906
2354
|
|
|
1907
2355
|
// src/openspec/resolver.ts
|
|
1908
|
-
import { execFile as
|
|
1909
|
-
import { promisify as
|
|
1910
|
-
var
|
|
2356
|
+
import { execFile as execFile3 } from "child_process";
|
|
2357
|
+
import { promisify as promisify3 } from "util";
|
|
2358
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
1911
2359
|
async function resolveOpenSpecExecutable() {
|
|
1912
2360
|
const executablePath = await findExecutable();
|
|
1913
2361
|
const version = await readVersion(executablePath);
|
|
@@ -1954,7 +2402,7 @@ async function readVersion(executablePath) {
|
|
|
1954
2402
|
}
|
|
1955
2403
|
}
|
|
1956
2404
|
function exec(command, args) {
|
|
1957
|
-
return
|
|
2405
|
+
return execFileAsync3(command, args, { shell: process.platform === "win32" });
|
|
1958
2406
|
}
|
|
1959
2407
|
|
|
1960
2408
|
// src/openspec/runner.ts
|
|
@@ -2000,7 +2448,7 @@ async function runOpenSpec(executablePath, command, args, options) {
|
|
|
2000
2448
|
}
|
|
2001
2449
|
|
|
2002
2450
|
// src/openspec/adapter.ts
|
|
2003
|
-
var
|
|
2451
|
+
var execFileAsync4 = promisify4(execFile4);
|
|
2004
2452
|
var DefaultOpenSpecAdapter = class {
|
|
2005
2453
|
identity;
|
|
2006
2454
|
async resolveExecutable() {
|
|
@@ -2012,7 +2460,7 @@ var DefaultOpenSpecAdapter = class {
|
|
|
2012
2460
|
const executable = identity.executablePath === "npx openspec" ? "npx" : identity.executablePath;
|
|
2013
2461
|
const args = identity.executablePath === "npx openspec" ? ["openspec", "--help"] : ["--help"];
|
|
2014
2462
|
try {
|
|
2015
|
-
const { stdout } = await
|
|
2463
|
+
const { stdout } = await execFileAsync4(executable, args, { shell: process.platform === "win32" });
|
|
2016
2464
|
return {
|
|
2017
2465
|
version: identity.version,
|
|
2018
2466
|
commands: parseCommands(stdout),
|
|
@@ -2056,12 +2504,12 @@ function parseCommands(help) {
|
|
|
2056
2504
|
}
|
|
2057
2505
|
|
|
2058
2506
|
// src/scanner/package.ts
|
|
2059
|
-
import { readFile as
|
|
2060
|
-
import { join as
|
|
2507
|
+
import { readFile as readFile14, stat as stat8 } from "fs/promises";
|
|
2508
|
+
import { join as join16 } from "path";
|
|
2061
2509
|
import { parse as parse2 } from "yaml";
|
|
2062
2510
|
async function readPackageJson(projectRoot) {
|
|
2063
2511
|
try {
|
|
2064
|
-
return JSON.parse(await
|
|
2512
|
+
return JSON.parse(await readFile14(join16(projectRoot, "package.json"), "utf8"));
|
|
2065
2513
|
} catch {
|
|
2066
2514
|
return null;
|
|
2067
2515
|
}
|
|
@@ -2127,7 +2575,7 @@ function detectFramework(pkg) {
|
|
|
2127
2575
|
}
|
|
2128
2576
|
async function detectLanguage(projectRoot, pkg) {
|
|
2129
2577
|
const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
|
|
2130
|
-
if (deps.typescript || await exists6(
|
|
2578
|
+
if (deps.typescript || await exists6(join16(projectRoot, "tsconfig.json"))) {
|
|
2131
2579
|
return "typescript";
|
|
2132
2580
|
}
|
|
2133
2581
|
return "javascript";
|
|
@@ -2142,7 +2590,7 @@ async function detectWorkspaces(projectRoot, pkg) {
|
|
|
2142
2590
|
return packageWorkspaces;
|
|
2143
2591
|
}
|
|
2144
2592
|
try {
|
|
2145
|
-
const workspace = parse2(await
|
|
2593
|
+
const workspace = parse2(await readFile14(join16(projectRoot, "pnpm-workspace.yaml"), "utf8"));
|
|
2146
2594
|
return (workspace?.packages ?? []).map((path) => ({
|
|
2147
2595
|
name: path,
|
|
2148
2596
|
path,
|
|
@@ -2162,7 +2610,7 @@ async function detectLockManagers(projectRoot) {
|
|
|
2162
2610
|
];
|
|
2163
2611
|
const found = [];
|
|
2164
2612
|
for (const [file, manager] of lockFiles) {
|
|
2165
|
-
if (await exists6(
|
|
2613
|
+
if (await exists6(join16(projectRoot, file))) {
|
|
2166
2614
|
found.push(manager);
|
|
2167
2615
|
}
|
|
2168
2616
|
}
|
|
@@ -2188,12 +2636,12 @@ async function exists6(path) {
|
|
|
2188
2636
|
|
|
2189
2637
|
// src/scanner/routes.ts
|
|
2190
2638
|
import { readdir as readdir2, stat as stat9 } from "fs/promises";
|
|
2191
|
-
import { join as
|
|
2639
|
+
import { join as join17, relative, sep } from "path";
|
|
2192
2640
|
async function scanRoutes(projectRoot) {
|
|
2193
2641
|
const candidates = ["src/routes", "src/pages", "app", "pages"];
|
|
2194
2642
|
const routes = [];
|
|
2195
2643
|
for (const candidate of candidates) {
|
|
2196
|
-
const root =
|
|
2644
|
+
const root = join17(projectRoot, candidate);
|
|
2197
2645
|
if (!await exists7(root)) {
|
|
2198
2646
|
continue;
|
|
2199
2647
|
}
|
|
@@ -2221,7 +2669,7 @@ async function listFiles(root) {
|
|
|
2221
2669
|
const entries = await readdir2(root, { withFileTypes: true });
|
|
2222
2670
|
const files = [];
|
|
2223
2671
|
for (const entry of entries) {
|
|
2224
|
-
const path =
|
|
2672
|
+
const path = join17(root, entry.name);
|
|
2225
2673
|
if (entry.isDirectory()) {
|
|
2226
2674
|
files.push(...await listFiles(path));
|
|
2227
2675
|
} else {
|
|
@@ -2285,6 +2733,11 @@ var OutputWriter = class {
|
|
|
2285
2733
|
}
|
|
2286
2734
|
}
|
|
2287
2735
|
warn(message, details) {
|
|
2736
|
+
if (this.json) {
|
|
2737
|
+
process.stderr.write(`${JSON.stringify({ ok: true, warning: message, details }, null, 2)}
|
|
2738
|
+
`);
|
|
2739
|
+
return;
|
|
2740
|
+
}
|
|
2288
2741
|
if (!this.json) {
|
|
2289
2742
|
process.stderr.write(`\u8B66\u544A\uFF1A${message}${formatDetails(details)}
|
|
2290
2743
|
`);
|
|
@@ -2373,6 +2826,7 @@ var program = new Command();
|
|
|
2373
2826
|
program.name("fet").description("Frontend workflow orchestration tool built around OpenSpec.").enablePositionalOptions().version(FET_VERSION).option("--cwd <path>", "\u6307\u5B9A\u9879\u76EE\u6839\u76EE\u5F55").option("--change <id>", "\u6307\u5B9A OpenSpec change").option("--yes", "\u5BF9\u4F4E\u98CE\u9669\u786E\u8BA4\u4F7F\u7528\u9ED8\u8BA4\u540C\u610F").option("--json", "\u8F93\u51FA\u673A\u5668\u53EF\u8BFB JSON").option("--verbose", "\u8F93\u51FA\u8BCA\u65AD\u7EC6\u8282").option("--no-color", "\u7981\u7528\u7EC8\u7AEF\u989C\u8272");
|
|
2374
2827
|
addGlobalOptions(program.command("init")).description("\u521D\u59CB\u5316 FET + OpenSpec").action(wrap("init", initCommand));
|
|
2375
2828
|
addGlobalOptions(program.command("update-context")).description("\u66F4\u65B0\u9879\u76EE\u4E0A\u4E0B\u6587").action(wrap("update-context", updateContextCommand));
|
|
2829
|
+
addGlobalOptions(program.command("fill-context")).description("Refresh IDE prompts for filling AGENTS.md placeholders").action(wrap("fill-context", fillContextCommand));
|
|
2376
2830
|
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(
|
|
2377
2831
|
wrap("doctor", (ctx, options) => doctorCommand(ctx, { fixLock: Boolean(options.fixLock) }))
|
|
2378
2832
|
);
|
|
@@ -2394,6 +2848,8 @@ function wrap(command, handler) {
|
|
|
2394
2848
|
const opts = isCommandLike(maybeCommand) ? { ...maybeCommand.parent?.opts(), ...maybeCommand.opts() } : program.opts();
|
|
2395
2849
|
const ctx = await createCommandContext(command, { ...opts, ...extractGlobalOptions(args) });
|
|
2396
2850
|
try {
|
|
2851
|
+
await confirmModelPolicyRecommendation(ctx);
|
|
2852
|
+
await warnIfContextPlaceholdersRemain(ctx);
|
|
2397
2853
|
await handler(ctx, ...args);
|
|
2398
2854
|
} catch (error) {
|
|
2399
2855
|
const fetError = toFetError(error);
|
|
@@ -2402,6 +2858,40 @@ function wrap(command, handler) {
|
|
|
2402
2858
|
}
|
|
2403
2859
|
};
|
|
2404
2860
|
}
|
|
2861
|
+
async function confirmModelPolicyRecommendation(ctx) {
|
|
2862
|
+
const mismatch = getCommandModelPolicyMismatch(ctx.command);
|
|
2863
|
+
if (!mismatch) {
|
|
2864
|
+
return;
|
|
2865
|
+
}
|
|
2866
|
+
const warning = formatModelPolicyMismatch(mismatch);
|
|
2867
|
+
ctx.output.warn(`${warning} You can stop now to switch models, or continue this command.`);
|
|
2868
|
+
if (ctx.yes || ctx.json || !process.stdin.isTTY || !process.stderr.isTTY) {
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
2872
|
+
try {
|
|
2873
|
+
const answer = (await rl.question("Continue anyway? [y/N] ")).trim().toLowerCase();
|
|
2874
|
+
if (answer !== "y" && answer !== "yes") {
|
|
2875
|
+
throw new FetError({
|
|
2876
|
+
code: "USER_CANCELLED" /* UserCancelled */,
|
|
2877
|
+
message: "Command cancelled so you can switch IDE model.",
|
|
2878
|
+
details: { command: ctx.command, detected: mismatch.detected, recommended: mismatch.recommended },
|
|
2879
|
+
suggestedCommand: `Switch IDE model, then rerun fet ${ctx.command}.`
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
2882
|
+
} finally {
|
|
2883
|
+
rl.close();
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
async function warnIfContextPlaceholdersRemain(ctx) {
|
|
2887
|
+
if (["init", "update-context", "fill-context", "doctor"].includes(ctx.command)) {
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
const count2 = await countAgentsLlmPlaceholders(ctx.projectRoot);
|
|
2891
|
+
if (count2 > 0) {
|
|
2892
|
+
ctx.output.warn(renderAgentsPlaceholderWarning(count2));
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2405
2895
|
function isCommandLike(value) {
|
|
2406
2896
|
return value instanceof Command;
|
|
2407
2897
|
}
|