@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.
- package/CHANGELOG.md +36 -0
- package/LICENSE +15 -0
- package/README.md +406 -0
- package/bin/lpm.js +334 -0
- package/index.d.ts +131 -0
- package/index.js +31 -0
- package/lib/api.js +324 -0
- package/lib/commands/add.js +1217 -0
- package/lib/commands/audit.js +283 -0
- package/lib/commands/cache.js +209 -0
- package/lib/commands/check-name.js +112 -0
- package/lib/commands/config.js +174 -0
- package/lib/commands/doctor.js +142 -0
- package/lib/commands/info.js +215 -0
- package/lib/commands/init.js +146 -0
- package/lib/commands/install.js +217 -0
- package/lib/commands/login.js +547 -0
- package/lib/commands/logout.js +94 -0
- package/lib/commands/marketplace-compare.js +164 -0
- package/lib/commands/marketplace-earnings.js +89 -0
- package/lib/commands/mcp-setup.js +363 -0
- package/lib/commands/open.js +82 -0
- package/lib/commands/outdated.js +291 -0
- package/lib/commands/pool-stats.js +100 -0
- package/lib/commands/publish.js +707 -0
- package/lib/commands/quality.js +211 -0
- package/lib/commands/remove.js +82 -0
- package/lib/commands/run.js +14 -0
- package/lib/commands/search.js +143 -0
- package/lib/commands/setup.js +92 -0
- package/lib/commands/skills.js +863 -0
- package/lib/commands/token-rotate.js +25 -0
- package/lib/commands/whoami.js +129 -0
- package/lib/config.js +240 -0
- package/lib/constants.js +190 -0
- package/lib/ecosystem.js +501 -0
- package/lib/editors.js +215 -0
- package/lib/import-rewriter.js +364 -0
- package/lib/install-targets/mcp-server.js +245 -0
- package/lib/install-targets/vscode-extension.js +178 -0
- package/lib/install-targets.js +82 -0
- package/lib/integrity.js +179 -0
- package/lib/lpm-config-prompts.js +102 -0
- package/lib/lpm-config.js +408 -0
- package/lib/project-utils.js +152 -0
- package/lib/quality/checks.js +654 -0
- package/lib/quality/display.js +139 -0
- package/lib/quality/score.js +115 -0
- package/lib/quality/swift-checks.js +447 -0
- package/lib/safe-path.js +180 -0
- package/lib/secure-store.js +288 -0
- package/lib/swift-project.js +637 -0
- package/lib/ui.js +40 -0
- 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
|
+
}
|
package/lib/integrity.js
ADDED
|
@@ -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
|
+
}
|