@1agh/maude 0.43.0 → 0.45.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 (126) hide show
  1. package/apps/studio/acp/bridge.ts +44 -10
  2. package/apps/studio/acp/env.ts +23 -0
  3. package/apps/studio/acp/login-state.ts +263 -0
  4. package/apps/studio/acp/plugin-bootstrap.ts +31 -25
  5. package/apps/studio/acp/probe.ts +152 -1
  6. package/apps/studio/annotations-layer.tsx +396 -46
  7. package/apps/studio/api.ts +395 -2
  8. package/apps/studio/bin/_audio-search.mjs +140 -0
  9. package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
  10. package/apps/studio/bin/_import-asset.mjs +815 -0
  11. package/apps/studio/bin/_import-brand.mjs +635 -0
  12. package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
  13. package/apps/studio/bin/_import-tokens.mjs +1201 -0
  14. package/apps/studio/bin/_transcribe.mjs +419 -0
  15. package/apps/studio/bin/_transcribe.test.mjs +80 -0
  16. package/apps/studio/bin/audio-search.sh +28 -0
  17. package/apps/studio/bin/generate.sh +154 -0
  18. package/apps/studio/bin/import-asset.sh +34 -0
  19. package/apps/studio/bin/import-brand.sh +29 -0
  20. package/apps/studio/bin/import-tokens.sh +33 -0
  21. package/apps/studio/bin/photo-bg-remove.sh +119 -21
  22. package/apps/studio/bin/transcribe.sh +39 -0
  23. package/apps/studio/canvas-edit.ts +197 -2
  24. package/apps/studio/canvas-lib.tsx +115 -6
  25. package/apps/studio/client/app.jsx +1954 -685
  26. package/apps/studio/client/generate-dialog.jsx +352 -0
  27. package/apps/studio/client/github.js +14 -0
  28. package/apps/studio/client/inspector-controls.jsx +781 -0
  29. package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
  30. package/apps/studio/client/panels/ChatPanel.jsx +15 -33
  31. package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
  32. package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
  33. package/apps/studio/client/panels/ReadinessList.jsx +227 -7
  34. package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
  35. package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
  36. package/apps/studio/client/photo-knobs.jsx +167 -172
  37. package/apps/studio/client/styles/3-shell-maude.css +334 -28
  38. package/apps/studio/client/styles/4-components.css +223 -0
  39. package/apps/studio/client/styles/6-acp-chat.css +86 -0
  40. package/apps/studio/client/tour/quick-setup-tour.js +39 -0
  41. package/apps/studio/commands/annotation-strokes-command.ts +13 -3
  42. package/apps/studio/config.schema.json +70 -0
  43. package/apps/studio/context-menu.tsx +11 -2
  44. package/apps/studio/context.ts +16 -0
  45. package/apps/studio/design-setup-readiness.ts +115 -0
  46. package/apps/studio/dist/client.bundle.js +2097 -2088
  47. package/apps/studio/dist/styles.css +1 -1
  48. package/apps/studio/draw/palette.ts +34 -0
  49. package/apps/studio/footage/schema.test.ts +92 -0
  50. package/apps/studio/footage/schema.ts +179 -1
  51. package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
  52. package/apps/studio/generation/adapters/gemini.ts +542 -0
  53. package/apps/studio/generation/adapters/groq.test.ts +80 -0
  54. package/apps/studio/generation/adapters/groq.ts +247 -0
  55. package/apps/studio/generation/audio-library.test.ts +95 -0
  56. package/apps/studio/generation/audio-library.ts +156 -0
  57. package/apps/studio/generation/captions.test.ts +78 -0
  58. package/apps/studio/generation/captions.ts +132 -0
  59. package/apps/studio/generation/download.ts +76 -0
  60. package/apps/studio/generation/elevenlabs.test.ts +223 -0
  61. package/apps/studio/generation/gemini.test.ts +350 -0
  62. package/apps/studio/generation/jobs.test.ts +128 -0
  63. package/apps/studio/generation/jobs.ts +243 -0
  64. package/apps/studio/generation/keys.test.ts +75 -0
  65. package/apps/studio/generation/keys.ts +162 -0
  66. package/apps/studio/generation/prefs.test.ts +95 -0
  67. package/apps/studio/generation/prefs.ts +82 -0
  68. package/apps/studio/generation/registry.ts +88 -0
  69. package/apps/studio/generation/types.test.ts +123 -0
  70. package/apps/studio/generation/types.ts +266 -0
  71. package/apps/studio/generation/whisper-models.test.ts +152 -0
  72. package/apps/studio/generation/whisper-models.ts +257 -0
  73. package/apps/studio/generation/whisper-spike-results.md +61 -0
  74. package/apps/studio/git/service.ts +3 -1
  75. package/apps/studio/http.ts +867 -9
  76. package/apps/studio/media/intro.mp4 +0 -0
  77. package/apps/studio/media-commit-chain.ts +119 -0
  78. package/apps/studio/paths.ts +10 -0
  79. package/apps/studio/readiness.ts +108 -20
  80. package/apps/studio/scaffold-design.ts +1145 -0
  81. package/apps/studio/server.ts +13 -1
  82. package/apps/studio/test/_helpers.ts +15 -2
  83. package/apps/studio/test/acp-bridge.test.ts +34 -1
  84. package/apps/studio/test/acp-env.test.ts +30 -0
  85. package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
  86. package/apps/studio/test/acp-session-plugins.test.ts +72 -1
  87. package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
  88. package/apps/studio/test/annotations-layer.test.ts +174 -0
  89. package/apps/studio/test/canvas-origin-gate.test.ts +37 -0
  90. package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
  91. package/apps/studio/test/csrf-write-guard.test.ts +23 -1
  92. package/apps/studio/test/design-setup-readiness.test.ts +155 -0
  93. package/apps/studio/test/element-structural-edit.test.ts +108 -0
  94. package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
  95. package/apps/studio/test/generate-route.test.ts +106 -0
  96. package/apps/studio/test/import-asset-browser.test.ts +64 -0
  97. package/apps/studio/test/import-asset-route.test.ts +71 -0
  98. package/apps/studio/test/import-asset.test.ts +399 -0
  99. package/apps/studio/test/import-brand-browser.test.ts +88 -0
  100. package/apps/studio/test/import-brand-route.test.ts +120 -0
  101. package/apps/studio/test/import-brand.test.ts +206 -0
  102. package/apps/studio/test/import-tokens.test.ts +722 -0
  103. package/apps/studio/test/media-commit-chain.test.ts +210 -0
  104. package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -0
  105. package/apps/studio/test/readiness.test.ts +13 -11
  106. package/apps/studio/test/scaffold-design.test.ts +122 -0
  107. package/apps/studio/test/tour-overlay.test.tsx +16 -0
  108. package/apps/studio/tool-palette.tsx +11 -6
  109. package/apps/studio/ui-prefs.ts +130 -0
  110. package/apps/studio/use-canvas-media-drop.tsx +4 -2
  111. package/apps/studio/use-chrome-visibility.tsx +8 -2
  112. package/apps/studio/whats-new.json +96 -0
  113. package/apps/studio/ws.ts +5 -0
  114. package/cli/bin/maude.mjs +6 -6
  115. package/cli/commands/design.mjs +33 -0
  116. package/cli/commands/init.mjs +7 -1
  117. package/cli/lib/gitignore-block.mjs +1 -0
  118. package/cli/lib/pkg-root.mjs +107 -0
  119. package/cli/lib/pkg-root.test.mjs +79 -0
  120. package/cli/lib/reconstruct-toolset.test.mjs +194 -0
  121. package/cli/lib/update-check.mjs +8 -0
  122. package/package.json +12 -10
  123. package/plugins/design/dependencies.json +18 -0
  124. package/plugins/flow/.claude-plugin/config.schema.json +5 -0
  125. package/plugins/flow/templates/ai-skeleton/scenario-guide.md +39 -0
  126. package/plugins/flow/templates/ai-skeleton/scenarios/README.md +2 -0
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ # import-asset.sh — hardened local-file SVG/PDF ingestion. Thin shim over
3
+ # _import-asset.mjs; reached via `maude design import-asset` (DDR-062), never
4
+ # a raw bin path. See _import-asset.mjs + DDR-167 for the full security
5
+ # rationale. Requires bun (not the node/bun fallback fetch-asset.sh uses) —
6
+ # _import-asset.mjs imports the `.ts` SVGO engine module directly, same
7
+ # constraint as svg-optimize.sh.
8
+ #
9
+ # Usage:
10
+ # import-asset.sh <local-path> --root <repo> [--design-root .design]
11
+ # [--kind svg|pdf|raster] [--json]
12
+ #
13
+ # stdout on success = one reference path per line (e.g. /assets/a44d3d60.svg).
14
+ # PDF import is not yet available (DDR-167 addendum) — the verb accepts a
15
+ # .pdf input and fails loud naming why, rather than silently rejecting it as
16
+ # an unknown flag. `raster` (PNG/JPEG, magic-byte sniffed) content-addresses
17
+ # a local image as-is — added for DDR-174/T15's vision-reconstruction source.
18
+ #
19
+ # Exit: 0 ok · 1 not-yet-available/other · 2 usage · 3 sanitize/validation
20
+ # reject · 4 read/parse error · 5 unsupported media type ·
21
+ # 6 write/containment error.
22
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
23
+
24
+ case "$1" in
25
+ --help|-h) sed -n '2,17p' "$0" | sed 's/^# \?//'; exit 0 ;;
26
+ esac
27
+
28
+ if ! command -v bun >/dev/null 2>&1; then
29
+ echo "import-asset.sh: bun is required (hard dependency — see plugins/design/dependencies.json)." >&2
30
+ echo " Install: curl -fsSL https://bun.sh/install | bash" >&2
31
+ exit 1
32
+ fi
33
+
34
+ exec bun run "$SCRIPT_DIR/_import-asset.mjs" "$@"
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ # import-brand.sh — brand-file typed-cue extraction (DDR-173). Thin shim over
3
+ # _import-brand.mjs; reached via `maude design import-brand` (DDR-062), never
4
+ # a raw bin path. Operates ONLY on an already-DDR-167-sanitized SVG asset
5
+ # (as produced by `maude design import-asset`) — never re-reads or
6
+ # re-sanitizes the original brand file (DDR-173 Decision 2: no parallel,
7
+ # ungated read path). Requires bun — _import-brand.mjs imports
8
+ # _import-tokens.mjs's `.mjs` grammar reuse and happy-dom directly.
9
+ #
10
+ # Usage:
11
+ # import-brand.sh <sanitized-svg-path> --root <repo> [--design-root .design] [--json]
12
+ #
13
+ # stdout on success = the typed payload `{ palette, fonts, logoRef, logoRasterRef }`.
14
+ #
15
+ # Exit: 0 ok · 1 other · 2 usage · 3 validation/hardening reject ·
16
+ # 4 read/parse error · 6 write/containment error.
17
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
18
+
19
+ case "$1" in
20
+ --help|-h) sed -n '2,15p' "$0" | sed 's/^# \?//'; exit 0 ;;
21
+ esac
22
+
23
+ if ! command -v bun >/dev/null 2>&1; then
24
+ echo "import-brand.sh: bun is required (hard dependency — see plugins/design/dependencies.json)." >&2
25
+ echo " Install: curl -fsSL https://bun.sh/install | bash" >&2
26
+ exit 1
27
+ fi
28
+
29
+ exec bun run "$SCRIPT_DIR/_import-brand.mjs" "$@"
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env bash
2
+ # import-tokens.sh — token-file import (W3C design-tokens / Style-Dictionary
3
+ # JSON / raw CSS custom properties -> the DS CSS-variable contract). Thin
4
+ # shim over _import-tokens.mjs; reached via `maude design import-tokens`
5
+ # (DDR-062), never a raw bin path. See _import-tokens.mjs + DDR-172 for the
6
+ # full mapping contract + security rationale. Requires bun — _import-tokens.mjs
7
+ # imports the `.ts` palette/paths modules directly, same constraint as
8
+ # import-asset.sh.
9
+ #
10
+ # All args are forwarded quoted ("$@") — never unquoted or eval-expanded
11
+ # (DDR-172 Decision 8 wrapper-hygiene requirement; the charset/shape
12
+ # validation itself happens in _import-tokens.mjs, not here).
13
+ #
14
+ # Usage:
15
+ # import-tokens.sh <token-file> --root <repo> [--design-root .design]
16
+ # [--ds <name> | --new-ds <name>] [--theme <name>]
17
+ # [--force-insert] [--json]
18
+ #
19
+ # Exit: 0 ok · 1 other · 2 usage · 3 validation/mapping reject ·
20
+ # 4 read/parse error · 5 unsupported format · 6 write/containment error.
21
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
22
+
23
+ case "$1" in
24
+ --help|-h) sed -n '2,16p' "$0" | sed 's/^# \?//'; exit 0 ;;
25
+ esac
26
+
27
+ if ! command -v bun >/dev/null 2>&1; then
28
+ echo "import-tokens.sh: bun is required (hard dependency — see plugins/design/dependencies.json)." >&2
29
+ echo " Install: curl -fsSL https://bun.sh/install | bash" >&2
30
+ exit 1
31
+ fi
32
+
33
+ exec bun run "$SCRIPT_DIR/_import-tokens.mjs" "$@"
@@ -16,6 +16,19 @@
16
16
  #
