Files
aitbc/dev/env/node_modules/@nomicfoundation/hardhat-utils/src/fs.ts
aitbc 816e258d4c refactor: move brother_node development artifact to dev/test-nodes subdirectory
Development Artifact Cleanup:
 BROTHER_NODE REORGANIZATION: Moved development test node to appropriate location
- dev/test-nodes/brother_node/: Moved from root directory for better organization
- Contains development configuration, test logs, and test chain data
- No impact on production systems - purely development/testing artifact

 DEVELOPMENT ARTIFACTS IDENTIFIED:
- Chain ID: aitbc-brother-chain (test/development chain)
- Ports: 8010 (P2P) and 8011 (RPC) - different from production
- Environment: .env file with test configuration
- Logs: rpc.log and node.log from development testing session (March 15, 2026)

 ROOT DIRECTORY CLEANUP: Removed development clutter from production directory
- brother_node/ moved to dev/test-nodes/brother_node/
- Root directory now contains only production-ready components
- Development artifacts properly organized in dev/ subdirectory

DIRECTORY STRUCTURE IMPROVEMENT:
📁 dev/test-nodes/: Development and testing node configurations
🏗️ Root Directory: Clean production structure with only essential components
🧪 Development Isolation: Test environments separated from production

BENEFITS:
 Clean Production Directory: No development artifacts in root
 Better Organization: Development nodes grouped in dev/ subdirectory
 Clear Separation: Production vs development environments clearly distinguished
 Maintainability: Easier to identify and manage development components

RESULT: Successfully moved brother_node development artifact to dev/test-nodes/ subdirectory, cleaning up the root directory while preserving development testing environment for future use.
2026-03-30 17:09:06 +02:00

889 lines
27 KiB
TypeScript
Executable File

