mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[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:
committed by
GitHub
Unverified
parent
c8e5db16c9
commit
cb63ee7f5d
@@ -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,
|
||||
|
||||
@@ -4,6 +4,8 @@ use codex_config::MarketplaceConfigUpdate;
|
||||
use codex_config::record_user_marketplace;
|
||||
use predicates::prelude::PredicateBooleanExt;
|
||||
use predicates::str::contains;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -489,6 +491,103 @@ async fn plugin_list_prints_plugins_in_a_table() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_json_prints_available_plugins_when_requested() -> Result<()> {
|
||||
let (codex_home, source) = setup_local_marketplace()?;
|
||||
let plugin_path = source.path().join("plugins").join("sample");
|
||||
|
||||
let assert = codex_command(codex_home.path())?
|
||||
.args(["plugin", "list", "--available", "--json"])
|
||||
.assert()
|
||||
.success();
|
||||
let stdout = assert.get_output().stdout.as_slice();
|
||||
let actual: serde_json::Value = serde_json::from_slice(stdout)?;
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
json!({
|
||||
"installed": [],
|
||||
"available": [
|
||||
{
|
||||
"pluginId": "sample@debug",
|
||||
"name": "sample",
|
||||
"marketplaceName": "debug",
|
||||
"version": "1.2.3",
|
||||
"installed": false,
|
||||
"enabled": false,
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": plugin_path.display().to_string(),
|
||||
},
|
||||
"installPolicy": "AVAILABLE",
|
||||
"authPolicy": "ON_INSTALL",
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_json_prints_installed_plugins() -> Result<()> {
|
||||
let (codex_home, source) = setup_local_marketplace()?;
|
||||
let plugin_path = source.path().join("plugins").join("sample");
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "add", "sample@debug"])
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let assert = codex_command(codex_home.path())?
|
||||
.args(["plugin", "list", "--json"])
|
||||
.assert()
|
||||
.success();
|
||||
let stdout = assert.get_output().stdout.as_slice();
|
||||
let actual: serde_json::Value = serde_json::from_slice(stdout)?;
|
||||
|
||||
assert_eq!(
|
||||
actual,
|
||||
json!({
|
||||
"installed": [
|
||||
{
|
||||
"pluginId": "sample@debug",
|
||||
"name": "sample",
|
||||
"marketplaceName": "debug",
|
||||
"version": "1.2.3",
|
||||
"installed": true,
|
||||
"enabled": true,
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": plugin_path.display().to_string(),
|
||||
},
|
||||
"installPolicy": "AVAILABLE",
|
||||
"authPolicy": "ON_INSTALL",
|
||||
},
|
||||
],
|
||||
"available": [],
|
||||
})
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_available_requires_json() -> Result<()> {
|
||||
let (codex_home, _source) = setup_local_marketplace()?;
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "list", "--available"])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(contains(
|
||||
"the following required arguments were not provided",
|
||||
))
|
||||
.stderr(contains("--json"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_shows_installed_version_when_plugin_is_installed() -> Result<()> {
|
||||
let (codex_home, _source) = setup_local_marketplace()?;
|
||||
|
||||
Reference in New Issue
Block a user