@irtio/cli 0.8.0 → 0.9.0
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/{dev-VJ2ATJTF.js → dev-7HQMR46Y.js} +95 -9
- package/dist/index.js +1 -1
- package/dist/init.js +3 -3
- package/package.json +7 -7
|
@@ -42,7 +42,7 @@ import "./chunk-UPHQM6NZ.js";
|
|
|
42
42
|
// src/dev.ts
|
|
43
43
|
import { existsSync } from "fs";
|
|
44
44
|
import { watch } from "fs";
|
|
45
|
-
import { mkdir } from "fs/promises";
|
|
45
|
+
import { mkdir, rm, writeFile as writeFile2 } from "fs/promises";
|
|
46
46
|
import { createRequire } from "module";
|
|
47
47
|
import * as path from "path";
|
|
48
48
|
import { fileURLToPath, pathToFileURL as pathToFileURL3 } from "url";
|
|
@@ -168,6 +168,7 @@ import {
|
|
|
168
168
|
BUS_MAILBOX_PREFIX as BUS_MAILBOX_PREFIX2,
|
|
169
169
|
BUS_OUTBOX as BUS_OUTBOX2,
|
|
170
170
|
CLOSE_EGRESS_WALL,
|
|
171
|
+
CLOSE_IDLE_TIMEOUT,
|
|
171
172
|
DEFAULT_ROOM_TYPE as DEFAULT_ROOM_TYPE3,
|
|
172
173
|
ErrorCode as ErrorCode3,
|
|
173
174
|
FrameType as FrameType3,
|
|
@@ -4004,10 +4005,15 @@ var SupervisorImpl = class {
|
|
|
4004
4005
|
for (const session of this.sessions) {
|
|
4005
4006
|
if (!session.open) continue;
|
|
4006
4007
|
if (session.missedPongs >= WS_PING_MISSES) {
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4008
|
+
session.sendError(
|
|
4009
|
+
"E_IDLE_TIMEOUT",
|
|
4010
|
+
formatError("E_IDLE_TIMEOUT", {
|
|
4011
|
+
misses: WS_PING_MISSES,
|
|
4012
|
+
seconds: Math.round((WS_PING_MISSES + 1) * WS_PING_MS / 1e3)
|
|
4013
|
+
}),
|
|
4014
|
+
false
|
|
4015
|
+
);
|
|
4016
|
+
session.closeSocket(CLOSE_IDLE_TIMEOUT, "E_IDLE_TIMEOUT");
|
|
4011
4017
|
continue;
|
|
4012
4018
|
}
|
|
4013
4019
|
session.missedPongs++;
|
|
@@ -6416,6 +6422,53 @@ function send(res, status, type, body) {
|
|
|
6416
6422
|
res.writeHead(status, { "content-type": type, "cache-control": "no-store" });
|
|
6417
6423
|
res.end(body);
|
|
6418
6424
|
}
|
|
6425
|
+
async function tryCommand(cmd, args) {
|
|
6426
|
+
try {
|
|
6427
|
+
const { execFile: execFile2 } = await import("child_process");
|
|
6428
|
+
return await new Promise((resolve2) => {
|
|
6429
|
+
const child = execFile2(
|
|
6430
|
+
cmd,
|
|
6431
|
+
[...args],
|
|
6432
|
+
{ timeout: PORT_LOOKUP_TIMEOUT_MS, windowsHide: true },
|
|
6433
|
+
(err, stdout) => resolve2(err ? void 0 : stdout)
|
|
6434
|
+
);
|
|
6435
|
+
child.on("error", () => resolve2(void 0));
|
|
6436
|
+
});
|
|
6437
|
+
} catch {
|
|
6438
|
+
return void 0;
|
|
6439
|
+
}
|
|
6440
|
+
}
|
|
6441
|
+
var PORT_LOOKUP_TIMEOUT_MS = 1500;
|
|
6442
|
+
async function whoHoldsPort(port) {
|
|
6443
|
+
try {
|
|
6444
|
+
let pid;
|
|
6445
|
+
if (process.platform === "win32") {
|
|
6446
|
+
const out2 = await tryCommand("netstat", ["-ano", "-p", "tcp"]);
|
|
6447
|
+
for (const line of (out2 ?? "").split(/\r?\n/)) {
|
|
6448
|
+
const cols = line.trim().split(/\s+/);
|
|
6449
|
+
if (cols.length < 5 || cols[3] !== "LISTENING") continue;
|
|
6450
|
+
if (!(cols[1] ?? "").endsWith(`:${port}`)) continue;
|
|
6451
|
+
const n2 = Number(cols[cols.length - 1]);
|
|
6452
|
+
if (Number.isInteger(n2) && n2 > 0) pid = n2;
|
|
6453
|
+
break;
|
|
6454
|
+
}
|
|
6455
|
+
if (pid === void 0) return void 0;
|
|
6456
|
+
const tasks = await tryCommand("tasklist", ["/FI", `PID eq ${pid}`, "/NH", "/FO", "CSV"]);
|
|
6457
|
+
const name2 = /^"([^"]+)"/.exec((tasks ?? "").trim())?.[1];
|
|
6458
|
+
return name2 ? `port ${port} is held by ${name2} (pid ${pid})` : `port ${port} is held by pid ${pid}`;
|
|
6459
|
+
}
|
|
6460
|
+
const out = await tryCommand("lsof", ["-i", `:${port}`, "-t", "-sTCP:LISTEN"]);
|
|
6461
|
+
const first = (out ?? "").split(/\s+/).filter(Boolean)[0];
|
|
6462
|
+
const n = Number(first);
|
|
6463
|
+
if (!Number.isInteger(n) || n <= 0) return void 0;
|
|
6464
|
+
pid = n;
|
|
6465
|
+
const ps = await tryCommand("ps", ["-p", String(pid), "-o", "comm="]);
|
|
6466
|
+
const name = (ps ?? "").trim().split(/\s+/)[0];
|
|
6467
|
+
return name ? `port ${port} is held by ${name} (pid ${pid})` : `port ${port} is held by pid ${pid}`;
|
|
6468
|
+
} catch {
|
|
6469
|
+
return void 0;
|
|
6470
|
+
}
|
|
6471
|
+
}
|
|
6419
6472
|
async function startDev(options = {}) {
|
|
6420
6473
|
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
6421
6474
|
const startedAt = Date.now();
|
|
@@ -6425,6 +6478,11 @@ async function startDev(options = {}) {
|
|
|
6425
6478
|
const projectId = config.project ?? "dev";
|
|
6426
6479
|
const outDir = path.join(cwd, ".irtio", "dev");
|
|
6427
6480
|
const port = options.port ?? DEFAULT_PORT;
|
|
6481
|
+
const stateDir = path.join(cwd, ".irtio", "snapshots");
|
|
6482
|
+
if (options.resetState === true) {
|
|
6483
|
+
await rm(stateDir, { recursive: true, force: true });
|
|
6484
|
+
log(pc.dim(`irtio dev: reset ${path.relative(cwd, stateDir)}`));
|
|
6485
|
+
}
|
|
6428
6486
|
await mkdir(outDir, { recursive: true });
|
|
6429
6487
|
await warnIfClientImportsRoom(cwd, entry, config, log);
|
|
6430
6488
|
const bundleOptions = {
|
|
@@ -6536,7 +6594,7 @@ async function startDev(options = {}) {
|
|
|
6536
6594
|
port,
|
|
6537
6595
|
host: "127.0.0.1",
|
|
6538
6596
|
tenantIdleMs: 0,
|
|
6539
|
-
store: new DiskStore(
|
|
6597
|
+
store: new DiskStore(stateDir),
|
|
6540
6598
|
publicUrl: `http://localhost:${port}`,
|
|
6541
6599
|
httpHandler,
|
|
6542
6600
|
...options.profile === true ? { profile: true } : {},
|
|
@@ -6552,8 +6610,10 @@ async function startDev(options = {}) {
|
|
|
6552
6610
|
const code = err?.code;
|
|
6553
6611
|
if (code === "EADDRINUSE" || code === "EACCES") {
|
|
6554
6612
|
const why = code === "EADDRINUSE" ? "something is already listening there" : "the OS refused the port (on Windows it may fall inside a reserved range \u2014 `netsh interface ipv4 show excludedportrange protocol=tcp`)";
|
|
6613
|
+
const holder = code === "EADDRINUSE" ? await whoHoldsPort(port) : void 0;
|
|
6555
6614
|
throw new Error(
|
|
6556
|
-
`irtio dev: cannot bind port ${port} \u2014 ${why}
|
|
6615
|
+
`irtio dev: cannot bind port ${port} \u2014 ${why}.` + (holder ? `
|
|
6616
|
+
${holder}` : "") + `
|
|
6557
6617
|
run: irtio dev --port <other port>
|
|
6558
6618
|
and point your page at it with window.IRT_URL = 'ws://localhost:<other port>'`
|
|
6559
6619
|
);
|
|
@@ -6605,6 +6665,24 @@ async function startDev(options = {}) {
|
|
|
6605
6665
|
}
|
|
6606
6666
|
}
|
|
6607
6667
|
const boundPort = supervisor.port;
|
|
6668
|
+
const devFile = path.join(cwd, ".irtio", "dev.json");
|
|
6669
|
+
await writeFile2(
|
|
6670
|
+
devFile,
|
|
6671
|
+
`${JSON.stringify(
|
|
6672
|
+
{
|
|
6673
|
+
port: boundPort,
|
|
6674
|
+
url: `ws://localhost:${boundPort}`,
|
|
6675
|
+
page: `http://localhost:${boundPort}/__irt/`,
|
|
6676
|
+
projectId,
|
|
6677
|
+
pid: process.pid,
|
|
6678
|
+
startedAt: new Date(startedAt).toISOString()
|
|
6679
|
+
},
|
|
6680
|
+
null,
|
|
6681
|
+
2
|
|
6682
|
+
)}
|
|
6683
|
+
`
|
|
6684
|
+
).catch(() => {
|
|
6685
|
+
});
|
|
6608
6686
|
return {
|
|
6609
6687
|
url: `ws://localhost:${boundPort}`,
|
|
6610
6688
|
page: `http://localhost:${boundPort}/__irt/`,
|
|
@@ -6615,6 +6693,8 @@ async function startDev(options = {}) {
|
|
|
6615
6693
|
if (stopped) return;
|
|
6616
6694
|
stopped = true;
|
|
6617
6695
|
watcher?.close();
|
|
6696
|
+
await rm(devFile, { force: true }).catch(() => {
|
|
6697
|
+
});
|
|
6618
6698
|
await supervisor.flushAll().catch(() => {
|
|
6619
6699
|
});
|
|
6620
6700
|
await supervisor.close();
|
|
@@ -6630,6 +6710,7 @@ options:
|
|
|
6630
6710
|
--room <file> the room entry (default: irtio/room.ts, irtio/room.js, room.ts)
|
|
6631
6711
|
--port <n> port to listen on (default 7070; 0 picks a free one)
|
|
6632
6712
|
--no-watch do not rebuild on change
|
|
6713
|
+
--reset-state delete .irtio/snapshots first, so every room starts fresh
|
|
6633
6714
|
-c, --config <f> the project file to read (default irtio.json)
|
|
6634
6715
|
--profile print a bandwidth breakdown per room once a second
|
|
6635
6716
|
--profile-top <n> rows per table (default 12; implies --profile)
|
|
@@ -6637,7 +6718,7 @@ options:
|
|
|
6637
6718
|
`;
|
|
6638
6719
|
function parseDevArgs(args) {
|
|
6639
6720
|
if (helpRequested(args)) throw helpFor(USAGE);
|
|
6640
|
-
const parsed = { watch: true, profile: false };
|
|
6721
|
+
const parsed = { watch: true, resetState: false, profile: false };
|
|
6641
6722
|
for (let i = 0; i < args.length; i++) {
|
|
6642
6723
|
const arg = args[i];
|
|
6643
6724
|
const eq = arg.indexOf("=");
|
|
@@ -6663,6 +6744,9 @@ function parseDevArgs(args) {
|
|
|
6663
6744
|
case "--no-watch":
|
|
6664
6745
|
parsed.watch = false;
|
|
6665
6746
|
break;
|
|
6747
|
+
case "--reset-state":
|
|
6748
|
+
parsed.resetState = true;
|
|
6749
|
+
break;
|
|
6666
6750
|
case "--profile":
|
|
6667
6751
|
parsed.profile = true;
|
|
6668
6752
|
break;
|
|
@@ -6692,6 +6776,7 @@ async function dev(args) {
|
|
|
6692
6776
|
parsed = parseDevArgs(args);
|
|
6693
6777
|
server = await startDev({
|
|
6694
6778
|
watch: parsed.watch,
|
|
6779
|
+
...parsed.resetState ? { resetState: true } : {},
|
|
6695
6780
|
...parsed.room !== void 0 ? { room: parsed.room } : {},
|
|
6696
6781
|
...parsed.port !== void 0 ? { port: parsed.port } : {},
|
|
6697
6782
|
...parsed.config !== void 0 ? { config: parsed.config } : {},
|
|
@@ -6763,5 +6848,6 @@ export {
|
|
|
6763
6848
|
USAGE,
|
|
6764
6849
|
dev,
|
|
6765
6850
|
parseDevArgs,
|
|
6766
|
-
startDev
|
|
6851
|
+
startDev,
|
|
6852
|
+
whoHoldsPort
|
|
6767
6853
|
};
|
package/dist/index.js
CHANGED
package/dist/init.js
CHANGED
|
@@ -15,7 +15,7 @@ import { createInterface } from "readline/promises";
|
|
|
15
15
|
import pc from "picocolors";
|
|
16
16
|
|
|
17
17
|
// src/agent-guide.generated.ts
|
|
18
|
-
var AGENT_GUIDE = "# irt.io integration guide\n\nThe whole product in one page, written for a context window. Everything below is enough to take a\nsingle-player web game to a deployed, verified multiplayer room without opening a dashboard.\n\n## The mental model\n\nA **room** is a server-side TypeScript file. It owns a small typed **schema**: the state two\nplayers have to agree on, and nothing else about your game. Each client owns its own instances and\nwrites them like local objects. The room's `validate` decides what a write may be. Everything else\nin your game stays where it was. You verify a room the way you verify code. Bots drive real\nsockets, a **scenario** asserts against the server's own recorded timeline, and the same seed gives\nthe same verdict.\n\n## The skeleton\n\nThe smallest project that deploys. Four files, and `irtio init` writes all of them.\n\n```ts\n// irtio/schema.ts\nimport { defineSchema, entity, f32, str, u8 } from '@irtio/schema';\n\nimport { rpc } from './rpc.js';\n\nexport const schema = defineSchema(\n {\n // One instance per connected client, owned by that client. Owned means writable on that\n // client and read-only everywhere else.\n players: entity({ x: f32, y: f32, name: str(24), color: u8 }),\n },\n {\n project: 'p_c0ffee1234abcd56', // written by `irtio init`; public, domain-locked, not a secret\n roles: ['player'] as const,\n rpc,\n },\n);\n```\n\n```ts\n// irtio/rpc.ts\nimport { server, u8 } from '@irtio/schema';\n\n// Typed calls in both directions. `server(...)` is client to server. Names and signatures are\n// part of the schema hash, so skew is caught as a version error instead of at runtime.\nexport const rpc = {\n cheer: server({ params: { volume: u8 } }),\n};\n```\n\n```ts\n// irtio/room.ts\nimport { defineRoom } from '@irtio/server';\n\nimport { schema } from './schema.js';\n\nconst WIDTH = 800;\nconst HEIGHT = 500;\n\nexport default defineRoom(schema, {\n mode: 'tick', // 'event' if nothing moves without a player doing something\n tickRate: 20,\n\n onJoin(state, ctx) {\n if (ctx.reconnecting) return;\n state.players.add(\n ctx.clientId,\n { x: 0, y: 0, name: ctx.name || 'anon', color: (ctx.tick * 37) % 256 },\n { owner: ctx.clientId }, // the line the whole client-side write model rests on\n );\n },\n\n onLeave(state, ctx) {\n state.players.remove(ctx.clientId);\n },\n\n // Owner writes pass through here before they are accepted. Return `next` to accept, `prev` to\n // reject, or a clamped object. This is where cheating stops. Delete it and anything that fits\n // the declared types is accepted.\n validate: {\n players(prev, next) {\n // Refuse what could never be a real value, lock the fields the room assigned at join,\n // and clamp the rest into the world.\n if (!Number.isFinite(next.x) || !Number.isFinite(next.y)) return prev;\n if (next.name !== prev.name || next.color !== prev.color) return prev;\n return {\n ...next,\n x: Math.min(WIDTH, Math.max(0, next.x)),\n y: Math.min(HEIGHT, Math.max(0, next.y)),\n };\n },\n },\n\n rpc: {\n cheer(state, params, ctx) {\n if (params.volume > 10) throw new Error('too loud'); // the normal way a room says no\n state.players.get(ctx.clientId)!.color = params.volume;\n },\n },\n\n tick() {}, // tick mode needs one, even empty\n});\n```\n\n```ts\n// your game, three lines added\nimport { joinRoom } from '@irtio/client';\n\nimport { schema } from './irtio/schema.js';\n\nconst room = await joinRoom(schema, { name: 'you' }); // reads ?room=, or creates one\nconst me = room.state.players[room.me]; // yours: write it like a local object\nif (me) me.x = 10;\n\nfor (const [, p] of room.render.players) draw(p); // `render` interpolates everyone else\n```\n\n`room.state` is the authoritative read path for game logic and tests. `room.render` has the same\nshapes and reads non-owned entities a beat behind arrival, interpolated, which is what you draw\nfrom. Writes are batched once per animation frame, and `room.flush()` forces one out now.\n`room.leave()` closes the session. A page rarely needs it; a test with several sessions always\ndoes. `joinRoom`'s options are `{ name, role, url, key, token, physics, transport }`; see\nthe client reference.\n\n`@irtio/client` is a module, so the page that imports it needs whatever bundler your game already\nuses. There is no `irtio build`.\n\n### Write the validate rule your game actually has\n\n`validate` is the one part of the skeleton you cannot copy without thinking. The wrong rule here\nis worse than no rule, because it refuses honest play silently. The server sends a correction, and\nthe thing does not go where the player put it.\n\nA **speed limit** (`if (Math.hypot(next.x - prev.x, next.y - prev.y) > MAX_STEP) return prev;`) is\nright when the thing being moved has a movement speed the game defines: a character, a vehicle, a\ndragged token. It is wrong for a **mouse cursor**. A cursor legitimately teleports when the pointer moves fast,\nor when it leaves the window and comes back. A player gains nothing by putting their cursor\nsomewhere.\n\nAsk what a cheat would actually win. Position, for a cursor, wins nothing, so clamp it and stop.\nPosition for a character wins map knowledge and reach, so limit the step. Score, turn order, hit\ndetection and inventory win the game outright, so they should not be client-owned at all: make\nthem `serverOwned` and change them through an RPC.\n\n## The four commands\n\n```bash\nnpx irtio init # scaffold the four files above plus irtio.json\nnpx irtio dev # bundle irtio/room.ts and run it locally\nnpx irtio simulate --scenario irtio/scenario.ts # real bots, your assertions, exit code\nnpx irtio deploy # create the project if new, classify, upload\n```\n\n`simulate` drives real clients at a room that is **already running**, so `dev` stays up in another\nterminal while you run it. The four are a loop, not a pipeline.\n\nEvery subcommand takes `--help`, and the per-command help is the complete list; the top-level\nsummary is a map.\n\n`dev` is the whole server, locally: it bundles the room file and runs it the way the hosted\nserver does, so it is what you develop and test against. It does not build or serve your\n**client**. That stays your own bundler's job, the same one your game already uses.\n\n`deploy` is the hosted path and needs `irtio login`. There is no offline `deploy`. Without an\naccount, `dev` plus your own bundler is as far as you get, and that runs everything below.\n\n`deploy` creates the project on first use and classifies the schema change against the last\ndeployment. It refuses breaking changes unless a migration covers them, then prints where the room\nis playable. `simulate` exits non-zero on a violation, so both belong in CI.\n\nWhen the question is \"why is this room using so much bandwidth\", both commands take `--profile`.\n`dev --profile` prints a per-room breakdown by collection and field once a second.\n`simulate --profile` prints one for the whole run as the bots saw it. The answer is usually a\nsingle wide field, or `overhead` telling you the updates are too small and too frequent. See\n[the profiler](/docs/guides/profiler).\n\n**Restart `dev` after editing the room.** With `--no-watch`, a `dev` you forgot to kill keeps\nserving the old bundle. `simulate` connects to it and reports a clean run against code you have\nreplaced. The report header names the bundle hash and how long the server has been up. Compare\nthose two lines between runs: an edit that changed nothing in the header changed nothing in the\ncode under test.\n\nAn agent driving this through MCP calls `project_create`, `origin_add`, `deploy`, `scenario_run`,\nthen `logs` or `metrics`. Sign in once with `npx irtio login`; the MCP config snippet is a command\nand carries no secret.\n\n## Verify a room with a scenario\n\nA scenario is a TypeScript module next to the room file. Assertions run against the recorded\nauthoritative timeline, one frame per server tick. A scenario can prove something about a race no\nclient could observe, and it replays: same seed, same verdict.\n\n```ts\n// irtio/scenario.ts\nimport { defineScenario } from '@irtio/bots';\n\nimport type { schema } from './schema.js';\n\nexport default defineScenario<typeof schema>({\n bots: 2,\n seconds: 4,\n seed: 41,\n\n // Adversarial in two words: bot 0 lags, and every bot tries illegal writes. `validate` above\n // is what has to refuse them.\n conditions: (index) => (index === 0 ? { rttMs: 200 } : undefined),\n cheat: true,\n\n script: async (bot) => {\n await bot.wait(200);\n await bot.room.call.cheer({ volume: 3 });\n await bot.wait(500);\n },\n\n assert: (timeline) => {\n timeline.check('nobody left the arena', () => {\n for (const tick of timeline.ticks) {\n for (const id of timeline.at(tick).players!.ids()) {\n const p = timeline.at(tick).players!.get(id)!;\n if (p.x < 0 || p.x > 800) throw new Error(`player ${id} at x=${p.x} on tick ${tick}`);\n }\n }\n });\n },\n});\n```\n\n`cheat: true` makes the bots write play-illegal values. If the run reports\n`HOLE bot <n> cheated and drew 0 corrections`, your `validate` accepted them, and that is the\nfinding. `truth: true` additionally saves the room at the end and diffs it against what each client\nactually received.\n\nTwo things to know before you read a result.\n\n**A cheat run that works fails the run by default.** Every refused write is a correction, and the\nbuilt-in `correction-storm` invariant fails above 5 corrections per second per bot. A deliberate\ncheater passes that immediately. Raise it for the run with `--corrections-max 25`, and read the\n`HOLE` line rather than the exit code as the verdict on `validate`.\n\n**The built-in invariants alone prove nothing about `validate`.** They are protocol invariants:\nframes decoded, nobody saw what they should not, bandwidth inside budget, no tick overruns. A room\nthat accepts every illegal write there is passes all of them. Only `cheat` and your own scenario\nassertions test server authority, so a run with neither says nothing about whether your game can be\ncheated.\n\n## One retrofit diff\n\nThe canvas case, the shortest one. The cursors canvas example is this diff as running code; see\n[the retrofit guide](/docs/guides/retrofit).\n\n```diff\n+import { joinRoom } from '@irtio/client';\n+\n+import { schema } from './irtio/schema.js';\n+\n+const room = await joinRoom(schema, { name: 'you' });\n-const player = { x: canvas.width / 2, y: canvas.height / 2, name: 'you', color: 200 };\n-\n canvas.addEventListener('pointermove', (event) => {\n const bounds = canvas.getBoundingClientRect();\n+ const player = room.state.players[room.me];\n+ if (!player) return;\n player.x = event.clientX - bounds.left;\n player.y = event.clientY - bounds.top;\n });\n\n function frame() {\n- const entries = [['me', player]];\n+ const entries = [...room.render.players];\n for (const [, p] of entries) draw(p);\n }\n```\n\nThe whole shape is an import, a join, an entity lookup instead of a local object, and one changed\niteration. The camera, particles, input handling, HUD, draw code and level geometry do not\nmove. If your retrofit touches those, it is doing more than a retrofit. Three.js and Phaser follow\nthe same four steps against their own loops; see the retrofit guide.\n\nYour entity does not exist for the first frame or two after the page loads, which is what\n`if (!player) return;` is for. Writing an instance you do not own is a compile error and a\nwarn-once no-op at runtime.\n\n## When it goes wrong\n\nThe codes you will actually meet, with the fix. The full catalogue is at `/docs/reference/errors`.\n\n| Code | Fix |\n|---|---|\n| `E_AUTH` | The project key was rejected. Check `project` in `irtio/schema.ts` matches the project you are deploying to. On localhost with no `irtio init` the client uses the key `dev`, which only `irtio dev` accepts |\n| `E_SCHEMA_MISMATCH` | The client was built with a different schema than the room is running. The hash covers every field, type, order, role and RPC signature. Rebuild the client, or deploy the schema the client has |\n| `E_ORIGIN` | The page's origin is not in the project's origin list. Register it. `localhost` is always allowed, so this only bites on a deployed page |\n| `E_ROOM_FULL` | The room is at `maxClients`, 64 by default. Raise it in `irtio/room.ts`, or join with no `?room=` to get a new room |\n| `E_ROOM_NOT_FOUND` | No such room, or an id that cannot be one. Do not construct ids yourself: let `joinRoom` create one and read `room.link` |\n| `E_WRITE_REJECTED` | Your `validate` refused the write, or the value did not fit its declared type. If this is a legitimate move, `validate` is too strict or the field too narrow |\n| `E_NOT_OWNER` | Something wrote an instance it does not own. `await room.requestOwnership(entity, id)` first, and re-read the instance after a grant |\n| `E_RPC_BAD_PARAMS` | Parameters did not match the declared shape. Types normally prevent this, so it means client and server were built from different `rpc.ts` files |\n| `E_RATE_LIMITED` | Read the message. `rate limited` is too many frames: batch instead of calling `room.flush()` in a loop. `too many connections from this address` is the per-IP cap (`connectionsPerIpPerMin`, default 120), which a load run from one machine has to raise |\n| `E_STARTING` | Not an error. The server was asleep and is waking. `room.status === 'starting'`; show a spinner and keep waiting |\n| `E_SLOW_CONSUMER` | The client was not draining its stream and was dropped; it reconnects itself. If it recurs, the room produces more per tick than the connection carries: lower `tickRate`, narrow field types, or use role visibility |\n| `E_CONNECT_FAILED` | Not a protocol code. The socket failed before the join. Read the URL in the message: `ws://localhost:7070` means `irtio dev` is not running |\n| `deploy refused: N breaking changes` | Snapshots cannot be read under the new schema. `irtio migrate create <name>`, write the transform, then `irtio deploy --allow-breaking`. Additive changes (a new field with `.default(...)` or `.opt`) need none of this |\n| `not logged in` | `deploy`, `logs`, `rooms`, `whoami` and `migrate create` need credentials. Run `irtio login` |\n| `HOLE bot <n> cheated and drew 0 corrections` | Not a failure, and the most important warning here. Illegal writes were accepted. Add rules to `validate` |\n\n## Past the skeleton\n\nThe skeleton above is the smallest thing that deploys. Each of these is one docs page\nand none of them changes the model.\n\n- Server-authoritative physics with client prediction: `irtio init --physics`, a shared\n `irtio/world.ts` both sides import. Three engines: Rapier in 3D (`/docs/physics/overview`),\n Rapier in 2D (`/docs/physics/rapier2d`), or matter.js in 2D (`/docs/physics/matter2d`,\n `tickRate: 60`). Recommend Rapier for a new game and reach for matter2d only for a room already\n on it. All three predict, and `room.prediction` reads the same either way\n (`/docs/physics/prediction`).\n- Scripted NPCs that are ordinary client sessions at the protocol level: `room.spawnNPC(...)`\n (`/docs/guides/npcs`).\n- Player storage that outlives a room (`room.kv`), room saves and restores, hibernation, and the\n `retention` option that sets how long a room's state survives\n (`/docs/persistence/player-storage`, `/docs/persistence/saves`, `/docs/concepts/hibernation`).\n- Signed player tokens, roles, and per-role visibility (`/docs/concepts/auth`,\n `/docs/concepts/visibility`).\n- A player identity that survives reconnects, hibernation and a closed tab\n (`/docs/concepts/identity`).\n- Ranked boards per project, written only by your room code\n (`/docs/persistence/leaderboards`).\n- Two strangers matched into a room without either knowing a room code\n (`/docs/concepts/quick-match`).\n- Voice chat in a room, on its own meter (`/docs/concepts/voice`).\n- One of your rooms sending a message to another (`/docs/concepts/room-bus`).\n- A project split across more than one server when a single one is full\n (`/docs/deploy/sharding`).\n\n**Starting from an engine.** Build against the engine's own loop from the first line: React and\nreact-three-fiber (`/docs/integrations/react`), Three.js (`/docs/integrations/threejs`), Phaser\n(`/docs/integrations/phaser`), PixiJS (`/docs/integrations/pixijs`), Babylon.js\n(`/docs/integrations/babylonjs`).\n\n**Moving a game that already exists.** Single-player, which is the diff above at full length:\ncanvas (`/docs/guides/retrofit`), Three.js (`/docs/guides/retrofit-threejs`), Phaser\n(`/docs/guides/retrofit-phaser`). Already multiplayer on something else: Colyseus\n(`/docs/migrate/colyseus`), Playroom (`/docs/migrate/playroom`), Socket.IO\n(`/docs/migrate/socket-io`).\n\nEvery term these pages use is defined once in the glossary (`/docs/reference/glossary`).\n\n## Rules of thumb\n\n- Sync the smallest set of facts two players must agree on. Everything else stays local.\n- A player reporting a fact about themselves is an owned write. Anything a player could gain by\n lying about (scores, deals, turn order, hit detection) is `serverOwned` plus an RPC.\n- Types are budgets, not hints. `f32`, `u8`, `str(24)` are what make an update a handful of bytes.\n- Never import `room.ts` from client code. Shared geometry and constants go in their own module\n that both sides import.\n- Do not paste a credential into a config file. `irtio login` writes one; MCP and the CLI find it.\n- Believe a room when a scenario with an adversarial pass says so, not when it looks right in two\n tabs.\n";
|
|
18
|
+
var AGENT_GUIDE = "# irt.io integration guide\n\nThe whole product in one page, written for a context window. Everything below is enough to take a\nsingle-player web game to a deployed, verified multiplayer room without opening a dashboard.\n\n## The mental model\n\nA **room** is a server-side TypeScript file. It owns a small typed **schema**: the state two\nplayers have to agree on, and nothing else about your game. Each client owns its own instances and\nwrites them like local objects. The room's `validate` decides what a write may be. Everything else\nin your game stays where it was. You verify a room the way you verify code. Bots drive real\nsockets, a **scenario** asserts against the server's own recorded timeline, and the same seed gives\nthe same verdict.\n\n## The skeleton\n\nThe smallest project that deploys. Four files, and `irtio init` writes all of them.\n\n```ts\n// irtio/schema.ts\nimport { defineSchema, entity, f32, str, u8 } from '@irtio/schema';\n\nimport { rpc } from './rpc.js';\n\nexport const schema = defineSchema(\n {\n // One instance per connected client, owned by that client. Owned means writable on that\n // client and read-only everywhere else.\n players: entity({ x: f32, y: f32, name: str(24), color: u8 }),\n },\n {\n project: 'p_c0ffee1234abcd56', // written by `irtio init`; public, domain-locked, not a secret\n roles: ['player'] as const,\n rpc,\n },\n);\n```\n\n```ts\n// irtio/rpc.ts\nimport { server, u8 } from '@irtio/schema';\n\n// Typed calls in both directions. `server(...)` is client to server. Names and signatures are\n// part of the schema hash, so skew is caught as a version error instead of at runtime.\nexport const rpc = {\n cheer: server({ params: { volume: u8 } }),\n};\n```\n\n```ts\n// irtio/room.ts\nimport { defineRoom } from '@irtio/server';\n\nimport { schema } from './schema.js';\n\nconst WIDTH = 800;\nconst HEIGHT = 500;\n\nexport default defineRoom(schema, {\n mode: 'tick', // 'event' if nothing moves without a player doing something\n tickRate: 20,\n\n onJoin(state, ctx) {\n if (ctx.reconnecting) return;\n state.players.add(\n ctx.clientId,\n { x: 0, y: 0, name: ctx.name || 'anon', color: (ctx.tick * 37) % 256 },\n { owner: ctx.clientId }, // the line the whole client-side write model rests on\n );\n },\n\n onLeave(state, ctx) {\n state.players.remove(ctx.clientId);\n },\n\n // Owner writes pass through here before they are accepted. Return `next` to accept, `prev` to\n // reject, or a clamped object. This is where cheating stops. Delete it and anything that fits\n // the declared types is accepted.\n validate: {\n players(prev, next) {\n // Refuse what could never be a real value, lock the fields the room assigned at join,\n // and clamp the rest into the world.\n if (!Number.isFinite(next.x) || !Number.isFinite(next.y)) return prev;\n if (next.name !== prev.name || next.color !== prev.color) return prev;\n return {\n ...next,\n x: Math.min(WIDTH, Math.max(0, next.x)),\n y: Math.min(HEIGHT, Math.max(0, next.y)),\n };\n },\n },\n\n rpc: {\n cheer(state, params, ctx) {\n if (params.volume > 10) throw new Error('too loud'); // the normal way a room says no\n state.players.get(ctx.clientId)!.color = params.volume;\n },\n },\n\n tick() {}, // tick mode needs one, even empty\n});\n```\n\n```ts\n// your game, three lines added\nimport { joinRoom } from '@irtio/client';\n\nimport { schema } from './irtio/schema.js';\n\nconst room = await joinRoom(schema, { name: 'you' }); // reads ?room=, or creates one\nconst me = room.state.players[room.me]; // yours: write it like a local object\nif (me) me.x = 10;\n\nfor (const [, p] of room.render.players) draw(p); // `render` interpolates everyone else\n```\n\n`room.state` is the authoritative read path for game logic and tests. `room.render` has the same\nshapes and reads non-owned entities a beat behind arrival, interpolated, which is what you draw\nfrom. Writes are batched once per animation frame, and `room.flush()` forces one out now.\n`room.leave()` closes the session. A page rarely needs it; a test with several sessions always\ndoes. `joinRoom`'s options are `{ name, role, url, key, token, physics, transport }`; see\nthe client reference.\n\n`@irtio/client` is a module, so the page that imports it needs whatever bundler your game already\nuses. There is no `irtio build`.\n\n### Write the validate rule your game actually has\n\n`validate` is the one part of the skeleton you cannot copy without thinking. The wrong rule here\nis worse than no rule, because it refuses honest play silently. The server sends a correction, and\nthe thing does not go where the player put it.\n\nA **speed limit** (`if (Math.hypot(next.x - prev.x, next.y - prev.y) > MAX_STEP) return prev;`) is\nright when the thing being moved has a movement speed the game defines: a character, a vehicle, a\ndragged token. It is wrong for a **mouse cursor**. A cursor legitimately teleports when the pointer moves fast,\nor when it leaves the window and comes back. A player gains nothing by putting their cursor\nsomewhere.\n\nAsk what a cheat would actually win. Position, for a cursor, wins nothing, so clamp it and stop.\nPosition for a character wins map knowledge and reach, so limit the step. Score, turn order, hit\ndetection and inventory win the game outright, so they should not be client-owned at all: make\nthem `serverOwned` and change them through an RPC.\n\n## The four commands\n\n```bash\nnpx --package @irtio/cli irtio init # scaffold the four files above plus irtio.json\nnpx --package @irtio/cli irtio dev # bundle irtio/room.ts and run it locally\nnpx --package @irtio/cli irtio simulate --scenario irtio/scenario.ts # real bots, your assertions, exit code\nnpx --package @irtio/cli irtio deploy # create the project if new, classify, upload\n```\n\n`simulate` drives real clients at a room that is **already running**, so `dev` stays up in another\nterminal while you run it. The four are a loop, not a pipeline.\n\nEvery subcommand takes `--help`, and the per-command help is the complete list; the top-level\nsummary is a map.\n\n`dev` is the whole server, locally: it bundles the room file and runs it the way the hosted\nserver does, so it is what you develop and test against. It does not build or serve your\n**client**. That stays your own bundler's job, the same one your game already uses.\n\n`deploy` is the hosted path and needs `irtio login`. There is no offline `deploy`. Without an\naccount, `dev` plus your own bundler is as far as you get, and that runs everything below.\n\n`deploy` creates the project on first use and classifies the schema change against the last\ndeployment. It refuses breaking changes unless a migration covers them, then prints where the room\nis playable. `simulate` exits non-zero on a violation, so both belong in CI.\n\nWhen the question is \"why is this room using so much bandwidth\", both commands take `--profile`.\n`dev --profile` prints a per-room breakdown by collection and field once a second.\n`simulate --profile` prints one for the whole run as the bots saw it. The answer is usually a\nsingle wide field, or `overhead` telling you the updates are too small and too frequent. See\n[the profiler](/docs/guides/profiler).\n\n**Restart `dev` after editing the room.** With `--no-watch`, a `dev` you forgot to kill keeps\nserving the old bundle. `simulate` connects to it and reports a clean run against code you have\nreplaced. The report header names the bundle hash and how long the server has been up. Compare\nthose two lines between runs: an edit that changed nothing in the header changed nothing in the\ncode under test.\n\nAn agent driving this through MCP calls `project_create`, `origin_add`, `deploy`, `scenario_run`,\nthen `logs` or `metrics`. Sign in once with `npx --package @irtio/cli irtio login`; the MCP config snippet is a command\nand carries no secret.\n\n## Verify a room with a scenario\n\nA scenario is a TypeScript module next to the room file. Assertions run against the recorded\nauthoritative timeline, one frame per server tick. A scenario can prove something about a race no\nclient could observe, and it replays: same seed, same verdict.\n\n```ts\n// irtio/scenario.ts\nimport { defineScenario } from '@irtio/bots';\n\nimport type { schema } from './schema.js';\n\nexport default defineScenario<typeof schema>({\n bots: 2,\n seconds: 4,\n seed: 41,\n\n // Adversarial in two words: bot 0 lags, and every bot tries illegal writes. `validate` above\n // is what has to refuse them.\n conditions: (index) => (index === 0 ? { rttMs: 200 } : undefined),\n cheat: true,\n\n script: async (bot) => {\n await bot.wait(200);\n await bot.room.call.cheer({ volume: 3 });\n await bot.wait(500);\n },\n\n assert: (timeline) => {\n timeline.check('nobody left the arena', () => {\n for (const tick of timeline.ticks) {\n for (const id of timeline.at(tick).players!.ids()) {\n const p = timeline.at(tick).players!.get(id)!;\n if (p.x < 0 || p.x > 800) throw new Error(`player ${id} at x=${p.x} on tick ${tick}`);\n }\n }\n });\n },\n});\n```\n\n`cheat: true` makes the bots write play-illegal values. If the run reports\n`HOLE bot <n> cheated and drew 0 corrections`, your `validate` accepted them, and that is the\nfinding. `truth: true` additionally saves the room at the end and diffs it against what each client\nactually received.\n\nTwo things to know before you read a result.\n\n**A cheat run that works fails the run by default.** Every refused write is a correction, and the\nbuilt-in `correction-storm` invariant fails above 5 corrections per second per bot. A deliberate\ncheater passes that immediately. Raise it for the run with `--corrections-max 25`, and read the\n`HOLE` line rather than the exit code as the verdict on `validate`.\n\n**The built-in invariants alone prove nothing about `validate`.** They are protocol invariants:\nframes decoded, nobody saw what they should not, bandwidth inside budget, no tick overruns. A room\nthat accepts every illegal write there is passes all of them. Only `cheat` and your own scenario\nassertions test server authority, so a run with neither says nothing about whether your game can be\ncheated.\n\n## One retrofit diff\n\nThe canvas case, the shortest one. The cursors canvas example is this diff as running code; see\n[the retrofit guide](/docs/guides/retrofit).\n\n```diff\n+import { joinRoom } from '@irtio/client';\n+\n+import { schema } from './irtio/schema.js';\n+\n+const room = await joinRoom(schema, { name: 'you' });\n-const player = { x: canvas.width / 2, y: canvas.height / 2, name: 'you', color: 200 };\n-\n canvas.addEventListener('pointermove', (event) => {\n const bounds = canvas.getBoundingClientRect();\n+ const player = room.state.players[room.me];\n+ if (!player) return;\n player.x = event.clientX - bounds.left;\n player.y = event.clientY - bounds.top;\n });\n\n function frame() {\n- const entries = [['me', player]];\n+ const entries = [...room.render.players];\n for (const [, p] of entries) draw(p);\n }\n```\n\nThe whole shape is an import, a join, an entity lookup instead of a local object, and one changed\niteration. The camera, particles, input handling, HUD, draw code and level geometry do not\nmove. If your retrofit touches those, it is doing more than a retrofit. Three.js and Phaser follow\nthe same four steps against their own loops; see the retrofit guide.\n\nYour entity does not exist for the first frame or two after the page loads, which is what\n`if (!player) return;` is for. Writing an instance you do not own is a compile error and a\nwarn-once no-op at runtime.\n\n## When it goes wrong\n\nThe codes you will actually meet, with the fix. The full catalogue is at `/docs/reference/errors`.\n\n| Code | Fix |\n|---|---|\n| `E_AUTH` | The project key was rejected. Check `project` in `irtio/schema.ts` matches the project you are deploying to. On localhost with no `irtio init` the client uses the key `dev`, which only `irtio dev` accepts |\n| `E_SCHEMA_MISMATCH` | The client was built with a different schema than the room is running. The hash covers every field, type, order, role and RPC signature. Rebuild the client, or deploy the schema the client has |\n| `E_ORIGIN` | The page's origin is not in the project's origin list. Register it. `localhost` is always allowed, so this only bites on a deployed page |\n| `E_ROOM_FULL` | The room is at `maxClients`, 64 by default. Raise it in `irtio/room.ts`, or join with no `?room=` to get a new room |\n| `E_ROOM_NOT_FOUND` | No such room, or an id that cannot be one. Do not construct ids yourself: let `joinRoom` create one and read `room.link` |\n| `E_WRITE_REJECTED` | Your `validate` refused the write, or the value did not fit its declared type. If this is a legitimate move, `validate` is too strict or the field too narrow |\n| `E_NOT_OWNER` | Something wrote an instance it does not own. `await room.requestOwnership(entity, id)` first, and re-read the instance after a grant |\n| `E_RPC_BAD_PARAMS` | Parameters did not match the declared shape. Types normally prevent this, so it means client and server were built from different `rpc.ts` files |\n| `E_RATE_LIMITED` | Read the message. `rate limited` is too many frames: batch instead of calling `room.flush()` in a loop. `too many connections from this address` is the per-IP cap (`connectionsPerIpPerMin`, default 120), which a load run from one machine has to raise |\n| `E_STARTING` | Not an error. The server was asleep and is waking. `room.status === 'starting'`; show a spinner and keep waiting |\n| `E_SLOW_CONSUMER` | The client was not draining its stream and was dropped; it reconnects itself. If it recurs, the room produces more per tick than the connection carries: lower `tickRate`, narrow field types, or use role visibility |\n| `E_CONNECT_FAILED` | Not a protocol code. The socket failed before the join. Read the URL in the message: `ws://localhost:7070` means `irtio dev` is not running |\n| `deploy refused: N breaking changes` | Snapshots cannot be read under the new schema. `irtio migrate create <name>`, write the transform, then `irtio deploy --allow-breaking`. Additive changes (a new field with `.default(...)` or `.opt`) need none of this |\n| `not logged in` | `deploy`, `logs`, `rooms`, `whoami` and `migrate create` need credentials. Run `irtio login` |\n| `HOLE bot <n> cheated and drew 0 corrections` | Not a failure, and the most important warning here. Illegal writes were accepted. Add rules to `validate` |\n\n## Past the skeleton\n\nThe skeleton above is the smallest thing that deploys. Each of these is one docs page\nand none of them changes the model.\n\n- Server-authoritative physics with client prediction: `irtio init --physics`, a shared\n `irtio/world.ts` both sides import. Three engines: Rapier in 3D (`/docs/physics/overview`),\n Rapier in 2D (`/docs/physics/rapier2d`), or matter.js in 2D (`/docs/physics/matter2d`,\n `tickRate: 60`). Recommend Rapier for a new game and reach for matter2d only for a room already\n on it. All three predict, and `room.prediction` reads the same either way\n (`/docs/physics/prediction`).\n- Scripted NPCs that are ordinary client sessions at the protocol level: `room.spawnNPC(...)`\n (`/docs/guides/npcs`).\n- Player storage that outlives a room (`room.kv`), room saves and restores, hibernation, and the\n `retention` option that sets how long a room's state survives\n (`/docs/persistence/player-storage`, `/docs/persistence/saves`, `/docs/concepts/hibernation`).\n- Signed player tokens, roles, and per-role visibility (`/docs/concepts/auth`,\n `/docs/concepts/visibility`).\n- A player identity that survives reconnects, hibernation and a closed tab\n (`/docs/concepts/identity`).\n- Ranked boards per project, written only by your room code\n (`/docs/persistence/leaderboards`).\n- Two strangers matched into a room without either knowing a room code\n (`/docs/concepts/quick-match`).\n- Voice chat in a room, on its own meter (`/docs/concepts/voice`).\n- One of your rooms sending a message to another (`/docs/concepts/room-bus`).\n- A project split across more than one server when a single one is full\n (`/docs/deploy/sharding`).\n\n**Starting from an engine.** Build against the engine's own loop from the first line: React and\nreact-three-fiber (`/docs/integrations/react`), Three.js (`/docs/integrations/threejs`), Phaser\n(`/docs/integrations/phaser`), PixiJS (`/docs/integrations/pixijs`), Babylon.js\n(`/docs/integrations/babylonjs`).\n\n**Moving a game that already exists.** Single-player, which is the diff above at full length:\ncanvas (`/docs/guides/retrofit`), Three.js (`/docs/guides/retrofit-threejs`), Phaser\n(`/docs/guides/retrofit-phaser`). Already multiplayer on something else: Colyseus\n(`/docs/migrate/colyseus`), Playroom (`/docs/migrate/playroom`), Socket.IO\n(`/docs/migrate/socket-io`).\n\nEvery term these pages use is defined once in the glossary (`/docs/reference/glossary`).\n\n## Rules of thumb\n\n- Sync the smallest set of facts two players must agree on. Everything else stays local.\n- A player reporting a fact about themselves is an owned write. Anything a player could gain by\n lying about (scores, deals, turn order, hit detection) is `serverOwned` plus an RPC.\n- Types are budgets, not hints. `f32`, `u8`, `str(24)` are what make an update a handful of bytes.\n- Never import `room.ts` from client code. Shared geometry and constants go in their own module\n that both sides import.\n- Do not paste a credential into a config file. `irtio login` writes one; MCP and the CLI find it.\n- Believe a room when a scenario with an adversarial pass says so, not when it looks right in two\n tabs.\n";
|
|
19
19
|
|
|
20
20
|
// src/init.ts
|
|
21
21
|
var INIT_DEPENDENCIES = [
|
|
@@ -422,7 +422,7 @@ function nextSteps(installCommand) {
|
|
|
422
422
|
pc.cyan(` ${installCommand}`),
|
|
423
423
|
"",
|
|
424
424
|
" 2. start the room",
|
|
425
|
-
pc.cyan(" npx irtio dev"),
|
|
425
|
+
pc.cyan(" npx --package @irtio/cli irtio dev"),
|
|
426
426
|
" it prints a ws:// URL and an inspector page.",
|
|
427
427
|
"",
|
|
428
428
|
" 3. join from your game \u2014 three lines:",
|
|
@@ -443,7 +443,7 @@ function nextSteps(installCommand) {
|
|
|
443
443
|
" shows the code, the link and a QR for you.)",
|
|
444
444
|
"",
|
|
445
445
|
" 5. check it under load before you believe it",
|
|
446
|
-
pc.cyan(" npx irtio simulate --bots 5 --seconds 10"),
|
|
446
|
+
pc.cyan(" npx --package @irtio/cli irtio simulate --bots 5 --seconds 10"),
|
|
447
447
|
"",
|
|
448
448
|
" irtio/AGENT.md is the whole product on one page: the model, this skeleton, the four",
|
|
449
449
|
" commands, a retrofit diff and every error worth knowing. Point your coding agent at it.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irtio/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "irtio CLI: local dev server, room scaffolding, bot simulation, deploy, logs and rooms",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"esbuild": "^0.27.3",
|
|
46
46
|
"picocolors": "^1.1.1",
|
|
47
47
|
"ws": "^8.18.0",
|
|
48
|
-
"@irtio/bots": "0.
|
|
49
|
-
"@irtio/client": "0.
|
|
50
|
-
"@irtio/
|
|
51
|
-
"@irtio/
|
|
52
|
-
"@irtio/
|
|
53
|
-
"@irtio/
|
|
48
|
+
"@irtio/bots": "0.9.0",
|
|
49
|
+
"@irtio/client": "0.9.0",
|
|
50
|
+
"@irtio/protocol": "0.9.0",
|
|
51
|
+
"@irtio/runtime": "0.9.0",
|
|
52
|
+
"@irtio/schema": "0.9.0",
|
|
53
|
+
"@irtio/server": "0.9.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/ws": "^8.5.13",
|