@nick848/fet 1.0.3 → 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 +6 -0
- package/dist/cli/index.js +341 -90
- 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,19 +578,20 @@ async function exists(path) {
|
|
|
473
578
|
}
|
|
474
579
|
|
|
475
580
|
// src/commands/doctor.ts
|
|
476
|
-
import { readFile as
|
|
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",
|
|
484
|
-
checks.push(await checkPlaceholders(
|
|
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));
|
|
485
591
|
for (const adapter of ctx.toolAdapters) {
|
|
486
592
|
checks.push(...await adapter.doctor(ctx.projectRoot));
|
|
487
593
|
}
|
|
488
|
-
const lockPath =
|
|
594
|
+
const lockPath = join8(ctx.projectRoot, "openspec", ".fet.lock");
|
|
489
595
|
if (await exists2(lockPath)) {
|
|
490
596
|
if (options.fixLock) {
|
|
491
597
|
await clearLock(ctx.projectRoot);
|
|
@@ -503,6 +609,25 @@ async function doctorCommand(ctx, options = {}) {
|
|
|
503
609
|
data: checks
|
|
504
610
|
});
|
|
505
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
|
+
}
|
|
506
631
|
async function checkOpenSpec(ctx) {
|
|
507
632
|
try {
|
|
508
633
|
const identity = await ctx.openSpec.resolveExecutable();
|
|
@@ -522,10 +647,10 @@ async function checkState(ctx) {
|
|
|
522
647
|
async function checkFile(id, path, missing, suggestedCommand) {
|
|
523
648
|
return await exists2(path) ? { id, status: "pass", message: `${id} \u5B58\u5728` } : { id, status: "warn", message: missing, suggestedCommand };
|
|
524
649
|
}
|
|
525
|
-
async function checkPlaceholders(
|
|
650
|
+
async function checkPlaceholders(projectRoot) {
|
|
526
651
|
try {
|
|
527
|
-
|
|
528
|
-
const count2 =
|
|
652
|
+
await readFile7(join8(projectRoot, "AGENTS.md"), "utf8");
|
|
653
|
+
const count2 = await countAgentsLlmPlaceholders(projectRoot);
|
|
529
654
|
return count2 ? {
|
|
530
655
|
id: "context-placeholders",
|
|
531
656
|
status: "warn",
|
|
@@ -546,15 +671,14 @@ async function exists2(path) {
|
|
|
546
671
|
}
|
|
547
672
|
|
|
548
673
|
// src/commands/fill-context.ts
|
|
549
|
-
import { mkdir as mkdir3
|
|
550
|
-
import { dirname as dirname5, join as
|
|
551
|
-
var placeholderPattern = /\[NEEDS? LLM INPUT\]/g;
|
|
674
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
675
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
552
676
|
async function fillContextCommand(ctx) {
|
|
553
677
|
await withProjectLock(
|
|
554
678
|
ctx.projectRoot,
|
|
555
679
|
{ command: "fill-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
|
|
556
680
|
async () => {
|
|
557
|
-
const handoffPath =
|
|
681
|
+
const handoffPath = join9(ctx.projectRoot, ".fet", "fill-context.md");
|
|
558
682
|
await mkdir3(dirname5(handoffPath), { recursive: true });
|
|
559
683
|
await atomicWrite(handoffPath, renderGenericHandoff());
|
|
560
684
|
for (const adapter of ctx.toolAdapters) {
|
|
@@ -573,7 +697,7 @@ async function fillContextCommand(ctx) {
|
|
|
573
697
|
}
|
|
574
698
|
}
|
|
575
699
|
);
|
|
576
|
-
const placeholders = await
|
|
700
|
+
const placeholders = await countAgentsLlmPlaceholders(ctx.projectRoot);
|
|
577
701
|
ctx.output.result({
|
|
578
702
|
ok: true,
|
|
579
703
|
command: "fill-context",
|
|
@@ -608,23 +732,15 @@ Use the IDE AI to complete FET-generated placeholders.
|
|
|
608
732
|
6. Run \`fet doctor\` and confirm no AGENTS.md placeholder warning remains.
|
|
609
733
|
`;
|
|
610
734
|
}
|
|
611
|
-
async function countPlaceholders(path) {
|
|
612
|
-
try {
|
|
613
|
-
const content = await readFile7(path, "utf8");
|
|
614
|
-
return [...content.matchAll(placeholderPattern)].length;
|
|
615
|
-
} catch {
|
|
616
|
-
return 0;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
735
|
|
|
620
736
|
// src/commands/proxy.ts
|
|
621
737
|
import { readFile as readFile10 } from "fs/promises";
|
|
622
|
-
import { join as
|
|
738
|
+
import { join as join11 } from "path";
|
|
623
739
|
|
|
624
740
|
// src/state/project.ts
|
|
625
|
-
import { execFile } from "child_process";
|
|
626
|
-
import { promisify } from "util";
|
|
627
|
-
var
|
|
741
|
+
import { execFile as execFile2 } from "child_process";
|
|
742
|
+
import { promisify as promisify2 } from "util";
|
|
743
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
628
744
|
async function detectProjectIdentity(projectRoot) {
|
|
629
745
|
const [gitRoot, branch, headCommit] = await Promise.all([
|
|
630
746
|
git(projectRoot, ["rev-parse", "--show-toplevel"]),
|
|
@@ -640,7 +756,7 @@ async function detectProjectIdentity(projectRoot) {
|
|
|
640
756
|
}
|
|
641
757
|
async function git(cwd, args) {
|
|
642
758
|
try {
|
|
643
|
-
const { stdout } = await
|
|
759
|
+
const { stdout } = await execFileAsync2("git", args, { cwd });
|
|
644
760
|
return stdout.trim() || null;
|
|
645
761
|
} catch {
|
|
646
762
|
return null;
|
|
@@ -649,7 +765,7 @@ async function git(cwd, args) {
|
|
|
649
765
|
|
|
650
766
|
// src/state/store.ts
|
|
651
767
|
import { mkdir as mkdir4, readFile as readFile8 } from "fs/promises";
|
|
652
|
-
import { join as
|
|
768
|
+
import { join as join10 } from "path";
|
|
653
769
|
|
|
654
770
|
// src/state/schema.ts
|
|
655
771
|
var phases = ["explore", "propose", "implement", "verify", "sync", "archive"];
|
|
@@ -670,6 +786,7 @@ function createGlobalState(fetVersion, project) {
|
|
|
670
786
|
scannerVersion: 1
|
|
671
787
|
},
|
|
672
788
|
toolAdapters: {},
|
|
789
|
+
graph: {},
|
|
673
790
|
verifyAuthorization: null,
|
|
674
791
|
lastDoctor: null
|
|
675
792
|
};
|
|
@@ -757,7 +874,7 @@ var StateStore = class {
|
|
|
757
874
|
}
|
|
758
875
|
async writeGlobal(state) {
|
|
759
876
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
760
|
-
await mkdir4(
|
|
877
|
+
await mkdir4(join10(this.projectRoot, "openspec"), { recursive: true });
|
|
761
878
|
await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
|
|
762
879
|
`);
|
|
763
880
|
}
|
|
@@ -778,15 +895,15 @@ var StateStore = class {
|
|
|
778
895
|
}
|
|
779
896
|
async writeChange(state) {
|
|
780
897
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
781
|
-
await mkdir4(
|
|
898
|
+
await mkdir4(join10(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
|
|
782
899
|
await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
|
|
783
900
|
`);
|
|
784
901
|
}
|
|
785
902
|
globalPath() {
|
|
786
|
-
return
|
|
903
|
+
return join10(this.projectRoot, "openspec", "fet-state.json");
|
|
787
904
|
}
|
|
788
905
|
changePath(changeId) {
|
|
789
|
-
return
|
|
906
|
+
return join10(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
|
|
790
907
|
}
|
|
791
908
|
};
|
|
792
909
|
function isNotFound(error) {
|
|
@@ -903,7 +1020,7 @@ async function createChangelogEntry(projectRoot, changeId) {
|
|
|
903
1020
|
};
|
|
904
1021
|
}
|
|
905
1022
|
async function appendChangelog(projectRoot, entry) {
|
|
906
|
-
const changelogPath =
|
|
1023
|
+
const changelogPath = join11(projectRoot, "CHANGELOG.md");
|
|
907
1024
|
const existing = await readOptional3(changelogPath);
|
|
908
1025
|
const block = `updateTime: ${entry.updateTime}
|
|
909
1026
|
\u66F4\u65B0\u5185\u5BB9:${entry.content}
|
|
@@ -914,12 +1031,12 @@ ${block}` : block;
|
|
|
914
1031
|
await atomicWrite(changelogPath, next);
|
|
915
1032
|
}
|
|
916
1033
|
async function readChangeRequirement(projectRoot, changeId) {
|
|
917
|
-
const changeRoot =
|
|
918
|
-
const proposal = await readOptional3(
|
|
1034
|
+
const changeRoot = join11(projectRoot, "openspec", "changes", changeId);
|
|
1035
|
+
const proposal = await readOptional3(join11(changeRoot, "proposal.md"));
|
|
919
1036
|
if (proposal) {
|
|
920
1037
|
return summarizeMarkdown(proposal);
|
|
921
1038
|
}
|
|
922
|
-
const readme = await readOptional3(
|
|
1039
|
+
const readme = await readOptional3(join11(changeRoot, "README.md"));
|
|
923
1040
|
if (readme) {
|
|
924
1041
|
return summarizeMarkdown(readme);
|
|
925
1042
|
}
|
|
@@ -1064,7 +1181,7 @@ async function assertVerified(ctx) {
|
|
|
1064
1181
|
// src/commands/verify.ts
|
|
1065
1182
|
import { createHash } from "crypto";
|
|
1066
1183
|
import { mkdir as mkdir5, readFile as readFile11, stat as stat4 } from "fs/promises";
|
|
1067
|
-
import { join as
|
|
1184
|
+
import { join as join12 } from "path";
|
|
1068
1185
|
async function verifyCommand(ctx, options) {
|
|
1069
1186
|
if (options.auto) {
|
|
1070
1187
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
@@ -1131,8 +1248,8 @@ async function verifyCommand(ctx, options) {
|
|
|
1131
1248
|
async function writeInstructions(ctx, changeId) {
|
|
1132
1249
|
await assertChangeExists(ctx, changeId);
|
|
1133
1250
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1134
|
-
const dir =
|
|
1135
|
-
const instructionsPath =
|
|
1251
|
+
const dir = join12(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
1252
|
+
const instructionsPath = join12(dir, "verify-instructions.md");
|
|
1136
1253
|
await mkdir5(dir, { recursive: true });
|
|
1137
1254
|
await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
|
|
1138
1255
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
@@ -1149,7 +1266,7 @@ async function writeInstructions(ctx, changeId) {
|
|
|
1149
1266
|
async function markDone(ctx, changeId) {
|
|
1150
1267
|
await assertChangeExists(ctx, changeId);
|
|
1151
1268
|
const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1152
|
-
const instructionsPath =
|
|
1269
|
+
const instructionsPath = join12(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
|
|
1153
1270
|
const instructions = await readInstructions(instructionsPath, changeId);
|
|
1154
1271
|
const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
|
|
1155
1272
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
@@ -1235,13 +1352,76 @@ async function resolveChangeId(ctx) {
|
|
|
1235
1352
|
});
|
|
1236
1353
|
}
|
|
1237
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
|
+
|
|
1238
1418
|
// src/cli/context.ts
|
|
1239
1419
|
import { resolve } from "path";
|
|
1240
1420
|
|
|
1241
1421
|
// src/adapters/codex/index.ts
|
|
1242
1422
|
import { mkdir as mkdir6, readFile as readFile12, stat as stat5 } from "fs/promises";
|
|
1243
1423
|
import { homedir } from "os";
|
|
1244
|
-
import { dirname as dirname6, join as
|
|
1424
|
+
import { dirname as dirname6, join as join13 } from "path";
|
|
1245
1425
|
|
|
1246
1426
|
// src/adapters/commands.ts
|
|
1247
1427
|
var FET_WORKFLOW_COMMANDS = [
|
|
@@ -1278,6 +1458,8 @@ Before doing FET or OpenSpec work in Codex, read:
|
|
|
1278
1458
|
- openspec/config.yaml
|
|
1279
1459
|
- the active change files under openspec/changes/<change-id>/, when a change is selected
|
|
1280
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
|
+
|
|
1281
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.
|
|
1282
1464
|
|
|
1283
1465
|
Command guides live in .codex/fet/commands/.
|
|
@@ -1313,8 +1495,12 @@ FET:END -->
|
|
|
1313
1495
|
|
|
1314
1496
|
# fet ${command}
|
|
1315
1497
|
|
|
1498
|
+
${renderIdeModelPolicy(command)}
|
|
1499
|
+
|
|
1316
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.
|
|
1317
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
|
+
|
|
1318
1504
|
Then run:
|
|
1319
1505
|
|
|
1320
1506
|
\`\`\`sh
|
|
@@ -1337,8 +1523,12 @@ FET:END -->
|
|
|
1337
1523
|
|
|
1338
1524
|
# fet passthrough
|
|
1339
1525
|
|
|
1526
|
+
${renderIdeModelPolicy("passthrough")}
|
|
1527
|
+
|
|
1340
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.
|
|
1341
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
|
+
|
|
1342
1532
|
Then run:
|
|
1343
1533
|
|
|
1344
1534
|
\`\`\`sh
|
|
@@ -1405,6 +1595,8 @@ Use FET as the entry point for this OpenSpec workflow.
|
|
|
1405
1595
|
|
|
1406
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.
|
|
1407
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
|
+
|
|
1408
1600
|
Run:
|
|
1409
1601
|
|
|
1410
1602
|
\`\`\`sh
|
|
@@ -1425,8 +1617,12 @@ FET:END -->
|
|
|
1425
1617
|
|
|
1426
1618
|
# fet fill-context
|
|
1427
1619
|
|
|
1620
|
+
${renderIdeModelPolicy("fill-context")}
|
|
1621
|
+
|
|
1428
1622
|
Use this command to complete FET-generated project context placeholders with Codex.
|
|
1429
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
|
+
|
|
1430
1626
|
First run:
|
|
1431
1627
|
|
|
1432
1628
|
\`\`\`sh
|
|
@@ -1809,6 +2005,7 @@ Output:
|
|
|
1809
2005
|
);
|
|
1810
2006
|
}
|
|
1811
2007
|
function renderManagedSlashPrompt(command, description, body) {
|
|
2008
|
+
const policyCommand = command.split(/\s+/)[1] ?? command;
|
|
1812
2009
|
return `<!-- FET:MANAGED
|
|
1813
2010
|
schemaVersion: 1
|
|
1814
2011
|
fetVersion: ${FET_VERSION}
|
|
@@ -1822,6 +2019,10 @@ description: ${description}
|
|
|
1822
2019
|
argument-hint: command arguments
|
|
1823
2020
|
---
|
|
1824
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
|
+
|
|
1825
2026
|
${body}
|
|
1826
2027
|
`;
|
|
1827
2028
|
}
|
|
@@ -1832,7 +2033,7 @@ var CodexAdapter = class {
|
|
|
1832
2033
|
adapterVersion = 1;
|
|
1833
2034
|
async detect(projectRoot) {
|
|
1834
2035
|
return {
|
|
1835
|
-
detected: await exists3(
|
|
2036
|
+
detected: await exists3(join13(projectRoot, ".codex")) || await exists3(join13(projectRoot, "AGENTS.md")),
|
|
1836
2037
|
reason: "Codex adapter is available for projects that use AGENTS.md"
|
|
1837
2038
|
};
|
|
1838
2039
|
}
|
|
@@ -1898,9 +2099,9 @@ var CodexAdapter = class {
|
|
|
1898
2099
|
};
|
|
1899
2100
|
function resolveTarget(projectRoot, file) {
|
|
1900
2101
|
if (file.root === "codex-home") {
|
|
1901
|
-
return
|
|
2102
|
+
return join13(resolveCodexHome(), file.path);
|
|
1902
2103
|
}
|
|
1903
|
-
return
|
|
2104
|
+
return join13(projectRoot, file.path);
|
|
1904
2105
|
}
|
|
1905
2106
|
function displayPathFor(file) {
|
|
1906
2107
|
if (file.root === "codex-home") {
|
|
@@ -1909,7 +2110,7 @@ function displayPathFor(file) {
|
|
|
1909
2110
|
return file.path;
|
|
1910
2111
|
}
|
|
1911
2112
|
function resolveCodexHome() {
|
|
1912
|
-
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ??
|
|
2113
|
+
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join13(homedir(), ".codex");
|
|
1913
2114
|
}
|
|
1914
2115
|
async function readExisting(path) {
|
|
1915
2116
|
try {
|
|
@@ -1929,7 +2130,7 @@ async function exists3(path) {
|
|
|
1929
2130
|
|
|
1930
2131
|
// src/adapters/cursor/index.ts
|
|
1931
2132
|
import { mkdir as mkdir7, readFile as readFile13, stat as stat6 } from "fs/promises";
|
|
1932
|
-
import { dirname as dirname7, join as
|
|
2133
|
+
import { dirname as dirname7, join as join14 } from "path";
|
|
1933
2134
|
|
|
1934
2135
|
// src/adapters/cursor/templates.ts
|
|
1935
2136
|
function cursorSkillFiles() {
|
|
@@ -1957,6 +2158,7 @@ alwaysApply: false
|
|
|
1957
2158
|
|
|
1958
2159
|
- AGENTS.md
|
|
1959
2160
|
- openspec/config.yaml
|
|
2161
|
+
- GitNexus code graph context, when available. Prefer it before broad repository scans; if it is unavailable, continue normally.
|
|
1960
2162
|
- \u5F53\u524D change \u76EE\u5F55\u4E0B\u7684 OpenSpec \u89C4\u5212\u4EA7\u7269
|
|
1961
2163
|
|
|
1962
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
|
|
@@ -1982,6 +2184,10 @@ disable-model-invocation: false
|
|
|
1982
2184
|
|
|
1983
2185
|
Run \`fet fill-context\` first if the IDE commands need refreshing.
|
|
1984
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
|
+
|
|
1985
2191
|
Then read:
|
|
1986
2192
|
|
|
1987
2193
|
- AGENTS.md
|
|
@@ -2004,6 +2210,10 @@ description: Run FET-managed OpenSpec ${command} workflow from the terminal
|
|
|
2004
2210
|
disable-model-invocation: true
|
|
2005
2211
|
---
|
|
2006
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
|
+
|
|
2007
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
|
|
2008
2218
|
|
|
2009
2219
|
\u8BF7\u5728\u7EC8\u7AEF\u4E2D\u6267\u884C\uFF1A
|
|
@@ -2022,7 +2232,7 @@ var CursorAdapter = class {
|
|
|
2022
2232
|
adapterVersion = 1;
|
|
2023
2233
|
async detect(projectRoot) {
|
|
2024
2234
|
return {
|
|
2025
|
-
detected: await exists4(
|
|
2235
|
+
detected: await exists4(join14(projectRoot, ".cursor")),
|
|
2026
2236
|
reason: "Cursor adapter is available for any project"
|
|
2027
2237
|
};
|
|
2028
2238
|
}
|
|
@@ -2039,7 +2249,7 @@ var CursorAdapter = class {
|
|
|
2039
2249
|
const written = [];
|
|
2040
2250
|
const skipped = [];
|
|
2041
2251
|
for (const file of plan.files) {
|
|
2042
|
-
const target =
|
|
2252
|
+
const target = join14(projectRoot, file.path);
|
|
2043
2253
|
const existing = await readExisting2(target);
|
|
2044
2254
|
if (existing && !existing.includes("FET:MANAGED") && !force) {
|
|
2045
2255
|
throw new FetError({
|
|
@@ -2062,7 +2272,7 @@ var CursorAdapter = class {
|
|
|
2062
2272
|
const plan = await this.planInstall(projectRoot);
|
|
2063
2273
|
const checks = [];
|
|
2064
2274
|
for (const file of plan.files) {
|
|
2065
|
-
const target =
|
|
2275
|
+
const target = join14(projectRoot, file.path);
|
|
2066
2276
|
const content = await readExisting2(target);
|
|
2067
2277
|
const managed = Boolean(content?.includes("FET:MANAGED"));
|
|
2068
2278
|
const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
|
|
@@ -2093,17 +2303,17 @@ async function exists4(path) {
|
|
|
2093
2303
|
}
|
|
2094
2304
|
|
|
2095
2305
|
// src/openspec/adapter.ts
|
|
2096
|
-
import { execFile as
|
|
2097
|
-
import { promisify as
|
|
2306
|
+
import { execFile as execFile4 } from "child_process";
|
|
2307
|
+
import { promisify as promisify4 } from "util";
|
|
2098
2308
|
|
|
2099
2309
|
// src/openspec/inspector.ts
|
|
2100
2310
|
import { readdir, stat as stat7 } from "fs/promises";
|
|
2101
|
-
import { join as
|
|
2311
|
+
import { join as join15 } from "path";
|
|
2102
2312
|
async function inspectOpenSpecProject(projectRoot) {
|
|
2103
|
-
const openspecPath =
|
|
2104
|
-
const changesPath =
|
|
2105
|
-
const legacyArchivePath =
|
|
2106
|
-
const changesArchivePath =
|
|
2313
|
+
const openspecPath = join15(projectRoot, "openspec");
|
|
2314
|
+
const changesPath = join15(openspecPath, "changes");
|
|
2315
|
+
const legacyArchivePath = join15(openspecPath, "archive");
|
|
2316
|
+
const changesArchivePath = join15(changesPath, "archive");
|
|
2107
2317
|
return {
|
|
2108
2318
|
exists: await exists5(openspecPath),
|
|
2109
2319
|
changes: await listDirectories(changesPath, { exclude: ["archive"] }),
|
|
@@ -2111,13 +2321,13 @@ async function inspectOpenSpecProject(projectRoot) {
|
|
|
2111
2321
|
};
|
|
2112
2322
|
}
|
|
2113
2323
|
async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
2114
|
-
const changePath =
|
|
2115
|
-
const tasksPath =
|
|
2116
|
-
const specsPath =
|
|
2324
|
+
const changePath = join15(projectRoot, "openspec", "changes", changeId);
|
|
2325
|
+
const tasksPath = join15(changePath, "tasks.md");
|
|
2326
|
+
const specsPath = join15(changePath, "specs");
|
|
2117
2327
|
return {
|
|
2118
2328
|
changeId,
|
|
2119
2329
|
exists: await exists5(changePath),
|
|
2120
|
-
hasProposal: await exists5(
|
|
2330
|
+
hasProposal: await exists5(join15(changePath, "proposal.md")),
|
|
2121
2331
|
hasTasks: await exists5(tasksPath),
|
|
2122
2332
|
hasSpecs: await exists5(specsPath),
|
|
2123
2333
|
tasksPath,
|
|
@@ -2143,9 +2353,9 @@ async function exists5(path) {
|
|
|
2143
2353
|
}
|
|
2144
2354
|
|
|
2145
2355
|
// src/openspec/resolver.ts
|
|
2146
|
-
import { execFile as
|
|
2147
|
-
import { promisify as
|
|
2148
|
-
var
|
|
2356
|
+
import { execFile as execFile3 } from "child_process";
|
|
2357
|
+
import { promisify as promisify3 } from "util";
|
|
2358
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
2149
2359
|
async function resolveOpenSpecExecutable() {
|
|
2150
2360
|
const executablePath = await findExecutable();
|
|
2151
2361
|
const version = await readVersion(executablePath);
|
|
@@ -2192,7 +2402,7 @@ async function readVersion(executablePath) {
|
|
|
2192
2402
|
}
|
|
2193
2403
|
}
|
|
2194
2404
|
function exec(command, args) {
|
|
2195
|
-
return
|
|
2405
|
+
return execFileAsync3(command, args, { shell: process.platform === "win32" });
|
|
2196
2406
|
}
|
|
2197
2407
|
|
|
2198
2408
|
// src/openspec/runner.ts
|
|
@@ -2238,7 +2448,7 @@ async function runOpenSpec(executablePath, command, args, options) {
|
|
|
2238
2448
|
}
|
|
2239
2449
|
|
|
2240
2450
|
// src/openspec/adapter.ts
|
|
2241
|
-
var
|
|
2451
|
+
var execFileAsync4 = promisify4(execFile4);
|
|
2242
2452
|
var DefaultOpenSpecAdapter = class {
|
|
2243
2453
|
identity;
|
|
2244
2454
|
async resolveExecutable() {
|
|
@@ -2250,7 +2460,7 @@ var DefaultOpenSpecAdapter = class {
|
|
|
2250
2460
|
const executable = identity.executablePath === "npx openspec" ? "npx" : identity.executablePath;
|
|
2251
2461
|
const args = identity.executablePath === "npx openspec" ? ["openspec", "--help"] : ["--help"];
|
|
2252
2462
|
try {
|
|
2253
|
-
const { stdout } = await
|
|
2463
|
+
const { stdout } = await execFileAsync4(executable, args, { shell: process.platform === "win32" });
|
|
2254
2464
|
return {
|
|
2255
2465
|
version: identity.version,
|
|
2256
2466
|
commands: parseCommands(stdout),
|
|
@@ -2295,11 +2505,11 @@ function parseCommands(help) {
|
|
|
2295
2505
|
|
|
2296
2506
|
// src/scanner/package.ts
|
|
2297
2507
|
import { readFile as readFile14, stat as stat8 } from "fs/promises";
|
|
2298
|
-
import { join as
|
|
2508
|
+
import { join as join16 } from "path";
|
|
2299
2509
|
import { parse as parse2 } from "yaml";
|
|
2300
2510
|
async function readPackageJson(projectRoot) {
|
|
2301
2511
|
try {
|
|
2302
|
-
return JSON.parse(await readFile14(
|
|
2512
|
+
return JSON.parse(await readFile14(join16(projectRoot, "package.json"), "utf8"));
|
|
2303
2513
|
} catch {
|
|
2304
2514
|
return null;
|
|
2305
2515
|
}
|
|
@@ -2365,7 +2575,7 @@ function detectFramework(pkg) {
|
|
|
2365
2575
|
}
|
|
2366
2576
|
async function detectLanguage(projectRoot, pkg) {
|
|
2367
2577
|
const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
|
|
2368
|
-
if (deps.typescript || await exists6(
|
|
2578
|
+
if (deps.typescript || await exists6(join16(projectRoot, "tsconfig.json"))) {
|
|
2369
2579
|
return "typescript";
|
|
2370
2580
|
}
|
|
2371
2581
|
return "javascript";
|
|
@@ -2380,7 +2590,7 @@ async function detectWorkspaces(projectRoot, pkg) {
|
|
|
2380
2590
|
return packageWorkspaces;
|
|
2381
2591
|
}
|
|
2382
2592
|
try {
|
|
2383
|
-
const workspace = parse2(await readFile14(
|
|
2593
|
+
const workspace = parse2(await readFile14(join16(projectRoot, "pnpm-workspace.yaml"), "utf8"));
|
|
2384
2594
|
return (workspace?.packages ?? []).map((path) => ({
|
|
2385
2595
|
name: path,
|
|
2386
2596
|
path,
|
|
@@ -2400,7 +2610,7 @@ async function detectLockManagers(projectRoot) {
|
|
|
2400
2610
|
];
|
|
2401
2611
|
const found = [];
|
|
2402
2612
|
for (const [file, manager] of lockFiles) {
|
|
2403
|
-
if (await exists6(
|
|
2613
|
+
if (await exists6(join16(projectRoot, file))) {
|
|
2404
2614
|
found.push(manager);
|
|
2405
2615
|
}
|
|
2406
2616
|
}
|
|
@@ -2426,12 +2636,12 @@ async function exists6(path) {
|
|
|
2426
2636
|
|
|
2427
2637
|
// src/scanner/routes.ts
|
|
2428
2638
|
import { readdir as readdir2, stat as stat9 } from "fs/promises";
|
|
2429
|
-
import { join as
|
|
2639
|
+
import { join as join17, relative, sep } from "path";
|
|
2430
2640
|
async function scanRoutes(projectRoot) {
|
|
2431
2641
|
const candidates = ["src/routes", "src/pages", "app", "pages"];
|
|
2432
2642
|
const routes = [];
|
|
2433
2643
|
for (const candidate of candidates) {
|
|
2434
|
-
const root =
|
|
2644
|
+
const root = join17(projectRoot, candidate);
|
|
2435
2645
|
if (!await exists7(root)) {
|
|
2436
2646
|
continue;
|
|
2437
2647
|
}
|
|
@@ -2459,7 +2669,7 @@ async function listFiles(root) {
|
|
|
2459
2669
|
const entries = await readdir2(root, { withFileTypes: true });
|
|
2460
2670
|
const files = [];
|
|
2461
2671
|
for (const entry of entries) {
|
|
2462
|
-
const path =
|
|
2672
|
+
const path = join17(root, entry.name);
|
|
2463
2673
|
if (entry.isDirectory()) {
|
|
2464
2674
|
files.push(...await listFiles(path));
|
|
2465
2675
|
} else {
|
|
@@ -2523,6 +2733,11 @@ var OutputWriter = class {
|
|
|
2523
2733
|
}
|
|
2524
2734
|
}
|
|
2525
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
|
+
}
|
|
2526
2741
|
if (!this.json) {
|
|
2527
2742
|
process.stderr.write(`\u8B66\u544A\uFF1A${message}${formatDetails(details)}
|
|
2528
2743
|
`);
|
|
@@ -2633,6 +2848,8 @@ function wrap(command, handler) {
|
|
|
2633
2848
|
const opts = isCommandLike(maybeCommand) ? { ...maybeCommand.parent?.opts(), ...maybeCommand.opts() } : program.opts();
|
|
2634
2849
|
const ctx = await createCommandContext(command, { ...opts, ...extractGlobalOptions(args) });
|
|
2635
2850
|
try {
|
|
2851
|
+
await confirmModelPolicyRecommendation(ctx);
|
|
2852
|
+
await warnIfContextPlaceholdersRemain(ctx);
|
|
2636
2853
|
await handler(ctx, ...args);
|
|
2637
2854
|
} catch (error) {
|
|
2638
2855
|
const fetError = toFetError(error);
|
|
@@ -2641,6 +2858,40 @@ function wrap(command, handler) {
|
|
|
2641
2858
|
}
|
|
2642
2859
|
};
|
|
2643
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
|
+
}
|
|
2644
2895
|
function isCommandLike(value) {
|
|
2645
2896
|
return value instanceof Command;
|
|
2646
2897
|
}
|