skillspp 0.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/background-executor.js +3895 -0
- package/dist/background-executor.js.map +7 -0
- package/dist/background-worker.js +59 -0
- package/dist/background-worker.js.map +7 -0
- package/dist/cli.js +762 -913
- package/dist/cli.js.map +4 -4
- package/package.json +8 -2
- package/src/assets/ascii/logo/skillspp-logo.session.json +14630 -0
- package/src/assets/ascii/logo/skillspp-logo.txt +9 -0
|
@@ -0,0 +1,3895 @@
|
|
|
1
|
+
// ../../packages/core/src/runtime/background-tasks.ts
|
|
2
|
+
import fs13 from "node:fs";
|
|
3
|
+
import os7 from "node:os";
|
|
4
|
+
import path14 from "node:path";
|
|
5
|
+
|
|
6
|
+
// ../../packages/core/src/runtime/agents.ts
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
var STANDARD_AGENTS = {
|
|
11
|
+
universal: {
|
|
12
|
+
displayName: "Universal",
|
|
13
|
+
projectSkillsDir: ".agents/skills",
|
|
14
|
+
globalSkillsDir: ".agents/skills",
|
|
15
|
+
installMarkers: [".agents"]
|
|
16
|
+
},
|
|
17
|
+
adal: {
|
|
18
|
+
displayName: "AdaL",
|
|
19
|
+
projectSkillsDir: ".adal/skills",
|
|
20
|
+
globalSkillsDir: ".adal/skills",
|
|
21
|
+
installMarkers: [".adal"]
|
|
22
|
+
},
|
|
23
|
+
antigravity: {
|
|
24
|
+
displayName: "Antigravity",
|
|
25
|
+
projectSkillsDir: ".agent/skills",
|
|
26
|
+
globalSkillsDir: ".gemini/antigravity/skills",
|
|
27
|
+
installMarkers: [".gemini/antigravity"]
|
|
28
|
+
},
|
|
29
|
+
augment: {
|
|
30
|
+
displayName: "Augment",
|
|
31
|
+
projectSkillsDir: ".augment/skills",
|
|
32
|
+
globalSkillsDir: ".augment/skills",
|
|
33
|
+
installMarkers: [".augment"]
|
|
34
|
+
},
|
|
35
|
+
"claude-code": {
|
|
36
|
+
displayName: "Claude Code",
|
|
37
|
+
projectSkillsDir: ".claude/skills",
|
|
38
|
+
globalSkillsDir: ".claude/skills",
|
|
39
|
+
installMarkers: [".claude"]
|
|
40
|
+
},
|
|
41
|
+
"cortex-code": {
|
|
42
|
+
displayName: "Cortex Code",
|
|
43
|
+
projectSkillsDir: ".cortex/skills",
|
|
44
|
+
globalSkillsDir: ".cortex/skills",
|
|
45
|
+
installMarkers: [".cortex"]
|
|
46
|
+
},
|
|
47
|
+
crush: {
|
|
48
|
+
displayName: "Crush",
|
|
49
|
+
projectSkillsDir: ".crush/skills",
|
|
50
|
+
globalSkillsDir: ".crush/skills",
|
|
51
|
+
installMarkers: [".crush"]
|
|
52
|
+
},
|
|
53
|
+
droid: {
|
|
54
|
+
displayName: "Droid",
|
|
55
|
+
projectSkillsDir: ".factory/skills",
|
|
56
|
+
globalSkillsDir: ".factory/skills",
|
|
57
|
+
installMarkers: [".factory"]
|
|
58
|
+
},
|
|
59
|
+
goose: {
|
|
60
|
+
displayName: "Goose",
|
|
61
|
+
projectSkillsDir: ".goose/skills",
|
|
62
|
+
globalSkillsDir: ".config/goose/skills",
|
|
63
|
+
installMarkers: [".config/goose"]
|
|
64
|
+
},
|
|
65
|
+
"iflow-cli": {
|
|
66
|
+
displayName: "iFlow CLI",
|
|
67
|
+
projectSkillsDir: ".iflow/skills",
|
|
68
|
+
globalSkillsDir: ".iflow/skills",
|
|
69
|
+
installMarkers: [".iflow"]
|
|
70
|
+
},
|
|
71
|
+
junie: {
|
|
72
|
+
displayName: "Junie",
|
|
73
|
+
projectSkillsDir: ".junie/skills",
|
|
74
|
+
globalSkillsDir: ".junie/skills",
|
|
75
|
+
installMarkers: [".junie"]
|
|
76
|
+
},
|
|
77
|
+
"kiro-cli": {
|
|
78
|
+
displayName: "Kiro CLI",
|
|
79
|
+
projectSkillsDir: ".kiro/skills",
|
|
80
|
+
globalSkillsDir: ".kiro/skills",
|
|
81
|
+
installMarkers: [".kiro"]
|
|
82
|
+
},
|
|
83
|
+
kode: {
|
|
84
|
+
displayName: "Kode",
|
|
85
|
+
projectSkillsDir: ".kode/skills",
|
|
86
|
+
globalSkillsDir: ".kode/skills",
|
|
87
|
+
installMarkers: [".kode"]
|
|
88
|
+
},
|
|
89
|
+
openclaw: {
|
|
90
|
+
displayName: "OpenClaw",
|
|
91
|
+
projectSkillsDir: "skills",
|
|
92
|
+
globalSkillsDir: ".openclaw/skills",
|
|
93
|
+
installMarkers: [".openclaw"]
|
|
94
|
+
},
|
|
95
|
+
openhands: {
|
|
96
|
+
displayName: "OpenHands",
|
|
97
|
+
projectSkillsDir: ".openhands/skills",
|
|
98
|
+
globalSkillsDir: ".openhands/skills",
|
|
99
|
+
installMarkers: [".openhands"]
|
|
100
|
+
},
|
|
101
|
+
"mistral-vibe": {
|
|
102
|
+
displayName: "Mistral Vibe",
|
|
103
|
+
projectSkillsDir: ".vibe/skills",
|
|
104
|
+
globalSkillsDir: ".vibe/skills",
|
|
105
|
+
installMarkers: [".vibe"]
|
|
106
|
+
},
|
|
107
|
+
neovate: {
|
|
108
|
+
displayName: "Neovate",
|
|
109
|
+
projectSkillsDir: ".neovate/skills",
|
|
110
|
+
globalSkillsDir: ".neovate/skills",
|
|
111
|
+
installMarkers: [".neovate"]
|
|
112
|
+
},
|
|
113
|
+
pochi: {
|
|
114
|
+
displayName: "Pochi",
|
|
115
|
+
projectSkillsDir: ".pochi/skills",
|
|
116
|
+
globalSkillsDir: ".pochi/skills",
|
|
117
|
+
installMarkers: [".pochi"]
|
|
118
|
+
},
|
|
119
|
+
qoder: {
|
|
120
|
+
displayName: "Qoder",
|
|
121
|
+
projectSkillsDir: ".qoder/skills",
|
|
122
|
+
globalSkillsDir: ".qoder/skills",
|
|
123
|
+
installMarkers: [".qoder"]
|
|
124
|
+
},
|
|
125
|
+
"qwen-code": {
|
|
126
|
+
displayName: "Qwen Code",
|
|
127
|
+
projectSkillsDir: ".qwen/skills",
|
|
128
|
+
globalSkillsDir: ".qwen/skills",
|
|
129
|
+
installMarkers: [".qwen"]
|
|
130
|
+
},
|
|
131
|
+
roo: {
|
|
132
|
+
displayName: "Roo Code",
|
|
133
|
+
projectSkillsDir: ".roo/skills",
|
|
134
|
+
globalSkillsDir: ".roo/skills",
|
|
135
|
+
installMarkers: [".roo"]
|
|
136
|
+
},
|
|
137
|
+
trae: {
|
|
138
|
+
displayName: "Trae",
|
|
139
|
+
projectSkillsDir: ".trae/skills",
|
|
140
|
+
globalSkillsDir: ".trae/skills",
|
|
141
|
+
installMarkers: [".trae"]
|
|
142
|
+
},
|
|
143
|
+
"trae-cn": {
|
|
144
|
+
displayName: "Trae CN",
|
|
145
|
+
projectSkillsDir: ".trae/skills",
|
|
146
|
+
globalSkillsDir: ".trae/skills",
|
|
147
|
+
installMarkers: [".trae"]
|
|
148
|
+
},
|
|
149
|
+
windsurf: {
|
|
150
|
+
displayName: "Windsurf",
|
|
151
|
+
projectSkillsDir: ".windsurf/skills",
|
|
152
|
+
globalSkillsDir: ".codeium/windsurf/skills",
|
|
153
|
+
installMarkers: [".windsurf", ".codeium/windsurf"]
|
|
154
|
+
},
|
|
155
|
+
zencoder: {
|
|
156
|
+
displayName: "Zencoder",
|
|
157
|
+
projectSkillsDir: ".zencoder/skills",
|
|
158
|
+
globalSkillsDir: ".zencoder/skills",
|
|
159
|
+
installMarkers: [".zencoder"]
|
|
160
|
+
},
|
|
161
|
+
continue: {
|
|
162
|
+
displayName: "Continue",
|
|
163
|
+
projectSkillsDir: ".continue/skills",
|
|
164
|
+
globalSkillsDir: ".continue/skills",
|
|
165
|
+
installMarkers: [".continue"]
|
|
166
|
+
},
|
|
167
|
+
codebuddy: {
|
|
168
|
+
displayName: "CodeBuddy",
|
|
169
|
+
projectSkillsDir: ".codebuddy/skills",
|
|
170
|
+
globalSkillsDir: ".codebuddy/skills",
|
|
171
|
+
installMarkers: [".codebuddy"]
|
|
172
|
+
},
|
|
173
|
+
"command-code": {
|
|
174
|
+
displayName: "Command Code",
|
|
175
|
+
projectSkillsDir: ".commandcode/skills",
|
|
176
|
+
globalSkillsDir: ".commandcode/skills",
|
|
177
|
+
installMarkers: [".commandcode"]
|
|
178
|
+
},
|
|
179
|
+
kilo: {
|
|
180
|
+
displayName: "Kilo Code",
|
|
181
|
+
projectSkillsDir: ".kilocode/skills",
|
|
182
|
+
globalSkillsDir: ".kilocode/skills",
|
|
183
|
+
installMarkers: [".kilocode"]
|
|
184
|
+
},
|
|
185
|
+
mcpjam: {
|
|
186
|
+
displayName: "MCPJam",
|
|
187
|
+
projectSkillsDir: ".mcpjam/skills",
|
|
188
|
+
globalSkillsDir: ".mcpjam/skills",
|
|
189
|
+
installMarkers: [".mcpjam"]
|
|
190
|
+
},
|
|
191
|
+
mux: {
|
|
192
|
+
displayName: "Mux",
|
|
193
|
+
projectSkillsDir: ".mux/skills",
|
|
194
|
+
globalSkillsDir: ".mux/skills",
|
|
195
|
+
installMarkers: [".mux"]
|
|
196
|
+
},
|
|
197
|
+
pi: {
|
|
198
|
+
displayName: "Pi",
|
|
199
|
+
projectSkillsDir: ".pi/skills",
|
|
200
|
+
globalSkillsDir: ".pi/agent/skills",
|
|
201
|
+
installMarkers: [".pi"]
|
|
202
|
+
},
|
|
203
|
+
replit: {
|
|
204
|
+
displayName: "Replit",
|
|
205
|
+
projectSkillsDir: ".agents/skills",
|
|
206
|
+
globalSkillsDir: ".config/agents/skills",
|
|
207
|
+
installMarkers: [".config/agents"]
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
var AGENTS = {
|
|
211
|
+
...STANDARD_AGENTS,
|
|
212
|
+
codex: {
|
|
213
|
+
displayName: "Codex",
|
|
214
|
+
projectSkillsDir: ".agents/skills",
|
|
215
|
+
globalSkillsDir: ".codex/skills",
|
|
216
|
+
installMarkers: [".codex"]
|
|
217
|
+
},
|
|
218
|
+
cursor: {
|
|
219
|
+
displayName: "Cursor",
|
|
220
|
+
projectSkillsDir: ".agents/skills",
|
|
221
|
+
globalSkillsDir: ".cursor/skills",
|
|
222
|
+
installMarkers: [".cursor"]
|
|
223
|
+
},
|
|
224
|
+
"gemini-cli": {
|
|
225
|
+
displayName: "Gemini CLI",
|
|
226
|
+
projectSkillsDir: ".agents/skills",
|
|
227
|
+
globalSkillsDir: ".gemini/skills",
|
|
228
|
+
installMarkers: [".gemini"]
|
|
229
|
+
},
|
|
230
|
+
"github-copilot": {
|
|
231
|
+
displayName: "GitHub Copilot",
|
|
232
|
+
projectSkillsDir: ".agents/skills",
|
|
233
|
+
globalSkillsDir: ".copilot/skills",
|
|
234
|
+
installMarkers: [".copilot"]
|
|
235
|
+
},
|
|
236
|
+
amp: {
|
|
237
|
+
displayName: "Amp",
|
|
238
|
+
projectSkillsDir: ".agents/skills",
|
|
239
|
+
globalSkillsDir: ".config/agents/skills",
|
|
240
|
+
installMarkers: [".config/agents"]
|
|
241
|
+
},
|
|
242
|
+
opencode: {
|
|
243
|
+
displayName: "OpenCode",
|
|
244
|
+
projectSkillsDir: ".agents/skills",
|
|
245
|
+
globalSkillsDir: ".config/opencode/skills",
|
|
246
|
+
installMarkers: [".config/opencode"]
|
|
247
|
+
},
|
|
248
|
+
windsurf: {
|
|
249
|
+
displayName: "Windsurf",
|
|
250
|
+
projectSkillsDir: ".windsurf/skills",
|
|
251
|
+
globalSkillsDir: ".codeium/windsurf/skills",
|
|
252
|
+
installMarkers: [".windsurf", ".codeium/windsurf"]
|
|
253
|
+
},
|
|
254
|
+
cline: {
|
|
255
|
+
displayName: "Cline",
|
|
256
|
+
projectSkillsDir: ".cline/skills",
|
|
257
|
+
globalSkillsDir: ".cline/skills",
|
|
258
|
+
installMarkers: [".cline"]
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
var ALL_AGENTS = Object.keys(AGENTS);
|
|
262
|
+
function isAgent(value) {
|
|
263
|
+
return Object.prototype.hasOwnProperty.call(AGENTS, value);
|
|
264
|
+
}
|
|
265
|
+
function resolveAgents(input) {
|
|
266
|
+
if (!input || input.length === 0) {
|
|
267
|
+
const detected = detectInstalledAgents();
|
|
268
|
+
return detected.length > 0 ? detected : ["opencode", "codex"];
|
|
269
|
+
}
|
|
270
|
+
if (input.includes("*")) {
|
|
271
|
+
const detected = detectInstalledAgents();
|
|
272
|
+
return detected.length > 0 ? detected : ALL_AGENTS;
|
|
273
|
+
}
|
|
274
|
+
const out = [];
|
|
275
|
+
for (const value of input) {
|
|
276
|
+
if (!isAgent(value)) {
|
|
277
|
+
throw new Error(`Unknown agent: ${value}`);
|
|
278
|
+
}
|
|
279
|
+
if (!out.includes(value)) {
|
|
280
|
+
out.push(value);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
function getAgentSkillsDir(agent, globalInstall, cwd) {
|
|
286
|
+
const relative = globalInstall ? AGENTS[agent].globalSkillsDir : AGENTS[agent].projectSkillsDir;
|
|
287
|
+
const base = globalInstall ? os.homedir() : cwd;
|
|
288
|
+
return path.join(base, relative);
|
|
289
|
+
}
|
|
290
|
+
function detectInstalledAgents(cwd = process.cwd()) {
|
|
291
|
+
const found = [];
|
|
292
|
+
for (const agent of Object.keys(AGENTS)) {
|
|
293
|
+
if (isAgentInstalled(agent, cwd)) {
|
|
294
|
+
found.push(agent);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return found;
|
|
298
|
+
}
|
|
299
|
+
function filterInstalledAgents(agents, cwd = process.cwd()) {
|
|
300
|
+
return agents.filter((agent) => isAgentInstalled(agent, cwd));
|
|
301
|
+
}
|
|
302
|
+
function isAgentInstalled(agent, cwd) {
|
|
303
|
+
const info = AGENTS[agent];
|
|
304
|
+
const home = os.homedir();
|
|
305
|
+
if (info.installMarkers) {
|
|
306
|
+
for (const marker of info.installMarkers) {
|
|
307
|
+
if (fs.existsSync(path.join(home, marker))) {
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const projectSkillsDir = getAgentSkillsDir(agent, false, cwd);
|
|
313
|
+
if (info.projectSkillsDir !== ".agents/skills" && fs.existsSync(projectSkillsDir)) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
const globalSkillsDir = getAgentSkillsDir(agent, true, cwd);
|
|
317
|
+
if (info.globalSkillsDir !== ".config/agents/skills" && info.globalSkillsDir !== ".agents/skills" && fs.existsSync(globalSkillsDir)) {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ../../packages/core/src/runtime/check-analysis.ts
|
|
324
|
+
import fs6 from "node:fs";
|
|
325
|
+
import path7 from "node:path";
|
|
326
|
+
|
|
327
|
+
// ../../packages/core/src/sources/skills.ts
|
|
328
|
+
import fs2 from "node:fs";
|
|
329
|
+
import os2 from "node:os";
|
|
330
|
+
import path2 from "node:path";
|
|
331
|
+
import matter from "gray-matter";
|
|
332
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "__pycache__"]);
|
|
333
|
+
function resolveSourceLabel(parsedSource) {
|
|
334
|
+
switch (parsedSource.type) {
|
|
335
|
+
case "local":
|
|
336
|
+
return parsedSource.localPath;
|
|
337
|
+
case "github":
|
|
338
|
+
case "git":
|
|
339
|
+
return parsedSource.repoUrl;
|
|
340
|
+
case "well-known":
|
|
341
|
+
case "catalog":
|
|
342
|
+
return parsedSource.url;
|
|
343
|
+
default:
|
|
344
|
+
return "";
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
function stageRemoteSkillFilesToTempDir(files, options) {
|
|
348
|
+
const tmp = fs2.mkdtempSync(path2.join(os2.tmpdir(), options?.prefix || "skillspp-remote-"));
|
|
349
|
+
try {
|
|
350
|
+
for (const [relativePath, content] of files.entries()) {
|
|
351
|
+
const resolved = path2.resolve(tmp, relativePath);
|
|
352
|
+
const rel = path2.relative(tmp, resolved);
|
|
353
|
+
if (!rel || rel.startsWith("..") || path2.isAbsolute(rel)) {
|
|
354
|
+
throw new Error(`Unsafe remote skill file path: ${relativePath}`);
|
|
355
|
+
}
|
|
356
|
+
fs2.mkdirSync(path2.dirname(resolved), { recursive: true });
|
|
357
|
+
fs2.writeFileSync(resolved, content, "utf8");
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
fs2.rmSync(tmp, { recursive: true, force: true });
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
path: tmp,
|
|
365
|
+
cleanup: () => fs2.rmSync(tmp, { recursive: true, force: true })
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
function hasSkillMd(dir) {
|
|
369
|
+
const skillPath = path2.join(dir, "SKILL.md");
|
|
370
|
+
return fs2.existsSync(skillPath) && fs2.statSync(skillPath).isFile();
|
|
371
|
+
}
|
|
372
|
+
async function hasSkillMdAsync(dir) {
|
|
373
|
+
const skillPath = path2.join(dir, "SKILL.md");
|
|
374
|
+
try {
|
|
375
|
+
const stat = await fs2.promises.stat(skillPath);
|
|
376
|
+
return stat.isFile();
|
|
377
|
+
} catch {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function parseSkillMd(skillMdPath) {
|
|
382
|
+
try {
|
|
383
|
+
const raw = fs2.readFileSync(skillMdPath, "utf8");
|
|
384
|
+
const { data } = matter(raw);
|
|
385
|
+
if (typeof data.name !== "string" || typeof data.description !== "string") {
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
name: data.name,
|
|
390
|
+
description: data.description,
|
|
391
|
+
path: path2.dirname(skillMdPath)
|
|
392
|
+
};
|
|
393
|
+
} catch {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
async function parseSkillMdAsync(skillMdPath) {
|
|
398
|
+
try {
|
|
399
|
+
const raw = await fs2.promises.readFile(skillMdPath, "utf8");
|
|
400
|
+
const { data } = matter(raw);
|
|
401
|
+
if (typeof data.name !== "string" || typeof data.description !== "string") {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
name: data.name,
|
|
406
|
+
description: data.description,
|
|
407
|
+
path: path2.dirname(skillMdPath)
|
|
408
|
+
};
|
|
409
|
+
} catch {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function findSkillDirsRecursive(dir, depth, maxDepth, out) {
|
|
414
|
+
if (depth > maxDepth) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (!fs2.existsSync(dir) || !fs2.statSync(dir).isDirectory()) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (hasSkillMd(dir)) {
|
|
421
|
+
out.push(dir);
|
|
422
|
+
}
|
|
423
|
+
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
424
|
+
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
findSkillDirsRecursive(path2.join(dir, entry.name), depth + 1, maxDepth, out);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async function findSkillDirsRecursiveAsync(dir, depth, maxDepth, out) {
|
|
431
|
+
if (depth > maxDepth) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
const stat = await fs2.promises.stat(dir);
|
|
436
|
+
if (!stat.isDirectory()) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
} catch {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (await hasSkillMdAsync(dir)) {
|
|
443
|
+
out.push(dir);
|
|
444
|
+
}
|
|
445
|
+
let entries = [];
|
|
446
|
+
try {
|
|
447
|
+
entries = await fs2.promises.readdir(dir, { withFileTypes: true });
|
|
448
|
+
} catch {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
for (const entry of entries) {
|
|
452
|
+
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
await findSkillDirsRecursiveAsync(path2.join(dir, entry.name), depth + 1, maxDepth, out);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function discoverSkills(basePath) {
|
|
459
|
+
const dirsToSearch = [
|
|
460
|
+
basePath,
|
|
461
|
+
path2.join(basePath, "skills"),
|
|
462
|
+
path2.join(basePath, "skills", ".curated"),
|
|
463
|
+
path2.join(basePath, "skills", ".experimental"),
|
|
464
|
+
path2.join(basePath, "skills", ".system"),
|
|
465
|
+
path2.join(basePath, ".agents", "skills"),
|
|
466
|
+
path2.join(basePath, ".agent", "skills")
|
|
467
|
+
];
|
|
468
|
+
const skillDirs = [];
|
|
469
|
+
for (const dir of dirsToSearch) {
|
|
470
|
+
if (!fs2.existsSync(dir) || !fs2.statSync(dir).isDirectory()) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
474
|
+
if (!entry.isDirectory()) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
const candidate = path2.join(dir, entry.name);
|
|
478
|
+
if (hasSkillMd(candidate)) {
|
|
479
|
+
skillDirs.push(candidate);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (hasSkillMd(basePath)) {
|
|
484
|
+
skillDirs.unshift(basePath);
|
|
485
|
+
}
|
|
486
|
+
if (skillDirs.length === 0) {
|
|
487
|
+
findSkillDirsRecursive(basePath, 0, 5, skillDirs);
|
|
488
|
+
}
|
|
489
|
+
const seen = /* @__PURE__ */ new Set();
|
|
490
|
+
const skills = [];
|
|
491
|
+
for (const dir of skillDirs) {
|
|
492
|
+
const parsed = parseSkillMd(path2.join(dir, "SKILL.md"));
|
|
493
|
+
if (!parsed) {
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (seen.has(parsed.name)) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
seen.add(parsed.name);
|
|
500
|
+
skills.push(parsed);
|
|
501
|
+
}
|
|
502
|
+
return skills;
|
|
503
|
+
}
|
|
504
|
+
async function discoverSkillsAsync(basePath) {
|
|
505
|
+
const dirsToSearch = [
|
|
506
|
+
basePath,
|
|
507
|
+
path2.join(basePath, "skills"),
|
|
508
|
+
path2.join(basePath, "skills", ".curated"),
|
|
509
|
+
path2.join(basePath, "skills", ".experimental"),
|
|
510
|
+
path2.join(basePath, "skills", ".system"),
|
|
511
|
+
path2.join(basePath, ".agents", "skills"),
|
|
512
|
+
path2.join(basePath, ".agent", "skills")
|
|
513
|
+
];
|
|
514
|
+
const skillDirs = [];
|
|
515
|
+
for (const dir of dirsToSearch) {
|
|
516
|
+
let stat;
|
|
517
|
+
try {
|
|
518
|
+
stat = await fs2.promises.stat(dir);
|
|
519
|
+
} catch {
|
|
520
|
+
stat = void 0;
|
|
521
|
+
}
|
|
522
|
+
if (!stat || !stat.isDirectory()) {
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
let entries = [];
|
|
526
|
+
try {
|
|
527
|
+
entries = await fs2.promises.readdir(dir, { withFileTypes: true });
|
|
528
|
+
} catch {
|
|
529
|
+
entries = [];
|
|
530
|
+
}
|
|
531
|
+
for (const entry of entries) {
|
|
532
|
+
if (!entry.isDirectory()) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
const candidate = path2.join(dir, entry.name);
|
|
536
|
+
if (await hasSkillMdAsync(candidate)) {
|
|
537
|
+
skillDirs.push(candidate);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (await hasSkillMdAsync(basePath)) {
|
|
542
|
+
skillDirs.unshift(basePath);
|
|
543
|
+
}
|
|
544
|
+
if (skillDirs.length === 0) {
|
|
545
|
+
await findSkillDirsRecursiveAsync(basePath, 0, 5, skillDirs);
|
|
546
|
+
}
|
|
547
|
+
const seen = /* @__PURE__ */ new Set();
|
|
548
|
+
const skills = [];
|
|
549
|
+
for (const dir of skillDirs) {
|
|
550
|
+
const parsed = await parseSkillMdAsync(path2.join(dir, "SKILL.md"));
|
|
551
|
+
if (!parsed) {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (seen.has(parsed.name)) {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
seen.add(parsed.name);
|
|
558
|
+
skills.push(parsed);
|
|
559
|
+
}
|
|
560
|
+
return skills;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ../../packages/core/src/sources/source-parser.ts
|
|
564
|
+
import path3 from "node:path";
|
|
565
|
+
function isLocalPath(input) {
|
|
566
|
+
return path3.isAbsolute(input) || input === "." || input === ".." || input.startsWith("./") || input.startsWith("../") || /^[a-zA-Z]:[\\/]/.test(input);
|
|
567
|
+
}
|
|
568
|
+
function parseSource(input) {
|
|
569
|
+
if (input.startsWith("catalog+https://")) {
|
|
570
|
+
return { type: "catalog", url: input.slice("catalog+".length) };
|
|
571
|
+
}
|
|
572
|
+
if (isLocalPath(input)) {
|
|
573
|
+
return { type: "local", localPath: path3.resolve(input) };
|
|
574
|
+
}
|
|
575
|
+
const githubTreeWithPath = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/);
|
|
576
|
+
if (githubTreeWithPath) {
|
|
577
|
+
const [, owner, repo, ref, subpath] = githubTreeWithPath;
|
|
578
|
+
return {
|
|
579
|
+
type: "github",
|
|
580
|
+
repoUrl: `https://github.com/${owner}/${repo.replace(/\.git$/, "")}.git`,
|
|
581
|
+
ref,
|
|
582
|
+
subpath
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
const githubTree = input.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)$/);
|
|
586
|
+
if (githubTree) {
|
|
587
|
+
const [, owner, repo, ref] = githubTree;
|
|
588
|
+
return {
|
|
589
|
+
type: "github",
|
|
590
|
+
repoUrl: `https://github.com/${owner}/${repo.replace(/\.git$/, "")}.git`,
|
|
591
|
+
ref
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
const githubRepo = input.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
595
|
+
if (githubRepo) {
|
|
596
|
+
const [, owner, repo] = githubRepo;
|
|
597
|
+
return {
|
|
598
|
+
type: "github",
|
|
599
|
+
repoUrl: `https://github.com/${owner}/${repo.replace(/\.git$/, "")}.git`
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
const gitlabRepo = input.match(/gitlab\.com\/([^/]+)\/([^/]+)/);
|
|
603
|
+
if (gitlabRepo) {
|
|
604
|
+
const [, owner, repo] = gitlabRepo;
|
|
605
|
+
return {
|
|
606
|
+
type: "git",
|
|
607
|
+
repoUrl: `https://gitlab.com/${owner}/${repo.replace(/\.git$/, "")}.git`
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
const shorthand = input.match(/^([^/]+)\/([^/]+)(?:\/(.+))?$/);
|
|
611
|
+
if (shorthand && !input.includes(":") && !input.startsWith(".")) {
|
|
612
|
+
const [, owner, repo, subpath] = shorthand;
|
|
613
|
+
return {
|
|
614
|
+
type: "github",
|
|
615
|
+
repoUrl: `https://github.com/${owner}/${repo.replace(/\.git$/, "")}.git`,
|
|
616
|
+
subpath
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
if (input.startsWith("http://") || input.startsWith("https://")) {
|
|
620
|
+
if (input.endsWith(".git")) {
|
|
621
|
+
return { type: "git", repoUrl: input };
|
|
622
|
+
}
|
|
623
|
+
return { type: "well-known", url: input };
|
|
624
|
+
}
|
|
625
|
+
return { type: "git", repoUrl: input };
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ../../packages/core/src/sources/git.ts
|
|
629
|
+
import fs3 from "node:fs";
|
|
630
|
+
import os3 from "node:os";
|
|
631
|
+
import path4 from "node:path";
|
|
632
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
633
|
+
function runGit(args, cwd) {
|
|
634
|
+
const result = spawnSync("git", args, {
|
|
635
|
+
cwd,
|
|
636
|
+
encoding: "utf8",
|
|
637
|
+
stdio: "pipe"
|
|
638
|
+
});
|
|
639
|
+
if (result.status !== 0) {
|
|
640
|
+
const stderr = (result.stderr || "").trim();
|
|
641
|
+
const stdout = (result.stdout || "").trim();
|
|
642
|
+
const detail = stderr || stdout || "git command failed";
|
|
643
|
+
throw new Error(`${detail}`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function runGitAsync(args, cwd) {
|
|
647
|
+
return new Promise((resolve, reject) => {
|
|
648
|
+
const child = spawn("git", args, {
|
|
649
|
+
cwd,
|
|
650
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
651
|
+
});
|
|
652
|
+
let stdout = "";
|
|
653
|
+
let stderr = "";
|
|
654
|
+
child.stdout.on("data", (chunk) => {
|
|
655
|
+
stdout += chunk.toString();
|
|
656
|
+
});
|
|
657
|
+
child.stderr.on("data", (chunk) => {
|
|
658
|
+
stderr += chunk.toString();
|
|
659
|
+
});
|
|
660
|
+
child.on("error", (error) => {
|
|
661
|
+
reject(error);
|
|
662
|
+
});
|
|
663
|
+
child.on("close", (code) => {
|
|
664
|
+
if (code === 0) {
|
|
665
|
+
resolve();
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const detail = stderr.trim() || stdout.trim() || "git command failed";
|
|
669
|
+
reject(new Error(detail));
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
function runGitOutputAsync(args, cwd) {
|
|
674
|
+
return new Promise((resolve, reject) => {
|
|
675
|
+
const child = spawn("git", args, {
|
|
676
|
+
cwd,
|
|
677
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
678
|
+
});
|
|
679
|
+
let stdout = "";
|
|
680
|
+
let stderr = "";
|
|
681
|
+
child.stdout.on("data", (chunk) => {
|
|
682
|
+
stdout += chunk.toString();
|
|
683
|
+
});
|
|
684
|
+
child.stderr.on("data", (chunk) => {
|
|
685
|
+
stderr += chunk.toString();
|
|
686
|
+
});
|
|
687
|
+
child.on("error", (error) => {
|
|
688
|
+
reject(error);
|
|
689
|
+
});
|
|
690
|
+
child.on("close", (code) => {
|
|
691
|
+
if (code === 0) {
|
|
692
|
+
resolve(stdout.trim());
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const detail = stderr.trim() || stdout.trim() || "git command failed";
|
|
696
|
+
reject(new Error(detail));
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
function applyCheckoutRefSync(repoDir, ref) {
|
|
701
|
+
if (!ref) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
runGit(["fetch", "--depth", "1", "origin", ref], repoDir);
|
|
705
|
+
runGit(["checkout", ref], repoDir);
|
|
706
|
+
}
|
|
707
|
+
async function applyCheckoutRefAsync(repoDir, ref) {
|
|
708
|
+
if (!ref) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
await runGitAsync(["fetch", "--depth", "1", "origin", ref], repoDir);
|
|
712
|
+
await runGitAsync(["checkout", ref], repoDir);
|
|
713
|
+
}
|
|
714
|
+
function prepareSourceDir(parsed) {
|
|
715
|
+
if (parsed.type === "local") {
|
|
716
|
+
if (!fs3.existsSync(parsed.localPath)) {
|
|
717
|
+
throw new Error(`Local source not found: ${parsed.localPath}`);
|
|
718
|
+
}
|
|
719
|
+
return { basePath: parsed.localPath };
|
|
720
|
+
}
|
|
721
|
+
const tmp = fs3.mkdtempSync(path4.join(os3.tmpdir(), "skillspp-cli-"));
|
|
722
|
+
runGit(["clone", "--depth", "1", parsed.repoUrl, tmp]);
|
|
723
|
+
const ref = parsed.type === "github" ? parsed.ref : void 0;
|
|
724
|
+
applyCheckoutRefSync(tmp, ref);
|
|
725
|
+
const basePath = parsed.type === "github" && parsed.subpath ? path4.join(tmp, parsed.subpath) : tmp;
|
|
726
|
+
return {
|
|
727
|
+
basePath,
|
|
728
|
+
cleanup: () => {
|
|
729
|
+
fs3.rmSync(tmp, { recursive: true, force: true });
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
async function prepareSourceDirAsync(parsed) {
|
|
734
|
+
if (parsed.type === "local") {
|
|
735
|
+
if (!fs3.existsSync(parsed.localPath)) {
|
|
736
|
+
throw new Error(`Local source not found: ${parsed.localPath}`);
|
|
737
|
+
}
|
|
738
|
+
return { basePath: parsed.localPath };
|
|
739
|
+
}
|
|
740
|
+
const tmp = fs3.mkdtempSync(path4.join(os3.tmpdir(), "skillspp-cli-"));
|
|
741
|
+
await runGitAsync(["clone", "--depth", "1", parsed.repoUrl, tmp]);
|
|
742
|
+
const ref = parsed.type === "github" ? parsed.ref : void 0;
|
|
743
|
+
await applyCheckoutRefAsync(tmp, ref);
|
|
744
|
+
const basePath = parsed.type === "github" && parsed.subpath ? path4.join(tmp, parsed.subpath) : tmp;
|
|
745
|
+
return {
|
|
746
|
+
basePath,
|
|
747
|
+
cleanup: () => {
|
|
748
|
+
fs3.rmSync(tmp, { recursive: true, force: true });
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
async function resolveGitHeadRefAsync(repoDir) {
|
|
753
|
+
return runGitOutputAsync(["rev-parse", "HEAD"], repoDir);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// ../../packages/core/src/providers/registry.ts
|
|
757
|
+
var ProviderRegistry = class {
|
|
758
|
+
providers = [];
|
|
759
|
+
register(provider) {
|
|
760
|
+
if (this.providers.some((item) => item.id === provider.id)) {
|
|
761
|
+
throw new Error(`Provider with id '${provider.id}' already registered`);
|
|
762
|
+
}
|
|
763
|
+
this.providers.push(provider);
|
|
764
|
+
}
|
|
765
|
+
findProvider(url) {
|
|
766
|
+
for (const provider of this.providers) {
|
|
767
|
+
if (provider.match(url).matches) {
|
|
768
|
+
return provider;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
getProviders() {
|
|
774
|
+
return [...this.providers];
|
|
775
|
+
}
|
|
776
|
+
getProviderById(id) {
|
|
777
|
+
return this.providers.find((item) => item.id === id) || null;
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
var registry = new ProviderRegistry();
|
|
781
|
+
function registerProvider(provider) {
|
|
782
|
+
registry.register(provider);
|
|
783
|
+
}
|
|
784
|
+
function getProviderById(id) {
|
|
785
|
+
return registry.getProviderById(id);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// ../../packages/core/src/providers/wellknown.ts
|
|
789
|
+
import dns from "node:dns/promises";
|
|
790
|
+
import net from "node:net";
|
|
791
|
+
var DEFAULT_OPTIONS = {
|
|
792
|
+
maxDownloadBytes: 5 * 1024 * 1024,
|
|
793
|
+
timeoutMs: 1e4,
|
|
794
|
+
maxRedirects: 3,
|
|
795
|
+
maxFilesPerSkill: 128,
|
|
796
|
+
maxSkillFileBytes: 512 * 1024
|
|
797
|
+
};
|
|
798
|
+
var EXCLUDED_HOSTS = /* @__PURE__ */ new Set(["github.com", "gitlab.com", "raw.githubusercontent.com"]);
|
|
799
|
+
var SKILL_CONFIG = {
|
|
800
|
+
kind: "skills",
|
|
801
|
+
displayLabel: "well-known skills",
|
|
802
|
+
indexPath: "/.well-known/skills/index.json",
|
|
803
|
+
entryLabel: "skill",
|
|
804
|
+
requireDescription: true,
|
|
805
|
+
missingManifestMessage: (name) => `Well-known skill '${name}' is missing SKILL.md`,
|
|
806
|
+
validateName(name) {
|
|
807
|
+
if (!/^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$/.test(name)) {
|
|
808
|
+
throw new Error(`Invalid well-known skill name: ${name}`);
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
hasRequiredManifest(filePath) {
|
|
812
|
+
return filePath.toLowerCase() === "skill.md";
|
|
813
|
+
},
|
|
814
|
+
buildRemoteResult({ entry, files, sourceUrl }) {
|
|
815
|
+
return {
|
|
816
|
+
name: entry.name,
|
|
817
|
+
description: entry.description || "",
|
|
818
|
+
installName: entry.name,
|
|
819
|
+
sourceUrl,
|
|
820
|
+
sourceType: "well-known",
|
|
821
|
+
files
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
var SecureWellKnownProvider = class {
|
|
826
|
+
id = "well-known";
|
|
827
|
+
displayName = "Secure Well-Known Skills";
|
|
828
|
+
match(url) {
|
|
829
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
830
|
+
return { matches: false };
|
|
831
|
+
}
|
|
832
|
+
try {
|
|
833
|
+
const parsed = new URL(url);
|
|
834
|
+
if (EXCLUDED_HOSTS.has(parsed.hostname.toLowerCase())) {
|
|
835
|
+
return { matches: false };
|
|
836
|
+
}
|
|
837
|
+
return { matches: true, sourceIdentifier: this.getSourceIdentifier(url) };
|
|
838
|
+
} catch {
|
|
839
|
+
return { matches: false };
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
getSourceIdentifier(url) {
|
|
843
|
+
const parsed = new URL(url);
|
|
844
|
+
const pathname = parsed.pathname.replace(/\/$/, "");
|
|
845
|
+
return pathname && pathname !== "/" ? `wellknown/${parsed.hostname}${pathname}` : `wellknown/${parsed.hostname}`;
|
|
846
|
+
}
|
|
847
|
+
async fetchAllSkills(url, options = {}) {
|
|
848
|
+
return this.fetchAllResources(url, options, SKILL_CONFIG);
|
|
849
|
+
}
|
|
850
|
+
async fetchAllResources(url, options, config) {
|
|
851
|
+
const normalized = this.normalizeOptions(options);
|
|
852
|
+
const budget = { remaining: normalized.maxDownloadBytes };
|
|
853
|
+
const parsed = new URL(url);
|
|
854
|
+
if (parsed.protocol !== "https:") {
|
|
855
|
+
throw new Error("Well-known provider requires HTTPS URLs");
|
|
856
|
+
}
|
|
857
|
+
await this.assertHostAllowed(parsed.hostname, normalized);
|
|
858
|
+
const { index, resolvedBase } = await this.fetchIndex(parsed, normalized, budget, config);
|
|
859
|
+
const resources = [];
|
|
860
|
+
for (const entry of index) {
|
|
861
|
+
resources.push(
|
|
862
|
+
await this.fetchResourceByEntry(resolvedBase, entry, normalized, budget, config)
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
return resources;
|
|
866
|
+
}
|
|
867
|
+
normalizeOptions(options) {
|
|
868
|
+
return {
|
|
869
|
+
allowHosts: (options.allowHosts || []).map((value) => value.trim().toLowerCase()).filter(Boolean),
|
|
870
|
+
denyHosts: (options.denyHosts || []).map((value) => value.trim().toLowerCase()).filter(Boolean),
|
|
871
|
+
maxDownloadBytes: options.maxDownloadBytes ?? DEFAULT_OPTIONS.maxDownloadBytes,
|
|
872
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_OPTIONS.timeoutMs,
|
|
873
|
+
maxRedirects: options.maxRedirects ?? DEFAULT_OPTIONS.maxRedirects,
|
|
874
|
+
maxFilesPerSkill: options.maxFilesPerSkill ?? DEFAULT_OPTIONS.maxFilesPerSkill,
|
|
875
|
+
maxSkillFileBytes: options.maxSkillFileBytes ?? DEFAULT_OPTIONS.maxSkillFileBytes
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
async fetchIndex(parsedUrl, options, budget, config) {
|
|
879
|
+
const candidates = this.buildBaseCandidates(parsedUrl, config.indexPath);
|
|
880
|
+
for (const base of candidates) {
|
|
881
|
+
const indexUrl = `${base}${config.indexPath}`;
|
|
882
|
+
try {
|
|
883
|
+
const jsonText = await this.fetchTextWithLimit(
|
|
884
|
+
indexUrl,
|
|
885
|
+
options.maxDownloadBytes,
|
|
886
|
+
options,
|
|
887
|
+
budget
|
|
888
|
+
);
|
|
889
|
+
const parsed = JSON.parse(jsonText);
|
|
890
|
+
const validated = this.validateIndex(parsed, options.maxFilesPerSkill, config);
|
|
891
|
+
return { index: validated, resolvedBase: base };
|
|
892
|
+
} catch {
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
throw new Error(`No valid ${config.displayLabel} index found at ${config.indexPath}`);
|
|
897
|
+
}
|
|
898
|
+
buildBaseCandidates(parsed, indexPath) {
|
|
899
|
+
const origin = parsed.origin;
|
|
900
|
+
const pathname = parsed.pathname.replace(/\/$/, "");
|
|
901
|
+
const marker = indexPath.replace(/\/index\.json$/, "");
|
|
902
|
+
const out = [];
|
|
903
|
+
if (pathname.includes(marker)) {
|
|
904
|
+
const prefix = pathname.slice(0, pathname.indexOf(marker));
|
|
905
|
+
out.push(`${origin}${prefix}`.replace(/\/$/, ""));
|
|
906
|
+
if (prefix !== "") {
|
|
907
|
+
out.push(origin);
|
|
908
|
+
}
|
|
909
|
+
} else {
|
|
910
|
+
out.push(`${origin}${pathname}`.replace(/\/$/, ""));
|
|
911
|
+
if (pathname !== "") {
|
|
912
|
+
out.push(origin);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return [
|
|
916
|
+
...new Set(out.map((value) => value.endsWith("/") ? value.slice(0, -1) : value))
|
|
917
|
+
].filter(Boolean);
|
|
918
|
+
}
|
|
919
|
+
validateIndex(raw, maxFilesPerSkill, config) {
|
|
920
|
+
if (!raw || typeof raw !== "object") {
|
|
921
|
+
throw new Error("Invalid well-known index: expected object");
|
|
922
|
+
}
|
|
923
|
+
const data = raw;
|
|
924
|
+
const rows = data[config.kind];
|
|
925
|
+
if (!Array.isArray(rows)) {
|
|
926
|
+
throw new Error(`Invalid well-known index: '${config.kind}' must be an array`);
|
|
927
|
+
}
|
|
928
|
+
return rows.map((item, idx) => {
|
|
929
|
+
if (!item || typeof item !== "object") {
|
|
930
|
+
throw new Error(`Invalid well-known index entry[${idx}]`);
|
|
931
|
+
}
|
|
932
|
+
const row = item;
|
|
933
|
+
const name = String(row.name || "").trim();
|
|
934
|
+
const description = typeof row.description === "string" ? row.description.trim() : void 0;
|
|
935
|
+
const files = Array.isArray(row.files) ? row.files.map((value) => String(value)) : [];
|
|
936
|
+
if (!name || files.length === 0) {
|
|
937
|
+
throw new Error(`Invalid well-known index entry[${idx}]: missing required fields`);
|
|
938
|
+
}
|
|
939
|
+
if (config.requireDescription && !description) {
|
|
940
|
+
throw new Error(`Invalid well-known index entry[${idx}]: missing required fields`);
|
|
941
|
+
}
|
|
942
|
+
config.validateName?.(name);
|
|
943
|
+
if (files.length > maxFilesPerSkill) {
|
|
944
|
+
throw new Error(`Too many files in well-known ${config.entryLabel} '${name}'`);
|
|
945
|
+
}
|
|
946
|
+
if (!files.some((filePath) => config.hasRequiredManifest(filePath))) {
|
|
947
|
+
throw new Error(config.missingManifestMessage(name));
|
|
948
|
+
}
|
|
949
|
+
for (const file of files) {
|
|
950
|
+
this.assertSafeRelativePath(file);
|
|
951
|
+
}
|
|
952
|
+
return { name, description, files };
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
assertSafeRelativePath(filePath) {
|
|
956
|
+
if (!filePath || filePath.startsWith("/") || filePath.startsWith("\\") || filePath.includes("..") || filePath.includes("\\")) {
|
|
957
|
+
throw new Error(`Unsafe well-known file path: ${filePath}`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
async fetchResourceByEntry(resolvedBase, entry, options, budget, config) {
|
|
961
|
+
const baseUrl = `${resolvedBase}/.well-known/${config.kind}/${entry.name}`;
|
|
962
|
+
const files = /* @__PURE__ */ new Map();
|
|
963
|
+
for (const filePath of entry.files) {
|
|
964
|
+
this.assertSafeRelativePath(filePath);
|
|
965
|
+
const fileUrl = `${baseUrl}/${filePath}`;
|
|
966
|
+
const text = await this.fetchTextWithLimit(
|
|
967
|
+
fileUrl,
|
|
968
|
+
options.maxSkillFileBytes,
|
|
969
|
+
options,
|
|
970
|
+
budget
|
|
971
|
+
);
|
|
972
|
+
if (text.includes("\0")) {
|
|
973
|
+
throw new Error(`Binary content is not allowed in well-known file: ${filePath}`);
|
|
974
|
+
}
|
|
975
|
+
files.set(filePath, text);
|
|
976
|
+
}
|
|
977
|
+
const manifestPath = this.pickPrimaryManifestPath(entry.files, config);
|
|
978
|
+
return config.buildRemoteResult({
|
|
979
|
+
entry,
|
|
980
|
+
files,
|
|
981
|
+
sourceUrl: `${baseUrl}/${manifestPath}`
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
pickPrimaryManifestPath(filePaths, config) {
|
|
985
|
+
const manifests = filePaths.filter((filePath) => config.hasRequiredManifest(filePath)).sort((left, right) => {
|
|
986
|
+
const leftDepth = left.split("/").length;
|
|
987
|
+
const rightDepth = right.split("/").length;
|
|
988
|
+
if (leftDepth !== rightDepth) {
|
|
989
|
+
return leftDepth - rightDepth;
|
|
990
|
+
}
|
|
991
|
+
return left.localeCompare(right);
|
|
992
|
+
});
|
|
993
|
+
const manifestPath = manifests[0];
|
|
994
|
+
if (!manifestPath) {
|
|
995
|
+
throw new Error("Missing required manifest in well-known index entry");
|
|
996
|
+
}
|
|
997
|
+
return manifestPath;
|
|
998
|
+
}
|
|
999
|
+
async fetchTextWithLimit(url, maxPerRequestBytes, options, budget) {
|
|
1000
|
+
let currentUrl = url;
|
|
1001
|
+
let redirects = 0;
|
|
1002
|
+
while (true) {
|
|
1003
|
+
const parsed = new URL(currentUrl);
|
|
1004
|
+
if (parsed.protocol !== "https:") {
|
|
1005
|
+
throw new Error("Well-known provider only allows HTTPS fetches");
|
|
1006
|
+
}
|
|
1007
|
+
await this.assertHostAllowed(parsed.hostname, options);
|
|
1008
|
+
const controller = new AbortController();
|
|
1009
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
1010
|
+
let response;
|
|
1011
|
+
try {
|
|
1012
|
+
response = await fetch(currentUrl, {
|
|
1013
|
+
redirect: "manual",
|
|
1014
|
+
signal: controller.signal
|
|
1015
|
+
});
|
|
1016
|
+
} finally {
|
|
1017
|
+
clearTimeout(timeout);
|
|
1018
|
+
}
|
|
1019
|
+
if (response.status >= 300 && response.status < 400) {
|
|
1020
|
+
const location = response.headers.get("location");
|
|
1021
|
+
if (!location) {
|
|
1022
|
+
throw new Error(`Redirect without location for ${currentUrl}`);
|
|
1023
|
+
}
|
|
1024
|
+
redirects += 1;
|
|
1025
|
+
if (redirects > options.maxRedirects) {
|
|
1026
|
+
throw new Error(`Too many redirects for ${url}`);
|
|
1027
|
+
}
|
|
1028
|
+
currentUrl = new URL(location, currentUrl).toString();
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
if (!response.ok) {
|
|
1032
|
+
throw new Error(`Failed to fetch ${currentUrl}: ${response.status} ${response.statusText}`);
|
|
1033
|
+
}
|
|
1034
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1035
|
+
if (bytes.byteLength > maxPerRequestBytes) {
|
|
1036
|
+
throw new Error(`Exceeded per-file download limit for ${currentUrl}`);
|
|
1037
|
+
}
|
|
1038
|
+
budget.remaining -= bytes.byteLength;
|
|
1039
|
+
if (budget.remaining < 0) {
|
|
1040
|
+
throw new Error(`Exceeded total download budget while fetching ${url}`);
|
|
1041
|
+
}
|
|
1042
|
+
return new TextDecoder("utf8").decode(bytes);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
async assertHostAllowed(hostname, options) {
|
|
1046
|
+
const lowerHost = hostname.toLowerCase();
|
|
1047
|
+
if (options.denyHosts.includes(lowerHost)) {
|
|
1048
|
+
throw new Error(`Host '${hostname}' is explicitly denied`);
|
|
1049
|
+
}
|
|
1050
|
+
if (options.allowHosts.length > 0 && !options.allowHosts.includes(lowerHost)) {
|
|
1051
|
+
throw new Error(`Host '${hostname}' is not in allowed host list`);
|
|
1052
|
+
}
|
|
1053
|
+
if (isLocalHostname(lowerHost)) {
|
|
1054
|
+
throw new Error(`Local or loopback host '${hostname}' is not allowed`);
|
|
1055
|
+
}
|
|
1056
|
+
const ips = await resolveHostIps(hostname);
|
|
1057
|
+
if (ips.some((ip) => isPrivateOrLocalIp(ip))) {
|
|
1058
|
+
throw new Error(`Host '${hostname}' resolves to a private/local address`);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
function isLocalHostname(hostname) {
|
|
1063
|
+
return hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local");
|
|
1064
|
+
}
|
|
1065
|
+
async function resolveHostIps(hostname) {
|
|
1066
|
+
const out = /* @__PURE__ */ new Set();
|
|
1067
|
+
try {
|
|
1068
|
+
const entries = await dns.lookup(hostname, { all: true });
|
|
1069
|
+
for (const entry of entries) {
|
|
1070
|
+
out.add(entry.address);
|
|
1071
|
+
}
|
|
1072
|
+
} catch {
|
|
1073
|
+
}
|
|
1074
|
+
return [...out];
|
|
1075
|
+
}
|
|
1076
|
+
function isPrivateOrLocalIp(ip) {
|
|
1077
|
+
const family = net.isIP(ip);
|
|
1078
|
+
if (family === 4) {
|
|
1079
|
+
return ip.startsWith("10.") || ip.startsWith("127.") || ip.startsWith("169.254.") || ip.startsWith("192.168.") || /^172\.(1[6-9]|2\d|3[0-1])\./.test(ip);
|
|
1080
|
+
}
|
|
1081
|
+
if (family === 6) {
|
|
1082
|
+
const normalized = ip.toLowerCase();
|
|
1083
|
+
return normalized === "::1" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe80:");
|
|
1084
|
+
}
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
var wellKnownProvider = new SecureWellKnownProvider();
|
|
1088
|
+
|
|
1089
|
+
// ../../packages/core/src/providers/catalog.ts
|
|
1090
|
+
var DEFAULT_OPTIONS2 = {
|
|
1091
|
+
maxDownloadBytes: 5 * 1024 * 1024,
|
|
1092
|
+
timeoutMs: 1e4,
|
|
1093
|
+
maxFilesPerSkill: 128,
|
|
1094
|
+
maxSkillFileBytes: 512 * 1024
|
|
1095
|
+
};
|
|
1096
|
+
var HttpCatalogProvider = class {
|
|
1097
|
+
id = "catalog";
|
|
1098
|
+
displayName = "HTTP Catalog Skills";
|
|
1099
|
+
match(url) {
|
|
1100
|
+
try {
|
|
1101
|
+
const parsed = new URL(url);
|
|
1102
|
+
if (parsed.protocol !== "https:") {
|
|
1103
|
+
return { matches: false };
|
|
1104
|
+
}
|
|
1105
|
+
return { matches: true, sourceIdentifier: this.getSourceIdentifier(url) };
|
|
1106
|
+
} catch {
|
|
1107
|
+
return { matches: false };
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
getSourceIdentifier(url) {
|
|
1111
|
+
const parsed = new URL(url);
|
|
1112
|
+
return `catalog/${parsed.host}${parsed.pathname.replace(/\/+$/, "")}`;
|
|
1113
|
+
}
|
|
1114
|
+
async fetchAllSkills(url, options = {}) {
|
|
1115
|
+
return this.fetchAllResources(url, options, {
|
|
1116
|
+
kind: "skills",
|
|
1117
|
+
indexLabel: "catalog skills",
|
|
1118
|
+
resolveIndexUrl(parsed) {
|
|
1119
|
+
return parsed.pathname.endsWith(".json") ? parsed.toString() : new URL(
|
|
1120
|
+
"index.json",
|
|
1121
|
+
parsed.toString().endsWith("/") ? parsed.toString() : `${parsed.toString()}/`
|
|
1122
|
+
).toString();
|
|
1123
|
+
},
|
|
1124
|
+
requireDescription: true,
|
|
1125
|
+
missingManifestMessage: (name) => `Catalog skill '${name}' is missing SKILL.md`,
|
|
1126
|
+
hasRequiredManifest(filePath) {
|
|
1127
|
+
return filePath.toLowerCase() === "skill.md";
|
|
1128
|
+
},
|
|
1129
|
+
buildRemoteResult({ entry, files, sourceUrl }) {
|
|
1130
|
+
return {
|
|
1131
|
+
name: entry.name,
|
|
1132
|
+
description: entry.description || "",
|
|
1133
|
+
installName: entry.name,
|
|
1134
|
+
sourceUrl,
|
|
1135
|
+
sourceType: "catalog",
|
|
1136
|
+
files
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
async fetchAllResources(url, options, config) {
|
|
1142
|
+
const parsed = new URL(url);
|
|
1143
|
+
if (parsed.protocol !== "https:") {
|
|
1144
|
+
throw new Error("Catalog provider requires HTTPS URLs");
|
|
1145
|
+
}
|
|
1146
|
+
const maxDownloadBytes = options.maxDownloadBytes ?? DEFAULT_OPTIONS2.maxDownloadBytes;
|
|
1147
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_OPTIONS2.timeoutMs;
|
|
1148
|
+
const maxFilesPerSkill = options.maxFilesPerSkill ?? DEFAULT_OPTIONS2.maxFilesPerSkill;
|
|
1149
|
+
const maxSkillFileBytes = options.maxSkillFileBytes ?? DEFAULT_OPTIONS2.maxSkillFileBytes;
|
|
1150
|
+
const indexUrl = config.resolveIndexUrl(parsed);
|
|
1151
|
+
const indexText = await this.fetchTextWithLimit(
|
|
1152
|
+
indexUrl,
|
|
1153
|
+
Math.min(maxDownloadBytes, maxSkillFileBytes),
|
|
1154
|
+
timeoutMs
|
|
1155
|
+
);
|
|
1156
|
+
const index = this.validateIndex(JSON.parse(indexText), maxFilesPerSkill, config);
|
|
1157
|
+
const out = [];
|
|
1158
|
+
let remaining = maxDownloadBytes - indexText.length;
|
|
1159
|
+
const indexBase = indexUrl.slice(0, indexUrl.lastIndexOf("/") + 1);
|
|
1160
|
+
for (const row of index) {
|
|
1161
|
+
const files = /* @__PURE__ */ new Map();
|
|
1162
|
+
for (const rel of row.files) {
|
|
1163
|
+
this.assertSafeRelativePath(rel);
|
|
1164
|
+
const fileUrl = new URL(`${row.name}/${rel}`, indexBase).toString();
|
|
1165
|
+
if (remaining <= 0) {
|
|
1166
|
+
throw new Error("Catalog download budget exhausted");
|
|
1167
|
+
}
|
|
1168
|
+
const text = await this.fetchTextWithLimit(
|
|
1169
|
+
fileUrl,
|
|
1170
|
+
Math.min(remaining, maxSkillFileBytes),
|
|
1171
|
+
timeoutMs
|
|
1172
|
+
);
|
|
1173
|
+
remaining -= text.length;
|
|
1174
|
+
files.set(rel, text);
|
|
1175
|
+
}
|
|
1176
|
+
out.push(
|
|
1177
|
+
config.buildRemoteResult({
|
|
1178
|
+
entry: row,
|
|
1179
|
+
files,
|
|
1180
|
+
sourceUrl: new URL(
|
|
1181
|
+
`${row.name}/${this.pickPrimaryManifestPath(row.files, config)}`,
|
|
1182
|
+
indexBase
|
|
1183
|
+
).toString()
|
|
1184
|
+
})
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
return out;
|
|
1188
|
+
}
|
|
1189
|
+
async fetchTextWithLimit(url, maxBytes, timeoutMs) {
|
|
1190
|
+
const controller = new AbortController();
|
|
1191
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1192
|
+
try {
|
|
1193
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
1194
|
+
if (!response.ok) {
|
|
1195
|
+
throw new Error(
|
|
1196
|
+
`Catalog fetch failed (${response.status} ${response.statusText}) for ${url}`
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
1200
|
+
if (bytes.byteLength > maxBytes) {
|
|
1201
|
+
throw new Error(`Catalog file exceeds size limit for ${url}`);
|
|
1202
|
+
}
|
|
1203
|
+
return new TextDecoder("utf8").decode(bytes);
|
|
1204
|
+
} finally {
|
|
1205
|
+
clearTimeout(timeout);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
validateIndex(raw, maxFilesPerSkill, config) {
|
|
1209
|
+
if (!raw || typeof raw !== "object") {
|
|
1210
|
+
throw new Error("Invalid catalog index: expected object");
|
|
1211
|
+
}
|
|
1212
|
+
const data = raw;
|
|
1213
|
+
const rows = data[config.kind];
|
|
1214
|
+
if (!Array.isArray(rows)) {
|
|
1215
|
+
throw new Error(`Invalid catalog index: '${config.kind}' must be an array`);
|
|
1216
|
+
}
|
|
1217
|
+
return rows.map((item, idx) => {
|
|
1218
|
+
if (!item || typeof item !== "object") {
|
|
1219
|
+
throw new Error(`Invalid catalog index entry[${idx}]`);
|
|
1220
|
+
}
|
|
1221
|
+
const row = item;
|
|
1222
|
+
const name = String(row.name || "").trim();
|
|
1223
|
+
const description = typeof row.description === "string" ? row.description.trim() : void 0;
|
|
1224
|
+
const files = Array.isArray(row.files) ? row.files.map((x) => String(x)) : [];
|
|
1225
|
+
if (!name || files.length === 0) {
|
|
1226
|
+
throw new Error(`Invalid catalog index entry[${idx}]: missing required fields`);
|
|
1227
|
+
}
|
|
1228
|
+
if (config.requireDescription && !description) {
|
|
1229
|
+
throw new Error(`Invalid catalog index entry[${idx}]: missing required fields`);
|
|
1230
|
+
}
|
|
1231
|
+
if (files.length > maxFilesPerSkill) {
|
|
1232
|
+
throw new Error(`Too many files in catalog ${config.kind.slice(0, -1)} '${name}'`);
|
|
1233
|
+
}
|
|
1234
|
+
if (!files.some((filePath) => config.hasRequiredManifest(filePath))) {
|
|
1235
|
+
throw new Error(config.missingManifestMessage(name));
|
|
1236
|
+
}
|
|
1237
|
+
for (const file of files) {
|
|
1238
|
+
this.assertSafeRelativePath(file);
|
|
1239
|
+
}
|
|
1240
|
+
return { name, description, files };
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
pickPrimaryManifestPath(filePaths, config) {
|
|
1244
|
+
const manifests = filePaths.filter((filePath) => config.hasRequiredManifest(filePath)).sort((left, right) => {
|
|
1245
|
+
const leftDepth = left.split("/").length;
|
|
1246
|
+
const rightDepth = right.split("/").length;
|
|
1247
|
+
if (leftDepth !== rightDepth) {
|
|
1248
|
+
return leftDepth - rightDepth;
|
|
1249
|
+
}
|
|
1250
|
+
return left.localeCompare(right);
|
|
1251
|
+
});
|
|
1252
|
+
const manifestPath = manifests[0];
|
|
1253
|
+
if (!manifestPath) {
|
|
1254
|
+
throw new Error("Catalog entry is missing required manifest");
|
|
1255
|
+
}
|
|
1256
|
+
return manifestPath;
|
|
1257
|
+
}
|
|
1258
|
+
assertSafeRelativePath(filePath) {
|
|
1259
|
+
if (!filePath || filePath.startsWith("/") || filePath.startsWith("\\") || filePath.includes("..") || filePath.includes("\\")) {
|
|
1260
|
+
throw new Error(`Unsafe catalog file path: ${filePath}`);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
var catalogProvider = new HttpCatalogProvider();
|
|
1265
|
+
|
|
1266
|
+
// ../../packages/core/src/providers/index.ts
|
|
1267
|
+
var initialized = false;
|
|
1268
|
+
function initializeProviders() {
|
|
1269
|
+
if (initialized) {
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
registerProvider(wellKnownProvider);
|
|
1273
|
+
registerProvider(catalogProvider);
|
|
1274
|
+
initialized = true;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// ../../packages/core/src/application/experimental.ts
|
|
1278
|
+
function assertExperimentalFeatureEnabled(feature, enabled) {
|
|
1279
|
+
if (enabled) {
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
if (feature === "catalog") {
|
|
1283
|
+
throw new Error("Catalog source is experimental and requires explicit experimental mode.");
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// ../../packages/core/src/sources/source-resolution.ts
|
|
1288
|
+
async function resolveWellKnownSkills(sourceUrl, options) {
|
|
1289
|
+
initializeProviders();
|
|
1290
|
+
const provider = getProviderById("well-known");
|
|
1291
|
+
if (!provider) {
|
|
1292
|
+
throw new Error("Well-known provider is not registered");
|
|
1293
|
+
}
|
|
1294
|
+
const wellKnown = provider;
|
|
1295
|
+
return wellKnown.fetchAllSkills(sourceUrl, {
|
|
1296
|
+
allowHosts: options.allowHost,
|
|
1297
|
+
denyHosts: options.denyHost,
|
|
1298
|
+
maxDownloadBytes: options.maxDownloadBytes
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
async function resolveCatalogSkills(sourceUrl, options) {
|
|
1302
|
+
assertExperimentalFeatureEnabled("catalog", Boolean(options.experimental));
|
|
1303
|
+
initializeProviders();
|
|
1304
|
+
const provider = getProviderById("catalog");
|
|
1305
|
+
if (!provider) {
|
|
1306
|
+
throw new Error("Catalog provider is not registered");
|
|
1307
|
+
}
|
|
1308
|
+
const catalog = provider;
|
|
1309
|
+
return catalog.fetchAllSkills(sourceUrl, {
|
|
1310
|
+
allowHosts: options.allowHost,
|
|
1311
|
+
denyHosts: options.denyHost,
|
|
1312
|
+
maxDownloadBytes: options.maxDownloadBytes
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// ../../packages/core/src/runtime/hash.ts
|
|
1317
|
+
import fs4 from "node:fs";
|
|
1318
|
+
import path5 from "node:path";
|
|
1319
|
+
import { createHash } from "node:crypto";
|
|
1320
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", "__pycache__"]);
|
|
1321
|
+
var SKIP_FILES = /* @__PURE__ */ new Set(["skillspp-lock.json", "skillspp-lock.yaml"]);
|
|
1322
|
+
function walkDir(baseDir, dir, hash) {
|
|
1323
|
+
const entries = fs4.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
1324
|
+
for (const entry of entries) {
|
|
1325
|
+
if (entry.isDirectory() && SKIP_DIRS2.has(entry.name)) {
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
const fullPath = path5.join(dir, entry.name);
|
|
1329
|
+
const relativePath = path5.relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
1330
|
+
if (entry.isDirectory()) {
|
|
1331
|
+
hash.update(`dir:${relativePath}
|
|
1332
|
+
`);
|
|
1333
|
+
walkDir(baseDir, fullPath, hash);
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
if (entry.isFile()) {
|
|
1337
|
+
if (SKIP_FILES.has(entry.name)) {
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
const content = fs4.readFileSync(fullPath);
|
|
1341
|
+
hash.update(`file:${relativePath}
|
|
1342
|
+
`);
|
|
1343
|
+
hash.update(content);
|
|
1344
|
+
hash.update("\n");
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
function waitForNextTurn() {
|
|
1349
|
+
return new Promise((resolve) => {
|
|
1350
|
+
setImmediate(resolve);
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
async function walkDirAsync(baseDir, dir, hash) {
|
|
1354
|
+
const entries = (await fs4.promises.readdir(dir, { withFileTypes: true })).sort(
|
|
1355
|
+
(a, b) => a.name.localeCompare(b.name)
|
|
1356
|
+
);
|
|
1357
|
+
for (const entry of entries) {
|
|
1358
|
+
await waitForNextTurn();
|
|
1359
|
+
if (entry.isDirectory() && SKIP_DIRS2.has(entry.name)) {
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
const fullPath = path5.join(dir, entry.name);
|
|
1363
|
+
const relativePath = path5.relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
1364
|
+
if (entry.isDirectory()) {
|
|
1365
|
+
hash.update(`dir:${relativePath}
|
|
1366
|
+
`);
|
|
1367
|
+
await walkDirAsync(baseDir, fullPath, hash);
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
if (entry.isFile()) {
|
|
1371
|
+
if (SKIP_FILES.has(entry.name)) {
|
|
1372
|
+
continue;
|
|
1373
|
+
}
|
|
1374
|
+
const content = await fs4.promises.readFile(fullPath);
|
|
1375
|
+
hash.update(`file:${relativePath}
|
|
1376
|
+
`);
|
|
1377
|
+
hash.update(content);
|
|
1378
|
+
hash.update("\n");
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
function hashDirectory(dirPath) {
|
|
1383
|
+
const resolved = path5.resolve(dirPath);
|
|
1384
|
+
if (!fs4.existsSync(resolved) || !fs4.statSync(resolved).isDirectory()) {
|
|
1385
|
+
throw new Error(`Directory not found for hashing: ${resolved}`);
|
|
1386
|
+
}
|
|
1387
|
+
const hash = createHash("sha256");
|
|
1388
|
+
walkDir(resolved, resolved, hash);
|
|
1389
|
+
return hash.digest("hex");
|
|
1390
|
+
}
|
|
1391
|
+
async function hashDirectoryAsync(dirPath) {
|
|
1392
|
+
const resolved = path5.resolve(dirPath);
|
|
1393
|
+
const stats = await fs4.promises.stat(resolved).catch(() => null);
|
|
1394
|
+
if (!stats || !stats.isDirectory()) {
|
|
1395
|
+
throw new Error(`Directory not found for hashing: ${resolved}`);
|
|
1396
|
+
}
|
|
1397
|
+
const hash = createHash("sha256");
|
|
1398
|
+
await walkDirAsync(resolved, resolved, hash);
|
|
1399
|
+
return hash.digest("hex");
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// ../../packages/core/src/runtime/lockfile.ts
|
|
1403
|
+
import fs5 from "node:fs";
|
|
1404
|
+
import path6 from "node:path";
|
|
1405
|
+
import YAML from "yaml";
|
|
1406
|
+
function resolveSourceLoadInput(source) {
|
|
1407
|
+
return source.type === "local" && source.canonical ? source.canonical : source.input;
|
|
1408
|
+
}
|
|
1409
|
+
function buildSourceIdentityCacheKey(parsed) {
|
|
1410
|
+
if (parsed.type === "local") {
|
|
1411
|
+
return `local:${parsed.localPath}`;
|
|
1412
|
+
}
|
|
1413
|
+
if (parsed.type === "well-known") {
|
|
1414
|
+
return `well-known:${parsed.url}`;
|
|
1415
|
+
}
|
|
1416
|
+
if (parsed.type === "catalog") {
|
|
1417
|
+
return `catalog:${parsed.url}`;
|
|
1418
|
+
}
|
|
1419
|
+
if (parsed.type === "github") {
|
|
1420
|
+
return `github:${parsed.repoUrl}:${parsed.ref || ""}:${parsed.subpath || ""}`;
|
|
1421
|
+
}
|
|
1422
|
+
return `git:${parsed.repoUrl}`;
|
|
1423
|
+
}
|
|
1424
|
+
function buildSourceLoadCacheKey(source) {
|
|
1425
|
+
return buildSourceIdentityCacheKey(parseSource(resolveSourceLoadInput(source)));
|
|
1426
|
+
}
|
|
1427
|
+
function perSkillLockfilePath(canonicalDir, format = "json") {
|
|
1428
|
+
if (format === "yaml") {
|
|
1429
|
+
return path6.join(canonicalDir, "skillspp-lock.yaml");
|
|
1430
|
+
}
|
|
1431
|
+
return path6.join(canonicalDir, "skillspp-lock.json");
|
|
1432
|
+
}
|
|
1433
|
+
function parseLockPayload(text, format) {
|
|
1434
|
+
const raw = format === "json" ? JSON.parse(text) : YAML.parse(text);
|
|
1435
|
+
if (!raw || raw.version !== 1) {
|
|
1436
|
+
return null;
|
|
1437
|
+
}
|
|
1438
|
+
return raw;
|
|
1439
|
+
}
|
|
1440
|
+
function readPerSkillLockfile(canonicalDir) {
|
|
1441
|
+
const jsonPath = perSkillLockfilePath(canonicalDir, "json");
|
|
1442
|
+
const yamlPath = perSkillLockfilePath(canonicalDir, "yaml");
|
|
1443
|
+
const raw = fs5.existsSync(jsonPath) ? parseLockPayload(fs5.readFileSync(jsonPath, "utf8"), "json") : fs5.existsSync(yamlPath) ? parseLockPayload(fs5.readFileSync(yamlPath, "utf8"), "yaml") : null;
|
|
1444
|
+
if (!raw) {
|
|
1445
|
+
return null;
|
|
1446
|
+
}
|
|
1447
|
+
if (raw.entry && typeof raw.entry.skillName === "string") {
|
|
1448
|
+
return raw.entry;
|
|
1449
|
+
}
|
|
1450
|
+
if (Array.isArray(raw.entries) && raw.entries[0] && typeof raw.entries[0].skillName === "string") {
|
|
1451
|
+
return raw.entries[0];
|
|
1452
|
+
}
|
|
1453
|
+
return null;
|
|
1454
|
+
}
|
|
1455
|
+
function writePerSkillLockfile(canonicalDir, entry, format) {
|
|
1456
|
+
const jsonPath = perSkillLockfilePath(canonicalDir, "json");
|
|
1457
|
+
const yamlPath = perSkillLockfilePath(canonicalDir, "yaml");
|
|
1458
|
+
fs5.mkdirSync(canonicalDir, { recursive: true });
|
|
1459
|
+
if (format === "yaml") {
|
|
1460
|
+
fs5.writeFileSync(yamlPath, YAML.stringify({ version: 1, entry }), "utf8");
|
|
1461
|
+
if (fs5.existsSync(jsonPath)) {
|
|
1462
|
+
fs5.rmSync(jsonPath, { force: true });
|
|
1463
|
+
}
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
fs5.writeFileSync(jsonPath, `${JSON.stringify({ version: 1, entry }, null, 2)}
|
|
1467
|
+
`, "utf8");
|
|
1468
|
+
if (fs5.existsSync(yamlPath)) {
|
|
1469
|
+
fs5.rmSync(yamlPath, { force: true });
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
function isSkillDirEntry(entry) {
|
|
1473
|
+
return entry.isDirectory() || entry.isSymbolicLink();
|
|
1474
|
+
}
|
|
1475
|
+
function listInstalledResourceDirs(_kind, globalInstall, cwd) {
|
|
1476
|
+
const out = /* @__PURE__ */ new Set();
|
|
1477
|
+
for (const agent of Object.keys(AGENTS)) {
|
|
1478
|
+
const resourceRoot = getAgentSkillsDir(agent, globalInstall, cwd);
|
|
1479
|
+
if (!fs5.existsSync(resourceRoot) || !fs5.statSync(resourceRoot).isDirectory()) {
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
for (const entry of fs5.readdirSync(resourceRoot, { withFileTypes: true })) {
|
|
1483
|
+
if (!isSkillDirEntry(entry)) {
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
out.add(path6.join(resourceRoot, entry.name));
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return [...out];
|
|
1490
|
+
}
|
|
1491
|
+
function lockEntrySortTime(entry) {
|
|
1492
|
+
const parsed = Date.parse(entry.updatedAt);
|
|
1493
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
1494
|
+
}
|
|
1495
|
+
function readLockfile(globalInstall, cwd) {
|
|
1496
|
+
return readResourceLockfile("skill", globalInstall, cwd);
|
|
1497
|
+
}
|
|
1498
|
+
function readResourceLockfile(kind, globalInstall, cwd) {
|
|
1499
|
+
const entriesBySkill = /* @__PURE__ */ new Map();
|
|
1500
|
+
for (const skillDir of listInstalledResourceDirs(kind, globalInstall, cwd)) {
|
|
1501
|
+
const entry = readPerSkillLockfile(skillDir);
|
|
1502
|
+
if (!entry || typeof entry.skillName !== "string") {
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
const existing = entriesBySkill.get(entry.skillName);
|
|
1506
|
+
if (!existing || lockEntrySortTime(entry) > lockEntrySortTime(existing)) {
|
|
1507
|
+
entriesBySkill.set(entry.skillName, entry);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
return {
|
|
1511
|
+
version: 1,
|
|
1512
|
+
entries: [...entriesBySkill.values()].sort((a, b) => a.skillName.localeCompare(b.skillName))
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
function writeLockfile(globalInstall, cwd, lockfile, format = "json") {
|
|
1516
|
+
writeResourceLockfile("skill", globalInstall, cwd, lockfile, format);
|
|
1517
|
+
}
|
|
1518
|
+
function writeResourceLockfile(_kind, _globalInstall, _cwd, lockfile, format = "json") {
|
|
1519
|
+
const normalized = [...lockfile.entries].sort((a, b) => a.skillName.localeCompare(b.skillName));
|
|
1520
|
+
for (const entry of normalized) {
|
|
1521
|
+
writePerSkillLockfile(entry.canonicalDir, entry, format);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
function upsertLockEntry(lockfile, entry) {
|
|
1525
|
+
return upsertResourceLockEntry(lockfile, entry);
|
|
1526
|
+
}
|
|
1527
|
+
function upsertResourceLockEntry(lockfile, entry) {
|
|
1528
|
+
const next = lockfile.entries.filter((item) => item.skillName !== entry.skillName);
|
|
1529
|
+
next.push(entry);
|
|
1530
|
+
return { version: 1, entries: next };
|
|
1531
|
+
}
|
|
1532
|
+
function listCanonicalSkillDirs(globalInstall, cwd) {
|
|
1533
|
+
return listCanonicalResourceDirs("skill", globalInstall, cwd);
|
|
1534
|
+
}
|
|
1535
|
+
function listCanonicalResourceDirs(kind, globalInstall, cwd) {
|
|
1536
|
+
return [
|
|
1537
|
+
...new Set(
|
|
1538
|
+
listInstalledResourceDirs(kind, globalInstall, cwd).map((dir) => path6.basename(dir))
|
|
1539
|
+
)
|
|
1540
|
+
].sort((a, b) => a.localeCompare(b));
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// ../../packages/core/src/runtime/check-analysis.ts
|
|
1544
|
+
function includeBySkill(entry, selected) {
|
|
1545
|
+
if (!selected || selected.length === 0 || selected.includes("*")) {
|
|
1546
|
+
return true;
|
|
1547
|
+
}
|
|
1548
|
+
return selected.includes(entry.skillName);
|
|
1549
|
+
}
|
|
1550
|
+
function migrateHint(skillName) {
|
|
1551
|
+
return `skillspp update ${skillName} --migrate <new-skill-source>`;
|
|
1552
|
+
}
|
|
1553
|
+
function resolveSourceMetadataIssue(entry) {
|
|
1554
|
+
if (!entry.source.canonical) {
|
|
1555
|
+
return `source canonical metadata missing; run ${migrateHint(entry.skillName)}`;
|
|
1556
|
+
}
|
|
1557
|
+
if (entry.source.type === "local") {
|
|
1558
|
+
if (!entry.source.resolvedPath) {
|
|
1559
|
+
return `local source metadata missing; run ${migrateHint(entry.skillName)}`;
|
|
1560
|
+
}
|
|
1561
|
+
if (!fs6.existsSync(entry.source.canonical)) {
|
|
1562
|
+
return `local source path not found; run ${migrateHint(entry.skillName)}`;
|
|
1563
|
+
}
|
|
1564
|
+
return null;
|
|
1565
|
+
}
|
|
1566
|
+
if (!entry.source.pinnedRef) {
|
|
1567
|
+
return `remote pin metadata missing; run ${migrateHint(entry.skillName)}`;
|
|
1568
|
+
}
|
|
1569
|
+
return null;
|
|
1570
|
+
}
|
|
1571
|
+
function createRetainedCleanup(cleanup) {
|
|
1572
|
+
let cleaned = false;
|
|
1573
|
+
let refCount = 0;
|
|
1574
|
+
const runCleanup = () => {
|
|
1575
|
+
if (cleaned) {
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
cleaned = true;
|
|
1579
|
+
cleanup?.();
|
|
1580
|
+
};
|
|
1581
|
+
const cleanupNow = () => {
|
|
1582
|
+
if (refCount === 0) {
|
|
1583
|
+
runCleanup();
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
const retainCleanup = () => {
|
|
1587
|
+
refCount += 1;
|
|
1588
|
+
let released = false;
|
|
1589
|
+
return () => {
|
|
1590
|
+
if (released) {
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
released = true;
|
|
1594
|
+
refCount -= 1;
|
|
1595
|
+
if (refCount === 0) {
|
|
1596
|
+
runCleanup();
|
|
1597
|
+
}
|
|
1598
|
+
};
|
|
1599
|
+
};
|
|
1600
|
+
return {
|
|
1601
|
+
cleanupNow,
|
|
1602
|
+
retainCleanup
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
function toAsyncResult(promise) {
|
|
1606
|
+
return promise.then(
|
|
1607
|
+
(value) => ({ ok: true, value }),
|
|
1608
|
+
(error) => ({ ok: false, error })
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
function unwrapAsyncResult(result) {
|
|
1612
|
+
if (result.ok) {
|
|
1613
|
+
return result.value;
|
|
1614
|
+
}
|
|
1615
|
+
throw result.error;
|
|
1616
|
+
}
|
|
1617
|
+
async function loadCachedSource(entry, options) {
|
|
1618
|
+
const sourceInput = resolveSourceLoadInput(entry.source);
|
|
1619
|
+
const parsed = parseSource(sourceInput);
|
|
1620
|
+
if (parsed.type === "well-known" || parsed.type === "catalog") {
|
|
1621
|
+
const remoteSkills = parsed.type === "well-known" ? await resolveWellKnownSkills(parsed.url, options) : await resolveCatalogSkills(parsed.url, options);
|
|
1622
|
+
return {
|
|
1623
|
+
kind: "remote",
|
|
1624
|
+
remoteSkills
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
const prepared = await prepareSourceDirAsync(
|
|
1628
|
+
parsed
|
|
1629
|
+
);
|
|
1630
|
+
const retainedCleanup = createRetainedCleanup(prepared.cleanup);
|
|
1631
|
+
try {
|
|
1632
|
+
const skills = await discoverSkillsAsync(prepared.basePath);
|
|
1633
|
+
return {
|
|
1634
|
+
kind: "prepared",
|
|
1635
|
+
basePath: prepared.basePath,
|
|
1636
|
+
skills,
|
|
1637
|
+
cleanupNow: retainedCleanup.cleanupNow,
|
|
1638
|
+
retainCleanup: retainedCleanup.retainCleanup
|
|
1639
|
+
};
|
|
1640
|
+
} catch (error) {
|
|
1641
|
+
retainedCleanup.cleanupNow();
|
|
1642
|
+
throw error;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
function resolveSafeRealPath(inputPath) {
|
|
1646
|
+
try {
|
|
1647
|
+
return fs6.realpathSync(inputPath);
|
|
1648
|
+
} catch {
|
|
1649
|
+
return path7.resolve(inputPath);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
function isLocalSymlinkSource(localPath) {
|
|
1653
|
+
try {
|
|
1654
|
+
if (!fs6.existsSync(localPath)) {
|
|
1655
|
+
return false;
|
|
1656
|
+
}
|
|
1657
|
+
return fs6.lstatSync(localPath).isSymbolicLink();
|
|
1658
|
+
} catch {
|
|
1659
|
+
return false;
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
async function buildRefreshedSourceMetadata(entry, resolved, cachedSource, sourceHash) {
|
|
1663
|
+
if (entry.source.type === "local") {
|
|
1664
|
+
const canonical2 = entry.source.canonical || entry.source.input;
|
|
1665
|
+
return {
|
|
1666
|
+
canonical: canonical2,
|
|
1667
|
+
resolvedPath: resolveSafeRealPath(resolved.skill.path),
|
|
1668
|
+
isSymlinkSource: isLocalSymlinkSource(canonical2),
|
|
1669
|
+
sourceSkillPath: resolved.sourceSkillPath
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
if (entry.source.type === "git" || entry.source.type === "github") {
|
|
1673
|
+
const nextPinnedRef = cachedSource.kind === "prepared" ? await resolveGitHeadRefAsync(cachedSource.basePath) : entry.source.pinnedRef;
|
|
1674
|
+
return {
|
|
1675
|
+
canonical: entry.source.canonical,
|
|
1676
|
+
pinnedRef: nextPinnedRef,
|
|
1677
|
+
sourceSkillPath: resolved.sourceSkillPath
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
const canonical = resolved.wellKnownSourceUrl || entry.source.canonical;
|
|
1681
|
+
return {
|
|
1682
|
+
canonical,
|
|
1683
|
+
pinnedRef: sourceHash,
|
|
1684
|
+
wellKnownSourceUrl: resolved.wellKnownSourceUrl
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
function resolveCandidateFromCachedSource(entry, cachedSource, keepResolved) {
|
|
1688
|
+
if (cachedSource.kind === "remote") {
|
|
1689
|
+
const matched2 = cachedSource.remoteSkills.find(
|
|
1690
|
+
(item) => item.installName === entry.source.selector.skillName
|
|
1691
|
+
);
|
|
1692
|
+
if (!matched2) {
|
|
1693
|
+
throw new Error(`Skill '${entry.source.selector.skillName}' not found in well-known source`);
|
|
1694
|
+
}
|
|
1695
|
+
const staged = stageRemoteSkillFilesToTempDir(matched2.files);
|
|
1696
|
+
return {
|
|
1697
|
+
skill: {
|
|
1698
|
+
name: matched2.installName,
|
|
1699
|
+
description: matched2.description,
|
|
1700
|
+
path: staged.path
|
|
1701
|
+
},
|
|
1702
|
+
wellKnownSourceUrl: matched2.sourceUrl,
|
|
1703
|
+
cleanup: staged.cleanup
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
const matched = entry.source.selector.relativePath ? cachedSource.skills.find(
|
|
1707
|
+
(item) => path7.resolve(item.path) === path7.resolve(path7.join(cachedSource.basePath, entry.source.selector.relativePath))
|
|
1708
|
+
) : cachedSource.skills.find((item) => item.name === entry.source.selector.skillName);
|
|
1709
|
+
if (!matched) {
|
|
1710
|
+
throw new Error(`Skill '${entry.source.selector.skillName}' not found in source`);
|
|
1711
|
+
}
|
|
1712
|
+
return {
|
|
1713
|
+
skill: matched,
|
|
1714
|
+
sourceSkillPath: path7.relative(cachedSource.basePath, matched.path) || ".",
|
|
1715
|
+
cleanup: keepResolved ? cachedSource.retainCleanup() : void 0
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
async function assessLockEntries(options, cwd, behavior = { keepResolved: false }) {
|
|
1719
|
+
const lock = readLockfile(Boolean(options.global), cwd);
|
|
1720
|
+
const entries = lock.entries.filter((entry) => includeBySkill(entry, options.skill));
|
|
1721
|
+
const drift = [];
|
|
1722
|
+
const assessments = [];
|
|
1723
|
+
const sourceOptions = {
|
|
1724
|
+
global: options.global,
|
|
1725
|
+
allowHost: options.allowHost,
|
|
1726
|
+
denyHost: options.denyHost,
|
|
1727
|
+
maxDownloadBytes: options.maxDownloadBytes,
|
|
1728
|
+
policyMode: options.policyMode,
|
|
1729
|
+
experimental: options.experimental
|
|
1730
|
+
};
|
|
1731
|
+
const sourceCache = /* @__PURE__ */ new Map();
|
|
1732
|
+
const getCachedSource = (entry) => {
|
|
1733
|
+
const key = buildSourceLoadCacheKey(entry.source);
|
|
1734
|
+
const existing = sourceCache.get(key);
|
|
1735
|
+
if (existing) {
|
|
1736
|
+
return existing;
|
|
1737
|
+
}
|
|
1738
|
+
const created = loadCachedSource(entry, sourceOptions);
|
|
1739
|
+
sourceCache.set(key, created);
|
|
1740
|
+
return created;
|
|
1741
|
+
};
|
|
1742
|
+
try {
|
|
1743
|
+
for (const entry of entries) {
|
|
1744
|
+
const assessment = {
|
|
1745
|
+
entry,
|
|
1746
|
+
drift: []
|
|
1747
|
+
};
|
|
1748
|
+
if (!fs6.existsSync(entry.canonicalDir) || !fs6.statSync(entry.canonicalDir).isDirectory()) {
|
|
1749
|
+
const row = {
|
|
1750
|
+
skillName: entry.skillName,
|
|
1751
|
+
kind: "local-modified",
|
|
1752
|
+
detail: "canonical directory is missing"
|
|
1753
|
+
};
|
|
1754
|
+
drift.push(row);
|
|
1755
|
+
assessment.drift.push(row);
|
|
1756
|
+
assessments.push(assessment);
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
const installedHash = unwrapAsyncResult(
|
|
1760
|
+
await toAsyncResult(hashDirectoryAsync(entry.canonicalDir))
|
|
1761
|
+
);
|
|
1762
|
+
if (installedHash !== entry.installedHash) {
|
|
1763
|
+
const row = {
|
|
1764
|
+
skillName: entry.skillName,
|
|
1765
|
+
kind: "local-modified",
|
|
1766
|
+
detail: "installed content differs from lockfile hash"
|
|
1767
|
+
};
|
|
1768
|
+
drift.push(row);
|
|
1769
|
+
assessment.drift.push(row);
|
|
1770
|
+
}
|
|
1771
|
+
const metadataIssue = resolveSourceMetadataIssue(entry);
|
|
1772
|
+
if (metadataIssue) {
|
|
1773
|
+
const row = {
|
|
1774
|
+
skillName: entry.skillName,
|
|
1775
|
+
kind: "migrate-required",
|
|
1776
|
+
detail: metadataIssue
|
|
1777
|
+
};
|
|
1778
|
+
drift.push(row);
|
|
1779
|
+
assessment.drift.push(row);
|
|
1780
|
+
assessments.push(assessment);
|
|
1781
|
+
continue;
|
|
1782
|
+
}
|
|
1783
|
+
let resolved;
|
|
1784
|
+
try {
|
|
1785
|
+
const cachedSource = unwrapAsyncResult(await toAsyncResult(getCachedSource(entry)));
|
|
1786
|
+
resolved = resolveCandidateFromCachedSource(entry, cachedSource, behavior.keepResolved);
|
|
1787
|
+
if (entry.source.type === "local") {
|
|
1788
|
+
const currentResolvedPath = fs6.realpathSync(resolved.skill.path);
|
|
1789
|
+
if (currentResolvedPath !== path7.resolve(entry.source.resolvedPath)) {
|
|
1790
|
+
const row = {
|
|
1791
|
+
skillName: entry.skillName,
|
|
1792
|
+
kind: "migrate-required",
|
|
1793
|
+
detail: `local source identity changed; run ${migrateHint(entry.skillName)}`
|
|
1794
|
+
};
|
|
1795
|
+
drift.push(row);
|
|
1796
|
+
assessment.drift.push(row);
|
|
1797
|
+
if (resolved.cleanup && !behavior.keepResolved) {
|
|
1798
|
+
resolved.cleanup();
|
|
1799
|
+
resolved = void 0;
|
|
1800
|
+
}
|
|
1801
|
+
assessments.push(assessment);
|
|
1802
|
+
continue;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
const sourceHash = await hashDirectoryAsync(resolved.skill.path);
|
|
1806
|
+
assessment.sourceHash = sourceHash;
|
|
1807
|
+
assessment.refreshedSource = await buildRefreshedSourceMetadata(
|
|
1808
|
+
entry,
|
|
1809
|
+
resolved,
|
|
1810
|
+
cachedSource,
|
|
1811
|
+
sourceHash
|
|
1812
|
+
);
|
|
1813
|
+
if (sourceHash !== entry.sourceHash) {
|
|
1814
|
+
const row = {
|
|
1815
|
+
skillName: entry.skillName,
|
|
1816
|
+
kind: "changed-source",
|
|
1817
|
+
detail: "source hash changed"
|
|
1818
|
+
};
|
|
1819
|
+
drift.push(row);
|
|
1820
|
+
assessment.drift.push(row);
|
|
1821
|
+
}
|
|
1822
|
+
if (behavior.keepResolved) {
|
|
1823
|
+
assessment.resolved = resolved;
|
|
1824
|
+
}
|
|
1825
|
+
} catch (error) {
|
|
1826
|
+
const asText = error instanceof Error ? error.message : String(error);
|
|
1827
|
+
const migrateRequired = entry.source.type === "local" && (asText.includes("Local source not found") || asText.includes("not found in source"));
|
|
1828
|
+
const row = {
|
|
1829
|
+
skillName: entry.skillName,
|
|
1830
|
+
kind: migrateRequired ? "migrate-required" : "missing-source",
|
|
1831
|
+
detail: migrateRequired ? `local source identity changed; run ${migrateHint(entry.skillName)}` : asText
|
|
1832
|
+
};
|
|
1833
|
+
drift.push(row);
|
|
1834
|
+
assessment.drift.push(row);
|
|
1835
|
+
} finally {
|
|
1836
|
+
if (resolved?.cleanup && !behavior.keepResolved) {
|
|
1837
|
+
resolved.cleanup();
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
assessments.push(assessment);
|
|
1841
|
+
}
|
|
1842
|
+
const canonical = listCanonicalSkillDirs(Boolean(options.global), cwd);
|
|
1843
|
+
const lockNames = new Set(lock.entries.map((item) => item.skillName));
|
|
1844
|
+
for (const skillName of canonical) {
|
|
1845
|
+
if (!lockNames.has(skillName) && includeBySkill({ skillName }, options.skill)) {
|
|
1846
|
+
drift.push({
|
|
1847
|
+
skillName,
|
|
1848
|
+
kind: "lock-missing",
|
|
1849
|
+
detail: "installed skill is not tracked in lockfile"
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
return { drift, checked: entries.length, assessments };
|
|
1854
|
+
} finally {
|
|
1855
|
+
if (!behavior.keepResolved) {
|
|
1856
|
+
for (const cachedSourcePromise of sourceCache.values()) {
|
|
1857
|
+
const cachedSource = await cachedSourcePromise.catch(() => null);
|
|
1858
|
+
if (cachedSource?.kind === "prepared") {
|
|
1859
|
+
cachedSource.cleanupNow();
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// ../../packages/core/src/runtime/validate-analysis.ts
|
|
1867
|
+
import fs9 from "node:fs";
|
|
1868
|
+
import path10 from "node:path";
|
|
1869
|
+
import os5 from "node:os";
|
|
1870
|
+
import matter2 from "gray-matter";
|
|
1871
|
+
|
|
1872
|
+
// ../../packages/core/src/runtime/skill-installer.ts
|
|
1873
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1874
|
+
import fs8 from "node:fs";
|
|
1875
|
+
import os4 from "node:os";
|
|
1876
|
+
import path9 from "node:path";
|
|
1877
|
+
import YAML2 from "yaml";
|
|
1878
|
+
import { z } from "zod";
|
|
1879
|
+
|
|
1880
|
+
// ../../packages/core/src/runtime/installer-security.ts
|
|
1881
|
+
import fs7 from "node:fs";
|
|
1882
|
+
import path8 from "node:path";
|
|
1883
|
+
function isInsideRoot(rootDir, candidatePath) {
|
|
1884
|
+
const relative = path8.relative(rootDir, candidatePath);
|
|
1885
|
+
return Boolean(relative) && !relative.startsWith("..") && !path8.isAbsolute(relative);
|
|
1886
|
+
}
|
|
1887
|
+
function toEscapeViolation(source) {
|
|
1888
|
+
return {
|
|
1889
|
+
rule: "installer-local-dependency-path-escape",
|
|
1890
|
+
message: `local dependency escapes source root: ${source}`,
|
|
1891
|
+
severity: "error",
|
|
1892
|
+
blocking: true
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
function evaluateInstallerLocalDependency(input, _options = {}) {
|
|
1896
|
+
if (path8.isAbsolute(input.source)) {
|
|
1897
|
+
return {
|
|
1898
|
+
ok: false,
|
|
1899
|
+
violation: {
|
|
1900
|
+
rule: "installer-local-dependency-absolute-path",
|
|
1901
|
+
message: `absolute local dependency paths are not allowed: ${input.source}`,
|
|
1902
|
+
severity: "error",
|
|
1903
|
+
blocking: true
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
const resolvedRoot = path8.resolve(input.sourceRoot);
|
|
1908
|
+
const resolvedSourcePath = path8.resolve(resolvedRoot, input.source);
|
|
1909
|
+
if (!isInsideRoot(resolvedRoot, resolvedSourcePath)) {
|
|
1910
|
+
return {
|
|
1911
|
+
ok: false,
|
|
1912
|
+
violation: toEscapeViolation(input.source)
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
if (fs7.existsSync(resolvedSourcePath)) {
|
|
1916
|
+
const realRoot = fs7.realpathSync.native ? fs7.realpathSync.native(resolvedRoot) : fs7.realpathSync(resolvedRoot);
|
|
1917
|
+
const realSource = fs7.realpathSync.native ? fs7.realpathSync.native(resolvedSourcePath) : fs7.realpathSync(resolvedSourcePath);
|
|
1918
|
+
if (!isInsideRoot(realRoot, realSource)) {
|
|
1919
|
+
return {
|
|
1920
|
+
ok: false,
|
|
1921
|
+
violation: toEscapeViolation(input.source)
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
return {
|
|
1926
|
+
ok: true,
|
|
1927
|
+
resolvedPath: resolvedSourcePath
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
// ../../packages/core/src/runtime/policy.ts
|
|
1932
|
+
function applyMode(result, mode) {
|
|
1933
|
+
if (mode === "enforce" || result.ok) {
|
|
1934
|
+
return result;
|
|
1935
|
+
}
|
|
1936
|
+
const violation = "violation" in result ? result.violation : void 0;
|
|
1937
|
+
if (!violation) {
|
|
1938
|
+
return result;
|
|
1939
|
+
}
|
|
1940
|
+
return {
|
|
1941
|
+
ok: false,
|
|
1942
|
+
violation: {
|
|
1943
|
+
...violation,
|
|
1944
|
+
severity: "warning",
|
|
1945
|
+
blocking: false
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
function evaluateInstallerLocalDependencyPolicy(input, mode) {
|
|
1950
|
+
return applyMode(evaluateInstallerLocalDependency(input, { policyMode: "fixed" }), mode);
|
|
1951
|
+
}
|
|
1952
|
+
function evaluateHookTrustPolicy(input) {
|
|
1953
|
+
if (input.sourceType !== "well-known") {
|
|
1954
|
+
return { allowed: true };
|
|
1955
|
+
}
|
|
1956
|
+
if (input.trustWellKnown) {
|
|
1957
|
+
return { allowed: true };
|
|
1958
|
+
}
|
|
1959
|
+
if (input.mode === "warn") {
|
|
1960
|
+
return {
|
|
1961
|
+
allowed: true,
|
|
1962
|
+
violation: {
|
|
1963
|
+
rule: "hook-trust-required",
|
|
1964
|
+
message: "Well-known hook commands are untrusted. Proceeding due to warning policy mode.",
|
|
1965
|
+
severity: "warning",
|
|
1966
|
+
blocking: false
|
|
1967
|
+
}
|
|
1968
|
+
};
|
|
1969
|
+
}
|
|
1970
|
+
return {
|
|
1971
|
+
allowed: false,
|
|
1972
|
+
violation: {
|
|
1973
|
+
rule: "hook-trust-required",
|
|
1974
|
+
message: "Blocked skill-installer hook commands for well-known source. Trust this source explicitly or use warning policy mode.",
|
|
1975
|
+
severity: "error",
|
|
1976
|
+
blocking: true
|
|
1977
|
+
}
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
// ../../packages/core/src/runtime/skill-installer.ts
|
|
1982
|
+
var InstallerSecurityError = class extends Error {
|
|
1983
|
+
violation;
|
|
1984
|
+
constructor(violation) {
|
|
1985
|
+
super(violation.message);
|
|
1986
|
+
this.name = "InstallerSecurityError";
|
|
1987
|
+
this.violation = violation;
|
|
1988
|
+
}
|
|
1989
|
+
};
|
|
1990
|
+
function isInstallerSecurityError(error) {
|
|
1991
|
+
return error instanceof InstallerSecurityError;
|
|
1992
|
+
}
|
|
1993
|
+
var InstallerPolicyError = class extends Error {
|
|
1994
|
+
violation;
|
|
1995
|
+
constructor(violation) {
|
|
1996
|
+
super(violation.message);
|
|
1997
|
+
this.name = "InstallerPolicyError";
|
|
1998
|
+
this.violation = violation;
|
|
1999
|
+
}
|
|
2000
|
+
};
|
|
2001
|
+
function isInstallerPolicyError(error) {
|
|
2002
|
+
return error instanceof InstallerPolicyError;
|
|
2003
|
+
}
|
|
2004
|
+
var dependencyObjectSchema = z.object({
|
|
2005
|
+
source: z.string().min(1),
|
|
2006
|
+
path: z.string().min(1)
|
|
2007
|
+
}).strict();
|
|
2008
|
+
var installerConfigSchema = z.object({
|
|
2009
|
+
schemaVersion: z.literal(1),
|
|
2010
|
+
"pre-install": z.array(z.string().min(1)).optional().default([]),
|
|
2011
|
+
dependencies: z.array(z.union([z.string().min(1), dependencyObjectSchema])).optional().default([]),
|
|
2012
|
+
"post-install": z.array(z.string().min(1)).optional().default([])
|
|
2013
|
+
}).strict();
|
|
2014
|
+
function ensureInsideRoot(rootDir, relativeTarget) {
|
|
2015
|
+
const resolved = path9.resolve(rootDir, relativeTarget);
|
|
2016
|
+
const relative = path9.relative(rootDir, resolved);
|
|
2017
|
+
if (!relative || relative.startsWith("..") || path9.isAbsolute(relative)) {
|
|
2018
|
+
throw new Error(`Unsafe destination path: ${relativeTarget}`);
|
|
2019
|
+
}
|
|
2020
|
+
return resolved;
|
|
2021
|
+
}
|
|
2022
|
+
function sourceLooksLikeUrl(source) {
|
|
2023
|
+
try {
|
|
2024
|
+
const parsed = new URL(source);
|
|
2025
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
2026
|
+
} catch {
|
|
2027
|
+
return false;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
function sourceLooksLikeRepoShorthand(source) {
|
|
2031
|
+
const trimmed = source.trim().replace(/^https?:\/\//, "");
|
|
2032
|
+
return /^(github\.com|gitlab\.com)\/[^/]+\/[^/]+(?:\.git)?\/?$/.test(trimmed);
|
|
2033
|
+
}
|
|
2034
|
+
function parseRepoSource(source) {
|
|
2035
|
+
const withoutProtocol = source.trim().replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
2036
|
+
const match = withoutProtocol.match(/^(github\.com|gitlab\.com)\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
2037
|
+
if (!match) {
|
|
2038
|
+
throw new Error(`Unsupported repository dependency source: ${source}`);
|
|
2039
|
+
}
|
|
2040
|
+
const host = match[1];
|
|
2041
|
+
const owner = match[2];
|
|
2042
|
+
const repo = match[3];
|
|
2043
|
+
return {
|
|
2044
|
+
repoUrl: `https://${host}/${owner}/${repo}.git`,
|
|
2045
|
+
repoName: repo
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
function deriveDestinationNameFromSource(source) {
|
|
2049
|
+
if (sourceLooksLikeUrl(source)) {
|
|
2050
|
+
const parsed = new URL(source);
|
|
2051
|
+
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
2052
|
+
const leaf2 = parts[parts.length - 1];
|
|
2053
|
+
if (!leaf2) {
|
|
2054
|
+
throw new Error(`Cannot derive dependency name from URL source: ${source}`);
|
|
2055
|
+
}
|
|
2056
|
+
return leaf2;
|
|
2057
|
+
}
|
|
2058
|
+
if (sourceLooksLikeRepoShorthand(source)) {
|
|
2059
|
+
return parseRepoSource(source).repoName;
|
|
2060
|
+
}
|
|
2061
|
+
const leaf = path9.basename(source);
|
|
2062
|
+
if (!leaf || leaf === "." || leaf === path9.sep) {
|
|
2063
|
+
throw new Error(`Cannot derive dependency name from source: ${source}`);
|
|
2064
|
+
}
|
|
2065
|
+
return leaf;
|
|
2066
|
+
}
|
|
2067
|
+
function classifyDependencySource(source) {
|
|
2068
|
+
if (sourceLooksLikeUrl(source)) {
|
|
2069
|
+
return "url";
|
|
2070
|
+
}
|
|
2071
|
+
if (sourceLooksLikeRepoShorthand(source)) {
|
|
2072
|
+
return "repo";
|
|
2073
|
+
}
|
|
2074
|
+
return "local";
|
|
2075
|
+
}
|
|
2076
|
+
function parseConfigObject(raw) {
|
|
2077
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
2078
|
+
throw new Error("Invalid skill-installer config: expected an object with top-level keys");
|
|
2079
|
+
}
|
|
2080
|
+
const parsed = raw;
|
|
2081
|
+
if (typeof parsed["skill-installer"] !== "undefined") {
|
|
2082
|
+
throw new Error(
|
|
2083
|
+
"Invalid skill-installer config: do not nest under 'skill-installer:'. Use top-level 'pre-install', 'dependencies', and 'post-install'."
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
if (Array.isArray(parsed.dependencies)) {
|
|
2087
|
+
const hasLegacyDependency = parsed.dependencies.some((entry) => {
|
|
2088
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
2089
|
+
return false;
|
|
2090
|
+
}
|
|
2091
|
+
const row = entry;
|
|
2092
|
+
return typeof row.type !== "undefined" || typeof row.url !== "undefined" || typeof row.name !== "undefined";
|
|
2093
|
+
});
|
|
2094
|
+
if (hasLegacyDependency) {
|
|
2095
|
+
throw new Error(
|
|
2096
|
+
"Invalid skill-installer config: legacy dependency format detected. Use schemaVersion: 1 and the lean dependencies format from docs/proposed-skill-format.md."
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
const validated = installerConfigSchema.safeParse(parsed);
|
|
2101
|
+
if (!validated.success) {
|
|
2102
|
+
const reason = validated.error.issues.map((issue) => issue.message).join("; ");
|
|
2103
|
+
throw new Error(
|
|
2104
|
+
`Invalid skill-installer config: ${reason}. Use docs/proposed-skill-format.md as the source-of-truth for schemaVersion, dependencies, pre-install, and post-install.`
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
const normalized = validated.data;
|
|
2108
|
+
return {
|
|
2109
|
+
schemaVersion: 1,
|
|
2110
|
+
preInstall: normalized["pre-install"],
|
|
2111
|
+
dependencies: normalized.dependencies.map(
|
|
2112
|
+
(dep) => typeof dep === "string" ? dep : { source: dep.source, path: dep.path }
|
|
2113
|
+
),
|
|
2114
|
+
postInstall: normalized["post-install"]
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
function assertNoLegacyInstallerBlock(skillDir) {
|
|
2118
|
+
const openAiYamlPath = path9.join(skillDir, "agents", "openai.yaml");
|
|
2119
|
+
if (!fs8.existsSync(openAiYamlPath) || !fs8.statSync(openAiYamlPath).isFile()) {
|
|
2120
|
+
return;
|
|
2121
|
+
}
|
|
2122
|
+
const parsed = YAML2.parse(fs8.readFileSync(openAiYamlPath, "utf8"));
|
|
2123
|
+
if (parsed && typeof parsed === "object" && typeof parsed["skill-installer"] !== "undefined") {
|
|
2124
|
+
throw new Error(
|
|
2125
|
+
"Legacy skill-installer config detected in agents/openai.yaml. Move it to skill-installer.yaml or skill-installer.json."
|
|
2126
|
+
);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
function loadInstallerConfig(skillDir) {
|
|
2130
|
+
assertNoLegacyInstallerBlock(skillDir);
|
|
2131
|
+
const yamlPath = path9.join(skillDir, "skill-installer.yaml");
|
|
2132
|
+
const jsonPath = path9.join(skillDir, "skill-installer.json");
|
|
2133
|
+
const hasYaml = fs8.existsSync(yamlPath) && fs8.statSync(yamlPath).isFile();
|
|
2134
|
+
const hasJson = fs8.existsSync(jsonPath) && fs8.statSync(jsonPath).isFile();
|
|
2135
|
+
if (hasYaml && hasJson) {
|
|
2136
|
+
throw new Error(
|
|
2137
|
+
"Both skill-installer.yaml and skill-installer.json exist. Keep only one installer config file."
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
if (!hasYaml && !hasJson) {
|
|
2141
|
+
return {
|
|
2142
|
+
schemaVersion: 1,
|
|
2143
|
+
preInstall: [],
|
|
2144
|
+
dependencies: [],
|
|
2145
|
+
postInstall: []
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
if (hasYaml) {
|
|
2149
|
+
const parsed = YAML2.parse(fs8.readFileSync(yamlPath, "utf8"));
|
|
2150
|
+
return parseConfigObject(parsed);
|
|
2151
|
+
}
|
|
2152
|
+
const raw = JSON.parse(fs8.readFileSync(jsonPath, "utf8"));
|
|
2153
|
+
return parseConfigObject(raw);
|
|
2154
|
+
}
|
|
2155
|
+
function runHookPhase(phase, commands, cwd, events) {
|
|
2156
|
+
for (const command of commands) {
|
|
2157
|
+
events.push({
|
|
2158
|
+
level: "info",
|
|
2159
|
+
message: `[skills] ${phase}: ${command}`
|
|
2160
|
+
});
|
|
2161
|
+
const result = spawnSync2("sh", ["-c", command], {
|
|
2162
|
+
cwd,
|
|
2163
|
+
encoding: "utf8",
|
|
2164
|
+
stdio: "pipe"
|
|
2165
|
+
});
|
|
2166
|
+
if (result.status !== 0) {
|
|
2167
|
+
const message = (result.stderr || result.stdout || "command failed").trim().split(/\r?\n/)[0] || "command failed";
|
|
2168
|
+
throw new Error(`${phase} command failed: ${command} (${message})`);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
function resolveDependencySourceAndTarget(dep) {
|
|
2173
|
+
if (typeof dep === "string") {
|
|
2174
|
+
return {
|
|
2175
|
+
source: dep,
|
|
2176
|
+
targetPath: deriveDestinationNameFromSource(dep)
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
return {
|
|
2180
|
+
source: dep.source,
|
|
2181
|
+
targetPath: dep.path
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
2184
|
+
function runGitClone(repoUrl, targetDir) {
|
|
2185
|
+
const result = spawnSync2("git", ["clone", "--depth", "1", repoUrl, targetDir], {
|
|
2186
|
+
encoding: "utf8",
|
|
2187
|
+
stdio: "pipe"
|
|
2188
|
+
});
|
|
2189
|
+
if (result.status !== 0) {
|
|
2190
|
+
const message = (result.stderr || result.stdout || "git clone failed").trim().split(/\r?\n/)[0] || "git clone failed";
|
|
2191
|
+
throw new Error(`Repository dependency clone failed: ${repoUrl} (${message})`);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
async function prepareDependency(source, targetPath, index, sourceCwd, stagingDir, policyMode, events) {
|
|
2195
|
+
const sourceKind = classifyDependencySource(source);
|
|
2196
|
+
if (sourceKind === "local") {
|
|
2197
|
+
const evaluation = evaluateInstallerLocalDependencyPolicy(
|
|
2198
|
+
{
|
|
2199
|
+
source,
|
|
2200
|
+
sourceRoot: sourceCwd
|
|
2201
|
+
},
|
|
2202
|
+
policyMode
|
|
2203
|
+
);
|
|
2204
|
+
if (!evaluation.ok) {
|
|
2205
|
+
const violation = "violation" in evaluation ? evaluation.violation : void 0;
|
|
2206
|
+
if (!violation) {
|
|
2207
|
+
throw new Error(`Dependency[${index}] policy evaluation failed for source: ${source}`);
|
|
2208
|
+
}
|
|
2209
|
+
if (violation.blocking) {
|
|
2210
|
+
throw new InstallerSecurityError(violation);
|
|
2211
|
+
}
|
|
2212
|
+
events.push({
|
|
2213
|
+
level: "warning",
|
|
2214
|
+
message: `[skills] ${violation.message}`
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
const sourcePath = evaluation.ok ? evaluation.resolvedPath : path9.resolve(sourceCwd, source);
|
|
2218
|
+
if (!fs8.existsSync(sourcePath)) {
|
|
2219
|
+
throw new Error(`Dependency[${index}] (local) source not found: ${source}`);
|
|
2220
|
+
}
|
|
2221
|
+
fs8.accessSync(sourcePath, fs8.constants.R_OK);
|
|
2222
|
+
events.push({
|
|
2223
|
+
level: "info",
|
|
2224
|
+
message: `[skills] dependency[${index}] preflight-validated: ${source}`
|
|
2225
|
+
});
|
|
2226
|
+
return {
|
|
2227
|
+
kind: "local",
|
|
2228
|
+
index,
|
|
2229
|
+
targetPath,
|
|
2230
|
+
sourcePath,
|
|
2231
|
+
sourceLabel: source
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
if (sourceKind === "repo") {
|
|
2235
|
+
const stagePath = ensureInsideRoot(
|
|
2236
|
+
stagingDir,
|
|
2237
|
+
`repo-${index}-${path9.basename(targetPath).replace(/[^a-zA-Z0-9._-]/g, "_")}`
|
|
2238
|
+
);
|
|
2239
|
+
const { repoUrl } = parseRepoSource(source);
|
|
2240
|
+
runGitClone(repoUrl, stagePath);
|
|
2241
|
+
events.push({
|
|
2242
|
+
level: "info",
|
|
2243
|
+
message: `[skills] dependency[${index}] staged: ${source}`
|
|
2244
|
+
});
|
|
2245
|
+
return {
|
|
2246
|
+
kind: "repo-staged",
|
|
2247
|
+
index,
|
|
2248
|
+
targetPath,
|
|
2249
|
+
repoStagePath: stagePath,
|
|
2250
|
+
sourceLabel: source
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
const target = new URL(source);
|
|
2254
|
+
if (target.protocol !== "http:" && target.protocol !== "https:") {
|
|
2255
|
+
throw new Error(`Dependency[${index}] (remote) unsupported protocol: ${target.protocol}`);
|
|
2256
|
+
}
|
|
2257
|
+
const response = await fetch(target.toString());
|
|
2258
|
+
if (!response.ok) {
|
|
2259
|
+
throw new Error(
|
|
2260
|
+
`Dependency[${index}] (remote) download failed: ${response.status} ${response.statusText}`
|
|
2261
|
+
);
|
|
2262
|
+
}
|
|
2263
|
+
const stagedFilePath = ensureInsideRoot(
|
|
2264
|
+
stagingDir,
|
|
2265
|
+
`remote-${index}-${path9.basename(targetPath)}`
|
|
2266
|
+
);
|
|
2267
|
+
fs8.mkdirSync(path9.dirname(stagedFilePath), { recursive: true });
|
|
2268
|
+
fs8.writeFileSync(stagedFilePath, Buffer.from(await response.arrayBuffer()));
|
|
2269
|
+
events.push({
|
|
2270
|
+
level: "info",
|
|
2271
|
+
message: `[skills] dependency[${index}] staged: ${source}`
|
|
2272
|
+
});
|
|
2273
|
+
return {
|
|
2274
|
+
kind: "remote-bytes",
|
|
2275
|
+
index,
|
|
2276
|
+
targetPath,
|
|
2277
|
+
remoteBufferPath: stagedFilePath,
|
|
2278
|
+
sourceLabel: source
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
async function prepareInstallerArtifacts(skillDir, sourceCwd, options = {}) {
|
|
2282
|
+
const config = loadInstallerConfig(skillDir);
|
|
2283
|
+
const sourceType = options.sourceType ?? "local";
|
|
2284
|
+
const allowHookCommands = options.allowHookCommands;
|
|
2285
|
+
const policyMode = options.policyMode ?? "enforce";
|
|
2286
|
+
const events = [];
|
|
2287
|
+
if (config.preInstall.length > 0 || config.postInstall.length > 0) {
|
|
2288
|
+
if (allowHookCommands === false) {
|
|
2289
|
+
throw new Error(
|
|
2290
|
+
"Blocked skill-installer hook commands by caller policy for pre/post install commands."
|
|
2291
|
+
);
|
|
2292
|
+
}
|
|
2293
|
+
const trustDecision = evaluateHookTrustPolicy({
|
|
2294
|
+
sourceType,
|
|
2295
|
+
trustWellKnown: Boolean(options.trustWellKnown),
|
|
2296
|
+
mode: policyMode
|
|
2297
|
+
});
|
|
2298
|
+
if (!trustDecision.allowed && trustDecision.violation) {
|
|
2299
|
+
throw new InstallerPolicyError(trustDecision.violation);
|
|
2300
|
+
}
|
|
2301
|
+
if (trustDecision.allowed && trustDecision.violation?.severity === "warning") {
|
|
2302
|
+
events.push({
|
|
2303
|
+
level: "warning",
|
|
2304
|
+
message: `[skills] ${trustDecision.violation.message}`
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
if (config.preInstall.length === 0 && config.dependencies.length === 0 && config.postInstall.length === 0) {
|
|
2309
|
+
return {
|
|
2310
|
+
preInstall: [],
|
|
2311
|
+
postInstall: [],
|
|
2312
|
+
preparedDependencies: [],
|
|
2313
|
+
stagingDir: "",
|
|
2314
|
+
events
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
const stagingDir = fs8.mkdtempSync(path9.join(os4.tmpdir(), "skillspp-installer-stage-"));
|
|
2318
|
+
const preparedDependencies = [];
|
|
2319
|
+
const seenTargetPaths = /* @__PURE__ */ new Set();
|
|
2320
|
+
try {
|
|
2321
|
+
for (let i = 0; i < config.dependencies.length; i += 1) {
|
|
2322
|
+
const dep = config.dependencies[i];
|
|
2323
|
+
const { source, targetPath } = resolveDependencySourceAndTarget(dep);
|
|
2324
|
+
const normalizedTarget = targetPath.replace(/\\/g, "/");
|
|
2325
|
+
if (seenTargetPaths.has(normalizedTarget)) {
|
|
2326
|
+
throw new Error(`Dependency[${i}] duplicate target path: ${targetPath}`);
|
|
2327
|
+
}
|
|
2328
|
+
seenTargetPaths.add(normalizedTarget);
|
|
2329
|
+
const destinationInSkill = ensureInsideRoot(skillDir, targetPath);
|
|
2330
|
+
if (fs8.existsSync(destinationInSkill)) {
|
|
2331
|
+
throw new Error(
|
|
2332
|
+
`Dependency[${i}] destination already exists: ${path9.relative(
|
|
2333
|
+
skillDir,
|
|
2334
|
+
destinationInSkill
|
|
2335
|
+
)}`
|
|
2336
|
+
);
|
|
2337
|
+
}
|
|
2338
|
+
const preparedDep = await prepareDependency(
|
|
2339
|
+
source,
|
|
2340
|
+
targetPath,
|
|
2341
|
+
i,
|
|
2342
|
+
sourceCwd,
|
|
2343
|
+
stagingDir,
|
|
2344
|
+
policyMode,
|
|
2345
|
+
events
|
|
2346
|
+
);
|
|
2347
|
+
preparedDependencies.push(preparedDep);
|
|
2348
|
+
}
|
|
2349
|
+
return {
|
|
2350
|
+
preInstall: config.preInstall,
|
|
2351
|
+
postInstall: config.postInstall,
|
|
2352
|
+
preparedDependencies,
|
|
2353
|
+
stagingDir,
|
|
2354
|
+
events
|
|
2355
|
+
};
|
|
2356
|
+
} catch (error) {
|
|
2357
|
+
fs8.rmSync(stagingDir, { recursive: true, force: true });
|
|
2358
|
+
throw error;
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
function cleanupPreparedInstallerArtifacts(prepared) {
|
|
2362
|
+
if (prepared.stagingDir) {
|
|
2363
|
+
fs8.rmSync(prepared.stagingDir, { recursive: true, force: true });
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
async function applyInstallerArtifacts(installedSkillDir, prepared) {
|
|
2367
|
+
if (prepared.preInstall.length === 0 && prepared.postInstall.length === 0 && prepared.preparedDependencies.length === 0) {
|
|
2368
|
+
return;
|
|
2369
|
+
}
|
|
2370
|
+
runHookPhase("pre-install", prepared.preInstall, installedSkillDir, prepared.events);
|
|
2371
|
+
for (const dep of prepared.preparedDependencies) {
|
|
2372
|
+
const destinationPath = ensureInsideRoot(installedSkillDir, dep.targetPath);
|
|
2373
|
+
if (fs8.existsSync(destinationPath)) {
|
|
2374
|
+
throw new Error(
|
|
2375
|
+
`Dependency[${dep.index}] destination already exists: ${path9.relative(
|
|
2376
|
+
installedSkillDir,
|
|
2377
|
+
destinationPath
|
|
2378
|
+
)}`
|
|
2379
|
+
);
|
|
2380
|
+
}
|
|
2381
|
+
fs8.mkdirSync(path9.dirname(destinationPath), { recursive: true });
|
|
2382
|
+
if (dep.kind === "local") {
|
|
2383
|
+
const stat = fs8.statSync(dep.sourcePath);
|
|
2384
|
+
if (stat.isDirectory()) {
|
|
2385
|
+
fs8.cpSync(dep.sourcePath, destinationPath, {
|
|
2386
|
+
recursive: true,
|
|
2387
|
+
force: false,
|
|
2388
|
+
errorOnExist: true
|
|
2389
|
+
});
|
|
2390
|
+
} else {
|
|
2391
|
+
fs8.copyFileSync(dep.sourcePath, destinationPath);
|
|
2392
|
+
}
|
|
2393
|
+
} else if (dep.kind === "remote-bytes") {
|
|
2394
|
+
fs8.copyFileSync(dep.remoteBufferPath, destinationPath);
|
|
2395
|
+
} else {
|
|
2396
|
+
try {
|
|
2397
|
+
fs8.renameSync(dep.repoStagePath, destinationPath);
|
|
2398
|
+
} catch (error) {
|
|
2399
|
+
const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "";
|
|
2400
|
+
if (code !== "EXDEV") {
|
|
2401
|
+
throw error;
|
|
2402
|
+
}
|
|
2403
|
+
fs8.cpSync(dep.repoStagePath, destinationPath, {
|
|
2404
|
+
recursive: true,
|
|
2405
|
+
force: false,
|
|
2406
|
+
errorOnExist: true
|
|
2407
|
+
});
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
prepared.events.push({
|
|
2411
|
+
level: "info",
|
|
2412
|
+
message: `[skills] dependency[${dep.index}] installed: ${dep.targetPath}`
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
runHookPhase("post-install", prepared.postInstall, installedSkillDir, prepared.events);
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
// ../../packages/core/src/contracts/errors/core-error.ts
|
|
2419
|
+
var CoreError = class extends Error {
|
|
2420
|
+
code;
|
|
2421
|
+
details;
|
|
2422
|
+
cause;
|
|
2423
|
+
constructor(input) {
|
|
2424
|
+
super(input.message);
|
|
2425
|
+
this.name = "CoreError";
|
|
2426
|
+
this.code = input.code;
|
|
2427
|
+
this.details = input.details;
|
|
2428
|
+
this.cause = input.cause;
|
|
2429
|
+
}
|
|
2430
|
+
};
|
|
2431
|
+
|
|
2432
|
+
// ../../packages/core/src/runtime/validate-analysis.ts
|
|
2433
|
+
var DEFAULT_MAX_LINES = 500;
|
|
2434
|
+
var DEFAULT_MAX_DESCRIPTION = 1024;
|
|
2435
|
+
function resolveThresholds(options) {
|
|
2436
|
+
return {
|
|
2437
|
+
maxLines: options.maxLines || DEFAULT_MAX_LINES,
|
|
2438
|
+
maxDescriptionChars: options.maxDescriptionChars || DEFAULT_MAX_DESCRIPTION
|
|
2439
|
+
};
|
|
2440
|
+
}
|
|
2441
|
+
function addDiagnostic(list, diagnostic) {
|
|
2442
|
+
list.push(diagnostic);
|
|
2443
|
+
}
|
|
2444
|
+
function discoverMarkdownReferences(content) {
|
|
2445
|
+
const refs = /* @__PURE__ */ new Set();
|
|
2446
|
+
const markdownLinkPattern = /\[[^\]]+\]\(([^)]+)\)/g;
|
|
2447
|
+
let match;
|
|
2448
|
+
while ((match = markdownLinkPattern.exec(content)) !== null) {
|
|
2449
|
+
const value = match[1].trim();
|
|
2450
|
+
if (!value || value.startsWith("http://") || value.startsWith("https://") || value.startsWith("#")) {
|
|
2451
|
+
continue;
|
|
2452
|
+
}
|
|
2453
|
+
refs.add(value);
|
|
2454
|
+
}
|
|
2455
|
+
return [...refs];
|
|
2456
|
+
}
|
|
2457
|
+
var typeRules = [
|
|
2458
|
+
{
|
|
2459
|
+
id: "framework-references-dir",
|
|
2460
|
+
when: (frontmatter) => frontmatter.type === "framework",
|
|
2461
|
+
validate: ({ skillDir, skillName, diagnostics }) => {
|
|
2462
|
+
const referencesDir = path10.join(skillDir, "references");
|
|
2463
|
+
if (!fs9.existsSync(referencesDir) || !fs9.statSync(referencesDir).isDirectory()) {
|
|
2464
|
+
addDiagnostic(diagnostics, {
|
|
2465
|
+
severity: "error",
|
|
2466
|
+
skill: skillName,
|
|
2467
|
+
file: referencesDir,
|
|
2468
|
+
rule: "framework-references-required",
|
|
2469
|
+
message: "framework skills must contain a references directory"
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
];
|
|
2475
|
+
async function validateInstallerDependencies(skillDir, sourceRoot, diagnostics, skillName) {
|
|
2476
|
+
const installerConfigPath = fs9.existsSync(path10.join(skillDir, "skill-installer.yaml")) ? path10.join(skillDir, "skill-installer.yaml") : path10.join(skillDir, "skill-installer.json");
|
|
2477
|
+
try {
|
|
2478
|
+
const installer = loadInstallerConfig(skillDir);
|
|
2479
|
+
const fallbackRoots = Array.from(
|
|
2480
|
+
/* @__PURE__ */ new Set([path10.resolve(sourceRoot), path10.resolve(process.cwd())])
|
|
2481
|
+
);
|
|
2482
|
+
let hasSecurityViolation = false;
|
|
2483
|
+
for (const dep of installer.dependencies) {
|
|
2484
|
+
const source = typeof dep === "string" ? dep : dep.source;
|
|
2485
|
+
const kind = classifyDependencySource(source);
|
|
2486
|
+
if (kind !== "local") {
|
|
2487
|
+
continue;
|
|
2488
|
+
}
|
|
2489
|
+
let accepted = false;
|
|
2490
|
+
let securityViolation;
|
|
2491
|
+
for (const root of fallbackRoots) {
|
|
2492
|
+
const evaluated = evaluateInstallerLocalDependencyPolicy(
|
|
2493
|
+
{
|
|
2494
|
+
source,
|
|
2495
|
+
sourceRoot: root
|
|
2496
|
+
},
|
|
2497
|
+
"enforce"
|
|
2498
|
+
);
|
|
2499
|
+
if (evaluated.ok) {
|
|
2500
|
+
accepted = true;
|
|
2501
|
+
break;
|
|
2502
|
+
} else {
|
|
2503
|
+
securityViolation = "violation" in evaluated ? evaluated.violation : void 0;
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
if (!accepted && securityViolation) {
|
|
2507
|
+
hasSecurityViolation = true;
|
|
2508
|
+
addDiagnostic(diagnostics, {
|
|
2509
|
+
severity: securityViolation.severity,
|
|
2510
|
+
skill: skillName,
|
|
2511
|
+
file: installerConfigPath,
|
|
2512
|
+
rule: securityViolation.rule,
|
|
2513
|
+
message: securityViolation.message
|
|
2514
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
if (hasSecurityViolation) {
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
const tempSkillDir = fs9.mkdtempSync(path10.join(os5.tmpdir(), "skillspp-validate-installer-"));
|
|
2521
|
+
try {
|
|
2522
|
+
const filesToCopy = ["SKILL.md", "skill-installer.yaml", "skill-installer.json"];
|
|
2523
|
+
for (const fileName of filesToCopy) {
|
|
2524
|
+
const src = path10.join(skillDir, fileName);
|
|
2525
|
+
if (fs9.existsSync(src) && fs9.statSync(src).isFile()) {
|
|
2526
|
+
fs9.copyFileSync(src, path10.join(tempSkillDir, fileName));
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
const agentsDir = path10.join(skillDir, "agents");
|
|
2530
|
+
if (fs9.existsSync(agentsDir) && fs9.statSync(agentsDir).isDirectory()) {
|
|
2531
|
+
fs9.cpSync(agentsDir, path10.join(tempSkillDir, "agents"), {
|
|
2532
|
+
recursive: true,
|
|
2533
|
+
force: true
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
let preparedSuccess = false;
|
|
2537
|
+
let lastMissingSourceError;
|
|
2538
|
+
let securityError;
|
|
2539
|
+
for (const root of fallbackRoots) {
|
|
2540
|
+
try {
|
|
2541
|
+
const prepared = await prepareInstallerArtifacts(tempSkillDir, root, {
|
|
2542
|
+
sourceType: "local",
|
|
2543
|
+
allowHookCommands: false,
|
|
2544
|
+
policyMode: "enforce"
|
|
2545
|
+
});
|
|
2546
|
+
cleanupPreparedInstallerArtifacts(prepared);
|
|
2547
|
+
preparedSuccess = true;
|
|
2548
|
+
break;
|
|
2549
|
+
} catch (error) {
|
|
2550
|
+
if (isInstallerSecurityError(error)) {
|
|
2551
|
+
securityError = error;
|
|
2552
|
+
break;
|
|
2553
|
+
}
|
|
2554
|
+
if (isInstallerPolicyError(error)) {
|
|
2555
|
+
securityError = error;
|
|
2556
|
+
break;
|
|
2557
|
+
}
|
|
2558
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2559
|
+
if (message.includes("(local) source not found")) {
|
|
2560
|
+
lastMissingSourceError = error;
|
|
2561
|
+
continue;
|
|
2562
|
+
}
|
|
2563
|
+
throw error;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
if (!preparedSuccess && securityError) {
|
|
2567
|
+
throw securityError;
|
|
2568
|
+
}
|
|
2569
|
+
if (!preparedSuccess && lastMissingSourceError) {
|
|
2570
|
+
throw lastMissingSourceError;
|
|
2571
|
+
}
|
|
2572
|
+
} finally {
|
|
2573
|
+
fs9.rmSync(tempSkillDir, { recursive: true, force: true });
|
|
2574
|
+
}
|
|
2575
|
+
} catch (error) {
|
|
2576
|
+
if (isInstallerSecurityError(error)) {
|
|
2577
|
+
addDiagnostic(diagnostics, {
|
|
2578
|
+
severity: error.violation.severity,
|
|
2579
|
+
skill: skillName,
|
|
2580
|
+
file: installerConfigPath,
|
|
2581
|
+
rule: error.violation.rule,
|
|
2582
|
+
message: error.violation.message
|
|
2583
|
+
});
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
2586
|
+
if (isInstallerPolicyError(error)) {
|
|
2587
|
+
addDiagnostic(diagnostics, {
|
|
2588
|
+
severity: "error",
|
|
2589
|
+
skill: skillName,
|
|
2590
|
+
file: installerConfigPath,
|
|
2591
|
+
rule: error.violation.rule,
|
|
2592
|
+
message: error.violation.message
|
|
2593
|
+
});
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
addDiagnostic(diagnostics, {
|
|
2597
|
+
severity: "warning",
|
|
2598
|
+
skill: skillName,
|
|
2599
|
+
file: installerConfigPath,
|
|
2600
|
+
rule: "missing-installer-local-dependency",
|
|
2601
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
async function validateSkillDir(skillDir, sourceRoot, diagnostics, strict, thresholds) {
|
|
2606
|
+
const skillMd = path10.join(skillDir, "SKILL.md");
|
|
2607
|
+
const skillName = path10.basename(skillDir);
|
|
2608
|
+
if (!fs9.existsSync(skillMd)) {
|
|
2609
|
+
addDiagnostic(diagnostics, {
|
|
2610
|
+
severity: "error",
|
|
2611
|
+
skill: skillName,
|
|
2612
|
+
file: skillMd,
|
|
2613
|
+
rule: "missing-skill-md",
|
|
2614
|
+
message: "SKILL.md is required"
|
|
2615
|
+
});
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
const content = fs9.readFileSync(skillMd, "utf8");
|
|
2619
|
+
const lines = content.split(/\r?\n/);
|
|
2620
|
+
if (lines.length > thresholds.maxLines) {
|
|
2621
|
+
addDiagnostic(diagnostics, {
|
|
2622
|
+
severity: strict ? "error" : "warning",
|
|
2623
|
+
skill: skillName,
|
|
2624
|
+
file: skillMd,
|
|
2625
|
+
rule: "line-budget",
|
|
2626
|
+
message: `SKILL.md has ${lines.length} lines (limit ${thresholds.maxLines})`
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
let parsed;
|
|
2630
|
+
try {
|
|
2631
|
+
parsed = matter2(content);
|
|
2632
|
+
} catch (error) {
|
|
2633
|
+
addDiagnostic(diagnostics, {
|
|
2634
|
+
severity: "error",
|
|
2635
|
+
skill: skillName,
|
|
2636
|
+
file: skillMd,
|
|
2637
|
+
rule: "invalid-frontmatter",
|
|
2638
|
+
message: error instanceof Error ? error.message : "frontmatter parsing failed"
|
|
2639
|
+
});
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
const data = parsed.data || {};
|
|
2643
|
+
const name = data.name;
|
|
2644
|
+
const description = data.description;
|
|
2645
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
2646
|
+
addDiagnostic(diagnostics, {
|
|
2647
|
+
severity: "error",
|
|
2648
|
+
skill: skillName,
|
|
2649
|
+
file: skillMd,
|
|
2650
|
+
rule: "missing-name",
|
|
2651
|
+
message: "frontmatter field 'name' is required"
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2654
|
+
if (typeof description !== "string" || !description.trim()) {
|
|
2655
|
+
addDiagnostic(diagnostics, {
|
|
2656
|
+
severity: "error",
|
|
2657
|
+
skill: skillName,
|
|
2658
|
+
file: skillMd,
|
|
2659
|
+
rule: "missing-description",
|
|
2660
|
+
message: "frontmatter field 'description' is required"
|
|
2661
|
+
});
|
|
2662
|
+
} else if (description.length > thresholds.maxDescriptionChars) {
|
|
2663
|
+
addDiagnostic(diagnostics, {
|
|
2664
|
+
severity: strict ? "error" : "warning",
|
|
2665
|
+
skill: skillName,
|
|
2666
|
+
file: skillMd,
|
|
2667
|
+
rule: "description-budget",
|
|
2668
|
+
message: `description has ${description.length} chars (limit ${thresholds.maxDescriptionChars})`
|
|
2669
|
+
});
|
|
2670
|
+
}
|
|
2671
|
+
if (typeof name === "string" && name.trim()) {
|
|
2672
|
+
const normalized = name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[.-]+|[.-]+$/g, "");
|
|
2673
|
+
if (normalized && normalized !== skillName.toLowerCase()) {
|
|
2674
|
+
addDiagnostic(diagnostics, {
|
|
2675
|
+
severity: "error",
|
|
2676
|
+
skill: skillName,
|
|
2677
|
+
file: skillMd,
|
|
2678
|
+
rule: "name-path-mismatch",
|
|
2679
|
+
message: `frontmatter name '${name}' does not match directory '${skillName}'`
|
|
2680
|
+
});
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
for (const ref of discoverMarkdownReferences(content)) {
|
|
2684
|
+
const resolved = path10.resolve(skillDir, ref);
|
|
2685
|
+
const rel = path10.relative(skillDir, resolved);
|
|
2686
|
+
if (!rel || rel.startsWith("..") || path10.isAbsolute(rel)) {
|
|
2687
|
+
continue;
|
|
2688
|
+
}
|
|
2689
|
+
if (!fs9.existsSync(resolved)) {
|
|
2690
|
+
addDiagnostic(diagnostics, {
|
|
2691
|
+
severity: "error",
|
|
2692
|
+
skill: skillName,
|
|
2693
|
+
file: skillMd,
|
|
2694
|
+
rule: "missing-reference",
|
|
2695
|
+
message: `referenced path not found: ${ref}`
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
for (const rule of typeRules) {
|
|
2700
|
+
if (rule.when(data)) {
|
|
2701
|
+
rule.validate({ skillDir, skillName, diagnostics });
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
await validateInstallerDependencies(skillDir, sourceRoot, diagnostics, skillName);
|
|
2705
|
+
}
|
|
2706
|
+
async function stageAndValidateLocalRoot(rootPath, dependencyRoot, seenSkillPaths, diagnostics, strict, thresholds, emitProgress) {
|
|
2707
|
+
const resolvedRoot = path10.resolve(rootPath);
|
|
2708
|
+
if (!fs9.existsSync(resolvedRoot) || !fs9.statSync(resolvedRoot).isDirectory()) {
|
|
2709
|
+
addDiagnostic(diagnostics, {
|
|
2710
|
+
severity: "error",
|
|
2711
|
+
skill: path10.basename(resolvedRoot),
|
|
2712
|
+
file: resolvedRoot,
|
|
2713
|
+
rule: "missing-root",
|
|
2714
|
+
message: "validation root does not exist"
|
|
2715
|
+
});
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
await emitProgress?.("discovering candidate skills");
|
|
2719
|
+
const skills = discoverSkills(resolvedRoot);
|
|
2720
|
+
if (skills.length === 0) {
|
|
2721
|
+
addDiagnostic(diagnostics, {
|
|
2722
|
+
severity: "error",
|
|
2723
|
+
skill: path10.basename(resolvedRoot),
|
|
2724
|
+
file: resolvedRoot,
|
|
2725
|
+
rule: "no-skills-discovered",
|
|
2726
|
+
message: "no SKILL.md files discovered"
|
|
2727
|
+
});
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
for (const skill of skills) {
|
|
2731
|
+
const resolvedSkillPath = path10.resolve(skill.path);
|
|
2732
|
+
if (seenSkillPaths.has(resolvedSkillPath)) {
|
|
2733
|
+
continue;
|
|
2734
|
+
}
|
|
2735
|
+
seenSkillPaths.add(resolvedSkillPath);
|
|
2736
|
+
await emitProgress?.(`validating ${skill.name}`);
|
|
2737
|
+
await validateSkillDir(skill.path, dependencyRoot, diagnostics, strict, thresholds);
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
async function stageAndValidateSource(options, diagnostics, thresholds, emitProgress) {
|
|
2741
|
+
if (!options.source) {
|
|
2742
|
+
throw new CoreError({
|
|
2743
|
+
code: "VALIDATION_MISSING_SOURCE",
|
|
2744
|
+
message: "validate requires a source unless CI mode is enabled"
|
|
2745
|
+
});
|
|
2746
|
+
}
|
|
2747
|
+
await emitProgress?.("parsing source");
|
|
2748
|
+
const parsed = parseSource(options.source);
|
|
2749
|
+
if (parsed.type === "well-known" || parsed.type === "catalog") {
|
|
2750
|
+
await emitProgress?.("discovering candidate skills");
|
|
2751
|
+
const remote = parsed.type === "well-known" ? await resolveWellKnownSkills(parsed.url, {
|
|
2752
|
+
allowHost: options.allowHost,
|
|
2753
|
+
denyHost: options.denyHost,
|
|
2754
|
+
maxDownloadBytes: options.maxDownloadBytes,
|
|
2755
|
+
experimental: options.experimental
|
|
2756
|
+
}) : await resolveCatalogSkills(parsed.url, {
|
|
2757
|
+
allowHost: options.allowHost,
|
|
2758
|
+
denyHost: options.denyHost,
|
|
2759
|
+
maxDownloadBytes: options.maxDownloadBytes,
|
|
2760
|
+
experimental: options.experimental
|
|
2761
|
+
});
|
|
2762
|
+
if (remote.length === 0) {
|
|
2763
|
+
addDiagnostic(diagnostics, {
|
|
2764
|
+
severity: "error",
|
|
2765
|
+
skill: parsed.url,
|
|
2766
|
+
file: parsed.url,
|
|
2767
|
+
rule: "no-skills-discovered",
|
|
2768
|
+
message: "no SKILL.md files discovered"
|
|
2769
|
+
});
|
|
2770
|
+
return;
|
|
2771
|
+
}
|
|
2772
|
+
const stagedRoots = [];
|
|
2773
|
+
try {
|
|
2774
|
+
for (const remoteSkill of remote) {
|
|
2775
|
+
const staged2 = stageRemoteSkillFilesToTempDir(remoteSkill.files, {
|
|
2776
|
+
prefix: "skillspp-validate-"
|
|
2777
|
+
});
|
|
2778
|
+
stagedRoots.push(staged2);
|
|
2779
|
+
await emitProgress?.(`validating ${remoteSkill.installName}`);
|
|
2780
|
+
await validateSkillDir(
|
|
2781
|
+
staged2.path,
|
|
2782
|
+
staged2.path,
|
|
2783
|
+
diagnostics,
|
|
2784
|
+
Boolean(options.strict),
|
|
2785
|
+
thresholds
|
|
2786
|
+
);
|
|
2787
|
+
}
|
|
2788
|
+
} finally {
|
|
2789
|
+
for (const staged2 of stagedRoots) {
|
|
2790
|
+
staged2.cleanup();
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
const staged = prepareSourceDir(parsed);
|
|
2796
|
+
try {
|
|
2797
|
+
await emitProgress?.("staging source");
|
|
2798
|
+
await stageAndValidateLocalRoot(
|
|
2799
|
+
staged.basePath,
|
|
2800
|
+
staged.basePath,
|
|
2801
|
+
/* @__PURE__ */ new Set(),
|
|
2802
|
+
diagnostics,
|
|
2803
|
+
Boolean(options.strict),
|
|
2804
|
+
thresholds,
|
|
2805
|
+
emitProgress
|
|
2806
|
+
);
|
|
2807
|
+
} finally {
|
|
2808
|
+
if (staged.cleanup) {
|
|
2809
|
+
staged.cleanup();
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
async function runValidateAnalysis(options, emitProgress) {
|
|
2814
|
+
const diagnostics = [];
|
|
2815
|
+
const thresholds = resolveThresholds(options);
|
|
2816
|
+
const seenSkillPaths = /* @__PURE__ */ new Set();
|
|
2817
|
+
if (options.ci) {
|
|
2818
|
+
await emitProgress?.("staging source");
|
|
2819
|
+
const roots = options.roots && options.roots.length > 0 ? options.roots : [process.cwd()];
|
|
2820
|
+
if (roots.length === 0) {
|
|
2821
|
+
throw new Error("No CI roots found to validate");
|
|
2822
|
+
}
|
|
2823
|
+
await emitProgress?.("discovering candidate skills");
|
|
2824
|
+
for (const root of roots) {
|
|
2825
|
+
await stageAndValidateLocalRoot(
|
|
2826
|
+
root,
|
|
2827
|
+
process.cwd(),
|
|
2828
|
+
seenSkillPaths,
|
|
2829
|
+
diagnostics,
|
|
2830
|
+
Boolean(options.strict),
|
|
2831
|
+
thresholds,
|
|
2832
|
+
emitProgress
|
|
2833
|
+
);
|
|
2834
|
+
}
|
|
2835
|
+
} else {
|
|
2836
|
+
await stageAndValidateSource(options, diagnostics, thresholds, emitProgress);
|
|
2837
|
+
}
|
|
2838
|
+
await emitProgress?.("collecting diagnostics");
|
|
2839
|
+
return { diagnostics };
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
// ../../packages/core/src/runtime/installer.ts
|
|
2843
|
+
import fs10 from "node:fs";
|
|
2844
|
+
import path11 from "node:path";
|
|
2845
|
+
function sanitizeSkillName(name) {
|
|
2846
|
+
const sanitized = name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[.-]+|[.-]+$/g, "");
|
|
2847
|
+
return sanitized || "unnamed-skill";
|
|
2848
|
+
}
|
|
2849
|
+
function ensureSafeInside(baseDir, target) {
|
|
2850
|
+
const resolvedBase = path11.resolve(baseDir);
|
|
2851
|
+
const resolvedTarget = path11.resolve(target);
|
|
2852
|
+
if (!(resolvedTarget === resolvedBase || resolvedTarget.startsWith(`${resolvedBase}${path11.sep}`))) {
|
|
2853
|
+
throw new Error(`Unsafe path detected: ${target}`);
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
function symlinkRelative(target, linkPath) {
|
|
2857
|
+
const parent = path11.dirname(linkPath);
|
|
2858
|
+
const relative = path11.relative(parent, target);
|
|
2859
|
+
const symlinkType = process.platform === "win32" ? "junction" : void 0;
|
|
2860
|
+
fs10.symlinkSync(relative, linkPath, symlinkType);
|
|
2861
|
+
}
|
|
2862
|
+
function installToAgent(itemName, canonicalDir, agent, mode, globalInstall, cwd, resolveAgentBaseDir) {
|
|
2863
|
+
const agentBase = resolveAgentBaseDir(agent, globalInstall, cwd);
|
|
2864
|
+
const agentDir = path11.join(agentBase, itemName);
|
|
2865
|
+
fs10.mkdirSync(agentBase, { recursive: true });
|
|
2866
|
+
ensureSafeInside(agentBase, agentDir);
|
|
2867
|
+
if (path11.resolve(agentDir) === path11.resolve(canonicalDir)) {
|
|
2868
|
+
return { agent, path: canonicalDir, mode };
|
|
2869
|
+
}
|
|
2870
|
+
if (mode === "copy") {
|
|
2871
|
+
fs10.rmSync(agentDir, { recursive: true, force: true });
|
|
2872
|
+
fs10.cpSync(canonicalDir, agentDir, { recursive: true, force: true });
|
|
2873
|
+
return { agent, path: agentDir, mode };
|
|
2874
|
+
}
|
|
2875
|
+
try {
|
|
2876
|
+
fs10.rmSync(agentDir, { recursive: true, force: true });
|
|
2877
|
+
symlinkRelative(canonicalDir, agentDir);
|
|
2878
|
+
return { agent, path: agentDir, mode: "symlink" };
|
|
2879
|
+
} catch {
|
|
2880
|
+
fs10.rmSync(agentDir, { recursive: true, force: true });
|
|
2881
|
+
fs10.cpSync(canonicalDir, agentDir, { recursive: true, force: true });
|
|
2882
|
+
return { agent, path: agentDir, mode: "copy" };
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
function installSkillToAgent(skillName, canonicalDir, agent, mode, globalInstall, cwd) {
|
|
2886
|
+
return installToAgent(
|
|
2887
|
+
skillName,
|
|
2888
|
+
canonicalDir,
|
|
2889
|
+
agent,
|
|
2890
|
+
mode,
|
|
2891
|
+
globalInstall,
|
|
2892
|
+
cwd,
|
|
2893
|
+
getAgentSkillsDir
|
|
2894
|
+
);
|
|
2895
|
+
}
|
|
2896
|
+
function installToCanonicalDir(sourcePath, itemName, canonicalBase) {
|
|
2897
|
+
const canonicalDir = path11.join(canonicalBase, itemName);
|
|
2898
|
+
fs10.mkdirSync(canonicalBase, { recursive: true });
|
|
2899
|
+
ensureSafeInside(canonicalBase, canonicalDir);
|
|
2900
|
+
fs10.rmSync(canonicalDir, { recursive: true, force: true });
|
|
2901
|
+
fs10.cpSync(sourcePath, canonicalDir, { recursive: true, force: true });
|
|
2902
|
+
return canonicalDir;
|
|
2903
|
+
}
|
|
2904
|
+
function installSkill(skill, agents, options) {
|
|
2905
|
+
if (agents.length === 0) {
|
|
2906
|
+
throw new Error("At least one target agent is required for installation.");
|
|
2907
|
+
}
|
|
2908
|
+
const uniqueAgents = Array.from(new Set(agents));
|
|
2909
|
+
const skillName = sanitizeSkillName(skill.name);
|
|
2910
|
+
const canonicalAgent = uniqueAgents[0];
|
|
2911
|
+
const canonicalBase = getAgentSkillsDir(canonicalAgent, options.globalInstall, options.cwd);
|
|
2912
|
+
const canonicalDir = installToCanonicalDir(skill.path, skillName, canonicalBase);
|
|
2913
|
+
const installedTo = [
|
|
2914
|
+
{ agent: canonicalAgent, path: canonicalDir, mode: options.mode },
|
|
2915
|
+
...uniqueAgents.slice(1).map(
|
|
2916
|
+
(agent) => installSkillToAgent(
|
|
2917
|
+
skillName,
|
|
2918
|
+
canonicalDir,
|
|
2919
|
+
agent,
|
|
2920
|
+
options.mode,
|
|
2921
|
+
options.globalInstall,
|
|
2922
|
+
options.cwd
|
|
2923
|
+
)
|
|
2924
|
+
)
|
|
2925
|
+
];
|
|
2926
|
+
return {
|
|
2927
|
+
skillName,
|
|
2928
|
+
canonicalDir,
|
|
2929
|
+
installedTo
|
|
2930
|
+
};
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
// ../../packages/core/src/sources/scanner.ts
|
|
2934
|
+
import fs11 from "node:fs";
|
|
2935
|
+
import os6 from "node:os";
|
|
2936
|
+
import path12 from "node:path";
|
|
2937
|
+
function listDirs(dir) {
|
|
2938
|
+
if (!fs11.existsSync(dir) || !fs11.statSync(dir).isDirectory()) {
|
|
2939
|
+
return [];
|
|
2940
|
+
}
|
|
2941
|
+
return fs11.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
2942
|
+
}
|
|
2943
|
+
function detectLocalGlobalConflicts(cwd) {
|
|
2944
|
+
const localDir = path12.join(cwd, ".agents", "skills");
|
|
2945
|
+
const globalDir = path12.join(os6.homedir(), ".config", "agents", "skills");
|
|
2946
|
+
const local = new Set(listDirs(localDir));
|
|
2947
|
+
const global = new Set(listDirs(globalDir));
|
|
2948
|
+
const overlap = [...local].filter((name) => global.has(name));
|
|
2949
|
+
return overlap.sort((a, b) => a.localeCompare(b)).map((skillName) => ({
|
|
2950
|
+
skillName,
|
|
2951
|
+
winner: "local"
|
|
2952
|
+
}));
|
|
2953
|
+
}
|
|
2954
|
+
function readPackageMeta(packageDir) {
|
|
2955
|
+
const packageJson = path12.join(packageDir, "package.json");
|
|
2956
|
+
if (!fs11.existsSync(packageJson) || !fs11.statSync(packageJson).isFile()) {
|
|
2957
|
+
return {
|
|
2958
|
+
name: path12.basename(packageDir),
|
|
2959
|
+
version: "0.0.0"
|
|
2960
|
+
};
|
|
2961
|
+
}
|
|
2962
|
+
const parsed = JSON.parse(fs11.readFileSync(packageJson, "utf8"));
|
|
2963
|
+
return {
|
|
2964
|
+
name: parsed.name || path12.basename(packageDir),
|
|
2965
|
+
version: parsed.version || "0.0.0"
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
function collectSkillsFromPackage(packageDir, depth, out) {
|
|
2969
|
+
const meta = readPackageMeta(packageDir);
|
|
2970
|
+
const skillsDir = path12.join(packageDir, "skills");
|
|
2971
|
+
if (fs11.existsSync(skillsDir) && fs11.statSync(skillsDir).isDirectory()) {
|
|
2972
|
+
for (const entry of fs11.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
2973
|
+
if (!entry.isDirectory()) {
|
|
2974
|
+
continue;
|
|
2975
|
+
}
|
|
2976
|
+
const skillDir = path12.join(skillsDir, entry.name);
|
|
2977
|
+
const skillMd = path12.join(skillDir, "SKILL.md");
|
|
2978
|
+
if (!fs11.existsSync(skillMd) || !fs11.statSync(skillMd).isFile()) {
|
|
2979
|
+
continue;
|
|
2980
|
+
}
|
|
2981
|
+
out.push({
|
|
2982
|
+
skillName: entry.name,
|
|
2983
|
+
skillDir,
|
|
2984
|
+
packageName: meta.name,
|
|
2985
|
+
packageVersion: meta.version,
|
|
2986
|
+
depth
|
|
2987
|
+
});
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
function walkNodeModules(nodeModulesDir, depth, maxDepth, seen, out) {
|
|
2992
|
+
const resolvedNodeModules = path12.resolve(nodeModulesDir);
|
|
2993
|
+
if (seen.has(resolvedNodeModules) || depth > maxDepth) {
|
|
2994
|
+
return;
|
|
2995
|
+
}
|
|
2996
|
+
seen.add(resolvedNodeModules);
|
|
2997
|
+
if (!fs11.existsSync(resolvedNodeModules) || !fs11.statSync(resolvedNodeModules).isDirectory()) {
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
for (const entry of fs11.readdirSync(resolvedNodeModules, {
|
|
3001
|
+
withFileTypes: true
|
|
3002
|
+
})) {
|
|
3003
|
+
if (!entry.isDirectory()) {
|
|
3004
|
+
continue;
|
|
3005
|
+
}
|
|
3006
|
+
if (entry.name.startsWith("@")) {
|
|
3007
|
+
const scopeDir = path12.join(resolvedNodeModules, entry.name);
|
|
3008
|
+
for (const scoped of fs11.readdirSync(scopeDir, { withFileTypes: true })) {
|
|
3009
|
+
if (!scoped.isDirectory()) {
|
|
3010
|
+
continue;
|
|
3011
|
+
}
|
|
3012
|
+
const packageDir2 = path12.join(scopeDir, scoped.name);
|
|
3013
|
+
collectSkillsFromPackage(packageDir2, depth, out);
|
|
3014
|
+
walkNodeModules(path12.join(packageDir2, "node_modules"), depth + 1, maxDepth, seen, out);
|
|
3015
|
+
}
|
|
3016
|
+
continue;
|
|
3017
|
+
}
|
|
3018
|
+
const packageDir = path12.join(resolvedNodeModules, entry.name);
|
|
3019
|
+
collectSkillsFromPackage(packageDir, depth, out);
|
|
3020
|
+
walkNodeModules(path12.join(packageDir, "node_modules"), depth + 1, maxDepth, seen, out);
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
function discoverTransitiveSkillCandidates(cwd, options = {}) {
|
|
3024
|
+
const maxDepth = options.maxDepth ?? 8;
|
|
3025
|
+
const out = [];
|
|
3026
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3027
|
+
walkNodeModules(path12.join(cwd, "node_modules"), 0, maxDepth, seen, out);
|
|
3028
|
+
return out.sort((a, b) => {
|
|
3029
|
+
if (a.skillName !== b.skillName) {
|
|
3030
|
+
return a.skillName.localeCompare(b.skillName);
|
|
3031
|
+
}
|
|
3032
|
+
if (a.depth !== b.depth) {
|
|
3033
|
+
return a.depth - b.depth;
|
|
3034
|
+
}
|
|
3035
|
+
if (a.packageName !== b.packageName) {
|
|
3036
|
+
return a.packageName.localeCompare(b.packageName);
|
|
3037
|
+
}
|
|
3038
|
+
if (a.packageVersion !== b.packageVersion) {
|
|
3039
|
+
return b.packageVersion.localeCompare(a.packageVersion);
|
|
3040
|
+
}
|
|
3041
|
+
return a.skillDir.localeCompare(b.skillDir);
|
|
3042
|
+
});
|
|
3043
|
+
}
|
|
3044
|
+
function detectTransitiveSkillConflicts(candidates) {
|
|
3045
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3046
|
+
for (const candidate of candidates) {
|
|
3047
|
+
const rows = grouped.get(candidate.skillName) || [];
|
|
3048
|
+
rows.push(candidate);
|
|
3049
|
+
grouped.set(candidate.skillName, rows);
|
|
3050
|
+
}
|
|
3051
|
+
const conflicts = [];
|
|
3052
|
+
for (const [skillName, rows] of grouped) {
|
|
3053
|
+
if (rows.length <= 1) {
|
|
3054
|
+
continue;
|
|
3055
|
+
}
|
|
3056
|
+
const sorted = [...rows].sort((a, b) => {
|
|
3057
|
+
if (a.depth !== b.depth) {
|
|
3058
|
+
return a.depth - b.depth;
|
|
3059
|
+
}
|
|
3060
|
+
if (a.packageName !== b.packageName) {
|
|
3061
|
+
return a.packageName.localeCompare(b.packageName);
|
|
3062
|
+
}
|
|
3063
|
+
if (a.packageVersion !== b.packageVersion) {
|
|
3064
|
+
return b.packageVersion.localeCompare(a.packageVersion);
|
|
3065
|
+
}
|
|
3066
|
+
return a.skillDir.localeCompare(b.skillDir);
|
|
3067
|
+
});
|
|
3068
|
+
conflicts.push({
|
|
3069
|
+
skillName,
|
|
3070
|
+
winner: sorted[0],
|
|
3071
|
+
losers: sorted.slice(1)
|
|
3072
|
+
});
|
|
3073
|
+
}
|
|
3074
|
+
return conflicts.sort((a, b) => a.skillName.localeCompare(b.skillName));
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
// ../../packages/core/src/runtime/installer-scaffold.ts
|
|
3078
|
+
import fs12 from "node:fs";
|
|
3079
|
+
import path13 from "node:path";
|
|
3080
|
+
import YAML3 from "yaml";
|
|
3081
|
+
function installerConfigSkeleton() {
|
|
3082
|
+
return {
|
|
3083
|
+
schemaVersion: 1,
|
|
3084
|
+
dependencies: [],
|
|
3085
|
+
"pre-install": [],
|
|
3086
|
+
"post-install": []
|
|
3087
|
+
};
|
|
3088
|
+
}
|
|
3089
|
+
function isFile(filePath) {
|
|
3090
|
+
return fs12.existsSync(filePath) && fs12.statSync(filePath).isFile();
|
|
3091
|
+
}
|
|
3092
|
+
function getInstallerConfigState(skillDir) {
|
|
3093
|
+
const yamlPath = path13.join(skillDir, "skill-installer.yaml");
|
|
3094
|
+
const jsonPath = path13.join(skillDir, "skill-installer.json");
|
|
3095
|
+
const hasYaml = isFile(yamlPath);
|
|
3096
|
+
const hasJson = isFile(jsonPath);
|
|
3097
|
+
if (hasYaml && hasJson) {
|
|
3098
|
+
throw new Error(
|
|
3099
|
+
"Both skill-installer.yaml and skill-installer.json exist. Keep only one installer config file."
|
|
3100
|
+
);
|
|
3101
|
+
}
|
|
3102
|
+
return {
|
|
3103
|
+
yamlPath,
|
|
3104
|
+
jsonPath,
|
|
3105
|
+
hasYaml,
|
|
3106
|
+
hasJson,
|
|
3107
|
+
missing: !hasYaml && !hasJson
|
|
3108
|
+
};
|
|
3109
|
+
}
|
|
3110
|
+
function listSkillsMissingInstallerConfig(skillDirs) {
|
|
3111
|
+
return skillDirs.filter((skillDir) => getInstallerConfigState(skillDir).missing);
|
|
3112
|
+
}
|
|
3113
|
+
function scaffoldInstallerConfigFile(skillDir, format) {
|
|
3114
|
+
const state = getInstallerConfigState(skillDir);
|
|
3115
|
+
if (!state.missing) {
|
|
3116
|
+
return { created: false };
|
|
3117
|
+
}
|
|
3118
|
+
const content = format === "yaml" ? YAML3.stringify(installerConfigSkeleton()) : JSON.stringify(installerConfigSkeleton(), null, 2);
|
|
3119
|
+
const destinationPath = format === "yaml" ? state.yamlPath : state.jsonPath;
|
|
3120
|
+
fs12.writeFileSync(destinationPath, `${content}
|
|
3121
|
+
`, "utf8");
|
|
3122
|
+
return { created: true, filePath: destinationPath };
|
|
3123
|
+
}
|
|
3124
|
+
function scaffoldInstallerConfigForSkills(skillDirs, format) {
|
|
3125
|
+
const created = [];
|
|
3126
|
+
for (const skillDir of skillDirs) {
|
|
3127
|
+
const result = scaffoldInstallerConfigFile(skillDir, format);
|
|
3128
|
+
if (result.created && result.filePath) {
|
|
3129
|
+
created.push(result.filePath);
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
return created;
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
// ../../packages/core/src/runtime/background-tasks.ts
|
|
3136
|
+
function serializeAssessments(assessments) {
|
|
3137
|
+
return assessments.map((assessment) => ({
|
|
3138
|
+
entry: assessment.entry,
|
|
3139
|
+
drift: assessment.drift
|
|
3140
|
+
}));
|
|
3141
|
+
}
|
|
3142
|
+
function resolveAddGlobalInstall(options) {
|
|
3143
|
+
if (options.globalFlagProvided) {
|
|
3144
|
+
return Boolean(options.global);
|
|
3145
|
+
}
|
|
3146
|
+
return false;
|
|
3147
|
+
}
|
|
3148
|
+
function resolveAddInstallMode(options) {
|
|
3149
|
+
if (options.symlinkFlagProvided) {
|
|
3150
|
+
return "symlink";
|
|
3151
|
+
}
|
|
3152
|
+
return "copy";
|
|
3153
|
+
}
|
|
3154
|
+
function resolveAddInstallerScaffoldFormat(options, missingCount) {
|
|
3155
|
+
if (missingCount === 0) {
|
|
3156
|
+
return void 0;
|
|
3157
|
+
}
|
|
3158
|
+
return options.yaml ? "yaml" : "json";
|
|
3159
|
+
}
|
|
3160
|
+
function sourceHashForInstalledSkill(options) {
|
|
3161
|
+
if (options.parsedSource.type === "local") {
|
|
3162
|
+
return hashDirectory(options.skillPath);
|
|
3163
|
+
}
|
|
3164
|
+
return options.beforeHash || hashDirectory(options.skillPath);
|
|
3165
|
+
}
|
|
3166
|
+
function canonicalSourceIdentity(options) {
|
|
3167
|
+
if (options.parsedSource.type === "local") {
|
|
3168
|
+
return options.parsedSource.localPath;
|
|
3169
|
+
}
|
|
3170
|
+
if (options.parsedSource.type === "well-known") {
|
|
3171
|
+
return options.wellKnownSourceUrl || options.parsedSource.url;
|
|
3172
|
+
}
|
|
3173
|
+
if (options.parsedSource.type === "catalog") {
|
|
3174
|
+
return options.wellKnownSourceUrl || options.parsedSource.url;
|
|
3175
|
+
}
|
|
3176
|
+
if (options.parsedSource.type === "github") {
|
|
3177
|
+
const suffix = options.parsedSource.subpath ? `#${options.parsedSource.subpath}` : "";
|
|
3178
|
+
return `${options.parsedSource.repoUrl}${suffix}`;
|
|
3179
|
+
}
|
|
3180
|
+
return options.parsedSource.repoUrl;
|
|
3181
|
+
}
|
|
3182
|
+
function resolveSafeRealPath2(inputPath) {
|
|
3183
|
+
try {
|
|
3184
|
+
return fs13.realpathSync(inputPath);
|
|
3185
|
+
} catch {
|
|
3186
|
+
return path14.resolve(inputPath);
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
function isLocalSymlinkSource2(localPath) {
|
|
3190
|
+
try {
|
|
3191
|
+
if (!fs13.existsSync(localPath)) {
|
|
3192
|
+
return false;
|
|
3193
|
+
}
|
|
3194
|
+
return fs13.lstatSync(localPath).isSymbolicLink();
|
|
3195
|
+
} catch {
|
|
3196
|
+
return false;
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
function lockfileNameForFormat(format) {
|
|
3200
|
+
return format === "yaml" ? "skillspp-lock.yaml" : "skillspp-lock.json";
|
|
3201
|
+
}
|
|
3202
|
+
function staleLockfileNameForFormat(format) {
|
|
3203
|
+
return format === "yaml" ? "skillspp-lock.json" : "skillspp-lock.yaml";
|
|
3204
|
+
}
|
|
3205
|
+
function propagateLockfileVisibility(options) {
|
|
3206
|
+
const lockName = lockfileNameForFormat(options.lockFormat);
|
|
3207
|
+
const staleLockName = staleLockfileNameForFormat(options.lockFormat);
|
|
3208
|
+
const canonicalLockPath = path14.join(options.canonicalDir, lockName);
|
|
3209
|
+
const canonicalRealPath = fs13.existsSync(options.canonicalDir) ? fs13.realpathSync(options.canonicalDir) : path14.resolve(options.canonicalDir);
|
|
3210
|
+
for (const targetDir of options.targetDirs) {
|
|
3211
|
+
const targetRealPath = fs13.existsSync(targetDir) ? fs13.realpathSync(targetDir) : path14.resolve(targetDir);
|
|
3212
|
+
if (targetRealPath === canonicalRealPath) {
|
|
3213
|
+
continue;
|
|
3214
|
+
}
|
|
3215
|
+
const targetLockPath = path14.join(targetDir, lockName);
|
|
3216
|
+
const staleTargetPath = path14.join(targetDir, staleLockName);
|
|
3217
|
+
fs13.copyFileSync(canonicalLockPath, targetLockPath);
|
|
3218
|
+
if (fs13.existsSync(staleTargetPath)) {
|
|
3219
|
+
fs13.rmSync(staleTargetPath, { force: true });
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
function writeLockEntryAfterInstall(options) {
|
|
3224
|
+
const installedHash = hashDirectory(options.outcome.canonicalDir);
|
|
3225
|
+
const resourceKind = options.resourceKind || "skill";
|
|
3226
|
+
const lock = readResourceLockfile(resourceKind, options.globalInstall, options.cwd);
|
|
3227
|
+
const entry = {
|
|
3228
|
+
skillName: options.outcome.skillName,
|
|
3229
|
+
global: options.globalInstall,
|
|
3230
|
+
installMode: options.mode,
|
|
3231
|
+
agents: options.outcome.installedTo.map((row) => row.agent),
|
|
3232
|
+
canonicalDir: options.outcome.canonicalDir,
|
|
3233
|
+
source: {
|
|
3234
|
+
input: options.sourceInput,
|
|
3235
|
+
type: options.sourceType,
|
|
3236
|
+
canonical: options.sourceCanonical,
|
|
3237
|
+
pinnedRef: options.sourcePinnedRef,
|
|
3238
|
+
resolvedPath: options.sourceResolvedPath,
|
|
3239
|
+
isSymlinkSource: options.sourceIsSymlink,
|
|
3240
|
+
selector: {
|
|
3241
|
+
skillName: options.sourceSkillName,
|
|
3242
|
+
relativePath: options.sourceSkillPath,
|
|
3243
|
+
wellKnownSourceUrl: options.wellKnownSourceUrl
|
|
3244
|
+
}
|
|
3245
|
+
},
|
|
3246
|
+
sourceHash: options.sourceHash,
|
|
3247
|
+
installedHash,
|
|
3248
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3249
|
+
};
|
|
3250
|
+
const next = upsertResourceLockEntry(lock, entry);
|
|
3251
|
+
writeResourceLockfile(
|
|
3252
|
+
resourceKind,
|
|
3253
|
+
options.globalInstall,
|
|
3254
|
+
options.cwd,
|
|
3255
|
+
next,
|
|
3256
|
+
options.lockFormat || "json"
|
|
3257
|
+
);
|
|
3258
|
+
const selectedFormat = options.lockFormat || "json";
|
|
3259
|
+
propagateLockfileVisibility({
|
|
3260
|
+
canonicalDir: options.outcome.canonicalDir,
|
|
3261
|
+
targetDirs: options.outcome.installedTo.map((destination) => destination.path),
|
|
3262
|
+
lockFormat: selectedFormat
|
|
3263
|
+
});
|
|
3264
|
+
const staleLockPath = path14.join(
|
|
3265
|
+
options.outcome.canonicalDir,
|
|
3266
|
+
staleLockfileNameForFormat(selectedFormat)
|
|
3267
|
+
);
|
|
3268
|
+
if (fs13.existsSync(staleLockPath)) {
|
|
3269
|
+
fs13.rmSync(staleLockPath, { force: true });
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
3272
|
+
function buildRemoteSkill(remote) {
|
|
3273
|
+
const staged = stageRemoteSkillFilesToTempDir(remote.files);
|
|
3274
|
+
return {
|
|
3275
|
+
skill: {
|
|
3276
|
+
name: remote.installName,
|
|
3277
|
+
description: remote.description,
|
|
3278
|
+
path: staged.path
|
|
3279
|
+
},
|
|
3280
|
+
cleanup: staged.cleanup
|
|
3281
|
+
};
|
|
3282
|
+
}
|
|
3283
|
+
async function runCheckScanTask(cwd, options, emitProgress) {
|
|
3284
|
+
await emitProgress("checking drift");
|
|
3285
|
+
const assessed = await assessLockEntries(options, cwd, {
|
|
3286
|
+
keepResolved: false
|
|
3287
|
+
});
|
|
3288
|
+
await emitProgress("checking local/global conflicts");
|
|
3289
|
+
const conflicts = detectLocalGlobalConflicts(cwd);
|
|
3290
|
+
await emitProgress("checking transitive conflicts");
|
|
3291
|
+
const transitiveCandidates = discoverTransitiveSkillCandidates(cwd);
|
|
3292
|
+
const transitiveConflicts = detectTransitiveSkillConflicts(transitiveCandidates);
|
|
3293
|
+
return {
|
|
3294
|
+
drift: assessed.drift,
|
|
3295
|
+
checked: assessed.checked,
|
|
3296
|
+
conflicts,
|
|
3297
|
+
transitiveConflicts
|
|
3298
|
+
};
|
|
3299
|
+
}
|
|
3300
|
+
async function runUpdateAssessTask(cwd, options, emitProgress) {
|
|
3301
|
+
await emitProgress("assessing drift");
|
|
3302
|
+
const assessed = await assessLockEntries(options, cwd, {
|
|
3303
|
+
keepResolved: false
|
|
3304
|
+
});
|
|
3305
|
+
return {
|
|
3306
|
+
assessments: serializeAssessments(assessed.assessments)
|
|
3307
|
+
};
|
|
3308
|
+
}
|
|
3309
|
+
function createBackupDir(skillName, sourceDir) {
|
|
3310
|
+
const backupRoot = fs13.mkdtempSync(path14.join(os7.tmpdir(), "skillspp-update-backup-"));
|
|
3311
|
+
const backupDir = path14.join(backupRoot, skillName);
|
|
3312
|
+
fs13.mkdirSync(backupDir, { recursive: true });
|
|
3313
|
+
fs13.cpSync(sourceDir, backupDir, { recursive: true, force: true });
|
|
3314
|
+
return backupDir;
|
|
3315
|
+
}
|
|
3316
|
+
async function applyEntryUpdate(assessment, options, cwd) {
|
|
3317
|
+
const { entry } = assessment;
|
|
3318
|
+
if (!assessment.resolved || !assessment.sourceHash) {
|
|
3319
|
+
throw new Error(`No resolved source available for ${entry.skillName}`);
|
|
3320
|
+
}
|
|
3321
|
+
const resolved = assessment.resolved;
|
|
3322
|
+
const sourceHash = assessment.sourceHash;
|
|
3323
|
+
const backupDir = createBackupDir(entry.skillName, entry.canonicalDir);
|
|
3324
|
+
try {
|
|
3325
|
+
const preparedInstaller = await prepareInstallerArtifacts(resolved.skill.path, cwd, {
|
|
3326
|
+
sourceType: entry.source.type,
|
|
3327
|
+
policyMode: options.policyMode || "enforce",
|
|
3328
|
+
trustWellKnown: Boolean(options.trustWellKnown)
|
|
3329
|
+
});
|
|
3330
|
+
const outcome = installSkill(resolved.skill, entry.agents, {
|
|
3331
|
+
mode: entry.installMode,
|
|
3332
|
+
globalInstall: entry.global,
|
|
3333
|
+
cwd
|
|
3334
|
+
});
|
|
3335
|
+
try {
|
|
3336
|
+
await applyInstallerArtifacts(outcome.canonicalDir, preparedInstaller);
|
|
3337
|
+
} finally {
|
|
3338
|
+
cleanupPreparedInstallerArtifacts(preparedInstaller);
|
|
3339
|
+
}
|
|
3340
|
+
const installedHash = await hashDirectoryAsync(outcome.canonicalDir);
|
|
3341
|
+
return {
|
|
3342
|
+
...entry,
|
|
3343
|
+
source: {
|
|
3344
|
+
...entry.source,
|
|
3345
|
+
canonical: assessment.refreshedSource?.canonical ?? entry.source.canonical,
|
|
3346
|
+
pinnedRef: assessment.refreshedSource?.pinnedRef ?? entry.source.pinnedRef,
|
|
3347
|
+
resolvedPath: assessment.refreshedSource?.resolvedPath ?? entry.source.resolvedPath,
|
|
3348
|
+
isSymlinkSource: assessment.refreshedSource?.isSymlinkSource ?? entry.source.isSymlinkSource,
|
|
3349
|
+
selector: {
|
|
3350
|
+
...entry.source.selector,
|
|
3351
|
+
relativePath: assessment.refreshedSource?.sourceSkillPath ?? entry.source.selector.relativePath,
|
|
3352
|
+
wellKnownSourceUrl: assessment.refreshedSource?.wellKnownSourceUrl ?? entry.source.selector.wellKnownSourceUrl
|
|
3353
|
+
}
|
|
3354
|
+
},
|
|
3355
|
+
sourceHash,
|
|
3356
|
+
installedHash,
|
|
3357
|
+
canonicalDir: outcome.canonicalDir,
|
|
3358
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3359
|
+
};
|
|
3360
|
+
} catch (error) {
|
|
3361
|
+
const rollbackSkill = {
|
|
3362
|
+
name: entry.skillName,
|
|
3363
|
+
description: `Rollback for ${entry.skillName}`,
|
|
3364
|
+
path: backupDir
|
|
3365
|
+
};
|
|
3366
|
+
installSkill(rollbackSkill, entry.agents, {
|
|
3367
|
+
mode: entry.installMode,
|
|
3368
|
+
globalInstall: entry.global,
|
|
3369
|
+
cwd
|
|
3370
|
+
});
|
|
3371
|
+
throw error;
|
|
3372
|
+
} finally {
|
|
3373
|
+
fs13.rmSync(path14.dirname(backupDir), { recursive: true, force: true });
|
|
3374
|
+
if (resolved.cleanup) {
|
|
3375
|
+
resolved.cleanup();
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
async function runUpdateApplyTask(payload, emitProgress) {
|
|
3380
|
+
const selectedOptions = {
|
|
3381
|
+
...payload.options,
|
|
3382
|
+
skill: payload.selectedSkillNames
|
|
3383
|
+
};
|
|
3384
|
+
await emitProgress("assessing selected skills");
|
|
3385
|
+
const assessed = await assessLockEntries(selectedOptions, payload.cwd, {
|
|
3386
|
+
keepResolved: true
|
|
3387
|
+
});
|
|
3388
|
+
const candidateAssessments = assessed.assessments.filter(
|
|
3389
|
+
(assessment) => !assessment.drift.some((item) => item.kind === "migrate-required") && assessment.drift.some(
|
|
3390
|
+
(item) => item.kind === "changed-source" || item.kind === "local-modified"
|
|
3391
|
+
)
|
|
3392
|
+
);
|
|
3393
|
+
let nextLock = readLockfile(Boolean(payload.options.global), payload.cwd);
|
|
3394
|
+
const updatedEntries = [];
|
|
3395
|
+
const ordered = [...candidateAssessments].sort(
|
|
3396
|
+
(a, b) => a.entry.skillName.localeCompare(b.entry.skillName)
|
|
3397
|
+
);
|
|
3398
|
+
try {
|
|
3399
|
+
for (const assessment of ordered) {
|
|
3400
|
+
await emitProgress(`updating ${assessment.entry.skillName}`);
|
|
3401
|
+
const updated = await applyEntryUpdate(assessment, payload.options, payload.cwd);
|
|
3402
|
+
updatedEntries.push(updated);
|
|
3403
|
+
nextLock = upsertLockEntry(nextLock, updated);
|
|
3404
|
+
}
|
|
3405
|
+
await emitProgress("writing lockfile");
|
|
3406
|
+
writeLockfile(Boolean(payload.options.global), payload.cwd, nextLock, payload.lockFormat);
|
|
3407
|
+
for (const updated of updatedEntries) {
|
|
3408
|
+
const targetDirs = Array.from(
|
|
3409
|
+
new Set(
|
|
3410
|
+
updated.agents.map(
|
|
3411
|
+
(agent) => path14.join(getAgentSkillsDir(agent, updated.global, payload.cwd), updated.skillName)
|
|
3412
|
+
)
|
|
3413
|
+
)
|
|
3414
|
+
);
|
|
3415
|
+
propagateLockfileVisibility({
|
|
3416
|
+
canonicalDir: updated.canonicalDir,
|
|
3417
|
+
targetDirs,
|
|
3418
|
+
lockFormat: payload.lockFormat
|
|
3419
|
+
});
|
|
3420
|
+
}
|
|
3421
|
+
return {
|
|
3422
|
+
updatedSkillNames: ordered.map((assessment) => assessment.entry.skillName)
|
|
3423
|
+
};
|
|
3424
|
+
} finally {
|
|
3425
|
+
for (const assessment of assessed.assessments) {
|
|
3426
|
+
if (assessment.resolved?.cleanup) {
|
|
3427
|
+
assessment.resolved.cleanup();
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
async function resolveMigrationSource(options) {
|
|
3433
|
+
const parsedSource = parseSource(options.sourceInput);
|
|
3434
|
+
if (parsedSource.type === "well-known" || parsedSource.type === "catalog") {
|
|
3435
|
+
const remoteSkills = parsedSource.type === "well-known" ? await resolveWellKnownSkills(parsedSource.url, options.addOptions) : await resolveCatalogSkills(parsedSource.url, options.addOptions);
|
|
3436
|
+
const remote = remoteSkills.find((item) => item.installName === options.skillName);
|
|
3437
|
+
if (!remote) {
|
|
3438
|
+
throw new Error(`Skill '${options.skillName}' not found in migration source`);
|
|
3439
|
+
}
|
|
3440
|
+
const staged = buildRemoteSkill(remote);
|
|
3441
|
+
const sourceHash = hashDirectory(staged.skill.path);
|
|
3442
|
+
return {
|
|
3443
|
+
parsedSource,
|
|
3444
|
+
skill: staged.skill,
|
|
3445
|
+
sourceHash,
|
|
3446
|
+
sourceCanonical: canonicalSourceIdentity({
|
|
3447
|
+
parsedSource,
|
|
3448
|
+
wellKnownSourceUrl: remote.sourceUrl
|
|
3449
|
+
}),
|
|
3450
|
+
sourcePinnedRef: sourceHash,
|
|
3451
|
+
wellKnownSourceUrl: remote.sourceUrl,
|
|
3452
|
+
cleanup: staged.cleanup
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
const prepared = await prepareSourceDirAsync(
|
|
3456
|
+
parsedSource
|
|
3457
|
+
);
|
|
3458
|
+
try {
|
|
3459
|
+
const skills = await discoverSkillsAsync(prepared.basePath);
|
|
3460
|
+
const skill = skills.find((item) => item.name === options.skillName);
|
|
3461
|
+
if (!skill) {
|
|
3462
|
+
throw new Error(`Skill '${options.skillName}' not found in migration source`);
|
|
3463
|
+
}
|
|
3464
|
+
const sourceHash = sourceHashForInstalledSkill({
|
|
3465
|
+
parsedSource,
|
|
3466
|
+
skillPath: skill.path
|
|
3467
|
+
});
|
|
3468
|
+
const sourcePinnedRef = parsedSource.type === "github" || parsedSource.type === "git" ? await resolveGitHeadRefAsync(prepared.basePath) : void 0;
|
|
3469
|
+
return {
|
|
3470
|
+
parsedSource,
|
|
3471
|
+
skill,
|
|
3472
|
+
sourceSkillPath: path14.relative(prepared.basePath, skill.path) || ".",
|
|
3473
|
+
sourceHash,
|
|
3474
|
+
sourceCanonical: canonicalSourceIdentity({ parsedSource }),
|
|
3475
|
+
sourcePinnedRef,
|
|
3476
|
+
sourceResolvedPath: parsedSource.type === "local" ? resolveSafeRealPath2(skill.path) : void 0,
|
|
3477
|
+
sourceIsSymlink: parsedSource.type === "local" ? isLocalSymlinkSource2(parsedSource.localPath) : void 0,
|
|
3478
|
+
cleanup: prepared.cleanup
|
|
3479
|
+
};
|
|
3480
|
+
} catch (error) {
|
|
3481
|
+
if (prepared.cleanup) {
|
|
3482
|
+
prepared.cleanup();
|
|
3483
|
+
}
|
|
3484
|
+
throw error;
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
async function runUpdateMigrateTask(payload, emitProgress) {
|
|
3488
|
+
await emitProgress("resolving migration source");
|
|
3489
|
+
const lock = readLockfile(Boolean(payload.options.global), payload.cwd);
|
|
3490
|
+
const entry = lock.entries.find((item) => item.skillName === payload.skillName);
|
|
3491
|
+
if (!entry) {
|
|
3492
|
+
throw new Error(`Unknown skill for migration: ${payload.skillName}`);
|
|
3493
|
+
}
|
|
3494
|
+
const source = await resolveMigrationSource({
|
|
3495
|
+
sourceInput: payload.sourceInput,
|
|
3496
|
+
skillName: payload.skillName,
|
|
3497
|
+
addOptions: payload.options
|
|
3498
|
+
});
|
|
3499
|
+
const backupDir = createBackupDir(entry.skillName, entry.canonicalDir);
|
|
3500
|
+
try {
|
|
3501
|
+
await emitProgress(`migrating ${entry.skillName}`);
|
|
3502
|
+
const preparedInstaller = await prepareInstallerArtifacts(source.skill.path, payload.cwd, {
|
|
3503
|
+
sourceType: source.parsedSource.type,
|
|
3504
|
+
policyMode: payload.options.policyMode || "enforce",
|
|
3505
|
+
trustWellKnown: Boolean(payload.options.trustWellKnown)
|
|
3506
|
+
});
|
|
3507
|
+
const outcome = installSkill(source.skill, entry.agents, {
|
|
3508
|
+
mode: entry.installMode,
|
|
3509
|
+
globalInstall: entry.global,
|
|
3510
|
+
cwd: payload.cwd
|
|
3511
|
+
});
|
|
3512
|
+
try {
|
|
3513
|
+
await applyInstallerArtifacts(outcome.canonicalDir, preparedInstaller);
|
|
3514
|
+
await emitProgress("writing lockfile");
|
|
3515
|
+
writeLockEntryAfterInstall({
|
|
3516
|
+
globalInstall: entry.global,
|
|
3517
|
+
cwd: payload.cwd,
|
|
3518
|
+
sourceInput: payload.sourceInput,
|
|
3519
|
+
sourceType: source.parsedSource.type,
|
|
3520
|
+
sourceCanonical: source.sourceCanonical,
|
|
3521
|
+
sourcePinnedRef: source.sourcePinnedRef,
|
|
3522
|
+
sourceResolvedPath: source.sourceResolvedPath,
|
|
3523
|
+
sourceIsSymlink: source.sourceIsSymlink,
|
|
3524
|
+
sourceSkillName: source.skill.name,
|
|
3525
|
+
sourceSkillPath: source.sourceSkillPath,
|
|
3526
|
+
wellKnownSourceUrl: source.wellKnownSourceUrl,
|
|
3527
|
+
sourceHash: source.sourceHash,
|
|
3528
|
+
outcome,
|
|
3529
|
+
mode: entry.installMode,
|
|
3530
|
+
lockFormat: payload.lockFormat
|
|
3531
|
+
});
|
|
3532
|
+
} finally {
|
|
3533
|
+
cleanupPreparedInstallerArtifacts(preparedInstaller);
|
|
3534
|
+
}
|
|
3535
|
+
} catch (error) {
|
|
3536
|
+
const rollbackSkill = {
|
|
3537
|
+
name: entry.skillName,
|
|
3538
|
+
description: `Rollback for ${entry.skillName}`,
|
|
3539
|
+
path: backupDir
|
|
3540
|
+
};
|
|
3541
|
+
installSkill(rollbackSkill, entry.agents, {
|
|
3542
|
+
mode: entry.installMode,
|
|
3543
|
+
globalInstall: entry.global,
|
|
3544
|
+
cwd: payload.cwd
|
|
3545
|
+
});
|
|
3546
|
+
throw error;
|
|
3547
|
+
} finally {
|
|
3548
|
+
fs13.rmSync(path14.dirname(backupDir), { recursive: true, force: true });
|
|
3549
|
+
if (source.cleanup) {
|
|
3550
|
+
source.cleanup();
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
return { skillName: entry.skillName };
|
|
3554
|
+
}
|
|
3555
|
+
async function runListDetectAgentsTask(cwd, options, emitProgress) {
|
|
3556
|
+
await emitProgress("detecting installed agents");
|
|
3557
|
+
const agents = options.agent ? filterInstalledAgents(resolveAgents(options.agent), cwd) : detectInstalledAgents(cwd);
|
|
3558
|
+
return { agents };
|
|
3559
|
+
}
|
|
3560
|
+
async function runListScanInventoryTask(payload, emitProgress) {
|
|
3561
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3562
|
+
for (const agent of payload.agents) {
|
|
3563
|
+
await emitProgress(`scanning installed skills (${AGENTS[agent].displayName})`);
|
|
3564
|
+
const dir = getAgentSkillsDir(agent, payload.globalInstall, payload.cwd);
|
|
3565
|
+
if (!fs13.existsSync(dir) || !fs13.statSync(dir).isDirectory()) {
|
|
3566
|
+
continue;
|
|
3567
|
+
}
|
|
3568
|
+
for (const entry of fs13.readdirSync(dir, { withFileTypes: true })) {
|
|
3569
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
3570
|
+
continue;
|
|
3571
|
+
}
|
|
3572
|
+
const fullPath = path14.join(dir, entry.name);
|
|
3573
|
+
let resolvedPath = fullPath;
|
|
3574
|
+
try {
|
|
3575
|
+
resolvedPath = fs13.realpathSync(fullPath);
|
|
3576
|
+
} catch {
|
|
3577
|
+
resolvedPath = fullPath;
|
|
3578
|
+
}
|
|
3579
|
+
const key = `${entry.name}:${resolvedPath}`;
|
|
3580
|
+
const existing = grouped.get(key);
|
|
3581
|
+
if (existing) {
|
|
3582
|
+
existing.agents.add(AGENTS[agent].displayName);
|
|
3583
|
+
} else {
|
|
3584
|
+
grouped.set(key, {
|
|
3585
|
+
name: entry.name,
|
|
3586
|
+
resolvedPath,
|
|
3587
|
+
agents: /* @__PURE__ */ new Set([AGENTS[agent].displayName])
|
|
3588
|
+
});
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
const rows = Array.from(grouped.values()).map((row) => ({
|
|
3593
|
+
name: row.name,
|
|
3594
|
+
resolvedPath: row.resolvedPath,
|
|
3595
|
+
agents: Array.from(row.agents).sort((a, b) => a.localeCompare(b))
|
|
3596
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
3597
|
+
return { rows };
|
|
3598
|
+
}
|
|
3599
|
+
async function resolveAddSourceSkills(sourceInput, options, emitProgress) {
|
|
3600
|
+
const parsed = parseSource(sourceInput);
|
|
3601
|
+
if (parsed.type === "well-known" || parsed.type === "catalog") {
|
|
3602
|
+
await emitProgress("fetching skill index");
|
|
3603
|
+
const remoteSkills = parsed.type === "well-known" ? await resolveWellKnownSkills(parsed.url, options) : await resolveCatalogSkills(parsed.url, options);
|
|
3604
|
+
if (remoteSkills.length === 0) {
|
|
3605
|
+
throw new Error("No skills found at remote endpoint");
|
|
3606
|
+
}
|
|
3607
|
+
return {
|
|
3608
|
+
skills: remoteSkills.map((skill) => ({
|
|
3609
|
+
name: skill.installName,
|
|
3610
|
+
description: skill.description
|
|
3611
|
+
}))
|
|
3612
|
+
};
|
|
3613
|
+
}
|
|
3614
|
+
await emitProgress("loading source");
|
|
3615
|
+
const prepared = await prepareSourceDirAsync(
|
|
3616
|
+
parsed
|
|
3617
|
+
);
|
|
3618
|
+
try {
|
|
3619
|
+
const skills = await discoverSkillsAsync(prepared.basePath);
|
|
3620
|
+
return {
|
|
3621
|
+
skills: skills.map((skill) => ({
|
|
3622
|
+
name: skill.name,
|
|
3623
|
+
description: skill.description
|
|
3624
|
+
}))
|
|
3625
|
+
};
|
|
3626
|
+
} finally {
|
|
3627
|
+
if (prepared.cleanup) {
|
|
3628
|
+
prepared.cleanup();
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
async function installSelectedAddSkills(payload, emitProgress) {
|
|
3633
|
+
const parsedSource = parseSource(payload.sourceInput);
|
|
3634
|
+
const globalInstall = resolveAddGlobalInstall(payload.options);
|
|
3635
|
+
const mode = resolveAddInstallMode(payload.options);
|
|
3636
|
+
if (parsedSource.type === "well-known" || parsedSource.type === "catalog") {
|
|
3637
|
+
await emitProgress("fetching selected skills");
|
|
3638
|
+
const remoteSkills = parsedSource.type === "well-known" ? await resolveWellKnownSkills(parsedSource.url, payload.options) : await resolveCatalogSkills(parsedSource.url, payload.options);
|
|
3639
|
+
const remoteByName = new Map(remoteSkills.map((remote) => [remote.installName, remote]));
|
|
3640
|
+
const tempCleanups = [];
|
|
3641
|
+
const stagedSelected = [];
|
|
3642
|
+
const sourceHashesBefore = /* @__PURE__ */ new Map();
|
|
3643
|
+
try {
|
|
3644
|
+
for (const skillName of payload.selectedSkillNames) {
|
|
3645
|
+
const remote = remoteByName.get(skillName);
|
|
3646
|
+
if (!remote) {
|
|
3647
|
+
throw new Error(`No matching well-known skills found in source`);
|
|
3648
|
+
}
|
|
3649
|
+
const staged = buildRemoteSkill(remote);
|
|
3650
|
+
tempCleanups.push(staged.cleanup);
|
|
3651
|
+
stagedSelected.push(staged.skill);
|
|
3652
|
+
sourceHashesBefore.set(staged.skill.path, hashDirectory(staged.skill.path));
|
|
3653
|
+
}
|
|
3654
|
+
const missingInstallerSkillDirs = listSkillsMissingInstallerConfig(
|
|
3655
|
+
stagedSelected.map((item) => item.path)
|
|
3656
|
+
);
|
|
3657
|
+
const scaffoldFormat = resolveAddInstallerScaffoldFormat(
|
|
3658
|
+
payload.options,
|
|
3659
|
+
missingInstallerSkillDirs.length
|
|
3660
|
+
);
|
|
3661
|
+
if (scaffoldFormat) {
|
|
3662
|
+
scaffoldInstallerConfigForSkills(missingInstallerSkillDirs, scaffoldFormat);
|
|
3663
|
+
}
|
|
3664
|
+
for (const localSkill of stagedSelected) {
|
|
3665
|
+
const remote = remoteByName.get(localSkill.name);
|
|
3666
|
+
if (!remote) {
|
|
3667
|
+
throw new Error(
|
|
3668
|
+
`Could not resolve remote metadata for selected skill '${localSkill.name}'`
|
|
3669
|
+
);
|
|
3670
|
+
}
|
|
3671
|
+
const sourceHash = sourceHashesBefore.get(localSkill.path) || hashDirectory(localSkill.path);
|
|
3672
|
+
const sourceCanonical = canonicalSourceIdentity({
|
|
3673
|
+
parsedSource,
|
|
3674
|
+
wellKnownSourceUrl: remote.sourceUrl
|
|
3675
|
+
});
|
|
3676
|
+
await emitProgress(`installing ${localSkill.name}`);
|
|
3677
|
+
const preparedInstaller = await prepareInstallerArtifacts(localSkill.path, payload.cwd, {
|
|
3678
|
+
sourceType: parsedSource.type,
|
|
3679
|
+
policyMode: payload.options.policyMode || "enforce",
|
|
3680
|
+
trustWellKnown: Boolean(payload.options.trustWellKnown)
|
|
3681
|
+
});
|
|
3682
|
+
const outcome = installSkill(localSkill, payload.agents, {
|
|
3683
|
+
mode,
|
|
3684
|
+
globalInstall,
|
|
3685
|
+
cwd: payload.cwd
|
|
3686
|
+
});
|
|
3687
|
+
try {
|
|
3688
|
+
await applyInstallerArtifacts(outcome.canonicalDir, preparedInstaller);
|
|
3689
|
+
writeLockEntryAfterInstall({
|
|
3690
|
+
globalInstall,
|
|
3691
|
+
cwd: payload.cwd,
|
|
3692
|
+
sourceInput: payload.sourceInput,
|
|
3693
|
+
sourceType: parsedSource.type,
|
|
3694
|
+
sourceCanonical,
|
|
3695
|
+
sourcePinnedRef: sourceHash,
|
|
3696
|
+
sourceSkillName: remote.installName,
|
|
3697
|
+
sourceHash,
|
|
3698
|
+
wellKnownSourceUrl: remote.sourceUrl,
|
|
3699
|
+
outcome,
|
|
3700
|
+
mode,
|
|
3701
|
+
lockFormat: payload.options.lockFormat
|
|
3702
|
+
});
|
|
3703
|
+
} finally {
|
|
3704
|
+
cleanupPreparedInstallerArtifacts(preparedInstaller);
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
} finally {
|
|
3708
|
+
for (const cleanup of tempCleanups) {
|
|
3709
|
+
cleanup();
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
return {
|
|
3713
|
+
installedSkillNames: [...payload.selectedSkillNames],
|
|
3714
|
+
agentCount: payload.agents.length
|
|
3715
|
+
};
|
|
3716
|
+
}
|
|
3717
|
+
await emitProgress("loading source");
|
|
3718
|
+
const prepared = await prepareSourceDirAsync(
|
|
3719
|
+
parsedSource
|
|
3720
|
+
);
|
|
3721
|
+
try {
|
|
3722
|
+
const sourceCanonical = canonicalSourceIdentity({ parsedSource });
|
|
3723
|
+
const sourcePinnedRef = parsedSource.type === "github" || parsedSource.type === "git" ? await resolveGitHeadRefAsync(prepared.basePath) : void 0;
|
|
3724
|
+
const sourceIsSymlink = parsedSource.type === "local" ? isLocalSymlinkSource2(parsedSource.localPath) : void 0;
|
|
3725
|
+
const skills = await discoverSkillsAsync(prepared.basePath);
|
|
3726
|
+
const selected = skills.filter((skill) => payload.selectedSkillNames.includes(skill.name));
|
|
3727
|
+
if (selected.length === 0) {
|
|
3728
|
+
throw new Error("No matching skills found in source");
|
|
3729
|
+
}
|
|
3730
|
+
const sourceHashesBefore = new Map(
|
|
3731
|
+
selected.map((skill) => [skill.path, hashDirectory(skill.path)])
|
|
3732
|
+
);
|
|
3733
|
+
const missingInstallerSkillDirs = listSkillsMissingInstallerConfig(
|
|
3734
|
+
selected.map((skill) => skill.path)
|
|
3735
|
+
);
|
|
3736
|
+
const scaffoldFormat = resolveAddInstallerScaffoldFormat(
|
|
3737
|
+
payload.options,
|
|
3738
|
+
missingInstallerSkillDirs.length
|
|
3739
|
+
);
|
|
3740
|
+
if (scaffoldFormat) {
|
|
3741
|
+
scaffoldInstallerConfigForSkills(missingInstallerSkillDirs, scaffoldFormat);
|
|
3742
|
+
}
|
|
3743
|
+
for (const skill of selected) {
|
|
3744
|
+
const sourceResolvedPath = parsedSource.type === "local" ? resolveSafeRealPath2(skill.path) : void 0;
|
|
3745
|
+
await emitProgress(`installing ${skill.name}`);
|
|
3746
|
+
const sourceSkillPath = path14.relative(prepared.basePath, skill.path) || ".";
|
|
3747
|
+
const preparedInstaller = await prepareInstallerArtifacts(skill.path, payload.cwd, {
|
|
3748
|
+
sourceType: parsedSource.type,
|
|
3749
|
+
policyMode: payload.options.policyMode || "enforce",
|
|
3750
|
+
trustWellKnown: Boolean(payload.options.trustWellKnown)
|
|
3751
|
+
});
|
|
3752
|
+
const outcome = installSkill(skill, payload.agents, {
|
|
3753
|
+
mode,
|
|
3754
|
+
globalInstall,
|
|
3755
|
+
cwd: payload.cwd
|
|
3756
|
+
});
|
|
3757
|
+
try {
|
|
3758
|
+
await applyInstallerArtifacts(outcome.canonicalDir, preparedInstaller);
|
|
3759
|
+
writeLockEntryAfterInstall({
|
|
3760
|
+
globalInstall,
|
|
3761
|
+
cwd: payload.cwd,
|
|
3762
|
+
sourceInput: payload.sourceInput,
|
|
3763
|
+
sourceType: parsedSource.type,
|
|
3764
|
+
sourceCanonical,
|
|
3765
|
+
sourcePinnedRef,
|
|
3766
|
+
sourceResolvedPath,
|
|
3767
|
+
sourceIsSymlink,
|
|
3768
|
+
sourceSkillName: skill.name,
|
|
3769
|
+
sourceSkillPath,
|
|
3770
|
+
sourceHash: sourceHashForInstalledSkill({
|
|
3771
|
+
parsedSource,
|
|
3772
|
+
skillPath: skill.path,
|
|
3773
|
+
beforeHash: sourceHashesBefore.get(skill.path)
|
|
3774
|
+
}),
|
|
3775
|
+
outcome,
|
|
3776
|
+
mode,
|
|
3777
|
+
lockFormat: payload.options.lockFormat
|
|
3778
|
+
});
|
|
3779
|
+
} finally {
|
|
3780
|
+
cleanupPreparedInstallerArtifacts(preparedInstaller);
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
return {
|
|
3784
|
+
installedSkillNames: selected.map((skill) => skill.name),
|
|
3785
|
+
agentCount: payload.agents.length
|
|
3786
|
+
};
|
|
3787
|
+
} finally {
|
|
3788
|
+
if (prepared.cleanup) {
|
|
3789
|
+
prepared.cleanup();
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
async function runFindFetchInventoryTask(sourceInput, options, emitProgress) {
|
|
3794
|
+
await emitProgress("parsing source");
|
|
3795
|
+
const parsedSource = parseSource(sourceInput);
|
|
3796
|
+
const sourceLabel = resolveSourceLabel(parsedSource);
|
|
3797
|
+
await emitProgress("fetching skill inventory");
|
|
3798
|
+
if (parsedSource.type === "well-known" || parsedSource.type === "catalog") {
|
|
3799
|
+
if (parsedSource.type === "catalog") {
|
|
3800
|
+
assertExperimentalFeatureEnabled("catalog", Boolean(options.experimental));
|
|
3801
|
+
}
|
|
3802
|
+
const remoteSkills = parsedSource.type === "well-known" ? await resolveWellKnownSkills(parsedSource.url, {
|
|
3803
|
+
allowHost: options.allowHost,
|
|
3804
|
+
denyHost: options.denyHost,
|
|
3805
|
+
maxDownloadBytes: options.maxDownloadBytes,
|
|
3806
|
+
experimental: options.experimental
|
|
3807
|
+
}) : await resolveCatalogSkills(parsedSource.url, {
|
|
3808
|
+
allowHost: options.allowHost,
|
|
3809
|
+
denyHost: options.denyHost,
|
|
3810
|
+
maxDownloadBytes: options.maxDownloadBytes,
|
|
3811
|
+
experimental: options.experimental
|
|
3812
|
+
});
|
|
3813
|
+
return {
|
|
3814
|
+
sourceType: parsedSource.type,
|
|
3815
|
+
sourceLabel,
|
|
3816
|
+
skills: remoteSkills.map((item) => ({
|
|
3817
|
+
name: item.installName,
|
|
3818
|
+
description: item.description
|
|
3819
|
+
}))
|
|
3820
|
+
};
|
|
3821
|
+
}
|
|
3822
|
+
const prepared = await prepareSourceDirAsync(parsedSource);
|
|
3823
|
+
try {
|
|
3824
|
+
const discovered = await discoverSkillsAsync(prepared.basePath);
|
|
3825
|
+
return {
|
|
3826
|
+
sourceType: parsedSource.type,
|
|
3827
|
+
sourceLabel,
|
|
3828
|
+
skills: discovered.map((item) => ({
|
|
3829
|
+
name: item.name,
|
|
3830
|
+
description: item.description
|
|
3831
|
+
}))
|
|
3832
|
+
};
|
|
3833
|
+
} finally {
|
|
3834
|
+
if (prepared.cleanup) {
|
|
3835
|
+
prepared.cleanup();
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
async function runValidateRunTask(options, emitProgress) {
|
|
3840
|
+
return await runValidateAnalysis(options, emitProgress);
|
|
3841
|
+
}
|
|
3842
|
+
function runBlockingTask(payload) {
|
|
3843
|
+
const end = Date.now() + payload.durationMs;
|
|
3844
|
+
while (Date.now() < end) {
|
|
3845
|
+
Math.sqrt(Math.random() * Number.MAX_SAFE_INTEGER);
|
|
3846
|
+
}
|
|
3847
|
+
return { durationMs: payload.durationMs };
|
|
3848
|
+
}
|
|
3849
|
+
async function executeBackgroundTask(request, emitProgress) {
|
|
3850
|
+
switch (request.kind) {
|
|
3851
|
+
case "check.scan":
|
|
3852
|
+
return await runCheckScanTask(request.payload.cwd, request.payload.options, emitProgress);
|
|
3853
|
+
case "update.assess":
|
|
3854
|
+
return await runUpdateAssessTask(request.payload.cwd, request.payload.options, emitProgress);
|
|
3855
|
+
case "update.apply":
|
|
3856
|
+
return await runUpdateApplyTask(request.payload, emitProgress);
|
|
3857
|
+
case "update.migrate":
|
|
3858
|
+
return await runUpdateMigrateTask(request.payload, emitProgress);
|
|
3859
|
+
case "list.detectAgents":
|
|
3860
|
+
return await runListDetectAgentsTask(
|
|
3861
|
+
request.payload.cwd,
|
|
3862
|
+
request.payload.options,
|
|
3863
|
+
emitProgress
|
|
3864
|
+
);
|
|
3865
|
+
case "list.scanInventory":
|
|
3866
|
+
return await runListScanInventoryTask(request.payload, emitProgress);
|
|
3867
|
+
case "add.fetchOrDiscover":
|
|
3868
|
+
return await resolveAddSourceSkills(
|
|
3869
|
+
request.payload.sourceInput,
|
|
3870
|
+
request.payload.options,
|
|
3871
|
+
emitProgress
|
|
3872
|
+
);
|
|
3873
|
+
case "add.install":
|
|
3874
|
+
return await installSelectedAddSkills(request.payload, emitProgress);
|
|
3875
|
+
case "find.fetchInventory":
|
|
3876
|
+
return await runFindFetchInventoryTask(
|
|
3877
|
+
request.payload.sourceInput,
|
|
3878
|
+
request.payload.options,
|
|
3879
|
+
emitProgress
|
|
3880
|
+
);
|
|
3881
|
+
case "validate.run":
|
|
3882
|
+
return await runValidateRunTask(request.payload.options, emitProgress);
|
|
3883
|
+
case "test.blocking":
|
|
3884
|
+
if (request.payload.progressLabel) {
|
|
3885
|
+
await emitProgress(request.payload.progressLabel);
|
|
3886
|
+
}
|
|
3887
|
+
return runBlockingTask(request.payload);
|
|
3888
|
+
default:
|
|
3889
|
+
throw new Error(`Unsupported background task: ${String(request)}`);
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
export {
|
|
3893
|
+
executeBackgroundTask
|
|
3894
|
+
};
|
|
3895
|
+
//# sourceMappingURL=background-executor.js.map
|