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
|
@@ -5,23 +5,48 @@ const Ec = require('../epic');
|
|
|
5
5
|
const fsAsync = require('fs').promises;
|
|
6
6
|
const readline = require('readline');
|
|
7
7
|
const { execSync } = require('child_process');
|
|
8
|
-
const { parseOptional
|
|
8
|
+
const { parseOptional } = require('../utils/momo-args');
|
|
9
9
|
const { copyDir, readJson, parseFile } = require('../utils/momo-file-utils');
|
|
10
|
+
const { selectSingle } = require('../utils/momo-menu');
|
|
10
11
|
|
|
11
|
-
// 全局技能仓库路径
|
|
12
|
-
const GLOBAL_SKILLS_DIR = path.join(os.homedir(), '.claude', 'skills');
|
|
13
12
|
// 远程仓库配置文件路径
|
|
14
13
|
const REPOSITORIES_CONFIG = path.join(__dirname, '../_skill/repositories.json');
|
|
15
14
|
// 本地缓存仓库目录
|
|
16
15
|
const LOCAL_REPO_CACHE_DIR = '.r2mo/repo';
|
|
17
16
|
|
|
18
|
-
// 来源标记
|
|
19
|
-
const SOURCE_GLOBAL = 'NORM';
|
|
20
|
-
const SOURCE_PROJECT = 'R2MO';
|
|
21
|
-
|
|
22
17
|
// 限定技仓库地址
|
|
23
18
|
const RESTRICTED_REPOSITORY = 'https://gitee.com/silentbalanceyh/r2mo-lain.git';
|
|
24
19
|
|
|
20
|
+
/**
|
|
21
|
+
* 获取目标路径配置(基于当前项目目录)
|
|
22
|
+
* @returns {Array} 目标路径配置列表
|
|
23
|
+
*/
|
|
24
|
+
const _getTargetPaths = () => {
|
|
25
|
+
const projectDir = process.cwd();
|
|
26
|
+
return [
|
|
27
|
+
{
|
|
28
|
+
name: 'Cursor 默认',
|
|
29
|
+
description: '.claude/skills/',
|
|
30
|
+
path: path.join(projectDir, '.claude', 'skills')
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'Antigravity',
|
|
34
|
+
description: '.agent/skills/',
|
|
35
|
+
path: path.join(projectDir, '.agent', 'skills')
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'Trae CN',
|
|
39
|
+
description: '.trae/skills/',
|
|
40
|
+
path: path.join(projectDir, '.trae', 'skills')
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'Trae',
|
|
44
|
+
description: '.trae/skills/',
|
|
45
|
+
path: path.join(projectDir, '.trae', 'skills')
|
|
46
|
+
}
|
|
47
|
+
];
|
|
48
|
+
};
|
|
49
|
+
|
|
25
50
|
/**
|
|
26
51
|
* 确保 .r2mo/repo 在 .gitignore 中
|
|
27
52
|
* @param {string} projectDir 项目目录
|
|
@@ -65,6 +90,17 @@ const _getLocalRepoPath = (projectDir, repoName) => {
|
|
|
65
90
|
return path.join(projectDir, LOCAL_REPO_CACHE_DIR, safeName);
|
|
66
91
|
};
|
|
67
92
|
|
|
93
|
+
/**
|
|
94
|
+
* 检查错误是否为服务器端错误
|
|
95
|
+
* @param {Error} error 错误对象
|
|
96
|
+
* @returns {boolean} 是否为服务器错误
|
|
97
|
+
*/
|
|
98
|
+
const _isServerError = (error) => {
|
|
99
|
+
const errorMsg = error.message || '';
|
|
100
|
+
// 检查常见的服务器错误代码
|
|
101
|
+
return /500|502|503|504|Internal Server Error/i.test(errorMsg);
|
|
102
|
+
};
|
|
103
|
+
|
|
68
104
|
/**
|
|
69
105
|
* 克隆或更新远程仓库到本地缓存
|
|
70
106
|
* @param {string} url 仓库 URL
|
|
@@ -89,6 +125,14 @@ const _cloneOrUpdateRepository = (url, localPath) => {
|
|
|
89
125
|
Ec.waiting('✓ 仓库已更新'.green);
|
|
90
126
|
return true;
|
|
91
127
|
} catch (error) {
|
|
128
|
+
// 检查是否为服务器错误
|
|
129
|
+
if (_isServerError(error)) {
|
|
130
|
+
Ec.warn('⚠️ 远程服务器暂时不可用(可能是 GitHub/Gitee 服务器问题)');
|
|
131
|
+
Ec.waiting('将使用本地缓存继续操作...'.yellow);
|
|
132
|
+
// 保留现有缓存,不删除
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
92
136
|
Ec.warn(`更新失败,将重新克隆: ${error.message}`);
|
|
93
137
|
// 删除损坏的缓存
|
|
94
138
|
fs.rmSync(localPath, { recursive: true, force: true });
|
|
@@ -106,42 +150,18 @@ const _cloneOrUpdateRepository = (url, localPath) => {
|
|
|
106
150
|
Ec.waiting('✓ 仓库克隆完成'.green);
|
|
107
151
|
return true;
|
|
108
152
|
} catch (error) {
|
|
153
|
+
// 检查是否为服务器错误
|
|
154
|
+
if (_isServerError(error)) {
|
|
155
|
+
Ec.error('❌ 远程服务器暂时不可用(可能是 GitHub/Gitee 服务器问题)');
|
|
156
|
+
Ec.waiting('请稍后重试,或检查网络连接');
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
109
160
|
Ec.error(`克隆仓库失败: ${error.message}`);
|
|
110
161
|
return false;
|
|
111
162
|
}
|
|
112
163
|
};
|
|
113
164
|
|
|
114
|
-
/**
|
|
115
|
-
* 截断描述文本
|
|
116
|
-
* @param {string} text 原始文本
|
|
117
|
-
* @param {number} maxLen 最大长度
|
|
118
|
-
* @returns {string} 截断后的文本
|
|
119
|
-
*/
|
|
120
|
-
const _truncateDescription = (text, maxLen = 50) => {
|
|
121
|
-
if (!text) return '无描述';
|
|
122
|
-
// 移除引号
|
|
123
|
-
text = text.replace(/^["']|["']$/g, '');
|
|
124
|
-
if (text.length <= maxLen) return text;
|
|
125
|
-
return text.substring(0, maxLen - 3) + '...';
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* 解析 -r 参数(支持无值的情况)
|
|
130
|
-
* @returns {Object} { hasRemote: boolean, remoteValue: string|null }
|
|
131
|
-
*/
|
|
132
|
-
const _parseRemoteArg = () => {
|
|
133
|
-
const result = parseOptional('remote', 'r');
|
|
134
|
-
return { hasRemote: result.hasFlag, remoteValue: result.value };
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* 解析 -g 参数
|
|
139
|
-
* @returns {boolean} 是否安装到全局目录
|
|
140
|
-
*/
|
|
141
|
-
const _parseGlobalArg = () => {
|
|
142
|
-
return parseBool('global', 'g');
|
|
143
|
-
};
|
|
144
|
-
|
|
145
165
|
/**
|
|
146
166
|
* 读取远程仓库配置
|
|
147
167
|
* @returns {Array} 仓库列表
|
|
@@ -169,10 +189,9 @@ const _parseSkillYaml = (filePath) => {
|
|
|
169
189
|
/**
|
|
170
190
|
* 扫描技能目录
|
|
171
191
|
* @param {string} skillsDir 技能目录路径
|
|
172
|
-
* @param {string} source 来源标记
|
|
173
192
|
* @returns {Array} 技能列表
|
|
174
193
|
*/
|
|
175
|
-
const _scanSkillsFromDir = (skillsDir
|
|
194
|
+
const _scanSkillsFromDir = (skillsDir) => {
|
|
176
195
|
const skills = [];
|
|
177
196
|
|
|
178
197
|
if (!fs.existsSync(skillsDir)) {
|
|
@@ -204,7 +223,6 @@ const _scanSkillsFromDir = (skillsDir, source = '') => {
|
|
|
204
223
|
version: metadata.version || '未知',
|
|
205
224
|
category: metadata.category || '未分类',
|
|
206
225
|
tags: metadata.tags || [],
|
|
207
|
-
source: source,
|
|
208
226
|
skillType: skillType,
|
|
209
227
|
repository: repository
|
|
210
228
|
});
|
|
@@ -217,7 +235,6 @@ const _scanSkillsFromDir = (skillsDir, source = '') => {
|
|
|
217
235
|
version: '未知',
|
|
218
236
|
category: '未分类',
|
|
219
237
|
tags: [],
|
|
220
|
-
source: source,
|
|
221
238
|
skillType: '通用技',
|
|
222
239
|
repository: ''
|
|
223
240
|
});
|
|
@@ -231,9 +248,8 @@ const _scanSkillsFromDir = (skillsDir, source = '') => {
|
|
|
231
248
|
return skills;
|
|
232
249
|
};
|
|
233
250
|
|
|
234
|
-
|
|
235
251
|
/**
|
|
236
|
-
*
|
|
252
|
+
* 递归拷贝目录
|
|
237
253
|
* @param {string} src 源目录
|
|
238
254
|
* @param {string} dest 目标目录
|
|
239
255
|
*/
|
|
@@ -242,107 +258,44 @@ const _copyDirectory = async (src, dest) => {
|
|
|
242
258
|
};
|
|
243
259
|
|
|
244
260
|
/**
|
|
245
|
-
*
|
|
246
|
-
* @param {
|
|
247
|
-
* @
|
|
261
|
+
* 截断描述文本
|
|
262
|
+
* @param {string} text 原始文本
|
|
263
|
+
* @param {number} maxLen 最大长度
|
|
264
|
+
* @returns {string} 截断后的文本
|
|
248
265
|
*/
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
console.log('[Momo AI]'.blue.bold + ' ====== 选择远程仓库 ======'.blue);
|
|
257
|
-
console.log('');
|
|
258
|
-
|
|
259
|
-
repositories.forEach((repo, index) => {
|
|
260
|
-
const isActive = index === cursor;
|
|
261
|
-
const pointer = isActive ? '▸'.cyan : ' ';
|
|
262
|
-
const name = repo.name;
|
|
263
|
-
|
|
264
|
-
if (isActive) {
|
|
265
|
-
console.log(` ${pointer} ${name.cyan.bold}`);
|
|
266
|
-
} else {
|
|
267
|
-
console.log(` ${pointer} ${name.cyan}`);
|
|
268
|
-
}
|
|
269
|
-
console.log(` ${repo.description.gray}`);
|
|
270
|
-
console.log(` ${repo.url.gray}`);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
console.log('');
|
|
274
|
-
console.log(' ─────────────────────────────────────────────────'.gray);
|
|
275
|
-
console.log(' ↑/↓ 移动 │ 回车 确认 │ q 退出'.gray);
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
readline.emitKeypressEvents(process.stdin);
|
|
279
|
-
if (process.stdin.isTTY) {
|
|
280
|
-
process.stdin.setRawMode(true);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
render();
|
|
284
|
-
|
|
285
|
-
const onKeypress = (str, key) => {
|
|
286
|
-
if (!key) return;
|
|
287
|
-
|
|
288
|
-
if (key.ctrl && key.name === 'c') {
|
|
289
|
-
process.stdin.setRawMode(false);
|
|
290
|
-
process.stdin.removeListener('keypress', onKeypress);
|
|
291
|
-
process.exit(0);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (key.name === 'q' || key.name === 'escape') {
|
|
295
|
-
process.stdin.setRawMode(false);
|
|
296
|
-
process.stdin.removeListener('keypress', onKeypress);
|
|
297
|
-
process.stdout.write('\x1B[2J\x1B[0f');
|
|
298
|
-
resolve(null);
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if (key.name === 'up') {
|
|
303
|
-
cursor = cursor > 0 ? cursor - 1 : repositories.length - 1;
|
|
304
|
-
render();
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (key.name === 'down') {
|
|
309
|
-
cursor = cursor < repositories.length - 1 ? cursor + 1 : 0;
|
|
310
|
-
render();
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
if (key.name === 'return') {
|
|
315
|
-
process.stdin.setRawMode(false);
|
|
316
|
-
process.stdin.removeListener('keypress', onKeypress);
|
|
317
|
-
process.stdout.write('\x1B[2J\x1B[0f');
|
|
318
|
-
resolve(repositories[cursor]);
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
};
|
|
266
|
+
const _truncateDescription = (text, maxLen = 50) => {
|
|
267
|
+
if (!text) return '无描述';
|
|
268
|
+
// 移除引号
|
|
269
|
+
text = text.replace(/^["']|["']$/g, '');
|
|
270
|
+
if (text.length <= maxLen) return text;
|
|
271
|
+
return text.substring(0, maxLen - 3) + '...';
|
|
272
|
+
};
|
|
322
273
|
|
|
323
|
-
|
|
324
|
-
|
|
274
|
+
/**
|
|
275
|
+
* 选择目标路径
|
|
276
|
+
* @returns {Promise<Object|null>} 选中的目标路径配置
|
|
277
|
+
*/
|
|
278
|
+
const _selectTargetPath = async () => {
|
|
279
|
+
const targetPaths = _getTargetPaths();
|
|
280
|
+
const selected = await selectSingle(targetPaths, '选择安装目标路径');
|
|
281
|
+
return selected;
|
|
325
282
|
};
|
|
326
283
|
|
|
327
284
|
/**
|
|
328
|
-
*
|
|
285
|
+
* 选择技能(多选菜单)
|
|
329
286
|
* @param {Array} skills 技能列表
|
|
330
287
|
* @param {string} title 菜单标题
|
|
331
|
-
* @
|
|
332
|
-
* @param {boolean} options.showSource 是否显示来源
|
|
333
|
-
* @param {boolean} options.showDescription 是否显示描述
|
|
334
|
-
* @param {boolean} options.showVersion 是否显示版本
|
|
335
|
-
* @returns {Promise<Object>} { indices: 选中的索引列表, names: 选中的名称列表 }
|
|
288
|
+
* @returns {Promise<Array>} 选中的技能索引列表
|
|
336
289
|
*/
|
|
337
|
-
const
|
|
338
|
-
|
|
290
|
+
const _selectSkills = async (skills, title) => {
|
|
291
|
+
// 对于远程技能(官方),不显示描述
|
|
292
|
+
const showDescription = false;
|
|
339
293
|
|
|
340
294
|
return new Promise((resolve) => {
|
|
341
295
|
const selected = new Array(skills.length).fill(false);
|
|
342
296
|
let cursor = 0;
|
|
343
297
|
|
|
344
298
|
const maxNameLen = Math.max(...skills.map(s => s.name.length), 4);
|
|
345
|
-
const maxSourceLen = showSource ? Math.max(...skills.map(s => (s.source || '').length), 4) : 0;
|
|
346
299
|
const maxTypeLen = Math.max(...skills.map(s => (s.skillType || '').length), 4);
|
|
347
300
|
|
|
348
301
|
const render = () => {
|
|
@@ -357,20 +310,7 @@ const _selectSkillsMenu = (skills, title = '技能安装菜单', options = {}) =
|
|
|
357
310
|
const checkbox = selected[index] ? '[✓]'.green : '[ ]';
|
|
358
311
|
const pointer = isActive ? '▸'.cyan : ' ';
|
|
359
312
|
const name = skill.name.padEnd(maxNameLen);
|
|
360
|
-
const version =
|
|
361
|
-
|
|
362
|
-
// 来源标记
|
|
363
|
-
let sourceTag = '';
|
|
364
|
-
if (showSource && skill.source) {
|
|
365
|
-
if (skill.source === SOURCE_GLOBAL) {
|
|
366
|
-
sourceTag = `[${skill.source}]`.gray;
|
|
367
|
-
} else if (skill.source === SOURCE_PROJECT) {
|
|
368
|
-
sourceTag = `[${skill.source}]`.magenta;
|
|
369
|
-
} else {
|
|
370
|
-
sourceTag = `[${skill.source}]`.gray;
|
|
371
|
-
}
|
|
372
|
-
sourceTag = sourceTag.padEnd(maxSourceLen + 10); // 补偿颜色码长度
|
|
373
|
-
}
|
|
313
|
+
const version = skill.version ? `v${skill.version}`.yellow : '';
|
|
374
314
|
|
|
375
315
|
// 技能类型标记(限定技/通用技)
|
|
376
316
|
const skillType = skill.skillType || '通用技';
|
|
@@ -378,16 +318,13 @@ const _selectSkillsMenu = (skills, title = '技能安装菜单', options = {}) =
|
|
|
378
318
|
? `[${skillType}]`.red
|
|
379
319
|
: `[${skillType}]`.green;
|
|
380
320
|
// 统一对齐:限定技和通用技都是3个字符,使用固定宽度确保对齐
|
|
381
|
-
// 实际显示宽度 = 方括号(2) + 文本(3) + 颜色码补偿(约15-20)
|
|
382
321
|
const typeTagPadded = typeTag.padEnd(25); // 固定宽度确保对齐
|
|
383
322
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
console.log(` ${pointer} ${checkbox} ${sourceTag}${typeTagPadded}${name.cyan} ${version}`);
|
|
388
|
-
}
|
|
323
|
+
// 统一技能名称样式,不加粗
|
|
324
|
+
const nameStyled = isActive ? name.cyan.bold : name.cyan;
|
|
325
|
+
console.log(` ${pointer} ${checkbox} ${typeTagPadded}${nameStyled} ${version}`);
|
|
389
326
|
|
|
390
|
-
//
|
|
327
|
+
// 显示描述(可选,远程技能不显示)
|
|
391
328
|
if (showDescription) {
|
|
392
329
|
const desc = _truncateDescription(skill.description, 50);
|
|
393
330
|
console.log(` ${desc.gray}`);
|
|
@@ -439,7 +376,7 @@ const _selectSkillsMenu = (skills, title = '技能安装菜单', options = {}) =
|
|
|
439
376
|
process.stdin.setRawMode(false);
|
|
440
377
|
process.stdin.removeListener('keypress', onKeypress);
|
|
441
378
|
process.stdout.write('\x1B[2J\x1B[0f');
|
|
442
|
-
resolve(
|
|
379
|
+
resolve([]);
|
|
443
380
|
return;
|
|
444
381
|
}
|
|
445
382
|
|
|
@@ -493,36 +430,26 @@ const _selectSkillsMenu = (skills, title = '技能安装菜单', options = {}) =
|
|
|
493
430
|
});
|
|
494
431
|
console.log('');
|
|
495
432
|
|
|
496
|
-
|
|
433
|
+
// 等待用户确认
|
|
434
|
+
resolve(result.indices);
|
|
497
435
|
return;
|
|
498
436
|
}
|
|
499
437
|
};
|
|
500
438
|
|
|
501
439
|
process.stdin.on('keypress', onKeypress);
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
* 交互式选择技能(包含确认步骤)
|
|
507
|
-
* @param {Array} skills 技能列表
|
|
508
|
-
* @param {string} title 菜单标题
|
|
509
|
-
* @param {Object} options 显示选项
|
|
510
|
-
* @returns {Promise<Array>} 选中的技能索引列表
|
|
511
|
-
*/
|
|
512
|
-
const _selectSkills = async (skills, title, options = {}) => {
|
|
513
|
-
const result = await _selectSkillsMenu(skills, title, options);
|
|
514
|
-
|
|
515
|
-
if (result.indices.length === 0) {
|
|
516
|
-
return [];
|
|
517
|
-
}
|
|
440
|
+
}).then(async (indices) => {
|
|
441
|
+
if (indices.length === 0) {
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
518
444
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
445
|
+
const answer = await Ec.ask('确认安装?(y/N): ');
|
|
446
|
+
if (answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes') {
|
|
447
|
+
return indices;
|
|
448
|
+
} else {
|
|
449
|
+
Ec.waiting('已取消安装');
|
|
450
|
+
return [];
|
|
451
|
+
}
|
|
452
|
+
});
|
|
526
453
|
};
|
|
527
454
|
|
|
528
455
|
/**
|
|
@@ -552,75 +479,44 @@ const _installSkills = async (skills, selectedIndices, targetDir) => {
|
|
|
552
479
|
};
|
|
553
480
|
|
|
554
481
|
/**
|
|
555
|
-
*
|
|
556
|
-
* @param {
|
|
482
|
+
* 选择远程仓库
|
|
483
|
+
* @param {Array} repositories 仓库列表
|
|
484
|
+
* @param {string} remoteValue -r 参数的值(如果提供)
|
|
485
|
+
* @returns {Promise<Object|null>} 选中的仓库
|
|
557
486
|
*/
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
Ec.
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
// 2. 扫描当前项目技能目录
|
|
572
|
-
const projectDir = process.cwd();
|
|
573
|
-
const projectSkillsDir = path.join(projectDir, 'skills');
|
|
574
|
-
|
|
575
|
-
Ec.waiting('正在扫描项目技能目录...');
|
|
576
|
-
if (fs.existsSync(projectSkillsDir)) {
|
|
577
|
-
const projectSkills = _scanSkillsFromDir(projectSkillsDir, SOURCE_PROJECT);
|
|
578
|
-
allSkills.push(...projectSkills);
|
|
579
|
-
Ec.waiting(` 找到 ${projectSkills.length} 个项目技能`);
|
|
580
|
-
} else {
|
|
581
|
-
Ec.waiting(` 项目目录不存在: ${projectSkillsDir}`.gray);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// 检查是否有可用技能
|
|
585
|
-
if (allSkills.length === 0) {
|
|
586
|
-
Ec.warn('未找到任何可用技能');
|
|
587
|
-
Ec.waiting(`请在 ${GLOBAL_SKILLS_DIR} 或 ${projectSkillsDir} 目录下添加技能`);
|
|
588
|
-
Ec.waiting('或使用 -r 参数从远程仓库安装');
|
|
589
|
-
process.exit(0);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
Ec.info(`共找到 ${allSkills.length} 个本地技能,正在打开选择菜单...`);
|
|
593
|
-
|
|
594
|
-
const selectedIndices = await _selectSkills(allSkills, '本地技能安装', {
|
|
595
|
-
showSource: true,
|
|
596
|
-
showDescription: true,
|
|
597
|
-
showVersion: true
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
if (selectedIndices.length === 0) {
|
|
601
|
-
Ec.waiting('未选择任何技能,退出');
|
|
602
|
-
process.exit(0);
|
|
487
|
+
const _selectRepository = async (repositories, remoteValue) => {
|
|
488
|
+
// 如果 -r 有值,直接查找对应的仓库
|
|
489
|
+
if (remoteValue) {
|
|
490
|
+
const repo = repositories.find(r =>
|
|
491
|
+
r.name === remoteValue ||
|
|
492
|
+
r.name.toLowerCase() === remoteValue.toLowerCase()
|
|
493
|
+
);
|
|
494
|
+
if (repo) {
|
|
495
|
+
return repo;
|
|
496
|
+
}
|
|
497
|
+
Ec.warn(`未找到仓库: ${remoteValue}`);
|
|
498
|
+
Ec.waiting('将显示仓库选择菜单...');
|
|
603
499
|
}
|
|
604
500
|
|
|
605
|
-
//
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
:
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
Ec.info(`将安装 ${selectedIndices.length} 个技能到 ${targetDir}`);
|
|
612
|
-
|
|
613
|
-
await _installSkills(allSkills, selectedIndices, targetDir);
|
|
501
|
+
// 显示仓库选择菜单
|
|
502
|
+
const menuItems = repositories.map(repo => ({
|
|
503
|
+
name: repo.name,
|
|
504
|
+
description: `${repo.description || ''} - ${repo.url}`,
|
|
505
|
+
repo: repo // 保存原始仓库对象的引用
|
|
506
|
+
}));
|
|
614
507
|
|
|
615
|
-
|
|
508
|
+
const selected = await selectSingle(menuItems, '选择远程仓库');
|
|
509
|
+
|
|
510
|
+
// 返回原始仓库对象
|
|
511
|
+
return selected ? selected.repo : null;
|
|
616
512
|
};
|
|
617
513
|
|
|
618
514
|
/**
|
|
619
515
|
* 从远程仓库安装技能
|
|
620
516
|
* @param {Object} repository 仓库配置
|
|
621
|
-
* @param {
|
|
517
|
+
* @param {string} targetPath 目标路径
|
|
622
518
|
*/
|
|
623
|
-
const _installFromRemote = async (repository,
|
|
519
|
+
const _installFromRemote = async (repository, targetPath) => {
|
|
624
520
|
const projectDir = process.cwd();
|
|
625
521
|
|
|
626
522
|
console.log('');
|
|
@@ -660,88 +556,56 @@ const _installFromRemote = async (repository, isGlobal = false) => {
|
|
|
660
556
|
|
|
661
557
|
Ec.info(`找到 ${skills.length} 个远程技能,正在打开选择菜单...`);
|
|
662
558
|
|
|
663
|
-
const selectedIndices = await _selectSkills(skills, `远程技能 (${repository.name})
|
|
664
|
-
showSource: false,
|
|
665
|
-
showDescription: false,
|
|
666
|
-
showVersion: false
|
|
667
|
-
});
|
|
559
|
+
const selectedIndices = await _selectSkills(skills, `远程技能 (${repository.name})`);
|
|
668
560
|
|
|
669
561
|
if (selectedIndices.length === 0) {
|
|
670
562
|
Ec.waiting('未选择任何技能,退出');
|
|
671
563
|
return;
|
|
672
564
|
}
|
|
673
565
|
|
|
674
|
-
// 根据 -g 参数决定安装位置
|
|
675
|
-
const targetDir = isGlobal
|
|
676
|
-
? GLOBAL_SKILLS_DIR
|
|
677
|
-
: path.join(projectDir, '.claude', 'skills');
|
|
678
|
-
|
|
679
566
|
console.log('');
|
|
680
|
-
Ec.info(`将安装 ${selectedIndices.length} 个技能到 ${
|
|
567
|
+
Ec.info(`将安装 ${selectedIndices.length} 个技能到 ${targetPath}`);
|
|
681
568
|
|
|
682
|
-
await _installSkills(skills, selectedIndices,
|
|
569
|
+
await _installSkills(skills, selectedIndices, targetPath);
|
|
683
570
|
|
|
684
571
|
Ec.info(`✅ 成功安装 ${selectedIndices.length} 个技能!`);
|
|
685
572
|
};
|
|
686
573
|
|
|
687
574
|
module.exports = async (options) => {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
Ec.info(`📦 安装目标: 全局目录 (${GLOBAL_SKILLS_DIR.cyan})`);
|
|
697
|
-
} else {
|
|
698
|
-
const projectDir = process.cwd();
|
|
699
|
-
const projectSkillsDir = path.join(projectDir, '.claude', 'skills');
|
|
700
|
-
console.log('');
|
|
701
|
-
Ec.info(`📦 安装目标: 项目目录 (${projectSkillsDir.cyan})`);
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
if (!hasRemote) {
|
|
705
|
-
// 不带 -r:从本地安装
|
|
706
|
-
await _installFromLocal(isGlobal);
|
|
707
|
-
process.exit(0);
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// 带 -r 参数:从远程安装
|
|
711
|
-
const repositories = _loadRepositories();
|
|
712
|
-
|
|
713
|
-
if (repositories.length === 0) {
|
|
714
|
-
Ec.error('❌ 未配置任何远程仓库');
|
|
715
|
-
Ec.waiting(`请在 ${REPOSITORIES_CONFIG} 中配置仓库`);
|
|
716
|
-
process.exit(1);
|
|
717
|
-
}
|
|
575
|
+
// 检查 -r 参数
|
|
576
|
+
const { hasFlag: hasRemote, value: remoteValue } = parseOptional('remote', 'r');
|
|
577
|
+
|
|
578
|
+
if (!hasRemote) {
|
|
579
|
+
Ec.error('❌ 此命令必须使用 -r 参数指定远程仓库');
|
|
580
|
+
Ec.waiting('用法: momo apply -r [仓库名]');
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
718
583
|
|
|
719
|
-
|
|
584
|
+
// 加载远程仓库配置
|
|
585
|
+
const repositories = _loadRepositories();
|
|
586
|
+
|
|
587
|
+
if (repositories.length === 0) {
|
|
588
|
+
Ec.error('❌ 未找到任何远程仓库配置');
|
|
589
|
+
Ec.waiting(`请检查配置文件: ${REPOSITORIES_CONFIG}`);
|
|
590
|
+
process.exit(1);
|
|
591
|
+
}
|
|
720
592
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
repositories.forEach(r => {
|
|
728
|
-
Ec.waiting(` - ${r.name}`);
|
|
729
|
-
});
|
|
730
|
-
process.exit(1);
|
|
731
|
-
}
|
|
732
|
-
} else {
|
|
733
|
-
// -r 无值:显示仓库选择菜单
|
|
734
|
-
selectedRepo = await _selectRepository(repositories);
|
|
735
|
-
if (!selectedRepo) {
|
|
736
|
-
Ec.waiting('未选择任何仓库,退出');
|
|
737
|
-
process.exit(0);
|
|
738
|
-
}
|
|
739
|
-
}
|
|
593
|
+
// 选择仓库
|
|
594
|
+
const repository = await _selectRepository(repositories, remoteValue);
|
|
595
|
+
if (!repository) {
|
|
596
|
+
Ec.waiting('已取消选择仓库');
|
|
597
|
+
process.exit(0);
|
|
598
|
+
}
|
|
740
599
|
|
|
741
|
-
|
|
600
|
+
// 选择目标路径
|
|
601
|
+
const targetPathConfig = await _selectTargetPath();
|
|
602
|
+
if (!targetPathConfig) {
|
|
603
|
+
Ec.waiting('已取消选择目标路径');
|
|
742
604
|
process.exit(0);
|
|
743
|
-
} catch (error) {
|
|
744
|
-
Ec.error(`❌ 执行过程中发生错误: ${error.message}`);
|
|
745
|
-
process.exit(1);
|
|
746
605
|
}
|
|
606
|
+
|
|
607
|
+
// 从远程仓库安装技能
|
|
608
|
+
await _installFromRemote(repository, targetPathConfig.path);
|
|
609
|
+
|
|
610
|
+
process.exit(0);
|
|
747
611
|
};
|