@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.
- package/dist/commands/cursor-style.js +208 -334
- package/dist/commands/natural.js +1 -1
- package/dist/commands/telemetry.js +44 -0
- package/dist/index.js +23 -0
- package/dist/repl.js +53 -4
- package/dist/types/prompt.js +2 -0
- package/dist/utils/prompt-helpers.js +62 -0
- package/dist/utils/settings.js +58 -0
- package/dist/utils/telemetry.js +81 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
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
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
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
|
-
|
|
1562
|
-
|
|
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
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1588
|
-
failedFiles.push(file);
|
|
1514
|
+
// Ignore staging failures; user can stage manually later
|
|
1589
1515
|
}
|
|
1516
|
+
return { success: true };
|
|
1590
1517
|
}
|
|
1591
|
-
|
|
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.
|
|
1620
|
-
|
|
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
|
-
|
|
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
|
package/dist/commands/natural.js
CHANGED
|
@@ -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
|
-
|
|
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,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
|
+
}
|