# Child Processes

Effect models running external commands as two pieces: a **`Command`** value
that describes what to run, and the **`ChildProcessSpawner`** service that
actually runs it. A `Command` is a plain, immutable description — building or
composing one never starts a process — so you can construct, pipe, and reuse
commands freely. These APIs live under `effect/unstable/process`, and the Node
implementation comes from `NodeServices.layer` (or the narrower
`NodeChildProcessSpawner.layer`).
**Caution:** The `effect/unstable/process` modules are **unstable**: they are production-ready
but the API may change between minor versions. Import them from
`effect/unstable/process`.

```ts
import { NodeServices } from "@effect/platform-node"
import { Console, Context, Effect, Layer, Schema, Stream, String } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

// Wrap process failures in a domain error rather than leaking PlatformError.
class DevToolsError extends Schema.TaggedErrorClass<DevToolsError>()(
  "DevToolsError",
  { cause: Schema.Defect }
) {}

class DevTools extends Context.Service<DevTools, {
  readonly nodeVersion: Effect.Effect<string, DevToolsError>
  readonly runLintFix: Effect.Effect<void, DevToolsError>
}>()("app/DevTools") {
  static readonly layer = Layer.effect(
    DevTools,
    Effect.gen(function*() {
      // Running a command requires a `ChildProcessSpawner`.
      const spawner = yield* ChildProcessSpawner.ChildProcessSpawner

      // `spawner.string` runs a command and collects ALL of its stdout into a
      // single string. Ideal for short, finite commands.
      const nodeVersion = spawner.string(
        ChildProcess.make("node", ["--version"])
      ).pipe(
        Effect.map(String.trim),
        Effect.mapError((cause) => new DevToolsError({ cause }))
      )

      const runLintFix = Effect.gen(function*() {
        // `spawner.spawn` returns a handle so you can stream output WHILE the
        // process is still running. It adds a `Scope` requirement to manage
        // the process lifecycle.
        const handle = yield* spawner.spawn(
          ChildProcess.make("pnpm", ["lint-fix"], {
            env: { FORCE_COLOR: "1" }, // set env vars for the child
            extendEnv: true            // ...on top of the parent's env
          })
        )

        // `handle.all` interleaves stdout and stderr as a byte Stream.
        yield* handle.all.pipe(
          Stream.decodeText(),
          Stream.splitLines,
          Stream.runForEach((line) => Console.log(`[lint-fix] ${line}`))
        )

        // Wait for the process to finish and inspect its exit code.
        const exitCode = yield* handle.exitCode
        if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
          return yield* new DevToolsError({
            cause: new Error(`lint-fix exited with ${exitCode}`)
          })
        }
      }).pipe(
        Effect.mapError((cause) =>
          cause instanceof DevToolsError ? cause : new DevToolsError({ cause })
        ),
        // `spawn` is scoped; `Effect.scoped` provides the scope and ensures the
        // child process is cleaned up when the effect completes or fails.
        Effect.scoped
      )

      return { nodeVersion, runLintFix } as const
    })
  ).pipe(
    // Provide the spawner from the Node platform services.
    Layer.provide(NodeServices.layer)
  )
}
```

## Describing a command

`ChildProcess.make` builds a `Command`. It accepts a program name with an array
of arguments, or a tagged-template form for shorthand. Arguments are passed
directly to the OS — they are **not** interpreted by a shell — so values
containing spaces or special characters are safe without quoting:

```ts
import { ChildProcess } from "effect/unstable/process"

// Explicit program + args (recommended; no shell quoting pitfalls).
const a = ChildProcess.make("git", ["commit", "-m", "a message with spaces"])

// Tagged-template shorthand for simple commands.
const b = ChildProcess.make`node --version`

// Configure the working directory, environment, and more via options.
const c = ChildProcess.make("npm", ["test"], {
  cwd: "packages/core",
  env: { CI: "true" },
  extendEnv: true
})
```

A `Command` is itself an `Effect` — yielding it spawns the process and returns a
handle (it calls `spawn` under the hood). The combinators below
(`setCwd`, `setEnv`, `prefix`, `pipeTo`) return a new `Command` without mutating
the original.

## Collecting output

When a command runs to completion and you want its output, the spawner offers
convenience methods that run the command and return the result directly — no
handle, no scope:

