@lpm-registry/cli 0.8.0 → 0.9.2

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/lib/api.js CHANGED
@@ -9,7 +9,15 @@
9
9
  * @module cli/lib/api
10
10
  */
11
11
 
12
- import { getRegistryUrl, getRetries, getTimeout, getToken } from "./config.js"
12
+ import {
13
+ getRefreshToken,
14
+ getRegistryUrl,
15
+ getRetries,
16
+ getTimeout,
17
+ getToken,
18
+ setRefreshToken,
19
+ setToken,
20
+ } from "./config.js"
13
21
  import {
14
22
  ERROR_MESSAGES,
15
23
  RATE_LIMIT_STATUS_CODES,
@@ -63,6 +71,44 @@ function parseRetryAfter(retryAfter) {
63
71
  return RETRY_BASE_DELAY_MS
64
72
  }
65
73
 
74
+ /**
75
+ * Attempt silent token refresh using the stored refresh token (Feature 44 Part B).
76
+ * @returns {Promise<string|null>} New access token if successful, null otherwise
77
+ */
78
+ async function tryRefresh() {
79
+ try {
80
+ const refreshToken = await getRefreshToken()
81
+ if (!refreshToken) return null
82
+
83
+ const registryUrl = getRegistryUrl()
84
+ const { createHash } = await import("node:crypto")
85
+ const { hostname, userInfo } = await import("node:os")
86
+ const fp = createHash("sha256")
87
+ .update(`${hostname()}:${userInfo().username}:lpm-cli`)
88
+ .digest("hex")
89
+
90
+ const resp = await fetch(`${registryUrl}/api/cli/refresh`, {
91
+ method: "POST",
92
+ headers: { "Content-Type": "application/json" },
93
+ body: JSON.stringify({ refreshToken, deviceFingerprint: fp }),
94
+ })
95
+
96
+ if (!resp.ok) return null
97
+
98
+ const data = await resp.json()
99
+ if (data.token) {
100
+ await setToken(data.token)
101
+ if (data.refreshToken) {
102
+ await setRefreshToken(data.refreshToken)
103
+ }
104
+ return data.token
105
+ }
106
+ } catch {
107
+ // Refresh failed — fall through to auth error
108
+ }
109
+ return null
110
+ }
111
+
66
112
  /**
67
113
  * @typedef {Object} RequestOptions
68
114
  * @property {Record<string, string>} [headers] - Request headers
@@ -113,9 +159,8 @@ export async function request(path, options = {}) {
113
159
 
114
160
  clearTimeout(timeoutId)
115
161
 
116
- // Handle authentication errors immediately (no retry)
117
- // Exception: 2FA OTP responses (401 with OTP_REQUIRED/OTP_INVALID code)
118
- // must be returned to the caller so the publish handler can prompt for OTP
162
+ // Handle authentication errors — try silent refresh before failing
163
+ // Exception: 2FA OTP responses must be returned to the caller
119
164
  if (response.status === 401) {
120
165
  const cloned = response.clone()
121
166
  let data = {}
@@ -127,6 +172,16 @@ export async function request(path, options = {}) {
127
172
  if (data.code === "OTP_REQUIRED" || data.code === "OTP_INVALID") {
128
173
  return response
129
174
  }
175
+
176
+ // Attempt silent refresh (Feature 44 Part B) — one retry on 401
177
+ if (attempt === 0) {
178
+ const refreshed = await tryRefresh()
179
+ if (refreshed) {
180
+ // Update the auth header and retry this request
181
+ headers.Authorization = `Bearer ${refreshed}`
182
+ continue
183
+ }
184
+ }
130
185
  throw new Error(ERROR_MESSAGES.notAuthenticated)
131
186
  }
132
187
 
@@ -1,7 +1,9 @@
1
+ import { createHash } from "node:crypto"
1
2
  import http from "node:http"
3
+ import { hostname, userInfo } from "node:os"
2
4
  import open from "open"
3
5
  import { request } from "../api.js"
4
- import { getRegistryUrl, setToken } from "../config.js"
6
+ import { getRegistryUrl, setRefreshToken, setToken } from "../config.js"
5
7
  import { WARNING_MESSAGES } from "../constants.js"
6
8
  import { createSpinner, log, printHeader } from "../ui.js"
7
9
 
@@ -188,11 +190,52 @@ export async function login() {
188
190
  res.setHeader("Access-Control-Allow-Origin", "*")
189
191
 
190
192
  if (url.pathname === "/callback") {
191
- const token = url.searchParams.get("token")
193
+ // Exchange code flow (Feature 42) — code is exchanged for token server-side
194
+ const exchangeCode = url.searchParams.get("code")
195
+ // Legacy fallback: direct token in URL (for older server versions)
196
+ const directToken = url.searchParams.get("token")
197
+
198
+ let token = directToken
199
+ let refreshToken = null
200
+ if (exchangeCode && !directToken) {
201
+ try {
202
+ const exchangeRes = await fetch(`${registryUrl}/api/cli/exchange`, {
203
+ method: "POST",
204
+ headers: { "Content-Type": "application/json" },
205
+ body: JSON.stringify({ code: exchangeCode }),
206
+ })
207
+ if (exchangeRes.ok) {
208
+ const data = await exchangeRes.json()
209
+ token = data.token
210
+ // Feature 44: store refresh token if server provides one
211
+ if (data.refreshToken) {
212
+ refreshToken = data.refreshToken
213
+ }
214
+ } else {
215
+ const err = await exchangeRes.json().catch(() => ({}))
216
+ throw new Error(err.error || "Exchange failed")
217
+ }
218
+ } catch (err) {
219
+ spinner.fail(`Token exchange failed: ${err.message}`)
220
+ res.writeHead(200, { "Content-Type": "text/html" })
221
+ res.end(
222
+ getErrorHtml(
223
+ "Exchange Failed",
224
+ "Failed to exchange the authorization code. Please try again.",
225
+ ),
226
+ )
227
+ closeAndExit(res)
228
+ return
229
+ }
230
+ }
192
231
 
193
232
  if (token) {
194
- // Use async setToken
233
+ // Store access token
195
234
  await setToken(token)
235
+ // Store refresh token for session auth (Feature 44 Part B)
236
+ if (refreshToken) {
237
+ await setRefreshToken(refreshToken)
238
+ }
196
239
 
197
240
  try {
198
241
  const response = await request("/-/whoami")
@@ -535,10 +578,15 @@ export async function login() {
535
578
  }
536
579
  })
537
580
 
581
+ // Compute device fingerprint for session auth (Feature 44 Part B)
582
+ const fp = createHash("sha256")
583
+ .update(`${hostname()}:${userInfo().username}:lpm-cli`)
584
+ .digest("hex")
585
+ const dn = encodeURIComponent(hostname())
586
+
538
587
  server.listen(0, async () => {
539
588
  const port = server.address().port
540
- // Assuming registryUrl is the base URL of the Next.js app
541
- const loginUrl = `${registryUrl}/cli/login?port=${port}`
589
+ const loginUrl = `${registryUrl}/cli/login?port=${port}&fp=${fp}&dn=${dn}`
542
590
 
543
591
  console.log(`Opening browser to: ${loginUrl}`)
544
592
  await open(loginUrl)
package/lib/config.js CHANGED
@@ -118,6 +118,26 @@ export async function clearToken() {
118
118
  config.delete("token")
119
119
  }
120
120
 
121
+ /**
122
+ * Store a refresh token in secure storage (Feature 44 Part B).
123
+ * Uses the same secure-store as auth tokens with a "refresh:" prefix.
124
+ * @param {string} refreshToken
125
+ * @returns {Promise<void>}
126
+ */
127
+ export async function setRefreshToken(refreshToken) {
128
+ const registryUrl = getRegistryUrl()
129
+ await secureSetToken(refreshToken, `refresh:${registryUrl}`)
130
+ }
131
+
132
+ /**
133
+ * Get the stored refresh token from secure storage.
134
+ * @returns {Promise<string|null>}
135
+ */
136
+ export async function getRefreshToken() {
137
+ const registryUrl = getRegistryUrl()
138
+ return secureGetToken(`refresh:${registryUrl}`)
139
+ }
140
+
121
141
  // ============================================================================
122
142
  // Registry URL Configuration
123
143
  // ============================================================================
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@lpm-registry/cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.2",
4
4
  "description": "CLI for Licensed Package Manager",
5
5
  "author": "LPM <hello@lpm.dev> (https://lpm.dev)",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/lpm-dev/cli"
8
+ "url": "https://github.com/lpm-dev/rust-client"
9
9
  },
