@1agh/maude 0.53.1 → 0.54.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 (58) hide show
  1. package/apps/studio/api.ts +13 -7
  2. package/apps/studio/bin/screenshot.sh +173 -17
  3. package/apps/studio/build.ts +37 -3
  4. package/apps/studio/canvas-build-sandbox.ts +385 -0
  5. package/apps/studio/canvas-build-worker.ts +85 -0
  6. package/apps/studio/client/app.jsx +308 -21
  7. package/apps/studio/client/canvas-url.js +7 -0
  8. package/apps/studio/client/export-center.jsx +10 -2
  9. package/apps/studio/client/panels/SettingsPanel.jsx +37 -12
  10. package/apps/studio/client/report-bug.jsx +125 -30
  11. package/apps/studio/client/styles/4-components-maude.css +5 -0
  12. package/apps/studio/client/styles/4-components.css +75 -0
  13. package/apps/studio/cloud-build.ts +208 -0
  14. package/apps/studio/collab/room.ts +43 -7
  15. package/apps/studio/context.ts +12 -5
  16. package/apps/studio/dist/client.bundle.js +910 -910
  17. package/apps/studio/dist/comment-mount.js +2 -2
  18. package/apps/studio/dist/styles.css +1 -1
  19. package/apps/studio/git/repo-lock.ts +305 -0
  20. package/apps/studio/git/service.ts +171 -21
  21. package/apps/studio/http.ts +383 -31
  22. package/apps/studio/inspect.ts +66 -6
  23. package/apps/studio/paths.ts +8 -0
  24. package/apps/studio/server.ts +216 -53
  25. package/apps/studio/session-scope.ts +101 -0
  26. package/apps/studio/sync/autocommit.ts +84 -46
  27. package/apps/studio/sync/index.ts +20 -0
  28. package/apps/studio/test/canvas-build-sandbox.test.ts +69 -0
  29. package/apps/studio/test/canvas-origin-gate.test.ts +66 -0
  30. package/apps/studio/test/canvas-shell-base.test.ts +107 -0
  31. package/apps/studio/test/canvas-url.test.ts +26 -0
  32. package/apps/studio/test/cloud-build.test.ts +95 -0
  33. package/apps/studio/test/cloud-session-role.test.ts +131 -0
  34. package/apps/studio/test/cloud-shell-surfaces.test.ts +119 -0
  35. package/apps/studio/test/collab-readonly-gate.test.ts +194 -0
  36. package/apps/studio/test/config-projection.test.ts +104 -0
  37. package/apps/studio/test/git-system-engine.test.ts +135 -0
  38. package/apps/studio/test/read-only-gate.test.ts +58 -0
  39. package/apps/studio/test/repo-concurrency.test.ts +146 -0
  40. package/apps/studio/test/repo-lock.test.ts +350 -0
  41. package/apps/studio/test/server-lifecycle.test.ts +11 -1
  42. package/apps/studio/test/session-runtime-state.test.ts +226 -0
  43. package/apps/studio/test/session-scope.test.ts +100 -0
  44. package/apps/studio/test/shell-importmap.test.ts +8 -1
  45. package/apps/studio/test/sync-autocommit.test.ts +76 -0
  46. package/apps/studio/test/sync-hardening.test.ts +75 -0
  47. package/apps/studio/test/workspace-containment.test.ts +90 -11
  48. package/apps/studio/use-collab.tsx +21 -1
  49. package/apps/studio/whats-new.json +47 -2
  50. package/apps/studio/workspace-mode.ts +161 -28
  51. package/apps/studio/ws.ts +74 -8
  52. package/cli/commands/kg.mjs +49 -19
  53. package/cli/lib/gitignore-block.mjs +3 -0
  54. package/package.json +11 -9
  55. package/plugins/design/dependencies.json +1 -1
  56. package/plugins/design/templates/_shell.html +66 -32
  57. package/plugins/flow/.claude-plugin/config.schema.json +1 -1
  58. package/plugins/flow/dependencies.json +1 -1
@@ -98,6 +98,7 @@ import { createHistory } from './history.ts';
98
98
  import { clearLocatorSlug, readLocator, writeLocator } from './locator.ts';
