# Ausgabe als JSON und Exit-Codes

Was die CLI auf welche Ausgabe schreibt und welcher Exit-Code welche Fehlerart bedeutet — die Tabelle, gegen die ein Skript entscheidet, ohne Text zu vergleichen.

> **Typ:** nachschlagen
> **Für wen:** Wer die CLI aus Skripten aufruft und Ergebnis, Fehler und Wiederholung auseinanderhalten muss
> **Voraussetzung:** die CLI direkt über `node …/dist/main.js` oder als `szenera` aufgerufen — nie über `pnpm run`, das jeden Fehlschlag als `1` meldet

## Die Form der Ausgabe

| Situation | Was die CLI tut |
|---|---|
| Standardausgabe ist kein Terminal (Röhre, Datei) | JSON, ohne dass `--json` nötig wäre |
| Standardausgabe ist ein Terminal | Kopfzeile und Tabelle für Menschen |
| `--json` bzw. `--table` | erzwingt die Form; schlägt die Umgebungsvariable |
| `SZENERA_OUTPUT=json` bzw. `table` | erzwingt die Form; schlägt die Erkennung |

Im JSON-Modus steht auf der Standardausgabe **die Nutzlast und sonst nichts** — keine Hülle, kein `ok`, kein `dauerMs`. Eine Liste ist deshalb `.items[]`, nicht `.daten.items[]`; `nextCursor` steht, solange weitere Seiten folgen, direkt in der Antwort:

```bash
szenera scene list --json | jq -r '.items[].id'
szenera device list > geraete.json
```

Für Menschen gilt das Gegenteil: Die Kopfzeile steht **immer** zuerst — auch bei einem schreibenden Befehl ohne Tabelle —, darunter die Tabelle oder, wenn der Befehl keine hat, die Daten als JSON.

## Fehler gehen auf die Fehlerausgabe

Fehler landen **immer** auf der Fehlerausgabe, nie auf der Standardausgabe. `szenera device list > geraete.json` hinterlässt im Fehlerfall keine Datei, die aussieht wie ein Ergebnis. Im JSON-Modus trägt der Fehler seine Art als Wort, damit ein Protokoll später ohne diese Tabelle lesbar bleibt:

```json
{
  "error": {
    "kind": "forbidden",
    "message": "…",
    "exitCode": 4,
    "code": "Permission.Denied",
    "status": 403,
    "traceId": "trc_…"
  }
}
```

`code`, `status`, `traceId`, `details` und `retryAfterSeconds` stehen dabei, wenn der Fehler von der API kam. Im Tabellen-Modus lautet dieselbe Meldung `✗ …`, gefolgt von `HTTP 403 · Permission.Denied` und `Spur trc_…`. Fortschrittszeilen wartender Befehle gehen ebenfalls auf die Fehlerausgabe; die Standardausgabe trägt am Ende genau ein Dokument.

## Die Exit-Codes

| Code | Art | Woher |
|---|---|---|
| 0 | `ok` | alles gut |
| 1 | `usage` | unbekannter Befehl, fehlende Stellung, unbekannte Flagge, Flagge ohne Wert, fehlendes `--yes`, falscher Kennungs-Präfix — **und eine Anfrage, die dem Vertrag widerspricht** (mit Feldpfad) |
| 2 | `config` | Profil fehlt oder ist kaputt, kein Schlüssel-Präfix, Profil gesperrt (`readOnly: true`) |
| 3 | `auth` | HTTP 401 |
| 4 | `forbidden` | HTTP 403 |
| 5 | `not-found` | HTTP 404 |
| 6 | `conflict` | HTTP 409 — Zustands- oder Fassungskonflikt |
| 7 | `validation` | HTTP 422 — der Server hat die Eingabe abgelehnt |
| 8 | `rate-limited` | HTTP 429; `retryAfterSeconds` steht im Fehler |
| 9 | `server` | HTTP 5xx **oder** eine Antwort, die dem Vertrag widerspricht |
| 10 | `network` | gar keine HTTP-Antwort: kein Netz, kein Name, abgebrochen |
| 11 | `failed` | der Vorgang kam durch und endete rot (Verarbeitung gescheitert, Upload abgewiesen, Befehl `failed`) |
| 12 | `timeout` | das Zeitlimit war erreicht; der Ausgang ist **unbekannt** |

Codes 1 und 2 sind Fehler auf Ihrer Seite, 3 bis 8 Absagen des Servers mit klarer Ursache, 9 und 10 Ursachen, bei denen ein späterer Versuch Sinn ergibt, 11 und 12 die Ausgänge eines Vorgangs, der lange nach seinem Aufruf endet. Andere Statuscodes unter 500, die keiner Zeile entsprechen, enden als 1.

## 11 und 12 sind nicht 9

Beide kamen durch: Der Aufruf war richtig, der Server hat geantwortet. Bei 11 ist das Ergebnis negativ — `asset upload` konnte die Bytes schieben, die Verarbeitung ist gescheitert; der Befund steht im Fehler. Bei 12 ist das Ergebnis offen — der Vorgang läuft womöglich weiter. Wer 12 wie „gescheitert“ behandelt, stößt einen laufenden Vorgang ein zweites Mal an; wer 11 wie 9 behandelt, wiederholt einen Aufruf, dessen Antwort schon feststeht.

## Anfrage und Antwort sind zwei Seiten

Der erzeugte Client prüft beide Richtungen gegen dieselben Schemas. Ging für den Aufruf **kein** Netzpaket hinaus, war es die Anfrage — Exit 1, mit Feldpfad:

```
$ szenera scene update scn_01K… --document=kaputt.json --version=2
{ "error": { "kind": "usage", "exitCode": 1, "message":
  "Die Anfrage an „composition.scenes.update" entspricht nicht dem Vertrag:
     · draftDocument.canvas: Required
     · draftDocument.widgets: Expected array, received string" } }
```

Bricht die **Antwort** den Vertrag, ist der häufige Grund ein Versatz der Stände — die Gegenstelle ist älter oder neuer als die CLI und schickt ein Feld nicht, das diese schon verlangt. Die Meldung sagt das, nennt die Stellen und den Weg zur Rohantwort:

```
$ szenera device get dev_01K… --raw
✗ Die Antwort auf „fleet.devices.get" widerspricht dem Vertrag, den diese CLI (0.1.0) kennt.
    Wahrscheinlich fährt die Gegenstelle einen anderen Stand — nicht: dort ist etwas kaputt.
    · displays: Required
    Was tatsächlich ankam, zeigt „--raw".
```

`--raw` gilt überall und legt bei genau diesem Fehler die **Rohantwort auf die Standardausgabe** (`null`, wenn kein Rumpf ankam); die Diagnose bleibt auf der Fehlerausgabe, der Exit-Code bleibt 9. Es ist die einzige Stelle, an der ein gescheiterter Aufruf etwas auf die Standardausgabe schreibt — damit das Dokument durch `jq` gehen kann statt aus einer Fehlermeldung gefischt zu werden.

## Weiterführendes

- [Skripte und CI](/de/cli/scripts-and-ci)
- [Wenn die CLI abbricht](/de/cli/troubleshooting)
- [Das Fehlerformat](/de/api/error-format)
