@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
@@ -33,11 +33,11 @@ import { requireApiKey, hasApiKey } from "../utils/api-key.js";
33
33
  import { hasTTY, prompt as promptText } from "../utils/tty.js";
34
34
  import { getApiKeyFromConfig } from "../config/index.js";
35
35
  import { sanitizeLLMResponse } from "./sanitize.js";
36
- import { isJsonMode, outputResult, log, exitWithError, usageError, apiError } from "./output.js";
36
+ import { isJsonMode, outputResult, log, exitWithError, usageError, apiError, generalError, authError, notFoundError } from "./output.js";
37
37
  import { commandExists } from "../utils/exec-safe.js";
38
38
  import { uploadToImgbb } from "./ai-script-pipeline.js";
39
39
  import { downloadVideo, formatTime } from "./ai-helpers.js";
40
- import { rejectControlChars } from "./validate.js";
40
+ import { rejectControlChars, validateOutputPath } from "./validate.js";
41
41
  import { resolveProvider } from "../utils/provider-resolver.js";
42
42
  import { executeThumbnailBestFrame } from "./ai-image.js";
43
43
  import { registerMotionCommand } from "./ai-motion.js";
@@ -116,16 +116,17 @@ Examples:
116
116
  if (hasTTY()) {
117
117
  prompt = await promptText(chalk.cyan("What would you like to generate? "));
118
118
  if (!prompt?.trim()) {
119
- console.error(chalk.red("Prompt is required."));
120
- return;
119
+ exitWithError(usageError("Prompt is required."));
121
120
  }
122
121
  }
123
122
  else {
124
- console.error(chalk.red("Prompt argument is required."));
125
- return;
123
+ exitWithError(usageError("Prompt argument is required.", "Usage: vibe generate image <prompt>"));
126
124
  }
127
125
  }
128
126
  rejectControlChars(prompt);
127
+ if (options.output) {
128
+ validateOutputPath(options.output);
129
+ }
129
130
  // Auto-resolve provider if user didn't explicitly set one
130
131
  let provider = options.provider.toLowerCase();
131
132
  const validProviders = ["openai", "dalle", "gemini", "grok", "runway"];
@@ -181,8 +182,8 @@ Examples:
181
182
  n: parseInt(options.count),
182
183
  });
183
184
  if (!result.success || !result.images) {
184
- spinner.fail(chalk.red(result.error || "Image generation failed"));
185
- process.exit(1);
185
+ spinner.fail(result.error || "Image generation failed");
186
+ exitWithError(apiError(result.error || "Image generation failed", true));
186
187
  }
187
188
  spinner.succeed(chalk.green(`Generated ${result.images.length} image(s) with OpenAI GPT Image 1.5`));
188
189
  if (isJsonMode()) {
@@ -260,8 +261,7 @@ Examples:
260
261
  // Validate aspect ratio
261
262
  const validRatios = ["1:1", "1:4", "1:8", "2:3", "3:2", "3:4", "4:1", "4:3", "4:5", "5:4", "8:1", "9:16", "16:9", "21:9"];
262
263
  if (options.ratio && !validRatios.includes(options.ratio)) {
263
- console.error(chalk.red(`Invalid ratio "${options.ratio}". Valid: ${validRatios.join(", ")}`));
264
- process.exit(1);
264
+ exitWithError(usageError(`Invalid ratio "${options.ratio}". Valid: ${validRatios.join(", ")}`));
265
265
  }
266
266
  const gemini = new GeminiProvider();
267
267
  await gemini.initialize({ apiKey });
@@ -288,8 +288,8 @@ Examples:
288
288
  usedLabel = "Nano Banana (fallback)";
289
289
  }
290
290
  if (!result.success || !result.images) {
291
- spinner.fail(chalk.red(result.error || "Image generation failed"));
292
- process.exit(1);
291
+ spinner.fail(result.error || "Image generation failed");
292
+ exitWithError(apiError(result.error || "Image generation failed", true));
293
293
  }
294
294
  spinner.succeed(chalk.green(`Generated ${result.images.length} image(s) with Gemini (${usedLabel})`));
295
295
  if (isJsonMode()) {
@@ -345,8 +345,8 @@ Examples:
345
345
  n: parseInt(options.count),
346
346
  });
347
347
  if (!result.success || !result.images) {
348
- spinner.fail(chalk.red(result.error || "Image generation failed"));
349
- process.exit(1);
348
+ spinner.fail(result.error || "Image generation failed");
349
+ exitWithError(apiError(result.error || "Image generation failed", true));
350
350
  }
351
351
  spinner.succeed(chalk.green(`Generated ${result.images.length} image(s) with xAI Grok`));
352
352
  if (isJsonMode()) {
@@ -416,8 +416,8 @@ Examples:
416
416
  const __dirname = dirname(__filename);
417
417
  const scriptPath = resolve(__dirname, "../../../../.claude/skills/runway-video/scripts/image.py");
418
418
  if (!options.output) {
419
- spinner.fail(chalk.red("Output path required for Runway. Use -o option."));
420
- process.exit(1);
419
+ spinner.fail("Output path required for Runway");
420
+ exitWithError(usageError("Output path required for Runway. Use -o option."));
421
421
  }
422
422
  const outputPath = resolve(process.cwd(), options.output);
423
423
  const args = [scriptPath, prompt, "-o", outputPath, "-r", options.ratio || "16:9"];
@@ -502,16 +502,17 @@ Examples:
502
502
  if (hasTTY()) {
503
503
  prompt = await promptText(chalk.cyan("Describe your video: "));
504
504
  if (!prompt?.trim()) {
505
- console.error(chalk.red("Prompt is required."));
506
- return;
505
+ exitWithError(usageError("Prompt is required."));
507
506
  }
508
507
  }
509
508
  else {
510
- console.error(chalk.red("Prompt argument is required."));
511
- return;
509
+ exitWithError(usageError("Prompt argument is required.", "Usage: vibe generate video <prompt>"));
512
510
  }
513
511
  }
514
512
  rejectControlChars(prompt);
513
+ if (options.output) {
514
+ validateOutputPath(options.output);
515
+ }
515
516
  let provider = options.provider.toLowerCase();
516
517
  const validProviders = ["runway", "kling", "veo", "grok"];
517
518
  if (!validProviders.includes(provider)) {
@@ -594,9 +595,7 @@ Examples:
594
595
  // Runway gen4_turbo requires an input image; gen4.5 supports text-to-video
595
596
  const runwayModel = options.runwayModel || "gen4.5";
596
597
  if (provider === "runway" && !options.image && runwayModel !== "gen4.5") {
597
- console.error(chalk.red(`Runway ${runwayModel} requires an input image. Use -i <image> or use gen4.5 for text-to-video.`));
598
- console.error(chalk.dim("Example: vibe generate video \"prompt\" -p runway -i image.png -o out.mp4"));
599
- process.exit(1);
598
+ exitWithError(usageError(`Runway ${runwayModel} requires an input image. Use -i <image> or use gen4.5 for text-to-video.`));
600
599
  }
601
600
  const spinner = ora(`Initializing ${providerName}...`).start();
602
601
  spinner.text = "Starting video generation...";
@@ -614,8 +613,8 @@ Examples:
614
613
  seed: options.seed ? parseInt(options.seed) : undefined,
615
614
  });
616
615
  if (result.status === "failed") {
617
- spinner.fail(chalk.red(result.error || "Failed to start generation"));
618
- process.exit(1);
616
+ spinner.fail(result.error || "Failed to start generation");
617
+ exitWithError(apiError(result.error || "Failed to start generation", true));
619
618
  }
620
619
  console.log();
621
620
  console.log(chalk.bold.cyan("Video Generation Started"));
@@ -641,8 +640,8 @@ Examples:
641
640
  const kling = new KlingProvider();
642
641
  await kling.initialize({ apiKey });
643
642
  if (!kling.isConfigured()) {
644
- spinner.fail(chalk.red("Invalid API key format. Use ACCESS_KEY:SECRET_KEY"));
645
- process.exit(1);
643
+ spinner.fail("Invalid API key format");
644
+ exitWithError(authError("KLING_API_KEY", "Kling"));
646
645
  }
647
646
  // Kling v2.x requires image URL, not base64 — auto-upload to ImgBB
648
647
  let klingImage = referenceImage;
@@ -650,17 +649,16 @@ Examples:
650
649
  spinner.text = "Uploading image to ImgBB for Kling...";
651
650
  const imgbbKey = (await getApiKeyFromConfig("imgbb")) || process.env.IMGBB_API_KEY;
