UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

284 lines 9.5 kB
import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; /** * FilesystemSandbox provides isolated temporary filesystems for testing. * * Creates temporary directory structures that can be safely manipulated * without affecting the real filesystem. Automatically cleans up on destruction. * * @example * ```typescript * const sandbox = new FilesystemSandbox(); * await sandbox.initialize(); * await sandbox.writeFile('test.txt', 'Hello World'); * const content = await sandbox.readFile('test.txt'); * await sandbox.cleanup(); * ``` */ export class FilesystemSandbox { sandboxPath = ''; initialized = false; constructor() { // Sandbox path will be set during initialization } /** * Initialize the sandbox by creating a temporary directory. * Must be called before any other operations. */ async initialize() { if (this.initialized) { throw new Error('Sandbox already initialized'); } const prefix = path.join(os.tmpdir(), 'aiwg-sandbox-'); this.sandboxPath = await fs.mkdtemp(prefix); this.initialized = true; } /** * Clean up the sandbox by removing the temporary directory. * Safe to call multiple times. */ async cleanup() { if (!this.sandboxPath || !this.initialized) { return; } try { // Ensure we're cleaning up a temp directory (safety check) if (!this.sandboxPath.startsWith(os.tmpdir())) { throw new Error('Refusing to cleanup: path is not in temp directory'); } await fs.rm(this.sandboxPath, { recursive: true, force: true }); this.initialized = false; this.sandboxPath = ''; } catch (error) { // Log error but don't throw - cleanup should be best-effort console.error('Error during sandbox cleanup:', error); } } /** * Write a file to the sandbox. * * @param relativePath - Path relative to sandbox root * @param content - File content (string or Buffer) * @param options - Write options (encoding, mode, flag) */ async writeFile(relativePath, content, options) { this.ensureInitialized(); this.validatePath(relativePath); const fullPath = this.resolvePath(relativePath); const dir = path.dirname(fullPath); // Create directory if it doesn't exist await fs.mkdir(dir, { recursive: true }); // Write file with options await fs.writeFile(fullPath, content, options); } async readFile(relativePath, encoding) { this.ensureInitialized(); this.validatePath(relativePath); const fullPath = this.resolvePath(relativePath); if (encoding === null) { return await fs.readFile(fullPath); } return await fs.readFile(fullPath, encoding || 'utf-8'); } /** * Delete a file from the sandbox. * * @param relativePath - Path relative to sandbox root */ async deleteFile(relativePath) { this.ensureInitialized(); this.validatePath(relativePath); const fullPath = this.resolvePath(relativePath); await fs.unlink(fullPath); } /** * Check if a file exists. * * @param relativePath - Path relative to sandbox root * @returns True if file exists */ async fileExists(relativePath) { this.ensureInitialized(); this.validatePath(relativePath); const fullPath = this.resolvePath(relativePath); try { const stat = await fs.stat(fullPath); return stat.isFile(); } catch (error) { if (error.code === 'ENOENT') { return false; } throw error; } } /** * Get file statistics. * * @param relativePath - Path relative to sandbox root * @returns File statistics */ async getFileStats(relativePath) { this.ensureInitialized(); this.validatePath(relativePath); const fullPath = this.resolvePath(relativePath); const stat = await fs.stat(fullPath); return { size: stat.size, isFile: stat.isFile(), isDirectory: stat.isDirectory(), createdAt: stat.birthtime, modifiedAt: stat.mtime }; } /** * Create a directory in the sandbox. * * @param relativePath - Path relative to sandbox root */ async createDirectory(relativePath) { this.ensureInitialized(); this.validatePath(relativePath); const fullPath = this.resolvePath(relativePath); await fs.mkdir(fullPath, { recursive: true }); } /** * Delete a directory from the sandbox. * * @param relativePath - Path relative to sandbox root * @param recursive - Whether to delete recursively (default: false) */ async deleteDirectory(relativePath, recursive = false) { this.ensureInitialized(); this.validatePath(relativePath); const fullPath = this.resolvePath(relativePath); if (recursive) { await fs.rm(fullPath, { recursive: true, force: true }); } else { await fs.rmdir(fullPath); } } /** * List directory contents. * * @param relativePath - Path relative to sandbox root (default: root) * @returns Array of file/directory names */ async listDirectory(relativePath = '.') { this.ensureInitialized(); this.validatePath(relativePath); const fullPath = this.resolvePath(relativePath); return await fs.readdir(fullPath); } /** * Check if a directory exists. * * @param relativePath - Path relative to sandbox root * @returns True if directory exists */ async directoryExists(relativePath) { this.ensureInitialized(); this.validatePath(relativePath); const fullPath = this.resolvePath(relativePath); try { const stat = await fs.stat(fullPath); return stat.isDirectory(); } catch (error) { if (error.code === 'ENOENT') { return false; } throw error; } } /** * Get the absolute path to a file/directory in the sandbox. * * @param relativePath - Optional relative path (default: sandbox root) * @returns Absolute path */ getPath(relativePath) { this.ensureInitialized(); if (!relativePath) { return this.sandboxPath; } this.validatePath(relativePath); return this.resolvePath(relativePath); } /** * Copy a file from the real filesystem into the sandbox. * * @param realPath - Path in the real filesystem * @param sandboxPath - Target path in sandbox (relative) */ async copyFromReal(realPath, sandboxPath) { this.ensureInitialized(); this.validatePath(sandboxPath); const sourcePath = path.resolve(realPath); const targetPath = this.resolvePath(sandboxPath); const targetDir = path.dirname(targetPath); // Create target directory if needed await fs.mkdir(targetDir, { recursive: true }); // Copy file await fs.copyFile(sourcePath, targetPath); } /** * Copy a file from the sandbox to the real filesystem. * * @param sandboxPath - Source path in sandbox (relative) * @param realPath - Target path in the real filesystem */ async copyToReal(sandboxPath, realPath) { this.ensureInitialized(); this.validatePath(sandboxPath); const sourcePath = this.resolvePath(sandboxPath); const targetPath = path.resolve(realPath); const targetDir = path.dirname(targetPath); // Create target directory if needed await fs.mkdir(targetDir, { recursive: true }); // Copy file await fs.copyFile(sourcePath, targetPath); } /** * Ensure the sandbox is initialized before operations. */ ensureInitialized() { if (!this.initialized || !this.sandboxPath) { throw new Error('Sandbox not initialized. Call initialize() first.'); } } /** * Validate that a path doesn't escape the sandbox. * * @param relativePath - Path to validate * @throws Error if path is invalid or escapes sandbox */ validatePath(relativePath) { // Reject absolute paths if (path.isAbsolute(relativePath)) { throw new Error('Absolute paths are not allowed in sandbox'); } // Reject paths with null bytes if (relativePath.includes('\0')) { throw new Error('Path contains null bytes'); } // Resolve the full path and ensure it's within sandbox const fullPath = path.resolve(this.sandboxPath, relativePath); if (!fullPath.startsWith(this.sandboxPath)) { throw new Error('Path escapes sandbox directory'); } } /** * Resolve a relative path to an absolute path within the sandbox. * * @param relativePath - Relative path * @returns Absolute path */ resolvePath(relativePath) { return path.join(this.sandboxPath, relativePath); } } //# sourceMappingURL=filesystem-sandbox.js.map