@nclamvn/vibecode-cli 1.6.0 → 1.8.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 (70) hide show
  1. package/bin/vibecode.js +101 -1
  2. package/docs-site/README.md +41 -0
  3. package/docs-site/blog/2019-05-28-first-blog-post.md +12 -0
  4. package/docs-site/blog/2019-05-29-long-blog-post.md +44 -0
  5. package/docs-site/blog/2021-08-01-mdx-blog-post.mdx +24 -0
  6. package/docs-site/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
  7. package/docs-site/blog/2021-08-26-welcome/index.md +29 -0
  8. package/docs-site/blog/authors.yml +25 -0
  9. package/docs-site/blog/tags.yml +19 -0
  10. package/docs-site/docs/commands/agent.md +162 -0
  11. package/docs-site/docs/commands/assist.md +71 -0
  12. package/docs-site/docs/commands/build.md +53 -0
  13. package/docs-site/docs/commands/config.md +30 -0
  14. package/docs-site/docs/commands/debug.md +173 -0
  15. package/docs-site/docs/commands/doctor.md +34 -0
  16. package/docs-site/docs/commands/go.md +128 -0
  17. package/docs-site/docs/commands/index.md +79 -0
  18. package/docs-site/docs/commands/init.md +42 -0
  19. package/docs-site/docs/commands/learn.md +82 -0
  20. package/docs-site/docs/commands/lock.md +33 -0
  21. package/docs-site/docs/commands/plan.md +29 -0
  22. package/docs-site/docs/commands/review.md +31 -0
  23. package/docs-site/docs/commands/snapshot.md +34 -0
  24. package/docs-site/docs/commands/start.md +32 -0
  25. package/docs-site/docs/commands/status.md +37 -0
  26. package/docs-site/docs/commands/undo.md +83 -0
  27. package/docs-site/docs/configuration.md +72 -0
  28. package/docs-site/docs/faq.md +83 -0
  29. package/docs-site/docs/getting-started.md +119 -0
  30. package/docs-site/docs/guides/agent-mode.md +94 -0
  31. package/docs-site/docs/guides/debug-mode.md +83 -0
  32. package/docs-site/docs/guides/magic-mode.md +107 -0
  33. package/docs-site/docs/installation.md +98 -0
  34. package/docs-site/docs/intro.md +67 -0
  35. package/docs-site/docusaurus.config.ts +141 -0
  36. package/docs-site/package-lock.json +18039 -0
  37. package/docs-site/package.json +48 -0
  38. package/docs-site/sidebars.ts +70 -0
  39. package/docs-site/src/components/HomepageFeatures/index.tsx +72 -0
  40. package/docs-site/src/components/HomepageFeatures/styles.module.css +16 -0
  41. package/docs-site/src/css/custom.css +30 -0
  42. package/docs-site/src/pages/index.module.css +23 -0
  43. package/docs-site/src/pages/index.tsx +44 -0
  44. package/docs-site/src/pages/markdown-page.md +7 -0
  45. package/docs-site/src/theme/Footer/index.tsx +127 -0
  46. package/docs-site/src/theme/Footer/styles.module.css +285 -0
  47. package/docs-site/static/.nojekyll +0 -0
  48. package/docs-site/static/img/docusaurus-social-card.jpg +0 -0
  49. package/docs-site/static/img/docusaurus.png +0 -0
  50. package/docs-site/static/img/favicon.ico +0 -0
  51. package/docs-site/static/img/logo.svg +1 -0
  52. package/docs-site/static/img/undraw_docusaurus_mountain.svg +171 -0
  53. package/docs-site/static/img/undraw_docusaurus_react.svg +170 -0
  54. package/docs-site/static/img/undraw_docusaurus_tree.svg +40 -0
  55. package/docs-site/tsconfig.json +8 -0
  56. package/package.json +2 -1
  57. package/src/commands/ask.js +230 -0
  58. package/src/commands/debug.js +109 -1
  59. package/src/commands/docs.js +167 -0
  60. package/src/commands/git.js +1024 -0
  61. package/src/commands/migrate.js +341 -0
  62. package/src/commands/refactor.js +205 -0
  63. package/src/commands/review.js +126 -1
  64. package/src/commands/security.js +229 -0
  65. package/src/commands/shell.js +486 -0
  66. package/src/commands/test.js +194 -0
  67. package/src/commands/watch.js +556 -0
  68. package/src/debug/image-analyzer.js +304 -0
  69. package/src/index.js +27 -0
  70. package/src/utils/image.js +222 -0