652
651
  if (!imgbbKey) {
653
- spinner.fail(chalk.red("Kling requires image URL. Set IMGBB_API_KEY for auto-upload."));
654
- console.error(chalk.dim("Run: vibe setup --full to configure ImgBB"));
655
- process.exit(1);
652
+ spinner.fail("ImgBB API key required");
653
+ exitWithError(authError("IMGBB_API_KEY", "ImgBB"));
656
654
  }
657
655
  // Extract raw base64 from data URI
658
656
  const base64Data = klingImage.split(",")[1];
659
657
  const imageBuffer = Buffer.from(base64Data, "base64");
660
658
  const uploadResult = await uploadToImgbb(imageBuffer, imgbbKey);
661
659
  if (!uploadResult.success || !uploadResult.url) {
662
- spinner.fail(chalk.red(`ImgBB upload failed: ${uploadResult.error}`));
663
- process.exit(1);
660
+ spinner.fail("ImgBB upload failed");
661
+ exitWithError(apiError(`ImgBB upload failed: ${uploadResult.error}`, true));
664
662
  }
665
663
  klingImage = uploadResult.url;
666
664
  spinner.text = "Starting video generation...";
@@ -674,8 +672,8 @@ Examples:
674
672
  mode: options.mode,
675
673
  });
676
674
  if (result.status === "failed") {
677
- spinner.fail(chalk.red(result.error || "Failed to start generation"));
678
- process.exit(1);
675
+ spinner.fail(result.error || "Failed to start generation");
676
+ exitWithError(apiError(result.error || "Failed to start generation", true));
679
677
  }
680
678
  console.log();
681
679
  console.log(chalk.bold.cyan("Video Generation Started"));
@@ -742,8 +740,8 @@ Examples:
742
740
  personGeneration: options.person,
743
741
  });
744
742
  if (result.status === "failed") {
745
- spinner.fail(chalk.red(result.error || "Failed to start generation"));
746
- process.exit(1);
743
+ spinner.fail(result.error || "Failed to start generation");
744
+ exitWithError(apiError(result.error || "Failed to start generation", true));
747
745
  }
748
746
  console.log();
749
747
  console.log(chalk.bold.cyan("Video Generation Started"));
@@ -772,8 +770,8 @@ Examples:
772
770
  aspectRatio: options.ratio,
773
771
  });
774
772
  if (result.status === "failed") {
775
- spinner.fail(chalk.red(result.error || "Failed to start generation"));
776
- process.exit(1);
773
+ spinner.fail(result.error || "Failed to start generation");
774
+ exitWithError(apiError(result.error || "Failed to start generation", true));
777
775
  }
778
776
  console.log();
779
777
  console.log(chalk.bold.cyan("Video Generation Started"));
@@ -794,8 +792,8 @@ Examples:
794
792
  }, 300000);
795
793
  }
796
794
  if (!finalResult || finalResult.status !== "completed") {
797
- spinner.fail(chalk.red(finalResult?.error || "Generation failed"));
798
- process.exit(1);
795
+ spinner.fail(finalResult?.error || "Generation failed");
796
+ exitWithError(apiError(finalResult?.error || "Generation failed", true));
799
797
  }
800
798
  spinner.succeed(chalk.green("Video generated"));
801
799
  if (isJsonMode()) {
@@ -854,16 +852,17 @@ generateCommand
854
852
  if (hasTTY()) {
855
853
  text = await promptText(chalk.cyan("What text to speak? "));
856
854
  if (!text?.trim()) {
857
- console.error(chalk.red("Text is required."));
858
- return;
855
+ exitWithError(usageError("Text is required."));
859
856
  }
860
857
  }
861
858
  else {
862
- console.error(chalk.red("Text argument is required."));
863
- return;
859
+ exitWithError(usageError("Text argument is required.", "Usage: vibe generate speech <text>"));
864
860
  }
865
861
  }
866
862
  rejectControlChars(text);
863
+ if (options.output) {
864
+ validateOutputPath(options.output);
865
+ }
867
866
  if (options.dryRun) {
868
867
  outputResult({ dryRun: true, command: "generate speech", params: { text, voice: options.voice, output: options.output } });
869
868
  return;
@@ -898,8 +897,8 @@ generateCommand
898
897
  voiceId: options.voice,
899
898
  });
