@lpm-registry/cli 0.2.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 (54) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +15 -0
  3. package/README.md +406 -0
  4. package/bin/lpm.js +334 -0
  5. package/index.d.ts +131 -0
  6. package/index.js +31 -0
  7. package/lib/api.js +324 -0
  8. package/lib/commands/add.js +1217 -0
  9. package/lib/commands/audit.js +283 -0
  10. package/lib/commands/cache.js +209 -0
  11. package/lib/commands/check-name.js +112 -0
  12. package/lib/commands/config.js +174 -0
  13. package/lib/commands/doctor.js +142 -0
  14. package/lib/commands/info.js +215 -0
  15. package/lib/commands/init.js +146 -0
  16. package/lib/commands/install.js +217 -0
  17. package/lib/commands/login.js +547 -0
  18. package/lib/commands/logout.js +94 -0
  19. package/lib/commands/marketplace-compare.js +164 -0
  20. package/lib/commands/marketplace-earnings.js +89 -0
  21. package/lib/commands/mcp-setup.js +363 -0
  22. package/lib/commands/open.js +82 -0
  23. package/lib/commands/outdated.js +291 -0
  24. package/lib/commands/pool-stats.js +100 -0
  25. package/lib/commands/publish.js +707 -0
  26. package/lib/commands/quality.js +211 -0
  27. package/lib/commands/remove.js +82 -0
  28. package/lib/commands/run.js +14 -0
  29. package/lib/commands/search.js +143 -0
  30. package/lib/commands/setup.js +92 -0
  31. package/lib/commands/skills.js +863 -0
  32. package/lib/commands/token-rotate.js +25 -0
  33. package/lib/commands/whoami.js +129 -0
  34. package/lib/config.js +240 -0
  35. package/lib/constants.js +190 -0
  36. package/lib/ecosystem.js +501 -0
  37. package/lib/editors.js +215 -0
  38. package/lib/import-rewriter.js +364 -0
  39. package/lib/install-targets/mcp-server.js +245 -0
  40. package/lib/install-targets/vscode-extension.js +178 -0
  41. package/lib/install-targets.js +82 -0
  42. package/lib/integrity.js +179 -0
  43. package/lib/lpm-config-prompts.js +102 -0
  44. package/lib/lpm-config.js +408 -0
  45. package/lib/project-utils.js +152 -0
  46. package/lib/quality/checks.js +654 -0
  47. package/lib/quality/display.js +139 -0
  48. package/lib/quality/score.js +115 -0
  49. package/lib/quality/swift-checks.js +447 -0
  50. package/lib/safe-path.js +180 -0
  51. package/lib/secure-store.js +288 -0
  52. package/lib/swift-project.js +637 -0
  53. package/lib/ui.js +40 -0
  54. package/package.json +74 -0
