@nick848/sf-cli 1.0.11 → 1.0.12

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/CHANGELOG.md CHANGED
@@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## v1.0.11 (2026-03-22)
9
+
10
+ **重大更新 - 规格确认后自动执行开发流程**
11
+
12
+ - ✨ 规格确认后自动执行:TDD 测试生成 → 开发实现 → 代码审核
13
+ - 🤖 `executeDevelopment()` - AI 自动生成代码实现
14
+ - 🔍 `executeReview()` - AI 自动进行代码审核
15
+ - 📦 自动解析代码块并写入文件
16
+ - 🎯 审核通过自动归档,审核失败可重新审核或回退
17
+
18
+ **新增函数**
19
+
20
+ - `executeDevelopment()` - 调用 AI 生成代码,自动写入文件
21
+ - `executeReview()` - 调用 AI 审核代码,返回问题和建议
22
+ - `buildDevelopmentPrompt()` - 构建开发提示词
23
+ - `parseCodeBlocks()` - 解析 AI 返回的代码块
24
+
25
+ **流程优化**
26
+
27
+ ```
28
+ 规格确认(y)
29
+ → 自动生成测试
30
+ → 自动生成代码
31
+ → 自动审核
32
+ → 通过:自动归档
33
+ → 失败:y 重新审核 / n 回退规格
34
+ ```
35
+
8
36
  ## v1.0.10 (2026-03-22)
9
37
 
10
38
  **重大重构 - 工作流系统重写**
package/dist/cli/index.js CHANGED
@@ -1971,18 +1971,67 @@ async function executeWorkflow(ctx) {
1971
1971
  lines.push("");
1972
1972
  lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 7/8: \u5F00\u53D1\u5B9E\u73B0 \u2501\u2501\u2501"));
1973
1973
  lines.push("");
1974
- lines.push(chalk9__default.default.yellow(" \u{1F4DD} \u5F00\u53D1\u9636\u6BB5"));
1975
- lines.push(chalk9__default.default.gray(" \u8BF7\u8C03\u7528 $frontend-dev \u6267\u884C\u5F00\u53D1\u4EFB\u52A1"));
1976
- lines.push(chalk9__default.default.gray(" \u6216\u624B\u52A8\u5B9E\u73B0\u4EE3\u7801\u540E\u8F93\u5165 continue \u7EE7\u7EED"));
1977
- return { output: lines.join("\n") };
1974
+ lines.push(chalk9__default.default.yellow(" \u{1F680} \u6B63\u5728\u8C03\u7528 AI \u751F\u6210\u4EE3\u7801..."));
1975
+ try {
1976
+ const developResult = await executeDevelopment(ctx, activeSession);
1977
+ if (developResult.success) {
1978
+ lines.push(chalk9__default.default.green(" \u2713 \u4EE3\u7801\u751F\u6210\u5B8C\u6210"));
1979
+ for (const file of developResult.files) {
1980
+ lines.push(chalk9__default.default.gray(` - ${file}`));
1981
+ }
1982
+ activeSession.implFiles = developResult.files;
1983
+ activeSession.phase = "review";
1984
+ } else {
1985
+ lines.push(chalk9__default.default.red(` \u2717 \u4EE3\u7801\u751F\u6210\u5931\u8D25: ${developResult.error}`));
1986
+ lines.push(chalk9__default.default.gray(" \u8BF7\u624B\u52A8\u5B9E\u73B0\u4EE3\u7801\u540E\u8F93\u5165 continue \u7EE7\u7EED"));
1987
+ return { output: lines.join("\n") };
1988
+ }
1989
+ } catch (error) {
1990
+ lines.push(chalk9__default.default.red(` \u2717 \u5F00\u53D1\u9636\u6BB5\u51FA\u9519: ${error.message}`));
1991
+ lines.push(chalk9__default.default.gray(" \u8BF7\u624B\u52A8\u5B9E\u73B0\u4EE3\u7801\u540E\u8F93\u5165 continue \u7EE7\u7EED"));
1992
+ return { output: lines.join("\n") };
1993
+ }
1978
1994
  }
