blitzreels 0.1.1 → 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
@@ -1,21 +1,473 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from "node:child_process";
3
- import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
- import { dirname, join, resolve } from "node:path";
5
+ import { basename, dirname, extname, join, resolve } from "node:path";
6
6
  import { BlitzReelsClient, BlitzReelsError, } from "@blitzreels/sdk";
7
7
  const CONFIG_PATH = join(homedir(), ".blitzreels", "config.json");
8
8
  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 ??
@@ -83,6 +535,484 @@ async function runCommand({ context }) {
83
535
  return;
84
536
  }
85
537
  requireApiKey({ apiKey: context.config.apiKey });
538
+ if (group === "workspace" && action === "settings" && subaction === "get") {
539
+ const result = await context.client.workspace.getSettings();
540
+ printResult({
541
+ json: context.json,
542
+ value: result,
543
+ human: [
544
+ `Workspace: ${result.settings.name}`,
545
+ `Description: ${result.settings.description ?? ""}`,
546
+ `Protected words: ${result.settings.protected_words.join(", ") || "-"}`,
547
+ `Defaults: ${JSON.stringify(result.settings.defaults)}`,
548
+ ],
549
+ });
550
+ return;
551
+ }
552
+ if (group === "workspace" &&
553
+ action === "settings" &&
554
+ subaction === "update") {
555
+ const result = await context.client.workspace.updateSettings({
556
+ name: getStringFlag({ flags: context.args.flags, name: "name" }),
557
+ description: parseNullableStringFlag({
558
+ flags: context.args.flags,
559
+ name: "description",
560
+ }),
561
+ protectedWords: parseCsvFlag({
562
+ flags: context.args.flags,
563
+ name: "protected-words",
564
+ }),
565
+ iconStorageKey: parseNullableStringFlag({
566
+ flags: context.args.flags,
567
+ name: "icon-storage-key",
568
+ }),
569
+ defaults: buildWorkspaceDefaults({ flags: context.args.flags }),
570
+ });
571
+ printResult({
572
+ json: context.json,
573
+ value: result,
574
+ human: [`Workspace settings updated: ${result.settings.name}`],
575
+ });
576
+ return;
577
+ }
578
+ if (group === "projects" && action === "list") {
579
+ const result = await context.client.projects.list({
580
+ limit: getNumberFlag({ flags: context.args.flags, name: "limit" }),
581
+ cursor: getStringFlag({ flags: context.args.flags, name: "cursor" }),
582
+ offset: getNumberFlag({ flags: context.args.flags, name: "offset" }),
583
+ search: getStringFlag({ flags: context.args.flags, name: "search" }),
584
+ status: parseProjectStatus({
585
+ value: getStringFlag({ flags: context.args.flags, name: "status" }),
586
+ }),
587
+ });
588
+ printResult({
589
+ json: context.json,
590
+ value: result,
591
+ human: result.projects.map((project) => `${project.id} ${project.status} ${project.name} (${project.aspect_ratio ?? "-"})`),
592
+ });
593
+ return;
594
+ }
595
+ if (group === "projects" && action === "create") {
596
+ const result = await context.client.projects.create({
597
+ name: requireStringFlag({ flags: context.args.flags, name: "name" }),
598
+ description: getStringFlag({
599
+ flags: context.args.flags,
600
+ name: "description",
601
+ }),
602
+ aspectRatio: parseAspectRatio({
603
+ value: getStringFlag({
604
+ flags: context.args.flags,
605
+ name: "aspect-ratio",
606
+ }),
607
+ }),
608
+ projectType: parseProjectType({
609
+ value: getStringFlag({
610
+ flags: context.args.flags,
611
+ name: "project-type",
612
+ }),
613
+ }),
614
+ carouselSettings: parseJsonObjectFlag({
615
+ flags: context.args.flags,
616
+ name: "carousel-settings-json",
617
+ }),
618
+ });
619
+ printResult({
620
+ json: context.json,
621
+ value: result,
622
+ human: [`Project created: ${result.id} ${result.name}`],
623
+ });
624
+ return;
625
+ }
626
+ if (group === "projects" && action === "get") {
627
+ const result = await context.client.projects.get({
628
+ projectId: requireStringFlag({
629
+ flags: context.args.flags,
630
+ name: "project-id",
631
+ }),
632
+ });
633
+ printResult({
634
+ json: context.json,
635
+ value: result,
636
+ human: [
637
+ `${result.id} ${result.status} ${result.name}`,
638
+ `Description: ${result.description ?? ""}`,
639
+ ],
640
+ });
641
+ return;
642
+ }
643
+ if (group === "projects" && action === "rename") {
644
+ const result = await context.client.projects.update({
645
+ projectId: requireStringFlag({
646
+ flags: context.args.flags,
647
+ name: "project-id",
648
+ }),
649
+ name: requireStringFlag({ flags: context.args.flags, name: "name" }),
650
+ description: undefined,
651
+ });
652
+ printResult({
653
+ json: context.json,
654
+ value: result,
655
+ human: [`Project renamed: ${result.id} ${result.name}`],
656
+ });
657
+ return;
658
+ }
659
+ if (group === "projects" && action === "update") {
660
+ const result = await context.client.projects.update({
661
+ projectId: requireStringFlag({
662
+ flags: context.args.flags,
663
+ name: "project-id",
664
+ }),
665
+ name: getStringFlag({ flags: context.args.flags, name: "name" }),
666
+ description: parseNullableStringFlag({
667
+ flags: context.args.flags,
668
+ name: "description",
669
+ }),
670
+ });
671
+ printResult({
672
+ json: context.json,
673
+ value: result,
674
+ human: [`Project updated: ${result.id} ${result.name}`],
675
+ });
676
+ return;
677
+ }
678
+ if (group === "projects" && action === "delete") {
679
+ requireConfirmDelete({ flags: context.args.flags });
680
+ const result = await context.client.projects.delete({
681
+ projectId: requireStringFlag({
682
+ flags: context.args.flags,
683
+ name: "project-id",
684
+ }),
685
+ });
686
+ printResult({
687
+ json: context.json,
688
+ value: result,
689
+ human: ["Project deleted."],
690
+ });
691
+ return;
692
+ }
693
+ if (group === "media" && action === "list") {
694
+ const result = await context.client.media.list({
695
+ folderId: getStringFlag({ flags: context.args.flags, name: "folder-id" }),
696
+ assetType: parseAssetType({
697
+ value: getStringFlag({ flags: context.args.flags, name: "asset-type" }),
698
+ }),
699
+ search: getStringFlag({ flags: context.args.flags, name: "search" }),
700
+ limit: getNumberFlag({ flags: context.args.flags, name: "limit" }),
701
+ offset: getNumberFlag({ flags: context.args.flags, name: "offset" }),
702
+ });
703
+ printResult({
704
+ json: context.json,
705
+ value: result,
706
+ human: result.assets.map((asset) => `${asset.id} ${asset.assetType} ${asset.name ?? ""} ${asset.durationSeconds ?? "-"}s`),
707
+ });
708
+ return;
709
+ }
710
+ if (group === "media" && action === "get") {
711
+ const result = await context.client.media.get({
712
+ assetId: requireStringFlag({
713
+ flags: context.args.flags,
714
+ name: "asset-id",
715
+ }),
716
+ });
717
+ printResult({
718
+ json: context.json,
719
+ value: result,
720
+ human: [
721
+ `${result.asset.id} ${result.asset.assetType} ${result.asset.name}`,
722
+ `URL: ${result.asset.fileUrl ?? ""}`,
723
+ ],
724
+ });
725
+ return;
726
+ }
727
+ if (group === "media" && action === "delete") {
728
+ requireConfirmDelete({ flags: context.args.flags });
729
+ const result = await context.client.media.delete({
730
+ assetId: requireStringFlag({
731
+ flags: context.args.flags,
732
+ name: "asset-id",
733
+ }),
734
+ });
735
+ printResult({
736
+ json: context.json,
737
+ value: result,
738
+ human: ["Media asset deleted."],
739
+ });
740
+ return;
741
+ }
742
+ if (group === "media" && action === "upload") {
743
+ const result = await uploadMediaFile({ context });
744
+ printResult({
745
+ json: context.json,
746
+ value: result,
747
+ human: [
748
+ `Media uploaded: ${result.id} ${result.name} (${result.asset_type}, ${result.processing_status})`,
749
+ ],
750
+ });
751
+ return;
752
+ }
753
+ if (group === "media" && action === "import-youtube") {
754
+ const result = await context.client.media.importYoutube({
755
+ url: requireStringFlag({ flags: context.args.flags, name: "url" }),
756
+ folderId: parseFolderIdFlag({
757
+ flags: context.args.flags,
758
+ name: "folder-id",
759
+ }),
760
+ quality: parseYoutubeQuality({
761
+ value: getStringFlag({ flags: context.args.flags, name: "quality" }),
762
+ }),
763
+ fps: getNumberFlag({ flags: context.args.flags, name: "fps" }),
764
+ audioLanguage: parseNullableStringFlag({
765
+ flags: context.args.flags,
766
+ name: "audio-language",
767
+ }),
768
+ });
769
+ printResult({
770
+ json: context.json,
771
+ value: result,
772
+ human: [`YouTube import queued: ${result.asset_id} ${result.status}`],
773
+ });
774
+ return;
775
+ }
776
+ if (group === "media" && action === "update") {
777
+ const result = await context.client.media.update({
778
+ assetId: requireStringFlag({
779
+ flags: context.args.flags,
780
+ name: "asset-id",
781
+ }),
782
+ name: getStringFlag({ flags: context.args.flags, name: "name" }),
783
+ description: parseNullableStringFlag({
784
+ flags: context.args.flags,
785
+ name: "description",
786
+ }),
787
+ folderId: parseFolderIdFlag({
788
+ flags: context.args.flags,
789
+ name: "folder-id",
790
+ }),
791
+ allowUsingAsBroll: getOptionalBooleanFlag({
792
+ flags: context.args.flags,
793
+ name: "allow-broll",
794
+ }),
795
+ });
796
+ printResult({
797
+ json: context.json,
798
+ value: result,
799
+ human: [`Media updated: ${result.asset.id} ${result.asset.name}`],
800
+ });
801
+ return;
802
+ }
803
+ if (group === "media" && action === "rename") {
804
+ const result = await context.client.media.update({
805
+ assetId: requireStringFlag({
806
+ flags: context.args.flags,
807
+ name: "asset-id",
808
+ }),
809
+ name: requireStringFlag({ flags: context.args.flags, name: "name" }),
810
+ description: undefined,
811
+ folderId: undefined,
812
+ allowUsingAsBroll: undefined,
813
+ });
814
+ printResult({
815
+ json: context.json,
816
+ value: result,
817
+ human: [`Media renamed: ${result.asset.id} ${result.asset.name}`],
818
+ });
819
+ return;
820
+ }
821
+ if (group === "media" && action === "move") {
822
+ const result = await context.client.media.move({
823
+ assetIds: parseAssetIds({ flags: context.args.flags }),
824
+ targetFolderId: requireFolderIdFlag({
825
+ flags: context.args.flags,
826
+ name: "target-folder-id",
827
+ }),
828
+ });
829
+ printResult({
830
+ json: context.json,
831
+ value: result,
832
+ human: [`Moved ${result.moved_count} media asset(s).`],
833
+ });
834
+ return;
835
+ }
836
+ if (group === "media" && action === "attach") {
837
+ const result = await context.client.media.attachToProject({
838
+ projectId: requireStringFlag({
839
+ flags: context.args.flags,
840
+ name: "project-id",
841
+ }),
842
+ assetId: requireStringFlag({
843
+ flags: context.args.flags,
844
+ name: "asset-id",
845
+ }),
846
+ startSeconds: getStringFlag({ flags: context.args.flags, name: "at" }) === undefined
847
+ ? undefined
848
+ : parseTimecode({
849
+ value: requireStringFlag({
850
+ flags: context.args.flags,
851
+ name: "at",
852
+ }),
853
+ }),
854
+ durationSeconds: getNumberFlag({ flags: context.args.flags, name: "duration" }) ?? null,
855
+ layerIndex: getNumberFlag({
856
+ flags: context.args.flags,
857
+ name: "layer-index",
858
+ }),
859
+ allowDuplicate: getOptionalBooleanFlag({
860
+ flags: context.args.flags,
861
+ name: "allow-duplicate",
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
+ }),
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
+ }
912
+ printResult({
913
+ json: context.json,
914
+ value: result,
915
+ human: [
916
+ `Media attached: ${result.inserted[0]?.timeline_item_id ?? result.message}`,
917
+ ],
918
+ });
919
+ return;
920
+ }
921
+ if (group === "media" && action === "folders" && subaction === "list") {
922
+ const result = await context.client.media.folders.list({
923
+ parentFolderId: getStringFlag({
924
+ flags: context.args.flags,
925
+ name: "parent-folder-id",
926
+ }),
927
+ });
928
+ printResult({
929
+ json: context.json,
930
+ value: result,
931
+ human: result.folders.map((folder) => `${folder.id} ${folder.name} (${folder.assetCount ?? 0} assets)`),
932
+ });
933
+ return;
934
+ }
935
+ if (group === "media" && action === "folders" && subaction === "create") {
936
+ const result = await context.client.media.folders.create({
937
+ name: requireStringFlag({ flags: context.args.flags, name: "name" }),
938
+ parentFolderId: parseFolderIdFlag({
939
+ flags: context.args.flags,
940
+ name: "parent-folder-id",
941
+ }),
942
+ description: parseNullableStringFlag({
943
+ flags: context.args.flags,
944
+ name: "description",
945
+ }),
946
+ iconType: getStringFlag({ flags: context.args.flags, name: "icon-type" }),
947
+ });
948
+ printResult({
949
+ json: context.json,
950
+ value: result,
951
+ human: [`Folder created: ${result.folder.id} ${result.folder.name}`],
952
+ });
953
+ return;
954
+ }
955
+ if (group === "media" && action === "folders" && subaction === "rename") {
956
+ const result = await context.client.media.folders.update({
957
+ folderId: requireStringFlag({
958
+ flags: context.args.flags,
959
+ name: "folder-id",
960
+ }),
961
+ name: requireStringFlag({ flags: context.args.flags, name: "name" }),
962
+ parentFolderId: undefined,
963
+ description: undefined,
964
+ iconType: undefined,
965
+ });
966
+ printResult({
967
+ json: context.json,
968
+ value: result,
969
+ human: [`Folder renamed: ${result.folder.id} ${result.folder.name}`],
970
+ });
971
+ return;
972
+ }
973
+ if (group === "media" && action === "folders" && subaction === "update") {
974
+ const result = await context.client.media.folders.update({
975
+ folderId: requireStringFlag({
976
+ flags: context.args.flags,
977
+ name: "folder-id",
978
+ }),
979
+ name: getStringFlag({ flags: context.args.flags, name: "name" }),
980
+ parentFolderId: parseFolderIdFlag({
981
+ flags: context.args.flags,
982
+ name: "parent-folder-id",
983
+ }),
984
+ description: parseNullableStringFlag({
985
+ flags: context.args.flags,
986
+ name: "description",
987
+ }),
988
+ iconType: getStringFlag({ flags: context.args.flags, name: "icon-type" }),
989
+ });
990
+ printResult({
991
+ json: context.json,
992
+ value: result,
993
+ human: [`Folder updated: ${result.folder.id} ${result.folder.name}`],
994
+ });
995
+ return;
996
+ }
997
+ if (group === "media" && action === "folders" && subaction === "delete") {
998
+ requireConfirmDelete({ flags: context.args.flags });
999
+ const result = await context.client.media.folders.delete({
1000
+ folderId: requireStringFlag({
1001
+ flags: context.args.flags,
1002
+ name: "folder-id",
1003
+ }),
1004
+ cascade: getOptionalBooleanFlag({
1005
+ flags: context.args.flags,
1006
+ name: "cascade",
1007
+ }),
1008
+ });
1009
+ printResult({
1010
+ json: context.json,
1011
+ value: result,
1012
+ human: ["Folder deleted."],
1013
+ });
1014
+ return;
1015
+ }
86
1016
  if (group === "project" && action === "inspect") {
87
1017
  const projectId = requireStringFlag({
88
1018
  flags: context.args.flags,
@@ -170,21 +1100,33 @@ async function runCommand({ context }) {
170
1100
  return;
171
1101
  }
172
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);
173
1114
  const result = await context.client.text.add({
174
1115
  projectId: requireStringFlag({
175
1116
  flags: context.args.flags,
176
1117
  name: "project-id",
177
1118
  }),
178
- text: requireStringFlag({ flags: context.args.flags, name: "text" }),
1119
+ text,
179
1120
  name: getStringFlag({ flags: context.args.flags, name: "name" }),
180
1121
  startSeconds: parseTimecode({
181
1122
  value: getStringFlag({ flags: context.args.flags, name: "at" }) ?? "0",
182
1123
  }),
183
- durationSeconds: getNumberFlag({ flags: context.args.flags, name: "duration" }) ?? 4,
1124
+ durationSeconds,
184
1125
  layerIndex: getNumberFlag({
185
1126
  flags: context.args.flags,
186
1127
  name: "layer-index",
187
1128
  }),
1129
+ spec,
188
1130
  });
189
1131
  printResult({
190
1132
  json: context.json,
@@ -226,6 +1168,29 @@ async function runCommand({ context }) {
226
1168
  return;
227
1169
  }
228
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);
229
1194
  const result = await context.client.text.update({
230
1195
  projectId: requireStringFlag({
231
1196
  flags: context.args.flags,
@@ -237,15 +1202,94 @@ async function runCommand({ context }) {
237
1202
  }),
238
1203
  text: getStringFlag({ flags: context.args.flags, name: "text" }),
239
1204
  name: getStringFlag({ flags: context.args.flags, name: "name" }),
240
- 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({
241
1217
  flags: context.args.flags,
242
- name: "spec-json",
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({
1241
+ flags: context.args.flags,
1242
+ name: "keep-aspect-ratio",
243
1243
  }),
244
1244
  });
245
1245
  printResult({
246
1246
  json: context.json,
247
1247
  value: result,
248
- 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)],
249
1293
  });
250
1294
  return;
251
1295
  }
@@ -420,6 +1464,154 @@ async function runCommand({ context }) {
420
1464
  });
421
1465
  return;
422
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
+ }
423
1615
  if (group === "captions" && action === "themes" && subaction === "list") {
424
1616
  const result = await context.client.captions.listThemes();
425
1617
  printResult({
@@ -525,7 +1717,7 @@ async function runCommand({ context }) {
525
1717
  });
526
1718
  return;
527
1719
  }
528
- throw new Error(`Unknown command: ${context.args.command.join(" ")}`);
1720
+ throw buildUnknownCommandError({ command: context.args.command });
529
1721
  }
530
1722
  async function setApiKey({ context }) {
531
1723
  const apiKey = getStringFlag({ flags: context.args.flags, name: "api-key" }) ??
@@ -632,6 +1824,52 @@ async function loginWithBrowser({ context, }) {
632
1824
  }
633
1825
  throw new Error("Timed out waiting for browser approval.");
634
1826
  }
1827
+ async function uploadMediaFile({ context }) {
1828
+ const filePath = resolve(requireStringFlag({ flags: context.args.flags, name: "file" }));
1829
+ const fileStats = await stat(filePath);
1830
+ if (!fileStats.isFile()) {
1831
+ throw new Error(`Not a file: ${filePath}`);
1832
+ }
1833
+ const fileName = getStringFlag({ flags: context.args.flags, name: "name" }) ??
1834
+ basename(filePath);
1835
+ const contentType = getStringFlag({ flags: context.args.flags, name: "content-type" }) ??
1836
+ inferContentType({ filePath });
1837
+ const init = await context.client.media.initUpload({
1838
+ fileName,
1839
+ contentType,
1840
+ });
1841
+ const bytes = await readFile(filePath);
1842
+ const uploadResponse = await fetch(init.upload_url, {
1843
+ method: "PUT",
1844
+ headers: { "Content-Type": contentType },
1845
+ body: new Blob([new Uint8Array(bytes)], { type: contentType }),
1846
+ });
1847
+ if (!uploadResponse.ok) {
1848
+ throw new Error(`Upload failed with ${uploadResponse.status}: ${await uploadResponse.text()}`);
1849
+ }
1850
+ return context.client.media.finalizeUpload({
1851
+ storageKey: init.storage_key,
1852
+ fileName,
1853
+ contentType,
1854
+ fileSizeBytes: fileStats.size,
1855
+ autoProcessEnabled: getOptionalBooleanFlag({
1856
+ flags: context.args.flags,
1857
+ name: "auto-process",
1858
+ }),
1859
+ autoTranscribe: getOptionalBooleanFlag({
1860
+ flags: context.args.flags,
1861
+ name: "auto-transcribe",
1862
+ }),
1863
+ autoDescribeAfterTranscribe: getOptionalBooleanFlag({
1864
+ flags: context.args.flags,
1865
+ name: "auto-describe",
1866
+ }),
1867
+ autoSuggestShorts: getOptionalBooleanFlag({
1868
+ flags: context.args.flags,
1869
+ name: "auto-suggest-shorts",
1870
+ }),
1871
+ });
1872
+ }
635
1873
  async function readJsonResponse({ response, url, action, }) {
636
1874
  const contentType = response.headers.get("content-type") ?? "";
637
1875
  const text = await response.text();
@@ -752,6 +1990,118 @@ function parseArgs({ argv }) {
752
1990
  }
753
1991
  return { command, flags };
754
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
+ }
755
2105
  function getStringFlag({ flags, name, }) {
756
2106
  const value = flags[name];
757
2107
  return typeof value === "string" ? value : undefined;
@@ -783,6 +2133,20 @@ function getNumberFlag({ flags, name, }) {
783
2133
  }
784
2134
  return parsed;
785
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
+ }
786
2150
  function parseJsonObjectFlag({ flags, name, }) {
787
2151
  const value = getStringFlag({ flags, name });
788
2152
  if (value === undefined) {
@@ -794,6 +2158,58 @@ function parseJsonObjectFlag({ flags, name, }) {
794
2158
  }
795
2159
  return parsed;
796
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
+ }
2186
+ function parseCsvFlag({ flags, name, }) {
2187
+ const value = getStringFlag({ flags, name });
2188
+ if (value === undefined) {
2189
+ return undefined;
2190
+ }
2191
+ return value
2192
+ .split(",")
2193
+ .map((item) => item.trim())
2194
+ .filter((item) => item.length > 0);
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
+ }
2203
+ function parseNullableStringFlag({ flags, name, }) {
2204
+ const value = getStringFlag({ flags, name });
2205
+ if (value === undefined) {
2206
+ return undefined;
2207
+ }
2208
+ if (value === "null") {
2209
+ return null;
2210
+ }
2211
+ return value;
2212
+ }
797
2213
  function parseStringRecordFlag({ flags, name, }) {
798
2214
  const value = parseJsonObjectFlag({ flags, name });
799
2215
  if (value === undefined) {
@@ -812,6 +2228,11 @@ function requireConfirmPreview({ flags }) {
812
2228
  }
813
2229
  return true;
814
2230
  }
2231
+ function requireConfirmDelete({ flags }) {
2232
+ if (!getBooleanFlag({ flags, name: "confirm-delete" })) {
2233
+ throw new Error("Delete commands require --confirm-delete.");
2234
+ }
2235
+ }
815
2236
  function parseTimecode({ value }) {
816
2237
  const trimmed = value.trim();
817
2238
  if (/^\d+(\.\d+)?$/.test(trimmed)) {
@@ -924,6 +2345,542 @@ function parseImageFormat({ value, }) {
924
2345
  }
925
2346
  throw new Error("--image-format must be jpeg or png.");
926
2347
  }
2348
+ function parseProjectStatus({ value, }) {
2349
+ if (value === undefined) {
2350
+ return undefined;
2351
+ }
2352
+ if (value === "active" || value === "archived" || value === "all") {
2353
+ return value;
2354
+ }
2355
+ throw new Error("--status must be active, archived, or all.");
2356
+ }
2357
+ function parseAspectRatio({ value, }) {
2358
+ if (value === undefined) {
2359
+ return undefined;
2360
+ }
2361
+ if (value === "9:16" ||
2362
+ value === "16:9" ||
2363
+ value === "1:1" ||
2364
+ value === "4:5") {
2365
+ return value;
2366
+ }
2367
+ throw new Error("--aspect-ratio must be 9:16, 16:9, 1:1, or 4:5.");
2368
+ }
2369
+ function parseProjectType({ value, }) {
2370
+ if (value === undefined) {
2371
+ return undefined;
2372
+ }
2373
+ if (value === "video" || value === "carousel") {
2374
+ return value;
2375
+ }
2376
+ throw new Error("--project-type must be video or carousel.");
2377
+ }
2378
+ function parseAssetType({ value, }) {
2379
+ if (value === undefined) {
2380
+ return undefined;
2381
+ }
2382
+ if (value === "video" ||
2383
+ value === "audio" ||
2384
+ value === "image" ||
2385
+ value === "document" ||
2386
+ value === "all") {
2387
+ return value;
2388
+ }
2389
+ throw new Error("--asset-type must be video, audio, image, document, or all.");
2390
+ }
2391
+ function parseYoutubeQuality({ value, }) {
2392
+ if (value === undefined) {
2393
+ return undefined;
2394
+ }
2395
+ if (value === "720p" ||
2396
+ value === "1080p" ||
2397
+ value === "1440p" ||
2398
+ value === "2160p") {
2399
+ return value;
2400
+ }
2401
+ throw new Error("--quality must be 720p, 1080p, 1440p, or 2160p.");
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
+ }
2442
+ function parseFolderIdFlag({ flags, name, }) {
2443
+ const value = getStringFlag({ flags, name });
2444
+ if (value === undefined) {
2445
+ return undefined;
2446
+ }
2447
+ if (value === "root" || value === "null") {
2448
+ return null;
2449
+ }
2450
+ return value;
2451
+ }
2452
+ function requireFolderIdFlag({ flags, name, }) {
2453
+ const value = parseFolderIdFlag({ flags, name });
2454
+ if (value === undefined) {
2455
+ throw new Error(`Missing required --${name}. Use "root" for no folder.`);
2456
+ }
2457
+ return value;
2458
+ }
2459
+ function parseAssetIds({ flags }) {
2460
+ const csv = getStringFlag({ flags, name: "asset-ids" }) ??
2461
+ getStringFlag({ flags, name: "asset-id" });
2462
+ if (!csv) {
2463
+ throw new Error("Missing required --asset-id or --asset-ids.");
2464
+ }
2465
+ return csv
2466
+ .split(",")
2467
+ .map((item) => item.trim())
2468
+ .filter((item) => item.length > 0);
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
+ }
2509
+ function inferContentType({ filePath }) {
2510
+ const extension = extname(filePath).toLowerCase();
2511
+ switch (extension) {
2512
+ case ".mp4":
2513
+ return "video/mp4";
2514
+ case ".mov":
2515
+ return "video/quicktime";
2516
+ case ".webm":
2517
+ return "video/webm";
2518
+ case ".mp3":
2519
+ return "audio/mpeg";
2520
+ case ".wav":
2521
+ return "audio/wav";
2522
+ case ".m4a":
2523
+ return "audio/mp4";
2524
+ case ".jpg":
2525
+ case ".jpeg":
2526
+ return "image/jpeg";
2527
+ case ".png":
2528
+ return "image/png";
2529
+ case ".webp":
2530
+ return "image/webp";
2531
+ default:
2532
+ return "application/octet-stream";
2533
+ }
2534
+ }
2535
+ function parseOrientation({ value, }) {
2536
+ if (value === undefined) {
2537
+ return undefined;
2538
+ }
2539
+ if (value === "null") {
2540
+ return null;
2541
+ }
2542
+ if (value === "landscape" || value === "portrait" || value === "square") {
2543
+ return value;
2544
+ }
2545
+ throw new Error("--orientation must be landscape, portrait, square, or null.");
2546
+ }
2547
+ function parseResolution({ value, }) {
2548
+ if (value === undefined) {
2549
+ return undefined;
2550
+ }
2551
+ if (value === "null") {
2552
+ return null;
2553
+ }
2554
+ if (value === "0.25" ||
2555
+ value === "0.5" ||
2556
+ value === "0.75" ||
2557
+ value === "1" ||
2558
+ value === "1.5" ||
2559
+ value === "2") {
2560
+ return value;
2561
+ }
2562
+ throw new Error("--resolution must be 0.25, 0.5, 0.75, 1, 1.5, 2, or null.");
2563
+ }
2564
+ function parseFrameRate({ value, }) {
2565
+ if (value === undefined) {
2566
+ return undefined;
2567
+ }
2568
+ if (value === "null") {
2569
+ return null;
2570
+ }
2571
+ if (value === "24" ||
2572
+ value === "25" ||
2573
+ value === "30" ||
2574
+ value === "50" ||
2575
+ value === "60") {
2576
+ return value;
2577
+ }
2578
+ throw new Error("--frame-rate must be 24, 25, 30, 50, 60, or null.");
2579
+ }
2580
+ function parseNullableAspectRatio({ value, }) {
2581
+ if (value === "null") {
2582
+ return null;
2583
+ }
2584
+ return parseAspectRatio({ value });
2585
+ }
2586
+ function buildWorkspaceDefaults({ flags }) {
2587
+ const defaults = {
2588
+ aspectRatio: parseNullableAspectRatio({
2589
+ value: getStringFlag({ flags, name: "aspect-ratio" }),
2590
+ }),
2591
+ orientation: parseOrientation({
2592
+ value: getStringFlag({ flags, name: "orientation" }),
2593
+ }),
2594
+ resolution: parseResolution({
2595
+ value: getStringFlag({ flags, name: "resolution" }),
2596
+ }),
2597
+ frameRate: parseFrameRate({
2598
+ value: getStringFlag({ flags, name: "frame-rate" }),
2599
+ }),
2600
+ autoManageBroll: getOptionalBooleanFlag({
2601
+ flags,
2602
+ name: "auto-manage-broll",
2603
+ }),
2604
+ captionThemeId: parseNullableStringFlag({
2605
+ flags,
2606
+ name: "caption-theme-id",
2607
+ }),
2608
+ };
2609
+ if (defaults.aspectRatio === undefined &&
2610
+ defaults.orientation === undefined &&
2611
+ defaults.resolution === undefined &&
2612
+ defaults.frameRate === undefined &&
2613
+ defaults.autoManageBroll === undefined &&
2614
+ defaults.captionThemeId === undefined) {
2615
+ return undefined;
2616
+ }
2617
+ return defaults;
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
+ }
927
2884
  function parseContextMode({ value, }) {
928
2885
  if (value === "summary" ||
929
2886
  value === "assets" ||
@@ -950,30 +2907,7 @@ function printHelp() {
950
2907
  console.log(`blitzreels
951
2908
 
952
2909
  Usage:
953
- blitzreels auth login [--no-browser]
954
- blitzreels auth set-key --api-key KEY [--base-url URL]
955
- blitzreels auth whoami [--json]
956
- blitzreels project inspect --project-id ID [--mode full] [--json]
957
- blitzreels project timeline --project-id ID [--json]
958
- blitzreels project snapshot --project-id ID --at 00:03 [--json]
959
- blitzreels project snapshots --project-id ID --at 00:01,00:03 [--json]
960
- blitzreels text add --project-id ID --text "Hook" [--at 0] [--duration 4]
961
- blitzreels text list --project-id ID [--json]
962
- blitzreels text update --project-id ID --content-item-id ID [--text "New hook"]
963
- blitzreels text remove --project-id ID --content-item-id ID
964
- blitzreels audio add --project-id ID --asset-id ID [--at 0] [--volume 0.6]
965
- blitzreels silence plan --project-id ID --timeline-item-id ID [--json]
966
- blitzreels silence apply --project-id ID --timeline-item-id ID --confirm-preview
967
- blitzreels mistakes plan --project-id ID --timeline-item-id ID [--json]
968
- blitzreels mistakes apply --project-id ID --timeline-item-id ID --confirm-preview
969
- blitzreels captions themes list [--json]
970
- blitzreels captions theme set --project-id ID --theme-id ID
971
- blitzreels captions theme set-default --theme-id ID
972
- blitzreels exports start --project-id ID [--resolution 1080p] [--format mp4]
973
- blitzreels exports wait --export-id ID [--json]
974
- blitzreels exports download --export-id ID --output ./short.mp4
975
- blitzreels api docs
976
- blitzreels api spec
2910
+ ${COMMAND_SPECS.map((spec) => ` ${spec.usage}`).join("\n")}
977
2911
 
978
2912
  Global flags:
979
2913
  --api-key KEY
@@ -982,14 +2916,69 @@ Global flags:
982
2916
  `);
983
2917
  }
984
2918
  main().catch((error) => {
2919
+ const json = isJsonOutputRequested();
985
2920
  if (error instanceof BlitzReelsError) {
986
- const requestId = error.requestId ? ` request_id=${error.requestId}` : "";
987
- 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
+ });
988
2929
  process.exitCode = 1;
989
2930
  return;
990
2931
  }
991
- const message = error instanceof Error ? error.message : String(error);
992
- 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
+ });
993
2950
  process.exitCode = 1;
994
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
+ }
995
2984
  //# sourceMappingURL=index.js.map