# CLI Reference

The `fafb-q` binary serves dual purpose: `fafb-q serve` runs the server, and `fafb-q <command>` runs CLI operations against a running server. One binary to distribute, one binary to learn.

## Quick Start

```bash
# Start the server
fafb-q serve --config fafb-q.toml

# Publish a message
fafb-q pub myns/orders "hello world"

# Subscribe and tail (like tail -f)
fafb-q sub myns/orders

# Fetch messages by offset
fafb-q fetch myns/orders --from 0 --count 10

# Check server health
fafb-q health
```

## Global Flags

Global flags are available on every subcommand.

| Flag | Short | Env Var | Default | Description |
|------|-------|---------|---------|-------------|
| `--server` | `-s` | `FAFB_SERVER` | `localhost:9876` | TCP server address (host:port) |
| `--http-server` | | `FAFB_HTTP_SERVER` | Falls back to `--server` | HTTP server address (host:port) |
| `--api-key` | `-k` | `FAFB_API_KEY` | | API key for authentication |
| `--namespace` | `-n` | | | Default namespace |
| `--timeout` | | | `30` | Command timeout in seconds |
| `--json` | | | | Force JSON output for all commands |
| `--verbose` | `-v` | | | Show protocol frames on stderr |
| `--http` | | | | Use HTTP instead of TCP |
| `--tls` | | | | Enable TLS |
| `--tls-ca` | | | | CA certificate path for TLS verification |
| `--tls-cert` | | | | Client certificate path for mTLS |
| `--tls-key` | | | | Client key path for mTLS |
| `--profile` | | | | Named profile from config file |
| `--color` | | | `auto` | Color mode: `auto`, `always`, `never` |

## Output Formats

The CLI auto-detects the output format based on whether stdout is a TTY:

- **Human** (TTY default) — colored, aligned, human-readable
- **JSON** (pipe default) — one JSON object per line (JSONL), machine-parseable
- **Raw** — payload bytes only (enabled with `--payload-only`)

Override auto-detection with `--json` or `--payload-only`.

The `NO_COLOR` environment variable disables colored output (standard convention). Use `--color=never` for the same effect.

## Exit Codes

| Code | Meaning |
|------|---------|
| 0 | Success / healthy |
| 1 | Error / unhealthy / command failure |
| 2 | Timeout |

## Topic Notation

Topics are specified as `namespace/topic`:

```bash
fafb-q pub myns/orders "hello"
```

If the topic argument does not contain a `/`, the namespace is resolved from `--namespace`, the config file, or the `FAFB_API_KEY`-associated namespace.

## Commands

### `serve` — Run the Server

```bash
fafb-q serve [flags]
```

Runs the fafb-q server. This is the default command — `fafb-q --config foo.toml` is equivalent to `fafb-q serve --config foo.toml` for backward compatibility.

**Flags:**

| Flag | Description |
|------|-------------|
| `--config, -c <path>` | Path to config TOML file |
| `--dump-config` | Print the resolved configuration (secrets redacted) and exit |
| `--tcp-bind <addr>` | Override TCP bind address (e.g., `0.0.0.0:9876`) |
| `--http-bind <addr>` | Override HTTP bind address (e.g., `0.0.0.0:8080`) |
| `--data-dir <path>` | Override storage data directory |
| `--log-level <level>` | Override log level (e.g., `debug`, `info,rust_fafb_q::storage=trace`) |

**Config file discovery** (highest to lowest priority):
1. `--config` flag
2. `FAFB_CONFIG` environment variable
3. `./config.toml` in the current working directory
4. `/etc/fafb-q/config.toml` system-wide default

See the [server configuration](../config/config.example.toml) and [deploy guide](deploy.md) for details.

---

### `pub` — Publish Messages

```bash
fafb-q pub <topic> [payload] [flags]
```

Publishes messages to a topic via TCP.

**Arguments:**

| Argument | Required | Description |
|----------|----------|-------------|
| `topic` | Yes | Target topic in `ns/topic` format |
| `payload` | No | Message payload as a string |

**Flags:**

