@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,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Command
|
|
3
|
+
*
|
|
4
|
+
* Manage CLI configuration values.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* lpm config list List all configuration values
|
|
8
|
+
* lpm config get <key> Get a specific config value
|
|
9
|
+
* lpm config set <key> <value> Set a config value
|
|
10
|
+
* lpm config delete <key> Delete a config value (reset to default)
|
|
11
|
+
*
|
|
12
|
+
* Configurable Keys:
|
|
13
|
+
* registry - Registry URL (default: https://lpm.dev)
|
|
14
|
+
* timeout - Request timeout in milliseconds (default: 30000)
|
|
15
|
+
* retries - Maximum retry attempts (default: 3)
|
|
16
|
+
*
|
|
17
|
+
* @module cli/lib/commands/config
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import chalk from "chalk"
|
|
21
|
+
import {
|
|
22
|
+
deleteConfigValue,
|
|
23
|
+
getAllConfig,
|
|
24
|
+
getConfigValue,
|
|
25
|
+
setConfigValue,
|
|
26
|
+
} from "../config.js"
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* List all configuration values.
|
|
30
|
+
*/
|
|
31
|
+
async function listConfig() {
|
|
32
|
+
console.log(chalk.bold("\nLPM Configuration:\n"))
|
|
33
|
+
|
|
34
|
+
const config = await getAllConfig()
|
|
35
|
+
|
|
36
|
+
// Format and display each config value
|
|
37
|
+
const entries = [
|
|
38
|
+
["Registry URL", config.registryUrl],
|
|
39
|
+
["Request Timeout", `${config.timeout}ms`],
|
|
40
|
+
["Max Retries", config.retries],
|
|
41
|
+
["Secure Storage", config.secureStorage],
|
|
42
|
+
[
|
|
43
|
+
"Authenticated",
|
|
44
|
+
config.authenticated ? chalk.green("Yes") : chalk.dim("No"),
|
|
45
|
+
],
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
const maxKeyLength = Math.max(...entries.map(([key]) => key.length))
|
|
49
|
+
|
|
50
|
+
for (const [key, value] of entries) {
|
|
51
|
+
const paddedKey = key.padEnd(maxKeyLength)
|
|
52
|
+
console.log(` ${chalk.cyan(paddedKey)} ${value}`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log("")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get a specific configuration value.
|
|
60
|
+
* @param {string} key - The config key to get
|
|
61
|
+
*/
|
|
62
|
+
function getConfig(key) {
|
|
63
|
+
const value = getConfigValue(key)
|
|
64
|
+
|
|
65
|
+
if (value === undefined) {
|
|
66
|
+
console.log(chalk.yellow(`Configuration key '${key}' is not set.`))
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(value)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Set a configuration value.
|
|
75
|
+
* @param {string} key - The config key to set
|
|
76
|
+
* @param {string} value - The value to set
|
|
77
|
+
*/
|
|
78
|
+
function setConfig(key, value) {
|
|
79
|
+
// Validate known keys
|
|
80
|
+
const numericKeys = ["timeout", "retries"]
|
|
81
|
+
if (numericKeys.includes(key)) {
|
|
82
|
+
const numValue = parseInt(value, 10)
|
|
83
|
+
if (Number.isNaN(numValue) || numValue < 0) {
|
|
84
|
+
console.error(chalk.red(`Error: '${key}' must be a positive number.`))
|
|
85
|
+
process.exit(1)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Validate registry URL
|
|
90
|
+
if (key === "registry" || key === "registryUrl") {
|
|
91
|
+
try {
|
|
92
|
+
new URL(value)
|
|
93
|
+
} catch {
|
|
94
|
+
console.error(chalk.red(`Error: '${value}' is not a valid URL.`))
|
|
95
|
+
process.exit(1)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const success = setConfigValue(key, value)
|
|
100
|
+
|
|
101
|
+
if (success) {
|
|
102
|
+
console.log(chalk.green(`Set ${key} = ${value}`))
|
|
103
|
+
} else {
|
|
104
|
+
console.error(chalk.red(`Failed to set '${key}'.`))
|
|
105
|
+
process.exit(1)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Delete a configuration value (reset to default).
|
|
111
|
+
* @param {string} key - The config key to delete
|
|
112
|
+
*/
|
|
113
|
+
function deleteConfig(key) {
|
|
114
|
+
const success = deleteConfigValue(key)
|
|
115
|
+
|
|
116
|
+
if (success) {
|
|
117
|
+
console.log(chalk.green(`Reset '${key}' to default value.`))
|
|
118
|
+
} else {
|
|
119
|
+
console.error(chalk.red(`Failed to delete '${key}'.`))
|
|
120
|
+
process.exit(1)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Execute the config command.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} action - The action to perform (list, get, set, delete)
|
|
128
|
+
* @param {string} [key] - The config key
|
|
129
|
+
* @param {string} [value] - The value to set
|
|
130
|
+
*/
|
|
131
|
+
export async function config(action, key, value) {
|
|
132
|
+
switch (action) {
|
|
133
|
+
case "list":
|
|
134
|
+
case undefined:
|
|
135
|
+
await listConfig()
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
case "get":
|
|
139
|
+
if (!key) {
|
|
140
|
+
console.error(chalk.red("Error: Key required."))
|
|
141
|
+
console.log(chalk.dim("Usage: lpm config get <key>"))
|
|
142
|
+
process.exit(1)
|
|
143
|
+
}
|
|
144
|
+
getConfig(key)
|
|
145
|
+
break
|
|
146
|
+
|
|
147
|
+
case "set":
|
|
148
|
+
if (!key || value === undefined) {
|
|
149
|
+
console.error(chalk.red("Error: Key and value required."))
|
|
150
|
+
console.log(chalk.dim("Usage: lpm config set <key> <value>"))
|
|
151
|
+
process.exit(1)
|
|
152
|
+
}
|
|
153
|
+
setConfig(key, value)
|
|
154
|
+
break
|
|
155
|
+
|
|
156
|
+
case "delete":
|
|
157
|
+
case "rm":
|
|
158
|
+
case "remove":
|
|
159
|
+
if (!key) {
|
|
160
|
+
console.error(chalk.red("Error: Key required."))
|
|
161
|
+
console.log(chalk.dim("Usage: lpm config delete <key>"))
|
|
162
|
+
process.exit(1)
|
|
163
|
+
}
|
|
164
|
+
deleteConfig(key)
|
|
165
|
+
break
|
|
166
|
+
|
|
167
|
+
default:
|
|
168
|
+
console.error(chalk.red(`Unknown action: ${action}`))
|
|
169
|
+
console.log(chalk.dim("Available actions: list, get, set, delete"))
|
|
170
|
+
process.exit(1)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export default config
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import chalk from "chalk"
|
|
4
|
+
import { getRegistryUrl, getToken } from "../config.js"
|
|
5
|
+
import { createSpinner, log, printHeader } from "../ui.js"
|
|
6
|
+
|
|
7
|
+
export async function doctor() {
|
|
8
|
+
printHeader()
|
|
9
|
+
log.info("Running health checks...\n")
|
|
10
|
+
|
|
11
|
+
const checks = []
|
|
12
|
+
|
|
13
|
+
// 1. Check Auth
|
|
14
|
+
const spinnerAuth = createSpinner("Checking authentication...").start()
|
|
15
|
+
const token = await getToken()
|
|
16
|
+
if (token) {
|
|
17
|
+
spinnerAuth.succeed("Authentication token found.")
|
|
18
|
+
checks.push({ name: "Auth Token", status: "ok" })
|
|
19
|
+
} else {
|
|
20
|
+
spinnerAuth.fail("No authentication token found. Run `lpm login`.")
|
|
21
|
+
checks.push({ name: "Auth Token", status: "fail" })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 2. Check Registry Reachability
|
|
25
|
+
const spinnerApi = createSpinner("Checking registry connection...").start()
|
|
26
|
+
try {
|
|
27
|
+
// Assuming we have a health endpoint or similar. Using the registry URL from config or default.
|
|
28
|
+
const registryUrl = getRegistryUrl()
|
|
29
|
+
const res = await fetch(`${registryUrl}/api/registry/health`)
|
|
30
|
+
if (res.ok) {
|
|
31
|
+
spinnerApi.succeed(`Registry reachable at ${registryUrl}`)
|
|
32
|
+
checks.push({ name: "Registry API", status: "ok" })
|
|
33
|
+
} else {
|
|
34
|
+
spinnerApi.fail(`Registry returned status ${res.status}`)
|
|
35
|
+
checks.push({ name: "Registry API", status: "fail" })
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
spinnerApi.fail(`Could not connect to registry: ${err.message}`)
|
|
39
|
+
checks.push({ name: "Registry API", status: "fail" })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 3. Check Quota (via whoami)
|
|
43
|
+
if (token) {
|
|
44
|
+
const spinnerQuota = createSpinner("Checking account quota...").start()
|
|
45
|
+
try {
|
|
46
|
+
// We can reuse the whoami logic or fetch directly
|
|
47
|
+
// Since we are in doctor, let's fetch directly using the token
|
|
48
|
+
const registryUrl = getRegistryUrl()
|
|
49
|
+
const res = await fetch(`${registryUrl}/api/registry/-/whoami`, {
|
|
50
|
+
headers: {
|
|
51
|
+
Authorization: `Bearer ${token}`,
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
if (res.ok) {
|
|
56
|
+
const data = await res.json()
|
|
57
|
+
if (data.plan_tier) {
|
|
58
|
+
const limits = data.limits || {}
|
|
59
|
+
let isOverLimit = false
|
|
60
|
+
const statusMsg = `Plan: ${data.plan_tier.toUpperCase()}`
|
|
61
|
+
|
|
62
|
+
if (limits.storageBytes) {
|
|
63
|
+
if (data.usage.storage_bytes > limits.storageBytes)
|
|
64
|
+
isOverLimit = true
|
|
65
|
+
}
|
|
66
|
+
if (
|
|
67
|
+
limits.privatePackages &&
|
|
68
|
+
limits.privatePackages !== Number.POSITIVE_INFINITY &&
|
|
69
|
+
limits.privatePackages !== null
|
|
70
|
+
) {
|
|
71
|
+
if (data.usage.private_packages > limits.privatePackages)
|
|
72
|
+
isOverLimit = true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (isOverLimit) {
|
|
76
|
+
spinnerQuota.fail(`${statusMsg} | OVER LIMIT`)
|
|
77
|
+
checks.push({ name: "Account Quota", status: "fail" })
|
|
78
|
+
} else {
|
|
79
|
+
spinnerQuota.succeed(
|
|
80
|
+
`${statusMsg} | Storage: ${(data.usage.storage_bytes / 1024 / 1024).toFixed(0)}MB`,
|
|
81
|
+
)
|
|
82
|
+
checks.push({ name: "Account Quota", status: "ok" })
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
spinnerQuota.info("Could not retrieve detailed quota info.")
|
|
86
|
+
checks.push({ name: "Account Quota", status: "info" })
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
spinnerQuota.warn("Could not fetch account details.")
|
|
90
|
+
checks.push({ name: "Account Quota", status: "warn" })
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
spinnerQuota.warn(`Quota check failed: ${err.message}`)
|
|
94
|
+
checks.push({ name: "Account Quota", status: "warn" })
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 4. Check .npmrc
|
|
99
|
+
const spinnerNpmrc = createSpinner("Checking .npmrc configuration...").start()
|
|
100
|
+
const npmrcPath = path.join(process.cwd(), ".npmrc")
|
|
101
|
+
if (fs.existsSync(npmrcPath)) {
|
|
102
|
+
const content = fs.readFileSync(npmrcPath, "utf-8")
|
|
103
|
+
if (content.includes("registry=")) {
|
|
104
|
+
spinnerNpmrc.succeed(".npmrc found and configured.")
|
|
105
|
+
checks.push({ name: ".npmrc", status: "ok" })
|
|
106
|
+
} else {
|
|
107
|
+
spinnerNpmrc.warn(".npmrc found but might be missing registry config.")
|
|
108
|
+
checks.push({ name: ".npmrc", status: "warn" })
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
spinnerNpmrc.info("No local .npmrc found (global config might be used).")
|
|
112
|
+
checks.push({ name: ".npmrc", status: "info" })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log("\nSummary:")
|
|
116
|
+
checks.forEach(c => {
|
|
117
|
+
const symbol =
|
|
118
|
+
c.status === "ok"
|
|
119
|
+
? "✔"
|
|
120
|
+
: c.status === "fail"
|
|
121
|
+
? "✖"
|
|
122
|
+
: c.status === "warn"
|
|
123
|
+
? "⚠"
|
|
124
|
+
: "ℹ"
|
|
125
|
+
const color =
|
|
126
|
+
c.status === "ok"
|
|
127
|
+
? chalk.green
|
|
128
|
+
: c.status === "fail"
|
|
129
|
+
? chalk.red
|
|
130
|
+
: c.status === "warn"
|
|
131
|
+
? chalk.yellow
|
|
132
|
+
: chalk.blue
|
|
133
|
+
console.log(color(`${symbol} ${c.name}`))
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
if (checks.some(c => c.status === "fail")) {
|
|
137
|
+
console.log(chalk.red("\nSome checks failed. Please fix the issues above."))
|
|
138
|
+
process.exit(1)
|
|
139
|
+
} else {
|
|
140
|
+
console.log(chalk.green("\nAll systems operational."))
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Info Command
|
|
3
|
+
*
|
|
4
|
+
* Display detailed information about a package.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* lpm info <package> [options]
|
|
8
|
+
*
|
|
9
|
+
* Options:
|
|
10
|
+
* --json Output in JSON format
|
|
11
|
+
* --versions Show all versions
|
|
12
|
+
*
|
|
13
|
+
* @module cli/lib/commands/info
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import chalk from "chalk"
|
|
17
|
+
import ora from "ora"
|
|
18
|
+
import { get } from "../api.js"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse a package specifier.
|
|
22
|
+
* @param {string} specifier - Package name (e.g., '@scope/name', 'name', 'name@1.0.0')
|
|
23
|
+
* @returns {{ name: string, scope?: string, version?: string }}
|
|
24
|
+
*/
|
|
25
|
+
function parsePackageSpecifier(specifier) {
|
|
26
|
+
let name = specifier
|
|
27
|
+
let scope
|
|
28
|
+
let version
|
|
29
|
+
|
|
30
|
+
// Handle version specifier
|
|
31
|
+
const versionMatch = name.match(/^(.+)@(\d+\.\d+\.\d+.*)$/)
|
|
32
|
+
if (versionMatch) {
|
|
33
|
+
name = versionMatch[1]
|
|
34
|
+
version = versionMatch[2]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Handle scope
|
|
38
|
+
if (name.startsWith("@")) {
|
|
39
|
+
const scopeMatch = name.match(/^@([^/]+)\/(.+)$/)
|
|
40
|
+
if (scopeMatch) {
|
|
41
|
+
scope = scopeMatch[1]
|
|
42
|
+
name = scopeMatch[2]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { name, scope, version }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Format a date for display.
|
|
51
|
+
* @param {string} dateStr
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
function formatDate(dateStr) {
|
|
55
|
+
const date = new Date(dateStr)
|
|
56
|
+
return date.toLocaleDateString("en-US", {
|
|
57
|
+
year: "numeric",
|
|
58
|
+
month: "short",
|
|
59
|
+
day: "numeric",
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Execute the info command.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} packageSpec - Package specifier
|
|
67
|
+
* @param {Object} options - Command options
|
|
68
|
+
* @param {boolean} [options.json] - Output as JSON
|
|
69
|
+
* @param {boolean} [options.versions] - Show all versions
|
|
70
|
+
*/
|
|
71
|
+
export async function info(packageSpec, options = {}) {
|
|
72
|
+
if (!packageSpec || packageSpec.trim() === "") {
|
|
73
|
+
console.error(chalk.red("Error: Package name required."))
|
|
74
|
+
console.log(chalk.dim("Usage: lpm info <package>"))
|
|
75
|
+
process.exit(1)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const { name, scope, version } = parsePackageSpecifier(packageSpec)
|
|
79
|
+
const packagePath = scope ? `@${scope}/${name}` : name
|
|
80
|
+
|
|
81
|
+
const spinner = ora(`Fetching info for ${packagePath}...`).start()
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// Registry uses NPM-compatible paths: /@scope/pkg or /@scope/pkg/version
|
|
85
|
+
const url = version
|
|
86
|
+
? `/${encodeURIComponent(packagePath)}/${version}`
|
|
87
|
+
: `/${encodeURIComponent(packagePath)}`
|
|
88
|
+
|
|
89
|
+
const response = await get(url, {
|
|
90
|
+
onRetry: (attempt, max) => {
|
|
91
|
+
spinner.text = `Fetching (retry ${attempt}/${max})...`
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
if (response.status === 404) {
|
|
97
|
+
throw new Error(`Package "${packagePath}" not found.`)
|
|
98
|
+
}
|
|
99
|
+
const data = await response.json().catch(() => ({}))
|
|
100
|
+
throw new Error(data.error || `Failed to fetch package info.`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const pkg = await response.json()
|
|
104
|
+
|
|
105
|
+
spinner.stop()
|
|
106
|
+
|
|
107
|
+
if (options.json) {
|
|
108
|
+
console.log(JSON.stringify(pkg, null, 2))
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Get latest version data from versions object
|
|
113
|
+
const latestVersionTag = pkg["dist-tags"]?.latest
|
|
114
|
+
const latestVersionData = latestVersionTag
|
|
115
|
+
? pkg.versions?.[latestVersionTag]
|
|
116
|
+
: null
|
|
117
|
+
|
|
118
|
+
// Display package info
|
|
119
|
+
const fullName = pkg.name
|
|
120
|
+
|
|
121
|
+
console.log("")
|
|
122
|
+
console.log(
|
|
123
|
+
chalk.bold.cyan(fullName) +
|
|
124
|
+
chalk.dim(`@${pkg.latestVersion || latestVersionTag || "no versions"}`),
|
|
125
|
+
)
|
|
126
|
+
console.log("")
|
|
127
|
+
|
|
128
|
+
if (pkg.description) {
|
|
129
|
+
console.log(chalk.white(pkg.description))
|
|
130
|
+
console.log("")
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Metadata table
|
|
134
|
+
const metadata = [
|
|
135
|
+
["Latest Version", pkg.latestVersion || latestVersionTag || "N/A"],
|
|
136
|
+
["License", latestVersionData?.license || pkg.license || "N/A"],
|
|
137
|
+
["Author", latestVersionData?.author || pkg.author || "N/A"],
|
|
138
|
+
["Published", pkg.publishedAt ? formatDate(pkg.publishedAt) : "N/A"],
|
|
139
|
+
["Downloads", (pkg.downloads ?? 0).toLocaleString()],
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
if (latestVersionData?.repository || pkg.repository) {
|
|
143
|
+
metadata.push([
|
|
144
|
+
"Repository",
|
|
145
|
+
latestVersionData?.repository || pkg.repository,
|
|
146
|
+
])
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (latestVersionData?.homepage || pkg.homepage) {
|
|
150
|
+
metadata.push(["Homepage", latestVersionData?.homepage || pkg.homepage])
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const maxKeyLength = Math.max(...metadata.map(([key]) => key.length))
|
|
154
|
+
|
|
155
|
+
for (const [key, value] of metadata) {
|
|
156
|
+
const paddedKey = key.padEnd(maxKeyLength)
|
|
157
|
+
console.log(` ${chalk.dim(paddedKey)} ${value}`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log("")
|
|
161
|
+
|
|
162
|
+
// Dependencies from latest version
|
|
163
|
+
const deps = latestVersionData?.dependencies || pkg.dependencies
|
|
164
|
+
if (deps && Object.keys(deps).length > 0) {
|
|
165
|
+
console.log(chalk.bold("Dependencies:"))
|
|
166
|
+
for (const [dep, ver] of Object.entries(deps)) {
|
|
167
|
+
console.log(` ${chalk.cyan(dep)} ${chalk.dim(ver)}`)
|
|
168
|
+
}
|
|
169
|
+
console.log("")
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Show versions if requested (versions is an object, not array)
|
|
173
|
+
if (
|
|
174
|
+
options.allVersions &&
|
|
175
|
+
pkg.versions &&
|
|
176
|
+
Object.keys(pkg.versions).length > 0
|
|
177
|
+
) {
|
|
178
|
+
console.log(chalk.bold("Versions:"))
|
|
179
|
+
const versionList = Object.keys(pkg.versions).reverse().slice(0, 20)
|
|
180
|
+
for (const v of versionList) {
|
|
181
|
+
const date = pkg.time?.[v] ? formatDate(pkg.time[v]) : ""
|
|
182
|
+
console.log(` ${chalk.green(v)} ${chalk.dim(date)}`)
|
|
183
|
+
}
|
|
184
|
+
if (Object.keys(pkg.versions).length > 20) {
|
|
185
|
+
console.log(
|
|
186
|
+
chalk.dim(
|
|
187
|
+
` ... and ${Object.keys(pkg.versions).length - 20} more versions`,
|
|
188
|
+
),
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
console.log("")
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Keywords
|
|
195
|
+
const keywords = latestVersionData?.keywords || pkg.keywords
|
|
196
|
+
if (keywords && keywords.length > 0) {
|
|
197
|
+
console.log(
|
|
198
|
+
chalk.bold("Keywords:") +
|
|
199
|
+
" " +
|
|
200
|
+
keywords.map(k => chalk.dim(k)).join(", "),
|
|
201
|
+
)
|
|
202
|
+
console.log("")
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Install hint
|
|
206
|
+
console.log(chalk.dim(`Install: lpm install ${fullName}`))
|
|
207
|
+
console.log("")
|
|
208
|
+
} catch (error) {
|
|
209
|
+
spinner.fail(chalk.red("Failed to fetch package info."))
|
|
210
|
+
console.error(chalk.red(` ${error.message}`))
|
|
211
|
+
process.exit(1)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export default info
|
|
@@ -0,0 +1,146 @@
|
|
|
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 { printHeader } from "../ui.js"
|
|
7
|
+
import { setup } from "./setup.js"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parse package name in the @lpm.dev/owner.package format
|
|
11
|
+
*/
|
|
12
|
+
function parsePackageName(name) {
|
|
13
|
+
if (!name.startsWith("@lpm.dev/")) {
|
|
14
|
+
return { error: "Package name must start with @lpm.dev/" }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const nameWithOwner = name.replace("@lpm.dev/", "")
|
|
18
|
+
const dotIndex = nameWithOwner.indexOf(".")
|
|
19
|
+
|
|
20
|
+
if (dotIndex === -1) {
|
|
21
|
+
return { error: "Format: @lpm.dev/owner.package-name" }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const owner = nameWithOwner.substring(0, dotIndex)
|
|
25
|
+
const pkgName = nameWithOwner.substring(dotIndex + 1)
|
|
26
|
+
|
|
27
|
+
// Validate owner format
|
|
28
|
+
if (!/^[a-z][a-z0-9-]*$/.test(owner)) {
|
|
29
|
+
return {
|
|
30
|
+
error:
|
|
31
|
+
"Owner must start with a letter and contain only lowercase letters, numbers, and hyphens",
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Validate package name format
|
|
36
|
+
if (!/^[a-z][a-z0-9-]*$/.test(pkgName)) {
|
|
37
|
+
return {
|
|
38
|
+
error:
|
|
39
|
+
"Package name must start with a letter and contain only lowercase letters, numbers, and hyphens",
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { owner, name: pkgName }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function init() {
|
|
47
|
+
printHeader()
|
|
48
|
+
|
|
49
|
+
p.intro(chalk.bgCyan(chalk.black(" lpm init ")))
|
|
50
|
+
|
|
51
|
+
const project = await p.group(
|
|
52
|
+
{
|
|
53
|
+
name: () =>
|
|
54
|
+
p.text({
|
|
55
|
+
message: "What is the name of your package?",
|
|
56
|
+
placeholder: "@lpm.dev/username.package-name",
|
|
57
|
+
validate: value => {
|
|
58
|
+
if (!value) return "Name is required"
|
|
59
|
+
const parsed = parsePackageName(value)
|
|
60
|
+
if (parsed.error) return parsed.error
|
|
61
|
+
},
|
|
62
|
+
}),
|
|
63
|
+
version: () =>
|
|
64
|
+
p.text({
|
|
65
|
+
message: "Version",
|
|
66
|
+
initialValue: "0.1.0",
|
|
67
|
+
}),
|
|
68
|
+
description: () =>
|
|
69
|
+
p.text({
|
|
70
|
+
message: "Description",
|
|
71
|
+
placeholder: "A brief description of your package",
|
|
72
|
+
}),
|
|
73
|
+
entry: () =>
|
|
74
|
+
p.text({
|
|
75
|
+
message: "Entry point",
|
|
76
|
+
initialValue: "index.js",
|
|
77
|
+
}),
|
|
78
|
+
license: () =>
|
|
79
|
+
p.select({
|
|
80
|
+
message: "License type",
|
|
81
|
+
options: [
|
|
82
|
+
{ value: "ISC", label: "ISC" },
|
|
83
|
+
{ value: "MIT", label: "MIT" },
|
|
84
|
+
{ value: "UNLICENSED", label: "UNLICENSED (Private/Proprietary)" },
|
|
85
|
+
],
|
|
86
|
+
}),
|
|
87
|
+
confirm: () =>
|
|
88
|
+
p.confirm({
|
|
89
|
+
message: "Create package.json?",
|
|
90
|
+
initialValue: true,
|
|
91
|
+
}),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
onCancel: () => {
|
|
95
|
+
p.cancel("Operation cancelled.")
|
|
96
|
+
process.exit(0)
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if (project.confirm) {
|
|
102
|
+
const registryUrl = getRegistryUrl()
|
|
103
|
+
const packageJson = {
|
|
104
|
+
name: project.name,
|
|
105
|
+
version: project.version,
|
|
106
|
+
description: project.description,
|
|
107
|
+
main: project.entry,
|
|
108
|
+
scripts: {
|
|
109
|
+
test: 'echo "Error: no test specified" && exit 1',
|
|
110
|
+
},
|
|
111
|
+
author: "",
|
|
112
|
+
license: project.license,
|
|
113
|
+
publishConfig: {
|
|
114
|
+
registry: `${registryUrl}/api/registry/`,
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const filePath = path.join(process.cwd(), "package.json")
|
|
119
|
+
fs.writeFileSync(filePath, JSON.stringify(packageJson, null, 2))
|
|
120
|
+
|
|
121
|
+
p.note(JSON.stringify(packageJson, null, 2), "package.json")
|
|
122
|
+
|
|
123
|
+
// Offer to setup .npmrc
|
|
124
|
+
const npmrcPath = path.join(process.cwd(), ".npmrc")
|
|
125
|
+
if (
|
|
126
|
+
!fs.existsSync(npmrcPath) ||
|
|
127
|
+
!fs.readFileSync(npmrcPath, "utf8").includes("@lpm.dev:registry")
|
|
128
|
+
) {
|
|
129
|
+
const setupNpmrc = await p.confirm({
|
|
130
|
+
message: "Create .npmrc for local development?",
|
|
131
|
+
initialValue: true,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
if (!p.isCancel(setupNpmrc) && setupNpmrc) {
|
|
135
|
+
await setup({ registry: registryUrl })
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
p.outro(
|
|
141
|
+
`Package initialized! Run ${chalk.cyan("lpm publish")} to push to the registry.`,
|
|
142
|
+
)
|
|
143
|
+
} else {
|
|
144
|
+
p.outro("Cancelled.")
|
|
145
|
+
}
|
|
146
|
+
}
|