alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
2,008 lines • 56 kB
JavaScript
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