1979
1995
  if (activeSession.phase === "review") {
1980
1996
  lines.push("");
1981
1997
  lines.push(chalk9__default.default.cyan("\u2501\u2501\u2501 \u9636\u6BB5 8/8: \u4EE3\u7801\u5BA1\u6838 \u2501\u2501\u2501"));
1982
1998
  lines.push("");
1983
- lines.push(chalk9__default.default.yellow(" \u{1F50D} \u4EE3\u7801\u5BA1\u6838\u9636\u6BB5"));
1984
- lines.push(chalk9__default.default.gray(" \u8BF7\u8C03\u7528 $code-reviewer \u6267\u884C\u4EE3\u7801\u5BA1\u6838"));
1985
- lines.push(chalk9__default.default.gray(" \u6216\u8F93\u5165 review \u5B8C\u6210\u5BA1\u6838"));
1999
+ lines.push(chalk9__default.default.yellow(" \u{1F50D} \u6B63\u5728\u8FDB\u884C\u4EE3\u7801\u5BA1\u6838..."));
2000
+ try {
2001
+ const reviewResult = await executeReview(ctx, activeSession);
2002
+ if (reviewResult.passed) {
2003
+ lines.push(chalk9__default.default.green(" \u2713 \u4EE3\u7801\u5BA1\u6838\u901A\u8FC7"));
2004
+ if (reviewResult.suggestions.length > 0) {
2005
+ lines.push(chalk9__default.default.gray(" \u5EFA\u8BAE:"));
2006
+ for (const s of reviewResult.suggestions.slice(0, 3)) {
2007
+ lines.push(chalk9__default.default.gray(` - ${s}`));
2008
+ }
2009
+ }
2010
+ await archiveWorkflow(ctx.options.workingDirectory);
2011
+ const summary = activeSession.refinedRequirement;
2012
+ activeSession = null;
2013
+ lines.push("");
2014
+ lines.push(chalk9__default.default.green.bold("\u{1F389} \u5DE5\u4F5C\u6D41\u5DF2\u5B8C\u6210\uFF01"));
2015
+ lines.push(chalk9__default.default.gray(`\u9700\u6C42: ${summary.slice(0, 60)}${summary.length > 60 ? "..." : ""}`));
2016
+ lines.push("");
2017
+ lines.push(chalk9__default.default.cyan("\u4F7F\u7528 /new <\u9700\u6C42> \u5F00\u59CB\u65B0\u7684\u5DE5\u4F5C\u6D41"));
2018
+ return { output: lines.join("\n") };
2019
+ } else {
2020
+ lines.push(chalk9__default.default.red(" \u2717 \u4EE3\u7801\u5BA1\u6838\u672A\u901A\u8FC7"));
2021
+ lines.push(chalk9__default.default.gray(" \u95EE\u9898:"));
2022
+ for (const issue of reviewResult.issues.slice(0, 5)) {
2023
+ lines.push(chalk9__default.default.red(` - ${issue}`));
2024
+ }
2025
+ lines.push("");
2026
+ lines.push(chalk9__default.default.yellow(" \u8BF7\u4FEE\u590D\u95EE\u9898\u540E\u8F93\u5165 y \u91CD\u65B0\u5BA1\u6838"));
2027
+ lines.push(chalk9__default.default.gray(" \u6216\u8F93\u5165 n \u56DE\u9000\u5230\u89C4\u683C\u9636\u6BB5"));
2028
+ return { output: lines.join("\n") };
2029
+ }
2030
+ } catch (error) {
2031
+ lines.push(chalk9__default.default.red(` \u2717 \u5BA1\u6838\u9636\u6BB5\u51FA\u9519: ${error.message}`));
2032
+ lines.push(chalk9__default.default.gray(" \u8F93\u5165 pass \u5F3A\u5236\u901A\u8FC7\uFF0C\u6216 fix \u4FEE\u590D\u95EE\u9898"));
2033
+ return { output: lines.join("\n") };
2034
+ }
1986
2035
  }
1987
2036
  return { output: lines.join("\n") };
1988
2037
  } catch (error) {
@@ -2065,19 +2114,25 @@ async function handleWorkflowInput(input, ctx) {
2065
2114
  }
2066
2115
  }
