# jg-process

> Run and stop child processes without a shell: timeouts, output limits, abort signals, and whole process trees.

- **Version:** `1.3.0`
- **Runtimes:** Node.js
- **Install:** `npm install jg-process`
- **On npm:** [jg-process](https://www.npmjs.com/package/jg-process)
- **Source:** [packages/jg-process](https://github.com/jesusgraterol/jg-packages/tree/main/packages/jg-process)
- **Recipes:** [jg-process recipes](https://jg-packages.jesusgraterol.dev/packages/jg-process/recipes/index.md)

## Usage

Run a command to completion and read its output. The run is bounded by a timeout and an output limit, and both stop the whole process tree:

```ts
import { runProcess } from 'jg-process';

const { exitCode, stdout, stderr, durationMs } = await runProcess({
  command: 'git',
  args: ['status', '--porcelain'],
  cwd: repositoryDirectory,
  timeoutSeconds: 30,
  signal: AbortSignal.timeout(60_000),
});
```

A non-zero exit code resolves, so callers decide what counts as failure. A timeout, too much output, or a failure to start rejects with `PROCESS_TIMED_OUT`, `PROCESS_OUTPUT_TOO_LARGE`, or `PROCESS_START_FAILED`.

Start a long-running process that you own, and stop it together with everything it started:

```ts
import { startProcess, stopProcessTree } from 'jg-process';

const server = startProcess({ command: 'node', args: ['server.js'], cwd: 'apps/api' });
process.once('SIGINT', () => void stopProcessTree(server));
```

Start a server and wait until it answers, or stop everything it started when it doesn't:

```ts
import { startProcessAndWait } from 'jg-process';

const api = await startProcessAndWait(
  { command: process.execPath, args: ['server.js'], cwd: 'apps/api' },
  { readyWhen: { url: 'http://127.0.0.1:3000/health' }, timeoutSeconds: 60 },
);
```

Run a package manager without a shell on every platform, including Windows, where its `.cmd` shim can't be spawned directly:

```ts
import { resolvePackageManagerCommand, runProcess } from 'jg-process';

await runProcess({ ...resolvePackageManagerCommand(['run', 'build']), timeoutSeconds: 600 });
```

Reserve a free port before handing it to a server, keeping the preferred one when it's free:

```ts
import { reservePort } from 'jg-process';

const reservation = await reservePort({ preferredPort: 3000 }); // 3000, or another free port
await reservation.release(); // right before the server binds it
```

## <a id="api-reference"></a>jg-process API reference

Every export of `jg-process@1.3.0`, 25 in all, with its signature, its errors, and an example. It's generated from the package's TSDoc.

- Functions: [`reservePort`](#api-reservePort), [`resolvePackageManagerCommand`](#api-resolvePackageManagerCommand), [`runProcess`](#api-runProcess), [`startProcess`](#api-startProcess), [`startProcessAndWait`](#api-startProcessAndWait), [`stopProcessTree`](#api-stopProcessTree), [`waitForPort`](#api-waitForPort), [`waitForUrl`](#api-waitForUrl)
- Constants: [`DEFAULT_GRACE_PERIOD_SECONDS`](#api-DEFAULT_GRACE_PERIOD_SECONDS), [`DEFAULT_MAX_OUTPUT_BYTES`](#api-DEFAULT_MAX_OUTPUT_BYTES), [`ERRORS`](#api-ERRORS)
- Types: [`IPackageManager`](#api-IPackageManager), [`IPackageManagerCommand`](#api-IPackageManagerCommand), [`IPortReservation`](#api-IPortReservation), [`IProcessResult`](#api-IProcessResult), [`IReadinessWaitOptions`](#api-IReadinessWaitOptions), [`IReadyWhen`](#api-IReadyWhen), [`IReservePortOptions`](#api-IReservePortOptions), [`IResolvePackageManagerOptions`](#api-IResolvePackageManagerOptions), [`IRunProcessOptions`](#api-IRunProcessOptions), [`IStartProcessAndWaitOptions`](#api-IStartProcessAndWaitOptions), [`IStartProcessCommand`](#api-IStartProcessCommand), [`IStartProcessOptions`](#api-IStartProcessOptions), [`IWaitForPortOptions`](#api-IWaitForPortOptions), [`IWaitForUrlOptions`](#api-IWaitForUrlOptions)

### <a id="api-functions"></a>Functions

#### <a id="api-reservePort"></a>`reservePort`

*Function*

Reserves a free TCP port by listening on it, so nothing else can take it until it's released, such as before passing it to a dev server or a test fixture. The preferred port is kept when it's free, and any free port is reserved when it's taken. Connections to the reserved port are closed at once, and the reservation doesn't keep the process running.

A child process that needs the port itself can't bind it while it's reserved, so release it right before starting the child. Another process can take the port in between.

```ts
reservePort(options?: IReservePortOptions): Promise<IPortReservation>
```

**Parameters**

- `options?` `IReservePortOptions`: The preferred port and the address. The address defaults to 127.0.0.1. Defaults to `{}`.

**Returns** `Promise<IPortReservation>`: A promise that resolves to the reservation.

**Throws**

- `INVALID_PORT`: The preferred port must be an integer from 1 to 65535.
- `PORT_RESERVATION_FAILED`: The port couldn't be reserved.

**Example**

```ts
import { reservePort } from 'jg-process';

const reservation = await reservePort({ preferredPort: 3000 });
console.log(`The port is ${reservation.port}`); // 3000, or another free port when it's taken
await reservation.release();
```

#### <a id="api-resolvePackageManagerCommand"></a>`resolvePackageManagerCommand`

*Function*

Builds the command that runs a package manager with the given arguments, without a shell, so it can be passed to `runProcess` or `startProcess` as it is on Windows, macOS, and Linux. It looks, in order, at: 1. `npm_execpath`, which a package manager sets for the scripts it runs, when it names the requested package manager, or any one when none is requested 2. the package manager installed in the project, resolved from `cwd` 3. PATH: an executable on POSIX, and on Windows an `.exe`, or a `.cmd` or `.ps1` shim whose script it runs through Node.js, because Node.js refuses to spawn a `.cmd` file without a shell

A JavaScript entry, such as `pnpm.cjs`, runs through the current Node.js. Without a requested package manager or `npm_execpath`, there's nothing to look for, so it throws.

```ts
resolvePackageManagerCommand(
  args: string[],
  options?: IResolvePackageManagerOptions,
): IPackageManagerCommand
```

**Parameters**

- `args` `string[]`: The arguments for the package manager, such as `['run', 'build']`.
- `options?` `IResolvePackageManagerOptions`: The package manager, the environment, and the directory to resolve from. Defaults to `{}`.

**Returns** `IPackageManagerCommand`: The command and its arguments.

**Throws**

- `UNSUPPORTED_PACKAGE_MANAGER`: The package manager must be pnpm, npm, yarn, or bun.
- `PACKAGE_MANAGER_NOT_FOUND`: The package manager's executable couldn't be found.

**Example**

```ts
import { resolvePackageManagerCommand, runProcess } from 'jg-process';

const command = resolvePackageManagerCommand(['run', 'build'], { packageManager: 'pnpm' });
const { exitCode } = await runProcess({ ...command, timeoutSeconds: 600 });
```

#### <a id="api-runProcess"></a>`runProcess`

*Function*

Runs a command to completion and resolves with its exit code, signal and output. It runs without a shell and, on POSIX, as the leader of its own process group.

Every exit resolves, whatever its code: callers decide which codes are acceptable, so process output never ends up in an error message. The call rejects only when the command can't start, the timeout passes, the output exceeds maxOutputBytes, or the signal aborts; in those cases the process tree is stopped first, so the rejection can arrive up to the grace period plus two seconds after the trigger.

Input that the child never reads is discarded without an error. When the child exits, descendants left in its process group are stopped. If one that couldn't be reached, such as a descendant that left the group with setsid or one orphaned on Windows, keeps stdout or stderr open for longer than the grace period, the call rejects instead of hanging.

Because the child leads its own group, a terminal's Ctrl+C reaches only the parent: CLIs should abort the signal when they receive SIGINT or SIGTERM.

```ts
runProcess(options: IRunProcessOptions): Promise<IProcessResult>
```

**Parameters**

- `options` `IRunProcessOptions`: The command, its arguments, and the limits of the run.

**Returns** `Promise<IProcessResult>`: A promise that resolves with the exit code, the signal and the output.

**Throws**

- `PROCESS_START_FAILED`: Unable to start the command
- `PROCESS_TIMED_OUT`: The process didn't finish within the timeout
- `PROCESS_OUTPUT_TOO_LARGE`: The process wrote more output than allowed
- `PROCESS_TREE_SURVIVED`: The process tree is still running after it was force-stopped
- The signal's reason when the signal aborts.

**Example**

```ts
import { runProcess } from 'jg-process';

const { exitCode, stdout } = await runProcess({ command: 'git', args: ['status', '--porcelain'], timeoutSeconds: 30 });
if (exitCode !== 0) {
  // handle the failure
}
```

#### <a id="api-startProcess"></a>`startProcess`

*Function*

Starts a long-running child that the caller owns, without a shell and, on POSIX, as the leader of its own process group, so stopProcessTree can later stop it together with its descendants. Because the child leads its own group, a terminal's Ctrl+C reaches only the parent: CLIs should stop the child when they receive SIGINT or SIGTERM. A command that can't start emits an 'error' event on the returned child.

```ts
startProcess(
  command: string,
  args: string[],
  options?: IStartProcessOptions,
): ChildProcess
```

**Parameters**

- `command` `string`: The executable to run.
- `args` `string[]`: The arguments passed to the executable.
- `options?` `IStartProcessOptions`: The working directory, the environment and the stdio configuration ('inherit' by default).

**Returns** `ChildProcess`: The started child process.

Starts a long-running child from a command object, the form runProcess takes, with the same behavior as the positional form.

```ts
startProcess(command: IStartProcessCommand): ChildProcess
```

**Parameters**

- `command` `IStartProcessCommand`: The executable, its arguments, and the working directory, environment, and stdio.

**Returns** `ChildProcess`: The started child process.

**Example 1**

```ts
import { startProcess, stopProcessTree } from 'jg-process';

const server = startProcess('node', ['server.js'], { stdio: 'inherit' });
process.once('SIGINT', () => void stopProcessTree(server));
```

**Example 2**

```ts
import { startProcess } from 'jg-process';

const server = startProcess({ command: 'node', args: ['server.js'], cwd: 'apps/api' });
```

#### <a id="api-startProcessAndWait"></a>`startProcessAndWait`

*Function*

Starts a long-running process, such as a dev server, with `startProcess`, and resolves with it once it's ready: its port accepts connections, its URL answers, or a probe returns a value other than null, undefined, or false. When it exits, fails to start, isn't ready in time, or the signal aborts, its whole process tree is stopped before the call rejects, so nothing is left running.

```ts
startProcessAndWait(
  command: IStartProcessCommand,
  options: IStartProcessAndWaitOptions,
): Promise<ChildProcess>
```

**Parameters**

- `command` `IStartProcessCommand`: The command, its arguments, and the working directory, environment, and stdio.
- `options` `IStartProcessAndWaitOptions`: What ready means, the deadline and pace, the signal, and the grace period stopProcessTree gets on failure.

**Returns** `Promise<ChildProcess>`: A promise that resolves to the ready child process, which the caller owns.

**Throws**

- `INVALID_PORT`: The port must be an integer from 1 to 65535.
- `INVALID_READINESS_OPTIONS`: The readiness options need finite, positive seconds.
- `PROCESS_NOT_READY`: The process wasn't ready in time.
- `PROCESS_EXITED_BEFORE_READY`: The process exited before it was ready.
- `PROCESS_START_FAILED`: Unable to start the process.
- `PROCESS_TREE_SURVIVED`: The process tree is still running after it was force-stopped.
- The signal's reason when the signal aborts.

**Example**

```ts
import { startProcessAndWait, stopProcessTree } from 'jg-process';

const server = await startProcessAndWait(
  { command: process.execPath, args: ['server.js'], cwd: 'apps/api' },
  { readyWhen: { url: 'http://127.0.0.1:3000/health' }, timeoutSeconds: 60 },
);
await stopProcessTree(server);
```

#### <a id="api-stopProcessTree"></a>`stopProcessTree`

*Function*

Stops a child started by startProcess or runProcess together with its descendants, and resolves once they are gone. It resolves immediately for a child without a pid.

On POSIX, it sends SIGTERM to the child's process group, waits up to the grace period, then sends SIGKILL and waits up to two more seconds. It is designed for children that lead their own process group, as startProcess and runProcess start them; for any other child, it signals the child alone. Descendants that start their own session or process group, for example with setsid, aren't stopped.

On Windows, it runs taskkill /T /F, which reaches only descendants of a root process that is still running: when the root has already exited, it resolves and orphaned descendants may remain.

```ts
stopProcessTree(child: ChildProcess, gracePeriodSeconds?: number): Promise<void>
```

**Parameters**

- `child` `ChildProcess`: The child process to stop.
- `gracePeriodSeconds?` `number`: No description. Defaults to `DEFAULT_GRACE_PERIOD_SECONDS`.

**Returns** `Promise<void>`: A promise that resolves once the process tree is gone.

**Throws**

- `PROCESS_TREE_SURVIVED`: The process tree is still running after it was force-stopped.

**Example**

```ts
import { startProcess, stopProcessTree } from 'jg-process';

const server = startProcess('node', ['server.js']);
await stopProcessTree(server, 10); // SIGTERM, then SIGKILL after 10 seconds
```

#### <a id="api-waitForPort"></a>`waitForPort`

*Function*

Waits until a TCP port accepts connections, such as a database or a dev server that a script started, probing every `intervalSeconds` until `timeoutSeconds` pass. With a `child`, its exit or failure to start ends the wait at once, since it can no longer become ready.

```ts
waitForPort(port: number, options: IWaitForPortOptions): Promise<void>
```

**Parameters**

- `port` `number`: The port, from 1 to 65535.
- `options` `IWaitForPortOptions`: The address, the deadline and pace, the signal, and the child to watch.

**Returns** `Promise<void>`: A promise that resolves once a connection succeeds.

**Throws**

- `INVALID_PORT`: The port must be an integer from 1 to 65535.
- `INVALID_READINESS_OPTIONS`: The readiness options need finite, positive seconds.
- `PROCESS_NOT_READY`: The process wasn't ready in time.
- `PROCESS_EXITED_BEFORE_READY`: The process exited before it was ready.
- `PROCESS_START_FAILED`: Unable to start the process.
- The signal's reason when the signal aborts.

**Example**

```ts
import { waitForPort } from 'jg-process';

await waitForPort(5432, { timeoutSeconds: 30 }); // PostgreSQL accepts connections
```

#### <a id="api-waitForUrl"></a>`waitForUrl`

*Function*

Waits until a URL answers in a way that means ready, by default with a 2xx status, such as a health endpoint. Redirects aren't followed, a network error means not ready yet, and every response's body is discarded. With a `child`, its exit or failure to start ends the wait at once.

```ts
waitForUrl(url: string | URL, options: IWaitForUrlOptions): Promise<void>
```

**Parameters**

- `url` `string | URL`: The URL.
- `options` `IWaitForUrlOptions`: The deadline and pace, the request timeout, the predicate, the signal, and the child to watch.

**Returns** `Promise<void>`: A promise that resolves once a response means ready.

**Throws**

- `INVALID_READINESS_OPTIONS`: The readiness options need finite, positive seconds.
- `PROCESS_NOT_READY`: The process wasn't ready in time.
- `PROCESS_EXITED_BEFORE_READY`: The process exited before it was ready.
- `PROCESS_START_FAILED`: Unable to start the process.
- The signal's reason when the signal aborts.

**Example**

```ts
import { waitForUrl } from 'jg-process';

await waitForUrl('http://127.0.0.1:3000/health', { timeoutSeconds: 60 });
```

### <a id="api-constants"></a>Constants

#### <a id="api-DEFAULT_GRACE_PERIOD_SECONDS"></a>`DEFAULT_GRACE_PERIOD_SECONDS`

*Constant*

the seconds stopProcessTree waits after SIGTERM before it sends SIGKILL

```ts
const DEFAULT_GRACE_PERIOD_SECONDS: 5
```

#### <a id="api-DEFAULT_MAX_OUTPUT_BYTES"></a>`DEFAULT_MAX_OUTPUT_BYTES`

*Constant*

the bytes stdout and stderr can hold together when runProcess gets no maxOutputBytes (1 MiB)

```ts
const DEFAULT_MAX_OUTPUT_BYTES: 1048576
```

#### <a id="api-ERRORS"></a>`ERRORS`

*Constant*

the codes for every error thrown by the package

```ts
const ERRORS: { PROCESS_START_FAILED: "PROCESS_START_FAILED"; PROCESS_TIMED_OUT: "PROCESS_TIMED_OUT"; PROCESS_OUTPUT_TOO_LARGE: "PROCESS_OUTPUT_TOO_LARGE"; PROCESS_TREE_SURVIVED: "PROCESS_TREE_SURVIVED"; INVALID_PORT: "INVALID_PORT"; PORT_RESERVATION_FAILED: "PORT_RESERVATION_FAILED"; UNSUPPORTED_PACKAGE_MANAGER: "UNSUPPORTED_PACKAGE_MANAGER"; PACKAGE_MANAGER_NOT_FOUND: "PACKAGE_MANAGER_NOT_FOUND"; INVALID_READINESS_OPTIONS: "INVALID_READINESS_OPTIONS"; PROCESS_NOT_READY: "PROCESS_NOT_READY"; PROCESS_EXITED_BEFORE_READY: "PROCESS_EXITED_BEFORE_READY" }
```

### <a id="api-types"></a>Types

#### <a id="api-IPackageManager"></a>`IPackageManager`

*Type*

the package managers resolvePackageManagerCommand finds

```ts
type IPackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';
```

#### <a id="api-IPackageManagerCommand"></a>`IPackageManagerCommand`

*Type*

a command to spawn without a shell, which runProcess and startProcess take as they are

```ts
type IPackageManagerCommand = {
    command: string;
    args: string[];
};
```

**Members**

- `command: string` (property)
- `args: string[]` (property)

#### <a id="api-IPortReservation"></a>`IPortReservation`

*Type*

a port held until it's released, so nothing else can take it meanwhile

```ts
type IPortReservation = {
    port: number;
    host: string;
    release: () => Promise<void>;
};
```

**Members**

- `port: number` (property): the reserved port
- `host: string` (property): the address it's reserved on
- `release: () => Promise<void>` (property): stops holding the port; calling it again does nothing

#### <a id="api-IProcessResult"></a>`IProcessResult`

*Type*

the outcome of a process that ran to completion

```ts
type IProcessResult = {
    exitCode: number | null;
    signalCode: NodeJS.Signals | null;
    stdout: string;
    stderr: string;
    durationMs: number;
};
```

**Members**

- `exitCode: number | null` (property): the exit code, or null when a signal ended the process
- `signalCode: NodeJS.Signals | null` (property): the signal that ended the process, or null when it exited on its own
- `stdout: string` (property): the output, decoded as UTF-8
- `stderr: string` (property)
- `durationMs: number` (property): the milliseconds between the start and the end of the run

#### <a id="api-IReadinessWaitOptions"></a>`IReadinessWaitOptions`

*Type*

what a readiness wait shares: its deadline, its pace, and what ends it early

```ts
type IReadinessWaitOptions = {
    timeoutSeconds: number;
    intervalSeconds?: number | undefined;
    signal?: AbortSignal | undefined;
    child?: ChildProcess | undefined;
};
```

**Members**

- `timeoutSeconds: number` (property): the most seconds the whole wait can take
- `intervalSeconds: number` (property): the seconds between probes. Defaults to 0.25
- `signal: AbortSignal` (property): stops the wait and rejects with the signal's reason when it aborts
- `child: ChildProcess` (property): a process whose exit or start failure ends the wait, because it can no longer become ready

#### <a id="api-IReadyWhen"></a>`IReadyWhen`

*Type*

what startProcessAndWait waits for: a port, a URL, or a probe returning other than null, undefined, or false

```ts
type IReadyWhen = {
    port: number;
    host?: string | undefined;
} | {
    url: string | URL;
    isReady?: IWaitForUrlOptions['isReady'];
} | {
    probe: (signal: AbortSignal) => unknown;
};
```

#### <a id="api-IReservePortOptions"></a>`IReservePortOptions`

*Type*

the options reservePort takes

```ts
type IReservePortOptions = {
    preferredPort?: number | undefined;
    host?: string | undefined;
};
```

**Members**

- `preferredPort: number` (property): the port to try first, from 1 to 65535; another free one is reserved when it's taken
- `host: string` (property): the address to listen on. Defaults to 127.0.0.1

#### <a id="api-IResolvePackageManagerOptions"></a>`IResolvePackageManagerOptions`

*Type*

the options resolvePackageManagerCommand takes

```ts
type IResolvePackageManagerOptions = {
    packageManager?: IPackageManager | undefined;
    env?: NodeJS.ProcessEnv | undefined;
    cwd?: string | undefined;
};
```

**Members**

- `packageManager: IPackageManager` (property): The package manager to run. Defaults to the one running the current package script, which npm_execpath names; without one, a package manager has to be named.
- `env: NodeJS.ProcessEnv` (property): the environment that holds npm_execpath and PATH. Defaults to process.env
- `cwd: string` (property): the directory a local install is resolved from. Defaults to the current one

#### <a id="api-IRunProcessOptions"></a>`IRunProcessOptions`

*Type*

the options accepted by runProcess

```ts
type IRunProcessOptions = {
    command: string;
    args: string[];
    cwd?: string;
    env?: NodeJS.ProcessEnv;
    input?: string | Uint8Array;
    timeoutSeconds?: number;
    maxOutputBytes?: number;
    signal?: AbortSignal;
};
```

**Members**

- `command: string` (property): the executable to run, without a shell
- `args: string[]` (property): the arguments passed to the executable
- `cwd: string` (property): the working directory. Defaults to the current one
- `env: NodeJS.ProcessEnv` (property): the environment variables. Defaults to process.env
- `input: string | Uint8Array` (property): the data written to the process's stdin before it is closed
- `timeoutSeconds: number` (property): the seconds after which the process tree is stopped and the call rejects
- `maxOutputBytes: number` (property): the bytes stdout and stderr can hold together. Defaults to DEFAULT_MAX_OUTPUT_BYTES
- `signal: AbortSignal` (property): stops the process tree and rejects with the signal's reason when it aborts

#### <a id="api-IStartProcessAndWaitOptions"></a>`IStartProcessAndWaitOptions`

*Type*

the options startProcessAndWait takes

```ts
type IStartProcessAndWaitOptions = Omit<IReadinessWaitOptions, 'child'> & {
    readyWhen: IReadyWhen;
    gracePeriodSeconds?: number | undefined;
};
```

#### <a id="api-IStartProcessCommand"></a>`IStartProcessCommand`

*Type*

a command and its options in one object, the form runProcess takes, accepted by startProcess too

```ts
type IStartProcessCommand = IStartProcessOptions & {
    command: string;
    args: string[];
};
```

#### <a id="api-IStartProcessOptions"></a>`IStartProcessOptions`

*Type*

the options accepted by startProcess

```ts
type IStartProcessOptions = {
    cwd?: string;
    env?: NodeJS.ProcessEnv;
    stdio?: StdioOptions;
};
```

**Members**

- `cwd: string` (property): the working directory. Defaults to the current one
- `env: NodeJS.ProcessEnv` (property): the environment variables. Defaults to process.env
- `stdio: StdioOptions` (property): the stdio configuration. Defaults to 'inherit'

#### <a id="api-IWaitForPortOptions"></a>`IWaitForPortOptions`

*Type*

the options waitForPort takes

```ts
type IWaitForPortOptions = IReadinessWaitOptions & {
    host?: string | undefined;
    connectTimeoutSeconds?: number | undefined;
};
```

#### <a id="api-IWaitForUrlOptions"></a>`IWaitForUrlOptions`

*Type*

the options waitForUrl takes

```ts
type IWaitForUrlOptions = IReadinessWaitOptions & {
    requestTimeoutSeconds?: number | undefined;
    isReady?: ((response: Response) => boolean | Promise<boolean>) | undefined;
};
```
