momo-ai 1.0.0 → 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 +117 -0
- package/package.json +1 -1
- package/src/_template/LAIN/.momo/advanced/actor.md +42 -0
- package/src/_template/LAIN/.momo/scripts/submodule-clean.sh +56 -0
- package/src/_template/LAIN/AGENTS.md +103 -0
- package/src/_template/LAIN/changes/proposal.md +39 -0
- package/src/_template/LAIN/changes/tasks/task-detail.md +45 -0
- package/src/_template/LAIN/changes/tasks.md +49 -0
- package/src/_template/LAIN/execute/admin-n-f-dashboard.md +53 -0
- package/src/_template/LAIN/execute/admin-n-f-form.md +51 -0
- package/src/_template/LAIN/execute/admin-n-f-home.md +49 -0
- package/src/_template/LAIN/execute/admin-n-f-list.md +52 -0
- package/src/_template/LAIN/execute/admin-n-f-login.md +56 -0
- package/src/_template/LAIN/specification/project-model.md +13 -0
- package/src/_template/LAIN/specification/project.md +73 -0
- package/src/_template/LAIN/specification/requirement.md +25 -0
- package/src/_template/PROMPT/add.md.ejs +5 -0
- package/src/_template/PROMPT/plan.md.ejs +5 -0
- package/src/_template/PROMPT/run.md.ejs +9 -0
- package/src/_template/PROMPT/tasks.md.ejs +5 -0
- package/src/commander/actor.json +12 -0
- package/src/commander/actors.json +6 -0
- package/src/commander/add.json +12 -0
- package/src/commander/archive.json +12 -0
- package/src/commander/env.json +7 -0
- package/src/commander/help.json +7 -0
- package/src/commander/init.json +13 -0
- package/src/commander/list.json +7 -0
- package/src/commander/open.json +7 -0
- package/src/commander/plan.json +12 -0
- package/src/commander/repo.json +18 -0
- package/src/commander/run.json +18 -0
- package/src/commander/show.json +12 -0
- package/src/commander/tasks.json +18 -0
- package/src/commander/unlock.json +6 -0
- package/src/commander/validate.json +12 -0
- package/src/epic/history/ai.economy.impl.fn.excel.js +148 -0
- package/src/epic/history/ai.economy.impl.fn.execute.js +117 -0
- package/src/epic/history/ai.economy.impl.fn.java.js +140 -0
- package/src/epic/history/ai.economy.impl.fn.json.js +23 -0
- package/src/epic/history/ai.economy.impl.fn.plugin.js +160 -0
- package/src/epic/history/ai.economy.impl.fn.react.js +219 -0
- package/src/epic/history/ai.export.impl.fn.parse.js +345 -0
- package/src/epic/history/ai.export.impl.fn.seek.js +42 -0
- package/src/epic/history/ai.export.interface.fn.string.js +5 -0
- package/src/epic/history/ai.export.interface.io.js +69 -0
- package/src/epic/history/ai.export.interface.util.js +17 -0
- package/src/epic/history/ai.parse.metadata.js +94 -0
- package/src/epic/history/ai.path.fn.dir.operation.js +87 -0
- package/src/epic/history/ai.path.fn.io.command.js +43 -0
- package/src/epic/history/ai.path.fn.io.specification.js +59 -0
- package/src/epic/history/ai.path.fn.io.typed.js +63 -0
- package/src/epic/history/ai.path.fn.out.content.js +47 -0
- package/src/epic/history/ai.string.fn.str.util.js +63 -0
- package/src/epic/history/ai.uncork.fn.element.feature.js +52 -0
- package/src/epic/history/ai.uncork.fn.it.feature.js +118 -0
- package/src/epic/history/ai.uncork.fn.to.typed.js +74 -0
- package/src/epic/history/ai.under.fn.cx.evaluate.js +81 -0
- package/src/epic/history/ai.under.fn.fx.terminal.js +143 -0
- package/src/epic/history/ai.unified.fn.fn.error.code.js +108 -0
- package/src/epic/history/ai.unified.fn.is.decision.js +20 -0
- package/src/epic/history/ai.unified.fn.sorter.element.js +26 -0
- package/src/epic/history/zero.__.fn.find.util.js +37 -0
- package/src/epic/history/zero.__.v.constant.js +5 -0
- package/src/epic/index.js +50 -0
- package/src/epic/lain.fn.execute.js +116 -0
- package/src/epic/lain.fn.parse.js +94 -0
- package/src/epic/momo.fn.cx.js +81 -0
- package/src/epic/momo.fn.dir.js +87 -0
- package/src/epic/momo.fn.element.js +52 -0
- package/src/epic/momo.fn.find.js +37 -0
- package/src/epic/momo.fn.fx.js +143 -0
- package/src/epic/momo.fn.io.js +157 -0
- package/src/epic/momo.fn.is.js +20 -0
- package/src/epic/momo.fn.it.js +118 -0
- package/src/epic/momo.fn.log.js +50 -0
- package/src/epic/momo.fn.out.js +47 -0
- package/src/epic/momo.fn.sorter.js +26 -0
- package/src/epic/momo.fn.str.js +63 -0
- package/src/epic/momo.fn.to.js +74 -0
- package/src/epic/momo.v.constant.js +5 -0
- package/src/epic/momo.v.errorcode.js +108 -0
- package/src/executor/executeActor.js +113 -0
- package/src/executor/executeActors.js +58 -0
- package/src/executor/executeAdd.js +248 -0
- package/src/executor/executeArchive.js +124 -0
- package/src/executor/executeEnv.js +158 -0
- package/src/executor/executeHelp.js +23 -0
- package/src/executor/executeInit.js +321 -0
- package/src/executor/executeList.js +111 -0
- package/src/executor/executeOpen.js +150 -0
- package/src/executor/executePlan.js +134 -0
- package/src/executor/executeRepo.js +234 -0
- package/src/executor/executeRun.js +527 -0
- package/src/executor/executeShow.js +164 -0
- package/src/executor/executeTasks.js +323 -0
- package/src/executor/executeUnlock.js +110 -0
- package/src/executor/executeValidate.js +210 -0
- package/src/executor/index.js +35 -0
- package/src/momo.js +39 -1
- package/.idea/copilot.data.migration.agent.xml +0 -6
- package/.idea/copilot.data.migration.ask.xml +0 -6
- package/.idea/copilot.data.migration.ask2agent.xml +0 -6
- package/.idea/copilot.data.migration.edit.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/r2mo-lain.iml +0 -12
- package/.idea/vcs.xml +0 -6
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
const Ec = require('../epic');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const util = require('util');
|
|
5
|
+
const {spawn} = require('child_process');
|
|
6
|
+
const fsAsync = require('fs').promises;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 读取模板文件并提取 <!-- BEGIN --> 到 <!-- END --> 之间的内容
|
|
10
|
+
* @param {string} templatePath 模板文件路径
|
|
11
|
+
* @returns {Promise<string>} 提取的模板内容
|
|
12
|
+
*/
|
|
13
|
+
const _readTemplate = async (templatePath) => {
|
|
14
|
+
try {
|
|
15
|
+
const content = await fsAsync.readFile(templatePath, 'utf8');
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
let beginIndex = -1;
|
|
18
|
+
let endIndex = -1;
|
|
19
|
+
|
|
20
|
+
for (let i = 0; i < lines.length; i++) {
|
|
21
|
+
if (lines[i].includes('<!-- BEGIN -->')) {
|
|
22
|
+
beginIndex = i;
|
|
23
|
+
} else if (lines[i].includes('<!-- END -->')) {
|
|
24
|
+
endIndex = i;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (beginIndex !== -1 && endIndex !== -1) {
|
|
30
|
+
// 提取 BEGIN 和 END 之间的内容(不包括 BEGIN 和 END 行)
|
|
31
|
+
return lines.slice(beginIndex + 1, endIndex).join('\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return content;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
Ec.waiting(`读取模板文件失败: ${error.message}`);
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 渲染 EJS 模板
|
|
43
|
+
* @param {string} template 模板内容
|
|
44
|
+
* @param {Object} data 数据对象
|
|
45
|
+
* @returns {string} 渲染后的内容
|
|
46
|
+
*/
|
|
47
|
+
const _renderTemplate = (template, data) => {
|
|
48
|
+
let result = template;
|
|
49
|
+
for (const key in data) {
|
|
50
|
+
if (data.hasOwnProperty(key)) {
|
|
51
|
+
const regex = new RegExp(`<%=\\s*${key}\\s*%>`, 'g');
|
|
52
|
+
result = result.replace(regex, data[key]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 将内容复制到剪贴板
|
|
60
|
+
* @param {string} content 要复制的内容
|
|
61
|
+
*/
|
|
62
|
+
const _copyToClipboard = async (content) => {
|
|
63
|
+
try {
|
|
64
|
+
const proc = spawn('pbcopy', {stdio: 'pipe'});
|
|
65
|
+
proc.stdin.write(content);
|
|
66
|
+
proc.stdin.end();
|
|
67
|
+
|
|
68
|
+
// 等待复制操作完成
|
|
69
|
+
await new Promise((resolve, reject) => {
|
|
70
|
+
proc.on('close', (code) => {
|
|
71
|
+
if (code === 0) {
|
|
72
|
+
resolve();
|
|
73
|
+
} else {
|
|
74
|
+
reject(new Error(`pbcopy 进程退出码: ${code}`));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
proc.on('error', reject);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
Ec.waiting('📄 任务执行提示词已拷贝到剪切板');
|
|
81
|
+
} catch (error) {
|
|
82
|
+
Ec.waiting(`⚠️ 无法拷贝到剪切板: ${error.message}`);
|
|
83
|
+
// 备选方案:显示内容
|
|
84
|
+
Ec.waiting('提示词内容:');
|
|
85
|
+
console.log(content);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 查找所有匹配的任务实例
|
|
91
|
+
* @param {string} taskName 任务名称
|
|
92
|
+
* @returns {Array} 任务实例列表
|
|
93
|
+
*/
|
|
94
|
+
const _findTaskInstances = (taskName) => {
|
|
95
|
+
const changesDir = path.resolve(process.cwd(), 'specification', 'changes');
|
|
96
|
+
const taskInstances = [];
|
|
97
|
+
|
|
98
|
+
if (!fs.existsSync(changesDir)) {
|
|
99
|
+
return taskInstances;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 获取所有需求目录
|
|
103
|
+
const requirements = fs.readdirSync(changesDir).filter(file =>
|
|
104
|
+
fs.statSync(path.join(changesDir, file)).isDirectory()
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// 查找所有匹配的任务文件
|
|
108
|
+
requirements.forEach(requirement => {
|
|
109
|
+
const tasksDir = path.resolve(changesDir, requirement, 'tasks');
|
|
110
|
+
|
|
111
|
+
// 检查 tasks 目录是否存在
|
|
112
|
+
if (fs.existsSync(tasksDir) && fs.statSync(tasksDir).isDirectory()) {
|
|
113
|
+
const taskFile = `${taskName}.md`;
|
|
114
|
+
const taskPath = path.resolve(tasksDir, taskFile);
|
|
115
|
+
|
|
116
|
+
// 检查任务文件是否存在
|
|
117
|
+
if (fs.existsSync(taskPath)) {
|
|
118
|
+
taskInstances.push({
|
|
119
|
+
name: taskName,
|
|
120
|
+
path: taskPath,
|
|
121
|
+
requirement: requirement,
|
|
122
|
+
relativePath: path.relative(process.cwd(), taskPath)
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return taskInstances;
|
|
129
|
+
};
|
|
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
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 获取所有可用的工作空间(排除已被锁定的)
|
|
180
|
+
* @returns {Array} 可用工作空间列表
|
|
181
|
+
*/
|
|
182
|
+
const _getAvailableWorkspaces = () => {
|
|
183
|
+
const sourceDir = path.resolve(process.cwd(), 'source');
|
|
184
|
+
const workspaces = [];
|
|
185
|
+
|
|
186
|
+
if (!fs.existsSync(sourceDir)) {
|
|
187
|
+
return workspaces;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 获取所有目录
|
|
191
|
+
const dirs = fs.readdirSync(sourceDir).filter(file =>
|
|
192
|
+
fs.statSync(path.join(sourceDir, file)).isDirectory()
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// 检查哪些工作空间可用(没有对应的.lock文件)
|
|
196
|
+
dirs.forEach(dir => {
|
|
197
|
+
const lockFilePath = path.resolve(sourceDir, `${dir}.lock`);
|
|
198
|
+
if (!fs.existsSync(lockFilePath)) {
|
|
199
|
+
workspaces.push({
|
|
200
|
+
name: dir,
|
|
201
|
+
path: path.join('source', dir)
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return workspaces;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 自动选择一个可用的工作空间
|
|
211
|
+
* @returns {Object|null} 选中的工作空间对象或null
|
|
212
|
+
*/
|
|
213
|
+
const _selectWorkspace = () => {
|
|
214
|
+
const workspaces = _getAvailableWorkspaces();
|
|
215
|
+
|
|
216
|
+
if (workspaces.length === 0) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 随机选择一个工作空间
|
|
221
|
+
const randomIndex = Math.floor(Math.random() * workspaces.length);
|
|
222
|
+
return workspaces[randomIndex];
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 创建任务锁文件
|
|
227
|
+
* @param {string} taskName 任务名称
|
|
228
|
+
* @param {string} requirementName 需求名称
|
|
229
|
+
* @param {string} actorName Actor名称
|
|
230
|
+
* @returns {string} 锁文件路径
|
|
231
|
+
*/
|
|
232
|
+
const _createTaskLock = (taskName, requirementName, actorName) => {
|
|
233
|
+
const taskLockDir = path.resolve(process.cwd(), 'specification', 'changes', requirementName, 'tasks');
|
|
234
|
+
const taskLockPath = path.resolve(taskLockDir, `${taskName}.lock`);
|
|
235
|
+
|
|
236
|
+
// 确保目录存在
|
|
237
|
+
if (!fs.existsSync(taskLockDir)) {
|
|
238
|
+
fs.mkdirSync(taskLockDir, {recursive: true});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 创建锁文件
|
|
242
|
+
fs.writeFileSync(taskLockPath, `Locked by: ${actorName}\nLocked at: ${new Date().toISOString()}\n`);
|
|
243
|
+
return taskLockPath;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 创建工作空间锁文件
|
|
248
|
+
* @param {string} workspaceName 工作空间名称
|
|
249
|
+
* @param {string} taskName 任务名称
|
|
250
|
+
* @param {string} actorName Actor名称
|
|
251
|
+
* @returns {string} 锁文件路径
|
|
252
|
+
*/
|
|
253
|
+
const _createWorkspaceLock = (workspaceName, taskName, actorName) => {
|
|
254
|
+
const sourceDir = path.resolve(process.cwd(), 'source');
|
|
255
|
+
const workspaceLockPath = path.resolve(sourceDir, `${workspaceName}.lock`);
|
|
256
|
+
|
|
257
|
+
// 创建锁文件
|
|
258
|
+
fs.writeFileSync(workspaceLockPath, `Task: ${taskName}\nLocked by: ${actorName}\nLocked at: ${new Date().toISOString()}\n`);
|
|
259
|
+
return workspaceLockPath;
|
|
260
|
+
};
|
|
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
|
+
|
|
314
|
+
module.exports = async (options) => {
|
|
315
|
+
// 参数提取
|
|
316
|
+
const parsed = Ec.parseArgument(options);
|
|
317
|
+
|
|
318
|
+
const actorName = parsed.actor;
|
|
319
|
+
const taskName = parsed.task;
|
|
320
|
+
|
|
321
|
+
try {
|
|
322
|
+
let selectedTask;
|
|
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("❌ 未找到任何任务");
|
|
333
|
+
Ec.askClose();
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
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
|
+
}
|
|
390
|
+
} else {
|
|
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}`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// 记录需求名称
|
|
471
|
+
const requirementName = selectedTask.requirement;
|
|
472
|
+
|
|
473
|
+
// 自动选择工作空间
|
|
474
|
+
const selectedWorkspace = _selectWorkspace();
|
|
475
|
+
|
|
476
|
+
if (!selectedWorkspace) {
|
|
477
|
+
Ec.error("❌ 所有工作空间都已被占用,请稍后再试");
|
|
478
|
+
Ec.askClose();
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// 创建锁文件
|
|
483
|
+
let taskLockPath = null;
|
|
484
|
+
let workspaceLockPath = null;
|
|
485
|
+
|
|
486
|
+
// 创建任务锁文件
|
|
487
|
+
if (requirementName) {
|
|
488
|
+
taskLockPath = _createTaskLock(selectedTask.name, requirementName, actorName);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// 创建工作空间锁文件
|
|
492
|
+
if (selectedWorkspace) {
|
|
493
|
+
workspaceLockPath = _createWorkspaceLock(selectedWorkspace.name, selectedTask.name, actorName);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 读取模板文件并填充参数,然后拷贝到剪贴板
|
|
497
|
+
const templatePath = path.resolve(__dirname, '../_template/PROMPT/run.md.ejs');
|
|
498
|
+
|
|
499
|
+
if (fs.existsSync(templatePath)) {
|
|
500
|
+
try {
|
|
501
|
+
// 读取并处理提示词模板
|
|
502
|
+
const templateContent = await _readTemplate(templatePath);
|
|
503
|
+
const renderedContent = _renderTemplate(templateContent, {
|
|
504
|
+
TASK: selectedTask.name,
|
|
505
|
+
REQ: requirementName,
|
|
506
|
+
ROOT: selectedWorkspace ? selectedWorkspace.path : 'source/<WORKSPACE_PATH>'
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// 将提示词复制到剪贴板
|
|
510
|
+
await _copyToClipboard(renderedContent);
|
|
511
|
+
Ec.info(`✅ 任务 ${selectedTask.name} 准备完成,请查看剪切板中的提示词`);
|
|
512
|
+
} catch (error) {
|
|
513
|
+
Ec.info(`✅ 任务 ${selectedTask.name} 查找完成`);
|
|
514
|
+
}
|
|
515
|
+
} else {
|
|
516
|
+
Ec.info(`✅ 任务 ${selectedTask.name} 查找完成`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
Ec.askClose();
|
|
520
|
+
process.exit(0);
|
|
521
|
+
|
|
522
|
+
} catch (error) {
|
|
523
|
+
Ec.error(`❌ 执行过程中发生错误: ${error.message}`);
|
|
524
|
+
Ec.askClose();
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const Ec = require('../epic');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 解析 Markdown 文件内容,提取标题结构
|
|
7
|
+
* @param {string} content 文件内容
|
|
8
|
+
* @returns {Array} 标题结构数组
|
|
9
|
+
*/
|
|
10
|
+
const _parseMarkdownStructure = (content) => {
|
|
11
|
+
const lines = content.split('\n');
|
|
12
|
+
const structure = [];
|
|
13
|
+
const stack = [];
|
|
14
|
+
|
|
15
|
+
lines.forEach(line => {
|
|
16
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
17
|
+
if (headingMatch) {
|
|
18
|
+
const level = headingMatch[1].length;
|
|
19
|
+
const title = headingMatch[2].trim();
|
|
20
|
+
|
|
21
|
+
// 调整栈以匹配当前层级
|
|
22
|
+
while (stack.length >= level) {
|
|
23
|
+
stack.pop();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const item = { level, title, children: [] };
|
|
27
|
+
|
|
28
|
+
if (stack.length > 0) {
|
|
29
|
+
stack[stack.length - 1].children.push(item);
|
|
30
|
+
} else {
|
|
31
|
+
structure.push(item);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
stack.push(item);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
return structure;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 打印树型结构
|
|
43
|
+
* @param {Array} items 结构项数组
|
|
44
|
+
* @param {number} indent 缩进级别
|
|
45
|
+
*/
|
|
46
|
+
const _printTree = (items, indent = 0) => {
|
|
47
|
+
items.forEach(item => {
|
|
48
|
+
const prefix = ' '.repeat(indent);
|
|
49
|
+
Ec.waiting(`${prefix}├─ ${item.title}`);
|
|
50
|
+
|
|
51
|
+
if (item.children && item.children.length > 0) {
|
|
52
|
+
_printTree(item.children, indent + 1);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 提取任务清单
|
|
59
|
+
* @param {string} tasksFile 任务文件路径
|
|
60
|
+
* @returns {Array} 任务列表
|
|
61
|
+
*/
|
|
62
|
+
const _extractTaskList = (tasksFile) => {
|
|
63
|
+
const tasks = [];
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
if (!fs.existsSync(tasksFile)) {
|
|
67
|
+
return tasks;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const content = fs.readFileSync(tasksFile, 'utf8');
|
|
71
|
+
const lines = content.split('\n');
|
|
72
|
+
|
|
73
|
+
// 使用与 executeList.js 相同的逻辑来匹配任务
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
// 匹配任务格式,例如: - [ ] [任务1简要描述](tasks/M01-T001-01.md)
|
|
76
|
+
if (line.trim().startsWith('- [') && line.includes('] [') && line.includes('](') && line.includes('tasks/')) {
|
|
77
|
+
// 提取任务描述部分,使用更兼容的正则表达式
|
|
78
|
+
const titleMatch = line.match(/] $$(.*?)$$$(tasks\/)/);
|
|
79
|
+
if (titleMatch) {
|
|
80
|
+
// 获取标题和链接
|
|
81
|
+
const fullMatch = line.match(/] $$(.*?)$$$(tasks\/[^)]+)$/);
|
|
82
|
+
if (fullMatch) {
|
|
83
|
+
tasks.push({
|
|
84
|
+
title: fullMatch[1],
|
|
85
|
+
link: fullMatch[2]
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
// 备用方案:简单提取方括号中的内容
|
|
90
|
+
const simpleMatch = line.match(/\[([^\]]+)\]/g);
|
|
91
|
+
if (simpleMatch && simpleMatch.length >= 2) {
|
|
92
|
+
tasks.push({
|
|
93
|
+
title: simpleMatch[1].substring(1, simpleMatch[1].length - 1),
|
|
94
|
+
link: ''
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
// 忽略错误,返回空任务列表
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return tasks;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
module.exports = (options) => {
|
|
108
|
+
// 参数提取
|
|
109
|
+
const parsed = Ec.parseArgument(options);
|
|
110
|
+
const requirementName = parsed.name || parsed.n;
|
|
111
|
+
|
|
112
|
+
// 验证参数
|
|
113
|
+
if (!requirementName) {
|
|
114
|
+
Ec.error("❌ 请提供需求名称 (-n, --name)");
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
// 构建路径
|
|
120
|
+
const projectDir = process.cwd();
|
|
121
|
+
const changesDir = path.join(projectDir, 'specification', 'changes');
|
|
122
|
+
const requirementDir = path.join(changesDir, requirementName);
|
|
123
|
+
|
|
124
|
+
// 检查需求目录是否存在
|
|
125
|
+
if (!fs.existsSync(requirementDir)) {
|
|
126
|
+
Ec.error(`❌ 需求 "${requirementName}" 不存在`);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 读取 proposal.md 文件
|
|
131
|
+
const proposalFile = path.join(requirementDir, 'proposal.md');
|
|
132
|
+
if (!fs.existsSync(proposalFile)) {
|
|
133
|
+
Ec.error(`❌ 需求 "${requirementName}" 中未找到 proposal.md 文件`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const content = fs.readFileSync(proposalFile, 'utf8');
|
|
138
|
+
|
|
139
|
+
// 解析并显示结构
|
|
140
|
+
Ec.waiting(`需求名称: ${requirementName}`);
|
|
141
|
+
Ec.waiting('文档结构:');
|
|
142
|
+
const structure = _parseMarkdownStructure(content);
|
|
143
|
+
_printTree(structure);
|
|
144
|
+
|
|
145
|
+
// 提取并显示任务清单
|
|
146
|
+
const tasksFile = path.join(requirementDir, 'tasks.md');
|
|
147
|
+
const tasks = _extractTaskList(tasksFile);
|
|
148
|
+
|
|
149
|
+
Ec.waiting(`\n任务清单 (共${tasks.length}个任务):`);
|
|
150
|
+
if (tasks.length > 0) {
|
|
151
|
+
tasks.forEach((task, index) => {
|
|
152
|
+
Ec.waiting(` ${index + 1}. ${task.title}`);
|
|
153
|
+
});
|
|
154
|
+
} else {
|
|
155
|
+
Ec.waiting(' 无');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
Ec.info('✅ 需求详情显示完成');
|
|
159
|
+
process.exit(0);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
Ec.error(`❌ 执行过程中发生错误: ${error.message}`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
};
|