bazaar.it 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +485 -3
  2. package/bin/baz.js +6 -1
  3. package/dist/commands/auth.d.ts +2 -0
  4. package/dist/commands/auth.js +109 -0
  5. package/dist/commands/capabilities.d.ts +2 -0
  6. package/dist/commands/capabilities.js +44 -0
  7. package/dist/commands/context.d.ts +13 -0
  8. package/dist/commands/context.js +498 -0
  9. package/dist/commands/export.d.ts +2 -0
  10. package/dist/commands/export.js +360 -0
  11. package/dist/commands/logs.d.ts +2 -0
  12. package/dist/commands/logs.js +180 -0
  13. package/dist/commands/loop.d.ts +2 -0
  14. package/dist/commands/loop.js +538 -0
  15. package/dist/commands/mcp.d.ts +2 -0
  16. package/dist/commands/mcp.js +143 -0
  17. package/dist/commands/media.d.ts +2 -0
  18. package/dist/commands/media.js +362 -0
  19. package/dist/commands/project.d.ts +2 -0
  20. package/dist/commands/project.js +786 -0
  21. package/dist/commands/prompt.d.ts +2 -0
  22. package/dist/commands/prompt.js +540 -0
  23. package/dist/commands/recipe.d.ts +15 -0
  24. package/dist/commands/recipe.js +607 -0
  25. package/dist/commands/review.d.ts +17 -0
  26. package/dist/commands/review.js +345 -0
  27. package/dist/commands/scenes.d.ts +2 -0
  28. package/dist/commands/scenes.js +481 -0
  29. package/dist/commands/share.d.ts +2 -0
  30. package/dist/commands/share.js +226 -0
  31. package/dist/commands/state.d.ts +2 -0
  32. package/dist/commands/state.js +171 -0
  33. package/dist/commands/status.d.ts +2 -0
  34. package/dist/commands/status.js +219 -0
  35. package/dist/commands/template.d.ts +2 -0
  36. package/dist/commands/template.js +123 -0
  37. package/dist/commands/verify.d.ts +2 -0
  38. package/dist/commands/verify.js +150 -0
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.js +124 -0
  41. package/dist/lib/api.d.ts +188 -0
  42. package/dist/lib/api.js +719 -0
  43. package/dist/lib/banner.d.ts +12 -0
  44. package/dist/lib/banner.js +69 -0
  45. package/dist/lib/config.d.ts +33 -0
  46. package/dist/lib/config.js +99 -0
  47. package/dist/lib/output.d.ts +52 -0
  48. package/dist/lib/output.js +162 -0
  49. package/dist/lib/project-state.d.ts +52 -0
  50. package/dist/lib/project-state.js +178 -0
  51. package/dist/lib/sse.d.ts +168 -0
  52. package/dist/lib/sse.js +227 -0
  53. package/dist/lib/version.d.ts +1 -0
  54. package/dist/lib/version.js +3 -0
  55. package/dist/repl.d.ts +4 -0
  56. package/dist/repl.js +764 -0
  57. package/package.json +32 -5
