Skip to content

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).

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)
)
}

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:

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.

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.
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.

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:

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.

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:

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:

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)

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 Streams.
  • 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:

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}`))
)
})

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

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:

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}`))
)

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

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.

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 section.

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).

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.

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)

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

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

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

import { ChildProcess } from "effect/unstable/process"
const timed = ChildProcess.make`echo foo`.pipe(ChildProcess.prefix`time`)
// => runs: time echo foo

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

import { ChildProcess } from "effect/unstable/process"
const cmd = ChildProcess.make`ls -la`.pipe(ChildProcess.setCwd("/tmp"))
// => command that runs `ls -la` in /tmp

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

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

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

import { ChildProcess } from "effect/unstable/process"
ChildProcess.isCommand(ChildProcess.make`ls`) // => true
ChildProcess.isCommand("ls") // => false

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

import { ChildProcess } from "effect/unstable/process"
ChildProcess.isStandardCommand(ChildProcess.make`ls`) // => true

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

import { ChildProcess } from "effect/unstable/process"
const piped = ChildProcess.make`ls`.pipe(
ChildProcess.pipeTo(ChildProcess.make`wc -l`)
)
ChildProcess.isPipedCommand(piped) // => true

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

import { ChildProcess } from "effect/unstable/process"
ChildProcess.parseFdName("fd3") // => 3
ChildProcess.parseFdName("fd1") // => undefined (must be >= 3)
ChildProcess.parseFdName("stdout") // => undefined

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

import { ChildProcess } from "effect/unstable/process"
ChildProcess.fdName(3) // => "fd3"

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

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

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

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

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"
})

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.

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).

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" }
})

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

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

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

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

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

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).

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)
}
})

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

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

TemplateExpression / TemplateExpressionItem

Section titled “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).

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

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.

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

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)

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

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)

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

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"

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

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", ...]

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

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))
)
})

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

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))
)
})

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

The branded operating-system ProcessId of the running process.

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)

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

import { ChildProcessSpawner } from "effect/unstable/process"
// handle.exitCode
// => yields ExitCode(0) on success
ChildProcessSpawner.ExitCode(0) // => the success code to compare against

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

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)

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

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)

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

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)

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

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

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

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

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

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

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

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)

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

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)

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.

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

Section titled “API reference: branded values & lower-level helpers”

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

import { ChildProcessSpawner } from "effect/unstable/process"
ChildProcessSpawner.ExitCode(0) // => branded 0 (success)

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

import { ChildProcessSpawner } from "effect/unstable/process"
ChildProcessSpawner.ProcessId(12345) // => branded process id

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

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

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.

import { ChildProcessSpawner } from "effect/unstable/process"
// const service = ChildProcessSpawner.make((command) => spawnImpl(command))
ChildProcessSpawner.make // => derives the convenience helpers from `spawn`