mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
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:
1 parent
055c7a7c53
commit
bb7d19bc24
6 files changed
+438
-9
No files matched your search
@@ -7,11 +7,14 @@ use codex_core::config::find_codex_home;
|
|||||||
use codex_core_plugins::PluginMarketplaceUpgradeOutcome;
|
use codex_core_plugins::PluginMarketplaceUpgradeOutcome;
|
||||||
use codex_core_plugins::PluginsManager;
|
use codex_core_plugins::PluginsManager;
|
||||||
use codex_core_plugins::marketplace::marketplace_root_dir;
|
use codex_core_plugins::marketplace::marketplace_root_dir;
|
||||||
|
use codex_core_plugins::marketplace_add::MarketplaceAddOutcome;
|
||||||
use codex_core_plugins::marketplace_add::MarketplaceAddRequest;
|
use codex_core_plugins::marketplace_add::MarketplaceAddRequest;
|
||||||
use codex_core_plugins::marketplace_add::add_marketplace;
|
use codex_core_plugins::marketplace_add::add_marketplace;
|
||||||
|
use codex_core_plugins::marketplace_remove::MarketplaceRemoveOutcome;
|
||||||
use codex_core_plugins::marketplace_remove::MarketplaceRemoveRequest;
|
use codex_core_plugins::marketplace_remove::MarketplaceRemoveRequest;
|
||||||
use codex_core_plugins::marketplace_remove::remove_marketplace;
|
use codex_core_plugins::marketplace_remove::remove_marketplace;
|
||||||
use codex_utils_cli::CliConfigOverrides;
|
use codex_utils_cli::CliConfigOverrides;
|
||||||
|
use serde::Serialize;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use crate::plugin_cmd::configured_marketplace_snapshot_issues;
|
use crate::plugin_cmd::configured_marketplace_snapshot_issues;
|
||||||
@@ -32,7 +35,7 @@ enum MarketplaceSubcommand {
|
|||||||
Add(AddMarketplaceArgs),
|
Add(AddMarketplaceArgs),
|
||||||
|
|
||||||
/// List plugin marketplaces Codex is currently considering and their roots.
|
/// List plugin marketplaces Codex is currently considering and their roots.
|
||||||
List,
|
List(ListMarketplaceArgs),
|
||||||
|
|
||||||
/// Refresh configured Git marketplace snapshots.
|
/// Refresh configured Git marketplace snapshots.
|
||||||
///
|
///
|
||||||
@@ -64,6 +67,18 @@ struct AddMarketplaceArgs {
|
|||||||
action = clap::ArgAction::Append
|
action = clap::ArgAction::Append
|
||||||
)]
|
)]
|
||||||
sparse_paths: Vec<String>,
|
sparse_paths: Vec<String>,
|
||||||
|
|
||||||
|
/// Output add result as JSON.
|
||||||
|
#[arg(long = "json")]
|
||||||
|
json: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(bin_name = "codex plugin marketplace list")]
|
||||||
|
struct ListMarketplaceArgs {
|
||||||
|
/// Output marketplace list as JSON.
|
||||||
|
#[arg(long = "json")]
|
||||||
|
json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
@@ -75,6 +90,10 @@ struct UpgradeMarketplaceArgs {
|
|||||||
/// Optional configured marketplace name to upgrade. Omit to upgrade all Git marketplaces.
|
/// Optional configured marketplace name to upgrade. Omit to upgrade all Git marketplaces.
|
||||||
#[arg(value_name = "MARKETPLACE_NAME")]
|
#[arg(value_name = "MARKETPLACE_NAME")]
|
||||||
marketplace_name: Option<String>,
|
marketplace_name: Option<String>,
|
||||||
|
|
||||||
|
/// Output upgrade result as JSON.
|
||||||
|
#[arg(long = "json")]
|
||||||
|
json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
@@ -86,6 +105,10 @@ struct RemoveMarketplaceArgs {
|
|||||||
/// Configured marketplace name to remove.
|
/// Configured marketplace name to remove.
|
||||||
#[arg(value_name = "MARKETPLACE_NAME")]
|
#[arg(value_name = "MARKETPLACE_NAME")]
|
||||||
marketplace_name: String,
|
marketplace_name: String,
|
||||||
|
|
||||||
|
/// Output remove result as JSON.
|
||||||
|
#[arg(long = "json")]
|
||||||
|
json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MarketplaceCli {
|
impl MarketplaceCli {
|
||||||
@@ -101,7 +124,7 @@ impl MarketplaceCli {
|
|||||||
|
|
||||||
match subcommand {
|
match subcommand {
|
||||||
MarketplaceSubcommand::Add(args) => run_add(args).await?,
|
MarketplaceSubcommand::Add(args) => run_add(args).await?,
|
||||||
MarketplaceSubcommand::List => run_list(overrides).await?,
|
MarketplaceSubcommand::List(args) => run_list(overrides, args).await?,
|
||||||
MarketplaceSubcommand::Upgrade(args) => run_upgrade(overrides, args).await?,
|
MarketplaceSubcommand::Upgrade(args) => run_upgrade(overrides, args).await?,
|
||||||
MarketplaceSubcommand::Remove(args) => run_remove(args).await?,
|
MarketplaceSubcommand::Remove(args) => run_remove(args).await?,
|
||||||
}
|
}
|
||||||
@@ -115,6 +138,7 @@ async fn run_add(args: AddMarketplaceArgs) -> Result<()> {
|
|||||||
source,
|
source,
|
||||||
ref_name,
|
ref_name,
|
||||||
sparse_paths,
|
sparse_paths,
|
||||||
|
json,
|
||||||
} = args;
|
} = args;
|
||||||
|
|
||||||
let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
|
let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
|
||||||
@@ -128,6 +152,12 @@ async fn run_add(args: AddMarketplaceArgs) -> Result<()> {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
if json {
|
||||||
|
let output = JsonMarketplaceAddOutput::from_outcome(outcome);
|
||||||
|
println!("{}", serde_json::to_string_pretty(&output)?);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if outcome.already_added {
|
if outcome.already_added {
|
||||||
println!(
|
println!(
|
||||||
"Marketplace `{}` is already added from {}.",
|
"Marketplace `{}` is already added from {}.",
|
||||||
@@ -147,7 +177,25 @@ async fn run_add(args: AddMarketplaceArgs) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_list(overrides: Vec<(String, toml::Value)>) -> Result<()> {
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct JsonMarketplaceAddOutput {
|
||||||
|
marketplace_name: String,
|
||||||
|
installed_root: String,
|
||||||
|
already_added: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JsonMarketplaceAddOutput {
|
||||||
|
fn from_outcome(outcome: MarketplaceAddOutcome) -> Self {
|
||||||
|
Self {
|
||||||
|
marketplace_name: outcome.marketplace_name,
|
||||||
|
installed_root: outcome.installed_root.as_path().display().to_string(),
|
||||||
|
already_added: outcome.already_added,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_list(overrides: Vec<(String, toml::Value)>, args: ListMarketplaceArgs) -> Result<()> {
|
||||||
let config = Config::load_with_cli_overrides(overrides)
|
let config = Config::load_with_cli_overrides(overrides)
|
||||||
.await
|
.await
|
||||||
.context("failed to load configuration")?;
|
.context("failed to load configuration")?;
|
||||||
@@ -191,6 +239,12 @@ async fn run_list(overrides: Vec<(String, toml::Value)>) -> Result<()> {
|
|||||||
bail!("failed to load marketplace(s):\n{issue_lines}");
|
bail!("failed to load marketplace(s):\n{issue_lines}");
|
||||||
}
|
}
|
||||||
let marketplaces = marketplace_listing.marketplaces;
|
let marketplaces = marketplace_listing.marketplaces;
|
||||||
|
if args.json {
|
||||||
|
let output = JsonMarketplaceListOutput::from_marketplaces(marketplaces);
|
||||||
|
println!("{}", serde_json::to_string_pretty(&output)?);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if marketplaces.is_empty() {
|
if marketplaces.is_empty() {
|
||||||
println!("No plugin marketplaces in scope.");
|
println!("No plugin marketplaces in scope.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -227,11 +281,48 @@ async fn run_list(overrides: Vec<(String, toml::Value)>) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct JsonMarketplaceListOutput {
|
||||||
|
marketplaces: Vec<JsonMarketplaceListEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JsonMarketplaceListOutput {
|
||||||
|
fn from_marketplaces(marketplaces: Vec<codex_core_plugins::marketplace::Marketplace>) -> Self {
|
||||||
|
let mut seen_roots = HashSet::new();
|
||||||
|
let marketplaces = marketplaces
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|marketplace| {
|
||||||
|
let root = marketplace_root_dir(&marketplace.path).ok()?;
|
||||||
|
if !seen_roots.insert(root.clone()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(JsonMarketplaceListEntry {
|
||||||
|
name: marketplace.name,
|
||||||
|
root: root.display().to_string(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self { marketplaces }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct JsonMarketplaceListEntry {
|
||||||
|
name: String,
|
||||||
|
root: String,
|
||||||
|
}
|
||||||
|
|
||||||
async fn run_upgrade(
|
async fn run_upgrade(
|
||||||
overrides: Vec<(String, toml::Value)>,
|
overrides: Vec<(String, toml::Value)>,
|
||||||
args: UpgradeMarketplaceArgs,
|
args: UpgradeMarketplaceArgs,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let UpgradeMarketplaceArgs { marketplace_name } = args;
|
let UpgradeMarketplaceArgs {
|
||||||
|
marketplace_name,
|
||||||
|
json,
|
||||||
|
} = args;
|
||||||
let config = Config::load_with_cli_overrides(overrides)
|
let config = Config::load_with_cli_overrides(overrides)
|
||||||
.await
|
.await
|
||||||
.context("failed to load configuration")?;
|
.context("failed to load configuration")?;
|
||||||
@@ -241,11 +332,18 @@ async fn run_upgrade(
|
|||||||
let outcome = manager
|
let outcome = manager
|
||||||
.upgrade_configured_marketplaces_for_config(&plugins_input, marketplace_name.as_deref())
|
.upgrade_configured_marketplaces_for_config(&plugins_input, marketplace_name.as_deref())
|
||||||
.map_err(anyhow::Error::msg)?;
|
.map_err(anyhow::Error::msg)?;
|
||||||
print_upgrade_outcome(&outcome, marketplace_name.as_deref())
|
if json {
|
||||||
|
print_upgrade_outcome_json(&outcome)
|
||||||
|
} else {
|
||||||
|
print_upgrade_outcome(&outcome, marketplace_name.as_deref())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_remove(args: RemoveMarketplaceArgs) -> Result<()> {
|
async fn run_remove(args: RemoveMarketplaceArgs) -> Result<()> {
|
||||||
let RemoveMarketplaceArgs { marketplace_name } = args;
|
let RemoveMarketplaceArgs {
|
||||||
|
marketplace_name,
|
||||||
|
json,
|
||||||
|
} = args;
|
||||||
let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
|
let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
|
||||||
let outcome = remove_marketplace(
|
let outcome = remove_marketplace(
|
||||||
codex_home.to_path_buf(),
|
codex_home.to_path_buf(),
|
||||||
@@ -253,6 +351,12 @@ async fn run_remove(args: RemoveMarketplaceArgs) -> Result<()> {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
if json {
|
||||||
|
let output = JsonMarketplaceRemoveOutput::from_outcome(outcome);
|
||||||
|
println!("{}", serde_json::to_string_pretty(&output)?);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
println!("Removed marketplace `{}`.", outcome.marketplace_name);
|
println!("Removed marketplace `{}`.", outcome.marketplace_name);
|
||||||
if let Some(installed_root) = outcome.removed_installed_root {
|
if let Some(installed_root) = outcome.removed_installed_root {
|
||||||
println!(
|
println!(
|
||||||
@@ -264,6 +368,76 @@ async fn run_remove(args: RemoveMarketplaceArgs) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct JsonMarketplaceRemoveOutput {
|
||||||
|
marketplace_name: String,
|
||||||
|
installed_root: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JsonMarketplaceRemoveOutput {
|
||||||
|
fn from_outcome(outcome: MarketplaceRemoveOutcome) -> Self {
|
||||||
|
Self {
|
||||||
|
marketplace_name: outcome.marketplace_name,
|
||||||
|
installed_root: outcome
|
||||||
|
.removed_installed_root
|
||||||
|
.map(|root| root.as_path().display().to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_upgrade_outcome_json(outcome: &PluginMarketplaceUpgradeOutcome) -> Result<()> {
|
||||||
|
for error in &outcome.errors {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to upgrade marketplace `{}`: {}",
|
||||||
|
error.marketplace_name, error.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !outcome.all_succeeded() {
|
||||||
|
bail!("{} upgrade failure(s) occurred.", outcome.errors.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = JsonMarketplaceUpgradeOutput::from_outcome(outcome);
|
||||||
|
println!("{}", serde_json::to_string_pretty(&output)?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct JsonMarketplaceUpgradeOutput {
|
||||||
|
selected_marketplaces: Vec<String>,
|
||||||
|
upgraded_roots: Vec<String>,
|
||||||
|
errors: Vec<JsonMarketplaceUpgradeError>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JsonMarketplaceUpgradeOutput {
|
||||||
|
fn from_outcome(outcome: &PluginMarketplaceUpgradeOutcome) -> Self {
|
||||||
|
Self {
|
||||||
|
selected_marketplaces: outcome.selected_marketplaces.clone(),
|
||||||
|
upgraded_roots: outcome
|
||||||
|
.upgraded_roots
|
||||||
|
.iter()
|
||||||
|
.map(|root| root.display().to_string())
|
||||||
|
.collect(),
|
||||||
|
errors: outcome
|
||||||
|
.errors
|
||||||
|
.iter()
|
||||||
|
.map(|error| JsonMarketplaceUpgradeError {
|
||||||
|
marketplace_name: error.marketplace_name.clone(),
|
||||||
|
message: error.message.clone(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct JsonMarketplaceUpgradeError {
|
||||||
|
marketplace_name: String,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
fn print_upgrade_outcome(
|
fn print_upgrade_outcome(
|
||||||
outcome: &PluginMarketplaceUpgradeOutcome,
|
outcome: &PluginMarketplaceUpgradeOutcome,
|
||||||
marketplace_name: Option<&str>,
|
marketplace_name: Option<&str>,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use codex_core::config::Config;
|
|||||||
use codex_core::config::find_codex_home;
|
use codex_core::config::find_codex_home;
|
||||||
use codex_core_plugins::ConfiguredMarketplace;
|
use codex_core_plugins::ConfiguredMarketplace;
|
||||||
use codex_core_plugins::OPENAI_BUNDLED_MARKETPLACE_NAME;
|
use codex_core_plugins::OPENAI_BUNDLED_MARKETPLACE_NAME;
|
||||||
|
use codex_core_plugins::PluginInstallOutcome;
|
||||||
use codex_core_plugins::PluginInstallRequest;
|
use codex_core_plugins::PluginInstallRequest;
|
||||||
use codex_core_plugins::PluginsConfigInput;
|
use codex_core_plugins::PluginsConfigInput;
|
||||||
use codex_core_plugins::PluginsManager;
|
use codex_core_plugins::PluginsManager;
|
||||||
@@ -73,6 +74,10 @@ pub struct AddPluginArgs {
|
|||||||
/// Configured marketplace name to use when PLUGIN does not include @MARKETPLACE.
|
/// Configured marketplace name to use when PLUGIN does not include @MARKETPLACE.
|
||||||
#[arg(long = "marketplace", short = 'm', value_name = "MARKETPLACE")]
|
#[arg(long = "marketplace", short = 'm', value_name = "MARKETPLACE")]
|
||||||
marketplace_name: Option<String>,
|
marketplace_name: Option<String>,
|
||||||
|
|
||||||
|
/// Output install result as JSON.
|
||||||
|
#[arg(long = "json")]
|
||||||
|
json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
@@ -107,6 +112,10 @@ pub struct RemovePluginArgs {
|
|||||||
/// Marketplace name to use when PLUGIN does not include @MARKETPLACE.
|
/// Marketplace name to use when PLUGIN does not include @MARKETPLACE.
|
||||||
#[arg(long = "marketplace", short = 'm', value_name = "MARKETPLACE")]
|
#[arg(long = "marketplace", short = 'm', value_name = "MARKETPLACE")]
|
||||||
marketplace_name: Option<String>,
|
marketplace_name: Option<String>,
|
||||||
|
|
||||||
|
/// Output remove result as JSON.
|
||||||
|
#[arg(long = "json")]
|
||||||
|
json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_plugin_add(
|
pub async fn run_plugin_add(
|
||||||
@@ -118,11 +127,16 @@ pub async fn run_plugin_add(
|
|||||||
plugins_input,
|
plugins_input,
|
||||||
manager,
|
manager,
|
||||||
} = load_plugin_command_context(overrides).await?;
|
} = load_plugin_command_context(overrides).await?;
|
||||||
|
let AddPluginArgs {
|
||||||
|
plugin,
|
||||||
|
marketplace_name,
|
||||||
|
json,
|
||||||
|
} = args;
|
||||||
let PluginSelection {
|
let PluginSelection {
|
||||||
plugin_name,
|
plugin_name,
|
||||||
marketplace_name,
|
marketplace_name,
|
||||||
..
|
..
|
||||||
} = parse_plugin_selection(args.plugin, args.marketplace_name)?;
|
} = parse_plugin_selection(plugin, marketplace_name)?;
|
||||||
let marketplace = find_marketplace_for_plugin(
|
let marketplace = find_marketplace_for_plugin(
|
||||||
&manager,
|
&manager,
|
||||||
codex_home.as_path(),
|
codex_home.as_path(),
|
||||||
@@ -137,6 +151,12 @@ pub async fn run_plugin_add(
|
|||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
if json {
|
||||||
|
let output = JsonPluginAddOutput::from_outcome(outcome);
|
||||||
|
println!("{}", serde_json::to_string_pretty(&output)?);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"Added plugin `{}` from marketplace `{}`.",
|
"Added plugin `{}` from marketplace `{}`.",
|
||||||
outcome.plugin_id.plugin_name, outcome.plugin_id.marketplace_name
|
outcome.plugin_id.plugin_name, outcome.plugin_id.marketplace_name
|
||||||
@@ -149,6 +169,30 @@ pub async fn run_plugin_add(
|
|||||||
Ok(())
|
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(
|
pub async fn run_plugin_list(
|
||||||
overrides: Vec<(String, toml::Value)>,
|
overrides: Vec<(String, toml::Value)>,
|
||||||
args: ListPluginsArgs,
|
args: ListPluginsArgs,
|
||||||
@@ -449,9 +493,22 @@ pub async fn run_plugin_remove(
|
|||||||
args: RemovePluginArgs,
|
args: RemovePluginArgs,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let PluginCommandContext { manager, .. } = load_plugin_command_context(overrides).await?;
|
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!(
|
println!(
|
||||||
"Removed plugin `{}` from marketplace `{}`.",
|
"Removed plugin `{}` from marketplace `{}`.",
|
||||||
selection.plugin_name, selection.marketplace_name
|
selection.plugin_name, selection.marketplace_name
|
||||||
@@ -460,6 +517,24 @@ pub async fn run_plugin_remove(
|
|||||||
Ok(())
|
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 {
|
struct PluginCommandContext {
|
||||||
codex_home: PathBuf,
|
codex_home: PathBuf,
|
||||||
plugins_input: PluginsConfigInput,
|
plugins_input: PluginsConfigInput,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use codex_config::CONFIG_TOML_FILE;
|
use codex_config::CONFIG_TOML_FILE;
|
||||||
use codex_core_plugins::installed_marketplaces::marketplace_install_root;
|
use codex_core_plugins::installed_marketplaces::marketplace_install_root;
|
||||||
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use predicates::str::contains;
|
use predicates::str::contains;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::json;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
@@ -70,6 +72,41 @@ async fn marketplace_add_local_directory_source() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn marketplace_add_json_prints_add_outcome() -> Result<()> {
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
let source = TempDir::new()?;
|
||||||
|
write_marketplace_source(source.path(), "local ref")?;
|
||||||
|
let source_parent = source.path().parent().unwrap();
|
||||||
|
let source_arg = format!("./{}", source.path().file_name().unwrap().to_string_lossy());
|
||||||
|
|
||||||
|
let assert = codex_command(codex_home.path())?
|
||||||
|
.current_dir(source_parent)
|
||||||
|
.args([
|
||||||
|
"plugin",
|
||||||
|
"marketplace",
|
||||||
|
"add",
|
||||||
|
source_arg.as_str(),
|
||||||
|
"--json",
|
||||||
|
])
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
let stdout = assert.get_output().stdout.as_slice();
|
||||||
|
let actual: serde_json::Value = serde_json::from_slice(stdout)?;
|
||||||
|
let expected_installed_root = AbsolutePathBuf::try_from(source.path().canonicalize()?)?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
actual,
|
||||||
|
json!({
|
||||||
|
"marketplaceName": "debug",
|
||||||
|
"installedRoot": expected_installed_root.as_path().display().to_string(),
|
||||||
|
"alreadyAdded": false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn marketplace_add_rejects_local_manifest_file_source() -> Result<()> {
|
async fn marketplace_add_rejects_local_manifest_file_source() -> Result<()> {
|
||||||
let codex_home = TempDir::new()?;
|
let codex_home = TempDir::new()?;
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ use anyhow::Result;
|
|||||||
use codex_config::MarketplaceConfigUpdate;
|
use codex_config::MarketplaceConfigUpdate;
|
||||||
use codex_config::record_user_marketplace;
|
use codex_config::record_user_marketplace;
|
||||||
use codex_core_plugins::installed_marketplaces::marketplace_install_root;
|
use codex_core_plugins::installed_marketplaces::marketplace_install_root;
|
||||||
|
use codex_utils_absolute_path::canonicalize_existing_preserving_symlinks;
|
||||||
use predicates::str::contains;
|
use predicates::str::contains;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::json;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
@@ -54,6 +57,32 @@ async fn marketplace_remove_deletes_config_and_installed_root() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn marketplace_remove_json_prints_remove_outcome() -> Result<()> {
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
record_user_marketplace(codex_home.path(), "debug", &configured_marketplace_update())?;
|
||||||
|
write_installed_marketplace(codex_home.path(), "debug")?;
|
||||||
|
let installed_root = marketplace_install_root(codex_home.path()).join("debug");
|
||||||
|
let normalized_installed_root = canonicalize_existing_preserving_symlinks(&installed_root)?;
|
||||||
|
|
||||||
|
let assert = codex_command(codex_home.path())?
|
||||||
|
.args(["plugin", "marketplace", "remove", "debug", "--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!({
|
||||||
|
"marketplaceName": "debug",
|
||||||
|
"installedRoot": normalized_installed_root.display().to_string(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn marketplace_remove_rejects_unknown_marketplace() -> Result<()> {
|
async fn marketplace_remove_rejects_unknown_marketplace() -> Result<()> {
|
||||||
let codex_home = TempDir::new()?;
|
let codex_home = TempDir::new()?;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use predicates::str::contains;
|
use predicates::str::contains;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::json;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
@@ -22,6 +24,29 @@ async fn marketplace_upgrade_runs_under_plugin() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn marketplace_upgrade_json_prints_upgrade_outcome() -> Result<()> {
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
|
||||||
|
let assert = codex_command(codex_home.path())?
|
||||||
|
.args(["plugin", "marketplace", "upgrade", "--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!({
|
||||||
|
"selectedMarketplaces": [],
|
||||||
|
"upgradedRoots": [],
|
||||||
|
"errors": [],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn marketplace_upgrade_no_longer_runs_at_top_level() -> Result<()> {
|
async fn marketplace_upgrade_no_longer_runs_at_top_level() -> Result<()> {
|
||||||
let codex_home = TempDir::new()?;
|
let codex_home = TempDir::new()?;
|
||||||
|
|||||||
@@ -338,6 +338,32 @@ async fn marketplace_list_shows_configured_marketplace_names() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn marketplace_list_json_prints_configured_marketplaces() -> Result<()> {
|
||||||
|
let (codex_home, source) = setup_local_marketplace()?;
|
||||||
|
|
||||||
|
let assert = codex_command(codex_home.path())?
|
||||||
|
.args(["plugin", "marketplace", "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!({
|
||||||
|
"marketplaces": [
|
||||||
|
{
|
||||||
|
"name": "debug",
|
||||||
|
"root": source.path().display().to_string(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn marketplace_list_includes_home_marketplace_when_present() -> Result<()> {
|
async fn marketplace_list_includes_home_marketplace_when_present() -> Result<()> {
|
||||||
let codex_home = TempDir::new()?;
|
let codex_home = TempDir::new()?;
|
||||||
@@ -802,6 +828,69 @@ async fn plugin_add_and_remove_updates_installed_plugin_config() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn plugin_add_json_prints_install_outcome() -> Result<()> {
|
||||||
|
let (codex_home, _source) = setup_local_marketplace()?;
|
||||||
|
|
||||||
|
let assert = codex_command(codex_home.path())?
|
||||||
|
.args(["plugin", "add", "sample@debug", "--json"])
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
let stdout = assert.get_output().stdout.as_slice();
|
||||||
|
let actual: serde_json::Value = serde_json::from_slice(stdout)?;
|
||||||
|
let installed_path = codex_home.path().join("plugins/cache/debug/sample/1.2.3");
|
||||||
|
let normalized_installed_path = canonicalize_existing_preserving_symlinks(&installed_path)?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
actual,
|
||||||
|
json!({
|
||||||
|
"pluginId": "sample@debug",
|
||||||
|
"name": "sample",
|
||||||
|
"marketplaceName": "debug",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"installedPath": normalized_installed_path.display().to_string(),
|
||||||
|
"authPolicy": "ON_INSTALL",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn plugin_remove_json_prints_remove_outcome() -> Result<()> {
|
||||||
|
let (codex_home, _source) = setup_local_marketplace()?;
|
||||||
|
|
||||||
|
codex_command(codex_home.path())?
|
||||||
|
.args(["plugin", "add", "sample@debug"])
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
|
||||||
|
let assert = codex_command(codex_home.path())?
|
||||||
|
.args([
|
||||||
|
"plugin",
|
||||||
|
"remove",
|
||||||
|
"sample",
|
||||||
|
"--marketplace",
|
||||||
|
"debug",
|
||||||
|
"--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!({
|
||||||
|
"pluginId": "sample@debug",
|
||||||
|
"name": "sample",
|
||||||
|
"marketplaceName": "debug",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn plugin_add_rejects_unconfigured_repo_local_marketplaces() -> Result<()> {
|
async fn plugin_add_rejects_unconfigured_repo_local_marketplaces() -> Result<()> {
|
||||||
let (codex_home, source) = setup_unconfigured_local_marketplace()?;
|
let (codex_home, source) = setup_unconfigured_local_marketplace()?;
|
||||||
|
|||||||
Reference in new issue
Block a user