@irtio/cli 0.6.0 → 0.8.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.
Files changed (30) hide show
  1. package/dist/api-keys-UTLYMZYN.js +222 -0
  2. package/dist/api.js +1 -1
  3. package/dist/bundle.js +1 -1
  4. package/dist/{chunk-3HQMVCYA.js → chunk-BQBOBFBO.js} +4 -4
  5. package/dist/{chunk-RNAH5T4W.js → chunk-IDF46P7R.js} +2 -0
  6. package/dist/{chunk-DKWG7MGO.js → chunk-JL235KIE.js} +1 -1
  7. package/dist/{chunk-OTSFRVJN.js → chunk-NOYGY3HA.js} +10 -3
  8. package/dist/{chunk-ZD4ND6X6.js → chunk-OCVALOGK.js} +1 -1
  9. package/dist/{chunk-RQSJZWQC.js → chunk-WFMRNGO5.js} +134 -105
  10. package/dist/{delete-project-VENS2B44.js → delete-project-MXUYNGAO.js} +2 -2
  11. package/dist/deploy.d.ts +3 -1
  12. package/dist/deploy.js +164 -6
  13. package/dist/{dev-QM26ONKS.js → dev-VJ2ATJTF.js} +220 -14
  14. package/dist/index.js +39 -15
  15. package/dist/init.js +3 -3
  16. package/dist/{keys-JHLMEGRA.js → keys-NXRIBJZP.js} +3 -3
  17. package/dist/{leaderboard-SYPSBPS3.js → leaderboard-ZBJBKETM.js} +65 -11
  18. package/dist/{login-2M73HBZT.js → login-3RVN5PPN.js} +2 -2
  19. package/dist/{logs-2W7CPZO5.js → logs-AT7G6YRH.js} +3 -3
  20. package/dist/{migrate-T3DZJREY.js → migrate-5YCFT63L.js} +4 -4
  21. package/dist/{ratings-VG32WFDG.js → ratings-XBLX2MUW.js} +3 -3
  22. package/dist/{rollback-SO74MVZV.js → rollback-LI4TDLQA.js} +3 -3
  23. package/dist/{rooms-VI33P4RA.js → rooms-52Q5KBUS.js} +179 -20
  24. package/dist/simulate.d.ts +163 -10
  25. package/dist/simulate.js +345 -38
  26. package/dist/{static-deploy-KOWFKWZA.js → static-deploy-7UCYINJB.js} +5 -5
  27. package/dist/{status-HF3ZEKB7.js → status-JZGKH2P6.js} +3 -3
  28. package/dist/{usage-4G23QXCH.js → usage-7S447INI.js} +3 -3
  29. package/dist/{whoami-KTMTQNHM.js → whoami-UFSWPWK6.js} +2 -2
  30. package/package.json +8 -7
package/dist/index.js CHANGED
@@ -99,9 +99,21 @@ commands:
99
99
  [retire] [--url <control>] retire finishes a rotation
100
100
  keys jwt-mint --secret <s> --sub <player> sign a test token locally with the documented
101
101
  [--room <id>] [--role <r>] [--ttl <sec>] recipe \u2014 the secret never leaves this machine
102
+ rooms [--active|--idle] list a project's rooms, with the retention
103
+ [--idle-longer-than <d>] window in force and when each will be deleted
104
+ rooms get <room> one room in full: type, size class, how many
105
+ saves it holds, how big its state is, deadline
106
+ rooms set <room> --retention <d|forever|clear> override the retention window for THIS room
102
107
  rooms saves <room> list a room's save generations, newest first
103
108
  rooms restore <room> --save <id> bring a room back from a save \u2014 DISCARDS its
104
109
  current state; stops the tenant if it is live
110
+ rooms delete <room> [--force] delete a room's stored state. Refused while it
111
+ is awake unless --force, which disconnects its
112
+ clients first
113
+ api-keys mint --scopes <list> [--label <t>] create a scoped API key for this project and
114
+ print it once. Scopes: rooms:read, rooms:write
115
+ api-keys list this project's keys, revoked ones included
116
+ api-keys revoke <id> stop a key working; the row stays as a record
105
117
 
106
118
  every command takes --help (or -h) and prints its own usage, which is the authority: the lines
107
119
  above are a map, and the command's own help is generated next to the code that parses it.
@@ -112,7 +124,8 @@ default location (same as setting IRT_CREDENTIALS_FILE). Sign in once with
112
124
  commands \u2014 the pattern for agents and sandboxes that cannot reach the home directory.
113
125
 
