@yottameta/yotta-skills-plugin 0.0.0 → 0.5.0
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/.agents/plugins/marketplace.json +25 -0
- package/.claude-plugin/marketplace.json +25 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/mcp.json +12 -0
- package/package.json +32 -13
- package/plugin.json +20 -0
- package/skills/yotta-skills/LICENSE +21 -0
- package/skills/yotta-skills/NOTICE +13 -0
- package/skills/yotta-skills/SKILL.md +248 -0
- package/skills/yotta-skills/assets/banner.png +0 -0
- package/skills/yotta-skills/bin/yotta-skills.js +622 -0
- package/skills/yotta-skills/references/install-flow.md +78 -0
- package/skills/yotta-skills/references/orchestration.md +88 -0
- package/skills/yotta-skills/references/skill-list.md +40 -0
- package/skills/yotta-skills/references/tutorial.md +132 -0
- package/skills/yotta-skills/scripts/yotta-skills-mcp.py +268 -0
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* yotta-skills(元阁)—— 全家技能一键安装 CLI(YottaSkills 合集)
|
|
4
|
+
*
|
|
5
|
+
* 用法:
|
|
6
|
+
* npx -y @yottameta/yotta-skills --list # 列出全家技能 + 版本 + 说明
|
|
7
|
+
* npx -y @yottameta/yotta-skills install --agent <name> # 装全家到智能体默认用户级目录(推荐)
|
|
8
|
+
* npx -y @yottameta/yotta-skills install --dir <path> # 装全家到指定目录
|
|
9
|
+
* npx -y @yottameta/yotta-skills install <skill> --dir <path> # 装单个技能
|
|
10
|
+
* npx -y @yottameta/yotta-skills update --agent <name> # 增量更新已装技能(补齐缺失/版本不一致)
|
|
11
|
+
* npx -y @yottameta/yotta-skills --dry-run # 预览将安装清单(不联网、不改动)
|
|
12
|
+
*
|
|
13
|
+
* 版本策略:清单锁定 `major.x`(不锁死 patch,维护性更新随最新);--pin 锁死精确版本。
|
|
14
|
+
* 依赖:Node.js 18+;npm(pack);系统 tar(解压);元信 scan 可选(装了 yotta-verify 自动启用)。
|
|
15
|
+
* 边界:只做「清单 + 下载 + 落位 + 汇总」;不内置任何技能本体;不 -g 污染。
|
|
16
|
+
*/
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const { spawnSync } = require('child_process');
|
|
23
|
+
|
|
24
|
+
const PKG_ROOT = path.join(__dirname, '..');
|
|
25
|
+
let VERSION = '0.2.1';
|
|
26
|
+
try { VERSION = require(path.join(PKG_ROOT, 'package.json')).version; } catch (_) { /* keep fallback */ }
|
|
27
|
+
|
|
28
|
+
function loadManifest() {
|
|
29
|
+
const file = process.env.YOTTA_SKILLS_MANIFEST || path.join(PKG_ROOT, 'skills.json');
|
|
30
|
+
let data;
|
|
31
|
+
try {
|
|
32
|
+
data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
33
|
+
} catch (e) {
|
|
34
|
+
die('无法读取技能清单: ' + file + '(' + e.message + ')');
|
|
35
|
+
}
|
|
36
|
+
const list = Array.isArray(data) ? data : data.skills;
|
|
37
|
+
if (!Array.isArray(list) || list.length === 0) die('技能清单为空: ' + file);
|
|
38
|
+
for (const s of list) {
|
|
39
|
+
for (const f of ['slug', 'name', 'pkg', 'version']) {
|
|
40
|
+
if (!s[f]) die('技能清单字段缺失 ' + f + ':' + JSON.stringify(s));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return list;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const MANIFEST = loadManifest();
|
|
47
|
+
|
|
48
|
+
// 智能体 -> 用户级默认技能目录(与各技能 install.js 同源;.agents/skills 并非通用目录)
|
|
49
|
+
const AGENT_DIRS = {
|
|
50
|
+
claude: { label: 'Claude Code', dirs: ['.claude/skills'] },
|
|
51
|
+
cursor: { label: 'Cursor', dirs: ['.cursor/skills', '.agents/skills'] },
|
|
52
|
+
codex: { label: 'Codex', dirs: ['.codex/skills'] }, // 特判:$CODEX_HOME/skills
|
|
53
|
+
gemini: { label: 'Gemini CLI', dirs: ['.gemini/skills', '.agents/skills'] },
|
|
54
|
+
goose: { label: 'Goose', dirs: ['.config/goose/skills', '.agents/skills'] },
|
|
55
|
+
amp: { label: 'Amp', dirs: ['.config/agents/skills', '.agents/skills'] },
|
|
56
|
+
opencode: { label: 'OpenCode', dirs: ['.config/opencode/skills'] }, // 特判:$XDG_CONFIG_HOME
|
|
57
|
+
windsurf: { label: 'Windsurf', dirs: ['.codeium/windsurf/skills'] },
|
|
58
|
+
workbuddy: { label: 'WorkBuddy', dirs: ['.workbuddy/skills'] },
|
|
59
|
+
kiro: { label: 'Kiro', dirs: ['.kiro/skills'] },
|
|
60
|
+
trae: { label: 'Trae Code CLI', dirs: ['.traecli/skills'] },
|
|
61
|
+
'trae-cn': { label: 'Trae IDE(国内)', dirs: ['.trae-cn/skills'] },
|
|
62
|
+
qwen: { label: 'Qwen Code', dirs: ['.qwen/skills'] },
|
|
63
|
+
comate: { label: 'Comate 文心快码', dirs: ['.comate/skills'] },
|
|
64
|
+
codebuddy: { label: 'CodeBuddy Code', dirs: ['.codebuddy/skills'] },
|
|
65
|
+
kimi: { label: 'Kimi Code CLI', dirs: ['.kimi/skills'] },
|
|
66
|
+
agents: { label: '通用 AGENTS.md', dirs: ['.agents/skills'] },
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function codexUserDir() {
|
|
70
|
+
const base = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
71
|
+
return path.join(base, 'skills');
|
|
72
|
+
}
|
|
73
|
+
function opencodeUserDir() {
|
|
74
|
+
const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
75
|
+
return path.join(base, 'opencode', 'skills');
|
|
76
|
+
}
|
|
77
|
+
function resolveUserDir(rel) {
|
|
78
|
+
if (rel === '.codex/skills') return codexUserDir();
|
|
79
|
+
if (rel === '.config/opencode/skills') return opencodeUserDir();
|
|
80
|
+
return path.join(os.homedir(), rel);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── 工具函数 ───────────────────────────────────────────────────────────────
|
|
84
|
+
function die(msg, code) {
|
|
85
|
+
process.stderr.write('错误:' + msg + '\n');
|
|
86
|
+
process.exit(code === undefined ? 2 : code);
|
|
87
|
+
}
|
|
88
|
+
function out(s) { process.stdout.write(s + '\n'); }
|
|
89
|
+
|
|
90
|
+
function tarBin() { return 'tar'; }
|
|
91
|
+
|
|
92
|
+
// 解析 npm 调用方式(Windows 的 .cmd 不能直接 spawn:EINVAL;cmd /c 引号脆弱)。
|
|
93
|
+
// 最优:定位 npm.cmd -> 读内容 -> 提取 node_modules/npm/bin/npm-cli.js -> 用 node 直接执行。
|
|
94
|
+
// 兼容:--npm / YOTTA_SKILLS_NPM 可指向 .js(node 执行)、.cmd(同样解析)、或可执行文件。
|
|
95
|
+
// 标准 npm 安装器布局:npm-cli.js 恒在 npm.cmd 同目录 node_modules/npm/bin/ 下(不解析 cmd 脚本内容,
|
|
96
|
+
// 避免 %dp0% 等 cmd 变量干扰;找不到则返回 null 走 shell 回退)
|
|
97
|
+
function npmCliFromCmd(cmdFile) {
|
|
98
|
+
try {
|
|
99
|
+
const p = path.join(path.dirname(cmdFile), 'node_modules', 'npm', 'bin', 'npm-cli.js');
|
|
100
|
+
return fs.existsSync(p) ? p : null;
|
|
101
|
+
} catch (_) { return null; }
|
|
102
|
+
}
|
|
103
|
+
function resolveNpm(opts) {
|
|
104
|
+
const custom = opts.npm || process.env.YOTTA_SKILLS_NPM;
|
|
105
|
+
if (custom) {
|
|
106
|
+
if (/\.js$/i.test(custom)) return { bin: process.execPath, prefix: [custom], shell: false };
|
|
107
|
+
if (/\.(cmd|bat)$/i.test(custom)) {
|
|
108
|
+
const cli = npmCliFromCmd(custom);
|
|
109
|
+
if (cli) return { bin: process.execPath, prefix: [cli], shell: false };
|
|
110
|
+
return { bin: custom, prefix: [], shell: true };
|
|
111
|
+
}
|
|
112
|
+
return { bin: custom, prefix: [], shell: false };
|
|
113
|
+
}
|
|
114
|
+
if (process.platform === 'win32') {
|
|
115
|
+
try {
|
|
116
|
+
const w = spawnSync('where.exe', ['npm.cmd'], { encoding: 'utf8', timeout: 15000 });
|
|
117
|
+
const line = (w.stdout || '').split(/\r?\n/).map(s => s.trim()).find(Boolean);
|
|
118
|
+
if (line) {
|
|
119
|
+
const cli = npmCliFromCmd(line);
|
|
120
|
+
if (cli) return { bin: process.execPath, prefix: [cli], shell: false };
|
|
121
|
+
}
|
|
122
|
+
} catch (_) { /* fallthrough */ }
|
|
123
|
+
return { bin: 'npm.cmd', prefix: [], shell: true };
|
|
124
|
+
}
|
|
125
|
+
return { bin: 'npm', prefix: [], shell: false };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
function parseArgs(argv) {
|
|
130
|
+
const opts = {
|
|
131
|
+
list: false, dryRun: false, pin: false, skipScan: false, force: false,
|
|
132
|
+
help: false, version: false, agent: null, dir: null, npm: null,
|
|
133
|
+
python: null, verify: null, command: null, skill: null, rest: [],
|
|
134
|
+
inventory: false, reindex: false, noReindex: false, json: false, project: false, route: null,
|
|
135
|
+
};
|
|
136
|
+
const positionals = [];
|
|
137
|
+
for (let i = 0; i < argv.length; i++) {
|
|
138
|
+
const a = argv[i];
|
|
139
|
+
const take = (name) => { const v = argv[i + 1]; if (v === undefined || v.startsWith('--')) die(name + ' 缺少参数值'); i++; return v; };
|
|
140
|
+
if (a === '--list' || a === '-l') opts.list = true;
|
|
141
|
+
else if (a === '--dry-run') opts.dryRun = true;
|
|
142
|
+
else if (a === '--pin') opts.pin = true;
|
|
143
|
+
else if (a === '--skip-scan') opts.skipScan = true;
|
|
144
|
+
else if (a === '--force') opts.force = true;
|
|
145
|
+
else if (a === '--help' || a === '-h') opts.help = true;
|
|
146
|
+
else if (a === '--version' || a === '-v') opts.version = true;
|
|
147
|
+
else if (a === '--inventory' || a === '--inv') opts.inventory = true;
|
|
148
|
+
else if (a === '--route') {
|
|
149
|
+
const value = argv[i + 1];
|
|
150
|
+
if (value === undefined || value.startsWith('--')) die('--route 缺少需求摘要');
|
|
151
|
+
opts.route = value;
|
|
152
|
+
i++;
|
|
153
|
+
}
|
|
154
|
+
else if (a === '--reindex' || a === '--rescan') opts.reindex = true;
|
|
155
|
+
else if (a === '--no-reindex') opts.noReindex = true;
|
|
156
|
+
else if (a === '--json') opts.json = true;
|
|
157
|
+
else if (a === '--project') opts.project = true;
|
|
158
|
+
else if (a === '--agent') opts.agent = take('--agent').toLowerCase();
|
|
159
|
+
else if (a === '--dir') opts.dir = take('--dir');
|
|
160
|
+
else if (a === '--npm') opts.npm = take('--npm');
|
|
161
|
+
else if (a === '--python') opts.python = take('--python');
|
|
162
|
+
else if (a === '--verify') opts.verify = take('--verify');
|
|
163
|
+
else if (a.startsWith('-')) die('未知参数: ' + a);
|
|
164
|
+
else positionals.push(a);
|
|
165
|
+
}
|
|
166
|
+
// 命令解析:install / update,其余位置参数 = 技能 slug(可多个)
|
|
167
|
+
for (const p of positionals) {
|
|
168
|
+
if (p === 'install' || p === 'update') {
|
|
169
|
+
if (opts.command && opts.command !== p) die('命令冲突:' + opts.command + ' 与 ' + p);
|
|
170
|
+
opts.command = p;
|
|
171
|
+
} else {
|
|
172
|
+
opts.rest.push(p);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// 直接给 slug 且无命令 → 视为 install 单个/多个
|
|
176
|
+
if (!opts.command && opts.rest.length > 0) opts.command = 'install';
|
|
177
|
+
opts.skills = opts.rest.map(s => s.toLowerCase());
|
|
178
|
+
return opts;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function skillRange(s) {
|
|
182
|
+
const major = String(s.version).split('.')[0];
|
|
183
|
+
return major + '.x';
|
|
184
|
+
}
|
|
185
|
+
function specOf(s, pin) {
|
|
186
|
+
return s.pkg + '@' + (pin ? s.version : skillRange(s));
|
|
187
|
+
}
|
|
188
|
+
function findSkill(slug) {
|
|
189
|
+
return MANIFEST.find(s => s.slug === slug || s.pkg === slug || s.pkg.replace('@yottameta/', '') === slug);
|
|
190
|
+
}
|
|
191
|
+
function selectSkills(opts) {
|
|
192
|
+
if (opts.skills.length === 0) return MANIFEST;
|
|
193
|
+
const picked = [];
|
|
194
|
+
for (const slug of opts.skills) {
|
|
195
|
+
const s = findSkill(slug);
|
|
196
|
+
if (!s) die('未知技能: ' + slug + '(可用: yotta-skills --list)');
|
|
197
|
+
picked.push(s);
|
|
198
|
+
}
|
|
199
|
+
return picked;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function readInstalledVersion(dir) {
|
|
203
|
+
const f = path.join(dir, 'SKILL.md');
|
|
204
|
+
try {
|
|
205
|
+
const text = fs.readFileSync(f, 'utf8');
|
|
206
|
+
const m = text.match(/^version:\s*([0-9]+\.[0-9]+\.[0-9]+)/m);
|
|
207
|
+
return m ? m[1] : null;
|
|
208
|
+
} catch (_) { return null; }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function shouldSkip(name, isFile) {
|
|
212
|
+
if (name === '__pycache__' || name === '.pytest_cache' || name === '.mypy_cache') return true;
|
|
213
|
+
if (isFile && (name.endsWith('.pyc') || name.endsWith('.pyo'))) return true;
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
function copyDir(src, dst, skip) {
|
|
217
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
218
|
+
if (skip.has(entry.name) || shouldSkip(entry.name, entry.isFile())) continue;
|
|
219
|
+
const s = path.join(src, entry.name);
|
|
220
|
+
const d = path.join(dst, entry.name);
|
|
221
|
+
if (entry.isDirectory()) {
|
|
222
|
+
fs.mkdirSync(d, { recursive: true });
|
|
223
|
+
copyDir(s, d, skip);
|
|
224
|
+
} else if (entry.isFile()) {
|
|
225
|
+
fs.copyFileSync(s, d);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function resolveTargetDir(opts) {
|
|
231
|
+
if (opts.dir) return path.resolve(opts.dir);
|
|
232
|
+
if (opts.agent) {
|
|
233
|
+
const info = AGENT_DIRS[opts.agent];
|
|
234
|
+
if (!info) die('未收录智能体: ' + opts.agent + '。可用: ' + Object.keys(AGENT_DIRS).join(', ') + ';或改用 --dir <路径>。');
|
|
235
|
+
return resolveUserDir(info.dirs[0]);
|
|
236
|
+
}
|
|
237
|
+
return null; // 未指定目标(项目级检测由调用方处理)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function detectProjectDir() {
|
|
241
|
+
const PROJECT_DIRS = [
|
|
242
|
+
'.claude/skills', '.cursor/skills', '.codex/skills', '.config/goose/skills',
|
|
243
|
+
'.config/agents/skills', '.opencode/skills', '.codeium/windsurf/skills',
|
|
244
|
+
'.workbuddy/skills', '.kiro/skills', '.traecli/skills', '.gemini/skills',
|
|
245
|
+
'.trae-cn/skills', '.qwen/skills', '.comate/skills', '.codebuddy/skills',
|
|
246
|
+
'.kimi/skills', '.agents/skills',
|
|
247
|
+
];
|
|
248
|
+
for (const d of PROJECT_DIRS) if (fs.existsSync(d)) return path.resolve(d);
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── 元信 scan(若可用)─────────────────────────────────────────────────────
|
|
253
|
+
function findVerifyEngine(dest, opts) {
|
|
254
|
+
const cands = [];
|
|
255
|
+
if (opts.verify) cands.push(path.resolve(opts.verify));
|
|
256
|
+
if (process.env.YOTTA_SKILLS_VERIFY) cands.push(path.resolve(process.env.YOTTA_SKILLS_VERIFY));
|
|
257
|
+
if (dest) cands.push(path.join(dest, 'yotta-verify', 'scripts', 'yotta_verify.py'));
|
|
258
|
+
for (const c of cands) {
|
|
259
|
+
try { if (fs.statSync(c).isFile()) return c; } catch (_) { /* next */ }
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
function findPython(opts) {
|
|
264
|
+
const cands = [];
|
|
265
|
+
if (opts.python) cands.push(opts.python);
|
|
266
|
+
if (process.env.YOTTA_SKILLS_PYTHON) cands.push(process.env.YOTTA_SKILLS_PYTHON);
|
|
267
|
+
cands.push('python3', 'python');
|
|
268
|
+
if (process.platform === 'win32') cands.push('py');
|
|
269
|
+
for (const c of cands) {
|
|
270
|
+
try {
|
|
271
|
+
const r = spawnSync(c, ['--version'], { encoding: 'utf8', timeout: 10000 });
|
|
272
|
+
if (r.status === 0) return c;
|
|
273
|
+
} catch (_) { /* next */ }
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
function runScan(engine, skillDir) {
|
|
278
|
+
const python = findPython({});
|
|
279
|
+
if (!python) return { ok: false, note: '未找到 python(元信 scan 需要 Python 3.8+)' };
|
|
280
|
+
// -B:禁止 Python 写 __pycache__(否则会在引擎所在目录(可能是已装技能)留下 .pyc 污染)
|
|
281
|
+
const r = spawnSync(python, ['-B', engine, 'scan', skillDir, '--json'], { encoding: 'utf8', timeout: 60000 });
|
|
282
|
+
if (r.status === null) return { ok: false, note: '元信 scan 执行失败' };
|
|
283
|
+
try {
|
|
284
|
+
const j = JSON.parse(r.stdout);
|
|
285
|
+
const c = j.counts || {};
|
|
286
|
+
return { ok: true, verdict: j.verdict, counts: c };
|
|
287
|
+
} catch (_) {
|
|
288
|
+
return { ok: false, note: '元信 scan 输出无法解析' };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── 安装 ───────────────────────────────────────────────────────────────────
|
|
293
|
+
function runNpmPack(skill, opts, packDir) {
|
|
294
|
+
const spec = specOf(skill, opts.pin);
|
|
295
|
+
const args = ['pack', spec, '--pack-destination', packDir];
|
|
296
|
+
const flags = (process.env.YOTTA_SKILLS_NPM_FLAGS || '').trim();
|
|
297
|
+
if (flags) args.push(...flags.split(/\s+/));
|
|
298
|
+
const npm = resolveNpm(opts);
|
|
299
|
+
const r = spawnSync(npm.bin, [...npm.prefix, ...args], { encoding: 'utf8', timeout: 180000, maxBuffer: 64 * 1024 * 1024, shell: npm.shell });
|
|
300
|
+
if (r.status !== 0) {
|
|
301
|
+
const raw = (r.stderr || r.stdout || 'npm pack 失败').trim();
|
|
302
|
+
const ll = raw.split(/\r?\n/).filter(Boolean);
|
|
303
|
+
const brief = ll.slice(-4).join(' | ');
|
|
304
|
+
return { error: brief || 'npm pack 失败', detail: raw };
|
|
305
|
+
}
|
|
306
|
+
const lines = (r.stdout || '').split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
307
|
+
let tarball = null;
|
|
308
|
+
for (const l of lines) if (/\.tgz$/.test(l)) tarball = l;
|
|
309
|
+
if (!tarball) {
|
|
310
|
+
try {
|
|
311
|
+
const found = fs.readdirSync(packDir).filter(f => f.endsWith('.tgz'));
|
|
312
|
+
if (found.length === 1) tarball = found[0];
|
|
313
|
+
} catch (_) { /* ignore */ }
|
|
314
|
+
}
|
|
315
|
+
if (!tarball) return { error: '未找到 npm pack 产物(' + spec + ')' };
|
|
316
|
+
const vm = String(tarball).match(/-([0-9]+\.[0-9]+\.[0-9]+)\.tgz$/);
|
|
317
|
+
return { tarball: path.join(packDir, tarball), resolved: vm ? vm[1] : null, spec };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function extractTarball(tarball, extractDir) {
|
|
321
|
+
const r = spawnSync(tarBin(), ['-xzf', tarball, '-C', extractDir], { encoding: 'utf8', timeout: 120000 });
|
|
322
|
+
if (r.status !== 0) return { error: (r.stderr || r.stdout || 'tar 解压失败').trim().split('\n').pop() };
|
|
323
|
+
const pkgDir = path.join(extractDir, 'package');
|
|
324
|
+
if (!fs.existsSync(path.join(pkgDir, 'SKILL.md'))) return { error: '解压产物缺少 SKILL.md(' + tarball + ')' };
|
|
325
|
+
return { pkgDir };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const COPY_SKIP = new Set(['package.json', 'bin', 'node_modules', '.git', '__pycache__']);
|
|
329
|
+
|
|
330
|
+
function installOne(skill, dest, opts) {
|
|
331
|
+
const target = path.join(dest, skill.slug);
|
|
332
|
+
const existing = readInstalledVersion(target);
|
|
333
|
+
if (!opts.force && existing === skill.version) {
|
|
334
|
+
return { skill, status: 'skip', version: existing, note: '已是最新' };
|
|
335
|
+
}
|
|
336
|
+
let tmp;
|
|
337
|
+
try {
|
|
338
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'yotta-skills-'));
|
|
339
|
+
const packDir = path.join(tmp, 'pack');
|
|
340
|
+
fs.mkdirSync(packDir, { recursive: true });
|
|
341
|
+
const packed = runNpmPack(skill, opts, packDir);
|
|
342
|
+
if (packed.error) {
|
|
343
|
+
if (packed.detail) out(' ' + packed.detail.split(/\r?\n/).filter(Boolean).slice(-12).join('\n '));
|
|
344
|
+
return { skill, status: 'fail', version: null, note: packed.error };
|
|
345
|
+
}
|
|
346
|
+
const extractDir = path.join(tmp, 'extract');
|
|
347
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
348
|
+
const extracted = extractTarball(packed.tarball, extractDir);
|
|
349
|
+
if (extracted.error) return { skill, status: 'fail', version: packed.resolved, note: extracted.error };
|
|
350
|
+
|
|
351
|
+
// 元信 scan(若可用):装前摘要,不拦截
|
|
352
|
+
if (!opts.skipScan) {
|
|
353
|
+
const engine = findVerifyEngine(dest, opts);
|
|
354
|
+
if (engine) {
|
|
355
|
+
const scan = runScan(engine, extracted.pkgDir);
|
|
356
|
+
if (scan.ok) {
|
|
357
|
+
const c = scan.counts;
|
|
358
|
+
const line = 'critical ' + (c.critical || 0) + ' / high ' + (c.high || 0) + ' / medium ' + (c.medium || 0) +
|
|
359
|
+
' / low ' + (c.low || 0) + ' / info ' + (c.info || 0);
|
|
360
|
+
out(' 元信 scan: ' + scan.verdict + '(' + line + ')');
|
|
361
|
+
if (scan.verdict && /DO NOT INSTALL/.test(scan.verdict)) out(' ⚠ 元信 verdict 为 DO NOT INSTALL,请人工复核后再使用。');
|
|
362
|
+
} else {
|
|
363
|
+
out(' 元信 scan: ' + scan.note);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
368
|
+
fs.mkdirSync(target, { recursive: true });
|
|
369
|
+
copyDir(extracted.pkgDir, target, COPY_SKIP);
|
|
370
|
+
return { skill, status: 'ok', version: packed.resolved || skill.version, note: null };
|
|
371
|
+
} catch (e) {
|
|
372
|
+
return { skill, status: 'fail', version: null, note: e.message };
|
|
373
|
+
} finally {
|
|
374
|
+
if (tmp) fs.rmSync(tmp, { recursive: true, force: true });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function runInstall(opts, dest) {
|
|
379
|
+
const skills = selectSkills(opts);
|
|
380
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— 安装 ' + skills.length + ' 个技能 -> ' + dest);
|
|
381
|
+
out('版本策略: ' + (opts.pin ? 'pin(锁死清单精确版本)' : 'range(' + skillRange(skills[0]) + ',跟随最新 patch;--pin 锁死)'));
|
|
382
|
+
const results = [];
|
|
383
|
+
let failed = 0;
|
|
384
|
+
for (const s of skills) {
|
|
385
|
+
const r = installOne(s, dest, opts);
|
|
386
|
+
results.push(r);
|
|
387
|
+
if (r.status === 'ok') out(' ✔ ' + s.slug.padEnd(22) + s.name + ' -> ' + (r.version || '?'));
|
|
388
|
+
else if (r.status === 'skip') out(' - ' + s.slug.padEnd(22) + s.name + ' (' + r.note + ',v' + r.version + ')');
|
|
389
|
+
else { failed++; out(' ✘ ' + s.slug.padEnd(22) + s.name + ' 失败: ' + r.note); }
|
|
390
|
+
}
|
|
391
|
+
const ok = results.filter(r => r.status === 'ok').length;
|
|
392
|
+
const skip = results.filter(r => r.status === 'skip').length;
|
|
393
|
+
out('');
|
|
394
|
+
out('汇总: 成功 ' + ok + ' / 跳过(已是最新) ' + skip + ' / 失败 ' + failed + '(共 ' + results.length + ')');
|
|
395
|
+
if (failed > 0) process.exitCode = 1;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function runUpdate(opts, dest) {
|
|
399
|
+
const skills = selectSkills(opts);
|
|
400
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— 增量更新 -> ' + dest);
|
|
401
|
+
const results = [];
|
|
402
|
+
let failed = 0;
|
|
403
|
+
for (const s of skills) {
|
|
404
|
+
const existing = readInstalledVersion(path.join(dest, s.slug));
|
|
405
|
+
if (existing === s.version) {
|
|
406
|
+
out(' - ' + s.slug.padEnd(22) + s.name + ' (已是最新,v' + existing + ')');
|
|
407
|
+
results.push({ skill: s, status: 'skip', version: existing });
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const r = installOne(s, dest, opts);
|
|
411
|
+
results.push(r);
|
|
412
|
+
if (r.status === 'ok') out(' ✔ ' + s.slug.padEnd(22) + s.name + ' -> ' + (r.version || '?') + (existing ? '(原 v' + existing + ')' : '(新装)'));
|
|
413
|
+
else if (r.status === 'skip') out(' - ' + s.slug.padEnd(22) + s.name + ' (' + r.note + ')');
|
|
414
|
+
else { failed++; out(' ✘ ' + s.slug.padEnd(22) + s.name + ' 失败: ' + r.note); }
|
|
415
|
+
}
|
|
416
|
+
const ok = results.filter(r => r.status === 'ok').length;
|
|
417
|
+
const skip = results.filter(r => r.status === 'skip').length;
|
|
418
|
+
out('');
|
|
419
|
+
out('汇总: 更新 ' + ok + ' / 已是最新 ' + skip + ' / 失败 ' + failed);
|
|
420
|
+
if (failed > 0) process.exitCode = 1;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── 展示 ───────────────────────────────────────────────────────────────────
|
|
424
|
+
function printList(opts) {
|
|
425
|
+
const skills = opts.skills.length ? selectSkills(opts) : MANIFEST;
|
|
426
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— 全家技能清单(' + skills.length + ' 个)');
|
|
427
|
+
out('版本策略: ' + (opts.pin ? 'pin(精确锁定)' : 'range(' + skillRange(skills[0]) + ' 起,跟随最新 patch;--pin 锁死)'));
|
|
428
|
+
out('');
|
|
429
|
+
for (const s of skills) {
|
|
430
|
+
out(' ' + s.slug.padEnd(22) + s.name.padEnd(5) + ' ' + specOf(s, opts.pin).padEnd(52) + ' ' + s.version + ' ' + s.desc);
|
|
431
|
+
}
|
|
432
|
+
out('');
|
|
433
|
+
out('安装: yotta-skills install --agent <name> 或 --dir <path>;预览: --dry-run;更新: update。');
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function printHelp() {
|
|
437
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— npx 一次装齐全家技能');
|
|
438
|
+
out('');
|
|
439
|
+
out('用法:');
|
|
440
|
+
out(' yotta-skills --list 列出全家技能 + 版本 + 说明');
|
|
441
|
+
out(' yotta-skills install --agent <name> 装全家到智能体默认用户级目录(推荐)');
|
|
442
|
+
out(' yotta-skills install --dir <path> 装全家到指定目录');
|
|
443
|
+
out(' yotta-skills install <skill> --dir <path> 装单个技能(可多个)');
|
|
444
|
+
out(' yotta-skills update --agent <name> 增量更新已装技能(补齐缺失 / 版本不一致)');
|
|
445
|
+
out(' yotta-skills --dry-run 预览将安装清单(不联网、不改动)');
|
|
446
|
+
out(' yotta-skills --inventory 盘点本机已装技能(自研扫描,不依赖任何元技能)');
|
|
447
|
+
out(' yotta-skills --reindex 重扫注册表(会话开工 / 装技能后自动调用;增量合并)');
|
|
448
|
+
out(' yotta-skills --route "<需求摘要>" 给出场景组合、调用顺序、缺失技能安装建议');
|
|
449
|
+
out('');
|
|
450
|
+
out('选项:');
|
|
451
|
+
out(' --agent <name> 智能体键名(--list 可查看;未知智能体请用 --dir)');
|
|
452
|
+
out(' --dir <path> 目标技能目录(技能会装到 <path>/<slug>)');
|
|
453
|
+
out(' --pin 锁死清单精确版本(默认 range:跟随同 major 最新 patch)');
|
|
454
|
+
out(' --force 已是最新也重装');
|
|
455
|
+
out(' --skip-scan 跳过元信装前 scan(装了 yotta-verify 自动启用)');
|
|
456
|
+
out(' --npm <path> 指定 npm 可执行文件(默认 npm / npm.cmd)');
|
|
457
|
+
out(' --python <path> 指定 python 可执行文件(元信 scan 用)');
|
|
458
|
+
out(' --verify <path> 指定 yotta_verify.py 路径(默认找目标目录已装的元信)');
|
|
459
|
+
out(' --json inventory / reindex / route 时输出 JSON');
|
|
460
|
+
out(' --route <需求> 静态编排路由(输出组合 / 顺序 / 依据 / 缺失技能建议)');
|
|
461
|
+
out(' --project inventory / reindex 时附加扫描当前项目 .agents/skills / .codex/skills');
|
|
462
|
+
out(' --no-reindex 安装 / 更新后不自动重扫注册表');
|
|
463
|
+
out(' -h, --help 帮助');
|
|
464
|
+
out(' -v, --version 版本');
|
|
465
|
+
out('');
|
|
466
|
+
out('支持智能体: ' + Object.keys(AGENT_DIRS).join(', '));
|
|
467
|
+
out('依赖: Node.js 18+ / npm / 系统 tar;环境变量 YOTTA_SKILLS_NPM / YOTTA_SKILLS_PYTHON / YOTTA_SKILLS_VERIFY / YOTTA_SKILLS_NPM_FLAGS 可覆盖。');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
// ── 技能盘点 / re-index(自研零依赖扫描核心) ────────────────────────────────
|
|
473
|
+
/** 重扫所有技能根目录并增量合并进注册表;返回 { result, registry, changes }。 */
|
|
474
|
+
function reindexRegistry(opts) {
|
|
475
|
+
const scan = require('../lib/skills-scan');
|
|
476
|
+
const extraDirs = opts.dir ? [opts.dir] : [];
|
|
477
|
+
const roots = scan.defaultRoots({ extraDirs: extraDirs, project: opts.project });
|
|
478
|
+
const result = scan.scanRoots(roots);
|
|
479
|
+
const prev = scan.readRegistry();
|
|
480
|
+
const { registry, changes } = scan.mergeRegistry(result, prev);
|
|
481
|
+
scan.saveRegistry(registry);
|
|
482
|
+
return { result, registry, changes };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function runInventory(opts) {
|
|
486
|
+
const scan = require('../lib/skills-scan');
|
|
487
|
+
const { result, registry, changes } = reindexRegistry(opts);
|
|
488
|
+
if (opts.json) {
|
|
489
|
+
out(JSON.stringify({
|
|
490
|
+
generated_at: registry.updated,
|
|
491
|
+
note: registry.note,
|
|
492
|
+
scanned: result.scanned,
|
|
493
|
+
errors: result.errors,
|
|
494
|
+
changes: changes,
|
|
495
|
+
skills: Object.values(registry.skills).sort((a, b) => a.slug.localeCompare(b.slug)),
|
|
496
|
+
}, null, 2));
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
out(scan.formatInventory(registry));
|
|
500
|
+
out('');
|
|
501
|
+
out('本次变化: 新增 ' + changes.added.length + ' / 更新 ' + changes.updated.length + ' / 消失 ' + changes.gone.length);
|
|
502
|
+
if (result.errors.length) {
|
|
503
|
+
out('跳过不存在目录 ' + result.errors.length + ' 个: ' + result.errors.map((e) => e.dir).join('; '));
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** --reindex:重扫 + 增量合并,变化聚焦输出(供会话开工 / 钩子使用)。 */
|
|
508
|
+
function runReindex(opts) {
|
|
509
|
+
const scan = require('../lib/skills-scan');
|
|
510
|
+
const { result, registry, changes } = reindexRegistry(opts);
|
|
511
|
+
if (opts.json) {
|
|
512
|
+
out(JSON.stringify({
|
|
513
|
+
reindexed_at: registry.updated,
|
|
514
|
+
note: registry.note,
|
|
515
|
+
count: Object.values(registry.skills).filter((s) => s.status !== 'gone').length,
|
|
516
|
+
changes: changes,
|
|
517
|
+
errors: result.errors,
|
|
518
|
+
scanned: result.scanned,
|
|
519
|
+
}, null, 2));
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
out('re-index 完成: 新增 ' + changes.added.length + ' / 更新 ' + changes.updated.length + ' / 消失 ' + changes.gone.length);
|
|
523
|
+
for (const slug of changes.added) out(' + ' + slug);
|
|
524
|
+
for (const slug of changes.updated) out(' ~ ' + slug);
|
|
525
|
+
for (const slug of changes.gone) out(' - ' + slug);
|
|
526
|
+
if (result.errors.length) {
|
|
527
|
+
out('跳过不存在目录 ' + result.errors.length + ' 个');
|
|
528
|
+
}
|
|
529
|
+
out('注册表: ' + scan.registryPath());
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** --route:静态编排路由,输出组合、顺序、角色、缺失技能建议与其他已装技能候选。 */
|
|
533
|
+
function runRoute(opts) {
|
|
534
|
+
const { routeRequest, defaultYottaSlugs } = require('../lib/route');
|
|
535
|
+
const { registry } = reindexRegistry(opts);
|
|
536
|
+
const yottaSlugs = new Set([...defaultYottaSlugs(), ...MANIFEST.map((s) => s.slug)]);
|
|
537
|
+
const result = routeRequest(opts.route, { registry, yottaSlugs });
|
|
538
|
+
if (opts.json) {
|
|
539
|
+
out(JSON.stringify(result, null, 2));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
out('路由结果: ' + result.playbook.name + '(置信度: ' + result.confidence + ')');
|
|
543
|
+
out('适配意图: ' + result.playbook.intent);
|
|
544
|
+
out('依据: ' + (result.matched_keywords.length ? result.matched_keywords.join('、') : '无明确匹配,回退到入口澄清'));
|
|
545
|
+
out('');
|
|
546
|
+
out('调用顺序:');
|
|
547
|
+
for (const skill of result.skills) {
|
|
548
|
+
out(' ' + skill.order + '. ' + skill.slug + ' [' + (skill.installed ? '已装' : '缺失') + '] - ' + skill.role);
|
|
549
|
+
}
|
|
550
|
+
if (result.missing_skills.length) {
|
|
551
|
+
out('');
|
|
552
|
+
out('缺失技能: ' + result.missing_skills.map((skill) => skill.slug).join(', '));
|
|
553
|
+
out('安装命令: ' + result.install_command);
|
|
554
|
+
out('安全提示: 安装前请先执行装前安全扫描;本命令不会自动安装。');
|
|
555
|
+
}
|
|
556
|
+
if (result.other_skill_candidates.length) {
|
|
557
|
+
out('');
|
|
558
|
+
out('其他已装技能候选(非元阁家族,仅本地机械匹配 frontmatter description):');
|
|
559
|
+
for (const c of result.other_skill_candidates) {
|
|
560
|
+
out(' - ' + c.slug + ' v' + c.version + ' [' + (c.sources || []).join(',') + ']');
|
|
561
|
+
out(' 匹配: ' + c.matched_terms.join('、') + '(得分 ' + c.score + ')· 扫描状态: ' + (c.scan_status === 'not_scanned' ? '未扫描' : c.scan_status));
|
|
562
|
+
out(' ' + (c.description || ''));
|
|
563
|
+
}
|
|
564
|
+
out('安全提示: 其他已装技能只读 frontmatter description 做机械匹配,不读取全文指令、不自动调用;使用/安装前请先执行装前安全扫描,决定权在用户。');
|
|
565
|
+
}
|
|
566
|
+
out('');
|
|
567
|
+
out('应用模式: 显式调用(可经用户确认后切换为按场景自动调用)');
|
|
568
|
+
out('说明: ' + result.disclaimer);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** 装技能后自动 re-index(--no-reindex 关闭):把本次落位结果反映进注册表。best-effort:失败不阻断安装。 */
|
|
572
|
+
function maybeAutoReindex(opts, dest) {
|
|
573
|
+
if (opts.noReindex || opts.dryRun || !dest) return;
|
|
574
|
+
const scan = require('../lib/skills-scan');
|
|
575
|
+
try {
|
|
576
|
+
const { changes } = reindexRegistry({ ...opts, dir: dest });
|
|
577
|
+
out('');
|
|
578
|
+
out('已自动 re-index 注册表(新增 ' + changes.added.length + ' / 更新 ' + changes.updated.length + ' / 消失 ' + changes.gone.length + ';注册表: ' + scan.registryPath() + ')');
|
|
579
|
+
} catch (e) {
|
|
580
|
+
out('');
|
|
581
|
+
out('提示: 自动 re-index 跳过(' + e.message + ');稍后可手动 --reindex 重扫');
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ── main ───────────────────────────────────────────────────────────────────
|
|
586
|
+
function main() {
|
|
587
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
588
|
+
if (opts.help) { printHelp(); return; }
|
|
589
|
+
if (opts.version) { out('yotta-skills v' + VERSION); return; }
|
|
590
|
+
if (opts.list) { printList(opts); return; }
|
|
591
|
+
if (opts.inventory && !opts.command) { runInventory(opts); return; }
|
|
592
|
+
if (opts.reindex && !opts.command) { runReindex(opts); return; }
|
|
593
|
+
if (opts.route && !opts.command) { runRoute(opts); return; }
|
|
594
|
+
|
|
595
|
+
const command = opts.command || 'install';
|
|
596
|
+
let dest = resolveTargetDir(opts);
|
|
597
|
+
if (!dest && !opts.dryRun) dest = detectProjectDir();
|
|
598
|
+
if (!dest && command === 'install' && !opts.dryRun) {
|
|
599
|
+
die('未指定目标:请用 --agent <name> 或 --dir <path>(当前目录未检测到项目级技能目录)。', 4);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (opts.dryRun) {
|
|
603
|
+
const skills = selectSkills(opts);
|
|
604
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— dry-run(' + command + ',' + skills.length + ' 个技能)');
|
|
605
|
+
out('目标: ' + (dest || '未指定(将检测项目级目录)'));
|
|
606
|
+
out('版本策略: ' + (opts.pin ? 'pin(锁死)' : 'range(' + skillRange(skills[0]) + ')'));
|
|
607
|
+
out('');
|
|
608
|
+
for (const s of skills) {
|
|
609
|
+
const tag = command === 'update' ? '将检查/更新' : '将安装';
|
|
610
|
+
out(' [' + tag + '] ' + s.slug.padEnd(22) + s.name.padEnd(5) + ' ' + specOf(s, opts.pin).padEnd(52) + ' ' + s.version + ' ' + s.desc);
|
|
611
|
+
}
|
|
612
|
+
out('');
|
|
613
|
+
out('(dry-run 未执行任何下载/写入)');
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (command === 'update') runUpdate(opts, dest);
|
|
618
|
+
else runInstall(opts, dest);
|
|
619
|
+
maybeAutoReindex(opts, dest);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
main();
|