@vibeframe/cli 0.27.0 → 0.29.0

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 (109) hide show
  1. package/LICENSE +21 -0
  2. package/dist/agent/adapters/index.d.ts +1 -0
  3. package/dist/agent/adapters/index.d.ts.map +1 -1
  4. package/dist/agent/adapters/index.js +5 -0
  5. package/dist/agent/adapters/index.js.map +1 -1
  6. package/dist/agent/adapters/openrouter.d.ts +16 -0
  7. package/dist/agent/adapters/openrouter.d.ts.map +1 -0
  8. package/dist/agent/adapters/openrouter.js +100 -0
  9. package/dist/agent/adapters/openrouter.js.map +1 -0
  10. package/dist/agent/types.d.ts +1 -1
  11. package/dist/agent/types.d.ts.map +1 -1
  12. package/dist/commands/agent.d.ts.map +1 -1
  13. package/dist/commands/agent.js +3 -1
  14. package/dist/commands/agent.js.map +1 -1
  15. package/dist/commands/setup.js +5 -2
  16. package/dist/commands/setup.js.map +1 -1
  17. package/dist/config/schema.d.ts +2 -1
  18. package/dist/config/schema.d.ts.map +1 -1
  19. package/dist/config/schema.js +2 -0
  20. package/dist/config/schema.js.map +1 -1
  21. package/dist/index.js +0 -0
  22. package/package.json +16 -12
  23. package/.turbo/turbo-build.log +0 -4
  24. package/.turbo/turbo-lint.log +0 -21
  25. package/.turbo/turbo-test.log +0 -689
  26. package/src/agent/adapters/claude.ts +0 -143
  27. package/src/agent/adapters/gemini.ts +0 -159
  28. package/src/agent/adapters/index.ts +0 -61
  29. package/src/agent/adapters/ollama.ts +0 -231
  30. package/src/agent/adapters/openai.ts +0 -116
  31. package/src/agent/adapters/xai.ts +0 -119
  32. package/src/agent/index.ts +0 -251
  33. package/src/agent/memory/index.ts +0 -151
  34. package/src/agent/prompts/system.ts +0 -106
  35. package/src/agent/tools/ai-editing.ts +0 -845
  36. package/src/agent/tools/ai-generation.ts +0 -1073
  37. package/src/agent/tools/ai-pipeline.ts +0 -1055
  38. package/src/agent/tools/ai.ts +0 -21
  39. package/src/agent/tools/batch.ts +0 -429
  40. package/src/agent/tools/e2e.test.ts +0 -545
  41. package/src/agent/tools/export.ts +0 -184
  42. package/src/agent/tools/filesystem.ts +0 -237
  43. package/src/agent/tools/index.ts +0 -150
  44. package/src/agent/tools/integration.test.ts +0 -775
  45. package/src/agent/tools/media.ts +0 -697
  46. package/src/agent/tools/project.ts +0 -313
  47. package/src/agent/tools/timeline.ts +0 -951
  48. package/src/agent/types.ts +0 -68
  49. package/src/commands/agent.ts +0 -340
  50. package/src/commands/ai-analyze.ts +0 -429
  51. package/src/commands/ai-animated-caption.ts +0 -390
  52. package/src/commands/ai-audio.ts +0 -941
  53. package/src/commands/ai-broll.ts +0 -490
  54. package/src/commands/ai-edit-cli.ts +0 -658
  55. package/src/commands/ai-edit.ts +0 -1542
  56. package/src/commands/ai-fill-gaps.ts +0 -566
  57. package/src/commands/ai-helpers.ts +0 -65
  58. package/src/commands/ai-highlights.ts +0 -1303
  59. package/src/commands/ai-image.ts +0 -761
  60. package/src/commands/ai-motion.ts +0 -347
  61. package/src/commands/ai-narrate.ts +0 -451
  62. package/src/commands/ai-review.ts +0 -309
  63. package/src/commands/ai-script-pipeline-cli.ts +0 -1710
  64. package/src/commands/ai-script-pipeline.ts +0 -1365
  65. package/src/commands/ai-suggest-edit.ts +0 -264
  66. package/src/commands/ai-video-fx.ts +0 -445
  67. package/src/commands/ai-video.ts +0 -915
  68. package/src/commands/ai-viral.ts +0 -595
  69. package/src/commands/ai-visual-fx.ts +0 -601
  70. package/src/commands/ai.test.ts +0 -627
  71. package/src/commands/ai.ts +0 -307
  72. package/src/commands/analyze.ts +0 -282
  73. package/src/commands/audio.ts +0 -644
  74. package/src/commands/batch.test.ts +0 -279
  75. package/src/commands/batch.ts +0 -440
  76. package/src/commands/detect.ts +0 -329
  77. package/src/commands/doctor.ts +0 -237
  78. package/src/commands/edit-cmd.ts +0 -1014
  79. package/src/commands/export.ts +0 -918
  80. package/src/commands/generate.ts +0 -2146
  81. package/src/commands/media.ts +0 -177
  82. package/src/commands/output.ts +0 -142
  83. package/src/commands/pipeline.ts +0 -398
  84. package/src/commands/project.test.ts +0 -127
  85. package/src/commands/project.ts +0 -149
  86. package/src/commands/sanitize.ts +0 -60
  87. package/src/commands/schema.ts +0 -130
  88. package/src/commands/setup.ts +0 -509
  89. package/src/commands/timeline.test.ts +0 -499
  90. package/src/commands/timeline.ts +0 -529
  91. package/src/commands/validate.ts +0 -77
  92. package/src/config/config.test.ts +0 -197
  93. package/src/config/index.ts +0 -125
  94. package/src/config/schema.ts +0 -82
  95. package/src/engine/index.ts +0 -2
  96. package/src/engine/project.test.ts +0 -702
  97. package/src/engine/project.ts +0 -439
  98. package/src/index.ts +0 -146
  99. package/src/utils/api-key.test.ts +0 -41
  100. package/src/utils/api-key.ts +0 -247
  101. package/src/utils/audio.ts +0 -83
  102. package/src/utils/exec-safe.ts +0 -75
  103. package/src/utils/first-run.ts +0 -52
  104. package/src/utils/provider-resolver.ts +0 -56
  105. package/src/utils/remotion.ts +0 -951
  106. package/src/utils/subtitle.test.ts +0 -227
  107. package/src/utils/subtitle.ts +0 -169
  108. package/src/utils/tty.ts +0 -196
  109. package/tsconfig.json +0 -20
