@nick848/fet 1.1.8 → 1.1.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/README.md +120 -13
- package/README_en.md +116 -13
- package/dist/cli/index.js +961 -165
- package/dist/cli/index.js.map +1 -1
- package/package.json +9 -1
package/dist/cli/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { createInterface as createInterface3 } from "readline/promises";
|
|
|
9
9
|
import { Command } from "commander";
|
|
10
10
|
|
|
11
11
|
// src/commands/doctor.ts
|
|
12
|
-
import {
|
|
12
|
+
import { stat as stat3 } from "fs/promises";
|
|
13
13
|
import { join as join6 } from "path";
|
|
14
14
|
|
|
15
15
|
// src/context-placeholders.ts
|
|
@@ -188,18 +188,18 @@ function toGitNexusState(detection, previous) {
|
|
|
188
188
|
};
|
|
189
189
|
}
|
|
190
190
|
async function inspectGitNexusGraph(projectRoot, env = process.env) {
|
|
191
|
-
const
|
|
192
|
-
const graphPath = join5(projectRoot,
|
|
191
|
+
const relative6 = env.FET_GITNEXUS_GRAPH_PATH?.trim() || DEFAULT_GRAPH_PATH;
|
|
192
|
+
const graphPath = join5(projectRoot, relative6);
|
|
193
193
|
try {
|
|
194
194
|
const info = await stat2(graphPath);
|
|
195
195
|
return {
|
|
196
|
-
graphPath:
|
|
196
|
+
graphPath: relative6,
|
|
197
197
|
graphExists: true,
|
|
198
198
|
lastIndexedAt: info.mtime.toISOString()
|
|
199
199
|
};
|
|
200
200
|
} catch {
|
|
201
201
|
return {
|
|
202
|
-
graphPath:
|
|
202
|
+
graphPath: relative6,
|
|
203
203
|
graphExists: false,
|
|
204
204
|
lastIndexedAt: null
|
|
205
205
|
};
|
|
@@ -268,7 +268,10 @@ async function doctorCommand(ctx, options = {}) {
|
|
|
268
268
|
checks.push(await checkState(ctx));
|
|
269
269
|
checks.push(await checkFile("agents", join6(ctx.projectRoot, "AGENTS.md"), "AGENTS.md \u7F3A\u5931", "fet update-context"));
|
|
270
270
|
checks.push(await checkFile("config", join6(ctx.projectRoot, "openspec", "config.yaml"), "openspec/config.yaml \u7F3A\u5931", "fet init"));
|
|
271
|
-
|
|
271
|
+
const placeholders = await checkPlaceholders(ctx);
|
|
272
|
+
if (placeholders) {
|
|
273
|
+
checks.push(placeholders);
|
|
274
|
+
}
|
|
272
275
|
checks.push(await checkGitNexus(ctx));
|
|
273
276
|
for (const adapter of ctx.toolAdapters) {
|
|
274
277
|
checks.push(...await adapter.doctor(ctx.projectRoot));
|
|
@@ -333,27 +336,21 @@ async function checkFile(id, path, missing, suggestedCommand) {
|
|
|
333
336
|
return await exists(path) ? { id, status: "pass", message: `${id} \u5B58\u5728` } : { id, status: "warn", message: missing, suggestedCommand };
|
|
334
337
|
}
|
|
335
338
|
async function checkPlaceholders(ctx) {
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return count2 ? {
|
|
340
|
-
id: "context-placeholders",
|
|
341
|
-
status: "warn",
|
|
342
|
-
message: ctx.language === "en" ? `AGENTS.md has ${count2} LLM placeholder(s)` : `AGENTS.md \u4ECD\u6709 ${count2} \u4E2A LLM \u5360\u4F4D\u7B26`,
|
|
343
|
-
suggestedCommand: "fet fill-context"
|
|
344
|
-
} : {
|
|
345
|
-
id: "context-placeholders",
|
|
346
|
-
status: "pass",
|
|
347
|
-
message: ctx.language === "en" ? "AGENTS.md placeholders resolved" : "AGENTS.md \u5360\u4F4D\u7B26\u5DF2\u5904\u7406"
|
|
348
|
-
};
|
|
349
|
-
} catch {
|
|
350
|
-
return {
|
|
351
|
-
id: "context-placeholders",
|
|
352
|
-
status: "warn",
|
|
353
|
-
message: ctx.language === "en" ? "AGENTS.md missing" : "AGENTS.md \u7F3A\u5931",
|
|
354
|
-
suggestedCommand: "fet update-context"
|
|
355
|
-
};
|
|
339
|
+
const agentsPath = join6(ctx.projectRoot, "AGENTS.md");
|
|
340
|
+
if (!await exists(agentsPath)) {
|
|
341
|
+
return null;
|
|
356
342
|
}
|
|
343
|
+
const count2 = await countAgentsLlmPlaceholders(ctx.projectRoot);
|
|
344
|
+
return count2 ? {
|
|
345
|
+
id: "context-placeholders",
|
|
346
|
+
status: "warn",
|
|
347
|
+
message: ctx.language === "en" ? `AGENTS.md has ${count2} LLM placeholder(s)` : `AGENTS.md \u4ECD\u6709 ${count2} \u4E2A LLM \u5360\u4F4D\u7B26`,
|
|
348
|
+
suggestedCommand: "fet fill-context"
|
|
349
|
+
} : {
|
|
350
|
+
id: "context-placeholders",
|
|
351
|
+
status: "pass",
|
|
352
|
+
message: ctx.language === "en" ? "AGENTS.md placeholders resolved" : "AGENTS.md \u5360\u4F4D\u7B26\u5DF2\u5904\u7406"
|
|
353
|
+
};
|
|
357
354
|
}
|
|
358
355
|
async function exists(path) {
|
|
359
356
|
try {
|
|
@@ -369,20 +366,20 @@ import { mkdir as mkdir3 } from "fs/promises";
|
|
|
369
366
|
import { dirname as dirname4, join as join10 } from "path";
|
|
370
367
|
|
|
371
368
|
// src/agents-miniprogram.ts
|
|
372
|
-
import { readFile as
|
|
369
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
373
370
|
import { join as join9 } from "path";
|
|
374
371
|
|
|
375
372
|
// src/scanner/miniprogram.ts
|
|
376
|
-
import { readdir, readFile as
|
|
373
|
+
import { readdir, readFile as readFile5, stat as stat5 } from "fs/promises";
|
|
377
374
|
import { join as join8, relative } from "path";
|
|
378
375
|
|
|
379
376
|
// src/scanner/package.ts
|
|
380
|
-
import { readFile as
|
|
377
|
+
import { readFile as readFile4, stat as stat4 } from "fs/promises";
|
|
381
378
|
import { join as join7 } from "path";
|
|
382
379
|
import { parse } from "yaml";
|
|
383
380
|
async function readPackageJson(projectRoot) {
|
|
384
381
|
try {
|
|
385
|
-
return JSON.parse(await
|
|
382
|
+
return JSON.parse(await readFile4(join7(projectRoot, "package.json"), "utf8"));
|
|
386
383
|
} catch {
|
|
387
384
|
return null;
|
|
388
385
|
}
|
|
@@ -463,7 +460,7 @@ async function detectWorkspaces(projectRoot, pkg) {
|
|
|
463
460
|
return packageWorkspaces;
|
|
464
461
|
}
|
|
465
462
|
try {
|
|
466
|
-
const workspace = parse(await
|
|
463
|
+
const workspace = parse(await readFile4(join7(projectRoot, "pnpm-workspace.yaml"), "utf8"));
|
|
467
464
|
return (workspace?.packages ?? []).map((path) => ({
|
|
468
465
|
name: path,
|
|
469
466
|
path,
|
|
@@ -604,7 +601,7 @@ async function resolveAppJsonPath(projectRoot, platform) {
|
|
|
604
601
|
const candidates = [];
|
|
605
602
|
if (platform.id === "wechat") {
|
|
606
603
|
try {
|
|
607
|
-
const config = JSON.parse(await
|
|
604
|
+
const config = JSON.parse(await readFile5(join8(projectRoot, "project.config.json"), "utf8"));
|
|
608
605
|
if (config.miniprogramRoot) {
|
|
609
606
|
candidates.push(join8(projectRoot, config.miniprogramRoot, "app.json"));
|
|
610
607
|
}
|
|
@@ -628,7 +625,7 @@ async function resolveAppJsonPath(projectRoot, platform) {
|
|
|
628
625
|
}
|
|
629
626
|
async function readAppJson(path) {
|
|
630
627
|
try {
|
|
631
|
-
return JSON.parse(await
|
|
628
|
+
return JSON.parse(await readFile5(path, "utf8"));
|
|
632
629
|
} catch {
|
|
633
630
|
return null;
|
|
634
631
|
}
|
|
@@ -991,7 +988,7 @@ async function applyMiniprogramAgentsContext(projectRoot, language) {
|
|
|
991
988
|
const detection = await detectMiniprogramProject(projectRoot);
|
|
992
989
|
let existing;
|
|
993
990
|
try {
|
|
994
|
-
existing = await
|
|
991
|
+
existing = await readFile6(agentsPath, "utf8");
|
|
995
992
|
} catch {
|
|
996
993
|
return {
|
|
997
994
|
applied: false,
|
|
@@ -1116,7 +1113,7 @@ import { mkdir as mkdir5 } from "fs/promises";
|
|
|
1116
1113
|
import { dirname as dirname6, join as join12 } from "path";
|
|
1117
1114
|
|
|
1118
1115
|
// src/graph-context.ts
|
|
1119
|
-
import { mkdir as mkdir4, readdir as readdir2, readFile as
|
|
1116
|
+
import { mkdir as mkdir4, readdir as readdir2, readFile as readFile7 } from "fs/promises";
|
|
1120
1117
|
import { dirname as dirname5, join as join11 } from "path";
|
|
1121
1118
|
var MAX_SOURCE_CONTEXT = 8e3;
|
|
1122
1119
|
var MAX_GRAPH_OUTPUT = 2e4;
|
|
@@ -1362,7 +1359,7 @@ async function listSpecFiles(specsRoot) {
|
|
|
1362
1359
|
}
|
|
1363
1360
|
async function readOptional(path) {
|
|
1364
1361
|
try {
|
|
1365
|
-
return await
|
|
1362
|
+
return await readFile7(path, "utf8");
|
|
1366
1363
|
} catch {
|
|
1367
1364
|
return null;
|
|
1368
1365
|
}
|
|
@@ -1768,19 +1765,19 @@ import { stat as stat6 } from "fs/promises";
|
|
|
1768
1765
|
import { join as join15 } from "path";
|
|
1769
1766
|
|
|
1770
1767
|
// src/commands/update-context.ts
|
|
1771
|
-
import { readFile as
|
|
1768
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
1772
1769
|
import { createInterface } from "readline/promises";
|
|
1773
1770
|
import { join as join14 } from "path";
|
|
1774
1771
|
|
|
1775
1772
|
// src/config/yaml.ts
|
|
1776
|
-
import { readFile as
|
|
1773
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1777
1774
|
import { parseDocument } from "yaml";
|
|
1778
1775
|
async function mergeFetConfig(configPath, renderedFetYaml) {
|
|
1779
1776
|
const fetDoc = parseDocument(renderedFetYaml);
|
|
1780
1777
|
const nextFet = fetDoc.get("fet", true);
|
|
1781
1778
|
let existing = "";
|
|
1782
1779
|
try {
|
|
1783
|
-
existing = await
|
|
1780
|
+
existing = await readFile8(configPath, "utf8");
|
|
1784
1781
|
} catch {
|
|
1785
1782
|
return renderedFetYaml;
|
|
1786
1783
|
}
|
|
@@ -2088,6 +2085,11 @@ function renderFetConfig(scan, language = "zh-CN") {
|
|
|
2088
2085
|
uiDisplayContract: {
|
|
2089
2086
|
enabled: true
|
|
2090
2087
|
},
|
|
2088
|
+
tdd: {
|
|
2089
|
+
enabled: true,
|
|
2090
|
+
mode: "require_before_apply",
|
|
2091
|
+
whenNoTestScript: "block"
|
|
2092
|
+
},
|
|
2091
2093
|
specLanguage: {
|
|
2092
2094
|
style: "layered_bilingual",
|
|
2093
2095
|
canonical: "en",
|
|
@@ -2253,6 +2255,135 @@ fet verify --done --change ${changeId}
|
|
|
2253
2255
|
`;
|
|
2254
2256
|
}
|
|
2255
2257
|
|
|
2258
|
+
// src/tdd/paths.ts
|
|
2259
|
+
function tddFetDirRelative(changeId) {
|
|
2260
|
+
return `openspec/changes/${changeId}/.fet`;
|
|
2261
|
+
}
|
|
2262
|
+
function tddManifestRelativePath(changeId) {
|
|
2263
|
+
return `${tddFetDirRelative(changeId)}/tdd-manifest.yaml`;
|
|
2264
|
+
}
|
|
2265
|
+
function tddSpecRelativePath(changeId) {
|
|
2266
|
+
return `${tddFetDirRelative(changeId)}/tdd-spec.md`;
|
|
2267
|
+
}
|
|
2268
|
+
function tddInstructionsRelativePath(changeId) {
|
|
2269
|
+
return `${tddFetDirRelative(changeId)}/tdd-instructions.md`;
|
|
2270
|
+
}
|
|
2271
|
+
function tddResultsRelativePath(changeId) {
|
|
2272
|
+
return `${tddFetDirRelative(changeId)}/tdd-results.json`;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// src/templates/tdd.ts
|
|
2276
|
+
function renderTddInstructions(changeId, manifest, language) {
|
|
2277
|
+
const manifestPath2 = tddManifestRelativePath(changeId);
|
|
2278
|
+
const specPath = tddSpecRelativePath(changeId);
|
|
2279
|
+
const caseList = manifest.cases.map((item) => `- \`${item.id}\`: ${item.title} \u2192 \`${item.testFile}\` (${item.testIds.join(", ")})`).join("\n");
|
|
2280
|
+
if (language === "en") {
|
|
2281
|
+
return `---
|
|
2282
|
+
schemaVersion: 1
|
|
2283
|
+
fetVersion: ${FET_VERSION}
|
|
2284
|
+
changeId: ${changeId}
|
|
2285
|
+
purpose: tdd-instructions
|
|
2286
|
+
generatedAt: ${manifest.generatedAt}
|
|
2287
|
+
---
|
|
2288
|
+
|
|
2289
|
+
# TDD instructions (this change)
|
|
2290
|
+
|
|
2291
|
+
Create or update unit tests **before** marking implementation tasks done in \`tasks.md\`.
|
|
2292
|
+
|
|
2293
|
+
## Sources
|
|
2294
|
+
${manifest.sources.map((s) => `- ${s}`).join("\n")}
|
|
2295
|
+
|
|
2296
|
+
## Cases (from ${manifestPath2})
|
|
2297
|
+
${caseList}
|
|
2298
|
+
|
|
2299
|
+
## Rules
|
|
2300
|
+
1. Each case must map to a real test file under the repo test tree.
|
|
2301
|
+
2. Tests should fail until implementation lands (red \u2192 green).
|
|
2302
|
+
3. Do not edit \`${manifestPath2}\` by hand unless fixing IDs; re-run \`fet tdd --change ${changeId}\` after planning changes.
|
|
2303
|
+
4. When tests exist, run \`fet test --change ${changeId}\` before \`fet verify\`.
|
|
2304
|
+
|
|
2305
|
+
Human-readable matrix: \`${specPath}\`
|
|
2306
|
+
`;
|
|
2307
|
+
}
|
|
2308
|
+
return `---
|
|
2309
|
+
schemaVersion: 1
|
|
2310
|
+
fetVersion: ${FET_VERSION}
|
|
2311
|
+
changeId: ${changeId}
|
|
2312
|
+
purpose: tdd-instructions
|
|
2313
|
+
generatedAt: ${manifest.generatedAt}
|
|
2314
|
+
---
|
|
2315
|
+
|
|
2316
|
+
# TDD \u6307\u4EE4\uFF08\u672C change\uFF09
|
|
2317
|
+
|
|
2318
|
+
\u5728\u5C06 \`tasks.md\` \u4E2D\u7684\u4EFB\u52A1\u6807\u4E3A\u5B8C\u6210\u4E4B\u524D\uFF0C\u5148\u521B\u5EFA\u6216\u66F4\u65B0\u5355\u5143\u6D4B\u8BD5\u3002
|
|
2319
|
+
|
|
2320
|
+
## \u6765\u6E90
|
|
2321
|
+
${manifest.sources.map((s) => `- ${s}`).join("\n")}
|
|
2322
|
+
|
|
2323
|
+
## \u7528\u4F8B\uFF08\u89C1 ${manifestPath2}\uFF09
|
|
2324
|
+
${caseList}
|
|
2325
|
+
|
|
2326
|
+
## \u89C4\u5219
|
|
2327
|
+
1. \u6BCF\u4E2A\u7528\u4F8B\u5FC5\u987B\u5BF9\u5E94\u4ED3\u5E93\u5185\u771F\u5B9E\u6D4B\u8BD5\u6587\u4EF6\u3002
|
|
2328
|
+
2. \u5B9E\u73B0\u843D\u5730\u524D\u6D4B\u8BD5\u5E94\u5904\u4E8E\u5931\u8D25\uFF08\u7EA2\uFF09\u72B6\u6001\uFF0C\u843D\u5730\u540E\u5E94\u53D8\u7EFF\u3002
|
|
2329
|
+
3. \u9664\u975E\u4FEE\u6B63 ID\uFF0C\u4E0D\u8981\u624B\u6539 \`${manifestPath2}\`\uFF1B\u89C4\u5212\u53D8\u66F4\u540E\u8BF7\u91CD\u65B0\u6267\u884C \`fet tdd --change ${changeId}\`\u3002
|
|
2330
|
+
4. \u6D4B\u8BD5\u5C31\u7EEA\u540E\u5148 \`fet test --change ${changeId}\`\uFF0C\u518D \`fet verify\`\u3002
|
|
2331
|
+
|
|
2332
|
+
\u53EF\u8BFB\u77E9\u9635\u89C1 \`${specPath}\`
|
|
2333
|
+
`;
|
|
2334
|
+
}
|
|
2335
|
+
function renderTddSpec(changeId, manifest, language) {
|
|
2336
|
+
const rows = manifest.cases.map((item) => renderSpecRow(item, language)).join("\n");
|
|
2337
|
+
if (language === "en") {
|
|
2338
|
+
return `---
|
|
2339
|
+
schemaVersion: 1
|
|
2340
|
+
changeId: ${changeId}
|
|
2341
|
+
generatedAt: ${manifest.generatedAt}
|
|
2342
|
+
planningFingerprint: ${manifest.planningFingerprint}
|
|
2343
|
+
---
|
|
2344
|
+
|
|
2345
|
+
# TDD case matrix
|
|
2346
|
+
|
|
2347
|
+
| ID | Scenario | Spec reference | Test file | Required |
|
|
2348
|
+
|----|----------|----------------|-----------|----------|
|
|
2349
|
+
${rows}
|
|
2350
|
+
`;
|
|
2351
|
+
}
|
|
2352
|
+
return `---
|
|
2353
|
+
schemaVersion: 1
|
|
2354
|
+
changeId: ${changeId}
|
|
2355
|
+
generatedAt: ${manifest.generatedAt}
|
|
2356
|
+
planningFingerprint: ${manifest.planningFingerprint}
|
|
2357
|
+
---
|
|
2358
|
+
|
|
2359
|
+
# TDD \u7528\u4F8B\u77E9\u9635
|
|
2360
|
+
|
|
2361
|
+
| ID | \u573A\u666F | Spec \u5F15\u7528 | \u6D4B\u8BD5\u6587\u4EF6 | \u5FC5\u9700 |
|
|
2362
|
+
|----|------|-----------|----------|------|
|
|
2363
|
+
${rows}
|
|
2364
|
+
`;
|
|
2365
|
+
}
|
|
2366
|
+
function renderSpecRow(item, language) {
|
|
2367
|
+
const required = language === "en" ? item.required ? "yes" : "no" : item.required ? "\u662F" : "\u5426";
|
|
2368
|
+
return `| ${item.id} | ${escapeTable(item.title)} | ${escapeTable(item.specRef)} | \`${item.testFile}\` | ${required} |`;
|
|
2369
|
+
}
|
|
2370
|
+
function escapeTable(value) {
|
|
2371
|
+
return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
2372
|
+
}
|
|
2373
|
+
function renderTddApplyNextSteps(changeId, language) {
|
|
2374
|
+
const manifestPath2 = tddManifestRelativePath(changeId);
|
|
2375
|
+
if (language === "en") {
|
|
2376
|
+
return [
|
|
2377
|
+
`Read ${manifestPath2} and tdd-instructions.md; implement code until fet test passes for this change.`,
|
|
2378
|
+
`Run fet test --change ${changeId} before fet verify.`
|
|
2379
|
+
];
|
|
2380
|
+
}
|
|
2381
|
+
return [
|
|
2382
|
+
`\u9605\u8BFB ${manifestPath2} \u4E0E tdd-instructions.md\uFF1B\u5B9E\u73B0\u4EE3\u7801\u76F4\u81F3\u672C change \u7684 fet test \u901A\u8FC7\u3002`,
|
|
2383
|
+
`\u5728 fet verify \u4E4B\u524D\u5148\u6267\u884C fet test --change ${changeId}\u3002`
|
|
2384
|
+
];
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2256
2387
|
// src/templates/figma-guard.ts
|
|
2257
2388
|
var FIGMA_URL_PATTERN = /https?:\/\/(?:www\.)?figma\.com\/(?:file|design|proto)\/[^\s)\]"'<>]+/gi;
|
|
2258
2389
|
function figmaStopHandoffRelativePath(changeId) {
|
|
@@ -2824,7 +2955,7 @@ async function confirmInitCanReplaceUnmanagedAgents(ctx) {
|
|
|
2824
2955
|
}
|
|
2825
2956
|
async function readOptional2(path) {
|
|
2826
2957
|
try {
|
|
2827
|
-
return await
|
|
2958
|
+
return await readFile9(path, "utf8");
|
|
2828
2959
|
} catch {
|
|
2829
2960
|
return null;
|
|
2830
2961
|
}
|
|
@@ -2892,11 +3023,11 @@ async function exists4(path) {
|
|
|
2892
3023
|
}
|
|
2893
3024
|
|
|
2894
3025
|
// src/commands/proxy.ts
|
|
2895
|
-
import { readFile as
|
|
2896
|
-
import { join as
|
|
3026
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
3027
|
+
import { join as join22 } from "path";
|
|
2897
3028
|
|
|
2898
3029
|
// src/figma-guard.ts
|
|
2899
|
-
import { readdir as readdir3, readFile as
|
|
3030
|
+
import { readdir as readdir3, readFile as readFile10, stat as stat7 } from "fs/promises";
|
|
2900
3031
|
import { join as join16, relative as relative2 } from "path";
|
|
2901
3032
|
import { parseDocument as parseDocument2 } from "yaml";
|
|
2902
3033
|
var DEFAULT_CONFIG = {
|
|
@@ -2906,7 +3037,7 @@ var DEFAULT_CONFIG = {
|
|
|
2906
3037
|
};
|
|
2907
3038
|
async function loadFigmaGuardConfig(projectRoot) {
|
|
2908
3039
|
try {
|
|
2909
|
-
const raw = await
|
|
3040
|
+
const raw = await readFile10(join16(projectRoot, "openspec", "config.yaml"), "utf8");
|
|
2910
3041
|
const doc = parseDocument2(raw);
|
|
2911
3042
|
const fetNode = doc.get("fet", true);
|
|
2912
3043
|
const node = fetNode?.get?.("figmaGuard");
|
|
@@ -3042,7 +3173,7 @@ async function walk(dir, files) {
|
|
|
3042
3173
|
async function readOptional3(path) {
|
|
3043
3174
|
try {
|
|
3044
3175
|
await stat7(path);
|
|
3045
|
-
return await
|
|
3176
|
+
return await readFile10(path, "utf8");
|
|
3046
3177
|
} catch {
|
|
3047
3178
|
return null;
|
|
3048
3179
|
}
|
|
@@ -3050,7 +3181,7 @@ async function readOptional3(path) {
|
|
|
3050
3181
|
|
|
3051
3182
|
// src/ui-display-contract.ts
|
|
3052
3183
|
import { existsSync as existsSync2 } from "fs";
|
|
3053
|
-
import { readdir as readdir4, readFile as
|
|
3184
|
+
import { readdir as readdir4, readFile as readFile11, stat as stat8 } from "fs/promises";
|
|
3054
3185
|
import { join as join17, relative as relative3 } from "path";
|
|
3055
3186
|
import { parse as parse3, parseDocument as parseDocument3 } from "yaml";
|
|
3056
3187
|
var DEFAULT_CONFIG2 = {
|
|
@@ -3062,7 +3193,7 @@ var BACKTICK_PATH_PATTERN = /`([^`]+\.(?:ya?ml|json))`/gi;
|
|
|
3062
3193
|
var OPENAPI_BARE_PATTERN = /\b(openapi\.ya?ml|swagger\.ya?ml|swagger\.json)\b/gi;
|
|
3063
3194
|
async function loadUiDisplayContractConfig(projectRoot) {
|
|
3064
3195
|
try {
|
|
3065
|
-
const raw = await
|
|
3196
|
+
const raw = await readFile11(join17(projectRoot, "openspec", "config.yaml"), "utf8");
|
|
3066
3197
|
const doc = parseDocument3(raw);
|
|
3067
3198
|
const fetNode = doc.get("fet", true);
|
|
3068
3199
|
const node = fetNode?.get?.("uiDisplayContract");
|
|
@@ -3336,10 +3467,263 @@ async function walk2(dir, files) {
|
|
|
3336
3467
|
async function readOptional4(path) {
|
|
3337
3468
|
try {
|
|
3338
3469
|
await stat8(path);
|
|
3339
|
-
return await
|
|
3470
|
+
return await readFile11(path, "utf8");
|
|
3471
|
+
} catch {
|
|
3472
|
+
return null;
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
// src/commands/change-id.ts
|
|
3477
|
+
function toKebabId(value) {
|
|
3478
|
+
return value.trim().toLowerCase().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3479
|
+
}
|
|
3480
|
+
function msg(language, zh, en) {
|
|
3481
|
+
return language === "en" ? en : zh;
|
|
3482
|
+
}
|
|
3483
|
+
async function resolveChangeId(ctx) {
|
|
3484
|
+
if (ctx.changeId) {
|
|
3485
|
+
return ctx.changeId;
|
|
3486
|
+
}
|
|
3487
|
+
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
3488
|
+
if (global.activeChangeId) {
|
|
3489
|
+
return global.activeChangeId;
|
|
3490
|
+
}
|
|
3491
|
+
const inspection = await ctx.openSpec.inspectProject(ctx.projectRoot);
|
|
3492
|
+
if (inspection.changes.length === 1 && inspection.changes[0]) {
|
|
3493
|
+
return inspection.changes[0];
|
|
3494
|
+
}
|
|
3495
|
+
throw new FetError({
|
|
3496
|
+
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
3497
|
+
message: msg(ctx.language, "\u65E0\u6CD5\u786E\u5B9A\u76EE\u6807 change", "Cannot determine which change to use"),
|
|
3498
|
+
details: { openChangeIds: inspection.changes },
|
|
3499
|
+
suggestedCommand: "fet <command> --change <change-id>"
|
|
3500
|
+
});
|
|
3501
|
+
}
|
|
3502
|
+
async function assertChangeExists(ctx, changeId) {
|
|
3503
|
+
const inspection = await ctx.openSpec.inspectChange(ctx.projectRoot, changeId);
|
|
3504
|
+
if (!inspection.exists) {
|
|
3505
|
+
throw new FetError({
|
|
3506
|
+
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
3507
|
+
message: msg(ctx.language, "\u6307\u5B9A\u7684 change \u4E0D\u5B58\u5728", "The specified change does not exist"),
|
|
3508
|
+
details: { changeId },
|
|
3509
|
+
suggestedCommand: `fet doctor`
|
|
3510
|
+
});
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
// src/tdd/config.ts
|
|
3515
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
3516
|
+
import { join as join18 } from "path";
|
|
3517
|
+
import { parseDocument as parseDocument4 } from "yaml";
|
|
3518
|
+
var DEFAULT_CONFIG3 = {
|
|
3519
|
+
enabled: true,
|
|
3520
|
+
mode: "require_before_apply",
|
|
3521
|
+
whenNoTestScript: "block"
|
|
3522
|
+
};
|
|
3523
|
+
async function loadTddConfig(projectRoot) {
|
|
3524
|
+
try {
|
|
3525
|
+
const raw = await readFile12(join18(projectRoot, "openspec", "config.yaml"), "utf8");
|
|
3526
|
+
const doc = parseDocument4(raw);
|
|
3527
|
+
const fetNode = doc.get("fet", true);
|
|
3528
|
+
const node = fetNode?.get?.("tdd");
|
|
3529
|
+
if (!node || typeof node.get !== "function") {
|
|
3530
|
+
return DEFAULT_CONFIG3;
|
|
3531
|
+
}
|
|
3532
|
+
const enabled = node.get("enabled");
|
|
3533
|
+
const modeRaw = node.get("mode");
|
|
3534
|
+
const whenNoTestScriptRaw = node.get("whenNoTestScript");
|
|
3535
|
+
return {
|
|
3536
|
+
enabled: enabled === void 0 ? true : Boolean(enabled),
|
|
3537
|
+
mode: parseGateMode(modeRaw),
|
|
3538
|
+
whenNoTestScript: parseWhenNoTestScript(whenNoTestScriptRaw)
|
|
3539
|
+
};
|
|
3540
|
+
} catch {
|
|
3541
|
+
return DEFAULT_CONFIG3;
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
function parseGateMode(value) {
|
|
3545
|
+
if (value === "off" || value === "optional" || value === "require_before_apply") {
|
|
3546
|
+
return value;
|
|
3547
|
+
}
|
|
3548
|
+
return DEFAULT_CONFIG3.mode;
|
|
3549
|
+
}
|
|
3550
|
+
function parseWhenNoTestScript(value) {
|
|
3551
|
+
if (value === "warn" || value === "skip") {
|
|
3552
|
+
return value;
|
|
3553
|
+
}
|
|
3554
|
+
return DEFAULT_CONFIG3.whenNoTestScript;
|
|
3555
|
+
}
|
|
3556
|
+
function isTddRequired(config) {
|
|
3557
|
+
return config.enabled && config.mode === "require_before_apply";
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
// src/tdd/fingerprint.ts
|
|
3561
|
+
import { createHash } from "crypto";
|
|
3562
|
+
import { readdir as readdir5, readFile as readFile13, stat as stat9 } from "fs/promises";
|
|
3563
|
+
import { join as join19, relative as relative4 } from "path";
|
|
3564
|
+
async function collectPlanningSources(projectRoot, changeId) {
|
|
3565
|
+
const changeRoot = join19(projectRoot, "openspec", "changes", changeId);
|
|
3566
|
+
const sources = [];
|
|
3567
|
+
const rootFiles = ["proposal.md", "tasks.md", "design.md"];
|
|
3568
|
+
for (const name of rootFiles) {
|
|
3569
|
+
const path = join19(changeRoot, name);
|
|
3570
|
+
if (await exists5(path)) {
|
|
3571
|
+
sources.push(relative4(projectRoot, path).replace(/\\/g, "/"));
|
|
3572
|
+
}
|
|
3573
|
+
}
|
|
3574
|
+
const specsDir = join19(changeRoot, "specs");
|
|
3575
|
+
if (await exists5(specsDir)) {
|
|
3576
|
+
for (const file of await walkFiles(specsDir)) {
|
|
3577
|
+
if (file.endsWith(".md")) {
|
|
3578
|
+
sources.push(relative4(projectRoot, file).replace(/\\/g, "/"));
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
return sources.sort();
|
|
3583
|
+
}
|
|
3584
|
+
async function computePlanningFingerprint(projectRoot, changeId) {
|
|
3585
|
+
const sources = await collectPlanningSources(projectRoot, changeId);
|
|
3586
|
+
const hash = createHash("sha256");
|
|
3587
|
+
for (const source of sources) {
|
|
3588
|
+
const content = await readFile13(join19(projectRoot, source), "utf8");
|
|
3589
|
+
hash.update(source);
|
|
3590
|
+
hash.update("\0");
|
|
3591
|
+
hash.update(content);
|
|
3592
|
+
hash.update("\0");
|
|
3593
|
+
}
|
|
3594
|
+
return `sha256:${hash.digest("hex")}`;
|
|
3595
|
+
}
|
|
3596
|
+
async function walkFiles(dir) {
|
|
3597
|
+
const entries = await readdir5(dir, { withFileTypes: true });
|
|
3598
|
+
const files = [];
|
|
3599
|
+
for (const entry of entries) {
|
|
3600
|
+
const path = join19(dir, entry.name);
|
|
3601
|
+
if (entry.isDirectory()) {
|
|
3602
|
+
files.push(...await walkFiles(path));
|
|
3603
|
+
} else if (entry.isFile()) {
|
|
3604
|
+
files.push(path);
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
return files;
|
|
3608
|
+
}
|
|
3609
|
+
async function exists5(path) {
|
|
3610
|
+
try {
|
|
3611
|
+
await stat9(path);
|
|
3612
|
+
return true;
|
|
3613
|
+
} catch {
|
|
3614
|
+
return false;
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
// src/tdd/manifest.ts
|
|
3619
|
+
import { mkdir as mkdir6, readFile as readFile14, stat as stat10 } from "fs/promises";
|
|
3620
|
+
import { dirname as dirname8, join as join20 } from "path";
|
|
3621
|
+
import { parse as parse4, stringify as stringify3 } from "yaml";
|
|
3622
|
+
function tddManifestPath(projectRoot, changeId) {
|
|
3623
|
+
return join20(projectRoot, tddManifestRelativePath(changeId));
|
|
3624
|
+
}
|
|
3625
|
+
async function readTddManifest(projectRoot, changeId) {
|
|
3626
|
+
const path = tddManifestPath(projectRoot, changeId);
|
|
3627
|
+
try {
|
|
3628
|
+
await stat10(path);
|
|
3340
3629
|
} catch {
|
|
3341
3630
|
return null;
|
|
3342
3631
|
}
|
|
3632
|
+
const doc = parse4(await readFile14(path, "utf8"));
|
|
3633
|
+
if (!doc || doc.schemaVersion !== 1 || doc.changeId !== changeId) {
|
|
3634
|
+
return null;
|
|
3635
|
+
}
|
|
3636
|
+
return doc;
|
|
3637
|
+
}
|
|
3638
|
+
async function writeTddManifest(projectRoot, manifest) {
|
|
3639
|
+
const relative6 = tddManifestRelativePath(manifest.changeId);
|
|
3640
|
+
const path = join20(projectRoot, relative6);
|
|
3641
|
+
await mkdir6(dirname8(path), { recursive: true });
|
|
3642
|
+
await atomicWrite(path, stringify3(manifest));
|
|
3643
|
+
return relative6;
|
|
3644
|
+
}
|
|
3645
|
+
async function writeTddResults(projectRoot, results) {
|
|
3646
|
+
const relative6 = tddResultsRelativePath(results.changeId);
|
|
3647
|
+
const path = join20(projectRoot, relative6);
|
|
3648
|
+
await mkdir6(dirname8(path), { recursive: true });
|
|
3649
|
+
await atomicWrite(path, `${JSON.stringify(results, null, 2)}
|
|
3650
|
+
`);
|
|
3651
|
+
return relative6;
|
|
3652
|
+
}
|
|
3653
|
+
function createTddManifest(input) {
|
|
3654
|
+
return {
|
|
3655
|
+
schemaVersion: 1,
|
|
3656
|
+
changeId: input.changeId,
|
|
3657
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3658
|
+
fetVersion: FET_VERSION,
|
|
3659
|
+
planningFingerprint: input.planningFingerprint,
|
|
3660
|
+
sources: input.sources,
|
|
3661
|
+
cases: input.cases,
|
|
3662
|
+
run: {
|
|
3663
|
+
mode: input.cases.length ? "manifest" : "workspace",
|
|
3664
|
+
fallbackCommand: input.testCommand
|
|
3665
|
+
}
|
|
3666
|
+
};
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
// src/tdd/gates.ts
|
|
3670
|
+
async function assertTddReady(ctx, changeId) {
|
|
3671
|
+
const config = await loadTddConfig(ctx.projectRoot);
|
|
3672
|
+
if (!isTddRequired(config)) {
|
|
3673
|
+
return;
|
|
3674
|
+
}
|
|
3675
|
+
const manifest = await readTddManifest(ctx.projectRoot, changeId);
|
|
3676
|
+
if (!manifest) {
|
|
3677
|
+
throw new FetError({
|
|
3678
|
+
code: "STATE_CORRUPTED" /* StateCorrupted */,
|
|
3679
|
+
message: msg(ctx.language, "\u7F3A\u5C11 TDD \u4EA7\u7269\uFF0C\u65E0\u6CD5\u8FDB\u5165 apply\u3002", "TDD artifacts are missing; cannot run apply."),
|
|
3680
|
+
details: { changeId, expected: tddManifestRelativePath(changeId) },
|
|
3681
|
+
suggestedCommand: `fet tdd --change ${changeId}`,
|
|
3682
|
+
recoverable: true
|
|
3683
|
+
});
|
|
3684
|
+
}
|
|
3685
|
+
const fingerprint2 = await computePlanningFingerprint(ctx.projectRoot, changeId);
|
|
3686
|
+
if (manifest.planningFingerprint !== fingerprint2) {
|
|
3687
|
+
throw new FetError({
|
|
3688
|
+
code: "STATE_CORRUPTED" /* StateCorrupted */,
|
|
3689
|
+
message: msg(
|
|
3690
|
+
ctx.language,
|
|
3691
|
+
"\u89C4\u5212\u4EA7\u7269\u5DF2\u53D8\u66F4\uFF0CTDD \u6E05\u5355\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210\u3002",
|
|
3692
|
+
"Planning artifacts changed; TDD manifest is stale. Regenerate it."
|
|
3693
|
+
),
|
|
3694
|
+
details: { changeId, manifestFingerprint: manifest.planningFingerprint, currentFingerprint: fingerprint2 },
|
|
3695
|
+
suggestedCommand: `fet tdd --change ${changeId}`,
|
|
3696
|
+
recoverable: true
|
|
3697
|
+
});
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
async function assertTestPassed(ctx, changeId) {
|
|
3701
|
+
const config = await loadTddConfig(ctx.projectRoot);
|
|
3702
|
+
if (!config.enabled || config.mode === "off") {
|
|
3703
|
+
return;
|
|
3704
|
+
}
|
|
3705
|
+
const change = await ctx.stateStore.readChange(changeId);
|
|
3706
|
+
const testRun = change?.testRun;
|
|
3707
|
+
if (testRun?.status === "skipped") {
|
|
3708
|
+
return;
|
|
3709
|
+
}
|
|
3710
|
+
if (testRun?.status === "passed" && await fingerprintMatches(ctx, changeId, testRun)) {
|
|
3711
|
+
return;
|
|
3712
|
+
}
|
|
3713
|
+
throw new FetError({
|
|
3714
|
+
code: "STATE_CORRUPTED" /* StateCorrupted */,
|
|
3715
|
+
message: msg(ctx.language, "\u672C change \u5C1A\u672A\u901A\u8FC7 fet test\u3002", "This change has not passed fet test yet."),
|
|
3716
|
+
details: { changeId, testRun: testRun ?? null },
|
|
3717
|
+
suggestedCommand: `fet test --change ${changeId}`,
|
|
3718
|
+
recoverable: true
|
|
3719
|
+
});
|
|
3720
|
+
}
|
|
3721
|
+
async function fingerprintMatches(ctx, changeId, testRun) {
|
|
3722
|
+
const current = await computePlanningFingerprint(ctx.projectRoot, changeId);
|
|
3723
|
+
return testRun.planningFingerprint === current;
|
|
3724
|
+
}
|
|
3725
|
+
function invalidateTestRun(state) {
|
|
3726
|
+
state.testRun = null;
|
|
3343
3727
|
}
|
|
3344
3728
|
|
|
3345
3729
|
// src/state/project.ts
|
|
@@ -3369,8 +3753,8 @@ async function git(cwd, args) {
|
|
|
3369
3753
|
}
|
|
3370
3754
|
|
|
3371
3755
|
// src/state/store.ts
|
|
3372
|
-
import { mkdir as
|
|
3373
|
-
import { join as
|
|
3756
|
+
import { mkdir as mkdir7, readFile as readFile15 } from "fs/promises";
|
|
3757
|
+
import { join as join21 } from "path";
|
|
3374
3758
|
|
|
3375
3759
|
// src/language.ts
|
|
3376
3760
|
var DEFAULT_LANGUAGE = "zh-CN";
|
|
@@ -3432,6 +3816,8 @@ function createChangeState(fetVersion, changeId, phase) {
|
|
|
3432
3816
|
lastSyncedAt: null
|
|
3433
3817
|
},
|
|
3434
3818
|
manualVerify: null,
|
|
3819
|
+
tdd: null,
|
|
3820
|
+
testRun: null,
|
|
3435
3821
|
lastOpenSpecCommand: null,
|
|
3436
3822
|
warnings: []
|
|
3437
3823
|
};
|
|
@@ -3488,7 +3874,7 @@ var StateStore = class {
|
|
|
3488
3874
|
project;
|
|
3489
3875
|
async readGlobal() {
|
|
3490
3876
|
try {
|
|
3491
|
-
const value = JSON.parse(await
|
|
3877
|
+
const value = JSON.parse(await readFile15(this.globalPath(), "utf8"));
|
|
3492
3878
|
assertGlobalState(value);
|
|
3493
3879
|
return value;
|
|
3494
3880
|
} catch (error) {
|
|
@@ -3503,13 +3889,13 @@ var StateStore = class {
|
|
|
3503
3889
|
}
|
|
3504
3890
|
async writeGlobal(state) {
|
|
3505
3891
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3506
|
-
await
|
|
3892
|
+
await mkdir7(join21(this.projectRoot, "openspec"), { recursive: true });
|
|
3507
3893
|
await atomicWrite(this.globalPath(), `${JSON.stringify(state, null, 2)}
|
|
3508
3894
|
`);
|
|
3509
3895
|
}
|
|
3510
3896
|
async readChange(changeId) {
|
|
3511
3897
|
try {
|
|
3512
|
-
const value = JSON.parse(await
|
|
3898
|
+
const value = JSON.parse(await readFile15(this.changePath(changeId), "utf8"));
|
|
3513
3899
|
assertChangeState(value);
|
|
3514
3900
|
return value;
|
|
3515
3901
|
} catch (error) {
|
|
@@ -3524,15 +3910,15 @@ var StateStore = class {
|
|
|
3524
3910
|
}
|
|
3525
3911
|
async writeChange(state) {
|
|
3526
3912
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3527
|
-
await
|
|
3913
|
+
await mkdir7(join21(this.projectRoot, "openspec", "changes", state.changeId), { recursive: true });
|
|
3528
3914
|
await atomicWrite(this.changePath(state.changeId), `${JSON.stringify(state, null, 2)}
|
|
3529
3915
|
`);
|
|
3530
3916
|
}
|
|
3531
3917
|
globalPath() {
|
|
3532
|
-
return
|
|
3918
|
+
return join21(this.projectRoot, "openspec", "fet-state.json");
|
|
3533
3919
|
}
|
|
3534
3920
|
changePath(changeId) {
|
|
3535
|
-
return
|
|
3921
|
+
return join21(this.projectRoot, "openspec", "changes", changeId, "fet-state.json");
|
|
3536
3922
|
}
|
|
3537
3923
|
};
|
|
3538
3924
|
function isNotFound(error) {
|
|
@@ -3540,11 +3926,11 @@ function isNotFound(error) {
|
|
|
3540
3926
|
}
|
|
3541
3927
|
|
|
3542
3928
|
// src/state/tasks.ts
|
|
3543
|
-
import { readFile as
|
|
3929
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
3544
3930
|
async function readCompletedTaskIds(tasksPath) {
|
|
3545
3931
|
let content;
|
|
3546
3932
|
try {
|
|
3547
|
-
content = await
|
|
3933
|
+
content = await readFile16(tasksPath, "utf8");
|
|
3548
3934
|
} catch {
|
|
3549
3935
|
return [];
|
|
3550
3936
|
}
|
|
@@ -3678,6 +4064,7 @@ async function applyWorkflowCommand(ctx, args) {
|
|
|
3678
4064
|
await withProjectLock(ctx.projectRoot, { command: "apply", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
3679
4065
|
await assertOpenSpecCommandSupported(ctx, "status", "apply");
|
|
3680
4066
|
await assertOpenSpecCommandSupported(ctx, "instructions", "apply");
|
|
4067
|
+
await assertTddReady(ctx, changeId);
|
|
3681
4068
|
runState.graphContext = await buildWorkflowGraphContext(ctx, {
|
|
3682
4069
|
command: "apply",
|
|
3683
4070
|
args: ["tasks", "--change", changeId],
|
|
@@ -3709,6 +4096,7 @@ async function applyWorkflowCommand(ctx, args) {
|
|
|
3709
4096
|
const applyNextSteps = [
|
|
3710
4097
|
`Read openspec/changes/${changeId}/tasks.md and the instructions output.`,
|
|
3711
4098
|
"Implement pending tasks and update task checkboxes only after the work is done.",
|
|
4099
|
+
...renderTddApplyNextSteps(changeId, ctx.language),
|
|
3712
4100
|
`Run fet verify --change ${changeId}`
|
|
3713
4101
|
];
|
|
3714
4102
|
if (uiContract) {
|
|
@@ -3853,7 +4241,7 @@ async function onboardWorkflowCommand(ctx) {
|
|
|
3853
4241
|
summary: "fet onboard loaded local FET/OpenSpec workflow context.",
|
|
3854
4242
|
nextSteps: [
|
|
3855
4243
|
inspection.changes.length ? `Open changes: ${inspection.changes.join(", ")}` : "No open changes found. Run fet propose <change-id-or-description> to start one.",
|
|
3856
|
-
"Use fet continue to prepare planning artifacts, fet apply for implementation
|
|
4244
|
+
"Use fet continue to prepare planning artifacts, fet tdd then fet apply for implementation, fet test then fet verify before archive."
|
|
3857
4245
|
],
|
|
3858
4246
|
data: { activeChangeId: state.activeChangeId, openChangeIds: inspection.changes, archivedChangeIds: inspection.archived }
|
|
3859
4247
|
});
|
|
@@ -3898,7 +4286,7 @@ async function artifactWorkflowCommand(ctx, command, args) {
|
|
|
3898
4286
|
`Create or update openspec/changes/${changeId}/${resolveOutputPath(status, artifactId)}`,
|
|
3899
4287
|
"Review the artifact with the user before generating the next planning file.",
|
|
3900
4288
|
`Run fet passthrough status --change ${changeId}`,
|
|
3901
|
-
status.isComplete ? `Run fet apply --change ${changeId}` : `Run fet continue --change ${changeId} when ready for the next artifact`
|
|
4289
|
+
status.isComplete ? `Run fet tdd --change ${changeId}, then fet apply --change ${changeId}` : `Run fet continue --change ${changeId} when ready for the next artifact`
|
|
3902
4290
|
];
|
|
3903
4291
|
if (uiContract) {
|
|
3904
4292
|
planningNextSteps.unshift(
|
|
@@ -3940,7 +4328,7 @@ async function ensureProposedChange(ctx, args) {
|
|
|
3940
4328
|
});
|
|
3941
4329
|
}
|
|
3942
4330
|
const input = args.join(" ").trim();
|
|
3943
|
-
const changeId = isKebabId(input) ? input :
|
|
4331
|
+
const changeId = isKebabId(input) ? input : toKebabId2(input);
|
|
3944
4332
|
if (!changeId) {
|
|
3945
4333
|
throw new FetError({
|
|
3946
4334
|
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
@@ -4090,7 +4478,7 @@ function resolveOutputPath(status, artifactId) {
|
|
|
4090
4478
|
function isKebabId(value) {
|
|
4091
4479
|
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
|
|
4092
4480
|
}
|
|
4093
|
-
function
|
|
4481
|
+
function toKebabId2(value) {
|
|
4094
4482
|
return value.trim().toLowerCase().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
4095
4483
|
}
|
|
4096
4484
|
function parseOpenSpecJson(stdout) {
|
|
@@ -4127,7 +4515,7 @@ async function createChangelogEntry(projectRoot, changeId) {
|
|
|
4127
4515
|
};
|
|
4128
4516
|
}
|
|
4129
4517
|
async function appendChangelog(projectRoot, entry) {
|
|
4130
|
-
const changelogPath =
|
|
4518
|
+
const changelogPath = join22(projectRoot, "CHANGELOG.md");
|
|
4131
4519
|
const existing = await readOptional5(changelogPath);
|
|
4132
4520
|
const legacyContentLabel = "\u66F4\u65B0\u5185\u5BB9";
|
|
4133
4521
|
const block = `updateTime: ${entry.updateTime}
|
|
@@ -4140,12 +4528,12 @@ ${block}` : block;
|
|
|
4140
4528
|
await atomicWrite(changelogPath, next);
|
|
4141
4529
|
}
|
|
4142
4530
|
async function readChangeRequirement(projectRoot, changeId) {
|
|
4143
|
-
const changeRoot =
|
|
4144
|
-
const proposal = await readOptional5(
|
|
4531
|
+
const changeRoot = join22(projectRoot, "openspec", "changes", changeId);
|
|
4532
|
+
const proposal = await readOptional5(join22(changeRoot, "proposal.md"));
|
|
4145
4533
|
if (proposal) {
|
|
4146
4534
|
return summarizeMarkdown(proposal);
|
|
4147
4535
|
}
|
|
4148
|
-
const readme = await readOptional5(
|
|
4536
|
+
const readme = await readOptional5(join22(changeRoot, "README.md"));
|
|
4149
4537
|
if (readme) {
|
|
4150
4538
|
return summarizeMarkdown(readme);
|
|
4151
4539
|
}
|
|
@@ -4157,7 +4545,7 @@ function summarizeMarkdown(content) {
|
|
|
4157
4545
|
}
|
|
4158
4546
|
async function readOptional5(path) {
|
|
4159
4547
|
try {
|
|
4160
|
-
return await
|
|
4548
|
+
return await readFile17(path, "utf8");
|
|
4161
4549
|
} catch {
|
|
4162
4550
|
return null;
|
|
4163
4551
|
}
|
|
@@ -4511,10 +4899,402 @@ async function updateCommand(ctx) {
|
|
|
4511
4899
|
});
|
|
4512
4900
|
}
|
|
4513
4901
|
|
|
4902
|
+
// src/commands/tdd.ts
|
|
4903
|
+
import { mkdir as mkdir8 } from "fs/promises";
|
|
4904
|
+
import { join as join24 } from "path";
|
|
4905
|
+
|
|
4906
|
+
// src/tdd/extract-cases.ts
|
|
4907
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
4908
|
+
import { join as join23 } from "path";
|
|
4909
|
+
async function extractCasesFromChange(projectRoot, changeId, sources) {
|
|
4910
|
+
const cases = [];
|
|
4911
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4912
|
+
for (const source of sources) {
|
|
4913
|
+
const content = await readFile18(join23(projectRoot, source), "utf8");
|
|
4914
|
+
if (source.endsWith("tasks.md")) {
|
|
4915
|
+
for (const item of extractTaskCases(content, changeId)) {
|
|
4916
|
+
if (!seen.has(item.id)) {
|
|
4917
|
+
seen.add(item.id);
|
|
4918
|
+
cases.push({ ...item, specRef: `${source} \u2014 ${item.specRef}` });
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
}
|
|
4922
|
+
if (source.includes("/specs/") && source.endsWith(".md")) {
|
|
4923
|
+
for (const item of extractScenarioCases(content, changeId, source)) {
|
|
4924
|
+
if (!seen.has(item.id)) {
|
|
4925
|
+
seen.add(item.id);
|
|
4926
|
+
cases.push(item);
|
|
4927
|
+
}
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
if (!cases.length) {
|
|
4932
|
+
cases.push({
|
|
4933
|
+
id: `${changeId}-smoke`,
|
|
4934
|
+
title: "Change smoke test",
|
|
4935
|
+
specRef: sources[0] ?? `openspec/changes/${changeId}/`,
|
|
4936
|
+
testFile: `tests/changes/${changeId}.test.ts`,
|
|
4937
|
+
testIds: [`${changeId}-smoke`],
|
|
4938
|
+
required: true
|
|
4939
|
+
});
|
|
4940
|
+
}
|
|
4941
|
+
return cases;
|
|
4942
|
+
}
|
|
4943
|
+
function extractTaskCases(content, changeId) {
|
|
4944
|
+
const cases = [];
|
|
4945
|
+
const lines = content.split(/\r?\n/);
|
|
4946
|
+
for (const line of lines) {
|
|
4947
|
+
const numbered = line.match(/^\s*[-*]\s+\[[ xX]?\]\s+((?:\d+(?:\.\d+)*\.?)?)\s*(.+)$/);
|
|
4948
|
+
if (numbered) {
|
|
4949
|
+
const taskId = numbered[1] || String(cases.length + 1);
|
|
4950
|
+
const title = numbered[2]?.trim() ?? taskId;
|
|
4951
|
+
const id = toKebabId(`${changeId}-task-${taskId}`) || `${changeId}-task-${cases.length + 1}`;
|
|
4952
|
+
cases.push({
|
|
4953
|
+
id,
|
|
4954
|
+
title,
|
|
4955
|
+
specRef: title,
|
|
4956
|
+
testFile: suggestTestFile(changeId, id),
|
|
4957
|
+
testIds: [id],
|
|
4958
|
+
required: true
|
|
4959
|
+
});
|
|
4960
|
+
continue;
|
|
4961
|
+
}
|
|
4962
|
+
const plain = line.match(/^\s*[-*]\s+\[[ xX]?\]\s+(.+)$/);
|
|
4963
|
+
if (plain?.[1]) {
|
|
4964
|
+
const title = plain[1].trim();
|
|
4965
|
+
const id = toKebabId(`${changeId}-${title}`) || `${changeId}-task-${cases.length + 1}`;
|
|
4966
|
+
cases.push({
|
|
4967
|
+
id,
|
|
4968
|
+
title,
|
|
4969
|
+
specRef: title,
|
|
4970
|
+
testFile: suggestTestFile(changeId, id),
|
|
4971
|
+
testIds: [id],
|
|
4972
|
+
required: true
|
|
4973
|
+
});
|
|
4974
|
+
}
|
|
4975
|
+
}
|
|
4976
|
+
return cases;
|
|
4977
|
+
}
|
|
4978
|
+
function extractScenarioCases(content, changeId, source) {
|
|
4979
|
+
const cases = [];
|
|
4980
|
+
const lines = content.split(/\r?\n/);
|
|
4981
|
+
let currentRequirement = "";
|
|
4982
|
+
for (const line of lines) {
|
|
4983
|
+
const req = line.match(/^#{2,4}\s+Requirement:\s*(.+)$/i);
|
|
4984
|
+
if (req?.[1]) {
|
|
4985
|
+
currentRequirement = req[1].trim();
|
|
4986
|
+
}
|
|
4987
|
+
const scenario = line.match(/^#{2,4}\s+Scenario:\s*(.+)$/i);
|
|
4988
|
+
if (scenario?.[1]) {
|
|
4989
|
+
const title = scenario[1].trim();
|
|
4990
|
+
const id = toKebabId(`${changeId}-${title}`) || `${changeId}-scenario-${cases.length + 1}`;
|
|
4991
|
+
cases.push({
|
|
4992
|
+
id,
|
|
4993
|
+
title,
|
|
4994
|
+
specRef: currentRequirement ? `Requirement: ${currentRequirement} \u2014 Scenario: ${title}` : `Scenario: ${title}`,
|
|
4995
|
+
testFile: suggestTestFile(changeId, id),
|
|
4996
|
+
testIds: [id],
|
|
4997
|
+
required: true
|
|
4998
|
+
});
|
|
4999
|
+
}
|
|
5000
|
+
}
|
|
5001
|
+
if (!cases.length && content.trim()) {
|
|
5002
|
+
const id = `${changeId}-spec-smoke`;
|
|
5003
|
+
cases.push({
|
|
5004
|
+
id,
|
|
5005
|
+
title: `Spec coverage for ${source}`,
|
|
5006
|
+
specRef: source,
|
|
5007
|
+
testFile: suggestTestFile(changeId, id),
|
|
5008
|
+
testIds: [id],
|
|
5009
|
+
required: true
|
|
5010
|
+
});
|
|
5011
|
+
}
|
|
5012
|
+
return cases;
|
|
5013
|
+
}
|
|
5014
|
+
function suggestTestFile(changeId, caseId) {
|
|
5015
|
+
const segment = caseId.replace(new RegExp(`^${changeId}-?`), "") || "suite";
|
|
5016
|
+
return `tests/changes/${changeId}/${segment}.test.ts`;
|
|
5017
|
+
}
|
|
5018
|
+
|
|
5019
|
+
// src/commands/tdd.ts
|
|
5020
|
+
async function tddCommand(ctx) {
|
|
5021
|
+
await withProjectLock(ctx.projectRoot, { command: "tdd", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
5022
|
+
const changeId = await resolveChangeId(ctx);
|
|
5023
|
+
await assertChangeExists(ctx, changeId);
|
|
5024
|
+
const config = await loadTddConfig(ctx.projectRoot);
|
|
5025
|
+
const sources = await collectPlanningSources(ctx.projectRoot, changeId);
|
|
5026
|
+
if (!sources.length) {
|
|
5027
|
+
throw new FetError({
|
|
5028
|
+
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
5029
|
+
message: msg(ctx.language, "\u672A\u627E\u5230\u53EF\u7528\u4E8E\u751F\u6210 TDD \u7684\u89C4\u5212\u4EA7\u7269\u3002", "No planning artifacts found for TDD generation."),
|
|
5030
|
+
details: { changeId },
|
|
5031
|
+
suggestedCommand: `fet continue --change ${changeId}`,
|
|
5032
|
+
recoverable: true
|
|
5033
|
+
});
|
|
5034
|
+
}
|
|
5035
|
+
const planningFingerprint = await computePlanningFingerprint(ctx.projectRoot, changeId);
|
|
5036
|
+
const cases = await extractCasesFromChange(ctx.projectRoot, changeId, sources);
|
|
5037
|
+
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
5038
|
+
const testCommand2 = scan.commands.test?.command ?? scan.commands["test:unit"]?.command ?? null;
|
|
5039
|
+
const manifest = createTddManifest({
|
|
5040
|
+
changeId,
|
|
5041
|
+
planningFingerprint,
|
|
5042
|
+
sources,
|
|
5043
|
+
cases,
|
|
5044
|
+
testCommand: testCommand2
|
|
5045
|
+
});
|
|
5046
|
+
const manifestPath2 = await writeTddManifest(ctx.projectRoot, manifest);
|
|
5047
|
+
const fetDir = join24(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
5048
|
+
await mkdir8(fetDir, { recursive: true });
|
|
5049
|
+
const instructionsPath = tddInstructionsRelativePath(changeId);
|
|
5050
|
+
const specPath = tddSpecRelativePath(changeId);
|
|
5051
|
+
await atomicWrite(join24(ctx.projectRoot, instructionsPath), renderTddInstructions(changeId, manifest, ctx.language));
|
|
5052
|
+
await atomicWrite(join24(ctx.projectRoot, specPath), renderTddSpec(changeId, manifest, ctx.language));
|
|
5053
|
+
const changeState = await ctx.stateStore.getOrCreateChange(changeId, "implement");
|
|
5054
|
+
changeState.tdd = {
|
|
5055
|
+
status: "ready",
|
|
5056
|
+
generatedAt: manifest.generatedAt,
|
|
5057
|
+
planningFingerprint,
|
|
5058
|
+
manifestPath: manifestPath2
|
|
5059
|
+
};
|
|
5060
|
+
invalidateTestRun(changeState);
|
|
5061
|
+
changeState.currentPhase = "implement";
|
|
5062
|
+
changeState.phases.implement = { status: "in_progress", updatedAt: manifest.generatedAt };
|
|
5063
|
+
await ctx.stateStore.writeChange(changeState);
|
|
5064
|
+
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
5065
|
+
global.activeChangeId = changeId;
|
|
5066
|
+
await ctx.stateStore.writeGlobal(global);
|
|
5067
|
+
ctx.output.result({
|
|
5068
|
+
ok: true,
|
|
5069
|
+
command: "tdd",
|
|
5070
|
+
summary: msg(
|
|
5071
|
+
ctx.language,
|
|
5072
|
+
`\u5DF2\u4E3A change "${changeId}" \u751F\u6210 TDD \u4EA7\u7269\uFF08${cases.length} \u4E2A\u7528\u4F8B\uFF09\u3002`,
|
|
5073
|
+
`Generated TDD artifacts for change "${changeId}" (${cases.length} case(s)).`
|
|
5074
|
+
),
|
|
5075
|
+
warnings: !testCommand2 && config.whenNoTestScript !== "skip" ? [
|
|
5076
|
+
msg(
|
|
5077
|
+
ctx.language,
|
|
5078
|
+
"\u9879\u76EE\u672A\u914D\u7F6E test \u811A\u672C\uFF1B\u5B9E\u73B0\u540E\u53EF\u80FD\u65E0\u6CD5\u8FD0\u884C fet test\u3002",
|
|
5079
|
+
"No test script found in the project; fet test may fail after implementation."
|
|
5080
|
+
)
|
|
5081
|
+
] : void 0,
|
|
5082
|
+
nextSteps: [
|
|
5083
|
+
msg(ctx.language, `\u9605\u8BFB ${instructionsPath}\uFF0C\u7531 IDE/AI \u521B\u5EFA/\u66F4\u65B0\u6E05\u5355\u4E2D\u7684\u6D4B\u8BD5\u6587\u4EF6\u3002`, `Read ${instructionsPath} and create/update test files listed in the manifest.`),
|
|
5084
|
+
msg(ctx.language, `\u5B8C\u6210\u540E\u8FD0\u884C fet apply --change ${changeId}`, `Then run fet apply --change ${changeId}`),
|
|
5085
|
+
msg(ctx.language, `\u5B9E\u73B0\u540E\u8FD0\u884C fet test --change ${changeId}`, `After implementation, run fet test --change ${changeId}`)
|
|
5086
|
+
],
|
|
5087
|
+
data: {
|
|
5088
|
+
changeId,
|
|
5089
|
+
manifestPath: manifestPath2,
|
|
5090
|
+
specPath,
|
|
5091
|
+
instructionsPath,
|
|
5092
|
+
caseCount: cases.length,
|
|
5093
|
+
planningFingerprint,
|
|
5094
|
+
testCommand: testCommand2
|
|
5095
|
+
}
|
|
5096
|
+
});
|
|
5097
|
+
});
|
|
5098
|
+
}
|
|
5099
|
+
|
|
5100
|
+
// src/commands/run-script.ts
|
|
5101
|
+
import { spawn as spawn2 } from "child_process";
|
|
5102
|
+
async function runShellCommand(commandLine, cwd, extraArgs = []) {
|
|
5103
|
+
const parts = splitCommandLine(commandLine);
|
|
5104
|
+
const executable = parts[0];
|
|
5105
|
+
if (!executable) {
|
|
5106
|
+
throw new Error("Empty command");
|
|
5107
|
+
}
|
|
5108
|
+
const args = [...parts.slice(1), ...extraArgs];
|
|
5109
|
+
return runProcess(executable, args, cwd, commandLine);
|
|
5110
|
+
}
|
|
5111
|
+
async function runProcess(executable, args, cwd, label = executable) {
|
|
5112
|
+
return new Promise((resolve2) => {
|
|
5113
|
+
const stdout = [];
|
|
5114
|
+
const stderr = [];
|
|
5115
|
+
const child = spawn2(executable, args, {
|
|
5116
|
+
cwd,
|
|
5117
|
+
shell: process.platform === "win32",
|
|
5118
|
+
env: process.env
|
|
5119
|
+
});
|
|
5120
|
+
child.stdout?.on("data", (chunk) => stdout.push(chunk));
|
|
5121
|
+
child.stderr?.on("data", (chunk) => stderr.push(chunk));
|
|
5122
|
+
child.on("close", (code, signal) => {
|
|
5123
|
+
resolve2({
|
|
5124
|
+
command: label,
|
|
5125
|
+
args,
|
|
5126
|
+
exitCode: code ?? 1,
|
|
5127
|
+
signal,
|
|
5128
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
5129
|
+
stderr: Buffer.concat(stderr).toString("utf8")
|
|
5130
|
+
});
|
|
5131
|
+
});
|
|
5132
|
+
child.on("error", () => {
|
|
5133
|
+
resolve2({
|
|
5134
|
+
command: label,
|
|
5135
|
+
args,
|
|
5136
|
+
exitCode: 1,
|
|
5137
|
+
signal: null,
|
|
5138
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
5139
|
+
stderr: Buffer.concat(stderr).toString("utf8")
|
|
5140
|
+
});
|
|
5141
|
+
});
|
|
5142
|
+
});
|
|
5143
|
+
}
|
|
5144
|
+
function splitCommandLine(commandLine) {
|
|
5145
|
+
return commandLine.trim().split(/\s+/);
|
|
5146
|
+
}
|
|
5147
|
+
|
|
5148
|
+
// src/commands/test.ts
|
|
5149
|
+
async function testCommand(ctx, options) {
|
|
5150
|
+
await withProjectLock(ctx.projectRoot, { command: "test", cwd: ctx.cwd, fetVersion: ctx.fetVersion }, async () => {
|
|
5151
|
+
const changeId = await resolveChangeId(ctx);
|
|
5152
|
+
await assertChangeExists(ctx, changeId);
|
|
5153
|
+
const config = await loadTddConfig(ctx.projectRoot);
|
|
5154
|
+
const manifest = await readTddManifest(ctx.projectRoot, changeId);
|
|
5155
|
+
if (!manifest) {
|
|
5156
|
+
throw new FetError({
|
|
5157
|
+
code: "STATE_CORRUPTED" /* StateCorrupted */,
|
|
5158
|
+
message: msg(ctx.language, "\u7F3A\u5C11 TDD \u6E05\u5355\uFF0C\u65E0\u6CD5\u8FD0\u884C fet test\u3002", "TDD manifest is missing; cannot run fet test."),
|
|
5159
|
+
details: { changeId },
|
|
5160
|
+
suggestedCommand: `fet tdd --change ${changeId}`,
|
|
5161
|
+
recoverable: true
|
|
5162
|
+
});
|
|
5163
|
+
}
|
|
5164
|
+
const planningFingerprint = await computePlanningFingerprint(ctx.projectRoot, changeId);
|
|
5165
|
+
if (manifest.planningFingerprint !== planningFingerprint) {
|
|
5166
|
+
throw new FetError({
|
|
5167
|
+
code: "STATE_CORRUPTED" /* StateCorrupted */,
|
|
5168
|
+
message: msg(ctx.language, "TDD \u6E05\u5355\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C fet tdd\u3002", "TDD manifest is stale; rerun fet tdd."),
|
|
5169
|
+
details: { changeId },
|
|
5170
|
+
suggestedCommand: `fet tdd --change ${changeId}`,
|
|
5171
|
+
recoverable: true
|
|
5172
|
+
});
|
|
5173
|
+
}
|
|
5174
|
+
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
5175
|
+
const testCommand2 = manifest.run.fallbackCommand ?? scan.commands.test?.command ?? scan.commands["test:unit"]?.command ?? null;
|
|
5176
|
+
if (!testCommand2) {
|
|
5177
|
+
if (config.whenNoTestScript === "skip") {
|
|
5178
|
+
await recordSkippedTestRun(ctx, changeId, planningFingerprint, manifestPath(changeId));
|
|
5179
|
+
ctx.output.result({
|
|
5180
|
+
ok: true,
|
|
5181
|
+
command: "test",
|
|
5182
|
+
summary: msg(ctx.language, "\u672A\u914D\u7F6E test \u811A\u672C\uFF0C\u5DF2\u8DF3\u8FC7 fet test\u3002", "No test script configured; fet test skipped."),
|
|
5183
|
+
warnings: [msg(ctx.language, "\u8DF3\u8FC7\u540E\u53EF\u76F4\u63A5 fet verify\uFF08\u82E5\u9879\u76EE\u7B56\u7565\u5141\u8BB8\uFF09\u3002", "You may proceed to fet verify if your policy allows.")],
|
|
5184
|
+
data: { changeId, skipped: true }
|
|
5185
|
+
});
|
|
5186
|
+
return;
|
|
5187
|
+
}
|
|
5188
|
+
throw new FetError({
|
|
5189
|
+
code: "CONFIG_INVALID" /* ConfigInvalid */,
|
|
5190
|
+
message: msg(ctx.language, "\u672A\u627E\u5230 test \u811A\u672C\uFF0C\u65E0\u6CD5\u6267\u884C fet test\u3002", "No test script found; cannot run fet test."),
|
|
5191
|
+
suggestedCommand: "\u5728 package.json \u4E2D\u6DFB\u52A0 scripts.test \u6216\u914D\u7F6E openspec/config.yaml fet.tdd.whenNoTestScript: skip"
|
|
5192
|
+
});
|
|
5193
|
+
}
|
|
5194
|
+
const extraArgs = buildTestArgs(manifest);
|
|
5195
|
+
const planLabel = `${testCommand2}${extraArgs.length ? ` ${extraArgs.join(" ")}` : ""}`;
|
|
5196
|
+
if (options.plan) {
|
|
5197
|
+
ctx.output.result({
|
|
5198
|
+
ok: true,
|
|
5199
|
+
command: "test",
|
|
5200
|
+
summary: msg(ctx.language, "\u5DF2\u751F\u6210 fet test \u6267\u884C\u8BA1\u5212\u3002", "Generated fet test execution plan."),
|
|
5201
|
+
data: {
|
|
5202
|
+
changeId,
|
|
5203
|
+
command: planLabel,
|
|
5204
|
+
cases: manifest.cases.map((item) => ({ id: item.id, testFile: item.testFile }))
|
|
5205
|
+
},
|
|
5206
|
+
nextSteps: [`fet test --change ${changeId}`]
|
|
5207
|
+
});
|
|
5208
|
+
return;
|
|
5209
|
+
}
|
|
5210
|
+
const result = await runShellCommand(testCommand2, ctx.projectRoot, extraArgs);
|
|
5211
|
+
const caseResults = manifest.cases.map((item) => ({
|
|
5212
|
+
id: item.id,
|
|
5213
|
+
status: result.exitCode === 0 ? "passed" : "failed",
|
|
5214
|
+
exitCode: result.exitCode,
|
|
5215
|
+
message: result.exitCode === 0 ? void 0 : msg(ctx.language, "\u6D4B\u8BD5\u547D\u4EE4\u672A\u901A\u8FC7", "Test command failed")
|
|
5216
|
+
}));
|
|
5217
|
+
const resultsDoc = {
|
|
5218
|
+
schemaVersion: 1,
|
|
5219
|
+
changeId,
|
|
5220
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5221
|
+
command: planLabel,
|
|
5222
|
+
exitCode: result.exitCode,
|
|
5223
|
+
planningFingerprint,
|
|
5224
|
+
cases: caseResults
|
|
5225
|
+
};
|
|
5226
|
+
const resultsPath = await writeTddResults(ctx.projectRoot, resultsDoc);
|
|
5227
|
+
const changeState = await ctx.stateStore.getOrCreateChange(changeId, "implement");
|
|
5228
|
+
changeState.testRun = {
|
|
5229
|
+
status: result.exitCode === 0 ? "passed" : "failed",
|
|
5230
|
+
ranAt: resultsDoc.ranAt,
|
|
5231
|
+
command: planLabel,
|
|
5232
|
+
exitCode: result.exitCode,
|
|
5233
|
+
planningFingerprint,
|
|
5234
|
+
manifestPath: manifestPath(changeId),
|
|
5235
|
+
resultsPath
|
|
5236
|
+
};
|
|
5237
|
+
await ctx.stateStore.writeChange(changeState);
|
|
5238
|
+
if (result.exitCode !== 0) {
|
|
5239
|
+
throw new FetError({
|
|
5240
|
+
code: "OPENSPEC_COMMAND_FAILED" /* OpenSpecCommandFailed */,
|
|
5241
|
+
message: msg(ctx.language, "fet test \u672A\u901A\u8FC7\u3002", "fet test failed."),
|
|
5242
|
+
details: {
|
|
5243
|
+
changeId,
|
|
5244
|
+
exitCode: result.exitCode,
|
|
5245
|
+
command: planLabel,
|
|
5246
|
+
resultsPath: tddResultsRelativePath(changeId),
|
|
5247
|
+
stderr: truncate(result.stderr)
|
|
5248
|
+
},
|
|
5249
|
+
suggestedCommand: `fet test --change ${changeId}`,
|
|
5250
|
+
recoverable: true
|
|
5251
|
+
});
|
|
5252
|
+
}
|
|
5253
|
+
ctx.output.result({
|
|
5254
|
+
ok: true,
|
|
5255
|
+
command: "test",
|
|
5256
|
+
summary: msg(ctx.language, `change "${changeId}" \u7684\u5355\u6D4B\u5DF2\u901A\u8FC7\u3002`, `Unit tests passed for change "${changeId}".`),
|
|
5257
|
+
nextSteps: [`fet verify --change ${changeId}`],
|
|
5258
|
+
data: {
|
|
5259
|
+
changeId,
|
|
5260
|
+
command: planLabel,
|
|
5261
|
+
resultsPath: tddResultsRelativePath(changeId),
|
|
5262
|
+
cases: caseResults
|
|
5263
|
+
}
|
|
5264
|
+
});
|
|
5265
|
+
});
|
|
5266
|
+
}
|
|
5267
|
+
function manifestPath(changeId) {
|
|
5268
|
+
return tddManifestRelativePath(changeId);
|
|
5269
|
+
}
|
|
5270
|
+
function buildTestArgs(manifest) {
|
|
5271
|
+
if (!manifest || manifest.run.mode === "workspace") {
|
|
5272
|
+
return [];
|
|
5273
|
+
}
|
|
5274
|
+
const files = [...new Set(manifest.cases.map((item) => item.testFile).filter(Boolean))];
|
|
5275
|
+
return files.length ? ["--", ...files] : [];
|
|
5276
|
+
}
|
|
5277
|
+
async function recordSkippedTestRun(ctx, changeId, planningFingerprint, manifestPathValue) {
|
|
5278
|
+
const changeState = await ctx.stateStore.getOrCreateChange(changeId, "implement");
|
|
5279
|
+
changeState.testRun = {
|
|
5280
|
+
status: "skipped",
|
|
5281
|
+
ranAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5282
|
+
command: "(skipped)",
|
|
5283
|
+
exitCode: 0,
|
|
5284
|
+
planningFingerprint,
|
|
5285
|
+
manifestPath: manifestPathValue,
|
|
5286
|
+
resultsPath: null
|
|
5287
|
+
};
|
|
5288
|
+
await ctx.stateStore.writeChange(changeState);
|
|
5289
|
+
}
|
|
5290
|
+
function truncate(value, max = 2e3) {
|
|
5291
|
+
return value.length > max ? `${value.slice(0, max)}\u2026` : value;
|
|
5292
|
+
}
|
|
5293
|
+
|
|
4514
5294
|
// src/commands/verify.ts
|
|
4515
|
-
import { createHash } from "crypto";
|
|
4516
|
-
import { mkdir as
|
|
4517
|
-
import { join as
|
|
5295
|
+
import { createHash as createHash2 } from "crypto";
|
|
5296
|
+
import { mkdir as mkdir9, readFile as readFile19, stat as stat11 } from "fs/promises";
|
|
5297
|
+
import { join as join25 } from "path";
|
|
4518
5298
|
async function verifyCommand(ctx, options) {
|
|
4519
5299
|
if (options.auto) {
|
|
4520
5300
|
const scan = await ctx.scanner.scan(ctx.projectRoot, {});
|
|
@@ -4580,10 +5360,11 @@ async function verifyCommand(ctx, options) {
|
|
|
4580
5360
|
}
|
|
4581
5361
|
async function writeInstructions(ctx, changeId) {
|
|
4582
5362
|
await assertChangeExists(ctx, changeId);
|
|
5363
|
+
await assertTestPassed(ctx, changeId);
|
|
4583
5364
|
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4584
|
-
const dir =
|
|
4585
|
-
const instructionsPath =
|
|
4586
|
-
await
|
|
5365
|
+
const dir = join25(ctx.projectRoot, "openspec", "changes", changeId, ".fet");
|
|
5366
|
+
const instructionsPath = join25(dir, "verify-instructions.md");
|
|
5367
|
+
await mkdir9(dir, { recursive: true });
|
|
4587
5368
|
await atomicWrite(instructionsPath, renderVerifyInstructions(changeId, generatedAt));
|
|
4588
5369
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
4589
5370
|
state.currentPhase = "verify";
|
|
@@ -4598,9 +5379,10 @@ async function writeInstructions(ctx, changeId) {
|
|
|
4598
5379
|
}
|
|
4599
5380
|
async function markDone(ctx, changeId) {
|
|
4600
5381
|
await assertChangeExists(ctx, changeId);
|
|
5382
|
+
await assertTestPassed(ctx, changeId);
|
|
4601
5383
|
const declaredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4602
|
-
const instructionsPath =
|
|
4603
|
-
const instructions = await readInstructions(instructionsPath, changeId);
|
|
5384
|
+
const instructionsPath = join25(ctx.projectRoot, "openspec", "changes", changeId, ".fet", "verify-instructions.md");
|
|
5385
|
+
const instructions = await readInstructions(ctx, instructionsPath, changeId);
|
|
4604
5386
|
const instructionsGeneratedAt = readFrontMatterValue(instructions, "generatedAt") ?? declaredAt;
|
|
4605
5387
|
const state = await ctx.stateStore.getOrCreateChange(changeId, "verify");
|
|
4606
5388
|
state.currentPhase = "verify";
|
|
@@ -4621,26 +5403,19 @@ async function markDone(ctx, changeId) {
|
|
|
4621
5403
|
nextSteps: [`fet sync --change ${changeId}`, `fet archive --change ${changeId}`]
|
|
4622
5404
|
});
|
|
4623
5405
|
}
|
|
4624
|
-
async function
|
|
4625
|
-
const inspection = await ctx.openSpec.inspectChange(ctx.projectRoot, changeId);
|
|
4626
|
-
if (!inspection.exists) {
|
|
4627
|
-
throw new FetError({
|
|
4628
|
-
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
4629
|
-
message: "\u6307\u5B9A\u7684 change \u4E0D\u5B58\u5728",
|
|
4630
|
-
details: { changeId },
|
|
4631
|
-
suggestedCommand: "fet verify --change <change-id>"
|
|
4632
|
-
});
|
|
4633
|
-
}
|
|
4634
|
-
}
|
|
4635
|
-
async function readInstructions(path, changeId) {
|
|
5406
|
+
async function readInstructions(ctx, path, changeId) {
|
|
4636
5407
|
try {
|
|
4637
|
-
await
|
|
4638
|
-
const content = await
|
|
5408
|
+
await stat11(path);
|
|
5409
|
+
const content = await readFile19(path, "utf8");
|
|
4639
5410
|
const fileChangeId = readFrontMatterValue(content, "changeId");
|
|
4640
5411
|
if (fileChangeId !== changeId) {
|
|
4641
5412
|
throw new FetError({
|
|
4642
5413
|
code: "STATE_CORRUPTED" /* StateCorrupted */,
|
|
4643
|
-
message:
|
|
5414
|
+
message: msg(
|
|
5415
|
+
ctx.language,
|
|
5416
|
+
"\u9A8C\u8BC1\u6307\u4EE4\u6587\u4EF6\u4E0E\u5F53\u524D change \u4E0D\u5339\u914D",
|
|
5417
|
+
"Verify instructions do not match the current change"
|
|
5418
|
+
),
|
|
4644
5419
|
details: { expected: changeId, actual: fileChangeId },
|
|
4645
5420
|
suggestedCommand: `fet verify --change ${changeId}`
|
|
4646
5421
|
});
|
|
@@ -4652,7 +5427,7 @@ async function readInstructions(path, changeId) {
|
|
|
4652
5427
|
}
|
|
4653
5428
|
throw new FetError({
|
|
4654
5429
|
code: "STATE_CORRUPTED" /* StateCorrupted */,
|
|
4655
|
-
message: "\u9A8C\u8BC1\u6307\u4EE4\u6587\u4EF6\u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u8BFB\u53D6",
|
|
5430
|
+
message: msg(ctx.language, "\u9A8C\u8BC1\u6307\u4EE4\u6587\u4EF6\u4E0D\u5B58\u5728\u6216\u65E0\u6CD5\u8BFB\u53D6", "Verify instructions file is missing or unreadable"),
|
|
4656
5431
|
details: { path },
|
|
4657
5432
|
suggestedCommand: `fet verify --change ${changeId}`
|
|
4658
5433
|
});
|
|
@@ -4663,26 +5438,7 @@ function readFrontMatterValue(content, key) {
|
|
|
4663
5438
|
return match?.[1]?.trim() ?? null;
|
|
4664
5439
|
}
|
|
4665
5440
|
function fingerprint(value) {
|
|
4666
|
-
return `sha256:${
|
|
4667
|
-
}
|
|
4668
|
-
async function resolveChangeId(ctx) {
|
|
4669
|
-
if (ctx.changeId) {
|
|
4670
|
-
return ctx.changeId;
|
|
4671
|
-
}
|
|
4672
|
-
const global = await ctx.stateStore.getOrCreateGlobal();
|
|
4673
|
-
if (global.activeChangeId) {
|
|
4674
|
-
return global.activeChangeId;
|
|
4675
|
-
}
|
|
4676
|
-
const inspection = await ctx.openSpec.inspectProject(ctx.projectRoot);
|
|
4677
|
-
if (inspection.changes.length === 1 && inspection.changes[0]) {
|
|
4678
|
-
return inspection.changes[0];
|
|
4679
|
-
}
|
|
4680
|
-
throw new FetError({
|
|
4681
|
-
code: "INVALID_ARGUMENTS" /* InvalidArguments */,
|
|
4682
|
-
message: "\u65E0\u6CD5\u786E\u5B9A\u8981\u9A8C\u8BC1\u7684 change",
|
|
4683
|
-
details: { openChangeIds: inspection.changes },
|
|
4684
|
-
suggestedCommand: "fet verify --change <change-id>"
|
|
4685
|
-
});
|
|
5441
|
+
return `sha256:${createHash2("sha256").update(JSON.stringify(value)).digest("hex")}`;
|
|
4686
5442
|
}
|
|
4687
5443
|
|
|
4688
5444
|
// src/model-policy.ts
|
|
@@ -4773,11 +5529,12 @@ function renderIdeModelPolicy(command, language = "zh-CN") {
|
|
|
4773
5529
|
import { resolve } from "path";
|
|
4774
5530
|
|
|
4775
5531
|
// src/adapters/codex/index.ts
|
|
4776
|
-
import { mkdir as
|
|
5532
|
+
import { mkdir as mkdir10, readFile as readFile20, stat as stat12 } from "fs/promises";
|
|
4777
5533
|
import { homedir } from "os";
|
|
4778
|
-
import { dirname as
|
|
5534
|
+
import { dirname as dirname9, join as join26 } from "path";
|
|
4779
5535
|
|
|
4780
5536
|
// src/adapters/commands.ts
|
|
5537
|
+
var FET_STANDALONE_COMMANDS = ["tdd", "test"];
|
|
4781
5538
|
var FET_WORKFLOW_COMMANDS = [
|
|
4782
5539
|
"explore",
|
|
4783
5540
|
"propose",
|
|
@@ -4792,7 +5549,7 @@ var FET_WORKFLOW_COMMANDS = [
|
|
|
4792
5549
|
"onboard"
|
|
4793
5550
|
];
|
|
4794
5551
|
var FET_GRAPH_COMMANDS = ["graph-status", "graph-setup", "graph-init", "graph-refresh", "graph-doctor", "graph-handoff"];
|
|
4795
|
-
var FET_ADAPTER_COMMANDS = [...FET_WORKFLOW_COMMANDS, "update", "fill-context", "passthrough", ...FET_GRAPH_COMMANDS];
|
|
5552
|
+
var FET_ADAPTER_COMMANDS = [...FET_WORKFLOW_COMMANDS, ...FET_STANDALONE_COMMANDS, "update", "fill-context", "passthrough", ...FET_GRAPH_COMMANDS];
|
|
4796
5553
|
function renderFetAdapterUsage(command, args = "[...args]") {
|
|
4797
5554
|
if (command.startsWith("graph-")) {
|
|
4798
5555
|
const subcommand = command.slice("graph-".length);
|
|
@@ -5860,7 +6617,7 @@ var CodexAdapter = class {
|
|
|
5860
6617
|
adapterVersion = 1;
|
|
5861
6618
|
async detect(projectRoot) {
|
|
5862
6619
|
return {
|
|
5863
|
-
detected: await
|
|
6620
|
+
detected: await exists6(join26(projectRoot, ".codex")) || await exists6(join26(projectRoot, "AGENTS.md")),
|
|
5864
6621
|
reason: "Codex adapter is available for projects that use AGENTS.md"
|
|
5865
6622
|
};
|
|
5866
6623
|
}
|
|
@@ -5899,7 +6656,7 @@ var CodexAdapter = class {
|
|
|
5899
6656
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
5900
6657
|
await createBackup(target);
|
|
5901
6658
|
}
|
|
5902
|
-
await
|
|
6659
|
+
await mkdir10(dirname9(target), { recursive: true });
|
|
5903
6660
|
await atomicWrite(target, file.content);
|
|
5904
6661
|
written.push(displayPath);
|
|
5905
6662
|
}
|
|
@@ -5926,9 +6683,9 @@ var CodexAdapter = class {
|
|
|
5926
6683
|
};
|
|
5927
6684
|
function resolveTarget(projectRoot, file) {
|
|
5928
6685
|
if (file.root === "codex-home") {
|
|
5929
|
-
return
|
|
6686
|
+
return join26(resolveCodexHome(), file.path);
|
|
5930
6687
|
}
|
|
5931
|
-
return
|
|
6688
|
+
return join26(projectRoot, file.path);
|
|
5932
6689
|
}
|
|
5933
6690
|
function displayPathFor(file) {
|
|
5934
6691
|
if (file.root === "codex-home") {
|
|
@@ -5937,18 +6694,18 @@ function displayPathFor(file) {
|
|
|
5937
6694
|
return file.path;
|
|
5938
6695
|
}
|
|
5939
6696
|
function resolveCodexHome() {
|
|
5940
|
-
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ??
|
|
6697
|
+
return process.env.FET_CODEX_HOME ?? process.env.CODEX_HOME ?? join26(homedir(), ".codex");
|
|
5941
6698
|
}
|
|
5942
6699
|
async function readExisting(path) {
|
|
5943
6700
|
try {
|
|
5944
|
-
return await
|
|
6701
|
+
return await readFile20(path, "utf8");
|
|
5945
6702
|
} catch {
|
|
5946
6703
|
return null;
|
|
5947
6704
|
}
|
|
5948
6705
|
}
|
|
5949
|
-
async function
|
|
6706
|
+
async function exists6(path) {
|
|
5950
6707
|
try {
|
|
5951
|
-
await
|
|
6708
|
+
await stat12(path);
|
|
5952
6709
|
return true;
|
|
5953
6710
|
} catch {
|
|
5954
6711
|
return false;
|
|
@@ -5956,8 +6713,8 @@ async function exists5(path) {
|
|
|
5956
6713
|
}
|
|
5957
6714
|
|
|
5958
6715
|
// src/adapters/cursor/index.ts
|
|
5959
|
-
import { mkdir as
|
|
5960
|
-
import { dirname as
|
|
6716
|
+
import { mkdir as mkdir11, readFile as readFile21, stat as stat13 } from "fs/promises";
|
|
6717
|
+
import { dirname as dirname10, join as join27 } from "path";
|
|
5961
6718
|
|
|
5962
6719
|
// src/adapters/cursor/templates.ts
|
|
5963
6720
|
function cursorFigmaStopRuleFile(language = DEFAULT_LANGUAGE) {
|
|
@@ -6089,6 +6846,9 @@ After installation, verify with \`gitnexus --version\`, then run \`fet graph ini
|
|
|
6089
6846
|
if (command === "apply") {
|
|
6090
6847
|
return renderApplySkill(usage, language);
|
|
6091
6848
|
}
|
|
6849
|
+
if (command === "tdd" || command === "test") {
|
|
6850
|
+
return renderTddTestSkill(command, usage, language);
|
|
6851
|
+
}
|
|
6092
6852
|
if (command === "propose" || command === "continue" || command === "ff") {
|
|
6093
6853
|
return renderPlanningSkill(command, usage, language);
|
|
6094
6854
|
}
|
|
@@ -6206,6 +6966,38 @@ ${figmaBlock}
|
|
|
6206
6966
|
${uiContractBlock}
|
|
6207
6967
|
|
|
6208
6968
|
\u6267\u884C\u524D\u8BF7\u9605\u8BFB AGENTS.md\u3001openspec/config.yaml \u4E0E\u5F53\u524D change \u4E0B\u7684 OpenSpec \u4EA7\u7269\u3002
|
|
6969
|
+
|
|
6970
|
+
\u5B9E\u65BD\u524D\u987B\u5DF2\u8FD0\u884C \`fet tdd\`\uFF1B\u5B58\u5728 \`openspec/changes/<change-id>/.fet/tdd-manifest.yaml\` \u65F6\u6309\u6E05\u5355\u7F16\u5199\u6D4B\u8BD5\u4E0E\u5B9E\u73B0\uFF0C\u5E76\u5728 \`fet verify\` \u524D\u5148 \`fet test\`\u3002
|
|
6971
|
+
`;
|
|
6972
|
+
}
|
|
6973
|
+
function renderTddTestSkill(command, usage, language) {
|
|
6974
|
+
const description = command === "tdd" ? language === "en" ? "Generate per-change TDD manifest and test instructions" : "\u751F\u6210\u672C change \u7684 TDD \u6E05\u5355\u4E0E\u6D4B\u8BD5\u6307\u5F15" : language === "en" ? "Run unit tests scoped to the change TDD manifest" : "\u6309 change TDD \u6E05\u5355\u8FD0\u884C\u5355\u6D4B";
|
|
6975
|
+
const body = command === "tdd" ? language === "en" ? `After planning artifacts exist, run this before \`fet apply\`. It writes \`openspec/changes/<change-id>/.fet/tdd-manifest.yaml\`, \`tdd-spec.md\`, and \`tdd-instructions.md\`. Then create failing tests in the repo before implementation.` : `\u89C4\u5212\u4EA7\u7269\u5C31\u7EEA\u540E\u3001\`fet apply\` \u4E4B\u524D\u8FD0\u884C\u3002\u4F1A\u5199\u5165 \`openspec/changes/<change-id>/.fet/tdd-manifest.yaml\`\u3001\`tdd-spec.md\`\u3001\`tdd-instructions.md\`\uFF0C\u518D\u5728\u4ED3\u5E93\u4E2D\u7F16\u5199\u9884\u671F\u5931\u8D25\u7684\u6D4B\u8BD5\u3002` : language === "en" ? `Run after implementation. Requires a current \`tdd-manifest.yaml\`. Records pass/fail in FET state; \`fet verify\` is blocked until this passes (unless configured to skip).` : `\u5B9E\u73B0\u5B8C\u6210\u540E\u8FD0\u884C\u3002\u9700\u8981\u6709\u6548\u7684 \`tdd-manifest.yaml\`\u3002\u7ED3\u679C\u5199\u5165 FET \u72B6\u6001\uFF1B\u672A\u901A\u8FC7\u524D \`fet verify\` \u4F1A\u88AB\u62E6\u622A\uFF08\u9664\u975E\u914D\u7F6E\u4E3A skip\uFF09\u3002`;
|
|
6976
|
+
return `<!-- FET:MANAGED
|
|
6977
|
+
schemaVersion: 1
|
|
6978
|
+
fetVersion: ${FET_VERSION}
|
|
6979
|
+
generator: cursor-adapter
|
|
6980
|
+
adapterVersion: 1
|
|
6981
|
+
command: ${usage}
|
|
6982
|
+
FET:END -->
|
|
6983
|
+
|
|
6984
|
+
---
|
|
6985
|
+
name: fet-${command}
|
|
6986
|
+
description: ${description}
|
|
6987
|
+
disable-model-invocation: true
|
|
6988
|
+
---
|
|
6989
|
+
|
|
6990
|
+
${renderIdeModelPolicy(command, language)}
|
|
6991
|
+
|
|
6992
|
+
${languageInstruction(language)}
|
|
6993
|
+
|
|
6994
|
+
\u8BF7\u5728\u7EC8\u7AEF\u4E2D\u6267\u884C\uFF1A
|
|
6995
|
+
|
|
6996
|
+
\`\`\`sh
|
|
6997
|
+
${usage}
|
|
6998
|
+
\`\`\`
|
|
6999
|
+
|
|
7000
|
+
${body}
|
|
6209
7001
|
`;
|
|
6210
7002
|
}
|
|
6211
7003
|
|
|
@@ -6215,7 +7007,7 @@ var CursorAdapter = class {
|
|
|
6215
7007
|
adapterVersion = 1;
|
|
6216
7008
|
async detect(projectRoot) {
|
|
6217
7009
|
return {
|
|
6218
|
-
detected: await
|
|
7010
|
+
detected: await exists7(join27(projectRoot, ".cursor")),
|
|
6219
7011
|
reason: "Cursor adapter is available for any project"
|
|
6220
7012
|
};
|
|
6221
7013
|
}
|
|
@@ -6232,7 +7024,7 @@ var CursorAdapter = class {
|
|
|
6232
7024
|
const written = [];
|
|
6233
7025
|
const skipped = [];
|
|
6234
7026
|
for (const file of plan.files) {
|
|
6235
|
-
const target =
|
|
7027
|
+
const target = join27(projectRoot, file.path);
|
|
6236
7028
|
const existing = await readExisting2(target);
|
|
6237
7029
|
if (existing && !existing.includes("FET:MANAGED") && !force) {
|
|
6238
7030
|
throw new FetError({
|
|
@@ -6245,7 +7037,7 @@ var CursorAdapter = class {
|
|
|
6245
7037
|
if (existing && !existing.includes("FET:MANAGED") && force) {
|
|
6246
7038
|
await createBackup(target);
|
|
6247
7039
|
}
|
|
6248
|
-
await
|
|
7040
|
+
await mkdir11(dirname10(target), { recursive: true });
|
|
6249
7041
|
await atomicWrite(target, file.content);
|
|
6250
7042
|
written.push(file.path);
|
|
6251
7043
|
}
|
|
@@ -6255,7 +7047,7 @@ var CursorAdapter = class {
|
|
|
6255
7047
|
const plan = await this.planInstall(projectRoot);
|
|
6256
7048
|
const checks = [];
|
|
6257
7049
|
for (const file of plan.files) {
|
|
6258
|
-
const target =
|
|
7050
|
+
const target = join27(projectRoot, file.path);
|
|
6259
7051
|
const content = await readExisting2(target);
|
|
6260
7052
|
const managed = Boolean(content?.includes("FET:MANAGED"));
|
|
6261
7053
|
const versionMatches = Boolean(content?.includes(`adapterVersion: ${this.adapterVersion}`));
|
|
@@ -6271,14 +7063,14 @@ var CursorAdapter = class {
|
|
|
6271
7063
|
};
|
|
6272
7064
|
async function readExisting2(path) {
|
|
6273
7065
|
try {
|
|
6274
|
-
return await
|
|
7066
|
+
return await readFile21(path, "utf8");
|
|
6275
7067
|
} catch {
|
|
6276
7068
|
return null;
|
|
6277
7069
|
}
|
|
6278
7070
|
}
|
|
6279
|
-
async function
|
|
7071
|
+
async function exists7(path) {
|
|
6280
7072
|
try {
|
|
6281
|
-
await
|
|
7073
|
+
await stat13(path);
|
|
6282
7074
|
return true;
|
|
6283
7075
|
} catch {
|
|
6284
7076
|
return false;
|
|
@@ -6290,45 +7082,45 @@ import { execFile as execFile4 } from "child_process";
|
|
|
6290
7082
|
import { promisify as promisify4 } from "util";
|
|
6291
7083
|
|
|
6292
7084
|
// src/openspec/inspector.ts
|
|
6293
|
-
import { readdir as
|
|
6294
|
-
import { join as
|
|
7085
|
+
import { readdir as readdir6, stat as stat14 } from "fs/promises";
|
|
7086
|
+
import { join as join28 } from "path";
|
|
6295
7087
|
async function inspectOpenSpecProject(projectRoot) {
|
|
6296
|
-
const openspecPath =
|
|
6297
|
-
const changesPath =
|
|
6298
|
-
const legacyArchivePath =
|
|
6299
|
-
const changesArchivePath =
|
|
7088
|
+
const openspecPath = join28(projectRoot, "openspec");
|
|
7089
|
+
const changesPath = join28(openspecPath, "changes");
|
|
7090
|
+
const legacyArchivePath = join28(openspecPath, "archive");
|
|
7091
|
+
const changesArchivePath = join28(changesPath, "archive");
|
|
6300
7092
|
return {
|
|
6301
|
-
exists: await
|
|
7093
|
+
exists: await exists8(openspecPath),
|
|
6302
7094
|
changes: await listDirectories(changesPath, { exclude: ["archive"] }),
|
|
6303
7095
|
archived: [.../* @__PURE__ */ new Set([...await listDirectories(legacyArchivePath), ...await listDirectories(changesArchivePath)])]
|
|
6304
7096
|
};
|
|
6305
7097
|
}
|
|
6306
7098
|
async function inspectOpenSpecChange(projectRoot, changeId) {
|
|
6307
|
-
const changePath =
|
|
6308
|
-
const tasksPath =
|
|
6309
|
-
const specsPath =
|
|
7099
|
+
const changePath = join28(projectRoot, "openspec", "changes", changeId);
|
|
7100
|
+
const tasksPath = join28(changePath, "tasks.md");
|
|
7101
|
+
const specsPath = join28(changePath, "specs");
|
|
6310
7102
|
return {
|
|
6311
7103
|
changeId,
|
|
6312
|
-
exists: await
|
|
6313
|
-
hasProposal: await
|
|
6314
|
-
hasTasks: await
|
|
6315
|
-
hasSpecs: await
|
|
7104
|
+
exists: await exists8(changePath),
|
|
7105
|
+
hasProposal: await exists8(join28(changePath, "proposal.md")),
|
|
7106
|
+
hasTasks: await exists8(tasksPath),
|
|
7107
|
+
hasSpecs: await exists8(specsPath),
|
|
6316
7108
|
tasksPath,
|
|
6317
7109
|
changePath
|
|
6318
7110
|
};
|
|
6319
7111
|
}
|
|
6320
7112
|
async function listDirectories(path, options = {}) {
|
|
6321
7113
|
try {
|
|
6322
|
-
const entries = await
|
|
7114
|
+
const entries = await readdir6(path, { withFileTypes: true });
|
|
6323
7115
|
const excluded = new Set(options.exclude ?? []);
|
|
6324
7116
|
return entries.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).map((entry) => entry.name);
|
|
6325
7117
|
} catch {
|
|
6326
7118
|
return [];
|
|
6327
7119
|
}
|
|
6328
7120
|
}
|
|
6329
|
-
async function
|
|
7121
|
+
async function exists8(path) {
|
|
6330
7122
|
try {
|
|
6331
|
-
await
|
|
7123
|
+
await stat14(path);
|
|
6332
7124
|
return true;
|
|
6333
7125
|
} catch {
|
|
6334
7126
|
return false;
|
|
@@ -6395,14 +7187,14 @@ function exec(command, args) {
|
|
|
6395
7187
|
}
|
|
6396
7188
|
|
|
6397
7189
|
// src/openspec/runner.ts
|
|
6398
|
-
import { spawn as
|
|
7190
|
+
import { spawn as spawn3 } from "child_process";
|
|
6399
7191
|
async function runOpenSpec(executablePath, command, args, options) {
|
|
6400
7192
|
const spawnCommand = executablePath === "npx openspec" ? "npx" : executablePath;
|
|
6401
7193
|
const spawnArgs = executablePath === "npx openspec" ? ["openspec", command, ...args] : [command, ...args];
|
|
6402
7194
|
return new Promise((resolve2, reject) => {
|
|
6403
7195
|
const stdout = [];
|
|
6404
7196
|
const stderr = [];
|
|
6405
|
-
const child =
|
|
7197
|
+
const child = spawn3(spawnCommand, spawnArgs, {
|
|
6406
7198
|
cwd: options.cwd,
|
|
6407
7199
|
stdio: options.stdio ?? "inherit",
|
|
6408
7200
|
shell: process.platform === "win32"
|
|
@@ -6511,14 +7303,14 @@ function escapeRegExp(value) {
|
|
|
6511
7303
|
}
|
|
6512
7304
|
|
|
6513
7305
|
// src/scanner/routes.ts
|
|
6514
|
-
import { readdir as
|
|
6515
|
-
import { join as
|
|
7306
|
+
import { readdir as readdir7, stat as stat15 } from "fs/promises";
|
|
7307
|
+
import { join as join29, relative as relative5, sep } from "path";
|
|
6516
7308
|
async function scanRoutes(projectRoot) {
|
|
6517
7309
|
const candidates = ["src/routes", "src/pages", "app", "pages"];
|
|
6518
7310
|
const routes = [];
|
|
6519
7311
|
for (const candidate of candidates) {
|
|
6520
|
-
const root =
|
|
6521
|
-
if (!await
|
|
7312
|
+
const root = join29(projectRoot, candidate);
|
|
7313
|
+
if (!await exists9(root)) {
|
|
6522
7314
|
continue;
|
|
6523
7315
|
}
|
|
6524
7316
|
for (const file of await listFiles(root)) {
|
|
@@ -6526,8 +7318,8 @@ async function scanRoutes(projectRoot) {
|
|
|
6526
7318
|
continue;
|
|
6527
7319
|
}
|
|
6528
7320
|
routes.push({
|
|
6529
|
-
path: inferRoutePath(
|
|
6530
|
-
source:
|
|
7321
|
+
path: inferRoutePath(relative5(root, file)),
|
|
7322
|
+
source: relative5(projectRoot, file).split(sep).join("/"),
|
|
6531
7323
|
inferred: true,
|
|
6532
7324
|
confidence: "medium"
|
|
6533
7325
|
});
|
|
@@ -6542,10 +7334,10 @@ function inferRoutePath(relativePath) {
|
|
|
6542
7334
|
return `/${withoutIndex}`.replace(/\/+/g, "/");
|
|
6543
7335
|
}
|
|
6544
7336
|
async function listFiles(root) {
|
|
6545
|
-
const entries = await
|
|
7337
|
+
const entries = await readdir7(root, { withFileTypes: true });
|
|
6546
7338
|
const files = [];
|
|
6547
7339
|
for (const entry of entries) {
|
|
6548
|
-
const path =
|
|
7340
|
+
const path = join29(root, entry.name);
|
|
6549
7341
|
if (entry.isDirectory()) {
|
|
6550
7342
|
files.push(...await listFiles(path));
|
|
6551
7343
|
} else {
|
|
@@ -6554,9 +7346,9 @@ async function listFiles(root) {
|
|
|
6554
7346
|
}
|
|
6555
7347
|
return files;
|
|
6556
7348
|
}
|
|
6557
|
-
async function
|
|
7349
|
+
async function exists9(path) {
|
|
6558
7350
|
try {
|
|
6559
|
-
await
|
|
7351
|
+
await stat15(path);
|
|
6560
7352
|
return true;
|
|
6561
7353
|
} catch {
|
|
6562
7354
|
return false;
|
|
@@ -6713,9 +7505,9 @@ async function createCommandContext(command, options) {
|
|
|
6713
7505
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
6714
7506
|
|
|
6715
7507
|
// src/update/check.ts
|
|
6716
|
-
import { mkdir as
|
|
7508
|
+
import { mkdir as mkdir12, readFile as readFile22, writeFile } from "fs/promises";
|
|
6717
7509
|
import { homedir as homedir2 } from "os";
|
|
6718
|
-
import { dirname as
|
|
7510
|
+
import { dirname as dirname11, join as join30 } from "path";
|
|
6719
7511
|
var DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
6720
7512
|
function getFetUpdateCheckMode(env = process.env) {
|
|
6721
7513
|
const value = env.FET_UPDATE_CHECK?.trim().toLowerCase();
|
|
@@ -6788,11 +7580,11 @@ function formatFetUpdateWarning(availability, language) {
|
|
|
6788
7580
|
}
|
|
6789
7581
|
function cachePath() {
|
|
6790
7582
|
const home = process.env.FET_UPDATE_CHECK_CACHE_HOME?.trim() || homedir2();
|
|
6791
|
-
return
|
|
7583
|
+
return join30(home, ".fet", "update-check-cache.json");
|
|
6792
7584
|
}
|
|
6793
7585
|
async function readUpdateCheckCache() {
|
|
6794
7586
|
try {
|
|
6795
|
-
const raw = await
|
|
7587
|
+
const raw = await readFile22(cachePath(), "utf8");
|
|
6796
7588
|
const parsed = JSON.parse(raw);
|
|
6797
7589
|
if (typeof parsed.latestVersion !== "string" || typeof parsed.checkedAt !== "string") {
|
|
6798
7590
|
return null;
|
|
@@ -6808,7 +7600,7 @@ async function readUpdateCheckCache() {
|
|
|
6808
7600
|
}
|
|
6809
7601
|
async function writeUpdateCheckCache(cache) {
|
|
6810
7602
|
const path = cachePath();
|
|
6811
|
-
await
|
|
7603
|
+
await mkdir12(dirname11(path), { recursive: true });
|
|
6812
7604
|
await writeFile(path, `${JSON.stringify(cache, null, 2)}
|
|
6813
7605
|
`, "utf8");
|
|
6814
7606
|
}
|
|
@@ -6895,6 +7687,10 @@ for (const action of ["init", "refresh"]) {
|
|
|
6895
7687
|
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(
|
|
6896
7688
|
wrap("doctor", (ctx, options) => doctorCommand(ctx, { fixLock: Boolean(options.fixLock) }))
|
|
6897
7689
|
);
|
|
7690
|
+
addGlobalOptions(program.command("tdd").description("\u6839\u636E\u89C4\u5212\u4EA7\u7269\u751F\u6210 change \u7EA7 TDD \u6E05\u5355\u4E0E\u6D4B\u8BD5\u6307\u5F15")).action(wrap("tdd", tddCommand));
|
|
7691
|
+
addGlobalOptions(program.command("test").description("\u6309 change TDD \u6E05\u5355\u8FD0\u884C\u5355\u6D4B\u5E76\u8BB0\u5F55\u7EFF\u706F\u72B6\u6001").option("--plan", "\u4EC5\u8F93\u51FA\u5C06\u6267\u884C\u7684\u6D4B\u8BD5\u547D\u4EE4\uFF0C\u4E0D\u8FD0\u884C")).action(
|
|
7692
|
+
wrap("test", (ctx, options) => testCommand(ctx, { plan: Boolean(options.plan) }))
|
|
7693
|
+
);
|
|
6898
7694
|
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(
|
|
6899
7695
|
wrap("verify", verifyCommand)
|
|
6900
7696
|
);
|