@ -397,49 +397,67 @@ export const listFileTree = async (
/**
* Watches for file stability before resolving the promise.
* Ensures that the directory and file exist before setting up the watcher.
* @param filePathArg The path of the file to monitor.
* Ensures that the directory/ file exists before setting up the watcher.
*
* **New behavior**: If the given path is a directory, this function will:
* 1. Wait for that directory to exist (creating a timeout if needed).
* 2. Watch the directory until at least one file appears.
* 3. Then wait for the first file in the directory to stabilize before resolving.
*
* @param fileOrDirPathArg The path of the file or directory to monitor.
* @param timeoutMs The maximum time to wait for the file to stabilize (in milliseconds). Default is 60 seconds.
* @returns A promise that resolves when the file is stable or rejects on timeout or error.
* @returns A promise that resolves when the target is stable or rejects on timeout/ error.
*/
export const waitForFileToBeReady = (
filePathArg : string ,
export const waitForFileToBeReady = async (
fileOrDir PathArg : string ,
timeoutMs : number = 60000
) : Promise < void > = > {
return new Promise ( async ( resolve , reject ) = > {
const startTime = Date . now ( ) ;
/**
* Ensure that a path (file or directory) exists. If it doesn't yet exist,
* wait until it does (or time out).
* @param pathToCheck The file or directory path to check.
*/
const ensurePathExists = async ( pathToCheck : string ) : Promise < void > = > {
while ( true ) {
try {
await plugins . smartpromise . fromCallback ( ( cb ) = >
plugins . fs . access ( pathToCheck , plugins . fs . constants . F_OK , cb )
) ;
return ;
} catch ( err : any ) {
if ( err . code !== 'ENOENT' ) {
throw err ; // Propagate unexpected errors
}
if ( Date . now ( ) - startTime > timeoutMs ) {
throw new Error ( ` Timeout waiting for path to exist: ${ pathToCheck } ` ) ;
}
await plugins . smartdelay . delayFor ( 500 ) ;
}
}
} ;
/**
* Checks if a file (not directory) is stable by comparing sizes
* across successive checks.
* @param filePathArg The path of the file to check.
* @returns A promise that resolves once the file stops changing.
*/
const waitForSingleFileToBeStable = async ( filePathArg : string ) : Promise < void > = > {
let lastFileSize = - 1 ;
let fileIsStable = false ;
const startTime = Date . now ( ) ;
const fileDir = plugins . path . dirname ( filePathArg ) ;
const ensureFileExists = async ( ) = > {
while ( true ) {
try {
// Check if the file exists
await plugins . smartpromise . fromCallback ( ( cb ) = >
plugins . fs . access ( filePathArg , plugins . fs . constants . F_OK , cb )
) ;
break ; // Exit the loop if the file exists
} catch ( err : any ) {
if ( err . code !== 'ENOENT' ) {
throw err ; // Propagate unexpected errors
}
if ( Date . now ( ) - startTime > timeoutMs ) {
reject ( new Error ( ` Timeout waiting for file to exist: ${ filePathArg } ` ) ) ;
return ;
}
// Wait and retry
await plugins . smartdelay . delayFor ( 500 ) ;
}
}
} ;
// We'll create a helper for repeated stats-checking logic
const checkFileStability = async ( ) = > {
try {
const stats = await plugins . smartpromise . fromCallback < plugins.fs.Stats > ( ( cb ) = >
plugins . fs . stat ( filePathArg , cb )
) ;
if ( stats . isDirectory ( ) ) {
// If it unexpectedly turns out to be a directory here, throw
throw new Error ( ` Expected a file but found a directory: ${ filePathArg } ` ) ;
}
if ( stats . size === lastFileSize ) {
fileIsStable = true ;
} else {
@ -447,49 +465,106 @@ export const waitForFileToBeReady = (
fileIsStable = false ;
}
} catch ( err : any ) {
// Ignore only if file not found
if ( err . code !== 'ENOENT' ) {
throw err ; // Only ignore ENOENT (file not found) errors
throw err ;
}
}
} ;
// Ensure the file exists before setting up the watcher
await ensureFile Exists ( ) ;
// Ensure file exists first
await ensurePath Exists ( filePathArg ) ;
const watcher = plugins . fs . watch ( filePathArg , { persistent : true } , async ( ) = > {
// Set up a watcher on the file itself
const fileWatcher = plugins . fs . watch ( filePathArg , { persistent : true } , async ( ) = > {
if ( ! fileIsStable ) {
await checkFileStability ( ) ;
}
} ) ;
watcher . on ( 'error' , ( error ) = > {
watcher . close ( ) ;
reject ( error ) ;
} ) ;
try {
// Poll until stable or timeout
while ( ! fileIsStable ) {
// Check for timeout
if ( Date . now ( ) - startTime > timeoutMs ) {
watcher . close ( ) ;
reject ( new Error ( ` Timeout waiting for file to stabilize: ${ filePathArg } ` ) ) ;
return ;
throw new Error ( ` Timeout waiting for file to stabilize: ${ filePathArg } ` ) ;
}
// Check file stability
await checkFileStability ( ) ;
if ( ! fileIsStable ) {
await plugins . smartdelay . delayFor ( 1000 ) ; // Polling interval
await plugins . smartdelay . delayFor ( 1000 ) ;
}
}
w atcher. close ( ) ;
resolve ( ) ;
} catch ( err ) {
watcher . close ( ) ;
reject ( err ) ;
} finally {
fileW atcher. close ( ) ;
}
} ) ;
} ;
/**
* Main logic: check if we have a directory or file at fileOrDirPathArg.
* If directory, wait for first file in the directory to appear and stabilize.
* If file, do the old single-file wait logic.
*/
const statsForGivenPath = await ( async ( ) = > {
try {
await ensurePathExists ( fileOrDirPathArg ) ;
return await plugins . smartpromise . fromCallback < plugins.fs.Stats > ( ( cb ) = >
plugins . fs . stat ( fileOrDirPathArg , cb )
) ;
} catch ( err ) {
// If there's an error (including timeout), just rethrow
throw err ;
}
} ) ( ) ;
if ( ! statsForGivenPath . isDirectory ( ) ) {
// It's a file – just do the single-file stability wait
await waitForSingleFileToBeStable ( fileOrDirPathArg ) ;
return ;
}
// Otherwise, it's a directory. Wait for the first file inside to appear and be stable
const dirPath = fileOrDirPathArg ;
// Helper to find the first file in the directory if it exists
const getFirstFileInDirectory = async ( ) : Promise < string | null > = > {
const entries = await plugins . smartpromise . fromCallback < string [ ] > ( ( cb ) = >
plugins . fs . readdir ( dirPath , cb )
) ;
// We only want actual files, not subdirectories
for ( const entry of entries ) {
const entryPath = plugins . path . join ( dirPath , entry ) ;
const entryStats = await plugins . smartpromise . fromCallback < plugins.fs.Stats > ( ( cb ) = >
plugins . fs . stat ( entryPath , cb )
) ;
if ( entryStats . isFile ( ) ) {
return entryPath ;
}
}
return null ;
} ;
// Wait for a file to appear in this directory
let firstFilePath = await getFirstFileInDirectory ( ) ;
if ( ! firstFilePath ) {
// Set up a watcher on the directory to see if a file appears
const directoryWatcher = plugins . fs . watch ( dirPath , { persistent : true } ) ;
try {
// We'll poll for the existence of a file in that directory
while ( ! firstFilePath ) {
if ( Date . now ( ) - startTime > timeoutMs ) {
throw new Error ( ` Timeout waiting for a file to appear in directory: ${ dirPath } ` ) ;
}
firstFilePath = await getFirstFileInDirectory ( ) ;
if ( ! firstFilePath ) {
await plugins . smartdelay . delayFor ( 1000 ) ;
}
}
} finally {
directoryWatcher . close ( ) ;
}
}
// Now that we have a file path, wait for that file to stabilize
await waitForSingleFileToBeStable ( firstFilePath ) ;
} ;
/**