@anytio/pspm 0.13.0 → 0.14.1

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.
@@ -0,0 +1,468 @@
1
+ import { dirname, join } from "node:path";
2
+ import { mkdir, readFile, stat, unlink, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import * as ini from "ini";
5
+ //#region ../../packages/shared/errors/src/extract.ts
6
+ const DEFAULT_STATUS_DESCRIPTIONS = {
7
+ 400: "Bad Request",
8
+ 401: "Unauthorized",
9
+ 403: "Forbidden",
10
+ 404: "Not Found",
11
+ 409: "Conflict",
12
+ 422: "Validation Error",
13
+ 429: "Too Many Requests",
14
+ 500: "Internal Server Error",
15
+ 502: "Bad Gateway",
16
+ 503: "Service Unavailable"
17
+ };
18
+ function getStatusDescription(status, overrides) {
19
+ return overrides?.[status] ?? DEFAULT_STATUS_DESCRIPTIONS[status] ?? `HTTP ${status}`;
20
+ }
21
+ function extractApiErrorMessage$1(response, fallbackMessage, options = {}) {
22
+ const errorData = response.data;
23
+ const overrides = options.statusDescriptions;
24
+ if (options.debug) {
25
+ const log = options.debugLogger ?? console.log;
26
+ log(`[debug] API response status: ${response.status}`);
27
+ log("[debug] API response data:", errorData);
28
+ }
29
+ if (typeof errorData === "string") {
30
+ if (response.status === 404) return `${fallbackMessage}: ${getStatusDescription(404, overrides)}`;
31
+ return `${fallbackMessage}: ${errorData} (HTTP ${response.status})`;
32
+ }
33
+ if (!errorData || typeof errorData !== "object") return `${fallbackMessage}: ${getStatusDescription(response.status, overrides)}`;
34
+ let errorMessage = errorData.message || fallbackMessage;
35
+ if (errorData.code === "VALIDATION_ERROR" && errorData.details) {
36
+ const issues = errorData.details.issues;
37
+ if (issues && Array.isArray(issues)) errorMessage = `Validation failed:\n${issues.map((issue) => {
38
+ return ` - ${issue.path?.join(".") || "input"}: ${issue.message || "invalid value"}`;
39
+ }).join("\n")}`;
40
+ }
41
+ if (errorData.code && !errorMessage.includes(errorData.code)) errorMessage = `[${errorData.code}] ${errorMessage}`;
42
+ if (response.status >= 400) errorMessage += ` (HTTP ${response.status})`;
43
+ if (errorData.requestId) errorMessage += `\n(Request ID: ${errorData.requestId})`;
44
+ return errorMessage;
45
+ }
46
+ //#endregion
47
+ //#region src/errors.ts
48
+ /**
49
+ * Base error class for PSPM configuration errors
50
+ */
51
+ var ConfigError = class extends Error {
52
+ constructor(message) {
53
+ super(message);
54
+ this.name = "ConfigError";
55
+ }
56
+ };
57
+ /**
58
+ * Error thrown when the user is not logged in
59
+ */
60
+ var NotLoggedInError = class extends ConfigError {
61
+ constructor() {
62
+ super("Not logged in. Run 'pspm login --api-key <key>' first, or set PSPM_API_KEY env var.");
63
+ this.name = "NotLoggedInError";
64
+ }
65
+ };
66
+ const CLI_STATUS_DESCRIPTIONS = {
67
+ 400: "Bad Request - The request was malformed",
68
+ 401: "Unauthorized - Please run 'pspm login' first",
69
+ 403: "Forbidden - You don't have permission for this action",
70
+ 404: "Not Found - The endpoint or resource doesn't exist",
71
+ 409: "Conflict - The resource already exists or there's a version conflict",
72
+ 422: "Validation Error - The request data is invalid",
73
+ 429: "Too Many Requests - Please slow down and try again",
74
+ 500: "Internal Server Error - Something went wrong on the server",
75
+ 502: "Bad Gateway - The server is temporarily unavailable",
76
+ 503: "Service Unavailable - The server is temporarily unavailable"
77
+ };
78
+ /**
79
+ * Extract a human-readable error message from an API response.
80
+ * Thin wrapper over `@anytio/errors/extract` with CLI-tuned status hints.
81
+ */
82
+ function extractApiErrorMessage(response, fallbackMessage) {
83
+ return extractApiErrorMessage$1(response, fallbackMessage, {
84
+ statusDescriptions: CLI_STATUS_DESCRIPTIONS,
85
+ debug: Boolean(process.env.PSPM_DEBUG),
86
+ debugLogger: (msg, data) => data === void 0 ? console.log(msg) : console.log(msg, JSON.stringify(data, null, 2))
87
+ });
88
+ }
89
+ //#endregion
90
+ //#region src/config-auth.ts
91
+ /**
92
+ * Get the auth token for a given registry URL.
93
+ * Falls back to the default API key if no registry-specific token is configured.
94
+ *
95
+ * @param config - The resolved configuration
96
+ * @param registryUrl - The registry URL
97
+ * @returns The auth token to use, or undefined if none available
98
+ */
99
+ function getTokenForRegistry(config, registryUrl) {
100
+ try {
101
+ const host = new URL(registryUrl).host;
102
+ if (config.registryTokens[host]) return config.registryTokens[host];
103
+ return config.apiKey;
104
+ } catch {
105
+ return config.apiKey;
106
+ }
107
+ }
108
+ /**
109
+ * Set credentials (authToken and optionally username/registry)
110
+ */
111
+ async function setCredentials(authToken, username, registry) {
112
+ const config = await readUserConfig();
113
+ config.authToken = authToken;
114
+ if (username) config.username = username;
115
+ if (registry && registry !== "https://registry.pspm.dev") config.registry = registry;
116
+ await writeUserConfig(config);
117
+ }
118
+ /**
119
+ * Clear credentials (authToken and username)
120
+ */
121
+ async function clearCredentials() {
122
+ const config = await readUserConfig();
123
+ config.authToken = void 0;
124
+ config.username = void 0;
125
+ await writeUserConfig(config);
126
+ }
127
+ /**
128
+ * Check if user is logged in
129
+ */
130
+ async function isLoggedIn() {
131
+ try {
132
+ return !!(await resolveConfig()).apiKey;
133
+ } catch {
134
+ return false;
135
+ }
136
+ }
137
+ /**
138
+ * Get the API key (throws if not logged in)
139
+ */
140
+ async function requireApiKey() {
141
+ const resolved = await resolveConfig();
142
+ if (!resolved.apiKey) {
143
+ if (process.env.PSPM_DEBUG) console.log("[config] requireApiKey: No API key found");
144
+ throw new NotLoggedInError();
145
+ }
146
+ if (process.env.PSPM_DEBUG) console.log(`[config] requireApiKey: Got API key (${resolved.apiKey.substring(0, 10)}...)`);
147
+ return resolved.apiKey;
148
+ }
149
+ /**
150
+ * Get the registry URL
151
+ */
152
+ async function getRegistryUrl() {
153
+ return (await resolveConfig()).registryUrl;
154
+ }
155
+ /**
156
+ * Get the encryption key for a given scope.
157
+ *
158
+ * Checks environment variable first (PSPM_ENCRYPTION_KEY_{SCOPE}),
159
+ * then falls back to ~/.pspmrc encryption-key:{scope}.
160
+ *
161
+ * @param scope - The scope (e.g., "@user/alice" or "@org/acme")
162
+ * @returns The encryption key, or undefined if not configured
163
+ */
164
+ async function getEncryptionKey(scope) {
165
+ const envSuffix = scope.replace(/^@/, "").replace(/\//g, "_").toUpperCase();
166
+ const envKey = process.env[`PSPM_ENCRYPTION_KEY_${envSuffix}`];
167
+ if (envKey) return envKey;
168
+ return (await readUserConfig()).encryptionKeys?.[scope];
169
+ }
170
+ /**
171
+ * Set the encryption key for a given scope in ~/.pspmrc.
172
+ *
173
+ * @param scope - The scope (e.g., "@user/alice" or "@org/acme")
174
+ * @param key - The encryption passphrase
175
+ */
176
+ async function setEncryptionKey(scope, key) {
177
+ const config = await readUserConfig();
178
+ if (!config.encryptionKeys) config.encryptionKeys = {};
179
+ config.encryptionKeys[scope] = key;
180
+ await writeUserConfig(config);
181
+ }
182
+ /**
183
+ * Remove the encryption key for a given scope from ~/.pspmrc.
184
+ *
185
+ * @param scope - The scope (e.g., "@user/alice" or "@org/acme")
186
+ */
187
+ async function removeEncryptionKey(scope) {
188
+ const config = await readUserConfig();
189
+ if (config.encryptionKeys) {
190
+ delete config.encryptionKeys[scope];
191
+ if (Object.keys(config.encryptionKeys).length === 0) config.encryptionKeys = void 0;
192
+ }
193
+ await writeUserConfig(config);
194
+ }
195
+ /**
196
+ * Derive the encryption scope from a package specifier.
197
+ *
198
+ * @user/alice/my-skill -> @user/alice
199
+ * @org/acme/tool -> @org/acme
200
+ */
201
+ function getEncryptionScope(namespace, owner) {
202
+ return `@${namespace}/${owner}`;
203
+ }
204
+ //#endregion
205
+ //#region src/config.ts
206
+ const DEFAULT_REGISTRY_URL = "https://registry.pspm.dev";
207
+ /**
208
+ * Get the user config file path (~/.pspmrc)
209
+ */
210
+ function getConfigPath() {
211
+ return join(homedir(), ".pspmrc");
212
+ }
213
+ /**
214
+ * Get the legacy config file path (~/.pspm/config.json) for migration
215
+ */
216
+ function getLegacyConfigPath() {
217
+ return join(homedir(), ".pspm", "config.json");
218
+ }
219
+ let _globalMode = false;
220
+ /**
221
+ * Set global mode. When enabled, all path functions return
222
+ * home-directory paths (~/.pspm/) instead of project-relative paths.
223
+ */
224
+ function setGlobalMode(global) {
225
+ _globalMode = global;
226
+ }
227
+ /**
228
+ * Check if global mode is enabled.
229
+ */
230
+ function isGlobalMode() {
231
+ return _globalMode;
232
+ }
233
+ /**
234
+ * Get the .pspm directory path
235
+ * Global: ~/.pspm/
236
+ * Project: ./.pspm/
237
+ */
238
+ function getPspmDir() {
239
+ if (_globalMode) return join(homedir(), ".pspm");
240
+ return join(process.cwd(), ".pspm");
241
+ }
242
+ /**
243
+ * Get the skills directory path
244
+ * Global: ~/.pspm/skills/
245
+ * Project: ./.pspm/skills/
246
+ */
247
+ function getSkillsDir() {
248
+ return join(getPspmDir(), "skills");
249
+ }
250
+ /**
251
+ * Get the cache directory path
252
+ * Global: ~/.pspm/cache/
253
+ * Project: ./.pspm/cache/
254
+ */
255
+ function getCacheDir() {
256
+ return join(getPspmDir(), "cache");
257
+ }
258
+ /**
259
+ * Get the lockfile path
260
+ * Global: ~/.pspm/pspm-lock.json
261
+ * Project: ./pspm-lock.json
262
+ */
263
+ function getLockfilePath() {
264
+ if (_globalMode) return join(homedir(), ".pspm", "pspm-lock.json");
265
+ return join(process.cwd(), "pspm-lock.json");
266
+ }
267
+ /**
268
+ * Get the legacy lockfile path (for migration)
269
+ */
270
+ function getLegacyLockfilePath() {
271
+ return join(process.cwd(), "skill-lock.json");
272
+ }
273
+ /**
274
+ * Get the legacy skills directory path (for migration)
275
+ */
276
+ function getLegacySkillsDir() {
277
+ return join(process.cwd(), ".skills");
278
+ }
279
+ /**
280
+ * Read the user config file (~/.pspmrc, INI format)
281
+ *
282
+ * Supports npm-style configuration:
283
+ * ```ini
284
+ * ; Default registry and auth
285
+ * registry = https://pspm.dev
286
+ * authToken = sk_default
287
+ *
288
+ * ; Scope mappings
289
+ * @myorg:registry = https://corp.pspm.io
290
+ *
291
+ * ; Per-registry tokens
292
+ * //pspm.dev:authToken = sk_public
293
+ * //corp.pspm.io:authToken = sk_corp
294
+ * ```
295
+ */
296
+ async function readUserConfig() {
297
+ const configPath = getConfigPath();
298
+ if (process.env.PSPM_DEBUG) console.log(`[config] Reading config from: ${configPath}`);
299
+ try {
300
+ const content = await readFile(configPath, "utf-8");
301
+ const parsed = ini.parse(content);
302
+ if (process.env.PSPM_DEBUG) console.log("[config] Parsed config:", JSON.stringify(parsed, null, 2));
303
+ const scopedRegistries = {};
304
+ for (const key of Object.keys(parsed)) {
305
+ const scopeMatch = key.match(/^(@[^:]+):registry$/);
306
+ if (scopeMatch) {
307
+ const scope = scopeMatch[1];
308
+ scopedRegistries[scope] = parsed[key];
309
+ }
310
+ }
311
+ const registryTokens = {};
312
+ for (const key of Object.keys(parsed)) {
313
+ const tokenMatch = key.match(/^\/\/([^:]+):authToken$/);
314
+ if (tokenMatch) {
315
+ const host = tokenMatch[1];
316
+ registryTokens[host] = parsed[key];
317
+ }
318
+ if (key.startsWith("//") && typeof parsed[key] === "object") {
319
+ const host = key.slice(2);
320
+ const section = parsed[key];
321
+ if (section.authToken) registryTokens[host] = section.authToken;
322
+ }
323
+ }
324
+ const encryptionKeys = {};
325
+ for (const key of Object.keys(parsed)) {
326
+ const encKeyMatch = key.match(/^encryption-key:(.+)$/);
327
+ if (encKeyMatch) {
328
+ const scope = encKeyMatch[1];
329
+ encryptionKeys[scope] = parsed[key];
330
+ }
331
+ }
332
+ return {
333
+ registry: parsed.registry,
334
+ authToken: parsed.authToken,
335
+ username: parsed.username,
336
+ scopedRegistries: Object.keys(scopedRegistries).length > 0 ? scopedRegistries : void 0,
337
+ registryTokens: Object.keys(registryTokens).length > 0 ? registryTokens : void 0,
338
+ encryptionKeys: Object.keys(encryptionKeys).length > 0 ? encryptionKeys : void 0
339
+ };
340
+ } catch (error) {
341
+ if (process.env.PSPM_DEBUG) console.log(`[config] Error reading config: ${error instanceof Error ? error.message : String(error)}`);
342
+ return {};
343
+ }
344
+ }
345
+ /**
346
+ * Write the user config file (~/.pspmrc, INI format)
347
+ */
348
+ async function writeUserConfig(config) {
349
+ const configPath = getConfigPath();
350
+ const lines = ["; PSPM Configuration", ""];
351
+ if (config.registry) lines.push(`registry = ${config.registry}`);
352
+ if (config.authToken) lines.push(`authToken = ${config.authToken}`);
353
+ if (config.username) lines.push(`username = ${config.username}`);
354
+ if (config.encryptionKeys && Object.keys(config.encryptionKeys).length > 0) {
355
+ lines.push("; Encryption keys (scope -> passphrase)");
356
+ for (const [scope, key] of Object.entries(config.encryptionKeys)) lines.push(`encryption-key:${scope} = ${key}`);
357
+ }
358
+ lines.push("");
359
+ await mkdir(dirname(configPath), { recursive: true });
360
+ await writeFile(configPath, lines.join("\n"));
361
+ if (process.env.PSPM_DEBUG) console.log(`[config] Wrote config to: ${configPath}`);
362
+ }
363
+ /**
364
+ * Find and read project config (.pspmrc) by searching up directory tree
365
+ */
366
+ async function findProjectConfig() {
367
+ let currentDir = process.cwd();
368
+ const root = dirname(currentDir);
369
+ while (currentDir !== root) {
370
+ const configPath = join(currentDir, ".pspmrc");
371
+ try {
372
+ if ((await stat(configPath)).isFile()) {
373
+ const content = await readFile(configPath, "utf-8");
374
+ try {
375
+ const parsed = ini.parse(content);
376
+ if (process.env.PSPM_DEBUG) console.log(`[config] Found project config at ${configPath}:`, JSON.stringify(parsed, null, 2));
377
+ return { registry: parsed.registry };
378
+ } catch {
379
+ try {
380
+ return { registry: JSON.parse(content).registryUrl };
381
+ } catch {}
382
+ }
383
+ }
384
+ } catch {}
385
+ currentDir = dirname(currentDir);
386
+ }
387
+ return null;
388
+ }
389
+ /**
390
+ * Migrate from legacy config format (~/.pspm/config.json) if it exists
391
+ */
392
+ async function migrateFromLegacyConfig() {
393
+ const legacyPath = getLegacyConfigPath();
394
+ try {
395
+ const content = await readFile(legacyPath, "utf-8");
396
+ const parsed = JSON.parse(content);
397
+ let config = {};
398
+ if (parsed.version === 2 && parsed.profiles) {
399
+ const v2Config = parsed;
400
+ const defaultProfileName = v2Config.defaultProfile || "default";
401
+ const profile = v2Config.profiles[defaultProfileName];
402
+ if (profile) config = {
403
+ registry: profile.registryUrl !== "https://registry.pspm.dev" ? profile.registryUrl : void 0,
404
+ authToken: profile.apiKey,
405
+ username: profile.username
406
+ };
407
+ console.log(`Migrating from legacy config (profile: ${defaultProfileName})...`);
408
+ } else {
409
+ const v1Config = parsed;
410
+ config = {
411
+ registry: v1Config.registryUrl !== "https://registry.pspm.dev" ? v1Config.registryUrl : void 0,
412
+ authToken: v1Config.apiKey,
413
+ username: v1Config.username
414
+ };
415
+ console.log("Migrating from legacy config...");
416
+ }
417
+ await writeUserConfig(config);
418
+ console.log(`Created new config at: ${getConfigPath()}`);
419
+ await unlink(legacyPath);
420
+ console.log(`Removed legacy config: ${legacyPath}`);
421
+ return config;
422
+ } catch {
423
+ return null;
424
+ }
425
+ }
426
+ /**
427
+ * Resolve the full configuration using cascade priority:
428
+ * 1. Environment variables (PSPM_REGISTRY_URL, PSPM_API_KEY)
429
+ * 2. Project config (.pspmrc in project directory)
430
+ * 3. User config (~/.pspmrc)
431
+ * 4. Defaults
432
+ */
433
+ async function resolveConfig() {
434
+ const newConfigPath = getConfigPath();
435
+ try {
436
+ await stat(newConfigPath);
437
+ } catch {
438
+ await migrateFromLegacyConfig();
439
+ }
440
+ const userConfig = await readUserConfig();
441
+ const projectConfig = await findProjectConfig();
442
+ let registryUrl = DEFAULT_REGISTRY_URL;
443
+ let apiKey = userConfig.authToken;
444
+ const username = userConfig.username;
445
+ const scopedRegistries = userConfig.scopedRegistries ?? {};
446
+ const registryTokens = userConfig.registryTokens ?? {};
447
+ if (userConfig.registry) registryUrl = userConfig.registry;
448
+ if (projectConfig?.registry) registryUrl = projectConfig.registry;
449
+ if (process.env.PSPM_REGISTRY_URL) registryUrl = process.env.PSPM_REGISTRY_URL;
450
+ if (process.env.PSPM_API_KEY) apiKey = process.env.PSPM_API_KEY;
451
+ if (process.env.PSPM_DEBUG) {
452
+ console.log("[config] Resolved config:");
453
+ console.log(`[config] registryUrl: ${registryUrl}`);
454
+ console.log(`[config] apiKey: ${apiKey ? "***" : "(not set)"}`);
455
+ console.log(`[config] username: ${username || "(not set)"}`);
456
+ console.log(`[config] scopedRegistries: ${JSON.stringify(scopedRegistries)}`);
457
+ console.log(`[config] registryTokens: ${Object.keys(registryTokens).length} configured`);
458
+ }
459
+ return {
460
+ registryUrl,
461
+ apiKey,
462
+ username,
463
+ scopedRegistries,
464
+ registryTokens
465
+ };
466
+ }
467
+ //#endregion
468
+ export { setCredentials as C, requireApiKey as S, extractApiErrorMessage as T, getEncryptionScope as _, getLegacyLockfilePath as a, isLoggedIn as b, getPspmDir as c, readUserConfig as d, resolveConfig as f, getEncryptionKey as g, clearCredentials as h, getConfigPath as i, getSkillsDir as l, writeUserConfig as m, findProjectConfig as n, getLegacySkillsDir as o, setGlobalMode as p, getCacheDir as r, getLockfilePath as s, DEFAULT_REGISTRY_URL as t, isGlobalMode as u, getRegistryUrl as v, setEncryptionKey as w, removeEncryptionKey as x, getTokenForRegistry as y };
@@ -0,0 +1,2 @@
1
+ import { f as resolveConfig, g as getEncryptionKey, p as setGlobalMode, w as setEncryptionKey, x as removeEncryptionKey } from "./config-BQy_Rjip.js";
2
+ export { getEncryptionKey, removeEncryptionKey, resolveConfig, setEncryptionKey, setGlobalMode };