| Flag | Short | Description |
|------|-------|-------------|
| `--file <path>` | | Read payload from a file |
| `--lines` | | Read stdin line-by-line, publish one message per line |
| `--ttl <secs>` | | Message TTL in seconds (0 = no expiry) |
| `--header` | `-H` | Message header as `"Key: Value"` (repeatable) |
| `--no-ack` | | Fire-and-forget mode — don't wait for server acknowledgement |

**Payload resolution priority:**
1. Positional `payload` argument
2. `--file <path>`
3. stdin (detected when stdin is not a TTY)

**Examples:**

```bash
# Publish a string
fafb-q pub myns/orders "hello world"

# Publish from stdin
echo '{"order_id": 42}' | fafb-q pub myns/orders

# Publish from file
fafb-q pub myns/orders --file order.json

# Publish with TTL and headers
fafb-q pub myns/orders --ttl 30 -H "Content-Type: application/json" -H "Trace-Id: abc123" '{"id": 1}'

# Multi-line mode (one message per line)
cat messages.jsonl | fafb-q pub myns/orders --lines

# Fire-and-forget (no ack wait)
fafb-q pub myns/orders --no-ack "fire and forget"
```

**Output (human):**

```
published message_id=42 topic=orders
```

**Output (JSON):**

```json
{"message_id": 42, "topic": "orders"}
```

For `--lines` with `--no-ack`:
```json
{"topic": "orders", "count": 5, "ack": false}
```

**Output (raw):** Silent (no output).

---

### `sub` — Subscribe and Stream

```bash
fafb-q sub <topic> [flags]
```

Opens a streaming subscription and prints messages as they arrive — like `tail -f` for a queue. Runs until Ctrl+C or `--count` is reached.

**Arguments:**

| Argument | Required | Description |
|----------|----------|-------------|
| `topic` | Yes | Topic in `ns/topic` format (supports wildcards like `ns/orders.*`) |

**Flags:**

| Flag | Short | Description |
|------|-------|-------------|
| `--from <offset>` | | Subscribe from a specific offset |
| `--since <timestamp>` | | Subscribe from a timestamp (ISO8601 or unix millis) |
| `--group` | `-g` | Join a consumer group |
| `--count <n>` | | Receive N messages then exit cleanly |
| `--no-ack` | | Don't send Ack (receive-only / monitoring mode) |
| `--payload-only` | | Output only payload bytes |

> **Note:** `--from` and `--since` are mutually exclusive.

**Examples:**

```bash
# Subscribe from head (new messages only)
fafb-q sub myns/orders

# Subscribe from a specific offset
fafb-q sub myns/orders --from 100

# Subscribe from a timestamp
fafb-q sub myns/orders --since "2026-03-23T10:00:00Z"

# Subscribe with consumer group
fafb-q sub myns/orders --group my-cli-group

# Subscribe to wildcard pattern
fafb-q sub "myns/orders.*"

# Receive 10 messages then exit
fafb-q sub myns/orders --count 10

# Monitor mode (no acks)
fafb-q sub myns/orders --no-ack

# Pipe payloads to jq
fafb-q sub myns/orders --payload-only | jq '.'
```

**Output (human):**

```
[orders] offset=42 ts=2026-03-23T10:15:30Z
hello world

[orders] offset=43 ts=2026-03-23T10:15:31Z
{"order_id": 42}
```

**Output (JSON, one line per message):**

```json
{"type":"subscribed","topic":"orders","start_offset":42}
{"topic":"orders","offset":42,"timestamp_millis":1711185330000,"message_id":1,"payload":"hello world"}
{"topic":"orders","offset":43,"timestamp_millis":1711185331000,"message_id":2,"payload_base64":"gAAAAA==","headers":[{"key":"Content-Type","value":"application/octet-stream"}],"ttl_ms":30000}
```

> **Note:** Binary payloads that are not valid UTF-8 are output as `payload_base64` instead of `payload`. Binary header values use `value_base64`. Messages with TTL include a `ttl_ms` field.

**Ctrl+C behavior:**

1. First Ctrl+C: sends `Unsubscribe` to the server, drains remaining buffered messages, waits for `UnsubscribeAck` (5s timeout), then exits with code 0.
2. Second Ctrl+C: force-closes immediately.