114
126
  every command that reads the project file takes -c/--config <file> to read a different one
115
- (dev, deploy, deploy --static, migrate create, rollback, logs, rooms, status, keys). Default irtio.json;
127
+ (dev, deploy, deploy --static, migrate create, rollback, logs, rooms, status, keys, api-keys).
128
+ Default irtio.json;
116
129
  the named file must exist. That is the test.irtio.json / prod.irtio.json pattern.
117
130
  `;
118
131
  switch (command) {
@@ -122,7 +135,7 @@ switch (command) {
122
135
  break;
123
136
  }
124
137
  case "dev": {
125
- const { dev } = await import("./dev-QM26ONKS.js");
138
+ const { dev } = await import("./dev-VJ2ATJTF.js");
126
139
  await dev(args);
127
140
  break;
128
141
  }
@@ -132,18 +145,18 @@ switch (command) {
132
145
  break;
133
146
  }
134
147
  case "login": {
135
- const { login } = await import("./login-2M73HBZT.js");
148
+ const { login } = await import("./login-3RVN5PPN.js");
136
149
  await login(args);
137
150
  break;
138
151
  }
139
152
  case "whoami": {
140
- const { whoami } = await import("./whoami-KTMTQNHM.js");
153
+ const { whoami } = await import("./whoami-UFSWPWK6.js");
141
154
  await whoami(args);
142
155
  break;
143
156
  }
144
157
  case "deploy": {
145
158
  if (args.includes("--static")) {
146
- const { staticDeploy } = await import("./static-deploy-KOWFKWZA.js");
159
+ const { staticDeploy } = await import("./static-deploy-7UCYINJB.js");
147
160
  await staticDeploy(args);
148
161
  break;
149
162
  }
@@ -152,52 +165,63 @@ switch (command) {
152
165
  break;
153
166
  }
154
167
  case "migrate": {
155
- const { migrate } = await import("./migrate-T3DZJREY.js");
168
+ const { migrate } = await import("./migrate-5YCFT63L.js");
156
169
  await migrate(args);
157
170
  break;
158
171
  }
159
172
  case "rollback": {
160
- const { rollback } = await import("./rollback-SO74MVZV.js");
173
+ const { rollback } = await import("./rollback-LI4TDLQA.js");
161
174
  await rollback(args);
162
175
  break;
163
176
  }
164
177
  case "keys": {
165
- const { keys } = await import("./keys-JHLMEGRA.js");
178
+ const { keys } = await import("./keys-NXRIBJZP.js");
166
179
  await keys(args);
167
180
  break;
168
181
  }
182
+ // ---- M6 lane A: rooms API ----
183
+ // Deliberately not folded into `keys` above: that one manages the HMAC secret a game signs its
184
+ // own player tokens with, and this one manages a credential a developer's backend authenticates
185
+ // to irtio with. Two ideas, two names, so "rotate my signing secret" is never one typo away
186
+ // from "revoke my cron job's key".
187
+ case "api-keys": {
188
+ const { apiKeys } = await import("./api-keys-UTLYMZYN.js");
189
+ await apiKeys(args);
190
+ break;
191
+ }
192
+ // ---- end M6 lane A ----
169
193
  case "logs": {
170
- const { logs } = await import("./logs-2W7CPZO5.js");
194
+ const { logs } = await import("./logs-AT7G6YRH.js");
171
195
  await logs(args);
172
196
  break;
173
197
  }
174
198
  case "rooms": {
175
- const { rooms } = await import("./rooms-VI33P4RA.js");
199
+ const { rooms } = await import("./rooms-52Q5KBUS.js");
176
200
  await rooms(args);
177
201
  break;
178
202
  }
179
203
  case "status": {
180
- const { status } = await import("./status-HF3ZEKB7.js");
204
+ const { status } = await import("./status-JZGKH2P6.js");
181
205
  await status(args);
182
206
  break;
183
207
  }
184
208
  case "delete-project": {
185
- const { deleteProject } = await import("./delete-project-VENS2B44.js");
209
+ const { deleteProject } = await import("./delete-project-MXUYNGAO.js");
186
210
  await deleteProject(args);
187
211
  break;
188
212
  }
189
213
  case "usage": {
190
- const { usage } = await import("./usage-4G23QXCH.js");
214
+ const { usage } = await import("./usage-7S447INI.js");
191
215
  await usage(args);
192
216
  break;
193
217
  }
194
218
  case "ratings": {
195
- const { ratings } = await import("./ratings-VG32WFDG.js");
219
+ const { ratings } = await import("./ratings-XBLX2MUW.js");
196
220
  await ratings(args);
197
221
  break;
198
222
  }
199
223
  case "leaderboard": {
200
- const { leaderboard } = await import("./leaderboard-SYPSBPS3.js");
224
+ const { leaderboard } = await import("./leaderboard-ZBJBKETM.js");
201
225
  await leaderboard(args);
202
226
  break;
203
227
  }
package/dist/init.js CHANGED
@@ -2,8 +2,8 @@ import {
2
2
  HelpRequested,
3
3
  helpFor,
4
4
  helpRequested
5
- } from "./chunk-ZD4ND6X6.js";
6
- import "./chunk-RNAH5T4W.js";
5
+ } from "./chunk-OCVALOGK.js";
6
+ import "./chunk-IDF46P7R.js";
7
7
  import "./chunk-UPHQM6NZ.js";
8
8
 
9
9
  // src/init.ts
@@ -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, Rapier in 3D (`/docs/physics/overview`) or matter.js in 2D\n (`/docs/physics/matter2d`, `tickRate: 60`). Both predict, and `room.prediction` reads the same\n either way (`/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 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";
19
19
 
