# `@m8t-stack/cli` — the `m8t` CLI

> 📖 Reference for the `m8t` command-line tool that manages m8t deployments.

## What it is

The `m8t` CLI manages a deployed m8t platform: identity (`whoami`, `status`, `doctor`), config (`config`, `switch`), team membership (`team`), channel bindings (`bind`), the gateway stack (`deploy`), hosted coders (`coder`), and brains (`brain`). It's published to npm, Homebrew, and Scoop.

## Why / how it fits

It's the operator's day-2 tool for the cloud platform — the successor to the old `deploy/setup.mjs`. It shares `@m8t-stack/api-contract` types with the gateway at build time and authenticates with your `az` session (`DefaultAzureCredential`).

## Install

```bash
# npm (any OS with Node 20+)
npm install -g @m8t-stack/cli

# Homebrew (macOS / Linux)
brew install m8t-labs/tap/m8t

# Scoop (Windows)
scoop bucket add m8t https://github.com/m8t-labs/scoop-bucket
scoop install m8t

# Verify
m8t version
```

The CLI bundles the shared workspace types into a single `dist/cli.js`, so the
npm install is self-contained (only the external runtime deps resolve from npm).
Publishing is automated by `.github/workflows/release-cli.yml` (OIDC trusted
publishing); see `publish/npm-trusted-publishing-setup.md`.

### Build from source (contributors)

```bash
pnpm --filter "@m8t-stack/cli" build   # produces dist/cli.js with shebang
cd apps/cli && pnpm pack               # produces m8t-stack-cli-<version>.tgz
npm install -g ./m8t-stack-cli-*.tgz
```

## Usage

```bash
m8t version                 # CLI version
m8t whoami                  # Identity + which gateway you're talking to (probes the backend)
m8t status                  # Full local snapshot: az identity, config.yaml, gateway cache, azd mode
m8t doctor                  # Health checks (az · config · tenant align · gateway · Foundry data-plane · model-quota) + fixes
m8t doctor --agent <name>   # + delivery-grant check: verifies the agent's MI holds Key Vault Secrets User
m8t open [webapp|foundry|portal]   # Open the deployed app / Foundry portal / resource group in a browser

# Local config (~/.m8t)
m8t config show                       # config.yaml (tenant/client/project) + the cached gateway
m8t config set <key> <value>          # set tenantId | clientId | projectEndpoint
m8t config reset                      # clear the gateway discovery cache
m8t switch --subscription <id|name>   # re-point local config at another deployment (snapshots first)
m8t switch <profile>                  # restore a saved profile;  m8t switch --list

# Team management
m8t team add <handle> --display "<name>" [--telegram <id>] [--slack <id>] [--teams <upn>]
m8t team list
m8t team show <handle>
m8t team add-identity <handle> [--telegram <id>] [--slack <id>] [--teams <upn>]
m8t team remove-identity <handle> --telegram|--slack|--teams
m8t team remove <handle>

# Deploy / update the gateway stack (replaces the old deploy/setup.mjs)
m8t deploy [--client-id <appId>] [--image-ref <ref>] [--location <region>] [--resource-group <name>]

# Brain management
m8t brain link <name> --repo <owner>/<repo> [--persona <path>] [--allow-non-reasoning] [--gateway-url <url>] [--legacy-github-mcp]
# --persona: override the recorded persona file (or use when no local agent yaml exists)
# --allow-non-reasoning: suppress the reasoning-model warning
# --gateway-url: gateway base URL (also M8T_GATEWAY_URL/config gatewayUrl)
# --legacy-github-mcp: explicit rollback to the broad GitHub MCP transport

# The desktop companions — Stacey and Ezra at the edge of the screen
m8t companion status      # installed? which version? is a newer one out?
m8t companion install     # take the released build, register for login, launch  (macOS · Windows)
m8t companion install --from <dir>   # a build staged from a clone instead
m8t companion repair      # reinstall over whatever is there now
m8t companion uninstall   # remove the app, the login item, and the records
```

