feat(cli): agent-native output, stdin symbols, and exit-code classes

Phase 1 of docs/DESIGN.md:

- Output format resolves -o > IDX_OUTPUT > general.output > auto; auto
  is a table on a terminal and JSON when stdout is piped or captured.
  `config init` now writes output = "auto".
- JSON is pretty only on a terminal and compact otherwise. New `-o
  jsonl` prints one array item per line with the same key order.
- No ANSI color when stdout is not a terminal (direct owo-colors calls
  now go through output::style).
- `stocks quote -` and `stocks compare -` read symbols from stdin.
- Exit codes by class: 2 invalid input/config, 3 not found or cache
  miss, 4 upstream unavailable, 5 unsupported, 1 other.

Golden changes are limited to those documented kinds and were verified
mechanically: JSON goldens are semantically identical, tables differ
only by removed ANSI codes and the column padding comfy-table had
reserved for them (cell contents identical), exit codes match each
error's class, and help/completions only gain the new --output values.
17 new golden cases cover auto, jsonl, stdin, and the new errors.

tests/cli.rs now pins IDX_OUTPUT=table in its harness (its tests assert
table text and run piped) and parses discover JSON instead of matching
pretty-printed whitespace.

Live A/B against main: 16/16 stocks cases identical; ownership discover
got IDX 403 on both binaries, now exiting 4 instead of 1 as intended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
hermes 2026-10-14 08:52:13 +00:00
commit 22b6dabaeb
119 changed files with 920 additions and 1896 deletions

View file

@ -44,7 +44,7 @@ use crate::api::default_provider;
use crate::cli::{Cli, Commands};
use crate::config::IdxConfig;
use crate::error::IdxError;
use crate::output::{OutputFormat, emit_error};
use crate::output::{OutputFormat, OutputSetting, emit_error};
fn main() {
if let Err(err) = run() {
@ -62,16 +62,19 @@ fn run() -> Result<(), IdxError> {
return Err(err);
}
};
output::json::configure(config.output, config::stdout_is_terminal());
output::style::set_color(!config.no_color);
match &cli.command {
Commands::Version => match config.output {
OutputFormat::Json => {
Commands::Version => {
if config.output.is_json() {
output::json::print_json(&serde_json::json!({
"version": env!("CARGO_PKG_VERSION")
}))?;
} else {
println!("{}", env!("CARGO_PKG_VERSION"));
}
OutputFormat::Table => println!("{}", env!("CARGO_PKG_VERSION")),
},
}
Commands::Completions { shell } => {
let mut cmd = Cli::command();
let name = cmd.get_name().to_owned();
@ -119,13 +122,15 @@ fn run() -> Result<(), IdxError> {
Ok(())
}
/// Output format for errors raised before the configuration has loaded.
fn bootstrap_output_format(cli: &Cli) -> OutputFormat {
if let Some(output) = cli.output {
return output;
}
match std::env::var("IDX_OUTPUT") {
Ok(value) if value.eq_ignore_ascii_case("json") => OutputFormat::Json,
_ => OutputFormat::Table,
}
let setting = cli
.output
.or_else(|| {
std::env::var("IDX_OUTPUT")
.ok()
.and_then(|value| OutputSetting::parse(&value))
})
.unwrap_or_default();
setting.resolve(config::stdout_is_terminal())
}