20
20
  // src/init.ts
21
21
  var INIT_DEPENDENCIES = [
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  readProjectConfig
3
- } from "./chunk-DKWG7MGO.js";
3
+ } from "./chunk-JL235KIE.js";
4
4
  import {
5
5
  helpRequested
6
- } from "./chunk-ZD4ND6X6.js";
6
+ } from "./chunk-OCVALOGK.js";
7
7
  import {
8
8
  createApiClient,
9
9
  isLoginRequired
10
- } from "./chunk-RNAH5T4W.js";
10
+ } from "./chunk-IDF46P7R.js";
11
11
  import {
12
12
  resolveControlUrlForUser
13
13
  } from "./chunk-UPHQM6NZ.js";
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  readProjectConfig
3
- } from "./chunk-DKWG7MGO.js";
3
+ } from "./chunk-JL235KIE.js";
4
4
  import {
5
5
  HelpRequested,
6
6
  helpFor,
7
7
  helpRequested
8
- } from "./chunk-ZD4ND6X6.js";
8
+ } from "./chunk-OCVALOGK.js";
9
9
  import {
10
10
  createApiClient,
11
11
  isLoginRequired
12
- } from "./chunk-RNAH5T4W.js";
12
+ } from "./chunk-IDF46P7R.js";
13
13
  import {
14
14
  resolveControlUrlForUser
15
15
  } from "./chunk-UPHQM6NZ.js";
@@ -33,6 +33,9 @@ delete remove a whole board, or one player's row with --player. Needs your log
33
33
 
34
34
  options:
35
35
  --limit <n> how many rows (default 10, max 100)
36
+ --period <key> read a past period (2026-09-03, 2026-W36, 2026-09, or a season
37
+ name). Default: whichever period the board is in now
38
+ --bucket <name> which cohort to read, on a board configured with buckets
36
39
  --cursor <c> continue from a previous page's nextCursor
37
40
  --all page through the whole board, following cursors
38
41
  --around <player> show the rows around one player instead of the top, with their rank
