@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/lpm-config.js DELETED
@@ -1,408 +0,0 @@
1
- /**
2
- * LPM Package Config System
3
- *
4
- * Handles lpm.config.json parsing, validation, condition evaluation,
5
- * file filtering, and dependency resolution
6
- * for the `lpm add` command.
7
- *
8
- * Pure logic module — no I/O beyond file reads, no prompts.
9
- *
10
- * @module cli/lib/lpm-config
11
- */
12
-
13
- import fs from "node:fs"
14
- import path from "node:path"
15
-
16
- /** Maximum lpm.config.json file size (1 MB) */
17
- const MAX_CONFIG_FILE_SIZE = 1024 * 1024
18
-
19
- /** Maximum number of file rules allowed */
20
- const MAX_FILE_RULES = 1000
21
-
22
- /**
23
- * Parse a package reference that may include version and URL query params.
24
- *
25
- * Supports:
26
- * @lpm.dev/owner.pkg
27
- * @lpm.dev/owner.pkg@1.0.0
28
- * @lpm.dev/owner.pkg?component=dialog&styling=panda
29
- * @lpm.dev/owner.pkg@1.0.0?component=dialog,button&styling=panda
30
- *
31
- * @param {string} ref - Package reference string
32
- * @returns {{ name: string, version: string, inlineConfig: Record<string, string>, providedParams: Set<string> }}
33
- */
34
- export function parseLpmPackageReference(ref) {
35
- // Split query string first
36
- const queryIndex = ref.indexOf("?")
37
- let packagePart = ref
38
- let queryString = ""
39
-
40
- if (queryIndex !== -1) {
41
- packagePart = ref.substring(0, queryIndex)
42
- queryString = ref.substring(queryIndex + 1)
43
- }
44
-
45
- // Extract version from package part
46
- let name = packagePart
47
- let version = "latest"
48
-
49
- // Find version separator: last @ after position 0 (position 0 is the scope @)
50
- const lastAt = packagePart.lastIndexOf("@")
51
- if (lastAt > 0) {
52
- name = packagePart.substring(0, lastAt)
53
- version = packagePart.substring(lastAt + 1)
54
- }
55
-
56
- // Parse query params
57
- const inlineConfig = {}
58
- const providedParams = new Set()
59
-
60
- if (queryString) {
61
- const params = new URLSearchParams(queryString)
62
- for (const [key, value] of params.entries()) {
63
- inlineConfig[key] = value
64
- providedParams.add(key)
65
- }
66
- }
67
-
68
- return { name, version, inlineConfig, providedParams }
69
- }
70
-
71
- /**
72
- * Read and validate lpm.config.json from an extracted package directory.
73
- *
74
- * @param {string} extractDir - Path to extracted tarball directory
75
- * @returns {import('./lpm-config.js').LpmConfig | null} Parsed config or null if not found
76
- */
77
- export function readLpmConfig(extractDir) {
78
- const configPath = path.join(extractDir, "lpm.config.json")
79
-
80
- if (!fs.existsSync(configPath)) {
81
- return null
82
- }
83
-
84
- const stat = fs.statSync(configPath)
85
- if (stat.size > MAX_CONFIG_FILE_SIZE) {
86
- throw new Error(
87
- `lpm.config.json exceeds maximum size of ${MAX_CONFIG_FILE_SIZE / 1024 / 1024}MB`,
88
- )
89
- }
90
-
91
- const raw = fs.readFileSync(configPath, "utf-8")
92
- const config = JSON.parse(raw)
93
-
94
- const errors = validateLpmConfig(config)
95
- if (errors.length > 0) {
96
- throw new Error(`Invalid lpm.config.json:\n ${errors.join("\n ")}`)
97
- }
98
-
99
- return config
100
- }
101
-
102
- /**
103
- * Validate a parsed lpm.config.json object.
104
- *
105
- * @param {object} config - Parsed lpm.config.json
106
- * @returns {string[]} Array of error messages (empty if valid)
107
- */
108
- export function validateLpmConfig(config) {
109
- const errors = []
110
-
111
- // Validate file rules
112
- if (config.files) {
113
- if (!Array.isArray(config.files)) {
114
- errors.push("'files' must be an array")
115
- } else {
116
- if (config.files.length > MAX_FILE_RULES) {
117
- errors.push(
118
- `Too many file rules (${config.files.length}). Maximum is ${MAX_FILE_RULES}.`,
119
- )
120
- }
121
-
122
- for (let i = 0; i < config.files.length; i++) {
123
- const rule = config.files[i]
124
-
125
- if (rule.src) {
126
- const normalizedSrc = rule.src.replace(/\\/g, "/")
127
- if (normalizedSrc.includes("..")) {
128
- errors.push(
129
- `files[${i}].src contains path traversal: "${rule.src}"`,
130
- )
131
- }
132
- if (normalizedSrc.startsWith("/")) {
133
- errors.push(
134
- `files[${i}].src must be a relative path: "${rule.src}"`,
135
- )
136
- }
137
- }
138
-
139
- if (rule.dest) {
140
- const normalizedDest = rule.dest.replace(/\\/g, "/")
141
- if (normalizedDest.includes("..")) {
142
- errors.push(
143
- `files[${i}].dest contains path traversal: "${rule.dest}"`,
144
- )
145
- }
146
- if (normalizedDest.startsWith("/")) {
147
- errors.push(
148
- `files[${i}].dest must be a relative path: "${rule.dest}"`,
149
- )
150
- }
151
- }
152
-
153
- if (
154
- rule.include &&
155
- !["always", "never", "when"].includes(rule.include)
156
- ) {
157
- errors.push(
158
- `files[${i}].include must be "always", "never", or "when". Got: "${rule.include}"`,
159
- )
160
- }
161
-
162
- if (rule.include === "when" && !rule.condition) {
163
- errors.push(`files[${i}] has include "when" but no condition object`)
164
- }
165
- }
166
- }
167
- }
168
-
169
- // Validate configSchema
170
- if (config.configSchema) {
171
- if (
172
- typeof config.configSchema !== "object" ||
173
- Array.isArray(config.configSchema)
174
- ) {
175
- errors.push("'configSchema' must be an object")
176
- } else {
177
- for (const [key, entry] of Object.entries(config.configSchema)) {
178
- if (!entry.type) {
179
- errors.push(`configSchema.${key} is missing 'type'`)
180
- } else if (!["select", "boolean"].includes(entry.type)) {
181
- errors.push(
182
- `configSchema.${key}.type must be "select" or "boolean". Got: "${entry.type}"`,
183
- )
184
- }
185
-
186
- if (entry.type === "select") {
187
- if (
188
- !entry.options ||
189
- !Array.isArray(entry.options) ||
190
- entry.options.length === 0
191
- ) {
192
- errors.push(
193
- `configSchema.${key} is type "select" but has no options`,
194
- )
195
- }
196
- }
197
- }
198
- }
199
- }
200
-
201
- // Validate importAlias
202
- if (config.importAlias !== undefined) {
203
- if (typeof config.importAlias !== "string") {
204
- errors.push('\'importAlias\' must be a string (e.g., "@/", "~/")')
205
- } else if (!config.importAlias.endsWith("/")) {
206
- errors.push(
207
- '\'importAlias\' must end with "/" (e.g., "@/", "~/", "@src/")',
208
- )
209
- }
210
- }
211
-
212
- return errors
213
- }
214
-
215
- /**
216
- * Evaluate whether a file rule should be included based on config and provided params.
217
- *
218
- * Implements "include all by default": if a condition key was NOT explicitly
219
- * provided by the user, the file is included regardless of its condition value.
220
- *
221
- * @param {object} fileRule - File rule from lpm.config.json
222
- * @param {Record<string, string>} mergedConfig - Merged configuration values
223
- * @param {Set<string>} providedParams - Set of parameter keys explicitly provided by the user
224
- * @returns {boolean} Whether the file should be included
225
- */
226
- export function evaluateCondition(fileRule, mergedConfig, providedParams) {
227
- if (!fileRule.include || fileRule.include === "always") return true
228
- if (fileRule.include === "never") return false
229
-
230
- if (fileRule.include === "when" && fileRule.condition) {
231
- // All condition entries must match (AND logic)
232
- for (const [conditionKey, conditionValue] of Object.entries(
233
- fileRule.condition,
234
- )) {
235
- // "Include all by default": if param was NOT provided, skip this check (include the file)
236
- if (!providedParams.has(conditionKey)) {
237
- continue
238
- }
239
-
240
- const configValue = mergedConfig[conditionKey]
241
-
242
- // Handle comma-separated multi-select values
243
- if (typeof configValue === "string" && configValue.includes(",")) {
244
- const selectedValues = configValue.split(",").map(v => v.trim())
245
- if (!selectedValues.includes(String(conditionValue))) {
246
- return false
247
- }
248
- } else {
249
- // Single value or boolean comparison
250
- if (String(configValue) !== String(conditionValue)) {
251
- return false
252
- }
253
- }
254
- }
255
-
256
- return true
257
- }
258
-
259
- // Default: include
260
- return true
261
- }
262
-
263
- /**
264
- * Filter file rules based on merged config and provided params.
265
- *
266
- * @param {object[]} files - Array of file rules from lpm.config.json
267
- * @param {Record<string, string>} mergedConfig - Merged configuration values
268
- * @param {Set<string>} providedParams - Set of parameter keys explicitly provided
269
- * @returns {object[]} Filtered file rules that should be included
270
- */
271
- export function filterFiles(files, mergedConfig, providedParams) {
272
- return files.filter(fileRule =>
273
- evaluateCondition(fileRule, mergedConfig, providedParams),
274
- )
275
- }
276
-
277
- /**
278
- * Resolve conditional dependencies based on config choices.
279
- *
280
- * @param {Record<string, Record<string, string[]>>} depConfig - Dependencies config from lpm.config.json
281
- * @param {Record<string, string>} mergedConfig - Merged configuration values
282
- * @returns {{ npm: string[], lpm: string[] }} Separated npm and LPM dependencies
283
- */
284
- export function resolveConditionalDependencies(depConfig, mergedConfig) {
285
- const allDeps = new Set()
286
-
287
- for (const [configKey, depMap] of Object.entries(depConfig)) {
288
- const selectedValue = mergedConfig[configKey]
289
- if (!selectedValue) continue
290
-
291
- // Handle comma-separated values
292
- const values =
293
- typeof selectedValue === "string" && selectedValue.includes(",")
294
- ? selectedValue.split(",").map(v => v.trim())
295
- : [selectedValue]
296
-
297
- for (const value of values) {
298
- const deps = depMap[value]
299
- if (Array.isArray(deps)) {
300
- for (const dep of deps) {
301
- allDeps.add(dep)
302
- }
303
- }
304
- }
305
- }
306
-
307
- const npm = []
308
- const lpm = []
309
-
310
- for (const dep of allDeps) {
311
- if (dep.startsWith("@lpm.dev/")) {
312
- lpm.push(dep)
313
- } else {
314
- npm.push(dep)
315
- }
316
- }
317
-
318
- return { npm, lpm }
319
- }
320
-
321
- /**
322
- * Expand glob-like src patterns to actual file paths.
323
- *
324
- * Supports simple patterns:
325
- * - Exact paths: "lib/utils.js"
326
- * - Directory wildcards: "components/dialog/**"
327
- *
328
- * @param {string} srcPattern - Source pattern from file rule
329
- * @param {string} extractDir - Extracted tarball directory
330
- * @returns {string[]} Array of matching file paths (relative to extractDir)
331
- */
332
- export function expandSrcGlob(srcPattern, extractDir) {
333
- // If pattern doesn't contain *, it's an exact path
334
- if (!srcPattern.includes("*")) {
335
- const fullPath = path.join(extractDir, srcPattern)
336
- if (fs.existsSync(fullPath)) {
337
- return [srcPattern]
338
- }
339
- return []
340
- }
341
-
342
- // Handle ** (directory wildcard)
343
- if (srcPattern.endsWith("/**")) {
344
- const baseDir = srcPattern.slice(0, -3) // Remove /**
345
- const fullBaseDir = path.join(extractDir, baseDir)
346
-
347
- if (
348
- !fs.existsSync(fullBaseDir) ||
349
- !fs.statSync(fullBaseDir).isDirectory()
350
- ) {
351
- return []
352
- }
353
-
354
- const results = []
355
- const collectFiles = (dir, relativeTo) => {
356
- const entries = fs.readdirSync(dir, { withFileTypes: true })
357
- for (const entry of entries) {
358
- const fullEntryPath = path.join(dir, entry.name)
359
- const relPath = path.relative(extractDir, fullEntryPath)
360
-
361
- if (entry.isDirectory()) {
362
- collectFiles(fullEntryPath, relativeTo)
363
- } else {
364
- results.push(relPath)
365
- }
366
- }
367
- }
368
-
369
- collectFiles(fullBaseDir, extractDir)
370
- return results
371
- }
372
-
373
- // Handle dir/*.ext patterns (single-directory wildcard)
374
- const lastSlash = srcPattern.lastIndexOf("/")
375
- const dirPart = lastSlash >= 0 ? srcPattern.slice(0, lastSlash) : "."
376
- const filePart = lastSlash >= 0 ? srcPattern.slice(lastSlash + 1) : srcPattern
377
-
378
- if (filePart.includes("*")) {
379
- const fullDir = path.join(extractDir, dirPart)
380
- if (!fs.existsSync(fullDir) || !fs.statSync(fullDir).isDirectory()) {
381
- return []
382
- }
383
-
384
- // Convert glob pattern to regex: *.mdc → /^.*\.mdc$/
385
- const escaped = filePart
386
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
387
- .replace(/\*/g, ".*")
388
- const regex = new RegExp(`^${escaped}$`)
389
-
390
- const entries = fs.readdirSync(fullDir, { withFileTypes: true })
391
- const results = []
392
- for (const entry of entries) {
393
- if (entry.isFile() && regex.test(entry.name)) {
394
- results.push(
395
- dirPart === "." ? entry.name : path.join(dirPart, entry.name),
396
- )
397
- }
398
- }
399
- return results
400
- }
401
-
402
- // For other patterns, treat as exact path
403
- const fullPath = path.join(extractDir, srcPattern)
404
- if (fs.existsSync(fullPath)) {
405
- return [srcPattern]
406
- }
407
- return []
408
- }
package/lib/oidc.js DELETED
@@ -1,166 +0,0 @@
1
- /**
2
- * OIDC (OpenID Connect) token exchange for secret-free CI operations.
3
- *
4
- * When running in GitHub Actions with `id-token: write` permission,
5
- * this module requests an OIDC JWT from GitHub and exchanges it for
6
- * a short-lived LPM token — no LPM_TOKEN secret required.
7
- *
8
- * Two modes:
9
- * - **Publish**: Requires trusted publisher config in LPM dashboard.
10
- * - **Read (install)**: Requires GitHub/GitLab account linked to LPM user.
11
- *
12
- * @module cli/lib/oidc
13
- */
14
-
15
- import { getRegistryUrl } from "./config.js"
16
-
17
- /**
18
- * Detect whether we are running in GitHub Actions with OIDC available.
19
- * Both env vars must be set for token request to work.
20
- *
21
- * @returns {boolean}
22
- */
23
- export function isGitHubActionsWithOidc() {
24
- return !!(
25
- process.env.ACTIONS_ID_TOKEN_REQUEST_URL &&
26
- process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
27
- )
28
- }
29
-
30
- /**
31
- * Detect whether we are running in GitLab CI with an OIDC token injected.
32
- * GitLab injects the JWT directly as an env var via id_tokens — no HTTP fetch needed.
33
- *
34
- * @returns {boolean}
35
- */
36
- export function isGitLabCiWithOidc() {
37
- return !!(
38
- process.env.GITLAB_CI === "true" && process.env.LPM_GITLAB_OIDC_TOKEN
39
- )
40
- }
41
-
42
- /**
43
- * Request a GitHub Actions OIDC JWT.
44
- * @returns {Promise<string>} The raw JWT string
45
- */
46
- async function requestGitHubOidcJwt() {
47
- const githubTokenUrl = `${process.env.ACTIONS_ID_TOKEN_REQUEST_URL}&audience=https://lpm.dev`
48
-
49
- const githubRes = await fetch(githubTokenUrl, {
50
- headers: {
51
- Authorization: `bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}`,
52
- },
53
- })
54
-
55
- if (!githubRes.ok) {
56
- const body = await githubRes.text().catch(() => "")
57
- throw new Error(
58
- `GitHub OIDC token request failed (${githubRes.status})${body ? `: ${body}` : ""}`,
59
- )
60
- }
61
-
62
- const { value: oidcJwt } = await githubRes.json()
63
- if (!oidcJwt) {
64
- throw new Error("GitHub OIDC response did not contain a token value")
65
- }
66
-
67
- return oidcJwt
68
- }
69
-
70
- /**
71
- * Exchange an OIDC JWT with the LPM server for a short-lived token.
72
- *
73
- * @param {string} oidcJwt - The raw OIDC JWT
74
- * @param {{ scope?: "read"|"publish", packageName?: string }} [options]
75
- * @returns {Promise<{ token: string, expiresAt: string }>}
76
- */
77
- async function exchangeJwtWithServer(oidcJwt, options = {}) {
78
- const { scope = "publish", packageName } = options
79
- const registryUrl = getRegistryUrl()
80
- const scopeParam = scope === "read" ? "?scope=read" : ""
81
-
82
- const body = { token: oidcJwt }
83
- if (packageName) body.package = packageName
84
-
85
- const exchangeRes = await fetch(
86
- `${registryUrl}/api/registry/-/token/oidc${scopeParam}`,
87
- {
88
- method: "POST",
89
- headers: { "Content-Type": "application/json" },
90
- body: JSON.stringify(body),
91
- },
92
- )
93
-
94
- if (!exchangeRes.ok) {
95
- const text = await exchangeRes.text().catch(() => "")
96
- throw new Error(
97
- `OIDC token exchange failed (${exchangeRes.status})${text ? `: ${text}` : ""}`,
98
- )
99
- }
100
-
101
- const data = await exchangeRes.json()
102
- if (!data.token) {
103
- throw new Error("OIDC exchange response did not contain a token")
104
- }
105
-
106
- return data
107
- }
108
-
109
- /**
110
- * Exchange a GitHub Actions OIDC JWT for a short-lived LPM token.
111
- *
112
- * @param {string} [packageName] - Required for publish scope, optional for read
113
- * @param {{ scope?: "read"|"publish" }} [options]
114
- * @returns {Promise<string>} Short-lived LPM token
115
- */
116
- export async function exchangeOidcToken(packageName, options = {}) {
117
- const oidcJwt = await requestGitHubOidcJwt()
118
- const data = await exchangeJwtWithServer(oidcJwt, {
119
- scope: options.scope || "publish",
120
- packageName,
121
- })
122
- return data.token
123
- }
124
-
125
- /**
126
- * Exchange a GitLab CI OIDC JWT for a short-lived LPM token.
127
- *
128
- * @param {string} [packageName] - Required for publish scope, optional for read
129
- * @param {{ scope?: "read"|"publish" }} [options]
130
- * @returns {Promise<string>} Short-lived LPM token
131
- */
132
- export async function exchangeGitLabOidcToken(packageName, options = {}) {
133
- const oidcJwt = process.env.LPM_GITLAB_OIDC_TOKEN
134
- if (!oidcJwt) {
135
- throw new Error(
136
- "LPM_GITLAB_OIDC_TOKEN is not set. " +
137
- "Add id_tokens: LPM_GITLAB_OIDC_TOKEN: { aud: https://lpm.dev } to your .gitlab-ci.yml",
138
- )
139
- }
140
-
141
- const data = await exchangeJwtWithServer(oidcJwt, {
142
- scope: options.scope || "publish",
143
- packageName,
144
- })
145
- return data.token
146
- }
147
-
148
- /**
149
- * Detect CI environment and exchange OIDC token for a read-only install token.
150
- * Works with both GitHub Actions and GitLab CI.
151
- *
152
- * @returns {Promise<string>} Short-lived read-only LPM token
153
- * @throws {Error} If not in a supported CI environment or exchange fails
154
- */
155
- export async function exchangeOidcInstallToken() {
156
- if (isGitHubActionsWithOidc()) {
157
- return exchangeOidcToken(null, { scope: "read" })
158
- }
159
- if (isGitLabCiWithOidc()) {
160
- return exchangeGitLabOidcToken(null, { scope: "read" })
161
- }
162
- throw new Error(
163
- "OIDC is not available. This command requires a CI environment with OIDC support " +
164
- "(GitHub Actions with `id-token: write` or GitLab CI with `id_tokens` configured).",
165
- )
166
- }