Add JSON output for plugin subcommands (#26631)

## Summary
- Follow-up to #25330 and #26417
- Add `--json` output for `codex plugin add` and `codex plugin remove`
- Add `--json` output for `codex plugin marketplace
add/list/upgrade/remove`
- Keep existing human-readable output unchanged
- Keep existing error handling/stderr behavior unchanged; `--json`
changes successful stdout output only
- Align marketplace add/remove JSON field names with the existing
app-server protocol shape
- Add CLI coverage for plugin and marketplace JSON outputs

## Validation
- `just fmt`
- `just fix -p codex-cli`
- `just test -p codex-cli`
This commit is contained in:
mpc-oai
2026-06-05 14:40:31 -05:00
committed by GitHub
Unverified
parent 055c7a7c53
commit bb7d19bc24
6 changed files with 438 additions and 9 deletions
+78 -3
View File
@@ -6,6 +6,7 @@ use codex_core::config::Config;
use codex_core::config::find_codex_home;
use codex_core_plugins::ConfiguredMarketplace;
use codex_core_plugins::OPENAI_BUNDLED_MARKETPLACE_NAME;
use codex_core_plugins::PluginInstallOutcome;
use codex_core_plugins::PluginInstallRequest;
use codex_core_plugins::PluginsConfigInput;
use codex_core_plugins::PluginsManager;
@@ -73,6 +74,10 @@ pub struct AddPluginArgs {
/// Configured marketplace name to use when PLUGIN does not include @MARKETPLACE.
#[arg(long = "marketplace", short = 'm', value_name = "MARKETPLACE")]
marketplace_name: Option<String>,
/// Output install result as JSON.
#[arg(long = "json")]
json: bool,
}
#[derive(Debug, Parser)]
@@ -107,6 +112,10 @@ pub struct RemovePluginArgs {
/// Marketplace name to use when PLUGIN does not include @MARKETPLACE.
#[arg(long = "marketplace", short = 'm', value_name = "MARKETPLACE")]
marketplace_name: Option<String>,
/// Output remove result as JSON.
#[arg(long = "json")]
json: bool,
}
pub async fn run_plugin_add(
@@ -118,11 +127,16 @@ pub async fn run_plugin_add(
plugins_input,
manager,
} = load_plugin_command_context(overrides).await?;
let AddPluginArgs {
plugin,
marketplace_name,
json,
} = args;
let PluginSelection {
plugin_name,
marketplace_name,
..
} = parse_plugin_selection(args.plugin, args.marketplace_name)?;
} = parse_plugin_selection(plugin, marketplace_name)?;
let marketplace = find_marketplace_for_plugin(
&manager,
codex_home.as_path(),
@@ -137,6 +151,12 @@ pub async fn run_plugin_add(
})
.await?;
if json {
let output = JsonPluginAddOutput::from_outcome(outcome);
println!("{}", serde_json::to_string_pretty(&output)?);
return Ok(());
}
println!(
"Added plugin `{}` from marketplace `{}`.",
outcome.plugin_id.plugin_name, outcome.plugin_id.marketplace_name
@@ -149,6 +169,30 @@ pub async fn run_plugin_add(
Ok(())
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonPluginAddOutput {
plugin_id: String,
name: String,
marketplace_name: String,
version: String,
installed_path: String,
auth_policy: &'static str,
}
impl JsonPluginAddOutput {
fn from_outcome(outcome: PluginInstallOutcome) -> Self {
Self {
plugin_id: outcome.plugin_id.as_key(),
name: outcome.plugin_id.plugin_name,
marketplace_name: outcome.plugin_id.marketplace_name,
version: outcome.plugin_version,
installed_path: outcome.installed_path.as_path().display().to_string(),
auth_policy: auth_policy_label(outcome.auth_policy),
}
}
}
pub async fn run_plugin_list(
overrides: Vec<(String, toml::Value)>,
args: ListPluginsArgs,
@@ -449,9 +493,22 @@ pub async fn run_plugin_remove(
args: RemovePluginArgs,
) -> Result<()> {
let PluginCommandContext { manager, .. } = load_plugin_command_context(overrides).await?;
let selection = parse_plugin_selection(args.plugin, args.marketplace_name)?;
let RemovePluginArgs {
plugin,
marketplace_name,
json,
} = args;
let selection = parse_plugin_selection(plugin, marketplace_name)?;
manager
.uninstall_plugin(selection.plugin_key.clone())
.await?;
if json {
let output = JsonPluginRemoveOutput::from_selection(selection);
println!("{}", serde_json::to_string_pretty(&output)?);
return Ok(());
}
manager.uninstall_plugin(selection.plugin_key).await?;
println!(
"Removed plugin `{}` from marketplace `{}`.",
selection.plugin_name, selection.marketplace_name
@@ -460,6 +517,24 @@ pub async fn run_plugin_remove(
Ok(())
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonPluginRemoveOutput {
plugin_id: String,
name: String,
marketplace_name: String,
}
impl JsonPluginRemoveOutput {
fn from_selection(selection: PluginSelection) -> Self {
Self {
plugin_id: selection.plugin_key,
name: selection.plugin_name,
marketplace_name: selection.marketplace_name,
}
}
}
struct PluginCommandContext {
codex_home: PathBuf,
plugins_input: PluginsConfigInput,