- `spawner.string(command)` — all stdout as a single `string`.
- `spawner.lines(command)` — stdout split into an array of lines.
- `spawner.exitCode(command)` — just the exit code.

```ts
import { Effect } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

const changedFiles = Effect.fn("changedFiles")(function*(baseRef: string) {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner

  // `lines` is perfect for line-oriented commands like `git diff`.
  const files = yield* spawner.lines(
    ChildProcess.make("git", ["diff", "--name-only", `${baseRef}...HEAD`])
  )

  return files.filter((file) => file.endsWith(".ts"))
})
```

Each of these collectors accepts an optional `{ includeStderr?: boolean }` to
fold stderr into the collected output.

## Pipelines

`ChildProcess.pipeTo` connects the output of one command to the input of
another, mirroring a shell pipe. The result is a single `Command`, so it runs as
a real OS pipeline rather than buffering through your process:

```ts
import { Effect } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

const recentSubjects = Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner

  // Equivalent to: git log --pretty=format:%s -n 20 | head -n 5
  const command = ChildProcess.make("git", [
    "log",
    "--pretty=format:%s",
    "-n",
    "20"
  ]).pipe(
    ChildProcess.pipeTo(ChildProcess.make("head", ["-n", "5"]))
  )

  return yield* spawner.lines(command)
})
```

By default `pipeTo` connects `stdout` to `stdin`. Pass `{ from: "stderr" }` or
`{ from: "all" }` to pipe a different stream, and `{ to: "fd3" }` to target a
custom file descriptor on the destination.

## Feeding stdin

A command can take its standard input from a `Stream` of bytes. Set `stdin` in
the options to a `Stream<Uint8Array, PlatformError>` (use `Stream.encodeText`
to convert from text), and the spawner pipes it into the process:

```ts
import { Effect, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

const sortLines = Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner

  // Pipe three lines of text into `sort` via the child's stdin.
  const input = Stream.make("banana\n", "apple\n", "cherry\n").pipe(
    Stream.encodeText
  )

  return yield* spawner.lines(
    ChildProcess.make("sort", [], { stdin: input })
  )
  // => ["apple", "banana", "cherry"]
})
```

When you have a handle from `spawn`, `handle.stdin` is a `Sink` you can run a
stream into for interactive, incremental writes:

```ts
import { Effect, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

const program = Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
  const handle = yield* spawner.spawn(ChildProcess.make`cat`)

  // Write to the process input by running a stream into `handle.stdin`.
  yield* Stream.make("hello\n").pipe(
    Stream.encodeText,
    Stream.run(handle.stdin)
  )
}).pipe(Effect.scoped)
```

## Streaming a running process

For long-running commands — build watchers, log tailers, servers — you want to
react to output as it arrives. `spawner.spawn` returns a `ChildProcessHandle`
exposing:

- `handle.stdout`, `handle.stderr`, `handle.all` — output as byte `Stream`s.
- `handle.stdin` — a `Sink` to write to the process input.
- `handle.exitCode` — an effect that completes when the process exits.
- `handle.isRunning`, `handle.pid`, and `handle.kill` for control.

You can also stream output without a handle using `spawner.streamLines` or
`spawner.streamString`, which return a `Stream` directly:

```ts
import { Console, Effect, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

const tailBuild = Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner

  yield* spawner.streamLines(
    ChildProcess.make("npm", ["run", "build", "--", "--watch"]),
    { includeStderr: true } // interleave stderr with stdout
  ).pipe(
    Stream.runForEach((line) => Console.log(`[build] ${line}`))
  )
})
```

## Inspecting exit codes and errors

Exit codes are branded with `ExitCode` to keep them distinct from ordinary
numbers. Compare against `ChildProcessSpawner.ExitCode(0)` to check for success:

```ts
import { Effect } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

const typecheck = Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner

  const code = yield* spawner.exitCode(
    ChildProcess.make("tsc", ["--noEmit"])
  )

  return code === ChildProcessSpawner.ExitCode(0) ? "passed" : "failed"
})
```

Failures to spawn, write to, or read from a process surface in the **error
channel** as a `PlatformError` — not as a defect. A non-zero exit code, by
contrast, is a *successful* run that produced a non-zero code, so you check it
explicitly as above. Wrap `PlatformError` into a domain error with
`Effect.mapError`, or match on it with `Effect.catchTag`:

