ludus-cli 0.1.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/install.js +184 -0
- package/package.json +37 -0
- package/run.js +33 -0
package/install.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const https = require("https");
|
|
7
|
+
const crypto = require("crypto");
|
|
8
|
+
const { spawnSync } = require("child_process");
|
|
9
|
+
|
|
10
|
+
const REPO = "jpvelasco/ludus";
|
|
11
|
+
const MAX_REDIRECTS = 5;
|
|
12
|
+
|
|
13
|
+
const PLATFORM_MAP = {
|
|
14
|
+
linux: "linux",
|
|
15
|
+
darwin: "darwin",
|
|
16
|
+
win32: "windows",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const ARCH_MAP = {
|
|
20
|
+
x64: "amd64",
|
|
21
|
+
arm64: "arm64",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function getPackageVersion() {
|
|
25
|
+
const pkg = JSON.parse(
|
|
26
|
+
fs.readFileSync(path.join(__dirname, "package.json"), "utf8")
|
|
27
|
+
);
|
|
28
|
+
const version = pkg.version;
|
|
29
|
+
|
|
30
|
+
// Validate semver format to prevent URL injection
|
|
31
|
+
if (!/^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?(\+[a-zA-Z0-9.]+)?$/.test(version)) {
|
|
32
|
+
throw new Error(`Invalid version format: ${version}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return version;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getExpectedChecksum(archiveName) {
|
|
39
|
+
const pkg = JSON.parse(
|
|
40
|
+
fs.readFileSync(path.join(__dirname, "package.json"), "utf8")
|
|
41
|
+
);
|
|
42
|
+
return pkg.binaryChecksums?.[archiveName] || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getArchiveName(version, platform, arch) {
|
|
46
|
+
const os = PLATFORM_MAP[platform];
|
|
47
|
+
const cpu = ARCH_MAP[arch];
|
|
48
|
+
if (!os || !cpu) {
|
|
49
|
+
throw new Error(`Unsupported platform: ${platform}/${arch}`);
|
|
50
|
+
}
|
|
51
|
+
const ext = platform === "win32" ? "zip" : "tar.gz";
|
|
52
|
+
return `ludus_${version}_${os}_${cpu}.${ext}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function download(url, redirectCount = 0) {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
if (redirectCount > MAX_REDIRECTS) {
|
|
58
|
+
return reject(new Error(`Too many redirects (max ${MAX_REDIRECTS})`));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
https
|
|
62
|
+
.get(url, (res) => {
|
|
63
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
64
|
+
return download(res.headers.location, redirectCount + 1).then(resolve, reject);
|
|
65
|
+
}
|
|
66
|
+
if (res.statusCode !== 200) {
|
|
67
|
+
return reject(new Error(`Download failed: HTTP ${res.statusCode} for ${url}`));
|
|
68
|
+
}
|
|
69
|
+
const chunks = [];
|
|
70
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
71
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
72
|
+
res.on("error", reject);
|
|
73
|
+
})
|
|
74
|
+
.on("error", reject);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function verifyChecksum(buffer, archiveName) {
|
|
79
|
+
const expected = getExpectedChecksum(archiveName);
|
|
80
|
+
if (!expected) {
|
|
81
|
+
console.log("ludus-cli: no checksum available, skipping verification");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const actual = crypto.createHash("sha256").update(buffer).digest("hex");
|
|
86
|
+
if (actual !== expected) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Checksum mismatch for ${archiveName}\n` +
|
|
89
|
+
` Expected: ${expected}\n` +
|
|
90
|
+
` Actual: ${actual}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
console.log("ludus-cli: checksum verified (SHA-256)");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Escape a string for use inside PowerShell single quotes.
|
|
97
|
+
// PowerShell single-quoted strings only need '' to represent a literal '.
|
|
98
|
+
function psEscape(s) {
|
|
99
|
+
return s.replace(/'/g, "''");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function spawnOrFail(cmd, args, label) {
|
|
103
|
+
const result = spawnSync(cmd, args, { stdio: "pipe" });
|
|
104
|
+
if (result.error) {
|
|
105
|
+
throw new Error(`${label}: ${result.error.message}`);
|
|
106
|
+
}
|
|
107
|
+
if (result.status !== 0) {
|
|
108
|
+
const stderr = result.stderr ? result.stderr.toString().trim() : "";
|
|
109
|
+
throw new Error(`${label} exited with code ${result.status}${stderr ? ": " + stderr : ""}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function extract(buffer, archiveName, binDir) {
|
|
114
|
+
const tmpDir = path.join(__dirname, ".tmp-install");
|
|
115
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
116
|
+
|
|
117
|
+
const archivePath = path.join(tmpDir, archiveName);
|
|
118
|
+
fs.writeFileSync(archivePath, buffer);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
if (archiveName.endsWith(".zip")) {
|
|
122
|
+
if (process.platform === "win32") {
|
|
123
|
+
spawnOrFail(
|
|
124
|
+
"powershell",
|
|
125
|
+
[
|
|
126
|
+
"-NoProfile",
|
|
127
|
+
"-Command",
|
|
128
|
+
`Expand-Archive -Force -Path '${psEscape(archivePath)}' -DestinationPath '${psEscape(tmpDir)}'`,
|
|
129
|
+
],
|
|
130
|
+
"Expand-Archive"
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
spawnOrFail("unzip", ["-o", archivePath, "-d", tmpDir], "unzip");
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
spawnOrFail("tar", ["-xzf", archivePath, "-C", tmpDir], "tar");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Find the binary in the extracted files
|
|
140
|
+
const binaryName = process.platform === "win32" ? "ludus.exe" : "ludus";
|
|
141
|
+
const extractedBinary = path.join(tmpDir, binaryName);
|
|
142
|
+
|
|
143
|
+
if (!fs.existsSync(extractedBinary)) {
|
|
144
|
+
throw new Error(`Binary ${binaryName} not found in archive`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
148
|
+
const destBinary = path.join(binDir, binaryName);
|
|
149
|
+
fs.copyFileSync(extractedBinary, destBinary);
|
|
150
|
+
|
|
151
|
+
if (process.platform !== "win32") {
|
|
152
|
+
fs.chmodSync(destBinary, 0o755);
|
|
153
|
+
}
|
|
154
|
+
} finally {
|
|
155
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function main() {
|
|
160
|
+
const version = getPackageVersion();
|
|
161
|
+
if (version === "0.0.0") {
|
|
162
|
+
console.error("ludus-cli: skipping binary download for development version");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const archiveName = getArchiveName(version, process.platform, process.arch);
|
|
167
|
+
const url = `https://github.com/${REPO}/releases/download/v${version}/${archiveName}`;
|
|
168
|
+
const binDir = path.join(__dirname, "bin");
|
|
169
|
+
|
|
170
|
+
console.log(`ludus-cli: downloading ${archiveName}...`);
|
|
171
|
+
const buffer = await download(url);
|
|
172
|
+
|
|
173
|
+
verifyChecksum(buffer, archiveName);
|
|
174
|
+
|
|
175
|
+
console.log("ludus-cli: extracting binary...");
|
|
176
|
+
extract(buffer, archiveName, binDir);
|
|
177
|
+
|
|
178
|
+
console.log("ludus-cli: installed successfully");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
main().catch((err) => {
|
|
182
|
+
console.error(`ludus-cli: installation failed: ${err.message}`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ludus-cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "UE5 dedicated server deployment CLI with MCP server",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/jpvelasco/ludus"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"ludus": "run.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"postinstall": "node install.js"
|
|
15
|
+
},
|
|
16
|
+
"os": [
|
|
17
|
+
"linux",
|
|
18
|
+
"darwin",
|
|
19
|
+
"win32"
|
|
20
|
+
],
|
|
21
|
+
"cpu": [
|
|
22
|
+
"x64",
|
|
23
|
+
"arm64"
|
|
24
|
+
],
|
|
25
|
+
"files": [
|
|
26
|
+
"install.js",
|
|
27
|
+
"run.js",
|
|
28
|
+
"bin/"
|
|
29
|
+
],
|
|
30
|
+
"binaryChecksums": {
|
|
31
|
+
"ludus_0.1.1_darwin_amd64.tar.gz": "0d694cfc6dc7bc8a5932af64142726cb9277e56e06995d9506ea892b100b21f9",
|
|
32
|
+
"ludus_0.1.1_darwin_arm64.tar.gz": "3e5c420a0acfe2b5a0ada39af8ef4c1b191626460aa60fc997d2bcfeb40535d4",
|
|
33
|
+
"ludus_0.1.1_linux_amd64.tar.gz": "cd230ca2b080986021adadd14e086555e021bfeee5eca3b5cc78f5146f6114a8",
|
|
34
|
+
"ludus_0.1.1_linux_arm64.tar.gz": "bd96a2a04a9af979c2bd6815fcc10c4bc9460b9a8aa8d06d898ad6202df10483",
|
|
35
|
+
"ludus_0.1.1_windows_amd64.zip": "7abe8327e3df427423516c34fe6382addbbde5ec7669617a97abd999d68de6cb"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/run.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { spawn } = require("child_process");
|
|
6
|
+
|
|
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}`);
|
|
27
|
+
}
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
child.on("exit", (code, signal) => {
|
|
32
|
+
process.exit(signal ? 1 : code || 0);
|
|
33
|
+
});
|