ludus-cli 0.1.1 → 0.1.3

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 +65 -0
  2. package/install.js +184 -184
  3. package/package.json +23 -6
  4. package/run.js +33 -33
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # ludus-cli
2
+
3
+ CLI tool that automates the end-to-end pipeline for deploying Unreal Engine 5 dedicated servers to AWS GameLift.
4
+
5
+ Ludus handles the entire workflow that would otherwise require dozens of manual steps across multiple tools: UE5 source builds, game server compilation, Docker containerization, ECR push, and GameLift fleet deployment.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g ludus-cli
11
+ ```
12
+
13
+ Or run directly:
14
+
15
+ ```bash
16
+ npx ludus-cli --help
17
+ ```
18
+
19
+ ## What it does
20
+
21
+ ```
22
+ ludus run --verbose
23
+ ```
24
+
25
+ This single command orchestrates six stages:
26
+
27
+ 1. **Prerequisite validation** — OS, engine source, game content, Docker, AWS CLI, disk space, RAM
28
+ 2. **Engine build** — UE5 source compilation
29
+ 3. **Game server build** — Dedicated server packaging via RunUAT
30
+ 4. **Container build** — Dockerfile generation and Docker image build
31
+ 5. **ECR push** — Docker image push to Amazon ECR
32
+ 6. **GameLift deploy** — Container fleet creation with IAM roles and polling
33
+
34
+ ## Deployment targets
35
+
36
+ | Target | Command | Docker required? | ARM64 (Graviton) |
37
+ |--------|---------|:---:|:---:|
38
+ | GameLift Containers | `ludus deploy fleet` | Yes | Yes |
39
+ | CloudFormation Stack | `ludus deploy stack` | Yes | Yes |
40
+ | GameLift Managed EC2 | `ludus deploy ec2` | No | Yes |
41
+ | GameLift Anywhere | `ludus deploy anywhere` | No | No |
42
+ | Binary export | `ludus deploy binary` | No | Yes |
43
+
44
+ ## AI Agent Integration (MCP)
45
+
46
+ Ludus includes a built-in [Model Context Protocol](https://modelcontextprotocol.io/) server exposing 21 tools. Any MCP-compatible AI agent can orchestrate the full pipeline programmatically.
47
+
48
+ ```json
49
+ {
50
+ "mcpServers": {
51
+ "ludus": {
52
+ "command": "npx",
53
+ "args": ["-y", "ludus-cli", "mcp"]
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ ## Documentation
60
+
61
+ Full documentation, configuration reference, and prerequisites: [github.com/jpvelasco/ludus](https://github.com/jpvelasco/ludus)
62
+
63
+ ## License
64
+
65
+ [MIT](https://github.com/jpvelasco/ludus/blob/main/LICENSE)
package/install.js CHANGED
@@ -1,184 +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
- });
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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ludus-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "UE5 dedicated server deployment CLI with MCP server",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,16 +22,33 @@
22
22
  "x64",
23
23
  "arm64"
24
24
  ],
25
+ "keywords": [
26
+ "unreal-engine",
27
+ "ue5",
28
+ "gamelift",
29
+ "aws",
30
+ "dedicated-server",
31
+ "game-server",
32
+ "gamedev",
33
+ "deployment",
34
+ "docker",
35
+ "mcp",
36
+ "cli",
37
+ "devops",
38
+ "arm64",
39
+ "graviton",
40
+ "buildgraph"
41
+ ],
25
42
  "files": [
26
43
  "install.js",
27
44
  "run.js",
28
45
  "bin/"
29
46
  ],
30
47
  "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"
48
+ "ludus_0.1.3_darwin_amd64.tar.gz": "ed0d7715e05b1cdac42a0f71aca557c0ad3b99952abcddf4eccd1bab37222fd1",
49
+ "ludus_0.1.3_darwin_arm64.tar.gz": "bbb49935b47d115f3e3e24bcd65aabe1be0cea2bdf039aec747849d709c21146",
50
+ "ludus_0.1.3_linux_amd64.tar.gz": "9a3093291d5b87c406af37a51750ac5b60e49758cf310ce03d1f6acba6dfb64d",
51
+ "ludus_0.1.3_linux_arm64.tar.gz": "d33be20dc2e6accb94a548df13981510265699dffc1dc278867e79f258b7bc9b",
52
+ "ludus_0.1.3_windows_amd64.zip": "b308f043e16c6abba1f7e995cdd4955a7e4d2105301ce413b7f82719d1e85c36"
36
53
  }
37
54
  }
package/run.js CHANGED
@@ -1,33 +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
- });
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
+ });