```ts
import { Effect } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

const safe = Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
  return yield* spawner.string(ChildProcess.make`does-not-exist`)
}).pipe(
  // PlatformError carries a `_tag` such as "SystemError" / "BadArgument".
  Effect.catch((error) => Effect.succeed(`failed: ${error._tag}`))
)
```

## Running it

Provide the spawner and run with `NodeRuntime.runMain`. Note how the program is
written entirely against the abstract `ChildProcessSpawner` — only the layer
mentions Node:

```ts
import { NodeRuntime } from "@effect/platform-node"
import { Effect } from "effect"

// `DevTools` from the first example provides its own layer.
const program = Effect.gen(function*() {
  const tools = yield* DevTools
  yield* Effect.log(`node=${yield* tools.nodeVersion}`)
}).pipe(Effect.provide(DevTools.layer))

NodeRuntime.runMain(program)
```

`NodeServices.layer` bundles the `ChildProcessSpawner` together with the other
core platform services (FileSystem, Path, Crypto, Terminal, Stdio). If you only
need to run processes, the narrower `NodeChildProcessSpawner.layer` provides just
the spawner. On Bun, use `BunChildProcessSpawner.layer` from
`@effect/platform-bun`.

```ts
import { NodeChildProcessSpawner } from "@effect/platform-node"
import { Effect } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

const program = Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
  return yield* spawner.string(ChildProcess.make`echo hi`)
}).pipe(
  // Provide only the spawner, not the full NodeServices bundle.
  Effect.provide(NodeChildProcessSpawner.layer)
)
```

