@zhangheteng/skill-hub 0.0.0-stage → 0.2.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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +7 -0
  3. package/README.md +32 -2
  4. package/apps/cli/dist/args.js +111 -0
  5. package/apps/cli/dist/commands/audit.js +38 -0
  6. package/apps/cli/dist/commands/backup.js +34 -0
  7. package/apps/cli/dist/commands/doctor.js +86 -0
  8. package/apps/cli/dist/commands/index-cmd.js +122 -0
  9. package/apps/cli/dist/commands/ingest.js +198 -0
  10. package/apps/cli/dist/commands/init.js +110 -0
  11. package/apps/cli/dist/commands/inspect.js +68 -0
  12. package/apps/cli/dist/commands/inventory.js +259 -0
  13. package/apps/cli/dist/commands/manage.js +149 -0
  14. package/apps/cli/dist/commands/manifest.js +127 -0
  15. package/apps/cli/dist/commands/restore.js +37 -0
  16. package/apps/cli/dist/commands/route.js +92 -0
  17. package/apps/cli/dist/commands/runtime.js +89 -0
  18. package/apps/cli/dist/commands/sync.js +195 -0
  19. package/apps/cli/dist/commands/takeover.js +45 -0
  20. package/apps/cli/dist/commands/usage.js +55 -0
  21. package/apps/cli/dist/commands/verify.js +48 -0
  22. package/apps/cli/dist/index.js +468 -0
  23. package/apps/cli/dist/output.js +7 -0
  24. package/apps/mcp-server/dist/http.js +265 -0
  25. package/apps/mcp-server/dist/index.js +42 -0
  26. package/apps/mcp-server/dist/server.js +541 -0
  27. package/apps/web/dist/index.js +355 -0
  28. package/apps/web/public/app.js +927 -0
  29. package/apps/web/public/index.html +109 -0
  30. package/apps/web/public/styles.css +1796 -0
  31. package/configs/agents/default.yaml +44 -0
  32. package/configs/models.yaml +55 -0
  33. package/configs/profiles/coding.yaml +29 -0
  34. package/configs/profiles/video.yaml +18 -0
  35. package/configs/taxonomy.yaml +24 -0
  36. package/package.json +42 -4
  37. package/packages/core/dist/agent-sources.js +38 -0
  38. package/packages/core/dist/audit.js +275 -0
  39. package/packages/core/dist/catalog.js +173 -0
  40. package/packages/core/dist/config.js +295 -0
  41. package/packages/core/dist/conflict.js +95 -0
  42. package/packages/core/dist/context-cost.js +19 -0
  43. package/packages/core/dist/frontmatter.js +118 -0
  44. package/packages/core/dist/hash.js +7 -0
  45. package/packages/core/dist/history.js +60 -0
  46. package/packages/core/dist/index.js +23 -0
  47. package/packages/core/dist/managed.js +1774 -0
  48. package/packages/core/dist/materialize.js +149 -0
  49. package/packages/core/dist/parse.js +74 -0
  50. package/packages/core/dist/scan.js +112 -0
  51. package/packages/core/dist/takeover.js +96 -0
  52. package/packages/core/dist/tree.js +61 -0
  53. package/packages/core/dist/types.js +4 -0
  54. package/packages/core/dist/usage.js +267 -0
  55. package/packages/router/dist/bm25.js +144 -0
  56. package/packages/router/dist/context-cost.js +82 -0
  57. package/packages/router/dist/external.js +107 -0
  58. package/packages/router/dist/factory.js +144 -0
  59. package/packages/router/dist/hub-service.js +589 -0
  60. package/packages/router/dist/index-store.js +153 -0
  61. package/packages/router/dist/index.js +22 -0
  62. package/packages/router/dist/llm/config.js +311 -0
  63. package/packages/router/dist/llm/http.js +102 -0
  64. package/packages/router/dist/llm/index.js +6 -0
  65. package/packages/router/dist/llm/ollama.js +68 -0
  66. package/packages/router/dist/llm/openai-compatible.js +66 -0
  67. package/packages/router/dist/llm/registry.js +103 -0
  68. package/packages/router/dist/llm/types.js +31 -0
  69. package/packages/router/dist/profile.js +124 -0
  70. package/packages/router/dist/runtime.js +270 -0
  71. package/packages/router/dist/scope.js +13 -0
  72. package/packages/router/dist/taxonomy.js +172 -0
  73. package/packages/router/dist/tokenize.js +38 -0
  74. package/packages/router/dist/types.js +1 -0
  75. package/packages/router/dist/typesafe.js +331 -0
  76. package/packages/router/dist/usage-overview.js +143 -0
  77. package/packages/router/dist/usage.js +76 -0
  78. package/packages/shared/dist/constants.js +14 -0
  79. package/packages/shared/dist/errors.js +90 -0
  80. package/packages/shared/dist/index.js +14 -0
  81. package/packages/shared/dist/logger.js +95 -0
  82. package/packages/shared/dist/paths.js +118 -0
  83. package/packages/shared/dist/types.js +12 -0
  84. package/packages/sync/dist/apply.js +227 -0
  85. package/packages/sync/dist/backup.js +193 -0
  86. package/packages/sync/dist/fs-utils.js +188 -0
  87. package/packages/sync/dist/index.js +18 -0
  88. package/packages/sync/dist/integrity.js +238 -0
  89. package/packages/sync/dist/plan.js +211 -0
  90. package/packages/sync/dist/restore.js +69 -0
  91. package/packages/sync/dist/scope.js +22 -0
  92. package/packages/sync/dist/types.js +1 -0
  93. package/packages/sync/dist/verify.js +189 -0
