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