refactor(agent): add result-based execution env
This commit is contained in:
359
packages/agent/src/harness/env/nodejs.ts
vendored
359
packages/agent/src/harness/env/nodejs.ts
vendored
@@ -1,33 +1,59 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { constants } from "node:fs";
|
||||
import { access, lstat, mkdir, mkdtemp, readdir, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
||||
import {
|
||||
access,
|
||||
appendFile,
|
||||
lstat,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readdir,
|
||||
readFile,
|
||||
realpath,
|
||||
rm,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { isAbsolute, join, resolve } from "node:path";
|
||||
import { type ExecutionEnv, FileError, type FileInfo, type FileKind } from "../types.js";
|
||||
import {
|
||||
type ExecutionEnv,
|
||||
ExecutionError,
|
||||
err,
|
||||
FileError,
|
||||
type FileInfo,
|
||||
type FileKind,
|
||||
ok,
|
||||
type Result,
|
||||
} from "../types.js";
|
||||
|
||||
function resolvePath(cwd: string, path: string): string {
|
||||
return isAbsolute(path) ? path : resolve(cwd, path);
|
||||
}
|
||||
|
||||
function fileKindFromStats(stats: { isFile(): boolean; isDirectory(): boolean; isSymbolicLink(): boolean }): FileKind {
|
||||
function fileKindFromStats(stats: {
|
||||
isFile(): boolean;
|
||||
isDirectory(): boolean;
|
||||
isSymbolicLink(): boolean;
|
||||
}): FileKind | undefined {
|
||||
if (stats.isFile()) return "file";
|
||||
if (stats.isDirectory()) return "directory";
|
||||
if (stats.isSymbolicLink()) return "symlink";
|
||||
throw new FileError("invalid", "Unsupported file type");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function fileInfoFromStats(
|
||||
path: string,
|
||||
stats: { isFile(): boolean; isDirectory(): boolean; isSymbolicLink(): boolean; size: number; mtimeMs: number },
|
||||
): FileInfo {
|
||||
return {
|
||||
): Result<FileInfo, FileError> {
|
||||
const kind = fileKindFromStats(stats);
|
||||
if (!kind) return err(new FileError("invalid", "Unsupported file type", path));
|
||||
return ok({
|
||||
name: path.replace(/\/+$/, "").split("/").pop() ?? path,
|
||||
path,
|
||||
kind: fileKindFromStats(stats),
|
||||
kind,
|
||||
size: stats.size,
|
||||
mtimeMs: stats.mtimeMs,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
||||
@@ -39,20 +65,26 @@ function toFileError(error: unknown, path?: string): FileError {
|
||||
if (isNodeError(error)) {
|
||||
const message = error.message;
|
||||
switch (error.code) {
|
||||
case "ABORT_ERR":
|
||||
return new FileError("aborted", message, path, error);
|
||||
case "ENOENT":
|
||||
return new FileError("not_found", message, path, { cause: error });
|
||||
return new FileError("not_found", message, path, error);
|
||||
case "EACCES":
|
||||
case "EPERM":
|
||||
return new FileError("permission_denied", message, path, { cause: error });
|
||||
return new FileError("permission_denied", message, path, error);
|
||||
case "ENOTDIR":
|
||||
return new FileError("not_directory", message, path, { cause: error });
|
||||
return new FileError("not_directory", message, path, error);
|
||||
case "EISDIR":
|
||||
return new FileError("is_directory", message, path, { cause: error });
|
||||
return new FileError("is_directory", message, path, error);
|
||||
case "EINVAL":
|
||||
return new FileError("invalid", message, path, { cause: error });
|
||||
return new FileError("invalid", message, path, error);
|
||||
}
|
||||
}
|
||||
return new FileError("unknown", error instanceof Error ? error.message : String(error), path, { cause: error });
|
||||
return new FileError("unknown", error instanceof Error ? error.message : String(error), path, error);
|
||||
}
|
||||
|
||||
function abortResult<TValue>(signal: AbortSignal | undefined, path?: string): Result<TValue, FileError> | undefined {
|
||||
return signal?.aborted ? err(new FileError("aborted", "aborted", path)) : undefined;
|
||||
}
|
||||
|
||||
async function pathExists(path: string): Promise<boolean> {
|
||||
@@ -71,7 +103,13 @@ async function runCommand(
|
||||
): Promise<{ stdout: string; status: number | null }> {
|
||||
return await new Promise((resolve) => {
|
||||
let stdout = "";
|
||||
const child = spawn(command, args, { stdio: ["ignore", "pipe", "ignore"] });
|
||||
let child: ReturnType<typeof spawn>;
|
||||
try {
|
||||
child = spawn(command, args, { stdio: ["ignore", "pipe", "ignore"] });
|
||||
} catch {
|
||||
resolve({ stdout: "", status: null });
|
||||
return;
|
||||
}
|
||||
const timeout = setTimeout(() => {
|
||||
if (child.pid) killProcessTree(child.pid);
|
||||
}, timeoutMs);
|
||||
@@ -100,12 +138,14 @@ async function findBashOnPath(): Promise<string | null> {
|
||||
return firstMatch && (await pathExists(firstMatch)) ? firstMatch : null;
|
||||
}
|
||||
|
||||
async function getShellConfig(customShellPath?: string): Promise<{ shell: string; args: string[] }> {
|
||||
async function getShellConfig(
|
||||
customShellPath?: string,
|
||||
): Promise<Result<{ shell: string; args: string[] }, ExecutionError>> {
|
||||
if (customShellPath) {
|
||||
if (await pathExists(customShellPath)) {
|
||||
return { shell: customShellPath, args: ["-c"] };
|
||||
return ok({ shell: customShellPath, args: ["-c"] });
|
||||
}
|
||||
throw new Error(`Custom shell path not found: ${customShellPath}`);
|
||||
return err(new ExecutionError("shell_unavailable", `Custom shell path not found: ${customShellPath}`));
|
||||
}
|
||||
if (process.platform === "win32") {
|
||||
const candidates: string[] = [];
|
||||
@@ -115,24 +155,24 @@ async function getShellConfig(customShellPath?: string): Promise<{ shell: string
|
||||
if (programFilesX86) candidates.push(`${programFilesX86}\\Git\\bin\\bash.exe`);
|
||||
for (const candidate of candidates) {
|
||||
if (await pathExists(candidate)) {
|
||||
return { shell: candidate, args: ["-c"] };
|
||||
return ok({ shell: candidate, args: ["-c"] });
|
||||
}
|
||||
}
|
||||
const bashOnPath = await findBashOnPath();
|
||||
if (bashOnPath) {
|
||||
return { shell: bashOnPath, args: ["-c"] };
|
||||
return ok({ shell: bashOnPath, args: ["-c"] });
|
||||
}
|
||||
throw new Error("No bash shell found");
|
||||
return err(new ExecutionError("shell_unavailable", "No bash shell found"));
|
||||
}
|
||||
|
||||
if (await pathExists("/bin/bash")) {
|
||||
return { shell: "/bin/bash", args: ["-c"] };
|
||||
return ok({ shell: "/bin/bash", args: ["-c"] });
|
||||
}
|
||||
const bashOnPath = await findBashOnPath();
|
||||
if (bashOnPath) {
|
||||
return { shell: bashOnPath, args: ["-c"] };
|
||||
return ok({ shell: bashOnPath, args: ["-c"] });
|
||||
}
|
||||
return { shell: "sh", args: ["-c"] };
|
||||
return ok({ shell: "sh", args: ["-c"] });
|
||||
}
|
||||
|
||||
function getShellEnv(baseEnv?: NodeJS.ProcessEnv, extraEnv?: Record<string, string>): NodeJS.ProcessEnv {
|
||||
@@ -184,46 +224,69 @@ export class NodeExecutionEnv implements ExecutionEnv {
|
||||
cwd?: string;
|
||||
env?: Record<string, string>;
|
||||
timeout?: number;
|
||||
signal?: AbortSignal;
|
||||
abortSignal?: AbortSignal;
|
||||
onStdout?: (chunk: string) => void;
|
||||
onStderr?: (chunk: string) => void;
|
||||
},
|
||||
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
||||
const cwd = options?.cwd ? resolvePath(this.cwd, options.cwd) : this.cwd;
|
||||
const { shell, args } = await getShellConfig(this.shellPath);
|
||||
): Promise<Result<{ stdout: string; stderr: string; exitCode: number }, ExecutionError>> {
|
||||
if (options?.abortSignal?.aborted) return err(new ExecutionError("aborted", "aborted"));
|
||||
|
||||
return await new Promise((resolvePromise, reject) => {
|
||||
const cwd = options?.cwd ? resolvePath(this.cwd, options.cwd) : this.cwd;
|
||||
const shellConfig = await getShellConfig(this.shellPath);
|
||||
if (!shellConfig.ok) return shellConfig;
|
||||
|
||||
return await new Promise((resolvePromise) => {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let settled = false;
|
||||
let timedOut = false;
|
||||
const child = spawn(shell, [...args, command], {
|
||||
cwd,
|
||||
detached: process.platform !== "win32",
|
||||
env: getShellEnv(this.shellEnv, options?.env),
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
let callbackError: ExecutionError | undefined;
|
||||
let child: ReturnType<typeof spawn> | undefined;
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const timeoutId =
|
||||
const onAbort = () => {
|
||||
if (child?.pid) {
|
||||
killProcessTree(child.pid);
|
||||
}
|
||||
};
|
||||
|
||||
const settle = (result: Result<{ stdout: string; stderr: string; exitCode: number }, ExecutionError>) => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
if (options?.abortSignal) options.abortSignal.removeEventListener("abort", onAbort);
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
resolvePromise(result);
|
||||
};
|
||||
|
||||
try {
|
||||
child = spawn(shellConfig.value.shell, [...shellConfig.value.args, command], {
|
||||
cwd,
|
||||
detached: process.platform !== "win32",
|
||||
env: getShellEnv(this.shellEnv, options?.env),
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
} catch (error) {
|
||||
settle(
|
||||
err(new ExecutionError("spawn_error", error instanceof Error ? error.message : String(error), error)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
timeoutId =
|
||||
typeof options?.timeout === "number"
|
||||
? setTimeout(() => {
|
||||
timedOut = true;
|
||||
if (child.pid) {
|
||||
if (child?.pid) {
|
||||
killProcessTree(child.pid);
|
||||
}
|
||||
}, options.timeout * 1000)
|
||||
: undefined;
|
||||
|
||||
const onAbort = () => {
|
||||
if (child.pid) {
|
||||
killProcessTree(child.pid);
|
||||
}
|
||||
};
|
||||
if (options?.signal) {
|
||||
if (options.signal.aborted) {
|
||||
if (options?.abortSignal) {
|
||||
if (options.abortSignal.aborted) {
|
||||
onAbort();
|
||||
} else {
|
||||
options.signal.addEventListener("abort", onAbort, { once: true });
|
||||
options.abortSignal.addEventListener("abort", onAbort, { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,137 +294,231 @@ export class NodeExecutionEnv implements ExecutionEnv {
|
||||
child.stderr?.setEncoding("utf8");
|
||||
child.stdout?.on("data", (chunk: string) => {
|
||||
stdout += chunk;
|
||||
options?.onStdout?.(chunk);
|
||||
try {
|
||||
options?.onStdout?.(chunk);
|
||||
} catch (error) {
|
||||
callbackError = new ExecutionError(
|
||||
"unknown",
|
||||
error instanceof Error ? error.message : String(error),
|
||||
error,
|
||||
);
|
||||
onAbort();
|
||||
}
|
||||
});
|
||||
child.stderr?.on("data", (chunk: string) => {
|
||||
stderr += chunk;
|
||||
options?.onStderr?.(chunk);
|
||||
try {
|
||||
options?.onStderr?.(chunk);
|
||||
} catch (error) {
|
||||
callbackError = new ExecutionError(
|
||||
"unknown",
|
||||
error instanceof Error ? error.message : String(error),
|
||||
error,
|
||||
);
|
||||
onAbort();
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", (error) => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
if (options?.signal) options.signal.removeEventListener("abort", onAbort);
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
reject(error);
|
||||
settle(err(new ExecutionError("spawn_error", error.message, error)));
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
if (options?.signal) options.signal.removeEventListener("abort", onAbort);
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (options?.signal?.aborted) {
|
||||
reject(new Error("aborted"));
|
||||
if (callbackError) {
|
||||
settle(err(callbackError));
|
||||
return;
|
||||
}
|
||||
if (options?.abortSignal?.aborted) {
|
||||
settle(err(new ExecutionError("aborted", "aborted")));
|
||||
return;
|
||||
}
|
||||
if (timedOut) {
|
||||
reject(new Error(`timeout:${options?.timeout}`));
|
||||
settle(err(new ExecutionError("timeout", `timeout:${options?.timeout}`)));
|
||||
return;
|
||||
}
|
||||
resolvePromise({ stdout, stderr, exitCode: code ?? 0 });
|
||||
settle(ok({ stdout, stderr, exitCode: code ?? 0 }));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async readTextFile(path: string): Promise<string> {
|
||||
async readTextFile(path: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>> {
|
||||
const resolved = resolvePath(this.cwd, path);
|
||||
const aborted = abortResult<string>(abortSignal, resolved);
|
||||
if (aborted) return aborted;
|
||||
try {
|
||||
return await readFile(resolved, "utf8");
|
||||
return ok(await readFile(resolved, { encoding: "utf8", signal: abortSignal }));
|
||||
} catch (error) {
|
||||
throw toFileError(error, resolved);
|
||||
return err(toFileError(error, resolved));
|
||||
}
|
||||
}
|
||||
|
||||
async readBinaryFile(path: string): Promise<Uint8Array> {
|
||||
async readBinaryFile(path: string, abortSignal?: AbortSignal): Promise<Result<Uint8Array, FileError>> {
|
||||
const resolved = resolvePath(this.cwd, path);
|
||||
const aborted = abortResult<Uint8Array>(abortSignal, resolved);
|
||||
if (aborted) return aborted;
|
||||
try {
|
||||
return await readFile(resolved);
|
||||
return ok(await readFile(resolved, { signal: abortSignal }));
|
||||
} catch (error) {
|
||||
throw toFileError(error, resolved);
|
||||
return err(toFileError(error, resolved));
|
||||
}
|
||||
}
|
||||
|
||||
async writeFile(path: string, content: string | Uint8Array): Promise<void> {
|
||||
async writeFile(
|
||||
path: string,
|
||||
content: string | Uint8Array,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Result<void, FileError>> {
|
||||
const resolved = resolvePath(this.cwd, path);
|
||||
const aborted = abortResult<void>(abortSignal, resolved);
|
||||
if (aborted) return aborted;
|
||||
try {
|
||||
await mkdir(resolve(resolved, ".."), { recursive: true });
|
||||
await writeFile(resolved, content);
|
||||
const afterMkdirAbort = abortResult<void>(abortSignal, resolved);
|
||||
if (afterMkdirAbort) return afterMkdirAbort;
|
||||
await writeFile(resolved, content, { signal: abortSignal });
|
||||
return ok(undefined);
|
||||
} catch (error) {
|
||||
throw toFileError(error, resolved);
|
||||
return err(toFileError(error, resolved));
|
||||
}
|
||||
}
|
||||
|
||||
async fileInfo(path: string): Promise<FileInfo> {
|
||||
async appendFile(
|
||||
path: string,
|
||||
content: string | Uint8Array,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<Result<void, FileError>> {
|
||||
const resolved = resolvePath(this.cwd, path);
|
||||
const aborted = abortResult<void>(abortSignal, resolved);
|
||||
if (aborted) return aborted;
|
||||
try {
|
||||
await mkdir(resolve(resolved, ".."), { recursive: true });
|
||||
const afterMkdirAbort = abortResult<void>(abortSignal, resolved);
|
||||
if (afterMkdirAbort) return afterMkdirAbort;
|
||||
await appendFile(resolved, content);
|
||||
const afterAppendAbort = abortResult<void>(abortSignal, resolved);
|
||||
if (afterAppendAbort) return afterAppendAbort;
|
||||
return ok(undefined);
|
||||
} catch (error) {
|
||||
return err(toFileError(error, resolved));
|
||||
}
|
||||
}
|
||||
|
||||
async fileInfo(path: string, abortSignal?: AbortSignal): Promise<Result<FileInfo, FileError>> {
|
||||
const resolved = resolvePath(this.cwd, path);
|
||||
const aborted = abortResult<FileInfo>(abortSignal, resolved);
|
||||
if (aborted) return aborted;
|
||||
try {
|
||||
return fileInfoFromStats(resolved, await lstat(resolved));
|
||||
} catch (error) {
|
||||
throw toFileError(error, resolved);
|
||||
return err(toFileError(error, resolved));
|
||||
}
|
||||
}
|
||||
|
||||
async listDir(path: string): Promise<FileInfo[]> {
|
||||
async listDir(path: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>> {
|
||||
const resolved = resolvePath(this.cwd, path);
|
||||
const aborted = abortResult<FileInfo[]>(abortSignal, resolved);
|
||||
if (aborted) return aborted;
|
||||
try {
|
||||
const entries = await readdir(resolved, { withFileTypes: true });
|
||||
const infos: FileInfo[] = [];
|
||||
for (const entry of entries) {
|
||||
const loopAbort = abortResult<FileInfo[]>(abortSignal, resolved);
|
||||
if (loopAbort) return loopAbort;
|
||||
const entryPath = resolve(resolved, entry.name);
|
||||
try {
|
||||
infos.push(fileInfoFromStats(entryPath, await lstat(entryPath)));
|
||||
const info = fileInfoFromStats(entryPath, await lstat(entryPath));
|
||||
if (info.ok) infos.push(info.value);
|
||||
} catch (error) {
|
||||
if (error instanceof FileError && error.code === "invalid") continue;
|
||||
throw error;
|
||||
return err(toFileError(error, entryPath));
|
||||
}
|
||||
}
|
||||
return infos;
|
||||
return ok(infos);
|
||||
} catch (error) {
|
||||
throw toFileError(error, resolved);
|
||||
return err(toFileError(error, resolved));
|
||||
}
|
||||
}
|
||||
|
||||
async realPath(path: string): Promise<string> {
|
||||
async realPath(path: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>> {
|
||||
const resolved = resolvePath(this.cwd, path);
|
||||
const aborted = abortResult<string>(abortSignal, resolved);
|
||||
if (aborted) return aborted;
|
||||
try {
|
||||
return await realpath(resolved);
|
||||
return ok(await realpath(resolved));
|
||||
} catch (error) {
|
||||
throw toFileError(error, resolved);
|
||||
return err(toFileError(error, resolved));
|
||||
}
|
||||
}
|
||||
|
||||
async exists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await this.fileInfo(path);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error instanceof FileError && error.code === "not_found") return false;
|
||||
throw error;
|
||||
}
|
||||
async exists(path: string, abortSignal?: AbortSignal): Promise<Result<boolean, FileError>> {
|
||||
const result = await this.fileInfo(path, abortSignal);
|
||||
if (result.ok) return ok(true);
|
||||
if (result.error.code === "not_found") return ok(false);
|
||||
return err(result.error);
|
||||
}
|
||||
|
||||
async createDir(path: string, options?: { recursive?: boolean }): Promise<void> {
|
||||
await mkdir(resolvePath(this.cwd, path), { recursive: options?.recursive });
|
||||
}
|
||||
|
||||
async remove(path: string, options?: { recursive?: boolean; force?: boolean }): Promise<void> {
|
||||
async createDir(
|
||||
path: string,
|
||||
options?: { recursive?: boolean; abortSignal?: AbortSignal },
|
||||
): Promise<Result<void, FileError>> {
|
||||
const resolved = resolvePath(this.cwd, path);
|
||||
const aborted = abortResult<void>(options?.abortSignal, resolved);
|
||||
if (aborted) return aborted;
|
||||
try {
|
||||
await mkdir(resolved, { recursive: options?.recursive ?? true });
|
||||
const afterMkdirAbort = abortResult<void>(options?.abortSignal, resolved);
|
||||
if (afterMkdirAbort) return afterMkdirAbort;
|
||||
return ok(undefined);
|
||||
} catch (error) {
|
||||
return err(toFileError(error, resolved));
|
||||
}
|
||||
}
|
||||
|
||||
async remove(
|
||||
path: string,
|
||||
options?: { recursive?: boolean; force?: boolean; abortSignal?: AbortSignal },
|
||||
): Promise<Result<void, FileError>> {
|
||||
const resolved = resolvePath(this.cwd, path);
|
||||
const aborted = abortResult<void>(options?.abortSignal, resolved);
|
||||
if (aborted) return aborted;
|
||||
try {
|
||||
await rm(resolved, { recursive: options?.recursive ?? false, force: options?.force ?? false });
|
||||
const afterRemoveAbort = abortResult<void>(options?.abortSignal, resolved);
|
||||
if (afterRemoveAbort) return afterRemoveAbort;
|
||||
return ok(undefined);
|
||||
} catch (error) {
|
||||
throw toFileError(error, resolved);
|
||||
return err(toFileError(error, resolved));
|
||||
}
|
||||
}
|
||||
|
||||
async createTempDir(prefix: string = "tmp-"): Promise<string> {
|
||||
return await mkdtemp(join(tmpdir(), prefix));
|
||||
async createTempDir(prefix: string = "tmp-", abortSignal?: AbortSignal): Promise<Result<string, FileError>> {
|
||||
const aborted = abortResult<string>(abortSignal);
|
||||
if (aborted) return aborted;
|
||||
try {
|
||||
const path = await mkdtemp(join(tmpdir(), prefix));
|
||||
const afterMkdtempAbort = abortResult<string>(abortSignal, path);
|
||||
if (afterMkdtempAbort) return afterMkdtempAbort;
|
||||
return ok(path);
|
||||
} catch (error) {
|
||||
return err(toFileError(error));
|
||||
}
|
||||
}
|
||||
|
||||
async createTempFile(options?: { prefix?: string; suffix?: string }): Promise<string> {
|
||||
const dir = await this.createTempDir("tmp-");
|
||||
const filePath = join(dir, `${options?.prefix ?? ""}${randomUUID()}${options?.suffix ?? ""}`);
|
||||
await writeFile(filePath, "");
|
||||
return filePath;
|
||||
async createTempFile(options?: {
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
abortSignal?: AbortSignal;
|
||||
}): Promise<Result<string, FileError>> {
|
||||
const dir = await this.createTempDir("tmp-", options?.abortSignal);
|
||||
if (!dir.ok) return dir;
|
||||
const filePath = join(dir.value, `${options?.prefix ?? ""}${randomUUID()}${options?.suffix ?? ""}`);
|
||||
const aborted = abortResult<string>(options?.abortSignal, filePath);
|
||||
if (aborted) return aborted;
|
||||
try {
|
||||
await writeFile(filePath, "", { signal: options?.abortSignal });
|
||||
return ok(filePath);
|
||||
} catch (error) {
|
||||
return err(toFileError(error, filePath));
|
||||
}
|
||||
}
|
||||
|
||||
async cleanup(): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user