@@ -179,139 +179,140 @@ async function execNpmInteractive(
179179 options : ExecNpmOptions = { } ,
180180) : Promise < NpmExecResult > {
181181 const openUrls = options . openUrls === true
182+ const { promise, resolve } = Promise . withResolvers < NpmExecResult > ( )
182183
183184 // Lazy-load node-pty so the native addon is only required when interactive mode is actually used.
184185 const pty = await import ( '@lydell/node-pty' )
185186
186- return new Promise ( resolve => {
187- const npmArgs = options . otp ? [ ...args , '--otp' , options . otp ] : args
187+ const npmArgs = options . otp ? [ ...args , '--otp' , options . otp ] : args
188188
189- if ( ! options . silent ) {
190- const displayCmd = options . otp
191- ? [ 'npm' , ...args , '--otp' , '******' ] . join ( ' ' )
192- : [ 'npm' , ...args ] . join ( ' ' )
193- logCommand ( `${ displayCmd } (interactive/pty)` )
194- }
189+ if ( ! options . silent ) {
190+ const displayCmd = options . otp
191+ ? [ 'npm' , ...args , '--otp' , '******' ] . join ( ' ' )
192+ : [ 'npm' , ...args ] . join ( ' ' )
193+ logCommand ( `${ displayCmd } (interactive/pty)` )
194+ }
195195
196- let output = ''
197- let resolved = false
198- let otpPromptSeen = false
199- let authUrlSeen = false
200- let enterSent = false
201- let authUrlTimeout : ReturnType < typeof setTimeout > | null = null
202- let authUrlTimedOut = false
196+ let output = ''
197+ let resolved = false
198+ let otpPromptSeen = false
199+ let authUrlSeen = false
200+ let enterSent = false
201+ let authUrlTimeout : ReturnType < typeof setTimeout > | null = null
202+ let authUrlTimedOut = false
203203
204- const env = createNpmEnv ( )
204+ const env = createNpmEnv ( )
205205
206- // When openUrls is false, tell npm not to open the browser.
207- // npm still prints the auth URL and polls doneUrl
208- if ( ! openUrls ) {
209- env . npm_config_browser = 'false'
210- }
206+ // When openUrls is false, tell npm not to open the browser.
207+ // npm still prints the auth URL and polls doneUrl
208+ if ( ! openUrls ) {
209+ env . npm_config_browser = 'false'
210+ }
211211
212- const child = pty . spawn ( 'npm' , npmArgs , {
213- name : 'xterm-256color' ,
214- cols : 120 ,
215- rows : 30 ,
216- env,
217- } )
212+ const child = pty . spawn ( 'npm' , npmArgs , {
213+ name : 'xterm-256color' ,
214+ cols : 120 ,
215+ rows : 30 ,
216+ env,
217+ } )
218218
219- // General timeout: 5 minutes (covers non-auth interactive commands)
220- const timeout = setTimeout ( ( ) => {
221- if ( resolved ) return
222- logDebug ( 'Interactive command timed out' , { output } )
223- child . kill ( )
224- } , 300000 )
225-
226- child . onData ( ( data : string ) => {
227- output += data
228- const clean = stripAnsi ( data )
229- logDebug ( 'pty data:' , { text : clean . trim ( ) } )
230-
231- const cleanAll = stripAnsi ( output )
232-
233- // Detect auth URL in output and notify the caller.
234- if ( ! authUrlSeen ) {
235- const urlMatch = cleanAll . match ( AUTH_URL_TITLE_RE )
236-
237- if ( urlMatch && urlMatch [ 1 ] ) {
238- authUrlSeen = true
239- const authUrl = urlMatch [ 1 ] . replace ( / [ . , ; : ! ? ) ] + $ / , '' )
240- logDebug ( 'Auth URL detected:' , { authUrl, openUrls } )
241- options . onAuthUrl ?.( authUrl )
242-
243- authUrlTimeout = setTimeout ( ( ) => {
244- if ( resolved ) return
245- authUrlTimedOut = true
246- logDebug ( 'Auth URL timeout (90s) — killing process' )
247- logError ( 'Authentication timed out after 90 seconds' )
248- child . kill ( )
249- } , AUTH_URL_TIMEOUT_MS )
250- }
219+ // General timeout: 5 minutes (covers non-auth interactive commands)
220+ const timeout = setTimeout ( ( ) => {
221+ if ( resolved ) return
222+ logDebug ( 'Interactive command timed out' , { output } )
223+ child . kill ( )
224+ } , 300000 )
225+
226+ child . onData ( ( data : string ) => {
227+ output += data
228+ const clean = stripAnsi ( data )
229+ logDebug ( 'pty data:' , { text : clean . trim ( ) } )
230+
231+ const cleanAll = stripAnsi ( output )
232+
233+ // Detect auth URL in output and notify the caller.
234+ if ( ! authUrlSeen ) {
235+ const urlMatch = cleanAll . match ( AUTH_URL_TITLE_RE )
236+
237+ if ( urlMatch && urlMatch [ 1 ] ) {
238+ authUrlSeen = true
239+ const authUrl = urlMatch [ 1 ] . replace ( / [ . , ; : ! ? ) ] + $ / , '' )
240+ logDebug ( 'Auth URL detected:' , { authUrl, openUrls } )
241+ options . onAuthUrl ?.( authUrl )
242+
243+ authUrlTimeout = setTimeout ( ( ) => {
244+ if ( resolved ) return
245+ authUrlTimedOut = true
246+ logDebug ( 'Auth URL timeout (90s) — killing process' )
247+ logError ( 'Authentication timed out after 90 seconds' )
248+ child . kill ( )
249+ } , AUTH_URL_TIMEOUT_MS )
251250 }
251+ }
252252
253- if ( authUrlSeen && openUrls && ! enterSent && AUTH_URL_PROMPT_RE . test ( cleanAll ) ) {
254- enterSent = true
255- logDebug ( 'Web auth prompt detected, pressing ENTER' )
256- child . write ( '\r' )
257- }
253+ if ( authUrlSeen && openUrls && ! enterSent && AUTH_URL_PROMPT_RE . test ( cleanAll ) ) {
254+ enterSent = true
255+ logDebug ( 'Web auth prompt detected, pressing ENTER' )
256+ child . write ( '\r' )
257+ }
258258
259- if ( ! otpPromptSeen && OTP_PROMPT_RE . test ( cleanAll ) ) {
260- otpPromptSeen = true
261- if ( options . otp ) {
262- logDebug ( 'OTP prompt detected, writing OTP' )
263- child . write ( options . otp + '\r' )
264- } else {
265- logDebug ( 'OTP prompt detected but no OTP provided, killing process' )
266- child . kill ( )
267- }
259+ if ( ! otpPromptSeen && OTP_PROMPT_RE . test ( cleanAll ) ) {
260+ otpPromptSeen = true
261+ if ( options . otp ) {
262+ logDebug ( 'OTP prompt detected, writing OTP' )
263+ child . write ( options . otp + '\r' )
264+ } else {
265+ logDebug ( 'OTP prompt detected but no OTP provided, killing process' )
266+ child . kill ( )
268267 }
269- } )
268+ }
269+ } )
270+
271+ child . onExit ( ( { exitCode } ) => {
272+ if ( resolved ) return
273+ resolved = true
274+ clearTimeout ( timeout )
275+ if ( authUrlTimeout ) clearTimeout ( authUrlTimeout )
276+
277+ const cleanOutput = stripAnsi ( output )
278+ logDebug ( 'Interactive command exited:' , { exitCode, output : cleanOutput } )
279+
280+ const requiresOtp =
281+ authUrlTimedOut || ( otpPromptSeen && ! options . otp ) || detectOtpRequired ( cleanOutput )
282+ const authFailure = detectAuthFailure ( cleanOutput )
283+ const urls = extractUrls ( cleanOutput )
270284
271- child . onExit ( ( { exitCode } ) => {
272- if ( resolved ) return
273- resolved = true
274- clearTimeout ( timeout )
275- if ( authUrlTimeout ) clearTimeout ( authUrlTimeout )
276-
277- const cleanOutput = stripAnsi ( output )
278- logDebug ( 'Interactive command exited:' , { exitCode, output : cleanOutput } )
279-
280- const requiresOtp =
281- authUrlTimedOut || ( otpPromptSeen && ! options . otp ) || detectOtpRequired ( cleanOutput )
282- const authFailure = detectAuthFailure ( cleanOutput )
283- const urls = extractUrls ( cleanOutput )
284-
285- if ( ! options . silent ) {
286- if ( exitCode === 0 ) {
287- logSuccess ( 'Done' )
288- } else if ( requiresOtp ) {
289- logError ( 'OTP required' )
290- } else if ( authFailure ) {
291- logError ( 'Authentication required - please run "npm login" and restart the connector' )
292- } else {
293- const firstLine = filterNpmWarnings ( cleanOutput ) . split ( '\n' ) [ 0 ] || 'Command failed'
294- logError ( firstLine )
295- }
285+ if ( ! options . silent ) {
286+ if ( exitCode === 0 ) {
287+ logSuccess ( 'Done' )
288+ } else if ( requiresOtp ) {
289+ logError ( 'OTP required' )
290+ } else if ( authFailure ) {
291+ logError ( 'Authentication required - please run "npm login" and restart the connector' )
292+ } else {
293+ const firstLine = filterNpmWarnings ( cleanOutput ) . split ( '\n' ) [ 0 ] || 'Command failed'
294+ logError ( firstLine )
296295 }
296+ }
297+
298+ // If auth URL timed out, force a non-zero exit code so it's marked as failed
299+ const finalExitCode = authUrlTimedOut ? 1 : exitCode
297300
298- // If auth URL timed out, force a non-zero exit code so it's marked as failed
299- const finalExitCode = authUrlTimedOut ? 1 : exitCode
300-
301- resolve ( {
302- stdout : cleanOutput . trim ( ) ,
303- stderr : requiresOtp
304- ? 'This operation requires a one-time password (OTP).'
305- : authFailure
306- ? 'Authentication failed. Please run "npm login" and restart the connector.'
307- : filterNpmWarnings ( cleanOutput ) ,
308- exitCode : finalExitCode ,
309- requiresOtp,
310- authFailure,
311- urls : urls . length > 0 ? urls : undefined ,
312- } )
301+ resolve ( {
302+ stdout : cleanOutput . trim ( ) ,
303+ stderr : requiresOtp
304+ ? 'This operation requires a one-time password (OTP).'
305+ : authFailure
306+ ? 'Authentication failed. Please run "npm login" and restart the connector.'
307+ : filterNpmWarnings ( cleanOutput ) ,
308+ exitCode : finalExitCode ,
309+ requiresOtp,
310+ authFailure,
311+ urls : urls . length > 0 ? urls : undefined ,
313312 } )
314313 } )
314+
315+ return promise
315316}
316317
317318async function execNpm ( args : string [ ] , options : ExecNpmOptions = { } ) : Promise < NpmExecResult > {
0 commit comments