17
17
  # Reached via `maude design photo-bg-remove` (never a raw bin path — DDR-062).
18
18
  #
19
+ # Security hardening (fix-photo-editor-followup-debt, Stage A): `--asset` is
20
+ # validated against the strict `assets/<sha8>.<ext>` shape (mirrors
21
+ # photo-store.ts's SHA_RE) rather than a loose glob — a permissive check
22
+ # previously let a crafted `--asset` value splice unescaped content into the
23
+ # generated harness `.tsx`. The harness-file write is symlink-safe (write to a
24
+ # freshly O_EXCL-created tmp path, then atomic `mv` into place — `mv`/rename(2)
25
+ # replaces a destination symlink's OWN dirent rather than following it, so even
26
+ # a pre-planted symlink at the harness path can't redirect the write into a
27
+ # real reviewed canvas file). The stdout deliverable is shape-validated before
28
+ # print. `--slug` failure hard-fails instead of downgrading to an unsanitized
29
+ # fallback. Concurrent invocations against the SAME asset serialize via a
30
+ # `mkdir`-based lock (portable — `flock` isn't available on macOS by default).
31
+ #
19
32
  # Usage:
20
33
  # photo-bg-remove.sh --asset <assets/<sha8>.<ext>> [--root <repo>]
21
34
  # [--slug <name>] [--timeout <secs>]