900
899
  if (!result.success || !result.audioBuffer) {
901
- spinner.fail(chalk.red(result.error || "TTS generation failed"));
902
- process.exit(1);
900
+ spinner.fail(result.error || "TTS generation failed");
901
+ exitWithError(apiError(result.error || "TTS generation failed", true));
903
902
  }
904
903
  const outputPath = resolve(process.cwd(), options.output);
905
904
  await writeFile(outputPath, result.audioBuffer);
@@ -945,9 +944,8 @@ generateCommand
945
944
  console.log();
946
945
  }
947
946
  catch (error) {
948
- console.error(chalk.red("TTS generation failed"));
949
- console.error(error);
950
- process.exit(1);
947
+ const msg = error instanceof Error ? error.message : String(error);
948
+ exitWithError(apiError(`TTS generation failed: ${msg}`, true));
951
949
  }
952
950
  });
953
951
  // ============================================================================
@@ -966,6 +964,9 @@ generateCommand
966
964
  .action(async (prompt, options) => {
967
965
  try {
968
966
  rejectControlChars(prompt);
967
+ if (options.output) {
968
+ validateOutputPath(options.output);
969
+ }
969
970
  if (options.dryRun) {
970
971
  outputResult({ dryRun: true, command: "generate sound-effect", params: { prompt, duration: options.duration, promptInfluence: options.promptInfluence, output: options.output } });
971
972
  return;
@@ -979,8 +980,8 @@ generateCommand
979
980
  promptInfluence: options.promptInfluence ? parseFloat(options.promptInfluence) : undefined,
980
981
  });
981
982
  if (!result.success || !result.audioBuffer) {
982
- spinner.fail(chalk.red(result.error || "Sound effect generation failed"));
983
- process.exit(1);
983
+ spinner.fail(result.error || "Sound effect generation failed");
984
+ exitWithError(apiError(result.error || "Sound effect generation failed", true));
984
985
  }
985
986
  const outputPath = resolve(process.cwd(), options.output);
986
987
  await writeFile(outputPath, result.audioBuffer);
@@ -993,9 +994,8 @@ generateCommand
993
994
  console.log();
994
995
  }
995
996
  catch (error) {
996
- console.error(chalk.red("Sound effect generation failed"));
997
- console.error(error);
998
- process.exit(1);
997
+ const msg = error instanceof Error ? error.message : String(error);
998
+ exitWithError(apiError(`Sound effect generation failed: ${msg}`, true));
999
999
  }
1000
1000
  });
1001
1001
  // ============================================================================
