@looop-games/cli 0.1.26 → 0.1.27

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
@@ -14,6 +14,23 @@ Versions: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.1.27] - 2026-08-09
18
+
19
+ ### Added
20
+ - `looop dev` now injects the **dev toolbox** into every game it serves — the
21
+ looop chip / `Ctrl/Cmd+Shift+L` overlay with the Inspector, your tweaks, and
22
+ a link to the Assets page (see the engine changelog for what is inside). The
23
+ tag is added only when your pinned engine actually ships the toolbox, so an
24
+ older engine just keeps working until you `looop update`. `?_toolbox=off`
25
+ hides it for a session. Published games are untouched — this exists only on
26
+ your dev server.
27
+
28
+ ### Changed
29
+ - **BREAKING** `looop inspect` is now `looop assets`.
30
+ Migration: same command, same arguments — `looop assets [<id>]` opens the
31
+ dev stack on the Assets page (the renamed asset inspector). The old verb is
32
+ gone; "Inspector" now names the toolbox's live-world tool instead.
33
+
17
34
  ## [0.1.26] - 2026-08-07
18
35
 
19
36
  ### Added
package/bin/looop.mjs CHANGED
@@ -28,7 +28,7 @@ const HELP = `looop — build and run Looop games
28
28
  Usage:
29
29
  looop create <name> Bootstrap a new game folder (multiplayer works out of the box)
30
30
  looop dev [--port <n>] Run the full dev stack (game + multiplayer + services)
31
- looop inspect [<id>] Browse and drive your models, sounds and assets on their own
31
+ looop assets [<id>] Browse and drive your models, sounds and assets on their own
32
32
  looop lane <name> Open an isolated copy of the game to experiment in, safely
33
33
  looop test Run the game's tests (*.test.mjs) and smokes (*.smoke.mjs)
34
34
  looop lint [--fix] Check the game against the Looop rules (runs inside 'looop test')
@@ -61,14 +61,14 @@ try {
61
61
  // Keep the process alive; servers + children hold the loop open.
62
62
  break;
63
63
  }
64
- case 'inspect': {
65
- // The same dev stack `looop dev` runs, opened on the inspector. Not a
66
- // second server: the inspector is a page under /shared/, which this one
64
+ case 'assets': {
65
+ // The same dev stack `looop dev` runs, opened on the assets page. Not a
66
+ // second server: the assets page is a page under /shared/, which this one
67
67
  // already mounts.
68
- const { inspectorUrl, openBrowser, itemArg } = await import('../lib/inspect.mjs');
68
+ const { assetsPageUrl, openBrowser, itemArg } = await import('../lib/assets-page.mjs');
69
69
  const handle = await dev({ port: flag('port') ? Number(flag('port')) : undefined });
70
- const target = inspectorUrl(handle.url, { item: itemArg(rest) });
71
- console.log(`\n 🔎 Inspector → ${target}\n`);
70
+ const target = assetsPageUrl(handle.url, { item: itemArg(rest) });
71
+ console.log(`\n 🔎 Assets → ${target}\n`);
72
72
  if (!rest.includes('--no-open')) openBrowser(target);
73
73
  const shutdown = () => {
74
74
  handle.stop();
@@ -1,9 +1,9 @@
1
- // `looop inspect` — the dev stack, opened on the asset inspector instead of the
1
+ // `looop assets` — the dev stack, opened on the Assets page instead of the
2
2
  // game.
3
3
  //
4
- // The inspector is a served page under /shared/, not a separate server: the dev
4
+ // the assets page is a served page under /shared/, not a separate server: the dev
5
5
  // server already mounts the engine's shared tree, so there is nothing new to
6
- // start and nothing to keep in sync. That also means the inspector still works
6
+ // start and nothing to keep in sync. That also means the assets page still works
7
7
  // when the game itself is broken, which is when it is most wanted.
8
8
  //
9
9
  // It finds the game on its own — the page asks the dev server what it is serving
@@ -13,18 +13,18 @@ import { spawn } from 'node:child_process';
13
13
  import { existsSync } from 'node:fs';
14
14
  import { join } from 'node:path';
15
15
 
16
- export const INSPECTOR_PATH = '/shared/ui/inspector/';
16
+ export const ASSETS_PAGE_PATH = '/shared/ui/assets/';
17
17
 
18
18
  // Built from the dev server's OWN url rather than an assumed localhost:8000: a
19
19
  // lane runs on its own port base and 8000 may belong to somebody else's server,
20
- // and an inspector opened on the wrong port inspects the wrong game — or none.
21
- export function inspectorUrl(gameUrl, { item } = {}) {
20
+ // and an assets page opened on the wrong port inspects the wrong game — or none.
21
+ export function assetsPageUrl(gameUrl, { item } = {}) {
22
22
  const base = new URL(gameUrl);
23
23
  const q = item ? `?_item=${encodeURIComponent(item)}` : '';
24
- return `${base.origin}${INSPECTOR_PATH}${q}`;
24
+ return `${base.origin}${ASSETS_PAGE_PATH}${q}`;
25
25
  }
26
26
 
27
- // Does this game have anything for the inspector to show?
27
+ // Does this game have anything for the assets page to show?
28
28
  //
29
29
  // The whole answer is `assets.js`, because the list IS the declaration — nothing
30
30
  // walks the folder looking for files any more. So this is the same question as
@@ -33,15 +33,15 @@ export function inspectorUrl(gameUrl, { item } = {}) {
33
33
  // points at is then invisible exactly when it would have helped.
34
34
  //
35
35
  // The file's CONTENTS are not read. A registry that declares nothing, or one
36
- // that throws on import, is a case the inspector page itself reports far better
36
+ // that throws on import, is a case the assets page itself reports far better
37
37
  // than a banner line could — and reading it here would mean the dev server
38
38
  // evaluating game code to decide how to print a URL.
39
- export function hasInspectableAssets(projectDir) {
39
+ export function hasDeclaredAssets(projectDir) {
40
40
  return !!projectDir && existsSync(join(projectDir, 'assets.js'));
41
41
  }
42
42
 
43
43
  // Flags that take a VALUE, so the value is not mistaken for the asset name.
44
- // `looop inspect --port 8210` is two argv entries, and reading "the first
44
+ // `looop assets --port 8210` is two argv entries, and reading "the first
45
45
  // argument that is not a flag" turns 8210 into the thing to open — on the one
46
46
  // command whose entire job is opening the right asset.
47
47
  const VALUED_FLAGS = new Set(['--port']);
package/lib/dev.mjs CHANGED
@@ -20,7 +20,7 @@ import { resolvePorts, portInUse, killPort, lanIp } from './ports.mjs';
20
20
  import { assertNoInertOverride, scanPrimitives } from './primitives.mjs';
21
21
  import { buildDevRoomServer, DEV_OVERRIDES_DIR } from './room-server.mjs';
22
22
  import { createFileWatcher, createRoomReloader } from './room-reload.mjs';
23
- import { hasInspectableAssets, inspectorUrl } from './inspect.mjs';
23
+ import { hasDeclaredAssets, assetsPageUrl } from './assets-page.mjs';
24
24
 
25
25
  // Resolve the partykit CLI entry from OUR dependencies (the game never
26
26
  // declares partykit; it rides @looop-games/cli). partykit's `exports` map hides
@@ -287,8 +287,8 @@ export async function dev({ cwd = process.cwd(), port, noMp = process.env.NO_MP
287
287
  // Only when this game declares assets (inspect.mjs). A line printed on every
288
288
  // single boot is a line nobody reads by the third one, and the tool it points
289
289
  // at is then invisible exactly when it would have helped.
290
- if (hasInspectableAssets(project.dir)) {
291
- log(` 🔍 ${inspectorUrl(url)} (your models, sounds and assets, one at a time)`);
290
+ if (hasDeclaredAssets(project.dir)) {
291
+ log(` 🔍 ${assetsPageUrl(url)} (your models, sounds and assets, one at a time)`);
292
292
  }
293
293
  log('────────────────────────────────────────────────────────────');
294
294
 
package/lib/inject.mjs CHANGED
@@ -14,6 +14,11 @@ import { dirname } from 'node:path';
14
14
  export const DEV_IDENTITY = { userId: 'dev-local-user', name: 'Dev', color: '#38bdf8' };
15
15
 
16
16
  export const PLATFORM_URL = '/shared/platform/platform.js';
17
+ // The dev toolbox — injected ONLY here, never by the production serve path
18
+ // (builder/functions/_shared/serve-identity.ts). That separation is the gate
19
+ // that keeps a dev surface off published pages: the production injector never
20
+ // names this module, so there is nothing to strip and nothing to trust.
21
+ export const TOOLBOX_URL = '/shared/ui/toolbox/toolbox.js';
17
22
 
18
23
  // JSON safe for an inline <script>: `<` → < so a value containing
19
24
  // "</script>" can't terminate the script early.
@@ -21,13 +26,22 @@ function jsJson(value) {
21
26
  return JSON.stringify(value).replaceAll('<', '\\u003c');
22
27
  }
23
28
 
24
- export function injectHeadTags(html, slug, { identity = DEV_IDENTITY, platformUrl = PLATFORM_URL } = {}) {
29
+ export function injectHeadTags(html, slug, { identity = DEV_IDENTITY, platformUrl = PLATFORM_URL, toolbox = false, toolboxUrl = TOOLBOX_URL } = {}) {
25
30
  const parts = [];
26
31
  if (slug) parts.push(`<script>window.GAME_SLUG = ${jsJson(slug)};</script>`);
27
32
  parts.push(`<script>window.LOOOP_IDENTITY = ${jsJson(identity)};</script>`);
28
33
  parts.push(
29
34
  `<script type="module">import { installPlatform } from "${platformUrl}"; installPlatform();</script>`,
30
35
  );
36
+ // Dev-only, opt-in per request: the server sets `toolbox` only when the
37
+ // resolved engine bundle carries the module (inject-if-present), so an older
38
+ // pinned engine under a newer CLI never gets a 404ing tag. Behind the
39
+ // platform tag — the boot gate's error handlers come up first.
40
+ if (toolbox) {
41
+ parts.push(
42
+ `<script type="module">import { ensureToolbox } from "${toolboxUrl}"; ensureToolbox();</script>`,
43
+ );
44
+ }
31
45
  const inject = '\n ' + parts.join('\n ');
32
46
  const m = /<head[^>]*>/i.exec(html);
33
47
  if (m) return html.slice(0, m.index + m[0].length) + inject + html.slice(m.index + m[0].length);
@@ -12,7 +12,7 @@ import http from 'node:http';
12
12
  import { EventEmitter } from 'node:events';
13
13
  import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs';
14
14
  import { extname, join, normalize, sep } from 'node:path';
15
- import { injectHeadTags, rewriteHtmlScripts, rewriteJsImports, PLATFORM_URL } from './inject.mjs';
15
+ import { injectHeadTags, rewriteHtmlScripts, rewriteJsImports, PLATFORM_URL, TOOLBOX_URL } from './inject.mjs';
16
16
  import { WHOAMI_PATH } from './ports.mjs';
17
17
 
18
18
  const RELOAD_CLIENT = `<script>
