@lpm-registry/cli 0.58.0 → 0.59.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.
Files changed (62) hide show
  1. package/README.md +25 -44
  2. package/bin/lpm.js +2 -354
  3. package/bin/lpx.js +4 -0
  4. package/bin/native.js +154 -0
  5. package/package.json +42 -82
  6. package/scripts/install-binary.js +219 -550
  7. package/CHANGELOG.md +0 -197
  8. package/LICENSE +0 -15
  9. package/index.d.ts +0 -131
  10. package/index.js +0 -31
  11. package/lib/api.js +0 -398
  12. package/lib/commands/add.js +0 -1217
  13. package/lib/commands/audit.js +0 -283
  14. package/lib/commands/cache.js +0 -209
  15. package/lib/commands/check-name.js +0 -112
  16. package/lib/commands/config.js +0 -186
  17. package/lib/commands/doctor.js +0 -140
  18. package/lib/commands/info.js +0 -215
  19. package/lib/commands/init.js +0 -146
  20. package/lib/commands/install.js +0 -264
  21. package/lib/commands/login.js +0 -595
  22. package/lib/commands/logout.js +0 -94
  23. package/lib/commands/marketplace-compare.js +0 -164
  24. package/lib/commands/marketplace-earnings.js +0 -89
  25. package/lib/commands/mcp-setup.js +0 -363
  26. package/lib/commands/npmrc.js +0 -178
  27. package/lib/commands/open.js +0 -82
  28. package/lib/commands/outdated.js +0 -291
  29. package/lib/commands/pool-stats.js +0 -100
  30. package/lib/commands/publish.js +0 -949
  31. package/lib/commands/quality.js +0 -211
  32. package/lib/commands/remove.js +0 -82
  33. package/lib/commands/run.js +0 -14
  34. package/lib/commands/search.js +0 -143
  35. package/lib/commands/setup.js +0 -221
  36. package/lib/commands/skills.js +0 -898
  37. package/lib/commands/token-rotate.js +0 -25
  38. package/lib/commands/uninstall.js +0 -107
  39. package/lib/commands/whoami.js +0 -129
  40. package/lib/config.js +0 -328
  41. package/lib/constants.js +0 -190
  42. package/lib/ecosystem.js +0 -501
  43. package/lib/editor-skills.js +0 -321
  44. package/lib/editors.js +0 -215
  45. package/lib/import-rewriter.js +0 -364
  46. package/lib/install-targets/mcp-server.js +0 -245
  47. package/lib/install-targets/vscode-extension.js +0 -178
  48. package/lib/install-targets.js +0 -82
  49. package/lib/integrity.js +0 -179
  50. package/lib/lpm-config-prompts.js +0 -102
  51. package/lib/lpm-config.js +0 -408
  52. package/lib/oidc.js +0 -166
  53. package/lib/project-utils.js +0 -152
  54. package/lib/quality/checks.js +0 -687
  55. package/lib/quality/display.js +0 -155
  56. package/lib/quality/score.js +0 -118
  57. package/lib/quality/swift-checks.js +0 -447
  58. package/lib/safe-path.js +0 -180
  59. package/lib/secure-store.js +0 -321
  60. package/lib/swift-project.js +0 -637
  61. package/lib/ui.js +0 -40
  62. package/scripts/__tests__/install-binary.test.js +0 -346
