diff --git a/src/core/file/fileSearch.ts b/src/core/file/fileSearch.ts index 46a994a5..60deb32a 100644 --- a/src/core/file/fileSearch.ts +++ b/src/core/file/fileSearch.ts @@ -3,8 +3,19 @@ import type { RepomixConfigMerged } from '../../config/configTypes.js'; import { defaultIgnoreList } from '../../config/defaultIgnore.js'; import { logger } from '../../shared/logger.js'; import { sortPaths } from './filePathSort.js'; +import { PermissionError, checkDirectoryPermissions } from './permissionCheck.js'; export const searchFiles = async (rootDir: string, config: RepomixConfigMerged): Promise => { + // First check directory permissions + const permissionCheck = await checkDirectoryPermissions(rootDir); + + if (!permissionCheck.hasPermission) { + if (permissionCheck.error instanceof PermissionError) { + throw permissionCheck.error; + } + throw new Error(`Cannot access directory ${rootDir}: ${permissionCheck.error?.message}`); + } + const includePatterns = config.include.length > 0 ? config.include : ['**/*']; try { @@ -25,6 +36,15 @@ export const searchFiles = async (rootDir: string, config: RepomixConfigMerged): absolute: false, dot: true, followSymbolicLinks: false, + }).catch((error) => { + // Handle EPERM errors specifically + if (error.code === 'EPERM' || error.code === 'EACCES') { + throw new PermissionError( + 'Permission denied while scanning directory. Please check folder access permissions for your terminal app.', + rootDir, + ); + } + throw error; }); logger.trace(`Filtered ${filePaths.length} files`); @@ -32,10 +52,16 @@ export const searchFiles = async (rootDir: string, config: RepomixConfigMerged): return sortedPaths; } catch (error: unknown) { + // Re-throw PermissionError as is + if (error instanceof PermissionError) { + throw error; + } + if (error instanceof Error) { logger.error('Error filtering files:', error.message); throw new Error(`Failed to filter files in directory ${rootDir}. Reason: ${error.message}`); } + logger.error('An unexpected error occurred:', error); throw new Error('An unexpected error occurred while filtering files.'); } diff --git a/src/core/file/permissionCheck.ts b/src/core/file/permissionCheck.ts new file mode 100644 index 00000000..d2ea53ff --- /dev/null +++ b/src/core/file/permissionCheck.ts @@ -0,0 +1,115 @@ +import { constants, access } from 'node:fs'; +import * as fs from 'node:fs/promises'; +import { platform } from 'node:os'; +import { promisify } from 'node:util'; +import { logger } from '../../shared/logger.js'; + +const asyncAccess = promisify(access); + +export interface PermissionCheckResult { + hasPermission: boolean; + error?: Error; + details?: { + read?: boolean; + write?: boolean; + execute?: boolean; + }; +} + +export class PermissionError extends Error { + constructor( + message: string, + public readonly path: string, + public readonly code?: string, + ) { + super(message); + this.name = 'PermissionError'; + } +} + +export async function checkDirectoryPermissions(dirPath: string): Promise { + try { + // First try to read directory contents + await fs.readdir(dirPath); + + // Check specific permissions + const details = { + read: false, + write: false, + execute: false, + }; + + try { + await asyncAccess(dirPath, constants.R_OK); + details.read = true; + } catch {} + + try { + await asyncAccess(dirPath, constants.W_OK); + details.write = true; + } catch {} + + try { + await asyncAccess(dirPath, constants.X_OK); + details.execute = true; + } catch {} + + const hasAllPermissions = details.read && details.write && details.execute; + + if (!hasAllPermissions) { + return { + hasPermission: false, + details, + }; + } + + return { + hasPermission: true, + details, + }; + } catch (error) { + if (error instanceof Error && 'code' in error) { + switch (error.code) { + case 'EPERM': + case 'EACCES': + case 'EISDIR': + return { + hasPermission: false, + error: new PermissionError(getMacOSPermissionMessage(dirPath, error.code), dirPath, error.code), + }; + default: + logger.debug('Directory permission check error:', error); + return { + hasPermission: false, + error: error as Error, + }; + } + } + return { + hasPermission: false, + error: error instanceof Error ? error : new Error(String(error)), + }; + } +} + +function getMacOSPermissionMessage(dirPath: string, errorCode?: string): string { + if (platform() === 'darwin') { + return `Permission denied: Cannot access '${dirPath}', error code: ${errorCode}. + +This error often occurs when macOS security restrictions prevent access to the directory. +To fix this: + +1. Open System Settings +2. Navigate to Privacy & Security > Files and Folders +3. Find your terminal app (Terminal.app, iTerm2, VS Code, etc.) +4. Grant necessary folder access permissions + +If your terminal app is not listed: +- Try running repomix command again +- When prompted by macOS, click "Allow" +- Restart your terminal app if needed +`; + } + + return `Permission denied: Cannot access '${dirPath}'`; +}