@koda-sl/baker-cli 0.203.0 → 0.208.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +64 -2
  2. package/dist/{chunk-UZYVSPCA.js → chunk-3E33AGNR.js} +2 -2
  3. package/dist/{chunk-SVHFT7AN.js → chunk-3WODSPNZ.js} +4 -4
  4. package/dist/{chunk-2R2DEPG7.js → chunk-CRXSLYEX.js} +3 -3
  5. package/dist/{chunk-5MPIOGRO.js → chunk-E2WYWHJJ.js} +370 -221
  6. package/dist/chunk-E2WYWHJJ.js.map +1 -0
  7. package/dist/{chunk-KHHD26LZ.js → chunk-FUSTYLP5.js} +3 -3
  8. package/dist/{chunk-6NZG2TCM.js → chunk-IRNM3T6B.js} +6 -1
  9. package/dist/{chunk-6NZG2TCM.js.map → chunk-IRNM3T6B.js.map} +1 -1
  10. package/dist/cli.js +767 -117
  11. package/dist/cli.js.map +1 -1
  12. package/dist/client-CF6ZORCO.js +15 -0
  13. package/dist/engine/index.js +3 -3
  14. package/dist/{env-FF7ZB5W5.js → env-7SCX7HAH.js} +4 -2
  15. package/dist/{output-YGPWST77.js → output-4YIOSWOG.js} +5 -5
  16. package/dist/{shared-CRWB5B7P.js → shared-EBF5DEDJ.js} +6 -6
  17. package/package.json +1 -1
  18. package/dist/chunk-5MPIOGRO.js.map +0 -1
  19. package/dist/client-MP7BKFKM.js +0 -15
  20. /package/dist/{chunk-UZYVSPCA.js.map → chunk-3E33AGNR.js.map} +0 -0
  21. /package/dist/{chunk-SVHFT7AN.js.map → chunk-3WODSPNZ.js.map} +0 -0
  22. /package/dist/{chunk-2R2DEPG7.js.map → chunk-CRXSLYEX.js.map} +0 -0
  23. /package/dist/{chunk-KHHD26LZ.js.map → chunk-FUSTYLP5.js.map} +0 -0
  24. /package/dist/{client-MP7BKFKM.js.map → client-CF6ZORCO.js.map} +0 -0
  25. /package/dist/{env-FF7ZB5W5.js.map → env-7SCX7HAH.js.map} +0 -0
  26. /package/dist/{output-YGPWST77.js.map → output-4YIOSWOG.js.map} +0 -0
  27. /package/dist/{shared-CRWB5B7P.js.map → shared-EBF5DEDJ.js.map} +0 -0
package/dist/cli.js CHANGED
@@ -18,18 +18,23 @@ import {
18
18
  SEEDANCE_PROFILE,
19
19
  VIDEO_GENERATE_MODELS,
20
20
  ValidationError,
21
+ YtDlpError,
22
+ canonicalUrl,
21
23
  clipParamRecipe,
22
24
  clipProfileFor,
23
25
  collectAssetRefLikes,
24
26
  createEngineFromEnv,
25
27
  defaultRegistry,
26
28
  describeFailureReason,
29
+ durationSeconds,
27
30
  elementMentionKeywords,
28
31
  estimateVideoCredits,
29
32
  fetchExternalBytes,
30
33
  generateCatalog,
31
34
  imageProfileFor,
35
+ isAudioOnly,
32
36
  isPersistedAssetRef,
37
+ isVimeoAuthFailure,
33
38
  looksLikeHttpUrl,
34
39
  maxInputReferences,
35
40
  maxInputSlot,
@@ -38,9 +43,11 @@ import {
38
43
  nearestSupportedImageSize,
39
44
  parseRefExpr,
40
45
  platformFormats,
46
+ probeYtDlp,
41
47
  promptMaxLength,
42
48
  requireCredentialsFromEnv,
43
49
  resolveConcurrency,
50
+ runYtDlp,
44
51
  sha256Hex,
45
52
  spineInputFlags,
46
53
  spineInputOps,
@@ -49,8 +56,9 @@ import {
49
56
  supportsReferenceToVideo,
50
57
  toModelSafeImage,
51
58
  ulid,
52
- validateCanvasDeep
53
- } from "./chunk-5MPIOGRO.js";
59
+ validateCanvasDeep,
60
+ ytDlpBlockSignal
61
+ } from "./chunk-E2WYWHJJ.js";
54
62
  import {
55
63
  csvOrJson,
56
64
  daysAgoIso,
@@ -59,7 +67,7 @@ import {
59
67
  resolveAccountIdArg,
60
68
  resolveEffectiveStatus,
61
69
  todayIso
62
- } from "./chunk-SVHFT7AN.js";
70
+ } from "./chunk-3WODSPNZ.js";
63
71
  import {
64
72
  buildQueryCacheKey,
65
73
  cacheGet,
@@ -75,20 +83,21 @@ import {
75
83
  writeAdsJson,
76
84
  writeAdsOutput,
77
85
  writeJsonEnvelope
78
- } from "./chunk-KHHD26LZ.js";
86
+ } from "./chunk-FUSTYLP5.js";
79
87
  import {
80
88
  ApiError,
81
89
  apiGet,
82
90
  apiPost,
83
91
  validateConvexId
84
- } from "./chunk-2R2DEPG7.js";
92
+ } from "./chunk-CRXSLYEX.js";
85
93
  import {
86
94
  installStreamTaps,
87
95
  logInvocation
88
- } from "./chunk-UZYVSPCA.js";
96
+ } from "./chunk-3E33AGNR.js";
89
97
  import {
90
98
  captureBudgetMs,
91
99
  captureProxyCredentials,
100
+ captureVimeoCookie,
92
101
  getEnv,
93
102
  isProxyFailure,
94
103
  plannedRoutes,
@@ -96,10 +105,10 @@ import {
96
105
  requireChatId,
97
106
  resolveChatId,
98
107
  shouldEscalate
99
- } from "./chunk-6NZG2TCM.js";
108
+ } from "./chunk-IRNM3T6B.js";
100
109
 
101
110
  // src/cli.ts
102
- import { defineCommand as defineCommand216, runMain } from "citty";
111
+ import { defineCommand as defineCommand217, runMain } from "citty";
103
112
 
104
113
  // src/commands/actions/index.ts
105
114
  import { defineCommand as defineCommand18 } from "citty";
