@lpm-registry/cli 0.8.0 → 0.10.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/lib/api.js +59 -4
- package/lib/commands/login.js +53 -5
- package/lib/config.js +20 -0
- package/package.json +7 -7
- package/bin/lpm-bin +0 -347
- package/bin/lpm.js.bak +0 -347
package/lib/api.js
CHANGED
|
@@ -9,7 +9,15 @@
|
|
|
9
9
|
* @module cli/lib/api
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
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
|
|
117
|
-
// Exception: 2FA OTP responses
|
|
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
|
|
package/lib/commands/login.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.10.0",
|
|
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/
|
|
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.
|
|
65
|
-
"@lpm-registry/cli-darwin-x64": "0.
|
|
66
|
-
"@lpm-registry/cli-linux-x64": "0.
|
|
67
|
-
"@lpm-registry/cli-linux-arm64": "0.
|
|
68
|
-
"@lpm-registry/cli-win32-x64": "0.
|
|
64
|
+
"@lpm-registry/cli-darwin-arm64": "0.10.0",
|
|
65
|
+
"@lpm-registry/cli-darwin-x64": "0.10.0",
|
|
66
|
+
"@lpm-registry/cli-linux-x64": "0.10.0",
|
|
67
|
+
"@lpm-registry/cli-linux-arm64": "0.10.0",
|
|
68
|
+
"@lpm-registry/cli-win32-x64": "0.10.0",
|
|
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
|
-
}
|