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 +28 -27
- package/package.json +1 -2
- package/src/_template/PROMPT/run.md.ejs +1 -1
- package/src/commander/run.json +1 -1
- package/src/executor/executeHelp.js +6 -1
- package/src/executor/executeInit.js +2 -1
- package/src/executor/executeOpen.js +25 -5
- package/src/executor/executePlan.js +17 -2
- package/src/executor/executeRun.js +303 -81
- package/src/executor/executeTasks.js +99 -36
package/README.md
CHANGED
|
@@ -1,30 +1,28 @@
|
|
|
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.
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
`AI 开发 + 人工审核`,本工具的宗旨就是 `prompt` 的模板化。
|
|
20
|
-
|
|
21
|
-
> 多个异构Agent!中文优先!
|
|
20
|
+
完整生命周期的整体开发流程,是 SDD 的一种落地手段——暴力、简单、易懂。由于辅助工具可选择模型,所以本工具不提供模型选择,模型本身取决于你所使用的工具本身,命令行只是一个辅助,核心开发模式为:
|
|
21
|
+
`AI 开发 + 人工审核`,本工具的核心是 `prompt` 的模板化。
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
> 除开 `momo` 命令后续会提供和模型直接交互的 `lain` 命令,近似 `iFlow / openspec` 的功能。
|
|
24
24
|
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
+
### 2.4. 控制台:定模型
|
|
94
100
|
|
|
95
|
-
|
|
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
|
-
##
|
|
103
|
+
## 3. 参考链接
|
|
103
104
|
|
|
104
|
-
###
|
|
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
|
-
###
|
|
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.
|
|
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
|
package/src/commander/run.json
CHANGED
|
@@ -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
|
-
|
|
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')) {
|
|
@@ -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);
|
|
@@ -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 {
|
|
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', {
|
|
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, {
|
|
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
|
-
|
|
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('❌ 无效的选择');
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
504
|
+
taskLockPath = _createTaskLock(selectedTask.name, requirementName, actorName);
|
|
283
505
|
}
|
|
284
|
-
|
|
506
|
+
|
|
285
507
|
// 创建工作空间锁文件
|
|
286
508
|
if (selectedWorkspace) {
|
|
287
|
-
workspaceLockPath = _createWorkspaceLock(selectedWorkspace.name,
|
|
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:
|
|
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(`✅ 任务 ${
|
|
526
|
+
await _copyToClipboard(renderedContent, requirementName, selectedTask.name);
|
|
527
|
+
Ec.info(`✅ 任务 ${selectedTask.name} 准备完成,请查看剪切板中的提示词`);
|
|
306
528
|
} catch (error) {
|
|
307
|
-
Ec.info(`✅ 任务 ${
|
|
529
|
+
Ec.info(`✅ 任务 ${selectedTask.name} 查找完成`);
|
|
308
530
|
}
|
|
309
531
|
} else {
|
|
310
|
-
Ec.info(`✅ 任务 ${
|
|
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}
|
|
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
|
};
|