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

@ -73,7 +73,10 @@ fn candidate_is_idx_binary(path: &Path) -> bool {
return false;
}
let Ok(output) = std::process::Command::new(path).arg("version").output() else {
let Ok(output) = std::process::Command::new(path)
.args(["-o", "table", "version"])
.output()
else {
return false;
};
@ -111,6 +114,10 @@ fn bin_with_root(root: &Path) -> Command {
let mut cmd = bin();
cmd.current_dir(env!("CARGO_MANIFEST_DIR"));
// Tests run with piped stdout, where the default (auto) format is JSON.
// Pin table so tests that assert table text keep doing so; `-o` and
// per-test IDX_OUTPUT still override this.
cmd.env("IDX_OUTPUT", "table");
cmd.env("XDG_CONFIG_HOME", &config_home);
cmd.env("XDG_CACHE_HOME", &cache_home);
cmd
@ -1271,18 +1278,18 @@ fn ownership_discover_json_includes_status() {
}"#;
let json_url = spawn_single_response_server("application/json", body.to_string());
test_bin("ownership-discover-json-status")
.env("IDX_CURL_IMPERSONATE_BIN", "curl")
.env("IDX_OWNERSHIP_ANNOUNCEMENT_API_URL", &json_url)
.env("IDX_OWNERSHIP_DATA_PAGE_URL", UNREACHABLE_DATA_PAGE_URL)
.env(
"IDX_OWNERSHIP_ANNOUNCEMENT_PAGE_URL",
"http://127.0.0.1/pengumuman",
)
.args(["-o", "json", "ownership", "discover", "--limit", "1"])
.assert()
.success()
.stdout(predicate::str::contains("\"status\": \"supported\""));
let discovered = run_success_json(
test_bin("ownership-discover-json-status")
.env("IDX_CURL_IMPERSONATE_BIN", "curl")
.env("IDX_OWNERSHIP_ANNOUNCEMENT_API_URL", &json_url)
.env("IDX_OWNERSHIP_DATA_PAGE_URL", UNREACHABLE_DATA_PAGE_URL)
.env(
"IDX_OWNERSHIP_ANNOUNCEMENT_PAGE_URL",
"http://127.0.0.1/pengumuman",
)
.args(["-o", "json", "ownership", "discover", "--limit", "1"]),
);
assert_eq!(discovered[0]["status"], "supported");
}
#[test]