@yottameta/yotta-skills 0.1.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/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/NOTICE +13 -0
- package/README.md +144 -0
- package/README.zh-CN.md +156 -0
- package/SKILL.md +117 -0
- package/assets/banner.png +0 -0
- package/bin/yotta-skills.js +484 -0
- package/install.sh +132 -0
- package/package.json +45 -0
- package/references/install-flow.md +68 -0
- package/references/skill-list.md +40 -0
- package/references/tutorial.md +106 -0
- package/skills.json +161 -0
|
@@ -0,0 +1,484 @@
|
|
|
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.1.0';
|
|
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
|
+
};
|
|
135
|
+
const positionals = [];
|
|
136
|
+
for (let i = 0; i < argv.length; i++) {
|
|
137
|
+
const a = argv[i];
|
|
138
|
+
const take = (name) => { const v = argv[i + 1]; if (v === undefined || v.startsWith('--')) die(name + ' 缺少参数值'); i++; return v; };
|
|
139
|
+
if (a === '--list' || a === '-l') opts.list = true;
|
|
140
|
+
else if (a === '--dry-run') opts.dryRun = true;
|
|
141
|
+
else if (a === '--pin') opts.pin = true;
|
|
142
|
+
else if (a === '--skip-scan') opts.skipScan = true;
|
|
143
|
+
else if (a === '--force') opts.force = true;
|
|
144
|
+
else if (a === '--help' || a === '-h') opts.help = true;
|
|
145
|
+
else if (a === '--version' || a === '-v') opts.version = true;
|
|
146
|
+
else if (a === '--agent') opts.agent = take('--agent').toLowerCase();
|
|
147
|
+
else if (a === '--dir') opts.dir = take('--dir');
|
|
148
|
+
else if (a === '--npm') opts.npm = take('--npm');
|
|
149
|
+
else if (a === '--python') opts.python = take('--python');
|
|
150
|
+
else if (a === '--verify') opts.verify = take('--verify');
|
|
151
|
+
else if (a.startsWith('-')) die('未知参数: ' + a);
|
|
152
|
+
else positionals.push(a);
|
|
153
|
+
}
|
|
154
|
+
// 命令解析:install / update,其余位置参数 = 技能 slug(可多个)
|
|
155
|
+
for (const p of positionals) {
|
|
156
|
+
if (p === 'install' || p === 'update') {
|
|
157
|
+
if (opts.command && opts.command !== p) die('命令冲突:' + opts.command + ' 与 ' + p);
|
|
158
|
+
opts.command = p;
|
|
159
|
+
} else {
|
|
160
|
+
opts.rest.push(p);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// 直接给 slug 且无命令 → 视为 install 单个/多个
|
|
164
|
+
if (!opts.command && opts.rest.length > 0) opts.command = 'install';
|
|
165
|
+
opts.skills = opts.rest.map(s => s.toLowerCase());
|
|
166
|
+
return opts;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function skillRange(s) {
|
|
170
|
+
const major = String(s.version).split('.')[0];
|
|
171
|
+
return major + '.x';
|
|
172
|
+
}
|
|
173
|
+
function specOf(s, pin) {
|
|
174
|
+
return s.pkg + '@' + (pin ? s.version : skillRange(s));
|
|
175
|
+
}
|
|
176
|
+
function findSkill(slug) {
|
|
177
|
+
return MANIFEST.find(s => s.slug === slug || s.pkg === slug || s.pkg.replace('@yottameta/', '') === slug);
|
|
178
|
+
}
|
|
179
|
+
function selectSkills(opts) {
|
|
180
|
+
if (opts.skills.length === 0) return MANIFEST;
|
|
181
|
+
const picked = [];
|
|
182
|
+
for (const slug of opts.skills) {
|
|
183
|
+
const s = findSkill(slug);
|
|
184
|
+
if (!s) die('未知技能: ' + slug + '(可用: yotta-skills --list)');
|
|
185
|
+
picked.push(s);
|
|
186
|
+
}
|
|
187
|
+
return picked;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function readInstalledVersion(dir) {
|
|
191
|
+
const f = path.join(dir, 'SKILL.md');
|
|
192
|
+
try {
|
|
193
|
+
const text = fs.readFileSync(f, 'utf8');
|
|
194
|
+
const m = text.match(/^version:\s*([0-9]+\.[0-9]+\.[0-9]+)/m);
|
|
195
|
+
return m ? m[1] : null;
|
|
196
|
+
} catch (_) { return null; }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function shouldSkip(name, isFile) {
|
|
200
|
+
if (name === '__pycache__' || name === '.pytest_cache' || name === '.mypy_cache') return true;
|
|
201
|
+
if (isFile && (name.endsWith('.pyc') || name.endsWith('.pyo'))) return true;
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
function copyDir(src, dst, skip) {
|
|
205
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
206
|
+
if (skip.has(entry.name) || shouldSkip(entry.name, entry.isFile())) continue;
|
|
207
|
+
const s = path.join(src, entry.name);
|
|
208
|
+
const d = path.join(dst, entry.name);
|
|
209
|
+
if (entry.isDirectory()) {
|
|
210
|
+
fs.mkdirSync(d, { recursive: true });
|
|
211
|
+
copyDir(s, d, skip);
|
|
212
|
+
} else if (entry.isFile()) {
|
|
213
|
+
fs.copyFileSync(s, d);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function resolveTargetDir(opts) {
|
|
219
|
+
if (opts.dir) return path.resolve(opts.dir);
|
|
220
|
+
if (opts.agent) {
|
|
221
|
+
const info = AGENT_DIRS[opts.agent];
|
|
222
|
+
if (!info) die('未收录智能体: ' + opts.agent + '。可用: ' + Object.keys(AGENT_DIRS).join(', ') + ';或改用 --dir <路径>。');
|
|
223
|
+
return resolveUserDir(info.dirs[0]);
|
|
224
|
+
}
|
|
225
|
+
return null; // 未指定目标(项目级检测由调用方处理)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function detectProjectDir() {
|
|
229
|
+
const PROJECT_DIRS = [
|
|
230
|
+
'.claude/skills', '.cursor/skills', '.codex/skills', '.config/goose/skills',
|
|
231
|
+
'.config/agents/skills', '.opencode/skills', '.codeium/windsurf/skills',
|
|
232
|
+
'.workbuddy/skills', '.kiro/skills', '.traecli/skills', '.gemini/skills',
|
|
233
|
+
'.trae-cn/skills', '.qwen/skills', '.comate/skills', '.codebuddy/skills',
|
|
234
|
+
'.kimi/skills', '.agents/skills',
|
|
235
|
+
];
|
|
236
|
+
for (const d of PROJECT_DIRS) if (fs.existsSync(d)) return path.resolve(d);
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── 元信 scan(若可用)─────────────────────────────────────────────────────
|
|
241
|
+
function findVerifyEngine(dest, opts) {
|
|
242
|
+
const cands = [];
|
|
243
|
+
if (opts.verify) cands.push(path.resolve(opts.verify));
|
|
244
|
+
if (process.env.YOTTA_SKILLS_VERIFY) cands.push(path.resolve(process.env.YOTTA_SKILLS_VERIFY));
|
|
245
|
+
if (dest) cands.push(path.join(dest, 'yotta-verify', 'scripts', 'yotta_verify.py'));
|
|
246
|
+
for (const c of cands) {
|
|
247
|
+
try { if (fs.statSync(c).isFile()) return c; } catch (_) { /* next */ }
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
function findPython(opts) {
|
|
252
|
+
const cands = [];
|
|
253
|
+
if (opts.python) cands.push(opts.python);
|
|
254
|
+
if (process.env.YOTTA_SKILLS_PYTHON) cands.push(process.env.YOTTA_SKILLS_PYTHON);
|
|
255
|
+
cands.push('python3', 'python');
|
|
256
|
+
if (process.platform === 'win32') cands.push('py');
|
|
257
|
+
for (const c of cands) {
|
|
258
|
+
try {
|
|
259
|
+
const r = spawnSync(c, ['--version'], { encoding: 'utf8', timeout: 10000 });
|
|
260
|
+
if (r.status === 0) return c;
|
|
261
|
+
} catch (_) { /* next */ }
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
function runScan(engine, skillDir) {
|
|
266
|
+
const python = findPython({});
|
|
267
|
+
if (!python) return { ok: false, note: '未找到 python(元信 scan 需要 Python 3.8+)' };
|
|
268
|
+
// -B:禁止 Python 写 __pycache__(否则会在引擎所在目录(可能是已装技能)留下 .pyc 污染)
|
|
269
|
+
const r = spawnSync(python, ['-B', engine, 'scan', skillDir, '--json'], { encoding: 'utf8', timeout: 60000 });
|
|
270
|
+
if (r.status === null) return { ok: false, note: '元信 scan 执行失败' };
|
|
271
|
+
try {
|
|
272
|
+
const j = JSON.parse(r.stdout);
|
|
273
|
+
const c = j.counts || {};
|
|
274
|
+
return { ok: true, verdict: j.verdict, counts: c };
|
|
275
|
+
} catch (_) {
|
|
276
|
+
return { ok: false, note: '元信 scan 输出无法解析' };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ── 安装 ───────────────────────────────────────────────────────────────────
|
|
281
|
+
function runNpmPack(skill, opts, packDir) {
|
|
282
|
+
const spec = specOf(skill, opts.pin);
|
|
283
|
+
const args = ['pack', spec, '--pack-destination', packDir];
|
|
284
|
+
const flags = (process.env.YOTTA_SKILLS_NPM_FLAGS || '').trim();
|
|
285
|
+
if (flags) args.push(...flags.split(/\s+/));
|
|
286
|
+
const npm = resolveNpm(opts);
|
|
287
|
+
const r = spawnSync(npm.bin, [...npm.prefix, ...args], { encoding: 'utf8', timeout: 180000, maxBuffer: 64 * 1024 * 1024, shell: npm.shell });
|
|
288
|
+
if (r.status !== 0) {
|
|
289
|
+
const raw = (r.stderr || r.stdout || 'npm pack 失败').trim();
|
|
290
|
+
const ll = raw.split(/\r?\n/).filter(Boolean);
|
|
291
|
+
const brief = ll.slice(-4).join(' | ');
|
|
292
|
+
return { error: brief || 'npm pack 失败', detail: raw };
|
|
293
|
+
}
|
|
294
|
+
const lines = (r.stdout || '').split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
295
|
+
let tarball = null;
|
|
296
|
+
for (const l of lines) if (/\.tgz$/.test(l)) tarball = l;
|
|
297
|
+
if (!tarball) {
|
|
298
|
+
try {
|
|
299
|
+
const found = fs.readdirSync(packDir).filter(f => f.endsWith('.tgz'));
|
|
300
|
+
if (found.length === 1) tarball = found[0];
|
|
301
|
+
} catch (_) { /* ignore */ }
|
|
302
|
+
}
|
|
303
|
+
if (!tarball) return { error: '未找到 npm pack 产物(' + spec + ')' };
|
|
304
|
+
const vm = String(tarball).match(/-([0-9]+\.[0-9]+\.[0-9]+)\.tgz$/);
|
|
305
|
+
return { tarball: path.join(packDir, tarball), resolved: vm ? vm[1] : null, spec };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function extractTarball(tarball, extractDir) {
|
|
309
|
+
const r = spawnSync(tarBin(), ['-xzf', tarball, '-C', extractDir], { encoding: 'utf8', timeout: 120000 });
|
|
310
|
+
if (r.status !== 0) return { error: (r.stderr || r.stdout || 'tar 解压失败').trim().split('\n').pop() };
|
|
311
|
+
const pkgDir = path.join(extractDir, 'package');
|
|
312
|
+
if (!fs.existsSync(path.join(pkgDir, 'SKILL.md'))) return { error: '解压产物缺少 SKILL.md(' + tarball + ')' };
|
|
313
|
+
return { pkgDir };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const COPY_SKIP = new Set(['package.json', 'bin', 'node_modules', '.git', '__pycache__']);
|
|
317
|
+
|
|
318
|
+
function installOne(skill, dest, opts) {
|
|
319
|
+
const target = path.join(dest, skill.slug);
|
|
320
|
+
const existing = readInstalledVersion(target);
|
|
321
|
+
if (!opts.force && existing === skill.version) {
|
|
322
|
+
return { skill, status: 'skip', version: existing, note: '已是最新' };
|
|
323
|
+
}
|
|
324
|
+
let tmp;
|
|
325
|
+
try {
|
|
326
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'yotta-skills-'));
|
|
327
|
+
const packDir = path.join(tmp, 'pack');
|
|
328
|
+
fs.mkdirSync(packDir, { recursive: true });
|
|
329
|
+
const packed = runNpmPack(skill, opts, packDir);
|
|
330
|
+
if (packed.error) {
|
|
331
|
+
if (packed.detail) out(' ' + packed.detail.split(/\r?\n/).filter(Boolean).slice(-12).join('\n '));
|
|
332
|
+
return { skill, status: 'fail', version: null, note: packed.error };
|
|
333
|
+
}
|
|
334
|
+
const extractDir = path.join(tmp, 'extract');
|
|
335
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
336
|
+
const extracted = extractTarball(packed.tarball, extractDir);
|
|
337
|
+
if (extracted.error) return { skill, status: 'fail', version: packed.resolved, note: extracted.error };
|
|
338
|
+
|
|
339
|
+
// 元信 scan(若可用):装前摘要,不拦截
|
|
340
|
+
if (!opts.skipScan) {
|
|
341
|
+
const engine = findVerifyEngine(dest, opts);
|
|
342
|
+
if (engine) {
|
|
343
|
+
const scan = runScan(engine, extracted.pkgDir);
|
|
344
|
+
if (scan.ok) {
|
|
345
|
+
const c = scan.counts;
|
|
346
|
+
const line = 'critical ' + (c.critical || 0) + ' / high ' + (c.high || 0) + ' / medium ' + (c.medium || 0) +
|
|
347
|
+
' / low ' + (c.low || 0) + ' / info ' + (c.info || 0);
|
|
348
|
+
out(' 元信 scan: ' + scan.verdict + '(' + line + ')');
|
|
349
|
+
if (scan.verdict && /DO NOT INSTALL/.test(scan.verdict)) out(' ⚠ 元信 verdict 为 DO NOT INSTALL,请人工复核后再使用。');
|
|
350
|
+
} else {
|
|
351
|
+
out(' 元信 scan: ' + scan.note);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
356
|
+
fs.mkdirSync(target, { recursive: true });
|
|
357
|
+
copyDir(extracted.pkgDir, target, COPY_SKIP);
|
|
358
|
+
return { skill, status: 'ok', version: packed.resolved || skill.version, note: null };
|
|
359
|
+
} catch (e) {
|
|
360
|
+
return { skill, status: 'fail', version: null, note: e.message };
|
|
361
|
+
} finally {
|
|
362
|
+
if (tmp) fs.rmSync(tmp, { recursive: true, force: true });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function runInstall(opts, dest) {
|
|
367
|
+
const skills = selectSkills(opts);
|
|
368
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— 安装 ' + skills.length + ' 个技能 -> ' + dest);
|
|
369
|
+
out('版本策略: ' + (opts.pin ? 'pin(锁死清单精确版本)' : 'range(' + skillRange(skills[0]) + ',跟随最新 patch;--pin 锁死)'));
|
|
370
|
+
const results = [];
|
|
371
|
+
let failed = 0;
|
|
372
|
+
for (const s of skills) {
|
|
373
|
+
const r = installOne(s, dest, opts);
|
|
374
|
+
results.push(r);
|
|
375
|
+
if (r.status === 'ok') out(' ✔ ' + s.slug.padEnd(22) + s.name + ' -> ' + (r.version || '?'));
|
|
376
|
+
else if (r.status === 'skip') out(' - ' + s.slug.padEnd(22) + s.name + ' (' + r.note + ',v' + r.version + ')');
|
|
377
|
+
else { failed++; out(' ✘ ' + s.slug.padEnd(22) + s.name + ' 失败: ' + r.note); }
|
|
378
|
+
}
|
|
379
|
+
const ok = results.filter(r => r.status === 'ok').length;
|
|
380
|
+
const skip = results.filter(r => r.status === 'skip').length;
|
|
381
|
+
out('');
|
|
382
|
+
out('汇总: 成功 ' + ok + ' / 跳过(已是最新) ' + skip + ' / 失败 ' + failed + '(共 ' + results.length + ')');
|
|
383
|
+
if (failed > 0) process.exitCode = 1;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function runUpdate(opts, dest) {
|
|
387
|
+
const skills = selectSkills(opts);
|
|
388
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— 增量更新 -> ' + dest);
|
|
389
|
+
const results = [];
|
|
390
|
+
let failed = 0;
|
|
391
|
+
for (const s of skills) {
|
|
392
|
+
const existing = readInstalledVersion(path.join(dest, s.slug));
|
|
393
|
+
if (existing === s.version) {
|
|
394
|
+
out(' - ' + s.slug.padEnd(22) + s.name + ' (已是最新,v' + existing + ')');
|
|
395
|
+
results.push({ skill: s, status: 'skip', version: existing });
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
const r = installOne(s, dest, opts);
|
|
399
|
+
results.push(r);
|
|
400
|
+
if (r.status === 'ok') out(' ✔ ' + s.slug.padEnd(22) + s.name + ' -> ' + (r.version || '?') + (existing ? '(原 v' + existing + ')' : '(新装)'));
|
|
401
|
+
else if (r.status === 'skip') out(' - ' + s.slug.padEnd(22) + s.name + ' (' + r.note + ')');
|
|
402
|
+
else { failed++; out(' ✘ ' + s.slug.padEnd(22) + s.name + ' 失败: ' + r.note); }
|
|
403
|
+
}
|
|
404
|
+
const ok = results.filter(r => r.status === 'ok').length;
|
|
405
|
+
const skip = results.filter(r => r.status === 'skip').length;
|
|
406
|
+
out('');
|
|
407
|
+
out('汇总: 更新 ' + ok + ' / 已是最新 ' + skip + ' / 失败 ' + failed);
|
|
408
|
+
if (failed > 0) process.exitCode = 1;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ── 展示 ───────────────────────────────────────────────────────────────────
|
|
412
|
+
function printList(opts) {
|
|
413
|
+
const skills = opts.skills.length ? selectSkills(opts) : MANIFEST;
|
|
414
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— 全家技能清单(' + skills.length + ' 个)');
|
|
415
|
+
out('版本策略: ' + (opts.pin ? 'pin(精确锁定)' : 'range(' + skillRange(skills[0]) + ' 起,跟随最新 patch;--pin 锁死)'));
|
|
416
|
+
out('');
|
|
417
|
+
for (const s of skills) {
|
|
418
|
+
out(' ' + s.slug.padEnd(22) + s.name.padEnd(5) + ' ' + specOf(s, opts.pin).padEnd(52) + ' ' + s.version + ' ' + s.desc);
|
|
419
|
+
}
|
|
420
|
+
out('');
|
|
421
|
+
out('安装: yotta-skills install --agent <name> 或 --dir <path>;预览: --dry-run;更新: update。');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function printHelp() {
|
|
425
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— npx 一次装齐全家技能');
|
|
426
|
+
out('');
|
|
427
|
+
out('用法:');
|
|
428
|
+
out(' yotta-skills --list 列出全家技能 + 版本 + 说明');
|
|
429
|
+
out(' yotta-skills install --agent <name> 装全家到智能体默认用户级目录(推荐)');
|
|
430
|
+
out(' yotta-skills install --dir <path> 装全家到指定目录');
|
|
431
|
+
out(' yotta-skills install <skill> --dir <path> 装单个技能(可多个)');
|
|
432
|
+
out(' yotta-skills update --agent <name> 增量更新已装技能(补齐缺失 / 版本不一致)');
|
|
433
|
+
out(' yotta-skills --dry-run 预览将安装清单(不联网、不改动)');
|
|
434
|
+
out('');
|
|
435
|
+
out('选项:');
|
|
436
|
+
out(' --agent <name> 智能体键名(--list 可查看;未知智能体请用 --dir)');
|
|
437
|
+
out(' --dir <path> 目标技能目录(技能会装到 <path>/<slug>)');
|
|
438
|
+
out(' --pin 锁死清单精确版本(默认 range:跟随同 major 最新 patch)');
|
|
439
|
+
out(' --force 已是最新也重装');
|
|
440
|
+
out(' --skip-scan 跳过元信装前 scan(装了 yotta-verify 自动启用)');
|
|
441
|
+
out(' --npm <path> 指定 npm 可执行文件(默认 npm / npm.cmd)');
|
|
442
|
+
out(' --python <path> 指定 python 可执行文件(元信 scan 用)');
|
|
443
|
+
out(' --verify <path> 指定 yotta_verify.py 路径(默认找目标目录已装的元信)');
|
|
444
|
+
out(' -h, --help 帮助');
|
|
445
|
+
out(' -v, --version 版本');
|
|
446
|
+
out('');
|
|
447
|
+
out('支持智能体: ' + Object.keys(AGENT_DIRS).join(', '));
|
|
448
|
+
out('依赖: Node.js 18+ / npm / 系统 tar;环境变量 YOTTA_SKILLS_NPM / YOTTA_SKILLS_PYTHON / YOTTA_SKILLS_VERIFY / YOTTA_SKILLS_NPM_FLAGS 可覆盖。');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ── main ───────────────────────────────────────────────────────────────────
|
|
452
|
+
function main() {
|
|
453
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
454
|
+
if (opts.help) { printHelp(); return; }
|
|
455
|
+
if (opts.version) { out('yotta-skills v' + VERSION); return; }
|
|
456
|
+
if (opts.list) { printList(opts); return; }
|
|
457
|
+
|
|
458
|
+
const command = opts.command || 'install';
|
|
459
|
+
let dest = resolveTargetDir(opts);
|
|
460
|
+
if (!dest && !opts.dryRun) dest = detectProjectDir();
|
|
461
|
+
if (!dest && command === 'install' && !opts.dryRun) {
|
|
462
|
+
die('未指定目标:请用 --agent <name> 或 --dir <path>(当前目录未检测到项目级技能目录)。', 4);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (opts.dryRun) {
|
|
466
|
+
const skills = selectSkills(opts);
|
|
467
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— dry-run(' + command + ',' + skills.length + ' 个技能)');
|
|
468
|
+
out('目标: ' + (dest || '未指定(将检测项目级目录)'));
|
|
469
|
+
out('版本策略: ' + (opts.pin ? 'pin(锁死)' : 'range(' + skillRange(skills[0]) + ')'));
|
|
470
|
+
out('');
|
|
471
|
+
for (const s of skills) {
|
|
472
|
+
const tag = command === 'update' ? '将检查/更新' : '将安装';
|
|
473
|
+
out(' [' + tag + '] ' + s.slug.padEnd(22) + s.name.padEnd(5) + ' ' + specOf(s, opts.pin).padEnd(52) + ' ' + s.version + ' ' + s.desc);
|
|
474
|
+
}
|
|
475
|
+
out('');
|
|
476
|
+
out('(dry-run 未执行任何下载/写入)');
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (command === 'update') runUpdate(opts, dest);
|
|
481
|
+
else runInstall(opts, dest);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
main();
|
package/install.sh
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# yotta-skills(元阁)多智能体安装脚本(YottaSkills)
|
|
3
|
+
# 用法:
|
|
4
|
+
# bash install.sh --agent <name> # 按智能体默认用户级目录安装
|
|
5
|
+
# bash install.sh --dir <path> # 装到指定目录(用户改过目录的智能体)
|
|
6
|
+
# bash install.sh -g # 装到全部已知智能体用户级目录
|
|
7
|
+
# bash install.sh # 检测并安装到已存在的项目级目录
|
|
8
|
+
# bash install.sh --list # 列出智能体 -> 默认目录
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
SKILL_NAME="yotta-skills"
|
|
12
|
+
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
case "$(uname -s)" in
|
|
14
|
+
MINGW*|MSYS*)
|
|
15
|
+
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -W)"
|
|
16
|
+
;;
|
|
17
|
+
esac
|
|
18
|
+
|
|
19
|
+
# 智能体 -> 用户级默认目录(--agent 装到第一个)
|
|
20
|
+
# .agents/skills 并非通用目录:OpenCode / Cursor / Cline / Amp / Kimi / Gemini CLI / GitHub Copilot 读取。
|
|
21
|
+
# 判断当前环境:Windows Git Bash 用 %USERPROFILE%,Unix 用 ~
|
|
22
|
+
_IS_WINDOWS=0
|
|
23
|
+
case "$(uname -s)" in
|
|
24
|
+
MINGW*|MSYS*|CYGWIN*) _IS_WINDOWS=1 ;;
|
|
25
|
+
esac
|
|
26
|
+
dirs_for() {
|
|
27
|
+
case "$1" in
|
|
28
|
+
claude) echo ".claude/skills" ;;
|
|
29
|
+
cursor) echo ".cursor/skills .agents/skills" ;;
|
|
30
|
+
codex) echo "__CODEX__" ;;
|
|
31
|
+
gemini) echo ".gemini/skills .agents/skills" ;;
|
|
32
|
+
goose) echo ".config/goose/skills .agents/skills" ;;
|
|
33
|
+
amp) echo ".config/agents/skills .agents/skills" ;;
|
|
34
|
+
opencode) echo "__OPENCODE__" ;;
|
|
35
|
+
windsurf) echo ".codeium/windsurf/skills" ;;
|
|
36
|
+
workbuddy) echo ".workbuddy/skills" ;;
|
|
37
|
+
kiro) echo ".kiro/skills" ;;
|
|
38
|
+
trae) echo ".traecli/skills" ;;
|
|
39
|
+
trae-cn) echo ".trae-cn/skills" ;;
|
|
40
|
+
qwen) echo ".qwen/skills" ;;
|
|
41
|
+
comate) echo ".comate/skills" ;;
|
|
42
|
+
codebuddy) echo ".codebuddy/skills" ;;
|
|
43
|
+
kimi) echo ".kimi/skills" ;;
|
|
44
|
+
agents) echo ".agents/skills" ;;
|
|
45
|
+
*) return 1 ;;
|
|
46
|
+
esac
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
codex_dir() {
|
|
50
|
+
if [ -n "${CODEX_HOME:-}" ]; then printf '%s' "$CODEX_HOME/skills"; else printf '%s' "$HOME/.codex/skills"; fi
|
|
51
|
+
}
|
|
52
|
+
opencode_dir() {
|
|
53
|
+
if [ -n "${XDG_CONFIG_HOME:-}" ]; then printf '%s' "$XDG_CONFIG_HOME/opencode/skills"; else printf '%s' "$HOME/.config/opencode/skills"; fi
|
|
54
|
+
}
|
|
55
|
+
resolve_user() {
|
|
56
|
+
case "$1" in
|
|
57
|
+
__CODEX__) codex_dir ;;
|
|
58
|
+
__OPENCODE__) opencode_dir ;;
|
|
59
|
+
*) printf '%s' "$HOME/$1" ;;
|
|
60
|
+
esac
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
install_to() {
|
|
64
|
+
mkdir -p "$1/$SKILL_NAME"
|
|
65
|
+
cp -r "$SOURCE_DIR/." "$1/$SKILL_NAME/"
|
|
66
|
+
rm -rf "$1/$SKILL_NAME/.git"
|
|
67
|
+
echo "installed -> $1/$SKILL_NAME"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
list() {
|
|
71
|
+
echo "智能体 -> 默认技能目录(--agent <name> 装到第一个,用户级):"
|
|
72
|
+
for a in claude cursor codex gemini goose amp opencode windsurf workbuddy kiro trae trae-cn qwen comate codebuddy kimi agents; do
|
|
73
|
+
local dirs first
|
|
74
|
+
dirs="$(dirs_for "$a")"
|
|
75
|
+
first="${dirs%% *}"
|
|
76
|
+
case "$first" in
|
|
77
|
+
__CODEX__) first=".codex/skills" ;;
|
|
78
|
+
__OPENCODE__) first=".config/opencode/skills" ;;
|
|
79
|
+
esac
|
|
80
|
+
if [ "$_IS_WINDOWS" = "1" ]; then
|
|
81
|
+
first="%USERPROFILE%\\${first//\//\\}"
|
|
82
|
+
else
|
|
83
|
+
first="~/$first"
|
|
84
|
+
fi
|
|
85
|
+
printf ' %-10s %s\n' "$a" "$first"
|
|
86
|
+
done
|
|
87
|
+
echo '说明:Windows 用 %USERPROFILE%,Linux/macOS 用 ~;仅收录有官方默认目录的智能体。'
|
|
88
|
+
echo '改了目录的请用 --dir <路径>,不要依赖默认位置;若设置了 CODEX_HOME / XDG_CONFIG_HOME,安装自动以该变量为准。'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
main() {
|
|
92
|
+
local agent="" dir="" global=0 show_list=0
|
|
93
|
+
while [ $# -gt 0 ]; do
|
|
94
|
+
case "$1" in
|
|
95
|
+
--agent) shift; agent="${1:-}" ;;
|
|
96
|
+
--dir) shift; dir="${1:-}" ;;
|
|
97
|
+
-g|--global) global=1 ;;
|
|
98
|
+
--list|-l) show_list=1 ;;
|
|
99
|
+
*) echo "未知参数: $1" >&2; exit 2 ;;
|
|
100
|
+
esac
|
|
101
|
+
shift
|
|
102
|
+
done
|
|
103
|
+
|
|
104
|
+
if [ "$show_list" = "1" ]; then list; return; fi
|
|
105
|
+
if [ -n "$dir" ]; then install_to "$dir"; echo "完成。"; return; fi
|
|
106
|
+
if [ -n "$agent" ]; then
|
|
107
|
+
local dirs first
|
|
108
|
+
if ! dirs="$(dirs_for "$agent")"; then
|
|
109
|
+
echo "未收录智能体: $agent。请用 --dir <路径> 指定技能目录。" >&2; exit 2
|
|
110
|
+
fi
|
|
111
|
+
first="${dirs%% *}"
|
|
112
|
+
install_to "$(resolve_user "$first")"; echo "完成。"; return
|
|
113
|
+
fi
|
|
114
|
+
if [ "$global" = "1" ]; then
|
|
115
|
+
echo "安装到全部已知智能体用户级目录..."
|
|
116
|
+
local dirs rel
|
|
117
|
+
for a in claude cursor codex gemini goose amp opencode windsurf workbuddy kiro trae trae-cn qwen comate codebuddy kimi agents; do
|
|
118
|
+
dirs="$(dirs_for "$a")"
|
|
119
|
+
for rel in $dirs; do install_to "$(resolve_user "$rel")"; done
|
|
120
|
+
done
|
|
121
|
+
echo "完成。"; return
|
|
122
|
+
fi
|
|
123
|
+
local installed=0 d
|
|
124
|
+
for d in .claude/skills .cursor/skills .codex/skills .config/goose/skills .config/agents/skills .opencode/skills .codeium/windsurf/skills .workbuddy/skills .kiro/skills .traecli/skills .gemini/skills .trae-cn/skills .qwen/skills .comate/skills .codebuddy/skills .kimi/skills .agents/skills; do
|
|
125
|
+
if [ -d "$d" ]; then install_to "$d"; installed=1; fi
|
|
126
|
+
done
|
|
127
|
+
if [ "$installed" = "0" ]; then
|
|
128
|
+
echo "未检测到项目级智能体目录。可用 --agent <name> / -g 装到用户级,或 --dir 指定。"
|
|
129
|
+
fi
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
main "$@"
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yottameta/yotta-skills",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "YuanGe (元阁) — one-command installer for the whole YottaMeta skill family: list, install, and update all published yotta-* skills into any agent (npx one-shot, zero dependencies, Node.js only).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"agent-skills",
|
|
8
|
+
"yotta-skills",
|
|
9
|
+
"yottaskills",
|
|
10
|
+
"installer",
|
|
11
|
+
"bundle",
|
|
12
|
+
"skill-family"
|
|
13
|
+
],
|
|
14
|
+
"files": [
|
|
15
|
+
"SKILL.md",
|
|
16
|
+
"LICENSE",
|
|
17
|
+
"NOTICE",
|
|
18
|
+
"README.md",
|
|
19
|
+
"README.zh-CN.md",
|
|
20
|
+
"CHANGELOG.md",
|
|
21
|
+
"install.sh",
|
|
22
|
+
"references",
|
|
23
|
+
"assets",
|
|
24
|
+
"bin",
|
|
25
|
+
"skills.json",
|
|
26
|
+
"!**/__pycache__",
|
|
27
|
+
"!**/*.pyc"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/YottaMeta/yotta-skills.git"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"bin": {
|
|
37
|
+
"yotta-skills": "bin/yotta-skills.js"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "node --test \"**/*.test.js\""
|
|
44
|
+
}
|
|
45
|
+
}
|