@yottameta/yotta-skills 0.8.0 → 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/CHANGELOG.md +97 -0
- package/README.md +23 -3
- package/README.zh-CN.md +24 -3
- package/SKILL.md +72 -5
- package/bin/yotta-skills.js +555 -20
- package/lib/hook-adapter.js +385 -0
- package/lib/install-health.js +151 -0
- package/lib/install-lifecycle.js +149 -0
- package/lib/install-pipeline.js +190 -15
- package/lib/install-snapshot.js +245 -0
- package/lib/manifest.js +9 -4
- package/lib/update-check.js +99 -0
- package/package.json +2 -1
- package/references/faq.md +22 -3
- package/references/install-flow.md +50 -1
- package/references/skill-list.md +10 -10
- package/references/tutorial.md +30 -0
- package/references/walkthroughs.md +11 -3
- package/scripts/yotta-skills-mcp.py +1 -1
- package/skill-manifest.json +34 -0
- package/skills.json +10 -10
package/bin/yotta-skills.js
CHANGED
|
@@ -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 锁死精确版本。
|
|
@@ -26,10 +29,15 @@ const http = require('http');
|
|
|
26
29
|
const https = require('https');
|
|
27
30
|
const evidenceLib = require('../lib/install-evidence');
|
|
28
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');
|
|
29
37
|
const { createInstaller, isSafeTarEntry } = require('../lib/install-pipeline');
|
|
30
38
|
|
|
31
39
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
32
|
-
let VERSION = '0.
|
|
40
|
+
let VERSION = '0.19.0';
|
|
33
41
|
try { VERSION = require(path.join(PKG_ROOT, 'package.json')).version; } catch (_) { /* keep fallback */ }
|
|
34
42
|
|
|
35
43
|
function loadManifest() {
|
|
@@ -140,7 +148,8 @@ function parseArgs(argv) {
|
|
|
140
148
|
help: false, version: false, agent: null, dir: null, npm: null,
|
|
141
149
|
python: null, verify: null, command: null, skill: null, rest: [],
|
|
142
150
|
inventory: false, reindex: false, noReindex: false, json: false, project: false, route: null,
|
|
143
|
-
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,
|
|
144
153
|
};
|
|
145
154
|
const positionals = [];
|
|
146
155
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -169,15 +178,27 @@ function parseArgs(argv) {
|
|
|
169
178
|
else if (a === '--npm') opts.npm = take('--npm');
|
|
170
179
|
else if (a === '--python') opts.python = take('--python');
|
|
171
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
|
+
}
|
|
172
188
|
else if (a === '--check') opts.check = true;
|
|
173
189
|
else if (a === '--auto') opts.auto = true;
|
|
190
|
+
else if (a === '--scheduled') opts.scheduled = true;
|
|
174
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');
|
|
175
196
|
else if (a.startsWith('-')) die('未知参数: ' + a, 2, '可用 --help 查看支持的选项。');
|
|
176
197
|
else positionals.push(a);
|
|
177
198
|
}
|
|
178
|
-
// 命令解析:install / update,其余位置参数 = 技能 slug(可多个)
|
|
199
|
+
// 命令解析:install / update / doctor / rollback,其余位置参数 = 技能 slug(可多个)
|
|
179
200
|
for (const p of positionals) {
|
|
180
|
-
if (p === 'install' || p === 'update') {
|
|
201
|
+
if (p === 'install' || p === 'update' || p === 'doctor' || p === 'rollback' || p === 'hook') {
|
|
181
202
|
if (opts.command && opts.command !== p) die('命令冲突:' + opts.command + ' 与 ' + p);
|
|
182
203
|
opts.command = p;
|
|
183
204
|
} else {
|
|
@@ -187,6 +208,9 @@ function parseArgs(argv) {
|
|
|
187
208
|
// 直接给 slug 且无命令 → 视为 install 单个/多个
|
|
188
209
|
if (!opts.command && opts.rest.length > 0) opts.command = 'install';
|
|
189
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
|
+
}
|
|
190
214
|
return opts;
|
|
191
215
|
}
|
|
192
216
|
|
|
@@ -287,15 +311,69 @@ async function runUpdateCheck(opts, dest) {
|
|
|
287
311
|
var lines = [];
|
|
288
312
|
var quiet = !!opts.json;
|
|
289
313
|
function say(s) { if (!quiet) lines.push(s); }
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
}
|
|
293
351
|
var installed = scanInstalledSlugs(dest);
|
|
294
352
|
if (installed.length === 0) {
|
|
295
|
-
|
|
296
|
-
|
|
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
|
+
};
|
|
297
374
|
}
|
|
298
375
|
var rows = [];
|
|
376
|
+
var failures = [];
|
|
299
377
|
var updates = 0, latest = 0, failed = 0, nonFamily = 0;
|
|
300
378
|
for (var i = 0; i < installed.length; i++) {
|
|
301
379
|
var it = installed[i];
|
|
@@ -308,6 +386,7 @@ async function runUpdateCheck(opts, dest) {
|
|
|
308
386
|
var res = await fetchRegistryLatest(fam.pkg, opts);
|
|
309
387
|
if (!res.ok) {
|
|
310
388
|
failed++;
|
|
389
|
+
failures.push({ slug: it.slug, error: res.error });
|
|
311
390
|
say(' ✘ ' + it.slug.padEnd(22) + '检查失败: ' + res.error);
|
|
312
391
|
continue;
|
|
313
392
|
}
|
|
@@ -326,11 +405,61 @@ async function runUpdateCheck(opts, dest) {
|
|
|
326
405
|
}
|
|
327
406
|
rows.push({ slug: it.slug, installed: cur, latest: current, pkg: fam.pkg, hasUpdate: !isUp, family: fam });
|
|
328
407
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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,不阻断检查结果 */ }
|
|
332
423
|
var code = (failed > 0) ? 1 : (updates > 0 ? 3 : 0);
|
|
333
|
-
|
|
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 };
|
|
334
463
|
}
|
|
335
464
|
|
|
336
465
|
async function runUpdateAuto(opts, dest) {
|
|
@@ -626,6 +755,296 @@ function runUpdate(opts, dest) {
|
|
|
626
755
|
return { failed, exitCode };
|
|
627
756
|
}
|
|
628
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;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
629
1048
|
// ── 展示 ───────────────────────────────────────────────────────────────────
|
|
630
1049
|
function printList(opts) {
|
|
631
1050
|
const skills = opts.skills.length ? selectSkills(opts) : MANIFEST;
|
|
@@ -648,10 +1067,18 @@ function printHelp() {
|
|
|
648
1067
|
out(' yotta-skills install --dir <path> 装全家到指定目录');
|
|
649
1068
|
out(' yotta-skills install <skill> --dir <path> 装单个技能(可多个)');
|
|
650
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 查看快照)');
|
|
651
1074
|
out(' yotta-skills --dry-run 预览将安装清单(不联网、不改动)');
|
|
652
1075
|
out(' yotta-skills --inventory 盘点本机已装技能(自研扫描,不依赖任何元技能)');
|
|
653
1076
|
out(' yotta-skills --reindex 重扫注册表(会话开工 / 装技能后自动调用;增量合并)');
|
|
654
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 声明');
|
|
655
1082
|
out('');
|
|
656
1083
|
out('选项:');
|
|
657
1084
|
out(' --agent <name> 智能体键名(--list 可查看;未知智能体请用 --dir)');
|
|
@@ -662,11 +1089,17 @@ function printHelp() {
|
|
|
662
1089
|
out(' --npm <path> 指定 npm 可执行文件(默认 npm / npm.cmd)');
|
|
663
1090
|
out(' --python <path> 指定 python 可执行文件(元信 scan 用)');
|
|
664
1091
|
out(' --verify <path> 指定 yotta_verify.py 路径(默认找目标目录已装的元信)');
|
|
665
|
-
out(' --json inventory / reindex / route 时输出 JSON');
|
|
1092
|
+
out(' --json inventory / reindex / route / doctor / rollback 时输出 JSON');
|
|
1093
|
+
out(' --slug <slug> doctor / rollback 时只处理指定技能');
|
|
666
1094
|
out(' --route <需求> 静态编排路由(输出组合 / 顺序 / 依据 / 缺失技能建议)');
|
|
667
1095
|
out(' --check update 时仅只读检查更新(联网对 npm 最新版本,不改动;退出码 0/3/1)');
|
|
1096
|
+
out(' --scheduled 与 update --check 合用:后台周检入口(未到期不联网;到期单次检查并写缓存)');
|
|
668
1097
|
out(' --auto update 时检查到家族更新后自动更新(仅 yotta-* 家族,含装前扫描)');
|
|
669
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)');
|
|
670
1103
|
out(' --project inventory / reindex 时附加扫描当前项目 .agents/skills / .codex/skills');
|
|
671
1104
|
out(' --no-reindex 安装 / 更新后不自动重扫注册表');
|
|
672
1105
|
out(' -h, --help 帮助');
|
|
@@ -791,20 +1224,121 @@ function maybeAutoReindex(opts, dest) {
|
|
|
791
1224
|
}
|
|
792
1225
|
}
|
|
793
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
|
+
|
|
794
1320
|
// ── main ───────────────────────────────────────────────────────────────────
|
|
795
1321
|
function main() {
|
|
796
1322
|
const opts = parseArgs(process.argv.slice(2));
|
|
797
1323
|
if (opts.help) { printHelp(); return; }
|
|
798
1324
|
if (opts.version) { out('yotta-skills v' + VERSION); return; }
|
|
799
|
-
if (opts.list) { printList(opts); return; }
|
|
1325
|
+
if (opts.list && !opts.command) { printList(opts); return; }
|
|
800
1326
|
if (opts.inventory && !opts.command) { runInventory(opts); return; }
|
|
801
1327
|
if (opts.reindex && !opts.command) { runReindex(opts); return; }
|
|
802
1328
|
if (opts.route && !opts.command) { runRoute(opts); return; }
|
|
803
1329
|
|
|
804
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
|
+
}
|
|
805
1339
|
let dest = resolveTargetDir(opts);
|
|
806
1340
|
if (!dest && !opts.dryRun) dest = detectProjectDir();
|
|
807
|
-
if (!dest && command === 'install' && !opts.dryRun) {
|
|
1341
|
+
if (!dest && (command === 'install' || command === 'doctor' || command === 'rollback') && !opts.dryRun) {
|
|
808
1342
|
die('未指定目标:请用 --agent <name> 或 --dir <path>(当前目录未检测到项目级技能目录)。', 4, '未收录智能体也可用 --dir 指定其技能目录。');
|
|
809
1343
|
}
|
|
810
1344
|
|
|
@@ -823,13 +1357,14 @@ function main() {
|
|
|
823
1357
|
return;
|
|
824
1358
|
}
|
|
825
1359
|
|
|
826
|
-
if (command === '
|
|
1360
|
+
if (command === 'doctor') {
|
|
1361
|
+
runDoctor(opts, dest);
|
|
1362
|
+
} else if (command === 'rollback') {
|
|
1363
|
+
runRollback(opts, dest);
|
|
1364
|
+
} else if (command === 'update') {
|
|
827
1365
|
if (opts.check || opts.auto) {
|
|
828
1366
|
var runFn = opts.auto ? runUpdateAuto : runUpdateCheck;
|
|
829
1367
|
runFn(opts, dest).then(function (r) {
|
|
830
|
-
if (opts.json && !opts.auto) {
|
|
831
|
-
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));
|
|
832
|
-
}
|
|
833
1368
|
process.exitCode = r.code;
|
|
834
1369
|
});
|
|
835
1370
|
} else {
|