@@ -0,0 +1,1024 @@
1
+ /**
2
+ * Git Integration for Vibecode CLI
3
+ * Native git commands with enhanced UI and AI-powered commit messages
4
+ * Phase K7: AI Diff Review
5
+ */
6
+
7
+ import { exec, spawn } from 'child_process';
8
+ import { promisify } from 'util';
9
+ import fs from 'fs/promises';
10
+ import path from 'path';
11
+ import chalk from 'chalk';
12
+ import inquirer from 'inquirer';
13
+
14
+ const execAsync = promisify(exec);
15
+
16
+ /**
17
+ * Main git command handler
18
+ */
19
+ export async function gitCommand(subcommand, args = [], options = {}) {
20
+ // Check if in git repo
21
+ const isGitRepo = await checkGitRepo();
22
+ if (!isGitRepo) {
23
+ console.log(chalk.red('\n Khong phai git repository.'));
24
+ console.log(chalk.gray(' Chay: git init\n'));
25
+ return;
26
+ }
27
+
28
+ // No subcommand = interactive menu
29
+ if (!subcommand) {
30
+ return interactiveGit(options);
31
+ }
32
+
33
+ switch (subcommand) {
34
+ case 'status':
35
+ case 's':
36
+ return gitStatus(options);
37
+ case 'commit':
38
+ case 'c':
39
+ return gitCommit(args, options);
40
+ case 'diff':
41
+ case 'd':
42
+ return gitDiff(args, options);
43
+ case 'branch':
44
+ case 'b':
45
+ return gitBranch(args, options);
46
+ case 'push':
47
+ return gitPush(options);
48
+ case 'pull':
49
+ return gitPull(options);
50
+ case 'log':
51
+ case 'l':
52
+ return gitLog(options);
53
+ case 'stash':
54
+ return gitStash(args, options);
55
+ case 'unstash':
56
+ return gitUnstash(options);
57
+ case 'add':
58
+ case 'a':
59
+ return gitAdd(args, options);
60
+ case 'reset':
61
+ return gitReset(args, options);
62
+ case 'checkout':
63
+ case 'co':
64
+ return gitCheckout(args, options);
65
+ default:
66
+ // Pass through to git
67
+ return gitPassthrough(subcommand, args);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Check if current directory is a git repository
73
+ */
74
+ async function checkGitRepo() {
75
+ try {
76
+ await execAsync('git rev-parse --git-dir');
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Get current git status summary
85
+ */
86
+ async function getGitStatus() {
87
+ try {
88
+ const { stdout: branch } = await execAsync('git branch --show-current');
89
+ const { stdout: status } = await execAsync('git status --porcelain');
90
+
91
+ const lines = status.trim().split('\n').filter(Boolean);
92
+ const staged = lines.filter(l => l[0] !== ' ' && l[0] !== '?').length;
93
+ const changed = lines.length;
94
+ const untracked = lines.filter(l => l.startsWith('??')).length;
95
+
96
+ // Get remote status
97
+ let ahead = 0;
98
+ let behind = 0;
99
+ try {
100
+ const { stdout: remote } = await execAsync('git rev-list --left-right --count HEAD...@{upstream} 2>/dev/null');
101
+ const parts = remote.trim().split('\t');
102
+ ahead = parseInt(parts[0]) || 0;
103
+ behind = parseInt(parts[1]) || 0;
104
+ } catch {
105
+ // No upstream
106
+ }
107
+
108
+ return {
109
+ branch: branch.trim() || 'HEAD detached',
110
+ changed,
111
+ staged,
112
+ untracked,
113
+ ahead,
114
+ behind,
115
+ files: lines
116
+ };
117
+ } catch {
118
+ return { branch: 'unknown', changed: 0, staged: 0, untracked: 0, ahead: 0, behind: 0, files: [] };
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Interactive git menu
124
+ */
125
+ async function interactiveGit(options) {
126
+ const status = await getGitStatus();
127
+
128
+ // Build status line
129
+ let syncStatus = '';
130
+ if (status.ahead > 0) syncStatus += chalk.green(` ${status.ahead}`);
131
+ if (status.behind > 0) syncStatus += chalk.red(` ${status.behind}`);
132
+
133
+ console.log(chalk.cyan(`
134
+ +----------------------------------------------------------------------+
135
+ | VIBECODE GIT |
136
+ | |
137
+ | Branch: ${chalk.green(status.branch.padEnd(54))}|
138
+ | Changes: ${chalk.yellow(String(status.changed).padEnd(10))} Staged: ${chalk.green(String(status.staged).padEnd(10))} Untracked: ${chalk.gray(String(status.untracked).padEnd(7))}|${syncStatus ? `
139
+ | Sync: ${syncStatus.padEnd(58)}|` : ''}
140
+ +----------------------------------------------------------------------+
141
+ `));
142
+
143
+ const choices = [
144
+ { name: ` Status ${status.changed > 0 ? chalk.yellow(`(${status.changed} changes)`) : chalk.gray('(clean)')}`, value: 'status' },
145
+ { name: ' Commit changes', value: 'commit' },
146
+ { name: ' View diff', value: 'diff' },
147
+ new inquirer.Separator(),
148
+ { name: ' Switch/create branch', value: 'branch' },
149
+ { name: ' Push to remote', value: 'push' },
150
+ { name: ' Pull from remote', value: 'pull' },
151
+ new inquirer.Separator(),
152
+ { name: ' View log', value: 'log' },
153
+ { name: ' Stash changes', value: 'stash' },
154
+ { name: ' Unstash changes', value: 'unstash' },
155
+ new inquirer.Separator(),
156
+ { name: ' Exit', value: 'exit' }
157
+ ];
158
+
159
+ const { action } = await inquirer.prompt([{
160
+ type: 'list',
161
+ name: 'action',
162
+ message: 'Select action:',
163
+ choices,
164
+ pageSize: 15
165
+ }]);
166
+
167
+ if (action === 'exit') {
168
+ console.log(chalk.gray('\n Bye!\n'));
169
+ return;
170
+ }
171
+
172
+ switch (action) {
173
+ case 'status': await gitStatus(options); break;
174
+ case 'commit': await gitCommit([], options); break;
175
+ case 'diff': await gitDiff([], options); break;
176
+ case 'branch': await gitBranch([], options); break;
177
+ case 'push': await gitPush(options); break;
178
+ case 'pull': await gitPull(options); break;
179
+ case 'log': await gitLog(options); break;
180
+ case 'stash': await gitStash([], options); break;
181
+ case 'unstash': await gitUnstash(options); break;
182
+ }
183
+
184
+ // Return to menu
185
+ console.log('');
186
+ const { continueMenu } = await inquirer.prompt([{
187
+ type: 'confirm',
188
+ name: 'continueMenu',
189
+ message: 'Back to git menu?',
190
+ default: true
191
+ }]);
192
+
193
+ if (continueMenu) {
194
+ return interactiveGit(options);
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Show git status with colors
200
+ */
201
+ async function gitStatus(options) {
202
+ try {
203
+ const { stdout } = await execAsync('git status');
204
+
205
+ // Colorize output
206
+ const colored = stdout
207
+ .replace(/modified:/g, chalk.yellow('modified:'))
208
+ .replace(/new file:/g, chalk.green('new file:'))
209
+ .replace(/deleted:/g, chalk.red('deleted:'))
210
+ .replace(/renamed:/g, chalk.blue('renamed:'))
211
+ .replace(/Untracked files:/g, chalk.gray('Untracked files:'))
212
+ .replace(/On branch (\S+)/g, `On branch ${chalk.green('$1')}`)
213
+ .replace(/Your branch is ahead/g, chalk.green('Your branch is ahead'))
214
+ .replace(/Your branch is behind/g, chalk.red('Your branch is behind'))
215
+ .replace(/nothing to commit/g, chalk.green('nothing to commit'));
216
+
217
+ console.log('\n' + colored);
218
+ } catch (error) {
219
+ console.log(chalk.red(`\n Error: ${error.message}\n`));
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Commit changes with optional AI-generated message
225
+ */
226
+ async function gitCommit(args, options) {
227
+ const status = await getGitStatus();
228
+
229
+ if (status.changed === 0) {
230
+ console.log(chalk.yellow('\n No changes to commit.\n'));
231
+ return;
232
+ }
233
+
234
+ // Show what will be committed
235
+ console.log(chalk.cyan('\n Files to commit:'));
236
+ for (const file of status.files) {
237
+ const statusCode = file.substring(0, 2);
238
+ const filename = file.substring(3);
239
+
240
+ if (statusCode.includes('M')) {
241
+ console.log(chalk.yellow(` M ${filename}`));
242
+ } else if (statusCode.includes('A')) {
243
+ console.log(chalk.green(` A ${filename}`));
244
+ } else if (statusCode.startsWith('??')) {
245
+ console.log(chalk.gray(` ? ${filename}`));
246
+ } else if (statusCode.includes('D')) {
247
+ console.log(chalk.red(` D ${filename}`));
248
+ } else if (statusCode.includes('R')) {
249
+ console.log(chalk.blue(` R ${filename}`));
250
+ } else {
251
+ console.log(chalk.gray(` ${statusCode} ${filename}`));
252
+ }
253
+ }
254
+ console.log('');
255
+
256
+ let message = args.join(' ');
257
+
258
+ // If message provided via -m option
259
+ if (options.message) {
260
+ message = options.message;
261
+ }
262
+
263
+ // If no message, generate or ask
264
+ if (!message) {
265
+ const { messageChoice } = await inquirer.prompt([{
266
+ type: 'list',
267
+ name: 'messageChoice',
268
+ message: 'Commit message:',
269
+ choices: [
270
+ { name: ' Enter message manually', value: 'manual' },
271
+ { name: ' AI generate message', value: 'ai' },
272
+ { name: ' Cancel', value: 'cancel' }
273
+ ]
274
+ }]);
275
+
276
+ if (messageChoice === 'cancel') {
277
+ console.log(chalk.gray('\n Cancelled.\n'));
278
+ return;
279
+ }
280
+
281
+ if (messageChoice === 'manual') {
282
+ const { manualMessage } = await inquirer.prompt([{
283
+ type: 'input',
284
+ name: 'manualMessage',
285
+ message: 'Enter commit message:',
286
+ validate: (input) => input.length > 0 || 'Message cannot be empty'
287
+ }]);
288
+ message = manualMessage;
289
+ } else {
290
+ // AI generate
291
+ console.log(chalk.gray('\n Generating commit message...\n'));
292
+ message = await generateCommitMessage(status.files);
293
+ console.log(chalk.cyan(` Generated: ${chalk.white(message)}\n`));
294
+
295
+ const { useGenerated } = await inquirer.prompt([{
296
+ type: 'confirm',
297
+ name: 'useGenerated',
298
+ message: 'Use this message?',
299
+ default: true
300
+ }]);
301
+
302
+ if (!useGenerated) {
303
+ const { manualMessage } = await inquirer.prompt([{
304
+ type: 'input',
305
+ name: 'manualMessage',
306
+ message: 'Enter commit message:',
307
+ default: message,
308
+ validate: (input) => input.length > 0 || 'Message cannot be empty'
309
+ }]);
310
+ message = manualMessage;
311
+ }
312
+ }
313
+ }
314
+
315
+ // Confirm
316
+ const { confirm } = await inquirer.prompt([{
317
+ type: 'confirm',
318
+ name: 'confirm',
319
+ message: `Commit with message: "${chalk.cyan(message)}"?`,
320
+ default: true
321
+ }]);
322
+
323
+ if (!confirm) {
324
+ console.log(chalk.gray('\n Cancelled.\n'));
325
+ return;
326
+ }
327
+
328
+ try {
329
+ // Stage all if --auto or untracked files exist
330
+ if (options.auto || status.untracked > 0) {
331
+ await execAsync('git add -A');
332
+ console.log(chalk.gray(' Staged all changes.'));
333
+ }
334
+
335
+ // Commit
336
+ const escapedMessage = message.replace(/"/g, '\\"').replace(/`/g, '\\`');
337
+ await execAsync(`git commit -m "${escapedMessage}"`);
338
+
339
+ console.log(chalk.green(`\n Committed: ${message}\n`));
340
+
341
+ // Ask to push
342
+ const { shouldPush } = await inquirer.prompt([{
343
+ type: 'confirm',
344
+ name: 'shouldPush',
345
+ message: 'Push to remote?',
346
+ default: false
347
+ }]);
348
+
349
+ if (shouldPush) {
350
+ await gitPush(options);
351
+ }
352
+ } catch (error) {
353
+ if (error.message.includes('nothing to commit')) {
354
+ console.log(chalk.yellow('\n Nothing to commit. Stage files first with: vibecode git add\n'));
355
+ } else {
356
+ console.log(chalk.red(`\n Commit failed: ${error.message}\n`));
357
+ }
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Generate commit message based on changed files
363
+ */
364
+ async function generateCommitMessage(files) {
365
+ // Get diff for context
366
+ let diffSummary = '';
367
+ try {
368
+ const { stdout } = await execAsync('git diff --stat HEAD 2>/dev/null || git diff --stat --cached');
369
+ diffSummary = stdout;
370
+ } catch {
371
+ // Ignore
372
+ }
373
+
374
+ const fileNames = files.map(f => f.substring(3).toLowerCase());
375
+
376
+ // Analyze file types
377
+ const hasNewFiles = files.some(f => f.startsWith('A') || f.startsWith('??'));
378
+ const hasModified = files.some(f => f.includes('M'));
379
+ const hasDeleted = files.some(f => f.includes('D'));
380
+
381
+ // Detect categories
382
+ const hasTests = fileNames.some(f => f.includes('test') || f.includes('spec'));
383
+ const hasDocs = fileNames.some(f => f.includes('readme') || f.endsWith('.md'));
384
+ const hasConfig = fileNames.some(f => f.includes('config') || f.includes('package.json') || f.includes('.rc'));
385
+ const hasStyles = fileNames.some(f => f.endsWith('.css') || f.endsWith('.scss') || f.endsWith('.less'));
386
+ const hasSrc = fileNames.some(f => f.includes('src/'));
387
+
388
+ // Determine type
389
+ let type = 'chore';
390
+ let subject = 'update project';
391
+
392
+ if (hasTests && !hasSrc) {
393
+ type = 'test';
394
+ subject = 'update tests';
395
+ } else if (hasDocs && files.length === 1) {
396
+ type = 'docs';
397
+ subject = 'update documentation';
398
+ } else if (hasConfig && files.length <= 2) {
399
+ type = 'chore';
400
+ subject = 'update configuration';
401
+ } else if (hasStyles && !hasSrc) {
402
+ type = 'style';
403
+ subject = 'update styles';
404
+ } else if (hasNewFiles && !hasModified && !hasDeleted) {
405
+ type = 'feat';
406
+ const newFileNames = files.filter(f => f.startsWith('A') || f.startsWith('??')).map(f => f.substring(3));
407
+ if (newFileNames.length === 1) {
408
+ subject = `add ${newFileNames[0].split('/').pop()}`;
409
+ } else {
410
+ subject = 'add new files';
411
+ }
412
+ } else if (hasDeleted && !hasNewFiles && !hasModified) {
413
+ type = 'refactor';
414
+ subject = 'remove unused files';
415
+ } else if (hasModified && files.length === 1) {
416
+ const modifiedFile = files[0].substring(3);
417
+ subject = `update ${modifiedFile.split('/').pop()}`;
418
+ }
419
+
420
+ // Extract scope from file paths
421
+ let scope = '';
422
+ const dirs = fileNames.map(f => {
423
+ const parts = f.split('/');
424
+ return parts.length > 1 ? parts[0] : '';
425
+ }).filter(Boolean);
426
+
427
+ if (dirs.length > 0) {
428
+ const uniqueDirs = [...new Set(dirs)];
429
+ if (uniqueDirs.length === 1 && uniqueDirs[0] !== 'src') {
430
+ scope = `(${uniqueDirs[0]})`;
431
+ }
432
+ }
433
+
434
+ return `${type}${scope}: ${subject}`;
435
+ }
436
+
437
+ /**
438
+ * Show diff with syntax highlighting
439
+ */
440
+ async function gitDiff(args, options) {
441
+ // K7: AI diff review
442
+ if (options.review || args.includes('--review')) {
443
+ return aiDiffReview();
444
+ }
445
+
446
+ try {
447
+ const file = args[0];
448
+ let cmd = 'git diff';
449
+
450
+ if (file && file !== '--review') {
451
+ cmd = `git diff -- "${file}"`;
452
+ } else if (options.staged) {
453
+ cmd = 'git diff --cached';
454
+ }
455
+
456
+ const { stdout } = await execAsync(cmd);
457
+
458
+ if (!stdout.trim()) {
459
+ console.log(chalk.yellow('\n No changes to show.\n'));
460
+ if (!options.staged) {
461
+ console.log(chalk.gray(' Tip: Use --staged to see staged changes.\n'));
462
+ }
463
+ return;
464
+ }
465
+
466
+ // Colorize diff
467
+ const colored = stdout
468
+ .split('\n')
469
+ .map(line => {
470
+ if (line.startsWith('+') && !line.startsWith('+++')) {
471
+ return chalk.green(line);
472
+ } else if (line.startsWith('-') && !line.startsWith('---')) {
473
+ return chalk.red(line);
474
+ } else if (line.startsWith('@@')) {
475
+ return chalk.cyan(line);
476
+ } else if (line.startsWith('diff ')) {
477
+ return chalk.bold.white('\n' + line);
478
+ } else if (line.startsWith('index ')) {
479
+ return chalk.gray(line);
480
+ }
481
+ return line;
482
+ })
483
+ .join('\n');
484
+
485
+ console.log('\n' + colored + '\n');
486
+ } catch (error) {
487
+ console.log(chalk.red(`\n Error: ${error.message}\n`));
488
+ }
489
+ }
490
+
491
+ /**
492
+ * K7: AI Diff Review
493
+ */
494
+ async function aiDiffReview() {
495
+ const cwd = process.cwd();
496
+
497
+ console.log(chalk.cyan(`
498
+ ╭────────────────────────────────────────────────────────────────────╮
499
+ │ 🔍 AI DIFF REVIEW │
500
+ │ │
501
+ │ Reviewing staged changes... │
502
+ │ │
503
+ ╰────────────────────────────────────────────────────────────────────╯
504
+ `));
505
+
506
+ // Get diff
507
+ let diff = '';
508
+ try {
509
+ const { stdout: stagedDiff } = await execAsync('git diff --staged', { cwd });
510
+ diff = stagedDiff;
511
+ } catch {}
512
+
513
+ if (!diff.trim()) {
514
+ // Try unstaged
515
+ try {
516
+ const { stdout: unstagedDiff } = await execAsync('git diff', { cwd });
517
+ diff = unstagedDiff;
518
+ } catch {}
519
+ }
520
+
521
+ if (!diff.trim()) {
522
+ console.log(chalk.yellow('\n No changes to review.\n'));
523
+ return;
524
+ }
525
+
526
+ // Truncate diff if too long
527
+ const maxDiffLength = 8000;
528
+ const truncatedDiff = diff.length > maxDiffLength
529
+ ? diff.substring(0, maxDiffLength) + '\n... (truncated)'
530
+ : diff;
531
+
532
+ const prompt = `
533
+ # Git Diff Review
534
+
535
+ Review these code changes:
536
+
537
+ \`\`\`diff
538
+ ${truncatedDiff}
539
+ \`\`\`
540
+
541
+ ## Review Criteria:
542
+ 1. **Correctness**: Are there any bugs or logic errors?
543
+ 2. **Best Practices**: Does it follow conventions and patterns?
544
+ 3. **Security**: Any security concerns introduced?
545
+ 4. **Performance**: Any performance issues?
546
+ 5. **Tests**: Should tests be added/updated?
547
+ 6. **Edge Cases**: Any edge cases not handled?
548
+
549
+ ## Output Format:
550
+ 1. **Overall Assessment**: (Good / Concerns / Issues)
551
+ 2. **Specific Feedback**: Per file/change feedback
552
+ 3. **Suggested Improvements**: What could be better
553
+ 4. **Recommended Commit Message**: A conventional commit message for these changes
554
+
555
+ Review the diff now.
556
+ `;
557
+
558
+ const promptFile = path.join(cwd, '.vibecode', 'diff-review-prompt.md');
559
+ await fs.mkdir(path.dirname(promptFile), { recursive: true });
560
+ await fs.writeFile(promptFile, prompt);
561
+
562
+ console.log(chalk.gray(' Reviewing with Claude Code...\n'));
563
+
564
+ await runClaudeCode(prompt, cwd);
565
+
566
+ console.log(chalk.green('\n✅ Diff review complete!\n'));
567
+ }
568
+
569
+ /**
570
+ * Run Claude Code with prompt
571
+ */
572
+ async function runClaudeCode(prompt, cwd) {
573
+ return new Promise((resolve) => {
574
+ const child = spawn('claude', ['-p', prompt, '--dangerously-skip-permissions'], {
575
+ cwd,
576
+ stdio: 'inherit'
577
+ });
578
+
579
+ child.on('close', resolve);
580
+ child.on('error', () => resolve());
581
+ });
582
+ }
583
+
584
+ /**
585
+ * Branch management
586
+ */
587
+ async function gitBranch(args, options) {
588
+ const branchName = args[0];
589
+
590
+ if (!branchName) {
591
+ // Show branches
592
+ try {
593
+ const { stdout: branches } = await execAsync('git branch -a');
594
+ const { stdout: current } = await execAsync('git branch --show-current');
595
+
596
+ console.log(chalk.cyan('\n Branches:\n'));
597
+
598
+ const lines = branches.split('\n').filter(Boolean);
599
+ const localBranches = [];
600
+ const remoteBranches = [];
601
+
602
+ for (const line of lines) {
603
+ const trimmed = line.replace('*', '').trim();
604
+ if (line.includes('remotes/')) {
605
+ remoteBranches.push(trimmed);
606
+ } else {
607
+ localBranches.push(trimmed);
608
+ if (trimmed === current.trim()) {
609
+ console.log(chalk.green(` * ${trimmed}`));
610
+ } else {
611
+ console.log(` ${trimmed}`);
612
+ }
613
+ }
614
+ }
615
+
616
+ if (remoteBranches.length > 0) {
617
+ console.log(chalk.gray('\n Remote branches:'));
618
+ for (const rb of remoteBranches.slice(0, 5)) {
619
+ console.log(chalk.gray(` ${rb}`));
620
+ }
621
+ if (remoteBranches.length > 5) {
622
+ console.log(chalk.gray(` ... and ${remoteBranches.length - 5} more`));
623
+ }
624
+ }
625
+ console.log('');
626
+
627
+ // Ask to switch or create
628
+ const { action } = await inquirer.prompt([{
629
+ type: 'list',
630
+ name: 'action',
631
+ message: 'Action:',
632
+ choices: [
633
+ { name: ' Switch branch', value: 'switch' },
634
+ { name: ' Create new branch', value: 'create' },
635
+ { name: ' Delete branch', value: 'delete' },
636
+ { name: ' Back', value: 'exit' }
637
+ ]
638
+ }]);
639
+
640
+ if (action === 'exit') return;
641
+
642
+ if (action === 'switch') {
643
+ const { branch } = await inquirer.prompt([{
644
+ type: 'list',
645
+ name: 'branch',
646
+ message: 'Select branch:',
647
+ choices: localBranches.filter(b => b !== current.trim())
648
+ }]);
649
+
650
+ await execAsync(`git checkout "${branch}"`);
651
+ console.log(chalk.green(`\n Switched to ${branch}\n`));
652
+ } else if (action === 'create') {
653
+ const { newBranch } = await inquirer.prompt([{
654
+ type: 'input',
655
+ name: 'newBranch',
656
+ message: 'New branch name:',
657
+ validate: (input) => /^[\w\-\/]+$/.test(input) || 'Invalid branch name'
658
+ }]);
659
+
660
+ await execAsync(`git checkout -b "${newBranch}"`);
661
+ console.log(chalk.green(`\n Created and switched to ${newBranch}\n`));
662
+ } else if (action === 'delete') {
663
+ const deletableBranches = localBranches.filter(b => b !== current.trim());
664
+
665
+ if (deletableBranches.length === 0) {
666
+ console.log(chalk.yellow('\n No branches to delete.\n'));
667
+ return;
668
+ }
669
+
670
+ const { branchToDelete } = await inquirer.prompt([{
671
+ type: 'list',
672
+ name: 'branchToDelete',
673
+ message: 'Select branch to delete:',
674
+ choices: deletableBranches
675
+ }]);
676
+
677
+ const { confirmDelete } = await inquirer.prompt([{
678
+ type: 'confirm',
679
+ name: 'confirmDelete',
680
+ message: `Delete branch "${branchToDelete}"?`,
681
+ default: false
682
+ }]);
683
+
684
+ if (confirmDelete) {
685
+ await execAsync(`git branch -d "${branchToDelete}"`);
686
+ console.log(chalk.green(`\n Deleted branch ${branchToDelete}\n`));
687
+ }
688
+ }
689
+ } catch (error) {
690
+ console.log(chalk.red(`\n Error: ${error.message}\n`));
691
+ }
692
+ } else {
693
+ // Create/switch to branch
694
+ try {
695
+ // Check if branch exists
696
+ const { stdout } = await execAsync('git branch');
697
+ const exists = stdout.split('\n').some(b => b.replace('*', '').trim() === branchName);
698
+
699
+ if (exists) {
700
+ await execAsync(`git checkout "${branchName}"`);
701
+ console.log(chalk.green(`\n Switched to ${branchName}\n`));
702
+ } else {
703
+ await execAsync(`git checkout -b "${branchName}"`);
704
+ console.log(chalk.green(`\n Created and switched to ${branchName}\n`));
705
+ }
706
+ } catch (error) {
707
+ console.log(chalk.red(`\n Error: ${error.message}\n`));
708
+ }
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Push to remote
714
+ */
715
+ async function gitPush(options) {
716
+ try {
717
+ const { stdout: branch } = await execAsync('git branch --show-current');
718
+ const branchName = branch.trim();
719
+
720
+ console.log(chalk.cyan(`\n Pushing to ${branchName}...`));
721
+
722
+ // Check if remote exists
723
+ try {
724
+ await execAsync('git remote get-url origin');
725
+ } catch {
726
+ console.log(chalk.yellow('\n No remote configured.'));
727
+ console.log(chalk.gray(' Run: git remote add origin <url>\n'));
728
+ return;
729
+ }
730
+
731
+ // Push with upstream
732
+ const { stdout, stderr } = await execAsync(`git push -u origin "${branchName}" 2>&1`);
733
+
734
+ console.log(chalk.green('\n Push successful!\n'));
735
+ if (stdout) console.log(chalk.gray(stdout));
736
+ } catch (error) {
737
+ if (error.message.includes('rejected')) {
738
+ console.log(chalk.red('\n Push rejected. Pull first with: vibecode git pull\n'));
739
+ } else {
740
+ console.log(chalk.red(`\n Push failed: ${error.message}\n`));
741
+ }
742
+ }
743
+ }
744
+
745
+ /**
746
+ * Pull from remote
747
+ */
748
+ async function gitPull(options) {
749
+ try {
750
+ console.log(chalk.cyan('\n Pulling from remote...'));
751
+
752
+ const { stdout } = await execAsync('git pull 2>&1');
753
+
754
+ if (stdout.includes('Already up to date')) {
755
+ console.log(chalk.green('\n Already up to date.\n'));
756
+ } else {
757
+ console.log(chalk.green('\n Pull successful!'));
758
+ console.log(stdout + '\n');
759
+ }
760
+ } catch (error) {
761
+ if (error.message.includes('conflict')) {
762
+ console.log(chalk.red('\n Merge conflicts detected!'));
763
+ console.log(chalk.gray(' Resolve conflicts and commit.\n'));
764
+ } else {
765
+ console.log(chalk.red(`\n Pull failed: ${error.message}\n`));
766
+ }
767
+ }
768
+ }
769
+
770
+ /**
771
+ * Show commit log
772
+ */
773
+ async function gitLog(options) {
774
+ try {
775
+ const count = options.count || 15;
776
+ const { stdout } = await execAsync(`git log --oneline -${count}`);
777
+
778
+ console.log(chalk.cyan('\n Recent commits:\n'));
779
+
780
+ const lines = stdout.split('\n').filter(Boolean);
781
+ for (let i = 0; i < lines.length; i++) {
782
+ const line = lines[i];
783
+ const [hash, ...messageParts] = line.split(' ');
784
+ const message = messageParts.join(' ');
785
+
786
+ // Highlight conventional commit types
787
+ let formattedMessage = message;
788
+ if (message.startsWith('feat')) {
789
+ formattedMessage = chalk.green('feat') + message.slice(4);
790
+ } else if (message.startsWith('fix')) {
791
+ formattedMessage = chalk.red('fix') + message.slice(3);
792
+ } else if (message.startsWith('docs')) {
793
+ formattedMessage = chalk.blue('docs') + message.slice(4);
794
+ } else if (message.startsWith('refactor')) {
795
+ formattedMessage = chalk.yellow('refactor') + message.slice(8);
796
+ } else if (message.startsWith('test')) {
797
+ formattedMessage = chalk.magenta('test') + message.slice(4);
798
+ } else if (message.startsWith('chore')) {
799
+ formattedMessage = chalk.gray('chore') + message.slice(5);
800
+ }
801
+
802
+ console.log(` ${chalk.yellow(hash)} ${formattedMessage}`);
803
+ }
804
+ console.log('');
805
+ } catch (error) {
806
+ console.log(chalk.red(`\n Error: ${error.message}\n`));
807
+ }
808
+ }
809
+
810
+ /**
811
+ * Stash changes
812
+ */
813
+ async function gitStash(args, options) {
814
+ try {
815
+ const message = args.join(' ');
816
+
817
+ // Check if there are changes
818
+ const { stdout: status } = await execAsync('git status --porcelain');
819
+ if (!status.trim()) {
820
+ console.log(chalk.yellow('\n No changes to stash.\n'));
821
+ return;
822
+ }
823
+
824
+ const cmd = message ? `git stash push -m "${message}"` : 'git stash';
825
+
826
+ await execAsync(cmd);
827
+ console.log(chalk.green('\n Changes stashed!\n'));
828
+
829
+ // Show stash list
830
+ const { stdout: list } = await execAsync('git stash list');
831
+ if (list.trim()) {
832
+ console.log(chalk.gray(' Current stashes:'));
833
+ const stashes = list.trim().split('\n').slice(0, 3);
834
+ for (const s of stashes) {
835
+ console.log(chalk.gray(` ${s}`));
836
+ }
837
+ console.log('');
838
+ }
839
+ } catch (error) {
840
+ console.log(chalk.red(`\n Stash failed: ${error.message}\n`));
841
+ }
842
+ }
843
+
844
+ /**
845
+ * Unstash changes
846
+ */
847
+ async function gitUnstash(options) {
848
+ try {
849
+ // List stashes
850
+ const { stdout } = await execAsync('git stash list');
851
+
852
+ if (!stdout.trim()) {
853
+ console.log(chalk.yellow('\n No stashes found.\n'));
854
+ return;
855
+ }
856
+
857
+ const stashes = stdout.trim().split('\n');
858
+
859
+ console.log(chalk.cyan('\n Available stashes:\n'));
860
+
861
+ const { stash } = await inquirer.prompt([{
862
+ type: 'list',
863
+ name: 'stash',
864
+ message: 'Select stash to apply:',
865
+ choices: stashes.map((s, i) => ({
866
+ name: ` ${s}`,
867
+ value: i
868
+ }))
869
+ }]);
870
+
871
+ const { action } = await inquirer.prompt([{
872
+ type: 'list',
873
+ name: 'action',
874
+ message: 'Action:',
875
+ choices: [
876
+ { name: ' Pop (apply and remove)', value: 'pop' },
877
+ { name: ' Apply (keep stash)', value: 'apply' },
878
+ { name: ' Drop (delete stash)', value: 'drop' },
879
+ { name: ' Cancel', value: 'cancel' }
880
+ ]
881
+ }]);
882
+
883
+ if (action === 'cancel') return;
884
+
885
+ await execAsync(`git stash ${action} stash@{${stash}}`);
886
+ console.log(chalk.green(`\n Stash ${action} successful!\n`));
887
+ } catch (error) {
888
+ if (error.message.includes('conflict')) {
889
+ console.log(chalk.red('\n Merge conflicts detected!'));
890
+ console.log(chalk.gray(' Resolve conflicts manually.\n'));
891
+ } else {
892
+ console.log(chalk.red(`\n Unstash failed: ${error.message}\n`));
893
+ }
894
+ }
895
+ }
896
+
897
+ /**
898
+ * Stage files
899
+ */
900
+ async function gitAdd(args, options) {
901
+ try {
902
+ if (args.length === 0 || options.all) {
903
+ await execAsync('git add -A');
904
+ console.log(chalk.green('\n Staged all changes.\n'));
905
+ } else {
906
+ for (const file of args) {
907
+ await execAsync(`git add "${file}"`);
908
+ console.log(chalk.green(` Staged: ${file}`));
909
+ }
910
+ console.log('');
911
+ }
912
+ } catch (error) {
913
+ console.log(chalk.red(`\n Error: ${error.message}\n`));
914
+ }
915
+ }
916
+
917
+ /**
918
+ * Reset staged files
919
+ */
920
+ async function gitReset(args, options) {
921
+ try {
922
+ if (args.length === 0) {
923
+ const { confirm } = await inquirer.prompt([{
924
+ type: 'confirm',
925
+ name: 'confirm',
926
+ message: 'Unstage all files?',
927
+ default: true
928
+ }]);
929
+
930
+ if (confirm) {
931
+ await execAsync('git reset HEAD');
932
+ console.log(chalk.green('\n Unstaged all files.\n'));
933
+ }
934
+ } else {
935
+ for (const file of args) {
936
+ await execAsync(`git reset HEAD "${file}"`);
937
+ console.log(chalk.green(` Unstaged: ${file}`));
938
+ }
939
+ console.log('');
940
+ }
941
+ } catch (error) {
942
+ console.log(chalk.red(`\n Error: ${error.message}\n`));
943
+ }
944
+ }
945
+
946
+ /**
947
+ * Checkout files
948
+ */
949
+ async function gitCheckout(args, options) {
950
+ if (args.length === 0) {
951
+ return gitBranch([], options);
952
+ }
953
+
954
+ const target = args[0];
955
+
956
+ try {
957
+ // Check if it's a branch
958
+ const { stdout: branches } = await execAsync('git branch');
959
+ const isBranch = branches.split('\n').some(b => b.replace('*', '').trim() === target);
960
+
961
+ if (isBranch) {
962
+ await execAsync(`git checkout "${target}"`);
963
+ console.log(chalk.green(`\n Switched to branch ${target}\n`));
964
+ } else {
965
+ // Assume it's a file - restore it
966
+ const { confirm } = await inquirer.prompt([{
967
+ type: 'confirm',
968
+ name: 'confirm',
969
+ message: `Discard changes to "${target}"?`,
970
+ default: false
971
+ }]);
972
+
973
+ if (confirm) {
974
+ await execAsync(`git checkout -- "${target}"`);
975
+ console.log(chalk.green(`\n Restored: ${target}\n`));
976
+ }
977
+ }
978
+ } catch (error) {
979
+ console.log(chalk.red(`\n Error: ${error.message}\n`));
980
+ }
981
+ }
982
+
983
+ /**
984
+ * Pass through to native git
985
+ */
986
+ async function gitPassthrough(subcommand, args) {
987
+ try {
988
+ const cmd = `git ${subcommand} ${args.join(' ')}`.trim();
989
+ console.log(chalk.gray(`\n Running: ${cmd}\n`));
990
+
991
+ const { stdout, stderr } = await execAsync(cmd);
992
+
993
+ if (stdout) console.log(stdout);
994
+ if (stderr) console.log(chalk.yellow(stderr));
995
+ } catch (error) {
996
+ console.log(chalk.red(`\n Error: ${error.message}\n`));
997
+ }
998
+ }
999
+
1000
+ /**
1001
+ * Auto-commit after vibecode actions (for integration with other commands)
1002
+ */
1003
+ export async function autoCommit(action, files = []) {
1004
+ const isGitRepo = await checkGitRepo();
1005
+ if (!isGitRepo) return false;
1006
+
1007
+ try {
1008
+ // Check if there are changes
1009
+ const { stdout } = await execAsync('git status --porcelain');
1010
+ if (!stdout.trim()) return false;
1011
+
1012
+ // Stage and commit
1013
+ await execAsync('git add -A');
1014
+ await execAsync(`git commit -m "vibecode: ${action}"`);
1015
+
1016
+ console.log(chalk.gray(`\n Auto-committed: vibecode: ${action}`));
1017
+ return true;
1018
+ } catch {
1019
+ // Silently fail - not critical
1020
+ return false;
1021
+ }
1022
+ }
1023
+
1024
+ export default gitCommand;