@nick848/fet 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +540 -186
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -179,7 +179,12 @@ function toGitNexusState(detection, previous) {
|
|
|
179
179
|
setupHandoffPath: previous?.setupHandoffPath ?? null,
|
|
180
180
|
setupHandoffUpdatedAt: previous?.setupHandoffUpdatedAt ?? null,
|
|
181
181
|
handoffPath: previous?.handoffPath ?? null,
|
|
182
|
-
handoffUpdatedAt: previous?.handoffUpdatedAt ?? null
|
|
182
|
+
handoffUpdatedAt: previous?.handoffUpdatedAt ?? null,
|
|
183
|
+
projectContextPath: previous?.projectContextPath ?? null,
|
|
184
|
+
projectContextUpdatedAt: previous?.projectContextUpdatedAt ?? null,
|
|
185
|
+
workflowContextPath: previous?.workflowContextPath ?? null,
|
|
186
|
+
workflowContextUpdatedAt: previous?.workflowContextUpdatedAt ?? null,
|
|
187
|
+
lastWorkflowGraphQuery: previous?.lastWorkflowGraphQuery ?? null
|
|
183
188
|
};
|
|
184
189
|
}
|
|
185
190
|
async function inspectGitNexusGraph(projectRoot, env = process.env) {
|
|
@@ -248,7 +253,8 @@ function resolveGitNexusCommand(env) {
|
|
|
248
253
|
const raw = env.FET_GITNEXUS_COMMAND?.trim() || env.FET_GITNEXUS_EXECUTABLE?.trim() || "gitnexus";
|
|
249
254
|
const parts = splitCommand(raw);
|
|
250
255
|
const [file = "gitnexus", ...args] = parts;
|
|
251
|
-
|
|
256
|
+
const resolvedFile = process.platform === "win32" && raw === "gitnexus" ? "gitnexus.cmd" : file;
|
|
257
|
+
return { file: resolvedFile, args, label: raw };
|
|
252
258
|
}
|
|
253
259
|
function splitCommand(value) {
|
|
254
260
|
const matches = value.match(/"[^"]+"|'[^']+'|\S+/g);
|
|
@@ -442,8 +448,313 @@ FET:END -->
|
|
|
442
448
|
}
|
|
443
449
|
|
|
444
450
|
// src/commands/graph.ts
|
|
445
|
-
import { mkdir as
|
|
451
|
+
import { mkdir as mkdir5 } from "fs/promises";
|
|
452
|
+
import { dirname as dirname6, join as join9 } from "path";
|
|
453
|
+
|
|
454
|
+
// src/graph-context.ts
|
|
455
|
+
import { mkdir as mkdir4, readdir, readFile as readFile5 } from "fs/promises";
|
|
446
456
|
import { dirname as dirname5, join as join8 } from "path";
|
|
457
|
+
var MAX_SOURCE_CONTEXT = 8e3;
|
|
458
|
+
var MAX_GRAPH_OUTPUT = 2e4;
|
|
459
|
+
async function buildProjectGraphContext(ctx, state, trigger) {
|
|
460
|
+
if (!isGraphReadable(state)) {
|
|
461
|
+
return {
|
|
462
|
+
generated: false,
|
|
463
|
+
path: null,
|
|
464
|
+
query: null,
|
|
465
|
+
warnings: ["GitNexus graph exists check did not pass; project graph context was not generated."]
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
const query = "FET OpenSpec workflow architecture commands adapters graph integration project structure";
|
|
469
|
+
const goal = "Summarize the repository modules, workflow entry points, and likely insertion points for future FET/OpenSpec work.";
|
|
470
|
+
const graphQuery = await runGitNexus(["query", query, "--goal", goal, "--limit", "8"], { cwd: ctx.projectRoot });
|
|
471
|
+
const status = await runGitNexus(["status"], { cwd: ctx.projectRoot });
|
|
472
|
+
const warnings = commandWarnings([["gitnexus query", graphQuery], ["gitnexus status", status]]);
|
|
473
|
+
const relativePath = ".fet/graph-context/project.md";
|
|
474
|
+
await writeGraphContext(
|
|
475
|
+
join8(ctx.projectRoot, relativePath),
|
|
476
|
+
renderProjectContext({
|
|
477
|
+
trigger,
|
|
478
|
+
state,
|
|
479
|
+
query,
|
|
480
|
+
goal,
|
|
481
|
+
status: commandText(status),
|
|
482
|
+
graphOutput: commandText(graphQuery),
|
|
483
|
+
warnings
|
|
484
|
+
})
|
|
485
|
+
);
|
|
486
|
+
return {
|
|
487
|
+
generated: true,
|
|
488
|
+
path: relativePath,
|
|
489
|
+
query,
|
|
490
|
+
warnings
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
async function buildWorkflowGraphContext(ctx, options) {
|
|
494
|
+
const existing = await ctx.stateStore.getOrCreateGlobal();
|
|
495
|
+
if (!existing.graph?.gitnexus?.graphExists) {
|
|
496
|
+
return {
|
|
497
|
+
generated: false,
|
|
498
|
+
path: null,
|
|
499
|
+
query: null,
|
|
500
|
+
warnings: []
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const state = await refreshGitNexusState(ctx);
|
|
504
|
+
if (!isGraphReadable(state)) {
|
|
505
|
+
return {
|
|
506
|
+
generated: false,
|
|
507
|
+
path: null,
|
|
508
|
+
query: null,
|
|
509
|
+
warnings: []
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
const sourceContext = await collectOpenSpecContext(ctx.projectRoot, options.changeId);
|
|
513
|
+
const query = buildWorkflowQuery(options, sourceContext);
|
|
514
|
+
const goal = workflowGoal(options.command);
|
|
515
|
+
const graphQuery = await runGitNexus(["query", query, "--goal", goal, "--limit", "8"], { cwd: ctx.projectRoot });
|
|
516
|
+
const detectChanges = shouldDetectChanges(options.command) ? await runGitNexus(["detect-changes", "--scope", "all"], { cwd: ctx.projectRoot }) : null;
|
|
517
|
+
const warnings = commandWarnings([
|
|
518
|
+
["gitnexus query", graphQuery],
|
|
519
|
+
...detectChanges ? [["gitnexus detect-changes", detectChanges]] : []
|
|
520
|
+
]);
|
|
521
|
+
const relativePath = `.fet/graph-context/${sanitizePathPart(options.changeId ?? options.command)}.md`;
|
|
522
|
+
await writeGraphContext(
|
|
523
|
+
join8(ctx.projectRoot, relativePath),
|
|
524
|
+
renderWorkflowContext({
|
|
525
|
+
state,
|
|
526
|
+
command: options.command,
|
|
527
|
+
args: options.args,
|
|
528
|
+
changeId: options.changeId,
|
|
529
|
+
query,
|
|
530
|
+
goal,
|
|
531
|
+
sourceContext,
|
|
532
|
+
graphOutput: commandText(graphQuery),
|
|
533
|
+
detectChanges: detectChanges ? commandText(detectChanges) : null,
|
|
534
|
+
warnings
|
|
535
|
+
})
|
|
536
|
+
);
|
|
537
|
+
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
538
|
+
global.graph ??= {};
|
|
539
|
+
global.graph.gitnexus = {
|
|
540
|
+
...state,
|
|
541
|
+
workflowContextPath: relativePath,
|
|
542
|
+
workflowContextUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
543
|
+
lastWorkflowGraphQuery: query
|
|
544
|
+
};
|
|
545
|
+
await ctx.stateStore.writeGlobal(global);
|
|
546
|
+
return {
|
|
547
|
+
generated: true,
|
|
548
|
+
path: relativePath,
|
|
549
|
+
query,
|
|
550
|
+
warnings
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
async function refreshGitNexusState(ctx) {
|
|
554
|
+
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
555
|
+
global.graph ??= {};
|
|
556
|
+
const detection = await detectGitNexus();
|
|
557
|
+
const graph2 = await inspectGitNexusGraph(ctx.projectRoot);
|
|
558
|
+
const state = mergeGitNexusGraphInfo(toGitNexusState(detection, global.graph.gitnexus), graph2);
|
|
559
|
+
global.graph.gitnexus = state;
|
|
560
|
+
await ctx.stateStore.writeGlobal(global);
|
|
561
|
+
return state;
|
|
562
|
+
}
|
|
563
|
+
function isGraphReadable(state) {
|
|
564
|
+
return Boolean(state.installed && state.graphExists);
|
|
565
|
+
}
|
|
566
|
+
async function writeGraphContext(path, content) {
|
|
567
|
+
await mkdir4(dirname5(path), { recursive: true });
|
|
568
|
+
await atomicWrite(path, content);
|
|
569
|
+
}
|
|
570
|
+
function renderProjectContext(options) {
|
|
571
|
+
return `<!-- FET:MANAGED
|
|
572
|
+
schemaVersion: 1
|
|
573
|
+
generator: graph-context
|
|
574
|
+
scope: project
|
|
575
|
+
FET:END -->
|
|
576
|
+
|
|
577
|
+
# FET GitNexus Project Graph Context
|
|
578
|
+
|
|
579
|
+
Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
580
|
+
Trigger: ${options.trigger}
|
|
581
|
+
|
|
582
|
+
Use this file before broad repository scans in FET/OpenSpec work. Treat it as graph-derived context: confirm concrete behavior by reading the source files it points to.
|
|
583
|
+
|
|
584
|
+
## Graph State
|
|
585
|
+
|
|
586
|
+
- Provider: GitNexus
|
|
587
|
+
- Installed: ${options.state.installed ? "yes" : "no"}
|
|
588
|
+
- Version: ${options.state.version ?? "unknown"}
|
|
589
|
+
- Graph path: ${options.state.graphPath ?? ".gitnexus"}
|
|
590
|
+
- Graph exists: ${options.state.graphExists ? "yes" : "no"}
|
|
591
|
+
- Last indexed at: ${options.state.lastIndexedAt ?? "unknown"}
|
|
592
|
+
|
|
593
|
+
## GitNexus Status
|
|
594
|
+
|
|
595
|
+
\`\`\`text
|
|
596
|
+
${clip(options.status, MAX_GRAPH_OUTPUT)}
|
|
597
|
+
\`\`\`
|
|
598
|
+
|
|
599
|
+
## Project Query
|
|
600
|
+
|
|
601
|
+
- Query: ${options.query}
|
|
602
|
+
- Goal: ${options.goal}
|
|
603
|
+
|
|
604
|
+
\`\`\`text
|
|
605
|
+
${clip(options.graphOutput, MAX_GRAPH_OUTPUT)}
|
|
606
|
+
\`\`\`
|
|
607
|
+
${renderWarnings(options.warnings)}
|
|
608
|
+
`;
|
|
609
|
+
}
|
|
610
|
+
function renderWorkflowContext(options) {
|
|
611
|
+
return `<!-- FET:MANAGED
|
|
612
|
+
schemaVersion: 1
|
|
613
|
+
generator: graph-context
|
|
614
|
+
scope: workflow
|
|
615
|
+
FET:END -->
|
|
616
|
+
|
|
617
|
+
# FET GitNexus Workflow Graph Context
|
|
618
|
+
|
|
619
|
+
Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
620
|
+
|
|
621
|
+
Read this before editing or generating OpenSpec artifacts for this workflow. Use the graph output to narrow likely files, symbols, dependencies, and impact areas. If it conflicts with OpenSpec artifacts or source code, OpenSpec artifacts and source code win.
|
|
622
|
+
|
|
623
|
+
## Workflow
|
|
624
|
+
|
|
625
|
+
- FET command: ${options.command}
|
|
626
|
+
- Args: ${options.args.length ? options.args.join(" ") : "(none)"}
|
|
627
|
+
- Change: ${options.changeId ?? "(none)"}
|
|
628
|
+
- Graph path: ${options.state.graphPath ?? ".gitnexus"}
|
|
629
|
+
- Last indexed at: ${options.state.lastIndexedAt ?? "unknown"}
|
|
630
|
+
|
|
631
|
+
## OpenSpec Context Used For Query
|
|
632
|
+
|
|
633
|
+
\`\`\`text
|
|
634
|
+
${clip(options.sourceContext || "(no change artifacts found yet)", MAX_SOURCE_CONTEXT)}
|
|
635
|
+
\`\`\`
|
|
636
|
+
|
|
637
|
+
## GitNexus Query
|
|
638
|
+
|
|
639
|
+
- Query: ${options.query}
|
|
640
|
+
- Goal: ${options.goal}
|
|
641
|
+
|
|
642
|
+
\`\`\`text
|
|
643
|
+
${clip(options.graphOutput, MAX_GRAPH_OUTPUT)}
|
|
644
|
+
\`\`\`
|
|
645
|
+
${options.detectChanges ? `
|
|
646
|
+
## GitNexus Change Impact
|
|
647
|
+
|
|
648
|
+
\`\`\`text
|
|
649
|
+
${clip(options.detectChanges, MAX_GRAPH_OUTPUT)}
|
|
650
|
+
\`\`\`
|
|
651
|
+
` : ""}
|
|
652
|
+
${renderWarnings(options.warnings)}
|
|
653
|
+
`;
|
|
654
|
+
}
|
|
655
|
+
function renderWarnings(warnings) {
|
|
656
|
+
if (!warnings.length) {
|
|
657
|
+
return "";
|
|
658
|
+
}
|
|
659
|
+
return `
|
|
660
|
+
## Warnings
|
|
661
|
+
|
|
662
|
+
${warnings.map((warning) => `- ${warning}`).join("\n")}
|
|
663
|
+
`;
|
|
664
|
+
}
|
|
665
|
+
async function collectOpenSpecContext(projectRoot, changeId) {
|
|
666
|
+
if (!changeId) {
|
|
667
|
+
return "";
|
|
668
|
+
}
|
|
669
|
+
const changeRoot = join8(projectRoot, "openspec", "changes", changeId);
|
|
670
|
+
const chunks = [];
|
|
671
|
+
for (const file of ["proposal.md", "design.md", "tasks.md", "README.md"]) {
|
|
672
|
+
const content = await readOptional(join8(changeRoot, file));
|
|
673
|
+
if (content) {
|
|
674
|
+
chunks.push(`## ${file}
|
|
675
|
+
${content}`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
const specsRoot = join8(changeRoot, "specs");
|
|
679
|
+
for (const spec of await listSpecFiles(specsRoot)) {
|
|
680
|
+
const content = await readOptional(spec.path);
|
|
681
|
+
if (content) {
|
|
682
|
+
chunks.push(`## ${spec.label}
|
|
683
|
+
${content}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return clip(chunks.join("\n\n"), MAX_SOURCE_CONTEXT);
|
|
687
|
+
}
|
|
688
|
+
async function listSpecFiles(specsRoot) {
|
|
689
|
+
try {
|
|
690
|
+
const capabilities = await readdir(specsRoot, { withFileTypes: true });
|
|
691
|
+
return capabilities.filter((entry) => entry.isDirectory()).map((entry) => ({
|
|
692
|
+
path: join8(specsRoot, entry.name, "spec.md"),
|
|
693
|
+
label: `specs/${entry.name}/spec.md`
|
|
694
|
+
}));
|
|
695
|
+
} catch {
|
|
696
|
+
return [];
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
async function readOptional(path) {
|
|
700
|
+
try {
|
|
701
|
+
return await readFile5(path, "utf8");
|
|
702
|
+
} catch {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
function buildWorkflowQuery(options, sourceContext) {
|
|
707
|
+
const artifactTerms = normalizeWhitespace(sourceContext).slice(0, 1200);
|
|
708
|
+
return normalizeWhitespace(
|
|
709
|
+
[
|
|
710
|
+
`FET OpenSpec ${options.command}`,
|
|
711
|
+
options.changeId ? `change ${options.changeId}` : "",
|
|
712
|
+
options.args.join(" "),
|
|
713
|
+
artifactTerms
|
|
714
|
+
].join(" ")
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
function workflowGoal(command) {
|
|
718
|
+
if (command === "apply") {
|
|
719
|
+
return "Find implementation files, symbols, dependencies, and likely blast radius for the current OpenSpec tasks.";
|
|
720
|
+
}
|
|
721
|
+
if (command === "verify") {
|
|
722
|
+
return "Find affected flows and source areas that should be checked while verifying this OpenSpec change.";
|
|
723
|
+
}
|
|
724
|
+
if (["explore", "propose", "new", "continue", "ff"].includes(command)) {
|
|
725
|
+
return "Find relevant modules, entry points, and existing behavior to make OpenSpec artifacts precise.";
|
|
726
|
+
}
|
|
727
|
+
return "Find relevant repository context for this FET/OpenSpec workflow command.";
|
|
728
|
+
}
|
|
729
|
+
function shouldDetectChanges(command) {
|
|
730
|
+
return ["apply", "verify", "sync"].includes(command);
|
|
731
|
+
}
|
|
732
|
+
function commandText(result) {
|
|
733
|
+
const output = [result.stdout.trim(), result.stderr.trim()].filter(Boolean).join("\n");
|
|
734
|
+
return output || `exit ${result.exitCode}`;
|
|
735
|
+
}
|
|
736
|
+
function commandWarnings(results) {
|
|
737
|
+
return results.filter(([, result]) => result.exitCode !== 0).map(([label, result]) => `${label} exited with ${result.exitCode}: ${firstLine(commandText(result))}`);
|
|
738
|
+
}
|
|
739
|
+
function firstLine(value) {
|
|
740
|
+
return value.trim().split(/\r?\n/)[0]?.trim() || "no output";
|
|
741
|
+
}
|
|
742
|
+
function clip(value, max) {
|
|
743
|
+
if (value.length <= max) {
|
|
744
|
+
return value;
|
|
745
|
+
}
|
|
746
|
+
return `${value.slice(0, max)}
|
|
747
|
+
|
|
748
|
+
[truncated ${value.length - max} characters]`;
|
|
749
|
+
}
|
|
750
|
+
function normalizeWhitespace(value) {
|
|
751
|
+
return value.replace(/\s+/g, " ").trim();
|
|
752
|
+
}
|
|
753
|
+
function sanitizePathPart(value) {
|
|
754
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "workflow";
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/commands/graph.ts
|
|
447
758
|
async function graphCommand(ctx, action, args = []) {
|
|
448
759
|
switch (action) {
|
|
449
760
|
case "status":
|
|
@@ -498,7 +809,7 @@ async function graphDoctorCommand(ctx) {
|
|
|
498
809
|
}
|
|
499
810
|
async function graphSetupCommand(ctx) {
|
|
500
811
|
let result;
|
|
501
|
-
const handoffPath =
|
|
812
|
+
const handoffPath = join9(ctx.projectRoot, ".fet", "graph-setup.md");
|
|
502
813
|
const installCommand = process.env.FET_GITNEXUS_INSTALL_COMMAND?.trim() || null;
|
|
503
814
|
await withProjectLock(ctx.projectRoot, { command: "graph setup", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
504
815
|
result = await refreshGraphState(ctx, { write: false });
|
|
@@ -534,7 +845,7 @@ async function graphSetupCommand(ctx) {
|
|
|
534
845
|
}
|
|
535
846
|
async function graphHandoffCommand(ctx) {
|
|
536
847
|
let result;
|
|
537
|
-
const handoffPath =
|
|
848
|
+
const handoffPath = join9(ctx.projectRoot, ".fet", "graph-handoff.md");
|
|
538
849
|
await withProjectLock(ctx.projectRoot, { command: "graph handoff", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
539
850
|
result = await refreshGraphState(ctx, { runStatus: true, write: false });
|
|
540
851
|
await writeHandoffFile(handoffPath, renderGraphUsageHandoff(result.state, ctx.language));
|
|
@@ -581,11 +892,14 @@ async function graphAnalyzeCommand(ctx, mode, args) {
|
|
|
581
892
|
});
|
|
582
893
|
}
|
|
583
894
|
const result = await refreshGraphState(ctx, { write: false });
|
|
895
|
+
const graphContext = await buildProjectGraphContext(ctx, result.state, `fet graph ${mode}`);
|
|
584
896
|
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
585
897
|
global.graph ??= {};
|
|
586
898
|
global.graph.gitnexus = {
|
|
587
899
|
...result.state,
|
|
588
|
-
lastRefreshAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
900
|
+
lastRefreshAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
901
|
+
projectContextPath: graphContext.path,
|
|
902
|
+
projectContextUpdatedAt: graphContext.generated ? (/* @__PURE__ */ new Date()).toISOString() : result.state.projectContextUpdatedAt ?? null
|
|
589
903
|
};
|
|
590
904
|
await ctx.stateStore.writeGlobal(global);
|
|
591
905
|
ctx.output.result({
|
|
@@ -595,9 +909,10 @@ async function graphAnalyzeCommand(ctx, mode, args) {
|
|
|
595
909
|
warnings: result.state.graphExists ? [] : [
|
|
596
910
|
ctx.language === "en" ? "GitNexus analyze completed, but the configured graph directory was not found." : "GitNexus analyze \u5DF2\u5B8C\u6210\uFF0C\u4F46\u672A\u53D1\u73B0\u914D\u7F6E\u7684\u4EE3\u7801\u56FE\u76EE\u5F55\u3002"
|
|
597
911
|
],
|
|
598
|
-
nextSteps: ctx.language === "en" ? ["Run fet graph status", "Use .fet/graph-handoff.md or generated IDE prompts to prefer graph context"] : ["\u8FD0\u884C fet graph status", "\u4F7F\u7528 .fet/graph-handoff.md \u6216\u751F\u6210\u7684 IDE \u63D0\u793A\uFF0C\u4F18\u5148\u53C2\u8003\u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587"],
|
|
912
|
+
nextSteps: ctx.language === "en" ? ["Run fet graph status", "Read .fet/graph-context/project.md before broad scans", "Use .fet/graph-handoff.md or generated IDE prompts to prefer graph context"] : ["\u8FD0\u884C fet graph status", "\u4F7F\u7528 .fet/graph-handoff.md \u6216\u751F\u6210\u7684 IDE \u63D0\u793A\uFF0C\u4F18\u5148\u53C2\u8003\u4EE3\u7801\u56FE\u4E0A\u4E0B\u6587"],
|
|
599
913
|
data: {
|
|
600
914
|
gitnexus: global.graph.gitnexus,
|
|
915
|
+
graphContext,
|
|
601
916
|
run: {
|
|
602
917
|
command: run.command,
|
|
603
918
|
stdout: run.stdout.trim(),
|
|
@@ -617,7 +932,7 @@ async function refreshGraphState(ctx, options = {}) {
|
|
|
617
932
|
gitnexusStatus = await runGitNexus(["status"], { cwd: ctx.projectRoot });
|
|
618
933
|
state = {
|
|
619
934
|
...state,
|
|
620
|
-
lastStatus:
|
|
935
|
+
lastStatus: firstLine2(gitnexusStatus.stdout) || firstLine2(gitnexusStatus.stderr) || `exit ${gitnexusStatus.exitCode}`
|
|
621
936
|
};
|
|
622
937
|
}
|
|
623
938
|
if (options.write ?? true) {
|
|
@@ -635,7 +950,7 @@ async function refreshGraphState(ctx, options = {}) {
|
|
|
635
950
|
};
|
|
636
951
|
}
|
|
637
952
|
async function writeHandoffFile(path, content) {
|
|
638
|
-
await
|
|
953
|
+
await mkdir5(dirname6(path), { recursive: true });
|
|
639
954
|
await atomicWrite(path, content);
|
|
640
955
|
}
|
|
641
956
|
function renderGraphSetupHandoff(state, options) {
|
|
@@ -780,24 +1095,24 @@ FET:END -->
|
|
|
780
1095
|
- \u6240\u6709\u751F\u6210\u4EA7\u7269\u4ECD\u5199\u5165\u6B63\u5E38\u7684 OpenSpec change \u76EE\u5F55\u3002
|
|
781
1096
|
`;
|
|
782
1097
|
}
|
|
783
|
-
function
|
|
1098
|
+
function firstLine2(value) {
|
|
784
1099
|
return value.trim().split(/\r?\n/)[0]?.trim() || null;
|
|
785
1100
|
}
|
|
786
1101
|
|
|
787
1102
|
// src/commands/init.ts
|
|
788
|
-
import { readFile as
|
|
789
|
-
import { join as
|
|
1103
|
+
import { readFile as readFile8, stat as stat4 } from "fs/promises";
|
|
1104
|
+
import { join as join12 } from "path";
|
|
790
1105
|
|
|
791
1106
|
// src/version.ts
|
|
792
1107
|
import { existsSync, readFileSync } from "fs";
|
|
793
|
-
import { dirname as
|
|
1108
|
+
import { dirname as dirname7, join as join10, parse } from "path";
|
|
794
1109
|
import { fileURLToPath } from "url";
|
|
795
1110
|
var FET_VERSION = readPackageVersion();
|
|
796
1111
|
function readPackageVersion() {
|
|
797
|
-
let currentDir =
|
|
1112
|
+
let currentDir = dirname7(fileURLToPath(import.meta.url));
|
|
798
1113
|
const root = parse(currentDir).root;
|
|
799
1114
|
while (true) {
|
|
800
|
-
const packageJsonPath =
|
|
1115
|
+
const packageJsonPath = join10(currentDir, "package.json");
|
|
801
1116
|
if (existsSync(packageJsonPath)) {
|
|
802
1117
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
803
1118
|
if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
|
|
@@ -808,7 +1123,7 @@ function readPackageVersion() {
|
|
|
808
1123
|
if (currentDir === root) {
|
|
809
1124
|
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D FET package.json");
|
|
810
1125
|
}
|
|
811
|
-
currentDir =
|
|
1126
|
+
currentDir = dirname7(currentDir);
|
|
812
1127
|
}
|
|
813
1128
|
}
|
|
814
1129
|
|
|
@@ -1259,18 +1574,18 @@ ${block}
|
|
|
1259
1574
|
}
|
|
1260
1575
|
|
|
1261
1576
|
// src/commands/update-context.ts
|
|
1262
|
-
import { readFile as
|
|
1263
|
-
import { join as
|
|
1577
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1578
|
+
import { join as join11 } from "path";
|
|
1264
1579
|
|
|
1265
1580
|
// src/config/yaml.ts
|
|
1266
|
-
import { readFile as
|
|
1581
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1267
1582
|
import { parseDocument } from "yaml";
|
|
1268
1583
|
async function mergeFetConfig(configPath, renderedFetYaml) {
|
|
1269
1584
|
const fetDoc = parseDocument(renderedFetYaml);
|
|
1270
1585
|
const nextFet = fetDoc.get("fet", true);
|
|
1271
1586
|
let existing = "";
|
|
1272
1587
|
try {
|
|
1273
|
-
existing = await
|
|
1588
|
+
existing = await readFile6(configPath, "utf8");
|
|
1274
1589
|
} catch {
|
|
1275
1590
|
return renderedFetYaml;
|
|
1276
1591
|
}
|
|
@@ -1294,14 +1609,14 @@ async function updateContextCommand(ctx) {
|
|
|
1294
1609
|
}
|
|
1295
1610
|
async function updateContextFiles(ctx) {
|
|
1296
1611
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
1297
|
-
const agentsPath =
|
|
1298
|
-
const configPath =
|
|
1299
|
-
const claudePath =
|
|
1300
|
-
const karpathyHandoffPath =
|
|
1301
|
-
const karpathyCursorPath =
|
|
1302
|
-
const existingAgents = await
|
|
1303
|
-
const existingClaude = await
|
|
1304
|
-
const existingKarpathyCursor = await
|
|
1612
|
+
const agentsPath = join11(ctx.projectRoot, "AGENTS.md");
|
|
1613
|
+
const configPath = join11(ctx.projectRoot, "openspec", "config.yaml");
|
|
1614
|
+
const claudePath = join11(ctx.projectRoot, "CLAUDE.md");
|
|
1615
|
+
const karpathyHandoffPath = join11(ctx.projectRoot, ".fet", "karpathy-guidelines.md");
|
|
1616
|
+
const karpathyCursorPath = join11(ctx.projectRoot, ".cursor", "rules", "karpathy-guidelines.mdc");
|
|
1617
|
+
const existingAgents = await readOptional2(agentsPath);
|
|
1618
|
+
const existingClaude = await readOptional2(claudePath);
|
|
1619
|
+
const existingKarpathyCursor = await readOptional2(karpathyCursorPath);
|
|
1305
1620
|
const warnings = [...scan.warnings];
|
|
1306
1621
|
if (existingAgents && hasInvalidManagedAutoRegion(existingAgents)) {
|
|
1307
1622
|
throw new FetError({
|
|
@@ -1349,9 +1664,9 @@ async function updateContextFiles(ctx) {
|
|
|
1349
1664
|
await ctx.stateStore.writeGlobal(state);
|
|
1350
1665
|
return { warnings };
|
|
1351
1666
|
}
|
|
1352
|
-
async function
|
|
1667
|
+
async function readOptional2(path) {
|
|
1353
1668
|
try {
|
|
1354
|
-
return await
|
|
1669
|
+
return await readFile7(path, "utf8");
|
|
1355
1670
|
} catch {
|
|
1356
1671
|
return null;
|
|
1357
1672
|
}
|
|
@@ -1359,7 +1674,7 @@ async function readOptional(path) {
|
|
|
1359
1674
|
|
|
1360
1675
|
// src/commands/init.ts
|
|
1361
1676
|
async function initCommand(ctx) {
|
|
1362
|
-
const alreadyInitialized = await exists2(
|
|
1677
|
+
const alreadyInitialized = await exists2(join12(ctx.projectRoot, "openspec", "config.yaml"));
|
|
1363
1678
|
let warnings = [];
|
|
1364
1679
|
await withProjectLock(ctx.projectRoot, { command: "init", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
1365
1680
|
const journal = createInitJournal(ctx.fetVersion);
|
|
@@ -1411,13 +1726,13 @@ async function initCommand(ctx) {
|
|
|
1411
1726
|
});
|
|
1412
1727
|
}
|
|
1413
1728
|
async function ensureGitignore(ctx) {
|
|
1414
|
-
const gitignorePath =
|
|
1415
|
-
const existing = await
|
|
1729
|
+
const gitignorePath = join12(ctx.projectRoot, ".gitignore");
|
|
1730
|
+
const existing = await readOptional3(gitignorePath);
|
|
1416
1731
|
await atomicWrite(gitignorePath, mergeGitignore(existing));
|
|
1417
1732
|
}
|
|
1418
|
-
async function
|
|
1733
|
+
async function readOptional3(path) {
|
|
1419
1734
|
try {
|
|
1420
|
-
return await
|
|
1735
|
+
return await readFile8(path, "utf8");
|
|
1421
1736
|
} catch {
|
|
1422
1737
|
return null;
|
|
1423
1738
|
}
|
|
@@ -1432,8 +1747,8 @@ async function exists2(path) {
|
|
|
1432
1747
|
}
|
|
1433
1748
|
|
|
1434
1749
|
// src/commands/proxy.ts
|
|
1435
|
-
import { readFile as
|
|
1436
|
-
import { join as
|
|
1750
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
1751
|
+
import { join as join14 } from "path";
|
|
1437
1752
|
|
|
1438
1753
|
// src/state/project.ts
|
|
1439
1754
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -1462,8 +1777,8 @@ async function git(cwd, args) {
|
|
|
1462
1777
|
}
|
|
1463
1778
|
|
|
1464
1779
|
// src/state/store.ts
|
|
1465
|
-
import { mkdir as
|
|
1466
|
-
import { join as
|
|
1780
|
+
import { mkdir as mkdir6, readFile as readFile9 } from "fs/promises";
|
|
1781
|
+
import { join as join13 } from "path";
|
|
1467
1782
|
|
|
1468
1783
|
// src/language.ts
|
|
1469
1784
|
var DEFAULT_LANGUAGE = "zh-CN";
|
|
@@ -1581,7 +1896,7 @@ var StateStore = class {
|
|
|
1581
1896
|
project;
|
|
1582
1897
|
async readGlobal() {
|
|
1583
1898
|
try {
|
|
1584
|
-
const value = JSON.parse(await
|
|
1899
|
+
const value = JSON.parse(await readFile9(this.globalPath(), "utf8"));
|
|
1585
1900
|
assertGlobalState(value);
|
|
1586
1901
|
return value;
|
|
1587
1902
|
} catch (error) {
|
|
@@ -1596,13 +1911,13 @@ var StateStore = class {
|
|
|
1596
1911
|
}
|
|
1597
1912
|
async writeGlobal(state) {
|
|
1598
1913
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1599
|
-
await
|
|
1914
|
+
await mkdir6(join13(this.projectRoot, "openspec"), { recursive: true });
|
|
1600
1915
|
await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
|
|
1601
1916
|
`);
|
|
1602
1917
|
}
|
|
1603
1918
|
async readChange(changeId) {
|
|
1604
1919
|
try {
|
|
1605
|
-
const value = JSON.parse(await
|
|
1920
|
+
const value = JSON.parse(await readFile9(this.changePath(changeId), "utf8"));
|
|
1606
1921
|
assertChangeState(value);
|
|
1607
1922
|
return value;
|
|
1608
1923
|
} catch (error) {
|
|
@@ -1617,15 +1932,15 @@ var StateStore = class {
|
|
|
1617
1932
|
}
|
|
1618
1933
|
async writeChange(state) {
|
|
1619
1934
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1620
|
-
await
|
|
1935
|
+
await mkdir6(join13(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
|
|
1621
1936
|
await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
|
|
1622
1937
|
`);
|
|
1623
1938
|
}
|
|
1624
1939
|
globalPath() {
|
|
1625
|
-
return
|
|
1940
|
+
return join13(this.projectRoot, "openspec", "fet-state.json");
|
|
1626
1941
|
}
|
|
1627
1942
|
changePath(changeId) {
|
|
1628
|
-
return
|
|
1943
|
+
return join13(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
|
|
1629
1944
|
}
|
|
1630
1945
|
};
|
|
1631
1946
|
function isNotFound(error) {
|
|
@@ -1633,11 +1948,11 @@ function isNotFound(error) {
|
|
|
1633
1948
|
}
|
|
1634
1949
|
|
|
1635
1950
|
// src/state/tasks.ts
|
|
1636
|
-
import { readFile as
|
|
1951
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
1637
1952
|
async function readCompletedTaskIds(tasksPath) {
|
|
1638
1953
|
let content;
|
|
1639
1954
|
try {
|
|
1640
|
-
content = await
|
|
1955
|
+
content = await readFile10(tasksPath, "utf8");
|
|
1641
1956
|
} catch {
|
|
1642
1957
|
return [];
|
|
1643
1958
|
}
|
|
@@ -1668,71 +1983,94 @@ var phaseByCommand = {
|
|
|
1668
1983
|
};
|
|
1669
1984
|
async function proxyCommand(ctx, command, args) {
|
|
1670
1985
|
const openSpecArgs = stripFetOptions(args);
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
} else if (state.activeChangeId && !inspection.changes.includes(state.activeChangeId)) {
|
|
1705
|
-
state.activeChangeId = inspection.changes.length === 1 ? inspection.changes[0] ?? null : null;
|
|
1706
|
-
} else if (!state.activeChangeId && inspection.changes.length === 1) {
|
|
1707
|
-
state.activeChangeId = inspection.changes[0] ?? null;
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
await ctx.stateStore.writeGlobal(state);
|
|
1711
|
-
const changeId = ctx.changeId ?? state.activeChangeId;
|
|
1712
|
-
if (changeId && inspection.changes.includes(changeId)) {
|
|
1713
|
-
const changeInspection = await ctx.openSpec.inspectChange(ctx.projectRoot, changeId);
|
|
1714
|
-
const changeState = await ctx.stateStore.getOrCreateChange(changeId, phaseByCommand[command] ?? "propose");
|
|
1715
|
-
changeState.currentPhase = phaseByCommand[command] ?? changeState.currentPhase;
|
|
1716
|
-
changeState.phases[changeState.currentPhase] = {
|
|
1717
|
-
status: "done",
|
|
1718
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1719
|
-
};
|
|
1720
|
-
changeState.lastOpenSpecCommand = {
|
|
1721
|
-
command: mapped.command,
|
|
1722
|
-
args: mapped.args,
|
|
1723
|
-
exitCode: result.exitCode,
|
|
1724
|
-
ranAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1725
|
-
};
|
|
1726
|
-
changeState.tasks.completedIds = await readCompletedTaskIds(changeInspection.tasksPath);
|
|
1727
|
-
changeState.tasks.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1728
|
-
await ctx.stateStore.writeChange(changeState);
|
|
1986
|
+
const runState = {};
|
|
1987
|
+
await withProjectLock(ctx.projectRoot, { command, cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
1988
|
+
if (["sync", "archive", "bulk-archive"].includes(command)) {
|
|
1989
|
+
await assertVerified(ctx);
|
|
1990
|
+
}
|
|
1991
|
+
const mapped = await mapOpenSpecCommand(ctx, command, openSpecArgs);
|
|
1992
|
+
await assertOpenSpecCommandSupported(ctx, mapped.command, command);
|
|
1993
|
+
const mappedChangeId = extractChangeId(mapped.args);
|
|
1994
|
+
const targetChangeId = command === "archive" ? mapped.args[0] ?? ctx.changeId ?? mappedChangeId : ctx.changeId ?? mappedChangeId;
|
|
1995
|
+
runState.graphContext = await buildWorkflowGraphContext(ctx, {
|
|
1996
|
+
command,
|
|
1997
|
+
args: mapped.args,
|
|
1998
|
+
changeId: targetChangeId
|
|
1999
|
+
});
|
|
2000
|
+
const changelogEntry = command === "archive" && targetChangeId ? await createChangelogEntry(ctx.projectRoot, targetChangeId) : null;
|
|
2001
|
+
const result = await ctx.openSpec.run(mapped.command, mapped.args, { cwd: ctx.projectRoot, stdio: ctx.json ? "pipe" : "inherit" });
|
|
2002
|
+
if (result.exitCode !== 0) {
|
|
2003
|
+
throw new FetError({
|
|
2004
|
+
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
2005
|
+
message: `OpenSpec ${command} failed.`,
|
|
2006
|
+
details: result,
|
|
2007
|
+
recoverable: true
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
if (changelogEntry) {
|
|
2011
|
+
await appendChangelog(ctx.projectRoot, changelogEntry);
|
|
2012
|
+
}
|
|
2013
|
+
const inspection = await ctx.openSpec.inspectProject(ctx.projectRoot);
|
|
2014
|
+
const state = await ctx.stateStore.getOrCreateGlobal();
|
|
2015
|
+
state.openChangeIds = inspection.changes;
|
|
2016
|
+
if (command === "archive") {
|
|
2017
|
+
if (!state.activeChangeId || state.activeChangeId === targetChangeId || !inspection.changes.includes(state.activeChangeId)) {
|
|
2018
|
+
state.activeChangeId = null;
|
|
1729
2019
|
}
|
|
2020
|
+
state.verifyAuthorization = null;
|
|
2021
|
+
} else if (ctx.changeId && inspection.changes.includes(ctx.changeId)) {
|
|
2022
|
+
state.activeChangeId = ctx.changeId;
|
|
2023
|
+
} else if (state.activeChangeId && !inspection.changes.includes(state.activeChangeId)) {
|
|
2024
|
+
state.activeChangeId = inspection.changes.length === 1 ? inspection.changes[0] ?? null : null;
|
|
2025
|
+
} else if (!state.activeChangeId && inspection.changes.length === 1) {
|
|
2026
|
+
state.activeChangeId = inspection.changes[0] ?? null;
|
|
1730
2027
|
}
|
|
1731
|
-
|
|
2028
|
+
await ctx.stateStore.writeGlobal(state);
|
|
2029
|
+
const changeId = ctx.changeId ?? state.activeChangeId;
|
|
2030
|
+
if (changeId && inspection.changes.includes(changeId)) {
|
|
2031
|
+
const changeInspection = await ctx.openSpec.inspectChange(ctx.projectRoot, changeId);
|
|
2032
|
+
const changeState = await ctx.stateStore.getOrCreateChange(changeId, phaseByCommand[command] ?? "propose");
|
|
2033
|
+
changeState.currentPhase = phaseByCommand[command] ?? changeState.currentPhase;
|
|
2034
|
+
changeState.phases[changeState.currentPhase] = {
|
|
2035
|
+
status: "done",
|
|
2036
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2037
|
+
};
|
|
2038
|
+
changeState.lastOpenSpecCommand = {
|
|
2039
|
+
command: mapped.command,
|
|
2040
|
+
args: mapped.args,
|
|
2041
|
+
exitCode: result.exitCode,
|
|
2042
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2043
|
+
};
|
|
2044
|
+
changeState.tasks.completedIds = await readCompletedTaskIds(changeInspection.tasksPath);
|
|
2045
|
+
changeState.tasks.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2046
|
+
await ctx.stateStore.writeChange(changeState);
|
|
2047
|
+
}
|
|
2048
|
+
});
|
|
2049
|
+
const graphContext = runState.graphContext;
|
|
1732
2050
|
ctx.output.result({
|
|
1733
2051
|
ok: true,
|
|
1734
2052
|
command,
|
|
1735
|
-
summary: `fet ${command}
|
|
2053
|
+
summary: `fet ${command} completed.`,
|
|
2054
|
+
warnings: graphContext?.warnings,
|
|
2055
|
+
nextSteps: graphContext?.generated && graphContext.path ? [`Read ${graphContext.path} before broad source scans or implementation decisions.`] : void 0,
|
|
2056
|
+
data: graphContext ? { graphContext } : void 0
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
async function assertOpenSpecCommandSupported(ctx, openSpecCommand, fetCommand) {
|
|
2060
|
+
const capabilities = await ctx.openSpec.getCapabilities();
|
|
2061
|
+
if (capabilities.commands.includes(openSpecCommand)) {
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
throw new FetError({
|
|
2065
|
+
code: "OPENSPEC_UNSUPPORTED_VERSION" /* OpenSpecUnsupportedVersion */,
|
|
2066
|
+
message: `OpenSpec CLI ${capabilities.version} does not expose command "${openSpecCommand}" required by "fet ${fetCommand}". FET will not substitute another workflow command automatically.`,
|
|
2067
|
+
details: {
|
|
2068
|
+
openSpecVersion: capabilities.version,
|
|
2069
|
+
requiredCommand: openSpecCommand,
|
|
2070
|
+
availableCommands: capabilities.commands,
|
|
2071
|
+
supported: capabilities.supported
|
|
2072
|
+
},
|
|
2073
|
+
suggestedCommand: "Upgrade OpenSpec to a version that supports this command, then rerun FET. Try: npm install -g @fission-ai/openspec@latest && fet doctor. If your OpenSpec version intentionally removed this command, pause and choose a compatible FET workflow instead of running ff automatically."
|
|
1736
2074
|
});
|
|
1737
2075
|
}
|
|
1738
2076
|
async function createChangelogEntry(projectRoot, changeId) {
|
|
@@ -1742,10 +2080,12 @@ async function createChangelogEntry(projectRoot, changeId) {
|
|
|
1742
2080
|
};
|
|
1743
2081
|
}
|
|
1744
2082
|
async function appendChangelog(projectRoot, entry) {
|
|
1745
|
-
const changelogPath =
|
|
1746
|
-
const existing = await
|
|
2083
|
+
const changelogPath = join14(projectRoot, "CHANGELOG.md");
|
|
2084
|
+
const existing = await readOptional4(changelogPath);
|
|
2085
|
+
const legacyContentLabel = "\u66F4\u65B0\u5185\u5BB9";
|
|
1747
2086
|
const block = `updateTime: ${entry.updateTime}
|
|
1748
|
-
|
|
2087
|
+
changeRequirement:${entry.content}
|
|
2088
|
+
${legacyContentLabel}:${entry.content}
|
|
1749
2089
|
`;
|
|
1750
2090
|
const next = existing?.trimEnd() ? `${existing.trimEnd()}
|
|
1751
2091
|
|
|
@@ -1753,12 +2093,12 @@ ${block}` : block;
|
|
|
1753
2093
|
await atomicWrite(changelogPath, next);
|
|
1754
2094
|
}
|
|
1755
2095
|
async function readChangeRequirement(projectRoot, changeId) {
|
|
1756
|
-
const changeRoot =
|
|
1757
|
-
const proposal = await
|
|
2096
|
+
const changeRoot = join14(projectRoot, "openspec", "changes", changeId);
|
|
2097
|
+
const proposal = await readOptional4(join14(changeRoot, "proposal.md"));
|
|
1758
2098
|
if (proposal) {
|
|
1759
2099
|
return summarizeMarkdown(proposal);
|
|
1760
2100
|
}
|
|
1761
|
-
const readme = await
|
|
2101
|
+
const readme = await readOptional4(join14(changeRoot, "README.md"));
|
|
1762
2102
|
if (readme) {
|
|
1763
2103
|
return summarizeMarkdown(readme);
|
|
1764
2104
|
}
|
|
@@ -1766,11 +2106,11 @@ async function readChangeRequirement(projectRoot, changeId) {
|
|
|
1766
2106
|
}
|
|
1767
2107
|
function summarizeMarkdown(content) {
|
|
1768
2108
|
const normalized = content.replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("<!--") && !line.startsWith("---")).join(" ");
|
|
1769
|
-
return normalized || "
|
|
2109
|
+
return normalized || "No change requirement found.";
|
|
1770
2110
|
}
|
|
1771
|
-
async function
|
|
2111
|
+
async function readOptional4(path) {
|
|
1772
2112
|
try {
|
|
1773
|
-
return await
|
|
2113
|
+
return await readFile11(path, "utf8");
|
|
1774
2114
|
} catch {
|
|
1775
2115
|
return null;
|
|
1776
2116
|
}
|
|
@@ -1792,7 +2132,7 @@ async function passthroughCommand(ctx, command, args) {
|
|
|
1792
2132
|
if (result.exitCode !== 0) {
|
|
1793
2133
|
throw new FetError({
|
|
1794
2134
|
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
1795
|
-
message: `OpenSpec ${command}
|
|
2135
|
+
message: `OpenSpec ${command} failed.`,
|
|
1796
2136
|
details: result
|
|
1797
2137
|
});
|
|
1798
2138
|
}
|
|
@@ -1834,18 +2174,6 @@ async function mapOpenSpecCommand(ctx, command, args) {
|
|
|
1834
2174
|
return { command: "new", args: args[0] === "change" ? args : ["change", ...args] };
|
|
1835
2175
|
case "archive":
|
|
1836
2176
|
return { command: "archive", args: [ctx.changeId ?? args[0] ?? await requireChangeId(ctx), ...args.slice(ctx.changeId ? 0 : 1)] };
|
|
1837
|
-
/*
|
|
1838
|
-
case "bulk-archive":
|
|
1839
|
-
throw new FetError({
|
|
1840
|
-
code: ErrorCode.InvalidArguments,
|
|
1841
|
-
message: "OpenSpec 1.2.0 不提供 bulk-archive 顶层命令",
|
|
1842
|
-
suggestedCommand: "逐个执行 fet archive --change <change-id>"
|
|
1843
|
-
});
|
|
1844
|
-
case "explore":
|
|
1845
|
-
return { command: "explore", args: ctx.changeId ? ["--change", ctx.changeId, ...args] : args };
|
|
1846
|
-
case "onboard":
|
|
1847
|
-
return { command: "instructions", args: [] };
|
|
1848
|
-
*/
|
|
1849
2177
|
default:
|
|
1850
2178
|
return { command, args };
|
|
1851
2179
|
}
|
|
@@ -1865,6 +2193,18 @@ async function withDefaultChange(ctx, args, allowWithArgs = false) {
|
|
|
1865
2193
|
}
|
|
1866
2194
|
return ["--change", await requireChangeId(ctx), ...args];
|
|
1867
2195
|
}
|
|
2196
|
+
function extractChangeId(args) {
|
|
2197
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2198
|
+
const arg = args[index];
|
|
2199
|
+
if (arg === "--change") {
|
|
2200
|
+
return args[index + 1] ?? null;
|
|
2201
|
+
}
|
|
2202
|
+
if (arg?.startsWith("--change=")) {
|
|
2203
|
+
return arg.slice("--change=".length) || null;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
return null;
|
|
2207
|
+
}
|
|
1868
2208
|
async function requireChangeId(ctx) {
|
|
1869
2209
|
if (ctx.changeId) {
|
|
1870
2210
|
return ctx.changeId;
|
|
@@ -1879,9 +2219,9 @@ async function requireChangeId(ctx) {
|
|
|
1879
2219
|
}
|
|
1880
2220
|
throw new FetError({
|
|
1881
2221
|
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
1882
|
-
message: "
|
|
2222
|
+
message: "No unambiguous OpenSpec change id was found.",
|
|
1883
2223
|
details: { openChangeIds: inspection.changes },
|
|
1884
|
-
suggestedCommand: "
|
|
2224
|
+
suggestedCommand: "Pass --change <change-id>."
|
|
1885
2225
|
});
|
|
1886
2226
|
}
|
|
1887
2227
|
async function assertVerified(ctx) {
|
|
@@ -1890,7 +2230,7 @@ async function assertVerified(ctx) {
|
|
|
1890
2230
|
if (!changeId) {
|
|
1891
2231
|
throw new FetError({
|
|
1892
2232
|
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
1893
|
-
message: "
|
|
2233
|
+
message: "A change id is required before this command can check FET verification.",
|
|
1894
2234
|
suggestedCommand: "fet verify --done --change <change-id>"
|
|
1895
2235
|
});
|
|
1896
2236
|
}
|
|
@@ -1899,7 +2239,7 @@ async function assertVerified(ctx) {
|
|
|
1899
2239
|
if (!inspection.changes.includes(changeId)) {
|
|
1900
2240
|
throw new FetError({
|
|
1901
2241
|
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
1902
|
-
message: "
|
|
2242
|
+
message: "The selected change does not exist in openspec/changes.",
|
|
1903
2243
|
details: { changeId, openChangeIds: inspection.changes },
|
|
1904
2244
|
suggestedCommand: "fet doctor"
|
|
1905
2245
|
});
|
|
@@ -1907,7 +2247,7 @@ async function assertVerified(ctx) {
|
|
|
1907
2247
|
if (change?.manualVerify?.status !== "declared_done") {
|
|
1908
2248
|
throw new FetError({
|
|
1909
2249
|
code: "STATE_CORRUPTED" /* StateCorrupted */,
|
|
1910
|
-
message: "
|
|
2250
|
+
message: "This change has not been marked verified by FET.",
|
|
1911
2251
|
details: { changeId },
|
|
1912
2252
|
suggestedCommand: `fet verify --change ${changeId}`
|
|
1913
2253
|
});
|
|
@@ -1916,8 +2256,8 @@ async function assertVerified(ctx) {
|
|
|
1916
2256
|
|
|
1917
2257
|
// src/commands/verify.ts
|
|
1918
2258
|
import { createHash } from "crypto";
|
|
1919
|
-
import { mkdir as
|
|
1920
|
-
import { join as
|
|
2259
|
+
import { mkdir as mkdir7, readFile as readFile12, stat as stat5 } from "fs/promises";
|
|
2260
|
+
import { join as join15 } from "path";
|
|
1921
2261
|
async function verifyCommand(ctx, options) {
|
|
1922
2262
|
if (options.auto) {
|
|
1923
2263
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
@@ -1984,9 +2324,9 @@ async function verifyCommand(ctx, options) {
|
|
|
1984
2324
|
async function writeInstructions(ctx, changeId) {
|
|
1985
2325
|
await assertChangeExists(ctx, changeId);
|
|
1986
2326
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1987
|
-
const dir =
|
|
1988
|
-
const instructionsPath =
|
|
1989
|
-
await
|
|
2327
|
+
const dir = join15(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
2328
|
+
const instructionsPath = join15(dir, "verify-instructions.md");
|
|
2329
|
+
await mkdir7(dir, { recursive: true });
|
|
1990
2330
|
await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
|
|
1991
2331
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
1992
2332
|
state.currentPhase = "verify";
|
|
@@ -2002,7 +2342,7 @@ async function writeInstructions(ctx, changeId) {
|
|
|
2002
2342
|
async function markDone(ctx, changeId) {
|
|
2003
2343
|
await assertChangeExists(ctx, changeId);
|
|
2004
2344
|
const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2005
|
-
const instructionsPath =
|
|
2345
|
+
const instructionsPath = join15(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
|
|
2006
2346
|
const instructions = await readInstructions(instructionsPath, changeId);
|
|
2007
2347
|
const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
|
|
2008
2348
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
@@ -2038,7 +2378,7 @@ async function assertChangeExists(ctx, changeId) {
|
|
|
2038
2378
|
async function readInstructions(path, changeId) {
|
|
2039
2379
|
try {
|
|
2040
2380
|
await stat5(path);
|
|
2041
|
-
const content = await
|
|
2381
|
+
const content = await readFile12(path, "utf8");
|
|
2042
2382
|
const fileChangeId = readFrontMatterValue(content, "changeId");
|
|
2043
2383
|
if (fileChangeId !== changeId) {
|
|
2044
2384
|
throw new FetError({
|
|
@@ -2176,9 +2516,9 @@ function renderIdeModelPolicy(command, language = "zh-CN") {
|
|
|
2176
2516
|
import { resolve } from "path";
|
|
2177
2517
|
|
|
2178
2518
|
// src/adapters/codex/index.ts
|
|
2179
|
-
import { mkdir as
|
|
2519
|
+
import { mkdir as mkdir8, readFile as readFile13, stat as stat6 } from "fs/promises";
|
|
2180
2520
|
import { homedir } from "os";
|
|
2181
|
-
import { dirname as
|
|
2521
|
+
import { dirname as dirname8, join as join16 } from "path";
|
|
2182
2522
|
|
|
2183
2523
|
// src/adapters/commands.ts
|
|
2184
2524
|
var FET_WORKFLOW_COMMANDS = [
|
|
@@ -2489,15 +2829,15 @@ After the command completes, report the GitNexus state, generated handoff files,
|
|
|
2489
2829
|
`;
|
|
2490
2830
|
}
|
|
2491
2831
|
function renderSlashPrompt(command, language) {
|
|
2832
|
+
if (command === "ff" || command === "propose") {
|
|
2833
|
+
return renderFastForwardSlashPrompt(command, language);
|
|
2834
|
+
}
|
|
2492
2835
|
if (language !== "en") {
|
|
2493
2836
|
return renderSlashPromptZh(command);
|
|
2494
2837
|
}
|
|
2495
2838
|
if (command === "continue") {
|
|
2496
2839
|
return renderContinueSlashPrompt(language);
|
|
2497
2840
|
}
|
|
2498
|
-
if (command === "ff" || command === "propose") {
|
|
2499
|
-
return renderFastForwardSlashPrompt(command, language);
|
|
2500
|
-
}
|
|
2501
2841
|
if (command === "explore") {
|
|
2502
2842
|
return renderExploreSlashPrompt(language);
|
|
2503
2843
|
}
|
|
@@ -3097,11 +3437,11 @@ Guardrails:
|
|
|
3097
3437
|
);
|
|
3098
3438
|
}
|
|
3099
3439
|
function renderFastForwardSlashPrompt(command, language) {
|
|
3100
|
-
const title = command === "propose" ? "Propose a new FET/OpenSpec change" : "Fast-forward FET/OpenSpec artifact creation";
|
|
3440
|
+
const title = language === "en" ? command === "propose" ? "Propose a new FET/OpenSpec change" : "Fast-forward FET/OpenSpec artifact creation" : command === "propose" ? "\u521B\u5EFA\u5E76\u8865\u9F50 FET/OpenSpec \u63D0\u6848\u4EA7\u7269" : "\u5FEB\u901F\u751F\u6210 FET/OpenSpec \u6240\u9700\u4EA7\u7269";
|
|
3101
3441
|
const commandLine = command === "propose" ? "fet propose <change-id-or-description>" : "fet ff --change <change-id>";
|
|
3102
3442
|
return renderManagedSlashPrompt(
|
|
3103
3443
|
`fet ${command} [...args]`,
|
|
3104
|
-
command === "propose" ? "Create a change and generate required OpenSpec artifacts" : "Generate required OpenSpec artifacts for a change",
|
|
3444
|
+
language === "en" ? command === "propose" ? "Create a change and generate required OpenSpec artifacts" : "Generate required OpenSpec artifacts for a change" : command === "propose" ? "\u521B\u5EFA\u5E76\u8865\u9F50 FET/OpenSpec \u63D0\u6848\u4EA7\u7269" : "\u5FEB\u901F\u751F\u6210 FET/OpenSpec \u6240\u9700\u4EA7\u7269",
|
|
3105
3445
|
`${title}.
|
|
3106
3446
|
|
|
3107
3447
|
Input after the slash command may be a change id or a description of what the user wants to build. For ff, it may be omitted when the active OpenSpec change is unambiguous.
|
|
@@ -3116,6 +3456,7 @@ Steps:
|
|
|
3116
3456
|
\`\`\`
|
|
3117
3457
|
4. Follow the native output. If it asks for clarification, ask the user rather than inventing details.
|
|
3118
3458
|
5. If the native output includes artifact paths or templates to write, create only those files and preserve OpenSpec structure.
|
|
3459
|
+
6. If FET reports that the OpenSpec CLI does not expose the requested command, stop immediately. Do not run \`fet ff\`, \`openspec ff\`, \`openspec change\`, or any alternative workflow command unless the user explicitly chooses that fallback after seeing the error.
|
|
3119
3460
|
|
|
3120
3461
|
Artifact rules:
|
|
3121
3462
|
- Follow the instruction field from OpenSpec/FET for each artifact.
|
|
@@ -3127,7 +3468,11 @@ Output:
|
|
|
3127
3468
|
- Change id and location.
|
|
3128
3469
|
- Artifacts created.
|
|
3129
3470
|
- Current status.
|
|
3130
|
-
- Next recommended command, usually /prompts:fet-apply <change-id
|
|
3471
|
+
- Next recommended command, usually /prompts:fet-apply <change-id>.
|
|
3472
|
+
|
|
3473
|
+
Guardrails:
|
|
3474
|
+
- Do not substitute one FET/OpenSpec workflow command for another after a command-not-found or unsupported-version error.
|
|
3475
|
+
- If OpenSpec appears outdated or incompatible, report the detected version and suggest \`npm install -g @fission-ai/openspec@latest\` or \`fet doctor\`, then wait for the user's decision.`,
|
|
3131
3476
|
void 0,
|
|
3132
3477
|
language
|
|
3133
3478
|
);
|
|
@@ -3165,7 +3510,7 @@ var CodexAdapter = class {
|
|
|
3165
3510
|
adapterVersion = 1;
|
|
3166
3511
|
async detect(projectRoot) {
|
|
3167
3512
|
return {
|
|
3168
|
-
detected: await exists3(
|
|
3513
|
+
detected: await exists3(join16(projectRoot, ".codex")) || await exists3(join16(projectRoot, "AGENTS.md")),
|
|
3169
3514
|
reason: "Codex adapter is available for projects that use AGENTS.md"
|
|
3170
3515
|
};
|
|
3171
3516
|
}
|
|
@@ -3204,7 +3549,7 @@ var CodexAdapter = class {
|
|
|
3204
3549
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
3205
3550
|
await createBackup(target);
|
|
3206
3551
|
}
|
|
3207
|
-
await
|
|
3552
|
+
await mkdir8(dirname8(target), { recursive: true });
|
|
3208
3553
|
await atomicWrite(target, file.content);
|
|
3209
3554
|
written.push(displayPath);
|
|
3210
3555
|
}
|
|
@@ -3231,9 +3576,9 @@ var CodexAdapter = class {
|
|
|
3231
3576
|
};
|
|
3232
3577
|
function resolveTarget(projectRoot, file) {
|
|
3233
3578
|
if (file.root === "codex-home") {
|
|
3234
|
-
return
|
|
3579
|
+
return join16(resolveCodexHome(), file.path);
|
|
3235
3580
|
}
|
|
3236
|
-
return
|
|
3581
|
+
return join16(projectRoot, file.path);
|
|
3237
3582
|
}
|
|
3238
3583
|
function displayPathFor(file) {
|
|
3239
3584
|
if (file.root === "codex-home") {
|
|
@@ -3242,11 +3587,11 @@ function displayPathFor(file) {
|
|
|
3242
3587
|
return file.path;
|
|
3243
3588
|
}
|
|
3244
3589
|
function resolveCodexHome() {
|
|
3245
|
-
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ??
|
|
3590
|
+
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join16(homedir(), ".codex");
|
|
3246
3591
|
}
|
|
3247
3592
|
async function readExisting(path) {
|
|
3248
3593
|
try {
|
|
3249
|
-
return await
|
|
3594
|
+
return await readFile13(path, "utf8");
|
|
3250
3595
|
} catch {
|
|
3251
3596
|
return null;
|
|
3252
3597
|
}
|
|
@@ -3261,8 +3606,8 @@ async function exists3(path) {
|
|
|
3261
3606
|
}
|
|
3262
3607
|
|
|
3263
3608
|
// src/adapters/cursor/index.ts
|
|
3264
|
-
import { mkdir as
|
|
3265
|
-
import { dirname as
|
|
3609
|
+
import { mkdir as mkdir9, readFile as readFile14, stat as stat7 } from "fs/promises";
|
|
3610
|
+
import { dirname as dirname9, join as join17 } from "path";
|
|
3266
3611
|
|
|
3267
3612
|
// src/adapters/cursor/templates.ts
|
|
3268
3613
|
function cursorSkillFiles(language = DEFAULT_LANGUAGE) {
|
|
@@ -3396,7 +3741,7 @@ var CursorAdapter = class {
|
|
|
3396
3741
|
adapterVersion = 1;
|
|
3397
3742
|
async detect(projectRoot) {
|
|
3398
3743
|
return {
|
|
3399
|
-
detected: await exists4(
|
|
3744
|
+
detected: await exists4(join17(projectRoot, ".cursor")),
|
|
3400
3745
|
reason: "Cursor adapter is available for any project"
|
|
3401
3746
|
};
|
|
3402
3747
|
}
|
|
@@ -3413,7 +3758,7 @@ var CursorAdapter = class {
|
|
|
3413
3758
|
const written = [];
|
|
3414
3759
|
const skipped = [];
|
|
3415
3760
|
for (const file of plan.files) {
|
|
3416
|
-
const target =
|
|
3761
|
+
const target = join17(projectRoot, file.path);
|
|
3417
3762
|
const existing = await readExisting2(target);
|
|
3418
3763
|
if (existing && !existing.includes("FET:MANAGED") && !force) {
|
|
3419
3764
|
throw new FetError({
|
|
@@ -3426,7 +3771,7 @@ var CursorAdapter = class {
|
|
|
3426
3771
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
3427
3772
|
await createBackup(target);
|
|
3428
3773
|
}
|
|
3429
|
-
await
|
|
3774
|
+
await mkdir9(dirname9(target), { recursive: true });
|
|
3430
3775
|
await atomicWrite(target, file.content);
|
|
3431
3776
|
written.push(file.path);
|
|
3432
3777
|
}
|
|
@@ -3436,7 +3781,7 @@ var CursorAdapter = class {
|
|
|
3436
3781
|
const plan = await this.planInstall(projectRoot);
|
|
3437
3782
|
const checks = [];
|
|
3438
3783
|
for (const file of plan.files) {
|
|
3439
|
-
const target =
|
|
3784
|
+
const target = join17(projectRoot, file.path);
|
|
3440
3785
|
const content = await readExisting2(target);
|
|
3441
3786
|
const managed = Boolean(content?.includes("FET:MANAGED"));
|
|
3442
3787
|
const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
|
|
@@ -3452,7 +3797,7 @@ var CursorAdapter = class {
|
|
|
3452
3797
|
};
|
|
3453
3798
|
async function readExisting2(path) {
|
|
3454
3799
|
try {
|
|
3455
|
-
return await
|
|
3800
|
+
return await readFile14(path, "utf8");
|
|
3456
3801
|
} catch {
|
|
3457
3802
|
return null;
|
|
3458
3803
|
}
|
|
@@ -3471,13 +3816,13 @@ import { execFile as execFile4 } from "child_process";
|
|
|
3471
3816
|
import { promisify as promisify4 } from "util";
|
|
3472
3817
|
|
|
3473
3818
|
// src/openspec/inspector.ts
|
|
3474
|
-
import { readdir, stat as stat8 } from "fs/promises";
|
|
3475
|
-
import { join as
|
|
3819
|
+
import { readdir as readdir2, stat as stat8 } from "fs/promises";
|
|
3820
|
+
import { join as join18 } from "path";
|
|
3476
3821
|
async function inspectOpenSpecProject(projectRoot) {
|
|
3477
|
-
const openspecPath =
|
|
3478
|
-
const changesPath =
|
|
3479
|
-
const legacyArchivePath =
|
|
3480
|
-
const changesArchivePath =
|
|
3822
|
+
const openspecPath = join18(projectRoot, "openspec");
|
|
3823
|
+
const changesPath = join18(openspecPath, "changes");
|
|
3824
|
+
const legacyArchivePath = join18(openspecPath, "archive");
|
|
3825
|
+
const changesArchivePath = join18(changesPath, "archive");
|
|
3481
3826
|
return {
|
|
3482
3827
|
exists: await exists5(openspecPath),
|
|
3483
3828
|
changes: await listDirectories(changesPath, { exclude: ["archive"] }),
|
|
@@ -3485,13 +3830,13 @@ async function inspectOpenSpecProject(projectRoot) {
|
|
|
3485
3830
|
};
|
|
3486
3831
|
}
|
|
3487
3832
|
async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
3488
|
-
const changePath =
|
|
3489
|
-
const tasksPath =
|
|
3490
|
-
const specsPath =
|
|
3833
|
+
const changePath = join18(projectRoot, "openspec", "changes", changeId);
|
|
3834
|
+
const tasksPath = join18(changePath, "tasks.md");
|
|
3835
|
+
const specsPath = join18(changePath, "specs");
|
|
3491
3836
|
return {
|
|
3492
3837
|
changeId,
|
|
3493
3838
|
exists: await exists5(changePath),
|
|
3494
|
-
hasProposal: await exists5(
|
|
3839
|
+
hasProposal: await exists5(join18(changePath, "proposal.md")),
|
|
3495
3840
|
hasTasks: await exists5(tasksPath),
|
|
3496
3841
|
hasSpecs: await exists5(specsPath),
|
|
3497
3842
|
tasksPath,
|
|
@@ -3500,7 +3845,7 @@ async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
|
3500
3845
|
}
|
|
3501
3846
|
async function listDirectories(path, options = {}) {
|
|
3502
3847
|
try {
|
|
3503
|
-
const entries = await
|
|
3848
|
+
const entries = await readdir2(path, { withFileTypes: true });
|
|
3504
3849
|
const excluded = new Set(options.exclude ?? []);
|
|
3505
3850
|
return entries.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).map((entry) => entry.name);
|
|
3506
3851
|
} catch {
|
|
@@ -3533,7 +3878,7 @@ async function findExecutable() {
|
|
|
3533
3878
|
const command = process.platform === "win32" ? "where.exe" : "which";
|
|
3534
3879
|
try {
|
|
3535
3880
|
const { stdout } = await exec(command, ["openspec"]);
|
|
3536
|
-
const first = stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
3881
|
+
const first = stdout.split(/\r?\n/).map((line) => line.trim()).sort((left, right) => executablePreference(left) - executablePreference(right)).find(Boolean);
|
|
3537
3882
|
if (first) {
|
|
3538
3883
|
return first;
|
|
3539
3884
|
}
|
|
@@ -3550,6 +3895,12 @@ async function findExecutable() {
|
|
|
3550
3895
|
});
|
|
3551
3896
|
}
|
|
3552
3897
|
}
|
|
3898
|
+
function executablePreference(path) {
|
|
3899
|
+
if (process.platform === "win32" && path.toLowerCase().endsWith(".cmd")) {
|
|
3900
|
+
return 0;
|
|
3901
|
+
}
|
|
3902
|
+
return 1;
|
|
3903
|
+
}
|
|
3553
3904
|
async function readVersion(executablePath) {
|
|
3554
3905
|
const command = executablePath === "npx openspec" ? "npx" : executablePath;
|
|
3555
3906
|
const args = executablePath === "npx openspec" ? ["openspec", "--version"] : ["--version"];
|
|
@@ -3664,16 +4015,19 @@ function parseCommands(help) {
|
|
|
3664
4015
|
"bulk-archive",
|
|
3665
4016
|
"onboard"
|
|
3666
4017
|
];
|
|
3667
|
-
return known.filter((command) =>
|
|
4018
|
+
return known.filter((command) => new RegExp(`\\b${escapeRegExp(command)}\\b`).test(help));
|
|
4019
|
+
}
|
|
4020
|
+
function escapeRegExp(value) {
|
|
4021
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3668
4022
|
}
|
|
3669
4023
|
|
|
3670
4024
|
// src/scanner/package.ts
|
|
3671
|
-
import { readFile as
|
|
3672
|
-
import { join as
|
|
4025
|
+
import { readFile as readFile15, stat as stat9 } from "fs/promises";
|
|
4026
|
+
import { join as join19 } from "path";
|
|
3673
4027
|
import { parse as parse2 } from "yaml";
|
|
3674
4028
|
async function readPackageJson(projectRoot) {
|
|
3675
4029
|
try {
|
|
3676
|
-
return JSON.parse(await
|
|
4030
|
+
return JSON.parse(await readFile15(join19(projectRoot, "package.json"), "utf8"));
|
|
3677
4031
|
} catch {
|
|
3678
4032
|
return null;
|
|
3679
4033
|
}
|
|
@@ -3739,7 +4093,7 @@ function detectFramework(pkg) {
|
|
|
3739
4093
|
}
|
|
3740
4094
|
async function detectLanguage(projectRoot, pkg) {
|
|
3741
4095
|
const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
|
|
3742
|
-
if (deps.typescript || await exists6(
|
|
4096
|
+
if (deps.typescript || await exists6(join19(projectRoot, "tsconfig.json"))) {
|
|
3743
4097
|
return "typescript";
|
|
3744
4098
|
}
|
|
3745
4099
|
return "javascript";
|
|
@@ -3754,7 +4108,7 @@ async function detectWorkspaces(projectRoot, pkg) {
|
|
|
3754
4108
|
return packageWorkspaces;
|
|
3755
4109
|
}
|
|
3756
4110
|
try {
|
|
3757
|
-
const workspace = parse2(await
|
|
4111
|
+
const workspace = parse2(await readFile15(join19(projectRoot, "pnpm-workspace.yaml"), "utf8"));
|
|
3758
4112
|
return (workspace?.packages ?? []).map((path) => ({
|
|
3759
4113
|
name: path,
|
|
3760
4114
|
path,
|
|
@@ -3774,7 +4128,7 @@ async function detectLockManagers(projectRoot) {
|
|
|
3774
4128
|
];
|
|
3775
4129
|
const found = [];
|
|
3776
4130
|
for (const [file, manager] of lockFiles) {
|
|
3777
|
-
if (await exists6(
|
|
4131
|
+
if (await exists6(join19(projectRoot, file))) {
|
|
3778
4132
|
found.push(manager);
|
|
3779
4133
|
}
|
|
3780
4134
|
}
|
|
@@ -3799,13 +4153,13 @@ async function exists6(path) {
|
|
|
3799
4153
|
}
|
|
3800
4154
|
|
|
3801
4155
|
// src/scanner/routes.ts
|
|
3802
|
-
import { readdir as
|
|
3803
|
-
import { join as
|
|
4156
|
+
import { readdir as readdir3, stat as stat10 } from "fs/promises";
|
|
4157
|
+
import { join as join20, relative, sep } from "path";
|
|
3804
4158
|
async function scanRoutes(projectRoot) {
|
|
3805
4159
|
const candidates = ["src/routes", "src/pages", "app", "pages"];
|
|
3806
4160
|
const routes = [];
|
|
3807
4161
|
for (const candidate of candidates) {
|
|
3808
|
-
const root =
|
|
4162
|
+
const root = join20(projectRoot, candidate);
|
|
3809
4163
|
if (!await exists7(root)) {
|
|
3810
4164
|
continue;
|
|
3811
4165
|
}
|
|
@@ -3830,10 +4184,10 @@ function inferRoutePath(relativePath) {
|
|
|
3830
4184
|
return `/${withoutIndex}`.replace(/\/+/g, "/");
|
|
3831
4185
|
}
|
|
3832
4186
|
async function listFiles(root) {
|
|
3833
|
-
const entries = await
|
|
4187
|
+
const entries = await readdir3(root, { withFileTypes: true });
|
|
3834
4188
|
const files = [];
|
|
3835
4189
|
for (const entry of entries) {
|
|
3836
|
-
const path =
|
|
4190
|
+
const path = join20(root, entry.name);
|
|
3837
4191
|
if (entry.isDirectory()) {
|
|
3838
4192
|
files.push(...await listFiles(path));
|
|
3839
4193
|
} else {
|