@vibeframe/cli 0.31.1 → 0.33.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 (110) hide show
  1. package/dist/commands/agent.d.ts.map +1 -1
  2. package/dist/commands/agent.js +8 -15
  3. package/dist/commands/agent.js.map +1 -1
  4. package/dist/commands/ai-analyze.d.ts.map +1 -1
  5. package/dist/commands/ai-analyze.js +9 -16
  6. package/dist/commands/ai-analyze.js.map +1 -1
  7. package/dist/commands/ai-audio.d.ts.map +1 -1
  8. package/dist/commands/ai-audio.js +129 -111
  9. package/dist/commands/ai-audio.js.map +1 -1
  10. package/dist/commands/ai-broll.d.ts.map +1 -1
  11. package/dist/commands/ai-broll.js +38 -23
  12. package/dist/commands/ai-broll.js.map +1 -1
  13. package/dist/commands/ai-edit-cli.d.ts.map +1 -1
  14. package/dist/commands/ai-edit-cli.js +53 -65
  15. package/dist/commands/ai-edit-cli.js.map +1 -1
  16. package/dist/commands/ai-fill-gaps.d.ts.map +1 -1
  17. package/dist/commands/ai-fill-gaps.js +13 -17
  18. package/dist/commands/ai-fill-gaps.js.map +1 -1
  19. package/dist/commands/ai-highlights.d.ts.map +1 -1
  20. package/dist/commands/ai-highlights.js +87 -60
  21. package/dist/commands/ai-highlights.js.map +1 -1
  22. package/dist/commands/ai-image.d.ts.map +1 -1
  23. package/dist/commands/ai-image.js +75 -60
  24. package/dist/commands/ai-image.js.map +1 -1
  25. package/dist/commands/ai-motion.d.ts.map +1 -1
  26. package/dist/commands/ai-motion.js +30 -5
  27. package/dist/commands/ai-motion.js.map +1 -1
  28. package/dist/commands/ai-narrate.d.ts.map +1 -1
  29. package/dist/commands/ai-narrate.js +19 -16
  30. package/dist/commands/ai-narrate.js.map +1 -1
  31. package/dist/commands/ai-review.d.ts.map +1 -1
  32. package/dist/commands/ai-review.js +24 -5
  33. package/dist/commands/ai-review.js.map +1 -1
  34. package/dist/commands/ai-script-pipeline-cli.d.ts.map +1 -1
  35. package/dist/commands/ai-script-pipeline-cli.js +114 -88
  36. package/dist/commands/ai-script-pipeline-cli.js.map +1 -1
  37. package/dist/commands/ai-script-pipeline.d.ts +12 -2
  38. package/dist/commands/ai-script-pipeline.d.ts.map +1 -1
  39. package/dist/commands/ai-script-pipeline.js +113 -27
  40. package/dist/commands/ai-script-pipeline.js.map +1 -1
  41. package/dist/commands/ai-suggest-edit.d.ts.map +1 -1
  42. package/dist/commands/ai-suggest-edit.js +16 -21
  43. package/dist/commands/ai-suggest-edit.js.map +1 -1
  44. package/dist/commands/ai-video-fx.d.ts.map +1 -1
  45. package/dist/commands/ai-video-fx.js +72 -71
  46. package/dist/commands/ai-video-fx.js.map +1 -1
  47. package/dist/commands/ai-video.d.ts.map +1 -1
  48. package/dist/commands/ai-video.js +99 -90
  49. package/dist/commands/ai-video.js.map +1 -1
  50. package/dist/commands/ai-viral.d.ts.map +1 -1
  51. package/dist/commands/ai-viral.js +12 -24
  52. package/dist/commands/ai-viral.js.map +1 -1
  53. package/dist/commands/ai-visual-fx.d.ts.map +1 -1
  54. package/dist/commands/ai-visual-fx.js +76 -60
  55. package/dist/commands/ai-visual-fx.js.map +1 -1
  56. package/dist/commands/analyze.js +4 -4
  57. package/dist/commands/analyze.js.map +1 -1
  58. package/dist/commands/audio.js +44 -44
  59. package/dist/commands/audio.js.map +1 -1
  60. package/dist/commands/batch.d.ts.map +1 -1
  61. package/dist/commands/batch.js +92 -39
  62. package/dist/commands/batch.js.map +1 -1
  63. package/dist/commands/detect.d.ts.map +1 -1
  64. package/dist/commands/detect.js +62 -11
  65. package/dist/commands/detect.js.map +1 -1
  66. package/dist/commands/edit-cmd.js +60 -64
  67. package/dist/commands/edit-cmd.js.map +1 -1
  68. package/dist/commands/export.d.ts.map +1 -1
  69. package/dist/commands/export.js +169 -97
  70. package/dist/commands/export.js.map +1 -1
  71. package/dist/commands/generate.js +125 -128
  72. package/dist/commands/generate.js.map +1 -1
  73. package/dist/commands/media.d.ts.map +1 -1
  74. package/dist/commands/media.js +7 -9
  75. package/dist/commands/media.js.map +1 -1
  76. package/dist/commands/output.js +2 -2
  77. package/dist/commands/output.js.map +1 -1
  78. package/dist/commands/pipeline.d.ts.map +1 -1
  79. package/dist/commands/pipeline.js +21 -27
  80. package/dist/commands/pipeline.js.map +1 -1
  81. package/dist/commands/project.d.ts.map +1 -1
  82. package/dist/commands/project.js +42 -9
  83. package/dist/commands/project.js.map +1 -1
  84. package/dist/commands/schema.d.ts.map +1 -1
  85. package/dist/commands/schema.js +10 -16
  86. package/dist/commands/schema.js.map +1 -1
  87. package/dist/commands/setup.d.ts.map +1 -1
  88. package/dist/commands/setup.js +248 -234
  89. package/dist/commands/setup.js.map +1 -1
  90. package/dist/commands/timeline.d.ts.map +1 -1
  91. package/dist/commands/timeline.js +185 -59
  92. package/dist/commands/timeline.js.map +1 -1
  93. package/dist/commands/validate.d.ts +3 -1
  94. package/dist/commands/validate.d.ts.map +1 -1
  95. package/dist/commands/validate.js +9 -2
  96. package/dist/commands/validate.js.map +1 -1
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +95 -17
  99. package/dist/index.js.map +1 -1
  100. package/dist/utils/api-key.d.ts.map +1 -1
  101. package/dist/utils/api-key.js +4 -2
  102. package/dist/utils/api-key.js.map +1 -1
  103. package/dist/utils/first-run.d.ts.map +1 -1
  104. package/dist/utils/first-run.js +5 -6
  105. package/dist/utils/first-run.js.map +1 -1
  106. package/dist/utils/tty.d.ts +1 -1
  107. package/dist/utils/tty.d.ts.map +1 -1
  108. package/dist/utils/tty.js +62 -3
  109. package/dist/utils/tty.js.map +1 -1
  110. package/package.json +3 -3