@@ -1017,6 +1017,9 @@ generateCommand
1017
1017
  .action(async (prompt, options) => {
1018
1018
  try {
1019
1019
  rejectControlChars(prompt);
1020
+ if (options.output) {
1021
+ validateOutputPath(options.output);
1022
+ }
1020
1023
  const provider = (options.provider || "elevenlabs").toLowerCase();
1021
1024
  if (options.dryRun) {
1022
1025
  outputResult({ dryRun: true, command: "generate music", params: { prompt, provider, duration: options.duration, model: options.model, output: options.output, instrumental: options.instrumental } });
@@ -1034,8 +1037,8 @@ generateCommand
1034
1037
  forceInstrumental: options.instrumental || false,
1035
1038
  });
1036
1039
  if (!result.success || !result.audioBuffer) {
1037
- spinner.fail(chalk.red(result.error || "Music generation failed"));
1038
- process.exit(1);
1040
+ spinner.fail(result.error || "Music generation failed");
1041
+ exitWithError(apiError(result.error || "Music generation failed", true));
1039
1042
  }
1040
1043
  const outputPath = resolve(process.cwd(), options.output);
1041
1044
  await writeFile(outputPath, result.audioBuffer);
@@ -1064,20 +1067,18 @@ generateCommand
1064
1067
  spinner.text = "Uploading melody reference...";
1065
1068
  const absPath = resolve(process.cwd(), options.melody);
1066
1069
  if (!existsSync(absPath)) {
1067
- spinner.fail(chalk.red(`Melody file not found: ${options.melody}`));
1068
- process.exit(1);
1070
+ spinner.fail(`Melody file not found: ${options.melody}`);
1071
+ exitWithError(notFoundError(options.melody));
1069
1072
  }
1070
- console.log(chalk.yellow("Note: Melody conditioning requires a publicly accessible URL"));
1071
- console.log(chalk.yellow("Please upload your melody file and provide the URL"));
1072
- process.exit(1);
1073
+ exitWithError(usageError("Melody conditioning requires a publicly accessible URL", "Please upload your melody file and provide the URL."));
1073
1074
  }
1074
1075
  const result = await replicate.generateMusic(prompt, {
1075
1076
  duration,
1076
1077
  model: options.model,
1077
1078
  });
1078
1079
  if (!result.success || !result.taskId) {
1079
- spinner.fail(chalk.red(result.error || "Music generation failed"));
1080
- process.exit(1);
1080
+ spinner.fail(result.error || "Music generation failed");
1081
+ exitWithError(apiError(result.error || "Music generation failed", true));
1081
1082
  }
1082
1083
  if (!options.wait) {
1083
1084
  spinner.succeed(chalk.green("Music generation started"));
@@ -1089,14 +1090,14 @@ generateCommand
1089
1090
  spinner.text = "Generating music (this may take a few minutes)...";
1090
1091
  const finalResult = await replicate.waitForMusic(result.taskId);
1091
1092
  if (!finalResult.success || !finalResult.audioUrl) {
1092
- spinner.fail(chalk.red(finalResult.error || "Music generation failed"));
1093
- process.exit(1);
1093
+ spinner.fail(finalResult.error || "Music generation failed");
1094
+ exitWithError(apiError(finalResult.error || "Music generation failed", true));
1094
1095
  }
1095
1096
  spinner.text = "Downloading generated audio...";
1096
1097
  const response = await fetch(finalResult.audioUrl);
1097
1098
  if (!response.ok) {
1098
- spinner.fail(chalk.red("Failed to download generated audio"));
1099
- process.exit(1);
1099
+ spinner.fail("Failed to download generated audio");
1100
+ exitWithError(apiError("Failed to download generated audio", true));
1100
1101
  }
1101
1102
  const audioBuffer = Buffer.from(await response.arrayBuffer());
1102
1103
  const outputPath = resolve(process.cwd(), options.output);
@@ -1114,9 +1115,8 @@ generateCommand
1114
1115
  }
1115
1116
  }
1116
1117
  catch (error) {
1117
- console.error(chalk.red("Music generation failed"));
1118
- console.error(error);
1119
- process.exit(1);
1118
+ const msg = error instanceof Error ? error.message : String(error);
1119
+ exitWithError(apiError(`Music generation failed: ${msg}`, true));
1120
1120
  }
1121
1121
  });
1122
1122
  // ============================================================================
@@ -1156,9 +1156,8 @@ generateCommand
1156
1156
  console.log();
1157
1157
  }
1158
1158
  catch (error) {
1159
- console.error(chalk.red("Failed to get music status"));
1160
- console.error(error);
1161
- process.exit(1);
1159
+ const msg = error instanceof Error ? error.message : String(error);
1160
+ exitWithError(apiError(`Failed to get music status: ${msg}`, true));
1162
1161
  }
1163
1162
  });
1164
1163
  // ============================================================================
