@@ -10,17 +10,6 @@ import { USER_INPUT_TIMEOUT_SECONDS } from './constants.js'; // Import the const
1010// Get the directory name of the current module
1111const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
1212
13- /**
14- * Generate a unique temporary file path
15- * @returns Path to a temporary file
16- */
17- async function getTempFilePath ( ) : Promise < string > {
18- const tempDir = os . tmpdir ( ) ;
19- const randomId = crypto . randomBytes ( 8 ) . toString ( 'hex' ) ;
20- const tempFile = path . join ( tempDir , `cmd-ui-response-${ randomId } .txt` ) ;
21- return tempFile ;
22- }
23-
2413/**
2514 * Display a command window with a prompt and return user input
2615 * @param projectName Name of the project requesting input (used for title)
@@ -38,7 +27,10 @@ export async function getCmdWindowInput(
3827 predefinedOptions ?: string [ ] ,
3928) : Promise < string > {
4029 // Create a temporary file for the detached process to write to
41- const tempFilePath = await getTempFilePath ( ) ;
30+ const sessionId = crypto . randomBytes ( 8 ) . toString ( 'hex' ) ;
31+ const tempDir = os . tmpdir ( ) ;
32+ const tempFilePath = path . join ( tempDir , `cmd-ui-response-${ sessionId } .txt` ) ;
33+ const heartbeatFilePath = path . join ( tempDir , `cmd-ui-heartbeat-${ sessionId } .txt` ) ;
4234 let processExited = false ;
4335
4436 return new Promise < string > ( async ( resolve ) => {
@@ -51,6 +43,7 @@ export async function getCmdWindowInput(
5143 prompt : promptMessage ,
5244 timeout : timeoutSeconds ,
5345 showCountdown,
46+ sessionId,
5447 outputFile : tempFilePath ,
5548 predefinedOptions,
5649 } ;
@@ -127,6 +120,36 @@ export async function getCmdWindowInput(
127120 // Create an empty temp file before watching for user response
128121 await fs . writeFile ( tempFilePath , '' , 'utf8' ) ;
129122
123+ // Wait briefly for the heartbeat file to potentially be created
124+ await new Promise ( res => setTimeout ( res , 500 ) ) ;
125+
126+ let lastHeartbeatTime = Date . now ( ) ;
127+ let heartbeatInterval : NodeJS . Timeout | null = null ;
128+ let heartbeatFileSeen = false ; // Track if we've ever seen the heartbeat file
129+ const startTime = Date . now ( ) ; // Record start time for initial grace period
130+
131+ // Helper function for cleanup and resolution
132+ const cleanupAndResolve = async ( response : string ) => {
133+ if ( heartbeatInterval ) {
134+ clearInterval ( heartbeatInterval ) ;
135+ heartbeatInterval = null ;
136+ }
137+ if ( watcher ) {
138+ watcher . close ( ) ; // Ensure watcher is closed
139+ }
140+ if ( timeoutHandle ) {
141+ clearTimeout ( timeoutHandle ) ; // Ensure timeout is cleared
142+ }
143+
144+ // Use Promise.allSettled to attempt cleanup without failing if one file is missing
145+ await Promise . allSettled ( [
146+ fs . unlink ( tempFilePath ) . catch ( ( ) => { } ) , // Ignore errors
147+ fs . unlink ( heartbeatFilePath ) . catch ( ( ) => { } ) // Ignore errors
148+ ] ) ;
149+
150+ resolve ( response ) ;
151+ } ;
152+
130153 // Watch for content being written to the temp file
131154 const watcher = watch ( tempFilePath , async ( eventType : string ) => {
132155 if ( eventType === 'change' ) {
@@ -136,33 +159,51 @@ export async function getCmdWindowInput(
136159 const response = data . trim ( ) ;
137160 watcher . close ( ) ;
138161 clearTimeout ( timeoutHandle ) ;
162+ if ( heartbeatInterval ) clearInterval ( heartbeatInterval ) ;
139163 cleanupAndResolve ( response ) ;
140164 }
141165 }
142166 } ) ;
143167
144- // Timeout to stop watching if no response within limit
145- const timeoutHandle = setTimeout (
146- ( ) => {
147- watcher . close ( ) ;
148- cleanupAndResolve ( '' ) ;
149- } ,
150- timeoutSeconds * 1000 + 5000 ,
151- ) ;
152-
153- // Helper function for cleanup and resolution
154- const cleanupAndResolve = async ( response : string ) => {
155- // Clean up the temporary file if it exists
168+ // Start heartbeat check interval
169+ heartbeatInterval = setInterval ( async ( ) => {
156170 try {
157- await fs . unlink ( tempFilePath ) ;
171+ const stats = await fs . stat ( heartbeatFilePath ) ;
172+ const now = Date . now ( ) ;
173+ // If file hasn't been modified in the last 3 seconds, assume dead
174+ if ( now - stats . mtime . getTime ( ) > 3000 ) {
175+ // console.log('Heartbeat expired.');
176+ cleanupAndResolve ( '' ) ;
177+ } else {
178+ lastHeartbeatTime = now ; // Update last known good time
179+ heartbeatFileSeen = true ; // Mark that we've seen the file
180+ }
158181 } catch ( err : any ) {
159- // Ignore if file is already removed, otherwise log unexpected errors
160- if ( err . code !== 'ENOENT' ) {
161- // console.error('Error deleting temp file:', err);
182+ if ( err . code === 'ENOENT' ) {
183+ // File not found
184+ if ( heartbeatFileSeen ) {
185+ // File existed before but is now gone, assume dead
186+ // console.log('Heartbeat file disappeared.');
187+ cleanupAndResolve ( '' ) ;
188+ } else if ( Date . now ( ) - startTime > 7000 ) {
189+ // File never appeared and initial grace period (7s) passed, assume dead
190+ // console.log('Heartbeat file never appeared.');
191+ cleanupAndResolve ( '' ) ;
192+ }
193+ // Otherwise, file just hasn't appeared yet, wait longer
194+ } else if ( err . code !== 'ENOENT' ) {
195+ // Log other errors, but potentially continue?
196+ // console.error('Heartbeat check error:', err);
197+ // Maybe stop checking if there's a persistent error other than ENOENT?
198+ // cleanupAndResolve(''); // Or resolve immediately on other errors?
162199 }
163200 }
201+ } , 1500 ) ; // Check every 1.5 seconds
164202
165- resolve ( response ) ;
166- } ;
203+ // Timeout to stop watching if no response within limit
204+ const timeoutHandle = setTimeout ( ( ) => {
205+ // console.log('Overall timeout reached.');
206+ cleanupAndResolve ( '' ) ;
207+ } , timeoutSeconds * 1000 + 5000 ) ; // Add a bit more buffer
167208 } ) ;
168209}
0 commit comments