momo-ai 1.0.0 → 1.0.1
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 +116 -0
- package/package.json +2 -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 +130 -0
- package/src/executor/executePlan.js +134 -0
- package/src/executor/executeRepo.js +234 -0
- package/src/executor/executeRun.js +321 -0
- package/src/executor/executeShow.js +164 -0
- package/src/executor/executeTasks.js +260 -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,321 @@
|
|
|
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 _getAvailableWorkspaces = () => {
|
|
136
|
+
const sourceDir = path.resolve(process.cwd(), 'source');
|
|
137
|
+
const workspaces = [];
|
|
138
|
+
|
|
139
|
+
if (!fs.existsSync(sourceDir)) {
|
|
140
|
+
return workspaces;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 获取所有目录
|
|
144
|
+
const dirs = fs.readdirSync(sourceDir).filter(file =>
|
|
145
|
+
fs.statSync(path.join(sourceDir, file)).isDirectory()
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// 检查哪些工作空间可用(没有对应的.lock文件)
|
|
149
|
+
dirs.forEach(dir => {
|
|
150
|
+
const lockFilePath = path.resolve(sourceDir, `${dir}.lock`);
|
|
151
|
+
if (!fs.existsSync(lockFilePath)) {
|
|
152
|
+
workspaces.push({
|
|
153
|
+
name: dir,
|
|
154
|
+
path: path.join('source', dir)
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return workspaces;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 自动选择一个可用的工作空间
|
|
164
|
+
* @returns {Object|null} 选中的工作空间对象或null
|
|
165
|
+
*/
|
|
166
|
+
const _selectWorkspace = () => {
|
|
167
|
+
const workspaces = _getAvailableWorkspaces();
|
|
168
|
+
|
|
169
|
+
if (workspaces.length === 0) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 随机选择一个工作空间
|
|
174
|
+
const randomIndex = Math.floor(Math.random() * workspaces.length);
|
|
175
|
+
return workspaces[randomIndex];
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 创建任务锁文件
|
|
180
|
+
* @param {string} taskName 任务名称
|
|
181
|
+
* @param {string} requirementName 需求名称
|
|
182
|
+
* @param {string} actorName Actor名称
|
|
183
|
+
* @returns {string} 锁文件路径
|
|
184
|
+
*/
|
|
185
|
+
const _createTaskLock = (taskName, requirementName, actorName) => {
|
|
186
|
+
const taskLockDir = path.resolve(process.cwd(), 'specification', 'changes', requirementName, 'tasks');
|
|
187
|
+
const taskLockPath = path.resolve(taskLockDir, `${taskName}.lock`);
|
|
188
|
+
|
|
189
|
+
// 确保目录存在
|
|
190
|
+
if (!fs.existsSync(taskLockDir)) {
|
|
191
|
+
fs.mkdirSync(taskLockDir, { recursive: true });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 创建锁文件
|
|
195
|
+
fs.writeFileSync(taskLockPath, `Locked by: ${actorName}\nLocked at: ${new Date().toISOString()}\n`);
|
|
196
|
+
return taskLockPath;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 创建工作空间锁文件
|
|
201
|
+
* @param {string} workspaceName 工作空间名称
|
|
202
|
+
* @param {string} taskName 任务名称
|
|
203
|
+
* @param {string} actorName Actor名称
|
|
204
|
+
* @returns {string} 锁文件路径
|
|
205
|
+
*/
|
|
206
|
+
const _createWorkspaceLock = (workspaceName, taskName, actorName) => {
|
|
207
|
+
const sourceDir = path.resolve(process.cwd(), 'source');
|
|
208
|
+
const workspaceLockPath = path.resolve(sourceDir, `${workspaceName}.lock`);
|
|
209
|
+
|
|
210
|
+
// 创建锁文件
|
|
211
|
+
fs.writeFileSync(workspaceLockPath, `Task: ${taskName}\nLocked by: ${actorName}\nLocked at: ${new Date().toISOString()}\n`);
|
|
212
|
+
return workspaceLockPath;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
module.exports = async (options) => {
|
|
216
|
+
// 参数提取
|
|
217
|
+
const parsed = Ec.parseArgument(options);
|
|
218
|
+
|
|
219
|
+
const actorName = parsed.actor;
|
|
220
|
+
const taskName = parsed.task;
|
|
221
|
+
|
|
222
|
+
// 检查必要参数
|
|
223
|
+
if (!taskName) {
|
|
224
|
+
Ec.error("❌ 缺少必要参数 -t/--task,请提供任务名称");
|
|
225
|
+
Ec.askClose();
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
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
|
+
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('❌ 无效的选择');
|
|
255
|
+
Ec.askClose();
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
selectedTask = taskInstances[selectedIndex];
|
|
260
|
+
} else {
|
|
261
|
+
selectedTask = taskInstances[0];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 记录需求名称
|
|
265
|
+
const requirementName = selectedTask.requirement;
|
|
266
|
+
|
|
267
|
+
// 自动选择工作空间
|
|
268
|
+
const selectedWorkspace = _selectWorkspace();
|
|
269
|
+
|
|
270
|
+
if (!selectedWorkspace) {
|
|
271
|
+
Ec.error("❌ 所有工作空间都已被占用,请稍后再试");
|
|
272
|
+
Ec.askClose();
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 创建锁文件
|
|
277
|
+
let taskLockPath = null;
|
|
278
|
+
let workspaceLockPath = null;
|
|
279
|
+
|
|
280
|
+
// 创建任务锁文件
|
|
281
|
+
if (requirementName) {
|
|
282
|
+
taskLockPath = _createTaskLock(taskName, requirementName, actorName);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 创建工作空间锁文件
|
|
286
|
+
if (selectedWorkspace) {
|
|
287
|
+
workspaceLockPath = _createWorkspaceLock(selectedWorkspace.name, taskName, actorName);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// 读取模板文件并填充参数,然后拷贝到剪切板
|
|
291
|
+
const templatePath = path.resolve(__dirname, '../_template/PROMPT/run.md.ejs');
|
|
292
|
+
|
|
293
|
+
if (fs.existsSync(templatePath)) {
|
|
294
|
+
try {
|
|
295
|
+
// 读取并处理提示词模板
|
|
296
|
+
const templateContent = await _readTemplate(templatePath);
|
|
297
|
+
const renderedContent = _renderTemplate(templateContent, {
|
|
298
|
+
TASK: taskName,
|
|
299
|
+
REQ: requirementName,
|
|
300
|
+
ROOT: selectedWorkspace ? selectedWorkspace.path : 'source/<WORKSPACE_PATH>'
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// 将提示词复制到剪贴板
|
|
304
|
+
await _copyToClipboard(renderedContent);
|
|
305
|
+
Ec.info(`✅ 任务 ${taskName} 准备完成,请查看剪切板中的提示词`);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
Ec.info(`✅ 任务 ${taskName} 查找完成`);
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
Ec.info(`✅ 任务 ${taskName} 查找完成`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
Ec.askClose();
|
|
314
|
+
process.exit(0);
|
|
315
|
+
|
|
316
|
+
} catch (error) {
|
|
317
|
+
Ec.error(`❌ 执行过程中发生错误: ${error.message}`);
|
|
318
|
+
Ec.askClose();
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
@@ -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
|
+
};
|