@nick848/fet 0.1.0 → 1.0.1
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/LICENSE +21 -21
- package/README.md +265 -44
- package/dist/chunk-FZOVNHE7.js +104 -0
- package/dist/chunk-FZOVNHE7.js.map +1 -0
- package/dist/cli/index.js +1816 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -49
- package/dist/apply.d.ts +0 -1
- package/dist/apply.js +0 -172
- package/dist/approval.d.ts +0 -2
- package/dist/approval.js +0 -26
- package/dist/atomic-write.d.ts +0 -5
- package/dist/atomic-write.js +0 -41
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -178
- package/dist/doctor.d.ts +0 -1
- package/dist/doctor.js +0 -93
- package/dist/fingerprint.d.ts +0 -6
- package/dist/fingerprint.js +0 -77
- package/dist/hooks.d.ts +0 -12
- package/dist/hooks.js +0 -47
- package/dist/init.d.ts +0 -4
- package/dist/init.js +0 -47
- package/dist/opencode-skills.d.ts +0 -3
- package/dist/opencode-skills.js +0 -236
- package/dist/openspec.d.ts +0 -16
- package/dist/openspec.js +0 -73
- package/dist/paths.d.ts +0 -9
- package/dist/paths.js +0 -20
- package/dist/prompt.d.ts +0 -4
- package/dist/prompt.js +0 -30
- package/dist/scanner.d.ts +0 -23
- package/dist/scanner.js +0 -352
- package/dist/skills.d.ts +0 -3
- package/dist/skills.js +0 -142
- package/dist/state.d.ts +0 -17
- package/dist/state.js +0 -126
- package/dist/tasks.d.ts +0 -13
- package/dist/tasks.js +0 -69
- package/dist/types.d.ts +0 -38
- package/dist/types.js +0 -1
- package/dist/validate.d.ts +0 -1
- package/dist/validate.js +0 -150
- package/dist/verify.d.ts +0 -6
- package/dist/verify.js +0 -193
- package/dist/watch-paths.d.ts +0 -2
- package/dist/watch-paths.js +0 -70
- package/dist/workflow-hints.d.ts +0 -2
- package/dist/workflow-hints.js +0 -9
|
@@ -0,0 +1,1816 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
FetError,
|
|
4
|
+
toFetError
|
|
5
|
+
} from "../chunk-FZOVNHE7.js";
|
|
6
|
+
|
|
7
|
+
// src/cli/index.ts
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
|
|
10
|
+
// src/commands/init.ts
|
|
11
|
+
import { readFile as readFile5, stat as stat2 } from "fs/promises";
|
|
12
|
+
import { join as join6 } from "path";
|
|
13
|
+
|
|
14
|
+
// src/fs/atomic-write.ts
|
|
15
|
+
import { dirname } from "path";
|
|
16
|
+
import { open, rename, mkdir } from "fs/promises";
|
|
17
|
+
async function atomicWrite(targetPath, content) {
|
|
18
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
19
|
+
const tempPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
|
|
20
|
+
const handle = await open(tempPath, "wx");
|
|
21
|
+
try {
|
|
22
|
+
await handle.writeFile(content);
|
|
23
|
+
await handle.sync();
|
|
24
|
+
} finally {
|
|
25
|
+
await handle.close();
|
|
26
|
+
}
|
|
27
|
+
await rename(tempPath, targetPath);
|
|
28
|
+
return { path: targetPath, tempPath };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/fs/backup.ts
|
|
32
|
+
import { copyFile, stat } from "fs/promises";
|
|
33
|
+
import { basename, dirname as dirname2, join } from "path";
|
|
34
|
+
async function createBackup(filePath) {
|
|
35
|
+
try {
|
|
36
|
+
await stat(filePath);
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").replace(/\..+$/, "").replace("T", "-");
|
|
41
|
+
const backupPath = join(dirname2(filePath), `${basename(filePath)}.fet-backup-${timestamp}`);
|
|
42
|
+
await copyFile(filePath, backupPath);
|
|
43
|
+
return backupPath;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/fs/lock.ts
|
|
47
|
+
import { hostname } from "os";
|
|
48
|
+
import { dirname as dirname3, join as join2 } from "path";
|
|
49
|
+
import { mkdir as mkdir2, open as open2, readFile, rm } from "fs/promises";
|
|
50
|
+
async function withProjectLock(projectRoot, metadata, fn) {
|
|
51
|
+
const lockPath = join2(projectRoot, "openspec", ".fet.lock");
|
|
52
|
+
const lock = {
|
|
53
|
+
pid: process.pid,
|
|
54
|
+
hostname: hostname(),
|
|
55
|
+
cwd: metadata.cwd,
|
|
56
|
+
command: metadata.command,
|
|
57
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
58
|
+
fetVersion: metadata.fetVersion
|
|
59
|
+
};
|
|
60
|
+
let handle;
|
|
61
|
+
try {
|
|
62
|
+
await mkdir2(dirname3(lockPath), { recursive: true });
|
|
63
|
+
handle = await open2(lockPath, "wx");
|
|
64
|
+
await handle.writeFile(JSON.stringify(lock, null, 2));
|
|
65
|
+
} catch {
|
|
66
|
+
throw new FetError({
|
|
67
|
+
code: "LOCK_HELD" /* LockHeld */,
|
|
68
|
+
message: "\u53E6\u4E00\u4E2A FET \u5199\u547D\u4EE4\u6B63\u5728\u8FD0\u884C",
|
|
69
|
+
details: await readExistingLock(lockPath),
|
|
70
|
+
suggestedCommand: "fet doctor --fix-lock"
|
|
71
|
+
});
|
|
72
|
+
} finally {
|
|
73
|
+
await handle?.close();
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
return await fn();
|
|
77
|
+
} finally {
|
|
78
|
+
await rm(lockPath, { force: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function readExistingLock(lockPath) {
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(await readFile(lockPath, "utf8"));
|
|
84
|
+
} catch {
|
|
85
|
+
return { path: lockPath };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function clearLock(projectRoot) {
|
|
89
|
+
const lockPath = join2(projectRoot, "openspec", ".fet.lock");
|
|
90
|
+
try {
|
|
91
|
+
await rm(lockPath);
|
|
92
|
+
return true;
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/fs/journal.ts
|
|
99
|
+
import { join as join3 } from "path";
|
|
100
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
101
|
+
function createInitJournal(fetVersion) {
|
|
102
|
+
return {
|
|
103
|
+
schemaVersion: 1,
|
|
104
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
105
|
+
completedAt: null,
|
|
106
|
+
fetVersion,
|
|
107
|
+
steps: []
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async function writeInitJournal(projectRoot, journal) {
|
|
111
|
+
await atomicWrite(
|
|
112
|
+
join3(projectRoot, "openspec", ".fet-init-journal.json"),
|
|
113
|
+
`${JSON.stringify(journal, null, 2)}
|
|
114
|
+
`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/version.ts
|
|
119
|
+
import { existsSync, readFileSync } from "fs";
|
|
120
|
+
import { dirname as dirname4, join as join4, parse } from "path";
|
|
121
|
+
import { fileURLToPath } from "url";
|
|
122
|
+
var FET_VERSION = readPackageVersion();
|
|
123
|
+
function readPackageVersion() {
|
|
124
|
+
let currentDir = dirname4(fileURLToPath(import.meta.url));
|
|
125
|
+
const root = parse(currentDir).root;
|
|
126
|
+
while (true) {
|
|
127
|
+
const packageJsonPath = join4(currentDir, "package.json");
|
|
128
|
+
if (existsSync(packageJsonPath)) {
|
|
129
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
130
|
+
if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
|
|
131
|
+
return packageJson.version;
|
|
132
|
+
}
|
|
133
|
+
throw new Error(`package.json \u7F3A\u5C11\u6709\u6548\u7684 version \u5B57\u6BB5: ${packageJsonPath}`);
|
|
134
|
+
}
|
|
135
|
+
if (currentDir === root) {
|
|
136
|
+
throw new Error("\u65E0\u6CD5\u5B9A\u4F4D FET package.json");
|
|
137
|
+
}
|
|
138
|
+
currentDir = dirname4(currentDir);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/templates/markers.ts
|
|
143
|
+
var AUTO_BEGIN = "<!-- FET:BEGIN AUTO -->";
|
|
144
|
+
var AUTO_END = "<!-- FET:END AUTO -->";
|
|
145
|
+
var USER_BEGIN = "<!-- FET:BEGIN USER -->";
|
|
146
|
+
var USER_END = "<!-- FET:END USER -->";
|
|
147
|
+
function hasManagedAutoRegion(content) {
|
|
148
|
+
return count(content, AUTO_BEGIN) === 1 && count(content, AUTO_END) === 1 && content.indexOf(AUTO_BEGIN) < content.indexOf(AUTO_END);
|
|
149
|
+
}
|
|
150
|
+
function hasInvalidManagedAutoRegion(content) {
|
|
151
|
+
const beginCount = count(content, AUTO_BEGIN);
|
|
152
|
+
const endCount = count(content, AUTO_END);
|
|
153
|
+
return beginCount !== endCount || beginCount > 1 || endCount > 1 || beginCount === 1 && content.indexOf(AUTO_BEGIN) > content.indexOf(AUTO_END);
|
|
154
|
+
}
|
|
155
|
+
function replaceManagedRegion(existing, generated) {
|
|
156
|
+
if (!existing) {
|
|
157
|
+
return generated;
|
|
158
|
+
}
|
|
159
|
+
const start = existing.indexOf(AUTO_BEGIN);
|
|
160
|
+
const end = existing.indexOf(AUTO_END);
|
|
161
|
+
if (start === -1 || end === -1 || end < start) {
|
|
162
|
+
return generated;
|
|
163
|
+
}
|
|
164
|
+
const before = existing.slice(0, start);
|
|
165
|
+
const after = existing.slice(end + AUTO_END.length);
|
|
166
|
+
const generatedAuto = extractAuto(generated);
|
|
167
|
+
return `${before}${AUTO_BEGIN}
|
|
168
|
+
${generatedAuto}
|
|
169
|
+
${AUTO_END}${after}`;
|
|
170
|
+
}
|
|
171
|
+
function extractAuto(content) {
|
|
172
|
+
const start = content.indexOf(AUTO_BEGIN);
|
|
173
|
+
const end = content.indexOf(AUTO_END);
|
|
174
|
+
if (start === -1 || end === -1 || end < start) {
|
|
175
|
+
return content.trim();
|
|
176
|
+
}
|
|
177
|
+
return content.slice(start + AUTO_BEGIN.length, end).trim();
|
|
178
|
+
}
|
|
179
|
+
function count(content, needle) {
|
|
180
|
+
return content.split(needle).length - 1;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/templates/agents-md.ts
|
|
184
|
+
function renderAgentsMd(scan) {
|
|
185
|
+
const commands2 = Object.entries(scan.commands).map(([name, command]) => `| ${name} | \`${command.command}\` | ${command.source} |`).join("\n");
|
|
186
|
+
const routes = scan.routes.map((route) => `| ${route.path} | ${route.source} | ${route.confidence}${route.inferred ? " inferred" : ""} |`).join("\n");
|
|
187
|
+
const workspaces = scan.project.workspaces.map((workspace) => `| ${workspace.name} | ${workspace.path} | ${workspace.source} |`).join("\n");
|
|
188
|
+
return `# Project Context
|
|
189
|
+
|
|
190
|
+
${AUTO_BEGIN}
|
|
191
|
+
## Project Snapshot
|
|
192
|
+
|
|
193
|
+
- Name: ${scan.project.name}
|
|
194
|
+
- Package Manager: ${scan.project.packageManager} (${scan.project.packageManagerConfidence})
|
|
195
|
+
- Framework: ${scan.project.framework.name} (${scan.project.framework.confidence})
|
|
196
|
+
- Language: ${scan.project.language}
|
|
197
|
+
- Monorepo: ${scan.project.monorepo ? "yes" : "no"}
|
|
198
|
+
|
|
199
|
+
## Workspaces
|
|
200
|
+
|
|
201
|
+
| Name | Path | Source |
|
|
202
|
+
|------|------|--------|
|
|
203
|
+
${workspaces || "| root | . | inferred |"}
|
|
204
|
+
|
|
205
|
+
## Commands
|
|
206
|
+
|
|
207
|
+
| Name | Command | Source |
|
|
208
|
+
|------|---------|--------|
|
|
209
|
+
${commands2 || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | [NEEDS LLM INPUT] |"}
|
|
210
|
+
|
|
211
|
+
## Structure
|
|
212
|
+
|
|
213
|
+
[NEEDS LLM INPUT]
|
|
214
|
+
|
|
215
|
+
## Routes
|
|
216
|
+
|
|
217
|
+
| Route | Source | Confidence |
|
|
218
|
+
|-------|--------|------------|
|
|
219
|
+
${routes || "| [NEEDS LLM INPUT] | [NEEDS LLM INPUT] | low |"}
|
|
220
|
+
|
|
221
|
+
## Conventions
|
|
222
|
+
|
|
223
|
+
[NEEDS LLM INPUT]
|
|
224
|
+
|
|
225
|
+
## Scanner Metadata
|
|
226
|
+
|
|
227
|
+
- Generated At: ${scan.generatedAt}
|
|
228
|
+
- FET Version: ${FET_VERSION}
|
|
229
|
+
- Scanner Version: ${scan.scannerVersion}
|
|
230
|
+
- Warnings: ${scan.warnings.length ? scan.warnings.join("; ") : "none"}
|
|
231
|
+
${AUTO_END}
|
|
232
|
+
|
|
233
|
+
${USER_BEGIN}
|
|
234
|
+
## Notes For AI
|
|
235
|
+
|
|
236
|
+
[NEEDS LLM INPUT]
|
|
237
|
+
${USER_END}
|
|
238
|
+
`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/templates/config-yaml.ts
|
|
242
|
+
import { stringify } from "yaml";
|
|
243
|
+
function renderFetConfig(scan) {
|
|
244
|
+
return stringify({
|
|
245
|
+
fet: {
|
|
246
|
+
schemaVersion: 1,
|
|
247
|
+
generatedAt: scan.generatedAt,
|
|
248
|
+
fetVersion: FET_VERSION,
|
|
249
|
+
scannerVersion: scan.scannerVersion,
|
|
250
|
+
project: {
|
|
251
|
+
packageManager: scan.project.packageManager,
|
|
252
|
+
packageManagerConfidence: scan.project.packageManagerConfidence,
|
|
253
|
+
framework: scan.project.framework,
|
|
254
|
+
language: scan.project.language,
|
|
255
|
+
monorepo: scan.project.monorepo,
|
|
256
|
+
workspaces: scan.project.workspaces
|
|
257
|
+
},
|
|
258
|
+
commands: scan.commands,
|
|
259
|
+
validation: {
|
|
260
|
+
monorepo: scan.project.monorepo,
|
|
261
|
+
missing: {
|
|
262
|
+
lint: "warn",
|
|
263
|
+
typecheck: "warn",
|
|
264
|
+
test: "warn"
|
|
265
|
+
},
|
|
266
|
+
workspaces: scan.project.workspaces
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/templates/verify-instructions.ts
|
|
273
|
+
function renderVerifyInstructions(changeId, generatedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
274
|
+
return `---
|
|
275
|
+
schemaVersion: 1
|
|
276
|
+
fetVersion: ${FET_VERSION}
|
|
277
|
+
generatedAt: ${generatedAt}
|
|
278
|
+
changeId: ${changeId}
|
|
279
|
+
purpose: manual-verify
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
# Verify Instructions
|
|
283
|
+
|
|
284
|
+
\u8BF7\u6309\u987A\u5E8F\u5B8C\u6210\u4EE5\u4E0B\u68C0\u67E5\uFF1A
|
|
285
|
+
|
|
286
|
+
1. \u8FD0\u884C OpenSpec \u89C4\u8303\u6821\u9A8C\uFF1A\`openspec verify\`
|
|
287
|
+
2. \u6309\u9879\u76EE\u7EA6\u5B9A\u8FD0\u884C lint\u3001typecheck\u3001test\u3002
|
|
288
|
+
3. \u68C0\u67E5\u672C\u6B21 change \u7684 \`tasks.md\` \u662F\u5426\u4E0E\u5B9E\u73B0\u72B6\u6001\u4E00\u81F4\u3002
|
|
289
|
+
|
|
290
|
+
\u5B8C\u6210\u540E\u8FD0\u884C\uFF1A
|
|
291
|
+
|
|
292
|
+
\`\`\`sh
|
|
293
|
+
fet verify --done --change ${changeId}
|
|
294
|
+
\`\`\`
|
|
295
|
+
`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/templates/gitignore.ts
|
|
299
|
+
var BEGIN = "# FET:BEGIN LOCAL STATE";
|
|
300
|
+
var END = "# FET:END LOCAL STATE";
|
|
301
|
+
var RULES = [
|
|
302
|
+
"openspec/fet-state.json",
|
|
303
|
+
"openspec/.fet.lock",
|
|
304
|
+
"openspec/.fet-init-journal.json",
|
|
305
|
+
"openspec/changes/*/fet-state.json",
|
|
306
|
+
"openspec/changes/*/.fet/"
|
|
307
|
+
];
|
|
308
|
+
function mergeGitignore(existing) {
|
|
309
|
+
const block = `${BEGIN}
|
|
310
|
+
${RULES.join("\n")}
|
|
311
|
+
${END}`;
|
|
312
|
+
if (!existing || !existing.trim()) {
|
|
313
|
+
return `${block}
|
|
314
|
+
`;
|
|
315
|
+
}
|
|
316
|
+
const start = existing.indexOf(BEGIN);
|
|
317
|
+
const end = existing.indexOf(END);
|
|
318
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
319
|
+
return `${existing.slice(0, start)}${block}${existing.slice(end + END.length)}`;
|
|
320
|
+
}
|
|
321
|
+
return `${existing.replace(/\s*$/, "")}
|
|
322
|
+
|
|
323
|
+
${block}
|
|
324
|
+
`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/commands/update-context.ts
|
|
328
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
329
|
+
import { join as join5 } from "path";
|
|
330
|
+
|
|
331
|
+
// src/config/yaml.ts
|
|
332
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
333
|
+
import { parseDocument } from "yaml";
|
|
334
|
+
async function mergeFetConfig(configPath, renderedFetYaml) {
|
|
335
|
+
const fetDoc = parseDocument(renderedFetYaml);
|
|
336
|
+
const nextFet = fetDoc.get("fet", true);
|
|
337
|
+
let existing = "";
|
|
338
|
+
try {
|
|
339
|
+
existing = await readFile3(configPath, "utf8");
|
|
340
|
+
} catch {
|
|
341
|
+
return renderedFetYaml;
|
|
342
|
+
}
|
|
343
|
+
const doc = parseDocument(existing || "{}");
|
|
344
|
+
doc.set("fet", nextFet);
|
|
345
|
+
return doc.toString();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/commands/update-context.ts
|
|
349
|
+
async function updateContextCommand(ctx) {
|
|
350
|
+
await withProjectLock(
|
|
351
|
+
ctx.projectRoot,
|
|
352
|
+
{ command: "update-context", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
|
|
353
|
+
async () => updateContextFiles(ctx)
|
|
354
|
+
);
|
|
355
|
+
ctx.output.result({
|
|
356
|
+
ok: true,
|
|
357
|
+
command: "update-context",
|
|
358
|
+
summary: "\u5DF2\u66F4\u65B0 AGENTS.md \u4E0E openspec/config.yaml \u7684 FET \u6258\u7BA1\u533A\u57DF\u3002"
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
async function updateContextFiles(ctx) {
|
|
362
|
+
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
363
|
+
const agentsPath = join5(ctx.projectRoot, "AGENTS.md");
|
|
364
|
+
const configPath = join5(ctx.projectRoot, "openspec", "config.yaml");
|
|
365
|
+
const existingAgents = await readOptional(agentsPath);
|
|
366
|
+
const warnings = [...scan.warnings];
|
|
367
|
+
if (existingAgents && hasInvalidManagedAutoRegion(existingAgents)) {
|
|
368
|
+
throw new FetError({
|
|
369
|
+
code: "CONFIG_INVALID" /* ConfigInvalid */,
|
|
370
|
+
message: "AGENTS.md \u7684 FET \u6258\u7BA1\u6807\u8BB0\u635F\u574F\u6216\u91CD\u590D",
|
|
371
|
+
details: { path: "AGENTS.md" },
|
|
372
|
+
suggestedCommand: "\u624B\u52A8\u4FEE\u590D FET:BEGIN AUTO / FET:END AUTO \u6807\u8BB0\u540E\u91CD\u8BD5"
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
if (existingAgents && !hasManagedAutoRegion(existingAgents)) {
|
|
376
|
+
if (!ctx.yes) {
|
|
377
|
+
throw new FetError({
|
|
378
|
+
code: "TOOL_ADAPTER_CONFLICT" /* ToolAdapterConflict */,
|
|
379
|
+
message: "AGENTS.md \u5DF2\u5B58\u5728\u4E14\u4E0D\u5305\u542B FET \u6258\u7BA1\u533A\u57DF",
|
|
380
|
+
details: { path: "AGENTS.md" },
|
|
381
|
+
suggestedCommand: ctx.command === "init" ? "\u786E\u8BA4\u53EF\u5907\u4EFD\u5E76\u66FF\u6362\u540E\u8FD0\u884C fet init --yes" : "\u786E\u8BA4\u53EF\u5907\u4EFD\u5E76\u66FF\u6362\u540E\u8FD0\u884C fet update-context --yes"
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
const backupPath = await createBackup(agentsPath);
|
|
385
|
+
if (backupPath) {
|
|
386
|
+
warnings.push(`\u5DF2\u5907\u4EFD\u975E\u6258\u7BA1 AGENTS.md \u5230 ${backupPath}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
await atomicWrite(agentsPath, replaceManagedRegion(existingAgents, renderAgentsMd(scan)));
|
|
390
|
+
await atomicWrite(configPath, await mergeFetConfig(configPath, renderFetConfig(scan)));
|
|
391
|
+
const state = await ctx.stateStore.getOrCreateGlobal();
|
|
392
|
+
state.context = {
|
|
393
|
+
agentsMdUpdatedAt: scan.generatedAt,
|
|
394
|
+
configUpdatedAt: scan.generatedAt,
|
|
395
|
+
scannerVersion: scan.scannerVersion
|
|
396
|
+
};
|
|
397
|
+
await ctx.stateStore.writeGlobal(state);
|
|
398
|
+
return { warnings };
|
|
399
|
+
}
|
|
400
|
+
async function readOptional(path) {
|
|
401
|
+
try {
|
|
402
|
+
return await readFile4(path, "utf8");
|
|
403
|
+
} catch {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/commands/init.ts
|
|
409
|
+
async function initCommand(ctx) {
|
|
410
|
+
const alreadyInitialized = await exists(join6(ctx.projectRoot, "openspec", "config.yaml"));
|
|
411
|
+
await withProjectLock(
|
|
412
|
+
ctx.projectRoot,
|
|
413
|
+
{ command: "init", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
|
|
414
|
+
async () => {
|
|
415
|
+
const journal = createInitJournal(ctx.fetVersion);
|
|
416
|
+
await writeInitJournal(ctx.projectRoot, journal);
|
|
417
|
+
const identity = await ctx.openSpec.resolveExecutable();
|
|
418
|
+
if (!alreadyInitialized) {
|
|
419
|
+
const result = await ctx.openSpec.run("init", ["--tools", "none"], { cwd: ctx.projectRoot, stdio: "inherit" });
|
|
420
|
+
if (result.exitCode !== 0) {
|
|
421
|
+
process.exitCode = result.exitCode;
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const contextResult = await updateContextFiles(ctx);
|
|
426
|
+
await ensureGitignore(ctx);
|
|
427
|
+
const state = await ctx.stateStore.getOrCreateGlobal();
|
|
428
|
+
state.openspec = identity;
|
|
429
|
+
for (const adapter of ctx.toolAdapters) {
|
|
430
|
+
const plan = await adapter.planInstall(ctx.projectRoot);
|
|
431
|
+
const result = await adapter.install(ctx.projectRoot, plan, ctx.yes);
|
|
432
|
+
state.toolAdapters[adapter.tool] = {
|
|
433
|
+
adapterVersion: adapter.adapterVersion,
|
|
434
|
+
installed: true,
|
|
435
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
436
|
+
};
|
|
437
|
+
journal.steps.push(...result.written.map((path) => ({ operation: "write", path, status: "done" })));
|
|
438
|
+
}
|
|
439
|
+
journal.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
440
|
+
await writeInitJournal(ctx.projectRoot, journal);
|
|
441
|
+
await ctx.stateStore.writeGlobal(state);
|
|
442
|
+
for (const warning of contextResult.warnings) {
|
|
443
|
+
ctx.output.warn(warning);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
);
|
|
447
|
+
ctx.output.result({
|
|
448
|
+
ok: true,
|
|
449
|
+
command: "init",
|
|
450
|
+
summary: "FET \u521D\u59CB\u5316\u5B8C\u6210\u3002",
|
|
451
|
+
nextSteps: ["\u4F7F\u7528 fet propose/new \u521B\u5EFA OpenSpec change", "\u4F7F\u7528 fet doctor \u68C0\u67E5\u9879\u76EE\u72B6\u6001"]
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
async function ensureGitignore(ctx) {
|
|
455
|
+
const gitignorePath = join6(ctx.projectRoot, ".gitignore");
|
|
456
|
+
const existing = await readOptional2(gitignorePath);
|
|
457
|
+
await atomicWrite(gitignorePath, mergeGitignore(existing));
|
|
458
|
+
}
|
|
459
|
+
async function readOptional2(path) {
|
|
460
|
+
try {
|
|
461
|
+
return await readFile5(path, "utf8");
|
|
462
|
+
} catch {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
async function exists(path) {
|
|
467
|
+
try {
|
|
468
|
+
await stat2(path);
|
|
469
|
+
return true;
|
|
470
|
+
} catch {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/commands/doctor.ts
|
|
476
|
+
import { stat as stat3 } from "fs/promises";
|
|
477
|
+
import { join as join7 } from "path";
|
|
478
|
+
async function doctorCommand(ctx, options = {}) {
|
|
479
|
+
const checks = [];
|
|
480
|
+
checks.push(await checkOpenSpec(ctx));
|
|
481
|
+
checks.push(await checkState(ctx));
|
|
482
|
+
checks.push(await checkFile("agents", join7(ctx.projectRoot, "AGENTS.md"), "AGENTS.md \u7F3A\u5931", "fet update-context"));
|
|
483
|
+
checks.push(await checkFile("config", join7(ctx.projectRoot, "openspec", "config.yaml"), "openspec/config.yaml \u7F3A\u5931", "fet init"));
|
|
484
|
+
for (const adapter of ctx.toolAdapters) {
|
|
485
|
+
checks.push(...await adapter.doctor(ctx.projectRoot));
|
|
486
|
+
}
|
|
487
|
+
const lockPath = join7(ctx.projectRoot, "openspec", ".fet.lock");
|
|
488
|
+
if (await exists2(lockPath)) {
|
|
489
|
+
if (options.fixLock) {
|
|
490
|
+
await clearLock(ctx.projectRoot);
|
|
491
|
+
checks.push({ id: "lock", status: "pass", message: "\u5DF2\u6E05\u7406 openspec/.fet.lock" });
|
|
492
|
+
} else {
|
|
493
|
+
checks.push({ id: "lock", status: "warn", message: "\u5B58\u5728 openspec/.fet.lock", suggestedCommand: "fet doctor --fix-lock" });
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
const warnings = checks.filter((check) => check.status !== "pass").map((check) => check.message);
|
|
497
|
+
ctx.output.result({
|
|
498
|
+
ok: true,
|
|
499
|
+
command: "doctor",
|
|
500
|
+
summary: warnings.length ? `\u8BCA\u65AD\u5B8C\u6210\uFF0C\u53D1\u73B0 ${warnings.length} \u4E2A\u9700\u8981\u5173\u6CE8\u7684\u95EE\u9898\u3002` : "\u8BCA\u65AD\u5B8C\u6210\uFF0C\u672A\u53D1\u73B0\u660E\u663E\u95EE\u9898\u3002",
|
|
501
|
+
warnings,
|
|
502
|
+
data: checks
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
async function checkOpenSpec(ctx) {
|
|
506
|
+
try {
|
|
507
|
+
const identity = await ctx.openSpec.resolveExecutable();
|
|
508
|
+
return { id: "openspec", status: "pass", message: `OpenSpec: ${identity.executablePath} (${identity.version})` };
|
|
509
|
+
} catch (error) {
|
|
510
|
+
return { id: "openspec", status: "fail", message: error instanceof Error ? error.message : "OpenSpec \u68C0\u6D4B\u5931\u8D25" };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async function checkState(ctx) {
|
|
514
|
+
try {
|
|
515
|
+
const state = await ctx.stateStore.readGlobal();
|
|
516
|
+
return state ? { id: "state", status: "pass", message: "FET \u5168\u5C40\u72B6\u6001\u53EF\u8BFB\u53D6" } : { id: "state", status: "warn", message: "FET \u5168\u5C40\u72B6\u6001\u5C1A\u672A\u521D\u59CB\u5316", suggestedCommand: "fet init" };
|
|
517
|
+
} catch (error) {
|
|
518
|
+
return { id: "state", status: "fail", message: error instanceof Error ? error.message : "FET \u72B6\u6001\u8BFB\u53D6\u5931\u8D25" };
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async function checkFile(id, path, missing, suggestedCommand) {
|
|
522
|
+
return await exists2(path) ? { id, status: "pass", message: `${id} \u5B58\u5728` } : { id, status: "warn", message: missing, suggestedCommand };
|
|
523
|
+
}
|
|
524
|
+
async function exists2(path) {
|
|
525
|
+
try {
|
|
526
|
+
await stat3(path);
|
|
527
|
+
return true;
|
|
528
|
+
} catch {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/state/project.ts
|
|
534
|
+
import { execFile } from "child_process";
|
|
535
|
+
import { promisify } from "util";
|
|
536
|
+
var execFileAsync = promisify(execFile);
|
|
537
|
+
async function detectProjectIdentity(projectRoot) {
|
|
538
|
+
const [gitRoot, branch, headCommit] = await Promise.all([
|
|
539
|
+
git(projectRoot, ["rev-parse", "--show-toplevel"]),
|
|
540
|
+
git(projectRoot, ["branch", "--show-current"]),
|
|
541
|
+
git(projectRoot, ["rev-parse", "HEAD"])
|
|
542
|
+
]);
|
|
543
|
+
return {
|
|
544
|
+
gitRoot,
|
|
545
|
+
worktreePath: projectRoot,
|
|
546
|
+
branch,
|
|
547
|
+
headCommit
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
async function git(cwd, args) {
|
|
551
|
+
try {
|
|
552
|
+
const { stdout } = await execFileAsync("git", args, { cwd });
|
|
553
|
+
return stdout.trim() || null;
|
|
554
|
+
} catch {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/state/store.ts
|
|
560
|
+
import { mkdir as mkdir3, readFile as readFile6 } from "fs/promises";
|
|
561
|
+
import { join as join8 } from "path";
|
|
562
|
+
|
|
563
|
+
// src/state/schema.ts
|
|
564
|
+
var phases = ["explore", "propose", "implement", "verify", "sync", "archive"];
|
|
565
|
+
function createGlobalState(fetVersion, project) {
|
|
566
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
567
|
+
return {
|
|
568
|
+
schemaVersion: 1,
|
|
569
|
+
fetVersion,
|
|
570
|
+
createdAt: now,
|
|
571
|
+
updatedAt: now,
|
|
572
|
+
project,
|
|
573
|
+
openspec: null,
|
|
574
|
+
activeChangeId: null,
|
|
575
|
+
openChangeIds: [],
|
|
576
|
+
context: {
|
|
577
|
+
agentsMdUpdatedAt: null,
|
|
578
|
+
configUpdatedAt: null,
|
|
579
|
+
scannerVersion: 1
|
|
580
|
+
},
|
|
581
|
+
toolAdapters: {},
|
|
582
|
+
verifyAuthorization: null,
|
|
583
|
+
lastDoctor: null
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function createChangeState(fetVersion, changeId, phase) {
|
|
587
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
588
|
+
return {
|
|
589
|
+
schemaVersion: 1,
|
|
590
|
+
fetVersion,
|
|
591
|
+
changeId,
|
|
592
|
+
createdAt: now,
|
|
593
|
+
updatedAt: now,
|
|
594
|
+
currentPhase: phase,
|
|
595
|
+
phases: Object.fromEntries(
|
|
596
|
+
phases.map((item) => [item, { status: item === phase ? "in_progress" : "not_started" }])
|
|
597
|
+
),
|
|
598
|
+
tasks: {
|
|
599
|
+
source: "tasks.md",
|
|
600
|
+
completedIds: [],
|
|
601
|
+
lastSyncedAt: null
|
|
602
|
+
},
|
|
603
|
+
manualVerify: null,
|
|
604
|
+
lastOpenSpecCommand: null,
|
|
605
|
+
warnings: []
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function assertGlobalState(value) {
|
|
609
|
+
if (!isRecord(value) || value.schemaVersion !== 1) {
|
|
610
|
+
throw unsupportedSchema("\u5168\u5C40\u72B6\u6001 schema \u4E0D\u53D7\u652F\u6301");
|
|
611
|
+
}
|
|
612
|
+
if (typeof value.fetVersion !== "string" || !isRecord(value.project)) {
|
|
613
|
+
throw corruptedState("\u5168\u5C40\u72B6\u6001\u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5");
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
function assertChangeState(value) {
|
|
617
|
+
if (!isRecord(value) || value.schemaVersion !== 1) {
|
|
618
|
+
throw unsupportedSchema("\u53D8\u66F4\u72B6\u6001 schema \u4E0D\u53D7\u652F\u6301");
|
|
619
|
+
}
|
|
620
|
+
if (typeof value.fetVersion !== "string" || typeof value.changeId !== "string") {
|
|
621
|
+
throw corruptedState("\u53D8\u66F4\u72B6\u6001\u7F3A\u5C11\u5FC5\u586B\u5B57\u6BB5");
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function unsupportedSchema(message) {
|
|
625
|
+
return new FetError({
|
|
626
|
+
code: "STATE_SCHEMA_UNSUPPORTED" /* StateSchemaUnsupported */,
|
|
627
|
+
message,
|
|
628
|
+
suggestedCommand: "npm update -g @nick848/fet"
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
function corruptedState(message) {
|
|
632
|
+
return new FetError({
|
|
633
|
+
code: "STATE_CORRUPTED" /* StateCorrupted */,
|
|
634
|
+
message,
|
|
635
|
+
suggestedCommand: "fet doctor --fix"
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
function isRecord(value) {
|
|
639
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/state/store.ts
|
|
643
|
+
var StateStore = class {
|
|
644
|
+
constructor(projectRoot, fetVersion, project) {
|
|
645
|
+
this.projectRoot = projectRoot;
|
|
646
|
+
this.fetVersion = fetVersion;
|
|
647
|
+
this.project = project;
|
|
648
|
+
}
|
|
649
|
+
projectRoot;
|
|
650
|
+
fetVersion;
|
|
651
|
+
project;
|
|
652
|
+
async readGlobal() {
|
|
653
|
+
try {
|
|
654
|
+
const value = JSON.parse(await readFile6(this.globalPath(), "utf8"));
|
|
655
|
+
assertGlobalState(value);
|
|
656
|
+
return value;
|
|
657
|
+
} catch (error) {
|
|
658
|
+
if (isNotFound(error)) {
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
throw error;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
async getOrCreateGlobal() {
|
|
665
|
+
return await this.readGlobal() ?? createGlobalState(this.fetVersion, this.project);
|
|
666
|
+
}
|
|
667
|
+
async writeGlobal(state) {
|
|
668
|
+
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
669
|
+
await mkdir3(join8(this.projectRoot, "openspec"), { recursive: true });
|
|
670
|
+
await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
|
|
671
|
+
`);
|
|
672
|
+
}
|
|
673
|
+
async readChange(changeId) {
|
|
674
|
+
try {
|
|
675
|
+
const value = JSON.parse(await readFile6(this.changePath(changeId), "utf8"));
|
|
676
|
+
assertChangeState(value);
|
|
677
|
+
return value;
|
|
678
|
+
} catch (error) {
|
|
679
|
+
if (isNotFound(error)) {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
throw error;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
async getOrCreateChange(changeId, phase) {
|
|
686
|
+
return await this.readChange(changeId) ?? createChangeState(this.fetVersion, changeId, phase);
|
|
687
|
+
}
|
|
688
|
+
async writeChange(state) {
|
|
689
|
+
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
690
|
+
await mkdir3(join8(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
|
|
691
|
+
await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
|
|
692
|
+
`);
|
|
693
|
+
}
|
|
694
|
+
globalPath() {
|
|
695
|
+
return join8(this.projectRoot, "openspec", "fet-state.json");
|
|
696
|
+
}
|
|
697
|
+
changePath(changeId) {
|
|
698
|
+
return join8(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
function isNotFound(error) {
|
|
702
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// src/state/tasks.ts
|
|
706
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
707
|
+
async function readCompletedTaskIds(tasksPath) {
|
|
708
|
+
let content;
|
|
709
|
+
try {
|
|
710
|
+
content = await readFile7(tasksPath, "utf8");
|
|
711
|
+
} catch {
|
|
712
|
+
return [];
|
|
713
|
+
}
|
|
714
|
+
const completed = [];
|
|
715
|
+
const pattern = /^\s*[-*]\s+\[[xX]\]\s+([0-9]+(?:\.[0-9]+)*)/gm;
|
|
716
|
+
let match;
|
|
717
|
+
while (match = pattern.exec(content)) {
|
|
718
|
+
if (match[1]) {
|
|
719
|
+
completed.push(match[1]);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return completed;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// src/commands/proxy.ts
|
|
726
|
+
var phaseByCommand = {
|
|
727
|
+
explore: "explore",
|
|
728
|
+
propose: "propose",
|
|
729
|
+
new: "propose",
|
|
730
|
+
continue: "propose",
|
|
731
|
+
ff: "propose",
|
|
732
|
+
apply: "implement",
|
|
733
|
+
verify: "verify",
|
|
734
|
+
sync: "sync",
|
|
735
|
+
archive: "archive",
|
|
736
|
+
"bulk-archive": "archive",
|
|
737
|
+
onboard: "explore"
|
|
738
|
+
};
|
|
739
|
+
async function proxyCommand(ctx, command, args) {
|
|
740
|
+
const openSpecArgs = stripFetOptions(args);
|
|
741
|
+
await withProjectLock(
|
|
742
|
+
ctx.projectRoot,
|
|
743
|
+
{ command, cwd: ctx.cwd, fetVersion: ctx.fetVersion },
|
|
744
|
+
async () => {
|
|
745
|
+
if (["sync", "archive", "bulk-archive"].includes(command)) {
|
|
746
|
+
await assertVerified(ctx);
|
|
747
|
+
}
|
|
748
|
+
const mapped = await mapOpenSpecCommand(ctx, command, openSpecArgs);
|
|
749
|
+
const result = await ctx.openSpec.run(mapped.command, mapped.args, { cwd: ctx.projectRoot, stdio: ctx.json ? "pipe" : "inherit" });
|
|
750
|
+
if (result.exitCode !== 0) {
|
|
751
|
+
throw new FetError({
|
|
752
|
+
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
753
|
+
message: `OpenSpec ${command} \u6267\u884C\u5931\u8D25`,
|
|
754
|
+
details: result,
|
|
755
|
+
recoverable: true
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
const inspection = await ctx.openSpec.inspectProject(ctx.projectRoot);
|
|
759
|
+
const state = await ctx.stateStore.getOrCreateGlobal();
|
|
760
|
+
state.openChangeIds = inspection.changes;
|
|
761
|
+
if (ctx.changeId && inspection.changes.includes(ctx.changeId)) {
|
|
762
|
+
state.activeChangeId = ctx.changeId;
|
|
763
|
+
} else if (state.activeChangeId && !inspection.changes.includes(state.activeChangeId)) {
|
|
764
|
+
state.activeChangeId = inspection.changes.length === 1 ? inspection.changes[0] ?? null : null;
|
|
765
|
+
} else if (!state.activeChangeId && inspection.changes.length === 1) {
|
|
766
|
+
state.activeChangeId = inspection.changes[0] ?? null;
|
|
767
|
+
}
|
|
768
|
+
await ctx.stateStore.writeGlobal(state);
|
|
769
|
+
const changeId = ctx.changeId ?? state.activeChangeId;
|
|
770
|
+
if (changeId && inspection.changes.includes(changeId)) {
|
|
771
|
+
const changeInspection = await ctx.openSpec.inspectChange(ctx.projectRoot, changeId);
|
|
772
|
+
const changeState = await ctx.stateStore.getOrCreateChange(changeId, phaseByCommand[command] ?? "propose");
|
|
773
|
+
changeState.currentPhase = phaseByCommand[command] ?? changeState.currentPhase;
|
|
774
|
+
changeState.phases[changeState.currentPhase] = {
|
|
775
|
+
status: "done",
|
|
776
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
777
|
+
};
|
|
778
|
+
changeState.lastOpenSpecCommand = {
|
|
779
|
+
command: mapped.command,
|
|
780
|
+
args: mapped.args,
|
|
781
|
+
exitCode: result.exitCode,
|
|
782
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
783
|
+
};
|
|
784
|
+
changeState.tasks.completedIds = await readCompletedTaskIds(changeInspection.tasksPath);
|
|
785
|
+
changeState.tasks.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
786
|
+
await ctx.stateStore.writeChange(changeState);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
);
|
|
790
|
+
ctx.output.result({
|
|
791
|
+
ok: true,
|
|
792
|
+
command,
|
|
793
|
+
summary: `fet ${command} \u5B8C\u6210\u3002`
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
async function passthroughCommand(ctx, command, args) {
|
|
797
|
+
const result = await ctx.openSpec.run(command, stripFetOptions(args), { cwd: ctx.projectRoot, stdio: ctx.json ? "pipe" : "inherit" });
|
|
798
|
+
if (result.exitCode !== 0) {
|
|
799
|
+
throw new FetError({
|
|
800
|
+
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
801
|
+
message: `OpenSpec ${command} \u6267\u884C\u5931\u8D25`,
|
|
802
|
+
details: result
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function stripFetOptions(args) {
|
|
807
|
+
const result = [];
|
|
808
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
809
|
+
const arg = args[index];
|
|
810
|
+
if (arg === "--cwd" || arg === "--change") {
|
|
811
|
+
index += 1;
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
if (arg?.startsWith("--cwd=") || arg?.startsWith("--change=")) {
|
|
815
|
+
continue;
|
|
816
|
+
}
|
|
817
|
+
if (arg === "--yes" || arg === "--json" || arg === "--verbose" || arg === "--no-color") {
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
if (arg) {
|
|
821
|
+
result.push(arg);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return result;
|
|
825
|
+
}
|
|
826
|
+
async function mapOpenSpecCommand(ctx, command, args) {
|
|
827
|
+
switch (command) {
|
|
828
|
+
case "propose":
|
|
829
|
+
case "new":
|
|
830
|
+
return { command: "new", args: args[0] === "change" ? args : ["change", ...args] };
|
|
831
|
+
case "continue":
|
|
832
|
+
return { command: "instructions", args: [...withoutUndefined(args[0] ? [args[0]] : ["proposal"]), "--change", await requireChangeId(ctx)] };
|
|
833
|
+
case "ff":
|
|
834
|
+
return { command: "status", args: ["--change", await requireChangeId(ctx)] };
|
|
835
|
+
case "apply":
|
|
836
|
+
return { command: "instructions", args: ["apply", "--change", await requireChangeId(ctx)] };
|
|
837
|
+
case "sync":
|
|
838
|
+
return { command: "validate", args: [ctx.changeId ?? args[0] ?? await requireChangeId(ctx), "--type", "change", "--strict"] };
|
|
839
|
+
case "archive":
|
|
840
|
+
return { command: "archive", args: [ctx.changeId ?? args[0] ?? await requireChangeId(ctx), ...args.slice(ctx.changeId ? 0 : 1)] };
|
|
841
|
+
case "bulk-archive":
|
|
842
|
+
throw new FetError({
|
|
843
|
+
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
844
|
+
message: "OpenSpec 1.2.0 \u4E0D\u63D0\u4F9B bulk-archive \u9876\u5C42\u547D\u4EE4",
|
|
845
|
+
suggestedCommand: "\u9010\u4E2A\u6267\u884C fet archive --change <change-id>"
|
|
846
|
+
});
|
|
847
|
+
case "explore":
|
|
848
|
+
return { command: "instructions", args: ["proposal", "--change", await requireChangeId(ctx)] };
|
|
849
|
+
case "onboard":
|
|
850
|
+
return { command: "instructions", args: [] };
|
|
851
|
+
default:
|
|
852
|
+
return { command, args };
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
async function requireChangeId(ctx) {
|
|
856
|
+
if (ctx.changeId) {
|
|
857
|
+
return ctx.changeId;
|
|
858
|
+
}
|
|
859
|
+
const state = await ctx.stateStore.getOrCreateGlobal();
|
|
860
|
+
if (state.activeChangeId) {
|
|
861
|
+
return state.activeChangeId;
|
|
862
|
+
}
|
|
863
|
+
const inspection = await ctx.openSpec.inspectProject(ctx.projectRoot);
|
|
864
|
+
if (inspection.changes.length === 1 && inspection.changes[0]) {
|
|
865
|
+
return inspection.changes[0];
|
|
866
|
+
}
|
|
867
|
+
throw new FetError({
|
|
868
|
+
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
869
|
+
message: "\u8BE5\u547D\u4EE4\u9700\u8981\u660E\u786E\u7684 change",
|
|
870
|
+
details: { openChangeIds: inspection.changes },
|
|
871
|
+
suggestedCommand: "\u6DFB\u52A0 --change <change-id>"
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
function withoutUndefined(values) {
|
|
875
|
+
return values.filter(Boolean);
|
|
876
|
+
}
|
|
877
|
+
async function assertVerified(ctx) {
|
|
878
|
+
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
879
|
+
const changeId = ctx.changeId ?? global.activeChangeId;
|
|
880
|
+
if (!changeId) {
|
|
881
|
+
throw new FetError({
|
|
882
|
+
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
883
|
+
message: "\u672A\u6307\u5B9A change\uFF0C\u65E0\u6CD5\u68C0\u67E5 verify \u72B6\u6001",
|
|
884
|
+
suggestedCommand: "fet verify --done --change <change-id>"
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
const change = await ctx.stateStore.readChange(changeId);
|
|
888
|
+
const inspection = await ctx.openSpec.inspectProject(ctx.projectRoot);
|
|
889
|
+
if (!inspection.changes.includes(changeId)) {
|
|
890
|
+
throw new FetError({
|
|
891
|
+
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
892
|
+
message: "\u6307\u5B9A\u7684 change \u4E0D\u5B58\u5728\u6216\u5DF2\u5F52\u6863",
|
|
893
|
+
details: { changeId, openChangeIds: inspection.changes },
|
|
894
|
+
suggestedCommand: "fet doctor"
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
if (change?.manualVerify?.status !== "declared_done") {
|
|
898
|
+
throw new FetError({
|
|
899
|
+
code: "STATE_CORRUPTED" /* StateCorrupted */,
|
|
900
|
+
message: "\u5F53\u524D change \u5C1A\u672A\u901A\u8FC7 FET verify",
|
|
901
|
+
details: { changeId },
|
|
902
|
+
suggestedCommand: `fet verify --change ${changeId}`
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// src/commands/verify.ts
|
|
908
|
+
import { createHash } from "crypto";
|
|
909
|
+
import { mkdir as mkdir4, readFile as readFile8, stat as stat4 } from "fs/promises";
|
|
910
|
+
import { join as join9 } from "path";
|
|
911
|
+
async function verifyCommand(ctx, options) {
|
|
912
|
+
if (options.auto) {
|
|
913
|
+
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
914
|
+
const plan = {
|
|
915
|
+
schemaVersion: 1,
|
|
916
|
+
packageManager: scan.project.packageManager,
|
|
917
|
+
workspaces: [
|
|
918
|
+
{
|
|
919
|
+
name: "root",
|
|
920
|
+
cwd: ".",
|
|
921
|
+
commands: Object.entries(scan.commands).filter(([name]) => ["lint", "typecheck", "test"].includes(name)).map(([dimension, command]) => ({
|
|
922
|
+
dimension,
|
|
923
|
+
command: command.command,
|
|
924
|
+
source: command.source,
|
|
925
|
+
required: command.required,
|
|
926
|
+
statusIfMissing: command.required ? "fail" : "warn"
|
|
927
|
+
}))
|
|
928
|
+
}
|
|
929
|
+
],
|
|
930
|
+
missing: ["lint", "typecheck", "test"].filter((name) => !scan.commands[name])
|
|
931
|
+
};
|
|
932
|
+
if (ctx.yes) {
|
|
933
|
+
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
934
|
+
global.verifyAuthorization = {
|
|
935
|
+
schemaVersion: 1,
|
|
936
|
+
approvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
937
|
+
commandFingerprint: fingerprint(plan),
|
|
938
|
+
packageManager: plan.packageManager,
|
|
939
|
+
plan: plan.workspaces.flatMap(
|
|
940
|
+
(workspace) => workspace.commands.map((command) => ({
|
|
941
|
+
cwd: workspace.cwd,
|
|
942
|
+
dimension: command.dimension,
|
|
943
|
+
command: command.command,
|
|
944
|
+
source: command.source,
|
|
945
|
+
required: command.required
|
|
946
|
+
}))
|
|
947
|
+
)
|
|
948
|
+
};
|
|
949
|
+
await ctx.stateStore.writeGlobal(global);
|
|
950
|
+
}
|
|
951
|
+
ctx.output.result({
|
|
952
|
+
ok: true,
|
|
953
|
+
command: "verify",
|
|
954
|
+
summary: ctx.yes ? "\u5DF2\u786E\u8BA4 verify --auto \u6267\u884C\u8BA1\u5212\uFF1BMVP \u6682\u4E0D\u6267\u884C\u4ED3\u5E93\u811A\u672C\u3002" : "\u5DF2\u751F\u6210 verify --auto \u6267\u884C\u8BA1\u5212\uFF1BMVP \u6682\u4E0D\u6267\u884C\u4ED3\u5E93\u811A\u672C\u3002",
|
|
955
|
+
warnings: plan.missing.map((name) => `\u672A\u53D1\u73B0 ${name} \u811A\u672C\uFF0C\u5C06\u5728\u81EA\u52A8\u6267\u884C\u7248\u672C\u4E2D\u6309\u914D\u7F6E\u5904\u7406\u3002`),
|
|
956
|
+
data: plan,
|
|
957
|
+
nextSteps: ctx.yes ? ["\u5F53\u524D\u7248\u672C\u8BF7\u8FD0\u884C fet verify \u8FDB\u5165\u624B\u52A8\u9A8C\u8BC1\u6A21\u5F0F"] : ["\u5BA1\u6838\u6267\u884C\u8BA1\u5212", "\u786E\u8BA4\u8BA1\u5212\u540E\u53EF\u8FD0\u884C fet verify --auto --yes", "\u5F53\u524D\u7248\u672C\u8BF7\u8FD0\u884C fet verify \u8FDB\u5165\u624B\u52A8\u9A8C\u8BC1\u6A21\u5F0F"]
|
|
958
|
+
});
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
await withProjectLock(
|
|
962
|
+
ctx.projectRoot,
|
|
963
|
+
{ command: "verify", cwd: ctx.cwd, fetVersion: ctx.fetVersion },
|
|
964
|
+
async () => {
|
|
965
|
+
const changeId = await resolveChangeId(ctx);
|
|
966
|
+
if (options.done) {
|
|
967
|
+
await markDone(ctx, changeId);
|
|
968
|
+
} else {
|
|
969
|
+
await writeInstructions(ctx, changeId);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
async function writeInstructions(ctx, changeId) {
|
|
975
|
+
await assertChangeExists(ctx, changeId);
|
|
976
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
977
|
+
const dir = join9(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
978
|
+
const instructionsPath = join9(dir, "verify-instructions.md");
|
|
979
|
+
await mkdir4(dir, { recursive: true });
|
|
980
|
+
await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
|
|
981
|
+
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
982
|
+
state.currentPhase = "verify";
|
|
983
|
+
state.phases.verify = { status: "in_progress", updatedAt: generatedAt };
|
|
984
|
+
await ctx.stateStore.writeChange(state);
|
|
985
|
+
ctx.output.result({
|
|
986
|
+
ok: true,
|
|
987
|
+
command: "verify",
|
|
988
|
+
summary: "\u5DF2\u751F\u6210\u624B\u52A8\u9A8C\u8BC1\u6307\u4EE4\u3002",
|
|
989
|
+
nextSteps: [`\u9605\u8BFB openspec/changes/${changeId}/.fet/verify-instructions.md`, `\u5B8C\u6210\u540E\u8FD0\u884C fet verify --done --change ${changeId}`]
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
async function markDone(ctx, changeId) {
|
|
993
|
+
await assertChangeExists(ctx, changeId);
|
|
994
|
+
const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
995
|
+
const instructionsPath = join9(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
|
|
996
|
+
const instructions = await readInstructions(instructionsPath, changeId);
|
|
997
|
+
const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
|
|
998
|
+
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
999
|
+
state.currentPhase = "verify";
|
|
1000
|
+
state.phases.verify = { status: "done", updatedAt: declaredAt };
|
|
1001
|
+
state.manualVerify = {
|
|
1002
|
+
mode: "manual",
|
|
1003
|
+
status: "declared_done",
|
|
1004
|
+
declaredAt,
|
|
1005
|
+
instructionsPath: `openspec/changes/${changeId}/.fet/verify-instructions.md`,
|
|
1006
|
+
instructionsGeneratedAt,
|
|
1007
|
+
evidence: null
|
|
1008
|
+
};
|
|
1009
|
+
await ctx.stateStore.writeChange(state);
|
|
1010
|
+
ctx.output.result({
|
|
1011
|
+
ok: true,
|
|
1012
|
+
command: "verify",
|
|
1013
|
+
summary: "\u5DF2\u8BB0\u5F55\u624B\u52A8\u9A8C\u8BC1\u5B8C\u6210\u58F0\u660E\u3002",
|
|
1014
|
+
nextSteps: [`fet sync --change ${changeId}`, `fet archive --change ${changeId}`]
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
async function assertChangeExists(ctx, changeId) {
|
|
1018
|
+
const inspection = await ctx.openSpec.inspectChange(ctx.projectRoot, changeId);
|
|
1019
|
+
if (!inspection.exists) {
|
|
1020
|
+
throw new FetError({
|
|
1021
|
+
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
1022
|
+
message: "\u6307\u5B9A\u7684 change \u4E0D\u5B58\u5728",
|
|
1023
|
+
details: { changeId },
|
|
1024
|
+
suggestedCommand: "fet verify --change <change-id>"
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
async function readInstructions(path, changeId) {
|
|
1029
|
+
try {
|
|
1030
|
+
await stat4(path);
|
|
1031
|
+
const content = await readFile8(path, "utf8");
|
|
1032
|
+
const fileChangeId = readFrontMatterValue(content, "changeId");
|
|
1033
|
+
if (fileChangeId !== changeId) {
|
|
1034
|
+
throw new FetError({
|
|
1035
|
+
code: "STATE_CORRUPTED" /* StateCorrupted */,
|
|
1036
|
+
message: "\u9A8C\u8BC1\u6307\u4EE4\u6587\u4EF6\u4E0E\u5F53\u524D change \u4E0D\u5339\u914D",
|
|
1037
|
+
details: { expected: changeId, actual: fileChangeId },
|
|
1038
|
+
suggestedCommand: `fet verify --change ${changeId}`
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
return content;
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
if (error instanceof FetError) {
|
|
1044
|
+
throw error;
|
|
1045
|
+
}
|
|
1046
|
+
throw new FetError({
|
|
1047
|
+
code: "STATE_CORRUPTED" /* StateCorrupted */,
|
|
1048
|
+
message: "\u9A8C\u8BC1\u6307\u4EE4\u6587\u4EF6\u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u8BFB\u53D6",
|
|
1049
|
+
details: { path },
|
|
1050
|
+
suggestedCommand: `fet verify --change ${changeId}`
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
function readFrontMatterValue(content, key) {
|
|
1055
|
+
const match = content.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
|
|
1056
|
+
return match?.[1]?.trim() ?? null;
|
|
1057
|
+
}
|
|
1058
|
+
function fingerprint(value) {
|
|
1059
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(value)).digest("hex")}`;
|
|
1060
|
+
}
|
|
1061
|
+
async function resolveChangeId(ctx) {
|
|
1062
|
+
if (ctx.changeId) {
|
|
1063
|
+
return ctx.changeId;
|
|
1064
|
+
}
|
|
1065
|
+
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
1066
|
+
if (global.activeChangeId) {
|
|
1067
|
+
return global.activeChangeId;
|
|
1068
|
+
}
|
|
1069
|
+
const inspection = await ctx.openSpec.inspectProject(ctx.projectRoot);
|
|
1070
|
+
if (inspection.changes.length === 1 && inspection.changes[0]) {
|
|
1071
|
+
return inspection.changes[0];
|
|
1072
|
+
}
|
|
1073
|
+
throw new FetError({
|
|
1074
|
+
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
1075
|
+
message: "\u65E0\u6CD5\u786E\u5B9A\u8981\u9A8C\u8BC1\u7684 change",
|
|
1076
|
+
details: { openChangeIds: inspection.changes },
|
|
1077
|
+
suggestedCommand: "fet verify --change <change-id>"
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// src/cli/context.ts
|
|
1082
|
+
import { resolve } from "path";
|
|
1083
|
+
|
|
1084
|
+
// src/adapters/cursor/index.ts
|
|
1085
|
+
import { mkdir as mkdir5, readFile as readFile9, stat as stat5 } from "fs/promises";
|
|
1086
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
1087
|
+
|
|
1088
|
+
// src/adapters/cursor/templates.ts
|
|
1089
|
+
var commands = [
|
|
1090
|
+
"explore",
|
|
1091
|
+
"propose",
|
|
1092
|
+
"new",
|
|
1093
|
+
"continue",
|
|
1094
|
+
"ff",
|
|
1095
|
+
"apply",
|
|
1096
|
+
"verify",
|
|
1097
|
+
"sync",
|
|
1098
|
+
"archive",
|
|
1099
|
+
"bulk-archive",
|
|
1100
|
+
"onboard"
|
|
1101
|
+
];
|
|
1102
|
+
function cursorSkillFiles() {
|
|
1103
|
+
return commands.map((command) => ({
|
|
1104
|
+
path: `.cursor/skills/fet-${command}/SKILL.md`,
|
|
1105
|
+
content: renderSkill(command)
|
|
1106
|
+
}));
|
|
1107
|
+
}
|
|
1108
|
+
function cursorRuleFile() {
|
|
1109
|
+
return {
|
|
1110
|
+
path: ".cursor/rules/fet-context.mdc",
|
|
1111
|
+
content: `<!-- FET:MANAGED
|
|
1112
|
+
schemaVersion: 1
|
|
1113
|
+
fetVersion: ${FET_VERSION}
|
|
1114
|
+
generator: cursor-adapter
|
|
1115
|
+
adapterVersion: 1
|
|
1116
|
+
FET:END -->
|
|
1117
|
+
|
|
1118
|
+
---
|
|
1119
|
+
description: Load FET project context for implementation tasks
|
|
1120
|
+
alwaysApply: false
|
|
1121
|
+
---
|
|
1122
|
+
|
|
1123
|
+
\u5F53\u7528\u6237\u8BF7\u6C42\u4FEE\u6539\u9879\u76EE\u3001\u5B9E\u73B0 OpenSpec change\u3001\u8FD0\u884C FET \u5DE5\u4F5C\u6D41\u6216\u89E3\u91CA\u9879\u76EE\u7ED3\u6784\u65F6\uFF0C\u4F18\u5148\u9605\u8BFB\uFF1A
|
|
1124
|
+
|
|
1125
|
+
- AGENTS.md
|
|
1126
|
+
- openspec/config.yaml
|
|
1127
|
+
- \u5F53\u524D change \u76EE\u5F55\u4E0B\u7684 OpenSpec \u89C4\u5212\u4EA7\u7269
|
|
1128
|
+
|
|
1129
|
+
\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
|
|
1130
|
+
`
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
function renderSkill(command) {
|
|
1134
|
+
return `<!-- FET:MANAGED
|
|
1135
|
+
schemaVersion: 1
|
|
1136
|
+
fetVersion: ${FET_VERSION}
|
|
1137
|
+
generator: cursor-adapter
|
|
1138
|
+
adapterVersion: 1
|
|
1139
|
+
command: fet ${command}
|
|
1140
|
+
FET:END -->
|
|
1141
|
+
|
|
1142
|
+
---
|
|
1143
|
+
name: fet-${command}
|
|
1144
|
+
description: Run FET-managed OpenSpec ${command} workflow from the terminal
|
|
1145
|
+
disable-model-invocation: true
|
|
1146
|
+
---
|
|
1147
|
+
|
|
1148
|
+
\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
|
|
1149
|
+
|
|
1150
|
+
\u8BF7\u5728\u7EC8\u7AEF\u4E2D\u6267\u884C\uFF1A
|
|
1151
|
+
|
|
1152
|
+
\`\`\`sh
|
|
1153
|
+
fet ${command}
|
|
1154
|
+
\`\`\`
|
|
1155
|
+
|
|
1156
|
+
\u6267\u884C\u524D\u8BF7\u786E\u8BA4\u5DF2\u9605\u8BFB AGENTS.md \u4E0E openspec/config.yaml\u3002
|
|
1157
|
+
`;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// src/adapters/cursor/index.ts
|
|
1161
|
+
var CursorAdapter = class {
|
|
1162
|
+
tool = "cursor";
|
|
1163
|
+
adapterVersion = 1;
|
|
1164
|
+
async detect(projectRoot) {
|
|
1165
|
+
return {
|
|
1166
|
+
detected: await exists3(join10(projectRoot, ".cursor")),
|
|
1167
|
+
reason: "Cursor adapter is available for any project"
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
async planInstall(_projectRoot) {
|
|
1171
|
+
return {
|
|
1172
|
+
tool: this.tool,
|
|
1173
|
+
files: [...cursorSkillFiles(), cursorRuleFile()].map((file) => ({
|
|
1174
|
+
...file,
|
|
1175
|
+
managed: true
|
|
1176
|
+
}))
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
async install(projectRoot, plan, force = false) {
|
|
1180
|
+
const written = [];
|
|
1181
|
+
const skipped = [];
|
|
1182
|
+
for (const file of plan.files) {
|
|
1183
|
+
const target = join10(projectRoot, file.path);
|
|
1184
|
+
const existing = await readExisting(target);
|
|
1185
|
+
if (existing && !existing.includes("FET:MANAGED") && !force) {
|
|
1186
|
+
throw new FetError({
|
|
1187
|
+
code: "TOOL_ADAPTER_CONFLICT" /* ToolAdapterConflict */,
|
|
1188
|
+
message: "Cursor \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\u4E14\u4E0D\u5F52 FET \u7BA1\u7406",
|
|
1189
|
+
details: { path: file.path },
|
|
1190
|
+
suggestedCommand: "fet init --yes"
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
1194
|
+
await createBackup(target);
|
|
1195
|
+
}
|
|
1196
|
+
await mkdir5(dirname5(target), { recursive: true });
|
|
1197
|
+
await atomicWrite(target, file.content);
|
|
1198
|
+
written.push(file.path);
|
|
1199
|
+
}
|
|
1200
|
+
return { tool: this.tool, written, skipped };
|
|
1201
|
+
}
|
|
1202
|
+
async doctor(projectRoot) {
|
|
1203
|
+
const plan = await this.planInstall(projectRoot);
|
|
1204
|
+
const checks = [];
|
|
1205
|
+
for (const file of plan.files) {
|
|
1206
|
+
const target = join10(projectRoot, file.path);
|
|
1207
|
+
const content = await readExisting(target);
|
|
1208
|
+
const managed = Boolean(content?.includes("FET:MANAGED"));
|
|
1209
|
+
const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
|
|
1210
|
+
checks.push({
|
|
1211
|
+
id: `cursor:${file.path}`,
|
|
1212
|
+
status: !content ? "warn" : managed && versionMatches ? "pass" : "warn",
|
|
1213
|
+
message: !content ? `${file.path} \u7F3A\u5931` : !managed ? `${file.path} \u5B58\u5728\u4F46\u4E0D\u5F52 FET \u7BA1\u7406` : !versionMatches ? `${file.path} adapterVersion \u5DF2\u8FC7\u671F` : `${file.path} \u5B58\u5728\u4E14\u7248\u672C\u5339\u914D`,
|
|
1214
|
+
suggestedCommand: !content || !managed || !versionMatches ? "fet init" : void 0
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
return checks;
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
async function readExisting(path) {
|
|
1221
|
+
try {
|
|
1222
|
+
return await readFile9(path, "utf8");
|
|
1223
|
+
} catch {
|
|
1224
|
+
return null;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
async function exists3(path) {
|
|
1228
|
+
try {
|
|
1229
|
+
await stat5(path);
|
|
1230
|
+
return true;
|
|
1231
|
+
} catch {
|
|
1232
|
+
return false;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// src/openspec/adapter.ts
|
|
1237
|
+
import { execFile as execFile3 } from "child_process";
|
|
1238
|
+
import { promisify as promisify3 } from "util";
|
|
1239
|
+
|
|
1240
|
+
// src/openspec/inspector.ts
|
|
1241
|
+
import { readdir, stat as stat6 } from "fs/promises";
|
|
1242
|
+
import { join as join11 } from "path";
|
|
1243
|
+
async function inspectOpenSpecProject(projectRoot) {
|
|
1244
|
+
const openspecPath = join11(projectRoot, "openspec");
|
|
1245
|
+
const changesPath = join11(openspecPath, "changes");
|
|
1246
|
+
const archivePath = join11(openspecPath, "archive");
|
|
1247
|
+
return {
|
|
1248
|
+
exists: await exists4(openspecPath),
|
|
1249
|
+
changes: await listDirectories(changesPath),
|
|
1250
|
+
archived: await listDirectories(archivePath)
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
1254
|
+
const changePath = join11(projectRoot, "openspec", "changes", changeId);
|
|
1255
|
+
const tasksPath = join11(changePath, "tasks.md");
|
|
1256
|
+
const specsPath = join11(changePath, "specs");
|
|
1257
|
+
return {
|
|
1258
|
+
changeId,
|
|
1259
|
+
exists: await exists4(changePath),
|
|
1260
|
+
hasProposal: await exists4(join11(changePath, "proposal.md")),
|
|
1261
|
+
hasTasks: await exists4(tasksPath),
|
|
1262
|
+
hasSpecs: await exists4(specsPath),
|
|
1263
|
+
tasksPath,
|
|
1264
|
+
changePath
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
async function listDirectories(path) {
|
|
1268
|
+
try {
|
|
1269
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
1270
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
1271
|
+
} catch {
|
|
1272
|
+
return [];
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
async function exists4(path) {
|
|
1276
|
+
try {
|
|
1277
|
+
await stat6(path);
|
|
1278
|
+
return true;
|
|
1279
|
+
} catch {
|
|
1280
|
+
return false;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// src/openspec/resolver.ts
|
|
1285
|
+
import { execFile as execFile2 } from "child_process";
|
|
1286
|
+
import { promisify as promisify2 } from "util";
|
|
1287
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
1288
|
+
async function resolveOpenSpecExecutable() {
|
|
1289
|
+
const executablePath = await findExecutable();
|
|
1290
|
+
const version = await readVersion(executablePath);
|
|
1291
|
+
return {
|
|
1292
|
+
executablePath,
|
|
1293
|
+
version,
|
|
1294
|
+
adapterVersion: 1
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
async function findExecutable() {
|
|
1298
|
+
const command = process.platform === "win32" ? "where.exe" : "which";
|
|
1299
|
+
try {
|
|
1300
|
+
const { stdout } = await exec(command, ["openspec"]);
|
|
1301
|
+
const first = stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
1302
|
+
if (first) {
|
|
1303
|
+
return first;
|
|
1304
|
+
}
|
|
1305
|
+
} catch {
|
|
1306
|
+
}
|
|
1307
|
+
try {
|
|
1308
|
+
await exec("npx", ["openspec", "--version"]);
|
|
1309
|
+
return "npx openspec";
|
|
1310
|
+
} catch {
|
|
1311
|
+
throw new FetError({
|
|
1312
|
+
code: "OPENSPEC_NOT_FOUND" /* OpenSpecNotFound */,
|
|
1313
|
+
message: "OpenSpec CLI \u672A\u5B89\u88C5",
|
|
1314
|
+
suggestedCommand: "npm install -g @fission-ai/openspec"
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
async function readVersion(executablePath) {
|
|
1319
|
+
const command = executablePath === "npx openspec" ? "npx" : executablePath;
|
|
1320
|
+
const args = executablePath === "npx openspec" ? ["openspec", "--version"] : ["--version"];
|
|
1321
|
+
try {
|
|
1322
|
+
const { stdout, stderr } = await exec(command, args);
|
|
1323
|
+
return stdout.trim() || stderr.trim() || "unknown";
|
|
1324
|
+
} catch (error) {
|
|
1325
|
+
throw new FetError({
|
|
1326
|
+
code: "OPENSPEC_UNSUPPORTED_VERSION" /* OpenSpecUnsupportedVersion */,
|
|
1327
|
+
message: "\u65E0\u6CD5\u8BFB\u53D6 OpenSpec \u7248\u672C",
|
|
1328
|
+
details: { executablePath },
|
|
1329
|
+
cause: error
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
function exec(command, args) {
|
|
1334
|
+
return execFileAsync2(command, args, { shell: process.platform === "win32" });
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// src/openspec/runner.ts
|
|
1338
|
+
import { spawn } from "child_process";
|
|
1339
|
+
async function runOpenSpec(executablePath, command, args, options) {
|
|
1340
|
+
const spawnCommand = executablePath === "npx openspec" ? "npx" : executablePath;
|
|
1341
|
+
const spawnArgs = executablePath === "npx openspec" ? ["openspec", command, ...args] : [command, ...args];
|
|
1342
|
+
return new Promise((resolve2, reject) => {
|
|
1343
|
+
const stdout = [];
|
|
1344
|
+
const stderr = [];
|
|
1345
|
+
const child = spawn(spawnCommand, spawnArgs, {
|
|
1346
|
+
cwd: options.cwd,
|
|
1347
|
+
stdio: options.stdio ?? "inherit",
|
|
1348
|
+
shell: process.platform === "win32"
|
|
1349
|
+
});
|
|
1350
|
+
if (child.stdout) {
|
|
1351
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
1352
|
+
}
|
|
1353
|
+
if (child.stderr) {
|
|
1354
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
1355
|
+
}
|
|
1356
|
+
child.on("error", (error) => {
|
|
1357
|
+
reject(
|
|
1358
|
+
new FetError({
|
|
1359
|
+
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
1360
|
+
message: "OpenSpec \u547D\u4EE4\u542F\u52A8\u5931\u8D25",
|
|
1361
|
+
details: { command, args },
|
|
1362
|
+
cause: error
|
|
1363
|
+
})
|
|
1364
|
+
);
|
|
1365
|
+
});
|
|
1366
|
+
child.on("close", (exitCode, signal) => {
|
|
1367
|
+
resolve2({
|
|
1368
|
+
command,
|
|
1369
|
+
args,
|
|
1370
|
+
exitCode: exitCode ?? 1,
|
|
1371
|
+
signal,
|
|
1372
|
+
stdout: stdout.length ? Buffer.concat(stdout).toString("utf8") : void 0,
|
|
1373
|
+
stderr: stderr.length ? Buffer.concat(stderr).toString("utf8") : void 0
|
|
1374
|
+
});
|
|
1375
|
+
});
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// src/openspec/adapter.ts
|
|
1380
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
1381
|
+
var DefaultOpenSpecAdapter = class {
|
|
1382
|
+
identity;
|
|
1383
|
+
async resolveExecutable() {
|
|
1384
|
+
this.identity ??= await resolveOpenSpecExecutable();
|
|
1385
|
+
return this.identity;
|
|
1386
|
+
}
|
|
1387
|
+
async getCapabilities() {
|
|
1388
|
+
const identity = await this.resolveExecutable();
|
|
1389
|
+
const executable = identity.executablePath === "npx openspec" ? "npx" : identity.executablePath;
|
|
1390
|
+
const args = identity.executablePath === "npx openspec" ? ["openspec", "--help"] : ["--help"];
|
|
1391
|
+
try {
|
|
1392
|
+
const { stdout } = await execFileAsync3(executable, args, { shell: process.platform === "win32" });
|
|
1393
|
+
return {
|
|
1394
|
+
version: identity.version,
|
|
1395
|
+
commands: parseCommands(stdout),
|
|
1396
|
+
supported: true
|
|
1397
|
+
};
|
|
1398
|
+
} catch {
|
|
1399
|
+
return {
|
|
1400
|
+
version: identity.version,
|
|
1401
|
+
commands: [],
|
|
1402
|
+
supported: false
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
async run(command, args, options) {
|
|
1407
|
+
const identity = await this.resolveExecutable();
|
|
1408
|
+
return runOpenSpec(identity.executablePath, command, args, options);
|
|
1409
|
+
}
|
|
1410
|
+
inspectProject(projectRoot) {
|
|
1411
|
+
return inspectOpenSpecProject(projectRoot);
|
|
1412
|
+
}
|
|
1413
|
+
inspectChange(projectRoot, changeId) {
|
|
1414
|
+
return inspectOpenSpecChange(projectRoot, changeId);
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
function parseCommands(help) {
|
|
1418
|
+
const known = [
|
|
1419
|
+
"init",
|
|
1420
|
+
"explore",
|
|
1421
|
+
"propose",
|
|
1422
|
+
"new",
|
|
1423
|
+
"continue",
|
|
1424
|
+
"ff",
|
|
1425
|
+
"apply",
|
|
1426
|
+
"verify",
|
|
1427
|
+
"sync",
|
|
1428
|
+
"archive",
|
|
1429
|
+
"bulk-archive",
|
|
1430
|
+
"onboard"
|
|
1431
|
+
];
|
|
1432
|
+
return known.filter((command) => help.includes(command));
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// src/scanner/package.ts
|
|
1436
|
+
import { readFile as readFile10, stat as stat7 } from "fs/promises";
|
|
1437
|
+
import { join as join12 } from "path";
|
|
1438
|
+
import { parse as parse2 } from "yaml";
|
|
1439
|
+
async function readPackageJson(projectRoot) {
|
|
1440
|
+
try {
|
|
1441
|
+
return JSON.parse(await readFile10(join12(projectRoot, "package.json"), "utf8"));
|
|
1442
|
+
} catch {
|
|
1443
|
+
return null;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
async function detectPackageManager(projectRoot, pkg) {
|
|
1447
|
+
const warnings = [];
|
|
1448
|
+
if (pkg?.packageManager) {
|
|
1449
|
+
const declared = pkg.packageManager.split("@")[0] ?? "unknown";
|
|
1450
|
+
const locks2 = await detectLockManagers(projectRoot);
|
|
1451
|
+
const conflicting = locks2.filter((item) => item !== declared);
|
|
1452
|
+
if (conflicting.length) {
|
|
1453
|
+
warnings.push(`packageManager \u58F0\u660E\u4E3A ${declared}\uFF0C\u4F46\u540C\u65F6\u53D1\u73B0\u9501\u6587\u4EF6\uFF1A${conflicting.join(", ")}`);
|
|
1454
|
+
}
|
|
1455
|
+
return { manager: declared, confidence: "high", warnings };
|
|
1456
|
+
}
|
|
1457
|
+
const locks = await detectLockManagers(projectRoot);
|
|
1458
|
+
if (locks.length > 1) {
|
|
1459
|
+
warnings.push(`\u53D1\u73B0\u591A\u4E2A\u5305\u7BA1\u7406\u5668\u9501\u6587\u4EF6\uFF1A${locks.join(", ")}\uFF0C\u9ED8\u8BA4\u4F7F\u7528 ${locks[0]}`);
|
|
1460
|
+
return { manager: locks[0] ?? "npm", confidence: "medium", warnings };
|
|
1461
|
+
}
|
|
1462
|
+
if (locks[0]) {
|
|
1463
|
+
return { manager: locks[0], confidence: "high", warnings };
|
|
1464
|
+
}
|
|
1465
|
+
return { manager: "npm", confidence: "low", warnings };
|
|
1466
|
+
}
|
|
1467
|
+
function extractCommands(pkg, packageManager) {
|
|
1468
|
+
const scripts = pkg?.scripts ?? {};
|
|
1469
|
+
const result = {};
|
|
1470
|
+
const scriptNames = ["dev", "build", "lint", "typecheck", "check", "test", "test:unit"];
|
|
1471
|
+
for (const name of scriptNames) {
|
|
1472
|
+
if (scripts[name]) {
|
|
1473
|
+
const dimension = name === "check" ? "typecheck" : name === "test:unit" ? "test" : name;
|
|
1474
|
+
if (result[dimension]) {
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
result[dimension] = {
|
|
1478
|
+
command: scriptCommand(packageManager, name),
|
|
1479
|
+
source: `package.json:scripts.${name}`,
|
|
1480
|
+
required: name === "build"
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
return result;
|
|
1485
|
+
}
|
|
1486
|
+
function detectFramework(pkg) {
|
|
1487
|
+
const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
|
|
1488
|
+
const candidates = [
|
|
1489
|
+
["next", ["next"]],
|
|
1490
|
+
["nuxt", ["nuxt"]],
|
|
1491
|
+
["vite", ["vite"]],
|
|
1492
|
+
["sveltekit", ["@sveltejs/kit"]],
|
|
1493
|
+
["angular", ["@angular/core", "@angular/cli"]],
|
|
1494
|
+
["react", ["react"]],
|
|
1495
|
+
["vue", ["vue"]],
|
|
1496
|
+
["svelte", ["svelte"]]
|
|
1497
|
+
];
|
|
1498
|
+
for (const [candidate, packages] of candidates) {
|
|
1499
|
+
if (packages.some((name) => deps[name])) {
|
|
1500
|
+
return { name: candidate, confidence: "high", sources: ["package.json"] };
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
return { name: "unknown", confidence: "low", sources: [] };
|
|
1504
|
+
}
|
|
1505
|
+
async function detectLanguage(projectRoot, pkg) {
|
|
1506
|
+
const deps = { ...pkg?.dependencies ?? {}, ...pkg?.devDependencies ?? {} };
|
|
1507
|
+
if (deps.typescript || await exists5(join12(projectRoot, "tsconfig.json"))) {
|
|
1508
|
+
return "typescript";
|
|
1509
|
+
}
|
|
1510
|
+
return "javascript";
|
|
1511
|
+
}
|
|
1512
|
+
async function detectWorkspaces(projectRoot, pkg) {
|
|
1513
|
+
const packageWorkspaces = normalizeWorkspaces(pkg?.workspaces).map((path) => ({
|
|
1514
|
+
name: path,
|
|
1515
|
+
path,
|
|
1516
|
+
source: "package.json:workspaces"
|
|
1517
|
+
}));
|
|
1518
|
+
if (packageWorkspaces.length) {
|
|
1519
|
+
return packageWorkspaces;
|
|
1520
|
+
}
|
|
1521
|
+
try {
|
|
1522
|
+
const workspace = parse2(await readFile10(join12(projectRoot, "pnpm-workspace.yaml"), "utf8"));
|
|
1523
|
+
return (workspace?.packages ?? []).map((path) => ({
|
|
1524
|
+
name: path,
|
|
1525
|
+
path,
|
|
1526
|
+
source: "pnpm-workspace.yaml:packages"
|
|
1527
|
+
}));
|
|
1528
|
+
} catch {
|
|
1529
|
+
return [];
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
async function detectLockManagers(projectRoot) {
|
|
1533
|
+
const lockFiles = [
|
|
1534
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
1535
|
+
["yarn.lock", "yarn"],
|
|
1536
|
+
["bun.lockb", "bun"],
|
|
1537
|
+
["bun.lock", "bun"],
|
|
1538
|
+
["package-lock.json", "npm"]
|
|
1539
|
+
];
|
|
1540
|
+
const found = [];
|
|
1541
|
+
for (const [file, manager] of lockFiles) {
|
|
1542
|
+
if (await exists5(join12(projectRoot, file))) {
|
|
1543
|
+
found.push(manager);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
return found;
|
|
1547
|
+
}
|
|
1548
|
+
function normalizeWorkspaces(workspaces) {
|
|
1549
|
+
if (Array.isArray(workspaces)) {
|
|
1550
|
+
return workspaces;
|
|
1551
|
+
}
|
|
1552
|
+
return workspaces?.packages ?? [];
|
|
1553
|
+
}
|
|
1554
|
+
function scriptCommand(packageManager, name) {
|
|
1555
|
+
return packageManager === "npm" ? `npm run ${name}` : `${packageManager} ${name}`;
|
|
1556
|
+
}
|
|
1557
|
+
async function exists5(path) {
|
|
1558
|
+
try {
|
|
1559
|
+
await stat7(path);
|
|
1560
|
+
return true;
|
|
1561
|
+
} catch {
|
|
1562
|
+
return false;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// src/scanner/routes.ts
|
|
1567
|
+
import { readdir as readdir2, stat as stat8 } from "fs/promises";
|
|
1568
|
+
import { join as join13, relative, sep } from "path";
|
|
1569
|
+
async function scanRoutes(projectRoot) {
|
|
1570
|
+
const candidates = ["src/routes", "src/pages", "app", "pages"];
|
|
1571
|
+
const routes = [];
|
|
1572
|
+
for (const candidate of candidates) {
|
|
1573
|
+
const root = join13(projectRoot, candidate);
|
|
1574
|
+
if (!await exists6(root)) {
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
for (const file of await listFiles(root)) {
|
|
1578
|
+
if (!/\.(tsx?|jsx?|vue|svelte)$/.test(file)) {
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
routes.push({
|
|
1582
|
+
path: inferRoutePath(relative(root, file)),
|
|
1583
|
+
source: relative(projectRoot, file).split(sep).join("/"),
|
|
1584
|
+
inferred: true,
|
|
1585
|
+
confidence: "medium"
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
return routes.slice(0, 100);
|
|
1590
|
+
}
|
|
1591
|
+
function inferRoutePath(relativePath) {
|
|
1592
|
+
const normalized = relativePath.split(sep).join("/");
|
|
1593
|
+
const withoutExt = normalized.replace(/\.(tsx?|jsx?|vue|svelte)$/, "");
|
|
1594
|
+
const withoutIndex = withoutExt.replace(/\/index$/, "").replace(/^index$/, "");
|
|
1595
|
+
return `/${withoutIndex}`.replace(/\/+/g, "/");
|
|
1596
|
+
}
|
|
1597
|
+
async function listFiles(root) {
|
|
1598
|
+
const entries = await readdir2(root, { withFileTypes: true });
|
|
1599
|
+
const files = [];
|
|
1600
|
+
for (const entry of entries) {
|
|
1601
|
+
const path = join13(root, entry.name);
|
|
1602
|
+
if (entry.isDirectory()) {
|
|
1603
|
+
files.push(...await listFiles(path));
|
|
1604
|
+
} else {
|
|
1605
|
+
files.push(path);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
return files;
|
|
1609
|
+
}
|
|
1610
|
+
async function exists6(path) {
|
|
1611
|
+
try {
|
|
1612
|
+
await stat8(path);
|
|
1613
|
+
return true;
|
|
1614
|
+
} catch {
|
|
1615
|
+
return false;
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// src/scanner/index.ts
|
|
1620
|
+
var ProjectScanner = class {
|
|
1621
|
+
async scan(projectRoot, _options = {}) {
|
|
1622
|
+
const pkg = await readPackageJson(projectRoot);
|
|
1623
|
+
const packageManager = await detectPackageManager(projectRoot, pkg);
|
|
1624
|
+
const framework = detectFramework(pkg);
|
|
1625
|
+
const workspaces = await detectWorkspaces(projectRoot, pkg);
|
|
1626
|
+
const language = await detectLanguage(projectRoot, pkg);
|
|
1627
|
+
const warnings = [...packageManager.warnings];
|
|
1628
|
+
if (framework.name === "unknown") {
|
|
1629
|
+
warnings.push("\u672A\u80FD\u4ECE package.json \u8BC6\u522B\u4E3B\u8981\u6846\u67B6");
|
|
1630
|
+
}
|
|
1631
|
+
return {
|
|
1632
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1633
|
+
scannerVersion: 1,
|
|
1634
|
+
project: {
|
|
1635
|
+
name: pkg?.name ?? "unknown",
|
|
1636
|
+
packageManager: packageManager.manager,
|
|
1637
|
+
packageManagerConfidence: packageManager.confidence,
|
|
1638
|
+
framework,
|
|
1639
|
+
language,
|
|
1640
|
+
monorepo: workspaces.length > 0,
|
|
1641
|
+
workspaces
|
|
1642
|
+
},
|
|
1643
|
+
commands: extractCommands(pkg, packageManager.manager),
|
|
1644
|
+
routes: await scanRoutes(projectRoot),
|
|
1645
|
+
conventions: [],
|
|
1646
|
+
skippedFiles: [],
|
|
1647
|
+
warnings
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
};
|
|
1651
|
+
|
|
1652
|
+
// src/cli/output.ts
|
|
1653
|
+
var OutputWriter = class {
|
|
1654
|
+
constructor(json) {
|
|
1655
|
+
this.json = json;
|
|
1656
|
+
}
|
|
1657
|
+
json;
|
|
1658
|
+
info(message, details) {
|
|
1659
|
+
if (!this.json) {
|
|
1660
|
+
process.stdout.write(`${message}${formatDetails(details)}
|
|
1661
|
+
`);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
warn(message, details) {
|
|
1665
|
+
if (!this.json) {
|
|
1666
|
+
process.stderr.write(`\u8B66\u544A\uFF1A${message}${formatDetails(details)}
|
|
1667
|
+
`);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
error(error) {
|
|
1671
|
+
if (this.json) {
|
|
1672
|
+
process.stderr.write(`${JSON.stringify({ ok: false, error: error.toJSON() }, null, 2)}
|
|
1673
|
+
`);
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
process.stderr.write(`FET \u65E0\u6CD5\u7EE7\u7EED\uFF1A${error.message}
|
|
1677
|
+
`);
|
|
1678
|
+
if (error.details !== void 0) {
|
|
1679
|
+
process.stderr.write(`
|
|
1680
|
+
\u8BE6\u60C5\uFF1A
|
|
1681
|
+
${formatBlock(error.details)}
|
|
1682
|
+
`);
|
|
1683
|
+
}
|
|
1684
|
+
if (error.suggestedCommand) {
|
|
1685
|
+
process.stderr.write(`
|
|
1686
|
+
\u5EFA\u8BAE\uFF1A
|
|
1687
|
+
${error.suggestedCommand}
|
|
1688
|
+
`);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
result(result) {
|
|
1692
|
+
if (this.json) {
|
|
1693
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
1694
|
+
`);
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
process.stdout.write(`${result.summary}
|
|
1698
|
+
`);
|
|
1699
|
+
for (const warning of result.warnings ?? []) {
|
|
1700
|
+
process.stdout.write(`\u8B66\u544A\uFF1A${warning}
|
|
1701
|
+
`);
|
|
1702
|
+
}
|
|
1703
|
+
if (result.nextSteps?.length) {
|
|
1704
|
+
process.stdout.write("\n\u4E0B\u4E00\u6B65\uFF1A\n");
|
|
1705
|
+
for (const step of result.nextSteps) {
|
|
1706
|
+
process.stdout.write(` ${step}
|
|
1707
|
+
`);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
};
|
|
1712
|
+
function formatDetails(details) {
|
|
1713
|
+
if (details === void 0) {
|
|
1714
|
+
return "";
|
|
1715
|
+
}
|
|
1716
|
+
return ` ${JSON.stringify(details)}`;
|
|
1717
|
+
}
|
|
1718
|
+
function formatBlock(details) {
|
|
1719
|
+
if (typeof details === "string") {
|
|
1720
|
+
return ` ${details}`;
|
|
1721
|
+
}
|
|
1722
|
+
return JSON.stringify(details, null, 2).split("\n").map((line) => ` ${line}`).join("\n");
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// src/cli/context.ts
|
|
1726
|
+
async function createCommandContext(command, options) {
|
|
1727
|
+
const projectRoot = resolve(options.cwd ?? process.cwd());
|
|
1728
|
+
const project = await detectProjectIdentity(projectRoot);
|
|
1729
|
+
const output = new OutputWriter(Boolean(options.json));
|
|
1730
|
+
return {
|
|
1731
|
+
command,
|
|
1732
|
+
cwd: projectRoot,
|
|
1733
|
+
projectRoot,
|
|
1734
|
+
isTty: Boolean(process.stdout.isTTY),
|
|
1735
|
+
json: Boolean(options.json),
|
|
1736
|
+
verbose: Boolean(options.verbose),
|
|
1737
|
+
yes: Boolean(options.yes),
|
|
1738
|
+
changeId: options.change,
|
|
1739
|
+
fetVersion: FET_VERSION,
|
|
1740
|
+
output,
|
|
1741
|
+
stateStore: new StateStore(projectRoot, FET_VERSION, project),
|
|
1742
|
+
openSpec: new DefaultOpenSpecAdapter(),
|
|
1743
|
+
scanner: new ProjectScanner(),
|
|
1744
|
+
toolAdapters: [new CursorAdapter()]
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// src/cli/index.ts
|
|
1749
|
+
var program = new Command();
|
|
1750
|
+
program.name("fet").description("Frontend workflow orchestration tool built around OpenSpec.").enablePositionalOptions().version(FET_VERSION).option("--cwd <path>", "\u6307\u5B9A\u9879\u76EE\u6839\u76EE\u5F55").option("--change <id>", "\u6307\u5B9A OpenSpec change").option("--yes", "\u5BF9\u4F4E\u98CE\u9669\u786E\u8BA4\u4F7F\u7528\u9ED8\u8BA4\u540C\u610F").option("--json", "\u8F93\u51FA\u673A\u5668\u53EF\u8BFB JSON").option("--verbose", "\u8F93\u51FA\u8BCA\u65AD\u7EC6\u8282").option("--no-color", "\u7981\u7528\u7EC8\u7AEF\u989C\u8272");
|
|
1751
|
+
addGlobalOptions(program.command("init")).description("\u521D\u59CB\u5316 FET + OpenSpec").action(wrap("init", initCommand));
|
|
1752
|
+
addGlobalOptions(program.command("update-context")).description("\u66F4\u65B0\u9879\u76EE\u4E0A\u4E0B\u6587").action(wrap("update-context", updateContextCommand));
|
|
1753
|
+
addGlobalOptions(program.command("doctor").description("\u8BCA\u65AD\u72B6\u6001\u3001\u914D\u7F6E\u4E0E\u4E00\u81F4\u6027").option("--fix-lock", "\u6E05\u7406 FET \u9501\u6587\u4EF6")).action(
|
|
1754
|
+
wrap("doctor", (ctx, options) => doctorCommand(ctx, { fixLock: Boolean(options.fixLock) }))
|
|
1755
|
+
);
|
|
1756
|
+
addGlobalOptions(program.command("verify").description("\u6700\u7EC8\u8D28\u91CF\u9A8C\u8BC1").option("--done", "\u58F0\u660E\u624B\u52A8\u9A8C\u8BC1\u5DF2\u5B8C\u6210").option("--auto", "\u751F\u6210\u6216\u6267\u884C\u81EA\u52A8\u9A8C\u8BC1\u8BA1\u5212")).action(wrap("verify", verifyCommand));
|
|
1757
|
+
for (const command of ["explore", "propose", "new", "continue", "ff", "apply", "sync", "archive", "bulk-archive", "onboard"]) {
|
|
1758
|
+
addGlobalOptions(program.command(`${command} [args...]`).description(`\u4EE3\u7406\u6267\u884C openspec ${command}`).allowUnknownOption(true).passThroughOptions()).action(wrap(command, (ctx, args = []) => proxyCommand(ctx, command, args)));
|
|
1759
|
+
}
|
|
1760
|
+
addGlobalOptions(program.command("passthrough <command> [args...]").description("\u900F\u4F20\u6682\u672A\u63A5\u7BA1\u7684 OpenSpec \u547D\u4EE4").allowUnknownOption(true).passThroughOptions()).action(wrap("passthrough", (ctx, command, args = []) => passthroughCommand(ctx, command, args)));
|
|
1761
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
1762
|
+
const json = process.argv.includes("--json");
|
|
1763
|
+
const output = new OutputWriter(json);
|
|
1764
|
+
const fetError = toFetError(error);
|
|
1765
|
+
output.error(fetError);
|
|
1766
|
+
process.exitCode = fetError.exitCode;
|
|
1767
|
+
});
|
|
1768
|
+
function wrap(command, handler) {
|
|
1769
|
+
return async (...args) => {
|
|
1770
|
+
const maybeCommand = args[args.length - 1];
|
|
1771
|
+
const opts = isCommandLike(maybeCommand) ? { ...maybeCommand.parent?.opts(), ...maybeCommand.opts() } : program.opts();
|
|
1772
|
+
const ctx = await createCommandContext(command, { ...opts, ...extractGlobalOptions(args) });
|
|
1773
|
+
try {
|
|
1774
|
+
await handler(ctx, ...args);
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
const fetError = toFetError(error);
|
|
1777
|
+
ctx.output.error(fetError);
|
|
1778
|
+
process.exitCode = fetError.exitCode;
|
|
1779
|
+
}
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
function isCommandLike(value) {
|
|
1783
|
+
return value instanceof Command;
|
|
1784
|
+
}
|
|
1785
|
+
function addGlobalOptions(command) {
|
|
1786
|
+
return command.option("--cwd <path>", "\u6307\u5B9A\u9879\u76EE\u6839\u76EE\u5F55").option("--change <id>", "\u6307\u5B9A OpenSpec change").option("--yes", "\u5BF9\u4F4E\u98CE\u9669\u786E\u8BA4\u4F7F\u7528\u9ED8\u8BA4\u540C\u610F").option("--json", "\u8F93\u51FA\u673A\u5668\u53EF\u8BFB JSON").option("--verbose", "\u8F93\u51FA\u8BCA\u65AD\u7EC6\u8282").option("--no-color", "\u7981\u7528\u7EC8\u7AEF\u989C\u8272");
|
|
1787
|
+
}
|
|
1788
|
+
function extractGlobalOptions(args) {
|
|
1789
|
+
const values = args.flatMap((arg) => Array.isArray(arg) ? arg : []);
|
|
1790
|
+
const options = {};
|
|
1791
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
1792
|
+
const value = values[index];
|
|
1793
|
+
if (typeof value !== "string") {
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
if (value === "--cwd" && typeof values[index + 1] === "string") {
|
|
1797
|
+
options.cwd = values[index + 1];
|
|
1798
|
+
index += 1;
|
|
1799
|
+
} else if (value.startsWith("--cwd=")) {
|
|
1800
|
+
options.cwd = value.slice("--cwd=".length);
|
|
1801
|
+
} else if (value === "--change" && typeof values[index + 1] === "string") {
|
|
1802
|
+
options.change = values[index + 1];
|
|
1803
|
+
index += 1;
|
|
1804
|
+
} else if (value.startsWith("--change=")) {
|
|
1805
|
+
options.change = value.slice("--change=".length);
|
|
1806
|
+
} else if (value === "--yes") {
|
|
1807
|
+
options.yes = true;
|
|
1808
|
+
} else if (value === "--json") {
|
|
1809
|
+
options.json = true;
|
|
1810
|
+
} else if (value === "--verbose") {
|
|
1811
|
+
options.verbose = true;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
return options;
|
|
1815
|
+
}
|
|
1816
|
+
//# sourceMappingURL=index.js.map
|