99
99
  import { STICKERS_DIR } from './paths.ts';
100
100
  import { getPaperPreset, MAX_PRINT_MM } from './print/units.ts';
101
+ import { sessionDir } from './session-scope.ts';
101
102
 
102
103
  // Directories that never hold user-facing canvases. Exported so the
103
104
  // external-canvas watcher (`canvas-list-watch.ts`) shares one source instead of
@@ -1324,8 +1325,11 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
1324
1325
 
1325
1326
  // ---------- Canvas state ----------
1326
1327
 
1328
+ // Cloud Phase 27 D3 — one member's place in the project is not another's.
1329
+ // `sessionDir` is a no-op without an ambient session, so a desktop resolves
1330
+ // the identical path it always did.
1327
1331
  function canvasStatePath(file: string): string {
1328
- return path.join(paths.canvasStateDir, `${fileSlug(file)}.json`);
1332
+ return path.join(sessionDir(paths.canvasStateDir), `${fileSlug(file)}.json`);
1329
1333
  }
1330
1334
 
1331
1335
  async function loadCanvasState(file: string) {
@@ -1468,7 +1472,9 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
1468
1472
  * canvas-ext). Returns null when `file` is not a valid canvas path. */
1469
1473
  function canvasViewPath(file: string): string | null {
1470
1474
  if (!canvasMetaPath(file)) return null; // reuse the containment + ext gate
1471
- return path.join(paths.canvasStateDir, `${fileSlug(file)}.view.json`);
1475
+ // D3 again — the camera is the other per-machine singleton two members in
1476
+ // one cell were silently sharing.
1477
+ return path.join(sessionDir(paths.canvasStateDir), `${fileSlug(file)}.view.json`);
1472
1478
  }
1473
1479
 
1474
1480
  const MAX_OVERLAY_KEYS = 32;
@@ -1572,7 +1578,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
1572
1578
  const vp = normalizeViewport(viewport);
1573
1579
  if (!vp) return null;
