ludus-cli 0.5.1 → 0.7.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.
Files changed (4) hide show
  1. package/README.md +12 -0
  2. package/install.js +94 -24
  3. package/package.json +8 -7
  4. package/run.js +65 -25
package/README.md CHANGED
@@ -10,12 +10,24 @@ Ludus handles the entire workflow that would otherwise require dozens of manual
10
10
  npm install -g ludus-cli
11
11
  ```
12
12
 
13
+ Upgrade to the latest version the same way:
14
+
15
+ ```bash
16
+ npm install -g ludus-cli@latest
17
+ ```
18
+
13
19
  Or run directly:
14
20
 
15
21
  ```bash
16
22
  npx ludus-cli --help
17
23
  ```
18
24
 
25
+ The package downloads a small prebuilt binary on install. If your environment
26
+ blocks install scripts (e.g. `--ignore-scripts`, pnpm, locked-down CI), `ludus`
27
+ fetches the matching binary on first run instead — no extra steps. To manage the
28
+ binary yourself (air-gapped setups), set `LUDUS_SKIP_AUTO_DOWNLOAD=1` and place
29
+ the `ludus` binary under the package's `bin/` directory.
30
+
19
31
  ## What it does
20
32
 
21
33
  ```
package/install.js CHANGED
@@ -9,6 +9,7 @@ const { spawnSync } = require("child_process");
9
9
 
10
10
  const REPO = "jpvelasco/ludus";
11
11
  const MAX_REDIRECTS = 5;
12
+ const MARKER = ".installed-version";
12
13
 