@@ -0,0 +1,786 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { loadConfig, saveConfig, hasAuth, getProjectId } from '../lib/config.js';
5
+ import { success, error, output, table, formatRelativeTime, formatDuration, confirmAction } from '../lib/output.js';
6
+ import { apiRequest, ApiError } from '../lib/api.js';
7
+ function normalizeProjectFormat(value) {
8
+ if (typeof value !== 'string')
9
+ return null;
10
+ const normalized = value.toLowerCase();
11
+ if (normalized === 'landscape' || normalized === 'portrait' || normalized === 'square') {
12
+ return normalized;
13
+ }
14
+ return null;
15
+ }
16
+ function handleProjectError(err, globalOpts) {
17
+ if (err instanceof ApiError) {
18
+ if (globalOpts.json) {
19
+ output(err.toJSON(), { json: true, compact: globalOpts.compact });
20
+ }
21
+ else {
22
+ error(err.message, err.suggestion);
23
+ }
24
+ process.exit(err.exitCode);
25
+ }
26
+ if (globalOpts.json) {
27
+ output({
28
+ type: 'error',
29
+ code: 'UNKNOWN',
30
+ message: err.message || 'Unknown error',
31
+ category: 'fatal',
32
+ retryable: false,
33
+ transient: false,
34
+ exitCode: 1,
35
+ }, { json: true, compact: globalOpts.compact });
36
+ }
37
+ else {
38
+ error(err.message);
39
+ }
40
+ process.exit(1);
41
+ }
42
+ function exitProjectAuthError(globalOpts) {
43
+ if (globalOpts.json) {
44
+ output({
45
+ type: 'error',
46
+ code: 'AUTH_MISSING',
47
+ message: 'Not authenticated',
48
+ category: 'auth',
49
+ retryable: false,
50
+ transient: false,
51
+ exitCode: 13,
52
+ suggestion: 'Run: baz auth login <api-key>',
53
+ }, { json: true, compact: globalOpts.compact });
54
+ }
55
+ else {
56
+ error('Not authenticated', 'Run: baz auth login <api-key>');
57
+ }
58
+ process.exit(13);
59
+ }
60
+ export const projectCommand = new Command('project')
61
+ .description('Manage projects');
62
+ /**
63
+ * baz project create --name "My Video"
64
+ */
65
+ projectCommand
66
+ .command('create')
67
+ .description('Create a new project')
68
+ .requiredOption('--name <name>', 'Project name')
69
+ .option('--description <text>', 'Project description')
70
+ .option('--format <format>', 'Video format: landscape, portrait, square', 'landscape')
71
+ .option('--no-activate', "Don't set as active project")
72
+ .action(async (options, cmd) => {
73
+ const globalOpts = cmd.optsWithGlobals();
74
+ const config = loadConfig({
75
+ configPath: globalOpts.config,
76
+ apiUrl: globalOpts.apiUrl,
77
+ });
78
+ if (!hasAuth(config)) {
79
+ exitProjectAuthError(globalOpts);
80
+ }
81
+ const spinner = globalOpts.json ? null : ora('Creating project...').start();
82
+ try {
83
+ const format = normalizeProjectFormat(options.format);
84
+ if (!format) {
85
+ spinner?.stop();
86
+ if (globalOpts.json) {
87
+ output({
88
+ type: 'error',
89
+ code: 'VALIDATION',
90
+ message: `Invalid format "${options.format}". Use: landscape, portrait, square`,
91
+ category: 'validation',
92
+ retryable: false,
93
+ transient: false,
94
+ exitCode: 65,
95
+ }, { json: true, compact: globalOpts.compact });
96
+ }
97
+ else {
98
+ error(`Invalid format "${options.format}"`, 'Use: --format landscape|portrait|square');
99
+ }
100
+ process.exit(65);
101
+ }
102
+ const result = await apiRequest(config, 'project.create', {
103
+ title: options.name,
104
+ initialMessage: options.description || options.name,
105
+ format,
106
+ });
107
+ // API returns { projectId: "..." }
108
+ const projectId = result.projectId || result.id;
109
+ if (!projectId) {
110
+ spinner?.stop();
111
+ error('Failed to create project - no ID returned');
112
+ process.exit(1);
113
+ }
114
+ const finalTitle = result.title || result.name || options.name;
115
+ spinner?.stop();
116
+ // Set as active project unless --no-activate
117
+ if (options.activate !== false) {
118
+ saveConfig({ activeProjectId: projectId });
119
+ }
120
+ if (globalOpts.json) {
121
+ output({
122
+ ...result,
123
+ projectId,
124
+ title: finalTitle,
125
+ requestedTitle: options.name,
126
+ format,
127
+ }, { json: true, compact: globalOpts.compact });
128
+ return;
129
+ }
130
+ success(`Created project: ${chalk.bold(finalTitle)}`);
131
+ console.log(` ID: ${projectId}`);
132
+ console.log(` Format: ${format}`);
133
+ if (finalTitle !== options.name) {
134
+ console.log(chalk.gray(` Title adjusted to keep it unique for your account`));
135
+ }
136
+ if (options.activate !== false) {
137
+ console.log(chalk.gray(' Set as active project'));
138
+ }
139
+ }
140
+ catch (err) {
141
+ spinner?.stop();
142
+ handleProjectError(err, globalOpts);
143
+ }
144
+ });
145
+ /**
146
+ * baz project list
147
+ */
148
+ projectCommand
149
+ .command('list')
150
+ .description('List all projects')
151
+ .option('--limit <n>', 'Number of projects to show', '20')
152
+ .option('--all', 'Show all projects (no limit)')
153
+ .action(async (options, cmd) => {
154
+ const globalOpts = cmd.optsWithGlobals();
155
+ const config = loadConfig({
156
+ configPath: globalOpts.config,
157
+ apiUrl: globalOpts.apiUrl,
158
+ });
159
+ if (!hasAuth(config)) {
160
+ exitProjectAuthError(globalOpts);
161
+ }
162
+ const spinner = globalOpts.json ? null : ora('Fetching projects...').start();
163
+ try {
164
+ const result = await apiRequest(config, 'project.list', {
165
+ limit: options.all ? undefined : parseInt(options.limit, 10),
166
+ });
167
+ spinner?.stop();
168
+ // API returns array directly or { projects: [...] }
169
+ const projects = Array.isArray(result) ? result : (result.projects || []);
170
+ if (globalOpts.json) {
171
+ // Lean output: strip props (huge JSONB), keep only what agents need
172
+ const lean = projects.map((p) => ({
173
+ id: p.id,
174
+ title: p.title || p.name || 'Untitled',
175
+ format: p.props?.meta?.format || 'landscape',
176
+ sceneCount: p.activeSceneCount ?? p.sceneCount ?? 0,
177
+ isFavorite: Boolean(p.isFavorite),
178
+ updatedAt: p.updatedAt,
179
+ createdAt: p.createdAt,
180
+ }));
181
+ output(lean, { json: true });
182
+ return;
183
+ }
184
+ if (projects.length === 0) {
185
+ console.log(chalk.gray('No projects found.'));
186
+ console.log(chalk.gray('Create one with: baz project create --name "My Video"'));
187
+ return;
188
+ }
189
+ console.log(`Your Projects (${projects.length} total)`);
190
+ console.log();
191
+ const rows = projects.map((p) => {
192
+ const isActive = p.id === config.activeProjectId;
193
+ const marker = isActive ? chalk.green('*') : ' ';
194
+ // API returns 'title' not 'name'
195
+ const title = p.title || p.name || 'Untitled';
196
+ const displayName = isActive ? chalk.bold(title) : title;
197
+ // Use activeSceneCount from server JOIN (authoritative)
198
+ const sceneCount = p.activeSceneCount ?? p.sceneCount ?? 0;
199
+ const scenes = `${sceneCount} scenes`;
200
+ const time = formatRelativeTime(p.updatedAt);
201
+ return [marker, p.id.slice(0, 8) + '...', displayName, scenes, time];
202
+ });
203
+ table(['', 'ID', 'Name', 'Scenes', 'Updated'], rows);
204
+ console.log();
205
+ console.log(chalk.gray('* = active project'));
206
+ }
207
+ catch (err) {
208
+ spinner?.stop();
209
+ handleProjectError(err, globalOpts);
210
+ }
211
+ });
212
+ /**
213
+ * baz project use <id>
214
+ */
215
+ projectCommand
216
+ .command('use <id>')
217
+ .description('Set active project')
218
+ .action(async (id, options, cmd) => {
219
+ const globalOpts = cmd.optsWithGlobals();
220
+ const config = loadConfig({
221
+ configPath: globalOpts.config,
222
+ apiUrl: globalOpts.apiUrl,
223
+ });
224
+ if (!hasAuth(config)) {
225
+ exitProjectAuthError(globalOpts);
226
+ }
227
+ // Verify project exists
228
+ const spinner = globalOpts.json ? null : ora('Verifying project...').start();
229
+ try {
230
+ const result = await apiRequest(config, 'project.getById', { id });
231
+ spinner?.stop();
232
+ saveConfig({ activeProjectId: id });
233
+ if (globalOpts.json) {
234
+ output({ activeProjectId: id, project: result }, { json: true });
235
+ return;
236
+ }
237
+ success(`Active project: ${chalk.bold(result.title || result.name || 'Untitled')} (${id})`);
238
+ }
239
+ catch (err) {
240
+ spinner?.stop();
241
+ handleProjectError(err, globalOpts);
242
+ }
243
+ });
244
+ /**
245
+ * baz project current
246
+ */
247
+ projectCommand
248
+ .command('current')
249
+ .description('Show active project')
250
+ .action(async (options, cmd) => {
251
+ const globalOpts = cmd.optsWithGlobals();
252
+ const config = loadConfig({
253
+ configPath: globalOpts.config,
254
+ apiUrl: globalOpts.apiUrl,
255
+ projectId: globalOpts.projectId,
256
+ });
257
+ if (!config.activeProjectId) {
258
+ if (globalOpts.json) {
259
+ output({ activeProjectId: null }, { json: true });
260
+ return;
261
+ }
262
+ console.log(chalk.gray('No active project set.'));
263
+ console.log(chalk.gray('Run: baz project use <id>'));
264
+ return;
265
+ }
266
+ if (!hasAuth(config)) {
267
+ exitProjectAuthError(globalOpts);
268
+ }
269
+ const spinner = globalOpts.json ? null : ora('Fetching project details...').start();
270
+ try {
271
+ const result = await apiRequest(config, 'project.getById', {
272
+ id: config.activeProjectId,
273
+ });
274
+ spinner?.stop();
275
+ if (globalOpts.json) {
276
+ output(result, { json: true });
277
+ return;
278
+ }
279
+ console.log(chalk.cyan('Active Project'));
280
+ console.log();
281
+ console.log(`Name: ${chalk.bold(result.title || result.name || 'Untitled')}`);
282
+ console.log(`ID: ${result.id}`);
283
+ console.log(`Scenes: ${result.props?.scenes?.length || result.sceneCount || 0}`);
284
+ console.log(`Duration: ${result.duration || '0:00'}`);
285
+ console.log(`Updated: ${result.updatedAt ? formatRelativeTime(result.updatedAt) : 'N/A'}`);
286
+ }
287
+ catch (err) {
288
+ spinner?.stop();
289
+ handleProjectError(err, globalOpts);
290
+ }
291
+ });
292
+ /**
293
+ * baz project format <format>
294
+ */
295
+ projectCommand
296
+ .command('format <format>')
297
+ .description('Change project format by duplicating to a new project with target format')
298
+ .option('--source <id>', 'Source project ID (defaults to active project)')
299
+ .option('--no-activate', "Don't set duplicated project as active")
300
+ .action(async (formatArg, options, cmd) => {
301
+ const globalOpts = cmd.optsWithGlobals();
302
+ const config = loadConfig({
303
+ configPath: globalOpts.config,
304
+ apiUrl: globalOpts.apiUrl,
305
+ projectId: globalOpts.projectId,
306
+ });
307
+ if (!hasAuth(config)) {
308
+ exitProjectAuthError(globalOpts);
309
+ }
310
+ const format = normalizeProjectFormat(formatArg);
311
+ if (!format) {
312
+ if (globalOpts.json) {
313
+ output({
314
+ type: 'error',
315
+ code: 'VALIDATION',
316
+ message: `Invalid format "${formatArg}". Use: landscape, portrait, square`,
317
+ category: 'validation',
318
+ retryable: false,
319
+ transient: false,
320
+ exitCode: 65,
321
+ }, { json: true, compact: globalOpts.compact });
322
+ }
323
+ else {
324
+ error(`Invalid format "${formatArg}"`, 'Use: baz project format landscape|portrait|square');
325
+ }
326
+ process.exit(65);
327
+ }
328
+ let sourceProjectId;
329
+ try {
330
+ sourceProjectId = options.source || getProjectId(config, globalOpts.projectId);
331
+ }
332
+ catch (err) {
333
+ if (globalOpts.json) {
334
+ output({
335
+ type: 'error',
336
+ code: 'VALIDATION',
337
+ message: err.message,
338
+ category: 'validation',
339
+ retryable: false,
340
+ transient: false,
341
+ exitCode: 64,
342
+ }, { json: true, compact: globalOpts.compact });
343
+ }
344
+ else {
345
+ error(err.message);
346
+ }
347
+ process.exit(64);
348
+ }
349
+ const spinner = globalOpts.json ? null : ora(`Converting project to ${format}...`).start();
350
+ try {
351
+ const duplicateResult = await apiRequest(config, 'project.duplicate', {
352
+ projectId: sourceProjectId,
353
+ format,
354
+ });
355
+ const newProjectId = duplicateResult.projectId;
356
+ if (!newProjectId) {
357
+ spinner?.stop();
358
+ error('Failed to convert project format - no new project ID returned');
359
+ process.exit(1);
360
+ }
361
+ const duplicatedProject = await apiRequest(config, 'project.getById', { id: newProjectId }).catch(() => null);
362
+ const activated = options.activate !== false;
363
+ if (activated) {
364
+ saveConfig({ activeProjectId: newProjectId });
365
+ }
366
+ spinner?.stop();
367
+ const payload = {
368
+ type: 'project_format_changed',
369
+ sourceProjectId,
370
+ projectId: newProjectId,
371
+ format: duplicatedProject?.props?.meta?.format || format,
372
+ title: duplicatedProject?.title || 'Untitled',
373
+ activated,
374
+ };
375
+ if (globalOpts.json) {
376
+ output(payload, { json: true, compact: globalOpts.compact });
377
+ return;
378
+ }
379
+ success(`Created ${format} duplicate`);
380
+ console.log(` Source: ${sourceProjectId}`);
381
+ console.log(` New project: ${newProjectId}`);
382
+ console.log(` Title: ${payload.title}`);
383
+ console.log(` Format: ${payload.format}`);
384
+ if (activated) {
385
+ console.log(chalk.gray(' Set as active project'));
386
+ }
387
+ console.log(chalk.gray(' Note: scenes and project context were copied to the new project'));
388
+ }
389
+ catch (err) {
390
+ spinner?.stop();
391
+ handleProjectError(err, globalOpts);
392
+ }
393
+ });
394
+ /**
395
+ * baz project duplicate [project-id]
396
+ */
397
+ projectCommand
398
+ .command('duplicate [id]')
399
+ .description('Duplicate a project (optionally changing format)')
400
+ .option('--name <name>', 'Rename the duplicated project')
401
+ .option('--format <format>', 'Target format: landscape, portrait, square')
402
+ .option('--no-activate', "Don't set duplicated project as active")
403
+ .action(async (id, options, cmd) => {
404
+ const globalOpts = cmd.optsWithGlobals();
405
+ const config = loadConfig({
406
+ configPath: globalOpts.config,
407
+ apiUrl: globalOpts.apiUrl,
408
+ projectId: globalOpts.projectId,
409
+ });
410
+ if (!hasAuth(config)) {
411
+ exitProjectAuthError(globalOpts);
412
+ }
413
+ // Resolve source project
414
+ let sourceProjectId;
415
+ try {
416
+ sourceProjectId = id || getProjectId(config, globalOpts.projectId);
417
+ }
418
+ catch (err) {
419
+ if (globalOpts.json) {
420
+ output({
421
+ type: 'error',
422
+ code: 'VALIDATION',
423
+ message: err.message,
424
+ category: 'validation',
425
+ retryable: false,
426
+ transient: false,
427
+ exitCode: 64,
428
+ }, { json: true, compact: globalOpts.compact });
429
+ }
430
+ else {
431
+ error(err.message);
432
+ }
433
+ process.exit(64);
434
+ }
435
+ // Validate format if provided
436
+ let format;
437
+ if (options.format) {
438
+ format = normalizeProjectFormat(options.format) ?? undefined;
439
+ if (!format) {
440
+ if (globalOpts.json) {
441
+ output({
442
+ type: 'error',
443
+ code: 'VALIDATION',
444
+ message: `Invalid format "${options.format}". Use: landscape, portrait, square`,
445
+ category: 'validation',
446
+ retryable: false,
447
+ transient: false,
448
+ exitCode: 65,
449
+ }, { json: true, compact: globalOpts.compact });
450
+ }
451
+ else {
452
+ error(`Invalid format "${options.format}"`, 'Use: --format landscape|portrait|square');
453
+ }
454
+ process.exit(65);
455
+ }
456
+ }
457
+ const spinner = globalOpts.json ? null : ora('Duplicating project...').start();
458
+ try {
459
+ const duplicateResult = await apiRequest(config, 'project.duplicate', {
460
+ projectId: sourceProjectId,
461
+ ...(format && { format }),
462
+ });
463
+ const newProjectId = duplicateResult.projectId;
464
+ if (!newProjectId) {
465
+ spinner?.stop();
466
+ error('Failed to duplicate project - no new project ID returned');
467
+ process.exit(1);
468
+ }
469
+ // Rename if requested
470
+ if (options.name) {
471
+ await apiRequest(config, 'project.rename', {
472
+ id: newProjectId,
473
+ title: options.name,
474
+ });
475
+ }
476
+ // Fetch the final state
477
+ const project = await apiRequest(config, 'project.getById', { id: newProjectId }).catch(() => null);
478
+ const activated = options.activate !== false;
479
+ if (activated) {
480
+ saveConfig({ activeProjectId: newProjectId });
481
+ }
482
+ spinner?.stop();
483
+ const payload = {
484
+ type: 'project_duplicated',
485
+ sourceProjectId,
486
+ projectId: newProjectId,
487
+ title: project?.title || options.name || 'Untitled',
488
+ format: project?.props?.meta?.format || format || 'unchanged',
489
+ activated,
490
+ };
491
+ if (globalOpts.json) {
492
+ output(payload, { json: true, compact: globalOpts.compact });
493
+ return;
494
+ }
495
+ success(`Duplicated project`);
496
+ console.log(` Source: ${sourceProjectId}`);
497
+ console.log(` New project: ${newProjectId}`);
498
+ console.log(` Title: ${payload.title}`);
499
+ if (format)
500
+ console.log(` Format: ${payload.format}`);
501
+ if (activated)
502
+ console.log(chalk.gray(' Set as active project'));
503
+ }
504
+ catch (err) {
505
+ spinner?.stop();
506
+ handleProjectError(err, globalOpts);
507
+ }
508
+ });
509
+ /**
510
+ * baz project settings
511
+ */
512
+ projectCommand
513
+ .command('settings')
514
+ .description('View or change project settings (format, dimensions)')
515
+ .option('--aspect <ratio>', 'Set aspect ratio: 16:9, 9:16, 1:1')
516
+ .option('--format <format>', 'Set format: landscape, portrait, square')
517
+ .action(async (options, cmd) => {
518
+ const globalOpts = cmd.optsWithGlobals();
519
+ const config = loadConfig({
520
+ configPath: globalOpts.config,
521
+ apiUrl: globalOpts.apiUrl,
522
+ projectId: globalOpts.projectId,
523
+ });
524
+ if (!hasAuth(config)) {
525
+ exitProjectAuthError(globalOpts);
526
+ }
527
+ let projectId;
528
+ try {
529
+ projectId = getProjectId(config, globalOpts.projectId);
530
+ }
531
+ catch (err) {
532
+ if (globalOpts.json) {
533
+ output({
534
+ type: 'error',
535
+ code: 'VALIDATION',
536
+ message: err.message,
537
+ category: 'validation',
538
+ retryable: false,
539
+ transient: false,
540
+ exitCode: 64,
541
+ }, { json: true, compact: globalOpts.compact });
542
+ }
543
+ else {
544
+ error(err.message);
545
+ }
546
+ process.exit(64);
547
+ }
548
+ // Map aspect ratio shortcuts to format
549
+ const ASPECT_MAP = {
550
+ '16:9': { format: 'landscape', width: 1920, height: 1080 },
551
+ '9:16': { format: 'portrait', width: 1080, height: 1920 },
552
+ '1:1': { format: 'square', width: 1080, height: 1080 },
553
+ };
554
+ let targetFormat;
555
+ let targetWidth;
556
+ let targetHeight;
557
+ if (options.aspect) {
558
+ const mapped = ASPECT_MAP[options.aspect];
559
+ if (!mapped) {
560
+ if (globalOpts.json) {
561
+ output({
562
+ type: 'error',
563
+ code: 'VALIDATION',
564
+ message: `Invalid aspect ratio "${options.aspect}". Use: 16:9, 9:16, 1:1`,
565
+ category: 'validation',
566
+ retryable: false,
567
+ transient: false,
568
+ exitCode: 65,
569
+ }, { json: true, compact: globalOpts.compact });
570
+ }
571
+ else {
572
+ error(`Invalid aspect ratio "${options.aspect}"`, 'Use: --aspect 16:9|9:16|1:1');
573
+ }
574
+ process.exit(65);
575
+ }
576
+ targetFormat = mapped.format;
577
+ targetWidth = mapped.width;
578
+ targetHeight = mapped.height;
579
+ }
580
+ else if (options.format) {
581
+ const f = normalizeProjectFormat(options.format);
582
+ if (!f) {
583
+ if (globalOpts.json) {
584
+ output({
585
+ type: 'error',
586
+ code: 'VALIDATION',
587
+ message: `Invalid format "${options.format}". Use: landscape, portrait, square`,
588
+ category: 'validation',
589
+ retryable: false,
590
+ transient: false,
591
+ exitCode: 65,
592
+ }, { json: true, compact: globalOpts.compact });
593
+ }
594
+ else {
595
+ error(`Invalid format "${options.format}"`, 'Use: --format landscape|portrait|square');
596
+ }
597
+ process.exit(65);
598
+ }
599
+ const dimensions = ASPECT_MAP[f === 'landscape' ? '16:9' : f === 'portrait' ? '9:16' : '1:1'];
600
+ targetFormat = f;
601
+ targetWidth = dimensions.width;
602
+ targetHeight = dimensions.height;
603
+ }
604
+ const isUpdate = targetFormat !== undefined;
605
+ const spinner = globalOpts.json ? null : ora(isUpdate ? 'Updating settings...' : 'Fetching settings...').start();
606
+ try {
607
+ if (isUpdate) {
608
+ // Use project.patch with JSON Patch ops
609
+ await apiRequest(config, 'project.patch', {
610
+ projectId,
611
+ operations: [
612
+ { op: 'replace', path: '/meta/format', value: targetFormat },
613
+ { op: 'replace', path: '/meta/width', value: targetWidth },
614
+ { op: 'replace', path: '/meta/height', value: targetHeight },
615
+ ],
616
+ });
617
+ }
618
+ // Fetch current state
619
+ const project = await apiRequest(config, 'project.getById', { id: projectId });
620
+ spinner?.stop();
621
+ const meta = project.props?.meta || {};
622
+ const payload = {
623
+ type: isUpdate ? 'settings_updated' : 'settings',
624
+ projectId,
625
+ title: project.title || 'Untitled',
626
+ format: meta.format || 'landscape',
627
+ width: meta.width || 1920,
628
+ height: meta.height || 1080,
629
+ duration: meta.duration || 0,
630
+ fps: 30,
631
+ };
632
+ if (globalOpts.json) {
633
+ output(payload, { json: true, compact: globalOpts.compact });
634
+ return;
635
+ }
636
+ if (isUpdate) {
637
+ success(`Updated project settings`);
638
+ }
639
+ else {
640
+ console.log(chalk.bold('Project Settings'));
641
+ console.log(chalk.gray('─'.repeat(40)));
642
+ }
643
+ console.log(` Title: ${payload.title}`);
644
+ console.log(` Format: ${payload.format}`);
645
+ console.log(` Size: ${payload.width}x${payload.height}`);
646
+ console.log(` Duration: ${formatDuration(payload.duration)}`);
647
+ console.log(` FPS: ${payload.fps}`);
648
+ }
649
+ catch (err) {
650
+ spinner?.stop();
651
+ handleProjectError(err, globalOpts);
652
+ }
653
+ });
654
+ /**
655
+ * baz project validate
656
+ */
657
+ projectCommand
658
+ .command('validate')
659
+ .description('Validate project timeline structure')
660
+ .action(async (options, cmd) => {
661
+ const globalOpts = cmd.optsWithGlobals();
662
+ const config = loadConfig({
663
+ configPath: globalOpts.config,
664
+ apiUrl: globalOpts.apiUrl,
665
+ projectId: globalOpts.projectId,
666
+ });
667
+ if (!hasAuth(config)) {
668
+ exitProjectAuthError(globalOpts);
669
+ }
670
+ let projectId;
671
+ try {
672
+ projectId = getProjectId(config, globalOpts.projectId);
673
+ }
674
+ catch (err) {
675
+ if (globalOpts.json) {
676
+ output({
677
+ type: 'error',
678
+ code: 'VALIDATION',
679
+ message: err.message,
680
+ category: 'validation',
681
+ retryable: false,
682
+ transient: false,
683
+ exitCode: 64,
684
+ }, { json: true, compact: globalOpts.compact });
685
+ }
686
+ else {
687
+ error(err.message);
688
+ }
689
+ process.exit(64);
690
+ }
691
+ const spinner = globalOpts.json ? null : ora('Validating project...').start();
692
+ try {
693
+ const result = await apiRequest(config, 'project.validate', { projectId });
694
+ spinner?.stop();
695
+ const payload = {
696
+ type: result.type || 'project_validation',
697
+ projectId,
698
+ projectTitle: result.projectTitle || 'Untitled',
699
+ valid: Boolean(result.valid),
700
+ summary: result.summary || {
701
+ sceneCount: 0,
702
+ trackCount: 0,
703
+ totalFrames: 0,
704
+ totalDurationSeconds: 0,
705
+ compilationErrorCount: 0,
706
+ },
707
+ errors: Array.isArray(result.errors) ? result.errors : [],
708
+ warnings: Array.isArray(result.warnings) ? result.warnings : [],
709
+ };
710
+ if (globalOpts.json) {
711
+ output(payload, { json: true, compact: globalOpts.compact });
712
+ }
713
+ else {
714
+ console.log(chalk.bold('Project Validation'));
715
+ console.log(chalk.gray('─'.repeat(60)));
716
+ console.log(`Project: ${payload.projectTitle} (${projectId.slice(0, 8)}...)`);
717
+ console.log(`Scenes: ${payload.summary.sceneCount}`);
718
+ console.log(`Tracks: ${payload.summary.trackCount}`);
719
+ console.log(`Duration: ${formatDuration(payload.summary.totalDurationSeconds)}`);
720
+ console.log(`Compilation errors: ${payload.summary.compilationErrorCount}`);
721
+ console.log();
722
+ if (payload.errors.length === 0 && payload.warnings.length === 0) {
723
+ console.log(chalk.green('✓ No validation issues found.'));
724
+ }
725
+ else {
726
+ for (const issue of payload.errors) {
727
+ console.log(`${chalk.red('✗')} [${issue.code}] ${issue.message}`);
728
+ }
729
+ for (const issue of payload.warnings) {
730
+ console.log(`${chalk.yellow('!')} [${issue.code}] ${issue.message}`);
731
+ }
732
+ }
733
+ }
734
+ process.exit(payload.valid ? 0 : 65);
735
+ }
736
+ catch (err) {
737
+ spinner?.stop();
738
+ handleProjectError(err, globalOpts);
739
+ }
740
+ });
741
+ /**
742
+ * baz project delete <id>
743
+ */
744
+ projectCommand
745
+ .command('delete <id>')
746
+ .description('Delete a project')
747
+ .option('--force', 'Skip confirmation')
748
+ .action(async (id, options, cmd) => {
749
+ const globalOpts = cmd.optsWithGlobals();
750
+ const config = loadConfig({
751
+ configPath: globalOpts.config,
752
+ apiUrl: globalOpts.apiUrl,
753
+ });
754
+ if (!hasAuth(config)) {
755
+ exitProjectAuthError(globalOpts);
756
+ }
757
+ if (!options.force) {
758
+ const confirmed = await confirmAction(`Delete project ${id}? This cannot be undone.`);
759
+ if (!confirmed) {
760
+ if (globalOpts.json) {
761
+ output({ deleted: false, id, reason: 'cancelled' }, { json: true });
762
+ return;
763
+ }
764
+ console.log(chalk.gray('Cancelled. Use --force to skip confirmation.'));
765
+ return;
766
+ }
767
+ }
768
+ const spinner = globalOpts.json ? null : ora('Deleting project...').start();
769
+ try {
770
+ await apiRequest(config, 'project.delete', { id });
771
+ spinner?.stop();
772
+ // Clear active project if it was deleted
773
+ if (config.activeProjectId === id) {
774
+ saveConfig({ activeProjectId: undefined });
775
+ }
776
+ if (globalOpts.json) {
777
+ output({ deleted: true, id }, { json: true });
778
+ return;
779
+ }
780
+ success(`Deleted project: ${id}`);
781
+ }
782
+ catch (err) {
783
+ spinner?.stop();
784
+ handleProjectError(err, globalOpts);
785
+ }
786
+ });