@anytio/pspm 0.12.0 → 0.14.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/CHANGELOG.md +26 -2
- package/CLI_GUIDE.md +248 -8
- package/README.md +4 -2
- package/dist/add-CcgUlOLa.js +755 -0
- package/dist/add-CcgUlOLa.js.map +1 -0
- package/dist/add-Cnn-OR9g.js +2 -0
- package/dist/api-client-CBTk37gh.js +2 -0
- package/dist/api-client-DBXUpGoX.js +452 -0
- package/dist/api-client-DBXUpGoX.js.map +1 -0
- package/dist/config-BQy_Rjip.js +470 -0
- package/dist/config-BQy_Rjip.js.map +1 -0
- package/dist/config-BZJ6_GsC.js +2 -0
- package/dist/index.js +2782 -6826
- package/dist/index.js.map +1 -1
- package/dist/install-gcvbBeWi.js +2 -0
- package/dist/install-lNvqIk5c.js +479 -0
- package/dist/install-lNvqIk5c.js.map +1 -0
- package/dist/symlinks-BTw8X0GG.js +1834 -0
- package/dist/symlinks-BTw8X0GG.js.map +1 -0
- package/package.json +14 -12
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
import { a as getGithubSkillVersion, g as listSkillVersions, m as getSkillVersion, n as configure, o as listGithubSkillVersions } from "./api-client-DBXUpGoX.js";
|
|
2
|
+
import { T as extractApiErrorMessage, f as resolveConfig, l as getSkillsDir, p as setGlobalMode, u as isGlobalMode, y as getTokenForRegistry } from "./config-BQy_Rjip.js";
|
|
3
|
+
import { C as downloadGitHubPackage, D as addGitHubDependency, E as addDependency, G as isGitHubSpecifier, H as generateRegistryIdentifier, J as parseGitHubShorthand, K as isGitHubUrl, N as readManifest, O as addLocalDependency, Q as resolveRecursive, R as parseAgentArg, T as getGitHubDisplayName, U as getGitHubSkillName, V as formatGitHubSpecifier, W as isGitHubShorthand, X as parseGitHubUrl, Y as parseGitHubSpecifier, Z as parseRegistrySpecifier, a as getRegistrySkillPath, c as addGitHubToLockfile, d as addToLockfileWithDeps, et as resolveVersion, f as addWellKnownToLockfile, i as getLocalSkillPath, k as addWellKnownDependency, l as addLocalToLockfile, n as getGitHubSkillPath, nt as printResolutionErrors, o as getWellKnownSkillPath, ot as calculateIntegrity, q as isRegistrySpecifier, t as createAgentSymlinks, w as extractGitHubPackage, z as promptForAgents } from "./symlinks-BTw8X0GG.js";
|
|
4
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
|
+
import { mkdir, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
//#region src/wellknown.ts
|
|
8
|
+
/**
|
|
9
|
+
* Well-Known Skills Discovery (RFC 8615)
|
|
10
|
+
*
|
|
11
|
+
* Fetches skills from any HTTPS domain that serves a
|
|
12
|
+
* /.well-known/skills/index.json endpoint.
|
|
13
|
+
*/
|
|
14
|
+
const WELL_KNOWN_PATH = ".well-known/skills";
|
|
15
|
+
const INDEX_FILE = "index.json";
|
|
16
|
+
const SKILL_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
17
|
+
/** Hosts that should NOT be treated as well-known (they have dedicated providers) */
|
|
18
|
+
const EXCLUDED_HOSTS = [
|
|
19
|
+
"github.com",
|
|
20
|
+
"gitlab.com",
|
|
21
|
+
"raw.githubusercontent.com"
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Check if a string looks like a well-known skills URL.
|
|
25
|
+
* Must be HTTP(S) and not a known git host.
|
|
26
|
+
*/
|
|
27
|
+
function isWellKnownSpecifier(input) {
|
|
28
|
+
if (!input.startsWith("http://") && !input.startsWith("https://")) return false;
|
|
29
|
+
try {
|
|
30
|
+
const parsed = new URL(input);
|
|
31
|
+
if (EXCLUDED_HOSTS.includes(parsed.hostname)) return false;
|
|
32
|
+
if (input.endsWith(".git")) return false;
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Extract hostname from a well-known URL (strips www. prefix).
|
|
40
|
+
*/
|
|
41
|
+
function getWellKnownHostname(url) {
|
|
42
|
+
try {
|
|
43
|
+
return new URL(url).hostname.replace(/^www\./, "");
|
|
44
|
+
} catch {
|
|
45
|
+
return url;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Validate a single skill entry from the index.
|
|
50
|
+
*/
|
|
51
|
+
function isValidSkillEntry(entry) {
|
|
52
|
+
if (!entry || typeof entry !== "object") return false;
|
|
53
|
+
const e = entry;
|
|
54
|
+
if (typeof e.name !== "string" || e.name.length === 0) return false;
|
|
55
|
+
if (typeof e.description !== "string" || e.description.length === 0) return false;
|
|
56
|
+
if (!SKILL_NAME_PATTERN.test(e.name)) return false;
|
|
57
|
+
if (!Array.isArray(e.files) || e.files.length === 0) return false;
|
|
58
|
+
let hasSkillMd = false;
|
|
59
|
+
for (const file of e.files) {
|
|
60
|
+
if (typeof file !== "string") return false;
|
|
61
|
+
if (file.startsWith("/") || file.startsWith("\\")) return false;
|
|
62
|
+
if (file.includes("..")) return false;
|
|
63
|
+
if (file.toLowerCase() === "skill.md") hasSkillMd = true;
|
|
64
|
+
}
|
|
65
|
+
if (!hasSkillMd) return false;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Validate a well-known index structure.
|
|
70
|
+
*/
|
|
71
|
+
function isValidIndex(data) {
|
|
72
|
+
if (!data || typeof data !== "object") return false;
|
|
73
|
+
const d = data;
|
|
74
|
+
if (!Array.isArray(d.skills)) return false;
|
|
75
|
+
return d.skills.every(isValidSkillEntry);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Fetch the well-known index from a URL.
|
|
79
|
+
*
|
|
80
|
+
* Tries path-relative first, then root:
|
|
81
|
+
* 1. {baseUrl}/.well-known/skills/index.json
|
|
82
|
+
* 2. {protocol}://{host}/.well-known/skills/index.json
|
|
83
|
+
*/
|
|
84
|
+
async function fetchWellKnownIndex(baseUrl) {
|
|
85
|
+
const parsed = new URL(baseUrl);
|
|
86
|
+
const pathRelativeUrl = `${baseUrl.replace(/\/$/, "")}/${WELL_KNOWN_PATH}/${INDEX_FILE}`;
|
|
87
|
+
const pathRelativeBase = `${baseUrl.replace(/\/$/, "")}/${WELL_KNOWN_PATH}`;
|
|
88
|
+
try {
|
|
89
|
+
const response = await fetch(pathRelativeUrl, { signal: AbortSignal.timeout(1e4) });
|
|
90
|
+
if (response.ok) {
|
|
91
|
+
const data = await response.json();
|
|
92
|
+
if (isValidIndex(data)) return {
|
|
93
|
+
index: data,
|
|
94
|
+
resolvedBaseUrl: pathRelativeBase
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
} catch {}
|
|
98
|
+
const rootUrl = `${parsed.protocol}//${parsed.host}/${WELL_KNOWN_PATH}/${INDEX_FILE}`;
|
|
99
|
+
const rootBase = `${parsed.protocol}//${parsed.host}/${WELL_KNOWN_PATH}`;
|
|
100
|
+
if (rootUrl !== pathRelativeUrl) try {
|
|
101
|
+
const response = await fetch(rootUrl, { signal: AbortSignal.timeout(1e4) });
|
|
102
|
+
if (response.ok) {
|
|
103
|
+
const data = await response.json();
|
|
104
|
+
if (isValidIndex(data)) return {
|
|
105
|
+
index: data,
|
|
106
|
+
resolvedBaseUrl: rootBase
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
} catch {}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Fetch a single skill's files from a well-known endpoint.
|
|
114
|
+
*/
|
|
115
|
+
async function fetchSkillFiles(baseUrl, entry) {
|
|
116
|
+
const skillBaseUrl = `${baseUrl}/${entry.name}`;
|
|
117
|
+
const files = /* @__PURE__ */ new Map();
|
|
118
|
+
let skillMdContent = "";
|
|
119
|
+
const results = await Promise.allSettled(entry.files.map(async (filePath) => {
|
|
120
|
+
const fileUrl = `${skillBaseUrl}/${filePath}`;
|
|
121
|
+
const response = await fetch(fileUrl, { signal: AbortSignal.timeout(1e4) });
|
|
122
|
+
if (!response.ok) throw new Error(`Failed to fetch ${fileUrl}: ${response.status}`);
|
|
123
|
+
return {
|
|
124
|
+
filePath,
|
|
125
|
+
content: await response.text()
|
|
126
|
+
};
|
|
127
|
+
}));
|
|
128
|
+
for (const result of results) if (result.status === "fulfilled") {
|
|
129
|
+
files.set(result.value.filePath, result.value.content);
|
|
130
|
+
if (result.value.filePath.toLowerCase() === "skill.md") skillMdContent = result.value.content;
|
|
131
|
+
}
|
|
132
|
+
if (!skillMdContent) return null;
|
|
133
|
+
return {
|
|
134
|
+
name: entry.name,
|
|
135
|
+
description: entry.description,
|
|
136
|
+
content: skillMdContent,
|
|
137
|
+
files,
|
|
138
|
+
sourceUrl: `${skillBaseUrl}/SKILL.md`,
|
|
139
|
+
indexEntry: entry
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Fetch all skills from a well-known endpoint.
|
|
144
|
+
*/
|
|
145
|
+
async function fetchWellKnownSkills(url) {
|
|
146
|
+
const result = await fetchWellKnownIndex(url);
|
|
147
|
+
if (!result) return null;
|
|
148
|
+
const { index, resolvedBaseUrl } = result;
|
|
149
|
+
const hostname = getWellKnownHostname(url);
|
|
150
|
+
const skillResults = await Promise.allSettled(index.skills.map((entry) => fetchSkillFiles(resolvedBaseUrl, entry)));
|
|
151
|
+
const skills = [];
|
|
152
|
+
for (const r of skillResults) if (r.status === "fulfilled" && r.value) skills.push(r.value);
|
|
153
|
+
if (skills.length === 0) return null;
|
|
154
|
+
return {
|
|
155
|
+
skills,
|
|
156
|
+
resolvedBaseUrl,
|
|
157
|
+
hostname
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Extract a well-known skill to the .pspm/skills/_wellknown/ directory.
|
|
162
|
+
*
|
|
163
|
+
* @param skill - The fetched skill
|
|
164
|
+
* @param hostname - Source hostname (for directory namespacing)
|
|
165
|
+
* @param skillsDir - Base skills directory (.pspm/skills)
|
|
166
|
+
* @returns Path to extracted skill (relative to project root)
|
|
167
|
+
*/
|
|
168
|
+
async function extractWellKnownSkill(skill, hostname, skillsDir) {
|
|
169
|
+
const destPath = join(skillsDir, "_wellknown", hostname, skill.name);
|
|
170
|
+
await rm(destPath, {
|
|
171
|
+
recursive: true,
|
|
172
|
+
force: true
|
|
173
|
+
});
|
|
174
|
+
await mkdir(destPath, { recursive: true });
|
|
175
|
+
for (const [filePath, content] of skill.files) {
|
|
176
|
+
const fullPath = join(destPath, filePath);
|
|
177
|
+
if (!fullPath.startsWith(destPath)) continue;
|
|
178
|
+
const { dirname } = await import("node:path");
|
|
179
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
180
|
+
await writeFile(fullPath, content, "utf-8");
|
|
181
|
+
}
|
|
182
|
+
return `.pspm/skills/_wellknown/${hostname}/${skill.name}`;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Calculate integrity hash for a well-known skill (hash of all file contents).
|
|
186
|
+
*/
|
|
187
|
+
function calculateWellKnownIntegrity(skill) {
|
|
188
|
+
const combined = [...skill.files.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([path, content]) => `${path}:${content}`).join("\n");
|
|
189
|
+
return calculateIntegrity(Buffer.from(combined, "utf-8"));
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Get display name for a well-known skill.
|
|
193
|
+
*/
|
|
194
|
+
function getWellKnownDisplayName(hostname, skillName) {
|
|
195
|
+
return `${hostname}/${skillName} (well-known)`;
|
|
196
|
+
}
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/commands/add-helpers.ts
|
|
199
|
+
/**
|
|
200
|
+
* Check if a specifier is a local file reference.
|
|
201
|
+
*/
|
|
202
|
+
function isLocalSpecifier(specifier) {
|
|
203
|
+
return specifier.startsWith("file:") || specifier.startsWith("./") || specifier.startsWith("../");
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Parse a local specifier and return the path.
|
|
207
|
+
*/
|
|
208
|
+
function parseLocalPath(specifier) {
|
|
209
|
+
if (specifier.startsWith("file:")) return specifier.slice(5);
|
|
210
|
+
return specifier;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Normalize a path to a file: specifier.
|
|
214
|
+
*/
|
|
215
|
+
function normalizeToFileSpecifier(path) {
|
|
216
|
+
if (path.startsWith("file:")) return path;
|
|
217
|
+
return `file:${path}`;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Get the root directory for symlink creation.
|
|
221
|
+
* Global: home directory. Project: current working directory.
|
|
222
|
+
*/
|
|
223
|
+
function getSymlinkRoot() {
|
|
224
|
+
return isGlobalMode() ? homedir() : process.cwd();
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Parse any GitHub input format into a GitHubSpecifier.
|
|
228
|
+
*
|
|
229
|
+
* Accepts:
|
|
230
|
+
* - github:owner/repo[/path][@ref] (canonical prefix format)
|
|
231
|
+
* - https://github.com/owner/repo (full URL)
|
|
232
|
+
* - https://github.com/owner/repo/tree/branch/path (tree URL)
|
|
233
|
+
* - owner/repo[/path] (shorthand)
|
|
234
|
+
*/
|
|
235
|
+
function parseAnyGitHubFormat(specifier) {
|
|
236
|
+
if (isGitHubSpecifier(specifier)) return parseGitHubSpecifier(specifier);
|
|
237
|
+
if (isGitHubUrl(specifier)) return parseGitHubUrl(specifier);
|
|
238
|
+
if (isGitHubShorthand(specifier)) return parseGitHubShorthand(specifier);
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/commands/add-github.ts
|
|
243
|
+
/**
|
|
244
|
+
* Validate and download a GitHub package.
|
|
245
|
+
*/
|
|
246
|
+
async function validateGitHubPackage(specifier) {
|
|
247
|
+
const parsed = parseAnyGitHubFormat(specifier);
|
|
248
|
+
if (!parsed) throw new Error(`Invalid GitHub specifier "${specifier}". Supported formats:\n github:owner/repo[/path][@ref]\n https://github.com/owner/repo[/tree/branch/path]\n owner/repo[/path]`);
|
|
249
|
+
const ref = parsed.ref || "HEAD";
|
|
250
|
+
console.log(`Resolving ${getGitHubDisplayName(parsed)}...`);
|
|
251
|
+
const result = await downloadGitHubPackage(parsed);
|
|
252
|
+
console.log(`Resolved ${specifier} (${ref}@${result.commit.slice(0, 7)})`);
|
|
253
|
+
return {
|
|
254
|
+
type: "github",
|
|
255
|
+
specifier,
|
|
256
|
+
parsed,
|
|
257
|
+
ref,
|
|
258
|
+
downloadResult: result
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Install a pre-validated GitHub package.
|
|
263
|
+
*/
|
|
264
|
+
async function installGitHubPackage(resolved, options) {
|
|
265
|
+
const { specifier, parsed, ref, downloadResult } = resolved;
|
|
266
|
+
console.log(`Installing ${specifier} (${ref}@${downloadResult.commit.slice(0, 7)})...`);
|
|
267
|
+
const skillsDir = getSkillsDir();
|
|
268
|
+
const destPath = await extractGitHubPackage(parsed, downloadResult.buffer, skillsDir);
|
|
269
|
+
const lockfileSpecifier = formatGitHubSpecifier({
|
|
270
|
+
owner: parsed.owner,
|
|
271
|
+
repo: parsed.repo,
|
|
272
|
+
path: parsed.path
|
|
273
|
+
});
|
|
274
|
+
await addGitHubToLockfile(lockfileSpecifier, {
|
|
275
|
+
version: downloadResult.commit.slice(0, 7),
|
|
276
|
+
resolved: `https://github.com/${parsed.owner}/${parsed.repo}`,
|
|
277
|
+
integrity: downloadResult.integrity,
|
|
278
|
+
gitCommit: downloadResult.commit,
|
|
279
|
+
gitRef: ref
|
|
280
|
+
});
|
|
281
|
+
await addGitHubDependency(lockfileSpecifier, ref);
|
|
282
|
+
const agents = options.resolvedAgents;
|
|
283
|
+
if (agents[0] !== "none") {
|
|
284
|
+
const manifest = await readManifest();
|
|
285
|
+
await createAgentSymlinks([{
|
|
286
|
+
name: getGitHubSkillName(parsed),
|
|
287
|
+
sourcePath: getGitHubSkillPath(parsed.owner, parsed.repo, parsed.path)
|
|
288
|
+
}], {
|
|
289
|
+
agents,
|
|
290
|
+
projectRoot: getSymlinkRoot(),
|
|
291
|
+
agentConfigs: manifest?.agents,
|
|
292
|
+
global: isGlobalMode()
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
console.log(`Installed ${specifier} (${ref}@${downloadResult.commit.slice(0, 7)})`);
|
|
296
|
+
console.log(`Location: ${destPath}`);
|
|
297
|
+
}
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/commands/add-local.ts
|
|
300
|
+
/**
|
|
301
|
+
* Validate a local package path exists and contains a valid skill.
|
|
302
|
+
*/
|
|
303
|
+
async function validateLocalPackage(specifier) {
|
|
304
|
+
const path = parseLocalPath(specifier);
|
|
305
|
+
const resolvedPath = resolve(process.cwd(), path);
|
|
306
|
+
const normalizedSpecifier = normalizeToFileSpecifier(path);
|
|
307
|
+
console.log(`Resolving ${specifier}...`);
|
|
308
|
+
try {
|
|
309
|
+
if (!(await stat(resolvedPath)).isDirectory()) throw new Error(`Path is not a directory: ${resolvedPath}`);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
if (error.code === "ENOENT") throw new Error(`Directory not found: ${resolvedPath}\n Check that the path exists and is accessible.`);
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
let hasSkillMd = false;
|
|
315
|
+
let hasPspmJson = false;
|
|
316
|
+
try {
|
|
317
|
+
await stat(join(resolvedPath, "SKILL.md"));
|
|
318
|
+
hasSkillMd = true;
|
|
319
|
+
} catch {}
|
|
320
|
+
try {
|
|
321
|
+
await stat(join(resolvedPath, "pspm.json"));
|
|
322
|
+
hasPspmJson = true;
|
|
323
|
+
} catch {}
|
|
324
|
+
if (!hasSkillMd && !hasPspmJson) throw new Error(`Not a valid skill directory: ${resolvedPath}\n Missing both SKILL.md and pspm.json. At least one is required.`);
|
|
325
|
+
const name = basename(resolvedPath);
|
|
326
|
+
console.log(`Resolved ${specifier} -> ${resolvedPath}`);
|
|
327
|
+
return {
|
|
328
|
+
type: "local",
|
|
329
|
+
specifier,
|
|
330
|
+
normalizedSpecifier,
|
|
331
|
+
path,
|
|
332
|
+
resolvedPath,
|
|
333
|
+
name
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Install a local package by creating a symlink.
|
|
338
|
+
*/
|
|
339
|
+
async function installLocalPackage(resolved, options) {
|
|
340
|
+
const { specifier, normalizedSpecifier, path, resolvedPath, name } = resolved;
|
|
341
|
+
console.log(`Installing ${specifier}...`);
|
|
342
|
+
const localSkillsDir = join(getSkillsDir(), "_local");
|
|
343
|
+
await mkdir(localSkillsDir, { recursive: true });
|
|
344
|
+
const symlinkPath = join(localSkillsDir, name);
|
|
345
|
+
const relativeTarget = relative(dirname(symlinkPath), resolvedPath);
|
|
346
|
+
try {
|
|
347
|
+
await rm(symlinkPath, { force: true });
|
|
348
|
+
} catch {}
|
|
349
|
+
await symlink(relativeTarget, symlinkPath);
|
|
350
|
+
await addLocalToLockfile(normalizedSpecifier, {
|
|
351
|
+
version: "local",
|
|
352
|
+
path,
|
|
353
|
+
resolvedPath,
|
|
354
|
+
name
|
|
355
|
+
});
|
|
356
|
+
await addLocalDependency(normalizedSpecifier);
|
|
357
|
+
const agents = options.resolvedAgents;
|
|
358
|
+
if (agents[0] !== "none") {
|
|
359
|
+
const manifest = await readManifest();
|
|
360
|
+
await createAgentSymlinks([{
|
|
361
|
+
name,
|
|
362
|
+
sourcePath: getLocalSkillPath(name)
|
|
363
|
+
}], {
|
|
364
|
+
agents,
|
|
365
|
+
projectRoot: getSymlinkRoot(),
|
|
366
|
+
agentConfigs: manifest?.agents,
|
|
367
|
+
global: isGlobalMode()
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
console.log(`Installed ${specifier} (local)`);
|
|
371
|
+
console.log(`Location: ${symlinkPath} -> ${resolvedPath}`);
|
|
372
|
+
}
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region src/commands/add-registry.ts
|
|
375
|
+
/**
|
|
376
|
+
* Validate and resolve a registry package (without downloading).
|
|
377
|
+
*/
|
|
378
|
+
async function validateRegistryPackage(specifier) {
|
|
379
|
+
const config = await resolveConfig();
|
|
380
|
+
const registryUrl = config.registryUrl;
|
|
381
|
+
const apiKey = getTokenForRegistry(config, registryUrl);
|
|
382
|
+
const parsed = parseRegistrySpecifier(specifier);
|
|
383
|
+
if (!parsed) throw new Error(`Invalid skill specifier "${specifier}". Use format: @user/{username}/{name}[@{version}] or @org/{orgname}/{name}[@{version}]`);
|
|
384
|
+
const { namespace, owner, name, subname, versionRange } = parsed;
|
|
385
|
+
const fullName = generateRegistryIdentifier({
|
|
386
|
+
namespace,
|
|
387
|
+
owner,
|
|
388
|
+
name,
|
|
389
|
+
subname
|
|
390
|
+
});
|
|
391
|
+
configure({
|
|
392
|
+
registryUrl,
|
|
393
|
+
apiKey
|
|
394
|
+
});
|
|
395
|
+
console.log(`Resolving ${specifier}...`);
|
|
396
|
+
const versionStrings = (await fetchRegistryVersions(namespace, owner, name, subname, fullName, apiKey)).map((v) => v.version);
|
|
397
|
+
const resolvedVersion = resolveVersion(versionRange || "*", versionStrings);
|
|
398
|
+
if (!resolvedVersion) throw new Error(`No version matching "${versionRange || "latest"}" found for ${fullName}. Available versions: ${versionStrings.join(", ")}`);
|
|
399
|
+
const { downloadUrl, checksum } = await fetchRegistryVersionInfo(namespace, owner, name, subname, resolvedVersion, fullName);
|
|
400
|
+
console.log(`Resolved ${fullName}@${resolvedVersion}`);
|
|
401
|
+
return {
|
|
402
|
+
type: "registry",
|
|
403
|
+
specifier,
|
|
404
|
+
namespace,
|
|
405
|
+
owner,
|
|
406
|
+
name,
|
|
407
|
+
subname,
|
|
408
|
+
versionRange,
|
|
409
|
+
resolvedVersion,
|
|
410
|
+
versionInfo: {
|
|
411
|
+
downloadUrl,
|
|
412
|
+
checksum
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
async function fetchRegistryVersions(namespace, owner, name, subname, fullName, apiKey) {
|
|
417
|
+
if (namespace === "github" && subname) {
|
|
418
|
+
const versionsResponse = await listGithubSkillVersions(owner, name, subname);
|
|
419
|
+
if (versionsResponse.status !== 200 || !versionsResponse.data) {
|
|
420
|
+
if (versionsResponse.status === 401) throw new Error(apiKey ? `Access denied to ${fullName}. You may not have permission to access this private package.` : `Package ${fullName} requires authentication. Please run 'pspm login' to authenticate`);
|
|
421
|
+
throw new Error(versionsResponse.error || `Skill ${fullName} not found`);
|
|
422
|
+
}
|
|
423
|
+
return versionsResponse.data;
|
|
424
|
+
}
|
|
425
|
+
const versionsResponse = await listSkillVersions(owner, name);
|
|
426
|
+
if (versionsResponse.status !== 200) {
|
|
427
|
+
if (versionsResponse.status === 401) {
|
|
428
|
+
if (!apiKey) throw new Error(`Package ${fullName} requires authentication. Please run 'pspm login' to authenticate`);
|
|
429
|
+
throw new Error(`Access denied to ${fullName}. You may not have permission to access this private package.`);
|
|
430
|
+
}
|
|
431
|
+
const errorMessage = extractApiErrorMessage(versionsResponse, `Skill ${fullName} not found`);
|
|
432
|
+
throw new Error(errorMessage);
|
|
433
|
+
}
|
|
434
|
+
if (versionsResponse.data.length === 0) throw new Error(`Skill ${fullName} not found`);
|
|
435
|
+
return versionsResponse.data;
|
|
436
|
+
}
|
|
437
|
+
async function fetchRegistryVersionInfo(namespace, owner, name, subname, resolvedVersion, fullName) {
|
|
438
|
+
if (namespace === "github" && subname) {
|
|
439
|
+
const versionResponse = await getGithubSkillVersion(owner, name, subname, resolvedVersion);
|
|
440
|
+
if (versionResponse.status !== 200 || !versionResponse.data) throw new Error(`Version ${resolvedVersion} not found for ${fullName}`);
|
|
441
|
+
return {
|
|
442
|
+
downloadUrl: versionResponse.data.downloadUrl,
|
|
443
|
+
checksum: versionResponse.data.checksum
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const versionResponse = await getSkillVersion(owner, name, resolvedVersion);
|
|
447
|
+
if (versionResponse.status !== 200 || !versionResponse.data) {
|
|
448
|
+
const errorMessage = extractApiErrorMessage(versionResponse, `Version ${resolvedVersion} not found`);
|
|
449
|
+
throw new Error(errorMessage);
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
downloadUrl: versionResponse.data.downloadUrl,
|
|
453
|
+
checksum: versionResponse.data.checksum
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Install a package from a DependencyNode (resolved from resolver).
|
|
458
|
+
*/
|
|
459
|
+
async function installFromNode(node, options) {
|
|
460
|
+
const parsed = parseRegistrySpecifier(node.name);
|
|
461
|
+
if (!parsed) throw new Error(`Invalid package name: ${node.name}`);
|
|
462
|
+
const { namespace, owner, name, subname } = parsed;
|
|
463
|
+
console.log(`Installing ${node.name}@${node.version}...`);
|
|
464
|
+
const config = await resolveConfig();
|
|
465
|
+
const tarballBuffer = await downloadNodeTarball(node, getTokenForRegistry(config, config.registryUrl));
|
|
466
|
+
const integrity = calculateIntegrity(tarballBuffer);
|
|
467
|
+
if (integrity !== node.integrity) throw new Error("Checksum verification failed");
|
|
468
|
+
const skillsDir = getSkillsDir();
|
|
469
|
+
const effectiveSkillName = subname ?? name;
|
|
470
|
+
const destDir = resolveRegistryDestDir(namespace, owner, name, subname, effectiveSkillName, skillsDir);
|
|
471
|
+
await extractRegistryTarball(destDir, tarballBuffer);
|
|
472
|
+
const resolvedDeps = {};
|
|
473
|
+
for (const [depName, range] of Object.entries(node.dependencies)) resolvedDeps[depName] = range;
|
|
474
|
+
await addToLockfileWithDeps(node.name, {
|
|
475
|
+
version: node.version,
|
|
476
|
+
resolved: node.downloadUrl,
|
|
477
|
+
integrity,
|
|
478
|
+
deprecated: node.deprecated
|
|
479
|
+
}, Object.keys(resolvedDeps).length > 0 ? resolvedDeps : void 0);
|
|
480
|
+
if (options.isDirect) {
|
|
481
|
+
const dependencyRange = node.versionRange || `^${node.version}`;
|
|
482
|
+
await addDependency(node.name, dependencyRange);
|
|
483
|
+
}
|
|
484
|
+
const agents = options.resolvedAgents;
|
|
485
|
+
if (agents[0] !== "none") {
|
|
486
|
+
const skillManifest = await readManifest();
|
|
487
|
+
await createAgentSymlinks([{
|
|
488
|
+
name: effectiveSkillName,
|
|
489
|
+
sourcePath: getRegistrySkillPath(namespace, owner, namespace === "github" && subname ? `${name}/${subname}` : name)
|
|
490
|
+
}], {
|
|
491
|
+
agents,
|
|
492
|
+
projectRoot: getSymlinkRoot(),
|
|
493
|
+
agentConfigs: skillManifest?.agents,
|
|
494
|
+
global: isGlobalMode()
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
console.log(`Installed ${node.name}@${node.version}`);
|
|
498
|
+
console.log(`Location: ${destDir}`);
|
|
499
|
+
}
|
|
500
|
+
async function downloadNodeTarball(node, apiKey) {
|
|
501
|
+
const isPresignedUrl = node.downloadUrl.includes(".r2.cloudflarestorage.com") || node.downloadUrl.includes("X-Amz-Signature");
|
|
502
|
+
const downloadHeaders = {};
|
|
503
|
+
if (!isPresignedUrl && apiKey) downloadHeaders.Authorization = `Bearer ${apiKey}`;
|
|
504
|
+
const tarballResponse = await fetch(node.downloadUrl, {
|
|
505
|
+
headers: downloadHeaders,
|
|
506
|
+
redirect: "follow"
|
|
507
|
+
});
|
|
508
|
+
if (!tarballResponse.ok) throw new Error(`Failed to download tarball (${tarballResponse.status})`);
|
|
509
|
+
return Buffer.from(await tarballResponse.arrayBuffer());
|
|
510
|
+
}
|
|
511
|
+
function resolveRegistryDestDir(namespace, owner, name, subname, effectiveSkillName, skillsDir) {
|
|
512
|
+
if (namespace === "org") return join(skillsDir, "_org", owner, effectiveSkillName);
|
|
513
|
+
if (namespace === "github" && subname) return join(skillsDir, "_github-registry", owner, name, subname);
|
|
514
|
+
return join(skillsDir, owner, effectiveSkillName);
|
|
515
|
+
}
|
|
516
|
+
async function extractRegistryTarball(destDir, tarballBuffer) {
|
|
517
|
+
await rm(destDir, {
|
|
518
|
+
recursive: true,
|
|
519
|
+
force: true
|
|
520
|
+
});
|
|
521
|
+
await mkdir(destDir, { recursive: true });
|
|
522
|
+
const { writeFile } = await import("node:fs/promises");
|
|
523
|
+
const tempFile = join(destDir, ".temp.tgz");
|
|
524
|
+
await writeFile(tempFile, tarballBuffer);
|
|
525
|
+
const { exec } = await import("node:child_process");
|
|
526
|
+
const { promisify } = await import("node:util");
|
|
527
|
+
const execAsync = promisify(exec);
|
|
528
|
+
try {
|
|
529
|
+
await execAsync(`tar -xzf "${tempFile}" -C "${destDir}" --strip-components=1`);
|
|
530
|
+
} finally {
|
|
531
|
+
await rm(tempFile, { force: true });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
//#endregion
|
|
535
|
+
//#region src/commands/add-wellknown.ts
|
|
536
|
+
/**
|
|
537
|
+
* Validate and fetch skills from a well-known endpoint.
|
|
538
|
+
*/
|
|
539
|
+
async function validateWellKnownPackage(specifier) {
|
|
540
|
+
const hostname = getWellKnownHostname(specifier);
|
|
541
|
+
console.log(`Discovering skills from ${hostname}...`);
|
|
542
|
+
const result = await fetchWellKnownSkills(specifier);
|
|
543
|
+
if (!result) throw new Error(`No well-known skills found at ${specifier}\n Expected: ${specifier}/.well-known/skills/index.json`);
|
|
544
|
+
console.log(`Found ${result.skills.length} skill(s) from ${hostname}: ${result.skills.map((s) => s.name).join(", ")}`);
|
|
545
|
+
return {
|
|
546
|
+
type: "wellknown",
|
|
547
|
+
specifier,
|
|
548
|
+
hostname: result.hostname,
|
|
549
|
+
skills: result.skills,
|
|
550
|
+
resolvedBaseUrl: result.resolvedBaseUrl
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Install pre-validated well-known skills.
|
|
555
|
+
*/
|
|
556
|
+
async function installWellKnownPackage(resolved, options) {
|
|
557
|
+
const { specifier, hostname, skills } = resolved;
|
|
558
|
+
const skillsDir = getSkillsDir();
|
|
559
|
+
for (const skill of skills) {
|
|
560
|
+
console.log(`Installing ${getWellKnownDisplayName(hostname, skill.name)}...`);
|
|
561
|
+
const destPath = await extractWellKnownSkill(skill, hostname, skillsDir);
|
|
562
|
+
const integrity = calculateWellKnownIntegrity(skill);
|
|
563
|
+
await addWellKnownToLockfile(`${specifier}#${skill.name}`, {
|
|
564
|
+
version: "well-known",
|
|
565
|
+
resolved: skill.sourceUrl,
|
|
566
|
+
integrity,
|
|
567
|
+
hostname,
|
|
568
|
+
name: skill.name,
|
|
569
|
+
files: [...skill.files.keys()]
|
|
570
|
+
});
|
|
571
|
+
await addWellKnownDependency(specifier, [skill.name]);
|
|
572
|
+
const agents = options.resolvedAgents;
|
|
573
|
+
if (agents[0] !== "none") {
|
|
574
|
+
const manifest = await readManifest();
|
|
575
|
+
await createAgentSymlinks([{
|
|
576
|
+
name: skill.name,
|
|
577
|
+
sourcePath: getWellKnownSkillPath(hostname, skill.name)
|
|
578
|
+
}], {
|
|
579
|
+
agents,
|
|
580
|
+
projectRoot: process.cwd(),
|
|
581
|
+
agentConfigs: manifest?.agents
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
console.log(`Installed ${getWellKnownDisplayName(hostname, skill.name)}`);
|
|
585
|
+
console.log(`Location: ${destPath}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
//#endregion
|
|
589
|
+
//#region src/commands/add.ts
|
|
590
|
+
async function add(specifiers, options) {
|
|
591
|
+
if (options.global) {
|
|
592
|
+
setGlobalMode(true);
|
|
593
|
+
console.log("Installing globally to ~/.pspm/\n");
|
|
594
|
+
}
|
|
595
|
+
console.log("Resolving packages...\n");
|
|
596
|
+
const { resolvedPackages, validationErrors } = await validateAllPackages(specifiers);
|
|
597
|
+
if (resolvedPackages.length === 0) {
|
|
598
|
+
console.error("No packages could be resolved.");
|
|
599
|
+
process.exit(1);
|
|
600
|
+
}
|
|
601
|
+
if (validationErrors.length > 0) console.log(`Resolved ${resolvedPackages.length} of ${specifiers.length} packages.\n`);
|
|
602
|
+
const config = await resolveConfig();
|
|
603
|
+
const apiKey = getTokenForRegistry(config, config.registryUrl);
|
|
604
|
+
const registryPackages = resolvedPackages.filter((p) => p.type === "registry");
|
|
605
|
+
const githubPackages = resolvedPackages.filter((p) => p.type === "github");
|
|
606
|
+
const localPackages = resolvedPackages.filter((p) => p.type === "local");
|
|
607
|
+
const wellKnownPackages = resolvedPackages.filter((p) => p.type === "wellknown");
|
|
608
|
+
let resolutionResult = null;
|
|
609
|
+
if (registryPackages.length > 0) {
|
|
610
|
+
const rootDeps = {};
|
|
611
|
+
for (const pkg of registryPackages) {
|
|
612
|
+
const fullName = generateRegistryIdentifier({
|
|
613
|
+
namespace: pkg.namespace,
|
|
614
|
+
owner: pkg.owner,
|
|
615
|
+
name: pkg.name,
|
|
616
|
+
subname: pkg.subname
|
|
617
|
+
});
|
|
618
|
+
rootDeps[fullName] = pkg.versionRange || `^${pkg.resolvedVersion}`;
|
|
619
|
+
}
|
|
620
|
+
console.log("Resolving dependencies...");
|
|
621
|
+
resolutionResult = await resolveRecursive(rootDeps, {
|
|
622
|
+
maxDepth: 5,
|
|
623
|
+
registryUrl: config.registryUrl,
|
|
624
|
+
apiKey
|
|
625
|
+
});
|
|
626
|
+
if (!resolutionResult.success) {
|
|
627
|
+
printResolutionErrors(resolutionResult.graph.errors, resolutionResult.graph.conflicts);
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
const transitiveDeps = resolutionResult.installOrder.filter((name) => !rootDeps[name]);
|
|
631
|
+
if (transitiveDeps.length > 0) console.log(`Resolved ${transitiveDeps.length} transitive dependencies.\n`);
|
|
632
|
+
else console.log();
|
|
633
|
+
}
|
|
634
|
+
const agents = await resolveAgents(options);
|
|
635
|
+
const results = [];
|
|
636
|
+
if (resolutionResult) for (const name of resolutionResult.installOrder) {
|
|
637
|
+
const node = resolutionResult.graph.nodes.get(name);
|
|
638
|
+
if (!node) continue;
|
|
639
|
+
try {
|
|
640
|
+
await installFromNode(node, {
|
|
641
|
+
...options,
|
|
642
|
+
resolvedAgents: agents,
|
|
643
|
+
isDirect: node.isDirect
|
|
644
|
+
});
|
|
645
|
+
results.push({
|
|
646
|
+
specifier: name,
|
|
647
|
+
success: true
|
|
648
|
+
});
|
|
649
|
+
} catch (error) {
|
|
650
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
651
|
+
results.push({
|
|
652
|
+
specifier: name,
|
|
653
|
+
success: false,
|
|
654
|
+
error: message
|
|
655
|
+
});
|
|
656
|
+
console.error(`Failed to install ${name}: ${message}\n`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
for (const resolved of githubPackages) try {
|
|
660
|
+
await installGitHubPackage(resolved, {
|
|
661
|
+
...options,
|
|
662
|
+
resolvedAgents: agents
|
|
663
|
+
});
|
|
664
|
+
results.push({
|
|
665
|
+
specifier: resolved.specifier,
|
|
666
|
+
success: true
|
|
667
|
+
});
|
|
668
|
+
} catch (error) {
|
|
669
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
670
|
+
results.push({
|
|
671
|
+
specifier: resolved.specifier,
|
|
672
|
+
success: false,
|
|
673
|
+
error: message
|
|
674
|
+
});
|
|
675
|
+
console.error(`Failed to install ${resolved.specifier}: ${message}\n`);
|
|
676
|
+
}
|
|
677
|
+
for (const resolved of localPackages) try {
|
|
678
|
+
await installLocalPackage(resolved, {
|
|
679
|
+
...options,
|
|
680
|
+
resolvedAgents: agents
|
|
681
|
+
});
|
|
682
|
+
results.push({
|
|
683
|
+
specifier: resolved.specifier,
|
|
684
|
+
success: true
|
|
685
|
+
});
|
|
686
|
+
} catch (error) {
|
|
687
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
688
|
+
results.push({
|
|
689
|
+
specifier: resolved.specifier,
|
|
690
|
+
success: false,
|
|
691
|
+
error: message
|
|
692
|
+
});
|
|
693
|
+
console.error(`Failed to install ${resolved.specifier}: ${message}\n`);
|
|
694
|
+
}
|
|
695
|
+
for (const resolved of wellKnownPackages) try {
|
|
696
|
+
await installWellKnownPackage(resolved, {
|
|
697
|
+
...options,
|
|
698
|
+
resolvedAgents: agents
|
|
699
|
+
});
|
|
700
|
+
results.push({
|
|
701
|
+
specifier: resolved.specifier,
|
|
702
|
+
success: true
|
|
703
|
+
});
|
|
704
|
+
} catch (error) {
|
|
705
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
706
|
+
results.push({
|
|
707
|
+
specifier: resolved.specifier,
|
|
708
|
+
success: false,
|
|
709
|
+
error: message
|
|
710
|
+
});
|
|
711
|
+
console.error(`Failed to install ${resolved.specifier}: ${message}\n`);
|
|
712
|
+
}
|
|
713
|
+
if (specifiers.length > 1) {
|
|
714
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
715
|
+
const failed = results.filter((r) => !r.success).length + validationErrors.length;
|
|
716
|
+
console.log(`\nSummary: ${succeeded} added, ${failed} failed`);
|
|
717
|
+
if (failed > 0) process.exit(1);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async function validateAllPackages(specifiers) {
|
|
721
|
+
const resolvedPackages = [];
|
|
722
|
+
const validationErrors = [];
|
|
723
|
+
for (const specifier of specifiers) try {
|
|
724
|
+
if (isLocalSpecifier(specifier)) resolvedPackages.push(await validateLocalPackage(specifier));
|
|
725
|
+
else if (isGitHubSpecifier(specifier) || isGitHubUrl(specifier) || isGitHubShorthand(specifier)) resolvedPackages.push(await validateGitHubPackage(specifier));
|
|
726
|
+
else if (isWellKnownSpecifier(specifier)) resolvedPackages.push(await validateWellKnownPackage(specifier));
|
|
727
|
+
else if (isRegistrySpecifier(specifier)) resolvedPackages.push(await validateRegistryPackage(specifier));
|
|
728
|
+
else throw new Error(`Unknown specifier format "${specifier}". Supported formats:\n @user/{username}/{name}[@version] (registry)\n @org/{orgname}/{name}[@version] (organization)\n github:owner/repo[/path][@ref] (github)\n file:./path/to/skill (local)\n owner/repo (github shorthand)`);
|
|
729
|
+
} catch (error) {
|
|
730
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
731
|
+
validationErrors.push({
|
|
732
|
+
specifier,
|
|
733
|
+
error: message
|
|
734
|
+
});
|
|
735
|
+
console.error(`Failed to resolve ${specifier}: ${message}\n`);
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
resolvedPackages,
|
|
739
|
+
validationErrors
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
async function resolveAgents(options) {
|
|
743
|
+
const manifest = await readManifest();
|
|
744
|
+
if (options.agent) return parseAgentArg(options.agent);
|
|
745
|
+
if (manifest) return parseAgentArg(void 0);
|
|
746
|
+
if (options.yes) return parseAgentArg(void 0);
|
|
747
|
+
console.log("No pspm.json found. Let's set up your project.\n");
|
|
748
|
+
const agents = await promptForAgents();
|
|
749
|
+
console.log();
|
|
750
|
+
return agents;
|
|
751
|
+
}
|
|
752
|
+
//#endregion
|
|
753
|
+
export { add as t };
|
|
754
|
+
|
|
755
|
+
//# sourceMappingURL=add-CcgUlOLa.js.map
|