momo-ai 1.0.21 → 1.0.22
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/r2mo-rad-lain/SKILL.md +63 -374
- package/.trae/skills/algorithmic-art/LICENSE.txt +202 -0
- package/.trae/skills/algorithmic-art/SKILL.md +405 -0
- package/.trae/skills/algorithmic-art/templates/generator_template.js +223 -0
- package/.trae/skills/algorithmic-art/templates/viewer.html +599 -0
- package/.trae/skills/doc-coauthoring/SKILL.md +375 -0
- package/.trae/skills/frontend-design/LICENSE.txt +177 -0
- package/.trae/skills/frontend-design/SKILL.md +42 -0
- package/.trae/skills/r2mo-rad-lain/SKILL.md +101 -0
- package/README.md +9 -32
- package/docs/images/r2mo-lain.png +0 -0
- package/package.json +11 -11
- package/skills/r2mo-rad-domain/SKILL.md +70 -0
- package/src/_skill/repositories.json +9 -3
- package/src/_template/LAIN/.obsidian/app.json +1 -0
- package/src/_template/LAIN/.obsidian/appearance.json +10 -0
- package/src/_template/LAIN/.obsidian/community-plugins.json +7 -0
- package/src/_template/LAIN/.obsidian/core-plugins.json +33 -0
- package/src/_template/LAIN/.obsidian/plugins/dataview/main.js +20876 -0
- package/src/_template/LAIN/.obsidian/plugins/dataview/manifest.json +11 -0
- package/src/_template/LAIN/.obsidian/plugins/dataview/styles.css +141 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-excalidraw-plugin/data.json +815 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-excalidraw-plugin/main.js +10 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-excalidraw-plugin/manifest.json +12 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-excalidraw-plugin/styles.css +1 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-kanban/main.js +153 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-kanban/manifest.json +11 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-kanban/styles.css +1 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-plantuml/main.js +7732 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-plantuml/manifest.json +10 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-plantuml/styles.css +38 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-tasks-plugin/main.js +504 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-tasks-plugin/manifest.json +12 -0
- package/src/_template/LAIN/.obsidian/plugins/obsidian-tasks-plugin/styles.css +1 -0
- package/src/_template/LAIN/.obsidian/snippets/body-font.css +27 -0
- package/src/_template/LAIN/.obsidian/themes/Primary/manifest.json +9 -0
- package/src/_template/LAIN/.obsidian/themes/Primary/theme.css +3878 -0
- package/src/_template/LAIN/.obsidian/themes/Retro Windows/manifest.json +7 -0
- package/src/_template/LAIN/.obsidian/themes/Retro Windows/theme.css +582 -0
- package/src/_template/LAIN/.obsidian/themes/RetroOS 98/manifest.json +9 -0
- package/src/_template/LAIN/.obsidian/themes/RetroOS 98/theme.css +2566 -0
- package/src/_template/LAIN/.obsidian/types.json +28 -0
- package/src/_template/LAIN/.obsidian/workspace.json +184 -0
- package/src/_template/LAIN/AGENTS.md +170 -16
- package/src/_template/R2MO/domain-enhance.md +10 -0
- package/src/commander/app.json +13 -0
- package/src/commander/apply.json +13 -0
- package/src/commander/ask.json +6 -0
- package/src/commander/docs.json +13 -0
- package/src/commander/domain.json +19 -0
- package/src/commander/init.json +1 -1
- package/src/commander/mmr0.json +6 -0
- package/src/commander/mmr2.json +6 -0
- package/src/executor/executeApp.js +133 -0
- package/src/executor/{executeSkills.js → executeApply.js} +166 -302
- package/src/executor/executeAsk.js +274 -0
- package/src/executor/executeDocs.js +498 -0
- package/src/executor/executeDomain.js +293 -0
- package/src/executor/executeInit.js +159 -383
- package/src/executor/executeMcp.js +74 -1
- package/src/executor/executeMmr0.js +488 -0
- package/src/executor/executeMmr2.js +880 -0
- package/src/executor/index.js +15 -3
- package/src/python/r2mo_proto.py +418 -0
- package/src/python/r2mo_proto_database.py +369 -0
- package/src/python/r2mo_proto_domain.py +458 -0
- package/src/utils/momo-menu.js +43 -13
- package/.claude/skills/r2mo-rad-lain/PROMPT.md +0 -281
- package/.claude/skills/r2mo-rad-lain/README.md +0 -192
- package/.claude/skills/r2mo-rad-lain/examples/argument-parsing.js +0 -154
- package/.claude/skills/r2mo-rad-lain/examples/file-operations.js +0 -182
- package/.claude/skills/r2mo-rad-lain/file-utils-api.md +0 -281
- package/.claude/skills/r2mo-rad-lain/menu-api.md +0 -187
- package/.claude/skills/r2mo-rad-lain/scripts/file-utils.js +0 -223
- package/.claude/skills/r2mo-rad-lain/scripts/menu.js +0 -289
- package/.claude/skills/r2mo-rad-lain/scripts/yaml-parser.js +0 -209
- package/.claude/skills/r2mo-rad-lain/templates/command.json.template +0 -13
- package/.claude/skills/r2mo-rad-lain/templates/executor.js.template +0 -32
- package/.claude/skills/r2mo-rad-lain/templates/interactive-menu.js.template +0 -221
- package/src/_template/LAIN/.momo/advanced/actor.md +0 -42
- package/src/_template/LAIN/.momo/advanced/refer.json +0 -46
- package/src/_template/LAIN/.momo/scripts/submodule-clean.sh +0 -56
- package/src/_template/LAIN/changes/proposal.md +0 -39
- package/src/_template/LAIN/changes/tasks/task-detail.md +0 -45
- package/src/_template/LAIN/changes/tasks.md +0 -49
- package/src/_template/LAIN/execute/admin-n-f-dashboard.md +0 -53
- package/src/_template/LAIN/execute/admin-n-f-form.md +0 -51
- package/src/_template/LAIN/execute/admin-n-f-home.md +0 -49
- package/src/_template/LAIN/execute/admin-n-f-list.md +0 -52
- package/src/_template/LAIN/execute/admin-n-f-login.md +0 -56
- package/src/_template/LAIN/specification/project-model.md +0 -13
- package/src/_template/LAIN/specification/project.md +0 -73
- package/src/_template/LAIN/specification/requirement.md +0 -25
- package/src/commander/skills.json +0 -20
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 文件操作工具模块
|
|
3
|
-
*
|
|
4
|
-
* 提供常用的文件和目录操作功能。
|
|
5
|
-
*
|
|
6
|
-
* @module file-utils
|
|
7
|
-
* @example
|
|
8
|
-
* const { copyDir, scanDir, createTempDir, cleanup } = require('./scripts/file-utils');
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const path = require('path');
|
|
13
|
-
const os = require('os');
|
|
14
|
-
const fsAsync = require('fs').promises;
|
|
15
|
-
const { execSync } = require('child_process');
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* 递归拷贝目录
|
|
19
|
-
*
|
|
20
|
-
* @param {string} src - 源目录路径
|
|
21
|
-
* @param {string} dest - 目标目录路径
|
|
22
|
-
* @returns {Promise<void>}
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* await copyDir('/source/dir', '/dest/dir');
|
|
26
|
-
*/
|
|
27
|
-
const copyDir = async (src, dest) => {
|
|
28
|
-
await fsAsync.mkdir(dest, { recursive: true });
|
|
29
|
-
const items = await fsAsync.readdir(src);
|
|
30
|
-
|
|
31
|
-
for (const item of items) {
|
|
32
|
-
const srcPath = path.join(src, item);
|
|
33
|
-
const destPath = path.join(dest, item);
|
|
34
|
-
const stat = await fsAsync.stat(srcPath);
|
|
35
|
-
|
|
36
|
-
if (stat.isDirectory()) {
|
|
37
|
-
await copyDir(srcPath, destPath);
|
|
38
|
-
} else {
|
|
39
|
-
await fsAsync.copyFile(srcPath, destPath);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* 扫描目录,返回子目录列表
|
|
46
|
-
*
|
|
47
|
-
* @param {string} dirPath - 目录路径
|
|
48
|
-
* @param {Function} [filter] - 过滤函数 (name, fullPath) => boolean
|
|
49
|
-
* @returns {Array<Object>} [{ name, path, isDirectory }]
|
|
50
|
-
*
|
|
51
|
-
* @example
|
|
52
|
-
* // 扫描所有子目录
|
|
53
|
-
* const dirs = scanDir('/some/path');
|
|
54
|
-
*
|
|
55
|
-
* // 带过滤条件
|
|
56
|
-
* const filtered = scanDir('/path', (name) => !name.startsWith('.'));
|
|
57
|
-
*/
|
|
58
|
-
const scanDir = (dirPath, filter = () => true) => {
|
|
59
|
-
const results = [];
|
|
60
|
-
|
|
61
|
-
if (!fs.existsSync(dirPath)) {
|
|
62
|
-
return results;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const items = fs.readdirSync(dirPath);
|
|
66
|
-
|
|
67
|
-
for (const item of items) {
|
|
68
|
-
const itemPath = path.join(dirPath, item);
|
|
69
|
-
|
|
70
|
-
try {
|
|
71
|
-
const stat = fs.statSync(itemPath);
|
|
72
|
-
|
|
73
|
-
if (stat.isDirectory() && filter(item, itemPath)) {
|
|
74
|
-
results.push({
|
|
75
|
-
name: item,
|
|
76
|
-
path: itemPath,
|
|
77
|
-
isDirectory: true
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
} catch (e) {
|
|
81
|
-
// 忽略无法访问的项
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return results;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 创建临时目录
|
|
90
|
-
*
|
|
91
|
-
* @param {string} [prefix='momo'] - 目录前缀
|
|
92
|
-
* @returns {string} 临时目录路径
|
|
93
|
-
*
|
|
94
|
-
* @example
|
|
95
|
-
* const tempDir = createTempDir('my-task');
|
|
96
|
-
* // 返回: /tmp/.my-task-1234567890
|
|
97
|
-
*/
|
|
98
|
-
const createTempDir = (prefix = 'momo') => {
|
|
99
|
-
return path.join(os.tmpdir(), `.${prefix}-${Date.now()}`);
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* 清理目录(删除)
|
|
104
|
-
*
|
|
105
|
-
* @param {string} dirPath - 目录路径
|
|
106
|
-
* @returns {Promise<boolean>} 是否成功
|
|
107
|
-
*
|
|
108
|
-
* @example
|
|
109
|
-
* await cleanup('/tmp/.momo-123');
|
|
110
|
-
*/
|
|
111
|
-
const cleanup = async (dirPath) => {
|
|
112
|
-
try {
|
|
113
|
-
if (dirPath && fs.existsSync(dirPath)) {
|
|
114
|
-
await fsAsync.rm(dirPath, { recursive: true, force: true });
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
|
-
return false;
|
|
118
|
-
} catch (error) {
|
|
119
|
-
console.warn(`清理目录失败: ${error.message}`);
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* 确保目录存在
|
|
126
|
-
*
|
|
127
|
-
* @param {string} dirPath - 目录路径
|
|
128
|
-
* @returns {Promise<void>}
|
|
129
|
-
*
|
|
130
|
-
* @example
|
|
131
|
-
* await ensureDir('/path/to/dir');
|
|
132
|
-
*/
|
|
133
|
-
const ensureDir = async (dirPath) => {
|
|
134
|
-
await fsAsync.mkdir(dirPath, { recursive: true });
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* 检查文件或目录是否存在
|
|
139
|
-
*
|
|
140
|
-
* @param {string} filePath - 文件路径
|
|
141
|
-
* @returns {boolean}
|
|
142
|
-
*/
|
|
143
|
-
const exists = (filePath) => {
|
|
144
|
-
return fs.existsSync(filePath);
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* 读取 JSON 文件
|
|
149
|
-
*
|
|
150
|
-
* @param {string} filePath - JSON 文件路径
|
|
151
|
-
* @returns {Object|null} 解析的对象,失败返回 null
|
|
152
|
-
*
|
|
153
|
-
* @example
|
|
154
|
-
* const config = readJson('/path/config.json');
|
|
155
|
-
*/
|
|
156
|
-
const readJson = (filePath) => {
|
|
157
|
-
try {
|
|
158
|
-
if (!fs.existsSync(filePath)) {
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
162
|
-
return JSON.parse(content);
|
|
163
|
-
} catch (error) {
|
|
164
|
-
return null;
|
|
165
|
-
}
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* 写入 JSON 文件
|
|
170
|
-
*
|
|
171
|
-
* @param {string} filePath - 文件路径
|
|
172
|
-
* @param {Object} data - 要写入的数据
|
|
173
|
-
* @param {number} [indent=4] - 缩进空格数
|
|
174
|
-
* @returns {Promise<void>}
|
|
175
|
-
*
|
|
176
|
-
* @example
|
|
177
|
-
* await writeJson('/path/config.json', { key: 'value' });
|
|
178
|
-
*/
|
|
179
|
-
const writeJson = async (filePath, data, indent = 4) => {
|
|
180
|
-
const dir = path.dirname(filePath);
|
|
181
|
-
await ensureDir(dir);
|
|
182
|
-
await fsAsync.writeFile(filePath, JSON.stringify(data, null, indent));
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Git 克隆仓库
|
|
187
|
-
*
|
|
188
|
-
* @param {string} url - 仓库 URL
|
|
189
|
-
* @param {string} destDir - 目标目录
|
|
190
|
-
* @param {Object} [options={}] - 选项
|
|
191
|
-
* @param {boolean} [options.shallow=true] - 是否浅克隆
|
|
192
|
-
* @returns {boolean} 是否成功
|
|
193
|
-
* @throws {Error} 克隆失败时抛出错误
|
|
194
|
-
*
|
|
195
|
-
* @example
|
|
196
|
-
* const tempDir = createTempDir('repo');
|
|
197
|
-
* gitClone('https://github.com/user/repo.git', tempDir);
|
|
198
|
-
*/
|
|
199
|
-
const gitClone = (url, destDir, options = {}) => {
|
|
200
|
-
const { shallow = true } = options;
|
|
201
|
-
const depthArg = shallow ? '--depth 1' : '';
|
|
202
|
-
|
|
203
|
-
try {
|
|
204
|
-
execSync(`git clone ${depthArg} "${url}" "${destDir}"`, {
|
|
205
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
206
|
-
});
|
|
207
|
-
return true;
|
|
208
|
-
} catch (error) {
|
|
209
|
-
throw new Error(`克隆仓库失败: ${error.message}`);
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
module.exports = {
|
|
214
|
-
copyDir,
|
|
215
|
-
scanDir,
|
|
216
|
-
createTempDir,
|
|
217
|
-
cleanup,
|
|
218
|
-
ensureDir,
|
|
219
|
-
exists,
|
|
220
|
-
readJson,
|
|
221
|
-
writeJson,
|
|
222
|
-
gitClone
|
|
223
|
-
};
|
|
@@ -1,289 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 交互式菜单模块
|
|
3
|
-
*
|
|
4
|
-
* 提供统一的交互式选择菜单功能,支持单选和多选模式。
|
|
5
|
-
*
|
|
6
|
-
* @module menu
|
|
7
|
-
* @example
|
|
8
|
-
* const { selectMultiple, selectSingle } = require('./scripts/menu');
|
|
9
|
-
* const result = await selectMultiple(items, '选择菜单');
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const readline = require('readline');
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* 清屏并移动光标到顶部
|
|
16
|
-
*/
|
|
17
|
-
const clearScreen = () => {
|
|
18
|
-
process.stdout.write('\x1B[2J\x1B[0f');
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 创建多选菜单
|
|
23
|
-
*
|
|
24
|
-
* @param {Array<Object>} items - 菜单项列表
|
|
25
|
-
* @param {string} items[].name - 项目名称(必需)
|
|
26
|
-
* @param {string} [items[].description] - 项目描述
|
|
27
|
-
* @param {string} [title='选择菜单'] - 菜单标题
|
|
28
|
-
* @param {Object} [options={}] - 配置选项
|
|
29
|
-
* @param {boolean} [options.showIndex=false] - 是否显示序号
|
|
30
|
-
* @param {boolean} [options.allowEmpty=false] - 是否允许空选择
|
|
31
|
-
* @returns {Promise<Object>} { indices: number[], items: Object[] }
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* const items = [
|
|
35
|
-
* { name: 'option-1', description: '第一个选项' },
|
|
36
|
-
* { name: 'option-2', description: '第二个选项' }
|
|
37
|
-
* ];
|
|
38
|
-
* const result = await selectMultiple(items, '请选择');
|
|
39
|
-
* console.log(result.items); // 选中的项
|
|
40
|
-
*/
|
|
41
|
-
const selectMultiple = (items, title = '选择菜单', options = {}) => {
|
|
42
|
-
const { showIndex = false, allowEmpty = false } = options;
|
|
43
|
-
|
|
44
|
-
return new Promise((resolve) => {
|
|
45
|
-
const selected = new Array(items.length).fill(false);
|
|
46
|
-
let cursor = 0;
|
|
47
|
-
|
|
48
|
-
const maxNameLen = Math.max(...items.map(i => (i.name || '').length), 4);
|
|
49
|
-
|
|
50
|
-
const render = () => {
|
|
51
|
-
clearScreen();
|
|
52
|
-
|
|
53
|
-
console.log('');
|
|
54
|
-
console.log(`[Momo AI]`.blue.bold + ` ====== ${title} ======`.blue);
|
|
55
|
-
console.log('');
|
|
56
|
-
|
|
57
|
-
items.forEach((item, index) => {
|
|
58
|
-
const isActive = index === cursor;
|
|
59
|
-
const checkbox = selected[index] ? '[✓]'.green : '[ ]';
|
|
60
|
-
const pointer = isActive ? '▸'.cyan : ' ';
|
|
61
|
-
const indexStr = showIndex ? `${index + 1}. `.gray : '';
|
|
62
|
-
const name = (item.name || '').padEnd(maxNameLen);
|
|
63
|
-
|
|
64
|
-
if (isActive) {
|
|
65
|
-
console.log(` ${pointer} ${checkbox} ${indexStr}${name.cyan.bold}`);
|
|
66
|
-
} else {
|
|
67
|
-
console.log(` ${pointer} ${checkbox} ${indexStr}${name.cyan}`);
|
|
68
|
-
}
|
|
69
|
-
if (item.description) {
|
|
70
|
-
console.log(` ${item.description.gray}`);
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
console.log('');
|
|
75
|
-
console.log(' ─────────────────────────────────────────────────'.gray);
|
|
76
|
-
console.log(' ↑/↓ 移动 │ 空格 选择/取消 │ a 全选 │ n 清空 │ 回车 确认 │ q 退出'.gray);
|
|
77
|
-
console.log('');
|
|
78
|
-
|
|
79
|
-
const selectedCount = selected.filter(s => s).length;
|
|
80
|
-
if (selectedCount > 0) {
|
|
81
|
-
console.log(` 已选择 ${selectedCount} 项`.green);
|
|
82
|
-
} else {
|
|
83
|
-
console.log(' 未选择任何项'.yellow);
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const getResult = () => {
|
|
88
|
-
const indices = [];
|
|
89
|
-
const selectedItems = [];
|
|
90
|
-
selected.forEach((sel, idx) => {
|
|
91
|
-
if (sel) {
|
|
92
|
-
indices.push(idx);
|
|
93
|
-
selectedItems.push(items[idx]);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
return { indices, items: selectedItems };
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
readline.emitKeypressEvents(process.stdin);
|
|
100
|
-
if (process.stdin.isTTY) {
|
|
101
|
-
process.stdin.setRawMode(true);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
render();
|
|
105
|
-
|
|
106
|
-
const onKeypress = (str, key) => {
|
|
107
|
-
if (!key) return;
|
|
108
|
-
|
|
109
|
-
if (key.ctrl && key.name === 'c') {
|
|
110
|
-
process.stdin.setRawMode(false);
|
|
111
|
-
process.stdin.removeListener('keypress', onKeypress);
|
|
112
|
-
process.exit(0);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (key.name === 'q' || key.name === 'escape') {
|
|
116
|
-
process.stdin.setRawMode(false);
|
|
117
|
-
process.stdin.removeListener('keypress', onKeypress);
|
|
118
|
-
clearScreen();
|
|
119
|
-
resolve({ indices: [], items: [] });
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (key.name === 'up') {
|
|
124
|
-
cursor = cursor > 0 ? cursor - 1 : items.length - 1;
|
|
125
|
-
render();
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (key.name === 'down') {
|
|
130
|
-
cursor = cursor < items.length - 1 ? cursor + 1 : 0;
|
|
131
|
-
render();
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (key.name === 'space') {
|
|
136
|
-
selected[cursor] = !selected[cursor];
|
|
137
|
-
render();
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (key.name === 'a') {
|
|
142
|
-
selected.fill(true);
|
|
143
|
-
render();
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (key.name === 'n') {
|
|
148
|
-
selected.fill(false);
|
|
149
|
-
render();
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (key.name === 'return') {
|
|
154
|
-
const result = getResult();
|
|
155
|
-
|
|
156
|
-
if (!allowEmpty && result.indices.length === 0) {
|
|
157
|
-
render();
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
process.stdin.setRawMode(false);
|
|
162
|
-
process.stdin.removeListener('keypress', onKeypress);
|
|
163
|
-
clearScreen();
|
|
164
|
-
|
|
165
|
-
if (result.items.length > 0) {
|
|
166
|
-
console.log('');
|
|
167
|
-
console.log('[Momo AI]'.blue.bold + ' 已选择:');
|
|
168
|
-
console.log('');
|
|
169
|
-
result.items.forEach(item => {
|
|
170
|
-
console.log(` ✓ ${item.name}`.green);
|
|
171
|
-
});
|
|
172
|
-
console.log('');
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
resolve(result);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
process.stdin.on('keypress', onKeypress);
|
|
181
|
-
});
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* 创建单选菜单
|
|
186
|
-
*
|
|
187
|
-
* @param {Array<Object>} items - 菜单项列表
|
|
188
|
-
* @param {string} items[].name - 项目名称(必需)
|
|
189
|
-
* @param {string} [items[].description] - 项目描述
|
|
190
|
-
* @param {string} [title='选择菜单'] - 菜单标题
|
|
191
|
-
* @returns {Promise<Object|null>} 选中的项,取消返回 null
|
|
192
|
-
*
|
|
193
|
-
* @example
|
|
194
|
-
* const items = [
|
|
195
|
-
* { name: 'repo-1', description: '仓库一' },
|
|
196
|
-
* { name: 'repo-2', description: '仓库二' }
|
|
197
|
-
* ];
|
|
198
|
-
* const selected = await selectSingle(items, '选择仓库');
|
|
199
|
-
* if (selected) {
|
|
200
|
-
* console.log(selected.name);
|
|
201
|
-
* }
|
|
202
|
-
*/
|
|
203
|
-
const selectSingle = (items, title = '选择菜单') => {
|
|
204
|
-
return new Promise((resolve) => {
|
|
205
|
-
let cursor = 0;
|
|
206
|
-
|
|
207
|
-
const maxNameLen = Math.max(...items.map(i => (i.name || '').length), 4);
|
|
208
|
-
|
|
209
|
-
const render = () => {
|
|
210
|
-
clearScreen();
|
|
211
|
-
|
|
212
|
-
console.log('');
|
|
213
|
-
console.log('[Momo AI]'.blue.bold + ` ====== ${title} ======`.blue);
|
|
214
|
-
console.log('');
|
|
215
|
-
|
|
216
|
-
items.forEach((item, index) => {
|
|
217
|
-
const isActive = index === cursor;
|
|
218
|
-
const pointer = isActive ? '▸'.cyan : ' ';
|
|
219
|
-
const name = (item.name || '').padEnd(maxNameLen);
|
|
220
|
-
|
|
221
|
-
if (isActive) {
|
|
222
|
-
console.log(` ${pointer} ${name.cyan.bold}`);
|
|
223
|
-
} else {
|
|
224
|
-
console.log(` ${pointer} ${name.cyan}`);
|
|
225
|
-
}
|
|
226
|
-
if (item.description) {
|
|
227
|
-
console.log(` ${item.description.gray}`);
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
console.log('');
|
|
232
|
-
console.log(' ─────────────────────────────────────────────────'.gray);
|
|
233
|
-
console.log(' ↑/↓ 移动 │ 回车 确认 │ q 退出'.gray);
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
readline.emitKeypressEvents(process.stdin);
|
|
237
|
-
if (process.stdin.isTTY) {
|
|
238
|
-
process.stdin.setRawMode(true);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
render();
|
|
242
|
-
|
|
243
|
-
const onKeypress = (str, key) => {
|
|
244
|
-
if (!key) return;
|
|
245
|
-
|
|
246
|
-
if (key.ctrl && key.name === 'c') {
|
|
247
|
-
process.stdin.setRawMode(false);
|
|
248
|
-
process.stdin.removeListener('keypress', onKeypress);
|
|
249
|
-
process.exit(0);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (key.name === 'q' || key.name === 'escape') {
|
|
253
|
-
process.stdin.setRawMode(false);
|
|
254
|
-
process.stdin.removeListener('keypress', onKeypress);
|
|
255
|
-
clearScreen();
|
|
256
|
-
resolve(null);
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (key.name === 'up') {
|
|
261
|
-
cursor = cursor > 0 ? cursor - 1 : items.length - 1;
|
|
262
|
-
render();
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (key.name === 'down') {
|
|
267
|
-
cursor = cursor < items.length - 1 ? cursor + 1 : 0;
|
|
268
|
-
render();
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (key.name === 'return') {
|
|
273
|
-
process.stdin.setRawMode(false);
|
|
274
|
-
process.stdin.removeListener('keypress', onKeypress);
|
|
275
|
-
clearScreen();
|
|
276
|
-
resolve(items[cursor]);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
process.stdin.on('keypress', onKeypress);
|
|
282
|
-
});
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
module.exports = {
|
|
286
|
-
selectMultiple,
|
|
287
|
-
selectSingle,
|
|
288
|
-
clearScreen
|
|
289
|
-
};
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* YAML Frontmatter 解析器
|
|
3
|
-
*
|
|
4
|
-
* 解析 Markdown 文件头部的 YAML 元数据(不依赖外部库)。
|
|
5
|
-
*
|
|
6
|
-
* @module yaml-parser
|
|
7
|
-
* @example
|
|
8
|
-
* const { parseYaml, parseFile } = require('./scripts/yaml-parser');
|
|
9
|
-
* const metadata = parseFile('/path/to/SKILL.md');
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* 解析 YAML 字符串(简单实现)
|
|
16
|
-
*
|
|
17
|
-
* 支持的格式:
|
|
18
|
-
* - 简单键值对: `key: value`
|
|
19
|
-
* - 带引号值: `key: "value"` 或 `key: 'value'`
|
|
20
|
-
* - 数组(缩进列表):
|
|
21
|
-
* ```
|
|
22
|
-
* tags:
|
|
23
|
-
* - item1
|
|
24
|
-
* - item2
|
|
25
|
-
* ```
|
|
26
|
-
*
|
|
27
|
-
* @param {string} yamlStr - YAML 字符串
|
|
28
|
-
* @returns {Object} 解析后的对象
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* const yaml = `
|
|
32
|
-
* name: my-skill
|
|
33
|
-
* version: 1.0.0
|
|
34
|
-
* tags:
|
|
35
|
-
* - tag1
|
|
36
|
-
* - tag2
|
|
37
|
-
* `;
|
|
38
|
-
* const obj = parseYaml(yaml);
|
|
39
|
-
* // { name: 'my-skill', version: '1.0.0', tags: ['tag1', 'tag2'] }
|
|
40
|
-
*/
|
|
41
|
-
const parseYaml = (yamlStr) => {
|
|
42
|
-
const metadata = {};
|
|
43
|
-
const lines = yamlStr.split('\n');
|
|
44
|
-
let arrayKey = null;
|
|
45
|
-
|
|
46
|
-
for (const line of lines) {
|
|
47
|
-
// 跳过空行和注释
|
|
48
|
-
if (line.trim() === '' || line.trim().startsWith('#')) {
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// 检查是否是数组项(以 - 开头,前面有空格)
|
|
53
|
-
const arrayMatch = line.match(/^(\s+)-\s+(.+)$/);
|
|
54
|
-
if (arrayMatch && arrayKey) {
|
|
55
|
-
if (!Array.isArray(metadata[arrayKey])) {
|
|
56
|
-
metadata[arrayKey] = [];
|
|
57
|
-
}
|
|
58
|
-
let value = arrayMatch[2].trim();
|
|
59
|
-
// 移除引号
|
|
60
|
-
value = value.replace(/^["']|["']$/g, '');
|
|
61
|
-
metadata[arrayKey].push(value);
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// 检查是否是键值对
|
|
66
|
-
const kvMatch = line.match(/^(\w[\w-]*):\s*(.*)$/);
|
|
67
|
-
if (kvMatch) {
|
|
68
|
-
const key = kvMatch[1];
|
|
69
|
-
let value = kvMatch[2].trim();
|
|
70
|
-
|
|
71
|
-
if (value === '') {
|
|
72
|
-
// 空值,可能是数组或对象的开始
|
|
73
|
-
arrayKey = key;
|
|
74
|
-
metadata[key] = [];
|
|
75
|
-
} else {
|
|
76
|
-
// 移除引号
|
|
77
|
-
value = value.replace(/^["']|["']$/g, '');
|
|
78
|
-
|
|
79
|
-
// 尝试转换类型
|
|
80
|
-
if (value === 'true') {
|
|
81
|
-
metadata[key] = true;
|
|
82
|
-
} else if (value === 'false') {
|
|
83
|
-
metadata[key] = false;
|
|
84
|
-
} else if (/^\d+$/.test(value)) {
|
|
85
|
-
metadata[key] = parseInt(value, 10);
|
|
86
|
-
} else if (/^\d+\.\d+$/.test(value)) {
|
|
87
|
-
metadata[key] = parseFloat(value);
|
|
88
|
-
} else {
|
|
89
|
-
metadata[key] = value;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
arrayKey = null;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return metadata;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* 从文件中解析 YAML Frontmatter
|
|
102
|
-
*
|
|
103
|
-
* Frontmatter 是文件开头被 `---` 包围的 YAML 内容:
|
|
104
|
-
* ```
|
|
105
|
-
* ---
|
|
106
|
-
* name: my-skill
|
|
107
|
-
* version: 1.0.0
|
|
108
|
-
* ---
|
|
109
|
-
* # 正文内容
|
|
110
|
-
* ```
|
|
111
|
-
*
|
|
112
|
-
* @param {string} filePath - 文件路径
|
|
113
|
-
* @returns {Object|null} 解析的元数据,失败返回 null
|
|
114
|
-
*
|
|
115
|
-
* @example
|
|
116
|
-
* const metadata = parseFile('/path/to/SKILL.md');
|
|
117
|
-
* if (metadata) {
|
|
118
|
-
* console.log(metadata.name);
|
|
119
|
-
* console.log(metadata.description);
|
|
120
|
-
* }
|
|
121
|
-
*/
|
|
122
|
-
const parseFile = (filePath) => {
|
|
123
|
-
try {
|
|
124
|
-
if (!fs.existsSync(filePath)) {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
129
|
-
return parseContent(content);
|
|
130
|
-
} catch (error) {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* 从字符串内容中解析 YAML Frontmatter
|
|
137
|
-
*
|
|
138
|
-
* @param {string} content - 文件内容
|
|
139
|
-
* @returns {Object|null} 解析的元数据
|
|
140
|
-
*/
|
|
141
|
-
const parseContent = (content) => {
|
|
142
|
-
const lines = content.split('\n');
|
|
143
|
-
|
|
144
|
-
let inYaml = false;
|
|
145
|
-
let yamlLines = [];
|
|
146
|
-
|
|
147
|
-
for (const line of lines) {
|
|
148
|
-
if (line.trim() === '---') {
|
|
149
|
-
if (!inYaml) {
|
|
150
|
-
inYaml = true;
|
|
151
|
-
continue;
|
|
152
|
-
} else {
|
|
153
|
-
// 结束 YAML 块
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
if (inYaml) {
|
|
158
|
-
yamlLines.push(line);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (yamlLines.length === 0) {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return parseYaml(yamlLines.join('\n'));
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* 提取 Frontmatter 之后的正文内容
|
|
171
|
-
*
|
|
172
|
-
* @param {string} content - 完整文件内容
|
|
173
|
-
* @returns {string} 正文内容
|
|
174
|
-
*
|
|
175
|
-
* @example
|
|
176
|
-
* const content = fs.readFileSync('file.md', 'utf8');
|
|
177
|
-
* const body = extractBody(content);
|
|
178
|
-
*/
|
|
179
|
-
const extractBody = (content) => {
|
|
180
|
-
const lines = content.split('\n');
|
|
181
|
-
let inYaml = false;
|
|
182
|
-
let yamlEnded = false;
|
|
183
|
-
let bodyStart = 0;
|
|
184
|
-
|
|
185
|
-
for (let i = 0; i < lines.length; i++) {
|
|
186
|
-
if (lines[i].trim() === '---') {
|
|
187
|
-
if (!inYaml) {
|
|
188
|
-
inYaml = true;
|
|
189
|
-
} else {
|
|
190
|
-
yamlEnded = true;
|
|
191
|
-
bodyStart = i + 1;
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (!yamlEnded) {
|
|
198
|
-
return content;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return lines.slice(bodyStart).join('\n').trim();
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
module.exports = {
|
|
205
|
-
parseYaml,
|
|
206
|
-
parseFile,
|
|
207
|
-
parseContent,
|
|
208
|
-
extractBody
|
|
209
|
-
};
|