@@ -21,6 +21,8 @@ import { getApiKey } from '../utils/api-key.js';
21
21
  import { execSafe, execSafeSync, commandExists } from '../utils/exec-safe.js';
22
22
  import { detectFormat, formatTranscript } from '../utils/subtitle.js';
23
23
  import { formatTime } from './ai-helpers.js';
24
+ import { exitWithError, authError, notFoundError, apiError, usageError, generalError, outputResult } from './output.js';
25
+ import { validateOutputPath } from "./validate.js";
24
26
  function _registerAudioCommands(aiCommand) {
25
27
  aiCommand
26
28
  .command("transcribe")
@@ -30,12 +32,19 @@ function _registerAudioCommands(aiCommand) {
30
32
  .option("-l, --language <lang>", "Language code (e.g., en, ko)")
31
33
  .option("-o, --output <path>", "Output file path")
32
34
  .option("-f, --format <format>", "Output format: json, srt, vtt (auto-detected from extension)")
35
+ .option("--dry-run", "Preview parameters without executing")
33
36
  .action(async (audioPath, options) => {
34
37
  try {
38
+ if (options.output) {
39
+ validateOutputPath(options.output);
40
+ }
41
+ if (options.dryRun) {
42
+ outputResult({ dryRun: true, command: "ai transcribe", params: { audio: audioPath, output: options.output, language: options.language, format: options.format } });
43
+ return;
44
+ }
35
45
  const apiKey = await getApiKey("OPENAI_API_KEY", "OpenAI", options.apiKey);
36
46
  if (!apiKey) {
37
- console.error(chalk.red("OpenAI API key required. Set OPENAI_API_KEY in .env or run: vibe setup"));
38
- process.exit(1);
47
+ exitWithError(authError("OPENAI_API_KEY", "OpenAI"));
39
48
  }
40
49
  const spinner = ora("Initializing Whisper...").start();
41
50
  const whisper = new WhisperProvider();
@@ -47,8 +56,8 @@ function _registerAudioCommands(aiCommand) {
47
56
  spinner.text = "Transcribing...";
48
57
  const result = await whisper.transcribe(audioBlob, options.language);
49
58
  if (result.status === "failed") {
50
- spinner.fail(chalk.red(`Transcription failed: ${result.error}`));
51
- process.exit(1);
59
+ spinner.fail("Transcription failed");
60
+ exitWithError(apiError(`Transcription failed: ${result.error}`, true));
52
61
  }
53
62
  spinner.succeed(chalk.green("Transcription complete"));
54
63
  console.log();
@@ -74,9 +83,7 @@ function _registerAudioCommands(aiCommand) {
74
83
  }
75
84
  }
76
85
  catch (error) {
77
- console.error(chalk.red("Transcription failed"));
78
- console.error(error);
79
- process.exit(1);
86
+ exitWithError(apiError(`Transcription failed: ${error instanceof Error ? error.message : String(error)}`, true));
80
87
  }
81
88
  });
82
89
  aiCommand
@@ -87,12 +94,19 @@ function _registerAudioCommands(aiCommand) {
87
94
  .option("-o, --output <path>", "Output audio file path", "output.mp3")
88
95
  .option("-v, --voice <id>", "Voice ID (default: Rachel)", "21m00Tcm4TlvDq8ikWAM")
89
96
  .option("--list-voices", "List available voices")
97
+ .option("--dry-run", "Preview parameters without executing")
90
98
  .action(async (text, options) => {
91
99
  try {
100
+ if (options.output) {
101
+ validateOutputPath(options.output);
102
+ }
103
+ if (options.dryRun) {
104
+ outputResult({ dryRun: true, command: "ai tts", params: { text, output: options.output, voice: options.voice } });
105
+ return;
106
+ }
92
107
  const apiKey = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
93
108
  if (!apiKey) {
94
- console.error(chalk.red("ElevenLabs API key required. Set ELEVENLABS_API_KEY in .env or run: vibe setup"));
95
- process.exit(1);
109
+ exitWithError(authError("ELEVENLABS_API_KEY", "ElevenLabs"));
96
110
  }
97
111
  const elevenlabs = new ElevenLabsProvider();
98
112
  await elevenlabs.initialize({ apiKey });
@@ -123,8 +137,8 @@ function _registerAudioCommands(aiCommand) {
123
137
  voiceId: options.voice,
124
138
  });
125
139
  if (!result.success || !result.audioBuffer) {
126
- spinner.fail(chalk.red(result.error || "TTS generation failed"));
127
- process.exit(1);
140
+ spinner.fail("TTS generation failed");
141
+ exitWithError(apiError(result.error || "TTS generation failed", true));
128
142
  }
129
143
  const outputPath = resolve(process.cwd(), options.output);
130
144
  await writeFile(outputPath, result.audioBuffer);
@@ -135,9 +149,7 @@ function _registerAudioCommands(aiCommand) {
135
149
  console.log();
136
150
  }
137
151
  catch (error) {
138
- console.error(chalk.red("TTS generation failed"));
139
- console.error(error);
140
- process.exit(1);
152
+ exitWithError(apiError(`TTS generation failed: ${error instanceof Error ? error.message : String(error)}`, true));
141
153
  }
142
154
  });
143
155
  aiCommand
@@ -148,8 +160,7 @@ function _registerAudioCommands(aiCommand) {
148
160
  try {
149
161
  const apiKey = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
150
162
  if (!apiKey) {
151
- console.error(chalk.red("ElevenLabs API key required. Set ELEVENLABS_API_KEY in .env or run: vibe setup"));
152
- process.exit(1);
163
+ exitWithError(authError("ELEVENLABS_API_KEY", "ElevenLabs"));
153
164
  }
154
165
  const spinner = ora("Fetching voices...").start();
155
166
  const elevenlabs = new ElevenLabsProvider();
@@ -167,9 +178,7 @@ function _registerAudioCommands(aiCommand) {
167
178
  console.log();
168
179
  }
169
180
  catch (error) {
170
- console.error(chalk.red("Failed to fetch voices"));
171
- console.error(error);
172
- process.exit(1);
181
+ exitWithError(apiError(`Failed to fetch voices: ${error instanceof Error ? error.message : String(error)}`, true));
173
182
  }
174
183
  });
175
184
  aiCommand
@@ -180,12 +189,19 @@ function _registerAudioCommands(aiCommand) {
180
189
  .option("-o, --output <path>", "Output audio file path", "sound-effect.mp3")
181
190
  .option("-d, --duration <seconds>", "Duration in seconds (0.5-22, default: auto)")
182
191
  .option("--prompt-influence <value>", "Prompt influence (0-1, default: 0.3)")
192
+ .option("--dry-run", "Preview parameters without executing")
183
193
  .action(async (prompt, options) => {
184
194
  try {
195
+ if (options.output) {
196
+ validateOutputPath(options.output);
197
+ }
198
+ if (options.dryRun) {
199
+ outputResult({ dryRun: true, command: "ai sfx", params: { prompt, output: options.output, duration: options.duration, promptInfluence: options.promptInfluence } });
200
+ return;
201
+ }
185
202
  const apiKey = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
186
203
  if (!apiKey) {
187
- console.error(chalk.red("ElevenLabs API key required. Set ELEVENLABS_API_KEY in .env or run: vibe setup"));
188
- process.exit(1);
204
+ exitWithError(authError("ELEVENLABS_API_KEY", "ElevenLabs"));
189
205
  }
190
206
  const spinner = ora("Generating sound effect...").start();
191
207
  const elevenlabs = new ElevenLabsProvider();
@@ -195,8 +211,8 @@ function _registerAudioCommands(aiCommand) {
195
211
  promptInfluence: options.promptInfluence ? parseFloat(options.promptInfluence) : undefined,
196
212
  });
197
213
  if (!result.success || !result.audioBuffer) {
198
- spinner.fail(chalk.red(result.error || "Sound effect generation failed"));
199
- process.exit(1);
214
+ spinner.fail("Sound effect generation failed");
215
+ exitWithError(apiError(result.error || "Sound effect generation failed", true));
200
216
  }
201
217
  const outputPath = resolve(process.cwd(), options.output);
202
218
  await writeFile(outputPath, result.audioBuffer);
@@ -205,9 +221,7 @@ function _registerAudioCommands(aiCommand) {
205
221
  console.log();
206
222
  }
207
223
  catch (error) {
208
- console.error(chalk.red("Sound effect generation failed"));
209
- console.error(error);
210
- process.exit(1);
224
+ exitWithError(apiError(`Sound effect generation failed: ${error instanceof Error ? error.message : String(error)}`, true));
211
225
  }
212
226
  });
213
227
  aiCommand
@@ -216,12 +230,19 @@ function _registerAudioCommands(aiCommand) {
216
230
  .argument("<audio>", "Input audio file path")
217
231
  .option("-k, --api-key <key>", "ElevenLabs API key (or set ELEVENLABS_API_KEY env)")
218
232
  .option("-o, --output <path>", "Output audio file path", "vocals.mp3")
233
+ .option("--dry-run", "Preview parameters without executing")
219
234
  .action(async (audioPath, options) => {
220
235
  try {
236
+ if (options.output) {
237
+ validateOutputPath(options.output);
238
+ }
239
+ if (options.dryRun) {
240
+ outputResult({ dryRun: true, command: "ai isolate", params: { audio: audioPath, output: options.output } });
241
+ return;
242
+ }
221
243
  const apiKey = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
222
244
  if (!apiKey) {
223
- console.error(chalk.red("ElevenLabs API key required. Set ELEVENLABS_API_KEY in .env or run: vibe setup"));
224
- process.exit(1);
245
+ exitWithError(authError("ELEVENLABS_API_KEY", "ElevenLabs"));
225
246
  }
226
247
  const spinner = ora("Reading audio file...").start();
227
248
  const absPath = resolve(process.cwd(), audioPath);
@@ -231,8 +252,8 @@ function _registerAudioCommands(aiCommand) {
231
252
  await elevenlabs.initialize({ apiKey });
232
253
  const result = await elevenlabs.isolateVocals(audioBuffer);
233
254
  if (!result.success || !result.audioBuffer) {
234
- spinner.fail(chalk.red(result.error || "Audio isolation failed"));
235
- process.exit(1);
255
+ spinner.fail("Audio isolation failed");
256
+ exitWithError(apiError(result.error || "Audio isolation failed", true));
236
257
  }
237
258
  const outputPath = resolve(process.cwd(), options.output);
238
259
  await writeFile(outputPath, result.audioBuffer);
@@ -241,9 +262,7 @@ function _registerAudioCommands(aiCommand) {
241
262
  console.log();
242
263
  }
243
264
  catch (error) {
244
- console.error(chalk.red("Audio isolation failed"));
245
- console.error(error);
246
- process.exit(1);
265
+ exitWithError(apiError(`Audio isolation failed: ${error instanceof Error ? error.message : String(error)}`, true));
247
266
  }
248
267
  });
249
268
  aiCommand
@@ -256,12 +275,16 @@ function _registerAudioCommands(aiCommand) {
256
275
  .option("--labels <json>", "Labels as JSON (e.g., '{\"accent\": \"american\"}')")
257
276
  .option("--remove-noise", "Remove background noise from samples")
258
277
  .option("--list", "List all available voices")
278
+ .option("--dry-run", "Preview parameters without executing")
259
279
  .action(async (samples, options) => {
260
280
  try {
281
+ if (options.dryRun) {
282
+ outputResult({ dryRun: true, command: "ai voice-clone", params: { samples, name: options.name, description: options.description, removeNoise: options.removeNoise } });
283
+ return;
284
+ }
261
285
  const apiKey = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs", options.apiKey);
262
286
  if (!apiKey) {
263
- console.error(chalk.red("ElevenLabs API key required. Set ELEVENLABS_API_KEY in .env or run: vibe setup"));
264
- process.exit(1);
287
+ exitWithError(authError("ELEVENLABS_API_KEY", "ElevenLabs"));
265
288
  }
266
289
  const elevenlabs = new ElevenLabsProvider();
267
290
  await elevenlabs.initialize({ apiKey });
@@ -286,20 +309,18 @@ function _registerAudioCommands(aiCommand) {
286
309
  }
287
310
  // Clone voice mode
288
311
  if (!options.name) {
289
- console.error(chalk.red("Voice name is required. Use --name <name>"));
290
- process.exit(1);
312
+ exitWithError(usageError("Voice name is required. Use --name <name>"));
291
313
  }
292
314
  if (!samples || samples.length === 0) {
293
- console.error(chalk.red("At least one audio sample is required"));
294
- process.exit(1);
315
+ exitWithError(usageError("At least one audio sample is required"));
295
316
  }
296
317
  const spinner = ora("Reading audio samples...").start();
297
318
  const audioBuffers = [];
298
319
  for (const samplePath of samples) {
299
320
  const absPath = resolve(process.cwd(), samplePath);
300
321
  if (!existsSync(absPath)) {
301
- spinner.fail(chalk.red(`File not found: ${samplePath}`));
302
- process.exit(1);
322
+ spinner.fail("File not found");
323
+ exitWithError(notFoundError(samplePath));
303
324
  }
304
325
  const buffer = await readFile(absPath);
305
326
  audioBuffers.push(buffer);
@@ -313,8 +334,8 @@ function _registerAudioCommands(aiCommand) {
313
334
  removeBackgroundNoise: options.removeNoise,
314
335
  });
315
336
  if (!result.success) {
316
- spinner.fail(chalk.red(result.error || "Voice cloning failed"));
317
- process.exit(1);
337
+ spinner.fail("Voice cloning failed");
338
+ exitWithError(apiError(result.error || "Voice cloning failed", true));
318
339
  }
319
340
  spinner.succeed(chalk.green("Voice cloned successfully"));
320
341
  console.log();
@@ -328,9 +349,7 @@ function _registerAudioCommands(aiCommand) {
328
349
  console.log();
329
350
  }
330
351
  catch (error) {
331
- console.error(chalk.red("Voice cloning failed"));
332
- console.error(error);
333
- process.exit(1);
352
+ exitWithError(apiError(`Voice cloning failed: ${error instanceof Error ? error.message : String(error)}`, true));
334
353
  }
335
354
  });
336
355
  aiCommand
@@ -343,12 +362,19 @@ function _registerAudioCommands(aiCommand) {
343
362
  .option("--model <model>", "Model variant: large, stereo-large, melody-large, stereo-melody-large", "stereo-large")
344
363
  .option("-o, --output <path>", "Output audio file path", "music.mp3")
345
364
  .option("--no-wait", "Don't wait for generation to complete (async mode)")
365
+ .option("--dry-run", "Preview parameters without executing")
346
366
  .action(async (prompt, options) => {
347
367
  try {
368
+ if (options.output) {
369
+ validateOutputPath(options.output);
370
+ }
371
+ if (options.dryRun) {
372
+ outputResult({ dryRun: true, command: "ai music", params: { prompt, output: options.output, duration: options.duration, model: options.model, melody: options.melody } });
373
+ return;
374
+ }
348
375
  const apiKey = await getApiKey("REPLICATE_API_TOKEN", "Replicate", options.apiKey);
349
376
  if (!apiKey) {
350
- console.error(chalk.red("Replicate API token required. Set REPLICATE_API_TOKEN in .env or run: vibe setup"));
351
- process.exit(1);
377
+ exitWithError(authError("REPLICATE_API_TOKEN", "Replicate"));
352
378
  }
353
379
  const replicate = new ReplicateProvider();
354
380
  await replicate.initialize({ apiKey });
@@ -360,14 +386,12 @@ function _registerAudioCommands(aiCommand) {
360
386
  spinner.text = "Uploading melody reference...";
361
387
  const absPath = resolve(process.cwd(), options.melody);
362
388
  if (!existsSync(absPath)) {
363
- spinner.fail(chalk.red(`Melody file not found: ${options.melody}`));
364
- process.exit(1);
389
+ spinner.fail("Melody file not found");
390
+ exitWithError(notFoundError(options.melody));
365
391
  }
366
392
  // For Replicate, we need a publicly accessible URL
367
393
  // In practice, users would need to host the file or use a data URL
368
- console.log(chalk.yellow("Note: Melody conditioning requires a publicly accessible URL"));
369
- console.log(chalk.yellow("Please upload your melody file and provide the URL"));
370
- process.exit(1);
394
+ exitWithError(usageError("Melody conditioning requires a publicly accessible URL. Please upload your melody file and provide the URL."));
371
395
  }
372
396
  const result = await replicate.generateMusic(prompt, {
373
397
  duration,
@@ -375,8 +399,8 @@ function _registerAudioCommands(aiCommand) {
375
399
  melodyUrl,
376
400
  });
377
401
  if (!result.success || !result.taskId) {
378
- spinner.fail(chalk.red(result.error || "Music generation failed"));
379
- process.exit(1);
402
+ spinner.fail("Music generation failed");
403
+ exitWithError(apiError(result.error || "Music generation failed", true));
380
404
  }
381
405
  if (!options.wait) {
382
406
  spinner.succeed(chalk.green("Music generation started"));
@@ -388,14 +412,14 @@ function _registerAudioCommands(aiCommand) {
388
412
  spinner.text = "Generating music (this may take a few minutes)...";
389
413
  const finalResult = await replicate.waitForMusic(result.taskId);
390
414
  if (!finalResult.success || !finalResult.audioUrl) {
391
- spinner.fail(chalk.red(finalResult.error || "Music generation failed"));
392
- process.exit(1);
415
+ spinner.fail("Music generation failed");
416
+ exitWithError(apiError(finalResult.error || "Music generation failed", true));
393
417
  }
394
418
  spinner.text = "Downloading generated audio...";
395
419
  const response = await fetch(finalResult.audioUrl);
396
420
  if (!response.ok) {
397
- spinner.fail(chalk.red("Failed to download generated audio"));
398
- process.exit(1);
421
+ spinner.fail("Failed to download generated audio");
422
+ exitWithError(apiError("Failed to download generated audio", true));
399
423
  }
400
424
  const audioBuffer = Buffer.from(await response.arrayBuffer());
401
425
  const outputPath = resolve(process.cwd(), options.output);
@@ -408,9 +432,7 @@ function _registerAudioCommands(aiCommand) {
408
432
  console.log();
409
433
  }
410
434
  catch (error) {
411
- console.error(chalk.red("Music generation failed"));
412
- console.error(error);
413
- process.exit(1);
435
+ exitWithError(apiError(`Music generation failed: ${error instanceof Error ? error.message : String(error)}`, true));
414
436
  }
415
437
  });
416
438
  aiCommand
@@ -422,8 +444,7 @@ function _registerAudioCommands(aiCommand) {
422
444
  try {
423
445
  const apiKey = await getApiKey("REPLICATE_API_TOKEN", "Replicate", options.apiKey);
424
446
  if (!apiKey) {
425
- console.error(chalk.red("Replicate API token required. Set REPLICATE_API_TOKEN in .env or run: vibe setup"));
426
- process.exit(1);
447
+ exitWithError(authError("REPLICATE_API_TOKEN", "Replicate"));
427
448
  }
428
449
  const replicate = new ReplicateProvider();
429
450
  await replicate.initialize({ apiKey });
@@ -446,9 +467,7 @@ function _registerAudioCommands(aiCommand) {
446
467
  console.log();
447
468
  }
448
469
  catch (error) {
449
- console.error(chalk.red("Failed to get music status"));
450
- console.error(error);
451
- process.exit(1);
470
+ exitWithError(apiError(`Failed to get music status: ${error instanceof Error ? error.message : String(error)}`, true));
452
471
  }
453
472
  });
454
473
  aiCommand
@@ -462,12 +481,19 @@ function _registerAudioCommands(aiCommand) {
462
481
  .option("--no-denoise", "Disable noise reduction")
463
482
  .option("--enhance", "Enable audio enhancement")
464
483
  .option("--noise-floor <dB>", "FFmpeg noise floor threshold", "-30")
484
+ .option("--dry-run", "Preview parameters without executing")
465
485
  .action(async (audioPath, options) => {
466
486
  try {
487
+ if (options.output) {
488
+ validateOutputPath(options.output);
489
+ }
467
490
  const absPath = resolve(process.cwd(), audioPath);
468
491
  if (!existsSync(absPath)) {
469
- console.error(chalk.red(`File not found: ${audioPath}`));
470
- process.exit(1);
492
+ exitWithError(notFoundError(audioPath));
493
+ }
494
+ if (options.dryRun) {
495
+ outputResult({ dryRun: true, command: "ai audio-restore", params: { audio: audioPath, output: options.output, ffmpeg: options.ffmpeg, denoise: options.denoise, enhance: options.enhance, noiseFloor: options.noiseFloor } });
496
+ return;
471
497
  }
472
498
  // Default output path
473
499
  const ext = extname(audioPath);
@@ -500,36 +526,24 @@ function _registerAudioCommands(aiCommand) {
500
526
  console.log();
501
527
  }
502
528
  catch (error) {
503
- spinner.fail(chalk.red("FFmpeg restoration failed"));
504
- if (error instanceof Error && "message" in error) {
505
- console.error(chalk.dim(error.message));
506
- }
507
- process.exit(1);
529
+ spinner.fail("FFmpeg restoration failed");
530
+ exitWithError(generalError(`FFmpeg restoration failed: ${error instanceof Error ? error.message : String(error)}`));
508
531
  }
509
532
  return;
510
533
  }
511
534
  // Replicate AI mode
512
535
  const apiKey = await getApiKey("REPLICATE_API_TOKEN", "Replicate", options.apiKey);
513
536
  if (!apiKey) {
514
- console.error(chalk.red("Replicate API token required. Set REPLICATE_API_TOKEN in .env or run: vibe setup"));
515
- console.error(chalk.dim("Or use --ffmpeg for free FFmpeg-based restoration"));
516
- process.exit(1);
537
+ exitWithError(authError("REPLICATE_API_TOKEN", "Replicate"));
517
538
  }
518
539
  const replicate = new ReplicateProvider();
519
540
  await replicate.initialize({ apiKey });
520
541
  // For Replicate, we need a publicly accessible URL
521
542
  // This is a limitation - users need to upload their file first
522
- console.log(chalk.yellow("Note: Replicate requires a publicly accessible audio URL"));
523
- console.log(chalk.yellow("For local files, use --ffmpeg for free local processing"));
524
- console.log();
525
- console.log(chalk.dim("Example with FFmpeg:"));
526
- console.log(chalk.dim(` pnpm vibe ai audio-restore ${audioPath} --ffmpeg`));
527
- process.exit(1);
543
+ exitWithError(usageError("Replicate requires a publicly accessible audio URL. For local files, use --ffmpeg for free local processing.", `pnpm vibe ai audio-restore ${audioPath} --ffmpeg`));
528
544
  }
529
545
  catch (error) {
530
- console.error(chalk.red("Audio restoration failed"));
531
- console.error(error);
532
- process.exit(1);
546
+ exitWithError(apiError(`Audio restoration failed: ${error instanceof Error ? error.message : String(error)}`, true));
533
547
  }
534
548
  });
535
549
  aiCommand
@@ -541,33 +555,35 @@ function _registerAudioCommands(aiCommand) {
541
555
  .option("-v, --voice <id>", "ElevenLabs voice ID for output")
542
556
  .option("--analyze-only", "Only analyze and show timing, don't generate audio")
543
557
  .option("-o, --output <path>", "Output file path")
558
+ .option("--dry-run", "Preview parameters without executing")
544
559
  .action(async (mediaPath, options) => {
545
560
  try {
561
+ if (options.output) {
562
+ validateOutputPath(options.output);
563
+ }
546
564
  if (!options.language) {
547
- console.error(chalk.red("Target language is required. Use -l or --language"));
548
- process.exit(1);
565
+ exitWithError(usageError("Target language is required. Use -l or --language"));
566
+ }
567
+ if (options.dryRun) {
568
+ outputResult({ dryRun: true, command: "ai dub", params: { media: mediaPath, output: options.output, language: options.language, source: options.source, voice: options.voice, analyzeOnly: options.analyzeOnly } });
569
+ return;
549
570
  }
550
571
  const absPath = resolve(process.cwd(), mediaPath);
551
572
  if (!existsSync(absPath)) {
552
- console.error(chalk.red(`File not found: ${mediaPath}`));
553
- process.exit(1);
573
+ exitWithError(notFoundError(mediaPath));
554
574
  }
555
575
  // Check required API keys
556
576
  const openaiKey = await getApiKey("OPENAI_API_KEY", "OpenAI", undefined);
557
577
  const anthropicKey = await getApiKey("ANTHROPIC_API_KEY", "Anthropic", undefined);
558
578
  const elevenlabsKey = await getApiKey("ELEVENLABS_API_KEY", "ElevenLabs", undefined);
559
579
  if (!openaiKey) {
560
- console.error(chalk.red("OpenAI API key required for transcription. Set OPENAI_API_KEY in .env or run: vibe setup"));
561
- process.exit(1);
580
+ exitWithError(authError("OPENAI_API_KEY", "OpenAI"));
562
581
  }
563
582
  if (!anthropicKey) {
564
- console.error(chalk.red("Anthropic API key required for translation. Set ANTHROPIC_API_KEY in .env or run: vibe setup"));
565
- process.exit(1);
583
+ exitWithError(authError("ANTHROPIC_API_KEY", "Anthropic"));
566
584
  }
567
585
  if (!options.analyzeOnly && !elevenlabsKey) {
568
- console.error(chalk.red("ElevenLabs API key required for TTS. Set ELEVENLABS_API_KEY in .env or run: vibe setup"));
569
- console.error(chalk.dim("Or use --analyze-only to preview timing without generating audio"));
570
- process.exit(1);
586
+ exitWithError(authError("ELEVENLABS_API_KEY", "ElevenLabs"));
571
587
  }
572
588
  const spinner = ora("Extracting audio...").start();
573
589
  // Check if input is video
@@ -582,8 +598,8 @@ function _registerAudioCommands(aiCommand) {
582
598
  audioPath = tempAudioPath;
583
599
  }
584
600
  catch (error) {
585
- spinner.fail(chalk.red("Failed to extract audio from video"));
586
- process.exit(1);
601
+ spinner.fail("Failed to extract audio from video");
602
+ exitWithError(generalError(`Failed to extract audio from video: ${error instanceof Error ? error.message : String(error)}`));
587
603
  }
588
604
  }
589
605
  // Step 2: Transcribe with Whisper
@@ -594,8 +610,8 @@ function _registerAudioCommands(aiCommand) {
594
610
  const audioBlob = new Blob([audioBuffer]);
595
611
  const transcriptResult = await whisper.transcribe(audioBlob, options.source);
596
612
  if (transcriptResult.status === "failed" || !transcriptResult.segments) {
597
- spinner.fail(chalk.red(`Transcription failed: ${transcriptResult.error}`));
598
- process.exit(1);
613
+ spinner.fail("Transcription failed");
614
+ exitWithError(apiError(`Transcription failed: ${transcriptResult.error}`, true));
599
615
  }
600
616
  // Step 3: Translate with Claude
601
617
  spinner.text = "Translating...";
@@ -733,9 +749,7 @@ function _registerAudioCommands(aiCommand) {
733
749
  }
734
750
  }
735
751
  catch (error) {
736
- console.error(chalk.red("Dubbing failed"));
737
- console.error(error);
738
- process.exit(1);
752
+ exitWithError(apiError(`Dubbing failed: ${error instanceof Error ? error.message : String(error)}`, true));
739
753
  }
740
754
  });
741
755
  // ============================================
@@ -752,16 +766,22 @@ function _registerAudioCommands(aiCommand) {
752
766
  .option("-r, --ratio <ratio>", "Compression ratio", "3")
753
767
  .option("-a, --attack <ms>", "Attack time in ms", "20")
754
768
  .option("-l, --release <ms>", "Release time in ms", "200")
769
+ .option("--dry-run", "Preview parameters without executing")
755
770
  .action(async (musicPath, options) => {
756
771
  try {
772
+ if (options.output) {
773
+ validateOutputPath(options.output);
774
+ }
757
775
  if (!options.voice) {
758
- console.error(chalk.red("Voice track required. Use --voice <path>"));
759
- process.exit(1);
776
+ exitWithError(usageError("Voice track required. Use --voice <path>"));
777
+ }
778
+ if (options.dryRun) {
779
+ outputResult({ dryRun: true, command: "ai duck", params: { music: musicPath, output: options.output, voice: options.voice, threshold: options.threshold, ratio: options.ratio, attack: options.attack, release: options.release } });
780
+ return;
760
781
  }
761
782
  // Check FFmpeg availability
762
783
  if (!commandExists("ffmpeg")) {
763
- console.error(chalk.red("FFmpeg not found. Please install FFmpeg."));
764
- process.exit(1);
784
+ exitWithError(generalError("FFmpeg not found. Please install FFmpeg."));
765
785
  }
766
786
  const spinner = ora("Processing audio ducking...").start();
767
787
  const absMusic = resolve(process.cwd(), musicPath);
@@ -791,9 +811,7 @@ function _registerAudioCommands(aiCommand) {
791
811
  console.log();
792
812
  }
793
813
  catch (error) {
794
- console.error(chalk.red("Audio ducking failed"));
795
- console.error(error);
796
- process.exit(1);
814
+ exitWithError(generalError(`Audio ducking failed: ${error instanceof Error ? error.message : String(error)}`));
797
815
  }
798
816
  });
799
817
  // AI Color Grading