import { resolve, normalize, dirname } from 'path';
import { existsSync } from 'fs';
import { mkdir } from 'fs/promises';
/**
* PathResolver provides cross-platform path handling utilities
* Works on Windows, Linux, and macOS
*/
export class PathResolver {
private workspaceRoot: string;
constructor(workspaceRoot?: string) {
// Use provided workspace root or fall back to process.cwd(), then resolve to absolute path
this.workspaceRoot = resolve(workspaceRoot || process.cwd());
}
/**
* Resolve a relative path to an absolute, normalized path
* Works on Windows, Linux, and macOS
*
* @param relativePath - The relative path to resolve
* @returns Absolute, normalized path
*/
resolveWorkspacePath(relativePath: string): string {
// Normalize first to handle mixed separators (e.g., "folder\\file" or "folder/file")
const normalized = normalize(relativePath);
// Then resolve relative to workspace
return resolve(this.workspaceRoot, normalized);
}
/**
* Validate that a path exists and is a file
*
* @param path - The path to validate (can be relative or absolute)
* @returns True if the path exists and is a file
*/
validateFilePath(path: string): boolean {
const fullPath = this.resolveWorkspacePath(path);
return existsSync(fullPath);
}
/**
* Ensure the directory for a given file path exists
* Creates parent directories recursively if they don't exist
*
* @param path - The file path (directory will be created for the parent)
*/
async ensureDirectory(path: string): Promise<void> {
const fullPath = this.resolveWorkspacePath(path);
const dir = dirname(fullPath);
await mkdir(dir, { recursive: true });
}
/**
* Check if a resolved path is safe (stays within workspace)
* Prevents path traversal attacks
*
* @param relativePath - The relative path to check
* @returns True if the path is safe (stays within workspace)
*/
isPathSafe(relativePath: string): boolean {
const resolvedPath = this.resolveWorkspacePath(relativePath);
const normalizedWorkspace = normalize(this.workspaceRoot);
return resolvedPath.startsWith(normalizedWorkspace);
}
/**
* Get the workspace root directory
*
* @returns The workspace root path
*/
getWorkspaceRoot(): string {
return this.workspaceRoot;
}
}