13
14
  const PLATFORM_MAP = {
14
15
  linux: "linux",
@@ -21,6 +22,18 @@ const ARCH_MAP = {
21
22
  arm64: "arm64",
22
23
  };
23
24
 
25
+ // log writes routine progress to stderr (never stdout) so it can't corrupt
26
+ // `ludus mcp` JSON-RPC or `--json` output, and stays quiet when silent.
27
+ function log(silent, msg) {
28
+ if (!silent) {
29
+ console.error(msg);
30
+ }
31
+ }
32
+
33
+ function binaryName(platform = process.platform) {
34
+ return platform === "win32" ? "ludus.exe" : "ludus";
35
+ }
36
+
24
37
  function getPackageVersion() {
25
38
  const pkg = JSON.parse(
26
39
  fs.readFileSync(path.join(__dirname, "package.json"), "utf8")
@@ -52,6 +65,21 @@ function getArchiveName(version, platform, arch) {
52
65
  return `ludus_${version}_${os}_${cpu}.${ext}`;
53
66
  }
54
67
 
68
+ // needsDownload reports whether the binary must be (re)fetched: true when the
69
+ // binary is missing, or when the recorded marker version doesn't match the
70
+ // package version (drift after a skipped/failed install or an upgrade).
71
+ function needsDownload(binDir, version) {
72
+ if (!fs.existsSync(path.join(binDir, binaryName()))) {
73
+ return true;
74
+ }
75
+ try {
76
+ const installed = fs.readFileSync(path.join(binDir, MARKER), "utf8").trim();
77
+ return installed !== version;
78
+ } catch {
79
+ return true;
80
+ }
81
+ }
82
+
55
83
  function download(url, redirectCount = 0) {
56
84
  return new Promise((resolve, reject) => {
57
85
  if (redirectCount > MAX_REDIRECTS) {
@@ -75,10 +103,10 @@ function download(url, redirectCount = 0) {
75
103
  });
76
104
  }
77
105
 
78
- function verifyChecksum(buffer, archiveName) {
106
+ function verifyChecksum(buffer, archiveName, { silent = false } = {}) {
79
107
  const expected = getExpectedChecksum(archiveName);
80
108
  if (!expected) {
81
- console.log("ludus-cli: no checksum available, skipping verification");
109
+ log(silent, "ludus-cli: no checksum available, skipping verification");
82
110
  return;
83
111
  }
84
112
 
@@ -90,7 +118,7 @@ function verifyChecksum(buffer, archiveName) {
90
118
  ` Actual: ${actual}`
91
119
  );
92
120
  }
93
- console.log("ludus-cli: checksum verified (SHA-256)");
121
+ log(silent, "ludus-cli: checksum verified (SHA-256)");
94
122
  }
95
123
 
96
124
  // Escape a string for use inside PowerShell single quotes.
@@ -110,9 +138,25 @@ function spawnOrFail(cmd, args, label) {
110
138
  }
111
139
  }
112
140
 
141
+ // placeBinary moves src onto dest atomically. renameSync is atomic on POSIX and
142
+ // overwrites; on Windows it refuses to overwrite an existing file, so fall back
143
+ // to removing dest first (the binary is never running during a self-heal).
144
+ function placeBinary(src, dest) {
145
+ try {
146
+ fs.renameSync(src, dest);
147
+ } catch (err) {
148
+ if (err.code === "EEXIST" || err.code === "EPERM") {
149
+ fs.rmSync(dest, { force: true });
150
+ fs.renameSync(src, dest);
151
+ } else {
152
+ throw err;
153
+ }
154
+ }
155
+ }
156
+
113
157
  function extract(buffer, archiveName, binDir) {
114
- const tmpDir = path.join(__dirname, ".tmp-install");
115
- fs.mkdirSync(tmpDir, { recursive: true });
158
+ // Unique temp dir per run so concurrent invocations don't clobber each other.
159
+ const tmpDir = fs.mkdtempSync(path.join(__dirname, ".tmp-install-"));
116
160
 
117
161
  const archivePath = path.join(tmpDir, archiveName);
118
162
  fs.writeFileSync(archivePath, buffer);
@@ -137,48 +181,74 @@ function extract(buffer, archiveName, binDir) {
137
181
  }
138
182
 
139
183
  // Find the binary in the extracted files
140
- const binaryName = process.platform === "win32" ? "ludus.exe" : "ludus";
141
- const extractedBinary = path.join(tmpDir, binaryName);
184
+ const bn = binaryName();
185
+ const extractedBinary = path.join(tmpDir, bn);
142
186
 
143
187
  if (!fs.existsSync(extractedBinary)) {
144
- throw new Error(`Binary ${binaryName} not found in archive`);
188
+ throw new Error(`Binary ${bn} not found in archive`);
145
189
  }
146
190
 
147
- fs.mkdirSync(binDir, { recursive: true });
148
- const destBinary = path.join(binDir, binaryName);
149
- fs.copyFileSync(extractedBinary, destBinary);
150
-
151
191
  if (process.platform !== "win32") {
152
- fs.chmodSync(destBinary, 0o755);
192
+ fs.chmodSync(extractedBinary, 0o755);
153
193
  }
194
+
195
+ fs.mkdirSync(binDir, { recursive: true });
196
+ placeBinary(extractedBinary, path.join(binDir, bn));
154
197
  } finally {
155
198
  fs.rmSync(tmpDir, { recursive: true, force: true });
156
199
  }
157
200
  }
158
201
 
159
- async function main() {
202
+ // ensureBinary makes the platform binary present and matching the package
203
+ // version, downloading it if missing or stale. It is a no-op (cheap file reads
204
+ // only) when the binary already matches. Safe to call on every CLI invocation.
205
+ // All progress goes to stderr; routine chatter is suppressed when silent.
206
+ async function ensureBinary({ silent = false } = {}) {
160
207
  const version = getPackageVersion();
161
208
  if (version === "0.0.0") {
162
- console.error("ludus-cli: skipping binary download for development version");
209
+ log(silent, "ludus-cli: skipping binary download for development version");
210
+ return;
211
+ }
212
+
213
+ const binDir = path.join(__dirname, "bin");
214
+ if (!needsDownload(binDir, version)) {
163
215
  return;
164
216
  }
165
217
 
166
218
  const archiveName = getArchiveName(version, process.platform, process.arch);
167
219
  const url = `https://github.com/${REPO}/releases/download/v${version}/${archiveName}`;
168
- const binDir = path.join(__dirname, "bin");
169
220
 
170
- console.log(`ludus-cli: downloading ${archiveName}...`);
221
+ // One concise notice whenever an actual download happens (even when silent),
222
+ // so a runtime self-heal isn't a silent multi-second hang.
223
+ console.error(`ludus-cli: fetching ludus binary v${version}...`);
224
+
225
+ log(silent, `ludus-cli: downloading ${archiveName}...`);
171
226
  const buffer = await download(url);
172
227
 
173
- verifyChecksum(buffer, archiveName);
228
+ verifyChecksum(buffer, archiveName, { silent });
174
229
 
175
- console.log("ludus-cli: extracting binary...");
230
+ log(silent, "ludus-cli: extracting binary...");
176
231
  extract(buffer, archiveName, binDir);
177
232
 
178
- console.log("ludus-cli: installed successfully");
233
+ // Marker is written only after the binary is in place, so a crash mid-install
234
+ // never leaves a marker that falsely claims success.
235
+ fs.writeFileSync(path.join(binDir, MARKER), version);
236
+
237
+ log(silent, "ludus-cli: installed successfully");
179
238
  }
180
239
 
181
- main().catch((err) => {
182
- console.error(`ludus-cli: installation failed: ${err.message}`);
183
- process.exit(1);
184
- });
240
+ if (require.main === module) {
241
+ ensureBinary({ silent: false }).catch((err) => {
242
+ console.error(`ludus-cli: installation failed: ${err.message}`);
243
+ process.exit(1);
244
+ });
245
+ }
246
+
247
+ module.exports = {
248
+ ensureBinary,
249
+ needsDownload,
250
+ getArchiveName,
251
+ getPackageVersion,
252
+ binaryName,
253
+ MARKER,
254
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ludus-cli",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "UE5 dedicated server deployment CLI with MCP server",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -11,7 +11,8 @@
11
11
  "ludus": "run.js"
12
12
  },
13
13
  "scripts": {
14
- "postinstall": "node install.js"
14
+ "postinstall": "node install.js",
15
+ "test": "node --test"
15
16
  },
16
17
  "os": [
17
18
  "linux",
@@ -45,10 +46,10 @@
45
46
  "bin/"
46
47
  ],
47
48
  "binaryChecksums": {
48
- "ludus_0.5.1_darwin_amd64.tar.gz": "801e721ba4dd339bab512537aa123e4c9280a7a5363ef40bfd9d1e7e14a3de01",
49
- "ludus_0.5.1_darwin_arm64.tar.gz": "d685fd571e161c5d57369d0b1b3dd833f4db1b3a5c7d5bb8dbc5674edc242e1a",
50
- "ludus_0.5.1_linux_amd64.tar.gz": "e5df3d6a72f1543ea839472b1e5decd302d4730ea91beef97296cc74df229069",
51
- "ludus_0.5.1_linux_arm64.tar.gz": "152d90ddb78a4083083b077fe3747281f539834667f0a6c237e4c9caa4cd9ca3",
52
- "ludus_0.5.1_windows_amd64.zip": "145e9aab871620919d1964072a7ade05bc2b1cdbc654d2b457a7714fd049e4b3"
49
+ "ludus_0.7.0_darwin_amd64.tar.gz": "6d866a491c343655d9ddfed2e2af6646600ce932df9e92985ca5448fcee09f3d",
50
+ "ludus_0.7.0_darwin_arm64.tar.gz": "57b21a09beb9d38a8f222f3152fbf9672d1eb7f078ecf1a443e42d04551734e0",
51
+ "ludus_0.7.0_linux_amd64.tar.gz": "82916ed60bbd128dd767c66812a19083db99d682f370e42a73ae34336bdcd105",
52
+ "ludus_0.7.0_linux_arm64.tar.gz": "87c112969156723251cdf463fc5fdbc6cf889cd9cfac897bc67e06d64b1675f7",
53
+ "ludus_0.7.0_windows_amd64.zip": "2befdd47c92c52ae83bad674db36e372e9e4e744e9a362af052ff8cefe94597a"
53
54
  }
54
55
  }
package/run.js CHANGED
@@ -3,31 +3,71 @@
3
3
 
4
4
  const path = require("path");
5
5
  const { spawn } = require("child_process");
6
+ const { ensureBinary, binaryName } = require("./install.js");
6
7
 
7
- const binaryName = process.platform === "win32" ? "ludus.exe" : "ludus";
8
- const binaryPath = path.join(__dirname, "bin", binaryName);
9
-
10
- const child = spawn(binaryPath, process.argv.slice(2), {
11
- stdio: "inherit",
12
- });
13
-
14
- // Forward signals so the Go binary shuts down cleanly
15
- ["SIGINT", "SIGTERM", "SIGHUP"].forEach((sig) => {
16
- process.on(sig, () => child.kill(sig));
17
- });
18
-
19
- child.on("error", (err) => {
20
- if (err.code === "ENOENT") {
21
- console.error(
22
- `ludus-cli: binary not found at ${binaryPath}\n` +
23
- "Run 'npm rebuild ludus-cli' or reinstall the package."
24
- );
25
- } else {
26
- console.error(`ludus-cli: failed to start: ${err.message}`);
8
+ const binaryPath = path.join(__dirname, "bin", binaryName());
9
+
10
+ function spawnBinary() {
11
+ const child = spawn(binaryPath, process.argv.slice(2), {
12
+ stdio: "inherit",
13
+ });
14
+
15
+ // Forward signals so the Go binary shuts down cleanly
16
+ ["SIGINT", "SIGTERM", "SIGHUP"].forEach((sig) => {
17
+ process.on(sig, () => child.kill(sig));
18
+ });
19
+
20
+ child.on("error", (err) => {
21
+ if (err.code === "ENOENT") {
22
+ console.error(
23
+ `ludus-cli: binary not found at ${binaryPath}\n` +
24
+ "Reinstall with: npm install -g ludus-cli@latest"
25
+ );
26
+ } else {
27
+ console.error(`ludus-cli: failed to start: ${err.message}`);
28
+ }
29
+ process.exit(1);
30
+ });
31
+
32
+ child.on("exit", (code, signal) => {
33
+ process.exit(signal ? 1 : code || 0);
34
+ });
35
+ }
36
+
37
+ async function main() {
38
+ // Escape hatch: deliberately manage the binary yourself / air-gapped setups.
39
+ // Skip the self-heal and spawn whatever is present (ENOENT guidance if not).
40
+ if (process.env.LUDUS_SKIP_AUTO_DOWNLOAD) {
41
+ spawnBinary();
42
+ return;
43
+ }
44
+
45
+ try {
46
+ // Self-heal: if postinstall was skipped (ignore-scripts, pnpm), failed
47
+ // mid-download, or the binary drifted from this package version, fetch the
48
+ // correct one. No-op (cheap file reads) when already in sync. silent:true
49
+ // keeps routine chatter off; an actual download still prints one stderr line.
50
+ await ensureBinary({ silent: true });
51
+ } catch (err) {
52
+ const code = err && err.code;
53
+ if (code === "EACCES" || code === "EPERM") {
54
+ console.error(
55
+ `ludus-cli: cannot write the ludus binary (permission denied).\n` +
56
+ "Reinstall with appropriate privileges:\n" +
57
+ " sudo npm install -g ludus-cli@latest (macOS/Linux)\n" +
58
+ " run your shell as Administrator, then the same command (Windows)"
59
+ );
60
+ } else {
61
+ console.error(
62
+ `ludus-cli: could not fetch the ludus binary: ${err.message}\n` +
63
+ "Check your network/proxy and retry. If you manage the binary yourself,\n" +
64
+ "set LUDUS_SKIP_AUTO_DOWNLOAD=1 to bypass this step."
65
+ );
66
+ }
67
+ process.exit(1);
27
68
  }
28
- process.exit(1);
29
- });
30
69
 
31
- child.on("exit", (code, signal) => {
32
- process.exit(signal ? 1 : code || 0);
33
- });
70
+ spawnBinary();
71
+ }
72
+
73
+ main();