edsger 0.30.0 → 0.30.1

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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(npx tsc:*)",
5
+ "Bash(npm run:*)"
6
+ ]
7
+ }
8
+ }
@@ -14,6 +14,7 @@ import { createBranches, getCurrentBranch, updateBranch, } from '../../services/
14
14
  import { prepareCustomBranchGitEnvironmentAsync, resetUncommittedChanges, } from '../../utils/git-branch-manager.js';
15
15
  import { createBranchPullRequest, } from '../code-implementation/branch-pr-creator.js';
16
16
  import { getGitHubConfig } from '../../api/github.js';
17
+ import { gitPush } from '../../utils/git-push.js';
17
18
  import { logFeaturePhaseEvent } from '../../services/audit-logs.js';
18
19
  import { createAutonomousSystemPrompt, createAutonomousUserPrompt, } from './prompts.js';
19
20
  /**
@@ -35,59 +36,6 @@ async function* prompt(userPrompt) {
35
36
  yield userMessage(userPrompt);
36
37
  await new Promise((res) => setTimeout(res, 10000));
37
38
  }
38
- /**
39
- * Push branch to remote repository
40
- */
41
- async function pushToRemote(branchName, verbose) {
42
- try {
43
- const { execSync } = await import('child_process');
44
- if (verbose) {
45
- logInfo(`📤 Pushing branch ${branchName} to remote...`);
46
- }
47
- try {
48
- execSync(`git push -u origin ${branchName}`, {
49
- encoding: 'utf-8',
50
- stdio: verbose ? 'inherit' : 'pipe',
51
- });
52
- return { success: true };
53
- }
54
- catch {
55
- try {
56
- execSync(`git push origin ${branchName}`, {
57
- encoding: 'utf-8',
58
- stdio: verbose ? 'inherit' : 'pipe',
59
- });
60
- return { success: true };
61
- }
62
- catch {
63
- if (verbose) {
64
- logInfo(`⚠️ Push rejected, attempting force push with lease...`);
65
- }
66
- try {
67
- execSync(`git push --force-with-lease origin ${branchName}`, {
68
- encoding: 'utf-8',
69
- stdio: verbose ? 'inherit' : 'pipe',
70
- });
71
- return { success: true };
72
- }
73
- catch (forceError) {
74
- return {
75
- success: false,
76
- error: forceError instanceof Error
77
- ? forceError.message
78
- : String(forceError),
79
- };
80
- }
81
- }
82
- }
83
- }
84
- catch (error) {
85
- return {
86
- success: false,
87
- error: error instanceof Error ? error.message : String(error),
88
- };
89
- }
90
- }
91
39
  /**
92
40
  * Run a single autonomous iteration using Claude Code SDK
93
41
  */
