# Elixir

Stream a CLI straight from your Phoenix or Plug app—no API required

Terminalwire streams a command-line app straight from your Phoenix or Plug server to your users' machines over a single WebSocket. Instead of building an API and shipping a separate client, you write your CLI in your app, calling your contexts, Ecto, and business logic directly, and it runs on the user's workstation with access to their terminal, files, and browser.

It fits the BEAM well: each connection is a supervised process and your CLI handler runs in its own task. The Elixir server is at feature parity with Rails and speaks the same wire protocol as the v2 Go client. It's open source at [terminalwire/elixir](https://github.com/terminalwire/elixir), published on [Hex](https://hex.pm/packages/terminalwire), with [docs on HexDocs](https://hexdocs.pm/terminalwire).

Each chapter below walks you through adding Terminalwire to an Elixir app and building command-line apps for your users.


## Getting Started

Terminalwire streams a command-line app straight from your Phoenix or Plug server to your users' machines over a single WebSocket. You write your CLI in your app, calling your contexts, Ecto, and business logic directly, and it runs on the user's workstation with access to their terminal, files, and browser. There's no API to build and no separate client to ship.

## Install

Add Terminalwire and a WebSocket adapter to your `mix.exs`:

```elixir
def deps do
  [
    {:terminalwire, "~> 0.1"},
    {:websock_adapter, "~> 0.5"}   # upgrades a Plug/Phoenix conn to a socket
  ]
end
```

Then fetch:

```sh
$ mix deps.get
```

`websock_adapter` is a separate dependency because Terminalwire only depends on the abstract [`WebSock`](https://hex.pm/packages/websock) interface, the common one spoken by Phoenix, Bandit, and Plug.Cowboy, so the same handler wires into all of them.

## Define your CLI

Define commands as functions with `Terminalwire.CLI`. Public functions are commands, their parameters are the command's arguments, and `@desc` is the help text — like Ruby's Thor.

```elixir
defmodule MyApp.CLI do
  use Terminalwire.CLI, name: "my-app"

  @desc "Greet someone by name"
  def hello(name) do
    puts("Hello, #{name}!")
  end

  @desc "Deploy to an environment"
  def deploy(env) do
    if String.trim(gets("Deploy to #{env}? [y/N] ")) == "y" do
      puts("Deploying #{env}…")
    else
      puts("Aborted")
    end
  end
end
```

Now `my-app hello Ada` runs `hello("Ada")`, `my-app deploy staging` runs `deploy("staging")`, and `my-app` (or `my-app help`) prints a generated command list. Inside a command, `puts`/`print`/`warn`/`gets`/`read_secret`/`env` talk to the user's terminal, and `context/0` reaches files, the browser, and the rest.

`Terminalwire.CLI` is a thin layer over a plain handler — a `run(ctx)` function taking a `Terminalwire.Server.Context`. Drop down to it when you want full control of parsing (flags, subcommands) with a library like [Optimus](https://hexdocs.pm/optimus); both use the same `Context`.

Either way, your code runs in its own BEAM process whose **group leader** is a Terminalwire IO device, so `IO.puts`, `IO.ANSI` colors, and any library that writes to standard IO (like [Owl](https://hexdocs.pm/owl)) stream straight to the user's terminal. The `Context` covers everything that isn't standard IO: args, prompts, the client's terminal, files, env, and the browser.

> **Never call `System.halt`** in your handler (or `Optimus.parse!`, which halts). It runs inside your server, so halting takes the whole server down. Return an integer exit code instead, and use `Context.warn/2` for stderr; writing to the `:stderr` device goes to the server's console, not the client's.

### The Context API

| | |
|---|---|
| **args** | `Context.args(ctx)` — the argv list you parse |
| **stdout** | `Context.puts/print` — or just `IO.puts` / `Owl.IO.puts` |
| **stderr** | `Context.warn(ctx, msg)` |
| **input** | `Context.gets(ctx, prompt)`, `Context.read_secret(ctx, prompt)` |
| **piped stdin** | `Context.read(ctx)`, `Context.read_chunk(ctx, n)` |
| **files** | `Context.file_read/file_write/file_append/file_delete/file_exists?` |
| **directories** | `Context.dir_list/dir_create/dir_delete/dir_exists?` |
| **env vars** | `Context.env(ctx, "NAME")` |
| **browser** | `Context.browser_launch(ctx, url)` |
| **terminal** | `Context.terminal(ctx)` — size, color, TTY flags |
| **raw input** | `Context.raw_input(ctx, mode, fn reader -> ... end)`, `Context.read_key(ctx, mode)` |

Files, env, and the browser are **gated by the client's entitlements**: your server *requests* access and the client enforces a per-app policy. The server never touches the user's machine on its own.

## Mount the endpoint

Upgrade your `/terminal` route to the ready-made `Terminalwire.WebSock` handler, passing your CLI as the `:handler`.

### Phoenix

```elixir
# lib/my_app_web/controllers/terminal_controller.ex
def show(conn, _params) do
  WebSockAdapter.upgrade(conn, Terminalwire.WebSock,
    [handler: &MyApp.CLI.run/1], timeout: :infinity)
end
```

```elixir
# lib/my_app_web/router.ex
get "/terminal", TerminalController, :show
```

### Plug / Bandit

```elixir
defmodule MyApp.Router do
  use Plug.Router
  plug :match
  plug :dispatch

  get "/terminal" do
    WebSockAdapter.upgrade(conn, Terminalwire.WebSock,
      [handler: &MyApp.CLI.run/1], timeout: :infinity)
  end

  match _, do: send_resp(conn, 404, "not found")
end
```

Use `timeout: :infinity` so a long-running command doesn't get its socket closed.

## Connect a client

In development, point a launcher stub at your server and run it like any other CLI. Create the file, make it executable, and go:

```sh
$ printf '#!/usr/bin/env terminalwire-exec\nurl: "ws://localhost:4000/terminal"\n' > my-app
$ chmod +x my-app
$ ./my-app hello Ada
Hello, Ada!
```

Phoenix's default development port is `4000`; a standalone Bandit example might use `8080`—match your server. If you don't have the Terminalwire client installed yet, see the [Client installation](../client/installation) guide.

## Feature parity

The Elixir server speaks the same wire protocol as the Ruby server and the Go client, verified by a shared conformance corpus. It's at parity with Rails: streaming stdout/stderr with flow control, live window resize, `Ctrl-C` interrupts (exit status 130), piped stdin (`cat data.csv | my-app import`), TTY and color detection, raw single-key input for REPLs and TUIs, files, env vars, browser launch, and per-app entitlements.