**GoingAway handling:** If the server sends a `GoingAway` frame (during graceful drain), the CLI prints the message and exits cleanly.

**Header display (human mode):** Headers are printed indented below the payload:

```
[orders] offset=42 ts=2026-03-23T10:15:30Z
{"order_id": 42}
  Content-Type: application/json
  Trace-Id: abc123
```

---

### `fetch` — Pull Messages

```bash
fafb-q fetch <topic> [flags]
```

Fetches messages using the stateless pull protocol. The client manages its own offset progression.

**Arguments:**

| Argument | Required | Description |
|----------|----------|-------------|
| `topic` | Yes | Topic in `ns/topic` format |

**Flags:**

| Flag | Description |
|------|-------------|
| `--from <offset>` | Start from a specific offset |
| `--since <timestamp>` | Start from a timestamp (ISO8601 or unix millis) |
| `--count <n>` | Max messages per batch (default: 100) |
| `--all` | Fetch all messages, paginating automatically |
| `--payload-only` | Output only payload bytes |

> **Note:** At least one of `--from`, `--since`, or `--all` must be specified. `--from` and `--since` are mutually exclusive. `--all` without `--from` starts from offset 0.

**Examples:**

```bash
# Fetch from offset
fafb-q fetch myns/orders --from 0 --count 100

# Fetch from timestamp
fafb-q fetch myns/orders --since "2026-03-23T10:00:00Z" --count 50

# Fetch all messages (auto-paginate)
fafb-q fetch myns/orders --from 0 --all

# Pipe payloads
fafb-q fetch myns/orders --from 0 --count 10 --payload-only | process_messages
```

**Pagination:** With `--all`, the CLI automatically pages through messages until it reaches the head offset. Each page fetches `--count` messages.

**Warnings:** If the requested offset is no longer available (already trimmed by retention), the CLI prints a warning and starts from the oldest available offset.

---

### `info` — Queue Information

```bash
fafb-q info [target]
```

Queries queue state via HTTP endpoints.

**Arguments:**

| Argument | Required | Description |
|----------|----------|-------------|
| `target` | Yes | Namespace name or `ns/topic` |

**Behavior:**

- If `target` contains `/`: queries topic details via `GET /v1/{namespace}/{topic}/info`
- If `target` is a plain name: lists all topics in the namespace via `GET /v1/{namespace}/topics`

**Examples:**

```bash
# Topic details
fafb-q info myns/orders

# List all topics in namespace
fafb-q info myns
```

**Output (topic info, human):**

```
Namespace: myns
Topic: orders
  Head offset:    1,234,567
  Base offset:    1,200,000
  Depth:          34,567
  Bytes:          128 MB
  Subscribers:    3 (2 groups)
```

**Output (JSON):** Raw API response passed through.

---

### `health` — Health Check

```bash
fafb-q health [flags]
```

Checks server health via HTTP. Useful in scripts, cron jobs, and monitoring.

**Flags:**

| Flag | Description |
|------|-------------|
| `--live` | Use liveness probe instead of readiness probe |

**Endpoints:**

- Default: `GET /health/ready` — returns 200 when server is initialized and running
- With `--live`: `GET /health/live` — returns 200 always (liveness probe)

**Exit codes:** 0 = healthy, 1 = unhealthy (503, connection refused, or timeout).

**Examples:**

```bash
# Readiness check (for scripts)
fafb-q health && echo "ready" || echo "not ready"

# Liveness check
fafb-q health --live

# JSON output
fafb-q health --json
```

---

### `drain` — Trigger Graceful Drain

```bash
fafb-q drain [flags]
```

Triggers graceful drain or checks drain status via admin HTTP endpoints.

**Flags:**

| Flag | Env Var | Description |
|------|---------|-------------|
| `--admin-key <key>` | `FAFB_ADMIN_API_KEY` | Admin API key (required) |
| `--timeout <secs>` | | Custom drain timeout passed to server |
| `--reason <string>` | | Drain reason (e.g., "upgrade", "maintenance") |
| `--status` | | Check drain status instead of triggering |
| `--wait` | | Poll until drain completes |

**Behavior:**

