momo-ai 1.0.1 → 1.0.3

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,30 +1,28 @@
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`
13
- 3. 中文规范支持,命令运行之后将 `prompt` 提示词拷贝到剪切板
11
+ 2. 工具支持:
12
+ - `Trea`(推荐使用,可支持多Agent模式)
13
+ - `Cursor`(稍贵,可支持多Agent模式)
14
+ - `Lingma`
15
+ - `Kiro`
16
+ 3. 中文规范,命令运行之后将 `prompt` 提示词拷贝到剪切板,直接复制到工具中即可使用,提示词生成之后会存储在 `.working` 目录下。
14
17
  4. 多个 Agent 协同,实现 Team 团队模式的 AI 开发,无模型限制(推荐使用同一个模型进行任务协同开发)
15
18
 
16
19
  和 `OpenSpec / Spec-Kit / Kiro` 原生工具不同,当前工具以 AI Agent 开发为核心,提供从 Plan -> Deployment
17
- 完整生命周期的整体开发流程,属于上层项目实施的开发工具——暴力、简单、易懂。由于 IDE( `TRAE / Cursor`
18
- )中可直接选择使用的模型,本工具不提供任何模型选择,模型本身取决于你所使用的工具本身,命令行只是一个辅助,核心开发模式为:
19
- `AI 开发 + 人工审核`,本工具的宗旨就是 `prompt` 的模板化。
20
-
21
- > 多个异构Agent!中文优先!
20
+ 完整生命周期的整体开发流程,是 SDD 的一种落地手段——暴力、简单、易懂。由于辅助工具可选择模型,所以本工具不提供模型选择,模型本身取决于你所使用的工具本身,命令行只是一个辅助,核心开发模式为:
21
+ `AI 开发 + 人工审核`,本工具的核心是 `prompt` 的模板化。
22
22
 
23
- ## 2. 使用
23
+ > 除开 `momo` 命令后续会提供和模型直接交互的 `lain` 命令,近似 `iFlow / openspec` 的功能。
24
24
 
25
- - 执行 `momo repo` 的过程中会依赖一个已经初始化好的项目 `Git` 地址,且当前项目本身依赖一个 `Git`(因为项目是以
26
- `submodule` 的方式添加到环境中的)。
27
- - 移除相关命令如 `submodule` 只支持 Linux/Mac 的脚本,这个用独立脚本防止更改丢失。
25
+ ## 2. 工具使用
28
26
 
29
27
  ### 2.1. 安装
30
28
 
@@ -34,9 +32,16 @@ npm install -g momo-ai
34
32
  momo help
35
33
  ```
36
34
 
37
- ### 2.2. 控制台:定模型
35
+ ### 2.2. 操作步骤
38
36
 
39
- > 控制台开发中(Lain 模式)
37
+ 1. 使用 `momo init` 初始化协同工程项目(运行之前可使用 `momo env` 检查环境)。
38
+ 2. 使用 `momo repo` 添加项目代码库以及工程实例(有多少个 `Agent` 工作就添加多少工程实例),添加完成后可使用 `momo open`
39
+ 直接打开工程。
40
+ 3. 更新 `project.md / project-model.md / requirement.md` 的细节文档(可用AI帮助拆分和书写)
41
+ 4. 使用 `momo add -n 需求名称` 添加细节需求(可用AI帮助拆分和书写:`momo plan -n 需求名称`)
42
+ 5. 使用 `momo tasks` 列出所有任务,并且使用 `momo run` 运行任务得到任务提示词
43
+
44
+ > 带 (CV) 标记的命令运行成功后可直接“Ctrl + V”粘贴到 TRAE / Lingma / Cursor / Kiro 中执行。
40
45
 
41
46
  ### 2.3. 命令:不定模型
42
47
 
@@ -87,28 +92,24 @@ momo actor -a "角色名称" # 添加新角色 -> 交互式命令,
87
92
 
88
93
  # momo run ( 生成提示词 -> 剪切板,自动计算空闲的 source-01 )
89
94
  momo run -a {actor} -t {task} # 将任务分配给角色,检查执行目录看哪些任务正在执行
95
+ momo run # 从菜单中选择任务
90
96
  momo unlock # 解锁任务 -> 任务状态 = 待办
