From e2fc9746948aed2fdc14f9e26237f8df5da6086b Mon Sep 17 00:00:00 2001 From: John Mitsch Date: Wed, 24 Jun 2026 10:36:49 -0400 Subject: [PATCH] feat(output): default to json instead of toon for non-TTY stdout (DX-5867) When stdout is not a terminal (piped / scripted / agent invocations), the default output format is now JSON instead of TOON. JSON is understood by virtually every downstream tool and parses cleanly, making it the safer universal default for the piped path; TOON is newer and not as widely supported, so it's a poor fit as the implicit default. TOON remains fully supported via `--format toon` and the config file. Only the default-when-unset changes; the CLI flag > config > built-in default ordering is unchanged, and the TTY default stays `table`. Syncs the module docs, `--format` help, the embedded `qn agent context` guide, and the README (intro example, formats table, config defaults) to describe JSON as the non-TTY default. --- README.md | 50 ++++++++++++++++++++++++++--------- src/cli.rs | 5 ++-- src/commands/agent/context.md | 4 +-- src/context.rs | 16 +++++++---- src/output.rs | 2 +- 5 files changed, 55 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 764c5d9..409e92d 100644 --- a/README.md +++ b/README.md @@ -15,16 +15,42 @@ ep-1 production active ethereum/mainnet shared yes https://ep-1.examp ep-2 — paused solana/mainnet dedicated no https://ep-2.example — showing 1–2 of 2 -# LLM-optimized TOON format (non-TTY default) +# Piped / non-TTY output defaults to JSON $ qn endpoint list | cat -data[2]{id,name,label,status,chain,network,is_dedicated,is_flat_rate,http_url,wss_url,tags,is_multichain}: - "ep-1","ep-1","production",active,ethereum,mainnet,false,false,"https://ep-1.example",null,"prod, eu",false - "ep-2","ep-2",null,paused,solana,mainnet,true,false,"https://ep-2.example",null,"",false -pagination: - total: 2 - limit: 20 - offset: 0 -error: null +{ + "data": [ + { + "id": "ep-1", + "name": "ep-1", + "label": "production", + "status": "active", + "chain": "ethereum", + "network": "mainnet", + "is_dedicated": false, + "is_flat_rate": false, + "http_url": "https://ep-1.example", + "wss_url": null, + "tags": ["prod", "eu"], + "is_multichain": false + }, + { + "id": "ep-2", + "name": "ep-2", + "label": null, + "status": "paused", + "chain": "solana", + "network": "mainnet", + "is_dedicated": true, + "is_flat_rate": false, + "http_url": "https://ep-2.example", + "wss_url": null, + "tags": [], + "is_multichain": false + } + ], + "pagination": { "total": 2, "limit": 20, "offset": 0 }, + "error": null +} ``` ## Installation @@ -139,10 +165,10 @@ Pick a format with `--format ` (alias `-o `): | `--format` | Best for | | --- | --- | | `table` | Humans on a TTY. Pretty UTF-8 tables with optional color. Default when stdout is a terminal. | -| `json` | Scripts and pipelines (`jq`, `gron`, …). | +| `json` | Scripts and pipelines (`jq`, `gron`, …). Default when stdout is **not** a terminal (piped / agent invocations). | | `yaml` | Same shape as JSON, easier to skim by eye. | | `md` | GitHub-flavored markdown — paste into PRs, issues, docs. | -| `toon` | [Token-Oriented Object Notation](https://github.com/toon-format/toon-rust) — compact serialization optimized for LLM prompts. Default when stdout is **not** a terminal (piped / agent invocations). | +| `toon` | [Token-Oriented Object Notation](https://github.com/toon-format/toon-rust) — compact serialization optimized for LLM prompts. | Other output flags: @@ -159,7 +185,7 @@ format = "yaml" # default --format value wide = true # always show extra columns in table/md output ``` -CLI flags win over config values. Built-in defaults: `format = "table"` when stdout is a TTY, `"toon"` otherwise; `wide = false`. +CLI flags win over config values. Built-in defaults: `format = "table"` when stdout is a TTY, `"json"` otherwise; `wide = false`. `qn` follows the [Command Line Interface Guidelines](https://clig.dev/): data on stdout, diagnostics on stderr, meaningful exit codes (0 success, 2 API error, 3 network error, 4 auth/config, 5 needs confirmation), and a documented `-h`/`--help` at every subcommand level. diff --git a/src/cli.rs b/src/cli.rs index 89fac6a..6e21cee 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -50,9 +50,10 @@ pub struct Cli { #[arg(long, global = true, value_name = "PATH")] pub config_file: Option, - /// Output format. `table` is the default human view; the others are + /// Output format. `table` is the human view; the others are /// pipeline-friendly serialized forms. If unset, falls back to the - /// `[output] format = "…"` value in ~/.config/qn/config.toml, then `table`. + /// `[output] format = "…"` value in ~/.config/qn/config.toml, then the + /// TTY-aware default: `table` when stdout is a terminal, `json` otherwise. #[arg(short = 'o', long = "format", global = true, value_enum)] pub format: Option, diff --git a/src/commands/agent/context.md b/src/commands/agent/context.md index e7dc5d7..ea00f80 100644 --- a/src/commands/agent/context.md +++ b/src/commands/agent/context.md @@ -37,7 +37,7 @@ network call. ## 2. Output contract -- Default format is `table` on a TTY and **`toon`** when stdout is not a TTY (piped). +- Default format is `table` on a TTY and **`json`** when stdout is not a TTY (piped). - Data goes to **stdout**; diagnostics, prompts, and ✓ confirmations go to **stderr**. - Formats: `table`, `md`, `json`, `yaml`, `toon`. The structured forms (`json`/`yaml`/`toon`) always include every field — `--wide` is not needed and @@ -169,7 +169,7 @@ qn kv set list - Mutations are never retried; re-running a failed create can double-provision (§5). - No account-wide wipe command exists by design (§4). -- Piped output defaults to `toon`, not `json` (§2). +- Piped output defaults to `json`; pass `-o toon` for the compact LLM form (§2). - `--base-url` overrides the API host; it exists for testing. - For *this* command, `-o yaml`/`-o toon`/`-o table` print Markdown (with a note on stderr); `-o json` produces the `{version, guide}` envelope. diff --git a/src/context.rs b/src/context.rs index 1616c71..378d671 100644 --- a/src/context.rs +++ b/src/context.rs @@ -21,7 +21,7 @@ pub struct GlobalArgs { /// (`~/.config/qn/config.toml`). pub config_file: Option, /// `None` means the user didn't pass `--format`; resolve via config file - /// (then the TTY-aware default: `Table` on a TTY, `Toon` off) when we + /// (then the TTY-aware default: `Table` on a TTY, `Json` off) when we /// build the [`Ctx`]. pub format: Option, pub wide: bool, @@ -38,7 +38,7 @@ pub struct GlobalArgs { impl GlobalArgs { /// Resolve the output format: CLI flag > config file > TTY-aware default - /// (`Table` on a TTY, `Toon` off). + /// (`Table` on a TTY, `Json` off). /// Used by [`Ctx::from_global`] and `auth` (which doesn't build a Ctx). pub fn resolve_format(&self, stdout_is_tty: bool) -> Format { self.resolve_output(stdout_is_tty).0 @@ -47,7 +47,7 @@ impl GlobalArgs { /// Resolve `(format, wide)` together so we only read the config file once. /// /// For each: CLI flag > config file > built-in default. The format default - /// is TTY-aware: `Table` when stdout is a terminal, `Toon` otherwise (so + /// is TTY-aware: `Table` when stdout is a terminal, `Json` otherwise (so /// agents / piped callers get a structured format by default). `--wide` is /// purely additive — the flag sets it true; the config file can also set /// it true; otherwise it's false. @@ -85,7 +85,7 @@ fn resolve_output_inner( let format = flag_format.or(cfg_format).unwrap_or(if stdout_is_tty { Format::Table } else { - Format::Toon + Format::Json }); let wide = flag_wide || cfg_wide; (format, wide) @@ -240,8 +240,14 @@ mod tests { } #[test] - fn default_is_toon_when_stdout_is_not_a_tty() { + fn default_is_json_when_stdout_is_not_a_tty() { let (f, _) = resolve_output_inner(None, false, None, false, false); + assert_eq!(f, Format::Json); + } + + #[test] + fn config_toon_overrides_non_tty_default() { + let (f, _) = resolve_output_inner(None, false, Some(Format::Toon), false, false); assert_eq!(f, Format::Toon); } diff --git a/src/output.rs b/src/output.rs index 6715d6f..dc25071 100644 --- a/src/output.rs +++ b/src/output.rs @@ -2,7 +2,7 @@ //! //! Five formats, selected by the global `--format/-o` flag. When the flag and //! the config file both leave the format unset, the default is TTY-aware: -//! `table` when stdout is a terminal (interactive use), `toon` otherwise +//! `table` when stdout is a terminal (interactive use), `json` otherwise //! (piped / agent invocations). See [`crate::context::GlobalArgs::resolve_output`]. //! //! - `table`: comfy-table with UTF-8 borders for humans on a TTY.