@yottameta/yotta-skills-plugin 0.7.2 → 0.19.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 +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/yotta-skills/SKILL.md +86 -12
- package/skills/yotta-skills/bin/yotta-skills.js +681 -90
- package/skills/yotta-skills/references/faq.md +26 -3
- package/skills/yotta-skills/references/install-flow.md +95 -7
- package/skills/yotta-skills/references/skill-list.md +10 -10
- package/skills/yotta-skills/references/tutorial.md +40 -6
- package/skills/yotta-skills/references/walkthroughs.md +11 -3
- package/skills/yotta-skills/scripts/yotta-skills-mcp.py +1 -1
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* npx -y @yottameta/yotta-skills install <skill> --dir <path> # 装单个技能
|
|
10
10
|
* npx -y @yottameta/yotta-skills update --agent <name> # 增量更新已装技能(补齐缺失/版本不一致)
|
|
11
11
|
* npx -y @yottameta/yotta-skills update --check # 只读检查更新(联网对 npm 最新,不改动;退出码 0/3/1)
|
|
12
|
+
* npx -y @yottameta/yotta-skills update --check --scheduled # 后台周检(未到期不联网,到期单次检查并写缓存)
|
|
12
13
|
* npx -y @yottameta/yotta-skills update --auto # 检查到家族更新后自动更新(仅 yotta-* 家族,含装前扫描)
|
|
14
|
+
* npx -y @yottameta/yotta-skills hook capabilities # 查看宿主六事件能力矩阵
|
|
15
|
+
* npx -y @yottameta/yotta-skills hook evaluate --event before_send --manifest <file> # 评估并留证
|
|
13
16
|
* npx -y @yottameta/yotta-skills --dry-run # 预览将安装清单(不联网、不改动)
|
|
14
17
|
*
|
|
15
18
|
* 版本策略:清单锁定 `major.x`(不锁死 patch,维护性更新随最新);--pin 锁死精确版本。
|
|
@@ -24,9 +27,17 @@ const os = require('os');
|
|
|
24
27
|
const { spawnSync } = require('child_process');
|
|
25
28
|
const http = require('http');
|
|
26
29
|
const https = require('https');
|
|
30
|
+
const evidenceLib = require('../lib/install-evidence');
|
|
31
|
+
const gateLib = require('../lib/verify-gate');
|
|
32
|
+
const healthLib = require('../lib/install-health');
|
|
33
|
+
const lifecycleLib = require('../lib/install-lifecycle');
|
|
34
|
+
const snapshotLib = require('../lib/install-snapshot');
|
|
35
|
+
const updateCheckLib = require('../lib/update-check');
|
|
36
|
+
const hookAdapterLib = require('../lib/hook-adapter');
|
|
37
|
+
const { createInstaller, isSafeTarEntry } = require('../lib/install-pipeline');
|
|
27
38
|
|
|
28
39
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
29
|
-
let VERSION = '0.
|
|
40
|
+
let VERSION = '0.19.0';
|
|
30
41
|
try { VERSION = require(path.join(PKG_ROOT, 'package.json')).version; } catch (_) { /* keep fallback */ }
|
|
31
42
|
|
|
32
43
|
function loadManifest() {
|
|
@@ -137,7 +148,8 @@ function parseArgs(argv) {
|
|
|
137
148
|
help: false, version: false, agent: null, dir: null, npm: null,
|
|
138
149
|
python: null, verify: null, command: null, skill: null, rest: [],
|
|
139
150
|
inventory: false, reindex: false, noReindex: false, json: false, project: false, route: null,
|
|
140
|
-
check: false, auto: false, registry: null,
|
|
151
|
+
check: false, auto: false, scheduled: false, registry: null, slug: null,
|
|
152
|
+
host: null, event: null, manifest: null, context: null,
|
|
141
153
|
};
|
|
142
154
|
const positionals = [];
|
|
143
155
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -166,15 +178,27 @@ function parseArgs(argv) {
|
|
|
166
178
|
else if (a === '--npm') opts.npm = take('--npm');
|
|
167
179
|
else if (a === '--python') opts.python = take('--python');
|
|
168
180
|
else if (a === '--verify') opts.verify = take('--verify');
|
|
181
|
+
else if (a === '--slug') {
|
|
182
|
+
const value = take('--slug').toLowerCase();
|
|
183
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(value)) {
|
|
184
|
+
die('--slug 格式非法', 2, '只允许小写字母、数字和连字符,例如 yotta-memory。');
|
|
185
|
+
}
|
|
186
|
+
opts.slug = value;
|
|
187
|
+
}
|
|
169
188
|
else if (a === '--check') opts.check = true;
|
|
170
189
|
else if (a === '--auto') opts.auto = true;
|
|
190
|
+
else if (a === '--scheduled') opts.scheduled = true;
|
|
171
191
|
else if (a === '--registry') opts.registry = take('--registry');
|
|
192
|
+
else if (a === '--host') opts.host = take('--host').toLowerCase();
|
|
193
|
+
else if (a === '--event') opts.event = take('--event').toLowerCase();
|
|
194
|
+
else if (a === '--manifest') opts.manifest = take('--manifest');
|
|
195
|
+
else if (a === '--context') opts.context = take('--context');
|
|
172
196
|
else if (a.startsWith('-')) die('未知参数: ' + a, 2, '可用 --help 查看支持的选项。');
|
|
173
197
|
else positionals.push(a);
|
|
174
198
|
}
|
|
175
|
-
// 命令解析:install / update,其余位置参数 = 技能 slug(可多个)
|
|
199
|
+
// 命令解析:install / update / doctor / rollback,其余位置参数 = 技能 slug(可多个)
|
|
176
200
|
for (const p of positionals) {
|
|
177
|
-
if (p === 'install' || p === 'update') {
|
|
201
|
+
if (p === 'install' || p === 'update' || p === 'doctor' || p === 'rollback' || p === 'hook') {
|
|
178
202
|
if (opts.command && opts.command !== p) die('命令冲突:' + opts.command + ' 与 ' + p);
|
|
179
203
|
opts.command = p;
|
|
180
204
|
} else {
|
|
@@ -184,6 +208,9 @@ function parseArgs(argv) {
|
|
|
184
208
|
// 直接给 slug 且无命令 → 视为 install 单个/多个
|
|
185
209
|
if (!opts.command && opts.rest.length > 0) opts.command = 'install';
|
|
186
210
|
opts.skills = opts.rest.map(s => s.toLowerCase());
|
|
211
|
+
if (opts.scheduled && (opts.command !== 'update' || !opts.check || opts.auto)) {
|
|
212
|
+
die('--scheduled 只能与 update --check 一起使用', 2, '请使用 update --check --scheduled;自动更新不使用后台调度入口。');
|
|
213
|
+
}
|
|
187
214
|
return opts;
|
|
188
215
|
}
|
|
189
216
|
|
|
@@ -284,15 +311,69 @@ async function runUpdateCheck(opts, dest) {
|
|
|
284
311
|
var lines = [];
|
|
285
312
|
var quiet = !!opts.json;
|
|
286
313
|
function say(s) { if (!quiet) lines.push(s); }
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
314
|
+
var scheduled = !!opts.scheduled;
|
|
315
|
+
var cache = updateCheckLib.readCache();
|
|
316
|
+
var target = cache.targets[updateCheckLib.targetKey(dest)] || null;
|
|
317
|
+
var due = updateCheckLib.isDue(target, Date.now());
|
|
318
|
+
if (!scheduled) {
|
|
319
|
+
say('yotta-skills(元阁)v' + VERSION + ' —— 检查更新(只读,不改动) -> ' + dest);
|
|
320
|
+
say('版本源: ' + (opts.registry || process.env.YOTTA_SKILLS_REGISTRY || 'https://registry.npmjs.org/'));
|
|
321
|
+
say('');
|
|
322
|
+
}
|
|
323
|
+
if (scheduled && !due) {
|
|
324
|
+
var cachedRows = (target && target.last_result && target.last_result.rows) || [];
|
|
325
|
+
var cachedPayload = {
|
|
326
|
+
dest: dest,
|
|
327
|
+
scheduled: true,
|
|
328
|
+
due: false,
|
|
329
|
+
checked: false,
|
|
330
|
+
updatable: cachedRows.filter(function (x) { return x.hasUpdate; }),
|
|
331
|
+
updates: (target && target.last_result && target.last_result.updates) || 0,
|
|
332
|
+
latest: (target && target.last_result && target.last_result.latest) || 0,
|
|
333
|
+
failed: 0,
|
|
334
|
+
nonFamily: (target && target.last_result && target.last_result.nonFamily) || 0,
|
|
335
|
+
cache: target,
|
|
336
|
+
error: null,
|
|
337
|
+
};
|
|
338
|
+
if (opts.json) out(JSON.stringify(cachedPayload, null, 2));
|
|
339
|
+
return {
|
|
340
|
+
code: 0,
|
|
341
|
+
scheduled: true,
|
|
342
|
+
due: false,
|
|
343
|
+
checked: false,
|
|
344
|
+
rows: cachedRows,
|
|
345
|
+
updates: cachedPayload.updates,
|
|
346
|
+
latest: cachedPayload.latest,
|
|
347
|
+
failed: 0,
|
|
348
|
+
nonFamily: cachedPayload.nonFamily,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
290
351
|
var installed = scanInstalledSlugs(dest);
|
|
291
352
|
if (installed.length === 0) {
|
|
292
|
-
|
|
293
|
-
|
|
353
|
+
var emptyResult = { updates: 0, latest: 0, failed: 0, nonFamily: 0, rows: [], errors: [] };
|
|
354
|
+
var emptyCacheRecord = null;
|
|
355
|
+
try {
|
|
356
|
+
emptyCacheRecord = updateCheckLib.recordCheck(os.homedir(), dest, { result: emptyResult, error: null });
|
|
357
|
+
} catch (_) { /* 缓存是后台优化的 best-effort,不阻断检查结果 */ }
|
|
358
|
+
if (!scheduled) {
|
|
359
|
+
say('(' + dest + ' 下未发现已装技能目录)');
|
|
360
|
+
for (var emptyIndex = 0; emptyIndex < lines.length; emptyIndex++) out(lines[emptyIndex]);
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
code: 0,
|
|
364
|
+
scheduled: scheduled,
|
|
365
|
+
due: true,
|
|
366
|
+
checked: true,
|
|
367
|
+
rows: [],
|
|
368
|
+
updates: 0,
|
|
369
|
+
latest: 0,
|
|
370
|
+
failed: 0,
|
|
371
|
+
nonFamily: 0,
|
|
372
|
+
cache: emptyCacheRecord,
|
|
373
|
+
};
|
|
294
374
|
}
|
|
295
375
|
var rows = [];
|
|
376
|
+
var failures = [];
|
|
296
377
|
var updates = 0, latest = 0, failed = 0, nonFamily = 0;
|
|
297
378
|
for (var i = 0; i < installed.length; i++) {
|
|
298
379
|
var it = installed[i];
|
|
@@ -305,6 +386,7 @@ async function runUpdateCheck(opts, dest) {
|
|
|
305
386
|
var res = await fetchRegistryLatest(fam.pkg, opts);
|
|
306
387
|
if (!res.ok) {
|
|
307
388
|
failed++;
|
|
389
|
+
failures.push({ slug: it.slug, error: res.error });
|
|
308
390
|
say(' ✘ ' + it.slug.padEnd(22) + '检查失败: ' + res.error);
|
|
309
391
|
continue;
|
|
310
392
|
}
|
|
@@ -323,11 +405,61 @@ async function runUpdateCheck(opts, dest) {
|
|
|
323
405
|
}
|
|
324
406
|
rows.push({ slug: it.slug, installed: cur, latest: current, pkg: fam.pkg, hasUpdate: !isUp, family: fam });
|
|
325
407
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
408
|
+
var result = {
|
|
409
|
+
updates: updates,
|
|
410
|
+
latest: latest,
|
|
411
|
+
failed: failed,
|
|
412
|
+
nonFamily: nonFamily,
|
|
413
|
+
rows: rows,
|
|
414
|
+
errors: failures,
|
|
415
|
+
};
|
|
416
|
+
var lastError = failures.length > 0
|
|
417
|
+
? failures.map(function (x) { return x.slug + ': ' + x.error; }).join('; ')
|
|
418
|
+
: null;
|
|
419
|
+
var cacheRecord = null;
|
|
420
|
+
try {
|
|
421
|
+
cacheRecord = updateCheckLib.recordCheck(os.homedir(), dest, { result: result, error: lastError });
|
|
422
|
+
} catch (_) { /* 缓存是后台优化的 best-effort,不阻断检查结果 */ }
|
|
329
423
|
var code = (failed > 0) ? 1 : (updates > 0 ? 3 : 0);
|
|
330
|
-
|
|
424
|
+
var updatable = rows.filter(function (x) { return x.hasUpdate; });
|
|
425
|
+
if (scheduled) {
|
|
426
|
+
if (updates > 0) {
|
|
427
|
+
for (var j = 0; j < updatable.length; j++) {
|
|
428
|
+
out(' ' + updatable[j].slug + ':本地 v' + (updatable[j].installed || '未知') + ' -> 最新 v' + updatable[j].latest);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (opts.json) {
|
|
432
|
+
out(JSON.stringify({
|
|
433
|
+
dest: dest,
|
|
434
|
+
scheduled: true,
|
|
435
|
+
due: true,
|
|
436
|
+
checked: true,
|
|
437
|
+
updatable: updatable,
|
|
438
|
+
updates: updates,
|
|
439
|
+
latest: latest,
|
|
440
|
+
failed: failed,
|
|
441
|
+
nonFamily: nonFamily,
|
|
442
|
+
cache: cacheRecord,
|
|
443
|
+
error: lastError,
|
|
444
|
+
}, null, 2));
|
|
445
|
+
}
|
|
446
|
+
return { code: 0, scheduled: true, due: true, checked: true, rows: rows, updates: updates, latest: latest, failed: failed, nonFamily: nonFamily };
|
|
447
|
+
}
|
|
448
|
+
say('汇总: 有更新 ' + updates + ' / 已最新 ' + latest + ' / 检查失败 ' + failed + ' / 非家族跳过 ' + nonFamily);
|
|
449
|
+
for (var lineIndex = 0; lineIndex < lines.length; lineIndex++) out(lines[lineIndex]);
|
|
450
|
+
if (opts.json) {
|
|
451
|
+
out(JSON.stringify({
|
|
452
|
+
dest: dest,
|
|
453
|
+
updatable: updatable,
|
|
454
|
+
updates: updates,
|
|
455
|
+
latest: latest,
|
|
456
|
+
failed: failed,
|
|
457
|
+
nonFamily: nonFamily,
|
|
458
|
+
cache: cacheRecord,
|
|
459
|
+
error: lastError,
|
|
460
|
+
}, null, 2));
|
|
461
|
+
}
|
|
462
|
+
return { code: code, scheduled: false, due: true, checked: true, rows: rows, updates: updates, latest: latest, failed: failed, nonFamily: nonFamily };
|
|
331
463
|
}
|
|
332
464
|
|
|
333
465
|
async function runUpdateAuto(opts, dest) {
|
|
@@ -343,19 +475,27 @@ async function runUpdateAuto(opts, dest) {
|
|
|
343
475
|
}
|
|
344
476
|
out('');
|
|
345
477
|
out('检测到 ' + r.updates + ' 个家族技能可更新,开始自动更新(安装管线,含装前安全扫描):');
|
|
346
|
-
var ok = 0, failed2 = 0;
|
|
478
|
+
var ok = 0, failed2 = 0, autoExitCode = 0;
|
|
347
479
|
for (var i = 0; i < r.rows.length; i++) {
|
|
348
480
|
var row = r.rows[i];
|
|
349
481
|
if (!row.hasUpdate) continue;
|
|
350
|
-
var res = installOne(
|
|
482
|
+
var res = installOne(
|
|
483
|
+
Object.assign({}, row.family, { version: row.latest }),
|
|
484
|
+
dest,
|
|
485
|
+
Object.assign({}, opts, { force: true, pin: true, skipScan: false }),
|
|
486
|
+
);
|
|
351
487
|
if (res.status === 'ok') ok++;
|
|
352
488
|
else if (res.status === 'skip') ok++;
|
|
353
|
-
else
|
|
489
|
+
else {
|
|
490
|
+
failed2++;
|
|
491
|
+
autoExitCode = Math.max(autoExitCode, res.exitCode || 1);
|
|
492
|
+
out(' ✘ ' + row.slug + ' 失败: ' + res.note);
|
|
493
|
+
}
|
|
354
494
|
}
|
|
355
495
|
out('');
|
|
356
496
|
out('自动更新汇总: 成功 ' + ok + ' / 失败 ' + failed2);
|
|
357
|
-
maybeAutoReindex(opts, dest);
|
|
358
|
-
return { code: failed2 > 0 ?
|
|
497
|
+
if (failed2 === 0) maybeAutoReindex(opts, dest);
|
|
498
|
+
return { code: failed2 > 0 ? autoExitCode : 0 };
|
|
359
499
|
}
|
|
360
500
|
|
|
361
501
|
function shouldSkip(name, isFile) {
|
|
@@ -399,17 +539,12 @@ function detectProjectDir() {
|
|
|
399
539
|
return null;
|
|
400
540
|
}
|
|
401
541
|
|
|
402
|
-
// ── 元信 scan
|
|
542
|
+
// ── 元信 scan 与安装门禁 ──────────────────────────────────────────────────
|
|
403
543
|
function findVerifyEngine(dest, opts) {
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
if (process.env.YOTTA_SKILLS_VERIFY) cands.push(path.resolve(process.env.YOTTA_SKILLS_VERIFY));
|
|
407
|
-
if (dest) cands.push(path.join(dest, 'yotta-verify', 'scripts', 'yotta_verify.py'));
|
|
408
|
-
for (const c of cands) {
|
|
409
|
-
try { if (fs.statSync(c).isFile()) return c; } catch (_) { /* next */ }
|
|
410
|
-
}
|
|
411
|
-
return null;
|
|
544
|
+
const registry = require('../lib/skills-scan').readRegistry();
|
|
545
|
+
return gateLib.findVerifier({ dest, opts, registry });
|
|
412
546
|
}
|
|
547
|
+
|
|
413
548
|
function findPython(opts) {
|
|
414
549
|
const cands = [];
|
|
415
550
|
if (opts.python) cands.push(opts.python);
|
|
@@ -424,19 +559,29 @@ function findPython(opts) {
|
|
|
424
559
|
}
|
|
425
560
|
return null;
|
|
426
561
|
}
|
|
562
|
+
|
|
427
563
|
function runScan(engine, skillDir) {
|
|
428
564
|
const python = findPython({});
|
|
429
|
-
if (!python) return { ok: false,
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
565
|
+
if (!python) return { ok: false, error: '未找到 Python(元信 scan 需要 Python 3.8+)' };
|
|
566
|
+
return gateLib.runVerifier(engine, skillDir, { python, spawnSync });
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function scanTarget(engine, skillDir) {
|
|
570
|
+
const scan = runScan(engine, skillDir);
|
|
571
|
+
if (scan.ok) {
|
|
572
|
+
const counts = scan.counts || {};
|
|
573
|
+
const line = 'critical ' + (counts.critical || 0) + ' / high ' + (counts.high || 0) +
|
|
574
|
+
' / medium ' + (counts.medium || 0) + ' / low ' + (counts.low || 0) +
|
|
575
|
+
' / info ' + (counts.info || 0);
|
|
576
|
+
out(' 元信 scan: ' + scan.verdict + '(' + line + ')');
|
|
577
|
+
if (scan.verdict === gateLib.BLOCK) out(' ⚠ 元信 verdict 为 DO NOT INSTALL,已阻断安装。');
|
|
578
|
+
else if (scan.verdict === gateLib.CAUTION || scan.verdict === gateLib.REVIEW) {
|
|
579
|
+
out(' ⚠ 元信 verdict 为 ' + scan.verdict + ',继续安装并保留风险证据。');
|
|
580
|
+
}
|
|
581
|
+
} else {
|
|
582
|
+
out(' 元信 scan: ' + scan.error);
|
|
439
583
|
}
|
|
584
|
+
return scan;
|
|
440
585
|
}
|
|
441
586
|
|
|
442
587
|
// ── 安装 ───────────────────────────────────────────────────────────────────
|
|
@@ -468,6 +613,13 @@ function runNpmPack(skill, opts, packDir) {
|
|
|
468
613
|
}
|
|
469
614
|
|
|
470
615
|
function extractTarball(tarball, extractDir) {
|
|
616
|
+
const listed = spawnSync(tarBin(), ['-tzf', tarball], { encoding: 'utf8', timeout: 120000, maxBuffer: 16 * 1024 * 1024 });
|
|
617
|
+
if (listed.status !== 0) {
|
|
618
|
+
return { error: (listed.stderr || listed.stdout || 'tar 列表读取失败').trim().split('\n').pop() };
|
|
619
|
+
}
|
|
620
|
+
const entries = String(listed.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
621
|
+
const unsafe = entries.find((entry) => !isSafeTarEntry(entry));
|
|
622
|
+
if (unsafe) return { error: '压缩包包含不安全路径: ' + unsafe };
|
|
471
623
|
const r = spawnSync(tarBin(), ['-xzf', tarball, '-C', extractDir], { encoding: 'utf8', timeout: 120000 });
|
|
472
624
|
if (r.status !== 0) return { error: (r.stderr || r.stdout || 'tar 解压失败').trim().split('\n').pop() };
|
|
473
625
|
const pkgDir = path.join(extractDir, 'package');
|
|
@@ -477,72 +629,94 @@ function extractTarball(tarball, extractDir) {
|
|
|
477
629
|
|
|
478
630
|
const COPY_SKIP = new Set(['package.json', 'bin', 'node_modules', '.git', '__pycache__']);
|
|
479
631
|
|
|
480
|
-
function
|
|
481
|
-
const
|
|
482
|
-
const
|
|
483
|
-
if (
|
|
484
|
-
return {
|
|
632
|
+
function ensureGate(context) {
|
|
633
|
+
const { skill, extracted, dest, opts } = context;
|
|
634
|
+
const current = findVerifyEngine(dest, opts);
|
|
635
|
+
if (current) {
|
|
636
|
+
return {
|
|
637
|
+
ok: true,
|
|
638
|
+
engine: current,
|
|
639
|
+
mode: opts.bootstrap ? 'trusted-bootstrap' : 'installed',
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
if (skill.slug === 'yotta-verify') {
|
|
643
|
+
const engine = path.join(extracted.pkgDir, 'scripts', 'yotta_verify.py');
|
|
644
|
+
if (!fs.existsSync(engine)) return { ok: false, error: '元信包内缺少 scripts/yotta_verify.py' };
|
|
645
|
+
return { ok: true, engine, mode: 'trusted-bootstrap' };
|
|
485
646
|
}
|
|
647
|
+
|
|
648
|
+
const verifier = findSkill('yotta-verify');
|
|
649
|
+
if (!verifier) return { ok: false, error: 'skills.json 缺少 yotta-verify' };
|
|
486
650
|
let tmp;
|
|
487
651
|
try {
|
|
488
|
-
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'yotta-
|
|
652
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'yotta-verify-bootstrap-'));
|
|
489
653
|
const packDir = path.join(tmp, 'pack');
|
|
490
654
|
fs.mkdirSync(packDir, { recursive: true });
|
|
491
|
-
const packed = runNpmPack(
|
|
492
|
-
if (packed.error) {
|
|
493
|
-
if (packed.detail) out(' ' + packed.detail.split(/\r?\n/).filter(Boolean).slice(-12).join('\n '));
|
|
494
|
-
return { skill, status: 'fail', version: null, note: packed.error };
|
|
495
|
-
}
|
|
655
|
+
const packed = runNpmPack(verifier, opts, packDir);
|
|
656
|
+
if (packed.error) return { ok: false, error: '元信自举下载失败: ' + packed.error };
|
|
496
657
|
const extractDir = path.join(tmp, 'extract');
|
|
497
658
|
fs.mkdirSync(extractDir, { recursive: true });
|
|
498
|
-
const
|
|
499
|
-
if (
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
fs.rmSync(target, { recursive: true, force: true });
|
|
518
|
-
fs.mkdirSync(target, { recursive: true });
|
|
519
|
-
copyDir(extracted.pkgDir, target, COPY_SKIP);
|
|
520
|
-
return { skill, status: 'ok', version: packed.resolved || skill.version, note: null };
|
|
521
|
-
} catch (e) {
|
|
522
|
-
return { skill, status: 'fail', version: null, note: e.message };
|
|
659
|
+
const extractedVerifier = extractTarball(packed.tarball, extractDir);
|
|
660
|
+
if (extractedVerifier.error) return { ok: false, error: '元信自举解包失败: ' + extractedVerifier.error };
|
|
661
|
+
const engine = path.join(extractedVerifier.pkgDir, 'scripts', 'yotta_verify.py');
|
|
662
|
+
if (!fs.existsSync(engine)) return { ok: false, error: '元信包内缺少 scripts/yotta_verify.py' };
|
|
663
|
+
|
|
664
|
+
out(' 元信未安装,使用可信源包自举并先自扫。');
|
|
665
|
+
const result = installOne(verifier, dest, {
|
|
666
|
+
...opts,
|
|
667
|
+
force: true,
|
|
668
|
+
skipScan: false,
|
|
669
|
+
bootstrap: true,
|
|
670
|
+
verify: engine,
|
|
671
|
+
});
|
|
672
|
+
if (result.status !== 'ok') return { ok: false, error: '元信自举失败: ' + result.note };
|
|
673
|
+
const installedEngine = path.join(dest, 'yotta-verify', 'scripts', 'yotta_verify.py');
|
|
674
|
+
if (!fs.existsSync(installedEngine)) return { ok: false, error: '元信自举后未找到安装引擎' };
|
|
675
|
+
return { ok: true, engine: installedEngine, mode: 'trusted-bootstrap' };
|
|
523
676
|
} finally {
|
|
524
677
|
if (tmp) fs.rmSync(tmp, { recursive: true, force: true });
|
|
525
678
|
}
|
|
526
679
|
}
|
|
527
680
|
|
|
681
|
+
const installOne = createInstaller({
|
|
682
|
+
runNpmPack,
|
|
683
|
+
extractTarball,
|
|
684
|
+
copyDir: (src, dst) => copyDir(src, dst, COPY_SKIP),
|
|
685
|
+
readInstalledVersion,
|
|
686
|
+
ensureGate,
|
|
687
|
+
scanTarget,
|
|
688
|
+
appendEvidence: evidenceLib.appendEvidence,
|
|
689
|
+
});
|
|
690
|
+
|
|
528
691
|
function runInstall(opts, dest) {
|
|
529
692
|
const skills = selectSkills(opts);
|
|
530
693
|
out('yotta-skills(元阁)v' + VERSION + ' —— 安装 ' + skills.length + ' 个技能 -> ' + dest);
|
|
531
694
|
out('版本策略: ' + (opts.pin ? 'pin(锁死清单精确版本)' : 'range(' + skillRange(skills[0]) + ',跟随最新 patch;--pin 锁死)'));
|
|
532
695
|
const results = [];
|
|
533
696
|
let failed = 0;
|
|
697
|
+
let exitCode = 0;
|
|
534
698
|
for (const s of skills) {
|
|
535
699
|
const r = installOne(s, dest, opts);
|
|
536
700
|
results.push(r);
|
|
537
|
-
if (r.status === 'ok')
|
|
701
|
+
if (r.status === 'ok') {
|
|
702
|
+
if (r.gate && r.gate.mode === 'explicit-unverified') {
|
|
703
|
+
out(' ⚠ ' + s.slug.padEnd(22) + '未执行装前扫描(explicit-unverified)');
|
|
704
|
+
}
|
|
705
|
+
out(' ✔ ' + s.slug.padEnd(22) + s.name + ' -> ' + (r.version || '?'));
|
|
706
|
+
}
|
|
538
707
|
else if (r.status === 'skip') out(' - ' + s.slug.padEnd(22) + s.name + ' (' + r.note + ',v' + r.version + ')');
|
|
539
|
-
else {
|
|
708
|
+
else {
|
|
709
|
+
failed++;
|
|
710
|
+
exitCode = Math.max(exitCode, r.exitCode || 1);
|
|
711
|
+
out(' ✘ ' + s.slug.padEnd(22) + s.name + ' 失败: ' + r.note);
|
|
712
|
+
}
|
|
540
713
|
}
|
|
541
714
|
const ok = results.filter(r => r.status === 'ok').length;
|
|
542
715
|
const skip = results.filter(r => r.status === 'skip').length;
|
|
543
716
|
out('');
|
|
544
717
|
out('汇总: 成功 ' + ok + ' / 跳过(已是最新) ' + skip + ' / 失败 ' + failed + '(共 ' + results.length + ')');
|
|
545
|
-
if (failed > 0) process.exitCode = 1;
|
|
718
|
+
if (failed > 0) process.exitCode = exitCode || 1;
|
|
719
|
+
return { failed, exitCode };
|
|
546
720
|
}
|
|
547
721
|
|
|
548
722
|
function runUpdate(opts, dest) {
|
|
@@ -550,6 +724,7 @@ function runUpdate(opts, dest) {
|
|
|
550
724
|
out('yotta-skills(元阁)v' + VERSION + ' —— 增量更新 -> ' + dest);
|
|
551
725
|
const results = [];
|
|
552
726
|
let failed = 0;
|
|
727
|
+
let exitCode = 0;
|
|
553
728
|
for (const s of skills) {
|
|
554
729
|
const existing = readInstalledVersion(path.join(dest, s.slug));
|
|
555
730
|
if (existing === s.version) {
|
|
@@ -559,15 +734,315 @@ function runUpdate(opts, dest) {
|
|
|
559
734
|
}
|
|
560
735
|
const r = installOne(s, dest, opts);
|
|
561
736
|
results.push(r);
|
|
562
|
-
if (r.status === 'ok')
|
|
737
|
+
if (r.status === 'ok') {
|
|
738
|
+
if (r.gate && r.gate.mode === 'explicit-unverified') {
|
|
739
|
+
out(' ⚠ ' + s.slug.padEnd(22) + '未执行装前扫描(explicit-unverified)');
|
|
740
|
+
}
|
|
741
|
+
out(' ✔ ' + s.slug.padEnd(22) + s.name + ' -> ' + (r.version || '?') + (existing ? '(原 v' + existing + ')' : '(新装)'));
|
|
742
|
+
}
|
|
563
743
|
else if (r.status === 'skip') out(' - ' + s.slug.padEnd(22) + s.name + ' (' + r.note + ')');
|
|
564
|
-
else {
|
|
744
|
+
else {
|
|
745
|
+
failed++;
|
|
746
|
+
exitCode = Math.max(exitCode, r.exitCode || 1);
|
|
747
|
+
out(' ✘ ' + s.slug.padEnd(22) + s.name + ' 失败: ' + r.note);
|
|
748
|
+
}
|
|
565
749
|
}
|
|
566
750
|
const ok = results.filter(r => r.status === 'ok').length;
|
|
567
751
|
const skip = results.filter(r => r.status === 'skip').length;
|
|
568
752
|
out('');
|
|
569
753
|
out('汇总: 更新 ' + ok + ' / 已是最新 ' + skip + ' / 失败 ' + failed);
|
|
570
|
-
if (failed > 0) process.exitCode = 1;
|
|
754
|
+
if (failed > 0) process.exitCode = exitCode || 1;
|
|
755
|
+
return { failed, exitCode };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// ── doctor / rollback ─────────────────────────────────────────────────────
|
|
759
|
+
function readInstalledManifest(target) {
|
|
760
|
+
const file = path.join(target, 'skill-manifest.json');
|
|
761
|
+
try {
|
|
762
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
763
|
+
} catch (_) {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function runCustomDoctor(skillDir, dest) {
|
|
769
|
+
const manifest = readInstalledManifest(skillDir);
|
|
770
|
+
if (!manifest || !manifest.install || !manifest.install.doctor) {
|
|
771
|
+
return { ok: true, skipped: true, error: null, result: null };
|
|
772
|
+
}
|
|
773
|
+
if (manifest.trust !== 'yottameta') {
|
|
774
|
+
return {
|
|
775
|
+
ok: false,
|
|
776
|
+
skipped: false,
|
|
777
|
+
error: 'manifest trust 不是 yottameta,拒绝执行自定义 doctor',
|
|
778
|
+
result: null,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
return lifecycleLib.runPhase(skillDir, manifest, 'doctor', {
|
|
782
|
+
skillDir,
|
|
783
|
+
packageDir: skillDir,
|
|
784
|
+
dest,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function doctorTargets(opts, dest) {
|
|
789
|
+
if (opts.slug) {
|
|
790
|
+
const skill = familySkillFor(opts.slug);
|
|
791
|
+
return skill ? [skill] : [];
|
|
792
|
+
}
|
|
793
|
+
if (opts.skills.length) {
|
|
794
|
+
return opts.skills.map((slug) => familySkillFor(slug)).filter(Boolean);
|
|
795
|
+
}
|
|
796
|
+
const found = [];
|
|
797
|
+
let entries;
|
|
798
|
+
try { entries = fs.readdirSync(dest, { withFileTypes: true }); } catch (_) { return found; }
|
|
799
|
+
for (const entry of entries) {
|
|
800
|
+
if (!entry.isDirectory() || !/^yotta-/.test(entry.name)) continue;
|
|
801
|
+
const skill = familySkillFor(entry.name);
|
|
802
|
+
if (skill) found.push(skill);
|
|
803
|
+
}
|
|
804
|
+
return found;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function doctorExitCode(payload) {
|
|
808
|
+
if (payload.ok) return 0;
|
|
809
|
+
const manifestFailure = payload.results.some((result) =>
|
|
810
|
+
(result.checks || []).some((check) =>
|
|
811
|
+
(check.id === 'manifest' || check.id === 'manifest_trust') && !check.ok));
|
|
812
|
+
return manifestFailure ? 6 : 1;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function runDoctor(opts, dest) {
|
|
816
|
+
const scan = require('../lib/skills-scan');
|
|
817
|
+
const registry = scan.readRegistry();
|
|
818
|
+
const targets = doctorTargets(opts, dest);
|
|
819
|
+
const results = [];
|
|
820
|
+
const errors = [];
|
|
821
|
+
const warnings = [];
|
|
822
|
+
const fixes = [];
|
|
823
|
+
|
|
824
|
+
if (targets.length === 0) {
|
|
825
|
+
errors.push(opts.slug ? '未找到技能: ' + opts.slug : '目标目录下没有可检查的元阁家族技能');
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
for (const skill of targets) {
|
|
829
|
+
const target = path.join(dest, skill.slug);
|
|
830
|
+
const result = healthLib.checkInstalledSkill({
|
|
831
|
+
slug: skill.slug,
|
|
832
|
+
target,
|
|
833
|
+
expectedVersion: skill.version || null,
|
|
834
|
+
expectedPackage: skill.pkg || null,
|
|
835
|
+
registry,
|
|
836
|
+
});
|
|
837
|
+
const custom = runCustomDoctor(target, dest);
|
|
838
|
+
result.custom_doctor = {
|
|
839
|
+
ok: !!custom.ok,
|
|
840
|
+
skipped: !!custom.skipped,
|
|
841
|
+
error: custom.error || null,
|
|
842
|
+
result: custom.result || null,
|
|
843
|
+
};
|
|
844
|
+
if (!custom.ok) {
|
|
845
|
+
result.ok = false;
|
|
846
|
+
result.errors.push('自定义 doctor 失败: ' + (custom.error || '未知错误'));
|
|
847
|
+
result.checks.push({
|
|
848
|
+
id: 'custom_doctor',
|
|
849
|
+
ok: false,
|
|
850
|
+
severity: 'error',
|
|
851
|
+
message: '自定义 doctor 失败: ' + (custom.error || '未知错误'),
|
|
852
|
+
hint: '修复技能包内的 doctor 脚本后重试',
|
|
853
|
+
});
|
|
854
|
+
} else if (!custom.skipped) {
|
|
855
|
+
result.checks.push({
|
|
856
|
+
id: 'custom_doctor',
|
|
857
|
+
ok: true,
|
|
858
|
+
severity: 'info',
|
|
859
|
+
message: '自定义 doctor 通过',
|
|
860
|
+
hint: null,
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
results.push(result);
|
|
864
|
+
errors.push.apply(errors, result.errors);
|
|
865
|
+
warnings.push.apply(warnings, result.warnings);
|
|
866
|
+
fixes.push.apply(fixes, result.fixes);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const payload = {
|
|
870
|
+
ok: errors.length === 0,
|
|
871
|
+
dir: dest,
|
|
872
|
+
checked: results.length,
|
|
873
|
+
results,
|
|
874
|
+
errors,
|
|
875
|
+
warnings,
|
|
876
|
+
fixes: Array.from(new Set(fixes)),
|
|
877
|
+
};
|
|
878
|
+
const code = doctorExitCode(payload);
|
|
879
|
+
if (opts.json) {
|
|
880
|
+
out(JSON.stringify(payload, null, 2));
|
|
881
|
+
} else {
|
|
882
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— doctor');
|
|
883
|
+
out('目标: ' + dest);
|
|
884
|
+
for (const result of results) {
|
|
885
|
+
out('');
|
|
886
|
+
out((result.ok ? '✔ ' : '✘ ') + result.slug + ' v' + (result.version || '未知'));
|
|
887
|
+
for (const check of result.checks) {
|
|
888
|
+
out(' ' + (check.ok ? '✔' : '✘') + ' ' + check.message + (check.hint ? '(修复: ' + check.hint + ')' : ''));
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
for (const item of warnings) out('[警告] ' + item);
|
|
892
|
+
for (const item of fixes) out('[建议] ' + item);
|
|
893
|
+
if (!payload.ok) out('doctor 未通过:请按上面的修复建议处理后重试。');
|
|
894
|
+
}
|
|
895
|
+
process.exitCode = code;
|
|
896
|
+
return payload;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function latestRollbackTarget(home) {
|
|
900
|
+
const file = path.join(home, '.yottaskills', 'install-log.jsonl');
|
|
901
|
+
let lines;
|
|
902
|
+
try { lines = fs.readFileSync(file, 'utf8').split(/\r?\n/).filter(Boolean); } catch (_) { return null; }
|
|
903
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
904
|
+
let entry;
|
|
905
|
+
try { entry = JSON.parse(lines[i]); } catch (_) { continue; }
|
|
906
|
+
if (!entry.skill || !entry.snapshot) continue;
|
|
907
|
+
if (!fs.existsSync(entry.snapshot)) continue;
|
|
908
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(entry.skill)) continue;
|
|
909
|
+
const expectedRoot = path.resolve(snapshotLib.snapshotRoot(home, entry.skill)) + path.sep;
|
|
910
|
+
if (!path.resolve(entry.snapshot).startsWith(expectedRoot)) continue;
|
|
911
|
+
const validation = snapshotLib.validateSnapshot(entry.snapshot);
|
|
912
|
+
if (validation.ok) return { slug: entry.skill, snapshot: entry.snapshot };
|
|
913
|
+
}
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function runRollback(opts, dest) {
|
|
918
|
+
const home = os.homedir();
|
|
919
|
+
const requestedSlug = opts.slug || (opts.skills.length ? opts.skills[0] : null);
|
|
920
|
+
|
|
921
|
+
if (opts.list) {
|
|
922
|
+
const rows = snapshotLib.listSnapshots(home, requestedSlug);
|
|
923
|
+
const payload = {
|
|
924
|
+
ok: true,
|
|
925
|
+
dir: dest || null,
|
|
926
|
+
slug: requestedSlug || null,
|
|
927
|
+
count: rows.filter((row) => row.valid).length,
|
|
928
|
+
snapshots: rows,
|
|
929
|
+
};
|
|
930
|
+
if (opts.json) out(JSON.stringify(payload, null, 2));
|
|
931
|
+
else {
|
|
932
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— rollback 快照');
|
|
933
|
+
for (const row of rows) {
|
|
934
|
+
out(' ' + (row.valid ? '✔' : '✘') + ' ' + row.slug + ' v' + (row.version || '未知') + ' ' + row.path + (row.reason ? '(' + row.reason + ')' : ''));
|
|
935
|
+
}
|
|
936
|
+
out('可用快照: ' + payload.count + ' / ' + rows.length);
|
|
937
|
+
}
|
|
938
|
+
return payload;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
let slug = requestedSlug;
|
|
942
|
+
let selected = null;
|
|
943
|
+
if (slug) {
|
|
944
|
+
const rows = snapshotLib.listSnapshots(home, slug);
|
|
945
|
+
selected = rows.find((row) => row.valid) || rows[0] || null;
|
|
946
|
+
} else {
|
|
947
|
+
const latest = latestRollbackTarget(home);
|
|
948
|
+
if (latest) {
|
|
949
|
+
slug = latest.slug;
|
|
950
|
+
selected = snapshotLib.listSnapshots(home, slug).find((row) => row.path === latest.snapshot && row.valid) || null;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (!slug) {
|
|
955
|
+
const payload = { ok: false, dir: dest, slug: null, errors: ['安装记录中没有可回滚的技能快照'] };
|
|
956
|
+
if (opts.json) out(JSON.stringify(payload, null, 2));
|
|
957
|
+
else out('回滚失败:安装记录中没有可回滚的技能快照。');
|
|
958
|
+
process.exitCode = 1;
|
|
959
|
+
return payload;
|
|
960
|
+
}
|
|
961
|
+
if (!selected || !selected.valid) {
|
|
962
|
+
const payload = {
|
|
963
|
+
ok: false,
|
|
964
|
+
dir: dest,
|
|
965
|
+
slug,
|
|
966
|
+
errors: [selected && selected.reason ? '选中的快照不可用: ' + selected.reason : '没有可用快照'],
|
|
967
|
+
};
|
|
968
|
+
if (opts.json) out(JSON.stringify(payload, null, 2));
|
|
969
|
+
else out('回滚失败:' + payload.errors[0]);
|
|
970
|
+
process.exitCode = 1;
|
|
971
|
+
return payload;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const target = path.join(dest, slug);
|
|
975
|
+
const restored = snapshotLib.restoreSnapshot(selected.path, target);
|
|
976
|
+
if (!restored.ok) {
|
|
977
|
+
const payload = { ok: false, dir: dest, slug, snapshot: selected.path, errors: [restored.error] };
|
|
978
|
+
if (opts.json) out(JSON.stringify(payload, null, 2));
|
|
979
|
+
else out('回滚失败:' + restored.error);
|
|
980
|
+
process.exitCode = 1;
|
|
981
|
+
return payload;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const family = familySkillFor(slug);
|
|
985
|
+
const scan = require('../lib/skills-scan');
|
|
986
|
+
const doctor = healthLib.checkInstalledSkill({
|
|
987
|
+
slug,
|
|
988
|
+
target,
|
|
989
|
+
expectedVersion: restored.version && restored.version !== 'unknown' ? restored.version : null,
|
|
990
|
+
expectedPackage: family ? family.pkg : null,
|
|
991
|
+
registry: scan.readRegistry(),
|
|
992
|
+
});
|
|
993
|
+
const custom = runCustomDoctor(target, dest);
|
|
994
|
+
if (!custom.ok) {
|
|
995
|
+
doctor.ok = false;
|
|
996
|
+
doctor.errors.push('自定义 doctor 失败: ' + (custom.error || '未知错误'));
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const payload = {
|
|
1000
|
+
ok: doctor.ok,
|
|
1001
|
+
dir: dest,
|
|
1002
|
+
slug,
|
|
1003
|
+
snapshot: selected.path,
|
|
1004
|
+
version: restored.version,
|
|
1005
|
+
doctor,
|
|
1006
|
+
errors: doctor.errors,
|
|
1007
|
+
warnings: doctor.warnings,
|
|
1008
|
+
fixes: doctor.fixes,
|
|
1009
|
+
reindexed: false,
|
|
1010
|
+
};
|
|
1011
|
+
try {
|
|
1012
|
+
evidenceLib.appendEvidence({
|
|
1013
|
+
event: 'rollback',
|
|
1014
|
+
skill: slug,
|
|
1015
|
+
package: family ? family.pkg : null,
|
|
1016
|
+
version: restored.version,
|
|
1017
|
+
decision: doctor.ok ? 'ok' : 'fail',
|
|
1018
|
+
snapshot: selected.path,
|
|
1019
|
+
}, { homeDir: home });
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
payload.ok = false;
|
|
1022
|
+
payload.errors.push('回滚证据写入失败: ' + error.message);
|
|
1023
|
+
}
|
|
1024
|
+
if (payload.ok && !opts.noReindex) {
|
|
1025
|
+
try {
|
|
1026
|
+
reindexRegistry({ ...opts, dir: dest });
|
|
1027
|
+
payload.reindexed = true;
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
payload.warnings.push('回滚完成,但注册表重扫失败: ' + error.message);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (opts.json) {
|
|
1034
|
+
out(JSON.stringify(payload, null, 2));
|
|
1035
|
+
} else {
|
|
1036
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— rollback');
|
|
1037
|
+
out('技能: ' + slug);
|
|
1038
|
+
out('快照: ' + selected.path);
|
|
1039
|
+
out('结果: ' + (payload.ok ? '✔ 已恢复 v' + (restored.version || '未知') : '✘ 恢复后 doctor 未通过'));
|
|
1040
|
+
if (payload.errors.length) out('错误: ' + payload.errors.join('; '));
|
|
1041
|
+
if (payload.warnings.length) out('警告: ' + payload.warnings.join('; '));
|
|
1042
|
+
if (payload.reindexed) out('已重扫本地技能注册表。');
|
|
1043
|
+
}
|
|
1044
|
+
process.exitCode = payload.ok ? 0 : 1;
|
|
1045
|
+
return payload;
|
|
571
1046
|
}
|
|
572
1047
|
|
|
573
1048
|
// ── 展示 ───────────────────────────────────────────────────────────────────
|
|
@@ -592,10 +1067,18 @@ function printHelp() {
|
|
|
592
1067
|
out(' yotta-skills install --dir <path> 装全家到指定目录');
|
|
593
1068
|
out(' yotta-skills install <skill> --dir <path> 装单个技能(可多个)');
|
|
594
1069
|
out(' yotta-skills update --agent <name> 增量更新已装技能(补齐缺失 / 版本不一致)');
|
|
1070
|
+
out(' yotta-skills update --check 只读联网检查更新(退出码 0/3/1)');
|
|
1071
|
+
out(' yotta-skills update --check --scheduled 后台周检(未到期不联网;到期单次检查并写缓存)');
|
|
1072
|
+
out(' yotta-skills doctor --dir <path> 只读自检技能目录(可加 --slug / --json)');
|
|
1073
|
+
out(' yotta-skills rollback --dir <path> 回滚最近一次技能安装或更新(--list 查看快照)');
|
|
595
1074
|
out(' yotta-skills --dry-run 预览将安装清单(不联网、不改动)');
|
|
596
1075
|
out(' yotta-skills --inventory 盘点本机已装技能(自研扫描,不依赖任何元技能)');
|
|
597
1076
|
out(' yotta-skills --reindex 重扫注册表(会话开工 / 装技能后自动调用;增量合并)');
|
|
598
1077
|
out(' yotta-skills --route "<需求摘要>" 给出场景组合、调用顺序、缺失技能安装建议');
|
|
1078
|
+
out(' yotta-skills hook capabilities 查看宿主六事件能力矩阵');
|
|
1079
|
+
out(' yotta-skills hook evaluate --event <event> --manifest <file> 评估 hook 声明并留证');
|
|
1080
|
+
out(' yotta-skills hook bind --manifest <file> 注册 hook 声明(幂等)');
|
|
1081
|
+
out(' yotta-skills hook unbind <binding-id> 反注册 hook 声明');
|
|
599
1082
|
out('');
|
|
600
1083
|
out('选项:');
|
|
601
1084
|
out(' --agent <name> 智能体键名(--list 可查看;未知智能体请用 --dir)');
|
|
@@ -606,11 +1089,17 @@ function printHelp() {
|
|
|
606
1089
|
out(' --npm <path> 指定 npm 可执行文件(默认 npm / npm.cmd)');
|
|
607
1090
|
out(' --python <path> 指定 python 可执行文件(元信 scan 用)');
|
|
608
1091
|
out(' --verify <path> 指定 yotta_verify.py 路径(默认找目标目录已装的元信)');
|
|
609
|
-
out(' --json inventory / reindex / route 时输出 JSON');
|
|
1092
|
+
out(' --json inventory / reindex / route / doctor / rollback 时输出 JSON');
|
|
1093
|
+
out(' --slug <slug> doctor / rollback 时只处理指定技能');
|
|
610
1094
|
out(' --route <需求> 静态编排路由(输出组合 / 顺序 / 依据 / 缺失技能建议)');
|
|
611
1095
|
out(' --check update 时仅只读检查更新(联网对 npm 最新版本,不改动;退出码 0/3/1)');
|
|
1096
|
+
out(' --scheduled 与 update --check 合用:后台周检入口(未到期不联网;到期单次检查并写缓存)');
|
|
612
1097
|
out(' --auto update 时检查到家族更新后自动更新(仅 yotta-* 家族,含装前扫描)');
|
|
613
1098
|
out(' --registry <url> npm registry 地址(默认 https://registry.npmjs.org/;YOTTA_SKILLS_REGISTRY 覆盖)');
|
|
1099
|
+
out(' --host <name> hook 适配宿主名(默认 generic;当前已实测 codex)');
|
|
1100
|
+
out(' --event <event> hook 六事件之一(before_start / before_tool / before_install / before_publish / after_milestone / before_send)');
|
|
1101
|
+
out(' --manifest <file> hook evaluate / bind 使用的 skill-manifest.json 路径');
|
|
1102
|
+
out(' --context <json> hook evaluate 的检查结果 JSON(checks / wrapperRegistered / evidence)');
|
|
614
1103
|
out(' --project inventory / reindex 时附加扫描当前项目 .agents/skills / .codex/skills');
|
|
615
1104
|
out(' --no-reindex 安装 / 更新后不自动重扫注册表');
|
|
616
1105
|
out(' -h, --help 帮助');
|
|
@@ -735,20 +1224,121 @@ function maybeAutoReindex(opts, dest) {
|
|
|
735
1224
|
}
|
|
736
1225
|
}
|
|
737
1226
|
|
|
1227
|
+
// ── 运行时 hook 适配层(P0-3) ───────────────────────────────────────────────
|
|
1228
|
+
function readJsonFile(file, label) {
|
|
1229
|
+
try {
|
|
1230
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
die(label + ' 读取失败: ' + error.message, 2, '请确认文件存在且为合法 JSON。');
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function runHook(opts) {
|
|
1237
|
+
const action = opts.rest[0] || 'capabilities';
|
|
1238
|
+
const host = opts.host || 'generic';
|
|
1239
|
+
const adapter = hookAdapterLib.createHookAdapter({ host });
|
|
1240
|
+
|
|
1241
|
+
if (action === 'capabilities') {
|
|
1242
|
+
const payload = { host, capabilities: adapter.capabilities() };
|
|
1243
|
+
if (opts.json) out(JSON.stringify(payload, null, 2));
|
|
1244
|
+
else {
|
|
1245
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— hook capabilities');
|
|
1246
|
+
out('宿主: ' + host);
|
|
1247
|
+
hookAdapterLib.EVENTS.forEach((event) => out(' ' + event.padEnd(18) + payload.capabilities[event]));
|
|
1248
|
+
}
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
if (action === 'evaluate') {
|
|
1253
|
+
if (!opts.event) die('hook evaluate 缺少 --event', 2, '请提供六个统一事件之一。');
|
|
1254
|
+
if (!opts.manifest) die('hook evaluate 缺少 --manifest', 2, '请提供 skill-manifest.json 路径。');
|
|
1255
|
+
const manifest = readJsonFile(opts.manifest, 'manifest');
|
|
1256
|
+
let context = {};
|
|
1257
|
+
if (opts.context) {
|
|
1258
|
+
try {
|
|
1259
|
+
context = JSON.parse(opts.context);
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
die('--context 不是合法 JSON: ' + error.message, 2, '请传入 JSON 对象。');
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
if (!context || typeof context !== 'object' || Array.isArray(context)) {
|
|
1265
|
+
die('--context 必须是 JSON 对象', 2);
|
|
1266
|
+
}
|
|
1267
|
+
const result = adapter.evaluate(opts.event, manifest, context);
|
|
1268
|
+
const entries = result.results.length > 0
|
|
1269
|
+
? result.results
|
|
1270
|
+
: [{ event: opts.event, skill: manifest.slug, action: null, decision: result.decision, evidence: {}, capability: adapter.capabilities()[opts.event] || 'unsupported', reason: result.user_message }];
|
|
1271
|
+
for (const entry of entries) {
|
|
1272
|
+
hookAdapterLib.appendHookEvidence({
|
|
1273
|
+
event: entry.event,
|
|
1274
|
+
skill: entry.skill,
|
|
1275
|
+
action: entry.action,
|
|
1276
|
+
result: entry.decision,
|
|
1277
|
+
capability: entry.capability,
|
|
1278
|
+
evidence: entry.evidence,
|
|
1279
|
+
reason: entry.reason,
|
|
1280
|
+
}, { homeDir: os.homedir() });
|
|
1281
|
+
}
|
|
1282
|
+
if (opts.json) out(JSON.stringify({ host, event: opts.event, ...result }, null, 2));
|
|
1283
|
+
else {
|
|
1284
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— hook evaluate');
|
|
1285
|
+
out('宿主: ' + host + ' / 事件: ' + opts.event);
|
|
1286
|
+
out('决策: ' + result.decision + ' / verified: ' + result.verified);
|
|
1287
|
+
out(result.user_message);
|
|
1288
|
+
for (const entry of result.results) out(' ' + entry.action + ': ' + entry.decision + '(' + entry.capability + ')');
|
|
1289
|
+
}
|
|
1290
|
+
process.exitCode = result.decision === 'block' ? 3 : 0;
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
if (action === 'bind') {
|
|
1295
|
+
if (!opts.manifest) die('hook bind 缺少 --manifest', 2, '请提供 skill-manifest.json 路径。');
|
|
1296
|
+
const manifest = readJsonFile(opts.manifest, 'manifest');
|
|
1297
|
+
const bindings = adapter.bind(manifest, opts.event || null);
|
|
1298
|
+
if (opts.json) out(JSON.stringify({ host, bindings }, null, 2));
|
|
1299
|
+
else {
|
|
1300
|
+
out('yotta-skills(元阁)v' + VERSION + ' —— hook bind');
|
|
1301
|
+
out('宿主: ' + host + ' / 绑定 ' + bindings.length + ' 项');
|
|
1302
|
+
for (const binding of bindings) out(' ' + binding.id + ' ' + binding.event + ' ' + binding.action + ' ' + binding.capability);
|
|
1303
|
+
}
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
if (action === 'unbind') {
|
|
1308
|
+
const id = opts.rest[1];
|
|
1309
|
+
if (!id) die('hook unbind 缺少 binding id', 2, '先用 hook bind / list 获取 id。');
|
|
1310
|
+
const removed = adapter.unbind(id);
|
|
1311
|
+
if (opts.json) out(JSON.stringify({ host, id, removed }, null, 2));
|
|
1312
|
+
else out(removed ? '已解除绑定: ' + id : '未找到绑定: ' + id);
|
|
1313
|
+
process.exitCode = removed ? 0 : 1;
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
die('未知 hook 子命令: ' + action, 2, '支持 capabilities / evaluate / bind / unbind。');
|
|
1318
|
+
}
|
|
1319
|
+
|
|
738
1320
|
// ── main ───────────────────────────────────────────────────────────────────
|
|
739
1321
|
function main() {
|
|
740
1322
|
const opts = parseArgs(process.argv.slice(2));
|
|
741
1323
|
if (opts.help) { printHelp(); return; }
|
|
742
1324
|
if (opts.version) { out('yotta-skills v' + VERSION); return; }
|
|
743
|
-
if (opts.list) { printList(opts); return; }
|
|
1325
|
+
if (opts.list && !opts.command) { printList(opts); return; }
|
|
744
1326
|
if (opts.inventory && !opts.command) { runInventory(opts); return; }
|
|
745
1327
|
if (opts.reindex && !opts.command) { runReindex(opts); return; }
|
|
746
1328
|
if (opts.route && !opts.command) { runRoute(opts); return; }
|
|
747
1329
|
|
|
748
1330
|
const command = opts.command || 'install';
|
|
1331
|
+
if (command === 'hook') {
|
|
1332
|
+
runHook(opts);
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
if (command === 'rollback' && opts.list) {
|
|
1336
|
+
runRollback(opts, null);
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
749
1339
|
let dest = resolveTargetDir(opts);
|
|
750
1340
|
if (!dest && !opts.dryRun) dest = detectProjectDir();
|
|
751
|
-
if (!dest && command === 'install' && !opts.dryRun) {
|
|
1341
|
+
if (!dest && (command === 'install' || command === 'doctor' || command === 'rollback') && !opts.dryRun) {
|
|
752
1342
|
die('未指定目标:请用 --agent <name> 或 --dir <path>(当前目录未检测到项目级技能目录)。', 4, '未收录智能体也可用 --dir 指定其技能目录。');
|
|
753
1343
|
}
|
|
754
1344
|
|
|
@@ -767,22 +1357,23 @@ function main() {
|
|
|
767
1357
|
return;
|
|
768
1358
|
}
|
|
769
1359
|
|
|
770
|
-
if (command === '
|
|
1360
|
+
if (command === 'doctor') {
|
|
1361
|
+
runDoctor(opts, dest);
|
|
1362
|
+
} else if (command === 'rollback') {
|
|
1363
|
+
runRollback(opts, dest);
|
|
1364
|
+
} else if (command === 'update') {
|
|
771
1365
|
if (opts.check || opts.auto) {
|
|
772
1366
|
var runFn = opts.auto ? runUpdateAuto : runUpdateCheck;
|
|
773
1367
|
runFn(opts, dest).then(function (r) {
|
|
774
|
-
if (opts.json && !opts.auto) {
|
|
775
|
-
out(JSON.stringify({ dest: dest, updatable: r.rows.filter(function (x) { return x.hasUpdate; }), updates: r.updates, latest: r.latest, failed: r.failed, nonFamily: r.nonFamily }, null, 2));
|
|
776
|
-
}
|
|
777
1368
|
process.exitCode = r.code;
|
|
778
1369
|
});
|
|
779
1370
|
} else {
|
|
780
|
-
runUpdate(opts, dest);
|
|
781
|
-
maybeAutoReindex(opts, dest);
|
|
1371
|
+
const result = runUpdate(opts, dest);
|
|
1372
|
+
if (!result.failed) maybeAutoReindex(opts, dest);
|
|
782
1373
|
}
|
|
783
1374
|
} else {
|
|
784
|
-
runInstall(opts, dest);
|
|
785
|
-
maybeAutoReindex(opts, dest);
|
|
1375
|
+
const result = runInstall(opts, dest);
|
|
1376
|
+
if (!result.failed) maybeAutoReindex(opts, dest);
|
|
786
1377
|
}
|
|
787
1378
|
}
|
|
788
1379
|
|