@lpm-registry/cli 0.58.0 → 0.59.1

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.
Files changed (62) hide show
  1. package/README.md +25 -44
  2. package/bin/lpm.js +2 -354
  3. package/bin/lpx.js +4 -0
  4. package/bin/native.js +154 -0
  5. package/package.json +42 -82
  6. package/scripts/install-binary.js +219 -550
  7. package/CHANGELOG.md +0 -197
  8. package/LICENSE +0 -15
  9. package/index.d.ts +0 -131
  10. package/index.js +0 -31
  11. package/lib/api.js +0 -398
  12. package/lib/commands/add.js +0 -1217
  13. package/lib/commands/audit.js +0 -283
  14. package/lib/commands/cache.js +0 -209
  15. package/lib/commands/check-name.js +0 -112
  16. package/lib/commands/config.js +0 -186
  17. package/lib/commands/doctor.js +0 -140
  18. package/lib/commands/info.js +0 -215
  19. package/lib/commands/init.js +0 -146
  20. package/lib/commands/install.js +0 -264
  21. package/lib/commands/login.js +0 -595
  22. package/lib/commands/logout.js +0 -94
  23. package/lib/commands/marketplace-compare.js +0 -164
  24. package/lib/commands/marketplace-earnings.js +0 -89
  25. package/lib/commands/mcp-setup.js +0 -363
  26. package/lib/commands/npmrc.js +0 -178
  27. package/lib/commands/open.js +0 -82
  28. package/lib/commands/outdated.js +0 -291
  29. package/lib/commands/pool-stats.js +0 -100
  30. package/lib/commands/publish.js +0 -949
  31. package/lib/commands/quality.js +0 -211
  32. package/lib/commands/remove.js +0 -82
  33. package/lib/commands/run.js +0 -14
  34. package/lib/commands/search.js +0 -143
  35. package/lib/commands/setup.js +0 -221
  36. package/lib/commands/skills.js +0 -898
  37. package/lib/commands/token-rotate.js +0 -25
  38. package/lib/commands/uninstall.js +0 -107
  39. package/lib/commands/whoami.js +0 -129
  40. package/lib/config.js +0 -328
  41. package/lib/constants.js +0 -190
  42. package/lib/ecosystem.js +0 -501
  43. package/lib/editor-skills.js +0 -321
  44. package/lib/editors.js +0 -215
  45. package/lib/import-rewriter.js +0 -364
  46. package/lib/install-targets/mcp-server.js +0 -245
  47. package/lib/install-targets/vscode-extension.js +0 -178
  48. package/lib/install-targets.js +0 -82
  49. package/lib/integrity.js +0 -179
  50. package/lib/lpm-config-prompts.js +0 -102
  51. package/lib/lpm-config.js +0 -408
  52. package/lib/oidc.js +0 -166
  53. package/lib/project-utils.js +0 -152
  54. package/lib/quality/checks.js +0 -687
  55. package/lib/quality/display.js +0 -155
  56. package/lib/quality/score.js +0 -118
  57. package/lib/quality/swift-checks.js +0 -447
  58. package/lib/safe-path.js +0 -180
  59. package/lib/secure-store.js +0 -321
  60. package/lib/swift-project.js +0 -637
  61. package/lib/ui.js +0 -40
  62. package/scripts/__tests__/install-binary.test.js +0 -346
