@koda-sl/baker-cli 0.131.0-dev.cad5e0bc7 → 0.132.0-dev.cad5e0bc7

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
@@ -4926,11 +4926,11 @@ function rawTextEntries(value) {
4926
4926
  const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
4927
4927
  return values.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
4928
4928
  }
4929
- function rawFileEntries(path21) {
4930
- if (typeof path21 !== "string" || path21.length === 0) {
4929
+ function rawFileEntries(path23) {
4930
+ if (typeof path23 !== "string" || path23.length === 0) {
4931
4931
  return [];
4932
4932
  }
4933
- return readFileSync2(path21, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
4933
+ return readFileSync2(path23, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
4934
4934
  }
4935
4935
  function keywordEntries(args) {
4936
4936
  const defaultMatch = typeof args["match-type"] === "string" ? args["match-type"].toUpperCase() : void 0;
@@ -4953,19 +4953,19 @@ function keywordEntries(args) {
4953
4953
  }
4954
4954
  return entries;
4955
4955
  }
4956
- function loadJsonFileArg(path21) {
4957
- if (typeof path21 !== "string" || path21.length === 0) {
4956
+ function loadJsonFileArg(path23) {
4957
+ if (typeof path23 !== "string" || path23.length === 0) {
4958
4958
  return {};
4959
4959
  }
4960
4960
  try {
4961
- const parsed = JSON.parse(readFileSync2(path21, "utf8"));
4961
+ const parsed = JSON.parse(readFileSync2(path23, "utf8"));
4962
4962
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4963
- failWriteValidation(`${path21} must contain a JSON object`);
4963
+ failWriteValidation(`${path23} must contain a JSON object`);
4964
4964
  }
4965
4965
  return parsed;
4966
4966
  } catch (err) {
4967
4967
  if (err instanceof SyntaxError) {
4968
- failWriteValidation(`${path21} is not valid JSON: ${err.message}`);
4968
+ failWriteValidation(`${path23} is not valid JSON: ${err.message}`);
4969
4969
  }
4970
4970
  throw err;
4971
4971
  }
@@ -5076,10 +5076,10 @@ async function stageUpdate(kind, customerId, target, payload) {
5076
5076
  async function stageTarget(kind, customerId, target) {
5077
5077
  await stageGoogleOp({ kind, customerId, target });
5078
5078
  }
5079
- async function draftAction(path21, body) {
5079
+ async function draftAction(path23, body) {
5080
5080
  try {
5081
5081
  const chatId = requireChatId();
5082
- const response = await apiPost(path21, { chatId, ...body });
5082
+ const response = await apiPost(path23, { chatId, ...body });
5083
5083
  writeJsonEnvelope(response);
5084
5084
  } catch (err) {
5085
5085
  handleGoogleError(err);
@@ -8834,19 +8834,19 @@ function failWriteValidation2(message) {
8834
8834
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
8835
8835
  process.exit(1);
8836
8836
  }
8837
- function loadJsonFileArg2(path21) {
8838
- if (typeof path21 !== "string" || path21.length === 0) {
8837
+ function loadJsonFileArg2(path23) {
8838
+ if (typeof path23 !== "string" || path23.length === 0) {
8839
8839
  return {};
8840
8840
  }
8841
8841
  try {
8842
- const parsed = JSON.parse(readFileSync6(path21, "utf8"));
8842
+ const parsed = JSON.parse(readFileSync6(path23, "utf8"));
8843
8843
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
8844
- failWriteValidation2(`${path21} must contain a JSON object`);
8844
+ failWriteValidation2(`${path23} must contain a JSON object`);
8845
8845
  }
8846
8846
  return parsed;
8847
8847
  } catch (err) {
8848
8848
  if (err instanceof SyntaxError) {
8849
- failWriteValidation2(`${path21} is not valid JSON: ${err.message}`);
8849
+ failWriteValidation2(`${path23} is not valid JSON: ${err.message}`);
8850
8850
  }
8851
8851
  throw err;
8852
8852
  }
@@ -8931,15 +8931,15 @@ function parseLocaleFlag(value) {
8931
8931
  }
8932
8932
  return { language: match[1], country: match[2].toUpperCase() };
8933
8933
  }
8934
- function loadTargetingFileArg(path21) {
8935
- if (typeof path21 !== "string" || path21.length === 0) {
8934
+ function loadTargetingFileArg(path23) {
8935
+ if (typeof path23 !== "string" || path23.length === 0) {
8936
8936
  return void 0;
8937
8937
  }
8938
- const parsed = loadJsonFileArg2(path21);
8938
+ const parsed = loadJsonFileArg2(path23);
8939
8939
  const criteria = parsed.targetingCriteria ?? parsed;
8940
8940
  if (!criteria.include) {
8941
8941
  failWriteValidation2(
8942
- `${path21} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
8942
+ `${path23} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
8943
8943
  );
8944
8944
  }
8945
8945
  return criteria;
@@ -8974,14 +8974,14 @@ function parseCsvLine(line) {
8974
8974
  cells.push(current);
8975
8975
  return cells.map((cell) => cell.trim());
8976
8976
  }
8977
- function parseListFileArg(path21, maxRows) {
8978
- if (typeof path21 !== "string" || path21.length === 0) {
8977
+ function parseListFileArg(path23, maxRows) {
8978
+ if (typeof path23 !== "string" || path23.length === 0) {
8979
8979
  return void 0;
8980
8980
  }
8981
- const raw = readFileSync6(path21, "utf8");
8981
+ const raw = readFileSync6(path23, "utf8");
8982
8982
  const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
8983
8983
  if (lines.length < 2) {
8984
- failWriteValidation2(`${path21} needs a header row and at least one data row`);
8984
+ failWriteValidation2(`${path23} needs a header row and at least one data row`);
8985
8985
  }
8986
8986
  const columns = parseCsvLine(lines[0]).map((column) => column.trim());
8987
8987
  const rows = [];
@@ -9000,7 +9000,7 @@ function parseListFileArg(path21, maxRows) {
9000
9000
  }
9001
9001
  }
9002
9002
  if (rows.length > maxRows) {
9003
- failWriteValidation2(`${path21} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
9003
+ failWriteValidation2(`${path23} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
9004
9004
  }
9005
9005
  return { columns, rows };
9006
9006
  }
@@ -11083,11 +11083,11 @@ var updateStatusSchema = z11.enum(UPDATE_STATUSES);
11083
11083
  function currencyMinimums2(currencyCode) {
11084
11084
  return CURRENCY_MINIMUMS2[currencyCode] ?? DEFAULT_CURRENCY_MINIMUM2;
11085
11085
  }
11086
- function validateDailyBudgetFloor(money, ctx, path21) {
11086
+ function validateDailyBudgetFloor(money, ctx, path23) {
11087
11087
  if (money?.currencyCode) {
11088
11088
  const min = currencyMinimums2(money.currencyCode).dailyBudgetMin;
11089
11089
  if (Number(money.amount) < min) {
11090
- ctx.addIssue({ code: "custom", path: path21, message: `below the ${min} ${money.currencyCode} daily minimum` });
11090
+ ctx.addIssue({ code: "custom", path: path23, message: `below the ${min} ${money.currencyCode} daily minimum` });
11091
11091
  }
11092
11092
  }
11093
11093
  }
@@ -11575,19 +11575,19 @@ function failWriteValidation3(message) {
11575
11575
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
11576
11576
  process.exit(1);
11577
11577
  }
11578
- function loadJsonFileArg3(path21) {
11579
- if (typeof path21 !== "string" || path21.length === 0) {
11578
+ function loadJsonFileArg3(path23) {
11579
+ if (typeof path23 !== "string" || path23.length === 0) {
11580
11580
  return {};
11581
11581
  }
11582
11582
  try {
11583
- const parsed = JSON.parse(readFileSync8(path21, "utf8"));
11583
+ const parsed = JSON.parse(readFileSync8(path23, "utf8"));
11584
11584
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
11585
- failWriteValidation3(`${path21} must contain a JSON object`);
11585
+ failWriteValidation3(`${path23} must contain a JSON object`);
11586
11586
  }
11587
11587
  return parsed;
11588
11588
  } catch (err) {
11589
11589
  if (err instanceof SyntaxError) {
11590
- failWriteValidation3(`${path21} is not valid JSON: ${err.message}`);
11590
+ failWriteValidation3(`${path23} is not valid JSON: ${err.message}`);
11591
11591
  }
11592
11592
  throw err;
11593
11593
  }
@@ -14809,12 +14809,12 @@ async function probeDuration(filePath) {
14809
14809
  }
14810
14810
 
14811
14811
  // src/commands/canvas/rerun.ts
14812
- import path11 from "path";
14812
+ import path13 from "path";
14813
14813
  import { defineCommand as defineCommand90 } from "citty";
14814
14814
 
14815
14815
  // src/commands/canvas/run.ts
14816
- import { readFile as readFile7 } from "fs/promises";
14817
- import path10 from "path";
14816
+ import { readFile as readFile9 } from "fs/promises";
14817
+ import path12 from "path";
14818
14818
  import { defineCommand as defineCommand89 } from "citty";
14819
14819
 
14820
14820
  // src/commands/canvas/placeholders.ts
@@ -14859,34 +14859,110 @@ function isResolvableRelative(value) {
14859
14859
  }
14860
14860
 
14861
14861
  // src/commands/canvas/source-version.ts
14862
- import { readFile as readFile3 } from "fs/promises";
14862
+ import { readFile as readFile4 } from "fs/promises";
14863
+ import path5 from "path";
14864
+
14865
+ // src/commands/canvas/scene-files.ts
14866
+ import { mkdir, readFile as readFile3, readdir as readdir2, rm, writeFile } from "fs/promises";
14863
14867
  import path4 from "path";
14868
+ var SCENES_DIR = "scenes";
14869
+ var GLOBAL_PROMPT_FILE = "prompt.json";
14870
+ var REBUILD_FILE = "prompt.rebuild.json";
14871
+ function sceneFileName(index, total) {
14872
+ const width = Math.max(2, String(Math.max(0, total - 1)).length);
14873
+ return `s${String(index).padStart(width, "0")}.json`;
14874
+ }
14875
+ function splitBlueprint(blueprint) {
14876
+ if (!blueprint || typeof blueprint !== "object") {
14877
+ return { global: {}, scenes: [] };
14878
+ }
14879
+ const { scenes, ...global } = blueprint;
14880
+ return { global, scenes: Array.isArray(scenes) ? scenes : [] };
14881
+ }
14882
+ async function writeSceneFiles(outDir, blueprint) {
14883
+ const { global, scenes } = splitBlueprint(blueprint);
14884
+ await writeFile(path4.join(outDir, GLOBAL_PROMPT_FILE), `${JSON.stringify(global, null, 2)}
14885
+ `, "utf8");
14886
+ const scenesDir = path4.join(outDir, SCENES_DIR);
14887
+ await mkdir(scenesDir, { recursive: true });
14888
+ const written = /* @__PURE__ */ new Set();
14889
+ for (let i = 0; i < scenes.length; i++) {
14890
+ const name = sceneFileName(i, scenes.length);
14891
+ written.add(name);
14892
+ await writeFile(path4.join(scenesDir, name), `${JSON.stringify(scenes[i], null, 2)}
14893
+ `, "utf8");
14894
+ }
14895
+ for (const name of await listSceneFileNames(scenesDir)) {
14896
+ if (!written.has(name)) await rm(path4.join(scenesDir, name), { force: true });
14897
+ }
14898
+ }
14899
+ async function listSceneFileNames(scenesDir) {
14900
+ let entries;
14901
+ try {
14902
+ entries = await readdir2(scenesDir);
14903
+ } catch {
14904
+ return [];
14905
+ }
14906
+ return entries.filter((n) => /^s\d+\.json$/.test(n)).sort(bySceneIndex);
14907
+ }
14908
+ async function listSceneFiles(creativeDir) {
14909
+ const scenesDir = path4.join(creativeDir, SCENES_DIR);
14910
+ return (await listSceneFileNames(scenesDir)).map((n) => path4.join(scenesDir, n));
14911
+ }
14912
+ async function reassembleBlueprint(creativeDir) {
14913
+ const files = await listSceneFiles(creativeDir);
14914
+ if (files.length === 0) return null;
14915
+ const globalRaw = await readFile3(path4.join(creativeDir, GLOBAL_PROMPT_FILE), "utf8");
14916
+ const global = JSON.parse(globalRaw);
14917
+ const scenes = [];
14918
+ for (const file of files) {
14919
+ scenes.push(JSON.parse(await readFile3(file, "utf8")));
14920
+ }
14921
+ return { ...global, scenes };
14922
+ }
14923
+ function bySceneIndex(a, b) {
14924
+ const ai = Number(a.match(/^s(\d+)\.json$/)?.[1] ?? 0);
14925
+ const bi = Number(b.match(/^s(\d+)\.json$/)?.[1] ?? 0);
14926
+ return ai - bi;
14927
+ }
14928
+
14929
+ // src/commands/canvas/source-version.ts
14864
14930
  async function computeSourceSha(canvasPath) {
14865
14931
  let canvasBytes;
14866
14932
  try {
14867
- canvasBytes = await readFile3(canvasPath);
14933
+ canvasBytes = await readFile4(canvasPath);
14868
14934
  } catch {
14869
14935
  return void 0;
14870
14936
  }
14871
- const promptPath = path4.join(path4.dirname(canvasPath), "prompt.json");
14937
+ const canvasDir = path5.dirname(canvasPath);
14938
+ const promptPath = path5.join(canvasDir, "prompt.json");
14872
14939
  let promptBytes;
14873
14940
  try {
14874
- promptBytes = await readFile3(promptPath);
14941
+ promptBytes = await readFile4(promptPath);
14875
14942
  } catch {
14876
14943
  promptBytes = Buffer.alloc(0);
14877
14944
  }
14878
- return sha256Hex(
14879
- Buffer.concat([
14880
- Buffer.from(`${canvasBytes.length}\0`),
14881
- canvasBytes,
14882
- Buffer.from(`${promptBytes.length}\0`),
14883
- promptBytes
14884
- ])
14885
- );
14945
+ const parts = [
14946
+ Buffer.from(`${canvasBytes.length}\0`),
14947
+ canvasBytes,
14948
+ Buffer.from(`${promptBytes.length}\0`),
14949
+ promptBytes
14950
+ ];
14951
+ for (const sceneFile of await listSceneFiles(canvasDir)) {
14952
+ let sceneBytes;
14953
+ try {
14954
+ sceneBytes = await readFile4(sceneFile);
14955
+ } catch {
14956
+ sceneBytes = Buffer.alloc(0);
14957
+ }
14958
+ parts.push(Buffer.from(`${sceneBytes.length}\0`), sceneBytes);
14959
+ }
14960
+ return sha256Hex(Buffer.concat(parts));
14886
14961
  }
14887
14962
 
14888
- // src/commands/canvas/style-projection.ts
14889
- import { readFile as readFile4, writeFile } from "fs/promises";
14963
+ // src/commands/canvas/scene-projection.ts
14964
+ import { readFile as readFile5 } from "fs/promises";
14965
+ import path6 from "path";
14890
14966
 
14891
14967
  // src/engine/scaffold/video.ts
14892
14968
  import { toCardinal as nwAr } from "n2words/ar-SA";
@@ -16902,6 +16978,14 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
16902
16978
  });
16903
16979
  registerMergedPhraseSlices(phrase, clipRef, clipStart, genDur, Boolean(chained), env, out);
16904
16980
  }
16981
+ function sumSceneDurations(env, first, last) {
16982
+ let total = 0;
16983
+ for (let s = first; s <= last; s++) {
16984
+ const sc = env.blueprint.scenes[s];
16985
+ if (sc) total += sceneDurationS(sc);
16986
+ }
16987
+ return total;
16988
+ }
16905
16989
  function registerMergedPhraseSlices(phrase, clipRef, clipStart, genDur, chained, env, out) {
16906
16990
  const shown = [...phrase.shownScenes].sort((a, b) => a - b);
16907
16991
  let r = 0;
@@ -16915,13 +16999,12 @@ function registerMergedPhraseSlices(phrase, clipRef, clipStart, genDur, chained,
16915
16999
  if (!firstSc) continue;
16916
17000
  const firstStart = firstSc.start_s ?? clipStart;
16917
17001
  const rawOffset = firstStart - clipStart;
16918
- const runEnd = env.blueprint.scenes[last]?.end_s ?? firstStart + sceneDurationS(firstSc);
16919
17002
  out.sceneSlice.set(first, {
16920
17003
  clipRef,
16921
17004
  // Snap a sub-frame offset (line-start vs scene-start drift) to 0 so a run that tiles
16922
17005
  // the clip hits the whole-clip fast path instead of a needless re-encode + tiny shift.
16923
17006
  offset: rawOffset < 0.05 ? 0 : rawOffset,
16924
- len: Math.max(0.5, runEnd - firstStart),
17007
+ len: Math.max(0.5, sumSceneDurations(env, first, last)),
16925
17008
  clipDur: genDur,
16926
17009
  ...firstRegistered && chained ? { continuesFrame: true } : {}
16927
17010
  });
@@ -18114,7 +18197,75 @@ function videoReport(input, elementsInput) {
18114
18197
  };
18115
18198
  }
18116
18199
 
18200
+ // src/commands/canvas/scene-projection.ts
18201
+ var PROMPT_NODE_TYPES = /* @__PURE__ */ new Set(["image_generate", "video_generate"]);
18202
+ function promptNodes(canvas) {
18203
+ const out = /* @__PURE__ */ new Map();
18204
+ const nodes = canvas?.nodes;
18205
+ if (!Array.isArray(nodes)) return out;
18206
+ for (const n of nodes) {
18207
+ if (typeof n?.id === "string" && typeof n?.type === "string" && PROMPT_NODE_TYPES.has(n.type)) {
18208
+ out.set(n.id, n);
18209
+ }
18210
+ }
18211
+ return out;
18212
+ }
18213
+ function nonPromptParamsDiverge(live = {}, rebuilt = {}) {
18214
+ const keys = /* @__PURE__ */ new Set([...Object.keys(live), ...Object.keys(rebuilt)]);
18215
+ keys.delete("prompt");
18216
+ for (const k of keys) {
18217
+ if (JSON.stringify(live[k]) !== JSON.stringify(rebuilt[k])) return true;
18218
+ }
18219
+ return false;
18220
+ }
18221
+ async function syncSceneNodeParams(canvas, canvasPath, log) {
18222
+ const creativeDir = path6.dirname(canvasPath);
18223
+ const blueprint = await reassembleBlueprint(creativeDir);
18224
+ if (!blueprint) return "not_applicable";
18225
+ let rebuildRaw;
18226
+ try {
18227
+ rebuildRaw = await readFile5(path6.join(creativeDir, REBUILD_FILE), "utf8");
18228
+ } catch {
18229
+ return "not_applicable";
18230
+ }
18231
+ const { elements, opts } = JSON.parse(rebuildRaw);
18232
+ const rebuilt = scaffoldVideoCanvas(blueprint, elements, opts);
18233
+ const rebuiltNodes = promptNodes(rebuilt);
18234
+ const liveNodes = promptNodes(canvas);
18235
+ const missingInLive = [...rebuiltNodes.keys()].filter((id) => !liveNodes.has(id));
18236
+ const missingInRebuilt = [...liveNodes.keys()].filter((id) => !rebuiltNodes.has(id));
18237
+ if (missingInLive.length > 0 || missingInRebuilt.length > 0) {
18238
+ log(
18239
+ "[scenes] scene structure changed since scaffold (a scene's route, timing, or count differs) \u2014 re-run `baker canvas scaffold-video` to rebuild the graph. Prompt-only edits were still applied where the nodes still match."
18240
+ );
18241
+ }
18242
+ let changed = 0;
18243
+ let structural = false;
18244
+ for (const [id, rebuiltNode] of rebuiltNodes) {
18245
+ const live = liveNodes.get(id);
18246
+ if (!live) continue;
18247
+ const liveParams = live.params ??= {};
18248
+ const rebuiltParams = rebuiltNode.params ?? {};
18249
+ if (nonPromptParamsDiverge(liveParams, rebuiltParams)) structural = true;
18250
+ if (typeof rebuiltParams.prompt === "string" && liveParams.prompt !== rebuiltParams.prompt) {
18251
+ liveParams.prompt = rebuiltParams.prompt;
18252
+ changed++;
18253
+ }
18254
+ }
18255
+ if (structural) {
18256
+ log(
18257
+ "[scenes] a scene edit changed clip timing or audio (not just prompt text) \u2014 the graph's timing nodes were NOT updated; re-run `baker canvas scaffold-video` so the spine and voice tracks match."
18258
+ );
18259
+ }
18260
+ if (changed === 0) return "up_to_date";
18261
+ log(
18262
+ `[scenes] re-flowed ${changed} scene prompt(s) from scenes/*.json into the graph \u2014 the affected frames/clips will re-bill on the next run`
18263
+ );
18264
+ return "regenerated";
18265
+ }
18266
+
18117
18267
  // src/commands/canvas/style-projection.ts
18268
+ import { readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
18118
18269
  function findBlueprintProjection(canvas) {
18119
18270
  if (!canvas || typeof canvas !== "object") return null;
18120
18271
  const nodes = canvas.nodes;
@@ -18142,10 +18293,10 @@ function renderStyleProjectionFromValue(blueprint) {
18142
18293
  async function syncStyleProjection(canvas, log) {
18143
18294
  const pair = findBlueprintProjection(canvas);
18144
18295
  if (!pair) return "not_applicable";
18145
- const rendered = renderStyleProjection(await readFile4(pair.promptPath, "utf8"));
18146
- const current = await readFile4(pair.stylePath, "utf8").catch(() => null);
18296
+ const rendered = renderStyleProjection(await readFile6(pair.promptPath, "utf8"));
18297
+ const current = await readFile6(pair.stylePath, "utf8").catch(() => null);
18147
18298
  if (current === rendered) return "up_to_date";
18148
- await writeFile(pair.stylePath, rendered, "utf8");
18299
+ await writeFile2(pair.stylePath, rendered, "utf8");
18149
18300
  log(
18150
18301
  "[style] prompt.style.json regenerated from prompt.json \u2014 every frame's shared ad spec changed; affected image frames will re-bill on the next run"
18151
18302
  );
@@ -18205,13 +18356,13 @@ ${body}` : header || body || compactJson(record);
18205
18356
  }
18206
18357
 
18207
18358
  // src/commands/canvas/run-record.ts
18208
- import path5 from "path";
18359
+ import path7 from "path";
18209
18360
  var MAX_RUN_NODES = 200;
18210
18361
  var MAX_OUTPUTS_PER_NODE = 10;
18211
18362
  var MAX_FINAL_OUTPUTS = 10;
18212
18363
  var MAX_CREATIVE_SLUG_LENGTH = 100;
18213
18364
  function creativeSlugFromCanvasPath(filePath) {
18214
- const normalized = filePath.split(path5.sep).join("/");
18365
+ const normalized = filePath.split(path7.sep).join("/");
18215
18366
  const match = normalized.match(/(?:^|\/)src\/creatives\/([a-z0-9](?:[a-z0-9-]*[a-z0-9])?)\//);
18216
18367
  const slug = match?.[1] ?? null;
18217
18368
  return slug && slug.length <= MAX_CREATIVE_SLUG_LENGTH ? slug : null;
@@ -18495,25 +18646,25 @@ var RunRecordPoster = class {
18495
18646
  };
18496
18647
 
18497
18648
  // src/commands/canvas/run-retention.ts
18498
- import { rm } from "fs/promises";
18499
- import path6 from "path";
18649
+ import { rm as rm2 } from "fs/promises";
18650
+ import path8 from "path";
18500
18651
  function runDirsToPrune(entries, keep, currentRunId) {
18501
18652
  const runs = entries.filter((e) => /^r_[0-9A-Za-z]+$/.test(e) && e !== currentRunId).sort();
18502
18653
  if (keep <= 0) return runs;
18503
18654
  return runs.slice(0, Math.max(0, runs.length - keep));
18504
18655
  }
18505
18656
  async function pruneOldRuns(outputsDir, keep, currentRunId, log) {
18506
- const { readdir: readdir6 } = await import("fs/promises");
18657
+ const { readdir: readdir7 } = await import("fs/promises");
18507
18658
  let entries;
18508
18659
  try {
18509
- entries = await readdir6(outputsDir);
18660
+ entries = await readdir7(outputsDir);
18510
18661
  } catch {
18511
18662
  return;
18512
18663
  }
18513
18664
  const toPrune = runDirsToPrune(entries, keep, currentRunId);
18514
18665
  if (toPrune.length === 0) return;
18515
18666
  for (const dir of toPrune) {
18516
- await rm(path6.join(outputsDir, dir), { recursive: true, force: true }).catch(
18667
+ await rm2(path8.join(outputsDir, dir), { recursive: true, force: true }).catch(
18517
18668
  (e) => log(`[prune ] could not remove ${dir}: ${e.message}`)
18518
18669
  );
18519
18670
  }
@@ -18521,34 +18672,34 @@ async function pruneOldRuns(outputsDir, keep, currentRunId, log) {
18521
18672
  }
18522
18673
 
18523
18674
  // src/commands/canvas/dirty-marker.ts
18524
- import { mkdir, readdir as readdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
18525
- import path7 from "path";
18675
+ import { mkdir as mkdir2, readdir as readdir3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
18676
+ import path9 from "path";
18526
18677
  function creativeDirtyDir(base) {
18527
- return base ?? path7.resolve("canvas", ".dirty");
18678
+ return base ?? path9.resolve("canvas", ".dirty");
18528
18679
  }
18529
18680
  function dirtyMarkerFile(slug, base) {
18530
- return path7.join(creativeDirtyDir(base), `${slug}.json`);
18681
+ return path9.join(creativeDirtyDir(base), `${slug}.json`);
18531
18682
  }
18532
18683
  async function clearCreativeDirty(slug, base) {
18533
18684
  try {
18534
- await rm2(dirtyMarkerFile(slug, base), { force: true });
18685
+ await rm3(dirtyMarkerFile(slug, base), { force: true });
18535
18686
  } catch {
18536
18687
  }
18537
18688
  }
18538
18689
 
18539
18690
  // src/commands/canvas/run-resume.ts
18540
- import { mkdir as mkdir2, readFile as readFile5, rm as rm3, writeFile as writeFile3 } from "fs/promises";
18541
- import path8 from "path";
18691
+ import { mkdir as mkdir3, readFile as readFile7, rm as rm4, writeFile as writeFile4 } from "fs/promises";
18692
+ import path10 from "path";
18542
18693
  function markerKey(canvasPath) {
18543
18694
  const slug = creativeSlugFromCanvasPath(canvasPath);
18544
- const identity2 = slug ?? path8.relative(process.cwd(), path8.resolve(canvasPath));
18695
+ const identity2 = slug ?? path10.relative(process.cwd(), path10.resolve(canvasPath));
18545
18696
  return sha256Hex(Buffer.from(identity2)).slice(0, 32);
18546
18697
  }
18547
18698
  function legacyMarkerKey(canvasPath) {
18548
- return sha256Hex(Buffer.from(path8.resolve(canvasPath))).slice(0, 32);
18699
+ return sha256Hex(Buffer.from(path10.resolve(canvasPath))).slice(0, 32);
18549
18700
  }
18550
18701
  function markerFile(outputsDir, key) {
18551
- return path8.join(outputsDir, ".inflight", `${key}.json`);
18702
+ return path10.join(outputsDir, ".inflight", `${key}.json`);
18552
18703
  }
18553
18704
  var REMOTE_ADOPT_STALE_MS = 12e4;
18554
18705
  function classifyRemoteRun(run, now) {
@@ -18584,7 +18735,7 @@ async function resolveRunId(opts) {
18584
18735
  async function readMarkerRunId(outputsDir, canvasPath) {
18585
18736
  for (const key of [markerKey(canvasPath), legacyMarkerKey(canvasPath)]) {
18586
18737
  try {
18587
- const raw = await readFile5(markerFile(outputsDir, key), "utf8");
18738
+ const raw = await readFile7(markerFile(outputsDir, key), "utf8");
18588
18739
  const parsed = JSON.parse(raw);
18589
18740
  if (typeof parsed.runId === "string" && parsed.runId.length > 0) return parsed.runId;
18590
18741
  } catch {
@@ -18595,23 +18746,23 @@ async function readMarkerRunId(outputsDir, canvasPath) {
18595
18746
  async function markRunInFlight(outputsDir, canvasPath, runId) {
18596
18747
  try {
18597
18748
  const file = markerFile(outputsDir, markerKey(canvasPath));
18598
- await mkdir2(path8.dirname(file), { recursive: true });
18599
- await writeFile3(file, JSON.stringify({ runId, canvasPath: path8.resolve(canvasPath), startedAt: Date.now() }));
18749
+ await mkdir3(path10.dirname(file), { recursive: true });
18750
+ await writeFile4(file, JSON.stringify({ runId, canvasPath: path10.resolve(canvasPath), startedAt: Date.now() }));
18600
18751
  } catch {
18601
18752
  }
18602
18753
  }
18603
18754
  async function clearRunMarker(outputsDir, canvasPath) {
18604
18755
  for (const key of [markerKey(canvasPath), legacyMarkerKey(canvasPath)]) {
18605
18756
  try {
18606
- await rm3(markerFile(outputsDir, key), { force: true });
18757
+ await rm4(markerFile(outputsDir, key), { force: true });
18607
18758
  } catch {
18608
18759
  }
18609
18760
  }
18610
18761
  }
18611
18762
 
18612
18763
  // src/commands/canvas/run-snapshot.ts
18613
- import { mkdir as mkdir3, readdir as readdir3, readFile as readFile6, stat as stat2, writeFile as writeFile4 } from "fs/promises";
18614
- import path9 from "path";
18764
+ import { mkdir as mkdir4, readdir as readdir4, readFile as readFile8, stat as stat2, writeFile as writeFile5 } from "fs/promises";
18765
+ import path11 from "path";
18615
18766
  var SNAPSHOT_SCHEMA = "baker-canvas-snapshot/1";
18616
18767
  var MAX_SNAPSHOT_FILE_BYTES = 32 * 1024 * 1024;
18617
18768
  var EXT_TO_MIME = {
@@ -18638,11 +18789,11 @@ var EXT_TO_MIME = {
18638
18789
  woff2: "font/woff2"
18639
18790
  };
18640
18791
  function mimeForFile(filePath) {
18641
- const ext = path9.extname(filePath).slice(1).toLowerCase();
18792
+ const ext = path11.extname(filePath).slice(1).toLowerCase();
18642
18793
  return EXT_TO_MIME[ext] ?? "application/octet-stream";
18643
18794
  }
18644
18795
  function toPosix(p) {
18645
- return p.split(path9.sep).join("/");
18796
+ return p.split(path11.sep).join("/");
18646
18797
  }
18647
18798
  function localPathRefsFromCanvas(parsed) {
18648
18799
  const nodes = parsed?.nodes;
@@ -18664,32 +18815,40 @@ function localPathRefsFromCanvas(parsed) {
18664
18815
  function isSnapshotablePath(value) {
18665
18816
  return typeof value === "string" && value.length > 0 && !value.includes("[TODO");
18666
18817
  }
18818
+ async function sourceRefsToSnapshot(canvasDir, parsed) {
18819
+ const refs = new Set(localPathRefsFromCanvas(parsed));
18820
+ for (const sceneFile of await listSceneFiles(canvasDir)) {
18821
+ refs.add(toPosix(path11.relative(canvasDir, sceneFile)));
18822
+ }
18823
+ refs.add(REBUILD_FILE);
18824
+ return [...refs];
18825
+ }
18667
18826
  async function uploadRunSnapshot(client, opts) {
18668
18827
  try {
18669
- const canvasDir = path9.dirname(opts.canvasPath);
18828
+ const canvasDir = path11.dirname(opts.canvasPath);
18670
18829
  const put = (bytes, mime) => putContentAddressed(client, bytes, mime, opts.signal);
18671
18830
  const canvasBytes = Buffer.from(opts.raw);
18672
18831
  const canvasUpload = await put(canvasBytes, "application/json");
18673
18832
  const files = [];
18674
18833
  const skipped = [];
18675
- for (const refPath of localPathRefsFromCanvas(opts.parsed)) {
18676
- const abs = path9.isAbsolute(refPath) ? refPath : path9.resolve(canvasDir, refPath);
18834
+ for (const refPath of await sourceRefsToSnapshot(canvasDir, opts.parsed)) {
18835
+ const abs = path11.isAbsolute(refPath) ? refPath : path11.resolve(canvasDir, refPath);
18677
18836
  let st;
18678
18837
  try {
18679
18838
  st = await stat2(abs);
18680
18839
  } catch {
18681
- skipped.push({ path: toPosix(path9.relative(canvasDir, abs)), reason: "missing" });
18840
+ skipped.push({ path: toPosix(path11.relative(canvasDir, abs)), reason: "missing" });
18682
18841
  continue;
18683
18842
  }
18684
18843
  const fileList = st.isDirectory() ? await listFilesRecursive(abs) : [abs];
18685
18844
  for (const file of fileList) {
18686
- const rel = toPosix(path9.relative(canvasDir, file));
18845
+ const rel = toPosix(path11.relative(canvasDir, file));
18687
18846
  const size = (await stat2(file)).size;
18688
18847
  if (size > MAX_SNAPSHOT_FILE_BYTES) {
18689
18848
  skipped.push({ path: rel, reason: `too large (${size} bytes)` });
18690
18849
  continue;
18691
18850
  }
18692
- const bytes = await readFile6(file);
18851
+ const bytes = await readFile8(file);
18693
18852
  const upload = await put(bytes, mimeForFile(file));
18694
18853
  files.push({ path: rel, sha256: upload.sha256, url: upload.url });
18695
18854
  }
@@ -18698,7 +18857,7 @@ async function uploadRunSnapshot(client, opts) {
18698
18857
  schema: SNAPSHOT_SCHEMA,
18699
18858
  creativeSlug: opts.creativeSlug,
18700
18859
  canvasSha: canvasUpload.sha256,
18701
- canvas: { path: path9.basename(opts.canvasPath), sha256: canvasUpload.sha256, url: canvasUpload.url },
18860
+ canvas: { path: path11.basename(opts.canvasPath), sha256: canvasUpload.sha256, url: canvasUpload.url },
18702
18861
  files,
18703
18862
  skipped: skipped.length > 0 ? skipped : void 0
18704
18863
  };
@@ -18724,8 +18883,8 @@ async function putContentAddressed(client, bytes, mime, signal) {
18724
18883
  return { sha256, url: publicUrl };
18725
18884
  }
18726
18885
  async function listFilesRecursive(dir) {
18727
- const entries = await readdir3(dir, { recursive: true, withFileTypes: true });
18728
- return entries.filter((d) => d.isFile()).map((d) => path9.join(d.parentPath, d.name));
18886
+ const entries = await readdir4(dir, { recursive: true, withFileTypes: true });
18887
+ return entries.filter((d) => d.isFile()).map((d) => path11.join(d.parentPath, d.name));
18729
18888
  }
18730
18889
  var SnapshotConflictError = class extends Error {
18731
18890
  conflicts;
@@ -18737,19 +18896,19 @@ var SnapshotConflictError = class extends Error {
18737
18896
  };
18738
18897
  async function restoreRunSnapshot(manifest, targetDir, opts = {}) {
18739
18898
  const entries = [manifest.canvas, ...manifest.files];
18740
- const resolvedTarget = path9.resolve(targetDir);
18899
+ const resolvedTarget = path11.resolve(targetDir);
18741
18900
  const planned = [];
18742
18901
  const conflicts = [];
18743
18902
  const upToDate = [];
18744
18903
  for (const entry of entries) {
18745
- if (path9.isAbsolute(entry.path) || entry.path.split("/").includes("..")) {
18904
+ if (path11.isAbsolute(entry.path) || entry.path.split("/").includes("..")) {
18746
18905
  throw new Error(`snapshot entry escapes the creative directory: ${entry.path}`);
18747
18906
  }
18748
- const target = path9.resolve(resolvedTarget, entry.path);
18749
- if (target !== resolvedTarget && !target.startsWith(resolvedTarget + path9.sep)) {
18907
+ const target = path11.resolve(resolvedTarget, entry.path);
18908
+ if (target !== resolvedTarget && !target.startsWith(resolvedTarget + path11.sep)) {
18750
18909
  throw new Error(`snapshot entry escapes the creative directory: ${entry.path}`);
18751
18910
  }
18752
- const existing = await readFile6(target).catch(() => null);
18911
+ const existing = await readFile8(target).catch(() => null);
18753
18912
  if (existing) {
18754
18913
  if (sha256Hex(existing) === entry.sha256) {
18755
18914
  upToDate.push(entry.path);
@@ -18771,11 +18930,11 @@ async function restoreRunSnapshot(manifest, targetDir, opts = {}) {
18771
18930
  if (sha256Hex(bytes) !== entry.sha256) {
18772
18931
  throw new Error(`snapshot download for ${entry.path} does not match its recorded sha256`);
18773
18932
  }
18774
- await mkdir3(path9.dirname(target), { recursive: true });
18775
- await writeFile4(target, bytes);
18933
+ await mkdir4(path11.dirname(target), { recursive: true });
18934
+ await writeFile5(target, bytes);
18776
18935
  restored.push(entry.path);
18777
18936
  }
18778
- return { canvasPath: path9.resolve(resolvedTarget, manifest.canvas.path), restored, upToDate };
18937
+ return { canvasPath: path11.resolve(resolvedTarget, manifest.canvas.path), restored, upToDate };
18779
18938
  }
18780
18939
 
18781
18940
  // src/commands/canvas/run.ts
@@ -18855,8 +19014,8 @@ function resolveMaxCredits(...candidates) {
18855
19014
  return void 0;
18856
19015
  }
18857
19016
  async function executeCanvasRun(opts) {
18858
- const filePath = path10.resolve(opts.file);
18859
- const raw = await readFile7(filePath, "utf8");
19017
+ const filePath = path12.resolve(opts.file);
19018
+ const raw = await readFile9(filePath, "utf8");
18860
19019
  let parsed;
18861
19020
  try {
18862
19021
  parsed = JSON.parse(raw);
@@ -18868,7 +19027,7 @@ async function executeCanvasRun(opts) {
18868
19027
  }
18869
19028
  const attemptedSlug = creativeSlugFromCanvasPath(filePath);
18870
19029
  if (attemptedSlug) await clearCreativeDirty(attemptedSlug);
18871
- parsed = resolveRelativeCanvasPaths(parsed, path10.dirname(filePath));
19030
+ parsed = resolveRelativeCanvasPaths(parsed, path12.dirname(filePath));
18872
19031
  try {
18873
19032
  await syncStyleProjection(parsed, (line) => process.stderr.write(`${line}
18874
19033
  `));
@@ -18883,6 +19042,24 @@ async function executeCanvasRun(opts) {
18883
19042
  null,
18884
19043
  2
18885
19044
  )}
19045
+ `
19046
+ );
19047
+ process.exit(2);
19048
+ }
19049
+ try {
19050
+ await syncSceneNodeParams(parsed, filePath, (line) => process.stderr.write(`${line}
19051
+ `));
19052
+ } catch (e) {
19053
+ const msg = e instanceof Error ? e.message : String(e);
19054
+ process.stderr.write(
19055
+ `${JSON.stringify(
19056
+ {
19057
+ ok: false,
19058
+ error: { code: "scene_projection", message: `a scenes/*.json file is not readable/valid or its rebuild failed: ${msg}` }
19059
+ },
19060
+ null,
19061
+ 2
19062
+ )}
18886
19063
  `
18887
19064
  );
18888
19065
  process.exit(2);
@@ -18952,7 +19129,7 @@ async function executeCanvasRun(opts) {
18952
19129
  const canvasSha = sha256Hex(Buffer.from(raw));
18953
19130
  const creativeSlug = creativeSlugFromCanvasPath(filePath) ?? void 0;
18954
19131
  const client = opts.record === false ? null : buildBackendClient();
18955
- const outputsDir = opts.outputsDir ? path10.resolve(opts.outputsDir) : path10.resolve("canvas");
19132
+ const outputsDir = opts.outputsDir ? path12.resolve(opts.outputsDir) : path12.resolve("canvas");
18956
19133
  const { runId, resumed, source, concurrentRunId } = await resolveRunId({
18957
19134
  explicitRunId: opts.runId,
18958
19135
  fresh: opts.fresh === true,
@@ -18986,7 +19163,7 @@ async function executeCanvasRun(opts) {
18986
19163
  const canvasSnapshotUrl = client && creativeSlug ? await uploadRunSnapshot(client, { canvasPath: filePath, raw, creativeSlug, parsed }) ?? void 0 : void 0;
18987
19164
  const recordMeta = {
18988
19165
  creativeSlug,
18989
- canvasPath: path10.relative(process.cwd(), filePath) || void 0,
19166
+ canvasPath: path12.relative(process.cwd(), filePath) || void 0,
18990
19167
  canvasSha,
18991
19168
  // The fingerprint the dashboard compares against the current source to flag
18992
19169
  // "edited since last render". Computed from the on-disk canvas.json +
@@ -19173,7 +19350,7 @@ var rerunCommand = defineCommand90({
19173
19350
  if (latest.canvasSha && manifest.canvasSha !== latest.canvasSha) {
19174
19351
  fail("snapshot_mismatch", `snapshot manifest for run ${latest.runId} does not match its recorded canvas sha`);
19175
19352
  }
19176
- const targetDir = path11.resolve("src", "creatives", slug);
19353
+ const targetDir = path13.resolve("src", "creatives", slug);
19177
19354
  let restoredCanvasPath;
19178
19355
  try {
19179
19356
  const restore = await restoreRunSnapshot(manifest, targetDir, { force: args["force-remote"] === true });
@@ -19222,8 +19399,8 @@ async function fetchManifest(url) {
19222
19399
  }
19223
19400
 
19224
19401
  // src/commands/canvas/scaffold-static-ad.ts
19225
- import { access, mkdir as mkdir4, readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
19226
- import path15 from "path";
19402
+ import { access, mkdir as mkdir5, readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
19403
+ import path17 from "path";
19227
19404
  import { defineCommand as defineCommand92 } from "citty";
19228
19405
 
19229
19406
  // src/engine/scaffold/staticAd.ts
@@ -19467,7 +19644,7 @@ function staticAdReport(input, elementsInput, opts) {
19467
19644
  }
19468
19645
 
19469
19646
  // src/commands/canvas/creative-definition.ts
19470
- import path12 from "path";
19647
+ import path14 from "path";
19471
19648
  var PLATFORM_VALUES = ["meta", "google", "linkedin", "tiktok", "youtube", "x", "other"];
19472
19649
  var FORMAT_VALUES = ["1:1", "4:5", "9:16", "16:9", "1.91:1"];
19473
19650
  function titleFromSlug(slug) {
@@ -19509,16 +19686,16 @@ function buildCreativeDefinition(input) {
19509
19686
  }
19510
19687
 
19511
19688
  // src/commands/canvas/scaffold-static-ad-paths.ts
19512
- import path13 from "path";
19689
+ import path15 from "path";
19513
19690
  function resolveScaffoldStaticAdPaths(rawFile, out, cwd = process.cwd(), slug) {
19514
19691
  const file = rawFile.trim();
19515
19692
  const imageIsUrl = /^https?:\/\//i.test(file);
19516
- const imageSource = imageIsUrl ? file : path13.resolve(cwd, file);
19517
- const outPath = out ? path13.resolve(cwd, out) : slug ? path13.join(cwd, "src", "creatives", slug, `${slug}.canvas.json`) : imageIsUrl ? path13.join(cwd, "static-ad.canvas.json") : path13.join(path13.dirname(imageSource), "static-ad.canvas.json");
19518
- const blueprintPath = path13.join(path13.dirname(outPath), "prompt.json");
19519
- const creativeDir = slug ? path13.dirname(outPath) : null;
19520
- const definitionPath = creativeDir ? path13.join(creativeDir, "_definition.md") : null;
19521
- const referencesDir = creativeDir ? path13.join(creativeDir, "references") : null;
19693
+ const imageSource = imageIsUrl ? file : path15.resolve(cwd, file);
19694
+ const outPath = out ? path15.resolve(cwd, out) : slug ? path15.join(cwd, "src", "creatives", slug, `${slug}.canvas.json`) : imageIsUrl ? path15.join(cwd, "static-ad.canvas.json") : path15.join(path15.dirname(imageSource), "static-ad.canvas.json");
19695
+ const blueprintPath = path15.join(path15.dirname(outPath), "prompt.json");
19696
+ const creativeDir = slug ? path15.dirname(outPath) : null;
19697
+ const definitionPath = creativeDir ? path15.join(creativeDir, "_definition.md") : null;
19698
+ const referencesDir = creativeDir ? path15.join(creativeDir, "references") : null;
19522
19699
  return { imageIsUrl, imageSource, outPath, blueprintPath, creativeDir, definitionPath, referencesDir };
19523
19700
  }
19524
19701
  var SCAFFOLD_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
@@ -19528,8 +19705,8 @@ function isValidScaffoldSlug(slug) {
19528
19705
  }
19529
19706
 
19530
19707
  // src/commands/canvas/sync-definition.ts
19531
- import { readdir as readdir4, readFile as readFile8, stat as stat3 } from "fs/promises";
19532
- import path14 from "path";
19708
+ import { readdir as readdir5, readFile as readFile10, stat as stat3 } from "fs/promises";
19709
+ import path16 from "path";
19533
19710
  import { defineCommand as defineCommand91 } from "citty";
19534
19711
 
19535
19712
  // src/commands/canvas/definition-graph.ts
@@ -19622,26 +19799,26 @@ async function syncCreativeDefinitionBestEffort(input) {
19622
19799
  }
19623
19800
  }
19624
19801
  async function resolveCanvasPath(inputPath) {
19625
- const resolved = path14.resolve(inputPath);
19802
+ const resolved = path16.resolve(inputPath);
19626
19803
  let dir = resolved;
19627
19804
  try {
19628
19805
  if ((await stat3(resolved)).isFile()) {
19629
19806
  if (resolved.endsWith(".canvas.json")) return resolved;
19630
- dir = path14.dirname(resolved);
19807
+ dir = path16.dirname(resolved);
19631
19808
  }
19632
19809
  } catch {
19633
- dir = resolved.endsWith(".canvas.json") ? path14.dirname(resolved) : resolved;
19810
+ dir = resolved.endsWith(".canvas.json") ? path16.dirname(resolved) : resolved;
19634
19811
  }
19635
19812
  let entries;
19636
19813
  try {
19637
- entries = await readdir4(dir);
19814
+ entries = await readdir5(dir);
19638
19815
  } catch {
19639
19816
  return null;
19640
19817
  }
19641
19818
  const canvases = entries.filter((name) => name.endsWith(".canvas.json"));
19642
19819
  const slug = creativeSlugFromCanvasPath(`${dir}/x/`);
19643
19820
  const chosen = (slug ? canvases.find((name) => name === `${slug}.canvas.json`) : void 0) ?? canvases[0];
19644
- return chosen ? path14.join(dir, chosen) : null;
19821
+ return chosen ? path16.join(dir, chosen) : null;
19645
19822
  }
19646
19823
  var syncDefinitionCommand = defineCommand91({
19647
19824
  meta: {
@@ -19664,7 +19841,7 @@ var syncDefinitionCommand = defineCommand91({
19664
19841
  if (!slug) return;
19665
19842
  let canvas;
19666
19843
  try {
19667
- canvas = JSON.parse(await readFile8(canvasPath, "utf8"));
19844
+ canvas = JSON.parse(await readFile10(canvasPath, "utf8"));
19668
19845
  } catch {
19669
19846
  return;
19670
19847
  }
@@ -19689,7 +19866,7 @@ async function uploadSourceAsReference(source, isUrl, client) {
19689
19866
  if (!res.ok) throw new Error(`failed to download source image (${res.status})`);
19690
19867
  bytes = Buffer.from(await res.arrayBuffer());
19691
19868
  } else {
19692
- bytes = await readFile9(source);
19869
+ bytes = await readFile11(source);
19693
19870
  }
19694
19871
  const safe = await toModelSafeImage(bytes);
19695
19872
  const sha256 = sha256Hex(safe.bytes);
@@ -19744,7 +19921,7 @@ DROP background extras, decorative props, generic scenery, and anything small or
19744
19921
  For each kept element return: { "type": one of logo|product|person|animal|badge, "label": a short UPPER_SNAKE_CASE name (e.g. LOGO, PRODUCT, HERO_DOG, TRUSTPILOT), "description": a concrete reusable description to source/shoot the real asset (include the exact expression for a living subject, and its castable attributes \u2014 breed/species for an animal, apparent age band, apparent origin/ethnicity, and wardrobe/setting for a person \u2014 so it can be recast to fit OUR audience/market), "expression": the facial expression for a living subject or null, "reason": why it is identity-critical, "locator": the blueprint entry this element came from as { "collection": one of "subjects" | "people" | "brands_logos", "index": its 0-based position in that array } (people -> people; logos/badges -> brands_logos; products/animals/objects -> subjects). Output ONLY the JSON object.`;
19745
19922
  async function loadAssetText(ref, label) {
19746
19923
  const r = ref;
19747
- if (typeof r?.path === "string") return readFile9(r.path, "utf8");
19924
+ if (typeof r?.path === "string") return readFile11(r.path, "utf8");
19748
19925
  if (typeof r?.url === "string") {
19749
19926
  const res = await fetch(r.url);
19750
19927
  if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
@@ -19897,7 +20074,7 @@ var scaffoldStaticAdCommand = defineCommand92({
19897
20074
  process.cwd(),
19898
20075
  slug
19899
20076
  );
19900
- await mkdir4(path15.dirname(outPath), { recursive: true });
20077
+ await mkdir5(path17.dirname(outPath), { recursive: true });
19901
20078
  const { describeModel, selectModel, layoutModel, genModel } = resolveModels(args);
19902
20079
  let durableSourceUrl;
19903
20080
  if (referencesDir) {
@@ -19923,7 +20100,7 @@ var scaffoldStaticAdCommand = defineCommand92({
19923
20100
  if (layout && annotated && typeof annotated === "object") {
19924
20101
  annotated.layout = layout;
19925
20102
  }
19926
- await writeFile5(blueprintPath, `${JSON.stringify(annotated, null, 2)}
20103
+ await writeFile6(blueprintPath, `${JSON.stringify(annotated, null, 2)}
19927
20104
  `, "utf8");
19928
20105
  let canvasImagePath = imageSource;
19929
20106
  let canvasImageIsUrl = imageIsUrl;
@@ -19960,10 +20137,10 @@ var scaffoldStaticAdCommand = defineCommand92({
19960
20137
  );
19961
20138
  process.exit(2);
19962
20139
  }
19963
- await writeFile5(outPath, `${JSON.stringify(canvas, null, 2)}
20140
+ await writeFile6(outPath, `${JSON.stringify(canvas, null, 2)}
19964
20141
  `, "utf8");
19965
20142
  if (definitionPath && !await fileExists(definitionPath)) {
19966
- await writeFile5(
20143
+ await writeFile6(
19967
20144
  definitionPath,
19968
20145
  buildCreativeDefinition({
19969
20146
  title: args.title ? String(args.title) : titleFromSlug(slug ?? ""),
@@ -20007,7 +20184,7 @@ var scaffoldStaticAdCommand = defineCommand92({
20007
20184
  run_estimated_credits: validation.estimatedCredits
20008
20185
  },
20009
20186
  checklist: {
20010
- edit_prompt: `Edit ${path15.basename(blueprintPath)} \u2014 it is the blueprint generated from your image; rewrite it into the ad you want (palette, copy, claims, subjects). It feeds the generator directly.`,
20187
+ edit_prompt: `Edit ${path17.basename(blueprintPath)} \u2014 it is the blueprint generated from your image; rewrite it into the ad you want (palette, copy, claims, subjects). It feeds the generator directly.`,
20011
20188
  assets_to_supply: report.elements,
20012
20189
  font_slot: report.includes_font ? "Drop a brand font at the [TODO] brandfont path (the describe pass recorded the ad's typefaces under `fonts` in prompt.json \u2014 match those). The font is wired into the render as a TYPE SPECIMEN reference so generated text takes the brand letterforms. Delete the brandfont + type_ref nodes to skip it." : "skipped (--skip-font)",
20013
20190
  actor_sheets: report.actor_sheets.length > 0 ? `Each living hero (${report.actor_sheets.join(", ")}) is fused into a generated multi-view reference sheet (image_reference_sheet) that the render grounds on \u2014 so drop ONE clean photo at that hero's ingest and the sheet builds the consistent turnaround. Pass --skip-actor-sheets to ground on the lone photo instead.` : "none (no person/animal heroes detected, or --skip-actor-sheets)",
@@ -20023,9 +20200,9 @@ var scaffoldStaticAdCommand = defineCommand92({
20023
20200
  });
20024
20201
 
20025
20202
  // src/commands/canvas/scaffold-video.ts
20026
- import { access as access2, cp, mkdir as mkdir5, readFile as readFile12, rm as rm5, writeFile as writeFile6 } from "fs/promises";
20203
+ import { access as access2, cp, mkdir as mkdir6, readFile as readFile14, rm as rm6, writeFile as writeFile7 } from "fs/promises";
20027
20204
  import { tmpdir as tmpdir2 } from "os";
20028
- import path18 from "path";
20205
+ import path20 from "path";
20029
20206
  import { defineCommand as defineCommand93 } from "citty";
20030
20207
 
20031
20208
  // src/engine/scaffold/lib/model-router.ts
@@ -20064,7 +20241,7 @@ function routeVideoModel(input) {
20064
20241
 
20065
20242
  // src/engine/nodes/local/lib/sceneDetect.ts
20066
20243
  import { execFile as execFile2 } from "child_process";
20067
- import { mkdtemp, readdir as readdir5, readFile as readFile10, rm as rm4 } from "fs/promises";
20244
+ import { mkdtemp, readdir as readdir6, readFile as readFile12, rm as rm5 } from "fs/promises";
20068
20245
  import { tmpdir } from "os";
20069
20246
  import { join as join2 } from "path";
20070
20247
  import { promisify as promisify2 } from "util";
@@ -20138,11 +20315,11 @@ async function runSceneDetectOnce(filePath, threshold, minSceneLenS, timeoutMs)
20138
20315
  ],
20139
20316
  { encoding: "utf-8", maxBuffer: 32 * 1024 * 1024, timeout: timeoutMs }
20140
20317
  );
20141
- const csvName = (await readdir5(outDir)).find((f) => f.toLowerCase().endsWith(".csv"));
20318
+ const csvName = (await readdir6(outDir)).find((f) => f.toLowerCase().endsWith(".csv"));
20142
20319
  if (!csvName) return [];
20143
- return parsePySceneDetectCsvCuts(await readFile10(join2(outDir, csvName), "utf-8"));
20320
+ return parsePySceneDetectCsvCuts(await readFile12(join2(outDir, csvName), "utf-8"));
20144
20321
  } finally {
20145
- await rm4(outDir, { recursive: true, force: true });
20322
+ await rm5(outDir, { recursive: true, force: true });
20146
20323
  }
20147
20324
  }
20148
20325
  async function detectSceneCutsPySceneDetect(filePath, opts = {}) {
@@ -20165,23 +20342,23 @@ async function detectSceneCutsPySceneDetect(filePath, opts = {}) {
20165
20342
 
20166
20343
  // src/commands/canvas/composition-path.ts
20167
20344
  import { existsSync as existsSync3 } from "fs";
20168
- import path16 from "path";
20345
+ import path18 from "path";
20169
20346
  function resolveShippedCanvasDir(name, startDir, exists = existsSync3, maxDepth = 8) {
20170
- const rel = path16.join("canvas", name);
20347
+ const rel = path18.join("canvas", name);
20171
20348
  let dir = startDir;
20172
20349
  for (let i = 0; i < maxDepth; i++) {
20173
- const candidate = path16.join(dir, rel);
20174
- if (exists(path16.join(candidate, "meta.json"))) return candidate;
20175
- const parent = path16.dirname(dir);
20350
+ const candidate = path18.join(dir, rel);
20351
+ if (exists(path18.join(candidate, "meta.json"))) return candidate;
20352
+ const parent = path18.dirname(dir);
20176
20353
  if (parent === dir) break;
20177
20354
  dir = parent;
20178
20355
  }
20179
- return path16.resolve(startDir, "../../../", rel);
20356
+ return path18.resolve(startDir, "../../../", rel);
20180
20357
  }
20181
20358
 
20182
20359
  // src/commands/canvas/gitignore.ts
20183
- import { appendFile, readFile as readFile11 } from "fs/promises";
20184
- import path17 from "path";
20360
+ import { appendFile, readFile as readFile13 } from "fs/promises";
20361
+ import path19 from "path";
20185
20362
  function missingGitignoreEntries(existing, entries) {
20186
20363
  const present = new Set(
20187
20364
  existing.split("\n").map((l) => l.trim().replace(/\/+$/, "")).filter((l) => l.length > 0 && !l.startsWith("#"))
@@ -20189,10 +20366,10 @@ function missingGitignoreEntries(existing, entries) {
20189
20366
  return entries.filter((e) => !present.has(e.trim().replace(/\/+$/, "")));
20190
20367
  }
20191
20368
  async function ensureGitignore(dir, entries) {
20192
- const file = path17.join(dir, ".gitignore");
20369
+ const file = path19.join(dir, ".gitignore");
20193
20370
  let existing;
20194
20371
  try {
20195
- existing = await readFile11(file, "utf8");
20372
+ existing = await readFile13(file, "utf8");
20196
20373
  } catch {
20197
20374
  return;
20198
20375
  }
@@ -20231,7 +20408,7 @@ ONE PERSON, MULTIPLE LOOKS: if a single individual plays MULTIPLE personas or wa
20231
20408
  For each kept element return: { "type": one of person|animal|product|logo|badge|location, "label": a short UPPER_SNAKE_CASE name (e.g. HERO, CREATOR_SKEPTIC, INSURANCE_CARD, LOGO), "description": a concrete reusable description to source/shoot the real asset \u2014 for a person/animal give a NEUTRAL castable role (e.g. "hero pet-owner, woman in her 30s" or "a small beagle"), NOT the original individual's literal face/identity: we RECAST with a FRESH person/animal, so never tell the agent to reuse the original. "expression": a living subject's typical expression or null, "cast_id": the global.cast id if it maps to one else null, "same_as": the label of another element this is the SAME individual as (different wardrobe/persona) else null, "scenes": the 0-based indices of ONLY the scenes where the element is ACTUALLY VISIBLE ON SCREEN \u2014 judged from that scene's start_frame_prompt / end_frame_prompt subjects and its action_detail, NOT from who is merely speaking. A narrator heard over b-roll is NOT present in that b-roll scene; a dog-running cutaway does NOT contain the couch creator just because she talks across it. Do NOT pad the list \u2014 an element wrongly listed in a scene makes the reproduction render the wrong subject there (e.g. the creator appearing in a pure-dog b-roll). When in doubt, leave a scene OUT. Output ONLY the JSON object.`;
20232
20409
  async function loadAssetText2(ref, label) {
20233
20410
  const r = ref;
20234
- if (typeof r?.path === "string") return readFile12(r.path, "utf8");
20411
+ if (typeof r?.path === "string") return readFile14(r.path, "utf8");
20235
20412
  if (typeof r?.url === "string") {
20236
20413
  const res = await fetch(r.url);
20237
20414
  if (!res.ok) throw new Error(`failed to fetch ${label} (${res.status})`);
@@ -20250,7 +20427,7 @@ async function loadTranscriptBestEffort(ref) {
20250
20427
  async function stageCaptions(outDir, transcript) {
20251
20428
  const text = transcript?.trim();
20252
20429
  if (!text || text === "[]") return {};
20253
- const compositionPath = path18.join(outDir, "tiktok-captions-composition");
20430
+ const compositionPath = path20.join(outDir, "tiktok-captions-composition");
20254
20431
  await cp(SHIPPED_CAPTIONS_DIR, compositionPath, { recursive: true });
20255
20432
  return { compositionPath };
20256
20433
  }
@@ -20268,12 +20445,12 @@ function patchCompositionHtml(html, dims) {
20268
20445
  return html.replace(/(<meta\s+name="viewport"\s+content="width=)\d+(,\s*height=)\d+(")/i, `$1${dims.w}$2${dims.h}$3`).replace(/(width:\s*)\d+(px;\s*height:\s*)\d+(px;)/i, `$1${dims.w}$2${dims.h}$3`).replace(/(data-width=")\d+(")/i, `$1${dims.w}$2`).replace(/(data-height=")\d+(")/i, `$1${dims.h}$2`);
20269
20446
  }
20270
20447
  async function stampCompositionDims(compositionDir, dims) {
20271
- const metaPath = path18.join(compositionDir, "meta.json");
20272
- const rawMeta = await readFile12(metaPath, "utf8");
20273
- await writeFile6(metaPath, patchCompositionMeta(rawMeta, dims), "utf8");
20274
- const htmlPath = path18.join(compositionDir, "index.html");
20275
- const rawHtml = await readFile12(htmlPath, "utf8");
20276
- await writeFile6(htmlPath, patchCompositionHtml(rawHtml, dims), "utf8");
20448
+ const metaPath = path20.join(compositionDir, "meta.json");
20449
+ const rawMeta = await readFile14(metaPath, "utf8");
20450
+ await writeFile7(metaPath, patchCompositionMeta(rawMeta, dims), "utf8");
20451
+ const htmlPath = path20.join(compositionDir, "index.html");
20452
+ const rawHtml = await readFile14(htmlPath, "utf8");
20453
+ await writeFile7(htmlPath, patchCompositionHtml(rawHtml, dims), "utf8");
20277
20454
  }
20278
20455
  function parseElements2(raw) {
20279
20456
  const parsed = JSON.parse(raw);
@@ -20320,7 +20497,7 @@ var VIDEO_EXT_BY_MIME = {
20320
20497
  "video/x-matroska": ".mkv"
20321
20498
  };
20322
20499
  function referenceVideoExt(url, contentType) {
20323
- const fromPath = path18.extname(new URL(url).pathname).toLowerCase();
20500
+ const fromPath = path20.extname(new URL(url).pathname).toLowerCase();
20324
20501
  if (fromPath && fromPath.length <= 5) return fromPath;
20325
20502
  const mime = (contentType ?? "").split(";")[0]?.trim().toLowerCase();
20326
20503
  return mime && VIDEO_EXT_BY_MIME[mime] || ".mp4";
@@ -20346,7 +20523,7 @@ function videoDefinitionDescription(blueprint) {
20346
20523
  return typeof product === "string" && product.trim() ? product.trim() : void 0;
20347
20524
  }
20348
20525
  async function materializeReferenceVideo(fileArg2) {
20349
- if (!/^https?:\/\//i.test(fileArg2)) return path18.resolve(fileArg2);
20526
+ if (!/^https?:\/\//i.test(fileArg2)) return path20.resolve(fileArg2);
20350
20527
  let res;
20351
20528
  try {
20352
20529
  res = await fetch(fileArg2);
@@ -20356,11 +20533,11 @@ async function materializeReferenceVideo(fileArg2) {
20356
20533
  if (!res.ok) throw new Error(`failed to download reference video (${res.status} ${res.statusText})`);
20357
20534
  const bytes = Buffer.from(await res.arrayBuffer());
20358
20535
  if (bytes.length === 0) throw new Error("reference video download was empty");
20359
- const dest = path18.join(
20536
+ const dest = path20.join(
20360
20537
  tmpdir2(),
20361
20538
  `baker-ref-${sha256Hex(bytes).slice(0, 16)}${referenceVideoExt(fileArg2, res.headers.get("content-type"))}`
20362
20539
  );
20363
- await writeFile6(dest, bytes);
20540
+ await writeFile7(dest, bytes);
20364
20541
  return dest;
20365
20542
  }
20366
20543
  function resolveSeamDedup(raw) {
@@ -20483,7 +20660,7 @@ async function runAnalysisPasses(deconstructCanvas, selectModel) {
20483
20660
  var scaffoldVideoCommand = defineCommand93({
20484
20661
  meta: {
20485
20662
  name: "scaffold-video",
20486
- description: "Turn a reference video into a runnable reproduction canvas in one command. Runs billed passes \u2014 video_deconstruct (the full scene-by-scene blueprint + transcript, baked to prompt.json as the editable 'prompt') and an AI selection of the video's RECURRING identity elements (person/animal/product/logo) \u2014 then scaffolds a pipeline where every scene boundary is a static-ad-grade frame (the blueprint as target_blueprint, a reference legend, the real frame as anchor) and each recurring element gets ONE shared [TODO] ingest slot wired into every frame it appears in. The clips feed Seedance an ultra-detailed motion brief (action, camera, dialogue, transcript). Edit prompt.json, drop the real source images, then `baker canvas run`."
20663
+ description: "Turn a reference video into a runnable reproduction canvas in one command. Runs billed passes \u2014 video_deconstruct (the full scene-by-scene blueprint + transcript, baked to a split, editable blueprint: a global prompt.json plus one small scenes/sNN.json per scene) and an AI selection of the video's RECURRING identity elements (person/animal/product/logo) \u2014 then scaffolds a pipeline where every scene boundary is a static-ad-grade frame (the blueprint as target_blueprint, a reference legend, the real frame as anchor) and each recurring element gets ONE shared [TODO] ingest slot wired into every frame it appears in. The clips feed Seedance an ultra-detailed motion brief (action, camera, dialogue, transcript). Edit a scene's scenes/sNN.json (or prompt.json for global cast/palette/brand), drop the real source images, then `baker canvas run`."
20487
20664
  },
20488
20665
  args: {
20489
20666
  file: {
@@ -20572,11 +20749,11 @@ var scaffoldVideoCommand = defineCommand93({
20572
20749
  } catch (e) {
20573
20750
  return fail3("download", e instanceof Error ? e.message : String(e));
20574
20751
  }
20575
- const base = path18.basename(videoPath, path18.extname(videoPath));
20576
- const outPath = args.out ? path18.resolve(String(args.out)) : slug ? path18.join(process.cwd(), "src", "creatives", slug, `${slug}.canvas.json`) : path18.join(path18.dirname(videoPath), `${base}.video.canvas.json`);
20577
- const outDir = path18.dirname(outPath);
20578
- const blueprintPath = path18.join(outDir, "prompt.json");
20579
- const blueprintStylePath = path18.join(outDir, "prompt.style.json");
20752
+ const base = path20.basename(videoPath, path20.extname(videoPath));
20753
+ const outPath = args.out ? path20.resolve(String(args.out)) : slug ? path20.join(process.cwd(), "src", "creatives", slug, `${slug}.canvas.json`) : path20.join(path20.dirname(videoPath), `${base}.video.canvas.json`);
20754
+ const outDir = path20.dirname(outPath);
20755
+ const blueprintPath = path20.join(outDir, "prompt.json");
20756
+ const blueprintStylePath = path20.join(outDir, "prompt.style.json");
20580
20757
  const frames = args.frames === "reuse" ? "reuse" : "generate";
20581
20758
  const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
20582
20759
  if (Number.isFinite(maxScenes)) {
@@ -20595,11 +20772,10 @@ var scaffoldVideoCommand = defineCommand93({
20595
20772
  shotCuts
20596
20773
  });
20597
20774
  const { blueprint, elements, transcript, creditsSpent } = await runAnalysisPasses(deconstructCanvas, selectModel);
20598
- await mkdir5(outDir, { recursive: true });
20775
+ await mkdir6(outDir, { recursive: true });
20599
20776
  const annotated = annotateBlueprintWithElements(blueprint, elements);
20600
- await writeFile6(blueprintPath, `${JSON.stringify(annotated, null, 2)}
20601
- `, "utf8");
20602
- await writeFile6(blueprintStylePath, renderStyleProjectionFromValue(annotated), "utf8");
20777
+ await writeSceneFiles(outDir, annotated);
20778
+ await writeFile7(blueprintStylePath, renderStyleProjectionFromValue(annotated), "utf8");
20603
20779
  let aspect;
20604
20780
  try {
20605
20781
  aspect = resolveAspect(
@@ -20617,12 +20793,12 @@ var scaffoldVideoCommand = defineCommand93({
20617
20793
  `
20618
20794
  );
20619
20795
  }
20620
- const compositionDest = path18.join(outDir, "video-overlay-composition");
20796
+ const compositionDest = path20.join(outDir, "video-overlay-composition");
20621
20797
  await cp(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
20622
20798
  await stampCompositionDims(compositionDest, outDims);
20623
- const indexPath = path18.join(compositionDest, "index.html");
20799
+ const indexPath = path20.join(compositionDest, "index.html");
20624
20800
  const overlayHtml = buildOverlayHtml(blueprint, { captionsActive: Boolean(transcript) });
20625
- const indexHtml = await readFile12(indexPath, "utf8");
20801
+ const indexHtml = await readFile14(indexPath, "utf8");
20626
20802
  const injected = indexHtml.replace("<!--OVERLAYS-->", () => overlayHtml);
20627
20803
  if (injected === indexHtml && overlayHtml.trim()) {
20628
20804
  fail3(
@@ -20630,16 +20806,16 @@ var scaffoldVideoCommand = defineCommand93({
20630
20806
  `video-overlay-composition/index.html is missing the <!--OVERLAYS--> marker \u2014 cannot inject the overlay layer`
20631
20807
  );
20632
20808
  }
20633
- await writeFile6(indexPath, injected, "utf8");
20809
+ await writeFile7(indexPath, injected, "utf8");
20634
20810
  const captions = await stageCaptions(outDir, transcript);
20635
20811
  if (captions.compositionPath) await stampCompositionDims(captions.compositionPath, outDims);
20636
20812
  const opts = {
20637
20813
  imageModel,
20638
20814
  videoModel,
20639
- overlayCompositionPath: path18.relative(outDir, compositionDest),
20640
- captionsCompositionPath: captions.compositionPath ? path18.relative(outDir, captions.compositionPath) : void 0,
20641
- blueprintPath: path18.relative(outDir, blueprintPath),
20642
- blueprintStylePath: path18.relative(outDir, blueprintStylePath),
20815
+ overlayCompositionPath: path20.relative(outDir, compositionDest),
20816
+ captionsCompositionPath: captions.compositionPath ? path20.relative(outDir, captions.compositionPath) : void 0,
20817
+ blueprintPath: path20.relative(outDir, blueprintPath),
20818
+ blueprintStylePath: path20.relative(outDir, blueprintStylePath),
20643
20819
  frames,
20644
20820
  ambient: Boolean(args.ambient),
20645
20821
  seamDedup: resolveSeamDedup(args["seam-dedup"]),
@@ -20655,7 +20831,7 @@ var scaffoldVideoCommand = defineCommand93({
20655
20831
  return fail3("scaffold", e instanceof Error ? e.message : String(e));
20656
20832
  }
20657
20833
  if (captions.compositionPath && !canvas.nodes.some((n) => n.id === "captions")) {
20658
- await rm5(captions.compositionPath, { recursive: true, force: true });
20834
+ await rm6(captions.compositionPath, { recursive: true, force: true });
20659
20835
  }
20660
20836
  const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(canvas, outDir), defaultRegistry());
20661
20837
  if (!validation.ok) {
@@ -20664,8 +20840,14 @@ var scaffoldVideoCommand = defineCommand93({
20664
20840
  todo.blocking_validation_issues = validation.issues;
20665
20841
  meta.todo = todo;
20666
20842
  }
20667
- await writeFile6(outPath, `${JSON.stringify(canvas, null, 2)}
20843
+ await writeFile7(outPath, `${JSON.stringify(canvas, null, 2)}
20668
20844
  `, "utf8");
20845
+ await writeFile7(
20846
+ path20.join(outDir, REBUILD_FILE),
20847
+ `${JSON.stringify({ elements, opts }, null, 2)}
20848
+ `,
20849
+ "utf8"
20850
+ );
20669
20851
  if (!validation.ok) {
20670
20852
  process.stderr.write(
20671
20853
  `${JSON.stringify(
@@ -20686,9 +20868,9 @@ var scaffoldVideoCommand = defineCommand93({
20686
20868
  await ensureGitignore(process.cwd(), ["canvas/", ".context/"]);
20687
20869
  const sourceRef = videoSourceReference(blueprint, fileArg2);
20688
20870
  if (slug) {
20689
- const definitionPath = path18.join(outDir, "_definition.md");
20871
+ const definitionPath = path20.join(outDir, "_definition.md");
20690
20872
  if (!await fileExists2(definitionPath)) {
20691
- await writeFile6(
20873
+ await writeFile7(
20692
20874
  definitionPath,
20693
20875
  buildCreativeDefinition({
20694
20876
  title: titleFromSlug(slug),
@@ -20741,7 +20923,7 @@ var scaffoldVideoCommand = defineCommand93({
20741
20923
  graph: canvas.metadata?.video?.graph_stats
20742
20924
  },
20743
20925
  checklist: {
20744
- edit_prompt: `Edit ${path18.basename(blueprintPath)} \u2014 the blueprint deconstructed from your video; rewrite it into the ad you want (cast, palette, copy, claims). Global cast/palette/brand edits flow into every frame automatically: \`baker canvas validate\`/\`run\` regenerate ${path18.basename(blueprintStylePath)} (the projection each frame's target_blueprint reads) from it \u2014 never edit that derived file by hand. Per-scene content is edited on each frame node's own prompt.`,
20926
+ edit_prompt: `The blueprint is split so you edit ONE small file at a time \u2014 never a giant one. Per-scene content (a scene's dialogue, action, frame prompts, overlays) lives in \`scenes/sNN.json\` \u2014 edit the single scene you want to change. Global cast/palette/brand/copy lives in \`${path20.basename(blueprintPath)}\`. \`baker canvas validate\`/\`run\` re-assemble the blueprint and re-flow every edited scene back into the render (and regenerate ${path20.basename(blueprintStylePath)}, the projection each frame's target_blueprint reads) \u2014 so your scene edits reach the render automatically. Never hand-edit the inlined node prompts in the canvas or the derived ${path20.basename(blueprintStylePath)}; both are regenerated.`,
20745
20927
  recurring_elements_to_supply: report.elements,
20746
20928
  voices_to_confirm: report.dialogue.map((d) => ({
20747
20929
  scene: d.scene,
@@ -20778,8 +20960,8 @@ var scaffoldVideoCommand = defineCommand93({
20778
20960
  });
20779
20961
 
20780
20962
  // src/commands/canvas/set-prompt.ts
20781
- import { readFile as readFile13, writeFile as writeFile7 } from "fs/promises";
20782
- import path19 from "path";
20963
+ import { readFile as readFile15, writeFile as writeFile8 } from "fs/promises";
20964
+ import path21 from "path";
20783
20965
  import { defineCommand as defineCommand94 } from "citty";
20784
20966
  function setNodePrompt(canvas, nodeId, text) {
20785
20967
  const nodes = canvas?.nodes;
@@ -20807,17 +20989,17 @@ var setPromptCommand = defineCommand94({
20807
20989
  "text-file": { type: "string", description: "Read the new prompt from a UTF-8 file (preserves accents/newlines)" }
20808
20990
  },
20809
20991
  async run({ args }) {
20810
- const filePath = path19.resolve(String(args.file));
20992
+ const filePath = path21.resolve(String(args.file));
20811
20993
  let canvas;
20812
20994
  try {
20813
- canvas = JSON.parse(await readFile13(filePath, "utf8"));
20995
+ canvas = JSON.parse(await readFile15(filePath, "utf8"));
20814
20996
  } catch (e) {
20815
20997
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "parse", message: String(e) } }, null, 2)}
20816
20998
  `);
20817
20999
  process.exit(2);
20818
21000
  }
20819
21001
  let text;
20820
- if (args["text-file"]) text = await readFile13(path19.resolve(String(args["text-file"])), "utf8");
21002
+ if (args["text-file"]) text = await readFile15(path21.resolve(String(args["text-file"])), "utf8");
20821
21003
  else if (args.text !== void 0) text = String(args.text);
20822
21004
  else {
20823
21005
  process.stderr.write(
@@ -20838,14 +21020,14 @@ var setPromptCommand = defineCommand94({
20838
21020
  process.exit(2);
20839
21021
  return;
20840
21022
  }
20841
- const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path19.dirname(filePath)), defaultRegistry());
21023
+ const validation = await validateCanvasDeep(resolveRelativeCanvasPaths(updated, path21.dirname(filePath)), defaultRegistry());
20842
21024
  if (!validation.ok) {
20843
21025
  process.stderr.write(`${JSON.stringify({ ok: false, error: { code: "validation", issues: validation.issues } }, null, 2)}
20844
21026
  `);
20845
21027
  process.exit(2);
20846
21028
  return;
20847
21029
  }
20848
- await writeFile7(filePath, `${JSON.stringify(updated, null, 2)}
21030
+ await writeFile8(filePath, `${JSON.stringify(updated, null, 2)}
20849
21031
  `, "utf8");
20850
21032
  process.stdout.write(`${JSON.stringify({ ok: true, node: String(args.node), bytes: text.length }, null, 2)}
20851
21033
  `);
@@ -20853,8 +21035,8 @@ var setPromptCommand = defineCommand94({
20853
21035
  });
20854
21036
 
20855
21037
  // src/commands/canvas/validate.ts
20856
- import { readFile as readFile14 } from "fs/promises";
20857
- import path20 from "path";
21038
+ import { readFile as readFile16 } from "fs/promises";
21039
+ import path22 from "path";
20858
21040
  import { defineCommand as defineCommand95 } from "citty";
20859
21041
  var validateCommand = defineCommand95({
20860
21042
  meta: {
@@ -20863,8 +21045,8 @@ var validateCommand = defineCommand95({
20863
21045
  },
20864
21046
  args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
20865
21047
  async run({ args }) {
20866
- const filePath = path20.resolve(String(args.file));
20867
- const raw = await readFile14(filePath, "utf8");
21048
+ const filePath = path22.resolve(String(args.file));
21049
+ const raw = await readFile16(filePath, "utf8");
20868
21050
  let parsed;
20869
21051
  try {
20870
21052
  parsed = JSON.parse(raw);
@@ -20874,7 +21056,7 @@ var validateCommand = defineCommand95({
20874
21056
  `);
20875
21057
  process.exit(2);
20876
21058
  }
20877
- parsed = resolveRelativeCanvasPaths(parsed, path20.dirname(filePath));
21059
+ parsed = resolveRelativeCanvasPaths(parsed, path22.dirname(filePath));
20878
21060
  let styleProjection = "not_applicable";
20879
21061
  try {
20880
21062
  styleProjection = await syncStyleProjection(parsed, (line) => process.stderr.write(`${line}
@@ -20883,6 +21065,18 @@ var validateCommand = defineCommand95({
20883
21065
  const msg = e instanceof Error ? e.message : String(e);
20884
21066
  process.stderr.write(
20885
21067
  `${JSON.stringify({ ok: false, error: { code: "style_projection", message: `prompt.json is not readable/valid JSON: ${msg}` } }, null, 2)}
21068
+ `
21069
+ );
21070
+ process.exit(2);
21071
+ }
21072
+ let sceneProjection = "not_applicable";
21073
+ try {
21074
+ sceneProjection = await syncSceneNodeParams(parsed, filePath, (line) => process.stderr.write(`${line}
21075
+ `));
21076
+ } catch (e) {
21077
+ const msg = e instanceof Error ? e.message : String(e);
21078
+ process.stderr.write(
21079
+ `${JSON.stringify({ ok: false, error: { code: "scene_projection", message: `a scenes/*.json file is not readable/valid or its rebuild failed: ${msg}` } }, null, 2)}
20886
21080
  `
20887
21081
  );
20888
21082
  process.exit(2);
@@ -20902,6 +21096,7 @@ var validateCommand = defineCommand95({
20902
21096
  estimated_credits: result.estimatedCredits,
20903
21097
  cost_preview: result.perNodeCredits ?? [],
20904
21098
  style_projection: styleProjection,
21099
+ scene_projection: sceneProjection,
20905
21100
  warnings: result.warnings ?? []
20906
21101
  },
20907
21102
  null,
@@ -20952,7 +21147,7 @@ import { defineCommand as defineCommand98 } from "citty";
20952
21147
  import { defineCommand as defineCommand97 } from "citty";
20953
21148
 
20954
21149
  // src/commands/images/api.ts
20955
- import { readFile as readFile15 } from "fs/promises";
21150
+ import { readFile as readFile17 } from "fs/promises";
20956
21151
  import { extname } from "path";
20957
21152
  var imageProcessingTimeoutMs = 18e4;
20958
21153
  var imageReadyPollIntervalMs = 2e3;
@@ -20966,7 +21161,7 @@ var mimeMap = {
20966
21161
  ".avif": "image/avif"
20967
21162
  };
20968
21163
  var defaultImageApiDeps = {
20969
- readFile: readFile15,
21164
+ readFile: readFile17,
20970
21165
  post: apiPost,
20971
21166
  get: apiGet,
20972
21167
  sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
@@ -21200,12 +21395,12 @@ function listFlowSlugs() {
21200
21395
  return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_") && entry.name !== ".gitkeep").map((entry) => entry.name).sort();
21201
21396
  }
21202
21397
  function readFlowTree(slug) {
21203
- const path21 = join3(flowsDir(), slug, "_data.json");
21204
- if (!existsSync4(path21)) {
21398
+ const path23 = join3(flowsDir(), slug, "_data.json");
21399
+ if (!existsSync4(path23)) {
21205
21400
  failLocal(`No form "${slug}". Run "baker flows list" to see the forms in this workspace.`);
21206
21401
  }
21207
21402
  try {
21208
- return JSON.parse(readFileSync9(path21, "utf-8"));
21403
+ return JSON.parse(readFileSync9(path23, "utf-8"));
21209
21404
  } catch (error) {
21210
21405
  failLocal(`Could not read form "${slug}": ${error instanceof Error ? error.message : String(error)}`);
21211
21406
  }
@@ -22213,7 +22408,7 @@ function cropSprite(input, region) {
22213
22408
 
22214
22409
  // src/lib/image/io.ts
22215
22410
  import { randomBytes } from "crypto";
22216
- import { glob as fsGlob, readFile as readFile16, rename, stat as stat4, writeFile as writeFile8 } from "fs/promises";
22411
+ import { glob as fsGlob, readFile as readFile18, rename, stat as stat4, writeFile as writeFile9 } from "fs/promises";
22217
22412
  import { dirname as dirname2, extname as extname2, join as join4, resolve as resolve4 } from "path";
22218
22413
  var REMOTE_RE = /^https?:\/\//i;
22219
22414
  var GLOB_RE = /[*?[\]{}]/;
@@ -22249,11 +22444,11 @@ async function readImageBuffer(pathOrUrl) {
22249
22444
  }
22250
22445
  return Buffer.from(await response.arrayBuffer());
22251
22446
  }
22252
- return readFile16(pathOrUrl);
22447
+ return readFile18(pathOrUrl);
22253
22448
  }
22254
- async function isDirectory(path21) {
22449
+ async function isDirectory(path23) {
22255
22450
  try {
22256
- const s = await stat4(path21);
22451
+ const s = await stat4(path23);
22257
22452
  return s.isDirectory();
22258
22453
  } catch {
22259
22454
  return false;
@@ -22272,7 +22467,7 @@ async function atomicWrite(targetPath, data) {
22272
22467
  const absolute = resolve4(targetPath);
22273
22468
  const dir = dirname2(absolute);
22274
22469
  const tmp = join4(dir, `.baker-image-${randomBytes(8).toString("hex")}.tmp`);
22275
- await writeFile8(tmp, data);
22470
+ await writeFile9(tmp, data);
22276
22471
  await rename(tmp, absolute);
22277
22472
  }
22278
22473
 
@@ -22615,7 +22810,7 @@ var findCommand = defineCommand112({
22615
22810
  });
22616
22811
 
22617
22812
  // src/commands/images/generate.ts
22618
- import { readFile as readFile17 } from "fs/promises";
22813
+ import { readFile as readFile19 } from "fs/promises";
22619
22814
  import { defineCommand as defineCommand113 } from "citty";
22620
22815
  import sharp2 from "sharp";
22621
22816
  var GENERATE_TIMEOUT_MS = 18e4;
@@ -22705,7 +22900,7 @@ async function resolveReferences(spec) {
22705
22900
  }
22706
22901
  let raw;
22707
22902
  try {
22708
- raw = await readFile17(entry);
22903
+ raw = await readFile19(entry);
22709
22904
  } catch {
22710
22905
  throw new ApiError("VALIDATION_ERROR", `Reference file not found: ${entry}`);
22711
22906
  }
@@ -26864,7 +27059,7 @@ var searchCommand3 = defineCommand159({
26864
27059
  var tagsCommand5 = makeTagsCommand("videos", "video", "/api/videos/tags");
26865
27060
 
26866
27061
  // src/commands/videos/upload.ts
26867
- import { readFile as readFile18, stat as stat5 } from "fs/promises";
27062
+ import { readFile as readFile20, stat as stat5 } from "fs/promises";
26868
27063
  import { extname as extname3 } from "path";
26869
27064
  import { defineCommand as defineCommand160 } from "citty";
26870
27065
  var MIME_MAP = {
@@ -26929,7 +27124,7 @@ var uploadCommand2 = defineCommand160({
26929
27124
  return;
26930
27125
  }
26931
27126
  const { uploadUrl, videoId } = await apiPost("/api/videos/upload", {});
26932
- const fileBuffer = await readFile18(filePath);
27127
+ const fileBuffer = await readFile20(filePath);
26933
27128
  const uploadResponse = await fetch(uploadUrl, {
26934
27129
  method: "PUT",
26935
27130
  headers: { "Content-Type": contentType },