/** * 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 { const resolvedPath = path.resolve(dirPath); // Check if path already exists using lstat (doesn't follow symlinks) try { const stats = await secureFs.lstat(resolvedPath); // Path exists - if it's a directory or symlink, consider it success if (stats.isDirectory() || stats.isSymbolicLink()) { return; } // 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 { 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; } }