- Default: triggers drain via `POST /admin/drain`
- With `--status`: queries `GET /admin/drain/status`
- With `--wait`: triggers drain, then polls status every 1s until the server reaches `shutting_down` state or the CLI timeout expires. If the server shuts down during polling (connection refused), the CLI treats this as drain-complete.

> **Note:** `--timeout` on `drain` is the **server-side** drain timeout (how long the server waits for clients to disconnect). The global `--timeout` flag is the **CLI-side** timeout (how long the CLI waits before giving up). When using `--wait`, set the global timeout higher than the drain timeout: `fafb-q --timeout 120 drain --admin-key $KEY --timeout 60 --wait`.

**Examples:**

```bash
# Trigger drain
fafb-q drain --admin-key "$ADMIN_KEY"

# Drain with custom timeout and reason
fafb-q drain --admin-key "$ADMIN_KEY" --timeout 60 --reason "upgrade"

# Wait for drain to complete
fafb-q drain --admin-key "$ADMIN_KEY" --wait --timeout 120

# Check drain status
fafb-q drain --admin-key "$ADMIN_KEY" --status
```

**Output (human):**

```
Drain initiated  active_connections=5  deadline=2026-03-23T10:16:00Z
```

**Error handling:**

- 403: invalid admin key
- 409: already draining

---

### `config` — Configuration Utilities

```bash
fafb-q config <action>
```

**Actions:**

#### `config validate`

```bash
fafb-q config validate --file fafb-q.toml
```

Loads and validates a server configuration file. Prints validation errors or confirms the config is valid. Exit code 0 = valid, 1 = invalid.

#### `config dump`

```bash
# Dump local config file
fafb-q config dump --file fafb-q.toml

# Fetch running server config (requires admin key)
fafb-q config dump --admin-key "$ADMIN_KEY"
```

Prints the resolved configuration. API keys are redacted in the output. When `--admin-key` is provided, fetches the running config from `GET /admin/config`.

#### `config example`

```bash
fafb-q config example > fafb-q.toml
```

Prints the embedded example configuration file to stdout. Useful for bootstrapping a new deployment.

---

### `bench` — Benchmarking

```bash
fafb-q bench <action>
```

Simple built-in benchmarks for quick performance testing. Results are for relative comparison (before/after changes), not absolute performance claims.

#### `bench pub` — Publish Benchmark

```bash
fafb-q bench pub <topic> [flags]
```

| Flag | Default | Description |
|------|---------|-------------|
| `--messages <n>` | 10000 | Number of messages to publish |
| `--payload-size <bytes>` | 256 | Payload size per message |
| `--concurrency <n>` | 1 | Parallel TCP connections |
| `--no-ack` | | Fire-and-forget pipelined publish |

```bash
# Basic publish benchmark
fafb-q bench pub myns/bench --messages 100000

# High-throughput fire-and-forget
fafb-q bench pub myns/bench --messages 100000 --no-ack --concurrency 4
```

**Output:**

```
Published 100,000 messages in 2.34s
  Throughput: 42,735 msg/s (10.4 MB/s)
  p50: 0.12ms  p99: 0.89ms  p999: 2.1ms  max: 5.3ms
```

> **Note:** `--no-ack` mode pipelines publishes without waiting for server acknowledgements. Latency percentiles are not reported in this mode.

#### `bench sub` — Subscribe Benchmark

```bash
fafb-q bench sub <topic> [flags]
```

| Flag | Short | Description |
|------|-------|-------------|
| `--group <name>` | `-g` | Consumer group name |
| `--from <offset>` | | Subscribe from specific offset (for historical consumption) |
| `--count <n>` | | Stop after N messages |

```bash
# Consume as fast as possible (Ctrl+C to stop)
fafb-q bench sub myns/bench

# Consume 100k messages from offset 0
fafb-q bench sub myns/bench --from 0 --count 100000
```

#### `bench latency` — End-to-End Latency

```bash
fafb-q bench latency <topic> [flags]
```

| Flag | Default | Description |
|------|---------|-------------|
| `--messages <n>` | 1000 | Number of messages |
| `--warmup <n>` | 0 | Discard initial samples |

Uses two TCP connections (one publisher, one subscriber) to measure round-trip latency.

