| /** | |
| * File system utilities that handle symlinks safely | |
| */ | |
| import { secureFs } from '@automaker/platform'; | |
| import path from 'path'; | |
| /** | |
| * Create a directory, handling symlinks safely to avoid ELOOP errors. | |
| * If the path already exists as a directory or symlink, returns success. | |
| */ | |
| export async function mkdirSafe(dirPath: string): Promise<void> { | |
| const resolvedPath = path.resolve(dirPath); | |
| // Check if path already exists using lstat (doesn't follow symlinks) | |
| try { | |
| const stats = await secureFs.lstat(resolvedPath); | |
| // Guard: some environments (e.g. mocked fs) may return undefined | |
| if (stats == null) { | |
| // Treat as path does not exist, fall through to create | |
| } else if (stats.isDirectory() || stats.isSymbolicLink()) { | |
| // Path exists - if it's a directory or symlink, consider it success | |
| return; | |
| } else { | |
| // It's a file - can't create directory | |
| throw new Error(`Path exists and is not a directory: ${resolvedPath}`); | |
| } | |
| } catch (error: any) { | |
| // ENOENT means path doesn't exist - we should create it | |
| if (error.code !== 'ENOENT') { | |
| // Some other error (could be ELOOP in parent path) | |
| // If it's ELOOP, the path involves symlinks - don't try to create | |
| if (error.code === 'ELOOP') { | |
| console.warn(`[fs-utils] Symlink loop detected at ${resolvedPath}, skipping mkdir`); | |
| return; | |
| } | |
| throw error; | |
| } | |
| } | |
| // Path doesn't exist, create it | |
| try { | |
| await secureFs.mkdir(resolvedPath, { recursive: true }); | |
| } catch (error: any) { | |
| // Handle race conditions and symlink issues | |
| if (error.code === 'EEXIST' || error.code === 'ELOOP') { | |
| return; | |
| } | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Check if a path exists, handling symlinks safely. | |
| * Returns true if the path exists as a file, directory, or symlink. | |
| */ | |
| export async function existsSafe(filePath: string): Promise<boolean> { | |
| try { | |
| await secureFs.lstat(filePath); | |
| return true; | |
| } catch (error: any) { | |
| if (error.code === 'ENOENT') { | |
| return false; | |
| } | |
| // ELOOP or other errors - path exists but is problematic | |
| if (error.code === 'ELOOP') { | |
| return true; // Symlink exists, even if looping | |
| } | |
| throw error; | |
| } | |
| } | |