@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,211 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import ora from "ora"
|
|
3
|
+
import { get } from "../api.js"
|
|
4
|
+
|
|
5
|
+
const TIER_COLORS = {
|
|
6
|
+
excellent: chalk.green,
|
|
7
|
+
good: chalk.blue,
|
|
8
|
+
fair: chalk.yellow,
|
|
9
|
+
"needs-work": chalk.gray,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const TIER_LABELS = {
|
|
13
|
+
excellent: "Excellent",
|
|
14
|
+
good: "Good",
|
|
15
|
+
fair: "Fair",
|
|
16
|
+
"needs-work": "Needs Work",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const CATEGORY_LABELS = {
|
|
20
|
+
documentation: "Documentation",
|
|
21
|
+
code: "Code Quality",
|
|
22
|
+
testing: "Testing",
|
|
23
|
+
health: "Package Health",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function progressBar(value, max, width = 18) {
|
|
27
|
+
const ratio = Math.min(value / max, 1)
|
|
28
|
+
const filled = Math.round(ratio * width)
|
|
29
|
+
const empty = width - filled
|
|
30
|
+
return chalk.cyan("█".repeat(filled)) + chalk.gray("░".repeat(empty))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse package name input.
|
|
35
|
+
* Accepts: "owner.pkg", "@lpm.dev/owner.pkg"
|
|
36
|
+
* @param {string} input
|
|
37
|
+
* @returns {string|null} cleaned name as "owner.pkg" or null if invalid
|
|
38
|
+
*/
|
|
39
|
+
function parseName(input) {
|
|
40
|
+
let cleaned = input
|
|
41
|
+
if (cleaned.startsWith("@lpm.dev/")) {
|
|
42
|
+
cleaned = cleaned.replace("@lpm.dev/", "")
|
|
43
|
+
}
|
|
44
|
+
const dotIndex = cleaned.indexOf(".")
|
|
45
|
+
if (dotIndex === -1 || dotIndex === 0 || dotIndex === cleaned.length - 1) {
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
return cleaned
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const TIPS = {
|
|
52
|
+
"has-readme": "Add a README.md with at least 100 characters",
|
|
53
|
+
"readme-install": "Add an install/getting started section to your README",
|
|
54
|
+
"readme-usage": "Add usage examples with code blocks to your README",
|
|
55
|
+
"readme-api": "Add an API/reference section to your README",
|
|
56
|
+
"has-changelog": "Add a CHANGELOG.md file",
|
|
57
|
+
"has-license": "Add a LICENSE file",
|
|
58
|
+
"has-types": 'Add TypeScript types ("types" field or .d.ts files)',
|
|
59
|
+
"intellisense-coverage": "Add .d.ts type definitions or JSDoc comments",
|
|
60
|
+
"esm-exports": 'Add "type": "module" or "exports" to package.json',
|
|
61
|
+
"tree-shakable": 'Add "sideEffects": false to package.json',
|
|
62
|
+
"no-eval": "Remove eval() and new Function() usage",
|
|
63
|
+
"has-engines": 'Add "engines": { "node": ">=18" } to package.json',
|
|
64
|
+
"has-exports-map": 'Add an "exports" map to package.json',
|
|
65
|
+
"small-deps": "Reduce the number of production dependencies",
|
|
66
|
+
"source-maps": "Include .js.map source maps for easier debugging",
|
|
67
|
+
"has-test-files": "Add test files (*.test.js, *.spec.js)",
|
|
68
|
+
"has-test-script": "Add a test script to package.json",
|
|
69
|
+
"has-description": "Add a description (>10 chars) to package.json",
|
|
70
|
+
"has-keywords": "Add keywords to package.json",
|
|
71
|
+
"has-repository": "Add a repository field to package.json",
|
|
72
|
+
"has-homepage": "Add a homepage field to package.json",
|
|
73
|
+
"reasonable-size": "Reduce unpacked size (check for unnecessary files)",
|
|
74
|
+
"no-vulnerabilities": "Fix known vulnerabilities in dependencies",
|
|
75
|
+
"maintenance-health": "Publish updates regularly (within 90 days)",
|
|
76
|
+
"semver-consistency": "Use valid semantic versioning (major.minor.patch)",
|
|
77
|
+
"author-verified": "Link your GitHub or LinkedIn in your profile settings",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Fetch and display the server-side quality report for a package.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} nameInput - Package name (e.g., "owner.pkg" or "@lpm.dev/owner.pkg")
|
|
84
|
+
* @param {Object} [options]
|
|
85
|
+
* @param {boolean} [options.json] - Output as JSON
|
|
86
|
+
*/
|
|
87
|
+
export async function quality(nameInput, options = {}) {
|
|
88
|
+
if (!nameInput || nameInput.trim() === "") {
|
|
89
|
+
if (options.json) {
|
|
90
|
+
console.log(JSON.stringify({ error: "Package name required." }))
|
|
91
|
+
} else {
|
|
92
|
+
console.error(chalk.red("Error: Package name required."))
|
|
93
|
+
console.log(chalk.dim("Usage: lpm quality owner.package-name"))
|
|
94
|
+
}
|
|
95
|
+
process.exit(1)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const cleaned = parseName(nameInput.trim())
|
|
99
|
+
if (!cleaned) {
|
|
100
|
+
if (options.json) {
|
|
101
|
+
console.log(
|
|
102
|
+
JSON.stringify({
|
|
103
|
+
error: "Invalid package name format. Use: owner.package-name",
|
|
104
|
+
}),
|
|
105
|
+
)
|
|
106
|
+
} else {
|
|
107
|
+
console.error(chalk.red("Error: Invalid package name format."))
|
|
108
|
+
console.log(chalk.dim("Expected: owner.package-name"))
|
|
109
|
+
}
|
|
110
|
+
process.exit(1)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const fullName = `@lpm.dev/${cleaned}`
|
|
114
|
+
const spinner = options.json
|
|
115
|
+
? null
|
|
116
|
+
: ora(`Fetching quality report for ${fullName}...`).start()
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const response = await get(`/quality?name=${encodeURIComponent(cleaned)}`, {
|
|
120
|
+
onRetry: spinner
|
|
121
|
+
? (attempt, max) => {
|
|
122
|
+
spinner.text = `Fetching (retry ${attempt}/${max})...`
|
|
123
|
+
}
|
|
124
|
+
: undefined,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const data = await response.json().catch(() => ({}))
|
|
128
|
+
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
throw new Error(data.error || `Request failed: ${response.status}`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (options.json) {
|
|
134
|
+
console.log(JSON.stringify(data, null, 2))
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (spinner) spinner.stop()
|
|
139
|
+
|
|
140
|
+
// No quality data
|
|
141
|
+
if (data.score === null || data.score === undefined) {
|
|
142
|
+
console.log("")
|
|
143
|
+
console.log(chalk.yellow(` ${fullName}: No quality data available.`))
|
|
144
|
+
console.log(
|
|
145
|
+
chalk.dim(" Publish with lpm CLI v0.16+ to generate quality scores."),
|
|
146
|
+
)
|
|
147
|
+
console.log("")
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Display quality report
|
|
152
|
+
const tier = data.tier || "needs-work"
|
|
153
|
+
const tierColor = TIER_COLORS[tier] || chalk.white
|
|
154
|
+
const tierLabel = TIER_LABELS[tier] || tier
|
|
155
|
+
|
|
156
|
+
console.log("")
|
|
157
|
+
console.log(` ${chalk.bold(fullName)}`)
|
|
158
|
+
console.log(
|
|
159
|
+
` ${chalk.bold("Quality Score:")} ${tierColor(`${data.score}/${data.maxScore}`)} ${tierColor(`(${tierLabel})`)}`,
|
|
160
|
+
)
|
|
161
|
+
console.log("")
|
|
162
|
+
|
|
163
|
+
// Category bars
|
|
164
|
+
if (data.categories) {
|
|
165
|
+
for (const [cat, { score: catScore, max }] of Object.entries(
|
|
166
|
+
data.categories,
|
|
167
|
+
)) {
|
|
168
|
+
const label = (CATEGORY_LABELS[cat] || cat).padEnd(16)
|
|
169
|
+
const bar = progressBar(catScore, max)
|
|
170
|
+
console.log(` ${label}${bar} ${catScore}/${max}`)
|
|
171
|
+
}
|
|
172
|
+
console.log("")
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Individual checks
|
|
176
|
+
if (data.checks && data.checks.length > 0) {
|
|
177
|
+
for (const check of data.checks) {
|
|
178
|
+
const icon = check.passed ? chalk.green("✓") : chalk.red("✗")
|
|
179
|
+
const label = check.passed ? check.label : chalk.dim(check.label)
|
|
180
|
+
const points = check.passed
|
|
181
|
+
? chalk.dim(` ${check.points}/${check.maxPoints}`)
|
|
182
|
+
: chalk.dim(` 0/${check.maxPoints}`)
|
|
183
|
+
const detail = check.detail ? chalk.dim(` — ${check.detail}`) : ""
|
|
184
|
+
console.log(` ${icon} ${label}${points}${detail}`)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Tips for failed checks
|
|
188
|
+
const failed = data.checks.filter(c => !c.passed)
|
|
189
|
+
if (failed.length > 0) {
|
|
190
|
+
console.log("")
|
|
191
|
+
const tips = failed.slice(0, 3)
|
|
192
|
+
for (const check of tips) {
|
|
193
|
+
const tip = TIPS[check.id] || `Improve: ${check.id}`
|
|
194
|
+
console.log(chalk.dim(` Tip: ${tip} (+${check.maxPoints} points)`))
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log("")
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (spinner) spinner.fail(chalk.red("Failed to fetch quality report."))
|
|
202
|
+
if (options.json) {
|
|
203
|
+
console.log(JSON.stringify({ error: error.message }))
|
|
204
|
+
} else {
|
|
205
|
+
console.error(chalk.red(` ${error.message}`))
|
|
206
|
+
}
|
|
207
|
+
process.exit(1)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export default quality
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as p from "@clack/prompts"
|
|
2
|
+
import chalk from "chalk"
|
|
3
|
+
import { getRegistryUrl, getToken } from "../config.js"
|
|
4
|
+
import { getHandler, hasCustomHandler } from "../install-targets.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Remove a previously added package.
|
|
8
|
+
*
|
|
9
|
+
* For packages with custom install handlers (e.g., MCP servers), this
|
|
10
|
+
* delegates to the handler's remove function. For standard source packages,
|
|
11
|
+
* it advises manual removal since files were copied into the project.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} pkgName - Package name (e.g., "@lpm.dev/owner.my-pkg")
|
|
14
|
+
* @param {object} options - CLI options
|
|
15
|
+
*/
|
|
16
|
+
export async function remove(pkgName, _options) {
|
|
17
|
+
p.intro(chalk.bgCyan(chalk.black(" lpm remove ")))
|
|
18
|
+
|
|
19
|
+
// 1. Auth Check
|
|
20
|
+
const token = await getToken()
|
|
21
|
+
if (!token) {
|
|
22
|
+
p.log.error("Not logged in. Run `lpm login` first.")
|
|
23
|
+
p.outro("")
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 2. Normalize package name
|
|
28
|
+
const name = pkgName.startsWith("@lpm.dev/") ? pkgName : `@lpm.dev/${pkgName}`
|
|
29
|
+
|
|
30
|
+
// 3. Fetch package metadata to determine type
|
|
31
|
+
const baseRegistryUrl = getRegistryUrl()
|
|
32
|
+
const registryUrl = baseRegistryUrl.endsWith("/api/registry")
|
|
33
|
+
? baseRegistryUrl
|
|
34
|
+
: `${baseRegistryUrl}/api/registry`
|
|
35
|
+
const encodedName = name.replace("/", "%2f")
|
|
36
|
+
|
|
37
|
+
let meta
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(`${registryUrl}/${encodedName}`, {
|
|
40
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
if (res.status === 404) {
|
|
45
|
+
p.log.error(`Package '${name}' not found.`)
|
|
46
|
+
p.outro("")
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
throw new Error(res.statusText)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
meta = await res.json()
|
|
53
|
+
} catch (err) {
|
|
54
|
+
p.log.error(`Failed to fetch package info: ${err.message}`)
|
|
55
|
+
p.outro("")
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 4. Determine package type from latest version's lpmConfig
|
|
60
|
+
const latestVersion = meta["dist-tags"]?.latest
|
|
61
|
+
const versionData = latestVersion ? meta.versions[latestVersion] : null
|
|
62
|
+
const packageType = versionData?.lpmConfig?.type || meta.packageType
|
|
63
|
+
|
|
64
|
+
// 5. Route to type-specific handler or show manual instructions
|
|
65
|
+
if (packageType && hasCustomHandler(packageType)) {
|
|
66
|
+
const handler = getHandler(packageType)
|
|
67
|
+
const result = await handler.remove({ name })
|
|
68
|
+
|
|
69
|
+
if (result.success) {
|
|
70
|
+
p.log.success(result.message)
|
|
71
|
+
} else {
|
|
72
|
+
p.log.error(result.message)
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
p.log.info(
|
|
76
|
+
`${chalk.cyan(name)} was installed as source files copied into your project.`,
|
|
77
|
+
)
|
|
78
|
+
p.log.info("Remove the files manually from your project directory.")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
p.outro("")
|
|
82
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
2
|
+
import { log } from "../ui.js"
|
|
3
|
+
|
|
4
|
+
export async function run(script, _options, command) {
|
|
5
|
+
const args = ["run", script, ...command.args]
|
|
6
|
+
|
|
7
|
+
const child = spawn("npm", args, { stdio: "inherit" })
|
|
8
|
+
|
|
9
|
+
child.on("close", code => process.exit(code))
|
|
10
|
+
child.on("error", err => {
|
|
11
|
+
log.error(`Failed to start npm: ${err.message}`)
|
|
12
|
+
process.exit(1)
|
|
13
|
+
})
|
|
14
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Command
|
|
3
|
+
*
|
|
4
|
+
* Search for packages using hybrid keyword + semantic search.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* lpm search <query> [options]
|
|
8
|
+
*
|
|
9
|
+
* Options:
|
|
10
|
+
* --limit <n> Maximum number of results (default: 20)
|
|
11
|
+
* --json Output in JSON format
|
|
12
|
+
*
|
|
13
|
+
* @module cli/lib/commands/search
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import chalk from "chalk"
|
|
17
|
+
import ora from "ora"
|
|
18
|
+
import { searchGet } from "../api.js"
|
|
19
|
+
import { DEFAULT_PAGE_LIMIT } from "../constants.js"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Format downloads count.
|
|
23
|
+
* @param {number} count
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
function formatDownloads(count) {
|
|
27
|
+
if (count >= 1000000) {
|
|
28
|
+
return `${(count / 1000000).toFixed(1)}M`
|
|
29
|
+
}
|
|
30
|
+
if (count >= 1000) {
|
|
31
|
+
return `${(count / 1000).toFixed(1)}K`
|
|
32
|
+
}
|
|
33
|
+
return String(count)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Execute the search command.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} query - Search query
|
|
40
|
+
* @param {Object} options - Command options
|
|
41
|
+
* @param {number} [options.limit] - Maximum results
|
|
42
|
+
* @param {boolean} [options.json] - Output as JSON
|
|
43
|
+
*/
|
|
44
|
+
export async function search(query, options = {}) {
|
|
45
|
+
if (!query || query.trim() === "") {
|
|
46
|
+
console.error(chalk.red("Error: Search query required."))
|
|
47
|
+
console.log(chalk.dim("Usage: lpm search <query>"))
|
|
48
|
+
process.exit(1)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const limit = options.limit || DEFAULT_PAGE_LIMIT
|
|
52
|
+
const spinner = ora(`Searching for "${query}"...`).start()
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const params = new URLSearchParams({
|
|
56
|
+
q: query,
|
|
57
|
+
mode: "semantic",
|
|
58
|
+
limit: String(limit),
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const response = await searchGet(`/packages?${params.toString()}`, {
|
|
62
|
+
onRetry: (attempt, max) => {
|
|
63
|
+
spinner.text = `Searching (retry ${attempt}/${max})...`
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
const data = await response.json().catch(() => ({}))
|
|
69
|
+
throw new Error(
|
|
70
|
+
data.error || `Search failed with status ${response.status}`,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const data = await response.json()
|
|
75
|
+
const packages = data.packages || []
|
|
76
|
+
|
|
77
|
+
spinner.stop()
|
|
78
|
+
|
|
79
|
+
if (options.json) {
|
|
80
|
+
console.log(JSON.stringify(packages, null, 2))
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (packages.length === 0) {
|
|
85
|
+
console.log(chalk.yellow(`\nNo packages found for "${query}".`))
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(
|
|
90
|
+
chalk.bold(
|
|
91
|
+
`\nFound ${packages.length} package${packages.length > 1 ? "s" : ""}:\n`,
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
// Calculate column widths (use LPM format: @lpm.dev/owner.name)
|
|
96
|
+
const maxNameLength = Math.max(
|
|
97
|
+
...packages.map(p => {
|
|
98
|
+
const owner = p.ownerSlug || p.owner
|
|
99
|
+
return (owner ? `@lpm.dev/${owner}.${p.name}` : p.name).length
|
|
100
|
+
}),
|
|
101
|
+
10,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
for (const pkg of packages) {
|
|
105
|
+
const owner = pkg.ownerSlug || pkg.owner
|
|
106
|
+
const fullName = owner ? `@lpm.dev/${owner}.${pkg.name}` : pkg.name
|
|
107
|
+
const paddedName = fullName.padEnd(maxNameLength)
|
|
108
|
+
const version = chalk.dim(
|
|
109
|
+
`v${pkg.latestVersion || pkg.version || "unknown"}`.padEnd(12),
|
|
110
|
+
)
|
|
111
|
+
const downloads = chalk.green(
|
|
112
|
+
`↓ ${formatDownloads(pkg.downloadCount ?? pkg.downloads ?? 0)}`.padEnd(
|
|
113
|
+
10,
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
console.log(` ${chalk.cyan(paddedName)} ${version} ${downloads}`)
|
|
118
|
+
|
|
119
|
+
if (pkg.description) {
|
|
120
|
+
const truncatedDesc =
|
|
121
|
+
pkg.description.length > 60
|
|
122
|
+
? `${pkg.description.slice(0, 57)}...`
|
|
123
|
+
: pkg.description
|
|
124
|
+
console.log(` ${chalk.dim(truncatedDesc)}`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log("")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (packages.length === limit) {
|
|
131
|
+
console.log(
|
|
132
|
+
chalk.dim(` Showing first ${limit} results. Use --limit to see more.`),
|
|
133
|
+
)
|
|
134
|
+
console.log("")
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
spinner.fail(chalk.red("Search failed."))
|
|
138
|
+
console.error(chalk.red(` ${error.message}`))
|
|
139
|
+
process.exit(1)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export default search
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import * as p from "@clack/prompts"
|
|
4
|
+
import chalk from "chalk"
|
|
5
|
+
import { getRegistryUrl } from "../config.js"
|
|
6
|
+
import { log, printHeader } from "../ui.js"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Configure .npmrc for LPM packages.
|
|
10
|
+
* Sets up the @lpm.dev scope to point to the LPM registry.
|
|
11
|
+
*/
|
|
12
|
+
export async function setup(options) {
|
|
13
|
+
printHeader()
|
|
14
|
+
|
|
15
|
+
p.intro(chalk.bgCyan(chalk.black(" lpm setup ")))
|
|
16
|
+
|
|
17
|
+
const registryUrl = options?.registry || getRegistryUrl()
|
|
18
|
+
const projectRoot = process.cwd()
|
|
19
|
+
const npmrcPath = path.join(projectRoot, ".npmrc")
|
|
20
|
+
|
|
21
|
+
// Check for package.json
|
|
22
|
+
const pkgJsonPath = path.join(projectRoot, "package.json")
|
|
23
|
+
if (!fs.existsSync(pkgJsonPath)) {
|
|
24
|
+
log.warn("No package.json found. Run this command in your project root.")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Check for existing .npmrc
|
|
28
|
+
let npmrcContent = ""
|
|
29
|
+
if (fs.existsSync(npmrcPath)) {
|
|
30
|
+
npmrcContent = fs.readFileSync(npmrcPath, "utf8")
|
|
31
|
+
|
|
32
|
+
// Check if already configured
|
|
33
|
+
if (npmrcContent.includes("@lpm.dev:registry")) {
|
|
34
|
+
const overwrite = await p.confirm({
|
|
35
|
+
message: ".npmrc already has LPM configuration. Overwrite?",
|
|
36
|
+
initialValue: false,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
40
|
+
p.outro("Setup cancelled.")
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Remove existing LPM config lines
|
|
45
|
+
npmrcContent = npmrcContent
|
|
46
|
+
.split("\n")
|
|
47
|
+
.filter(line => {
|
|
48
|
+
return (
|
|
49
|
+
!line.includes("@lpm.dev:registry") &&
|
|
50
|
+
!line.includes("lpm.dev/api/registry/:_authToken") &&
|
|
51
|
+
!line.includes("# LPM Registry")
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
.join("\n")
|
|
55
|
+
.trim()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Build registry URL
|
|
60
|
+
const fullRegistryUrl = registryUrl.endsWith("/api/registry")
|
|
61
|
+
? registryUrl
|
|
62
|
+
: `${registryUrl}/api/registry`
|
|
63
|
+
|
|
64
|
+
// Remove protocol for auth token config
|
|
65
|
+
const registryHost = fullRegistryUrl.replace(/^https?:/, "")
|
|
66
|
+
|
|
67
|
+
// Add LPM registry config
|
|
68
|
+
const lpmConfig = `
|
|
69
|
+
# LPM Registry
|
|
70
|
+
@lpm.dev:registry=${fullRegistryUrl}
|
|
71
|
+
${registryHost}/:_authToken=\${LPM_TOKEN}
|
|
72
|
+
`.trim()
|
|
73
|
+
|
|
74
|
+
// Combine with existing content
|
|
75
|
+
npmrcContent = npmrcContent ? `${npmrcContent}\n\n${lpmConfig}` : lpmConfig
|
|
76
|
+
|
|
77
|
+
// Write .npmrc
|
|
78
|
+
fs.writeFileSync(npmrcPath, `${npmrcContent}\n`)
|
|
79
|
+
|
|
80
|
+
p.note(
|
|
81
|
+
`@lpm.dev:registry=${fullRegistryUrl}\n${registryHost}/:_authToken=\${LPM_TOKEN}`,
|
|
82
|
+
".npmrc configuration",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
console.log("")
|
|
86
|
+
log.success(".npmrc configured for LPM packages.")
|
|
87
|
+
log.info("For local development: Run `lpm login` to authenticate.")
|
|
88
|
+
log.info("For CI/CD: Set the LPM_TOKEN environment variable.")
|
|
89
|
+
console.log("")
|
|
90
|
+
|
|
91
|
+
p.outro("Setup complete!")
|
|
92
|
+
}
|