Process & CLI
What a CLI tool or containerized service needs day-to-day: argv, environment, exit codes, stdin, signals, timing, and child processes. The recurring divergence: every event/handler API here takes one listener slot per event, an arrow/function-literal only, firing synchronously from the event loop's dispatch rather than through a microtask queue. child_process (spawn/exec/fork/spawnSync) lives here too — it is what a process orchestrator actually reaches for.
process.argv
ExactThe command-line arguments, in Node's shape: argv[0] and argv[1] are the executable's absolute path (a compiled program is its own script) and the arguments follow, so process.argv.slice(2) is what the user typed. An out-of-range read is undefined.
Returnsstring[] — The argument vector.
const args: string[] = process.argv
for (const a of args) console.log(a)process.env
ExactEnvironment variables, via both dot and bracket notation. A missing variable is undefined (typeof included), so ??, === undefined and narrowing behave as in Node.
Returnsstring | undefined — The value, or undefined if unset.
const path: string = process.env.PATH ?? "no PATH set"process.exit(code)
ExactExits the process: the code (validated like exitCode, or process.exitCode when omitted) becomes the exit code, 'exit' fires once, then C exit(); code after it is unreachable.
Returnsnever — Does not return.
fs.readFileSync(0)
ExactReads all of stdin (file descriptor 0) to its end, blocking — the Node idiom for a CLI tool that takes piped input. A line-at-a-time read is the readline module.
Returnsstring — Everything on stdin.
import { readFileSync } from 'fs'
const input = readFileSync(0, 'utf8')
console.log(input.split('\n')[0])Synchronous file read/write
ExactConfig, data, and log I/O for a CLI program — the full sync/async/stream fs surface has its own reference page.
process.cwd()
ExactThe current working directory, via POSIX getcwd(NULL, 0) (auto-sizing).
Returnsstring — The current directory.
const startDir: string = process.cwd()process.chdir(path)
ExactChanges the current working directory via POSIX chdir(). A failure throws Node's error: ENOENT: no such file or directory, chdir '<cwd>' -> '<dir>', with errno, code, syscall, path and dest.
Returnsvoid — Nothing.
process.chdir("/tmp")process.pid
ExactThe current process ID, via POSIX getpid(). A property read, not a call.
Returnsnumber — The process ID.
process.platform
ExactA pure compile-time constant ("darwin"/"linux"/"win32"/…) baked in at codegen — no runtime code. Under the --target cross-compile flag it follows the target: codegen reads the target OS/arch through a target abstraction, so an OS-aligned (e.g. Linux→Linux) or macOS→Linux cross build bakes the target's platform, not the build host's.
Returnsstring — The OS platform.
console.log(process.platform, process.arch)process.arch
ExactA compile-time constant ("arm64"/"x64"/…) from the Go compiler's runtime.GOARCH, exactly like process.platform.
Returnsstring — The CPU architecture.
process.execPath
ExactThe absolute, symlink-resolved path of the running executable — the compiled program is itself the binary (no separate runtime). Always absolute like Node's, not a raw argv[0].
Returnsstring — The executable's absolute path.
process.emitWarning(warning, type?)
Works, with caveatsEmits the process 'warning' event on the next tick with an Error whose name is the type ("Warning" by default), with code and detail when given; the default listener prints (node:<pid>) [<code>] <name>: <message> and a <detail> line to stderr, as Node's does.
Returnsvoid — Nothing.
process.emitWarning("legacy call site", { type: "DeprecationWarning", code: "DEP001" })- The one-shot
(Usenode --trace-warnings ...)hint Node prints is omitted: a compiled program has no--trace-warningsflag.
process.kill(pid, signal?)
ExactSends signal (SIGTERM by default) to pid via POSIX kill(). The signal may be a name ('SIGTERM', 'SIGINT', …) or a number; an unknown name throws ERR_UNKNOWN_SIGNAL. A failure throws Node's 'kill <code>' Error with code, errno and syscall; signal 0 is the standard existence check and delivers nothing. On Windows SIGTERM/SIGKILL/SIGINT/SIGQUIT terminate the target unconditionally (exit code 1), as in Node, and any other signal throws ENOSYS.
Returnstrue — true, when the signal was sent.
process.stdout.write / stderr.write
Works, with caveatsprocess.stdout and process.stderr are Node's streams, made on first use by what the descriptor is: a tty.WriteStream on a terminal, a net.Socket on a pipe or socket, a synchronous Writable on a file. write takes a string or a Buffer, an encoding and a callback, and appends nothing — unlike console.log/.error's trailing newline. Writes to a terminal, a file or a POSIX pipe are synchronous, as in Node, so output stays ordered with console.log; pipe() into them works.
Returnsvoid — Nothing.
process.stdout.write(chunk.toUpperCase())- Text-only (byte-string, not binary-safe past an embedded null — the same scope narrowing
console.loghas). - Beyond
.write,.isTTY(a realisatty(fd)probe), and.columns/.rows(a liveioctl(TIOCGWINSZ)read),process.stdout/stderrexpose no other stream surface (no'drain').
process.stdout.columns / .rows
ExactThe terminal's width and height, on a tty.WriteStream: read at creation and refreshed on SIGWINCH, which emits 'resize' when the size changed. Off a TTY (piped/redirected) the stream has no columns/rows: a real undefined, as in Node; ?? 80 is the idiomatic fallback.
Returnsnumber | undefined — Columns or rows, or undefined when the fd is not a terminal.
console.log(process.stdout.columns + "x" + process.stdout.rows)- Stored into a declared
numberbinding off a TTY, the value reads 0, notundefined.
process.stdin.setRawMode / klain:tty
ExactA tty.ReadStream method that puts the terminal into raw mode (libuv's UV_TTY_MODE_RAW): canonical mode, echo and signal generation are off, so keystrokes arrive one at a time and Ctrl-C comes through as the byte 3; isRaw reports the mode, and the original mode is restored at exit. When fd 0 is not a terminal, process.stdin is a net.Socket without setRawMode, as in Node — guard on process.stdin.isTTY.
Returnsvoid — Nothing.
import { readKey } from 'klain:tty'
process.stdin.setRawMode(true)
const k = readKey()- A no-op when fd 0 is not a terminal (
tcgetattrfails);setRawMode(false)restores the state captured on the first enable. - Auto-restore on a crash is the application's responsibility (same footgun as Node).
readByte()/readKey()are a bespoke, non-Nodeklain:surface — Node has no synchronous single-key read. Don't mix them withprocess.stdin.on('data')in the same run (that puts fd 0 in non-blocking mode).
klain:tty readByte / readKey
Works, with caveatsSynchronous single-keystroke reads off fd 0, for a blocking raw-input loop. readByte() returns one byte (or -1 at EOF); readKey() returns one read()'s whole burst as a string, so an escape sequence like an arrow key (ESC [ A) is a single key. A bespoke klain: primitive because Node has no synchronous single-key read — it uses process.stdin.on('data') events.
Returnsnumber | string — The byte (-1 at EOF) or the keystroke string.
import { readKey } from 'klain:tty'
const k = readKey()- Non-Node surface under the explicit
klain:specifier. - Blocking reads on fd 0; don't mix with
process.stdin.on('data')in the same run (which puts fd 0 in non-blocking mode, so a synchronous read would see EOF-on-EAGAIN). - Pair with
process.stdin.setRawMode(true)so keystrokes arrive un-echoed, one at a time.
Writing process.env
ExactSets an environment variable to String(value) through setenv, compound assignment included (process.env.X += …); a Symbol throws TypeError. A written variable is inherited by child processes, and delete process.env.KEY unsets it.
Returnsvoid — Nothing.
process.env.GREETING = "hi"process.env as a value
Works, with caveatsThe bare process.env is the live environment block as a dynamic object — a copy of environ on POSIX, GetEnvironmentStringsW on Windows (the hidden =X: per-drive entries skipped, key case kept) — so every enumeration form works and every value is a string.
for (const [k, v] of Object.entries(process.env)) {
if (k.startsWith("MYAPP_")) console.log(k, v)
}
const child = { ...process.env, NODE_ENV: "production" }- The object is a snapshot taken when the bare
process.envexpression is evaluated: keyed reads/writes (process.env.X) stay live, but a write through an alias (const env = process.env; env.X = '1') lands in the copy, not the environment — Node's object is a live proxy.
process.on(signal, listener)
ExactAny signal is an event name — SIGINT/SIGTERM to shut down gracefully, SIGWINCH (terminal resize) to re-read process.stdout.columns/.rows and repaint a TUI, SIGHUP, SIGUSR1/2, … The first listener starts a watcher and removing the last restores the default disposition; SIGKILL/SIGSTOP throw uv_signal_start EINVAL. On Windows, as in Node, SIGINT fires on Ctrl+C, SIGBREAK on Ctrl+Break, and a SIGTERM listener is accepted but never fires.
ReturnsNodeJS.Process — process, for chaining.
process as an EventEmitter ('exit'/'uncaughtException'/'unhandledRejection'/'warning') + exitCode
Works, with caveatsprocess is an EventEmitter: on/once/off/emit/listenerCount and the rest, any event name. 'exit' fires at normal end and from process.exit(), receiving the exit code, and a listener may set process.exitCode; process.exit() with no argument uses it. 'uncaughtException' receives the thrown value and its origin. A rejected promise nothing handles is reported after the microtask checkpoint, as Node's default does: 'unhandledRejection' receives (reason, promise), else it is an uncaught exception.
Returnsvoid — Registers a lifecycle listener.
process.on('exit', (code: number) => console.log("exiting with code", code))
process.exitCode = 0- After a synchronous throw,
'uncaughtException'runs the listener (suppressing the default print) but the process still exits (code 1) — the setjmp/longjmp model has already unwound to the top-level catch, so it can't resume like Node.
process.hrtime() / .bigint()
ExactCLOCK_MONOTONIC high-resolution timing. hrtime() returns a [seconds, nanoseconds] tuple; hrtime(prev) returns the elapsed diff from a prior reading (with Node's borrow), and rejects a non-array or wrong-length argument as Node does; .bigint() returns total-nanoseconds as a bigint.
Returns[number, number] | bigint — The timestamp or elapsed diff.
const start = process.hrtime()
// ...work...
const [s, ns] = process.hrtime(start)process.getuid() / geteuid() / getgid() / getegid()
ExactDirect POSIX credential reads returning a number, matching Node-on-POSIX. Node leaves all four undefined on Windows: there the bare call is rejected at compile time in typed code and throws Node's TypeError when reached in untyped JS, and the portable optional call process.getuid?.() evaluates to undefined — it is typed number | undefined on every host.
Returnsnumber — The uid/gid.
process.memoryUsage()
Works, with caveatsReturns Node's object shape. rss is the process's instantaneous resident set size (Darwin task_info, Linux /proc/self/statm), matching Node's live rss. heapTotal/heapUsed report this compiler's own object heap — Boehm's collected heap under -mm=gc, otherwise the C allocator's arena (Darwin malloc_zone_statistics, glibc mallinfo2) — so they are real and grow with allocation, a native reinterpretation rather than V8's number. external/arrayBuffers have no native off-heap analogue and stay 0.
Returnsobject — Memory-usage byte counts; rss and heapTotal/heapUsed are populated, external/arrayBuffers are 0.
const m = process.memoryUsage(); console.log(m.rss > 0)process.nextTick(fn)
Works, with caveatsEnqueues fn on the existing microtask queue.
Returnsvoid — Nothing.
process.nextTick(() => console.log("ran after synchronous code"))- Shares the one microtask FIFO with Promise reactions rather than draining strictly before them (Node keeps a separate nextTick queue ahead of promises); zero-argument
() => voidcallback only.
process.version / process.versions
Works, with caveatsprocess.version = "v22.11.0"; process.versions is a real object (JSON.stringify/field access/Object.keys all work), matching Node v22.11.0's node/v8 verbatim, plus a klain key for this compiler's version — stamped at link time from the release tag (the same value `klainmain --version` prints).
Returnsstring | object — Version string / versions object.
console.log(process.version, process.versions.klain)versionsreports only honest values —node/v8/klain— deliberately omitting bundled-lib keys this compiler doesn't ship (uv/undici/icu/…) rather than fabricate them.- The real linked-library versions (
openssl/zlib) are a follow-on — they need a runtime lib query. node/v8track the compatibility *ceiling* (the pinned test-corpus release); a--node-compatfloor is deferred.
process.uptime()
ExactSeconds (a double) since process start, captured once at main entry via CLOCK_MONOTONIC.
Returnsnumber — Seconds since start.
process.umask([mask])
ExactThe process's file-mode creation mask. With no argument it reads the mask the way libuv does (set 0, restore); with one (a number, or an octal string such as '022') it sets it and returns the previous value. POSIX umask(2), so a file created afterwards carries the mask; on Windows it is the CRT's _umask — 0 by default and inert for created files, exactly Node's own answer there.
Returnsnumber — The current (or previous) mask, e.g. 0o022.
const prev = process.umask(0o077)
fs.writeFileSync('secret', '…') // mode 600 on POSIX
process.umask(prev)process.stdin (streamed)
Works, with caveatsNode's stdin stream: a tty.ReadStream on a terminal, otherwise a net.Socket over the descriptor (a pipe, a socket or a file). It starts paused; a 'data' listener, resume(), pipe() or for await starts it, and pause() stops reading so the process can exit. 'data' delivers Buffers unless setEncoding is called; read()/'readable' and several listeners per event work as on any Readable.
Returnstypeof process.stdin — The stream, for chaining.
process.stdin.setEncoding("utf8")
.on("data", (chunk: string) => process.stdout.write(chunk.toUpperCase()))
.on("end", () => console.error("done"))'data'delivers each chunk as a UTF-8 string, not aBuffer—setEncoding('utf8')is a faithful no-op but a non-utf8 encoding is rejected (no binary mode).- Flowing mode only (attaching
'data'starts it) — no.pause()/.resume()/.read()/'readable'. - One listener per event, arrow/function-expression literal only; callbacks fire synchronously from the dispatch, not via a microtask.
readline module
Works, with caveatsNode's readline over any Readable — stdin, a file stream, a child's stdout, a socket: 'line' per line (\r\n, \n, \r and the Unicode separators end one), 'close' at the end, question() with an AbortSignal, prompt/setPrompt, pause/resume, and for await (const line of rl). On a terminal it edits the line as Node does: cursor and word movement, kill ring, undo, history and tab completion. readline/promises' question returns a promise.
ReturnsInterface — The readline interface.
- Listener callbacks fire synchronously from the event loop's dispatch, not deferred through a microtask queue.
- One listener per event, an arrow/function-expression literal only; no line history/editing, no
SIGINT/'pause'/'resume', no completer. - The
createInterfaceoptions object is accepted but ignored — input is always stdin, output always stdout; one active interface at a time.
child_process.spawn / exec / execFile (async)
Works, with caveatsfork()+execvp() with three pipes; the read ends are non-blocking and fold into the same select() event loop as Worker/channel pipes. spawn streams (child.stdout/stderr 'data'/'end', child.on('close'/'exit'/'error'), child.stdin.write/.end, child.pid, child.kill); exec runs via /bin/sh -c and execFile execvp's directly, both buffering into one (err, stdout, stderr) callback.
ReturnsChildProcess — The child handle.
- Listener callbacks fire synchronously from the event loop's dispatch, not deferred through the microtask queue.
- One listener per event, an arrow/function-expression literal only; no
removeListener. - spawn's options support
{ cwd, shell, stdio: 'pipe', env }—cwdis real;shell: trueroutes the command through the platform shell (/bin/sh -c/cmd.exe /d /s /c, honouring%ComSpec%) as one string (a shell path, a dynamic value, orshell:true+ an args array are clean rejections);env(an object literal of string values) fully replaces the child's environment, Node's non-merging semantics;windowsHide(literal bool) suppresses the child's console window on Windows (CREATE_NO_WINDOW), a no-op on POSIX;timeout(ms) has the event loop kill the child withkillSignal(a literal signal name/number, default SIGTERM), surfacing as the(null, '<signal>')exit event;detached(literal bool) makes the child a new session/group leader —setsidon POSIX,DETACHED_PROCESSon Windows;child.unref()/ref()drop or restore the child's hold on the event-loop keepalive;stdiotakes'pipe'/'inherit'/'ignore'as a string or a 3-element[stdin, stdout, stderr]array — inherit wires the fd to the parent's, ignore sends it to/dev/null/NUL (fd numbers,'ipc'and streams are clean rejections).exec(cmd[, options], cb)/execFile(file[, args][, options], cb)take the same options plusmaxBuffer(1 MiB default; past it the child is killed and the callback getsRangeError [ERR_CHILD_PROCESS_STDIO_MAXBUFFER]) and ashellpath; a timeout/kill error carrieskilled: trueand thesignalname, and an(err: any)parameter sees a real boxed Error. - The
exec/execFilecallback Error'scodeis null — Node's is the numeric exit code, which the string-typedcodeslot cannot carry;cmdis absent. exec/execSynchand the command to the shell verbatim (embedded quotes survive); on Windows a.bat/.cmdspawned without a shell is refused, surfacing as the spawn'error'event (EINVAL), closing the CVE-2024-27980 injection path.- A spawn that fails to start (a missing command → ENOENT) emits
'error'with anErrorand not'exit'; the Error carries.message,.code('ENOENT'— the canonicale.code === 'ENOENT'guard) and.errno(libuv's errno:-2on POSIX,-4058on Windows)..syscall/.path/.spawnargsand Node'sspawn <file> <CODE>message wording (which need the command string) are still unset. 'close'/'exit'fire as(code, signal)like Node: a signal death givescode = nulland the signal name ('SIGTERM'), a normal exit the integer code andsignal = null. POSIX recovers the signal from the wait status; Windows from the signalchild.killrecorded. A legacy listener with a single non-nullablenumberparam sees0on a signal death (type itnumber | nullfor the faithfulnull).- The exit
codeis the child's real width — the full 32-bit value on Windows (cmd /c exit 300→300), the 8-bit wait-status field on POSIX (300 & 0xff = 44, as Node also reports there). child.killtakes a signal name string only as a literal (resolved to the host's signal number at compile time) or a number; a dynamic signal-name string is a clean rejection.- Handles and their buffered output are never freed (
-mm=manual).
child_process.fork (self-fork + IPC)
Works, with caveatsNode's own mechanism: socketpair + re-exec of the current binary with NODE_CHANNEL_FD set; json serialization, newline-framed. Parent: child.send(msg, cb?), child.on('message'/'exit'/'close'), child.disconnect(); child: process.send(msg, cb?), process.on('message'/'disconnect'), process.disconnect(), process.connected. child.send(msg, server | socket) passes a net.Server or net.Socket to the other process, which gets its own. process.send is undefined in a program that was not forked.
ReturnsChildProcess — The forked child with an IPC channel.
- Self-fork only: the module must be this program's entry (forking a different module is
ERR_CHILD_PROCESS_FORK_MODULE). serialization: 'advanced'is not ported: messages always cross as json.- A
dgramsocket cannot be sent as a handle; on Windows no handle can yet.
child_process.spawnSync / execSync / execFileSync
ExactA native blocking spawn on its own libuv loop, as Node's spawn_sync is (uv_spawn): the piped stdio read to completion (no deadlock when a child fills stderr while stdout drains), an optional input fed to stdin, a timeout/maxBuffer kill, then the exit; execSync runs via /bin/sh -c (%ComSpec% /d /s /c on Windows). Node's full options set: cwd, env (replaces the child's), input (string or Uint8Array), argv0, stdio ('pipe'/'inherit'/'ignore', string or 3-element array), timeout + killSignal (error.code ETIMEDOUT), maxBuffer (1 MiB default, ENOBUFS on overrun), shell (true or a shell path; args join the command with spaces), windowsHide, windowsVerbatimArguments. spawnSync returns Node's record in Node's key order — status is null on a signal death or a never-started child, output is [null, stdout, stderr], error carries code/errno/syscall 'spawnSync <file>'/path for a failed spawn (ENOENT: status null, pid 0, null streams). execSync/execFileSync return stdout — a Buffer, or a string with encoding: 'utf8' — or throw: the spawn/timeout/maxBuffer error itself, or 'Command failed: <cmd>' plus the captured stderr, the Error carrying the ExecException own properties status, signal, output, pid, stdout, stderr (readable through catch (e: any)); without an explicit stdio the child's stderr is mirrored to ours, as in Node.
ReturnsSpawnSyncResult | string — The result object / captured stdout.
const r = spawnSync("echo", ["kalimera", "kosme"])
console.log(r.status, r.stdout.trim())- An unpiped
stdout/stderr(stdio: 'inherit'/'ignore') isnullat run time while the field stays typedstring(Node's own typings do the same);outputis typed(string | null)[].