Skip to main content

Terminal

The Terminal class is the framework's abstraction over Node.js child_process. It provides cross-platform process execution with environment normalization, proxy forwarding, progress reporting, and proper error handling.

Package: io-oidis-localhost Namespace: Io.Oidis.Localhost.Connectors.Terminal Availability: Backend only (Node.js)

Never use child_process directly

All child process operations must go through Terminal. Direct usage of child_process.exec(), child_process.spawn(), child_process.spawnSync(), or any other child_process method is a convention violation. Terminal handles platform differences, environment variables, proxy settings, and logging automatically.

API Overview​

MethodDescription
Execute()Run a command and wait for exit. Returns PID. Result via callback.
ExecuteAsync()Promise-wrapped Execute(). Returns IExecuteResult.
Spawn()Run a command with streaming stdout/stderr via OnMessage events.
SpawnAsync()Promise-wrapped Spawn(). Returns IExecuteResult.
Elevate()Run with admin/elevated privileges (PowerShell on Windows, sudo on Unix).
ElevateAsync()Promise-wrapped Elevate(). Returns IExecuteResult.
Open()Open a file or URL with the OS default handler.
OpenAsync()Promise-wrapped Open().
Kill()Kill processes by path or name. Platform-specific (wmic on Windows, pgrep//proc on Linux).
KillAsync()Promise-wrapped Kill().
NormalizeEnvironmentPath()Normalizes the PATH environment variable for cross-platform compatibility.

IExecuteResult​

Both ExecuteAsync() and SpawnAsync() return:

interface IExecuteResult {
exitCode : number;
std : string[]; // [stdout, stderr]
}

ITerminalOptions​

All process methods accept ITerminalOptions for fine-grained control:

PropertyTypeDescription
cwdstringWorking directory (defaults to process.cwd())
envRecord<string, string>Environment variables (proxy vars auto-forwarded)
shellbooleanRun in shell (default: true)
detachedbooleanRun detached (only supported by Spawn)
verbosebooleanLog command and output (default: true)
windowsHidebooleanHide console window on Windows (default: true)
advanced.coloredbooleanPreserve ANSI color codes in output
advanced.noExitOnStderrbooleanDon't treat stderr output as failure
advanced.noTerminalLogbooleanSuppress terminal logging
advanced.useStdInbooleanEnable stdin passthrough (Spawn only)

When to Use Which Method​

ScenarioMethod
Run a command and get exit code + outputExecuteAsync()
Long-running process with live output (build, dev server)SpawnAsync() or Spawn() with callback
Install software or run admin commandsElevateAsync()
Open a URL in browser or file in default appOpenAsync()
Kill a running process by nameKillAsync()

Cross-Platform Handling​

Terminal automatically handles:

  • PATH normalization — adjusts PATH separators and entries per platform
  • Proxy forwarding — HTTP_PROXY, HTTPS_PROXY, NO_PROXY (both cases) are propagated to child processes
  • Detached process tracking — sets IS_OIDIS_DETACHED_PROCESS environment variable
  • WUI app detection — auto-appends --disable-console for Oidis WUI executables
  • Command normalization — path separators adjusted via path.normalize()

Examples​

Good: Using Terminal​

// Async execution — wait for result
const terminal : Terminal = Loader.getInstance().getTerminal();
const result : IExecuteResult = await terminal.ExecuteAsync("git", ["status"], {
cwd : projectPath,
verbose: false
});

if (result.exitCode !== 0) {
LogIt.Error(`Git status failed: ${result.std[1]}`);
}
// Spawn with streaming output
terminal.Spawn("npm", ["run", "build"], {
cwd : projectPath,
verbose: true
});

Bad: Direct child_process​

// ❌ WRONG — direct child_process usage
import * as childProcess from "child_process";

const result = childProcess.spawnSync("git", ["status"], {
cwd : projectPath,
encoding: "utf-8"
});

Exception: Direct child_process for stdin Piping​

Terminal's useStdIn option sets stdio to ["inherit", "pipe", "pipe"] — it inherits the parent process's stdin stream. This is designed for interactive CLI tools where the user types input directly (e.g., password prompts, confirmation dialogs).

However, some use cases require programmatic stdin piping — writing data to the child process's stdin stream and then closing it. Terminal does not support this because:

  1. "inherit" vs "pipe" — Terminal uses "inherit" for stdin, which connects the child directly to the parent's stdin. There is no writable stream to write to programmatically.
  2. No write API — Terminal exposes child.stdin only through the StdinType marker mechanism (for interactive prompts), not for arbitrary data writing.
  3. No close API — Some tools (e.g., CLI agents) require stdin to be closed after writing to signal end-of-input. Terminal has no mechanism for this.

In these cases, direct child_process.spawn() with stdio: ["pipe", "pipe", "pipe"] is acceptable.

Example: ClaudeAgentRunner​

ClaudeAgentRunner in io-oidis-pm spawns the Claude CLI (claude -p) and pipes a prompt via stdin:

const proc : ChildProcess = spawn("claude", args, {
cwd : options.cwd,
stdio: ["pipe", "pipe", "pipe"],
shell: true
});

// Write prompt to stdin and close it to signal end-of-input
proc.stdin!.write(prompt);
proc.stdin!.end();

// Collect streamed JSON output from stdout
proc.stdout!.on("data", (chunk : Buffer) : void => {
output += chunk.toString();
});

This pattern is valid because Terminal cannot pipe data to stdin — it can only inherit the parent's stdin for interactive use. The trade-off is acceptable: you lose Terminal's automatic environment normalization and proxy forwarding, but gain the ability to programmatically control the child's stdin stream.

When is direct child_process acceptable?

Use direct child_process only when you need to write data to stdin programmatically and close it. For all other scenarios — commands, builds, dev servers, interactive prompts — always use Terminal.

Exception: Direct child_process for Large Streamed Output​

Both entry points hold the child's entire output in memory before handing it over: Execute() is built on child_process.exec (with its maxBuffer), and Spawn() appends every stdout/stderr chunk to a string that is only delivered in the completion callback. That is right for commands whose output is a few lines, and wrong for a tool whose output is the data — unzip -p inflating a 100 MB archive member, tar -xOf, a converter writing a table to stdout. Piping such output through Terminal means the whole member sits in the heap before the first byte reaches the parser.

For those, use child_process.spawn directly with stdio: ["ignore", "pipe", "pipe"] and consume child.stdout as a stream, with three guards Terminal would otherwise give you for free:

  1. Arguments as an array, no shell — the member names come from the archive, not from you.
  2. End the consumer only on a clean exit — pipe child.stdout into a PassThrough with {end: false} and call end() from the child's close event when the exit code is acceptable, destroy(error) otherwise. A child that dies halfway must not look like a short file.
  3. Kill the child when the consumer stops early — listen for close on the pass-through and child.kill() if the process is still running, or a capped read leaves the tool blocked on a full pipe.

Io.Oidis.Labs.Utils.ArchiveStreams (io-oidis-labs) is the reference implementation: it prefers the system unzip when EnvironmentHelper.IsLinux()/IsMac() and a one-time unzip -v probe succeed, and falls back to an in-process zip.js stream otherwise, behind one NodeJS.ReadableStream interface.

API Reference​

Terminal extends BaseConnector and is accessible via Loader.getInstance().getTerminal().

ITerminalOptions​

interface ITerminalOptions {
cwd? : string; // Working directory. Defaults to process.cwd().
env? : object; // Environment variables. Inherits proxy vars from host when set.
verbose? : boolean; // Print command and output to log. Default: true.
shell? : boolean; // Run through shell. Default: true.
detached?: boolean; // Run as detached process (Spawn only). Default: false.
advanced?: {
colored? : boolean; // Preserve ANSI color codes in stdout. Default: false.
noTerminalLog? : boolean; // Suppress all terminal log output. Default: false.
noExitOnStderr?: boolean; // Exit early if stderr contains "fatal:" or "error:". Default: false.
useStdIn? : boolean; // Inherit stdin from parent (Spawn only). Default: false.
};
}

Environment variable notes:

  • When env is set, HTTP_PROXY, HTTPS_PROXY, NO_PROXY and their lowercase variants are automatically copied from the host process.
  • When running in detached mode, IS_OIDIS_DETACHED_PROCESS=1 is injected automatically.
  • When env is not set, process.env is used in full.

Execute()​

Runs a command using child_process.exec (buffered stdout/stderr). Suitable for short-lived commands where output is collected on exit.

public Execute(
$cmd : string,
$args : string[],
$cwdOrOptions : string | ITerminalOptions,
$callback? : (($exitCode: number, $std?: string[]) => void) | IResponse
) : number
ParameterTypeDescription
$cmdstringCommand to execute.
$argsstring[]Argument list. Pass [] for no arguments.
$cwdOrOptionsstring | ITerminalOptionsWorking directory string or full options object.
$callback(exitCode, std?) => void or IResponseOptional callback. std[0] = stdout, std[1] = stderr.

Returns the child process PID, or -1 on error.

Exit codes:

  • 0 — success
  • Non-zero — command-specific error
  • -1 — process spawn failure or unhandled exception

Example — synchronous style via async wrapper:

const result : IExecuteResult = await Loader.getInstance().getTerminal().ExecuteAsync(
"git",
["status", "--short"],
{ cwd: "/my/project", verbose: false }
);
LogIt.Info("exit: " + result.exitCode + ", out: " + result.std[0]);

Example — callback style with environment variables:

Loader.getInstance().getTerminal().Execute(
"node",
["--version"],
{
cwd : process.cwd(),
env : { MY_VAR: "hello" },
verbose: true
},
($exitCode : number, $std : string[]) : void => {
LogIt.Info("Node version: " + $std[0]);
}
);

ExecuteAsync()​

Promise-based wrapper around Execute().

public async ExecuteAsync(
$cmd : string,
$args : string[],
$cwdOrOptions : string | ITerminalOptions
) : Promise<IExecuteResult>

Returns IExecuteResult:

interface IExecuteResult {
exitCode : number;
std : string[]; // [0] = stdout, [1] = stderr
}

Spawn()​

Runs a command using child_process.spawn (streaming stdout/stderr). Suitable for long-running processes, interactive commands, and real-time output streaming.

public Spawn(
$cmd : string,
$args : string[],
$cwdOrOptions : string | ITerminalOptions,
$callback? : (($exitCode: number, $std?: string[]) => void) | IResponse
) : number

Spawn supports IResponse.OnChange() for streaming output chunks. When the process writes a StdinType.STRING or StdinType.PASS token to stdout, the framework forwards stdin to the child process via response.OnChange($data, child.stdin).

When detached: true is set in options, stdio is configured as ["pipe", "pipe", "pipe", "ipc"] so IPC messages are also forwarded via OnChange.


SpawnAsync()​

public async SpawnAsync(
$cmd : string,
$args : string[],
$cwdOrOptions : string | ITerminalOptions
) : Promise<IExecuteResult>

Open()​

Opens a file or URL using the OS default application (xdg-open, open, or Windows shell association).

public Open($path : string, $callback? : (($exitCode: number) => void) | IResponse) : number
public async OpenAsync($path : string) : Promise<number>

Elevate()​

Runs a command with elevated privileges. Uses powershell Start-Process -Verb RunAs on Windows and sudo on Unix.

public Elevate(
$cmd : string,
$args : string[],
$cwd : string,
$callback?: (($exitCode: number, $std?: string[]) => void) | IResponse
) : number

public async ElevateAsync(
$cmd : string,
$args : string[],
$cwd : string
) : Promise<IExecuteResult>

Kill()​

Terminates a running process by binary name or full path.

public Kill($pathOrName : string, $callback? : (($status: boolean) => void) | IResponse) : void
public async KillAsync($pathOrName : string) : Promise<void>
  • If $pathOrName is a bare name (e.g., "node"), all processes with that name are terminated.
  • If $pathOrName is a full path, only the process matching that exact executable path is terminated.
  • On Windows uses wmic; on Linux uses /proc/<pid>/exe; not implemented on macOS.

NormalizeEnvironmentPath()​

Prepends custom path entries to the system PATH, deduplicating entries and preserving the host path.

public NormalizeEnvironmentPath($input : string) : string

Typically used inside ITerminalOptions.env.PATH to add tool directories ahead of system defaults.


Abort Handling​

Every Execute and Spawn call registers an abort handler. If the IResponse is aborted (e.g., via LiveContent abort), the underlying child process receives SIGINT.

// Internally registered automatically:
response.AddAbortHandler(() : void => {
child.kill("SIGINT");
});

See also: Connectors Reference | Utilities Overview | Vanilla vs Framework