blitzreels 0.1.2 → 0.1.3

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/index.js CHANGED
@@ -9,13 +9,465 @@ const KEYCHAIN_SERVICE = "BlitzReels CLI";
9
9
  const KEYCHAIN_ACCOUNT = "api-key";
10
10
  const DEFAULT_WAIT_INTERVAL_MS = 3000;
11
11
  const DEFAULT_WAIT_TIMEOUT_MS = 20 * 60 * 1000;
12
+ const GLOBAL_FLAGS = ["api-key", "base-url", "json", "help"];
13
+ const TEXT_STYLE_FLAGS = [
14
+ "spec-json",
15
+ "font-family",
16
+ "font-size",
17
+ "font-weight",
18
+ "color",
19
+ "background-color",
20
+ "padding-x",
21
+ "padding-y",
22
+ "border-radius",
23
+ "top",
24
+ "left",
25
+ "text-align",
26
+ "text-shadow",
27
+ "no-background",
28
+ ];
29
+ const CAPTION_WORD_SELECTOR_FLAGS = [
30
+ "word-id",
31
+ "word-ids",
32
+ "timeline-item-id",
33
+ "match-text",
34
+ "match-pattern",
35
+ ];
36
+ const CAPTION_WORD_STYLE_FLAGS = [
37
+ "style-json",
38
+ "font-family",
39
+ "font-weight",
40
+ "font-style",
41
+ "color",
42
+ "active-color",
43
+ "background-color",
44
+ "active-animation",
45
+ "text-transform",
46
+ "text-shadow",
47
+ "glow-color",
48
+ "glow-blur-px",
49
+ "scale",
50
+ "offset-x",
51
+ "offset-y",
52
+ "rotation-deg",
53
+ "letter-spacing",
54
+ "opacity",
55
+ "clear-existing",
56
+ ];
57
+ function commandSpec({ command, usage, flags = [], }) {
58
+ return { command, usage, flags };
59
+ }
60
+ const COMMAND_SPECS = [
61
+ commandSpec({
62
+ command: ["auth", "login"],
63
+ usage: "blitzreels auth login [--no-browser] [--timeout-ms 120000]",
64
+ flags: ["no-browser", "timeout-ms"],
65
+ }),
66
+ commandSpec({
67
+ command: ["auth", "set-key"],
68
+ usage: "blitzreels auth set-key --api-key KEY [--base-url URL]",
69
+ }),
70
+ commandSpec({
71
+ command: ["auth", "whoami"],
72
+ usage: "blitzreels auth whoami [--json]",
73
+ }),
74
+ commandSpec({
75
+ command: ["workspace", "settings", "get"],
76
+ usage: "blitzreels workspace settings get [--json]",
77
+ }),
78
+ commandSpec({
79
+ command: ["workspace", "settings", "update"],
80
+ usage: "blitzreels workspace settings update [--name NAME] [--description TEXT|null] [--protected-words foo,bar] [--icon-storage-key KEY|null] [--aspect-ratio 9:16] [--orientation portrait] [--resolution 1080p] [--frame-rate 30] [--auto-manage-broll true] [--caption-theme-id ID|null]",
81
+ flags: [
82
+ "name",
83
+ "description",
84
+ "protected-words",
85
+ "icon-storage-key",
86
+ "aspect-ratio",
87
+ "orientation",
88
+ "resolution",
89
+ "frame-rate",
90
+ "auto-manage-broll",
91
+ "caption-theme-id",
92
+ ],
93
+ }),
94
+ commandSpec({
95
+ command: ["projects", "list"],
96
+ usage: "blitzreels projects list [--search QUERY] [--status active|archived|all] [--limit 50] [--offset 0] [--cursor CURSOR] [--json]",
97
+ flags: ["search", "status", "limit", "offset", "cursor"],
98
+ }),
99
+ commandSpec({
100
+ command: ["projects", "create"],
101
+ usage: "blitzreels projects create --name NAME [--description TEXT] [--aspect-ratio 9:16] [--project-type video|carousel] [--carousel-settings-json JSON]",
102
+ flags: [
103
+ "name",
104
+ "description",
105
+ "aspect-ratio",
106
+ "project-type",
107
+ "carousel-settings-json",
108
+ ],
109
+ }),
110
+ commandSpec({
111
+ command: ["projects", "get"],
112
+ usage: "blitzreels projects get --project-id ID [--json]",
113
+ flags: ["project-id"],
114
+ }),
115
+ commandSpec({
116
+ command: ["projects", "rename"],
117
+ usage: "blitzreels projects rename --project-id ID --name NAME",
118
+ flags: ["project-id", "name"],
119
+ }),
120
+ commandSpec({
121
+ command: ["projects", "update"],
122
+ usage: "blitzreels projects update --project-id ID [--name NAME] [--description TEXT|null]",
123
+ flags: ["project-id", "name", "description"],
124
+ }),
125
+ commandSpec({
126
+ command: ["projects", "delete"],
127
+ usage: "blitzreels projects delete --project-id ID --confirm-delete",
128
+ flags: ["project-id", "confirm-delete"],
129
+ }),
130
+ commandSpec({
131
+ command: ["media", "list"],
132
+ usage: "blitzreels media list [--folder-id ID] [--asset-type video|audio|image|document] [--search QUERY] [--limit 100] [--offset 0] [--json]",
133
+ flags: ["folder-id", "asset-type", "search", "limit", "offset"],
134
+ }),
135
+ commandSpec({
136
+ command: ["media", "get"],
137
+ usage: "blitzreels media get --asset-id ID [--json]",
138
+ flags: ["asset-id"],
139
+ }),
140
+ commandSpec({
141
+ command: ["media", "delete"],
142
+ usage: "blitzreels media delete --asset-id ID --confirm-delete",
143
+ flags: ["asset-id", "confirm-delete"],
144
+ }),
145
+ commandSpec({
146
+ command: ["media", "upload"],
147
+ usage: "blitzreels media upload --file ./video.mp4 [--name NAME] [--content-type video/mp4] [--auto-process true] [--auto-transcribe true] [--auto-describe true] [--auto-suggest-shorts true]",
148
+ flags: [
149
+ "file",
150
+ "name",
151
+ "content-type",
152
+ "auto-process",
153
+ "auto-transcribe",
154
+ "auto-describe",
155
+ "auto-suggest-shorts",
156
+ ],
157
+ }),
158
+ commandSpec({
159
+ command: ["media", "import-youtube"],
160
+ usage: "blitzreels media import-youtube --url URL [--folder-id ID|root] [--quality 1080p] [--fps 30] [--audio-language fr|null]",
161
+ flags: ["url", "folder-id", "quality", "fps", "audio-language"],
162
+ }),
163
+ commandSpec({
164
+ command: ["media", "rename"],
165
+ usage: "blitzreels media rename --asset-id ID --name NAME",
166
+ flags: ["asset-id", "name"],
167
+ }),
168
+ commandSpec({
169
+ command: ["media", "update"],
170
+ usage: "blitzreels media update --asset-id ID [--name NAME] [--description TEXT|null] [--folder-id ID|root] [--allow-broll true]",
171
+ flags: ["asset-id", "name", "description", "folder-id", "allow-broll"],
172
+ }),
173
+ commandSpec({
174
+ command: ["media", "move"],
175
+ usage: "blitzreels media move --asset-id ID|--asset-ids ID1,ID2 --target-folder-id ID|root",
176
+ flags: ["asset-id", "asset-ids", "target-folder-id"],
177
+ }),
178
+ commandSpec({
179
+ command: ["media", "attach"],
180
+ usage: "blitzreels media attach --project-id ID --asset-id ID [--at 0] [--duration 4] [--layer-index 2] [--allow-duplicate true] [--position-preset center] [--position-x 0] [--position-y 0] [--width-px 320] [--height-px 180] [--scale 1.2] [--opacity 0.9] [--animation-preset popIn]",
181
+ flags: [
182
+ "project-id",
183
+ "asset-id",
184
+ "at",
185
+ "duration",
186
+ "layer-index",
187
+ "allow-duplicate",
188
+ "position-preset",
189
+ "position-x",
190
+ "position-y",
191
+ "width-px",
192
+ "height-px",
193
+ "scale",
194
+ "opacity",
195
+ "animation-preset",
196
+ ],
197
+ }),
198
+ commandSpec({
199
+ command: ["media", "folders", "list"],
200
+ usage: "blitzreels media folders list [--parent-folder-id ID] [--json]",
201
+ flags: ["parent-folder-id"],
202
+ }),
203
+ commandSpec({
204
+ command: ["media", "folders", "create"],
205
+ usage: "blitzreels media folders create --name NAME [--parent-folder-id ID] [--description TEXT|null] [--icon-type video]",
206
+ flags: ["name", "parent-folder-id", "description", "icon-type"],
207
+ }),
208
+ commandSpec({
209
+ command: ["media", "folders", "rename"],
210
+ usage: "blitzreels media folders rename --folder-id ID --name NAME",
211
+ flags: ["folder-id", "name"],
212
+ }),
213
+ commandSpec({
214
+ command: ["media", "folders", "update"],
215
+ usage: "blitzreels media folders update --folder-id ID [--name NAME] [--parent-folder-id ID|root] [--description TEXT|null] [--icon-type video]",
216
+ flags: [
217
+ "folder-id",
218
+ "name",
219
+ "parent-folder-id",
220
+ "description",
221
+ "icon-type",
222
+ ],
223
+ }),
224
+ commandSpec({
225
+ command: ["media", "folders", "delete"],
226
+ usage: "blitzreels media folders delete --folder-id ID --confirm-delete [--cascade true]",
227
+ flags: ["folder-id", "confirm-delete", "cascade"],
228
+ }),
229
+ commandSpec({
230
+ command: ["project", "inspect"],
231
+ usage: "blitzreels project inspect --project-id ID [--mode full] [--json]",
232
+ flags: ["project-id", "mode"],
233
+ }),
234
+ commandSpec({
235
+ command: ["project", "timeline"],
236
+ usage: "blitzreels project timeline --project-id ID [--json]",
237
+ flags: ["project-id"],
238
+ }),
239
+ commandSpec({
240
+ command: ["project", "snapshot"],
241
+ usage: "blitzreels project snapshot --project-id ID --at 00:03 [--target-width 608] [--image-format jpeg|png|webp] [--json]",
242
+ flags: ["project-id", "at", "target-width", "image-format"],
243
+ }),
244
+ commandSpec({
245
+ command: ["project", "snapshots"],
246
+ usage: "blitzreels project snapshots --project-id ID --at 00:01,00:03 [--target-width 608] [--image-format jpeg|png|webp] [--json]",
247
+ flags: ["project-id", "at", "target-width", "image-format"],
248
+ }),
249
+ commandSpec({
250
+ command: ["timeline", "transform"],
251
+ usage: "blitzreels timeline transform --project-id ID --timeline-item-id ID [--position-x 0] [--position-y 0] [--width-px 320|null] [--height-px 180|null] [--keep-aspect-ratio true]",
252
+ flags: [
253
+ "project-id",
254
+ "timeline-item-id",
255
+ "position-x",
256
+ "position-y",
257
+ "width-px",
258
+ "height-px",
259
+ "keep-aspect-ratio",
260
+ ],
261
+ }),
262
+ commandSpec({
263
+ command: ["timeline", "move"],
264
+ usage: "blitzreels timeline move --project-id ID --timeline-item-id ID --at 00:03 [--layer-index 2]",
265
+ flags: ["project-id", "timeline-item-id", "at", "layer-index"],
266
+ }),
267
+ commandSpec({
268
+ command: ["timeline", "delete"],
269
+ usage: "blitzreels timeline delete --project-id ID --timeline-item-id ID|--timeline-item-ids ID1,ID2 [--auto-pack true]",
270
+ flags: ["project-id", "timeline-item-id", "timeline-item-ids", "auto-pack"],
271
+ }),
272
+ commandSpec({
273
+ command: ["text", "add"],
274
+ usage: "blitzreels text add --project-id ID --text \"Hook\" [--name NAME] [--at 0] [--duration 4] [--layer-index 2] [--spec-json JSON] [--font-family Inter] [--font-size 64] [--font-weight 700] [--color '#fff'] [--background-color '#000'] [--padding-x 32] [--padding-y 16] [--border-radius 24] [--top '18%'] [--left '50%'] [--text-align center] [--text-shadow '0 6px 18px #000'] [--no-background]",
275
+ flags: [
276
+ "project-id",
277
+ "text",
278
+ "name",
279
+ "at",
280
+ "duration",
281
+ "layer-index",
282
+ ...TEXT_STYLE_FLAGS,
283
+ ],
284
+ }),
285
+ commandSpec({
286
+ command: ["text", "list"],
287
+ usage: "blitzreels text list --project-id ID [--json]",
288
+ flags: ["project-id"],
289
+ }),
290
+ commandSpec({
291
+ command: ["text", "update"],
292
+ usage: "blitzreels text update --project-id ID --content-item-id ID [--text \"New hook\"] [--name NAME] [--spec-json JSON] [--font-family Pacifico] [--font-size 82] [--font-weight 700] [--color '#17FFA6'] [--background-color '#051914'] [--padding-x 32] [--padding-y 16] [--border-radius 24] [--top '18%'] [--left '50%'] [--text-align center] [--text-shadow '0 6px 18px #000'] [--no-background]",
293
+ flags: [
294
+ "project-id",
295
+ "content-item-id",
296
+ "text",
297
+ "name",
298
+ ...TEXT_STYLE_FLAGS,
299
+ ],
300
+ }),
301
+ commandSpec({
302
+ command: ["text", "remove"],
303
+ usage: "blitzreels text remove --project-id ID --content-item-id ID",
304
+ flags: ["project-id", "content-item-id"],
305
+ }),
306
+ commandSpec({
307
+ command: ["audio", "add"],
308
+ usage: "blitzreels audio add --project-id ID --asset-id ID [--at 0] [--duration 30] [--volume 0.6] [--fade-in 0.5] [--fade-out 0.5] [--loop true]",
309
+ flags: [
310
+ "project-id",
311
+ "asset-id",
312
+ "at",
313
+ "duration",
314
+ "volume",
315
+ "fade-in",
316
+ "fade-out",
317
+ "loop",
318
+ ],
319
+ }),
320
+ commandSpec({
321
+ command: ["silence", "plan"],
322
+ usage: "blitzreels silence plan --project-id ID --timeline-item-id ID [--padding-ms 120] [--silence-threshold-db -35] [--min-silence-duration 0.25] [--remove-blips-max-duration 0.12] [--use-caption-validation true] [--json]",
323
+ flags: [
324
+ "project-id",
325
+ "timeline-item-id",
326
+ "padding-ms",
327
+ "silence-threshold-db",
328
+ "min-silence-duration",
329
+ "remove-blips-max-duration",
330
+ "use-caption-validation",
331
+ ],
332
+ }),
333
+ commandSpec({
334
+ command: ["silence", "apply"],
335
+ usage: "blitzreels silence apply --project-id ID --timeline-item-id ID --confirm-preview [--padding-ms 120] [--silence-threshold-db -35] [--min-silence-duration 0.25] [--remove-blips-max-duration 0.12] [--use-caption-validation true] [--delete-silent-clips true] [--manual-segment-overrides-json JSON]",
336
+ flags: [
337
+ "project-id",
338
+ "timeline-item-id",
339
+ "confirm-preview",
340
+ "padding-ms",
341
+ "silence-threshold-db",
342
+ "min-silence-duration",
343
+ "remove-blips-max-duration",
344
+ "use-caption-validation",
345
+ "delete-silent-clips",
346
+ "manual-segment-overrides-json",
347
+ ],
348
+ }),
349
+ commandSpec({
350
+ command: ["mistakes", "plan"],
351
+ usage: "blitzreels mistakes plan --project-id ID --timeline-item-id ID [--json]",
352
+ flags: ["project-id", "timeline-item-id"],
353
+ }),
354
+ commandSpec({
355
+ command: ["mistakes", "apply"],
356
+ usage: "blitzreels mistakes apply --project-id ID --timeline-item-id ID --confirm-preview [--plan-json JSON] [--manual-verdicts-json JSON] [--delete-mistakes true]",
357
+ flags: [
358
+ "project-id",
359
+ "timeline-item-id",
360
+ "confirm-preview",
361
+ "plan-json",
362
+ "manual-verdicts-json",
363
+ "delete-mistakes",
364
+ ],
365
+ }),
366
+ commandSpec({
367
+ command: ["captions", "words", "list"],
368
+ usage: "blitzreels captions words list --project-id ID [--timeline-item-id ID] [--match-text TEXT] [--limit 100] [--offset 0]",
369
+ flags: ["project-id", "timeline-item-id", "match-text", "limit", "offset"],
370
+ }),
371
+ commandSpec({
372
+ command: ["captions", "words", "text"],
373
+ usage: 'blitzreels captions words text --project-id ID --word-id ID --text "Fix"|--new-text "Fix"',
374
+ flags: ["project-id", "word-id", "text", "new-text"],
375
+ }),
376
+ commandSpec({
377
+ command: ["captions", "words", "batch-text"],
378
+ usage: 'blitzreels captions words batch-text --project-id ID --updates-json \'[{"word_id":"...","new_text":"..."}]\'',
379
+ flags: ["project-id", "updates-json"],
380
+ }),
381
+ commandSpec({
382
+ command: ["captions", "words", "style"],
383
+ usage: "blitzreels captions words style --project-id ID [--word-id ID|--word-ids IDs|--timeline-item-id ID|--match-text TEXT|--match-pattern numbers|caps|exclamations] [--style-json JSON|null] [--font-family Pacifico] [--font-weight 700] [--font-style italic] [--color '#17FFA6'] [--active-color '#fff'] [--background-color '#051914'] [--active-animation pop] [--text-transform uppercase] [--text-shadow '0 6px 18px #000'] [--glow-color '#17FFA6'] [--glow-blur-px 18] [--scale 1.45] [--offset-x 0] [--offset-y -8] [--rotation-deg -2] [--letter-spacing 1] [--opacity 1] [--clear-existing true]",
384
+ flags: [
385
+ "project-id",
386
+ ...CAPTION_WORD_SELECTOR_FLAGS,
387
+ ...CAPTION_WORD_STYLE_FLAGS,
388
+ ],
389
+ }),
390
+ commandSpec({
391
+ command: ["captions", "words", "emphasis"],
392
+ usage: "blitzreels captions words emphasis --project-id ID [--word-id ID|--word-ids IDs|--timeline-item-id ID|--match-text TEXT|--match-pattern numbers|caps|exclamations] [--emphasis true]",
393
+ flags: ["project-id", ...CAPTION_WORD_SELECTOR_FLAGS, "emphasis"],
394
+ }),
395
+ commandSpec({
396
+ command: ["captions", "words", "delete"],
397
+ usage: "blitzreels captions words delete --project-id ID --word-id ID|--word-ids ID1,ID2",
398
+ flags: ["project-id", "word-id", "word-ids"],
399
+ }),
400
+ commandSpec({
401
+ command: ["captions", "words", "merge"],
402
+ usage: 'blitzreels captions words merge --project-id ID --word-ids ID1,ID2 [--text "Merged"]',
403
+ flags: ["project-id", "word-ids", "text"],
404
+ }),
405
+ commandSpec({
406
+ command: ["captions", "words", "split"],
407
+ usage: 'blitzreels captions words split --project-id ID --word-id ID --words "foo,bar"',
408
+ flags: ["project-id", "word-id", "words"],
409
+ }),
410
+ commandSpec({
411
+ command: ["captions", "themes", "list"],
412
+ usage: "blitzreels captions themes list [--json]",
413
+ }),
414
+ commandSpec({
415
+ command: ["captions", "theme", "set"],
416
+ usage: "blitzreels captions theme set --project-id ID --theme-id ID",
417
+ flags: ["project-id", "theme-id"],
418
+ }),
419
+ commandSpec({
420
+ command: ["captions", "theme", "set-default"],
421
+ usage: "blitzreels captions theme set-default --theme-id ID",
422
+ flags: ["theme-id"],
423
+ }),
424
+ commandSpec({
425
+ command: ["exports", "start"],
426
+ usage: "blitzreels exports start --project-id ID [--resolution 1080p] [--format mp4] [--include-captions true]",
427
+ flags: ["project-id", "resolution", "format", "include-captions"],
428
+ }),
429
+ commandSpec({
430
+ command: ["exports", "wait"],
431
+ usage: "blitzreels exports wait --export-id ID [--interval-ms 3000] [--timeout-ms 1200000] [--json]",
432
+ flags: ["export-id", "interval-ms", "timeout-ms"],
433
+ }),
434
+ commandSpec({
435
+ command: ["exports", "download"],
436
+ usage: "blitzreels exports download --export-id ID --output ./short.mp4",
437
+ flags: ["export-id", "output"],
438
+ }),
439
+ commandSpec({
440
+ command: ["api", "docs"],
441
+ usage: "blitzreels api docs",
442
+ }),
443
+ commandSpec({
444
+ command: ["api", "spec"],
445
+ usage: "blitzreels api spec",
446
+ }),
447
+ ];
448
+ class CliUsageError extends Error {
449
+ code;
450
+ suggestions;
451
+ usage;
452
+ constructor({ code, message, suggestions = [], usage, }) {
453
+ super(message);
454
+ this.name = "CliUsageError";
455
+ this.code = code;
456
+ this.suggestions = suggestions;
457
+ this.usage = usage;
458
+ }
459
+ }
12
460
  async function main() {
13
461
  const parsed = parseArgs({ argv: process.argv.slice(2) });
14
- if (parsed.command.length === 0 ||
15
- getBooleanFlag({ flags: parsed.flags, name: "help" })) {
462
+ if (parsed.command.length === 0) {
16
463
  printHelp();
17
464
  return;
18
465
  }
466
+ if (getBooleanFlag({ flags: parsed.flags, name: "help" })) {
467
+ printCommandHelpOrThrow({ command: parsed.command });
468
+ return;
469
+ }
470
+ validateParsedArgs({ parsed });
19
471
  const config = await loadConfig();
20
472
  const apiKey = getStringFlag({ flags: parsed.flags, name: "api-key" }) ??
21
473
  process.env.BLITZREELS_API_KEY ??
@@ -408,7 +860,55 @@ async function runCommand({ context }) {
408
860
  flags: context.args.flags,
409
861
  name: "allow-duplicate",
410
862
  }),
863
+ positionPreset: parseMediaPositionPreset({
864
+ value: getStringFlag({
865
+ flags: context.args.flags,
866
+ name: "position-preset",
867
+ }),
868
+ }),
869
+ positionX: getNumberFlag({
870
+ flags: context.args.flags,
871
+ name: "position-x",
872
+ }),
873
+ positionY: getNumberFlag({
874
+ flags: context.args.flags,
875
+ name: "position-y",
876
+ }),
877
+ widthPx: getNumberFlag({
878
+ flags: context.args.flags,
879
+ name: "width-px",
880
+ }),
881
+ heightPx: getNumberFlag({
882
+ flags: context.args.flags,
883
+ name: "height-px",
884
+ }),
885
+ scale: getNumberFlag({ flags: context.args.flags, name: "scale" }),
886
+ opacity: getNumberFlag({ flags: context.args.flags, name: "opacity" }),
887
+ animationPreset: parseMediaAnimationPreset({
888
+ value: getStringFlag({
889
+ flags: context.args.flags,
890
+ name: "animation-preset",
891
+ }),
892
+ }),
411
893
  });