@@ -29,11 +42,16 @@
29
42
  # Stderr: progress / diagnostics.
30
43
  # Exit: 0 success / 1 missing input, server, or agent-browser / 2 bad args
31
44
  # / 3 drive/navigation failed / 4 the harness itself reported an error
32
- # / 5 timed out waiting for the ML pass to finish.
45
+ # (or produced a malformed result) / 5 timed out waiting for the ML
46
+ # pass to finish / 6 another run is already in progress for this asset
33
47
 
34
48
  set -uo pipefail
35
49
 
36
50
  ASSET="" SLUG="" REPO="" TIMEOUT=150
51
+ # Mirrors photo-store.ts's SHA_RE shape: lowercase hex sha8 (8-64 chars),
52
+ # case-insensitive extension. Shared between --asset validation (Task 1) and
53
+ # the stdout-deliverable validation (Task 3) so the two checks can't drift.
54
+ ASSET_SHAPE_RE='^assets/[0-9a-f]{8,64}\.[a-zA-Z0-9]+$'
37
55
 
38
56
  while [ $# -gt 0 ]; do
39
57
  case "$1" in
@@ -42,7 +60,7 @@ while [ $# -gt 0 ]; do
42
60
  --root) REPO="$2"; shift 2 ;;
43
61
  --timeout) TIMEOUT="$2"; shift 2 ;;