1574
1580
  try {
1575
- await mkdir(paths.canvasStateDir, { recursive: true });
1581
+ await mkdir(sessionDir(paths.canvasStateDir), { recursive: true });
1576
1582
  const current = await readCanvasViewRaw(file);
1577
1583
  await Bun.write(viewAbs, `${JSON.stringify({ ...current, viewport: vp }, null, 2)}\n`);
1578
1584
  return vp;
@@ -1597,7 +1603,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
1597
1603
  const ov = normalizeOverlays(overlays);
1598
1604
  if (ov === null) return null;
1599
1605
  try {
1600
- await mkdir(paths.canvasStateDir, { recursive: true });
1606
+ await mkdir(sessionDir(paths.canvasStateDir), { recursive: true });
1601
1607
  const current = await readCanvasViewRaw(file);
1602
1608
  const prevOverlays =
1603
1609
  current.overlays && typeof current.overlays === 'object' && !Array.isArray(current.overlays)
@@ -1710,7 +1716,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
1710
1716
  try {
1711
1717
  const current = await readCanvasViewRaw(file);
1712
1718
  const { overlays: _drop, ...rest } = current;
1713
- await mkdir(paths.canvasStateDir, { recursive: true });
1719
+ await mkdir(sessionDir(paths.canvasStateDir), { recursive: true });
1714
1720
  await Bun.write(viewAbs, `${JSON.stringify(rest, null, 2)}\n`);
1715
1721
  } catch {
1716
1722
  /* best-effort clear */
@@ -1732,12 +1738,12 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
1732
1738
  const current = await readCanvasViewRaw(file);
1733
1739
  if (patch.locked === null) {
1734
1740
  const { locked: _drop, ...rest } = current;
1735
- await mkdir(paths.canvasStateDir, { recursive: true });
1741
+ await mkdir(sessionDir(paths.canvasStateDir), { recursive: true });
1736
1742
  await Bun.write(viewAbs, `${JSON.stringify(rest, null, 2)}\n`);
1737
1743
  } else {
1738
1744
  const lk = normalizeLocked(patch.locked);
1739
1745
  if (lk !== null) {
1740
- await mkdir(paths.canvasStateDir, { recursive: true });
1746
+ await mkdir(sessionDir(paths.canvasStateDir), { recursive: true });
1741
1747
  await Bun.write(viewAbs, `${JSON.stringify({ ...current, locked: lk }, null, 2)}\n`);
1742
1748
  }
1743
1749
  }
@@ -6,14 +6,29 @@
6
6
  #
7
7
  # Usage:
8
8
  # screenshot.sh [--port N | --url URL]
9
- # [--screen <id> | --element <id> | --selector <css> | --full]
9
+ # [--screen <id> | --element <id> | --selector <css> | --full | --shell]
10
10
  # [--all-screens] [--out <path>] [--out-dir <dir>]
11
11
  # [--timeout 8] [--engine auto|agent-browser|playwright]
12
- # [--theme <name>] [--root <repo>]
12
+ # [--theme <name>] [--root <repo>] [--canvas <rel|"">]
13
13
  #
14
14
  # Notes:
15
- # - Exactly one of --screen / --element / --selector / --full is required
16
- # (or --all-screens which loops over every [data-dc-screen]/[data-dc-slot]).
15
+ # - Exactly one of --screen / --element / --selector / --full / --shell is
16
+ # required (or --all-screens, which loops over every
17
+ # [data-dc-screen]/[data-dc-slot]).
18
+ # - --shell captures the STUDIO ITSELF, not a canvas: it points at the server
19
+ # root (`/`) instead of `_canvas-shell.html`, so Maude's own chrome —
20
+ # menubar, sidebar, status bar, toasts — is in frame. That chrome is where
21
+ # most reported UI bugs actually live, and no canvas-scoped capture can ever
22
+ # show it. Open tabs are `useState([])` in the client (never restored from
23
+ # `_active.json` or localStorage), so a bare load lands on "Nothing open
24
+ # yet"; shell mode therefore clicks the active canvas's file-tree row first
25
+ # to bring the shot back in line with what the user is looking at.
26
+ # - --canvas says WHICH canvas shell mode opens, overriding `_active.json`.
27
+ # Passing it EMPTY means "open nothing" — that's not the same as omitting
28
+ # it. `_active.json` is global and sticky (it outlives every closed tab and
29
+ # any session can write it), so a caller that knows the truth — the studio
30
+ # client knows its own open tab — must be able to say "nothing is open"
31
+ # instead of having a stale path resolved behind its back.
17
32
  # - --out required for single-shot modes; --out-dir required for --all-screens.
18
33
  # - --theme <name> forces every `[data-theme]` element (DS artboard wrappers)
19
34
  # to that value BEFORE capture, via a DOM eval — does not touch the actual
@@ -37,6 +52,11 @@ ENGINE="auto"
37
52
  ALL_SCREENS=0
38
53
  ROOT=""
39
54
  THEME=""
55
+ ACTIVE=""
56
+ # Tri-state for --canvas: unset → resolve from _active.json (the CLI default);
57
+ # set to a path → open that; set to "" → open nothing. The empty case is
58
+ # load-bearing, so it can't collapse into "unset".
59
+ CANVAS_SET=0
40
60
 
41
61
  while [ $# -gt 0 ]; do
42
62
  case "$1" in
@@ -44,6 +64,8 @@ while [ $# -gt 0 ]; do
44
64
  --element) MODE="element"; SEL="$2"; shift 2 ;;
45
65
  --selector) MODE="selector"; SEL="$2"; shift 2 ;;
46
66
  --full) MODE="full"; shift ;;
67
+ --shell) MODE="shell"; shift ;;
68
+ --canvas) CANVAS_SET=1; ACTIVE="$2"; shift 2 ;;
47
69
  --all-screens) ALL_SCREENS=1; shift ;;
48
70
  --url) URL="$2"; shift 2 ;;
49
71
  --port) PORT="$2"; shift 2 ;;
@@ -82,7 +104,7 @@ fi
82
104
  if [ $ALL_SCREENS -eq 1 ]; then
83
105
  [ -z "$OUT_DIR" ] && { echo "screenshot.sh: --all-screens needs --out-dir" >&2; exit 2; }