package/lib/api.js DELETED
@@ -1,398 +0,0 @@
1
- /**
2
- * API Client with Retry, Timeout, and Rate Limit Handling
3
- *
4
- * Provides robust network communication with:
5
- * - Configurable timeouts using AbortController
6
- * - Exponential backoff retry for transient failures
7
- * - Rate limit handling with Retry-After header support
8
- *
9
- * @module cli/lib/api
10
- */
11
-
12
- import {
13
- getRefreshToken,
14
- getRegistryUrl,
15
- getRetries,
16
- getTimeout,
17
- getToken,
18
- setRefreshToken,
19
- setToken,
20
- } from "./config.js"
21
- import {
22
- ERROR_MESSAGES,
23
- RATE_LIMIT_STATUS_CODES,
24
- RETRY_BACKOFF_MULTIPLIER,
25
- RETRY_BASE_DELAY_MS,
26
- RETRY_MAX_DELAY_MS,
27
- RETRYABLE_STATUS_CODES,
28
- } from "./constants.js"
29
-
30
- /**
31
- * Sleep for a specified duration.
32
- * @param {number} ms - Milliseconds to sleep
33
- * @returns {Promise<void>}
34
- */
35
- function sleep(ms) {
36
- return new Promise(resolve => setTimeout(resolve, ms))
37
- }
38
-
39
- /**
40
- * Calculate exponential backoff delay.
41
- * @param {number} attempt - Current attempt number (0-based)
42
- * @returns {number} - Delay in milliseconds
43
- */
44
- function getBackoffDelay(attempt) {
45
- const delay = RETRY_BASE_DELAY_MS * RETRY_BACKOFF_MULTIPLIER ** attempt
46
- // Add jitter (±10%)
47
- const jitter = delay * 0.1 * (Math.random() * 2 - 1)
48
- return Math.min(delay + jitter, RETRY_MAX_DELAY_MS)
49
- }
50
-
51
- /**
52
- * Parse Retry-After header value.
53
- * @param {string | null} retryAfter - Header value (seconds or HTTP date)
54
- * @returns {number} - Delay in milliseconds
55
- */
56
- function parseRetryAfter(retryAfter) {
57
- if (!retryAfter) return RETRY_BASE_DELAY_MS
58
-
59
- // Try parsing as seconds
60
- const seconds = parseInt(retryAfter, 10)
61
- if (!Number.isNaN(seconds)) {
62
- return seconds * 1000
63
- }
64
-
65
- // Try parsing as HTTP date
66
- const date = new Date(retryAfter)
67
- if (!Number.isNaN(date.getTime())) {
68
- return Math.max(0, date.getTime() - Date.now())
69
- }
70
-
71
- return RETRY_BASE_DELAY_MS
72
- }
73
-
74
- /**
75
- * Attempt silent token refresh using the stored refresh token (Feature 44 Part B).
76
- * @returns {Promise<string|null>} New access token if successful, null otherwise
77
- */
78
- async function tryRefresh() {
79
- try {
80
- const refreshToken = await getRefreshToken()
81
- if (!refreshToken) return null
82
-
83
- const registryUrl = getRegistryUrl()
84
- const { createHash } = await import("node:crypto")
85
- const { hostname, userInfo } = await import("node:os")
86
- const fp = createHash("sha256")
87
- .update(`${hostname()}:${userInfo().username}:lpm-cli`)
88
- .digest("hex")
89
-
90
- const resp = await fetch(`${registryUrl}/api/cli/refresh`, {
91
- method: "POST",
92
- headers: { "Content-Type": "application/json" },
93
- body: JSON.stringify({ refreshToken, deviceFingerprint: fp }),
94
- })
95
-
96
- if (!resp.ok) return null
97
-
98
- const data = await resp.json()
99
- if (data.token) {
100
- await setToken(data.token)
101
- if (data.refreshToken) {
102
- await setRefreshToken(data.refreshToken)
103
- }
104
- return data.token
105
- }
106
- } catch {
107
- // Refresh failed — fall through to auth error
108
- }
109
- return null
110
- }
111
-
112
- /**
113
- * @typedef {Object} RequestOptions
114
- * @property {Record<string, string>} [headers] - Request headers
115
- * @property {string} [method] - HTTP method
116
- * @property {string | Buffer} [body] - Request body
117
- * @property {boolean} [skipRetry] - Skip retry logic for this request
118
- * @property {number} [timeout] - Override default timeout
119
- * @property {(attempt: number, maxRetries: number) => void} [onRetry] - Callback when retrying
120
- * @property {(seconds: number) => void} [onRateLimited] - Callback when rate limited
121
- */
122
-
123
- /**
124
- * Make an API request with timeout, retry, and rate limit handling.
125
- *
126
- * @param {string} path - API path (will be prefixed with options.prefix or /api/registry)
127
- * @param {RequestOptions & { prefix?: string }} [options={}] - Request options
128
- * @returns {Promise<import('node-fetch').Response>}
129
- * @throws {Error} On network failure, timeout, or authentication error
130
- */
131
- export async function request(path, options = {}) {
132
- const token = await getToken()
133
- const registryUrl = getRegistryUrl()
134
- const prefix = options.prefix ?? "/api/registry"
135
- const url = `${registryUrl}${prefix}${path}`
136
- const maxRetries = options.skipRetry ? 0 : getRetries()
137
- const timeout = options.timeout ?? getTimeout()
138
-
139
- const headers = {
140
- ...options.headers,
141
- }
142
-
143
- if (token) {
144
- headers.Authorization = `Bearer ${token}`
145
- }
146
-
147
- let lastError = null
148
-
149
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
150
- const controller = new AbortController()
151
- const timeoutId = setTimeout(() => controller.abort(), timeout)
152
-
153
- try {
154
- const response = await fetch(url, {
155
- ...options,
156
- headers,
157
- signal: controller.signal,
158
- })
159
-
160
- clearTimeout(timeoutId)
161
-
162
- // Handle authentication errors — try silent refresh before failing
163
- // Exception: 2FA OTP responses must be returned to the caller
164
- if (response.status === 401) {
165
- const cloned = response.clone()
166
- let data = {}
167
- try {
168
- data = await cloned.json()
169
- } catch {
170
- // Not JSON
171
- }
172
- if (data.code === "OTP_REQUIRED" || data.code === "OTP_INVALID") {
173
- return response
174
- }
175
-
176
- // Attempt silent refresh (Feature 44 Part B) — one retry on 401
177
- if (attempt === 0) {
178
- const refreshed = await tryRefresh()
179
- if (refreshed) {
180
- // Update the auth header and retry this request
181
- headers.Authorization = `Bearer ${refreshed}`
182
- continue
183
- }
184
- }
185
- throw new Error(ERROR_MESSAGES.notAuthenticated)
186
- }
187
-
188
- if (response.status === 403) {
189
- const cloned403 = response.clone()
190
- const data = await cloned403.json().catch(() => ({}))
191
-
192
- // 2FA enforcement responses must be returned to the caller
193
- if (data.code === "ORG_2FA_REQUIRED") {
194
- return response
195
- }
196
-
197
- const errorMessage = data.error || "Access denied."
198
-
199
- // Check for license/purchase required error
200
- const purchaseMatch = errorMessage.match(
201
- /Package (@[^/]+\/[^\s]+) requires purchase/,
202
- )
203
- if (purchaseMatch) {
204
- const pkgName = purchaseMatch[1]
205
- const registryUrl = getRegistryUrl()
206
- throw new Error(
207
- `${errorMessage}\n Purchase: ${registryUrl}/${pkgName.replace("@", "")}`,
208
- )
209
- }
210
-
211
- throw new Error(errorMessage)
212
- }
213
-
214
- // Handle rate limiting
215
- if (RATE_LIMIT_STATUS_CODES.includes(response.status)) {
216
- const retryAfter = response.headers.get("Retry-After")
217
- const delayMs = parseRetryAfter(retryAfter)
218
- const delaySec = Math.ceil(delayMs / 1000)
219
-
220
- if (options.onRateLimited) {
221
- options.onRateLimited(delaySec)
222
- }
223
-
224
- // Only retry if we have attempts left
225
- if (attempt < maxRetries) {
226
- await sleep(delayMs)
227
- continue
228
- }
229
-
230
- throw new Error(ERROR_MESSAGES.rateLimited)
231
- }
232
-
233
- // Handle retryable server errors
234
- if (
235
- RETRYABLE_STATUS_CODES.includes(response.status) &&
236
- attempt < maxRetries
237
- ) {
238
- const delay = getBackoffDelay(attempt)
239
-
240
- if (options.onRetry) {
241
- options.onRetry(attempt + 1, maxRetries)
242
- }
243
-
244
- await sleep(delay)
245
- continue
246
- }
247
-
248
- return response
249
- } catch (error) {
250
- clearTimeout(timeoutId)
251
-
252
- // Handle abort (timeout)
253
- if (error.name === "AbortError") {
254
- lastError = new Error(ERROR_MESSAGES.timeout)
255
-
256
- if (attempt < maxRetries) {
257
- const delay = getBackoffDelay(attempt)
258
- if (options.onRetry) {
259
- options.onRetry(attempt + 1, maxRetries)
260
- }
261
- await sleep(delay)
262
- continue
263
- }
264
-
265
- throw lastError
266
- }
267
-
268
- // Handle network errors
269
- if (
270
- error.code === "ECONNREFUSED" ||
271
- error.code === "ENOTFOUND" ||
272
- error.type === "system"
273
- ) {
274
- lastError = new Error(ERROR_MESSAGES.networkError)
275
-
276
- if (attempt < maxRetries) {
277
- const delay = getBackoffDelay(attempt)
278
- if (options.onRetry) {
279
- options.onRetry(attempt + 1, maxRetries)
280
- }
281
- await sleep(delay)
282
- continue
283
- }
284
-
285
- throw lastError
286
- }
287
-
288
- // Re-throw other errors (like auth errors)
289
- throw error
290
- }
291
- }
292
-
293
- // Should not reach here, but just in case
294
- throw lastError || new Error(ERROR_MESSAGES.networkError)
295
- }
296
-
297
- /**
298
- * Make a GET request.
299
- * @param {string} path - API path
300
- * @param {RequestOptions} [options={}] - Request options
301
- * @returns {Promise<import('node-fetch').Response>}
302
- */
303
- export function get(path, options = {}) {
304
- return request(path, { ...options, method: "GET" })
305
- }
306
-
307
- /**
308
- * Make a GET request to the search API (/api/search prefix).
309
- * @param {string} path - Path after /api/search (e.g., '/packages?q=test')
310
- * @param {RequestOptions} [options={}] - Request options
311
- * @returns {Promise<import('node-fetch').Response>}
312
- */
313
- export function searchGet(path, options = {}) {
314
- return request(path, { ...options, method: "GET", prefix: "/api/search" })
315
- }
316
-
317
- /**
318
- * Make a POST request with JSON body.
319
- * @param {string} path - API path
320
- * @param {unknown} data - Request body (will be JSON serialized)
321
- * @param {RequestOptions} [options={}] - Request options
322
- * @returns {Promise<import('node-fetch').Response>}
323
- */
324
- export function post(path, data, options = {}) {
325
- return request(path, {
326
- ...options,
327
- method: "POST",
328
- headers: {
329
- "Content-Type": "application/json",
330
- ...options.headers,
331
- },
332
- body: JSON.stringify(data),
333
- })
334
- }
335
-
336
- /**
337
- * Make a PUT request with JSON body.
338
- * @param {string} path - API path
339
- * @param {unknown} data - Request body (will be JSON serialized)
340
- * @param {RequestOptions} [options={}] - Request options
341
- * @returns {Promise<import('node-fetch').Response>}
342
- */
343
- export function put(path, data, options = {}) {
344
- return request(path, {
345
- ...options,
346
- method: "PUT",
347
- headers: {
348
- "Content-Type": "application/json",
349
- ...options.headers,
350
- },
351
- body: JSON.stringify(data),
352
- })
353
- }
354
-
355
- /**
356
- * Check token validity and scopes.
357
- * @returns {Promise<{valid: boolean, scopes?: string[], user?: string, error?: string}>}
358
- */
359
- export async function checkToken() {
360
- try {
361
- const response = await get("/cli/check", { skipRetry: true })
362
-
363
- if (!response.ok) {
364
- const data = await response.json().catch(() => ({}))
365
- return { valid: false, error: data.error || "Token validation failed" }
366
- }
367
-
368
- const data = await response.json()
369
- return { valid: true, ...data }
370
- } catch (error) {
371
- return { valid: false, error: error.message }
372
- }
373
- }
374
-
375
- /**
376
- * Verify token has required scope before an operation.
377
- * @param {string} requiredScope - The scope to check for
378
- * @returns {Promise<{valid: boolean, error?: string}>}
379
- */
380
- export async function verifyTokenScope(requiredScope) {
381
- const result = await checkToken()
382
-
383
- if (!result.valid) {
384
- return { valid: false, error: result.error }
385
- }
386
-
387
- const scopes = result.scopes || []
388
- const hasScope = scopes.includes(requiredScope) || scopes.includes("full")
389
-
390
- if (!hasScope) {
391
- return {
392
- valid: false,
393
- error: ERROR_MESSAGES.tokenMissingScope(requiredScope),
394
- }
395
- }
396
-
397
- return { valid: true }
398
- }