`m8t bootstrap status --watch` installs the companions, so most people never run these. They are
their own software with their own version: each platform release names the companion build
it carries, and these commands fetch it, check it against the digest the release pinned,
and verify every file of what they wrote. See
[`guides/install/companion.md`](https://github.com/m8t-labs/ezra/blob/main/guides/install/companion.md).

### `--subscription <id>` flag

Available on all gateway-using commands (`whoami`, `config show`, `config reset`,
`team add`, `team list`, `team show`, `team add-identity`, `team remove-identity`,
`team remove`, `bind add`, `bind list`, `bind show`, `bind remove`, `bind cleanup`).
Pins gateway discovery to the named subscription, bypassing the active `az`
subscription. Useful when you manage multiple m8t deployments across
subscriptions or when your shell's active subscription differs from the deployment
target.

```bash
m8t team list --subscription <subscription-id-or-name>
```

## Local context & config — `status` / `doctor` / `switch`

The CLI reads two local files under `~/.m8t/`: **`config.yaml`** (your `tenantId`, `clientId`, and Foundry `projectEndpoint` — used by the web app + architect) and **`cli-config.yaml`** (the auto-discovered gateway URL cache).

- **`m8t status`** — a read-only snapshot of everything local: your signed-in `az` identity + active subscription/tenant, `config.yaml` (tenant / client / project), the gateway cache, the `azd` auth mode, and whether your `az` tenant matches `config.yaml` (tenant alignment). `--output json` for scripting.
- **`m8t doctor`** — runs health checks and prints a fix for each failure; exits non-zero if any check FAILs. Checks: `az` signed in · `config.yaml` valid · tenant alignment · gateway reachable (`/api/me`) · **Foundry data-plane reachable** (a 401/403 prints `m8t prereqs --fix` plus the exact `az role assignment create … "Foundry User"` command) · **Key Vault secrets readable** (the same gap one resource over — without it `m8t coder deploy` / `m8t azure-exec deploy` fail) · **model-quota check** (warns for any deployed model with 0 TPM quota in the region). Results stream as each check runs. Pass `--agent <name>` to add a targeted delivery-grant check: reads the agent's deployed env and verifies its managed identity holds `Key Vault Secrets User` on the configured vault.
- **`m8t config set <key> <value>`** — write `tenantId`, `clientId`, or `projectEndpoint` into `config.yaml` (validated). `m8t config show` now displays `config.yaml` alongside the gateway cache.
- **`m8t switch`** — re-point your local config at another deployment in one step. `--subscription <id|name>` discovers the gateway there and derives every `config.yaml` value from its container env (zero manual IDs); `<profile>` restores a saved profile; `--list` lists profiles. Every switch **snapshots the outgoing config to a profile first** (label it with `--as <name>`), so it's reversible — e.g. `m8t switch --subscription <new> --as work`, then later `m8t switch personal`.

## Open in the browser — `m8t open`

```bash
m8t open            # the deployed webapp (gateway URL) — default
m8t open foundry    # the Microsoft Foundry portal (ai.azure.com)
m8t open portal     # the resource group in the Azure portal
m8t open --print    # print the URL instead of launching a browser (pipe-friendly)
```

## Deploy / update the gateway — `m8t deploy`

Provisions (or updates) the gateway/webapp stack via Bicep — the successor to the old `node deploy/setup.mjs`. See [`deploy/README.md`](../../deploy/README.md) for the full flow.

```bash
# Bring-your-own app registration (required if you can't create Entra app regs, e.g. a directory guest):
m8t deploy --client-id <appId> --image-ref <acr-or-ghcr-ref> --location <region>
```

Key flags: `--client-id <appId>` (reuse an existing app reg + skip **all** Microsoft Graph writes), `--image-ref <ref>` (full image ref; default `ghcr.io/m8t-labs/m8t:latest` — pass an `*.azurecr.io/...` ref for ACR images), `--resource-group` (default `rg-m8t-stack`), `--location`, `--suffix`, `--foundry-endpoint`, `--foundry-resource-id`.

> ⚠️ `m8t deploy` is **declarative** — it makes the live stack match the Bicep template. If a deployment was hand-modified after provisioning (e.g. switched to a private-ACR image with a UserAssigned pull identity), re-running the bicep can revert those changes. **Preview with `az deployment group create --what-if` before applying against a live stack.**

## Binding management — `m8t bind`

Wire channel bots (Telegram, Slack, Teams) to Foundry workers. The binding
management API + CLI cover all three channels; a channel can only complete
a bind once its adapter (which delivers inbound messages from that
channel) is registered at gateway startup — Telegram ships built in, Slack
and Teams adapters are not registered yet.

```bash
# Create a binding (Telegram's adapter ships built in)
m8t bind add telegram \
  --worker cmo \
  --bot-token <from-botfather> \
  --slug cmo-tg \
  --bot-username "@startup_cmo_bot"

# List all bindings, optionally filtered
m8t bind list
m8t bind list --channel telegram
m8t bind list --status orphaned

# Inspect one binding (token NEVER returned — only the KV URI)
m8t bind show cmo-tg

# Remove a binding (cascade: unregister webhook → KV → conversations → row)
m8t bind remove cmo-tg
m8t bind remove cmo-tg --yes      # skip confirm

# Orphan-only cleanup (refuses to act on active bindings)
m8t bind cleanup <bindingId>
m8t bind cleanup --all-orphaned
```

> `--worker` is the **case-sensitive, lowercase** Foundry agent name as deployed (e.g. `cmo`). A wrong name isn't rejected at bind time — it surfaces later as an orphaned binding on the first message.

### Binding a channel with no registered adapter

`m8t bind add slack ...` (or `teams`) currently fails with:

```text
error: no adapter for channel 'slack'. The channel-specific adapter is not
       registered in this build.
```

This is correct — Slack and Teams adapters aren't registered yet. Once a
channel's adapter registers at gateway startup, binding creation for that
channel unlocks.

### Cascade-delete idempotency

If `m8t bind remove` fails mid-cascade, it prints which steps completed
and which step failed, and recommends re-running the command. Each step
is independently idempotent — a re-run resumes safely from the failure
point.

## Output modes

Pretty table or key-value block by default; pipe or redirect, and the CLI auto-switches to JSON:

```bash
m8t team list                   # pretty
m8t team list | jq              # auto-JSON
m8t team list --output json     # explicit JSON
m8t team list --output pretty   # force pretty even on pipe
```

## Exit codes

- `0` success
- `1` any failure (auth, network, validation, backend)
- `2` user-cancelled

Granular error info in stderr's structured JSON envelope.

## Prerequisites

- Node 20+
- `az login` against the customer's Azure tenant. CLI uses `DefaultAzureCredential` — no per-admin pre-registration.

## Troubleshooting

### `error: You are not signed in to Azure.`

Run `az login` first. The CLI uses your active `az` session to acquire a token for the gateway.

### `error: No m8t deployment found in subscription <name>.`

The deployment is in a different subscription. Switch with:

```bash
az account set --subscription <other-sub>
m8t config reset
m8t team list
```

### `error: Multiple m8t deployments found ...`

You have more than one gateway in this subscription. The CLI prompts you to pick one when running interactively (e.g. `m8t whoami`). The choice is cached at `~/.m8t/cli-config.yaml`. Or pass `--subscription <id>` to target a specific subscription and skip the picker entirely.

### `(gateway is starting up...)`

The deployed gateway is configured with `min-replicas=0` (scale-to-zero). First request after idle pays ~10–20s cold start. The CLI retries once on 502/503/504.

### `error: partial write — <channel-A> succeeded, <channel-B> failed.`

A multi-identity `m8t team add` partially succeeded. Run `m8t team show <handle>` to verify the current state, then `m8t team add-identity <handle> --<channel-B> <id>` to fill in the missing identity.

### CLI version vs gateway version drift

The CLI and the gateway share `@m8t-stack/api-contract` types at compile time (the CLI bundles them at build). If the gateway is on an older version and returns a response in an unknown shape, the CLI surfaces `BAD_RESPONSE` with a "this may be a gateway version mismatch" hint.
