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 +8 -7
- package/package.json +1 -2
- package/src/_template/PROMPT/run.md.ejs +1 -1
- package/src/executor/executeOpen.js +25 -5
- package/src/executor/executeRun.js +285 -79
- package/src/executor/executeTasks.js +99 -36
package/README.md
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Momo SDD - Spec Driven Development 工具
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/momo-ai)
|
|
4
|
-
|
|
5
|
-

|
|
3
|
+
 | [](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
|
|
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
|
-
|
|
100
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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', {
|
|
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, {
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
488
|
+
taskLockPath = _createTaskLock(selectedTask.name, requirementName, actorName);
|
|
283
489
|
}
|
|
284
|
-
|
|
490
|
+
|
|
285
491
|
// 创建工作空间锁文件
|
|
286
492
|
if (selectedWorkspace) {
|
|
287
|
-
workspaceLockPath = _createWorkspaceLock(selectedWorkspace.name,
|
|
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:
|
|
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(`✅ 任务 ${
|
|
511
|
+
Ec.info(`✅ 任务 ${selectedTask.name} 准备完成,请查看剪切板中的提示词`);
|
|
306
512
|
} catch (error) {
|
|
307
|
-
Ec.info(`✅ 任务 ${
|
|
513
|
+
Ec.info(`✅ 任务 ${selectedTask.name} 查找完成`);
|
|
308
514
|
}
|
|
309
515
|
} else {
|
|
310
|
-
Ec.info(`✅ 任务 ${
|
|
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}
|
|
93
|
-
* @
|
|
90
|
+
* 从 Markdown 文件中提取第一个标题
|
|
91
|
+
* @param {string} filePath 文件路径
|
|
92
|
+
* @returns {string} 第一个标题内容
|
|
94
93
|
*/
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
};
|