yieldless/node
Tuple wrappers for Node filesystem work and child-process execution.
yieldless/node wraps the pieces of Node that backend tools and desktop apps touch constantly: filesystem calls and external commands.
Exports
Filesystem
accessSafe(path)readFileSafe(path, encoding?)writeFileSafe(path, contents, options?)readdirSafe(path)mkdirSafe(path, options?)rmSafe(path, options?)statSafe(path)
Processes
runCommand(file, args?, options?)runCommandSafe(file, args?, options?)runShellCommand(command, options?)runShellCommandSafe(command, options?)CommandErrorCommandTimeoutErrorCommandOutputLimitError
CommandOptions accepts:
cwd,env,input,signal, andwindowsHidetimeoutMsfor command deadlinesmaxOutputBytesto stop noisy commands before they consume too much memoryonStdout(chunk)andonStderr(chunk)for live outputkillSignalwhen aborted commands need a signal other than Node's default
Filesystem example
import { readFileSafe } from "yieldless/node";
const [error, contents] = await readFileSafe(".git/HEAD");Child-process example
import { runCommandSafe } from "yieldless/node";
const [error, result] = await runCommandSafe(
"pnpm",
["test"],
{
cwd: workspacePath,
timeoutMs: 60_000,
maxOutputBytes: 1024 * 1024,
signal,
},
);Successful results include { command, args, cwd, durationMs, stdout, stderr, exitCode, signal }, which makes logging and diagnostics easier without parsing thrown errors.
runCommand() vs runCommandSafe()
runCommand()throws on non-zero exit and returns command metadata plus captured outputrunCommandSafe()wraps that behavior into a tuple
CommandError includes the command, args, duration, output, exit code, signal, and Node error code when one exists.
Exec-style shell command strings
Prefer runCommandSafe(file, args) when you can. It keeps argument boundaries explicit.
Use runShellCommandSafe() only when shell syntax is the point: pipes, redirects, environment expansion, or command strings provided by a trusted developer tool.
import { runShellCommandSafe } from "yieldless/node";
const [error, result] = await runShellCommandSafe(
"pnpm test -- --runInBand",
{
cwd: workspacePath,
timeoutMs: 60_000,
onStdout: (chunk) => {
process.stdout.write(chunk);
},
},
);Do not pass user input into a shell command string. If the command contains user-provided values, use runCommandSafe(file, args) instead.
Live output
Commands still capture output by default, but onStdout and onStderr let you stream progress to logs, terminals, or a UI while the command runs.
const [error, result] = await runCommandSafe("npm", ["run", "build"], {
cwd: workspacePath,
timeoutMs: 120_000,
onStdout: (chunk) => appendBuildLog(chunk),
onStderr: (chunk) => appendBuildLog(chunk),
});
if (error) {
appendBuildLog(error.stderr);
}Output limits
Use maxOutputBytes when a command can print unbounded logs. If the combined output exceeds the limit, the command is aborted and returns CommandOutputLimitError with partial captured output.
import { CommandOutputLimitError, runCommandSafe } from "yieldless/node";
const [error, result] = await runCommandSafe("npm", ["test"], {
maxOutputBytes: 512 * 1024,
timeoutMs: 60_000,
});
if (error instanceof CommandOutputLimitError) {
logger.warn({ stdout: error.stdout }, "test output exceeded the capture limit");
}Cancellation
If you pass an AbortSignal, Yieldless forwards it to Node's native child-process signal handling and waits for the subprocess to close before it settles the wrapper promise.
Good
Use filesystem tuple helpers at the file boundary.
const [readError, contents] = await readFileSafe(configPath);
if (readError) return [readError, null] as const;Use runCommandSafe() when non-zero exit status is a normal operational failure.
const [error, result] = await runCommandSafe("node", ["--check", filePath], {
cwd: workspacePath,
signal,
});
if (error instanceof CommandError) {
logger.warn({ durationMs: error.durationMs, stderr: error.stderr }, "syntax check failed");
}Avoid
Do not shell-concatenate user input.
await runCommandSafe("sh", ["-c", `git -C ${repoPath} status`]);Pass the executable and args separately so Node handles argument boundaries.