44
62
  --help|-h)
45
- sed -n '2,29p' "$0" | sed 's/^# \?//'
63
+ sed -n '2,39p' "$0" | sed 's/^# \?//'
46
64
  exit 0
47
65
  ;;
48
66
  *)
@@ -55,14 +73,42 @@ done
55
73
  [ -n "$ASSET" ] || { echo "photo-bg-remove.sh: --asset <assets/<sha8>.<ext>> required" >&2; exit 2; }
56
74
  # Strip a leading designRoot-relative slash/prefix if the caller passed one.
57
75
  ASSET="${ASSET#/}"
58
- case "$ASSET" in
59
- assets/*) : ;;
60
- *) echo "photo-bg-remove.sh: --asset must look like assets/<sha8>.<ext>, got '$ASSET'" >&2; exit 2 ;;
61
- esac
76
+ # Strict shape check (Task 1) — replaces the old `case "$ASSET" in assets/*)`
77
+ # glob, which only checked the PREFIX and let anything after it (quotes,
78
+ # backticks, `../`, JSX-breaking sequences) ride unescaped into the generated
79
+ # harness .tsx below. `[[ =~ ]]` against a character-class regex is the bash
80
+ # equivalent of photo-store.ts's `SHA_RE` hex check — `..`, `/`, quotes, and
81
+ # any non-hex/non-alnum content can never match, so nothing but a clean
82
+ # `assets/<sha8>.<ext>` reference survives validation. Captures the bare sha8
83
+ # for reuse as a filesystem-safe lock key (Task 6).
84
+ if [[ "$ASSET" =~ ^assets/([0-9a-f]{8,64})\.[a-zA-Z0-9]+$ ]]; then
85
+ ASSET_KEY="${BASH_REMATCH[1]}"
86
+ else
87
+ echo "photo-bg-remove.sh: --asset must look like assets/<sha8>.<ext> (lowercase hex, 8-64 chars), got '$ASSET'" >&2
88
+ exit 2
89
+ fi
62
90
 
63
91
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
64
92
  [ -n "$SLUG" ] || SLUG=$(basename "$ASSET" | sed -E 's/\.[^.]+$//')
65
- SLUG=$(bash "$SCRIPT_DIR/slug.sh" "$SLUG" 2>/dev/null || printf '%s' "$SLUG" | tr '[:upper:]' '[:lower:]')
93
+ # Hard-fail (Task 4) instead of downgrading to a lowercased-but-unstripped
94
+ # fallback on slug.sh failure — a safety subprocess's failure must never
95
+ # silently continue with a less-safe code path. This repo has documented
96
+ # history (DDR-045) of sibling-script resolution breaking specifically in
97
+ # packaged/compiled distributions, so this failure mode is realistic.
98
+ # (SLUG_INPUT is captured separately so a failed substitution — which still
99
+ # assigns its, here empty, stdout to $SLUG — doesn't blank the diagnostic.)
100
+ SLUG_INPUT="$SLUG"
101
+ if ! SLUG="$(bash "$SCRIPT_DIR/slug.sh" "$SLUG_INPUT" 2>/dev/null)"; then
102
+ echo "photo-bg-remove.sh: slug.sh failed to normalize slug '$SLUG_INPUT' — refusing to fall back to an unsanitized value" >&2
103
+ exit 1
104
+ fi
105
+ # Assigned this early (not just before the agent-browser open call below) so
106
+ # it's always bound by the time `trap cleanup EXIT` is installed — a security
107
+ # review of this plan found that `cleanup()` unconditionally reads $SESSION,
108
+ # and under `set -u` an exit on one of the harness-write failure paths (before
109
+ # SESSION used to be assigned) would abort mid-cleanup on the unbound
110
+ # variable, skipping the `rmdir "$LOCK_DIR"` below it and leaking the lock.
111
+ SESSION="photo-bgremove-$SLUG-$$"
66
112
 
67
113
  # ---------- resolve repo + design root + port ----------
68
114
  if [ -z "$REPO" ]; then
@@ -97,29 +143,69 @@ if [ -z "${AGENT_BROWSER_EXECUTABLE_PATH:-}" ]; then
97
143
  [ -n "$BROWSER_PATH" ] && export AGENT_BROWSER_EXECUTABLE_PATH="$BROWSER_PATH"
98
144
  fi
99
145
 
100
- # ---------- generate the harness canvas ----------
146
+ # ---------- per-asset concurrency guard (Task 6) ----------
147
+ # `mkdir` is atomic on every POSIX filesystem and needs no extra binary —
148
+ # `flock` isn't available on macOS by default, so this is the portable choice.
149
+ # Keyed on the asset's sha8 (already validated hex — safe as a path segment),
150
+ # NOT global, so two different assets still run concurrently. Fail-fast rather
151
+ # than queue/wait: a full job queue is out of scope for this backstop, whose
152
+ # only job is refusing to let a runaway loop spawn unbounded concurrent
153
+ # browser+WASM processes against the same asset.
101
154
  PHOTO_DIR="$DESIGN_ROOT/_photo"
102
155
  mkdir -p "$PHOTO_DIR"
156
+ LOCK_DIR="$PHOTO_DIR/.lock.$ASSET_KEY"
157
+ TMP_HARNESS=""
158
+ if ! mkdir "$LOCK_DIR" 2>/dev/null; then
159
+ echo "photo-bg-remove.sh: another photo-bg-remove run is already in progress for asset '$ASSET' — refusing to run concurrently against the same asset (lock: ${LOCK_DIR#$REPO/})" >&2
160
+ exit 6
161
+ fi
162
+
163
+ cleanup() {
164
+ "$AB" --session "$SESSION" close >/dev/null 2>&1 || true
165
+ [ -n "$TMP_HARNESS" ] && rm -f "$TMP_HARNESS" 2>/dev/null
166
+ rmdir "$LOCK_DIR" 2>/dev/null || true
167
+ }
168
+ trap cleanup EXIT
169
+
170
+ # ---------- generate the harness canvas (symlink-safe — Task 2) ----------
171
+ # Write to a freshly O_CREAT|O_EXCL-created tmp path in the SAME directory
172
+ # (same filesystem, so the rename below is atomic), then `mv` into place.
173
+ # `mv`/rename(2) replaces the destination NAME, never dereferences it — so
174
+ # even a pre-planted symlink at $HARNESS_TSX gets its dirent swapped out
175
+ # rather than followed, unlike the bare `{ ... } > "$HARNESS_TSX"` truncating
176
+ # redirect this replaces (which WOULD follow a symlink and could splice
177
+ # attacker-controlled JSX into whatever real file it pointed at). `set -C`
178
+ # (noclobber) makes bash open the tmp path with O_EXCL, so a pre-existing tmp
179
+ # path (including a dangling symlink) fails the write instead of following it
180
+ # — the bash equivalent of sync/atomic-write.ts's Node `wx` flag (DDR-054 §2c).
103
181
  HARNESS_TSX="$PHOTO_DIR/$SLUG.bgremove.tsx"
104
- {
105
- echo "import { PhotoBgRemoveHarness } from '@maude/canvas-lib';"
106
- echo
107
- echo "// AUTO-GENERATED by \`maude design photo-bg-remove\` — headless bg-removal proof"
108
- echo "// harness. Lives under _photo/ (gitignored); safe to delete + regenerate."
109
- echo "export default function PhotoBgRemoveCanvas() {"
110
- printf ' return <PhotoBgRemoveHarness source=%s />;\n' "\"$ASSET\""
111
- echo "}"
112
- } > "$HARNESS_TSX"
182
+ TMP_HARNESS="$PHOTO_DIR/.$SLUG.bgremove.tsx.tmp.$$.$RANDOM$RANDOM"
183
+ if ! (
184
+ set -C
185
+ {
186
+ echo "import { PhotoBgRemoveHarness } from '@maude/canvas-lib';"
187
+ echo
188
+ echo "// AUTO-GENERATED by \`maude design photo-bg-remove\` — headless bg-removal proof"
189
+ echo "// harness. Lives under _photo/ (gitignored); safe to delete + regenerate."
190
+ echo "export default function PhotoBgRemoveCanvas() {"
191
+ printf ' return <PhotoBgRemoveHarness source=%s />;\n' "\"$ASSET\""
192
+ echo "}"
193
+ } > "$TMP_HARNESS"
194
+ ); then
195
+ echo "photo-bg-remove.sh: failed to create harness scratch file (possible pre-existing path at $TMP_HARNESS)" >&2
196
+ exit 1
197
+ fi
198
+ if ! mv "$TMP_HARNESS" "$HARNESS_TSX"; then
199
+ echo "photo-bg-remove.sh: atomic rename of harness file failed" >&2
200
+ exit 1
201
+ fi
202
+ TMP_HARNESS="" # renamed away — nothing left for cleanup() to remove
113
203
  echo "→ harness canvas: ${HARNESS_TSX#$REPO/}" >&2
114
204
 
115
205
  REL="_photo/$SLUG.bgremove.tsx"
116
206
  URL="http://localhost:$PORT/_canvas-shell.html?canvas=$REL"
117
- SESSION="photo-bgremove-$SLUG-$$"
118
207
  SEL="[data-photo-bgremove-status]"
119
208
 
120
- cleanup() { "$AB" --session "$SESSION" close >/dev/null 2>&1 || true; }
121
- trap cleanup EXIT
122
-
123
209
  echo "→ driving $URL (session $SESSION)" >&2
124
210
  if ! "$AB" --session "$SESSION" open "$URL" >&2; then
125
211
  echo "photo-bg-remove.sh: agent-browser failed to open the harness canvas" >&2
@@ -154,7 +240,19 @@ fi
154
240
 
155
241
  RESULT="$("$AB" --session "$SESSION" get attr "$SEL" data-photo-bgremove-result 2>/dev/null | tr -d '"')"
156
242
  [ -n "$RESULT" ] || { echo "photo-bg-remove.sh: harness reported done but no result asset" >&2; exit 4; }
243
+ # Shape-validate the deliverable (Task 3) before it's ever trusted as far as
244
+ # stdout — the harness runs inside the (untrusted, DDR-054) canvas origin, so
245
+ # a malformed/crafted `data-photo-bgremove-result` value is treated exactly
246
+ # like the empty-result case above rather than printed verbatim.
247
+ if [[ ! "$RESULT" =~ $ASSET_SHAPE_RE ]]; then
248
+ echo "photo-bg-remove.sh: harness reported a malformed result asset '$RESULT'" >&2
249
+ exit 4
250
+ fi
157
251
 
158
252
  echo "→ background removed → $RESULT" >&2
159
253
  # Last stdout line = the new matte asset path, for $(maude design photo-bg-remove ...) capture.
160
254
  printf '%s\n' "$RESULT"
255
+
256
+ # Clean up the harness scratch file on the SUCCESS path only (Task 5) — left
257
+ # in place on any error exit above, where it's useful for debugging.
258
+ rm -f "$HARNESS_TSX"
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bash
2
+ # transcribe.sh — feature-ai-media-generation (Phase 2, DDR-164). Local, no-key
3
+ # speech-to-text for automatic subtitles: run whisper.cpp on an audio/video file
4
+ # and emit SRT/VTT through the shared captions.ts reflow. The DEFAULT subtitle
5
+ # path — free, offline, private, word-timestamped — with cloud STT (ElevenLabs
6
+ # Scribe / Groq Whisper) as the fallback when whisper.cpp isn't installed.
7
+ #
8
+ # Reached via `maude design transcribe` (never a raw bin path — DDR-062). No dev
9
+ # server needed; the work is a local whisper.cpp spawn (mirrors probe-footage).
10
+ #
11
+ # Usage:
12
+ # transcribe.sh --source <assets/x.mp4|file> [--root <repo>]
13
+ # [--provider whisper|elevenlabs|groq]
14
+ # [--format srt|vtt|both] [--model <id>] [--whisper <bin>]
15
+ # [--lang <code>] [--segments]
16
+ #
17
+ # The engine is an EXPLICIT user choice (Task 2.6, DDR-164), never an automatic
18
+ # fallback: --provider wins, else the project's generation.transcription.provider
19
+ # config, else local whisper — and it SAYS which it chose. A chosen cloud engine
20
+ # (elevenlabs / groq) needs the dev server running (the key is resolved
21
+ # server-side) and an assets/<sha8> source; whisper is local, free, no key.
22
+ #
23
+ # whisper.cpp is a SOFT dependency (plugins/design/dependencies.json). A ggml
24
+ # model is required (--model / $MAUDE_WHISPER_MODEL); whisper.cpp ships no default
25
+ # and does not auto-download. See _transcribe.mjs for the download pointer.
26
+ #
27
+ # Stdout (last line): the produced .srt path (for $(...) capture).
28
+ # Exit: 0 ok / 1 dependency-or-input problem / 3 transcription failed.
29
+
30
+ set -euo pipefail
31
+
32
+ if ! command -v bun >/dev/null 2>&1; then
33
+ echo "transcribe.sh: bun is required (hard dependency — see plugins/design/dependencies.json)." >&2
34
+ echo " Install: curl -fsSL https://bun.sh/install | bash" >&2
35
+ exit 1
36
+ fi
37
+
38
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
39
+ exec bun run "$SCRIPT_DIR/_transcribe.mjs" "$@"
@@ -3406,8 +3406,14 @@ function collectJsxByTag(program: AnyNode, tagName: string): AnyNode[] {
3406
3406
  * `width="430"` and break the numeric prop. Overwrites an existing `{expr}` or
3407
3407
  * `"literal"`, or inserts the attribute after the tag name if missing.
3408
3408
  */
