@@ -105,17 +105,85 @@ export class PatchClient {
105105 }
106106
107107 try {
108+ // Diagnostic logging for tokens (without exposing the token)
109+ if ( authToken ) {
110+ console . log ( `Auth token provided: ${ authToken . substring ( 0 , 4 ) } ...${ authToken . substring ( authToken . length - 4 ) } (${ authToken . length } chars)` ) ;
111+ console . log ( `Token type: ${ this . identifyTokenType ( authToken ) } ` ) ;
112+ } else {
113+ console . log ( 'No auth token provided for repository access' ) ;
114+ }
115+
108116 // Prepare auth for private repositories if token is provided
109117 let authenticatedRepoUrl = repoUrl ;
110118 if ( authToken ) {
111- // Insert auth token into the URL
119+ // Validate token format
120+ if ( ! this . isValidGitHubToken ( authToken ) ) {
121+ console . warn ( 'Warning: Token format does not match standard GitHub token patterns' ) ;
122+ }
123+
124+ // Use token for authentication but handle differently based on URL format
112125 const url = new URL ( repoUrl ) ;
113- authenticatedRepoUrl = repoUrl . replace ( `${ url . protocol } //` , `${ url . protocol } //${ authToken } @` ) ;
126+
127+ // GitHub-specific handling with multiple auth methods
128+ if ( url . hostname === 'github.com' ) {
129+ // Determine the correct authentication format based on token type
130+ const tokenType = this . identifyTokenType ( authToken ) ;
131+
132+ // Create authentication URLs in order of preference
133+ const authUrls : string [ ] = [ ] ;
134+
135+ if ( tokenType === 'GitHub App Installation Token' ) {
136+ // For GitHub App installation tokens (ghs_*), x-access-token is preferred
137+ authUrls . push ( `https://x-access-token:${ authToken } @github.com${ url . pathname } ` ) ;
138+ authUrls . push ( `https://${ authToken } @github.com${ url . pathname } ` ) ;
139+ } else if ( tokenType === 'OAuth App Token' ) {
140+ // For OAuth tokens, oauth2: prefix is preferred
141+ authUrls . push ( `https://oauth2:${ authToken } @github.com${ url . pathname } ` ) ;
142+ authUrls . push ( `https://${ authToken } @github.com${ url . pathname } ` ) ;
143+ } else {
144+ // For PATs and unknown tokens, try multiple formats
145+ authUrls . push ( `https://${ authToken } @github.com${ url . pathname } ` ) ;
146+ authUrls . push ( `https://x-access-token:${ authToken } @github.com${ url . pathname } ` ) ;
147+ authUrls . push ( `https://oauth2:${ authToken } @github.com${ url . pathname } ` ) ;
148+ }
149+
150+ console . log ( 'Prepared multiple authentication methods for GitHub' ) ;
151+
152+ // Try multiple authentication methods in sequence
153+ let cloned = false ;
154+ let lastError : Error | null = null ;
155+
156+ for ( const authUrl of authUrls ) {
157+ try {
158+ console . log ( 'Attempting GitHub repository clone...' ) ;
159+ await this . attemptRepositoryClone ( authUrl , this . workingDir ) ;
160+ cloned = true ;
161+ console . log ( 'Authentication method succeeded' ) ;
162+ break ;
163+ } catch ( cloneError ) {
164+ const errorMessage = cloneError instanceof Error ? cloneError . message : String ( cloneError ) ;
165+ lastError = cloneError instanceof Error ? cloneError : new Error ( String ( cloneError ) ) ;
166+ console . log ( `Authentication method failed: ${ this . sanitizeErrorMessage ( errorMessage ) } ` ) ;
167+ }
168+ }
169+
170+ // If all methods failed, throw the last error
171+ if ( ! cloned ) {
172+ if ( lastError ) {
173+ throw lastError ;
174+ }
175+ throw new Error ( 'All authentication methods failed. Please verify the token has the "contents: read" permission.' ) ;
176+ }
177+ } else {
178+ // For other Git providers, use their URL structure
179+ authenticatedRepoUrl = repoUrl . replace ( `${ url . protocol } //` , `${ url . protocol } //${ authToken } @` ) ;
180+ await this . attemptRepositoryClone ( authenticatedRepoUrl , this . workingDir ) ;
181+ }
182+ } else {
183+ // No auth token provided - try public access
184+ console . log ( 'Attempting to clone repository without authentication' ) ;
185+ await this . attemptRepositoryClone ( repoUrl , this . workingDir ) ;
114186 }
115-
116- // Clone the repository
117- console . log ( `Cloning repository: ${ repoUrl } to ${ this . workingDir } ` ) ;
118- await execa ( 'git' , [ 'clone' , authenticatedRepoUrl , this . workingDir ] ) ;
119187
120188 // Configure Git for commits
121189 await execa ( 'git' , [ 'config' , 'user.name' , 'patchmycode' ] , { cwd : this . workingDir } ) ;
@@ -252,13 +320,64 @@ export class PatchClient {
252320 // Configure push URL with auth if needed
253321 if ( authToken ) {
254322 const url = new URL ( repoUrl ) ;
255- const authenticatedPushUrl = repoUrl . replace ( `${ url . protocol } //` , `${ url . protocol } //${ authToken } @` ) ;
323+ // Use proper GitHub authentication format based on token type
324+ let authenticatedPushUrl ;
325+
326+ if ( url . hostname === 'github.com' ) {
327+ const tokenType = this . identifyTokenType ( authToken ) ;
328+
329+ if ( tokenType === 'GitHub App Installation Token' ) {
330+ // For GitHub App installation tokens (ghs_*)
331+ authenticatedPushUrl = `https://x-access-token:${ authToken } @github.com${ url . pathname } ` ;
332+ } else if ( tokenType === 'OAuth App Token' ) {
333+ // For OAuth tokens
334+ authenticatedPushUrl = `https://oauth2:${ authToken } @github.com${ url . pathname } ` ;
335+ } else if ( tokenType === 'Personal Access Token' || tokenType === 'Fine-grained Personal Access Token' ) {
336+ // For PATs
337+ authenticatedPushUrl = `https://oauth2:${ authToken } @github.com${ url . pathname } ` ;
338+ } else {
339+ // Fallback for unknown token types - try x-access-token format
340+ authenticatedPushUrl = `https://x-access-token:${ authToken } @github.com${ url . pathname } ` ;
341+ console . log ( 'Using x-access-token format for GitHub authentication' ) ;
342+ }
343+ } else {
344+ // For non-GitHub repositories
345+ authenticatedPushUrl = repoUrl . replace ( `${ url . protocol } //` , `${ url . protocol } //${ authToken } @` ) ;
346+ }
347+
348+ // Apply the authenticated URL for push
349+ console . log ( `Configuring authenticated remote for pushing to ${ url . hostname } ${ url . pathname } ` ) ;
256350 await execa ( 'git' , [ 'remote' , 'set-url' , 'origin' , authenticatedPushUrl ] , { cwd : this . workingDir } ) ;
257351 }
258352
259353 // Push the changes
260354 console . log ( `Pushing changes to branch: ${ branchName } ` ) ;
261- await execa ( 'git' , [ 'push' , 'origin' , branchName ] , { cwd : this . workingDir } ) ;
355+ // Set environment variables for Git to prevent prompting
356+ const pushEnv = {
357+ ...process . env ,
358+ GIT_TERMINAL_PROMPT : '0' ,
359+ GIT_ASKPASS : 'echo' ,
360+ GCM_INTERACTIVE : 'never'
361+ } ;
362+
363+ try {
364+ await execa ( 'git' , [ 'push' , 'origin' , branchName ] , {
365+ cwd : this . workingDir ,
366+ env : pushEnv ,
367+ timeout : 60000 // 1 minute timeout for push
368+ } ) ;
369+ } catch ( pushError ) {
370+ const errorMessage = pushError instanceof Error ? pushError . message : String ( pushError ) ;
371+ console . error ( `Push error (sanitized): ${ this . sanitizeErrorMessage ( errorMessage ) } ` ) ;
372+
373+ if ( errorMessage . includes ( 'could not read Username' ) ||
374+ errorMessage . includes ( 'Authentication failed' ) ||
375+ errorMessage . includes ( '403' ) ||
376+ errorMessage . includes ( '401' ) ) {
377+ throw new Error ( 'Failed to push changes: Authentication error. The token may not have write access to this repository.' ) ;
378+ }
379+ throw new Error ( `Failed to push changes: ${ this . sanitizeErrorMessage ( errorMessage ) } ` ) ;
380+ }
262381
263382 return {
264383 success : true ,
@@ -278,9 +397,13 @@ export class PatchClient {
278397
279398 // Try to get additional information from the error
280399 let errorMessage = error instanceof Error ? error . message : String ( error ) ;
281- const fullError = errorMessage ; // Keep the full error for logging
282400
283- // Log the full error for debugging
401+ // Sanitize error message to remove any tokens
402+ errorMessage = this . sanitizeErrorMessage ( errorMessage ) ;
403+
404+ const fullError = errorMessage ; // Keep the sanitized error for logging
405+
406+ // Log the sanitized error for debugging
284407 console . error ( 'Full Aider error:' , fullError ) ;
285408
286409 // Check for specific error types
@@ -305,12 +428,12 @@ export class PatchClient {
305428 errorMessage = `Aider command line error: ${ errorMessage } \n\nThis may be due to version differences. Try updating Aider: pip install -U aider-chat` ;
306429 }
307430
308- // If using Claude model, add specific advice
431+ // Log Claude-specific advice to console, but don't include it in the user-facing error message
309432 if ( this . isClaudeModel ( ) ) {
310- errorMessage += '\n\nAdditional Claude troubleshooting:\n' +
311- '• Make sure ANTHROPIC_API_KEY is set and valid\n' +
312- '• Try different flags for Claude in AIDER_EXTRA_ARGS (--anthropic or --claude)\n' +
313- '• Check Aider version compatibility with Claude models' ;
433+ console . log ( 'Additional Claude troubleshooting (for developers):' ) ;
434+ console . log ( '• Make sure ANTHROPIC_API_KEY is set and valid' ) ;
435+ console . log ( '• Try different flags for Claude in AIDER_EXTRA_ARGS (--anthropic or --claude)' ) ;
436+ console . log ( '• Check Aider version compatibility with Claude models' ) ;
314437 }
315438
316439 return {
@@ -324,6 +447,29 @@ export class PatchClient {
324447 }
325448 }
326449
450+ /**
451+ * Sanitize error messages to remove sensitive information like tokens
452+ */
453+ private sanitizeErrorMessage ( message : string ) : string {
454+ // Remove any GitHub tokens that might be in error messages
455+ message = message . replace ( / h t t p s : \/ \/ [ ^ @ : ] + : [ ^ @ : ] + @ / g, 'https://' ) ;
456+ message = message . replace ( / h t t p s : \/ \/ [ ^ @ : ] + @ / g, 'https://' ) ;
457+
458+ // Specifically handle GitHub tokens (ghs_*)
459+ message = message . replace ( / g h s _ [ a - z A - Z 0 - 9 ] { 16 , } / g, 'ghs_REDACTED' ) ;
460+
461+ // Remove any filepath with potential tokens
462+ message = message . replace ( / c l o n e \s + [ ' " ] h t t p s : \/ \/ .* ?@ .* ?[ ' " ] / , 'clone [REPOSITORY_URL]' ) ;
463+ message = message . replace ( / g i t c l o n e \s + ' [ ^ ' ] * ' / g, 'git clone [REPOSITORY_URL]' ) ;
464+ message = message . replace ( / g i t c l o n e \s + " [ ^ " ] * " / g, 'git clone [REPOSITORY_URL]' ) ;
465+
466+ // Remove any API keys that might be in the message
467+ message = message . replace ( / k e y [ - _ ] [ a - z A - Z 0 - 9 ] { 20 , } / g, 'key-REDACTED' ) ;
468+ message = message . replace ( / s k [ - _ ] [ a - z A - Z 0 - 9 ] { 20 , } / g, 'sk-REDACTED' ) ;
469+
470+ return message ;
471+ }
472+
327473 /**
328474 * Clean up temporary directory
329475 */
@@ -338,4 +484,91 @@ export class PatchClient {
338484 }
339485 }
340486 }
487+
488+ /**
489+ * Validate GitHub token format
490+ */
491+ private isValidGitHubToken ( token : string ) : boolean {
492+ // Check for common GitHub token formats
493+ const githubPAT = / ^ g h p _ [ a - z A - Z 0 - 9 ] { 20 , } $ / ; // Personal Access Token
494+ const githubOAuth = / ^ g h o _ [ a - z A - Z 0 - 9 ] { 20 , } $ / ; // OAuth Access Token
495+ const githubInstall = / ^ g h s _ [ a - z A - Z 0 - 9 ] { 20 , } $ / ; // GitHub App Installation Token
496+ const githubUser = / ^ g i t h u b _ p a t _ [ a - z A - Z 0 - 9 _ ] { 20 , } $ / ; // Fine-grained PAT
497+
498+ // If it matches a known pattern, great
499+ if ( githubPAT . test ( token ) ||
500+ githubOAuth . test ( token ) ||
501+ githubInstall . test ( token ) ||
502+ githubUser . test ( token ) ) {
503+ return true ;
504+ }
505+
506+ // Otherwise, check for basic requirements (some minimum length and no whitespace)
507+ // This is to accommodate different token formats while still catching obvious errors
508+ return token . length >= 10 && ! / \s / . test ( token ) ;
509+ }
510+
511+ /**
512+ * Attempts to clone a repository with proper error handling
513+ */
514+ private async attemptRepositoryClone ( repoUrl : string , targetDir : string ) : Promise < void > {
515+ // Safely log the clone attempt without exposing tokens
516+ const safeUrl = repoUrl . replace ( / \/ \/ [ ^ @ ] + @ / , '//' ) . replace ( / \/ \/ [ ^ @ : ] + : [ ^ @ : ] + @ / , '//' ) ;
517+ console . log ( `Cloning repository from ${ safeUrl } to ${ targetDir } ` ) ;
518+
519+ try {
520+ // Setup environment for git
521+ const cloneEnv = {
522+ ...process . env ,
523+ GIT_TERMINAL_PROMPT : '0' ,
524+ GIT_ASKPASS : 'echo' ,
525+ GCM_INTERACTIVE : 'never' // Disable GitHub credential manager interactive prompts
526+ } ;
527+
528+ // Clone with credentials in environment and URL
529+ await execa ( 'git' , [ 'clone' , repoUrl , targetDir , '--depth' , '1' ] , {
530+ env : cloneEnv ,
531+ stdio : [ 'ignore' , 'pipe' , 'pipe' ] ,
532+ timeout : 60000 // 1 minute timeout for clone
533+ } ) ;
534+
535+ console . log ( 'Repository clone successful' ) ;
536+ } catch ( cloneError ) {
537+ const errorMessage = cloneError instanceof Error ? cloneError . message : String ( cloneError ) ;
538+
539+ // Check for common auth errors
540+ if ( errorMessage . includes ( 'Authentication failed' ) ||
541+ errorMessage . includes ( 'Invalid username or password' ) ||
542+ errorMessage . includes ( 'could not read Username' ) ||
543+ errorMessage . includes ( '403' ) ||
544+ errorMessage . includes ( '401' ) ) {
545+
546+ // Provide specific error for permission issues
547+ if ( errorMessage . includes ( 'Permission to' ) && errorMessage . includes ( 'denied' ) ) {
548+ throw new Error ( 'Permission denied. The token does not have access to this repository. Check that it has "contents: read" permission.' ) ;
549+ }
550+
551+ throw new Error ( 'Authentication failed. Token may be invalid or missing required permissions (needs "contents: read" at minimum).' ) ;
552+ }
553+
554+ // For timeout errors
555+ if ( errorMessage . includes ( 'timed out' ) || errorMessage . includes ( 'ETIMEDOUT' ) ) {
556+ throw new Error ( 'Repository clone timed out. Check network connectivity or repository size.' ) ;
557+ }
558+
559+ // For other errors, provide the sanitized message
560+ throw new Error ( `Repository clone failed: ${ this . sanitizeErrorMessage ( errorMessage ) } ` ) ;
561+ }
562+ }
563+
564+ /**
565+ * Identify the type of token provided
566+ */
567+ private identifyTokenType ( token : string ) : string {
568+ if ( token . startsWith ( 'ghp_' ) ) return 'Personal Access Token' ;
569+ if ( token . startsWith ( 'gho_' ) ) return 'OAuth App Token' ;
570+ if ( token . startsWith ( 'ghs_' ) ) return 'GitHub App Installation Token' ;
571+ if ( token . startsWith ( 'github_pat_' ) ) return 'Fine-grained Personal Access Token' ;
572+ return 'Unknown Token Type' ;
573+ }
341574}
0 commit comments