use anyhow::Context;use anyhow::Result;use anyhow::bail;use clap::Parser;use codex_core::config::Config;use codex_core::config::find_codex_home;use codex_core_plugins::PluginMarketplaceUpgradeOutcome;use codex_core_plugins::PluginsConfigInput;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::marketplace_root_dir;use codex_core_plugins::marketplace_add::MarketplaceAddOutcome;use codex_core_plugins::marketplace_add::MarketplaceAddRequest;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::remove_marketplace;use codex_utils_cli::CliConfigOverrides;use serde::Serialize;use std::collections::HashMap;use std::collections::HashSet;use std::path::Path;use std::path::PathBuf;use crate::plugin_cmd::JsonMarketplaceSource;use crate::plugin_cmd::configured_marketplace_snapshot_issues;use crate::plugin_cmd::configured_marketplace_sources;use crate::plugin_cmd::load_cli_auth_mode;#[derive(Debug, Parser)]#[command(bin_name = "codex plugin marketplace")]pub struct MarketplaceCli { #[clap(flatten)] pub config_overrides: CliConfigOverrides, #[command(subcommand)] subcommand: MarketplaceSubcommand,}#[derive(Debug, clap::Subcommand)]enum MarketplaceSubcommand { /// Add a local or Git marketplace to the configured marketplace sources. Add(AddMarketplaceArgs), /// List plugin marketplaces Codex is currently considering and their roots. List(ListMarketplaceArgs), /// Refresh configured Git marketplace snapshots. /// /// Omit MARKETPLACE_NAME to upgrade all configured Git marketplaces. Upgrade(UpgradeMarketplaceArgs), /// Remove a configured marketplace source by name. Remove(RemoveMarketplaceArgs),}#[derive(Debug, Parser)]#[command( bin_name = "codex plugin marketplace add", after_help = "Examples:\n codex plugin marketplace add ./path/to/marketplace\n codex plugin marketplace add owner/repo --ref main\n codex plugin marketplace add https://github.com/owner/repo --sparse plugins/foo")]struct AddMarketplaceArgs { /// Marketplace source: a local path, owner/repo[@ref], HTTPS Git URL, or SSH Git URL. #[arg(value_name = "SOURCE")] source: String, /// Git ref to fetch for Git marketplace sources. #[arg(long = "ref", value_name = "REF")] ref_name: Option<String>, /// Sparse checkout path for Git marketplace sources. Can be repeated. #[arg( long = "sparse", value_name = "PATH", action = clap::ArgAction::Append )] 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)]#[command( bin_name = "codex plugin marketplace upgrade", after_help = "Examples:\n codex plugin marketplace upgrade\n codex plugin marketplace upgrade debug")]struct UpgradeMarketplaceArgs { /// Optional configured marketplace name to upgrade. Omit to upgrade all Git marketplaces. #[arg(value_name = "MARKETPLACE_NAME")] marketplace_name: Option<String>, /// Output upgrade result as JSON. #[arg(long = "json")] json: bool,}#[derive(Debug, Parser)]#[command( bin_name = "codex plugin marketplace remove", after_help = "Example:\n codex plugin marketplace remove debug")]struct RemoveMarketplaceArgs { /// Configured marketplace name to remove. #[arg(value_name = "MARKETPLACE_NAME")] marketplace_name: String, /// Output remove result as JSON. #[arg(long = "json")] json: bool,}impl MarketplaceCli { pub async fn run(self) -> Result<()> { let MarketplaceCli { config_overrides, subcommand, } = self; let overrides = config_overrides .parse_overrides() .map_err(anyhow::Error::msg)?; match subcommand { MarketplaceSubcommand::Add(args) => run_add(args).await?, MarketplaceSubcommand::List(args) => run_list(overrides, args).await?, MarketplaceSubcommand::Upgrade(args) => run_upgrade(overrides, args).await?, MarketplaceSubcommand::Remove(args) => run_remove(args).await?, } Ok(()) }}async fn run_add(args: AddMarketplaceArgs) -> Result<()> { let AddMarketplaceArgs { source, ref_name, sparse_paths, json, } = args; let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?; let outcome = add_marketplace( codex_home.to_path_buf(), MarketplaceAddRequest { source, ref_name, sparse_paths, }, ) .await?; if json { let output = JsonMarketplaceAddOutput::from_outcome(outcome); println!("{}", serde_json::to_string_pretty(&output)?); return Ok(()); } if outcome.already_added { println!( "Marketplace `{}` is already added from {}.", outcome.marketplace_name, outcome.source_display ); } else { println!( "Added marketplace `{}` from {}.", outcome.marketplace_name, outcome.source_display ); } println!( "Installed marketplace root: {}", outcome.installed_root.as_path().display() ); Ok(())}#[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) .await .context("failed to load configuration")?; let manager = PluginsManager::new(config.codex_home.to_path_buf()); manager.set_auth_mode(load_cli_auth_mode(&config).await); let plugins_input = config.plugins_config_input(); let marketplace_listing = manager .discover_marketplaces_for_config(&plugins_input, &[]) .context("failed to list plugin marketplaces")?; let mut load_issues = configured_marketplace_snapshot_issues( config.codex_home.as_path(), &plugins_input, &marketplace_listing.errors, /*marketplace_name*/ None, ); let mut issue_paths = load_issues .iter() .map(|issue| issue.path.clone()) .collect::<HashSet<_>>(); for error in &marketplace_listing.errors { if issue_paths.insert(error.path.to_path_buf()) { load_issues.push(crate::plugin_cmd::ConfiguredMarketplaceSnapshotIssue { marketplace_name: error.path.display().to_string(), path: error.path.to_path_buf(), message: error.message.clone(), }); } } if !load_issues.is_empty() { let issue_lines = load_issues .iter() .map(|issue| { format!( "- `{}` at {}: {}", issue.marketplace_name, issue.path.display(), issue.message ) }) .collect::<Vec<_>>() .join("\n"); bail!("failed to load marketplace(s):\n{issue_lines}"); } let marketplaces = marketplace_listing.marketplaces; if args.json { let marketplace_sources = configured_marketplace_sources_by_root(config.codex_home.as_path(), &plugins_input); let output = JsonMarketplaceListOutput::from_marketplaces(marketplaces, &marketplace_sources); println!("{}", serde_json::to_string_pretty(&output)?); return Ok(()); } if marketplaces.is_empty() { println!("No plugin marketplaces in scope."); return Ok(()); } let mut seen_roots = HashSet::new(); let mut rows = Vec::new(); for marketplace in marketplaces { let Ok(root) = marketplace_root_dir(&marketplace.path) else { continue; }; if !seen_roots.insert(root.clone()) { continue; } rows.push((marketplace.name, root)); } let marketplace_width = rows .iter() .map(|(name, _)| name.len()) .max() .unwrap_or("MARKETPLACE".len()) .max("MARKETPLACE".len()); println!("{:<marketplace_width$} ROOT", "MARKETPLACE"); for (marketplace_name, root) in rows { println!( "{:<marketplace_width$} {}", marketplace_name, root.display() ); } 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>, marketplace_sources: &HashMap<PathBuf, JsonMarketplaceSource>, ) -> 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 { marketplace_source: marketplace_sources.get(root.as_path()).cloned(), name: marketplace.name, root: root.display().to_string(), }) }) .collect(); Self { marketplaces } }}#[derive(Debug, Serialize)]#[serde(rename_all = "camelCase")]struct JsonMarketplaceListEntry { name: String, root: String, #[serde(skip_serializing_if = "Option::is_none")] marketplace_source: Option<JsonMarketplaceSource>,}fn configured_marketplace_sources_by_root( codex_home: &Path, plugins_input: &PluginsConfigInput,) -> HashMap<PathBuf, JsonMarketplaceSource> { let marketplace_sources = configured_marketplace_sources(plugins_input); let Some(user_config) = plugins_input.config_layer_stack.effective_user_config() else { return HashMap::new(); }; let Some(marketplaces) = user_config .get("marketplaces") .and_then(toml::Value::as_table) else { return HashMap::new(); }; let default_install_root = marketplace_install_root(codex_home); marketplaces .iter() .filter_map(|(marketplace_name, marketplace)| { let marketplace_source = marketplace_sources.get(marketplace_name)?; let root = resolve_configured_marketplace_root( marketplace_name, marketplace, &default_install_root, )?; Some((root, marketplace_source.clone())) }) .collect()}async fn run_upgrade( overrides: Vec<(String, toml::Value)>, args: UpgradeMarketplaceArgs,) -> Result<()> { let UpgradeMarketplaceArgs { marketplace_name, json, } = args; let config = Config::load_with_cli_overrides(overrides) .await .context("failed to load configuration")?; let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?; let manager = PluginsManager::new(codex_home.to_path_buf()); let plugins_input = config.plugins_config_input(); let outcome = manager .upgrade_configured_marketplaces_for_config(&plugins_input, marketplace_name.as_deref()) .map_err(anyhow::Error::msg)?; if json { print_upgrade_outcome_json(&outcome) } else { print_upgrade_outcome(&outcome, marketplace_name.as_deref()) }}async fn run_remove(args: RemoveMarketplaceArgs) -> Result<()> { let RemoveMarketplaceArgs { marketplace_name, json, } = args; let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?; let outcome = remove_marketplace( codex_home.to_path_buf(), MarketplaceRemoveRequest { marketplace_name }, ) .await?; if json { let output = JsonMarketplaceRemoveOutput::from_outcome(outcome); println!("{}", serde_json::to_string_pretty(&output)?); return Ok(()); } println!("Removed marketplace `{}`.", outcome.marketplace_name); if let Some(installed_root) = outcome.removed_installed_root { println!( "Removed installed marketplace root: {}", installed_root.as_path().display() ); } 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( outcome: &PluginMarketplaceUpgradeOutcome, marketplace_name: Option<&str>,) -> 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 selection_label = marketplace_name.unwrap_or("all configured Git marketplaces"); if outcome.selected_marketplaces.is_empty() { println!("No configured Git marketplaces to upgrade."); } else if outcome.upgraded_roots.is_empty() { if marketplace_name.is_some() { println!("Marketplace `{selection_label}` is already up to date."); } else { println!("All configured Git marketplaces are already up to date."); } } else if marketplace_name.is_some() { println!("Upgraded marketplace `{selection_label}` to the latest configured revision."); for root in &outcome.upgraded_roots { println!("Installed marketplace root: {}", root.display()); } } else { println!("Upgraded {} marketplace(s).", outcome.upgraded_roots.len()); for root in &outcome.upgraded_roots { println!("Installed marketplace root: {}", root.display()); } } Ok(())}#[cfg(test)]mod tests { use super::*; use pretty_assertions::assert_eq; #[test] fn sparse_paths_parse_before_or_after_source() { let sparse_before_source = AddMarketplaceArgs::try_parse_from(["add", "--sparse", "plugins/foo", "owner/repo"]) .unwrap(); assert_eq!(sparse_before_source.source, "owner/repo"); assert_eq!(sparse_before_source.sparse_paths, vec!["plugins/foo"]); let sparse_after_source = AddMarketplaceArgs::try_parse_from(["add", "owner/repo", "--sparse", "plugins/foo"]) .unwrap(); assert_eq!(sparse_after_source.source, "owner/repo"); assert_eq!(sparse_after_source.sparse_paths, vec!["plugins/foo"]); let repeated_sparse = AddMarketplaceArgs::try_parse_from([ "add", "--sparse", "plugins/foo", "--sparse", "skills/bar", "owner/repo", ]) .unwrap(); assert_eq!(repeated_sparse.source, "owner/repo"); assert_eq!( repeated_sparse.sparse_paths, vec!["plugins/foo", "skills/bar"] ); } #[test] fn upgrade_subcommand_parses_optional_marketplace_name() { let upgrade_all = UpgradeMarketplaceArgs::try_parse_from(["upgrade"]).unwrap(); assert_eq!(upgrade_all.marketplace_name, None); let upgrade_one = UpgradeMarketplaceArgs::try_parse_from(["upgrade", "debug"]).unwrap(); assert_eq!(upgrade_one.marketplace_name.as_deref(), Some("debug")); } #[test] fn remove_subcommand_parses_marketplace_name() { let remove = RemoveMarketplaceArgs::try_parse_from(["remove", "debug"]).unwrap(); assert_eq!(remove.marketplace_name, "debug"); }}