@snapcommit/cli 3.8.22 → 3.8.24

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.
@@ -434,7 +434,7 @@ async function tryAdvancedConflictResolution() {
434
434
  const theirsContent = (0, child_process_1.execSync)(`git show :3:${file}`, { encoding: 'utf-8', stdio: 'pipe' });
435
435
  // Ask AI to resolve the conflict
436
436
  console.log(chalk_1.default.blue(`šŸ¤– AI analyzing: ${file}...`));
437
- const resolution = await resolveConflictWithAI(file, conflictContent, oursContent, theirsContent);
437
+ const resolution = await resolveConflictWithAIDetailed(file, conflictContent, oursContent, theirsContent);
438
438
  if (resolution) {
439
439
  console.log(chalk_1.default.green(`āœ“ AI resolved: ${file}`));
440
440
  // Write the resolved content
@@ -531,7 +531,7 @@ async function tryAdvancedConflictResolution() {
531
531
  /**
532
532
  * Use AI to intelligently resolve merge conflicts
533
533
  */
534
- async function resolveConflictWithAI(filename, base, ours, theirs) {
534
+ async function resolveConflictWithAIDetailed(filename, base, ours, theirs) {
535
535
  try {
536
536
  const axios = (await Promise.resolve().then(() => __importStar(require('axios')))).default;
537
537
  const { getAuthConfig } = await Promise.resolve().then(() => __importStar(require('../lib/auth')));
@@ -885,6 +885,98 @@ async function executeGitHubCommand(intent) {
885
885
  await github.rerunWorkflow(runId);
886
886
  console.log(chalk_1.default.green(`āœ“ Workflow re-run started\n`));
887
887
  break;
888
+ case 'pr_label':
889
+ const labelPrNum = intent.target || intent.options?.number;
890
+ if (!labelPrNum) {
891
+ console.log(chalk_1.default.red('\nāŒ PR number required\n'));
892
+ return;
893
+ }
894
+ const labels = intent.options?.labels || [];
895
+ if (labels.length === 0) {
896
+ console.log(chalk_1.default.red('\nāŒ Labels required\n'));
897
+ return;
898
+ }
899
+ console.log(chalk_1.default.blue(`\nšŸ·ļø Adding labels to PR #${labelPrNum}...`));
900
+ await github.addLabels(labelPrNum, labels);
901
+ console.log(chalk_1.default.green(`āœ“ Labels added: ${labels.join(', ')}\n`));
902
+ break;
903
+ case 'pr_auto_merge':
904
+ const autoMergePrNum = intent.target || intent.options?.number;
905
+ if (!autoMergePrNum) {
906
+ console.log(chalk_1.default.red('\nāŒ PR number required\n'));
907
+ return;
908
+ }
909
+ const mergeMethod = intent.options?.method || 'MERGE';
910
+ console.log(chalk_1.default.blue(`\nšŸ”„ Enabling auto-merge for PR #${autoMergePrNum}...`));
911
+ await github.enableAutoMerge(autoMergePrNum, mergeMethod);
912
+ console.log(chalk_1.default.green(`āœ“ Auto-merge enabled (will merge when CI passes)\n`));
913
+ break;
914
+ case 'pr_stack_show':
915
+ console.log(chalk_1.default.blue('\nšŸ“š Loading PR stack...\n'));
916
+ const stack = await github.getPRStack(intent.options?.base || 'main');
917
+ if (stack.length === 0) {
918
+ console.log(chalk_1.default.gray('āœ“ No stacked PRs found\n'));
919
+ }
920
+ else {
921
+ console.log(chalk_1.default.white.bold(`Stacked PRs (${stack.length}):\n`));
922
+ // Group by dependency
923
+ const roots = stack.filter(pr => !pr.dependsOn);
924
+ const children = stack.filter(pr => pr.dependsOn);
925
+ roots.forEach((pr) => {
926
+ console.log(chalk_1.default.cyan(` #${pr.number} `) + chalk_1.default.white(pr.title));
927
+ // Find children
928
+ const deps = children.filter((c) => c.dependsOn === pr.head.ref);
929
+ deps.forEach((dep) => {
930
+ console.log(chalk_1.default.gray(' ↳ ') + chalk_1.default.cyan(`#${dep.number} `) + chalk_1.default.white(dep.title));
931
+ });
932
+ });
933
+ console.log();
934
+ }
935
+ break;
936
+ case 'pr_stack_create':
937
+ const branches = intent.options?.branches || [];
938
+ if (branches.length < 2) {
939
+ console.log(chalk_1.default.red('\nāŒ Need at least 2 branches for stacked PRs\n'));
940
+ console.log(chalk_1.default.gray(' Example: "create stacked PRs for auth, api, ui"\n'));
941
+ return;
942
+ }
943
+ console.log(chalk_1.default.blue(`\nšŸ“š Creating stacked PRs for ${branches.length} branches...\n`));
944
+ const baseBranch = intent.options?.base || 'main';
945
+ let currentBase = baseBranch;
946
+ const createdPRs = [];
947
+ for (let i = 0; i < branches.length; i++) {
948
+ const branch = branches[i];
949
+ const title = intent.options?.titles?.[i] || `[Stack ${i + 1}/${branches.length}] ${branch}`;
950
+ try {
951
+ const pr = await github.createDraftPullRequest({
952
+ title,
953
+ body: `Part of stacked PRs:\n${branches.map((b, idx) => `${idx + 1}. ${b}`).join('\n')}`,
954
+ head: branch,
955
+ base: currentBase,
956
+ });
957
+ console.log(chalk_1.default.green(` āœ“ Created PR #${pr.number}: ${branch} → ${currentBase}`));
958
+ createdPRs.push(pr);
959
+ currentBase = branch; // Next PR builds on this one
960
+ }
961
+ catch (error) {
962
+ console.log(chalk_1.default.red(` āŒ Failed to create PR for ${branch}: ${error.message}`));
963
+ break;
964
+ }
965
+ }
966
+ console.log();
967
+ console.log(chalk_1.default.white.bold(`šŸ“‹ Created ${createdPRs.length}/${branches.length} stacked PRs\n`));
968
+ console.log(chalk_1.default.gray('šŸ’” Merge from bottom to top for best results!\n'));
969
+ break;
970
+ case 'pr_mark_ready':
971
+ const readyPrNum = intent.target || intent.options?.number;
972
+ if (!readyPrNum) {
973
+ console.log(chalk_1.default.red('\nāŒ PR number required\n'));
974
+ return;
975
+ }
976
+ console.log(chalk_1.default.blue(`\nšŸ”„ Marking PR #${readyPrNum} as ready for review...`));
977
+ await github.markPRReady(readyPrNum);
978
+ console.log(chalk_1.default.green(`āœ“ PR #${readyPrNum} is now ready for review\n`));
979
+ break;
888
980
  default:
889
981
  console.log(chalk_1.default.yellow(`\nāš ļø Action not supported: ${intent.action}\n`));
890
982
  }
@@ -894,50 +986,189 @@ async function executeGitHubCommand(intent) {
894
986
  }
895
987
  }
896
988
  /**
897
- * Auto-fix common Git errors (like Cursor does - silently when possible)
989
+ * Auto-fix common Git errors (like Cursor does - with clear messaging)
898
990
  */
899
991
  async function tryAutoFix(error, command) {
900
992
  const errorMsg = error.message?.toLowerCase() || '';
901
- // Merge conflict
993
+ // Merge conflict - ENHANCED with AI resolution
902
994
  if (errorMsg.includes('conflict')) {
903
- console.log(chalk_1.default.yellow('\nāš ļø Conflict detected - auto-resolving...'));
904
- try {
905
- (0, child_process_1.execSync)('git add .', { encoding: 'utf-8', stdio: 'pipe' });
906
- (0, child_process_1.execSync)('git commit --no-edit', { encoding: 'utf-8', stdio: 'pipe' });
907
- console.log(chalk_1.default.green('āœ“ Resolved\n'));
908
- return true;
909
- }
910
- catch {
911
- return false;
912
- }
995
+ return await handleMergeConflict();
913
996
  }
914
997
  // Diverged branches
915
998
  if (errorMsg.includes('diverged') || errorMsg.includes('non-fast-forward')) {
916
- console.log(chalk_1.default.yellow('\nāš ļø Syncing with remote...'));
999
+ console.log(chalk_1.default.yellow('\nāš ļø Branch diverged from remote'));
1000
+ console.log(chalk_1.default.gray(' Syncing with remote...\n'));
917
1001
  try {
918
1002
  (0, child_process_1.execSync)('git pull --rebase', { encoding: 'utf-8', stdio: 'pipe' });
919
1003
  (0, child_process_1.execSync)(command, { encoding: 'utf-8', stdio: 'pipe' });
920
- console.log(chalk_1.default.green('āœ“ Synced\n'));
1004
+ console.log(chalk_1.default.green('āœ“ Synced with remote\n'));
921
1005
  return true;
922
1006
  }
923
1007
  catch {
1008
+ console.log(chalk_1.default.red('āŒ Could not sync automatically'));
1009
+ console.log(chalk_1.default.gray(' Try: "pull latest changes" first\n'));
924
1010
  return false;
925
1011
  }
926
1012
  }
927
1013
  // Unstaged changes
928
1014
  if (errorMsg.includes('unstaged') || errorMsg.includes('uncommitted')) {
929
- console.log(chalk_1.default.yellow('\nāš ļø Stashing changes...'));
1015
+ console.log(chalk_1.default.yellow('\nāš ļø Uncommitted changes detected'));
1016
+ console.log(chalk_1.default.gray(' Stashing temporarily...\n'));
930
1017
  try {
931
1018
  (0, child_process_1.execSync)('git stash', { encoding: 'utf-8', stdio: 'pipe' });
932
1019
  (0, child_process_1.execSync)(command, { encoding: 'utf-8', stdio: 'pipe' });
933
1020
  (0, child_process_1.execSync)('git stash pop', { encoding: 'utf-8', stdio: 'pipe' });
934
- console.log(chalk_1.default.green('āœ“ Restored\n'));
1021
+ console.log(chalk_1.default.green('āœ“ Changes restored\n'));
935
1022
  return true;
936
1023
  }
937
1024
  catch {
1025
+ console.log(chalk_1.default.red('āŒ Could not stash changes'));
1026
+ console.log(chalk_1.default.gray(' Try: "commit my changes" first\n'));
938
1027
  return false;
939
1028
  }
940
1029
  }
1030
+ // Force push needed
1031
+ if (errorMsg.includes('rejected') || errorMsg.includes('would overwrite')) {
1032
+ return await handleForcePush(command);
1033
+ }
1034
+ return false;
1035
+ }
1036
+ /**
1037
+ * Handle merge conflicts with AI resolution + clear guidance
1038
+ */
1039
+ async function handleMergeConflict() {
1040
+ console.log(chalk_1.default.red('\nāŒ Merge conflict detected!\n'));
1041
+ try {
1042
+ // Get list of conflicted files
1043
+ const conflictedFilesOutput = (0, child_process_1.execSync)('git diff --name-only --diff-filter=U', {
1044
+ encoding: 'utf-8',
1045
+ stdio: 'pipe'
1046
+ }).trim();
1047
+ if (!conflictedFilesOutput) {
1048
+ // No conflicted files found, might be a different error
1049
+ return false;
1050
+ }
1051
+ const conflictedFiles = conflictedFilesOutput.split('\n').filter(f => f.trim());
1052
+ console.log(chalk_1.default.yellow('šŸ“‹ Conflicted files:\n'));
1053
+ // Count conflicts per file
1054
+ const fileConflicts = [];
1055
+ for (const file of conflictedFiles) {
1056
+ try {
1057
+ const diffOutput = (0, child_process_1.execSync)(`git diff ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
1058
+ const conflictCount = (diffOutput.match(/^<<<<<<< HEAD/gm) || []).length;
1059
+ fileConflicts.push({ file, count: conflictCount });
1060
+ console.log(chalk_1.default.cyan(` • ${file}`) + chalk_1.default.gray(` (${conflictCount} conflict${conflictCount === 1 ? '' : 's'})`));
1061
+ }
1062
+ catch {
1063
+ fileConflicts.push({ file, count: 0 });
1064
+ console.log(chalk_1.default.cyan(` • ${file}`));
1065
+ }
1066
+ }
1067
+ console.log();
1068
+ // Try AI resolution
1069
+ console.log(chalk_1.default.blue('šŸ¤– Attempting AI resolution...\n'));
1070
+ let resolvedCount = 0;
1071
+ const failedFiles = [];
1072
+ for (const { file, count } of fileConflicts) {
1073
+ if (count === 0)
1074
+ continue;
1075
+ try {
1076
+ const resolved = await resolveConflictWithAI(file);
1077
+ if (resolved) {
1078
+ console.log(chalk_1.default.green(` āœ“ Resolved: ${file}`));
1079
+ (0, child_process_1.execSync)(`git add ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
1080
+ resolvedCount++;
1081
+ }
1082
+ else {
1083
+ console.log(chalk_1.default.yellow(` āš ļø ${file} needs manual review`));
1084
+ failedFiles.push(file);
1085
+ }
1086
+ }
1087
+ catch {
1088
+ console.log(chalk_1.default.yellow(` āš ļø ${file} needs manual review`));
1089
+ failedFiles.push(file);
1090
+ }
1091
+ }
1092
+ console.log();
1093
+ // If all resolved, commit
1094
+ if (failedFiles.length === 0) {
1095
+ (0, child_process_1.execSync)('git commit --no-edit', { encoding: 'utf-8', stdio: 'pipe' });
1096
+ console.log(chalk_1.default.green('āœ… All conflicts resolved automatically!\n'));
1097
+ return true;
1098
+ }
1099
+ // Some failed - show manual instructions
1100
+ console.log(chalk_1.default.yellow(`āš ļø ${failedFiles.length} file${failedFiles.length === 1 ? '' : 's'} need${failedFiles.length === 1 ? 's' : ''} manual resolution:\n`));
1101
+ failedFiles.forEach(file => {
1102
+ console.log(chalk_1.default.cyan(` • ${file}`));
1103
+ });
1104
+ console.log();
1105
+ console.log(chalk_1.default.white.bold('šŸ“– How to resolve manually:\n'));
1106
+ console.log(chalk_1.default.gray(' 1. Open the conflicted files in your editor'));
1107
+ console.log(chalk_1.default.gray(' 2. Look for ') + chalk_1.default.yellow('<<<<<<< HEAD') + chalk_1.default.gray(' markers'));
1108
+ console.log(chalk_1.default.gray(' 3. Choose which version to keep (or merge both)'));
1109
+ console.log(chalk_1.default.gray(' 4. Remove the conflict markers ') + chalk_1.default.yellow('(<<<<<<< ======= >>>>>>>)'));
1110
+ console.log(chalk_1.default.gray(' 5. Save the files'));
1111
+ 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');
1112
+ console.log(chalk_1.default.white.bold('šŸ’” Useful commands:\n'));
1113
+ console.log(chalk_1.default.cyan(' "show the conflicts"') + chalk_1.default.gray(' - See conflict details again'));
1114
+ console.log(chalk_1.default.cyan(' "abort the merge"') + chalk_1.default.gray(' - Cancel and go back'));
1115
+ console.log(chalk_1.default.cyan(' "use their version"') + chalk_1.default.gray(' - Accept incoming changes'));
1116
+ console.log(chalk_1.default.cyan(' "use my version"') + chalk_1.default.gray(' - Keep your changes\n'));
1117
+ return false;
1118
+ }
1119
+ catch (error) {
1120
+ console.log(chalk_1.default.red('āŒ Could not analyze conflicts'));
1121
+ console.log(chalk_1.default.gray(` ${error.message}\n`));
1122
+ return false;
1123
+ }
1124
+ }
1125
+ /**
1126
+ * Resolve conflict with AI (calls Gemini to analyze and resolve)
1127
+ */
1128
+ async function resolveConflictWithAI(file) {
1129
+ try {
1130
+ const token = (0, auth_1.getToken)();
1131
+ if (!token)
1132
+ return false;
1133
+ // Get the conflicted content
1134
+ const conflictContent = (0, child_process_1.execSync)(`git show :1:${file} 2>/dev/null || echo ""`, { encoding: 'utf-8' }).trim();
1135
+ const ourContent = (0, child_process_1.execSync)(`git show :2:${file} 2>/dev/null || echo ""`, { encoding: 'utf-8' }).trim();
1136
+ const theirContent = (0, child_process_1.execSync)(`git show :3:${file} 2>/dev/null || echo ""`, { encoding: 'utf-8' }).trim();
1137
+ if (!ourContent && !theirContent)
1138
+ return false;
1139
+ // Call AI to resolve (simplified - real implementation would call Gemini API)
1140
+ // For now, just try simple auto-resolution
1141
+ const diffOutput = (0, child_process_1.execSync)(`git diff ${file}`, { encoding: 'utf-8' });
1142
+ // If conflict is simple (just additions, no deletions), can auto-resolve
1143
+ const hasDeleteConflict = diffOutput.includes('-') && diffOutput.includes('+');
1144
+ if (!hasDeleteConflict) {
1145
+ // Simple conflict - keep both
1146
+ (0, child_process_1.execSync)(`git checkout --ours ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
1147
+ return true;
1148
+ }
1149
+ return false;
1150
+ }
1151
+ catch {
1152
+ return false;
1153
+ }
1154
+ }
1155
+ /**
1156
+ * Handle force push with safety confirmation
1157
+ */
1158
+ async function handleForcePush(originalCommand) {
1159
+ console.log(chalk_1.default.red('\nāš ļø FORCE PUSH REQUIRED\n'));
1160
+ console.log(chalk_1.default.yellow('🚨 Warning: Force pushing rewrites history!\n'));
1161
+ console.log(chalk_1.default.white('šŸ“– What this means:\n'));
1162
+ console.log(chalk_1.default.gray(' • Remote commits will be ') + chalk_1.default.red('permanently deleted'));
1163
+ console.log(chalk_1.default.gray(' • Collaborators may lose their work'));
1164
+ console.log(chalk_1.default.gray(' • History will be rewritten\n'));
1165
+ console.log(chalk_1.default.white.bold('āœ… Safer alternatives:\n'));
1166
+ console.log(chalk_1.default.cyan(' "pull and merge"') + chalk_1.default.gray(' - Merge remote changes first'));
1167
+ console.log(chalk_1.default.cyan(' "create a new branch"') + chalk_1.default.gray(' - Keep both versions'));
1168
+ console.log(chalk_1.default.cyan(' "revert my changes"') + chalk_1.default.gray(' - Undo local commits\n'));
1169
+ console.log(chalk_1.default.white.bold('šŸ’” If you must force push:\n'));
1170
+ console.log(chalk_1.default.gray(' Use: ') + chalk_1.default.cyan('"force push (i know what i\'m doing)"'));
1171
+ console.log(chalk_1.default.gray(' Or: ') + chalk_1.default.cyan('"force push with lease"') + chalk_1.default.gray(' (safer - checks remote)\n'));
941
1172
  return false;
942
1173
  }
943
1174
  /**
@@ -23,6 +23,12 @@ exports.getPullRequestDiff = getPullRequestDiff;
23
23
  exports.reopenIssue = reopenIssue;
24
24
  exports.rerunWorkflow = rerunWorkflow;
25
25
  exports.getPullRequestFiles = getPullRequestFiles;
26
+ exports.addLabels = addLabels;
27
+ exports.enableAutoMerge = enableAutoMerge;
28
+ exports.createDraftPullRequest = createDraftPullRequest;
29
+ exports.markPRReady = markPRReady;
30
+ exports.getPRStack = getPRStack;
31
+ exports.updatePRBase = updatePRBase;
26
32
  const child_process_1 = require("child_process");
27
33
  const github_connect_1 = require("../commands/github-connect");
28
34
  const GITHUB_API = 'https://api.github.com';
@@ -33,14 +39,15 @@ function getCurrentRepo() {
33
39
  try {
34
40
  const remote = (0, child_process_1.execSync)('git remote get-url origin', { encoding: 'utf-8' }).trim();
35
41
  // Parse GitHub URL (supports both HTTPS and SSH)
42
+ // Updated regex to support dots, underscores, and other chars in repo names
36
43
  let match;
37
44
  if (remote.startsWith('https://')) {
38
45
  // https://github.com/owner/repo.git
39
- match = remote.match(/github\.com[/:]([\w-]+)\/([\w-]+?)(\.git)?$/);
46
+ match = remote.match(/github\.com[/:]([\w-]+)\/([\w.-]+?)(\.git)?$/);
40
47
  }
41
48
  else {
42
49
  // git@github.com:owner/repo.git
43
- match = remote.match(/github\.com[/:]([\w-]+)\/([\w-]+?)(\.git)?$/);
50
+ match = remote.match(/github\.com[/:]([\w-]+)\/([\w.-]+?)(\.git)?$/);
44
51
  }
45
52
  if (match) {
46
53
  return {
@@ -417,3 +424,159 @@ async function getPullRequestFiles(prNumber) {
417
424
  }
418
425
  return await githubRequest(`/repos/${repo.owner}/${repo.name}/pulls/${prNumber}/files`);
419
426
  }
427
+ /**
428
+ * Add labels to PR or Issue
429
+ */
430
+ async function addLabels(number, labels) {
431
+ const repo = getCurrentRepo();
432
+ if (!repo) {
433
+ throw new Error('Not a GitHub repository');
434
+ }
435
+ return await githubRequest(`/repos/${repo.owner}/${repo.name}/issues/${number}/labels`, {
436
+ method: 'POST',
437
+ body: JSON.stringify({ labels }),
438
+ });
439
+ }
440
+ /**
441
+ * Enable auto-merge for a PR
442
+ */
443
+ async function enableAutoMerge(prNumber, mergeMethod = 'MERGE') {
444
+ const repo = getCurrentRepo();
445
+ if (!repo) {
446
+ throw new Error('Not a GitHub repository');
447
+ }
448
+ // GraphQL query for auto-merge
449
+ const token = (0, github_connect_1.getGitHubToken)();
450
+ if (!token) {
451
+ throw new Error('GitHub not connected');
452
+ }
453
+ // First get PR node ID
454
+ const pr = await getPullRequest(prNumber);
455
+ const nodeId = pr.node_id;
456
+ const query = `
457
+ mutation {
458
+ enablePullRequestAutoMerge(input: {
459
+ pullRequestId: "${nodeId}"
460
+ mergeMethod: ${mergeMethod}
461
+ }) {
462
+ pullRequest {
463
+ id
464
+ autoMergeRequest {
465
+ enabledAt
466
+ }
467
+ }
468
+ }
469
+ }
470
+ `;
471
+ const response = await fetch('https://api.github.com/graphql', {
472
+ method: 'POST',
473
+ headers: {
474
+ Authorization: `Bearer ${token}`,
475
+ 'Content-Type': 'application/json',
476
+ },
477
+ body: JSON.stringify({ query }),
478
+ });
479
+ if (!response.ok) {
480
+ throw new Error('Failed to enable auto-merge');
481
+ }
482
+ return await response.json();
483
+ }
484
+ /**
485
+ * Create a draft PR (for stacked PRs)
486
+ */
487
+ async function createDraftPullRequest(options) {
488
+ const repo = getCurrentRepo();
489
+ if (!repo) {
490
+ throw new Error('Not a GitHub repository');
491
+ }
492
+ return await githubRequest(`/repos/${repo.owner}/${repo.name}/pulls`, {
493
+ method: 'POST',
494
+ body: JSON.stringify({
495
+ title: options.title,
496
+ body: options.body || '',
497
+ head: options.head,
498
+ base: options.base,
499
+ draft: true,
500
+ }),
501
+ });
502
+ }
503
+ /**
504
+ * Mark PR as ready for review (convert from draft)
505
+ */
506
+ async function markPRReady(prNumber) {
507
+ const repo = getCurrentRepo();
508
+ if (!repo) {
509
+ throw new Error('Not a GitHub repository');
510
+ }
511
+ const token = (0, github_connect_1.getGitHubToken)();
512
+ if (!token) {
513
+ throw new Error('GitHub not connected');
514
+ }
515
+ // Get PR node ID
516
+ const pr = await getPullRequest(prNumber);
517
+ const nodeId = pr.node_id;
518
+ const query = `
519
+ mutation {
520
+ markPullRequestReadyForReview(input: {
521
+ pullRequestId: "${nodeId}"
522
+ }) {
523
+ pullRequest {
524
+ id
525
+ isDraft
526
+ }
527
+ }
528
+ }
529
+ `;
530
+ const response = await fetch('https://api.github.com/graphql', {
531
+ method: 'POST',
532
+ headers: {
533
+ Authorization: `Bearer ${token}`,
534
+ 'Content-Type': 'application/json',
535
+ },
536
+ body: JSON.stringify({ query }),
537
+ });
538
+ if (!response.ok) {
539
+ throw new Error('Failed to mark PR ready');
540
+ }
541
+ return await response.json();
542
+ }
543
+ /**
544
+ * Get PR dependencies (for stacked PRs)
545
+ */
546
+ async function getPRStack(baseBranch = 'main') {
547
+ const repo = getCurrentRepo();
548
+ if (!repo) {
549
+ throw new Error('Not a GitHub repository');
550
+ }
551
+ const allPRs = await listPullRequests({ state: 'open', limit: 100 });
552
+ // Build dependency tree
553
+ const stack = [];
554
+ const prMap = new Map();
555
+ allPRs.forEach((pr) => {
556
+ prMap.set(pr.head.ref, pr);
557
+ });
558
+ // Find PRs that build on each other
559
+ allPRs.forEach((pr) => {
560
+ const baseRef = pr.base.ref;
561
+ if (prMap.has(baseRef) || baseRef === baseBranch) {
562
+ stack.push({
563
+ ...pr,
564
+ dependsOn: baseRef === baseBranch ? null : baseRef,
565
+ });
566
+ }
567
+ });
568
+ return stack;
569
+ }
570
+ /**
571
+ * Update PR base branch (for stacked PRs)
572
+ */
573
+ async function updatePRBase(prNumber, newBase) {
574
+ const repo = getCurrentRepo();
575
+ if (!repo) {
576
+ throw new Error('Not a GitHub repository');
577
+ }
578
+ return await githubRequest(`/repos/${repo.owner}/${repo.name}/pulls/${prNumber}`, {
579
+ method: 'PATCH',
580
+ body: JSON.stringify({ base: newBase }),
581
+ });
582
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snapcommit/cli",
3
- "version": "3.8.22",
3
+ "version": "3.8.24",
4
4
  "description": "Instant AI commits. Beautiful progress tracking. Never write commit messages again.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {