# 09 — CLI helpers + diagnostic CLI + extension primitive

Exit-code constants + exception → exit-code translator +
subcommand registry. Plus the `Registry[F]` generic primitive
that's the shared shape behind every plugin point.

> **Read first:** [`library_common.md`](library_common.md),
> [`02_exceptions.md`](02_exceptions.md).

---

## Motivation

Every CLI rolls its own `if __name__ == "__main__":` ladder:
parse argv, dispatch to handler, catch typed exceptions, map
to exit codes, ship a crash report on the catch-all branch.
The shape is the same across consumers. `mgf.common.cli` ships
that shape so consumers focus on the actual subcommand
handlers, not the routing.

The `mgf-common` console-script itself uses the same primitive
(eat-your-own-dogfood). Consumers building `vmanager` or
`myapp` get the diagnostic surface (`<myapp> doctor`,
`<myapp> explain`, etc.) for free.

The generic [`Registry[F]`](../../PUBLIC_API.md#mgfcommonregistry)
in `mgf.common.registry` is the shared shape behind every
plugin point in the library (vault backends, crash sinks, log
handlers, redaction patterns, span processors, CLI subcommands).
Consumers can build their own registries on the same
primitive.

---

## Goal

```python
# myapp/cli.py
import sys

from mgf.common import bootstrap
from mgf.common.cli import (
    EXIT_SUCCESS,
    build_cli,
    dispatch,
    run_and_translate,
    subcommand,
)

@subcommand("greet", help="Greet someone")
def run_greet(args) -> int:
    print(f"hello, {args.name}")
    return EXIT_SUCCESS

def main() -> int:
    parser = build_cli(prog="myapp")
    return dispatch(parser.parse_args(), parser=parser)

if __name__ == "__main__":
    with bootstrap():
        sys.exit(run_and_translate(main, sys.argv[1:]))
```

That's a complete consumer CLI. The four `mgf-common` built-in
subcommands (`explain` / `validate` / `doctor` / `migrate`)
appear under your prog name automatically.

---

## Quick start

### Stable exit-code constants

```python
from mgf.common.cli import (
    EXIT_SUCCESS,            # 0
    EXIT_CONFIG_ERROR,       # 1
    EXIT_OPERATION_ERROR,    # 2
    EXIT_GUEST_ERROR,        # 3
    EXIT_ENVIRONMENT_ERROR,  # 4
    EXIT_PROVIDER_ERROR,     # 5
    EXIT_VAULT_ERROR,        # 6
    EXIT_UNKNOWN,            # 10
)
```

Stable across the 0.x window — any bump is a breaking change
(rule AP-04).

### Translate typed exceptions

```python
from mgf.common.cli import run_and_translate
from mgf.common.exceptions import AppConfigError

def main(argv: list[str]) -> int:
    if not config_path.exists():
        raise AppConfigError(f"config missing: {config_path}")
    return EXIT_SUCCESS

if __name__ == "__main__":
    sys.exit(run_and_translate(main, sys.argv[1:]))
    # AppConfigError → EXIT_CONFIG_ERROR (1)
```

`run_and_translate` walks the exception's MRO, maps to the
right `EXIT_*` code, and ships untyped exceptions through
`report_now` before returning `EXIT_UNKNOWN`. NEVER raises.

### Register a custom subcommand

```python
from mgf.common.cli import subcommand, EXIT_SUCCESS

def _setup(p):
    p.add_argument("--name", required=True)

@subcommand("greet", help="Greet someone", parser_setup=_setup)
def run_greet(args) -> int:
    print(f"hello, {args.name}")
    return EXIT_SUCCESS
```

`subcommand` is a short alias for `register_cli_subcommand`
(both are the SAME function — ). Call form also
supported:

```python
register_cli_subcommand(
    "greet",
    handler=run_greet,
    help="Greet someone",
    parser_setup=_setup,
)
```

### Build a consumer top-level CLI

```python
from mgf.common.cli import build_cli, dispatch

parser = build_cli(prog="myapp")
args = parser.parse_args()
return dispatch(args, parser=parser)
```

`build_cli(include_builtins=False)` exposes only
consumer-registered subcommands.

---

## Public API

### Exit codes (numeric pin per AP-04)

[`EXIT_SUCCESS`](../../PUBLIC_API.md#mgfcommoncli) (0),
[`EXIT_CONFIG_ERROR`](../../PUBLIC_API.md#mgfcommoncli) (1),
[`EXIT_OPERATION_ERROR`](../../PUBLIC_API.md#mgfcommoncli) (2),
[`EXIT_GUEST_ERROR`](../../PUBLIC_API.md#mgfcommoncli) (3),
[`EXIT_ENVIRONMENT_ERROR`](../../PUBLIC_API.md#mgfcommoncli) (4),
[`EXIT_PROVIDER_ERROR`](../../PUBLIC_API.md#mgfcommoncli) (5),
[`EXIT_VAULT_ERROR`](../../PUBLIC_API.md#mgfcommoncli) (6),
[`EXIT_UNKNOWN`](../../PUBLIC_API.md#mgfcommoncli) (10).

### Translator

| Name | Purpose |
|---|---|
| [`run_and_translate(main_fn, argv, *, exit_code_map=None, crash_source="cli")`](../../PUBLIC_API.md#mgfcommoncli) | Run + translate. Returns int; never raises. |
| [`translate_exceptions(*, exit_code_map=None, crash_source="cli")`](../../PUBLIC_API.md#mgfcommoncli) | Decorator form. |
| [`main_entry(main_fn, *, exit_code_map=None, crash_source="cli")`](../../PUBLIC_API.md#mgfcommoncli) | One-liner that also calls `sys.exit`. |

### Subcommand registry

| Name | Purpose |
|---|---|
| [`register_cli_subcommand(name, *, handler, help, parser_setup=None)`](../../PUBLIC_API.md#mgfcommoncli) | Register a subcommand. Decorator + plain-call forms. |
| [`subcommand`](../../PUBLIC_API.md#mgfcommoncli) | Short alias (same function as `register_cli_subcommand`). |
| [`unregister_cli_subcommand(name)`](../../PUBLIC_API.md#mgfcommoncli) | No-op if unknown. |
| [`CliSubcommand`](../../PUBLIC_API.md#mgfcommoncli) | Frozen dataclass record. |
| [`build_cli(prog="mgf-common", *, description=None, include_builtins=True)`](../../PUBLIC_API.md#mgfcommoncli) | Build an `ArgumentParser` from the registry. |
| [`dispatch(args, *, parser=None)`](../../PUBLIC_API.md#mgfcommoncli) | Run the registered handler for `args.command`. |

### Settings provenance

| Name | Purpose |
|---|---|
| [`field_provenance(settings, *, init_overrides=None, file_overrides=None)`](../../PUBLIC_API.md#mgfcommoncli) | Per-field source attribution. The shim that powers `mgf-common explain`. |
| [`env_vars_known_to_mgf()`](../../PUBLIC_API.md#mgfcommoncli) | Sorted list of every env var `MgfSettings` consults. |
| [`FieldSource`](../../PUBLIC_API.md#mgfcommoncli) / [`SourceKind`](../../PUBLIC_API.md#mgfcommoncli) | Frozen dataclass + `Literal` for the provenance result. |

Code:
[`src/mgf/common/cli/`](../../src/mgf/common/cli/),
[`_subcommand_registry.py`](../../src/mgf/common/cli/_subcommand_registry.py),
[`_handler.py`](../../src/mgf/common/cli/_handler.py),
[`_provenance.py`](../../src/mgf/common/cli/_provenance.py).

---

## The diagnostic `mgf-common` CLI

Four built-in subcommands (also reachable from any consumer
CLI built with `build_cli(include_builtins=True)`):

| Subcommand | Purpose |
|---|---|
| `mgf-common explain` | Print active `MgfSettings` with each field's source (default / preset / env / pyproject / init). Secret-shaped fields redacted. |
| `mgf-common validate <config.yaml>` | Schema-validate a YAML file against `MgfSettings`. Exits 1 on schema error, 2 on file error. |
| `mgf-common doctor` | Install-state + wiring health checks. ✓ pass / ⚠ heads-up / ✗ failure. Exits 0 unless any ✗. |
| `mgf-common migrate <from> <to> <path>` | Mechanical-rename codemod for → upgrades (`EnvironmentError` → `HostEnvironmentError`, `make_settings_config` → `settings_config`). |

---

## The generic `Registry[F]` primitive

`mgf.common.registry` factors out the pattern shared across the
six hand-rolled registries (vault backends, crash sinks, log
handlers, redaction patterns, span processors, CLI
subcommands). Consumers can build their own registries on the
same primitive.

```python
from collections.abc import Callable
from typing import Any
from mgf.common.registry import Registry, make_registry

# Define the factory signature
HypervisorFactory = Callable[[dict[str, Any]], "HypervisorBackend"]

# Make the registry
HYPERVISORS: Registry[HypervisorFactory] = make_registry("hypervisor")

# Register impls
@HYPERVISORS.register("libvirt")
def _make_libvirt(config: dict[str, Any]) -> HypervisorBackend:
    ...

@HYPERVISORS.register("qemu")
def _make_qemu(config: dict[str, Any]) -> HypervisorBackend:
    ...

# Look up + dispatch
factory = HYPERVISORS.get(name)
if factory is None:
    raise ResourceNotFoundError(name)
hyp = factory(config)
```

Public methods on `Registry[F]`: `register(key)` decorator,
`unregister(key)` no-op, `get(key) -> F | None`, `snapshot()`
shallow copy, `__iter__`, `__contains__`, `__len__`,
`__repr__`. The six existing hand-rolled registries are NOT
migrated to use this — they work and migration carries blast
radius for marginal reward; the primitive is the shape for
FUTURE registries.

Code:
[`src/mgf/common/registry.py`](../../src/mgf/common/registry.py).

---

## Patterns

### Custom exit-code map

When a CLI wants `CancellationError → 130` (Unix
128+SIGINT convention):

```python
from mgf.common.cli import run_and_translate, EXIT_OPERATION_ERROR
from mgf.common.exceptions import CancellationError

rc = run_and_translate(
    main,
    argv,
    exit_code_map={CancellationError: 130},
)
```

The library deliberately doesn't make 130 the default —
`CancellationError` is `OperationError` subclass; consumers
that don't care about the distinction get exit code 2.

### Consumer top-level CLI shape

```python
# myapp/cli.py
import sys
from mgf.common import bootstrap
from mgf.common.cli import build_cli, dispatch, run_and_translate

# ... register your subcommands ...

def main(argv: list[str] | None = None) -> int:
    parser = build_cli(prog="myapp", description="My App CLI.")
    args = parser.parse_args(argv if argv is not None else sys.argv[1:])
    with bootstrap():
        return dispatch(args, parser=parser)

if __name__ == "__main__":
    sys.exit(run_and_translate(main, sys.argv[1:]))
```

The reference consumer
[`example_app/src/example_app/cli.py`](../../example_app/src/example_app/cli.py)
ships exactly this shape.

---

## Links

- **Public API:** [`PUBLIC_API.md` `mgf.common.cli` section](../../PUBLIC_API.md#mgfcommoncli),
  [`mgf.common.registry` section](../../PUBLIC_API.md#mgfcommonregistry)
- **Engineering standards:**
  [`API_DESIGN.md`](../standards/API_DESIGN.md) AP-04 (deprecation cycle), AP-10 (PUBLIC_API.md sync)
- **Reference consumer:**
  [`example_app/`](../../example_app/) — full CLI integration
- **Code:**
  [`src/mgf/common/cli/`](../../src/mgf/common/cli/),
  [`registry.py`](../../src/mgf/common/registry.py)
- **Related docs:**
  - [`02_exceptions.md`](02_exceptions.md) — every category
    base maps to a stable exit code
  - [`08_progress.md`](08_progress.md) — `CancellationError`
    handling in CLIs
  - [`04_settings.md`](04_settings.md) — `mgf-common explain`
    shows resolved `MgfSettings`