@@ -0,0 +1,25 @@
1
+ import { request } from "../api.js"
2
+ import { setToken } from "../config.js"
3
+ import { createSpinner, printHeader } from "../ui.js"
4
+
5
+ export async function rotateToken() {
6
+ printHeader()
7
+ const spinner = createSpinner("Rotating token...").start()
8
+ try {
9
+ const response = await request("/-/token/rotate", {
10
+ method: "POST",
11
+ })
12
+
13
+ if (!response.ok) {
14
+ throw new Error(`Request failed with status ${response.status}`)
15
+ }
16
+
17
+ const data = await response.json()
18
+ const newToken = data.token
19
+
20
+ await setToken(newToken)
21
+ spinner.succeed("Token rotated successfully! Local config updated.")
22
+ } catch (error) {
23
+ spinner.fail(`Error: ${error.message}`)
24
+ }
25
+ }
@@ -0,0 +1,129 @@
1
+ import { request } from "../api.js"
2
+ import { getRegistryUrl } from "../config.js"
3
+ import { log, printHeader } from "../ui.js"
4
+
5
+ export async function whoami(options = {}) {
6
+ if (!options.json) printHeader()
7
+ try {
8
+ const response = await request("/-/whoami")
9
+
10
+ if (!response.ok) {
11
+ throw new Error(`Request failed with status ${response.status}`)
12
+ }
13
+
14
+ const data = await response.json()
15
+
16
+ if (options.json) {
17
+ const result = {
18
+ username: data.username,
19
+ profileUsername: data.profile_username || null,
20
+ email: data.email || null,
21
+ plan: data.plan_tier || null,
22
+ hasPoolAccess: data.has_pool_access || false,
23
+ usage: data.usage || null,
24
+ limits: data.limits || null,
25
+ orgs: (data.organizations || []).map(org => ({
26
+ slug: org.slug,
27
+ name: org.name,
28
+ role: org.role,
29
+ })),
30
+ }
31
+ console.log(JSON.stringify(result, null, 2))
32
+ return
33
+ }
34
+
35
+ log.success(`Logged in as: ${data.username}`)
36
+
37
+ if (data.plan_tier) {
38
+ console.log("") // Spacer
39
+ log.info(`Plan: ${data.plan_tier.toUpperCase()}`)
40
+
41
+ // Pool subscription status
42
+ if (data.has_pool_access) {
43
+ log.success("Pool: Active")
44
+ } else {
45
+ log.info("Pool: Not subscribed")
46
+ }
47
+
48
+ if (data.usage) {
49
+ const storageMB = (data.usage.storage_bytes / (1024 * 1024)).toFixed(2)
50
+ const limits = data.limits || {}
51
+
52
+ // Storage Check - backend returns storageBytes (not storage_gb)
53
+ if (limits.storageBytes) {
54
+ const limitMB = (limits.storageBytes / (1024 * 1024)).toFixed(0)
55
+ const storageMsg = `Storage Used: ${storageMB}MB / ${limitMB}MB`
56
+ if (data.usage.storage_bytes > limits.storageBytes) {
57
+ log.error(`${storageMsg} (OVER LIMIT)`)
58
+ } else {
59
+ log.info(storageMsg)
60
+ }
61
+ } else {
62
+ log.info(`Storage Used: ${storageMB}MB`)
63
+ }
64
+
65
+ // Package Count Check - backend returns privatePackages (not private_packages)
66
+ if (limits.privatePackages !== undefined) {
67
+ if (
68
+ limits.privatePackages === Number.POSITIVE_INFINITY ||
69
+ limits.privatePackages === null
70
+ ) {
71
+ log.info(
72
+ `Private Packages: ${data.usage.private_packages} (Unlimited)`,
73
+ )
74
+ } else {
75
+ const pkgMsg = `Private Packages: ${data.usage.private_packages} / ${limits.privatePackages}`
76
+ if (data.usage.private_packages > limits.privatePackages) {
77
+ log.error(`${pkgMsg} (OVER LIMIT)`)
78
+ } else {
79
+ log.info(pkgMsg)
80
+ }
81
+ }
82
+ } else {
83
+ log.info(`Private Packages: ${data.usage.private_packages}`)
84
+ }
85
+
86
+ // Over limit warning
87
+ const overStorage =
88
+ limits.storageBytes && data.usage.storage_bytes > limits.storageBytes
89
+ const overPackages =
90
+ limits.privatePackages &&
91
+ limits.privatePackages !== Number.POSITIVE_INFINITY &&
92
+ limits.privatePackages !== null &&
93
+ data.usage.private_packages > limits.privatePackages
94
+
95
+ if (overStorage || overPackages) {
96
+ const registryUrl = getRegistryUrl()
97
+ console.log("")
98
+ log.warn("Your account is over its plan limits.")
99
+ log.warn("Write access (publishing, inviting members) is restricted.")
100
+ log.warn(
101
+ `Upgrade your plan: ${registryUrl}/dashboard/settings/billing`,
102
+ )
103
+ }
104
+ }
105
+ }
106
+
107
+ // Display available scopes for publishing
108
+ const registryUrl = getRegistryUrl()
109
+ console.log("")
110
+ log.info("Available Scopes:")
111
+
112
+ // Personal scope
113
+ if (data.profile_username) {
114
+ log.info(` Personal: @lpm.dev/${data.profile_username}.*`)
115
+ } else {
116
+ log.warn(` Personal: Not set (${registryUrl}/dashboard/settings)`)
117
+ }
118
+
119
+ // Organization scopes
120
+ if (data.organizations?.length > 0) {
121
+ log.info(" Organizations:")
122
+ for (const org of data.organizations) {
123
+ log.info(` @lpm.dev/${org.slug}.* (${org.role})`)
124
+ }
125
+ }
126
+ } catch (error) {
127
+ log.error(`Error: ${error.message}`)
128
+ }
129
+ }
package/lib/config.js ADDED
@@ -0,0 +1,240 @@
1
+ /**
2
+ * CLI Configuration Management
3
+ *
4
+ * Handles both secure credential storage and general configuration.
5
+ * Token storage migrated to secure-store for keychain integration.
6
+ *
7
+ * @module cli/lib/config
8
+ */
9
+
10
+ import Conf from "conf"
11
+ import {
12
+ DEFAULT_REGISTRY_URL,
13
+ MAX_RETRIES,
14
+ REQUEST_TIMEOUT_MS,
15
+ } from "./constants.js"
16
+ import {
17
+ isUsingKeychain,
18
+ clearToken as secureClearToken,
19
+ getToken as secureGetToken,
20
+ setToken as secureSetToken,
21
+ } from "./secure-store.js"
22
+
23
+ // ============================================================================
24
+ // General Configuration Store (non-sensitive data)
25
+ // ============================================================================
26
+
27
+ const config = new Conf({
28
+ projectName: "lpm-cli",
29
+ defaults: {
30
+ registryUrl: DEFAULT_REGISTRY_URL,
31
+ timeout: REQUEST_TIMEOUT_MS,
32
+ retries: MAX_RETRIES,
33
+ },
34
+ })
35
+
36
+ // ============================================================================
37
+ // Token Management (Secure Storage)
38
+ // ============================================================================
39
+
40
+ /**
41
+ * Get the stored auth token from secure storage.
42
+ * Priority: LPM_TOKEN env var > secure storage > legacy storage
43
+ * @returns {Promise<string | null>}
44
+ */
45
+ export async function getToken() {
46
+ // Check environment variable first (useful for CI/CD and testing)
47
+ const envToken = process.env.LPM_TOKEN
48
+ if (envToken) return envToken
49
+
50
+ // Try secure store
51
+ const secureToken = await secureGetToken()
52
+ if (secureToken) return secureToken
53
+
54
+ // Migration: check if token exists in old storage
55
+ const legacyToken = config.get("token")
56
+ if (legacyToken) {
57
+ // Migrate to secure storage
58
+ await secureSetToken(legacyToken)
59
+ config.delete("token")
60
+ return legacyToken
61
+ }
62
+
63
+ return null
64
+ }
65
+
66
+ /**
67
+ * Set the auth token in secure storage.
68
+ * @param {string | null} token
69
+ * @returns {Promise<void>}
70
+ */
71
+ export async function setToken(token) {
72
+ if (token === null) {
73
+ await secureClearToken()
74
+ } else {
75
+ await secureSetToken(token)
76
+ }
77
+ // Ensure legacy token is cleared
78
+ config.delete("token")
79
+ }
80
+
81
+ /**
82
+ * Clear the auth token from secure storage.
83
+ * @returns {Promise<void>}
84
+ */
85
+ export async function clearToken() {
86
+ await secureClearToken()
87
+ config.delete("token")
88
+ }
89
+
90
+ // ============================================================================
91
+ // Registry URL Configuration
92
+ // ============================================================================
93
+
94
+ /**
95
+ * Get the registry URL.
96
+ * Priority: LPM_REGISTRY_URL env var > stored config > default
97
+ * @returns {string}
98
+ */
99
+ export function getRegistryUrl() {
100
+ // Check environment variables first (useful for CI/CD and testing)
101
+ const envUrl = process.env.LPM_REGISTRY_URL
102
+ if (envUrl) return envUrl
103
+
104
+ return config.get("registryUrl", DEFAULT_REGISTRY_URL)
105
+ }
106
+
107
+ /**
108
+ * Set the registry URL.
109
+ * @param {string} url
110
+ */
111
+ export function setRegistryUrl(url) {
112
+ config.set("registryUrl", url)
113
+ }
114
+
115
+ // ============================================================================
116
+ // Timeout Configuration
117
+ // ============================================================================
118
+
119
+ /**
120
+ * Get the request timeout in milliseconds.
121
+ * @returns {number}
122
+ */
123
+ export function getTimeout() {
124
+ return config.get("timeout", REQUEST_TIMEOUT_MS)
125
+ }
126
+
127
+ /**
128
+ * Set the request timeout in milliseconds.
129
+ * @param {number} ms
130
+ */
131
+ export function setTimeout(ms) {
132
+ config.set("timeout", ms)
133
+ }
134
+
135
+ // ============================================================================
136
+ // Retry Configuration
137
+ // ============================================================================
138
+
139
+ /**
140
+ * Get the maximum retry count.
141
+ * @returns {number}
142
+ */
143
+ export function getRetries() {
144
+ return config.get("retries", MAX_RETRIES)
145
+ }
146
+
147
+ /**
148
+ * Set the maximum retry count.
149
+ * @param {number} count
150
+ */
151
+ export function setRetries(count) {
152
+ config.set("retries", count)
153
+ }
154
+
155
+ // ============================================================================
156
+ // General Configuration Access
157
+ // ============================================================================
158
+
159
+ /**
160
+ * Get all configuration values.
161
+ * @returns {Promise<Record<string, unknown>>}
162
+ */
163
+ export async function getAllConfig() {
164
+ const usingKeychain = await isUsingKeychain()
165
+ const hasToken = !!(await getToken())
166
+
167
+ return {
168
+ registryUrl: getRegistryUrl(),
169
+ timeout: getTimeout(),
170
+ retries: getRetries(),
171
+ secureStorage: usingKeychain ? "keychain" : "encrypted-file",
172
+ authenticated: hasToken,
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Get a specific configuration value.
178
+ * @param {string} key
179
+ * @returns {unknown}
180
+ */
181
+ export function getConfigValue(key) {
182
+ const configMap = {
183
+ registry: getRegistryUrl,
184
+ registryUrl: getRegistryUrl,
185
+ timeout: getTimeout,
186
+ retries: getRetries,
187
+ }
188
+
189
+ const getter = configMap[key]
190
+ if (getter) return getter()
191
+ return config.get(key)
192
+ }
193
+
194
+ /**
195
+ * Set a specific configuration value.
196
+ * @param {string} key
197
+ * @param {unknown} value
198
+ * @returns {boolean} - True if set successfully
199
+ */
200
+ export function setConfigValue(key, value) {
201
+ const configMap = {
202
+ registry: setRegistryUrl,
203
+ registryUrl: setRegistryUrl,
204
+ timeout: val => setTimeout(Number(val)),
205
+ retries: val => setRetries(Number(val)),
206
+ }
207
+
208
+ const setter = configMap[key]
209
+ if (setter) {
210
+ setter(value)
211
+ return true
212
+ }
213
+
214
+ // Allow setting arbitrary config values
215
+ config.set(key, value)
216
+ return true
217
+ }
218
+
219
+ /**
220
+ * Delete a specific configuration value.
221
+ * @param {string} key
222
+ * @returns {boolean} - True if deleted
223
+ */
224
+ export function deleteConfigValue(key) {
225
+ const protectedKeys = ["registryUrl", "registry", "timeout", "retries"]
226
+ if (protectedKeys.includes(key)) {
227
+ // Reset to default instead of deleting
228
+ const defaults = {
229
+ registryUrl: DEFAULT_REGISTRY_URL,
230
+ registry: DEFAULT_REGISTRY_URL,
231
+ timeout: REQUEST_TIMEOUT_MS,
232
+ retries: MAX_RETRIES,
233
+ }
234
+ config.set(key === "registry" ? "registryUrl" : key, defaults[key])
235
+ return true
236
+ }
237
+
238
+ config.delete(key)
239
+ return true
240
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * CLI Constants Configuration
3
+ * Centralized configuration for all CLI behavior.
4
+ *
5
+ * @module cli/lib/constants
6
+ */
7
+
8
+ // ============================================================================
9
+ // Network Configuration
10
+ // ============================================================================
11
+
12
+ /** Maximum number of retry attempts for failed requests */
13
+ export const MAX_RETRIES = 3
14
+
15
+ /** Request timeout in milliseconds (30 seconds) */
16
+ export const REQUEST_TIMEOUT_MS = 30_000
17
+
18
+ /** Base delay for exponential backoff in milliseconds */
19
+ export const RETRY_BASE_DELAY_MS = 1_000
20
+
21
+ /** Maximum delay between retries in milliseconds */
22
+ export const RETRY_MAX_DELAY_MS = 10_000
23
+
24
+ /** Multiplier for exponential backoff */
25
+ export const RETRY_BACKOFF_MULTIPLIER = 2
26
+
27
+ // ============================================================================
28
+ // Cache Configuration
29
+ // ============================================================================
30
+
31
+ /** Cache directory name (relative to user's home) */
32
+ export const CACHE_DIR_NAME = ".lpm-cache"
33
+
34
+ /** Maximum cache size in bytes (500 MB) */
35
+ export const MAX_CACHE_SIZE_BYTES = 500 * 1024 * 1024
36
+
37
+ /** Cache entry TTL in milliseconds (7 days) */
38
+ export const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000
39
+
40
+ // ============================================================================
41
+ // Security Configuration
42
+ // ============================================================================
43
+
44
+ /** Keytar service name for credential storage */
45
+ export const KEYTAR_SERVICE_NAME = "lpm-cli"
46
+
47
+ /** Keytar account name for token storage */
48
+ export const KEYTAR_ACCOUNT_NAME = "auth-token"
49
+
50
+ /** Token scopes that allow publishing */
51
+ export const PUBLISH_SCOPES = ["publish", "write", "full"]
52
+
53
+ /** Token scopes that allow reading */
54
+ export const READ_SCOPES = ["read", "publish", "write", "full"]
55
+
56
+ // ============================================================================
57
+ // API Configuration
58
+ // ============================================================================
59
+
60
+ /** Default registry URL */
61
+ export const DEFAULT_REGISTRY_URL = "https://lpm.dev"
62
+
63
+ /** API version prefix */
64
+ export const API_VERSION = "v1"
65
+
66
+ /** HTTP status codes that trigger retries */
67
+ export const RETRYABLE_STATUS_CODES = [408, 429, 500, 502, 503, 504]
68
+
69
+ /** HTTP status codes that indicate rate limiting */
70
+ export const RATE_LIMIT_STATUS_CODES = [429]
71
+
72
+ // ============================================================================
73
+ // CLI Configuration
74
+ // ============================================================================
75
+
76
+ /** CLI name for display purposes */
77
+ export const CLI_NAME = "lpm"
78
+
79
+ /** Default pagination limit for list commands */
80
+ export const DEFAULT_PAGE_LIMIT = 20
81
+
82
+ /** Maximum pagination limit */
83
+ export const MAX_PAGE_LIMIT = 100
84
+
85
+ // ============================================================================
86
+ // File System Configuration
87
+ // ============================================================================
88
+
89
+ /** Default source directory for component extraction */
90
+ export const DEFAULT_COMPONENTS_DIR = "components"
91
+
92
+ /** Allowed file extensions for source code extraction */
93
+ export const ALLOWED_SOURCE_EXTENSIONS = [
94
+ ".js",
95
+ ".jsx",
96
+ ".ts",
97
+ ".tsx",
98
+ ".css",
99
+ ".scss",
100
+ ".json",
101
+ ".md",
102
+ ]
103
+
104
+ /** Maximum file size for source extraction (10 MB) */
105
+ export const MAX_SOURCE_FILE_SIZE_BYTES = 10 * 1024 * 1024
106
+
107
+ // ============================================================================
108
+ // Integrity Verification
109
+ // ============================================================================
110
+
111
+ /** Default hash algorithm for tarball verification */
112
+ export const DEFAULT_HASH_ALGORITHM = "sha512"
113
+
114
+ /** Supported hash algorithms */
115
+ export const SUPPORTED_HASH_ALGORITHMS = ["sha256", "sha384", "sha512"]
116
+
117
+ // ============================================================================
118
+ // Spinner Messages
119
+ // ============================================================================
120
+
121
+ export const SPINNER_MESSAGES = {
122
+ authenticating: "Authenticating...",
123
+ downloading: "Downloading package...",
124
+ extracting: "Extracting files...",
125
+ publishing: "Publishing package...",
126
+ verifying: "Verifying integrity...",
127
+ retrying: (attempt, max) => `Retrying (${attempt}/${max})...`,
128
+ rateLimited: seconds => `Rate limited. Waiting ${seconds}s...`,
129
+ readingConfig: "Reading package configuration...",
130
+ filteringFiles: "Filtering files based on configuration...",
131
+ }
132
+
133
+ // ============================================================================
134
+ // Error Messages
135
+ // ============================================================================
136
+
137
+ export const ERROR_MESSAGES = {
138
+ notAuthenticated: "Not authenticated. Run `lpm login` first.",
139
+ tokenExpired: "Token expired. Run `lpm login` to refresh.",
140
+ tokenMissingScope: scope =>
141
+ `Token missing required scope: ${scope}. Run \`lpm token-rotate --scope ${scope}\` to fix.`,
142
+ networkError: "Network error. Check your connection and try again.",
143
+ rateLimited: "Rate limited. Please wait and try again.",
144
+ integrityMismatch:
145
+ "Package integrity check failed. Download may be corrupted.",
146
+ pathTraversal: "Invalid path: path traversal detected.",
147
+ timeout:
148
+ "Request timed out. Try again or increase timeout with `lpm config set timeout <ms>`.",
149
+ invalidLpmConfig: "Invalid lpm.config.json: ",
150
+ invalidConfigValue: (key, value, allowed) =>
151
+ `Invalid value "${value}" for "${key}". Allowed: ${allowed.join(", ")}`,
152
+ missingRequiredConfig: key =>
153
+ `Required config parameter "${key}" not provided. Use ?${key}=value in the package URL.`,
154
+ }
155
+
156
+ // ============================================================================
157
+ // Warning Messages
158
+ // ============================================================================
159
+
160
+ export const WARNING_MESSAGES = {
161
+ usernameNotSet: "Your personal username is not set.",
162
+ usernameNotSetHint: registryUrl =>
163
+ `Set it to publish packages under your personal owner:\n ${registryUrl}/dashboard/settings`,
164
+ ownerMismatch: owner =>
165
+ `Package owner "@lpm.dev/${owner}" doesn't match your available owners.`,
166
+ // Legacy - kept for backward compatibility
167
+ scopeMismatch: scope =>
168
+ `Package owner "@lpm.dev/${scope}" doesn't match your available owners.`,
169
+ noOrganizations: "You have no organizations.",
170
+ createOrgHint: registryUrl =>
171
+ `Create one at: ${registryUrl}/dashboard/orgs/new`,
172
+ ownerFixHint:
173
+ 'Either:\n 1. Set your username/org slug to match the package owner\n 2. Change package.json "name" to use @lpm.dev/YOUR_OWNER.package-name',
174
+ // Legacy - kept for backward compatibility
175
+ scopeFixHint:
176
+ 'Either:\n 1. Set your username/org slug to match the package owner\n 2. Change package.json "name" to use @lpm.dev/YOUR_OWNER.package-name',
177
+ }
178
+
179
+ // ============================================================================
180
+ // Success Messages
181
+ // ============================================================================
182
+
183
+ export const SUCCESS_MESSAGES = {
184
+ // Updated for new format: @lpm.dev/owner.package-name
185
+ // owner = username or org slug, pkgName = package name (without owner prefix)
186
+ publishPersonal: (registryUrl, owner, pkgName, version) =>
187
+ `Successfully published @lpm.dev/${owner}.${pkgName}@${version}\n ${registryUrl}/${owner}.${pkgName}`,
188
+ publishOrg: (registryUrl, owner, pkgName, version) =>
189
+ `Successfully published @lpm.dev/${owner}.${pkgName}@${version}\n ${registryUrl}/${owner}.${pkgName}`,
190
+ }