@@ -265,14 +213,19 @@ export async function runAutonomousDevelopment(options, config, _checklistContex
265
213
  logInfo(`⏱️ Iteration ${totalIterations} took ${(iterationDuration / 1000).toFixed(0)}s`);
266
214
  if (iterationResult.success) {
267
215
  logInfo(`✅ Iteration ${totalIterations}: ${iterationResult.summary || 'completed'}`);
268
- // Push to remote
269
- const pushResult = await pushToRemote(devBranchName, verbose);
216
+ // Get GitHub config for push authentication and PR creation
217
+ const githubConfig = await getGitHubConfig(featureId, verbose);
218
+ // Push to remote with authentication
219
+ const pushResult = gitPush({
220
+ branchName: devBranchName,
221
+ token: githubConfig.configured ? githubConfig.token : undefined,
222
+ verbose,
223
+ });
270
224
  if (!pushResult.success && verbose) {
271
225
  logError(`⚠️ Failed to push: ${pushResult.error}`);
272
226
  }
273
227
  // Create PR after first successful iteration
274
228
  if (!prUrl) {
275
- const githubConfig = await getGitHubConfig(featureId, verbose);
276
229
  if (githubConfig.configured &&
277
230
  githubConfig.token &&
278
231
  githubConfig.owner &&
@@ -3,8 +3,8 @@
3
3
  * Creates pull requests from dev/ branches to feat/ branches
4
4
  */
5
5
  import { Octokit } from '@octokit/rest';
6
- import { execSync } from 'child_process';
7
6
  import { logInfo, logError } from '../../utils/logger.js';
7
+ import { gitPush } from '../../utils/git-push.js';
8
8
  // GitHub PR title best practice: keep under 72 characters
9
9
  const MAX_PR_TITLE_LENGTH = 72;
10
10
  const PR_TITLE_PREFIX = 'feat: ';
@@ -40,62 +40,6 @@ export function devBranchToFeatBranch(devBranchName) {
40
40
  // If already feat/ or other format, return as-is
41
41
  return devBranchName;
42
42
  }
43
- /**
44
- * Push a branch to remote
45
- * Falls back to force-with-lease if normal push fails (e.g., after rebase)
46
- */
47
- async function pushBranch(branchName, verbose) {
48
- try {
49
- if (verbose) {
50
- logInfo(`📤 Pushing branch ${branchName} to remote...`);
51
- }
52
- // Try to push with -u flag (sets upstream if not already set)
53
- try {
54
- execSync(`git push -u origin ${branchName}`, {
55
- encoding: 'utf-8',
56
- stdio: verbose ? 'inherit' : 'pipe',
57
- });
58
- return { success: true };
59
- }
60
- catch (error) {
61
- // If push fails, try without -u flag
62
- try {
63
- execSync(`git push origin ${branchName}`, {
64
- encoding: 'utf-8',
65
- stdio: verbose ? 'inherit' : 'pipe',
66
- });
67
- return { success: true };
68
- }
69
- catch (retryError) {
70
- // If push still fails (likely non-fast-forward after rebase),
71
- // use force-with-lease for safer force push
72
- if (verbose) {
73
- logInfo(`⚠️ Push rejected, attempting force push with lease...`);
74
- }
75
- try {
76
- execSync(`git push --force-with-lease origin ${branchName}`, {
77
- encoding: 'utf-8',
78
- stdio: verbose ? 'inherit' : 'pipe',
79
- });
80
- if (verbose) {
81
- logInfo(`✅ Successfully force pushed ${branchName}`);
82
- }
83
- return { success: true };
84
- }
85
- catch (forceError) {
86
- const errorMessage = forceError instanceof Error
87
- ? forceError.message
88
- : String(forceError);
89
- return { success: false, error: errorMessage };
90
- }
91
- }
92
- }
93
- }
94
- catch (error) {
95
- const errorMessage = error instanceof Error ? error.message : String(error);
96
- return { success: false, error: errorMessage };
97
- }
98
- }
99
43
  /**
100
44
  * Create a pull request from a dev/ branch to its corresponding feat/ branch
101
45
  * This is used after code implementation in multi-branch features
@@ -115,7 +59,7 @@ export async function createBranchPullRequest(config, devBranchName, featureName
115
59
  if (verbose) {
116
60
  logInfo(`📤 Pushing ${devBranchName} to remote...`);
117
61
  }
118
- const pushResult = await pushBranch(devBranchName, verbose);
62
+ const pushResult = gitPush({ branchName: devBranchName, token: githubToken, verbose });
119
63
  if (!pushResult.success) {
120
64
  return {
121
65
  success: false,
@@ -1,6 +1,7 @@
1
1
  import { query } from '@anthropic-ai/claude-agent-sdk';
2
2
  import { DEFAULT_MODEL } from '../../constants.js';
3
3
  import { logInfo, logError } from '../../utils/logger.js';
4
+ import { gitPush } from '../../utils/git-push.js';
4
5
  import { formatChecklistsForContext, } from '../../services/checklist.js';
5
6
  import { getFeedbacksForPhase, formatFeedbacksForContext, } from '../../services/feedbacks.js';
6
7
  import { fetchCodeImplementationContext, formatContextForPrompt, } from './context.js';
@@ -164,6 +165,7 @@ export const implementFeatureCode = async (options, config, checklistContext) =>
164
165
  // Sync feat branch to main before rebase to ensure it's up to date
165
166
  // This prevents the PR (dev → feat) from showing extra commits
166
167
  let featSyncedToMain = false;
168
+ let githubTokenForPush;
167
169
  if (devBranchName.startsWith('dev/')) {
168
170
  try {
169
171
  const githubConfig = await getGitHubConfig(featureId, verbose);
@@ -171,6 +173,7 @@ export const implementFeatureCode = async (options, config, checklistContext) =>
171
173
  githubConfig.token &&
172
174
  githubConfig.owner &&
173
175
  githubConfig.repo) {
176
+ githubTokenForPush = githubConfig.token;
174
177
  const { devBranchToFeatBranch } = await import('./branch-pr-creator.js');
175
178
  const featBranchName = devBranchToFeatBranch(devBranchName);
176
179
  if (verbose) {
@@ -200,6 +203,7 @@ export const implementFeatureCode = async (options, config, checklistContext) =>
200
203
  },
201
204
  forcePushAfterRebase: featSyncedToMain, // Trigger GitHub to recalculate PR diff
202
205
  baseBranchCompleted, // Tell conflict resolver to use --theirs for all conflicts
206
+ githubToken: githubTokenForPush,
203
207
  });
204
208
  try {
205
209
  // Fetch all required context information via MCP endpoints
@@ -462,7 +466,11 @@ export const implementFeatureCode = async (options, config, checklistContext) =>
462
466
  if (verbose) {
463
467
  logInfo(`📤 Pushing code to remote repository...`);
464
468
  }
465
- const pushResult = await pushToRemote(structuredImplementationResult.branch_name || branchName, verbose);
469
+ const pushResult = gitPush({
470
+ branchName: structuredImplementationResult.branch_name || branchName,
471
+ token: githubTokenForPush,
472
+ verbose,
473
+ });
466
474
  if (!pushResult.success && verbose) {
467
475
  logError(`⚠️ Failed to push to remote: ${pushResult.error}`);
468
476
  logInfo(' Code is committed locally and will be reviewed. Manual push may be needed.');
@@ -854,69 +862,3 @@ const parseImplementationResponse = (response, featureId) => {
854
862
  };
855
863
  }
856
864
  };
857
- /**
858
- * Push branch to remote repository
859
- * Falls back to force-with-lease if normal push fails (e.g., after rebase)
860
- */
861
- async function pushToRemote(branchName, verbose) {
862
- try {
863
- // Import exec from child_process
864
- const { execSync } = await import('child_process');
865
- if (verbose) {
866
- logInfo(`Pushing branch ${branchName} to remote...`);
867
- }
868
- // Try to push with -u flag (sets upstream if not already set)
869
- try {
870
- execSync(`git push -u origin ${branchName}`, {
871
- encoding: 'utf-8',
872
- stdio: verbose ? 'inherit' : 'pipe',
873
- });
874
- return { success: true };
875
- }
876
- catch (error) {
877
- // If push fails, it might be because branch already has upstream
878
- // Try without -u flag
879
- try {
880
- execSync(`git push origin ${branchName}`, {
881
- encoding: 'utf-8',
882
- stdio: verbose ? 'inherit' : 'pipe',
883
- });
884
- return { success: true };
885
- }
886
- catch (retryError) {
887
- // If push still fails (likely non-fast-forward after rebase),
888
- // use force-with-lease for safer force push
889
- // force-with-lease ensures we don't overwrite others' work by checking remote state
890
- if (verbose) {
891
- logInfo(`⚠️ Push rejected, attempting force push with lease...`);
892
- }
893
- try {
894
- execSync(`git push --force-with-lease origin ${branchName}`, {
895
- encoding: 'utf-8',
896
- stdio: verbose ? 'inherit' : 'pipe',
897
- });
898
- if (verbose) {
899
- logInfo(`✅ Successfully force pushed ${branchName}`);
900
- }
901
- return { success: true };
902
- }
903
- catch (forceError) {
904
- const errorMessage = forceError instanceof Error
905
- ? forceError.message
906
- : String(forceError);
907
- return {
908
- success: false,
909
- error: errorMessage,
910
- };
911
- }
912
- }
913
- }
914
- }
915
- catch (error) {
916
- const errorMessage = error instanceof Error ? error.message : String(error);
917
- return {
918
- success: false,
919
- error: errorMessage,
920
- };
921
- }
922
- }
@@ -6,7 +6,7 @@
6
6
  import { query } from '@anthropic-ai/claude-agent-sdk';
7
7
  import { DEFAULT_MODEL } from '../../constants.js';
8
8
  import { logInfo, logError } from '../../utils/logger.js';
9
- import { execSync } from 'child_process';
9
+ import { gitPushCurrentBranch } from '../../utils/git-push.js';
10
10
  import { fetchCodeRefineContext, } from './context.js';
11
11
  import { getFeedbacksForPhase, formatFeedbacksForContext, } from '../../services/feedbacks.js';
12
12
  import { createSystemPrompt, createCodeRefinePrompt } from './prompts.js';
@@ -26,41 +26,6 @@ async function* prompt(refinePrompt) {
26
26
  yield userMessage(refinePrompt);
27
27
  await new Promise((res) => setTimeout(res, 10000));
28
28
  }
29
- // Git utility functions are now imported from shared module
30
- /**
31
- * Push changes to remote branch
32
- */
33
- const pushChanges = (verbose) => {
34
- try {
35
- if (verbose) {
36
- logInfo(`📤 Pushing changes to remote...`);
37
- }
38
- execSync('git push origin $(git branch --show-current)', {
39
- encoding: 'utf-8',
40
- });
41
- if (verbose) {
42
- logInfo(`✅ Successfully pushed changes`);
43
- }
44
- }
45
- catch (error) {
46
- // If push fails due to non-fast-forward, use force-with-lease for safer force push
47
- // force-with-lease ensures we don't overwrite others' work by checking remote state
48
- if (verbose) {
49
- logInfo(`⚠️ Push rejected, attempting force push with lease...`);
50
- }
51
- try {
52
- execSync('git push --force-with-lease origin $(git branch --show-current)', {
53
- encoding: 'utf-8',
54
- });
55
- if (verbose) {
56
- logInfo(`✅ Successfully force pushed changes`);
57
- }
58
- }
59
- catch (forceError) {
60
- throw new Error(`Failed to push changes: ${forceError instanceof Error ? forceError.message : String(forceError)}`);
61
- }
62
- }
63
- };
64
29
  /**
65
30
  * Main code refine function with built-in verification loop
66
31
  * Similar to technical-design, this includes an iterative improvement cycle:
@@ -178,6 +143,7 @@ export const refineCodeFromPRFeedback = async (options, config, checklistContext
178
143
  },
179
144
  forcePushAfterRebase: featSyncedToMain,
180
145
  baseBranchCompleted: baseBranchCompletedForRebase,
146
+ githubToken,
181
147
  })
182
148
  : await preparePhaseGitEnvironmentAsync(featureId, 'main', verbose, true, {
183
149
  model: DEFAULT_MODEL,
@@ -258,7 +224,10 @@ Please ensure Claude Code commits all changes before completing the refine phase
258
224
  logInfo('✅ All changes committed. Pushing to remote...');
259
225
  }
260
226
  try {
261
- pushChanges(verbose);
227
+ const pushResult = gitPushCurrentBranch({ token: githubToken, verbose });
228
+ if (!pushResult.success) {
229
+ throw new Error(pushResult.error || 'Push failed');
230
+ }
262
231
  }
263
232
  catch (pushError) {
264
233
  logError(`Failed to push changes: ${pushError}`);
@@ -232,6 +232,7 @@ export const reviewPullRequest = async (options, config, checklistContext) => {
232
232
  },
233
233
  forcePushAfterRebase: featSyncedToMain,
234
234
  baseBranchCompleted: baseBranchCompletedForRebase,
235
+ githubToken,
235
236
  })
236
237
  : await preparePhaseGitEnvironmentAsync(featureId, 'main', verbose, true, {
237
238
  model: DEFAULT_MODEL,
@@ -108,6 +108,7 @@ export const executeFeaturePRs = async (options, config) => {
108
108
  repo: context.githubConfig.repo,
109
109
  forkInfo: context.forkInfo,
110
110
  verbose,
111
+ token: context.githubConfig.token,
111
112
  };
112
113
  const executionSummary = {
113
114
  branchesCreated: 0,
@@ -8,6 +8,7 @@ export interface PRExecutionConfig {
8
8
  repo: string;
9
9
  forkInfo: RepoForkInfo;
10
10
  verbose?: boolean;
11
+ token?: string;
11
12
  }
12
13
  export interface PRBranchResult {
13
14
  branchName: string;
@@ -19,7 +20,7 @@ export interface PRBranchResult {
19
20
  /**
20
21
  * Push a branch to remote with force-with-lease fallback
21
22
  */
22
- export declare function pushBranch(branchName: string, verbose?: boolean): void;
23
+ export declare function pushBranch(branchName: string, verbose?: boolean, token?: string): void;
23
24
  /**
24
25
  * Get the HEAD SHA of a local branch
25
26
  */
@@ -2,53 +2,24 @@
2
2
  * PR Executor for pushing branches to remote and generating compare URLs
3
3
  * Handles fork-aware URL construction for manual PR creation
4
4
  */
5
- import { execSync } from 'child_process';
6
5
  import { logInfo } from '../../utils/logger.js';
7
6
  import { updatePullRequest } from '../../services/pull-requests.js';
7
+ import { gitPush } from '../../utils/git-push.js';
8
+ import { execFileSync } from 'child_process';
8
9
  /**
9
10
  * Push a branch to remote with force-with-lease fallback
10
11
  */
11
- export function pushBranch(branchName, verbose) {
12
- try {
13
- if (verbose) {
14
- logInfo(`📤 Pushing branch ${branchName} to remote...`);
15
- }
16
- try {
17
- execSync(`git push -u origin ${branchName}`, {
18
- encoding: 'utf-8',
19
- stdio: verbose ? 'inherit' : 'pipe',
20
- });
21
- }
22
- catch {
23
- try {
24
- execSync(`git push origin ${branchName}`, {
25
- encoding: 'utf-8',
26
- stdio: verbose ? 'inherit' : 'pipe',
27
- });
28
- }
29
- catch {
30
- if (verbose) {
31
- logInfo(`⚠️ Push rejected, attempting force push with lease...`);
32
- }
33
- execSync(`git push --force-with-lease origin ${branchName}`, {
34
- encoding: 'utf-8',
35
- stdio: verbose ? 'inherit' : 'pipe',
36
- });
37
- if (verbose) {
38
- logInfo(`✅ Successfully force pushed ${branchName}`);
39
- }
40
- }
41
- }
42
- }
43
- catch (error) {
44
- throw new Error(`Failed to push branch ${branchName}: ${error instanceof Error ? error.message : String(error)}`);
12
+ export function pushBranch(branchName, verbose, token) {
13
+ const result = gitPush({ branchName, token, verbose });
14
+ if (!result.success) {
15
+ throw new Error(`Failed to push branch ${branchName}: ${result.error}`);
45
16
  }
46
17
  }
47
18
  /**
48
19
  * Get the HEAD SHA of a local branch
49
20
  */
50
21
  export function getLocalBranchSha(branchName) {
51
- return execSync(`git rev-parse ${branchName}`, {
22
+ return execFileSync('git', ['rev-parse', branchName], {
52
23
  encoding: 'utf-8',
53
24
  }).trim();
54
25
  }
@@ -68,10 +39,10 @@ export function buildCompareUrl(forkInfo, owner, repo, branchName, base = 'main'
68
39
  * Push a branch and build the compare URL
69
40
  */
70
41
  export function pushBranchAndBuildUrl(config, branchName) {
71
- const { owner, repo, forkInfo, verbose } = config;
42
+ const { owner, repo, forkInfo, verbose, token } = config;
72
43
  const headSha = getLocalBranchSha(branchName);
73
44
  try {
74
- pushBranch(branchName, verbose);
45
+ pushBranch(branchName, verbose, token);
75
46
  const compareUrl = buildCompareUrl(forkInfo, owner, repo, branchName);
76
47
  if (verbose) {
77
48
  logInfo(`🔗 Compare URL: ${compareUrl}`);
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { Octokit } from '@octokit/rest';
6
6
  import { execSync } from 'child_process';
7
+ import { gitPush } from '../../utils/git-push.js';
7
8
  // GitHub PR title best practice: keep under 72 characters
8
9
  const MAX_PR_TITLE_LENGTH = 72;
9
10
  const PR_TITLE_PREFIX = 'feat: ';
@@ -115,39 +116,6 @@ const switchToBranch = (branch, verbose) => {
115
116
  throw new Error(`Failed to switch to branch ${branch}: ${error}`);
116
117
  }
117
118
  };
118
- /**
119
- * Push current branch to remote
120
- * Falls back to force-with-lease if normal push fails (e.g., after rebase)
121
- */
122
- const pushBranch = (branch, verbose) => {
123
- try {
124
- if (verbose) {
125
- console.log(`📤 Pushing branch ${branch} to remote...`);
126
- }
127
- execSync(`git push -u origin ${branch}`, { encoding: 'utf-8' });
128
- if (verbose) {
129
- console.log(`✅ Branch pushed successfully`);
130
- }
131
- }
132
- catch (error) {
133
- // If push fails (likely non-fast-forward after rebase),
134
- // use force-with-lease for safer force push
135
- if (verbose) {
136
- console.log(`⚠️ Push rejected, attempting force push with lease...`);
137
- }
138
- try {
139
- execSync(`git push --force-with-lease origin ${branch}`, {
140
- encoding: 'utf-8',
141
- });
142
- if (verbose) {
143
- console.log(`✅ Successfully force pushed ${branch}`);
144
- }
145
- }
146
- catch (forceError) {
147
- throw new Error(`Failed to push branch: ${forceError}`);
148
- }
149
- }
150
- };
151
119
  /**
152
120
  * Switch to main branch
153
121
  */
@@ -276,7 +244,10 @@ export async function createPullRequest(config, feature) {
276
244
  }
277
245
  try {
278
246
  // Push current branch to remote first
279
- pushBranch(currentBranch, verbose);
247
+ const pushResult = gitPush({ branchName: currentBranch, token: githubToken, verbose });
248
+ if (!pushResult.success) {
249
+ throw new Error(`Failed to push branch: ${pushResult.error}`);
250
+ }
280
251
  // Get the base branch (main) SHA to create feat/ branch from
281
252
  const { data: baseBranchData } = await octokit.repos.getBranch({
282
253
  owner,
@@ -306,7 +277,10 @@ export async function createPullRequest(config, feature) {
306
277
  }
307
278
  else {
308
279
  // If not a dev/ branch, push it normally
309
- pushBranch(currentBranch, verbose);
280
+ const pushResult = gitPush({ branchName: currentBranch, token: githubToken, verbose });
281
+ if (!pushResult.success) {
282
+ throw new Error(`Failed to push branch: ${pushResult.error}`);
283
+ }
310
284
  }
311
285
  // Generate PR title and body
312
286
  const title = generatePRTitle(feature.name);
@@ -212,6 +212,8 @@ export interface RebaseWithConflictResolutionOptions {
212
212
  * GitHub to recalculate the PR diff.
213
213
  */
214
214
  forcePushAfterRebase?: boolean;
215
+ /** GitHub App installation token for authenticated push */
216
+ githubToken?: string;
215
217
  }
216
218
  /**
217
219
  * Switch to feature branch and rebase with base branch, with optional automatic conflict resolution
@@ -7,6 +7,7 @@ import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
  import { Octokit } from '@octokit/rest';
9
9
  import { logInfo, logError } from './logger.js';
10
+ import { gitForcePush } from './git-push.js';
10
11
  /**
11
12
  * Get current Git branch name
12
13
  */
@@ -689,7 +690,7 @@ export async function syncFeatBranchWithMain(featBranch, githubToken, owner, rep
689
690
  * @returns Object containing the previous branch name and conflict resolution result
690
691
  */
691
692
  export async function switchToFeatureBranchAndRebaseAsync(options) {
692
- const { featureBranch, baseBranch = 'main', rebaseTargetBranch, originalBaseBranch, verbose, resolveConflicts = false, conflictResolverConfig, forcePushAfterRebase = false, baseBranchCompleted = false, } = options;
693
+ const { featureBranch, baseBranch = 'main', rebaseTargetBranch, originalBaseBranch, verbose, resolveConflicts = false, conflictResolverConfig, forcePushAfterRebase = false, baseBranchCompleted = false, githubToken, } = options;
693
694
  // Determine the actual target branch for rebase
694
695
  // If rebaseTargetBranch is set (e.g., main), use it; otherwise use baseBranch
695
696
  const actualRebaseTarget = rebaseTargetBranch || baseBranch;
@@ -864,23 +865,9 @@ export async function switchToFeatureBranchAndRebaseAsync(options) {
864
865
  }
865
866
  // Force push after rebase to trigger GitHub to recalculate PR diff
866
867
  if (forcePushAfterRebase) {
867
- try {
868
- if (verbose) {
869
- logInfo(`📤 Force pushing ${featureBranch} to trigger PR diff recalculation...`);
870
- }
871
- execSync(`git push --force-with-lease origin ${featureBranch}`, {
872
- encoding: 'utf-8',
873
- stdio: verbose ? 'inherit' : 'pipe',
874
- });
875
- if (verbose) {
876
- logInfo(`✅ Successfully force pushed ${featureBranch}`);
877
- }
878
- }
879
- catch (pushError) {
880
- if (verbose) {
881
- logInfo(`⚠️ Could not force push ${featureBranch}: ${pushError instanceof Error ? pushError.message : String(pushError)}`);
882
- }
883
- // Don't fail the whole process if push fails
868
+ const pushResult = gitForcePush({ branchName: featureBranch, token: githubToken, verbose });
869
+ if (!pushResult.success && verbose) {
870
+ logInfo(`⚠️ Could not force push ${featureBranch}: ${pushResult.error}`);
884
871
  }
885
872
  }
886
873
  return { previousBranch };
@@ -908,23 +895,9 @@ export async function switchToFeatureBranchAndRebaseAsync(options) {
908
895
  }
909
896
  // Force push after rebase to trigger GitHub to recalculate PR diff
910
897
  if (forcePushAfterRebase) {
911
- try {
912
- if (verbose) {
913
- logInfo(`📤 Force pushing ${featureBranch} to trigger PR diff recalculation...`);
914
- }
915
- execSync(`git push --force-with-lease origin ${featureBranch}`, {
916
- encoding: 'utf-8',
917
- stdio: verbose ? 'inherit' : 'pipe',
918
- });
919
- if (verbose) {
920
- logInfo(`✅ Successfully force pushed ${featureBranch}`);
921
- }
922
- }
923
- catch (pushError) {
924
- if (verbose) {
925
- logInfo(`⚠️ Could not force push ${featureBranch}: ${pushError instanceof Error ? pushError.message : String(pushError)}`);
926
- }
927
- // Don't fail the whole process if push fails
898
+ const pushResult = gitForcePush({ branchName: featureBranch, token: githubToken, verbose });
899
+ if (!pushResult.success && verbose) {
900
+ logInfo(`⚠️ Could not force push ${featureBranch}: ${pushResult.error}`);
928
901
  }
929
902
  }
930
903
  return {
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Shared git push utilities with GitHub App credential helper support.
3
+ *
4
+ * Uses `git -c credential.helper=...` to pass the token securely per-command,
5
+ * matching the pattern established in workspace-manager.ts for clone/fetch.
6
+ * Uses `execFileSync` instead of `execSync` to avoid shell metacharacter injection.
7
+ */
8
+ export interface GitPushOptions {
9
+ branchName: string;
10
+ token?: string;
11
+ verbose?: boolean;
12
+ cwd?: string;
13
+ }
14
+ export interface GitPushResult {
15
+ success: boolean;
16
+ error?: string;
17
+ method?: 'push-u' | 'push' | 'force-with-lease';
18
+ }
19
+ /**
20
+ * Build git args with optional credential helper prefix.
21
+ * When a token is provided, prepends `-c credential.helper=...` so git
22
+ * authenticates with the GitHub App installation token.
23
+ */
24
+ export declare function buildCredentialArgs(token?: string): string[];
25
+ /**
26
+ * Push a branch to remote with three-level fallback:
27
+ * 1. git push -u origin <branch>
28
+ * 2. git push origin <branch>
29
+ * 3. git push --force-with-lease origin <branch>
30
+ *
31
+ * When `token` is provided, each command uses a credential helper for authentication.
32
+ */
33
+ export declare function gitPush(options: GitPushOptions): GitPushResult;
34
+ /**
35
+ * Force push a branch using --force-with-lease.
36
+ * Used after rebase operations to update the remote branch.
37
+ */
38
+ export declare function gitForcePush(options: GitPushOptions): GitPushResult;
39
+ /**
40
+ * Push the current branch to remote.
41
+ * Resolves the current branch name first, then delegates to gitPush.
42
+ */
43
+ export declare function gitPushCurrentBranch(options: Omit<GitPushOptions, 'branchName'>): GitPushResult;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Shared git push utilities with GitHub App credential helper support.
3
+ *
4
+ * Uses `git -c credential.helper=...` to pass the token securely per-command,
5
+ * matching the pattern established in workspace-manager.ts for clone/fetch.
6
+ * Uses `execFileSync` instead of `execSync` to avoid shell metacharacter injection.
7
+ */
8
+ import { execFileSync } from 'child_process';
9
+ import { logInfo } from './logger.js';
10
+ /**
11
+ * Build git args with optional credential helper prefix.
12
+ * When a token is provided, prepends `-c credential.helper=...` so git
13
+ * authenticates with the GitHub App installation token.
14
+ */
15
+ export function buildCredentialArgs(token) {
16
+ if (!token)
17
+ return [];
18
+ const credentialHelper = `!f() { echo "username=x-access-token"; echo "password=${token}"; }; f`;
19
+ return ['-c', `credential.helper=${credentialHelper}`];
20
+ }
21
+ /**
22
+ * Push a branch to remote with three-level fallback:
23
+ * 1. git push -u origin <branch>
24
+ * 2. git push origin <branch>
25
+ * 3. git push --force-with-lease origin <branch>
26
+ *
27
+ * When `token` is provided, each command uses a credential helper for authentication.
28
+ */
29
+ export function gitPush(options) {
30
+ const { branchName, token, verbose, cwd } = options;
31
+ const credArgs = buildCredentialArgs(token);
32
+ const execOpts = {
33
+ cwd,
34
+ stdio: (verbose ? 'inherit' : 'pipe'),
35
+ };
36
+ if (verbose) {
37
+ logInfo(`📤 Pushing branch ${branchName} to remote...`);
38
+ }
39
+ // Try 1: push -u (set upstream tracking)
40
+ try {
41
+ execFileSync('git', [...credArgs, 'push', '-u', 'origin', branchName], execOpts);
42
+ return { success: true, method: 'push-u' };
43
+ }
44
+ catch {
45
+ // fall through
46
+ }
47
+ // Try 2: plain push
48
+ try {
49
+ execFileSync('git', [...credArgs, 'push', 'origin', branchName], execOpts);
50
+ return { success: true, method: 'push' };
51
+ }
52
+ catch {
53
+ // fall through
54
+ }
55
+ // Try 3: force push with lease
56
+ if (verbose) {
57
+ logInfo(`⚠️ Push rejected, attempting force push with lease...`);
58
+ }
59
+ try {
60
+ execFileSync('git', [...credArgs, 'push', '--force-with-lease', 'origin', branchName], execOpts);
61
+ if (verbose) {
62
+ logInfo(`✅ Successfully force pushed ${branchName}`);
63
+ }
64
+ return { success: true, method: 'force-with-lease' };
65
+ }
66
+ catch (error) {
67
+ return {
68
+ success: false,
69
+ error: error instanceof Error ? error.message : String(error),
70
+ };
71
+ }
72
+ }
73
+ /**
74
+ * Force push a branch using --force-with-lease.
75
+ * Used after rebase operations to update the remote branch.
76
+ */
77
+ export function gitForcePush(options) {
78
+ const { branchName, token, verbose, cwd } = options;
79
+ const credArgs = buildCredentialArgs(token);
80
+ const execOpts = {
81
+ cwd,
82
+ stdio: (verbose ? 'inherit' : 'pipe'),
83
+ };
84
+ if (verbose) {
85
+ logInfo(`📤 Force pushing ${branchName} to remote...`);
86
+ }
87
+ try {
88
+ execFileSync('git', [...credArgs, 'push', '--force-with-lease', 'origin', branchName], execOpts);
89
+ if (verbose) {
90
+ logInfo(`✅ Successfully force pushed ${branchName}`);
91
+ }
92
+ return { success: true, method: 'force-with-lease' };
93
+ }
94
+ catch (error) {
95
+ return {
96
+ success: false,
97
+ error: error instanceof Error ? error.message : String(error),
98
+ };
99
+ }
100
+ }
101
+ /**
102
+ * Push the current branch to remote.
103
+ * Resolves the current branch name first, then delegates to gitPush.
104
+ */
105
+ export function gitPushCurrentBranch(options) {
106
+ const { token, verbose, cwd } = options;
107
+ let branchName;
108
+ try {
109
+ branchName = execFileSync('git', ['branch', '--show-current'], {
110
+ encoding: 'utf-8',
111
+ cwd,
112
+ stdio: 'pipe',
113
+ }).trim();
114
+ }
115
+ catch (error) {
116
+ return {
117
+ success: false,
118
+ error: `Failed to resolve current branch: ${error instanceof Error ? error.message : String(error)}`,
119
+ };
120
+ }
121
+ if (!branchName) {
122
+ return { success: false, error: 'Could not determine current branch (detached HEAD?)' };
123
+ }
124
+ return gitPush({ branchName, token, verbose, cwd });
125
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.30.0",
3
+ "version": "0.30.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"