Skip to content

Commit e2fc974

Browse files
committed
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.
1 parent 00fd637 commit e2fc974

5 files changed

Lines changed: 55 additions & 22 deletions

File tree

README.md

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,42 @@ ep-1 production active ethereum/mainnet shared yes https://ep-1.examp
1515
ep-2 — paused solana/mainnet dedicated no https://ep-2.example —
1616
showing 1–2 of 2
1717
18-
# LLM-optimized TOON format (non-TTY default)
18+
# Piped / non-TTY output defaults to JSON
1919
$ qn endpoint list | cat
20-
data[2]{id,name,label,status,chain,network,is_dedicated,is_flat_rate,http_url,wss_url,tags,is_multichain}:
21-
"ep-1","ep-1","production",active,ethereum,mainnet,false,false,"https://ep-1.example",null,"prod, eu",false
22-
"ep-2","ep-2",null,paused,solana,mainnet,true,false,"https://ep-2.example",null,"",false
23-
pagination:
24-
total: 2
25-
limit: 20
26-
offset: 0
27-
error: null
20+
{
21+
"data": [
22+
{
23+
"id": "ep-1",
24+
"name": "ep-1",
25+
"label": "production",
26+
"status": "active",
27+
"chain": "ethereum",
28+
"network": "mainnet",
29+
"is_dedicated": false,
30+
"is_flat_rate": false,
31+
"http_url": "https://ep-1.example",
32+
"wss_url": null,
33+
"tags": ["prod", "eu"],
34+
"is_multichain": false
35+
},
36+
{
37+
"id": "ep-2",
38+
"name": "ep-2",
39+
"label": null,
40+
"status": "paused",
41+
"chain": "solana",
42+
"network": "mainnet",
43+
"is_dedicated": true,
44+
"is_flat_rate": false,
45+
"http_url": "https://ep-2.example",
46+
"wss_url": null,
47+
"tags": [],
48+
"is_multichain": false
49+
}
50+
],
51+
"pagination": { "total": 2, "limit": 20, "offset": 0 },
52+
"error": null
53+
}
2854
```
2955

3056
## Installation
@@ -139,10 +165,10 @@ Pick a format with `--format <FMT>` (alias `-o <FMT>`):
139165
| `--format` | Best for |
140166
| --- | --- |
141167
| `table` | Humans on a TTY. Pretty UTF-8 tables with optional color. Default when stdout is a terminal. |
142-
| `json` | Scripts and pipelines (`jq`, `gron`, …). |
168+
| `json` | Scripts and pipelines (`jq`, `gron`, …). Default when stdout is **not** a terminal (piped / agent invocations). |
143169
| `yaml` | Same shape as JSON, easier to skim by eye. |
144170
| `md` | GitHub-flavored markdown — paste into PRs, issues, docs. |
145-
| `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). |
171+
| `toon` | [Token-Oriented Object Notation](https://github.com/toon-format/toon-rust) — compact serialization optimized for LLM prompts. |
146172

147173
Other output flags:
148174

@@ -159,7 +185,7 @@ format = "yaml" # default --format value
159185
wide = true # always show extra columns in table/md output
160186
```
161187

162-
CLI flags win over config values. Built-in defaults: `format = "table"` when stdout is a TTY, `"toon"` otherwise; `wide = false`.
188+
CLI flags win over config values. Built-in defaults: `format = "table"` when stdout is a TTY, `"json"` otherwise; `wide = false`.
163189

164190
`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.
165191

src/cli.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@ pub struct Cli {
5050
#[arg(long, global = true, value_name = "PATH")]
5151
pub config_file: Option<std::path::PathBuf>,
5252

53-
/// Output format. `table` is the default human view; the others are
53+
/// Output format. `table` is the human view; the others are
5454
/// pipeline-friendly serialized forms. If unset, falls back to the
55-
/// `[output] format = "…"` value in ~/.config/qn/config.toml, then `table`.
55+
/// `[output] format = "…"` value in ~/.config/qn/config.toml, then the
56+
/// TTY-aware default: `table` when stdout is a terminal, `json` otherwise.
5657
#[arg(short = 'o', long = "format", global = true, value_enum)]
5758
pub format: Option<Format>,
5859

src/commands/agent/context.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ network call.
3737

3838
## 2. Output contract
3939

40-
- Default format is `table` on a TTY and **`toon`** when stdout is not a TTY (piped).
40+
- Default format is `table` on a TTY and **`json`** when stdout is not a TTY (piped).
4141
- Data goes to **stdout**; diagnostics, prompts, and ✓ confirmations go to **stderr**.
4242
- Formats: `table`, `md`, `json`, `yaml`, `toon`. The structured forms
4343
(`json`/`yaml`/`toon`) always include every field — `--wide` is not needed and
@@ -169,7 +169,7 @@ qn kv set list
169169

170170
- Mutations are never retried; re-running a failed create can double-provision (§5).
171171
- No account-wide wipe command exists by design (§4).
172-
- Piped output defaults to `toon`, not `json` (§2).
172+
- Piped output defaults to `json`; pass `-o toon` for the compact LLM form (§2).
173173
- `--base-url` overrides the API host; it exists for testing.
174174
- For *this* command, `-o yaml`/`-o toon`/`-o table` print Markdown (with a note on
175175
stderr); `-o json` produces the `{version, guide}` envelope.

src/context.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub struct GlobalArgs {
2121
/// (`~/.config/qn/config.toml`).
2222
pub config_file: Option<std::path::PathBuf>,
2323
/// `None` means the user didn't pass `--format`; resolve via config file
24-
/// (then the TTY-aware default: `Table` on a TTY, `Toon` off) when we
24+
/// (then the TTY-aware default: `Table` on a TTY, `Json` off) when we
2525
/// build the [`Ctx`].
2626
pub format: Option<Format>,
2727
pub wide: bool,
@@ -38,7 +38,7 @@ pub struct GlobalArgs {
3838

3939
impl GlobalArgs {
4040
/// Resolve the output format: CLI flag > config file > TTY-aware default
41-
/// (`Table` on a TTY, `Toon` off).
41+
/// (`Table` on a TTY, `Json` off).
4242
/// Used by [`Ctx::from_global`] and `auth` (which doesn't build a Ctx).
4343
pub fn resolve_format(&self, stdout_is_tty: bool) -> Format {
4444
self.resolve_output(stdout_is_tty).0
@@ -47,7 +47,7 @@ impl GlobalArgs {
4747
/// Resolve `(format, wide)` together so we only read the config file once.
4848
///
4949
/// For each: CLI flag > config file > built-in default. The format default
50-
/// is TTY-aware: `Table` when stdout is a terminal, `Toon` otherwise (so
50+
/// is TTY-aware: `Table` when stdout is a terminal, `Json` otherwise (so
5151
/// agents / piped callers get a structured format by default). `--wide` is
5252
/// purely additive — the flag sets it true; the config file can also set
5353
/// it true; otherwise it's false.
@@ -85,7 +85,7 @@ fn resolve_output_inner(
8585
let format = flag_format.or(cfg_format).unwrap_or(if stdout_is_tty {
8686
Format::Table
8787
} else {
88-
Format::Toon
88+
Format::Json
8989
});
9090
let wide = flag_wide || cfg_wide;
9191
(format, wide)
@@ -240,8 +240,14 @@ mod tests {
240240
}
241241

242242
#[test]
243-
fn default_is_toon_when_stdout_is_not_a_tty() {
243+
fn default_is_json_when_stdout_is_not_a_tty() {
244244
let (f, _) = resolve_output_inner(None, false, None, false, false);
245+
assert_eq!(f, Format::Json);
246+
}
247+
248+
#[test]
249+
fn config_toon_overrides_non_tty_default() {
250+
let (f, _) = resolve_output_inner(None, false, Some(Format::Toon), false, false);
245251
assert_eq!(f, Format::Toon);
246252
}
247253

src/output.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//!
33
//! Five formats, selected by the global `--format/-o` flag. When the flag and
44
//! the config file both leave the format unset, the default is TTY-aware:
5-
//! `table` when stdout is a terminal (interactive use), `toon` otherwise
5+
//! `table` when stdout is a terminal (interactive use), `json` otherwise
66
//! (piped / agent invocations). See [`crate::context::GlobalArgs::resolve_output`].
77
//!
88
//! - `table`: comfy-table with UTF-8 borders for humans on a TTY.

0 commit comments

Comments
 (0)