91
97
  ```
92
98
 
93
- ## 3. 操作步骤
99
+ ### 2.4. 控制台:定模型
94
100
 
95
- 1. 使用 `momo init` 初始化协同工程项目(运行之前可使用 `momo env` 检查环境)。
96
- 2. 使用 `momo repo` 添加项目代码库以及工程实例(有多少个 `Agent` 工作就添加多少工程实例),添加完成后可使用 `momo open`
97
- 直接打开工程。
98
- 3. 更新 `project.md / project-model.md / requirement.md` 的细节文档(可用AI帮助拆分和书写)
99
- 3. 使用 `momo add -n 需求名称` 添加细节需求(可用AI帮助拆分和书写:`momo plan -n 需求名称`)
100
- 4. 使用 `momo tasks` 列出所有任务,并且使用 `momo run` 运行任务得到任务提示词
101
+ > 控制台开发中(Lain 模式)
101
102
 
102
- ## 4. 参考链接
103
+ ## 3. 参考链接
103
104
 
104
- ### 4.1. 旧版
105
+ ### 3.1. 旧版
105
106
 
106
107
  - (后端)Zero Ecotope:<https://www.zerows.io>
107
108
  - (前端)Zero UI:<https://www.vertxui.cn>
108
109
  - (工具)Zero AI:<https://www.vertxai.cn>
109
110
  - (标准)Zero Schema:<https://www.vertx-cloud.cn>
110
111
 
111
- ### 4.2. 新增
112
+ ### 3.2. 新增
112
113
 
113
114
  - Maven 统一版本管理:<https://gitee.com/silentbalanceyh/rachel-momo>
114
115
  - Rapid快速开发框架:<https://gitee.com/silentbalanceyh/r2mo-rapid>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "momo-ai",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "executor": "executeRun",
3
- "description": "执行指定的任务!",
3
+ "description": "(CV)执行指定的任务!",
4
4
  "command": "run",
5
5
  "options": [
6
6
  {
@@ -4,7 +4,12 @@ module.exports = (options) => {
4
4
  const metadata = Ec.parseMetadata();
5
5
  metadata.forEach(runner => {
6
6
  Ec.waiting(`命令:` + `momo ${runner.command} [-options]`.green);
7
- Ec.waiting(`说明:${runner.description}`.yellow);
7
+ // 检查描述中是否包含"CV",如果包含则在(CV)和说明之间添加红色高亮emoji
8
+ let description = runner.description;
9
+ if (description.includes("(CV)")) {
10
+ description = description.replace("(CV)", "(CV)📋️ ");
11
+ }
12
+ Ec.waiting(`说明`.yellow + `:${description}`);
8
13
  const options = runner.options || [];
9
14
  options.forEach(option => {
10
15
  if (option.hasOwnProperty('default')) {
@@ -26,7 +26,8 @@ const _ioDirectory = async (baseDir) => {
26
26
  "integration/windsurf/",
27
27
  "integration/github/",
28
28
  "integration/claude-code/",
29
- "integration/chatgpt/"
29
+ "integration/chatgpt/",
30
+ ".working/"
30
31
  ];
31
32
 
32
33
  // 创建所有目录
@@ -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);
@@ -68,8 +68,9 @@ const _isRequirementExists = (changesDir, requirementName) => {
68
68
  /**
69
69
  * 将内容复制到剪贴板(去除换行符)
70
70
  * @param {string} content 要复制的内容
71
+ * @param {string} requirementName 需求名称
71
72
  */
72
- const _copyToClipboard = async (content) => {
73
+ const _copyToClipboard = async (content, requirementName) => {
73
74
  try {
74
75
  // 去除换行符,将内容合并为一行后再复制到剪贴板
75
76
  const contentWithoutNewlines = content.replace(/\r?\n|\r/g, ' ');
@@ -77,6 +78,20 @@ const _copyToClipboard = async (content) => {
77
78
  proc.stdin.write(contentWithoutNewlines);
78
79
  proc.stdin.end();
79
80
  Ec.waiting('✅ 计划提示词已复制到剪贴板');
81
+
82
+ // 保存内容到 .working 目录
83
+ const workingDir = path.resolve(process.cwd(), '.working');
84
+ const fileName = `REQ-${requirementName}.txt`;
85
+ const filePath = path.join(workingDir, fileName);
86
+
87
+ // 确保 .working 目录存在
88
+ if (!fs.existsSync(workingDir)) {
89
+ await fsAsync.mkdir(workingDir, { recursive: true });
90
+ }
91
+
92
+ // 写入文件
93
+ await fsAsync.writeFile(filePath, content);
94
+ Ec.waiting(`📄 计划提示词已保存到文件: ${filePath}`);
80
95
  } catch (error) {
81
96
  Ec.waiting(`复制到剪贴板失败: ${error.message}`);
82
97
  // 在非 macOS 系统上可能没有 pbcopy,提供备选方案
@@ -123,7 +138,7 @@ module.exports = async (options) => {
123
138
  const renderedContent = _renderTemplate(templateContent, { NAME: requirementName });
124
139
 
125
140
  // 将提示词复制到剪贴板
126
- await _copyToClipboard(renderedContent);
141
+ await _copyToClipboard(renderedContent, requirementName);
127
142
 
128
143
  Ec.info(`✅ 成功生成需求 "${requirementName}" 的计划提示词`);
129
144
  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}`);
