@yottameta/yotta-skills-plugin 0.29.1 → 0.29.2
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 +12 -2
- package/skills/yotta-skills/assets/view.html +39 -0
- package/skills/yotta-skills/bin/yotta-skills.js +206 -7
- package/skills/yotta-skills/lib/cli-help.js +8 -1
- package/skills/yotta-skills/lib/hub-adopt.js +1 -1
- package/skills/yotta-skills/lib/hub-migrate.js +183 -0
- package/skills/yotta-skills/lib/hub-scan.js +37 -3
- package/skills/yotta-skills/lib/hub-view-server.js +55 -1
- package/skills/yotta-skills/lib/hub.js +8 -4
- package/skills/yotta-skills/lib/self-install.js +5 -1
- package/skills/yotta-skills/lib/skills-config.js +170 -0
- package/skills/yotta-skills/references/hub.md +18 -1
- package/skills/yotta-skills/references/skill-list.md +2 -2
- package/skills/yotta-skills/scan-policy.json +3 -3
- package/skills/yotta-skills/scripts/yotta-skills-mcp.py +1 -1
- package/skills/yotta-skills/skills.json +2 -2
|
@@ -40,7 +40,8 @@ const CLI_GLOBAL_OPTIONS = [
|
|
|
40
40
|
{ flag: '--yes', arg: '', what: 'usage reset 的确认开关', when: '确认清空本地使用记录时', caution: '会清空记录文件' },
|
|
41
41
|
{ flag: '--explain', arg: '', what: 'decide-memory 文本报告追加信号明细', when: '想看裁决依据时', caution: '' },
|
|
42
42
|
{ flag: '--promote', arg: '', what: 'decide-memory 只写本地建议文件(不写元忆)', when: '想留下裁决建议文件时', caution: '不会写元忆、不会删除任何内容' },
|
|
43
|
-
{ flag: '--hub', arg: '<path>', what: 'Hub 目录(默认 ~/.yottaskills/hub)', when: '使用自定义 Hub 位置时', caution: 'YOTTA_SKILLS_HUB
|
|
43
|
+
{ flag: '--hub', arg: '<path>', what: 'Hub 目录(默认 ~/.yottaskills/hub;持久配置见 hub config)', when: '使用自定义 Hub 位置时', caution: '覆盖顺序:--hub > YOTTA_SKILLS_HUB > config.json > 默认' },
|
|
44
|
+
{ flag: '--move', arg: '', what: 'hub config set 迁移:复制校验旧 Hub → 重链宿主 → 旧 Hub 入回收站(7 天)', when: '换 Hub 位置并带上已有技能时', caution: 'fail-closed:校验失败不切配置;重链失败保留旧 Hub' },
|
|
44
45
|
{ flag: '--port', arg: '<n>', what: 'view 面板端口(默认 8789)', when: '端口冲突或想固定端口时', caution: '仅 127.0.0.1 监听' },
|
|
45
46
|
{ flag: '--all', arg: '', what: 'hub link / unlink 作用于全部已核实宿主', when: '全量分发或解除时', caution: '' },
|
|
46
47
|
{ flag: '--include-discovered', arg: '', what: 'hub link / unlink / remove 时纳入自动发现目录', when: '确认要扩围到未核实目录时', caution: '默认只链已核实宿主;桥接目录永不链接' },
|
|
@@ -183,6 +184,9 @@ const CLI_HELP_MODEL = [
|
|
|
183
184
|
{ name: 'hosts exclude', usage: 'hub hosts exclude <agentId|dir> [--json]', what: '加入「不接管」名单(发现 / 显示 / 链接三层跳过)', when: '某宿主 / 目录不归元阁管理时', options: ['--json'] },
|
|
184
185
|
{ name: 'hosts include', usage: 'hub hosts include <agentId|dir> [--json]', what: '恢复接管(移出「不接管」名单)', when: '重新纳入发现 / 链接时', options: ['--json'] },
|
|
185
186
|
{ 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
|
+
{ 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 天)', when: '换 Hub 存储位置时', options: ['--hub', '--move', '--json'] },
|
|
189
|
+
{ name: 'config clear', usage: 'hub config clear [--json]', what: '清除配置覆盖,恢复 flag > env > 默认', when: '撤销持久 Hub 位置时', options: ['--json'] },
|
|
186
190
|
{ name: 'install', usage: 'hub install [skill...]', what: '把元技能装进 Hub 真源(清单 27 + 特殊家族 5)', when: '初始化 Hub 或补装技能时', options: ['--hub'] },
|
|
187
191
|
{ name: 'update', usage: 'hub update', what: '更新 Hub 内的元技能(非元技能不参与;只升不降)', when: '日常维护 Hub 真源时', options: ['--hub'] },
|
|
188
192
|
{ 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'] },
|
|
@@ -300,6 +304,9 @@ const CLI_HELP_QUICK = [
|
|
|
300
304
|
{ cmd: 'hub hosts exclude', example: 'yotta-skills hub hosts exclude <agentId|目录>' },
|
|
301
305
|
{ cmd: 'hub hosts include', example: 'yotta-skills hub hosts include <agentId|目录>' },
|
|
302
306
|
{ cmd: 'hub hosts set', example: 'yotta-skills hub hosts set <agentId> --dir <目录>' },
|
|
307
|
+
{ cmd: 'hub config get', example: 'yotta-skills hub config get', note: '查看生效位置与来源' },
|
|
308
|
+
{ cmd: 'hub config set', example: 'yotta-skills hub config set --hub <Hub目录>', note: '持久化 Hub 位置;迁移旧 Hub 加 --move' },
|
|
309
|
+
{ cmd: 'hub config clear', example: 'yotta-skills hub config clear', note: '清除持久覆盖,恢复默认解析' },
|
|
303
310
|
{ cmd: 'hub hosts remove', example: 'yotta-skills hub hosts remove <技能目录>', note: '只移除注册,不删目录' },
|
|
304
311
|
{ cmd: 'hub hosts remove', example: 'yotta-skills hub hosts remove <技能目录> --purge', danger: true, note: '清理残留目录:先预览,--yes 才执行;目录入回收站保留 7 天' },
|
|
305
312
|
{ cmd: 'hub install', example: 'yotta-skills hub install' },
|
|
@@ -269,7 +269,7 @@ function refreshFrom(options) {
|
|
|
269
269
|
updatedAt: new Date().toISOString(),
|
|
270
270
|
});
|
|
271
271
|
hubLib.appendAudit(hubDir, { event: 'refresh', slug, source, verdict: scan.verdict, backup });
|
|
272
|
-
return { ok: true, slug, source, backup, verdict: scan.verdict };
|
|
272
|
+
return { ok: true, slug, source, backup, verdict: scan.verdict, scanPolicy: scan.policy || null };
|
|
273
273
|
} catch (error) {
|
|
274
274
|
if (tmp) fs.rmSync(tmp, { recursive: true, force: true });
|
|
275
275
|
return { ok: false, slug, error: error.message };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* Hub 迁移(0.29.2 U4)—— `hub config set --hub <new> --move` 的两段式实现。
|
|
4
|
+
*
|
|
5
|
+
* 1) stageHubCopy({ from, to }):复制旧 Hub → 逐技能 treeHash 校验;失败
|
|
6
|
+
* fail-closed(清理暂存副本、旧 Hub 不动、配置不切换)。
|
|
7
|
+
* 2) relinkAndRetireOldHub({ from, to, targets }):`hub link` 以新 Hub 为源、
|
|
8
|
+
* --force 重链各宿主 → 全部成功才把旧 Hub 移入回收站(7 天);有失败则
|
|
9
|
+
* 保留旧 Hub 供重试(此时新旧内容一致,链接混合指向两个 Hub 均可用)。
|
|
10
|
+
*
|
|
11
|
+
* 配置切换(config.json 写入)由调用方在两段之间执行;写入失败时调用
|
|
12
|
+
* discardStagedCopy() 清理暂存副本。
|
|
13
|
+
*/
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const hubLib = require('./hub');
|
|
17
|
+
|
|
18
|
+
function nowStamp() {
|
|
19
|
+
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function safeRealpath(target) {
|
|
23
|
+
try {
|
|
24
|
+
return fs.realpathSync.native(target);
|
|
25
|
+
} catch (_) {
|
|
26
|
+
try {
|
|
27
|
+
return fs.realpathSync(target);
|
|
28
|
+
} catch (_) {
|
|
29
|
+
return path.resolve(target);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function samePath(left, right) {
|
|
35
|
+
const a = safeRealpath(left);
|
|
36
|
+
const b = safeRealpath(right);
|
|
37
|
+
if (process.platform === 'win32') return a.toLowerCase() === b.toLowerCase();
|
|
38
|
+
return a === b;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isInside(root, target) {
|
|
42
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
43
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function listEntries(dir) {
|
|
47
|
+
try {
|
|
48
|
+
return fs.readdirSync(dir);
|
|
49
|
+
} catch (_) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 清理暂存副本(仅在 stage 阶段失败或配置写入失败时调用)。 */
|
|
55
|
+
function discardStagedCopy(options) {
|
|
56
|
+
const opts = options || {};
|
|
57
|
+
const to = path.resolve(opts.to);
|
|
58
|
+
try {
|
|
59
|
+
if (opts.existed) {
|
|
60
|
+
for (const name of listEntries(to)) fs.rmSync(path.join(to, name), { recursive: true, force: true });
|
|
61
|
+
} else {
|
|
62
|
+
fs.rmSync(to, { recursive: true, force: true });
|
|
63
|
+
}
|
|
64
|
+
} catch (_) { /* 保留现场供排查 */ }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 复制 + 校验(不切配置)。返回 { ok, error?, from, to, existed, verifiedSkills }。 */
|
|
68
|
+
function stageHubCopy(options) {
|
|
69
|
+
const opts = options || {};
|
|
70
|
+
const from = path.resolve(opts.from);
|
|
71
|
+
const to = path.resolve(opts.to);
|
|
72
|
+
if (samePath(from, to)) return { ok: false, error: '新旧 Hub 路径相同:' + from };
|
|
73
|
+
if (isInside(from, to) || isInside(to, from)) return { ok: false, error: '新旧 Hub 不能互相嵌套:' + from + ' / ' + to };
|
|
74
|
+
let stat = null;
|
|
75
|
+
try {
|
|
76
|
+
stat = fs.statSync(from);
|
|
77
|
+
} catch (_) { /* handled below */ }
|
|
78
|
+
if (!stat || !stat.isDirectory()) return { ok: false, error: '旧 Hub 不存在或不是目录:' + from };
|
|
79
|
+
const existed = fs.existsSync(to);
|
|
80
|
+
if (existed && listEntries(to).length > 0) {
|
|
81
|
+
return { ok: false, error: '新 Hub 目录非空(fail-closed,不合并):' + to };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
fs.mkdirSync(to, { recursive: true });
|
|
86
|
+
fs.cpSync(from, to, { recursive: true });
|
|
87
|
+
} catch (error) {
|
|
88
|
+
discardStagedCopy({ to, existed });
|
|
89
|
+
return { ok: false, error: '复制失败:' + error.message };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const mismatches = [];
|
|
93
|
+
for (const name of listEntries(from)) {
|
|
94
|
+
if (!fs.existsSync(path.join(to, name))) mismatches.push('entry:' + name);
|
|
95
|
+
}
|
|
96
|
+
const skills = hubLib.scanHubSkills(from);
|
|
97
|
+
for (const skill of skills) {
|
|
98
|
+
let sourceStat = null;
|
|
99
|
+
try {
|
|
100
|
+
sourceStat = fs.lstatSync(skill.dir);
|
|
101
|
+
} catch (_) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// in-place / 外部链接条目:随链接原样复制,不做内容校验。
|
|
105
|
+
if (sourceStat.isSymbolicLink() || !isInside(from, skill.dir)) continue;
|
|
106
|
+
const copied = path.join(to, path.relative(from, skill.dir));
|
|
107
|
+
let copiedStat = null;
|
|
108
|
+
try {
|
|
109
|
+
copiedStat = fs.lstatSync(copied);
|
|
110
|
+
} catch (_) { /* mismatch below */ }
|
|
111
|
+
if (!copiedStat || !copiedStat.isDirectory()) {
|
|
112
|
+
mismatches.push(skill.slug);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
if (hubLib.hashTree(skill.dir) !== hubLib.hashTree(copied)) mismatches.push(skill.slug);
|
|
117
|
+
} catch (_) {
|
|
118
|
+
mismatches.push(skill.slug);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (mismatches.length > 0) {
|
|
122
|
+
discardStagedCopy({ to, existed });
|
|
123
|
+
return {
|
|
124
|
+
ok: false,
|
|
125
|
+
error: '迁移校验失败(未切换配置;已清理暂存副本):' + mismatches.slice(0, 8).join(', ') +
|
|
126
|
+
(mismatches.length > 8 ? ' 等 ' + mismatches.length + ' 项' : ''),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { ok: true, from, to, existed, verifiedSkills: skills.length };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 重链(新 Hub 为源)+ 旧 Hub 入回收站(仅当重链无失败)。 */
|
|
133
|
+
function relinkAndRetireOldHub(options) {
|
|
134
|
+
const opts = options || {};
|
|
135
|
+
const from = path.resolve(opts.from);
|
|
136
|
+
const to = path.resolve(opts.to);
|
|
137
|
+
const targets = Array.isArray(opts.targets) ? opts.targets : [];
|
|
138
|
+
const relink = [];
|
|
139
|
+
let incomplete = 0;
|
|
140
|
+
for (const target of targets) {
|
|
141
|
+
if (!target || !target.dir) continue;
|
|
142
|
+
if (samePath(target.dir, from) || samePath(target.dir, to)) continue;
|
|
143
|
+
const result = hubLib.linkSkills({
|
|
144
|
+
hubDir: to,
|
|
145
|
+
targetDir: target.dir,
|
|
146
|
+
agentId: target.agentId,
|
|
147
|
+
label: target.label,
|
|
148
|
+
force: true,
|
|
149
|
+
manifest: opts.manifest,
|
|
150
|
+
homeDir: opts.homeDir,
|
|
151
|
+
env: opts.env,
|
|
152
|
+
});
|
|
153
|
+
const notLinked = result.results.filter((item) => item.status !== 'linked' && item.status !== 'missing');
|
|
154
|
+
incomplete += notLinked.length;
|
|
155
|
+
relink.push({
|
|
156
|
+
dir: target.dir,
|
|
157
|
+
label: target.label,
|
|
158
|
+
linked: result.results.filter((item) => item.status === 'linked').length,
|
|
159
|
+
incomplete: notLinked.map((item) => item.slug + ': ' + item.note),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let trashedTo = null;
|
|
164
|
+
let trashError = null;
|
|
165
|
+
if (incomplete === 0) {
|
|
166
|
+
try {
|
|
167
|
+
const trashRoot = hubLib.resolveTrashRoot(from, { env: opts.env });
|
|
168
|
+
const dest = path.join(trashRoot, 'hub-migrate-' + nowStamp(), path.basename(from));
|
|
169
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
170
|
+
hubLib.moveEntryAcrossDevices(from, dest);
|
|
171
|
+
trashedTo = dest;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
trashError = error.message;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { ok: incomplete === 0, from, to, relink, incomplete, trashedTo, trashError };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
discardStagedCopy,
|
|
181
|
+
stageHubCopy,
|
|
182
|
+
relinkAndRetireOldHub,
|
|
183
|
+
};
|
|
@@ -13,6 +13,10 @@ const { spawnSync } = require('child_process');
|
|
|
13
13
|
const depsLib = require('./deps');
|
|
14
14
|
const gateLib = require('./verify-gate');
|
|
15
15
|
const trustedVerifierLib = require('./trusted-verifier');
|
|
16
|
+
const scanPolicyLib = require('./scan-policy');
|
|
17
|
+
const hubLib = require('./hub');
|
|
18
|
+
|
|
19
|
+
const POLICY_PATH = path.join(__dirname, '..', 'scan-policy.json');
|
|
16
20
|
|
|
17
21
|
function currentCommand() {
|
|
18
22
|
const args = process.argv.slice(2).map((a) => (/\s/.test(a) ? '"' + a + '"' : a));
|
|
@@ -52,6 +56,21 @@ function runScan(engine, skillDir, opts) {
|
|
|
52
56
|
return gateLib.runVerifier(engine, skillDir, { python, spawnSync });
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
/**
|
|
60
|
+
* 复用安装管线的 scanPolicy 复核(0.29.2):版本 + treeHash 绑定,fail-closed。
|
|
61
|
+
* 未命中例外表 / 绑定不匹配时返回原始 scan(不豁免任何发现)。
|
|
62
|
+
*/
|
|
63
|
+
function reviewScanWithPolicy(scan, context) {
|
|
64
|
+
const ctx = context || {};
|
|
65
|
+
const policy = scanPolicyLib.loadPolicy(ctx.policyPath || POLICY_PATH);
|
|
66
|
+
return scanPolicyLib.applyScanPolicy(scan, {
|
|
67
|
+
slug: ctx.slug,
|
|
68
|
+
version: ctx.version,
|
|
69
|
+
pkgDir: ctx.pkgDir,
|
|
70
|
+
policy,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
55
74
|
/** Hub adopt scan engine: Hub itself first, then every discovered host dir. */
|
|
56
75
|
function scanEngineForHub(hubDir, discovery, opts) {
|
|
57
76
|
const direct = findVerifyEngine(hubDir, opts);
|
|
@@ -69,7 +88,7 @@ function scanEngineForHub(hubDir, discovery, opts) {
|
|
|
69
88
|
* Scan one candidate directory for Hub adoption.
|
|
70
89
|
* Returns { ok, verdict, counts, block } or { ok:false, error }.
|
|
71
90
|
*/
|
|
72
|
-
function hubScanSkill(hubDir, discovery, opts, skillDir) {
|
|
91
|
+
function hubScanSkill(hubDir, discovery, opts, skillDir, context) {
|
|
73
92
|
const options = opts || {};
|
|
74
93
|
if (options.skipScan) {
|
|
75
94
|
return { ok: true, verdict: 'explicit-unverified', counts: null, block: false };
|
|
@@ -83,8 +102,22 @@ function hubScanSkill(hubDir, discovery, opts, skillDir) {
|
|
|
83
102
|
}
|
|
84
103
|
const scan = runScan(engine, skillDir, options);
|
|
85
104
|
if (!scan.ok) return scan;
|
|
86
|
-
const
|
|
87
|
-
|
|
105
|
+
const ctx = context || {};
|
|
106
|
+
const meta = hubLib.readSkillMeta(skillDir);
|
|
107
|
+
const reviewed = reviewScanWithPolicy(scan, {
|
|
108
|
+
slug: ctx.slug || (meta && (meta.slug || meta.name)) || null,
|
|
109
|
+
version: ctx.version || (meta && meta.version) || null,
|
|
110
|
+
pkgDir: skillDir,
|
|
111
|
+
policyPath: options.policyPath,
|
|
112
|
+
});
|
|
113
|
+
const verdict = gateLib.evaluateVerdict(reviewed.verdict);
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
verdict: reviewed.verdict,
|
|
117
|
+
counts: reviewed.counts,
|
|
118
|
+
block: verdict.block,
|
|
119
|
+
policy: reviewed.policy || null,
|
|
120
|
+
};
|
|
88
121
|
}
|
|
89
122
|
|
|
90
123
|
module.exports = {
|
|
@@ -92,6 +125,7 @@ module.exports = {
|
|
|
92
125
|
findVerifyEngine,
|
|
93
126
|
findPython,
|
|
94
127
|
runScan,
|
|
128
|
+
reviewScanWithPolicy,
|
|
95
129
|
scanEngineForHub,
|
|
96
130
|
hubScanSkill,
|
|
97
131
|
};
|
|
@@ -35,6 +35,7 @@ const usageLib = require('./usage-journal');
|
|
|
35
35
|
const cliHelpLib = require('./cli-help');
|
|
36
36
|
const hostsRegistryLib = require('./hosts-registry');
|
|
37
37
|
const hostPurgeLib = require('./host-purge');
|
|
38
|
+
const skillsConfigLib = require('./skills-config');
|
|
38
39
|
|
|
39
40
|
const DEFAULT_HOST = '127.0.0.1';
|
|
40
41
|
const DEFAULT_PORT = 8789;
|
|
@@ -44,7 +45,7 @@ const DEFAULT_LIMIT = 100;
|
|
|
44
45
|
const MAX_LIMIT = 500;
|
|
45
46
|
const TOKEN_HEADER = 'x-yotta-view-token';
|
|
46
47
|
const TOKEN_PLACEHOLDER = '__YOTTA_VIEW_TOKEN__';
|
|
47
|
-
const CONFIRM = { unlink: 'unlink', rollback: 'rollback', remove: 'remove', purge: 'purge' };
|
|
48
|
+
const CONFIRM = { unlink: 'unlink', rollback: 'rollback', remove: 'remove', purge: 'purge', hubConfig: 'hub-config' };
|
|
48
49
|
const SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
49
50
|
|
|
50
51
|
function nowIso() {
|
|
@@ -164,9 +165,19 @@ function overviewPayload(ctx) {
|
|
|
164
165
|
discovery: discovery(ctx),
|
|
165
166
|
});
|
|
166
167
|
const audit = readJsonlTail(hubAuditPath(ctx.hubDir), 8);
|
|
168
|
+
const hubNow = skillsConfigLib.resolveHub({ homeDir: ctx.homeDir, env: ctx.env });
|
|
167
169
|
return {
|
|
168
170
|
version: ctx.version,
|
|
169
171
|
hubDir: ctx.hubDir,
|
|
172
|
+
// 0.29.2 U4:Hub 位置来源 / 配置覆盖 / 重启提示(面板「Hub 位置」区)。
|
|
173
|
+
hub: {
|
|
174
|
+
dir: ctx.hubDir,
|
|
175
|
+
source: ctx.hubSource || 'default',
|
|
176
|
+
configured: hubNow.configured,
|
|
177
|
+
configFile: hubNow.configFile,
|
|
178
|
+
effectiveNow: hubNow.dir,
|
|
179
|
+
restartRequired: !ctx.hubFlag && !sameDir(hubNow.dir, ctx.hubDir),
|
|
180
|
+
},
|
|
170
181
|
standard: hubLib.STANDARD_ID,
|
|
171
182
|
generatedAt: nowIso(),
|
|
172
183
|
status: {
|
|
@@ -675,6 +686,46 @@ function writeHostInclude(ctx, body) {
|
|
|
675
686
|
return { code: 200, payload: { action: 'hosts.include', removed: result.removed } };
|
|
676
687
|
}
|
|
677
688
|
|
|
689
|
+
/** 0.29.2 U4:面板「Hub 位置」设置 / 清除(写 config.json;重启 view 生效)。 */
|
|
690
|
+
function writeHubConfig(ctx, body) {
|
|
691
|
+
if (String(body.confirm || '') !== CONFIRM.hubConfig) {
|
|
692
|
+
return { code: 400, payload: { error: '确认串不匹配(需要 "' + CONFIRM.hubConfig + '")。' } };
|
|
693
|
+
}
|
|
694
|
+
const configOpts = { homeDir: ctx.homeDir, env: ctx.env };
|
|
695
|
+
const current = skillsConfigLib.resolveHub(configOpts);
|
|
696
|
+
if (body.clear) {
|
|
697
|
+
const result = skillsConfigLib.clearHub(configOpts);
|
|
698
|
+
if (!result.ok) return { code: 400, payload: { error: result.error } };
|
|
699
|
+
if (fs.existsSync(current.dir)) {
|
|
700
|
+
try {
|
|
701
|
+
hubLib.appendAudit(current.dir, { event: 'config.clear', removed: result.removed, via: 'view' });
|
|
702
|
+
} catch (_) { /* best-effort */ }
|
|
703
|
+
}
|
|
704
|
+
return { code: 200, payload: { action: 'config.clear', removed: result.removed, restartRequired: true } };
|
|
705
|
+
}
|
|
706
|
+
const hub = String(body.hub || '').trim();
|
|
707
|
+
const guard = skillsConfigLib.validateHubPath(hub, configOpts);
|
|
708
|
+
if (!guard.ok) return { code: 400, payload: { error: guard.error } };
|
|
709
|
+
const result = skillsConfigLib.setHub(configOpts, { hub });
|
|
710
|
+
if (!result.ok) return { code: 400, payload: { error: result.error } };
|
|
711
|
+
const auditDir = fs.existsSync(result.hub) ? result.hub : current.dir;
|
|
712
|
+
if (fs.existsSync(auditDir)) {
|
|
713
|
+
try {
|
|
714
|
+
hubLib.appendAudit(auditDir, { event: 'config.set', hub: result.hub, previousHub: current.dir, via: 'view' });
|
|
715
|
+
} catch (_) { /* best-effort */ }
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
code: 200,
|
|
719
|
+
payload: {
|
|
720
|
+
action: 'config.set',
|
|
721
|
+
hub: result.hub,
|
|
722
|
+
previous: current.dir,
|
|
723
|
+
configFile: result.configFile,
|
|
724
|
+
restartRequired: true,
|
|
725
|
+
},
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
678
729
|
function json(res, code, value) {
|
|
679
730
|
if (res.writableEnded) return;
|
|
680
731
|
res.writeHead(code, {
|
|
@@ -840,6 +891,7 @@ function handlePost(ctx, pathname, req, res) {
|
|
|
840
891
|
if (pathname === '/api/hosts/set') return writeHostSet(ctx, body);
|
|
841
892
|
if (pathname === '/api/hosts/exclude') return writeHostExclude(ctx, body);
|
|
842
893
|
if (pathname === '/api/hosts/include') return writeHostInclude(ctx, body);
|
|
894
|
+
if (pathname === '/api/hub/config') return writeHubConfig(ctx, body);
|
|
843
895
|
return { code: 404, payload: { error: 'not found' } };
|
|
844
896
|
};
|
|
845
897
|
ctx.queue(run)
|
|
@@ -857,6 +909,8 @@ function createHubViewServer(options) {
|
|
|
857
909
|
let chain = Promise.resolve();
|
|
858
910
|
const ctx = {
|
|
859
911
|
hubDir: path.resolve(opts.hubDir || hubLib.resolveHubDir({})),
|
|
912
|
+
hubSource: opts.hubSource || 'default',
|
|
913
|
+
hubFlag: Boolean(opts.hubFlag),
|
|
860
914
|
homeDir: opts.homeDir || os.homedir(),
|
|
861
915
|
env: opts.env || process.env,
|
|
862
916
|
manifest: opts.manifest || [],
|
|
@@ -14,6 +14,7 @@ const os = require('os');
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const scanLib = require('./skills-scan');
|
|
16
16
|
const skillsCliLock = require('./skills-cli-lock');
|
|
17
|
+
const skillsConfigLib = require('./skills-config');
|
|
17
18
|
|
|
18
19
|
const STANDARD_ID = 'yotta-skills-hub/v1';
|
|
19
20
|
const STATE_VERSION = 1;
|
|
@@ -45,11 +46,13 @@ function nowIso() {
|
|
|
45
46
|
return new Date().toISOString();
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/** 解析生效 Hub(含来源):--hub > YOTTA_SKILLS_HUB > config.json > 默认。 */
|
|
50
|
+
function resolveHub(opts) {
|
|
51
|
+
return skillsConfigLib.resolveHub(opts);
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
function resolveHubDir(opts) {
|
|
49
|
-
|
|
50
|
-
const explicit = options.hub || process.env.YOTTA_SKILLS_HUB;
|
|
51
|
-
if (explicit) return path.resolve(explicit);
|
|
52
|
-
return path.join(options.homeDir || os.homedir(), '.yottaskills', 'hub');
|
|
55
|
+
return resolveHub(opts).dir;
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
function hubPaths(hubDir) {
|
|
@@ -1498,6 +1501,7 @@ module.exports = {
|
|
|
1498
1501
|
AUDIT_FILE,
|
|
1499
1502
|
TRASH_RETENTION_DAYS,
|
|
1500
1503
|
HUB_FAMILY_EXTRAS,
|
|
1504
|
+
resolveHub,
|
|
1501
1505
|
resolveHubDir,
|
|
1502
1506
|
hubPaths,
|
|
1503
1507
|
readHubState,
|
|
@@ -194,12 +194,16 @@ function whereInfo(opts) {
|
|
|
194
194
|
const options = opts || {};
|
|
195
195
|
const running = safeRealpath(process.argv[1] || '');
|
|
196
196
|
const registry = readSelfRegistry(options);
|
|
197
|
+
const hubResolved = hubLib.resolveHub(options);
|
|
197
198
|
return {
|
|
198
199
|
action: 'where',
|
|
199
200
|
running,
|
|
200
201
|
version: readPackageVersion(packageRoot()),
|
|
201
202
|
defaultDir: defaultSelfDir(options),
|
|
202
|
-
hubDir:
|
|
203
|
+
hubDir: hubResolved.dir,
|
|
204
|
+
hubSource: hubResolved.source,
|
|
205
|
+
hubConfigured: hubResolved.configured,
|
|
206
|
+
hubConfigFile: hubResolved.configFile,
|
|
203
207
|
registry: selfRegistryPath(options),
|
|
204
208
|
installs: registry.installs.map((item) => ({
|
|
205
209
|
dir: item.dir,
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* 用户级配置(0.29.2 U4)—— <root>/config.json。
|
|
4
|
+
*
|
|
5
|
+
* root = YOTTA_SKILLS_HOME 或 ~/.yottaskills(与 hosts.json / self.json 同根,
|
|
6
|
+
* 独立于 Hub 目录本身)。schema v1:{ version, hub: "<path>" | null }。
|
|
7
|
+
*
|
|
8
|
+
* Hub 解析优先级:--hub flag > YOTTA_SKILLS_HUB env > config.hub > 默认 <root>/hub。
|
|
9
|
+
* set 为 fail-closed 校验:不能是文件、不能与配置根 / 独立安装目录重叠、
|
|
10
|
+
* 不能是锁 / 数据桥接目录;路径不存在时允许(首次使用时创建)。
|
|
11
|
+
*/
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const agentDirsLib = require('./agent-dirs');
|
|
16
|
+
|
|
17
|
+
const CONFIG_VERSION = 1;
|
|
18
|
+
|
|
19
|
+
function resolveSkillsRoot(opts) {
|
|
20
|
+
const options = opts || {};
|
|
21
|
+
const env = options.env || process.env;
|
|
22
|
+
const explicit = options.skillsRoot || env.YOTTA_SKILLS_HOME;
|
|
23
|
+
if (explicit) return path.resolve(explicit);
|
|
24
|
+
return path.join(options.homeDir || os.homedir(), '.yottaskills');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function configPath(opts) {
|
|
28
|
+
return path.join(resolveSkillsRoot(opts), 'config.json');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function defaultHubDir(opts) {
|
|
32
|
+
return path.join(resolveSkillsRoot(opts), 'hub');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readConfig(opts) {
|
|
36
|
+
try {
|
|
37
|
+
const value = JSON.parse(fs.readFileSync(configPath(opts), 'utf8'));
|
|
38
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
39
|
+
return {
|
|
40
|
+
version: CONFIG_VERSION,
|
|
41
|
+
hub: typeof value.hub === 'string' && value.hub.trim() ? value.hub : null,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
} catch (_) { /* missing or corrupt config falls back to empty */ }
|
|
45
|
+
return { version: CONFIG_VERSION, hub: null };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function writeConfig(opts, config) {
|
|
49
|
+
const file = configPath(opts);
|
|
50
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
51
|
+
const tmp = file + '.tmp-' + process.pid;
|
|
52
|
+
fs.writeFileSync(tmp, JSON.stringify({
|
|
53
|
+
version: CONFIG_VERSION,
|
|
54
|
+
hub: config && config.hub ? config.hub : null,
|
|
55
|
+
}, null, 2) + '\n', 'utf8');
|
|
56
|
+
fs.renameSync(tmp, file);
|
|
57
|
+
return file;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 解析生效 Hub:返回 { dir, source, configured, configFile, defaultDir }。 */
|
|
61
|
+
function resolveHub(opts) {
|
|
62
|
+
const options = opts || {};
|
|
63
|
+
const env = options.env || process.env;
|
|
64
|
+
const config = readConfig(options);
|
|
65
|
+
const base = {
|
|
66
|
+
configured: config.hub,
|
|
67
|
+
configFile: configPath(options),
|
|
68
|
+
defaultDir: defaultHubDir(options),
|
|
69
|
+
};
|
|
70
|
+
if (options.hub) return { dir: path.resolve(options.hub), source: 'flag', ...base };
|
|
71
|
+
if (env.YOTTA_SKILLS_HUB) return { dir: path.resolve(env.YOTTA_SKILLS_HUB), source: 'env', ...base };
|
|
72
|
+
if (config.hub) return { dir: path.resolve(config.hub), source: 'config', ...base };
|
|
73
|
+
return { dir: base.defaultDir, source: 'default', ...base };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sourceLabel(source) {
|
|
77
|
+
if (source === 'flag') return '--hub 参数';
|
|
78
|
+
if (source === 'env') return 'YOTTA_SKILLS_HUB 环境变量';
|
|
79
|
+
if (source === 'config') return 'config.json 配置';
|
|
80
|
+
return '默认位置';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function safeRealpath(target) {
|
|
84
|
+
try {
|
|
85
|
+
return fs.realpathSync.native(target);
|
|
86
|
+
} catch (_) {
|
|
87
|
+
try {
|
|
88
|
+
return fs.realpathSync(target);
|
|
89
|
+
} catch (_) {
|
|
90
|
+
return path.resolve(target);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function samePath(left, right) {
|
|
96
|
+
const a = safeRealpath(left);
|
|
97
|
+
const b = safeRealpath(right);
|
|
98
|
+
if (process.platform === 'win32') return a.toLowerCase() === b.toLowerCase();
|
|
99
|
+
return a === b;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isInside(root, target) {
|
|
103
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
104
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** fail-closed 校验 Hub 路径;返回 { ok:true, dir } 或 { ok:false, error }。 */
|
|
108
|
+
function validateHubPath(input, opts) {
|
|
109
|
+
const options = opts || {};
|
|
110
|
+
const raw = String(input == null ? '' : input).trim();
|
|
111
|
+
if (!raw) return { ok: false, error: '缺少路径:hub config set --hub <path>' };
|
|
112
|
+
const dir = path.resolve(raw);
|
|
113
|
+
let stat = null;
|
|
114
|
+
try {
|
|
115
|
+
stat = fs.statSync(dir);
|
|
116
|
+
} catch (_) { /* 不存在允许:首次使用时创建 */ }
|
|
117
|
+
if (stat && !stat.isDirectory()) return { ok: false, error: '不是目录:' + dir };
|
|
118
|
+
const root = resolveSkillsRoot(options);
|
|
119
|
+
if (isInside(dir, root)) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
error: 'Hub 不能与配置根相同或包含配置根(' + root + '):config.json / hosts.json / self.json 需保持独立',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const selfDir = path.join(root, 'yotta-skills');
|
|
126
|
+
if (samePath(dir, selfDir) || isInside(selfDir, dir) || isInside(dir, selfDir)) {
|
|
127
|
+
return { ok: false, error: 'Hub 不能与独立安装目录重叠(' + selfDir + ')' };
|
|
128
|
+
}
|
|
129
|
+
if (agentDirsLib.isBridgeOnlyDir(dir, options)) {
|
|
130
|
+
return { ok: false, error: '锁 / 数据桥接目录不能作为 Hub:' + dir };
|
|
131
|
+
}
|
|
132
|
+
return { ok: true, dir };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 写入持久 Hub 覆盖;返回 { ok, hub, configFile, previous }。 */
|
|
136
|
+
function setHub(opts, input) {
|
|
137
|
+
const options = opts || {};
|
|
138
|
+
const guard = validateHubPath(input && input.hub, options);
|
|
139
|
+
if (!guard.ok) return guard;
|
|
140
|
+
const config = readConfig(options);
|
|
141
|
+
const previous = config.hub;
|
|
142
|
+
config.hub = guard.dir;
|
|
143
|
+
const file = writeConfig(options, config);
|
|
144
|
+
return { ok: true, hub: guard.dir, configFile: file, previous };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 清除持久 Hub 覆盖(恢复 flag > env > 默认)。 */
|
|
148
|
+
function clearHub(opts) {
|
|
149
|
+
const options = opts || {};
|
|
150
|
+
const config = readConfig(options);
|
|
151
|
+
if (!config.hub) return { ok: false, error: '没有可清除的 Hub 配置覆盖(当前来源不是 config.json)' };
|
|
152
|
+
const removed = config.hub;
|
|
153
|
+
config.hub = null;
|
|
154
|
+
const file = writeConfig(options, config);
|
|
155
|
+
return { ok: true, removed, configFile: file };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = {
|
|
159
|
+
CONFIG_VERSION,
|
|
160
|
+
resolveSkillsRoot,
|
|
161
|
+
configPath,
|
|
162
|
+
defaultHubDir,
|
|
163
|
+
readConfig,
|
|
164
|
+
writeConfig,
|
|
165
|
+
resolveHub,
|
|
166
|
+
sourceLabel,
|
|
167
|
+
validateHubPath,
|
|
168
|
+
setHub,
|
|
169
|
+
clearHub,
|
|
170
|
+
};
|
|
@@ -12,7 +12,24 @@ symlink),升级只改真源,已链接宿主即时生效。
|
|
|
12
12
|
- 技能台账:`<hub>/.yotta-hub.json`
|
|
13
13
|
- 链接台账:`<hub>/.yotta-links.json`
|
|
14
14
|
- 收敛回收站:`~/.yottaskills/trash/<时间戳>/<宿主>/<技能>/`(保留 7 天,可用 `YOTTA_SKILLS_TRASH` 覆盖)
|
|
15
|
-
-
|
|
15
|
+
- 覆盖方式(优先级):`--hub <dir>` > `YOTTA_SKILLS_HUB` > `config.json`(`hub config set`)> 默认
|
|
16
|
+
|
|
17
|
+
## Hub 位置持久化(0.29.2 起)
|
|
18
|
+
|
|
19
|
+
`hub config` 把 Hub 位置写进用户级配置(与 hosts.json / self.json 同根):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx -y @yottameta/yotta-skills hub config get # 生效位置 + 来源 + 配置文件
|
|
23
|
+
npx -y @yottameta/yotta-skills hub config set --hub "D:\my-hub" # 只换指针(旧 Hub 保留)
|
|
24
|
+
npx -y @yottameta/yotta-skills hub config set --hub "D:\my-hub" --move # 迁移:复制校验 → 重链 → 旧 Hub 入回收站
|
|
25
|
+
npx -y @yottameta/yotta-skills hub config clear # 清除覆盖,回到 flag > env > 默认
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- 配置文件:`<YOTTA_SKILLS_HOME>/config.json`(默认 `~/.yottaskills/config.json`;schema v1,原子写)。
|
|
29
|
+
- 校验(fail-closed):路径不能是文件、不能与配置根 / 独立安装目录重叠、不能是锁 / 数据桥接目录;路径不存在允许(首次使用时创建)。
|
|
30
|
+
- `--move` 语义:旧 Hub 复制到新位置 → 逐技能 treeHash 校验 → 切换配置 → `hub link` 以新 Hub 为源 `--force` 重链 → 旧 Hub 入回收站(保留 7 天)。校验失败不切配置;重链未全部完成时旧 Hub 保留供重试(新旧内容一致,混合链接均可用)。
|
|
31
|
+
- 面板「Hub 位置」区显示来源 / 配置覆盖 / 重启提示,并提供「设置新位置 / 清除覆盖」(写操作需页面令牌 + `hub-config` 确认串);已在运行的 `view` 需重启后生效。
|
|
32
|
+
- 误用防护:`--hub` 只允许与 `hub` / `view` / `where` 一起使用;其它命令(含裸 `--hub`)fail-closed 报错,不再静默忽略。
|
|
16
33
|
|
|
17
34
|
## 元技能唯一性收敛(`hub link` 执行时)
|
|
18
35
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# 全家技能清单(27 个)
|
|
2
2
|
|
|
3
3
|
> 机器权威源:`skills.json`(随包分发);本文件为人工可读副本,新技能发布后须同步更新
|
|
4
|
-
> `skills.json`(登记表为内部台账,不随包分发)。清单更新日期:2026-10-
|
|
4
|
+
> `skills.json`(登记表为内部台账,不随包分发)。清单更新日期:2026-10-05。
|
|
5
5
|
|
|
6
6
|
| slug | 中文名 | npm 包 | 清单版本 | 家族 | 说明 |
|
|
7
7
|
|---|---|---|---|---|
|
|
8
8
|
| `yotta-anti-shallow` | 元谨 | `@yottameta/yotta-anti-shallow` | 1.4.2 | quality | 防 AI 敷衍规则引擎(深入分析/根因追溯时激活) |
|
|
9
9
|
| `yotta-code-quality` | 元质 | `@yottameta/yotta-code-quality` | 0.4.3 | quality | 结对式代码质量评审(十二类腐化风险 + 0-100 健康分) |
|
|
10
10
|
| `yotta-workflow` | 元序 | `@yottameta/yotta-workflow` | 0.4.3 | workflow | 跨会话/跨项目通用工作流协议(状态就近存) |
|
|
11
|
-
| `yotta-memory` | 元忆 | `@yottameta/yotta-memory` | 0.22.
|
|
11
|
+
| `yotta-memory` | 元忆 | `@yottameta/yotta-memory` | 0.22.3 | memory | 有权限边界的文件式智能体记忆(FACT/PREF/BOUND/COMMIT) |
|
|
12
12
|
| `yotta-learn` | 元习 | `@yottameta/yotta-learn` | 0.2.2 | memory | 学习沉淀 CLI:错误/纠正/洞见沉淀为 .learnings/ 条目 |
|
|
13
13
|
| `yotta-security-audit` | 元安 | `@yottameta/yotta-security-audit` | 0.3.1 | security | 安全扫描引擎:13 类检测器 + 系统安全基线 + 学生数据隐私(教育版) |
|
|
14
14
|
| `yotta-vetter` | 元审 | `@yottameta/yotta-vetter` | 0.2.7 | security | 安全审查协议:四阶段 review + SAFE TO INSTALL 判定 |
|