@@ -0,0 +1,110 @@
1
+ import { access, readFile, mkdir, writeFile } from "node:fs/promises";
2
+ import { constants as fsConstants } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { createEmptyCatalog, writeCatalog } from "#skill-hub/core";
6
+ import { getDefaultHubHome, hubLayout } from "#skill-hub/shared";
7
+ import { printJson, printLines } from "../output.js";
8
+ export async function runInit(options) {
9
+ const layout = hubLayout(getDefaultHubHome());
10
+ const created = [];
11
+ const skipped = [];
12
+ for (const dir of [layout.home, layout.skills, layout.index, layout.backups]) {
13
+ try {
14
+ await access(dir, fsConstants.F_OK);
15
+ skipped.push(dir);
16
+ }
17
+ catch {
18
+ await mkdir(dir, { recursive: true });
19
+ created.push(dir);
20
+ }
21
+ }
22
+ // config.yaml
23
+ let configAction = "skipped";
24
+ try {
25
+ await access(layout.config, fsConstants.F_OK);
26
+ if (options.force) {
27
+ await writeDefaultConfig(layout.config);
28
+ configAction = "overwritten";
29
+ }
30
+ }
31
+ catch {
32
+ await writeDefaultConfig(layout.config);
33
+ configAction = "created";
34
+ created.push(layout.config);
35
+ }
36
+ // catalog.json
37
+ let catalogAction = "skipped";
38
+ try {
39
+ await access(layout.catalog, fsConstants.F_OK);
40
+ if (options.force) {
41
+ await writeCatalog(layout.catalog, createEmptyCatalog());
42
+ catalogAction = "overwritten";
43
+ }
44
+ }
45
+ catch {
46
+ await writeCatalog(layout.catalog, createEmptyCatalog());
47
+ catalogAction = "created";
48
+ created.push(layout.catalog);
49
+ }
50
+ const result = {
51
+ ok: true,
52
+ home: layout.home,
53
+ created,
54
+ skipped,
55
+ config: { path: layout.config, action: configAction },
56
+ catalog: { path: layout.catalog, action: catalogAction },
57
+ };
58
+ if (options.json)
59
+ printJson(result);
60
+ else {
61
+ printLines([
62
+ `init hub → ${layout.home}`,
63
+ ` config: ${configAction}`,
64
+ ` catalog: ${catalogAction}`,
65
+ ` created dirs: ${created.filter((c) => !c.endsWith(".yaml") && !c.endsWith(".json")).length}`,
66
+ ]);
67
+ }
68
+ return 0;
69
+ }
70
+ async function writeDefaultConfig(target) {
71
+ // 优先从仓库模板复制
72
+ const here = path.dirname(fileURLToPath(import.meta.url));
73
+ const candidates = [
74
+ path.resolve(here, "../../../../configs/agents/default.yaml"),
75
+ path.resolve(process.cwd(), "configs/agents/default.yaml"),
76
+ ];
77
+ for (const c of candidates) {
78
+ try {
79
+ await access(c, fsConstants.R_OK);
80
+ await mkdir(path.dirname(target), { recursive: true });
81
+ const template = await readFile(c, "utf8");
82
+ await writeFile(target, template.replaceAll("~/.skill-hub", getDefaultHubHome()), "utf8");
83
+ return;
84
+ }
85
+ catch {
86
+ // try next
87
+ }
88
+ }
89
+ const fallback = `version: 1
90
+ canonical_dir: ~/.skill-hub/skills
91
+ index_dir: ~/.skill-hub/index
92
+ backup_dir: ~/.skill-hub/backups
93
+ agents:
94
+ - id: workbuddy
95
+ skills_dir: ~/.workbuddy/skills
96
+ enabled: true
97
+ sources: []
98
+ router:
99
+ top_k: 5
100
+ engine: bm25
101
+ sync:
102
+ mode: symlink
103
+ conflict: report
104
+ require_backup: true
105
+ privacy:
106
+ telemetry: false
107
+ `;
108
+ await mkdir(path.dirname(target), { recursive: true });
109
+ await writeFile(target, fallback.replaceAll("~/.skill-hub", getDefaultHubHome()), "utf8");
110
+ }
@@ -0,0 +1,68 @@
1
+ import { skillExplain, skillHistory, skillPaths } from "#skill-hub/router";
2
+ import { SkillHubError } from "#skill-hub/shared";
3
+ import { printJson, printLines } from "../output.js";
4
+ export async function runExplain(options) {
5
+ const result = await skillExplain(options);
6
+ if (options.json) {
7
+ printJson(result);
8
+ return 0;
9
+ }
10
+ printLines([
11
+ `explain ${result.name}`,
12
+ ` scope: ${result.scope}`,
13
+ ` source: ${result.source ?? "unknown"}`,
14
+ ` hash: ${result.meta.hash}`,
15
+ ` revision: ${result.revision ?? "unknown"}`,
16
+ ` status: ${result.status ?? "unknown"}`,
17
+ ` recorded: ${result.meta.path}`,
18
+ ` variants: ${result.variants.length}`,
19
+ ` winner: ${result.winner?.path ?? result.meta.path}`,
20
+ ` reason: ${result.winnerReason ?? "无同名不同 hash 冲突"}`,
21
+ ` overlay: ${result.overlay.overlayOf ?? "none"}`,
22
+ ` projections: ${result.paths.agents.filter((item) => item.status === "ok").length}/${result.paths.agents.length} healthy`,
23
+ ` history: ${result.history.length} events`,
24
+ ]);
25
+ if (result.conflict) {
26
+ printLines([
27
+ " conflict variants:",
28
+ ...result.conflict.variants.map((variant) => ` - ${variant.path} ${variant.hash} ${variant.agentId ?? ""}`.trim()),
29
+ ]);
30
+ }
31
+ return 0;
32
+ }
33
+ export async function runPath(options) {
34
+ const result = await skillPaths(options);
35
+ if (options.json) {
36
+ printJson(result);
37
+ return 0;
38
+ }
39
+ printLines([
40
+ `path ${result.name}`,
41
+ ` canonical: ${result.canonicalPath} (${result.canonicalExists ? "exists" : "missing"})`,
42
+ ` recorded: ${result.recordedPath}`,
43
+ ` source: ${result.source.path ?? result.source.ref ?? "unknown"}`,
44
+ " agents:",
45
+ ...result.agents.map((agent) => ` - ${agent.agentId}${agent.enabled ? "" : " (disabled)"}: ${agent.status} → ${agent.target}${agent.detail ? ` (${agent.detail})` : ""}`),
46
+ ]);
47
+ return 0;
48
+ }
49
+ export async function runHistory(options) {
50
+ if (options.limit !== undefined && (!Number.isFinite(options.limit) || options.limit < 1)) {
51
+ throw new SkillHubError({ code: "E_CONFIG", message: "history --limit 必须是正整数" });
52
+ }
53
+ const result = await skillHistory(options);
54
+ if (options.json) {
55
+ printJson(result);
56
+ return 0;
57
+ }
58
+ printLines([
59
+ `history${result.name ? ` ${result.name}` : ""} events=${result.entries.length}`,
60
+ ` file: ${result.path}`,
61
+ ...result.entries.map((entry) => {
62
+ const subject = entry.name ?? "hub";
63
+ const target = entry.agentId ? ` agent=${entry.agentId}` : "";
64
+ return ` - ${entry.at} ${entry.type}/${entry.action ?? "event"} ${subject}${target}${entry.hash ? ` hash=${entry.hash}` : ""}`;
65
+ }),
66
+ ]);
67
+ return 0;
68
+ }
@@ -0,0 +1,259 @@
1
+ import { writeFile, mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { agentSkillSources, detectConflicts, effectiveSkillScope, loadHubConfig, mergeSkillMetas, resolveHubConfigPaths, scanSkills, selectPreferredSkill, sourceMappingToProvenance, } from "#skill-hub/core";
4
+ import { expandUserPath } from "#skill-hub/shared";
5
+ import { printJson, printLines } from "../output.js";
6
+ export async function runInventory(options) {
7
+ const config = resolveHubConfigPaths(options.configPath ? await loadHubConfig(options.configPath) : await loadHubConfig());
8
+ const scopeFilter = options.scopeFilter?.length ? new Set(options.scopeFilter) : null;
9
+ let agents = config.agents.filter((a) => a.enabled && (!scopeFilter || scopeFilter.has("user")));
10
+ if (options.agentFilter?.length) {
11
+ const set = new Set(options.agentFilter);
12
+ agents = agents.filter((a) => set.has(a.id));
13
+ }
14
+ const agentReports = [];
15
+ const sourceReports = [];
16
+ const all = [];
17
+ const sourcePresence = new Map();
18
+ for (const a of agents) {
19
+ const p = expandUserPath(a.skills_dir);
20
+ try {
21
+ const skills = await scanSkills(p, {
22
+ agentId: a.id,
23
+ source: p,
24
+ provenance: { scope: "user", sourceType: "local", sourceRef: p },
25
+ maxDepth: 4,
26
+ });
27
+ all.push(...skills);
28
+ agentReports.push({
29
+ id: a.id,
30
+ path: p,
31
+ exists: true,
32
+ count: skills.length,
33
+ skills: skills.map((s) => s.name),
34
+ scope: "user",
35
+ });
36
+ }
37
+ catch {
38
+ agentReports.push({
39
+ id: a.id,
40
+ path: p,
41
+ exists: false,
42
+ count: 0,
43
+ skills: [],
44
+ scope: "user",
45
+ });
46
+ }
47
+ }
48
+ let sources = agentSkillSources(config).filter((s) => s.enabled);
49
+ if (options.sourceFilter?.length) {
50
+ const wanted = new Set(options.sourceFilter);
51
+ sources = sources.filter((s) => wanted.has(s.id));
52
+ }
53
+ if (scopeFilter)
54
+ sources = sources.filter((s) => scopeFilter.has(s.scope));
55
+ for (const source of sources) {
56
+ const p = expandUserPath(source.path);
57
+ const provenance = sourceMappingToProvenance(source, p);
58
+ try {
59
+ const skills = await scanSkills(p, {
60
+ source: p,
61
+ provenance,
62
+ maxDepth: 5,
63
+ });
64
+ all.push(...skills);
65
+ for (const skill of skills) {
66
+ const set = sourcePresence.get(skill.name) ?? new Set();
67
+ set.add(source.id);
68
+ sourcePresence.set(skill.name, set);
69
+ }
70
+ sourceReports.push({
71
+ id: source.id,
72
+ path: p,
73
+ scope: source.scope,
74
+ source_type: source.source_type ?? "local",
75
+ source_ref: source.source_ref ?? p,
76
+ revision: source.revision,
77
+ status: source.status ?? "active",
78
+ overlay_of: source.overlay_of,
79
+ exists: true,
80
+ count: skills.length,
81
+ skills: skills.map((s) => s.name),
82
+ });
83
+ }
84
+ catch {
85
+ sourceReports.push({
86
+ id: source.id,
87
+ path: p,
88
+ scope: source.scope,
89
+ source_type: source.source_type ?? "local",
90
+ source_ref: source.source_ref ?? p,
91
+ revision: source.revision,
92
+ status: source.status ?? "active",
93
+ overlay_of: source.overlay_of,
94
+ exists: false,
95
+ count: 0,
96
+ skills: [],
97
+ });
98
+ }
99
+ }
100
+ const merged = mergeSkillMetas(all);
101
+ const conflicts = detectConflicts(all);
102
+ const unionNames = new Set(merged.map((s) => s.name));
103
+ const effective = new Map();
104
+ const byName = new Map();
105
+ for (const skill of all) {
106
+ const list = byName.get(skill.name) ?? [];
107
+ list.push(skill);
108
+ byName.set(skill.name, list);
109
+ }
110
+ for (const [name, list] of byName) {
111
+ const preferred = selectPreferredSkill(list);
112
+ if (preferred)
113
+ effective.set(name, preferred);
114
+ }
115
+ // 仅单端
116
+ const presence = new Map();
117
+ for (const s of all) {
118
+ const set = presence.get(s.name) ?? new Set();
119
+ for (const id of s.agentsPresent ?? [])
120
+ set.add(id);
121
+ // 若 scan 未标 agent,尝试从报告反推
122
+ presence.set(s.name, set);
123
+ }
124
+ for (const rep of agentReports) {
125
+ for (const name of rep.skills) {
126
+ const set = presence.get(name) ?? new Set();
127
+ set.add(rep.id);
128
+ presence.set(name, set);
129
+ }
130
+ }
131
+ const only = {};
132
+ for (const [name, agentsSet] of presence) {
133
+ if (agentsSet.size === 1) {
134
+ const id = [...agentsSet][0];
135
+ only[id] = only[id] ?? [];
136
+ only[id].push(name);
137
+ }
138
+ }
139
+ for (const k of Object.keys(only)) {
140
+ only[k].sort();
141
+ }
142
+ const onlySources = {};
143
+ for (const [name, sourceIds] of sourcePresence) {
144
+ if (sourceIds.size === 1) {
145
+ const id = [...sourceIds][0];
146
+ onlySources[id] = onlySources[id] ?? [];
147
+ onlySources[id].push(name);
148
+ }
149
+ }
150
+ for (const k of Object.keys(onlySources))
151
+ onlySources[k].sort();
152
+ const scopeCounts = {
153
+ user: 0,
154
+ workspace: 0,
155
+ project: 0,
156
+ };
157
+ for (const skill of effective.values()) {
158
+ scopeCounts[effectiveSkillScope(skill)] += 1;
159
+ }
160
+ // 两两交集规模
161
+ const overlaps = [];
162
+ for (let i = 0; i < agentReports.length; i += 1) {
163
+ for (let j = i + 1; j < agentReports.length; j += 1) {
164
+ const A = new Set(agentReports[i].skills);
165
+ const B = agentReports[j].skills;
166
+ let c = 0;
167
+ for (const n of B)
168
+ if (A.has(n))
169
+ c += 1;
170
+ overlaps.push({
171
+ a: agentReports[i].id,
172
+ b: agentReports[j].id,
173
+ count: c,
174
+ });
175
+ }
176
+ }
177
+ const result = {
178
+ generatedAt: new Date().toISOString(),
179
+ agents: agentReports.map(({ id, path: p, exists, count }) => ({
180
+ id,
181
+ path: p,
182
+ exists,
183
+ count,
184
+ scope: "user",
185
+ })),
186
+ sources: sourceReports,
187
+ scope_filter: options.scopeFilter ?? null,
188
+ scope_counts: scopeCounts,
189
+ union_count: unionNames.size,
190
+ total_scanned_entries: all.length,
191
+ overlaps,
192
+ only,
193
+ only_sources: onlySources,
194
+ effective_skills: [...effective.values()]
195
+ .sort((a, b) => a.name.localeCompare(b.name))
196
+ .map((s) => ({
197
+ name: s.name,
198
+ path: s.path,
199
+ hash: s.hash,
200
+ scope: effectiveSkillScope(s),
201
+ source: s.source ?? null,
202
+ provenance: s.provenance ?? null,
203
+ })),
204
+ conflicts: conflicts.map((c) => ({
205
+ name: c.name,
206
+ variants: c.variants,
207
+ winner: c.winner ?? null,
208
+ winner_reason: c.winnerReason ?? null,
209
+ })),
210
+ };
211
+ if (options.outPath) {
212
+ const abs = path.resolve(options.outPath);
213
+ await mkdir(path.dirname(abs), { recursive: true });
214
+ if (abs.endsWith(".json")) {
215
+ await writeFile(abs, `${JSON.stringify(result, null, 2)}\n`, "utf8");
216
+ }
217
+ else {
218
+ const md = [
219
+ `# skill-hub inventory`,
220
+ ``,
221
+ `- generatedAt: ${result.generatedAt}`,
222
+ `- union_count: ${result.union_count}`,
223
+ `- sources: ${result.sources.length}`,
224
+ `- conflicts: ${result.conflicts.length}`,
225
+ ``,
226
+ `## Agents`,
227
+ ...result.agents.map((a) => `- **${a.id}** [user]: ${a.count} @ \`${a.path}\` (${a.exists ? "ok" : "missing"})`),
228
+ ``,
229
+ `## Skill Sources`,
230
+ result.sources.length === 0
231
+ ? `- (none configured)`
232
+ : result.sources
233
+ .map((s) => `- **${s.id}** [${s.scope}]: ${s.count} @ \`${s.path}\` (${s.exists ? "ok" : "missing"})`)
234
+ .join("\n"),
235
+ ``,
236
+ `## Conflicts`,
237
+ result.conflicts.length === 0
238
+ ? `- (none)`
239
+ : result.conflicts
240
+ .map((c) => `- ${c.name} (${c.variants.length} variants; winner=${c.winner?.path ?? "?"})\n ${c.winner_reason ?? ""}`)
241
+ .join("\n"),
242
+ ``,
243
+ ].join("\n");
244
+ await writeFile(abs, md, "utf8");
245
+ }
246
+ }
247
+ if (options.json) {
248
+ printJson(result);
249
+ }
250
+ else {
251
+ printLines([
252
+ `inventory union=${result.union_count} entries=${result.total_scanned_entries} conflicts=${result.conflicts.length}`,
253
+ ...result.agents.map((a) => ` ${a.id.padEnd(12)} ${String(a.count).padStart(4)} ${a.path}`),
254
+ ...result.sources.map((s) => ` source:${s.id.padEnd(8)} ${s.scope.padEnd(9)} ${String(s.count).padStart(4)} ${s.path}`),
255
+ options.outPath ? `written: ${options.outPath}` : "",
256
+ ].filter(Boolean));
257
+ }
258
+ return 0;
259
+ }
@@ -0,0 +1,149 @@
1
+ import { lstat, stat } from "node:fs/promises";
2
+ import { appendHistoryBatch, createEmptyCatalog, loadHubConfig, manageSkills, readCatalog, resolveHubConfigPaths, upsertSkills, writeCatalog, } from "#skill-hub/core";
3
+ import { SkillHubError, getDefaultHubHome, hubLayout } from "#skill-hub/shared";
4
+ import { runIndex } from "./index-cmd.js";
5
+ import { printJson, printLines } from "../output.js";
6
+ export async function runManage(options) {
7
+ if (options.action === "check-updates" && (options.apply || options.yes)) {
8
+ throw new SkillHubError({
9
+ code: "E_CONFIG",
10
+ message: "check-updates 是只读检查,不接受 --apply 或 --yes",
11
+ });
12
+ }
13
+ const dryRun = options.dryRun || !options.apply || options.action === "list";
14
+ if (!dryRun && !options.yes)
15
+ throw new SkillHubError({
16
+ code: "E_CONFIG",
17
+ message: "写入 Hub 需要 --apply --yes;先运行默认预览",
18
+ });
19
+ const layout = hubLayout(getDefaultHubHome());
20
+ const config = resolveHubConfigPaths(await loadHubConfig(options.configPath));
21
+ // 在任何文件写入前检查 catalog;损坏文件不能被当作空库覆盖。
22
+ let catalog = createEmptyCatalog();
23
+ if (options.action !== "list" && options.action !== "check-updates") {
24
+ const exists = await lstat(layout.catalog).catch((err) => {
25
+ if (err.code === "ENOENT")
26
+ return null;
27
+ throw err;
28
+ });
29
+ if (exists)
30
+ catalog = await readCatalog(layout.catalog);
31
+ }
32
+ let overlayOf;
33
+ if (options.overlayOf !== undefined) {
34
+ overlayOf = options.overlayOf.trim();
35
+ if (!overlayOf) {
36
+ throw new SkillHubError({
37
+ code: "E_CONFIG",
38
+ message: "--overlay-of 需要 base Skill 名称",
39
+ });
40
+ }
41
+ if (options.action !== "install" && options.action !== "adopt") {
42
+ throw new SkillHubError({
43
+ code: "E_CONFIG",
44
+ message: "--overlay-of 仅适用于 install/adopt",
45
+ });
46
+ }
47
+ const base = catalog.skills[overlayOf];
48
+ if (!base) {
49
+ throw new SkillHubError({
50
+ code: "E_NOT_FOUND",
51
+ message: `overlay base 不在当前 catalog 中: ${overlayOf}`,
52
+ details: { overlayOf },
53
+ });
54
+ }
55
+ try {
56
+ if (!(await stat(base.path)).isDirectory()) {
57
+ throw new SkillHubError({
58
+ code: "E_PATH",
59
+ message: `overlay base 路径不是目录: ${overlayOf}`,
60
+ details: { name: overlayOf, path: base.path },
61
+ });
62
+ }
63
+ }
64
+ catch (err) {
65
+ if (err instanceof SkillHubError)
66
+ throw err;
67
+ if (err.code === "ENOENT") {
68
+ throw new SkillHubError({
69
+ code: "E_NOT_FOUND",
70
+ message: `overlay base 路径不存在: ${overlayOf}`,
71
+ details: { name: overlayOf, path: base.path },
72
+ cause: err,
73
+ });
74
+ }
75
+ throw err;
76
+ }
77
+ }
78
+ const result = await manageSkills({
79
+ action: options.action,
80
+ source: options.source,
81
+ names: options.names,
82
+ ref: options.ref,
83
+ overlayOf,
84
+ backupPath: options.backupPath,
85
+ planToken: options.planToken,
86
+ canonicalDir: config.canonical_dir,
87
+ stateDir: layout.home,
88
+ dryRun,
89
+ });
90
+ if (!dryRun && result.skills.length) {
91
+ const next = upsertSkills(catalog, result.skills).catalog;
92
+ await writeCatalog(layout.catalog, next);
93
+ await appendHistoryBatch(layout.history, result.items
94
+ .filter((i) => i.status === "applied")
95
+ .map((item) => ({
96
+ at: new Date().toISOString(),
97
+ type: next.skills[item.name]?.provenance?.overlayOf
98
+ ? "overlay"
99
+ : "ingest",
100
+ name: item.name,
101
+ action: item.action,
102
+ source: item.source,
103
+ path: next.skills[item.name]?.path,
104
+ hash: next.skills[item.name]?.hash,
105
+ detail: { managed: true, backupPath: item.backupPath },
106
+ })));
107
+ await runIndex({ json: true, rebuild: false, configPath: options.configPath, quiet: true });
108
+ }
109
+ if (options.json)
110
+ printJson({
111
+ ...result,
112
+ indexUpdated: !dryRun && result.skills.length > 0,
113
+ next: options.action === "list"
114
+ ? null
115
+ : options.action === "check-updates"
116
+ ? "仅检查 managed 来源锁;Hub 和 Agent 未写入。未知状态不代表已是最新。"
117
+ : dryRun
118
+ ? options.action === "update" || options.action === "migrate-source"
119
+ ? "仅生成预览;核对差异及来源后,用 --plan-token <planToken> --apply --yes 确认同一计划。"
120
+ : "仅生成预览;Hub 和 Agent 未写入。核对后才可使用 --apply --yes。"
121
+ : "Hub 处理完成;Agent 可通过 MCP 按需搜索并精确读取 Skill,无需默认 sync",
122
+ });
123
+ else {
124
+ const descriptions = new Map(result.skills.map((skill) => [skill.name, skill.description]));
125
+ printLines([
126
+ `${options.action} ${dryRun ? "(dry-run / 只读)" : "完成"}`,
127
+ ...result.items.map((item) => ` ${item.action} ${item.name}${descriptions.get(item.name) ? ` — ${descriptions.get(item.name)}` : ""} [${item.status}]${item.localStatus ? ` local=${item.localStatus} upstream=${item.upstreamStatus} update=${item.updateAvailable ?? "unknown"}` : ""}${item.message ? ` ${item.message}` : ""}${item.backupPath ? ` backup=${item.backupPath}` : ""}`),
128
+ ...result.items.flatMap((item) => (item.changes ?? []).map((change) => ` ${change.change} ${change.path} (${change.before?.kind ?? "absent"} → ${change.after?.kind ?? "absent"})`)),
129
+ ...result.items.flatMap((item) => item.previousSource && item.nextSource
130
+ ? [
131
+ ` 来源迁移: ${JSON.stringify(item.previousSource)} → ${JSON.stringify(item.nextSource)}`,
132
+ ]
133
+ : []),
134
+ ...(result.planToken
135
+ ? [
136
+ `更新确认指纹: ${result.planToken}`,
137
+ ...(dryRun
138
+ ? ["确认写入需 --plan-token <指纹> --apply --yes;token 不代替用户授权"]
139
+ : []),
140
+ ]
141
+ : []),
142
+ ...(result.issues ?? []).map((issue) => ` issue [${issue.phase}/${issue.code}]${issue.name ? ` ${issue.name}` : ""}${issue.path ? ` ${issue.path}` : ""}: ${issue.message}${issue.hint ? `;${issue.hint}` : ""}`),
143
+ ...(result.complete !== undefined ? [`检查完整: ${result.complete}`] : []),
144
+ `来源锁: ${result.lockPath}`,
145
+ ...(!dryRun ? ["catalog/index 已更新;可通过 MCP 按需加载,无需默认 sync"] : []),
146
+ ]);
147
+ }
148
+ return result.complete === false ? 3 : 0;
149
+ }