# `Denox.CLI.Run`
[🔗](https://github.com/gsmlg-dev/denox/blob/v0.9.0/lib/denox/cli/run.ex#L1)

Run Deno programs as managed subprocesses using the bundled CLI.

Same API as `Denox.Run`, but spawns the bundled `deno` binary from
`Denox.CLI` as a subprocess instead of using the in-process NIF runtime.
The primary use case is **npm: and jsr: packages**, which require Deno's
full Node.js-compatible resolver and are not supported by the NIF backend.
Also useful when Deno CLI features such as `--unstable-kv` are needed.

## Additional Options

All options from `Denox.Run` are supported. Additionally:

  - `:deno_flags` - extra flags inserted after `deno run` and before the
    specifier (e.g. `["--no-check", "--unstable-kv"]`)

## Examples

    {:ok, pid} = Denox.CLI.Run.start_link(
      package: "@modelcontextprotocol/server-github",
      permissions: :all,
      env: %{"GITHUB_PERSONAL_ACCESS_TOKEN" => token}
    )

    :ok = Denox.CLI.Run.send(pid, data)
    {:ok, line} = Denox.CLI.Run.recv(pid, timeout: 5000)

## Convenience Functions

All convenience helpers from `Denox.Run.Base` are available:

    # One-shot capture
    {:ok, lines} = Denox.CLI.Run.capture(
      package: "@modelcontextprotocol/server-github",
      permissions: :all,
      env: %{"GITHUB_PERSONAL_ACCESS_TOKEN" => token}
    )

    # Bracket-style with automatic cleanup
    Denox.CLI.Run.with_runtime(
      [package: "npm:cowsay", permissions: :all],
      fn pid ->
        :ok = Denox.CLI.Run.send(pid, "hello")
        {:ok, line} = Denox.CLI.Run.recv(pid, timeout: 5000)
        line
      end
    )

## Telemetry Events

Denox.CLI.Run emits the following telemetry events:

  * `[:denox, :run, :start]` — emitted when the runtime starts
    * Measurements: `%{system_time: integer}`
    * Metadata: `%{package: string | nil, file: string | nil, backend: :cli}`

  * `[:denox, :run, :stop]` — emitted when the runtime exits
    * Measurements: `%{system_time: integer}`
    * Metadata: `%{package: string | nil, file: string | nil, exit_status: integer, backend: :cli}`

  * `[:denox, :run, :recv]` — emitted for each stdout line received
    * Measurements: `%{system_time: integer}`
    * Metadata: `%{line_bytes: integer, backend: :cli}`

# `t`

```elixir
@type t() :: %Denox.CLI.Run{
  backend_state: term(),
  exit_status: term(),
  file: term(),
  package: term(),
  recv_waiters: term(),
  stdout_buffer: term(),
  subscribers: term()
}
```

# `alive?`

```elixir
@spec alive?(GenServer.server()) :: boolean()
```

Check if the runtime is still running.

# `capture`

```elixir
@spec capture(keyword()) :: {:ok, [String.t()]} | {:error, term()}
```

Run a script and capture all stdout output.

Starts a managed runtime, collects all output lines until the script
exits or the per-line timeout is reached, then stops the runtime.

Returns `{:ok, lines}` where `lines` is a list of stdout lines in order,
or `{:error, reason}` if the runtime failed to start.

## Options

Same as `start_link/1`, plus:

  - `:timeout` - per-line receive timeout in milliseconds (default: `30_000`).
    If no new line arrives within this window, collection stops and the
    lines collected so far are returned.

## Example

    {:ok, lines} = Denox.Run.capture(
      file: "scripts/generate.ts",
      permissions: :all
    )
    # lines => ["line 1", "line 2", ...]

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `os_pid`

```elixir
@spec os_pid(GenServer.server()) ::
  {:ok, non_neg_integer()} | {:error, :not_available | :not_running}
```

Return the OS PID of the process, if available.

Returns `{:ok, pid}` for CLI-backed runtimes or `{:error, :not_available}`
for NIF-backed runtimes where no separate OS process exists.

# `recv`

```elixir
@spec recv(
  GenServer.server(),
  keyword()
) :: {:ok, String.t()} | {:error, :timeout | :closed}
```

Receive the next line from stdout.

## Options
  - `:timeout` - milliseconds to wait (default: 5000)

# `send`

```elixir
@spec send(GenServer.server(), String.t()) :: :ok | {:error, term()}
```

Send data to stdin of the running process.

A newline (`\n`) is automatically appended if `data` does not
already end with one.

Returns `:ok` on success, or `{:error, :closed}` if the process
has already exited.

# `send_and_recv`

```elixir
@spec send_and_recv(GenServer.server(), String.t(), keyword()) ::
  {:ok, String.t()} | {:error, term()}
```

Send data to stdin and wait for the next stdout line.

A convenience wrapper around `send/2` + `recv/2` for the common
request-response pattern (e.g. JSON-RPC over stdio, MCP servers).

Returns `{:ok, response_line}` or `{:error, reason}` where reason is
`:timeout`, `:closed`, or the send error.

## Options

  - `:timeout` - milliseconds to wait for a response (default: 5000)

## Example

    request = Jason.encode!(%{jsonrpc: "2.0", method: "ping", id: 1})
    {:ok, response_line} = Denox.Run.send_and_recv(pid, request, timeout: 5000)
    response = Jason.decode!(response_line)

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

Start a managed Deno runtime.

## Options

  - `:package` - JSR/npm package specifier
  - `:file` - local file path to run
  - `:permissions` - permission mode; defaults to `:none` (deny all) when omitted:
    - `:all` — allow all permissions (`-A` in Deno CLI)
    - `:none` — deny all permissions (Deno's default behaviour)
    - keyword list — granular permissions, e.g. `[allow_net: true, allow_read: ["/tmp"],
      deny_env: true]`. Supports both `allow_*` and `deny_*` keys.
  - `:env` - map of environment variables
  - `:args` - extra arguments after the specifier
  - `:name` - GenServer name for registration
  - `:buffer_size` - (NIF backend only) stdout channel capacity in lines;
    controls how many lines the NIF can buffer before applying backpressure.
    Range `[1, 100_000]`; `0` or omitted uses the default of `1024` lines.

# `stop`

```elixir
@spec stop(GenServer.server()) :: :ok
```

Stop the runtime.

# `stream`

```elixir
@spec stream(keyword()) :: Enumerable.t()
```

Stream stdout lines from a script as a lazy `Stream`.

Starts a managed runtime and returns an `Enumerable` that yields
stdout lines one at a time. The runtime is automatically stopped
when the stream is fully consumed or halted (e.g. via `Stream.take/2`
or `Enum.take/2`).

This is useful for processing large outputs without buffering all
lines in memory, or for early termination once a condition is met.

## Options

Same as `start_link/1`, plus:

  - `:timeout` - per-line receive timeout in milliseconds (default: `30_000`).
    If no new line arrives within this window, the stream halts.

## Example

    # Collect the first 5 lines of a long-running script
    Denox.Run.stream(file: "server.ts", permissions: :all)
    |> Enum.take(5)

    # Filter lines matching a pattern
    Denox.Run.stream(file: "generate.ts", permissions: :all)
    |> Stream.filter(&String.contains?(&1, "ERROR"))
    |> Enum.to_list()

> #### Errors on start {: .warning}
>
> If the runtime fails to start, the stream raises a `RuntimeError`
> when enumerated. Use `capture/1` if you prefer an `{:error, reason}`
> tuple instead.

# `stream_from`

```elixir
@spec stream_from(
  GenServer.server(),
  keyword()
) :: Enumerable.t()
```

Stream stdout lines from an already-running server as a lazy `Stream`.

Unlike `stream/1`, which starts a new runtime, `stream_from/2` works
with an existing server PID. The stream halts when the runtime exits or
a per-line timeout is reached. The server is **not** stopped when the
stream halts — the caller retains ownership.

This pairs naturally with `with_runtime/2` when you want lazy output
enumeration after sending input:

    Denox.Run.with_runtime([file: "script.ts", permissions: :all], fn pid ->
      :ok = Denox.Run.send(pid, Jason.encode!(request))
      Denox.Run.stream_from(pid) |> Enum.to_list()
    end)

## Options

  - `:timeout` - per-line receive timeout in milliseconds (default: `5000`).
    If no new line arrives within this window, the stream halts.

# `subscribe`

```elixir
@spec subscribe(GenServer.server()) :: :ok
```

Subscribe the calling process to stdout messages.

After subscribing, the calling process will receive:

  - `{:denox_run_stdout, server_pid, line}` for each stdout line
  - `{:denox_run_exit, server_pid, exit_status}` when the process exits

If the subscribing process dies, it is automatically removed from the
subscriber list without needing an explicit `unsubscribe/1` call.

# `unsubscribe`

```elixir
@spec unsubscribe(GenServer.server()) :: :ok
```

Unsubscribe from stdout messages.

The calling process will stop receiving `{:denox_run_stdout, ...}` and
`{:denox_run_exit, ...}` messages from this server.

# `with_runtime`

```elixir
@spec with_runtime(
  keyword(),
  (GenServer.server() -&gt; result)
) :: result | {:error, term()}
when result: term()
```

Execute a function with a managed runtime, ensuring cleanup on exit.

Starts a runtime with `opts`, calls `fun` with the server PID, then
stops the runtime — even if `fun` raises an exception.

Returns the value returned by `fun`. If `fun` returns an error tuple,
that tuple is passed through as-is. Returns `{:error, reason}` if the
runtime failed to start.

## Example

    Denox.Run.with_runtime([file: "scripts/tool.ts", permissions: :all], fn pid ->
      :ok = Denox.Run.send(pid, Jason.encode!(%{method: "init"}))
      {:ok, line} = Denox.Run.recv(pid, timeout: 10_000)
      Jason.decode!(line)
    end)

---

*Consult [api-reference.md](api-reference.md) for complete listing*
