UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

2,008 lines 56 kB
import { $inject, $module, AlephaError, Json, isFileLike } from "alepha"; import { exec, spawn } from "node:child_process"; import { $logger } from "alepha/logger"; import { basename, join } from "node:path"; import { createReadStream } from "node:fs"; import { access, copyFile, cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises"; import { PassThrough, Readable } from "node:stream"; import { fileURLToPath } from "node:url"; //#region ../../src/system/providers/FileSystemProvider.ts /** * FileSystem interface providing utilities for working with files. */ var FileSystemProvider = class {}; //#endregion //#region ../../src/system/providers/NodeShellProvider.ts /** * Node.js implementation of ShellProvider. * * Executes shell commands using Node.js child_process module. * Supports binary resolution from node_modules/.bin for local packages. */ var NodeShellProvider = class { log = $logger(); fs = $inject(FileSystemProvider); /** * Run a shell command or binary. */ async run(command, options = {}) { const { resolve = false, capture = false, root, env } = options; const cwd = root ?? process.cwd(); this.log.debug(`Shell: ${command}`, { cwd, resolve, capture }); let executable; let args; if (resolve) { const [bin, ...rest] = this.parseCommand(command); executable = await this.resolveExecutable(bin, cwd); args = rest; } else [executable, ...args] = this.parseCommand(command); const shellCommand = this.buildShellCommand(executable, args); if (capture) return this.execCapture(shellCommand, { cwd, env }); return this.execInherit(executable, args, { cwd, env }); } /** * Execute command with inherited stdio (streams to terminal). */ async execInherit(executable, args, options) { const proc = process.platform === "win32" ? spawn(this.buildShellCommand(executable, args), [], { stdio: "inherit", cwd: options.cwd, shell: true, env: { ...process.env, ...options.env } }) : spawn(executable, args, { stdio: "inherit", cwd: options.cwd, env: { ...process.env, ...options.env } }); return new Promise((resolve, reject) => { proc.on("exit", (code) => { if (code === 0 || code === null) resolve(""); else reject(new AlephaError(`Command exited with code ${code}`)); }); proc.on("error", reject); }); } /** * Build a shell command string with proper escaping for Windows. * Quotes both executable and arguments that contain spaces or special characters. */ buildShellCommand(executable, args) { const escapeForShell = (str) => { if (/[\s"&|<>^()]/.test(str)) return `"${str.replace(/"/g, "\\\"")}"`; return str; }; return [escapeForShell(executable), ...args.map(escapeForShell)].join(" "); } /** * Execute command and capture stdout. */ execCapture(command, options) { return new Promise((resolve, reject) => { exec(command, { cwd: options.cwd, maxBuffer: 50 * 1024 * 1024, env: { ...process.env, LOG_FORMAT: "pretty", ...options.env } }, (err, stdout, stderr) => { if (err) { err.stdout = stdout; err.stderr = stderr; reject(err); } else resolve(stdout); }); }); } /** * Resolve executable path from node_modules/.bin. * * Search order: * 1. Local: node_modules/.bin/ * 2. Pnpm nested: node_modules/alepha/node_modules/.bin/ * 3. Monorepo: Walk up to 3 parent directories */ async resolveExecutable(name, root) { const suffixes = process.platform === "win32" ? [ ".cmd", ".exe", "" ] : [""]; for (const suffix of suffixes) { let execPath = await this.findExecutable(root, `node_modules/.bin/${name}${suffix}`); if (execPath) return execPath; execPath = await this.findExecutable(root, `node_modules/alepha/node_modules/.bin/${name}${suffix}`); if (execPath) return execPath; let parentDir = this.fs.join(root, ".."); for (let i = 0; i < 3; i++) { execPath = await this.findExecutable(parentDir, `node_modules/.bin/${name}${suffix}`); if (execPath) return execPath; parentDir = this.fs.join(parentDir, ".."); } } throw new AlephaError(`Could not find executable for '${name}'. Make sure the package is installed.`); } /** * Check if executable exists at path. */ async findExecutable(root, relativePath) { const fullPath = this.fs.join(root, relativePath); if (await this.fs.exists(fullPath)) return fullPath; } /** * Check if a command is installed and available in the system PATH. */ isInstalled(command) { return new Promise((resolve) => { exec(process.platform === "win32" ? `where ${command}` : `command -v ${command}`, (error) => resolve(!error)); }); } /** * Parse a command string into executable and arguments. * * Handles quoted arguments properly for paths with spaces. * Supports both single and double quotes. */ parseCommand(command) { const result = []; let current = ""; let inQuote = null; for (let i = 0; i < command.length; i++) { const char = command[i]; if (inQuote) if (char === inQuote) inQuote = null; else current += char; else if (char === "\"" || char === "'") inQuote = char; else if (char === " ") { if (current) { result.push(current); current = ""; } } else current += char; } if (current) result.push(current); return result; } }; //#endregion //#region ../../src/system/providers/BunShellProvider.ts /** * Bun implementation of ShellProvider. * * Executes shell commands using Bun's native `Bun.spawn` and `Bun.which`, * skipping the `node:child_process` compatibility layer for better performance. * * Inherits executable resolution (`node_modules/.bin` walk) and command parsing * from `NodeShellProvider`. */ var BunShellProvider = class extends NodeShellProvider { /** * Execute command with inherited stdio (streams to terminal). */ async execInherit(executable, args, options) { const code = await Bun.spawn([executable, ...args], { cwd: options.cwd, env: { ...process.env, ...options.env }, stdout: "inherit", stderr: "inherit", stdin: "inherit" }).exited; if (code !== 0) throw new AlephaError(`Command exited with code ${code}`); return ""; } /** * Execute command and capture stdout. */ async execCapture(command, options) { const [executable, ...args] = this.parseCommand(command); const proc = Bun.spawn([executable, ...args], { cwd: options.cwd, env: { ...process.env, LOG_FORMAT: "pretty", ...options.env }, stdout: "pipe", stderr: "pipe" }); const [stdout, stderr, code] = await Promise.all([ new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited ]); if (code !== 0) { const err = new AlephaError(`Command exited with code ${code}: ${stderr || stdout}`); err.stdout = stdout; err.stderr = stderr; throw err; } return stdout; } /** * Check if a command is installed and available in the system PATH. */ async isInstalled(command) { return Bun.which(command) !== null; } }; //#endregion //#region ../../src/system/providers/MemoryFileSystemProvider.ts /** * In-memory implementation of FileSystemProvider for testing. * * This provider stores all files and directories in memory, making it ideal for * unit tests that need to verify file operations without touching the real file system. * * @example * ```typescript * // In tests, substitute the real FileSystemProvider with MemoryFileSystemProvider * const alepha = Alepha.create().with({ * provide: FileSystemProvider, * use: MemoryFileSystemProvider, * }); * * // Run code that uses FileSystemProvider * const service = alepha.inject(MyService); * await service.saveFile("test.txt", "Hello World"); * * // Verify the file was written * const memoryFs = alepha.inject(MemoryFileSystemProvider); * expect(memoryFs.files.get("test.txt")?.toString()).toBe("Hello World"); * ``` */ var MemoryFileSystemProvider = class { json = $inject(Json); /** * In-memory storage for files (path -> content) */ files = /* @__PURE__ */ new Map(); /** * In-memory storage for directories */ directories = /* @__PURE__ */ new Set(); /** * Track mkdir calls for test assertions */ mkdirCalls = []; /** * Track writeFile calls for test assertions */ writeFileCalls = []; /** * Track readFile calls for test assertions */ readFileCalls = []; /** * Track rm calls for test assertions */ rmCalls = []; /** * Track join calls for test assertions */ joinCalls = []; /** * Error to throw on mkdir (for testing error handling) */ mkdirError = null; /** * Error to throw on writeFile (for testing error handling) */ writeFileError = null; /** * Error to throw on readFile (for testing error handling) */ readFileError = null; constructor(options = {}) { this.mkdirError = options.mkdirError ?? null; this.writeFileError = options.writeFileError ?? null; this.readFileError = options.readFileError ?? null; } /** * Join path segments using forward slashes. * Uses Node's path.join for proper normalization (handles .. and .) */ join(...paths) { this.joinCalls.push(paths); return join(...paths); } /** * Normalize path separators to forward slashes for consistent internal storage. * This ensures Windows paths work correctly in the in-memory file system. */ normalizePath(path) { return path.replace(/\\/g, "/"); } /** * Create a FileLike object from various sources. */ createFile(options) { if ("path" in options) { const filePath = options.path; const buffer = this.files.get(filePath); if (buffer === void 0) throw new AlephaError(`ENOENT: no such file or directory, open '${filePath}'`); return { name: options.name ?? basename(filePath), type: options.type ?? "application/octet-stream", size: buffer.byteLength, lastModified: Date.now(), stream: () => { throw new AlephaError("Stream not implemented in MemoryFileSystemProvider"); }, arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), text: async () => buffer.toString("utf-8") }; } if ("buffer" in options) { const buffer = options.buffer; return { name: options.name ?? "file", type: options.type ?? "application/octet-stream", size: buffer.byteLength, lastModified: Date.now(), stream: () => { throw new AlephaError("Stream not implemented in MemoryFileSystemProvider"); }, arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), text: async () => buffer.toString("utf-8") }; } if ("text" in options) { const buffer = Buffer.from(options.text, "utf-8"); return { name: options.name ?? "file.txt", type: options.type ?? "text/plain", size: buffer.byteLength, lastModified: Date.now(), stream: () => { throw new AlephaError("Stream not implemented in MemoryFileSystemProvider"); }, arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength), text: async () => options.text }; } throw new AlephaError("MemoryFileSystemProvider.createFile: unsupported options. Only buffer and text are supported."); } /** * Remove a file or directory from memory. */ async rm(path, options) { this.rmCalls.push({ path, options }); if (!(this.files.has(path) || this.directories.has(path)) && !options?.force) throw new AlephaError(`ENOENT: no such file or directory, rm '${path}'`); if (this.directories.has(path)) if (options?.recursive) { this.directories.delete(path); for (const filePath of this.files.keys()) if (filePath.startsWith(`${path}/`)) this.files.delete(filePath); for (const dirPath of this.directories) if (dirPath.startsWith(`${path}/`)) this.directories.delete(dirPath); } else throw new AlephaError(`EISDIR: illegal operation on a directory, rm '${path}'`); else this.files.delete(path); } /** * Copy a file or directory in memory. */ async cp(src, dest, options) { if (this.directories.has(src)) { this.directories.add(dest); for (const [filePath, content] of this.files) if (filePath.startsWith(`${src}/`)) { const newPath = filePath.replace(src, dest); this.files.set(newPath, Buffer.from(content)); } } else if (this.files.has(src)) { const content = this.files.get(src); this.files.set(dest, Buffer.from(content)); } else throw new AlephaError(`ENOENT: no such file or directory, cp '${src}'`); } /** * Move/rename a file or directory in memory. */ async mv(src, dest) { if (this.directories.has(src)) { this.directories.delete(src); this.directories.add(dest); for (const [filePath, content] of this.files) if (filePath.startsWith(`${src}/`)) { const newPath = filePath.replace(src, dest); this.files.delete(filePath); this.files.set(newPath, content); } } else if (this.files.has(src)) { const content = this.files.get(src); this.files.delete(src); this.files.set(dest, content); } else throw new AlephaError(`ENOENT: no such file or directory, mv '${src}'`); } /** * Create a directory in memory. */ async mkdir(path, options) { this.mkdirCalls.push({ path, options }); if (this.mkdirError) throw this.mkdirError; const normalizedPath = this.normalizePath(path); if (this.directories.has(normalizedPath) && !options?.recursive) throw new AlephaError(`EEXIST: file already exists, mkdir '${path}'`); this.directories.add(normalizedPath); if (options?.recursive) { const parts = normalizedPath.split("/").filter(Boolean); let current = ""; for (const part of parts) { current = current ? `${current}/${part}` : part; this.directories.add(current); } } } /** * List files in a directory. */ async ls(path, options) { const normalizedPath = this.normalizePath(path).replace(/\/$/, ""); const entries = /* @__PURE__ */ new Set(); for (const filePath of this.files.keys()) { const normalizedFilePath = this.normalizePath(filePath); if (normalizedFilePath.startsWith(`${normalizedPath}/`)) { const relativePath = normalizedFilePath.slice(normalizedPath.length + 1); const parts = relativePath.split("/"); if (options?.recursive) entries.add(relativePath); else entries.add(parts[0]); } } for (const dirPath of this.directories) { const normalizedDirPath = this.normalizePath(dirPath); if (normalizedDirPath.startsWith(`${normalizedPath}/`) && normalizedDirPath !== normalizedPath) { const relativePath = normalizedDirPath.slice(normalizedPath.length + 1); const parts = relativePath.split("/"); if (options?.recursive) entries.add(relativePath); else if (parts.length === 1) entries.add(parts[0]); } } let result = Array.from(entries); if (!options?.hidden) result = result.filter((entry) => !entry.startsWith(".")); return result.sort(); } /** * Check if a file or directory exists in memory. */ async exists(path) { return this.files.has(path) || this.directories.has(path); } /** * Read a file from memory. */ async readFile(path) { this.readFileCalls.push(path); if (this.readFileError) throw this.readFileError; const content = this.files.get(path); if (!content) throw new AlephaError(`ENOENT: no such file or directory, open '${path}'`); return content; } /** * Read a file from memory as text. */ async readTextFile(path) { return (await this.readFile(path)).toString("utf-8"); } /** * Read a file from memory as JSON. */ async readJsonFile(path) { const text = await this.readTextFile(path); return this.json.parse(text); } /** * Write a file to memory. */ async writeFile(path, data) { const dataStr = typeof data === "string" ? data : data instanceof Buffer || data instanceof Uint8Array ? data.toString("utf-8") : await data.text(); this.writeFileCalls.push({ path, data: dataStr }); if (this.writeFileError) throw this.writeFileError; const buffer = typeof data === "string" ? Buffer.from(data, "utf-8") : data instanceof Buffer ? data : data instanceof Uint8Array ? Buffer.from(data) : Buffer.from(await data.text(), "utf-8"); this.files.set(path, buffer); } /** * Reset all in-memory state (useful between tests). */ reset() { this.files.clear(); this.directories.clear(); this.mkdirCalls = []; this.writeFileCalls = []; this.readFileCalls = []; this.rmCalls = []; this.joinCalls = []; this.mkdirError = null; this.writeFileError = null; this.readFileError = null; } /** * Check if a file was written during the test. * * @example * ```typescript * expect(fs.wasWritten("/project/tsconfig.json")).toBe(true); * ``` */ wasWritten(path) { return this.writeFileCalls.some((call) => call.path === path); } /** * Check if a file was written with content matching a pattern. * * @example * ```typescript * expect(fs.wasWrittenMatching("/project/tsconfig.json", /extends/)).toBe(true); * ``` */ wasWrittenMatching(path, pattern) { const call = this.writeFileCalls.find((c) => c.path === path); return call ? pattern.test(call.data) : false; } /** * Check if a file was read during the test. * * @example * ```typescript * expect(fs.wasRead("/project/package.json")).toBe(true); * ``` */ wasRead(path) { return this.readFileCalls.includes(path); } /** * Check if a file was deleted during the test. * * @example * ```typescript * expect(fs.wasDeleted("/project/old-file.txt")).toBe(true); * ``` */ wasDeleted(path) { return this.rmCalls.some((call) => call.path === path); } /** * Get the content of a file as a string (convenience method for testing). */ getFileContent(path) { return this.files.get(path)?.toString("utf-8"); } }; //#endregion //#region ../../src/system/providers/MemoryShellProvider.ts /** * In-memory implementation of ShellProvider for testing. * * Records all commands that would be executed without actually running them. * Can be configured to return specific outputs or throw errors for testing. * * @example * ```typescript * // In tests, substitute the real ShellProvider with MemoryShellProvider * const alepha = Alepha.create().with({ * provide: ShellProvider, * use: MemoryShellProvider, * }); * * // Configure mock behavior * const shell = alepha.inject(MemoryShellProvider); * shell.configure({ * outputs: { "echo hello": "hello\n" }, * errors: { "failing-cmd": "Command failed" }, * }); * * // Or use the fluent API * shell.outputs.set("another-cmd", "output"); * shell.errors.set("another-error", "Error message"); * * // Run code that uses ShellProvider * const service = alepha.inject(MyService); * await service.doSomething(); * * // Verify commands were called * expect(shell.calls).toHaveLength(2); * expect(shell.calls[0].command).toBe("yarn install"); * ``` */ var MemoryShellProvider = class { /** * All recorded shell calls. */ calls = []; /** * Simulated outputs for specific commands. */ outputs = /* @__PURE__ */ new Map(); /** * Commands that should throw an error. */ errors = /* @__PURE__ */ new Map(); /** * Commands considered installed in the system PATH. */ installedCommands = /* @__PURE__ */ new Set(); /** * Configure the mock with predefined outputs, errors, and installed commands. */ configure(options) { if (options.outputs) for (const [cmd, output] of Object.entries(options.outputs)) this.outputs.set(cmd, output); if (options.errors) for (const [cmd, error] of Object.entries(options.errors)) this.errors.set(cmd, error); if (options.installedCommands) for (const cmd of options.installedCommands) this.installedCommands.add(cmd); return this; } /** * Record command and return simulated output. */ async run(command, options = {}) { this.calls.push({ command, options }); const errorMsg = this.errors.get(command); if (errorMsg) throw new AlephaError(errorMsg); return this.outputs.get(command) ?? ""; } /** * Check if a specific command was called. */ wasCalled(command) { return this.calls.some((call) => call.command === command); } /** * Check if a command matching a pattern was called. */ wasCalledMatching(pattern) { return this.calls.some((call) => pattern.test(call.command)); } /** * Get all calls matching a pattern. */ getCallsMatching(pattern) { return this.calls.filter((call) => pattern.test(call.command)); } /** * Check if a command is installed. */ async isInstalled(command) { return this.installedCommands.has(command); } /** * Reset all recorded state. */ reset() { this.calls = []; this.outputs.clear(); this.errors.clear(); this.installedCommands.clear(); } }; //#endregion //#region ../../src/system/services/FileDetector.ts /** * Service for detecting file types and getting content types. * * @example * ```typescript * const detector = alepha.inject(FileDetector); * * // Get content type from filename * const mimeType = detector.getContentType("image.png"); // "image/png" * * // Detect file type by magic bytes * const stream = createReadStream('image.png'); * const result = await detector.detectFileType(stream, 'image.png'); * console.log(result.mimeType); // 'image/png' * console.log(result.verified); // true if magic bytes match * ``` */ var FileDetector = class FileDetector { /** * Magic byte signatures for common file formats. * Each signature is represented as an array of bytes or null (wildcard). */ static MAGIC_BYTES = { png: [{ signature: [ 137, 80, 78, 71, 13, 10, 26, 10 ], mimeType: "image/png" }], jpg: [ { signature: [ 255, 216, 255, 224 ], mimeType: "image/jpeg" }, { signature: [ 255, 216, 255, 225 ], mimeType: "image/jpeg" }, { signature: [ 255, 216, 255, 226 ], mimeType: "image/jpeg" }, { signature: [ 255, 216, 255, 227 ], mimeType: "image/jpeg" }, { signature: [ 255, 216, 255, 232 ], mimeType: "image/jpeg" } ], jpeg: [ { signature: [ 255, 216, 255, 224 ], mimeType: "image/jpeg" }, { signature: [ 255, 216, 255, 225 ], mimeType: "image/jpeg" }, { signature: [ 255, 216, 255, 226 ], mimeType: "image/jpeg" }, { signature: [ 255, 216, 255, 227 ], mimeType: "image/jpeg" }, { signature: [ 255, 216, 255, 232 ], mimeType: "image/jpeg" } ], gif: [{ signature: [ 71, 73, 70, 56, 55, 97 ], mimeType: "image/gif" }, { signature: [ 71, 73, 70, 56, 57, 97 ], mimeType: "image/gif" }], webp: [{ signature: [ 82, 73, 70, 70, null, null, null, null, 87, 69, 66, 80 ], mimeType: "image/webp" }], bmp: [{ signature: [66, 77], mimeType: "image/bmp" }], ico: [{ signature: [ 0, 0, 1, 0 ], mimeType: "image/x-icon" }], tiff: [{ signature: [ 73, 73, 42, 0 ], mimeType: "image/tiff" }, { signature: [ 77, 77, 0, 42 ], mimeType: "image/tiff" }], tif: [{ signature: [ 73, 73, 42, 0 ], mimeType: "image/tiff" }, { signature: [ 77, 77, 0, 42 ], mimeType: "image/tiff" }], pdf: [{ signature: [ 37, 80, 68, 70, 45 ], mimeType: "application/pdf" }], zip: [ { signature: [ 80, 75, 3, 4 ], mimeType: "application/zip" }, { signature: [ 80, 75, 5, 6 ], mimeType: "application/zip" }, { signature: [ 80, 75, 7, 8 ], mimeType: "application/zip" } ], rar: [{ signature: [ 82, 97, 114, 33, 26, 7 ], mimeType: "application/vnd.rar" }], "7z": [{ signature: [ 55, 122, 188, 175, 39, 28 ], mimeType: "application/x-7z-compressed" }], tar: [{ signature: [ 117, 115, 116, 97, 114 ], mimeType: "application/x-tar" }], gz: [{ signature: [31, 139], mimeType: "application/gzip" }], tgz: [{ signature: [31, 139], mimeType: "application/gzip" }], mp3: [ { signature: [255, 251], mimeType: "audio/mpeg" }, { signature: [255, 243], mimeType: "audio/mpeg" }, { signature: [255, 242], mimeType: "audio/mpeg" }, { signature: [ 73, 68, 51 ], mimeType: "audio/mpeg" } ], wav: [{ signature: [ 82, 73, 70, 70, null, null, null, null, 87, 65, 86, 69 ], mimeType: "audio/wav" }], ogg: [{ signature: [ 79, 103, 103, 83 ], mimeType: "audio/ogg" }], flac: [{ signature: [ 102, 76, 97, 67 ], mimeType: "audio/flac" }], mp4: [ { signature: [ null, null, null, null, 102, 116, 121, 112 ], mimeType: "video/mp4" }, { signature: [ null, null, null, null, 102, 116, 121, 112, 105, 115, 111, 109 ], mimeType: "video/mp4" }, { signature: [ null, null, null, null, 102, 116, 121, 112, 109, 112, 52, 50 ], mimeType: "video/mp4" } ], webm: [{ signature: [ 26, 69, 223, 163 ], mimeType: "video/webm" }], avi: [{ signature: [ 82, 73, 70, 70, null, null, null, null, 65, 86, 73, 32 ], mimeType: "video/x-msvideo" }], mov: [{ signature: [ null, null, null, null, 102, 116, 121, 112, 113, 116, 32, 32 ], mimeType: "video/quicktime" }], mkv: [{ signature: [ 26, 69, 223, 163 ], mimeType: "video/x-matroska" }], docx: [{ signature: [ 80, 75, 3, 4 ], mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }], xlsx: [{ signature: [ 80, 75, 3, 4 ], mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }], pptx: [{ signature: [ 80, 75, 3, 4 ], mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation" }], doc: [{ signature: [ 208, 207, 17, 224, 161, 177, 26, 225 ], mimeType: "application/msword" }], xls: [{ signature: [ 208, 207, 17, 224, 161, 177, 26, 225 ], mimeType: "application/vnd.ms-excel" }], ppt: [{ signature: [ 208, 207, 17, 224, 161, 177, 26, 225 ], mimeType: "application/vnd.ms-powerpoint" }] }; /** * All possible format signatures for checking against actual file content */ static ALL_SIGNATURES = Object.entries(FileDetector.MAGIC_BYTES).flatMap(([ext, signatures]) => signatures.map((sig) => ({ ext, ...sig }))); /** * MIME type map for file extensions. * * Can be used to get the content type of file based on its extension. * Feel free to add more mime types in your project! */ static mimeMap = { json: "application/json", txt: "text/plain", html: "text/html", htm: "text/html", xml: "application/xml", csv: "text/csv", pdf: "application/pdf", md: "text/markdown", markdown: "text/markdown", rtf: "application/rtf", css: "text/css", js: "application/javascript", mjs: "application/javascript", ts: "application/typescript", jsx: "text/jsx", tsx: "text/tsx", zip: "application/zip", rar: "application/vnd.rar", "7z": "application/x-7z-compressed", tar: "application/x-tar", gz: "application/gzip", tgz: "application/gzip", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", svg: "image/svg+xml", bmp: "image/bmp", ico: "image/x-icon", tiff: "image/tiff", tif: "image/tiff", mp3: "audio/mpeg", wav: "audio/wav", ogg: "audio/ogg", m4a: "audio/mp4", aac: "audio/aac", flac: "audio/flac", mp4: "video/mp4", webm: "video/webm", avi: "video/x-msvideo", mov: "video/quicktime", wmv: "video/x-ms-wmv", flv: "video/x-flv", mkv: "video/x-matroska", doc: "application/msword", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", xls: "application/vnd.ms-excel", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ppt: "application/vnd.ms-powerpoint", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation", woff: "font/woff", woff2: "font/woff2", ttf: "font/ttf", otf: "font/otf", eot: "application/vnd.ms-fontobject" }; /** * Reverse MIME type map for looking up extensions from MIME types. * Prefers shorter, more common extensions when multiple exist. */ static reverseMimeMap = (() => { const reverse = {}; for (const [ext, mimeType] of Object.entries(FileDetector.mimeMap)) if (!reverse[mimeType]) reverse[mimeType] = ext; return reverse; })(); /** * Returns the file extension for a given MIME type. * * @param mimeType - The MIME type to look up * @returns The file extension (without dot), or "bin" if not found * * @example * ```typescript * const detector = alepha.inject(FileDetector); * const ext = detector.getExtensionFromMimeType("image/png"); // "png" * const ext2 = detector.getExtensionFromMimeType("application/octet-stream"); // "bin" * ``` */ getExtensionFromMimeType(mimeType) { return FileDetector.reverseMimeMap[mimeType] || "bin"; } /** * Returns the content type of file based on its filename. * * @param filename - The filename to check * @returns The MIME type * * @example * ```typescript * const detector = alepha.inject(FileDetector); * const mimeType = detector.getContentType("image.png"); // "image/png" * ``` */ getContentType(filename) { const ext = filename.toLowerCase().split(".").pop() || ""; return FileDetector.mimeMap[ext] || "application/octet-stream"; } /** * Detects the file type by checking magic bytes against the stream content. * * @param stream - The readable stream to check * @param filename - The filename (used to get the extension) * @returns File type information including MIME type, extension, and verification status * * @example * ```typescript * const detector = alepha.inject(FileDetector); * const stream = createReadStream('image.png'); * const result = await detector.detectFileType(stream, 'image.png'); * console.log(result.mimeType); // 'image/png' * console.log(result.verified); // true if magic bytes match * ``` */ async detectFileType(stream, filename) { const expectedMimeType = this.getContentType(filename); const lastDotIndex = filename.lastIndexOf("."); const ext = lastDotIndex > 0 ? filename.substring(lastDotIndex + 1).toLowerCase() : ""; const { buffer, stream: newStream } = await this.peekBytes(stream, 16); const expectedSignatures = FileDetector.MAGIC_BYTES[ext]; if (expectedSignatures) { for (const { signature, mimeType } of expectedSignatures) if (this.matchesSignature(buffer, signature)) return { mimeType, extension: ext, verified: true, stream: newStream }; } for (const { ext: detectedExt, signature, mimeType } of FileDetector.ALL_SIGNATURES) if (detectedExt !== ext && this.matchesSignature(buffer, signature)) return { mimeType, extension: detectedExt, verified: true, stream: newStream }; return { mimeType: expectedMimeType, extension: ext, verified: false, stream: newStream }; } /** * Reads all bytes from a stream and returns the first N bytes along with a new stream containing all data. * This approach reads the entire stream upfront to avoid complex async handling issues. * * @protected */ async peekBytes(stream, numBytes) { const chunks = []; for await (const chunk of stream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); const allData = Buffer.concat(chunks); return { buffer: allData.subarray(0, numBytes), stream: Readable.from(allData) }; } /** * Checks if a buffer matches a magic byte signature. * * @protected */ matchesSignature(buffer, signature) { if (buffer.length < signature.length) return false; for (let i = 0; i < signature.length; i++) if (signature[i] !== null && buffer[i] !== signature[i]) return false; return true; } }; //#endregion //#region ../../src/system/providers/NodeFileSystemProvider.ts /** * Node.js implementation of FileSystem interface. * * @example * ```typescript * const fs = alepha.inject(NodeFileSystemProvider); * * // Create from URL * const file1 = fs.createFile({ url: "file:///path/to/file.png" }); * * // Create from Buffer * const file2 = fs.createFile({ buffer: Buffer.from("hello"), name: "hello.txt" }); * * // Create from text * const file3 = fs.createFile({ text: "Hello, world!", name: "greeting.txt" }); * * // File operations * await fs.mkdir("/tmp/mydir", { recursive: true }); * await fs.cp("/src/file.txt", "/dest/file.txt"); * await fs.mv("/old/path.txt", "/new/path.txt"); * const files = await fs.ls("/tmp"); * await fs.rm("/tmp/file.txt"); * ``` */ var NodeFileSystemProvider = class { detector = $inject(FileDetector); json = $inject(Json); join(...paths) { return join(...paths); } /** * Creates a FileLike object from various sources. * * @param options - Options for creating the file * @returns A FileLike object * * @example * ```typescript * const fs = alepha.inject(NodeFileSystemProvider); * * // From URL * const file1 = fs.createFile({ url: "https://example.com/image.png" }); * * // From Buffer * const file2 = fs.createFile({ * buffer: Buffer.from("hello"), * name: "hello.txt", * type: "text/plain" * }); * * // From text * const file3 = fs.createFile({ text: "Hello!", name: "greeting.txt" }); * * // From stream with detection * const stream = createReadStream("/path/to/file.png"); * const file4 = fs.createFile({ stream, name: "image.png" }); * ``` */ createFile(options) { if ("path" in options) { const filePath = options.path; const filename = basename(filePath); return this.createFileFromUrl(`file://${filePath}`, { type: options.type, name: options.name || filename }); } if ("url" in options) return this.createFileFromUrl(options.url, { type: options.type, name: options.name }); if ("response" in options) { if (!options.response.body) throw new AlephaError("Response has no body stream"); const res = options.response; const sizeHeader = res.headers.get("content-length"); const size = sizeHeader ? parseInt(sizeHeader, 10) : void 0; let name = options.name; const contentDisposition = res.headers.get("content-disposition"); if (contentDisposition && !name) { const match = contentDisposition.match(/filename="?([^"]+)"?/); if (match) name = match[1]; } const type = options.type || res.headers.get("content-type") || void 0; return this.createFileFromStream(options.response.body, { type, name, size }); } if ("file" in options) return this.createFileFromWebFile(options.file, { type: options.type, name: options.name, size: options.size }); if ("buffer" in options) return this.createFileFromBuffer(options.buffer, { type: options.type, name: options.name }); if ("arrayBuffer" in options) return this.createFileFromBuffer(Buffer.from(options.arrayBuffer), { type: options.type, name: options.name }); if ("text" in options) return this.createFileFromBuffer(Buffer.from(options.text, "utf-8"), { type: options.type || "text/plain", name: options.name || "file.txt" }); if ("stream" in options) return this.createFileFromStream(options.stream, { type: options.type, name: options.name, size: options.size }); throw new AlephaError("Invalid createFile options: no valid source provided"); } /** * Removes a file or directory. * * @param path - The path to remove * @param options - Remove options * * @example * ```typescript * const fs = alepha.inject(NodeFileSystemProvider); * * // Remove a file * await fs.rm("/tmp/file.txt"); * * // Remove a directory recursively * await fs.rm("/tmp/mydir", { recursive: true }); * * // Remove with force (no error if doesn't exist) * await fs.rm("/tmp/maybe-exists.txt", { force: true }); * ``` */ async rm(path, options) { await rm(path, options); } /** * Copies a file or directory. * * @param src - Source path * @param dest - Destination path * @param options - Copy options * * @example * ```typescript * const fs = alepha.inject(NodeFileSystemProvider); * * // Copy a file * await fs.cp("/src/file.txt", "/dest/file.txt"); * * // Copy a directory (recursive by default) * await fs.cp("/src/dir", "/dest/dir"); * * // Copy with force (overwrite existing) * await fs.cp("/src/file.txt", "/dest/file.txt", { force: true }); * ``` */ async cp(src, dest, options) { if ((await stat(src)).isDirectory()) await cp(src, dest, { recursive: options?.recursive ?? true, force: options?.force ?? false }); else await copyFile(src, dest); } /** * Moves/renames a file or directory. * * @param src - Source path * @param dest - Destination path * * @example * ```typescript * const fs = alepha.inject(NodeFileSystemProvider); * * // Move/rename a file * await fs.mv("/old/path.txt", "/new/path.txt"); * * // Move a directory * await fs.mv("/old/dir", "/new/dir"); * ``` */ async mv(src, dest) { await rename(src, dest); } /** * Creates a directory. * * @param path - The directory path to create * @param options - Mkdir options * * @example * ```typescript * const fs = alepha.inject(NodeFileSystemProvider); * * // Create a directory * await fs.mkdir("/tmp/mydir"); * * // Create nested directories * await fs.mkdir("/tmp/path/to/dir", { recursive: true }); * * // Create with specific permissions * await fs.mkdir("/tmp/mydir", { mode: 0o755 }); * ``` */ async mkdir(path, options = {}) { const p = mkdir(path, { recursive: options.recursive ?? true, mode: options.mode }); if (options.force === false) await p; else await p.catch(() => {}); } /** * Lists files in a directory. * * @param path - The directory path to list * @param options - List options * @returns Array of filenames * * @example * ```typescript * const fs = alepha.inject(NodeFileSystemProvider); * * // List files in a directory * const files = await fs.ls("/tmp"); * console.log(files); // ["file1.txt", "file2.txt", "subdir"] * * // List with hidden files * const allFiles = await fs.ls("/tmp", { hidden: true }); * * // List recursively * const allFilesRecursive = await fs.ls("/tmp", { recursive: true }); * ``` */ async ls(path, options) { const entries = await readdir(path); const filteredEntries = options?.hidden ? entries : entries.filter((e) => !e.startsWith(".")); if (options?.recursive) { const allFiles = []; for (const entry of filteredEntries) { const fullPath = join(path, entry); if ((await stat(fullPath)).isDirectory()) { allFiles.push(entry); const subFiles = await this.ls(fullPath, options); allFiles.push(...subFiles.map((f) => join(entry, f))); } else allFiles.push(entry); } return allFiles; } return filteredEntries; } /** * Checks if a file or directory exists. * * @param path - The path to check * @returns True if the path exists, false otherwise * * @example * ```typescript * const fs = alepha.inject(NodeFileSystemProvider); * * if (await fs.exists("/tmp/file.txt")) { * console.log("File exists"); * } * ``` */ async exists(path) { try { await access(path); return true; } catch { return false; } } /** * Reads the content of a file. * * @param path - The file path to read * @returns The file content as a Buffer * * @example * ```typescript * const fs = alepha.inject(NodeFileSystemProvider); * * const buffer = await fs.readFile("/tmp/file.txt"); * console.log(buffer.toString("utf-8")); * ``` */ async readFile(path) { return await readFile(path); } /** * Writes data to a file. * * @param path - The file path to write to * @param data - The data to write (Buffer or string) * * @example * ```typescript * const fs = alepha.inject(NodeFileSystemProvider); * * // Write string * await fs.writeFile("/tmp/file.txt", "Hello, world!"); * * // Write Buffer * await fs.writeFile("/tmp/file.bin", Buffer.from([0x01, 0x02, 0x03])); * ``` */ async writeFile(path, data) { if (isFileLike(data)) { await writeFile(path, Readable.from(data.stream())); return; } await writeFile(path, data); } /** * Reads the content of a file as a string. * * @param path - The file path to read * @returns The file content as a string * * @example * ```typescript * const fs = alepha.inject(NodeFileSystemProvider); * const content = await fs.readTextFile("/tmp/file.txt"); * ``` */ async readTextFile(path) { return (await this.readFile(path)).toString("utf-8"); } /** * Reads the content of a file as JSON. * * @param path - The file path to read * @returns The parsed JSON content * * @example * ```typescript * const fs = alepha.inject(NodeFileSystemProvider); * const config = await fs.readJsonFile<{ name: string }>("/tmp/config.json"); * ``` */ async readJsonFile(path) { const text = await this.readTextFile(path); return this.json.parse(text); } /** * Creates a FileLike object from a Web File. * * @protected */ createFileFromWebFile(source, options = {}) { const name = options.name ?? source.name; return { name, type: options.type ?? (source.type || this.detector.getContentType(name)), size: options.size ?? source.size ?? 0, lastModified: source.lastModified || Date.now(), stream: () => source.stream(), arrayBuffer: async () => { return await source.arrayBuffer(); }, text: async () => { return await source.text(); } }; } /** * Creates a FileLike object from a Buffer. * * @protected */ createFileFromBuffer(source, options = {}) { const name = options.name ?? "file"; return { name, type: options.type ?? this.detector.getContentType(options.name ?? name), size: source.byteLength, lastModified: Date.now(), stream: () => Readable.from(source), arrayBuffer: async () => { return this.bufferToArrayBuffer(source); }, text: async () => { return source.toString("utf-8"); } }; } /** * Creates a FileLike object from a stream. * * @protected */ createFileFromStream(source, options = {}) { let buffer = null; return { name: options.name ?? "file", type: options.type ?? this.detector.getContentType(options.name ?? "file"), size: options.size ?? 0, lastModified: Date.now(), stream: () => source, _buffer: null, arrayBuffer: async () => { buffer ??= await this.streamToBuffer(source); return this.bufferToArrayBuffer(buffer); }, text: async () => { buffer ??= await this.streamToBuffer(source); return buffer.toString("utf-8"); } }; } /** * Creates a FileLike object from a URL. * * @protected */ createFileFromUrl(url, options = {}) { const parsedUrl = new URL(url); const filename = options.name || parsedUrl.pathname.split("/").pop() || "file"; let buffer = null; return { name: filename, type: options.type ?? this.detector.getContentType(filename), size: 0, lastModified: Date.now(), stream: () => this.createStreamFromUrl(url), arrayBuffer: async () => { buffer ??= await this.loadFromUrl(url); return this.bufferToArrayBuffer(buffer); }, text: async () => { buffer ??= await this.loadFromUrl(url); return buffer.toString("utf-8"); }, filepath: url }; } /** * Gets a streaming response from a URL. * * @protected */ getStreamingResponse(url) { const stream = new PassThrough(); fetch(url).then((res) => Readable.fromWeb(res.body).pipe(stream)).catch((err) => stream.destroy(err)); return stream; } /** * Loads data from a URL. * * @protected */ async loadFromUrl(url) { const parsedUrl = new URL(url); if (parsedUrl.protocol === "file:") return await readFile(fileURLToPath(url)); else if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") { const response = await fetch(url); if (!response.ok) throw new AlephaError(`Failed to fetch ${url}: ${response.status} ${response.statusText}`); const arrayBuffer = await response.arrayBuffer(); return Buffer.from(arrayBuffer); } else throw new AlephaError(`Unsupported protocol: ${parsedUrl.protocol}`); } /** * Creates a stream from a URL. * * @protected */ createStreamFromUrl(url) { const parsedUrl = new URL(url); if (parsedUrl.protocol === "file:") return createReadStream(fileURLToPath(url)); else if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") return this.getStreamingResponse(url); else throw new AlephaError(`Unsupported protocol: ${parsedUrl.protocol}`); } /** * Converts a stream-like object to a Buffer. * * @protected */ async streamToBuffer(streamLike) { const stream = streamLike instanceof Readable ? streamLike : Readable.fromWeb(streamLike); return new Promise((resolve, reject) => { const buffer = []; stream.on("data", (chunk) => buffer.push(Buffer.from(chunk))); stream.on("end", () => resolve(Buffer.concat(buffer))); stream.on("error", (err) => reject(new AlephaError("Error converting stream", { cause: err }))); }); } /** * Converts a Node.js Buffer to an ArrayBuffer. * * @protected */ bufferToArrayBuffer(buffer) { return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); } }; //#endregion //#region ../../src/system/providers/ShellProvider.ts /** * Abstract provider for executing shell commands and binaries. * * Implementations: * - `NodeShellProvider` - Real shell execution using Node.js child_process * - `MemoryShellProvider` - In-memory mock for testing * * @example * ```typescript * class MyService { * protected readonly shell = $inject(ShellProvider); * * async build() { * // Run shell command directly * await this.shell.run("yarn install"); * * // Run local binary with resolution * await this.shell.run("vite build", { resolve: true }); * * // Capture output * const output = await this.shell.run("echo hello", { capture: true }); * } * } * ``` */ var ShellProvider = class {}; //#endregion //#region ../../src/system/providers/WorkerdFileSystemProvider.ts /** * Web-standard implementation of FileSystemProvider for Cloudflare Workers and other edge runtimes. * * Uses only Web APIs (ReadableStream, TextEncoder, etc.) — no Node.js-specific APIs. * Provides working `createFile` with proper streaming support. * Filesystem operations (rm, cp, mv, etc.) are not available in edge runtimes and will throw. * * @example * ```typescript * const fs = alepha.inject(WorkerdFileSystemProvider); * * // Create from text (returns FileLike with web ReadableStream) * const file = fs.createFile({ text: "Hello!", name: "greeting.txt" }); * const stream = file.stream(); // ReadableStream (web standard) * ``` */ var WorkerdFileSystemProvider = class { detector = $inject(FileDetector); json = $inject(Json); encoder = new TextEncoder(); decoder = new TextDecoder(); join(...paths) { const parts = paths.join("/").replace(/\/+/g, "/").split("/"); const resolved = []; for (const part of parts) if (part === "..") resolved.pop(); else if (part !== ".") resolved.push(part); return resolved.join("/") || "."; } createFile(options) { if ("text" in options) return this.createFileFromText(options.text, { type: options.type, name: options.name }); if ("arrayBuffer" in options) return this.createFileFromArrayBuffer(options.arrayBuffer, { type: options.type, name: options.name }); if ("buffer" in options) { const ab = options.buffer instanceof ArrayBuffer ? options.buffer : options.buffer.buffer.slice(options.buffer.byteOffset, options.buffer.byteOffset + options.buffer.byteLength); return this.createFileFromArrayBuffer(ab, { type: options.type, name: options.name }); } if ("file" in options) return this.createFileFromWebFile(options.file, { type: options.type, name: options.name, size: options.size }); if ("response" in options) return this.createFileFromResponse(options.response, { type: options.type, name: options.name }); if ("stream" in options) return this.createFileFromStream(options.stream, { type: options.type, name: options.name, size: options.size }); if ("url" in options) return this.createFileFromUrl(options.url, { type: options.type, name: options.name }); if ("path" in options) throw new AlephaError("WorkerdFileSystemProvider.createFile: 'path' source is not supported in edge runtimes."); throw new AlephaError("WorkerdFileSystemProvider.createFile: unsupported options."); } createFileFromText(text, options = {}) { const encoded = this.encoder.encode(text); const name = options.name ?? "file.txt"; return { name, type: options.type ?? this.detector.getContentType(name), size: encoded.byteLength, lastModified: Date.now(), stream: () => new ReadableStream({ start(controller) { controller.enqueue(encoded); controller.close(); } }), arrayBuffer: async () => encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength), text: asyn