# `Macaw.Host.Ctl`
[🔗](https://github.com/dcoai/macaw_host/blob/v0.1.4/lib/macaw/host/ctl.ex#L1)

Generates an app's host control script, `<app>-ctl`, so the apps of the
family stop hand-maintaining thousands of lines of the same script.

The script follows the deployable-containers pattern: a single Elixir
file, standard library only, with one line of host state (`@data_dir`,
in `Ctl.Home`, the first module), every docker argv built in `Ctl.Docker`,
and the host touched only through the swappable `Ctl.World`. It is built
from three parts:

- the **generic modules**, EEx templates in `priv/ctl/` compiled into this
  module: `Ctl.Home`, `Ctl.App`, `Ctl.World`, `Ctl.Script`, `Ctl.Secret`,
  `Ctl.Env`, `Ctl.Docker`, `Ctl.Commands` and `Ctl`;
- the **description** (`Macaw.Host.Ctl.Description`), from the app's
  `mix.exs`, which fills in everything app-specific;
- the app's **own commands**, a source file defining `Ctl.AppCommands`,
  spliced in as it is.

`generate/2` is pure, so it is tested without Mix; `mix macaw.host.ctl`
writes its result, and `--check` compares it with the committed file.

## The `Ctl.AppCommands` contract

The app's file holds one module, `Ctl.AppCommands`, and nothing beside
it: a module of its own goes inside it (`Ctl.AppCommands.Helpers`), and
code outside it, which would run every time the script loads, is
refused. The module has:

- `commands/0`: `[{name, usage, help}]`, the app's commands. `name` is
  what is typed (`"asterisk"`), `usage` how `help` shows it
  (`"asterisk [CLI COMMAND]"`), `help` one sentence. A name the script
  already has (`start`, `config` …) is never dispatched here.
- `run(name, env, args)`: runs one, returning what the common commands
  do: `:ok`, `{:error, message}` (printed, exit 1) or `{:halt, status}`.
  It is called only once the data directory is set.
- optionally `run_args(env)`: extra `docker run` arguments (more ports,
  say), placed before DOCKER_RUN_ARGS.

It builds its docker command lines with `Ctl.Docker` (`exec_args/3`,
`rpc_args/2`, `eval_args/3`, `release_args/4`), runs them with
`Ctl.Commands.stream/1` or `interactive/1`, checks the container with
`Ctl.Commands.ensure_running/1` or `ensure_ready/1`, and reads what it
manages from `Ctl.App`. A secret goes to the container by name, through
`eval_args/3` or `release_args/4`, never on a command line.

# `app_commands`

```elixir
@spec app_commands(String.t() | nil) :: {:ok, String.t()} | {:error, String.t()}
```

Checks the source of an app's commands: it parses, holds nothing but the
one `Ctl.AppCommands` module (no other module, no code outside it,
which would run whenever the script loads), and defines `commands/0`
and `run/3`, so it can neither replace a generic module nor carry a
second `@data_dir`. nil is the module of no commands.

# `build`

```elixir
@spec build(keyword() | Macaw.Host.Ctl.Description.t(), String.t() | nil) ::
  {:ok, String.t()} | {:error, String.t()}
```

As `generate/2`: `{:ok, script}` or `{:error, message}`.

# `compare`

```elixir
@spec compare(String.t(), String.t()) :: :ok | {:error, String.t()}
```

`:ok` when `committed` is exactly `generated`, else `{:error, message}`
naming the first line that differs, for `mix macaw.host.ctl --check`.

# `generate`

```elixir
@spec generate(keyword() | Macaw.Host.Ctl.Description.t(), String.t() | nil) ::
  String.t()
```

The script for `description` (a `:ctl` keyword list, or a map
`Macaw.Host.Ctl.Description.new/2` returned) and the source of the app's
`Ctl.AppCommands` (nil for none). Raises `ArgumentError` naming the fault
when either is invalid.

---

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