@@ -63,6 +66,23 @@ function parseLeaderboardArgs(args) {
63
66
  case "--all":
64
67
  parsed.all = true;
65
68
  break;
69
+ // ---- M6 lane B: boards ----
70
+ case "--period": {
71
+ const value = args[++i];
72
+ if (value === void 0 || value === "") {
73
+ throw new Error("irtio leaderboard: --period needs a period key, e.g. 2026-W36");
74
+ }
75
+ parsed.period = value;
76
+ break;
77
+ }
78
+ case "--bucket": {
79
+ const value = args[++i];
80
+ if (value === void 0 || value === "") {
81
+ throw new Error("irtio leaderboard: --bucket needs a bucket name, e.g. eu");
82
+ }
83
+ parsed.bucket = value;
84
+ break;
85
+ }
66
86
  case "--yes":
67
87
  parsed.yes = true;
68
88
  break;
@@ -125,7 +145,12 @@ function parseLeaderboardArgs(args) {
125
145
  }
126
146
  function formatLeaderboard(body, around) {
127
147
  const direction = body.direction === "lower" ? "lower is better" : "higher is better";
128
- const out = [pc.dim(`board ${body.board} \xB7 ${direction}`), ""];
148
+ const scope = [
149
+ body.period === void 0 || body.period === "" ? void 0 : `period ${body.period}`,
150
+ body.bucket === void 0 || body.bucket === "" ? void 0 : `bucket ${body.bucket}`
151
+ ].filter((part) => part !== void 0);
152
+ const header = [`board ${body.board}`, ...scope, direction].join(" \xB7 ");
153
+ const out = [pc.dim(header), ""];
129
154
  if (body.entries.length === 0) {
130
155
  out.push(
131
156
  pc.dim(
@@ -150,20 +175,29 @@ async function runLeaderboard(options) {
150
175
  const base = `${controlUrl}/v1/leaderboard/${encodeURIComponent(
151
176
  options.project
152
177
  )}/${encodeURIComponent(options.board)}`;
178
+ const scoped = () => {
179
+ const query = new URLSearchParams();
180
+ if (options.period !== void 0) query.set("period", options.period);
181
+ if (options.bucket !== void 0) query.set("bucket", options.bucket);
182
+ return query;
183
+ };
184
+ const withQuery = (url, query) => {
185
+ const q = query.toString();
186
+ return `${url}${q === "" ? "" : `?${q}`}`;
187
+ };
153
188
  if (options.around !== void 0) {
154
189
  const body2 = await readPublicJson(
155
190
  fetchImpl,
156
- `${base}/around/${encodeURIComponent(options.around)}`
191
+ withQuery(`${base}/around/${encodeURIComponent(options.around)}`, scoped())
157
192
  );
158
193
  for (const line of formatLeaderboard(body2, options.around)) log(line);
159
194
  return body2;
160
195
  }
161
196
  const pageUrl = (cursor) => {
162
- const query = new URLSearchParams();
197
+ const query = scoped();
163
198
  if (options.limit !== void 0) query.set("limit", String(options.limit));
164
199
  if (cursor !== void 0) query.set("cursor", cursor);
165
- const q = query.toString();
166
- return `${base}/top${q === "" ? "" : `?${q}`}`;
200
+ return withQuery(`${base}/top`, query);
167
201
  };
168
202
  let body = await readPublicJson(fetchImpl, pageUrl(options.cursor));
169
203
  const entries = [...body.entries];
@@ -210,10 +244,22 @@ function formatBoardList(boards) {
210
244
  ];
211
245
  }
212
246
  const nameWidth = Math.max(5, ...boards.map((b) => b.board.length));
213
- const out = [pc.dim(`${"BOARD".padEnd(nameWidth)} ${"ROWS".padStart(7)} DIRECTION`)];
247
+ const rotates = boards.some((b) => b.period !== void 0 || b.buckets === true);
248
+ const periodOf = (b) => b.period === void 0 ? "all-time" : `${b.period}${b.currentPeriod === void 0 ? "" : ` (${b.currentPeriod})`}`;
249
+ const periodWidth = rotates ? Math.max(6, ...boards.map((b) => periodOf(b).length)) : 0;
250
+ const head = rotates ? `${"BOARD".padEnd(nameWidth)} ${"ROWS".padStart(7)} ${"PERIOD".padEnd(periodWidth)} DIRECTION` : `${"BOARD".padEnd(nameWidth)} ${"ROWS".padStart(7)} DIRECTION`;
251
+ const out = [pc.dim(head)];
214
252
  for (const b of boards) {
215
- const note = b.configured && b.rows === 0 ? pc.dim(" (configured, no rows yet)") : "";
216
- out.push(`${b.board.padEnd(nameWidth)} ${String(b.rows).padStart(7)} ${b.direction}${note}`);
253
+ const notes = [
254
+ b.buckets === true ? "buckets" : void 0,
255
+ b.keep === void 0 ? void 0 : `keeps ${b.keep}`,
256
+ b.configured && b.rows === 0 ? "configured, no rows yet" : void 0
257
+ ].filter((n) => n !== void 0);
258
+ const note = notes.length === 0 ? "" : pc.dim(` (${notes.join(", ")})`);
259
+ const scope = rotates ? `${periodOf(b).padEnd(periodWidth)} ` : "";
260
+ out.push(
261
+ `${b.board.padEnd(nameWidth)} ${String(b.rows).padStart(7)} ${scope}${b.direction}${note}`
262
+ );
217
263
  }
218
264
  return out;
219
265
  }
@@ -312,6 +358,12 @@ async function leaderboard(args, deps = {}) {
312
358
  if (parsed.cursor !== void 0 && parsed.all === true) {
313
359
  throw new Error("irtio leaderboard: --cursor and --all are two ways to do the same thing");
314
360
  }
361
+ if (parsed.period !== void 0 && parsed.action !== void 0) {
362
+ throw new Error("irtio leaderboard: --period is for a read, not for list or delete");
363
+ }
364
+ if (parsed.bucket !== void 0 && parsed.action !== void 0) {
365
+ throw new Error("irtio leaderboard: --bucket is for a read, not for list or delete");
366
+ }
315
367
  if (parsed.around !== void 0 && (parsed.cursor !== void 0 || parsed.all === true)) {
316
368
  throw new Error("irtio leaderboard: --around does not page; it is already a window");
317
369
  }
@@ -323,6 +375,8 @@ async function leaderboard(args, deps = {}) {
323
375
  ...parsed.around !== void 0 ? { around: parsed.around } : {},
324
376
  ...parsed.cursor !== void 0 ? { cursor: parsed.cursor } : {},
325
377
  ...parsed.all === true ? { all: true } : {},
378
+ ...parsed.period !== void 0 ? { period: parsed.period } : {},
379
+ ...parsed.bucket !== void 0 ? { bucket: parsed.bucket } : {},
326
380
  ...parsed.url !== void 0 ? { controlUrl: parsed.url } : {}
327
381
  });
328
382
  } catch (err) {
@@ -2,8 +2,8 @@ import {
2
2
  HelpRequested,
3
3
  helpFor,
4
4
  helpRequested
5
- } from "./chunk-ZD4ND6X6.js";
6
- import "./chunk-RNAH5T4W.js";
5
+ } from "./chunk-OCVALOGK.js";
6
+ import "./chunk-IDF46P7R.js";
7
7
  import {
8
8
  credentialsPath,
9
9
  resolveControlUrl,
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  readProjectConfig
3
- } from "./chunk-DKWG7MGO.js";
3
+ } from "./chunk-JL235KIE.js";
4
4
  import {
5
5
  HelpRequested,
6
6
  helpFor,
7
7
  helpRequested
8
- } from "./chunk-ZD4ND6X6.js";
8
+ } from "./chunk-OCVALOGK.js";
9
9
  import {
10
10
  createApiClient,
11
11
  isLoginRequired
12
- } from "./chunk-RNAH5T4W.js";
12
+ } from "./chunk-IDF46P7R.js";
13
13
  import {
14
14
  resolveControlUrlForUser
15
15
  } from "./chunk-UPHQM6NZ.js";
