@@ -187,10 +187,22 @@ breaks the first time a hint or a colour is added, and breaking here means telli
187187approved in their browser that it failed.
188188*/
189189pub fn token_in ( output : & str ) -> Option < String > {
190- plain ( output)
191- . split ( |c : char | c. is_whitespace ( ) || c == '"' || c == '\'' )
192- . map ( |word| word. trim_matches ( |c : char | !c. is_ascii_alphanumeric ( ) && c != '-' && c != '_' ) )
193- . find ( |word| word. starts_with ( PLAN_TOKEN_PREFIX ) && word. len ( ) > 30 )
190+ fn token_character ( c : char ) -> bool {
191+ c. is_ascii_alphanumeric ( ) || c == '-' || c == '_'
192+ }
193+
194+ let text = plain ( output) ;
195+ // Cursor positioning can separate a label from its token visually without a space
196+ // in the stream. Accept the prefix after punctuation, but not inside another word.
197+ text. match_indices ( PLAN_TOKEN_PREFIX )
198+ . filter ( |( at, _) | !matches ! ( text[ ..* at] . chars( ) . next_back( ) , Some ( c) if token_character( c) ) )
199+ . map ( |( at, _) | {
200+ text[ at..]
201+ . split ( |c| !token_character ( c) )
202+ . next ( )
203+ . unwrap_or ( "" )
204+ } )
205+ . find ( |token| token. len ( ) > 30 )
194206 . map ( str:: to_string)
195207}
196208
@@ -773,12 +785,18 @@ mod tests {
773785 let source = root. join ( "podman.rs" ) ;
774786 std:: fs:: write (
775787 & source,
776- r#"use std::io::{Read, Write} ;
788+ r#"use std::io::Write;
777789 fn main() {
778790 print!("\x1b]8;;https://claude.ai/oauth/authorize?synthetic=terminal-lifetime\x1b\\Sign in\x1b]8;;\x1b\\\r\n");
791+ println!("Paste code here if prompted");
779792 std::io::stdout().flush().unwrap();
780- let mut input = Vec::new();
781- std::io::stdin().read_to_end(&mut input).unwrap();
793+ let mut input = String::new();
794+ std::io::stdin().read_line(&mut input).unwrap();
795+ assert_eq!(input.trim(), format!("{}#{}", "c".repeat(43), "s".repeat(48)));
796+ print!("Your OAuth token (valid for 1 year):");
797+ std::io::stdout().flush().unwrap();
798+ std::thread::sleep(std::time::Duration::from_millis(60));
799+ println!("\x1b[40G\x1b[32msk-ant-oat01-{}\x1b[0m", "s".repeat(95));
782800 }"# ,
783801 )
784802 . unwrap ( ) ;
@@ -803,8 +821,11 @@ mod tests {
803821 "the login child must survive until the code can be supplied"
804822 ) ;
805823 let draining = std:: sync:: Arc :: downgrade ( & signing. output ) ;
806- signing. stop ( ) ;
807- drop ( signing) ;
824+ let code = format ! ( "{}#{}" , "c" . repeat( 43 ) , "s" . repeat( 48 ) ) ;
825+ assert_eq ! (
826+ signing. finish( & code) ?,
827+ format!( "sk-ant-oat01-{}" , "s" . repeat( 95 ) )
828+ ) ;
808829 // Modern ClosePseudoConsole returns before its clients disconnect. The
809830 // reader's EOF, not the master's drop, marks completed console cleanup.
810831 // Keep this inside the deadline before deleting the fixture executable.
@@ -1034,6 +1055,16 @@ mod tests {
10341055 assert_eq ! ( authorize_url_in( "" ) , None ) ;
10351056 }
10361057
1058+ #[ test]
1059+ fn token_after_a_cursor_positioned_label_is_found_in_full ( ) {
1060+ let token = format ! ( "{PLAN_TOKEN_PREFIX}01-{}" , "s" . repeat( 95 ) ) ;
1061+ let output = format ! (
1062+ "Your OAuth token (valid for 1 year):\x1b [1G\x1b [32m{token}\x1b [0m\n Store this token safely."
1063+ ) ;
1064+ assert_eq ! ( token_in( & output) , Some ( token. clone( ) ) ) ;
1065+ assert_eq ! ( token_in( & format!( "other-{token}" ) ) , None ) ;
1066+ }
1067+
10371068 /// The stripper has to survive what a TUI actually emits, including a bare ESC pair.
10381069 #[ test]
10391070 fn escapes_come_out_and_the_words_stay ( ) {
0 commit comments