UNPKG

@tabbybyte/minion

Version:

A cross-runtime CLI tool for AI-powered command execution. Auto-detects and uses Bun for performance when available, falls back to Node.js.

229 lines (208 loc) 6.35 kB
import { dirname, resolve, normalize, join, isAbsolute } from "path" import { readdir, mkdir, stat } from "fs/promises" import { file, writeContent } from "./runtime-compat.js" /** * OS-agnostic file utility functions for safe file operations * Provides robust error handling and path normalization * Uses runtime detection to leverage Bun APIs when available */ /** * Safely read a file with proper error handling * @param {string} filePath - Path to the file * @returns {Promise<string>} File contents */ async function safeReadFile(filePath) { try { const normalizedPath = normalizePath(filePath) await checkFileExists(normalizedPath) return await file.text(normalizedPath) } catch (error) { throw new Error(`Failed to read file '${filePath}': ${error.message}`) } } /** * Safely write a file with directory creation if needed * @param {string} filePath - Path to the file * @param {string} content - Content to write * @returns {Promise<void>} */ async function safeWriteFile(filePath, content) { try { const normalizedPath = normalizePath(filePath) const dir = dirname(normalizedPath) // Create directory if it doesn't exist await ensureDirectory(dir) await writeContent(normalizedPath, content) } catch (error) { throw new Error(`Failed to write file '${filePath}': ${error.message}`) } } /** * Safely append content to a file * @param {string} filePath - Path to the file * @param {string} content - Content to append * @param {string} encoding - File encoding (default: 'utf-8') * @returns {Promise<void>} */ async function safeAppendFile(filePath, content, encoding = "utf-8") { try { const normalizedPath = normalizePath(filePath) let existingContent = "" // Read existing content if file exists if (await fileExists(normalizedPath)) { existingContent = await safeReadFile(normalizedPath, encoding) } const newContent = existingContent + content await writeContent(normalizedPath, newContent) } catch (error) { throw new Error(`Failed to append to file '${filePath}': ${error.message}`) } } /** * Check if a file exists * @param {string} filePath - Path to the file * @returns {Promise<boolean>} */ async function fileExists(filePath) { try { const normalizedPath = normalizePath(filePath) return await file.exists(normalizedPath) } catch { return false } } /** * Check if a file exists and throw if it doesn't * @param {string} filePath - Path to the file * @returns {Promise<void>} */ async function checkFileExists(filePath) { if (!(await fileExists(filePath))) { throw new Error(`File does not exist: ${filePath}`) } } /** * Get file statistics * @param {string} filePath - Path to the file * @returns {Promise<import('fs').Stats>} */ async function getFileStats(filePath) { try { const normalizedPath = normalizePath(filePath) return await stat(normalizedPath) } catch (error) { throw new Error(`Failed to get file stats for '${filePath}': ${error.message}`) } } /** * List directory contents * @param {string} dirPath - Path to the directory * @returns {Promise<string[]>} */ async function listDirectory(dirPath) { try { const normalizedPath = normalizePath(dirPath) return await readdir(normalizedPath) } catch (error) { throw new Error(`Failed to list directory '${dirPath}': ${error.message}`) } } /** * Ensure a directory exists, create if it doesn't * @param {string} dirPath - Path to the directory * @returns {Promise<void>} */ async function ensureDirectory(dirPath) { try { const normalizedPath = normalizePath(dirPath) // Use Node.js fs/promises mkdir since Bun doesn't have Bun.mkdir await mkdir(normalizedPath, { recursive: true }) } catch (error) { throw new Error(`Failed to create directory '${dirPath}': ${error.message}`) } } /** * Normalize and resolve file paths for cross-platform compatibility * @param {string} filePath - Path to normalize * @returns {string} Normalized path */ function normalizePath(filePath) { if (!filePath || typeof filePath !== "string") { throw new Error("Invalid file path provided") } // Convert to absolute path if relative const absolutePath = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath) // Normalize path separators and resolve . and .. segments return normalize(absolutePath) } /** * Safely join path segments * @param {...string} paths - Path segments to join * @returns {string} Joined path */ function safePath(...paths) { return normalizePath(join(...paths)) } /** * Validate that a file path is safe (not trying to escape working directory) * @param {string} filePath - Path to validate * @param {string} baseDir - Base directory to restrict to (default: cwd) * @returns {boolean} */ function isPathSafe(filePath, baseDir = process.cwd()) { try { const normalizedPath = normalizePath(filePath) const normalizedBase = normalizePath(baseDir) // Check if the path is within the base directory return normalizedPath.startsWith(normalizedBase) } catch { return false } } /** * Get file extension * @param {string} filePath - Path to the file * @returns {string} File extension (with dot) */ function getFileExtension(filePath) { const normalized = normalizePath(filePath) const lastDot = normalized.lastIndexOf(".") const lastSlash = Math.max(normalized.lastIndexOf("/"), normalized.lastIndexOf("\\")) if (lastDot > lastSlash) { return normalized.substring(lastDot) } return "" } /** * Get file name without extension * @param {string} filePath - Path to the file * @returns {string} File name without extension */ function getFileNameWithoutExtension(filePath) { const normalized = normalizePath(filePath) const lastSlash = Math.max(normalized.lastIndexOf("/"), normalized.lastIndexOf("\\")) const fileName = normalized.substring(lastSlash + 1) const lastDot = fileName.lastIndexOf(".") if (lastDot > 0) { return fileName.substring(0, lastDot) } return fileName } // Exports export { safeReadFile, safeWriteFile, safeAppendFile, fileExists, checkFileExists, getFileStats, listDirectory, ensureDirectory, normalizePath, safePath, isPathSafe, getFileExtension, getFileNameWithoutExtension }