@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/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 process6 from "process";
5
-
6
- // package.json
7
- var version = "0.0.1";
10
+ import process9 from "process";
8
11
 
9
- // src/commands/create.ts
10
- import { existsSync as existsSync3 } from "fs";
11
- import process5 from "process";
12
- import { createInterface } from "readline/promises";
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
- // src/create/project.ts
16
- import { spawn } from "child_process";
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
- import process from "process";
21
- import { Readable, Transform } from "stream";
22
- import { pipeline } from "stream/promises";
23
- import AdmZip from "adm-zip";
24
-
25
- // src/dashboard/format.ts
26
- var BYTE_UNITS = ["B", "KiB", "MiB", "GiB", "TiB"];
27
- function formatBytes(bytes, decimals = 1) {
28
- if (!Number.isFinite(bytes) || bytes <= 0)
29
- return "0 B";
30
- const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), BYTE_UNITS.length - 1);
31
- const value = bytes / 1024 ** exponent;
32
- return `${exponent === 0 ? value : value.toFixed(decimals)} ${BYTE_UNITS[exponent]}`;
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 formatRate(bytesPerSecond) {
35
- return `${formatBytes(bytesPerSecond)}/s`;
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 formatDuration(totalSeconds) {
38
- const seconds = Math.max(0, Math.floor(totalSeconds));
39
- const d = Math.floor(seconds / 86400);
40
- const h = Math.floor(seconds % 86400 / 3600);
41
- const m = Math.floor(seconds % 3600 / 60);
42
- const s = seconds % 60;
43
- if (d > 0)
44
- return `${d}d ${h}h ${m}m`;
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 formatMs(ms) {
52
- return `${ms.toFixed(ms < 10 ? 2 : 1)} ms`;
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 formatPercent(value) {
55
- return `${value.toFixed(1)}%`;
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
- // src/create/project.ts
59
- var TEMPLATE_URL = "https://github.com/MavonEngine/Core/archive/refs/tags/0.0.15-alpha.zip";
60
- var TEMPLATE_SUBPATH = "packages/multiplayer-template";
61
- var PROJECT_NAME_RE = /^[\w@./-][\w@./\- ]*$/;
62
- function projectDirFor(projectName, parentDir) {
63
- return resolve(parentDir, projectName);
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
- async function downloadZip(url, { report, signal, fetch = globalThis.fetch }) {
66
- const response = await fetch(url, { signal });
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 extractTemplate(zipPath, templateSubpath, destDir) {
88
- const entries = new AdmZip(zipPath).getEntries();
89
- if (entries.length === 0)
90
- throw new Error("Downloaded zip archive is empty");
91
- const rootFolder = entries[0].entryName.split("/")[0];
92
- const templatePrefix = `${rootFolder}/${templateSubpath}/`;
93
- const templateEntries = entries.filter((e) => e.entryName.startsWith(templatePrefix));
94
- if (templateEntries.length === 0)
95
- throw new Error(`Template path "${templateSubpath}" not found in zip archive (root: ${rootFolder})`);
96
- const root = resolve(destDir);
97
- let files = 0;
98
- for (const entry of templateEntries) {
99
- const relativePath = entry.entryName.slice(templatePrefix.length);
100
- if (!relativePath)
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 files;
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
- function runCommand(command, args, cwd, { report, signal }) {
117
- return new Promise((resolvePromise, reject) => {
118
- const child = spawn(command, args, {
119
- cwd,
120
- stdio: ["ignore", "pipe", "pipe"],
121
- // npm is a .cmd shim on Windows
122
- shell: process.platform === "win32",
123
- signal
124
- });
125
- child.stdout.setEncoding("utf8").on("data", report.output);
126
- child.stderr.setEncoding("utf8").on("data", report.output);
127
- child.on("error", (err) => {
128
- reject(err.code === "ENOENT" ? new Error(`'${command}' was not found on your PATH`) : err);
129
- });
130
- child.on("exit", (code, exitSignal) => {
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
- async function createProject({ projectName, parentDir, report, signal, templateUrl = TEMPLATE_URL, fetch, install = true }) {
139
- const projectDir = projectDirFor(projectName, parentDir);
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
- report.step("Extracting the template\u2026");
151
- const files = extractTemplate(zipPath, TEMPLATE_SUBPATH, projectDir);
152
- report.output(`Extracted ${files} files
153
- `);
154
- } finally {
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
- if (install) {
165
- signal?.throwIfAborted();
166
- report.step("Installing dependencies\u2026");
167
- report.output("Running 'npm install'\n");
168
- await runCommand("npm", ["install"], projectDir, { report, signal });
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 = process3.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 spawn3 } from "child_process";
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 process4 from "process";
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 script of the MavonEngine project you are in (game server and client)
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 = process4.cwd(), env = process4.env) {
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 && typeof pkg?.scripts?.dev === "string")
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 (process4.platform === "win32")
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
- process4.kill(p, signal);
1557
+ process3.kill(p, signal);
1505
1558
  } catch {
1506
1559
  }
1507
1560
  }
1508
1561
  }
1509
1562
  function isAlive(pid) {
1510
1563
  try {
1511
- process4.kill(pid, 0);
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 (process4.platform === "win32") {
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 child = spawn3(pm, ["run", "dev"], {
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: process4.platform === "win32"
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 dev' in ${root}
1599
+ const announce = `mavonengine: running '${pm} run ${script}' in ${root}
1545
1600
  `;
1546
1601
  if (capture)
1547
1602
  keep(announce);
1548
1603
  else
1549
- process4.stdout.write(announce);
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
- process4.on("SIGINT", onInterrupt);
1558
- process4.on("SIGTERM", forward);
1559
- process4.on("SIGHUP", forward);
1560
- process4.on("exit", onExit);
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
- process4.off("SIGINT", onInterrupt);
1565
- process4.off("SIGTERM", forward);
1566
- process4.off("SIGHUP", forward);
1567
- process4.off("exit", onExit);
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 (process4.platform === "win32") {
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
- process4.stderr.write(`${dev.output()}
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
- if (options.dashboard && process4.stdout.isTTY && process4.stdin.isTTY)
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 = process5.cwd(), env = process5.env) {
1646
- const { values, positionals } = parseArgs3({
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: process5.stdin, output: process5.stdout });
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
- process5.stderr.write(`mavonengine: stopped creating the project, what was set up so far is in ${projectDirFor(projectName, parentDir)}
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
- process5.stderr.write(`${dev.output()}
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
- process5.stderr.write(output);
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) => process5.stdout.write(chunk)
2002
+ output: (chunk) => process6.stdout.write(chunk)
1741
2003
  };
1742
2004
  const dir = await createProject({ projectName, parentDir, report });
1743
- process5.stdout.write(`
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 = !!process5.stdout.isTTY && !!process5.stdin.isTTY;
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 (existsSync3(dir))
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
- process6.stdout.write(HELP);
2431
+ process9.stdout.write(HELP);
1785
2432
  return 0;
1786
2433
  }
1787
2434
  if (command === "-v" || command === "--version") {
1788
- process6.stdout.write(`${version}
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
- process6.stdout.write(CREATE_HELP);
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
- process6.stdout.write(DASHBOARD_HELP);
2450
+ process9.stdout.write(DASHBOARD_HELP);
1804
2451
  return 0;
1805
2452
  }
1806
- if (!process6.stdout.isTTY || !process6.stdin.isTTY) {
1807
- process6.stderr.write("mavonengine dashboard needs an interactive terminal\n");
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
- process6.stdout.write(DEV_HELP);
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(process6.argv.slice(2)).then((code) => process6.exit(code)).catch((err) => {
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
- process6.stderr.write(`mavonengine: ${err.message}
2498
+ process9.stderr.write(`mavonengine: ${err.message}
1828
2499
  `);
1829
- process6.exit(isUsage ? 2 : 1);
2500
+ process9.exit(isUsage ? 2 : 1);
1830
2501
  });