@@ -5761,6 +5770,30 @@ var testimonialsOutscraperWebhookResponseSchema = z18.object({
5761
5770
  // ../api/src/videos.ts
5762
5771
  import { z as z19 } from "zod";
5763
5772
  var videoStatusSchema = z19.enum(["uploading", "uploaded", "processing", "ready", "error"]);
5773
+ var videoSourceSchema = z19.enum([
5774
+ "uploaded",
5775
+ "url",
5776
+ "bilibili",
5777
+ "bluesky",
5778
+ "dailymotion",
5779
+ "facebook",
5780
+ "instagram",
5781
+ "loom",
5782
+ "newgrounds",
5783
+ "ok",
5784
+ "pinterest",
5785
+ "reddit",
5786
+ "rutube",
5787
+ "snapchat",
5788
+ "streamable",
5789
+ "tiktok",
5790
+ "tumblr",
5791
+ "twitch",
5792
+ "twitter",
5793
+ "vimeo",
5794
+ "vk",
5795
+ "youtube"
5796
+ ]);
5764
5797
  var videoTranscriptSegmentSchema = z19.object({
5765
5798
  text: z19.string(),
5766
5799
  startSecond: z19.number(),
@@ -5861,6 +5894,25 @@ var videosUploadRequestSchema = z19.object({
5861
5894
  descriptionContext: z19.string().optional()
5862
5895
  });
5863
5896
  var videosUploadResponseSchema = z19.object({ uploadUrl: z19.string(), videoId: z19.string() });
5897
+ var videosIngestRequestSchema = z19.object({
5898
+ // Must be a direct media file Mux can pull (mp4/mov/webm/…). A page URL —
5899
+ // a YouTube watch link, a TikTok post — is not one: the CLI resolves those to
5900
+ // a media file before calling this route.
5901
+ url: z19.string().url(),
5902
+ source: videoSourceSchema,
5903
+ externalId: z19.string().optional(),
5904
+ externalUrl: z19.string().optional(),
5905
+ // What the person adding the clip knew that the frames cannot show — which
5906
+ // campaign it belongs to, whether the person on camera is a real customer.
5907
+ // The upload route has carried this since it existed; ingest silently dropped
5908
+ // it, so a link-added clip was analysed with strictly less to go on than the
5909
+ // same file uploaded by hand.
5910
+ descriptionContext: z19.string().optional()
5911
+ });
5912
+ var videosIngestResponseSchema = z19.object({
5913
+ videoId: z19.string(),
5914
+ deduped: z19.boolean()
5915
+ });
5864
5916
  var videosDeleteRequestSchema = z19.object({ id: z19.string().min(1, "Missing video ID") });
5865
5917
  var videosDeleteResponseSchema = z19.object({ ok: z19.literal(true) });
5866
5918
 
@@ -8821,11 +8873,11 @@ function rawTextEntries(value) {
8821
8873
  const values = Array.isArray(value) ? value : typeof value === "string" ? [value] : [];
8822
8874
  return values.filter((v) => typeof v === "string").flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
8823
8875
  }
8824
- function rawFileEntries(path37) {
8825
- if (typeof path37 !== "string" || path37.length === 0) {
8876
+ function rawFileEntries(path38) {
8877
+ if (typeof path38 !== "string" || path38.length === 0) {
8826
8878
  return [];
8827
8879
  }
8828
- return readFileSync2(path37, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
8880
+ return readFileSync2(path38, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
8829
8881
  }
8830
8882
  function keywordEntries(args) {
8831
8883
  const defaultMatch = typeof args["match-type"] === "string" ? args["match-type"].toUpperCase() : void 0;
@@ -8848,19 +8900,19 @@ function keywordEntries(args) {
8848
8900
  }
8849
8901
  return entries;
8850
8902
  }
8851
- function loadJsonFileArg(path37) {
8852
- if (typeof path37 !== "string" || path37.length === 0) {
8903
+ function loadJsonFileArg(path38) {
8904
+ if (typeof path38 !== "string" || path38.length === 0) {
8853
8905
  return {};
8854
8906
  }
8855
8907
  try {
8856
- const parsed = JSON.parse(readFileSync2(path37, "utf8"));
8908
+ const parsed = JSON.parse(readFileSync2(path38, "utf8"));
8857
8909
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
8858
- failWriteValidation(`${path37} must contain a JSON object`);
8910
+ failWriteValidation(`${path38} must contain a JSON object`);
8859
8911
  }
8860
8912
  return parsed;
8861
8913
  } catch (err) {
8862
8914
  if (err instanceof SyntaxError) {
8863
- failWriteValidation(`${path37} is not valid JSON: ${err.message}`);
8915
+ failWriteValidation(`${path38} is not valid JSON: ${err.message}`);
8864
8916
  }
8865
8917
  throw err;
8866
8918
  }
@@ -8990,10 +9042,10 @@ async function stageUpdate(kind, customerId, target, payload, hints) {
8990
9042
  async function stageTarget(kind, customerId, target, hints) {
8991
9043
  await stageGoogleOp({ kind, customerId, target }, hints);
8992
9044
  }
8993
- async function draftAction(path37, body, chat) {
9045
+ async function draftAction(path38, body, chat) {
8994
9046
  try {
8995
9047
  const chatId = resolveChatId(chat);
8996
- const response = await apiPost(path37, { chatId, ...body });
9048
+ const response = await apiPost(path38, { chatId, ...body });
8997
9049
  writeJsonEnvelope(response);
8998
9050
  } catch (err) {
8999
9051
  handleGoogleError(err);
@@ -11851,19 +11903,19 @@ function failWriteValidation2(message) {
11851
11903
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
11852
11904
  process.exit(1);
11853
11905
  }
11854
- function loadJsonFileArg2(path37) {
11855
- if (typeof path37 !== "string" || path37.length === 0) {
11906
+ function loadJsonFileArg2(path38) {
11907
+ if (typeof path38 !== "string" || path38.length === 0) {
11856
11908
  return {};
11857
11909
  }
11858
11910
  try {
11859
- const parsed = JSON.parse(readFileSync4(path37, "utf8"));
11911
+ const parsed = JSON.parse(readFileSync4(path38, "utf8"));
11860
11912
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
11861
- failWriteValidation2(`${path37} must contain a JSON object`);
11913
+ failWriteValidation2(`${path38} must contain a JSON object`);
11862
11914
  }
11863
11915
  return parsed;
11864
11916
  } catch (err) {
11865
11917
  if (err instanceof SyntaxError) {
11866
- failWriteValidation2(`${path37} is not valid JSON: ${err.message}`);
11918
+ failWriteValidation2(`${path38} is not valid JSON: ${err.message}`);
11867
11919
  }
11868
11920
  throw err;
11869
11921
  }
@@ -11948,15 +12000,15 @@ function parseLocaleFlag(value) {
11948
12000
  }
11949
12001
  return { language: match[1], country: match[2].toUpperCase() };
11950
12002
  }
11951
- function loadTargetingFileArg(path37) {
11952
- if (typeof path37 !== "string" || path37.length === 0) {
12003
+ function loadTargetingFileArg(path38) {
12004
+ if (typeof path38 !== "string" || path38.length === 0) {
11953
12005
  return void 0;
11954
12006
  }
11955
- const parsed = loadJsonFileArg2(path37);
12007
+ const parsed = loadJsonFileArg2(path38);
11956
12008
  const criteria = parsed.targetingCriteria ?? parsed;
11957
12009
  if (!criteria.include) {
11958
12010
  failWriteValidation2(
11959
- `${path37} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
12011
+ `${path38} must contain targeting criteria with an "include" block (see baker schema ads.linkedin.campaigns.create)`
11960
12012
  );
11961
12013
  }
11962
12014
  return criteria;
@@ -11991,14 +12043,14 @@ function parseCsvLine(line) {
11991
12043
  cells.push(current);
11992
12044
  return cells.map((cell) => cell.trim());
11993
12045
  }
11994
- function parseListFileArg(path37, maxRows) {
11995
- if (typeof path37 !== "string" || path37.length === 0) {
12046
+ function parseListFileArg(path38, maxRows) {
12047
+ if (typeof path38 !== "string" || path38.length === 0) {
11996
12048
  return void 0;
11997
12049
  }
11998
- const raw = readFileSync4(path37, "utf8");
12050
+ const raw = readFileSync4(path38, "utf8");
11999
12051
  const lines = raw.split(/\r?\n/).filter((line) => line.trim().length > 0);
12000
12052
  if (lines.length < 2) {
12001
- failWriteValidation2(`${path37} needs a header row and at least one data row`);
12053
+ failWriteValidation2(`${path38} needs a header row and at least one data row`);
12002
12054
  }
12003
12055
  const columns = parseCsvLine(lines[0]).map((column) => column.trim());
12004
12056
  const rows = [];
@@ -12017,7 +12069,7 @@ function parseListFileArg(path37, maxRows) {
12017
12069
  }
12018
12070
  }
12019
12071
  if (rows.length > maxRows) {
12020
- failWriteValidation2(`${path37} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
12072
+ failWriteValidation2(`${path38} has ${rows.length} rows \u2014 the inline limit is ${maxRows}. Split the list.`);
12021
12073
  }
12022
12074
  return { columns, rows };
12023
12075
  }
@@ -12113,11 +12165,11 @@ function readPositionals(args) {
12113
12165
  function splitIdList(raw) {
12114
12166
  return raw.split(",").map((id) => id.trim()).filter(Boolean);
12115
12167
  }
12116
- function idsFileEntries(path37) {
12117
- if (typeof path37 !== "string" || path37.length === 0) {
12168
+ function idsFileEntries(path38) {
12169
+ if (typeof path38 !== "string" || path38.length === 0) {
12118
12170
  return [];
12119
12171
  }
12120
- return readFileSync4(path37, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).flatMap(splitIdList);
12172
+ return readFileSync4(path38, "utf8").split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).flatMap(splitIdList);
12121
12173
  }
12122
12174
  function requireTargets(args, entity) {
12123
12175
  const positionals = readPositionals(args);
@@ -14740,9 +14792,9 @@ function compactRow(row) {
14740
14792
  ...destination.postUrn ? { postUrn: destination.postUrn } : {}
14741
14793
  };
14742
14794
  }
14743
- function readPath(row, path37) {
14795
+ function readPath(row, path38) {
14744
14796
  let current = row;
14745
- for (const segment of path37.split(".")) {
14797
+ for (const segment of path38.split(".")) {
14746
14798
  const record = asRecord2(current);
14747
14799
  if (!record) return void 0;
14748
14800
  current = record[segment];
@@ -14752,10 +14804,10 @@ function readPath(row, path37) {
14752
14804
  function projectFields(rows, paths) {
14753
14805
  return rows.map((row) => {
14754
14806
  const projected = {};
14755
- for (const path37 of paths) {
14756
- const value = readPath(row, path37);
14807
+ for (const path38 of paths) {
14808
+ const value = readPath(row, path38);
14757
14809
  if (value !== void 0) {
14758
- projected[path37] = value;
14810
+ projected[path38] = value;
14759
14811
  }
14760
14812
  }
14761
14813
  return projected;
@@ -16049,11 +16101,11 @@ var updateStatusSchema = z21.enum(UPDATE_STATUSES);
16049
16101
  function currencyMinimums2(currencyCode) {
16050
16102
  return CURRENCY_MINIMUMS2[currencyCode] ?? DEFAULT_CURRENCY_MINIMUM2;
16051
16103
  }
16052
- function validateDailyBudgetFloor(money, ctx, path37) {
16104
+ function validateDailyBudgetFloor(money, ctx, path38) {
16053
16105
  if (money?.currencyCode) {
16054
16106
  const min = currencyMinimums2(money.currencyCode).dailyBudgetMin;
16055
16107
  if (Number(money.amount) < min) {
16056
- ctx.addIssue({ code: "custom", path: path37, message: `below the ${min} ${money.currencyCode} daily minimum` });
16108
+ ctx.addIssue({ code: "custom", path: path38, message: `below the ${min} ${money.currencyCode} daily minimum` });
16057
16109
  }
16058
16110
  }
16059
16111
  }
@@ -16715,19 +16767,19 @@ function failWriteValidation3(message) {
16715
16767
  writeJsonEnvelope({ ok: false, error: { code: "VALIDATION_ERROR", message } });
16716
16768
  process.exit(1);
16717
16769
  }
16718
- function loadJsonFileArg3(path37) {
16719
- if (typeof path37 !== "string" || path37.length === 0) {
16770
+ function loadJsonFileArg3(path38) {
16771
+ if (typeof path38 !== "string" || path38.length === 0) {
16720
16772
  return {};
16721
16773
  }
16722
16774
  try {
16723
- const parsed = JSON.parse(readFileSync8(path37, "utf8"));
16775
+ const parsed = JSON.parse(readFileSync8(path38, "utf8"));
16724
16776
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
16725
- failWriteValidation3(`${path37} must contain a JSON object`);
16777
+ failWriteValidation3(`${path38} must contain a JSON object`);
16726
16778
  }
16727
16779
  return parsed;
16728
16780
  } catch (err) {
16729
16781
  if (err instanceof SyntaxError) {
16730
- failWriteValidation3(`${path37} is not valid JSON: ${err.message}`);
16782
+ failWriteValidation3(`${path38} is not valid JSON: ${err.message}`);
16731
16783
  }
16732
16784
  throw err;
16733
16785
  }
@@ -17270,10 +17322,10 @@ function duplicateCommand2(entity, label) {
17270
17322
  replace: { type: "boolean", description: "Pause the original once the copy publishes" }
17271
17323
  },
17272
17324
  run: async ({ args }) => {
17273
- const { apiPost: apiPost2 } = await import("./client-MP7BKFKM.js");
17274
- const { requireChatId: requireChatId2 } = await import("./env-FF7ZB5W5.js");
17275
- const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-YGPWST77.js");
17276
- const { handleMetaError: handleMetaError2 } = await import("./shared-CRWB5B7P.js");
17325
+ const { apiPost: apiPost2 } = await import("./client-CF6ZORCO.js");
17326
+ const { requireChatId: requireChatId2 } = await import("./env-7SCX7HAH.js");
17327
+ const { writeJsonEnvelope: writeJsonEnvelope2 } = await import("./output-4YIOSWOG.js");
17328
+ const { handleMetaError: handleMetaError2 } = await import("./shared-EBF5DEDJ.js");
17277
17329
  try {
17278
17330
  const accountId = bareAccountId2(args);
17279
17331
  const chatId = requireChatId2();
@@ -27898,12 +27950,12 @@ function listFlowSlugs() {
27898
27950
  return readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith("_") && entry.name !== ".gitkeep").map((entry) => entry.name).sort();
27899
27951
  }
27900
27952
  function readFlowTree(slug) {
27901
- const path37 = join3(flowsDir(), slug, "_data.json");
27902
- if (!existsSync5(path37)) {
27953
+ const path38 = join3(flowsDir(), slug, "_data.json");
27954
+ if (!existsSync5(path38)) {
27903
27955
  failLocal(`No form "${slug}". Run "baker flows list" to see the forms in this workspace.`);
27904
27956
  }
27905
27957
  try {
27906
- return JSON.parse(readFileSync9(path37, "utf-8"));
27958
+ return JSON.parse(readFileSync9(path38, "utf-8"));
27907
27959
  } catch (error) {
27908
27960
  failLocal(`Could not read form "${slug}": ${error instanceof Error ? error.message : String(error)}`);
27909
27961
  }
@@ -28316,10 +28368,10 @@ async function stageOps(ops) {
28316
28368
  handleError(err);
28317
28369
  }
28318
28370
  }
28319
- async function draftAction2(path37, body, chat) {
28371
+ async function draftAction2(path38, body, chat) {
28320
28372
  const chatId = resolveChatId(chat);
28321
28373
  try {
28322
- const data = await apiPost(path37, { chatId, ...body });
28374
+ const data = await apiPost(path38, { chatId, ...body });
28323
28375
  writeJsonEnvelope({ ok: true, data });
28324
28376
  return data;
28325
28377
  } catch (err) {
@@ -30340,9 +30392,9 @@ async function readImageBuffer(pathOrUrl) {
30340
30392
  }
30341
30393
  return readFile19(pathOrUrl);
30342
30394
  }
30343
- async function isDirectory(path37) {
30395
+ async function isDirectory(path38) {
30344
30396
  try {
30345
- const s = await stat4(path37);
30397
+ const s = await stat4(path38);
30346
30398
  return s.isDirectory();
30347
30399
  } catch {
30348
30400
  return false;
@@ -30626,13 +30678,13 @@ function resolveDownloadPath({ baseName, extension, out, outIsDirectory: outIsDi
30626
30678
  }
30627
30679
  function disambiguate(paths) {
30628
30680
  const taken = /* @__PURE__ */ new Set();
30629
- return paths.map((path37) => {
30630
- if (!taken.has(path37)) {
30631
- taken.add(path37);
30632
- return path37;
30681
+ return paths.map((path38) => {
30682
+ if (!taken.has(path38)) {
30683
+ taken.add(path38);
30684
+ return path38;
30633
30685
  }
30634
- const ext = extname3(path37);
30635
- const stem = path37.slice(0, path37.length - ext.length);
30686
+ const ext = extname3(path38);
30687
+ const stem = path38.slice(0, path38.length - ext.length);
30636
30688
  let n = 2;
30637
30689
  while (taken.has(`${stem}-${n}${ext}`)) n += 1;
30638
30690
  const unique = `${stem}-${n}${ext}`;
@@ -30759,10 +30811,10 @@ async function runDownloads(plan) {
30759
30811
  const paths = disambiguate(fetched.map((item) => item.path));
30760
30812
  const downloaded = [];
30761
30813
  for (const [index, item] of fetched.entries()) {
30762
- const path37 = paths[index] ?? item.path;
30814
+ const path38 = paths[index] ?? item.path;
30763
30815
  try {
30764
- await atomicWrite(path37, item.buffer);
30765
- downloaded.push({ input: item.input, output: path37, bytes: item.buffer.length, contentType: item.contentType });
30816
+ await atomicWrite(path38, item.buffer);
30817
+ downloaded.push({ input: item.input, output: path38, bytes: item.buffer.length, contentType: item.contentType });
30766
30818
  } catch (err) {
30767
30819
  failed.push({ input: item.input, error: failureMessage(err, "Write failed") });
30768
30820
  }
@@ -32513,14 +32565,14 @@ async function processInternal(inputBuffer, isSVG, options) {
32513
32565
  processedData = result.data;
32514
32566
  info = result.info;
32515
32567
  }
32516
- let pipeline = sharp2(processedData, {
32568
+ let pipeline2 = sharp2(processedData, {
32517
32569
  raw: { channels: 4, width: info.width, height: info.height }
32518
32570
  }).png();
32519
32571
  if (options.shrinkToContent) {
32520
32572
  stages.push("trim");
32521
- pipeline = pipeline.trim();
32573
+ pipeline2 = pipeline2.trim();
32522
32574
  }
32523
- const outputBuffer = await pipeline.toBuffer();
32575
+ const outputBuffer = await pipeline2.toBuffer();
32524
32576
  return { buffer: outputBuffer, stages };
32525
32577
  }
32526
32578
  function applyResize(buffer, resize) {
@@ -40493,7 +40545,7 @@ function parseImageRefs(spec) {
40493
40545
  }
40494
40546
  var defaultDeps = {
40495
40547
  ingest: (url) => apiPost("/api/images/ingest", { url, source: "uploaded" }),
40496
- upload: (path37) => uploadLocalImage({ file: path37, contentType: detectImageContentType(path37), source: "uploaded" })
40548
+ upload: (path38) => uploadLocalImage({ file: path38, contentType: detectImageContentType(path38), source: "uploaded" })
40497
40549
  };
40498
40550
  async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
40499
40551
  const refs = parseImageRefs(spec);
@@ -40513,10 +40565,10 @@ async function resolveLibraryImageIds(spec, limit, deps = defaultDeps) {
40513
40565
  }
40514
40566
  return { imageIds, added };
40515
40567
  }
40516
- function uploadFailure(path37) {
40568
+ function uploadFailure(path38) {
40517
40569
  return (error) => {
40518
40570
  if (error instanceof ApiError) throw error;
40519
- throw new ApiError("VALIDATION_ERROR", `Could not read "${path37}" as an image.`);
40571
+ throw new ApiError("VALIDATION_ERROR", `Could not read "${path38}" as an image.`);
40520
40572
  };
40521
40573
  }
40522
40574
 
@@ -41566,10 +41618,10 @@ async function stageOp4(op) {
41566
41618
  handleError4(err);
41567
41619
  }
41568
41620
  }
41569
- async function draftAction3(path37, body, chat) {
41621
+ async function draftAction3(path38, body, chat) {
41570
41622
  const chatId = resolveChatId(chat);
41571
41623
  try {
41572
- const data = await apiPost(path37, { chatId, ...body });
41624
+ const data = await apiPost(path38, { chatId, ...body });
41573
41625
  writeJsonEnvelope({ ok: true, data });
41574
41626
  return data;
41575
41627
  } catch (err) {
@@ -42542,7 +42594,7 @@ Full guide: __tooling__/docs/tools/baker/testimonials.md`
42542
42594
  });
42543
42595
 
42544
42596
  // src/commands/videos/index.ts
42545
- import { defineCommand as defineCommand202 } from "citty";
42597
+ import { defineCommand as defineCommand203 } from "citty";
42546
42598
 
42547
42599
  // src/commands/videos/delete.ts
42548
42600
  import { defineCommand as defineCommand197 } from "citty";
@@ -42673,8 +42725,420 @@ var groupCommand2 = defineCommand199({
42673
42725
  }
42674
42726
  });
42675
42727
 
42676
- // src/commands/videos/search.ts
42728
+ // src/commands/videos/ingest.ts
42729
+ import { mkdtemp as mkdtemp2, rm as rm7, stat as stat7 } from "fs/promises";
42730
+ import { tmpdir as tmpdir3 } from "os";
42731
+ import path37 from "path";
42677
42732
  import { defineCommand as defineCommand200 } from "citty";
42733
+
42734
+ // src/lib/streamUpload.ts
42735
+ import { createHash as createHash2 } from "crypto";
42736
+ import { createReadStream } from "fs";
42737
+ import { Readable } from "stream";
42738
+ import { pipeline } from "stream/promises";
42739
+ async function hashFile(filePath) {
42740
+ const hash = createHash2("sha256");
42741
+ await pipeline(createReadStream(filePath), hash);
42742
+ return hash.digest("hex");
42743
+ }
42744
+ async function putFileStreamed(args) {
42745
+ const body = Readable.toWeb(createReadStream(args.filePath));
42746
+ return await fetch(args.putUrl, {
42747
+ method: "PUT",
42748
+ body,
42749
+ headers: {
42750
+ "Content-Type": args.contentType,
42751
+ "Content-Length": String(args.sizeBytes)
42752
+ },
42753
+ duplex: "half"
42754
+ });
42755
+ }
42756
+
42757
+ // src/commands/videos/videoPlatforms.ts
42758
+ var PLATFORM_DOMAINS = {
42759
+ youtube: ["youtube.com", "youtu.be"],
42760
+ instagram: ["instagram.com", "ddinstagram.com"],
42761
+ tiktok: ["tiktok.com"],
42762
+ twitter: ["twitter.com", "x.com", "vxtwitter.com", "fixvx.com"],
42763
+ facebook: ["facebook.com", "fb.watch", "fb.com"],
42764
+ reddit: ["reddit.com", "redd.it"],
42765
+ vimeo: ["vimeo.com"],
42766
+ dailymotion: ["dailymotion.com", "dai.ly"],
42767
+ twitch: ["twitch.tv"],
42768
+ bluesky: ["bsky.app"],
42769
+ bilibili: ["bilibili.com", "bilibili.tv", "b23.tv"],
42770
+ vk: ["vk.com", "vk.ru", "vkvideo.ru"],
42771
+ ok: ["ok.ru"],
42772
+ pinterest: ["pinterest.com", "pin.it"],
42773
+ tumblr: ["tumblr.com"],
42774
+ snapchat: ["snapchat.com"],
42775
+ loom: ["loom.com"],
42776
+ streamable: ["streamable.com"],
42777
+ newgrounds: ["newgrounds.com"],
42778
+ rutube: ["rutube.ru"]
42779
+ };
42780
+ var VIDEO_PLATFORMS = Object.keys(PLATFORM_DOMAINS);
42781
+ function platformFromUrl(rawUrl) {
42782
+ let host;
42783
+ try {
42784
+ host = new URL(rawUrl).hostname.toLowerCase();
42785
+ } catch {
42786
+ return "url";
42787
+ }
42788
+ for (const [platform, domains] of Object.entries(PLATFORM_DOMAINS)) {
42789
+ if (domains.some((domain) => host === domain || host.endsWith(`.${domain}`))) {
42790
+ return platform;
42791
+ }
42792
+ }
42793
+ return "url";
42794
+ }
42795
+
42796
+ // src/commands/videos/ingestUrl.ts
42797
+ var MAX_VIDEO_INGEST_BYTES = 16 * 1024 * 1024 * 1024;
42798
+ var MAX_VIDEO_INGEST_SECONDS = 3 * 60 * 60;
42799
+ function refuseByDuration(seconds, maxSeconds) {
42800
+ if (seconds === void 0 || seconds <= maxSeconds) return null;
42801
+ return `That video is ${(seconds / 3600).toFixed(1)} hours long \u2014 the limit is ${Math.round(maxSeconds / 3600)} hours.`;
42802
+ }
42803
+ var DOWNLOAD_TIMEOUT_FLOOR_MS = 10 * 60 * 1e3;
42804
+ var DOWNLOAD_TIMEOUT_CEILING_MS = 15 * 60 * 1e3;
42805
+ function downloadTimeoutMs(seconds) {
42806
+ if (seconds === void 0) return DOWNLOAD_TIMEOUT_FLOOR_MS;
42807
+ const scaled = seconds * 2 * 1e3;
42808
+ return Math.min(Math.max(scaled, DOWNLOAD_TIMEOUT_FLOOR_MS), DOWNLOAD_TIMEOUT_CEILING_MS);
42809
+ }
42810
+ var INGEST_MAX_HEIGHT = 1080;
42811
+ var DIRECT_MEDIA_EXTENSIONS = [".mp4", ".mov", ".webm", ".m4v", ".mkv", ".avi"];
42812
+ function isDirectMediaUrl(rawUrl) {
42813
+ const pathname = safePathname(rawUrl);
42814
+ return DIRECT_MEDIA_EXTENSIONS.some((ext) => pathname.endsWith(ext));
42815
+ }
42816
+ function resolveIngestSource(explicit, rawUrl) {
42817
+ if (explicit) return explicit;
42818
+ return platformFromUrl(rawUrl);
42819
+ }
42820
+ var TOOLING_OR_BLOCK_SIGNATURES = [
42821
+ "requested format is not available",
42822
+ "only images are available",
42823
+ "nsig",
42824
+ "sign in to confirm",
42825
+ // bot check — distinct from "Private video. Sign in if…"
42826
+ "unable to download api page",
42827
+ "http error 403",
42828
+ // yt-dlp's own "the site changed under me" signature. It prints a GitHub issue
42829
+ // link whenever a page parsed nothing like the shape the extractor expects —
42830
+ // which is precisely a downloader that has aged out, not a bad link. Observed
42831
+ // live on TikTok, which probed fine and then broke ninety minutes later from
42832
+ // the same machine, identically through direct, datacenter and residential
42833
+ // exits. Without this it fell through to "check the link is public", sending
42834
+ // the reader to inspect a video that was never the problem.
42835
+ "please report this issue"
42836
+ ];
42837
+ function isToolingOrBlockFailure(detail) {
42838
+ const haystack = detail.toLowerCase();
42839
+ return TOOLING_OR_BLOCK_SIGNATURES.some((signature) => haystack.includes(signature));
42840
+ }
42841
+ var REGION_BLOCK_SIGNATURES = [
42842
+ "in your country",
42843
+ "geo-restricted",
42844
+ "geo restricted",
42845
+ "not available from your location",
42846
+ "blocked in your country"
42847
+ ];
42848
+ function isDrmProtected(detail) {
42849
+ return detail.toLowerCase().includes("drm protected");
42850
+ }
42851
+ function isRegionBlocked(detail) {
42852
+ const haystack = detail.toLowerCase();
42853
+ return REGION_BLOCK_SIGNATURES.some((signature) => haystack.includes(signature));
42854
+ }
42855
+ function worthDownloadingInstead(errorCode) {
42856
+ return errorCode === "VALIDATION_ERROR";
42857
+ }
42858
+ function directDedupeKey(source, rawUrl) {
42859
+ return `${source}:${rawUrl}`;
42860
+ }
42861
+ function safePathname(rawUrl) {
42862
+ try {
42863
+ return new URL(rawUrl).pathname.toLowerCase();
42864
+ } catch {
42865
+ return "";
42866
+ }
42867
+ }
42868
+
42869
+ // src/commands/videos/ingest.ts
42870
+ registerSchema({
42871
+ command: "videos.ingest",
42872
+ description: "Add a video to the library from a URL \u2014 a direct file, or a YouTube/TikTok/Vimeo page.",
42873
+ args: {
42874
+ url: { type: "string", description: "Video URL (direct file or a watch/post page)", required: true },
42875
+ source: { type: "string", description: "Provenance: url | youtube", required: false },
42876
+ "external-id": { type: "string", description: "Provider asset id (deduped on)", required: false },
42877
+ "external-url": { type: "string", description: "Canonical page URL", required: false },
42878
+ download: {
42879
+ type: "boolean",
42880
+ description: "Force the download path instead of letting Baker fetch the URL directly",
42881
+ required: false,
42882
+ default: false
42883
+ },
42884
+ country: {
42885
+ type: "string",
42886
+ description: "Two-letter country to fetch from (e.g. US, DE, JP) \u2014 use when a video is blocked in your region",
42887
+ required: false
42888
+ },
42889
+ context: {
42890
+ type: "string",
42891
+ description: "What you know that the video cannot show \u2014 the campaign, whether the person on camera is a real customer, what it is for. Highest-leverage flag here: it is what makes the clip findable later.",
42892
+ required: false
42893
+ },
42894
+ "dry-run": { type: "boolean", description: "Preview the operation without executing", required: false }
42895
+ }
42896
+ });
42897
+ var ingestCommand2 = defineCommand200({
42898
+ meta: {
42899
+ name: "ingest",
42900
+ description: "Add a video to the library from a URL. A direct file URL is handed straight to Baker, which fetches it. A page URL (YouTube, TikTok, Vimeo, Instagram) is downloaded here first, then uploaded \u2014 and a direct URL that Baker cannot fetch falls back to that same path automatically.\n\nExample: baker videos ingest https://www.youtube.com/watch?v=abc123"
42901
+ },
42902
+ args: {
42903
+ url: { type: "positional", description: "Video URL", required: false },
42904
+ source: { type: "string", description: "Provenance: url | youtube", required: false },
42905
+ "external-id": { type: "string", description: "Provider asset id", required: false },
42906
+ "external-url": { type: "string", description: "Canonical page URL", required: false },
42907
+ download: { type: "boolean", description: "Force the download path", required: false, default: false },
42908
+ country: { type: "string", description: "Two-letter country to fetch from, e.g. US", required: false },
42909
+ context: {
42910
+ type: "string",
42911
+ description: "Context for the analysis \u2014 campaign, who is on camera, intended use",
42912
+ required: false
42913
+ },
42914
+ "dry-run": { type: "boolean", description: "Preview without executing", required: false, default: false }
42915
+ },
42916
+ run: ({ args }) => runVideoIngest({
42917
+ url: args.url,
42918
+ source: args.source,
42919
+ externalId: args["external-id"],
42920
+ externalUrl: args["external-url"],
42921
+ forceDownload: args.download === true,
42922
+ country: args.country,
42923
+ context: args.context,
42924
+ dryRun: args["dry-run"] === true
42925
+ })
42926
+ });
42927
+ async function runVideoIngest(opts) {
42928
+ const url = opts.url;
42929
+ if (!url) {
42930
+ writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message: "URL is required" } });
42931
+ process.exit(1);
42932
+ }
42933
+ const direct = !opts.forceDownload && isDirectMediaUrl(url);
42934
+ const country = opts.country?.trim() || void 0;
42935
+ const args = {
42936
+ url,
42937
+ source: resolveIngestSource(opts.source, url),
42938
+ externalId: opts.externalId,
42939
+ // Deliberately NOT pre-filled with the pasted URL. Doing so shadowed the
42940
+ // canonical URL the probe resolves (`?? canonicalUrl(...)` below could never
42941
+ // run), so the same video filed under a different `externalUrl` depending on
42942
+ // whether the link was copied from a share sheet, a shorts page or the
42943
+ // address bar. `downloadThenIngest` falls back to `args.url` when the probe
42944
+ // resolves nothing, which is the value this used to hard-code.
42945
+ externalUrl: opts.externalUrl,
42946
+ descriptionContext: opts.context?.trim() || void 0
42947
+ };
42948
+ const validation = videosIngestRequestSchema.safeParse({ ...args, url });
42949
+ if (!validation.success) {
42950
+ const problem = validation.error.issues[0];
42951
+ writeJson({
42952
+ ok: false,
42953
+ error: {
42954
+ code: "VALIDATION_ERROR",
42955
+ message: problem ? `${problem.path.join(".") || "request"}: ${problem.message}` : "Invalid request"
42956
+ }
42957
+ });
42958
+ process.exit(1);
42959
+ }
42960
+ if (opts.dryRun) {
42961
+ reportDryRun(args, direct, country);
42962
+ return;
42963
+ }
42964
+ try {
42965
+ writeJson({ ok: true, data: await ingestByRoute(args, direct, country) });
42966
+ } catch (err) {
42967
+ reportIngestFailure(err);
42968
+ }
42969
+ }
42970
+ function reportDryRun(args, direct, country) {
42971
+ writeJson({
42972
+ ok: true,
42973
+ dryRun: true,
42974
+ operation: "videos.ingest",
42975
+ params: {
42976
+ url: args.url,
42977
+ source: args.source,
42978
+ // Known up front on the direct route, because it is derived from the URL
42979
+ // alone. The download route's key comes from the probe, which a dry run
42980
+ // does not perform, so there it stays null rather than becoming a guess.
42981
+ externalId: args.externalId ?? (direct ? directDedupeKey(args.source, args.url) : null),
42982
+ // A page URL's canonical form is only known after the probe, which a dry
42983
+ // run does not perform — so this is null rather than a guess.
42984
+ externalUrl: args.externalUrl ?? null,
42985
+ direct,
42986
+ country: country ?? null,
42987
+ descriptionContext: args.descriptionContext ?? null
42988
+ }
42989
+ });
42990
+ }
42991
+ async function ingestByRoute(args, direct, country) {
42992
+ if (!direct) {
42993
+ return { ...await downloadThenIngest(args, country), via: "download" };
42994
+ }
42995
+ try {
42996
+ const externalId = args.externalId ?? directDedupeKey(args.source, args.url);
42997
+ return { ...await ingestUrl({ ...args, externalId }), via: "direct" };
42998
+ } catch (err) {
42999
+ if (!(err instanceof ApiError) || !worthDownloadingInstead(err.code)) throw err;
43000
+ return { ...await downloadThenIngest(args, country), via: "download", fallbackFrom: err.message };
43001
+ }
43002
+ }
43003
+ function reportYtDlpFailure(err) {
43004
+ const detail = `${err.stderrTail} ${err.message}`;
43005
+ if (isProxyFailure(ytDlpBlockSignal(detail))) {
43006
+ fail7(
43007
+ "Couldn't download that video: our connection to the internet was refused.",
43008
+ "This is Baker's egress proxy, not the link \u2014 its credentials look wrong or expired. The link is probably fine. Report it; retrying won't help until the proxy is fixed."
43009
+ );
43010
+ }
43011
+ if (isDrmProtected(detail)) {
43012
+ fail7(
43013
+ "That video is copy-protected, so it can't be added to the library.",
43014
+ "The publisher encrypted this one against downloading. Nothing will change that \u2014 not retrying, not a different region. Ask whoever owns the video for the original file and add it with `baker videos upload <file>`."
43015
+ );
43016
+ }
43017
+ if (isVimeoAuthFailure(detail)) {
43018
+ fail7(
43019
+ "Vimeo wouldn't hand over that video \u2014 Baker isn't signed in to Vimeo.",
43020
+ "Vimeo now refuses anonymous downloads entirely, so this needs a signed-in session and the one Baker holds has either expired or was never set. Nothing is wrong with the link. Report it so the Vimeo cookie can be refreshed; meanwhile download the file and use `baker videos upload <file>`."
43021
+ );
43022
+ }
43023
+ if (isRegionBlocked(detail)) {
43024
+ fail7(
43025
+ `That video isn't available in the region we're fetching from. ${err.stderrTail || err.message}`,
43026
+ "Re-run with `--country <two-letter code>` for a country where the video plays \u2014 e.g. `--country US`. Pick the one the video belongs to (the uploader's country is the usual answer); each attempt costs a paid connection, so choose rather than work through the list."
43027
+ );
43028
+ }
43029
+ fail7(
43030
+ `Couldn't download that video. ${err.stderrTail || err.message}`,
43031
+ isToolingOrBlockFailure(detail) ? "The site wouldn't hand over the video \u2014 usually the downloader being out of date against a site that changed, or the request being blocked. Retrying won't help, and the link is probably fine. Report it so the tool can be updated; meanwhile download the file and use `baker videos upload <file>`." : "Check the link is public and playable. Private or age-restricted videos can't be downloaded \u2014 download the file yourself and use `baker videos upload <file>`."
43032
+ );
43033
+ }
43034
+ function fail7(message, fix, code = "INGEST_FAILED") {
43035
+ writeJson({ ok: false, error: { code, message, fix } });
43036
+ process.exit(1);
43037
+ }
43038
+ function reportIngestFailure(err) {
43039
+ if (err instanceof ApiError) {
43040
+ writeJson({ ok: false, error: { code: err.code, message: err.message } });
43041
+ process.exit(1);
43042
+ }
43043
+ if (err instanceof YtDlpError) reportYtDlpFailure(err);
43044
+ const detail = err instanceof Error ? err.message : String(err);
43045
+ if (detail.includes("timed out after")) {
43046
+ fail7(
43047
+ "That video took too long to download and was stopped part-way.",
43048
+ "Long or slow-serving videos can outrun the download window. Try a shorter clip, or download this one yourself and use `baker videos upload <file>`.",
43049
+ "INTERNAL_ERROR"
43050
+ );
43051
+ }
43052
+ fail7(
43053
+ `Couldn't add that video. ${detail}`,
43054
+ "This is a fault on Baker's side, not a problem with the link. Report it.",
43055
+ "INTERNAL_ERROR"
43056
+ );
43057
+ }
43058
+ function dedupeKey(args, info) {
43059
+ const id = typeof info.id === "string" ? info.id : void 0;
43060
+ if (!id) return void 0;
43061
+ if (info.extractor === "generic") return `${args.source}:${canonicalUrl(info) ?? args.url}`;
43062
+ return `${args.source}:${id}`;
43063
+ }
43064
+ function ingestUrl(args) {
43065
+ const body = {
43066
+ url: args.url,
43067
+ source: args.source
43068
+ };
43069
+ if (args.externalId) body.externalId = args.externalId;
43070
+ if (args.externalUrl) body.externalUrl = args.externalUrl;
43071
+ if (args.descriptionContext) body.descriptionContext = args.descriptionContext;
43072
+ return apiPost("/api/videos/ingest", body);
43073
+ }
43074
+ async function downloadThenIngest(args, country) {
43075
+ const vimeoCookie = captureVimeoCookie();
43076
+ const workDir = await mkdtemp2(path37.join(tmpdir3(), "videos-ingest-"));
43077
+ try {
43078
+ const probe = await probeYtDlp({ url: args.url, country, vimeoCookie, cookieDir: workDir });
43079
+ if (isAudioOnly(probe.info)) {
43080
+ throw new ApiError(
43081
+ "VALIDATION_ERROR",
43082
+ "That link is audio, not video, so it can't go in the video library. Music and podcast links have no picture to store."
43083
+ );
43084
+ }
43085
+ const tooLong = refuseByDuration(durationSeconds(probe.info), MAX_VIDEO_INGEST_SECONDS);
43086
+ if (tooLong) {
43087
+ throw new ApiError("VALIDATION_ERROR", tooLong);
43088
+ }
43089
+ const externalId = args.externalId ?? dedupeKey(args, probe.info);
43090
+ const { filePath, tier } = await runYtDlp({
43091
+ url: args.url,
43092
+ audioOnly: false,
43093
+ workDir,
43094
+ maxHeight: INGEST_MAX_HEIGHT,
43095
+ country,
43096
+ vimeoCookie,
43097
+ // Budgeted from the probed length, so the duration we accept and the time
43098
+ // we allow to fetch it cannot drift apart.
43099
+ timeoutMs: downloadTimeoutMs(durationSeconds(probe.info))
43100
+ });
43101
+ const stats = await stat7(filePath);
43102
+ if (stats.size > MAX_VIDEO_INGEST_BYTES) {
43103
+ throw new ApiError(
43104
+ "VALIDATION_ERROR",
43105
+ `That video is ${(stats.size / 1024 ** 3).toFixed(1)} GB, over the ${Math.round(MAX_VIDEO_INGEST_BYTES / 1024 ** 3)} GB limit for this route.`
43106
+ );
43107
+ }
43108
+ const publicUrl = await uploadToAssetStore(filePath, stats.size);
43109
+ return {
43110
+ ...await ingestUrl({
43111
+ ...args,
43112
+ url: publicUrl,
43113
+ externalId,
43114
+ externalUrl: args.externalUrl ?? canonicalUrl(probe.info) ?? args.url
43115
+ }),
43116
+ egress: tier,
43117
+ // Named in the result for the same reason `egress` is: a geo-targeted exit
43118
+ // is a deliberate, billable choice and must not be invisible afterwards.
43119
+ ...country ? { country } : {}
43120
+ };
43121
+ } finally {
43122
+ await rm7(workDir, { recursive: true, force: true }).catch(() => {
43123
+ });
43124
+ }
43125
+ }
43126
+ async function uploadToAssetStore(filePath, sizeBytes) {
43127
+ const sha256 = await hashFile(filePath);
43128
+ const { putUrl, publicUrl } = await apiPost("/api/canvas/assets/presign", {
43129
+ sha256,
43130
+ mime: "video/mp4",
43131
+ purpose: "source"
43132
+ });
43133
+ const putRes = await putFileStreamed({ putUrl, filePath, contentType: "video/mp4", sizeBytes });
43134
+ if (!putRes.ok) {
43135
+ throw new ApiError("INTERNAL_ERROR", `Storing the downloaded video failed: HTTP ${putRes.status}`);
43136
+ }
43137
+ return publicUrl;
43138
+ }
43139
+
43140
+ // src/commands/videos/search.ts
43141
+ import { defineCommand as defineCommand201 } from "citty";
42678
43142
  registerSchema({
42679
43143
  command: "videos.search",
42680
43144
  description: "Search videos by text query. Only returns ready videos.",
@@ -42684,7 +43148,7 @@ registerSchema({
42684
43148
  tags: { type: "string", description: "Comma-separated tags to filter by", required: false }
42685
43149
  }
42686
43150
  });
42687
- var searchCommand4 = defineCommand200({
43151
+ var searchCommand4 = defineCommand201({
42688
43152
  meta: {
42689
43153
  name: "search",
42690
43154
  description: "Semantic search videos by text query. Uses hybrid BM25 + vector + reranking. Example: baker videos search 'product demo' --tags tutorial"
@@ -42734,9 +43198,9 @@ var searchCommand4 = defineCommand200({
42734
43198
  var tagsCommand5 = makeTagsCommand("videos", "video", "/api/videos/tags");
42735
43199
 
42736
43200
  // src/commands/videos/upload.ts
42737
- import { readFile as readFile24, stat as stat7 } from "fs/promises";
43201
+ import { readFile as readFile24, stat as stat8 } from "fs/promises";
42738
43202
  import { basename as basename3, extname as extname4 } from "path";
42739
- import { defineCommand as defineCommand201 } from "citty";
43203
+ import { defineCommand as defineCommand202 } from "citty";
42740
43204
  var MIME_MAP = {
42741
43205
  ".mp4": "video/mp4",
42742
43206
  ".mov": "video/quicktime",
@@ -42746,9 +43210,9 @@ var MIME_MAP = {
42746
43210
  };
42747
43211
  registerSchema({
42748
43212
  command: "videos.upload",
42749
- description: "Upload a video file to Baker (via Mux direct upload)",
43213
+ description: "Upload a video to Baker \u2014 a local file (via Mux direct upload) or a remote http(s) URL.",
42750
43214
  args: {
42751
- file: { type: "string", description: "Path to the video file", required: true },
43215
+ file: { type: "string", description: "Local video file path, or a remote http(s) URL", required: true },
42752
43216
  "content-type": {
42753
43217
  type: "string",
42754
43218
  description: "MIME type (auto-detected from extension if omitted)",
@@ -42775,33 +43239,50 @@ function detectContentType(filePath) {
42775
43239
  }
42776
43240
  return mime;
42777
43241
  }
42778
- var uploadCommand2 = defineCommand201({
43242
+ function isRemoteUrl3(value) {
43243
+ return /^https?:\/\//i.test(value);
43244
+ }
43245
+ var uploadCommand2 = defineCommand202({
42779
43246
  meta: {
42780
43247
  name: "upload",
42781
- description: "Upload a video file to Baker via Mux direct upload. Auto-detects content type. Example: baker videos upload ./demo.mp4"
43248
+ description: "Upload a video to Baker \u2014 accepts a local file path OR a remote http(s) URL.\n\nLocal: auto-detects content type and uploads via Mux direct upload.\nRemote: hands off to `videos ingest` (direct fetch, or download-then-upload for a YouTube/TikTok/Vimeo page).\n\nExamples:\n baker videos upload ./demo.mp4\n baker videos upload https://www.youtube.com/watch?v=abc123"
42782
43249
  },
42783
43250
  args: {
42784
- file: { type: "positional", description: "Path to the video file", required: false },
42785
- "content-type": { type: "string", description: "MIME type (auto-detected if omitted)", required: false },
43251
+ file: { type: "positional", description: "Local video file path or remote http(s) URL", required: false },
43252
+ "content-type": { type: "string", description: "MIME type (local only \u2014 auto-detected)", required: false },
42786
43253
  context: {
42787
43254
  type: "string",
42788
43255
  description: "What this clip is and what it is for \u2014 who is on camera, which campaign, how it was shot. Start here: the analysis reads it, so it is what makes the clip findable later.",
42789
43256
  required: false
42790
43257
  },
43258
+ source: { type: "string", description: "Provenance (URL mode only): url | youtube", required: false },
43259
+ "external-id": { type: "string", description: "Provider asset id (URL mode only)", required: false },
43260
+ "external-url": { type: "string", description: "Canonical page URL (URL mode only)", required: false },
42791
43261
  "dry-run": { type: "boolean", description: "Preview without executing", required: false, default: false }
42792
43262
  },
42793
43263
  run: async ({ args }) => {
42794
43264
  try {
42795
43265
  const filePath = args.file;
42796
43266
  if (!filePath) {
42797
- writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message: "File path is required" } });
43267
+ writeJson({ ok: false, error: { code: "VALIDATION_ERROR", message: "File path or URL is required" } });
42798
43268
  process.exit(1);
42799
43269
  }
43270
+ if (isRemoteUrl3(filePath)) {
43271
+ await runVideoIngest({
43272
+ url: filePath,
43273
+ source: args.source,
43274
+ externalId: args["external-id"],
43275
+ externalUrl: args["external-url"],
43276
+ context: args.context,
43277
+ dryRun: args["dry-run"] === true
43278
+ });
43279
+ return;
43280
+ }
42800
43281
  const contentType = args["content-type"] || detectContentType(filePath);
42801
43282
  const originalFilename = basename3(filePath);
42802
43283
  const descriptionContext = args.context;
42803
43284
  if (args["dry-run"]) {
42804
- const fileStats = await stat7(filePath);
43285
+ const fileStats = await stat8(filePath);
42805
43286
  writeJson({
42806
43287
  ok: true,
42807
43288
  dryRun: true,
@@ -42845,16 +43326,17 @@ var uploadCommand2 = defineCommand201({
42845
43326
  });
42846
43327
 
42847
43328
  // src/commands/videos/index.ts
42848
- var videosCommand = defineCommand202({
43329
+ var videosCommand = defineCommand203({
42849
43330
  meta: {
42850
43331
  name: "videos",
42851
- description: `Find and manage videos in Baker. Subcommands: search, get, upload, delete, tags.
43332
+ description: `Find and manage videos in Baker. Subcommands: search, get, upload, ingest, delete, tags.
42852
43333
 
42853
43334
  Examples:
42854
43335
  baker videos search "product demo" --limit 5
42855
43336
  baker videos search "tutorial" --tags explainer
42856
43337
  baker videos get <video-id>
42857
43338
  baker videos upload ./demo.mp4
43339
+ baker videos ingest https://www.youtube.com/watch?v=abc123
42858
43340
  baker videos delete <video-id> --dry-run
42859
43341
  baker videos tags
42860
43342
  Full guide: __tooling__/docs/tools/baker/videos.md`
@@ -42864,16 +43346,17 @@ Full guide: __tooling__/docs/tools/baker/videos.md`
42864
43346
  group: groupCommand2,
42865
43347
  search: searchCommand4,
42866
43348
  upload: uploadCommand2,
43349
+ ingest: ingestCommand2,
42867
43350
  delete: deleteCommand3,
42868
43351
  tags: tagsCommand5
42869
43352
  }
42870
43353
  });
42871
43354
 
42872
43355
  // src/commands/winning-ads/index.ts
42873
- import { defineCommand as defineCommand215 } from "citty";
43356
+ import { defineCommand as defineCommand216 } from "citty";
42874
43357
 
42875
43358
  // src/commands/winning-ads/advertisers.ts
42876
- import { defineCommand as defineCommand203 } from "citty";
43359
+ import { defineCommand as defineCommand204 } from "citty";
42877
43360
 
42878
43361
  // src/commands/winning-ads/shared.ts
42879
43362
  function splitList2(value) {
@@ -42926,7 +43409,7 @@ function advertiserNormalizer(record, full) {
42926
43409
  last_synced_at: record.last_synced_at ?? null
42927
43410
  };
42928
43411
  }
42929
- var advertisersCommand2 = defineCommand203({
43412
+ var advertisersCommand2 = defineCommand204({
42930
43413
  meta: {
42931
43414
  name: "advertisers",
42932
43415
  description: 'List corpus advertisers by name or domain. Find your own advertiser for --exclude-advertiser, or a competitor for --advertiser-id / winners. Example: baker winning-ads advertisers "Deel" --output md'
@@ -42984,7 +43467,7 @@ var advertisersCommand2 = defineCommand203({
42984
43467
  });
42985
43468
 
42986
43469
  // src/commands/winning-ads/brief.ts
42987
- import { defineCommand as defineCommand204 } from "citty";
43470
+ import { defineCommand as defineCommand205 } from "citty";
42988
43471
  registerSchema({
42989
43472
  command: "winning-ads.brief",
42990
43473
  description: "Generate a creative brief grounded in strategically-similar winning ads. Optionally describe the target creative with --dna (JSON) and steer with --notes.",
@@ -43030,7 +43513,7 @@ function parseDna(raw) {
43030
43513
  }
43031
43514
  return parsed;
43032
43515
  }
43033
- var briefCommand = defineCommand204({
43516
+ var briefCommand = defineCommand205({
43034
43517
  meta: {
43035
43518
  name: "brief",
43036
43519
  description: `Generate a creative brief from winning references. Example: baker winning-ads brief --dna '{"angle":"cost savings"}' --notes "B2B, LinkedIn video" --k 8`
@@ -43066,7 +43549,7 @@ var briefCommand = defineCommand204({
43066
43549
  });
43067
43550
 
43068
43551
  // src/commands/winning-ads/content.ts
43069
- import { defineCommand as defineCommand205 } from "citty";
43552
+ import { defineCommand as defineCommand206 } from "citty";
43070
43553
  registerSchema({
43071
43554
  command: "winning-ads.content",
43072
43555
  description: "Read what's INSIDE one winning ad: the spoken transcript, the on-screen text, and the ad copy. Use this after `search`/`winners`/`feed` return a shortlist \u2014 pass an ad_id to understand a reference before reproducing it. Add --full for speech, pacing, and soundtrack detail. Video ads carry the transcript/on-screen text; static ads carry only the copy.",
@@ -43079,7 +43562,7 @@ registerSchema({
43079
43562
  }
43080
43563
  }
43081
43564
  });
43082
- var contentCommand = defineCommand205({
43565
+ var contentCommand = defineCommand206({
43083
43566
  meta: {
43084
43567
  name: "content",
43085
43568
  description: "Read the transcript + on-screen text + copy of one winning ad. Example: baker winning-ads content adg_123 --platform meta --full --output md"
@@ -43128,7 +43611,7 @@ var contentCommand = defineCommand205({
43128
43611
  });
43129
43612
 
43130
43613
  // src/commands/winning-ads/feed.ts
43131
- import { defineCommand as defineCommand206 } from "citty";
43614
+ import { defineCommand as defineCommand207 } from "citty";
43132
43615
  function buildFeedParams(input) {
43133
43616
  const params = {};
43134
43617
  const advertiser = splitList2(input.advertiser);
@@ -43180,7 +43663,7 @@ registerSchema({
43180
43663
  format: { type: "string", description: "Comma-separated formats to include (e.g. static,video)", required: false }
43181
43664
  }
43182
43665
  });
43183
- var feedCommand = defineCommand206({
43666
+ var feedCommand = defineCommand207({
43184
43667
  meta: {
43185
43668
  name: "feed",
43186
43669
  description: "Winners across every brand you follow (browse, then trim per advertiser). Example: baker winning-ads feed --per-advertiser 5 --output md"
@@ -43265,7 +43748,7 @@ var feedCommand = defineCommand206({
43265
43748
  });
43266
43749
 
43267
43750
  // src/commands/winning-ads/follow.ts
43268
- import { defineCommand as defineCommand207 } from "citty";
43751
+ import { defineCommand as defineCommand208 } from "citty";
43269
43752
  var PLATFORMS = ["meta", "linkedin"];
43270
43753
  registerSchema({
43271
43754
  command: "winning-ads.follow",
@@ -43280,7 +43763,7 @@ registerSchema({
43280
43763
  label: { type: "string", description: "Optional display label (defaults to the resolved name)", required: false }
43281
43764
  }
43282
43765
  });
43283
- var followCommand = defineCommand207({
43766
+ var followCommand = defineCommand208({
43284
43767
  meta: {
43285
43768
  name: "follow",
43286
43769
  description: 'Follow a brand to track ALL its ads \u2014 every platform and country. --platform is how we read your input, not a limit. A domain tracks both Meta + LinkedIn. Example: baker winning-ads follow "deel.com" --platform meta'
@@ -43327,7 +43810,7 @@ var followCommand = defineCommand207({
43327
43810
  });
43328
43811
 
43329
43812
  // src/commands/winning-ads/follow-competitors.ts
43330
- import { defineCommand as defineCommand208 } from "citty";
43813
+ import { defineCommand as defineCommand209 } from "citty";
43331
43814
  var PLATFORMS2 = ["meta", "linkedin"];
43332
43815
  var BATCH_TIMEOUT_MS = 3e5;
43333
43816
  function buildFollowBatchBody(input) {
@@ -43360,7 +43843,7 @@ registerSchema({
43360
43843
  }
43361
43844
  }
43362
43845
  });
43363
- var followCompetitorsCommand = defineCommand208({
43846
+ var followCompetitorsCommand = defineCommand209({
43364
43847
  meta: {
43365
43848
  name: "follow-competitors",
43366
43849
  description: 'Follow many brands at once by domain \u2014 add every competitor in one call. Example: baker winning-ads follow-competitors "deel.com,notion.so,hubspot.com"'
@@ -43435,7 +43918,7 @@ var followCompetitorsCommand = defineCommand208({
43435
43918
  });
43436
43919
 
43437
43920
  // src/commands/winning-ads/following.ts
43438
- import { defineCommand as defineCommand209 } from "citty";
43921
+ import { defineCommand as defineCommand210 } from "citty";
43439
43922
  registerSchema({
43440
43923
  command: "winning-ads.following",
43441
43924
  description: "List the brands you follow in your ad-dna library, with each one's status (ready vs still adding) and cached ad counts.",
@@ -43468,7 +43951,7 @@ function followingNormalizer(record, full) {
43468
43951
  platforms: Array.isArray(record.platforms) ? record.platforms : []
43469
43952
  };
43470
43953
  }
43471
- var followingCommand = defineCommand209({
43954
+ var followingCommand = defineCommand210({
43472
43955
  meta: {
43473
43956
  name: "following",
43474
43957
  description: "List brands you follow, with status (ready / adding\u2026) and cached counts. Example: baker winning-ads following --output md"
@@ -43503,7 +43986,7 @@ var followingCommand = defineCommand209({
43503
43986
  });
43504
43987
 
43505
43988
  // src/commands/winning-ads/patterns.ts
43506
- import { defineCommand as defineCommand210 } from "citty";
43989
+ import { defineCommand as defineCommand211 } from "citty";
43507
43990
  registerSchema({
43508
43991
  command: "winning-ads.patterns",
43509
43992
  description: "Mine what separates two cohorts of ads: pass a comma-list of winning ad ids (--winners) and a comma-list of weaker ad ids (--duds). Returns the discriminating DNA fields.",
@@ -43542,7 +44025,7 @@ function discriminatorRow(record) {
43542
44025
  top_values_duds: Array.isArray(record.top_values_b) ? record.top_values_b.join(", ") : ""
43543
44026
  };
43544
44027
  }
43545
- var patternsCommand = defineCommand210({
44028
+ var patternsCommand = defineCommand211({
43546
44029
  meta: {
43547
44030
  name: "patterns",
43548
44031
  description: "Discover what separates winning ads from weak ones. Example: baker winning-ads patterns --winners a_1,a_2,a_3 --duds a_9,a_8 --output md"
@@ -43598,7 +44081,7 @@ var patternsCommand = defineCommand210({
43598
44081
  });
43599
44082
 
43600
44083
  // src/commands/winning-ads/search.ts
43601
- import { defineCommand as defineCommand211 } from "citty";
44084
+ import { defineCommand as defineCommand212 } from "citty";
43602
44085
  registerSchema({
43603
44086
  command: "winning-ads.search",
43604
44087
  description: "Search the ad-dna corpus of scored winning ads. Returns a lean shortlist (advertiser, summary, scores, media_url) to pick a reference to reproduce.",
@@ -43706,7 +44189,7 @@ function buildSearchBody2(args) {
43706
44189
  }
43707
44190
  return body;
43708
44191
  }
43709
- var searchCommand5 = defineCommand211({
44192
+ var searchCommand5 = defineCommand212({
43710
44193
  meta: {
43711
44194
  name: "search",
43712
44195
  description: "Search winning reference ads. Example: baker winning-ads search 'B2B SaaS before/after AI automation' --platform meta --format static --winner-category winner --exclude-advertiser adv_123 --output md"
@@ -43821,7 +44304,7 @@ var searchCommand5 = defineCommand211({
43821
44304
  });
43822
44305
 
43823
44306
  // src/commands/winning-ads/seeds.ts
43824
- import { defineCommand as defineCommand212 } from "citty";
44307
+ import { defineCommand as defineCommand213 } from "citty";
43825
44308
  function leanRow(r) {
43826
44309
  return {
43827
44310
  key: r.key,
@@ -43849,7 +44332,7 @@ function makeSeedCommand(opts) {
43849
44332
  limit: { type: "number", description: "Max keys 1-100 (default 20)", required: false, default: 20 }
43850
44333
  }
43851
44334
  });
43852
- return defineCommand212({
44335
+ return defineCommand213({
43853
44336
  meta: { name: opts.name, description: opts.description },
43854
44337
  args: {
43855
44338
  platform: { type: "string", description: "Single platform to segment on", required: false },
@@ -43898,7 +44381,7 @@ var formatsCommand = makeSeedCommand({
43898
44381
  });
43899
44382
 
43900
44383
  // src/commands/winning-ads/unfollow.ts
43901
- import { defineCommand as defineCommand213 } from "citty";
44384
+ import { defineCommand as defineCommand214 } from "citty";
43902
44385
  registerSchema({
43903
44386
  command: "winning-ads.unfollow",
43904
44387
  description: "Stop following a brand \u2014 removes it from your ad-dna library by advertiser id.",
@@ -43906,7 +44389,7 @@ registerSchema({
43906
44389
  advertiser: { type: "string", description: "Advertiser id to unfollow", required: true }
43907
44390
  }
43908
44391
  });
43909
- var unfollowCommand = defineCommand213({
44392
+ var unfollowCommand = defineCommand214({
43910
44393
  meta: {
43911
44394
  name: "unfollow",
43912
44395
  description: "Stop following a brand by advertiser id. Example: baker winning-ads unfollow adv_123"
@@ -43927,7 +44410,7 @@ var unfollowCommand = defineCommand213({
43927
44410
  });
43928
44411
 
43929
44412
  // src/commands/winning-ads/winners.ts
43930
- import { defineCommand as defineCommand214 } from "citty";
44413
+ import { defineCommand as defineCommand215 } from "citty";
43931
44414
  registerSchema({
43932
44415
  command: "winning-ads.winners",
43933
44416
  description: "Top winning ads for one advertiser id (from `advertisers` or `following`). Returns lean winner cards; add --full for DNA + longevity.",
@@ -43937,7 +44420,7 @@ registerSchema({
43937
44420
  platform: { type: "string", description: "Filter to a single platform: meta|linkedin", required: false }
43938
44421
  }
43939
44422
  });
43940
- var winnersCommand = defineCommand214({
44423
+ var winnersCommand = defineCommand215({
43941
44424
  meta: {
43942
44425
  name: "winners",
43943
44426
  description: "Top winning ads for a specific advertiser id. Example: baker winning-ads winners adv_123 --top 15 --output md"
@@ -43987,7 +44470,7 @@ var winnersCommand = defineCommand214({
43987
44470
  });
43988
44471
 
43989
44472
  // src/commands/winning-ads/index.ts
43990
- var winningAdsCommand = defineCommand215({
44473
+ var winningAdsCommand = defineCommand216({
43991
44474
  meta: {
43992
44475
  name: "winning-ads",
43993
44476
  description: `Search the ad-dna corpus of scored "winning" ads for reference creatives to reproduce, and manage the brands your library tracks. Proxied through the Baker backend (BAKER_API_KEY) \u2014 no separate token needed.
@@ -44042,6 +44525,172 @@ Full guide: __tooling__/docs/tools/baker/winning-ads.md`
44042
44525
  }
44043
44526
  });
44044
44527
 
44528
+ // src/unknown-flags.ts
44529
+ var ALWAYS_ACCEPTED = /* @__PURE__ */ new Set(["help", "h", "version"]);
44530
+ function subCommandsOf(command) {
44531
+ const sub = command.subCommands;
44532
+ return sub && typeof sub === "object" ? sub : null;
44533
+ }
44534
+ function argsOf(command) {
44535
+ const args = command.args;
44536
+ return args && typeof args === "object" ? args : null;
44537
+ }
44538
+ function resolveCommand2(root, argv) {
44539
+ let command = root;
44540
+ for (const token of argv) {
44541
+ if (token === "--" || token.startsWith("-")) {
44542
+ break;
44543
+ }
44544
+ const subCommands = subCommandsOf(command);
44545
+ if (!subCommands) {
44546
+ break;
44547
+ }
44548
+ const next = subCommands[token];
44549
+ if (next === void 0) {
44550
+ break;
44551
+ }
44552
+ if (typeof next !== "object") {
44553
+ return { command, resolved: false };
44554
+ }
44555
+ command = next;
44556
+ }
44557
+ return { command, resolved: true };
44558
+ }
44559
+ function typeOf(def) {
44560
+ return def && typeof def === "object" ? def.type : void 0;
44561
+ }
44562
+ function acceptedNames(command) {
44563
+ const args = argsOf(command);
44564
+ if (!args) {
44565
+ return null;
44566
+ }
44567
+ const names = new Set(ALWAYS_ACCEPTED);
44568
+ for (const [name, def] of Object.entries(args)) {
44569
+ if (typeOf(def) === "positional") {
44570
+ continue;
44571
+ }
44572
+ for (const spelling of [name, kebab(name), camel(name)]) {
44573
+ names.add(spelling);
44574
+ if (typeOf(def) === "boolean") {
44575
+ names.add(`no-${spelling}`);
44576
+ }
44577
+ }
44578
+ }
44579
+ return names;
44580
+ }
44581
+ function kebab(name) {
44582
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
44583
+ }
44584
+ function camel(name) {
44585
+ return name.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
44586
+ }
44587
+ function findUnknownFlags(root, argv) {
44588
+ const { command, resolved } = resolveCommand2(root, argv);
44589
+ if (!resolved) {
44590
+ return [];
44591
+ }
44592
+ const accepted = acceptedNames(command);
44593
+ if (!accepted) {
44594
+ return [];
44595
+ }
44596
+ const unknown = [];
44597
+ for (const token of argv) {
44598
+ if (token === "--") {
44599
+ break;
44600
+ }
44601
+ if (!token.startsWith("--")) {
44602
+ continue;
44603
+ }
44604
+ const name = token.slice(2).split("=")[0] ?? "";
44605
+ if (name.length > 0 && !accepted.has(name) && !unknown.includes(token)) {
44606
+ unknown.push(token);
44607
+ }
44608
+ }
44609
+ return unknown;
44610
+ }
44611
+ function closestFlag(unknown, command) {
44612
+ const accepted = acceptedNames(command);
44613
+ if (!accepted) {
44614
+ return null;
44615
+ }
44616
+ const name = unknown.replace(/^--/, "").split("=")[0] ?? "";
44617
+ const scored = [...accepted].filter((candidate) => !ALWAYS_ACCEPTED.has(candidate)).map((candidate) => ({
44618
+ candidate,
44619
+ prefix: sharedPrefix(name, candidate),
44620
+ distance: editDistance(name, candidate)
44621
+ })).filter(({ prefix, distance }) => prefix >= 4 || distance <= 3).sort((a, b) => b.prefix - a.prefix || a.distance - b.distance);
44622
+ return scored[0] ? `--${scored[0].candidate}` : null;
44623
+ }
44624
+ function sharedPrefix(a, b) {
44625
+ let i = 0;
44626
+ while (i < a.length && i < b.length && a[i] === b[i]) {
44627
+ i++;
44628
+ }
44629
+ return i;
44630
+ }
44631
+ function editDistance(a, b) {
44632
+ let previous = Array.from({ length: b.length + 1 }, (_, j) => j);
44633
+ for (let i = 1; i <= a.length; i++) {
44634
+ const current = [i];
44635
+ for (let j = 1; j <= b.length; j++) {
44636
+ const substitution = (previous[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1);
44637
+ current.push(Math.min((previous[j] ?? 0) + 1, (current[j - 1] ?? 0) + 1, substitution));
44638
+ }
44639
+ previous = current;
44640
+ }
44641
+ return previous[b.length] ?? 0;
44642
+ }
44643
+ function commandFor(root, argv) {
44644
+ const { command, resolved } = resolveCommand2(root, argv);
44645
+ return resolved ? command : null;
44646
+ }
44647
+ function unknownFlagEnvelope(unknown, commandPath, suggestion) {
44648
+ const list = unknown.join(", ");
44649
+ return {
44650
+ ok: false,
44651
+ error: {
44652
+ code: "UNKNOWN_ARGUMENT",
44653
+ message: `${list} ${unknown.length > 1 ? "are not options" : "is not an option"} of \`baker ${commandPath}\`.`,
44654
+ fix: {
44655
+ action: "check_schema",
44656
+ explanation: (suggestion ? `Did you mean ${suggestion}? ` : "") + `Run \`baker schema ${commandPath}\` to see every option this command takes. Do not re-run the same call: until this version an unrecognised option was accepted and ignored, so a command that looked like it had filtered or narrowed the result was in fact answering the unfiltered question.`
44657
+ },
44658
+ retryable: false
44659
+ }
44660
+ };
44661
+ }
44662
+ function commandPathOf(root, argv) {
44663
+ const path38 = [];
44664
+ let command = root;
44665
+ for (const token of argv) {
44666
+ if (token === "--" || token.startsWith("-")) {
44667
+ break;
44668
+ }
44669
+ const subCommands = subCommandsOf(command);
44670
+ const next = subCommands?.[token];
44671
+ if (next === void 0 || typeof next !== "object") {
44672
+ break;
44673
+ }
44674
+ path38.push(token);
44675
+ command = next;
44676
+ }
44677
+ return path38.join(" ");
44678
+ }
44679
+ function refuseUnknownFlags(root, argv) {
44680
+ const unknown = findUnknownFlags(root, argv);
44681
+ if (unknown.length === 0) {
44682
+ return;
44683
+ }
44684
+ const command = commandFor(root, argv);
44685
+ const first = unknown[0];
44686
+ const suggestion = command && first ? closestFlag(first, command) : null;
44687
+ process.stdout.write(
44688
+ `${JSON.stringify(unknownFlagEnvelope(unknown, commandPathOf(root, argv), suggestion), null, 2)}
44689
+ `
44690
+ );
44691
+ process.exit(1);
44692
+ }
44693
+
44045
44694
  // src/version.ts
44046
44695
  import { readFileSync as readFileSync14 } from "fs";
44047
44696
  function packageJsonUrl() {
@@ -44059,7 +44708,7 @@ function getCliVersion() {
44059
44708
  }
44060
44709
 
44061
44710
  // src/cli.ts
44062
- var main = defineCommand216({
44711
+ var main = defineCommand217({
44063
44712
  meta: {
44064
44713
  name: "baker",
44065
44714
  version: getCliVersion(),
@@ -44101,5 +44750,6 @@ Introspection: Run 'baker schema <command>' to inspect argument schemas.`
44101
44750
  });
44102
44751
  installStreamTaps();
44103
44752
  logInvocation(process.argv.slice(2));
44753
+ refuseUnknownFlags(main, process.argv.slice(2));
44104
44754
  runMain(main);
44105
44755
  //# sourceMappingURL=cli.js.map