[codex] Add plugin list JSON output (#25330)

## Summary
- add `--json` output to `codex plugin list` with `installed` and
`available` arrays
- add `--available` for JSON output only; using it without `--json` is
rejected
- keep the existing non-JSON table output unchanged
- add CLI coverage for JSON installed/available output and the
`--available`/`--json` requirement

## Validation
- `just test -p codex-cli plugin_list`
- `just fix -p codex-cli`
- `git diff --check`

Note: `just fmt` ran Rust formatting first, then failed in the Python
ruff step because `openai-codex-cli-bin==0.132.0` has no wheel for this
Linux platform.
This commit is contained in:
xl-openai
2026-06-01 21:27:06 -07:00
committed by GitHub
Unverified
parent c8e5db16c9
commit cb63ee7f5d
2 changed files with 250 additions and 1 deletions
+151 -1
View File
@@ -12,10 +12,14 @@ use codex_core_plugins::PluginsManager;
use codex_core_plugins::installed_marketplaces::marketplace_install_root;
use codex_core_plugins::installed_marketplaces::resolve_configured_marketplace_root;
use codex_core_plugins::marketplace::MarketplaceListError;
use codex_core_plugins::marketplace::MarketplacePluginAuthPolicy;
use codex_core_plugins::marketplace::MarketplacePluginInstallPolicy;
use codex_core_plugins::marketplace::MarketplacePluginSource;
use codex_core_plugins::marketplace::find_marketplace_manifest_path;
use codex_plugin::PluginId;
use codex_plugin::validate_plugin_segment;
use codex_utils_cli::CliConfigOverrides;
use serde::Serialize;
use std::path::Path;
use std::path::PathBuf;
@@ -73,12 +77,20 @@ pub struct AddPluginArgs {
#[derive(Debug, Parser)]
#[command(
bin_name = "codex plugin list",
after_help = "Examples:\n codex plugin list\n codex plugin list --marketplace debug"
after_help = "Examples:\n codex plugin list\n codex plugin list --marketplace debug\n codex plugin list --json\n codex plugin list --available --json"
)]
pub struct ListPluginsArgs {
/// Only list plugins from this configured marketplace name.
#[arg(long = "marketplace", short = 'm', value_name = "MARKETPLACE")]
marketplace_name: Option<String>,
/// Output plugin list as JSON.
#[arg(long = "json")]
json: bool,
/// Include uninstalled marketplace plugins in the JSON output.
#[arg(long = "available", requires = "json")]
available: bool,
}
#[derive(Debug, Parser)]
@@ -166,6 +178,12 @@ pub async fn run_plugin_list(
})
.collect::<Vec<_>>();
if args.json {
let output = JsonPluginListOutput::from_marketplaces(marketplaces, args.available);
println!("{}", serde_json::to_string_pretty(&output)?);
return Ok(());
}
if marketplaces.is_empty() {
if let Some(marketplace_name) = args.marketplace_name {
println!("No plugins found in marketplace `{marketplace_name}`.");
@@ -240,6 +258,138 @@ pub async fn run_plugin_list(
Ok(())
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonPluginListOutput {
installed: Vec<JsonPluginListEntry>,
available: Vec<JsonPluginListEntry>,
}
impl JsonPluginListOutput {
fn from_marketplaces(
marketplaces: Vec<codex_core_plugins::ConfiguredMarketplace>,
include_available: bool,
) -> Self {
let mut installed = Vec::new();
let mut available = Vec::new();
for marketplace in marketplaces {
for plugin in marketplace.plugins {
let entry = JsonPluginListEntry::from_configured_plugin(&marketplace.name, plugin);
if entry.installed {
installed.push(entry);
} else if include_available {
available.push(entry);
}
}
}
Self {
installed,
available,
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct JsonPluginListEntry {
plugin_id: String,
name: String,
marketplace_name: String,
version: Option<String>,
installed: bool,
enabled: bool,
source: JsonPluginSource,
install_policy: &'static str,
auth_policy: &'static str,
}
impl JsonPluginListEntry {
fn from_configured_plugin(
marketplace_name: &str,
plugin: codex_core_plugins::ConfiguredMarketplacePlugin,
) -> Self {
let version = plugin.installed_version.or(plugin.local_version);
Self {
plugin_id: plugin.id,
name: plugin.name,
marketplace_name: marketplace_name.to_string(),
version,
installed: plugin.installed,
enabled: plugin.enabled,
source: JsonPluginSource::from_marketplace_source(plugin.source),
install_policy: install_policy_label(plugin.policy.installation),
auth_policy: auth_policy_label(plugin.policy.authentication),
}
}
}
#[derive(Debug, Serialize)]
#[serde(tag = "source", rename_all = "kebab-case")]
enum JsonPluginSource {
Local {
path: String,
},
Git {
url: String,
#[serde(rename = "ref", skip_serializing_if = "Option::is_none")]
ref_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
sha: Option<String>,
},
GitSubdir {
url: String,
path: String,
#[serde(rename = "ref", skip_serializing_if = "Option::is_none")]
ref_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
sha: Option<String>,
},
}
impl JsonPluginSource {
fn from_marketplace_source(source: MarketplacePluginSource) -> Self {
match source {
MarketplacePluginSource::Local { path } => Self::Local {
path: path.as_path().display().to_string(),
},
MarketplacePluginSource::Git {
url,
path: Some(path),
ref_name,
sha,
} => Self::GitSubdir {
url,
path,
ref_name,
sha,
},
MarketplacePluginSource::Git {
url,
path: None,
ref_name,
sha,
} => Self::Git { url, ref_name, sha },
}
}
}
fn install_policy_label(policy: MarketplacePluginInstallPolicy) -> &'static str {
match policy {
MarketplacePluginInstallPolicy::NotAvailable => "NOT_AVAILABLE",
MarketplacePluginInstallPolicy::Available => "AVAILABLE",
MarketplacePluginInstallPolicy::InstalledByDefault => "INSTALLED_BY_DEFAULT",
}
}
fn auth_policy_label(policy: MarketplacePluginAuthPolicy) -> &'static str {
match policy {
MarketplacePluginAuthPolicy::OnInstall => "ON_INSTALL",
MarketplacePluginAuthPolicy::OnUse => "ON_USE",
}
}
pub async fn run_plugin_remove(
overrides: Vec<(String, toml::Value)>,
args: RemovePluginArgs,