( turnStartTime: TurnStartTime, outputsDir: string, )
| 60 | * @param outputsDir - The directory to scan for modified files |
| 61 | */ |
| 62 | export async function findModifiedFiles( |
| 63 | turnStartTime: TurnStartTime, |
| 64 | outputsDir: string, |
| 65 | ): Promise<string[]> { |
| 66 | // Use recursive flag to get all entries in one call |
| 67 | let entries: Awaited<ReturnType<typeof fs.readdir>> |
| 68 | try { |
| 69 | entries = await fs.readdir(outputsDir, { |
| 70 | withFileTypes: true, |
| 71 | recursive: true, |
| 72 | }) |
| 73 | } catch { |
| 74 | // Directory doesn't exist or is not accessible |
| 75 | return [] |
| 76 | } |
| 77 | |
| 78 | // Filter to regular files only (skip symlinks for security) and build full paths |
| 79 | const filePaths: string[] = [] |
| 80 | for (const entry of entries) { |
| 81 | if (entry.isSymbolicLink()) { |
| 82 | continue |
| 83 | } |
| 84 | if (entry.isFile()) { |
| 85 | // entry.parentPath is available in Node 20+, fallback to entry.path for older versions |
| 86 | const parentPath = getEntryParentPath(entry, outputsDir) |
| 87 | filePaths.push(path.join(parentPath, entry.name)) |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | if (filePaths.length === 0) { |
| 92 | logDebug('No files found in outputs directory') |
| 93 | return [] |
| 94 | } |
| 95 | |
| 96 | // Parallelize stat calls for all files |
| 97 | const statResults = await Promise.all( |
| 98 | filePaths.map(async filePath => { |
| 99 | try { |
| 100 | const stat = await fs.lstat(filePath) |
| 101 | // Skip if it became a symlink between readdir and stat (race condition) |
| 102 | if (stat.isSymbolicLink()) { |
| 103 | return null |
| 104 | } |
| 105 | return { filePath, mtimeMs: stat.mtimeMs } |
| 106 | } catch { |
| 107 | // File may have been deleted between readdir and stat |
| 108 | return null |
| 109 | } |
| 110 | }), |
| 111 | ) |
| 112 | |
| 113 | // Filter to files modified since turn start |
| 114 | const modifiedFiles: string[] = [] |
| 115 | for (const result of statResults) { |
| 116 | if (result && result.mtimeMs >= turnStartTime) { |
| 117 | modifiedFiles.push(result.filePath) |
| 118 | } |
| 119 | } |
no test coverage detected