Yieldless
Reference

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?)
  • CommandError
  • CommandTimeoutError
  • CommandOutputLimitError

CommandOptions accepts:

  • cwd, env, input, signal, and windowsHide
  • timeoutMs for command deadlines
  • maxOutputBytes to stop noisy commands before they consume too much memory
  • onStdout(chunk) and onStderr(chunk) for live output
  • killSignal when 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 output
  • runCommandSafe() 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.

On this page