@@ -1177,11 +1176,13 @@ generateCommand
1177
1176
  .action(async (content, options) => {
1178
1177
  try {
1179
1178
  rejectControlChars(content);
1179
+ if (options.output) {
1180
+ validateOutputPath(options.output);
1181
+ }
1180
1182
  // Validate creativity level
1181
1183
  const creativity = options.creativity?.toLowerCase();
1182
1184
  if (creativity && creativity !== "low" && creativity !== "high") {
1183
- console.error(chalk.red("Invalid creativity level. Use 'low' or 'high'."));
1184
- process.exit(1);
1185
+ exitWithError(usageError("Invalid creativity level. Use 'low' or 'high'."));
1185
1186
  }
1186
1187
  let textContent = content;
1187
1188
  if (options.file) {
@@ -1201,8 +1202,8 @@ generateCommand
1201
1202
  await claude.initialize({ apiKey });
1202
1203
  const segments = await claude.analyzeContent(textContent, options.duration ? parseFloat(options.duration) : undefined, { creativity: creativity });
1203
1204
  if (segments.length === 0) {
1204
- spinner.fail(chalk.red("Could not generate storyboard"));
1205
- process.exit(1);
1205
+ spinner.fail("Could not generate storyboard");
1206
+ exitWithError(apiError("Could not generate storyboard", true));
1206
1207
  }
1207
1208
  spinner.succeed(chalk.green(`Generated ${segments.length} segments`));
1208
1209
  for (const seg of segments) {
@@ -1243,9 +1244,8 @@ generateCommand
1243
1244
  }
1244
1245
  }
1245
1246
  catch (error) {
1246
- console.error(chalk.red("Storyboard generation failed"));
1247
- console.error(error);
1248
- process.exit(1);
1247
+ const msg = error instanceof Error ? error.message : String(error);
1248
+ exitWithError(apiError(`Storyboard generation failed: ${msg}`, true));
1249
1249
  }
1250
1250
  });
1251
1251
  // ============================================================================
@@ -1269,16 +1269,17 @@ generateCommand
1269
1269
  try {
1270
1270
  if (description)
1271
1271
  rejectControlChars(description);
1272
+ if (options.output) {
1273
+ validateOutputPath(options.output);
1274
+ }
1272
1275
  // Best-frame mode: analyze video with Gemini and extract frame
1273
1276
  if (options.bestFrame) {
1274
1277
  const absVideoPath = resolve(process.cwd(), options.bestFrame);
1275
1278
  if (!existsSync(absVideoPath)) {
1276
- console.error(chalk.red(`Video not found: ${absVideoPath}`));
1277
- process.exit(1);
1279
+ exitWithError(notFoundError(absVideoPath));
1278
1280
  }
1279
1281
  if (!commandExists("ffmpeg")) {
1280
- console.error(chalk.red("FFmpeg not found. Please install FFmpeg."));
1281
- process.exit(1);
1282
+ exitWithError(generalError("FFmpeg not found", "Install with: brew install ffmpeg (macOS) or apt install ffmpeg (Linux)"));
1282
1283
  }
1283
1284
  const apiKey = await requireApiKey("GOOGLE_API_KEY", "Google", options.apiKey);
1284
1285
  const name = basename(options.bestFrame, extname(options.bestFrame));
@@ -1292,8 +1293,8 @@ generateCommand
1292
1293
  apiKey,
1293
1294
  });
1294
1295
  if (!result.success) {
1295
- spinner.fail(chalk.red(result.error || "Best frame extraction failed"));
1296
- process.exit(1);
1296
+ spinner.fail(result.error || "Best frame extraction failed");
1297
+ exitWithError(apiError(result.error || "Best frame extraction failed", true));
1297
1298
  }
1298
1299
  spinner.succeed(chalk.green("Best frame extracted"));
1299
1300
  if (isJsonMode()) {
@@ -1312,9 +1313,7 @@ generateCommand
1312
1313
  }
1313
1314
  // Generation mode: create thumbnail with DALL-E
1314
1315
  if (!description) {
1315
- console.error(chalk.red("Description required for thumbnail generation."));
1316
- console.error(chalk.dim("Usage: vibe generate thumbnail <description> or vibe generate thumbnail --best-frame <video>"));
1317
- process.exit(1);
1316
+ exitWithError(usageError("Description required for thumbnail generation.", "Usage: vibe generate thumbnail <description> or vibe generate thumbnail --best-frame <video>"));
1318
1317
  }
1319
1318
  const apiKey = await requireApiKey("OPENAI_API_KEY", "OpenAI", options.apiKey);
1320
1319
  const spinner = ora("Generating thumbnail...").start();
@@ -1322,8 +1321,8 @@ generateCommand
1322
1321
  await openaiImage.initialize({ apiKey });
1323
1322
  const result = await openaiImage.generateThumbnail(description, options.style);
1324
1323
  if (!result.success || !result.images) {
1325
- spinner.fail(chalk.red(result.error || "Thumbnail generation failed"));
1326
- process.exit(1);
1324
+ spinner.fail(result.error || "Thumbnail generation failed");
1325
+ exitWithError(apiError(result.error || "Thumbnail generation failed", true));
1327
1326
  }
1328
1327
  spinner.succeed(chalk.green("Thumbnail generated"));
1329
1328
  const img = result.images[0];
@@ -1382,9 +1381,8 @@ generateCommand
1382
1381
  }
1383
1382
  }