@@ -1,571 +1,240 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * LPM CLI Binary Installer — 3-tier fallback + hard-link optimization.
5
- *
6
- * Runs as a postinstall hook. Resolution order:
7
- *
8
- * 1. optionalDependencies — platform package installed by npm (fastest, cached).
9
- * Integrity story: npm provenance attestation, verified by the npm registry
10
- * at install time. No extra check needed here.
11
- * 2. GitHub Releases — direct download if optionalDependencies missing
12
- * (--no-optional, corporate proxies blocking scoped packages, resolution
13
- * failure). Integrity story: every byte is anchored to a Sigstore-signed
14
- * `SHA256SUMS.txt` manifest published per release. The `sigstore` npm
15
- * package is consulted as an optional dependency — when resolvable, full
16
- * Sigstore bundle verification runs; when not, SHA-256 against the signed
17
- * manifest is the floor. Fail-closed on any integrity gate.
18
- * 3. Graceful fallback — JS CLI handles commands if Tier 2 returns null.
19
- *
20
- * After resolving the binary, hard-links it over bin/lpm.js so subsequent
21
- * runs execute the native binary directly (zero Node.js overhead).
22
- *
23
- * Set `LPM_INSTALL_INSECURE=1` to skip every Tier-2 integrity check (escape
24
- * valve for emergency installs from releases that predate the signed-install
25
- * gate). Emits a loud WARN on every use.
26
- *
27
- * Follows the esbuild postinstall pattern: https://github.com/evanw/esbuild
28
- */
29
-
30
- import { execSync } from "node:child_process"
31
- import crypto from "node:crypto"
32
- import fs from "node:fs"
33
- import https from "node:https"
34
- import { createRequire } from "node:module"
35
- import os from "node:os"
36
- import path from "node:path"
37
- import { fileURLToPath, pathToFileURL } from "node:url"
38
-
39
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
40
- const pkgDir = path.join(__dirname, "..")
41
- const binDir = path.join(pkgDir, "bin")
42
- const require = createRequire(import.meta.url)
43
-
44
- const PLATFORMS = {
45
- "darwin-arm64": { pkg: "@lpm-registry/cli-darwin-arm64", binary: "lpm" },
46
- "darwin-x64": { pkg: "@lpm-registry/cli-darwin-x64", binary: "lpm" },
47
- "linux-x64": { pkg: "@lpm-registry/cli-linux-x64", binary: "lpm" },
48
- "linux-arm64": { pkg: "@lpm-registry/cli-linux-arm64", binary: "lpm" },
49
- "win32-x64": { pkg: "@lpm-registry/cli-win32-x64", binary: "lpm.exe" },
50
- }
51
-
52
- const GITHUB_RELEASES =
53
- "https://github.com/lpm-dev/rust-client/releases/download"
54
- const GITHUB_BINARY_NAMES = {
55
- "darwin-arm64": "lpm-darwin-arm64",
56
- "darwin-x64": "lpm-darwin-x64",
57
- "linux-x64": "lpm-linux-x64",
58
- "linux-arm64": "lpm-linux-arm64",
59
- "win32-x64": "lpm-win32-x64.exe",
60
- }
61
-
62
- const MANIFEST_MAX_BYTES = 64 * 1024
63
- const BUNDLE_MAX_BYTES = 1024 * 1024
64
- const ASSET_MAX_BYTES = 256 * 1024 * 1024
65
-
66
- const SIGSTORE_IDENTITY = {
67
- certificateIdentityRegExp:
68
- "^https://github\\.com/lpm-dev/rust-client/\\.github/workflows/release\\.yml@.+",
69
- certificateOIDCIssuer: "https://token.actions.githubusercontent.com",
70
- }
71
-
72
- export async function main() {
73
- const platform = `${process.platform}-${os.arch()}`
74
- const platformInfo = PLATFORMS[platform]
75
-
76
- if (!platformInfo) {
77
- console.log(
78
- `[lpm] No pre-built binary for ${platform}. Using JS CLI fallback.`,
79
- )
80
- return
81
- }
82
-
83
- const pkg = JSON.parse(
84
- fs.readFileSync(path.join(pkgDir, "package.json"), "utf-8"),
85
- )
86
-
87
- let binaryPath = tryOptionalDependency(platformInfo)
88
-
89
- if (!binaryPath) {
90
- const githubBinaryName = GITHUB_BINARY_NAMES[platform]
91
- if (githubBinaryName) {
92
- binaryPath = await tryGitHubDownload(pkg.version, githubBinaryName)
93
- }
94
- }
95
-
96
- if (!binaryPath) {
97
- console.log("[lpm] Binary not available. Using JS CLI fallback.")
98
- return
99
- }
100
-
101
- if (process.platform !== "win32") {
102
- tryHardLink(binaryPath)
103
- } else {
104
- tryCopyBinary(binaryPath)
105
- }
106
- }
107
-
108
- function tryOptionalDependency(platformInfo) {
109
- try {
110
- const pkgJsonPath = require.resolve(`${platformInfo.pkg}/package.json`)
111
- const platformPkgDir = path.dirname(pkgJsonPath)
112
- const binaryPath = path.join(platformPkgDir, platformInfo.binary)
113
-
114
- if (fs.existsSync(binaryPath)) {
115
- try {
116
- execSync(`"${binaryPath}" --version`, {
117
- stdio: "ignore",
118
- timeout: 5000,
119
- })
120
- console.log(`[lpm] Using native binary from ${platformInfo.pkg}`)
121
- return binaryPath
122
- } catch {
123
- console.log(
124
- `[lpm] Binary from ${platformInfo.pkg} exists but not executable.`,
125
- )
126
- }
127
- }
128
- } catch {
129
- // Package not installed (--no-optional, or resolution failed).
130
- }
131
-
132
- return null
1
+ import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import {
8
+ LpmWrapperError,
9
+ PLATFORMS,
10
+ platformKey,
11
+ resolveNativeBinary,
12
+ } from "../bin/native.js";
13
+
14
+ const require = createRequire(import.meta.url);
15
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
16
+ const defaultWrapperRoot = path.resolve(scriptDir, "..");
17
+ const defaultValidateTimeoutMs = 30_000;
18
+ const commands = ["lpm", "lpx"];
19
+
20
+ export function installNativeBinaries(options = {}) {
21
+ const wrapperRoot = options.wrapperRoot ?? defaultWrapperRoot;
22
+ const platform = options.platform ?? process.platform;
23
+ const arch = options.arch ?? os.arch();
24
+ const env = options.env ?? process.env;
25
+ const requireFn = options.requireFn ?? require;
26
+ const fsModule = options.fsModule ?? fs;
27
+ const spawnSyncFn = options.spawnSyncFn ?? spawnSync;
28
+ const logFn = options.logFn ?? console.log;
29
+ const validateTimeoutMs =
30
+ options.validateTimeoutMs ?? defaultValidateTimeoutMs;
31
+
32
+ const key = platformKey(platform, arch);
33
+ const spec = PLATFORMS[key];
34
+ if (!spec) {
35
+ logFn(
36
+ `[lpm] No pre-built native npm package for ${key}; the JS wrapper will report this at runtime.`,
37
+ );
38
+ return { status: "unsupported", platform: key, optimized: [] };
39
+ }
40
+
41
+ const expectedVersion = readWrapperVersion(wrapperRoot, fsModule);
42
+ const lpmBinary = resolveNativeBinary({
43
+ command: "lpm",
44
+ platform,
45
+ arch,
46
+ env,
47
+ requireFn,
48
+ });
49
+
50
+ validateNativeBinary(lpmBinary.path, {
51
+ expectedVersion,
52
+ env,
53
+ spawnSyncFn,
54
+ validateTimeoutMs,
55
+ });
56
+
57
+ const optimized = [];
58
+ for (const command of commands) {
59
+ const native =
60
+ command === "lpm"
61
+ ? lpmBinary
62
+ : resolveNativeBinary({
63
+ command,
64
+ platform,
65
+ arch,
66
+ env,
67
+ requireFn,
68
+ });
69
+ const target = optimizedTargetPath(wrapperRoot, platform, command);
70
+ const action = stageOptimizedBinary({
71
+ source: native.path,
72
+ target,
73
+ platform,
74
+ fsModule,
75
+ });
76
+ optimized.push({
77
+ command,
78
+ source: native.path,
79
+ target,
80
+ action,
81
+ });
82
+ }
83
+
84
+ logFn(`[lpm] Installed native binary from ${spec.pkg}`);
85
+ return { status: "installed", platform: key, package: spec.pkg, optimized };
133
86
  }
134
87
 
135
- /**
136
- * Tier 2: download binary from GitHub Releases with end-to-end integrity.
137
- *
138
- * Returns the path to the verified, executable binary on success, or null
139
- * on any failure (manifest 404, SHA mismatch, Sigstore rejection, network
140
- * error). `null` falls through to the JS CLI fallback in `main` — Tier 2
141
- * never silently installs an unverified binary.
142
- *
143
- * Exported for testability — `vi.mock` of the network calls drives this
144
- * function directly.
145
- */
146
- export async function tryGitHubDownload(version, githubBinaryName, deps = {}) {
147
- const downloadFn = deps.download || download
148
- const downloadToBufferFn = deps.downloadToBuffer || downloadToBuffer
149
- const sha256FileFn = deps.sha256File || sha256File
150
- const verifyFn = deps.tryVerifyManifestSignature || tryVerifyManifestSignature
151
- const destDir = deps.destDir || binDir
152
- const insecure = process.env.LPM_INSTALL_INSECURE === "1"
153
-
154
- const manifestUrl = `${GITHUB_RELEASES}/v${version}/SHA256SUMS.txt`
155
- const bundleUrl = `${GITHUB_RELEASES}/v${version}/SHA256SUMS.txt.sigstore`
156
- const assetUrl = `${GITHUB_RELEASES}/v${version}/${githubBinaryName}`
157
-
158
- let manifestBody = null
159
- let bundleBody = null
160
-
161
- if (insecure) {
162
- console.log(
163
- "[lpm] WARN: LPM_INSTALL_INSECURE=1 — skipping ALL integrity verification on Tier-2 download",
164
- )
165
- } else {
166
- try {
167
- manifestBody = await downloadToBufferFn(manifestUrl, MANIFEST_MAX_BYTES)
168
- } catch (err) {
169
- if (err && err.statusCode === 404) {
170
- console.log(
171
- `[lpm] ERROR: release v${version} does not ship SHA256SUMS.txt — this release predates LPM's signed-install gate.`,
172
- )
173
- console.log(
174
- `[lpm] Install manually from https://github.com/lpm-dev/rust-client/releases/v${version}`,
175
- )
176
- console.log(
177
- "[lpm] Or set LPM_INSTALL_INSECURE=1 to skip integrity verification (NOT recommended).",
178
- )
179
- } else {
180
- console.log(
181
- `[lpm] ERROR: could not fetch signed manifest: ${err.message}`,
182
- )
183
- }
184
- return null
185
- }
186
- try {
187
- bundleBody = await downloadToBufferFn(bundleUrl, BUNDLE_MAX_BYTES)
188
- } catch {
189
- // Bundle 404 / fetch failure is not an attack signal by itself —
190
- // the SHA-256 gate against the manifest still runs. cosign /
191
- // sigstore verification is opportunistic on this channel.
192
- bundleBody = null
193
- }
194
-
195
- if (bundleBody) {
196
- const verdict = await verifyFn(manifestBody, bundleBody)
197
- if (verdict === "verified") {
198
- console.log(
199
- "[lpm] Verified Sigstore signature on manifest (identity-pinned to release.yml)",
200
- )
201
- } else if (verdict === "rejected") {
202
- // Verifier ran and refused — strong attack signal.
203
- console.log(
204
- "[lpm] ERROR: Sigstore verifier rejected the manifest signature.",
205
- )
206
- console.log(
207
- "[lpm] Refusing to install. See https://github.com/lpm-dev/rust-client/issues",
208
- )
209
- return null
210
- } else {
211
- // Optional `sigstore` package not resolvable — degrade to
212
- // SHA-only with a loud WARN so the user knows what to
213
- // install for the stronger posture.
214
- console.log(
215
- "[lpm] WARN: optional 'sigstore' package not available; falling back to SHA-256 against the signed manifest only.",
216
- )
217
- console.log(
218
- "[lpm] For Sigstore verification, run `npm install --include=optional` or unblock the sigstore package.",
219
- )
220
- }
221
- }
222
- }
223
-
224
- const destName = process.platform === "win32" ? "lpm-bin.exe" : "lpm-bin"
225
- const destPath = path.join(destDir, destName)
226
- const tmpPath = `${destPath}.tmp.${process.pid}`
227
-
228
- try {
229
- console.log("[lpm] Downloading native binary from GitHub Releases...")
230
- await downloadFn(assetUrl, tmpPath, 0, ASSET_MAX_BYTES)
231
- } catch (err) {
232
- console.log(`[lpm] Download failed: ${err.message}. Using JS CLI fallback.`)
233
- safeUnlink(tmpPath)
234
- return null
235
- }
236
-
237
- if (manifestBody) {
238
- const actualSha = await sha256FileFn(tmpPath)
239
- const expectedSha = parseManifestEntry(
240
- manifestBody.toString("utf-8"),
241
- githubBinaryName,
242
- )
243
- if (!expectedSha) {
244
- console.log(
245
- `[lpm] ERROR: manifest does not enumerate ${githubBinaryName}; release-pipeline bug.`,
246
- )
247
- safeUnlink(tmpPath)
248
- return null
249
- }
250
- if (actualSha !== expectedSha) {
251
- console.log(`[lpm] ERROR: SHA-256 mismatch for ${githubBinaryName}`)
252
- console.log(
253
- `[lpm] expected: ${expectedSha} (from signed manifest)`,
254
- )
255
- console.log(`[lpm] actual: ${actualSha}`)
256
- console.log("[lpm] Refusing to install — strong tampering signal.")
257
- safeUnlink(tmpPath)
258
- return null
259
- }
260
- console.log(`[lpm] Verified SHA-256: ${actualSha}`)
261
- }
262
-
263
- try {
264
- fs.renameSync(tmpPath, destPath)
265
- fs.chmodSync(destPath, 0o755)
266
- } catch (err) {
267
- console.log(`[lpm] Could not install verified binary: ${err.message}`)
268
- safeUnlink(tmpPath)
269
- return null
270
- }
271
-
272
- console.log("[lpm] Native binary installed from GitHub Releases.")
273
- return destPath
88
+ export function validateNativeBinary(
89
+ binaryPath,
90
+ {
91
+ expectedVersion = "",
92
+ env = process.env,
93
+ spawnSyncFn = spawnSync,
94
+ validateTimeoutMs = defaultValidateTimeoutMs,
95
+ } = {},
96
+ ) {
97
+ let result;
98
+ try {
99
+ result = spawnSyncFn(binaryPath, ["--version"], {
100
+ stdio: ["ignore", "pipe", "pipe"],
101
+ encoding: "utf8",
102
+ env,
103
+ timeout: validateTimeoutMs,
104
+ });
105
+ } catch (error) {
106
+ throw new Error(
107
+ `Native binary at ${binaryPath} could not be started: ${error.message}`,
108
+ { cause: error },
109
+ );
110
+ }
111
+
112
+ if (result.error) {
113
+ const timeout =
114
+ result.error.code === "ETIMEDOUT"
115
+ ? ` within ${Math.ceil(validateTimeoutMs / 1000)}s`
116
+ : "";
117
+ throw new Error(
118
+ `Native binary at ${binaryPath} did not report its version${timeout}: ${result.error.message}`,
119
+ { cause: result.error },
120
+ );
121
+ }
122
+
123
+ if (result.signal) {
124
+ throw new Error(
125
+ `Native binary at ${binaryPath} was interrupted by ${result.signal} while reporting its version.`,
126
+ );
127
+ }
128
+
129
+ if (result.status !== 0) {
130
+ const stderr = String(result.stderr ?? "").trim();
131
+ throw new Error(
132
+ `Native binary at ${binaryPath} exited with status ${result.status} while reporting its version.${stderr ? ` ${stderr}` : ""}`,
133
+ );
134
+ }
135
+
136
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
137
+ if (expectedVersion && !output.includes(expectedVersion)) {
138
+ throw new Error(
139
+ `Native binary at ${binaryPath} reported version ${JSON.stringify(output || "<empty>")} instead of ${expectedVersion}.`,
140
+ );
141
+ }
142
+
143
+ return output;
274
144
  }
275
145
 
276
- function tryHardLink(binaryPath) {
277
- const shimPath = path.join(binDir, "lpm.js")
278
- const backupPath = path.join(binDir, "lpm.js.bak")
279
-
280
- try {
281
- if (fs.existsSync(shimPath) && !fs.existsSync(backupPath)) {
282
- fs.copyFileSync(shimPath, backupPath)
283
- }
284
-
285
- if (fs.existsSync(shimPath)) {
286
- fs.unlinkSync(shimPath)
287
- }
288
- fs.linkSync(binaryPath, shimPath)
289
-
290
- console.log("[lpm] Hard-linked native binary (zero Node.js overhead)")
291
- } catch (err) {
292
- if (fs.existsSync(backupPath) && !fs.existsSync(shimPath)) {
293
- fs.copyFileSync(backupPath, shimPath)
294
- }
295
- console.log(`[lpm] Hard-link failed (${err.code}). Using JS wrapper.`)
296
- }
146
+ export function stageOptimizedBinary({ source, target, platform, fsModule = fs }) {
147
+ fsModule.mkdirSync(path.dirname(target), { recursive: true });
148
+ if (platform === "win32") {
149
+ atomicCopy(source, target, fsModule);
150
+ return "copy";
151
+ }
152
+
153
+ try {
154
+ atomicLink(source, target, fsModule);
155
+ return "link";
156
+ } catch {
157
+ atomicCopy(source, target, fsModule);
158
+ return "copy";
159
+ }
297
160
  }
298
161
 
299
- function tryCopyBinary(binaryPath) {
300
- const destPath = path.join(binDir, "lpm-bin.exe")
301
- try {
302
- fs.copyFileSync(binaryPath, destPath)
303
- console.log("[lpm] Copied native binary to bin/")
304
- } catch (err) {
305
- console.log(`[lpm] Copy failed: ${err.message}. Using JS wrapper.`)
306
- }
162
+ function optimizedTargetPath(wrapperRoot, platform, command) {
163
+ const filename = platform === "win32" ? `${command}.exe` : `${command}.js`;
164
+ return path.join(wrapperRoot, "bin", filename);
307
165
  }
308
166
 
309
- /**
310
- * Download a file from a URL, following redirects (GitHub → S3).
311
- *
312
- * HTTPS-only end-to-end: the entry check rejects any non-`https://`
313
- * URL, and redirect Locations are resolved against the current URL
314
- * before re-entering the function — so an attacker-controlled mirror
315
- * cannot downgrade transport mid-chain via `Location: http://…`.
316
- *
317
- * Streams to disk with a per-response byte cap so a compromised mirror
318
- * cannot exhaust disk by serving an unbounded body. The 5-redirect cap
319
- * is preserved from the pre-integrity-gate shape.
320
- *
321
- * Exported for testability.
322
- */
323
- export function download(url, dest, redirects = 0, maxBytes = ASSET_MAX_BYTES) {
324
- if (redirects > 5) return Promise.reject(new Error("Too many redirects"))
325
- if (!url.startsWith("https://")) {
326
- return Promise.reject(
327
- new Error(
328
- `refusing non-HTTPS transport: ${url} (Tier-2 download requires HTTPS end-to-end)`,
329
- ),
330
- )
331
- }
332
-
333
- return new Promise((resolve, reject) => {
334
- const req = https.get(url, res => {
335
- if (
336
- res.statusCode >= 300 &&
337
- res.statusCode < 400 &&
338
- res.headers.location
339
- ) {
340
- res.resume()
341
- let nextUrl
342
- try {
343
- nextUrl = new URL(res.headers.location, url).href
344
- } catch {
345
- reject(
346
- new Error(`invalid redirect Location: ${res.headers.location}`),
347
- )
348
- return
349
- }
350
- resolve(download(nextUrl, dest, redirects + 1, maxBytes))
351
- return
352
- }
353
-
354
- if (res.statusCode !== 200) {
355
- const err = new Error(`HTTP ${res.statusCode}`)
356
- err.statusCode = res.statusCode
357
- res.resume()
358
- reject(err)
359
- return
360
- }
361
-
362
- const advertised = Number(res.headers["content-length"])
363
- if (Number.isFinite(advertised) && advertised > maxBytes) {
364
- res.resume()
365
- reject(
366
- new Error(
367
- `response advertises ${advertised} bytes; cap of ${maxBytes} would be exceeded`,
368
- ),
369
- )
370
- return
371
- }
372
-
373
- let received = 0
374
- const out = fs.createWriteStream(dest)
375
- res.on("data", chunk => {
376
- received += chunk.length
377
- if (received > maxBytes) {
378
- out.destroy()
379
- res.destroy()
380
- safeUnlink(dest)
381
- reject(
382
- new Error(
383
- `response body exceeded cap of ${maxBytes} bytes mid-stream`,
384
- ),
385
- )
386
- }
387
- })
388
- res.pipe(out)
389
- out.on("finish", () => {
390
- out.close()
391
- resolve()
392
- })
393
- out.on("error", reject)
394
- })
395
- req.on("error", reject)
396
- })
167
+ function readWrapperVersion(wrapperRoot, fsModule) {
168
+ try {
169
+ const manifest = JSON.parse(
170
+ fsModule.readFileSync(path.join(wrapperRoot, "package.json"), "utf8"),
171
+ );
172
+ return typeof manifest.version === "string" ? manifest.version : "";
173
+ } catch {
174
+ return "";
175
+ }
397
176
  }
398
177
 
399
- /**
400
- * Buffered variant of [`download`] — fetches a bounded body into memory
401
- * and returns it as a Buffer. Used for the manifest + Sigstore bundle,
402
- * both of which are small and must be inspected as a whole before any
403
- * other byte is honored.
404
- *
405
- * HTTPS-only: identical transport-hardening posture as [`download`].
406
- *
407
- * Exported for testability.
408
- */
409
- export function downloadToBuffer(url, maxBytes, redirects = 0) {
410
- if (redirects > 5) return Promise.reject(new Error("Too many redirects"))
411
- if (!url.startsWith("https://")) {
412
- return Promise.reject(
413
- new Error(
414
- `refusing non-HTTPS transport: ${url} (Tier-2 download requires HTTPS end-to-end)`,
415
- ),
416
- )
417
- }
418
-
419
- return new Promise((resolve, reject) => {
420
- const req = https.get(url, res => {
421
- if (
422
- res.statusCode >= 300 &&
423
- res.statusCode < 400 &&
424
- res.headers.location
425
- ) {
426
- res.resume()
427
- let nextUrl
428
- try {
429
- nextUrl = new URL(res.headers.location, url).href
430
- } catch {
431
- reject(
432
- new Error(`invalid redirect Location: ${res.headers.location}`),
433
- )
434
- return
435
- }
436
- resolve(downloadToBuffer(nextUrl, maxBytes, redirects + 1))
437
- return
438
- }
439
-
440
- if (res.statusCode !== 200) {
441
- const err = new Error(`HTTP ${res.statusCode}`)
442
- err.statusCode = res.statusCode
443
- res.resume()
444
- reject(err)
445
- return
446
- }
447
-
448
- const advertised = Number(res.headers["content-length"])
449
- if (Number.isFinite(advertised) && advertised > maxBytes) {
450
- res.resume()
451
- reject(
452
- new Error(
453
- `response advertises ${advertised} bytes; cap of ${maxBytes} would be exceeded`,
454
- ),
455
- )
456
- return
457
- }
458
-
459
- const chunks = []
460
- let received = 0
461
- res.on("data", chunk => {
462
- received += chunk.length
463
- if (received > maxBytes) {
464
- res.destroy()
465
- reject(
466
- new Error(
467
- `response body exceeded cap of ${maxBytes} bytes mid-stream`,
468
- ),
469
- )
470
- return
471
- }
472
- chunks.push(chunk)
473
- })
474
- res.on("end", () => resolve(Buffer.concat(chunks)))
475
- res.on("error", reject)
476
- })
477
- req.on("error", reject)
478
- })
178
+ function atomicLink(source, target, fsModule) {
179
+ replaceWithTemp(target, fsModule, temp => {
180
+ fsModule.linkSync(source, temp);
181
+ fsModule.chmodSync(temp, 0o755);
182
+ });
479
183
  }
480
184
 
481
- /**
482
- * Compute the SHA-256 of a file as a lowercase hex string, streaming
483
- * the file so we never buffer the whole asset in memory.
484
- *
485
- * Exported for testability.
486
- */
487
- export function sha256File(filePath) {
488
- return new Promise((resolve, reject) => {
489
- const hash = crypto.createHash("sha256")
490
- const stream = fs.createReadStream(filePath)
491
- stream.on("data", chunk => hash.update(chunk))
492
- stream.on("end", () => resolve(hash.digest("hex")))
493
- stream.on("error", reject)
494
- })
185
+ function atomicCopy(source, target, fsModule) {
186
+ replaceWithTemp(target, fsModule, temp => {
187
+ fsModule.copyFileSync(source, temp);
188
+ fsModule.chmodSync(temp, 0o755);
189
+ });
495
190
  }
496
191
 
497
- /**
498
- * Look up the SHA-256 for `filename` in a parsed SHA256SUMS.txt body.
499
- *
500
- * Format: `<64-hex> <filename>\n` per line, two ASCII spaces between
501
- * digest and name (GNU coreutils default; macOS shasum -a 256 matches).
502
- * Malformed lines return null — a malformed manifest is an integrity
503
- * failure, not a silent miss; callers should refuse to install.
504
- *
505
- * Exported for testability.
506
- */
507
- export function parseManifestEntry(manifestText, filename) {
508
- const lines = manifestText.split("\n")
509
- for (const raw of lines) {
510
- const line = raw.replace(/\r$/, "")
511
- if (line.length === 0) continue
512
- const idx = line.indexOf(" ")
513
- if (idx !== 64) return null
514
- const digest = line.slice(0, 64).toLowerCase()
515
- if (!/^[0-9a-f]{64}$/.test(digest)) return null
516
- const name = line.slice(idx + 2).trim()
517
- if (name.length === 0) return null
518
- if (name === filename) return digest
519
- }
520
- return null
192
+ function replaceWithTemp(target, fsModule, writeTemp) {
193
+ const temp = `${target}.${process.pid}.${Date.now()}.tmp`;
194
+ let replaced = false;
195
+
196
+ try {
197
+ unlinkIfExists(temp, fsModule);
198
+ writeTemp(temp);
199
+ fsModule.renameSync(temp, target);
200
+ replaced = true;
201
+ } finally {
202
+ if (!replaced) {
203
+ unlinkIfExists(temp, fsModule);
204
+ }
205
+ }
521
206
  }
522
207
 
523
- function safeUnlink(p) {
524
- try {
525
- if (fs.existsSync(p)) fs.unlinkSync(p)
526
- } catch {
527
- // Best-effort.
528
- }
208
+ function unlinkIfExists(filePath, fsModule) {
209
+ try {
210
+ fsModule.unlinkSync(filePath);
211
+ } catch (error) {
212
+ if (error?.code !== "ENOENT") {
213
+ throw error;
214
+ }
215
+ }
529
216
  }
530
217
 
531
- /**
532
- * Verify a Sigstore bundle against a manifest body using the optional
533
- * `sigstore` package. Returns:
534
- *
535
- * "verified" — bundle verified under the lpm-dev/rust-client
536
- * release.yml identity pin.
537
- * "rejected" — sigstore package present and refused the bundle.
538
- * "unavailable" — sigstore package not resolvable (--no-optional,
539
- * firewalled, install failure). Caller degrades to
540
- * SHA-only.
541
- *
542
- * Exported for testability.
543
- */
544
- export async function tryVerifyManifestSignature(manifestBody, bundleBody) {
545
- let sigstore
546
- try {
547
- sigstore = await import("sigstore")
548
- } catch {
549
- return "unavailable"
550
- }
551
- try {
552
- const bundle = JSON.parse(bundleBody.toString("utf-8"))
553
- await sigstore.verify(bundle, manifestBody, SIGSTORE_IDENTITY)
554
- return "verified"
555
- } catch {
556
- return "rejected"
557
- }
218
+ function printInstallError(error) {
219
+ if (error instanceof LpmWrapperError) {
220
+ console.error(`[lpm] ${error.message}`);
221
+ for (const hint of error.hints) {
222
+ console.error(`[lpm] ${hint}`);
223
+ }
224
+ } else {
225
+ console.error(`[lpm] Failed to install native binary: ${error.message}`);
226
+ }
227
+
228
+ console.error(
229
+ "[lpm] Reinstall with optional dependencies and install scripts enabled: npm install -g @lpm-registry/cli",
230
+ );
558
231
  }
559
232
 
560
- // Only run `main` when this module is executed as a script (the
561
- // postinstall hook). Test files that `import { tryGitHubDownload, … }`
562
- // from this module should not trigger a real install attempt during
563
- // suite setup.
564
- const invokedAsScript =
565
- process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
566
-
567
- if (invokedAsScript) {
568
- main().catch(() => {
569
- // Silently fail — JS CLI fallback will handle all commands.
570
- })
233
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
234
+ try {
235
+ installNativeBinaries();
236
+ } catch (error) {
237
+ printInstallError(error);
238
+ process.exit(1);
239
+ }
571
240
  }