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.
Files changed (109) hide show
  1. package/.claude/skills/algorithmic-art/LICENSE.txt +202 -0
  2. package/.claude/skills/algorithmic-art/SKILL.md +405 -0
  3. package/.claude/skills/algorithmic-art/templates/generator_template.js +223 -0
  4. package/.claude/skills/algorithmic-art/templates/viewer.html +599 -0
  5. package/.claude/skills/r2mo-rad-lain/PROMPT.md +281 -0
  6. package/.claude/skills/r2mo-rad-lain/README.md +192 -0
  7. package/.claude/skills/r2mo-rad-lain/SKILL.md +412 -0
  8. package/.claude/skills/r2mo-rad-lain/examples/argument-parsing.js +154 -0
  9. package/.claude/skills/r2mo-rad-lain/examples/file-operations.js +182 -0
  10. package/.claude/skills/r2mo-rad-lain/file-utils-api.md +281 -0
  11. package/.claude/skills/r2mo-rad-lain/menu-api.md +187 -0
  12. package/.claude/skills/r2mo-rad-lain/scripts/file-utils.js +223 -0
  13. package/.claude/skills/r2mo-rad-lain/scripts/menu.js +289 -0
  14. package/.claude/skills/r2mo-rad-lain/scripts/yaml-parser.js +209 -0
  15. package/.claude/skills/r2mo-rad-lain/templates/command.json.template +13 -0
  16. package/.claude/skills/r2mo-rad-lain/templates/executor.js.template +32 -0
  17. package/.claude/skills/r2mo-rad-lain/templates/interactive-menu.js.template +221 -0
  18. package/.cursor/mcp.json +17 -0
  19. package/.obsidian/app.json +1 -0
  20. package/.obsidian/appearance.json +4 -0
  21. package/.obsidian/community-plugins.json +4 -0
  22. package/.obsidian/core-plugins.json +33 -0
  23. package/.obsidian/plugins/ai-agent/main.js +98495 -0
  24. package/.obsidian/plugins/ai-agent/manifest.json +11 -0
  25. package/.obsidian/plugins/ai-agent/styles.css +806 -0
  26. package/.obsidian/plugins/dataview/main.js +20876 -0
  27. package/.obsidian/plugins/dataview/manifest.json +11 -0
  28. package/.obsidian/plugins/dataview/styles.css +141 -0
  29. package/.obsidian/plugins/obsidian-excalidraw-plugin/main.js +10 -0
  30. package/.obsidian/plugins/obsidian-excalidraw-plugin/manifest.json +12 -0
  31. package/.obsidian/plugins/obsidian-excalidraw-plugin/styles.css +1 -0
  32. package/.obsidian/plugins/templater-obsidian/main.js +45 -0
  33. package/.obsidian/plugins/templater-obsidian/manifest.json +11 -0
  34. package/.obsidian/plugins/templater-obsidian/styles.css +226 -0
  35. package/.obsidian/plugins/terminal/main.js +200 -0
  36. package/.obsidian/plugins/terminal/manifest.json +14 -0
  37. package/.obsidian/plugins/terminal/styles.css +32 -0
  38. package/.obsidian/themes/AnuPpuccin/manifest.json +7 -0
  39. package/.obsidian/themes/AnuPpuccin/theme.css +9080 -0
  40. package/.obsidian/themes/Things/manifest.json +7 -0
  41. package/.obsidian/themes/Things/theme.css +1628 -0
  42. package/.obsidian/workspace.json +196 -0
  43. package/README.md +10 -123
  44. package/docs/images/logo.jpeg +0 -0
  45. package/install.sh +1 -0
  46. package/package.json +6 -2
  47. package/skills/r2mo-rad-lain/SKILL.md +101 -0
  48. package/src/_agent/trae/momo-datamodel.json +2 -1
  49. package/src/_agent/trae/momo-module-req.json +2 -1
  50. package/src/_agent/trae/momo-system-req.json +2 -1
  51. package/src/_agent/trae/momo-task.json +2 -1
  52. package/src/_agent/trae/momo-taskplan.json +2 -1
  53. package/src/_mcp/skills-server.mjs +70 -0
  54. package/src/_skill/repositories.json +16 -0
  55. package/src/_template/LAIN/.momo/advanced/refer.json +0 -4
  56. package/src/commander/help.json +5 -0
  57. package/src/commander/mcp.json +13 -0
  58. package/src/commander/open.json +8 -2
  59. package/src/commander/skills.json +20 -0
  60. package/src/executor/executeEnv.js +48 -38
  61. package/src/executor/executeHelp.js +77 -16
  62. package/src/executor/executeInit.js +203 -149
  63. package/src/executor/executeMcp.js +290 -0
  64. package/src/executor/executeOpen.js +144 -125
  65. package/src/executor/executeSkills.js +747 -0
  66. package/src/executor/index.js +5 -39
  67. package/src/momo.js +2 -1
  68. package/src/utils/momo-args.js +39 -0
  69. package/src/utils/momo-file-utils.js +75 -0
  70. package/src/utils/momo-menu.js +54 -0
  71. package/src/commander/actor.json +0 -12
  72. package/src/commander/actors.json +0 -6
  73. package/src/commander/add.json +0 -12
  74. package/src/commander/agent.json +0 -12
  75. package/src/commander/agentcfg.json +0 -5
  76. package/src/commander/archive.json +0 -12
  77. package/src/commander/commit.json +0 -12
  78. package/src/commander/console.json +0 -7
  79. package/src/commander/lain.json +0 -7
  80. package/src/commander/list.json +0 -7
  81. package/src/commander/plan.json +0 -12
  82. package/src/commander/project.json +0 -12
  83. package/src/commander/pull.json +0 -6
  84. package/src/commander/push.json +0 -6
  85. package/src/commander/repo.json +0 -18
  86. package/src/commander/run.json +0 -18
  87. package/src/commander/show.json +0 -12
  88. package/src/commander/tasks.json +0 -18
  89. package/src/commander/unlock.json +0 -6
  90. package/src/commander/validate.json +0 -12
  91. package/src/executor/executeActor.js +0 -133
  92. package/src/executor/executeActors.js +0 -58
  93. package/src/executor/executeAdd.js +0 -307
  94. package/src/executor/executeAgent.js +0 -224
  95. package/src/executor/executeAgentCfg.js +0 -195
  96. package/src/executor/executeArchive.js +0 -124
  97. package/src/executor/executeCommit.js +0 -202
  98. package/src/executor/executeConsole.js +0 -142
  99. package/src/executor/executeList.js +0 -133
  100. package/src/executor/executePlan.js +0 -164
  101. package/src/executor/executeProject.js +0 -312
  102. package/src/executor/executePull.js +0 -127
  103. package/src/executor/executePush.js +0 -243
  104. package/src/executor/executeRepo.js +0 -238
  105. package/src/executor/executeRun.js +0 -644
  106. package/src/executor/executeShow.js +0 -164
  107. package/src/executor/executeTasks.js +0 -384
  108. package/src/executor/executeUnlock.js +0 -110
  109. package/src/executor/executeValidate.js +0 -210
