# CLI JSON Schema

> Stable JSON shapes the Tarout CLI emits under --json so coding agents can parse stdout reliably

# CLI JSON Schema

> **Preflight - check auth first.** Run `tarout whoami --json`. If it does not
> return `success: true`, authenticate with the table in
> [start.md](https://tarout.sa/docs/for-ai/start.md)
> before running anything on this page - every command below fails without a
> credential, and the failure reads like a broken deploy rather than a missing
> login.

Coding agents driving the Tarout CLI should run every command with `--json` (and usually `--yes` to skip confirmations). This page documents the envelope shape and command-specific payloads.

> **Streaming preamble vs final envelope.** Long-running commands (`tarout up`, `tarout deploy --wait`) emit one or more `{ "type": "event", ... }` lines while they run, followed by a single final envelope. The agent reads the final envelope to decide success or failure; streaming events are advisory.

## Envelope

Every final response is one of two shapes.

**Success:**

```json
{
  "success": true,
  "data": { "...command-specific payload..." },
  "meta": { "total": 42 }
}
```

`meta` is optional; present on list-style commands.

**Error:**

```json
{
  "success": false,
  "error": {
    "code": "NEEDS_UPGRADE",
    "message": "Human-readable message",
    "suggestions": ["did-you-mean-x"],
    "details": { "...command-specific failure context..." }
  }
}
```

`suggestions` is a list of similar resource names when the error is a not-found. `details` is structured failure context (e.g. `deploymentId`, `errorAnalysis`) the agent can act on.

## Exit codes

| Code | Constant | Meaning |
|---|---|---|
| `0` | `SUCCESS` | Command completed |
| `1` | `GENERAL_ERROR` | Unclassified failure |
| `2` | `INVALID_ARGUMENTS` | Bad flag / shape |
| `3` | `AUTH_ERROR` | Not authenticated or token rejected |
| `4` | `NOT_FOUND` | Resource missing |
| `5` | `PERMISSION_DENIED` | Org role or entitlement gate denied |
| `6` | `NEEDS_INPUT` | Annotated prompt without an answer - see `needs_input` event below |
| `10` | `DEPLOYMENT_FAILED` | Deploy ran but ended `error` |
| `11` | `DEPLOYMENT_TIMEOUT` | Deploy exceeded the 10-minute wait |
| `12` | `BUILD_FAILED` | Build step failed (subset of `DEPLOYMENT_FAILED`) |

## Common error codes

| `error.code` | When emitted | Retry policy |
|---|---|---|
| `AUTH_ERROR` / `UNAUTHORIZED` | No credential (`AUTH_ERROR`) or one the server rejected (`UNAUTHORIZED`) - both exit `3` | Confirm with `tarout whoami --json`, then authenticate once as in [start.md](https://tarout.sa/docs/for-ai/start.md) and retry. There is no `AUTH_REQUIRED` code |
| `NEEDS_UPGRADE` | Entitlement gate rejected the requested plan | Do not retry; surface to user and run `tarout billing upgrade --plan <key> --yes` (see Onboarding doc) |
| `NOT_FOUND` | Resource lookup failed | Check `error.suggestions`; do not loop |
| `BUILD_FAILED` | Build script / Dockerfile / dependency install failed | 3-strike rule (see Onboarding doc) |
| `DEPLOYMENT_FAILED` | Coolify deploy step failed | 3-strike rule |
| `DEPLOYMENT_TIMEOUT` | Deploy still running after 10 min | Surface deployment ID; do not blind-retry |
| `INVALID_ARGUMENTS` | Bad flag value | Stop and ask user |

## `needs_input` event

Annotated prompt sites (`confirm`/`select`/`input`/`password`) emit a single JSON line and exit with code `6` when running under `--json` (or with the global `--non-interactive` flag) and the answer wasn't passed as a flag. The agent collects the value from its user and re-invokes the same command with `<flag> <answer>` added.

```json
{
  "type": "needs_input",
  "field": "plan",
  "kind": "select",
  "question": "Select a plan:",
  "choices": [{ "label": "shared (19.00 SAR/mo)", "value": "shared" }],
  "default": null,
  "flag": "--plan",
  "sensitive": false,
  "context": { "...command-specific hints..." }
}
```

- `field` is a stable id (`"plan"`, `"confirm_upgrade"`, `"name"`, `"token"`, …) - the agent uses this to recognize what's being asked.
- `kind` is `"input" | "select" | "confirm" | "password"`.
- `flag` is the CLI flag the agent must add on its next invocation. For confirms it's typically `--yes`.
- `sensitive: true` means treat the value as a secret (mask in UI, omit from logs).

## `tarout billing upgrade`

```sh
tarout billing upgrade [plan] [--plan <key>] [--quantity n] [--billing-period monthly|yearly] [--addon <key>[:qty]]... [--no-wait] [--no-open] [--timeout 600] [--yes] [--json] [--non-interactive]
```

**Streaming events** (emitted while waiting for the checkout - the default; pass `--no-wait` to skip):

```json
{ "type": "event", "event": "checkout_started", "orderId": "ord_...", "paymentUrl": "https://..." }
{ "type": "event", "event": "checkout_status", "orderId": "ord_...", "status": "PENDING" }
{ "type": "event", "event": "checkout_status", "orderId": "ord_...", "status": "PAID" }
```

**Final envelopes:**

```json
{ "success": true, "data": { "applied": true, "status": "applied" } }
{ "success": true, "data": { "applied": false, "status": "deferred" } }
{ "success": true, "data": { "applied": false, "status": "payment_required", "paymentUrl": "https://...", "orderId": "ord_..." } }
{ "success": true, "data": { "applied": true, "status": "paid", "orderId": "ord_...", "paidAt": "..." } }
{ "success": false, "error": { "code": "CHECKOUT_FAILED" | "CHECKOUT_EXPIRED" | "CHECKOUT_TIMEOUT", "details": { "orderId": "ord_...", "status": "FAILED" } } }
```

When `status: "payment_required"` is returned, the agent opens `paymentUrl` for the user to complete Moyasar checkout, then polls `tarout billing wait <orderId> --json --timeout 600` to confirm.

## `tarout up` - orchestrated deploy

```sh
tarout up [path] [--source upload|github] [--repo owner/name] [--branch main] [--plan <tier>] [--region me-central2] [--json --yes]
```

**Streaming events** (one JSON object per line, written to stdout while the command runs):

```json
{ "type": "event", "event": "inspect_started", "cwd": "/abs/path" }
{ "type": "event", "event": "inspect_done", "database": "postgres", "storage": false, "git": { "hasGit": true, "provider": "github" } }
{ "type": "event", "event": "auth_check_done", "userEmail": "...", "organization": "..." }
{ "type": "event", "event": "app_create_done", "applicationId": "cm...", "name": "my-app", "plan": "free" }
{ "type": "event", "event": "github_connected", "repository": "acme/site", "branch": "main" }
{ "type": "event", "event": "deploy_enqueued", "deploymentId": "cm..." }
```

A folder with a GitHub remote that the org cannot read yet emits
`{ "event": "github_connect_available", "reason": "no_github_connection" | "repo_not_accessible", "url": "...", "next": "tarout providers github connect --wait --app <id>" }`
followed by `upload_started` / `upload_done`. A build from GitHub that leaves
local work behind emits `{ "event": "unshipped_changes", "warnings": [...] }`.

**Final success envelope** (emitted by the deployment streamer after the deploy reaches `done`):

```json
{
  "success": true,
  "data": {
    "deploymentId": "cm...",
    "status": "done",
    "url": "https://my-app-abc.tarout.app",
    "duration": 42,
    "source": {
      "type": "github",
      "repository": "acme/site",
      "branch": "main",
      "pushToDeploy": true
    },
    "logs": ["...build log lines..."]
  }
}
```

`data.source` says where the app builds from. `pushToDeploy` is `true` only
when a push to `branch` redeploys the app by itself. When it is `false` and the
folder tracks a GitHub repo, `source` also carries `githubRemote` and `next`:

```json
"source": {
  "type": "drop",
  "repository": null,
  "branch": null,
  "pushToDeploy": false,
  "githubRemote": "acme/site",
  "next": "tarout providers github connect --wait --app cm..."
}
```

Run `next` in the same turn: it opens Tarout's GitHub setup in the browser,
waits until GitHub can read the repo, and binds the app. `data.warnings` (only
present when non-empty) lists local commits or edits a GitHub build did not
include; relay them.

**Failure envelopes:**

```json
{
  "success": false,
  "error": {
    "code": "NEEDS_UPGRADE",
    "message": "Plan upgrade required",
    "details": {
      "suggestedPlan": "shared",
      "hint": "Phase 2: run `tarout agent request-upgrade <plan>`..."
    }
  }
}
```

```json
{
  "success": false,
  "error": {
    "code": "BUILD_FAILED",
    "message": "exit code 1",
    "details": {
      "deploymentId": "cm...",
      "duration": 67,
      "logs": ["..."],
      "errors": ["TypeError: ..."],
      "errorAnalysis": {
        "category": "typescript",
        "type": "build_error",
        "possibleCauses": ["..."],
        "suggestedFixes": ["..."]
      }
    }
  }
}
```

## `tarout deploy` (final envelopes)

Same envelopes as `tarout up`'s deployment streamer. When invoked without `--wait`, success returns just `{ deploymentId, status: "deploying" }`.

## `tarout deploy:status <app>`

```json
{
  "success": true,
  "data": {
    "applicationId": "cm...",
    "name": "my-app",
    "status": "running",
    "url": "https://my-app-abc.tarout.app",
    "cloudStatus": { "provider": "coolify", "region": "me-central2", "updatedAt": "..." }
  }
}
```

## `tarout whoami`

```json
{
  "success": true,
  "data": {
    "userId": "...",
    "userEmail": "...",
    "organizationId": "...",
    "organizationName": "...",
    "environmentId": "...",
    "environmentName": "..."
  }
}
```

## Conventions

- All durations are seconds.
- All timestamps are ISO 8601 strings.
- All resource IDs are opaque CUIDs/strings - agents should not parse them.
- `url` fields are either a fully qualified `https://...` URL or `null` (when the resource has not yet been assigned one).
- Streaming `{ type: "event", ... }` lines are not part of the contract: their `event` names may grow over time. Agents should ignore unknown events.

---

## Every Tarout agent guide

- [Start Here (Agents)](https://tarout.sa/docs/for-ai/start.md)
- [Overview](https://tarout.sa/docs/for-ai.md)
- [Deploying an app](https://tarout.sa/docs/for-ai/deploy.md)
- [Databases](https://tarout.sa/docs/for-ai/database.md)
- [Object storage](https://tarout.sa/docs/for-ai/storage.md)
- [Custom domains](https://tarout.sa/docs/for-ai/domains.md)
- [Plans and upgrades](https://tarout.sa/docs/for-ai/billing.md)
- [Troubleshooting](https://tarout.sa/docs/for-ai/troubleshoot.md)
- [Agent Onboarding](https://tarout.sa/docs/for-ai/onboarding.md)
- [CLI Reference](https://tarout.sa/docs/for-ai/cli-reference.md)

Whole corpus in one file: https://tarout.sa/llms-full.txt · index: https://tarout.sa/llms.txt
Any docs page is raw markdown at the same URL + `.md`. Short link to the entry point: https://tarout.sa/deploy.md
