@1agh/maude 0.58.3 → 0.60.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 (81) hide show
  1. package/apps/studio/annotations-layer.tsx +49 -15
  2. package/apps/studio/bin/_import-asset.mjs +18 -0
  3. package/apps/studio/bin/_import-figma.mjs +1180 -242
  4. package/apps/studio/bin/_perf-probe-safari.mjs +332 -0
  5. package/apps/studio/bin/_perf-probe.mjs +228 -0
  6. package/apps/studio/bin/_perf-shared.mjs +345 -0
  7. package/apps/studio/bin/_video-playwright.mjs +17 -4
  8. package/apps/studio/bin/import-figma.sh +10 -1
  9. package/apps/studio/bin/perf.sh +228 -0
  10. package/apps/studio/bin/smoke.sh +49 -5
  11. package/apps/studio/canvas-lib.tsx +148 -6
  12. package/apps/studio/client/app.jsx +152 -37
  13. package/apps/studio/client/panels/SyncPanel.jsx +320 -0
  14. package/apps/studio/client/panels/TimelinePanel.jsx +29 -1
  15. package/apps/studio/client/panels/timeline-comp-target.js +101 -0
  16. package/apps/studio/client/styles/3-shell-maude.css +40 -0
  17. package/apps/studio/client/styles/4-components.css +4 -4
  18. package/apps/studio/context.ts +4 -0
  19. package/apps/studio/dist/client.bundle.js +772 -772
  20. package/apps/studio/dist/styles.css +1 -1
  21. package/apps/studio/exporters/video-encode-lib.ts +8 -5
  22. package/apps/studio/exporters/video.ts +10 -0
  23. package/apps/studio/figma/assets.test.ts +92 -0
  24. package/apps/studio/figma/assets.ts +63 -9
  25. package/apps/studio/figma/codegen-client.test.ts +276 -0
  26. package/apps/studio/figma/codegen-client.ts +509 -0
  27. package/apps/studio/figma/codegen-fonts.test.ts +103 -0
  28. package/apps/studio/figma/codegen-fonts.ts +195 -0
  29. package/apps/studio/figma/codegen-values.test.ts +179 -0
  30. package/apps/studio/figma/codegen-values.ts +270 -0
  31. package/apps/studio/figma/endpoints.ts +73 -0
  32. package/apps/studio/figma/fig-decode.test.ts +788 -0
  33. package/apps/studio/figma/fig-decode.ts +839 -0
  34. package/apps/studio/figma/fig-differential.test.ts +182 -0
  35. package/apps/studio/figma/fig-kiwi.ts +410 -0
  36. package/apps/studio/figma/fig-translator.test.ts +192 -0
  37. package/apps/studio/figma/fig-vector.test.ts +113 -0
  38. package/apps/studio/figma/fig-vector.ts +145 -0
  39. package/apps/studio/figma/fig-zip.ts +270 -0
  40. package/apps/studio/figma/from-codegen.test.ts +408 -0
  41. package/apps/studio/figma/from-codegen.ts +1103 -0
  42. package/apps/studio/figma/sanitize.test.ts +69 -0
  43. package/apps/studio/figma/sanitize.ts +146 -47
  44. package/apps/studio/figma/tailwind-map.test.ts +142 -0
  45. package/apps/studio/figma/tailwind-map.ts +545 -0
  46. package/apps/studio/figma/to-artboard.ts +41 -1
  47. package/apps/studio/figma/to-render.ts +25 -3
  48. package/apps/studio/figma/types.ts +6 -1
  49. package/apps/studio/http.ts +94 -0
  50. package/apps/studio/sync/asset-push-worker.ts +84 -0
  51. package/apps/studio/sync/asset-push.ts +441 -39
  52. package/apps/studio/sync/asset-sweep.ts +262 -0
  53. package/apps/studio/sync/connection-state.ts +71 -3
  54. package/apps/studio/sync/index.ts +39 -6
  55. package/apps/studio/sync/presentation.ts +21 -0
  56. package/apps/studio/sync/status.ts +18 -0
  57. package/apps/studio/sync/supervisor.ts +20 -0
  58. package/apps/studio/test/canvas-origin-gate.test.ts +13 -0
  59. package/apps/studio/test/figma-explode.test.ts +438 -0
  60. package/apps/studio/test/fixtures/perf-canvas.mjs +201 -0
  61. package/apps/studio/test/import-figma.test.ts +192 -4
  62. package/apps/studio/test/sync-asset-push-worker.test.ts +183 -0
  63. package/apps/studio/test/sync-asset-push.test.ts +639 -47
  64. package/apps/studio/test/sync-asset-sweep.test.ts +243 -0
  65. package/apps/studio/test/sync-connection-state.test.ts +66 -0
  66. package/apps/studio/test/sync-panel-surface.test.ts +123 -0
  67. package/apps/studio/test/sync-resync-routes.test.ts +125 -0
  68. package/apps/studio/test/sync-status.test.ts +28 -0
  69. package/apps/studio/test/sync-supervisor.test.ts +46 -0
  70. package/apps/studio/test/timeline-comp-target.test.ts +139 -0
  71. package/apps/studio/test/video-comp.test.ts +81 -1
  72. package/apps/studio/test/video-encode-lib.test.ts +63 -0
  73. package/apps/studio/use-artboard-drag.tsx +37 -3
  74. package/apps/studio/video-comp.tsx +51 -0
  75. package/apps/studio/whats-new.json +87 -0
  76. package/cli/commands/design.mjs +7 -0
  77. package/cli/commands/kg.mjs +8 -1
  78. package/cli/commands/kg.test.mjs +24 -0
  79. package/cli/lib/figma-codegen-reachability.test.mjs +104 -0
  80. package/cli/lib/figma-import-controls.test.mjs +70 -0
  81. package/package.json +8 -8
