momo-ai 1.0.19 → 1.0.21
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/.claude/skills/algorithmic-art/LICENSE.txt +202 -0
- package/.claude/skills/algorithmic-art/SKILL.md +405 -0
- package/.claude/skills/algorithmic-art/templates/generator_template.js +223 -0
- package/.claude/skills/algorithmic-art/templates/viewer.html +599 -0
- package/.claude/skills/r2mo-rad-lain/PROMPT.md +281 -0
- package/.claude/skills/r2mo-rad-lain/README.md +192 -0
- package/.claude/skills/r2mo-rad-lain/SKILL.md +412 -0
- package/.claude/skills/r2mo-rad-lain/examples/argument-parsing.js +154 -0
- package/.claude/skills/r2mo-rad-lain/examples/file-operations.js +182 -0
- package/.claude/skills/r2mo-rad-lain/file-utils-api.md +281 -0
- package/.claude/skills/r2mo-rad-lain/menu-api.md +187 -0
- package/.claude/skills/r2mo-rad-lain/scripts/file-utils.js +223 -0
- package/.claude/skills/r2mo-rad-lain/scripts/menu.js +289 -0
- package/.claude/skills/r2mo-rad-lain/scripts/yaml-parser.js +209 -0
- package/.claude/skills/r2mo-rad-lain/templates/command.json.template +13 -0
- package/.claude/skills/r2mo-rad-lain/templates/executor.js.template +32 -0
- package/.claude/skills/r2mo-rad-lain/templates/interactive-menu.js.template +221 -0
- package/.cursor/mcp.json +17 -0
- package/.obsidian/app.json +1 -0
- package/.obsidian/appearance.json +4 -0
- package/.obsidian/community-plugins.json +4 -0
- package/.obsidian/core-plugins.json +33 -0
- package/.obsidian/plugins/ai-agent/main.js +98495 -0
- package/.obsidian/plugins/ai-agent/manifest.json +11 -0
- package/.obsidian/plugins/ai-agent/styles.css +806 -0
- package/.obsidian/plugins/dataview/main.js +20876 -0
- package/.obsidian/plugins/dataview/manifest.json +11 -0
- package/.obsidian/plugins/dataview/styles.css +141 -0
- package/.obsidian/plugins/obsidian-excalidraw-plugin/main.js +10 -0
- package/.obsidian/plugins/obsidian-excalidraw-plugin/manifest.json +12 -0
- package/.obsidian/plugins/obsidian-excalidraw-plugin/styles.css +1 -0
- package/.obsidian/plugins/templater-obsidian/main.js +45 -0
- package/.obsidian/plugins/templater-obsidian/manifest.json +11 -0
- package/.obsidian/plugins/templater-obsidian/styles.css +226 -0
- package/.obsidian/plugins/terminal/main.js +200 -0
- package/.obsidian/plugins/terminal/manifest.json +14 -0
- package/.obsidian/plugins/terminal/styles.css +32 -0
- package/.obsidian/themes/AnuPpuccin/manifest.json +7 -0
- package/.obsidian/themes/AnuPpuccin/theme.css +9080 -0
- package/.obsidian/themes/Things/manifest.json +7 -0
- package/.obsidian/themes/Things/theme.css +1628 -0
- package/.obsidian/workspace.json +196 -0
- package/README.md +10 -123
- package/docs/images/logo.jpeg +0 -0
- package/install.sh +1 -0
- package/package.json +6 -2
- package/skills/r2mo-rad-lain/SKILL.md +101 -0
- package/src/_agent/trae/momo-datamodel.json +2 -1
- package/src/_agent/trae/momo-module-req.json +2 -1
- package/src/_agent/trae/momo-system-req.json +2 -1
- package/src/_agent/trae/momo-task.json +2 -1
- package/src/_agent/trae/momo-taskplan.json +2 -1
- package/src/_mcp/skills-server.mjs +70 -0
- package/src/_skill/repositories.json +16 -0
- package/src/_template/LAIN/.momo/advanced/refer.json +0 -4
- package/src/commander/help.json +5 -0
- package/src/commander/mcp.json +13 -0
- package/src/commander/open.json +8 -2
- package/src/commander/skills.json +20 -0
- package/src/executor/executeEnv.js +48 -38
- package/src/executor/executeHelp.js +77 -16
- package/src/executor/executeInit.js +203 -149
- package/src/executor/executeMcp.js +290 -0
- package/src/executor/executeOpen.js +144 -125
- package/src/executor/executeSkills.js +747 -0
- package/src/executor/index.js +5 -39
- package/src/momo.js +2 -1
- package/src/utils/momo-args.js +39 -0
- package/src/utils/momo-file-utils.js +75 -0
- package/src/utils/momo-menu.js +54 -0
- package/src/commander/actor.json +0 -12
- package/src/commander/actors.json +0 -6
- package/src/commander/add.json +0 -12
- package/src/commander/agent.json +0 -12
- package/src/commander/agentcfg.json +0 -5
- package/src/commander/archive.json +0 -12
- package/src/commander/commit.json +0 -12
- package/src/commander/console.json +0 -7
- package/src/commander/lain.json +0 -7
- package/src/commander/list.json +0 -7
- package/src/commander/plan.json +0 -12
- package/src/commander/project.json +0 -12
- package/src/commander/pull.json +0 -6
- package/src/commander/push.json +0 -6
- package/src/commander/repo.json +0 -18
- package/src/commander/run.json +0 -18
- package/src/commander/show.json +0 -12
- package/src/commander/tasks.json +0 -18
- package/src/commander/unlock.json +0 -6
- package/src/commander/validate.json +0 -12
- package/src/executor/executeActor.js +0 -133
- package/src/executor/executeActors.js +0 -58
- package/src/executor/executeAdd.js +0 -307
- package/src/executor/executeAgent.js +0 -224
- package/src/executor/executeAgentCfg.js +0 -195
- package/src/executor/executeArchive.js +0 -124
- package/src/executor/executeCommit.js +0 -202
- package/src/executor/executeConsole.js +0 -142
- package/src/executor/executeList.js +0 -133
- package/src/executor/executePlan.js +0 -164
- package/src/executor/executeProject.js +0 -312
- package/src/executor/executePull.js +0 -127
- package/src/executor/executePush.js +0 -243
- package/src/executor/executeRepo.js +0 -238
- package/src/executor/executeRun.js +0 -644
- package/src/executor/executeShow.js +0 -164
- package/src/executor/executeTasks.js +0 -384
- package/src/executor/executeUnlock.js +0 -110
- package/src/executor/executeValidate.js +0 -210
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文件操作示例
|
|
3
|
+
* 演示常用的文件操作模式
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const fsAsync = require('fs').promises;
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
// ============================================================
|
|
13
|
+
// 递归拷贝目录
|
|
14
|
+
// ============================================================
|
|
15
|
+
|
|
16
|
+
const copyDirectory = async (src, dest) => {
|
|
17
|
+
// 确保目标目录存在
|
|
18
|
+
await fsAsync.mkdir(dest, { recursive: true });
|
|
19
|
+
|
|
20
|
+
const items = await fsAsync.readdir(src);
|
|
21
|
+
|
|
22
|
+
for (const item of items) {
|
|
23
|
+
const srcPath = path.join(src, item);
|
|
24
|
+
const destPath = path.join(dest, item);
|
|
25
|
+
const stat = await fsAsync.stat(srcPath);
|
|
26
|
+
|
|
27
|
+
if (stat.isDirectory()) {
|
|
28
|
+
await copyDirectory(srcPath, destPath);
|
|
29
|
+
} else {
|
|
30
|
+
await fsAsync.copyFile(srcPath, destPath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ============================================================
|
|
36
|
+
// 扫描目录
|
|
37
|
+
// ============================================================
|
|
38
|
+
|
|
39
|
+
const scanDirectory = (dirPath, filter = () => true) => {
|
|
40
|
+
const results = [];
|
|
41
|
+
|
|
42
|
+
if (!fs.existsSync(dirPath)) {
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const items = fs.readdirSync(dirPath);
|
|
47
|
+
|
|
48
|
+
for (const item of items) {
|
|
49
|
+
const itemPath = path.join(dirPath, item);
|
|
50
|
+
const stat = fs.statSync(itemPath);
|
|
51
|
+
|
|
52
|
+
if (stat.isDirectory() && filter(item, itemPath)) {
|
|
53
|
+
results.push({
|
|
54
|
+
name: item,
|
|
55
|
+
path: itemPath,
|
|
56
|
+
isDirectory: true
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return results;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// ============================================================
|
|
65
|
+
// 解析 YAML 头部 (Frontmatter)
|
|
66
|
+
// ============================================================
|
|
67
|
+
|
|
68
|
+
const parseYamlFrontmatter = (filePath) => {
|
|
69
|
+
try {
|
|
70
|
+
if (!fs.existsSync(filePath)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
75
|
+
const lines = content.split('\n');
|
|
76
|
+
|
|
77
|
+
let inYaml = false;
|
|
78
|
+
let yamlLines = [];
|
|
79
|
+
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
if (line.trim() === '---') {
|
|
82
|
+
if (!inYaml) {
|
|
83
|
+
inYaml = true;
|
|
84
|
+
continue;
|
|
85
|
+
} else {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (inYaml) {
|
|
90
|
+
yamlLines.push(line);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (yamlLines.length === 0) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 简单 YAML 解析
|
|
99
|
+
const metadata = {};
|
|
100
|
+
let arrayKey = null;
|
|
101
|
+
|
|
102
|
+
for (const line of yamlLines) {
|
|
103
|
+
if (line.trim() === '') continue;
|
|
104
|
+
|
|
105
|
+
// 数组项
|
|
106
|
+
const arrayMatch = line.match(/^\s+-\s+(.+)$/);
|
|
107
|
+
if (arrayMatch && arrayKey) {
|
|
108
|
+
if (!metadata[arrayKey]) {
|
|
109
|
+
metadata[arrayKey] = [];
|
|
110
|
+
}
|
|
111
|
+
metadata[arrayKey].push(arrayMatch[1].trim());
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 键值对
|
|
116
|
+
const kvMatch = line.match(/^(\w+):\s*(.*)$/);
|
|
117
|
+
if (kvMatch) {
|
|
118
|
+
const key = kvMatch[1];
|
|
119
|
+
const value = kvMatch[2].trim();
|
|
120
|
+
|
|
121
|
+
if (value === '') {
|
|
122
|
+
arrayKey = key;
|
|
123
|
+
metadata[key] = [];
|
|
124
|
+
} else {
|
|
125
|
+
// 移除引号
|
|
126
|
+
metadata[key] = value.replace(/^["']|["']$/g, '');
|
|
127
|
+
arrayKey = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return metadata;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// ============================================================
|
|
139
|
+
// 临时目录管理
|
|
140
|
+
// ============================================================
|
|
141
|
+
|
|
142
|
+
const createTempDir = (prefix = 'momo') => {
|
|
143
|
+
return path.join(os.tmpdir(), `.${prefix}-${Date.now()}`);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const cleanupTempDir = async (tempDir) => {
|
|
147
|
+
try {
|
|
148
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
149
|
+
await fsAsync.rm(tempDir, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.warn(`清理临时目录失败: ${error.message}`);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// ============================================================
|
|
157
|
+
// Git 操作
|
|
158
|
+
// ============================================================
|
|
159
|
+
|
|
160
|
+
const gitClone = (url, destDir) => {
|
|
161
|
+
try {
|
|
162
|
+
execSync(`git clone --depth 1 "${url}" "${destDir}"`, {
|
|
163
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
164
|
+
});
|
|
165
|
+
return true;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
throw new Error(`克隆仓库失败: ${error.message}`);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// ============================================================
|
|
172
|
+
// 导出
|
|
173
|
+
// ============================================================
|
|
174
|
+
|
|
175
|
+
module.exports = {
|
|
176
|
+
copyDirectory,
|
|
177
|
+
scanDirectory,
|
|
178
|
+
parseYamlFrontmatter,
|
|
179
|
+
createTempDir,
|
|
180
|
+
cleanupTempDir,
|
|
181
|
+
gitClone
|
|
182
|
+
};
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# 文件操作工具 API
|
|
2
|
+
|
|
3
|
+
本文档描述 `scripts/file-utils.js` 和 `scripts/yaml-parser.js` 模块的完整 API。
|
|
4
|
+
|
|
5
|
+
## file-utils.js
|
|
6
|
+
|
|
7
|
+
### 快速开始
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
const {
|
|
11
|
+
copyDir,
|
|
12
|
+
scanDir,
|
|
13
|
+
createTempDir,
|
|
14
|
+
cleanup,
|
|
15
|
+
ensureDir,
|
|
16
|
+
exists,
|
|
17
|
+
readJson,
|
|
18
|
+
writeJson,
|
|
19
|
+
gitClone
|
|
20
|
+
} = require('./scripts/file-utils');
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### API 参考
|
|
24
|
+
|
|
25
|
+
#### copyDir(src, dest)
|
|
26
|
+
|
|
27
|
+
递归拷贝目录。
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
await copyDir('/source/path', '/dest/path');
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
| 参数 | 类型 | 描述 |
|
|
34
|
+
|------|------|------|
|
|
35
|
+
| src | string | 源目录路径 |
|
|
36
|
+
| dest | string | 目标目录路径 |
|
|
37
|
+
|
|
38
|
+
**返回值:** Promise\<void\>
|
|
39
|
+
|
|
40
|
+
#### scanDir(dirPath, filter?)
|
|
41
|
+
|
|
42
|
+
扫描目录,返回子目录列表。
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
// 扫描所有子目录
|
|
46
|
+
const dirs = scanDir('/some/path');
|
|
47
|
+
|
|
48
|
+
// 带过滤条件(排除隐藏目录)
|
|
49
|
+
const dirs = scanDir('/path', (name) => !name.startsWith('.'));
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
| 参数 | 类型 | 描述 |
|
|
53
|
+
|------|------|------|
|
|
54
|
+
| dirPath | string | 目录路径 |
|
|
55
|
+
| filter | Function | 过滤函数 (name, fullPath) => boolean |
|
|
56
|
+
|
|
57
|
+
**返回值:** Array\<{ name, path, isDirectory }\>
|
|
58
|
+
|
|
59
|
+
#### createTempDir(prefix?)
|
|
60
|
+
|
|
61
|
+
创建临时目录路径(不会实际创建目录)。
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
const tempDir = createTempDir('my-task');
|
|
65
|
+
// 返回: /tmp/.my-task-1234567890
|
|
66
|
+
|
|
67
|
+
// 需要配合 ensureDir 或 gitClone 等实际创建
|
|
68
|
+
await ensureDir(tempDir);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
| 参数 | 类型 | 默认值 | 描述 |
|
|
72
|
+
|------|------|--------|------|
|
|
73
|
+
| prefix | string | 'momo' | 目录前缀 |
|
|
74
|
+
|
|
75
|
+
**返回值:** string
|
|
76
|
+
|
|
77
|
+
#### cleanup(dirPath)
|
|
78
|
+
|
|
79
|
+
删除目录(递归)。
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
await cleanup('/tmp/.momo-123');
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
| 参数 | 类型 | 描述 |
|
|
86
|
+
|------|------|------|
|
|
87
|
+
| dirPath | string | 目录路径 |
|
|
88
|
+
|
|
89
|
+
**返回值:** Promise\<boolean\> - 是否成功
|
|
90
|
+
|
|
91
|
+
#### ensureDir(dirPath)
|
|
92
|
+
|
|
93
|
+
确保目录存在(不存在则创建)。
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
await ensureDir('/path/to/dir');
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### exists(filePath)
|
|
100
|
+
|
|
101
|
+
检查文件或目录是否存在。
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
if (exists('/path/to/file')) {
|
|
105
|
+
// ...
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### readJson(filePath)
|
|
110
|
+
|
|
111
|
+
读取 JSON 文件。
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
const config = readJson('/path/config.json');
|
|
115
|
+
if (config) {
|
|
116
|
+
console.log(config.name);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**返回值:** Object | null
|
|
121
|
+
|
|
122
|
+
#### writeJson(filePath, data, indent?)
|
|
123
|
+
|
|
124
|
+
写入 JSON 文件。
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
await writeJson('/path/config.json', { key: 'value' }, 2);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
| 参数 | 类型 | 默认值 | 描述 |
|
|
131
|
+
|------|------|--------|------|
|
|
132
|
+
| filePath | string | - | 文件路径 |
|
|
133
|
+
| data | Object | - | 要写入的数据 |
|
|
134
|
+
| indent | number | 4 | 缩进空格数 |
|
|
135
|
+
|
|
136
|
+
#### gitClone(url, destDir, options?)
|
|
137
|
+
|
|
138
|
+
Git 克隆仓库。
|
|
139
|
+
|
|
140
|
+
```javascript
|
|
141
|
+
const tempDir = createTempDir('repo');
|
|
142
|
+
try {
|
|
143
|
+
gitClone('https://github.com/user/repo.git', tempDir);
|
|
144
|
+
// 使用克隆的内容
|
|
145
|
+
} finally {
|
|
146
|
+
await cleanup(tempDir);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
| 参数 | 类型 | 描述 |
|
|
151
|
+
|------|------|------|
|
|
152
|
+
| url | string | 仓库 URL |
|
|
153
|
+
| destDir | string | 目标目录 |
|
|
154
|
+
| options.shallow | boolean | 是否浅克隆(默认 true) |
|
|
155
|
+
|
|
156
|
+
**返回值:** boolean
|
|
157
|
+
**异常:** 失败时抛出 Error
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## yaml-parser.js
|
|
162
|
+
|
|
163
|
+
### 快速开始
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
const { parseFile, parseYaml, extractBody } = require('./scripts/yaml-parser');
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### API 参考
|
|
170
|
+
|
|
171
|
+
#### parseFile(filePath)
|
|
172
|
+
|
|
173
|
+
从文件中解析 YAML Frontmatter。
|
|
174
|
+
|
|
175
|
+
```javascript
|
|
176
|
+
const metadata = parseFile('/path/to/SKILL.md');
|
|
177
|
+
if (metadata) {
|
|
178
|
+
console.log(metadata.name);
|
|
179
|
+
console.log(metadata.description);
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**返回值:** Object | null
|
|
184
|
+
|
|
185
|
+
#### parseYaml(yamlStr)
|
|
186
|
+
|
|
187
|
+
解析 YAML 字符串。
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
const yaml = `
|
|
191
|
+
name: my-skill
|
|
192
|
+
version: 1.0.0
|
|
193
|
+
tags:
|
|
194
|
+
- tag1
|
|
195
|
+
- tag2
|
|
196
|
+
`;
|
|
197
|
+
const obj = parseYaml(yaml);
|
|
198
|
+
// { name: 'my-skill', version: '1.0.0', tags: ['tag1', 'tag2'] }
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**支持的格式:**
|
|
202
|
+
- 简单键值对: `key: value`
|
|
203
|
+
- 带引号值: `key: "value"` 或 `key: 'value'`
|
|
204
|
+
- 数组(缩进列表)
|
|
205
|
+
- 布尔值: `true`, `false`
|
|
206
|
+
- 数字: `123`, `1.23`
|
|
207
|
+
|
|
208
|
+
**不支持:**
|
|
209
|
+
- 嵌套对象
|
|
210
|
+
- 多行字符串
|
|
211
|
+
- 复杂 YAML 语法
|
|
212
|
+
|
|
213
|
+
#### parseContent(content)
|
|
214
|
+
|
|
215
|
+
从字符串内容中解析 YAML Frontmatter。
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
const content = fs.readFileSync('file.md', 'utf8');
|
|
219
|
+
const metadata = parseContent(content);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### extractBody(content)
|
|
223
|
+
|
|
224
|
+
提取 Frontmatter 之后的正文内容。
|
|
225
|
+
|
|
226
|
+
```javascript
|
|
227
|
+
const content = fs.readFileSync('file.md', 'utf8');
|
|
228
|
+
const body = extractBody(content);
|
|
229
|
+
console.log(body); // Markdown 正文
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 常用模式
|
|
235
|
+
|
|
236
|
+
### 临时目录工作流
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
const { createTempDir, cleanup, gitClone, scanDir } = require('./scripts/file-utils');
|
|
240
|
+
|
|
241
|
+
const tempDir = createTempDir('task');
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
// 克隆仓库
|
|
245
|
+
gitClone('https://github.com/user/repo.git', tempDir);
|
|
246
|
+
|
|
247
|
+
// 扫描目录
|
|
248
|
+
const items = scanDir(path.join(tempDir, 'skills'));
|
|
249
|
+
|
|
250
|
+
// 处理内容...
|
|
251
|
+
|
|
252
|
+
} finally {
|
|
253
|
+
// 确保清理
|
|
254
|
+
await cleanup(tempDir);
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### 技能扫描模式
|
|
259
|
+
|
|
260
|
+
```javascript
|
|
261
|
+
const { scanDir } = require('./scripts/file-utils');
|
|
262
|
+
const { parseFile } = require('./scripts/yaml-parser');
|
|
263
|
+
const path = require('path');
|
|
264
|
+
|
|
265
|
+
const scanSkills = (skillsDir) => {
|
|
266
|
+
const dirs = scanDir(skillsDir);
|
|
267
|
+
|
|
268
|
+
return dirs.map(dir => {
|
|
269
|
+
const skillFile = path.join(dir.path, 'SKILL.md');
|
|
270
|
+
const metadata = parseFile(skillFile);
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
dirname: dir.name,
|
|
274
|
+
path: dir.path,
|
|
275
|
+
name: metadata?.name || dir.name,
|
|
276
|
+
description: metadata?.description || '无描述',
|
|
277
|
+
version: metadata?.version || '未知'
|
|
278
|
+
};
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
```
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# 交互式菜单 API
|
|
2
|
+
|
|
3
|
+
本文档描述 `scripts/menu.js` 模块的完整 API。该模块提供统一的交互式选择菜单功能。
|
|
4
|
+
|
|
5
|
+
## 快速开始
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
const { selectMultiple, selectSingle } = require('./scripts/menu');
|
|
9
|
+
|
|
10
|
+
// 多选菜单
|
|
11
|
+
const items = [
|
|
12
|
+
{ name: 'option-1', description: '第一个选项' },
|
|
13
|
+
{ name: 'option-2', description: '第二个选项' }
|
|
14
|
+
];
|
|
15
|
+
const result = await selectMultiple(items, '请选择');
|
|
16
|
+
|
|
17
|
+
// 单选菜单
|
|
18
|
+
const selected = await selectSingle(items, '选择一个');
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## API 参考
|
|
22
|
+
|
|
23
|
+
### selectMultiple(items, title, options)
|
|
24
|
+
|
|
25
|
+
创建多选菜单,支持空格选择、全选、清空等操作。
|
|
26
|
+
|
|
27
|
+
**参数:**
|
|
28
|
+
|
|
29
|
+
| 参数 | 类型 | 必需 | 描述 |
|
|
30
|
+
|------|------|------|------|
|
|
31
|
+
| items | Array\<Object\> | 是 | 菜单项列表 |
|
|
32
|
+
| items[].name | string | 是 | 项目名称 |
|
|
33
|
+
| items[].description | string | 否 | 项目描述 |
|
|
34
|
+
| title | string | 否 | 菜单标题,默认 '选择菜单' |
|
|
35
|
+
| options | Object | 否 | 配置选项 |
|
|
36
|
+
| options.showIndex | boolean | 否 | 是否显示序号,默认 false |
|
|
37
|
+
| options.allowEmpty | boolean | 否 | 是否允许空选择,默认 false |
|
|
38
|
+
|
|
39
|
+
**返回值:**
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
{
|
|
43
|
+
indices: number[], // 选中项的索引
|
|
44
|
+
items: Object[] // 选中的项
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**操作说明:**
|
|
49
|
+
|
|
50
|
+
| 按键 | 功能 |
|
|
51
|
+
|------|------|
|
|
52
|
+
| ↑/↓ | 上下移动光标 |
|
|
53
|
+
| 空格 | 选择/取消当前项 |
|
|
54
|
+
| a | 全选 |
|
|
55
|
+
| n | 清空选择 |
|
|
56
|
+
| 回车 | 确认选择 |
|
|
57
|
+
| q/ESC | 退出(返回空结果) |
|
|
58
|
+
|
|
59
|
+
**示例:**
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
const items = [
|
|
63
|
+
{ name: 'skill-1', description: '技能描述一' },
|
|
64
|
+
{ name: 'skill-2', description: '技能描述二' },
|
|
65
|
+
{ name: 'skill-3', description: '技能描述三' }
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const result = await selectMultiple(items, '选择要安装的技能', {
|
|
69
|
+
showIndex: true,
|
|
70
|
+
allowEmpty: false
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (result.items.length > 0) {
|
|
74
|
+
console.log('选中的技能:');
|
|
75
|
+
result.items.forEach(item => {
|
|
76
|
+
console.log(` - ${item.name}`);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### selectSingle(items, title)
|
|
82
|
+
|
|
83
|
+
创建单选菜单,只能选择一项。
|
|
84
|
+
|
|
85
|
+
**参数:**
|
|
86
|
+
|
|
87
|
+
| 参数 | 类型 | 必需 | 描述 |
|
|
88
|
+
|------|------|------|------|
|
|
89
|
+
| items | Array\<Object\> | 是 | 菜单项列表 |
|
|
90
|
+
| items[].name | string | 是 | 项目名称 |
|
|
91
|
+
| items[].description | string | 否 | 项目描述 |
|
|
92
|
+
| title | string | 否 | 菜单标题 |
|
|
93
|
+
|
|
94
|
+
**返回值:**
|
|
95
|
+
|
|
96
|
+
- 选中的项(Object)
|
|
97
|
+
- 取消时返回 `null`
|
|
98
|
+
|
|
99
|
+
**操作说明:**
|
|
100
|
+
|
|
101
|
+
| 按键 | 功能 |
|
|
102
|
+
|------|------|
|
|
103
|
+
| ↑/↓ | 上下移动光标 |
|
|
104
|
+
| 回车 | 确认选择 |
|
|
105
|
+
| q/ESC | 退出(返回 null) |
|
|
106
|
+
|
|
107
|
+
**示例:**
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
const repos = [
|
|
111
|
+
{ name: 'repo-1', description: '仓库一' },
|
|
112
|
+
{ name: 'repo-2', description: '仓库二' }
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const selected = await selectSingle(repos, '选择仓库');
|
|
116
|
+
if (selected) {
|
|
117
|
+
console.log(`已选择: ${selected.name}`);
|
|
118
|
+
} else {
|
|
119
|
+
console.log('已取消');
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### clearScreen()
|
|
124
|
+
|
|
125
|
+
清屏并将光标移动到顶部。
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
const { clearScreen } = require('./scripts/menu');
|
|
129
|
+
clearScreen();
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## 使用模式
|
|
133
|
+
|
|
134
|
+
### 菜单 + 确认模式
|
|
135
|
+
|
|
136
|
+
推荐将菜单选择和确认分离:
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
const Ec = require('../epic');
|
|
140
|
+
const { selectMultiple } = require('./scripts/menu');
|
|
141
|
+
|
|
142
|
+
// 1. 菜单选择(raw mode)
|
|
143
|
+
const result = await selectMultiple(items, '选择菜单');
|
|
144
|
+
|
|
145
|
+
if (result.items.length === 0) {
|
|
146
|
+
Ec.waiting('未选择任何项');
|
|
147
|
+
process.exit(0);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 2. 确认(普通输入模式)
|
|
151
|
+
const answer = await Ec.ask('确认操作?(y/N): ');
|
|
152
|
+
if (answer.toLowerCase() !== 'y') {
|
|
153
|
+
Ec.waiting('已取消');
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 3. 执行操作
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 注意事项
|
|
161
|
+
|
|
162
|
+
1. **不要在菜单操作中使用 Ec.ask**:菜单使用 raw mode,与 Ec.ask 的 readline 冲突
|
|
163
|
+
2. **菜单完成后才能使用 Ec.ask**:菜单函数返回后已退出 raw mode
|
|
164
|
+
3. **Ctrl+C 会直接退出进程**:这是预期行为,确保用户可以随时终止
|
|
165
|
+
|
|
166
|
+
## 自定义扩展
|
|
167
|
+
|
|
168
|
+
如需自定义菜单样式,可以基于 `selectMultiple` 的实现进行修改。关键点:
|
|
169
|
+
|
|
170
|
+
```javascript
|
|
171
|
+
// 1. 启用 raw mode
|
|
172
|
+
readline.emitKeypressEvents(process.stdin);
|
|
173
|
+
process.stdin.setRawMode(true);
|
|
174
|
+
|
|
175
|
+
// 2. 渲染菜单
|
|
176
|
+
const render = () => {
|
|
177
|
+
process.stdout.write('\x1B[2J\x1B[0f'); // 清屏
|
|
178
|
+
// ... 绘制菜单
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// 3. 监听按键
|
|
182
|
+
process.stdin.on('keypress', onKeypress);
|
|
183
|
+
|
|
184
|
+
// 4. 退出时恢复
|
|
185
|
+
process.stdin.setRawMode(false);
|
|
186
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
187
|
+
```
|