bazaar.it 0.2.1 → 0.2.2

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/README.md CHANGED
@@ -66,7 +66,7 @@ $ baz
66
66
  ██████╔╝██║ ██║███████╗
67
67
  ╚═════╝ ╚═╝ ╚═╝╚══════╝
68
68
 
69
- AI-powered video generation v0.2.0
69
+ AI-powered video generation v0.2.2
70
70
  ─────────────────────────────────────
71
71
  Type a command or prompt. Use 'help' for commands.
72
72
 
@@ -157,14 +157,11 @@ baz prompt "Create something cool" --verbose
157
157
  # List all scenes in active project
158
158
  baz scenes list
159
159
 
160
- # Get scene details
161
- baz scenes list
162
-
163
160
  # Get scene TSX code
164
161
  baz scenes code <scene-id>
165
162
 
166
- # Get scene code
167
- baz scenes code <scene-id>
163
+ # Replace scene TSX code from file
164
+ baz scenes set-code <scene-id> --file ./scene.tsx
168
165
  ```
169
166
 
170
167
  ### Media Upload
@@ -346,6 +343,7 @@ Returns:
346
343
 
347
344
  ```
348
345
  --json Output as JSON (for scripting)
346
+ --compact Compact JSON output (token-efficient for agents)
349
347
  --verbose Show detailed output
350
348
  --config <path> Custom config file path
351
349
  --api-url <url> Override API URL
@@ -406,7 +404,7 @@ export BAZ_API_KEY="$BAZAAR_API_KEY"
406
404
  baz prompt "Create demo video: $PR_DESCRIPTION" --project-id "$PROJECT_ID"
407
405
 
408
406
  # Export when ready