@@ -1163,6 +1163,33 @@ export function createHttp(
1163
1163
  },
1164
1164
  };
1165
1165
  },
1166
+ // `--explode` from the panel. Same lazy import and same ONE implementation
1167
+ // as the routes above — and note what is NOT here: the raw codegen response
1168
+ // never leaves the verb, so this route returns the same code-owned
1169
+ // accounting an agent would see on stdout (DDR-219 D10).
1170
+ async explode({ canvas, artboard, dryRun, confirmDocument }) {
1171
+ const mod = await import('./bin/_import-figma.mjs');
1172
+ const r = await mod.explodeArtboard({
1173
+ root: ctx.paths.repoRoot,
1174
+ designRootRel: ctx.paths.designRel,
1175
+ canvasRel: canvas,
1176
+ artboardId: artboard,
1177
+ dryRun: Boolean(dryRun),
1178
+ confirmDocument: Boolean(confirmDocument),
1179
+ });
1180
+ return {
1181
+ summary: {
1182
+ canvas: r.canvas,
1183
+ artboard: r.artboardId,
1184
+ route: 'codegen',
1185
+ endpoint: 'local',
1186
+ responseSha256: r.responseSha256,
1187
+ written: r.written,
1188
+ assets: r.assets ?? null,
1189
+ dispositions: r.report.entries,
1190
+ },
1191
+ };
1192
+ },
1166
1193
  });
1167
1194
  const gitJson = (r: { status: number; json: unknown }) =>
1168
1195
  Response.json(r.json, { status: r.status, headers: { 'Cache-Control': 'no-store' } });
@@ -2441,6 +2468,53 @@ export function createHttp(
2441
2468
  return gitJson(await cloudApi.detach());
2442
2469
  },
2443
2470
 
