momo-ai 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,14 @@
1
- # Rachel Momo SDD - Spec Driven Development 工具
1
+ # Momo SDD - Spec Driven Development 工具
2
2
 
3
- [![Downloads](https://img.shields.io/npm/dm/momo-ai.svg)](https://www.npmjs.com/package/momo-ai)
4
-
5
- ![npm version](https://img.shields.io/npm/v/momo-ai.svg)
3
+ ![npm version](https://img.shields.io/npm/v/momo-ai.svg) | [![Downloads](https://img.shields.io/npm/dm/momo-ai.svg)](https://www.npmjs.com/package/momo-ai)
4
+ > For Rachel Momo
6
5
 
7
6
  ## 1. 介绍
8
7
 
9
8
  当前工具会在操作系统中安装 `momo` 命令,使用它进行 `SDD - Spec Driven Development` 开发:
10
9
 
11
10
  1. 参考:`OpenSpec / Spec-Kit / Kiro`
12
- 2. 工具:`TRAE` 工具集成(推荐使用国际版)/ `Cursor` / `Lingma`
11
+ 2. 工具:`TRAE` 工具集成(推荐使用国际版)/ `Cursor`(推荐使用,可实现多个Agent开发)
13
12
  3. 中文规范支持,命令运行之后将 `prompt` 提示词拷贝到剪切板
14
13
  4. 多个 Agent 协同,实现 Team 团队模式的 AI 开发,无模型限制(推荐使用同一个模型进行任务协同开发)
15
14
 
@@ -96,8 +95,10 @@ momo unlock # 解锁任务 -> 任务状态 = 待办
96
95
  2. 使用 `momo repo` 添加项目代码库以及工程实例(有多少个 `Agent` 工作就添加多少工程实例),添加完成后可使用 `momo open`
97
96
  直接打开工程。
98
97
  3. 更新 `project.md / project-model.md / requirement.md` 的细节文档(可用AI帮助拆分和书写)
99
- 3. 使用 `momo add -n 需求名称` 添加细节需求(可用AI帮助拆分和书写:`momo plan -n 需求名称`)
100
- 4. 使用 `momo tasks` 列出所有任务,并且使用 `momo run` 运行任务得到任务提示词
98
+ 4. 使用 `momo add -n 需求名称` 添加细节需求(可用AI帮助拆分和书写:`momo plan -n 需求名称`)
99
+ 5. 使用 `momo tasks` 列出所有任务,并且使用 `momo run` 运行任务得到任务提示词
100
+
101
+ > 带 (CV) 标记的命令运行成功后可直接“Ctrl + V”粘贴到 TRAE / Lingma / Cursor / Kiro 中执行。
101
102
 
102
103
  ## 4. 参考链接
103
104
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "momo-ai",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Rachel Momo ( OpenSpec )",
5
5
  "main": "src/momo.js",
6
6
  "bin": {
@@ -44,7 +44,6 @@
44
44
  "lodash": "^4.17.21",
45
45
  "mkdirp": "^3.0.1",
46
46
  "mockjs": "^1.1.0",
47
- "momo-ai": "^1.0.0",
48
47
  "random-js": "^2.1.0",
49
48
  "superagent": "^8.0.9",
50
49
  "taffydb": "^2.7.3",
@@ -1,7 +1,7 @@
1
1
  # 任务执行需求
2
2
 
3
3
  <!-- BEGIN -->
4
- 在 source/<%= ROOT %> 目录下完成任务 <%= TASK %> 的开发工作(任务详情:specification/changes/<%= REQ %>/tasks/<%= TASK %>.md)。
4
+ 在 source/<%= ROOT %> 目录下完成任务 <%= TASK %> 的开发工作(任务详情:specification/changes/<%= REQ %>/tasks/<%= TASK %>.md),任务完成后将并且将 specification/changes/<%= REQ %>/tasks/<%= TASK %>.md 文件更名为 <%= TASK %>.done,并且更新 并且将 specification/changes/<%= REQ %>/tasks.md 中的总任务清单。
5
5
 
6
6
  任务完成后,请记得删除以下两个锁文件:
7
7
  1. 任务锁文件: specification/changes/<%= REQ %>/tasks/<%= TASK %>.lock
@@ -32,7 +32,15 @@ module.exports = async (options) => {
32
32
  // 如果只有一个工具可用,直接使用它
33
33
  if (availableTools.length === 1) {
34
34
  const tool = availableTools[0];
35
- Ec.waiting(`🔍 检测到可用工具: ${tool.name}`);
35
+ let toolDisplayName = tool.name;
36
+ if (tool.name === 'Trae' || tool.name === 'Cursor') {
37
+ toolDisplayName = `${tool.name}(${'Multi-Agent'.cyan})`;
38
+ }
39
+ if (tool.name === 'Trae') {
40
+ Ec.waiting(`🔍 检测到可用工具: ${toolDisplayName.green}`);
41
+ } else {
42
+ Ec.waiting(`🔍 检测到可用工具: ${toolDisplayName}`);
43
+ }
36
44
  await _openWithTool(tool.command);
37
45
  Ec.askClose();
38
46
  process.exit(0);
@@ -40,9 +48,17 @@ module.exports = async (options) => {
40
48
 
41
49
  // 显示交互式选择菜单
42
50
  Ec.waiting('🔍 检测到多个可用的AI工具,请选择要使用的工具:');
43
- const choices = availableTools.map((tool, index) =>
44
- `${index + 1}. ${tool.name}`
45
- );
51
+ const choices = availableTools.map((tool, index) => {
52
+ let toolDisplayName = tool.name;
53
+ if (tool.name === 'Trae' || tool.name === 'Cursor') {
54
+ toolDisplayName = `${tool.name}(${'Multi-Agent'.cyan})`;
55
+ }
56
+ if (tool.name === 'Trae') {
57
+ return `${index + 1}. ${toolDisplayName} ${'推荐'.green}`;
58
+ } else {
59
+ return `${index + 1}. ${toolDisplayName}`;
60
+ }
61
+ });
46
62
 
47
63
  // 添加退出选项
48
64
  choices.push(`${choices.length + 1}. 退出`);
@@ -69,7 +85,11 @@ module.exports = async (options) => {
69
85
 
70
86
  // 执行选择的工具
71
87
  const selectedTool = availableTools[selectedIndex];
72
- Ec.waiting(`🚀 正在使用 ${selectedTool.name} 打开项目...`);
88
+ let toolDisplayName = selectedTool.name;
89
+ if (selectedTool.name === 'Trae' || selectedTool.name === 'Cursor') {
90
+ toolDisplayName = `${selectedTool.name}(${'Multi-Agent'.cyan})`;
91
+ }
92
+ Ec.waiting(`🚀 正在使用 ${toolDisplayName} 打开项目...`);
73
93
  await _openWithTool(selectedTool.command);
74
94
  Ec.askClose();
75
95
  process.exit(0);
@@ -2,7 +2,7 @@ const Ec = require('../epic');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const util = require('util');
5
- const { spawn } = require('child_process');
5
+ const {spawn} = require('child_process');
6
6
  const fsAsync = require('fs').promises;
7
7
 
8
8
  /**
@@ -16,7 +16,7 @@ const _readTemplate = async (templatePath) => {
16
16
  const lines = content.split('\n');
17
17
  let beginIndex = -1;
18
18
  let endIndex = -1;
19
-
19
+
20
20
  for (let i = 0; i < lines.length; i++) {
21
21
  if (lines[i].includes('<!-- BEGIN -->')) {
22
22
  beginIndex = i;
@@ -25,12 +25,12 @@ const _readTemplate = async (templatePath) => {
25
25
  break;
26
26
  }
27
27
  }
28
-
28
+
29
29
  if (beginIndex !== -1 && endIndex !== -1) {
30
30
  // 提取 BEGIN 和 END 之间的内容(不包括 BEGIN 和 END 行)
31
31
  return lines.slice(beginIndex + 1, endIndex).join('\n');
32
32
  }
33
-
33
+
34
34
  return content;
35
35
  } catch (error) {
36
36
  Ec.waiting(`读取模板文件失败: ${error.message}`);
@@ -61,10 +61,10 @@ const _renderTemplate = (template, data) => {
61
61
  */
62
62
  const _copyToClipboard = async (content) => {
63
63
  try {
64
- const proc = spawn('pbcopy', { stdio: 'pipe' });
64
+ const proc = spawn('pbcopy', {stdio: 'pipe'});
65
65
  proc.stdin.write(content);
66
66
  proc.stdin.end();
67
-
67
+
68
68
  // 等待复制操作完成
69
69
  await new Promise((resolve, reject) => {
70
70
  proc.on('close', (code) => {
@@ -76,7 +76,7 @@ const _copyToClipboard = async (content) => {
76
76
  });
77
77
  proc.on('error', reject);
78
78
  });
79
-
79
+
80
80
  Ec.waiting('📄 任务执行提示词已拷贝到剪切板');
81
81
  } catch (error) {
82
82
  Ec.waiting(`⚠️ 无法拷贝到剪切板: ${error.message}`);
@@ -94,25 +94,25 @@ const _copyToClipboard = async (content) => {
94
94
  const _findTaskInstances = (taskName) => {
95
95
  const changesDir = path.resolve(process.cwd(), 'specification', 'changes');
96
96
  const taskInstances = [];
97
-
97
+
98
98
  if (!fs.existsSync(changesDir)) {
99
99
  return taskInstances;
100
100
  }
101
-
101
+
102
102
  // 获取所有需求目录
103
- const requirements = fs.readdirSync(changesDir).filter(file =>
103
+ const requirements = fs.readdirSync(changesDir).filter(file =>
104
104
  fs.statSync(path.join(changesDir, file)).isDirectory()
105
105
  );
106
-
106
+
107
107
  // 查找所有匹配的任务文件
108
108
  requirements.forEach(requirement => {
109
109
  const tasksDir = path.resolve(changesDir, requirement, 'tasks');
110
-
110
+
111
111
  // 检查 tasks 目录是否存在
112
112
  if (fs.existsSync(tasksDir) && fs.statSync(tasksDir).isDirectory()) {
113
113
  const taskFile = `${taskName}.md`;
114
114
  const taskPath = path.resolve(tasksDir, taskFile);
115
-
115
+
116
116
  // 检查任务文件是否存在
117
117
  if (fs.existsSync(taskPath)) {
118
118
  taskInstances.push({
@@ -124,10 +124,57 @@ const _findTaskInstances = (taskName) => {
124
124
  }
125
125
  }
126
126
  });
127
-
127
+
128
128
  return taskInstances;
129
129
  };
130
130
 
131
+ /**
132
+ * 枚举所有可用的任务
133
+ * @returns {Array} 任务列表
134
+ */
135
+ const _enumerateAllTasks = () => {
136
+ const changesDir = path.resolve(process.cwd(), 'specification', 'changes');
137
+ const tasks = [];
138
+
139
+ if (!fs.existsSync(changesDir)) {
140
+ return tasks;
141
+ }
142
+
143
+ // 获取所有需求目录
144
+ const requirements = fs.readdirSync(changesDir).filter(file =>
145
+ fs.statSync(path.join(changesDir, file)).isDirectory()
146
+ );
147
+
148
+ // 查找所有任务文件
149
+ requirements.forEach(requirement => {
150
+ const tasksDir = path.resolve(changesDir, requirement, 'tasks');
151
+
152
+ // 检查 tasks 目录是否存在
153
+ if (fs.existsSync(tasksDir) && fs.statSync(tasksDir).isDirectory()) {
154
+ const taskFiles = fs.readdirSync(tasksDir).filter(file =>
155
+ file.endsWith('.md')
156
+ );
157
+
158
+ taskFiles.forEach(taskFile => {
159
+ const taskName = path.basename(taskFile, '.md');
160
+ const taskPath = path.resolve(tasksDir, taskFile);
161
+ const title = _extractTitleFromMarkdown(taskPath);
162
+
163
+ tasks.push({
164
+ name: taskName,
165
+ path: taskPath,
166
+ requirement: requirement,
167
+ relativePath: path.relative(process.cwd(), taskPath),
168
+ display: `${requirement}/${taskName}`,
169
+ title: title
170
+ });
171
+ });
172
+ }
173
+ });
174
+
175
+ return tasks;
176
+ };
177
+
131
178
  /**
132
179
  * 获取所有可用的工作空间(排除已被锁定的)
133
180
  * @returns {Array} 可用工作空间列表
@@ -135,16 +182,16 @@ const _findTaskInstances = (taskName) => {
135
182
  const _getAvailableWorkspaces = () => {
136
183
  const sourceDir = path.resolve(process.cwd(), 'source');
137
184
  const workspaces = [];
138
-
185
+
139
186
  if (!fs.existsSync(sourceDir)) {
140
187
  return workspaces;
141
188
  }
142
-
189
+
143
190
  // 获取所有目录
144
- const dirs = fs.readdirSync(sourceDir).filter(file =>
191
+ const dirs = fs.readdirSync(sourceDir).filter(file =>
145
192
  fs.statSync(path.join(sourceDir, file)).isDirectory()
146
193
  );
147
-
194
+
148
195
  // 检查哪些工作空间可用(没有对应的.lock文件)
149
196
  dirs.forEach(dir => {
150
197
  const lockFilePath = path.resolve(sourceDir, `${dir}.lock`);
@@ -155,7 +202,7 @@ const _getAvailableWorkspaces = () => {
155
202
  });
156
203
  }
157
204
  });
158
-
205
+
159
206
  return workspaces;
160
207
  };
161
208
 
@@ -165,11 +212,11 @@ const _getAvailableWorkspaces = () => {
165
212
  */
166
213
  const _selectWorkspace = () => {
167
214
  const workspaces = _getAvailableWorkspaces();
168
-
215
+
169
216
  if (workspaces.length === 0) {
170
217
  return null;
171
218
  }
172
-
219
+
173
220
  // 随机选择一个工作空间
174
221
  const randomIndex = Math.floor(Math.random() * workspaces.length);
175
222
  return workspaces[randomIndex];
@@ -185,12 +232,12 @@ const _selectWorkspace = () => {
185
232
  const _createTaskLock = (taskName, requirementName, actorName) => {
186
233
  const taskLockDir = path.resolve(process.cwd(), 'specification', 'changes', requirementName, 'tasks');
187
234
  const taskLockPath = path.resolve(taskLockDir, `${taskName}.lock`);
188
-
235
+
189
236
  // 确保目录存在
190
237
  if (!fs.existsSync(taskLockDir)) {
191
- fs.mkdirSync(taskLockDir, { recursive: true });
238
+ fs.mkdirSync(taskLockDir, {recursive: true});
192
239
  }
193
-
240
+
194
241
  // 创建锁文件
195
242
  fs.writeFileSync(taskLockPath, `Locked by: ${actorName}\nLocked at: ${new Date().toISOString()}\n`);
196
243
  return taskLockPath;
@@ -206,110 +253,269 @@ const _createTaskLock = (taskName, requirementName, actorName) => {
206
253
  const _createWorkspaceLock = (workspaceName, taskName, actorName) => {
207
254
  const sourceDir = path.resolve(process.cwd(), 'source');
208
255
  const workspaceLockPath = path.resolve(sourceDir, `${workspaceName}.lock`);
209
-
256
+
210
257
  // 创建锁文件
211
258
  fs.writeFileSync(workspaceLockPath, `Task: ${taskName}\nLocked by: ${actorName}\nLocked at: ${new Date().toISOString()}\n`);
212
259
  return workspaceLockPath;
213
260
  };
214
261
 
262
+ /**
263
+ * 检查任务状态
264
+ * @param {string} taskName 任务名称
265
+ * @param {string} tasksDir 任务目录
266
+ * @returns {string} 任务状态
267
+ */
268
+ const _checkTaskStatus = (taskName, tasksDir) => {
269
+ // 检查.lock文件(进行中)
270
+ const lockFile = path.join(tasksDir, `${taskName}.lock`);
271
+ if (fs.existsSync(lockFile)) {
272
+ return '进行中';
273
+ }
274
+
275
+ // 检查.done文件(已完成)
276
+ const doneFile = path.join(tasksDir, `${taskName}.done`);
277
+ if (fs.existsSync(doneFile)) {
278
+ return '已完成';
279
+ }
280
+
281
+ // 默认状态(未开始)
282
+ return '未开始';
283
+ };
284
+
285
+ /**
286
+ * 从 Markdown 文件中提取第一个标题
287
+ * @param {string} filePath 文件路径
288
+ * @returns {string} 第一个标题内容
289
+ */
290
+ const _extractTitleFromMarkdown = (filePath) => {
291
+ try {
292
+ if (!fs.existsSync(filePath)) {
293
+ return path.basename(filePath, '.md');
294
+ }
295
+
296
+ const content = fs.readFileSync(filePath, 'utf8');
297
+ const lines = content.split('\n');
298
+
299
+ for (const line of lines) {
300
+ // 匹配 # 开头的标题行
301
+ const titleMatch = line.match(/^#\s+(.+)$/);
302
+ if (titleMatch) {
303
+ return titleMatch[1].trim();
304
+ }
305
+ }
306
+
307
+ // 如果没有找到标题,使用文件名(不含扩展名)作为任务名称
308
+ return path.basename(filePath, '.md');
309
+ } catch (error) {
310
+ return path.basename(filePath, '.md');
311
+ }
312
+ };
313
+
215
314
  module.exports = async (options) => {
216
315
  // 参数提取
217
316
  const parsed = Ec.parseArgument(options);
218
-
317
+
219
318
  const actorName = parsed.actor;
220
319
  const taskName = parsed.task;
221
-
222
- // 检查必要参数
223
- if (!taskName) {
224
- Ec.error("❌ 缺少必要参数 -t/--task,请提供任务名称");
225
- Ec.askClose();
226
- process.exit(1);
227
- }
228
-
320
+
229
321
  try {
230
- // 查找所有匹配的任务实例
231
- const taskInstances = _findTaskInstances(taskName);
232
-
233
- if (taskInstances.length === 0) {
234
- Ec.error(`❌ 未找到任务 ${taskName}`);
235
- Ec.askClose();
236
- process.exit(1);
237
- }
238
-
239
322
  let selectedTask;
240
- // 如果有多个任务实例,让用户选择
241
- if (taskInstances.length > 1) {
242
- Ec.waiting(`🔍 发现 ${taskInstances.length} 个重复任务,请选择要执行的任务:`);
243
-
244
- taskInstances.forEach((task, index) => {
245
- Ec.waiting(`${index + 1}. ${task.relativePath}`);
246
- });
247
-
248
- // 获取用户选择
249
- const answer = await Ec.ask('请输入选项编号: ');
250
- const selectedIndex = parseInt(answer) - 1;
251
-
252
- // 验证选择
253
- if (isNaN(selectedIndex) || selectedIndex < 0 || selectedIndex >= taskInstances.length) {
254
- Ec.error('❌ 无效的选择');
323
+
324
+ // 如果没有提供任务名称,则让用户选择任务
325
+ if (!taskName || taskName === 'unset') {
326
+ Ec.waiting("🔍 未指定任务,正在枚举所有可用任务...");
327
+
328
+ // 枚举所有任务
329
+ const allTasks = _enumerateAllTasks();
330
+
331
+ if (allTasks.length === 0) {
332
+ Ec.error("❌ 未找到任何任务");
255
333
  Ec.askClose();
256
334
  process.exit(1);
257
335
  }
258
-
259
- selectedTask = taskInstances[selectedIndex];
336
+
337
+ // 循环直到用户选择一个非进行中的任务
338
+ while (true) {
339
+ Ec.waiting(`🔍 找到 ${allTasks.length} 个任务,请选择要执行的任务:`);
340
+
341
+ allTasks.forEach((task, index) => {
342
+ // 获取任务状态
343
+ const tasksDir = path.resolve(process.cwd(), 'specification', 'changes', task.requirement, 'tasks');
344
+ const status = _checkTaskStatus(task.name, tasksDir);
345
+
346
+ // 为状态添加颜色代码
347
+ let coloredStatus;
348
+ switch (status) {
349
+ case '进行中':
350
+ coloredStatus = status.blue;
351
+ break;
352
+ case '已完成':
353
+ coloredStatus = status.green;
354
+ break;
355
+ case '未开始':
356
+ coloredStatus = status.red;
357
+ break;
358
+ default:
359
+ coloredStatus = status;
360
+ }
361
+
362
+ const coloredName = task.name.cyan;
363
+ Ec.waiting(`${index + 1}. [${coloredStatus}] ${task.display} (${coloredName})`);
364
+ });
365
+
366
+ // 获取用户选择
367
+ const answer = await Ec.ask('请输入选项编号: ');
368
+ const selectedIndex = parseInt(answer) - 1;
369
+
370
+ // 验证选择
371
+ if (isNaN(selectedIndex) || selectedIndex < 0 || selectedIndex >= allTasks.length) {
372
+ Ec.error('❌ 无效的选择');
373
+ continue;
374
+ }
375
+
376
+ // 检查选中的任务是否正在进行中
377
+ const tasksDir = path.resolve(process.cwd(), 'specification', 'changes', allTasks[selectedIndex].requirement, 'tasks');
378
+ const status = _checkTaskStatus(allTasks[selectedIndex].name, tasksDir);
379
+
380
+ if (status === '进行中') {
381
+ Ec.error('❌ 不能选择正在进行中的任务,请重新选择');
382
+ continue;
383
+ }
384
+
385
+ // 直接使用用户选择的任务,跳过重复检查
386
+ selectedTask = allTasks[selectedIndex];
387
+ Ec.waiting(`✅ 选择任务: ${selectedTask.display}`);
388
+ break;
389
+ }
260
390
  } else {
261
- selectedTask = taskInstances[0];
391
+ // 查找所有匹配的任务实例
392
+ const taskInstances = _findTaskInstances(taskName);
393
+
394
+ if (taskInstances.length === 0) {
395
+ Ec.error(`❌ 未找到任务 ${taskName}`);
396
+ Ec.askClose();
397
+ process.exit(1);
398
+ }
399
+
400
+ // 如果有多个任务实例,让用户选择
401
+ if (taskInstances.length > 1) {
402
+ // 循环直到用户选择一个非进行中的任务
403
+ while (true) {
404
+ Ec.waiting(`🔍 发现 ${taskInstances.length} 个重复任务,请选择要执行的任务:`);
405
+
406
+ taskInstances.forEach((task, index) => {
407
+ // 获取任务状态
408
+ const tasksDir = path.resolve(process.cwd(), 'specification', 'changes', task.requirement, 'tasks');
409
+ const status = _checkTaskStatus(task.name, tasksDir);
410
+
411
+ // 为状态添加颜色代码
412
+ let coloredStatus;
413
+ switch (status) {
414
+ case '进行中':
415
+ coloredStatus = status.blue;
416
+ break;
417
+ case '已完成':
418
+ coloredStatus = status.green;
419
+ break;
420
+ case '未开始':
421
+ coloredStatus = status.red;
422
+ break;
423
+ default:
424
+ coloredStatus = status;
425
+ }
426
+
427
+ const coloredName = task.name.cyan;
428
+ Ec.waiting(`${index + 1}. [${coloredStatus}] ${task.relativePath} (${coloredName})`);
429
+ });
430
+
431
+ // 获取用户选择
432
+ const answer = await Ec.ask('请输入选项编号: ');
433
+ const selectedIndex = parseInt(answer) - 1;
434
+
435
+ // 验证选择
436
+ if (isNaN(selectedIndex) || selectedIndex < 0 || selectedIndex >= taskInstances.length) {
437
+ Ec.error('❌ 无效的选择');
438
+ continue;
439
+ }
440
+
441
+ // 检查选中的任务是否正在进行中
442
+ const tasksDir = path.resolve(process.cwd(), 'specification', 'changes', taskInstances[selectedIndex].requirement, 'tasks');
443
+ const status = _checkTaskStatus(taskInstances[selectedIndex].name, tasksDir);
444
+
445
+ if (status === '进行中') {
446
+ Ec.error('❌ 不能选择正在进行中的任务,请重新选择');
447
+ continue;
448
+ }
449
+
450
+ selectedTask = taskInstances[selectedIndex];
451
+ break;
452
+ }
453
+ } else {
454
+ // 检查唯一任务是否正在进行中
455
+ const tasksDir = path.resolve(process.cwd(), 'specification', 'changes', taskInstances[0].requirement, 'tasks');
456
+ const status = _checkTaskStatus(taskInstances[0].name, tasksDir);
457
+
458
+ if (status === '进行中') {
459
+ Ec.error(`❌ 任务 ${taskName} 正在进行中,无法重复执行`);
460
+ Ec.askClose();
461
+ process.exit(1);
462
+ }
463
+
464
+ selectedTask = taskInstances[0];
465
+ }
466
+
467
+ Ec.waiting(`✅ 找到任务: ${selectedTask.relativePath}`);
262
468
  }
263
-
469
+
264
470
  // 记录需求名称
265
471
  const requirementName = selectedTask.requirement;
266
-
472
+
267
473
  // 自动选择工作空间
268
474
  const selectedWorkspace = _selectWorkspace();
269
-
475
+
270
476
  if (!selectedWorkspace) {
271
477
  Ec.error("❌ 所有工作空间都已被占用,请稍后再试");
272
478
  Ec.askClose();
273
479
  process.exit(1);
274
480
  }
275
-
481
+
276
482
  // 创建锁文件
277
483
  let taskLockPath = null;
278
484
  let workspaceLockPath = null;
279
-
485
+
280
486
  // 创建任务锁文件
281
487
  if (requirementName) {
282
- taskLockPath = _createTaskLock(taskName, requirementName, actorName);
488
+ taskLockPath = _createTaskLock(selectedTask.name, requirementName, actorName);
283
489
  }
284
-
490
+
285
491
  // 创建工作空间锁文件
286
492
  if (selectedWorkspace) {
287
- workspaceLockPath = _createWorkspaceLock(selectedWorkspace.name, taskName, actorName);
493
+ workspaceLockPath = _createWorkspaceLock(selectedWorkspace.name, selectedTask.name, actorName);
288
494
  }
289
-
290
- // 读取模板文件并填充参数,然后拷贝到剪切板
495
+
496
+ // 读取模板文件并填充参数,然后拷贝到剪贴板
291
497
  const templatePath = path.resolve(__dirname, '../_template/PROMPT/run.md.ejs');
292
-
498
+
293
499
  if (fs.existsSync(templatePath)) {
294
500
  try {
295
501
  // 读取并处理提示词模板
296
502
  const templateContent = await _readTemplate(templatePath);
297
- const renderedContent = _renderTemplate(templateContent, {
298
- TASK: taskName,
503
+ const renderedContent = _renderTemplate(templateContent, {
504
+ TASK: selectedTask.name,
299
505
  REQ: requirementName,
300
506
  ROOT: selectedWorkspace ? selectedWorkspace.path : 'source/<WORKSPACE_PATH>'
301
507
  });
302
-
508
+
303
509
  // 将提示词复制到剪贴板
304
510
  await _copyToClipboard(renderedContent);
305
- Ec.info(`✅ 任务 ${taskName} 准备完成,请查看剪切板中的提示词`);
511
+ Ec.info(`✅ 任务 ${selectedTask.name} 准备完成,请查看剪切板中的提示词`);
306
512
  } catch (error) {
307
- Ec.info(`✅ 任务 ${taskName} 查找完成`);
513
+ Ec.info(`✅ 任务 ${selectedTask.name} 查找完成`);
308
514
  }
309
515
  } else {
310
- Ec.info(`✅ 任务 ${taskName} 查找完成`);
516
+ Ec.info(`✅ 任务 ${selectedTask.name} 查找完成`);
311
517
  }
312
-
518
+
313
519
  Ec.askClose();
314
520
  process.exit(0);
315
521
 
@@ -61,7 +61,6 @@ const _renderTemplate = (template, data) => {
61
61
  */
62
62
  const _copyToClipboard = async (content) => {
63
63
  try {
64
- Ec.waiting("📋 正在将内容复制到剪切板...");
65
64
  const proc = spawn('pbcopy', { stdio: 'pipe' });
66
65
  proc.stdin.write(content);
67
66
  proc.stdin.end();
@@ -88,37 +87,55 @@ const _copyToClipboard = async (content) => {
88
87
  };
89
88
 
90
89
  /**
91
- * 处理剪切板任务
92
- * @param {string} taskName 任务名称
93
- * @param {Array} taskInstances 任务实例列表
90
+ * 从 Markdown 文件中提取第一个标题
91
+ * @param {string} filePath 文件路径
92
+ * @returns {string} 第一个标题内容
94
93
  */
95
- const _handleClipboardTask = async (taskName, taskInstances) => {
96
- // 读取模板文件并填充参数,然后拷贝到剪切板
97
- const templatePath = path.resolve(__dirname, '../_template/PROMPT/tasks.md.ejs');
98
- Ec.waiting(`📄 读取模板文件: ${templatePath}`);
99
-
100
- if (fs.existsSync(templatePath)) {
101
- try {
102
- // 读取并处理提示词模板
103
- const templateContent = await _readTemplate(templatePath);
104
- Ec.waiting("🔧 渲染模板参数");
105
-
106
- const renderedContent = _renderTemplate(templateContent, {
107
- REQ: taskInstances[0].requirement,
108
- TASK: taskName
109
- });
110
-
111
- Ec.waiting("📋 准备复制到剪切板");
112
- Ec.waiting(`📋 剪切板内容预览: ${renderedContent.substring(0, 50)}${renderedContent.length > 50 ? '...' : ''}`);
113
-
114
- // 将提示词复制到剪贴板
115
- await _copyToClipboard(renderedContent);
116
- } catch (error) {
117
- Ec.waiting(`⚠️ 处理模板时出错: ${error.message}`);
94
+ const _extractTitleFromMarkdown = (filePath) => {
95
+ try {
96
+ if (!fs.existsSync(filePath)) {
97
+ return path.basename(filePath, '.md');
118
98
  }
119
- } else {
120
- Ec.waiting(`⚠️ 未找到模板文件: ${templatePath}`);
99
+
100
+ const content = fs.readFileSync(filePath, 'utf8');
101
+ const lines = content.split('\n');
102
+
103
+ for (const line of lines) {
104
+ // 匹配 # 开头的标题行
105
+ const titleMatch = line.match(/^#\s+(.+)$/);
106
+ if (titleMatch) {
107
+ return titleMatch[1].trim();
108
+ }
109
+ }
110
+
111
+ // 如果没有找到标题,使用文件名(不含扩展名)作为任务名称
112
+ return path.basename(filePath, '.md');
113
+ } catch (error) {
114
+ return path.basename(filePath, '.md');
115
+ }
116
+ };
117
+
118
+ /**
119
+ * 检查任务状态
120
+ * @param {string} taskName 任务名称
121
+ * @param {string} tasksDir 任务目录
122
+ * @returns {string} 任务状态
123
+ */
124
+ const _checkTaskStatus = (taskName, tasksDir) => {
125
+ // 检查.lock文件(进行中)
126
+ const lockFile = path.join(tasksDir, `${taskName}.lock`);
127
+ if (fs.existsSync(lockFile)) {
128
+ return '进行中';
129
+ }
130
+
131
+ // 检查.done文件(已完成)
132
+ const doneFile = path.join(tasksDir, `${taskName}.done`);
133
+ if (fs.existsSync(doneFile)) {
134
+ return '已完成';
121
135
  }
136
+
137
+ // 默认状态(未开始)
138
+ return '未开始';
122
139
  };
123
140
 
124
141
  module.exports = async (options) => {
@@ -148,12 +165,8 @@ module.exports = async (options) => {
148
165
  process.exit(0);
149
166
  }
150
167
 
151
- Ec.waiting(`📁 找到 ${requirements.length} 个需求目录`);
152
-
153
168
  // 如果指定了任务名称,只检查该任务的重复信息
154
169
  if (taskName) {
155
- Ec.waiting(`🔍 检查任务 ${taskName} 的重复信息...`);
156
-
157
170
  const taskInstances = [];
158
171
 
159
172
  // 查找指定任务
@@ -222,11 +235,31 @@ module.exports = async (options) => {
222
235
  taskFiles.forEach(taskFile => {
223
236
  const name = path.basename(taskFile, '.md');
224
237
  const taskPath = path.relative(process.cwd(), path.resolve(tasksDir, taskFile));
238
+ const title = _extractTitleFromMarkdown(path.resolve(tasksDir, taskFile));
239
+ const status = _checkTaskStatus(name, tasksDir);
240
+
241
+ // 为状态添加颜色代码
242
+ let coloredStatus;
243
+ switch (status) {
244
+ case '进行中':
245
+ coloredStatus = status.blue;
246
+ break;
247
+ case '已完成':
248
+ coloredStatus = status.green;
249
+ break;
250
+ case '未开始':
251
+ coloredStatus = status.red;
252
+ break;
253
+ default:
254
+ coloredStatus = status;
255
+ }
225
256
 
226
257
  tasks.push({
227
258
  name: name,
259
+ title: title,
228
260
  path: taskPath,
229
- requirement: requirement
261
+ requirement: requirement,
262
+ status: coloredStatus
230
263
  });
231
264
  });
232
265
  }
@@ -241,10 +274,11 @@ module.exports = async (options) => {
241
274
  process.exit(0);
242
275
  }
243
276
 
244
- // 列出所有任务(包含路径)
277
+ // 列出所有任务(包含路径和状态,状态在前)
245
278
  Ec.waiting(`📊 共找到 ${tasks.length} 个任务:`);
246
279
  tasks.forEach((task, index) => {
247
- Ec.waiting(`${index + 1}. ${task.name}, ${task.path}`);
280
+ const coloredName = task.name.cyan;
281
+ Ec.waiting(`${index + 1}. [${task.status}] ${task.title} (${coloredName}), ${task.path}`);
248
282
  });
249
283
 
250
284
  // 执行剪切板任务(使用第一个任务)
@@ -257,4 +291,33 @@ module.exports = async (options) => {
257
291
  Ec.askClose();
258
292
  process.exit(1);
259
293
  }
294
+ };
295
+
296
+ /**
297
+ * 处理剪切板任务
298
+ * @param {string} taskName 任务名称
299
+ * @param {Array} taskInstances 任务实例列表
300
+ */
301
+ const _handleClipboardTask = async (taskName, taskInstances) => {
302
+ // 读取模板文件并填充参数,然后拷贝到剪切板
303
+ const templatePath = path.resolve(__dirname, '../_template/PROMPT/tasks.md.ejs');
304
+
305
+ if (fs.existsSync(templatePath)) {
306
+ try {
307
+ // 读取并处理提示词模板
308
+ const templateContent = await _readTemplate(templatePath);
309
+
310
+ const renderedContent = _renderTemplate(templateContent, {
311
+ REQ: taskInstances[0].requirement,
312
+ TASK: taskName
313
+ });
314
+
315
+ // 将提示词复制到剪贴板
316
+ await _copyToClipboard(renderedContent);
317
+ } catch (error) {
318
+ Ec.waiting(`⚠️ 处理模板时出错: ${error.message}`);
319
+ }
320
+ } else {
321
+ Ec.waiting(`⚠️ 未找到模板文件: ${templatePath}`);
322
+ }
260
323
  };