1384
1383
  catch (error) {
1385
- console.error(chalk.red("Thumbnail generation failed"));
1386
- console.error(error);
1387
- process.exit(1);
1384
+ const msg = error instanceof Error ? error.message : String(error);
1385
+ exitWithError(apiError(`Thumbnail generation failed: ${msg}`, true));
1388
1386
  }
1389
1387
  });
1390
1388
  // ============================================================================
@@ -1401,6 +1399,9 @@ generateCommand
1401
1399
  .action(async (description, options) => {
1402
1400
  try {
1403
1401
  rejectControlChars(description);
1402
+ if (options.output) {
1403
+ validateOutputPath(options.output);
1404
+ }
1404
1405
  if (options.dryRun) {
1405
1406
  outputResult({ dryRun: true, command: "generate background", params: { description, aspect: options.aspect, output: options.output } });
1406
1407
  return;
@@ -1411,8 +1412,8 @@ generateCommand
1411
1412
  await openaiImage.initialize({ apiKey });
1412
1413
  const result = await openaiImage.generateBackground(description, options.aspect);
1413
1414
  if (!result.success || !result.images) {
1414
- spinner.fail(chalk.red(result.error || "Background generation failed"));
1415
- process.exit(1);
1415
+ spinner.fail(result.error || "Background generation failed");
1416
+ exitWithError(apiError(result.error || "Background generation failed", true));
1416
1417
  }
1417
1418
  spinner.succeed(chalk.green("Background generated"));
1418
1419
  const img = result.images[0];
@@ -1471,9 +1472,8 @@ generateCommand
1471
1472
  }
1472
1473
  }
1473
1474
  catch (error) {
1474
- console.error(chalk.red("Background generation failed"));
1475
- console.error(error);
1476
- process.exit(1);
1475
+ const msg = error instanceof Error ? error.message : String(error);
1476
+ exitWithError(apiError(`Background generation failed: ${msg}`, true));
1477
1477
  }
1478
1478
  });
1479
1479
  // ============================================================================
@@ -1649,14 +1649,12 @@ generateCommand
1649
1649
  }
1650
1650
  }
1651
1651
  else {
1652
- console.error(chalk.red(`Invalid provider: ${provider}. Use grok, runway, or kling.`));
1653
- process.exit(1);
1652
+ exitWithError(usageError(`Invalid provider: ${provider}. Use grok, runway, or kling.`));
1654
1653
  }
1655
1654
  }
1656
1655
  catch (error) {
1657
- console.error(chalk.red("Failed to get status"));
1658
- console.error(error);
1659
- process.exit(1);
1656
+ const msg = error instanceof Error ? error.message : String(error);
1657
+ exitWithError(apiError(`Failed to get status: ${msg}`, true));
1660
1658
  }
1661
1659
  });
1662
1660
  // ============================================================================
@@ -1686,8 +1684,8 @@ generateCommand
1686
1684
  }
1687
1685
  }
1688
1686
  else {
1689
- spinner.fail(chalk.red("Failed to cancel generation"));
1690
- process.exit(1);
1687
+ spinner.fail("Failed to cancel generation");
1688
+ exitWithError(apiError("Failed to cancel generation", true));
1691
1689
  }
1692
1690
  }
1693
1691
  else if (provider === "runway") {
@@ -1704,19 +1702,17 @@ generateCommand
1704
1702
  }
1705
1703
  }
1706
1704
  else {
1707
- spinner.fail(chalk.red("Failed to cancel generation"));
1708
- process.exit(1);
1705
+ spinner.fail("Failed to cancel generation");
1706
+ exitWithError(apiError("Failed to cancel generation", true));
1709
1707
  }
1710
1708
  }
1711
1709
  else {
1712
- console.error(chalk.red(`Invalid provider: ${provider}. Use grok or runway.`));
1713
- process.exit(1);
1710
+ exitWithError(usageError(`Invalid provider: ${provider}. Use grok or runway.`));
1714
1711
  }
1715
1712
  }
1716
1713
  catch (error) {
1717
- console.error(chalk.red("Failed to cancel"));
1718
- console.error(error);
1719
- process.exit(1);
1714
+ const msg = error instanceof Error ? error.message : String(error);
1715
+ exitWithError(apiError(`Failed to cancel: ${msg}`, true));
1720
1716
  }
1721
1717
  });
1722
1718
  // ============================================================================
