@bitmagic/cli 0.1.64-dev.9 → 0.1.65-dev.1

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 (48) hide show
  1. package/README.md +28 -15
  2. package/assets/blender-level/README.md +6 -1
  3. package/assets/blender-level/bmlevel/core.py +78 -1
  4. package/assets/blender-level/review.py +106 -11
  5. package/assets/blender-level/tests/test_core.py +49 -0
  6. package/dist/cli.d.ts +8 -0
  7. package/dist/commands/forge.d.ts +8 -2
  8. package/dist/commands/forge.js +10 -4
  9. package/dist/commands/forge.js.map +1 -1
  10. package/dist/commands/generate.js +48 -24
  11. package/dist/commands/generate.js.map +1 -1
  12. package/dist/commands/publish.d.ts +5 -0
  13. package/dist/commands/publish.js +29 -3
  14. package/dist/commands/publish.js.map +1 -1
  15. package/dist/editor/design.d.ts +1 -0
  16. package/dist/editor/design.js +7 -3
  17. package/dist/editor/design.js.map +1 -1
  18. package/dist/editor/game-info-panel.d.ts +33 -0
  19. package/dist/editor/game-info-panel.js +179 -0
  20. package/dist/editor/game-info-panel.js.map +1 -0
  21. package/dist/editor/game-info.d.ts +55 -0
  22. package/dist/editor/game-info.js +126 -0
  23. package/dist/editor/game-info.js.map +1 -0
  24. package/dist/editor/server.js +59 -3
  25. package/dist/editor/server.js.map +1 -1
  26. package/dist/editor/shell-page.js +196 -11
  27. package/dist/editor/shell-page.js.map +1 -1
  28. package/dist/forge/two-d-gate.d.ts +10 -0
  29. package/dist/forge/two-d-gate.js +85 -82
  30. package/dist/forge/two-d-gate.js.map +1 -1
  31. package/dist/ios/collect-urls.d.ts +2 -0
  32. package/dist/ios/collect-urls.js +2 -2
  33. package/dist/ios/collect-urls.js.map +1 -1
  34. package/dist/publish/client.d.ts +18 -0
  35. package/dist/publish/client.js.map +1 -1
  36. package/dist/publish/record.d.ts +9 -1
  37. package/dist/publish/record.js +2 -0
  38. package/dist/publish/record.js.map +1 -1
  39. package/dist/publish/urls.d.ts +29 -0
  40. package/dist/publish/urls.js +39 -0
  41. package/dist/publish/urls.js.map +1 -0
  42. package/dist/scaffold/blender-level-skill-files.d.ts +7 -0
  43. package/dist/scaffold/blender-level-skill-files.js +129 -21
  44. package/dist/scaffold/blender-level-skill-files.js.map +1 -1
  45. package/dist/scaffold/project-files.js +2 -2
  46. package/dist/scaffold/skill-files.js +18 -1
  47. package/dist/scaffold/skill-files.js.map +1 -1
  48. package/package.json +4 -4
@@ -35,6 +35,7 @@ export declare const BM: {
35
35
  readonly shadowDialog: "0 8px 24px rgba(0, 0, 0, 0.5)";
36
36
  readonly shadowCard: "0 8px 8px rgba(0, 0, 0, 0.3)";
37
37
  readonly glowAqua: "0 0 24px rgba(160, 218, 185, 0.45)";
38
+ readonly focusRing: "0 0 0 2px rgba(160, 218, 185, 0.45)";
38
39
  readonly glowPink: "0 0 24px rgba(224, 33, 138, 0.4)";
39
40
  readonly insetBorderDim: "inset 0 0 0 1px #7c7c7c";
40
41
  readonly insetBorderFocus: "inset 0 0 0 1px #ffffff, 0 0 0 2px rgba(160, 218, 185, 0.45)";
@@ -35,6 +35,9 @@ export const BM = {
35
35
  shadowDialog: '0 8px 24px rgba(0, 0, 0, 0.5)',
36
36
  shadowCard: '0 8px 8px rgba(0, 0, 0, 0.3)',
37
37
  glowAqua: '0 0 24px rgba(160, 218, 185, 0.45)',
38
+ /* The keyboard-focus ring every control here wears. A token because a page's own CSS may not
39
+ carry a colour literal (shell-page.test.ts), and a focusable control it adds needs this one. */
40
+ focusRing: '0 0 0 2px rgba(160, 218, 185, 0.45)',
38
41
  glowPink: '0 0 24px rgba(224, 33, 138, 0.4)',
39
42
  insetBorderDim: 'inset 0 0 0 1px #7c7c7c',
40
43
  insetBorderFocus: 'inset 0 0 0 1px #ffffff, 0 0 0 2px rgba(160, 218, 185, 0.45)',
@@ -78,6 +81,7 @@ export const BITMAGIC_TOKENS_CSS = `
78
81
  --bm-shadow-dialog: ${BM.shadowDialog};
79
82
  --bm-shadow-card: ${BM.shadowCard};
80
83
  --bm-glow-aqua: ${BM.glowAqua};
84
+ --bm-focus-ring: ${BM.focusRing};
81
85
  --bm-glow-pink: ${BM.glowPink};
82
86
  --bm-font: ${BM.font};
83
87
  --bm-font-mono: ${BM.fontMono};
@@ -110,7 +114,7 @@ export const BITMAGIC_CONTROLS_CSS = `
110
114
  box-shadow .15s ease, transform .15s ease;
111
115
  }
112
116
  .bm-pill:hover:not(:disabled) { background: var(--bm-surface-card); border-color: var(--bm-text-dim); }
113
- .bm-pill:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(160, 218, 185, .45); }
117
+ .bm-pill:focus-visible { outline: none; box-shadow: var(--bm-focus-ring); }
114
118
  .bm-pill:disabled { opacity: .5; cursor: default; }
115
119
 
