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 afterdeno runand 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}
- Measurements:
[:denox, :run, :stop]— emitted when the runtime exits- Measurements:
%{system_time: integer} Metadata:
%{package: string | nil, file: string | nil, exit_status: integer, backend: :cli}
- Measurements:
[:denox, :run, :recv]— emitted for each stdout line received- Measurements:
%{system_time: integer} - Metadata:
%{line_bytes: integer, backend: :cli}
- Measurements:
Summary
Functions
Check if the runtime is still running.
Run a script and capture all stdout output.
Returns a specification to start this module under a supervisor.
Return the OS PID of the process, if available.
Receive the next line from stdout.
Send data to stdin of the running process.
Send data to stdin and wait for the next stdout line.
Start a managed Deno runtime.
Stop the runtime.
Stream stdout lines from a script as a lazy Stream.
Stream stdout lines from an already-running server as a lazy Stream.
Subscribe the calling process to stdout messages.
Unsubscribe from stdout messages.
Execute a function with a managed runtime, ensuring cleanup on exit.
Types
Functions
@spec alive?(GenServer.server()) :: boolean()
Check if the runtime is still running.
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", ...]
Returns a specification to start this module under a supervisor.
See Supervisor.
@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.
@spec recv( GenServer.server(), keyword() ) :: {:ok, String.t()} | {:error, :timeout | :closed}
Receive the next line from stdout.
Options
:timeout- milliseconds to wait (default: 5000)
@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.
@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)
@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 (-Ain 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 bothallow_*anddeny_*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];0or omitted uses the default of1024lines.
@spec stop(GenServer.server()) :: :ok
Stop the runtime.
@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
If the runtime fails to start, the stream raises a RuntimeError
when enumerated. Use capture/1 if you prefer an {:error, reason}
tuple instead.
@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.
@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.
@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.
@spec with_runtime( keyword(), (GenServer.server() -> 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)