@homie-rocks/studio 0.19.2 → 0.21.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/CHANGELOG.md CHANGED
@@ -17,9 +17,99 @@ To bring a studio up to date, tell Claude: "Upgrade my studio to the newest Homi
17
17
  `npx -y @homie-rocks/studio@latest upgrade`, which shows what's new since the version your studio pins (from this
18
18
  file) and what the upgrade would change, and changes nothing until you agree.
19
19
 
20
+ ## [0.21.0] - 2026-10-02
21
+
22
+ **Plugin 0.22.0** · [#31](https://github.com/homie-rocks/homie/pull/31)
23
+
24
+ The Homie mod for Claude Code: your studio, its rooms and a real seat in a live game beside the chat, with a look
25
+ before protected files, deploys and spending, and keys kept out of what Claude reads.
26
+
27
+ ### Added
28
+
29
+ - Plugin: the Homie mod (Claude Code 2.1.287 or later). Inside a studio, a one-row band above the prompt shows the
30
+ studio, the game, the build step, ▶ Play and how many people are playing now. Outside a studio it adds nothing.
31
+ - Plugin: the Studio pane (`/studio`) opens by itself when a build starts, on a wide enough terminal. **Build** has the
32
+ live Plan, Build, Checks, Deploy feed and the latest check frame, or a live Watch of a room of the game being built.
33
+ **Rooms** lists who is playing and the AI players, with Watch and Join links; Announce, Mute and Kick go through
34
+ the office, and Mute and Kick are only ever asked for, so you still confirm them with one tap. **Games** has
35
+ launch states and the remix switch; **Stats** and **Codex** link to the rest. **Lab** shows each game's last Game
36
+ Lab check (New's phases beside Today's, whether the replays match) and the lab's link while it runs.
37
+ - Plugin: the Arcade pane (`/arcade`). Take a real seat in a public room of a Homie game (Homie Arcade's, or your
38
+ studio's own) and play it in the pane with w/a/s/d or the arrow keys while Claude works. It runs in one headless
39
+ Chrome on your computer at the lowest priority, about 8 frames a second in a terminal, pauses while the pane is
40
+ hidden and stops when you leave.
41
+ - Plugin: a Parts pane for the `parallel` skill: each part, its last step, and the merge waiting for all of them.
42
+ - Plugin: instant commands that answer without a Claude turn: `/play`, `/watch [room]`, `/rooms`, `/build`, `/codex`,
43
+ `/deploy-status`, `/perf-numbers`, `/parts`, `/arcade`. They print links or open a pane; nothing opens a browser.
44
+ - Plugin: guards. An edit to a file matched by studio.json `"protect"` (globs, such as `"games/*/game.json"`) shows
45
+ the diff and waits for Proceed or Cancel. So does a deploy (where it goes, what it creates, the commits and files
46
+ since the last one, the checks) and a paid fal or ElevenLabs run that would pass studio.json `"budget"`.
47
+ - Plugin: Cloudflare tokens, provider keys, agent passes, office keys and one-time owner links are taken out of
48
+ command output before Claude reads it.
49
+ - Plugin: setup status, check, playtest and deploy results are drawn as checklists and rows, with the live link.
50
+ - `homie-studio office mute <game> <room> <seat | name> [--minutes 10] [--off]`, asked for like `office kick`.
51
+ - A build's latest check frame is also kept as a small picture beside its progress feed, for the mod to draw.
52
+
53
+ ### Upgrade notes
54
+
55
+ - Every part of the mod has its own switch (`/config`, or `pluginConfigs` in settings.json). Codex installs the plugin
56
+ as before; it does not run plugin hooks, so there it is the skills and tools you had.
57
+ - `homie-studio upgrade --apply` adds `office mute` and the `protect` and `budget` lines to AGENTS.md.
58
+
59
+ ## [0.20.0] - 2026-10-02
60
+
61
+ **Plugin 0.21.0** · [#30](https://github.com/homie-rocks/homie/pull/30) · [release-2026-10-02-studio-0.20.0](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-02-studio-0.20.0)
62
+
63
+ A Game Lab: tune how one move of your game feels (a jump, a hit, a dash), your new version beside your last commit,
64
+ frame by frame.
65
+
66
+ ### Added
67
+
68
+ - Plugin: the `lab` skill. Say "iterate on the jump", "make the hit feel punchier" or "tune the drift". Claude first
69
+ teaches your game to tell the lab about that move (its phases, the numbers that shape it) without changing how it
70
+ plays, and commits that. Then it proposes the change as named phases and numbers, shows it to you New beside Today,
71
+ keeps only what you like, and can record a side-by-side clip to share.
72
+ - `homie-studio lab <game>`: the Game Lab, a page on your own computer. One short take of your game plays in New (your
73
+ working copy, rebuilt every time a file is saved) beside Today (your last commit), on one clock, with the same
74
+ presses on the same frames. Slow it to a quarter or a tenth, step a frame at a time, try 30, 15 or 12 frames a
75
+ second, or switch to a phone-sized screen; the frames are the same frames at any speed.
76
+ - In the lab: a timeline that names each phase of the move, graphs of New against Today, captions, the game's own
77
+ onion skin and arcs, and its views (close, the whole arena). Its numbers are sliders: move one and New plays it at
78
+ once, and "Keep in code" writes the ones you like into the game's `tunables.json`. Press REC and play a move to make
79
+ a new take; both versions get your presses.
80
+ - `homie-studio lab check <game>`: the same take with no window. How long each phase lasts in each version, the peaks
81
+ of what the game tracks, whether a replay landed on exactly the same frames, a contact sheet and a still.
82
+ `homie-studio lab set <game> name=value` writes a number into `tunables.json`; `homie-studio lab --stop` stops the lab.
83
+ - Homie for Claude Desktop (`homie-studio mcp`): `game_lab` opens the lab and answers with a card: a still of both
84
+ versions at the move's busiest moment, their phases side by side, and Open.
85
+ - For your game's code, `@homie-rocks/studio/lab`: `lab.tunables`, `lab.phase`, `lab.track`, `lab.camera`,
86
+ `lab.overlay`, `lab.stage` (a training dummy for one move) and `lab.random` (dice for sparks and shake). Every call
87
+ does nothing outside the lab, so the calls stay in your game.
88
+ - Plugin: the perf skill's `try --measure` measures a change you keep for how it feels and says what it costs, without
89
+ reverting it (`try` on its own reverts anything that is not faster).
90
+
91
+ ### Changed
92
+
93
+ - Gem Rush's bump lands. The bumped body holds for 70 ms, white and squashed against the hit, then flies stretched
94
+ along it, eases to a stop 185 px away at any frame rate (it used to slide a different distance at 12 and at 60
95
+ frames a second, then creep), and wobbles as it settles. Sparks, and a small camera kick for whoever was in it.
96
+ - Ember Vale's strike lands. The slime freezes white when it is hit and its damage pops up, a swipe carries through,
97
+ the slime is pushed back and wobbles like jelly, and a kill bursts. Your hero steps into each strike, so three
98
+ strikes in a row still reach.
99
+ - Both starters come ready for the lab: their numbers in `tunables.json` and a take in `lab.json`, which
100
+ `homie-studio game new` copies with the game. A new studio's AGENTS.md names both files and the lab command.
101
+
102
+ ### Upgrade notes
103
+
104
+ - A game you made from a starter before 0.20.0 keeps its old feel: the new bump and strike come with a new copy
105
+ (`homie-studio game new`). To tune a move of your own game, tell Claude "build a Game Lab for <the move>": it adds
106
+ the lab's calls and a take, and commits that first.
107
+ - Today is your last commit, so commit (or stash) your work in progress before you open the lab. Its builds live in
108
+ `.studio/lab/`, which git already ignores.
109
+
20
110
  ## [0.19.2] - 2026-10-02
21
111
 
22
- **Plugin 0.20.2** · [#29](https://github.com/homie-rocks/homie/pull/29) · [npm](https://www.npmjs.com/package/@homie-rocks/studio/v/0.19.2)
112
+ **Plugin 0.20.2** · [#29](https://github.com/homie-rocks/homie/pull/29) · [release-2026-10-02-studio-0.19.2](https://github.com/homie-rocks/homie/releases/tag/release-2026-10-02-studio-0.19.2)
23
113
 
24
114
  Release notes: this changelog, on every GitHub release, inside the npm package, and at the top of
25
115
  `homie-studio upgrade`.
@@ -846,7 +936,8 @@ The first studio toolkit: a studio in one folder, a multiplayer starter, and dep
846
936
  create, and `publish` to the homie.rocks directory.
847
937
  - Plugin: the `studio-setup`, `game` and `publish` skills, and the remote Homie MCP.
848
938
 
849
- [0.19.2]: https://www.npmjs.com/package/@homie-rocks/studio/v/0.19.2
939
+ [0.20.0]: https://github.com/homie-rocks/homie/pull/30
940
+ [0.19.2]: https://github.com/homie-rocks/homie/releases/tag/release-2026-10-02-studio-0.19.2
850
941
  [0.19.1]: https://github.com/homie-rocks/homie/releases/tag/release-2026-10-02-studio-0.19.1
851
942
  [0.19.0]: https://github.com/homie-rocks/homie/releases/tag/release-2026-10-02-studio-0.19.0
852
943
  [0.18.2]: https://github.com/homie-rocks/homie/releases/tag/release-2026-10-02-studio-0.18.2
package/README.md CHANGED
@@ -16,6 +16,7 @@ npx homie-studio game new crown-thief --from gem-rush --name "Crown Thief"
16
16
  npx homie-studio dev # the whole site locally
17
17
  npx homie-studio check crown-thief --url http://127.0.0.1:8787
18
18
  npx homie-studio perf crown-thief --url http://127.0.0.1:8787 # how fast it runs: a computer and an emulated phone, host and replica
19
+ npx homie-studio lab crown-thief # the Game Lab: how a move feels, New beside Today, frame by frame
19
20
  npx homie-studio deploy --plan # what deploy will create, and what it costs; changes nothing
20
21
  npx homie-studio deploy # the studio's own Cloudflare
21
22
  npx homie-studio publish # the homie.rocks directory
@@ -452,6 +453,44 @@ Minified JavaScript gzips about as well as source text, and a bundle with three.
452
453
  source in strings, which no minifier touches; the line for such a file says `minified, 19% GLSL shader source in
453
454
  strings`, and the JSON's `code` has every measure.
454
455
 
456
+ ## How it feels: the Game Lab (`homie-studio lab`)
457
+
458
+ ```sh
459
+ npx homie-studio lab crown-thief [--today HEAD|<ref>] [--port 8790] # http://127.0.0.1:8790/crown-thief/ until --stop
460
+ npx homie-studio lab check crown-thief [--take knock] [--fps 30] [--device phone] # the same, headless: numbers and pictures
461
+ npx homie-studio lab set crown-thief hitStopMs=80 knockDistance=200 # values into games/crown-thief/tunables.json
462
+ npx homie-studio lab --stop
463
+ ```
464
+
465
+ A page on this computer for one mechanic at a time (a jump, a hit, a dash, a drift). It plays one short **take**
466
+ (`games/<id>/lab.json`: a few seconds, a seed, the presses) in two builds side by side: **New**, the working tree,
467
+ rebuilt on every save, and **Today**, the last commit (or any ref), built from git's own checkout of it with only the
468
+ game's files (and what its bundle reads from the studio), cached per commit in `.studio/lab/`. Both play on the lab's
469
+ clock: requestAnimationFrame, performance.now(), Date.now(), timers and Math.random() are the lab's in the game's page
470
+ (its harness, `lab/harness.js`, is the page's first script), so frame f is exactly f/fps seconds in both builds, the
471
+ dice are the same, the presses land on the same frames, and netplay's offline host, a round's clock and a knockback's
472
+ timer all slow down, pause and step together. Every replay is compared with the last run, frame by frame: the page
473
+ says "Replays match", or the frame where a build read something the lab does not drive.
474
+
475
+ The page: New, Today, both side by side or Today ghosted over New; the game's own views (`lab.camera`) and overlays
476
+ (`lab.overlay`: onion skin, arcs, reach); Desk (1280x800) or Phone (390x844); play, pause, a frame back or forward,
477
+ start and end; 1x, ½, ¼ and ⅒ speed (each frame is the same frame at any speed); 60, 30, 15 or 12 frames a second (what
478
+ a slower device draws); a frame counter; a timeline of the phases each build names (`lab.phase`), scrubbable, with each
479
+ frame's motion marked HOLD, MOVE or FAST; graphs of every tracked value (`lab.track`), New against Today, beside the
480
+ body's speed from the port probe and the game's JavaScript per frame; the phase each pane is in, as a caption; the
481
+ game's tunables (`games/<id>/tunables.json`) as sliders, with Today's value marked, that restart New with the new value
482
+ and write kept values back into the file ("Keep in code", one tunable a line); and REC, which records a new take while
483
+ the person plays it in New (both builds get the same presses, on the same frames). A phone opens it too.
484
+
485
+ A game opts in with `@homie-rocks/studio/lab` (`lab` from `/port` too, `HomiePort.lab` in a static game): every call is
486
+ a no-op outside the lab. Both starters do: Gem Rush's bump (a dummy to bump: `lab.stage`) and Ember Vale's strike (a
487
+ training slime). `lab check` plays the take headless twice per build and writes `.studio/lab/<id>/check-<time>/`:
488
+ summary.json (each phase's frames, each tracked value's peak and where it ends, the JavaScript per frame, errors, the
489
+ replay check), REPORT.md (New against Today), sheet.png (the lab at each phase's start) and still.jpg; and
490
+ `.studio/lab/<id>/latest.json`, the last check in a few fields (`.studio/lab/server.json` says where a running lab is).
491
+ The local MCP's `game_lab` starts the lab and answers with a card. The Homie plugin's `lab` skill has the method:
492
+ instrument and commit first (Today is the game as it is), then change how it feels, and keep what the person likes.
493
+
455
494
  ## Checks on a computer without a GPU
456
495
 
457
496
  `check`, `port check` and `look` run Chrome headless. On a Mac they use its GPU. On Linux (a Claude Code cloud
@@ -37,6 +37,20 @@
37
37
  * (better, worse or the same within the noise: medians, a 95% interval, a rank test,
38
38
  * and the guards that must not get worse; runs taken side by side with the same
39
39
  * --pair label on both sides are judged as pairs, so a computer's drift cancels)
40
+ * homie-studio lab <id> [--port 8790] [--today HEAD|<ref>] [--take <name>]
41
+ * (the Game Lab, on this computer, until --stop: one take of the game (games/<id>/lab.json)
42
+ * played in New, the working tree, rebuilt on every save, beside Today, built from git's
43
+ * checkout of the ref, on one clock with the same seed and presses: slowed to a tenth or
44
+ * a frame at a time, at 60, 30, 15 or 12 fps, on a computer or a phone; a timeline of the
45
+ * phases the game names, graphs New against Today, the game's own overlays and views,
46
+ * sliders that write kept values into games/<id>/tunables.json, and REC for a new take.
47
+ * The game calls @homie-rocks/studio/lab; the plugin's lab skill has the method)
48
+ * homie-studio lab check <id> [--take <name>] [--today <ref>] [--device desk|phone] [--fps 60] [--frames 6] [--out <dir>]
49
+ * (the same take headless, twice per build: every phase, each tracked value's peak, the
50
+ * game's JavaScript per frame, whether a replay lands on the same frames, a contact sheet
51
+ * and a still; .studio/lab/<id>/check-<time>/)
52
+ * homie-studio lab set <id> <name>=<value> ... (values into games/<id>/tunables.json, one tunable a line)
53
+ * homie-studio lab --stop (this studio's Game Lab only, and the checkouts Today was built from)
40
54
  * homie-studio chrome [install] [--fresh] (which Chrome the checks use; on Linux, `install` fetches Chrome for Testing;
41
55
  * --fresh fetches it even when the machine has a Chrome)
42
56
  * homie-studio look [<path>...] [--url <site>] [--shots <dir>] [--only computer,phone,sideways]
@@ -80,8 +94,9 @@
80
94
  * homie-studio office invite <game> [--label "<who>"] [--uses 1|<n>|any] [--count 1] [--days <n>] [--server <id>]
81
95
  * homie-studio office launch <game> private|invite|public [--remixable on|off] [--max <n>|game]
82
96
  * homie-studio office kick <game> <room> <seat number | name> [--minutes 10]
97
+ * homie-studio office mute <game> <room> <seat number | name> [--minutes 10] [--off]
83
98
  * homie-studio office close <game> <room> [--minutes 10] [--reopen]
84
- * (kick, close and launch are ASKED for: the owner confirms each with one
99
+ * (kick, mute, close and launch are ASKED for: the owner confirms each with one
85
100
  * tap in their own browser, from the link this prints)
86
101
  * homie-studio office revoke (every office key, play ticket and pending ask ends)
87
102
  *
@@ -169,6 +184,7 @@ import { look } from '../lib/look.mjs';
169
184
  import { importPort, planPort } from '../lib/port.mjs';
170
185
  import { portCheck } from '../lib/port-check.mjs';
171
186
  import { DEFAULT_GOAL, perfCompare, perfRun, perfSizes } from '../lib/perf.mjs';
187
+ import { LAB_PORT, labServe, labSet, labStop } from '../lib/lab.mjs';
172
188
  import { R2_COST, lineOf, mediaPlan, r2OverOf, recordUpload, resolveMedia, sizeOf, typeOf } from '../lib/media.mjs';
173
189
  import { ensureMigrations, migrationWord, newStudio } from '../lib/scaffold.mjs';
174
190
  import { lineDiff, upgradeApply, upgradePlan } from '../lib/upgrade.mjs';
@@ -177,7 +193,7 @@ import { listGames, newGame, readStudio, remixGame, requireStudio, siteUrl, star
177
193
  import { STUDIO_VERSION } from '../lib/version.mjs';
178
194
  import { statsKey, statsLink, statsRevoke, statsShare, statsShow } from '../lib/stats.mjs';
179
195
  import { playersOwner, playersShow } from '../lib/players.mjs';
180
- import { officeAnnounce, officeClose, officeInvite, officeKey, officeKick, officeLaunch, officeLines, officeLink, officeRevoke, officeShow } from '../lib/office.mjs';
196
+ import { officeAnnounce, officeClose, officeInvite, officeKey, officeKick, officeLaunch, officeLines, officeLink, officeMute, officeRevoke, officeShow } from '../lib/office.mjs';
181
197
  import { agentsBrain, agentsBrainKey, agentsPass, agentsPasses, agentsRevoke, serversClose, serversLevel, serversLines, serversList, serversMember, serversNew, serversSet } from '../lib/servers.mjs';
182
198
  import { AgentSeat } from '../lib/agent-seat.mjs';
183
199
  import { Feed, currentFeed, currentId, flushProgress, publicFeed, readFeed, recordChange, startProgress } from '../lib/progress.mjs';
@@ -191,7 +207,7 @@ import { formatHandoff, handoff } from '../lib/handoff.mjs';
191
207
 
192
208
  const argv = process.argv.slice(2);
193
209
  const flags = new Map();
194
- const BOOL_FLAGS = ['revoke', 'json', 'yes', 'detach', 'no-install', 'plan', 'stop', 'share', 'apply', 'diff', 'template', 'ci', 'fresh', 'install', 'remove', 'replace', 'artifact', 'open', 'reopen', 'kids', 'remote-ai', 'dry-run', 'verify', 'maps', 'profile'];
210
+ const BOOL_FLAGS = ['off', 'revoke', 'json', 'yes', 'detach', 'no-install', 'plan', 'stop', 'share', 'apply', 'diff', 'template', 'ci', 'fresh', 'install', 'remove', 'replace', 'artifact', 'open', 'reopen', 'kids', 'remote-ai', 'dry-run', 'verify', 'maps', 'profile'];
195
211
  const positional = [];
196
212
  for (let i = 0; i < argv.length; i++) {
197
213
  const a = argv[i];
@@ -234,6 +250,25 @@ function print(result) {
234
250
  for (const m of result.siteSkipped ?? []) lines.push(` left out: ${m.what}: ${m.why}`);
235
251
  if (result.codexes?.length) lines.push(`Game Codex (the owner's private pages; codex link <id> opens one): ${result.codexes.map((x) => `/_studio/codex/${x}/`).join(', ')}`);
236
252
  break;
253
+ case 'lab':
254
+ lines.push(result.already ? `The Game Lab is running: ${result.url}` : `The Game Lab stopped (it was at ${result.url}).`);
255
+ break;
256
+ case 'lab stop':
257
+ lines.push(result.stopped.length ? 'Stopped this studio\'s Game Lab.' : 'No Game Lab of this studio was running.', ...(result.removed.length ? [`Removed the checkouts Today was built from: ${result.removed.join(', ')}`] : []));
258
+ break;
259
+ case 'lab set':
260
+ lines.push(result.changed.length ? `${result.file}: ${result.changed.map((c) => `${c.name} ${c.from} -> ${c.to}`).join(', ')}` : `${result.file}: nothing changed`);
261
+ break;
262
+ case 'lab check': {
263
+ const det = (x) => (x === null ? 'same frames' : `DIFFERED from frame ${x}`);
264
+ lines.push(`${result.game}: take "${result.take ?? '(none)'}", ${result.frames} frames at ${result.fps} fps`,
265
+ ` New: ${result.phases.new.join(' · ') || 'no phases'}; JavaScript ${result.cost.new?.mean} ms a frame (p95 ${result.cost.new?.p95}); replay: ${det(result.deterministic.new)}`,
266
+ result.phases.today ? ` Today (${result.today}): ${result.phases.today.join(' · ') || 'no phases'}; JavaScript ${result.cost.today?.mean} ms a frame (p95 ${result.cost.today?.p95}); replay: ${det(result.deterministic.today)}` : ` Today: ${result.todayNote ?? 'none'}`,
267
+ ...(result.software ? [` Note: a software renderer (${result.renderer}): pictures are right, the JavaScript numbers are slow`] : []),
268
+ ...result.errors.map((e) => ` error: ${e}`),
269
+ `Report: ${result.report}`, `Pictures: ${result.sheet}, ${result.still}`);
270
+ break;
271
+ }
237
272
  case 'media list':
238
273
  for (const kind of ['music', 'videos']) {
239
274
  lines.push(`${kind}/manifest.json: ${result[kind].pages.length} page(s)`);
@@ -377,6 +412,7 @@ function print(result) {
377
412
  case 'agents brain':
378
413
  case 'office launch':
379
414
  case 'office kick':
415
+ case 'office mute':
380
416
  case 'office close':
381
417
  lines.push(result.asked ? `Asked: ${result.what}` : result.message, ...(result.asked ? [`Owner's one-tap link (until the ask ends, 15 min): ${result.link}`, result.use] : []));
382
418
  break;
@@ -555,6 +591,7 @@ async function main() {
555
591
  if (cmd === 'games') return { ok: true, command: 'games', games: listGames(root).map(({ dir, ...g }) => ({ ...g, dir: relative(root, dir) })) };
556
592
  if (cmd === 'build') return tracked(root, 'build', () => build(root, { only: positional[1] ?? null, log, maps: flags.has('maps') }), 'build');
557
593
  if (cmd === 'perf' && sub === 'sizes') return perfSizes(root, positional[2] ?? listGames(root)[0]?.id);
594
+ if (cmd === 'lab') return labCommand(root, sub);
558
595
  if (cmd === 'perf') {
559
596
  const game = sub ?? listGames(root)[0]?.id;
560
597
  const url = flags.get('url') ?? siteUrl(root);
@@ -619,6 +656,7 @@ async function main() {
619
656
  if (sub === 'invite') return officeInvite(root, positional[2], { url, label: flags.get('label'), uses: flags.get('uses'), count: flags.get('count'), days: flags.get('days'), server: flags.get('server') });
620
657
  if (sub === 'launch') return officeLaunch(root, positional[2], positional[3], { url, remixable: flags.get('remixable'), max: flags.get('max') });
621
658
  if (sub === 'kick') return officeKick(root, positional[2], positional[3], positional.slice(4).join(' ') || undefined, { url, minutes: flags.get('minutes') });
659
+ if (sub === 'mute') return officeMute(root, positional[2], positional[3], positional.slice(4).join(' ') || undefined, { url, minutes: flags.get('minutes'), off: flags.has('off') });
622
660
  if (sub === 'close') return officeClose(root, positional[2], positional[3], { url, minutes: flags.get('minutes'), reopen: flags.has('reopen') });
623
661
  if (sub === 'revoke') return officeRevoke(root, { url });
624
662
  }
@@ -662,6 +700,21 @@ async function main() {
662
700
  return { ok: false, command: cmd, why: `unknown command "${[cmd, sub].filter(Boolean).join(' ')}" (homie-studio help)` };
663
701
  }
664
702
 
703
+ /** `homie-studio lab …` (lib/lab.mjs, lib/lab-check.mjs): the Game Lab of one game. */
704
+ async function labCommand(root, sub) {
705
+ if (flags.has('stop') || sub === 'stop') return labStop(root);
706
+ if (sub === 'set') return labSet(root, positional[2], positional.slice(3));
707
+ if (sub === 'check') {
708
+ const { labCheck } = await import('../lib/lab-check.mjs');
709
+ const num = (k, d) => (flags.has(k) ? Number(flags.get(k)) : d);
710
+ return labCheck(root, positional[2] ?? null, {
711
+ take: flags.get('take') ?? null, today: String(flags.get('today') ?? 'HEAD'), device: flags.get('device') ?? null, fps: num('fps', null),
712
+ out: flags.get('out') ? resolve(String(flags.get('out'))) : null, still: flags.get('still') ? resolve(String(flags.get('still'))) : null, frames: num('frames', 6), log,
713
+ });
714
+ }
715
+ return labServe(root, sub ?? null, { port: Number(flags.get('port') ?? LAB_PORT), today: String(flags.get('today') ?? 'HEAD'), take: flags.get('take') ?? null, log });
716
+ }
717
+
665
718
  /** `homie-studio codex …` (lib/codex.mjs): the Game Codex of one game. */
666
719
  function codexCommand(root, sub) {
667
720
  const games = listGames(root);