@1agh/maude 0.60.6 → 1.0.2
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/acp/index.ts +1 -0
- package/apps/studio/ai-banner.tsx +1 -0
- package/apps/studio/annotations-context-toolbar.tsx +3 -1
- package/apps/studio/annotations-layer.tsx +33 -16
- package/apps/studio/api.ts +108 -18
- package/apps/studio/artboard-guides-overlay.tsx +5 -1
- package/apps/studio/assets-s3.ts +6 -1
- package/apps/studio/bin/_import-figma.mjs +8 -3
- package/apps/studio/build.ts +1 -1
- package/apps/studio/canvas-artifacts.ts +21 -0
- package/apps/studio/canvas-build.ts +14 -9
- package/apps/studio/canvas-comment-mount.tsx +27 -30
- package/apps/studio/canvas-icons.tsx +1 -1
- package/apps/studio/canvas-lib.tsx +94 -5
- package/apps/studio/canvas-list-watch.ts +14 -1
- package/apps/studio/canvas-shell.tsx +8 -0
- package/apps/studio/client/app.jsx +224 -45
- package/apps/studio/client/panels/GitPanel.jsx +121 -13
- package/apps/studio/client/panels/SettingsPanel.jsx +1 -1
- package/apps/studio/client/panels/SyncConsentDialog.jsx +182 -0
- package/apps/studio/client/panels/SyncPanel.jsx +595 -1
- package/apps/studio/client/styles/3-shell-maude.css +38 -0
- package/apps/studio/clip-ops.ts +8 -1
- package/apps/studio/cloud/endpoints.ts +265 -3
- package/apps/studio/collab/origins.ts +3 -1
- package/apps/studio/comments-overlay.tsx +5 -0
- package/apps/studio/config.schema.json +24 -0
- package/apps/studio/context-menu.tsx +40 -27
- package/apps/studio/context.ts +57 -8
- package/apps/studio/cursors-overlay.tsx +25 -13
- package/apps/studio/dist/client.bundle.js +686 -686
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/exporters/jobs.ts +77 -15
- package/apps/studio/exporters/remote.ts +190 -0
- package/apps/studio/exporters/video-encode-lib.ts +10 -4
- package/apps/studio/figma/to-strokes.ts +11 -9
- package/apps/studio/gifenc.d.ts +51 -0
- package/apps/studio/git/log-format.ts +88 -0
- package/apps/studio/git/safe-rel.ts +96 -0
- package/apps/studio/git/service.ts +46 -27
- package/apps/studio/hmr-broadcast.ts +10 -0
- package/apps/studio/http.ts +384 -6
- package/apps/studio/participants-chrome.tsx +1 -0
- package/apps/studio/photo-store.ts +7 -0
- package/apps/studio/react-augment.d.ts +17 -0
- package/apps/studio/runtime-bundle.ts +6 -1
- package/apps/studio/server.ts +43 -8
- package/apps/studio/sync/agent.ts +70 -82
- package/apps/studio/sync/asset-push.ts +28 -6
- package/apps/studio/sync/autocommit.ts +106 -5
- package/apps/studio/sync/cell-file-events.ts +117 -0
- package/apps/studio/sync/cell-pairing.ts +20 -5
- package/apps/studio/sync/cell-write-nudge.ts +244 -0
- package/apps/studio/sync/codec.ts +155 -3
- package/apps/studio/sync/cold-start-apply.ts +211 -0
- package/apps/studio/sync/ctl-heal.ts +253 -0
- package/apps/studio/sync/ctl-provider.ts +217 -0
- package/apps/studio/sync/decide-file.ts +335 -0
- package/apps/studio/sync/file-ledger.ts +581 -0
- package/apps/studio/sync/file-membership.ts +32 -0
- package/apps/studio/sync/file-plane.ts +1400 -0
- package/apps/studio/sync/file-pull.ts +41 -4
- package/apps/studio/sync/hub-link.ts +16 -1
- package/apps/studio/sync/hub-listing.ts +46 -0
- package/apps/studio/sync/hubs-config.ts +16 -0
- package/apps/studio/sync/index.ts +919 -214
- package/apps/studio/sync/journal-client.ts +200 -0
- package/apps/studio/sync/migrate-seed.ts +99 -67
- package/apps/studio/sync/poke.ts +50 -0
- package/apps/studio/sync/projection.ts +13 -0
- package/apps/studio/sync/pull-budget.ts +86 -0
- package/apps/studio/sync/settings.ts +110 -0
- package/apps/studio/sync/status.ts +68 -0
- package/apps/studio/sync/trash.ts +243 -0
- package/apps/studio/sync/untrusted.ts +30 -10
- package/apps/studio/test/_helpers.ts +8 -0
- package/apps/studio/test/canvas-build.test.ts +63 -0
- package/apps/studio/test/canvas-list-watch.test.ts +17 -0
- package/apps/studio/test/canvas-move-api.test.ts +31 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +12 -0
- package/apps/studio/test/canvas-shell-build-error.test.ts +49 -0
- package/apps/studio/test/cloud-history-hardening.test.ts +165 -0
- package/apps/studio/test/cloud-history-posture.test.ts +230 -0
- package/apps/studio/test/cloud-session-role.test.ts +30 -0
- package/apps/studio/test/cloud-shell-surfaces.test.ts +39 -0
- package/apps/studio/test/cold-start-apply.test.ts +303 -0
- package/apps/studio/test/collab-stress.test.ts +9 -1
- package/apps/studio/test/export-lane.test.ts +245 -0
- package/apps/studio/test/fixtures/video-comp-fixture.tsx +1 -1
- package/apps/studio/test/git-log-format.test.ts +95 -0
- package/apps/studio/test/git-safe-rel.test.ts +132 -0
- package/apps/studio/test/hmr-broadcast.test.ts +26 -0
- package/apps/studio/test/peer-selection-follows-camera.test.tsx +131 -0
- package/apps/studio/test/shared-doc-cell-pairing.test.ts +5 -2
- package/apps/studio/test/sync-agent.test.ts +78 -0
- package/apps/studio/test/sync-asset-push.test.ts +90 -16
- package/apps/studio/test/sync-autocommit.test.ts +80 -0
- package/apps/studio/test/sync-cell-write-nudge.test.ts +346 -0
- package/apps/studio/test/sync-ctl-channel.test.ts +508 -0
- package/apps/studio/test/sync-decide-file.test.ts +420 -0
- package/apps/studio/test/sync-file-ledger.test.ts +334 -0
- package/apps/studio/test/sync-file-membership.test.ts +17 -1
- package/apps/studio/test/sync-file-plane.test.ts +976 -0
- package/apps/studio/test/sync-hub-listing.test.ts +46 -0
- package/apps/studio/test/sync-meta-codec.test.ts +76 -0
- package/apps/studio/test/sync-move-retirement.test.ts +231 -0
- package/apps/studio/test/sync-panel-surface.test.ts +20 -0
- package/apps/studio/test/sync-path-pull.test.ts +67 -1
- package/apps/studio/test/sync-pull-budget.test.ts +169 -0
- package/apps/studio/test/sync-seed-defers-to-hub.test.ts +83 -0
- package/apps/studio/test/sync-settings-routes.test.ts +195 -0
- package/apps/studio/test/sync-settings.test.ts +151 -0
- package/apps/studio/test/sync-status.test.ts +69 -0
- package/apps/studio/test/sync-trash.test.ts +132 -0
- package/apps/studio/test/workspace-containment.test.ts +45 -9
- package/apps/studio/tsconfig.json +9 -10
- package/apps/studio/use-annotation-resize.tsx +14 -3
- package/apps/studio/use-collab.tsx +3 -1
- package/apps/studio/whats-new.json +99 -0
- package/apps/studio/workspace-mode.ts +110 -62
- package/apps/studio/ws.ts +22 -1
- package/cli/bin/claude-design-server.mjs +19 -0
- package/cli/commands/design.mjs +25 -6
- package/cli/commands/hub-workspace.mjs +243 -22
- package/cli/commands/hub-workspace.test.mjs +171 -0
- package/cli/commands/hub.mjs +71 -1
- package/cli/lib/design-link.mjs +186 -1
- package/cli/lib/design-ownership.mjs +330 -0
- package/cli/lib/design-ownership.test.mjs +329 -0
- package/cli/lib/hubs-config.mjs +21 -0
- package/cli/lib/hubs-config.test.mjs +47 -1
- package/cli/lib/workspace-plan.mjs +298 -5
- package/cli/lib/workspace-plan.test.mjs +215 -1
- package/package.json +10 -10
- package/plugins/design/templates/_shell.html +43 -2
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +1 -1
- package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +1 -1
- package/apps/studio/server.mjs +0 -1312
- package/apps/studio/sync/asset-pull.ts +0 -210
- package/apps/studio/sync/asset-push-worker.ts +0 -84
- package/apps/studio/sync/asset-sweep.ts +0 -262
- package/apps/studio/test/sync-asset-pull.test.ts +0 -161
- package/apps/studio/test/sync-asset-push-worker.test.ts +0 -183
- package/apps/studio/test/sync-asset-sweep.test.ts +0 -243
|
@@ -71,10 +71,12 @@ describe('checkContainment — what a cell may serve', () => {
|
|
|
71
71
|
|
|
72
72
|
test('a VARIANT of a forbidden route is still forbidden', () => {
|
|
73
73
|
// The realistic way this gets reintroduced: not `/_api/export` verbatim, but
|
|
74
|
-
// a sibling that does the same thing under a new name.
|
|
74
|
+
// a sibling that does the same thing under a new name. (`/_api/export-jobs`
|
|
75
|
+
// moved OUT of this list when render-workers excepted it explicitly — an
|
|
76
|
+
// except-listed exact path is a classified decision, not a variant.)
|
|
75
77
|
for (const route of [
|
|
76
|
-
'/_api/export-jobs',
|
|
77
78
|
'/_api/export/png',
|
|
79
|
+
'/_api/export-jobs-v2',
|
|
78
80
|
'/_api/github/repos',
|
|
79
81
|
'/_api/claude/signin',
|
|
80
82
|
'/_ws/acp',
|
|
@@ -237,7 +239,10 @@ describe('the vocabulary itself', () => {
|
|
|
237
239
|
'/_api/generate',
|
|
238
240
|
'/_api/github',
|
|
239
241
|
'/_api/hub',
|
|
240
|
-
|
|
242
|
+
// `/_api/photo-edit` left this list on purpose: it stores a validated
|
|
243
|
+
// JSON sidecar and evaluates nothing — the decode happens in the
|
|
244
|
+
// member's browser. Withholding it made photo edits unsavable in the
|
|
245
|
+
// cloud. See workspace-mode.ts's header for the fuller account.
|
|
241
246
|
// Spawns a headless browser against the studio — same evaluation as
|
|
242
247
|
// `/_api/export`, and there is no browser in a cell image.
|
|
243
248
|
'/_api/shell-shot',
|
|
@@ -281,15 +286,16 @@ describe('pruneForWorkspace — how a cell can boot at all', () => {
|
|
|
281
286
|
expect(Object.keys(routes).sort()).toEqual([
|
|
282
287
|
'/_api/asset',
|
|
283
288
|
'/_api/comments',
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
]);
|
|
287
|
-
expect(removed).toEqual([
|
|
288
|
-
'/_api/export',
|
|
289
|
+
// Kept since render-workers — the job lane enqueues/streams, it never
|
|
290
|
+
// evaluates (the `except` escape on the `/_api/export` prefix).
|
|
289
291
|
'/_api/export-jobs',
|
|
290
|
-
'/_api/
|
|
292
|
+
'/_api/git-committers',
|
|
293
|
+
// Kept since the unwithhold — a photo edit is a stored sidecar, and a
|
|
294
|
+
// cell that cannot store it cannot save photo edits at all.
|
|
291
295
|
'/_api/photo-edit',
|
|
296
|
+
'/_health',
|
|
292
297
|
]);
|
|
298
|
+
expect(removed).toEqual(['/_api/export', '/_api/generate/keys']);
|
|
293
299
|
// Handlers are carried through by reference — pruning must not rebuild them.
|
|
294
300
|
expect(routes['/_api/asset']).toBe('a');
|
|
295
301
|
});
|
|
@@ -310,6 +316,36 @@ describe('pruneForWorkspace — how a cell can boot at all', () => {
|
|
|
310
316
|
expect(() => assertContainment(Object.keys(table), { env: CELL })).toThrow(/REFUSING TO START/);
|
|
311
317
|
});
|
|
312
318
|
|
|
319
|
+
test('the export JOB lane survives the prune; the synchronous render does not (render-workers)', () => {
|
|
320
|
+
// feature-cloud-export-render-workers: enqueue/list/download evaluate
|
|
321
|
+
// nothing — the render happens in the maude-render service (or refuses,
|
|
322
|
+
// lane `none`). The synchronous `/_api/export` stays forbidden, and so
|
|
323
|
+
// does any FUTURE `/_api/export-*` variant nobody has classified — the
|
|
324
|
+
// escape is an explicit exact-path allowlist, not a loosened prefix.
|
|
325
|
+
const table = {
|
|
326
|
+
'/_api/export': 'x',
|
|
327
|
+
'/_api/export-jobs': 'j',
|
|
328
|
+
'/_api/export-jobs/download': 'd',
|
|
329
|
+
'/_api/export-history': 'h',
|
|
330
|
+
'/_api/export-next-phase-surprise': 'f',
|
|
331
|
+
'/_health': 'ok',
|
|
332
|
+
};
|
|
333
|
+
const { routes, removed } = pruneForWorkspace(table);
|
|
334
|
+
expect(Object.keys(routes).sort()).toEqual([
|
|
335
|
+
'/_api/export-history',
|
|
336
|
+
'/_api/export-jobs',
|
|
337
|
+
'/_api/export-jobs/download',
|
|
338
|
+
'/_health',
|
|
339
|
+
]);
|
|
340
|
+
expect(removed).toEqual(['/_api/export', '/_api/export-next-phase-surprise']);
|
|
341
|
+
// The single predicate agrees (the fetch fall-through gates on it too).
|
|
342
|
+
expect(isForbiddenRoute('/_api/export')).toBe(true);
|
|
343
|
+
expect(isForbiddenRoute('/_api/export-jobs')).toBe(false);
|
|
344
|
+
expect(isForbiddenRoute('/_api/export-jobs/download')).toBe(false);
|
|
345
|
+
expect(isForbiddenRoute('/_api/export-history')).toBe(false);
|
|
346
|
+
expect(isForbiddenRoute('/_api/export-next-phase-surprise')).toBe(true);
|
|
347
|
+
});
|
|
348
|
+
|
|
313
349
|
test('pruning an already-clean table is a no-op', () => {
|
|
314
350
|
const table = Object.fromEntries(ALLOWED_ROUTES.map((r) => [r, () => {}]));
|
|
315
351
|
const { routes, removed } = pruneForWorkspace(table);
|
|
@@ -19,15 +19,14 @@
|
|
|
19
19
|
"noEmit": true,
|
|
20
20
|
"allowJs": true,
|
|
21
21
|
"forceConsistentCasingInFileNames": true,
|
|
22
|
-
"allowImportingTsExtensions": true
|
|
22
|
+
"allowImportingTsExtensions": true,
|
|
23
|
+
// @maude/canvas-lib is the virtual specifier canvases import; the dev-server's
|
|
24
|
+
// Bun.build plugin resolves it to this same file (DDR-025).
|
|
25
|
+
"paths": {
|
|
26
|
+
"@maude/canvas-lib": ["./canvas-lib.tsx"]
|
|
27
|
+
}
|
|
23
28
|
},
|
|
24
|
-
"include":
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"client/**/*.ts",
|
|
28
|
-
"client/**/*.tsx",
|
|
29
|
-
"client/**/*.jsx",
|
|
30
|
-
"test/**/*.ts"
|
|
31
|
-
],
|
|
32
|
-
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
29
|
+
"//include": "Broad by design. An explicit root list silently orphaned 52 tracked files (canvas-lib.tsx, every overlay, commands/**) because it lacked '*.tsx' — reached by no checker at all. scripts/check-tsc-coverage.sh pins this: every tracked non-test .ts/.tsx here must appear in `tsc --listFiles`.",
|
|
30
|
+
"include": ["**/*.ts", "**/*.tsx", "**/*.jsx"],
|
|
31
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"]
|
|
33
32
|
}
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
strokeCenter,
|
|
40
40
|
strokeRotation,
|
|
41
41
|
} from './annotations-layer.tsx';
|
|
42
|
-
import { useViewportControllerContext } from './canvas-lib.tsx';
|
|
42
|
+
import { getLiveViewport, useViewportControllerContext } from './canvas-lib.tsx';
|
|
43
43
|
import { useAnnotationSelection } from './use-annotation-selection.tsx';
|
|
44
44
|
|
|
45
45
|
const RESIZE_CSS = `
|
|
@@ -583,9 +583,12 @@ export function AnnotationResizeOverlay({ store }: { store: StrokesStoreValue |
|
|
|
583
583
|
return store.strokes.find((s) => s.id === selectedId) ?? null;
|
|
584
584
|
}, [selectedId, store]);
|
|
585
585
|
|
|
586
|
+
// Live for the same reason the rAF loop is: a trackpad pinch mid-drag moves
|
|
587
|
+
// the camera without republishing, and unprojecting the pointer through a
|
|
588
|
+
// stale viewport would resize the stroke to the wrong world size.
|
|
586
589
|
const screenToWorld = useCallback(
|
|
587
590
|
(cx: number, cy: number): [number, number] => {
|
|
588
|
-
const v = vp ?? { x: 0, y: 0, zoom: 1 };
|
|
591
|
+
const v = getLiveViewport() ?? vp ?? { x: 0, y: 0, zoom: 1 };
|
|
589
592
|
const z = v.zoom || 1;
|
|
590
593
|
return [(cx - v.x) / z, (cy - v.y) / z];
|
|
591
594
|
},
|
|
@@ -595,6 +598,14 @@ export function AnnotationResizeOverlay({ store }: { store: StrokesStoreValue |
|
|
|
595
598
|
// rAF loop — repositions handles on every frame while a single resizable
|
|
596
599
|
// stroke is selected. Cheaper than wiring pan/zoom observers because the
|
|
597
600
|
// halo overlays already follow the same pattern.
|
|
601
|
+
//
|
|
602
|
+
// It MUST read `getLiveViewport()` per tick, not the `vp` captured in this
|
|
603
|
+
// effect's closure: the published viewport is settle-cadence (SETTLE_MS), so
|
|
604
|
+
// through a pan/zoom the closure holds one frozen value and the loop spends
|
|
605
|
+
// the whole gesture recomputing the SAME stale screen positions. The handles
|
|
606
|
+
// sat where the selection used to be and only snapped onto it ~half a second
|
|
607
|
+
// after the gesture ended. `liveViewport` is written on every applyViewport
|
|
608
|
+
// with no publish lag, and equals `vp` once settled.
|
|
598
609
|
useEffect(() => {
|
|
599
610
|
if (!selectedStroke || !isResizable(selectedStroke)) {
|
|
600
611
|
const c = containerRef.current;
|
|
@@ -609,7 +620,7 @@ export function AnnotationResizeOverlay({ store }: { store: StrokesStoreValue |
|
|
|
609
620
|
rafRef.current = null;
|
|
610
621
|
const c = containerRef.current;
|
|
611
622
|
if (!c) return;
|
|
612
|
-
const v = vp ?? { x: 0, y: 0, zoom: 1 };
|
|
623
|
+
const v = getLiveViewport() ?? vp ?? { x: 0, y: 0, zoom: 1 };
|
|
613
624
|
const z = v.zoom || 1;
|
|
614
625
|
// Wave H — screen-constant halo offset (HALO_PAD_PX at any zoom).
|
|
615
626
|
const positions = handlePositions(selectedStroke, HALO_PAD_PX / z);
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import * as decoding from 'lib0/decoding';
|
|
20
20
|
import * as encoding from 'lib0/encoding';
|
|
21
|
+
import type { JSX } from 'react';
|
|
21
22
|
import {
|
|
22
23
|
createContext,
|
|
23
24
|
type ReactNode,
|
|
@@ -602,7 +603,8 @@ function createSession(slug: string): CollabSession {
|
|
|
602
603
|
|
|
603
604
|
function sendFrame(ws: WebSocket, payload: Uint8Array) {
|
|
604
605
|
try {
|
|
605
|
-
|
|
606
|
+
// Uint8Array<ArrayBufferLike> vs BufferSource — see saveAsset in api.ts.
|
|
607
|
+
ws.send(payload as Uint8Array<ArrayBuffer>);
|
|
606
608
|
} catch {
|
|
607
609
|
/* dead socket — close handler will reconnect */
|
|
608
610
|
}
|
|
@@ -1,6 +1,105 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./whats-new.schema.json",
|
|
3
3
|
"entries": [
|
|
4
|
+
{
|
|
5
|
+
"id": "export-from-the-cloud-studio",
|
|
6
|
+
"version": "1.0.1",
|
|
7
|
+
"date": "2026-08-20",
|
|
8
|
+
"kind": "feature",
|
|
9
|
+
"surface": "design-ui",
|
|
10
|
+
"title": "Export works in the cloud studio now",
|
|
11
|
+
"summary": "Exporting a canvas to PNG, PDF, PPTX, ZIP or video from a project open in your browser used to fail. Now it runs as a background job — kick it off, keep working, and download it from the notifications when it's done. Where a workspace has no render service configured, the formats that need one say so and point you at the desktop app; ZIP always works."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "your-sync-controls-are-in-the-app-now",
|
|
15
|
+
"version": "1.0.1",
|
|
16
|
+
"date": "2026-08-20",
|
|
17
|
+
"kind": "feature",
|
|
18
|
+
"surface": "design-ui",
|
|
19
|
+
"title": "Sync settings, consent and a Trash you can find",
|
|
20
|
+
"summary": "The Sync panel now holds the controls that used to mean editing a config file by hand: turn file sync or delete propagation on and off, choose how first-link conflicts resolve, and see per-file delivery. A first-time notice asks before a workspace starts mirroring your whole design folder, and deleted or replaced files land in a Trash section you can restore from or clear — no more hunting for a hidden folder."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "moving-a-canvas-reaches-every-machine",
|
|
24
|
+
"version": "1.0.0",
|
|
25
|
+
"date": "2026-08-20",
|
|
26
|
+
"kind": "fix",
|
|
27
|
+
"surface": "design-ui",
|
|
28
|
+
"title": "Moving a canvas reaches every machine",
|
|
29
|
+
"summary": "Moving a canvas into a folder now shows up everywhere, instead of only on the machine that moved it. Canvases made on one machine also stop arriving blank or reverting when you edit them somewhere else."
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "durable-backups-for-every-file",
|
|
33
|
+
"version": "1.0.0",
|
|
34
|
+
"date": "2026-08-20",
|
|
35
|
+
"kind": "improvement",
|
|
36
|
+
"surface": "design-ui",
|
|
37
|
+
"title": "Every file in a cloud project is backed up now",
|
|
38
|
+
"summary": "Cloud-hosted projects now keep a durable copy of every file — design-system stylesheets, shared modules, fonts, and images anywhere in the folder, not just top-level assets. A project that restarts in the cloud comes back whole instead of missing its styling."
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "the-whole-design-folder-syncs",
|
|
42
|
+
"version": "1.0.0",
|
|
43
|
+
"date": "2026-08-20",
|
|
44
|
+
"kind": "feature",
|
|
45
|
+
"surface": "design-ui",
|
|
46
|
+
"title": "Your whole design folder syncs now",
|
|
47
|
+
"summary": "A linked project mirrors everything in .design/ both ways — design-system stylesheets, READMEs, fonts, images — not just the canvases. Before, a teammate opening the project got every canvas and none of the styling it depended on."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"id": "deleting-a-file-deletes-it-everywhere",
|
|
51
|
+
"version": "1.0.0",
|
|
52
|
+
"date": "2026-08-20",
|
|
53
|
+
"kind": "feature",
|
|
54
|
+
"surface": "design-ui",
|
|
55
|
+
"title": "Delete a file and it stays deleted",
|
|
56
|
+
"summary": "Removing a file now removes it for everyone, instead of it quietly coming back on the next sync. If more than a handful go at once — a branch switch, a bad restore — sync pauses and the Sync panel says what it was about to do; nothing is removed until you agree. Anything it does remove is moved to a trash folder on both machines rather than deleted."
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"id": "repo-owned-or-hub-owned",
|
|
60
|
+
"version": "1.0.0",
|
|
61
|
+
"date": "2026-08-20",
|
|
62
|
+
"kind": "improvement",
|
|
63
|
+
"surface": "design-ui",
|
|
64
|
+
"title": "One owner for your design folder",
|
|
65
|
+
"summary": "A project is either committed to your repo or mirrored by your hub — never both, which used to let a git pull and a sync undo each other. Switching either way takes one command and never moves a file off your disk. Linking also asks, once, whether that hub may send shared code to this machine."
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "history-follows-the-cloud",
|
|
69
|
+
"version": "1.0.0",
|
|
70
|
+
"date": "2026-08-20",
|
|
71
|
+
"kind": "fix",
|
|
72
|
+
"title": "History shows what the cloud is saving",
|
|
73
|
+
"summary": "When a folder is connected to Maude Cloud, the History tab now lists the cloud's saved versions instead of your machine's — headed by the cloud project's name, and you can open a canvas at any of them, including versions that only ever existed in the cloud. If the cloud can't be reached it says so, with a Retry, rather than looking empty. While the cloud is saving, the app also stops keeping a separate account of the folder: no unsaved marks in the tree, no draft switcher. Disconnect and it all returns live. Your own git is untouched.",
|
|
74
|
+
"surface": "design-ui"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"id": "canvas-zoom-and-selection-feel",
|
|
78
|
+
"version": "1.0.0",
|
|
79
|
+
"date": "2026-08-20",
|
|
80
|
+
"kind": "improvement",
|
|
81
|
+
"title": "Zooming and drawing feel right",
|
|
82
|
+
"summary": "The mouse wheel and trackpad pinch now move the zoom about twice as far per notch, closer to the canvas tools you already use. Ink you draw scales with the zoom in the browser the way it always did in the desktop app, and the resize handles around a selection now follow a pan or zoom live instead of sitting where the shape used to be until you let go.",
|
|
83
|
+
"surface": "design-ui"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"id": "cloud-canvases-heal-themselves",
|
|
87
|
+
"version": "1.0.0",
|
|
88
|
+
"date": "2026-08-20",
|
|
89
|
+
"kind": "fix",
|
|
90
|
+
"title": "The cloud stops needing a reload",
|
|
91
|
+
"summary": "A file that arrives in the cloud now shows up in an open tab by itself. Before, the bytes landed but nothing told the page, so a picture stayed a broken frame until you reloaded by hand — it looked like sync had failed when it had already finished.",
|
|
92
|
+
"surface": "design-ui"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"id": "instant-asset-sync",
|
|
96
|
+
"version": "0.60.7",
|
|
97
|
+
"date": "2026-08-16",
|
|
98
|
+
"kind": "improvement",
|
|
99
|
+
"title": "Pictures sync in seconds",
|
|
100
|
+
"summary": "A picture you drop on a canvas is pushed by itself the moment it lands, and the other side fetches it the moment the reference arrives — seconds end to end, instead of waiting for the next full sweep or polling tick.",
|
|
101
|
+
"surface": "design-ui"
|
|
102
|
+
},
|
|
4
103
|
{
|
|
5
104
|
"id": "assets-arrive-and-heal",
|
|
6
105
|
"version": "0.60.6",
|
|
@@ -35,79 +35,118 @@ export function isWorkspaceMode(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
|
35
35
|
return env.MAUDE_WORKSPACE_MODE === '1';
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Where an export gets rendered — the one question every export surface asks.
|
|
40
|
+
*
|
|
41
|
+
* `local` this process spawns the capture browser (desktop / plain server;
|
|
42
|
+
* pre-render-worker behavior, unchanged).
|
|
43
|
+
* `remote` a workspace cell with a render service configured: browser-needing
|
|
44
|
+
* formats are dispatched to `MAUDE_RENDER_URL` as jobs; the browser
|
|
45
|
+
* never enters THIS image (the containment invariant holds).
|
|
46
|
+
* `none` a workspace cell with no render service: browser-needing formats
|
|
47
|
+
* refuse with a remedy, browser-free formats (zip) still run here.
|
|
48
|
+
*
|
|
49
|
+
* Resolved once per boot-shaped question (env does not change while a cell
|
|
50
|
+
* runs — env applies at container start, see apps/cells/wrangler.toml).
|
|
51
|
+
*/
|
|
52
|
+
export type RenderLane = 'local' | 'remote' | 'none';
|
|
53
|
+
|
|
54
|
+
export function resolveRenderLane(env: NodeJS.ProcessEnv = process.env): RenderLane {
|
|
55
|
+
if (!isWorkspaceMode(env)) return 'local';
|
|
56
|
+
return env.MAUDE_RENDER_URL ? 'remote' : 'none';
|
|
57
|
+
}
|
|
58
|
+
|
|
38
59
|
/**
|
|
39
60
|
* Route paths a workspace cell must not expose, with the reason each one
|
|
40
61
|
* violates containment. The reason travels with the entry so the error message
|
|
41
62
|
* teaches rather than just refusing.
|
|
42
63
|
*
|
|
43
|
-
* These are prefixes:
|
|
44
|
-
* is
|
|
64
|
+
* These are prefixes: a variant of a forbidden surface is still forbidden.
|
|
65
|
+
* The ONE escape is an entry's `except` list — EXACT paths that leave the
|
|
66
|
+
* prefix's shadow because they hold none of what the prefix is forbidden for.
|
|
67
|
+
* feature-cloud-export-render-workers used it first: `/_api/export-jobs` +
|
|
68
|
+
* `/_api/export-history` enqueue, list and stream — the EVALUATION the
|
|
69
|
+
* `/_api/export` prefix names happens in the maude-render service (or is
|
|
70
|
+
* refused, lane `none`), never here. An unlisted future `/_api/export-*`
|
|
71
|
+
* route stays forbidden by default, which is the property that makes an
|
|
72
|
+
* exact-path allowlist safer than narrowing the prefix.
|
|
45
73
|
*
|
|
46
74
|
* TWO KINDS OF ENTRY LIVE HERE, and both belong:
|
|
47
75
|
*
|
|
48
|
-
* 1. surfaces that EVALUATE tenant content on our compute (export,
|
|
76
|
+
* 1. surfaces that EVALUATE tenant content on our compute (export, shell-shot);
|
|
77
|
+
* — NOT `/_api/photo-edit`, which was withheld here for a while under
|
|
78
|
+
* that rationale and never fit it: the route validates and stores a JSON
|
|
79
|
+
* sidecar (`assets/<sha8>.photo.json`), and the DECODING it was blamed
|
|
80
|
+
* for happens in the member's own browser (PhotoPreviewBridge) — the
|
|
81
|
+
* exact same division of labour that makes `/_canvas-shell` servable.
|
|
82
|
+
* The withhold's real effect was that photo edits could not be saved in
|
|
83
|
+
* the cloud at all, reported as "photo editing doesn't work";
|
|
49
84
|
* 2. surfaces that hold, spend or reveal a SECRET on our compute — a provider
|
|
50
85
|
* key, the operator's GitHub token, the user's own `claude` session, the
|
|
51
86
|
* process log. DDR-123's "claude never on our infra" is only a fact while
|
|
52
87
|
* these are unreachable, and Cloud Phase 27 D1 is where they stop being in
|
|
53
88
|
* the image at all. Until then, unreachable is the floor, not the ceiling.
|
|
54
89
|
*/
|
|
55
|
-
export const FORBIDDEN_ROUTE_PREFIXES: ReadonlyArray<{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
90
|
+
export const FORBIDDEN_ROUTE_PREFIXES: ReadonlyArray<{
|
|
91
|
+
prefix: string;
|
|
92
|
+
why: string;
|
|
93
|
+
except?: readonly string[];
|
|
94
|
+
}> = Object.freeze([
|
|
95
|
+
{
|
|
96
|
+
prefix: '/_api/export',
|
|
97
|
+
why: 'export renders the canvas through a headless browser — it EVALUATES tenant TSX',
|
|
98
|
+
// The job lane: enqueue / list+stream / history. No evaluation happens
|
|
99
|
+
// in-cell — browser formats dispatch to maude-render (exporters/jobs.ts
|
|
100
|
+
// render lane), zip runs browser-free, and lane `none` refuses with a
|
|
101
|
+
// remedy. DDR amending DDR-209 A′1 records the contract.
|
|
102
|
+
except: ['/_api/export-jobs', '/_api/export-jobs/download', '/_api/export-history'],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
prefix: '/_api/generate',
|
|
106
|
+
why: 'media generation runs tenant-authored prompts against a provider key held here',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
prefix: '/_api/figma',
|
|
110
|
+
why: 'the Figma lane STORES and SPENDS a user personal access token, and imports write the design root — a cell must hold no provider credential (DDR-216 D2/D3, same class as /_api/generate)',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
prefix: '/_api/shell-shot',
|
|
114
|
+
why: 'the shell screenshot spawns a headless browser against the studio — the same evaluation `/_api/export` is forbidden for',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
prefix: '/_ws/acp',
|
|
118
|
+
why: 'the ACP bridge spawns the user’s own `claude` and can drive file edits (DDR-123 is desktop-only)',
|
|
119
|
+
},
|
|
120
|
+
// ---- Cloud Phase 27 D1 — the secret-bearing surfaces, named at last ----
|
|
121
|
+
{
|
|
122
|
+
prefix: '/_api/acp',
|
|
123
|
+
why: 'the ACP surface drives the user’s own `claude` session; a cell must not hold one (DDR-123)',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
prefix: '/_api/claude',
|
|
127
|
+
why: 'installing or signing in to `claude` from a cell would put a user’s subscription on our infra (DDR-123)',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
prefix: '/_api/cloud',
|
|
131
|
+
why: 'cloud sign-in mints and stores an account session — a cell IS the cloud, it must not be a client of it',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
prefix: '/_api/github',
|
|
135
|
+
why: 'the GitHub lane spends an operator credential; a cell asks the control plane, it never holds one (DDR-201)',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
prefix: '/_api/hub',
|
|
139
|
+
why: 'hub linking rewrites which server owns this project — a cell must not repoint itself',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
prefix: '/_api/debug-bundle',
|
|
143
|
+
why: 'the diagnostic bundle reads this process’s logs, where every cell secret has had a chance to appear',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
prefix: '/_api/design',
|
|
147
|
+
why: 'design-system init shells out to the CLI on our compute against tenant-chosen input',
|
|
148
|
+
},
|
|
149
|
+
]);
|
|
111
150
|
|
|
112
151
|
/**
|
|
113
152
|
* Routes a cell MAY serve, but only while the contract that makes them safe is
|
|
@@ -168,9 +207,18 @@ function matches(route: string, prefix: string): boolean {
|
|
|
168
207
|
return route === prefix || route.startsWith(prefix);
|
|
169
208
|
}
|
|
170
209
|
|
|
171
|
-
/**
|
|
210
|
+
/** The forbidden entry shadowing `route`, honoring exact-path `except` escapes. */
|
|
211
|
+
function forbiddenEntryFor(
|
|
212
|
+
route: string
|
|
213
|
+
): { prefix: string; why: string; except?: readonly string[] } | undefined {
|
|
214
|
+
return FORBIDDEN_ROUTE_PREFIXES.find(
|
|
215
|
+
(f) => matches(route, f.prefix) && !f.except?.includes(route)
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** True when `route` is (or starts with) a forbidden prefix and is not excepted. */
|
|
172
220
|
export function isForbiddenRoute(route: string): boolean {
|
|
173
|
-
return
|
|
221
|
+
return forbiddenEntryFor(route) !== undefined;
|
|
174
222
|
}
|
|
175
223
|
|
|
176
224
|
/** True when `route` is one a cell may serve only while the sandbox is armed. */
|
|
@@ -234,7 +282,7 @@ export function checkContainment(
|
|
|
234
282
|
const routes: ContainmentReport['routes'] = [];
|
|
235
283
|
const unattested: ContainmentReport['unattested'] = [];
|
|
236
284
|
for (const route of routeNames) {
|
|
237
|
-
const forbidden =
|
|
285
|
+
const forbidden = forbiddenEntryFor(route);
|
|
238
286
|
if (forbidden) {
|
|
239
287
|
routes.push({ route, prefix: forbidden.prefix, why: forbidden.why });
|
|
240
288
|
continue;
|
package/apps/studio/ws.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { Collab, RoomConn } from './collab/index.ts';
|
|
|
10
10
|
import type { Context } from './context.ts';
|
|
11
11
|
import { createContainerWriteBridge, createHmrBroadcaster } from './hmr-broadcast.ts';
|
|
12
12
|
import type { InspectRegistry } from './inspect.ts';
|
|
13
|
+
import { startCellFileEvents } from './sync/cell-file-events.ts';
|
|
13
14
|
import { isWorkspaceMode } from './workspace-mode.ts';
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -274,7 +275,27 @@ export function createWs(
|
|
|
274
275
|
// iframe stays stale until a manual reload (inspector-edits-live-render RCA).
|
|
275
276
|
// Synthesise the fs:any the watcher owes us from each write's activity:suppress
|
|
276
277
|
// arm. Workspace-mode only — locally fs.watch fires and this would double-load.
|
|
277
|
-
if (isWorkspaceMode())
|
|
278
|
+
if (isWorkspaceMode()) {
|
|
279
|
+
createContainerWriteBridge(ctx);
|
|
280
|
+
// Sync v2 Increment 2/3 (DDR-226 §4) — the OTHER half of the same gap, and
|
|
281
|
+
// now both ways across it. The bridge above covers writes THIS process
|
|
282
|
+
// makes; a desktop asset PUT or a bucket→checkout refill happens in the HUB
|
|
283
|
+
// process, and nothing crossed that boundary.
|
|
284
|
+
//
|
|
285
|
+
// hub → child the hub pokes over the reserved `maude.files` channel and
|
|
286
|
+
// this turns the poke into the `fs:any` the watcher owed us
|
|
287
|
+
// child → hub the same `fs:any` (from the bridge above) is named to the
|
|
288
|
+
// hub, which journals it at once instead of finding it on
|
|
289
|
+
// the 15-minute walk-import belt
|
|
290
|
+
//
|
|
291
|
+
// Without the second direction the two sync directions have wildly
|
|
292
|
+
// different latency — seconds one way, up to a quarter of an hour the other
|
|
293
|
+
// — which is indistinguishable from "cloud → desktop is broken".
|
|
294
|
+
//
|
|
295
|
+
// Deliberately NOT gated on cell pairing — that is a one-tenant pilot and
|
|
296
|
+
// the gap is fleet-wide. See sync/ctl-provider.ts.
|
|
297
|
+
startCellFileEvents(ctx);
|
|
298
|
+
}
|
|
278
299
|
|
|
279
300
|
// Phase 13 / DDR-029 — canvas activity overlay. activity.ts emits
|
|
280
301
|
// `activity:change` per file as edits land + go idle. The overlay renders
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `claude-design-server` — legacy direct alias for the design dev server.
|
|
3
|
+
// Historically this bin WAS the zero-dep Node server (apps/studio/server.mjs);
|
|
4
|
+
// that server is gone (Bun authoritative since DDR-009, deleted in the post-1.0
|
|
5
|
+
// hardening pass), so the alias now forwards to `maude design serve`, which owns
|
|
6
|
+
// binary resolution, the RCA G3 bun shim, and the loud no-runtime refusal.
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { resolve } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const maude = resolve(fileURLToPath(import.meta.url), '..', 'maude.mjs');
|
|
12
|
+
const child = spawn(process.execPath, [maude, 'design', 'serve', ...process.argv.slice(2)], {
|
|
13
|
+
stdio: 'inherit',
|
|
14
|
+
});
|
|
15
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
16
|
+
child.on('error', (err) => {
|
|
17
|
+
process.stderr.write(`claude-design-server: ${err.message}\n`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
});
|
package/cli/commands/design.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { createRequire } from 'node:module';
|
|
|
13
13
|
import { homedir } from 'node:os';
|
|
14
14
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
15
15
|
import { parseArgs } from '../lib/argv.mjs';
|
|
16
|
-
import { runAdopt, runLink, runStatus, runUnlink } from '../lib/design-link.mjs';
|
|
16
|
+
import { runAdopt, runDetach, runLink, runStatus, runUnlink } from '../lib/design-link.mjs';
|
|
17
17
|
import { writeGitignoreBlock } from '../lib/gitignore-block.mjs';
|
|
18
18
|
import { isCompiledBinary } from '../lib/pkg-root.mjs';
|
|
19
19
|
|
|
@@ -23,6 +23,7 @@ const SUBCOMMANDS = new Set([
|
|
|
23
23
|
'export',
|
|
24
24
|
'link',
|
|
25
25
|
'unlink',
|
|
26
|
+
'detach',
|
|
26
27
|
'status',
|
|
27
28
|
'adopt',
|
|
28
29
|
'help',
|
|
@@ -171,6 +172,7 @@ export async function run({ args, pkgRoot }) {
|
|
|
171
172
|
if (sub === 'export') return runExport({ args });
|
|
172
173
|
if (sub === 'link') return runLink({ args });
|
|
173
174
|
if (sub === 'unlink') return runUnlink({ args });
|
|
175
|
+
if (sub === 'detach') return runDetach({ args });
|
|
174
176
|
if (sub === 'status') return runStatus({ args });
|
|
175
177
|
if (sub === 'adopt') return runAdopt({ args });
|
|
176
178
|
}
|
|
@@ -323,7 +325,7 @@ function usage() {
|
|
|
323
325
|
return `maude design <verb> [options]
|
|
324
326
|
|
|
325
327
|
Lifecycle:
|
|
326
|
-
serve · init · export · link · adopt · unlink · status
|
|
328
|
+
serve · init · export · link · adopt · detach · unlink · status
|
|
327
329
|
|
|
328
330
|
Dev-tooling (dispatch to the dev-server bash helpers — DDR-062):
|
|
329
331
|
screenshot · server-up · prep · slug · bootstrap-check · runtime-health
|
|
@@ -380,6 +382,12 @@ Dev-tooling (dispatch to the dev-server bash helpers — DDR-062):
|
|
|
380
382
|
Alias of 'link --adopt'. Shipped as a discoverable subcommand so
|
|
381
383
|
first-time bootstrap reads like 'git remote add' + 'git push -f'.
|
|
382
384
|
|
|
385
|
+
detach [--yes] [--keep-token]
|
|
386
|
+
Take .design/ back into this repo: unlink the hub and remove the
|
|
387
|
+
hub-owned ignore, so git sees the folder again. Nothing is
|
|
388
|
+
downloaded — the local mirror is already complete — and nothing
|
|
389
|
+
is committed for you. The reverse of 'adopt' (DDR-228).
|
|
390
|
+
|
|
383
391
|
unlink [--keep-token]
|
|
384
392
|
Drop linkedHub from .design/config.json + remove the per-machine
|
|
385
393
|
token (unless --keep-token). Repo returns to solo mode. Local
|
|
@@ -466,7 +474,6 @@ async function runServe({ args, pkgRoot }) {
|
|
|
466
474
|
}
|
|
467
475
|
|
|
468
476
|
const tsEntry = resolve(pkgRoot, 'apps', 'studio', 'server.ts');
|
|
469
|
-
const mjsEntry = resolve(pkgRoot, 'apps', 'studio', 'server.mjs');
|
|
470
477
|
|
|
471
478
|
const hasBun = await new Promise((res) => {
|
|
472
479
|
const probe = spawn('bun', ['--version'], { stdio: 'ignore' });
|
|
@@ -474,9 +481,21 @@ async function runServe({ args, pkgRoot }) {
|
|
|
474
481
|
probe.on('exit', (code) => res(code === 0));
|
|
475
482
|
});
|
|
476
483
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
484
|
+
if (!hasBun) {
|
|
485
|
+
// The pre-DDR-009 zero-dep Node server used to be the silent fallback here.
|
|
486
|
+
// It was deleted (post-1.0 hardening, T0b): it served a years-stale feature
|
|
487
|
+
// surface, so "works without bun" really meant "silently degrades to a
|
|
488
|
+
// different product". Bun is authoritative (DDR-009) — refuse loud instead.
|
|
489
|
+
process.stderr.write(
|
|
490
|
+
`maude design serve: running from source requires bun (DDR-009 — the dev\nserver is Bun-authoritative; the old Node fallback served a stale feature\nsurface and was removed).\n\n Install bun: curl -fsSL https://bun.sh/install | bash\n Or use the shipped platform binary instead of the source tree:\n npm i -g @1agh/maude && maude design serve\n\nRepo: ${pkgRoot}\n`
|
|
491
|
+
);
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const child = spawn('bun', ['run', tsEntry, ...forwarded], {
|
|
496
|
+
stdio: 'inherit',
|
|
497
|
+
env: process.env,
|
|
498
|
+
});
|
|
480
499
|
child.on('exit', (code) => process.exit(code ?? 0));
|
|
481
500
|
child.on('error', (err) => {
|
|
482
501
|
process.stderr.write(`maude design serve: ${err.message}\n`);
|