import type { JsonTypes, ParsedElementInfo } from "@streamparser/json-node";
import type { FileHandle } from "node:fs/promises";
import fsPromises from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { pipeline } from "node:stream/promises";
import { JSONParser } from "@streamparser/json-node";
import { JsonStreamStringify } from "json-stream-stringify";
import { ensureError, ensureNodeErrnoExceptionError } from "./error.js";
import {
FileNotFoundError,
FileSystemAccessError,
InvalidFileFormatError,
JsonSerializationError,
FileAlreadyExistsError,
NotADirectoryError,
IsDirectoryError,
DirectoryNotEmptyError,
} from "./errors/fs.js";
/**
* Determines the canonical pathname for a given path, resolving any symbolic
* links, and returns it.
*
* @throws FileNotFoundError if absolutePath doesn't exist.
* @throws FileSystemAccessError for any other error.
*/
export async function getRealPath(absolutePath: string): Promise<string> {
try {
return await fsPromises.realpath(path.normalize(absolutePath));
} catch (e) {
ensureNodeErrnoExceptionError(e);
if (e.code === "ENOENT") {
throw new FileNotFoundError(absolutePath, e);
}
throw new FileSystemAccessError(e.message, e);
}
}
/**
* Recursively searches a directory and its subdirectories for files that
* satisfy the specified condition, returning their absolute paths.
*
* @param dirFrom The absolute path of the directory to start the search from.
* @param matches A function to filter files (not directories).
* @param directoryFilter A function to filter which directories to recurse into
* @returns An array of absolute paths. Each file has its true case, except
* for the initial dirFrom part, which preserves the given casing.
* No order is guaranteed. If dirFrom doesn't exist `[]` is returned.
* @throws NotADirectoryError if dirFrom is not a directory.
* @throws FileSystemAccessError for any other error.
*/
export async function getAllFilesMatching(
dirFrom: string,
matches?: (absolutePathToFile: string) => Promise<boolean> | boolean,
directoryFilter?: (absolutePathToDir: string) => Promise<boolean> | boolean,
): Promise<string[]> {
const dirContent = await readdirOrEmpty(dirFrom);
const results = await Promise.all(
dirContent.map(async (file) => {
const absolutePathToFile = path.join(dirFrom, file);
if (await isDirectory(absolutePathToFile)) {
if (
directoryFilter === undefined ||
(await directoryFilter(absolutePathToFile))
) {
return getAllFilesMatching(
absolutePathToFile,
matches,
directoryFilter,
);
}
return [];
} else if (matches === undefined || (await matches(absolutePathToFile))) {
return absolutePathToFile;
} else {
return [];
}
}),
);
return results.flat();
}
/**
* Recursively searches a directory and its subdirectories for directories that
* satisfy the specified condition, returning their absolute paths. Once a
* directory is found, its subdirectories are not searched.
*
* Note: dirFrom is never returned, nor is `matches` called on it.
*
* @param dirFrom The absolute path of the directory to start the search from.
* @param matches A function to filter directories (not files).
* @returns An array of absolute paths. Each path has its true case, except
* for the initial dirFrom part, which preserves the given casing.
* No order is guaranteed. If dirFrom doesn't exist `[]` is returned.
* @throws NotADirectoryError if dirFrom is not a directory.
* @throws FileSystemAccessError for any other error.
*/
export async function getAllDirectoriesMatching(
dirFrom: string,
matches?: (absolutePathToDir: string) => Promise<boolean> | boolean,
): Promise<string[]> {
const dirContent = await readdirOrEmpty(dirFrom);
const results = await Promise.all(
dirContent.map(async (file) => {
const absolutePathToFile = path.join(dirFrom, file);
if (!(await isDirectory(absolutePathToFile))) {
return [];
}
if (matches === undefined || (await matches(absolutePathToFile))) {
return absolutePathToFile;
}
return getAllDirectoriesMatching(absolutePathToFile, matches);
}),
);
return results.flat();
}
/**
* Determines the true case path of a given relative path from a specified
* directory, without resolving symbolic links, and returns it.
*
* @param from The absolute path of the directory to start the search from.
* @param relativePath The relative path to get the true case of.
* @returns The true case of the relative path.
* @throws FileNotFoundError if the starting directory or the relative path doesn't exist.
* @throws NotADirectoryError if the starting directory is not a directory.
* @throws FileSystemAccessError for any other error.
*/
export async function getFileTrueCase(
from: string,
relativePath: string,
): Promise<string> {
const dirEntries = await readdirOrEmpty(from);
const segments = relativePath.split(path.sep);
const nextDir = segments[0];
const nextDirLowerCase = nextDir.toLowerCase();
for (const dirEntry of dirEntries) {
if (dirEntry.toLowerCase() === nextDirLowerCase) {
if (segments.length === 1) {
return dirEntry;
}
return path.join(
dirEntry,
await getFileTrueCase(
path.join(from, dirEntry),
path.relative(nextDir, relativePath),
),
);
}
}
throw new FileNotFoundError(path.join(from, relativePath));
}
/**
* Checks if a given path is a directory.
*
* @param absolutePath The path to check.
* @returns `true` if the path is a directory, `false` otherwise.
* @throws FileNotFoundError if the path doesn't exist.
* @throws FileSystemAccessError for any other error.
*/
export async function isDirectory(absolutePath: string): Promise<boolean> {
try {
return (await fsPromises.lstat(absolutePath)).isDirectory();
} catch (e) {
ensureNodeErrnoExceptionError(e);
if (e.code === "ENOENT") {
throw new FileNotFoundError(absolutePath, e);
}
throw new FileSystemAccessError(e.message, e);
}
}
/**
* Reads a JSON file and parses it. The encoding used is "utf8".
*
* @param absolutePathToFile The path to the file.
* @returns The parsed JSON object.
* @throws FileNotFoundError if the file doesn't exist.
* @throws InvalidFileFormatError if the file is not a valid JSON file.
* @throws IsDirectoryError if the path is a directory instead of a file.
* @throws FileSystemAccessError for any other error.
*/
export async function readJsonFile<T>(absolutePathToFile: string): Promise<T> {
const content = await readUtf8File(absolutePathToFile);
try {
return JSON.parse(content.toString());
} catch (e) {
ensureError(e);
throw new InvalidFileFormatError(absolutePathToFile, e);
}
}
/**
* Reads a JSON file as a stream and parses it. The encoding used is "utf8".
* This function should be used when parsing very large JSON files.
*
* @param absolutePathToFile The path to the file.
* @returns The parsed JSON object.
* @throws FileNotFoundError if the file doesn't exist.
* @throws InvalidFileFormatError if the file is not a valid JSON file.
* @throws IsDirectoryError if the path is a directory instead of a file.
* @throws FileSystemAccessError for any other error.
*/
export async function readJsonFileAsStream<T>(
absolutePathToFile: string,
): Promise<T> {
let fileHandle: FileHandle | undefined;
try {
fileHandle = await fsPromises.open(absolutePathToFile, "r");
const fileReadStream = fileHandle.createReadStream();
// NOTE: We set a separator to disable self-closing to be able to use the parser
// in the stream.pipeline context; see https://github.com/juanjoDiaz/streamparser-json/issues/47
const jsonParser = new JSONParser({
separator: "",
});
const result: T | undefined = await pipeline(
fileReadStream,
jsonParser,
async (
elements: AsyncIterable<ParsedElementInfo.ParsedElementInfo>,
): Promise<any | undefined> => {
let value: JsonTypes.JsonPrimitive | JsonTypes.JsonStruct | undefined;
for await (const element of elements) {
value = element.value;
}
return value;
},
);
if (result === undefined) {
throw new Error("No data");
}
return result;
} catch (e) {
ensureError(e);
// If the code is defined, we assume the error to be related to the file system
if ("code" in e) {
if (e.code === "ENOENT") {
throw new FileNotFoundError(absolutePathToFile, e);
}
if (e.code === "EISDIR") {
throw new IsDirectoryError(absolutePathToFile, e);
}
// If the code is defined, we assume the error to be related to the file system
if (e.code !== undefined) {
throw new FileSystemAccessError(absolutePathToFile, e);
}
}
// Otherwise, we assume the error to be related to the file formatting
throw new InvalidFileFormatError(absolutePathToFile, e);
} finally {
// Explicitly closing the file handle to fully release the underlying resources
await fileHandle?.close();
}
}
/**
* Writes an object to a JSON file. The encoding used is "utf8" and the file is overwritten.
* If part of the path doesn't exist, it will be created.
*
* @param absolutePathToFile The path to the file. If the file exists, it will be overwritten.
* @param object The object to write.
* @throws JsonSerializationError if the object can't be serialized to JSON.
* @throws FileSystemAccessError for any other error.
*/
export async function writeJsonFile<T>(
absolutePathToFile: string,
object: T,
): Promise<void> {
let content;
try {
content = JSON.stringify(object, null, 2);
} catch (e) {
ensureError(e);
throw new JsonSerializationError(absolutePathToFile, e);
}
await writeUtf8File(absolutePathToFile, content);
}
/**
* Writes an object to a JSON file as stream. The encoding used is "utf8" and the file is overwritten.
* If part of the path doesn't exist, it will be created.
* This function should be used when stringifying very large JSON objects.
*
* @param absolutePathToFile The path to the file. If the file exists, it will be overwritten.
* @param object The object to write.
* @throws JsonSerializationError if the object can't be serialized to JSON.
* @throws FileSystemAccessError for any other error.
*/
export async function writeJsonFileAsStream<T>(
absolutePathToFile: string,
object: T,
): Promise<void> {
const dirPath = path.dirname(absolutePathToFile);
const dirExists = await exists(dirPath);
if (!dirExists) {
await mkdir(dirPath);
}
let fileHandle: FileHandle | undefined;
try {
fileHandle = await fsPromises.open(absolutePathToFile, "w");
const jsonStream = new JsonStreamStringify(object);
const fileWriteStream = fileHandle.createWriteStream();
await pipeline(jsonStream, fileWriteStream);
} catch (e) {
ensureError(e);
// if the directory was created, we should remove it
if (dirExists === false) {
try {
await remove(dirPath);
// we don't want to override the original error
} catch (_error) {}
}
// If the code is defined, we assume the error to be related to the file system
if ("code" in e && e.code !== undefined) {
throw new FileSystemAccessError(e.message, e);
}
// Otherwise, we assume the error to be related to the file formatting
throw new JsonSerializationError(absolutePathToFile, e);
} finally {
// NOTE: Historically, not closing the file handle caused issues on Windows,
// for example, when trying to move the file previously written to by this function
await fileHandle?.close();
}
}
/**
* Reads a file and returns its content as a string. The encoding used is "utf8".
*
* @param absolutePathToFile The path to the file.
* @returns The content of the file as a string.
* @throws FileNotFoundError if the file doesn't exist.
* @throws IsDirectoryError if the path is a directory instead of a file.
* @throws FileSystemAccessError for any other error.
*/
export async function readUtf8File(
absolutePathToFile: string,
): Promise<string> {
try {
return await fsPromises.readFile(absolutePathToFile, { encoding: "utf8" });
} catch (e) {
ensureNodeErrnoExceptionError(e);
if (e.code === "ENOENT") {
throw new FileNotFoundError(absolutePathToFile, e);
}
if (e.code === "EISDIR") {
throw new IsDirectoryError(absolutePathToFile, e);
}
throw new FileSystemAccessError(e.message, e);
}
}
/**
* Writes a string to a file. The encoding used is "utf8" and the file is overwritten by default.
* If part of the path doesn't exist, it will be created.
*
* @param absolutePathToFile The path to the file.
* @param data The data to write.
* @param flag The flag to use when writing the file. If not provided, the file will be overwritten.
* See https://nodejs.org/docs/latest-v20.x/api/fs.html#file-system-flags for more information.
* @throws FileAlreadyExistsError if the file already exists and the flag "x" is used.
* @throws FileSystemAccessError for any other error.
*/
export async function writeUtf8File(
absolutePathToFile: string,
data: string,
flag?: string,
): Promise<void> {
const dirPath = path.dirname(absolutePathToFile);
const dirExists = await exists(dirPath);
if (!dirExists) {
await mkdir(dirPath);
}
try {
await fsPromises.writeFile(absolutePathToFile, data, {
encoding: "utf8",
flag,
});
} catch (e) {
ensureNodeErrnoExceptionError(e);
// if the directory was created, we should remove it
if (dirExists === false) {
try {
await remove(dirPath);
// we don't want to override the original error
} catch (_error) {}
}
if (e.code === "ENOENT") {
throw new FileNotFoundError(absolutePathToFile, e);
}
// flag "x" has been used and the file already exists
if (e.code === "EEXIST") {
throw new FileAlreadyExistsError(absolutePathToFile, e);
}
throw new FileSystemAccessError(e.message, e);
}
}
/**
* Reads a file and returns its content as a Uint8Array.
*
* @param absolutePathToFile The path to the file.
* @returns The content of the file as a Uint8Array.
* @throws FileNotFoundError if the file doesn't exist.
* @throws IsDirectoryError if the path is a directory instead of a file.
* @throws FileSystemAccessError for any other error.
*/
export async function readBinaryFile(
absolutePathToFile: string,
): Promise<Uint8Array> {
try {
const buffer = await fsPromises.readFile(absolutePathToFile);
return new Uint8Array(buffer);
} catch (e) {
ensureNodeErrnoExceptionError(e);
if (e.code === "ENOENT") {
throw new FileNotFoundError(absolutePathToFile, e);
}
if (e.code === "EISDIR") {
throw new IsDirectoryError(absolutePathToFile, e);
}
throw new FileSystemAccessError(e.message, e);
}
}
/**
* Reads a directory and returns its content as an array of strings.
*
* @param absolutePathToDir The path to the directory.
* @returns An array of strings with the names of the files and directories in the directory.
* @throws FileNotFoundError if the directory doesn't exist.
* @throws NotADirectoryError if the path is not a directory.
* @throws FileSystemAccessError for any other error.
*/
export async function readdir(absolutePathToDir: string): Promise<string[]> {
try {
return await fsPromises.readdir(absolutePathToDir);
} catch (e) {
ensureNodeErrnoExceptionError(e);
if (e.code === "ENOENT") {
throw new FileNotFoundError(absolutePathToDir, e);
}
if (e.code === "ENOTDIR") {
throw new NotADirectoryError(absolutePathToDir, e);
}
throw new FileSystemAccessError(e.message, e);
}
}
/**
* Wrapper around `readdir` that returns an empty array if the directory doesn't exist.
*
* @see readdir
*/
async function readdirOrEmpty(dirFrom: string): Promise<string[]> {
try {
return await readdir(dirFrom);
} catch (error) {
if (error instanceof FileNotFoundError) {
return [];
}
throw error;
}
}
/**
* Creates a directory and any necessary directories along the way. If the directory already exists,
* nothing is done.
*
* @param absolutePath The path to the directory to create.
* @throws FileSystemAccessError for any error.
*/
export async function mkdir(absolutePath: string): Promise<void> {
try {
await fsPromises.mkdir(absolutePath, { recursive: true });
} catch (e) {
ensureNodeErrnoExceptionError(e);
throw new FileSystemAccessError(e.message, e);
}
}
/**
* Alias for `mkdir`.
* @see mkdir
*/
export const ensureDir: typeof mkdir = mkdir;
/**
* Creates a temporary directory with the specified prefix.
*
* @param prefix The prefix to use for the temporary directory.
* @returns The absolute path to the created temporary directory.
* @throws FileSystemAccessError for any error.
*/
export async function mkdtemp(prefix: string): Promise<string> {
try {
return await getRealPath(
await fsPromises.mkdtemp(path.join(tmpdir(), prefix)),
);
} catch (e) {
ensureNodeErrnoExceptionError(e);
throw new FileSystemAccessError(e.message, e);
}
}
/**
* Retrieves the last change time of a file or directory's properties.
* This includes changes to the file's metadata or contents.
*
* @param absolutePath The absolute path to the file or directory.
* @returns The time of the last change as a Date object.
* @throws FileNotFoundError if the path does not exist.
* @throws FileSystemAccessError for any other error.
*/
export async function getChangeTime(absolutePath: string): Promise<Date> {
try {
const stats = await fsPromises.stat(absolutePath);
return stats.ctime;
} catch (e) {
ensureNodeErrnoExceptionError(e);
if (e.code === "ENOENT") {
throw new FileNotFoundError(absolutePath, e);
}
throw new FileSystemAccessError(e.message, e);
}
}
/**
* Retrieves the last access time of a file or directory's properties.
*
* @param absolutePath The absolute path to the file or directory.
* @returns The time of the last access as a Date object.
* @throws FileNotFoundError if the path does not exist.
* @throws FileSystemAccessError for any other error.
*/
export async function getAccessTime(absolutePath: string): Promise<Date> {
try {
const stats = await fsPromises.stat(absolutePath);
return stats.atime;
} catch (e) {
ensureNodeErrnoExceptionError(e);
if (e.code === "ENOENT") {
throw new FileNotFoundError(absolutePath, e);
}
throw new FileSystemAccessError(e.message, e);
}
}
/**
* Retrieves the size of a file.
*
* @param absolutePath The absolute path to the file.
* @returns The size of the file in bytes.
* @throws FileNotFoundError if the path does not exist.
* @throws FileSystemAccessError for any other error.
*/
export async function getFileSize(absolutePath: string): Promise<number> {
try {
const stats = await fsPromises.stat(absolutePath);
return stats.size;
} catch (e) {
ensureNodeErrnoExceptionError(e);
if (e.code === "ENOENT") {
throw new FileNotFoundError(absolutePath, e);
}
throw new FileSystemAccessError(e.message, e);
}
}
/**
* Checks if a file or directory exists.
*
* @param absolutePath The absolute path to the file or directory.
* @returns A boolean indicating whether the file or directory exists.
*/
export async function exists(absolutePath: string): Promise<boolean> {
try {
await fsPromises.access(absolutePath);
return true;
} catch (_error) {
return false;
}
}
/**
* Copies a file from a source to a destination.
* If the destination file already exists, it will be overwritten.
*
* @param source The path to the source file. It can't be a directory.
* @param destination The path to the destination file. It can't be a directory.
* @throws FileNotFoundError if the source path or the destination path doesn't exist.
* @throws IsDirectoryError if the source path or the destination path is a directory.
* @throws FileSystemAccessError for any other error.
*/
export async function copy(source: string, destination: string): Promise<void> {
// We must proactively check if the source is a directory.
// On modern Linux kernels (6.x+), the `copy_file_range` system call used by
// Node.js may return success (0 bytes copied) when the source is a directory
// instead of throwing EISDIR. Node.js interprets this 0-byte success as a
// completed operation, resulting in no error being thrown.
if (await isDirectory(source)) {
throw new IsDirectoryError(source, undefined);
}
try {
await fsPromises.copyFile(source, destination);
} catch (e) {
ensureNodeErrnoExceptionError(e);
if (e.code === "ENOENT") {
if (!(await exists(source))) {
throw new FileNotFoundError(source, e);
}
if (!(await exists(destination))) {
throw new FileNotFoundError(destination, e);
}
}
// On linux, trying to copy a directory will throw EISDIR,
// on Windows it will throw EPERM, and on macOS it will throw ENOTSUP.
if (e.code === "EISDIR" || e.code === "EPERM" || e.code === "ENOTSUP") {
if (await isDirectory(source)) {
throw new IsDirectoryError(source, e);
}
if (await isDirectory(destination)) {
throw new IsDirectoryError(destination, e);
}
}
throw new FileSystemAccessError(e.message, e);
}
}
/**
* Moves a file or directory from a source to a destination. If the source is a
* file and the destination is a file that already exists, it will be overwritten.
* If the source is a directory and the destination is a directory, it needs to be empty.
*
* Note: This method may not work when moving files between different mount points
* or file systems, as the underlying `fsPromises.rename` method may not support it.
*
* @param source The path to the source file or directory.
* @param destination The path to the destination file or directory.
* @throws FileNotFoundError if the source path or the destination path doesn't exist.
* @throws DirectoryNotEmptyError if the source path is a directory and the destination
* path is a directory that is not empty.
* @throws FileSystemAccessError for any other error.
*/
export async function move(source: string, destination: string): Promise<void> {
try {
await fsPromises.rename(source, destination);
} catch (e) {
ensureNodeErrnoExceptionError(e);
if (e.code === "ENOENT") {
if (!(await exists(source))) {
throw new FileNotFoundError(source, e);
}
if (!(await exists(path.dirname(destination)))) {
throw new FileNotFoundError(destination, e);
}
}
// On linux, trying to move a non-empty directory will throw ENOTEMPTY,
// while on Windows it will throw EPERM.
if (e.code === "ENOTEMPTY" || e.code === "EPERM") {
if (await isDirectory(source)) {
throw new DirectoryNotEmptyError(destination, e);
}
}
throw new FileSystemAccessError(e.message, e);
}
}
/**
* Removes a file or directory recursively.
* Exceptions are ignored for non-existent paths.
*
* @param absolutePath The path to the file or directory to remove.
* @throws FileSystemAccessError for any error, except for non-existent path errors.
*/
export async function remove(absolutePath: string): Promise<void> {
try {
await fsPromises.rm(absolutePath, {
recursive: true,
force: true,
maxRetries: 3,
retryDelay: 300,
});
} catch (e) {
ensureNodeErrnoExceptionError(e);
throw new FileSystemAccessError(e.message, e);
}
}
/**
* Changes the permissions of a file or directory.
*
* @param absolutePath The path to the file or directory.
* @param mode The permissions to set. It can be a string or a number representing the octal mode.
* @throws FileNotFoundError if the path doesn't exist.
* @throws FileSystemAccessError for any other error.
*/
export async function chmod(
absolutePath: string,
mode: string | number,
): Promise<void> {
try {
await fsPromises.chmod(absolutePath, mode);
} catch (e) {
ensureNodeErrnoExceptionError(e);
if (e.code === "ENOENT") {
throw new FileNotFoundError(absolutePath, e);
}
throw new FileSystemAccessError(e.message, e);
}
}
/**
* Creates a file with an empty content. If the file already exists, it will be overwritten.
* If part of the path doesn't exist, it will be created.
*
* @param absolutePath The path to the file to create.
* @throws FileSystemAccessError for any other error.
*/
export async function createFile(absolutePath: string): Promise<void> {
await writeUtf8File(absolutePath, "");
}
/**
* Empties a directory by recursively removing all its content. If the
* directory doesn't exist, it will be created. The directory itself is
* not removed.
*
* @param absolutePath The path to the directory to empty.
* @throws NotADirectoryError if the path is not a directory.
* @throws FileSystemAccessError for any other error.
*/
export async function emptyDir(absolutePath: string): Promise<void> {
let isDir;
let mode;
try {
const stats = await fsPromises.stat(absolutePath);
isDir = stats.isDirectory();
mode = stats.mode;
} catch (e) {
ensureNodeErrnoExceptionError(e);
if (e.code === "ENOENT") {
await mkdir(absolutePath);
return;
}
throw new FileSystemAccessError(e.message, e);
}
if (!isDir) {
throw new NotADirectoryError(absolutePath, new Error());
}
await remove(absolutePath);
await mkdir(absolutePath);
// eslint-disable-next-line no-bitwise -- Bitwise is common in fs permissions
await chmod(absolutePath, mode & 0o777);
}
/**
* Looks for a file in the current directory and its parents.
*
* @param fileName The name of the file to look for.
* @param from The directory to start the search from. Defaults to the current working directory.
* @returns The absolute path to the file, or `undefined` if it wasn't found.
*/
export async function findUp(
fileName: string,
from?: string,
): Promise<string | undefined> {
if (from === undefined) {
from = process.cwd();
}
let currentDir = from;
while (true) {
const absolutePath = path.join(currentDir, fileName);
if (await exists(absolutePath)) {
return absolutePath;
}
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) {
return undefined;
}
currentDir = parentDir;
}
}
/**
* This function uses some heuristics to check if a file is binary by reading the first bytesToCheck bytes from the file.
*/
export async function isBinaryFile(
filePath: string,
bytesToCheck = 8000,
): Promise<boolean> {
const fd = await fsPromises.open(filePath, "r");
const buffer = Buffer.alloc(bytesToCheck);
const { bytesRead } = await fd.read(buffer, 0, bytesToCheck, 0);
await fd.close();
let nonPrintable = 0;
for (let i = 0; i < bytesRead; i++) {
const byte = buffer[i];
// Allow common text ranges: tab, newline, carriage return, and printable ASCII
if (
byte === 9 || // tab
byte === 10 || // newline
byte === 13 || // carriage return
(byte >= 32 && byte <= 126)
) {
continue;
}
nonPrintable++;
}
// Heuristic: if more than ~30% of bytes are non-printable, assume binary
return nonPrintable / bytesRead > 0.3;
}
export {
FileNotFoundError,
FileSystemAccessError,
InvalidFileFormatError,
JsonSerializationError,
FileAlreadyExistsError,
NotADirectoryError,
IsDirectoryError,
DirectoryNotEmptyError,
} from "./errors/fs.js";