# `izri` — Command-line interface

Single-binary CLI for [Izri](https://github.com/legendify-dev/izri). Talks to
the Izri API over tRPC and is the foundation every other integration
(GitHub Action, VS Code, JetBrains) shells out to.

## Install

```bash
# One-shot run (no install)
npx @izri/cli@latest --version

# Global install
npm install -g @izri/cli
izri --version
```

The CLI requires **Node.js 18 or newer**.

## Configure

The CLI reads two environment variables:

| Variable          | Required | Example                          |
| ----------------- | -------- | -------------------------------- |
| `IZRI_API_URL`    | yes      | `https://api.izri.example.com`   |
| `IZRI_API_TOKEN`  | yes      | `izri_xxxxxxxxxxxxxxxx`          |
| `IZRI_PROJECT_ID` | no       | `01HK0...` (ULID)                |

Generate the token from the dashboard. The CLI rejects any token that doesn't
start with `izri_` — the API short-circuits to bearer-token validation only
for that prefix, so anything else would just produce an opaque 401 at request
time.

`IZRI_PROJECT_ID` is optional: when unset and you're running inside a git
checkout with a GitHub `origin` remote, the CLI auto-resolves the project by
matching `owner/repo` against `listAccessibleProjects` (same comparison the
GitHub-App webhook uses). Pass `--project <id>` to override.

## Commands

### `izri run`

Trigger a manual test run for the current repo and (optionally) wait for it
to finish.

```bash
izri run                              # fire-and-forget
izri run --watch                      # poll until terminal
izri run --project 01HK...            # explicit project
```

Resolution order: `--project <id>` → `IZRI_PROJECT_ID` → auto-match by origin
remote. The current `HEAD` SHA and branch are sent along automatically.

`--watch` polls every 3 seconds until the run reaches one of
`PASSED`/`FAILED`/`ERROR`/`CANCELLED`, with a 30-minute hard cap. The CLI
exits **non-zero on any terminal status except `PASSED`** so CI scripts can
branch on it.

### `izri status`

Print the latest delta report for the current `HEAD`.

```bash
izri status              # formatted summary
izri status --json       # raw JSON for piping
```

The output renders the umbrella verdict (`izri/quality`) and each per-signal
child (`izri/scope`, `izri/tests`, `izri/hallucination`, `izri/visual`),
including hard-findings count and confidence.

Exits non-zero when no report exists yet for the current `HEAD` — usually
because the runner hasn't finished. Re-run after `izri run --watch`.

### `izri check-scope`

Run the scope analyzer on the local diff against a base ref.

```bash
izri check-scope                      # vs origin/main, deterministic only
izri check-scope --base origin/dev    # different base
izri check-scope --semantic           # opt into the LLM layer (tier-gated)
izri check-scope --json               # raw JSON
```

The CLI builds the `files[]` payload from `git diff --name-status -M` +
`git diff --numstat -M`, merges on the post-rename filename, and sends it to
`analysis.checkScope`. The procedure caps `files[]` at 500; the CLI
pre-flights that and tells you to narrow `--base` instead of returning a
Zod error.

Exits non-zero only when there's at least one **`error`-severity** finding.
Warnings and info-level nudges stay at exit `0`.

## Exit codes

All commands honour the same convention:

| Exit | Meaning                                                                |
| ---- | ---------------------------------------------------------------------- |
| `0`  | Success (run passed, scope clean, status fetched).                     |
| `1`  | Hard failure: missing env, run not passed, error findings, no report.  |

This lets you gate any pipeline with `izri run --watch && deploy.sh` or
`izri check-scope || exit 1` without parsing output.

## Use in CI

GitHub Actions example:

```yaml
- name: Izri scope check
  env:
    IZRI_API_URL:   ${{ vars.IZRI_API_URL }}
    IZRI_API_TOKEN: ${{ secrets.IZRI_API_TOKEN }}
  run: |
    npx @izri/cli@latest check-scope --base origin/${{ github.base_ref }}

- name: Izri test run
  env:
    IZRI_API_URL:   ${{ vars.IZRI_API_URL }}
    IZRI_API_TOKEN: ${{ secrets.IZRI_API_TOKEN }}
  run: |
    npx @izri/cli@latest run --watch
```

The GitHub Action (`legendify-dev/izri-action@v1`, issue #239) wraps these
calls so you don't have to manage the `npx` boilerplate yourself.

## Troubleshooting

**`IZRI_API_URL is required` or `IZRI_API_TOKEN is required`**
Set both env vars before invoking the CLI. Most shells pick them up from
`.env` files automatically when paired with `direnv` or `dotenvx`.

**`IZRI_API_TOKEN must start with `izri_``**
You're passing a token from a different system (e.g. a GitHub PAT). Generate
an Izri-native token from the dashboard.

**`No accessible project matches "owner/repo"`**
Either connect the repo via the dashboard first, or pass `--project <id>`.
Run `izri --help` to confirm the binary version, and verify your token's
organization has the project linked.

**`Multiple projects match "owner/repo"`**
Two or more projects in your org point at the same repository. The CLI lists
their IDs; pass `--project <id>` to disambiguate. (The GitHub-App webhook
silently picks oldest in this case; the CLI is interactive so it asks.)

**`Diff against origin/main contains 1234 files — the analyzer caps at 500.`**
Choose a closer base with `--base origin/<branch>` or `--base HEAD~50`. The
500-file cap matches the server's hard limit.

**`git is not installed or not on PATH`**
Install git. The CLI shells out for diff + ref resolution; there is no
pure-JS fallback.

**`izri run` against a workspace-heavy local repo fails with `SUT exited
with code 1 before ready`**
The runner clones the repo into a fresh tmpdir per run. That tmpdir has
no `node_modules`, no built `packages/*/lib/` outputs, and no
`.env.shared` (which is `.gitignore`-d). If the project's
`.izri/izri.yml → runtime.start` calls `pnpm dev:*`, `pnpm db:migrate`,
or any step that depends on a built workspace, it will exit immediately
before the ready probe fires.

This is the **pre-warm contract** (#314): bring-up work like
`pnpm install`, `pnpm build:packages`, and `pnpm env:generate` must
happen before the runner spawns the SUT. Three options, in increasing
order of effort:

1. **Hosted runner** — the runner image bakes in pre-built dependencies
   and a pre-warmed `.env.shared`. No work on the project side; the
   default deploy path. This is what staging / prod do today.

2. **Local laptop runner** — run `pnpm install && pnpm build:packages
   && pnpm env:generate` once in the repo before `izri run`. The runner
   reuses the repo's existing `node_modules` and `packages/*/lib/`
   (only the .env.shared rewrite + `db:migrate` happen inside the
   tmpdir clone). See `deploy/README.md → "Staging runner: runs on the
   laptop"`.

3. **`izri check-scope` only** — for the *"I just want to see what
   changed"* use case, `izri check-scope` runs the deterministic scope
   analyzer against the local diff without any SUT bring-up. No
   pre-warm required.

If `.izri/izri.yml` does declare a non-trivial `start:`, document the
pre-warm steps in the repo's README so first-time contributors aren't
stuck on the cryptic exit-code-1.

## Where the CLI fits

The CLI is the **foundation of the integration ladder** (issue #238). The
GitHub Action (#239), VS Code extension (#248), and JetBrains plugin (#249)
all shell out to `izri`. The MCP server (`@izri/mcp`) is a sibling — same
auth shape, same `IZRI_API_*` env vars, but speaks Model Context Protocol
instead of taking shell arguments.

## Source

[`apps/cli/`](../apps/cli/) in the `legendify-dev/izri` monorepo. Bug reports
and contributions welcome on [GitHub](https://github.com/legendify-dev/izri/issues).
