@yottameta/yotta-skills-plugin 0.29.4 → 0.29.5
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 +8 -4
- package/skills/yotta-skills/assets/view.html +191 -11
- package/skills/yotta-skills/bin/yotta-skills.js +277 -86
- package/skills/yotta-skills/lib/cli-help.js +7 -2
- package/skills/yotta-skills/lib/hub-adopt.js +92 -23
- package/skills/yotta-skills/lib/hub-migrate.js +212 -1
- package/skills/yotta-skills/lib/hub-targets.js +85 -0
- package/skills/yotta-skills/lib/hub-view-server.js +238 -7
- package/skills/yotta-skills/lib/hub.js +198 -9
- package/skills/yotta-skills/lib/install-pipeline.js +53 -5
- package/skills/yotta-skills/lib/npm-pack.js +3 -0
- package/skills/yotta-skills/lib/skills-config.js +99 -5
- package/skills/yotta-skills/lib/skills-scan.js +3 -0
- package/skills/yotta-skills/references/hub.md +23 -4
- package/skills/yotta-skills/scripts/yotta-skills-mcp.py +1 -1
|
@@ -37,11 +37,12 @@ const CLI_GLOBAL_OPTIONS = [
|
|
|
37
37
|
{ flag: '--context', arg: '<json>', what: 'hook evaluate 的检查结果 JSON', when: '外部传入 checks / wrapperRegistered / evidence 时', caution: '' },
|
|
38
38
|
{ flag: '--skill', arg: '<slug>', what: 'usage mark 的技能 slug', when: '记录技能使用信号时', caution: '' },
|
|
39
39
|
{ flag: '--signal', arg: '<name>', what: 'usage mark 的信号:used / named / accepted', when: '记录技能使用信号时', caution: '' },
|
|
40
|
-
{ flag: '--yes', arg: '', what: 'usage reset
|
|
40
|
+
{ flag: '--yes', arg: '', what: '确认开关(usage reset / hub hosts remove --purge / hub cleanup-backups / hub config rollback)', when: '确认执行破坏性或迁移类操作时', caution: '默认都是预览;--yes 才真正执行' },
|
|
41
41
|
{ flag: '--explain', arg: '', what: 'decide-memory 文本报告追加信号明细', when: '想看裁决依据时', caution: '' },
|
|
42
42
|
{ flag: '--promote', arg: '', what: 'decide-memory 只写本地建议文件(不写元忆)', when: '想留下裁决建议文件时', caution: '不会写元忆、不会删除任何内容' },
|
|
43
43
|
{ flag: '--hub', arg: '<path>', what: 'Hub 目录(默认 ~/.yottaskills/hub;持久配置见 hub config)', when: '使用自定义 Hub 位置时', caution: '覆盖顺序:--hub > YOTTA_SKILLS_HUB > config.json > 默认' },
|
|
44
44
|
{ flag: '--move', arg: '', what: 'hub config set 迁移:复制校验旧 Hub → 重链宿主 → 旧 Hub 入回收站(7 天)', when: '换 Hub 位置并带上已有技能时', caution: 'fail-closed:校验失败不切配置;重链失败保留旧 Hub' },
|
|
45
|
+
{ flag: '--clean-residue', arg: '', what: 'hub config set --move / rollback:目标只含空 Hub 台账残留时先移入回收站再迁移', when: '迁移 / 回滚目标目录有 .yotta-hub.json 残留时', caution: '只清理元阁自己的空台账残留,不合并其它内容' },
|
|
45
46
|
{ flag: '--port', arg: '<n>', what: 'view 面板端口(默认 8789)', when: '端口冲突或想固定端口时', caution: '仅 127.0.0.1 监听' },
|
|
46
47
|
{ flag: '--all', arg: '', what: 'hub link / unlink 作用于全部已核实宿主', when: '全量分发或解除时', caution: '' },
|
|
47
48
|
{ flag: '--include-discovered', arg: '', what: 'hub link / unlink / remove 时纳入自动发现目录', when: '确认要扩围到未核实目录时', caution: '默认只链已核实宿主;桥接目录永不链接' },
|
|
@@ -185,8 +186,10 @@ const CLI_HELP_MODEL = [
|
|
|
185
186
|
{ name: 'hosts include', usage: 'hub hosts include <agentId|dir> [--json]', what: '恢复接管(移出「不接管」名单)', when: '重新纳入发现 / 链接时', options: ['--json'] },
|
|
186
187
|
{ name: 'hosts set', usage: 'hub hosts set <agentId> --dir <dir> [--label <名称>] [--json] | hub hosts set <agentId> --clear', what: '目录覆盖:用户级覆盖内置映射;--clear 恢复默认', when: '宿主技能目录与内置映射不一致时', options: ['--dir', '--label', '--clear', '--json'] },
|
|
187
188
|
{ name: 'config get', usage: 'hub config get [--json]', what: '查看生效 Hub 位置与来源(--hub / YOTTA_SKILLS_HUB / config.json / 默认)', when: '确认当前 Hub 与配置覆盖时', options: ['--json'] },
|
|
188
|
-
{ name: 'config set', usage: 'hub config set --hub <path> [--move]', what: '持久化 Hub 位置;--move 迁移旧 Hub(复制校验 → 重链 → 回收站 7
|
|
189
|
+
{ name: 'config set', usage: 'hub config set --hub <path> [--move [--clean-residue]]', what: '持久化 Hub 位置;--move 迁移旧 Hub(复制校验 → 重链 → 回收站 7 天);未迁移时记录原位置供面板提示', when: '换 Hub 存储位置时', options: ['--hub', '--move', '--clean-residue', '--json'] },
|
|
190
|
+
{ name: 'config rollback', usage: 'hub config rollback [--yes] [--clean-residue]', what: '回滚到上次迁移前的 Hub 位置(位置回退,用当前内容;反向迁移同内核)', when: '迁移后想退回原位置时', options: ['--yes', '--clean-residue', '--json'] },
|
|
189
191
|
{ name: 'config clear', usage: 'hub config clear [--json]', what: '清除配置覆盖,恢复 flag > env > 默认', when: '撤销持久 Hub 位置时', options: ['--json'] },
|
|
192
|
+
{ name: 'cleanup-backups', usage: 'hub cleanup-backups [--yes] [--include-discovered]', what: '清理宿主 / Hub 里的 .yottaskills-backup-* / .yottaskills-staging 历史残留(移入回收站 7 天)', when: '宿主把备份当技能扫描、或迁移后要清残留时', options: ['--yes', '--include-discovered', '--dir', '--agent', '--json'] },
|
|
190
193
|
{ name: 'install', usage: 'hub install [skill...]', what: '把元技能装进 Hub 真源(清单 27 + 特殊家族 5)', when: '初始化 Hub 或补装技能时', options: ['--hub'] },
|
|
191
194
|
{ name: 'update', usage: 'hub update', what: '更新 Hub 内的元技能(非元技能不参与;只升不降)', when: '日常维护 Hub 真源时', options: ['--hub'] },
|
|
192
195
|
{ name: 'adopt', usage: 'hub adopt [--scan | --apply] [--include <a,b>] [--in-place]', what: '把各宿主已有技能收编进 Hub(默认复制保真)', when: '接管机器上已装的技能时', options: ['--scan', '--apply', '--include', '--in-place', '--force', '--skip-scan', '--allow-unverified'] },
|
|
@@ -306,7 +309,9 @@ const CLI_HELP_QUICK = [
|
|
|
306
309
|
{ cmd: 'hub hosts set', example: 'yotta-skills hub hosts set <agentId> --dir <目录>' },
|
|
307
310
|
{ cmd: 'hub config get', example: 'yotta-skills hub config get', note: '查看生效位置与来源' },
|
|
308
311
|
{ cmd: 'hub config set', example: 'yotta-skills hub config set --hub <Hub目录>', note: '持久化 Hub 位置;迁移旧 Hub 加 --move' },
|
|
312
|
+
{ cmd: 'hub config rollback', example: 'yotta-skills hub config rollback --yes', note: '回滚到上次迁移前的位置(位置回退,用当前内容)', danger: true },
|
|
309
313
|
{ cmd: 'hub config clear', example: 'yotta-skills hub config clear', note: '清除持久覆盖,恢复默认解析' },
|
|
314
|
+
{ cmd: 'hub cleanup-backups', example: 'yotta-skills hub cleanup-backups --yes', note: '历史备份 / 暂存残留移入回收站(7 天可恢复)', danger: true },
|
|
310
315
|
{ cmd: 'hub hosts remove', example: 'yotta-skills hub hosts remove <技能目录>', note: '只移除注册,不删目录' },
|
|
311
316
|
{ cmd: 'hub hosts remove', example: 'yotta-skills hub hosts remove <技能目录> --purge', danger: true, note: '清理残留目录:先预览,--yes 才执行;目录入回收站保留 7 天' },
|
|
312
317
|
{ cmd: 'hub install', example: 'yotta-skills hub install' },
|
|
@@ -151,9 +151,14 @@ function replaceWithBackup(target, options) {
|
|
|
151
151
|
return null;
|
|
152
152
|
}
|
|
153
153
|
if (!opts.force) throw new Error('目标已存在真目录;如需替换请显式加 --force');
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
// 0.29.5 S3:旧副本不再原地留 <name>.yottaskills-backup-*(宿主会当技能扫),
|
|
155
|
+
// 统一移入回收站(7 天可恢复);跨卷走复制 + 校验 + 删源。
|
|
156
|
+
const trashRoot = opts.trashRoot || hubLib.resolveTrashRoot(opts.hubDir, opts);
|
|
157
|
+
const stamp = hubLib.trashRunStamp(opts.now);
|
|
158
|
+
const dest = path.join(trashRoot, stamp, 'hub-adopt', path.basename(target));
|
|
159
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
160
|
+
const transfer = hubLib.moveEntryAcrossDevices(target, dest, { renameEntry: opts.renameEntry });
|
|
161
|
+
return { path: dest, method: transfer.method, trashRoot };
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
function copyFidelity(sourceDir, targetDir) {
|
|
@@ -182,6 +187,9 @@ function applyCandidates(options) {
|
|
|
182
187
|
const include = opts.include || [];
|
|
183
188
|
const results = [];
|
|
184
189
|
fs.mkdirSync(hubDir, { recursive: true });
|
|
190
|
+
try {
|
|
191
|
+
hubLib.pruneTrash(hubLib.resolveTrashRoot(hubDir, opts));
|
|
192
|
+
} catch (_) { /* best-effort */ }
|
|
185
193
|
|
|
186
194
|
for (const candidate of candidates) {
|
|
187
195
|
if (!includeMatch(candidate.slug, include)) continue;
|
|
@@ -220,8 +228,15 @@ function applyCandidates(options) {
|
|
|
220
228
|
}
|
|
221
229
|
let backup = null;
|
|
222
230
|
let tmp = null;
|
|
231
|
+
let installed = false;
|
|
223
232
|
try {
|
|
224
|
-
backup = replaceWithBackup(target, {
|
|
233
|
+
backup = replaceWithBackup(target, {
|
|
234
|
+
force: opts.force,
|
|
235
|
+
hubDir,
|
|
236
|
+
env: opts.env,
|
|
237
|
+
trashRoot: opts.trashRoot,
|
|
238
|
+
renameEntry: opts.renameEntry,
|
|
239
|
+
});
|
|
225
240
|
if (opts.inPlace) {
|
|
226
241
|
hubLib.createDirLink(candidate.source, target);
|
|
227
242
|
} else {
|
|
@@ -229,27 +244,44 @@ function applyCandidates(options) {
|
|
|
229
244
|
fs.renameSync(tmp, target);
|
|
230
245
|
tmp = null;
|
|
231
246
|
}
|
|
247
|
+
installed = true;
|
|
232
248
|
results.push({
|
|
233
249
|
slug: candidate.slug,
|
|
234
250
|
status: 'imported',
|
|
235
|
-
note: opts.inPlace ? '已原地登记并链接到 Hub' : '已复制到 Hub'
|
|
236
|
-
|
|
251
|
+
note: (opts.inPlace ? '已原地登记并链接到 Hub' : '已复制到 Hub') +
|
|
252
|
+
(backup ? ';旧副本已移入回收站(保留 ' + hubLib.TRASH_RETENTION_DAYS + ' 天)' : ''),
|
|
253
|
+
backup: backup ? backup.path : null,
|
|
237
254
|
scanVerdict: scan.verdict,
|
|
238
255
|
sourcePath: candidate.source,
|
|
239
256
|
sourceAgent: candidate.sourceHost,
|
|
240
257
|
});
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
258
|
+
try {
|
|
259
|
+
hubLib.appendAudit(hubDir, {
|
|
260
|
+
event: 'adopt',
|
|
261
|
+
slug: candidate.slug,
|
|
262
|
+
source: candidate.source,
|
|
263
|
+
sourceHost: candidate.sourceHost,
|
|
264
|
+
inPlace: Boolean(opts.inPlace),
|
|
265
|
+
verdict: scan.verdict,
|
|
266
|
+
backup: backup ? backup.path : null,
|
|
267
|
+
});
|
|
268
|
+
} catch (_) { /* 审计失败不把已完成的收编判失败 */ }
|
|
250
269
|
} catch (error) {
|
|
251
270
|
if (tmp) fs.rmSync(tmp, { recursive: true, force: true });
|
|
252
|
-
|
|
271
|
+
let restored = false;
|
|
272
|
+
if (backup && !installed) {
|
|
273
|
+
try {
|
|
274
|
+
hubLib.moveEntryAcrossDevices(backup.path, target, { renameEntry: opts.renameEntry });
|
|
275
|
+
restored = true;
|
|
276
|
+
} catch (_) { /* 保留回收站副本供人工恢复 */ }
|
|
277
|
+
}
|
|
278
|
+
results.push({
|
|
279
|
+
slug: candidate.slug,
|
|
280
|
+
status: 'error',
|
|
281
|
+
note: error.message +
|
|
282
|
+
(backup && !restored ? '(旧副本保留在回收站:' + backup.path + ')' : ''),
|
|
283
|
+
backup: backup && !restored ? backup.path : null,
|
|
284
|
+
});
|
|
253
285
|
}
|
|
254
286
|
}
|
|
255
287
|
const synced = hubLib.syncHubState(hubDir, { manifest: opts.manifest, homeDir: opts.homeDir, env: opts.env });
|
|
@@ -286,23 +318,60 @@ function refreshFrom(options) {
|
|
|
286
318
|
}
|
|
287
319
|
let backup = null;
|
|
288
320
|
let tmp = null;
|
|
321
|
+
let installed = false;
|
|
289
322
|
try {
|
|
290
|
-
backup = replaceWithBackup(target, {
|
|
323
|
+
backup = replaceWithBackup(target, {
|
|
324
|
+
force: true,
|
|
325
|
+
hubDir,
|
|
326
|
+
env: opts.env,
|
|
327
|
+
trashRoot: opts.trashRoot,
|
|
328
|
+
renameEntry: opts.renameEntry,
|
|
329
|
+
});
|
|
291
330
|
tmp = copyFidelity(source, target);
|
|
292
331
|
fs.renameSync(tmp, target);
|
|
293
332
|
tmp = null;
|
|
333
|
+
installed = true;
|
|
334
|
+
} catch (error) {
|
|
335
|
+
if (tmp) fs.rmSync(tmp, { recursive: true, force: true });
|
|
336
|
+
let restored = false;
|
|
337
|
+
if (backup && !installed) {
|
|
338
|
+
try {
|
|
339
|
+
hubLib.moveEntryAcrossDevices(backup.path, target, { renameEntry: opts.renameEntry });
|
|
340
|
+
restored = true;
|
|
341
|
+
} catch (_) { /* 保留回收站副本供人工恢复 */ }
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
ok: false,
|
|
345
|
+
slug,
|
|
346
|
+
error: error.message + (backup && !restored ? '(旧副本保留在回收站:' + backup.path + ')' : ''),
|
|
347
|
+
backup: backup && !restored ? backup.path : null,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
294
351
|
hubLib.syncHubState(hubDir, { manifest: opts.manifest, homeDir: opts.homeDir, env: opts.env });
|
|
295
352
|
persistImportMeta(hubDir, slug, {
|
|
296
353
|
sourcePath: source,
|
|
297
354
|
scanVerdict: scan.verdict,
|
|
298
355
|
updatedAt: new Date().toISOString(),
|
|
299
356
|
});
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
357
|
+
} catch (_) { /* 状态刷新 best-effort;链接 / 台账下次同步 */ }
|
|
358
|
+
try {
|
|
359
|
+
hubLib.appendAudit(hubDir, {
|
|
360
|
+
event: 'refresh',
|
|
361
|
+
slug,
|
|
362
|
+
source,
|
|
363
|
+
verdict: scan.verdict,
|
|
364
|
+
backup: backup ? backup.path : null,
|
|
365
|
+
});
|
|
366
|
+
} catch (_) { /* 审计失败不把已完成的刷新判失败 */ }
|
|
367
|
+
return {
|
|
368
|
+
ok: true,
|
|
369
|
+
slug,
|
|
370
|
+
source,
|
|
371
|
+
backup: backup ? backup.path : null,
|
|
372
|
+
verdict: scan.verdict,
|
|
373
|
+
scanPolicy: scan.policy || null,
|
|
374
|
+
};
|
|
306
375
|
}
|
|
307
376
|
|
|
308
377
|
module.exports = {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
/**
|
|
3
|
-
* Hub 迁移(0.29.2 U4
|
|
3
|
+
* Hub 迁移(0.29.2 U4;0.29.5 抽出共享内核)—— `hub config set --hub <new> --move`
|
|
4
|
+
* 的两段式实现。
|
|
4
5
|
*
|
|
5
6
|
* 1) stageHubCopy({ from, to }):复制旧 Hub → 逐技能 treeHash 校验;失败
|
|
6
7
|
* fail-closed(清理暂存副本、旧 Hub 不动、配置不切换)。
|
|
@@ -10,10 +11,15 @@
|
|
|
10
11
|
*
|
|
11
12
|
* 配置切换(config.json 写入)由调用方在两段之间执行;写入失败时调用
|
|
12
13
|
* discardStagedCopy() 清理暂存副本。
|
|
14
|
+
*
|
|
15
|
+
* 0.29.5:runHubMigration() 把「复制 → 切配置 → 重链 → 旧 Hub 入回收站 → 记录
|
|
16
|
+
* lastMigration」收成一个共享内核,CLI `hub config set --move` / `hub config
|
|
17
|
+
* rollback` 与面板迁移向导 / 一键回滚共用,避免两套行为漂移。
|
|
13
18
|
*/
|
|
14
19
|
const fs = require('fs');
|
|
15
20
|
const path = require('path');
|
|
16
21
|
const hubLib = require('./hub');
|
|
22
|
+
const skillsConfigLib = require('./skills-config');
|
|
17
23
|
|
|
18
24
|
function nowStamp() {
|
|
19
25
|
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
@@ -51,6 +57,48 @@ function listEntries(dir) {
|
|
|
51
57
|
}
|
|
52
58
|
}
|
|
53
59
|
|
|
60
|
+
/** 空 Hub 台账残留:目标目录里只有元阁自己写过的空台账 / 审计文件。 */
|
|
61
|
+
const HUB_RESIDUE_FILES = new Set(['.yotta-hub.json', '.yotta-hub-audit.jsonl']);
|
|
62
|
+
|
|
63
|
+
/** 只读检查目标目录状态(迁移 fail-closed 判定 + 面板「清理残留并迁移」入口)。 */
|
|
64
|
+
function inspectTargetDir(dir) {
|
|
65
|
+
const resolved = path.resolve(dir);
|
|
66
|
+
const exists = fs.existsSync(resolved);
|
|
67
|
+
const entries = exists ? listEntries(resolved) : [];
|
|
68
|
+
const residueOnly = entries.length > 0 && entries.every((name) => HUB_RESIDUE_FILES.has(name));
|
|
69
|
+
return {
|
|
70
|
+
dir: resolved,
|
|
71
|
+
exists,
|
|
72
|
+
entries,
|
|
73
|
+
empty: entries.length === 0,
|
|
74
|
+
residueOnly,
|
|
75
|
+
residueFiles: residueOnly ? entries : [],
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 把「空 Hub 台账残留」移入回收站(保留 7 天),返回 { ok, moved, trashDir }。 */
|
|
80
|
+
function clearTargetResidue(dir, options) {
|
|
81
|
+
const opts = options || {};
|
|
82
|
+
const resolved = path.resolve(dir);
|
|
83
|
+
const state = inspectTargetDir(resolved);
|
|
84
|
+
if (!state.residueOnly) return { ok: false, error: '目标目录不是空 Hub 台账残留:' + resolved };
|
|
85
|
+
const trashRoot = hubLib.resolveTrashRoot(opts.hubDir || resolved, opts);
|
|
86
|
+
const dest = path.join(trashRoot, 'hub-residue-' + nowStamp());
|
|
87
|
+
const moved = [];
|
|
88
|
+
try {
|
|
89
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
90
|
+
for (const name of state.residueFiles) {
|
|
91
|
+
const from = path.join(resolved, name);
|
|
92
|
+
const to = path.join(dest, name);
|
|
93
|
+
hubLib.moveEntryAcrossDevices(from, to);
|
|
94
|
+
moved.push({ from, to });
|
|
95
|
+
}
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return { ok: false, error: '清理残留失败:' + error.message, moved, trashDir: dest };
|
|
98
|
+
}
|
|
99
|
+
return { ok: true, moved, trashDir: dest };
|
|
100
|
+
}
|
|
101
|
+
|
|
54
102
|
/** 清理暂存副本(仅在 stage 阶段失败或配置写入失败时调用)。 */
|
|
55
103
|
function discardStagedCopy(options) {
|
|
56
104
|
const opts = options || {};
|
|
@@ -176,8 +224,171 @@ function relinkAndRetireOldHub(options) {
|
|
|
176
224
|
return { ok: incomplete === 0, from, to, relink, incomplete, trashedTo, trashError };
|
|
177
225
|
}
|
|
178
226
|
|
|
227
|
+
function classifyStageError(error) {
|
|
228
|
+
const text = String(error || '');
|
|
229
|
+
if (/非空/.test(text)) return 'target-non-empty';
|
|
230
|
+
if (/嵌套/.test(text)) return 'target-nested';
|
|
231
|
+
if (/相同/.test(text)) return 'target-same';
|
|
232
|
+
return 'stage-failed';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* 共享迁移内核:stage → 切配置 → 重链 → 旧 Hub 入回收站 → 记录 lastMigration。
|
|
237
|
+
*
|
|
238
|
+
* options:
|
|
239
|
+
* - from / to(必填);targets(宿主目录数组;不传则不重链)
|
|
240
|
+
* - configOpts(默认 { homeDir, env });manifest / homeDir / env
|
|
241
|
+
* - via('cli' | 'view');kind('migrate' | 'rollback')
|
|
242
|
+
* - cleanResidue(true = 目标为空 Hub 台账残留时先移入回收站再迁移)
|
|
243
|
+
*
|
|
244
|
+
* 返回 { ok, phase, report?, error?, code?, stage?, finish? };部分失败
|
|
245
|
+
* (重链未全部完成)时旧 Hub 保留、report 记录 incomplete,绝不报成功。
|
|
246
|
+
*/
|
|
247
|
+
function runHubMigration(options) {
|
|
248
|
+
const opts = options || {};
|
|
249
|
+
const from = path.resolve(opts.from);
|
|
250
|
+
const to = path.resolve(opts.to);
|
|
251
|
+
const via = opts.via || 'cli';
|
|
252
|
+
const kind = opts.kind === 'rollback' ? 'rollback' : 'migrate';
|
|
253
|
+
const configOpts = opts.configOpts || { homeDir: opts.homeDir, env: opts.env };
|
|
254
|
+
|
|
255
|
+
const oldHasContent = fs.existsSync(from) && listEntries(from).length > 0;
|
|
256
|
+
if (!oldHasContent) {
|
|
257
|
+
const setResult = skillsConfigLib.setHub(configOpts, { hub: to });
|
|
258
|
+
if (!setResult.ok) return { ok: false, phase: 'config', error: setResult.error, code: 'config-failed' };
|
|
259
|
+
return {
|
|
260
|
+
ok: true,
|
|
261
|
+
phase: 'done',
|
|
262
|
+
switchedOnly: true,
|
|
263
|
+
report: {
|
|
264
|
+
moved: false,
|
|
265
|
+
kind,
|
|
266
|
+
from,
|
|
267
|
+
to,
|
|
268
|
+
at: new Date().toISOString(),
|
|
269
|
+
reason: '旧 Hub 不存在或为空(' + from + '),仅切换指针',
|
|
270
|
+
verifiedSkills: 0,
|
|
271
|
+
relinkDirs: 0,
|
|
272
|
+
incomplete: 0,
|
|
273
|
+
trashedTo: null,
|
|
274
|
+
trashError: null,
|
|
275
|
+
oldHubKept: null,
|
|
276
|
+
via,
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let residue = null;
|
|
282
|
+
const targetState = inspectTargetDir(to);
|
|
283
|
+
if (targetState.residueOnly) {
|
|
284
|
+
if (!opts.cleanResidue) {
|
|
285
|
+
return {
|
|
286
|
+
ok: false,
|
|
287
|
+
phase: 'preflight',
|
|
288
|
+
code: 'target-residue',
|
|
289
|
+
error: '新 Hub 目录含空 Hub 台账残留(' + targetState.residueFiles.join('、') + '):' + to +
|
|
290
|
+
'。可勾选「清理残留并迁移」后重试。',
|
|
291
|
+
target: targetState,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
residue = clearTargetResidue(to, { hubDir: from, env: opts.env });
|
|
295
|
+
if (!residue.ok) return { ok: false, phase: 'preflight', code: 'residue-clean-failed', error: residue.error };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const staged = stageHubCopy({ from, to });
|
|
299
|
+
if (!staged.ok) {
|
|
300
|
+
return { ok: false, phase: 'stage', code: classifyStageError(staged.error), error: staged.error };
|
|
301
|
+
}
|
|
302
|
+
const setResult = skillsConfigLib.setHub(configOpts, { hub: to });
|
|
303
|
+
if (!setResult.ok) {
|
|
304
|
+
discardStagedCopy({ to, existed: staged.existed });
|
|
305
|
+
return { ok: false, phase: 'config', code: 'config-failed', error: setResult.error };
|
|
306
|
+
}
|
|
307
|
+
const finish = relinkAndRetireOldHub({
|
|
308
|
+
from,
|
|
309
|
+
to,
|
|
310
|
+
targets: opts.targets,
|
|
311
|
+
manifest: opts.manifest,
|
|
312
|
+
homeDir: opts.homeDir,
|
|
313
|
+
env: opts.env,
|
|
314
|
+
});
|
|
315
|
+
const report = {
|
|
316
|
+
moved: true,
|
|
317
|
+
kind,
|
|
318
|
+
from,
|
|
319
|
+
to,
|
|
320
|
+
at: new Date().toISOString(),
|
|
321
|
+
verifiedSkills: staged.verifiedSkills,
|
|
322
|
+
relinkDirs: finish.relink.length,
|
|
323
|
+
relink: finish.relink,
|
|
324
|
+
incomplete: finish.incomplete,
|
|
325
|
+
trashedTo: finish.trashedTo,
|
|
326
|
+
trashError: finish.trashError,
|
|
327
|
+
oldHubKept: (finish.incomplete > 0 || finish.trashError) ? from : null,
|
|
328
|
+
via,
|
|
329
|
+
};
|
|
330
|
+
skillsConfigLib.recordLastMigration(configOpts, report);
|
|
331
|
+
return {
|
|
332
|
+
ok: finish.ok,
|
|
333
|
+
phase: finish.ok ? 'done' : 'relink',
|
|
334
|
+
report,
|
|
335
|
+
finish,
|
|
336
|
+
residue,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** 回收站保留剩余天数(向上取整,最小 0;at 缺失按 0 天算)。 */
|
|
341
|
+
function trashDaysLeft(at, options) {
|
|
342
|
+
const opts = options || {};
|
|
343
|
+
const days = Number(opts.days) > 0 ? Number(opts.days) : hubLib.TRASH_RETENTION_DAYS;
|
|
344
|
+
const started = Date.parse(at || '');
|
|
345
|
+
if (!Number.isFinite(started)) return 0;
|
|
346
|
+
const elapsedDays = Math.floor(Math.max(0, Date.now() - started) / (24 * 60 * 60 * 1000));
|
|
347
|
+
return Math.max(0, days - elapsedDays);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* 回滚预览(只读):当前 Hub → 上次迁移的原位置,反向迁移同内核。
|
|
352
|
+
* 语义 = 位置回退(用当前内容,不丢迁移后改动),不是恢复旧快照。
|
|
353
|
+
*/
|
|
354
|
+
function previewRollback(options) {
|
|
355
|
+
const opts = options || {};
|
|
356
|
+
const configOpts = opts.configOpts || { homeDir: opts.homeDir, env: opts.env };
|
|
357
|
+
const config = skillsConfigLib.readConfig(configOpts);
|
|
358
|
+
const last = config.lastMigration;
|
|
359
|
+
if (!last) {
|
|
360
|
+
return { ok: false, code: 'no-migration', error: '没有可回滚的迁移记录(config.json 无 lastMigration)' };
|
|
361
|
+
}
|
|
362
|
+
const current = opts.currentDir
|
|
363
|
+
? { dir: path.resolve(opts.currentDir), source: opts.currentSource || 'flag' }
|
|
364
|
+
: skillsConfigLib.resolveHub(configOpts);
|
|
365
|
+
const target = inspectTargetDir(last.from);
|
|
366
|
+
const blocked = [];
|
|
367
|
+
if (!fs.existsSync(current.dir)) blocked.push('当前 Hub 不存在:' + current.dir);
|
|
368
|
+
if (target.exists && !target.empty && !target.residueOnly) {
|
|
369
|
+
blocked.push('回滚目标非空(fail-closed,不合并):' + target.dir);
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
ok: blocked.length === 0,
|
|
373
|
+
code: blocked.length ? 'rollback-blocked' : null,
|
|
374
|
+
blocked,
|
|
375
|
+
current: current.dir,
|
|
376
|
+
target: target.dir,
|
|
377
|
+
targetState: target,
|
|
378
|
+
lastMigration: last,
|
|
379
|
+
daysLeft: trashDaysLeft(last.at, opts),
|
|
380
|
+
command: 'yotta-skills hub config rollback --yes',
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
179
384
|
module.exports = {
|
|
180
385
|
discardStagedCopy,
|
|
181
386
|
stageHubCopy,
|
|
182
387
|
relinkAndRetireOldHub,
|
|
388
|
+
inspectTargetDir,
|
|
389
|
+
clearTargetResidue,
|
|
390
|
+
runHubMigration,
|
|
391
|
+
previewRollback,
|
|
392
|
+
trashDaysLeft,
|
|
393
|
+
HUB_RESIDUE_FILES,
|
|
183
394
|
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Hub 链接目标范围(0.29.5)—— `hub link --all` 与面板迁移向导 / 一键回滚的
|
|
4
|
+
* 重链范围共用同一真源,避免 CLI 与面板两套范围漂移。
|
|
5
|
+
*
|
|
6
|
+
* 默认范围 = 通用目录(Universal .agents / 通用 AGENTS.md)+ 已核实宿主目录;
|
|
7
|
+
* `includeDiscovered` 显式纳入自动发现目录;桥接目录永不纳入(发现层已过滤
|
|
8
|
+
* 不接管名单与 YottaCode)。
|
|
9
|
+
*/
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const agentDirsLib = require('./agent-dirs');
|
|
14
|
+
const hostsRegistryLib = require('./hosts-registry');
|
|
15
|
+
|
|
16
|
+
function normalizeTargetKey(dir) {
|
|
17
|
+
const resolved = path.resolve(dir);
|
|
18
|
+
let real = resolved;
|
|
19
|
+
try {
|
|
20
|
+
real = fs.realpathSync.native(resolved);
|
|
21
|
+
} catch (_) { /* missing path: fall back to the resolved spelling */ }
|
|
22
|
+
return process.platform === 'win32' ? real.toLowerCase() : real;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 通用技能目录(与旧 CLI universalSkillDirs 同源)。 */
|
|
26
|
+
function universalSkillDirs(options) {
|
|
27
|
+
const opts = options || {};
|
|
28
|
+
const home = opts.homeDir || os.homedir();
|
|
29
|
+
const env = opts.env || process.env;
|
|
30
|
+
const xdg = env.XDG_CONFIG_HOME || path.join(home, '.config');
|
|
31
|
+
return [
|
|
32
|
+
{ dir: path.join(xdg, 'agents', 'skills'), agentId: 'universal', label: 'Universal .agents' },
|
|
33
|
+
{ dir: path.join(home, '.agents', 'skills'), agentId: 'agents', label: '通用 AGENTS.md' },
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 计算 --all 范围目标(只读;不创建目录、不链接)。
|
|
39
|
+
* options: { discovery, registry, homeDir, env, includeDiscovered }
|
|
40
|
+
*/
|
|
41
|
+
function computeHubTargets(options) {
|
|
42
|
+
const opts = options || {};
|
|
43
|
+
const discovery = opts.discovery || { hosts: [] };
|
|
44
|
+
const homeDir = opts.homeDir || os.homedir();
|
|
45
|
+
const env = opts.env || process.env;
|
|
46
|
+
const registry = opts.registry || hostsRegistryLib.readHostsRegistry({ homeDir, env });
|
|
47
|
+
const excludedEntry = (input) => hostsRegistryLib.excludedEntryFor(registry, input);
|
|
48
|
+
const targets = [];
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
const add = (dir, agentId, label, meta) => {
|
|
51
|
+
if (!dir) return;
|
|
52
|
+
const resolved = path.resolve(dir);
|
|
53
|
+
const key = normalizeTargetKey(resolved);
|
|
54
|
+
if (seen.has(key)) return;
|
|
55
|
+
seen.add(key);
|
|
56
|
+
targets.push({
|
|
57
|
+
dir: resolved,
|
|
58
|
+
agentId: agentId || null,
|
|
59
|
+
label: label || '指定目录',
|
|
60
|
+
verified: Boolean(meta && meta.verified),
|
|
61
|
+
bridgeOnly: Boolean(meta && meta.bridgeOnly),
|
|
62
|
+
detection: (meta && meta.detection) || 'mapping',
|
|
63
|
+
explicit: Boolean(meta && meta.explicit),
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
for (const item of universalSkillDirs({ homeDir, env })) {
|
|
68
|
+
if (agentDirsLib.isYottaCodeDir(item.dir)) continue;
|
|
69
|
+
if (excludedEntry({ dir: item.dir, agentId: item.agentId })) continue;
|
|
70
|
+
add(item.dir, item.agentId, item.label, { verified: true });
|
|
71
|
+
}
|
|
72
|
+
for (const host of discovery.hosts || []) {
|
|
73
|
+
if (!host.exists) continue;
|
|
74
|
+
if (host.bridgeOnly) continue;
|
|
75
|
+
if (!opts.includeDiscovered && !host.verified) continue;
|
|
76
|
+
add(host.dir, host.agentId, host.label, host);
|
|
77
|
+
}
|
|
78
|
+
return targets;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
normalizeTargetKey,
|
|
83
|
+
universalSkillDirs,
|
|
84
|
+
computeHubTargets,
|
|
85
|
+
};
|