@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,178 @@
1
+ /**
2
+ * VS Code Extension Install Target
3
+ *
4
+ * Handles `lpm add` for packages with `type: "vscode-extension"` in lpm.config.json.
5
+ *
6
+ * Instead of copying source files into the project, this handler:
7
+ * 1. Copies the extracted package into ~/.vscode/extensions/ with the correct naming
8
+ * 2. VS Code auto-detects the new extension on restart
9
+ *
10
+ * @module cli/lib/install-targets/vscode-extension
11
+ */
12
+
13
+ import fs from "node:fs"
14
+ import os from "node:os"
15
+ import path from "node:path"
16
+ import * as p from "@clack/prompts"
17
+ import chalk from "chalk"
18
+
19
+ /**
20
+ * Get the VS Code extensions directory (cross-platform).
21
+ * @returns {string}
22
+ */
23
+ function getExtensionsDir() {
24
+ return path.join(os.homedir(), ".vscode", "extensions")
25
+ }
26
+
27
+ /**
28
+ * Derive the extension folder name from the package name and version.
29
+ *
30
+ * LPM format: @lpm.dev/owner.my-extension → owner.my-extension-1.0.0
31
+ * This matches VS Code's publisher.extension-version convention.
32
+ *
33
+ * @param {string} pkgName - Full package reference (e.g., "@lpm.dev/owner.my-extension")
34
+ * @param {string} version - Package version
35
+ * @returns {string}
36
+ */
37
+ function deriveExtensionFolderName(pkgName, version) {
38
+ // Strip @lpm.dev/ prefix → "owner.my-extension"
39
+ const baseName = pkgName.replace("@lpm.dev/", "")
40
+ return `${baseName}-${version}`
41
+ }
42
+
43
+ /**
44
+ * Recursively copy a directory.
45
+ *
46
+ * @param {string} src - Source directory
47
+ * @param {string} dest - Destination directory
48
+ */
49
+ function copyDirRecursive(src, dest) {
50
+ fs.mkdirSync(dest, { recursive: true })
51
+ const entries = fs.readdirSync(src, { withFileTypes: true })
52
+
53
+ for (const entry of entries) {
54
+ const srcPath = path.join(src, entry.name)
55
+ const destPath = path.join(dest, entry.name)
56
+
57
+ if (entry.isDirectory()) {
58
+ copyDirRecursive(srcPath, destPath)
59
+ } else {
60
+ fs.copyFileSync(srcPath, destPath)
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Install a VS Code extension package.
67
+ *
68
+ * Called by `lpm add` when the package has `type: "vscode-extension"` in lpm.config.json.
69
+ *
70
+ * @param {object} params
71
+ * @param {string} params.name - Package name (e.g., "@lpm.dev/owner.my-extension")
72
+ * @param {string} params.version - Package version
73
+ * @param {object} params.lpmConfig - Parsed lpm.config.json
74
+ * @param {string} params.extractDir - Temp directory with extracted package files
75
+ * @param {object} params.options - CLI options (force, yes)
76
+ * @returns {Promise<{ success: boolean, message: string }>}
77
+ */
78
+ export async function installVscodeExtension({
79
+ name,
80
+ version,
81
+ lpmConfig: _lpmConfig,
82
+ extractDir,
83
+ options,
84
+ }) {
85
+ const extensionsDir = getExtensionsDir()
86
+ const folderName = deriveExtensionFolderName(name, version)
87
+ const targetDir = path.join(extensionsDir, folderName)
88
+
89
+ // Check if VS Code extensions directory exists
90
+ if (!fs.existsSync(path.join(os.homedir(), ".vscode"))) {
91
+ return {
92
+ success: false,
93
+ message:
94
+ "VS Code not detected. Install VS Code first (no ~/.vscode directory found).",
95
+ }
96
+ }
97
+
98
+ // Check for existing version
99
+ if (fs.existsSync(targetDir)) {
100
+ if (!options?.force && !options?.yes) {
101
+ const overwrite = await p.confirm({
102
+ message: `Extension ${folderName} already exists. Overwrite?`,
103
+ initialValue: false,
104
+ })
105
+
106
+ if (p.isCancel(overwrite) || !overwrite) {
107
+ return {
108
+ success: false,
109
+ message: "Installation cancelled.",
110
+ }
111
+ }
112
+ }
113
+
114
+ // Remove existing version
115
+ fs.rmSync(targetDir, { recursive: true, force: true })
116
+ }
117
+
118
+ // Copy extension files to the extensions directory
119
+ try {
120
+ copyDirRecursive(extractDir, targetDir)
121
+ } catch (err) {
122
+ return {
123
+ success: false,
124
+ message: `Failed to install extension: ${err.message}`,
125
+ }
126
+ }
127
+
128
+ return {
129
+ success: true,
130
+ message: `VS Code extension installed to ${chalk.dim(targetDir)}. Restart VS Code to activate.`,
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Remove a VS Code extension package.
136
+ *
137
+ * @param {object} params
138
+ * @param {string} params.name - Package name
139
+ * @returns {Promise<{ success: boolean, message: string }>}
140
+ */
141
+ export async function removeVscodeExtension({ name }) {
142
+ const extensionsDir = getExtensionsDir()
143
+ const baseName = name.replace("@lpm.dev/", "")
144
+
145
+ // Find matching extension folders (any version)
146
+ if (!fs.existsSync(extensionsDir)) {
147
+ return {
148
+ success: true,
149
+ message: "No VS Code extensions directory found.",
150
+ }
151
+ }
152
+
153
+ const entries = fs.readdirSync(extensionsDir)
154
+ const matching = entries.filter(e => e.startsWith(`${baseName}-`))
155
+
156
+ if (matching.length === 0) {
157
+ return {
158
+ success: true,
159
+ message: "Extension was not installed via LPM.",
160
+ }
161
+ }
162
+
163
+ let count = 0
164
+ for (const folder of matching) {
165
+ const fullPath = path.join(extensionsDir, folder)
166
+ try {
167
+ fs.rmSync(fullPath, { recursive: true, force: true })
168
+ count++
169
+ } catch (err) {
170
+ console.error(chalk.red(` Failed to remove ${folder}: ${err.message}`))
171
+ }
172
+ }
173
+
174
+ return {
175
+ success: true,
176
+ message: `Removed ${count} extension version${count > 1 ? "s" : ""}. Restart VS Code to apply.`,
177
+ }
178
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Install Target Resolver
3
+ *
4
+ * Routes package types to type-specific install handlers.
5
+ * When `lpm add` encounters a package with a `type` in lpm.config.json,
6
+ * it delegates to the appropriate handler instead of the default file-copy flow.
7
+ *
8
+ * @module cli/lib/install-targets
9
+ */
10
+
11
+ import {
12
+ installMcpServer,
13
+ removeMcpServer,
14
+ } from "./install-targets/mcp-server.js"
15
+ import {
16
+ installVscodeExtension,
17
+ removeVscodeExtension,
18
+ } from "./install-targets/vscode-extension.js"
19
+
20
+ /**
21
+ * Registry of package types that have custom install behavior.
22
+ * Types not listed here fall through to the default source file-copy flow.
23
+ */
24
+ const INSTALL_HANDLERS = {
25
+ "mcp-server": {
26
+ install: installMcpServer,
27
+ remove: removeMcpServer,
28
+ },
29
+ "vscode-extension": {
30
+ install: installVscodeExtension,
31
+ remove: removeVscodeExtension,
32
+ },
33
+ }
34
+
35
+ /**
36
+ * Default install target paths for package types that use the standard
37
+ * file-copy flow but with a well-known destination.
38
+ *
39
+ * When a package has a type listed here and the user doesn't provide --path,
40
+ * the CLI skips the "Where to install?" prompt and uses this default.
41
+ *
42
+ * If the package's files[] rules define explicit `dest` paths, targetDir is
43
+ * set to the project root (since dest is relative to targetDir).
44
+ * Otherwise, targetDir is set to this default path.
45
+ */
46
+ const DEFAULT_TARGETS = {
47
+ "cursor-rules": ".cursor/rules",
48
+ "github-action": ".github",
49
+ }
50
+
51
+ /**
52
+ * Check if a package type has a custom install handler.
53
+ *
54
+ * @param {string} type - Package type from lpm.config.json
55
+ * @returns {boolean}
56
+ */
57
+ export function hasCustomHandler(type) {
58
+ return !!INSTALL_HANDLERS[type]
59
+ }
60
+
61
+ /**
62
+ * Get the install handler for a package type.
63
+ *
64
+ * @param {string} type - Package type from lpm.config.json
65
+ * @returns {{ install: Function, remove: Function } | null}
66
+ */
67
+ export function getHandler(type) {
68
+ return INSTALL_HANDLERS[type] || null
69
+ }
70
+
71
+ /**
72
+ * Get the default install target path for a package type.
73
+ *
74
+ * Returns a path relative to the project root. Types not listed here
75
+ * use the standard interactive prompt to determine the install path.
76
+ *
77
+ * @param {string} type - Package type from lpm.config.json
78
+ * @returns {string | null} Relative path or null if no default
79
+ */
80
+ export function getDefaultTarget(type) {
81
+ return DEFAULT_TARGETS[type] || null
82
+ }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Tarball Integrity Verification
3
+ *
4
+ * Provides cryptographic hash verification for downloaded packages.
5
+ * Supports SHA-256, SHA-384, and SHA-512 algorithms.
6
+ *
7
+ * @module cli/lib/integrity
8
+ */
9
+
10
+ import { createHash } from "node:crypto"
11
+ import {
12
+ DEFAULT_HASH_ALGORITHM,
13
+ ERROR_MESSAGES,
14
+ SUPPORTED_HASH_ALGORITHMS,
15
+ } from "./constants.js"
16
+
17
+ /**
18
+ * Parse an integrity string (SRI format).
19
+ * Format: algorithm-base64hash
20
+ * Example: sha512-abc123...
21
+ *
22
+ * @param {string} integrity - The integrity string
23
+ * @returns {{ algorithm: string, hash: string } | null}
24
+ */
25
+ export function parseIntegrity(integrity) {
26
+ if (!integrity || typeof integrity !== "string") {
27
+ return null
28
+ }
29
+
30
+ const match = integrity.match(/^(sha256|sha384|sha512)-(.+)$/i)
31
+ if (!match) {
32
+ return null
33
+ }
34
+
35
+ const [, algorithm, hash] = match
36
+ return {
37
+ algorithm: algorithm.toLowerCase(),
38
+ hash,
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Calculate the hash of a buffer.
44
+ *
45
+ * @param {Buffer} buffer - The data to hash
46
+ * @param {string} [algorithm='sha512'] - Hash algorithm
47
+ * @returns {string} - Base64 encoded hash
48
+ */
49
+ export function calculateHash(buffer, algorithm = DEFAULT_HASH_ALGORITHM) {
50
+ if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) {
51
+ throw new Error(`Unsupported hash algorithm: ${algorithm}`)
52
+ }
53
+
54
+ return createHash(algorithm).update(buffer).digest("base64")
55
+ }
56
+
57
+ /**
58
+ * Generate an integrity string (SRI format) for a buffer.
59
+ *
60
+ * @param {Buffer} buffer - The data to hash
61
+ * @param {string} [algorithm='sha512'] - Hash algorithm
62
+ * @returns {string} - Integrity string (e.g., 'sha512-abc123...')
63
+ */
64
+ export function generateIntegrity(buffer, algorithm = DEFAULT_HASH_ALGORITHM) {
65
+ const hash = calculateHash(buffer, algorithm)
66
+ return `${algorithm}-${hash}`
67
+ }
68
+
69
+ /**
70
+ * Verify the integrity of a buffer against an expected hash.
71
+ *
72
+ * @param {Buffer} buffer - The data to verify
73
+ * @param {string} expectedIntegrity - Expected integrity string (SRI format)
74
+ * @returns {{ valid: boolean, error?: string, actual?: string }}
75
+ */
76
+ export function verifyIntegrity(buffer, expectedIntegrity) {
77
+ const parsed = parseIntegrity(expectedIntegrity)
78
+
79
+ if (!parsed) {
80
+ return {
81
+ valid: false,
82
+ error: "Invalid integrity format. Expected format: algorithm-base64hash",
83
+ }
84
+ }
85
+
86
+ const { algorithm, hash: expectedHash } = parsed
87
+ const actualHash = calculateHash(buffer, algorithm)
88
+
89
+ if (actualHash !== expectedHash) {
90
+ return {
91
+ valid: false,
92
+ error: ERROR_MESSAGES.integrityMismatch,
93
+ actual: `${algorithm}-${actualHash}`,
94
+ }
95
+ }
96
+
97
+ return { valid: true }
98
+ }
99
+
100
+ /**
101
+ * Verify integrity with multiple allowed hashes.
102
+ * Useful when a package may have multiple valid integrity values.
103
+ *
104
+ * @param {Buffer} buffer - The data to verify
105
+ * @param {string[]} integrities - Array of valid integrity strings
106
+ * @returns {{ valid: boolean, matchedIntegrity?: string, error?: string }}
107
+ */
108
+ export function verifyIntegrityMultiple(buffer, integrities) {
109
+ if (!integrities || integrities.length === 0) {
110
+ return { valid: false, error: "No integrity values provided" }
111
+ }
112
+
113
+ for (const integrity of integrities) {
114
+ const result = verifyIntegrity(buffer, integrity)
115
+ if (result.valid) {
116
+ return { valid: true, matchedIntegrity: integrity }
117
+ }
118
+ }
119
+
120
+ return {
121
+ valid: false,
122
+ error: ERROR_MESSAGES.integrityMismatch,
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Create a streaming hash verifier.
128
+ * Useful for large files where buffering the entire file is not practical.
129
+ *
130
+ * @param {string} [algorithm='sha512'] - Hash algorithm
131
+ * @returns {{ update: (chunk: Buffer) => void, verify: (expectedIntegrity: string) => { valid: boolean, error?: string } }}
132
+ */
133
+ export function createStreamVerifier(algorithm = DEFAULT_HASH_ALGORITHM) {
134
+ const hash = createHash(algorithm)
135
+
136
+ return {
137
+ /**
138
+ * Update the hash with a chunk of data.
139
+ * @param {Buffer} chunk
140
+ */
141
+ update(chunk) {
142
+ hash.update(chunk)
143
+ },
144
+
145
+ /**
146
+ * Finalize and verify against expected integrity.
147
+ * @param {string} expectedIntegrity
148
+ * @returns {{ valid: boolean, error?: string, actual?: string }}
149
+ */
150
+ verify(expectedIntegrity) {
151
+ const parsed = parseIntegrity(expectedIntegrity)
152
+ if (!parsed) {
153
+ return {
154
+ valid: false,
155
+ error: "Invalid integrity format",
156
+ }
157
+ }
158
+
159
+ // Algorithm must match
160
+ if (parsed.algorithm !== algorithm) {
161
+ return {
162
+ valid: false,
163
+ error: `Algorithm mismatch: expected ${parsed.algorithm}, got ${algorithm}`,
164
+ }
165
+ }
166
+
167
+ const actualHash = hash.digest("base64")
168
+ if (actualHash !== parsed.hash) {
169
+ return {
170
+ valid: false,
171
+ error: ERROR_MESSAGES.integrityMismatch,
172
+ actual: `${algorithm}-${actualHash}`,
173
+ }
174
+ }
175
+
176
+ return { valid: true }
177
+ },
178
+ }
179
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Interactive prompts for LPM package configuration.
3
+ *
4
+ * Generates @clack/prompts calls from a configSchema definition.
5
+ * Only prompts for parameters not already provided via URL params.
6
+ *
7
+ * @module cli/lib/lpm-config-prompts
8
+ */
9
+
10
+ import * as p from "@clack/prompts"
11
+
12
+ /**
13
+ * Prompt the user for missing config parameters.
14
+ *
15
+ * Iterates over configSchema entries. For each key NOT in inlineConfig,
16
+ * shows an appropriate prompt based on the field type.
17
+ *
18
+ * @param {Record<string, object>} configSchema - Config schema from lpm.config.json
19
+ * @param {Record<string, string>} inlineConfig - Already-provided params from URL
20
+ * @param {Record<string, *>} defaultConfig - Default values
21
+ * @returns {Promise<Record<string, *>>} User answers for missing params
22
+ */
23
+ export async function promptForMissingConfig(
24
+ configSchema,
25
+ inlineConfig,
26
+ defaultConfig,
27
+ ) {
28
+ const answers = {}
29
+ const missingKeys = Object.keys(configSchema).filter(
30
+ key => !(key in inlineConfig),
31
+ )
32
+
33
+ if (missingKeys.length === 0) return answers
34
+
35
+ for (const key of missingKeys) {
36
+ const schema = configSchema[key]
37
+ const defaultValue = defaultConfig?.[key] ?? schema.default
38
+
39
+ if (schema.type === "boolean") {
40
+ const result = await p.confirm({
41
+ message: schema.label || `Enable ${key}?`,
42
+ initialValue: defaultValue ?? false,
43
+ })
44
+
45
+ if (p.isCancel(result)) {
46
+ p.cancel("Operation cancelled.")
47
+ process.exit(0)
48
+ }
49
+
50
+ answers[key] = String(result)
51
+ } else if (schema.type === "select" && schema.multiSelect) {
52
+ const options = (schema.options || []).map(opt => {
53
+ if (typeof opt === "string") {
54
+ return { value: opt, label: opt }
55
+ }
56
+ return { value: opt.value, label: opt.label || opt.value }
57
+ })
58
+
59
+ const allValues = options.map(opt => opt.value)
60
+ const result = await p.multiselect({
61
+ message: schema.label || `Select ${key}:`,
62
+ options,
63
+ initialValues: Array.isArray(defaultValue)
64
+ ? defaultValue
65
+ : defaultValue
66
+ ? [defaultValue]
67
+ : allValues,
68
+ required: false,
69
+ })
70
+
71
+ if (p.isCancel(result)) {
72
+ p.cancel("Operation cancelled.")
73
+ process.exit(0)
74
+ }
75
+
76
+ // Join as comma-separated string (matches URL param format)
77
+ answers[key] = Array.isArray(result) ? result.join(",") : result
78
+ } else if (schema.type === "select") {
79
+ const options = (schema.options || []).map(opt => {
80
+ if (typeof opt === "string") {
81
+ return { value: opt, label: opt }
82
+ }
83
+ return { value: opt.value, label: opt.label || opt.value }
84
+ })
85
+
86
+ const result = await p.select({
87
+ message: schema.label || `Select ${key}:`,
88
+ options,
89
+ initialValue: defaultValue,
90
+ })
91
+
92
+ if (p.isCancel(result)) {
93
+ p.cancel("Operation cancelled.")
94
+ process.exit(0)
95
+ }
96
+
97
+ answers[key] = result
98
+ }
99
+ }
100
+
101
+ return answers
102
+ }