momo-ai 1.0.20 → 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/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/SKILL.md +101 -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/.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 +12 -148
- package/docs/images/logo.jpeg +0 -0
- package/docs/images/r2mo-lain.png +0 -0
- package/install.sh +1 -0
- package/package.json +15 -11
- package/skills/r2mo-rad-domain/SKILL.md +70 -0
- package/skills/r2mo-rad-lain/SKILL.md +101 -0
- package/src/_mcp/skills-server.mjs +70 -0
- package/src/_skill/repositories.json +22 -0
- 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/help.json +5 -0
- package/src/commander/init.json +1 -1
- package/src/commander/mcp.json +13 -0
- package/src/commander/mmr0.json +6 -0
- package/src/commander/mmr2.json +6 -0
- package/src/commander/open.json +8 -2
- package/src/executor/executeApp.js +133 -0
- package/src/executor/executeApply.js +611 -0
- package/src/executor/executeAsk.js +274 -0
- package/src/executor/executeDocs.js +498 -0
- package/src/executor/executeDomain.js +293 -0
- package/src/executor/executeEnv.js +48 -38
- package/src/executor/executeHelp.js +77 -16
- package/src/executor/executeInit.js +176 -346
- package/src/executor/executeMcp.js +363 -0
- package/src/executor/executeMmr0.js +488 -0
- package/src/executor/executeMmr2.js +880 -0
- package/src/executor/executeOpen.js +144 -125
- package/src/executor/index.js +17 -39
- package/src/momo.js +2 -1
- 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-args.js +39 -0
- package/src/utils/momo-file-utils.js +75 -0
- package/src/utils/momo-menu.js +84 -0
- 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/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 -299
- package/src/executor/executeAgentCfg.js +0 -210
- 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 -313
- 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,611 @@
|
|
|
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 } = require('../utils/momo-args');
|
|
9
|
+
const { copyDir, readJson, parseFile } = require('../utils/momo-file-utils');
|
|
10
|
+
const { selectSingle } = require('../utils/momo-menu');
|
|
11
|
+
|
|
12
|
+
// 远程仓库配置文件路径
|
|
13
|
+
const REPOSITORIES_CONFIG = path.join(__dirname, '../_skill/repositories.json');
|
|
14
|
+
// 本地缓存仓库目录
|
|
15
|
+
const LOCAL_REPO_CACHE_DIR = '.r2mo/repo';
|
|
16
|
+
|
|
17
|
+
// 限定技仓库地址
|
|
18
|
+
const RESTRICTED_REPOSITORY = 'https://gitee.com/silentbalanceyh/r2mo-lain.git';
|
|
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
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 确保 .r2mo/repo 在 .gitignore 中
|
|
52
|
+
* @param {string} projectDir 项目目录
|
|
53
|
+
*/
|
|
54
|
+
const _ensureGitIgnore = (projectDir) => {
|
|
55
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
56
|
+
const ignoreEntry = '.r2mo/repo';
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
let content = '';
|
|
60
|
+
if (fs.existsSync(gitignorePath)) {
|
|
61
|
+
content = fs.readFileSync(gitignorePath, 'utf8');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 检查是否已经存在
|
|
65
|
+
const lines = content.split('\n');
|
|
66
|
+
const hasEntry = lines.some(line => line.trim() === ignoreEntry);
|
|
67
|
+
|
|
68
|
+
if (!hasEntry) {
|
|
69
|
+
// 添加到 .gitignore
|
|
70
|
+
const newContent = content.endsWith('\n') || content === ''
|
|
71
|
+
? content + ignoreEntry + '\n'
|
|
72
|
+
: content + '\n' + ignoreEntry + '\n';
|
|
73
|
+
fs.writeFileSync(gitignorePath, newContent);
|
|
74
|
+
Ec.waiting(`已将 ${ignoreEntry} 添加到 .gitignore`);
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
Ec.warn(`更新 .gitignore 失败: ${error.message}`);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 获取仓库本地缓存路径
|
|
83
|
+
* @param {string} projectDir 项目目录
|
|
84
|
+
* @param {string} repoName 仓库名称
|
|
85
|
+
* @returns {string} 本地缓存路径
|
|
86
|
+
*/
|
|
87
|
+
const _getLocalRepoPath = (projectDir, repoName) => {
|
|
88
|
+
// 将 / 替换为 - 避免路径问题
|
|
89
|
+
const safeName = repoName.replace(/\//g, '-');
|
|
90
|
+
return path.join(projectDir, LOCAL_REPO_CACHE_DIR, safeName);
|
|
91
|
+
};
|
|
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
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 克隆或更新远程仓库到本地缓存
|
|
106
|
+
* @param {string} url 仓库 URL
|
|
107
|
+
* @param {string} localPath 本地缓存路径
|
|
108
|
+
* @returns {boolean} 是否成功
|
|
109
|
+
*/
|
|
110
|
+
const _cloneOrUpdateRepository = (url, localPath) => {
|
|
111
|
+
// 确保父目录存在
|
|
112
|
+
const parentDir = path.dirname(localPath);
|
|
113
|
+
if (!fs.existsSync(parentDir)) {
|
|
114
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 检查是否已有本地缓存
|
|
118
|
+
if (fs.existsSync(localPath)) {
|
|
119
|
+
Ec.waiting(`发现本地缓存: ${localPath}`.cyan);
|
|
120
|
+
Ec.waiting('正在拉取最新更新...');
|
|
121
|
+
try {
|
|
122
|
+
execSync(`cd "${localPath}" && git pull --quiet`, {
|
|
123
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
124
|
+
});
|
|
125
|
+
Ec.waiting('✓ 仓库已更新'.green);
|
|
126
|
+
return true;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
// 检查是否为服务器错误
|
|
129
|
+
if (_isServerError(error)) {
|
|
130
|
+
Ec.warn('⚠️ 远程服务器暂时不可用(可能是 GitHub/Gitee 服务器问题)');
|
|
131
|
+
Ec.waiting('将使用本地缓存继续操作...'.yellow);
|
|
132
|
+
// 保留现有缓存,不删除
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
Ec.warn(`更新失败,将重新克隆: ${error.message}`);
|
|
137
|
+
// 删除损坏的缓存
|
|
138
|
+
fs.rmSync(localPath, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 克隆仓库
|
|
143
|
+
Ec.waiting(`正在克隆仓库: ${url}`.cyan);
|
|
144
|
+
Ec.waiting(`本地缓存: ${localPath}`);
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
execSync(`git clone --depth 1 "${url}" "${localPath}"`, {
|
|
148
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
149
|
+
});
|
|
150
|
+
Ec.waiting('✓ 仓库克隆完成'.green);
|
|
151
|
+
return true;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
// 检查是否为服务器错误
|
|
154
|
+
if (_isServerError(error)) {
|
|
155
|
+
Ec.error('❌ 远程服务器暂时不可用(可能是 GitHub/Gitee 服务器问题)');
|
|
156
|
+
Ec.waiting('请稍后重试,或检查网络连接');
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
Ec.error(`克隆仓库失败: ${error.message}`);
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* 读取远程仓库配置
|
|
167
|
+
* @returns {Array} 仓库列表
|
|
168
|
+
*/
|
|
169
|
+
const _loadRepositories = () => {
|
|
170
|
+
try {
|
|
171
|
+
const config = readJson(REPOSITORIES_CONFIG);
|
|
172
|
+
return config ? (config.repositories || []) : [];
|
|
173
|
+
} catch (error) {
|
|
174
|
+
Ec.warn(`读取仓库配置失败: ${error.message}`);
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 解析 SKILL.md 文件的 YAML 头部
|
|
181
|
+
* @param {string} filePath SKILL.md 文件路径
|
|
182
|
+
* @returns {Object|null} 解析的元数据对象
|
|
183
|
+
*/
|
|
184
|
+
const _parseSkillYaml = (filePath) => {
|
|
185
|
+
const parsed = parseFile(filePath);
|
|
186
|
+
return parsed ? parsed.attributes : null;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 扫描技能目录
|
|
191
|
+
* @param {string} skillsDir 技能目录路径
|
|
192
|
+
* @returns {Array} 技能列表
|
|
193
|
+
*/
|
|
194
|
+
const _scanSkillsFromDir = (skillsDir) => {
|
|
195
|
+
const skills = [];
|
|
196
|
+
|
|
197
|
+
if (!fs.existsSync(skillsDir)) {
|
|
198
|
+
return skills;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const items = fs.readdirSync(skillsDir);
|
|
202
|
+
|
|
203
|
+
for (const item of items) {
|
|
204
|
+
const skillDir = path.join(skillsDir, item);
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const stat = fs.statSync(skillDir);
|
|
208
|
+
|
|
209
|
+
if (stat.isDirectory()) {
|
|
210
|
+
const skillFile = path.join(skillDir, 'SKILL.md');
|
|
211
|
+
const metadata = _parseSkillYaml(skillFile);
|
|
212
|
+
|
|
213
|
+
if (metadata) {
|
|
214
|
+
// 判断技能类型:检查 repository 字段
|
|
215
|
+
const repository = metadata.repository || '';
|
|
216
|
+
const skillType = repository === RESTRICTED_REPOSITORY ? '限定技' : '通用技';
|
|
217
|
+
|
|
218
|
+
skills.push({
|
|
219
|
+
dirname: item,
|
|
220
|
+
path: skillDir,
|
|
221
|
+
name: metadata.name || item,
|
|
222
|
+
description: metadata.description || '无描述',
|
|
223
|
+
version: metadata.version || '未知',
|
|
224
|
+
category: metadata.category || '未分类',
|
|
225
|
+
tags: metadata.tags || [],
|
|
226
|
+
skillType: skillType,
|
|
227
|
+
repository: repository
|
|
228
|
+
});
|
|
229
|
+
} else {
|
|
230
|
+
skills.push({
|
|
231
|
+
dirname: item,
|
|
232
|
+
path: skillDir,
|
|
233
|
+
name: item,
|
|
234
|
+
description: '(未找到 SKILL.md)',
|
|
235
|
+
version: '未知',
|
|
236
|
+
category: '未分类',
|
|
237
|
+
tags: [],
|
|
238
|
+
skillType: '通用技',
|
|
239
|
+
repository: ''
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} catch (e) {
|
|
244
|
+
// 忽略无法访问的目录
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return skills;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 递归拷贝目录
|
|
253
|
+
* @param {string} src 源目录
|
|
254
|
+
* @param {string} dest 目标目录
|
|
255
|
+
*/
|
|
256
|
+
const _copyDirectory = async (src, dest) => {
|
|
257
|
+
await copyDir(src, dest);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 截断描述文本
|
|
262
|
+
* @param {string} text 原始文本
|
|
263
|
+
* @param {number} maxLen 最大长度
|
|
264
|
+
* @returns {string} 截断后的文本
|
|
265
|
+
*/
|
|
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
|
+
};
|
|
273
|
+
|
|
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;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 选择技能(多选菜单)
|
|
286
|
+
* @param {Array} skills 技能列表
|
|
287
|
+
* @param {string} title 菜单标题
|
|
288
|
+
* @returns {Promise<Array>} 选中的技能索引列表
|
|
289
|
+
*/
|
|
290
|
+
const _selectSkills = async (skills, title) => {
|
|
291
|
+
// 对于远程技能(官方),不显示描述
|
|
292
|
+
const showDescription = false;
|
|
293
|
+
|
|
294
|
+
return new Promise((resolve) => {
|
|
295
|
+
const selected = new Array(skills.length).fill(false);
|
|
296
|
+
let cursor = 0;
|
|
297
|
+
|
|
298
|
+
const maxNameLen = Math.max(...skills.map(s => s.name.length), 4);
|
|
299
|
+
const maxTypeLen = Math.max(...skills.map(s => (s.skillType || '').length), 4);
|
|
300
|
+
|
|
301
|
+
const render = () => {
|
|
302
|
+
process.stdout.write('\x1B[2J\x1B[0f');
|
|
303
|
+
|
|
304
|
+
console.log('');
|
|
305
|
+
console.log('[Momo AI]'.blue.bold + ` ====== ${title} ======`.blue);
|
|
306
|
+
console.log('');
|
|
307
|
+
|
|
308
|
+
skills.forEach((skill, index) => {
|
|
309
|
+
const isActive = index === cursor;
|
|
310
|
+
const checkbox = selected[index] ? '[✓]'.green : '[ ]';
|
|
311
|
+
const pointer = isActive ? '▸'.cyan : ' ';
|
|
312
|
+
const name = skill.name.padEnd(maxNameLen);
|
|
313
|
+
const version = skill.version ? `v${skill.version}`.yellow : '';
|
|
314
|
+
|
|
315
|
+
// 技能类型标记(限定技/通用技)
|
|
316
|
+
const skillType = skill.skillType || '通用技';
|
|
317
|
+
const typeTag = skillType === '限定技'
|
|
318
|
+
? `[${skillType}]`.red
|
|
319
|
+
: `[${skillType}]`.green;
|
|
320
|
+
// 统一对齐:限定技和通用技都是3个字符,使用固定宽度确保对齐
|
|
321
|
+
const typeTagPadded = typeTag.padEnd(25); // 固定宽度确保对齐
|
|
322
|
+
|
|
323
|
+
// 统一技能名称样式,不加粗
|
|
324
|
+
const nameStyled = isActive ? name.cyan.bold : name.cyan;
|
|
325
|
+
console.log(` ${pointer} ${checkbox} ${typeTagPadded}${nameStyled} ${version}`);
|
|
326
|
+
|
|
327
|
+
// 显示描述(可选,远程技能不显示)
|
|
328
|
+
if (showDescription) {
|
|
329
|
+
const desc = _truncateDescription(skill.description, 50);
|
|
330
|
+
console.log(` ${desc.gray}`);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
console.log('');
|
|
335
|
+
console.log(' ─────────────────────────────────────────────────'.gray);
|
|
336
|
+
console.log(' ↑/↓ 移动 │ 空格 选择/取消 │ a 全选 │ n 清空 │ 回车 确认 │ q 退出'.gray);
|
|
337
|
+
console.log('');
|
|
338
|
+
|
|
339
|
+
const selectedCount = selected.filter(s => s).length;
|
|
340
|
+
if (selectedCount > 0) {
|
|
341
|
+
console.log(` 已选择 ${selectedCount} 个技能`.green);
|
|
342
|
+
} else {
|
|
343
|
+
console.log(' 未选择任何技能'.yellow);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const getSelectedResult = () => {
|
|
348
|
+
const indices = [];
|
|
349
|
+
const names = [];
|
|
350
|
+
selected.forEach((sel, idx) => {
|
|
351
|
+
if (sel) {
|
|
352
|
+
indices.push(idx);
|
|
353
|
+
names.push(skills[idx].name);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
return { indices, names };
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
readline.emitKeypressEvents(process.stdin);
|
|
360
|
+
if (process.stdin.isTTY) {
|
|
361
|
+
process.stdin.setRawMode(true);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
render();
|
|
365
|
+
|
|
366
|
+
const onKeypress = (str, key) => {
|
|
367
|
+
if (!key) return;
|
|
368
|
+
|
|
369
|
+
if (key.ctrl && key.name === 'c') {
|
|
370
|
+
process.stdin.setRawMode(false);
|
|
371
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
372
|
+
process.exit(0);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (key.name === 'q' || key.name === 'escape') {
|
|
376
|
+
process.stdin.setRawMode(false);
|
|
377
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
378
|
+
process.stdout.write('\x1B[2J\x1B[0f');
|
|
379
|
+
resolve([]);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (key.name === 'up') {
|
|
384
|
+
cursor = cursor > 0 ? cursor - 1 : skills.length - 1;
|
|
385
|
+
render();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (key.name === 'down') {
|
|
390
|
+
cursor = cursor < skills.length - 1 ? cursor + 1 : 0;
|
|
391
|
+
render();
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (key.name === 'space') {
|
|
396
|
+
selected[cursor] = !selected[cursor];
|
|
397
|
+
render();
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (key.name === 'a') {
|
|
402
|
+
selected.fill(true);
|
|
403
|
+
render();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (key.name === 'n') {
|
|
408
|
+
selected.fill(false);
|
|
409
|
+
render();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (key.name === 'return') {
|
|
414
|
+
const result = getSelectedResult();
|
|
415
|
+
|
|
416
|
+
if (result.indices.length === 0) {
|
|
417
|
+
render();
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
process.stdin.setRawMode(false);
|
|
422
|
+
process.stdin.removeListener('keypress', onKeypress);
|
|
423
|
+
|
|
424
|
+
process.stdout.write('\x1B[2J\x1B[0f');
|
|
425
|
+
console.log('');
|
|
426
|
+
console.log('[Momo AI]'.blue.bold + ' 即将安装以下技能:');
|
|
427
|
+
console.log('');
|
|
428
|
+
result.names.forEach(name => {
|
|
429
|
+
console.log(` ✓ ${name}`.green);
|
|
430
|
+
});
|
|
431
|
+
console.log('');
|
|
432
|
+
|
|
433
|
+
// 等待用户确认
|
|
434
|
+
resolve(result.indices);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
process.stdin.on('keypress', onKeypress);
|
|
440
|
+
}).then(async (indices) => {
|
|
441
|
+
if (indices.length === 0) {
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
|
|
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
|
+
});
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* 安装选中的技能
|
|
457
|
+
* @param {Array} skills 技能列表
|
|
458
|
+
* @param {Array} selectedIndices 选中的索引
|
|
459
|
+
* @param {string} targetDir 目标目录
|
|
460
|
+
*/
|
|
461
|
+
const _installSkills = async (skills, selectedIndices, targetDir) => {
|
|
462
|
+
await fsAsync.mkdir(targetDir, { recursive: true });
|
|
463
|
+
|
|
464
|
+
for (const index of selectedIndices) {
|
|
465
|
+
const skill = skills[index];
|
|
466
|
+
const destPath = path.join(targetDir, skill.dirname);
|
|
467
|
+
|
|
468
|
+
Ec.waiting(`正在安装技能: ${skill.name}`.cyan);
|
|
469
|
+
|
|
470
|
+
if (fs.existsSync(destPath)) {
|
|
471
|
+
Ec.warn(` 技能 "${skill.name}" 已存在,将覆盖...`);
|
|
472
|
+
await fsAsync.rm(destPath, { recursive: true, force: true });
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
await _copyDirectory(skill.path, destPath);
|
|
476
|
+
|
|
477
|
+
Ec.waiting(` ✓ 已安装到 ${destPath}`.green);
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* 选择远程仓库
|
|
483
|
+
* @param {Array} repositories 仓库列表
|
|
484
|
+
* @param {string} remoteValue -r 参数的值(如果提供)
|
|
485
|
+
* @returns {Promise<Object|null>} 选中的仓库
|
|
486
|
+
*/
|
|
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('将显示仓库选择菜单...');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// 显示仓库选择菜单
|
|
502
|
+
const menuItems = repositories.map(repo => ({
|
|
503
|
+
name: repo.name,
|
|
504
|
+
description: `${repo.description || ''} - ${repo.url}`,
|
|
505
|
+
repo: repo // 保存原始仓库对象的引用
|
|
506
|
+
}));
|
|
507
|
+
|
|
508
|
+
const selected = await selectSingle(menuItems, '选择远程仓库');
|
|
509
|
+
|
|
510
|
+
// 返回原始仓库对象
|
|
511
|
+
return selected ? selected.repo : null;
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* 从远程仓库安装技能
|
|
516
|
+
* @param {Object} repository 仓库配置
|
|
517
|
+
* @param {string} targetPath 目标路径
|
|
518
|
+
*/
|
|
519
|
+
const _installFromRemote = async (repository, targetPath) => {
|
|
520
|
+
const projectDir = process.cwd();
|
|
521
|
+
|
|
522
|
+
console.log('');
|
|
523
|
+
Ec.info(`已选择仓库: ${repository.name}`);
|
|
524
|
+
Ec.waiting(`仓库地址: ${repository.url}`);
|
|
525
|
+
Ec.waiting(`技能目录: ${repository.skillsPath || 'skills'}`);
|
|
526
|
+
console.log('');
|
|
527
|
+
|
|
528
|
+
// 确保 .r2mo/repo 在 .gitignore 中
|
|
529
|
+
_ensureGitIgnore(projectDir);
|
|
530
|
+
|
|
531
|
+
// 获取本地缓存路径
|
|
532
|
+
const localRepoPath = _getLocalRepoPath(projectDir, repository.name);
|
|
533
|
+
|
|
534
|
+
// 克隆或更新仓库
|
|
535
|
+
const success = _cloneOrUpdateRepository(repository.url, localRepoPath);
|
|
536
|
+
if (!success) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// 检查技能目录是否存在
|
|
541
|
+
const skillsPath = path.join(localRepoPath, repository.skillsPath || 'skills');
|
|
542
|
+
|
|
543
|
+
if (!fs.existsSync(skillsPath)) {
|
|
544
|
+
Ec.error(`❌ 仓库中未找到技能目录: ${repository.skillsPath || 'skills'}`);
|
|
545
|
+
Ec.waiting('请检查仓库配置中的 skillsPath 是否正确');
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
Ec.waiting('✓ 技能目录存在,正在扫描...'.green);
|
|
550
|
+
const skills = _scanSkillsFromDir(skillsPath);
|
|
551
|
+
|
|
552
|
+
if (skills.length === 0) {
|
|
553
|
+
Ec.warn('技能目录存在,但未找到任何有效技能');
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
Ec.info(`找到 ${skills.length} 个远程技能,正在打开选择菜单...`);
|
|
558
|
+
|
|
559
|
+
const selectedIndices = await _selectSkills(skills, `远程技能 (${repository.name})`);
|
|
560
|
+
|
|
561
|
+
if (selectedIndices.length === 0) {
|
|
562
|
+
Ec.waiting('未选择任何技能,退出');
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
console.log('');
|
|
567
|
+
Ec.info(`将安装 ${selectedIndices.length} 个技能到 ${targetPath}`);
|
|
568
|
+
|
|
569
|
+
await _installSkills(skills, selectedIndices, targetPath);
|
|
570
|
+
|
|
571
|
+
Ec.info(`✅ 成功安装 ${selectedIndices.length} 个技能!`);
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
module.exports = async (options) => {
|
|
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
|
+
}
|
|
583
|
+
|
|
584
|
+
// 加载远程仓库配置
|
|
585
|
+
const repositories = _loadRepositories();
|
|
586
|
+
|
|
587
|
+
if (repositories.length === 0) {
|
|
588
|
+
Ec.error('❌ 未找到任何远程仓库配置');
|
|
589
|
+
Ec.waiting(`请检查配置文件: ${REPOSITORIES_CONFIG}`);
|
|
590
|
+
process.exit(1);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// 选择仓库
|
|
594
|
+
const repository = await _selectRepository(repositories, remoteValue);
|
|
595
|
+
if (!repository) {
|
|
596
|
+
Ec.waiting('已取消选择仓库');
|
|
597
|
+
process.exit(0);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// 选择目标路径
|
|
601
|
+
const targetPathConfig = await _selectTargetPath();
|
|
602
|
+
if (!targetPathConfig) {
|
|
603
|
+
Ec.waiting('已取消选择目标路径');
|
|
604
|
+
process.exit(0);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// 从远程仓库安装技能
|
|
608
|
+
await _installFromRemote(repository, targetPathConfig.path);
|
|
609
|
+
|
|
610
|
+
process.exit(0);
|
|
611
|
+
};
|