@lpm-registry/cli 0.59.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.
package/README.md CHANGED
@@ -8,7 +8,9 @@ npm install -g @lpm-registry/cli
8
8
 
9
9
  This package is the npm gateway for the native LPM CLI. It installs a small
10
10
  JavaScript launcher plus the matching platform package, such as
11
- `@lpm-registry/cli-darwin-arm64` or `@lpm-registry/cli-linux-x64`.
11
+ `@lpm-registry/cli-darwin-arm64` or `@lpm-registry/cli-linux-x64`. A tiny
12
+ `postinstall` verifier checks the native binary and wires the global command to
13
+ it where the platform allows that.
12
14
 
13
15
  The JavaScript launcher does not implement package-manager behavior. All CLI
14
16
  commands run in the native Rust binary.
@@ -36,5 +38,8 @@ with optional dependencies enabled:
36
38
  npm install -g @lpm-registry/cli
37
39
  ```
38
40
 
41
+ If install scripts were disabled, reinstall without `--ignore-scripts` so npm
42
+ can validate the native binary during installation.
43
+
39
44
  Other install methods are documented at
40
45
  [github.com/lpm-dev/rust-client](https://github.com/lpm-dev/rust-client).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lpm-registry/cli",
3
- "version": "0.59.0",
3
+ "version": "0.59.1",
4
4
  "description": "LPM CLI native binary installer for npm",
5
5
  "repository": {
6
6
  "type": "git",
@@ -22,20 +22,22 @@
22
22
  },
23
23
  "files": [
24
24
  "bin",
25
+ "scripts",
25
26
  "README.md"
26
27
  ],
27
28
  "optionalDependencies": {
28
- "@lpm-registry/cli-darwin-arm64": "0.59.0",
29
- "@lpm-registry/cli-darwin-x64": "0.59.0",
30
- "@lpm-registry/cli-linux-arm64": "0.59.0",
31
- "@lpm-registry/cli-linux-x64": "0.59.0",
32
- "@lpm-registry/cli-win32-x64": "0.59.0"
29
+ "@lpm-registry/cli-darwin-arm64": "0.59.1",
30
+ "@lpm-registry/cli-darwin-x64": "0.59.1",
31
+ "@lpm-registry/cli-linux-arm64": "0.59.1",
32
+ "@lpm-registry/cli-linux-x64": "0.59.1",
33
+ "@lpm-registry/cli-win32-x64": "0.59.1"
33
34
  },
34
35
  "type": "module",
35
36
  "engines": {
36
37
  "node": ">=18.0.0"
37
38
  },
38
39
  "scripts": {
40
+ "postinstall": "node scripts/install-binary.js",
39
41
  "test": "node --test tests/*.test.mjs",
40
42
  "pack:check": "npm pack --dry-run --json"
41
43
  }
@@ -0,0 +1,240 @@
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 };
86
+ }
87
+
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;
144
+ }
145
+
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
+ }
160
+ }
161
+
162
+ function optimizedTargetPath(wrapperRoot, platform, command) {
163
+ const filename = platform === "win32" ? `${command}.exe` : `${command}.js`;
164
+ return path.join(wrapperRoot, "bin", filename);
165
+ }
166
+
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
+ }
176
+ }
177
+
178
+ function atomicLink(source, target, fsModule) {
179
+ replaceWithTemp(target, fsModule, temp => {
180
+ fsModule.linkSync(source, temp);
181
+ fsModule.chmodSync(temp, 0o755);
182
+ });
183
+ }
184
+
185
+ function atomicCopy(source, target, fsModule) {
186
+ replaceWithTemp(target, fsModule, temp => {
187
+ fsModule.copyFileSync(source, temp);
188
+ fsModule.chmodSync(temp, 0o755);
189
+ });
190
+ }
191
+
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
+ }
206
+ }
207
+
208
+ function unlinkIfExists(filePath, fsModule) {
209
+ try {
210
+ fsModule.unlinkSync(filePath);
211
+ } catch (error) {
212
+ if (error?.code !== "ENOENT") {
213
+ throw error;
214
+ }
215
+ }
216
+ }
217
+
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
+ );
231
+ }
232
+
233
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
234
+ try {
235
+ installNativeBinaries();
236
+ } catch (error) {
237
+ printInstallError(error);
238
+ process.exit(1);
239
+ }
240
+ }