@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.
- package/README.md +25 -44
- package/bin/lpm.js +2 -354
- package/bin/lpx.js +4 -0
- package/bin/native.js +154 -0
- package/package.json +42 -82
- package/scripts/install-binary.js +219 -550
- package/CHANGELOG.md +0 -197
- package/LICENSE +0 -15
- package/index.d.ts +0 -131
- package/index.js +0 -31
- package/lib/api.js +0 -398
- package/lib/commands/add.js +0 -1217
- package/lib/commands/audit.js +0 -283
- package/lib/commands/cache.js +0 -209
- package/lib/commands/check-name.js +0 -112
- package/lib/commands/config.js +0 -186
- package/lib/commands/doctor.js +0 -140
- package/lib/commands/info.js +0 -215
- package/lib/commands/init.js +0 -146
- package/lib/commands/install.js +0 -264
- package/lib/commands/login.js +0 -595
- package/lib/commands/logout.js +0 -94
- package/lib/commands/marketplace-compare.js +0 -164
- package/lib/commands/marketplace-earnings.js +0 -89
- package/lib/commands/mcp-setup.js +0 -363
- package/lib/commands/npmrc.js +0 -178
- package/lib/commands/open.js +0 -82
- package/lib/commands/outdated.js +0 -291
- package/lib/commands/pool-stats.js +0 -100
- package/lib/commands/publish.js +0 -949
- package/lib/commands/quality.js +0 -211
- package/lib/commands/remove.js +0 -82
- package/lib/commands/run.js +0 -14
- package/lib/commands/search.js +0 -143
- package/lib/commands/setup.js +0 -221
- package/lib/commands/skills.js +0 -898
- package/lib/commands/token-rotate.js +0 -25
- package/lib/commands/uninstall.js +0 -107
- package/lib/commands/whoami.js +0 -129
- package/lib/config.js +0 -328
- package/lib/constants.js +0 -190
- package/lib/ecosystem.js +0 -501
- package/lib/editor-skills.js +0 -321
- package/lib/editors.js +0 -215
- package/lib/import-rewriter.js +0 -364
- package/lib/install-targets/mcp-server.js +0 -245
- package/lib/install-targets/vscode-extension.js +0 -178
- package/lib/install-targets.js +0 -82
- package/lib/integrity.js +0 -179
- package/lib/lpm-config-prompts.js +0 -102
- package/lib/lpm-config.js +0 -408
- package/lib/oidc.js +0 -166
- package/lib/project-utils.js +0 -152
- package/lib/quality/checks.js +0 -687
- package/lib/quality/display.js +0 -155
- package/lib/quality/score.js +0 -118
- package/lib/quality/swift-checks.js +0 -447
- package/lib/safe-path.js +0 -180
- package/lib/secure-store.js +0 -321
- package/lib/swift-project.js +0 -637
- package/lib/ui.js +0 -40
- 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
|
-
}
|