@vgai/sdk 0.4.0-canary.20260715.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/package.json +27 -0
- package/src/cinematic/capabilities-operations.ts +128 -0
- package/src/cinematic/cue-operations.ts +198 -0
- package/src/cinematic/gsap-operations.ts +126 -0
- package/src/cinematic/index.ts +59 -0
- package/src/cinematic/preview-operations.ts +279 -0
- package/src/cinematic/preview-transport.ts +244 -0
- package/src/cinematic/render-operations.ts +409 -0
- package/src/cinematic/render-transport.ts +238 -0
- package/src/cinematic/theatre-operations.ts +306 -0
- package/src/editor/camera-operations.ts +169 -0
- package/src/editor/console-operations.ts +87 -0
- package/src/editor/hierarchy-operations.ts +95 -0
- package/src/editor/index.ts +62 -0
- package/src/editor/open-operations.ts +209 -0
- package/src/editor/screenshot-operations.ts +99 -0
- package/src/editor/selection-operations.ts +144 -0
- package/src/editor/session-operations.ts +73 -0
- package/src/editor/source-location-operations.ts +106 -0
- package/src/editor/transport.ts +647 -0
- package/src/errors.ts +72 -0
- package/src/http/http-projection.ts +349 -0
- package/src/http/index.ts +11 -0
- package/src/index.ts +67 -0
- package/src/mcp/index.ts +16 -0
- package/src/mcp/mcp-projection.ts +288 -0
- package/src/operations.ts +83 -0
- package/src/play/control-operations.ts +205 -0
- package/src/play/debug-command-operations.ts +245 -0
- package/src/play/index.ts +66 -0
- package/src/play/input-operations.ts +316 -0
- package/src/play/lifecycle-operations.ts +271 -0
- package/src/play/log-operations.ts +279 -0
- package/src/play/run-ticks-operations.ts +141 -0
- package/src/play/state-operations.ts +210 -0
- package/src/play/status-operations.ts +160 -0
- package/src/play/transport.ts +728 -0
- package/src/project/asset-operations.ts +243 -0
- package/src/project/component-operations.ts +337 -0
- package/src/project/discovery-operations.ts +269 -0
- package/src/project/entity-operations.ts +366 -0
- package/src/project/index.ts +55 -0
- package/src/project/input-map-operations.ts +233 -0
- package/src/project/manifest-operations.ts +355 -0
- package/src/project/scene-operations.ts +426 -0
- package/src/project/shared.ts +299 -0
- package/src/registry.ts +285 -0
- package/src/render/capabilities/ffmpeg.ts +141 -0
- package/src/render/index.ts +15 -0
- package/src/render/render-cinematic.ts +1847 -0
- package/src/types.ts +101 -0
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vgai/sdk",
|
|
3
|
+
"author": "Volter AI, Inc.",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"version": "0.4.0-canary.20260715.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/volter-ai/vgai-engine.git",
|
|
10
|
+
"directory": "packages/vgai-sdk"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./src/index.ts",
|
|
20
|
+
"./registry": "./src/registry.ts"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@vgai/engine": "0.4.0-canary.20260715.0",
|
|
24
|
+
"playwright": "^1.58.2",
|
|
25
|
+
"zod": "^4.3.6"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cinematic.capabilities` (B5, §8 B5 "report capture/codec capabilities").
|
|
3
|
+
*
|
|
4
|
+
* Wraps `checkFfmpegCapability` (`packages/vgai-sdk/src/render/capabilities/ffmpeg.ts`, A1) for
|
|
5
|
+
* the ffmpeg/ffprobe BINARY presence/version half, and adds the DECLARED
|
|
6
|
+
* format/codec support `packages/vgai-sdk/src/render/render-cinematic.ts` itself
|
|
7
|
+
* hard-codes (`buildEncodeArgs`/`requireValidBackgroundFormat`) — mp4/h264,
|
|
8
|
+
* webm/vp9(+alpha), and png (no video codec, no container). This is a
|
|
9
|
+
* DECLARATION of what the render pipeline is WRITTEN to attempt, not an
|
|
10
|
+
* independent `ffmpeg -codecs`/`-encoders` probe: `checkFfmpegCapability`
|
|
11
|
+
* itself only runs `<bin> -version` (binary presence), never inspects which
|
|
12
|
+
* codecs a given ffmpeg build was compiled with — so an ffmpeg binary that
|
|
13
|
+
* IS present but lacks `libx264`/`libvpx-vp9` still reports success here,
|
|
14
|
+
* and would only fail later, at actual encode time, with ffmpeg's own raw
|
|
15
|
+
* stderr (surfaced by `cinematic.render.status`'s error field). This mirrors
|
|
16
|
+
* `checkFfmpegCapability`'s own scope honestly rather than fabricating a
|
|
17
|
+
* codec-level guarantee it was never designed to make.
|
|
18
|
+
*
|
|
19
|
+
* `FfmpegCapabilityResult`'s failure shape
|
|
20
|
+
* (`{ ok: false, errors: FfmpegCapabilityError[] }`) does NOT retain
|
|
21
|
+
* command/version for whichever of ffmpeg/ffprobe DID succeed when the
|
|
22
|
+
* other is missing (`checkFfmpegCapability`'s own success values are
|
|
23
|
+
* discarded once any error exists — see its source) — so in that mixed
|
|
24
|
+
* case this op honestly reports the present-but-unprobed-for-version binary
|
|
25
|
+
* as `{ ok: true }` with no `command`/`version`, rather than fabricating
|
|
26
|
+
* either.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { z } from 'zod';
|
|
30
|
+
import { defineOperation, type OperationRegistry } from '../registry.js';
|
|
31
|
+
import { checkFfmpegCapability } from '../render/capabilities/ffmpeg.js';
|
|
32
|
+
|
|
33
|
+
const BinaryCapability = z.discriminatedUnion('ok', [
|
|
34
|
+
z.object({
|
|
35
|
+
ok: z.literal(true),
|
|
36
|
+
command: z
|
|
37
|
+
.string()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe('Absent when the sibling binary was missing (see module jsdoc).'),
|
|
40
|
+
version: z
|
|
41
|
+
.string()
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('Absent when the sibling binary was missing (see module jsdoc).'),
|
|
44
|
+
}),
|
|
45
|
+
z.object({ ok: z.literal(false), installGuidance: z.string() }),
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
const CinematicCapabilitiesInput = z.object({
|
|
49
|
+
ffmpegBin: z.string().optional().describe('Override the ffmpeg binary name/path to probe.'),
|
|
50
|
+
ffprobeBin: z.string().optional().describe('Override the ffprobe binary name/path to probe.'),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const CinematicCapabilitiesResult = z.object({
|
|
54
|
+
ffmpeg: BinaryCapability,
|
|
55
|
+
ffprobe: BinaryCapability,
|
|
56
|
+
supportedFormats: z
|
|
57
|
+
.array(
|
|
58
|
+
z.object({
|
|
59
|
+
format: z.enum(['mp4', 'webm', 'png']),
|
|
60
|
+
videoCodec: z.string().optional().describe('Absent for png (no video encode step at all).'),
|
|
61
|
+
alpha: z
|
|
62
|
+
.boolean()
|
|
63
|
+
.describe('Whether this format/codec combination can carry an alpha channel.'),
|
|
64
|
+
}),
|
|
65
|
+
)
|
|
66
|
+
.describe(
|
|
67
|
+
'Formats packages/vgai-sdk/src/render/render-cinematic.ts is WRITTEN to attempt — a declared ' +
|
|
68
|
+
'capability list, not an independent ffmpeg -codecs probe (see module jsdoc).',
|
|
69
|
+
),
|
|
70
|
+
warnings: z.array(z.string()),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const DECLARED_FORMATS = [
|
|
74
|
+
{ format: 'mp4' as const, videoCodec: 'libx264', alpha: false },
|
|
75
|
+
{ format: 'webm' as const, videoCodec: 'libvpx-vp9', alpha: true },
|
|
76
|
+
{ format: 'png' as const, alpha: true },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
export const cinematicCapabilities = defineOperation({
|
|
80
|
+
name: 'cinematic.capabilities',
|
|
81
|
+
summary:
|
|
82
|
+
'Report ffmpeg/ffprobe capture capability and the render pipeline’s declared codec/format support.',
|
|
83
|
+
description:
|
|
84
|
+
'Wraps checkFfmpegCapability (A1) for binary presence/version, plus a DECLARED (not probed) ' +
|
|
85
|
+
'format/codec list matching render-cinematic.ts’s own hard-coded encode args (see module jsdoc ' +
|
|
86
|
+
'for the honest scope limit).',
|
|
87
|
+
input: CinematicCapabilitiesInput,
|
|
88
|
+
result: CinematicCapabilitiesResult,
|
|
89
|
+
errors: [],
|
|
90
|
+
requires: {},
|
|
91
|
+
host: 'node',
|
|
92
|
+
mutates: false,
|
|
93
|
+
supportsDryRun: false,
|
|
94
|
+
permission: { risk: 'read', summary: 'Runs `ffmpeg -version`/`ffprobe -version`; no writes.' },
|
|
95
|
+
async impl(input) {
|
|
96
|
+
const check = checkFfmpegCapability({
|
|
97
|
+
...(input.ffmpegBin !== undefined ? { ffmpegBin: input.ffmpegBin } : {}),
|
|
98
|
+
...(input.ffprobeBin !== undefined ? { ffprobeBin: input.ffprobeBin } : {}),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (check.ok) {
|
|
102
|
+
return {
|
|
103
|
+
ffmpeg: { ok: true as const, ...check.ffmpeg },
|
|
104
|
+
ffprobe: { ok: true as const, ...check.ffprobe },
|
|
105
|
+
supportedFormats: DECLARED_FORMATS,
|
|
106
|
+
warnings: [],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const ffmpegErr = check.errors.find((e) => e.binary === 'ffmpeg');
|
|
111
|
+
const ffprobeErr = check.errors.find((e) => e.binary === 'ffprobe');
|
|
112
|
+
return {
|
|
113
|
+
ffmpeg: ffmpegErr
|
|
114
|
+
? { ok: false as const, installGuidance: ffmpegErr.installGuidance }
|
|
115
|
+
: { ok: true as const },
|
|
116
|
+
ffprobe: ffprobeErr
|
|
117
|
+
? { ok: false as const, installGuidance: ffprobeErr.installGuidance }
|
|
118
|
+
: { ok: true as const },
|
|
119
|
+
supportedFormats: DECLARED_FORMATS,
|
|
120
|
+
warnings: check.errors.map((e) => e.message),
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
/** Register cinematic.capabilities onto `registry`. */
|
|
126
|
+
export function registerCapabilitiesOperations(registry: OperationRegistry): void {
|
|
127
|
+
registry.register(cinematicCapabilities);
|
|
128
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cinematic.cue.list` / `cinematic.cue.validate`
|
|
3
|
+
* (B5, §8 B5 "list cues and validate cue ordering/IDs").
|
|
4
|
+
*
|
|
5
|
+
* `CinematicCue` (`packages/engine/src/animation/cinematic-cues.ts`) is
|
|
6
|
+
* ordinary TypeScript — a plain array literal with a `run`/`reverse`
|
|
7
|
+
* function per entry (§3.2/§5.4: "Cues must not contain an alternate
|
|
8
|
+
* property-animation model" — deliberately NOT a VGAI-owned serialized
|
|
9
|
+
* format). There is therefore no JSON/manifest this op can parse
|
|
10
|
+
* authoritatively; like B2's `project.xstate.discover`/`.validate`
|
|
11
|
+
* (`../project/discovery-operations.ts`), this is a STATIC HEURISTIC over
|
|
12
|
+
* source text, not a parse/execute — it can both miss cues (defined via
|
|
13
|
+
* spread, computed properties, or indirection) and never proves `run`/
|
|
14
|
+
* `reverse` themselves are correct. Named as a limit, not silently passed
|
|
15
|
+
* off as full validation, exactly like that module's own documented limit.
|
|
16
|
+
*
|
|
17
|
+
* The heuristic looks for the literal shape every committed cue array in
|
|
18
|
+
* this repo actually uses (`packages/engine/e2e/reference-cinematic/main.ts`):
|
|
19
|
+
* `{ id: '...', time: <number>, ... }` — `id` immediately followed by
|
|
20
|
+
* `time` (whitespace/newlines between fields, comma-separated, matching
|
|
21
|
+
* `CinematicCue`'s own field order convention).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
25
|
+
import { isAbsolute, join } from 'node:path';
|
|
26
|
+
import { z } from 'zod';
|
|
27
|
+
import { OperationError } from '../errors.js';
|
|
28
|
+
import { defineOperation, type ErrorDefinition, type OperationRegistry } from '../registry.js';
|
|
29
|
+
import type { OperationContext } from '../types.js';
|
|
30
|
+
import { NO_ROOT_ERROR } from './theatre-operations.js';
|
|
31
|
+
|
|
32
|
+
export const CUE_FILE_NOT_FOUND_ERROR: ErrorDefinition = {
|
|
33
|
+
code: 'FILE_NOT_FOUND',
|
|
34
|
+
summary: 'The referenced cue module file does not exist on disk.',
|
|
35
|
+
data: z.object({ path: z.string() }),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const CUE_LITERAL_PATTERN =
|
|
39
|
+
/\{\s*id:\s*['"]([^'"]+)['"]\s*,\s*time:\s*(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g;
|
|
40
|
+
|
|
41
|
+
export interface HeuristicCue {
|
|
42
|
+
readonly id: string;
|
|
43
|
+
readonly time: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extract `{ id: '...', time: <n> }`-shaped object literals from `source`,
|
|
48
|
+
* in the order they appear in the text (== the array's declaration/
|
|
49
|
+
* registration order, per `createCueRunner`'s equal-timestamp tiebreak
|
|
50
|
+
* convention). Best-effort text heuristic — see module jsdoc.
|
|
51
|
+
*/
|
|
52
|
+
export function extractHeuristicCues(source: string): HeuristicCue[] {
|
|
53
|
+
const cues: HeuristicCue[] = [];
|
|
54
|
+
for (const match of source.matchAll(CUE_LITERAL_PATTERN)) {
|
|
55
|
+
const id = match[1];
|
|
56
|
+
const time = Number(match[2]);
|
|
57
|
+
if (id !== undefined && Number.isFinite(time)) cues.push({ id, time });
|
|
58
|
+
}
|
|
59
|
+
return cues;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const CueSourceInput = z.object({
|
|
63
|
+
source: z.string().optional().describe('Raw TypeScript source to scan directly.'),
|
|
64
|
+
path: z
|
|
65
|
+
.string()
|
|
66
|
+
.optional()
|
|
67
|
+
.describe('Path to a TypeScript module to read and scan (absolute, or root-relative).'),
|
|
68
|
+
root: z
|
|
69
|
+
.string()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe('Base directory for a relative `path`. Defaults to ctx.projectRoot.'),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
function resolveSource(input: z.infer<typeof CueSourceInput>, ctx: OperationContext): string {
|
|
75
|
+
if (input.source !== undefined) return input.source;
|
|
76
|
+
if (input.path === undefined) {
|
|
77
|
+
throw new OperationError('BAD_INPUT', 'One of `source` or `path` must be given.', {});
|
|
78
|
+
}
|
|
79
|
+
const root = input.root ?? ctx.projectRoot;
|
|
80
|
+
const absPath = isAbsolute(input.path) ? input.path : root ? join(root, input.path) : undefined;
|
|
81
|
+
if (!absPath) {
|
|
82
|
+
throw new OperationError(
|
|
83
|
+
'NO_ROOT',
|
|
84
|
+
'A relative `path` was given but neither `root` nor ctx.projectRoot is available.',
|
|
85
|
+
{},
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (!existsSync(absPath)) {
|
|
89
|
+
throw new OperationError('FILE_NOT_FOUND', `File not found: ${absPath}`, { path: absPath });
|
|
90
|
+
}
|
|
91
|
+
return readFileSync(absPath, 'utf-8');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const HeuristicCueSchema = z.object({ id: z.string(), time: z.number() });
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// cinematic.cue.list
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
const CinematicCueListResult = z.object({
|
|
101
|
+
cues: z
|
|
102
|
+
.array(HeuristicCueSchema)
|
|
103
|
+
.describe('Every {id, time} pair found, in source declaration order.'),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
export const cinematicCueList = defineOperation({
|
|
107
|
+
name: 'cinematic.cue.list',
|
|
108
|
+
summary: 'Best-effort static list of {id, time} cue entries in a TypeScript cue module.',
|
|
109
|
+
description:
|
|
110
|
+
'STATIC HEURISTIC over source text, not a parse/execute (see module jsdoc) — CinematicCue is ' +
|
|
111
|
+
'ordinary TypeScript (§3.2/§5.4), not a VGAI-owned serialized format.',
|
|
112
|
+
input: CueSourceInput,
|
|
113
|
+
result: CinematicCueListResult,
|
|
114
|
+
errors: [
|
|
115
|
+
NO_ROOT_ERROR,
|
|
116
|
+
CUE_FILE_NOT_FOUND_ERROR,
|
|
117
|
+
{ code: 'BAD_INPUT', summary: 'Neither `source` nor `path` was given.', data: z.object({}) },
|
|
118
|
+
],
|
|
119
|
+
requires: {},
|
|
120
|
+
host: 'node',
|
|
121
|
+
mutates: false,
|
|
122
|
+
supportsDryRun: false,
|
|
123
|
+
permission: { risk: 'read', summary: 'Reads one file’s text (or a given string); no writes.' },
|
|
124
|
+
async impl(input, ctx) {
|
|
125
|
+
const source = resolveSource(input, ctx);
|
|
126
|
+
return { cues: extractHeuristicCues(source) };
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// cinematic.cue.validate
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
const CinematicCueValidateResult = z.object({
|
|
135
|
+
valid: z.boolean(),
|
|
136
|
+
issues: z.array(z.string()),
|
|
137
|
+
cues: z.array(HeuristicCueSchema),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
export const cinematicCueValidate = defineOperation({
|
|
141
|
+
name: 'cinematic.cue.validate',
|
|
142
|
+
summary:
|
|
143
|
+
'Validate unique cue IDs and ascending time ordering for the cues found by cinematic.cue.list’s ' +
|
|
144
|
+
'same heuristic.',
|
|
145
|
+
description:
|
|
146
|
+
'STATIC HEURISTIC (see cinematic.cue.list / module jsdoc for the same limit). Two checks, ' +
|
|
147
|
+
'mirroring createCueRunner’s own runtime contract (packages/engine/src/animation/cinematic-cues.ts): ' +
|
|
148
|
+
'(1) no duplicate `id` (the runtime throws on this); (2) declaration order is already non-decreasing ' +
|
|
149
|
+
'by `time` (createCueRunner re-sorts regardless, but declaration order drifting from evaluation order ' +
|
|
150
|
+
'is a readability/authoring smell worth flagging, not a hard runtime requirement).',
|
|
151
|
+
input: CueSourceInput,
|
|
152
|
+
result: CinematicCueValidateResult,
|
|
153
|
+
errors: [
|
|
154
|
+
NO_ROOT_ERROR,
|
|
155
|
+
CUE_FILE_NOT_FOUND_ERROR,
|
|
156
|
+
{ code: 'BAD_INPUT', summary: 'Neither `source` nor `path` was given.', data: z.object({}) },
|
|
157
|
+
],
|
|
158
|
+
requires: {},
|
|
159
|
+
host: 'node',
|
|
160
|
+
mutates: false,
|
|
161
|
+
supportsDryRun: false,
|
|
162
|
+
permission: { risk: 'read', summary: 'Reads one file’s text (or a given string); no writes.' },
|
|
163
|
+
async impl(input, ctx) {
|
|
164
|
+
const source = resolveSource(input, ctx);
|
|
165
|
+
const cues = extractHeuristicCues(source);
|
|
166
|
+
const issues: string[] = [];
|
|
167
|
+
|
|
168
|
+
const seen = new Map<string, number>();
|
|
169
|
+
cues.forEach((cue, i) => {
|
|
170
|
+
if (seen.has(cue.id)) {
|
|
171
|
+
issues.push(
|
|
172
|
+
`Duplicate cue id "${cue.id}" at positions ${seen.get(cue.id)} and ${i} — ids must be unique.`,
|
|
173
|
+
);
|
|
174
|
+
} else {
|
|
175
|
+
seen.set(cue.id, i);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
for (let i = 1; i < cues.length; i++) {
|
|
180
|
+
const prev = cues[i - 1]!;
|
|
181
|
+
const cur = cues[i]!;
|
|
182
|
+
if (cur.time < prev.time) {
|
|
183
|
+
issues.push(
|
|
184
|
+
`Cue "${cur.id}" (time ${cur.time}) is declared after "${prev.id}" (time ${prev.time}) but ` +
|
|
185
|
+
'has an earlier time — declaration order drifts from evaluation order.',
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { valid: issues.length === 0, issues, cues };
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
/** Register cinematic.cue.list / cinematic.cue.validate onto `registry`. */
|
|
195
|
+
export function registerCueOperations(registry: OperationRegistry): void {
|
|
196
|
+
registry.register(cinematicCueList);
|
|
197
|
+
registry.register(cinematicCueValidate);
|
|
198
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cinematic.gsap.inspect` (B5, §8 B5 "inspect registered deterministic GSAP
|
|
3
|
+
* timelines").
|
|
4
|
+
*
|
|
5
|
+
* DECISION (per this unit's brief — "static/metadata where possible; named
|
|
6
|
+
* error if runtime-only"): this is STATIC-ONLY, by design, not a wiring gap.
|
|
7
|
+
* `registerGsap` (`packages/engine/src/animation/gsap-registration.ts`)
|
|
8
|
+
* stores its bookkeeping in a module-private
|
|
9
|
+
* `WeakMap<AnimationClock, WeakSet<gsap.core.Timeline>>` — deliberately
|
|
10
|
+
* un-enumerable (no `.list()`/registry object is exported at all, so that
|
|
11
|
+
* neither a disposed clock nor a disposed timeline is kept reachable/leaked
|
|
12
|
+
* just so something else could list it later). There is consequently no
|
|
13
|
+
* live surface — local or over any wire protocol — that could ever answer
|
|
14
|
+
* "what GSAP timelines are registered on this running clock right now",
|
|
15
|
+
* independent of this unit's file-ownership boundary (that WeakMap is
|
|
16
|
+
* `packages/engine/src/animation/gsap-registration.ts`, not
|
|
17
|
+
* `vgai-sdk`, and adding an enumeration API to it is out of this unit's
|
|
18
|
+
* scope regardless). So the honest, useful thing this op CAN do is exactly
|
|
19
|
+
* what B2's `project.xstate.discover` does for XState machines
|
|
20
|
+
* (`../project/discovery-operations.ts`) — best-effort STATIC discovery of
|
|
21
|
+
* source files that call `registerGsap(...)`, clearly reported as
|
|
22
|
+
* static-only, never a live registration count.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { z } from 'zod';
|
|
28
|
+
import { OperationError } from '../errors.js';
|
|
29
|
+
import { listProjectFiles } from '../project/shared.js';
|
|
30
|
+
import { defineOperation, type OperationRegistry } from '../registry.js';
|
|
31
|
+
import { NO_ROOT_ERROR } from './theatre-operations.js';
|
|
32
|
+
|
|
33
|
+
/** True when the file text looks like it registers a GSAP timeline for deterministic scrub/export. */
|
|
34
|
+
function findGsapRegistrations(text: string): {
|
|
35
|
+
importsGsapRegistration: boolean;
|
|
36
|
+
callCount: number;
|
|
37
|
+
} {
|
|
38
|
+
const importsGsapRegistration = /from\s+['"][^'"]*gsap-registration(?:\.js)?['"]/.test(text);
|
|
39
|
+
const callMatches = text.match(/\bregisterGsap\s*\(/g);
|
|
40
|
+
return { importsGsapRegistration, callCount: callMatches?.length ?? 0 };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const CinematicGsapInspectInput = z.object({
|
|
44
|
+
root: z
|
|
45
|
+
.string()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe('Directory to scan for GSAP registration call sites. Defaults to ctx.projectRoot.'),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const CinematicGsapInspectResult = z.object({
|
|
51
|
+
root: z.string(),
|
|
52
|
+
staticInspectionOnly: z
|
|
53
|
+
.literal(true)
|
|
54
|
+
.describe(
|
|
55
|
+
'Always true — see module jsdoc: registerGsap stores registrations in an un-enumerable ' +
|
|
56
|
+
'WeakMap/WeakSet by design, so there is no live surface this (or any) op could poll instead.',
|
|
57
|
+
),
|
|
58
|
+
registrations: z
|
|
59
|
+
.array(
|
|
60
|
+
z.object({
|
|
61
|
+
path: z.string(),
|
|
62
|
+
importsGsapRegistration: z.boolean(),
|
|
63
|
+
callCount: z
|
|
64
|
+
.number()
|
|
65
|
+
.describe(
|
|
66
|
+
'Number of `registerGsap(` call-site TEXT matches in this file (a heuristic count, not a proof of how many timelines end up registered at runtime).',
|
|
67
|
+
),
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
.describe('Every *.ts/*.tsx file whose text imports gsap-registration, sorted by path.'),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export const cinematicGsapInspect = defineOperation({
|
|
74
|
+
name: 'cinematic.gsap.inspect',
|
|
75
|
+
summary:
|
|
76
|
+
'Best-effort static discovery of source files that call registerGsap(...) to register a ' +
|
|
77
|
+
'deterministic GSAP timeline.',
|
|
78
|
+
description:
|
|
79
|
+
'STATIC-ONLY BY DESIGN, not a wiring gap — see module jsdoc. registerGsap’s bookkeeping is an ' +
|
|
80
|
+
'un-enumerable WeakMap/WeakSet (packages/engine/src/animation/gsap-registration.ts), so no ' +
|
|
81
|
+
'live "what is registered right now" surface exists anywhere to poll instead.',
|
|
82
|
+
input: CinematicGsapInspectInput,
|
|
83
|
+
result: CinematicGsapInspectResult,
|
|
84
|
+
errors: [NO_ROOT_ERROR],
|
|
85
|
+
requires: {},
|
|
86
|
+
host: 'node',
|
|
87
|
+
mutates: false,
|
|
88
|
+
supportsDryRun: false,
|
|
89
|
+
permission: { risk: 'read', summary: 'Reads *.ts/*.tsx file contents under root; no writes.' },
|
|
90
|
+
async impl(input, ctx) {
|
|
91
|
+
const root = input.root ?? ctx.projectRoot;
|
|
92
|
+
if (!root) {
|
|
93
|
+
throw new OperationError(
|
|
94
|
+
'NO_ROOT',
|
|
95
|
+
'Neither an explicit `root` input nor ctx.projectRoot was given.',
|
|
96
|
+
{},
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const candidates = listProjectFiles(
|
|
100
|
+
root,
|
|
101
|
+
(p) => (p.endsWith('.ts') || p.endsWith('.tsx')) && !p.endsWith('.d.ts'),
|
|
102
|
+
);
|
|
103
|
+
const registrations: { path: string; importsGsapRegistration: boolean; callCount: number }[] =
|
|
104
|
+
[];
|
|
105
|
+
for (const rel of candidates) {
|
|
106
|
+
const abs = join(root, rel);
|
|
107
|
+
let text: string;
|
|
108
|
+
try {
|
|
109
|
+
if (!statSync(abs).isFile()) continue;
|
|
110
|
+
text = readFileSync(abs, 'utf-8');
|
|
111
|
+
} catch {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const found = findGsapRegistrations(text);
|
|
115
|
+
if (found.importsGsapRegistration || found.callCount > 0) {
|
|
116
|
+
registrations.push({ path: rel, ...found });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { root, staticInspectionOnly: true as const, registrations };
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
/** Register cinematic.gsap.inspect onto `registry`. */
|
|
124
|
+
export function registerGsapOperations(registry: OperationRegistry): void {
|
|
125
|
+
registry.register(cinematicGsapInspect);
|
|
126
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B5 — `cinematic.*` operations
|
|
3
|
+
* (docs/AI-NATIVE-AUTHORING-IMPLEMENTATION-SPEC.md §8 B5). Registered
|
|
4
|
+
* separately from B1's `registerBuiltinOperations` (`../operations.ts`),
|
|
5
|
+
* B2's `registerProjectOperations` (`../project/index.ts`), B3's
|
|
6
|
+
* `registerEditorOperations` (`../editor/index.ts`), and B4's
|
|
7
|
+
* `registerPlayOperations` (`../play/index.ts`) — the default `operations`
|
|
8
|
+
* singleton (`../index.ts`) calls all five.
|
|
9
|
+
*
|
|
10
|
+
* Removes B1's `cinematic.render` stand-in (`operations.ts`, which always
|
|
11
|
+
* threw `NOT_IMPLEMENTED`) now that this module registers the REAL
|
|
12
|
+
* implementation under the same name — same precedent B3 set for
|
|
13
|
+
* `editor.selection.get` (see `operations.ts`'s updated module jsdoc).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export * from './capabilities-operations.js';
|
|
17
|
+
export * from './cue-operations.js';
|
|
18
|
+
export * from './gsap-operations.js';
|
|
19
|
+
export * from './preview-operations.js';
|
|
20
|
+
export type {
|
|
21
|
+
CinematicOpenPreviewFn,
|
|
22
|
+
CinematicPreviewHandle,
|
|
23
|
+
CinematicPreviewSession,
|
|
24
|
+
} from './preview-transport.js';
|
|
25
|
+
export {
|
|
26
|
+
_clearPreviewSessionsForTests,
|
|
27
|
+
getCinematicOpenPreviewFn,
|
|
28
|
+
getPreviewSession,
|
|
29
|
+
} from './preview-transport.js';
|
|
30
|
+
export * from './render-operations.js';
|
|
31
|
+
export type {
|
|
32
|
+
CinematicRenderFn,
|
|
33
|
+
CinematicRenderJob,
|
|
34
|
+
CinematicRenderJobError,
|
|
35
|
+
} from './render-transport.js';
|
|
36
|
+
export {
|
|
37
|
+
_clearRenderJobsForTests,
|
|
38
|
+
getCinematicRenderFn,
|
|
39
|
+
getRenderJob,
|
|
40
|
+
} from './render-transport.js';
|
|
41
|
+
export * from './theatre-operations.js';
|
|
42
|
+
|
|
43
|
+
import type { OperationRegistry } from '../registry.js';
|
|
44
|
+
import { registerCapabilitiesOperations } from './capabilities-operations.js';
|
|
45
|
+
import { registerCueOperations } from './cue-operations.js';
|
|
46
|
+
import { registerGsapOperations } from './gsap-operations.js';
|
|
47
|
+
import { registerPreviewOperations } from './preview-operations.js';
|
|
48
|
+
import { registerRenderOperations } from './render-operations.js';
|
|
49
|
+
import { registerTheatreOperations } from './theatre-operations.js';
|
|
50
|
+
|
|
51
|
+
/** Register every B5 `cinematic.*` operation onto `registry`. */
|
|
52
|
+
export function registerCinematicOperations(registry: OperationRegistry): void {
|
|
53
|
+
registerTheatreOperations(registry);
|
|
54
|
+
registerCueOperations(registry);
|
|
55
|
+
registerGsapOperations(registry);
|
|
56
|
+
registerCapabilitiesOperations(registry);
|
|
57
|
+
registerRenderOperations(registry);
|
|
58
|
+
registerPreviewOperations(registry);
|
|
59
|
+
}
|