@lpm-registry/cli 0.57.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +21 -45
  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 +40 -82
  6. package/CHANGELOG.md +0 -197
  7. package/LICENSE +0 -15
  8. package/index.d.ts +0 -131
  9. package/index.js +0 -31
  10. package/lib/api.js +0 -398
  11. package/lib/commands/add.js +0 -1217
  12. package/lib/commands/audit.js +0 -283
  13. package/lib/commands/cache.js +0 -209
  14. package/lib/commands/check-name.js +0 -112
  15. package/lib/commands/config.js +0 -186
  16. package/lib/commands/doctor.js +0 -140
  17. package/lib/commands/info.js +0 -215
  18. package/lib/commands/init.js +0 -146
  19. package/lib/commands/install.js +0 -264
  20. package/lib/commands/login.js +0 -595
  21. package/lib/commands/logout.js +0 -94
  22. package/lib/commands/marketplace-compare.js +0 -164
  23. package/lib/commands/marketplace-earnings.js +0 -89
  24. package/lib/commands/mcp-setup.js +0 -363
  25. package/lib/commands/npmrc.js +0 -178
  26. package/lib/commands/open.js +0 -82
  27. package/lib/commands/outdated.js +0 -291
  28. package/lib/commands/pool-stats.js +0 -100
  29. package/lib/commands/publish.js +0 -949
  30. package/lib/commands/quality.js +0 -211
  31. package/lib/commands/remove.js +0 -82
  32. package/lib/commands/run.js +0 -14
  33. package/lib/commands/search.js +0 -143
  34. package/lib/commands/setup.js +0 -221
  35. package/lib/commands/skills.js +0 -898
  36. package/lib/commands/token-rotate.js +0 -25
  37. package/lib/commands/uninstall.js +0 -107
  38. package/lib/commands/whoami.js +0 -129
  39. package/lib/config.js +0 -328
  40. package/lib/constants.js +0 -190
  41. package/lib/ecosystem.js +0 -501
  42. package/lib/editor-skills.js +0 -321
  43. package/lib/editors.js +0 -215
  44. package/lib/import-rewriter.js +0 -364
  45. package/lib/install-targets/mcp-server.js +0 -245
  46. package/lib/install-targets/vscode-extension.js +0 -178
  47. package/lib/install-targets.js +0 -82
  48. package/lib/integrity.js +0 -179
  49. package/lib/lpm-config-prompts.js +0 -102
  50. package/lib/lpm-config.js +0 -408
  51. package/lib/oidc.js +0 -166
  52. package/lib/project-utils.js +0 -152
  53. package/lib/quality/checks.js +0 -687
  54. package/lib/quality/display.js +0 -155
  55. package/lib/quality/score.js +0 -118
  56. package/lib/quality/swift-checks.js +0 -447
  57. package/lib/safe-path.js +0 -180
  58. package/lib/secure-store.js +0 -321
  59. package/lib/swift-project.js +0 -637
  60. package/lib/ui.js +0 -40
  61. package/scripts/__tests__/install-binary.test.js +0 -346
  62. package/scripts/install-binary.js +0 -571
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
- }