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)
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
| Method | Description |
|---|---|
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:
| Property | Type | Description |
|---|---|---|
cwd | string | Working directory (defaults to process.cwd()) |
env | Record<string, string> | Environment variables (proxy vars auto-forwarded) |
shell | boolean | Run in shell (default: true) |
detached | boolean | Run detached (only supported by Spawn) |
verbose | boolean | Log command and output (default: true) |
windowsHide | boolean | Hide console window on Windows (default: true) |
advanced.colored | boolean | Preserve ANSI color codes in output |
advanced.noExitOnStderr | boolean | Don't treat stderr output as failure |
advanced.noTerminalLog | boolean | Suppress terminal logging |
advanced.useStdIn | boolean | Enable stdin passthrough (Spawn only) |
When to Use Which Method
| Scenario | Method |
|---|---|
| Run a command and get exit code + output | ExecuteAsync() |
| Long-running process with live output (build, dev server) | SpawnAsync() or Spawn() with callback |
| Install software or run admin commands | ElevateAsync() |
| Open a URL in browser or file in default app | OpenAsync() |
| Kill a running process by name | KillAsync() |
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_PROCESSenvironment variable - WUI app detection — auto-appends
--disable-consolefor 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:
"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.- No write API — Terminal exposes
child.stdinonly through theStdinTypemarker mechanism (for interactive prompts), not for arbitrary data writing. - 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.
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:
- Arguments as an array, no shell — the member names come from the archive, not from you.
- End the consumer only on a clean exit — pipe
child.stdoutinto aPassThroughwith{end: false}and callend()from the child'scloseevent when the exit code is acceptable,destroy(error)otherwise. A child that dies halfway must not look like a short file. - Kill the child when the consumer stops early — listen for
closeon the pass-through andchild.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
envis set,HTTP_PROXY,HTTPS_PROXY,NO_PROXYand their lowercase variants are automatically copied from the host process. - When running in detached mode,
IS_OIDIS_DETACHED_PROCESS=1is injected automatically. - When
envis not set,process.envis 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
| Parameter | Type | Description |
|---|---|---|
$cmd | string | Command to execute. |
$args | string[] | Argument list. Pass [] for no arguments. |
$cwdOrOptions | string | ITerminalOptions | Working directory string or full options object. |
$callback | (exitCode, std?) => void or IResponse | Optional 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
$pathOrNameis a bare name (e.g.,"node"), all processes with that name are terminated. - If
$pathOrNameis 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