2067
2116
  if (activeSession.phase === "review") {
2068
- if (trimmed === "review" || trimmed === "\u5BA1\u6838" || trimmed === "pass" || trimmed === "\u901A\u8FC7") {
2117
+ if (trimmed === "pass" || trimmed === "\u901A\u8FC7" || trimmed === "\u5F3A\u5236\u901A\u8FC7") {
2069
2118
  await archiveWorkflow(ctx.options.workingDirectory);
2070
2119
  const summary = activeSession.refinedRequirement;
2071
2120
  activeSession = null;
2072
2121
  return {
2073
2122
  output: chalk9__default.default.green("\u2713 \u5DE5\u4F5C\u6D41\u5DF2\u5B8C\u6210") + chalk9__default.default.gray(`
2074
- \u9700\u6C42: ${summary.slice(0, 60)}...`) + chalk9__default.default.cyan("\n\n\u4F7F\u7528 /new <\u9700\u6C42> \u5F00\u59CB\u65B0\u7684\u5DE5\u4F5C\u6D41")
2123
+ \u9700\u6C42: ${summary.slice(0, 60)}${summary.length > 60 ? "..." : ""}`) + chalk9__default.default.cyan("\n\n\u4F7F\u7528 /new <\u9700\u6C42> \u5F00\u59CB\u65B0\u7684\u5DE5\u4F5C\u6D41")
2075
2124
  };
2076
2125
  }
2077
- if (trimmed === "fail" || trimmed === "\u5931\u8D25" || trimmed === "reject" || trimmed === "\u62D2\u7EDD") {
2126
+ if (trimmed === "y" || trimmed === "yes" || trimmed === "\u91CD\u65B0\u5BA1\u6838" || trimmed === "retry") {
2127
+ return executeWorkflow(ctx);
2128
+ }
2129
+ if (trimmed === "n" || trimmed === "no" || trimmed === "\u56DE\u9000" || trimmed === "rollback") {
2078
2130
  activeSession.phase = "spec";
2131
+ return executeWorkflow(ctx);
2132
+ }
2133
+ if (trimmed === "fix" || trimmed === "\u4FEE\u590D") {
2079
2134
  return {
2080
- output: chalk9__default.default.yellow("\u21A9\uFE0F \u5BA1\u6838\u672A\u901A\u8FC7\uFF0C\u56DE\u9000\u5230\u89C4\u683C\u9636\u6BB5") + chalk9__default.default.gray("\n\u8BF7\u91CD\u65B0\u786E\u8BA4\u6216\u4FEE\u6539\u89C4\u683C") + chalk9__default.default.yellow("\n\n\u4F7F\u7528 y \u786E\u8BA4\uFF0Cn \u91CD\u65B0\u751F\u6210")
2135
+ output: chalk9__default.default.yellow("\u{1F4DD} \u8BF7\u624B\u52A8\u4FEE\u590D\u4EE3\u7801\u95EE\u9898") + chalk9__default.default.gray("\n\u4FEE\u590D\u5B8C\u6210\u540E\u8F93\u5165 y \u91CD\u65B0\u5BA1\u6838") + chalk9__default.default.gray("\n\u6216\u8F93\u5165 n \u56DE\u9000\u5230\u89C4\u683C\u9636\u6BB5")
2081
2136
  };
2082
2137
  }
2083
2138
  }
@@ -2199,6 +2254,207 @@ function getCategoryLabel(category) {
2199
2254
  };
2200
2255
  return labels[category] || category;
2201
2256
  }
2257
+ async function executeDevelopment(ctx, session) {
2258
+ const workingDir = ctx.options.workingDirectory;
2259
+ const files = [];
2260
+ try {
2261
+ const systemPrompt = buildDevelopmentPrompt(session);
2262
+ const messages = [
2263
+ {
2264
+ role: "system",
2265
+ content: `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684\u524D\u7AEF\u5F00\u53D1\u5DE5\u7A0B\u5E08\u3002\u8BF7\u6839\u636E\u9700\u6C42\u89C4\u683C\u751F\u6210\u4EE3\u7801\u5B9E\u73B0\u3002
2266
+
2267
+ \u8981\u6C42\uFF1A
2268
+ 1. \u751F\u6210\u5B8C\u6574\u3001\u53EF\u8FD0\u884C\u7684\u4EE3\u7801
2269
+ 2. \u9075\u5FAA\u9879\u76EE\u73B0\u6709\u7684\u4EE3\u7801\u98CE\u683C\u548C\u89C4\u8303
2270
+ 3. \u4F7F\u7528\u9879\u76EE\u6307\u5B9A\u7684\u6280\u672F\u6808
2271
+ 4. \u4EE3\u7801\u8981\u6709\u9002\u5F53\u7684\u6CE8\u91CA
2272
+ 5. \u8FD4\u56DE\u683C\u5F0F\uFF1A\u6BCF\u4E2A\u6587\u4EF6\u7528 \`\`\`filename \u4EE3\u7801 \`\`\` \u5305\u88F9
2273
+
2274
+ \u9879\u76EE\u4FE1\u606F\uFF1A
2275
+ - \u540D\u79F0: ${session.context?.name}
2276
+ - \u6846\u67B6: ${session.context?.framework || "\u672A\u6307\u5B9A"}
2277
+ - \u6280\u672F\u6808: ${session.context?.techStack.join(", ") || "\u672A\u6307\u5B9A"}
2278
+
2279
+ ${session.context?.devStandards ? `\u5F00\u53D1\u89C4\u8303\uFF1A
2280
+ ${session.context.devStandards.slice(0, 2e3)}` : ""}`
2281
+ },
2282
+ {
2283
+ role: "user",
2284
+ content: systemPrompt
2285
+ }
2286
+ ];
2287
+ const response = await ctx.modelService.sendMessage(messages, {
2288
+ temperature: 0.3,
2289
+ maxTokens: 8e3,
2290
+ agent: "frontend-dev"
2291
+ });
2292
+ const codeBlocks = parseCodeBlocks(response.content);
2293
+ for (const block of codeBlocks) {
2294
+ const filePath = path7__namespace.join(workingDir, block.filename);
2295
+ const dir = path7__namespace.dirname(filePath);
2296
+ await fs7__namespace.mkdir(dir, { recursive: true });
2297
+ await fs7__namespace.writeFile(filePath, block.code, "utf-8");
2298
+ files.push(block.filename);
2299
+ }
2300
+ if (files.length === 0) {
2301
+ const implDir = path7__namespace.join(workingDir, "src", "features");
2302
+ await fs7__namespace.mkdir(implDir, { recursive: true });
2303
+ const featureName = session.specItems[0]?.title || "feature";
2304
+ const fileName = `${featureName.replace(/[^a-zA-Z0-9]/g, "_")}.ts`;
2305
+ const filePath = path7__namespace.join(implDir, fileName);
2306
+ const stubCode = `/**
2307
+ * ${session.requirement}
2308
+ *
2309
+ * TODO: \u6B64\u6587\u4EF6\u7531 AI \u81EA\u52A8\u751F\u6210\uFF0C\u8BF7\u6839\u636E\u9700\u6C42\u5B8C\u5584\u5B9E\u73B0
2310
+ */
2311
+
2312
+ export function ${featureName.replace(/[^a-zA-Z0-9]/g, "")}() {
2313
+ // TODO: \u5B9E\u73B0\u529F\u80FD
2314
+ console.log('${featureName} - \u5F85\u5B9E\u73B0');
2315
+ }
2316
+ `;
2317
+ await fs7__namespace.writeFile(filePath, stubCode, "utf-8");
2318
+ files.push(`src/features/${fileName}`);
2319
+ }
2320
+ return { success: true, files };
2321
+ } catch (error) {
2322
+ return {
2323
+ success: false,
2324
+ files: [],
2325
+ error: error.message
2326
+ };
2327
+ }
2328
+ }
2329
+ function buildDevelopmentPrompt(session) {
2330
+ const lines = [];
2331
+ lines.push("## \u9700\u6C42\u63CF\u8FF0");
2332
+ lines.push(session.refinedRequirement);
2333
+ lines.push("");
2334
+ lines.push("## BDD \u573A\u666F");
2335
+ for (const scenario of session.bddScenarios) {
2336
+ lines.push(`### ${scenario.feature}`);
2337
+ for (const s of scenario.scenarios) {
2338
+ lines.push(`- ${s.name}`);
2339
+ }
2340
+ }
2341
+ lines.push("");
2342
+ lines.push("## \u4EFB\u52A1\u5217\u8868");
2343
+ for (const item of session.specItems) {
2344
+ lines.push(`- [${item.id}] ${item.title}: ${item.description}`);
2345
+ }
2346
+ lines.push("");
2347
+ lines.push("\u8BF7\u6839\u636E\u4EE5\u4E0A\u9700\u6C42\u89C4\u683C\u751F\u6210\u4EE3\u7801\u5B9E\u73B0\u3002");
2348
+ return lines.join("\n");
2349
+ }
2350
+ function parseCodeBlocks(content) {
2351
+ const blocks = [];
2352
+ const regex = /```(\S+)\n([\s\S]*?)```/g;
2353
+ let match;
2354
+ while ((match = regex.exec(content)) !== null) {
2355
+ const filename = match[1];
2356
+ const code = match[2].trim();
2357
+ if (["text", "json", "markdown", "md"].includes(filename.toLowerCase())) {
2358
+ continue;
2359
+ }
2360
+ blocks.push({ filename, code });
2361
+ }
2362
+ return blocks;
2363
+ }
2364
+ async function executeReview(ctx, session) {
2365
+ const workingDir = ctx.options.workingDirectory;
2366
+ const issues = [];
2367
+ const suggestions = [];
2368
+ try {
2369
+ const codeContents = [];
2370
+ for (const file of session.implFiles) {
2371
+ try {
2372
+ const content = await fs7__namespace.readFile(path7__namespace.join(workingDir, file), "utf-8");
2373
+ codeContents.push(`// ${file}
2374
+ ${content}`);
2375
+ } catch {
2376
+ }
2377
+ }
2378
+ const testContents = [];
2379
+ for (const file of session.testFiles) {
2380
+ try {
2381
+ const content = await fs7__namespace.readFile(path7__namespace.join(workingDir, file), "utf-8");
2382
+ testContents.push(`// ${file}
2383
+ ${content}`);
2384
+ } catch {
2385
+ }
2386
+ }
2387
+ const messages = [
2388
+ {
2389
+ role: "system",
2390
+ content: `\u4F60\u662F\u4E00\u4E2A\u4E13\u4E1A\u7684\u4EE3\u7801\u5BA1\u6838\u4E13\u5BB6\u3002\u8BF7\u5BA1\u6838\u4EE5\u4E0B\u4EE3\u7801\u5B9E\u73B0\u662F\u5426\u7B26\u5408\u9700\u6C42\u89C4\u683C\u3002
2391
+
2392
+ \u5BA1\u6838\u6807\u51C6\uFF1A
2393
+ 1. \u4EE3\u7801\u662F\u5426\u6B63\u786E\u5B9E\u73B0\u4E86\u9700\u6C42\u89C4\u683C\u4E2D\u7684\u529F\u80FD
2394
+ 2. \u4EE3\u7801\u8D28\u91CF\uFF08\u53EF\u8BFB\u6027\u3001\u53EF\u7EF4\u62A4\u6027\uFF09
2395
+ 3. \u6F5C\u5728\u7684 bug \u548C\u8FB9\u754C\u60C5\u51B5\u5904\u7406
2396
+ 4. \u4EE3\u7801\u98CE\u683C\u662F\u5426\u7B26\u5408\u9879\u76EE\u89C4\u8303
2397
+ 5. \u6D4B\u8BD5\u8986\u76D6\u662F\u5426\u5145\u5206
2398
+
2399
+ \u8FD4\u56DE\u683C\u5F0F\uFF1A
2400
+ - \u5982\u679C\u901A\u8FC7\u5BA1\u6838\uFF0C\u8FD4\u56DE "\u5BA1\u6838\u901A\u8FC7" \u5E76\u5217\u51FA\u6539\u8FDB\u5EFA\u8BAE
2401
+ - \u5982\u679C\u4E0D\u901A\u8FC7\uFF0C\u5217\u51FA\u5177\u4F53\u95EE\u9898
2402
+
2403
+ \u9879\u76EE\u4FE1\u606F\uFF1A
2404
+ - \u6846\u67B6: ${session.context?.framework || "\u672A\u6307\u5B9A"}
2405
+ - \u6280\u672F\u6808: ${session.context?.techStack.join(", ") || "\u672A\u6307\u5B9A"}
2406
+
2407
+ ${session.context?.devStandards ? `\u5F00\u53D1\u89C4\u8303\uFF1A
2408
+ ${session.context.devStandards.slice(0, 1e3)}` : ""}`
2409
+ },
2410
+ {
2411
+ role: "user",
2412
+ content: `## \u9700\u6C42\u89C4\u683C
2413
+ ${session.refinedRequirement}
2414
+
2415
+ ## \u4EFB\u52A1\u5217\u8868
2416
+ ${session.specItems.map((item) => `- [${item.id}] ${item.title}`).join("\n")}
2417
+
2418
+ ## \u5B9E\u73B0\u4EE3\u7801
2419
+ ${codeContents.join("\n\n") || "\uFF08\u65E0\u4EE3\u7801\u6587\u4EF6\uFF09"}
2420
+
2421
+ ## \u6D4B\u8BD5\u4EE3\u7801
2422
+ ${testContents.join("\n\n") || "\uFF08\u65E0\u6D4B\u8BD5\u6587\u4EF6\uFF09"}
2423
+
2424
+ \u8BF7\u5BA1\u6838\u4EE5\u4E0A\u4EE3\u7801\u5B9E\u73B0\u3002`
2425
+ }
2426
+ ];
2427
+ const response = await ctx.modelService.sendMessage(messages, {
2428
+ temperature: 0.2,
2429
+ maxTokens: 2e3,
2430
+ agent: "code-reviewer"
2431
+ });
2432
+ const result = response.content;
2433
+ const passed = result.includes("\u5BA1\u6838\u901A\u8FC7") || result.includes("\u901A\u8FC7") || !result.includes("\u4E0D\u901A\u8FC7");
2434
+ const issueMatches = result.match(/问题[::]\s*([\s\S]*?)(?=建议|$)/i);
2435
+ if (issueMatches) {
2436
+ const issueList = issueMatches[1].split(/[\n-]/).filter((s) => s.trim());
2437
+ issues.push(...issueList.map((s) => s.trim()).filter((s) => s));
2438
+ }
2439
+ const suggestionMatches = result.match(/建议[::]\s*([\s\S]*?)$/i);
2440
+ if (suggestionMatches) {
2441
+ const suggestionList = suggestionMatches[1].split(/[\n-]/).filter((s) => s.trim());
2442
+ suggestions.push(...suggestionList.map((s) => s.trim()).filter((s) => s));
2443
+ }
2444
+ if (issues.length === 0 && !passed) {
2445
+ const lines = result.split("\n").filter((l) => l.includes("\u95EE\u9898") || l.includes("\u9519\u8BEF") || l.includes("\u7F3A\u9677"));
2446
+ issues.push(...lines.map((l) => l.replace(/^[-*]\s*/, "").trim()).filter((l) => l));
2447
+ }
2448
+ if (suggestions.length === 0) {
2449
+ const lines = result.split("\n").filter((l) => l.includes("\u5EFA\u8BAE") || l.includes("\u6539\u8FDB") || l.includes("\u4F18\u5316"));
2450
+ suggestions.push(...lines.map((l) => l.replace(/^[-*]\s*/, "").trim()).filter((l) => l));
2451
+ }
2452
+ return { passed, issues, suggestions };
2453
+ } catch (error) {
2454
+ suggestions.push(`\u5BA1\u6838\u8FC7\u7A0B\u51FA\u9519: ${error.message}`);
2455
+ return { passed: true, issues, suggestions };
2456
+ }
2457
+ }
2202
2458
  async function readProjectContext(workingDir) {
2203
2459
  const context = {
2204
2460
  name: path7__namespace.basename(workingDir),