@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.
@@ -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.24.2';
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
- // 智能体 -> 用户级默认技能目录(与各技能 install.js 同源;.agents/skills 并非通用目录)
76
- const AGENT_DIRS = {
77
- claude: { label: 'Claude Code', dirs: ['.claude/skills'] },
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
- const base = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
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
- const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
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
- if (rel === '.codex/skills') return codexUserDir();
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).join(', '));
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;