@yottameta/yotta-skills-plugin 0.29.0 → 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 +28 -3
- package/skills/yotta-skills/assets/view.html +130 -5
- package/skills/yotta-skills/bin/yotta-skills.js +465 -37
- package/skills/yotta-skills/lib/agent-dirs.js +17 -3
- package/skills/yotta-skills/lib/agent-discovery.js +109 -11
- package/skills/yotta-skills/lib/cli-help.js +17 -3
- package/skills/yotta-skills/lib/hosts-registry.js +143 -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 +131 -1
- package/skills/yotta-skills/lib/hub.js +8 -4
- package/skills/yotta-skills/lib/install-pipeline.js +39 -2
- package/skills/yotta-skills/lib/self-install.js +5 -1
- package/skills/yotta-skills/lib/skills-config.js +170 -0
- package/skills/yotta-skills/lib/skills-scan.js +25 -0
- package/skills/yotta-skills/references/hub.md +48 -1
- package/skills/yotta-skills/references/install-flow.md +5 -2
- 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
|
@@ -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: {
|
|
@@ -204,12 +215,17 @@ function hostsPayload(ctx) {
|
|
|
204
215
|
brokenLinks: hostLinks.filter((link) => link.status !== 'ok').length,
|
|
205
216
|
};
|
|
206
217
|
});
|
|
218
|
+
const registry = hostsRegistryLib.readHostsRegistry({ homeDir: ctx.homeDir, env: ctx.env });
|
|
207
219
|
return {
|
|
208
220
|
generatedAt: found.generatedAt,
|
|
209
221
|
hubDir: ctx.hubDir,
|
|
210
222
|
standard: hubLib.STANDARD_ID,
|
|
211
223
|
hosts,
|
|
212
224
|
installed: found.installed,
|
|
225
|
+
// 0.29.1 U2:面板只渲染未配对标记(与 CLI hubHostLines 共用单一真源)。
|
|
226
|
+
unpaired: discoveryLib.unpairedMarkers(found),
|
|
227
|
+
excluded: registry.excluded || [],
|
|
228
|
+
overrides: registry.overrides || [],
|
|
213
229
|
links,
|
|
214
230
|
hubSkills,
|
|
215
231
|
summary: {
|
|
@@ -219,6 +235,9 @@ function hostsPayload(ctx) {
|
|
|
219
235
|
discovered: hosts.filter((host) => host.exists && !host.verified && !host.bridgeOnly).length,
|
|
220
236
|
bridge: hosts.filter((host) => host.exists && host.bridgeOnly).length,
|
|
221
237
|
installedMarks: found.installed.length,
|
|
238
|
+
unpairedMarks: discoveryLib.unpairedMarkers(found).length,
|
|
239
|
+
excluded: (registry.excluded || []).length,
|
|
240
|
+
overrides: (registry.overrides || []).length,
|
|
222
241
|
links: links.length,
|
|
223
242
|
brokenLinks: links.filter((link) => link.status !== 'ok').length,
|
|
224
243
|
// 0.29.0 F3:宿主生命周期状态细分(面板分组 / 筛选)。
|
|
@@ -603,6 +622,110 @@ function writeHostPurge(ctx, body) {
|
|
|
603
622
|
};
|
|
604
623
|
}
|
|
605
624
|
|
|
625
|
+
// ── 宿主标记 / 目录覆盖 / 不接管(0.29.1 U2;与 CLI 同源 hosts.json) ─────────
|
|
626
|
+
function writeHostMark(ctx, body) {
|
|
627
|
+
const dir = String(body.dir || '').trim();
|
|
628
|
+
if (!dir) return { code: 400, payload: { error: '缺少目录。' } };
|
|
629
|
+
const state = String(body.state || '').trim().toLowerCase();
|
|
630
|
+
const result = hostsRegistryLib.markHost({ homeDir: ctx.homeDir, env: ctx.env }, { dir, state });
|
|
631
|
+
if (!result.ok) return { code: 400, payload: { error: result.error } };
|
|
632
|
+
auditViewHostEvent(ctx, { event: 'hosts.mark', dir: result.entry.dir, state, via: 'view' });
|
|
633
|
+
return { code: 200, payload: { action: 'hosts.mark', entry: result.entry } };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function writeHostSet(ctx, body) {
|
|
637
|
+
const agentId = String(body.agentId || '').trim().toLowerCase();
|
|
638
|
+
if (!agentId) return { code: 400, payload: { error: '缺少 agentId。' } };
|
|
639
|
+
const registryOpts = { homeDir: ctx.homeDir, env: ctx.env };
|
|
640
|
+
if (body.clear) {
|
|
641
|
+
const result = hostsRegistryLib.clearHostOverride(registryOpts, { agentId });
|
|
642
|
+
if (!result.ok) return { code: 400, payload: { error: result.error } };
|
|
643
|
+
auditViewHostEvent(ctx, { event: 'hosts.set', agentId, cleared: true, dir: result.removed.dir, via: 'view' });
|
|
644
|
+
return { code: 200, payload: { action: 'hosts.set', cleared: true, removed: result.removed } };
|
|
645
|
+
}
|
|
646
|
+
const dir = String(body.dir || '').trim();
|
|
647
|
+
const result = hostsRegistryLib.setHostOverride(registryOpts, { agentId, dir, label: body.label });
|
|
648
|
+
if (!result.ok) return { code: 400, payload: { error: result.error } };
|
|
649
|
+
auditViewHostEvent(ctx, {
|
|
650
|
+
event: 'hosts.set',
|
|
651
|
+
agentId,
|
|
652
|
+
dir: result.entry.dir,
|
|
653
|
+
label: result.entry.label,
|
|
654
|
+
via: 'view',
|
|
655
|
+
});
|
|
656
|
+
return { code: 200, payload: { action: 'hosts.set', entry: result.entry } };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function writeHostExclude(ctx, body) {
|
|
660
|
+
const target = String(body.target || '').trim();
|
|
661
|
+
if (!target) return { code: 400, payload: { error: '缺少 target(agentId 或目录)。' } };
|
|
662
|
+
const result = hostsRegistryLib.excludeHost({ homeDir: ctx.homeDir, env: ctx.env }, { target });
|
|
663
|
+
if (!result.ok) return { code: 400, payload: { error: result.error } };
|
|
664
|
+
auditViewHostEvent(ctx, {
|
|
665
|
+
event: 'hosts.exclude',
|
|
666
|
+
kind: result.entry.kind,
|
|
667
|
+
value: result.entry.value,
|
|
668
|
+
label: result.entry.label,
|
|
669
|
+
via: 'view',
|
|
670
|
+
});
|
|
671
|
+
return { code: 200, payload: { action: 'hosts.exclude', entry: result.entry, already: Boolean(result.already) } };
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function writeHostInclude(ctx, body) {
|
|
675
|
+
const target = String(body.target || '').trim();
|
|
676
|
+
if (!target) return { code: 400, payload: { error: '缺少 target(agentId 或目录)。' } };
|
|
677
|
+
const result = hostsRegistryLib.includeHost({ homeDir: ctx.homeDir, env: ctx.env }, { target });
|
|
678
|
+
if (!result.ok) return { code: 400, payload: { error: result.error } };
|
|
679
|
+
auditViewHostEvent(ctx, {
|
|
680
|
+
event: 'hosts.include',
|
|
681
|
+
kind: result.removed.kind,
|
|
682
|
+
value: result.removed.value,
|
|
683
|
+
label: result.removed.label,
|
|
684
|
+
via: 'view',
|
|
685
|
+
});
|
|
686
|
+
return { code: 200, payload: { action: 'hosts.include', removed: result.removed } };
|
|
687
|
+
}
|
|
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
|
+
|
|
606
729
|
function json(res, code, value) {
|
|
607
730
|
if (res.writableEnded) return;
|
|
608
731
|
res.writeHead(code, {
|
|
@@ -764,6 +887,11 @@ function handlePost(ctx, pathname, req, res) {
|
|
|
764
887
|
if (pathname === '/api/hosts/add') return writeHostAdd(ctx, body);
|
|
765
888
|
if (pathname === '/api/hosts/remove') return writeHostRemove(ctx, body);
|
|
766
889
|
if (pathname === '/api/hosts/purge') return writeHostPurge(ctx, body);
|
|
890
|
+
if (pathname === '/api/hosts/mark') return writeHostMark(ctx, body);
|
|
891
|
+
if (pathname === '/api/hosts/set') return writeHostSet(ctx, body);
|
|
892
|
+
if (pathname === '/api/hosts/exclude') return writeHostExclude(ctx, body);
|
|
893
|
+
if (pathname === '/api/hosts/include') return writeHostInclude(ctx, body);
|
|
894
|
+
if (pathname === '/api/hub/config') return writeHubConfig(ctx, body);
|
|
767
895
|
return { code: 404, payload: { error: 'not found' } };
|
|
768
896
|
};
|
|
769
897
|
ctx.queue(run)
|
|
@@ -781,6 +909,8 @@ function createHubViewServer(options) {
|
|
|
781
909
|
let chain = Promise.resolve();
|
|
782
910
|
const ctx = {
|
|
783
911
|
hubDir: path.resolve(opts.hubDir || hubLib.resolveHubDir({})),
|
|
912
|
+
hubSource: opts.hubSource || 'default',
|
|
913
|
+
hubFlag: Boolean(opts.hubFlag),
|
|
784
914
|
homeDir: opts.homeDir || os.homedir(),
|
|
785
915
|
env: opts.env || process.env,
|
|
786
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,
|
|
@@ -8,6 +8,7 @@ const lifecycleLib = require('./install-lifecycle');
|
|
|
8
8
|
const healthLib = require('./install-health');
|
|
9
9
|
const snapshotLib = require('./install-snapshot');
|
|
10
10
|
const hookAdapterLib = require('./hook-adapter');
|
|
11
|
+
const scanLib = require('./skills-scan');
|
|
11
12
|
|
|
12
13
|
function nowTag() {
|
|
13
14
|
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
@@ -60,6 +61,15 @@ function restoreBackup(target, backup, hadTarget) {
|
|
|
60
61
|
return false;
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
/** 0.29.1 U1「只升不降」跳过原因文案(equal / local-ahead / unknown)。 */
|
|
65
|
+
function versionSkipNote(relation, installed, target) {
|
|
66
|
+
if (relation === 'equal') return '已是最新';
|
|
67
|
+
if (relation === 'local-ahead') {
|
|
68
|
+
return '本地领先 v' + installed + ' > 目标 v' + target + ',保留不降级(--force 可强制)';
|
|
69
|
+
}
|
|
70
|
+
return '无法比较版本(本地 v' + installed + ' / 目标 v' + target + '),保留(--force 可强制)';
|
|
71
|
+
}
|
|
72
|
+
|
|
63
73
|
function createInstaller(deps) {
|
|
64
74
|
const homeDir = deps.homeDir || os.homedir();
|
|
65
75
|
const runPhase = deps.runPhase || lifecycleLib.runPhase;
|
|
@@ -83,8 +93,18 @@ function createInstaller(deps) {
|
|
|
83
93
|
return function installOne(skill, dest, opts) {
|
|
84
94
|
const target = path.join(dest, skill.slug);
|
|
85
95
|
const existingVersion = deps.readInstalledVersion(target);
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
// 0.29.1 U1「只升不降」:pin 模式目标即最终版本,入口即可判定;
|
|
97
|
+
// range / latest 模式(目标非最终版本)在 npm pack 解析出精确版本后复判(见下方)。
|
|
98
|
+
const relation = scanLib.versionRelation(existingVersion, skill.version);
|
|
99
|
+
if (!opts.force && opts.pin !== false &&
|
|
100
|
+
(relation === 'equal' || relation === 'local-ahead' || relation === 'unknown')) {
|
|
101
|
+
return {
|
|
102
|
+
skill,
|
|
103
|
+
status: 'skip',
|
|
104
|
+
version: existingVersion,
|
|
105
|
+
note: versionSkipNote(relation, existingVersion, skill.version),
|
|
106
|
+
exitCode: 0,
|
|
107
|
+
};
|
|
88
108
|
}
|
|
89
109
|
if (opts.dryRun) {
|
|
90
110
|
// 0.29.0 D1:只读预览 —— 不发网络请求、不写任何文件(含台账)。
|
|
@@ -124,6 +144,23 @@ function createInstaller(deps) {
|
|
|
124
144
|
return { skill, status: 'fail', version: null, note: packed.error, exitCode: 1 };
|
|
125
145
|
}
|
|
126
146
|
|
|
147
|
+
// 0.29.1 U1:解析出精确版本后、任何解压 / 快照 / 写入之前复判(latest / range 路径)。
|
|
148
|
+
const resolvedVersion = packed.resolved || (skill.version === 'latest' ? null : skill.version);
|
|
149
|
+
if (!opts.force && existingVersion) {
|
|
150
|
+
const resolvedRelation = resolvedVersion
|
|
151
|
+
? scanLib.versionRelation(existingVersion, resolvedVersion)
|
|
152
|
+
: 'unknown';
|
|
153
|
+
if (resolvedRelation === 'equal' || resolvedRelation === 'local-ahead' || resolvedRelation === 'unknown') {
|
|
154
|
+
return {
|
|
155
|
+
skill,
|
|
156
|
+
status: 'skip',
|
|
157
|
+
version: existingVersion,
|
|
158
|
+
note: versionSkipNote(resolvedRelation, existingVersion, resolvedVersion || skill.version),
|
|
159
|
+
exitCode: 0,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
127
164
|
const extractDir = path.join(tmp, 'extract');
|
|
128
165
|
fs.mkdirSync(extractDir, { recursive: true });
|
|
129
166
|
extracted = deps.extractTarball(packed.tarball, extractDir);
|
|
@@ -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,
|