@@ -1,18 +1,18 @@
1
1
  import {
2
2
  readProjectConfig
3
- } from "./chunk-DKWG7MGO.js";
3
+ } from "./chunk-JL235KIE.js";
4
4
  import {
5
5
  bundleRoom
6
- } from "./chunk-OTSFRVJN.js";
6
+ } from "./chunk-NOYGY3HA.js";
7
7
  import {
8
8
  HelpRequested,
9
9
  helpFor,
10
10
  helpRequested
11
- } from "./chunk-ZD4ND6X6.js";
11
+ } from "./chunk-OCVALOGK.js";
12
12
  import {
13
13
  createApiClient,
14
14
  isLoginRequired
15
- } from "./chunk-RNAH5T4W.js";
15
+ } from "./chunk-IDF46P7R.js";
16
16
  import {
17
17
  resolveControlUrlForUser
18
18
  } from "./chunk-UPHQM6NZ.js";
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  readProjectConfig
3
- } from "./chunk-DKWG7MGO.js";
3
+ } from "./chunk-JL235KIE.js";
4
4
  import {
5
5
  HelpRequested,
6
6
  helpFor,
7
7
  helpRequested
8
- } from "./chunk-ZD4ND6X6.js";
8
+ } from "./chunk-OCVALOGK.js";
9
9
  import {
10
10
  createApiClient,
11
11
  isLoginRequired
12
- } from "./chunk-RNAH5T4W.js";
12
+ } from "./chunk-IDF46P7R.js";
13
13
  import {
14
14
  resolveControlUrlForUser
15
15
  } from "./chunk-UPHQM6NZ.js";
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  readProjectConfig
3
- } from "./chunk-DKWG7MGO.js";
3
+ } from "./chunk-JL235KIE.js";
4
4
  import {
5
5
  HelpRequested,
6
6
  helpFor,
7
7
  helpRequested
8
- } from "./chunk-ZD4ND6X6.js";
8
+ } from "./chunk-OCVALOGK.js";
9
9
  import {
10
10
  createApiClient,
11
11
  isLoginRequired
12
- } from "./chunk-RNAH5T4W.js";
12
+ } from "./chunk-IDF46P7R.js";
13
13
  import {
14
14
  resolveControlUrlForUser
15
15
  } from "./chunk-UPHQM6NZ.js";