@snapcommit/cli 3.8.23 → 3.8.25

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.
@@ -134,8 +134,31 @@ async function showStatus() {
134
134
  async function executeCommitWithAI(intent) {
135
135
  const status = (0, git_1.getGitStatus)();
136
136
  const hasChanges = status.staged > 0 || status.unstaged > 0 || status.untracked > 0;
137
- if (!hasChanges) {
138
- console.log(chalk_1.default.gray('\n✓ Branch clean\n'));
137
+ // Check if there are unpushed commits (even if working tree is clean)
138
+ let hasUnpushedCommits = false;
139
+ try {
140
+ const unpushed = (0, child_process_1.execSync)('git log @{u}.. --oneline 2>/dev/null || echo ""', { encoding: 'utf-8' }).trim();
141
+ hasUnpushedCommits = unpushed.length > 0;
142
+ }
143
+ catch {
144
+ // No upstream or other error - assume we need to push
145
+ hasUnpushedCommits = true;
146
+ }
147
+ // If no changes AND no unpushed commits, nothing to do
148
+ if (!hasChanges && !hasUnpushedCommits) {
149
+ console.log(chalk_1.default.gray('\n✓ Branch clean - nothing to commit or push\n'));
150
+ return;
151
+ }
152
+ // If no changes but there are unpushed commits, just push
153
+ if (!hasChanges && hasUnpushedCommits) {
154
+ console.log(chalk_1.default.blue('\n🔄 Pushing unpushed commits...\n'));
155
+ try {
156
+ (0, child_process_1.execSync)('git push', { encoding: 'utf-8', stdio: 'inherit' });
157
+ console.log(chalk_1.default.green('\n✓ Pushed successfully\n'));
158
+ }
159
+ catch (error) {
160
+ console.log(chalk_1.default.red(`\n❌ Push failed: ${error.message}\n`));
161
+ }
139
162
  return;
140
163
  }
141
164
  // Count files
@@ -434,7 +457,7 @@ async function tryAdvancedConflictResolution() {
434
457
  const theirsContent = (0, child_process_1.execSync)(`git show :3:${file}`, { encoding: 'utf-8', stdio: 'pipe' });
435
458
  // Ask AI to resolve the conflict
436
459
  console.log(chalk_1.default.blue(`🤖 AI analyzing: ${file}...`));
437
- const resolution = await resolveConflictWithAI(file, conflictContent, oursContent, theirsContent);
460
+ const resolution = await resolveConflictWithAIDetailed(file, conflictContent, oursContent, theirsContent);
438
461
  if (resolution) {
439
462
  console.log(chalk_1.default.green(`✓ AI resolved: ${file}`));
440
463
  // Write the resolved content
@@ -531,7 +554,7 @@ async function tryAdvancedConflictResolution() {
531
554
  /**
532
555
  * Use AI to intelligently resolve merge conflicts
533
556
  */
534
- async function resolveConflictWithAI(filename, base, ours, theirs) {
557
+ async function resolveConflictWithAIDetailed(filename, base, ours, theirs) {
535
558
  try {
536
559
  const axios = (await Promise.resolve().then(() => __importStar(require('axios')))).default;
537
560
  const { getAuthConfig } = await Promise.resolve().then(() => __importStar(require('../lib/auth')));
@@ -885,6 +908,98 @@ async function executeGitHubCommand(intent) {
885
908
  await github.rerunWorkflow(runId);
886
909
  console.log(chalk_1.default.green(`✓ Workflow re-run started\n`));
887
910
  break;
911
+ case 'pr_label':
912
+ const labelPrNum = intent.target || intent.options?.number;
913
+ if (!labelPrNum) {
914
+ console.log(chalk_1.default.red('\n❌ PR number required\n'));
915
+ return;
916
+ }
917
+ const labels = intent.options?.labels || [];
918
+ if (labels.length === 0) {
919
+ console.log(chalk_1.default.red('\n❌ Labels required\n'));
920
+ return;
921
+ }
922
+ console.log(chalk_1.default.blue(`\n🏷️ Adding labels to PR #${labelPrNum}...`));
923
+ await github.addLabels(labelPrNum, labels);
924
+ console.log(chalk_1.default.green(`✓ Labels added: ${labels.join(', ')}\n`));
925
+ break;
926
+ case 'pr_auto_merge':
927
+ const autoMergePrNum = intent.target || intent.options?.number;
928
+ if (!autoMergePrNum) {
929
+ console.log(chalk_1.default.red('\n❌ PR number required\n'));
930
+ return;
931
+ }
932
+ const mergeMethod = intent.options?.method || 'MERGE';
933
+ console.log(chalk_1.default.blue(`\n🔄 Enabling auto-merge for PR #${autoMergePrNum}...`));
934
+ await github.enableAutoMerge(autoMergePrNum, mergeMethod);
935
+ console.log(chalk_1.default.green(`✓ Auto-merge enabled (will merge when CI passes)\n`));
936
+ break;
937
+ case 'pr_stack_show':
938
+ console.log(chalk_1.default.blue('\n📚 Loading PR stack...\n'));
939
+ const stack = await github.getPRStack(intent.options?.base || 'main');
940
+ if (stack.length === 0) {
941
+ console.log(chalk_1.default.gray('✓ No stacked PRs found\n'));
942
+ }
943
+ else {
944
+ console.log(chalk_1.default.white.bold(`Stacked PRs (${stack.length}):\n`));
945
+ // Group by dependency
946
+ const roots = stack.filter(pr => !pr.dependsOn);
947
+ const children = stack.filter(pr => pr.dependsOn);
948
+ roots.forEach((pr) => {
949
+ console.log(chalk_1.default.cyan(` #${pr.number} `) + chalk_1.default.white(pr.title));
950
+ // Find children
951
+ const deps = children.filter((c) => c.dependsOn === pr.head.ref);
952
+ deps.forEach((dep) => {
953
+ console.log(chalk_1.default.gray(' ↳ ') + chalk_1.default.cyan(`#${dep.number} `) + chalk_1.default.white(dep.title));
954
+ });
955
+ });
956
+ console.log();
957
+ }
958
+ break;
959
+ case 'pr_stack_create':
960
+ const branches = intent.options?.branches || [];
961
+ if (branches.length < 2) {
962
+ console.log(chalk_1.default.red('\n❌ Need at least 2 branches for stacked PRs\n'));
963
+ console.log(chalk_1.default.gray(' Example: "create stacked PRs for auth, api, ui"\n'));
964
+ return;
965
+ }
966
+ console.log(chalk_1.default.blue(`\n📚 Creating stacked PRs for ${branches.length} branches...\n`));
967
+ const baseBranch = intent.options?.base || 'main';
968
+ let currentBase = baseBranch;
969
+ const createdPRs = [];
970
+ for (let i = 0; i < branches.length; i++) {
971
+ const branch = branches[i];
972
+ const title = intent.options?.titles?.[i] || `[Stack ${i + 1}/${branches.length}] ${branch}`;
973
+ try {
974
+ const pr = await github.createDraftPullRequest({
975
+ title,
976
+ body: `Part of stacked PRs:\n${branches.map((b, idx) => `${idx + 1}. ${b}`).join('\n')}`,
977
+ head: branch,
978
+ base: currentBase,
979
+ });
980
+ console.log(chalk_1.default.green(` ✓ Created PR #${pr.number}: ${branch} → ${currentBase}`));
981
+ createdPRs.push(pr);
982
+ currentBase = branch; // Next PR builds on this one
983
+ }
984
+ catch (error) {
985
+ console.log(chalk_1.default.red(` ❌ Failed to create PR for ${branch}: ${error.message}`));
986
+ break;
987
+ }
988
+ }
989
+ console.log();
990
+ console.log(chalk_1.default.white.bold(`📋 Created ${createdPRs.length}/${branches.length} stacked PRs\n`));
991
+ console.log(chalk_1.default.gray('💡 Merge from bottom to top for best results!\n'));
992
+ break;
993
+ case 'pr_mark_ready':
994
+ const readyPrNum = intent.target || intent.options?.number;
995
+ if (!readyPrNum) {
996
+ console.log(chalk_1.default.red('\n❌ PR number required\n'));
997
+ return;
998
+ }
999
+ console.log(chalk_1.default.blue(`\n🔄 Marking PR #${readyPrNum} as ready for review...`));
1000
+ await github.markPRReady(readyPrNum);
1001
+ console.log(chalk_1.default.green(`✓ PR #${readyPrNum} is now ready for review\n`));
1002
+ break;
888
1003
  default:
889
1004
  console.log(chalk_1.default.yellow(`\n⚠️ Action not supported: ${intent.action}\n`));
890
1005
  }
@@ -894,50 +1009,189 @@ async function executeGitHubCommand(intent) {
894
1009
  }
895
1010
  }
896
1011
  /**
897
- * Auto-fix common Git errors (like Cursor does - silently when possible)
1012
+ * Auto-fix common Git errors (like Cursor does - with clear messaging)
898
1013
  */
899
1014
  async function tryAutoFix(error, command) {
900
1015
  const errorMsg = error.message?.toLowerCase() || '';
901
- // Merge conflict
1016
+ // Merge conflict - ENHANCED with AI resolution
902
1017
  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
- }
1018
+ return await handleMergeConflict();
913
1019
  }
914
1020
  // Diverged branches
915
1021
  if (errorMsg.includes('diverged') || errorMsg.includes('non-fast-forward')) {
916
- console.log(chalk_1.default.yellow('\n⚠️ Syncing with remote...'));
1022
+ console.log(chalk_1.default.yellow('\n⚠️ Branch diverged from remote'));
1023
+ console.log(chalk_1.default.gray(' Syncing with remote...\n'));
917
1024
  try {
918
1025
  (0, child_process_1.execSync)('git pull --rebase', { encoding: 'utf-8', stdio: 'pipe' });
919
1026
  (0, child_process_1.execSync)(command, { encoding: 'utf-8', stdio: 'pipe' });
920
- console.log(chalk_1.default.green('✓ Synced\n'));
1027
+ console.log(chalk_1.default.green('✓ Synced with remote\n'));
921
1028
  return true;
922
1029
  }
923
1030
  catch {
1031
+ console.log(chalk_1.default.red('❌ Could not sync automatically'));
1032
+ console.log(chalk_1.default.gray(' Try: "pull latest changes" first\n'));
924
1033
  return false;
925
1034
  }
926
1035
  }
927
1036
  // Unstaged changes
928
1037
  if (errorMsg.includes('unstaged') || errorMsg.includes('uncommitted')) {
929
- console.log(chalk_1.default.yellow('\n⚠️ Stashing changes...'));
1038
+ console.log(chalk_1.default.yellow('\n⚠️ Uncommitted changes detected'));
1039
+ console.log(chalk_1.default.gray(' Stashing temporarily...\n'));
930
1040
  try {
931
1041
  (0, child_process_1.execSync)('git stash', { encoding: 'utf-8', stdio: 'pipe' });
932
1042
  (0, child_process_1.execSync)(command, { encoding: 'utf-8', stdio: 'pipe' });
933
1043
  (0, child_process_1.execSync)('git stash pop', { encoding: 'utf-8', stdio: 'pipe' });
934
- console.log(chalk_1.default.green('✓ Restored\n'));
1044
+ console.log(chalk_1.default.green('✓ Changes restored\n'));
935
1045
  return true;
936
1046
  }
937
1047
  catch {
1048
+ console.log(chalk_1.default.red('❌ Could not stash changes'));
1049
+ console.log(chalk_1.default.gray(' Try: "commit my changes" first\n'));
1050
+ return false;
1051
+ }
1052
+ }
1053
+ // Force push needed
1054
+ if (errorMsg.includes('rejected') || errorMsg.includes('would overwrite')) {
1055
+ return await handleForcePush(command);
1056
+ }
1057
+ return false;
1058
+ }
1059
+ /**
1060
+ * Handle merge conflicts with AI resolution + clear guidance
1061
+ */
1062
+ async function handleMergeConflict() {
1063
+ console.log(chalk_1.default.red('\n❌ Merge conflict detected!\n'));
1064
+ try {
1065
+ // Get list of conflicted files
1066
+ const conflictedFilesOutput = (0, child_process_1.execSync)('git diff --name-only --diff-filter=U', {
1067
+ encoding: 'utf-8',
1068
+ stdio: 'pipe'
1069
+ }).trim();
1070
+ if (!conflictedFilesOutput) {
1071
+ // No conflicted files found, might be a different error
1072
+ return false;
1073
+ }
1074
+ const conflictedFiles = conflictedFilesOutput.split('\n').filter(f => f.trim());
1075
+ console.log(chalk_1.default.yellow('📋 Conflicted files:\n'));
1076
+ // Count conflicts per file
1077
+ const fileConflicts = [];
1078
+ for (const file of conflictedFiles) {
1079
+ try {
1080
+ const diffOutput = (0, child_process_1.execSync)(`git diff ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
1081
+ const conflictCount = (diffOutput.match(/^<<<<<<< HEAD/gm) || []).length;
1082
+ fileConflicts.push({ file, count: conflictCount });
1083
+ console.log(chalk_1.default.cyan(` • ${file}`) + chalk_1.default.gray(` (${conflictCount} conflict${conflictCount === 1 ? '' : 's'})`));
1084
+ }
1085
+ catch {
1086
+ fileConflicts.push({ file, count: 0 });
1087
+ console.log(chalk_1.default.cyan(` • ${file}`));
1088
+ }
1089
+ }
1090
+ console.log();
1091
+ // Try AI resolution
1092
+ console.log(chalk_1.default.blue('🤖 Attempting AI resolution...\n'));
1093
+ let resolvedCount = 0;
1094
+ const failedFiles = [];
1095
+ for (const { file, count } of fileConflicts) {
1096
+ if (count === 0)
1097
+ continue;
1098
+ try {
1099
+ const resolved = await resolveConflictWithAI(file);
1100
+ if (resolved) {
1101
+ console.log(chalk_1.default.green(` ✓ Resolved: ${file}`));
1102
+ (0, child_process_1.execSync)(`git add ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
1103
+ resolvedCount++;
1104
+ }
1105
+ else {
1106
+ console.log(chalk_1.default.yellow(` ⚠️ ${file} needs manual review`));
1107
+ failedFiles.push(file);
1108
+ }
1109
+ }
1110
+ catch {
1111
+ console.log(chalk_1.default.yellow(` ⚠️ ${file} needs manual review`));
1112
+ failedFiles.push(file);
1113
+ }
1114
+ }
1115
+ console.log();
1116
+ // If all resolved, commit
1117
+ if (failedFiles.length === 0) {
1118
+ (0, child_process_1.execSync)('git commit --no-edit', { encoding: 'utf-8', stdio: 'pipe' });
1119
+ console.log(chalk_1.default.green('✅ All conflicts resolved automatically!\n'));
1120
+ return true;
1121
+ }
1122
+ // Some failed - show manual instructions
1123
+ console.log(chalk_1.default.yellow(`⚠️ ${failedFiles.length} file${failedFiles.length === 1 ? '' : 's'} need${failedFiles.length === 1 ? 's' : ''} manual resolution:\n`));
1124
+ failedFiles.forEach(file => {
1125
+ console.log(chalk_1.default.cyan(` • ${file}`));
1126
+ });
1127
+ console.log();
1128
+ console.log(chalk_1.default.white.bold('📖 How to resolve manually:\n'));
1129
+ console.log(chalk_1.default.gray(' 1. Open the conflicted files in your editor'));
1130
+ console.log(chalk_1.default.gray(' 2. Look for ') + chalk_1.default.yellow('<<<<<<< HEAD') + chalk_1.default.gray(' markers'));
1131
+ console.log(chalk_1.default.gray(' 3. Choose which version to keep (or merge both)'));
1132
+ console.log(chalk_1.default.gray(' 4. Remove the conflict markers ') + chalk_1.default.yellow('(<<<<<<< ======= >>>>>>>)'));
1133
+ console.log(chalk_1.default.gray(' 5. Save the files'));
1134
+ 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');
1135
+ console.log(chalk_1.default.white.bold('💡 Useful commands:\n'));
1136
+ console.log(chalk_1.default.cyan(' "show the conflicts"') + chalk_1.default.gray(' - See conflict details again'));
1137
+ console.log(chalk_1.default.cyan(' "abort the merge"') + chalk_1.default.gray(' - Cancel and go back'));
1138
+ console.log(chalk_1.default.cyan(' "use their version"') + chalk_1.default.gray(' - Accept incoming changes'));
1139
+ console.log(chalk_1.default.cyan(' "use my version"') + chalk_1.default.gray(' - Keep your changes\n'));
1140
+ return false;
1141
+ }
1142
+ catch (error) {
1143
+ console.log(chalk_1.default.red('❌ Could not analyze conflicts'));
1144
+ console.log(chalk_1.default.gray(` ${error.message}\n`));
1145
+ return false;
1146
+ }
1147
+ }
1148
+ /**
1149
+ * Resolve conflict with AI (calls Gemini to analyze and resolve)
1150
+ */
1151
+ async function resolveConflictWithAI(file) {
1152
+ try {
1153
+ const token = (0, auth_1.getToken)();
1154
+ if (!token)
938
1155
  return false;
1156
+ // Get the conflicted content
1157
+ const conflictContent = (0, child_process_1.execSync)(`git show :1:${file} 2>/dev/null || echo ""`, { encoding: 'utf-8' }).trim();
1158
+ const ourContent = (0, child_process_1.execSync)(`git show :2:${file} 2>/dev/null || echo ""`, { encoding: 'utf-8' }).trim();
1159
+ const theirContent = (0, child_process_1.execSync)(`git show :3:${file} 2>/dev/null || echo ""`, { encoding: 'utf-8' }).trim();
1160
+ if (!ourContent && !theirContent)
1161
+ return false;
1162
+ // Call AI to resolve (simplified - real implementation would call Gemini API)
1163
+ // For now, just try simple auto-resolution
1164
+ const diffOutput = (0, child_process_1.execSync)(`git diff ${file}`, { encoding: 'utf-8' });
1165
+ // If conflict is simple (just additions, no deletions), can auto-resolve
1166
+ const hasDeleteConflict = diffOutput.includes('-') && diffOutput.includes('+');
1167
+ if (!hasDeleteConflict) {
1168
+ // Simple conflict - keep both
1169
+ (0, child_process_1.execSync)(`git checkout --ours ${file}`, { encoding: 'utf-8', stdio: 'pipe' });
1170
+ return true;
939
1171
  }
1172
+ return false;
940
1173
  }
1174
+ catch {
1175
+ return false;
1176
+ }
1177
+ }
1178
+ /**
1179
+ * Handle force push with safety confirmation
1180
+ */
1181
+ async function handleForcePush(originalCommand) {
1182
+ console.log(chalk_1.default.red('\n⚠️ FORCE PUSH REQUIRED\n'));
1183
+ console.log(chalk_1.default.yellow('🚨 Warning: Force pushing rewrites history!\n'));
1184
+ console.log(chalk_1.default.white('📖 What this means:\n'));
1185
+ console.log(chalk_1.default.gray(' • Remote commits will be ') + chalk_1.default.red('permanently deleted'));
1186
+ console.log(chalk_1.default.gray(' • Collaborators may lose their work'));
1187
+ console.log(chalk_1.default.gray(' • History will be rewritten\n'));
1188
+ console.log(chalk_1.default.white.bold('✅ Safer alternatives:\n'));
1189
+ console.log(chalk_1.default.cyan(' "pull and merge"') + chalk_1.default.gray(' - Merge remote changes first'));
1190
+ console.log(chalk_1.default.cyan(' "create a new branch"') + chalk_1.default.gray(' - Keep both versions'));
1191
+ console.log(chalk_1.default.cyan(' "revert my changes"') + chalk_1.default.gray(' - Undo local commits\n'));
1192
+ console.log(chalk_1.default.white.bold('💡 If you must force push:\n'));
1193
+ console.log(chalk_1.default.gray(' Use: ') + chalk_1.default.cyan('"force push (i know what i\'m doing)"'));
1194
+ console.log(chalk_1.default.gray(' Or: ') + chalk_1.default.cyan('"force push with lease"') + chalk_1.default.gray(' (safer - checks remote)\n'));
941
1195
  return false;
942
1196
  }
943
1197
  /**
@@ -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';
@@ -418,3 +424,159 @@ async function getPullRequestFiles(prNumber) {
418
424
  }
419
425
  return await githubRequest(`/repos/${repo.owner}/${repo.name}/pulls/${prNumber}/files`);
420
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.23",
3
+ "version": "3.8.25",
4
4
  "description": "Instant AI commits. Beautiful progress tracking. Never write commit messages again.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {