@glissade/cli 0.43.1-pre.0 → 0.44.0-pre.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/dist/cli.js CHANGED
@@ -91,6 +91,7 @@ const USAGE = `usage:
91
91
  gs mcp <scene-module> start an MCP stdio server for this scene: describe / list_targets / apply_patch / undo / render_frame (the AI-native write layer)
92
92
  gs build [filter...] [--config <glissade.config.ts>] [--affected <git-ref>] [--explain] content-graph DAG runner: narrate→sfx→loudness→render per scene, runs ONLY the stale subtree. --affected <ref> pre-filters to scenes a git diff since <ref> touched (rebuild only what a change set touched; composed with the per-step content-hash staleness)
93
93
  gs describe [--out <api.json>] [--examples] snapshot THIS engine's describe() API manifest (stdout, or --out to a file) — the input to gs migrate
94
+ gs types [--out <file.ts>] [--from <api.json>] [--check] codegen a type-checked track() SDK from the describe() manifest: only registered animatable paths + their value types compile, so a typo'd path or wrong value-type id is a COMPILE error (import track from the generated file). --check fails if --out is stale. Zero-runtime (types + a re-typed re-export of the real track)
94
95
  gs migrate <baseline-api.json> [--json] [--check] diff a saved API manifest against the current engine: moved imports / removed / added / changed, with a suggested fix per breaking item (advisory; --check exits non-zero on any breaking change for CI gating)
95
96
  gs repin <scene-module> --golden <dir> [--name <p>] [--frames a,b,..] [--fps <n>] [--since <ref>] [--write] [--only a,b] [--heatmap <dir>] [--floor <ssim>] [--force] narration-aware golden reviewer: render current vs committed goldens, report perceptual delta + the re-narration cause, re-pin only frames you allow (default dry-run; --floor refuses a bigger-than-expected drop)
96
97
  gs localize <scene-module> --to <locale> [--from <locale>] [--write] [--strict] [--keep-voice] [--json] fork a narration into a new locale (clone segment/pause structure, PRESERVING beat ids so .start() anchors survive) + stub messages.<locale>.json from the scene's t() ids, running the render path's parity + localize checks BEFORE any TTS. Default dry-run (exits non-zero on drift); --write emits <base>.<locale>.narration.json + messages.<locale>.json (re-localize CARRIES existing translations over — never clobbers); --strict refuses to write on a preflight failure
@@ -195,7 +196,7 @@ async function main() {
195
196
  process.stdout.write(`${describe().version}\n`);
196
197
  return;
197
198
  }
198
- if (command !== "render" && command !== "diff" && command !== "verify-determinism" && command !== "dev" && command !== "import" && command !== "narrate" && command !== "narration-lint" && command !== "sfx" && command !== "prepare" && command !== "measure-loudness" && command !== "fonts" && command !== "cache" && command !== "mcp" && command !== "build" && command !== "describe" && command !== "migrate" && command !== "repin" && command !== "master" && command !== "localize") {
199
+ if (command !== "render" && command !== "diff" && command !== "verify-determinism" && command !== "dev" && command !== "import" && command !== "narrate" && command !== "narration-lint" && command !== "sfx" && command !== "prepare" && command !== "measure-loudness" && command !== "fonts" && command !== "cache" && command !== "mcp" && command !== "build" && command !== "describe" && command !== "migrate" && command !== "repin" && command !== "master" && command !== "localize" && command !== "types") {
199
200
  console.error(USAGE);
200
201
  process.exit(command === void 0 || command === "help" || command === "--help" ? 0 : 1);
201
202
  }
@@ -307,6 +308,40 @@ async function main() {
307
308
  } else process.stdout.write(json);
308
309
  return;
309
310
  }
311
+ if (command === "types") {
312
+ const { flags: tf } = parseArgs(rest);
313
+ const { generateTypedSdk } = await import("./typedSdk.js");
314
+ const { readFileSync, writeFileSync, existsSync } = await import("node:fs");
315
+ const from = tf.get("from");
316
+ let manifest;
317
+ if (from) {
318
+ try {
319
+ manifest = JSON.parse(readFileSync(from, "utf8"));
320
+ } catch (err) {
321
+ fail(`gs types: could not read manifest '${from}': ${err instanceof Error ? err.message : String(err)}`);
322
+ }
323
+ if (typeof manifest.version !== "string" || manifest.nodes === void 0) fail(`'${from}' is not a describe() API manifest (missing version/nodes)`);
324
+ } else {
325
+ const { describe } = await import("@glissade/scene/describe");
326
+ manifest = describe();
327
+ }
328
+ const src = generateTypedSdk(manifest);
329
+ const outPath = tf.get("out");
330
+ if (tf.has("check")) {
331
+ if (!outPath) fail("gs types --check needs --out <file> (the committed typed-SDK file to verify)");
332
+ if ((existsSync(outPath) ? readFileSync(outPath, "utf8") : "") !== src) {
333
+ process.stderr.write(`gs types: ${outPath} is STALE — run \`gs types --out ${outPath}\` to regenerate\n`);
334
+ process.exit(1);
335
+ }
336
+ process.stderr.write(`gs types: ${outPath} is up to date\n`);
337
+ return;
338
+ }
339
+ if (outPath) {
340
+ writeFileSync(outPath, src);
341
+ process.stderr.write(`gs types: wrote a typed track() SDK (${Object.keys(manifest.nodes).length} node types) → ${outPath}\n`);
342
+ } else process.stdout.write(src);
343
+ return;
344
+ }
310
345
  if (command === "migrate") {
311
346
  const { positional: mp, flags: mf } = parseArgs(rest);
312
347
  const baselinePath = mp[0];
@@ -0,0 +1,96 @@
1
+ //#region src/typedSdk.ts
2
+ /** value-type id → the TS type its keyframes carry. Unmapped ids fall back to `unknown`. */
3
+ const TS_VALUE_TYPE = {
4
+ number: "number",
5
+ vec2: "readonly [number, number]",
6
+ "vec2-arc": "readonly [number, number]",
7
+ color: "string",
8
+ string: "string",
9
+ boolean: "boolean",
10
+ paint: "Paint",
11
+ fontAxes: "FontAxes",
12
+ path: "PathValue"
13
+ };
14
+ /** core types the generated file must import when a mapped value type references them. */
15
+ const CORE_TYPE_IMPORTS = {
16
+ paint: "Paint",
17
+ fontAxes: "FontAxes",
18
+ path: "PathValue"
19
+ };
20
+ /**
21
+ * Collect every animatable prop path across the manifest's node taxonomy (+ the
22
+ * component surface is construction-only, so excluded). Deduped by path; the FIRST
23
+ * value type wins and a differing later one is reported in `conflicts` (base targets
24
+ * are uniform — a conflict is a taxonomy smell worth surfacing, not silently picking).
25
+ */
26
+ function collectTargets(manifest) {
27
+ const byPath = /* @__PURE__ */ new Map();
28
+ const conflicts = [];
29
+ for (const node of Object.values(manifest.nodes)) for (const [path, prop] of Object.entries(node.props)) {
30
+ if (!prop.animatable || prop.target === void 0) continue;
31
+ const existing = byPath.get(path);
32
+ if (existing === void 0) byPath.set(path, prop.type);
33
+ else if (existing !== prop.type) conflicts.push(`${path}: ${existing} vs ${prop.type}`);
34
+ }
35
+ return {
36
+ targets: [...byPath.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([path, valueType]) => ({
37
+ path,
38
+ valueType
39
+ })),
40
+ conflicts
41
+ };
42
+ }
43
+ const tsValueOf = (valueType) => TS_VALUE_TYPE[valueType] ?? "unknown";
44
+ /**
45
+ * Generate the typed-SDK source (a `.ts` module). Emits a `KnownTrackPath` union, a
46
+ * `TrackTarget` template (`` `${string}/${KnownTrackPath}` ``), per-path `TypeIdOf` /
47
+ * `ValueOf` conditional maps, and a re-typed `track` re-export whose runtime IS
48
+ * `@glissade/core`'s `track` (only the types are narrowed). Deterministic: paths are
49
+ * sorted, so the same manifest → byte-identical output (drift-guardable with `--check`).
50
+ */
51
+ function generateTypedSdk(manifest) {
52
+ const { targets, conflicts } = collectTargets(manifest);
53
+ const usedCoreTypes = [...new Set(targets.map((t) => CORE_TYPE_IMPORTS[t.valueType]).filter((n) => n !== void 0))].sort();
54
+ const L = [];
55
+ L.push(`// GENERATED by \`gs types\` from the describe() API manifest v${manifest.version} — DO NOT EDIT.`);
56
+ L.push("// Regenerate after a glissade upgrade (or when you add a defineComponent target):");
57
+ L.push("// gs types --out glissade-targets.ts");
58
+ L.push("// Then import `track` from here instead of @glissade/core to type-check your targets.");
59
+ if (conflicts.length) L.push(`// NOTE: ${conflicts.length} path(s) carry >1 value type across nodes: ${conflicts.join(", ")}`);
60
+ L.push("");
61
+ L.push(`import { track as _track, type Track, type Key } from '@glissade/core';`);
62
+ if (usedCoreTypes.length) L.push(`import type { ${usedCoreTypes.join(", ")} } from '@glissade/core';`);
63
+ L.push("");
64
+ L.push("/** Every animatable prop path in the describe() taxonomy — a typo'd path is a type error. */");
65
+ L.push(`export type KnownTrackPath =`);
66
+ for (const t of targets) L.push(` | '${t.path}'`);
67
+ L.push(` ;`);
68
+ L.push("");
69
+ L.push("/** A track target: any node id + a KNOWN animatable path. */");
70
+ L.push("export type TrackTarget = `${string}/${KnownTrackPath}`;");
71
+ L.push("");
72
+ L.push("/** The value-type id each path expects (a wrong `type` arg is a type error). */");
73
+ L.push("type TypeIdOf<P extends TrackTarget> =");
74
+ for (const t of targets) L.push(` P extends \`\${string}/${t.path}\` ? '${t.valueType}' :`);
75
+ L.push(" never;");
76
+ L.push("");
77
+ L.push("/** The value each path animates (the keyframe value type). */");
78
+ L.push("type ValueOf<P extends TrackTarget> =");
79
+ for (const t of targets) L.push(` P extends \`\${string}/${t.path}\` ? ${tsValueOf(t.valueType)} :`);
80
+ L.push(" never;");
81
+ L.push("");
82
+ L.push("/**");
83
+ L.push(" * Type-checked `track` — only a KNOWN path + its correct value type compile.");
84
+ L.push(" * Runtime is `@glissade/core`'s `track` verbatim (only the types are narrowed).");
85
+ L.push(" */");
86
+ L.push("export const track = _track as <P extends TrackTarget>(");
87
+ L.push(" target: P,");
88
+ L.push(" type: TypeIdOf<P>,");
89
+ L.push(" keys: Key<ValueOf<P>>[],");
90
+ L.push(" opts?: { editable?: boolean },");
91
+ L.push(") => Track<ValueOf<P>>;");
92
+ L.push("");
93
+ return L.join("\n");
94
+ }
95
+ //#endregion
96
+ export { generateTypedSdk };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/cli",
3
- "version": "0.43.1-pre.0",
3
+ "version": "0.44.0-pre.0",
4
4
  "description": "glissade CLI: headless rendering via backend-skia (+ FFmpeg mux).",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -28,15 +28,15 @@
28
28
  "@napi-rs/canvas": "^0.1.65",
29
29
  "esbuild": "0.28.0",
30
30
  "jiti": "^2.4.2",
31
- "@glissade/backend-skia": "0.43.1-pre.0",
32
- "@glissade/core": "0.43.1-pre.0",
33
- "@glissade/interact": "0.43.1-pre.0",
34
- "@glissade/lottie": "0.43.1-pre.0",
35
- "@glissade/narrate": "0.43.1-pre.0",
36
- "@glissade/player": "0.43.1-pre.0",
37
- "@glissade/scene": "0.43.1-pre.0",
38
- "@glissade/sfx": "0.43.1-pre.0",
39
- "@glissade/svg": "0.43.1-pre.0"
31
+ "@glissade/backend-skia": "0.44.0-pre.0",
32
+ "@glissade/core": "0.44.0-pre.0",
33
+ "@glissade/interact": "0.44.0-pre.0",
34
+ "@glissade/lottie": "0.44.0-pre.0",
35
+ "@glissade/narrate": "0.44.0-pre.0",
36
+ "@glissade/player": "0.44.0-pre.0",
37
+ "@glissade/scene": "0.44.0-pre.0",
38
+ "@glissade/sfx": "0.44.0-pre.0",
39
+ "@glissade/svg": "0.44.0-pre.0"
40
40
  },
41
41
  "repository": {
42
42
  "type": "git",