84
106
  else
85
- [ -z "$MODE" ] && { echo "screenshot.sh: pick one of --full/--screen/--element/--selector or --all-screens" >&2; exit 2; }
107
+ [ -z "$MODE" ] && { echo "screenshot.sh: pick one of --full/--shell/--screen/--element/--selector or --all-screens" >&2; exit 2; }
86
108
  [ -z "$OUT" ] && { echo "screenshot.sh: --out required for single-shot modes" >&2; exit 2; }
87
109
  fi
88
110
 
@@ -125,26 +147,58 @@ if [ -z "$URL" ]; then
125
147
  fi
126
148
  [ -z "$PORT" ] && { echo "screenshot.sh: no --url/--port given and _server.json not found (run server-up.sh)" >&2; exit 1; }
127
149
 
128
- if [ -f "$ACTIVE_JSON" ] && command -v jq >/dev/null 2>&1; then
150
+ if [ $CANVAS_SET -eq 0 ] && [ -f "$ACTIVE_JSON" ] && command -v jq >/dev/null 2>&1; then
129
151
  ACTIVE=$(jq -r '.active // empty' "$ACTIVE_JSON" 2>/dev/null)
130
152
  fi
131
- [ -z "$ACTIVE" ] && { echo "screenshot.sh: no active canvas in _active.json (open one in browser first)" >&2; exit 1; }
132
153
 
133
- # URL-encode spaces (rough); leave other chars alone.
134
- ACTIVE_ENC=$(printf '%s' "$ACTIVE" | sed 's/ /%20/g')
135
- # Canvases mount through the canvas shell. The bare `/<rel>` route 404s when
136
- # the canvas-origin sandbox is on (default since phase-9.1); only
137
- # `/_canvas-shell.html?canvas=<rel>` renders the canvas (valid in both
138
- # split-on and legacy same-origin modes).
139
- URL="http://localhost:${PORT}/_canvas-shell.html?canvas=${ACTIVE_ENC}"
154
+ # Shell mode targets the studio root, and an active canvas is a NICE-TO-HAVE
155
+ # (it decides which file-tree row we click) rather than a precondition — a
156
+ # chrome bug is worth capturing even with nothing open.
157
+ if [ "$MODE" = "shell" ]; then
158
+ URL="http://localhost:${PORT}/"
159
+ else
160
+ [ -z "$ACTIVE" ] && { echo "screenshot.sh: no active canvas in _active.json (open one in browser first)" >&2; exit 1; }
161
+
162
+ # URL-encode spaces (rough); leave other chars alone.
163
+ ACTIVE_ENC=$(printf '%s' "$ACTIVE" | sed 's/ /%20/g')
164
+ # Canvases mount through the canvas shell. The bare `/<rel>` route 404s when
165
+ # the canvas-origin sandbox is on (default since phase-9.1); only
166
+ # `/_canvas-shell.html?canvas=<rel>` renders the canvas (valid in both
167
+ # split-on and legacy same-origin modes).
168
+ URL="http://localhost:${PORT}/_canvas-shell.html?canvas=${ACTIVE_ENC}"
169
+ fi
140
170
  fi
141
171
 
172
+ # Shell mode still wants the active canvas even when the caller passed --url
173
+ # outright (the URL says WHERE to look; the active canvas says WHAT to open).
174
+ if [ "$MODE" = "shell" ] && [ $CANVAS_SET -eq 0 ] && [ -z "$ACTIVE" ]; then
175
+ SHELL_REPO="${ROOT:-${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}}"
176
+ if [ -f "$SHELL_REPO/.design/_active.json" ] && command -v jq >/dev/null 2>&1; then
177
+ ACTIVE=$(jq -r '.active // empty' "$SHELL_REPO/.design/_active.json" 2>/dev/null)
178
+ fi
179
+ fi
180
+
181
+ # The file-tree row testid the client stamps (app.jsx `pathTestIdSlug`):
182
+ # designRoot dot-folder stripped, extension stripped, non-alphanumerics folded
183
+ # to single dashes, lowercased, dashes trimmed. Lowercasing happens BEFORE the
184
+ # extension strip so we don't need sed's non-portable `I` flag for `.TSX`.
185
+ # Output is [a-z0-9-] only, which is what makes it safe to splice into the JS
186
+ # selector string eval'd below.
187
+ shell_row_testid() {
188
+ printf '%s' "$1" \
189
+ | sed -E 's#^\.[^/]+/##' \
190
+ | tr '[:upper:]' '[:lower:]' \
191
+ | sed -E 's/\.(tsx|html?)$//' \
192
+ | sed -E 's/[^a-z0-9]+/-/g' \
193
+ | sed -E 's/^-+//; s/-+$//'
194
+ }
195
+
142
196
  # ---------- selector mapping ----------
