# Scripts and CI

Run espx unattended: wait for log lines, read --json output, act on exit codes.

espx is built to run without a person watching. Every choice is a flag, nothing prompts, and the monitor can decide on its own when a run passed.

## Nothing prompts

No espx command waits for input unless you pass `-i`. A missing choice is an error that names the flag to pass, and espflash is always told `--non-interactive` so its board picker never opens. `-i` is the opt-in for a person at a terminal:

| Command             | `-i` does                                                |
| ------------------- | -------------------------------------------------------- |
| `espx new -i`       | opens esp-generate's terminal UI                         |
| `espx flash -i`     | lets espflash ask which board when several are connected |
| `espx uninstall -i` | asks about each item                                     |

`-i` without a terminal is an error (`not_a_tty`).

The one thing espx detects instead of being told is the serial monitor. `run`, `monitor` and `flash --monitor` keep espflash's key handling when stdin is a terminal, and read the port themselves when it is not. So `espx run | grep panic` works.

## Wait for a line

```sh
espx run --until 'ready|panicked' --timeout 2m
espx run --expect 'wifi up' --expect 'ready' --reject 'PANIC|rst:0x7' --timeout 1m
espx monitor --until 'Hello world!' --timeout 30s
espx replay crash.jsonl --until 'PANIC' --fast
```

These flags work on `run`, `monitor` and `replay`. Each pattern is a regular expression in the syntax of Rust's `regex-lite` crate, tested against the text of each monitor line.

| Flag          | Repeatable | Effect                                                                                                                                                            |
| ------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--until RE`  | no         | Exit `0` on the first line that matches                                                                                                                           |
| `--expect RE` | yes        | An ordered script. Each regex must match a line after the line the previous one matched. Each match is an `expected` event with its index; the last one exits `0` |
| `--reject RE` | yes        | Fail at once, with exit `1`, on any line that matches. Tested before the expectations on every line                                                               |
| `--timeout D` | no         | Stop after `D` and exit `124`. Takes `30s`, `2m`, `1h`, or a bare number of seconds                                                                               |

`--until` is a one-pattern `--expect`, added after any `--expect`s. On `run` the timeout clock starts when the flashed board boots, not when the build starts. When a timeout runs out, the output up to that point is still printed, and the error names the pattern still pending.

> **Note**
>
> `--timeout` exits `124` whenever it runs out, even with no pattern to wait for. `espx monitor --timeout 10s` captures ten seconds of output and then exits `124`. Treat `124` as "time is up", not as a crash.

If the stream ends first (a replay reaches the end of its file) with a pattern still pending, espx exits `1` with `monitor_closed`. With nothing pending it exits `0`.

## JSON output

`--json` changes only stdout. There are two shapes.

| Commands                                                                                   | `--json` prints                                                                    |
| ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
| `devices`, `info`, `chips`, `doctor`, `boards`, `board`, `deps`, `context`, `agent`, `log` | One pretty-printed JSON document                                                   |
| `build`, `flash`, `run`, `monitor`, `replay`                                               | One JSON event per line, each stamped with `ts`, host milliseconds since the epoch |
| `devices --watch`                                                                          | One `attached` or `detached` event per line                                        |

```text
{"ts":1789180727730,"event":"reset"}
{"ts":1789180727741,"event":"line","text":"ESP-ROM:esp32c6-20220919"}
{"ts":1789180727741,"event":"boot","reason":"usb","code":21,"raw":"rst:0x15 (USB_UART_HPSYS),boot:0x6c (SPI_FAST_FLASH_BOOT)"}
{"ts":1789180728120,"event":"line","text":"\u001b[32mINFO - Hello world!\u001b[0m"}
{"ts":1789180728120,"event":"expected","pattern":"Hello world!","index":0}
{"ts":1789180728120,"event":"matched","pattern":"Hello world!"}
```

A plain-text log line keeps esp-println's colour codes in `text`; a decoded defmt line carries `level`, `module` and `location` as separate fields. A stream ends with `matched`, `rejected`, `timeout` or `done`. `build` and `flash` end with `done`. Progress lines and warnings stay on stderr either way, and in `--json` mode every child process (cargo, espflash) has its stdout sent to stderr too, so stdout parses line by line. The [JSON events](https://www.espx.dev/docs/reference/events) reference lists every event and field, and `schema/espx.json` in the espx repository is the JSON Schema of all of them.

```sh
espx monitor --json --timeout 20s | jq -c 'select(.event == "panic")'
espx build --json | jq -r 'select(.event == "size") | .flash'
```

## Errors

A failing command prints one `error:` line on stderr that says what to do next, and exits with a code from the table below. The line is text even with `--json`. The stable, machine-readable error codes (`build_failed`, `board_not_attached`, `policy_denied`) appear in MCP tool results and in the agent audit log; see [Exit codes](https://www.espx.dev/docs/reference/exit-codes#error-codes).

## Exit codes

| Code  | Meaning                                                                                               |
| ----- | ----------------------------------------------------------------------------------------------------- |
| `0`   | Success, including a `--until` match or the last `--expect` match                                     |
| `1`   | Any other failure: a build error, a rejected line, a missing board, a bad flag                        |
| `124` | `--timeout` ran out (the coreutils convention)                                                        |
| `130` | A child process was interrupted, usually your `Ctrl+C` reaching cargo or espflash. Nothing is printed |

## In CI

A CI job with a board attached to the runner can build, flash and check the firmware in a few lines:

```sh title="test-on-board.sh"
#!/bin/sh
set -e
export ESPX_NO_UPDATE_CHECK=1

espx setup
espx build --json > build.jsonl
espx run --expect 'boot ok' --expect 'self-test passed' \
         --reject 'PANIC|panicked|rst:0x7' \
         --timeout 2m --json > run.jsonl
```

- `espx setup` is idempotent, so it costs nothing on a runner that already has the tools.
- `ESPX_NO_UPDATE_CHECK` (any value) stops the daily background update check.
- Bind the project to the runner's board with `[board] id` in `espx.toml`, or pass `--port`, when the runner has more than one board attached.
- Add `--record run.jsonl` to `espx monitor` to keep a session as a build artifact; `espx replay` plays it back anywhere, without the board. See [Monitor and replay](https://www.espx.dev/docs/cli/monitor#record-and-replay).

Builds and replays need no board, so the parts of a pipeline that check recorded output can run on any machine.

## Related

- [Monitor and replay](https://www.espx.dev/docs/cli/monitor)
- [JSON events](https://www.espx.dev/docs/reference/events)
- [Exit codes](https://www.espx.dev/docs/reference/exit-codes)