@@ -1,329 +0,0 @@
1
- import { Command } from "commander";
2
- import { readFile, writeFile } from "node:fs/promises";
3
- import { resolve, basename } from "node:path";
4
- import chalk from "chalk";
5
- import ora from "ora";
6
- import { Project, type ProjectFile } from "../engine/index.js";
7
- import { execSafe, commandExists, ffprobeDuration } from "../utils/exec-safe.js";
8
-
9
- export const detectCommand = new Command("detect")
10
- .description("Auto-detect scenes, beats, and silences in media");
11
-
12
- /**
13
- * Scene change detection using FFmpeg
14
- */
15
- detectCommand
16
- .command("scenes")
17
- .description("Detect scene changes in video")
18
- .argument("<video>", "Video file path")
19
- .option("-t, --threshold <value>", "Scene change threshold (0-1)", "0.3")
20
- .option("-o, --output <path>", "Output JSON file with timestamps")
21
- .option("-p, --project <path>", "Add scenes as clips to project")
22
- .action(async (videoPath: string, options) => {
23
- const spinner = ora("Detecting scenes...").start();
24
-
25
- try {
26
- // Check if FFmpeg is available
27
- if (!commandExists("ffmpeg")) {
28
- spinner.fail(chalk.red("FFmpeg not found. Please install FFmpeg."));
29
- process.exit(1);
30
- }
31
-
32
- const absPath = resolve(process.cwd(), videoPath);
33
- const threshold = parseFloat(options.threshold);
34
-
35
- // Use FFmpeg to detect scene changes
36
- spinner.text = "Analyzing video...";
37
-
38
- const { stdout: sceneStdout, stderr: sceneStderr } = await execSafe("ffmpeg", [
39
- "-i", absPath,
40
- "-filter:v", `select='gt(scene,${threshold})',showinfo`,
41
- "-f", "null", "-",
42
- ], { maxBuffer: 50 * 1024 * 1024 }).catch((err) => {
43
- // ffmpeg writes filter output to stderr and exits non-zero with -f null
44
- if (err.stdout !== undefined || err.stderr !== undefined) {
45
- return { stdout: err.stdout || "", stderr: err.stderr || "" };
46
- }
47
- throw err;
48
- });
49
- const output = sceneStdout + sceneStderr;
50
-
51
- // Parse scene change timestamps from showinfo output
52
- const scenes: { timestamp: number; score: number }[] = [];
53
- const regex = /pts_time:(\d+\.?\d*)/g;
54
- let match;
55
-
56
- // Always start with 0
57
- scenes.push({ timestamp: 0, score: 1 });
58
-
59
- while ((match = regex.exec(output)) !== null) {
60
- const timestamp = parseFloat(match[1]);
61
- scenes.push({ timestamp, score: threshold });
62
- }
63
-
64
- // Get video duration
65
- const totalDuration = await ffprobeDuration(absPath);
66
-
67
- spinner.succeed(chalk.green(`Detected ${scenes.length} scenes`));
68
-
69
- console.log();
70
- console.log(chalk.bold.cyan("Scene Timestamps"));
71
- console.log(chalk.dim("─".repeat(60)));
72
-
73
- for (let i = 0; i < scenes.length; i++) {
74
- const start = scenes[i].timestamp;
75
- const end = i < scenes.length - 1 ? scenes[i + 1].timestamp : totalDuration;
76
- const duration = end - start;
77
- console.log(
78
- `${chalk.yellow(`[${i + 1}]`)} ${formatTimestamp(start)} - ${formatTimestamp(end)} ${chalk.dim(`(${duration.toFixed(1)}s)`)}`
79
- );
80
- }
81
- console.log();
82
-
83
- // Save to JSON
84
- if (options.output) {
85
- const outputPath = resolve(process.cwd(), options.output);
86
- const result = {
87
- source: absPath,
88
- totalDuration,
89
- threshold,
90
- scenes: scenes.map((s, i) => ({
91
- index: i,
92
- startTime: s.timestamp,
93
- endTime: i < scenes.length - 1 ? scenes[i + 1].timestamp : totalDuration,
94
- duration:
95
- (i < scenes.length - 1 ? scenes[i + 1].timestamp : totalDuration) - s.timestamp,
96
- })),
97
- };
98
- await writeFile(outputPath, JSON.stringify(result, null, 2), "utf-8");
99
- console.log(chalk.green(`Saved to: ${outputPath}`));
100
- }
101
-
102
- // Add to project
103
- if (options.project) {
104
- const projectPath = resolve(process.cwd(), options.project);
105
- const content = await readFile(projectPath, "utf-8");
106
- const data: ProjectFile = JSON.parse(content);
107
- const project = Project.fromJSON(data);
108
-
109
- // Add video as source if not exists
110
- let source = project.getSources().find((s) => s.url === absPath);
111
- if (!source) {
112
- source = project.addSource({
113
- name: basename(absPath),
114
- type: "video",
115
- url: absPath,
116
- duration: totalDuration,
117
- });
118
- }
119
-
120
- // Add clips for each scene
121
- for (let i = 0; i < scenes.length; i++) {
122
- const start = scenes[i].timestamp;
123
- const end = i < scenes.length - 1 ? scenes[i + 1].timestamp : totalDuration;
124
- const duration = end - start;
125
-
126
- project.addClip({
127
- sourceId: source.id,
128
- trackId: project.getTracks().find((t) => t.type === "video")?.id || "",
129
- startTime: start,
130
- duration,
131
- sourceStartOffset: start,
132
- sourceEndOffset: end,
133
- });
134
- }
135
-
136
- await writeFile(projectPath, JSON.stringify(project.toJSON(), null, 2), "utf-8");
137
- console.log(chalk.green(`Added ${scenes.length} clips to project`));
138
- }
139
- } catch (error) {
140
- spinner.fail(chalk.red("Scene detection failed"));
141
- console.error(error);
142
- process.exit(1);
143
- }
144
- });
145
-
146
- /**
147
- * Silence detection using FFmpeg
148
- */
149
- detectCommand
150
- .command("silence")
151
- .description("Detect silence in audio/video")
152
- .argument("<media>", "Media file path")
153
- .option("-n, --noise <dB>", "Noise threshold in dB", "-30")
154
- .option("-d, --duration <sec>", "Minimum silence duration", "0.5")
155
- .option("-o, --output <path>", "Output JSON file with timestamps")
156
- .action(async (mediaPath: string, options) => {
157
- const spinner = ora("Detecting silence...").start();
158
-
159
- try {
160
- const absPath = resolve(process.cwd(), mediaPath);
161
- const noise = options.noise;
162
- const duration = options.duration;
163
-
164
- const { stdout: silStdout, stderr: silStderr } = await execSafe("ffmpeg", [
165
- "-i", absPath,
166
- "-af", `silencedetect=noise=${noise}dB:d=${duration}`,
167
- "-f", "null", "-",
168
- ], { maxBuffer: 50 * 1024 * 1024 }).catch((err) => {
169
- if (err.stdout !== undefined || err.stderr !== undefined) {
170
- return { stdout: err.stdout || "", stderr: err.stderr || "" };
171
- }
172
- throw err;
173
- });
174
- const output = silStdout + silStderr;
175
-
176
- // Parse silence periods
177
- const silences: { start: number; end: number; duration: number }[] = [];
178
- const startRegex = /silence_start: (\d+\.?\d*)/g;
179
- const endRegex = /silence_end: (\d+\.?\d*) \| silence_duration: (\d+\.?\d*)/g;
180
-
181
- const starts: number[] = [];
182
- let match;
183
-
184
- while ((match = startRegex.exec(output)) !== null) {
185
- starts.push(parseFloat(match[1]));
186
- }
187
-
188
- let i = 0;
189
- while ((match = endRegex.exec(output)) !== null) {
190
- if (i < starts.length) {
191
- silences.push({
192
- start: starts[i],
193
- end: parseFloat(match[1]),
194
- duration: parseFloat(match[2]),
195
- });
196
- i++;
197
- }
198
- }
199
-
200
- spinner.succeed(chalk.green(`Detected ${silences.length} silence periods`));
201
-
202
- console.log();
203
- console.log(chalk.bold.cyan("Silence Periods"));
204
- console.log(chalk.dim("─".repeat(60)));
205
-
206
- for (let i = 0; i < silences.length; i++) {
207
- const s = silences[i];
208
- console.log(
209
- `${chalk.yellow(`[${i + 1}]`)} ${formatTimestamp(s.start)} - ${formatTimestamp(s.end)} ${chalk.dim(`(${s.duration.toFixed(1)}s)`)}`
210
- );
211
- }
212
- console.log();
213
-
214
- if (options.output) {
215
- const outputPath = resolve(process.cwd(), options.output);
216
- await writeFile(
217
- outputPath,
218
- JSON.stringify({ source: absPath, silences }, null, 2),
219
- "utf-8"
220
- );
221
- console.log(chalk.green(`Saved to: ${outputPath}`));
222
- }
223
- } catch (error) {
224
- spinner.fail(chalk.red("Silence detection failed"));
225
- console.error(error);
226
- process.exit(1);
227
- }
228
- });
229
-
230
- /**
231
- * Beat detection for music sync
232
- */
233
- detectCommand
234
- .command("beats")
235
- .description("Detect beats in audio (for music sync)")
236
- .argument("<audio>", "Audio file path")
237
- .option("-o, --output <path>", "Output JSON file with timestamps")
238
- .action(async (audioPath: string, options) => {
239
- const spinner = ora("Detecting beats...").start();
240
-
241
- try {
242
- const absPath = resolve(process.cwd(), audioPath);
243
-
244
- // Use FFmpeg's ebur128 filter for loudness analysis
245
- // This gives us a rough approximation of beat positions
246
- const { stdout: beatStdout, stderr: beatStderr } = await execSafe("ffmpeg", [
247
- "-i", absPath,
248
- "-af", "aresample=16000,ebur128=peak=true",
249
- "-f", "null", "-",
250
- ], { maxBuffer: 50 * 1024 * 1024 }).catch((err) => {
251
- if (err.stdout !== undefined || err.stderr !== undefined) {
252
- return { stdout: err.stdout || "", stderr: err.stderr || "" };
253
- }
254
- throw err;
255
- });
256
- const beatOutput = beatStdout + beatStderr;
257
-
258
- // Extract peak moments as approximate beats
259
- const beats: number[] = [];
260
- const peakRegex = /t:\s*(\d+\.?\d*)\s+M:\s*-?\d+\.?\d*\s+S:\s*-?\d+\.?\d*\s+I:\s*-?\d+\.?\d*\s+LUFS\s+LRA:\s*\d+\.?\d*\s+LU\s+FTPK:\s*(-?\d+\.?\d*)/g;
261
-
262
- let match;
263
- let lastBeatTime = -0.5;
264
-
265
- while ((match = peakRegex.exec(beatOutput)) !== null) {
266
- const time = parseFloat(match[1]);
267
- const peak = parseFloat(match[2]);
268
-
269
- // Consider it a beat if peak is high and enough time has passed
270
- if (peak > -10 && time - lastBeatTime > 0.3) {
271
- beats.push(time);
272
- lastBeatTime = time;
273
- }
274
- }
275
-
276
- // Fallback: if no beats detected, use simple interval-based detection
277
- if (beats.length === 0) {
278
- spinner.text = "Using interval-based detection...";
279
-
280
- // Get duration
281
- const totalDuration = await ffprobeDuration(absPath);
282
-
283
- // Estimate BPM from audio length (rough approximation)
284
- const estimatedBPM = 120;
285
- const beatInterval = 60 / estimatedBPM;
286
-
287
- for (let t = 0; t < totalDuration; t += beatInterval) {
288
- beats.push(t);
289
- }
290
- }
291
-
292
- spinner.succeed(chalk.green(`Detected ${beats.length} beats`));
293
-
294
- console.log();
295
- console.log(chalk.bold.cyan("Beat Timestamps"));
296
- console.log(chalk.dim("─".repeat(60)));
297
-
298
- // Show first 20 beats
299
- const displayBeats = beats.slice(0, 20);
300
- for (let i = 0; i < displayBeats.length; i++) {
301
- console.log(`${chalk.yellow(`[${i + 1}]`)} ${formatTimestamp(displayBeats[i])}`);
302
- }
303
-
304
- if (beats.length > 20) {
305
- console.log(chalk.dim(`... and ${beats.length - 20} more`));
306
- }
307
- console.log();
308
-
309
- if (options.output) {
310
- const outputPath = resolve(process.cwd(), options.output);
311
- await writeFile(
312
- outputPath,
313
- JSON.stringify({ source: absPath, beatCount: beats.length, beats }, null, 2),
314
- "utf-8"
315
- );
316
- console.log(chalk.green(`Saved to: ${outputPath}`));
317
- }
318
- } catch (error) {
319
- spinner.fail(chalk.red("Beat detection failed"));
320
- console.error(error);
321
- process.exit(1);
322
- }
323
- });
324
-
325
- function formatTimestamp(seconds: number): string {
326
- const mins = Math.floor(seconds / 60);
327
- const secs = (seconds % 60).toFixed(2);
328
- return `${mins.toString().padStart(2, "0")}:${secs.padStart(5, "0")}`;
329
- }
@@ -1,237 +0,0 @@
1
- /**
2
- * Doctor command - System health check and capability report
3
- */
4
-
5
- import { Command } from "commander";
6
- import chalk from "chalk";
7
- import { access } from "node:fs/promises";
8
- import { CONFIG_PATH } from "../config/index.js";
9
- import { PROVIDER_ENV_VARS } from "../config/schema.js";
10
- import { commandExists } from "../utils/exec-safe.js";
11
- import { execSafe } from "../utils/exec-safe.js";
12
- import { loadEnv } from "../utils/api-key.js";
13
-
14
- /** Mapping of env vars to the commands they unlock */
15
- const COMMAND_KEY_MAP: Record<string, string[]> = {
16
- GOOGLE_API_KEY: [
17
- "generate image",
18
- "generate video -p veo",
19
- "edit image",
20
- "analyze media",
21
- "analyze video",
22
- "analyze review",
23
- ],
24
- OPENAI_API_KEY: [
25
- "agent -p openai",
26
- "generate image -p openai",
27
- "edit image -p openai",
28
- "audio transcribe",
29
- "edit caption",
30
- "edit jump-cut",
31
- ],
32
- ANTHROPIC_API_KEY: [
33
- "agent -p claude",
34
- "generate storyboard",
35
- "generate motion",
36
- "edit grade",
37
- "edit reframe",
38
- "edit speed-ramp",
39
- "pipeline script-to-video",
40
- ],
41
- XAI_API_KEY: [
42
- "agent -p xai",
43
- "generate image -p grok",
44
- "generate video",
45
- "edit image -p grok",
46
- ],
47
- ELEVENLABS_API_KEY: [
48
- "generate speech",
49
- "generate sound-effect",
50
- "generate music",
51
- "audio voices",
52
- "audio voice-clone",
53
- "audio dub",
54
- ],
55
- KLING_API_KEY: ["generate video -p kling"],
56
- RUNWAY_API_SECRET: ["generate video -p runway"],
57
- REPLICATE_API_TOKEN: ["generate music -p replicate"],
58
- IMGBB_API_KEY: ["generate video -p kling (image-to-video)"],
59
- };
60
-
61
- /** Commands that need no API key (FFmpeg only) */
62
- const FREE_COMMANDS = [
63
- "edit silence-cut",
64
- "edit noise-reduce",
65
- "edit fade",
66
- "edit text-overlay",
67
- "detect scenes",
68
- "detect silence",
69
- "detect beats",
70
- "export",
71
- "project create/info",
72
- "timeline (all)",
73
- "batch import/concat",
74
- ];
75
-
76
- export const doctorCommand = new Command("doctor")
77
- .description("Check system health and available commands")
78
- .option("--json", "Output in JSON format")
79
- .addHelpText(
80
- "after",
81
- `
82
- Examples:
83
- $ vibe doctor Show system health and capabilities
84
- $ vibe doctor --json Machine-readable output
85
- `
86
- )
87
- .action(async (options) => {
88
- const isJson = options.json || process.env.VIBE_JSON_OUTPUT === "1";
89
- const results = await runDiagnostics();
90
-
91
- if (isJson) {
92
- console.log(JSON.stringify(results, null, 2));
93
- return;
94
- }
95
-
96
- printReport(results);
97
- });
98
-
99
- interface DiagnosticResults {
100
- system: {
101
- node: { version: string; ok: boolean };
102
- ffmpeg: { version: string | null; ok: boolean };
103
- config: { path: string; ok: boolean };
104
- };
105
- providers: Record<
106
- string,
107
- { envVar: string; configured: boolean; commands: string[] }
108
- >;
109
- readyCount: number;
110
- totalCount: number;
111
- }
112
-
113
- async function runDiagnostics(): Promise<DiagnosticResults> {
114
- loadEnv();
115
-
116
- // System checks
117
- const nodeVersion = process.version;
118
-
119
- let ffmpegVersion: string | null = null;
120
- const ffmpegExists = commandExists("ffmpeg");
121
- if (ffmpegExists) {
122
- try {
123
- const result = await execSafe("ffmpeg", ["-version"]);
124
- const match = result.stdout.match(/ffmpeg version (\S+)/);
125
- ffmpegVersion = match ? match[1] : "unknown";
126
- } catch {
127
- ffmpegVersion = "unknown";
128
- }
129
- }
130
-
131
- let configExists = false;
132
- try {
133
- await access(CONFIG_PATH);
134
- configExists = true;
135
- } catch {
136
- // not configured
137
- }
138
-
139
- // Provider checks
140
- const providers: DiagnosticResults["providers"] = {};
141
- let readyCount = FREE_COMMANDS.length;
142
- let totalCount = FREE_COMMANDS.length;
143
-
144
- for (const [envVar, commands] of Object.entries(COMMAND_KEY_MAP)) {
145
- const configured = !!process.env[envVar];
146
- const providerName =
147
- Object.entries(PROVIDER_ENV_VARS).find(([, v]) => v === envVar)?.[0] ??
148
- envVar;
149
- providers[providerName] = { envVar, configured, commands };
150
- totalCount += commands.length;
151
- if (configured) {
152
- readyCount += commands.length;
153
- }
154
- }
155
-
156
- return {
157
- system: {
158
- node: { version: nodeVersion, ok: true },
159
- ffmpeg: { version: ffmpegVersion, ok: ffmpegExists },
160
- config: { path: CONFIG_PATH, ok: configExists },
161
- },
162
- providers,
163
- readyCount,
164
- totalCount,
165
- };
166
- }
167
-
168
- function printReport(results: DiagnosticResults): void {
169
- console.log();
170
- console.log(chalk.bold(" System"));
171
-
172
- // Node
173
- const nodeIcon = results.system.node.ok ? chalk.green("OK") : chalk.red("MISSING");
174
- console.log(` Node.js ${results.system.node.version} ${nodeIcon}`);
175
-
176
- // FFmpeg
177
- if (results.system.ffmpeg.ok) {
178
- console.log(
179
- ` FFmpeg ${results.system.ffmpeg.version} ${chalk.green("OK")}`
180
- );
181
- } else {
182
- console.log(` FFmpeg ${chalk.red("NOT FOUND")} ${chalk.dim("Install: brew install ffmpeg")}`);
183
- }
184
-
185
- // Config
186
- if (results.system.config.ok) {
187
- console.log(` Config ${chalk.green("OK")} ${chalk.dim(results.system.config.path)}`);
188
- } else {
189
- console.log(
190
- ` Config ${chalk.yellow("NOT SET")} ${chalk.dim("Run: vibe setup")}`
191
- );
192
- }
193
-
194
- console.log();
195
- console.log(chalk.bold(" API Keys"));
196
-
197
- const configured: string[] = [];
198
- const missing: string[] = [];
199
-
200
- for (const [name, info] of Object.entries(results.providers)) {
201
- if (info.configured) {
202
- configured.push(name);
203
- console.log(
204
- ` ${chalk.green("OK")} ${info.envVar.padEnd(24)} ${chalk.dim(info.commands.join(", "))}`
205
- );
206
- } else {
207
- missing.push(name);
208
- }
209
- }
210
-
211
- if (missing.length > 0) {
212
- for (const name of missing) {
213
- const info = results.providers[name];
214
- console.log(
215
- ` ${chalk.red("--")} ${info.envVar.padEnd(24)} ${chalk.dim(info.commands.join(", "))}`
216
- );
217
- }
218
- }
219
-
220
- // Free commands
221
- console.log();
222
- console.log(chalk.bold(" No API key needed"));
223
- console.log(` ${chalk.dim(FREE_COMMANDS.join(", "))}`);
224
-
225
- // Summary
226
- console.log();
227
- const pct = Math.round((results.readyCount / results.totalCount) * 100);
228
- const readyColor = pct >= 80 ? chalk.green : pct >= 40 ? chalk.yellow : chalk.red;
229
- console.log(
230
- ` Ready: ${readyColor(`${results.readyCount}/${results.totalCount}`)} commands (${pct}%)`
231
- );
232
-
233
- if (missing.length > 0) {
234
- console.log(chalk.dim(` Run 'vibe setup' to configure more providers.`));
235
- }
236
- console.log();
237
- }