@yottameta/yotta-skills 0.24.2 → 0.25.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 +9 -0
- package/README.md +20 -0
- package/README.zh-CN.md +20 -0
- package/SKILL.md +43 -2
- package/bin/yotta-skills.js +415 -29
- package/lib/agent-dirs.js +201 -0
- package/lib/agent-discovery.js +302 -0
- package/lib/hub-adopt.js +268 -0
- package/lib/hub.js +583 -0
- package/lib/skills-cli-lock.js +52 -0
- package/lib/skills-scan.js +28 -38
- package/package.json +1 -1
- package/references/hub.md +66 -0
- package/scripts/yotta-skills-mcp.py +1 -1
- package/skill-manifest.json +1 -1
package/bin/yotta-skills.js
CHANGED
|
@@ -45,11 +45,15 @@ const routeDynamicLib = require('../lib/route-dynamic');
|
|
|
45
45
|
const depsLib = require('../lib/deps');
|
|
46
46
|
const untarLib = require('../lib/untar');
|
|
47
47
|
const registryFetchLib = require('../lib/registry-fetch');
|
|
48
|
+
const agentDirsLib = require('../lib/agent-dirs');
|
|
49
|
+
const agentDiscoveryLib = require('../lib/agent-discovery');
|
|
50
|
+
const hubLib = require('../lib/hub');
|
|
51
|
+
const hubAdoptLib = require('../lib/hub-adopt');
|
|
48
52
|
const { createInstaller, isSafeTarEntry } = require('../lib/install-pipeline');
|
|
49
53
|
const { COPY_SKIP, copyDir } = require('../lib/copy-tree');
|
|
50
54
|
|
|
51
55
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
52
|
-
let VERSION = '0.
|
|
56
|
+
let VERSION = '0.25.0';
|
|
53
57
|
try { VERSION = require(path.join(PKG_ROOT, 'package.json')).version; } catch (_) { /* keep fallback */ }
|
|
54
58
|
|
|
55
59
|
function loadManifest() {
|
|
@@ -72,39 +76,18 @@ function loadManifest() {
|
|
|
72
76
|
|
|
73
77
|
const MANIFEST = loadManifest();
|
|
74
78
|
|
|
75
|
-
// 智能体 ->
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
cursor: { label: 'Cursor', dirs: ['.cursor/skills', '.agents/skills'] },
|
|
79
|
-
codex: { label: 'Codex', dirs: ['.codex/skills'] }, // 特判:$CODEX_HOME/skills
|
|
80
|
-
gemini: { label: 'Gemini CLI', dirs: ['.gemini/skills', '.agents/skills'] },
|
|
81
|
-
goose: { label: 'Goose', dirs: ['.config/goose/skills', '.agents/skills'] },
|
|
82
|
-
amp: { label: 'Amp', dirs: ['.config/agents/skills', '.agents/skills'] },
|
|
83
|
-
opencode: { label: 'OpenCode', dirs: ['.config/opencode/skills'] }, // 特判:$XDG_CONFIG_HOME
|
|
84
|
-
windsurf: { label: 'Windsurf', dirs: ['.codeium/windsurf/skills'] },
|
|
85
|
-
workbuddy: { label: 'WorkBuddy', dirs: ['.workbuddy/skills'] },
|
|
86
|
-
kiro: { label: 'Kiro', dirs: ['.kiro/skills'] },
|
|
87
|
-
trae: { label: 'Trae Code CLI', dirs: ['.traecli/skills'] },
|
|
88
|
-
'trae-cn': { label: 'Trae IDE(国内)', dirs: ['.trae-cn/skills'] },
|
|
89
|
-
qwen: { label: 'Qwen Code', dirs: ['.qwen/skills'] },
|
|
90
|
-
comate: { label: 'Comate 文心快码', dirs: ['.comate/skills'] },
|
|
91
|
-
codebuddy: { label: 'CodeBuddy Code', dirs: ['.codebuddy/skills'] },
|
|
92
|
-
kimi: { label: 'Kimi Code CLI', dirs: ['.kimi/skills'] },
|
|
93
|
-
agents: { label: '通用 AGENTS.md', dirs: ['.agents/skills'] },
|
|
94
|
-
};
|
|
79
|
+
// 智能体 -> 用户级默认技能目录。单一真源在 lib/agent-dirs.js
|
|
80
|
+
// (兼容 Vercel Labs `skills` CLI / agentskills.io 的宿主表,MIT)。
|
|
81
|
+
const AGENT_DIRS = agentDirsLib.AGENT_DIRS;
|
|
95
82
|
|
|
96
83
|
function codexUserDir() {
|
|
97
|
-
|
|
98
|
-
return path.join(base, 'skills');
|
|
84
|
+
return agentDirsLib.resolveUserDir('.codex/skills', { homeDir: os.homedir(), env: process.env });
|
|
99
85
|
}
|
|
100
86
|
function opencodeUserDir() {
|
|
101
|
-
|
|
102
|
-
return path.join(base, 'opencode', 'skills');
|
|
87
|
+
return agentDirsLib.resolveUserDir('.config/opencode/skills', { homeDir: os.homedir(), env: process.env });
|
|
103
88
|
}
|
|
104
89
|
function resolveUserDir(rel) {
|
|
105
|
-
|
|
106
|
-
if (rel === '.config/opencode/skills') return opencodeUserDir();
|
|
107
|
-
return path.join(os.homedir(), rel);
|
|
90
|
+
return agentDirsLib.resolveUserDir(rel, { homeDir: os.homedir(), env: process.env });
|
|
108
91
|
}
|
|
109
92
|
|
|
110
93
|
// ── 工具函数 ───────────────────────────────────────────────────────────────
|
|
@@ -170,6 +153,9 @@ function parseArgs(argv) {
|
|
|
170
153
|
host: null, event: null, manifest: null, context: null,
|
|
171
154
|
signal: null, yes: false, explain: false, promote: false,
|
|
172
155
|
only: [], domain: null, installedOnly: false,
|
|
156
|
+
hub: null, all: false, inPlace: false, allowUnverified: false,
|
|
157
|
+
from: null, include: [], as: null, skillsCli: null,
|
|
158
|
+
scan: false, apply: false,
|
|
173
159
|
};
|
|
174
160
|
const positionals = [];
|
|
175
161
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -219,6 +205,22 @@ function parseArgs(argv) {
|
|
|
219
205
|
}
|
|
220
206
|
else if (a === '--domain') opts.domain = take('--domain').toLowerCase();
|
|
221
207
|
else if (a === '--installed-only') opts.installedOnly = true;
|
|
208
|
+
else if (a === '--hub') opts.hub = take('--hub');
|
|
209
|
+
else if (a === '--all') opts.all = true;
|
|
210
|
+
else if (a === '--in-place') opts.inPlace = true;
|
|
211
|
+
else if (a === '--scan') opts.scan = true;
|
|
212
|
+
else if (a === '--apply') opts.apply = true;
|
|
213
|
+
else if (a === '--allow-unverified') opts.allowUnverified = true;
|
|
214
|
+
else if (a === '--from') opts.from = take('--from');
|
|
215
|
+
else if (a === '--include') {
|
|
216
|
+
const value = take('--include');
|
|
217
|
+
for (const item of value.split(',')) {
|
|
218
|
+
const slug = item.trim().toLowerCase();
|
|
219
|
+
if (slug) opts.include.push(slug);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else if (a === '--as') opts.as = take('--as').toLowerCase();
|
|
223
|
+
else if (a === '--skills-cli') opts.skillsCli = take('--skills-cli');
|
|
222
224
|
else if (a === '--host') opts.host = take('--host').toLowerCase();
|
|
223
225
|
else if (a === '--event') opts.event = take('--event').toLowerCase();
|
|
224
226
|
else if (a === '--manifest') opts.manifest = take('--manifest');
|
|
@@ -233,7 +235,12 @@ function parseArgs(argv) {
|
|
|
233
235
|
}
|
|
234
236
|
// 命令解析:install / update / doctor / rollback,其余位置参数 = 技能 slug(可多个)
|
|
235
237
|
for (const p of positionals) {
|
|
238
|
+
if (opts.command === 'hub') {
|
|
239
|
+
opts.rest.push(p);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
236
242
|
if (p === 'install' || p === 'update' || p === 'doctor' || p === 'rollback' || p === 'hook'
|
|
243
|
+
|| p === 'hub'
|
|
237
244
|
|| p === 'usage' || p === 'decide-memory') {
|
|
238
245
|
if (opts.command && opts.command !== p) die('命令冲突:' + opts.command + ' 与 ' + p);
|
|
239
246
|
opts.command = p;
|
|
@@ -244,12 +251,21 @@ function parseArgs(argv) {
|
|
|
244
251
|
// 直接给 slug 且无命令 → 视为 install 单个/多个
|
|
245
252
|
if (!opts.command && opts.rest.length > 0) opts.command = 'install';
|
|
246
253
|
opts.skills = opts.rest.map(s => s.toLowerCase());
|
|
254
|
+
if (opts.command === 'hub') {
|
|
255
|
+
opts.hubAction = opts.skills[0] || 'status';
|
|
256
|
+
opts.skills = opts.skills.slice(1);
|
|
257
|
+
}
|
|
247
258
|
if (opts.scheduled && (opts.command !== 'update' || !opts.check || opts.auto)) {
|
|
248
259
|
die('--scheduled 只能与 update --check 一起使用', 2, '请使用 update --check --scheduled;自动更新不使用后台调度入口。');
|
|
249
260
|
}
|
|
250
261
|
if (opts.installedOnly && opts.command !== 'update') {
|
|
251
262
|
die('--installed-only 只能与 update 一起使用', 2, '标准配方:yotta-skills update --installed-only --dir <技能目录>。');
|
|
252
263
|
}
|
|
264
|
+
if ((opts.all || opts.inPlace || opts.allowUnverified || opts.from || opts.include.length || opts.as || opts.scan || opts.apply)
|
|
265
|
+
&& opts.command !== 'hub') {
|
|
266
|
+
die('--all / --in-place / --allow-unverified / --from / --include / --as / --scan / --apply 只能与 hub 命令一起使用', 2,
|
|
267
|
+
'Hub 用法:yotta-skills hub link --all 或 yotta-skills hub adopt --scan。');
|
|
268
|
+
}
|
|
253
269
|
if (opts.domain && !domainValues().includes(opts.domain)) {
|
|
254
270
|
die('未知 domain: ' + opts.domain + '。可用: ' + domainValues().join(', '), 2, 'domain 对齐家族索引 9 类;用 yotta-skills --list 查看技能清单。');
|
|
255
271
|
}
|
|
@@ -1306,6 +1322,16 @@ function printHelp() {
|
|
|
1306
1322
|
out(' yotta-skills hook evaluate --event <event> --manifest <file> 评估 hook 声明并留证');
|
|
1307
1323
|
out(' yotta-skills hook bind --manifest <file> 注册 hook 声明(幂等)');
|
|
1308
1324
|
out(' yotta-skills hook unbind <binding-id> 反注册 hook 声明');
|
|
1325
|
+
out(' yotta-skills hub hosts 发现本机已装智能体与技能目录(不读元忆)');
|
|
1326
|
+
out(' yotta-skills hub install [skill...] 把元技能装进本机 Hub(单点真源)');
|
|
1327
|
+
out(' yotta-skills hub adopt --scan 只读预演:扫描各宿主现有技能,输出收编候选与冲突');
|
|
1328
|
+
out(' yotta-skills hub adopt --apply 把选中技能复制进 Hub(默认保留原目录;--in-place 原地登记)');
|
|
1329
|
+
out(' yotta-skills hub refresh <slug> --from <path> 手动同步非元阁技能(无统一更新源)');
|
|
1330
|
+
out(' yotta-skills hub link --all 把 Hub 技能链接到全部已发现宿主(junction / symlink)');
|
|
1331
|
+
out(' yotta-skills hub link --agent <id> 只链接到一个宿主(未知宿主用 --dir)');
|
|
1332
|
+
out(' yotta-skills hub unlink --all 只删除链接,不动 Hub 真源(fail-closed)');
|
|
1333
|
+
out(' yotta-skills hub status 查看 Hub 技能 / 来源 / 链接 / 宿主');
|
|
1334
|
+
out(' yotta-skills hub doctor 检查断链 / 目标缺失 / slug 不一致 / 目录权限');
|
|
1309
1335
|
out('');
|
|
1310
1336
|
out('选项:');
|
|
1311
1337
|
out(' --agent <name> 智能体键名(--list 可查看;未知智能体请用 --dir)');
|
|
@@ -1336,12 +1362,18 @@ function printHelp() {
|
|
|
1336
1362
|
out(' --yes usage reset 确认清空');
|
|
1337
1363
|
out(' --explain decide-memory 文本报告追加信号明细');
|
|
1338
1364
|
out(' --promote decide-memory 只写本地建议文件(不写元忆)');
|
|
1365
|
+
out(' --hub <path> Hub 目录(默认 ~/.yottaskills/hub;YOTTA_SKILLS_HUB 覆盖)');
|
|
1366
|
+
out(' --all hub link / unlink 时作用于全部已发现宿主');
|
|
1367
|
+
out(' --scan hub adopt 只读预演(默认行为)');
|
|
1368
|
+
out(' --apply hub adopt 执行收编');
|
|
1369
|
+
out(' --in-place hub adopt 原地登记,不复制(原目录删除即断链)');
|
|
1370
|
+
out(' --allow-unverified 显式允许未扫描导入(默认 high / critical 阻断)');
|
|
1339
1371
|
out(' --project inventory / reindex 时附加扫描当前项目 .agents/skills / .codex/skills');
|
|
1340
1372
|
out(' --no-reindex 安装 / 更新后不自动重扫注册表');
|
|
1341
1373
|
out(' -h, --help 帮助');
|
|
1342
1374
|
out(' -v, --version 版本');
|
|
1343
1375
|
out('');
|
|
1344
|
-
out('支持智能体: ' + Object.keys(AGENT_DIRS).
|
|
1376
|
+
out('支持智能体: ' + Object.keys(AGENT_DIRS).length + ' 个已收录映射(本机实际发现用 yotta-skills hub hosts)');
|
|
1345
1377
|
out('依赖: Node.js 18+(必需);npm 与系统 tar 为回退通道(内置拉包 / 内置解包为主);元信 scan 需要 Python 3.8+(--python / YOTTA_SKILLS_PYTHON 可指向宿主自带 Python)。');
|
|
1346
1378
|
out('环境变量: YOTTA_SKILLS_FETCH(builtin|npm)/ YOTTA_SKILLS_EXTRACT(builtin|tar)/ YOTTA_SKILLS_NPM / YOTTA_SKILLS_PYTHON / YOTTA_SKILLS_VERIFY / YOTTA_SKILLS_NPM_FLAGS / YOTTA_SKILLS_REGISTRY / YOTTA_SKILLS_REGISTRY_FILE / YOTTA_SKILLS_MANIFEST 可覆盖。');
|
|
1347
1379
|
}
|
|
@@ -1800,6 +1832,356 @@ function runHook(opts) {
|
|
|
1800
1832
|
die('未知 hook 子命令: ' + action, 2, '支持 capabilities / evaluate / bind / unbind。');
|
|
1801
1833
|
}
|
|
1802
1834
|
|
|
1835
|
+
// ── Hub(本机单点安装 + 链接分发) ─────────────────────────────────────────
|
|
1836
|
+
function universalSkillDirs() {
|
|
1837
|
+
const home = os.homedir();
|
|
1838
|
+
const xdg = process.env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
1839
|
+
return [
|
|
1840
|
+
{ dir: path.join(xdg, 'agents', 'skills'), agentId: 'universal', label: 'Universal .agents' },
|
|
1841
|
+
{ dir: path.join(home, '.agents', 'skills'), agentId: 'agents', label: '通用 AGENTS.md' },
|
|
1842
|
+
];
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
function hubTargetDirs(opts, discovery) {
|
|
1846
|
+
const targets = [];
|
|
1847
|
+
const seen = new Set();
|
|
1848
|
+
const add = (dir, agentId, label) => {
|
|
1849
|
+
if (!dir) return;
|
|
1850
|
+
const resolved = path.resolve(dir);
|
|
1851
|
+
const key = process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
1852
|
+
if (seen.has(key)) return;
|
|
1853
|
+
seen.add(key);
|
|
1854
|
+
targets.push({ dir: resolved, agentId: agentId || null, label: label || '指定目录' });
|
|
1855
|
+
};
|
|
1856
|
+
|
|
1857
|
+
if (opts.dir) {
|
|
1858
|
+
add(opts.dir, null, '指定目录');
|
|
1859
|
+
return targets;
|
|
1860
|
+
}
|
|
1861
|
+
if (opts.agent) {
|
|
1862
|
+
const info = AGENT_DIRS[opts.agent];
|
|
1863
|
+
if (!info) {
|
|
1864
|
+
die('未收录智能体: ' + opts.agent + '。可用: ' + Object.keys(AGENT_DIRS).join(', ') + ';或改用 --dir <路径>。',
|
|
1865
|
+
2, '先运行 yotta-skills hub hosts 查看本机实际技能目录。');
|
|
1866
|
+
}
|
|
1867
|
+
add(resolveUserDir(info.dirs[0]), opts.agent, info.label);
|
|
1868
|
+
return targets;
|
|
1869
|
+
}
|
|
1870
|
+
if (!opts.all) {
|
|
1871
|
+
die('hub link / unlink 需要 --agent <id>、--dir <path> 或 --all', 2,
|
|
1872
|
+
'示例:yotta-skills hub link --all;或 yotta-skills hub link --agent codex。');
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
for (const item of universalSkillDirs()) add(item.dir, item.agentId, item.label);
|
|
1876
|
+
for (const host of discovery.hosts) {
|
|
1877
|
+
if (!host.exists) continue;
|
|
1878
|
+
add(host.dir, host.agentId, host.label);
|
|
1879
|
+
}
|
|
1880
|
+
return targets;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
function hubHostLines(discovery) {
|
|
1884
|
+
const lines = [];
|
|
1885
|
+
const existing = discovery.hosts.filter((host) => host.exists);
|
|
1886
|
+
const missing = discovery.hosts.filter((host) => !host.exists);
|
|
1887
|
+
for (const host of existing) {
|
|
1888
|
+
lines.push(' ' + (host.known ? '✔' : '?') + ' ' +
|
|
1889
|
+
String(host.label).padEnd(28) + host.dir.padEnd(64) +
|
|
1890
|
+
String(host.skillCount).padStart(3) + ' 个技能' +
|
|
1891
|
+
(host.known ? ' [已收录]' : ' [自动发现]'));
|
|
1892
|
+
}
|
|
1893
|
+
const noSkillDir = discovery.installed.filter((item) => !item.hasSkillsDir);
|
|
1894
|
+
for (const item of noSkillDir) {
|
|
1895
|
+
lines.push(' △ ' + String(item.label).padEnd(28) + '已安装,未发现技能目录 [不适用]');
|
|
1896
|
+
}
|
|
1897
|
+
return { lines, existing, missing, noSkillDir };
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
function printHubHosts(discovery, json) {
|
|
1901
|
+
if (json) {
|
|
1902
|
+
out(JSON.stringify(discovery, null, 2));
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
const view = hubHostLines(discovery);
|
|
1906
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— Hub 宿主发现(文件系统优先,不读元忆)');
|
|
1907
|
+
out('本机发现技能目录 ' + view.existing.length + ' 个 / 已装应用标记 ' + discovery.installed.length +
|
|
1908
|
+
' 个 / 未发现技能目录 ' + view.noSkillDir.length + ' 个');
|
|
1909
|
+
for (const line of view.lines) out(line);
|
|
1910
|
+
if (view.missing.length > 0) {
|
|
1911
|
+
out('');
|
|
1912
|
+
out('未安装 / 未发现目录 ' + view.missing.length + ' 个(已收录映射,等安装后自动出现)。');
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
function printHubStatus(payload, discovery, json) {
|
|
1917
|
+
if (json) {
|
|
1918
|
+
out(JSON.stringify({ ...payload, discovery }, null, 2));
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— Hub 状态');
|
|
1922
|
+
out('标准: ' + payload.standard);
|
|
1923
|
+
out('Hub: ' + payload.hubDir);
|
|
1924
|
+
out('技能: ' + payload.summary.skills + ' / 非元阁技能: ' + payload.summary.external +
|
|
1925
|
+
' / 链接: ' + payload.summary.links + ' / 异常链接: ' + payload.summary.brokenLinks);
|
|
1926
|
+
out('');
|
|
1927
|
+
for (const skill of payload.skills) {
|
|
1928
|
+
const origin = skill.origin === 'yotta' ? '元技能' : '非元阁技能(无更新源)';
|
|
1929
|
+
const links = skill.links.length > 0 ? skill.links.map((link) => link.label || link.agent || '链接').join(', ') : '-';
|
|
1930
|
+
out(' ' + skill.slug.padEnd(26) + 'v' + String(skill.version || '-').padEnd(10) +
|
|
1931
|
+
origin.padEnd(22) + '链接: ' + links +
|
|
1932
|
+
(skill.status === 'missing' ? ' [目标缺失]' : ''));
|
|
1933
|
+
}
|
|
1934
|
+
if (payload.skills.length === 0) out(' (Hub 为空;先运行 yotta-skills hub install)');
|
|
1935
|
+
const broken = payload.links.filter((link) => link.status !== 'ok');
|
|
1936
|
+
if (broken.length > 0) {
|
|
1937
|
+
out('');
|
|
1938
|
+
out('异常链接:');
|
|
1939
|
+
for (const link of broken) out(' ✘ ' + link.slug + ' ' + link.target + '(' + link.status + ')');
|
|
1940
|
+
}
|
|
1941
|
+
out('');
|
|
1942
|
+
printHubHosts(discovery, false);
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
function hubScanEngine(hubDir, discovery, opts) {
|
|
1946
|
+
const direct = findVerifyEngine(hubDir, opts);
|
|
1947
|
+
if (direct) return direct;
|
|
1948
|
+
for (const host of discovery.hosts) {
|
|
1949
|
+
if (!host.exists) continue;
|
|
1950
|
+
const engine = findVerifyEngine(host.dir, opts);
|
|
1951
|
+
if (engine) return engine;
|
|
1952
|
+
}
|
|
1953
|
+
return null;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
function hubScanSkill(hubDir, discovery, opts, skillDir) {
|
|
1957
|
+
if (opts.skipScan) {
|
|
1958
|
+
return { ok: true, verdict: 'explicit-unverified', counts: null, block: false };
|
|
1959
|
+
}
|
|
1960
|
+
const engine = hubScanEngine(hubDir, discovery, opts);
|
|
1961
|
+
if (!engine) {
|
|
1962
|
+
return {
|
|
1963
|
+
ok: false,
|
|
1964
|
+
error: '未找到元信扫描引擎(可先安装 yotta-verify,或用 --allow-unverified 显式降级)',
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
const scan = runScan(engine, skillDir, opts);
|
|
1968
|
+
if (!scan.ok) return scan;
|
|
1969
|
+
const verdict = gateLib.evaluateVerdict(scan.verdict);
|
|
1970
|
+
return { ok: true, verdict: scan.verdict, counts: scan.counts, block: verdict.block };
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
function printAdoptScan(payload, json) {
|
|
1974
|
+
if (json) {
|
|
1975
|
+
out(JSON.stringify(payload, null, 2));
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— Hub 收编预演(只读)');
|
|
1979
|
+
out('Hub: ' + payload.hubDir);
|
|
1980
|
+
out('候选 ' + payload.summary.candidates + ' / 冲突 ' + payload.summary.conflicts +
|
|
1981
|
+
' / 已在 Hub ' + payload.summary.alreadyInHub);
|
|
1982
|
+
out('');
|
|
1983
|
+
for (const item of payload.candidates) {
|
|
1984
|
+
const mark = item.conflict ? '△' : item.inHub ? '·' : '✔';
|
|
1985
|
+
out(' ' + mark + ' ' + item.slug.padEnd(26) + 'v' + String(item.version || '-').padEnd(10) +
|
|
1986
|
+
(item.sourceHost || '-') + ' ' + (item.source || ''));
|
|
1987
|
+
if (item.conflict) {
|
|
1988
|
+
out(' 冲突副本: ' + item.variants.map((variant) =>
|
|
1989
|
+
(variant.host || '?') + ' v' + (variant.version || '?')).join(', '));
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
if (payload.candidates.length === 0) out(' (没有发现可收编技能)');
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
function printAdoptApply(payload, json) {
|
|
1996
|
+
if (json) {
|
|
1997
|
+
out(JSON.stringify(payload, null, 2));
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— Hub 收编');
|
|
2001
|
+
out('Hub: ' + payload.hubDir);
|
|
2002
|
+
for (const item of payload.results) {
|
|
2003
|
+
const mark = item.status === 'imported' ? '✔' : item.status === 'skip' ? '·' : item.status === 'conflict' ? '△' : '✘';
|
|
2004
|
+
out(' ' + mark + ' ' + item.slug.padEnd(26) + item.note);
|
|
2005
|
+
}
|
|
2006
|
+
out('汇总: 收编 ' + payload.results.filter((item) => item.status === 'imported').length +
|
|
2007
|
+
' / 跳过 ' + payload.results.filter((item) => item.status === 'skip').length +
|
|
2008
|
+
' / 冲突 ' + payload.results.filter((item) => item.status === 'conflict').length +
|
|
2009
|
+
' / 失败 ' + payload.results.filter((item) => !['imported', 'skip', 'conflict'].includes(item.status)).length);
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
function printHubDoctor(payload, json) {
|
|
2013
|
+
if (json) {
|
|
2014
|
+
out(JSON.stringify(payload, null, 2));
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— Hub doctor');
|
|
2018
|
+
out('Hub: ' + payload.hubDir);
|
|
2019
|
+
for (const check of payload.checks) {
|
|
2020
|
+
if (check.ok && check.severity !== 'info') continue;
|
|
2021
|
+
const mark = check.ok ? '✔' : check.severity === 'warning' ? '△' : '✘';
|
|
2022
|
+
out(' ' + mark + ' ' + check.message + (check.hint ? '(修复: ' + check.hint + ')' : ''));
|
|
2023
|
+
}
|
|
2024
|
+
out('汇总: 错误 ' + payload.summary.errors + ' / 警告 ' + payload.summary.warnings);
|
|
2025
|
+
if (!payload.ok) out('Hub doctor 未通过:请按上面的修复建议处理后重试。');
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
function runHub(opts) {
|
|
2029
|
+
const hubDir = hubLib.resolveHubDir(opts);
|
|
2030
|
+
const action = opts.hubAction || 'status';
|
|
2031
|
+
const discovery = agentDiscoveryLib.discoverHosts({ homeDir: os.homedir(), env: process.env });
|
|
2032
|
+
|
|
2033
|
+
if (action === 'hosts' || action === 'discover') {
|
|
2034
|
+
printHubHosts(discovery, opts.json);
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
if (action === 'install' || action === 'update') {
|
|
2039
|
+
out('Hub: ' + hubDir);
|
|
2040
|
+
const result = action === 'install' ? runInstall(opts, hubDir) : runUpdate(opts, hubDir);
|
|
2041
|
+
const synced = hubLib.syncHubState(hubDir, { manifest: MANIFEST });
|
|
2042
|
+
const count = Object.values(synced.state.skills).filter((item) => item.status === 'present').length;
|
|
2043
|
+
out('');
|
|
2044
|
+
out('Hub 台账已更新: ' + count + ' 个技能;标准 ' + hubLib.STANDARD_ID);
|
|
2045
|
+
if (result.failed > 0) process.exitCode = result.exitCode || 1;
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
if (action === 'adopt') {
|
|
2050
|
+
if (opts.as) {
|
|
2051
|
+
die('hub adopt --as 暂未启用', 2, '先收编原 slug;改名 / 冲突改写放到后续里程碑,避免静默改技能身份。');
|
|
2052
|
+
}
|
|
2053
|
+
const scan = hubAdoptLib.scanCandidates({ hubDir, discovery });
|
|
2054
|
+
if (!opts.apply) {
|
|
2055
|
+
printAdoptScan(scan, opts.json);
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
const payload = hubAdoptLib.applyCandidates({
|
|
2059
|
+
hubDir,
|
|
2060
|
+
candidates: scan.candidates,
|
|
2061
|
+
include: opts.include,
|
|
2062
|
+
force: opts.force,
|
|
2063
|
+
inPlace: opts.inPlace,
|
|
2064
|
+
skipScan: opts.skipScan,
|
|
2065
|
+
allowUnverified: opts.allowUnverified,
|
|
2066
|
+
manifest: MANIFEST,
|
|
2067
|
+
homeDir: os.homedir(),
|
|
2068
|
+
env: process.env,
|
|
2069
|
+
scan: (skillDir) => hubScanSkill(hubDir, discovery, opts, skillDir),
|
|
2070
|
+
});
|
|
2071
|
+
printAdoptApply(payload, opts.json);
|
|
2072
|
+
if (payload.results.some((item) => !['imported', 'skip', 'conflict'].includes(item.status))) process.exitCode = 1;
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
if (action === 'refresh') {
|
|
2077
|
+
if (!opts.from) die('hub refresh 缺少 --from <path>', 2, '示例:yotta-skills hub refresh my-skill --from <技能目录>。');
|
|
2078
|
+
if (opts.skills.length !== 1) die('hub refresh 需要一个且仅一个技能 slug', 2, '示例:yotta-skills hub refresh my-skill --from <技能目录>。');
|
|
2079
|
+
const result = hubAdoptLib.refreshFrom({
|
|
2080
|
+
hubDir,
|
|
2081
|
+
slug: opts.skills[0],
|
|
2082
|
+
from: opts.from,
|
|
2083
|
+
skipScan: opts.skipScan,
|
|
2084
|
+
allowUnverified: opts.allowUnverified,
|
|
2085
|
+
manifest: MANIFEST,
|
|
2086
|
+
homeDir: os.homedir(),
|
|
2087
|
+
env: process.env,
|
|
2088
|
+
scan: (skillDir) => hubScanSkill(hubDir, discovery, opts, skillDir),
|
|
2089
|
+
});
|
|
2090
|
+
if (opts.json) out(JSON.stringify(result, null, 2));
|
|
2091
|
+
else if (result.ok) out('已刷新: ' + result.slug + '(来源 ' + result.source + ')');
|
|
2092
|
+
else out('刷新失败: ' + result.error);
|
|
2093
|
+
if (!result.ok) process.exitCode = 1;
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
if (action === 'link') {
|
|
2098
|
+
const targets = hubTargetDirs(opts, discovery);
|
|
2099
|
+
let failed = 0;
|
|
2100
|
+
const targetPayloads = [];
|
|
2101
|
+
for (const target of targets) {
|
|
2102
|
+
if (path.resolve(target.dir) === path.resolve(hubDir)) {
|
|
2103
|
+
targetPayloads.push({ dir: target.dir, label: target.label, skipped: true, results: [] });
|
|
2104
|
+
if (!opts.json) out('跳过 Hub 自身: ' + target.dir);
|
|
2105
|
+
continue;
|
|
2106
|
+
}
|
|
2107
|
+
const result = hubLib.linkSkills({
|
|
2108
|
+
hubDir,
|
|
2109
|
+
targetDir: target.dir,
|
|
2110
|
+
agentId: target.agentId,
|
|
2111
|
+
label: target.label,
|
|
2112
|
+
slugs: opts.skills,
|
|
2113
|
+
force: opts.force,
|
|
2114
|
+
dryRun: opts.dryRun,
|
|
2115
|
+
});
|
|
2116
|
+
targetPayloads.push({ dir: target.dir, label: target.label, results: result.results });
|
|
2117
|
+
if (!opts.json) {
|
|
2118
|
+
out('');
|
|
2119
|
+
out('目标: ' + target.dir + '(' + target.label + ')');
|
|
2120
|
+
for (const item of result.results) {
|
|
2121
|
+
const mark = item.status === 'linked' ? '✔' : item.status === 'conflict' ? '△' : item.status === 'error' ? '✘' : '·';
|
|
2122
|
+
out(' ' + mark + ' ' + item.slug.padEnd(26) + item.note);
|
|
2123
|
+
if (item.status === 'error') failed++;
|
|
2124
|
+
}
|
|
2125
|
+
} else {
|
|
2126
|
+
failed += result.results.filter((item) => item.status === 'error').length;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
if (opts.json) out(JSON.stringify({ hubDir, targets: targetPayloads, failed }, null, 2));
|
|
2130
|
+
if (failed > 0) process.exitCode = 1;
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
if (action === 'unlink') {
|
|
2135
|
+
const targets = hubTargetDirs(opts, discovery);
|
|
2136
|
+
let failed = 0;
|
|
2137
|
+
const targetPayloads = [];
|
|
2138
|
+
for (const target of targets) {
|
|
2139
|
+
const result = hubLib.unlinkSkills({
|
|
2140
|
+
hubDir,
|
|
2141
|
+
targetDir: target.dir,
|
|
2142
|
+
slugs: opts.skills,
|
|
2143
|
+
dryRun: opts.dryRun,
|
|
2144
|
+
});
|
|
2145
|
+
targetPayloads.push({ dir: target.dir, label: target.label, results: result.results });
|
|
2146
|
+
if (!opts.json) {
|
|
2147
|
+
out('');
|
|
2148
|
+
out('目标: ' + target.dir + '(' + target.label + ')');
|
|
2149
|
+
for (const item of result.results) {
|
|
2150
|
+
const mark = item.status === 'unlinked' ? '✔' : item.status === 'refused' ? '△' : item.status === 'error' ? '✘' : '·';
|
|
2151
|
+
out(' ' + mark + ' ' + item.slug.padEnd(26) + item.note);
|
|
2152
|
+
if (item.status === 'error') failed++;
|
|
2153
|
+
}
|
|
2154
|
+
} else {
|
|
2155
|
+
failed += result.results.filter((item) => item.status === 'error').length;
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
if (opts.json) out(JSON.stringify({ hubDir, targets: targetPayloads, failed }, null, 2));
|
|
2159
|
+
if (failed > 0) process.exitCode = 1;
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
if (action === 'status') {
|
|
2164
|
+
const payload = hubLib.status({ hubDir, manifest: MANIFEST });
|
|
2165
|
+
printHubStatus(payload, discovery, opts.json);
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
if (action === 'doctor') {
|
|
2170
|
+
const payload = hubLib.doctor({
|
|
2171
|
+
hubDir,
|
|
2172
|
+
manifest: MANIFEST,
|
|
2173
|
+
homeDir: os.homedir(),
|
|
2174
|
+
env: process.env,
|
|
2175
|
+
});
|
|
2176
|
+
printHubDoctor(payload, opts.json);
|
|
2177
|
+
if (!payload.ok) process.exitCode = 1;
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
die('未知 hub 子命令: ' + action, 2,
|
|
2182
|
+
'支持 install / update / adopt / refresh / link / unlink / status / hosts / doctor。');
|
|
2183
|
+
}
|
|
2184
|
+
|
|
1803
2185
|
// ── main ───────────────────────────────────────────────────────────────────
|
|
1804
2186
|
function main() {
|
|
1805
2187
|
const node = depsLib.nodeCheck();
|
|
@@ -1828,6 +2210,10 @@ function main() {
|
|
|
1828
2210
|
runHook(opts);
|
|
1829
2211
|
return;
|
|
1830
2212
|
}
|
|
2213
|
+
if (command === 'hub') {
|
|
2214
|
+
runHub(opts);
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
1831
2217
|
if (command === 'rollback' && opts.list) {
|
|
1832
2218
|
runRollback(opts, null);
|
|
1833
2219
|
return;
|