@@ -0,0 +1,747 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const Ec = require('../epic');
5
+ const fsAsync = require('fs').promises;
6
+ const readline = require('readline');
7
+ const { execSync } = require('child_process');
8
+ const { parseOptional, parseBool } = require('../utils/momo-args');
9
+ const { copyDir, readJson, parseFile } = require('../utils/momo-file-utils');
10
+
11
+ // 全局技能仓库路径
12
+ const GLOBAL_SKILLS_DIR = path.join(os.homedir(), '.claude', 'skills');
13
+ // 远程仓库配置文件路径
14
+ const REPOSITORIES_CONFIG = path.join(__dirname, '../_skill/repositories.json');
15
+ // 本地缓存仓库目录
16
+ const LOCAL_REPO_CACHE_DIR = '.r2mo/repo';
17
+
18
+ // 来源标记
19
+ const SOURCE_GLOBAL = 'NORM';
20
+ const SOURCE_PROJECT = 'R2MO';
21
+
22
+ // 限定技仓库地址
23
+ const RESTRICTED_REPOSITORY = 'https://gitee.com/silentbalanceyh/r2mo-lain.git';
24
+
25
+ /**
26
+ * 确保 .r2mo/repo 在 .gitignore 中
27
+ * @param {string} projectDir 项目目录
28
+ */
29
+ const _ensureGitIgnore = (projectDir) => {
30
+ const gitignorePath = path.join(projectDir, '.gitignore');
31
+ const ignoreEntry = '.r2mo/repo';
32
+
33
+ try {
34
+ let content = '';
35
+ if (fs.existsSync(gitignorePath)) {
36
+ content = fs.readFileSync(gitignorePath, 'utf8');
37
+ }
38
+
39
+ // 检查是否已经存在
40
+ const lines = content.split('\n');
41
+ const hasEntry = lines.some(line => line.trim() === ignoreEntry);
42
+
43
+ if (!hasEntry) {
44
+ // 添加到 .gitignore
45
+ const newContent = content.endsWith('\n') || content === ''
46
+ ? content + ignoreEntry + '\n'
47
+ : content + '\n' + ignoreEntry + '\n';
48
+ fs.writeFileSync(gitignorePath, newContent);
49
+ Ec.waiting(`已将 ${ignoreEntry} 添加到 .gitignore`);
50
+ }
51
+ } catch (error) {
52
+ Ec.warn(`更新 .gitignore 失败: ${error.message}`);
53
+ }
54
+ };
55
+
56
+ /**
57
+ * 获取仓库本地缓存路径
58
+ * @param {string} projectDir 项目目录
59
+ * @param {string} repoName 仓库名称
60
+ * @returns {string} 本地缓存路径
61
+ */
62
+ const _getLocalRepoPath = (projectDir, repoName) => {
63
+ // 将 / 替换为 - 避免路径问题
64
+ const safeName = repoName.replace(/\//g, '-');
65
+ return path.join(projectDir, LOCAL_REPO_CACHE_DIR, safeName);
66
+ };
67
+
68
+ /**
69
+ * 克隆或更新远程仓库到本地缓存
70
+ * @param {string} url 仓库 URL
71
+ * @param {string} localPath 本地缓存路径
72
+ * @returns {boolean} 是否成功
73
+ */
74
+ const _cloneOrUpdateRepository = (url, localPath) => {
75
+ // 确保父目录存在
76
+ const parentDir = path.dirname(localPath);
77
+ if (!fs.existsSync(parentDir)) {
78
+ fs.mkdirSync(parentDir, { recursive: true });
79
+ }
80
+
81
+ // 检查是否已有本地缓存
82
+ if (fs.existsSync(localPath)) {
83
+ Ec.waiting(`发现本地缓存: ${localPath}`.cyan);
84
+ Ec.waiting('正在拉取最新更新...');
85
+ try {
86
+ execSync(`cd "${localPath}" && git pull --quiet`, {
87
+ stdio: ['pipe', 'pipe', 'pipe']
88
+ });
89
+ Ec.waiting('✓ 仓库已更新'.green);
90
+ return true;
91
+ } catch (error) {
92
+ Ec.warn(`更新失败,将重新克隆: ${error.message}`);
93
+ // 删除损坏的缓存
94
+ fs.rmSync(localPath, { recursive: true, force: true });
95
+ }
96
+ }
97
+
98
+ // 克隆仓库
99
+ Ec.waiting(`正在克隆仓库: ${url}`.cyan);
100
+ Ec.waiting(`本地缓存: ${localPath}`);
101
+
102
+ try {
103
+ execSync(`git clone --depth 1 "${url}" "${localPath}"`, {
104
+ stdio: ['pipe', 'pipe', 'pipe']
105
+ });
106
+ Ec.waiting('✓ 仓库克隆完成'.green);
107
+ return true;
108
+ } catch (error) {
109
+ Ec.error(`克隆仓库失败: ${error.message}`);
110
+ return false;
111
+ }
112
+ };
113
+
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
+ /**
146
+ * 读取远程仓库配置
147
+ * @returns {Array} 仓库列表
148
+ */
149
+ const _loadRepositories = () => {
150
+ try {
151
+ const config = readJson(REPOSITORIES_CONFIG);
152
+ return config ? (config.repositories || []) : [];
153
+ } catch (error) {
154
+ Ec.warn(`读取仓库配置失败: ${error.message}`);
155
+ return [];
156
+ }
157
+ };
158
+
159
+ /**
160
+ * 解析 SKILL.md 文件的 YAML 头部
161
+ * @param {string} filePath SKILL.md 文件路径
162
+ * @returns {Object|null} 解析的元数据对象
163
+ */
164
+ const _parseSkillYaml = (filePath) => {
165
+ const parsed = parseFile(filePath);
166
+ return parsed ? parsed.attributes : null;
167
+ };
168
+
169
+ /**
170
+ * 扫描技能目录
171
+ * @param {string} skillsDir 技能目录路径
172
+ * @param {string} source 来源标记
173
+ * @returns {Array} 技能列表
174
+ */
175
+ const _scanSkillsFromDir = (skillsDir, source = '') => {
176
+ const skills = [];
177
+
178
+ if (!fs.existsSync(skillsDir)) {
179
+ return skills;
180
+ }
181
+
182
+ const items = fs.readdirSync(skillsDir);
183
+
184
+ for (const item of items) {
185
+ const skillDir = path.join(skillsDir, item);
186
+
187
+ try {
188
+ const stat = fs.statSync(skillDir);
189
+
190
+ if (stat.isDirectory()) {
191
+ const skillFile = path.join(skillDir, 'SKILL.md');
192
+ const metadata = _parseSkillYaml(skillFile);
193
+
194
+ if (metadata) {
195
+ // 判断技能类型:检查 repository 字段
196
+ const repository = metadata.repository || '';
197
+ const skillType = repository === RESTRICTED_REPOSITORY ? '限定技' : '通用技';
198
+
199
+ skills.push({
200
+ dirname: item,
201
+ path: skillDir,
202
+ name: metadata.name || item,
203
+ description: metadata.description || '无描述',
204
+ version: metadata.version || '未知',
205
+ category: metadata.category || '未分类',
206
+ tags: metadata.tags || [],
207
+ source: source,
208
+ skillType: skillType,
209
+ repository: repository
210
+ });
211
+ } else {
212
+ skills.push({
213
+ dirname: item,
214
+ path: skillDir,
215
+ name: item,
216
+ description: '(未找到 SKILL.md)',
217
+ version: '未知',
218
+ category: '未分类',
219
+ tags: [],
220
+ source: source,
221
+ skillType: '通用技',
222
+ repository: ''
223
+ });
224
+ }
225
+ }
226
+ } catch (e) {
227
+ // 忽略无法访问的目录
228
+ }
229
+ }
230
+
231
+ return skills;
232
+ };
233
+
234
+
235
+ /**
236
+ * 递归拷贝目录(使用工具函数)
237
+ * @param {string} src 源目录
238
+ * @param {string} dest 目标目录
239
+ */
240
+ const _copyDirectory = async (src, dest) => {
241
+ await copyDir(src, dest);
242
+ };
243
+
244
+ /**
245
+ * 仓库选择菜单
246
+ * @param {Array} repositories 仓库列表
247
+ * @returns {Promise<Object|null>} 选中的仓库
248
+ */
249
+ const _selectRepository = async (repositories) => {
250
+ return new Promise((resolve) => {
251
+ let cursor = 0;
252
+
253
+ const render = () => {
254
+ process.stdout.write('\x1B[2J\x1B[0f');
255
+ console.log('');
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
+ };
322
+
323
+ process.stdin.on('keypress', onKeypress);
324
+ });
325
+ };
326
+
327
+ /**
328
+ * 交互式多选菜单(仅选择阶段)
329
+ * @param {Array} skills 技能列表
330
+ * @param {string} title 菜单标题
331
+ * @param {Object} options 显示选项
332
+ * @param {boolean} options.showSource 是否显示来源
333
+ * @param {boolean} options.showDescription 是否显示描述
334
+ * @param {boolean} options.showVersion 是否显示版本
335
+ * @returns {Promise<Object>} { indices: 选中的索引列表, names: 选中的名称列表 }
336
+ */
337
+ const _selectSkillsMenu = (skills, title = '技能安装菜单', options = {}) => {
338
+ const { showSource = false, showDescription = true, showVersion = true } = options;
339
+
340
+ return new Promise((resolve) => {
341
+ const selected = new Array(skills.length).fill(false);
342
+ let cursor = 0;
343
+
344
+ 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
+ const maxTypeLen = Math.max(...skills.map(s => (s.skillType || '').length), 4);
347
+
348
+ const render = () => {
349
+ process.stdout.write('\x1B[2J\x1B[0f');
350
+
351
+ console.log('');
352
+ console.log('[Momo AI]'.blue.bold + ` ====== ${title} ======`.blue);
353
+ console.log('');
354
+
355
+ skills.forEach((skill, index) => {
356
+ const isActive = index === cursor;
357
+ const checkbox = selected[index] ? '[✓]'.green : '[ ]';
358
+ const pointer = isActive ? '▸'.cyan : ' ';
359
+ const name = skill.name.padEnd(maxNameLen);
360
+ const version = (showVersion && skill.version) ? `v${skill.version}`.yellow : '';
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
+ }
374
+
375
+ // 技能类型标记(限定技/通用技)
376
+ const skillType = skill.skillType || '通用技';
377
+ const typeTag = skillType === '限定技'
378
+ ? `[${skillType}]`.red
379
+ : `[${skillType}]`.green;
380
+ // 统一对齐:限定技和通用技都是3个字符,使用固定宽度确保对齐
381
+ // 实际显示宽度 = 方括号(2) + 文本(3) + 颜色码补偿(约15-20)
382
+ const typeTagPadded = typeTag.padEnd(25); // 固定宽度确保对齐
383
+
384
+ if (isActive) {
385
+ console.log(` ${pointer} ${checkbox} ${sourceTag}${typeTagPadded}${name.cyan.bold} ${version}`);
386
+ } else {
387
+ console.log(` ${pointer} ${checkbox} ${sourceTag}${typeTagPadded}${name.cyan} ${version}`);
388
+ }
389
+
390
+ // 显示描述(可选)
391
+ if (showDescription) {
392
+ const desc = _truncateDescription(skill.description, 50);
393
+ console.log(` ${desc.gray}`);
394
+ }
395
+ });
396
+
397
+ console.log('');
398
+ console.log(' ─────────────────────────────────────────────────'.gray);
399
+ console.log(' ↑/↓ 移动 │ 空格 选择/取消 │ a 全选 │ n 清空 │ 回车 确认 │ q 退出'.gray);
400
+ console.log('');
401
+
402
+ const selectedCount = selected.filter(s => s).length;
403
+ if (selectedCount > 0) {
404
+ console.log(` 已选择 ${selectedCount} 个技能`.green);
405
+ } else {
406
+ console.log(' 未选择任何技能'.yellow);
407
+ }
408
+ };
409
+
410
+ const getSelectedResult = () => {
411
+ const indices = [];
412
+ const names = [];
413
+ selected.forEach((sel, idx) => {
414
+ if (sel) {
415
+ indices.push(idx);
416
+ names.push(skills[idx].name);
417
+ }
418
+ });
419
+ return { indices, names };
420
+ };
421
+
422
+ readline.emitKeypressEvents(process.stdin);
423
+ if (process.stdin.isTTY) {
424
+ process.stdin.setRawMode(true);
425
+ }
426
+
427
+ render();
428
+
429
+ const onKeypress = (str, key) => {
430
+ if (!key) return;
431
+
432
+ if (key.ctrl && key.name === 'c') {
433
+ process.stdin.setRawMode(false);
434
+ process.stdin.removeListener('keypress', onKeypress);
435
+ process.exit(0);
436
+ }
437
+
438
+ if (key.name === 'q' || key.name === 'escape') {
439
+ process.stdin.setRawMode(false);
440
+ process.stdin.removeListener('keypress', onKeypress);
441
+ process.stdout.write('\x1B[2J\x1B[0f');
442
+ resolve({ indices: [], names: [] });
443
+ return;
444
+ }
445
+
446
+ if (key.name === 'up') {
447
+ cursor = cursor > 0 ? cursor - 1 : skills.length - 1;
448
+ render();
449
+ return;
450
+ }
451
+
452
+ if (key.name === 'down') {
453
+ cursor = cursor < skills.length - 1 ? cursor + 1 : 0;
454
+ render();
455
+ return;
456
+ }
457
+
458
+ if (key.name === 'space') {
459
+ selected[cursor] = !selected[cursor];
460
+ render();
461
+ return;
462
+ }
463
+
464
+ if (key.name === 'a') {
465
+ selected.fill(true);
466
+ render();
467
+ return;
468
+ }
469
+
470
+ if (key.name === 'n') {
471
+ selected.fill(false);
472
+ render();
473
+ return;
474
+ }
475
+
476
+ if (key.name === 'return') {
477
+ const result = getSelectedResult();
478
+
479
+ if (result.indices.length === 0) {
480
+ render();
481
+ return;
482
+ }
483
+
484
+ process.stdin.setRawMode(false);
485
+ process.stdin.removeListener('keypress', onKeypress);
486
+
487
+ process.stdout.write('\x1B[2J\x1B[0f');
488
+ console.log('');
489
+ console.log('[Momo AI]'.blue.bold + ' 即将安装以下技能:');
490
+ console.log('');
491
+ result.names.forEach(name => {
492
+ console.log(` ✓ ${name}`.green);
493
+ });
494
+ console.log('');
495
+
496
+ resolve(result);
497
+ return;
498
+ }
499
+ };
500
+
501
+ 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
+ }
518
+
519
+ const answer = await Ec.ask('确认安装?(y/N): ');
520
+ if (answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes') {
521
+ return result.indices;
522
+ } else {
523
+ Ec.waiting('已取消安装');
524
+ return [];
525
+ }
526
+ };
527
+
528
+ /**
529
+ * 安装选中的技能
530
+ * @param {Array} skills 技能列表
531
+ * @param {Array} selectedIndices 选中的索引
532
+ * @param {string} targetDir 目标目录
533
+ */
534
+ const _installSkills = async (skills, selectedIndices, targetDir) => {
535
+ await fsAsync.mkdir(targetDir, { recursive: true });
536
+
537
+ for (const index of selectedIndices) {
538
+ const skill = skills[index];
539
+ const destPath = path.join(targetDir, skill.dirname);
540
+
541
+ Ec.waiting(`正在安装技能: ${skill.name}`.cyan);
542
+
543
+ if (fs.existsSync(destPath)) {
544
+ Ec.warn(` 技能 "${skill.name}" 已存在,将覆盖...`);
545
+ await fsAsync.rm(destPath, { recursive: true, force: true });
546
+ }
547
+
548
+ await _copyDirectory(skill.path, destPath);
549
+
550
+ Ec.waiting(` ✓ 已安装到 ${destPath}`.green);
551
+ }
552
+ };
553
+
554
+ /**
555
+ * 从本地安装技能(扫描全局和项目目录)
556
+ * @param {boolean} isGlobal 是否安装到全局目录
557
+ */
558
+ const _installFromLocal = async (isGlobal = false) => {
559
+ const allSkills = [];
560
+
561
+ // 1. 扫描全局技能目录
562
+ Ec.waiting('正在扫描全局技能仓库...');
563
+ if (fs.existsSync(GLOBAL_SKILLS_DIR)) {
564
+ const globalSkills = _scanSkillsFromDir(GLOBAL_SKILLS_DIR, SOURCE_GLOBAL);
565
+ allSkills.push(...globalSkills);
566
+ Ec.waiting(` 找到 ${globalSkills.length} 个全局技能`);
567
+ } else {
568
+ Ec.waiting(` 全局目录不存在: ${GLOBAL_SKILLS_DIR}`.gray);
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);
603
+ }
604
+
605
+ // 根据 -g 参数决定安装位置
606
+ const targetDir = isGlobal
607
+ ? GLOBAL_SKILLS_DIR
608
+ : path.join(projectDir, '.claude', 'skills');
609
+
610
+ console.log('');
611
+ Ec.info(`将安装 ${selectedIndices.length} 个技能到 ${targetDir}`);
612
+
613
+ await _installSkills(allSkills, selectedIndices, targetDir);
614
+
615
+ Ec.info(`✅ 成功安装 ${selectedIndices.length} 个技能!`);
616
+ };
617
+
618
+ /**
619
+ * 从远程仓库安装技能
620
+ * @param {Object} repository 仓库配置
621
+ * @param {boolean} isGlobal 是否安装到全局目录
622
+ */
623
+ const _installFromRemote = async (repository, isGlobal = false) => {
624
+ const projectDir = process.cwd();
625
+
626
+ console.log('');
627
+ Ec.info(`已选择仓库: ${repository.name}`);
628
+ Ec.waiting(`仓库地址: ${repository.url}`);
629
+ Ec.waiting(`技能目录: ${repository.skillsPath || 'skills'}`);
630
+ console.log('');
631
+
632
+ // 确保 .r2mo/repo 在 .gitignore 中
633
+ _ensureGitIgnore(projectDir);
634
+
635
+ // 获取本地缓存路径
636
+ const localRepoPath = _getLocalRepoPath(projectDir, repository.name);
637
+
638
+ // 克隆或更新仓库
639
+ const success = _cloneOrUpdateRepository(repository.url, localRepoPath);
640
+ if (!success) {
641
+ return;
642
+ }
643
+
644
+ // 检查技能目录是否存在
645
+ const skillsPath = path.join(localRepoPath, repository.skillsPath || 'skills');
646
+
647
+ if (!fs.existsSync(skillsPath)) {
648
+ Ec.error(`❌ 仓库中未找到技能目录: ${repository.skillsPath || 'skills'}`);
649
+ Ec.waiting('请检查仓库配置中的 skillsPath 是否正确');
650
+ return;
651
+ }
652
+
653
+ Ec.waiting('✓ 技能目录存在,正在扫描...'.green);
654
+ const skills = _scanSkillsFromDir(skillsPath);
655
+
656
+ if (skills.length === 0) {
657
+ Ec.warn('技能目录存在,但未找到任何有效技能');
658
+ return;
659
+ }
660
+
661
+ Ec.info(`找到 ${skills.length} 个远程技能,正在打开选择菜单...`);
662
+
663
+ const selectedIndices = await _selectSkills(skills, `远程技能 (${repository.name})`, {
664
+ showSource: false,
665
+ showDescription: false,
666
+ showVersion: false
667
+ });
668
+
669
+ if (selectedIndices.length === 0) {
670
+ Ec.waiting('未选择任何技能,退出');
671
+ return;
672
+ }
673
+
674
+ // 根据 -g 参数决定安装位置
675
+ const targetDir = isGlobal
676
+ ? GLOBAL_SKILLS_DIR
677
+ : path.join(projectDir, '.claude', 'skills');
678
+
679
+ console.log('');
680
+ Ec.info(`将安装 ${selectedIndices.length} 个技能到 ${targetDir}`);
681
+
682
+ await _installSkills(skills, selectedIndices, targetDir);
683
+
684
+ Ec.info(`✅ 成功安装 ${selectedIndices.length} 个技能!`);
685
+ };
686
+
687
+ module.exports = async (options) => {
688
+ try {
689
+ // 解析 -r 和 -g 参数
690
+ const { hasRemote, remoteValue } = _parseRemoteArg();
691
+ const isGlobal = _parseGlobalArg();
692
+
693
+ // 显示安装目标信息
694
+ if (isGlobal) {
695
+ console.log('');
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
+ }
718
+
719
+ let selectedRepo = null;
720
+
721
+ if (remoteValue) {
722
+ // -r 有值:直接使用指定仓库
723
+ selectedRepo = repositories.find(r => r.name === remoteValue);
724
+ if (!selectedRepo) {
725
+ Ec.error(`❌ 未找到仓库: ${remoteValue}`);
726
+ Ec.waiting('可用的仓库:');
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
+ }
740
+
741
+ await _installFromRemote(selectedRepo, isGlobal);
742
+ process.exit(0);
743
+ } catch (error) {
744
+ Ec.error(`❌ 执行过程中发生错误: ${error.message}`);
745
+ process.exit(1);
746
+ }
747
+ };