@snapcommit/cli 3.9.20 → 3.10.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.
@@ -42,6 +42,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
43
  exports.executeCursorStyle = executeCursorStyle;
44
44
  const chalk_1 = __importDefault(require("chalk"));
45
+ const fs_1 = __importDefault(require("fs"));
45
46
  const child_process_1 = require("child_process");
46
47
  const git_1 = require("../utils/git");
47
48
  const auth_1 = require("../lib/auth");
@@ -343,42 +344,8 @@ async function executeCommitWithAI(intent, originalInput = '') {
343
344
  catch (pullError) {
344
345
  // Handle rebase conflicts
345
346
  if (pullError.message.includes('conflict')) {
346
- console.log(chalk_1.default.yellow('\n⚠️ Merge conflicts detected - resolving...'));
347
- // Try to auto-resolve conflicts
348
347
  const resolved = await tryAdvancedConflictResolution();
349
- if (resolved) {
350
- console.log(chalk_1.default.green('✓ Conflicts resolved automatically!\n'));
351
- }
352
- else {
353
- // Better messaging for manual resolution
354
- console.log(chalk_1.default.yellow('\n⚠️ AI could not auto-resolve these conflicts\n'));
355
- console.log(chalk_1.default.white('Why? These conflicts require human judgment:'));
356
- console.log(chalk_1.default.gray(' • Business logic decisions (which approach is correct?)'));
357
- console.log(chalk_1.default.gray(' • Architectural changes (incompatible design choices)'));
358
- console.log(chalk_1.default.gray(' • Semantic conflicts (both valid, but different intent)\n'));
359
- console.log(chalk_1.default.white('Conflicted files:'));
360
- const conflicts = (0, child_process_1.execSync)('git diff --name-only --diff-filter=U', { encoding: 'utf-8' });
361
- conflicts.split('\n').filter(f => f.trim()).forEach(file => {
362
- console.log(chalk_1.default.red(` ✗ ${file}`));
363
- });
364
- console.log(chalk_1.default.cyan('\n📖 How to resolve manually:\n'));
365
- console.log(chalk_1.default.white(' 1. Open each file above in your editor'));
366
- console.log(chalk_1.default.gray(' Look for conflict markers: ') + chalk_1.default.yellow('<<<<<<< HEAD'));
367
- console.log(chalk_1.default.gray(' ') + chalk_1.default.yellow('======='));
368
- console.log(chalk_1.default.gray(' ') + chalk_1.default.yellow('>>>>>>> branch'));
369
- console.log('');
370
- console.log(chalk_1.default.white(' 2. Choose which code to keep (or merge both)'));
371
- console.log(chalk_1.default.gray(' Remove the conflict markers when done'));
372
- console.log('');
373
- console.log(chalk_1.default.white(' 3. After fixing, stage the resolved files:'));
374
- console.log(chalk_1.default.cyan(' git add <files>'));
375
- console.log('');
376
- console.log(chalk_1.default.white(' 4. Continue the rebase:'));
377
- console.log(chalk_1.default.cyan(' git rebase --continue'));
378
- console.log('');
379
- console.log(chalk_1.default.white(' 5. Then push your changes:'));
380
- console.log(chalk_1.default.cyan(' snap') + chalk_1.default.gray(' and type ') + chalk_1.default.cyan('push changes\n'));
381
- console.log(chalk_1.default.gray('💡 Tip: Most conflicts are simple! Just pick the better code.\n'));
348
+ if (!resolved) {
382
349
  return;
383
350
  }
384
351
  }
@@ -452,162 +419,6 @@ async function executeCommitWithAI(intent, originalInput = '') {
452
419
  console.log();
453
420
  }
454
421
  }
455
- /**
456
- * AI-POWERED conflict resolution - intelligently resolves merge conflicts!
457
- */
458
- async function tryAdvancedConflictResolution() {
459
- try {
460
- // Get conflicted files
461
- const conflictedFiles = (0, child_process_1.execSync)('git diff --name-only --diff-filter=U', { encoding: 'utf-8' })
462
- .split('\n')
463
- .filter(f => f.trim());
464
- if (conflictedFiles.length === 0)
465
- return false;
466
- console.log(chalk_1.default.white(`\n📋 Analyzing ${conflictedFiles.length} conflicted file(s)...\n`));
467
- // For each conflicted file, use AI to resolve
468
- for (const file of conflictedFiles) {
469
- try {
470
- // Read the conflicted file
471
- const conflictContent = (0, child_process_1.execSync)(`git show :1:${file}`, { encoding: 'utf-8', stdio: 'pipe' });
472
- const oursContent = (0, child_process_1.execSync)(`git show :2:${file}`, { encoding: 'utf-8', stdio: 'pipe' });
473
- const theirsContent = (0, child_process_1.execSync)(`git show :3:${file}`, { encoding: 'utf-8', stdio: 'pipe' });
474
- // Ask AI to resolve the conflict
475
- console.log(chalk_1.default.blue(`🤖 AI analyzing: ${file}...`));
476
- const resolution = await resolveConflictWithAIDetailed(file, conflictContent, oursContent, theirsContent);
477
- if (resolution) {
478
- console.log(chalk_1.default.green(`✓ AI resolved: ${file}`));
479
- // Write the resolved content
480
- const fs = await Promise.resolve().then(() => __importStar(require('fs')));
481
- fs.writeFileSync(file, resolution);
482
- (0, child_process_1.execSync)(`git add ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
483
- }
484
- else {
485
- // Fallback: ask user which version to keep
486
- console.log(chalk_1.default.yellow(`\n⚠️ ${file} - Choose resolution:`));
487
- console.log(chalk_1.default.gray(' 1. Keep your changes (ours)'));
488
- console.log(chalk_1.default.gray(' 2. Keep their changes (theirs)'));
489
- console.log(chalk_1.default.gray(' 3. Skip (resolve manually later)\n'));
490
- const rlConflict = await Promise.resolve().then(() => __importStar(require('readline')));
491
- const rlChoice = rlConflict.createInterface({
492
- input: process.stdin,
493
- output: process.stdout,
494
- });
495
- const choice = await new Promise((resolve) => {
496
- rlChoice.question(chalk_1.default.cyan('Choice (1/2/3): '), (ans) => {
497
- setImmediate(() => rlChoice.close());
498
- resolve(ans.trim());
499
- });
500
- });
501
- if (choice === '1') {
502
- (0, child_process_1.execSync)(`git checkout --ours ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
503
- (0, child_process_1.execSync)(`git add ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
504
- console.log(chalk_1.default.green(`✓ Kept your changes: ${file}`));
505
- }
506
- else if (choice === '2') {
507
- (0, child_process_1.execSync)(`git checkout --theirs ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
508
- (0, child_process_1.execSync)(`git add ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
509
- console.log(chalk_1.default.green(`✓ Kept their changes: ${file}`));
510
- }
511
- else {
512
- console.log(chalk_1.default.gray(`⊘ Skipped: ${file}`));
513
- return false; // User wants to resolve manually
514
- }
515
- }
516
- }
517
- catch (fileError) {
518
- console.log(chalk_1.default.yellow(`⚠️ Couldn't auto-resolve ${file}`));
519
- // Fallback: ask user
520
- const rlFallback = await Promise.resolve().then(() => __importStar(require('readline')));
521
- const rlFallbackChoice = rlFallback.createInterface({
522
- input: process.stdin,
523
- output: process.stdout,
524
- });
525
- const fallbackChoice = await new Promise((resolve) => {
526
- rlFallbackChoice.question(chalk_1.default.cyan(`Keep (1) yours or (2) theirs? [1/2]: `), (ans) => {
527
- setImmediate(() => rlFallbackChoice.close());
528
- resolve(ans.trim());
529
- });
530
- });
531
- if (fallbackChoice === '1') {
532
- (0, child_process_1.execSync)(`git checkout --ours ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
533
- (0, child_process_1.execSync)(`git add ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
534
- }
535
- else if (fallbackChoice === '2') {
536
- (0, child_process_1.execSync)(`git checkout --theirs ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
537
- (0, child_process_1.execSync)(`git add ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
538
- }
539
- else {
540
- return false;
541
- }
542
- }
543
- }
544
- // Continue the rebase
545
- try {
546
- (0, child_process_1.execSync)('git rebase --continue', { encoding: 'utf-8', stdio: 'pipe' });
547
- return true;
548
- }
549
- catch (continueError) {
550
- // If rebase fails, might need to commit first
551
- try {
552
- (0, child_process_1.execSync)('git commit --no-edit', { encoding: 'utf-8', stdio: 'pipe' });
553
- (0, child_process_1.execSync)('git rebase --continue', { encoding: 'utf-8', stdio: 'pipe' });
554
- return true;
555
- }
556
- catch {
557
- return false;
558
- }
559
- }
560
- }
561
- catch (error) {
562
- console.log(chalk_1.default.red(`❌ Conflict resolution failed: ${error.message}`));
563
- try {
564
- (0, child_process_1.execSync)('git rebase --abort', { encoding: 'utf-8', stdio: 'pipe' });
565
- }
566
- catch { }
567
- return false;
568
- }
569
- }
570
- /**
571
- * Use AI to intelligently resolve merge conflicts
572
- */
573
- async function resolveConflictWithAIDetailed(filename, base, ours, theirs) {
574
- try {
575
- const axios = (await Promise.resolve().then(() => __importStar(require('axios')))).default;
576
- const { getAuthConfig } = await Promise.resolve().then(() => __importStar(require('../lib/auth')));
577
- // Get auth token
578
- const authConfig = getAuthConfig();
579
- if (!authConfig || !authConfig.token) {
580
- console.log(chalk_1.default.yellow('⚠️ Authentication required for AI conflict resolution'));
581
- return null;
582
- }
583
- // Get the backend URL from env or default
584
- const backendUrl = process.env.SNAPCOMMIT_BACKEND_URL || 'https://www.snapcommit.dev';
585
- const response = await axios.post(`${backendUrl}/api/ai/resolve-conflict`, {
586
- filename,
587
- base,
588
- ours,
589
- theirs,
590
- token: authConfig.token,
591
- }, {
592
- headers: {
593
- 'Content-Type': 'application/json',
594
- },
595
- timeout: 30000, // 30 second timeout
596
- });
597
- if (response.data?.resolution) {
598
- return response.data.resolution;
599
- }
600
- return null;
601
- }
602
- catch (error) {
603
- // AI resolution failed - fall back to manual choice
604
- if (error.response?.status === 403) {
605
- console.log(chalk_1.default.red('\n❌ Subscription required for AI conflict resolution'));
606
- console.log(chalk_1.default.cyan('Visit: https://www.snapcommit.dev/pricing\n'));
607
- }
608
- return null;
609
- }
610
- }
611
422
  /**
612
423
  * Execute Git commands - Cursor-style (clean, fast, auto-fix)
613
424
  * NOW SUPPORTS: commits, pushes, pulls, merges, rebases, cherry-picks, stash,
@@ -710,51 +521,7 @@ async function executeGitCommands(commands) {
710
521
  }
711
522
  }
712
523
  if (hasConflicts) {
713
- console.log(chalk_1.default.yellow('\n⚠️ Merge conflicts detected!\n'));
714
- // Try AI auto-resolution first
715
- console.log(chalk_1.default.blue('🤖 Attempting AI conflict resolution...\n'));
716
- const resolved = await tryAdvancedConflictResolution();
717
- if (resolved) {
718
- console.log(chalk_1.default.green('✓ Conflicts resolved automatically!\n'));
719
- // Complete the merge
720
- try {
721
- (0, child_process_1.execSync)('git add -A', { encoding: 'utf-8', stdio: 'pipe' });
722
- (0, child_process_1.execSync)('git commit --no-edit', { encoding: 'utf-8', stdio: 'pipe' });
723
- console.log(chalk_1.default.green('✓ Merge completed\n'));
724
- }
725
- catch {
726
- console.log(chalk_1.default.red('\n❌ Failed to complete merge after resolution\n'));
727
- }
728
- return;
729
- }
730
- // AI couldn't resolve - show manual instructions
731
- console.log(chalk_1.default.yellow('⚠️ AI could not auto-resolve these conflicts\n'));
732
- console.log(chalk_1.default.white('Why? These conflicts require human judgment:'));
733
- console.log(chalk_1.default.gray(' • Business logic decisions (which approach is correct?)'));
734
- console.log(chalk_1.default.gray(' • Architectural changes (incompatible design choices)'));
735
- console.log(chalk_1.default.gray(' • Semantic conflicts (both valid, but different intent)\n'));
736
- console.log(chalk_1.default.white('Conflicted files:'));
737
- try {
738
- const conflicts = (0, child_process_1.execSync)('git diff --name-only --diff-filter=U', { encoding: 'utf-8' });
739
- conflicts.split('\n').filter(f => f.trim()).forEach(file => {
740
- console.log(chalk_1.default.red(` ✗ ${file}`));
741
- });
742
- }
743
- catch {
744
- console.log(chalk_1.default.gray(' (could not list files)\n'));
745
- }
746
- console.log(chalk_1.default.cyan('\n📖 How to resolve manually:\n'));
747
- console.log(chalk_1.default.white(' 1. Open each file above in your editor'));
748
- console.log(chalk_1.default.gray(' Look for conflict markers: ') + chalk_1.default.yellow('<<<<<<< HEAD'));
749
- console.log(chalk_1.default.gray(' ') + chalk_1.default.yellow('======='));
750
- console.log(chalk_1.default.gray(' ') + chalk_1.default.yellow('>>>>>>> branch'));
751
- console.log('');
752
- console.log(chalk_1.default.white(' 2. Choose which code to keep (or merge both)'));
753
- console.log(chalk_1.default.gray(' Remove the conflict markers when done'));
754
- console.log('');
755
- console.log(chalk_1.default.white(' 3. After fixing, in snap type:'));
756
- console.log(chalk_1.default.cyan(' complete the merge\n'));
757
- console.log(chalk_1.default.gray('💡 Tip: Most conflicts are simple! Just pick the better code.\n'));
524
+ await tryAdvancedConflictResolution();
758
525
  return;
759
526
  }
760
527
  // Skip harmless errors
@@ -1536,120 +1303,227 @@ async function tryAutoFix(error, command) {
1536
1303
  * Handle merge conflicts with AI resolution + clear guidance
1537
1304
  */
1538
1305
  async function handleMergeConflict() {
1539
- console.log(chalk_1.default.red('\n❌ Merge conflict detected!\n'));
1306
+ const result = await tryAdvancedConflictResolution();
1307
+ return result;
1308
+ }
1309
+ /**
1310
+ * AI-POWERED conflict resolution - intelligently resolves merge conflicts!
1311
+ */
1312
+ async function tryAdvancedConflictResolution() {
1313
+ const result = await attemptConflictAutoResolution();
1314
+ return displayConflictResolution(result);
1315
+ }
1316
+ async function attemptConflictAutoResolution() {
1317
+ const details = collectConflictDetails();
1318
+ if (details.length === 0) {
1319
+ return { status: 'no-conflicts', details, failedFiles: [] };
1320
+ }
1321
+ const token = (0, auth_1.getToken)();
1322
+ if (!token) {
1323
+ return {
1324
+ status: 'manual',
1325
+ details,
1326
+ failedFiles: details.map((detail) => detail.file),
1327
+ reason: 'missing-token',
1328
+ };
1329
+ }
1330
+ const failedFiles = [];
1331
+ let detectedReason;
1332
+ for (const detail of details) {
1333
+ const { success, reason } = await resolveConflictWithAI(detail, token);
1334
+ if (!success) {
1335
+ failedFiles.push(detail.file);
1336
+ if (!detectedReason && reason) {
1337
+ detectedReason = reason;
1338
+ }
1339
+ }
1340
+ }
1341
+ if (failedFiles.length === 0) {
1342
+ finalizePendingMerge();
1343
+ return { status: 'resolved', details, failedFiles: [] };
1344
+ }
1345
+ return {
1346
+ status: 'manual',
1347
+ details,
1348
+ failedFiles,
1349
+ reason: detectedReason,
1350
+ };
1351
+ }
1352
+ function displayConflictResolution(result) {
1353
+ if (result.status === 'no-conflicts') {
1354
+ return false;
1355
+ }
1356
+ if (result.details.length > 0) {
1357
+ printConflictSummary(result.details);
1358
+ }
1359
+ if (result.status === 'resolved') {
1360
+ console.log(chalk_1.default.green('🤖 AI resolved all merge conflicts!\n'));
1361
+ return true;
1362
+ }
1363
+ printManualResolutionGuide(result.details, result.failedFiles, result.reason);
1364
+ return false;
1365
+ }
1366
+ function collectConflictDetails() {
1367
+ let output = '';
1540
1368
  try {
1541
- // Get list of conflicted files
1542
- const conflictedFilesOutput = (0, child_process_1.execSync)('git diff --name-only --diff-filter=U', {
1369
+ output = (0, child_process_1.execSync)('git diff --name-only --diff-filter=U', {
1543
1370
  encoding: 'utf-8',
1544
- stdio: 'pipe'
1371
+ stdio: 'pipe',
1545
1372
  }).trim();
1546
- if (!conflictedFilesOutput) {
1547
- // No conflicted files found, might be a different error
1548
- return false;
1373
+ }
1374
+ catch {
1375
+ return [];
1376
+ }
1377
+ if (!output) {
1378
+ return [];
1379
+ }
1380
+ const files = output
1381
+ .split('\n')
1382
+ .map((file) => file.trim())
1383
+ .filter(Boolean);
1384
+ return files.map((file) => {
1385
+ let conflictCount = 0;
1386
+ try {
1387
+ const content = fs_1.default.readFileSync(file, 'utf-8');
1388
+ conflictCount = (content.match(/^<<<<<<< /gm) || []).length;
1549
1389
  }
1550
- const conflictedFiles = conflictedFilesOutput.split('\n').filter(f => f.trim());
1551
- console.log(chalk_1.default.yellow('📋 Conflicted files:\n'));
1552
- // Count conflicts per file
1553
- const fileConflicts = [];
1554
- for (const file of conflictedFiles) {
1555
- try {
1556
- const diffOutput = (0, child_process_1.execSync)(`git diff ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
1557
- const conflictCount = (diffOutput.match(/^<<<<<<< HEAD/gm) || []).length;
1558
- fileConflicts.push({ file, count: conflictCount });
1559
- console.log(chalk_1.default.cyan(` • ${file}`) + chalk_1.default.gray(` (${conflictCount} conflict${conflictCount === 1 ? '' : 's'})`));
1390
+ catch {
1391
+ conflictCount = 0;
1392
+ }
1393
+ return {
1394
+ file,
1395
+ conflictCount,
1396
+ base: readStageBlob(':1', file),
1397
+ ours: readStageBlob(':2', file),
1398
+ theirs: readStageBlob(':3', file),
1399
+ };
1400
+ });
1401
+ }
1402
+ function readStageBlob(stage, file) {
1403
+ try {
1404
+ const escaped = escapePathForShell(file);
1405
+ return (0, child_process_1.execSync)(`git show ${stage}:"${escaped}"`, { encoding: 'utf-8', stdio: 'pipe' });
1406
+ }
1407
+ catch {
1408
+ return '';
1409
+ }
1410
+ }
1411
+ function printConflictSummary(details) {
1412
+ if (!details.length) {
1413
+ return;
1414
+ }
1415
+ console.log(chalk_1.default.yellow('⚠️ Merge conflicts detected!\n'));
1416
+ const totalConflicts = details.reduce((sum, detail) => sum + (detail.conflictCount || 0), 0);
1417
+ const suffix = totalConflicts > 0
1418
+ ? chalk_1.default.gray(` (${totalConflicts} conflict${totalConflicts === 1 ? '' : 's'} total)`)
1419
+ : '';
1420
+ console.log(chalk_1.default.red(`❌ Merge conflict${details.length === 1 ? '' : 's'} in ${details.length} file${details.length === 1 ? '' : 's'}`) + suffix);
1421
+ for (const detail of details) {
1422
+ const countText = detail.conflictCount
1423
+ ? chalk_1.default.gray(` (${detail.conflictCount} conflict${detail.conflictCount === 1 ? '' : 's'})`)
1424
+ : '';
1425
+ console.log(chalk_1.default.white(` • ${detail.file}`) + countText);
1426
+ }
1427
+ console.log();
1428
+ }
1429
+ function printManualResolutionGuide(details, failedFiles, reason) {
1430
+ const filesToHighlight = failedFiles.length
1431
+ ? details.filter((detail) => failedFiles.includes(detail.file))
1432
+ : details;
1433
+ if (!filesToHighlight.length) {
1434
+ return;
1435
+ }
1436
+ if (reason === 'missing-token') {
1437
+ console.log(chalk_1.default.red('❌ Authentication required for AI conflict resolution\n'));
1438
+ console.log(chalk_1.default.gray(' Run: ') + chalk_1.default.cyan('snap login') + chalk_1.default.gray(' to reconnect, then retry.\n'));
1439
+ }
1440
+ else if (reason === 'subscription-required') {
1441
+ console.log(chalk_1.default.red('❌ Subscription required for AI conflict resolution\n'));
1442
+ console.log(chalk_1.default.cyan('Visit: https://www.snapcommit.dev/pricing\n'));
1443
+ }
1444
+ else if (reason === 'auth-error') {
1445
+ console.log(chalk_1.default.red('❌ Your session expired for AI conflict resolution\n'));
1446
+ console.log(chalk_1.default.gray(' Run: ') + chalk_1.default.cyan('snap login') + chalk_1.default.gray(' to refresh your credentials.\n'));
1447
+ }
1448
+ console.log(chalk_1.default.yellow('🤖 AI tried to resolve but needs your help!\n'));
1449
+ console.log(chalk_1.default.white.bold('📋 What to do:'));
1450
+ console.log(chalk_1.default.gray(' 1. Open the files above in your editor'));
1451
+ console.log(chalk_1.default.gray(' 2. Look for conflict markers like ') + chalk_1.default.yellow('<<<<<<< HEAD'));
1452
+ console.log(chalk_1.default.gray(' 3. Decide which changes to keep (yours, theirs, or a merge)'));
1453
+ console.log(chalk_1.default.gray(' 4. Remove the markers ') + chalk_1.default.yellow('<<<<<<< ======= >>>>>>>'));
1454
+ console.log(chalk_1.default.gray(' 5. Stage the fixes: ') + chalk_1.default.cyan('git add <file>'));
1455
+ console.log(chalk_1.default.gray(' 6. Back in snap, type: ') + chalk_1.default.cyan('commit the resolved changes'));
1456
+ console.log();
1457
+ console.log(chalk_1.default.white.bold('💡 Helpful commands:'));
1458
+ console.log(chalk_1.default.cyan(' show the conflicts') + chalk_1.default.gray(' – Reprint this summary'));
1459
+ console.log(chalk_1.default.cyan(' abort the merge') + chalk_1.default.gray(' – Cancel and restore previous state'));
1460
+ console.log(chalk_1.default.cyan(' use my version / use their version') + chalk_1.default.gray(' – Quick helpers for specific files\n'));
1461
+ console.log(chalk_1.default.white('Files still needing attention:'));
1462
+ filesToHighlight.forEach((detail) => {
1463
+ const countText = detail.conflictCount
1464
+ ? chalk_1.default.gray(` (${detail.conflictCount} conflict${detail.conflictCount === 1 ? '' : 's'})`)
1465
+ : '';
1466
+ console.log(chalk_1.default.cyan(` • ${detail.file}`) + countText);
1467
+ });
1468
+ console.log();
1469
+ }
1470
+ function finalizePendingMerge() {
1471
+ const commands = ['git rebase --continue', 'git merge --continue', 'git commit --no-edit'];
1472
+ for (const command of commands) {
1473
+ try {
1474
+ (0, child_process_1.execSync)(command, { encoding: 'utf-8', stdio: 'pipe' });
1475
+ return true;
1476
+ }
1477
+ catch {
1478
+ // Try the next strategy
1479
+ }
1480
+ }
1481
+ return false;
1482
+ }
1483
+ async function resolveConflictWithAI(detail, token) {
1484
+ try {
1485
+ const backendUrl = process.env.SNAPCOMMIT_BACKEND_URL || 'https://www.snapcommit.dev';
1486
+ const response = await fetch(`${backendUrl}/api/ai/resolve-conflict`, {
1487
+ method: 'POST',
1488
+ headers: { 'Content-Type': 'application/json' },
1489
+ body: JSON.stringify({
1490
+ filename: detail.file,
1491
+ base: detail.base,
1492
+ ours: detail.ours,
1493
+ theirs: detail.theirs,
1494
+ token,
1495
+ }),
1496
+ signal: AbortSignal.timeout(30000),
1497
+ });
1498
+ if (!response.ok) {
1499
+ if (response.status === 403) {
1500
+ return { success: false, reason: 'subscription-required' };
1560
1501
  }
1561
- catch {
1562
- fileConflicts.push({ file, count: 0 });
1563
- console.log(chalk_1.default.cyan(` • ${file}`));
1502
+ if (response.status === 401) {
1503
+ return { success: false, reason: 'auth-error' };
1564
1504
  }
1505
+ return { success: false, reason: 'api-error' };
1565
1506
  }
1566
- console.log();
1567
- // Try AI resolution
1568
- console.log(chalk_1.default.blue('🤖 Attempting AI resolution...\n'));
1569
- let resolvedCount = 0;
1570
- const failedFiles = [];
1571
- for (const { file, count } of fileConflicts) {
1572
- if (count === 0)
1573
- continue;
1507
+ const data = await response.json().catch(() => null);
1508
+ if (data?.resolution) {
1509
+ fs_1.default.writeFileSync(detail.file, data.resolution, 'utf-8');
1574
1510
  try {
1575
- const resolved = await resolveConflictWithAI(file);
1576
- if (resolved) {
1577
- console.log(chalk_1.default.green(` ✓ Resolved: ${file}`));
1578
- (0, child_process_1.execSync)(`git add ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
1579
- resolvedCount++;
1580
- }
1581
- else {
1582
- console.log(chalk_1.default.yellow(` ⚠️ ${file} needs manual review`));
1583
- failedFiles.push(file);
1584
- }
1511
+ (0, child_process_1.execSync)(`git add -- "${escapePathForShell(detail.file)}"`, { encoding: 'utf-8', stdio: 'pipe' });
1585
1512
  }
1586
1513
  catch {
1587
- console.log(chalk_1.default.yellow(` ⚠️ ${file} needs manual review`));
1588
- failedFiles.push(file);
1514
+ // Ignore staging failures; user can stage manually later
1589
1515
  }
1516
+ return { success: true };
1590
1517
  }
1591
- console.log();
1592
- // If all resolved, commit
1593
- if (failedFiles.length === 0) {
1594
- (0, child_process_1.execSync)('git commit --no-edit', { encoding: 'utf-8', stdio: 'pipe' });
1595
- console.log(chalk_1.default.green('✅ All conflicts resolved automatically!\n'));
1596
- return true;
1597
- }
1598
- // Some failed - show manual instructions
1599
- console.log(chalk_1.default.yellow(`⚠️ ${failedFiles.length} file${failedFiles.length === 1 ? '' : 's'} need${failedFiles.length === 1 ? 's' : ''} manual resolution:\n`));
1600
- failedFiles.forEach(file => {
1601
- console.log(chalk_1.default.cyan(` • ${file}`));
1602
- });
1603
- console.log();
1604
- console.log(chalk_1.default.white.bold('📖 How to resolve manually:\n'));
1605
- console.log(chalk_1.default.gray(' 1. Open the conflicted files in your editor'));
1606
- console.log(chalk_1.default.gray(' 2. Look for ') + chalk_1.default.yellow('<<<<<<< HEAD') + chalk_1.default.gray(' markers'));
1607
- console.log(chalk_1.default.gray(' 3. Choose which version to keep (or merge both)'));
1608
- console.log(chalk_1.default.gray(' 4. Remove the conflict markers ') + chalk_1.default.yellow('(<<<<<<< ======= >>>>>>>)'));
1609
- console.log(chalk_1.default.gray(' 5. Save the files'));
1610
- console.log(chalk_1.default.gray(' 6. Type: ') + chalk_1.default.cyan('"commit the resolved changes"') + chalk_1.default.gray(' or ') + chalk_1.default.cyan('"continue"') + '\n');
1611
- console.log(chalk_1.default.white.bold('💡 Useful commands:\n'));
1612
- console.log(chalk_1.default.cyan(' "show the conflicts"') + chalk_1.default.gray(' - See conflict details again'));
1613
- console.log(chalk_1.default.cyan(' "abort the merge"') + chalk_1.default.gray(' - Cancel and go back'));
1614
- console.log(chalk_1.default.cyan(' "use their version"') + chalk_1.default.gray(' - Accept incoming changes'));
1615
- console.log(chalk_1.default.cyan(' "use my version"') + chalk_1.default.gray(' - Keep your changes\n'));
1616
- return false;
1518
+ return { success: false, reason: 'api-error' };
1617
1519
  }
1618
1520
  catch (error) {
1619
- console.log(chalk_1.default.red('❌ Could not analyze conflicts'));
1620
- console.log(chalk_1.default.gray(` ${error.message}\n`));
1621
- return false;
1521
+ console.log(chalk_1.default.yellow(`⚠️ AI resolution failed: ${error.message}`));
1522
+ return { success: false, reason: 'api-error' };
1622
1523
  }
1623
1524
  }
1624
- /**
1625
- * Resolve conflict with AI (calls Gemini to analyze and resolve)
1626
- */
1627
- async function resolveConflictWithAI(file) {
1628
- try {
1629
- const token = (0, auth_1.getToken)();
1630
- if (!token)
1631
- return false;
1632
- // Get the conflicted content
1633
- const conflictContent = (0, child_process_1.execSync)(`git show :1:${file} 2>/dev/null || echo ""`, { encoding: 'utf-8' }).trim();
1634
- const ourContent = (0, child_process_1.execSync)(`git show :2:${file} 2>/dev/null || echo ""`, { encoding: 'utf-8' }).trim();
1635
- const theirContent = (0, child_process_1.execSync)(`git show :3:${file} 2>/dev/null || echo ""`, { encoding: 'utf-8' }).trim();
1636
- if (!ourContent && !theirContent)
1637
- return false;
1638
- // Call AI to resolve (simplified - real implementation would call Gemini API)
1639
- // For now, just try simple auto-resolution
1640
- const diffOutput = (0, child_process_1.execSync)(`git diff ${file}`, { encoding: 'utf-8' });
1641
- // If conflict is simple (just additions, no deletions), can auto-resolve
1642
- const hasDeleteConflict = diffOutput.includes('-') && diffOutput.includes('+');
1643
- if (!hasDeleteConflict) {
1644
- // Simple conflict - keep both
1645
- (0, child_process_1.execSync)(`git checkout --ours ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
1646
- return true;
1647
- }
1648
- return false;
1649
- }
1650
- catch {
1651
- return false;
1652
- }
1525
+ function escapePathForShell(value) {
1526
+ return value.replace(/(["\\$`])/g, '\\$1');
1653
1527
  }
1654
1528
  /**
1655
1529
  * Handle force push with safety confirmation
@@ -16,7 +16,7 @@ const git_1 = require("../utils/git");
16
16
  * Main natural language command handler - Cursor-style!
17
17
  * Now everything works like Cursor - just say what you want!
18
18
  */
19
- async function naturalCommand(userInput) {
19
+ async function naturalCommand(userInput, prompt) {
20
20
  // Ensure authentication first
21
21
  const authConfig = await (0, auth_1.ensureAuth)();
22
22
  if (!authConfig) {
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.telemetryStatusCommand = telemetryStatusCommand;
7
+ exports.telemetryEnableCommand = telemetryEnableCommand;
8
+ exports.telemetryDisableCommand = telemetryDisableCommand;
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const settings_1 = require("../utils/settings");
11
+ const telemetry_1 = require("../utils/telemetry");
12
+ async function telemetryStatusCommand() {
13
+ const settings = (0, settings_1.getSettings)();
14
+ console.log(chalk_1.default.bold('\nTelemetry Status:\n'));
15
+ console.log(chalk_1.default.white(` Enabled: ${(0, telemetry_1.isTelemetryEnabled)() ? chalk_1.default.green('yes') : chalk_1.default.red('no')}`));
16
+ if (settings.telemetryPromptedAt) {
17
+ const prompted = new Date(settings.telemetryPromptedAt).toLocaleString();
18
+ console.log(chalk_1.default.gray(` Last prompted: ${prompted}`));
19
+ }
20
+ else {
21
+ console.log(chalk_1.default.gray(' Last prompted: never'));
22
+ }
23
+ console.log('\nPrivacy: Data is anonymous diagnostics and usage patterns only. No repo contents or secrets.\n');
24
+ }
25
+ async function telemetryEnableCommand() {
26
+ if ((0, telemetry_1.isTelemetryEnabled)()) {
27
+ console.log(chalk_1.default.green('\nTelemetry is already enabled. Thank you! 🙌\n'));
28
+ return;
29
+ }
30
+ (0, telemetry_1.setTelemetryEnabled)(true);
31
+ (0, telemetry_1.markTelemetryPrompted)();
32
+ (0, telemetry_1.recordTelemetry)('telemetry_enabled');
33
+ console.log(chalk_1.default.green('\n✅ Telemetry enabled. Thanks for helping us make SnapCommit smarter.\n'));
34
+ }
35
+ async function telemetryDisableCommand() {
36
+ if (!(0, telemetry_1.isTelemetryEnabled)()) {
37
+ console.log(chalk_1.default.yellow('\nTelemetry is already disabled.\n'));
38
+ return;
39
+ }
40
+ (0, telemetry_1.setTelemetryEnabled)(false);
41
+ (0, settings_1.updateSettings)({ telemetryPromptedAt: Date.now() });
42
+ (0, telemetry_1.recordTelemetry)('telemetry_disabled');
43
+ console.log(chalk_1.default.green('\n✅ Telemetry disabled. You can re-enable anytime with `snap telemetry enable`.\n'));
44
+ }
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ const natural_1 = require("./commands/natural");
22
22
  const conflict_1 = require("./commands/conflict");
23
23
  const uninstall_1 = require("./commands/uninstall");
24
24
  const github_connect_1 = require("./commands/github-connect");
25
+ const telemetry_1 = require("./commands/telemetry");
25
26
  const repl_1 = require("./repl");
26
27
  // Load environment variables from root .env
27
28
  (0, dotenv_1.config)({ path: path_1.default.join(__dirname, '../../.env') });
@@ -149,6 +150,28 @@ githubCmd
149
150
  .action(async () => {
150
151
  await (0, github_connect_1.githubDisconnectCommand)();
151
152
  });
153
+ // Telemetry management
154
+ const telemetryCmd = program
155
+ .command('telemetry')
156
+ .description('Manage anonymous telemetry');
157
+ telemetryCmd
158
+ .command('enable')
159
+ .description('Enable anonymous telemetry diagnostics')
160
+ .action(async () => {
161
+ await (0, telemetry_1.telemetryEnableCommand)();
162
+ });
163
+ telemetryCmd
164
+ .command('disable')
165
+ .description('Disable anonymous telemetry diagnostics')
166
+ .action(async () => {
167
+ await (0, telemetry_1.telemetryDisableCommand)();
168
+ });
169
+ telemetryCmd
170
+ .command('status')
171
+ .description('Show telemetry status')
172
+ .action(async () => {
173
+ await (0, telemetry_1.telemetryStatusCommand)();
174
+ });
152
175
  // Command: snapcommit uninstall
153
176
  program
154
177
  .command('uninstall')
package/dist/repl.js CHANGED
@@ -101,6 +101,52 @@ async function startREPL() {
101
101
  }
102
102
  console.log(chalk_1.default.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
103
103
  console.log(chalk_1.default.bold.yellow('🎯 Try it now! Type what you want to do:\n'));
104
+ const promptState = {
105
+ resolver: null,
106
+ };
107
+ const promptController = {
108
+ ask: (question, options = {}) => new Promise((resolve) => {
109
+ if (promptState.resolver) {
110
+ console.log(chalk_1.default.red('\n❌ Internal prompt error: previous question still active.\n'));
111
+ resolve(options.defaultValue ?? '');
112
+ return;
113
+ }
114
+ const { defaultValue, suffix } = options;
115
+ const segments = [`\n${question}`];
116
+ if (suffix)
117
+ segments.push(` ${suffix}`);
118
+ if (defaultValue !== undefined) {
119
+ segments.push(` ${chalk_1.default.gray(`[default: ${defaultValue}]`)}`);
120
+ }
121
+ process.stdout.write(segments.join('') + '\n' + chalk_1.default.cyan('› '));
122
+ promptState.resolver = (value) => {
123
+ const trimmed = value.trim();
124
+ if (defaultValue !== undefined && trimmed === '') {
125
+ resolve(defaultValue);
126
+ }
127
+ else {
128
+ resolve(trimmed);
129
+ }
130
+ };
131
+ }),
132
+ confirm: async (question, options = {}) => {
133
+ const defaultYes = options.defaultValue ?? false;
134
+ const hint = defaultYes ? chalk_1.default.gray('[Y/n]') : chalk_1.default.gray('[y/N]');
135
+ while (true) {
136
+ const response = (await promptController.ask(`${question} ${hint}`)).toLowerCase();
137
+ if (!response) {
138
+ return defaultYes;
139
+ }
140
+ if (response === 'y' || response === 'yes') {
141
+ return true;
142
+ }
143
+ if (response === 'n' || response === 'no') {
144
+ return false;
145
+ }
146
+ console.log(chalk_1.default.yellow('Please answer with yes or no.'));
147
+ }
148
+ },
149
+ };
104
150
  const rl = readline_1.default.createInterface({
105
151
  input: process.stdin,
106
152
  output: process.stdout,
@@ -108,6 +154,12 @@ async function startREPL() {
108
154
  });
109
155
  rl.prompt();
110
156
  rl.on('line', async (input) => {
157
+ if (promptState.resolver) {
158
+ const resolver = promptState.resolver;
159
+ promptState.resolver = null;
160
+ resolver(input);
161
+ return;
162
+ }
111
163
  const line = input.trim();
112
164
  if (!line) {
113
165
  rl.prompt();
@@ -315,13 +367,10 @@ async function startREPL() {
315
367
  }
316
368
  // Everything else is natural language
317
369
  try {
318
- rl.pause(); // Pause REPL readline to avoid conflicts
319
- await (0, natural_1.naturalCommand)(line);
320
- rl.resume(); // Resume REPL readline
370
+ await (0, natural_1.naturalCommand)(line, promptController);
321
371
  }
322
372
  catch (error) {
323
373
  console.log(chalk_1.default.red(`\n❌ Error: ${error.message}\n`));
324
- rl.resume(); // Make sure to resume even on error
325
374
  }
326
375
  rl.prompt();
327
376
  });
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createPromptHelpers = createPromptHelpers;
7
+ const readline_1 = __importDefault(require("readline"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ function createPromptHelpers(prompt) {
10
+ return {
11
+ ask: (question, options) => askWithFallback(question, options, prompt),
12
+ confirm: (question, options) => confirmWithFallback(question, options, prompt),
13
+ };
14
+ }
15
+ async function askWithFallback(question, options = {}, prompt) {
16
+ if (prompt) {
17
+ return prompt.ask(question, options);
18
+ }
19
+ const rl = readline_1.default.createInterface({
20
+ input: process.stdin,
21
+ output: process.stdout,
22
+ });
23
+ const { defaultValue, suffix } = options;
24
+ const segments = [question];
25
+ if (suffix)
26
+ segments.push(` ${suffix}`);
27
+ if (defaultValue !== undefined) {
28
+ segments.push(` ${chalk_1.default.gray(`[default: ${defaultValue}]`)}`);
29
+ }
30
+ return await new Promise((resolve) => {
31
+ rl.question(`${segments.join('')}` + '\n› ', (answer) => {
32
+ rl.close();
33
+ const trimmed = answer.trim();
34
+ if (defaultValue !== undefined && trimmed === '') {
35
+ resolve(defaultValue);
36
+ }
37
+ else {
38
+ resolve(trimmed);
39
+ }
40
+ });
41
+ });
42
+ }
43
+ async function confirmWithFallback(question, options = {}, prompt) {
44
+ if (prompt) {
45
+ return prompt.confirm(question, options);
46
+ }
47
+ const defaultYes = options.defaultValue ?? false;
48
+ const hint = defaultYes ? '[Y/n]' : '[y/N]';
49
+ while (true) {
50
+ const answer = (await askWithFallback(`${question} ${hint}`, {}, undefined)).toLowerCase();
51
+ if (!answer) {
52
+ return defaultYes;
53
+ }
54
+ if (answer === 'y' || answer === 'yes') {
55
+ return true;
56
+ }
57
+ if (answer === 'n' || answer === 'no') {
58
+ return false;
59
+ }
60
+ console.log(chalk_1.default.yellow('Please answer with yes or no.'));
61
+ }
62
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getSettings = getSettings;
7
+ exports.saveSettings = saveSettings;
8
+ exports.updateSettings = updateSettings;
9
+ exports.getOrCreateDeviceId = getOrCreateDeviceId;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const os_1 = __importDefault(require("os"));
13
+ const crypto_1 = __importDefault(require("crypto"));
14
+ const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.snapcommit');
15
+ const SETTINGS_FILE = path_1.default.join(CONFIG_DIR, 'settings.json');
16
+ const defaultSettings = {
17
+ telemetryEnabled: false,
18
+ telemetryPromptedAt: 0,
19
+ };
20
+ function ensureConfigDir() {
21
+ if (!fs_1.default.existsSync(CONFIG_DIR)) {
22
+ fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true });
23
+ }
24
+ }
25
+ function getSettings() {
26
+ try {
27
+ ensureConfigDir();
28
+ if (!fs_1.default.existsSync(SETTINGS_FILE)) {
29
+ return { ...defaultSettings };
30
+ }
31
+ const raw = fs_1.default.readFileSync(SETTINGS_FILE, 'utf-8');
32
+ const parsed = JSON.parse(raw);
33
+ return { ...defaultSettings, ...parsed };
34
+ }
35
+ catch {
36
+ return { ...defaultSettings };
37
+ }
38
+ }
39
+ function saveSettings(settings) {
40
+ ensureConfigDir();
41
+ const merged = { ...defaultSettings, ...settings };
42
+ fs_1.default.writeFileSync(SETTINGS_FILE, JSON.stringify(merged, null, 2));
43
+ return merged;
44
+ }
45
+ function updateSettings(partial) {
46
+ const current = getSettings();
47
+ const merged = { ...current, ...partial };
48
+ return saveSettings(merged);
49
+ }
50
+ function getOrCreateDeviceId() {
51
+ const settings = getSettings();
52
+ if (settings.deviceId) {
53
+ return settings.deviceId;
54
+ }
55
+ const deviceId = crypto_1.default.randomUUID();
56
+ updateSettings({ deviceId });
57
+ return deviceId;
58
+ }
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isTelemetryEnabled = isTelemetryEnabled;
7
+ exports.setTelemetryEnabled = setTelemetryEnabled;
8
+ exports.markTelemetryPrompted = markTelemetryPrompted;
9
+ exports.recordTelemetry = recordTelemetry;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const os_1 = __importDefault(require("os"));
13
+ const settings_1 = require("./settings");
14
+ const LOG_DIR = path_1.default.join(os_1.default.homedir(), '.snapcommit', 'logs');
15
+ const LOG_FILE = path_1.default.join(LOG_DIR, 'telemetry.log');
16
+ const TELEMETRY_ENDPOINT = process.env.SNAPCOMMIT_TELEMETRY_URL || 'https://www.snapcommit.dev/api/telemetry';
17
+ let packageVersion = '0.0.0';
18
+ try {
19
+ const pkgPath = path_1.default.join(__dirname, '../../package.json');
20
+ const raw = fs_1.default.readFileSync(pkgPath, 'utf-8');
21
+ const parsed = JSON.parse(raw);
22
+ packageVersion = parsed.version || packageVersion;
23
+ }
24
+ catch {
25
+ // ignore
26
+ }
27
+ function ensureLogDir() {
28
+ if (!fs_1.default.existsSync(LOG_DIR)) {
29
+ fs_1.default.mkdirSync(LOG_DIR, { recursive: true });
30
+ }
31
+ }
32
+ function isTelemetryEnabled() {
33
+ return Boolean((0, settings_1.getSettings)().telemetryEnabled);
34
+ }
35
+ function setTelemetryEnabled(enabled) {
36
+ (0, settings_1.updateSettings)({ telemetryEnabled: enabled });
37
+ }
38
+ function markTelemetryPrompted() {
39
+ (0, settings_1.updateSettings)({ telemetryPromptedAt: Date.now() });
40
+ }
41
+ function appendLocalLog(entry) {
42
+ try {
43
+ ensureLogDir();
44
+ fs_1.default.appendFileSync(LOG_FILE, JSON.stringify(entry) + '\n');
45
+ }
46
+ catch {
47
+ // ignore logging failures
48
+ }
49
+ }
50
+ function recordTelemetry(event, payload = {}) {
51
+ if (!isTelemetryEnabled()) {
52
+ return;
53
+ }
54
+ const timestamp = new Date().toISOString();
55
+ const deviceId = (0, settings_1.getOrCreateDeviceId)();
56
+ const body = {
57
+ event,
58
+ payload,
59
+ timestamp,
60
+ version: packageVersion,
61
+ deviceId,
62
+ };
63
+ // Fire and forget
64
+ void (async () => {
65
+ try {
66
+ const response = await fetch(TELEMETRY_ENDPOINT, {
67
+ method: 'POST',
68
+ headers: {
69
+ 'Content-Type': 'application/json',
70
+ },
71
+ body: JSON.stringify(body),
72
+ });
73
+ if (!response.ok) {
74
+ appendLocalLog({ ...body, status: response.status, error: await response.text().catch(() => undefined) });
75
+ }
76
+ }
77
+ catch (error) {
78
+ appendLocalLog({ ...body, error: error?.message || 'network-error' });
79
+ }
80
+ })();
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snapcommit/cli",
3
- "version": "3.9.20",
3
+ "version": "3.10.0",
4
4
  "description": "Instant AI commits. Beautiful progress tracking. Never write commit messages again.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {