@@ -91,19 +91,68 @@ function clearWarmupAttempt(sessionId: string): void {
9191 warmupAttemptedSessionIds . delete ( sessionId ) ;
9292}
9393
94- async function openBrowser ( url : string ) : Promise < void > {
94+ function isWSL ( ) : boolean {
95+ if ( process . platform !== "linux" ) return false ;
96+ try {
97+ const { readFileSync } = require ( "node:fs" ) ;
98+ const release = readFileSync ( "/proc/version" , "utf8" ) . toLowerCase ( ) ;
99+ return release . includes ( "microsoft" ) || release . includes ( "wsl" ) ;
100+ } catch {
101+ return false ;
102+ }
103+ }
104+
105+ function isWSL2 ( ) : boolean {
106+ if ( ! isWSL ( ) ) return false ;
107+ try {
108+ const { readFileSync } = require ( "node:fs" ) ;
109+ const version = readFileSync ( "/proc/version" , "utf8" ) . toLowerCase ( ) ;
110+ return version . includes ( "wsl2" ) || version . includes ( "microsoft-standard" ) ;
111+ } catch {
112+ return false ;
113+ }
114+ }
115+
116+ function isRemoteEnvironment ( ) : boolean {
117+ if ( process . env . SSH_CLIENT || process . env . SSH_TTY || process . env . SSH_CONNECTION ) {
118+ return true ;
119+ }
120+ if ( process . env . REMOTE_CONTAINERS || process . env . CODESPACES ) {
121+ return true ;
122+ }
123+ if ( process . platform === "linux" && ! process . env . DISPLAY && ! process . env . WAYLAND_DISPLAY && ! isWSL ( ) ) {
124+ return true ;
125+ }
126+ return false ;
127+ }
128+
129+ function shouldSkipLocalServer ( ) : boolean {
130+ return isWSL2 ( ) || isRemoteEnvironment ( ) ;
131+ }
132+
133+ async function openBrowser ( url : string ) : Promise < boolean > {
95134 try {
96135 if ( process . platform === "darwin" ) {
97136 exec ( `open "${ url } "` ) ;
98- return ;
137+ return true ;
99138 }
100139 if ( process . platform === "win32" ) {
101- exec ( `start "${ url } "` ) ;
102- return ;
140+ exec ( `start "" "${ url } "` ) ;
141+ return true ;
142+ }
143+ if ( isWSL ( ) ) {
144+ try {
145+ exec ( `wslview "${ url } "` ) ;
146+ return true ;
147+ } catch { }
148+ }
149+ if ( ! process . env . DISPLAY && ! process . env . WAYLAND_DISPLAY ) {
150+ return false ;
103151 }
104152 exec ( `xdg-open "${ url } "` ) ;
153+ return true ;
105154 } catch {
106- // ignore
155+ return false ;
107156 }
108157}
109158
@@ -168,6 +217,24 @@ function parseOAuthCallbackInput(
168217 }
169218}
170219
220+ async function promptManualOAuthInput (
221+ fallbackState : string ,
222+ ) : Promise < AntigravityTokenExchangeResult > {
223+ console . log ( "1. Open the URL above in your browser and complete Google sign-in." ) ;
224+ console . log ( "2. After approving, copy the full redirected localhost URL from the address bar." ) ;
225+ console . log ( "3. Paste it back here.\n" ) ;
226+
227+ const callbackInput = await promptOAuthCallbackValue (
228+ "Paste the redirect URL (or just the code) here: " ,
229+ ) ;
230+ const params = parseOAuthCallbackInput ( callbackInput , fallbackState ) ;
231+ if ( "error" in params ) {
232+ return { type : "failed" , error : params . error } ;
233+ }
234+
235+ return exchangeAntigravity ( params . code , params . state ) ;
236+ }
237+
171238function clampInt ( value : number , min : number , max : number ) : number {
172239 if ( ! Number . isFinite ( value ) ) {
173240 return min ;
@@ -1481,6 +1548,8 @@ export const createAntigravityPlugin = (providerId: string) => async (
14811548 // CLI flow (`opencode auth login`) passes an inputs object.
14821549 if ( inputs ) {
14831550 const accounts : Array < Extract < AntigravityTokenExchangeResult , { type : "success" } > > = [ ] ;
1551+ const noBrowser = inputs . noBrowser === "true" || inputs [ "no-browser" ] === "true" ;
1552+ const useManualMode = noBrowser || shouldSkipLocalServer ( ) ;
14841553
14851554 // Check for existing accounts and prompt user for login mode
14861555 let startFresh = true ;
@@ -1507,6 +1576,20 @@ export const createAntigravityPlugin = (providerId: string) => async (
15071576 const projectId = await promptProjectId ( ) ;
15081577
15091578 const result = await ( async ( ) : Promise < AntigravityTokenExchangeResult > => {
1579+ const authorization = await authorizeAntigravity ( projectId ) ;
1580+ const fallbackState = getStateFromAuthorizationUrl ( authorization . url ) ;
1581+
1582+ console . log ( "\nOAuth URL:\n" + authorization . url + "\n" ) ;
1583+
1584+ if ( useManualMode ) {
1585+ const browserOpened = await openBrowser ( authorization . url ) ;
1586+ if ( ! browserOpened ) {
1587+ console . log ( "Could not open browser automatically." ) ;
1588+ console . log ( "Please open the URL above manually in your local browser.\n" ) ;
1589+ }
1590+ return promptManualOAuthInput ( fallbackState ) ;
1591+ }
1592+
15101593 let listener : OAuthListener | null = null ;
15111594 if ( ! isHeadless ) {
15121595 try {
@@ -1516,53 +1599,62 @@ export const createAntigravityPlugin = (providerId: string) => async (
15161599 }
15171600 }
15181601
1519- const authorization = await authorizeAntigravity ( projectId ) ;
1520- const fallbackState = getStateFromAuthorizationUrl ( authorization . url ) ;
1521-
1522- console . log ( "\nOAuth URL:\n" + authorization . url + "\n" ) ;
1523-
15241602 if ( ! isHeadless ) {
15251603 await openBrowser ( authorization . url ) ;
15261604 }
15271605
15281606 if ( listener ) {
15291607 try {
1530- const callbackUrl = await listener . waitForCallback ( ) ;
1608+ const SOFT_TIMEOUT_MS = 30000 ;
1609+ const callbackPromise = listener . waitForCallback ( ) ;
1610+ const timeoutPromise = new Promise < never > ( ( _ , reject ) =>
1611+ setTimeout ( ( ) => reject ( new Error ( "SOFT_TIMEOUT" ) ) , SOFT_TIMEOUT_MS )
1612+ ) ;
1613+
1614+ let callbackUrl : URL ;
1615+ try {
1616+ callbackUrl = await Promise . race ( [ callbackPromise , timeoutPromise ] ) ;
1617+ } catch ( err ) {
1618+ if ( err instanceof Error && err . message === "SOFT_TIMEOUT" ) {
1619+ console . log ( "\n⏳ Automatic callback not received after 30 seconds." ) ;
1620+ console . log ( "You can paste the redirect URL manually.\n" ) ;
1621+ console . log ( "OAuth URL (in case you need it again):" ) ;
1622+ console . log ( authorization . url + "\n" ) ;
1623+
1624+ try {
1625+ await listener . close ( ) ;
1626+ } catch { }
1627+
1628+ return promptManualOAuthInput ( fallbackState ) ;
1629+ }
1630+ throw err ;
1631+ }
1632+
15311633 const params = extractOAuthCallbackParams ( callbackUrl ) ;
15321634 if ( ! params ) {
15331635 return { type : "failed" , error : "Missing code or state in callback URL" } ;
15341636 }
15351637
15361638 return exchangeAntigravity ( params . code , params . state ) ;
15371639 } catch ( error ) {
1640+ if ( error instanceof Error && error . message !== "SOFT_TIMEOUT" ) {
1641+ return {
1642+ type : "failed" ,
1643+ error : error . message ,
1644+ } ;
1645+ }
15381646 return {
15391647 type : "failed" ,
15401648 error : error instanceof Error ? error . message : "Unknown error" ,
15411649 } ;
15421650 } finally {
15431651 try {
15441652 await listener . close ( ) ;
1545- } catch {
1546- // ignore
1547- }
1653+ } catch { }
15481654 }
15491655 }
15501656
1551- console . log ( "1. Open the URL below in your browser and complete Google sign-in." ) ;
1552- console . log (
1553- "2. After approving, copy the full redirected localhost URL from the address bar." ,
1554- ) ;
1555- console . log ( "3. Paste it back here." ) ;
1556-
1557- const callbackInput = await promptOAuthCallbackValue (
1558- "Paste the redirect URL (or just the code) here: " ,
1559- ) ;
1560- const params = parseOAuthCallbackInput ( callbackInput , fallbackState ) ;
1561- if ( "error" in params ) {
1562- return { type : "failed" , error : params . error } ;
1563- }
1564-
1565- return exchangeAntigravity ( params . code , params . state ) ;
1657+ return promptManualOAuthInput ( fallbackState ) ;
15661658 } ) ( ) ;
15671659
15681660 if ( result . type === "failed" ) {
@@ -1662,8 +1754,10 @@ export const createAntigravityPlugin = (providerId: string) => async (
16621754 const existingStorage = await loadAccounts ( ) ;
16631755 const existingCount = existingStorage ?. accounts . length ?? 0 ;
16641756
1757+ const useManualFlow = isHeadless || shouldSkipLocalServer ( ) ;
1758+
16651759 let listener : OAuthListener | null = null ;
1666- if ( ! isHeadless ) {
1760+ if ( ! useManualFlow ) {
16671761 try {
16681762 listener = await startOAuthListener ( ) ;
16691763 } catch {
@@ -1674,8 +1768,12 @@ export const createAntigravityPlugin = (providerId: string) => async (
16741768 const authorization = await authorizeAntigravity ( projectId ) ;
16751769 const fallbackState = getStateFromAuthorizationUrl ( authorization . url ) ;
16761770
1677- if ( ! isHeadless ) {
1678- await openBrowser ( authorization . url ) ;
1771+ if ( ! useManualFlow ) {
1772+ const browserOpened = await openBrowser ( authorization . url ) ;
1773+ if ( ! browserOpened ) {
1774+ listener ?. close ( ) . catch ( ( ) => { } ) ;
1775+ listener = null ;
1776+ }
16791777 }
16801778
16811779 if ( listener ) {
@@ -1685,8 +1783,26 @@ export const createAntigravityPlugin = (providerId: string) => async (
16851783 "Complete sign-in in your browser. We'll automatically detect the redirect back to localhost." ,
16861784 method : "auto" ,
16871785 callback : async ( ) : Promise < AntigravityTokenExchangeResult > => {
1786+ const CALLBACK_TIMEOUT_MS = 30000 ;
16881787 try {
1689- const callbackUrl = await listener . waitForCallback ( ) ;
1788+ const callbackPromise = listener . waitForCallback ( ) ;
1789+ const timeoutPromise = new Promise < never > ( ( _ , reject ) =>
1790+ setTimeout ( ( ) => reject ( new Error ( "CALLBACK_TIMEOUT" ) ) , CALLBACK_TIMEOUT_MS ) ,
1791+ ) ;
1792+
1793+ let callbackUrl : URL ;
1794+ try {
1795+ callbackUrl = await Promise . race ( [ callbackPromise , timeoutPromise ] ) ;
1796+ } catch ( err ) {
1797+ if ( err instanceof Error && err . message === "CALLBACK_TIMEOUT" ) {
1798+ return {
1799+ type : "failed" ,
1800+ error : "Callback timeout - please use CLI with --no-browser flag for manual input" ,
1801+ } ;
1802+ }
1803+ throw err ;
1804+ }
1805+
16901806 const params = extractOAuthCallbackParams ( callbackUrl ) ;
16911807 if ( ! params ) {
16921808 return { type : "failed" , error : "Missing code or state in callback URL" } ;
@@ -1695,13 +1811,10 @@ export const createAntigravityPlugin = (providerId: string) => async (
16951811 const result = await exchangeAntigravity ( params . code , params . state ) ;
16961812 if ( result . type === "success" ) {
16971813 try {
1698- // TUI flow adds to existing accounts (non-destructive)
16991814 await persistAccountPool ( [ result ] , false ) ;
17001815 } catch {
1701- // ignore
17021816 }
17031817
1704- // Show appropriate toast message
17051818 const newTotal = existingCount + 1 ;
17061819 const toastMessage = existingCount > 0
17071820 ? `Added account${ result . email ? ` (${ result . email } )` : "" } - ${ newTotal } total`
@@ -1715,7 +1828,6 @@ export const createAntigravityPlugin = (providerId: string) => async (
17151828 } ,
17161829 } ) ;
17171830 } catch {
1718- // TUI may not be available
17191831 }
17201832 }
17211833
@@ -1729,7 +1841,6 @@ export const createAntigravityPlugin = (providerId: string) => async (
17291841 try {
17301842 await listener . close ( ) ;
17311843 } catch {
1732- // ignore
17331844 }
17341845 }
17351846 } ,
0 commit comments