@mavonengine/cli 0.0.2 → 0.0.4

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 CHANGED
@@ -54,6 +54,36 @@ The project root is the nearest `package.json`, from the current directory upwar
54
54
  | `--transparent` | | Let a transparent terminal background show through |
55
55
  | `--no-open` | | Don't open the game in the browser once the server is up |
56
56
 
57
+
58
+ ### Installing plugins while it runs
59
+
60
+ `mavonengine dev` can install packages into the project and restart the dev servers afterwards, so a new plugin is loaded without stopping anything yourself:
61
+
62
+ - from the dashboard's Plugins tab, with `i` on a plugin that is available to install;
63
+ - from the editor's Plugins view, with the Install button of an available plugin, or by typing any package (a name, `name@version`, a `file:` path or a URL) into "Another package".
64
+
65
+ Both ask first and point at the package's source: installing a package runs its code on your machine, and nothing reviews what you install. The install is the same as [`mavonengine install --yes`](#mavonengine-install), followed by a restart of the dev script.
66
+
67
+ The editor reaches `mavonengine dev` through the dev server it started (`POST /__mavonengine/plugin-install`), which only takes requests from localhost and from the game's own page, so another site open in the browser can't install anything. Version ranges such as `^1.0.0` and anything with spaces are refused there; use the terminal for those.
68
+
69
+ ## `mavonengine plugins`
70
+
71
+ Lists the plugins that are available to install.
72
+
73
+ ```sh
74
+ npx @mavonengine/cli plugins
75
+ ```
76
+
77
+ The list is [`plugins.json`](https://github.com/MavonEngine/Core/blob/main/plugins.json) at the root of the MavonEngine repository, read from the `main` branch on GitHub every time the command runs:
78
+
79
+ ```
80
+ https://raw.githubusercontent.com/MavonEngine/Core/main/plugins.json
81
+ ```
82
+
83
+ So a plugin shows up as soon as it is added to that file on `main` (GitHub may serve the file a few minutes old). Inside a project the command also says which of the plugins are already in its `mavonengine.json`. `--json` prints the list as JSON, and `--url <url>` or `MAVONENGINE_PLUGINS_URL` reads it from somewhere else, such as a fork or a branch.
84
+
85
+ To list a plugin, add an entry to `plugins.json`: `package` (the npm name) is required; `name`, `description`, `target` (`client`, `server` or `both`), `engine` (the supported `@mavonengine/core` range), `homepage` and `readme` (the URL of its README as Markdown, such as the raw file on GitHub, which the editor shows) are optional.
86
+
57
87
  ## `mavonengine install`
58
88
 
59
89
  Installs packages into the project you are in, with its package manager. For each one that is a MavonEngine [plugin](#writing-a-plugin), it shows where the plugin runs (client, server or both) and which prefabs it brings, and asks whether to add it to the `plugins` in `mavonengine.json`. Registered plugins load on the client and/or the game server and are built into the release.
@@ -121,16 +151,26 @@ npx @mavonengine/cli dashboard --sim
121
151
  | `--host <host>` | `localhost` | Server host |
122
152
  | `--port <port>` | `8050` | Server HTTP port (`ServerOptions.httpPort`) |
123
153
  | `--url <url>` | | Full metrics URL, overrides `--host` and `--port` |
124
- | `--token <token>` | `$MAVONENGINE_METRICS_TOKEN` | Metrics token (`ServerOptions.metrics.token`) |
154
+ | `--token <token>` | `$MAVONENGINE_METRICS_TOKEN`, then `$MAVONENGINE_ADMIN_TOKEN` | Metrics token (`ServerOptions.metrics.token`), or the server's [admin token](#admin-token) |
125
155
  | `--interval <ms>` | `1000` | Poll interval |
126
156
  | `--theme <name>` | `dark` | `dark`, `dracula`, `nord`, `tokyoNight`, `gruvbox`, `matrix`, `monochrome`, `highContrast`, `light` |
127
157
  | `--transparent` | | Let a transparent terminal background show through. Text can be hard to read over a light wallpaper |
128
158
  | `--sim` | | Simulated data instead of a server |
129
159
 
130
- Keys: `q` quits, `r` refreshes now, `o` opens the game the server serves in the browser, `1`–`4` switch between the Overview, Players, Logs and HTTP tabs. The mouse wheel scrolls the players table, the logs and the requests; scroll the logs back to the bottom, or the requests back to the top, to follow new ones again.
160
+ Keys: `q` quits, `r` refreshes now, `o` opens the game the server serves in the browser, `1`–`7` switch between the Overview, Players, Logs, HTTP, Network, Routes and Plugins tabs. The mouse wheel scrolls the players table, the logs, the requests, the packets and the routes; scroll the logs back to the bottom, or the requests and packets back to the top, to follow new ones again.
131
161
 
132
162
  The HTTP tab lists every request the game server's HTTP server received, newest first: the client (static files or Vite in dev), your own `onHttp` routes, the geckos.io signaling requests and WebSocket upgrades such as Vite's HMR socket, each with status, duration, size and client address. Query strings are left out since they can hold tokens, and the dashboard's own polling is not recorded. The game traffic itself is WebRTC, not HTTP, so it shows on the Players tab instead; with a separate `socketPort` the signaling requests go to that port and don't show up either.
133
163
 
164
+ The Network tab shows the game socket while developing. State sync lists each player with the entities in its last state update, its last acknowledged input and the size and tick of that update (red when it fell behind the others), and counts the synced entities no player is in range of. By message breaks the traffic of the last 5 seconds down by message name and direction: packets per second, average size and bytes per second, the largest share first. Below, every packet and network event, newest first, with its client, size, tick, input sequence id and a few facts about its payload; `p` shows the pings, `e` only the events (`connect`, `disconnect`, `dropped`, `sync_enter`, `sync_leave`). Click a packet, or move through the list with the arrow keys (or `j` / `k`), to see its content next to the list: the payload as it went over the wire, scrolled with the mouse wheel. `esc`, or a second click on it, closes it. The server drops the payloads of old packets to bound its memory, so only recent ones can be opened. The packets are only fetched while the tab shows, and only a server that is not running in production records them, see the [network tools](#mavonengine-mcp).
165
+
166
+ The Routes tab lists every HTTP endpoint the server has: the engine's own, the ones your server registered in `onHttp` and those of plugins, each with its method, path, whether it only exists in development or is served in production too, where it comes from and what it does. The list is read from the running server, so a new route shows up without being declared anywhere; say what it does with `describeRoute()` from `@mavonengine/core/Networking/Server/Http/RouteCatalog`, and pass it `devOnly: true` for a route you only register in development, since a route that doesn't say is listed as `prod + dev`. It needs a server on a `@mavonengine/core` release with `GET /api/game/routes`.
167
+
168
+ The Plugins tab lists the plugins in the project's `mavonengine.json` with their version, whether they are enabled, where they run and how many endpoints and prefabs they have. `↑`/`↓` (or `j`/`k`, or a click) pick one to see its description, supported engine versions, license, prefabs and the HTTP endpoints it registered. A server plugin that is enabled but not running in this server shows as `not loaded`. It needs a server on a `@mavonengine/core` release with `GET /api/game/plugins`.
169
+
170
+ Below it, "Available to install" lists the plugins from [`mavonengine plugins`](#mavonengine-plugins) that the project doesn't have yet. The dashboard reads that list from GitHub itself, the first time the tab is shown and again on `r`; a dashboard that never opens the tab never asks for it.
171
+
172
+ Under [`mavonengine dev`](#mavonengine-dev) a plugin from that list can be installed right there: select it with `↓` and press `i`. The dashboard first asks whether you checked its source, because installing a package runs its code on your machine; `y` installs it with the project's package manager, adds it to `mavonengine.json` and restarts the dev servers, `n` cancels. A dashboard started on its own with `mavonengine dashboard` only shows the command, since it can't restart servers it didn't start.
173
+
134
174
  ### Server access
135
175
 
136
176
  The metrics endpoint is configured on the game server:
@@ -150,9 +190,24 @@ const server = new MyServer(logger, physicsWorld, {
150
190
 
151
191
  - With no token, the endpoint only answers requests from localhost.
152
192
  - With `NODE_ENV=production` and no token, the endpoint is turned off.
193
+ - The server's [admin token](#admin-token) is taken as well, and turns the endpoint on in production by itself.
153
194
  - Set `metrics.enabled: false` to turn it off everywhere.
154
195
  - The logs endpoint follows the same rules. It serves whatever passes your logger's level.
155
196
 
197
+ ### Admin token
198
+
199
+ A game server has one admin token, for everything that changes it or tells what not everyone should know. The engine and every plugin use the same one, so no plugin needs a secret of its own:
200
+
201
+ ```sh
202
+ MAVONENGINE_ADMIN_TOKEN=… npm start
203
+ ```
204
+
205
+ or `adminToken` in the server's options. Requests send it as `Authorization: Bearer <token>`. Without a token, admin routes only answer requests from localhost.
206
+
207
+ - It also reads the metrics, so the dashboard and `mavonengine mcp` take it for `--token`. The metrics token stays read-only: it administers nothing.
208
+ - [`@mavonengine/chat`](../plugins/chat) guards its admin API with it, and [`@mavonengine/http-monitoring`](../plugins/http-monitoring) takes it as the password of its login.
209
+ - The Routes tab shows which routes ask for it.
210
+
156
211
  ## `mavonengine mcp`
157
212
 
158
213
  A [Model Context Protocol](https://modelcontextprotocol.io) server over stdio, so an AI coding assistant can look at your running game: the game server, and the game tabs open in a browser. It works with any MCP client and any model: Claude Code, Cursor, VS Code, Codex, Gemini CLI, Zed, or a local model behind an MCP-capable client.
@@ -179,6 +234,8 @@ Game server tools:
179
234
  | `get_entity` | One entity with the state it sends to clients (dev only) |
180
235
  | `recent_logs` | Server log lines, filtered by level and text |
181
236
  | `recent_requests` | HTTP requests the server answered, filtered by path and status |
237
+ | `network_packets` / `network_packet` | Packets the server sent and received and its network events, per client: size, tick, input sequence id; and one packet with its payload (dev only) |
238
+ | `network_sync` | Who is sent what: each player's tracked entities and last state update, and by how many players each entity is tracked (dev only) |
182
239
 
183
240
  Client tools, for the game tabs open while Vite serves the client in dev:
184
241
 
@@ -188,12 +245,24 @@ Client tools, for the game tabs open while Vite serves the client in dev:
188
245
  | `client_status` | Draw calls and triangles of one frame, GPU geometries / textures / programs, JS heap, canvas, camera |
189
246
  | `client_entities` / `client_entity` | The tab's world, in the same shape as the server's, with its local player marked |
190
247
  | `client_scene` | The scene loaded in the tab, and which objects the client built |
191
- | `client_network` | Connection, ping, and inputs sent but not yet acknowledged by the server |
248
+ | `client_network` | Connection, ping, inputs sent but not yet acknowledged by the server, and the last state update: its server tick, ack and age |
249
+ | `client_packets` / `client_packet` | Packets the tab sent and received and its network events, and one packet with its payload |
250
+ | `network_timeline` | The server's records for one tab and the tab's own, merged by time |
192
251
  | `client_logs` | Console output, uncaught errors and unhandled rejections, filtered by level and text |
193
252
  | `client_screenshot` | What the tab renders, as an image (the WebGL canvas, without HTML UI) |
194
253
  | `client_trace` | Records a few seconds of the tab and analyses it: frame times, render CPU vs GPU, composer passes, ticks, WebGL work per frame, the bottleneck, and the worst hitches with their causes. Saves a flame chart trace file |
195
254
  | `compare_entity` | One entity on the server next to a tab's copy: position / rotation / scale deltas and differing fields |
196
255
 
256
+ Plugin tools:
257
+
258
+ | Tool | |
259
+ | --- | --- |
260
+ | `list_plugins` | The plugins in the project's `mavonengine.json`: version, whether each is enabled and runs on the server, its prefabs and the HTTP endpoints it registered |
261
+ | `available_plugins` | The official plugins from [`mavonengine plugins`](#mavonengine-plugins): what each does, where it runs, the command that installs it, and whether the project already has it |
262
+ | `plugin_readme` | The README of one of them as Markdown: its options, how a game hooks into it and its limits |
263
+
264
+ With these an assistant can suggest a plugin instead of building what one already does, and set it up from its readme. It can't install one: every tool is read-only, so it hands you the command. `available_plugins` and `plugin_readme` read the list and the readmes from GitHub when they are used; `--plugins-url` or `MAVONENGINE_PLUGINS_URL` points them at another list and `--no-plugin-list` leaves them out.
265
+
197
266
  Documentation tools, passed through to the MCP server of [mavonengine.com](https://mavonengine.com) (`https://mavonengine.com/mcp`), so answers come from the current docs:
198
267
 
199
268
  | Tool | |
@@ -213,6 +282,8 @@ The client tools go through the dev bridge of the Vite plugin in `@mavonengine/c
213
282
 
214
283
  `client_trace` patches the tab only while it records and puts everything back afterwards. It times every frame and composer pass, counts the WebGL calls on the context, keeps slow single calls (shader compiles, texture uploads, `readPixels` / `getError` stalls), long animation frames with the scripts that ran, and your own `performance.measure()` spans. GPU time per frame needs `EXT_disjoint_timer_query_webgl2` (Chrome and Edge on most GPUs; the three-perf panel pauses while tracing since it uses the same queries). JS call stacks come from the [JS Self-Profiling API](https://wicg.github.io/js-self-profiling/) in Chromium browsers: the Vite plugin serves dev pages with `Document-Policy: js-profiling` for it, so reload tabs opened before you updated. The trace file is [Chrome Trace Event](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU) JSON with tracks for frames, passes, GPU time, ticks, JS samples and slow WebGL calls: open it in [Perfetto](https://ui.perfetto.dev), `chrome://tracing` or [speedscope](https://www.speedscope.app). Keep the tab in the foreground while it records, browsers throttle hidden tabs.
215
284
 
285
+ The network tools read what both sides record while developing: every message through the game socket, the engine's and your own, with its size, and events such as `connect`, `disconnect`, `dropped` (the transport dropped a message), `sync_enter` / `sync_leave` (entities coming into or leaving a player's sync distance) and, on a client, `state_gap` (state updates that never arrived) and `state_out_of_order`. Each side keeps its last 2000 records, and 8 MB of their payloads. The server records when `NODE_ENV` is not `production` and serves them on `GET /api/game/dev/net/packets`, `/packets/:id` and `/sync`; a tab records while the dev bridge or the editor runs. Nothing is recorded, and the socket is left untouched, in a production build. `network_timeline` orders the two sides by their own clocks, which line up on one machine.
286
+
216
287
  To let `compare_entity` match your own player with the server's, set `game.localPlayer` to the entity the tab controls, or give it `isLocalPlayer = true`: the server keys players by socket id, which the client often doesn't.
217
288
 
218
289
  ## Writing a plugin
@@ -284,6 +355,18 @@ export default defineServerPlugin({
284
355
 
285
356
  A hook that throws is logged and turned off for that plugin, and the game keeps running.
286
357
 
358
+ Guard a plugin's admin routes with `ctx.requireAdmin`, which asks for the server's [admin token](#admin-token), instead of giving the plugin a token of its own:
359
+
360
+ ```ts
361
+ export default defineServerPlugin({
362
+ http(app, ctx) {
363
+ app.post('/api/my-plugin/reset', ctx.requireAdmin, (_req, res) => res.json({ reset: true }))
364
+ },
365
+ })
366
+ ```
367
+
368
+ A plugin that takes the token some other way, such as the password of a login, checks it with `ctx.adminToken.matches(candidate)`, without ever seeing the token.
369
+
287
370
  Projects made with `mavonengine create` are already set up for plugins. To set up an existing project:
288
371
 
289
372
  ```ts
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  DEFAULT_DOCS_URL,
4
4
  version
5
- } from "./chunk-TNN4S5K4.js";
5
+ } from "./chunk-TGGH7KA4.js";
6
6
 
7
7
  // src/mcp/docs.ts
8
8
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // package.json
4
- var version = "0.0.2";
4
+ var version = "0.0.4";
5
5
 
6
6
  // src/mcp/config.ts
7
7
  import { existsSync, readFileSync, writeFileSync } from "fs";
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../core/dist/Plugins/registry.js
4
+ var PLUGIN_REGISTRY_URL = "https://raw.githubusercontent.com/MavonEngine/Core/main/plugins.json";
5
+ var PLUGIN_REGISTRY_ENV = "MAVONENGINE_PLUGINS_URL";
6
+ var SUPPORTED_REGISTRY_VERSION = 1;
7
+ var PluginRegistryError = class extends Error {
8
+ };
9
+ function text(value) {
10
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
11
+ }
12
+ function normalizeRegistry(raw, url) {
13
+ if (!raw || typeof raw !== "object" || Array.isArray(raw) || !Array.isArray(raw.plugins))
14
+ throw new PluginRegistryError(`${url} is not a plugin list: expected { "plugins": [...] }`);
15
+ const { version, plugins } = raw;
16
+ return {
17
+ url,
18
+ version: typeof version === "number" ? version : 1,
19
+ plugins: plugins.flatMap((entry) => {
20
+ const plugin = entry && typeof entry === "object" ? entry : {};
21
+ const pkg = text(plugin.package);
22
+ if (!pkg)
23
+ return [];
24
+ const target = plugin.target === "client" || plugin.target === "server" || plugin.target === "both" ? plugin.target : void 0;
25
+ return [{
26
+ package: pkg,
27
+ name: text(plugin.name) ?? pkg,
28
+ description: text(plugin.description),
29
+ target,
30
+ engine: text(plugin.engine),
31
+ homepage: text(plugin.homepage),
32
+ readme: text(plugin.readme)
33
+ }];
34
+ })
35
+ };
36
+ }
37
+ function notInProject(plugins, inProject) {
38
+ const have = new Set(inProject);
39
+ return plugins.filter((plugin) => !have.has(plugin.package));
40
+ }
41
+ var HTTP_URL_RE = /^https?:\/\//i;
42
+ var MAX_REMOTE_README_CHARS = 2e5;
43
+ async function fetchPluginReadme(plugin, { timeoutMs = 1e4, fetch = globalThis.fetch } = {}) {
44
+ const url = plugin.readme;
45
+ if (!url || !HTTP_URL_RE.test(url))
46
+ throw new PluginRegistryError(`The list has no readme for ${plugin.package}`);
47
+ let res;
48
+ try {
49
+ res = await fetch(url, { cache: "no-store", signal: AbortSignal.timeout(timeoutMs) });
50
+ } catch (err) {
51
+ throw new PluginRegistryError(`Can't reach ${url}: ${err.message}`);
52
+ }
53
+ if (!res.ok)
54
+ throw new PluginRegistryError(res.status === 404 ? `${url} was not found` : `${url} answered ${res.status} ${res.statusText}`);
55
+ const markdown = await res.text();
56
+ return markdown.length > MAX_REMOTE_README_CHARS ? `${markdown.slice(0, MAX_REMOTE_README_CHARS)}
57
+
58
+ \u2026 (cut off)` : markdown;
59
+ }
60
+ async function fetchPluginRegistry({ url = PLUGIN_REGISTRY_URL, timeoutMs = 1e4, fetch = globalThis.fetch } = {}) {
61
+ let res;
62
+ try {
63
+ res = await fetch(url, { cache: "no-store", signal: AbortSignal.timeout(timeoutMs) });
64
+ } catch (err) {
65
+ throw new PluginRegistryError(`Can't reach ${url}: ${err.message}`);
66
+ }
67
+ if (res.status === 404)
68
+ throw new PluginRegistryError(`${url} was not found`);
69
+ if (!res.ok)
70
+ throw new PluginRegistryError(`${url} answered ${res.status} ${res.statusText}`);
71
+ let raw;
72
+ try {
73
+ raw = JSON.parse(await res.text());
74
+ } catch (err) {
75
+ throw new PluginRegistryError(`${url} is not valid JSON: ${err.message}`);
76
+ }
77
+ return normalizeRegistry(raw, url);
78
+ }
79
+
80
+ export {
81
+ PLUGIN_REGISTRY_URL,
82
+ PLUGIN_REGISTRY_ENV,
83
+ SUPPORTED_REGISTRY_VERSION,
84
+ notInProject,
85
+ fetchPluginReadme,
86
+ fetchPluginRegistry
87
+ };
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  DocsClient,
4
4
  registerDocsTools
5
- } from "./chunk-OSCNM66O.js";
5
+ } from "./chunk-KHC2RAKX.js";
6
6
  import {
7
7
  DEFAULT_DOCS_URL
8
- } from "./chunk-TNN4S5K4.js";
8
+ } from "./chunk-TGGH7KA4.js";
9
9
  export {
10
10
  DEFAULT_DOCS_URL,
11
11
  DocsClient,