@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.
- package/apps/studio/api.ts +13 -7
- package/apps/studio/bin/screenshot.sh +173 -17
- package/apps/studio/build.ts +37 -3
- package/apps/studio/canvas-build-sandbox.ts +385 -0
- package/apps/studio/canvas-build-worker.ts +85 -0
- package/apps/studio/client/app.jsx +308 -21
- package/apps/studio/client/canvas-url.js +7 -0
- package/apps/studio/client/export-center.jsx +10 -2
- package/apps/studio/client/panels/SettingsPanel.jsx +37 -12
- package/apps/studio/client/report-bug.jsx +125 -30
- package/apps/studio/client/styles/4-components-maude.css +5 -0
- package/apps/studio/client/styles/4-components.css +75 -0
- package/apps/studio/cloud-build.ts +208 -0
- package/apps/studio/collab/room.ts +43 -7
- package/apps/studio/context.ts +12 -5
- package/apps/studio/dist/client.bundle.js +910 -910
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/git/repo-lock.ts +305 -0
- package/apps/studio/git/service.ts +171 -21
- package/apps/studio/http.ts +383 -31
- package/apps/studio/inspect.ts +66 -6
- package/apps/studio/paths.ts +8 -0
- package/apps/studio/server.ts +216 -53
- package/apps/studio/session-scope.ts +101 -0
- package/apps/studio/sync/autocommit.ts +84 -46
- package/apps/studio/sync/index.ts +20 -0
- package/apps/studio/test/canvas-build-sandbox.test.ts +69 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +66 -0
- package/apps/studio/test/canvas-shell-base.test.ts +107 -0
- package/apps/studio/test/canvas-url.test.ts +26 -0
- package/apps/studio/test/cloud-build.test.ts +95 -0
- package/apps/studio/test/cloud-session-role.test.ts +131 -0
- package/apps/studio/test/cloud-shell-surfaces.test.ts +119 -0
- package/apps/studio/test/collab-readonly-gate.test.ts +194 -0
- package/apps/studio/test/config-projection.test.ts +104 -0
- package/apps/studio/test/git-system-engine.test.ts +135 -0
- package/apps/studio/test/read-only-gate.test.ts +58 -0
- package/apps/studio/test/repo-concurrency.test.ts +146 -0
- package/apps/studio/test/repo-lock.test.ts +350 -0
- package/apps/studio/test/server-lifecycle.test.ts +11 -1
- package/apps/studio/test/session-runtime-state.test.ts +226 -0
- package/apps/studio/test/session-scope.test.ts +100 -0
- package/apps/studio/test/shell-importmap.test.ts +8 -1
- package/apps/studio/test/sync-autocommit.test.ts +76 -0
- package/apps/studio/test/sync-hardening.test.ts +75 -0
- package/apps/studio/test/workspace-containment.test.ts +90 -11
- package/apps/studio/use-collab.tsx +21 -1
- package/apps/studio/whats-new.json +47 -2
- package/apps/studio/workspace-mode.ts +161 -28
- package/apps/studio/ws.ts +74 -8
- package/cli/commands/kg.mjs +49 -19
- package/cli/lib/gitignore-block.mjs +3 -0
- package/package.json +11 -9
- package/plugins/design/dependencies.json +1 -1
- package/plugins/design/templates/_shell.html +66 -32
- package/plugins/flow/.claude-plugin/config.schema.json +1 -1
- package/plugins/flow/dependencies.json +1 -1
package/apps/studio/api.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
16
|
-
# (or --all-screens which loops over every
|
|
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
|
-
#
|
|
134
|
-
|
|
135
|
-
#
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/apps/studio/build.ts
CHANGED
|
@@ -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(
|
|
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
|
}
|