@@ -3,6 +3,22 @@ const fs = require("fs");
33const path = require ( "path" ) ;
44const os = require ( "os" ) ;
55
6+ let runPtyCommand = null ;
7+ try {
8+ ( { runPtyCommand } = require ( "./pty-runner" ) ) ;
9+ } catch ( error ) {
10+ console . warn (
11+ "[ClaudeCliDetector] node-pty unavailable, will fall back to external terminal:" ,
12+ error ?. message || error
13+ ) ;
14+ }
15+
16+ const ANSI_REGEX =
17+ // eslint-disable-next-line no-control-regex
18+ / \u001b \[ [ 0 - 9 ; ? ] * [ - / ] * [ @ - ~ ] | \u001b [ @ - _ ] | \u001b \] [ ^ \u0007 ] * \u0007 / g;
19+
20+ const stripAnsi = ( text = "" ) => text . replace ( ANSI_REGEX , "" ) ;
21+
622/**
723 * Claude CLI Detector
824 *
@@ -459,6 +475,247 @@ class ClaudeCliDetector {
459475 note : "This token is from your Claude subscription and allows you to use Claude without API charges." ,
460476 } ;
461477 }
478+
479+ /**
480+ * Extract OAuth token from command output
481+ * Tries multiple patterns to find the token
482+ * @param {string } output The command output
483+ * @returns {string|null } Extracted token or null
484+ */
485+ static extractTokenFromOutput ( output ) {
486+ // Pattern 1: CLAUDE_CODE_OAUTH_TOKEN=<token> or CLAUDE_CODE_OAUTH_TOKEN: <token>
487+ const envMatch = output . match (
488+ / C L A U D E _ C O D E _ O A U T H _ T O K E N [ = : ] \s * [ " ' ] ? ( [ a - z A - Z 0 - 9 _ \- \. ] + ) [ " ' ] ? / i
489+ ) ;
490+ if ( envMatch ) return envMatch [ 1 ] ;
491+
492+ // Pattern 2: "Token: <token>" or "token: <token>"
493+ const tokenLabelMatch = output . match (
494+ / \b t o k e n [: \s] + [ " ' ] ? ( [ a - z A - Z 0 - 9 _ \- \. ] { 40 , } ) [ " ' ] ? / i
495+ ) ;
496+ if ( tokenLabelMatch ) return tokenLabelMatch [ 1 ] ;
497+
498+ // Pattern 3: Look for token after success/authenticated message
499+ const successMatch = output . match (
500+ / (?: s u c c e s s | a u t h e n t i c a t e d | g e n e r a t e d | t o k e n i s ) [ ^ \n ] * \n \s * ( [ a - z A - Z 0 - 9 _ \- \. ] { 40 , } ) / i
501+ ) ;
502+ if ( successMatch ) return successMatch [ 1 ] ;
503+
504+ // Pattern 4: Standalone long alphanumeric string on its own line (last resort)
505+ // This catches tokens that are printed on their own line
506+ const lines = output . split ( "\n" ) ;
507+ for ( const line of lines ) {
508+ const trimmed = line . trim ( ) ;
509+ // Token should be 40+ chars, alphanumeric with possible hyphens/underscores/dots
510+ if ( / ^ [ a - z A - Z 0 - 9 _ \- \. ] { 40 , } $ / . test ( trimmed ) ) {
511+ return trimmed ;
512+ }
513+ }
514+
515+ return null ;
516+ }
517+
518+ /**
519+ * Run claude setup-token command to generate OAuth token
520+ * Opens an external terminal window since Claude CLI requires TTY for its Ink-based UI
521+ * @param {Function } onProgress Callback for progress updates
522+ * @returns {Promise<Object> } Result indicating terminal was opened
523+ */
524+ static async runSetupToken ( onProgress ) {
525+ const detection = this . detectClaudeInstallation ( ) ;
526+
527+ if ( ! detection . installed ) {
528+ throw {
529+ success : false ,
530+ error : "Claude CLI is not installed. Please install it first." ,
531+ requiresManualAuth : false ,
532+ } ;
533+ }
534+
535+ const claudePath = detection . path ;
536+ const platform = process . platform ;
537+ const preferPty =
538+ ( platform === "win32" ||
539+ platform === "darwin" ||
540+ process . env . CLAUDE_AUTH_FORCE_PTY === "1" ) &&
541+ process . env . CLAUDE_AUTH_DISABLE_PTY !== "1" ;
542+
543+ const send = ( data ) => {
544+ if ( onProgress && data ) {
545+ onProgress ( { type : "stdout" , data } ) ;
546+ }
547+ } ;
548+
549+ if ( preferPty && runPtyCommand ) {
550+ try {
551+ send ( "Starting in-app terminal session for Claude auth...\n" ) ;
552+ send ( "If your browser opens, complete sign-in and return here.\n\n" ) ;
553+
554+ const ptyResult = await runPtyCommand ( claudePath , [ "setup-token" ] , {
555+ cols : 120 ,
556+ rows : 30 ,
557+ onData : ( chunk ) => send ( chunk ) ,
558+ env : {
559+ FORCE_COLOR : "1" ,
560+ } ,
561+ } ) ;
562+
563+ const cleanedOutput = stripAnsi ( ptyResult . output || "" ) ;
564+ const token = this . extractTokenFromOutput ( cleanedOutput ) ;
565+
566+ if ( ptyResult . success && token ) {
567+ send ( "\nCaptured token automatically.\n" ) ;
568+ return {
569+ success : true ,
570+ token,
571+ requiresManualAuth : false ,
572+ terminalOpened : false ,
573+ } ;
574+ }
575+
576+ if ( ptyResult . success && ! token ) {
577+ send (
578+ "\nCLI completed but token was not detected automatically. You can copy it above or retry.\n"
579+ ) ;
580+ return {
581+ success : true ,
582+ requiresManualAuth : true ,
583+ terminalOpened : false ,
584+ error : "Could not capture token automatically" ,
585+ output : cleanedOutput ,
586+ } ;
587+ }
588+
589+ send (
590+ `\nClaude CLI exited with code ${ ptyResult . exitCode } . Falling back to manual copy.\n`
591+ ) ;
592+ return {
593+ success : false ,
594+ error : `Claude CLI exited with code ${ ptyResult . exitCode } ` ,
595+ requiresManualAuth : true ,
596+ output : cleanedOutput ,
597+ } ;
598+ } catch ( error ) {
599+ console . error ( "[ClaudeCliDetector] PTY auth failed, falling back:" , error ) ;
600+ send (
601+ `In-app terminal failed (${ error ?. message || "unknown error" } ). Falling back to external terminal...\n`
602+ ) ;
603+ }
604+ }
605+
606+ // Fallback: external terminal window
607+ if ( preferPty && ! runPtyCommand ) {
608+ send ( "In-app terminal unavailable (node-pty not loaded)." ) ;
609+ } else if ( ! preferPty ) {
610+ send ( "Using system terminal for authentication on this platform." ) ;
611+ }
612+ send ( "Opening system terminal for authentication...\n" ) ;
613+
614+ // Helper function to check if a command exists asynchronously
615+ const commandExists = ( cmd ) => {
616+ return new Promise ( ( resolve ) => {
617+ require ( "child_process" ) . exec (
618+ `which ${ cmd } ` ,
619+ { timeout : 1000 } ,
620+ ( error ) => {
621+ resolve ( ! error ) ;
622+ }
623+ ) ;
624+ } ) ;
625+ } ;
626+
627+ // For Linux, find available terminal first (async)
628+ let linuxTerminal = null ;
629+ if ( platform !== "win32" && platform !== "darwin" ) {
630+ const terminals = [
631+ [ "gnome-terminal" , [ "--" , claudePath , "setup-token" ] ] ,
632+ [ "konsole" , [ "-e" , claudePath , "setup-token" ] ] ,
633+ [ "xterm" , [ "-e" , claudePath , "setup-token" ] ] ,
634+ [ "x-terminal-emulator" , [ "-e" , `${ claudePath } setup-token` ] ] ,
635+ ] ;
636+
637+ for ( const [ term , termArgs ] of terminals ) {
638+ const exists = await commandExists ( term ) ;
639+ if ( exists ) {
640+ linuxTerminal = { command : term , args : termArgs } ;
641+ break ;
642+ }
643+ }
644+ }
645+
646+ return new Promise ( ( resolve , reject ) => {
647+ // Open command in external terminal since Claude CLI requires TTY
648+ let command , args ;
649+
650+ if ( platform === "win32" ) {
651+ // Windows: Open new cmd window that stays open
652+ command = "cmd" ;
653+ args = [ "/c" , "start" , "cmd" , "/k" , `"${ claudePath } " setup-token` ] ;
654+ } else if ( platform === "darwin" ) {
655+ // macOS: Open Terminal.app
656+ command = "osascript" ;
657+ args = [
658+ "-e" ,
659+ `tell application "Terminal" to do script "${ claudePath } setup-token"` ,
660+ "-e" ,
661+ 'tell application "Terminal" to activate' ,
662+ ] ;
663+ } else {
664+ // Linux: Use the terminal we found earlier
665+ if ( ! linuxTerminal ) {
666+ reject ( {
667+ success : false ,
668+ error :
669+ "Could not find a terminal emulator. Please run 'claude setup-token' manually in your terminal." ,
670+ requiresManualAuth : true ,
671+ } ) ;
672+ return ;
673+ }
674+ command = linuxTerminal . command ;
675+ args = linuxTerminal . args ;
676+ }
677+
678+ console . log (
679+ "[ClaudeCliDetector] Spawning terminal:" ,
680+ command ,
681+ args . join ( " " )
682+ ) ;
683+
684+ const proc = spawn ( command , args , {
685+ detached : true ,
686+ stdio : "ignore" ,
687+ shell : platform === "win32" ,
688+ } ) ;
689+
690+ proc . unref ( ) ;
691+
692+ proc . on ( "error" , ( error ) => {
693+ console . error ( "[ClaudeCliDetector] Failed to open terminal:" , error ) ;
694+ reject ( {
695+ success : false ,
696+ error : `Failed to open terminal: ${ error . message } ` ,
697+ requiresManualAuth : true ,
698+ } ) ;
699+ } ) ;
700+
701+ // Give the terminal a moment to open
702+ setTimeout ( ( ) => {
703+ send ( "Terminal window opened!\n\n" ) ;
704+ send ( "1. Complete the sign-in in your browser\n" ) ;
705+ send ( "2. Copy the token from the terminal\n" ) ;
706+ send ( "3. Paste it below\n" ) ;
707+
708+ // Resolve with manual auth required since we can't capture from external terminal
709+ resolve ( {
710+ success : true ,
711+ requiresManualAuth : true ,
712+ terminalOpened : true ,
713+ message :
714+ "Terminal opened. Complete authentication and paste the token below." ,
715+ } ) ;
716+ } , 500 ) ;
717+ } ) ;
718+ }
462719}
463720
464721module . exports = ClaudeCliDetector ;
0 commit comments