@@ -58,13 +58,15 @@ const _renderTemplate = (template, data) => {
58
58
  /**
59
59
  * 将内容复制到剪贴板
60
60
  * @param {string} content 要复制的内容
61
+ * @param {string} requirementName 需求名称
62
+ * @param {string} taskName 任务名称
61
63
  */
62
- const _copyToClipboard = async (content) => {
64
+ const _copyToClipboard = async (content, requirementName, taskName) => {
63
65
  try {
64
- const proc = spawn('pbcopy', { stdio: 'pipe' });
66
+ const proc = spawn('pbcopy', {stdio: 'pipe'});
65
67
  proc.stdin.write(content);
66
68
  proc.stdin.end();
67
-
69
+
68
70
  // 等待复制操作完成
69
71
  await new Promise((resolve, reject) => {
70
72
  proc.on('close', (code) => {
@@ -76,8 +78,22 @@ const _copyToClipboard = async (content) => {
76
78
  });
77
79
  proc.on('error', reject);
78
80
  });
79
-
81
+
80
82
  Ec.waiting('📄 任务执行提示词已拷贝到剪切板');
83
+
84
+ // 保存内容到 .working 目录
85
+ const workingDir = path.resolve(process.cwd(), '.working');
86
+ const fileName = `REQ-${requirementName}-${taskName}.txt`;
87
+ const filePath = path.join(workingDir, fileName);
88
+
89
+ // 确保 .working 目录存在
90
+ if (!fs.existsSync(workingDir)) {
91
+ await fsAsync.mkdir(workingDir, { recursive: true });
92
+ }
93
+
94
+ // 写入文件
95
+ await fsAsync.writeFile(filePath, content);
96
+ Ec.waiting(`📄 任务执行提示词已保存到文件: ${filePath}`);
81
97
  } catch (error) {
82
98
  Ec.waiting(`⚠️ 无法拷贝到剪切板: ${error.message}`);
83
99
  // 备选方案:显示内容
@@ -94,25 +110,25 @@ const _copyToClipboard = async (content) => {
94
110
  const _findTaskInstances = (taskName) => {
95
111
  const changesDir = path.resolve(process.cwd(), 'specification', 'changes');
96
112
  const taskInstances = [];
97
-
113
+
98
114
  if (!fs.existsSync(changesDir)) {
99
115
  return taskInstances;
100
116
  }
101
-
117
+
102
118
  // 获取所有需求目录
103
- const requirements = fs.readdirSync(changesDir).filter(file =>
119
+ const requirements = fs.readdirSync(changesDir).filter(file =>
104
120
  fs.statSync(path.join(changesDir, file)).isDirectory()
105
121
  );
106
-
122
+
107
123
  // 查找所有匹配的任务文件
108
124
  requirements.forEach(requirement => {
109
125
  const tasksDir = path.resolve(changesDir, requirement, 'tasks');
110
-
126
+
111
127
  // 检查 tasks 目录是否存在
112
128
  if (fs.existsSync(tasksDir) && fs.statSync(tasksDir).isDirectory()) {
113
129
  const taskFile = `${taskName}.md`;
114
130
  const taskPath = path.resolve(tasksDir, taskFile);
115
-
131
+
116
132
  // 检查任务文件是否存在
117
133
  if (fs.existsSync(taskPath)) {
118
134
  taskInstances.push({
@@ -124,10 +140,57 @@ const _findTaskInstances = (taskName) => {
124
140
  }
125
141
  }
126
142
  });
127
-
143
+
128
144
  return taskInstances;
129
145
  };
130
146
 
147
+ /**
148
+ * 枚举所有可用的任务
149
+ * @returns {Array} 任务列表
150
+ */
151
+ const _enumerateAllTasks = () => {
152
+ const changesDir = path.resolve(process.cwd(), 'specification', 'changes');
153
+ const tasks = [];
154
+
155
+ if (!fs.existsSync(changesDir)) {
156
+ return tasks;
157
+ }
158
+
159
+ // 获取所有需求目录
160
+ const requirements = fs.readdirSync(changesDir).filter(file =>
161
+ fs.statSync(path.join(changesDir, file)).isDirectory()
162
+ );
163
+
164
+ // 查找所有任务文件
165
+ requirements.forEach(requirement => {
166
+ const tasksDir = path.resolve(changesDir, requirement, 'tasks');
167
+
168
+ // 检查 tasks 目录是否存在
169
+ if (fs.existsSync(tasksDir) && fs.statSync(tasksDir).isDirectory()) {
170
+ const taskFiles = fs.readdirSync(tasksDir).filter(file =>
171
+ file.endsWith('.md')
172
+ );
173
+
174
+ taskFiles.forEach(taskFile => {
175
+ const taskName = path.basename(taskFile, '.md');
176
+ const taskPath = path.resolve(tasksDir, taskFile);
177
+ const title = _extractTitleFromMarkdown(taskPath);
178
+
179
+ tasks.push({
180
+ name: taskName,
181
+ path: taskPath,
182
+ requirement: requirement,
183
+ relativePath: path.relative(process.cwd(), taskPath),
184
+ display: `${requirement}/${taskName}`,
185
+ title: title
186
+ });
187
+ });
188
+ }
189
+ });
190
+
191
+ return tasks;
192
+ };
193
+
131
194
  /**
132
195
  * 获取所有可用的工作空间(排除已被锁定的)
133
196
  * @returns {Array} 可用工作空间列表
@@ -135,16 +198,16 @@ const _findTaskInstances = (taskName) => {
135
198
  const _getAvailableWorkspaces = () => {
136
199
  const sourceDir = path.resolve(process.cwd(), 'source');
137
200
  const workspaces = [];
138
-
201
+
139
202
  if (!fs.existsSync(sourceDir)) {
140
203
  return workspaces;
141
204
  }
142
-
205
+
143
206
  // 获取所有目录
144
- const dirs = fs.readdirSync(sourceDir).filter(file =>
207
+ const dirs = fs.readdirSync(sourceDir).filter(file =>
145
208
  fs.statSync(path.join(sourceDir, file)).isDirectory()
146
209
  );
147
-
210
+
148
211
  // 检查哪些工作空间可用(没有对应的.lock文件)
149
212
  dirs.forEach(dir => {
150
213
  const lockFilePath = path.resolve(sourceDir, `${dir}.lock`);
@@ -155,7 +218,7 @@ const _getAvailableWorkspaces = () => {
155
218
  });
156
219
  }
157
220
  });
158
-
221
+
159
222
  return workspaces;
160
223
  };
161
224
 
@@ -165,11 +228,11 @@ const _getAvailableWorkspaces = () => {
165
228
  */
166
229
  const _selectWorkspace = () => {
167
230
  const workspaces = _getAvailableWorkspaces();
168
-
231
+
169
232
  if (workspaces.length === 0) {
170
233
  return null;
171
234
  }
172
-
235
+
173
236
  // 随机选择一个工作空间
174
237
  const randomIndex = Math.floor(Math.random() * workspaces.length);
175
238
  return workspaces[randomIndex];
@@ -185,12 +248,12 @@ const _selectWorkspace = () => {
185
248
  const _createTaskLock = (taskName, requirementName, actorName) => {
186
249
  const taskLockDir = path.resolve(process.cwd(), 'specification', 'changes', requirementName, 'tasks');
187
250
  const taskLockPath = path.resolve(taskLockDir, `${taskName}.lock`);
188
-
251
+
189
252
  // 确保目录存在
190
253
  if (!fs.existsSync(taskLockDir)) {
191
- fs.mkdirSync(taskLockDir, { recursive: true });
254
+ fs.mkdirSync(taskLockDir, {recursive: true});
192
255
  }
193
-
256
+
194
257
  // 创建锁文件
195
258
  fs.writeFileSync(taskLockPath, `Locked by: ${actorName}\nLocked at: ${new Date().toISOString()}\n`);
196
259
  return taskLockPath;
@@ -206,110 +269,269 @@ const _createTaskLock = (taskName, requirementName, actorName) => {
206
269
  const _createWorkspaceLock = (workspaceName, taskName, actorName) => {
207
270
  const sourceDir = path.resolve(process.cwd(), 'source');
208
271
  const workspaceLockPath = path.resolve(sourceDir, `${workspaceName}.lock`);
209
-
272
+
210
273
  // 创建锁文件
211
274
  fs.writeFileSync(workspaceLockPath, `Task: ${taskName}\nLocked by: ${actorName}\nLocked at: ${new Date().toISOString()}\n`);
212
275
  return workspaceLockPath;
213
276
  };
214
277
 
278
+ /**
279
+ * 检查任务状态
280
+ * @param {string} taskName 任务名称
281
+ * @param {string} tasksDir 任务目录
282
+ * @returns {string} 任务状态
283
+ */
284
+ const _checkTaskStatus = (taskName, tasksDir) => {
285
+ // 检查.lock文件(进行中)
286
+ const lockFile = path.join(tasksDir, `${taskName}.lock`);
287
+ if (fs.existsSync(lockFile)) {
288
+ return '进行中';
289
+ }
290
+
291
+ // 检查.done文件(已完成)
292
+ const doneFile = path.join(tasksDir, `${taskName}.done`);
293
+ if (fs.existsSync(doneFile)) {
294
+ return '已完成';
295
+ }
296
+
297
+ // 默认状态(未开始)
298
+ return '未开始';
299
+ };
300
+
301
+ /**
302
+ * 从 Markdown 文件中提取第一个标题
303
+ * @param {string} filePath 文件路径
304
+ * @returns {string} 第一个标题内容
305
+ */
306
+ const _extractTitleFromMarkdown = (filePath) => {
307
+ try {
308
+ if (!fs.existsSync(filePath)) {
309
+ return path.basename(filePath, '.md');
310
+ }
311
+
312
+ const content = fs.readFileSync(filePath, 'utf8');
313
+ const lines = content.split('\n');
314
+
315
+ for (const line of lines) {
316
+ // 匹配 # 开头的标题行
317
+ const titleMatch = line.match(/^#\s+(.+)$/);
318
+ if (titleMatch) {
319
+ return titleMatch[1].trim();
320
+ }
321
+ }
322
+
323
+ // 如果没有找到标题,使用文件名(不含扩展名)作为任务名称
324
+ return path.basename(filePath, '.md');
325
+ } catch (error) {
326
+ return path.basename(filePath, '.md');
327
+ }
328
+ };
329
+
215
330
  module.exports = async (options) => {
216
331
  // 参数提取
217
332
  const parsed = Ec.parseArgument(options);
218
-
333
+
219
334
  const actorName = parsed.actor;
220
335
  const taskName = parsed.task;
221
-
222
- // 检查必要参数
223
- if (!taskName) {
224
- Ec.error("❌ 缺少必要参数 -t/--task,请提供任务名称");
225
- Ec.askClose();
226
- process.exit(1);
227
- }
228
-
336
+
229
337
  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
338
  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('❌ 无效的选择');
339
+
340
+ // 如果没有提供任务名称,则让用户选择任务
341
+ if (!taskName || taskName === 'unset') {
342
+ Ec.waiting("🔍 未指定任务,正在枚举所有可用任务...");
343
+
344
+ // 枚举所有任务
345
+ const allTasks = _enumerateAllTasks();
346
+
347
+ if (allTasks.length === 0) {
348
+ Ec.error("❌ 未找到任何任务");
255
349
  Ec.askClose();
256
350
  process.exit(1);
257
351
  }
258
-
259
- selectedTask = taskInstances[selectedIndex];
352
+
353
+ // 循环直到用户选择一个非进行中的任务
354
+ while (true) {
355
+ Ec.waiting(`🔍 找到 ${allTasks.length} 个任务,请选择要执行的任务:`);
356
+
357
+ allTasks.forEach((task, index) => {
358
+ // 获取任务状态
359
+ const tasksDir = path.resolve(process.cwd(), 'specification', 'changes', task.requirement, 'tasks');
360
+ const status = _checkTaskStatus(task.name, tasksDir);
361
+
362
+ // 为状态添加颜色代码
363
+ let coloredStatus;
364
+ switch (status) {
365
+ case '进行中':
366
+ coloredStatus = status.blue;
367
+ break;
368
+ case '已完成':
369
+ coloredStatus = status.green;
370
+ break;
371
+ case '未开始':
372
+ coloredStatus = status.red;
373
+ break;
374
+ default:
375
+ coloredStatus = status;
376
+ }
377
+
378
+ const coloredName = task.name.cyan;
379
+ Ec.waiting(`${index + 1}. [${coloredStatus}] ${task.display} (${coloredName})`);
380
+ });
381
+
382
+ // 获取用户选择
383
+ const answer = await Ec.ask('请输入选项编号: ');
384
+ const selectedIndex = parseInt(answer) - 1;
385
+
386
+ // 验证选择
387
+ if (isNaN(selectedIndex) || selectedIndex < 0 || selectedIndex >= allTasks.length) {
388
+ Ec.error('❌ 无效的选择');
389
+ continue;
390
+ }
391
+
392
+ // 检查选中的任务是否正在进行中
393
+ const tasksDir = path.resolve(process.cwd(), 'specification', 'changes', allTasks[selectedIndex].requirement, 'tasks');
394
+ const status = _checkTaskStatus(allTasks[selectedIndex].name, tasksDir);
395
+
396
+ if (status === '进行中') {
397
+ Ec.error('❌ 不能选择正在进行中的任务,请重新选择');
398
+ continue;
399
+ }
400
+
401
+ // 直接使用用户选择的任务,跳过重复检查
402
+ selectedTask = allTasks[selectedIndex];
403
+ Ec.waiting(`✅ 选择任务: ${selectedTask.display}`);
404
+ break;
405
+ }
260
406
  } else {
261
- selectedTask = taskInstances[0];
407
+ // 查找所有匹配的任务实例
408
+ const taskInstances = _findTaskInstances(taskName);
409
+
410
+ if (taskInstances.length === 0) {
411
+ Ec.error(`❌ 未找到任务 ${taskName}`);
412
+ Ec.askClose();
413
+ process.exit(1);
414
+ }
415
+
416
+ // 如果有多个任务实例,让用户选择
417
+ if (taskInstances.length > 1) {
418
+ // 循环直到用户选择一个非进行中的任务
419
+ while (true) {
420
+ Ec.waiting(`🔍 发现 ${taskInstances.length} 个重复任务,请选择要执行的任务:`);
421
+
422
+ taskInstances.forEach((task, index) => {
423
+ // 获取任务状态
424
+ const tasksDir = path.resolve(process.cwd(), 'specification', 'changes', task.requirement, 'tasks');
425
+ const status = _checkTaskStatus(task.name, tasksDir);
426
+
427
+ // 为状态添加颜色代码
428
+ let coloredStatus;
429
+ switch (status) {
430
+ case '进行中':
431
+ coloredStatus = status.blue;
432
+ break;
433
+ case '已完成':
434
+ coloredStatus = status.green;
435
+ break;
436
+ case '未开始':
437
+ coloredStatus = status.red;
438
+ break;
439
+ default:
440
+ coloredStatus = status;
441
+ }
442
+
443
+ const coloredName = task.name.cyan;
444
+ Ec.waiting(`${index + 1}. [${coloredStatus}] ${task.relativePath} (${coloredName})`);
445
+ });
446
+
447
+ // 获取用户选择
448
+ const answer = await Ec.ask('请输入选项编号: ');
449
+ const selectedIndex = parseInt(answer) - 1;
450
+
451
+ // 验证选择
452
+ if (isNaN(selectedIndex) || selectedIndex < 0 || selectedIndex >= taskInstances.length) {
453
+ Ec.error('❌ 无效的选择');
454
+ continue;
455
+ }
456
+
457
+ // 检查选中的任务是否正在进行中
458
+ const tasksDir = path.resolve(process.cwd(), 'specification', 'changes', taskInstances[selectedIndex].requirement, 'tasks');
459
+ const status = _checkTaskStatus(taskInstances[selectedIndex].name, tasksDir);
460
+
461
+ if (status === '进行中') {
462
+ Ec.error('❌ 不能选择正在进行中的任务,请重新选择');
463
+ continue;
464
+ }
465
+
466
+ selectedTask = taskInstances[selectedIndex];
467
+ break;
468
+ }
469
+ } else {
470
+ // 检查唯一任务是否正在进行中
471
+ const tasksDir = path.resolve(process.cwd(), 'specification', 'changes', taskInstances[0].requirement, 'tasks');
472
+ const status = _checkTaskStatus(taskInstances[0].name, tasksDir);
473
+
474
+ if (status === '进行中') {
475
+ Ec.error(`❌ 任务 ${taskName} 正在进行中,无法重复执行`);
476
+ Ec.askClose();
477
+ process.exit(1);
478
+ }
479
+
480
+ selectedTask = taskInstances[0];
481
+ }
482
+
483
+ Ec.waiting(`✅ 找到任务: ${selectedTask.relativePath}`);
262
484
  }
263
-
485
+
264
486
  // 记录需求名称
265
487
  const requirementName = selectedTask.requirement;
266
-
488
+
267
489
  // 自动选择工作空间
268
490
  const selectedWorkspace = _selectWorkspace();
269
-
491
+
270
492
  if (!selectedWorkspace) {
271
493
  Ec.error("❌ 所有工作空间都已被占用,请稍后再试");
272
494
  Ec.askClose();
273
495
  process.exit(1);
274
496
  }
275
-
497
+
276
498
  // 创建锁文件
277
499
  let taskLockPath = null;
278
500
  let workspaceLockPath = null;
279
-
501
+
280
502
  // 创建任务锁文件
281
503
  if (requirementName) {
282
- taskLockPath = _createTaskLock(taskName, requirementName, actorName);
504
+ taskLockPath = _createTaskLock(selectedTask.name, requirementName, actorName);
283
505
  }
284
-
506
+
285
507
  // 创建工作空间锁文件
286
508
  if (selectedWorkspace) {
287
- workspaceLockPath = _createWorkspaceLock(selectedWorkspace.name, taskName, actorName);
509
+ workspaceLockPath = _createWorkspaceLock(selectedWorkspace.name, selectedTask.name, actorName);
288
510
  }
289
-
290
- // 读取模板文件并填充参数,然后拷贝到剪切板
511
+
512
+ // 读取模板文件并填充参数,然后拷贝到剪贴板
291
513
  const templatePath = path.resolve(__dirname, '../_template/PROMPT/run.md.ejs');
292
-
514
+
293
515
  if (fs.existsSync(templatePath)) {
294
516
  try {
295
517
  // 读取并处理提示词模板
296
518
  const templateContent = await _readTemplate(templatePath);
297
- const renderedContent = _renderTemplate(templateContent, {
298
- TASK: taskName,
519
+ const renderedContent = _renderTemplate(templateContent, {
520
+ TASK: selectedTask.name,
299
521
  REQ: requirementName,
300
522
  ROOT: selectedWorkspace ? selectedWorkspace.path : 'source/<WORKSPACE_PATH>'
301
523
  });
302
-
524
+
303
525
  // 将提示词复制到剪贴板
304
- await _copyToClipboard(renderedContent);
305
- Ec.info(`✅ 任务 ${taskName} 准备完成,请查看剪切板中的提示词`);
526
+ await _copyToClipboard(renderedContent, requirementName, selectedTask.name);
527
+ Ec.info(`✅ 任务 ${selectedTask.name} 准备完成,请查看剪切板中的提示词`);
306
528
  } catch (error) {
307
- Ec.info(`✅ 任务 ${taskName} 查找完成`);
529
+ Ec.info(`✅ 任务 ${selectedTask.name} 查找完成`);
308
530
  }
309
531
  } else {
310
- Ec.info(`✅ 任务 ${taskName} 查找完成`);
532
+ Ec.info(`✅ 任务 ${selectedTask.name} 查找完成`);
311
533
  }
312
-
534
+
313
535
  Ec.askClose();
314
536
  process.exit(0);
315
537
 
@@ -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
  };