# `Milvex.Migration.CLI`

Layered, halt-free entry point for `mix milvex.migrate`.

Parses argv, validates flags, resolves modules / prefixes, computes plans
via `Milvex.Migration.Plan.diff/4`, optionally applies them via
`Milvex.Migration.Runner.apply/2`, and renders output via
`Milvex.Migration.Reporter.render/2`.

Returns `{exit_code, iodata}`. Never calls `System.halt/1`. The Mix task
wraps this and is responsible for halting.

## Exit code priority hierarchy

When multiple conditions could apply, the higher number wins (1 > 2 > 4 > 3 > 0):

  * 0 — clean
  * 1 — configuration / argv error
  * 2 — at least one impossible op present (plan mode); apply blocked by impossible
  * 3 — destructive ops present without `--allow-drop` (plan mode); skipped by runner (apply mode)
  * 4 — at least one RPC failure during apply; describe_collection error during plan computation

## Dependency injection

Callers may pass `fetch_config_fn` and `connect_fn` to override the defaults.
This is used in tests to avoid touching real Application env or live
processes.

# `connect_fn`

```elixir
@type connect_fn() :: (atom() | nil -&gt; {:ok, GenServer.server()} | {:error, term()})
```

# `fetch_config_fn`

```elixir
@type fetch_config_fn() :: (atom(), atom() -&gt; term())
```

# `opts`

```elixir
@type opts() :: %{
  mode: :plan | :apply,
  allow_drop: boolean(),
  manage_load: boolean(),
  modules: [String.t()],
  prefixes: [String.t()],
  format: :text | :json,
  connection: String.t() | nil
}
```

# `run`

```elixir
@spec run([String.t()], fetch_config_fn(), connect_fn()) :: {0..4, iodata()}
```

Parses `argv`, resolves the target modules/prefixes and connection, computes
migration plans, and runs them in either `--plan` or `--apply` mode.

Returns `{exit_code, output}` where `output` is iodata ready to write to
stdout. `fetch_config_fn` and `connect_fn` are injectable for testing.

---

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