116
120
  /* Primary — the one creative action on a surface. Dark text: DESIGN.md forbids white on the
@@ -172,7 +176,7 @@ export const BITMAGIC_CONTROLS_CSS = `
172
176
  transition: background .15s ease, border-color .15s ease, color .15s ease;
173
177
  }
174
178
  .bm-icon-btn:hover:not(:disabled) { background: var(--bm-surface-card); border-color: var(--bm-text-dim); }
175
- .bm-icon-btn:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(160, 218, 185, .45); }
179
+ .bm-icon-btn:focus-visible { outline: none; box-shadow: var(--bm-focus-ring); }
176
180
 
177
181
  /* === Vertical separator (toolbar) ========================================================= */
178
182
  .bm-separator-v { width: 1px; align-self: stretch; margin: 12px 4px; background: var(--bm-border); }
@@ -214,7 +218,7 @@ export const BITMAGIC_CONTROLS_CSS = `
214
218
  }
215
219
  .bm-menu-item:hover:not(:disabled) { background: var(--bm-border-muted); }
216
220
  .bm-menu-item:focus-visible { outline: none; background: var(--bm-border-muted);
217
- box-shadow: 0 0 0 2px rgba(160, 218, 185, .45); }
221
+ box-shadow: var(--bm-focus-ring); }
218
222
  .bm-menu-item:disabled { opacity: .5; cursor: default; }
219
223
 
220
224
  /* === Input ================================================================================ */
@@ -1 +1 @@
1
- {"version":3,"file":"design.js","sourceRoot":"","sources":["../../src/editor/design.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,4FAA4F;AAC5F,MAAM,CAAC,MAAM,EAAE,GAAG;IAChB,EAAE,EAAE,SAAS;IACb,OAAO,EAAE,SAAS;IAClB,UAAU,EAAE,SAAS;IACrB,WAAW,EAAE,SAAS;IAEtB,WAAW,EAAE,SAAS;IACtB,SAAS,EAAE,SAAS;IACpB,OAAO,EAAE,SAAS;IAClB,YAAY,EAAE,SAAS;IAEvB,MAAM,EAAE,SAAS;IACjB,WAAW,EAAE,SAAS;IAEtB,IAAI,EAAE,SAAS;IACf,SAAS,EAAE,SAAS;IACpB,WAAW,EAAE,SAAS;IACtB,IAAI,EAAE,SAAS;IACf,SAAS,EAAE,SAAS;IACpB,WAAW,EAAE,SAAS;IACtB,OAAO,EAAE,SAAS;IAClB,MAAM,EAAE,SAAS;IAEjB,YAAY,EAAE,+BAA+B;IAC7C,UAAU,EAAE,8BAA8B;IAC1C,QAAQ,EAAE,oCAAoC;IAC9C,QAAQ,EAAE,kCAAkC;IAC5C,cAAc,EAAE,yBAAyB;IACzC,gBAAgB,EAAE,8DAA8D;IAEhF,IAAI,EAAE,8EAA8E;IACpF,QAAQ,EAAE,gDAAgD;CAClD,CAAC;AAEX;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;;0HAEwF,CAAC;AAE3H,uFAAuF;AACvF,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;eAGpB,EAAE,CAAC,EAAE;oBACA,EAAE,CAAC,OAAO;wBACN,EAAE,CAAC,UAAU;yBACZ,EAAE,CAAC,WAAW;iBACtB,EAAE,CAAC,WAAW;uBACR,EAAE,CAAC,SAAS;qBACd,EAAE,CAAC,OAAO;mBACZ,EAAE,CAAC,MAAM;yBACH,EAAE,CAAC,WAAW;iBACtB,EAAE,CAAC,IAAI;uBACD,EAAE,CAAC,SAAS;iBAClB,EAAE,CAAC,IAAI;uBACD,EAAE,CAAC,SAAS;oBACf,EAAE,CAAC,OAAO;mBACX,EAAE,CAAC,MAAM;;;;;;;0BAOF,EAAE,CAAC,YAAY;wBACjB,EAAE,CAAC,UAAU;sBACf,EAAE,CAAC,QAAQ;sBACX,EAAE,CAAC,QAAQ;iBAChB,EAAE,CAAC,IAAI;sBACF,EAAE,CAAC,QAAQ;;CAEhC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAqInB,EAAE,CAAC,cAAc;;;;;;;kCAOD,EAAE,CAAC,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsDpD,CAAC"}
1
+ {"version":3,"file":"design.js","sourceRoot":"","sources":["../../src/editor/design.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,4FAA4F;AAC5F,MAAM,CAAC,MAAM,EAAE,GAAG;IAChB,EAAE,EAAE,SAAS;IACb,OAAO,EAAE,SAAS;IAClB,UAAU,EAAE,SAAS;IACrB,WAAW,EAAE,SAAS;IAEtB,WAAW,EAAE,SAAS;IACtB,SAAS,EAAE,SAAS;IACpB,OAAO,EAAE,SAAS;IAClB,YAAY,EAAE,SAAS;IAEvB,MAAM,EAAE,SAAS;IACjB,WAAW,EAAE,SAAS;IAEtB,IAAI,EAAE,SAAS;IACf,SAAS,EAAE,SAAS;IACpB,WAAW,EAAE,SAAS;IACtB,IAAI,EAAE,SAAS;IACf,SAAS,EAAE,SAAS;IACpB,WAAW,EAAE,SAAS;IACtB,OAAO,EAAE,SAAS;IAClB,MAAM,EAAE,SAAS;IAEjB,YAAY,EAAE,+BAA+B;IAC7C,UAAU,EAAE,8BAA8B;IAC1C,QAAQ,EAAE,oCAAoC;IAC9C;sGACkG;IAClG,SAAS,EAAE,qCAAqC;IAChD,QAAQ,EAAE,kCAAkC;IAC5C,cAAc,EAAE,yBAAyB;IACzC,gBAAgB,EAAE,8DAA8D;IAEhF,IAAI,EAAE,8EAA8E;IACpF,QAAQ,EAAE,gDAAgD;CAClD,CAAC;AAEX;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;;0HAEwF,CAAC;AAE3H,uFAAuF;AACvF,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;eAGpB,EAAE,CAAC,EAAE;oBACA,EAAE,CAAC,OAAO;wBACN,EAAE,CAAC,UAAU;yBACZ,EAAE,CAAC,WAAW;iBACtB,EAAE,CAAC,WAAW;uBACR,EAAE,CAAC,SAAS;qBACd,EAAE,CAAC,OAAO;mBACZ,EAAE,CAAC,MAAM;yBACH,EAAE,CAAC,WAAW;iBACtB,EAAE,CAAC,IAAI;uBACD,EAAE,CAAC,SAAS;iBAClB,EAAE,CAAC,IAAI;uBACD,EAAE,CAAC,SAAS;oBACf,EAAE,CAAC,OAAO;mBACX,EAAE,CAAC,MAAM;;;;;;;0BAOF,EAAE,CAAC,YAAY;wBACjB,EAAE,CAAC,UAAU;sBACf,EAAE,CAAC,QAAQ;uBACV,EAAE,CAAC,SAAS;sBACb,EAAE,CAAC,QAAQ;iBAChB,EAAE,CAAC,IAAI;sBACF,EAAE,CAAC,QAAQ;;CAEhC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAqInB,EAAE,CAAC,cAAc;;;;;;;kCAOD,EAAE,CAAC,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsDpD,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The Game-info dialog's CSS and markup, composed into `shell-page.ts`.
3
+ *
4
+ * Split out for the reason `publish-panel.ts` and `asset-detail-panel.ts` were: the shell page is
5
+ * the busiest file in the package. Style and markup carry no closure state, so they can leave
6
+ * without threading anything across a boundary; the behaviour stays inline there, because it
7
+ * needs the enclosing IIFE's element lookups and its `closeMore` / Escape chain.
8
+ *
9
+ * What this surface is for: the bar spelled out the game's name, id and environment side by side
10
+ * and had no room left, while the two things that decide how the whole project LOOKS — the
11
+ * accepted reference image and the cover art — were visible only in a terminal. One click on the
12
+ * name now answers both: what this game is, and what it is supposed to look like.
13
+ *
14
+ * Same template-literal rules as its host: **no backticks**, and a backslash in a regex has to be
15
+ * doubled to survive the literal. Every colour is a `--bm-*` token; `shell-page.test.ts` fails the
16
+ * build on a hex that is not black or white.
17
+ */
18
+ /**
19
+ * Styles for `#info-overlay` and for the bar control that opens it.
20
+ *
21
+ * The trigger is not a pill: a pill is a control, and this is the page's identity — the game's
22
+ * name, in the weight the bar already gave it, that happens to be clickable. Only the hover and
23
+ * focus treatments are added, so it reads as a control the moment a pointer is near it.
24
+ */
25
+ export declare const GAME_INFO_PANEL_CSS = "\n /* The name IS the button. Bold and white like the label it replaces, with the id and env it\n absorbed now one click away rather than two more slots in the bar. */\n #game-info-toggle {\n display: flex;\n align-items: center;\n gap: 6px;\n max-width: 26vw;\n padding: 4px 8px;\n background: transparent;\n border: 1px solid transparent;\n border-radius: 6px;\n color: var(--bm-text);\n font-family: var(--bm-font);\n font-size: 14px;\n font-weight: 700;\n line-height: 1.2;\n cursor: pointer;\n transition: background .15s ease, border-color .15s ease;\n }\n #game-info-toggle:hover { background: var(--bm-surface-alt); border-color: var(--bm-border-muted); }\n #game-info-toggle:focus-visible { outline: none; box-shadow: var(--bm-focus-ring); }\n #game-info-toggle .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n /* The affordance. Dim, so the name still reads as the name rather than as a menu. */\n #game-info-toggle .chevron { flex: none; color: var(--bm-text-dim); font-size: 10px; }\n\n /* Level with the other two dialogs: all three are opened from different controls and never\n stack. */\n #info-overlay { z-index: 10; }\n #info-dialog { width: min(560px, 92vw); max-height: 86vh; overflow-y: auto; padding: 20px; }\n #info-dialog h2 { margin-bottom: 16px; }\n /* A definition list, because that is what this is: a fixed set of fields with one value each.\n Two columns so the values line up and the whole block is scannable without reading it. */\n #info-facts { display: grid; grid-template-columns: auto 1fr; gap: 8px 16px; margin: 0 0 20px; }\n #info-facts dt { font-size: 12px; font-weight: 700; text-transform: uppercase;\n letter-spacing: 1.4px; color: var(--bm-text-muted); }\n #info-facts dd { margin: 0; min-width: 0; color: var(--bm-text); overflow-wrap: anywhere; }\n /* Ids, versions and modes are read character by character; names and genres are read as words. */\n #info-facts dd.mono { font-family: var(--bm-font-mono); font-size: 12px; }\n #info-facts dd .note { color: var(--bm-text-dim); }\n /* The two pictures, side by side: they are the same kind of thing and are compared, not read\n in order. They wrap to one column when the dialog is at its narrowest. */\n #info-art { display: flex; flex-wrap: wrap; gap: 16px; }\n .info-art-card { flex: 1 1 200px; min-width: 0; }\n .info-art-card .bm-label { display: block; margin-bottom: 8px; }\n /* A square frame either way, so a present picture and a missing one leave the dialog the same\n height and the two cards stay level.\n\n A button rather than a div, because with a picture in it that is what it is: the thumbnail is\n a couple of hundred pixels of an image generated at 1024, which is not enough to judge the\n look it is supposed to fix \u2014 clicking it opens the full size. Disabled while the card is\n empty, so the control is dead exactly when there is nothing to open, and reachable by keyboard\n for free either way. */\n .info-art-frame {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n aspect-ratio: 1 / 1;\n padding: 12px;\n background: var(--bm-surface-alt);\n border: 1px solid var(--bm-border-muted);\n border-radius: 6px;\n color: inherit;\n font: inherit;\n text-align: center;\n cursor: zoom-in;\n transition: border-color .15s ease;\n }\n .info-art-frame:hover:not(:disabled) { border-color: var(--bm-text-dim); }\n .info-art-frame:focus-visible { outline: none; box-shadow: var(--bm-focus-ring); }\n /* Not dimmed like an ordinary disabled control: there is nothing to enable here, and greying the\n \"not yet\" copy would hide the command that fixes it. */\n .info-art-frame:disabled { cursor: default; }\n /* The frame holds the picture AND the \"not yet\" copy, and shows exactly one of them: the class\n is applied by openInfo(), because a page assembled as a string has no inline styles to toggle\n (DESIGN.md, and the same rule the Creator follows). */\n .info-art-frame img { display: none; max-width: 100%; max-height: 100%; border-radius: 4px; }\n .info-art-frame.has-image img { display: block; }\n .info-art-frame.has-image .empty { display: none; }\n /* What to run to make the missing one, rather than the bare fact that it is missing. */\n .info-art-frame .empty { font-size: 12px; color: var(--bm-text-dim); }\n .info-art-frame .empty code { display: block; margin-top: 6px; color: var(--bm-aqua);\n font-family: var(--bm-font-mono); }\n .info-art-card .caption { margin-top: 8px; font-size: 12px; color: var(--bm-text-dim);\n overflow-wrap: anywhere; }\n .info-art-card .caption a { color: var(--bm-aqua); text-decoration: none; }\n .info-art-card .caption a:hover { text-decoration: underline; }\n /* An expiring reference URL is the one thing on this card worth interrupting for. */\n .info-art-card .caption .stale { color: var(--bm-pumpkin); }\n /* === The full-size view ===================================================================\n A layer of its own above the dialog rather than a new browser tab: the dev view is the window\n the creator is already in, and a tab showing a bare image on a loopback port is a place they\n then have to come back from. Click anywhere, or Escape, and the dialog is still underneath\n exactly as it was. */\n #info-zoom { z-index: 11; flex-direction: column; gap: 12px; cursor: zoom-out; }\n #info-zoom img { max-width: 92vw; max-height: 84vh; border-radius: 6px;\n box-shadow: var(--bm-shadow-dialog); }\n #info-zoom .hint { font-size: 12px; color: var(--bm-text-muted); }\n\n /* The sidecar could not answer. Said in the dialog rather than in the bar's status line, which\n belongs to the save state and would be overwritten by the next poll. */\n #info-error { display: none; margin: 0 0 16px; font-size: 12px; color: var(--bm-pumpkin); }\n #info-error.show { display: block; }\n #info-actions { display: flex; justify-content: flex-end; margin-top: 20px; }\n";
26
+ /**
27
+ * Markup for the Game-info dialog.
28
+ *
29
+ * Every value is empty until `openInfo()` fills it from `/api/game-info`. Nothing may be
30
+ * interpolated at render time: this page is built once when the server starts, and `bitmagic
31
+ * cover` or `bitmagic reference accept` usually runs hours later in another terminal.
32
+ */
33
+ export declare const GAME_INFO_PANEL_HTML = " <div id=\"info-overlay\" class=\"bm-overlay\">\n <div id=\"info-dialog\" class=\"bm-dialog\">\n <h2 id=\"info-title\"></h2>\n <p id=\"info-error\"></p>\n <dl id=\"info-facts\">\n <dt>Game id</dt><dd id=\"info-game-id\" class=\"mono\"></dd>\n <dt>Environment</dt><dd id=\"info-environment\" class=\"mono\"></dd>\n <dt>Genre</dt><dd id=\"info-genre\"></dd>\n <dt>Physics</dt><dd id=\"info-physics\" class=\"mono\"></dd>\n <dt>Art style</dt><dd id=\"info-art-style\"></dd>\n <dt>Platform</dt><dd id=\"info-platform\"></dd>\n <dt>Engine</dt><dd id=\"info-engine\" class=\"mono\"></dd>\n </dl>\n <div id=\"info-art\">\n <div class=\"info-art-card\">\n <span class=\"bm-label\">Reference image</span>\n <button id=\"info-reference-frame\" class=\"info-art-frame\" type=\"button\" disabled title=\"See it full size\">\n <img id=\"info-reference-img\" alt=\"This game's accepted reference image\">\n <span id=\"info-reference-empty\" class=\"empty\">No reference image yet.<code>bitmagic reference make</code></span>\n </button>\n <p id=\"info-reference-caption\" class=\"caption\"></p>\n </div>\n <div class=\"info-art-card\">\n <span class=\"bm-label\">Cover art</span>\n <button id=\"info-cover-frame\" class=\"info-art-frame\" type=\"button\" disabled title=\"See it full size\">\n <img id=\"info-cover-img\" alt=\"This game's cover art\">\n <span id=\"info-cover-empty\" class=\"empty\">No cover art yet.<code>bitmagic cover</code></span>\n </button>\n <p id=\"info-cover-caption\" class=\"caption\"></p>\n </div>\n </div>\n <div id=\"info-actions\">\n <button id=\"info-close\" class=\"bm-pill bm-pill--primary\" type=\"button\">Close</button>\n </div>\n </div>\n </div>\n <div id=\"info-zoom\" class=\"bm-overlay\">\n <img id=\"info-zoom-img\" alt=\"\">\n <span class=\"hint\">Click anywhere to go back.</span>\n </div>\n";
@@ -0,0 +1,179 @@
1
+ /**
2
+ * The Game-info dialog's CSS and markup, composed into `shell-page.ts`.
3
+ *
4
+ * Split out for the reason `publish-panel.ts` and `asset-detail-panel.ts` were: the shell page is
5
+ * the busiest file in the package. Style and markup carry no closure state, so they can leave
6
+ * without threading anything across a boundary; the behaviour stays inline there, because it
7
+ * needs the enclosing IIFE's element lookups and its `closeMore` / Escape chain.
8
+ *
9
+ * What this surface is for: the bar spelled out the game's name, id and environment side by side
10
+ * and had no room left, while the two things that decide how the whole project LOOKS — the
11
+ * accepted reference image and the cover art — were visible only in a terminal. One click on the
12
+ * name now answers both: what this game is, and what it is supposed to look like.
13
+ *
14
+ * Same template-literal rules as its host: **no backticks**, and a backslash in a regex has to be
15
+ * doubled to survive the literal. Every colour is a `--bm-*` token; `shell-page.test.ts` fails the
16
+ * build on a hex that is not black or white.
17
+ */
18
+ /**
19
+ * Styles for `#info-overlay` and for the bar control that opens it.
20
+ *
21
+ * The trigger is not a pill: a pill is a control, and this is the page's identity — the game's
22
+ * name, in the weight the bar already gave it, that happens to be clickable. Only the hover and
23
+ * focus treatments are added, so it reads as a control the moment a pointer is near it.
24
+ */
25
+ export const GAME_INFO_PANEL_CSS = `
26
+ /* The name IS the button. Bold and white like the label it replaces, with the id and env it
27
+ absorbed now one click away rather than two more slots in the bar. */
28
+ #game-info-toggle {
29
+ display: flex;
30
+ align-items: center;
31
+ gap: 6px;
32
+ max-width: 26vw;
33
+ padding: 4px 8px;
34
+ background: transparent;
35
+ border: 1px solid transparent;
36
+ border-radius: 6px;
37
+ color: var(--bm-text);
38
+ font-family: var(--bm-font);
39
+ font-size: 14px;
40
+ font-weight: 700;
41
+ line-height: 1.2;
42
+ cursor: pointer;
43
+ transition: background .15s ease, border-color .15s ease;
44
+ }
45
+ #game-info-toggle:hover { background: var(--bm-surface-alt); border-color: var(--bm-border-muted); }
46
+ #game-info-toggle:focus-visible { outline: none; box-shadow: var(--bm-focus-ring); }
47
+ #game-info-toggle .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
48
+ /* The affordance. Dim, so the name still reads as the name rather than as a menu. */
49
+ #game-info-toggle .chevron { flex: none; color: var(--bm-text-dim); font-size: 10px; }
50
+
51
+ /* Level with the other two dialogs: all three are opened from different controls and never
52
+ stack. */
53
+ #info-overlay { z-index: 10; }
54
+ #info-dialog { width: min(560px, 92vw); max-height: 86vh; overflow-y: auto; padding: 20px; }
55
+ #info-dialog h2 { margin-bottom: 16px; }
56
+ /* A definition list, because that is what this is: a fixed set of fields with one value each.
57
+ Two columns so the values line up and the whole block is scannable without reading it. */
58
+ #info-facts { display: grid; grid-template-columns: auto 1fr; gap: 8px 16px; margin: 0 0 20px; }
59
+ #info-facts dt { font-size: 12px; font-weight: 700; text-transform: uppercase;
60
+ letter-spacing: 1.4px; color: var(--bm-text-muted); }
61
+ #info-facts dd { margin: 0; min-width: 0; color: var(--bm-text); overflow-wrap: anywhere; }
62
+ /* Ids, versions and modes are read character by character; names and genres are read as words. */
63
+ #info-facts dd.mono { font-family: var(--bm-font-mono); font-size: 12px; }
64
+ #info-facts dd .note { color: var(--bm-text-dim); }
65
+ /* The two pictures, side by side: they are the same kind of thing and are compared, not read
66
+ in order. They wrap to one column when the dialog is at its narrowest. */
67
+ #info-art { display: flex; flex-wrap: wrap; gap: 16px; }
68
+ .info-art-card { flex: 1 1 200px; min-width: 0; }
69
+ .info-art-card .bm-label { display: block; margin-bottom: 8px; }
70
+ /* A square frame either way, so a present picture and a missing one leave the dialog the same
71
+ height and the two cards stay level.
72
+
73
+ A button rather than a div, because with a picture in it that is what it is: the thumbnail is
74
+ a couple of hundred pixels of an image generated at 1024, which is not enough to judge the
75
+ look it is supposed to fix — clicking it opens the full size. Disabled while the card is
76
+ empty, so the control is dead exactly when there is nothing to open, and reachable by keyboard
77
+ for free either way. */
78
+ .info-art-frame {
79
+ display: flex;
80
+ align-items: center;
81
+ justify-content: center;
82
+ width: 100%;
83
+ aspect-ratio: 1 / 1;
84
+ padding: 12px;
85
+ background: var(--bm-surface-alt);
86
+ border: 1px solid var(--bm-border-muted);
87
+ border-radius: 6px;
88
+ color: inherit;
89
+ font: inherit;
90
+ text-align: center;
91
+ cursor: zoom-in;
92
+ transition: border-color .15s ease;
93
+ }
94
+ .info-art-frame:hover:not(:disabled) { border-color: var(--bm-text-dim); }
95
+ .info-art-frame:focus-visible { outline: none; box-shadow: var(--bm-focus-ring); }
96
+ /* Not dimmed like an ordinary disabled control: there is nothing to enable here, and greying the
97
+ "not yet" copy would hide the command that fixes it. */
98
+ .info-art-frame:disabled { cursor: default; }
99
+ /* The frame holds the picture AND the "not yet" copy, and shows exactly one of them: the class
100
+ is applied by openInfo(), because a page assembled as a string has no inline styles to toggle
101
+ (DESIGN.md, and the same rule the Creator follows). */
102
+ .info-art-frame img { display: none; max-width: 100%; max-height: 100%; border-radius: 4px; }
103
+ .info-art-frame.has-image img { display: block; }
104
+ .info-art-frame.has-image .empty { display: none; }
105
+ /* What to run to make the missing one, rather than the bare fact that it is missing. */
106
+ .info-art-frame .empty { font-size: 12px; color: var(--bm-text-dim); }
107
+ .info-art-frame .empty code { display: block; margin-top: 6px; color: var(--bm-aqua);
108
+ font-family: var(--bm-font-mono); }
109
+ .info-art-card .caption { margin-top: 8px; font-size: 12px; color: var(--bm-text-dim);
110
+ overflow-wrap: anywhere; }
111
+ .info-art-card .caption a { color: var(--bm-aqua); text-decoration: none; }
112
+ .info-art-card .caption a:hover { text-decoration: underline; }
113
+ /* An expiring reference URL is the one thing on this card worth interrupting for. */
114
+ .info-art-card .caption .stale { color: var(--bm-pumpkin); }
115
+ /* === The full-size view ===================================================================
116
+ A layer of its own above the dialog rather than a new browser tab: the dev view is the window
117
+ the creator is already in, and a tab showing a bare image on a loopback port is a place they
118
+ then have to come back from. Click anywhere, or Escape, and the dialog is still underneath
119
+ exactly as it was. */
120
+ #info-zoom { z-index: 11; flex-direction: column; gap: 12px; cursor: zoom-out; }
121
+ #info-zoom img { max-width: 92vw; max-height: 84vh; border-radius: 6px;
122
+ box-shadow: var(--bm-shadow-dialog); }
123
+ #info-zoom .hint { font-size: 12px; color: var(--bm-text-muted); }
124
+
125
+ /* The sidecar could not answer. Said in the dialog rather than in the bar's status line, which
126
+ belongs to the save state and would be overwritten by the next poll. */
127
+ #info-error { display: none; margin: 0 0 16px; font-size: 12px; color: var(--bm-pumpkin); }
128
+ #info-error.show { display: block; }
129
+ #info-actions { display: flex; justify-content: flex-end; margin-top: 20px; }
130
+ `;
131
+ /**
132
+ * Markup for the Game-info dialog.
133
+ *
134
+ * Every value is empty until `openInfo()` fills it from `/api/game-info`. Nothing may be
135
+ * interpolated at render time: this page is built once when the server starts, and `bitmagic
136
+ * cover` or `bitmagic reference accept` usually runs hours later in another terminal.
137
+ */
138
+ export const GAME_INFO_PANEL_HTML = ` <div id="info-overlay" class="bm-overlay">
139
+ <div id="info-dialog" class="bm-dialog">
140
+ <h2 id="info-title"></h2>
141
+ <p id="info-error"></p>
142
+ <dl id="info-facts">
143
+ <dt>Game id</dt><dd id="info-game-id" class="mono"></dd>
144
+ <dt>Environment</dt><dd id="info-environment" class="mono"></dd>
145
+ <dt>Genre</dt><dd id="info-genre"></dd>
146
+ <dt>Physics</dt><dd id="info-physics" class="mono"></dd>
147
+ <dt>Art style</dt><dd id="info-art-style"></dd>
148
+ <dt>Platform</dt><dd id="info-platform"></dd>
149
+ <dt>Engine</dt><dd id="info-engine" class="mono"></dd>
150
+ </dl>
151
+ <div id="info-art">
152
+ <div class="info-art-card">
153
+ <span class="bm-label">Reference image</span>
154
+ <button id="info-reference-frame" class="info-art-frame" type="button" disabled title="See it full size">
155
+ <img id="info-reference-img" alt="This game's accepted reference image">
156
+ <span id="info-reference-empty" class="empty">No reference image yet.<code>bitmagic reference make</code></span>
157
+ </button>
158
+ <p id="info-reference-caption" class="caption"></p>
159
+ </div>
160
+ <div class="info-art-card">
161
+ <span class="bm-label">Cover art</span>
162
+ <button id="info-cover-frame" class="info-art-frame" type="button" disabled title="See it full size">
163
+ <img id="info-cover-img" alt="This game's cover art">
164
+ <span id="info-cover-empty" class="empty">No cover art yet.<code>bitmagic cover</code></span>
165
+ </button>
166
+ <p id="info-cover-caption" class="caption"></p>
167
+ </div>
168
+ </div>
169
+ <div id="info-actions">
170
+ <button id="info-close" class="bm-pill bm-pill--primary" type="button">Close</button>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ <div id="info-zoom" class="bm-overlay">
175
+ <img id="info-zoom-img" alt="">
176
+ <span class="hint">Click anywhere to go back.</span>
177
+ </div>
178
+ `;
179
+ //# sourceMappingURL=game-info-panel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"game-info-panel.js","sourceRoot":"","sources":["../../src/editor/game-info-panel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyGlC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwCnC,CAAC"}
@@ -0,0 +1,55 @@
1
+ import type { ProjectMetadata } from '../project/context.js';
2
+ /** The two pictures a project carries. Both are local files under `.bitmagic/`, both re-hostable. */
3
+ export type GameInfoImageKind = 'reference' | 'cover';
4
+ export interface GameInfoImage {
5
+ /** The public URL the record names — where this picture lives for everyone but this machine. */
6
+ url: string;
7
+ /** Whether the sidecar can serve local bytes. False means the dialog falls back to `url`. */
8
+ local: boolean;
9
+ /** When it was accepted (reference) or generated (cover), ISO. */
10
+ at: string;
11
+ /** Reference only: false when re-hosting failed, so `url` is the source and will expire. */
12
+ hosted?: boolean;
13
+ /** Reference only, and only when a generated candidate rather than a bare URL was accepted. */
14
+ candidateId?: string;
15
+ }
16
+ export interface GameInfo {
17
+ gameName: string;
18
+ gameId: string;
19
+ environment: string;
20
+ engineVersion: string;
21
+ template: string;
22
+ /** `gameGenre` from the work copy, falling back to what `bitmagic.json` recorded at scaffold. */
23
+ genre: string;
24
+ /** `physicsMode` from the work copy. Null on the genres that declare none. */
25
+ physicsMode: string | null;
26
+ /** What every generation in this project inherits — `voxel` when game.json declares nothing. */
27
+ artStyle: string;
28
+ /** False when that style is the default rather than a declaration, which is worth saying. */
29
+ artStyleDeclared: boolean;
30
+ /** `desktop` unless the project says otherwise; null when it declares nothing at all. */
31
+ primaryPlatform: string | null;
32
+ reference: GameInfoImage | null;
33
+ cover: GameInfoImage | null;
34
+ }
35
+ export interface GameInfoSource {
36
+ root: string;
37
+ metadata: ProjectMetadata;
38
+ gameId: string;
39
+ gameName: string;
40
+ environment: string;
41
+ }
42
+ export declare function readGameInfo(source: GameInfoSource): GameInfo;
43
+ /**
44
+ * The bytes of one picture, for the dialog's thumbnail.
45
+ *
46
+ * Served from disk rather than linked to the public URL because the local copy is the one that is
47
+ * always there: an accepted reference whose re-host failed carries an Asset Forger URL that
48
+ * expires, and a cover's URL is on whichever environment generated it. The content type is
49
+ * sniffed rather than taken from the extension — both files are written as `.webp` whatever the
50
+ * generator actually returned.
51
+ */
52
+ export declare function readGameInfoImage(root: string, kind: GameInfoImageKind): {
53
+ bytes: Buffer;
54
+ contentType: string;
55
+ } | null;
@@ -0,0 +1,126 @@
1
+ /**
2
+ * What this project IS — the facts the dev view's bar used to spell out, plus the two pictures
3
+ * that decide how it looks.
4
+ *
5
+ * The bar carried the game's name, its id and its environment as three separate slots in a row
6
+ * that had run out of them. They answer one question — "which game is this?" — so they collapsed
7
+ * behind the name, and the dialog that opens has room for the rest of the answer: the genre and
8
+ * physics the engine boots with, the art style every generation inherits, and the accepted
9
+ * reference and cover art. The reference in particular had no window anywhere: `bitmagic
10
+ * reference show` draws it in a terminal, which is not where the creator is looking.
11
+ *
12
+ * Read on every open, never captured at startup. `bitmagic dev` runs for hours and the commands
13
+ * that write these files (`reference accept`, `cover`) run in another terminal — often an agent's
14
+ * — so anything cached here would answer with the project as it was this morning.
15
+ *
16
+ * Best effort throughout, exactly like every other reader of these files: a missing or corrupt
17
+ * record reads as "none yet", never as an error. This is a window onto the project, and a window
18
+ * must not be the thing that fails.
19
+ */
20
+ import * as fs from 'fs';
21
+ import * as path from 'path';
22
+ import { DEFAULT_ART_STYLE } from '@bitmagic/asset-core/art-style';
23
+ import { readArtStyleDeclaration } from '../project/art-style.js';
24
+ import { readPlatformDeclaration } from '../project/platform-declaration.js';
25
+ import { readAcceptedReference, referenceDirPath } from '../project/reference.js';
26
+ import { sniffImageContentType } from '../project/reference-file.js';
27
+ import { coverImagePath, readCoverRecord } from '../commands/cover.js';
28
+ import { aliasSourceDir } from '../scaffold/aliases.js';
29
+ /** One work-copy field, or undefined when the file is missing, unreadable or not an object. */
30
+ function workGameJson(root) {
31
+ const file = path.join(root, aliasSourceDir('work'), 'game.json');
32
+ let parsed;
33
+ try {
34
+ parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
40
+ ? parsed
41
+ : null;
42
+ }
43
+ function stringField(source, key) {
44
+ const value = source?.[key];
45
+ return typeof value === 'string' && value !== '' ? value : null;
46
+ }
47
+ /**
48
+ * The local copy of one of the two pictures, when there is one.
49
+ *
50
+ * The reference's path comes out of a JSON file, so it is checked against the directory it is
51
+ * supposed to be in rather than trusted: this server serves whatever it returns, and
52
+ * `.bitmagic/reference.json` is a file a creator can hand-edit.
53
+ */
54
+ function localImagePath(root, kind) {
55
+ if (kind === 'cover') {
56
+ const file = coverImagePath(root);
57
+ return fs.existsSync(file) ? file : null;
58
+ }
59
+ const accepted = readAcceptedReference(root);
60
+ if (!accepted?.imagePath)
61
+ return null;
62
+ const file = path.resolve(root, accepted.imagePath);
63
+ if (!file.startsWith(referenceDirPath(root) + path.sep))
64
+ return null;
65
+ return fs.existsSync(file) ? file : null;
66
+ }
67
+ function referenceImage(root) {
68
+ const accepted = readAcceptedReference(root);
69
+ if (!accepted)
70
+ return null;
71
+ return {
72
+ url: accepted.url,
73
+ local: localImagePath(root, 'reference') !== null,
74
+ at: accepted.acceptedAt,
75
+ hosted: accepted.hosted,
76
+ ...(accepted.candidateId !== undefined ? { candidateId: accepted.candidateId } : {}),
77
+ };
78
+ }
79
+ function coverImage(root) {
80
+ const record = readCoverRecord(root);
81
+ if (!record)
82
+ return null;
83
+ return { url: record.url, local: localImagePath(root, 'cover') !== null, at: record.generatedAt };
84
+ }
85
+ export function readGameInfo(source) {
86
+ const game = workGameJson(source.root);
87
+ const style = readArtStyleDeclaration(source.root);
88
+ const platform = readPlatformDeclaration(source.root);
89
+ return {
90
+ gameName: source.gameName,
91
+ gameId: source.gameId,
92
+ environment: source.environment,
93
+ engineVersion: source.metadata.engineVersion,
94
+ template: source.metadata.template,
95
+ genre: stringField(game, 'gameGenre') ?? source.metadata.genre,
96
+ physicsMode: stringField(game, 'physicsMode'),
97
+ artStyle: style.artStyle ?? DEFAULT_ART_STYLE,
98
+ artStyleDeclared: style.artStyle !== undefined,
99
+ primaryPlatform: platform.primaryPlatform ?? null,
100
+ reference: referenceImage(source.root),
101
+ cover: coverImage(source.root),
102
+ };
103
+ }
104
+ /**
105
+ * The bytes of one picture, for the dialog's thumbnail.
106
+ *
107
+ * Served from disk rather than linked to the public URL because the local copy is the one that is
108
+ * always there: an accepted reference whose re-host failed carries an Asset Forger URL that
109
+ * expires, and a cover's URL is on whichever environment generated it. The content type is
110
+ * sniffed rather than taken from the extension — both files are written as `.webp` whatever the
111
+ * generator actually returned.
112
+ */
113
+ export function readGameInfoImage(root, kind) {
114
+ const file = localImagePath(root, kind);
115
+ if (file === null)
116
+ return null;
117
+ let bytes;
118
+ try {
119
+ bytes = fs.readFileSync(file);
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ return { bytes, contentType: sniffImageContentType(bytes)?.contentType ?? 'image/webp' };
125
+ }
126
+ //# sourceMappingURL=game-info.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"game-info.js","sourceRoot":"","sources":["../../src/editor/game-info.ts"],"names":[],"mappings":"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"}
@@ -25,10 +25,12 @@ import { applyModificationsToWorld } from '../forge/apply-modifications.js';
25
25
  import { readProjectGameData } from '../forge/browser-host.js';
26
26
  import { projectWorldJsonPath } from '../forge/run-pipeline.js';
27
27
  import { renderEditorShell } from './shell-page.js';
28
+ import { readGameInfo, readGameInfoImage } from './game-info.js';
28
29
  import { createCliUpdateReader } from './cli-update.js';
29
30
  import { renderAssetPreviewPage } from './asset-preview-page.js';
30
31
  import { renderAnimationPreviewPage } from './animation-preview-page.js';
31
32
  import { readPublishRecord } from '../publish/record.js';
33
+ import { promotedPublishUrl } from '../publish/urls.js';
32
34
  import { encodeQr } from '../render/qr.js';
33
35
  import { encodeQrPng } from '../render/qr-png.js';
34
36
  import { buildSceneModifications } from './save.js';
@@ -1040,6 +1042,56 @@ export async function startEditorServer(options) {
1040
1042
  }
1041
1043
  return;
1042
1044
  }
1045
+ /**
1046
+ * Who this project is, and what it is supposed to look like — the bar's name button opens
1047
+ * this.
1048
+ *
1049
+ * Read per request rather than captured at startup for the same reason `/api/state` reads
1050
+ * the publish record every poll: `bitmagic dev` runs for hours, and `bitmagic cover` or
1051
+ * `bitmagic reference accept` almost always runs later, in another terminal — often an
1052
+ * agent's. Cheap enough not to need a poll of its own: nothing reads it until the creator
1053
+ * opens the dialog.
1054
+ */
1055
+ if (req.method === 'GET' && url === '/api/game-info') {
1056
+ sendJson(res, 200, {
1057
+ ok: true,
1058
+ info: readGameInfo({
1059
+ root: options.root,
1060
+ metadata: options.metadata,
1061
+ gameId: options.gameId,
1062
+ gameName: options.gameName,
1063
+ environment: options.environment,
1064
+ }),
1065
+ });
1066
+ return;
1067
+ }
1068
+ /**
1069
+ * The local copy of the reference or the cover, for the dialog's thumbnails.
1070
+ *
1071
+ * Local bytes rather than the public URL, because the local copy is the one that is always
1072
+ * reachable: a reference whose re-host failed carries an expiring Asset Forger URL, and a
1073
+ * cover's URL lives on whichever environment generated it. No caching — `bitmagic cover`
1074
+ * and `reference accept` rewrite these paths in place, and a browser holding the previous
1075
+ * picture would show a creator the art they just replaced.
1076
+ */
1077
+ if (req.method === 'GET' && url === '/api/game-info/image') {
1078
+ const kind = query.get('kind');
1079
+ if (kind !== 'reference' && kind !== 'cover') {
1080
+ sendJson(res, 400, { ok: false, error: 'kind must be reference or cover' });
1081
+ return;
1082
+ }
1083
+ const image = readGameInfoImage(options.root, kind);
1084
+ if (image === null) {
1085
+ sendJson(res, 404, { ok: false, error: `No local ${kind} image in this project` });
1086
+ return;
1087
+ }
1088
+ res.writeHead(200, {
1089
+ 'Content-Type': image.contentType,
1090
+ 'Content-Length': image.bytes.length,
1091
+ 'Cache-Control': 'no-store',
1092
+ }).end(image.bytes);
1093
+ return;
1094
+ }
1043
1095
  // Long-lived: the shell holds this open for its whole session. Must be checked before any
1044
1096
  // route that would end the response.
1045
1097
  if (req.method === 'GET' && url === '/api/events/stream') {
@@ -1237,13 +1289,17 @@ export async function startEditorServer(options) {
1237
1289
  */
1238
1290
  if (req.method === 'GET' && url === '/api/publish/qr.png') {
1239
1291
  const record = readPublishRecord(options.root);
1240
- const matrix = record === null ? null : encodeQr(record.url);
1292
+ // The shareable URL, matching both the panel's link and what the publishing terminal
1293
+ // drew — see `promotedPublishUrl`. Encoding `record.url` here would put a phone on one
1294
+ // frozen build while the link beside the code pointed at the newest.
1295
+ const matrix = record === null ? null : encodeQr(promotedPublishUrl(record));
1241
1296
  if (matrix === null) {
1242
1297
  sendJson(res, 404, { ok: false, error: 'This project has not been published yet.' });
1243
1298
  return;
1244
1299
  }
1245
- // No caching: publishing again keeps the same URL for an alias publish but not for a
1246
- // versioned one, and a stale code would send a phone to the previous build.
1300
+ // No caching: the shareable URL is stable across ordinary re-publishes, but a publish
1301
+ // that flips visibility moves it between the `/play/` page and the `/games/` form, and a
1302
+ // stale code would send a phone to the one that is no longer right.
1247
1303
  res.writeHead(200, {
1248
1304
  'Content-Type': 'image/png',
1249
1305
  'Cache-Control': 'no-store',