@1agh/maude 0.42.0 → 0.43.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/.ai/cache/_stats.json +7 -0
- package/apps/studio/acp/bootstrap-brief.ts +12 -2
- package/apps/studio/annotations-layer.tsx +1055 -278
- package/apps/studio/annotations-model.ts +58 -0
- package/apps/studio/api.ts +173 -22
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
- package/apps/studio/bin/_video-playwright.mjs +141 -71
- package/apps/studio/bin/ingest-footage.sh +27 -0
- package/apps/studio/bin/photo-adjust.sh +163 -0
- package/apps/studio/bin/photo-bg-remove.sh +160 -0
- package/apps/studio/bin/probe-footage.sh +28 -0
- package/apps/studio/bin/read-annotations.mjs +125 -2
- package/apps/studio/canvas-edit.ts +459 -10
- package/apps/studio/canvas-lib.tsx +523 -5
- package/apps/studio/canvas-pipeline.ts +65 -7
- package/apps/studio/canvas-shell.tsx +156 -16
- package/apps/studio/client/app.jsx +451 -34
- package/apps/studio/client/export-center.jsx +35 -1
- package/apps/studio/client/github.js +4 -0
- package/apps/studio/client/panels/GitPanel.jsx +8 -0
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
- package/apps/studio/client/panels/StickerPicker.jsx +160 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
- package/apps/studio/client/photo-knobs.jsx +437 -0
- package/apps/studio/client/styles/3-shell-maude.css +42 -0
- package/apps/studio/commands/edit-source-command.ts +31 -4
- package/apps/studio/context-menu.tsx +31 -6
- package/apps/studio/dist/client.bundle.js +5561 -19
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
- package/apps/studio/dist/runtime/pixi-js.js +519 -519
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +25 -0
- package/apps/studio/exporters/jobs.ts +47 -5
- package/apps/studio/exporters/video.ts +24 -4
- package/apps/studio/footage/schema.test.ts +195 -0
- package/apps/studio/footage/schema.ts +484 -0
- package/apps/studio/footage-store.ts +235 -0
- package/apps/studio/fs-watch.ts +10 -0
- package/apps/studio/git/endpoints.ts +68 -7
- package/apps/studio/git/service.ts +196 -20
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +173 -14
- package/apps/studio/input-router.tsx +10 -0
- package/apps/studio/inspect.ts +53 -1
- package/apps/studio/paths.ts +10 -0
- package/apps/studio/photo/filters.ts +285 -0
- package/apps/studio/photo/pipeline.ts +607 -0
- package/apps/studio/photo/schema.ts +367 -0
- package/apps/studio/photo-store.ts +154 -0
- package/apps/studio/runtime-bundle.ts +29 -6
- package/apps/studio/server.ts +5 -1
- package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
- package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
- package/apps/studio/stickers/life-style/best.png +0 -0
- package/apps/studio/stickers/life-style/come-on.png +0 -0
- package/apps/studio/stickers/life-style/cut.png +0 -0
- package/apps/studio/stickers/life-style/detox.png +0 -0
- package/apps/studio/stickers/life-style/just.png +0 -0
- package/apps/studio/stickers/life-style/manifest.json +68 -0
- package/apps/studio/stickers/life-style/mirror.png +0 -0
- package/apps/studio/stickers/life-style/nice.png +0 -0
- package/apps/studio/stickers/life-style/objects.png +0 -0
- package/apps/studio/stickers/life-style/ok.png +0 -0
- package/apps/studio/stickers/life-style/queen.png +0 -0
- package/apps/studio/stickers/life-style/star.png +0 -0
- package/apps/studio/stickers/life-style/sun.png +0 -0
- package/apps/studio/stickers/life-style/water.png +0 -0
- package/apps/studio/stickers/life-style/yeah.png +0 -0
- package/apps/studio/stickers/life-style/you-can.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
- package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
- package/apps/studio/stickers/project-status/group-100.png +0 -0
- package/apps/studio/stickers/project-status/group-101.png +0 -0
- package/apps/studio/stickers/project-status/group-102.png +0 -0
- package/apps/studio/stickers/project-status/group-103.png +0 -0
- package/apps/studio/stickers/project-status/group-104.png +0 -0
- package/apps/studio/stickers/project-status/group-105.png +0 -0
- package/apps/studio/stickers/project-status/group-106.png +0 -0
- package/apps/studio/stickers/project-status/group-107.png +0 -0
- package/apps/studio/stickers/project-status/group-108.png +0 -0
- package/apps/studio/stickers/project-status/group-109.png +0 -0
- package/apps/studio/stickers/project-status/group-110.png +0 -0
- package/apps/studio/stickers/project-status/group-111.png +0 -0
- package/apps/studio/stickers/project-status/group-112.png +0 -0
- package/apps/studio/stickers/project-status/group-113.png +0 -0
- package/apps/studio/stickers/project-status/group-114.png +0 -0
- package/apps/studio/stickers/project-status/group-115.png +0 -0
- package/apps/studio/stickers/project-status/group-117.png +0 -0
- package/apps/studio/stickers/project-status/group-118.png +0 -0
- package/apps/studio/stickers/project-status/group-119.png +0 -0
- package/apps/studio/stickers/project-status/group-120.png +0 -0
- package/apps/studio/stickers/project-status/group-121.png +0 -0
- package/apps/studio/stickers/project-status/group-122.png +0 -0
- package/apps/studio/stickers/project-status/group-41.png +0 -0
- package/apps/studio/stickers/project-status/group-42.png +0 -0
- package/apps/studio/stickers/project-status/group-43.png +0 -0
- package/apps/studio/stickers/project-status/group-44.png +0 -0
- package/apps/studio/stickers/project-status/group-45.png +0 -0
- package/apps/studio/stickers/project-status/group-46.png +0 -0
- package/apps/studio/stickers/project-status/group-47.png +0 -0
- package/apps/studio/stickers/project-status/group-48.png +0 -0
- package/apps/studio/stickers/project-status/group-49.png +0 -0
- package/apps/studio/stickers/project-status/group-50.png +0 -0
- package/apps/studio/stickers/project-status/group-51.png +0 -0
- package/apps/studio/stickers/project-status/group-52.png +0 -0
- package/apps/studio/stickers/project-status/group-53.png +0 -0
- package/apps/studio/stickers/project-status/group-54.png +0 -0
- package/apps/studio/stickers/project-status/group-55.png +0 -0
- package/apps/studio/stickers/project-status/group-56.png +0 -0
- package/apps/studio/stickers/project-status/group-57.png +0 -0
- package/apps/studio/stickers/project-status/group-58.png +0 -0
- package/apps/studio/stickers/project-status/group-59.png +0 -0
- package/apps/studio/stickers/project-status/group-60.png +0 -0
- package/apps/studio/stickers/project-status/group-61.png +0 -0
- package/apps/studio/stickers/project-status/group-62.png +0 -0
- package/apps/studio/stickers/project-status/group-63.png +0 -0
- package/apps/studio/stickers/project-status/group-64.png +0 -0
- package/apps/studio/stickers/project-status/group-65.png +0 -0
- package/apps/studio/stickers/project-status/group-66.png +0 -0
- package/apps/studio/stickers/project-status/group-67.png +0 -0
- package/apps/studio/stickers/project-status/group-68.png +0 -0
- package/apps/studio/stickers/project-status/group-69.png +0 -0
- package/apps/studio/stickers/project-status/group-70.png +0 -0
- package/apps/studio/stickers/project-status/group-71.png +0 -0
- package/apps/studio/stickers/project-status/group-72.png +0 -0
- package/apps/studio/stickers/project-status/group-73.png +0 -0
- package/apps/studio/stickers/project-status/group-74.png +0 -0
- package/apps/studio/stickers/project-status/group-75.png +0 -0
- package/apps/studio/stickers/project-status/group-76.png +0 -0
- package/apps/studio/stickers/project-status/group-77.png +0 -0
- package/apps/studio/stickers/project-status/group-78.png +0 -0
- package/apps/studio/stickers/project-status/group-79.png +0 -0
- package/apps/studio/stickers/project-status/group-80.png +0 -0
- package/apps/studio/stickers/project-status/group-81.png +0 -0
- package/apps/studio/stickers/project-status/group-82.png +0 -0
- package/apps/studio/stickers/project-status/group-83.png +0 -0
- package/apps/studio/stickers/project-status/group-84.png +0 -0
- package/apps/studio/stickers/project-status/group-85.png +0 -0
- package/apps/studio/stickers/project-status/group-86.png +0 -0
- package/apps/studio/stickers/project-status/group-87.png +0 -0
- package/apps/studio/stickers/project-status/group-88.png +0 -0
- package/apps/studio/stickers/project-status/group-89.png +0 -0
- package/apps/studio/stickers/project-status/group-90.png +0 -0
- package/apps/studio/stickers/project-status/group-91.png +0 -0
- package/apps/studio/stickers/project-status/group-92.png +0 -0
- package/apps/studio/stickers/project-status/group-93.png +0 -0
- package/apps/studio/stickers/project-status/group-94.png +0 -0
- package/apps/studio/stickers/project-status/group-96.png +0 -0
- package/apps/studio/stickers/project-status/group-97.png +0 -0
- package/apps/studio/stickers/project-status/group-98.png +0 -0
- package/apps/studio/stickers/project-status/group-99.png +0 -0
- package/apps/studio/stickers/project-status/manifest.json +328 -0
- package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
- package/apps/studio/test/acp-commands.test.ts +2 -1
- package/apps/studio/test/asset-api.test.ts +3 -3
- package/apps/studio/test/canvas-edit.test.ts +17 -6
- package/apps/studio/test/canvas-origin-gate.test.ts +13 -0
- package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
- package/apps/studio/test/edit-source-command.test.ts +23 -0
- package/apps/studio/test/element-structural-edit.test.ts +46 -0
- package/apps/studio/test/footage-store.test.ts +100 -0
- package/apps/studio/test/git-branches.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +56 -1
- package/apps/studio/test/input-router.test.ts +33 -0
- package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
- package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
- package/apps/studio/test/photo-edit-api.test.ts +201 -0
- package/apps/studio/test/photo-filters.test.ts +149 -0
- package/apps/studio/test/photo-pipeline.test.ts +106 -0
- package/apps/studio/test/photo-taxonomy.test.ts +96 -0
- package/apps/studio/test/read-annotations.test.ts +164 -0
- package/apps/studio/test/shell-importmap.test.ts +49 -0
- package/apps/studio/test/text-editability-stamp.test.ts +131 -0
- package/apps/studio/test/timeline-parse.test.ts +57 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
- package/apps/studio/test/undo-stack.test.ts +33 -0
- package/apps/studio/test/video-asset.test.ts +5 -5
- package/apps/studio/test/video-render-bridge.test.ts +23 -1
- package/apps/studio/text-caret.ts +239 -0
- package/apps/studio/tool-palette.tsx +43 -20
- package/apps/studio/undo-stack.ts +94 -2
- package/apps/studio/use-annotation-resize.tsx +4 -4
- package/apps/studio/use-canvas-media-drop.tsx +38 -1
- package/apps/studio/use-selection-set.tsx +21 -0
- package/apps/studio/video-comp.tsx +48 -7
- package/apps/studio/whats-new.json +53 -0
- package/cli/commands/design.mjs +34 -3
- package/cli/lib/gitignore-block.mjs +1 -0
- package/package.json +10 -10
- package/plugins/design/templates/_shell.html +1 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file footage-store.ts — Footage-analysis + EDL sidecar persistence
|
|
3
|
+
* (feature-footage-analysis-director, Task 2).
|
|
4
|
+
* @scope apps/studio/footage-store.ts
|
|
5
|
+
* @purpose Read / write the two VERSIONED sidecars behind the footage
|
|
6
|
+
* "director" pipeline (DDR-115 taxonomy):
|
|
7
|
+
* • `assets/<sha8>.footage.json` — a `FootageAnalysis` (per clip)
|
|
8
|
+
* • `<designRoot>/<slug>.edl.json` — an `Edl` (per cut)
|
|
9
|
+
* Mirrors `photo-store.ts`'s `createX(ctx)` factory + `Bun.write`/
|
|
10
|
+
* `Bun.file` shape. Server-side only — imports the DEPENDENCY-FREE
|
|
11
|
+
* `footage/schema.ts`.
|
|
12
|
+
*
|
|
13
|
+
* @security UNLIKE `/_api/photo-edit`, the `/_api/footage` route is
|
|
14
|
+
* MAIN-ORIGIN ONLY (privileged — NOT in either canvas allowlist).
|
|
15
|
+
* It is written by the `footage-analyst` / `footage-director` agents
|
|
16
|
+
* over loopback, never by the untrusted canvas iframe (DDR-054), so
|
|
17
|
+
* a `GET → 405` assertion from the canvas origin guards it
|
|
18
|
+
* (canvas-origin-gate.test.ts). The write path is still derived from
|
|
19
|
+
* caller-supplied params, so the DDR-088 cap stack is preserved:
|
|
20
|
+
* 1. strict sha8 / slug extraction (hex or kebab, bounded) —
|
|
21
|
+
* rejects `..`, `/`, `%2f`, absolute paths by construction;
|
|
22
|
+
* 2. explicit containment assert (poisoned-designRoot backstop);
|
|
23
|
+
* 3. structural `validateFootageAnalysis` / `validateEdl`;
|
|
24
|
+
* 4. body/file size cap (256 KB — generous for analysis JSON).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
|
|
29
|
+
import type { Context } from './context.ts';
|
|
30
|
+
import {
|
|
31
|
+
EDL_VERSION,
|
|
32
|
+
type Edl,
|
|
33
|
+
FOOTAGE_ANALYSIS_VERSION,
|
|
34
|
+
type FootageAnalysis,
|
|
35
|
+
validateEdl,
|
|
36
|
+
validateFootageAnalysis,
|
|
37
|
+
} from './footage/schema.ts';
|
|
38
|
+
|
|
39
|
+
/** Max bytes for either sidecar (read + write). Analysis JSON is small; slack. */
|
|
40
|
+
export const FOOTAGE_MAX_BYTES = 256 * 1024;
|
|
41
|
+
|
|
42
|
+
const SHA_RE = /^[0-9a-f]{8,64}$/;
|
|
43
|
+
// A cut slug: lowercase kebab, bounded. Matches the `slug.sh` normalization
|
|
44
|
+
// shape — no path segment, no dot, so `..`/`/`/absolute can never survive.
|
|
45
|
+
const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,80}$/;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Extract + validate the content-address sha8 from an `asset` param, in any of
|
|
49
|
+
* the forms a caller might hold (`assets/<sha8>.<ext>`, `/assets/<sha8>.png`,
|
|
50
|
+
* `<sha8>.png`, bare `<sha8>`). The hex regex is the traversal defense.
|
|
51
|
+
* (Copied from photo-store.assetSha8 — same contract, kept local so the two
|
|
52
|
+
* stores stay independent.)
|
|
53
|
+
*/
|
|
54
|
+
export function assetSha8(param: string | null | undefined): string | null {
|
|
55
|
+
if (typeof param !== 'string') return null;
|
|
56
|
+
let s = param.trim();
|
|
57
|
+
if (!s) return null;
|
|
58
|
+
s = s.replace(/^\/+/, '');
|
|
59
|
+
s = s.replace(/^assets\//, '');
|
|
60
|
+
s = s.replace(/\.[A-Za-z0-9]+$/, '');
|
|
61
|
+
return SHA_RE.test(s) ? s : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Validate a cut slug (already-normalized kebab), or null. */
|
|
65
|
+
export function edlSlug(param: string | null | undefined): string | null {
|
|
66
|
+
if (typeof param !== 'string') return null;
|
|
67
|
+
const s = param.trim();
|
|
68
|
+
return SLUG_RE.test(s) ? s : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface FootageStore {
|
|
72
|
+
analysisPathForSha8(sha8: string): string;
|
|
73
|
+
edlPathForSlug(slug: string): string;
|
|
74
|
+
getAnalysis(assetParam: string | null | undefined): Promise<FootageAnalysis | null>;
|
|
75
|
+
saveAnalysis(
|
|
76
|
+
assetParam: string | null | undefined,
|
|
77
|
+
analysis: unknown
|
|
78
|
+
): Promise<
|
|
79
|
+
| { ok: true; path: string; analysis: FootageAnalysis }
|
|
80
|
+
| { ok: false; status: number; error: string }
|
|
81
|
+
>;
|
|
82
|
+
getEdl(slugParam: string | null | undefined): Promise<Edl | null>;
|
|
83
|
+
saveEdl(
|
|
84
|
+
slugParam: string | null | undefined,
|
|
85
|
+
edl: unknown
|
|
86
|
+
): Promise<{ ok: true; path: string; edl: Edl } | { ok: false; status: number; error: string }>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createFootageStore(ctx: Context): FootageStore {
|
|
90
|
+
const designRoot = ctx.paths.designRoot;
|
|
91
|
+
const assetsDir = path.join(designRoot, 'assets');
|
|
92
|
+
|
|
93
|
+
function analysisPathForSha8(sha8: string): string {
|
|
94
|
+
if (!SHA_RE.test(sha8)) throw new Error(`invalid sha8: ${sha8}`);
|
|
95
|
+
const abs = path.join(assetsDir, `${sha8}.footage.json`);
|
|
96
|
+
const rel = path.relative(assetsDir, abs);
|
|
97
|
+
if (rel !== `${sha8}.footage.json` || rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
98
|
+
throw new Error(`footage path escaped assets/: ${abs}`);
|
|
99
|
+
}
|
|
100
|
+
return abs;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function edlPathForSlug(slug: string): string {
|
|
104
|
+
if (!SLUG_RE.test(slug)) throw new Error(`invalid slug: ${slug}`);
|
|
105
|
+
const abs = path.join(designRoot, `${slug}.edl.json`);
|
|
106
|
+
const rel = path.relative(designRoot, abs);
|
|
107
|
+
if (rel !== `${slug}.edl.json` || rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
108
|
+
throw new Error(`edl path escaped designRoot: ${abs}`);
|
|
109
|
+
}
|
|
110
|
+
return abs;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function readSidecar<T>(
|
|
114
|
+
abs: string,
|
|
115
|
+
validate: (v: unknown) => { ok: boolean }
|
|
116
|
+
): Promise<T | null> {
|
|
117
|
+
try {
|
|
118
|
+
const file = Bun.file(abs);
|
|
119
|
+
if (!(await file.exists())) return null;
|
|
120
|
+
if (file.size > FOOTAGE_MAX_BYTES) return null;
|
|
121
|
+
const parsed = JSON.parse(await file.text());
|
|
122
|
+
return validate(parsed).ok ? (parsed as T) : null;
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function getAnalysis(
|
|
129
|
+
assetParam: string | null | undefined
|
|
130
|
+
): Promise<FootageAnalysis | null> {
|
|
131
|
+
const sha8 = assetSha8(assetParam);
|
|
132
|
+
if (!sha8) return null;
|
|
133
|
+
return readSidecar<FootageAnalysis>(analysisPathForSha8(sha8), validateFootageAnalysis);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function saveAnalysis(
|
|
137
|
+
assetParam: string | null | undefined,
|
|
138
|
+
analysis: unknown
|
|
139
|
+
): Promise<
|
|
140
|
+
| { ok: true; path: string; analysis: FootageAnalysis }
|
|
141
|
+
| { ok: false; status: number; error: string }
|
|
142
|
+
> {
|
|
143
|
+
const sha8 = assetSha8(assetParam);
|
|
144
|
+
if (!sha8) return { ok: false, status: 400, error: 'invalid or missing asset (expected sha8)' };
|
|
145
|
+
|
|
146
|
+
let serialized: string;
|
|
147
|
+
try {
|
|
148
|
+
serialized = JSON.stringify(analysis ?? {});
|
|
149
|
+
} catch {
|
|
150
|
+
return { ok: false, status: 400, error: 'body is not serializable JSON' };
|
|
151
|
+
}
|
|
152
|
+
if (Buffer.byteLength(serialized, 'utf8') > FOOTAGE_MAX_BYTES) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
status: 413,
|
|
156
|
+
error: `footage analysis exceeds ${FOOTAGE_MAX_BYTES} bytes`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const check = validateFootageAnalysis(analysis);
|
|
161
|
+
if (!check.ok) {
|
|
162
|
+
return {
|
|
163
|
+
ok: false,
|
|
164
|
+
status: 400,
|
|
165
|
+
error: `invalid FootageAnalysis: ${check.errors.join('; ')}`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Stamp version only; the sidecar filename (<sha8>.footage.json) is the
|
|
170
|
+
// authoritative source key. `asset` is already validated as a well-formed
|
|
171
|
+
// relative path — keep the client's value verbatim.
|
|
172
|
+
const toWrite: FootageAnalysis = {
|
|
173
|
+
...(analysis as FootageAnalysis),
|
|
174
|
+
version: FOOTAGE_ANALYSIS_VERSION,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
let abs: string;
|
|
178
|
+
try {
|
|
179
|
+
abs = analysisPathForSha8(sha8);
|
|
180
|
+
} catch (e) {
|
|
181
|
+
return { ok: false, status: 400, error: e instanceof Error ? e.message : 'bad path' };
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
await Bun.write(abs, JSON.stringify(toWrite, null, 2));
|
|
185
|
+
} catch (e) {
|
|
186
|
+
return { ok: false, status: 500, error: e instanceof Error ? e.message : 'write failed' };
|
|
187
|
+
}
|
|
188
|
+
return { ok: true, path: path.posix.join('assets', `${sha8}.footage.json`), analysis: toWrite };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function getEdl(slugParam: string | null | undefined): Promise<Edl | null> {
|
|
192
|
+
const slug = edlSlug(slugParam);
|
|
193
|
+
if (!slug) return null;
|
|
194
|
+
return readSidecar<Edl>(edlPathForSlug(slug), validateEdl);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function saveEdl(
|
|
198
|
+
slugParam: string | null | undefined,
|
|
199
|
+
edl: unknown
|
|
200
|
+
): Promise<{ ok: true; path: string; edl: Edl } | { ok: false; status: number; error: string }> {
|
|
201
|
+
const slug = edlSlug(slugParam);
|
|
202
|
+
if (!slug) return { ok: false, status: 400, error: 'invalid or missing slug (expected kebab)' };
|
|
203
|
+
|
|
204
|
+
let serialized: string;
|
|
205
|
+
try {
|
|
206
|
+
serialized = JSON.stringify(edl ?? {});
|
|
207
|
+
} catch {
|
|
208
|
+
return { ok: false, status: 400, error: 'body is not serializable JSON' };
|
|
209
|
+
}
|
|
210
|
+
if (Buffer.byteLength(serialized, 'utf8') > FOOTAGE_MAX_BYTES) {
|
|
211
|
+
return { ok: false, status: 413, error: `edl exceeds ${FOOTAGE_MAX_BYTES} bytes` };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const check = validateEdl(edl);
|
|
215
|
+
if (!check.ok) {
|
|
216
|
+
return { ok: false, status: 400, error: `invalid Edl: ${check.errors.join('; ')}` };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const toWrite: Edl = { ...(edl as Edl), version: EDL_VERSION };
|
|
220
|
+
let abs: string;
|
|
221
|
+
try {
|
|
222
|
+
abs = edlPathForSlug(slug);
|
|
223
|
+
} catch (e) {
|
|
224
|
+
return { ok: false, status: 400, error: e instanceof Error ? e.message : 'bad path' };
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
await Bun.write(abs, JSON.stringify(toWrite, null, 2));
|
|
228
|
+
} catch (e) {
|
|
229
|
+
return { ok: false, status: 500, error: e instanceof Error ? e.message : 'write failed' };
|
|
230
|
+
}
|
|
231
|
+
return { ok: true, path: `${slug}.edl.json`, edl: toWrite };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { analysisPathForSha8, edlPathForSlug, getAnalysis, saveAnalysis, getEdl, saveEdl };
|
|
235
|
+
}
|
package/apps/studio/fs-watch.ts
CHANGED
|
@@ -34,6 +34,16 @@ export function createFsWatch(ctx: Context): FsWatch {
|
|
|
34
34
|
|
|
35
35
|
function start() {
|
|
36
36
|
if (watcher) return;
|
|
37
|
+
// Capture/CI opt-out (MAUDE_NO_WATCH=1): a headless render server has no
|
|
38
|
+
// interactive editor to hot-reload, so it never needs the watcher. It's a
|
|
39
|
+
// DEFENSIVE measure for long video exports — an HMR hard-reload broadcast
|
|
40
|
+
// fired mid-capture (from a concurrent write to designRoot, e.g. a peer edit
|
|
41
|
+
// or a runtime-state save NOT in the skip-list below) destroys the Playwright
|
|
42
|
+
// page's execution context. (NB: in the 2026-07-10 cinematic-cut dogfood the
|
|
43
|
+
// ACTUAL cause of the "Execution context was destroyed" crashes turned out to
|
|
44
|
+
// be renderer compositing pressure — too many full-frame mix-blend-mode /
|
|
45
|
+
// filter layers per frame — not the watcher; this guard is belt-and-braces.)
|
|
46
|
+
if (process.env.MAUDE_NO_WATCH === '1') return;
|
|
37
47
|
try {
|
|
38
48
|
watcher = watch(ctx.paths.designRoot, { recursive: true }, (_event, filename) => {
|
|
39
49
|
if (!filename) return;
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
// echoed, or persisted (it is stripped from the JSON we return on error).
|
|
15
15
|
|
|
16
16
|
import type { Context } from '../context.ts';
|
|
17
|
+
import { parseGitHubRemote } from '../github/endpoints.ts';
|
|
18
|
+
import { createPullRequest, GitHubApiError } from '../github/service.ts';
|
|
17
19
|
import { getGithubToken } from '../github/token.ts';
|
|
18
20
|
import {
|
|
19
21
|
type GitFileStatus,
|
|
@@ -284,19 +286,64 @@ export function createGitEndpoints(ctx: Context): GitEndpoints {
|
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
async function fold(body: unknown): Promise<GitEndpointResult> {
|
|
287
|
-
// Token resolution mirrors push
|
|
288
|
-
//
|
|
289
|
+
// Token resolution mirrors push: body token → keychain bridge → undefined. Needed
|
|
290
|
+
// for the GitHub PR API call; an ssh draft-push authenticates with the user's key.
|
|
289
291
|
const token = readToken(body) ?? (await getGithubToken()) ?? undefined;
|
|
290
292
|
const b = (body ?? {}) as { name?: unknown; remote?: unknown };
|
|
291
293
|
const name = safeGitArg(b.name);
|
|
292
294
|
if (name === undefined) return bad('Invalid draft name.');
|
|
293
295
|
if (b.remote != null && safeRemoteArg(b.remote) === undefined) return bad('Invalid remote.');
|
|
294
296
|
const res = await gitFoldDraft(dir, name, token, { remote: safeRemoteArg(b.remote) });
|
|
295
|
-
if (res.ok)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
297
|
+
if (!res.ok) {
|
|
298
|
+
if (res.authRequired)
|
|
299
|
+
return { status: 401, json: { ok: false, authRequired: true, error: res.error } };
|
|
300
|
+
if (res.conflict)
|
|
301
|
+
return { status: 409, json: { ok: false, conflict: true, error: res.error } };
|
|
302
|
+
return { status: 502, json: { ok: false, error: res.error ?? 'Could not add the draft.' } };
|
|
303
|
+
}
|
|
304
|
+
// Local-merge path (no GitHub remote): the draft is already in the Shared version.
|
|
305
|
+
if (!res.prReady) return { status: 200, json: { ok: true, shared: res.shared } };
|
|
306
|
+
// PR path: the draft branch is pushed — open a pull request draft→shared. The merge
|
|
307
|
+
// itself happens on GitHub after review (this is how a protected `main` is added to).
|
|
308
|
+
const gh = res.remoteUrl ? parseGitHubRemote(res.remoteUrl) : null;
|
|
309
|
+
if (!gh)
|
|
310
|
+
return draftPublishedNoPr(
|
|
311
|
+
res.shared,
|
|
312
|
+
res.head,
|
|
313
|
+
'Draft published. Open a pull request on your Git host to add it to the Shared version.'
|
|
314
|
+
);
|
|
315
|
+
if (!token)
|
|
316
|
+
return draftPublishedNoPr(
|
|
317
|
+
res.shared,
|
|
318
|
+
res.head,
|
|
319
|
+
'Draft published. Sign in with GitHub in Maude to open the pull request.'
|
|
320
|
+
);
|
|
321
|
+
try {
|
|
322
|
+
const pr = await createPullRequest(token, gh.owner, gh.repo, {
|
|
323
|
+
head: res.head as string,
|
|
324
|
+
base: res.base as string,
|
|
325
|
+
title: `Add draft “${res.head}” to ${res.base}`,
|
|
326
|
+
body: 'Opened from Maude — “Add to Shared version”.',
|
|
327
|
+
});
|
|
328
|
+
return {
|
|
329
|
+
status: 200,
|
|
330
|
+
// Surface the resolved destination repo (F2): the user should SEE which
|
|
331
|
+
// owner/repo their draft was pushed to / PR'd against, not just "the Shared
|
|
332
|
+
// version" — a poisoned `origin` would otherwise silently target another repo.
|
|
333
|
+
json: {
|
|
334
|
+
ok: true,
|
|
335
|
+
shared: res.shared,
|
|
336
|
+
prUrl: pr.html_url,
|
|
337
|
+
prNumber: pr.number,
|
|
338
|
+
repo: `${gh.owner}/${gh.repo}`,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
} catch (e) {
|
|
342
|
+
// The draft IS pushed — a PR-creation failure is a partial success, not a hard
|
|
343
|
+
// fail. Surface the reason and keep the draft so the user can retry / open it.
|
|
344
|
+
const msg = e instanceof GitHubApiError ? e.message : 'Could not open the pull request.';
|
|
345
|
+
return draftPublishedNoPr(res.shared, res.head, msg);
|
|
346
|
+
}
|
|
300
347
|
}
|
|
301
348
|
|
|
302
349
|
async function fetchRemote(body: unknown): Promise<GitEndpointResult> {
|
|
@@ -335,6 +382,20 @@ function readToken(body: unknown): string | null {
|
|
|
335
382
|
return typeof t === 'string' && t.length > 0 ? t : null;
|
|
336
383
|
}
|
|
337
384
|
|
|
385
|
+
/** Fold partial-success: the draft branch was pushed but no PR could be opened (no
|
|
386
|
+
* sign-in, a non-GitHub remote, or the PR call failed). `ok:true` because the work IS
|
|
387
|
+
* published; the UI shows `error` as a heads-up and keeps the draft for a retry. */
|
|
388
|
+
function draftPublishedNoPr(
|
|
389
|
+
shared: string | undefined,
|
|
390
|
+
head: string | undefined,
|
|
391
|
+
error: string
|
|
392
|
+
): GitEndpointResult {
|
|
393
|
+
return {
|
|
394
|
+
status: 200,
|
|
395
|
+
json: { ok: true, shared, published: head, prUnavailable: true, error },
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
338
399
|
/** DDR-112 — expand each selected path to include any DIRTY same-directory,
|
|
339
400
|
* same-stem sidecar (`<stem>.meta.json`, `<stem>.annotations.svg`, …). The `.`
|
|
340
401
|
* delimiter prevents `ui/Pricing` from grabbing `ui/Pricing v3.*`. */
|