409
- baz export --json > export-result.json
407
+ baz export start --wait --json > export-result.json
410
408
  ```
411
409
 
412
410
  ### Complex Workflow
@@ -4,6 +4,15 @@ import { loadConfig, saveConfig, hasAuth, getConfigPath } from '../lib/config.js
4
4
  import { success, error, info, output } from '../lib/output.js';
5
5
  export const authCommand = new Command('auth')
6
6
  .description('Manage authentication');
7
+ function getGlobalOptsFromActionArgs(actionArgs) {
8
+ for (let i = actionArgs.length - 1; i >= 0; i--) {
9
+ const maybeCmd = actionArgs[i];
10
+ if (maybeCmd && typeof maybeCmd.optsWithGlobals === 'function') {
11
+ return maybeCmd.optsWithGlobals();
12
+ }
13
+ }
14
+ return {};
15
+ }
7
16
  /**
8
17
  * baz auth login
9
18
  */
@@ -11,7 +20,13 @@ authCommand
11
20
  .command('login')
12
21
  .description('Configure API key for authentication')
13
22
  .argument('[api-key]', 'API key (or set BAZ_API_KEY env var)')
14
- .action(async (apiKey) => {
23
+ .action(async (...actionArgs) => {
24
+ const apiKey = actionArgs[0];
25
+ const globalOpts = getGlobalOptsFromActionArgs(actionArgs);
26
+ const config = loadConfig({
27
+ configPath: globalOpts.config,
28
+ apiUrl: globalOpts.apiUrl,
29
+ });
15
30
  // If no key provided, check environment
16
31
  const key = apiKey || process.env.BAZ_API_KEY;
17
32
  if (!key) {
@@ -23,7 +38,7 @@ authCommand
23
38
  console.log(' 1. Pass directly: baz auth login <your-api-key>');
24
39
  console.log(' 2. Environment: export BAZ_API_KEY=<your-api-key>');
25
40
  console.log();
26
- console.log(chalk.gray('Get your API key at https://bazaar.it/api'));
41
+ console.log(chalk.gray('Get your API key at https://bazaar.it/settings/api-keys'));
27
42
  process.exit(1);
28
43
  }
29
44
  // Validate key format
@@ -32,9 +47,8 @@ authCommand
32
47
  process.exit(1);
33
48
  }
34
49
  // Save to config first (needed for the validation request)
35
- saveConfig({ apiKey: key });
50
+ saveConfig({ apiKey: key, apiUrl: config.apiUrl }, { configPath: globalOpts.config });
36
51
  // Validate key against server
37
- const config = loadConfig();
38
52
  try {
39
53
  const url = `${config.apiUrl}/api/trpc/apiKey.list`;
40
54
  const res = await fetch(url, {
@@ -43,10 +57,10 @@ authCommand
43
57
  });
44
58
  if (res.ok) {
45
59
  success('Authenticated successfully');
46
- console.log(` Key saved to ${chalk.gray(getConfigPath())}`);
60
+ console.log(` Key saved to ${chalk.gray(getConfigPath(globalOpts.config))}`);
47
61
  }
48
62
  else if (res.status === 401) {
49
- saveConfig({ apiKey: undefined });
63
+ saveConfig({ apiKey: undefined, apiUrl: config.apiUrl }, { configPath: globalOpts.config });
50
64
  error('Invalid API key', 'The key was not recognized by the server. Check for typos or generate a new key.');
51
65
  process.exit(1);
52
66
  }
@@ -66,8 +80,8 @@ authCommand
66
80
  authCommand
67
81
  .command('status')
68
82
  .description('Show current authentication status')
69
- .action(async (options, cmd) => {
70
- const globalOpts = cmd.optsWithGlobals();
83
+ .action(async (...actionArgs) => {
84
+ const globalOpts = getGlobalOptsFromActionArgs(actionArgs);
71
85
  const config = loadConfig({
72
86
  configPath: globalOpts.config,
73
87
  apiUrl: globalOpts.apiUrl,
@@ -76,10 +90,10 @@ authCommand
76
90
  authenticated: hasAuth(config),
77
91
  apiUrl: config.apiUrl,
78
92
  activeProjectId: config.activeProjectId || null,
79
- configFile: getConfigPath(),
93
+ configFile: getConfigPath(globalOpts.config),
80
94
  };
81
95
  if (globalOpts.json) {
82
- output(data, { json: true });
96
+ output(data, { json: true, compact: globalOpts.compact });
83
97
  return;
84
98
  }
85
99
  console.log(chalk.cyan('Bazaar CLI Status'));
@@ -95,7 +109,7 @@ authCommand
95
109
  console.log();
96
110
  console.log(`API URL: ${config.apiUrl}`);
97
111
  console.log(`Active Project: ${config.activeProjectId || chalk.gray('none')}`);
98
- console.log(`Config: ${getConfigPath()}`);
112
+ console.log(`Config: ${getConfigPath(globalOpts.config)}`);
99
113
  });
100
114
  /**
101
115
  * baz auth logout
@@ -103,7 +117,12 @@ authCommand
103
117
  authCommand
104
118
  .command('logout')
105
119
  .description('Clear stored credentials')
106
- .action(async () => {
107
- saveConfig({ apiKey: undefined });
120
+ .action(async (...actionArgs) => {
121
+ const globalOpts = getGlobalOptsFromActionArgs(actionArgs);
122
+ const config = loadConfig({
123
+ configPath: globalOpts.config,
124
+ apiUrl: globalOpts.apiUrl,
125
+ });
126
+ saveConfig({ apiKey: undefined, apiUrl: config.apiUrl }, { configPath: globalOpts.config });
108
127
  success('Logged out. API key removed from config.');
109
128
  });
@@ -68,7 +68,7 @@ exportCommand
68
68
  }
69
69
  process.exit(64); // Input error exit code
70
70
  }
71
- const spinner = ora('Starting export...').start();
71
+ const spinner = globalOpts.json ? null : ora('Starting export...').start();
72
72
  const startTime = Date.now();
73
73
  const timeoutMs = parseInt(options.timeout, 10) * 1000;
74
74
  try {
@@ -80,7 +80,8 @@ exportCommand
80
80
  const renderId = result.renderId || result.id;
81
81
  // If --wait flag, block until complete
82
82
  if (options.wait) {
83
- spinner.text = 'Rendering...';
83
+ if (spinner)
84
+ spinner.text = 'Rendering...';
84
85
  const checkStatus = async () => {
85
86
  return await apiRequest(config, 'render.getRenderStatus', { renderId });
86
87
  };
@@ -90,7 +91,7 @@ exportCommand
90
91
  // Check timeout
91
92
  const elapsed = Date.now() - startTime;
92
93
  if (elapsed > timeoutMs) {
93
- spinner.fail();
94
+ spinner?.fail();
94
95
  if (globalOpts.json) {
95
96
  console.log(JSON.stringify({
96
97
  type: 'error',
@@ -122,11 +123,12 @@ exportCommand
122
123
  }));
123
124
  }
124
125
  }
125
- spinner.text = `Rendering... ${progress}%`;
126
+ if (spinner)
127
+ spinner.text = `Rendering... ${progress}%`;
126
128
  await new Promise(r => setTimeout(r, 3000));
127
129
  status = await checkStatus();
128
130
  }
129
- spinner.stop();
131
+ spinner?.stop();
130
132
  if (status.status === 'completed' || status.status === 'done') {
131
133
  const finalResult = {
132
134
  type: 'export_completed',
@@ -137,7 +139,7 @@ exportCommand
137
139
  continue: false,
138
140
  };
139
141
  if (globalOpts.json) {
140
- output(finalResult, { json: true });
142
+ output(finalResult, { json: true, compact: globalOpts.compact });
141
143
  }
142
144
  else {
143
145
  success('Export complete!');
@@ -168,14 +170,14 @@ exportCommand
168
170
  }
169
171
  else {
170
172
  // Non-blocking mode (original behavior)
171
- spinner.stop();
173
+ spinner?.stop();
172
174
  if (globalOpts.json) {
173
175
  output({
174
176
  type: 'export_started',
175
177
  exportId: renderId,
176
178
  status: 'pending',
177
179
  continue: true, // Agent should poll for completion
178
- }, { json: true });
180
+ }, { json: true, compact: globalOpts.compact });
179
181
  return;
180
182
  }
181
183
  success(`Export started!`);
@@ -186,7 +188,7 @@ exportCommand
186
188
  }
187
189
  }
188
190
  catch (err) {
189
- spinner.stop();
191
+ spinner?.stop();
190
192
  if (err instanceof ApiError) {
191
193
  if (globalOpts.json) {
192
194
  output({ ...err.toJSON(), continue: false }, { json: true, compact: globalOpts.compact });
@@ -237,18 +239,19 @@ exportCommand
237
239
  return result;
238
240
  };
239
241
  if (options.wait) {
240
- const spinner = ora('Waiting for export to complete...').start();
242
+ const spinner = globalOpts.json ? null : ora('Waiting for export to complete...').start();
241
243
  try {
242
244
  let status = await checkStatus();
243
245
  while (status.status === 'pending' || status.status === 'rendering' || status.status === 'in_progress') {
244
246
  const progress = status.progress || 0;
245
- spinner.text = `Rendering... ${progress}%`;
247
+ if (spinner)
248
+ spinner.text = `Rendering... ${progress}%`;
246
249
  await new Promise(r => setTimeout(r, 3000));
247
250
  status = await checkStatus();
248
251
  }
249
- spinner.stop();
252
+ spinner?.stop();
250
253
  if (globalOpts.json) {
251
- output(status, { json: true });
254
+ output(status, { json: true, compact: globalOpts.compact });
252
255
  return;
253
256
  }
254
257
  if (status.status === 'completed' || status.status === 'done') {
@@ -263,18 +266,40 @@ exportCommand
263
266
  }
264
267
  }
265
268
  catch (err) {
266
- spinner.stop();
267
- error(err.message);
269
+ spinner?.stop();
270
+ if (err instanceof ApiError) {
271
+ if (globalOpts.json) {
272
+ output(err.toJSON(), { json: true, compact: globalOpts.compact });
273
+ }
274
+ else {
275
+ error(err.message, err.suggestion);
276
+ }
277
+ process.exit(err.exitCode);
278
+ }
279
+ if (globalOpts.json) {
280
+ output({
281
+ type: 'error',
282
+ code: 'UNKNOWN',
283
+ message: err.message,
284
+ category: 'fatal',
285
+ retryable: false,
286
+ transient: false,
287
+ exitCode: 1,
288
+ }, { json: true, compact: globalOpts.compact });
289
+ }
290
+ else {
291
+ error(err.message);
292
+ }
268
293
  process.exit(1);
269
294
  }
270
295
  }
271
296
  else {
272
- const spinner = ora('Checking status...').start();
297
+ const spinner = globalOpts.json ? null : ora('Checking status...').start();
273
298
  try {
274
299
  const status = await checkStatus();
275
- spinner.stop();
300
+ spinner?.stop();
276
301
  if (globalOpts.json) {
277
- output(status, { json: true });
302
+ output(status, { json: true, compact: globalOpts.compact });
278
303
  return;
279
304
  }
280
305
  const statusColor = status.status === 'completed' || status.status === 'done' ? chalk.green :
@@ -292,8 +317,30 @@ exportCommand
292
317
  }
293
318
  }
294
319
  catch (err) {
295
- spinner.stop();
296
- error(err.message);
320
+ spinner?.stop();
321
+ if (err instanceof ApiError) {
322
+ if (globalOpts.json) {
323
+ output(err.toJSON(), { json: true, compact: globalOpts.compact });
324
+ }
325
+ else {
326
+ error(err.message, err.suggestion);
327
+ }
328
+ process.exit(err.exitCode);
329
+ }
330
+ if (globalOpts.json) {
331
+ output({
332
+ type: 'error',
333
+ code: 'UNKNOWN',
334
+ message: err.message,
335
+ category: 'fatal',
336
+ retryable: false,
337
+ transient: false,
338
+ exitCode: 1,
339
+ }, { json: true, compact: globalOpts.compact });
340
+ }
341
+ else {
342
+ error(err.message);
343
+ }
297
344
  process.exit(1);
298
345
  }
299
346
  }
@@ -320,18 +367,31 @@ exportCommand
320
367
  projectId = getProjectId(config, globalOpts.projectId);
321
368
  }
322
369
  catch (err) {
323
- error(err.message);
324
- process.exit(1);
370
+ if (globalOpts.json) {
371
+ output({
372
+ type: 'error',
373
+ code: 'VALIDATION',
374
+ message: err.message,
375
+ category: 'validation',
376
+ retryable: false,
377
+ transient: false,
378
+ exitCode: 64,
379
+ }, { json: true, compact: globalOpts.compact });
380
+ }
381
+ else {
382
+ error(err.message);
383
+ }
384
+ process.exit(64);
325
385
  }
326
- const spinner = ora('Fetching exports...').start();
386
+ const spinner = globalOpts.json ? null : ora('Fetching exports...').start();
327
387
  try {
328
388
  const result = await apiRequest(config, 'render.listRenders', {
329
389
  projectId,
330
390
  limit: parseInt(options.limit, 10),
331
391
  });
332
- spinner.stop();
392
+ spinner?.stop();
333
393
  if (globalOpts.json) {
334
- output(result, { json: true });
394
+ output(result, { json: true, compact: globalOpts.compact });
335
395
  return;
336
396
  }
337
397
  const renders = Array.isArray(result) ? result : result.renders || [];
@@ -353,8 +413,30 @@ exportCommand
353
413
  }
354
414
  }
355
415
  catch (err) {
356
- spinner.stop();
357
- error(err.message);
416
+ spinner?.stop();
417
+ if (err instanceof ApiError) {
418
+ if (globalOpts.json) {
419
+ output(err.toJSON(), { json: true, compact: globalOpts.compact });
420
+ }
421
+ else {
422
+ error(err.message, err.suggestion);
423
+ }
424
+ process.exit(err.exitCode);
425
+ }
426
+ if (globalOpts.json) {
427
+ output({
428
+ type: 'error',
429
+ code: 'UNKNOWN',
430
+ message: err.message,
431
+ category: 'fatal',
432
+ retryable: false,
433
+ transient: false,
434
+ exitCode: 1,
435
+ }, { json: true, compact: globalOpts.compact });
436
+ }
437
+ else {
438
+ error(err.message);
439
+ }
358
440
  process.exit(1);
359
441
  }
360
442
  });
@@ -75,7 +75,7 @@ function parseBudgetMs(input) {
75
75
  }
76
76
  export const loopCommand = new Command('loop')
77
77
  .description('Iterate prompt → review in a basic OODA loop')
78
- .argument('<text>', 'The prompt text (or use --file)')
78
+ .argument('[text]', 'The prompt text (or use --file)')
79
79
  .option('--mode <mode>', 'Generation mode: agent (default), agent-max, multi-scene', 'agent')
80
80
  .option('--max', 'Shorthand for --mode agent-max')
81
81
  .option('--max-iterations <n>', 'Maximum iterations (default: 3)', '3')
@@ -93,6 +93,15 @@ export const loopCommand = new Command('loop')
93
93
  const agentModeEnv = process.env.BAZ_AGENT === '1';
94
94
  const jsonOutput = globalOpts.json || agentModeEnv;
95
95
  const streamJson = options.streamJson === true || agentModeEnv;
96
+ const emitCliError = (payload, fallbackMessage, fallbackSuggestion) => {
97
+ if (streamJson || jsonOutput) {
98
+ console.log(JSON.stringify(payload));
99
+ }
100
+ else {
101
+ error(fallbackMessage, fallbackSuggestion);
102
+ }
103
+ process.exit(typeof payload.exitCode === 'number' ? payload.exitCode : 1);
104
+ };
96
105
  const config = loadConfig({
97
106
  configPath: globalOpts.config,
98
107
  apiUrl: globalOpts.apiUrl,
@@ -141,13 +150,27 @@ export const loopCommand = new Command('loop')
141
150
  promptText = readFileSync(options.file, 'utf-8').trim();
142
151
  }
143
152
  catch {
144
- error(`Failed to read file: ${options.file}`);
145
- process.exit(1);
153
+ emitCliError({
154
+ type: 'error',
155
+ code: 'VALIDATION',
156
+ message: `Failed to read file: ${options.file}`,
157
+ category: 'validation',
158
+ retryable: false,
159
+ continue: false,
160
+ exitCode: 64,
161
+ }, `Failed to read file: ${options.file}`);
146
162
  }
147
163
  }
148
164
  if (!promptText) {
149
- error('No prompt provided');
150
- process.exit(1);
165
+ emitCliError({
166
+ type: 'error',
167
+ code: 'VALIDATION',
168
+ message: 'No prompt provided',
169
+ category: 'validation',
170
+ retryable: false,
171
+ continue: false,
172
+ exitCode: 64,
173
+ }, 'No prompt provided');
151
174
  }
152
175
  const mode = options.max ? 'agent-max' : options.mode;
153
176
  const requirements = normalizeRequirements(options.requirements);
@@ -159,39 +182,60 @@ export const loopCommand = new Command('loop')
159
182
  if (options.plan)
160
183
  maxIterations = 1;
161
184
  if (budgetMs !== undefined && budgetMs <= 0) {
162
- error('Budget must be a positive duration (e.g., 30s, 2m)');
163
- process.exit(64);
185
+ emitCliError({
186
+ type: 'error',
187
+ code: 'VALIDATION',
188
+ message: 'Budget must be a positive duration (e.g., 30s, 2m)',
189
+ category: 'validation',
190
+ retryable: false,
191
+ continue: false,
192
+ exitCode: 64,
193
+ }, 'Budget must be a positive duration (e.g., 30s, 2m)');
164
194
  }
165
195
  // Handle image uploads
166
196
  const imageUrls = [];
167
197
  if (options.image && options.image.length > 0) {
168
- const uploadSpinner = ora('Uploading images...').start();
198
+ const uploadSpinner = (streamJson || jsonOutput) ? null : ora('Uploading images...').start();
169
199
  try {
170
200
  for (const imagePath of options.image) {
171
201
  if (!existsSync(imagePath)) {
172
- uploadSpinner.fail();
173
- error(`Image file not found: ${imagePath}`);
174
- process.exit(1);
202
+ uploadSpinner?.fail();
203
+ emitCliError({
204
+ type: 'error',
205
+ code: 'NOT_FOUND',
206
+ message: `Image file not found: ${imagePath}`,
207
+ category: 'validation',
208
+ retryable: false,
209
+ continue: false,
210
+ exitCode: 64,
211
+ }, `Image file not found: ${imagePath}`);
175
212
  }
176
213
  const imageBuffer = readFileSync(imagePath);
177
214
  const url = await uploadImage(config, { projectId, buffer: imageBuffer, filename: imagePath });
178
215
  imageUrls.push(url);
179
216
  }
180
- uploadSpinner.succeed(`Uploaded ${imageUrls.length} image${imageUrls.length > 1 ? 's' : ''}`);
217
+ uploadSpinner?.succeed(`Uploaded ${imageUrls.length} image${imageUrls.length > 1 ? 's' : ''}`);
181
218
  }
182
219
  catch (err) {
183
- uploadSpinner.fail();
220
+ uploadSpinner?.fail();
184
221
  const errMsg = err instanceof ApiError
185
222
  ? `${err.message}${err.suggestion ? ` — ${err.suggestion}` : ''}`
186
223
  : err.message;
187
- error(`Failed to upload images: ${errMsg}`);
188
- process.exit(1);
224
+ emitCliError({
225
+ type: 'error',
226
+ code: err instanceof ApiError ? err.code : 'UNKNOWN',
227
+ message: `Failed to upload images: ${errMsg}`,
228
+ category: err instanceof ApiError ? err.category : 'fatal',
229
+ retryable: err instanceof ApiError ? err.retryable : false,
230
+ continue: false,
231
+ exitCode: err instanceof ApiError ? err.exitCode : 1,
232
+ }, `Failed to upload images: ${errMsg}`);
189
233
  }
190
234
  }
191
235
  // Handle URL attachments
192
236
  const urlContents = [];
193
237
  if (options.url && options.url.length > 0) {
194
- const urlSpinner = ora('Fetching URLs...').start();
238
+ const urlSpinner = (streamJson || jsonOutput) ? null : ora('Fetching URLs...').start();
195
239
  try {
196
240
  for (const url of options.url) {
197
241
  const result = await fetchUrlContent(url);
@@ -201,15 +245,22 @@ export const loopCommand = new Command('loop')
201
245
  content: result.content.slice(0, 10000),
202
246
  });
203
247
  }
204
- urlSpinner.succeed(`Fetched ${urlContents.length} URL${urlContents.length > 1 ? 's' : ''}`);
248
+ urlSpinner?.succeed(`Fetched ${urlContents.length} URL${urlContents.length > 1 ? 's' : ''}`);
205
249
  }
206
250
  catch (err) {
207
- urlSpinner.fail();
251
+ urlSpinner?.fail();
208
252
  const errMsg = err instanceof ApiError
209
253
  ? `${err.message}${err.details ? ` — ${err.details}` : ''}`
210
254
  : err.message;
211
- error(`Failed to fetch URL: ${errMsg}`);
212
- process.exit(1);
255
+ emitCliError({
256
+ type: 'error',
257
+ code: err instanceof ApiError ? err.code : 'UNKNOWN',
258
+ message: `Failed to fetch URL: ${errMsg}`,
259
+ category: err instanceof ApiError ? err.category : 'fatal',
260
+ retryable: err instanceof ApiError ? err.retryable : false,
261
+ continue: false,
262
+ exitCode: err instanceof ApiError ? err.exitCode : 1,
263
+ }, `Failed to fetch URL: ${errMsg}`);
213
264
  }
214
265
  if (urlContents.length > 0) {
215
266
  const urlContext = urlContents.map(u => `--- Content from ${u.url}${u.title ? ` (${u.title})` : ''} ---\n${u.content}`).join('\n\n');
@@ -249,6 +300,7 @@ export const loopCommand = new Command('loop')
249
300
  let currentPrompt = basePrompt;
250
301
  const results = [];
251
302
  const loopStart = Date.now();
303
+ let fatalErrorMessage = null;
252
304
  const emit = (payload) => {
253
305
  if (streamJson) {
254
306
  console.log(JSON.stringify(payload));
@@ -376,6 +428,7 @@ export const loopCommand = new Command('loop')
376
428
  if (spinner)
377
429
  spinner.stop();
378
430
  const message = err instanceof Error ? err.message : String(err);
431
+ fatalErrorMessage = message;
379
432
  if (streamJson || jsonOutput) {
380
433
  console.log(JSON.stringify({
381
434
  type: 'error',
@@ -535,4 +588,17 @@ export const loopCommand = new Command('loop')
535
588
  projectId,
536
589
  mode,
537
590
  });
591
+ if (fatalErrorMessage) {
592
+ process.exit(1);
593
+ }
594
+ const lastResult = results.length > 0 ? results[results.length - 1] : null;
595
+ if (lastResult) {
596
+ if (lastResult.success === false) {
597
+ process.exit(1);
598
+ }
599
+ if (Array.isArray(lastResult.requirements?.missing) &&
600
+ lastResult.requirements.missing.length > 0) {
601
+ process.exit(65);
602
+ }
603
+ }
538
604
  });
@@ -4,7 +4,7 @@ import ora from 'ora';
4
4
  import { readFileSync, existsSync, statSync } from 'fs';
5
5
  import { basename, extname } from 'path';
6
6
  import { loadConfig, hasAuth, getProjectId } from '../lib/config.js';
7
- import { apiRequest, uploadMedia } from '../lib/api.js';
7
+ import { apiRequest, uploadMedia, ApiError } from '../lib/api.js';
8
8
  import { error, output, table } from '../lib/output.js';
9
9
  /**
10
10
  * Format file size in human-readable format
@@ -204,7 +204,7 @@ mediaCommand
204
204
  console.log(chalk.gray(` Project: ${projectId}`));
205
205
  console.log();
206
206
  }
207
- const spinner = ora('Uploading...').start();
207
+ const spinner = globalOpts.json ? null : ora('Uploading...').start();
208
208
  try {
209
209
  const buffer = readFileSync(filePath);
210
210
  const result = await uploadMedia(config, {
@@ -213,9 +213,9 @@ mediaCommand
213
213
  filename,
214
214
  mimeType,
215
215
  });
216
- spinner.succeed('Upload complete');
216
+ spinner?.succeed('Upload complete');
217
217
  if (globalOpts.json) {
218
- output(result, { json: true });
218
+ output(result, { json: true, compact: globalOpts.compact });
219
219
  }
220
220
  else {
221
221
  console.log();
@@ -230,7 +230,16 @@ mediaCommand
230
230
  }
231
231
  }
232
232
  catch (err) {
233
- spinner.fail('Upload failed');
233
+ spinner?.fail('Upload failed');
234
+ if (err instanceof ApiError) {
235
+ if (globalOpts.json) {
236
+ output(err.toJSON(), { json: true, compact: globalOpts.compact });
237
+ }
238
+ else {
239
+ error(err.message, err.suggestion);
240
+ }
241
+ process.exit(err.exitCode);
242
+ }
234
243
  if (globalOpts.json) {
235
244
  output({
236
245
  type: 'error',
@@ -343,6 +352,15 @@ mediaCommand
343
352
  }
344
353
  catch (err) {
345
354
  spinner?.stop();
355
+ if (err instanceof ApiError) {
356
+ if (globalOpts.json) {
357
+ output(err.toJSON(), { json: true, compact: globalOpts.compact });
358
+ }
359
+ else {
360
+ error(err.message, err.suggestion);
361
+ }
362
+ process.exit(err.exitCode);
363
+ }
346
364
  if (globalOpts.json) {
347
365
  output({
348
366
  type: 'error',
@@ -115,7 +115,7 @@ projectCommand
115
115
  spinner?.stop();
116
116
  // Set as active project unless --no-activate
117
117
  if (options.activate !== false) {
118
- saveConfig({ activeProjectId: projectId });
118
+ saveConfig({ activeProjectId: projectId }, { configPath: globalOpts.config });
119
119
  }
120
120
  if (globalOpts.json) {
121
121
  output({
@@ -178,7 +178,7 @@ projectCommand
178
178
  updatedAt: p.updatedAt,
179
179
  createdAt: p.createdAt,
180
180
  }));
181
- output(lean, { json: true });
181
+ output(lean, { json: true, compact: globalOpts.compact });
182
182
  return;
183
183
  }
184
184
  if (projects.length === 0) {
@@ -229,9 +229,9 @@ projectCommand
229
229
  try {
230
230
  const result = await apiRequest(config, 'project.getById', { id });
231
231
  spinner?.stop();
232
- saveConfig({ activeProjectId: id });
232
+ saveConfig({ activeProjectId: id }, { configPath: globalOpts.config });
233
233
  if (globalOpts.json) {
234
- output({ activeProjectId: id, project: result }, { json: true });
234
+ output({ activeProjectId: id, project: result }, { json: true, compact: globalOpts.compact });
235
235
  return;
236
236
  }
237
237
  success(`Active project: ${chalk.bold(result.title || result.name || 'Untitled')} (${id})`);
@@ -256,7 +256,7 @@ projectCommand
256
256
  });
257
257
  if (!config.activeProjectId) {
258
258
  if (globalOpts.json) {
259
- output({ activeProjectId: null }, { json: true });
259
+ output({ activeProjectId: null }, { json: true, compact: globalOpts.compact });
260
260
  return;
261
261
  }
262
262
  console.log(chalk.gray('No active project set.'));
@@ -273,7 +273,7 @@ projectCommand
273
273
  });
274
274
  spinner?.stop();
275
275
  if (globalOpts.json) {
276
- output(result, { json: true });
276
+ output(result, { json: true, compact: globalOpts.compact });
277
277
  return;
278
278
  }
279
279
  console.log(chalk.cyan('Active Project'));
@@ -361,7 +361,7 @@ projectCommand
361
361
  const duplicatedProject = await apiRequest(config, 'project.getById', { id: newProjectId }).catch(() => null);
362
362
  const activated = options.activate !== false;
363
363
  if (activated) {
364
- saveConfig({ activeProjectId: newProjectId });
364
+ saveConfig({ activeProjectId: newProjectId }, { configPath: globalOpts.config });
365
365
  }
366
366
  spinner?.stop();
367
367
  const payload = {
@@ -477,7 +477,7 @@ projectCommand
477
477
  const project = await apiRequest(config, 'project.getById', { id: newProjectId }).catch(() => null);
478
478
  const activated = options.activate !== false;
479
479
  if (activated) {
480
- saveConfig({ activeProjectId: newProjectId });
480
+ saveConfig({ activeProjectId: newProjectId }, { configPath: globalOpts.config });
481
481
  }
482
482
  spinner?.stop();
483
483
  const payload = {
@@ -758,7 +758,7 @@ projectCommand
758
758
  const confirmed = await confirmAction(`Delete project ${id}? This cannot be undone.`);
759
759
  if (!confirmed) {
760
760
  if (globalOpts.json) {
761
- output({ deleted: false, id, reason: 'cancelled' }, { json: true });
761
+ output({ deleted: false, id, reason: 'cancelled' }, { json: true, compact: globalOpts.compact });
762
762
  return;
763
763
  }
764
764
  console.log(chalk.gray('Cancelled. Use --force to skip confirmation.'));
@@ -771,10 +771,10 @@ projectCommand
771
771
  spinner?.stop();
772
772
  // Clear active project if it was deleted
773
773
  if (config.activeProjectId === id) {
774
- saveConfig({ activeProjectId: undefined });
774
+ saveConfig({ activeProjectId: undefined }, { configPath: globalOpts.config });
775
775
  }
776
776
  if (globalOpts.json) {
777
- output({ deleted: true, id }, { json: true });
777
+ output({ deleted: true, id }, { json: true, compact: globalOpts.compact });
778
778
  return;
779
779
  }
780
780
  success(`Deleted project: ${id}`);
@@ -44,53 +44,53 @@ export const promptCommand = new Command('prompt')
44
44
  const globalOpts = cmd.optsWithGlobals();
45
45
  const agentModeEnv = process.env.BAZ_AGENT === '1';
46
46
  const jsonOutput = globalOpts.json || agentModeEnv;
47
+ const streamJson = options.streamJson === true || agentModeEnv;
48
+ const emitCliError = (payload, fallbackMessage, fallbackSuggestion) => {
49
+ if (streamJson || jsonOutput) {
50
+ console.log(JSON.stringify(payload));
51
+ }
52
+ else {
53
+ error(fallbackMessage, fallbackSuggestion);
54
+ }
55
+ process.exit(typeof payload.exitCode === 'number' ? payload.exitCode : 1);
56
+ };
47
57
  const config = loadConfig({
48
58
  configPath: globalOpts.config,
49
59
  apiUrl: globalOpts.apiUrl,
50
60
  projectId: globalOpts.projectId,
51
61
  });
52
62
  if (!hasAuth(config)) {
53
- if (options.streamJson || jsonOutput) {
54
- console.log(JSON.stringify({
55
- type: 'error',
56
- code: 'AUTH_MISSING',
57
- errorType: 'AUTH_MISSING',
58
- message: 'Not authenticated',
59
- category: 'auth',
60
- retryable: false,
61
- transient: false,
62
- suggestion: 'Run: baz auth login <api-key>',
63
- continue: false,
64
- }));
65
- }
66
- else {
67
- error('Not authenticated', 'Run: baz auth login <api-key>');
68
- }
69
- process.exit(13); // Auth error exit code
63
+ emitCliError({
64
+ type: 'error',
65
+ code: 'AUTH_MISSING',
66
+ errorType: 'AUTH_MISSING',
67
+ message: 'Not authenticated',
68
+ category: 'auth',
69
+ retryable: false,
70
+ transient: false,
71
+ suggestion: 'Run: baz auth login <api-key>',
72
+ continue: false,
73
+ exitCode: 13,
74
+ }, 'Not authenticated', 'Run: baz auth login <api-key>');
70
75
  }
71
76
  // Get project ID
72
- let projectId;
77
+ let projectId = '';
73
78
  try {
74
79
  projectId = getProjectId(config, globalOpts.projectId);
75
80
  }
76
81
  catch (err) {
77
- if (options.streamJson || jsonOutput) {
78
- console.log(JSON.stringify({
79
- type: 'error',
80
- code: 'VALIDATION',
81
- errorType: 'VALIDATION',
82
- message: err.message,
83
- category: 'validation',
84
- retryable: false,
85
- transient: false,
86
- suggestion: 'Set active project with: baz project use <id>',
87
- continue: false,
88
- }));
89
- }
90
- else {
91
- error(err.message);
92
- }
93
- process.exit(64); // Input error exit code
82
+ emitCliError({
83
+ type: 'error',
84
+ code: 'VALIDATION',
85
+ errorType: 'VALIDATION',
86
+ message: err.message,
87
+ category: 'validation',
88
+ retryable: false,
89
+ transient: false,
90
+ suggestion: 'Set active project with: baz project use <id>',
91
+ continue: false,
92
+ exitCode: 64,
93
+ }, err.message);
94
94
  }
95
95
  // Get prompt text: --file takes precedence, then positional arg
96
96
  let promptText = text || '';
@@ -99,46 +99,83 @@ export const promptCommand = new Command('prompt')
99
99
  promptText = readFileSync(options.file, 'utf-8').trim();
100
100
  }
101
101
  catch (err) {
102
- error(`Failed to read file: ${options.file}`);
103
- process.exit(1);
102
+ emitCliError({
103
+ type: 'error',
104
+ code: 'VALIDATION',
105
+ errorType: 'VALIDATION',
106
+ message: `Failed to read file: ${options.file}`,
107
+ category: 'validation',
108
+ retryable: false,
109
+ transient: false,
110
+ continue: false,
111
+ exitCode: 64,
112
+ }, `Failed to read file: ${options.file}`);
104
113
  }
105
114
  }
106
115
  if (!promptText) {
107
- error('No prompt provided. Pass text as argument or use --file <path>');
108
- process.exit(1);
116
+ emitCliError({
117
+ type: 'error',
118
+ code: 'VALIDATION',
119
+ errorType: 'VALIDATION',
120
+ message: 'No prompt provided. Pass text as argument or use --file <path>',
121
+ category: 'validation',
122
+ retryable: false,
123
+ transient: false,
124
+ continue: false,
125
+ exitCode: 64,
126
+ }, 'No prompt provided. Pass text as argument or use --file <path>');
109
127
  }
110
128
  // Determine mode
111
129
  const mode = options.max ? 'agent-max' : options.mode;
112
130
  // Handle image uploads
113
131
  const imageUrls = [];
114
132
  if (options.image && options.image.length > 0) {
115
- const uploadSpinner = ora('Uploading images...').start();
133
+ const uploadSpinner = (streamJson || jsonOutput) ? null : ora('Uploading images...').start();
116
134
  try {
117
135
  for (const imagePath of options.image) {
118
136
  if (!existsSync(imagePath)) {
119
- uploadSpinner.fail();
120
- error(`Image file not found: ${imagePath}`);
121
- process.exit(1);
137
+ uploadSpinner?.fail();
138
+ emitCliError({
139
+ type: 'error',
140
+ code: 'NOT_FOUND',
141
+ errorType: 'NOT_FOUND',
142
+ message: `Image file not found: ${imagePath}`,
143
+ category: 'validation',
144
+ retryable: false,
145
+ transient: false,
146
+ continue: false,
147
+ exitCode: 64,
148
+ }, `Image file not found: ${imagePath}`);
122
149
  }
123
150
  const imageBuffer = readFileSync(imagePath);
124
151
  const url = await uploadImage(config, { projectId, buffer: imageBuffer, filename: imagePath });
125
152
  imageUrls.push(url);
126
153
  }
127
- uploadSpinner.succeed(`Uploaded ${imageUrls.length} image${imageUrls.length > 1 ? 's' : ''}`);
154
+ uploadSpinner?.succeed(`Uploaded ${imageUrls.length} image${imageUrls.length > 1 ? 's' : ''}`);
128
155
  }
129
156
  catch (err) {
130
- uploadSpinner.fail();
157
+ uploadSpinner?.fail();
131
158
  const errMsg = err instanceof ApiError
132
159
  ? `${err.message}${err.suggestion ? ` — ${err.suggestion}` : ''}`
133
160
  : err.message;
134
- error(`Failed to upload images: ${errMsg}`);
135
- process.exit(1);
161
+ emitCliError({
162
+ type: 'error',
163
+ code: err instanceof ApiError ? err.code : 'UNKNOWN',
164
+ errorType: err instanceof ApiError ? err.code : 'UNKNOWN',
165
+ message: `Failed to upload images: ${errMsg}`,
166
+ category: err instanceof ApiError ? err.category : 'fatal',
167
+ retryable: err instanceof ApiError ? err.retryable : false,
168
+ transient: err instanceof ApiError ? err.transient : false,
169
+ suggestion: err instanceof ApiError ? err.suggestion : undefined,
170
+ continue: false,
171
+ exitCode: err instanceof ApiError ? err.exitCode : 1,
172
+ }, `Failed to upload images: ${errMsg}`);
136
173
  }
137
174
  }
138
175
  // Handle URL attachments
139
176
  const urlContents = [];
140
177
  if (options.url && options.url.length > 0) {
141
- const urlSpinner = ora('Fetching URLs...').start();
178
+ const urlSpinner = (streamJson || jsonOutput) ? null : ora('Fetching URLs...').start();
142
179
  try {
143
180
  for (const url of options.url) {
144
181
  const result = await fetchUrlContent(url);
@@ -148,15 +185,25 @@ export const promptCommand = new Command('prompt')
148
185
  content: result.content.slice(0, 10000), // Limit content length
149
186
  });
150
187
  }
151
- urlSpinner.succeed(`Fetched ${urlContents.length} URL${urlContents.length > 1 ? 's' : ''}`);
188
+ urlSpinner?.succeed(`Fetched ${urlContents.length} URL${urlContents.length > 1 ? 's' : ''}`);
152
189
  }
153
190
  catch (err) {
154
- urlSpinner.fail();
191
+ urlSpinner?.fail();
155
192
  const errMsg = err instanceof ApiError
156
193
  ? `${err.message}${err.details ? ` — ${err.details}` : ''}`
157
194
  : err.message;
158
- error(`Failed to fetch URL: ${errMsg}`);
159
- process.exit(1);
195
+ emitCliError({
196
+ type: 'error',
197
+ code: err instanceof ApiError ? err.code : 'UNKNOWN',
198
+ errorType: err instanceof ApiError ? err.code : 'UNKNOWN',
199
+ message: `Failed to fetch URL: ${errMsg}`,
200
+ category: err instanceof ApiError ? err.category : 'fatal',
201
+ retryable: err instanceof ApiError ? err.retryable : false,
202
+ transient: err instanceof ApiError ? err.transient : false,
203
+ suggestion: err instanceof ApiError ? err.suggestion : undefined,
204
+ continue: false,
205
+ exitCode: err instanceof ApiError ? err.exitCode : 1,
206
+ }, `Failed to fetch URL: ${errMsg}`);
160
207
  }
161
208
  // Prepend URL content to the prompt
162
209
  if (urlContents.length > 0) {
@@ -164,8 +211,6 @@ export const promptCommand = new Command('prompt')
164
211
  promptText = `${urlContext}\n\n---\n\nUser request: ${promptText}`;
165
212
  }
166
213
  }
167
- // For bots: --stream-json outputs NDJSON (newline-delimited JSON) with timestamps
168
- const streamJson = options.streamJson === true || agentModeEnv;
169
214
  // Show mode info (skip for JSON modes)
170
215
  if (!jsonOutput && !streamJson && options.stream !== false) {
171
216
  console.log(chalk.cyan(`Mode: ${mode}`));
@@ -110,7 +110,7 @@ scenesCommand
110
110
  hasCode: Boolean(s.tsxCode),
111
111
  hasCompilationError: Boolean(s.compilationError),
112
112
  }));
113
- output(lean, { json: true });
113
+ output(lean, { json: true, compact: globalOpts.compact });
114
114
  return;
115
115
  }
116
116
  if (!scenes || scenes.length === 0) {
@@ -246,7 +246,7 @@ scenesCommand
246
246
  const code = scene.tsxCode || '// No code available';
247
247
  const name = scene.name || 'Untitled';
248
248
  if (globalOpts.json) {
249
- output({ sceneId, name, code }, { json: true });
249
+ output({ sceneId, name, code }, { json: true, compact: globalOpts.compact });
250
250
  return;
251
251
  }
252
252
  if (options.output) {
@@ -310,7 +310,7 @@ scenesCommand
310
310
  const confirmed = await confirmAction(`Delete scene ${sceneId}? This cannot be undone.`);
311
311
  if (!confirmed) {
312
312
  if (globalOpts.json) {
313
- output({ deleted: false, sceneId, reason: 'cancelled' }, { json: true });
313
+ output({ deleted: false, sceneId, reason: 'cancelled' }, { json: true, compact: globalOpts.compact });
314
314
  return;
315
315
  }
316
316
  console.log(chalk.gray('Cancelled. Use --force to skip confirmation.'));
@@ -325,7 +325,7 @@ scenesCommand
325
325
  });
326
326
  spinner?.stop();
327
327
  if (globalOpts.json) {
328
- output({ deleted: true, sceneId, projectId }, { json: true });
328
+ output({ deleted: true, sceneId, projectId }, { json: true, compact: globalOpts.compact });
329
329
  return;
330
330
  }
331
331
  success(`Deleted scene: ${sceneId}`);
@@ -2,8 +2,8 @@ import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import ora from 'ora';
4
4
  import { loadConfig, hasAuth, getProjectId } from '../lib/config.js';
5
- import { apiRequest } from '../lib/api.js';
6
- import { error, formatDuration } from '../lib/output.js';
5
+ import { apiRequest, ApiError } from '../lib/api.js';
6
+ import { error, formatDuration, output } from '../lib/output.js';
7
7
  /**
8
8
  * Render an ASCII timeline of scenes
9
9
  */
@@ -79,7 +79,7 @@ export const statusCommand = new Command('status')
79
79
  });
80
80
  if (!hasAuth(config)) {
81
81
  if (globalOpts.json) {
82
- console.log(JSON.stringify({
82
+ output({
83
83
  type: 'error',
84
84
  code: 'AUTH_MISSING',
85
85
  message: 'Not authenticated',
@@ -88,7 +88,7 @@ export const statusCommand = new Command('status')
88
88
  transient: false,
89
89
  exitCode: 13,
90
90
  suggestion: 'Run: baz auth login <api-key>',
91
- }));
91
+ }, { json: true, compact: globalOpts.compact });
92
92
  }
93
93
  else {
94
94
  error('Not authenticated', 'Run: baz auth login <api-key>');
@@ -101,7 +101,7 @@ export const statusCommand = new Command('status')
101
101
  }
102
102
  catch (err) {
103
103
  if (globalOpts.json) {
104
- console.log(JSON.stringify({
104
+ output({
105
105
  type: 'error',
106
106
  code: 'VALIDATION',
107
107
  message: err.message,
@@ -109,14 +109,14 @@ export const statusCommand = new Command('status')
109
109
  retryable: false,
110
110
  transient: false,
111
111
  exitCode: 64,
112
- }));
112
+ }, { json: true, compact: globalOpts.compact });
113
113
  }
114
114
  else {
115
115
  error(err.message);
116
116
  }
117
117
  process.exit(64);
118
118
  }
119
- const spinner = ora('Fetching project status...').start();
119
+ const spinner = globalOpts.json ? null : ora('Fetching project status...').start();
120
120
  try {
121
121
  // Use full project query as canonical source of scene state.
122
122
  const projectResult = await apiRequest(config, 'project.getFullProject', {
@@ -124,7 +124,7 @@ export const statusCommand = new Command('status')
124
124
  include: ['scenes'],
125
125
  includeSceneCode: false,
126
126
  });
127
- spinner.stop();
127
+ spinner?.stop();
128
128
  const project = projectResult.project || projectResult;
129
129
  const scenes = projectResult.scenes || [];
130
130
  const fps = 30;
@@ -149,7 +149,7 @@ export const statusCommand = new Command('status')
149
149
  const trackCount = trackSet.size || 1;
150
150
  // JSON output
151
151
  if (globalOpts.json) {
152
- console.log(JSON.stringify({
152
+ output({
153
153
  projectId,
154
154
  title: project.title || 'Untitled',
155
155
  scenes: scenes.length,
@@ -164,7 +164,7 @@ export const statusCommand = new Command('status')
164
164
  durationInFrames: getDurationFrames(s),
165
165
  hasError: hasError(s),
166
166
  })),
167
- }, null, 2));
167
+ }, { json: true, compact: globalOpts.compact });
168
168
  return;
169
169
  }
170
170
  // Pretty output
@@ -212,8 +212,30 @@ export const statusCommand = new Command('status')
212
212
  console.log();
213
213
  }
214
214
  catch (err) {
215
- spinner.stop();
216
- error(err.message);
215
+ spinner?.stop();
216
+ if (err instanceof ApiError) {
217
+ if (globalOpts.json) {
218
+ output(err.toJSON(), { json: true, compact: globalOpts.compact });
219
+ }
220
+ else {
221
+ error(err.message, err.suggestion);
222
+ }
223
+ process.exit(err.exitCode);
224
+ }
225
+ if (globalOpts.json) {
226
+ output({
227
+ type: 'error',
228
+ code: 'UNKNOWN',
229
+ message: err.message,
230
+ category: 'fatal',
231
+ retryable: false,
232
+ transient: false,
233
+ exitCode: 1,
234
+ }, { json: true, compact: globalOpts.compact });
235
+ }
236
+ else {
237
+ error(err.message);
238
+ }
217
239
  process.exit(1);
218
240
  }
219
241
  });
@@ -7,6 +7,7 @@ export interface Config {
7
7
  outputFormat?: 'pretty' | 'json';
8
8
  };
9
9
  }
10
+ export declare const PRODUCTION_API_URL = "https://bazaar.it";
10
11
  /**
11
12
  * Load config from file, environment, and CLI options
12
13
  */
@@ -18,7 +19,9 @@ export declare function loadConfig(options?: {
18
19
  /**
19
20
  * Save config to file
20
21
  */
21
- export declare function saveConfig(updates: Partial<Config>): void;
22
+ export declare function saveConfig(updates: Partial<Config>, options?: {
23
+ configPath?: string;
24
+ }): void;
22
25
  /**
23
26
  * Get active project ID or throw if not set
24
27
  */
@@ -30,4 +33,4 @@ export declare function hasAuth(config: Config): boolean;
30
33
  /**
31
34
  * Get config file path for display
32
35
  */
33
- export declare function getConfigPath(): string;
36
+ export declare function getConfigPath(configPath?: string): string;
@@ -1,8 +1,12 @@
1
1
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
2
  import { homedir } from 'os';
3
- import { join } from 'path';
3
+ import { dirname, join } from 'path';
4
+ export const PRODUCTION_API_URL = 'https://bazaar.it';
5
+ const KNOWN_STAGING_API_URLS = new Set([
6
+ 'https://coco.bazaar.it',
7
+ ]);
4
8
  const DEFAULT_CONFIG = {
5
- apiUrl: 'https://bazaar.it',
9
+ apiUrl: PRODUCTION_API_URL,
6
10
  defaults: {
7
11
  mode: 'agent',
8
12
  outputFormat: 'pretty',
@@ -10,6 +14,15 @@ const DEFAULT_CONFIG = {
10
14
  };
11
15
  const CONFIG_DIR = join(homedir(), '.bazaar');
12
16
  const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
17
+ function normalizeApiUrl(apiUrl, options) {
18
+ const normalized = (apiUrl || PRODUCTION_API_URL).trim().replace(/\/+$/, '');
19
+ // Guard against accidentally shipping/sticking users on staging.
20
+ // Staging is still allowed via explicit override: --api-url, BAZ_API_URL, or BAZ_ALLOW_STAGING=1.
21
+ if (!options?.allowKnownStaging && KNOWN_STAGING_API_URLS.has(normalized)) {
22
+ return PRODUCTION_API_URL;
23
+ }
24
+ return normalized;
25
+ }
13
26
  /**
14
27
  * Load config from file, environment, and CLI options
15
28
  */
@@ -48,21 +61,27 @@ export function loadConfig(options) {
48
61
  if (options?.projectId) {
49
62
  config.activeProjectId = options.projectId;
50
63
  }
64
+ const allowKnownStaging = process.env.BAZ_ALLOW_STAGING === '1' ||
65
+ Boolean(process.env.BAZ_API_URL) ||
66
+ Boolean(options?.apiUrl);
67
+ config.apiUrl = normalizeApiUrl(config.apiUrl, { allowKnownStaging });
51
68
  return config;
52
69
  }
53
70
  /**
54
71
  * Save config to file
55
72
  */
56
- export function saveConfig(updates) {
73
+ export function saveConfig(updates, options) {
74
+ const configPath = options?.configPath || CONFIG_FILE;
75
+ const configDir = dirname(configPath);
57
76
  // Ensure directory exists
58
- if (!existsSync(CONFIG_DIR)) {
59
- mkdirSync(CONFIG_DIR, { recursive: true });
77
+ if (!existsSync(configDir)) {
78
+ mkdirSync(configDir, { recursive: true });
60
79
  }
61
80
  // Load existing config
62
81
  let config = { ...DEFAULT_CONFIG };
63
- if (existsSync(CONFIG_FILE)) {
82
+ if (existsSync(configPath)) {
64
83
  try {
65
- const fileContent = readFileSync(CONFIG_FILE, 'utf-8');
84
+ const fileContent = readFileSync(configPath, 'utf-8');
66
85
  config = { ...config, ...JSON.parse(fileContent) };
67
86
  }
68
87
  catch {
@@ -71,8 +90,11 @@ export function saveConfig(updates) {
71
90
  }
72
91
  // Apply updates
73
92
  config = { ...config, ...updates };
93
+ const allowKnownStaging = process.env.BAZ_ALLOW_STAGING === '1' ||
94
+ Object.prototype.hasOwnProperty.call(updates, 'apiUrl');
95
+ config.apiUrl = normalizeApiUrl(config.apiUrl, { allowKnownStaging });
74
96
  // Write back
75
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
97
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
76
98
  }
77
99
  /**
78
100
  * Get active project ID or throw if not set
@@ -94,6 +116,6 @@ export function hasAuth(config) {
94
116
  /**
95
117
  * Get config file path for display
96
118
  */
97
- export function getConfigPath() {
98
- return CONFIG_FILE;
119
+ export function getConfigPath(configPath) {
120
+ return configPath || CONFIG_FILE;
99
121
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazaar.it",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "AI-powered motion graphics from the command line — bazaar.it",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,7 +17,14 @@
17
17
  "type": "git",
18
18
  "url": "https://github.com/bazaar-it/bazaar"
19
19
  },
20
- "keywords": ["video", "ai", "motion-graphics", "remotion", "cli", "bazaar"],
20
+ "keywords": [
21
+ "video",
22
+ "ai",
23
+ "motion-graphics",
24
+ "remotion",
25
+ "cli",
26
+ "bazaar"
27
+ ],
21
28
  "author": "Markus Hogne <markus@bazaar.it>",
22
29
  "license": "MIT",
23
30
  "scripts": {