# @nice-code/process — documentation Supervise a child process from TypeScript — spawn, readiness, restart backoff, log capture, and cross-platform tree teardown. This file concatenates only the @nice-code/process pages of https://nicecode.io — point a local AI coding assistant at it when you're working with just this package. For the full set (every nice-code package and how they fit together) use /llms.txt. Generated from src/content/docs — do not edit by hand. --- # Managing a Process Source: /nice-process/managing-a-process Description: Spawn correctness, an always-drained output pipeline, readiness, restart policy, and a stop ladder that ends the whole tree — the same on Windows, Linux, WSL, and macOS. `@nice-code/process` manages **one child process** properly. Not a task runner, not a supervisor — one process, with the parts that are easy to get subtly wrong done once: argv-first spawn, an output pipeline that never blocks the child, readiness you can await, and a stop that ends the whole **tree** rather than the pid you happen to hold. ```bash bun add @nice-code/process ``` ```ts import { createNiceProcess } from "@nice-code/process"; const web = createNiceProcess({ id: "web", run: ["bun", "run", "dev"], cwd: "./apps/web", ready: { kind: "port", port: 5173 }, }); await web.start(); await web.whenReady(); // … await web.stop(); ``` ## Spawn is argv-first `run` is an **array**, and it is passed through untouched — no shell, no splitting, no quoting rules to lose an argument to: ```ts run: ["node", "server.js", "--name", "my app"] // one argument, spaces and all ``` A string needs an explicit `shell`, because a string is a *shell program*, not a command: ```ts run: "tsc --noEmit && biome check .", shell: true, ``` A constrained string with no metacharacters is split for you, but anything quoted or piped is rejected with a pointer at the two real forms rather than being split wrong and run anyway. On Windows, `.cmd`/`.bat` shims resolve through `PATH` + `PATHEXT` and are wrapped in `cmd.exe` with verbatim arguments, so a shim's arguments survive its own quoting rules. ## Readiness is a contract, not a sleep ```ts ready: { kind: "logMatch", match: "ready in" } // a banner on stdout/stderr ready: { kind: "port", port: 5173 } // something is really listening ready: { kind: "httpOk", port: 8787, path: "/health" } // …and it answers ready: { kind: "delay", ms: 500 } // last resort ready: { kind: "exit0" } // for a one-shot task ``` `whenReady()` resolves when the contract is met and **rejects** with a typed reason otherwise — and it is bound to the current run, so a restart never resolves a previous run's waiter. Two behaviours worth knowing: - **Readiness latches after a stability window.** A port that opens and closes during a startup crash is not ready. A `healthProbeMs` keeps probing afterwards, and a later failure moves the process to `unready` rather than pretending it is still up. - **A readiness timeout never kills anything.** The process keeps running and is marked `unready` with the reason. A slow cold start is not a crash, and the log is right there to explain it. `logMatch` is matched per line with ANSI removed. Match something short and early — a line longer than `log.lineBytes` (8 KiB) arrives as bounded segments, so a pattern buried in a huge JSON payload can straddle a boundary and never match. ## Output: bounded, ordered, never blocking The pipe buffer is the child's backpressure, so we always drain it. Every line carries a monotonic `seq`, a stable `id`, a wall-clock `ts` (display only) and a monotonic `at` (ordering). ```ts for await (const line of web.lines()) { process.stdout.write(`${line.text}\n`); } const history = web.ring(); // the bounded tail, always available ``` - A bare `\r` becomes a **`replace` op on the same line id**, so a progress bar is one line that updates rather than ten thousand lines. - A 10 MB line with no newline arrives as capped continuation frames — never one unbounded buffer waiting for a `\n`. - A consumer that stops reading gets an explicit **gap marker**, not silent loss and not unbounded memory: ```ts for await (const line of web.lines({ maxQueuedBytes: 64_000, onGap: (n) => warn(`${n} dropped`) })) { } ``` `seq` and line ids continue **across restarts**, because the ring outlives a run: a spinner in the second run can never overwrite a line from the first. ## Stopping ends the tree `stop()` walks a ladder and verifies at each rung — optional `stopInput` (a key some tools take as "quit"), then SIGINT → SIGTERM → SIGKILL on POSIX, against the **process group**; on Windows, a Job Object owns the tree, with `taskkill /T /F` as an honestly-reported degraded tier. ```ts const capability = web.capability; // { treeMechanism: "job-object" | "process-group" | "taskkill-degraded", degraded, notes } ``` This is the part most code gets wrong: killing `child.pid` for `npm run dev` kills the shim and leaves the actual server holding the port. A dead root is not a dead tree, and `stop()` reaps a surviving descendant after a voluntary root exit. `stop()` is idempotent, safe during `starting` (it waits for the spawn to settle first), and reports whether the exit was expected — so a UI never paints a stop you asked for as a failure. ## Restarts have a budget ```ts restart: { on: "unexpected-exit", backoff: { initialMs: 500, maxMs: 15_000, jitter: true }, maxAttempts: 5, windowMs: 60_000, stableAfterReadyMs: 10_000, } ``` Jitter matters: ten processes restarting in lockstep after a laptop wakes is a thundering herd. The budget resets only after the process has genuinely *served* for `stableAfterReadyMs`. **`beforeReady` defaults to `false`.** A service that dies before its first readiness is almost always misconfigured, and many start commands mutate the tree (`wrangler types`, a formatter pass) — retrying those blindly re-mutates it. Opt in per process when a pre-ready retry is genuinely right. Backoff and uptime use a **monotonic** clock, so a laptop sleeping or NTP stepping the clock cannot collapse a deadline. ## The ledger: orphans, and never killing the wrong thing Attach a ledger and every spawn records `{pid, startStamp, treeId}`. A later start reaps what a crashed owner left behind. ```ts import { openProcessLedger } from "@nice-code/process"; const ledger = await openProcessLedger({ dir: "./.nice/ledger", namespace: "my-app" }); ``` The rule that makes this safe: **a pid match alone never authorizes a kill.** Pids are reused. A record whose fingerprint no longer matches is *quarantined* — surfaced, never killed. A record whose owner is still alive is an execution lease and is left strictly alone. ## Logs on disk ```ts log: { file: { dir: "./.logs", keepRuns: 5, maxTotalBytes: 50_000_000 } } ``` Segmented per run with retention. If the disk fills or a write fails, it degrades to ring-only with an in-band marker — it never crashes and never blocks the child.