```bash
# Latency benchmark with warmup
fafb-q bench latency myns/bench --messages 5000 --warmup 100
```

**Output:**

```
Latency benchmark: 1,000 messages
  p50: 0.12ms  p99: 0.89ms  p999: 2.1ms  max: 5.3ms
```

Progress is printed to stderr every 2 seconds for long-running benchmarks.

---

### `completions` — Shell Completions

```bash
fafb-q completions <shell>
```

Generates shell completion scripts. Supported shells: `bash`, `zsh`, `fish`, `powershell`, `elvish`.

```bash
# Bash
fafb-q completions bash > ~/.bash_completion.d/fafb-q

# Zsh
fafb-q completions zsh > ~/.zfunc/_fafb-q

# Fish
fafb-q completions fish > ~/.config/fish/completions/fafb-q.fish
```

## Configuration File

The CLI reads connection defaults from `~/.fafb-q/config.toml`. The file is optional — missing is not an error.

```toml
[default]
server = "localhost:9876"
http_server = "localhost:9877"
api_key = "sk_dev_abc123"
namespace = "myapp"
tls = false

[profiles.production]
server = "fafb-q.prod.internal:9876"
http_server = "fafb-q.prod.internal:9877"
api_key_env = "FAFB_PROD_KEY"   # read API key from env var
namespace = "prod"
tls = true
tls_ca = "/etc/ssl/ca.pem"

[profiles.staging]
server = "fafb-q.staging.internal:9876"
api_key = "sk_staging_xyz"
namespace = "staging"
```

### Profiles

Named profiles allow switching between environments:

```bash
fafb-q --profile production sub prod/orders
fafb-q --profile staging info staging/orders
```

Profile values override `[default]` values. CLI flags override profile values. The resolution order is:

```
config file [default] < config file [profile] < environment variables < CLI flags
```

### API Key Resolution

The API key is resolved in this order:
1. `--api-key` CLI flag
2. `FAFB_API_KEY` environment variable
3. Value from `api_key_env` field in the active profile (reads the named env var)
4. `api_key` field in the active profile
5. `api_key` field in `[default]`

The `api_key_env` field is useful for avoiding secrets in config files — it stores the name of an environment variable to read the key from, rather than the key itself.

## TLS

All TCP commands support TLS and mTLS:

```bash
# TLS with CA verification
fafb-q --tls --tls-ca /path/to/ca.pem pub myns/orders "hello"

# mTLS (mutual TLS)
fafb-q --tls --tls-ca /path/to/ca.pem \
  --tls-cert /path/to/client.pem \
  --tls-key /path/to/client-key.pem \
  sub myns/orders
```

TLS settings can also be stored in the config file:

```toml
[default]
tls = true
tls_ca = "/etc/ssl/ca.pem"
tls_cert = "/etc/ssl/client.pem"
tls_key = "/etc/ssl/client-key.pem"
```

HTTP commands (`info`, `health`, `drain`, `config dump --admin-key`) use `--tls-ca` and `--tls-cert`/`--tls-key` to configure the HTTP client's TLS settings.

## Verbose Mode

The `--verbose` flag prints protocol frames to stderr, useful for debugging connection issues:

```bash
fafb-q -v pub myns/orders "test"
```

```
>>> Frame::Connect { version: 1, namespace: "myns", api_key: "****", capabilities: 127 }
<<< Frame::ConnectAck { success: true, message: "ok", server_capabilities: 127 }
>>> Frame::Publish { topic: "orders", ... }
<<< Frame::PublishAck { message_id: 42 }
```

API keys are redacted in verbose output. Frame debug output is truncated at 256 characters for large payloads.

## Timestamp Formats

Commands that accept timestamps (`--since` on `sub` and `fetch`) support multiple formats:

| Format | Example | Interpretation |
|--------|---------|----------------|
| Unix milliseconds | `1711185330000` | Exact timestamp |
| ISO8601 | `2026-03-23T10:15:30Z` | UTC timestamp |
| ISO8601 with fractions | `2026-03-23T10:15:30.500Z` | Sub-second precision |
| Date only | `2026-03-23` | Midnight UTC |

## Piping and Scripting