3409
- function writeNumericAttr(s: MagicString, opening: AnyNode, name: string, value: number): void {
3410
- const num = Math.max(1, Math.round(value));
3409
+ function writeNumericAttr(
3410
+ s: MagicString,
3411
+ opening: AnyNode,
3412
+ name: string,
3413
+ value: number,
3414
+ min = 1
3415
+ ): void {
3416
+ const num = Math.max(min, Math.round(value));
3411
3417
  const attr = findAttribute(opening, name);
3412
3418
  if (attr) {
3413
3419
  const v = attr.value;
@@ -3494,6 +3500,195 @@ export async function resizeArtboard(
3494
3500
  });
3495
3501
  }
3496
3502
 
3503
+ /**
3504
+ * Write/clear a BARE JSX boolean attribute (`<DCArtboard fixed>`, not
3505
+ * `fixed={true}` or a string). `value=false` reuses `removeStringAttr`'s
3506
+ * generic span-plus-leading-whitespace removal (it doesn't care whether the
3507
+ * attribute it's removing has a value) — only the `true` insert path is new.
3508
+ */
3509
+ function writeBooleanAttr(
3510
+ s: MagicString,
3511
+ opening: AnyNode,
3512
+ name: string,
3513
+ value: boolean,
3514
+ source: string
3515
+ ): void {
3516
+ if (!value) {
3517
+ removeStringAttr(s, opening, name, source);
3518
+ return;
3519
+ }
3520
+ if (findAttribute(opening, name)) return; // already present in some form
3521
+ const insertAt: number | undefined = opening?.name?.end;
3522
+ if (typeof insertAt !== 'number') throw new Error('Opening element has no resolvable name range');
3523
+ s.appendLeft(insertAt, ` ${name}`);
3524
+ }
3525
+
3526
+ /**
3527
+ * Toggle an artboard's height sizing mode (Hug ⇄ Fixed CSS-panel control).
3528
+ * `fixed=true` adds the bare `fixed` prop and, when `freezeHeight` is given
3529
+ * (the board's current measured height), also writes it as the exact
3530
+ * `height` — so pinning to Fixed doesn't snap the box back to whatever the
3531
+ * JSX `height` floor happened to be. `fixed=false` removes the `fixed` prop;
3532
+ * `height` is left as-is (it resumes being the hug floor). Same id-prop
3533
+ * addressing as applyResizeArtboard (DDR-027 — an artboard frame carries no
3534
+ * data-cd-id). Pure; reparse-gated.
3535
+ */
3536
+ export function applySetArtboardHug(
3537
+ canvasAbsPath: string,
3538
+ source: string,
3539
+ artboardId: string,
3540
+ fixed: boolean,
3541
+ freezeHeight?: number
3542
+ ): { source: string } {
3543
+ const parsed = parseSync(canvasAbsPath, source, { sourceType: 'module' });
3544
+ if (parsed.errors && parsed.errors.length > 0) {
3545
+ throw new CanvasEditError(
3546
+ `oxc-parser failed on ${canvasAbsPath}: ${parsed.errors[0]?.message ?? 'unknown'}`,
3547
+ { canvas: canvasAbsPath, id: artboardId }
3548
+ );
3549
+ }
3550
+ const artboards = collectJsxByTag(parsed.program, 'DCArtboard');
3551
+ const target = artboards.find((a) => getStringAttr(a.openingElement, 'id') === artboardId);
3552
+ if (!target) {
3553
+ throw new CanvasEditError(`<DCArtboard id="${artboardId}"> not found in ${canvasAbsPath}`, {
3554
+ canvas: canvasAbsPath,
3555
+ id: artboardId,
3556
+ });
3557
+ }
3558
+ const s = new MagicString(source);
3559
+ writeBooleanAttr(s, target.openingElement, 'fixed', fixed, source);
3560
+ if (fixed && typeof freezeHeight === 'number') {
3561
+ writeNumericAttr(s, target.openingElement, 'height', freezeHeight);
3562
+ }
3563
+ const out = s.toString();
3564
+ const check = parseSync(canvasAbsPath, out, { sourceType: 'module' });
3565
+ if (check.errors && check.errors.length > 0) {
3566
+ throw new CanvasEditError(
3567
+ `artboard hug-toggle produced invalid source (${check.errors[0]?.message ?? 'parse error'})`,
3568
+ { canvas: canvasAbsPath, id: artboardId }
3569
+ );
3570
+ }
3571
+ return { source: out };
3572
+ }
3573
+
3574
+ /** Set an artboard's Hug/Fixed mode on disk (atomic write + cross-process lock). */
3575
+ export async function setArtboardHug(
3576
+ canvasAbsPath: string,
3577
+ artboardId: string,
3578
+ fixed: boolean,
3579
+ freezeHeight?: number
3580
+ ): Promise<{ source: string }> {
3581
+ return withLock(canvasAbsPath, async () => {
3582
+ const file = Bun.file(canvasAbsPath);
3583
+ if (!(await file.exists())) {
3584
+ throw new CanvasEditError(`Canvas not found: ${canvasAbsPath}`, {
3585
+ canvas: canvasAbsPath,
3586
+ id: artboardId,
3587
+ });
3588
+ }
3589
+ const source = await file.text();
3590
+ const next = applySetArtboardHug(canvasAbsPath, source, artboardId, fixed, freezeHeight);
3591
+ if (next.source === source) return next;
3592
+ const tmp = `${canvasAbsPath}.tmp.${Math.random().toString(36).slice(2, 10)}`;
3593
+ await Bun.write(tmp, next.source);
3594
+ const { rename } = await import('node:fs/promises');
3595
+ await rename(tmp, canvasAbsPath);
3596
+ return next;
3597
+ });
3598
+ }
3599
+
3600
+ /** Patch shape for {@link applySetArtboardStyle} — `null` resets/removes that
3601
+ * prop back to the engine default; `undefined`/absent leaves it untouched. */
3602
+ export interface ArtboardStylePatch {
3603
+ background?: string | null;
3604
+ padding?: number | null;
3605
+ layout?: string | null;
3606
+ gap?: number | null;
3607
+ }
3608
+
3609
+ /**
3610
+ * Write the artboard "more settings" props (background / padding / layout /
3611
+ * gap) — applied to `.dc-artboard-body` by DCArtboard, not the frame chrome.
3612
+ * String props (background/layout) reuse `editStringAttr`/`removeStringAttr`;
3613
+ * numeric props (padding/gap) reuse `writeNumericAttr` with a 0 floor (unlike
3614
+ * width/height, 0 padding/gap is a normal value, not a degenerate one). Same
3615
+ * id-prop addressing + reparse-gate as applyResizeArtboard/applySetArtboardHug.
3616
+ */
3617
+ export function applySetArtboardStyle(
3618
+ canvasAbsPath: string,
3619
+ source: string,
3620
+ artboardId: string,
3621
+ patch: ArtboardStylePatch
3622
+ ): { source: string } {
3623
+ const parsed = parseSync(canvasAbsPath, source, { sourceType: 'module' });
3624
+ if (parsed.errors && parsed.errors.length > 0) {
3625
+ throw new CanvasEditError(
3626
+ `oxc-parser failed on ${canvasAbsPath}: ${parsed.errors[0]?.message ?? 'unknown'}`,
3627
+ { canvas: canvasAbsPath, id: artboardId }
3628
+ );
3629
+ }
3630
+ const artboards = collectJsxByTag(parsed.program, 'DCArtboard');
3631
+ const target = artboards.find((a) => getStringAttr(a.openingElement, 'id') === artboardId);
3632
+ if (!target) {
3633
+ throw new CanvasEditError(`<DCArtboard id="${artboardId}"> not found in ${canvasAbsPath}`, {
3634
+ canvas: canvasAbsPath,
3635
+ id: artboardId,
3636
+ });
3637
+ }
3638
+ const s = new MagicString(source);
3639
+ const opening = target.openingElement;
3640
+ if ('background' in patch) {
3641
+ if (patch.background == null) removeStringAttr(s, opening, 'background', source);
3642
+ else editStringAttr(s, opening, 'background', patch.background, canvasAbsPath, artboardId);
3643
+ }
3644
+ if ('layout' in patch) {
3645
+ if (patch.layout == null) removeStringAttr(s, opening, 'layout', source);
3646
+ else editStringAttr(s, opening, 'layout', patch.layout, canvasAbsPath, artboardId);
3647
+ }
3648
+ if ('padding' in patch) {
3649
+ if (patch.padding == null) removeStringAttr(s, opening, 'padding', source);
3650
+ else writeNumericAttr(s, opening, 'padding', patch.padding, 0);
3651
+ }
3652
+ if ('gap' in patch) {
3653
+ if (patch.gap == null) removeStringAttr(s, opening, 'gap', source);
3654
+ else writeNumericAttr(s, opening, 'gap', patch.gap, 0);
3655
+ }
3656
+ const out = s.toString();
3657
+ const check = parseSync(canvasAbsPath, out, { sourceType: 'module' });
3658
+ if (check.errors && check.errors.length > 0) {
3659
+ throw new CanvasEditError(
3660
+ `artboard style edit produced invalid source (${check.errors[0]?.message ?? 'parse error'})`,
3661
+ { canvas: canvasAbsPath, id: artboardId }
3662
+ );
3663
+ }
3664
+ return { source: out };
3665
+ }
3666
+
3667
+ /** Set an artboard's style props on disk (atomic write + cross-process lock). */
3668
+ export async function setArtboardStyle(
3669
+ canvasAbsPath: string,
3670
+ artboardId: string,
3671
+ patch: ArtboardStylePatch
3672
+ ): Promise<{ source: string }> {
3673
+ return withLock(canvasAbsPath, async () => {
3674
+ const file = Bun.file(canvasAbsPath);
3675
+ if (!(await file.exists())) {
3676
+ throw new CanvasEditError(`Canvas not found: ${canvasAbsPath}`, {
3677
+ canvas: canvasAbsPath,
3678
+ id: artboardId,
3679
+ });
3680
+ }
3681
+ const source = await file.text();
3682
+ const next = applySetArtboardStyle(canvasAbsPath, source, artboardId, patch);
3683
+ if (next.source === source) return next;
3684
+ const tmp = `${canvasAbsPath}.tmp.${Math.random().toString(36).slice(2, 10)}`;
3685
+ await Bun.write(tmp, next.source);
3686
+ const { rename } = await import('node:fs/promises');
3687
+ await rename(tmp, canvasAbsPath);
3688
+ return next;
3689
+ });
3690
+ }
3691
+
3497
3692
  /**
3498
3693
  * Delete the `<DCArtboard id="…">` whose `id` prop equals `artboardId` — the
3499
3694
  * artboard counterpart of applyDeleteElement (an artboard is addressed by its id