To consume command output with the full set of stream operators — buffering,
batching, decoding — see the [Streaming](https://effect.plants.sh/streaming/) section.

## API reference: `ChildProcess`

The `ChildProcess` module builds and combines `Command` values. Everything here
is pure: nothing runs until you spawn the command (by yielding it or passing it
to a `ChildProcessSpawner` method).

### `make`

Builds a `StandardCommand`. Supports three calling conventions: a tagged
template, an options object followed by a tagged template, or an explicit
`(command, args?, options?)` array form.

```ts
import { ChildProcess } from "effect/unstable/process"

const t = ChildProcess.make`git status` // template form
const o = ChildProcess.make({ cwd: "/tmp" })`ls -la` // options + template
const a = ChildProcess.make("git", ["status"]) // array form
// => all three are StandardCommand values (not yet running)
```

### `pipeTo`

Connects the output of one command to the input of another, producing a single
`PipedCommand`. Defaults to `stdout` → `stdin`; customize with
`{ from, to }`.

```ts
import { ChildProcess } from "effect/unstable/process"

const pipeline = ChildProcess.make`cat package.json`.pipe(
  ChildProcess.pipeTo(ChildProcess.make`grep name`)
)
// => PipedCommand equivalent to: cat package.json | grep name
```

### `prefix`

Prepends another command in front of an existing one. For pipelines, only the
leftmost command is prefixed. Useful for wrappers like `time` or `nice`.

```ts
import { ChildProcess } from "effect/unstable/process"

const timed = ChildProcess.make`echo foo`.pipe(ChildProcess.prefix`time`)
// => runs: time echo foo
```

### `setCwd`

Returns a new command with its working directory set. For pipelines, applies to
every command in the pipeline.

```ts
import { ChildProcess } from "effect/unstable/process"

const cmd = ChildProcess.make`ls -la`.pipe(ChildProcess.setCwd("/tmp"))
// => command that runs `ls -la` in /tmp
```

### `setEnv`

Returns a new command with environment variables merged in (overriding duplicate
keys). For pipelines, applies to every command in the pipeline.

```ts
import { ChildProcess } from "effect/unstable/process"

const cmd = ChildProcess.make`node script.js`.pipe(
  ChildProcess.setEnv({ NODE_ENV: "test" })
)
// => command with NODE_ENV=test added to its environment
```

### `isCommand`

Type guard: returns `true` if a value is any `Command`.

```ts
import { ChildProcess } from "effect/unstable/process"

ChildProcess.isCommand(ChildProcess.make`ls`) // => true
ChildProcess.isCommand("ls") // => false
```

### `isStandardCommand`

Narrows a `Command` to a `StandardCommand` (a single program, not a pipeline).

```ts
import { ChildProcess } from "effect/unstable/process"

ChildProcess.isStandardCommand(ChildProcess.make`ls`) // => true
```

### `isPipedCommand`

Narrows a `Command` to a `PipedCommand` (a pipeline built with `pipeTo`).

```ts
import { ChildProcess } from "effect/unstable/process"

const piped = ChildProcess.make`ls`.pipe(
  ChildProcess.pipeTo(ChildProcess.make`wc -l`)
)
ChildProcess.isPipedCommand(piped) // => true
```

### `parseFdName`

Parses an fd name like `"fd3"` to its numeric index, returning `undefined` for
invalid names or indices below 3.

```ts
import { ChildProcess } from "effect/unstable/process"

ChildProcess.parseFdName("fd3") // => 3
ChildProcess.parseFdName("fd1") // => undefined (must be >= 3)
ChildProcess.parseFdName("stdout") // => undefined
```

### `fdName`

Creates an fd name string from a numeric index. The inverse of `parseFdName`.

```ts
import { ChildProcess } from "effect/unstable/process"

ChildProcess.fdName(3) // => "fd3"
```

## API reference: command types

### `Command`

A union of `StandardCommand | PipedCommand`. Every `Command` is also an
`Effect<ChildProcessHandle, PlatformError, ChildProcessSpawner | Scope>`, so it
can be yielded directly to spawn.

### `StandardCommand`

A single program: `{ _tag: "StandardCommand", command, args, options }`. Produced
by `make`.

### `PipedCommand`

A pipeline: `{ _tag: "PipedCommand", left, right, options }`. Produced by
`pipeTo`.

### `CommandOptions`

The options object accepted by `make`. It extends `KillOptions` and adds:
`cwd`, `env`, `extendEnv`, `shell`, `detached`, `stdin`, `stdout`, `stderr`, and
`additionalFds`.

```ts
import { ChildProcess } from "effect/unstable/process"

const cmd = ChildProcess.make("node", ["server.js"], {
  cwd: "/app",
  env: { PORT: "3000" },
  extendEnv: true,        // merge with process.env
  detached: false,
  killSignal: "SIGTERM",  // from KillOptions
  forceKillAfter: "5 seconds"
})
```

### `KillOptions`

Controls termination: `killSignal` (defaults to `"SIGTERM"`) and
`forceKillAfter` (a `Duration.Input` after which `"SIGKILL"` is sent). Used both
in `CommandOptions` and as the argument to `handle.kill`.

### `StdinConfig` / `StdoutConfig` / `StderrConfig`

Detailed per-stream configuration objects accepted by the `stdin` / `stdout` /
`stderr` options. `StdinConfig` carries `stream` (the `CommandInput`),
`endOnDone`, and `encoding`; `StdoutConfig` / `StderrConfig` carry `stream` (a
`CommandOutput`).

```ts
import { ChildProcess } from "effect/unstable/process"
import { Stream } from "effect"

const cmd = ChildProcess.make("cat", [], {
  stdin: {
    stream: Stream.make("hi").pipe(Stream.encodeText),
    endOnDone: true,
    encoding: "utf-8"
  },
  stdout: { stream: "pipe" }
})
```

### `CommandInput`

The set of stdin configurations: `"pipe"`, `"inherit"`, `"ignore"`,
`"overlapped"`, or a `Stream<Uint8Array, PlatformError>` piped into the process.

### `CommandOutput`

The set of stdout/stderr configurations: `"pipe"`, `"inherit"`, `"ignore"`,
`"overlapped"`, or a `Sink<Uint8Array, Uint8Array, never, PlatformError>` that
receives the output.

### `PipeOptions`

Options for `pipeTo`: `from` (a `PipeFromOption`) and `to` (a `PipeToOption`).

### `PipeFromOption` / `PipeToOption`

`PipeFromOption` is ``"stdout" | "stderr" | "all" | `fd${number}` ``;
`PipeToOption` is ``"stdin" | `fd${number}` ``.

```ts
import { ChildProcess } from "effect/unstable/process"

const p = ChildProcess.make`build`.pipe(
  ChildProcess.pipeTo(ChildProcess.make`tee log.txt`, { from: "all", to: "stdin" })
)
// => pipes interleaved stdout+stderr into tee's stdin
```

### `AdditionalFdConfig`

Configuration for extra file descriptors (`fd3` and up) supplied via the
`additionalFds` option. Each entry is `{ type: "input", stream? }` (parent
writes to child) or `{ type: "output", sink? }` (parent reads from child).

```ts
import { ChildProcess } from "effect/unstable/process"

const cmd = ChildProcess.make("my-program", [], {
  additionalFds: {
    fd3: { type: "output" } // read child output on fd 3 via handle.getOutputFd(3)
  }
})
```

### `Signal`

A union of POSIX signal names (`"SIGTERM"`, `"SIGKILL"`, `"SIGINT"`, …) accepted
by `KillOptions.killSignal` and `handle.kill`.

### `Encoding`

A union of supported text encodings (`"utf-8"`, `"ascii"`, `"hex"`, `"base64"`,
…) used by `StdinConfig.encoding`.

### `TemplateExpression` / `TemplateExpressionItem`

The values allowed inside `make` template literals: a `string | number |
boolean`, or a readonly array of those (each element becomes a separate
argument).

```ts
import { ChildProcess } from "effect/unstable/process"

const files = ["a.ts", "b.ts"]
const cmd = ChildProcess.make`prettier --write ${files}`
// => prettier --write a.ts b.ts
```

## API reference: `ChildProcessSpawner`

The service that executes commands. Obtain it with `yield*
ChildProcessSpawner.ChildProcessSpawner`. The convenience methods all accept an
optional `{ includeStderr?: boolean }` to fold stderr into the output.

### `spawn`

The primitive. Spawns a command and returns a scoped `ChildProcessHandle` for
streaming, writing stdin, and lifecycle control. Adds a `Scope` requirement.

```ts
import { Effect, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

const program = Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
  const handle = yield* spawner.spawn(ChildProcess.make`echo hi`)
  return yield* Stream.runCollect(handle.stdout) // => Chunk of bytes
}).pipe(Effect.scoped)
```

### `exitCode`

Runs a command to completion and returns just its branded `ExitCode`. Manages
its own scope.

```ts
import { Effect } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

const code = Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
  return yield* spawner.exitCode(ChildProcess.make`true`)
})
// => ExitCode(0)
```

### `string`

Runs a command and collects all output as a single `string`.

```ts
import { Effect } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

const out = Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
  return yield* spawner.string(ChildProcess.make`echo hello`)
})
// => "hello\n"
```

### `lines`

Runs a command and collects output split into an `Array<string>` of lines.

```ts
import { Effect } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

const ls = Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
  return yield* spawner.lines(ChildProcess.make`ls`)
})
// => ["file-a.ts", "file-b.ts", ...]
```

### `streamString`

Streams output as decoded text chunks (a `Stream<string, PlatformError>`)
without collecting it — good for long-running processes.

```ts
import { Effect, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

const program = Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
  yield* spawner.streamString(ChildProcess.make`tail -f log.txt`).pipe(
    Stream.runForEach((chunk) => Effect.log(chunk))
  )
})
```

### `streamLines`

Streams output split into lines as a `Stream<string, PlatformError>`. The
line-oriented counterpart of `streamString`.

```ts
import { Console, Effect, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

const program = Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
  yield* spawner.streamLines(ChildProcess.make`tail -f log.txt`).pipe(
    Stream.runForEach((line) => Console.log(line))
  )
})
```

## API reference: `ChildProcessHandle`

The handle returned by `spawn`. Bound to the surrounding `Scope`: when the scope
closes, the process is cleaned up.

### `pid`

The branded operating-system `ProcessId` of the running process.

```ts
import { Effect } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
  const handle = yield* spawner.spawn(ChildProcess.make`sleep 1`)
  return handle.pid // => ProcessId(12345)
}).pipe(Effect.scoped)
```

### `exitCode`

An `Effect<ExitCode, PlatformError>` that completes with the branded exit code
when the process exits.

```ts
import { ChildProcessSpawner } from "effect/unstable/process"

// handle.exitCode
// => yields ExitCode(0) on success
ChildProcessSpawner.ExitCode(0) // => the success code to compare against
```

### `isRunning`

An `Effect<boolean, PlatformError>` reporting whether the process is still
running.

```ts
import { Effect } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
  const handle = yield* spawner.spawn(ChildProcess.make`sleep 5`)
  return yield* handle.isRunning // => true
}).pipe(Effect.scoped)
```

### `kill`

Terminates the process, optionally with custom `KillOptions`. Defaults to
`SIGTERM`.

```ts
import { Effect } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
  const handle = yield* spawner.spawn(ChildProcess.make`sleep 60`)
  yield* handle.kill({ killSignal: "SIGINT", forceKillAfter: "2 seconds" })
}).pipe(Effect.scoped)
```

### `stdin`

A `Sink<void, Uint8Array, never, PlatformError>`. Run a byte stream into it to
write to the process's standard input.

```ts
import { Effect, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
  const handle = yield* spawner.spawn(ChildProcess.make`cat`)
  yield* Stream.make("hi\n").pipe(Stream.encodeText, Stream.run(handle.stdin))
}).pipe(Effect.scoped)
```

### `stdout`

A `Stream<Uint8Array, PlatformError>` of the process's standard output.

```ts
import { Stream } from "effect"
// Stream.runCollect(handle.stdout) // => Chunk<Uint8Array>
Stream
```

### `stderr`

A `Stream<Uint8Array, PlatformError>` of the process's standard error.

```ts
import { Stream } from "effect"
// handle.stderr.pipe(Stream.decodeText, Stream.runForEach(Effect.log))
Stream
```

### `all`

A `Stream<Uint8Array, PlatformError>` that interleaves stdout and stderr. Avoid
combining it with reading `stdout`/`stderr` separately, which can interleave
unexpectedly.

```ts
import { Stream } from "effect"
// handle.all.pipe(Stream.decodeText, Stream.splitLines)
Stream
```

### `getInputFd`

Returns an input `Sink` for an additional file descriptor configured via
`additionalFds`. Unconfigured fds return a draining sink.

```ts
import { Effect, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
  const handle = yield* spawner.spawn(
    ChildProcess.make("prog", [], { additionalFds: { fd3: { type: "input" } } })
  )
  yield* Stream.make("data").pipe(
    Stream.encodeText,
    Stream.run(handle.getInputFd(3))
  )
}).pipe(Effect.scoped)
```

### `getOutputFd`

Returns an output `Stream` for an additional output file descriptor.
Unconfigured fds return an empty stream.

```ts
import { Effect, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
  const handle = yield* spawner.spawn(
    ChildProcess.make("prog", [], { additionalFds: { fd3: { type: "output" } } })
  )
  return yield* Stream.runCollect(handle.getOutputFd(3)) // => Chunk<Uint8Array>
}).pipe(Effect.scoped)
```

### `unref`

Removes the child from the parent's reference count so the parent can exit
without waiting. Yields a `Reref` effect that re-references the child when run.

```ts
import { Effect } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"

Effect.gen(function*() {
  const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
  const handle = yield* spawner.spawn(ChildProcess.make`./server`)
  const reref = yield* handle.unref // parent no longer blocked by child
  yield* Effect.sleep("1 second")
  yield* reref // restore default behavior
}).pipe(Effect.scoped)
```

## API reference: branded values & lower-level helpers

### `ExitCode`

A `Brand.Branded<number, "ExitCode">`. Construct or compare with
`ChildProcessSpawner.ExitCode(n)`.

```ts
import { ChildProcessSpawner } from "effect/unstable/process"

ChildProcessSpawner.ExitCode(0) // => branded 0 (success)
```

### `ProcessId`

A `Brand.Branded<number, "ProcessId">` representing an OS process id, exposed as
`handle.pid`. Construct with `ChildProcessSpawner.ProcessId(n)`.

```ts
import { ChildProcessSpawner } from "effect/unstable/process"

ChildProcessSpawner.ProcessId(12345) // => branded process id
```

### `Reref`

An `Effect<void, PlatformError>` returned by `handle.unref`; running it
re-adds the child to the parent's reference count.

### `makeHandle`

Constructs a `ChildProcessHandle` from its fields. Used when implementing a
custom platform spawner, not in application code.

### `ChildProcessSpawner.make`

Builds a `ChildProcessSpawner` service from a single `spawn` implementation,
deriving `exitCode`, `string`, `lines`, `streamString`, and `streamLines`
automatically. This is how the Node and Bun backends are implemented.

```ts
import { ChildProcessSpawner } from "effect/unstable/process"

// const service = ChildProcessSpawner.make((command) => spawnImpl(command))
ChildProcessSpawner.make // => derives the convenience helpers from `spawn`
```