@@ -1739,6 +1735,9 @@ generateCommand
1739
1735
  .action(async (id, options) => {
1740
1736
  try {
1741
1737
  const provider = (options.provider || "kling").toLowerCase();
1738
+ if (options.output) {
1739
+ validateOutputPath(options.output);
1740
+ }
1742
1741
  if (options.dryRun) {
1743
1742
  outputResult({ dryRun: true, command: "generate video-extend", params: { id, provider, prompt: options.prompt, duration: options.duration, negative: options.negative, veoModel: options.veoModel } });
1744
1743
  return;
@@ -1749,8 +1748,8 @@ generateCommand
1749
1748
  const kling = new KlingProvider();
1750
1749
  await kling.initialize({ apiKey });
1751
1750
  if (!kling.isConfigured()) {
1752
- spinner.fail(chalk.red("Invalid API key format. Use ACCESS_KEY:SECRET_KEY"));
1753
- process.exit(1);
1751
+ spinner.fail("Invalid API key format");
1752
+ exitWithError(authError("KLING_API_KEY", "Kling"));
1754
1753
  }
1755
1754
  spinner.text = "Starting video extension...";
1756
1755
  const result = await kling.extendVideo(id, {
@@ -1759,8 +1758,8 @@ generateCommand
1759
1758
  duration: options.duration,
1760
1759
  });
1761
1760
  if (result.status === "failed") {
1762
- spinner.fail(chalk.red(result.error || "Failed to start extension"));
1763
- process.exit(1);
1761
+ spinner.fail(result.error || "Failed to start extension");
1762
+ exitWithError(apiError(result.error || "Failed to start extension", true));
1764
1763
  }
1765
1764
  console.log();
1766
1765
  console.log(chalk.bold.cyan("Video Extension Started"));
@@ -1780,8 +1779,8 @@ generateCommand
1780
1779
  spinner.text = `Extending video... ${status.status}`;
1781
1780
  }, 600000);
1782
1781
  if (finalResult.status !== "completed") {
1783
- spinner.fail(chalk.red(finalResult.error || "Extension failed"));
1784
- process.exit(1);
1782
+ spinner.fail(finalResult.error || "Extension failed");
1783
+ exitWithError(apiError(finalResult.error || "Extension failed", true));
1785
1784
  }
1786
1785
  spinner.succeed(chalk.green("Video extended"));
1787
1786
  if (isJsonMode()) {
@@ -1832,8 +1831,8 @@ generateCommand
1832
1831
  model: veoModel,
1833
1832
  });
1834
1833
  if (result.status === "failed") {
1835
- spinner.fail(chalk.red(result.error || "Failed to start extension"));
1836
- process.exit(1);
1834
+ spinner.fail(result.error || "Failed to start extension");
1835
+ exitWithError(apiError(result.error || "Failed to start extension", true));
1837
1836
  }
1838
1837
  console.log();
1839
1838
  console.log(chalk.bold.cyan("Veo Video Extension Started"));
@@ -1853,8 +1852,8 @@ generateCommand
1853
1852
  spinner.text = `Extending video... ${status.status}`;
1854
1853
  }, 300000);
1855
1854
  if (finalResult.status !== "completed") {
1856
- spinner.fail(chalk.red(finalResult.error || "Extension failed"));
1857
- process.exit(1);
1855
+ spinner.fail(finalResult.error || "Extension failed");
1856
+ exitWithError(apiError(finalResult.error || "Extension failed", true));
1858
1857
  }
1859
1858
  spinner.succeed(chalk.green("Video extended"));
1860
1859
  if (isJsonMode()) {
@@ -1886,14 +1885,12 @@ generateCommand
1886
1885
  }
1887
1886
  }
1888
1887
  else {
1889
- console.error(chalk.red(`Invalid provider: ${provider}. Video extend supports: kling, veo`));
1890
- process.exit(1);
1888
+ exitWithError(usageError(`Invalid provider: ${provider}. Video extend supports: kling, veo`));
1891
1889
  }
1892
1890
  }
1893
1891
  catch (error) {
1894
- console.error(chalk.red("Video extension failed"));
1895
- console.error(error);
1896
- process.exit(1);
1892
+ const msg = error instanceof Error ? error.message : String(error);
1893
+ exitWithError(apiError(`Video extension failed: ${msg}`, true));
1897
1894
  }
1898
1895
  });
1899
1896
  //# sourceMappingURL=generate.js.map