143
197
  case "$MODE" in
144
198
  screen) CSS_SEL="[data-dc-screen=\"$SEL\"], [data-dc-slot=\"$SEL\"]" ;;
145
199
  element) CSS_SEL="[data-dc-element=\"$SEL\"]" ;;
146
200
  selector) CSS_SEL="$SEL" ;;
147
- full|"") CSS_SEL="" ;;
201
+ full|shell|"") CSS_SEL="" ;;
148
202
  esac
149
203
 
150
204
  # ---------- engine resolution ----------
@@ -153,6 +207,15 @@ esac
153
207
  # would let a same-user attacker shadow node/chrome in the app dir), else the one
154
208
  # on PATH.
155
209
  AB="${MAUDE_AGENT_BROWSER:-agent-browser}"
210
+
211
+ # agent-browser sessions are SHARED by name (default: "default"). Shell mode is
212
+ # fired automatically by the Report-a-Bug dialog, so on the shared session it
213
+ # would navigate whatever browser an agent has mid-task out from under it. Pin
214
+ # it to its own session; the canvas modes keep the shared one deliberately, so
215
+ # an agent's `/design:*` captures stay in the session it is already driving.
216
+ if [ "$MODE" = "shell" ] && [ -z "$AGENT_BROWSER_SESSION" ]; then
217
+ export AGENT_BROWSER_SESSION="maude-shell-shot"
218
+ fi
156
219
  if [ "$ENGINE" = "auto" ]; then
157
220
  if command -v "$AB" >/dev/null 2>&1; then
158
221
  ENGINE="agent-browser"
@@ -222,9 +285,94 @@ pw_screenshot() {
222
285
  return 0
223
286
  }
224
287
 