10
10
  "homepage": "https://lpm.dev",
11
11
  "license": "ISC",
@@ -61,11 +61,11 @@
61
61
  "xcode": "^3.0.1"
62
62
  },
63
63
  "optionalDependencies": {
64
- "@lpm-registry/cli-darwin-arm64": "0.8.0",
65
- "@lpm-registry/cli-darwin-x64": "0.8.0",
66
- "@lpm-registry/cli-linux-x64": "0.8.0",
67
- "@lpm-registry/cli-linux-arm64": "0.8.0",
68
- "@lpm-registry/cli-win32-x64": "0.8.0",
64
+ "@lpm-registry/cli-darwin-arm64": "0.9.2",
65
+ "@lpm-registry/cli-darwin-x64": "0.9.2",
66
+ "@lpm-registry/cli-linux-x64": "0.9.2",
67
+ "@lpm-registry/cli-linux-arm64": "0.9.2",
68
+ "@lpm-registry/cli-win32-x64": "0.9.2",
69
69
  "keytar": "^7.9.0"
70
70
  },
71
71
  "type": "module",
package/bin/lpm-bin DELETED
@@ -1,347 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * LPM CLI Entry Point — Minimal fallback wrapper.
5
- *
6
- * In most cases, this file has been replaced by a hard link to the native
7
- * Rust binary during postinstall (zero Node.js overhead). This JS wrapper
8
- * only runs when:
9
- *
10
- * 1. Hard-link failed (cross-device mount, permissions)
11
- * 2. npm rebuild recreated the symlink
12
- * 3. Binary download failed during postinstall
13
- *
14
- * Resolution order:
15
- * 1. Try platform package binary (optionalDependencies)
16
- * 2. Try lpm-bin in bin/ directory (GitHub Releases download)
17
- * 3. Fall through to JS CLI implementation
18
- */
19
-
20
- import { execFileSync } from "node:child_process"
21
- import fs from "node:fs"
22
- import { createRequire } from "node:module"
23
- import os from "node:os"
24
- import path from "node:path"
25
- import { fileURLToPath } from "node:url"
26
-
27
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
28
- const require = createRequire(import.meta.url)
29
-
30
- // ─── Try native binary (Tier 1: platform package) ──────────────────
31
-
32
- const PLATFORMS = {
33
- "darwin-arm64": { pkg: "@lpm-registry/cli-darwin-arm64", binary: "lpm" },
34
- "darwin-x64": { pkg: "@lpm-registry/cli-darwin-x64", binary: "lpm" },
35
- "linux-x64": { pkg: "@lpm-registry/cli-linux-x64", binary: "lpm" },
36
- "linux-arm64": { pkg: "@lpm-registry/cli-linux-arm64", binary: "lpm" },
37
- "win32-x64": { pkg: "@lpm-registry/cli-win32-x64", binary: "lpm.exe" },
38
- }
39
-
40
- // Allow override via environment variable (for debugging / custom builds)
41
- const envBinary = process.env.LPM_BINARY_PATH
42
- if (envBinary && fs.existsSync(envBinary)) {
43
- runBinary(envBinary)
44
- }
45
-
46
- // Try platform package from optionalDependencies
47
- const platform = `${process.platform}-${os.arch()}`
48
- const platformInfo = PLATFORMS[platform]
49
-
50
- if (platformInfo) {
51
- try {
52
- const pkgJsonPath = require.resolve(`${platformInfo.pkg}/package.json`)
53
- const binaryPath = path.join(path.dirname(pkgJsonPath), platformInfo.binary)
54
- if (fs.existsSync(binaryPath)) {
55
- runBinary(binaryPath)
56
- }
57
- } catch {
58
- // Package not installed
59
- }
60
- }
61
-
62
- // ─── Try native binary (Tier 2: downloaded by postinstall) ─────────
63
-
64
- const legacyBinaryName =
65
- process.platform === "win32" ? "lpm-bin.exe" : "lpm-bin"
66
- const legacyBinaryPath = path.join(__dirname, legacyBinaryName)
67
-
68
- if (fs.existsSync(legacyBinaryPath)) {
69
- runBinary(legacyBinaryPath)
70
- }
71
-
72
- // ─── JS CLI fallback (Tier 3) ──────────────────────────────────────
73
-
74
- import { Command } from "commander"
75
- import updateNotifier from "update-notifier"
76
- import { add } from "../lib/commands/add.js"
77
- import { audit } from "../lib/commands/audit.js"
78
- import { cache } from "../lib/commands/cache.js"
79
- import { checkName } from "../lib/commands/check-name.js"
80
- import { config } from "../lib/commands/config.js"
81
- import { doctor } from "../lib/commands/doctor.js"
82
- import { info } from "../lib/commands/info.js"
83
- import { init } from "../lib/commands/init.js"
84
- import { install } from "../lib/commands/install.js"
85
- import { login } from "../lib/commands/login.js"
86
- import { logout } from "../lib/commands/logout.js"
87
- import { marketplaceCompare } from "../lib/commands/marketplace-compare.js"
88
- import { marketplaceEarnings } from "../lib/commands/marketplace-earnings.js"
89
- import { mcpRemove, mcpSetup, mcpStatus } from "../lib/commands/mcp-setup.js"
90
- import { npmrc } from "../lib/commands/npmrc.js"
91
- import { openDashboard } from "../lib/commands/open.js"
92
- import { outdated } from "../lib/commands/outdated.js"
93
- import { poolStats } from "../lib/commands/pool-stats.js"
94
- import { publish } from "../lib/commands/publish.js"
95
- import { quality } from "../lib/commands/quality.js"
96
- import { remove } from "../lib/commands/remove.js"
97
- import { run } from "../lib/commands/run.js"
98
- import { search } from "../lib/commands/search.js"
99
- import { setup } from "../lib/commands/setup.js"
100
- import {
101
- skillsClean,
102
- skillsInstall,
103
- skillsList,
104
- skillsValidate,
105
- } from "../lib/commands/skills.js"
106
- import { rotateToken } from "../lib/commands/token-rotate.js"
107
- import { uninstall } from "../lib/commands/uninstall.js"
108
- import { whoami } from "../lib/commands/whoami.js"
109
-
110
- // Load package.json
111
- const pkg = JSON.parse(
112
- fs.readFileSync(path.join(__dirname, "../package.json"), "utf-8"),
113
- )
114
-
115
- // Check for updates
116
- updateNotifier({ pkg }).notify()
117
-
118
- const program = new Command()
119
-
120
- program
121
- .name("lpm")
122
- .description("CLI for Licensed Package Manager")
123
- .version(pkg.version)
124
-
125
- // Authentication
126
- program
127
- .command("login")
128
- .alias("l")
129
- .description("Authenticate with the registry")
130
- .action(login)
131
- program
132
- .command("logout")
133
- .alias("lo")
134
- .description("Clear stored authentication token")
135
- .option("--revoke", "Also revoke the token on the server")
136
- .option("--clear-cache", "Clear local package cache")
137
- .action(logout)
138
- program
139
- .command("whoami")
140
- .description("Check current authenticated user")
141
- .option("--json", "Output in JSON format")
142
- .action(whoami)
143
-
144
- // Package Management
145
- program
146
- .command("init")
147
- .description("Interactively create a package.json for LPM")
148
- .action(init)
149
- program
150
- .command("install [packages...]")
151
- .alias("i")
152
- .description("Install packages")
153
- .option("--json", "Machine-readable JSON output")
154
- .option("--no-skills", "Skip fetching Agent Skills")
155
- .option("--pm <manager>", "Package manager to use")
156
- .action(install)
157
- program
158
- .command("uninstall <packages...>")
159
- .aliases(["un", "unlink"])
160
- .description("Uninstall packages")
161
- .option("--pm <manager>", "Package manager to use")
162
- .action(uninstall)
163
- program
164
- .command("publish")
165
- .alias("p")
166
- .description("Publish a package to the registry")
167
- .option("--check", "Quality check only")
168
- .option("--min-score <score>", "Minimum quality score")
169
- .option("--provenance", "Force OIDC provenance")
170
- .option("--dry-run", "Preview without publishing")
171
- .action(publish)
172
- program
173
- .command("add <package>")
174
- .description("Extract package source code")
175
- .option("-p, --path <path>", "Target directory")
176
- .option("-f, --force", "Overwrite existing files")
177
- .option("-y, --yes", "Accept defaults")
178
- .option("--alias <alias>", "Import alias prefix")
179
- .option("--target <name>", "Swift SPM target")
180
- .option("--json", "JSON output")
181
- .option("--dry-run", "Preview only")
182
- .action(add)
183
- program
184
- .command("remove <package>")
185
- .alias("rm")
186
- .description("Remove a previously added package")
187
- .action(remove)
188
-
189
- // Discovery
190
- program
191
- .command("search <query>")
192
- .description("Search packages")
193
- .option("--limit <n>", "Max results", "20")
194
- .option("--json", "JSON output")
195
- .action((q, o) => search(q, { ...o, limit: parseInt(o.limit, 10) }))
196
- program
197
- .command("info <package>")
198
- .description("Package info")
199
- .option("--json", "JSON output")
200
- .option("-a, --all-versions", "Show all versions")
201
- .action(info)
202
- program
203
- .command("check-name <name>")
204
- .description("Check name availability")
205
- .option("--json", "JSON output")
206
- .action(checkName)
207
- program
208
- .command("quality <package>")
209
- .description("Quality report")
210
- .option("--json", "JSON output")
211
- .action(quality)
212
-
213
- // Maintenance
214
- program
215
- .command("audit [action]")
216
- .description("Security audit")
217
- .option("--json", "JSON output")
218
- .option("--level <level>", "Minimum severity")
219
- .action(audit)
220
- program
221
- .command("outdated")
222
- .description("Check outdated deps")
223
- .option("--json", "JSON output")
224
- .option("--all", "Show all deps")
225
- .action(outdated)
226
- program.command("doctor").description("Health check").action(doctor)
227
-
228
- // Configuration
229
- program
230
- .command("setup")
231
- .description("Configure .npmrc")
232
- .option("-r, --registry <url>", "Custom registry URL")
233
- .option("--oidc", "Exchange OIDC token")
234
- .option("--scoped", "Scoped registry only")
235
- .action(setup)
236
- program
237
- .command("npmrc")
238
- .description("Generate read-only .npmrc token")
239
- .option("-d, --days <number>", "Token expiry days")
240
- .option("--scoped", "Scoped registry only")
241
- .action(npmrc)
242
- program
243
- .command("config [action] [key] [value]")
244
- .description("Manage CLI config")
245
- .action(config)
246
- program
247
- .command("set <key> <value>")
248
- .description('Shortcut for "lpm config set"')
249
- .action((k, v) => config("set", k, v))
250
- program
251
- .command("cache <action>")
252
- .description("Manage local cache")
253
- .action(cache)
254
-
255
- // Utility
256
- program
257
- .command("open")
258
- .description("Open dashboard in browser")
259
- .action(openDashboard)
260
- program
261
- .command("run <script>")
262
- .description("Run npm scripts")
263
- .allowUnknownOption()
264
- .action(run)
265
-
266
- // Subcommands
267
- const token = program.command("token").description("Token management")
268
- token.command("rotate").description("Rotate token").action(rotateToken)
269
-
270
- const pool = program.command("pool").description("Pool revenue")
271
- pool
272
- .command("stats")
273
- .description("Pool earnings")
274
- .option("--json", "JSON output")
275
- .action(poolStats)
276
-
277
- const mcp = program.command("mcp").description("MCP server")
278
- mcp
279
- .command("setup")
280
- .description("Configure MCP server")
281
- .option("--project", "Project-level config")
282
- .action(mcpSetup)
283
- mcp
284
- .command("remove")
285
- .description("Remove MCP server")
286
- .option("--project", "Project-level only")
287
- .action(mcpRemove)
288
- mcp
289
- .command("status")
290
- .description("MCP server status")
291
- .option("--verbose", "Show diagnostics")
292
- .action(mcpStatus)
293
-
294
- const skills = program.command("skills").description("Agent Skills")
295
- skills
296
- .command("validate")
297
- .description("Validate skills files")
298
- .option("--json", "JSON output")
299
- .action(skillsValidate)
300
- skills
301
- .command("install [package]")
302
- .description("Install skills")
303
- .option("--json", "JSON output")
304
- .action(skillsInstall)
305
- skills
306
- .command("list")
307
- .description("List available skills")
308
- .option("--json", "JSON output")
309
- .action(skillsList)
310
- skills
311
- .command("clean")
312
- .description("Remove installed skills")
313
- .option("--json", "JSON output")
314
- .action(skillsClean)
315
-
316
- const marketplace = program.command("marketplace").description("Marketplace")
317
- marketplace
318
- .command("compare <input>")
319
- .description("Find comparable packages")
320
- .option("--json", "JSON output")
321
- .option("--category <category>", "Filter by category")
322
- .option("--limit <n>", "Max results")
323
- .action(marketplaceCompare)
324
- marketplace
325
- .command("earnings")
326
- .description("Revenue summary")
327
- .option("--json", "JSON output")
328
- .action(marketplaceEarnings)
329
-
330
- program.parse()
331
-
332
- // ─── Helper ────────────────────────────────────────────────────────
333
-
334
- function runBinary(binaryPath) {
335
- try {
336
- execFileSync(binaryPath, process.argv.slice(2), {
337
- stdio: "inherit",
338
- env: process.env,
339
- })
340
- process.exit(0)
341
- } catch (err) {
342
- if (err.status != null) {
343
- process.exit(err.status)
344
- }
345
- // Binary failed to execute — fall through to JS CLI
346
- }
347
- }
package/bin/lpm.js.bak DELETED
@@ -1,347 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * LPM CLI Entry Point — Minimal fallback wrapper.
5
- *
6
- * In most cases, this file has been replaced by a hard link to the native
7
- * Rust binary during postinstall (zero Node.js overhead). This JS wrapper
8
- * only runs when:
9
- *
10
- * 1. Hard-link failed (cross-device mount, permissions)
11
- * 2. npm rebuild recreated the symlink
12
- * 3. Binary download failed during postinstall
13
- *
14
- * Resolution order:
15
- * 1. Try platform package binary (optionalDependencies)
16
- * 2. Try lpm-bin in bin/ directory (GitHub Releases download)
17
- * 3. Fall through to JS CLI implementation
18
- */
19
-
20
- import { execFileSync } from "node:child_process"
21
- import fs from "node:fs"
22
- import { createRequire } from "node:module"
23
- import os from "node:os"
24
- import path from "node:path"
25
- import { fileURLToPath } from "node:url"
26
-
27
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
28
- const require = createRequire(import.meta.url)
29
-
30
- // ─── Try native binary (Tier 1: platform package) ──────────────────
31
-
32
- const PLATFORMS = {
33
- "darwin-arm64": { pkg: "@lpm-registry/cli-darwin-arm64", binary: "lpm" },
34
- "darwin-x64": { pkg: "@lpm-registry/cli-darwin-x64", binary: "lpm" },
35
- "linux-x64": { pkg: "@lpm-registry/cli-linux-x64", binary: "lpm" },
36
- "linux-arm64": { pkg: "@lpm-registry/cli-linux-arm64", binary: "lpm" },
37
- "win32-x64": { pkg: "@lpm-registry/cli-win32-x64", binary: "lpm.exe" },
38
- }
39
-
40
- // Allow override via environment variable (for debugging / custom builds)
41
- const envBinary = process.env.LPM_BINARY_PATH
42
- if (envBinary && fs.existsSync(envBinary)) {
43
- runBinary(envBinary)
44
- }
45
-
46
- // Try platform package from optionalDependencies
47
- const platform = `${process.platform}-${os.arch()}`
48
- const platformInfo = PLATFORMS[platform]
49
-
50
- if (platformInfo) {
51
- try {
52
- const pkgJsonPath = require.resolve(`${platformInfo.pkg}/package.json`)
53
- const binaryPath = path.join(path.dirname(pkgJsonPath), platformInfo.binary)
54
- if (fs.existsSync(binaryPath)) {
55
- runBinary(binaryPath)
56
- }
57
- } catch {
58
- // Package not installed
59
- }
60
- }
61
-
62
- // ─── Try native binary (Tier 2: downloaded by postinstall) ─────────
63
-
64
- const legacyBinaryName =
65
- process.platform === "win32" ? "lpm-bin.exe" : "lpm-bin"
66
- const legacyBinaryPath = path.join(__dirname, legacyBinaryName)
67
-
68
- if (fs.existsSync(legacyBinaryPath)) {
69
- runBinary(legacyBinaryPath)
70
- }
71
-
72
- // ─── JS CLI fallback (Tier 3) ──────────────────────────────────────
73
-
74
- import { Command } from "commander"
75
- import updateNotifier from "update-notifier"
76
- import { add } from "../lib/commands/add.js"
77
- import { audit } from "../lib/commands/audit.js"
78
- import { cache } from "../lib/commands/cache.js"
79
- import { checkName } from "../lib/commands/check-name.js"
80
- import { config } from "../lib/commands/config.js"
81
- import { doctor } from "../lib/commands/doctor.js"
82
- import { info } from "../lib/commands/info.js"
83
- import { init } from "../lib/commands/init.js"
84
- import { install } from "../lib/commands/install.js"
85
- import { login } from "../lib/commands/login.js"
86
- import { logout } from "../lib/commands/logout.js"
87
- import { marketplaceCompare } from "../lib/commands/marketplace-compare.js"
88
- import { marketplaceEarnings } from "../lib/commands/marketplace-earnings.js"
89
- import { mcpRemove, mcpSetup, mcpStatus } from "../lib/commands/mcp-setup.js"
90
- import { npmrc } from "../lib/commands/npmrc.js"
91
- import { openDashboard } from "../lib/commands/open.js"
92
- import { outdated } from "../lib/commands/outdated.js"
93
- import { poolStats } from "../lib/commands/pool-stats.js"
94
- import { publish } from "../lib/commands/publish.js"
95
- import { quality } from "../lib/commands/quality.js"
96
- import { remove } from "../lib/commands/remove.js"
97
- import { run } from "../lib/commands/run.js"
98
- import { search } from "../lib/commands/search.js"
99
- import { setup } from "../lib/commands/setup.js"
100
- import {
101
- skillsClean,
102
- skillsInstall,
103
- skillsList,
104
- skillsValidate,
105
- } from "../lib/commands/skills.js"
106
- import { rotateToken } from "../lib/commands/token-rotate.js"
107
- import { uninstall } from "../lib/commands/uninstall.js"
108
- import { whoami } from "../lib/commands/whoami.js"
109
-
110
- // Load package.json
111
- const pkg = JSON.parse(
112
- fs.readFileSync(path.join(__dirname, "../package.json"), "utf-8"),
113
- )
114
-
115
- // Check for updates
116
- updateNotifier({ pkg }).notify()
117
-
118
- const program = new Command()
119
-
120
- program
121
- .name("lpm")
122
- .description("CLI for Licensed Package Manager")
123
- .version(pkg.version)
124
-
125
- // Authentication
126
- program
127
- .command("login")
128
- .alias("l")
129
- .description("Authenticate with the registry")
130
- .action(login)
131
- program
132
- .command("logout")
133
- .alias("lo")
134
- .description("Clear stored authentication token")
135
- .option("--revoke", "Also revoke the token on the server")
136
- .option("--clear-cache", "Clear local package cache")
137
- .action(logout)
138
- program
139
- .command("whoami")
140
- .description("Check current authenticated user")
141
- .option("--json", "Output in JSON format")
142
- .action(whoami)
143
-
144
- // Package Management
145
- program
146
- .command("init")
147
- .description("Interactively create a package.json for LPM")
148
- .action(init)
149
- program
150
- .command("install [packages...]")
151
- .alias("i")
152
- .description("Install packages")
153
- .option("--json", "Machine-readable JSON output")
154
- .option("--no-skills", "Skip fetching Agent Skills")
155
- .option("--pm <manager>", "Package manager to use")
156
- .action(install)
157
- program
158
- .command("uninstall <packages...>")
159
- .aliases(["un", "unlink"])
160
- .description("Uninstall packages")
161
- .option("--pm <manager>", "Package manager to use")
162
- .action(uninstall)
163
- program
164
- .command("publish")
165
- .alias("p")
166
- .description("Publish a package to the registry")
167
- .option("--check", "Quality check only")
168
- .option("--min-score <score>", "Minimum quality score")
169
- .option("--provenance", "Force OIDC provenance")
170
- .option("--dry-run", "Preview without publishing")
171
- .action(publish)
172
- program
173
- .command("add <package>")
174
- .description("Extract package source code")
175
- .option("-p, --path <path>", "Target directory")
176
- .option("-f, --force", "Overwrite existing files")
177
- .option("-y, --yes", "Accept defaults")
178
- .option("--alias <alias>", "Import alias prefix")
179
- .option("--target <name>", "Swift SPM target")
180
- .option("--json", "JSON output")
181
- .option("--dry-run", "Preview only")
182
- .action(add)
183
- program
184
- .command("remove <package>")
185
- .alias("rm")
186
- .description("Remove a previously added package")
187
- .action(remove)
188
-
189
- // Discovery
190
- program
191
- .command("search <query>")
192
- .description("Search packages")
193
- .option("--limit <n>", "Max results", "20")
194
- .option("--json", "JSON output")
195
- .action((q, o) => search(q, { ...o, limit: parseInt(o.limit, 10) }))
196
- program
197
- .command("info <package>")
198
- .description("Package info")
199
- .option("--json", "JSON output")
200
- .option("-a, --all-versions", "Show all versions")
201
- .action(info)
202
- program
203
- .command("check-name <name>")
204
- .description("Check name availability")
205
- .option("--json", "JSON output")
206
- .action(checkName)
207
- program
208
- .command("quality <package>")
209
- .description("Quality report")
210
- .option("--json", "JSON output")
211
- .action(quality)
212
-
213
- // Maintenance
214
- program
215
- .command("audit [action]")
216
- .description("Security audit")
217
- .option("--json", "JSON output")
218
- .option("--level <level>", "Minimum severity")
219
- .action(audit)
220
- program
221
- .command("outdated")
222
- .description("Check outdated deps")
223
- .option("--json", "JSON output")
224
- .option("--all", "Show all deps")
225
- .action(outdated)
226
- program.command("doctor").description("Health check").action(doctor)
227
-
228
- // Configuration
229
- program
230
- .command("setup")
231
- .description("Configure .npmrc")
232
- .option("-r, --registry <url>", "Custom registry URL")
233
- .option("--oidc", "Exchange OIDC token")
234
- .option("--scoped", "Scoped registry only")
235
- .action(setup)
236
- program
237
- .command("npmrc")
238
- .description("Generate read-only .npmrc token")
239
- .option("-d, --days <number>", "Token expiry days")
240
- .option("--scoped", "Scoped registry only")
241
- .action(npmrc)
242
- program
243
- .command("config [action] [key] [value]")
244
- .description("Manage CLI config")
245
- .action(config)
246
- program
247
- .command("set <key> <value>")
248
- .description('Shortcut for "lpm config set"')
249
- .action((k, v) => config("set", k, v))
250
- program
251
- .command("cache <action>")
252
- .description("Manage local cache")
253
- .action(cache)
254
-
255
- // Utility
256
- program
257
- .command("open")
258
- .description("Open dashboard in browser")
259
- .action(openDashboard)
260
- program
261
- .command("run <script>")
262
- .description("Run npm scripts")
263
- .allowUnknownOption()
264
- .action(run)
265
-
266
- // Subcommands
267
- const token = program.command("token").description("Token management")
268
- token.command("rotate").description("Rotate token").action(rotateToken)
269
-
270
- const pool = program.command("pool").description("Pool revenue")
271
- pool
272
- .command("stats")
273
- .description("Pool earnings")
274
- .option("--json", "JSON output")
275
- .action(poolStats)
276
-
277
- const mcp = program.command("mcp").description("MCP server")
278
- mcp
279
- .command("setup")
280
- .description("Configure MCP server")
281
- .option("--project", "Project-level config")
282
- .action(mcpSetup)
283
- mcp
284
- .command("remove")
285
- .description("Remove MCP server")
286
- .option("--project", "Project-level only")
287
- .action(mcpRemove)
288
- mcp
289
- .command("status")
290
- .description("MCP server status")
291
- .option("--verbose", "Show diagnostics")
292
- .action(mcpStatus)
293
-
294
- const skills = program.command("skills").description("Agent Skills")
295
- skills
296
- .command("validate")
297
- .description("Validate skills files")
298
- .option("--json", "JSON output")
299
- .action(skillsValidate)
300
- skills
301
- .command("install [package]")
302
- .description("Install skills")
303
- .option("--json", "JSON output")
304
- .action(skillsInstall)
305
- skills
306
- .command("list")
307
- .description("List available skills")
308
- .option("--json", "JSON output")
309
- .action(skillsList)
310
- skills
311
- .command("clean")
312
- .description("Remove installed skills")
313
- .option("--json", "JSON output")
314
- .action(skillsClean)
315
-
316
- const marketplace = program.command("marketplace").description("Marketplace")
317
- marketplace
318
- .command("compare <input>")
319
- .description("Find comparable packages")
320
- .option("--json", "JSON output")
321
- .option("--category <category>", "Filter by category")
322
- .option("--limit <n>", "Max results")
323
- .action(marketplaceCompare)
324
- marketplace
325
- .command("earnings")
326
- .description("Revenue summary")
327
- .option("--json", "JSON output")
328
- .action(marketplaceEarnings)
329
-
330
- program.parse()
331
-
332
- // ─── Helper ────────────────────────────────────────────────────────
333
-
334
- function runBinary(binaryPath) {
335
- try {
336
- execFileSync(binaryPath, process.argv.slice(2), {
337
- stdio: "inherit",
338
- env: process.env,
339
- })
340
- process.exit(0)
341
- } catch (err) {
342
- if (err.status != null) {
343
- process.exit(err.status)
344
- }
345
- // Binary failed to execute — fall through to JS CLI
346
- }
347
- }