fix(cli): idx config prints JSON only when JSON is asked for

Auto output stays plain text when piped, as in 0.3.2, so scripts that
read $(idx config path) or $(idx config get KEY) keep working; -o json,
IDX_OUTPUT=json, or general.output = "json" print JSON. The config
goldens return to their 0.3.2 content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3syn2fN2LNo5pvTSqc6eh
This commit is contained in:
hermes 2026-10-18 02:11:31 +00:00
commit 57eccc5843
10 changed files with 62 additions and 18 deletions

View file

@ -1,7 +1,7 @@
use clap::{Args, Subcommand}; use clap::{Args, Subcommand};
use crate::config::{config_path, ensure_default_config, get_config_value, set_config_value}; use crate::config::{config_path, ensure_default_config, get_config_value, set_config_value};
use crate::output::{OutputFormat, json}; use crate::output::{OutputSetting, json};
use idx_core::error::IdxError; use idx_core::error::IdxError;
#[derive(Debug, Args)] #[derive(Debug, Args)]
@ -23,10 +23,11 @@ pub enum ConfigSubcommand {
Path, Path,
} }
pub fn handle(cmd: &ConfigCmd, format: &OutputFormat) -> Result<(), IdxError> { pub fn handle(cmd: &ConfigCmd, setting: OutputSetting) -> Result<(), IdxError> {
// Each result as plain text, or one JSON document with -o json. // Plain text, as scripts read `$(idx config path)`, unless JSON is asked
// for (-o json, IDX_OUTPUT, or general.output); auto stays text when piped.
let print = |text: String, document: serde_json::Value| { let print = |text: String, document: serde_json::Value| {
if format.is_json() { if matches!(setting, OutputSetting::Json | OutputSetting::Jsonl) {
json::print_json(&document) json::print_json(&document)
} else { } else {
println!("{}", crate::output::text::terminal_safe(&text)); println!("{}", crate::output::text::terminal_safe(&text));

View file

@ -14,6 +14,8 @@ use idx_core::error::IdxError;
pub struct IdxConfig { pub struct IdxConfig {
pub core: CoreConfig, pub core: CoreConfig,
pub output: OutputFormat, pub output: OutputFormat,
/// The setting `output` came from, before auto was resolved.
pub output_setting: OutputSetting,
pub no_color: bool, pub no_color: bool,
} }
@ -32,7 +34,7 @@ impl IdxConfig {
pub fn load_with_cli(cli: &Cli) -> Result<Self, IdxError> { pub fn load_with_cli(cli: &Cli) -> Result<Self, IdxError> {
let mut core = CoreConfig::load()?; let mut core = CoreConfig::load()?;
core.verbose = cli.verbose > 0; core.verbose = cli.verbose > 0;
let mut output = OutputSetting::Auto.resolve(stdout_is_terminal()); let mut setting = OutputSetting::Auto;
let mut no_color = false; let mut no_color = false;
let path = config_path()?; let path = config_path()?;
@ -41,8 +43,8 @@ impl IdxConfig {
let parsed: FileConfig = let parsed: FileConfig =
toml::from_str(&raw).map_err(|e| IdxError::ConfigError(e.to_string()))?; toml::from_str(&raw).map_err(|e| IdxError::ConfigError(e.to_string()))?;
if let Some(general) = parsed.general { if let Some(general) = parsed.general {
if let Some(setting) = general.output { if let Some(file_setting) = general.output {
output = setting.resolve(stdout_is_terminal()); setting = file_setting;
} }
if let Some(color) = general.color { if let Some(color) = general.color {
no_color = !color; no_color = !color;
@ -51,25 +53,25 @@ impl IdxConfig {
} }
if let Ok(value) = std::env::var("IDX_OUTPUT") { if let Ok(value) = std::env::var("IDX_OUTPUT") {
let setting = OutputSetting::parse(&value).ok_or_else(|| { setting = OutputSetting::parse(&value).ok_or_else(|| {
IdxError::ConfigError(format!( IdxError::ConfigError(format!(
"invalid IDX_OUTPUT value: '{value}', expected 'auto', 'table', 'json', or 'jsonl'" "invalid IDX_OUTPUT value: '{value}', expected 'auto', 'table', 'json', or 'jsonl'"
)) ))
})?; })?;
output = setting.resolve(stdout_is_terminal());
} }
if let Ok(value) = std::env::var("IDX_NO_COLOR") { if let Ok(value) = std::env::var("IDX_NO_COLOR") {
no_color = value == "1" || value.eq_ignore_ascii_case("true"); no_color = value == "1" || value.eq_ignore_ascii_case("true");
} }
if let Some(setting) = cli.output { if let Some(cli_setting) = cli.output {
output = setting.resolve(stdout_is_terminal()); setting = cli_setting;
} }
no_color = no_color || cli.no_color || !stdout_is_terminal(); no_color = no_color || cli.no_color || !stdout_is_terminal();
Ok(Self { Ok(Self {
core, core,
output, output: setting.resolve(stdout_is_terminal()),
output_setting: setting,
no_color, no_color,
}) })
} }

View file

@ -62,7 +62,7 @@ fn run() -> Result<(), IdxError> {
} }
} }
Commands::Config(cfg) => { Commands::Config(cfg) => {
if let Err(err) = cli::config::handle(cfg, &config.output) { if let Err(err) = cli::config::handle(cfg, config.output_setting) {
emit_error(&err, &config.output); emit_error(&err, &config.output);
return Err(err); return Err(err);
} }

View file

@ -980,6 +980,43 @@ fn config_set_and_get_provider_round_trip() {
.stdout(predicate::str::contains("msn")); .stdout(predicate::str::contains("msn"));
} }
#[test]
fn config_prints_json_only_when_json_is_asked_for() {
// Piped with auto output, scripts read plain text: `$(idx config path)`.
let root = test_env_dir("config-output-format");
bin_with_root(&root)
.args(["config", "set", "general.provider", "msn"])
.assert()
.success()
.stdout("ok\n");
bin_with_root(&root)
.args(["config", "get", "general.provider"])
.assert()
.success()
.stdout("msn\n");
let flag = run_success_json(bin_with_root(&root).args([
"-o",
"json",
"config",
"get",
"general.provider",
]));
assert_eq!(
flag,
serde_json::json!({"key": "general.provider", "value": "msn"})
);
let env = run_success_json(
bin_with_root(&root)
.env("IDX_OUTPUT", "json")
.args(["config", "path"]),
);
assert!(
env["path"].as_str().unwrap().ends_with("config.toml"),
"{env}"
);
}
#[test] #[test]
fn config_set_and_get_ownership_db_path_round_trip() { fn config_set_and_get_ownership_db_path_round_trip() {
let root = test_env_dir("config-ownership-db-path"); let root = test_env_dir("config-ownership-db-path");

View file

@ -1,4 +1,4 @@
exit: 0 exit: 0
--- stdout --- stdout
{"key":"general.provider","value":"msn"} msn
--- stderr --- stderr

View file

@ -1,4 +1,4 @@
exit: 0 exit: 0
--- stdout --- stdout
{"path":"<ROOT>/config/idx/config.toml"} <ROOT>/config/idx/config.toml
--- stderr --- stderr

View file

@ -1,4 +1,4 @@
exit: 0 exit: 0
--- stdout --- stdout
{"path":"<ROOT>/config/idx/config.toml"} <ROOT>/config/idx/config.toml
--- stderr --- stderr

View file

@ -1,4 +1,4 @@
exit: 0 exit: 0
--- stdout --- stdout
{"key":"general.output","ok":true,"value":"jsonl"} ok
--- stderr --- stderr

View file

@ -1,4 +1,4 @@
exit: 0 exit: 0
--- stdout --- stdout
{"key":"general.provider","ok":true,"value":"yahoo"} ok
--- stderr --- stderr

View file

@ -22,6 +22,10 @@ idx config get cache.quote_ttl
idx config path idx config path
``` ```
`idx config` prints plain text, even when piped, so `$(idx config path)` works
in scripts. Ask for JSON with `-o json`, `IDX_OUTPUT=json`, or
`output = "json"`.
```toml ```toml
[general] [general]
provider = "msn" # msn | yahoo provider = "msn" # msn | yahoo