yg-team-cli 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +882 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +4502 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4329 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4329 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command13 } from "commander";
|
|
5
|
+
import chalk2 from "chalk";
|
|
6
|
+
|
|
7
|
+
// src/lib/logger.ts
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import ora from "ora";
|
|
10
|
+
var Logger = class {
|
|
11
|
+
spinner = null;
|
|
12
|
+
/**
|
|
13
|
+
* 打印标题(加粗)
|
|
14
|
+
*/
|
|
15
|
+
header(text) {
|
|
16
|
+
console.log(chalk.bold(text));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 打印成功信息(绿色)
|
|
20
|
+
*/
|
|
21
|
+
success(text) {
|
|
22
|
+
console.log(chalk.green("\u2713"), text);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 打印错误信息(红色)
|
|
26
|
+
*/
|
|
27
|
+
error(text) {
|
|
28
|
+
console.error(chalk.red("\u2717"), text);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 打印警告信息(黄色)
|
|
32
|
+
*/
|
|
33
|
+
warn(text) {
|
|
34
|
+
console.warn(chalk.yellow("\u26A0"), text);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 打印信息(蓝色)
|
|
38
|
+
*/
|
|
39
|
+
info(text) {
|
|
40
|
+
console.info(chalk.blue("\u2139"), text);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 打印步骤信息
|
|
44
|
+
*/
|
|
45
|
+
step(text) {
|
|
46
|
+
console.log(chalk.cyan("\u2192"), text);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 打印空行
|
|
50
|
+
*/
|
|
51
|
+
newLine() {
|
|
52
|
+
console.log("");
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 打印分隔线
|
|
56
|
+
*/
|
|
57
|
+
separator(char = "\u2500", length = 50) {
|
|
58
|
+
console.log(chalk.gray(char.repeat(length)));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 开始加载动画
|
|
62
|
+
*/
|
|
63
|
+
startLoading(text) {
|
|
64
|
+
this.spinner = ora({
|
|
65
|
+
text,
|
|
66
|
+
color: "cyan"
|
|
67
|
+
}).start();
|
|
68
|
+
return this.spinner;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 更新加载文本
|
|
72
|
+
*/
|
|
73
|
+
updateLoading(text) {
|
|
74
|
+
if (this.spinner) {
|
|
75
|
+
this.spinner.text = text;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 停止加载并显示成功
|
|
80
|
+
*/
|
|
81
|
+
succeedLoading(text) {
|
|
82
|
+
if (this.spinner) {
|
|
83
|
+
this.spinner.succeed(text);
|
|
84
|
+
this.spinner = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 停止加载并显示失败
|
|
89
|
+
*/
|
|
90
|
+
failLoading(text) {
|
|
91
|
+
if (this.spinner) {
|
|
92
|
+
this.spinner.fail(text);
|
|
93
|
+
this.spinner = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* 停止加载并显示警告
|
|
98
|
+
*/
|
|
99
|
+
warnLoading(text) {
|
|
100
|
+
if (this.spinner) {
|
|
101
|
+
this.spinner.warn(text);
|
|
102
|
+
this.spinner = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 停止加载并显示信息
|
|
107
|
+
*/
|
|
108
|
+
infoLoading(text) {
|
|
109
|
+
if (this.spinner) {
|
|
110
|
+
this.spinner.info(text);
|
|
111
|
+
this.spinner = null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 打印表格
|
|
116
|
+
*/
|
|
117
|
+
table(headers, rows) {
|
|
118
|
+
const columnWidths = headers.map((h, i) => {
|
|
119
|
+
const maxWidth = Math.max(
|
|
120
|
+
h.length,
|
|
121
|
+
...rows.map((row) => (row[i] || "").length)
|
|
122
|
+
);
|
|
123
|
+
return maxWidth + 2;
|
|
124
|
+
});
|
|
125
|
+
const headerRow = headers.map((h, i) => chalk.bold(h.padEnd(columnWidths[i]))).join("");
|
|
126
|
+
console.log(headerRow);
|
|
127
|
+
const separator = columnWidths.map((w) => "\u2500".repeat(w)).join("\u253C");
|
|
128
|
+
console.log(chalk.gray(separator));
|
|
129
|
+
rows.forEach((row) => {
|
|
130
|
+
const dataRow = row.map((cell, i) => (cell || "").padEnd(columnWidths[i])).join("");
|
|
131
|
+
console.log(dataRow);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 打印代码块
|
|
136
|
+
*/
|
|
137
|
+
code(code, language = "") {
|
|
138
|
+
if (language) {
|
|
139
|
+
console.log(chalk.gray(`\`\`\`${language}`));
|
|
140
|
+
}
|
|
141
|
+
console.log(chalk.gray(code));
|
|
142
|
+
if (language) {
|
|
143
|
+
console.log(chalk.gray("```"));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 打印列表
|
|
148
|
+
*/
|
|
149
|
+
list(items, indent = 2) {
|
|
150
|
+
items.forEach((item) => {
|
|
151
|
+
console.log(" ".repeat(indent) + chalk.gray("\u2022") + " " + item);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* 打印带标签的文本
|
|
156
|
+
*/
|
|
157
|
+
labeled(label, text, labelColor = "blue") {
|
|
158
|
+
const coloredLabel = chalk[labelColor](`[${label}]`);
|
|
159
|
+
console.log(`${coloredLabel} ${text}`);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
var logger = new Logger();
|
|
163
|
+
|
|
164
|
+
// src/commands/init.ts
|
|
165
|
+
import { Command } from "commander";
|
|
166
|
+
import inquirer from "inquirer";
|
|
167
|
+
import path2 from "path";
|
|
168
|
+
import fs2 from "fs-extra";
|
|
169
|
+
|
|
170
|
+
// src/lib/claude.ts
|
|
171
|
+
import { execa } from "execa";
|
|
172
|
+
var ClaudeAI = class {
|
|
173
|
+
verbose;
|
|
174
|
+
constructor(verbose = false) {
|
|
175
|
+
this.verbose = verbose;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* 检查 Claude CLI 是否已安装
|
|
179
|
+
*/
|
|
180
|
+
async checkInstalled() {
|
|
181
|
+
try {
|
|
182
|
+
await execa("claude", ["--version"], { stdio: "pipe" });
|
|
183
|
+
return true;
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* 获取 Claude 版本
|
|
190
|
+
*/
|
|
191
|
+
async getVersion() {
|
|
192
|
+
try {
|
|
193
|
+
const { stdout } = await execa("claude", ["--version"], { stdio: "pipe" });
|
|
194
|
+
return stdout.trim();
|
|
195
|
+
} catch {
|
|
196
|
+
throw new Error("\u65E0\u6CD5\u83B7\u53D6 Claude \u7248\u672C");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* 发送 prompt 到 Claude
|
|
201
|
+
*/
|
|
202
|
+
async prompt(promptText, options) {
|
|
203
|
+
const spinner = logger.startLoading("\u6B63\u5728\u8C03\u7528 Claude...");
|
|
204
|
+
try {
|
|
205
|
+
const args = ["--no-confirm", "-p", promptText];
|
|
206
|
+
if (options?.contextFiles && options.contextFiles.length > 0) {
|
|
207
|
+
for (const file of options.contextFiles) {
|
|
208
|
+
args.unshift("--context", file);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const { stdout } = await execa("claude", args, {
|
|
212
|
+
stdio: this.verbose ? "inherit" : "pipe",
|
|
213
|
+
timeout: options?.timeout || 3e5
|
|
214
|
+
// 默认 5 分钟
|
|
215
|
+
});
|
|
216
|
+
spinner.succeed("Claude \u54CD\u5E94\u5B8C\u6210");
|
|
217
|
+
return stdout;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
spinner.fail("Claude \u8C03\u7528\u5931\u8D25");
|
|
220
|
+
if (error.killed && error.signal === "SIGTERM") {
|
|
221
|
+
throw new Error("Claude \u6267\u884C\u8D85\u65F6");
|
|
222
|
+
}
|
|
223
|
+
throw new Error(`Claude \u8C03\u7528\u5931\u8D25: ${error.message}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* 使用 prompt 模板和参数发送请求
|
|
228
|
+
*/
|
|
229
|
+
async promptWithTemplate(template, data, options) {
|
|
230
|
+
let promptText = template;
|
|
231
|
+
for (const [key, value] of Object.entries(data)) {
|
|
232
|
+
const regex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
|
|
233
|
+
promptText = promptText.replace(regex, String(value));
|
|
234
|
+
}
|
|
235
|
+
return await this.prompt(promptText, options);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* 发送对话(支持上下文)
|
|
239
|
+
*/
|
|
240
|
+
async chat(messages, options) {
|
|
241
|
+
const spinner = logger.startLoading("\u6B63\u5728\u8C03\u7528 Claude...");
|
|
242
|
+
try {
|
|
243
|
+
const fullPrompt = messages.map((msg) => {
|
|
244
|
+
const role = msg.role === "system" ? "\u7CFB\u7EDF\u6307\u4EE4" : `${msg.role === "user" ? "\u7528\u6237" : "\u52A9\u624B"}`;
|
|
245
|
+
return `[${role}]: ${msg.content}`;
|
|
246
|
+
}).join("\n\n");
|
|
247
|
+
const { stdout } = await execa("claude", ["--no-confirm", "-p", fullPrompt], {
|
|
248
|
+
stdio: this.verbose ? "inherit" : "pipe",
|
|
249
|
+
timeout: options?.timeout || 3e5
|
|
250
|
+
});
|
|
251
|
+
spinner.succeed("Claude \u54CD\u5E94\u5B8C\u6210");
|
|
252
|
+
return stdout;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
spinner.fail("Claude \u8C03\u7528\u5931\u8D25");
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* 分析代码
|
|
260
|
+
*/
|
|
261
|
+
async analyzeCode(code, language, question) {
|
|
262
|
+
const prompt = `\u8BF7\u5206\u6790\u4EE5\u4E0B ${language} \u4EE3\u7801\uFF1A${question ? `\u5E76\u56DE\u7B54\uFF1A${question}` : ""}
|
|
263
|
+
|
|
264
|
+
\`\`\`${language}
|
|
265
|
+
${code}
|
|
266
|
+
\`\`\`
|
|
267
|
+
|
|
268
|
+
\u8BF7\u63D0\u4F9B\uFF1A
|
|
269
|
+
1. \u4EE3\u7801\u529F\u80FD\u6982\u8FF0
|
|
270
|
+
2. \u6F5C\u5728\u95EE\u9898
|
|
271
|
+
3. \u6539\u8FDB\u5EFA\u8BAE
|
|
272
|
+
`;
|
|
273
|
+
return await this.prompt(prompt);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* 生成代码
|
|
277
|
+
*/
|
|
278
|
+
async generateCode(description, language, context) {
|
|
279
|
+
let prompt = `\u8BF7\u7528 ${language} \u7F16\u5199\u4EE3\u7801\u5B9E\u73B0\u4EE5\u4E0B\u529F\u80FD\uFF1A
|
|
280
|
+
|
|
281
|
+
${description}
|
|
282
|
+
|
|
283
|
+
`;
|
|
284
|
+
if (context) {
|
|
285
|
+
prompt += `\u53C2\u8003\u4E0A\u4E0B\u6587\uFF1A
|
|
286
|
+
|
|
287
|
+
${context}
|
|
288
|
+
|
|
289
|
+
`;
|
|
290
|
+
}
|
|
291
|
+
prompt += `\u8981\u6C42\uFF1A
|
|
292
|
+
1. \u4EE3\u7801\u7B80\u6D01\u6613\u8BFB
|
|
293
|
+
2. \u5305\u542B\u5FC5\u8981\u7684\u6CE8\u91CA
|
|
294
|
+
3. \u9075\u5FAA\u6700\u4F73\u5B9E\u8DF5
|
|
295
|
+
4. \u53EA\u8FD4\u56DE\u4EE3\u7801\uFF0C\u4E0D\u8981\u989D\u5916\u89E3\u91CA
|
|
296
|
+
`;
|
|
297
|
+
return await this.prompt(prompt);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* 审查代码
|
|
301
|
+
*/
|
|
302
|
+
async reviewCode(code, language) {
|
|
303
|
+
const prompt = `\u8BF7\u5BA1\u67E5\u4EE5\u4E0B ${language} \u4EE3\u7801\uFF0C\u63D0\u4F9B\u8BE6\u7EC6\u53CD\u9988\uFF1A
|
|
304
|
+
|
|
305
|
+
\`\`\`${language}
|
|
306
|
+
${code}
|
|
307
|
+
\`\`\`
|
|
308
|
+
|
|
309
|
+
\u8BF7\u6309\u4EE5\u4E0B\u683C\u5F0F\u56DE\u590D\uFF1A
|
|
310
|
+
## \u53D1\u73B0\u7684\u95EE\u9898
|
|
311
|
+
- [\u4E25\u91CD\u7A0B\u5EA6] \u95EE\u9898\u63CF\u8FF0
|
|
312
|
+
|
|
313
|
+
## \u6539\u8FDB\u5EFA\u8BAE
|
|
314
|
+
- \u5EFA\u8BAE\u5185\u5BB9
|
|
315
|
+
|
|
316
|
+
## \u4EAE\u70B9
|
|
317
|
+
- \u505A\u5F97\u597D\u7684\u5730\u65B9
|
|
318
|
+
|
|
319
|
+
\u8BF7\u7528 JSON \u683C\u5F0F\u56DE\u590D\uFF1A
|
|
320
|
+
{
|
|
321
|
+
"issues": [{"severity": "high|medium|low", "message": "\u95EE\u9898\u63CF\u8FF0"}],
|
|
322
|
+
"suggestions": ["\u5EFA\u8BAE1", "\u5EFA\u8BAE2"],
|
|
323
|
+
"highlights": ["\u4EAE\u70B91", "\u4EAE\u70B92"]
|
|
324
|
+
}
|
|
325
|
+
`;
|
|
326
|
+
const response = await this.prompt(prompt);
|
|
327
|
+
try {
|
|
328
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
329
|
+
if (jsonMatch) {
|
|
330
|
+
return JSON.parse(jsonMatch[0]);
|
|
331
|
+
}
|
|
332
|
+
} catch {
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
issues: [],
|
|
336
|
+
suggestions: [response],
|
|
337
|
+
highlights: []
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* 生成 Spec 文档
|
|
342
|
+
*/
|
|
343
|
+
async generateSpec(featureName, description, projectContext, dependencies) {
|
|
344
|
+
let prompt = `Role: Senior Fullstack Developer
|
|
345
|
+
|
|
346
|
+
\u4F60\u662F\u4E00\u4E2A\u8D44\u6DF1\u7684\u5168\u6808\u5DE5\u7A0B\u5E08\uFF0C\u73B0\u5728\u9700\u8981\u4E3A\u4E00\u4E2A\u65B0\u529F\u80FD\u751F\u6210\u89C4\u683C\u6587\u6863\uFF08Spec\uFF09\u3002
|
|
347
|
+
|
|
348
|
+
## \u529F\u80FD\u540D\u79F0
|
|
349
|
+
${featureName}
|
|
350
|
+
|
|
351
|
+
## \u529F\u80FD\u63CF\u8FF0
|
|
352
|
+
${description}
|
|
353
|
+
`;
|
|
354
|
+
if (dependencies && dependencies.length > 0) {
|
|
355
|
+
prompt += `
|
|
356
|
+
## \u4F9D\u8D56\u529F\u80FD
|
|
357
|
+
\u6B64\u529F\u80FD\u4F9D\u8D56\u4EE5\u4E0B\u5DF2\u5B8C\u6210\u7684\u529F\u80FD\uFF1A${dependencies.join("\u3001")}
|
|
358
|
+
`;
|
|
359
|
+
}
|
|
360
|
+
prompt += `
|
|
361
|
+
## \u9879\u76EE\u6280\u672F\u6808
|
|
362
|
+
\u540E\u7AEF: Java 17 + Spring Boot 3 + MyBatis Plus + MySQL 8.0
|
|
363
|
+
\u524D\u7AEF: Next.js 14 (App Router) + TypeScript + Tailwind CSS
|
|
364
|
+
|
|
365
|
+
## \u9879\u76EE\u5F53\u524D\u72B6\u6001
|
|
366
|
+
${projectContext}
|
|
367
|
+
|
|
368
|
+
## \u4EFB\u52A1
|
|
369
|
+
\u8BF7\u6839\u636E\u529F\u80FD\u63CF\u8FF0\uFF0C\u751F\u6210\u4E00\u4E2A\u5B8C\u6574\u7684\u529F\u80FD\u89C4\u683C\u6587\u6863 Spec\u3002
|
|
370
|
+
|
|
371
|
+
## Spec \u6587\u6863\u683C\u5F0F\u8981\u6C42
|
|
372
|
+
\`\`\`markdown
|
|
373
|
+
# [\u529F\u80FD\u6807\u9898]
|
|
374
|
+
|
|
375
|
+
## \u529F\u80FD\u6982\u8FF0
|
|
376
|
+
**\u529F\u80FD\u540D\u79F0**: [\u529F\u80FD\u4E2D\u6587\u540D]
|
|
377
|
+
**\u4F18\u5148\u7EA7**: P0/P1/P2 (\u6839\u636E\u529F\u80FD\u91CD\u8981\u6027\u5224\u65AD)
|
|
378
|
+
**\u9884\u4F30\u5DE5\u65F6**: X \u5929 (\u6839\u636E\u590D\u6742\u5EA6\u8BC4\u4F30\uFF1A\u7B80\u53551-2\u5929\uFF0C\u4E2D\u7B493-5\u5929\uFF0C\u590D\u67425-10\u5929)
|
|
379
|
+
**\u72B6\u6001**: \u5F85\u62C6\u5206
|
|
380
|
+
**\u521B\u5EFA\u65E5\u671F**: {{DATE}}
|
|
381
|
+
|
|
382
|
+
## \u4F9D\u8D56\u5173\u7CFB
|
|
383
|
+
**\u524D\u7F6E\u4F9D\u8D56**:
|
|
384
|
+
${dependencies ? dependencies.map((d) => `- [x] ${d}`).join("\n") : "- (\u65E0)"}
|
|
385
|
+
|
|
386
|
+
**\u88AB\u4F9D\u8D56\u4E8E**:
|
|
387
|
+
- (\u81EA\u52A8\u751F\u6210\uFF0C\u8868\u793A\u54EA\u4E9B spec \u4F9D\u8D56\u672C\u529F\u80FD)
|
|
388
|
+
|
|
389
|
+
## \u80CC\u666F\u4E0E\u76EE\u6807
|
|
390
|
+
[\u6839\u636E\u529F\u80FD\u63CF\u8FF0\uFF0C\u7B80\u660E\u627C\u8981\u5730\u8BF4\u660E\u529F\u80FD\u7684\u80CC\u666F\u548C\u8981\u89E3\u51B3\u7684\u95EE\u9898]
|
|
391
|
+
|
|
392
|
+
## \u529F\u80FD\u9700\u6C42
|
|
393
|
+
### \u7528\u6237\u6545\u4E8B
|
|
394
|
+
\`\`\`
|
|
395
|
+
\u4F5C\u4E3A [\u5177\u4F53\u89D2\u8272]
|
|
396
|
+
\u6211\u5E0C\u671B [\u5177\u4F53\u529F\u80FD]
|
|
397
|
+
\u4EE5\u4FBF [\u5B9E\u73B0\u7684\u4EF7\u503C]
|
|
398
|
+
\`\`\`
|
|
399
|
+
|
|
400
|
+
### \u529F\u80FD\u70B9
|
|
401
|
+
\u5217\u51FA 3-8 \u4E2A\u4E3B\u8981\u529F\u80FD\u70B9\uFF0C\u6BCF\u4E2A\u529F\u80FD\u70B9\u7528\u4E00\u53E5\u8BDD\u63CF\u8FF0\u3002
|
|
402
|
+
|
|
403
|
+
## \u6280\u672F\u8BBE\u8BA1
|
|
404
|
+
### API \u8BBE\u8BA1
|
|
405
|
+
\u5217\u51FA\u4E3B\u8981\u7684 API \u7AEF\u70B9\uFF0C\u683C\u5F0F\uFF1A
|
|
406
|
+
- \`METHOD /api/path\` - \u7B80\u77ED\u8BF4\u660E
|
|
407
|
+
|
|
408
|
+
### \u6570\u636E\u6A21\u578B
|
|
409
|
+
\u5217\u51FA\u9700\u8981\u7684\u6570\u636E\u8868\uFF0C\u683C\u5F0F\uFF1A
|
|
410
|
+
- \`table_name\` (\u8868\u8BF4\u660E) - \u5B57\u6BB5\u8BF4\u660E
|
|
411
|
+
|
|
412
|
+
## \u91CC\u7A0B\u7891 (Milestones)
|
|
413
|
+
> \u6CE8: \u4F7F\u7528 \`team-cli breakdown ${StringUtils.toKebabCase(featureName)}.md\` \u62C6\u5206\u6B64 spec \u4E3A milestones \u548C todos
|
|
414
|
+
|
|
415
|
+
----
|
|
416
|
+
*\u751F\u6210\u4E8E: {{TIMESTAMP}} by Claude*
|
|
417
|
+
\`\`\`
|
|
418
|
+
|
|
419
|
+
## \u6CE8\u610F\u4E8B\u9879
|
|
420
|
+
1. \u53EA\u751F\u6210 Spec \u6587\u6863\uFF0C\u4E0D\u8981\u5B9E\u73B0\u4EFB\u4F55\u4EE3\u7801
|
|
421
|
+
2. \u4F18\u5148\u7EA7\u5224\u65AD\u6807\u51C6\uFF1A
|
|
422
|
+
- P0: \u6838\u5FC3\u529F\u80FD\uFF0C\u963B\u585E\u5176\u4ED6\u529F\u80FD
|
|
423
|
+
- P1: \u91CD\u8981\u529F\u80FD\uFF0C\u5F71\u54CD\u7528\u6237\u4F53\u9A8C
|
|
424
|
+
- P2: \u589E\u5F3A\u529F\u80FD\uFF0C\u9526\u4E0A\u6DFB\u82B1
|
|
425
|
+
3. \u5DE5\u65F6\u8BC4\u4F30\u6807\u51C6\uFF1A
|
|
426
|
+
- \u7B80\u5355\u529F\u80FD: 1-2 \u5929
|
|
427
|
+
- \u4E2D\u7B49\u529F\u80FD: 3-5 \u5929
|
|
428
|
+
- \u590D\u6742\u529F\u80FD: 5-10 \u5929
|
|
429
|
+
4. \u529F\u80FD\u70B9\u8981\u5177\u4F53\u53EF\u6267\u884C\uFF0C\u907F\u514D\u8FC7\u4E8E\u62BD\u8C61
|
|
430
|
+
5. API \u548C\u6570\u636E\u6A21\u578B\u8981\u7B26\u5408\u5B9E\u9645\u6280\u672F\u6808
|
|
431
|
+
`;
|
|
432
|
+
return await this.prompt(prompt, { timeout: 18e4 });
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* 生成 Bugfix 报告
|
|
436
|
+
*/
|
|
437
|
+
async generateBugfixReport(bugDescription, reproductionSteps, projectContext) {
|
|
438
|
+
const prompt = `Role: Senior Fullstack Developer
|
|
439
|
+
|
|
440
|
+
\u7528\u6237\u62A5\u544A\u4E86\u4E00\u4E2A Bug\uFF0C\u9700\u8981\u4F60\u751F\u6210 Bugfix \u89C4\u683C\u6587\u6863\u3002
|
|
441
|
+
|
|
442
|
+
## Bug \u63CF\u8FF0
|
|
443
|
+
${bugDescription}
|
|
444
|
+
|
|
445
|
+
## \u590D\u73B0\u6B65\u9AA4
|
|
446
|
+
${reproductionSteps.map((step, i) => `${i + 1}. ${step}`).join("\n")}
|
|
447
|
+
|
|
448
|
+
## \u9879\u76EE\u5F53\u524D\u72B6\u6001
|
|
449
|
+
${projectContext}
|
|
450
|
+
|
|
451
|
+
## \u4EFB\u52A1
|
|
452
|
+
\u8BF7\u751F\u6210\u4E00\u4E2A\u5B8C\u6574\u7684 Bugfix \u62A5\u544A\uFF0C\u5305\u62EC\uFF1A
|
|
453
|
+
1. \u95EE\u9898\u5206\u6790
|
|
454
|
+
2. \u6839\u672C\u539F\u56E0\u63A8\u6D4B
|
|
455
|
+
3. \u4FEE\u590D\u5EFA\u8BAE
|
|
456
|
+
4. \u6D4B\u8BD5\u8BA1\u5212
|
|
457
|
+
`;
|
|
458
|
+
return await this.prompt(prompt, { timeout: 12e4 });
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
var claudeAI = new ClaudeAI();
|
|
462
|
+
|
|
463
|
+
// src/lib/utils.ts
|
|
464
|
+
import fs from "fs-extra";
|
|
465
|
+
import path from "path";
|
|
466
|
+
import { glob } from "glob";
|
|
467
|
+
var FileUtils = class {
|
|
468
|
+
/**
|
|
469
|
+
* 确保目录存在
|
|
470
|
+
*/
|
|
471
|
+
static async ensureDir(dir) {
|
|
472
|
+
await fs.ensureDir(dir);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* 读取文件内容
|
|
476
|
+
*/
|
|
477
|
+
static async read(file) {
|
|
478
|
+
return await fs.readFile(file, "utf-8");
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* 写入文件内容
|
|
482
|
+
*/
|
|
483
|
+
static async write(file, content) {
|
|
484
|
+
await fs.writeFile(file, content, "utf-8");
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* 检查文件是否存在
|
|
488
|
+
*/
|
|
489
|
+
static async exists(file) {
|
|
490
|
+
return await fs.pathExists(file);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* 复制文件
|
|
494
|
+
*/
|
|
495
|
+
static async copy(src, dest) {
|
|
496
|
+
await fs.copy(src, dest);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* 删除文件或目录
|
|
500
|
+
*/
|
|
501
|
+
static async remove(file) {
|
|
502
|
+
await fs.remove(file);
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* 移动文件
|
|
506
|
+
*/
|
|
507
|
+
static async move(src, dest) {
|
|
508
|
+
await fs.move(src, dest);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* 使用 glob 查找文件
|
|
512
|
+
*/
|
|
513
|
+
static async findFiles(pattern, cwd) {
|
|
514
|
+
return await glob(pattern, {
|
|
515
|
+
cwd,
|
|
516
|
+
absolute: false,
|
|
517
|
+
nodir: true
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* 读取 JSON 文件
|
|
522
|
+
*/
|
|
523
|
+
static async readJson(file) {
|
|
524
|
+
return await fs.readJson(file);
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* 写入 JSON 文件
|
|
528
|
+
*/
|
|
529
|
+
static async writeJson(file, data) {
|
|
530
|
+
await fs.writeJson(file, data, { spaces: 2 });
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
var StringUtils2 = class {
|
|
534
|
+
/**
|
|
535
|
+
* 转换为 kebab-case
|
|
536
|
+
*/
|
|
537
|
+
static toKebabCase(str) {
|
|
538
|
+
return str.toLowerCase().replace(/[\s_]+/g, "-").replace(/[^\w\-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* 转换为 PascalCase
|
|
542
|
+
*/
|
|
543
|
+
static toPascalCase(str) {
|
|
544
|
+
return str.replace(/[-_\s](\w)/g, (_, c) => c.toUpperCase()).replace(/^\w/, (c) => c.toUpperCase());
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* 转换为 camelCase
|
|
548
|
+
*/
|
|
549
|
+
static toCamelCase(str) {
|
|
550
|
+
const pascal = this.toPascalCase(str);
|
|
551
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* 转换为 snake_case
|
|
555
|
+
*/
|
|
556
|
+
static toSnakeCase(str) {
|
|
557
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`).replace(/^_/, "").replace(/-+/g, "_").replace(/[\s]+/g, "_");
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* 首字母大写
|
|
561
|
+
*/
|
|
562
|
+
static capitalize(str) {
|
|
563
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* 截断字符串
|
|
567
|
+
*/
|
|
568
|
+
static truncate(str, length, suffix = "...") {
|
|
569
|
+
if (str.length <= length) return str;
|
|
570
|
+
return str.slice(0, length - suffix.length) + suffix;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* 生成 slug
|
|
574
|
+
*/
|
|
575
|
+
static slugify(str) {
|
|
576
|
+
return str.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
var DateUtils = class _DateUtils {
|
|
580
|
+
/**
|
|
581
|
+
* 格式化日期
|
|
582
|
+
*/
|
|
583
|
+
static format(date = /* @__PURE__ */ new Date(), format = "YYYY-MM-DD HH:mm:ss") {
|
|
584
|
+
const year = date.getFullYear();
|
|
585
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
586
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
587
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
588
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
589
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
590
|
+
return format.replace("YYYY", String(year)).replace("MM", month).replace("DD", day).replace("HH", hours).replace("mm", minutes).replace("ss", seconds);
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* 获取相对时间
|
|
594
|
+
*/
|
|
595
|
+
static relative(date) {
|
|
596
|
+
const now = /* @__PURE__ */ new Date();
|
|
597
|
+
const diff = now.getTime() - date.getTime();
|
|
598
|
+
const seconds = Math.floor(diff / 1e3);
|
|
599
|
+
const minutes = Math.floor(seconds / 60);
|
|
600
|
+
const hours = Math.floor(minutes / 60);
|
|
601
|
+
const days = Math.floor(hours / 24);
|
|
602
|
+
if (days > 7) {
|
|
603
|
+
return _DateUtils.format(date, "YYYY-MM-DD");
|
|
604
|
+
} else if (days > 0) {
|
|
605
|
+
return `${days} \u5929\u524D`;
|
|
606
|
+
} else if (hours > 0) {
|
|
607
|
+
return `${hours} \u5C0F\u65F6\u524D`;
|
|
608
|
+
} else if (minutes > 0) {
|
|
609
|
+
return `${minutes} \u5206\u949F\u524D`;
|
|
610
|
+
} else {
|
|
611
|
+
return "\u521A\u521A";
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
var GitUtils = class {
|
|
616
|
+
/**
|
|
617
|
+
* 检查是否在 Git 仓库中
|
|
618
|
+
*/
|
|
619
|
+
static async isGitRepo(cwd = process.cwd()) {
|
|
620
|
+
const gitDir = path.join(cwd, ".git");
|
|
621
|
+
return await FileUtils.exists(gitDir);
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* 获取当前分支名
|
|
625
|
+
*/
|
|
626
|
+
static async getCurrentBranch(cwd = process.cwd()) {
|
|
627
|
+
const { execa: execa3 } = await import("execa");
|
|
628
|
+
const { stdout } = await execa3("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
629
|
+
cwd
|
|
630
|
+
});
|
|
631
|
+
return stdout.trim();
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* 获取当前 commit hash
|
|
635
|
+
*/
|
|
636
|
+
static async getCurrentCommit(cwd = process.cwd()) {
|
|
637
|
+
const { execa: execa3 } = await import("execa");
|
|
638
|
+
const { stdout } = await execa3("git", ["rev-parse", "HEAD"], { cwd });
|
|
639
|
+
return stdout.trim().slice(0, 7);
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
var SpecUtils = class {
|
|
643
|
+
/**
|
|
644
|
+
* 解析 spec 文件
|
|
645
|
+
*/
|
|
646
|
+
static async parseSpec(file) {
|
|
647
|
+
const content = await FileUtils.read(file);
|
|
648
|
+
const spec = {};
|
|
649
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
650
|
+
if (titleMatch) {
|
|
651
|
+
spec.title = titleMatch[1];
|
|
652
|
+
}
|
|
653
|
+
const nameMatch = content.match(/\*\*功能名称\*\*:\s*(.+)$/m);
|
|
654
|
+
if (nameMatch) {
|
|
655
|
+
spec.name = nameMatch[1];
|
|
656
|
+
}
|
|
657
|
+
const priorityMatch = content.match(/\*\*优先级\*\*:\s*(P[0-2])/);
|
|
658
|
+
if (priorityMatch) {
|
|
659
|
+
spec.priority = priorityMatch[1];
|
|
660
|
+
}
|
|
661
|
+
const statusMatch = content.match(/\*\*状态\*\*:\s*(.+)$/m);
|
|
662
|
+
if (statusMatch) {
|
|
663
|
+
spec.status = statusMatch[1];
|
|
664
|
+
}
|
|
665
|
+
const depSection = content.match(/## 依赖关系\s+([\s\S]+?)##/m);
|
|
666
|
+
if (depSection) {
|
|
667
|
+
const depMatches = depSection[1].matchAll(/- \[([ x])\]\s*(.+)$/gm);
|
|
668
|
+
spec.dependencies = Array.from(depMatches).map((m) => ({
|
|
669
|
+
completed: m[1] === "x",
|
|
670
|
+
name: m[2]
|
|
671
|
+
}));
|
|
672
|
+
}
|
|
673
|
+
return spec;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* 获取 spec 状态
|
|
677
|
+
*/
|
|
678
|
+
static async getSpecStatus(file) {
|
|
679
|
+
try {
|
|
680
|
+
const spec = await this.parseSpec(file);
|
|
681
|
+
return spec.status || "\u672A\u77E5";
|
|
682
|
+
} catch {
|
|
683
|
+
return "\u9519\u8BEF";
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// src/commands/init.ts
|
|
689
|
+
import { Listr } from "listr2";
|
|
690
|
+
var initCommand = new Command("init").argument("[project-name]", "\u9879\u76EE\u540D\u79F0").description("\u521D\u59CB\u5316\u65B0\u9879\u76EE").option("-d, --dir <directory>", "\u9879\u76EE\u76EE\u5F55", ".").option("--no-docker", "\u4E0D\u751F\u6210 Docker \u914D\u7F6E").option("--no-git", "\u4E0D\u521D\u59CB\u5316 Git").action(async (projectName, options) => {
|
|
691
|
+
try {
|
|
692
|
+
if (!projectName) {
|
|
693
|
+
const answers = await inquirer.prompt([
|
|
694
|
+
{
|
|
695
|
+
type: "input",
|
|
696
|
+
name: "projectName",
|
|
697
|
+
message: "\u8BF7\u8F93\u5165\u9879\u76EE\u540D\u79F0:",
|
|
698
|
+
default: "my-project",
|
|
699
|
+
validate: (input) => {
|
|
700
|
+
if (!/^[a-z0-9-]+$/.test(input)) {
|
|
701
|
+
return "\u9879\u76EE\u540D\u79F0\u53EA\u80FD\u5305\u542B\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u8FDE\u5B57\u7B26";
|
|
702
|
+
}
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
]);
|
|
707
|
+
projectName = answers.projectName;
|
|
708
|
+
}
|
|
709
|
+
if (!StringUtils2.validateProjectName(projectName)) {
|
|
710
|
+
logger.error("\u9879\u76EE\u540D\u79F0\u53EA\u80FD\u5305\u542B\u5C0F\u5199\u5B57\u6BCD\u3001\u6570\u5B57\u548C\u8FDE\u5B57\u7B26");
|
|
711
|
+
process.exit(1);
|
|
712
|
+
}
|
|
713
|
+
logger.header("\u521D\u59CB\u5316\u9879\u76EE");
|
|
714
|
+
logger.newLine();
|
|
715
|
+
const hasClaude = await claudeAI.checkInstalled();
|
|
716
|
+
if (!hasClaude) {
|
|
717
|
+
logger.error("\u672A\u68C0\u6D4B\u5230 Claude CLI");
|
|
718
|
+
logger.info("\u8BF7\u5B89\u88C5 Claude CLI: npm install -g @anthropic-ai/claude-code");
|
|
719
|
+
process.exit(1);
|
|
720
|
+
}
|
|
721
|
+
const projectPath = path2.resolve(options.dir, projectName);
|
|
722
|
+
if (await FileUtils.exists(projectPath)) {
|
|
723
|
+
logger.error(`\u76EE\u5F55\u5DF2\u5B58\u5728: ${projectPath}`);
|
|
724
|
+
logger.info("\u8BF7\u9009\u62E9\u5176\u4ED6\u9879\u76EE\u540D\u79F0\u6216\u5220\u9664\u73B0\u6709\u76EE\u5F55");
|
|
725
|
+
process.exit(1);
|
|
726
|
+
}
|
|
727
|
+
const tasks = new Listr(
|
|
728
|
+
[
|
|
729
|
+
{
|
|
730
|
+
title: "\u68C0\u67E5\u73AF\u5883",
|
|
731
|
+
task: async () => {
|
|
732
|
+
const version = await claudeAI.getVersion();
|
|
733
|
+
logger.info(`Claude \u7248\u672C: ${version}`);
|
|
734
|
+
}
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
title: "\u521B\u5EFA\u9879\u76EE\u76EE\u5F55",
|
|
738
|
+
task: async () => {
|
|
739
|
+
await FileUtils.ensureDir(projectPath);
|
|
740
|
+
await FileUtils.ensureDir(path2.join(projectPath, "frontend"));
|
|
741
|
+
await FileUtils.ensureDir(path2.join(projectPath, "backend"));
|
|
742
|
+
await FileUtils.ensureDir(path2.join(projectPath, "docs/specs"));
|
|
743
|
+
await FileUtils.ensureDir(path2.join(projectPath, "docs/api"));
|
|
744
|
+
await FileUtils.ensureDir(path2.join(projectPath, "docs/sessions"));
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
title: "\u751F\u6210\u6280\u672F\u6808\u6587\u6863",
|
|
749
|
+
task: async () => {
|
|
750
|
+
await generateTechStack(projectPath);
|
|
751
|
+
}
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
title: "\u751F\u6210\u5F00\u53D1\u89C4\u8303\u6587\u6863",
|
|
755
|
+
task: async () => {
|
|
756
|
+
await generateConventions(projectPath);
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
title: "\u751F\u6210 AI Memory",
|
|
761
|
+
task: async () => {
|
|
762
|
+
await generateAIMemory(projectPath, projectName);
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
title: "\u751F\u6210 Spec \u6A21\u677F",
|
|
767
|
+
task: async () => {
|
|
768
|
+
await generateSpecTemplate(projectPath);
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
title: "\u514B\u9686\u540E\u7AEF\u6A21\u677F",
|
|
773
|
+
task: async () => {
|
|
774
|
+
await cloneBackendTemplate(projectPath);
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
title: "\u751F\u6210\u524D\u7AEF\u811A\u624B\u67B6",
|
|
779
|
+
task: async () => {
|
|
780
|
+
await generateFrontendScaffold(projectPath);
|
|
781
|
+
}
|
|
782
|
+
},
|
|
783
|
+
...options.docker ? [
|
|
784
|
+
{
|
|
785
|
+
title: "\u751F\u6210 Docker \u914D\u7F6E",
|
|
786
|
+
task: async () => {
|
|
787
|
+
await generateDockerFiles(projectPath);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
] : [],
|
|
791
|
+
...options.git ? [
|
|
792
|
+
{
|
|
793
|
+
title: "\u521D\u59CB\u5316 Git",
|
|
794
|
+
task: async () => {
|
|
795
|
+
await initGit(projectPath, projectName);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
] : []
|
|
799
|
+
],
|
|
800
|
+
{
|
|
801
|
+
concurrent: false,
|
|
802
|
+
exitOnError: true
|
|
803
|
+
}
|
|
804
|
+
);
|
|
805
|
+
await tasks.run();
|
|
806
|
+
logger.newLine();
|
|
807
|
+
logger.success(`\u9879\u76EE ${projectName} \u521D\u59CB\u5316\u5B8C\u6210\uFF01`);
|
|
808
|
+
logger.newLine();
|
|
809
|
+
logger.info("\u4E0B\u4E00\u6B65:");
|
|
810
|
+
logger.step(`cd ${projectName}`);
|
|
811
|
+
logger.step("team-cli add-feature <feature-name>");
|
|
812
|
+
logger.step("team-cli breakdown docs/specs/xxx.md");
|
|
813
|
+
logger.step("team-cli dev");
|
|
814
|
+
logger.newLine();
|
|
815
|
+
} catch (error) {
|
|
816
|
+
logger.error(`\u521D\u59CB\u5316\u5931\u8D25: ${error.message}`);
|
|
817
|
+
if (process.env.DEBUG) {
|
|
818
|
+
console.error(error);
|
|
819
|
+
}
|
|
820
|
+
process.exit(1);
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
async function generateTechStack(projectPath) {
|
|
824
|
+
const content = `# \u6280\u672F\u6808
|
|
825
|
+
|
|
826
|
+
## \u540E\u7AEF\u6280\u672F\u6808
|
|
827
|
+
|
|
828
|
+
| \u7EC4\u4EF6 | \u6280\u672F\u9009\u578B | \u7248\u672C | \u8BF4\u660E |
|
|
829
|
+
|------|---------|------|------|
|
|
830
|
+
| \u8BED\u8A00 | Java | 17 | LTS \u7248\u672C |
|
|
831
|
+
| \u6846\u67B6 | Spring Boot | 3.2 | \u73B0\u4EE3\u5316 Java \u6846\u67B6 |
|
|
832
|
+
| \u6784\u5EFA\u5DE5\u5177 | Gradle | 8.x | \u5FEB\u901F\u3001\u7075\u6D3B\u7684\u6784\u5EFA\u5DE5\u5177 |
|
|
833
|
+
| ORM | MyBatis Plus | 3.5 | \u589E\u5F3A MyBatis\uFF0C\u7B80\u5316 CRUD |
|
|
834
|
+
| \u6570\u636E\u5E93 | MySQL | 8.0 | \u5173\u7CFB\u578B\u6570\u636E\u5E93 |
|
|
835
|
+
| \u7F13\u5B58 | Redis | 7.x | \u7F13\u5B58\u548C\u4F1A\u8BDD\u5B58\u50A8 |
|
|
836
|
+
| \u6587\u6863 | SpringDoc OpenAPI | 2.3 | API \u6587\u6863\u81EA\u52A8\u751F\u6210 |
|
|
837
|
+
|
|
838
|
+
## \u524D\u7AEF\u6280\u672F\u6808
|
|
839
|
+
|
|
840
|
+
| \u7EC4\u4EF6 | \u6280\u672F\u9009\u578B | \u7248\u672C | \u8BF4\u660E |
|
|
841
|
+
|------|---------|------|------|
|
|
842
|
+
| \u6846\u67B6 | Next.js | 14 | React \u5168\u6808\u6846\u67B6 |
|
|
843
|
+
| \u8BED\u8A00 | TypeScript | 5.x | \u7C7B\u578B\u5B89\u5168 |
|
|
844
|
+
| \u6837\u5F0F | Tailwind CSS | 3.x | \u539F\u5B50\u5316 CSS |
|
|
845
|
+
| UI \u5E93 | Shadcn/UI | latest | \u57FA\u4E8E Radix UI \u7684\u7EC4\u4EF6\u5E93 |
|
|
846
|
+
| \u56FE\u6807 | Lucide React | latest | \u4E00\u81F4\u6027\u56FE\u6807\u5E93 |
|
|
847
|
+
| \u72B6\u6001\u7BA1\u7406 | React Context + Hooks | - | \u8F7B\u91CF\u7EA7\u72B6\u6001\u7BA1\u7406 |
|
|
848
|
+
| \u8868\u5355 | React Hook Form | 7.x | \u9AD8\u6027\u80FD\u8868\u5355 |
|
|
849
|
+
| \u6570\u636E\u9A8C\u8BC1 | Zod | 3.x | TypeScript \u4F18\u5148\u7684\u9A8C\u8BC1\u5E93 |
|
|
850
|
+
|
|
851
|
+
## \u5F00\u53D1\u5DE5\u5177
|
|
852
|
+
|
|
853
|
+
| \u5DE5\u5177 | \u7528\u9014 |
|
|
854
|
+
|------|------|
|
|
855
|
+
| ESLint | \u4EE3\u7801\u68C0\u67E5 |
|
|
856
|
+
| Prettier | \u4EE3\u7801\u683C\u5F0F\u5316 |
|
|
857
|
+
| Husky | Git Hooks |
|
|
858
|
+
| Commitlint | \u63D0\u4EA4\u4FE1\u606F\u89C4\u8303 |
|
|
859
|
+
| Docker | \u5BB9\u5668\u5316\u90E8\u7F72 |
|
|
860
|
+
|
|
861
|
+
## \u4EE3\u7801\u89C4\u8303
|
|
862
|
+
|
|
863
|
+
### \u540E\u7AEF\u89C4\u8303
|
|
864
|
+
|
|
865
|
+
- \u4F7F\u7528 DTO \u6A21\u5F0F\u8FDB\u884C\u6570\u636E\u4F20\u8F93
|
|
866
|
+
- Controller \u8D1F\u8D23\u63A5\u6536\u8BF7\u6C42\uFF0CService \u5904\u7406\u4E1A\u52A1\u903B\u8F91
|
|
867
|
+
- \u4F7F\u7528 @Validated \u8FDB\u884C\u53C2\u6570\u6821\u9A8C
|
|
868
|
+
- \u7EDF\u4E00\u5F02\u5E38\u5904\u7406\u548C\u54CD\u5E94\u683C\u5F0F
|
|
869
|
+
- API \u8DEF\u5F84\u4F7F\u7528 kebab-case
|
|
870
|
+
- \u7C7B\u540D\u4F7F\u7528 PascalCase
|
|
871
|
+
- \u65B9\u6CD5\u540D\u4F7F\u7528 camelCase
|
|
872
|
+
|
|
873
|
+
### \u524D\u7AEF\u89C4\u8303
|
|
874
|
+
|
|
875
|
+
- \u7EC4\u4EF6\u4F7F\u7528 PascalCase
|
|
876
|
+
- \u6587\u4EF6\u540D\u4F7F\u7528 kebab-case
|
|
877
|
+
- \u4F7F\u7528 TypeScript \u7C7B\u578B\u5B9A\u4E49
|
|
878
|
+
- Props \u63A5\u53E3\u5B9A\u4E49\u5728\u7EC4\u4EF6\u5185\u90E8
|
|
879
|
+
- \u4F7F\u7528 Semantic HTML
|
|
880
|
+
- \u54CD\u5E94\u5F0F\u8BBE\u8BA1\u4F18\u5148
|
|
881
|
+
|
|
882
|
+
### Git \u89C4\u8303
|
|
883
|
+
|
|
884
|
+
- \u5206\u652F\u547D\u540D: \`feature/xxx\`, \`bugfix/xxx\`, \`hotfix/xxx\`
|
|
885
|
+
- \u63D0\u4EA4\u4FE1\u606F: \`feat: xxx\`, \`fix: xxx\`, \`docs: xxx\`
|
|
886
|
+
- \u63D0\u4EA4\u524D\u5FC5\u987B\u901A\u8FC7 lint \u68C0\u67E5
|
|
887
|
+
|
|
888
|
+
## \u76EE\u5F55\u7ED3\u6784
|
|
889
|
+
|
|
890
|
+
\`\`\`
|
|
891
|
+
backend/
|
|
892
|
+
\u251C\u2500\u2500 src/main/java/com/example/demo/
|
|
893
|
+
\u2502 \u251C\u2500\u2500 controller/ # API \u63A7\u5236\u5668
|
|
894
|
+
\u2502 \u251C\u2500\u2500 service/ # \u4E1A\u52A1\u903B\u8F91
|
|
895
|
+
\u2502 \u251C\u2500\u2500 mapper/ # \u6570\u636E\u8BBF\u95EE
|
|
896
|
+
\u2502 \u251C\u2500\u2500 entity/ # \u6570\u636E\u6A21\u578B
|
|
897
|
+
\u2502 \u251C\u2500\u2500 dto/ # \u6570\u636E\u4F20\u8F93\u5BF9\u8C61
|
|
898
|
+
\u2502 \u251C\u2500\u2500 config/ # \u914D\u7F6E\u7C7B
|
|
899
|
+
\u2502 \u2514\u2500\u2500 util/ # \u5DE5\u5177\u7C7B
|
|
900
|
+
\u251C\u2500\u2500 src/main/resources/
|
|
901
|
+
\u2502 \u251C\u2500\u2500 mapper/ # MyBatis XML
|
|
902
|
+
\u2502 \u2514\u2500\u2500 application.yml # \u914D\u7F6E\u6587\u4EF6
|
|
903
|
+
\u2514\u2500\u2500 src/test/ # \u6D4B\u8BD5\u4EE3\u7801
|
|
904
|
+
|
|
905
|
+
frontend/
|
|
906
|
+
\u251C\u2500\u2500 src/
|
|
907
|
+
\u2502 \u251C\u2500\u2500 app/ # Next.js App Router
|
|
908
|
+
\u2502 \u251C\u2500\u2500 components/ # React \u7EC4\u4EF6
|
|
909
|
+
\u2502 \u251C\u2500\u2500 lib/ # \u5DE5\u5177\u5E93
|
|
910
|
+
\u2502 \u2514\u2500\u2500 types/ # TypeScript \u7C7B\u578B
|
|
911
|
+
\u2514\u2500\u2500 public/ # \u9759\u6001\u8D44\u6E90
|
|
912
|
+
|
|
913
|
+
docs/
|
|
914
|
+
\u251C\u2500\u2500 specs/ # \u529F\u80FD\u89C4\u683C\u6587\u6863
|
|
915
|
+
\u251C\u2500\u2500 api/ # API \u6587\u6863
|
|
916
|
+
\u2514\u2500\u2500 sessions/ # \u5F00\u53D1\u4F1A\u8BDD\u8BB0\u5F55
|
|
917
|
+
\`\`\`
|
|
918
|
+
`;
|
|
919
|
+
await FileUtils.write(path2.join(projectPath, "TECH_STACK.md"), content);
|
|
920
|
+
}
|
|
921
|
+
async function generateConventions(projectPath) {
|
|
922
|
+
const content = `# \u5F00\u53D1\u89C4\u8303
|
|
923
|
+
|
|
924
|
+
## \u540E\u7AEF\u5F00\u53D1\u89C4\u8303
|
|
925
|
+
|
|
926
|
+
### 1. \u5206\u5C42\u67B6\u6784
|
|
927
|
+
|
|
928
|
+
\`\`\`
|
|
929
|
+
Controller \u2192 Service \u2192 Mapper \u2192 Database
|
|
930
|
+
\u2193 \u2193
|
|
931
|
+
DTO Entity
|
|
932
|
+
\`\`\`
|
|
933
|
+
|
|
934
|
+
**\u804C\u8D23\u5212\u5206:**
|
|
935
|
+
- **Controller**: \u63A5\u6536 HTTP \u8BF7\u6C42\uFF0C\u53C2\u6570\u6821\u9A8C\uFF0C\u8C03\u7528 Service
|
|
936
|
+
- **Service**: \u4E1A\u52A1\u903B\u8F91\u5904\u7406\uFF0C\u4E8B\u52A1\u7BA1\u7406
|
|
937
|
+
- **Mapper**: \u6570\u636E\u5E93\u64CD\u4F5C\uFF0CSQL \u6267\u884C
|
|
938
|
+
- **DTO**: \u6570\u636E\u4F20\u8F93\u5BF9\u8C61\uFF0C\u7528\u4E8E API \u63A5\u53E3
|
|
939
|
+
- **Entity**: \u6570\u636E\u5E93\u5B9E\u4F53\u6620\u5C04
|
|
940
|
+
|
|
941
|
+
### 2. \u547D\u540D\u89C4\u8303
|
|
942
|
+
|
|
943
|
+
| \u7C7B\u578B | \u89C4\u8303 | \u793A\u4F8B |
|
|
944
|
+
|------|------|------|
|
|
945
|
+
| \u7C7B\u540D | PascalCase | \`UserController\` |
|
|
946
|
+
| \u65B9\u6CD5\u540D | camelCase | \`getUserById\` |
|
|
947
|
+
| \u5E38\u91CF | UPPER_SNAKE_CASE | \`MAX_RETRY_COUNT\` |
|
|
948
|
+
| \u5305\u540D | \u5C0F\u5199\u70B9\u5206\u9694 | \`com.example.demo.controller\` |
|
|
949
|
+
| API \u8DEF\u5F84 | kebab-case | \`/api/users\` |
|
|
950
|
+
|
|
951
|
+
### 3. API \u8BBE\u8BA1
|
|
952
|
+
|
|
953
|
+
**RESTful \u98CE\u683C:**
|
|
954
|
+
|
|
955
|
+
\`\`\`
|
|
956
|
+
GET /api/users # \u5217\u8868
|
|
957
|
+
GET /api/users/{id} # \u8BE6\u60C5
|
|
958
|
+
POST /api/users # \u521B\u5EFA
|
|
959
|
+
PUT /api/users/{id} # \u66F4\u65B0
|
|
960
|
+
DELETE /api/users/{id} # \u5220\u9664
|
|
961
|
+
\`\`\`
|
|
962
|
+
|
|
963
|
+
**\u7EDF\u4E00\u54CD\u5E94\u683C\u5F0F:**
|
|
964
|
+
|
|
965
|
+
\`\`\`json
|
|
966
|
+
{
|
|
967
|
+
"code": 200,
|
|
968
|
+
"message": "success",
|
|
969
|
+
"data": {...}
|
|
970
|
+
}
|
|
971
|
+
\`\`\`
|
|
972
|
+
|
|
973
|
+
### 4. \u5F02\u5E38\u5904\u7406
|
|
974
|
+
|
|
975
|
+
\u4F7F\u7528 \`@ControllerAdvice\` \u7EDF\u4E00\u5904\u7406\u5F02\u5E38\uFF1A
|
|
976
|
+
|
|
977
|
+
\`\`\`java
|
|
978
|
+
@RestControllerAdvice
|
|
979
|
+
public class GlobalExceptionHandler {
|
|
980
|
+
|
|
981
|
+
@ExceptionHandler(BusinessException.class)
|
|
982
|
+
public Result<Void> handleBusinessException(BusinessException e) {
|
|
983
|
+
return Result.error(e.getMessage());
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
\`\`\`
|
|
987
|
+
|
|
988
|
+
### 5. \u53C2\u6570\u6821\u9A8C
|
|
989
|
+
|
|
990
|
+
\u4F7F\u7528 \`@Validated\` \u548C JSR-303 \u6CE8\u89E3\uFF1A
|
|
991
|
+
|
|
992
|
+
\`\`\`java
|
|
993
|
+
@PostMapping("/users")
|
|
994
|
+
public Result<User> createUser(@Validated @RequestBody UserDTO dto) {
|
|
995
|
+
// ...
|
|
996
|
+
}
|
|
997
|
+
\`\`\`
|
|
998
|
+
|
|
999
|
+
### 6. \u4E8B\u52A1\u7BA1\u7406
|
|
1000
|
+
|
|
1001
|
+
\u5728 Service \u5C42\u4F7F\u7528 \`@Transactional\`\uFF1A
|
|
1002
|
+
|
|
1003
|
+
\`\`\`java
|
|
1004
|
+
@Service
|
|
1005
|
+
public class UserService {
|
|
1006
|
+
|
|
1007
|
+
@Transactional(rollbackFor = Exception.class)
|
|
1008
|
+
public void createUserWithRole(User user, Role role) {
|
|
1009
|
+
// ...
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
\`\`\`
|
|
1013
|
+
|
|
1014
|
+
## \u524D\u7AEF\u5F00\u53D1\u89C4\u8303
|
|
1015
|
+
|
|
1016
|
+
### 1. \u7EC4\u4EF6\u8BBE\u8BA1
|
|
1017
|
+
|
|
1018
|
+
**\u7EC4\u4EF6\u7ED3\u6784:**
|
|
1019
|
+
|
|
1020
|
+
\`\`\`typescript
|
|
1021
|
+
// \u7EC4\u4EF6\u5B9A\u4E49
|
|
1022
|
+
interface Props {
|
|
1023
|
+
// props \u7C7B\u578B\u5B9A\u4E49
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
export function ComponentName({ prop1, prop2 }: Props) {
|
|
1027
|
+
// \u7EC4\u4EF6\u903B\u8F91
|
|
1028
|
+
|
|
1029
|
+
return (
|
|
1030
|
+
// JSX
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
\`\`\`
|
|
1034
|
+
|
|
1035
|
+
**\u547D\u540D\u89C4\u8303:**
|
|
1036
|
+
- \u7EC4\u4EF6\u6587\u4EF6: kebab-case (\`user-card.tsx\`)
|
|
1037
|
+
- \u7EC4\u4EF6\u540D: PascalCase (\`UserCard\`)
|
|
1038
|
+
- Hook \u6587\u4EF6: kebab-case, \u4EE5 \`use-\` \u5F00\u5934 (\`use-user-data.ts\`)
|
|
1039
|
+
|
|
1040
|
+
### 2. TypeScript \u4F7F\u7528
|
|
1041
|
+
|
|
1042
|
+
**\u5B9A\u4E49\u63A5\u53E3:**
|
|
1043
|
+
|
|
1044
|
+
\`\`\`typescript
|
|
1045
|
+
interface User {
|
|
1046
|
+
id: string;
|
|
1047
|
+
name: string;
|
|
1048
|
+
email: string;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
interface CreateUserDto {
|
|
1052
|
+
name: string;
|
|
1053
|
+
email: string;
|
|
1054
|
+
}
|
|
1055
|
+
\`\`\`
|
|
1056
|
+
|
|
1057
|
+
**\u907F\u514D\u4F7F\u7528 \`any\`:**
|
|
1058
|
+
|
|
1059
|
+
\`\`\`typescript
|
|
1060
|
+
// \u274C \u4E0D\u597D
|
|
1061
|
+
const data: any = await fetchData();
|
|
1062
|
+
|
|
1063
|
+
// \u2705 \u597D
|
|
1064
|
+
const data: User = await fetchData();
|
|
1065
|
+
\`\`\`
|
|
1066
|
+
|
|
1067
|
+
### 3. \u72B6\u6001\u7BA1\u7406
|
|
1068
|
+
|
|
1069
|
+
**\u4F7F\u7528 React Context:**
|
|
1070
|
+
|
|
1071
|
+
\`\`\`typescript
|
|
1072
|
+
const UserContext = createContext<UserContextType>({
|
|
1073
|
+
user: null,
|
|
1074
|
+
login: async () => {},
|
|
1075
|
+
logout: () => {},
|
|
1076
|
+
});
|
|
1077
|
+
\`\`\`
|
|
1078
|
+
|
|
1079
|
+
**\u4F18\u5148\u4F7F\u7528\u672C\u5730\u72B6\u6001:**
|
|
1080
|
+
|
|
1081
|
+
\`\`\`typescript
|
|
1082
|
+
// \u7B80\u5355\u72B6\u6001\u4F7F\u7528 useState
|
|
1083
|
+
const [count, setCount] = useState(0);
|
|
1084
|
+
|
|
1085
|
+
// \u590D\u6742\u903B\u8F91\u4F7F\u7528 useReducer
|
|
1086
|
+
const [state, dispatch] = useReducer(reducer, initialState);
|
|
1087
|
+
\`\`\`
|
|
1088
|
+
|
|
1089
|
+
### 4. \u6837\u5F0F\u89C4\u8303
|
|
1090
|
+
|
|
1091
|
+
**\u4F7F\u7528 Tailwind CSS:**
|
|
1092
|
+
|
|
1093
|
+
\`\`\`tsx
|
|
1094
|
+
<div className="flex items-center gap-4 p-4 bg-white rounded-lg">
|
|
1095
|
+
{/* ... */}
|
|
1096
|
+
</div>
|
|
1097
|
+
\`\`\`
|
|
1098
|
+
|
|
1099
|
+
**\u54CD\u5E94\u5F0F\u8BBE\u8BA1:**
|
|
1100
|
+
|
|
1101
|
+
\`\`\`tsx
|
|
1102
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
1103
|
+
{/* ... */}
|
|
1104
|
+
</div>
|
|
1105
|
+
\`\`\`
|
|
1106
|
+
|
|
1107
|
+
### 5. \u8868\u5355\u5904\u7406
|
|
1108
|
+
|
|
1109
|
+
**\u4F7F\u7528 React Hook Form:**
|
|
1110
|
+
|
|
1111
|
+
\`\`\`typescript
|
|
1112
|
+
const { register, handleSubmit, formState: { errors } } = useForm<FormData>();
|
|
1113
|
+
|
|
1114
|
+
const onSubmit = (data: FormData) => {
|
|
1115
|
+
console.log(data);
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
return (
|
|
1119
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
1120
|
+
<input {...register('name', { required: true })} />
|
|
1121
|
+
{errors.name && <span>\u5FC5\u586B</span>}
|
|
1122
|
+
</form>
|
|
1123
|
+
);
|
|
1124
|
+
\`\`\`
|
|
1125
|
+
|
|
1126
|
+
### 6. \u6570\u636E\u83B7\u53D6
|
|
1127
|
+
|
|
1128
|
+
**\u4F7F\u7528 Server Components (Next.js 14):**
|
|
1129
|
+
|
|
1130
|
+
\`\`\`typescript
|
|
1131
|
+
async function getUser(id: string) {
|
|
1132
|
+
const res = await fetch(\`/api/users/\${id}\`);
|
|
1133
|
+
if (!res.ok) throw new Error('Failed to fetch');
|
|
1134
|
+
return res.json();
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
export default async function UserPage({ params }) {
|
|
1138
|
+
const user = await getUser(params.id);
|
|
1139
|
+
return <div>{user.name}</div>;
|
|
1140
|
+
}
|
|
1141
|
+
\`\`\`
|
|
1142
|
+
|
|
1143
|
+
## Git \u5DE5\u4F5C\u6D41
|
|
1144
|
+
|
|
1145
|
+
### \u5206\u652F\u7B56\u7565
|
|
1146
|
+
|
|
1147
|
+
\`\`\`
|
|
1148
|
+
main # \u4E3B\u5206\u652F\uFF0C\u59CB\u7EC8\u4FDD\u6301\u7A33\u5B9A
|
|
1149
|
+
\u251C\u2500\u2500 feature/xxx # \u529F\u80FD\u5206\u652F
|
|
1150
|
+
\u251C\u2500\u2500 bugfix/xxx # Bug \u4FEE\u590D\u5206\u652F
|
|
1151
|
+
\u2514\u2500\u2500 hotfix/xxx # \u7D27\u6025\u4FEE\u590D\u5206\u652F
|
|
1152
|
+
\`\`\`
|
|
1153
|
+
|
|
1154
|
+
### \u63D0\u4EA4\u4FE1\u606F\u89C4\u8303
|
|
1155
|
+
|
|
1156
|
+
\`\`\`
|
|
1157
|
+
<type>(<scope>): <subject>
|
|
1158
|
+
|
|
1159
|
+
<body>
|
|
1160
|
+
|
|
1161
|
+
<footer>
|
|
1162
|
+
\`\`\`
|
|
1163
|
+
|
|
1164
|
+
**\u7C7B\u578B (type):**
|
|
1165
|
+
- \`feat\`: \u65B0\u529F\u80FD
|
|
1166
|
+
- \`fix\`: Bug \u4FEE\u590D
|
|
1167
|
+
- \`docs\`: \u6587\u6863\u66F4\u65B0
|
|
1168
|
+
- \`style\`: \u4EE3\u7801\u683C\u5F0F\u8C03\u6574
|
|
1169
|
+
- \`refactor\`: \u4EE3\u7801\u91CD\u6784
|
|
1170
|
+
- \`test\`: \u6D4B\u8BD5\u76F8\u5173
|
|
1171
|
+
- \`chore\`: \u6784\u5EFA/\u5DE5\u5177\u76F8\u5173
|
|
1172
|
+
|
|
1173
|
+
**\u793A\u4F8B:**
|
|
1174
|
+
|
|
1175
|
+
\`\`\`
|
|
1176
|
+
feat(auth): add user login feature
|
|
1177
|
+
|
|
1178
|
+
implement JWT based authentication with:
|
|
1179
|
+
- login endpoint
|
|
1180
|
+
- token refresh
|
|
1181
|
+
- logout handling
|
|
1182
|
+
|
|
1183
|
+
Closes #123
|
|
1184
|
+
\`\`\`
|
|
1185
|
+
|
|
1186
|
+
## \u6D4B\u8BD5\u89C4\u8303
|
|
1187
|
+
|
|
1188
|
+
### \u540E\u7AEF\u6D4B\u8BD5
|
|
1189
|
+
|
|
1190
|
+
\`\`\`java
|
|
1191
|
+
@SpringBootTest
|
|
1192
|
+
class UserServiceTest {
|
|
1193
|
+
|
|
1194
|
+
@Autowired
|
|
1195
|
+
private UserService userService;
|
|
1196
|
+
|
|
1197
|
+
@Test
|
|
1198
|
+
void shouldCreateUser() {
|
|
1199
|
+
// given
|
|
1200
|
+
User user = new User("John");
|
|
1201
|
+
|
|
1202
|
+
// when
|
|
1203
|
+
User saved = userService.save(user);
|
|
1204
|
+
|
|
1205
|
+
// then
|
|
1206
|
+
assertNotNull(saved.getId());
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
\`\`\`
|
|
1210
|
+
|
|
1211
|
+
### \u524D\u7AEF\u6D4B\u8BD5
|
|
1212
|
+
|
|
1213
|
+
\`\`\`typescript
|
|
1214
|
+
import { render, screen } from '@testing-library/react';
|
|
1215
|
+
|
|
1216
|
+
test('renders user name', () => {
|
|
1217
|
+
render(<UserProfile name="John" />);
|
|
1218
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
1219
|
+
});
|
|
1220
|
+
\`\`\`
|
|
1221
|
+
`;
|
|
1222
|
+
await FileUtils.write(path2.join(projectPath, "CONVENTIONS.md"), content);
|
|
1223
|
+
}
|
|
1224
|
+
async function generateAIMemory(projectPath, projectName) {
|
|
1225
|
+
const content = `# AI Memory - \u9879\u76EE\u72B6\u6001\u8BB0\u5F55
|
|
1226
|
+
|
|
1227
|
+
## \u9879\u76EE\u4FE1\u606F
|
|
1228
|
+
- **\u9879\u76EE\u540D\u79F0**: ${projectName}
|
|
1229
|
+
- **\u521B\u5EFA\u65F6\u95F4**: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
|
|
1230
|
+
- **\u5F53\u524D\u9636\u6BB5**: \u521D\u59CB\u5316
|
|
1231
|
+
- **\u6700\u540E\u66F4\u65B0**: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
|
|
1232
|
+
|
|
1233
|
+
## \u529F\u80FD\u6E05\u5355 (Feature Inventory)
|
|
1234
|
+
|
|
1235
|
+
| \u529F\u80FD | Spec \u6587\u4EF6 | \u72B6\u6001 | \u8FDB\u5EA6 | \u5B8C\u6210\u65E5\u671F | \u5907\u6CE8 |
|
|
1236
|
+
|------|----------|------|------|---------|------|
|
|
1237
|
+
|
|
1238
|
+
## API \u5217\u8868 (API Inventory)
|
|
1239
|
+
|
|
1240
|
+
> \u672C\u90E8\u5206\u7531 team-cli \u81EA\u52A8\u626B\u63CF\u540E\u7AEF Controller \u751F\u6210
|
|
1241
|
+
|
|
1242
|
+
## \u6570\u636E\u6A21\u578B\u6982\u89C8
|
|
1243
|
+
|
|
1244
|
+
| \u8868\u540D | \u8BF4\u660E | \u5B57\u6BB5\u6570 | \u5173\u8054\u8868 | \u72B6\u6001 |
|
|
1245
|
+
|------|------|--------|--------|------|
|
|
1246
|
+
| user | \u7528\u6237\u8868 | - | - | - |
|
|
1247
|
+
|
|
1248
|
+
## \u5DF2\u5B8C\u6210\u7684 Milestones
|
|
1249
|
+
|
|
1250
|
+
## \u5F53\u524D\u4EFB\u52A1
|
|
1251
|
+
|
|
1252
|
+
## \u5F85\u5904\u7406\u4EFB\u52A1
|
|
1253
|
+
|
|
1254
|
+
## \u6280\u672F\u503A\u52A1
|
|
1255
|
+
|
|
1256
|
+
| \u65E5\u671F | \u95EE\u9898 | \u8BA1\u5212\u4FEE\u590D |
|
|
1257
|
+
|------|------|---------|
|
|
1258
|
+
|
|
1259
|
+
## \u5173\u952E\u51B3\u7B56\u8BB0\u5F55
|
|
1260
|
+
|
|
1261
|
+
| \u65E5\u671F | \u51B3\u7B56 | \u539F\u56E0 |
|
|
1262
|
+
|------|------|------|
|
|
1263
|
+
| ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]} | \u521D\u59CB\u5316\u9879\u76EE | \u4F7F\u7528 Spring Boot 3 + Next.js 14 \u6280\u672F\u6808 |
|
|
1264
|
+
|
|
1265
|
+
## \u6CE8\u610F\u4E8B\u9879
|
|
1266
|
+
|
|
1267
|
+
## Bugfix \u8BB0\u5F55
|
|
1268
|
+
|
|
1269
|
+
| Bug ID | \u65E5\u671F | \u95EE\u9898\u63CF\u8FF0 | \u72B6\u6001 |
|
|
1270
|
+
|--------|------|---------|------|
|
|
1271
|
+
`;
|
|
1272
|
+
await FileUtils.write(path2.join(projectPath, "AI_MEMORY.md"), content);
|
|
1273
|
+
}
|
|
1274
|
+
async function generateSpecTemplate(projectPath) {
|
|
1275
|
+
const content = `# [\u529F\u80FD\u6807\u9898]
|
|
1276
|
+
|
|
1277
|
+
## \u529F\u80FD\u6982\u8FF0
|
|
1278
|
+
**\u529F\u80FD\u540D\u79F0**: [\u529F\u80FD\u4E2D\u6587\u540D]
|
|
1279
|
+
**\u4F18\u5148\u7EA7**: P0/P1/P2
|
|
1280
|
+
**\u9884\u4F30\u5DE5\u65F6**: X \u5929
|
|
1281
|
+
**\u72B6\u6001**: \u5F85\u62C6\u5206
|
|
1282
|
+
**\u521B\u5EFA\u65E5\u671F**: {{DATE}}
|
|
1283
|
+
|
|
1284
|
+
## \u4F9D\u8D56\u5173\u7CFB
|
|
1285
|
+
**\u524D\u7F6E\u4F9D\u8D56**:
|
|
1286
|
+
- [ ] dependency-1.md
|
|
1287
|
+
- [ ] dependency-2.md
|
|
1288
|
+
|
|
1289
|
+
**\u88AB\u4F9D\u8D56\u4E8E**:
|
|
1290
|
+
- (\u81EA\u52A8\u751F\u6210)
|
|
1291
|
+
|
|
1292
|
+
## \u80CC\u666F\u4E0E\u76EE\u6807
|
|
1293
|
+
[\u7B80\u660E\u627C\u8981\u5730\u8BF4\u660E\u529F\u80FD\u7684\u80CC\u666F\u548C\u8981\u89E3\u51B3\u7684\u95EE\u9898]
|
|
1294
|
+
|
|
1295
|
+
## \u529F\u80FD\u9700\u6C42
|
|
1296
|
+
|
|
1297
|
+
### \u7528\u6237\u6545\u4E8B
|
|
1298
|
+
\`\`\`
|
|
1299
|
+
\u4F5C\u4E3A [\u5177\u4F53\u89D2\u8272]
|
|
1300
|
+
\u6211\u5E0C\u671B [\u5177\u4F53\u529F\u80FD]
|
|
1301
|
+
\u4EE5\u4FBF [\u5B9E\u73B0\u7684\u4EF7\u503C]
|
|
1302
|
+
\`\`\`
|
|
1303
|
+
|
|
1304
|
+
### \u529F\u80FD\u70B9
|
|
1305
|
+
1. \u529F\u80FD\u70B9\u4E00
|
|
1306
|
+
2. \u529F\u80FD\u70B9\u4E8C
|
|
1307
|
+
3. \u529F\u80FD\u70B9\u4E09
|
|
1308
|
+
|
|
1309
|
+
## \u6280\u672F\u8BBE\u8BA1
|
|
1310
|
+
|
|
1311
|
+
### API \u8BBE\u8BA1
|
|
1312
|
+
- \`POST /api/xxx\` - \u521B\u5EFA XXX
|
|
1313
|
+
- \`GET /api/xxx/{id}\` - \u83B7\u53D6 XXX \u8BE6\u60C5
|
|
1314
|
+
- \`PUT /api/xxx/{id}\` - \u66F4\u65B0 XXX
|
|
1315
|
+
- \`DELETE /api/xxx/{id}\` - \u5220\u9664 XXX
|
|
1316
|
+
|
|
1317
|
+
### \u6570\u636E\u6A21\u578B
|
|
1318
|
+
- \`table_name\` (\u8868\u8BF4\u660E) - \u5B57\u6BB5\u8BF4\u660E
|
|
1319
|
+
|
|
1320
|
+
## \u91CC\u7A0B\u7891 (Milestones)
|
|
1321
|
+
|
|
1322
|
+
> \u6CE8: \u4F7F\u7528 \`team-cli breakdown\` \u62C6\u5206\u6B64 spec \u4E3A milestones \u548C todos
|
|
1323
|
+
|
|
1324
|
+
### Milestone 1: [\u91CC\u7A0B\u7891\u540D\u79F0]
|
|
1325
|
+
- [ ] Todo 1
|
|
1326
|
+
- [ ] Todo 2
|
|
1327
|
+
- [ ] Todo 3
|
|
1328
|
+
|
|
1329
|
+
### Milestone 2: [\u91CC\u7A0B\u7891\u540D\u79F0]
|
|
1330
|
+
- [ ] Todo 1
|
|
1331
|
+
- [ ] Todo 2
|
|
1332
|
+
|
|
1333
|
+
----
|
|
1334
|
+
*\u751F\u6210\u4E8E: {{TIMESTAMP}} by team-cli*
|
|
1335
|
+
`;
|
|
1336
|
+
await FileUtils.write(path2.join(projectPath, "docs/specs/template.md"), content);
|
|
1337
|
+
}
|
|
1338
|
+
async function cloneBackendTemplate(projectPath) {
|
|
1339
|
+
const templateRepo = process.env.TEMPLATE_REPO || "git@gitlab.yungu-inc.org:yungu-app/java-scaffold-template.git";
|
|
1340
|
+
const backendPath = path2.join(projectPath, "backend");
|
|
1341
|
+
try {
|
|
1342
|
+
const { execaCommand } = await import("execa");
|
|
1343
|
+
const tempDir = path2.join(projectPath, ".template-temp");
|
|
1344
|
+
await execaCommand(`git clone --depth=1 ${templateRepo} ${tempDir}`, {
|
|
1345
|
+
stdio: "inherit",
|
|
1346
|
+
timeout: 6e4
|
|
1347
|
+
});
|
|
1348
|
+
await fs2.copy(tempDir, backendPath, {
|
|
1349
|
+
filter: (src) => !src.includes(".git")
|
|
1350
|
+
});
|
|
1351
|
+
await fs2.remove(tempDir);
|
|
1352
|
+
const gitDir = path2.join(backendPath, ".git");
|
|
1353
|
+
if (await FileUtils.exists(gitDir)) {
|
|
1354
|
+
await FileUtils.remove(gitDir);
|
|
1355
|
+
}
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
logger.warn("\u514B\u9686\u540E\u7AEF\u6A21\u677F\u5931\u8D25\uFF0C\u521B\u5EFA\u57FA\u7840\u7ED3\u6784");
|
|
1358
|
+
await FileUtils.ensureDir(path2.join(backendPath, "src/main/java/com/example"));
|
|
1359
|
+
await FileUtils.ensureDir(path2.join(backendPath, "src/main/resources"));
|
|
1360
|
+
await FileUtils.ensureDir(path2.join(backendPath, "src/test/java"));
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
async function generateFrontendScaffold(projectPath) {
|
|
1364
|
+
const frontendPath = path2.join(projectPath, "frontend");
|
|
1365
|
+
try {
|
|
1366
|
+
const prompt = `Read TECH_STACK.md and CONVENTIONS.md.
|
|
1367
|
+
Initialize a Next.js 14 frontend in ./frontend with:
|
|
1368
|
+
- TypeScript
|
|
1369
|
+
- Tailwind CSS
|
|
1370
|
+
- App Router structure
|
|
1371
|
+
- ESLint and Prettier configured
|
|
1372
|
+
- Basic layout and page components
|
|
1373
|
+
|
|
1374
|
+
Do not run any servers, just generate the folder structure and configuration files.`;
|
|
1375
|
+
await claudeAI.prompt(prompt, {
|
|
1376
|
+
contextFiles: [
|
|
1377
|
+
path2.join(projectPath, "TECH_STACK.md"),
|
|
1378
|
+
path2.join(projectPath, "CONVENTIONS.md")
|
|
1379
|
+
]
|
|
1380
|
+
});
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
logger.warn("Claude \u751F\u6210\u524D\u7AEF\u5931\u8D25\uFF0C\u5C06\u521B\u5EFA\u57FA\u7840\u7ED3\u6784");
|
|
1383
|
+
await FileUtils.ensureDir(path2.join(frontendPath, "src/app"));
|
|
1384
|
+
await FileUtils.ensureDir(path2.join(frontendPath, "src/components"));
|
|
1385
|
+
await FileUtils.ensureDir(path2.join(frontendPath, "src/lib"));
|
|
1386
|
+
await FileUtils.ensureDir(path2.join(frontendPath, "public"));
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
async function generateDockerFiles(projectPath) {
|
|
1390
|
+
logger.info("Docker \u914D\u7F6E\u751F\u6210\u5F85\u5B9E\u73B0");
|
|
1391
|
+
}
|
|
1392
|
+
async function initGit(projectPath, projectName) {
|
|
1393
|
+
try {
|
|
1394
|
+
const { execaCommand } = await import("execa");
|
|
1395
|
+
await execaCommand("git init", { cwd: projectPath, stdio: "pipe" });
|
|
1396
|
+
await execaCommand("git add .", { cwd: projectPath, stdio: "pipe" });
|
|
1397
|
+
await execaCommand(
|
|
1398
|
+
`git commit -m "feat: initialize ${projectName} project with team-cli"`,
|
|
1399
|
+
{ cwd: projectPath, stdio: "pipe" }
|
|
1400
|
+
);
|
|
1401
|
+
logger.success("Git \u4ED3\u5E93\u521D\u59CB\u5316\u5B8C\u6210");
|
|
1402
|
+
} catch (error) {
|
|
1403
|
+
logger.warn(`Git \u521D\u59CB\u5316\u5931\u8D25: ${error.message}`);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// src/commands/breakdown.ts
|
|
1408
|
+
import { Command as Command2 } from "commander";
|
|
1409
|
+
import inquirer2 from "inquirer";
|
|
1410
|
+
import path3 from "path";
|
|
1411
|
+
import { Listr as Listr2 } from "listr2";
|
|
1412
|
+
var breakdownCommand = new Command2("breakdown").argument("[spec-file]", "Spec \u6587\u4EF6\u8DEF\u5F84").description("\u5C06 spec \u62C6\u5206\u4E3A milestones \u548C todos").action(async (specFile) => {
|
|
1413
|
+
try {
|
|
1414
|
+
logger.header("Spec \u62C6\u5206");
|
|
1415
|
+
logger.newLine();
|
|
1416
|
+
const hasTechStack = await FileUtils.exists("TECH_STACK.md");
|
|
1417
|
+
if (!hasTechStack) {
|
|
1418
|
+
logger.error("\u5F53\u524D\u76EE\u5F55\u4E0D\u662F\u4E00\u4E2A\u6709\u6548\u7684 team-cli \u9879\u76EE");
|
|
1419
|
+
logger.info("\u8BF7\u5148\u8FD0\u884C team-cli init \u6216\u5207\u6362\u5230\u9879\u76EE\u76EE\u5F55");
|
|
1420
|
+
process.exit(1);
|
|
1421
|
+
}
|
|
1422
|
+
const hasClaude = await claudeAI.checkInstalled();
|
|
1423
|
+
if (!hasClaude) {
|
|
1424
|
+
logger.error("\u672A\u68C0\u6D4B\u5230 Claude CLI");
|
|
1425
|
+
logger.info("\u8BF7\u5B89\u88C5 Claude CLI: npm install -g @anthropic-ai/claude-code");
|
|
1426
|
+
process.exit(1);
|
|
1427
|
+
}
|
|
1428
|
+
const tasks = new Listr2([
|
|
1429
|
+
{
|
|
1430
|
+
title: "\u626B\u63CF spec \u6587\u4EF6",
|
|
1431
|
+
task: async () => {
|
|
1432
|
+
const specDir = "docs/specs";
|
|
1433
|
+
const exists = await FileUtils.exists(specDir);
|
|
1434
|
+
if (!exists) {
|
|
1435
|
+
throw new Error(`specs \u76EE\u5F55\u4E0D\u5B58\u5728: ${specDir}`);
|
|
1436
|
+
}
|
|
1437
|
+
const files = await FileUtils.findFiles("*.md", specDir);
|
|
1438
|
+
return files.filter((f) => !f.includes("template"));
|
|
1439
|
+
}
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
title: "\u9009\u62E9 spec \u6587\u4EF6",
|
|
1443
|
+
task: async (ctx) => {
|
|
1444
|
+
if (ctx.specs.length === 0) {
|
|
1445
|
+
throw new Error("\u672A\u627E\u5230 spec \u6587\u4EF6");
|
|
1446
|
+
}
|
|
1447
|
+
if (!specFile) {
|
|
1448
|
+
const { selectedFile } = await inquirer2.prompt([
|
|
1449
|
+
{
|
|
1450
|
+
type: "list",
|
|
1451
|
+
name: "selectedFile",
|
|
1452
|
+
message: "\u8BF7\u9009\u62E9\u8981\u62C6\u5206\u7684 spec \u6587\u4EF6:",
|
|
1453
|
+
choices: ctx.specs
|
|
1454
|
+
}
|
|
1455
|
+
]);
|
|
1456
|
+
return path3.join("docs/specs", selectedFile);
|
|
1457
|
+
}
|
|
1458
|
+
const fullPath = specFile.startsWith("docs/specs/") ? specFile : path3.join("docs/specs", specFile);
|
|
1459
|
+
const exists = await FileUtils.exists(fullPath);
|
|
1460
|
+
if (!exists) {
|
|
1461
|
+
throw new Error(`Spec \u6587\u4EF6\u4E0D\u5B58\u5728: ${specFile}`);
|
|
1462
|
+
}
|
|
1463
|
+
return fullPath;
|
|
1464
|
+
}
|
|
1465
|
+
},
|
|
1466
|
+
{
|
|
1467
|
+
title: "\u8BFB\u53D6 spec \u5185\u5BB9",
|
|
1468
|
+
task: async (ctx) => {
|
|
1469
|
+
ctx.specContent = await FileUtils.read(ctx.selectedFile);
|
|
1470
|
+
logger.success(`\u5DF2\u9009\u62E9: ${ctx.selectedFile}`);
|
|
1471
|
+
}
|
|
1472
|
+
},
|
|
1473
|
+
{
|
|
1474
|
+
title: "\u8C03\u7528 Claude \u62C6\u5206 spec",
|
|
1475
|
+
task: async (ctx) => {
|
|
1476
|
+
const prompt = buildBreakdownPrompt(ctx.specContent);
|
|
1477
|
+
logger.newLine();
|
|
1478
|
+
logger.separator("\u2500", 60);
|
|
1479
|
+
logger.info("Claude \u6267\u884C\u4E2D...");
|
|
1480
|
+
logger.separator("\u2500", 60);
|
|
1481
|
+
logger.newLine();
|
|
1482
|
+
const result = await claudeAI.prompt(prompt, {
|
|
1483
|
+
contextFiles: ["TECH_STACK.md", "CONVENTIONS.md"]
|
|
1484
|
+
});
|
|
1485
|
+
logger.newLine();
|
|
1486
|
+
logger.separator("\u2500", 60);
|
|
1487
|
+
logger.newLine();
|
|
1488
|
+
return result;
|
|
1489
|
+
}
|
|
1490
|
+
},
|
|
1491
|
+
{
|
|
1492
|
+
title: "\u66F4\u65B0 spec \u6587\u4EF6",
|
|
1493
|
+
task: async (ctx) => {
|
|
1494
|
+
await FileUtils.write(ctx.selectedFile, ctx.breakdownResult);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
]);
|
|
1498
|
+
try {
|
|
1499
|
+
const ctx = await tasks.run();
|
|
1500
|
+
logger.newLine();
|
|
1501
|
+
logger.header("Spec \u62C6\u5206\u5B8C\u6210!");
|
|
1502
|
+
logger.success(`Milestones \u5DF2\u6DFB\u52A0\u5230: ctx.selectedFile}`);
|
|
1503
|
+
logger.newLine();
|
|
1504
|
+
logger.info("\u4E0B\u4E00\u6B65:");
|
|
1505
|
+
logger.step("1. \u68C0\u67E5\u62C6\u5206\u7ED3\u679C\uFF0C\u6839\u636E\u9700\u8981\u8C03\u6574");
|
|
1506
|
+
logger.step("2. \u8FD0\u884C 'team-cli dev' \u9009\u62E9 milestone \u8FDB\u884C\u5F00\u53D1");
|
|
1507
|
+
logger.newLine();
|
|
1508
|
+
} catch (error) {
|
|
1509
|
+
if (error.message) {
|
|
1510
|
+
logger.error(error.message);
|
|
1511
|
+
}
|
|
1512
|
+
throw error;
|
|
1513
|
+
}
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
logger.error(`\u62C6\u5206\u5931\u8D25: ${error.message}`);
|
|
1516
|
+
if (process.env.DEBUG) {
|
|
1517
|
+
console.error(error);
|
|
1518
|
+
}
|
|
1519
|
+
process.exit(1);
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
function buildBreakdownPrompt(specContent) {
|
|
1523
|
+
return `Role: Senior Technical Lead and Agile Coach
|
|
1524
|
+
|
|
1525
|
+
Task: Break down the following feature spec into milestones and todo lists.
|
|
1526
|
+
|
|
1527
|
+
Context:
|
|
1528
|
+
- Read TECH_STACK.md for technology constraints
|
|
1529
|
+
- Read CONVENTIONS.md for coding standards
|
|
1530
|
+
- Each milestone should be completable in 1-3 days
|
|
1531
|
+
- Each todo should be a concrete, actionable task
|
|
1532
|
+
|
|
1533
|
+
Spec Content:
|
|
1534
|
+
\`\`\`
|
|
1535
|
+
${specContent}
|
|
1536
|
+
\`\`\`
|
|
1537
|
+
|
|
1538
|
+
Output Requirements:
|
|
1539
|
+
1. Parse the existing spec content
|
|
1540
|
+
2. Break it down into 2-5 milestones
|
|
1541
|
+
3. Each milestone should have:
|
|
1542
|
+
- Clear name and objective
|
|
1543
|
+
- Estimated days (1-3 days per milestone)
|
|
1544
|
+
- Todo list with 3-8 actionable items
|
|
1545
|
+
4. Todo items should be:
|
|
1546
|
+
- Concrete and specific
|
|
1547
|
+
- Testable
|
|
1548
|
+
- Independent as much as possible
|
|
1549
|
+
|
|
1550
|
+
Format the milestones section as:
|
|
1551
|
+
|
|
1552
|
+
\`\`\`markdown
|
|
1553
|
+
## \u91CC\u7A0B\u7891 (Milestones)
|
|
1554
|
+
|
|
1555
|
+
### Milestone 1: [\u91CC\u7A0B\u7891\u540D\u79F0]
|
|
1556
|
+
**\u76EE\u6807**: [\u7B80\u77ED\u63CF\u8FF0\u8FD9\u4E2A\u91CC\u7A0B\u7891\u7684\u76EE\u6807]
|
|
1557
|
+
**\u9884\u4F30**: 2 \u5929
|
|
1558
|
+
|
|
1559
|
+
- [ ] Todo 1 - \u5177\u4F53\u53EF\u6267\u884C\u7684\u4EFB\u52A1
|
|
1560
|
+
- [ ] Todo 2 - \u5177\u4F53\u53EF\u6267\u884C\u7684\u4EFB\u52A1
|
|
1561
|
+
- [ ] Todo 3 - \u5177\u4F53\u53EF\u6267\u884C\u7684\u4EFB\u52A1
|
|
1562
|
+
|
|
1563
|
+
### Milestone 2: [\u91CC\u7A0B\u7891\u540D\u79F0]
|
|
1564
|
+
**\u76EE\u6807**: [\u7B80\u77ED\u63CF\u8FF0\u8FD9\u4E2A\u91CC\u7A0B\u7891\u7684\u76EE\u6807]
|
|
1565
|
+
**\u9884\u4F30**: 3 \u5929
|
|
1566
|
+
|
|
1567
|
+
- [ ] Todo 1
|
|
1568
|
+
- [ ] Todo 2
|
|
1569
|
+
- [ ] Todo 3
|
|
1570
|
+
- [ ] Todo 4
|
|
1571
|
+
\`\`\`
|
|
1572
|
+
|
|
1573
|
+
Important Instructions:
|
|
1574
|
+
1. Update the spec file directly with the milestone breakdown
|
|
1575
|
+
2. Keep all existing content, just add/update the milestones section
|
|
1576
|
+
3. If milestones section exists, replace it with new breakdown
|
|
1577
|
+
4. If milestones section doesn't exist, add it after "\u6280\u672F\u8BBE\u8BA1" section
|
|
1578
|
+
5. After updating the file, exit immediately
|
|
1579
|
+
6. Do not ask any questions
|
|
1580
|
+
7. Make sure todos are actionable and can be completed independently
|
|
1581
|
+
8. Consider dependencies when ordering todos within a milestone`;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// src/commands/dev.ts
|
|
1585
|
+
import { Command as Command3 } from "commander";
|
|
1586
|
+
import inquirer3 from "inquirer";
|
|
1587
|
+
import path4 from "path";
|
|
1588
|
+
var devCommand = new Command3("dev").description("\u5F00\u53D1\u6A21\u5F0F\uFF0C\u6267\u884C\u5177\u4F53\u4EFB\u52A1").action(async () => {
|
|
1589
|
+
try {
|
|
1590
|
+
logger.header("\u5F00\u53D1\u6A21\u5F0F");
|
|
1591
|
+
logger.newLine();
|
|
1592
|
+
const hasTechStack = await FileUtils.exists("TECH_STACK.md");
|
|
1593
|
+
if (!hasTechStack) {
|
|
1594
|
+
logger.error("\u5F53\u524D\u76EE\u5F55\u4E0D\u662F\u4E00\u4E2A\u6709\u6548\u7684 team-cli \u9879\u76EE");
|
|
1595
|
+
logger.info("\u8BF7\u5148\u8FD0\u884C 'team-cli init <project-name>' \u6216\u5207\u6362\u5230\u9879\u76EE\u76EE\u5F55");
|
|
1596
|
+
process.exit(1);
|
|
1597
|
+
}
|
|
1598
|
+
logger.success("\u68C0\u6D4B\u5230\u9879\u76EE\u4E0A\u4E0B\u6587");
|
|
1599
|
+
const hasClaude = await claudeAI.checkInstalled();
|
|
1600
|
+
if (!hasClaude) {
|
|
1601
|
+
logger.error("\u672A\u68C0\u6D4B\u5230 Claude CLI");
|
|
1602
|
+
logger.info("\u8BF7\u5B89\u88C5 Claude CLI: npm install -g @anthropic-ai/claude-code");
|
|
1603
|
+
process.exit(1);
|
|
1604
|
+
}
|
|
1605
|
+
const selectedSpec = await selectSpec();
|
|
1606
|
+
const selectedMilestone = await selectMilestone(selectedSpec);
|
|
1607
|
+
const selectedTodo = await selectTodo(selectedSpec, selectedMilestone);
|
|
1608
|
+
await executeDevelopment(selectedSpec, selectedMilestone, selectedTodo);
|
|
1609
|
+
} catch (error) {
|
|
1610
|
+
logger.error(`\u5F00\u53D1\u6A21\u5F0F\u6267\u884C\u5931\u8D25: ${error.message}`);
|
|
1611
|
+
if (process.env.DEBUG) {
|
|
1612
|
+
console.error(error);
|
|
1613
|
+
}
|
|
1614
|
+
process.exit(1);
|
|
1615
|
+
}
|
|
1616
|
+
});
|
|
1617
|
+
async function selectSpec() {
|
|
1618
|
+
logger.step("\u6B65\u9AA4 1/3: \u9009\u62E9 spec \u6587\u4EF6...");
|
|
1619
|
+
logger.newLine();
|
|
1620
|
+
const specDir = "docs/specs";
|
|
1621
|
+
const exists = await FileUtils.exists(specDir);
|
|
1622
|
+
if (!exists) {
|
|
1623
|
+
throw new Error("docs/specs \u76EE\u5F55\u4E0D\u5B58\u5728");
|
|
1624
|
+
}
|
|
1625
|
+
const files = await FileUtils.findFiles("*.md", specDir);
|
|
1626
|
+
const specFiles = files.filter((f) => !f.includes("template"));
|
|
1627
|
+
if (specFiles.length === 0) {
|
|
1628
|
+
throw new Error("\u672A\u627E\u5230 spec \u6587\u4EF6");
|
|
1629
|
+
}
|
|
1630
|
+
const specs = [];
|
|
1631
|
+
for (let i = 0; i < specFiles.length; i++) {
|
|
1632
|
+
const file = path4.join(specDir, specFiles[i]);
|
|
1633
|
+
const spec = await FileUtils.read(file);
|
|
1634
|
+
const status = parseSpecStatus(spec);
|
|
1635
|
+
const dependencies = parseDependencies(spec);
|
|
1636
|
+
specs.push({
|
|
1637
|
+
file,
|
|
1638
|
+
name: specFiles[i],
|
|
1639
|
+
status,
|
|
1640
|
+
dependencies,
|
|
1641
|
+
index: i
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
const sortedSpecs = topologicalSort(specs);
|
|
1645
|
+
logger.info("\u53EF\u7528\u7684 spec \u6587\u4EF6:");
|
|
1646
|
+
logger.newLine();
|
|
1647
|
+
const choices = sortedSpecs.map((spec, idx) => {
|
|
1648
|
+
const statusIcon = getStatusIcon(spec.status);
|
|
1649
|
+
const statusColor = getStatusColor(spec.status);
|
|
1650
|
+
const recommendInfo = idx === 0 ? "[\u63A8\u8350\u4ECE\u8FD9\u5F00\u59CB] " : "";
|
|
1651
|
+
const depInfo = spec.dependencies.length > 0 ? `[\u4F9D\u8D56: ${spec.dependencies.join(", ")}] ` : "";
|
|
1652
|
+
return {
|
|
1653
|
+
name: `${statusIcon} [${spec.status}] ${recommendInfo}${depInfo}${spec.name}`,
|
|
1654
|
+
value: spec.file,
|
|
1655
|
+
short: spec.name
|
|
1656
|
+
};
|
|
1657
|
+
});
|
|
1658
|
+
logger.info(" \u2713 = \u5DF2\u5B8C\u6210 \u27F3 = \u8FDB\u884C\u4E2D \u25C9 = \u5DF2\u62C6\u5206 \u25CB = \u672A\u5F00\u59CB");
|
|
1659
|
+
logger.info(" \u6839\u636E\u4F9D\u8D56\u5173\u7CFB\u63A8\u8350\u7684\u5F00\u53D1\u987A\u5E8F\u5DF2\u6807\u6CE8");
|
|
1660
|
+
logger.newLine();
|
|
1661
|
+
const { selectedFile } = await inquirer3.prompt([
|
|
1662
|
+
{
|
|
1663
|
+
type: "list",
|
|
1664
|
+
name: "selectedFile",
|
|
1665
|
+
message: "\u9009\u62E9\u8981\u5F00\u53D1\u7684 spec:",
|
|
1666
|
+
choices
|
|
1667
|
+
}
|
|
1668
|
+
]);
|
|
1669
|
+
logger.success(`\u5DF2\u9009\u62E9: ${selectedFile}`);
|
|
1670
|
+
return selectedFile;
|
|
1671
|
+
}
|
|
1672
|
+
async function selectMilestone(specFile) {
|
|
1673
|
+
logger.newLine();
|
|
1674
|
+
logger.step("\u6B65\u9AA4 2/3: \u89E3\u6790 milestones...");
|
|
1675
|
+
logger.newLine();
|
|
1676
|
+
const specContent = await FileUtils.read(specFile);
|
|
1677
|
+
const milestones = parseMilestones(specContent);
|
|
1678
|
+
if (milestones.length === 0) {
|
|
1679
|
+
logger.info("\u8BE5 spec \u5C1A\u672A\u62C6\u5206 milestones");
|
|
1680
|
+
const { breakdownNow } = await inquirer3.prompt([
|
|
1681
|
+
{
|
|
1682
|
+
type: "confirm",
|
|
1683
|
+
name: "breakdownNow",
|
|
1684
|
+
message: "\u662F\u5426\u73B0\u5728\u62C6\u5206?",
|
|
1685
|
+
default: true
|
|
1686
|
+
}
|
|
1687
|
+
]);
|
|
1688
|
+
if (breakdownNow) {
|
|
1689
|
+
logger.info("\u6B63\u5728\u8C03\u7528 breakdown...");
|
|
1690
|
+
throw new Error("breakdown \u547D\u4EE4\u9700\u8981\u8FDB\u4E00\u6B65\u5B9E\u73B0");
|
|
1691
|
+
} else {
|
|
1692
|
+
logger.info("\u5C06\u76F4\u63A5\u5B9E\u73B0\u6574\u4E2A spec");
|
|
1693
|
+
return "\u6574\u4E2A spec";
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
const choices = milestones.map((m, idx) => ({
|
|
1697
|
+
name: `${idx + 1}. ${m.title} (${m.todos.length} \u4E2A\u4EFB\u52A1)`,
|
|
1698
|
+
value: m.title,
|
|
1699
|
+
short: m.title
|
|
1700
|
+
}));
|
|
1701
|
+
choices.push({
|
|
1702
|
+
name: `${milestones.length + 1}. \u6574\u4E2A spec (\u5168\u90E8 milestones)`,
|
|
1703
|
+
value: "\u6574\u4E2A spec",
|
|
1704
|
+
short: "\u6574\u4E2A spec"
|
|
1705
|
+
});
|
|
1706
|
+
const { milestone } = await inquirer3.prompt([
|
|
1707
|
+
{
|
|
1708
|
+
type: "list",
|
|
1709
|
+
name: "milestone",
|
|
1710
|
+
message: "\u9009\u62E9 milestone:",
|
|
1711
|
+
choices
|
|
1712
|
+
}
|
|
1713
|
+
]);
|
|
1714
|
+
return milestone;
|
|
1715
|
+
}
|
|
1716
|
+
async function selectTodo(specFile, milestone) {
|
|
1717
|
+
if (milestone === "\u6574\u4E2A spec") {
|
|
1718
|
+
return "\u5168\u90E8\u529F\u80FD";
|
|
1719
|
+
}
|
|
1720
|
+
logger.newLine();
|
|
1721
|
+
logger.step("\u6B65\u9AA4 3/3: \u9009\u62E9 todo \u4EFB\u52A1...");
|
|
1722
|
+
logger.newLine();
|
|
1723
|
+
const specContent = await FileUtils.read(specFile);
|
|
1724
|
+
const todos = parseTodos(specContent, milestone);
|
|
1725
|
+
if (todos.length === 0) {
|
|
1726
|
+
logger.warn("\u8BE5 milestone \u6CA1\u6709 todo \u4EFB\u52A1");
|
|
1727
|
+
const { implementAll } = await inquirer3.prompt([
|
|
1728
|
+
{
|
|
1729
|
+
type: "confirm",
|
|
1730
|
+
name: "implementAll",
|
|
1731
|
+
message: "\u5B9E\u73B0\u6574\u4E2A milestone?",
|
|
1732
|
+
default: true
|
|
1733
|
+
}
|
|
1734
|
+
]);
|
|
1735
|
+
return implementAll ? "\u5168\u90E8\u529F\u80FD" : milestone;
|
|
1736
|
+
}
|
|
1737
|
+
const choices = todos.map((todo2, idx) => ({
|
|
1738
|
+
name: `${idx + 1}. ${todo2}`,
|
|
1739
|
+
value: todo2,
|
|
1740
|
+
short: todo2
|
|
1741
|
+
}));
|
|
1742
|
+
choices.push({
|
|
1743
|
+
name: `${todos.length + 1}. \u5168\u90E8\u4EFB\u52A1 (\u6574\u4E2A milestone)`,
|
|
1744
|
+
value: "\u5168\u90E8\u4EFB\u52A1",
|
|
1745
|
+
short: "\u5168\u90E8\u4EFB\u52A1"
|
|
1746
|
+
});
|
|
1747
|
+
const { todo } = await inquirer3.prompt([
|
|
1748
|
+
{
|
|
1749
|
+
type: "list",
|
|
1750
|
+
name: "todo",
|
|
1751
|
+
message: "\u9009\u62E9 todo \u4EFB\u52A1:",
|
|
1752
|
+
choices
|
|
1753
|
+
}
|
|
1754
|
+
]);
|
|
1755
|
+
return todo;
|
|
1756
|
+
}
|
|
1757
|
+
async function executeDevelopment(specFile, milestone, todo) {
|
|
1758
|
+
logger.newLine();
|
|
1759
|
+
logger.step("\u6784\u5EFA Prompt \u5E76\u8C03\u7528 Claude...");
|
|
1760
|
+
let taskDescription;
|
|
1761
|
+
if (milestone === "\u6574\u4E2A spec") {
|
|
1762
|
+
taskDescription = "\u5B9E\u73B0\u6574\u4E2A spec \u7684\u6240\u6709\u529F\u80FD";
|
|
1763
|
+
} else if (todo === "\u5168\u90E8\u529F\u80FD" || todo === "\u5168\u90E8\u4EFB\u52A1") {
|
|
1764
|
+
taskDescription = `\u5B9E\u73B0 milestone '${milestone}' \u7684\u6240\u6709\u4EFB\u52A1`;
|
|
1765
|
+
} else {
|
|
1766
|
+
taskDescription = `\u5B9E\u73B0 milestone '${milestone}' \u7684\u4EFB\u52A1: ${todo}`;
|
|
1767
|
+
}
|
|
1768
|
+
const prompt = buildDevPrompt(specFile, milestone, todo, taskDescription);
|
|
1769
|
+
logger.newLine();
|
|
1770
|
+
logger.separator("\u2500", 60);
|
|
1771
|
+
logger.info("Claude \u6267\u884C\u4E2D...");
|
|
1772
|
+
logger.separator("\u2500", 60);
|
|
1773
|
+
logger.newLine();
|
|
1774
|
+
logger.info(` \u4EFB\u52A1\u63CF\u8FF0: ${taskDescription}`);
|
|
1775
|
+
logger.info(` Spec \u6587\u4EF6: ${specFile}`);
|
|
1776
|
+
logger.newLine();
|
|
1777
|
+
const result = await claudeAI.prompt(prompt, {
|
|
1778
|
+
contextFiles: ["TECH_STACK.md", "CONVENTIONS.md", "AI_MEMORY.md", specFile]
|
|
1779
|
+
});
|
|
1780
|
+
logger.newLine();
|
|
1781
|
+
logger.separator("\u2500", 60);
|
|
1782
|
+
logger.newLine();
|
|
1783
|
+
await generateSessionLog(specFile, milestone, todo, taskDescription, result);
|
|
1784
|
+
logger.header("\u5F00\u53D1\u4EFB\u52A1\u5B8C\u6210!");
|
|
1785
|
+
logger.success("\u4F1A\u8BDD\u65E5\u5FD7\u5DF2\u4FDD\u5B58");
|
|
1786
|
+
logger.newLine();
|
|
1787
|
+
logger.info("\u4E0B\u4E00\u6B65:");
|
|
1788
|
+
logger.step("1. \u68C0\u67E5\u751F\u6210\u7684\u4EE3\u7801");
|
|
1789
|
+
logger.step("2. \u8FD0\u884C 'team-cli dev' \u7EE7\u7EED\u4E0B\u4E00\u4E2A\u4EFB\u52A1");
|
|
1790
|
+
logger.newLine();
|
|
1791
|
+
}
|
|
1792
|
+
function parseSpecStatus(spec) {
|
|
1793
|
+
const statusMatch = spec.match(/状态.*[::]\s*(.+)/);
|
|
1794
|
+
if (statusMatch) {
|
|
1795
|
+
const status = statusMatch[1].replace(/\*\*/g, "").trim();
|
|
1796
|
+
if (status.includes("\u5DF2\u5B8C\u6210")) return "\u5DF2\u5B8C\u6210";
|
|
1797
|
+
if (status.includes("\u8FDB\u884C\u4E2D")) return "\u8FDB\u884C\u4E2D";
|
|
1798
|
+
if (status.includes("\u5DF2\u62C6\u5206")) return "\u5DF2\u62C6\u5206";
|
|
1799
|
+
}
|
|
1800
|
+
return "\u672A\u5F00\u59CB";
|
|
1801
|
+
}
|
|
1802
|
+
function parseDependencies(spec) {
|
|
1803
|
+
const deps = [];
|
|
1804
|
+
let inDepsSection = false;
|
|
1805
|
+
const lines = spec.split("\n");
|
|
1806
|
+
for (const line of lines) {
|
|
1807
|
+
if (line.includes("## \u4F9D\u8D56\u5173\u7CFB") || line.includes("## \u4F9D\u8D56")) {
|
|
1808
|
+
inDepsSection = true;
|
|
1809
|
+
continue;
|
|
1810
|
+
}
|
|
1811
|
+
if (inDepsSection && line.startsWith("## ")) {
|
|
1812
|
+
break;
|
|
1813
|
+
}
|
|
1814
|
+
const match = line.match(/^-\s+\[[ x ]\]\s*(.+\.md)/);
|
|
1815
|
+
if (inDepsSection && match) {
|
|
1816
|
+
const dep = match[1];
|
|
1817
|
+
if (dep !== "\u65E0" && dep) {
|
|
1818
|
+
deps.push(dep);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
return deps;
|
|
1823
|
+
}
|
|
1824
|
+
function parseMilestones(spec) {
|
|
1825
|
+
const milestones = [];
|
|
1826
|
+
const lines = spec.split("\n");
|
|
1827
|
+
let currentMilestone = null;
|
|
1828
|
+
let inMilestone = false;
|
|
1829
|
+
for (const line of lines) {
|
|
1830
|
+
if (line.match(/^###\s+Milestone\s+\d+:/)) {
|
|
1831
|
+
if (currentMilestone) {
|
|
1832
|
+
milestones.push(currentMilestone);
|
|
1833
|
+
}
|
|
1834
|
+
const title = line.replace(/^###\s+/, "").trim();
|
|
1835
|
+
currentMilestone = { title, todos: [] };
|
|
1836
|
+
inMilestone = true;
|
|
1837
|
+
continue;
|
|
1838
|
+
}
|
|
1839
|
+
if (inMilestone && currentMilestone) {
|
|
1840
|
+
if (line.match(/^###\s+Milestone/)) {
|
|
1841
|
+
milestones.push(currentMilestone);
|
|
1842
|
+
currentMilestone = null;
|
|
1843
|
+
continue;
|
|
1844
|
+
}
|
|
1845
|
+
const todoMatch = line.match(/^-\s+\[[ x ]\]\s*(.+)/);
|
|
1846
|
+
if (todoMatch) {
|
|
1847
|
+
currentMilestone.todos.push(todoMatch[1].trim());
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
if (currentMilestone) {
|
|
1852
|
+
milestones.push(currentMilestone);
|
|
1853
|
+
}
|
|
1854
|
+
return milestones;
|
|
1855
|
+
}
|
|
1856
|
+
function parseTodos(spec, milestoneTitle) {
|
|
1857
|
+
const lines = spec.split("\n");
|
|
1858
|
+
const todos = [];
|
|
1859
|
+
let inTargetMilestone = false;
|
|
1860
|
+
for (const line of lines) {
|
|
1861
|
+
if (line.includes(milestoneTitle)) {
|
|
1862
|
+
inTargetMilestone = true;
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1865
|
+
if (inTargetMilestone) {
|
|
1866
|
+
if (line.match(/^###\s+Milestone/)) {
|
|
1867
|
+
break;
|
|
1868
|
+
}
|
|
1869
|
+
const todoMatch = line.match(/^-\s+\[[ x ]\]\s*(.+)/);
|
|
1870
|
+
if (todoMatch) {
|
|
1871
|
+
todos.push(todoMatch[1].trim());
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
return todos;
|
|
1876
|
+
}
|
|
1877
|
+
function topologicalSort(specs) {
|
|
1878
|
+
const sorted = [];
|
|
1879
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1880
|
+
function visit(spec) {
|
|
1881
|
+
if (visited.has(spec.index)) return;
|
|
1882
|
+
visited.add(spec.index);
|
|
1883
|
+
for (const depName of spec.dependencies) {
|
|
1884
|
+
const dep = specs.find((s) => s.name === depName);
|
|
1885
|
+
if (dep) {
|
|
1886
|
+
visit(dep);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
sorted.push(spec);
|
|
1890
|
+
}
|
|
1891
|
+
for (const spec of specs) {
|
|
1892
|
+
visit(spec);
|
|
1893
|
+
}
|
|
1894
|
+
return sorted;
|
|
1895
|
+
}
|
|
1896
|
+
function getStatusIcon(status) {
|
|
1897
|
+
switch (status) {
|
|
1898
|
+
case "\u5DF2\u5B8C\u6210":
|
|
1899
|
+
return "\u2713";
|
|
1900
|
+
case "\u8FDB\u884C\u4E2D":
|
|
1901
|
+
return "\u27F3";
|
|
1902
|
+
case "\u5DF2\u62C6\u5206":
|
|
1903
|
+
return "\u25C9";
|
|
1904
|
+
default:
|
|
1905
|
+
return "\u25CB";
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
function getStatusColor(status) {
|
|
1909
|
+
return status;
|
|
1910
|
+
}
|
|
1911
|
+
function buildDevPrompt(specFile, milestone, todo, taskDescription) {
|
|
1912
|
+
return `Role: Senior Fullstack Developer
|
|
1913
|
+
|
|
1914
|
+
Context:
|
|
1915
|
+
- Read and follow TECH_STACK.md for technology choices
|
|
1916
|
+
- Read and follow CONVENTIONS.md for coding standards
|
|
1917
|
+
- Check AI_MEMORY.md for project context and history
|
|
1918
|
+
|
|
1919
|
+
Task: ${taskDescription}
|
|
1920
|
+
|
|
1921
|
+
Spec File: ${specFile}
|
|
1922
|
+
Milestone: ${milestone}
|
|
1923
|
+
Todo: ${todo}
|
|
1924
|
+
|
|
1925
|
+
Process:
|
|
1926
|
+
1. First, read and analyze the full spec file
|
|
1927
|
+
2. Focus on the selected milestone/todo
|
|
1928
|
+
3. Define API endpoints and data models if needed
|
|
1929
|
+
4. Write tests first (TDD approach)
|
|
1930
|
+
5. Implement the backend code in ./backend
|
|
1931
|
+
6. Implement the frontend integration in ./frontend
|
|
1932
|
+
7. Update AI_MEMORY.md with completed tasks
|
|
1933
|
+
8. Update the spec file to mark completed todos (if applicable)
|
|
1934
|
+
|
|
1935
|
+
Important:
|
|
1936
|
+
- Follow the tech stack exactly (Java 17, Spring Boot 3, MyBatis Plus, Next.js 14, TypeScript)
|
|
1937
|
+
- Use DTO pattern for all API requests/responses
|
|
1938
|
+
- Write clean, well-documented code
|
|
1939
|
+
- After completing the task, exit immediately without waiting for further input
|
|
1940
|
+
`;
|
|
1941
|
+
}
|
|
1942
|
+
async function generateSessionLog(specFile, milestone, todo, taskDescription, result) {
|
|
1943
|
+
const sessionDir = "docs/sessions";
|
|
1944
|
+
await FileUtils.ensureDir(sessionDir);
|
|
1945
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1946
|
+
const specName = path4.basename(specFile, ".md");
|
|
1947
|
+
const logFile = path4.join(sessionDir, `${timestamp}_${specName}.md`);
|
|
1948
|
+
const content = `# \u5F00\u53D1\u4F1A\u8BDD\u8BB0\u5F55
|
|
1949
|
+
|
|
1950
|
+
**\u65F6\u95F4**: ${(/* @__PURE__ */ new Date()).toLocaleString("zh-CN")}
|
|
1951
|
+
**Spec**: ${specFile}
|
|
1952
|
+
**Milestone**: ${milestone}
|
|
1953
|
+
**Todo**: ${todo}
|
|
1954
|
+
|
|
1955
|
+
## \u4EFB\u52A1\u63CF\u8FF0
|
|
1956
|
+
|
|
1957
|
+
${taskDescription}
|
|
1958
|
+
|
|
1959
|
+
## \u6267\u884C\u7ED3\u679C
|
|
1960
|
+
|
|
1961
|
+
\`\`\`
|
|
1962
|
+
${result}
|
|
1963
|
+
\`\`\`
|
|
1964
|
+
|
|
1965
|
+
## \u751F\u6210\u7684\u6587\u4EF6
|
|
1966
|
+
|
|
1967
|
+
<!-- TODO: \u5217\u51FA\u751F\u6210\u7684\u6587\u4EF6 -->
|
|
1968
|
+
|
|
1969
|
+
## \u4E0B\u4E00\u6B65
|
|
1970
|
+
|
|
1971
|
+
- [ ] \u6D4B\u8BD5\u751F\u6210\u7684\u4EE3\u7801
|
|
1972
|
+
- [ ] \u66F4\u65B0 spec \u6587\u4EF6\u4E2D\u7684 todo \u72B6\u6001
|
|
1973
|
+
- [ ] \u7EE7\u7EED\u4E0B\u4E00\u4E2A\u4EFB\u52A1
|
|
1974
|
+
`;
|
|
1975
|
+
await FileUtils.write(logFile, content);
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// src/commands/add-feature.ts
|
|
1979
|
+
import { Command as Command4 } from "commander";
|
|
1980
|
+
import inquirer4 from "inquirer";
|
|
1981
|
+
import path5 from "path";
|
|
1982
|
+
import { Listr as Listr3 } from "listr2";
|
|
1983
|
+
var addFeatureCommand = new Command4("add-feature").argument("<feature-name>", "\u529F\u80FD\u540D\u79F0").description("\u6DFB\u52A0\u65B0\u529F\u80FD\uFF08\u652F\u6301 PRD \u6216\u7B80\u5355\u63CF\u8FF0\u6A21\u5F0F\uFF09").action(async (featureName) => {
|
|
1984
|
+
try {
|
|
1985
|
+
logger.header("\u6DFB\u52A0\u65B0\u529F\u80FD");
|
|
1986
|
+
logger.newLine();
|
|
1987
|
+
const hasTechStack = await FileUtils.exists("TECH_STACK.md");
|
|
1988
|
+
if (!hasTechStack) {
|
|
1989
|
+
logger.error("\u5F53\u524D\u76EE\u5F55\u4E0D\u662F\u4E00\u4E2A\u6709\u6548\u7684 team-cli \u9879\u76EE");
|
|
1990
|
+
logger.info("\u8BF7\u5148\u8FD0\u884C team-cli init \u6216\u5207\u6362\u5230\u9879\u76EE\u76EE\u5F55");
|
|
1991
|
+
process.exit(1);
|
|
1992
|
+
}
|
|
1993
|
+
const hasClaude = await claudeAI.checkInstalled();
|
|
1994
|
+
if (!hasClaude) {
|
|
1995
|
+
logger.error("\u672A\u68C0\u6D4B\u5230 Claude CLI");
|
|
1996
|
+
logger.info("\u8BF7\u5B89\u88C5 Claude CLI: npm install -g @anthropic-ai/claude-code");
|
|
1997
|
+
process.exit(1);
|
|
1998
|
+
}
|
|
1999
|
+
const featureSlug = StringUtils2.toKebabCase(featureName);
|
|
2000
|
+
const specFile = path5.join("docs/specs", `${featureSlug}.md`);
|
|
2001
|
+
const specExists = await FileUtils.exists(specFile);
|
|
2002
|
+
if (specExists) {
|
|
2003
|
+
logger.error(`Spec \u6587\u4EF6\u5DF2\u5B58\u5728: ${specFile}`);
|
|
2004
|
+
logger.info("\u5982\u9700\u91CD\u65B0\u751F\u6210\uFF0C\u8BF7\u5148\u5220\u9664\uFF1A");
|
|
2005
|
+
logger.info(` rm ${specFile}`);
|
|
2006
|
+
process.exit(1);
|
|
2007
|
+
}
|
|
2008
|
+
const { mode } = await inquirer4.prompt([
|
|
2009
|
+
{
|
|
2010
|
+
type: "list",
|
|
2011
|
+
name: "mode",
|
|
2012
|
+
message: "\u9009\u62E9\u9700\u6C42\u8F93\u5165\u6A21\u5F0F:",
|
|
2013
|
+
choices: [
|
|
2014
|
+
{ name: "PRD \u6587\u6863\u6A21\u5F0F (\u5DF2\u6709\u8BE6\u7EC6 PRD \u6587\u6863)", value: "prd" },
|
|
2015
|
+
{ name: "\u7B80\u5355\u63CF\u8FF0\u6A21\u5F0F (\u4E00\u53E5\u8BDD\u9700\u6C42 + \u6A21\u5757\u4F9D\u8D56)", value: "simple" }
|
|
2016
|
+
]
|
|
2017
|
+
}
|
|
2018
|
+
]);
|
|
2019
|
+
if (mode === "prd") {
|
|
2020
|
+
await addFeatureFromPrd(featureName, featureSlug, specFile);
|
|
2021
|
+
} else {
|
|
2022
|
+
await addFeatureSimple(featureName, featureSlug, specFile);
|
|
2023
|
+
}
|
|
2024
|
+
} catch (error) {
|
|
2025
|
+
logger.error(`\u6DFB\u52A0\u529F\u80FD\u5931\u8D25: ${error.message}`);
|
|
2026
|
+
if (process.env.DEBUG) {
|
|
2027
|
+
console.error(error);
|
|
2028
|
+
}
|
|
2029
|
+
process.exit(1);
|
|
2030
|
+
}
|
|
2031
|
+
});
|
|
2032
|
+
async function addFeatureFromPrd(featureName, featureSlug, specFile) {
|
|
2033
|
+
const { prdPath } = await inquirer4.prompt([
|
|
2034
|
+
{
|
|
2035
|
+
type: "input",
|
|
2036
|
+
name: "prdPath",
|
|
2037
|
+
message: "\u8BF7\u8F93\u5165 PRD \u6587\u6863\u8DEF\u5F84:",
|
|
2038
|
+
validate: async (input) => {
|
|
2039
|
+
const exists = await FileUtils.exists(input);
|
|
2040
|
+
return exists || "PRD \u6587\u6863\u4E0D\u5B58\u5728";
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
]);
|
|
2044
|
+
const tasks = new Listr3([
|
|
2045
|
+
{
|
|
2046
|
+
title: "\u8BFB\u53D6 PRD \u6587\u6863",
|
|
2047
|
+
task: async (ctx2) => {
|
|
2048
|
+
ctx2.prdContent = await FileUtils.read(prdPath);
|
|
2049
|
+
}
|
|
2050
|
+
},
|
|
2051
|
+
{
|
|
2052
|
+
title: "\u626B\u63CF\u5DF2\u5B8C\u6210\u529F\u80FD",
|
|
2053
|
+
task: async (ctx2) => {
|
|
2054
|
+
const specDir = "docs/specs";
|
|
2055
|
+
const files = await FileUtils.findFiles("*.md", specDir);
|
|
2056
|
+
const specs = files.filter((f) => !f.includes("template"));
|
|
2057
|
+
ctx2.completedSpecs = [];
|
|
2058
|
+
for (const file of specs) {
|
|
2059
|
+
const status = await SpecUtils.getSpecStatus(path5.join(specDir, file));
|
|
2060
|
+
if (status === "\u5DF2\u5B8C\u6210") {
|
|
2061
|
+
ctx2.completedSpecs.push(file.replace(".md", ""));
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
},
|
|
2066
|
+
{
|
|
2067
|
+
title: "\u6784\u5EFA\u9879\u76EE\u4E0A\u4E0B\u6587",
|
|
2068
|
+
task: async (ctx2) => {
|
|
2069
|
+
ctx2.projectContext = await buildProjectContext();
|
|
2070
|
+
}
|
|
2071
|
+
},
|
|
2072
|
+
{
|
|
2073
|
+
title: "\u8C03\u7528 Claude \u751F\u6210 spec",
|
|
2074
|
+
task: async (ctx2) => {
|
|
2075
|
+
const prompt = buildPrdPrompt(
|
|
2076
|
+
featureName,
|
|
2077
|
+
ctx2.prdContent,
|
|
2078
|
+
ctx2.projectContext,
|
|
2079
|
+
ctx2.completedSpecs
|
|
2080
|
+
);
|
|
2081
|
+
logger.newLine();
|
|
2082
|
+
logger.separator("\u2500", 60);
|
|
2083
|
+
logger.info("Claude \u751F\u6210 spec \u4E2D...");
|
|
2084
|
+
logger.separator("\u2500", 60);
|
|
2085
|
+
logger.newLine();
|
|
2086
|
+
const result = await claudeAI.prompt(prompt, {
|
|
2087
|
+
contextFiles: ["TECH_STACK.md", "CONVENTIONS.md", "AI_MEMORY.md"]
|
|
2088
|
+
});
|
|
2089
|
+
logger.newLine();
|
|
2090
|
+
logger.separator("\u2500", 60);
|
|
2091
|
+
logger.newLine();
|
|
2092
|
+
return result;
|
|
2093
|
+
}
|
|
2094
|
+
},
|
|
2095
|
+
{
|
|
2096
|
+
title: "\u4FDD\u5B58 spec \u6587\u4EF6",
|
|
2097
|
+
task: async (ctx2) => {
|
|
2098
|
+
await FileUtils.write(specFile, ctx2.generatedSpec);
|
|
2099
|
+
}
|
|
2100
|
+
},
|
|
2101
|
+
{
|
|
2102
|
+
title: "\u66F4\u65B0 AI_MEMORY",
|
|
2103
|
+
task: async () => {
|
|
2104
|
+
await updateAiMemory(featureName, featureSlug);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
]);
|
|
2108
|
+
const ctx = await tasks.run();
|
|
2109
|
+
logger.success(`Spec \u6587\u4EF6\u5DF2\u751F\u6210: ${specFile}`);
|
|
2110
|
+
await showSpecPreview(specFile);
|
|
2111
|
+
await askToAdjust(specFile);
|
|
2112
|
+
}
|
|
2113
|
+
async function addFeatureSimple(featureName, featureSlug, specFile) {
|
|
2114
|
+
const { description } = await inquirer4.prompt([
|
|
2115
|
+
{
|
|
2116
|
+
type: "input",
|
|
2117
|
+
name: "description",
|
|
2118
|
+
message: "\u529F\u80FD\u63CF\u8FF0 (\u4E00\u53E5\u8BDD\u8BF4\u660E):",
|
|
2119
|
+
validate: (input) => input.trim().length > 0 || "\u63CF\u8FF0\u4E0D\u80FD\u4E3A\u7A7A"
|
|
2120
|
+
}
|
|
2121
|
+
]);
|
|
2122
|
+
const tasks = new Listr3([
|
|
2123
|
+
{
|
|
2124
|
+
title: "\u626B\u63CF\u5DF2\u5B8C\u6210\u529F\u80FD",
|
|
2125
|
+
task: async (ctx2) => {
|
|
2126
|
+
const specDir = "docs/specs";
|
|
2127
|
+
const files = await FileUtils.findFiles("*.md", specDir);
|
|
2128
|
+
const specs = files.filter((f) => !f.includes("template"));
|
|
2129
|
+
ctx2.completedSpecs = [];
|
|
2130
|
+
for (const file of specs) {
|
|
2131
|
+
const status = await SpecUtils.getSpecStatus(path5.join(specDir, file));
|
|
2132
|
+
if (status === "\u5DF2\u5B8C\u6210") {
|
|
2133
|
+
ctx2.completedSpecs.push(file.replace(".md", ""));
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
},
|
|
2138
|
+
{
|
|
2139
|
+
title: "\u9009\u62E9\u4F9D\u8D56\u529F\u80FD",
|
|
2140
|
+
task: async (ctx2) => {
|
|
2141
|
+
if (ctx2.completedSpecs.length === 0) {
|
|
2142
|
+
ctx2.selectedDeps = [];
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
const { dependencies } = await inquirer4.prompt([
|
|
2146
|
+
{
|
|
2147
|
+
type: "checkbox",
|
|
2148
|
+
name: "dependencies",
|
|
2149
|
+
message: "\u9009\u62E9\u6B64\u529F\u80FD\u4F9D\u8D56\u7684\u5DF2\u6709\u529F\u80FD (\u53EF\u591A\u9009\uFF0C\u76F4\u63A5\u56DE\u8F66\u8DF3\u8FC7):",
|
|
2150
|
+
choices: ctx2.completedSpecs
|
|
2151
|
+
}
|
|
2152
|
+
]);
|
|
2153
|
+
ctx2.selectedDeps = dependencies;
|
|
2154
|
+
}
|
|
2155
|
+
},
|
|
2156
|
+
{
|
|
2157
|
+
title: "\u6784\u5EFA\u9879\u76EE\u4E0A\u4E0B\u6587",
|
|
2158
|
+
task: async (ctx2) => {
|
|
2159
|
+
ctx2.projectContext = await buildProjectContext();
|
|
2160
|
+
}
|
|
2161
|
+
},
|
|
2162
|
+
{
|
|
2163
|
+
title: "\u8C03\u7528 Claude \u751F\u6210 spec",
|
|
2164
|
+
task: async (ctx2) => {
|
|
2165
|
+
const prompt = buildSimplePrompt(
|
|
2166
|
+
featureName,
|
|
2167
|
+
description,
|
|
2168
|
+
ctx2.projectContext,
|
|
2169
|
+
ctx2.selectedDeps
|
|
2170
|
+
);
|
|
2171
|
+
logger.newLine();
|
|
2172
|
+
logger.separator("\u2500", 60);
|
|
2173
|
+
logger.info("Claude \u751F\u6210 spec \u4E2D...");
|
|
2174
|
+
logger.separator("\u2500", 60);
|
|
2175
|
+
logger.newLine();
|
|
2176
|
+
const result = await claudeAI.prompt(prompt, {
|
|
2177
|
+
contextFiles: ["TECH_STACK.md", "CONVENTIONS.md", "AI_MEMORY.md"]
|
|
2178
|
+
});
|
|
2179
|
+
logger.newLine();
|
|
2180
|
+
logger.separator("\u2500", 60);
|
|
2181
|
+
logger.newLine();
|
|
2182
|
+
return result;
|
|
2183
|
+
}
|
|
2184
|
+
},
|
|
2185
|
+
{
|
|
2186
|
+
title: "\u4FDD\u5B58 spec \u6587\u4EF6",
|
|
2187
|
+
task: async (ctx2) => {
|
|
2188
|
+
await FileUtils.write(specFile, ctx2.generatedSpec);
|
|
2189
|
+
}
|
|
2190
|
+
},
|
|
2191
|
+
{
|
|
2192
|
+
title: "\u66F4\u65B0 AI_MEMORY",
|
|
2193
|
+
task: async () => {
|
|
2194
|
+
await updateAiMemory(featureName, featureSlug);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
]);
|
|
2198
|
+
const ctx = await tasks.run();
|
|
2199
|
+
logger.success(`Spec \u6587\u4EF6\u5DF2\u751F\u6210: ${specFile}`);
|
|
2200
|
+
await showSpecPreview(specFile);
|
|
2201
|
+
await askToAdjust(specFile);
|
|
2202
|
+
}
|
|
2203
|
+
async function buildProjectContext() {
|
|
2204
|
+
const context = [];
|
|
2205
|
+
if (await FileUtils.exists("backend/src/main/java")) {
|
|
2206
|
+
context.push("### \u540E\u7AEF\u7ED3\u6784");
|
|
2207
|
+
const backendDirs = await FileUtils.findFiles("*/", "backend/src/main/java");
|
|
2208
|
+
const limited = backendDirs.slice(0, 10);
|
|
2209
|
+
limited.forEach((dir) => {
|
|
2210
|
+
context.push(` - backend/src/main/java/${dir}`);
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
if (await FileUtils.exists("frontend/src")) {
|
|
2214
|
+
context.push("");
|
|
2215
|
+
context.push("### \u524D\u7AEF\u7ED3\u6784");
|
|
2216
|
+
const frontendDirs = await FileUtils.findFiles("*/", "frontend/src");
|
|
2217
|
+
const limited = frontendDirs.slice(0, 10);
|
|
2218
|
+
limited.forEach((dir) => {
|
|
2219
|
+
context.push(` - frontend/src/${dir}`);
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
if (await FileUtils.exists("docs/specs")) {
|
|
2223
|
+
context.push("");
|
|
2224
|
+
context.push("### \u5DF2\u6709\u529F\u80FD");
|
|
2225
|
+
const files = await FileUtils.findFiles("*.md", "docs/specs");
|
|
2226
|
+
const specs = files.filter((f) => !f.includes("template"));
|
|
2227
|
+
for (const file of specs) {
|
|
2228
|
+
const status = await SpecUtils.getSpecStatus(path5.join("docs/specs", file));
|
|
2229
|
+
context.push(` - ${file.replace(".md", "")} [${status}]`);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
return context.join("\n");
|
|
2233
|
+
}
|
|
2234
|
+
function buildPrdPrompt(featureName, prdContent, projectContext, completedSpecs) {
|
|
2235
|
+
const deps = completedSpecs.length > 0 ? completedSpecs.join("\u3001") : "(\u65E0)";
|
|
2236
|
+
return `Role: Senior Fullstack Developer
|
|
2237
|
+
|
|
2238
|
+
\u4F60\u73B0\u5728\u9700\u8981\u6839\u636E PRD \u6587\u6863\u751F\u6210\u4E00\u4E2A\u529F\u80FD\u89C4\u683C\uFF08Spec\uFF09\u6587\u6863\u3002
|
|
2239
|
+
|
|
2240
|
+
## \u9879\u76EE\u6280\u672F\u6808
|
|
2241
|
+
\u540E\u7AEF: Java 17 + Spring Boot 3 + MyBatis Plus + MySQL 8.0
|
|
2242
|
+
\u524D\u7AEF: Next.js 14 (App Router) + TypeScript + Tailwind CSS
|
|
2243
|
+
|
|
2244
|
+
## \u9879\u76EE\u5F53\u524D\u72B6\u6001
|
|
2245
|
+
${projectContext}
|
|
2246
|
+
|
|
2247
|
+
## \u529F\u80FD\u540D\u79F0
|
|
2248
|
+
${featureName}
|
|
2249
|
+
|
|
2250
|
+
## PRD \u6587\u6863\u5185\u5BB9
|
|
2251
|
+
${prdContent}
|
|
2252
|
+
|
|
2253
|
+
## \u4F9D\u8D56\u529F\u80FD
|
|
2254
|
+
\u6B64\u529F\u80FD\u53EF\u80FD\u4F9D\u8D56\u4EE5\u4E0B\u5DF2\u5B8C\u6210\u7684\u529F\u80FD\uFF1A${deps}
|
|
2255
|
+
|
|
2256
|
+
## \u4EFB\u52A1
|
|
2257
|
+
\u8BF7\u6839\u636E PRD \u6587\u6863\uFF0C\u751F\u6210\u4E00\u4E2A\u5B8C\u6574\u7684\u529F\u80FD\u89C4\u683C\u6587\u6863 Spec\u3002
|
|
2258
|
+
|
|
2259
|
+
## Spec \u6587\u6863\u683C\u5F0F\u8981\u6C42
|
|
2260
|
+
\`\`\`markdown
|
|
2261
|
+
# [\u529F\u80FD\u6807\u9898]
|
|
2262
|
+
|
|
2263
|
+
## \u529F\u80FD\u6982\u8FF0
|
|
2264
|
+
**\u529F\u80FD\u540D\u79F0**: ${featureName}
|
|
2265
|
+
**\u4F18\u5148\u7EA7**: P0/P1/P2 (\u6839\u636E\u529F\u80FD\u91CD\u8981\u6027\u5224\u65AD)
|
|
2266
|
+
**\u9884\u4F30\u5DE5\u65F6**: X \u5929 (\u6839\u636E\u590D\u6742\u5EA6\u8BC4\u4F30\uFF1A\u7B80\u53551-2\u5929\uFF0C\u4E2D\u7B493-5\u5929\uFF0C\u590D\u67425-10\u5929)
|
|
2267
|
+
**\u72B6\u6001**: \u5F85\u62C6\u5206
|
|
2268
|
+
**\u521B\u5EFA\u65E5\u671F**: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
|
|
2269
|
+
|
|
2270
|
+
## \u4F9D\u8D56\u5173\u7CFB
|
|
2271
|
+
**\u524D\u7F6E\u4F9D\u8D56**:
|
|
2272
|
+
${completedSpecs.map((s) => `- [x] ${s}`).join("\n") || "- (\u65E0)"}
|
|
2273
|
+
|
|
2274
|
+
**\u88AB\u4F9D\u8D56\u4E8E**:
|
|
2275
|
+
- (\u81EA\u52A8\u751F\u6210\uFF0C\u8868\u793A\u54EA\u4E9B spec \u4F9D\u8D56\u672C\u529F\u80FD)
|
|
2276
|
+
|
|
2277
|
+
## \u80CC\u666F\u4E0E\u76EE\u6807
|
|
2278
|
+
[\u6839\u636E PRD \u63D0\u53D6\u80CC\u666F\u548C\u76EE\u6807]
|
|
2279
|
+
|
|
2280
|
+
## \u529F\u80FD\u9700\u6C42
|
|
2281
|
+
### \u7528\u6237\u6545\u4E8B
|
|
2282
|
+
\`\`\`
|
|
2283
|
+
\u4F5C\u4E3A [\u5177\u4F53\u89D2\u8272]
|
|
2284
|
+
\u6211\u5E0C\u671B [\u5177\u4F53\u529F\u80FD]
|
|
2285
|
+
\u4EE5\u4FBF [\u5B9E\u73B0\u7684\u4EF7\u503C]
|
|
2286
|
+
\`\`\`
|
|
2287
|
+
|
|
2288
|
+
### \u529F\u80FD\u70B9
|
|
2289
|
+
[\u4ECE PRD \u63D0\u53D6 3-8 \u4E2A\u4E3B\u8981\u529F\u80FD\u70B9]
|
|
2290
|
+
|
|
2291
|
+
## \u6280\u672F\u8BBE\u8BA1
|
|
2292
|
+
### API \u8BBE\u8BA1
|
|
2293
|
+
\u5217\u51FA\u4E3B\u8981\u7684 API \u7AEF\u70B9\uFF0C\u683C\u5F0F\uFF1A
|
|
2294
|
+
- \`METHOD /api/path\` - \u7B80\u77ED\u8BF4\u660E
|
|
2295
|
+
|
|
2296
|
+
### \u6570\u636E\u6A21\u578B
|
|
2297
|
+
\u5217\u51FA\u9700\u8981\u7684\u6570\u636E\u8868\uFF0C\u683C\u5F0F\uFF1A
|
|
2298
|
+
- \`table_name\` (\u8868\u8BF4\u660E) - \u5B57\u6BB5\u8BF4\u660E
|
|
2299
|
+
|
|
2300
|
+
## \u91CC\u7A0B\u7891 (Milestones)
|
|
2301
|
+
> \u6CE8: \u4F7F\u7528 \`team-cli breakdown ${featureName}.md\` \u62C6\u5206\u6B64 spec \u4E3A milestones \u548C todos
|
|
2302
|
+
|
|
2303
|
+
----
|
|
2304
|
+
*\u751F\u6210\u4E8E: ${(/* @__PURE__ */ new Date()).toISOString()} by Claude*
|
|
2305
|
+
\`\`\`
|
|
2306
|
+
|
|
2307
|
+
## \u6CE8\u610F\u4E8B\u9879
|
|
2308
|
+
1. \u53EA\u751F\u6210 Spec \u6587\u6863\uFF0C\u4E0D\u8981\u5B9E\u73B0\u4EFB\u4F55\u4EE3\u7801
|
|
2309
|
+
2. \u4F18\u5148\u7EA7\u5224\u65AD\u6807\u51C6\uFF1A
|
|
2310
|
+
- P0: \u6838\u5FC3\u529F\u80FD\uFF0C\u963B\u585E\u5176\u4ED6\u529F\u80FD
|
|
2311
|
+
- P1: \u91CD\u8981\u529F\u80FD\uFF0C\u5F71\u54CD\u7528\u6237\u4F53\u9A8C
|
|
2312
|
+
- P2: \u589E\u5F3A\u529F\u80FD\uFF0C\u9526\u4E0A\u6DFB\u82B1
|
|
2313
|
+
3. \u5DE5\u65F6\u8BC4\u4F30\u6807\u51C6\uFF1A
|
|
2314
|
+
- \u7B80\u5355\u529F\u80FD: 1-2 \u5929
|
|
2315
|
+
- \u4E2D\u7B49\u529F\u80FD: 3-5 \u5929
|
|
2316
|
+
- \u590D\u6742\u529F\u80FD: 5-10 \u5929
|
|
2317
|
+
4. \u529F\u80FD\u70B9\u8981\u5177\u4F53\u53EF\u6267\u884C\uFF0C\u907F\u514D\u8FC7\u4E8E\u62BD\u8C61
|
|
2318
|
+
5. API \u548C\u6570\u636E\u6A21\u578B\u8981\u7B26\u5408\u5B9E\u9645\u6280\u672F\u6808
|
|
2319
|
+
6. \u4ECE PRD \u4E2D\u63D0\u53D6\u5173\u952E\u4FE1\u606F\uFF0C\u4FDD\u6301\u7B80\u6D01`;
|
|
2320
|
+
}
|
|
2321
|
+
function buildSimplePrompt(featureName, description, projectContext, dependencies) {
|
|
2322
|
+
const deps = dependencies.length > 0 ? dependencies.join("\u3001") : "(\u65E0)";
|
|
2323
|
+
return `Role: Senior Fullstack Developer
|
|
2324
|
+
|
|
2325
|
+
\u7528\u6237\u60F3\u8981\u6DFB\u52A0\u4E00\u4E2A\u65B0\u529F\u80FD\uFF0C\u9700\u8981\u4F60\u751F\u6210\u529F\u80FD\u89C4\u683C\u6587\u6863\uFF08Spec\uFF09\u3002
|
|
2326
|
+
|
|
2327
|
+
## \u529F\u80FD\u540D\u79F0
|
|
2328
|
+
${featureName}
|
|
2329
|
+
|
|
2330
|
+
## \u529F\u80FD\u63CF\u8FF0
|
|
2331
|
+
${description}
|
|
2332
|
+
|
|
2333
|
+
## \u4F9D\u8D56\u529F\u80FD
|
|
2334
|
+
\u6B64\u529F\u80FD\u4F9D\u8D56\u4EE5\u4E0B\u5DF2\u5B8C\u6210\u7684\u529F\u80FD\uFF1A${deps}
|
|
2335
|
+
|
|
2336
|
+
## \u9879\u76EE\u6280\u672F\u6808
|
|
2337
|
+
\u540E\u7AEF: Java 17 + Spring Boot 3 + MyBatis Plus + MySQL 8.0
|
|
2338
|
+
\u524D\u7AEF: Next.js 14 (App Router) + TypeScript + Tailwind CSS
|
|
2339
|
+
|
|
2340
|
+
## \u9879\u76EE\u5F53\u524D\u72B6\u6001
|
|
2341
|
+
${projectContext}
|
|
2342
|
+
|
|
2343
|
+
## \u4EFB\u52A1
|
|
2344
|
+
\u8BF7\u6839\u636E\u529F\u80FD\u63CF\u8FF0\uFF0C\u751F\u6210\u4E00\u4E2A\u5B8C\u6574\u7684\u529F\u80FD\u89C4\u683C\u6587\u6863 Spec\u3002
|
|
2345
|
+
|
|
2346
|
+
## Spec \u6587\u6863\u683C\u5F0F\u8981\u6C42
|
|
2347
|
+
\`\`\`markdown
|
|
2348
|
+
# [\u529F\u80FD\u6807\u9898]
|
|
2349
|
+
|
|
2350
|
+
## \u529F\u80FD\u6982\u8FF0
|
|
2351
|
+
**\u529F\u80FD\u540D\u79F0**: ${featureName}
|
|
2352
|
+
**\u4F18\u5148\u7EA7**: P0/P1/P2 (\u6839\u636E\u529F\u80FD\u91CD\u8981\u6027\u5224\u65AD)
|
|
2353
|
+
**\u9884\u4F30\u5DE5\u65F6**: X \u5929 (\u6839\u636E\u590D\u6742\u5EA6\u8BC4\u4F30\uFF1A\u7B80\u53551-2\u5929\uFF0C\u4E2D\u7B493-5\u5929\uFF0C\u590D\u67425-10\u5929)
|
|
2354
|
+
**\u72B6\u6001**: \u5F85\u62C6\u5206
|
|
2355
|
+
**\u521B\u5EFA\u65E5\u671F**: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
|
|
2356
|
+
|
|
2357
|
+
## \u4F9D\u8D56\u5173\u7CFB
|
|
2358
|
+
**\u524D\u7F6E\u4F9D\u8D56**:
|
|
2359
|
+
${dependencies.map((d) => `- [x] ${d}`).join("\n") || "- (\u65E0)"}
|
|
2360
|
+
|
|
2361
|
+
**\u88AB\u4F9D\u8D56\u4E8E**:
|
|
2362
|
+
- (\u81EA\u52A8\u751F\u6210\uFF0C\u8868\u793A\u54EA\u4E9B spec \u4F9D\u8D56\u672C\u529F\u80FD)
|
|
2363
|
+
|
|
2364
|
+
## \u80CC\u666F\u4E0E\u76EE\u6807
|
|
2365
|
+
[\u6839\u636E\u529F\u80FD\u63CF\u8FF0\uFF0C\u7B80\u660E\u627C\u8981\u5730\u8BF4\u660E\u529F\u80FD\u7684\u80CC\u666F\u548C\u8981\u89E3\u51B3\u7684\u95EE\u9898]
|
|
2366
|
+
|
|
2367
|
+
## \u529F\u80FD\u9700\u6C42
|
|
2368
|
+
### \u7528\u6237\u6545\u4E8B
|
|
2369
|
+
\`\`\`
|
|
2370
|
+
\u4F5C\u4E3A [\u5177\u4F53\u89D2\u8272]
|
|
2371
|
+
\u6211\u5E0C\u671B [\u5177\u4F53\u529F\u80FD]
|
|
2372
|
+
\u4EE5\u4FBF [\u5B9E\u73B0\u7684\u4EF7\u503C]
|
|
2373
|
+
\`\`\`
|
|
2374
|
+
|
|
2375
|
+
### \u529F\u80FD\u70B9
|
|
2376
|
+
\u5217\u51FA 3-8 \u4E2A\u4E3B\u8981\u529F\u80FD\u70B9\uFF0C\u6BCF\u4E2A\u529F\u80FD\u70B9\u7528\u4E00\u53E5\u8BDD\u63CF\u8FF0\u3002
|
|
2377
|
+
|
|
2378
|
+
## \u6280\u672F\u8BBE\u8BA1
|
|
2379
|
+
### API \u8BBE\u8BA1
|
|
2380
|
+
\u5217\u51FA\u4E3B\u8981\u7684 API \u7AEF\u70B9\uFF0C\u683C\u5F0F\uFF1A
|
|
2381
|
+
- \`METHOD /api/path\` - \u7B80\u77ED\u8BF4\u660E
|
|
2382
|
+
|
|
2383
|
+
### \u6570\u636E\u6A21\u578B
|
|
2384
|
+
\u5217\u51FA\u9700\u8981\u7684\u6570\u636E\u8868\uFF0C\u683C\u5F0F\uFF1A
|
|
2385
|
+
- \`table_name\` (\u8868\u8BF4\u660E) - \u5B57\u6BB5\u8BF4\u660E
|
|
2386
|
+
|
|
2387
|
+
## \u91CC\u7A0B\u7891 (Milestones)
|
|
2388
|
+
> \u6CE8: \u4F7F\u7528 \`team-cli breakdown ${featureName}.md\` \u62C6\u5206\u6B64 spec \u4E3A milestones \u548C todos
|
|
2389
|
+
|
|
2390
|
+
----
|
|
2391
|
+
*\u751F\u6210\u4E8E: ${(/* @__PURE__ */ new Date()).toISOString()} by Claude*
|
|
2392
|
+
\`\`\`
|
|
2393
|
+
|
|
2394
|
+
## \u6CE8\u610F\u4E8B\u9879
|
|
2395
|
+
1. \u53EA\u751F\u6210 Spec \u6587\u6863\uFF0C\u4E0D\u8981\u5B9E\u73B0\u4EFB\u4F55\u4EE3\u7801
|
|
2396
|
+
2. \u4F18\u5148\u7EA7\u5224\u65AD\u8981\u5408\u7406\uFF0C\u907F\u514D\u5168\u90E8\u662F P0
|
|
2397
|
+
3. \u5DE5\u65F6\u8BC4\u4F30\u8981\u5B9E\u9645\uFF0C\u4E0D\u8981\u8FC7\u4E8E\u4E50\u89C2
|
|
2398
|
+
4. \u529F\u80FD\u70B9\u8981\u5177\u4F53\u53EF\u6267\u884C
|
|
2399
|
+
5. \u6839\u636E\u529F\u80FD\u63CF\u8FF0\u63A8\u65AD\u5408\u7406\u7684 API \u548C\u6570\u636E\u6A21\u578B
|
|
2400
|
+
6. \u53C2\u8003\u5DF2\u6709\u529F\u80FD\u7684\u6280\u672F\u98CE\u683C\u4FDD\u6301\u4E00\u81F4\u6027`;
|
|
2401
|
+
}
|
|
2402
|
+
async function updateAiMemory(featureName, featureSlug) {
|
|
2403
|
+
const aiMemoryFile = "AI_MEMORY.md";
|
|
2404
|
+
const exists = await FileUtils.exists(aiMemoryFile);
|
|
2405
|
+
if (!exists) {
|
|
2406
|
+
logger.warn("AI_MEMORY.md \u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u66F4\u65B0");
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
let content = await FileUtils.read(aiMemoryFile);
|
|
2410
|
+
if (!content.includes("## \u529F\u80FD\u6E05\u5355")) {
|
|
2411
|
+
const featureList = `
|
|
2412
|
+
## \u529F\u80FD\u6E05\u5355 (Feature Inventory)
|
|
2413
|
+
|
|
2414
|
+
| \u529F\u80FD | Spec \u6587\u4EF6 | \u72B6\u6001 | \u8FDB\u5EA6 | \u5B8C\u6210\u65E5\u671F | \u5907\u6CE8 |
|
|
2415
|
+
|------|----------|------|------|---------|------|
|
|
2416
|
+
| ${featureName} | ${featureSlug}.md | \u25CB \u672A\u5F00\u59CB | 0/0 | - | |
|
|
2417
|
+
`;
|
|
2418
|
+
content = content.replace("(/\u9879\u76EE\u4FE1\u606F/[^#]*)", `$1
|
|
2419
|
+
${featureList}`);
|
|
2420
|
+
} else {
|
|
2421
|
+
const newRow = `| ${featureName} | ${featureSlug}.md | \u25CB \u672A\u5F00\u59CB | 0/0 | - | |`;
|
|
2422
|
+
content = content.replace(
|
|
2423
|
+
/(\| 功能 \| Spec 文件 \| 状态 \| 进度 \| 完成日期 \| 备注 \|)/,
|
|
2424
|
+
`$1
|
|
2425
|
+
${newRow}`
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
await FileUtils.write(aiMemoryFile, content);
|
|
2429
|
+
logger.success("AI_MEMORY.md \u5DF2\u66F4\u65B0");
|
|
2430
|
+
}
|
|
2431
|
+
async function showSpecPreview(specFile) {
|
|
2432
|
+
logger.newLine();
|
|
2433
|
+
logger.header("\u751F\u6210\u7684 Spec \u9884\u89C8:");
|
|
2434
|
+
logger.newLine();
|
|
2435
|
+
const content = await FileUtils.read(specFile);
|
|
2436
|
+
const lines = content.split("\n");
|
|
2437
|
+
const preview = lines.slice(0, 40).join("\n");
|
|
2438
|
+
console.log(preview);
|
|
2439
|
+
if (lines.length > 40) {
|
|
2440
|
+
console.log("...");
|
|
2441
|
+
console.log(`(\u5171 ${lines.length} \u884C)`);
|
|
2442
|
+
}
|
|
2443
|
+
console.log("");
|
|
2444
|
+
}
|
|
2445
|
+
async function askToAdjust(specFile) {
|
|
2446
|
+
const { needAdjust } = await inquirer4.prompt([
|
|
2447
|
+
{
|
|
2448
|
+
type: "confirm",
|
|
2449
|
+
name: "needAdjust",
|
|
2450
|
+
message: "\u662F\u5426\u9700\u8981\u8C03\u6574 Spec \u5185\u5BB9?",
|
|
2451
|
+
default: false
|
|
2452
|
+
}
|
|
2453
|
+
]);
|
|
2454
|
+
if (needAdjust) {
|
|
2455
|
+
logger.info("\u6B63\u5728\u6253\u5F00\u7F16\u8F91\u5668...");
|
|
2456
|
+
const editor = process.env.EDITOR || "vim";
|
|
2457
|
+
const { execaCommand } = await import("execa");
|
|
2458
|
+
await execaCommand(`${editor} ${specFile}`, { stdio: "inherit" });
|
|
2459
|
+
}
|
|
2460
|
+
logger.newLine();
|
|
2461
|
+
logger.info("\u4E0B\u4E00\u6B65:");
|
|
2462
|
+
logger.step(`1. \u8FD0\u884C 'team-cli breakdown ${specFile}' \u62C6\u5206\u4E3A milestones`);
|
|
2463
|
+
logger.step("2. \u8FD0\u884C 'team-cli dev' \u9009\u62E9 milestone \u8FDB\u884C\u5F00\u53D1");
|
|
2464
|
+
logger.newLine();
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
// src/commands/split-prd.ts
|
|
2468
|
+
import { Command as Command5 } from "commander";
|
|
2469
|
+
import path6 from "path";
|
|
2470
|
+
import { Listr as Listr4 } from "listr2";
|
|
2471
|
+
var splitPrdCommand = new Command5("split-prd").argument("<prd-folder>", "PRD \u6587\u6863\u76EE\u5F55").description("\u5C06 PRD \u62C6\u5206\u6210\u591A\u4E2A specs").action(async (prdFolder) => {
|
|
2472
|
+
try {
|
|
2473
|
+
logger.header("PRD \u62C6\u5206");
|
|
2474
|
+
logger.newLine();
|
|
2475
|
+
const hasTechStack = await FileUtils.exists("TECH_STACK.md");
|
|
2476
|
+
if (!hasTechStack) {
|
|
2477
|
+
logger.error("\u5F53\u524D\u76EE\u5F55\u4E0D\u662F\u4E00\u4E2A\u6709\u6548\u7684 team-cli \u9879\u76EE");
|
|
2478
|
+
logger.info("\u8BF7\u5148\u8FD0\u884C 'team-cli init <project-name>' \u6216\u5207\u6362\u5230\u9879\u76EE\u76EE\u5F55");
|
|
2479
|
+
process.exit(1);
|
|
2480
|
+
}
|
|
2481
|
+
const folderExists = await FileUtils.exists(prdFolder);
|
|
2482
|
+
if (!folderExists) {
|
|
2483
|
+
throw new Error(`PRD \u6587\u4EF6\u5939\u4E0D\u5B58\u5728: ${prdFolder}`);
|
|
2484
|
+
}
|
|
2485
|
+
const hasClaude = await claudeAI.checkInstalled();
|
|
2486
|
+
if (!hasClaude) {
|
|
2487
|
+
logger.error("\u672A\u68C0\u6D4B\u5230 Claude CLI");
|
|
2488
|
+
logger.info("\u8BF7\u5B89\u88C5 Claude CLI: npm install -g @anthropic-ai/claude-code");
|
|
2489
|
+
process.exit(1);
|
|
2490
|
+
}
|
|
2491
|
+
const tasks = new Listr4([
|
|
2492
|
+
{
|
|
2493
|
+
title: "\u626B\u63CF PRD \u6587\u4EF6",
|
|
2494
|
+
task: async (ctx2) => {
|
|
2495
|
+
const supportedExtensions = ["md", "txt", "markdown"];
|
|
2496
|
+
ctx2.prdFiles = [];
|
|
2497
|
+
for (const ext of supportedExtensions) {
|
|
2498
|
+
const files = await FileUtils.findFiles(`*.${ext}`, prdFolder);
|
|
2499
|
+
ctx2.prdFiles.push(...files.map((f) => path6.join(prdFolder, f)));
|
|
2500
|
+
}
|
|
2501
|
+
if (ctx2.prdFiles.length === 0) {
|
|
2502
|
+
throw new Error(
|
|
2503
|
+
"\u672A\u627E\u5230 PRD \u6587\u6863 (\u652F\u6301 .md, .txt, .markdown)"
|
|
2504
|
+
);
|
|
2505
|
+
}
|
|
2506
|
+
ctx2.prdFile = ctx2.prdFiles[0];
|
|
2507
|
+
if (ctx2.prdFiles.length > 1) {
|
|
2508
|
+
logger.info(`\u627E\u5230\u591A\u4E2A PRD \u6587\u6863\uFF0C\u4F7F\u7528: ${ctx2.prdFile}`);
|
|
2509
|
+
} else {
|
|
2510
|
+
logger.success(`\u627E\u5230 PRD \u6587\u6863: ${ctx2.prdFile}`);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
},
|
|
2514
|
+
{
|
|
2515
|
+
title: "\u626B\u63CF\u622A\u56FE\u6587\u4EF6",
|
|
2516
|
+
task: async (ctx2) => {
|
|
2517
|
+
const screenshotDir = path6.join(prdFolder, "screenshots");
|
|
2518
|
+
const dirExists = await FileUtils.exists(screenshotDir);
|
|
2519
|
+
if (!dirExists) {
|
|
2520
|
+
logger.info("\u672A\u627E\u5230 screenshots \u76EE\u5F55\uFF0C\u8DF3\u8FC7\u622A\u56FE");
|
|
2521
|
+
ctx2.screenshots = [];
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
const imageExtensions = ["png", "jpg", "jpeg", "gif", "webp"];
|
|
2525
|
+
ctx2.screenshots = [];
|
|
2526
|
+
for (const ext of imageExtensions) {
|
|
2527
|
+
const files = await FileUtils.findFiles(`*.${ext}`, screenshotDir);
|
|
2528
|
+
ctx2.screenshots.push(...files.map((f) => path6.join(screenshotDir, f)));
|
|
2529
|
+
}
|
|
2530
|
+
logger.success(`\u627E\u5230 ${ctx2.screenshots.length} \u4E2A\u622A\u56FE\u6587\u4EF6`);
|
|
2531
|
+
}
|
|
2532
|
+
},
|
|
2533
|
+
{
|
|
2534
|
+
title: "\u626B\u63CF demo \u4EE3\u7801\u4ED3\u5E93",
|
|
2535
|
+
task: async (ctx2) => {
|
|
2536
|
+
const entries = await FileUtils.findFiles("*/", prdFolder);
|
|
2537
|
+
ctx2.demoRepos = [];
|
|
2538
|
+
for (const entry of entries) {
|
|
2539
|
+
const dirPath = path6.join(prdFolder, entry);
|
|
2540
|
+
const gitDir = path6.join(dirPath, ".git");
|
|
2541
|
+
const hasGit = await FileUtils.exists(gitDir);
|
|
2542
|
+
if (hasGit) {
|
|
2543
|
+
ctx2.demoRepos.push(dirPath);
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
if (ctx2.demoRepos.length > 0) {
|
|
2547
|
+
logger.success(`\u627E\u5230 demo \u4ED3\u5E93: ${ctx2.demoRepos.join(", ")}`);
|
|
2548
|
+
} else {
|
|
2549
|
+
logger.info("\u672A\u627E\u5230 demo \u4EE3\u7801\u4ED3\u5E93\uFF0C\u8DF3\u8FC7");
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
},
|
|
2553
|
+
{
|
|
2554
|
+
title: "\u8BFB\u53D6 PRD \u5185\u5BB9",
|
|
2555
|
+
task: async (ctx2) => {
|
|
2556
|
+
ctx2.prdContent = await FileUtils.read(ctx2.prdFile);
|
|
2557
|
+
}
|
|
2558
|
+
},
|
|
2559
|
+
{
|
|
2560
|
+
title: "\u8C03\u7528 Claude \u62C6\u5206 PRD",
|
|
2561
|
+
task: async (ctx2) => {
|
|
2562
|
+
const prompt = buildSplitPrdPrompt(
|
|
2563
|
+
ctx2.prdContent,
|
|
2564
|
+
ctx2.screenshots,
|
|
2565
|
+
ctx2.demoRepos
|
|
2566
|
+
);
|
|
2567
|
+
logger.newLine();
|
|
2568
|
+
logger.separator("\u2500", 60);
|
|
2569
|
+
logger.info("Claude \u6267\u884C\u4E2D...");
|
|
2570
|
+
logger.separator("\u2500", 60);
|
|
2571
|
+
logger.newLine();
|
|
2572
|
+
return await claudeAI.prompt(prompt, {
|
|
2573
|
+
contextFiles: ["TECH_STACK.md", "CONVENTIONS.md"]
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
]);
|
|
2578
|
+
const ctx = await tasks.run();
|
|
2579
|
+
logger.newLine();
|
|
2580
|
+
logger.separator("\u2500", 60);
|
|
2581
|
+
logger.newLine();
|
|
2582
|
+
logger.header("PRD \u62C6\u5206\u5B8C\u6210!");
|
|
2583
|
+
logger.success("Spec \u6587\u4EF6\u5DF2\u751F\u6210\u5230 docs/specs/ \u76EE\u5F55");
|
|
2584
|
+
logger.newLine();
|
|
2585
|
+
logger.info("\u4E0B\u4E00\u6B65:");
|
|
2586
|
+
logger.step("1. \u68C0\u67E5\u751F\u6210\u7684 spec \u6587\u4EF6");
|
|
2587
|
+
logger.step("2. \u8FD0\u884C 'team-cli breakdown <spec-file>' \u62C6\u5206 milestones");
|
|
2588
|
+
logger.step("3. \u8FD0\u884C 'team-cli dev' \u5F00\u59CB\u5F00\u53D1");
|
|
2589
|
+
logger.newLine();
|
|
2590
|
+
} catch (error) {
|
|
2591
|
+
logger.error(`PRD \u62C6\u5206\u5931\u8D25: ${error.message}`);
|
|
2592
|
+
if (process.env.DEBUG) {
|
|
2593
|
+
console.error(error);
|
|
2594
|
+
}
|
|
2595
|
+
process.exit(1);
|
|
2596
|
+
}
|
|
2597
|
+
});
|
|
2598
|
+
function buildSplitPrdPrompt(prdContent, screenshots, demoRepos) {
|
|
2599
|
+
let prompt = `Role: Senior Product Manager and Technical Architect
|
|
2600
|
+
|
|
2601
|
+
Task: Analyze the following PRD and split it into multiple independent feature specifications (specs).
|
|
2602
|
+
|
|
2603
|
+
Context:
|
|
2604
|
+
- Read TECH_STACK.md for technology constraints
|
|
2605
|
+
- Read CONVENTIONS.md for coding standards
|
|
2606
|
+
- Each spec should be a standalone feature that can be developed independently
|
|
2607
|
+
|
|
2608
|
+
PRD Content:
|
|
2609
|
+
\`\`\`
|
|
2610
|
+
${prdContent}
|
|
2611
|
+
\`\`\`
|
|
2612
|
+
`;
|
|
2613
|
+
if (screenshots.length > 0) {
|
|
2614
|
+
prompt += `
|
|
2615
|
+
Screenshots available (${screenshots.length} files):
|
|
2616
|
+
`;
|
|
2617
|
+
for (const screenshot of screenshots) {
|
|
2618
|
+
prompt += ` - ${screenshot}
|
|
2619
|
+
`;
|
|
2620
|
+
}
|
|
2621
|
+
prompt += `You can use the Read tool to view these screenshots for visual reference.
|
|
2622
|
+
`;
|
|
2623
|
+
}
|
|
2624
|
+
if (demoRepos.length > 0) {
|
|
2625
|
+
prompt += `
|
|
2626
|
+
Demo Code Repos available:
|
|
2627
|
+
`;
|
|
2628
|
+
for (const repo of demoRepos) {
|
|
2629
|
+
prompt += ` - ${repo}
|
|
2630
|
+
`;
|
|
2631
|
+
}
|
|
2632
|
+
prompt += `You can explore these repos to understand existing implementation patterns.
|
|
2633
|
+
`;
|
|
2634
|
+
}
|
|
2635
|
+
prompt += `
|
|
2636
|
+
Output Requirements:
|
|
2637
|
+
1. Split the PRD into multiple feature specs based on functionality
|
|
2638
|
+
2. Each spec should follow the template format (see docs/specs/template.md)
|
|
2639
|
+
3. Create each spec as a separate markdown file in docs/specs/
|
|
2640
|
+
4. Use kebab-case for filenames (e.g., user-authentication.md, data-export.md)
|
|
2641
|
+
5. Each spec must include:
|
|
2642
|
+
- Feature Overview (\u529F\u80FD\u6982\u8FF0)
|
|
2643
|
+
- Background & Goals (\u80CC\u666F\u4E0E\u76EE\u6807)
|
|
2644
|
+
- Functional Requirements (\u529F\u80FD\u9700\u6C42)
|
|
2645
|
+
- Technical Design (\u6280\u672F\u8BBE\u8BA1)
|
|
2646
|
+
- Acceptance Criteria (\u9A8C\u6536\u6807\u51C6)
|
|
2647
|
+
|
|
2648
|
+
Spec Format Template:
|
|
2649
|
+
\`\`\`markdown
|
|
2650
|
+
# [\u529F\u80FD\u6807\u9898]
|
|
2651
|
+
|
|
2652
|
+
## \u529F\u80FD\u6982\u8FF0
|
|
2653
|
+
**\u529F\u80FD\u540D\u79F0**: [\u529F\u80FD\u4E2D\u6587\u540D]
|
|
2654
|
+
**\u4F18\u5148\u7EA7**: P0/P1/P2
|
|
2655
|
+
**\u9884\u4F30\u5DE5\u65F6**: X \u5929
|
|
2656
|
+
**\u72B6\u6001**: \u5F85\u62C6\u5206
|
|
2657
|
+
**\u521B\u5EFA\u65E5\u671F**: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
|
|
2658
|
+
|
|
2659
|
+
## \u4F9D\u8D56\u5173\u7CFB
|
|
2660
|
+
**\u524D\u7F6E\u4F9D\u8D56**:
|
|
2661
|
+
- (\u5217\u51FA\u4F9D\u8D56\u7684\u5176\u4ED6 spec)
|
|
2662
|
+
|
|
2663
|
+
**\u88AB\u4F9D\u8D56\u4E8E**:
|
|
2664
|
+
- (\u81EA\u52A8\u751F\u6210)
|
|
2665
|
+
|
|
2666
|
+
## \u80CC\u666F\u4E0E\u76EE\u6807
|
|
2667
|
+
[\u7B80\u660E\u627C\u8981\u5730\u8BF4\u660E\u529F\u80FD\u7684\u80CC\u666F\u548C\u8981\u89E3\u51B3\u7684\u95EE\u9898]
|
|
2668
|
+
|
|
2669
|
+
## \u529F\u80FD\u9700\u6C42
|
|
2670
|
+
|
|
2671
|
+
### \u7528\u6237\u6545\u4E8B
|
|
2672
|
+
\`\`\`
|
|
2673
|
+
\u4F5C\u4E3A [\u5177\u4F53\u89D2\u8272]
|
|
2674
|
+
\u6211\u5E0C\u671B [\u5177\u4F53\u529F\u80FD]
|
|
2675
|
+
\u4EE5\u4FBF [\u5B9E\u73B0\u7684\u4EF7\u503C]
|
|
2676
|
+
\`\`\`
|
|
2677
|
+
|
|
2678
|
+
### \u529F\u80FD\u70B9
|
|
2679
|
+
\u5217\u51FA 3-8 \u4E2A\u4E3B\u8981\u529F\u80FD\u70B9
|
|
2680
|
+
|
|
2681
|
+
## \u6280\u672F\u8BBE\u8BA1
|
|
2682
|
+
|
|
2683
|
+
### API \u8BBE\u8BA1
|
|
2684
|
+
\u5217\u51FA\u4E3B\u8981\u7684 API \u7AEF\u70B9
|
|
2685
|
+
|
|
2686
|
+
### \u6570\u636E\u6A21\u578B
|
|
2687
|
+
\u5217\u51FA\u9700\u8981\u7684\u6570\u636E\u8868
|
|
2688
|
+
|
|
2689
|
+
## \u9A8C\u6536\u6807\u51C6
|
|
2690
|
+
- [ ] \u9A8C\u6536\u6807\u51C6 1
|
|
2691
|
+
- [ ] \u9A8C\u6536\u6807\u51C6 2
|
|
2692
|
+
- [ ] \u9A8C\u6536\u6807\u51C6 3
|
|
2693
|
+
|
|
2694
|
+
----
|
|
2695
|
+
*\u751F\u6210\u4E8E: ${(/* @__PURE__ */ new Date()).toISOString()} by Claude*
|
|
2696
|
+
\`\`\`
|
|
2697
|
+
|
|
2698
|
+
IMPORTANT:
|
|
2699
|
+
- After generating all spec files, exit immediately without waiting for further input
|
|
2700
|
+
- Do not ask any questions
|
|
2701
|
+
- Each spec should be implementable in 1-3 days
|
|
2702
|
+
- Dependencies between features should be clearly noted in the \u4F9D\u8D56\u5173\u7CFB section
|
|
2703
|
+
`;
|
|
2704
|
+
return prompt;
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
// src/commands/bugfix.ts
|
|
2708
|
+
import { Command as Command6 } from "commander";
|
|
2709
|
+
import inquirer5 from "inquirer";
|
|
2710
|
+
import path7 from "path";
|
|
2711
|
+
import { Listr as Listr5 } from "listr2";
|
|
2712
|
+
var bugfixCommand = new Command6("bugfix").description("\u521B\u5EFA Bugfix \u8BB0\u5F55").action(async () => {
|
|
2713
|
+
try {
|
|
2714
|
+
logger.header("\u521B\u5EFA Bugfix \u8BB0\u5F55");
|
|
2715
|
+
logger.newLine();
|
|
2716
|
+
const hasTechStack = await FileUtils.exists("TECH_STACK.md");
|
|
2717
|
+
if (!hasTechStack) {
|
|
2718
|
+
logger.error("\u5F53\u524D\u76EE\u5F55\u4E0D\u662F\u4E00\u4E2A\u6709\u6548\u7684 team-cli \u9879\u76EE");
|
|
2719
|
+
logger.info("\u8BF7\u5148\u8FD0\u884C 'team-cli init <project-name>' \u6216\u5207\u6362\u5230\u9879\u76EE\u76EE\u5F55");
|
|
2720
|
+
process.exit(1);
|
|
2721
|
+
}
|
|
2722
|
+
const answers = await inquirer5.prompt([
|
|
2723
|
+
{
|
|
2724
|
+
type: "list",
|
|
2725
|
+
name: "severity",
|
|
2726
|
+
message: "Bug \u4E25\u91CD\u7A0B\u5EA6:",
|
|
2727
|
+
choices: [
|
|
2728
|
+
{ name: "\u9AD8 - \u963B\u585E\u529F\u80FD\u6216\u5F71\u54CD\u6838\u5FC3\u6D41\u7A0B", value: "\u9AD8" },
|
|
2729
|
+
{ name: "\u4E2D - \u5F71\u54CD\u529F\u80FD\u4F46\u4E0D\u963B\u585E", value: "\u4E2D" },
|
|
2730
|
+
{ name: "\u4F4E - \u5C0F\u95EE\u9898\u6216\u4F53\u9A8C\u95EE\u9898", value: "\u4F4E" }
|
|
2731
|
+
],
|
|
2732
|
+
default: "\u4E2D"
|
|
2733
|
+
},
|
|
2734
|
+
{
|
|
2735
|
+
type: "input",
|
|
2736
|
+
name: "description",
|
|
2737
|
+
message: "Bug \u63CF\u8FF0:",
|
|
2738
|
+
validate: (input) => input.trim().length > 0 || "\u63CF\u8FF0\u4E0D\u80FD\u4E3A\u7A7A"
|
|
2739
|
+
},
|
|
2740
|
+
{
|
|
2741
|
+
type: "input",
|
|
2742
|
+
name: "reproduction",
|
|
2743
|
+
message: "\u590D\u73B0\u6B65\u9AA4:",
|
|
2744
|
+
default: "1. \u6B65\u9AA4\u4E00\n2. \u6B65\u9AA4\u4E8C\n3. \u6B65\u9AA4\u4E09"
|
|
2745
|
+
},
|
|
2746
|
+
{
|
|
2747
|
+
type: "list",
|
|
2748
|
+
name: "scope",
|
|
2749
|
+
message: "\u5F71\u54CD\u8303\u56F4:",
|
|
2750
|
+
choices: ["\u5168\u90E8\u7528\u6237", "\u90E8\u5206\u7528\u6237", "\u4E2A\u522B\u7528\u6237", "\u672A\u77E5"],
|
|
2751
|
+
default: "\u90E8\u5206\u7528\u6237"
|
|
2752
|
+
}
|
|
2753
|
+
]);
|
|
2754
|
+
const bugId = generateBugId();
|
|
2755
|
+
const timestamp = DateUtils.format(/* @__PURE__ */ new Date(), "YYYY-MM-DD");
|
|
2756
|
+
const relatedSpec = await findRelatedSpec(answers.description);
|
|
2757
|
+
const bugfixDir = "docs/bugfixes";
|
|
2758
|
+
await FileUtils.ensureDir(bugfixDir);
|
|
2759
|
+
const bugfixFile = path7.join(bugfixDir, `${timestamp}_${bugId}.md`);
|
|
2760
|
+
const content = formatBugfixDocument({
|
|
2761
|
+
id: bugId,
|
|
2762
|
+
severity: answers.severity,
|
|
2763
|
+
description: answers.description,
|
|
2764
|
+
reproduction: answers.reproduction,
|
|
2765
|
+
scope: answers.scope,
|
|
2766
|
+
relatedSpec,
|
|
2767
|
+
timestamp
|
|
2768
|
+
});
|
|
2769
|
+
await FileUtils.write(bugfixFile, content);
|
|
2770
|
+
logger.success(`Bugfix \u8BB0\u5F55\u5DF2\u521B\u5EFA: ${bugfixFile}`);
|
|
2771
|
+
logger.newLine();
|
|
2772
|
+
logger.info("\u4E0B\u4E00\u6B65:");
|
|
2773
|
+
logger.step("1. \u5206\u6790 bug \u6839\u672C\u539F\u56E0");
|
|
2774
|
+
logger.step("2. \u8FD0\u884C 'team-cli dev' \u9009\u62E9\u5173\u8054\u7684 spec \u8FDB\u884C\u4FEE\u590D");
|
|
2775
|
+
logger.step("3. \u4FEE\u590D\u540E\u66F4\u65B0 bugfix \u6587\u6863\u72B6\u6001");
|
|
2776
|
+
logger.newLine();
|
|
2777
|
+
} catch (error) {
|
|
2778
|
+
logger.error(`\u521B\u5EFA bugfix \u5931\u8D25: ${error.message}`);
|
|
2779
|
+
if (process.env.DEBUG) {
|
|
2780
|
+
console.error(error);
|
|
2781
|
+
}
|
|
2782
|
+
process.exit(1);
|
|
2783
|
+
}
|
|
2784
|
+
});
|
|
2785
|
+
var hotfixCommand = new Command6("hotfix").description("\u7D27\u6025\u4FEE\u590D\u6D41\u7A0B").action(async () => {
|
|
2786
|
+
try {
|
|
2787
|
+
logger.header("\u7D27\u6025\u4FEE\u590D\u6D41\u7A0B");
|
|
2788
|
+
logger.newLine();
|
|
2789
|
+
const hasTechStack = await FileUtils.exists("TECH_STACK.md");
|
|
2790
|
+
if (!hasTechStack) {
|
|
2791
|
+
logger.error("\u5F53\u524D\u76EE\u5F55\u4E0D\u662F\u4E00\u4E2A\u6709\u6548\u7684 team-cli \u9879\u76EE");
|
|
2792
|
+
logger.info("\u8BF7\u5148\u8FD0\u884C 'team-cli init <project-name>' \u6216\u5207\u6362\u5230\u9879\u76EE\u76EE\u5F55");
|
|
2793
|
+
process.exit(1);
|
|
2794
|
+
}
|
|
2795
|
+
logger.warn("\u6CE8\u610F: hotfix \u7528\u4E8E\u7D27\u6025\u95EE\u9898\u4FEE\u590D");
|
|
2796
|
+
logger.warn("\u4FEE\u590D\u540E\u9700\u8981\u521B\u5EFA\u89C4\u8303 bugfix \u8BB0\u5F55");
|
|
2797
|
+
logger.newLine();
|
|
2798
|
+
const answers = await inquirer5.prompt([
|
|
2799
|
+
{
|
|
2800
|
+
type: "input",
|
|
2801
|
+
name: "description",
|
|
2802
|
+
message: "\u7D27\u6025\u95EE\u9898\u63CF\u8FF0:",
|
|
2803
|
+
validate: (input) => input.trim().length > 0 || "\u63CF\u8FF0\u4E0D\u80FD\u4E3A\u7A7A"
|
|
2804
|
+
},
|
|
2805
|
+
{
|
|
2806
|
+
type: "list",
|
|
2807
|
+
name: "scope",
|
|
2808
|
+
message: "\u5F71\u54CD\u8303\u56F4:",
|
|
2809
|
+
choices: ["\u6240\u6709\u7528\u6237", "\u90E8\u5206\u7528\u6237", "\u7279\u5B9A\u7528\u6237"],
|
|
2810
|
+
default: "\u6240\u6709\u7528\u6237"
|
|
2811
|
+
},
|
|
2812
|
+
{
|
|
2813
|
+
type: "input",
|
|
2814
|
+
name: "solution",
|
|
2815
|
+
message: "\u4E34\u65F6\u89E3\u51B3\u65B9\u6848:",
|
|
2816
|
+
validate: (input) => input.trim().length > 0 || "\u65B9\u6848\u4E0D\u80FD\u4E3A\u7A7A"
|
|
2817
|
+
}
|
|
2818
|
+
]);
|
|
2819
|
+
const hotfixId = generateHotfixId();
|
|
2820
|
+
const branchName = `hotfix/${hotfixId}`;
|
|
2821
|
+
const tasks = new Listr5([
|
|
2822
|
+
{
|
|
2823
|
+
title: "\u521B\u5EFA hotfix \u5206\u652F",
|
|
2824
|
+
task: async () => {
|
|
2825
|
+
const { execa: execa3 } = await import("execa");
|
|
2826
|
+
await execa3("git", ["checkout", "-b", branchName], { stdio: "inherit" });
|
|
2827
|
+
}
|
|
2828
|
+
},
|
|
2829
|
+
{
|
|
2830
|
+
title: "\u521B\u5EFA hotfix \u8BB0\u5F55",
|
|
2831
|
+
task: async () => {
|
|
2832
|
+
const timestamp = DateUtils.format(/* @__PURE__ */ new Date(), "YYYY-MM-DD HH:mm:ss");
|
|
2833
|
+
const hotfixDir = "docs/hotfixes";
|
|
2834
|
+
await FileUtils.ensureDir(hotfixDir);
|
|
2835
|
+
const hotfixFile = path7.join(hotfixDir, `${timestamp}_${hotfixId}.md`);
|
|
2836
|
+
const content = formatHotfixDocument({
|
|
2837
|
+
id: hotfixId,
|
|
2838
|
+
description: answers.description,
|
|
2839
|
+
scope: answers.scope,
|
|
2840
|
+
solution: answers.solution,
|
|
2841
|
+
branch: branchName,
|
|
2842
|
+
timestamp
|
|
2843
|
+
});
|
|
2844
|
+
await FileUtils.write(hotfixFile, content);
|
|
2845
|
+
}
|
|
2846
|
+
},
|
|
2847
|
+
{
|
|
2848
|
+
title: "\u521B\u5EFA\u521D\u59CB commit",
|
|
2849
|
+
task: async () => {
|
|
2850
|
+
const { execa: execa3 } = await import("execa");
|
|
2851
|
+
await execa3("git", ["add", "docs/hotfixes"], { stdio: "pipe" });
|
|
2852
|
+
await execa3(
|
|
2853
|
+
"git",
|
|
2854
|
+
[
|
|
2855
|
+
"commit",
|
|
2856
|
+
"-m",
|
|
2857
|
+
`hotfix: ${answers.description}
|
|
2858
|
+
|
|
2859
|
+
Temporary solution: ${answers.solution}`
|
|
2860
|
+
],
|
|
2861
|
+
{ stdio: "pipe" }
|
|
2862
|
+
);
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
]);
|
|
2866
|
+
await tasks.run();
|
|
2867
|
+
logger.newLine();
|
|
2868
|
+
logger.success(`Hotfix \u5206\u652F\u5DF2\u521B\u5EFA: ${branchName}`);
|
|
2869
|
+
logger.newLine();
|
|
2870
|
+
logger.info("\u4E0B\u4E00\u6B65:");
|
|
2871
|
+
logger.step("1. \u5B9E\u65BD\u4FEE\u590D");
|
|
2872
|
+
logger.step("2. \u8FD0\u884C 'team-cli lint' \u68C0\u67E5\u4EE3\u7801");
|
|
2873
|
+
logger.step("3. \u63D0\u4EA4\u5E76\u63A8\u9001\u5230\u8FDC\u7A0B");
|
|
2874
|
+
logger.step("4. \u521B\u5EFA PR \u8FDB\u884C code review");
|
|
2875
|
+
logger.step("5. \u5408\u5E76\u540E\u521B\u5EFA\u89C4\u8303 bugfix \u8BB0\u5F55");
|
|
2876
|
+
logger.newLine();
|
|
2877
|
+
} catch (error) {
|
|
2878
|
+
logger.error(`Hotfix \u6D41\u7A0B\u5931\u8D25: ${error.message}`);
|
|
2879
|
+
if (process.env.DEBUG) {
|
|
2880
|
+
console.error(error);
|
|
2881
|
+
}
|
|
2882
|
+
process.exit(1);
|
|
2883
|
+
}
|
|
2884
|
+
});
|
|
2885
|
+
function generateBugId() {
|
|
2886
|
+
const date = /* @__PURE__ */ new Date();
|
|
2887
|
+
const year = date.getFullYear();
|
|
2888
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
2889
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
2890
|
+
return `BUG-${year}${month}${day}-001`;
|
|
2891
|
+
}
|
|
2892
|
+
function generateHotfixId() {
|
|
2893
|
+
const date = /* @__PURE__ */ new Date();
|
|
2894
|
+
const timestamp = date.getTime().toString().slice(-6);
|
|
2895
|
+
return `HF-${timestamp}`;
|
|
2896
|
+
}
|
|
2897
|
+
async function findRelatedSpec(description) {
|
|
2898
|
+
const specDir = "docs/specs";
|
|
2899
|
+
const exists = await FileUtils.exists(specDir);
|
|
2900
|
+
if (!exists) {
|
|
2901
|
+
return "";
|
|
2902
|
+
}
|
|
2903
|
+
const files = await FileUtils.findFiles("*.md", specDir);
|
|
2904
|
+
const specs = files.filter((f) => !f.includes("template"));
|
|
2905
|
+
if (specs.length === 0) {
|
|
2906
|
+
return "";
|
|
2907
|
+
}
|
|
2908
|
+
const keywords = extractKeywords(description);
|
|
2909
|
+
for (const file of specs) {
|
|
2910
|
+
const filePath = path7.join(specDir, file);
|
|
2911
|
+
const content = await FileUtils.read(filePath);
|
|
2912
|
+
for (const keyword of keywords) {
|
|
2913
|
+
if (content.toLowerCase().includes(keyword.toLowerCase())) {
|
|
2914
|
+
return file.replace(".md", "");
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
return "";
|
|
2919
|
+
}
|
|
2920
|
+
function extractKeywords(text) {
|
|
2921
|
+
const stopWords = ["\u7684", "\u662F", "\u5728", "\u6709", "\u548C", "\u6216", "\u4E86", "\u4E0D", "\u5417", "\u5462"];
|
|
2922
|
+
const words = text.toLowerCase().replace(/[^\w\s\u4e00-\u9fa5]/g, "").split(/\s+/).filter((w) => w.length > 1 && !stopWords.includes(w));
|
|
2923
|
+
return words.slice(0, 5);
|
|
2924
|
+
}
|
|
2925
|
+
function formatBugfixDocument(data) {
|
|
2926
|
+
return `# Bugfix: ${data.description}
|
|
2927
|
+
|
|
2928
|
+
## Bug \u4FE1\u606F
|
|
2929
|
+
- **ID**: ${data.id}
|
|
2930
|
+
- **\u4E25\u91CD\u7A0B\u5EA6**: ${data.severity}
|
|
2931
|
+
- **\u72B6\u6001**: \u5F85\u4FEE\u590D
|
|
2932
|
+
- **\u521B\u5EFA\u65F6\u95F4**: ${data.timestamp}
|
|
2933
|
+
|
|
2934
|
+
## \u95EE\u9898\u63CF\u8FF0
|
|
2935
|
+
${data.description}
|
|
2936
|
+
|
|
2937
|
+
## \u590D\u73B0\u6B65\u9AA4
|
|
2938
|
+
${data.reproduction}
|
|
2939
|
+
|
|
2940
|
+
## \u5F71\u54CD\u8303\u56F4
|
|
2941
|
+
${data.scope}
|
|
2942
|
+
|
|
2943
|
+
## \u5173\u8054 Spec
|
|
2944
|
+
${data.relatedSpec ? `- ${data.relatedSpec}` : "- (\u65E0)"}
|
|
2945
|
+
|
|
2946
|
+
## \u4FEE\u590D\u65B9\u6848
|
|
2947
|
+
- [ ] \u5206\u6790\u6839\u672C\u539F\u56E0
|
|
2948
|
+
- [ ] \u8BBE\u8BA1\u4FEE\u590D\u65B9\u6848
|
|
2949
|
+
- [ ] \u5B9E\u65BD\u4FEE\u590D
|
|
2950
|
+
- [ ] \u7F16\u5199\u6D4B\u8BD5
|
|
2951
|
+
- [ ] \u9A8C\u8BC1\u4FEE\u590D
|
|
2952
|
+
|
|
2953
|
+
## \u9A8C\u8BC1\u6E05\u5355
|
|
2954
|
+
- [ ] \u672C\u5730\u6D4B\u8BD5\u901A\u8FC7
|
|
2955
|
+
- [ ] \u5355\u5143\u6D4B\u8BD5\u8986\u76D6
|
|
2956
|
+
- [ ] \u56DE\u5F52\u6D4B\u8BD5\u901A\u8FC7
|
|
2957
|
+
- [ ] \u66F4\u65B0\u76F8\u5173\u6587\u6863
|
|
2958
|
+
|
|
2959
|
+
---
|
|
2960
|
+
*\u521B\u5EFA\u4E8E: ${data.timestamp} by team-cli*
|
|
2961
|
+
`;
|
|
2962
|
+
}
|
|
2963
|
+
function formatHotfixDocument(data) {
|
|
2964
|
+
return `# Hotfix: ${data.description}
|
|
2965
|
+
|
|
2966
|
+
## Hotfix \u4FE1\u606F
|
|
2967
|
+
- **ID**: ${data.id}
|
|
2968
|
+
- **\u521B\u5EFA\u65F6\u95F4**: ${data.timestamp}
|
|
2969
|
+
|
|
2970
|
+
## \u95EE\u9898\u63CF\u8FF0
|
|
2971
|
+
${data.description}
|
|
2972
|
+
|
|
2973
|
+
## \u5F71\u54CD\u8303\u56F4
|
|
2974
|
+
${data.scope}
|
|
2975
|
+
|
|
2976
|
+
## \u4E34\u65F6\u89E3\u51B3\u65B9\u6848
|
|
2977
|
+
${data.solution}
|
|
2978
|
+
|
|
2979
|
+
## \u4FEE\u590D\u5206\u652F
|
|
2980
|
+
\`\`\`
|
|
2981
|
+
git checkout ${data.branch}
|
|
2982
|
+
\`\`\`
|
|
2983
|
+
|
|
2984
|
+
## \u540E\u7EED\u6B65\u9AA4
|
|
2985
|
+
- [ ] \u5B9E\u65BD\u7D27\u6025\u4FEE\u590D
|
|
2986
|
+
- [ ] \u63A8\u9001\u5230\u8FDC\u7A0B
|
|
2987
|
+
- [ ] \u521B\u5EFA PR \u8FDB\u884C code review
|
|
2988
|
+
- [ ] \u7D27\u6025\u5408\u5E76\u5230\u4E3B\u5206\u652F
|
|
2989
|
+
- [ ] \u521B\u5EFA\u89C4\u8303 bugfix \u8BB0\u5F55
|
|
2990
|
+
- [ ] \u8BA1\u5212\u5F7B\u5E95\u4FEE\u590D\u65B9\u6848
|
|
2991
|
+
|
|
2992
|
+
---
|
|
2993
|
+
*\u521B\u5EFA\u4E8E: ${data.timestamp} by team-cli*
|
|
2994
|
+
`;
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
// src/commands/lint.ts
|
|
2998
|
+
import { Command as Command7 } from "commander";
|
|
2999
|
+
import { execa as execa2 } from "execa";
|
|
3000
|
+
var lintCommand = new Command7("lint").option("--fix", "\u81EA\u52A8\u4FEE\u590D\u95EE\u9898").description("\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5 (\u524D\u7AEF + \u540E\u7AEF)").action(async (options) => {
|
|
3001
|
+
try {
|
|
3002
|
+
logger.header("\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5");
|
|
3003
|
+
logger.newLine();
|
|
3004
|
+
const hasTechStack = await FileUtils.exists("TECH_STACK.md");
|
|
3005
|
+
if (!hasTechStack) {
|
|
3006
|
+
logger.error("\u5F53\u524D\u76EE\u5F55\u4E0D\u662F\u4E00\u4E2A\u6709\u6548\u7684 team-cli \u9879\u76EE");
|
|
3007
|
+
logger.info("\u8BF7\u5148\u8FD0\u884C 'team-cli init <project-name>' \u6216\u5207\u6362\u5230\u9879\u76EE\u76EE\u5F55");
|
|
3008
|
+
process.exit(1);
|
|
3009
|
+
}
|
|
3010
|
+
const results = {
|
|
3011
|
+
frontend: { exists: false, passed: false, errors: [] },
|
|
3012
|
+
backend: { exists: false, passed: false, errors: [] }
|
|
3013
|
+
};
|
|
3014
|
+
const frontendExists = await FileUtils.exists("frontend/package.json");
|
|
3015
|
+
if (frontendExists) {
|
|
3016
|
+
results.frontend.exists = true;
|
|
3017
|
+
try {
|
|
3018
|
+
logger.step("\u68C0\u67E5\u524D\u7AEF\u4EE3\u7801...");
|
|
3019
|
+
if (options.fix) {
|
|
3020
|
+
await execa2("npm", ["run", "lint", "--", "--fix"], {
|
|
3021
|
+
cwd: "frontend",
|
|
3022
|
+
stdio: "inherit"
|
|
3023
|
+
});
|
|
3024
|
+
} else {
|
|
3025
|
+
await execa2("npm", ["run", "lint"], {
|
|
3026
|
+
cwd: "frontend",
|
|
3027
|
+
stdio: "inherit"
|
|
3028
|
+
});
|
|
3029
|
+
}
|
|
3030
|
+
await execa2("npx", ["tsc", "--noEmit"], {
|
|
3031
|
+
cwd: "frontend",
|
|
3032
|
+
stdio: "pipe"
|
|
3033
|
+
});
|
|
3034
|
+
results.frontend.passed = true;
|
|
3035
|
+
logger.success("\u524D\u7AEF\u4EE3\u7801\u68C0\u67E5\u901A\u8FC7");
|
|
3036
|
+
} catch (error) {
|
|
3037
|
+
results.frontend.errors.push(error.message);
|
|
3038
|
+
logger.error("\u524D\u7AEF\u4EE3\u7801\u68C0\u67E5\u5931\u8D25");
|
|
3039
|
+
}
|
|
3040
|
+
} else {
|
|
3041
|
+
logger.info("\u672A\u627E\u5230\u524D\u7AEF\u9879\u76EE (frontend/package.json)");
|
|
3042
|
+
}
|
|
3043
|
+
logger.newLine();
|
|
3044
|
+
const backendExists = await FileUtils.exists("backend/build.gradle") || await FileUtils.exists("backend/pom.xml");
|
|
3045
|
+
if (backendExists) {
|
|
3046
|
+
results.backend.exists = true;
|
|
3047
|
+
const hasGradle = await FileUtils.exists("backend/build.gradle") || await FileUtils.exists("backend/build.gradle.kts");
|
|
3048
|
+
const hasMaven = await FileUtils.exists("backend/pom.xml");
|
|
3049
|
+
try {
|
|
3050
|
+
logger.step("\u68C0\u67E5\u540E\u7AEF\u4EE3\u7801...");
|
|
3051
|
+
if (hasGradle) {
|
|
3052
|
+
if (options.fix) {
|
|
3053
|
+
await execa2("./gradlew", ["spotlessApply"], {
|
|
3054
|
+
cwd: "backend",
|
|
3055
|
+
stdio: "inherit"
|
|
3056
|
+
});
|
|
3057
|
+
}
|
|
3058
|
+
await execa2("./gradlew", ["checkstyleMain", "compileJava"], {
|
|
3059
|
+
cwd: "backend",
|
|
3060
|
+
stdio: "inherit"
|
|
3061
|
+
});
|
|
3062
|
+
} else if (hasMaven) {
|
|
3063
|
+
if (options.fix) {
|
|
3064
|
+
await execa2("./mvnw", ["spotless:apply"], {
|
|
3065
|
+
cwd: "backend",
|
|
3066
|
+
stdio: "inherit"
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
await execa2("./mvnw", ["checkstyle:check", "compile"], {
|
|
3070
|
+
cwd: "backend",
|
|
3071
|
+
stdio: "inherit"
|
|
3072
|
+
});
|
|
3073
|
+
}
|
|
3074
|
+
results.backend.passed = true;
|
|
3075
|
+
logger.success("\u540E\u7AEF\u4EE3\u7801\u68C0\u67E5\u901A\u8FC7");
|
|
3076
|
+
} catch (error) {
|
|
3077
|
+
results.backend.errors.push(error.message);
|
|
3078
|
+
logger.error("\u540E\u7AEF\u4EE3\u7801\u68C0\u67E5\u5931\u8D25");
|
|
3079
|
+
}
|
|
3080
|
+
} else {
|
|
3081
|
+
logger.info("\u672A\u627E\u5230\u540E\u7AEF\u9879\u76EE (build.gradle \u6216 pom.xml)");
|
|
3082
|
+
}
|
|
3083
|
+
logger.newLine();
|
|
3084
|
+
logger.separator("=", 50);
|
|
3085
|
+
logger.newLine();
|
|
3086
|
+
let hasErrors = false;
|
|
3087
|
+
if (results.frontend.exists) {
|
|
3088
|
+
if (results.frontend.passed) {
|
|
3089
|
+
logger.success("\u524D\u7AEF: \u2713 \u901A\u8FC7");
|
|
3090
|
+
} else {
|
|
3091
|
+
logger.error("\u524D\u7AEF: \u2717 \u5931\u8D25");
|
|
3092
|
+
hasErrors = true;
|
|
3093
|
+
}
|
|
3094
|
+
}
|
|
3095
|
+
if (results.backend.exists) {
|
|
3096
|
+
if (results.backend.passed) {
|
|
3097
|
+
logger.success("\u540E\u7AEF: \u2713 \u901A\u8FC7");
|
|
3098
|
+
} else {
|
|
3099
|
+
logger.error("\u540E\u7AEF: \u2717 \u5931\u8D25");
|
|
3100
|
+
hasErrors = true;
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
logger.newLine();
|
|
3104
|
+
if (hasErrors) {
|
|
3105
|
+
logger.error("\u4EE3\u7801\u68C0\u67E5\u5931\u8D25");
|
|
3106
|
+
logger.info("\u8FD0\u884C 'team-cli lint --fix' \u5C1D\u8BD5\u81EA\u52A8\u4FEE\u590D");
|
|
3107
|
+
process.exit(1);
|
|
3108
|
+
} else {
|
|
3109
|
+
logger.success("\u4EE3\u7801\u68C0\u67E5\u5168\u90E8\u901A\u8FC7");
|
|
3110
|
+
}
|
|
3111
|
+
} catch (error) {
|
|
3112
|
+
logger.error(`\u4EE3\u7801\u68C0\u67E5\u5931\u8D25: ${error.message}`);
|
|
3113
|
+
if (process.env.DEBUG) {
|
|
3114
|
+
console.error(error);
|
|
3115
|
+
}
|
|
3116
|
+
process.exit(1);
|
|
3117
|
+
}
|
|
3118
|
+
});
|
|
3119
|
+
|
|
3120
|
+
// src/commands/status.ts
|
|
3121
|
+
import { Command as Command8 } from "commander";
|
|
3122
|
+
import path8 from "path";
|
|
3123
|
+
var statusCommand = new Command8("status").description("\u67E5\u770B\u9879\u76EE\u72B6\u6001").action(async () => {
|
|
3124
|
+
try {
|
|
3125
|
+
logger.header("\u9879\u76EE\u72B6\u6001");
|
|
3126
|
+
logger.newLine();
|
|
3127
|
+
const hasTechStack = await FileUtils.exists("TECH_STACK.md");
|
|
3128
|
+
if (!hasTechStack) {
|
|
3129
|
+
logger.error("\u5F53\u524D\u76EE\u5F55\u4E0D\u662F\u4E00\u4E2A\u6709\u6548\u7684 team-cli \u9879\u76EE");
|
|
3130
|
+
logger.info("\u8BF7\u5148\u8FD0\u884C 'team-cli init <project-name>' \u6216\u5207\u6362\u5230\u9879\u76EE\u76EE\u5F55");
|
|
3131
|
+
process.exit(1);
|
|
3132
|
+
}
|
|
3133
|
+
await displayProjectInfo();
|
|
3134
|
+
await displayFeatureInventory();
|
|
3135
|
+
await displayGitStatus();
|
|
3136
|
+
await displayRecentActivity();
|
|
3137
|
+
} catch (error) {
|
|
3138
|
+
logger.error(`\u83B7\u53D6\u72B6\u6001\u5931\u8D25: ${error.message}`);
|
|
3139
|
+
if (process.env.DEBUG) {
|
|
3140
|
+
console.error(error);
|
|
3141
|
+
}
|
|
3142
|
+
process.exit(1);
|
|
3143
|
+
}
|
|
3144
|
+
});
|
|
3145
|
+
async function displayProjectInfo() {
|
|
3146
|
+
logger.info("\u9879\u76EE\u4FE1\u606F:");
|
|
3147
|
+
logger.newLine();
|
|
3148
|
+
if (await FileUtils.exists("AI_MEMORY.md")) {
|
|
3149
|
+
const content = await FileUtils.read("AI_MEMORY.md");
|
|
3150
|
+
const projectName = content.match(/项目名称.*[::]\s*(.+)/);
|
|
3151
|
+
const currentPhase = content.match(/当前阶段.*[::]\s*(.+)/);
|
|
3152
|
+
const lastUpdated = content.match(/最后更新.*[::]\s*(.+)/);
|
|
3153
|
+
if (projectName) logger.step(`\u540D\u79F0: ${projectName[1].trim()}`);
|
|
3154
|
+
if (currentPhase) logger.step(`\u9636\u6BB5: ${currentPhase[1].trim()}`);
|
|
3155
|
+
if (lastUpdated) logger.step(`\u66F4\u65B0: ${lastUpdated[1].trim()}`);
|
|
3156
|
+
} else {
|
|
3157
|
+
logger.step("AI_MEMORY.md \u4E0D\u5B58\u5728");
|
|
3158
|
+
}
|
|
3159
|
+
logger.newLine();
|
|
3160
|
+
}
|
|
3161
|
+
async function displayFeatureInventory() {
|
|
3162
|
+
logger.info("\u529F\u80FD\u6E05\u5355:");
|
|
3163
|
+
logger.newLine();
|
|
3164
|
+
const specDir = "docs/specs";
|
|
3165
|
+
const exists = await FileUtils.exists(specDir);
|
|
3166
|
+
if (!exists) {
|
|
3167
|
+
logger.info(" (\u65E0 spec \u6587\u4EF6)");
|
|
3168
|
+
logger.newLine();
|
|
3169
|
+
return;
|
|
3170
|
+
}
|
|
3171
|
+
const files = await FileUtils.findFiles("*.md", specDir);
|
|
3172
|
+
const specs = files.filter((f) => !f.includes("template"));
|
|
3173
|
+
if (specs.length === 0) {
|
|
3174
|
+
logger.info(" (\u65E0 spec \u6587\u4EF6)");
|
|
3175
|
+
logger.newLine();
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
const inventory = [];
|
|
3179
|
+
for (const file of specs) {
|
|
3180
|
+
const filePath = path8.join(specDir, file);
|
|
3181
|
+
const content = await FileUtils.read(filePath);
|
|
3182
|
+
const status = parseSpecStatus2(content);
|
|
3183
|
+
inventory.push({
|
|
3184
|
+
name: file.replace(".md", ""),
|
|
3185
|
+
status,
|
|
3186
|
+
progress: getProgress(content)
|
|
3187
|
+
});
|
|
3188
|
+
}
|
|
3189
|
+
const tableData = inventory.map((item) => [
|
|
3190
|
+
item.name,
|
|
3191
|
+
item.status,
|
|
3192
|
+
item.progress
|
|
3193
|
+
]);
|
|
3194
|
+
logger.table(["\u529F\u80FD", "\u72B6\u6001", "\u8FDB\u5EA6"], tableData);
|
|
3195
|
+
logger.newLine();
|
|
3196
|
+
const completed = inventory.filter((i) => i.status === "\u5DF2\u5B8C\u6210").length;
|
|
3197
|
+
const inProgress = inventory.filter((i) => i.status === "\u8FDB\u884C\u4E2D").length;
|
|
3198
|
+
const pending = inventory.filter((i) => i.status === "\u672A\u5F00\u59CB").length;
|
|
3199
|
+
logger.info(`\u603B\u8BA1: ${inventory.length} | \u5DF2\u5B8C\u6210: ${completed} | \u8FDB\u884C\u4E2D: ${inProgress} | \u672A\u5F00\u59CB: ${pending}`);
|
|
3200
|
+
logger.newLine();
|
|
3201
|
+
}
|
|
3202
|
+
async function displayGitStatus() {
|
|
3203
|
+
const isRepo = await GitUtils.isGitRepo();
|
|
3204
|
+
if (!isRepo) {
|
|
3205
|
+
logger.info("Git \u72B6\u6001: \u975E Git \u4ED3\u5E93");
|
|
3206
|
+
logger.newLine();
|
|
3207
|
+
return;
|
|
3208
|
+
}
|
|
3209
|
+
try {
|
|
3210
|
+
const branch = await GitUtils.getCurrentBranch();
|
|
3211
|
+
const commit = await GitUtils.getCurrentCommit();
|
|
3212
|
+
logger.info("Git \u72B6\u6001:");
|
|
3213
|
+
logger.step(`\u5206\u652F: ${branch}`);
|
|
3214
|
+
logger.step(`\u63D0\u4EA4: ${commit}`);
|
|
3215
|
+
logger.newLine();
|
|
3216
|
+
} catch {
|
|
3217
|
+
logger.info("Git \u72B6\u6001: \u65E0\u6CD5\u83B7\u53D6");
|
|
3218
|
+
logger.newLine();
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
async function displayRecentActivity() {
|
|
3222
|
+
logger.info("\u6700\u8FD1\u6D3B\u52A8:");
|
|
3223
|
+
logger.newLine();
|
|
3224
|
+
const sessionDir = "docs/sessions";
|
|
3225
|
+
const exists = await FileUtils.exists(sessionDir);
|
|
3226
|
+
if (!exists) {
|
|
3227
|
+
logger.info(" (\u65E0\u4F1A\u8BDD\u8BB0\u5F55)");
|
|
3228
|
+
logger.newLine();
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
3231
|
+
const files = await FileUtils.findFiles("*.md", sessionDir);
|
|
3232
|
+
if (files.length === 0) {
|
|
3233
|
+
logger.info(" (\u65E0\u4F1A\u8BDD\u8BB0\u5F55)");
|
|
3234
|
+
logger.newLine();
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
3237
|
+
const sorted = files.sort().reverse().slice(0, 5);
|
|
3238
|
+
for (const file of sorted) {
|
|
3239
|
+
const filePath = path8.join(sessionDir, file);
|
|
3240
|
+
const stat = await FileUtils.read(filePath);
|
|
3241
|
+
const specMatch = stat.match(/\*\*Spec\*\*:\s*(.+)/);
|
|
3242
|
+
const spec = specMatch ? specMatch[1].trim() : "\u672A\u77E5";
|
|
3243
|
+
const match = file.match(/(\d{4}-\d{2}-\d{2})/);
|
|
3244
|
+
const date = match ? match[1] : "\u672A\u77E5";
|
|
3245
|
+
logger.step(`${date} - ${spec}`);
|
|
3246
|
+
}
|
|
3247
|
+
logger.newLine();
|
|
3248
|
+
if (files.length > 5) {
|
|
3249
|
+
logger.info(`(\u8FD8\u6709 ${files.length - 5} \u4E2A\u5386\u53F2\u4F1A\u8BDD\u8BB0\u5F55)`);
|
|
3250
|
+
logger.newLine();
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
function parseSpecStatus2(spec) {
|
|
3254
|
+
const statusMatch = spec.match(/状态.*[::]\s*(.+)/);
|
|
3255
|
+
if (statusMatch) {
|
|
3256
|
+
const status = statusMatch[1].replace(/\*\*/g, "").trim();
|
|
3257
|
+
if (status.includes("\u5DF2\u5B8C\u6210")) return "\u2713 \u5DF2\u5B8C\u6210";
|
|
3258
|
+
if (status.includes("\u8FDB\u884C\u4E2D")) return "\u27F3 \u8FDB\u884C\u4E2D";
|
|
3259
|
+
if (status.includes("\u5DF2\u62C6\u5206")) return "\u25C9 \u5DF2\u62C6\u5206";
|
|
3260
|
+
}
|
|
3261
|
+
return "\u25CB \u672A\u5F00\u59CB";
|
|
3262
|
+
}
|
|
3263
|
+
function getProgress(spec) {
|
|
3264
|
+
const milestoneMatches = spec.match(/###\s+Milestone\s+\d+:/g);
|
|
3265
|
+
const milestones = milestoneMatches ? milestoneMatches.length : 0;
|
|
3266
|
+
const todoMatches = spec.match(/-\s+\[[ x ]\]/g);
|
|
3267
|
+
const totalTodos = todoMatches ? todoMatches.length : 0;
|
|
3268
|
+
const completedMatches = spec.match(/-\s+\[x\]/g);
|
|
3269
|
+
const completedTodos = completedMatches ? completedMatches.length : 0;
|
|
3270
|
+
if (totalTodos === 0) {
|
|
3271
|
+
return "-";
|
|
3272
|
+
}
|
|
3273
|
+
return `${completedTodos}/${totalTodos}`;
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
// src/commands/detect-deps.ts
|
|
3277
|
+
import { Command as Command9 } from "commander";
|
|
3278
|
+
import path9 from "path";
|
|
3279
|
+
import inquirer6 from "inquirer";
|
|
3280
|
+
var detectDepsCommand = new Command9("detect-deps").argument("[spec-file]", "Spec \u6587\u4EF6\u8DEF\u5F84").description("\u68C0\u6D4B\u4F9D\u8D56\u5173\u7CFB").action(async (specFile) => {
|
|
3281
|
+
try {
|
|
3282
|
+
logger.header("\u68C0\u6D4B\u4F9D\u8D56\u5173\u7CFB");
|
|
3283
|
+
logger.newLine();
|
|
3284
|
+
const hasTechStack = await FileUtils.exists("TECH_STACK.md");
|
|
3285
|
+
if (!hasTechStack) {
|
|
3286
|
+
logger.error("\u5F53\u524D\u76EE\u5F55\u4E0D\u662F\u4E00\u4E2A\u6709\u6548\u7684 team-cli \u9879\u76EE");
|
|
3287
|
+
logger.info("\u8BF7\u5148\u8FD0\u884C 'team-cli init <project-name>' \u6216\u5207\u6362\u5230\u9879\u76EE\u76EE\u5F55");
|
|
3288
|
+
process.exit(1);
|
|
3289
|
+
}
|
|
3290
|
+
if (!specFile) {
|
|
3291
|
+
logger.info("\u672A\u6307\u5B9A spec \u6587\u4EF6\uFF0C\u5C06\u626B\u63CF\u6240\u6709 specs...");
|
|
3292
|
+
logger.newLine();
|
|
3293
|
+
const specsDir = "docs/specs";
|
|
3294
|
+
const exists = await FileUtils.exists(specsDir);
|
|
3295
|
+
if (!exists) {
|
|
3296
|
+
logger.error("\u672A\u627E\u5230 spec \u6587\u4EF6");
|
|
3297
|
+
process.exit(1);
|
|
3298
|
+
}
|
|
3299
|
+
const files = await FileUtils.findFiles("*.md", specsDir);
|
|
3300
|
+
const specs = files.filter((f) => f !== "template.md");
|
|
3301
|
+
if (specs.length === 0) {
|
|
3302
|
+
logger.error("\u672A\u627E\u5230 spec \u6587\u4EF6");
|
|
3303
|
+
process.exit(1);
|
|
3304
|
+
}
|
|
3305
|
+
for (const spec of specs) {
|
|
3306
|
+
const specPath = path9.join(specsDir, spec);
|
|
3307
|
+
logger.step(`\u5904\u7406: ${spec}`);
|
|
3308
|
+
await detectDependencies(specPath);
|
|
3309
|
+
logger.newLine();
|
|
3310
|
+
}
|
|
3311
|
+
} else {
|
|
3312
|
+
const exists = await FileUtils.exists(specFile);
|
|
3313
|
+
if (!exists) {
|
|
3314
|
+
logger.error(`Spec \u6587\u4EF6\u4E0D\u5B58\u5728: ${specFile}`);
|
|
3315
|
+
process.exit(1);
|
|
3316
|
+
}
|
|
3317
|
+
await detectDependencies(specFile);
|
|
3318
|
+
}
|
|
3319
|
+
logger.header("\u4F9D\u8D56\u68C0\u6D4B\u5B8C\u6210");
|
|
3320
|
+
} catch (error) {
|
|
3321
|
+
logger.error(`\u4F9D\u8D56\u68C0\u6D4B\u5931\u8D25: ${error.message}`);
|
|
3322
|
+
if (process.env.DEBUG) {
|
|
3323
|
+
console.error(error);
|
|
3324
|
+
}
|
|
3325
|
+
process.exit(1);
|
|
3326
|
+
}
|
|
3327
|
+
});
|
|
3328
|
+
async function detectDependencies(specFile) {
|
|
3329
|
+
logger.step("\u81EA\u52A8\u68C0\u6D4B\u4F9D\u8D56\u5173\u7CFB...");
|
|
3330
|
+
const projectDir = ".";
|
|
3331
|
+
const allDeps = /* @__PURE__ */ new Set();
|
|
3332
|
+
const backendDir = path9.join(projectDir, "backend");
|
|
3333
|
+
const backendExists = await FileUtils.exists(backendDir);
|
|
3334
|
+
if (backendExists) {
|
|
3335
|
+
const apiDeps = await scanBackendApiCalls(backendDir);
|
|
3336
|
+
apiDeps.forEach((d) => allDeps.add(d));
|
|
3337
|
+
const entityDeps = await scanBackendEntityRelations(backendDir);
|
|
3338
|
+
entityDeps.forEach((d) => allDeps.add(d));
|
|
3339
|
+
const serviceDeps = await scanBackendServiceRefs(backendDir);
|
|
3340
|
+
serviceDeps.forEach((d) => allDeps.add(d));
|
|
3341
|
+
}
|
|
3342
|
+
const frontendDir = path9.join(projectDir, "frontend");
|
|
3343
|
+
const frontendExists = await FileUtils.exists(frontendDir);
|
|
3344
|
+
if (frontendExists) {
|
|
3345
|
+
const frontendDeps = await scanFrontendApiCalls(frontendDir);
|
|
3346
|
+
frontendDeps.forEach((d) => allDeps.add(d));
|
|
3347
|
+
}
|
|
3348
|
+
if (allDeps.size === 0) {
|
|
3349
|
+
logger.info("\u672A\u68C0\u6D4B\u5230\u660E\u786E\u7684\u4F9D\u8D56\u5173\u7CFB");
|
|
3350
|
+
return;
|
|
3351
|
+
}
|
|
3352
|
+
const detectedSpecs = [];
|
|
3353
|
+
for (const dep of allDeps) {
|
|
3354
|
+
const matchedSpec = await findSpecByKeyword(dep, "docs/specs");
|
|
3355
|
+
if (matchedSpec && !detectedSpecs.includes(matchedSpec)) {
|
|
3356
|
+
detectedSpecs.push(matchedSpec);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
if (detectedSpecs.length === 0) {
|
|
3360
|
+
logger.info("\u68C0\u6D4B\u5230\u4F9D\u8D56\uFF0C\u4F46\u672A\u627E\u5230\u5BF9\u5E94\u7684 spec \u6587\u4EF6");
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
logger.success(`\u68C0\u6D4B\u5230 ${detectedSpecs.length} \u4E2A\u6F5C\u5728\u4F9D\u8D56:`);
|
|
3364
|
+
for (const spec of detectedSpecs) {
|
|
3365
|
+
logger.step(`- ${spec}`);
|
|
3366
|
+
}
|
|
3367
|
+
logger.newLine();
|
|
3368
|
+
const answers = await inquirer6.prompt([
|
|
3369
|
+
{
|
|
3370
|
+
type: "confirm",
|
|
3371
|
+
name: "autoUpdate",
|
|
3372
|
+
message: "\u662F\u5426\u81EA\u52A8\u66F4\u65B0\u4F9D\u8D56\u5173\u7CFB\u5230 spec \u6587\u4EF6?",
|
|
3373
|
+
default: true
|
|
3374
|
+
}
|
|
3375
|
+
]);
|
|
3376
|
+
if (answers.autoUpdate) {
|
|
3377
|
+
await updateSpecDependencies(specFile, detectedSpecs);
|
|
3378
|
+
logger.success("\u4F9D\u8D56\u5173\u7CFB\u5DF2\u66F4\u65B0");
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
async function scanBackendApiCalls(backendDir) {
|
|
3382
|
+
const deps = [];
|
|
3383
|
+
const srcDir = path9.join(backendDir, "src");
|
|
3384
|
+
try {
|
|
3385
|
+
const javaFiles = await FileUtils.findFiles("*.java", srcDir);
|
|
3386
|
+
for (const file of javaFiles) {
|
|
3387
|
+
const filePath = path9.join(srcDir, file);
|
|
3388
|
+
const content = await FileUtils.read(filePath);
|
|
3389
|
+
const pathRegex = /"(\/api\/[^"]+)"/g;
|
|
3390
|
+
let match;
|
|
3391
|
+
while ((match = pathRegex.exec(content)) !== null) {
|
|
3392
|
+
const fullPath = match[1];
|
|
3393
|
+
const parts = fullPath.split("/").filter(Boolean);
|
|
3394
|
+
if (parts.length > 1) {
|
|
3395
|
+
deps.push(parts[1]);
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
} catch (error) {
|
|
3400
|
+
}
|
|
3401
|
+
return deps;
|
|
3402
|
+
}
|
|
3403
|
+
async function scanBackendEntityRelations(backendDir) {
|
|
3404
|
+
const deps = [];
|
|
3405
|
+
const srcDir = path9.join(backendDir, "src");
|
|
3406
|
+
try {
|
|
3407
|
+
const javaFiles = await FileUtils.findFiles("*.java", srcDir);
|
|
3408
|
+
for (const file of javaFiles) {
|
|
3409
|
+
const filePath = path9.join(srcDir, file);
|
|
3410
|
+
const content = await FileUtils.read(filePath);
|
|
3411
|
+
if (content.includes("@JoinColumn") || content.includes("@ManyToOne") || content.includes("@OneToMany")) {
|
|
3412
|
+
const typeRegex = /type\s*=\s*(\w+)/g;
|
|
3413
|
+
let match;
|
|
3414
|
+
while ((match = typeRegex.exec(content)) !== null) {
|
|
3415
|
+
deps.push(match[1].toLowerCase());
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
} catch (error) {
|
|
3420
|
+
}
|
|
3421
|
+
return deps;
|
|
3422
|
+
}
|
|
3423
|
+
async function scanBackendServiceRefs(backendDir) {
|
|
3424
|
+
const deps = [];
|
|
3425
|
+
const srcDir = path9.join(backendDir, "src");
|
|
3426
|
+
try {
|
|
3427
|
+
const javaFiles = await FileUtils.findFiles("*.java", srcDir);
|
|
3428
|
+
for (const file of javaFiles) {
|
|
3429
|
+
const filePath = path9.join(srcDir, file);
|
|
3430
|
+
const content = await FileUtils.read(filePath);
|
|
3431
|
+
const serviceRegex = /private\s+(\w+)Service/g;
|
|
3432
|
+
let match;
|
|
3433
|
+
while ((match = serviceRegex.exec(content)) !== null) {
|
|
3434
|
+
const serviceName = match[1];
|
|
3435
|
+
const moduleName = serviceName.replace(/Service$/, "").toLowerCase();
|
|
3436
|
+
deps.push(moduleName);
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
} catch (error) {
|
|
3440
|
+
}
|
|
3441
|
+
return deps;
|
|
3442
|
+
}
|
|
3443
|
+
async function scanFrontendApiCalls(frontendDir) {
|
|
3444
|
+
const deps = [];
|
|
3445
|
+
const srcDir = path9.join(frontendDir, "src");
|
|
3446
|
+
try {
|
|
3447
|
+
const tsFiles = await FileUtils.findFiles("*.{ts,tsx,js,jsx}", srcDir);
|
|
3448
|
+
for (const file of tsFiles) {
|
|
3449
|
+
const filePath = path9.join(srcDir, file);
|
|
3450
|
+
const content = await FileUtils.read(filePath);
|
|
3451
|
+
const pathRegex = /"(\/api\/[^"]+)"/g;
|
|
3452
|
+
let match;
|
|
3453
|
+
while ((match = pathRegex.exec(content)) !== null) {
|
|
3454
|
+
const fullPath = match[1];
|
|
3455
|
+
const parts = fullPath.split("/").filter(Boolean);
|
|
3456
|
+
if (parts.length > 1) {
|
|
3457
|
+
deps.push(parts[1]);
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
} catch (error) {
|
|
3462
|
+
}
|
|
3463
|
+
return deps;
|
|
3464
|
+
}
|
|
3465
|
+
async function findSpecByKeyword(keyword, specsDir) {
|
|
3466
|
+
const exists = await FileUtils.exists(specsDir);
|
|
3467
|
+
if (!exists) {
|
|
3468
|
+
return null;
|
|
3469
|
+
}
|
|
3470
|
+
try {
|
|
3471
|
+
const files = await FileUtils.findFiles("*.md", specsDir);
|
|
3472
|
+
for (const file of files) {
|
|
3473
|
+
if (file === "template.md") continue;
|
|
3474
|
+
const name = file.replace(".md", "");
|
|
3475
|
+
if (name.includes(keyword) || keyword.includes(name)) {
|
|
3476
|
+
return name;
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
} catch (error) {
|
|
3480
|
+
}
|
|
3481
|
+
return null;
|
|
3482
|
+
}
|
|
3483
|
+
async function updateSpecDependencies(specFile, deps) {
|
|
3484
|
+
let content = await FileUtils.read(specFile);
|
|
3485
|
+
if (!content.includes("## \u4F9D\u8D56\u5173\u7CFB")) {
|
|
3486
|
+
const targetSection = "## \u80CC\u666F\u4E0E\u76EE\u6807";
|
|
3487
|
+
const insertIndex = content.indexOf(targetSection);
|
|
3488
|
+
if (insertIndex !== -1) {
|
|
3489
|
+
const depsSection = `
|
|
3490
|
+
## \u4F9D\u8D56\u5173\u7CFB
|
|
3491
|
+
|
|
3492
|
+
**\u524D\u7F6E\u4F9D\u8D56**:
|
|
3493
|
+
${deps.map((d) => ` - [x] ${d}`).join("\n")}
|
|
3494
|
+
|
|
3495
|
+
**\u88AB\u4F9D\u8D56\u4E8E**:
|
|
3496
|
+
- (\u81EA\u52A8\u751F\u6210\uFF0C\u8868\u793A\u54EA\u4E9B spec \u4F9D\u8D56\u672C\u529F\u80FD)
|
|
3497
|
+
|
|
3498
|
+
`;
|
|
3499
|
+
content = content.slice(0, insertIndex) + depsSection + content.slice(insertIndex);
|
|
3500
|
+
}
|
|
3501
|
+
} else {
|
|
3502
|
+
const lines = content.split("\n");
|
|
3503
|
+
let inDeps = false;
|
|
3504
|
+
const result = [];
|
|
3505
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3506
|
+
const line = lines[i];
|
|
3507
|
+
if (line.startsWith("## \u4F9D\u8D56\u5173\u7CFB")) {
|
|
3508
|
+
inDeps = true;
|
|
3509
|
+
result.push(line);
|
|
3510
|
+
result.push("");
|
|
3511
|
+
result.push("**\u524D\u7F6E\u4F9D\u8D56**:");
|
|
3512
|
+
for (const dep of deps) {
|
|
3513
|
+
result.push(` - [x] ${dep}`);
|
|
3514
|
+
}
|
|
3515
|
+
result.push("");
|
|
3516
|
+
result.push("**\u88AB\u4F9D\u8D56\u4E8E**:");
|
|
3517
|
+
result.push(" - (\u81EA\u52A8\u751F\u6210\uFF0C\u8868\u793A\u54EA\u4E9B spec \u4F9D\u8D56\u672C\u529F\u80FD)");
|
|
3518
|
+
continue;
|
|
3519
|
+
}
|
|
3520
|
+
if (inDeps) {
|
|
3521
|
+
if (line.startsWith("## ") && !line.startsWith("## \u4F9D\u8D56\u5173\u7CFB")) {
|
|
3522
|
+
inDeps = false;
|
|
3523
|
+
result.push(line);
|
|
3524
|
+
}
|
|
3525
|
+
continue;
|
|
3526
|
+
}
|
|
3527
|
+
result.push(line);
|
|
3528
|
+
}
|
|
3529
|
+
content = result.join("\n");
|
|
3530
|
+
}
|
|
3531
|
+
await FileUtils.write(specFile, content);
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
// src/commands/sync-memory.ts
|
|
3535
|
+
import { Command as Command10 } from "commander";
|
|
3536
|
+
import path10 from "path";
|
|
3537
|
+
var syncMemoryCommand = new Command10("sync-memory").description("\u540C\u6B65 AI_MEMORY.md").action(async () => {
|
|
3538
|
+
try {
|
|
3539
|
+
logger.header("\u540C\u6B65 AI_MEMORY.md");
|
|
3540
|
+
logger.newLine();
|
|
3541
|
+
const hasTechStack = await FileUtils.exists("TECH_STACK.md");
|
|
3542
|
+
if (!hasTechStack) {
|
|
3543
|
+
logger.error("\u5F53\u524D\u76EE\u5F55\u4E0D\u662F\u4E00\u4E2A\u6709\u6548\u7684 team-cli \u9879\u76EE");
|
|
3544
|
+
logger.info("\u8BF7\u5148\u8FD0\u884C 'team-cli init <project-name>' \u6216\u5207\u6362\u5230\u9879\u76EE\u76EE\u5F55");
|
|
3545
|
+
process.exit(1);
|
|
3546
|
+
}
|
|
3547
|
+
const aiMemoryFile = "AI_MEMORY.md";
|
|
3548
|
+
const exists = await FileUtils.exists(aiMemoryFile);
|
|
3549
|
+
if (!exists) {
|
|
3550
|
+
logger.info("AI_MEMORY.md \u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u540C\u6B65");
|
|
3551
|
+
process.exit(0);
|
|
3552
|
+
}
|
|
3553
|
+
await syncFeatureInventory(aiMemoryFile, ".");
|
|
3554
|
+
await syncApiInventory(aiMemoryFile, ".");
|
|
3555
|
+
await syncDataModels(aiMemoryFile, ".");
|
|
3556
|
+
await updateSyncTime(aiMemoryFile);
|
|
3557
|
+
logger.success("AI_MEMORY.md \u5DF2\u540C\u6B65");
|
|
3558
|
+
} catch (error) {
|
|
3559
|
+
logger.error(`\u540C\u6B65\u5931\u8D25: ${error.message}`);
|
|
3560
|
+
if (process.env.DEBUG) {
|
|
3561
|
+
console.error(error);
|
|
3562
|
+
}
|
|
3563
|
+
process.exit(1);
|
|
3564
|
+
}
|
|
3565
|
+
});
|
|
3566
|
+
async function syncFeatureInventory(aiMemoryFile, projectDir) {
|
|
3567
|
+
logger.step("\u540C\u6B65\u529F\u80FD\u6E05\u5355...");
|
|
3568
|
+
const specsDir = path10.join(projectDir, "docs/specs");
|
|
3569
|
+
const exists = await FileUtils.exists(specsDir);
|
|
3570
|
+
if (!exists) {
|
|
3571
|
+
return;
|
|
3572
|
+
}
|
|
3573
|
+
const files = await FileUtils.findFiles("*.md", specsDir);
|
|
3574
|
+
const specs = files.filter((f) => f !== "template.md");
|
|
3575
|
+
const lines = [];
|
|
3576
|
+
lines.push("## \u529F\u80FD\u6E05\u5355 (Feature Inventory)");
|
|
3577
|
+
lines.push("");
|
|
3578
|
+
lines.push("| \u529F\u80FD | Spec \u6587\u4EF6 | \u72B6\u6001 | \u8FDB\u5EA6 | \u5B8C\u6210\u65E5\u671F | \u5907\u6CE8 |");
|
|
3579
|
+
lines.push("|------|----------|------|------|---------|------|");
|
|
3580
|
+
for (const specFile of specs) {
|
|
3581
|
+
const name = specFile.replace(".md", "");
|
|
3582
|
+
const displayName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
3583
|
+
const specPath = path10.join(specsDir, specFile);
|
|
3584
|
+
const content = await FileUtils.read(specPath);
|
|
3585
|
+
const status = parseSpecStatus3(content);
|
|
3586
|
+
const progress = getSpecProgress(content);
|
|
3587
|
+
let completionDate = "-";
|
|
3588
|
+
if (status === "\u2705 \u5DF2\u5B8C\u6210") {
|
|
3589
|
+
const dateMatch = content.match(/完成日期.*[::]\s*(.+)/);
|
|
3590
|
+
if (dateMatch) {
|
|
3591
|
+
completionDate = dateMatch[1].trim();
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
lines.push(`| ${displayName} | ${name}.md | ${status} | ${progress} | ${completionDate} | |`);
|
|
3595
|
+
}
|
|
3596
|
+
const newContent = lines.join("\n") + "\n";
|
|
3597
|
+
await replaceOrInsertSection(aiMemoryFile, "## \u529F\u80FD\u6E05\u5355", newContent);
|
|
3598
|
+
}
|
|
3599
|
+
async function syncApiInventory(aiMemoryFile, projectDir) {
|
|
3600
|
+
logger.step("\u540C\u6B65 API \u5217\u8868...");
|
|
3601
|
+
const backendDir = path10.join(projectDir, "backend");
|
|
3602
|
+
const exists = await FileUtils.exists(backendDir);
|
|
3603
|
+
if (!exists) {
|
|
3604
|
+
return;
|
|
3605
|
+
}
|
|
3606
|
+
const lines = [];
|
|
3607
|
+
lines.push("## API \u5217\u8868 (API Inventory)");
|
|
3608
|
+
lines.push("");
|
|
3609
|
+
lines.push("> \u672C\u90E8\u5206\u7531 team-cli \u81EA\u52A8\u626B\u63CF\u540E\u7AEF Controller \u751F\u6210");
|
|
3610
|
+
lines.push("");
|
|
3611
|
+
const srcDir = path10.join(backendDir, "src");
|
|
3612
|
+
const controllers = await FileUtils.findFiles("*Controller.java", srcDir);
|
|
3613
|
+
if (controllers.length === 0) {
|
|
3614
|
+
lines.push("\u6682\u65E0 API");
|
|
3615
|
+
} else {
|
|
3616
|
+
for (const controllerFile of controllers) {
|
|
3617
|
+
const controllerPath = path10.join(srcDir, controllerFile);
|
|
3618
|
+
const controllerName = controllerFile.replace(".java", "");
|
|
3619
|
+
const module = controllerName.replace(/Controller$/, "").toLowerCase();
|
|
3620
|
+
lines.push(`### ${module} \u6A21\u5757`);
|
|
3621
|
+
lines.push("");
|
|
3622
|
+
lines.push("| \u65B9\u6CD5 | \u8DEF\u5F84 | \u8BF4\u660E | \u72B6\u6001 | \u65E5\u671F |");
|
|
3623
|
+
lines.push("|------|------|------|------|------|");
|
|
3624
|
+
const apis = await scanControllerApis(controllerPath);
|
|
3625
|
+
for (const api of apis) {
|
|
3626
|
+
lines.push(`| ${api.method} | ${api.path} | ${api.description} | \u2705 | ${api.date} |`);
|
|
3627
|
+
}
|
|
3628
|
+
lines.push("");
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
const newContent = lines.join("\n");
|
|
3632
|
+
await replaceOrInsertSection(aiMemoryFile, "## API \u5217\u8868", newContent);
|
|
3633
|
+
}
|
|
3634
|
+
async function scanControllerApis(controllerPath) {
|
|
3635
|
+
const apis = [];
|
|
3636
|
+
const content = await FileUtils.read(controllerPath);
|
|
3637
|
+
let classPath = "";
|
|
3638
|
+
const classRequestMappingMatch = content.match(/@RequestMapping\("([^"]+)"\)/);
|
|
3639
|
+
if (classRequestMappingMatch) {
|
|
3640
|
+
classPath = classRequestMappingMatch[1];
|
|
3641
|
+
}
|
|
3642
|
+
const methodRegex = /@(GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)\("([^"]+)"\)\s*\n\s*public\s+(\w+)\s*\(([^)]*)\)/g;
|
|
3643
|
+
let match;
|
|
3644
|
+
while ((match = methodRegex.exec(content)) !== null) {
|
|
3645
|
+
const mappingType = match[1];
|
|
3646
|
+
const methodPath = match[2];
|
|
3647
|
+
const methodName = match[3];
|
|
3648
|
+
let httpMethod = "";
|
|
3649
|
+
switch (mappingType) {
|
|
3650
|
+
case "GetMapping":
|
|
3651
|
+
httpMethod = "GET";
|
|
3652
|
+
break;
|
|
3653
|
+
case "PostMapping":
|
|
3654
|
+
httpMethod = "POST";
|
|
3655
|
+
break;
|
|
3656
|
+
case "PutMapping":
|
|
3657
|
+
httpMethod = "PUT";
|
|
3658
|
+
break;
|
|
3659
|
+
case "DeleteMapping":
|
|
3660
|
+
httpMethod = "DELETE";
|
|
3661
|
+
break;
|
|
3662
|
+
case "PatchMapping":
|
|
3663
|
+
httpMethod = "PATCH";
|
|
3664
|
+
break;
|
|
3665
|
+
}
|
|
3666
|
+
let fullPath = methodPath;
|
|
3667
|
+
if (classPath && !methodPath.startsWith("/api")) {
|
|
3668
|
+
fullPath = `${classPath}${methodPath}`;
|
|
3669
|
+
}
|
|
3670
|
+
const description = extractMethodComment(content, methodName);
|
|
3671
|
+
apis.push({
|
|
3672
|
+
method: httpMethod,
|
|
3673
|
+
path: fullPath,
|
|
3674
|
+
description,
|
|
3675
|
+
date: DateUtils.format(/* @__PURE__ */ new Date(), "YYYY-MM-DD")
|
|
3676
|
+
});
|
|
3677
|
+
}
|
|
3678
|
+
return apis;
|
|
3679
|
+
}
|
|
3680
|
+
function extractMethodComment(content, methodName) {
|
|
3681
|
+
const methodIndex = content.indexOf(`${methodName}(`);
|
|
3682
|
+
if (methodIndex === -1) {
|
|
3683
|
+
return "";
|
|
3684
|
+
}
|
|
3685
|
+
const beforeMethod = content.substring(Math.max(0, methodIndex - 500), methodIndex);
|
|
3686
|
+
const commentMatch = beforeMethod.match(/\*\s*([^\n*]+)/g);
|
|
3687
|
+
if (commentMatch && commentMatch.length > 0) {
|
|
3688
|
+
return commentMatch[0].replace(/\*\s?/, "").trim();
|
|
3689
|
+
}
|
|
3690
|
+
return "";
|
|
3691
|
+
}
|
|
3692
|
+
async function syncDataModels(aiMemoryFile, projectDir) {
|
|
3693
|
+
logger.step("\u540C\u6B65\u6570\u636E\u6A21\u578B...");
|
|
3694
|
+
const backendDir = path10.join(projectDir, "backend");
|
|
3695
|
+
const exists = await FileUtils.exists(backendDir);
|
|
3696
|
+
if (!exists) {
|
|
3697
|
+
return;
|
|
3698
|
+
}
|
|
3699
|
+
const lines = [];
|
|
3700
|
+
lines.push("## \u6570\u636E\u6A21\u578B (Data Models)");
|
|
3701
|
+
lines.push("");
|
|
3702
|
+
lines.push("> \u672C\u90E8\u5206\u7531 team-cli \u81EA\u52A8\u626B\u63CF\u540E\u7AEF Entity \u751F\u6210");
|
|
3703
|
+
lines.push("");
|
|
3704
|
+
const srcDir = path10.join(backendDir, "src");
|
|
3705
|
+
const entities = await FileUtils.findFiles("*Entity.java", srcDir);
|
|
3706
|
+
if (entities.length === 0) {
|
|
3707
|
+
lines.push("\u6682\u65E0\u6570\u636E\u6A21\u578B");
|
|
3708
|
+
} else {
|
|
3709
|
+
lines.push("| \u6A21\u578B | \u8BF4\u660E | \u5B57\u6BB5 | \u5173\u8054 |");
|
|
3710
|
+
lines.push("|------|------|------|------|");
|
|
3711
|
+
for (const entityFile of entities) {
|
|
3712
|
+
const entityPath = path10.join(srcDir, entityFile);
|
|
3713
|
+
const entityName = entityFile.replace(".java", "").replace(/Entity$/, "");
|
|
3714
|
+
const displayName = entityName.split(/(?=[A-Z])/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
|
|
3715
|
+
const content = await FileUtils.read(entityPath);
|
|
3716
|
+
const classCommentMatch = content.match(/\/\*\*\s*\n([^*]|\*[^/])*\*\//);
|
|
3717
|
+
const description = classCommentMatch ? classCommentMatch[0].replace(/\/\*\*|\*\/|\*/g, "").trim() : "";
|
|
3718
|
+
const fieldCount = (content.match(/private\s+\w+/g) || []).length;
|
|
3719
|
+
const relations = [];
|
|
3720
|
+
if (content.includes("@ManyToOne")) relations.push("Many-to-One");
|
|
3721
|
+
if (content.includes("@OneToMany")) relations.push("One-to-Many");
|
|
3722
|
+
if (content.includes("@OneToOne")) relations.push("One-to-One");
|
|
3723
|
+
if (content.includes("@ManyToMany")) relations.push("Many-to-Many");
|
|
3724
|
+
lines.push(`| ${displayName} | ${description} | ${fieldCount} | ${relations.join(", ") || "-"} |`);
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
const newContent = lines.join("\n");
|
|
3728
|
+
await replaceOrInsertSection(aiMemoryFile, "## \u6570\u636E\u6A21\u578B", newContent);
|
|
3729
|
+
}
|
|
3730
|
+
async function updateSyncTime(aiMemoryFile) {
|
|
3731
|
+
const content = await FileUtils.read(aiMemoryFile);
|
|
3732
|
+
const timestamp = DateUtils.format(/* @__PURE__ */ new Date(), "YYYY-MM-DD HH:mm:ss");
|
|
3733
|
+
let updated = content;
|
|
3734
|
+
if (content.includes("\u6700\u540E\u540C\u6B65")) {
|
|
3735
|
+
updated = content.replace(
|
|
3736
|
+
/最后同步.*[::]\s*.+/,
|
|
3737
|
+
`\u6700\u540E\u540C\u6B65: ${timestamp}`
|
|
3738
|
+
);
|
|
3739
|
+
} else {
|
|
3740
|
+
updated = content.trimEnd() + `
|
|
3741
|
+
|
|
3742
|
+
---
|
|
3743
|
+
|
|
3744
|
+
*\u6700\u540E\u540C\u6B65: ${timestamp} by team-cli*
|
|
3745
|
+
`;
|
|
3746
|
+
}
|
|
3747
|
+
await FileUtils.write(aiMemoryFile, updated);
|
|
3748
|
+
}
|
|
3749
|
+
async function replaceOrInsertSection(aiMemoryFile, sectionTitle, newContent) {
|
|
3750
|
+
const content = await FileUtils.read(aiMemoryFile);
|
|
3751
|
+
if (content.includes(sectionTitle)) {
|
|
3752
|
+
const lines = content.split("\n");
|
|
3753
|
+
const result = [];
|
|
3754
|
+
let inSection = false;
|
|
3755
|
+
let skip = false;
|
|
3756
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3757
|
+
const line = lines[i];
|
|
3758
|
+
if (line.startsWith(sectionTitle)) {
|
|
3759
|
+
inSection = true;
|
|
3760
|
+
result.push(newContent);
|
|
3761
|
+
skip = true;
|
|
3762
|
+
continue;
|
|
3763
|
+
}
|
|
3764
|
+
if (inSection) {
|
|
3765
|
+
if (line.startsWith("## ") && !line.startsWith(sectionTitle)) {
|
|
3766
|
+
inSection = false;
|
|
3767
|
+
skip = false;
|
|
3768
|
+
result.push(line);
|
|
3769
|
+
}
|
|
3770
|
+
continue;
|
|
3771
|
+
}
|
|
3772
|
+
if (!skip) {
|
|
3773
|
+
result.push(line);
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
await FileUtils.write(aiMemoryFile, result.join("\n"));
|
|
3777
|
+
} else {
|
|
3778
|
+
await FileUtils.write(aiMemoryFile, content.trimEnd() + "\n\n" + newContent + "\n");
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
function parseSpecStatus3(spec) {
|
|
3782
|
+
const statusMatch = spec.match(/状态.*[::]\s*(.+)/);
|
|
3783
|
+
if (statusMatch) {
|
|
3784
|
+
const status = statusMatch[1].replace(/\*\*/g, "").trim();
|
|
3785
|
+
if (status.includes("\u5DF2\u5B8C\u6210")) return "\u2705 \u5DF2\u5B8C\u6210";
|
|
3786
|
+
if (status.includes("\u8FDB\u884C\u4E2D")) return "\u27F3 \u8FDB\u884C\u4E2D";
|
|
3787
|
+
if (status.includes("\u5DF2\u62C6\u5206")) return "\u25C9 \u5DF2\u62C6\u5206";
|
|
3788
|
+
}
|
|
3789
|
+
return "\u25CB \u672A\u5F00\u59CB";
|
|
3790
|
+
}
|
|
3791
|
+
function getSpecProgress(spec) {
|
|
3792
|
+
const todoMatches = spec.match(/-\s+\[[ x ]\]/g);
|
|
3793
|
+
const totalTodos = todoMatches ? todoMatches.length : 0;
|
|
3794
|
+
const completedMatches = spec.match(/-\s+\[x\]/g);
|
|
3795
|
+
const completedTodos = completedMatches ? completedMatches.length : 0;
|
|
3796
|
+
if (totalTodos === 0) {
|
|
3797
|
+
return "-";
|
|
3798
|
+
}
|
|
3799
|
+
return `${completedTodos}/${totalTodos}`;
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
// src/commands/check-api.ts
|
|
3803
|
+
import { Command as Command11 } from "commander";
|
|
3804
|
+
import path11 from "path";
|
|
3805
|
+
import inquirer7 from "inquirer";
|
|
3806
|
+
import { Listr as Listr6 } from "listr2";
|
|
3807
|
+
var checkApiCommand = new Command11("check-api").description("API \u68C0\u67E5\uFF08\u51B2\u7A81/\u53D8\u66F4/Registry\uFF09").action(async () => {
|
|
3808
|
+
try {
|
|
3809
|
+
logger.header("API \u68C0\u67E5");
|
|
3810
|
+
logger.newLine();
|
|
3811
|
+
const hasTechStack = await FileUtils.exists("TECH_STACK.md");
|
|
3812
|
+
if (!hasTechStack) {
|
|
3813
|
+
logger.error("\u5F53\u524D\u76EE\u5F55\u4E0D\u662F\u4E00\u4E2A\u6709\u6548\u7684 team-cli \u9879\u76EE");
|
|
3814
|
+
logger.info("\u8BF7\u5148\u8FD0\u884C 'team-cli init <project-name>' \u6216\u5207\u6362\u5230\u9879\u76EE\u76EE\u5F55");
|
|
3815
|
+
process.exit(1);
|
|
3816
|
+
}
|
|
3817
|
+
const answers = await inquirer7.prompt([
|
|
3818
|
+
{
|
|
3819
|
+
type: "list",
|
|
3820
|
+
name: "checkType",
|
|
3821
|
+
message: "\u9009\u62E9\u68C0\u67E5\u7C7B\u578B:",
|
|
3822
|
+
choices: [
|
|
3823
|
+
{ name: "\u68C0\u6D4B API \u51B2\u7A81", value: "conflicts" },
|
|
3824
|
+
{ name: "\u68C0\u6D4B API \u53D8\u66F4", value: "changes" },
|
|
3825
|
+
{ name: "\u751F\u6210 API Registry", value: "registry" },
|
|
3826
|
+
{ name: "\u5168\u90E8\u6267\u884C", value: "all" }
|
|
3827
|
+
],
|
|
3828
|
+
default: "all"
|
|
3829
|
+
}
|
|
3830
|
+
]);
|
|
3831
|
+
const tasks = new Listr6([]);
|
|
3832
|
+
if (answers.checkType === "conflicts" || answers.checkType === "all") {
|
|
3833
|
+
tasks.add({
|
|
3834
|
+
title: "\u68C0\u6D4B API \u51B2\u7A81",
|
|
3835
|
+
task: async () => {
|
|
3836
|
+
await checkApiConflicts(".");
|
|
3837
|
+
}
|
|
3838
|
+
});
|
|
3839
|
+
}
|
|
3840
|
+
if (answers.checkType === "changes" || answers.checkType === "all") {
|
|
3841
|
+
tasks.add({
|
|
3842
|
+
title: "\u68C0\u6D4B API \u53D8\u66F4",
|
|
3843
|
+
task: async () => {
|
|
3844
|
+
await detectApiChanges(".");
|
|
3845
|
+
}
|
|
3846
|
+
});
|
|
3847
|
+
}
|
|
3848
|
+
if (answers.checkType === "registry" || answers.checkType === "all") {
|
|
3849
|
+
tasks.add({
|
|
3850
|
+
title: "\u751F\u6210 API Registry",
|
|
3851
|
+
task: async () => {
|
|
3852
|
+
await generateApiRegistry(".");
|
|
3853
|
+
}
|
|
3854
|
+
});
|
|
3855
|
+
}
|
|
3856
|
+
await tasks.run();
|
|
3857
|
+
logger.newLine();
|
|
3858
|
+
logger.header("API \u68C0\u67E5\u5B8C\u6210");
|
|
3859
|
+
} catch (error) {
|
|
3860
|
+
logger.error(`API \u68C0\u67E5\u5931\u8D25: ${error.message}`);
|
|
3861
|
+
if (process.env.DEBUG) {
|
|
3862
|
+
console.error(error);
|
|
3863
|
+
}
|
|
3864
|
+
process.exit(1);
|
|
3865
|
+
}
|
|
3866
|
+
});
|
|
3867
|
+
async function checkApiConflicts(projectDir) {
|
|
3868
|
+
const backendDir = path11.join(projectDir, "backend");
|
|
3869
|
+
const exists = await FileUtils.exists(backendDir);
|
|
3870
|
+
if (!exists) {
|
|
3871
|
+
logger.info("\u672A\u627E\u5230\u540E\u7AEF\u9879\u76EE");
|
|
3872
|
+
return;
|
|
3873
|
+
}
|
|
3874
|
+
logger.step("\u626B\u63CF\u540E\u7AEF API...");
|
|
3875
|
+
logger.newLine();
|
|
3876
|
+
const apiMap = /* @__PURE__ */ new Map();
|
|
3877
|
+
const srcDir = path11.join(backendDir, "src");
|
|
3878
|
+
const controllers = await FileUtils.findFiles("*Controller.java", srcDir);
|
|
3879
|
+
for (const controllerFile of controllers) {
|
|
3880
|
+
const controllerPath = path11.join(srcDir, controllerFile);
|
|
3881
|
+
const apis = await extractApisFromController(controllerPath);
|
|
3882
|
+
for (const api of apis) {
|
|
3883
|
+
const key = `${api.method}:${api.path}`;
|
|
3884
|
+
if (!apiMap.has(key)) {
|
|
3885
|
+
apiMap.set(key, []);
|
|
3886
|
+
}
|
|
3887
|
+
apiMap.get(key).push(controllerFile);
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
const conflicts = [];
|
|
3891
|
+
for (const [key, controllers2] of apiMap.entries()) {
|
|
3892
|
+
if (controllers2.length > 1) {
|
|
3893
|
+
conflicts.push({ key, controllers: controllers2 });
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
if (conflicts.length === 0) {
|
|
3897
|
+
logger.success("\u672A\u53D1\u73B0 API \u51B2\u7A81");
|
|
3898
|
+
} else {
|
|
3899
|
+
logger.error(`\u53D1\u73B0 ${conflicts.length} \u4E2A API \u51B2\u7A81:`);
|
|
3900
|
+
logger.newLine();
|
|
3901
|
+
for (const conflict of conflicts) {
|
|
3902
|
+
logger.error(`\u51B2\u7A81: ${conflict.key}`);
|
|
3903
|
+
for (const controller of conflict.controllers) {
|
|
3904
|
+
logger.step(` - ${controller}`);
|
|
3905
|
+
}
|
|
3906
|
+
logger.newLine();
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
async function detectApiChanges(projectDir) {
|
|
3911
|
+
const backendDir = path11.join(projectDir, "backend");
|
|
3912
|
+
const registryFile = path11.join(projectDir, "docs/api-registry.md");
|
|
3913
|
+
const registryExists = await FileUtils.exists(registryFile);
|
|
3914
|
+
if (!registryExists) {
|
|
3915
|
+
logger.info("API Registry \u4E0D\u5B58\u5728\uFF0C\u8DF3\u8FC7\u53D8\u66F4\u68C0\u6D4B");
|
|
3916
|
+
logger.info("\u8FD0\u884C 'team-cli check-api' \u9009\u62E9 '\u751F\u6210 API Registry'");
|
|
3917
|
+
return;
|
|
3918
|
+
}
|
|
3919
|
+
logger.step("\u68C0\u6D4B API \u53D8\u66F4...");
|
|
3920
|
+
logger.newLine();
|
|
3921
|
+
const registryContent = await FileUtils.read(registryFile);
|
|
3922
|
+
const existingApis = extractApisFromRegistry(registryContent);
|
|
3923
|
+
const currentApis = /* @__PURE__ */ new Map();
|
|
3924
|
+
const srcDir = path11.join(backendDir, "src");
|
|
3925
|
+
const controllers = await FileUtils.findFiles("*Controller.java", srcDir);
|
|
3926
|
+
for (const controllerFile of controllers) {
|
|
3927
|
+
const controllerPath = path11.join(srcDir, controllerFile);
|
|
3928
|
+
const apis = await extractApisFromController(controllerPath);
|
|
3929
|
+
for (const api of apis) {
|
|
3930
|
+
const key = `${api.method}:${api.path}`;
|
|
3931
|
+
currentApis.set(key, api);
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
const added = [];
|
|
3935
|
+
const removed = [];
|
|
3936
|
+
const modified = [];
|
|
3937
|
+
for (const [key, api] of currentApis.entries()) {
|
|
3938
|
+
if (!existingApis.has(key)) {
|
|
3939
|
+
added.push({ method: api.method, path: api.path });
|
|
3940
|
+
} else {
|
|
3941
|
+
const existingApi = existingApis.get(key);
|
|
3942
|
+
if (existingApi.description !== api.description) {
|
|
3943
|
+
modified.push({
|
|
3944
|
+
method: api.method,
|
|
3945
|
+
path: api.path,
|
|
3946
|
+
oldDesc: existingApi.description,
|
|
3947
|
+
newDesc: api.description
|
|
3948
|
+
});
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
for (const [key, api] of existingApis.entries()) {
|
|
3953
|
+
if (!currentApis.has(key)) {
|
|
3954
|
+
removed.push({ method: api.method, path: api.path });
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
let hasChanges = false;
|
|
3958
|
+
if (added.length > 0) {
|
|
3959
|
+
hasChanges = true;
|
|
3960
|
+
logger.success(`\u65B0\u589E API (${added.length}):`);
|
|
3961
|
+
for (const api of added) {
|
|
3962
|
+
logger.step(` + ${api.method} ${api.path}`);
|
|
3963
|
+
}
|
|
3964
|
+
logger.newLine();
|
|
3965
|
+
}
|
|
3966
|
+
if (removed.length > 0) {
|
|
3967
|
+
hasChanges = true;
|
|
3968
|
+
logger.error(`\u5220\u9664 API (${removed.length}):`);
|
|
3969
|
+
for (const api of removed) {
|
|
3970
|
+
logger.step(` - ${api.method} ${api.path}`);
|
|
3971
|
+
}
|
|
3972
|
+
logger.newLine();
|
|
3973
|
+
}
|
|
3974
|
+
if (modified.length > 0) {
|
|
3975
|
+
hasChanges = true;
|
|
3976
|
+
logger.warn(`\u4FEE\u6539 API (${modified.length}):`);
|
|
3977
|
+
for (const api of modified) {
|
|
3978
|
+
logger.step(` ~ ${api.method} ${api.path}`);
|
|
3979
|
+
logger.step(` \u65E7: ${api.oldDesc}`);
|
|
3980
|
+
logger.step(` \u65B0: ${api.newDesc}`);
|
|
3981
|
+
}
|
|
3982
|
+
logger.newLine();
|
|
3983
|
+
}
|
|
3984
|
+
if (!hasChanges) {
|
|
3985
|
+
logger.success("\u672A\u68C0\u6D4B\u5230 API \u53D8\u66F4");
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3988
|
+
async function generateApiRegistry(projectDir) {
|
|
3989
|
+
const registryFile = path11.join(projectDir, "docs/api-registry.md");
|
|
3990
|
+
logger.step("\u626B\u63CF\u5E76\u751F\u6210 API Registry...");
|
|
3991
|
+
await FileUtils.ensureDir(path11.dirname(registryFile));
|
|
3992
|
+
const header = `# API Registry
|
|
3993
|
+
|
|
3994
|
+
> \u672C\u6587\u4EF6\u8BB0\u5F55\u6240\u6709 API \u7684\u5B9A\u4E49\u3001\u7248\u672C\u548C\u53D8\u66F4\u5386\u53F2
|
|
3995
|
+
|
|
3996
|
+
## API \u89C4\u8303
|
|
3997
|
+
|
|
3998
|
+
### \u57FA\u7840\u4FE1\u606F
|
|
3999
|
+
- **Base URL**: \`/api\`
|
|
4000
|
+
- **\u8BA4\u8BC1\u65B9\u5F0F**: JWT Bearer Token
|
|
4001
|
+
- **\u6570\u636E\u683C\u5F0F**: JSON
|
|
4002
|
+
- **\u5B57\u7B26\u7F16\u7801**: UTF-8
|
|
4003
|
+
|
|
4004
|
+
### \u54CD\u5E94\u7801\u89C4\u8303
|
|
4005
|
+
| \u72B6\u6001\u7801 | \u8BF4\u660E |
|
|
4006
|
+
|--------|------|
|
|
4007
|
+
| 200 | \u6210\u529F |
|
|
4008
|
+
| 201 | \u521B\u5EFA\u6210\u529F |
|
|
4009
|
+
| 400 | \u8BF7\u6C42\u53C2\u6570\u9519\u8BEF |
|
|
4010
|
+
| 401 | \u672A\u8BA4\u8BC1 |
|
|
4011
|
+
| 403 | \u65E0\u6743\u9650 |
|
|
4012
|
+
| 404 | \u8D44\u6E90\u4E0D\u5B58\u5728 |
|
|
4013
|
+
| 500 | \u670D\u52A1\u5668\u9519\u8BEF |
|
|
4014
|
+
|
|
4015
|
+
---
|
|
4016
|
+
|
|
4017
|
+
*\u6700\u540E\u66F4\u65B0: ${DateUtils.format(/* @__PURE__ */ new Date(), "YYYY-MM-DD HH:mm:ss")}*
|
|
4018
|
+
`;
|
|
4019
|
+
let content = header;
|
|
4020
|
+
const backendDir = path11.join(projectDir, "backend");
|
|
4021
|
+
const exists = await FileUtils.exists(backendDir);
|
|
4022
|
+
if (exists) {
|
|
4023
|
+
const srcDir = path11.join(backendDir, "src");
|
|
4024
|
+
const controllers = await FileUtils.findFiles("*Controller.java", srcDir);
|
|
4025
|
+
const moduleMap = /* @__PURE__ */ new Map();
|
|
4026
|
+
for (const controllerFile of controllers) {
|
|
4027
|
+
const controllerPath = path11.join(srcDir, controllerFile);
|
|
4028
|
+
const controllerName = controllerFile.replace(".java", "");
|
|
4029
|
+
const module = controllerName.replace(/Controller$/, "").toLowerCase();
|
|
4030
|
+
if (!moduleMap.has(module)) {
|
|
4031
|
+
moduleMap.set(module, []);
|
|
4032
|
+
}
|
|
4033
|
+
const apis = await extractApisFromController(controllerPath);
|
|
4034
|
+
moduleMap.get(module).push(...apis);
|
|
4035
|
+
}
|
|
4036
|
+
for (const [module, apis] of moduleMap.entries()) {
|
|
4037
|
+
content += `
|
|
4038
|
+
## ${module.charAt(0).toUpperCase() + module.slice(1)} \u6A21\u5757
|
|
4039
|
+
|
|
4040
|
+
`;
|
|
4041
|
+
for (const api of apis) {
|
|
4042
|
+
content += `### ${api.method} ${api.path}
|
|
4043
|
+
|
|
4044
|
+
`;
|
|
4045
|
+
content += `**\u7248\u672C**: v1.0
|
|
4046
|
+
|
|
4047
|
+
`;
|
|
4048
|
+
content += `**\u8BF4\u660E**: ${api.description || "-"}
|
|
4049
|
+
|
|
4050
|
+
`;
|
|
4051
|
+
content += `---
|
|
4052
|
+
|
|
4053
|
+
`;
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
await FileUtils.write(registryFile, content);
|
|
4058
|
+
logger.success(`API Registry \u5DF2\u751F\u6210: ${registryFile}`);
|
|
4059
|
+
}
|
|
4060
|
+
async function extractApisFromController(controllerPath) {
|
|
4061
|
+
const apis = [];
|
|
4062
|
+
const content = await FileUtils.read(controllerPath);
|
|
4063
|
+
let classPath = "";
|
|
4064
|
+
const classRequestMappingMatch = content.match(/@RequestMapping\("([^"]+)"\)/);
|
|
4065
|
+
if (classRequestMappingMatch) {
|
|
4066
|
+
classPath = classRequestMappingMatch[1];
|
|
4067
|
+
}
|
|
4068
|
+
const methodRegex = /@(GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)\("([^"]+)"\)\s*\n\s*public\s+(\w+)\s*\(([^)]*)\)/g;
|
|
4069
|
+
let match;
|
|
4070
|
+
while ((match = methodRegex.exec(content)) !== null) {
|
|
4071
|
+
const mappingType = match[1];
|
|
4072
|
+
const methodPath = match[2];
|
|
4073
|
+
const methodName = match[3];
|
|
4074
|
+
let httpMethod = "";
|
|
4075
|
+
switch (mappingType) {
|
|
4076
|
+
case "GetMapping":
|
|
4077
|
+
httpMethod = "GET";
|
|
4078
|
+
break;
|
|
4079
|
+
case "PostMapping":
|
|
4080
|
+
httpMethod = "POST";
|
|
4081
|
+
break;
|
|
4082
|
+
case "PutMapping":
|
|
4083
|
+
httpMethod = "PUT";
|
|
4084
|
+
break;
|
|
4085
|
+
case "DeleteMapping":
|
|
4086
|
+
httpMethod = "DELETE";
|
|
4087
|
+
break;
|
|
4088
|
+
case "PatchMapping":
|
|
4089
|
+
httpMethod = "PATCH";
|
|
4090
|
+
break;
|
|
4091
|
+
}
|
|
4092
|
+
let fullPath = methodPath;
|
|
4093
|
+
if (classPath && !methodPath.startsWith("/api")) {
|
|
4094
|
+
fullPath = `${classPath}${methodPath}`;
|
|
4095
|
+
}
|
|
4096
|
+
const description = extractMethodComment2(content, methodName);
|
|
4097
|
+
apis.push({
|
|
4098
|
+
method: httpMethod,
|
|
4099
|
+
path: fullPath,
|
|
4100
|
+
description
|
|
4101
|
+
});
|
|
4102
|
+
}
|
|
4103
|
+
return apis;
|
|
4104
|
+
}
|
|
4105
|
+
function extractApisFromRegistry(registryContent) {
|
|
4106
|
+
const apis = /* @__PURE__ */ new Map();
|
|
4107
|
+
const apiRegex = /### (GET|POST|PUT|DELETE|PATCH) ([^\n]+)\n\n\*\*版本\*\*:.+?\n\n\*\*说明\*\*:\s*([^\n-]+)/g;
|
|
4108
|
+
let match;
|
|
4109
|
+
while ((match = apiRegex.exec(registryContent)) !== null) {
|
|
4110
|
+
const method = match[1];
|
|
4111
|
+
const path13 = match[2].trim();
|
|
4112
|
+
const description = match[3].trim();
|
|
4113
|
+
const key = `${method}:${path13}`;
|
|
4114
|
+
apis.set(key, { method, path: path13, description });
|
|
4115
|
+
}
|
|
4116
|
+
return apis;
|
|
4117
|
+
}
|
|
4118
|
+
function extractMethodComment2(content, methodName) {
|
|
4119
|
+
const methodIndex = content.indexOf(`${methodName}(`);
|
|
4120
|
+
if (methodIndex === -1) {
|
|
4121
|
+
return "";
|
|
4122
|
+
}
|
|
4123
|
+
const beforeMethod = content.substring(Math.max(0, methodIndex - 500), methodIndex);
|
|
4124
|
+
const commentMatch = beforeMethod.match(/\*\s*([^\n*]+)/g);
|
|
4125
|
+
if (commentMatch && commentMatch.length > 0) {
|
|
4126
|
+
return commentMatch[0].replace(/\*\s?/, "").trim();
|
|
4127
|
+
}
|
|
4128
|
+
return "";
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
// src/commands/logs.ts
|
|
4132
|
+
import { Command as Command12 } from "commander";
|
|
4133
|
+
import path12 from "path";
|
|
4134
|
+
import inquirer8 from "inquirer";
|
|
4135
|
+
var logsCommand = new Command12("logs").argument("[filter]", "\u8FC7\u6EE4\u5668 (today, --all, \u6216\u65E5\u671F YYYY-MM-DD)").description("\u67E5\u770B\u4F1A\u8BDD\u65E5\u5FD7").action(async (filter = "today") => {
|
|
4136
|
+
try {
|
|
4137
|
+
logger.header("\u4F1A\u8BDD\u65E5\u5FD7");
|
|
4138
|
+
logger.newLine();
|
|
4139
|
+
const hasTechStack = await FileUtils.exists("TECH_STACK.md");
|
|
4140
|
+
if (!hasTechStack) {
|
|
4141
|
+
logger.error("\u5F53\u524D\u76EE\u5F55\u4E0D\u662F\u4E00\u4E2A\u6709\u6548\u7684 team-cli \u9879\u76EE");
|
|
4142
|
+
logger.info("\u8BF7\u5148\u8FD0\u884C 'team-cli init <project-name>' \u6216\u5207\u6362\u5230\u9879\u76EE\u76EE\u5F55");
|
|
4143
|
+
process.exit(1);
|
|
4144
|
+
}
|
|
4145
|
+
const sessionsDir = "docs/sessions";
|
|
4146
|
+
const dirExists = await FileUtils.exists(sessionsDir);
|
|
4147
|
+
if (!dirExists) {
|
|
4148
|
+
logger.info("\u6682\u65E0\u4F1A\u8BDD\u65E5\u5FD7");
|
|
4149
|
+
logger.info("\u8FD0\u884C 'team-cli dev' \u540E\u4F1A\u81EA\u52A8\u751F\u6210\u65E5\u5FD7");
|
|
4150
|
+
process.exit(0);
|
|
4151
|
+
}
|
|
4152
|
+
let targetDir = "";
|
|
4153
|
+
let displayTitle = "";
|
|
4154
|
+
switch (filter) {
|
|
4155
|
+
case "":
|
|
4156
|
+
case "today": {
|
|
4157
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
4158
|
+
targetDir = path12.join(sessionsDir, today);
|
|
4159
|
+
const todayExists = await FileUtils.exists(targetDir);
|
|
4160
|
+
if (!todayExists) {
|
|
4161
|
+
logger.info("\u4ECA\u65E5\u6682\u65E0\u4F1A\u8BDD\u65E5\u5FD7");
|
|
4162
|
+
process.exit(0);
|
|
4163
|
+
}
|
|
4164
|
+
displayTitle = "\u663E\u793A\u4ECA\u65E5\u4F1A\u8BDD\u65E5\u5FD7:";
|
|
4165
|
+
break;
|
|
4166
|
+
}
|
|
4167
|
+
case "--all":
|
|
4168
|
+
case "-a": {
|
|
4169
|
+
targetDir = sessionsDir;
|
|
4170
|
+
displayTitle = "\u663E\u793A\u6240\u6709\u4F1A\u8BDD\u65E5\u5FD7:";
|
|
4171
|
+
break;
|
|
4172
|
+
}
|
|
4173
|
+
default: {
|
|
4174
|
+
targetDir = path12.join(sessionsDir, filter);
|
|
4175
|
+
const dateExists = await FileUtils.exists(targetDir);
|
|
4176
|
+
if (!dateExists) {
|
|
4177
|
+
logger.error(`\u672A\u627E\u5230\u65E5\u671F '${filter}' \u7684\u65E5\u5FD7`);
|
|
4178
|
+
logger.info("\u53EF\u7528\u65E5\u671F:");
|
|
4179
|
+
const entries = await FileUtils.findFiles("*/", sessionsDir);
|
|
4180
|
+
const dates = entries.slice(0, 10);
|
|
4181
|
+
for (const date of dates) {
|
|
4182
|
+
logger.info(` ${date.replace("/", "")}`);
|
|
4183
|
+
}
|
|
4184
|
+
process.exit(1);
|
|
4185
|
+
}
|
|
4186
|
+
displayTitle = `\u663E\u793A ${filter} \u7684\u4F1A\u8BDD\u65E5\u5FD7:`;
|
|
4187
|
+
break;
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
logger.info(displayTitle);
|
|
4191
|
+
logger.newLine();
|
|
4192
|
+
const logs = await collectLogFiles(targetDir);
|
|
4193
|
+
if (logs.length === 0) {
|
|
4194
|
+
logger.info("\u65E0\u65E5\u5FD7\u6587\u4EF6");
|
|
4195
|
+
process.exit(0);
|
|
4196
|
+
}
|
|
4197
|
+
for (let i = 0; i < logs.length; i++) {
|
|
4198
|
+
const relPath = path12.relative(sessionsDir, logs[i]);
|
|
4199
|
+
logger.step(`${i + 1}) ${relPath}`);
|
|
4200
|
+
}
|
|
4201
|
+
logger.newLine();
|
|
4202
|
+
const answers = await inquirer8.prompt([
|
|
4203
|
+
{
|
|
4204
|
+
type: "input",
|
|
4205
|
+
name: "selection",
|
|
4206
|
+
message: "\u8F93\u5165\u7F16\u53F7\u67E5\u770B\u8BE6\u60C5 (\u6216 Enter \u9000\u51FA):",
|
|
4207
|
+
default: ""
|
|
4208
|
+
}
|
|
4209
|
+
]);
|
|
4210
|
+
const selection = answers.selection.trim();
|
|
4211
|
+
if (selection === "") {
|
|
4212
|
+
process.exit(0);
|
|
4213
|
+
}
|
|
4214
|
+
const selectionNum = parseInt(selection, 10);
|
|
4215
|
+
if (isNaN(selectionNum) || selectionNum < 1 || selectionNum > logs.length) {
|
|
4216
|
+
logger.error("\u65E0\u6548\u7684\u9009\u62E9");
|
|
4217
|
+
process.exit(1);
|
|
4218
|
+
}
|
|
4219
|
+
const selectedLog = logs[selectionNum - 1];
|
|
4220
|
+
logger.newLine();
|
|
4221
|
+
logger.header("\u65E5\u5FD7\u8BE6\u60C5");
|
|
4222
|
+
logger.newLine();
|
|
4223
|
+
const content = await FileUtils.read(selectedLog);
|
|
4224
|
+
console.log(content);
|
|
4225
|
+
} catch (error) {
|
|
4226
|
+
logger.error(`\u67E5\u770B\u65E5\u5FD7\u5931\u8D25: ${error.message}`);
|
|
4227
|
+
if (process.env.DEBUG) {
|
|
4228
|
+
console.error(error);
|
|
4229
|
+
}
|
|
4230
|
+
process.exit(1);
|
|
4231
|
+
}
|
|
4232
|
+
});
|
|
4233
|
+
async function collectLogFiles(targetDir) {
|
|
4234
|
+
const logs = [];
|
|
4235
|
+
try {
|
|
4236
|
+
const allFiles = await FileUtils.findFiles("*.md", targetDir);
|
|
4237
|
+
const filtered = allFiles.filter((f) => f !== "index.md");
|
|
4238
|
+
for (const file of filtered) {
|
|
4239
|
+
const filePath = path12.join(targetDir, file);
|
|
4240
|
+
const stat = await FileUtils.exists(filePath);
|
|
4241
|
+
if (stat) {
|
|
4242
|
+
logs.push(filePath);
|
|
4243
|
+
}
|
|
4244
|
+
}
|
|
4245
|
+
} catch (error) {
|
|
4246
|
+
}
|
|
4247
|
+
return logs;
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
// src/index.ts
|
|
4251
|
+
var program = new Command13();
|
|
4252
|
+
program.name("team-cli").description("AI-Native \u56E2\u961F\u7814\u53D1\u811A\u624B\u67B6").version("2.0.0");
|
|
4253
|
+
program.option("-v, --verbose", "\u8BE6\u7EC6\u8F93\u51FA\u6A21\u5F0F").option("--debug", "\u8C03\u8BD5\u6A21\u5F0F");
|
|
4254
|
+
program.addCommand(initCommand);
|
|
4255
|
+
program.addCommand(splitPrdCommand);
|
|
4256
|
+
program.addCommand(breakdownCommand);
|
|
4257
|
+
program.addCommand(devCommand);
|
|
4258
|
+
program.addCommand(addFeatureCommand);
|
|
4259
|
+
program.addCommand(bugfixCommand);
|
|
4260
|
+
program.addCommand(hotfixCommand);
|
|
4261
|
+
program.addCommand(lintCommand);
|
|
4262
|
+
program.addCommand(statusCommand);
|
|
4263
|
+
program.addCommand(detectDepsCommand);
|
|
4264
|
+
program.addCommand(syncMemoryCommand);
|
|
4265
|
+
program.addCommand(checkApiCommand);
|
|
4266
|
+
program.addCommand(logsCommand);
|
|
4267
|
+
program.action(() => {
|
|
4268
|
+
showHelp();
|
|
4269
|
+
});
|
|
4270
|
+
function showHelp() {
|
|
4271
|
+
console.log("");
|
|
4272
|
+
logger.header("team-cli - AI-Native \u56E2\u961F\u7814\u53D1\u811A\u624B\u67B6");
|
|
4273
|
+
console.log("");
|
|
4274
|
+
console.log(chalk2.bold("\u4F7F\u7528\u65B9\u6CD5:"));
|
|
4275
|
+
console.log(" team-cli init [project-name] \u521D\u59CB\u5316\u65B0\u9879\u76EE");
|
|
4276
|
+
console.log(" team-cli split-prd <prd-folder> \u5C06 PRD \u62C6\u5206\u6210\u591A\u4E2A specs");
|
|
4277
|
+
console.log(" team-cli breakdown [spec-file] \u5C06 spec \u62C6\u5206\u4E3A milestones \u548C todos");
|
|
4278
|
+
console.log(" team-cli dev \u5F00\u53D1\u6A21\u5F0F\uFF0C\u6267\u884C\u5177\u4F53\u4EFB\u52A1");
|
|
4279
|
+
console.log(" team-cli add-feature <name> \u6DFB\u52A0\u65B0\u529F\u80FD");
|
|
4280
|
+
console.log(" team-cli bugfix \u521B\u5EFA Bugfix \u8BB0\u5F55");
|
|
4281
|
+
console.log(" team-cli hotfix \u521B\u5EFA Hotfix");
|
|
4282
|
+
console.log(" team-cli detect-deps [spec] \u68C0\u6D4B\u4F9D\u8D56\u5173\u7CFB");
|
|
4283
|
+
console.log(" team-cli sync-memory \u540C\u6B65 AI_MEMORY.md");
|
|
4284
|
+
console.log(" team-cli check-api API \u68C0\u67E5\uFF08\u51B2\u7A81/\u53D8\u66F4/Registry\uFF09");
|
|
4285
|
+
console.log(" team-cli status \u67E5\u770B\u9879\u76EE\u72B6\u6001");
|
|
4286
|
+
console.log(" team-cli lint \u4EE3\u7801\u8D28\u91CF\u68C0\u67E5 (\u524D\u7AEF+\u540E\u7AEF)");
|
|
4287
|
+
console.log(" team-cli logs [date] \u67E5\u770B\u4F1A\u8BDD\u65E5\u5FD7");
|
|
4288
|
+
console.log(" team-cli --help \u663E\u793A\u5E2E\u52A9\u4FE1\u606F");
|
|
4289
|
+
console.log("");
|
|
4290
|
+
console.log(chalk2.bold("\u793A\u4F8B:"));
|
|
4291
|
+
console.log(" team-cli init my-project");
|
|
4292
|
+
console.log(" cd my-project");
|
|
4293
|
+
console.log(" team-cli add-feature payment-system");
|
|
4294
|
+
console.log(" team-cli breakdown docs/specs/xxx.md");
|
|
4295
|
+
console.log(" team-cli dev");
|
|
4296
|
+
console.log("");
|
|
4297
|
+
console.log(chalk2.bold("\u5F00\u53D1\u6D41\u7A0B:"));
|
|
4298
|
+
console.log(" 1. PRD \u2192 specs (split-prd)");
|
|
4299
|
+
console.log(" 2. spec \u2192 milestones + todos (breakdown)");
|
|
4300
|
+
console.log(" 3. \u9009\u62E9 milestone/todo \u2192 \u5B9E\u73B0 (dev)");
|
|
4301
|
+
console.log("");
|
|
4302
|
+
console.log(chalk2.bold("\u8FED\u4EE3\u6D41\u7A0B:"));
|
|
4303
|
+
console.log(" team-cli add-feature <name> # \u6DFB\u52A0\u65B0\u529F\u80FD");
|
|
4304
|
+
console.log(" team-cli detect-deps [spec] # \u68C0\u6D4B\u4F9D\u8D56\u5173\u7CFB");
|
|
4305
|
+
console.log(" team-cli sync-memory # \u540C\u6B65 AI_MEMORY");
|
|
4306
|
+
console.log(" team-cli check-api # API \u68C0\u67E5");
|
|
4307
|
+
console.log(" team-cli bugfix # \u521B\u5EFA bugfix");
|
|
4308
|
+
console.log(" team-cli hotfix # \u7D27\u6025\u4FEE\u590D");
|
|
4309
|
+
console.log(" team-cli status # \u67E5\u770B\u9879\u76EE\u72B6\u6001");
|
|
4310
|
+
console.log("");
|
|
4311
|
+
console.log(chalk2.gray("\u66F4\u591A\u4FE1\u606F: https://github.com/yungu/team-cli"));
|
|
4312
|
+
console.log("");
|
|
4313
|
+
}
|
|
4314
|
+
process.on("uncaughtException", (error) => {
|
|
4315
|
+
logger.error("\u672A\u6355\u83B7\u7684\u5F02\u5E38");
|
|
4316
|
+
if (process.env.DEBUG) {
|
|
4317
|
+
console.error(error);
|
|
4318
|
+
}
|
|
4319
|
+
process.exit(1);
|
|
4320
|
+
});
|
|
4321
|
+
process.on("unhandledRejection", (reason) => {
|
|
4322
|
+
logger.error("\u672A\u5904\u7406\u7684 Promise \u62D2\u7EDD");
|
|
4323
|
+
if (process.env.DEBUG) {
|
|
4324
|
+
console.error(reason);
|
|
4325
|
+
}
|
|
4326
|
+
process.exit(1);
|
|
4327
|
+
});
|
|
4328
|
+
program.parse(process.argv);
|
|
4329
|
+
//# sourceMappingURL=index.js.map
|