@mavonengine/cli 0.0.1 → 0.0.2
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 +204 -8
- package/dist/chunk-OSCNM66O.js +91 -0
- package/dist/chunk-TNN4S5K4.js +44 -0
- package/dist/docs-4C7X6FUQ.js +13 -0
- package/dist/index.js +902 -231
- package/dist/server-35RY6IGC.js +731 -0
- package/package.json +7 -5
package/dist/index.js
CHANGED
|
@@ -1,173 +1,208 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_DOCS_URL,
|
|
4
|
+
MCP_CONFIG_FILE,
|
|
5
|
+
version,
|
|
6
|
+
writeMcpConfig
|
|
7
|
+
} from "./chunk-TNN4S5K4.js";
|
|
2
8
|
|
|
3
9
|
// src/index.ts
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
// package.json
|
|
7
|
-
var version = "0.0.1";
|
|
10
|
+
import process9 from "process";
|
|
8
11
|
|
|
9
|
-
// src/commands/
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
12
|
+
// src/commands/build.ts
|
|
13
|
+
import { spawn as spawn3 } from "child_process";
|
|
14
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
15
|
+
import { join as join3, resolve as resolve3 } from "path";
|
|
16
|
+
import process4 from "process";
|
|
13
17
|
import { parseArgs as parseArgs3 } from "util";
|
|
14
18
|
|
|
15
|
-
//
|
|
16
|
-
import {
|
|
17
|
-
import { createWriteStream, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
18
|
-
import { tmpdir } from "os";
|
|
19
|
+
// ../core/dist/Plugins/ProjectConfig.js
|
|
20
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
19
21
|
import { dirname, isAbsolute, join, relative, resolve } from "path";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
var CONFIG_FILE = "mavonengine.json";
|
|
23
|
+
var TARGETS = ["client", "server", "both"];
|
|
24
|
+
var PluginConfigError = class extends Error {
|
|
25
|
+
};
|
|
26
|
+
function configPath(root) {
|
|
27
|
+
return join(root, CONFIG_FILE);
|
|
28
|
+
}
|
|
29
|
+
function readConfig(root) {
|
|
30
|
+
const path = configPath(root);
|
|
31
|
+
if (!existsSync(path))
|
|
32
|
+
return { plugins: {} };
|
|
33
|
+
let config;
|
|
34
|
+
try {
|
|
35
|
+
config = JSON.parse(readFileSync(path, "utf8"));
|
|
36
|
+
} catch (err) {
|
|
37
|
+
throw new PluginConfigError(`${path} is not valid JSON: ${err.message}`);
|
|
38
|
+
}
|
|
39
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
40
|
+
throw new PluginConfigError(`${path} must contain a JSON object`);
|
|
41
|
+
const result = config;
|
|
42
|
+
if (result.plugins === void 0)
|
|
43
|
+
result.plugins = {};
|
|
44
|
+
else if (!result.plugins || typeof result.plugins !== "object" || Array.isArray(result.plugins))
|
|
45
|
+
throw new PluginConfigError(`"plugins" in ${path} must be an object`);
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
function writeConfig(root, config) {
|
|
49
|
+
writeFileSync(configPath(root), `${JSON.stringify(config, null, 2)}
|
|
50
|
+
`);
|
|
33
51
|
}
|
|
34
|
-
function
|
|
35
|
-
|
|
52
|
+
function updateConfig(root, change) {
|
|
53
|
+
const config = readConfig(root);
|
|
54
|
+
if (!change(config))
|
|
55
|
+
return false;
|
|
56
|
+
writeConfig(root, config);
|
|
57
|
+
return true;
|
|
36
58
|
}
|
|
37
|
-
function
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (h > 0)
|
|
46
|
-
return `${h}h ${m}m ${s}s`;
|
|
47
|
-
if (m > 0)
|
|
48
|
-
return `${m}m ${s}s`;
|
|
49
|
-
return `${s}s`;
|
|
59
|
+
function addPluginToConfig(root, pkg) {
|
|
60
|
+
return updateConfig(root, (config) => {
|
|
61
|
+
const existing = config.plugins[pkg];
|
|
62
|
+
if (existing && existing.enabled !== false)
|
|
63
|
+
return false;
|
|
64
|
+
config.plugins[pkg] = existing ? { ...existing, enabled: true } : { enabled: true };
|
|
65
|
+
return true;
|
|
66
|
+
});
|
|
50
67
|
}
|
|
51
|
-
function
|
|
52
|
-
|
|
68
|
+
function findPackageDir(root, pkg) {
|
|
69
|
+
for (let dir = resolve(root); ; dir = dirname(dir)) {
|
|
70
|
+
const candidate = join(dir, "node_modules", pkg);
|
|
71
|
+
if (existsSync(join(candidate, "package.json")))
|
|
72
|
+
return candidate;
|
|
73
|
+
if (dirname(dir) === dir)
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
53
76
|
}
|
|
54
|
-
function
|
|
55
|
-
|
|
77
|
+
function validateManifest(pkg, value) {
|
|
78
|
+
if (!value || typeof value !== "object")
|
|
79
|
+
throw new PluginConfigError(`${pkg}: "mavonengine" in package.json must be an object`);
|
|
80
|
+
const manifest = value;
|
|
81
|
+
if (!TARGETS.includes(manifest.target))
|
|
82
|
+
throw new PluginConfigError(`${pkg}: "mavonengine.target" must be one of ${TARGETS.join(", ")}`);
|
|
83
|
+
for (const side of ["client", "server"]) {
|
|
84
|
+
if (runsOn(manifest.target, side) && typeof manifest[side] !== "string")
|
|
85
|
+
throw new PluginConfigError(`${pkg}: "mavonengine.${side}" is required for target "${manifest.target}"`);
|
|
86
|
+
}
|
|
87
|
+
return manifest;
|
|
56
88
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
89
|
+
function readPluginManifest(root, pkg) {
|
|
90
|
+
const dir = findPackageDir(root, pkg);
|
|
91
|
+
if (!dir)
|
|
92
|
+
return void 0;
|
|
93
|
+
const packageJson = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
94
|
+
if (packageJson.mavonengine === void 0)
|
|
95
|
+
return void 0;
|
|
96
|
+
return { package: pkg, version: packageJson.version, dir, manifest: validateManifest(pkg, packageJson.mavonengine) };
|
|
64
97
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (!response.ok || !response.body)
|
|
68
|
-
throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
|
|
69
|
-
const total = Number(response.headers.get("content-length")) || 0;
|
|
70
|
-
let received = 0;
|
|
71
|
-
const progress = new Transform({
|
|
72
|
-
transform(chunk, _encoding, callback) {
|
|
73
|
-
received += chunk.length;
|
|
74
|
-
report.step(`Downloading the template\u2026 ${formatBytes(received)}${total ? ` of ${formatBytes(total)}` : ""}`);
|
|
75
|
-
callback(null, chunk);
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
const dest = join(tmpdir(), `mavonengine-create-${process.pid}-${Date.now()}.zip`);
|
|
79
|
-
try {
|
|
80
|
-
await pipeline(Readable.fromWeb(response.body), progress, createWriteStream(dest), { signal });
|
|
81
|
-
} catch (err) {
|
|
82
|
-
rmSync(dest, { force: true });
|
|
83
|
-
throw err;
|
|
84
|
-
}
|
|
85
|
-
return dest;
|
|
98
|
+
function runsOn(target, side) {
|
|
99
|
+
return target === "both" || target === side;
|
|
86
100
|
}
|
|
87
|
-
function
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
for (const entry of
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
function resolveInside(base, path) {
|
|
102
|
+
const full = resolve(base, path);
|
|
103
|
+
const inside = relative(base, full);
|
|
104
|
+
return inside && !inside.startsWith("..") && !isAbsolute(inside) ? full : void 0;
|
|
105
|
+
}
|
|
106
|
+
function assetsDir(plugin) {
|
|
107
|
+
return resolve(plugin.dir, plugin.manifest.assets?.dir ?? "assets");
|
|
108
|
+
}
|
|
109
|
+
function enabledPlugins(root, side) {
|
|
110
|
+
const plugins = [];
|
|
111
|
+
const warnings = [];
|
|
112
|
+
for (const [pkg, entry] of Object.entries(readConfig(root).plugins)) {
|
|
113
|
+
if (entry?.enabled === false)
|
|
114
|
+
continue;
|
|
115
|
+
let installed;
|
|
116
|
+
try {
|
|
117
|
+
installed = readPluginManifest(root, pkg);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
warnings.push(err.message);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!installed) {
|
|
123
|
+
warnings.push(`${pkg} is listed in ${CONFIG_FILE} but is not installed or is not a MavonEngine plugin`);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (!runsOn(installed.manifest.target, side))
|
|
127
|
+
continue;
|
|
128
|
+
const entryPath = resolveInside(installed.dir, installed.manifest[side]);
|
|
129
|
+
if (!entryPath || !existsSync(entryPath)) {
|
|
130
|
+
warnings.push(`${pkg}: ${side} entry ${installed.manifest[side]} not found, is the plugin built?`);
|
|
101
131
|
continue;
|
|
102
|
-
const outputPath = resolve(root, relativePath);
|
|
103
|
-
const inside = relative(root, outputPath);
|
|
104
|
-
if (inside.startsWith("..") || isAbsolute(inside))
|
|
105
|
-
throw new Error(`Refusing to extract ${entry.entryName} outside the project`);
|
|
106
|
-
if (entry.isDirectory) {
|
|
107
|
-
mkdirSync(outputPath, { recursive: true });
|
|
108
|
-
} else {
|
|
109
|
-
mkdirSync(dirname(outputPath), { recursive: true });
|
|
110
|
-
writeFileSync(outputPath, entry.getData());
|
|
111
|
-
files++;
|
|
112
132
|
}
|
|
133
|
+
plugins.push({
|
|
134
|
+
...installed,
|
|
135
|
+
options: entry?.options ?? {},
|
|
136
|
+
prefabs: Array.isArray(entry?.prefabs) ? entry.prefabs : [],
|
|
137
|
+
entry: entryPath
|
|
138
|
+
});
|
|
113
139
|
}
|
|
114
|
-
return
|
|
140
|
+
return { plugins, warnings };
|
|
141
|
+
}
|
|
142
|
+
function shippedAssetPaths(manifest, usedPrefabs) {
|
|
143
|
+
const assets = manifest.assets;
|
|
144
|
+
const paths = [...assets?.shared ?? []];
|
|
145
|
+
for (const prefab of usedPrefabs)
|
|
146
|
+
paths.push(...assets?.prefabs?.[prefab] ?? []);
|
|
147
|
+
return [...new Set(paths)];
|
|
148
|
+
}
|
|
149
|
+
function releaseAssets(root) {
|
|
150
|
+
const { plugins, warnings } = enabledPlugins(root, "client");
|
|
151
|
+
const assets = [];
|
|
152
|
+
for (const plugin of plugins) {
|
|
153
|
+
const base = assetsDir(plugin);
|
|
154
|
+
for (const path of shippedAssetPaths(plugin.manifest, plugin.prefabs)) {
|
|
155
|
+
const from = resolveInside(base, path);
|
|
156
|
+
if (!from) {
|
|
157
|
+
warnings.push(`${plugin.package}: asset ${path} points outside its assets directory`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (!existsSync(from)) {
|
|
161
|
+
warnings.push(`${plugin.package}: asset ${path} not found`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
assets.push({ package: plugin.package, from, to: `plugins/${plugin.package}/${path}`, path });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return { assets, warnings };
|
|
115
168
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (code === 0)
|
|
132
|
-
resolvePromise();
|
|
133
|
-
else
|
|
134
|
-
reject(new Error(`'${command} ${args.join(" ")}' ${code === null ? `was stopped by ${exitSignal}` : `exited with code ${code}`}`));
|
|
135
|
-
});
|
|
136
|
-
});
|
|
169
|
+
|
|
170
|
+
// src/commands/dashboard.ts
|
|
171
|
+
import process2 from "process";
|
|
172
|
+
import { parseArgs } from "util";
|
|
173
|
+
|
|
174
|
+
// src/browser.ts
|
|
175
|
+
import { spawn } from "child_process";
|
|
176
|
+
import process from "process";
|
|
177
|
+
var AMPERSAND_RE = /&/g;
|
|
178
|
+
function openCommand(url, platform = process.platform) {
|
|
179
|
+
if (platform === "darwin")
|
|
180
|
+
return ["open", [url]];
|
|
181
|
+
if (platform === "win32")
|
|
182
|
+
return ["cmd", ["/c", "start", '""', url.replace(AMPERSAND_RE, "^&")]];
|
|
183
|
+
return ["xdg-open", [url]];
|
|
137
184
|
}
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
if (existsSync(projectDir))
|
|
141
|
-
throw new Error(`Directory "${projectDir}" already exists`);
|
|
142
|
-
report.output(`Creating project "${projectName}" in ${projectDir}
|
|
143
|
-
`);
|
|
144
|
-
mkdirSync(projectDir, { recursive: true });
|
|
145
|
-
report.step("Downloading the template\u2026");
|
|
146
|
-
report.output(`Downloading the template from ${templateUrl}
|
|
147
|
-
`);
|
|
148
|
-
const zipPath = await downloadZip(templateUrl, { report, signal, fetch });
|
|
185
|
+
function openBrowser(url) {
|
|
186
|
+
const [command, args] = openCommand(url);
|
|
149
187
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
rmSync(zipPath, { force: true });
|
|
156
|
-
}
|
|
157
|
-
const pkgJsonPath = join(projectDir, "package.json");
|
|
158
|
-
if (existsSync(pkgJsonPath)) {
|
|
159
|
-
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
160
|
-
pkg.name = projectName;
|
|
161
|
-
writeFileSync(pkgJsonPath, `${JSON.stringify(pkg, null, 2)}
|
|
162
|
-
`);
|
|
188
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true, windowsHide: true });
|
|
189
|
+
child.on("error", () => {
|
|
190
|
+
});
|
|
191
|
+
child.unref();
|
|
192
|
+
} catch {
|
|
163
193
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
194
|
+
}
|
|
195
|
+
function canOpenBrowserHere(env = process.env) {
|
|
196
|
+
return !env.SSH_CONNECTION && !env.SSH_TTY;
|
|
197
|
+
}
|
|
198
|
+
async function servesPage(url, { timeoutMs = 3e3, fetch = globalThis.fetch } = {}) {
|
|
199
|
+
try {
|
|
200
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
201
|
+
await res.body?.cancel();
|
|
202
|
+
return res.ok && (res.headers.get("content-type") ?? "").includes("text/html");
|
|
203
|
+
} catch {
|
|
204
|
+
return false;
|
|
169
205
|
}
|
|
170
|
-
return projectDir;
|
|
171
206
|
}
|
|
172
207
|
|
|
173
208
|
// src/dashboard/snapshot.ts
|
|
@@ -483,44 +518,6 @@ var MetricsClient = class {
|
|
|
483
518
|
}
|
|
484
519
|
};
|
|
485
520
|
|
|
486
|
-
// src/commands/dashboard.ts
|
|
487
|
-
import process3 from "process";
|
|
488
|
-
import { parseArgs } from "util";
|
|
489
|
-
|
|
490
|
-
// src/browser.ts
|
|
491
|
-
import { spawn as spawn2 } from "child_process";
|
|
492
|
-
import process2 from "process";
|
|
493
|
-
var AMPERSAND_RE = /&/g;
|
|
494
|
-
function openCommand(url, platform = process2.platform) {
|
|
495
|
-
if (platform === "darwin")
|
|
496
|
-
return ["open", [url]];
|
|
497
|
-
if (platform === "win32")
|
|
498
|
-
return ["cmd", ["/c", "start", '""', url.replace(AMPERSAND_RE, "^&")]];
|
|
499
|
-
return ["xdg-open", [url]];
|
|
500
|
-
}
|
|
501
|
-
function openBrowser(url) {
|
|
502
|
-
const [command, args] = openCommand(url);
|
|
503
|
-
try {
|
|
504
|
-
const child = spawn2(command, args, { stdio: "ignore", detached: true, windowsHide: true });
|
|
505
|
-
child.on("error", () => {
|
|
506
|
-
});
|
|
507
|
-
child.unref();
|
|
508
|
-
} catch {
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
function canOpenBrowserHere(env = process2.env) {
|
|
512
|
-
return !env.SSH_CONNECTION && !env.SSH_TTY;
|
|
513
|
-
}
|
|
514
|
-
async function servesPage(url, { timeoutMs = 3e3, fetch = globalThis.fetch } = {}) {
|
|
515
|
-
try {
|
|
516
|
-
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
517
|
-
await res.body?.cancel();
|
|
518
|
-
return res.ok && (res.headers.get("content-type") ?? "").includes("text/html");
|
|
519
|
-
} catch {
|
|
520
|
-
return false;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
521
|
// src/dashboard/simulator.ts
|
|
525
522
|
var HISTORY_LENGTH = 60;
|
|
526
523
|
function mulberry32(seed) {
|
|
@@ -715,6 +712,39 @@ function bootBanner({ version: version2, threeRevision, rapierVersion, mode }) {
|
|
|
715
712
|
// src/dashboard/view.ts
|
|
716
713
|
import { DEFAULT_COLOR, hex } from "@profullstack/hqtui";
|
|
717
714
|
|
|
715
|
+
// src/dashboard/format.ts
|
|
716
|
+
var BYTE_UNITS = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
717
|
+
function formatBytes(bytes, decimals = 1) {
|
|
718
|
+
if (!Number.isFinite(bytes) || bytes <= 0)
|
|
719
|
+
return "0 B";
|
|
720
|
+
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), BYTE_UNITS.length - 1);
|
|
721
|
+
const value = bytes / 1024 ** exponent;
|
|
722
|
+
return `${exponent === 0 ? value : value.toFixed(decimals)} ${BYTE_UNITS[exponent]}`;
|
|
723
|
+
}
|
|
724
|
+
function formatRate(bytesPerSecond) {
|
|
725
|
+
return `${formatBytes(bytesPerSecond)}/s`;
|
|
726
|
+
}
|
|
727
|
+
function formatDuration(totalSeconds) {
|
|
728
|
+
const seconds = Math.max(0, Math.floor(totalSeconds));
|
|
729
|
+
const d = Math.floor(seconds / 86400);
|
|
730
|
+
const h = Math.floor(seconds % 86400 / 3600);
|
|
731
|
+
const m = Math.floor(seconds % 3600 / 60);
|
|
732
|
+
const s = seconds % 60;
|
|
733
|
+
if (d > 0)
|
|
734
|
+
return `${d}d ${h}h ${m}m`;
|
|
735
|
+
if (h > 0)
|
|
736
|
+
return `${h}h ${m}m ${s}s`;
|
|
737
|
+
if (m > 0)
|
|
738
|
+
return `${m}m ${s}s`;
|
|
739
|
+
return `${s}s`;
|
|
740
|
+
}
|
|
741
|
+
function formatMs(ms) {
|
|
742
|
+
return `${ms.toFixed(ms < 10 ? 2 : 1)} ms`;
|
|
743
|
+
}
|
|
744
|
+
function formatPercent(value) {
|
|
745
|
+
return `${value.toFixed(1)}%`;
|
|
746
|
+
}
|
|
747
|
+
|
|
718
748
|
// src/dashboard/logo.ts
|
|
719
749
|
var LOGO_SIZES = [
|
|
720
750
|
[
|
|
@@ -1290,7 +1320,7 @@ Options:
|
|
|
1290
1320
|
var STARTUP_POLL_MS = 250;
|
|
1291
1321
|
var UsageError = class extends Error {
|
|
1292
1322
|
};
|
|
1293
|
-
function parseDashboardArgs(args, env =
|
|
1323
|
+
function parseDashboardArgs(args, env = process2.env) {
|
|
1294
1324
|
const { values } = parseArgs({
|
|
1295
1325
|
args,
|
|
1296
1326
|
options: {
|
|
@@ -1378,19 +1408,22 @@ async function runDashboard(options, signal) {
|
|
|
1378
1408
|
}
|
|
1379
1409
|
|
|
1380
1410
|
// src/commands/dev.ts
|
|
1381
|
-
import { execFileSync, spawn as
|
|
1411
|
+
import { execFileSync, spawn as spawn2 } from "child_process";
|
|
1382
1412
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
1383
1413
|
import { constants } from "os";
|
|
1384
1414
|
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
1385
|
-
import
|
|
1415
|
+
import process3 from "process";
|
|
1386
1416
|
import { setTimeout as delay } from "timers/promises";
|
|
1387
1417
|
import { parseArgs as parseArgs2 } from "util";
|
|
1388
1418
|
var DEV_HELP = `Usage: mavonengine dev [options]
|
|
1389
1419
|
|
|
1390
|
-
Run the dev
|
|
1420
|
+
Run the dev servers of the MavonEngine project you are in (game server and client)
|
|
1391
1421
|
and show the dashboard for it. Once the game server is up, the game opens in the
|
|
1392
1422
|
browser. Quitting the dashboard stops the dev servers.
|
|
1393
1423
|
|
|
1424
|
+
Runs the project's 'dev:server' script, or its 'dev' script when there is none.
|
|
1425
|
+
So 'dev' can be 'mavonengine dev' itself, as in the multiplayer template.
|
|
1426
|
+
|
|
1394
1427
|
Options:
|
|
1395
1428
|
--cwd <dir> Start looking for the project here (default: current directory)
|
|
1396
1429
|
--no-dashboard Only run the dev script, with its output in the terminal
|
|
@@ -1411,6 +1444,9 @@ var LOCKFILES = [
|
|
|
1411
1444
|
["package-lock.json", "npm"]
|
|
1412
1445
|
];
|
|
1413
1446
|
var WHITESPACE_RE = /\s+/;
|
|
1447
|
+
var RUNS_MAVONENGINE_DEV_RE = /\bmavonengine\s+dev\b/;
|
|
1448
|
+
var DEV_ENV_FLAG = "MAVONENGINE_DEV_SCRIPT";
|
|
1449
|
+
var DEV_SERVER_SCRIPT = "dev:server";
|
|
1414
1450
|
var DEV_FLAGS = {
|
|
1415
1451
|
cwd: { type: "string" },
|
|
1416
1452
|
dashboard: { type: "boolean", default: true },
|
|
@@ -1420,7 +1456,7 @@ var DEV_FLAGS = {
|
|
|
1420
1456
|
open: { type: "boolean", default: true },
|
|
1421
1457
|
help: { type: "boolean", short: "h", default: false }
|
|
1422
1458
|
};
|
|
1423
|
-
function parseDevArgs(args, cwd =
|
|
1459
|
+
function parseDevArgs(args, cwd = process3.cwd(), env = process3.env) {
|
|
1424
1460
|
const { values } = parseArgs2({
|
|
1425
1461
|
args,
|
|
1426
1462
|
options: DEV_FLAGS,
|
|
@@ -1455,11 +1491,28 @@ function readPackageJson(dir) {
|
|
|
1455
1491
|
return void 0;
|
|
1456
1492
|
}
|
|
1457
1493
|
}
|
|
1494
|
+
function devScriptOf(pkg) {
|
|
1495
|
+
const scripts = pkg?.scripts ?? {};
|
|
1496
|
+
if (typeof scripts[DEV_SERVER_SCRIPT] === "string")
|
|
1497
|
+
return DEV_SERVER_SCRIPT;
|
|
1498
|
+
return typeof scripts.dev === "string" ? "dev" : void 0;
|
|
1499
|
+
}
|
|
1500
|
+
function checkDevScript(root, env = process3.env) {
|
|
1501
|
+
const pkg = readPackageJson(root);
|
|
1502
|
+
const script = devScriptOf(pkg);
|
|
1503
|
+
if (!script)
|
|
1504
|
+
throw new UsageError(`${join2(root, "package.json")} has no '${DEV_SERVER_SCRIPT}' or 'dev' script`);
|
|
1505
|
+
const command = String(pkg?.scripts?.[script] ?? "");
|
|
1506
|
+
if (env[DEV_ENV_FLAG] || RUNS_MAVONENGINE_DEV_RE.test(command)) {
|
|
1507
|
+
throw new UsageError(`the '${script}' script starts 'mavonengine dev' again. Put the command that starts the game server in a '${DEV_SERVER_SCRIPT}' script: 'mavonengine dev' runs that instead of 'dev'`);
|
|
1508
|
+
}
|
|
1509
|
+
return script;
|
|
1510
|
+
}
|
|
1458
1511
|
function findProjectRoot(start) {
|
|
1459
1512
|
for (let dir = resolve2(start); ; dir = dirname2(dir)) {
|
|
1460
1513
|
const pkg = readPackageJson(dir);
|
|
1461
1514
|
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
1462
|
-
if ("@mavonengine/core" in deps &&
|
|
1515
|
+
if ("@mavonengine/core" in deps && devScriptOf(pkg))
|
|
1463
1516
|
return dir;
|
|
1464
1517
|
if (dirname2(dir) === dir)
|
|
1465
1518
|
return void 0;
|
|
@@ -1490,7 +1543,7 @@ function descendants(root, psOutput) {
|
|
|
1490
1543
|
return found;
|
|
1491
1544
|
}
|
|
1492
1545
|
function processTree(pid) {
|
|
1493
|
-
if (
|
|
1546
|
+
if (process3.platform === "win32")
|
|
1494
1547
|
return [];
|
|
1495
1548
|
try {
|
|
1496
1549
|
return [pid, ...descendants(pid, execFileSync("ps", ["-A", "-o", "pid=,ppid="], { encoding: "utf8" }))];
|
|
@@ -1501,14 +1554,14 @@ function processTree(pid) {
|
|
|
1501
1554
|
function signalAll(pids, signal) {
|
|
1502
1555
|
for (const p of pids) {
|
|
1503
1556
|
try {
|
|
1504
|
-
|
|
1557
|
+
process3.kill(p, signal);
|
|
1505
1558
|
} catch {
|
|
1506
1559
|
}
|
|
1507
1560
|
}
|
|
1508
1561
|
}
|
|
1509
1562
|
function isAlive(pid) {
|
|
1510
1563
|
try {
|
|
1511
|
-
|
|
1564
|
+
process3.kill(pid, 0);
|
|
1512
1565
|
return true;
|
|
1513
1566
|
} catch {
|
|
1514
1567
|
return false;
|
|
@@ -1520,7 +1573,7 @@ async function waitForExit(pids, timeoutMs) {
|
|
|
1520
1573
|
await delay(50);
|
|
1521
1574
|
}
|
|
1522
1575
|
function killTree(pid, signal) {
|
|
1523
|
-
if (
|
|
1576
|
+
if (process3.platform === "win32") {
|
|
1524
1577
|
try {
|
|
1525
1578
|
execFileSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
|
|
1526
1579
|
} catch {
|
|
@@ -1531,22 +1584,24 @@ function killTree(pid, signal) {
|
|
|
1531
1584
|
}
|
|
1532
1585
|
function startDev(root, { capture = false } = {}) {
|
|
1533
1586
|
const pm = detectPackageManager(root);
|
|
1534
|
-
const
|
|
1587
|
+
const script = checkDevScript(root);
|
|
1588
|
+
const child = spawn2(pm, ["run", script], {
|
|
1535
1589
|
cwd: root,
|
|
1590
|
+
env: { ...process3.env, [DEV_ENV_FLAG]: "1" },
|
|
1536
1591
|
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
1537
1592
|
// npm, pnpm and yarn are .cmd shims on Windows
|
|
1538
|
-
shell:
|
|
1593
|
+
shell: process3.platform === "win32"
|
|
1539
1594
|
});
|
|
1540
1595
|
let output = "";
|
|
1541
1596
|
const keep = (chunk) => {
|
|
1542
1597
|
output = (output + chunk).slice(-MAX_CAPTURED_OUTPUT);
|
|
1543
1598
|
};
|
|
1544
|
-
const announce = `mavonengine: running '${pm} run
|
|
1599
|
+
const announce = `mavonengine: running '${pm} run ${script}' in ${root}
|
|
1545
1600
|
`;
|
|
1546
1601
|
if (capture)
|
|
1547
1602
|
keep(announce);
|
|
1548
1603
|
else
|
|
1549
|
-
|
|
1604
|
+
process3.stdout.write(announce);
|
|
1550
1605
|
child.stdout?.setEncoding("utf8").on("data", keep);
|
|
1551
1606
|
child.stderr?.setEncoding("utf8").on("data", keep);
|
|
1552
1607
|
let running = true;
|
|
@@ -1554,17 +1609,17 @@ function startDev(root, { capture = false } = {}) {
|
|
|
1554
1609
|
};
|
|
1555
1610
|
const forward = (signal) => child.pid && killTree(child.pid, signal);
|
|
1556
1611
|
const onExit = () => running && child.pid && killTree(child.pid, "SIGTERM");
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1612
|
+
process3.on("SIGINT", onInterrupt);
|
|
1613
|
+
process3.on("SIGTERM", forward);
|
|
1614
|
+
process3.on("SIGHUP", forward);
|
|
1615
|
+
process3.on("exit", onExit);
|
|
1561
1616
|
const exited = new Promise((resolvePromise, reject) => {
|
|
1562
1617
|
const cleanup = () => {
|
|
1563
1618
|
running = false;
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1619
|
+
process3.off("SIGINT", onInterrupt);
|
|
1620
|
+
process3.off("SIGTERM", forward);
|
|
1621
|
+
process3.off("SIGHUP", forward);
|
|
1622
|
+
process3.off("exit", onExit);
|
|
1568
1623
|
};
|
|
1569
1624
|
child.on("error", (err) => {
|
|
1570
1625
|
cleanup();
|
|
@@ -1577,7 +1632,7 @@ function startDev(root, { capture = false } = {}) {
|
|
|
1577
1632
|
});
|
|
1578
1633
|
const stop = async () => {
|
|
1579
1634
|
if (running && child.pid) {
|
|
1580
|
-
if (
|
|
1635
|
+
if (process3.platform === "win32") {
|
|
1581
1636
|
killTree(child.pid, "SIGTERM");
|
|
1582
1637
|
} else {
|
|
1583
1638
|
const tree = processTree(child.pid);
|
|
@@ -1612,7 +1667,7 @@ async function runDevWithDashboard(root, dashboard) {
|
|
|
1612
1667
|
if (quit)
|
|
1613
1668
|
return 0;
|
|
1614
1669
|
const exitCode = await dev.exited;
|
|
1615
|
-
|
|
1670
|
+
process3.stderr.write(`${dev.output()}
|
|
1616
1671
|
mavonengine: the dev script exited with code ${exitCode}
|
|
1617
1672
|
`);
|
|
1618
1673
|
return exitCode;
|
|
@@ -1621,11 +1676,218 @@ async function runDevCommand(options) {
|
|
|
1621
1676
|
const root = findProjectRoot(options.cwd);
|
|
1622
1677
|
if (!root)
|
|
1623
1678
|
throw new UsageError(`no MavonEngine project found in ${options.cwd} or its parents (looking for a package.json with @mavonengine/core and a dev script)`);
|
|
1624
|
-
|
|
1679
|
+
checkDevScript(root);
|
|
1680
|
+
if (options.dashboard && process3.stdout.isTTY && process3.stdin.isTTY)
|
|
1625
1681
|
return runDevWithDashboard(root, options.dashboard);
|
|
1626
1682
|
return runDev(root);
|
|
1627
1683
|
}
|
|
1628
1684
|
|
|
1685
|
+
// src/commands/build.ts
|
|
1686
|
+
var BUILD_HELP = `Usage: mavonengine build [options]
|
|
1687
|
+
|
|
1688
|
+
Build the MavonEngine project you are in for release by running its build script.
|
|
1689
|
+
Plugin assets go into the client's output under plugins/<package>/: the shared
|
|
1690
|
+
ones, and those of the prefabs listed in ${CONFIG_FILE}. The others are left out.
|
|
1691
|
+
|
|
1692
|
+
Options:
|
|
1693
|
+
--cwd <dir> Start looking for the project here (default: current directory)
|
|
1694
|
+
-h, --help Show this help
|
|
1695
|
+
`;
|
|
1696
|
+
function parseBuildArgs(args, cwd = process4.cwd()) {
|
|
1697
|
+
const { values } = parseArgs3({
|
|
1698
|
+
args,
|
|
1699
|
+
options: {
|
|
1700
|
+
cwd: { type: "string" },
|
|
1701
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1702
|
+
},
|
|
1703
|
+
strict: true,
|
|
1704
|
+
allowPositionals: false
|
|
1705
|
+
});
|
|
1706
|
+
if (values.help)
|
|
1707
|
+
return "help";
|
|
1708
|
+
return { cwd: resolve3(cwd, values.cwd ?? ".") };
|
|
1709
|
+
}
|
|
1710
|
+
function summarizeReleaseAssets(root, assets, warnings) {
|
|
1711
|
+
const plugins = readConfig(root).plugins;
|
|
1712
|
+
const enabled = Object.entries(plugins).filter(([, entry]) => entry?.enabled !== false);
|
|
1713
|
+
if (enabled.length === 0)
|
|
1714
|
+
return "";
|
|
1715
|
+
const lines = [`
|
|
1716
|
+
Plugins in ${CONFIG_FILE}:`];
|
|
1717
|
+
for (const [pkg, entry] of enabled) {
|
|
1718
|
+
const count = assets.filter((a) => a.package === pkg).length;
|
|
1719
|
+
const prefabs = entry?.prefabs?.length ? `, prefabs: ${entry.prefabs.join(", ")}` : "";
|
|
1720
|
+
lines.push(` ${pkg}: ${count} asset${count === 1 ? "" : "s"}${prefabs}`);
|
|
1721
|
+
}
|
|
1722
|
+
for (const warning of warnings)
|
|
1723
|
+
lines.push(` warning: ${warning}`);
|
|
1724
|
+
return `${lines.join("\n")}
|
|
1725
|
+
`;
|
|
1726
|
+
}
|
|
1727
|
+
function run(command, args, cwd) {
|
|
1728
|
+
return new Promise((resolvePromise, reject) => {
|
|
1729
|
+
const child = spawn3(command, args, { cwd, stdio: "inherit", shell: process4.platform === "win32" });
|
|
1730
|
+
child.on("error", (err) => {
|
|
1731
|
+
reject(err.code === "ENOENT" ? new Error(`'${command}' was not found on your PATH`) : err);
|
|
1732
|
+
});
|
|
1733
|
+
child.on("exit", (code) => resolvePromise(code ?? 1));
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
async function runBuildCommand(options) {
|
|
1737
|
+
const root = findProjectRoot(options.cwd);
|
|
1738
|
+
if (!root)
|
|
1739
|
+
throw new UsageError(`no MavonEngine project found in ${options.cwd} or its parents (looking for a package.json with @mavonengine/core and a dev script)`);
|
|
1740
|
+
const pkg = JSON.parse(readFileSync3(join3(root, "package.json"), "utf8"));
|
|
1741
|
+
if (typeof pkg.scripts?.build !== "string")
|
|
1742
|
+
throw new UsageError(`${join3(root, "package.json")} has no build script`);
|
|
1743
|
+
const pm = detectPackageManager(root);
|
|
1744
|
+
process4.stdout.write(`mavonengine: running '${pm} run build' in ${root}
|
|
1745
|
+
`);
|
|
1746
|
+
const code = await run(pm, ["run", "build"], root);
|
|
1747
|
+
if (code !== 0) {
|
|
1748
|
+
process4.stderr.write(`mavonengine: the build script exited with code ${code}
|
|
1749
|
+
`);
|
|
1750
|
+
return code;
|
|
1751
|
+
}
|
|
1752
|
+
if (existsSync3(join3(root, CONFIG_FILE))) {
|
|
1753
|
+
const { assets, warnings } = releaseAssets(root);
|
|
1754
|
+
process4.stdout.write(summarizeReleaseAssets(root, assets, warnings));
|
|
1755
|
+
}
|
|
1756
|
+
return 0;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// src/commands/create.ts
|
|
1760
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1761
|
+
import process6 from "process";
|
|
1762
|
+
import { createInterface } from "readline/promises";
|
|
1763
|
+
import { parseArgs as parseArgs4 } from "util";
|
|
1764
|
+
|
|
1765
|
+
// src/create/project.ts
|
|
1766
|
+
import { spawn as spawn4 } from "child_process";
|
|
1767
|
+
import { createWriteStream, existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
1768
|
+
import { tmpdir } from "os";
|
|
1769
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolve4 } from "path";
|
|
1770
|
+
import process5 from "process";
|
|
1771
|
+
import { Readable, Transform } from "stream";
|
|
1772
|
+
import { pipeline } from "stream/promises";
|
|
1773
|
+
import AdmZip from "adm-zip";
|
|
1774
|
+
var TEMPLATE_URL = "https://github.com/MavonEngine/Core/archive/refs/tags/0.0.16-alpha.zip";
|
|
1775
|
+
var TEMPLATE_SUBPATH = "packages/multiplayer-template";
|
|
1776
|
+
var PROJECT_NAME_RE = /^[\w@./-][\w@./\- ]*$/;
|
|
1777
|
+
function projectDirFor(projectName, parentDir) {
|
|
1778
|
+
return resolve4(parentDir, projectName);
|
|
1779
|
+
}
|
|
1780
|
+
async function downloadZip(url, { report, signal, fetch = globalThis.fetch }) {
|
|
1781
|
+
const response = await fetch(url, { signal });
|
|
1782
|
+
if (!response.ok || !response.body)
|
|
1783
|
+
throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
|
|
1784
|
+
const total = Number(response.headers.get("content-length")) || 0;
|
|
1785
|
+
let received = 0;
|
|
1786
|
+
const progress = new Transform({
|
|
1787
|
+
transform(chunk, _encoding, callback) {
|
|
1788
|
+
received += chunk.length;
|
|
1789
|
+
report.step(`Downloading the template\u2026 ${formatBytes(received)}${total ? ` of ${formatBytes(total)}` : ""}`);
|
|
1790
|
+
callback(null, chunk);
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
const dest = join4(tmpdir(), `mavonengine-create-${process5.pid}-${Date.now()}.zip`);
|
|
1794
|
+
try {
|
|
1795
|
+
await pipeline(Readable.fromWeb(response.body), progress, createWriteStream(dest), { signal });
|
|
1796
|
+
} catch (err) {
|
|
1797
|
+
rmSync(dest, { force: true });
|
|
1798
|
+
throw err;
|
|
1799
|
+
}
|
|
1800
|
+
return dest;
|
|
1801
|
+
}
|
|
1802
|
+
function extractTemplate(zipPath, templateSubpath, destDir) {
|
|
1803
|
+
const entries = new AdmZip(zipPath).getEntries();
|
|
1804
|
+
if (entries.length === 0)
|
|
1805
|
+
throw new Error("Downloaded zip archive is empty");
|
|
1806
|
+
const rootFolder = entries[0].entryName.split("/")[0];
|
|
1807
|
+
const templatePrefix = `${rootFolder}/${templateSubpath}/`;
|
|
1808
|
+
const templateEntries = entries.filter((e) => e.entryName.startsWith(templatePrefix));
|
|
1809
|
+
if (templateEntries.length === 0)
|
|
1810
|
+
throw new Error(`Template path "${templateSubpath}" not found in zip archive (root: ${rootFolder})`);
|
|
1811
|
+
const root = resolve4(destDir);
|
|
1812
|
+
let files = 0;
|
|
1813
|
+
for (const entry of templateEntries) {
|
|
1814
|
+
const relativePath = entry.entryName.slice(templatePrefix.length);
|
|
1815
|
+
if (!relativePath)
|
|
1816
|
+
continue;
|
|
1817
|
+
const outputPath = resolve4(root, relativePath);
|
|
1818
|
+
const inside = relative2(root, outputPath);
|
|
1819
|
+
if (inside.startsWith("..") || isAbsolute2(inside))
|
|
1820
|
+
throw new Error(`Refusing to extract ${entry.entryName} outside the project`);
|
|
1821
|
+
if (entry.isDirectory) {
|
|
1822
|
+
mkdirSync(outputPath, { recursive: true });
|
|
1823
|
+
} else {
|
|
1824
|
+
mkdirSync(dirname3(outputPath), { recursive: true });
|
|
1825
|
+
writeFileSync2(outputPath, entry.getData());
|
|
1826
|
+
files++;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
return files;
|
|
1830
|
+
}
|
|
1831
|
+
function runCommand(command, args, cwd, { report, signal }) {
|
|
1832
|
+
return new Promise((resolvePromise, reject) => {
|
|
1833
|
+
const child = spawn4(command, args, {
|
|
1834
|
+
cwd,
|
|
1835
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1836
|
+
// npm is a .cmd shim on Windows
|
|
1837
|
+
shell: process5.platform === "win32",
|
|
1838
|
+
signal
|
|
1839
|
+
});
|
|
1840
|
+
child.stdout.setEncoding("utf8").on("data", report.output);
|
|
1841
|
+
child.stderr.setEncoding("utf8").on("data", report.output);
|
|
1842
|
+
child.on("error", (err) => {
|
|
1843
|
+
reject(err.code === "ENOENT" ? new Error(`'${command}' was not found on your PATH`) : err);
|
|
1844
|
+
});
|
|
1845
|
+
child.on("exit", (code, exitSignal) => {
|
|
1846
|
+
if (code === 0)
|
|
1847
|
+
resolvePromise();
|
|
1848
|
+
else
|
|
1849
|
+
reject(new Error(`'${command} ${args.join(" ")}' ${code === null ? `was stopped by ${exitSignal}` : `exited with code ${code}`}`));
|
|
1850
|
+
});
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
async function createProject({ projectName, parentDir, report, signal, templateUrl = TEMPLATE_URL, fetch, install = true }) {
|
|
1854
|
+
const projectDir = projectDirFor(projectName, parentDir);
|
|
1855
|
+
if (existsSync4(projectDir))
|
|
1856
|
+
throw new Error(`Directory "${projectDir}" already exists`);
|
|
1857
|
+
report.output(`Creating project "${projectName}" in ${projectDir}
|
|
1858
|
+
`);
|
|
1859
|
+
mkdirSync(projectDir, { recursive: true });
|
|
1860
|
+
report.step("Downloading the template\u2026");
|
|
1861
|
+
report.output(`Downloading the template from ${templateUrl}
|
|
1862
|
+
`);
|
|
1863
|
+
const zipPath = await downloadZip(templateUrl, { report, signal, fetch });
|
|
1864
|
+
try {
|
|
1865
|
+
report.step("Extracting the template\u2026");
|
|
1866
|
+
const files = extractTemplate(zipPath, TEMPLATE_SUBPATH, projectDir);
|
|
1867
|
+
report.output(`Extracted ${files} files
|
|
1868
|
+
`);
|
|
1869
|
+
} finally {
|
|
1870
|
+
rmSync(zipPath, { force: true });
|
|
1871
|
+
}
|
|
1872
|
+
const pkgJsonPath = join4(projectDir, "package.json");
|
|
1873
|
+
if (existsSync4(pkgJsonPath)) {
|
|
1874
|
+
const pkg = JSON.parse(readFileSync4(pkgJsonPath, "utf-8"));
|
|
1875
|
+
pkg.name = projectName;
|
|
1876
|
+
writeFileSync2(pkgJsonPath, `${JSON.stringify(pkg, null, 2)}
|
|
1877
|
+
`);
|
|
1878
|
+
}
|
|
1879
|
+
if (writeMcpConfig(projectDir))
|
|
1880
|
+
report.output(`Wrote ${MCP_CONFIG_FILE} so AI assistants with MCP support can inspect the running game server
|
|
1881
|
+
`);
|
|
1882
|
+
if (install) {
|
|
1883
|
+
signal?.throwIfAborted();
|
|
1884
|
+
report.step("Installing dependencies\u2026");
|
|
1885
|
+
report.output("Running 'npm install'\n");
|
|
1886
|
+
await runCommand("npm", ["install"], projectDir, { report, signal });
|
|
1887
|
+
}
|
|
1888
|
+
return projectDir;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1629
1891
|
// src/commands/create.ts
|
|
1630
1892
|
var CREATE_HELP = `Usage: mavonengine create [name] [options]
|
|
1631
1893
|
|
|
@@ -1642,8 +1904,8 @@ Options:
|
|
|
1642
1904
|
-h, --help Show this help
|
|
1643
1905
|
`;
|
|
1644
1906
|
var MAX_SETUP_OUTPUT = 64 * 1024;
|
|
1645
|
-
function parseCreateArgs(args, cwd =
|
|
1646
|
-
const { values, positionals } =
|
|
1907
|
+
function parseCreateArgs(args, cwd = process6.cwd(), env = process6.env) {
|
|
1908
|
+
const { values, positionals } = parseArgs4({
|
|
1647
1909
|
args,
|
|
1648
1910
|
options: DEV_FLAGS,
|
|
1649
1911
|
strict: true,
|
|
@@ -1664,7 +1926,7 @@ function checkProjectName(name) {
|
|
|
1664
1926
|
throw new UsageError(`invalid project name '${name}'`);
|
|
1665
1927
|
}
|
|
1666
1928
|
async function promptProjectName() {
|
|
1667
|
-
const rl = createInterface({ input:
|
|
1929
|
+
const rl = createInterface({ input: process6.stdin, output: process6.stdout });
|
|
1668
1930
|
try {
|
|
1669
1931
|
return (await rl.question("Project name: ")).trim();
|
|
1670
1932
|
} finally {
|
|
@@ -1718,18 +1980,18 @@ async function runCreateWithDashboard(projectName, parentDir, dashboard) {
|
|
|
1718
1980
|
return 0;
|
|
1719
1981
|
await work.catch(() => {
|
|
1720
1982
|
});
|
|
1721
|
-
|
|
1983
|
+
process6.stderr.write(`mavonengine: stopped creating the project, what was set up so far is in ${projectDirFor(projectName, parentDir)}
|
|
1722
1984
|
`);
|
|
1723
1985
|
return 130;
|
|
1724
1986
|
}
|
|
1725
1987
|
try {
|
|
1726
1988
|
const exitCode = await work;
|
|
1727
|
-
|
|
1989
|
+
process6.stderr.write(`${dev.output()}
|
|
1728
1990
|
mavonengine: the dev script exited with code ${exitCode}
|
|
1729
1991
|
`);
|
|
1730
1992
|
return exitCode;
|
|
1731
1993
|
} catch (err) {
|
|
1732
|
-
|
|
1994
|
+
process6.stderr.write(output);
|
|
1733
1995
|
throw err;
|
|
1734
1996
|
}
|
|
1735
1997
|
}
|
|
@@ -1737,16 +1999,16 @@ async function runCreatePlain(projectName, parentDir) {
|
|
|
1737
1999
|
const report = {
|
|
1738
2000
|
step: () => {
|
|
1739
2001
|
},
|
|
1740
|
-
output: (chunk) =>
|
|
2002
|
+
output: (chunk) => process6.stdout.write(chunk)
|
|
1741
2003
|
};
|
|
1742
2004
|
const dir = await createProject({ projectName, parentDir, report });
|
|
1743
|
-
|
|
2005
|
+
process6.stdout.write(`
|
|
1744
2006
|
mavonengine: your project is ready at ${dir}
|
|
1745
2007
|
`);
|
|
1746
2008
|
return runDev(dir);
|
|
1747
2009
|
}
|
|
1748
2010
|
async function runCreateCommand(options) {
|
|
1749
|
-
const interactive = !!
|
|
2011
|
+
const interactive = !!process6.stdout.isTTY && !!process6.stdin.isTTY;
|
|
1750
2012
|
let projectName = options.projectName;
|
|
1751
2013
|
if (!projectName) {
|
|
1752
2014
|
if (!interactive)
|
|
@@ -1757,13 +2019,395 @@ async function runCreateCommand(options) {
|
|
|
1757
2019
|
checkProjectName(projectName);
|
|
1758
2020
|
}
|
|
1759
2021
|
const dir = projectDirFor(projectName, options.cwd);
|
|
1760
|
-
if (
|
|
2022
|
+
if (existsSync5(dir))
|
|
1761
2023
|
throw new UsageError(`${dir} already exists`);
|
|
1762
2024
|
if (options.dashboard && interactive)
|
|
1763
2025
|
return runCreateWithDashboard(projectName, options.cwd, options.dashboard);
|
|
1764
2026
|
return runCreatePlain(projectName, options.cwd);
|
|
1765
2027
|
}
|
|
1766
2028
|
|
|
2029
|
+
// src/commands/install.ts
|
|
2030
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
2031
|
+
import { join as join5, resolve as resolve5 } from "path";
|
|
2032
|
+
import process7 from "process";
|
|
2033
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
2034
|
+
import { parseArgs as parseArgs5 } from "util";
|
|
2035
|
+
var INSTALL_HELP = `Usage: mavonengine install <package...> [options]
|
|
2036
|
+
|
|
2037
|
+
Install packages into the MavonEngine project you are in. For each one that is a
|
|
2038
|
+
MavonEngine plugin, asks whether to add it to the plugins in ${CONFIG_FILE}, so it
|
|
2039
|
+
loads on the client and/or the game server and is built into the release.
|
|
2040
|
+
|
|
2041
|
+
Takes anything your package manager does: names, name@version, file: paths, git URLs.
|
|
2042
|
+
|
|
2043
|
+
Options:
|
|
2044
|
+
--cwd <dir> Start looking for the project here (default: current directory)
|
|
2045
|
+
-y, --yes Add plugins to ${CONFIG_FILE} without asking
|
|
2046
|
+
--no-register Only install, leave ${CONFIG_FILE} alone
|
|
2047
|
+
-h, --help Show this help
|
|
2048
|
+
`;
|
|
2049
|
+
var PACKAGE_SPEC_RE = /^((?:@[\w.-]+\/)?[\w.-]+)(?:@.*)?$/;
|
|
2050
|
+
var LOCAL_PATH_RE = /^(?:\.{1,2}(?:[/\\]|$)|[/~]|[a-z]:[/\\])/i;
|
|
2051
|
+
var TARGET_LABELS = {
|
|
2052
|
+
client: "client only",
|
|
2053
|
+
server: "server only",
|
|
2054
|
+
both: "client + server"
|
|
2055
|
+
};
|
|
2056
|
+
function parseInstallArgs(args, cwd = process7.cwd()) {
|
|
2057
|
+
const { values, positionals } = parseArgs5({
|
|
2058
|
+
args,
|
|
2059
|
+
options: {
|
|
2060
|
+
cwd: { type: "string" },
|
|
2061
|
+
yes: { type: "boolean", short: "y", default: false },
|
|
2062
|
+
register: { type: "boolean", default: true },
|
|
2063
|
+
help: { type: "boolean", short: "h", default: false }
|
|
2064
|
+
},
|
|
2065
|
+
strict: true,
|
|
2066
|
+
allowPositionals: true,
|
|
2067
|
+
allowNegative: true
|
|
2068
|
+
});
|
|
2069
|
+
if (values.help)
|
|
2070
|
+
return "help";
|
|
2071
|
+
if (positionals.length === 0)
|
|
2072
|
+
throw new UsageError(`which package? e.g. mavonengine install my-plugin
|
|
2073
|
+
|
|
2074
|
+
${INSTALL_HELP}`);
|
|
2075
|
+
return { cwd: resolve5(cwd, values.cwd ?? "."), packages: positionals, yes: values.yes, register: values.register };
|
|
2076
|
+
}
|
|
2077
|
+
function packageNameFromSpec(spec, root) {
|
|
2078
|
+
const localPath = spec.startsWith("file:") ? spec.slice("file:".length) : LOCAL_PATH_RE.test(spec) ? spec : void 0;
|
|
2079
|
+
if (localPath !== void 0) {
|
|
2080
|
+
const packageJson = join5(resolve5(root, localPath), "package.json");
|
|
2081
|
+
try {
|
|
2082
|
+
return existsSync6(packageJson) ? JSON.parse(readFileSync5(packageJson, "utf8")).name : void 0;
|
|
2083
|
+
} catch {
|
|
2084
|
+
return void 0;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
if (spec.includes(":") || spec.endsWith(".tgz"))
|
|
2088
|
+
return void 0;
|
|
2089
|
+
return PACKAGE_SPEC_RE.exec(spec)?.[1];
|
|
2090
|
+
}
|
|
2091
|
+
function installArgs(pm, packages) {
|
|
2092
|
+
return [pm === "npm" ? "install" : "add", ...packages];
|
|
2093
|
+
}
|
|
2094
|
+
function dependencyNames(root) {
|
|
2095
|
+
const path = join5(root, "package.json");
|
|
2096
|
+
if (!existsSync6(path))
|
|
2097
|
+
return /* @__PURE__ */ new Map();
|
|
2098
|
+
const pkg = JSON.parse(readFileSync5(path, "utf8"));
|
|
2099
|
+
return new Map(Object.entries({ ...pkg.devDependencies, ...pkg.dependencies }));
|
|
2100
|
+
}
|
|
2101
|
+
function installedNames(specs, root, before, after) {
|
|
2102
|
+
const names = specs.map((spec) => packageNameFromSpec(spec, root)).filter((name) => !!name);
|
|
2103
|
+
for (const [name, version2] of after) {
|
|
2104
|
+
if (before.get(name) !== version2)
|
|
2105
|
+
names.push(name);
|
|
2106
|
+
}
|
|
2107
|
+
return [...new Set(names)];
|
|
2108
|
+
}
|
|
2109
|
+
function describePlugin(plugin, engineVersion) {
|
|
2110
|
+
const { manifest } = plugin;
|
|
2111
|
+
const lines = [`${plugin.package}${plugin.version ? `@${plugin.version}` : ""} is a MavonEngine plugin (${TARGET_LABELS[manifest.target]})`];
|
|
2112
|
+
const prefabs = Object.keys(manifest.assets?.prefabs ?? {});
|
|
2113
|
+
if (prefabs.length)
|
|
2114
|
+
lines.push(` Prefabs: ${prefabs.join(", ")}. Their assets ship in the release once you place them in the editor`);
|
|
2115
|
+
if (manifest.engine)
|
|
2116
|
+
lines.push(` Made for @mavonengine/core ${manifest.engine}${engineVersion ? `, you have ${engineVersion}` : ""}`);
|
|
2117
|
+
return `${lines.join("\n")}
|
|
2118
|
+
`;
|
|
2119
|
+
}
|
|
2120
|
+
function confirmOnTerminal(question) {
|
|
2121
|
+
const rl = createInterface2({ input: process7.stdin, output: process7.stdout });
|
|
2122
|
+
return rl.question(question).then((answer) => ["", "y", "yes"].includes(answer.trim().toLowerCase())).finally(() => rl.close());
|
|
2123
|
+
}
|
|
2124
|
+
var terminalIO = {
|
|
2125
|
+
run: (command, args, cwd) => runCommand(command, args, cwd, {
|
|
2126
|
+
report: { step: () => {
|
|
2127
|
+
}, output: (chunk) => process7.stdout.write(chunk) }
|
|
2128
|
+
}),
|
|
2129
|
+
confirm: confirmOnTerminal,
|
|
2130
|
+
get interactive() {
|
|
2131
|
+
return !!process7.stdin.isTTY && !!process7.stdout.isTTY;
|
|
2132
|
+
},
|
|
2133
|
+
write: (text) => process7.stdout.write(text)
|
|
2134
|
+
};
|
|
2135
|
+
async function runInstallCommand(options, io = terminalIO) {
|
|
2136
|
+
const root = findProjectRoot(options.cwd);
|
|
2137
|
+
if (!root)
|
|
2138
|
+
throw new UsageError(`no MavonEngine project found in ${options.cwd} or its parents (looking for a package.json with @mavonengine/core and a dev script)`);
|
|
2139
|
+
const config = readConfig(root);
|
|
2140
|
+
const pm = detectPackageManager(root);
|
|
2141
|
+
const args = installArgs(pm, options.packages);
|
|
2142
|
+
const before = dependencyNames(root);
|
|
2143
|
+
io.write(`mavonengine: running '${pm} ${args.join(" ")}' in ${root}
|
|
2144
|
+
`);
|
|
2145
|
+
await io.run(pm, args, root);
|
|
2146
|
+
const engineVersion = installedEngineVersion(root);
|
|
2147
|
+
let failed = false;
|
|
2148
|
+
const names = installedNames(options.packages, root, before, dependencyNames(root));
|
|
2149
|
+
if (names.length === 0)
|
|
2150
|
+
io.write(`
|
|
2151
|
+
mavonengine: couldn't tell which packages were installed. If one is a plugin, run 'mavonengine install <its name>' to register it
|
|
2152
|
+
`);
|
|
2153
|
+
for (const name of names) {
|
|
2154
|
+
let plugin;
|
|
2155
|
+
try {
|
|
2156
|
+
plugin = readPluginManifest(root, name);
|
|
2157
|
+
} catch (err) {
|
|
2158
|
+
io.write(`
|
|
2159
|
+
mavonengine: ${err.message}
|
|
2160
|
+
`);
|
|
2161
|
+
failed = true;
|
|
2162
|
+
continue;
|
|
2163
|
+
}
|
|
2164
|
+
if (!plugin) {
|
|
2165
|
+
io.write(`
|
|
2166
|
+
${name} is installed. It is not a MavonEngine plugin (no "mavonengine" field in its package.json), so ${CONFIG_FILE} is left alone
|
|
2167
|
+
`);
|
|
2168
|
+
continue;
|
|
2169
|
+
}
|
|
2170
|
+
io.write(`
|
|
2171
|
+
${describePlugin(plugin, engineVersion)}`);
|
|
2172
|
+
const entry = config.plugins[name];
|
|
2173
|
+
if (entry && entry.enabled !== false) {
|
|
2174
|
+
io.write(` Already in ${CONFIG_FILE}
|
|
2175
|
+
`);
|
|
2176
|
+
continue;
|
|
2177
|
+
}
|
|
2178
|
+
if (!options.register)
|
|
2179
|
+
continue;
|
|
2180
|
+
if (!options.yes && !io.interactive) {
|
|
2181
|
+
io.write(` Not added to ${CONFIG_FILE}: run with --yes, or add "${name}": { "enabled": true } to its "plugins"
|
|
2182
|
+
`);
|
|
2183
|
+
continue;
|
|
2184
|
+
}
|
|
2185
|
+
if (!options.yes && !await io.confirm(`Add ${name} to the plugins in ${CONFIG_FILE}? (Y/n) `)) {
|
|
2186
|
+
io.write(` Not added. Add "${name}": { "enabled": true } to "plugins" in ${CONFIG_FILE} to use it later
|
|
2187
|
+
`);
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
addPluginToConfig(root, name);
|
|
2191
|
+
io.write(` Added to ${join5(root, CONFIG_FILE)}
|
|
2192
|
+
`);
|
|
2193
|
+
if (plugin.manifest.target !== "client")
|
|
2194
|
+
io.write(` Restart the game server ('mavonengine dev') to load it there
|
|
2195
|
+
`);
|
|
2196
|
+
}
|
|
2197
|
+
return failed ? 1 : 0;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
// src/commands/mcp.ts
|
|
2201
|
+
import process8 from "process";
|
|
2202
|
+
import { parseArgs as parseArgs6 } from "util";
|
|
2203
|
+
|
|
2204
|
+
// src/mcp/GameServerClient.ts
|
|
2205
|
+
var GameServerError = class extends Error {
|
|
2206
|
+
};
|
|
2207
|
+
var DEV_UNAVAILABLE = "The dev endpoints are not available: the game server runs with NODE_ENV=production (they are never served there), or it uses a @mavonengine/core release without them";
|
|
2208
|
+
var GameServerClient = class {
|
|
2209
|
+
url;
|
|
2210
|
+
clientUrl;
|
|
2211
|
+
base;
|
|
2212
|
+
clientBase;
|
|
2213
|
+
token;
|
|
2214
|
+
timeoutMs;
|
|
2215
|
+
fetchImpl;
|
|
2216
|
+
constructor({ url, clientUrl, token, timeoutMs = 3e3, fetch = globalThis.fetch }) {
|
|
2217
|
+
this.base = new URL(url.endsWith("/") ? url : `${url}/`);
|
|
2218
|
+
this.url = this.base.href;
|
|
2219
|
+
this.clientBase = clientUrl ? new URL(clientUrl.endsWith("/") ? clientUrl : `${clientUrl}/`) : this.base;
|
|
2220
|
+
this.clientUrl = this.clientBase.href;
|
|
2221
|
+
this.token = token;
|
|
2222
|
+
this.timeoutMs = timeoutMs;
|
|
2223
|
+
this.fetchImpl = fetch;
|
|
2224
|
+
}
|
|
2225
|
+
health() {
|
|
2226
|
+
return this.get("api/game/health");
|
|
2227
|
+
}
|
|
2228
|
+
metrics() {
|
|
2229
|
+
return this.get("api/game/metrics");
|
|
2230
|
+
}
|
|
2231
|
+
logs(afterId = 0) {
|
|
2232
|
+
return this.get("api/game/logs", { after: String(afterId) }, "The server does not keep logs (metrics.logBufferSize is 0)");
|
|
2233
|
+
}
|
|
2234
|
+
requests(afterId = 0) {
|
|
2235
|
+
return this.get("api/game/requests", { after: String(afterId) }, "The server does not record requests (metrics.requestLogSize is 0)");
|
|
2236
|
+
}
|
|
2237
|
+
scene() {
|
|
2238
|
+
return this.get("api/game/dev/scene", {}, DEV_UNAVAILABLE);
|
|
2239
|
+
}
|
|
2240
|
+
entities(query = {}) {
|
|
2241
|
+
const params = {};
|
|
2242
|
+
if (query.type)
|
|
2243
|
+
params.type = query.type;
|
|
2244
|
+
if (query.near)
|
|
2245
|
+
params.near = `${query.near.x},${query.near.y},${query.near.z}`;
|
|
2246
|
+
for (const key of ["radius", "limit", "offset"]) {
|
|
2247
|
+
if (query[key] !== void 0)
|
|
2248
|
+
params[key] = String(query[key]);
|
|
2249
|
+
}
|
|
2250
|
+
return this.get("api/game/dev/entities", params, DEV_UNAVAILABLE);
|
|
2251
|
+
}
|
|
2252
|
+
entity(id) {
|
|
2253
|
+
return this.get(`api/game/dev/entities/${encodeURIComponent(id)}`, {}, DEV_UNAVAILABLE);
|
|
2254
|
+
}
|
|
2255
|
+
/** The game tabs open in a browser, through the dev bridge */
|
|
2256
|
+
clients() {
|
|
2257
|
+
return this.bridge("__mavonengine/clients", {});
|
|
2258
|
+
}
|
|
2259
|
+
/**
|
|
2260
|
+
* Ask one tab. `clientId` defaults to the only open tab (or the only one not running the editor)
|
|
2261
|
+
*/
|
|
2262
|
+
client(query, params = {}, clientId = "auto") {
|
|
2263
|
+
const defined = Object.fromEntries(Object.entries(params).filter(([, v]) => v !== void 0).map(([k, v]) => [k, String(v)]));
|
|
2264
|
+
const timeoutMs = query === "trace" ? Number(params.durationMs ?? 3e3) + 15e3 : query === "screenshot" ? 15e3 : query === "status" ? 1e4 : 8e3;
|
|
2265
|
+
return this.bridge(`__mavonengine/clients/${encodeURIComponent(clientId)}/${query}`, defined, timeoutMs);
|
|
2266
|
+
}
|
|
2267
|
+
async bridge(path, params, timeoutMs = 5e3) {
|
|
2268
|
+
const target = new URL(path, this.clientBase);
|
|
2269
|
+
for (const [key, value] of Object.entries(params))
|
|
2270
|
+
target.searchParams.set(key, value);
|
|
2271
|
+
let res;
|
|
2272
|
+
try {
|
|
2273
|
+
res = await this.fetchImpl(target, {
|
|
2274
|
+
headers: this.token ? { Authorization: `Bearer ${this.token}` } : {},
|
|
2275
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
2276
|
+
});
|
|
2277
|
+
} catch (err) {
|
|
2278
|
+
throw new GameServerError(`Can't reach the client dev server at ${this.clientUrl}: ${err.message}. Is it running?`);
|
|
2279
|
+
}
|
|
2280
|
+
const isJson = res.headers.get("content-type")?.includes("application/json");
|
|
2281
|
+
const body = isJson ? await res.json().catch(() => null) : null;
|
|
2282
|
+
if (!isJson || res.status === 404 && !body?.error) {
|
|
2283
|
+
throw new GameServerError(`No dev bridge at ${this.clientUrl}. The game client must be served by Vite in dev with the mavonengine Vite plugin (@mavonengine/core/vite.config). If Vite runs on its own port, pass --client-url, e.g. http://localhost:5173`);
|
|
2284
|
+
}
|
|
2285
|
+
if (res.status === 401 || res.status === 403)
|
|
2286
|
+
throw new GameServerError(`${body?.error ?? "Not allowed"}: pass --token or set MAVONENGINE_METRICS_TOKEN for both the CLI and the dev server`);
|
|
2287
|
+
if (!res.ok)
|
|
2288
|
+
throw new GameServerError(body?.error ?? `Client dev server responded ${res.status} ${res.statusText}`);
|
|
2289
|
+
return body;
|
|
2290
|
+
}
|
|
2291
|
+
async get(path, params = {}, notFound) {
|
|
2292
|
+
const target = new URL(path, this.base);
|
|
2293
|
+
for (const [key, value] of Object.entries(params))
|
|
2294
|
+
target.searchParams.set(key, value);
|
|
2295
|
+
let res;
|
|
2296
|
+
try {
|
|
2297
|
+
res = await this.fetchImpl(target, {
|
|
2298
|
+
headers: this.token ? { Authorization: `Bearer ${this.token}` } : {},
|
|
2299
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
2300
|
+
});
|
|
2301
|
+
} catch (err) {
|
|
2302
|
+
throw new GameServerError(`Can't reach the game server at ${this.url}: ${err.message}. Is it running?`);
|
|
2303
|
+
}
|
|
2304
|
+
if (res.status === 401)
|
|
2305
|
+
throw new GameServerError("Metrics token missing or wrong: pass --token or set MAVONENGINE_METRICS_TOKEN");
|
|
2306
|
+
if (res.status === 403)
|
|
2307
|
+
throw new GameServerError("The game server only answers localhost without a token: pass --token or set MAVONENGINE_METRICS_TOKEN");
|
|
2308
|
+
if (res.status === 404)
|
|
2309
|
+
throw new GameServerError(notFound ?? "Metrics endpoints not found: they are turned off in the server options, or it runs in production without a metrics token");
|
|
2310
|
+
if (res.status === 400)
|
|
2311
|
+
throw new GameServerError(`Bad request: ${(await res.json().catch(() => null))?.error ?? res.statusText}`);
|
|
2312
|
+
if (!res.ok)
|
|
2313
|
+
throw new GameServerError(`Game server responded ${res.status} ${res.statusText}`);
|
|
2314
|
+
return res.json();
|
|
2315
|
+
}
|
|
2316
|
+
};
|
|
2317
|
+
|
|
2318
|
+
// src/commands/mcp.ts
|
|
2319
|
+
var MCP_HELP = `Usage: mavonengine mcp [options]
|
|
2320
|
+
|
|
2321
|
+
Model Context Protocol (MCP) server over stdio, for any MCP client and model
|
|
2322
|
+
(Claude Code, Cursor, VS Code, Codex, Gemini CLI, Zed, local models, \u2026).
|
|
2323
|
+
It lets the assistant read a running game: the game server (status, metrics,
|
|
2324
|
+
players, scene, entities, logs, HTTP requests) and the game tabs open in a
|
|
2325
|
+
browser (renderer stats, entities, network, console, screenshots), and compare
|
|
2326
|
+
an entity on both sides. Every tool is read-only. Server scene and entity tools
|
|
2327
|
+
need the game server in development (NODE_ENV is not "production"); client
|
|
2328
|
+
tools need the client served by Vite in dev.
|
|
2329
|
+
|
|
2330
|
+
Register it in your client as a stdio server with this command:
|
|
2331
|
+
npx -y @mavonengine/cli mcp [options]
|
|
2332
|
+
|
|
2333
|
+
Most clients take JSON like this (projects from 'mavonengine create' get it in .mcp.json):
|
|
2334
|
+
{ "mcpServers": { "mavonengine": { "command": "npx", "args": ["-y", "@mavonengine/cli", "mcp"] } } }
|
|
2335
|
+
|
|
2336
|
+
Options:
|
|
2337
|
+
--host <host> Server host (default: localhost)
|
|
2338
|
+
--port <port> Server HTTP port (default: 8050)
|
|
2339
|
+
--url <url> Server base URL, e.g. http://localhost:8050, overrides --host and --port
|
|
2340
|
+
--client-url <url> Where Vite serves the client in dev (default: the server URL,
|
|
2341
|
+
which serves it in the template), e.g. http://localhost:5173
|
|
2342
|
+
--token <token> Metrics token (or set MAVONENGINE_METRICS_TOKEN)
|
|
2343
|
+
--docs-url <url> MCP server of the documentation for the docs_* tools
|
|
2344
|
+
(default: https://mavonengine.com/mcp)
|
|
2345
|
+
--no-docs Leave out the docs_* tools, e.g. offline
|
|
2346
|
+
-h, --help Show this help
|
|
2347
|
+
`;
|
|
2348
|
+
function parseMcpArgs(args, env = process8.env) {
|
|
2349
|
+
const { values } = parseArgs6({
|
|
2350
|
+
args,
|
|
2351
|
+
options: {
|
|
2352
|
+
"host": { type: "string", default: "localhost" },
|
|
2353
|
+
"port": { type: "string", default: "8050" },
|
|
2354
|
+
"url": { type: "string" },
|
|
2355
|
+
"client-url": { type: "string" },
|
|
2356
|
+
"token": { type: "string" },
|
|
2357
|
+
"docs-url": { type: "string" },
|
|
2358
|
+
"docs": { type: "boolean", default: true },
|
|
2359
|
+
"help": { type: "boolean", short: "h", default: false }
|
|
2360
|
+
},
|
|
2361
|
+
strict: true,
|
|
2362
|
+
allowPositionals: false,
|
|
2363
|
+
allowNegative: true
|
|
2364
|
+
});
|
|
2365
|
+
if (values.help)
|
|
2366
|
+
return "help";
|
|
2367
|
+
let url = values.url;
|
|
2368
|
+
if (!url) {
|
|
2369
|
+
const port = Number(values.port);
|
|
2370
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
2371
|
+
throw new UsageError("--port must be between 1 and 65535");
|
|
2372
|
+
const host = values.host.includes(":") && !values.host.startsWith("[") ? `[${values.host}]` : values.host;
|
|
2373
|
+
url = `http://${host}:${port}`;
|
|
2374
|
+
}
|
|
2375
|
+
for (const [flag, value] of [["--url", url], ["--client-url", values["client-url"]], ["--docs-url", values["docs-url"]]]) {
|
|
2376
|
+
try {
|
|
2377
|
+
if (value !== void 0)
|
|
2378
|
+
void new URL(value);
|
|
2379
|
+
} catch {
|
|
2380
|
+
throw new UsageError(`Invalid ${flag}: ${value}`);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
return {
|
|
2384
|
+
client: new GameServerClient({ url, clientUrl: values["client-url"], token: values.token ?? env.MAVONENGINE_METRICS_TOKEN }),
|
|
2385
|
+
docsUrl: values.docs ? values["docs-url"] ?? DEFAULT_DOCS_URL : false
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
async function runMcpCommand({ client, docsUrl }) {
|
|
2389
|
+
const [{ StdioServerTransport }, { createMcpServer }, { DocsClient }] = await Promise.all([
|
|
2390
|
+
import("@modelcontextprotocol/sdk/server/stdio.js"),
|
|
2391
|
+
import("./server-35RY6IGC.js"),
|
|
2392
|
+
import("./docs-4C7X6FUQ.js")
|
|
2393
|
+
]);
|
|
2394
|
+
const docs = docsUrl ? new DocsClient(docsUrl) : void 0;
|
|
2395
|
+
const server = createMcpServer(client, docs);
|
|
2396
|
+
const transport = new StdioServerTransport();
|
|
2397
|
+
const closed = new Promise((resolve6) => {
|
|
2398
|
+
transport.onclose = resolve6;
|
|
2399
|
+
process8.stdin.on("end", resolve6);
|
|
2400
|
+
});
|
|
2401
|
+
await server.connect(transport);
|
|
2402
|
+
process8.stderr.write(`mavonengine mcp: reading the game server at ${client.url}
|
|
2403
|
+
`);
|
|
2404
|
+
await closed;
|
|
2405
|
+
await server.close().catch(() => {
|
|
2406
|
+
});
|
|
2407
|
+
await docs?.close();
|
|
2408
|
+
return 0;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
1767
2411
|
// src/index.ts
|
|
1768
2412
|
var HELP = `Usage: mavonengine <command> [options]
|
|
1769
2413
|
|
|
@@ -1771,6 +2415,9 @@ Commands:
|
|
|
1771
2415
|
create Create a project from the multiplayer template and start it
|
|
1772
2416
|
dashboard Live terminal dashboard for a running game server
|
|
1773
2417
|
dev Run the project's dev servers (game server and client)
|
|
2418
|
+
install Install packages and register engine plugins in mavonengine.json
|
|
2419
|
+
build Build the project for release, with the plugin assets it uses
|
|
2420
|
+
mcp MCP server so AI assistants (any MCP client) can inspect a running game server
|
|
1774
2421
|
|
|
1775
2422
|
Options:
|
|
1776
2423
|
-h, --help Show help
|
|
@@ -1781,18 +2428,18 @@ Run 'mavonengine <command> --help' for command options.
|
|
|
1781
2428
|
async function main(argv) {
|
|
1782
2429
|
const [command, ...args] = argv;
|
|
1783
2430
|
if (!command || command === "-h" || command === "--help" || command === "help") {
|
|
1784
|
-
|
|
2431
|
+
process9.stdout.write(HELP);
|
|
1785
2432
|
return 0;
|
|
1786
2433
|
}
|
|
1787
2434
|
if (command === "-v" || command === "--version") {
|
|
1788
|
-
|
|
2435
|
+
process9.stdout.write(`${version}
|
|
1789
2436
|
`);
|
|
1790
2437
|
return 0;
|
|
1791
2438
|
}
|
|
1792
2439
|
if (command === "create") {
|
|
1793
2440
|
const options = parseCreateArgs(args);
|
|
1794
2441
|
if (options === "help") {
|
|
1795
|
-
|
|
2442
|
+
process9.stdout.write(CREATE_HELP);
|
|
1796
2443
|
return 0;
|
|
1797
2444
|
}
|
|
1798
2445
|
return runCreateCommand(options);
|
|
@@ -1800,11 +2447,11 @@ async function main(argv) {
|
|
|
1800
2447
|
if (command === "dashboard") {
|
|
1801
2448
|
const options = parseDashboardArgs(args);
|
|
1802
2449
|
if (options === "help") {
|
|
1803
|
-
|
|
2450
|
+
process9.stdout.write(DASHBOARD_HELP);
|
|
1804
2451
|
return 0;
|
|
1805
2452
|
}
|
|
1806
|
-
if (!
|
|
1807
|
-
|
|
2453
|
+
if (!process9.stdout.isTTY || !process9.stdin.isTTY) {
|
|
2454
|
+
process9.stderr.write("mavonengine dashboard needs an interactive terminal\n");
|
|
1808
2455
|
return 1;
|
|
1809
2456
|
}
|
|
1810
2457
|
await runDashboard(options);
|
|
@@ -1813,18 +2460,42 @@ async function main(argv) {
|
|
|
1813
2460
|
if (command === "dev") {
|
|
1814
2461
|
const options = parseDevArgs(args);
|
|
1815
2462
|
if (options === "help") {
|
|
1816
|
-
|
|
2463
|
+
process9.stdout.write(DEV_HELP);
|
|
1817
2464
|
return 0;
|
|
1818
2465
|
}
|
|
1819
2466
|
return runDevCommand(options);
|
|
1820
2467
|
}
|
|
2468
|
+
if (command === "install" || command === "i" || command === "add") {
|
|
2469
|
+
const options = parseInstallArgs(args);
|
|
2470
|
+
if (options === "help") {
|
|
2471
|
+
process9.stdout.write(INSTALL_HELP);
|
|
2472
|
+
return 0;
|
|
2473
|
+
}
|
|
2474
|
+
return runInstallCommand(options);
|
|
2475
|
+
}
|
|
2476
|
+
if (command === "mcp") {
|
|
2477
|
+
const options = parseMcpArgs(args);
|
|
2478
|
+
if (options === "help") {
|
|
2479
|
+
process9.stdout.write(MCP_HELP);
|
|
2480
|
+
return 0;
|
|
2481
|
+
}
|
|
2482
|
+
return runMcpCommand(options);
|
|
2483
|
+
}
|
|
2484
|
+
if (command === "build") {
|
|
2485
|
+
const options = parseBuildArgs(args);
|
|
2486
|
+
if (options === "help") {
|
|
2487
|
+
process9.stdout.write(BUILD_HELP);
|
|
2488
|
+
return 0;
|
|
2489
|
+
}
|
|
2490
|
+
return runBuildCommand(options);
|
|
2491
|
+
}
|
|
1821
2492
|
throw new UsageError(`Unknown command '${command}'
|
|
1822
2493
|
|
|
1823
2494
|
${HELP}`);
|
|
1824
2495
|
}
|
|
1825
|
-
main(
|
|
2496
|
+
main(process9.argv.slice(2)).then((code) => process9.exit(code)).catch((err) => {
|
|
1826
2497
|
const isUsage = err instanceof UsageError || err.code?.startsWith("ERR_PARSE_ARGS");
|
|
1827
|
-
|
|
2498
|
+
process9.stderr.write(`mavonengine: ${err.message}
|
|
1828
2499
|
`);
|
|
1829
|
-
|
|
2500
|
+
process9.exit(isUsage ? 2 : 1);
|
|
1830
2501
|
});
|