288
+ # ---------- shell mode: open the active canvas in the studio ----------
289
+ # The studio root always boots to "Nothing open yet" (tabs are `useState([])`,
290
+ # never rehydrated), so a bare shell shot would show chrome over an empty
291
+ # canvas area. Click the active canvas's file-tree row and wait for the canvas
292
+ # iframe to appear. Every leg is best-effort — chrome is the point of this mode,
293
+ # so a missing row or a slow mount degrades to "chrome, nothing open" rather
294
+ # than failing the capture.
295
+ # A reachable port is not a mounted app: the server can answer while the client
296
+ # bundle is still parsing, or React can fail to mount and leave an empty #root
297
+ # (the exact shape of the v0.51.1 blank-window bug). Wait for the studio's own
298
+ # file list before capturing, so a broken shell fails loud instead of shipping a
299
+ # blank PNG that looks like a real screenshot.
300
+ await_studio_shell() {
301
+ [ "$ENGINE" = "agent-browser" ] || return 0
302
+ local poll=0
303
+ while [ $poll -lt "$TIMEOUT" ]; do
304
+ local n
305
+ n=$("$AB" eval "document.querySelectorAll('[data-testid^=\"canvas-row-\"]').length" 2>/dev/null | tr -d '[:space:]')
306
+ case "$n" in
307
+ ''|*[!0-9]*|0) ;;
308
+ *) return 0 ;;
309
+ esac
310
+ sleep 1
311
+ poll=$((poll + 1))
312
+ done
313
+ return 1
314
+ }
315
+
316
+ open_active_in_shell() {
317
+ [ "$ENGINE" = "agent-browser" ] || return 0
318
+ [ -n "$ACTIVE" ] || { echo "→ shell: no active canvas — capturing chrome as-is" >&2; return 0; }
319
+ local slug
320
+ slug=$(shell_row_testid "$ACTIVE")
321
+ [ -n "$slug" ] || return 0
322
+ # The file tree is React-rendered, so the row does not exist at load — poll
323
+ # for it (a single probe 1 s after navigate reliably misses) and click the
324
+ # moment it appears.
325
+ local clicked=""
326
+ local wait=0
327
+ while [ $wait -lt "$TIMEOUT" ]; do
328
+ clicked=$("$AB" eval "(function(){var r=document.querySelector('[data-testid=\"canvas-row-$slug\"]');if(!r)return 'miss';r.click();return 'hit'})()" 2>/dev/null | tr -d '[:space:]"')
329
+ [ "$clicked" = "hit" ] && break
330
+ sleep 1
331
+ wait=$((wait + 1))
332
+ done
333
+ if [ "$clicked" != "hit" ]; then
334
+ echo "→ shell: no file-tree row for '$slug' after ${TIMEOUT}s — capturing chrome as-is" >&2
335
+ return 0
336
+ fi
337
+ # Wait for the canvas iframe to attach; it renders cross-origin, so the parent
338
+ # can only observe the element, never its contents. A short settle follows so
339
+ # the iframe has painted before we capture.
340
+ local poll=0
341
+ while [ $poll -lt "$TIMEOUT" ]; do
342
+ sleep 1
343
+ poll=$((poll + 1))
344
+ local n
345
+ n=$("$AB" eval "document.querySelectorAll('iframe').length" 2>/dev/null | tr -d '[:space:]')
346
+ case "$n" in
347
+ ''|*[!0-9]*|0) continue ;;
348
+ *) sleep 2; echo "→ shell: opened '$ACTIVE'" >&2; return 0 ;;
349
+ esac
350
+ done
351
+ echo "→ shell: canvas iframe never attached after ${TIMEOUT}s — capturing chrome as-is" >&2
352
+ }
353
+
225
354
  navigate_once() {
226
355
  if [ "$ENGINE" = "agent-browser" ]; then
227
- "$AB" open "$URL" >&2
356
+ # A failed navigation MUST propagate. agent-browser leaves the session on a
357
+ # blank page and `screenshot` then happily writes a ~4 kB white PNG, so
358
+ # ignoring this exit code turns "the server is down" into "here is your
359
+ # screenshot" — which for the Report-a-Bug shell shot means a blank image
360
+ # labelled "Maude window" lands in someone's bug report.
361
+ if ! "$AB" open "$URL" >&2; then
362
+ echo "✗ navigation failed: $URL" >&2
363
+ return 1
364
+ fi
365
+ # Shell mode lands on the studio root, which has no DC mount to poll for —
366
+ # the canvas arrives only after we click a row.
367
+ if [ "$MODE" = "shell" ]; then
368
+ if ! await_studio_shell; then
369
+ echo "✗ the studio shell never mounted at $URL" >&2
370
+ return 1
371
+ fi
372
+ open_active_in_shell
373
+ apply_theme_override
374
+ return 0
375
+ fi
228
376
  # Wait for canvas to mount — Babel/React canvases take 2–4s to settle.
229
377
  # Poll for [data-dc-screen] or [data-dc-slot] up to $TIMEOUT seconds; fall
230
378
  # through to a fixed sleep when the page isn't a DC canvas.
@@ -387,7 +535,15 @@ if [ $ALL_SCREENS -eq 1 ]; then
387
535
  fi
388
536
 
389
537
  # ---------- single-shot ----------
390
- navigate_once
538
+ # A failed navigation is exactly the port-bounce case (DDR-094), so give
539
+ # relive_url its one shot before giving up rather than capturing a blank page.
540
+ if ! navigate_once; then
541
+ if ! relive_url || ! navigate_once; then
542
+ echo "✗ could not load $URL — no screenshot written" >&2
543
+ rm -f "$OUT"
544
+ exit 3
545
+ fi
546
+ fi
391
547
  if capture_resilient "$CSS_SEL" "$OUT"; then
392
548
  echo "$OUT"
393
549
  exit 0
@@ -21,6 +21,8 @@ import { fileURLToPath } from 'node:url';
21
21
 
22
22
  import { browserslistToTargets, bundle as lcssBundle } from 'lightningcss';
23
23
 
24
+ import { cloudStubPlugin } from './cloud-build.ts';
25
+
24
26
  const ROOT = dirname(fileURLToPath(import.meta.url));
25
27
  const DIST = join(ROOT, 'dist');
26
28
 
@@ -32,6 +34,9 @@ const MODE: 'dev' | 'release' | 'dry' = ARGS.has('--dry-run')
32
34
  ? 'release'
33
35
  : 'dev';
34
36
  const WATCH = ARGS.has('--watch');
37
+ /** Cloud Phase 27 D1 — build the CELL's variant (secret-bearing surfaces
38
+ * removed from the binary, not merely un-routed). `--cloud` with `--release`. */
39
+ const CLOUD = ARGS.has('--cloud');
35
40
 
36
41
  const PLATFORM_MATRIX = [
37
42
  'bun-darwin-arm64',
@@ -336,11 +341,20 @@ import ${JSON.stringify(realServerRel)};
336
341
  return entryPath;
337
342
  }
338
343
 
339
- async function buildServerBinary(target: PlatformTarget): Promise<{ outPath: string }> {
344
+ async function buildServerBinary(
345
+ target: PlatformTarget,
346
+ /**
347
+ * Cloud Phase 27 D1 — the variant a CELL runs, with the secret-bearing
348
+ * surfaces removed from the binary rather than merely un-routed. Named
349
+ * separately (`maude-cloud-<slug>`) so a desktop release can never pick one
350
+ * up by accident, and so both can be built from one checkout.
351
+ */
352
+ { cloud = false }: { cloud?: boolean } = {}
353
+ ): Promise<{ outPath: string }> {
340
354
  ensureDist();
341
355
  const slug = platformSlug(target);
342
356
  const ext = slug.startsWith('win32') ? '.exe' : '';
343
- const outPath = join(DIST, `maude-${slug}${ext}`);
357
+ const outPath = join(DIST, `maude-${cloud ? 'cloud-' : ''}${slug}${ext}`);
344
358
  const realEntry = join(ROOT, 'server.ts');
345
359
  if (!existsSync(realEntry)) {
346
360
  // Legacy fallback — pre-DDR-009 .mjs path. Doesn't use oxc-parser, so the
@@ -369,6 +383,26 @@ async function buildServerBinary(target: PlatformTarget): Promise<{ outPath: str
369
383
  // filter skipped it (cross-compile case — see ensureBindingForTarget docs).
370
384
  await ensureBindingForTarget(oxcBindingSlug(slug));
371
385
  const entry = writeCompileEntry(target);
386
+
387
+ // The CLOUD variant goes through the Bun.build API rather than the CLI,
388
+ // because that is the only path that takes a plugin — and a plugin is what
389
+ // makes D1 elimination rather than un-routing. `--define` + dead-code
390
+ // elimination was tried first and does not do it: Bun inlines the
391
+ // dynamically-imported module and keeps the branch, so the bytes stay.
392
+ if (cloud) {
393
+ const result = await Bun.build({
394
+ entrypoints: [entry],
395
+ target: 'bun',
396
+ minify: true,
397
+ plugins: [cloudStubPlugin(ROOT)],
398
+ compile: { target, outfile: outPath },
399
+ } as Parameters<typeof Bun.build>[0]);
400
+ if (!result.success) {
401
+ throw new Error(`cloud compile failed for ${target}:\n${result.logs.map(String).join('\n')}`);
402
+ }
403
+ return { outPath };
404
+ }
405
+
372
406
  const proc = Bun.spawn(
373
407
  [
374
408
  'bun',
@@ -538,7 +572,7 @@ async function main() {
538
572
  : [currentTarget()];
539
573
  for (const target of targets) {
540
574
  const t = performance.now();
541
- const bin = await buildServerBinary(target);
575
+ const bin = await buildServerBinary(target, { cloud: CLOUD });
542
576
  console.log(`[build] ${bin.outPath} (${(performance.now() - t).toFixed(0)} ms)`);
543
577
  }
544
578
  }