2471
+ // ── Resync (feature-sync-resync-and-out-of-process-sweep) ───────────────
2472
+ // MAIN-ORIGIN ONLY: absent from BOTH CANVAS_SAFE_API and startCanvasServer's
2473
+ // `routes` map (DDR-088's two-allowlist rule). Canvas content is untrusted
2474
+ // (DDR-054) and must not be able to command the desktop to re-push a whole
2475
+ // project — a canvas that could would be an amplification primitive against
2476
+ // the person's own hub, and a way to spend their rate-limit budget.
2477
+ '/_api/sync/resync': async (req: Request) => {
2478
+ if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
2479
+ if (!sameOriginWrite(req))
2480
+ return new Response('cross-origin write rejected', { status: 403 });
2481
+ if (!isTrustedRequestHost(req))
2482
+ return new Response('local request required', { status: 403 });
2483
+ const control = ctx.syncControl;
2484
+ if (!control) {
2485
+ return gitJson({
2486
+ status: 200,
2487
+ json: { ok: false, reason: 'no-supervisor', detail: 'Restart Maude to start syncing.' },
2488
+ });
2489
+ }
2490
+ // REFUSED, not queued — see `SyncSupervisor.busy`. A second press must
2491
+ // not buy a second full re-link of every canvas.
2492
+ if (control.busy?.()) {
2493
+ return gitJson({
2494
+ status: 409,
2495
+ json: { ok: false, reason: 'busy', detail: 'Syncing is already restarting.' },
2496
+ });
2497
+ }
2498
+ // No argument: KEEP the configured hub. Only an explicit `null` unlinks,
2499
+ // and this route must never be able to.
2500
+ const sync = await control.restart();
2501
+ return gitJson({ status: 200, json: { ok: true, sync } });
2502
+ },
2503
+
2504
+ '/_api/sync/cancel-assets': async (req: Request) => {
2505
+ // Cancel is scoped to the ASSET SWEEP: killing a reconnect mid-handshake
2506
+ // is not a meaningful gesture, killing a multi-hundred-megabyte upload
2507
+ // is. Safe by construction — uploads are idempotent and the hub writes
2508
+ // temp-then-rename, so no half-written asset can survive this.
2509
+ if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
2510
+ if (!sameOriginWrite(req))
2511
+ return new Response('cross-origin write rejected', { status: 403 });
2512
+ if (!isTrustedRequestHost(req))
2513
+ return new Response('local request required', { status: 403 });
2514
+ const cancelled = ctx.syncControl?.current?.()?.cancelAssetSweep() ?? false;
2515
+ return gitJson({ status: 200, json: { ok: true, cancelled } });
2516
+ },
2517
+
2444
2518
  '/_api/github/identity': async (req: Request) => {
2445
2519
  if (req.method !== 'GET') return new Response('Method not allowed', { status: 405 });
2446
2520
  if (!isTrustedRequestHost(req))
@@ -2548,6 +2622,26 @@ export function createHttp(
2548
2622
  return gitJson(await figmaApi.runImport(body));
2549
2623
  },
2550
2624
 
2625
+ // `--explode` — one artboard, via the LOCAL Dev Mode MCP (DDR-219 D2).
2626
+ //
2627
+ // Same triple gate, and it needs it more than the others: this route reaches
2628
+ // an unauthenticated loopback service that reads whatever Figma document the
2629
+ // user has open, and then WRITES into the design root. A canvas-reachable
2630
+ // version of it would hand the untrusted iframe (DDR-054) a primitive that
2631
+ // reads the user's open design — so it is in NEITHER canvas allowlist, the
2632
+ // same standing rule DDR-088 sets for every privileged route, asserted in
2633
+ // `test/canvas-origin-gate.test.ts` and by the grep test in
2634
+ // `cli/lib/figma-codegen-reachability.test.mjs`.
2635
+ '/_api/figma/explode': async (req: Request) => {
2636
+ if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
2637
+ if (!sameOriginWrite(req))
2638
+ return new Response('cross-origin write rejected', { status: 403 });
2639
+ if (!isTrustedRequestHost(req))
2640
+ return new Response('local request required', { status: 403 });
2641
+ const body = await readJson<unknown>(req, 8 * 1024);
2642
+ return gitJson(await figmaApi.explode(body));
2643
+ },
2644
+
2551
2645
  '/_api/github/create-project': async (req: Request) => {
2552
2646
  if (req.method !== 'POST') return new Response('Method not allowed', { status: 405 });
2553
2647
  if (!sameOriginWrite(req))
@@ -0,0 +1,84 @@
1
+ // The asset sweep, in its own process — feature-sync-resync-and-out-of-process-sweep.
2
+ //
3
+ // WHY A SEPARATE PROCESS.
4
+ //
5
+ // The sweep crashes Bun 1.3.3 *when it runs alongside the dev server*, proven by
6
+ // isolation: the identical sweep against the same live hub completes standalone
7
+ // (182/182, 0 failures) and crash-loops in-process (three `Segmentation fault at
8
+ // address 0x0` plus a bus error, then the supervisor gave up after 3 restarts).
9
+ // Nobody has a fix for the fault itself, so the boundary is the fix: a fault
10
+ // here costs the sweep, and the editor — canvases, browser UI, ACP panel —
11
+ // stays up. The parent reports the dead child as a failed sweep.
12
+ //
13
+ // It is also what makes a Resync button safe to offer at all: pressing it
14
+ // re-runs this, and a button that can kill the person's dev server is not a
15
+ // button.
16
+ //
17
+ // PROTOCOL. argv gives the design root, the hub URL and a file holding the
18
+ // credential. stdout carries NDJSON — one tagged JSON object per line:
19
+ //
20
+ // {"t":"progress", …AssetPushProgress} many, throttled by pushAssets
21
+ // {"t":"result", …AssetPushResult} exactly one, on a clean finish
22
+ // {"t":"error", "message": "…"} exactly one, on a thrown sweep
23
+ //
24
+ // Tagged rather than positional ("the last line is the result") so a parent
25
+ // reading a truncated stream can still tell a final answer from a progress
26
+ // tick. Nothing else may reach stdout — one stray `console.log` from an import
27
+ // would land mid-stream — so diagnostics go to stderr, which the parent logs.
28
+ //
29
+ // THE CREDENTIAL ARRIVES IN A FILE, NEVER IN ARGV. `ps` is world-readable on
30
+ // every platform this ships to; a hub token on a command line is a hub token
31
+ // handed to every other user on the machine. The file is written 0600 by the
32
+ // parent and unlinked when the sweep ends. It is re-read per call, not cached,
33
+ // so a parent that rewrites it after a silent renewal is picked up mid-sweep.
34
+
35
+ import { readFileSync } from 'node:fs';
36
+
37
+ import { type AssetPushProgress, type AssetPushResult, pushAssets } from './asset-push.ts';
38
+
39
+ const [, , designRoot, hubUrl, tokenFile] = process.argv;
40
+
41
+ /** One NDJSON line. Synchronous — ordering with the result line is the contract. */
42
+ function emit(line: { t: 'progress' | 'result' | 'error' } & Record<string, unknown>): void {
43
+ process.stdout.write(`${JSON.stringify(line)}\n`);
44
+ }
45
+
46
+ async function main(): Promise<void> {
47
+ if (!designRoot || !hubUrl || !tokenFile) {
48
+ emit({ t: 'error', message: 'usage: <designRoot> <hubUrl> <tokenFile>' });
49
+ process.exit(2);
50
+ }
51
+
52
+ // Read at call time — `pushAssets` asks per request, and a renewal mid-sweep
53
+ // rewrites the file underneath us. A read that fails (the parent unlinked it
54
+ // during teardown) falls back to the last value we saw rather than sending an
55
+ // empty Authorization, which the hub would answer 401 for every remaining
56
+ // file and report as 182 failures instead of one dead sweep.
57
+ let lastToken = '';
58
+ const token = (): string => {
59
+ try {
60
+ lastToken = readFileSync(tokenFile, 'utf8').trim() || lastToken;
61
+ } catch {
62
+ /* keep the last good one */
63
+ }
64
+ return lastToken;
65
+ };
66
+
67
+ const result: AssetPushResult = await pushAssets({
68
+ designRoot,
69
+ hubUrl,
70
+ token,
71
+ // stderr, never stdout — stdout is the protocol.
72
+ log: {
73
+ log: (...a: unknown[]) => process.stderr.write(`${a.join(' ')}\n`),
74
+ warn: (...a: unknown[]) => process.stderr.write(`${a.join(' ')}\n`),
75
+ },
76
+ onProgress: (p: AssetPushProgress) => emit({ t: 'progress', ...p }),
77
+ });
78
+ emit({ t: 'result', ...result });
79
+ }
80
+
81
+ main().catch((err) => {
82
+ emit({ t: 'error', message: String((err as Error)?.message ?? err).slice(0, 2000) });
83
+ process.exit(1);
84
+ });