894
+ const requestedLayer = getNumberFlag({
895
+ flags: context.args.flags,
896
+ name: "layer-index",
897
+ });
898
+ if (requestedLayer !== undefined) {
899
+ await Promise.all(result.inserted
900
+ .map((item) => item.timeline_item_id)
901
+ .filter((item) => typeof item === "string")
902
+ .map((timelineItemId) => context.client.timeline.move({
903
+ projectId: requireStringFlag({
904
+ flags: context.args.flags,
905
+ name: "project-id",
906
+ }),
907
+ timelineItemId,
908
+ newStartSeconds: result.inserted.find((item) => item.timeline_item_id === timelineItemId)?.start_seconds ?? 0,
909
+ newLayerIndex: requestedLayer,
910
+ })));
911
+ }
412
912
  printResult({
413
913
  json: context.json,
414
914
  value: result,
@@ -600,21 +1100,33 @@ async function runCommand({ context }) {
600
1100
  return;
601
1101
  }
602
1102
  if (group === "text" && action === "add") {
1103
+ const text = requireStringFlag({ flags: context.args.flags, name: "text" });
1104
+ const durationSeconds = getNumberFlag({ flags: context.args.flags, name: "duration" }) ?? 4;
1105
+ const spec = parseJsonObjectFlag({ flags: context.args.flags, name: "spec-json" }) ??
1106
+ (hasTextStyleFlags({ flags: context.args.flags })
1107
+ ? buildTextOverlaySpec({
1108
+ text,
1109
+ name: getStringFlag({ flags: context.args.flags, name: "name" }),
1110
+ durationSeconds,
1111
+ flags: context.args.flags,
1112
+ })
1113
+ : undefined);
603
1114
  const result = await context.client.text.add({
604
1115
  projectId: requireStringFlag({
605
1116
  flags: context.args.flags,
606
1117
  name: "project-id",
607
1118
  }),
608
- text: requireStringFlag({ flags: context.args.flags, name: "text" }),
1119
+ text,
609
1120
  name: getStringFlag({ flags: context.args.flags, name: "name" }),
610
1121
  startSeconds: parseTimecode({
611
1122
  value: getStringFlag({ flags: context.args.flags, name: "at" }) ?? "0",
612
1123
  }),
613
- durationSeconds: getNumberFlag({ flags: context.args.flags, name: "duration" }) ?? 4,
1124
+ durationSeconds,
614
1125
  layerIndex: getNumberFlag({
615
1126
  flags: context.args.flags,
616
1127
  name: "layer-index",
617
1128
  }),
1129
+ spec,
618
1130
  });
619
1131
  printResult({
620
1132
  json: context.json,
@@ -656,6 +1168,29 @@ async function runCommand({ context }) {
656
1168
  return;
657
1169
  }
658
1170
  if (group === "text" && action === "update") {
1171
+ const explicitSpec = parseJsonObjectFlag({
1172
+ flags: context.args.flags,
1173
+ name: "spec-json",
1174
+ });
1175
+ const spec = explicitSpec ??
1176
+ (hasTextStyleFlags({ flags: context.args.flags })
1177
+ ? await buildUpdatedTextSpec({
1178
+ context,
1179
+ projectId: requireStringFlag({
1180
+ flags: context.args.flags,
1181
+ name: "project-id",
1182
+ }),
1183
+ contentItemId: requireStringFlag({
1184
+ flags: context.args.flags,
1185
+ name: "content-item-id",
1186
+ }),
1187
+ nextText: getStringFlag({
1188
+ flags: context.args.flags,
1189
+ name: "text",
1190
+ }),
1191
+ flags: context.args.flags,
1192
+ })
1193
+ : undefined);
659
1194
  const result = await context.client.text.update({
660
1195
  projectId: requireStringFlag({
661
1196
  flags: context.args.flags,
@@ -667,15 +1202,94 @@ async function runCommand({ context }) {
667
1202
  }),
668
1203
  text: getStringFlag({ flags: context.args.flags, name: "text" }),
669
1204
  name: getStringFlag({ flags: context.args.flags, name: "name" }),
670
- spec: parseJsonObjectFlag({
1205
+ spec,
1206
+ });
1207
+ printResult({
1208
+ json: context.json,
1209
+ value: result,
1210
+ human: ["Text updated."],
1211
+ });
1212
+ return;
1213
+ }
1214
+ if (group === "timeline" && action === "transform") {
1215
+ const result = await context.client.timeline.transform({
1216
+ projectId: requireStringFlag({
1217
+ flags: context.args.flags,
1218
+ name: "project-id",
1219
+ }),
1220
+ timelineItemId: requireStringFlag({
1221
+ flags: context.args.flags,
1222
+ name: "timeline-item-id",
1223
+ }),
1224
+ positionX: getNumberFlag({
1225
+ flags: context.args.flags,
1226
+ name: "position-x",
1227
+ }),
1228
+ positionY: getNumberFlag({
1229
+ flags: context.args.flags,
1230
+ name: "position-y",
1231
+ }),
1232
+ widthPx: parseNullableNumberFlag({
1233
+ flags: context.args.flags,
1234
+ name: "width-px",
1235
+ }),
1236
+ heightPx: parseNullableNumberFlag({
1237
+ flags: context.args.flags,
1238
+ name: "height-px",
1239
+ }),
1240
+ keepAspectRatio: getOptionalBooleanFlag({
671
1241
  flags: context.args.flags,
672
- name: "spec-json",
1242
+ name: "keep-aspect-ratio",
673
1243
  }),
674
1244
  });
675
1245
  printResult({
676
1246
  json: context.json,
677
1247
  value: result,
678
- human: ["Text updated."],
1248
+ human: [JSON.stringify(result, null, 2)],
1249
+ });
1250
+ return;
1251
+ }
1252
+ if (group === "timeline" && action === "move") {
1253
+ const result = await context.client.timeline.move({
1254
+ projectId: requireStringFlag({
1255
+ flags: context.args.flags,
1256
+ name: "project-id",
1257
+ }),
1258
+ timelineItemId: requireStringFlag({
1259
+ flags: context.args.flags,
1260
+ name: "timeline-item-id",
1261
+ }),
1262
+ newStartSeconds: parseTimecode({
1263
+ value: requireStringFlag({ flags: context.args.flags, name: "at" }),
1264
+ }),
1265
+ newLayerIndex: getNumberFlag({
1266
+ flags: context.args.flags,
1267
+ name: "layer-index",
1268
+ }),
1269
+ });
1270
+ printResult({
1271
+ json: context.json,
1272
+ value: result,
1273
+ human: [JSON.stringify(result, null, 2)],
1274
+ });
1275
+ return;
1276
+ }
1277
+ if (group === "timeline" && action === "delete") {
1278
+ const result = await context.client.timeline.delete({
1279
+ projectId: requireStringFlag({
1280
+ flags: context.args.flags,
1281
+ name: "project-id",
1282
+ }),
1283
+ timelineItemIds: parseTimelineItemIds({ flags: context.args.flags }),
1284
+ autoPack: getOptionalBooleanFlag({
1285
+ flags: context.args.flags,
1286
+ name: "auto-pack",
1287
+ }),
1288
+ });
1289
+ printResult({
1290
+ json: context.json,
1291
+ value: result,
1292
+ human: [JSON.stringify(result, null, 2)],
679
1293
  });
680
1294
  return;
681
1295
  }
@@ -850,6 +1464,154 @@ async function runCommand({ context }) {
850
1464
  });
851
1465
  return;
852
1466
  }
1467
+ if (group === "captions" && action === "words" && subaction === "list") {
1468
+ const result = await context.client.captions.listWords({
1469
+ projectId: requireStringFlag({
1470
+ flags: context.args.flags,
1471
+ name: "project-id",
1472
+ }),
1473
+ timelineItemId: getStringFlag({
1474
+ flags: context.args.flags,
1475
+ name: "timeline-item-id",
1476
+ }),
1477
+ matchText: getStringFlag({
1478
+ flags: context.args.flags,
1479
+ name: "match-text",
1480
+ }),
1481
+ limit: getNumberFlag({ flags: context.args.flags, name: "limit" }),
1482
+ offset: getNumberFlag({ flags: context.args.flags, name: "offset" }),
1483
+ });
1484
+ printResult({
1485
+ json: context.json,
1486
+ value: result,
1487
+ human: result.words.map((word) => JSON.stringify(word)),
1488
+ });
1489
+ return;
1490
+ }
1491
+ if (group === "captions" && action === "words" && subaction === "text") {
1492
+ const result = await context.client.captions.updateWordText({
1493
+ projectId: requireStringFlag({
1494
+ flags: context.args.flags,
1495
+ name: "project-id",
1496
+ }),
1497
+ wordId: requireStringFlag({ flags: context.args.flags, name: "word-id" }),
1498
+ newText: getStringFlag({ flags: context.args.flags, name: "new-text" }) ??
1499
+ requireStringFlag({ flags: context.args.flags, name: "text" }),
1500
+ });
1501
+ printResult({
1502
+ json: context.json,
1503
+ value: result,
1504
+ human: [
1505
+ `Updated ${result.word_id}: ${result.old_text} -> ${result.new_text}`,
1506
+ ],
1507
+ });
1508
+ return;
1509
+ }
1510
+ if (group === "captions" &&
1511
+ action === "words" &&
1512
+ subaction === "batch-text") {
1513
+ const result = await context.client.captions.batchUpdateWordText({
1514
+ projectId: requireStringFlag({
1515
+ flags: context.args.flags,
1516
+ name: "project-id",
1517
+ }),
1518
+ updates: parseWordTextUpdatesFlag({
1519
+ flags: context.args.flags,
1520
+ name: "updates-json",
1521
+ }),
1522
+ });
1523
+ printResult({
1524
+ json: context.json,
1525
+ value: result,
1526
+ human: [result.message],
1527
+ });
1528
+ return;
1529
+ }
1530
+ if (group === "captions" && action === "words" && subaction === "style") {
1531
+ const styleJson = parseNullableJsonObjectFlag({
1532
+ flags: context.args.flags,
1533
+ name: "style-json",
1534
+ });
1535
+ const style = styleJson === undefined
1536
+ ? buildCaptionWordStyleFromFlags({ flags: context.args.flags })
1537
+ : styleJson;
1538
+ const result = await context.client.captions.setWordStyle({
1539
+ ...buildCaptionWordSelector({ flags: context.args.flags }),
1540
+ style,
1541
+ clearExisting: getOptionalBooleanFlag({
1542
+ flags: context.args.flags,
1543
+ name: "clear-existing",
1544
+ }),
1545
+ });
1546
+ printResult({
1547
+ json: context.json,
1548
+ value: result,
1549
+ human: [`Updated ${result.updated_count} caption word(s).`],
1550
+ });
1551
+ return;
1552
+ }
1553
+ if (group === "captions" && action === "words" && subaction === "emphasis") {
1554
+ const result = await context.client.captions.setWordEmphasis({
1555
+ ...buildCaptionWordSelector({ flags: context.args.flags }),
1556
+ emphasis: getOptionalBooleanFlag({
1557
+ flags: context.args.flags,
1558
+ name: "emphasis",
1559
+ }) ?? true,
1560
+ });
1561
+ printResult({
1562
+ json: context.json,
1563
+ value: result,
1564
+ human: [`Updated ${result.updated_count} caption word(s).`],
1565
+ });
1566
+ return;
1567
+ }
1568
+ if (group === "captions" && action === "words" && subaction === "delete") {
1569
+ const result = await context.client.captions.deleteWords({
1570
+ projectId: requireStringFlag({
1571
+ flags: context.args.flags,
1572
+ name: "project-id",
1573
+ }),
1574
+ wordIds: parseWordIds({ flags: context.args.flags }),
1575
+ });
1576
+ printResult({
1577
+ json: context.json,
1578
+ value: result,
1579
+ human: [result.message],
1580
+ });
1581
+ return;
1582
+ }
1583
+ if (group === "captions" && action === "words" && subaction === "merge") {
1584
+ const result = await context.client.captions.mergeWords({
1585
+ projectId: requireStringFlag({
1586
+ flags: context.args.flags,
1587
+ name: "project-id",
1588
+ }),
1589
+ wordIds: parseWordIds({ flags: context.args.flags }),
1590
+ text: getStringFlag({ flags: context.args.flags, name: "text" }),
1591
+ });
1592
+ printResult({
1593
+ json: context.json,
1594
+ value: result,
1595
+ human: [result.message],
1596
+ });
1597
+ return;
1598
+ }
1599
+ if (group === "captions" && action === "words" && subaction === "split") {
1600
+ const result = await context.client.captions.splitWord({
1601
+ projectId: requireStringFlag({
1602
+ flags: context.args.flags,
1603
+ name: "project-id",
1604
+ }),
1605
+ wordId: requireStringFlag({ flags: context.args.flags, name: "word-id" }),
1606
+ words: parseCsvRequiredFlag({ flags: context.args.flags, name: "words" }),
1607
+ });
1608
+ printResult({
1609
+ json: context.json,
1610
+ value: result,
1611
+ human: [result.message],
1612
+ });
1613
+ return;
1614
+ }
853
1615
  if (group === "captions" && action === "themes" && subaction === "list") {
854
1616
  const result = await context.client.captions.listThemes();
855
1617
  printResult({
@@ -955,7 +1717,7 @@ async function runCommand({ context }) {
955
1717
  });
956
1718
  return;
957
1719
  }
958
- throw new Error(`Unknown command: ${context.args.command.join(" ")}`);
1720
+ throw buildUnknownCommandError({ command: context.args.command });
959
1721
  }
960
1722
  async function setApiKey({ context }) {
961
1723
  const apiKey = getStringFlag({ flags: context.args.flags, name: "api-key" }) ??
@@ -1228,6 +1990,118 @@ function parseArgs({ argv }) {
1228
1990
  }
1229
1991
  return { command, flags };
1230
1992
  }
1993
+ function validateParsedArgs({ parsed }) {
1994
+ const spec = findCommandSpec({ command: parsed.command });
1995
+ if (!spec) {
1996
+ throw buildUnknownCommandError({ command: parsed.command });
1997
+ }
1998
+ const allowedFlags = new Set([...GLOBAL_FLAGS, ...spec.flags]);
1999
+ for (const flag of Object.keys(parsed.flags)) {
2000
+ if (!allowedFlags.has(flag)) {
2001
+ throw buildUnknownFlagError({ spec, flag });
2002
+ }
2003
+ }
2004
+ }
2005
+ function findCommandSpec({ command, }) {
2006
+ return COMMAND_SPECS.find((spec) => commandsEqual(spec.command, command));
2007
+ }
2008
+ function commandsEqual(left, right) {
2009
+ return (left.length === right.length &&
2010
+ left.every((part, index) => part === right[index]));
2011
+ }
2012
+ function commandLabel({ command }) {
2013
+ return command.join(" ");
2014
+ }
2015
+ function printCommandHelpOrThrow({ command }) {
2016
+ const spec = findCommandSpec({ command });
2017
+ if (!spec) {
2018
+ throw buildUnknownCommandError({ command });
2019
+ }
2020
+ printCommandHelp({ spec });
2021
+ }
2022
+ function printCommandHelp({ spec }) {
2023
+ const flags = [...new Set([...spec.flags, ...GLOBAL_FLAGS])].sort();
2024
+ console.log(`Usage:
2025
+ ${spec.usage}
2026
+
2027
+ Flags:
2028
+ ${flags.map((flag) => ` --${flag}`).join("\n")}
2029
+ `);
2030
+ }
2031
+ function buildUnknownCommandError({ command, }) {
2032
+ const suggestions = suggestCommands({ command }).map((spec) => spec.usage);
2033
+ return new CliUsageError({
2034
+ code: "unknown_command",
2035
+ message: `Unknown command: ${commandLabel({ command })}`,
2036
+ suggestions,
2037
+ usage: suggestions[0],
2038
+ });
2039
+ }
2040
+ function buildUnknownFlagError({ spec, flag, }) {
2041
+ const flags = [...new Set([...spec.flags, ...GLOBAL_FLAGS])];
2042
+ const suggestions = suggestValues({ value: flag, candidates: flags }).map((candidate) => `--${candidate}`);
2043
+ return new CliUsageError({
2044
+ code: "unknown_flag",
2045
+ message: `Unknown flag --${flag} for \`${commandLabel({ command: spec.command })}\`.`,
2046
+ suggestions,
2047
+ usage: spec.usage,
2048
+ });
2049
+ }
2050
+ function suggestCommands({ command }) {
2051
+ const joined = commandLabel({ command });
2052
+ const prefixMatches = COMMAND_SPECS.filter((spec) => commandsEqual(spec.command.slice(0, command.length), command));
2053
+ if (prefixMatches.length > 0) {
2054
+ return prefixMatches.slice(0, 3);
2055
+ }
2056
+ return COMMAND_SPECS.map((spec) => ({
2057
+ spec,
2058
+ score: commandSimilarity({
2059
+ left: joined,
2060
+ right: commandLabel({ command: spec.command }),
2061
+ }),
2062
+ }))
2063
+ .filter((item) => item.score >= 0.45)
2064
+ .sort((left, right) => right.score - left.score)
2065
+ .slice(0, 3)
2066
+ .map((item) => item.spec);
2067
+ }
2068
+ function suggestValues({ value, candidates, }) {
2069
+ return candidates
2070
+ .map((candidate) => ({
2071
+ candidate,
2072
+ score: commandSimilarity({ left: value, right: candidate }),
2073
+ }))
2074
+ .filter((item) => item.score >= 0.45)
2075
+ .sort((left, right) => right.score - left.score)
2076
+ .slice(0, 3)
2077
+ .map((item) => item.candidate);
2078
+ }
2079
+ function commandSimilarity({ left, right, }) {
2080
+ const maxLength = Math.max(left.length, right.length);
2081
+ if (maxLength === 0) {
2082
+ return 1;
2083
+ }
2084
+ return 1 - levenshteinDistance({ left, right }) / maxLength;
2085
+ }
2086
+ function levenshteinDistance({ left, right, }) {
2087
+ const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
2088
+ const current = Array.from({ length: right.length + 1 }, () => 0);
2089
+ for (let leftIndex = 1; leftIndex <= left.length; leftIndex++) {
2090
+ current[0] = leftIndex;
2091
+ for (let rightIndex = 1; rightIndex <= right.length; rightIndex++) {
2092
+ const substitutionCost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
2093
+ current[rightIndex] = Math.min((current[rightIndex - 1] ?? 0) + 1, (previous[rightIndex] ?? 0) + 1, (previous[rightIndex - 1] ?? 0) + substitutionCost);
2094
+ }
2095
+ for (let index = 0; index < previous.length; index++) {
2096
+ previous[index] = current[index] ?? 0;
2097
+ }
2098
+ }
2099
+ return previous[right.length] ?? 0;
2100
+ }
2101
+ function usageForCurrentCommand() {
2102
+ const parsed = parseArgs({ argv: process.argv.slice(2) });
2103
+ return findCommandSpec({ command: parsed.command })?.usage;
2104
+ }
1231
2105
  function getStringFlag({ flags, name, }) {
1232
2106
  const value = flags[name];
1233
2107
  return typeof value === "string" ? value : undefined;
@@ -1259,6 +2133,20 @@ function getNumberFlag({ flags, name, }) {
1259
2133
  }
1260
2134
  return parsed;
1261
2135
  }
2136
+ function parseNullableNumberFlag({ flags, name, }) {
2137
+ const value = getStringFlag({ flags, name });
2138
+ if (value === undefined) {
2139
+ return undefined;
2140
+ }
2141
+ if (value === "null") {
2142
+ return null;
2143
+ }
2144
+ const parsed = Number(value);
2145
+ if (!Number.isFinite(parsed)) {
2146
+ throw new Error(`--${name} must be a number or null.`);
2147
+ }
2148
+ return parsed;
2149
+ }
1262
2150
  function parseJsonObjectFlag({ flags, name, }) {
1263
2151
  const value = getStringFlag({ flags, name });
1264
2152
  if (value === undefined) {
@@ -1270,6 +2158,31 @@ function parseJsonObjectFlag({ flags, name, }) {
1270
2158
  }
1271
2159
  return parsed;
1272
2160
  }
2161
+ function parseNullableJsonObjectFlag({ flags, name, }) {
2162
+ const value = getStringFlag({ flags, name });
2163
+ if (value === undefined) {
2164
+ return undefined;
2165
+ }
2166
+ if (value === "null") {
2167
+ return null;
2168
+ }
2169
+ const parsed = JSON.parse(value);
2170
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2171
+ throw new Error(`--${name} must be a JSON object or null.`);
2172
+ }
2173
+ return parsed;
2174
+ }
2175
+ function parseJsonArrayFlag({ flags, name, }) {
2176
+ const value = getStringFlag({ flags, name });
2177
+ if (value === undefined) {
2178
+ return undefined;
2179
+ }
2180
+ const parsed = JSON.parse(value);
2181
+ if (!Array.isArray(parsed)) {
2182
+ throw new Error(`--${name} must be a JSON array.`);
2183
+ }
2184
+ return parsed;
2185
+ }
1273
2186
  function parseCsvFlag({ flags, name, }) {
1274
2187
  const value = getStringFlag({ flags, name });
1275
2188
  if (value === undefined) {
@@ -1280,6 +2193,13 @@ function parseCsvFlag({ flags, name, }) {
1280
2193
  .map((item) => item.trim())
1281
2194
  .filter((item) => item.length > 0);
1282
2195
  }
2196
+ function parseCsvRequiredFlag({ flags, name, }) {
2197
+ const parsed = parseCsvFlag({ flags, name });
2198
+ if (parsed === undefined || parsed.length === 0) {
2199
+ throw new Error(`Missing required --${name}.`);
2200
+ }
2201
+ return parsed;
2202
+ }
1283
2203
  function parseNullableStringFlag({ flags, name, }) {
1284
2204
  const value = getStringFlag({ flags, name });
1285
2205
  if (value === undefined) {
@@ -1480,6 +2400,45 @@ function parseYoutubeQuality({ value, }) {
1480
2400
  }
1481
2401
  throw new Error("--quality must be 720p, 1080p, 1440p, or 2160p.");
1482
2402
  }
2403
+ function parseMediaPositionPreset({ value, }) {
2404
+ if (value === undefined) {
2405
+ return undefined;
2406
+ }
2407
+ if (value === "center" ||
2408
+ value === "top-left" ||
2409
+ value === "top-right" ||
2410
+ value === "bottom-left" ||
2411
+ value === "bottom-right" ||
2412
+ value === "full-screen") {
2413
+ return value;
2414
+ }
2415
+ throw new Error("--position-preset must be center, top-left, top-right, bottom-left, bottom-right, or full-screen.");
2416
+ }
2417
+ function parseMediaAnimationPreset({ value, }) {
2418
+ if (value === undefined) {
2419
+ return undefined;
2420
+ }
2421
+ if (value === "none" ||
2422
+ value === "fadeIn" ||
2423
+ value === "fadeOut" ||
2424
+ value === "zoomIn" ||
2425
+ value === "slideIn" ||
2426
+ value === "popIn" ||
2427
+ value === "bounce" ||
2428
+ value === "spin") {
2429
+ return value;
2430
+ }
2431
+ throw new Error("--animation-preset must be none, fadeIn, fadeOut, zoomIn, slideIn, popIn, bounce, or spin.");
2432
+ }
2433
+ function parseCaptionMatchPattern({ value, }) {
2434
+ if (value === undefined) {
2435
+ return undefined;
2436
+ }
2437
+ if (value === "numbers" || value === "caps" || value === "exclamations") {
2438
+ return value;
2439
+ }
2440
+ throw new Error("--match-pattern must be numbers, caps, or exclamations.");
2441
+ }
1483
2442
  function parseFolderIdFlag({ flags, name, }) {
1484
2443
  const value = getStringFlag({ flags, name });
1485
2444
  if (value === undefined) {
@@ -1508,6 +2467,45 @@ function parseAssetIds({ flags }) {
1508
2467
  .map((item) => item.trim())
1509
2468
  .filter((item) => item.length > 0);
1510
2469
  }
2470
+ function parseTimelineItemIds({ flags }) {
2471
+ const csv = getStringFlag({ flags, name: "timeline-item-ids" }) ??
2472
+ getStringFlag({ flags, name: "timeline-item-id" });
2473
+ if (!csv) {
2474
+ throw new Error("Missing required --timeline-item-id or --timeline-item-ids.");
2475
+ }
2476
+ return csv
2477
+ .split(",")
2478
+ .map((item) => item.trim())
2479
+ .filter((item) => item.length > 0);
2480
+ }
2481
+ function parseWordIds({ flags }) {
2482
+ const csv = getStringFlag({ flags, name: "word-ids" }) ??
2483
+ getStringFlag({ flags, name: "word-id" });
2484
+ if (!csv) {
2485
+ throw new Error("Missing required --word-id or --word-ids.");
2486
+ }
2487
+ return csv
2488
+ .split(",")
2489
+ .map((item) => item.trim())
2490
+ .filter((item) => item.length > 0);
2491
+ }
2492
+ function parseWordTextUpdatesFlag({ flags, name, }) {
2493
+ const parsed = parseJsonArrayFlag({ flags, name });
2494
+ if (parsed === undefined) {
2495
+ throw new Error(`Missing required --${name}.`);
2496
+ }
2497
+ return parsed.map((item, index) => {
2498
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
2499
+ throw new Error(`--${name}[${index}] must be an object.`);
2500
+ }
2501
+ const record = item;
2502
+ if (typeof record.word_id !== "string" ||
2503
+ typeof record.new_text !== "string") {
2504
+ throw new Error(`--${name}[${index}] must include word_id and new_text.`);
2505
+ }
2506
+ return { word_id: record.word_id, new_text: record.new_text };
2507
+ });
2508
+ }
1511
2509
  function inferContentType({ filePath }) {
1512
2510
  const extension = extname(filePath).toLowerCase();
1513
2511
  switch (extension) {
@@ -1618,6 +2616,271 @@ function buildWorkspaceDefaults({ flags }) {
1618
2616
  }
1619
2617
  return defaults;
1620
2618
  }
2619
+ const TEXT_STYLE_FLAG_NAMES = [
2620
+ "font-family",
2621
+ "font-size",
2622
+ "font-weight",
2623
+ "color",
2624
+ "background-color",
2625
+ "padding-x",
2626
+ "padding-y",
2627
+ "border-radius",
2628
+ "top",
2629
+ "left",
2630
+ "text-align",
2631
+ "text-shadow",
2632
+ "no-background",
2633
+ ];
2634
+ function hasTextStyleFlags({ flags }) {
2635
+ return TEXT_STYLE_FLAG_NAMES.some((name) => name in flags);
2636
+ }
2637
+ function buildTextOverlaySpec({ text, name, durationSeconds, flags, }) {
2638
+ const durationInFrames = Math.max(1, Math.round(durationSeconds * 30));
2639
+ const top = getStringFlag({ flags, name: "top" }) ?? "18%";
2640
+ const left = getStringFlag({ flags, name: "left" }) ?? "50%";
2641
+ const textStyle = buildTextStyle({ flags });
2642
+ return {
2643
+ id: `cli-${Date.now()}`,
2644
+ fps: 30,
2645
+ mode: "elements",
2646
+ name: name ?? text,
2647
+ fonts: [String(textStyle.fontFamily ?? "Inter")],
2648
+ width: 1080,
2649
+ height: 1920,
2650
+ defaults: {
2651
+ color: textStyle.color,
2652
+ fontSize: textStyle.fontSize,
2653
+ fontFamily: textStyle.fontFamily,
2654
+ },
2655
+ elements: [
2656
+ {
2657
+ id: `group-${Date.now()}`,
2658
+ type: "group",
2659
+ style: {
2660
+ top,
2661
+ left,
2662
+ display: "inline-flex",
2663
+ opacity: {
2664
+ keyframes: [
2665
+ { frame: 0, value: 0 },
2666
+ { frame: 10, value: 1 },
2667
+ { frame: Math.max(10, durationInFrames - 10), value: 1 },
2668
+ { frame: durationInFrames, value: 0 },
2669
+ ],
2670
+ },
2671
+ position: "absolute",
2672
+ transform: "translate(-50%, -50%)",
2673
+ alignItems: "center",
2674
+ flexDirection: "column",
2675
+ },
2676
+ children: [
2677
+ {
2678
+ id: `text-${Date.now()}`,
2679
+ type: "text",
2680
+ style: textStyle,
2681
+ layout: "none",
2682
+ content: text,
2683
+ },
2684
+ ],
2685
+ },
2686
+ ],
2687
+ metadata: {
2688
+ kind: "overlay",
2689
+ createdBy: "blitzreels-cli",
2690
+ },
2691
+ durationInFrames,
2692
+ };
2693
+ }
2694
+ function buildTextStyle({ flags }) {
2695
+ const noBackground = getBooleanFlag({ flags, name: "no-background" });
2696
+ const backgroundColor = noBackground
2697
+ ? "transparent"
2698
+ : (getStringFlag({ flags, name: "background-color" }) ??
2699
+ "rgba(17, 24, 39, 0.86)");
2700
+ const style = {
2701
+ color: getStringFlag({ flags, name: "color" }) ?? "#FFFFFF",
2702
+ padding: `${getNumberFlag({ flags, name: "padding-y" }) ?? 12}px ${getNumberFlag({ flags, name: "padding-x" }) ?? 24}px`,
2703
+ fontSize: getNumberFlag({ flags, name: "font-size" }) ?? 48,
2704
+ textAlign: parseTextAlign({
2705
+ value: getStringFlag({ flags, name: "text-align" }) ?? "center",
2706
+ }),
2707
+ fontFamily: getStringFlag({ flags, name: "font-family" }) ?? "Inter",
2708
+ fontWeight: getStringFlag({ flags, name: "font-weight" }) ?? "700",
2709
+ lineHeight: 1.12,
2710
+ textShadow: getStringFlag({ flags, name: "text-shadow" }) ??
2711
+ "0 8px 22px rgba(0, 0, 0, 0.52)",
2712
+ borderRadius: getNumberFlag({ flags, name: "border-radius" }) ?? 18,
2713
+ letterSpacing: 0,
2714
+ backgroundColor,
2715
+ bubbleJoinStyle: "reverse-radius",
2716
+ boxDecorationBreak: "clone",
2717
+ };
2718
+ return style;
2719
+ }
2720
+ async function buildUpdatedTextSpec({ context, projectId, contentItemId, nextText, flags, }) {
2721
+ const items = await context.client.text.list({ projectId });
2722
+ const item = items.data.find((candidate) => candidate.content_item_id === contentItemId);
2723
+ if (!item?.spec) {
2724
+ throw new Error(`Content item ${contentItemId} has no editable text spec.`);
2725
+ }
2726
+ return applyTextStyleToSpec({
2727
+ spec: item.spec,
2728
+ text: nextText,
2729
+ flags,
2730
+ });
2731
+ }
2732
+ function applyTextStyleToSpec({ spec, text, flags, }) {
2733
+ const cloned = JSON.parse(JSON.stringify(spec));
2734
+ const elements = Array.isArray(cloned.elements) ? cloned.elements : [];
2735
+ const firstGroup = elements.find((element) => Boolean(element) &&
2736
+ typeof element === "object" &&
2737
+ element.type === "group");
2738
+ const children = Array.isArray(firstGroup?.children)
2739
+ ? firstGroup.children
2740
+ : [];
2741
+ const firstText = children.find((child) => Boolean(child) &&
2742
+ typeof child === "object" &&
2743
+ child.type === "text");
2744
+ if (!firstText) {
2745
+ throw new Error("Text overlay spec has no editable text element.");
2746
+ }
2747
+ firstText.style = {
2748
+ ...(firstText.style ?? {}),
2749
+ ...buildTextStyle({ flags }),
2750
+ };
2751
+ if (text !== undefined) {
2752
+ firstText.content = text;
2753
+ }
2754
+ if (firstGroup) {
2755
+ const style = (firstGroup.style ??
2756
+ {});
2757
+ firstGroup.style = {
2758
+ ...style,
2759
+ top: getStringFlag({ flags, name: "top" }) ?? style.top,
2760
+ left: getStringFlag({ flags, name: "left" }) ?? style.left,
2761
+ };
2762
+ }
2763
+ return cloned;
2764
+ }
2765
+ function parseTextAlign({ value, }) {
2766
+ if (value === "left" ||
2767
+ value === "center" ||
2768
+ value === "right" ||
2769
+ value === "justify") {
2770
+ return value;
2771
+ }
2772
+ throw new Error("--text-align must be left, center, right, or justify.");
2773
+ }
2774
+ function buildCaptionWordSelector({ flags }) {
2775
+ const wordIds = parseCsvFlag({ flags, name: "word-ids" }) ??
2776
+ parseCsvFlag({ flags, name: "word-id" });
2777
+ return {
2778
+ projectId: requireStringFlag({ flags, name: "project-id" }),
2779
+ wordIds,
2780
+ matchText: getStringFlag({ flags, name: "match-text" }),
2781
+ matchPattern: parseCaptionMatchPattern({
2782
+ value: getStringFlag({ flags, name: "match-pattern" }),
2783
+ }),
2784
+ timelineItemId: getStringFlag({ flags, name: "timeline-item-id" }),
2785
+ };
2786
+ }
2787
+ function buildCaptionWordStyleFromFlags({ flags, }) {
2788
+ const style = {};
2789
+ setIfDefined({
2790
+ target: style,
2791
+ key: "fontFamily",
2792
+ value: getStringFlag({ flags, name: "font-family" }),
2793
+ });
2794
+ setIfDefined({
2795
+ target: style,
2796
+ key: "fontWeight",
2797
+ value: getStringFlag({ flags, name: "font-weight" }),
2798
+ });
2799
+ setIfDefined({
2800
+ target: style,
2801
+ key: "color",
2802
+ value: getStringFlag({ flags, name: "color" }),
2803
+ });
2804
+ setIfDefined({
2805
+ target: style,
2806
+ key: "activeColor",
2807
+ value: getStringFlag({ flags, name: "active-color" }),
2808
+ });
2809
+ setIfDefined({
2810
+ target: style,
2811
+ key: "backgroundColor",
2812
+ value: getStringFlag({ flags, name: "background-color" }),
2813
+ });
2814
+ setIfDefined({
2815
+ target: style,
2816
+ key: "fontStyle",
2817
+ value: getStringFlag({ flags, name: "font-style" }),
2818
+ });
2819
+ setIfDefined({
2820
+ target: style,
2821
+ key: "activeAnimation",
2822
+ value: getStringFlag({ flags, name: "active-animation" }),
2823
+ });
2824
+ setIfDefined({
2825
+ target: style,
2826
+ key: "textTransform",
2827
+ value: getStringFlag({ flags, name: "text-transform" }),
2828
+ });
2829
+ setIfDefined({
2830
+ target: style,
2831
+ key: "textShadow",
2832
+ value: getStringFlag({ flags, name: "text-shadow" }),
2833
+ });
2834
+ setIfDefined({
2835
+ target: style,
2836
+ key: "glowColor",
2837
+ value: getStringFlag({ flags, name: "glow-color" }),
2838
+ });
2839
+ setIfDefined({
2840
+ target: style,
2841
+ key: "scale",
2842
+ value: getNumberFlag({ flags, name: "scale" }),
2843
+ });
2844
+ setIfDefined({
2845
+ target: style,
2846
+ key: "offsetX",
2847
+ value: getNumberFlag({ flags, name: "offset-x" }),
2848
+ });
2849
+ setIfDefined({
2850
+ target: style,
2851
+ key: "offsetY",
2852
+ value: getNumberFlag({ flags, name: "offset-y" }),
2853
+ });
2854
+ setIfDefined({
2855
+ target: style,
2856
+ key: "rotationDeg",
2857
+ value: getNumberFlag({ flags, name: "rotation-deg" }),
2858
+ });
2859
+ setIfDefined({
2860
+ target: style,
2861
+ key: "letterSpacing",
2862
+ value: getNumberFlag({ flags, name: "letter-spacing" }),
2863
+ });
2864
+ setIfDefined({
2865
+ target: style,
2866
+ key: "opacity",
2867
+ value: getNumberFlag({ flags, name: "opacity" }),
2868
+ });
2869
+ setIfDefined({
2870
+ target: style,
2871
+ key: "glowBlurPx",
2872
+ value: getNumberFlag({ flags, name: "glow-blur-px" }),
2873
+ });
2874
+ if (Object.keys(style).length === 0) {
2875
+ throw new Error("Provide --style-json or at least one caption word style flag.");
2876
+ }
2877
+ return style;
2878
+ }
2879
+ function setIfDefined({ target, key, value, }) {
2880
+ if (value !== undefined) {
2881
+ target[key] = value;
2882
+ }
2883
+ }
1621
2884
  function parseContextMode({ value, }) {
1622
2885
  if (value === "summary" ||
1623
2886
  value === "assets" ||
@@ -1644,52 +2907,7 @@ function printHelp() {
1644
2907
  console.log(`blitzreels
1645
2908
 
1646
2909
  Usage:
1647
- blitzreels auth login [--no-browser]
1648
- blitzreels auth set-key --api-key KEY [--base-url URL]
1649
- blitzreels auth whoami [--json]
1650
- blitzreels workspace settings get [--json]
1651
- blitzreels workspace settings update [--name NAME] [--protected-words foo,bar]
1652
- blitzreels projects list [--search QUERY] [--status active|archived|all]
1653
- blitzreels projects create --name NAME [--aspect-ratio 9:16]
1654
- blitzreels projects get --project-id ID [--json]
1655
- blitzreels projects rename --project-id ID --name NAME
1656
- blitzreels projects update --project-id ID [--name NAME] [--description TEXT]
1657
- blitzreels projects delete --project-id ID --confirm-delete
1658
- blitzreels media list [--asset-type video|audio|image|document] [--json]
1659
- blitzreels media get --asset-id ID [--json]
1660
- blitzreels media delete --asset-id ID --confirm-delete
1661
- blitzreels media upload --file ./video.mp4 [--auto-transcribe true]
1662
- blitzreels media import-youtube --url URL [--folder-id ID] [--quality 1080p]
1663
- blitzreels media rename --asset-id ID --name NAME
1664
- blitzreels media update --asset-id ID [--description TEXT] [--folder-id ID|root]
1665
- blitzreels media move --asset-id ID --target-folder-id ID|root
1666
- blitzreels media attach --project-id ID --asset-id ID [--at 0]
1667
- blitzreels media folders list [--json]
1668
- blitzreels media folders create --name NAME [--parent-folder-id ID]
1669
- blitzreels media folders rename --folder-id ID --name NAME
1670
- blitzreels media folders update --folder-id ID [--parent-folder-id ID|root]
1671
- blitzreels media folders delete --folder-id ID --confirm-delete
1672
- blitzreels project inspect --project-id ID [--mode full] [--json]
1673
- blitzreels project timeline --project-id ID [--json]
1674
- blitzreels project snapshot --project-id ID --at 00:03 [--json]
1675
- blitzreels project snapshots --project-id ID --at 00:01,00:03 [--json]
1676
- blitzreels text add --project-id ID --text "Hook" [--at 0] [--duration 4]
1677
- blitzreels text list --project-id ID [--json]
1678
- blitzreels text update --project-id ID --content-item-id ID [--text "New hook"]
1679
- blitzreels text remove --project-id ID --content-item-id ID
1680
- blitzreels audio add --project-id ID --asset-id ID [--at 0] [--volume 0.6]
1681
- blitzreels silence plan --project-id ID --timeline-item-id ID [--json]
1682
- blitzreels silence apply --project-id ID --timeline-item-id ID --confirm-preview
1683
- blitzreels mistakes plan --project-id ID --timeline-item-id ID [--json]
1684
- blitzreels mistakes apply --project-id ID --timeline-item-id ID --confirm-preview
1685
- blitzreels captions themes list [--json]
1686
- blitzreels captions theme set --project-id ID --theme-id ID
1687
- blitzreels captions theme set-default --theme-id ID
1688
- blitzreels exports start --project-id ID [--resolution 1080p] [--format mp4]
1689
- blitzreels exports wait --export-id ID [--json]
1690
- blitzreels exports download --export-id ID --output ./short.mp4
1691
- blitzreels api docs
1692
- blitzreels api spec
2910
+ ${COMMAND_SPECS.map((spec) => ` ${spec.usage}`).join("\n")}
1693
2911
 
1694
2912
  Global flags:
1695
2913
  --api-key KEY
@@ -1698,14 +2916,69 @@ Global flags:
1698
2916
  `);
1699
2917
  }
1700
2918
  main().catch((error) => {
2919
+ const json = isJsonOutputRequested();
1701
2920
  if (error instanceof BlitzReelsError) {
1702
- const requestId = error.requestId ? ` request_id=${error.requestId}` : "";
1703
- console.error(`BlitzReels API error ${error.status}:${requestId} ${error.message}`);
2921
+ printCliError({
2922
+ json,
2923
+ code: "api_error",
2924
+ message: `BlitzReels API error ${error.status}: ${error.message}`,
2925
+ status: error.status,
2926
+ requestId: error.requestId ?? undefined,
2927
+ usage: usageForCurrentCommand(),
2928
+ });
1704
2929
  process.exitCode = 1;
1705
2930
  return;
1706
2931
  }
1707
- const message = error instanceof Error ? error.message : String(error);
1708
- console.error(message);
2932
+ if (error instanceof CliUsageError) {
2933
+ printCliError({
2934
+ json,
2935
+ code: error.code,
2936
+ message: error.message,
2937
+ suggestions: error.suggestions,
2938
+ usage: error.usage,
2939
+ });
2940
+ process.exitCode = 1;
2941
+ return;
2942
+ }
2943
+ const usage = usageForCurrentCommand();
2944
+ printCliError({
2945
+ json,
2946
+ code: "cli_error",
2947
+ message: error instanceof Error ? error.message : String(error),
2948
+ usage,
2949
+ });
1709
2950
  process.exitCode = 1;
1710
2951
  });
2952
+ function isJsonOutputRequested() {
2953
+ return process.argv.slice(2).some((value) => value === "--json");
2954
+ }
2955
+ function printCliError({ json, code, message, suggestions = [], usage, status, requestId, }) {
2956
+ if (json) {
2957
+ console.error(JSON.stringify({
2958
+ error: {
2959
+ code,
2960
+ message,
2961
+ suggestions,
2962
+ usage,
2963
+ status,
2964
+ request_id: requestId,
2965
+ },
2966
+ }, null, 2));
2967
+ return;
2968
+ }
2969
+ console.error(message);
2970
+ if (suggestions.length > 0) {
2971
+ console.error("Did you mean?");
2972
+ for (const suggestion of suggestions) {
2973
+ console.error(` ${suggestion.startsWith("--") ? suggestion : suggestion}`);
2974
+ }
2975
+ }
2976
+ if (usage) {
2977
+ console.error("Usage:");
2978
+ console.error(` ${usage}`);
2979
+ }
2980
+ if (requestId) {
2981
+ console.error(`Request ID: ${requestId}`);
2982
+ }
2983
+ }
1711
2984
  //# sourceMappingURL=index.js.map