ludus-cli 0.5.1 → 0.6.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/README.md +12 -0
- package/install.js +94 -24
- package/package.json +8 -7
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
fs.
|
|
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
|
|
141
|
-
const extractedBinary = path.join(tmpDir,
|
|
184
|
+
const bn = binaryName();
|
|
185
|
+
const extractedBinary = path.join(tmpDir, bn);
|
|
142
186
|
|
|
143
187
|
if (!fs.existsSync(extractedBinary)) {
|
|
144
|
-
throw new Error(`Binary ${
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
230
|
+
log(silent, "ludus-cli: extracting binary...");
|
|
176
231
|
extract(buffer, archiveName, binDir);
|
|
177
232
|
|
|
178
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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.
|
|
3
|
+
"version": "0.6.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.
|
|
49
|
-
"ludus_0.
|
|
50
|
-
"ludus_0.
|
|
51
|
-
"ludus_0.
|
|
52
|
-
"ludus_0.
|
|
49
|
+
"ludus_0.6.0_darwin_amd64.tar.gz": "d66b9597f3447bf1a84711b5f4b0ff195211d7d756cf215884f76134759b76e7",
|
|
50
|
+
"ludus_0.6.0_darwin_arm64.tar.gz": "5d80680767046d27b7a3ce20ccd9eacd6b739b94e9637ce62ba34b94d2407219",
|
|
51
|
+
"ludus_0.6.0_linux_amd64.tar.gz": "d118225fa186f9981473bd21db5b023b9d69f2e1126e0946bfb9ae97fb500cbb",
|
|
52
|
+
"ludus_0.6.0_linux_arm64.tar.gz": "e7c696aab9d30bf888a7064e4fd377701ccb1f934d47e7734ee507c22499e2f9",
|
|
53
|
+
"ludus_0.6.0_windows_amd64.zip": "e7691965bd1c5405a2374f8bede3deb125f94f176fa185d64bb60f8e67d6970b"
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
11
|
-
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
// Forward signals so the Go binary shuts down cleanly
|
|
15
|
-
["SIGINT", "SIGTERM", "SIGHUP"].forEach((sig) => {
|
|
16
|
-
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
child.on("error", (err) => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
70
|
+
spawnBinary();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
main();
|