@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,164 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import ora from "ora"
|
|
3
|
+
import { get } from "../api.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format cents to a dollar string.
|
|
7
|
+
* @param {number} cents
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
function formatCents(cents) {
|
|
11
|
+
if (!cents && cents !== 0) return "—"
|
|
12
|
+
return `$${(cents / 100).toFixed(2)}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse package name to extract category or clean name.
|
|
17
|
+
* @param {string} input
|
|
18
|
+
* @returns {{ isPackage: boolean, owner?: string, name?: string, category?: string }}
|
|
19
|
+
*/
|
|
20
|
+
function parseInput(input) {
|
|
21
|
+
let cleaned = input
|
|
22
|
+
if (cleaned.startsWith("@lpm.dev/")) {
|
|
23
|
+
cleaned = cleaned.replace("@lpm.dev/", "")
|
|
24
|
+
}
|
|
25
|
+
const dotIndex = cleaned.indexOf(".")
|
|
26
|
+
if (dotIndex > 0 && dotIndex < cleaned.length - 1) {
|
|
27
|
+
return {
|
|
28
|
+
isPackage: true,
|
|
29
|
+
owner: cleaned.substring(0, dotIndex),
|
|
30
|
+
name: cleaned.substring(dotIndex + 1),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Treat as category/keyword
|
|
34
|
+
return { isPackage: false, category: input }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Find and display comparable marketplace packages.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} input - Package name or category
|
|
41
|
+
* @param {Object} [options]
|
|
42
|
+
* @param {boolean} [options.json] - Output as JSON
|
|
43
|
+
* @param {string} [options.category] - Filter by category
|
|
44
|
+
* @param {number} [options.limit] - Max results
|
|
45
|
+
*/
|
|
46
|
+
export async function marketplaceCompare(input, options = {}) {
|
|
47
|
+
if (!input || input.trim() === "") {
|
|
48
|
+
if (options.json) {
|
|
49
|
+
console.log(
|
|
50
|
+
JSON.stringify({ error: "Package name or category required." }),
|
|
51
|
+
)
|
|
52
|
+
} else {
|
|
53
|
+
console.error(chalk.red("Error: Package name or category required."))
|
|
54
|
+
console.log(
|
|
55
|
+
chalk.dim("Usage: lpm marketplace compare owner.package-name"),
|
|
56
|
+
)
|
|
57
|
+
console.log(
|
|
58
|
+
chalk.dim(" lpm marketplace compare --category ui-components"),
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
process.exit(1)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const parsed = parseInput(input.trim())
|
|
65
|
+
const category =
|
|
66
|
+
options.category || (parsed.isPackage ? null : parsed.category)
|
|
67
|
+
const query = parsed.isPackage ? parsed.name : null
|
|
68
|
+
|
|
69
|
+
const params = new URLSearchParams()
|
|
70
|
+
if (category) params.set("category", category)
|
|
71
|
+
if (query) params.set("q", query)
|
|
72
|
+
if (options.limit) params.set("limit", String(options.limit))
|
|
73
|
+
|
|
74
|
+
const spinner = options.json
|
|
75
|
+
? null
|
|
76
|
+
: ora("Fetching comparable packages...").start()
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const response = await get(
|
|
80
|
+
`/marketplace/comparables?${params.toString()}`,
|
|
81
|
+
{
|
|
82
|
+
onRetry: spinner
|
|
83
|
+
? (attempt, max) => {
|
|
84
|
+
spinner.text = `Fetching (retry ${attempt}/${max})...`
|
|
85
|
+
}
|
|
86
|
+
: undefined,
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const data = await response.json().catch(() => ({}))
|
|
91
|
+
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw new Error(data.error || `Request failed: ${response.status}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (options.json) {
|
|
97
|
+
console.log(JSON.stringify(data, null, 2))
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (spinner) spinner.stop()
|
|
102
|
+
|
|
103
|
+
const comparables = data.comparables || []
|
|
104
|
+
|
|
105
|
+
console.log("")
|
|
106
|
+
if (category) {
|
|
107
|
+
console.log(chalk.bold(` Marketplace: ${category}`))
|
|
108
|
+
} else if (parsed.isPackage) {
|
|
109
|
+
console.log(
|
|
110
|
+
chalk.bold(` Similar to: @lpm.dev/${parsed.owner}.${parsed.name}`),
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (data.stats?.priceRange) {
|
|
115
|
+
const pr = data.stats.priceRange
|
|
116
|
+
console.log(
|
|
117
|
+
chalk.dim(
|
|
118
|
+
` Price range: ${formatCents(pr.minCents)} — ${formatCents(pr.maxCents)} (median: ${formatCents(pr.medianCents)})`,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
console.log(chalk.dim(` ${data.stats?.total || 0} packages found`))
|
|
123
|
+
console.log("")
|
|
124
|
+
|
|
125
|
+
if (comparables.length === 0) {
|
|
126
|
+
console.log(chalk.dim(" No comparable packages found."))
|
|
127
|
+
console.log("")
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Table
|
|
132
|
+
const nameWidth = Math.max(30, ...comparables.map(c => c.name.length + 2))
|
|
133
|
+
console.log(
|
|
134
|
+
` ${chalk.dim("Package".padEnd(nameWidth))}${chalk.dim("Price".padStart(10))}${chalk.dim("Quality".padStart(10))}${chalk.dim("Downloads".padStart(12))}${chalk.dim("Mode".padStart(14))}`,
|
|
135
|
+
)
|
|
136
|
+
console.log(chalk.dim(` ${"─".repeat(nameWidth + 46)}`))
|
|
137
|
+
|
|
138
|
+
for (const pkg of comparables) {
|
|
139
|
+
const name = pkg.name.padEnd(nameWidth)
|
|
140
|
+
const price = pkg.pricing
|
|
141
|
+
? formatCents(pkg.pricing.minPriceCents).padStart(10)
|
|
142
|
+
: chalk.dim("Pool".padStart(10))
|
|
143
|
+
const quality =
|
|
144
|
+
pkg.qualityScore !== null
|
|
145
|
+
? `${pkg.qualityScore}/100`.padStart(10)
|
|
146
|
+
: chalk.dim("—".padStart(10))
|
|
147
|
+
const downloads = (pkg.downloadCount || 0).toLocaleString().padStart(12)
|
|
148
|
+
const mode = pkg.distributionMode.padStart(14)
|
|
149
|
+
console.log(` ${name}${price}${quality}${downloads}${chalk.dim(mode)}`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log("")
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (spinner) spinner.fail(chalk.red("Failed to fetch comparable packages."))
|
|
155
|
+
if (options.json) {
|
|
156
|
+
console.log(JSON.stringify({ error: error.message }))
|
|
157
|
+
} else {
|
|
158
|
+
console.error(chalk.red(` ${error.message}`))
|
|
159
|
+
}
|
|
160
|
+
process.exit(1)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export default marketplaceCompare
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import chalk from "chalk"
|
|
2
|
+
import ora from "ora"
|
|
3
|
+
import { get } from "../api.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format cents to a dollar string.
|
|
7
|
+
* @param {number} cents
|
|
8
|
+
* @returns {string}
|
|
9
|
+
*/
|
|
10
|
+
function formatCents(cents) {
|
|
11
|
+
if (!cents && cents !== 0) return "$0.00"
|
|
12
|
+
return `$${(cents / 100).toFixed(2)}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Fetch and display Marketplace earnings for the authenticated user.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object} [options]
|
|
19
|
+
* @param {boolean} [options.json] - Output as JSON
|
|
20
|
+
*/
|
|
21
|
+
export async function marketplaceEarnings(options = {}) {
|
|
22
|
+
const spinner = options.json
|
|
23
|
+
? null
|
|
24
|
+
: ora("Fetching marketplace earnings...").start()
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const response = await get("/marketplace/earnings", {
|
|
28
|
+
skipRetry: false,
|
|
29
|
+
onRetry: spinner
|
|
30
|
+
? (attempt, max) => {
|
|
31
|
+
spinner.text = `Fetching (retry ${attempt}/${max})...`
|
|
32
|
+
}
|
|
33
|
+
: undefined,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const data = await response.json().catch(() => ({}))
|
|
37
|
+
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(data.error || `Request failed: ${response.status}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (options.json) {
|
|
43
|
+
console.log(JSON.stringify(data, null, 2))
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (spinner) spinner.stop()
|
|
48
|
+
|
|
49
|
+
console.log("")
|
|
50
|
+
console.log(chalk.bold(" Marketplace Earnings"))
|
|
51
|
+
console.log("")
|
|
52
|
+
|
|
53
|
+
if (!data.totalSales) {
|
|
54
|
+
console.log(chalk.dim(" No marketplace sales yet."))
|
|
55
|
+
console.log(
|
|
56
|
+
chalk.dim(
|
|
57
|
+
" Publish a package with Marketplace distribution to start selling.",
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
console.log("")
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(
|
|
65
|
+
` ${chalk.dim("Total Sales:")} ${data.totalSales.toLocaleString()}`,
|
|
66
|
+
)
|
|
67
|
+
console.log(
|
|
68
|
+
` ${chalk.dim("Gross Revenue:")} ${formatCents(data.grossRevenueCents)}`,
|
|
69
|
+
)
|
|
70
|
+
console.log(
|
|
71
|
+
` ${chalk.dim("Platform Fees:")} ${chalk.red(formatCents(data.platformFeesCents))}`,
|
|
72
|
+
)
|
|
73
|
+
console.log(
|
|
74
|
+
` ${chalk.bold("Net Revenue:")} ${chalk.green(formatCents(data.netRevenueCents))}`,
|
|
75
|
+
)
|
|
76
|
+
console.log("")
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (spinner)
|
|
79
|
+
spinner.fail(chalk.red("Failed to fetch marketplace earnings."))
|
|
80
|
+
if (options.json) {
|
|
81
|
+
console.log(JSON.stringify({ error: error.message }))
|
|
82
|
+
} else {
|
|
83
|
+
console.error(chalk.red(` ${error.message}`))
|
|
84
|
+
}
|
|
85
|
+
process.exit(1)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export default marketplaceEarnings
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process"
|
|
2
|
+
import fs from "node:fs"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import { fileURLToPath } from "node:url"
|
|
5
|
+
import * as p from "@clack/prompts"
|
|
6
|
+
import chalk from "chalk"
|
|
7
|
+
import { getRegistryUrl, getToken } from "../config.js"
|
|
8
|
+
import { DEFAULT_REGISTRY_URL } from "../constants.js"
|
|
9
|
+
import {
|
|
10
|
+
addMcpServer,
|
|
11
|
+
EDITORS,
|
|
12
|
+
getMcpServerConfig,
|
|
13
|
+
hasMcpServer,
|
|
14
|
+
removeMcpServerEntry,
|
|
15
|
+
shortPath,
|
|
16
|
+
} from "../editors.js"
|
|
17
|
+
|
|
18
|
+
const SERVER_NAME = "lpm-registry"
|
|
19
|
+
|
|
20
|
+
const MCP_SERVER_PACKAGE = "@lpm-registry/mcp-server"
|
|
21
|
+
const THIS_FILE_DIR = path.dirname(fileURLToPath(import.meta.url))
|
|
22
|
+
const LOCAL_MCP_SERVER_CANDIDATES = [
|
|
23
|
+
path.resolve(process.cwd(), "mcp-server", "bin", "mcp-server.js"),
|
|
24
|
+
path.resolve(process.cwd(), "..", "mcp-server", "bin", "mcp-server.js"),
|
|
25
|
+
path.resolve(
|
|
26
|
+
THIS_FILE_DIR,
|
|
27
|
+
"..",
|
|
28
|
+
"..",
|
|
29
|
+
"..",
|
|
30
|
+
"..",
|
|
31
|
+
"mcp-server",
|
|
32
|
+
"bin",
|
|
33
|
+
"mcp-server.js",
|
|
34
|
+
),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
function resolveNpxCommand() {
|
|
38
|
+
const lookupCommand = process.platform === "win32" ? "where" : "which"
|
|
39
|
+
const result = spawnSync(lookupCommand, ["npx"], { encoding: "utf-8" })
|
|
40
|
+
|
|
41
|
+
if (result.status === 0) {
|
|
42
|
+
const lines = (result.stdout || "")
|
|
43
|
+
.split("\n")
|
|
44
|
+
.map(line => line.trim())
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
|
|
47
|
+
if (lines.length > 0) {
|
|
48
|
+
return lines[0]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return "npx"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getLocalMcpServerPath() {
|
|
56
|
+
for (const candidate of LOCAL_MCP_SERVER_CANDIDATES) {
|
|
57
|
+
if (fs.existsSync(candidate)) {
|
|
58
|
+
return candidate
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getServerConfig(registryUrl = DEFAULT_REGISTRY_URL) {
|
|
66
|
+
const localServerPath = getLocalMcpServerPath()
|
|
67
|
+
const hasCustomRegistry =
|
|
68
|
+
typeof registryUrl === "string" &&
|
|
69
|
+
registryUrl.length > 0 &&
|
|
70
|
+
registryUrl !== DEFAULT_REGISTRY_URL
|
|
71
|
+
|
|
72
|
+
const env = hasCustomRegistry ? { LPM_REGISTRY_URL: registryUrl } : undefined
|
|
73
|
+
|
|
74
|
+
if (localServerPath) {
|
|
75
|
+
return {
|
|
76
|
+
command: process.execPath,
|
|
77
|
+
args: [localServerPath],
|
|
78
|
+
...(env ? { env } : {}),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
command: resolveNpxCommand(),
|
|
84
|
+
args: ["-y", `${MCP_SERVER_PACKAGE}@latest`],
|
|
85
|
+
...(env ? { env } : {}),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isCommandRunnable(command) {
|
|
90
|
+
if (!command) return false
|
|
91
|
+
|
|
92
|
+
if (path.isAbsolute(command)) {
|
|
93
|
+
return fs.existsSync(command)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const lookupCommand = process.platform === "win32" ? "where" : "which"
|
|
97
|
+
const result = spawnSync(lookupCommand, [command], { stdio: "ignore" })
|
|
98
|
+
return result.status === 0
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getEditorStatus(editor, isProject = false) {
|
|
102
|
+
const configPath =
|
|
103
|
+
isProject && editor.projectPath
|
|
104
|
+
? path.resolve(process.cwd(), editor.projectPath)
|
|
105
|
+
: editor.globalPath
|
|
106
|
+
|
|
107
|
+
const entry = getMcpServerConfig(configPath, editor.serverKey, SERVER_NAME)
|
|
108
|
+
const installed = !!entry
|
|
109
|
+
|
|
110
|
+
if (!installed) {
|
|
111
|
+
return {
|
|
112
|
+
editor,
|
|
113
|
+
configPath,
|
|
114
|
+
installed: false,
|
|
115
|
+
runnable: false,
|
|
116
|
+
entry: null,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const command = entry.command
|
|
121
|
+
const runnable = isCommandRunnable(command)
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
editor,
|
|
125
|
+
configPath,
|
|
126
|
+
installed: true,
|
|
127
|
+
runnable,
|
|
128
|
+
entry,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// Setup command
|
|
134
|
+
// ============================================================================
|
|
135
|
+
|
|
136
|
+
export async function mcpSetup(options = {}) {
|
|
137
|
+
p.intro(chalk.bgCyan(chalk.black(" lpm mcp setup ")))
|
|
138
|
+
const registryUrl = getRegistryUrl()
|
|
139
|
+
const serverConfig = getServerConfig(registryUrl)
|
|
140
|
+
|
|
141
|
+
// Check authentication and offer login
|
|
142
|
+
let token = await getToken()
|
|
143
|
+
if (!token) {
|
|
144
|
+
const shouldLogin = await p.confirm({
|
|
145
|
+
message:
|
|
146
|
+
"Not logged in. Login now for full MCP functionality? (recommended)",
|
|
147
|
+
initialValue: true,
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
if (p.isCancel(shouldLogin)) {
|
|
151
|
+
p.cancel("Setup cancelled.")
|
|
152
|
+
process.exit(0)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (shouldLogin) {
|
|
156
|
+
p.log.info("Opening browser for login...\n")
|
|
157
|
+
const result = spawnSync("lpm", ["login"], {
|
|
158
|
+
stdio: "inherit",
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
if (result.status === 0) {
|
|
162
|
+
token = await getToken()
|
|
163
|
+
} else {
|
|
164
|
+
p.log.warn("Login did not complete. Continuing without authentication.")
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const isProject = !!options.project
|
|
170
|
+
|
|
171
|
+
// Detect installed editors
|
|
172
|
+
const detected = EDITORS.filter(e => e.detect())
|
|
173
|
+
|
|
174
|
+
if (detected.length === 0) {
|
|
175
|
+
p.log.warn("No supported editors detected on this machine.")
|
|
176
|
+
p.note(
|
|
177
|
+
"Supported: Claude Code, Cursor, VS Code, Claude Desktop, Windsurf",
|
|
178
|
+
"Supported editors",
|
|
179
|
+
)
|
|
180
|
+
p.outro("Install a supported editor and try again.")
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Filter to editors that support project config when --project is used
|
|
185
|
+
const eligible = isProject ? detected.filter(e => e.projectPath) : detected
|
|
186
|
+
|
|
187
|
+
if (eligible.length === 0) {
|
|
188
|
+
p.log.warn("No detected editors support project-level MCP config.")
|
|
189
|
+
p.outro(
|
|
190
|
+
`Try ${chalk.cyan("lpm mcp setup")} without --project for global setup.`,
|
|
191
|
+
)
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Build multiselect options
|
|
196
|
+
const selectOptions = eligible.map(editor => {
|
|
197
|
+
const configPath =
|
|
198
|
+
isProject && editor.projectPath
|
|
199
|
+
? path.resolve(process.cwd(), editor.projectPath)
|
|
200
|
+
: editor.globalPath
|
|
201
|
+
|
|
202
|
+
const installed = hasMcpServer(configPath, editor.serverKey, SERVER_NAME)
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
value: editor.id,
|
|
206
|
+
label: installed
|
|
207
|
+
? `${editor.name} ${chalk.dim("(already configured)")}`
|
|
208
|
+
: editor.name,
|
|
209
|
+
hint: shortPath(configPath),
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const selected = await p.multiselect({
|
|
214
|
+
message: isProject
|
|
215
|
+
? "Add LPM MCP server to (project-level):"
|
|
216
|
+
: "Add LPM MCP server to:",
|
|
217
|
+
options: selectOptions,
|
|
218
|
+
initialValues: selectOptions.map(o => o.value),
|
|
219
|
+
required: true,
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
if (p.isCancel(selected)) {
|
|
223
|
+
p.cancel("Setup cancelled.")
|
|
224
|
+
process.exit(0)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Write configs
|
|
228
|
+
let count = 0
|
|
229
|
+
for (const editorId of selected) {
|
|
230
|
+
const editor = EDITORS.find(e => e.id === editorId)
|
|
231
|
+
const configPath =
|
|
232
|
+
isProject && editor.projectPath
|
|
233
|
+
? path.resolve(process.cwd(), editor.projectPath)
|
|
234
|
+
: editor.globalPath
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
addMcpServer(configPath, editor.serverKey, SERVER_NAME, {
|
|
238
|
+
...serverConfig,
|
|
239
|
+
})
|
|
240
|
+
p.log.success(`${editor.name} ${chalk.dim(shortPath(configPath))}`)
|
|
241
|
+
count++
|
|
242
|
+
} catch (err) {
|
|
243
|
+
p.log.error(`${editor.name}: ${err.message}`)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (count > 0) {
|
|
248
|
+
const authLine = token
|
|
249
|
+
? `Auth: Using keychain token from ${chalk.cyan("lpm login")}`
|
|
250
|
+
: `Auth: Run ${chalk.cyan("lpm login")} to enable authenticated tools`
|
|
251
|
+
|
|
252
|
+
p.note(authLine, `Added to ${count} editor${count > 1 ? "s" : ""}`)
|
|
253
|
+
|
|
254
|
+
if (registryUrl !== DEFAULT_REGISTRY_URL) {
|
|
255
|
+
p.log.info(
|
|
256
|
+
`Using custom registry URL for MCP: ${chalk.cyan(registryUrl)}`,
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
p.outro("Restart your editors to activate the MCP server.")
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ============================================================================
|
|
265
|
+
// Remove command
|
|
266
|
+
// ============================================================================
|
|
267
|
+
|
|
268
|
+
export async function mcpRemove(options = {}) {
|
|
269
|
+
p.intro(chalk.bgCyan(chalk.black(" lpm mcp remove ")))
|
|
270
|
+
|
|
271
|
+
const isProject = !!options.project
|
|
272
|
+
let count = 0
|
|
273
|
+
|
|
274
|
+
for (const editor of EDITORS) {
|
|
275
|
+
if (isProject && !editor.projectPath) continue
|
|
276
|
+
|
|
277
|
+
const configPath =
|
|
278
|
+
isProject && editor.projectPath
|
|
279
|
+
? path.resolve(process.cwd(), editor.projectPath)
|
|
280
|
+
: editor.globalPath
|
|
281
|
+
|
|
282
|
+
if (removeMcpServerEntry(configPath, editor.serverKey, SERVER_NAME)) {
|
|
283
|
+
p.log.success(
|
|
284
|
+
`Removed from ${editor.name} ${chalk.dim(shortPath(configPath))}`,
|
|
285
|
+
)
|
|
286
|
+
count++
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (count === 0) {
|
|
291
|
+
p.log.info("LPM MCP server was not configured in any editor.")
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
p.outro(
|
|
295
|
+
count > 0 ? "Done. Restart your editors to apply." : "Nothing to remove.",
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ============================================================================
|
|
300
|
+
// Status command
|
|
301
|
+
// ============================================================================
|
|
302
|
+
|
|
303
|
+
export async function mcpStatus(options = {}) {
|
|
304
|
+
p.intro(chalk.bgCyan(chalk.black(" lpm mcp status ")))
|
|
305
|
+
|
|
306
|
+
const token = await getToken()
|
|
307
|
+
const isVerbose = !!options.verbose
|
|
308
|
+
let found = 0
|
|
309
|
+
let notRunnable = 0
|
|
310
|
+
|
|
311
|
+
for (const editor of EDITORS) {
|
|
312
|
+
const status = getEditorStatus(editor, false)
|
|
313
|
+
const globalInstalled = status.installed
|
|
314
|
+
|
|
315
|
+
if (globalInstalled) {
|
|
316
|
+
if (status.runnable) {
|
|
317
|
+
p.log.success(
|
|
318
|
+
`${editor.name} ${chalk.dim(shortPath(editor.globalPath))}`,
|
|
319
|
+
)
|
|
320
|
+
} else {
|
|
321
|
+
p.log.warn(
|
|
322
|
+
`${editor.name} ${chalk.dim(shortPath(editor.globalPath))} ${chalk.dim("(configured, command not found)")}`,
|
|
323
|
+
)
|
|
324
|
+
notRunnable++
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (isVerbose) {
|
|
328
|
+
const args = Array.isArray(status.entry?.args) ? status.entry.args : []
|
|
329
|
+
const command = status.entry?.command || "(missing)"
|
|
330
|
+
p.log.info(` command: ${command}`)
|
|
331
|
+
p.log.info(` args: ${args.length > 0 ? args.join(" ") : "(none)"}`)
|
|
332
|
+
if (!status.runnable) {
|
|
333
|
+
p.log.info(
|
|
334
|
+
` hint: use ${chalk.cyan("lpm mcp setup")} again to refresh config on this machine`,
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
found++
|
|
339
|
+
} else if (isVerbose) {
|
|
340
|
+
p.log.info(
|
|
341
|
+
`${editor.name} ${chalk.dim(shortPath(editor.globalPath))} ${chalk.dim("(not configured)")}`,
|
|
342
|
+
)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (found === 0) {
|
|
347
|
+
p.log.info("LPM MCP server is not configured in any editor.")
|
|
348
|
+
p.note(`Run ${chalk.cyan("lpm mcp setup")} to configure it.`, "Get started")
|
|
349
|
+
} else if (notRunnable > 0) {
|
|
350
|
+
p.note(
|
|
351
|
+
`${notRunnable} configured editor${notRunnable > 1 ? "s" : ""} have a command that is not runnable in this environment.`,
|
|
352
|
+
"Action needed",
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const authStatus = token
|
|
357
|
+
? chalk.green("Authenticated")
|
|
358
|
+
: chalk.yellow(`Not logged in — run ${chalk.cyan("lpm login")}`)
|
|
359
|
+
|
|
360
|
+
p.log.info(`Auth: ${authStatus}`)
|
|
361
|
+
|
|
362
|
+
p.outro("")
|
|
363
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from "node:fs"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import open from "open"
|
|
4
|
+
import { request } from "../api.js"
|
|
5
|
+
import { getRegistryUrl } from "../config.js"
|
|
6
|
+
import { createSpinner } from "../ui.js"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse owner from LPM package name.
|
|
10
|
+
* @param {string} name - Package name (e.g., '@lpm.dev/owner.pkg-name')
|
|
11
|
+
* @returns {{ owner: string, pkgName: string } | null}
|
|
12
|
+
*/
|
|
13
|
+
function parseOwner(name) {
|
|
14
|
+
if (name.startsWith("@lpm.dev/")) {
|
|
15
|
+
const nameWithOwner = name.replace("@lpm.dev/", "")
|
|
16
|
+
const dotIndex = nameWithOwner.indexOf(".")
|
|
17
|
+
if (dotIndex === -1) return null
|
|
18
|
+
return {
|
|
19
|
+
owner: nameWithOwner.substring(0, dotIndex),
|
|
20
|
+
pkgName: nameWithOwner.substring(dotIndex + 1),
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function openDashboard() {
|
|
27
|
+
const spinner = createSpinner("Opening dashboard...").start()
|
|
28
|
+
const registryUrl = getRegistryUrl()
|
|
29
|
+
|
|
30
|
+
// Default URL
|
|
31
|
+
let url = `${registryUrl}/dashboard`
|
|
32
|
+
|
|
33
|
+
// Check if we are in a package directory
|
|
34
|
+
const pkgPath = path.join(process.cwd(), "package.json")
|
|
35
|
+
if (fs.existsSync(pkgPath)) {
|
|
36
|
+
try {
|
|
37
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
|
|
38
|
+
const parsed = parseOwner(pkg.name)
|
|
39
|
+
|
|
40
|
+
if (parsed) {
|
|
41
|
+
const { owner, pkgName } = parsed
|
|
42
|
+
spinner.text = `Detected package ${pkg.name}. Checking owner...`
|
|
43
|
+
|
|
44
|
+
// Fetch user info to determine if owner is personal or org
|
|
45
|
+
try {
|
|
46
|
+
const response = await request("/-/whoami")
|
|
47
|
+
if (response.ok) {
|
|
48
|
+
const data = await response.json()
|
|
49
|
+
|
|
50
|
+
// Check if owner matches user's personal username
|
|
51
|
+
if (data.profile_username && data.profile_username === owner) {
|
|
52
|
+
url = `${registryUrl}/dashboard/packages`
|
|
53
|
+
spinner.text = `Opening personal packages dashboard...`
|
|
54
|
+
}
|
|
55
|
+
// Check if owner matches one of user's organizations
|
|
56
|
+
else if (data.organizations?.some(org => org.slug === owner)) {
|
|
57
|
+
url = `${registryUrl}/dashboard/orgs/${owner}/packages`
|
|
58
|
+
spinner.text = `Opening ${owner} organization packages...`
|
|
59
|
+
}
|
|
60
|
+
// Owner not recognized, fall back to public package page
|
|
61
|
+
else {
|
|
62
|
+
url = `${registryUrl}/${owner}.${pkgName}`
|
|
63
|
+
spinner.text = `Opening public package page...`
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
// Not authenticated, open public package page
|
|
67
|
+
url = `${registryUrl}/${owner}.${pkgName}`
|
|
68
|
+
spinner.text = `Opening public package page...`
|
|
69
|
+
}
|
|
70
|
+
} catch (_apiError) {
|
|
71
|
+
// API error, fall back to dashboard
|
|
72
|
+
url = `${registryUrl}/dashboard`
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch (_e) {
|
|
76
|
+
// ignore JSON parse errors
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await open(url)
|
|
81
|
+
spinner.succeed(`Opened ${url}`)
|
|
82
|
+
}
|