@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/safe-path.js DELETED
@@ -1,180 +0,0 @@
1
- /**
2
- * Safe Path Resolution
3
- *
4
- * Provides path traversal protection for file operations.
5
- * Ensures all paths stay within a designated base directory.
6
- *
7
- * @module cli/lib/safe-path
8
- */
9
-
10
- import { isAbsolute, normalize, relative, resolve } from "node:path"
11
- import { ERROR_MESSAGES } from "./constants.js"
12
-
13
- /**
14
- * Resolve a path safely within a base directory.
15
- * Prevents path traversal attacks using ../ sequences or absolute paths.
16
- *
17
- * @param {string} basePath - The base directory (must be absolute)
18
- * @param {string} userPath - The user-provided path to resolve
19
- * @returns {{ safe: boolean, resolvedPath?: string, error?: string }}
20
- */
21
- export function resolveSafePath(basePath, userPath) {
22
- // Base path must be absolute
23
- if (!isAbsolute(basePath)) {
24
- return {
25
- safe: false,
26
- error: "Base path must be absolute",
27
- }
28
- }
29
-
30
- // Reject absolute paths from user input
31
- if (isAbsolute(userPath)) {
32
- return {
33
- safe: false,
34
- error: ERROR_MESSAGES.pathTraversal,
35
- }
36
- }
37
-
38
- // Normalize the base path
39
- const normalizedBase = normalize(basePath)
40
-
41
- // Resolve the full path
42
- const resolvedPath = resolve(normalizedBase, userPath)
43
-
44
- // Normalize the resolved path
45
- const normalizedResolved = normalize(resolvedPath)
46
-
47
- // Check if resolved path is within base
48
- const relativePath = relative(normalizedBase, normalizedResolved)
49
-
50
- // Path is outside base if:
51
- // 1. It starts with '..' (goes above base)
52
- // 2. It's an absolute path (on Windows, this can happen with drive changes)
53
- if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
54
- return {
55
- safe: false,
56
- error: ERROR_MESSAGES.pathTraversal,
57
- }
58
- }
59
-
60
- return {
61
- safe: true,
62
- resolvedPath: normalizedResolved,
63
- }
64
- }
65
-
66
- /**
67
- * Check if a path contains dangerous patterns.
68
- * This is a quick check before more expensive resolution.
69
- *
70
- * @param {string} userPath - The path to check
71
- * @returns {boolean} - True if the path looks dangerous
72
- */
73
- export function hasDangerousPatterns(userPath) {
74
- if (!userPath || typeof userPath !== "string") {
75
- return true
76
- }
77
-
78
- // Dangerous patterns
79
- const dangerousPatterns = [
80
- // Null bytes (can bypass checks in some systems)
81
- "\0",
82
- // Windows drive letters
83
- /^[a-zA-Z]:/,
84
- // UNC paths
85
- /^\\\\|^\/\//,
86
- // Excessive parent traversal
87
- /\.\.[/\\]\.\.[/\\]\.\.[/\\]/,
88
- // Hidden files/directories (optional, depends on use case)
89
- // /\/\./,
90
- ]
91
-
92
- for (const pattern of dangerousPatterns) {
93
- if (typeof pattern === "string") {
94
- if (userPath.includes(pattern)) return true
95
- } else {
96
- if (pattern.test(userPath)) return true
97
- }
98
- }
99
-
100
- return false
101
- }
102
-
103
- /**
104
- * Sanitize a filename by removing or replacing dangerous characters.
105
- *
106
- * @param {string} filename - The filename to sanitize
107
- * @returns {string} - Sanitized filename
108
- */
109
- export function sanitizeFilename(filename) {
110
- if (!filename || typeof filename !== "string") {
111
- return ""
112
- }
113
-
114
- return (
115
- filename
116
- // Remove null bytes
117
- .replace(/\0/g, "")
118
- // Replace path separators
119
- .replace(/[/\\]/g, "-")
120
- // Remove other dangerous characters
121
- .replace(/[<>:"|?*]/g, "")
122
- // Collapse multiple dashes
123
- .replace(/-+/g, "-")
124
- // Trim dashes from ends
125
- .replace(/^-+|-+$/g, "")
126
- // Limit length
127
- .slice(0, 255)
128
- )
129
- }
130
-
131
- /**
132
- * Validate a package component path.
133
- * Used by the `lpm add` command to ensure extracted files stay in bounds.
134
- *
135
- * @param {string} projectRoot - The project root directory
136
- * @param {string} componentPath - The path where the component should be extracted
137
- * @returns {{ valid: boolean, resolvedPath?: string, error?: string }}
138
- */
139
- export function validateComponentPath(projectRoot, componentPath) {
140
- // Quick dangerous pattern check
141
- if (hasDangerousPatterns(componentPath)) {
142
- return {
143
- valid: false,
144
- error: ERROR_MESSAGES.pathTraversal,
145
- }
146
- }
147
-
148
- // Full path resolution and validation
149
- const result = resolveSafePath(projectRoot, componentPath)
150
-
151
- return {
152
- valid: result.safe,
153
- resolvedPath: result.resolvedPath,
154
- error: result.error,
155
- }
156
- }
157
-
158
- /**
159
- * Validate each file path in a tarball before extraction.
160
- * Prevents zip slip attacks.
161
- *
162
- * @param {string} extractDir - The extraction directory
163
- * @param {string[]} filePaths - Array of file paths from the tarball
164
- * @returns {{ valid: boolean, invalidPaths: string[] }}
165
- */
166
- export function validateTarballPaths(extractDir, filePaths) {
167
- const invalidPaths = []
168
-
169
- for (const filePath of filePaths) {
170
- const result = resolveSafePath(extractDir, filePath)
171
- if (!result.safe) {
172
- invalidPaths.push(filePath)
173
- }
174
- }
175
-
176
- return {
177
- valid: invalidPaths.length === 0,
178
- invalidPaths,
179
- }
180
- }
@@ -1,321 +0,0 @@
1
- /**
2
- * Secure Store - OS Keychain Storage with Fallback
3
- *
4
- * Provides secure credential storage using the system keychain.
5
- * Falls back to encrypted file storage if keytar is unavailable.
6
- *
7
- * @module cli/lib/secure-store
8
- */
9
-
10
- import {
11
- createCipheriv,
12
- createDecipheriv,
13
- randomBytes,
14
- scryptSync,
15
- } from "node:crypto"
16
- import {
17
- existsSync,
18
- mkdirSync,
19
- readFileSync,
20
- unlinkSync,
21
- writeFileSync,
22
- } from "node:fs"
23
- import { homedir } from "node:os"
24
- import { join } from "node:path"
25
- import { KEYTAR_ACCOUNT_NAME, KEYTAR_SERVICE_NAME } from "./constants.js"
26
-
27
- /** @type {import('keytar') | null} */
28
- let keytar = null
29
-
30
- /**
31
- * Try to load keytar for native keychain access.
32
- * Falls back gracefully if not available.
33
- */
34
- async function loadKeytar() {
35
- if (keytar !== null) return keytar
36
-
37
- try {
38
- // Dynamic import to avoid hard dependency
39
- const mod = await import("keytar")
40
- // ESM dynamic import returns { default: keytarModule }
41
- keytar = mod.default || mod
42
- return keytar
43
- } catch {
44
- // keytar not available (missing native dependencies)
45
- keytar = false
46
- return null
47
- }
48
- }
49
-
50
- // ============================================================================
51
- // Fallback Encrypted File Storage
52
- // ============================================================================
53
-
54
- const ENCRYPTED_STORE_DIR = join(homedir(), ".lpm")
55
- const ENCRYPTED_STORE_FILE = join(ENCRYPTED_STORE_DIR, ".credentials")
56
- const SALT_FILE = join(ENCRYPTED_STORE_DIR, ".salt")
57
-
58
- /**
59
- * Get or create encryption salt.
60
- * @returns {Buffer}
61
- */
62
- function getOrCreateSalt() {
63
- if (existsSync(SALT_FILE)) {
64
- return readFileSync(SALT_FILE)
65
- }
66
-
67
- const salt = randomBytes(32)
68
- if (!existsSync(ENCRYPTED_STORE_DIR)) {
69
- mkdirSync(ENCRYPTED_STORE_DIR, { recursive: true, mode: 0o700 })
70
- }
71
- writeFileSync(SALT_FILE, salt, { mode: 0o600 })
72
- return salt
73
- }
74
-
75
- /**
76
- * Derive encryption key from machine-specific data.
77
- * Uses a combination of hostname, username, and random salt.
78
- * @returns {Buffer}
79
- */
80
- function deriveKey() {
81
- const salt = getOrCreateSalt()
82
- // Use machine-specific data as part of the key derivation
83
- const machineId = `${homedir()}-${process.env.USER || "user"}`
84
- return scryptSync(machineId, salt, 32)
85
- }
86
-
87
- /**
88
- * Encrypt a value using AES-256-GCM.
89
- * @param {string} value - The value to encrypt
90
- * @returns {string} - Base64 encoded encrypted data
91
- */
92
- function encrypt(value) {
93
- const key = deriveKey()
94
- const iv = randomBytes(16)
95
- const cipher = createCipheriv("aes-256-gcm", key, iv)
96
-
97
- let encrypted = cipher.update(value, "utf8", "base64")
98
- encrypted += cipher.final("base64")
99
-
100
- const authTag = cipher.getAuthTag()
101
-
102
- // Format: iv:authTag:encrypted
103
- return `${iv.toString("base64")}:${authTag.toString("base64")}:${encrypted}`
104
- }
105
-
106
- /**
107
- * Decrypt a value using AES-256-GCM.
108
- * @param {string} encryptedValue - Base64 encoded encrypted data
109
- * @returns {string | null} - Decrypted value or null if failed
110
- */
111
- function decrypt(encryptedValue) {
112
- try {
113
- const key = deriveKey()
114
- const [ivBase64, authTagBase64, encrypted] = encryptedValue.split(":")
115
-
116
- const iv = Buffer.from(ivBase64, "base64")
117
- const authTag = Buffer.from(authTagBase64, "base64")
118
-
119
- const decipher = createDecipheriv("aes-256-gcm", key, iv)
120
- decipher.setAuthTag(authTag)
121
-
122
- let decrypted = decipher.update(encrypted, "base64", "utf8")
123
- decrypted += decipher.final("utf8")
124
-
125
- return decrypted
126
- } catch {
127
- // Decryption failed (wrong key, corrupted data, etc.)
128
- return null
129
- }
130
- }
131
-
132
- /**
133
- * Read encrypted store from file.
134
- * @returns {Record<string, string>}
135
- */
136
- function readEncryptedStore() {
137
- if (!existsSync(ENCRYPTED_STORE_FILE)) {
138
- return {}
139
- }
140
-
141
- try {
142
- const content = readFileSync(ENCRYPTED_STORE_FILE, "utf8")
143
- const decrypted = decrypt(content)
144
- if (!decrypted) return {}
145
- return JSON.parse(decrypted)
146
- } catch {
147
- return {}
148
- }
149
- }
150
-
151
- /**
152
- * Write encrypted store to file.
153
- * @param {Record<string, string>} store
154
- */
155
- function writeEncryptedStore(store) {
156
- if (!existsSync(ENCRYPTED_STORE_DIR)) {
157
- mkdirSync(ENCRYPTED_STORE_DIR, { recursive: true, mode: 0o700 })
158
- }
159
-
160
- const content = encrypt(JSON.stringify(store))
161
- writeFileSync(ENCRYPTED_STORE_FILE, content, { mode: 0o600 })
162
- }
163
-
164
- // ============================================================================
165
- // Public API
166
- // ============================================================================
167
-
168
- /**
169
- * Store a secret securely.
170
- * Uses OS keychain if available, falls back to encrypted file.
171
- *
172
- * @param {string} key - The key to store the secret under
173
- * @param {string} value - The secret value
174
- * @returns {Promise<void>}
175
- */
176
- export async function setSecret(key, value) {
177
- const kt = await loadKeytar()
178
-
179
- if (kt) {
180
- await kt.setPassword(KEYTAR_SERVICE_NAME, key, value)
181
- return
182
- }
183
-
184
- // Fallback to encrypted file
185
- const store = readEncryptedStore()
186
- store[key] = value
187
- writeEncryptedStore(store)
188
- }
189
-
190
- /**
191
- * Retrieve a secret.
192
- * Uses OS keychain if available, falls back to encrypted file.
193
- *
194
- * @param {string} key - The key to retrieve
195
- * @returns {Promise<string | null>}
196
- */
197
- export async function getSecret(key) {
198
- const kt = await loadKeytar()
199
-
200
- if (kt) {
201
- return kt.getPassword(KEYTAR_SERVICE_NAME, key)
202
- }
203
-
204
- // Fallback to encrypted file
205
- const store = readEncryptedStore()
206
- return store[key] || null
207
- }
208
-
209
- /**
210
- * Delete a secret.
211
- * Uses OS keychain if available, falls back to encrypted file.
212
- *
213
- * @param {string} key - The key to delete
214
- * @returns {Promise<boolean>}
215
- */
216
- export async function deleteSecret(key) {
217
- const kt = await loadKeytar()
218
-
219
- if (kt) {
220
- return kt.deletePassword(KEYTAR_SERVICE_NAME, key)
221
- }
222
-
223
- // Fallback to encrypted file
224
- const store = readEncryptedStore()
225
- if (key in store) {
226
- delete store[key]
227
- writeEncryptedStore(store)
228
- return true
229
- }
230
- return false
231
- }
232
-
233
- /**
234
- * Clear all stored secrets.
235
- * @returns {Promise<void>}
236
- */
237
- export async function clearAllSecrets() {
238
- const kt = await loadKeytar()
239
-
240
- if (kt) {
241
- // keytar doesn't have a "clear all" - delete known keys
242
- await kt.deletePassword(KEYTAR_SERVICE_NAME, KEYTAR_ACCOUNT_NAME)
243
- return
244
- }
245
-
246
- // Fallback: delete the encrypted store file
247
- if (existsSync(ENCRYPTED_STORE_FILE)) {
248
- unlinkSync(ENCRYPTED_STORE_FILE)
249
- }
250
- }
251
-
252
- /**
253
- * Check if secure storage is using native keychain.
254
- * @returns {Promise<boolean>}
255
- */
256
- export async function isUsingKeychain() {
257
- const kt = await loadKeytar()
258
- return kt !== null && kt !== false
259
- }
260
-
261
- // ============================================================================
262
- // Token-Specific Helpers (Registry-Aware)
263
- // ============================================================================
264
-
265
- /**
266
- * Build a registry-scoped key for token storage.
267
- * Each registry URL gets its own token slot so dev and production
268
- * tokens don't overwrite each other.
269
- * @param {string} [registryUrl]
270
- * @returns {string}
271
- */
272
- function tokenKey(registryUrl) {
273
- if (!registryUrl) return KEYTAR_ACCOUNT_NAME
274
- return `${KEYTAR_ACCOUNT_NAME}:${registryUrl}`
275
- }
276
-
277
- /**
278
- * Get the stored auth token for a specific registry.
279
- * Falls back to the legacy un-scoped key and migrates it forward.
280
- * @param {string} [registryUrl] - Registry URL to scope the token to
281
- * @returns {Promise<string | null>}
282
- */
283
- export async function getToken(registryUrl) {
284
- const key = tokenKey(registryUrl)
285
-
286
- // Try registry-scoped key first
287
- const scoped = await getSecret(key)
288
- if (scoped) return scoped
289
-
290
- // Migration: if legacy un-scoped token exists, migrate it to the
291
- // default registry key (production) so existing logins keep working
292
- if (registryUrl) {
293
- const legacy = await getSecret(KEYTAR_ACCOUNT_NAME)
294
- if (legacy) {
295
- await setSecret(key, legacy)
296
- await deleteSecret(KEYTAR_ACCOUNT_NAME)
297
- return legacy
298
- }
299
- }
300
-
301
- return null
302
- }
303
-
304
- /**
305
- * Set the auth token for a specific registry.
306
- * @param {string} token
307
- * @param {string} [registryUrl] - Registry URL to scope the token to
308
- * @returns {Promise<void>}
309
- */
310
- export async function setToken(token, registryUrl) {
311
- return setSecret(tokenKey(registryUrl), token)
312
- }
313
-
314
- /**
315
- * Clear the auth token for a specific registry.
316
- * @param {string} [registryUrl] - Registry URL to scope the token to
317
- * @returns {Promise<boolean>}
318
- */
319
- export async function clearToken(registryUrl) {
320
- return deleteSecret(tokenKey(registryUrl))
321
- }