@@ -158,7 +158,7 @@ export function createStaticServer({
158
158
  // the only page that gets the platform layer there. Injecting it into every
159
159
  // HTML file the dev server happens to serve puts the boot gate, the loading
160
160
  // curtain and the M-key menu on top of pages that are not games and cannot
161
- // satisfy them — the shared inspector page (/shared/ui/inspector/) renders
161
+ // satisfy them — the shared inspector page (/shared/ui/assets/) renders
162
162
  // its stage underneath a curtain waiting for an identity it never asked for.
163
163
  const m = /^\/games\/([^/]+)\//.exec(urlPath);
164
164
  if (m && resolveUrl(PLATFORM_URL)) {
@@ -171,7 +171,11 @@ export function createStaticServer({
171
171
  const tabIdentity = as
172
172
  ? { userId: `dev-local-${as}`, name: as, color: '#f472b6' }
173
173
  : identity;
174
- html = injectHeadTags(html, m[1], tabIdentity ? { identity: tabIdentity } : {});
174
+ // The toolbox rides only when the resolved engine carries it — an older
175
+ // pinned engine gets no 404ing tag (it gains the toolbox at `looop
176
+ // update`). Per-request, so a bundle swap mid-session is honoured.
177
+ const toolbox = !!resolveUrl(TOOLBOX_URL);
178
+ html = injectHeadTags(html, m[1], { ...(tabIdentity ? { identity: tabIdentity } : {}), toolbox });
175
179
  }
176
180
  if (injectReload) {
177
181
  html = html.includes('</body>') ? html.replace('</body>', RELOAD_CLIENT + '</body>') : html + RELOAD_CLIENT;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@looop-games/cli",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
4
4
  "description": "Looop game development CLI — dev server, login, and publishing for standalone Looop games.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",