The CLI is designed for Unix-style composition:

```bash
# Fetch and filter with jq
fafb-q fetch myns/orders --from 0 --all --json | jq 'select(.payload | fromjson | .status == "failed")'

# Publish from a file, one message per line
cat events.jsonl | fafb-q pub myns/events --lines

# Subscribe and pipe payloads to a processor
fafb-q sub myns/orders --payload-only | while read -r line; do
  process_order "$line"
done

# Health check in a script
if fafb-q health --timeout 5; then
  echo "Server is ready"
else
  echo "Server is not ready" >&2
  exit 1
fi

# Trigger drain and wait
fafb-q drain --admin-key "$KEY" --wait --timeout 120 && echo "Drained"
```

When stdout is piped, output defaults to JSON (JSONL) automatically. Use `--json` to force JSON even on a TTY.

## Binary Payloads and Encoding

The CLI handles binary (non-UTF-8) payloads gracefully across all output modes:

| Output Mode | Binary Payload | Binary Header Values |
|-------------|----------------|---------------------|
| Human | `<binary N bytes>` placeholder | Lossy UTF-8 display |
| JSON | `payload_base64` field (base64-encoded) | `value_base64` field (base64-encoded) |
| Raw | Raw bytes to stdout | N/A |

In JSON mode, UTF-8 payloads use the `payload` field. Non-UTF-8 payloads use `payload_base64` instead (never both). The same applies to header values: UTF-8 values use `value`, binary values use `value_base64`.

## HTTP vs TCP

By default, commands use TCP for full protocol support and streaming. The `--http` flag switches to HTTP for environments where only HTTP is available (firewalls, proxies).

| Command | TCP | HTTP |
|---------|-----|------|
| `pub` | Full support | Not yet implemented |
| `sub` | Streaming | Not available (no streaming over HTTP) |
| `fetch` | Full support | Not yet implemented |
| `info` | N/A (always HTTP) | Full support |
| `health` | N/A (always HTTP) | Full support |
| `drain` | N/A (always HTTP) | Full support |
| `config` | N/A (always HTTP) | Full support |

The `--http-server` flag (or `FAFB_HTTP_SERVER` env var) sets the HTTP server address separately from the TCP address, since the server typically listens on different ports for TCP and HTTP.

## Troubleshooting

**Connection refused**
```
error: connection refused (os error 61)
```
The server is not running or the address is wrong. Check `--server` points to the correct host:port. For HTTP commands (`info`, `health`), check `--http-server`.

**Authentication failed**
```
error: auth failed: unknown API key
```
The API key is invalid or not configured for the target namespace. Check `--api-key`, `FAFB_API_KEY`, or the config file.

**Namespace mismatch**
```
error: auth failed: namespace mismatch
```
The API key is valid but not authorized for the namespace specified in the topic. Check that the namespace in `ns/topic` matches the key's namespace.

**Offset not available**
```
warning: requested offset 0 not available, starting from 1500
```
The requested offset has been trimmed by retention. The server automatically clamps to the oldest available offset. This is a warning, not an error.

**No API key specified**
```
error: no API key specified — use --api-key, FAFB_API_KEY, or config file
```
Commands that connect to the server require an API key. Set it via flag, environment variable, or `~/.fafb-q/config.toml`.

**Timeout**
```
error: operation timed out
```
The command exceeded the `--timeout` duration (default 30s). Increase the timeout for slow operations: `fafb-q --timeout 120 fetch myns/orders --from 0 --all`.

**Use `--verbose` for protocol debugging**

When a command fails unexpectedly, re-run with `-v` to see the raw protocol frames exchanged with the server. This shows exactly what the client sent and what the server replied.

## Related Documentation

- [TCP Wire Protocol](protocol.md) — frame format and message types
- [HTTP API](api.md) — HTTP endpoint reference
- [Deploy Guide](deploy.md) — graceful deployment and drain operations
- [Fetch & Consumer Guide](fetch_consumer_guide.md) — pull-based consumption patterns
- [Wildcard Subscriptions](wildcard_subscriptions.md) — pattern-based topic matching
- [Delivery Guarantees](delivery_guarantees.md) — ack, nack, and DLQ semantics
