@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,283 @@
1
+ /**
2
+ * Audit Command
3
+ *
4
+ * Scan project dependencies for known security vulnerabilities.
5
+ *
6
+ * Usage:
7
+ * lpm audit [options]
8
+ * lpm audit fix
9
+ *
10
+ * Options:
11
+ * --json Output in JSON format
12
+ * --level Minimum severity to report (low, moderate, high, critical)
13
+ *
14
+ * @module cli/lib/commands/audit
15
+ */
16
+
17
+ import { existsSync, readFileSync } from "node:fs"
18
+ import { join } from "node:path"
19
+ import chalk from "chalk"
20
+ import ora from "ora"
21
+ import { post } from "../api.js"
22
+
23
+ /**
24
+ * Severity levels in order of priority.
25
+ */
26
+ const SEVERITY_ORDER = ["critical", "high", "moderate", "low", "info"]
27
+
28
+ /**
29
+ * Severity colors.
30
+ */
31
+ const SEVERITY_COLORS = {
32
+ critical: chalk.bgRed.white,
33
+ high: chalk.red,
34
+ moderate: chalk.yellow,
35
+ low: chalk.blue,
36
+ info: chalk.dim,
37
+ }
38
+
39
+ /**
40
+ * Read and parse package.json from current directory.
41
+ * @returns {{ dependencies: Record<string, string>, devDependencies: Record<string, string> } | null}
42
+ */
43
+ function readPackageJson() {
44
+ const packageJsonPath = join(process.cwd(), "package.json")
45
+
46
+ if (!existsSync(packageJsonPath)) {
47
+ return null
48
+ }
49
+
50
+ try {
51
+ const content = readFileSync(packageJsonPath, "utf8")
52
+ return JSON.parse(content)
53
+ } catch {
54
+ return null
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Read and parse package-lock.json for exact versions.
60
+ * @returns {Record<string, { version: string, resolved?: string }> | null}
61
+ */
62
+ function readPackageLock() {
63
+ const lockPath = join(process.cwd(), "package-lock.json")
64
+
65
+ if (!existsSync(lockPath)) {
66
+ return null
67
+ }
68
+
69
+ try {
70
+ const content = readFileSync(lockPath, "utf8")
71
+ const lock = JSON.parse(content)
72
+ return lock.packages || lock.dependencies || null
73
+ } catch {
74
+ return null
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Format vulnerability count by severity.
80
+ * @param {Object} counts
81
+ * @returns {string}
82
+ */
83
+ function formatVulnCounts(counts) {
84
+ const parts = []
85
+
86
+ for (const severity of SEVERITY_ORDER) {
87
+ const count = counts[severity] || 0
88
+ if (count > 0) {
89
+ const color = SEVERITY_COLORS[severity] || chalk.dim
90
+ parts.push(color(`${count} ${severity}`))
91
+ }
92
+ }
93
+
94
+ return parts.join(", ") || chalk.green("0 vulnerabilities")
95
+ }
96
+
97
+ /**
98
+ * Execute the audit command.
99
+ *
100
+ * @param {string} [action] - Optional action ('fix')
101
+ * @param {Object} options - Command options
102
+ * @param {boolean} [options.json] - Output as JSON
103
+ * @param {string} [options.level] - Minimum severity level
104
+ */
105
+ export async function audit(action, options = {}) {
106
+ const spinner = ora("Reading dependencies...").start()
107
+
108
+ // Read package.json
109
+ const packageJson = readPackageJson()
110
+
111
+ if (!packageJson) {
112
+ spinner.fail(chalk.red("No package.json found in current directory."))
113
+ process.exit(1)
114
+ }
115
+
116
+ const dependencies = {
117
+ ...(packageJson.dependencies || {}),
118
+ ...(packageJson.devDependencies || {}),
119
+ }
120
+
121
+ const depCount = Object.keys(dependencies).length
122
+
123
+ if (depCount === 0) {
124
+ spinner.succeed(chalk.green("No dependencies to audit."))
125
+ return
126
+ }
127
+
128
+ // Try to get exact versions from lock file
129
+ const lockData = readPackageLock()
130
+
131
+ // Build dependency list for API
132
+ const depList = Object.entries(dependencies).map(([name, version]) => {
133
+ // Try to get exact version from lock
134
+ let exactVersion = version
135
+ if (lockData) {
136
+ const lockEntry = lockData[name] || lockData[`node_modules/${name}`]
137
+ if (lockEntry?.version) {
138
+ exactVersion = lockEntry.version
139
+ }
140
+ }
141
+ return { name, version: exactVersion }
142
+ })
143
+
144
+ spinner.text = `Auditing ${depCount} dependencies...`
145
+
146
+ try {
147
+ const response = await post(
148
+ "/audit",
149
+ { dependencies: depList },
150
+ {
151
+ onRetry: (attempt, max) => {
152
+ spinner.text = `Auditing (retry ${attempt}/${max})...`
153
+ },
154
+ },
155
+ )
156
+
157
+ if (!response.ok) {
158
+ const data = await response.json().catch(() => ({}))
159
+ throw new Error(data.error || "Audit request failed.")
160
+ }
161
+
162
+ const data = await response.json()
163
+ const vulnerabilities = data.vulnerabilities || []
164
+
165
+ spinner.stop()
166
+
167
+ // Filter by severity level if specified
168
+ let filteredVulns = vulnerabilities
169
+ if (options.level) {
170
+ const levelIndex = SEVERITY_ORDER.indexOf(options.level)
171
+ if (levelIndex !== -1) {
172
+ filteredVulns = vulnerabilities.filter(v => {
173
+ const vIndex = SEVERITY_ORDER.indexOf(v.severity)
174
+ return vIndex !== -1 && vIndex <= levelIndex
175
+ })
176
+ }
177
+ }
178
+
179
+ // JSON output
180
+ if (options.json) {
181
+ console.log(
182
+ JSON.stringify(
183
+ {
184
+ scanned: depCount,
185
+ vulnerabilities: filteredVulns,
186
+ counts: data.counts || {},
187
+ },
188
+ null,
189
+ 2,
190
+ ),
191
+ )
192
+ return
193
+ }
194
+
195
+ // No vulnerabilities
196
+ if (filteredVulns.length === 0) {
197
+ console.log(
198
+ chalk.green(
199
+ `\n✓ No vulnerabilities found in ${depCount} dependencies.\n`,
200
+ ),
201
+ )
202
+ return
203
+ }
204
+
205
+ // Display vulnerabilities
206
+ console.log(
207
+ chalk.bold(
208
+ `\nFound ${formatVulnCounts(data.counts || {})} in ${depCount} dependencies.\n`,
209
+ ),
210
+ )
211
+
212
+ // Group by package
213
+ const byPackage = {}
214
+ for (const vuln of filteredVulns) {
215
+ const key = vuln.package
216
+ if (!byPackage[key]) {
217
+ byPackage[key] = []
218
+ }
219
+ byPackage[key].push(vuln)
220
+ }
221
+
222
+ // Display each package
223
+ for (const [pkg, vulns] of Object.entries(byPackage)) {
224
+ console.log(chalk.cyan.bold(pkg))
225
+
226
+ for (const vuln of vulns) {
227
+ const severityColor = SEVERITY_COLORS[vuln.severity] || chalk.dim
228
+ const severity = severityColor(vuln.severity.padEnd(10))
229
+ const title = vuln.title || vuln.id || "Unknown vulnerability"
230
+
231
+ console.log(` ${severity} ${title}`)
232
+
233
+ if (vuln.vulnerable_versions) {
234
+ console.log(chalk.dim(` Vulnerable: ${vuln.vulnerable_versions}`))
235
+ }
236
+
237
+ if (vuln.patched_versions) {
238
+ console.log(
239
+ chalk.green(` Fix: Upgrade to ${vuln.patched_versions}`),
240
+ )
241
+ }
242
+
243
+ if (vuln.url) {
244
+ console.log(chalk.dim(` More info: ${vuln.url}`))
245
+ }
246
+ }
247
+
248
+ console.log("")
249
+ }
250
+
251
+ // Fix suggestion
252
+ if (action !== "fix") {
253
+ console.log(chalk.dim("Run `lpm audit fix` to attempt automatic fixes."))
254
+ console.log("")
255
+ }
256
+
257
+ // Handle fix action
258
+ if (action === "fix") {
259
+ console.log(chalk.yellow("Automatic fix is not yet implemented."))
260
+ console.log(
261
+ chalk.dim(
262
+ "Please update vulnerable packages manually based on the recommendations above.",
263
+ ),
264
+ )
265
+ console.log("")
266
+ }
267
+
268
+ // Exit with error code if vulnerabilities found
269
+ if (
270
+ filteredVulns.some(
271
+ v => v.severity === "critical" || v.severity === "high",
272
+ )
273
+ ) {
274
+ process.exit(1)
275
+ }
276
+ } catch (error) {
277
+ spinner.fail(chalk.red("Audit failed."))
278
+ console.error(chalk.red(` ${error.message}`))
279
+ process.exit(1)
280
+ }
281
+ }
282
+
283
+ export default audit
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Cache Command
3
+ *
4
+ * Manage the local package cache.
5
+ *
6
+ * Usage:
7
+ * lpm cache clean Clear all cached packages
8
+ * lpm cache list Show cached packages with sizes
9
+ * lpm cache path Show cache directory location
10
+ *
11
+ * @module cli/lib/commands/cache
12
+ */
13
+
14
+ import { existsSync, readdirSync, rmSync, statSync } from "node:fs"
15
+ import { homedir } from "node:os"
16
+ import { join } from "node:path"
17
+ import chalk from "chalk"
18
+ import ora from "ora"
19
+ import { CACHE_DIR_NAME } from "../constants.js"
20
+
21
+ /** Full path to cache directory */
22
+ const CACHE_DIR = join(homedir(), CACHE_DIR_NAME)
23
+
24
+ /**
25
+ * Format bytes to human-readable size.
26
+ * @param {number} bytes
27
+ * @returns {string}
28
+ */
29
+ function formatSize(bytes) {
30
+ if (bytes === 0) return "0 B"
31
+
32
+ const units = ["B", "KB", "MB", "GB"]
33
+ const k = 1024
34
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
35
+
36
+ return `${(bytes / k ** i).toFixed(2)} ${units[i]}`
37
+ }
38
+
39
+ /**
40
+ * Get the size of a directory recursively.
41
+ * @param {string} dirPath
42
+ * @returns {number} - Total size in bytes
43
+ */
44
+ function getDirectorySize(dirPath) {
45
+ if (!existsSync(dirPath)) return 0
46
+
47
+ let totalSize = 0
48
+
49
+ const entries = readdirSync(dirPath, { withFileTypes: true })
50
+
51
+ for (const entry of entries) {
52
+ const fullPath = join(dirPath, entry.name)
53
+
54
+ if (entry.isDirectory()) {
55
+ totalSize += getDirectorySize(fullPath)
56
+ } else if (entry.isFile()) {
57
+ totalSize += statSync(fullPath).size
58
+ }
59
+ }
60
+
61
+ return totalSize
62
+ }
63
+
64
+ /**
65
+ * List cached packages with their sizes.
66
+ * @param {string} dirPath
67
+ * @param {string} [prefix='']
68
+ * @returns {{ name: string, size: number }[]}
69
+ */
70
+ function listCacheEntries(dirPath, prefix = "") {
71
+ if (!existsSync(dirPath)) return []
72
+
73
+ const entries = []
74
+ const items = readdirSync(dirPath, { withFileTypes: true })
75
+
76
+ for (const item of items) {
77
+ const fullPath = join(dirPath, item.name)
78
+ const name = prefix ? `${prefix}/${item.name}` : item.name
79
+
80
+ if (item.isDirectory()) {
81
+ // Check if this is a package directory (has .tgz files)
82
+ const contents = readdirSync(fullPath)
83
+ const hasTarballs = contents.some(f => f.endsWith(".tgz"))
84
+
85
+ if (hasTarballs) {
86
+ entries.push({
87
+ name,
88
+ size: getDirectorySize(fullPath),
89
+ versions: contents.filter(f => f.endsWith(".tgz")).length,
90
+ })
91
+ } else {
92
+ // Recurse into scope directories
93
+ entries.push(...listCacheEntries(fullPath, name))
94
+ }
95
+ }
96
+ }
97
+
98
+ return entries
99
+ }
100
+
101
+ /**
102
+ * Clear all cached packages.
103
+ */
104
+ export async function clearCache() {
105
+ const spinner = ora("Clearing cache...").start()
106
+
107
+ try {
108
+ if (!existsSync(CACHE_DIR)) {
109
+ spinner.info("Cache is already empty.")
110
+ return
111
+ }
112
+
113
+ const sizeBefore = getDirectorySize(CACHE_DIR)
114
+
115
+ rmSync(CACHE_DIR, { recursive: true, force: true })
116
+
117
+ spinner.succeed(
118
+ chalk.green(`Cleared ${formatSize(sizeBefore)} from cache.`),
119
+ )
120
+ } catch (error) {
121
+ spinner.fail(chalk.red("Failed to clear cache."))
122
+ console.error(chalk.red(` ${error.message}`))
123
+ process.exit(1)
124
+ }
125
+ }
126
+
127
+ /**
128
+ * List cached packages.
129
+ */
130
+ function listCachedPackages() {
131
+ console.log(chalk.bold("\nCached Packages:\n"))
132
+
133
+ if (!existsSync(CACHE_DIR)) {
134
+ console.log(chalk.dim(" No packages cached."))
135
+ console.log("")
136
+ return
137
+ }
138
+
139
+ const entries = listCacheEntries(CACHE_DIR)
140
+
141
+ if (entries.length === 0) {
142
+ console.log(chalk.dim(" No packages cached."))
143
+ console.log("")
144
+ return
145
+ }
146
+
147
+ // Sort by size descending
148
+ entries.sort((a, b) => b.size - a.size)
149
+
150
+ const totalSize = entries.reduce((sum, e) => sum + e.size, 0)
151
+
152
+ // Display each package
153
+ const maxNameLength = Math.max(...entries.map(e => e.name.length))
154
+
155
+ for (const entry of entries) {
156
+ const paddedName = entry.name.padEnd(maxNameLength)
157
+ const size = formatSize(entry.size).padStart(10)
158
+ const versions = chalk.dim(
159
+ `(${entry.versions} version${entry.versions > 1 ? "s" : ""})`,
160
+ )
161
+ console.log(` ${chalk.cyan(paddedName)} ${size} ${versions}`)
162
+ }
163
+
164
+ console.log("")
165
+ console.log(
166
+ chalk.bold(
167
+ ` Total: ${formatSize(totalSize)} in ${entries.length} package${entries.length > 1 ? "s" : ""}`,
168
+ ),
169
+ )
170
+ console.log("")
171
+ }
172
+
173
+ /**
174
+ * Show cache directory path.
175
+ */
176
+ function showCachePath() {
177
+ console.log(CACHE_DIR)
178
+ }
179
+
180
+ /**
181
+ * Execute the cache command.
182
+ *
183
+ * @param {string} action - The action to perform (clean, list, path)
184
+ */
185
+ export async function cache(action) {
186
+ switch (action) {
187
+ case "clean":
188
+ case "clear":
189
+ await clearCache()
190
+ break
191
+
192
+ case "list":
193
+ case "ls":
194
+ listCachedPackages()
195
+ break
196
+
197
+ case "path":
198
+ case "dir":
199
+ showCachePath()
200
+ break
201
+
202
+ default:
203
+ console.error(chalk.red(`Unknown action: ${action}`))
204
+ console.log(chalk.dim("Available actions: clean, list, path"))
205
+ process.exit(1)
206
+ }
207
+ }
208
+
209
+ export default cache
@@ -0,0 +1,112 @@
1
+ import chalk from "chalk"
2
+ import ora from "ora"
3
+ import { get } from "../api.js"
4
+
5
+ /**
6
+ * Validate the package name format.
7
+ * Must be owner.package-name (no @lpm.dev/ prefix needed, but accepted).
8
+ * @param {string} input
9
+ * @returns {{ owner: string, name: string } | null}
10
+ */
11
+ function parseName(input) {
12
+ let cleaned = input
13
+ if (cleaned.startsWith("@lpm.dev/")) {
14
+ cleaned = cleaned.replace("@lpm.dev/", "")
15
+ }
16
+ const dotIndex = cleaned.indexOf(".")
17
+ if (dotIndex === -1 || dotIndex === 0 || dotIndex === cleaned.length - 1) {
18
+ return null
19
+ }
20
+ return {
21
+ owner: cleaned.substring(0, dotIndex),
22
+ name: cleaned.substring(dotIndex + 1),
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Check if a package name is available on the LPM registry.
28
+ *
29
+ * @param {string} nameInput - Package name (e.g., "owner.package-name" or "@lpm.dev/owner.package-name")
30
+ * @param {Object} [options]
31
+ * @param {boolean} [options.json] - Output as JSON
32
+ */
33
+ export async function checkName(nameInput, options = {}) {
34
+ if (!nameInput || nameInput.trim() === "") {
35
+ if (options.json) {
36
+ console.log(JSON.stringify({ error: "Package name required." }))
37
+ } else {
38
+ console.error(chalk.red("Error: Package name required."))
39
+ console.log(chalk.dim("Usage: lpm check-name owner.package-name"))
40
+ }
41
+ process.exit(1)
42
+ }
43
+
44
+ const parsed = parseName(nameInput.trim())
45
+ if (!parsed) {
46
+ if (options.json) {
47
+ console.log(
48
+ JSON.stringify({
49
+ error: "Invalid package name format. Use: owner.package-name",
50
+ }),
51
+ )
52
+ } else {
53
+ console.error(chalk.red("Error: Invalid package name format."))
54
+ console.log(chalk.dim("Expected: owner.package-name"))
55
+ console.log(chalk.dim("Example: lpm check-name tolgaergin.my-utils"))
56
+ }
57
+ process.exit(1)
58
+ }
59
+
60
+ const fullName = `@lpm.dev/${parsed.owner}.${parsed.name}`
61
+ const spinner = options.json
62
+ ? null
63
+ : ora(`Checking availability of ${fullName}...`).start()
64
+
65
+ try {
66
+ const queryName = `${parsed.owner}.${parsed.name}`
67
+ const response = await get(
68
+ `/check-name?name=${encodeURIComponent(queryName)}`,
69
+ {
70
+ onRetry: spinner
71
+ ? (attempt, max) => {
72
+ spinner.text = `Checking (retry ${attempt}/${max})...`
73
+ }
74
+ : undefined,
75
+ },
76
+ )
77
+
78
+ const data = await response.json().catch(() => ({}))
79
+
80
+ if (!response.ok) {
81
+ throw new Error(data.error || `Unexpected response: ${response.status}`)
82
+ }
83
+
84
+ if (options.json) {
85
+ console.log(JSON.stringify(data, null, 2))
86
+ return
87
+ }
88
+
89
+ if (data.available) {
90
+ spinner.succeed(chalk.green(`${fullName} is available!`))
91
+ if (!data.ownerExists) {
92
+ console.log(
93
+ chalk.yellow(
94
+ ` Note: Owner "${parsed.owner}" doesn't exist yet. You'll need to create this account first.`,
95
+ ),
96
+ )
97
+ }
98
+ } else {
99
+ spinner.fail(chalk.red(`${fullName} is already taken.`))
100
+ }
101
+ } catch (error) {
102
+ if (spinner) spinner.fail(chalk.red("Failed to check name availability."))
103
+ if (options.json) {
104
+ console.log(JSON.stringify({ error: error.message }))
105
+ } else {
106
+ console.error(chalk.red(` ${error.message}`))
107
+ }
108
+ process.exit(1)
109
+ }
110
+ }
111
+
112
+ export default checkName