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::ConfiguredMarketplace; use codex_core_plugins::OPENAI_BUNDLED_MARKETPLACE_NAME; use codex_core_plugins::PluginInstallRequest; 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::MarketplaceListError; 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 std::path::Path; use std::path::PathBuf; use crate::marketplace_cmd::MarketplaceCli; const OPENAI_BUNDLED_ALPHA_MARKETPLACE_NAME: &str = "openai-bundled-alpha"; const OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME: &str = "openai-primary-runtime"; #[derive(Debug, Parser)] #[command(bin_name = "codex plugin")] pub struct PluginCli { #[clap(flatten)] pub config_overrides: CliConfigOverrides, #[command(subcommand)] pub subcommand: PluginSubcommand, } #[derive(Debug, clap::Subcommand)] pub enum PluginSubcommand { /// Install a plugin from a configured marketplace snapshot. /// /// Pass either `PLUGIN@MARKETPLACE` or pass `PLUGIN` with /// `--marketplace MARKETPLACE`. Add(AddPluginArgs), /// List plugins available from configured marketplace snapshots. List(ListPluginsArgs), /// Add, list, upgrade, or remove configured plugin marketplaces. Marketplace(MarketplaceCli), /// Remove an installed plugin from local config and cache. /// /// Pass either `PLUGIN@MARKETPLACE` or pass `PLUGIN` with /// `--marketplace MARKETPLACE`. Remove(RemovePluginArgs), } #[derive(Debug, Parser)] #[command( bin_name = "codex plugin add", after_help = "Examples:\n codex plugin add sample@debug\n codex plugin add sample --marketplace debug" )] pub struct AddPluginArgs { /// Plugin selector to install: either PLUGIN@MARKETPLACE or PLUGIN with --marketplace. #[arg(value_name = "PLUGIN[@MARKETPLACE]")] plugin: String, /// Configured marketplace name to use when PLUGIN does not include @MARKETPLACE. #[arg(long = "marketplace", short = 'm', value_name = "MARKETPLACE")] marketplace_name: Option, } #[derive(Debug, Parser)] #[command( bin_name = "codex plugin list", after_help = "Examples:\n codex plugin list\n codex plugin list --marketplace debug" )] pub struct ListPluginsArgs { /// Only list plugins from this configured marketplace name. #[arg(long = "marketplace", short = 'm', value_name = "MARKETPLACE")] marketplace_name: Option, } #[derive(Debug, Parser)] #[command( bin_name = "codex plugin remove", after_help = "Examples:\n codex plugin remove sample@debug\n codex plugin remove sample --marketplace debug" )] pub struct RemovePluginArgs { /// Plugin selector to remove: either PLUGIN@MARKETPLACE or PLUGIN with --marketplace. #[arg(value_name = "PLUGIN[@MARKETPLACE]")] plugin: String, /// Marketplace name to use when PLUGIN does not include @MARKETPLACE. #[arg(long = "marketplace", short = 'm', value_name = "MARKETPLACE")] marketplace_name: Option, } pub async fn run_plugin_add( overrides: Vec<(String, toml::Value)>, args: AddPluginArgs, ) -> Result<()> { let PluginCommandContext { codex_home, plugins_input, manager, } = load_plugin_command_context(overrides).await?; let PluginSelection { plugin_name, marketplace_name, .. } = parse_plugin_selection(args.plugin, args.marketplace_name)?; let marketplace = find_marketplace_for_plugin( &manager, codex_home.as_path(), &plugins_input, &marketplace_name, &plugin_name, )?; let outcome = manager .install_plugin(PluginInstallRequest { plugin_name, marketplace_path: marketplace.path, }) .await?; println!( "Added plugin `{}` from marketplace `{}`.", outcome.plugin_id.plugin_name, outcome.plugin_id.marketplace_name ); println!( "Installed plugin root: {}", outcome.installed_path.as_path().display() ); Ok(()) } pub async fn run_plugin_list( overrides: Vec<(String, toml::Value)>, args: ListPluginsArgs, ) -> Result<()> { let PluginCommandContext { codex_home, plugins_input, manager, .. } = load_plugin_command_context(overrides).await?; let outcome = manager .list_marketplaces_for_config(&plugins_input, &[]) .context("failed to list marketplace plugins")?; ensure_configured_marketplace_snapshots_loaded( codex_home.as_path(), &plugins_input, &outcome.errors, /*marketplace_name*/ None, )?; let marketplaces = outcome .marketplaces .into_iter() .filter(|marketplace| { args.marketplace_name .as_ref() .is_none_or(|name| marketplace.name == *name) }) .collect::>(); if marketplaces.is_empty() { if let Some(marketplace_name) = args.marketplace_name { println!("No plugins found in marketplace `{marketplace_name}`."); } else { println!("No marketplace plugins found."); } } else { for (index, marketplace) in marketplaces.into_iter().enumerate() { let mut rows = Vec::new(); let mut plugin_width = "PLUGIN".len(); let mut status_width = "STATUS".len(); let mut installed_version_width = "VERSION".len(); let mut path_width = "PATH".len(); for plugin in &marketplace.plugins { let state = if plugin.installed && plugin.enabled { "installed, enabled" } else if plugin.installed { "installed, disabled" } else { "not installed" }; let installed_version = plugin.installed_version.clone().unwrap_or_default(); let path = match &plugin.source { codex_core_plugins::marketplace::MarketplacePluginSource::Local { path } => { path.as_path().display().to_string() } codex_core_plugins::marketplace::MarketplacePluginSource::Git { url, path, ref_name, sha, } => { let mut parts = vec![url.clone()]; if let Some(path) = path { parts.push(format!("path `{path}`")); } if let Some(ref_name) = ref_name { parts.push(format!("ref `{ref_name}`")); } if let Some(sha) = sha { parts.push(format!("sha `{sha}`")); } parts.join(", ") } }; plugin_width = plugin_width.max(plugin.id.len()); status_width = status_width.max(state.len()); installed_version_width = installed_version_width.max(installed_version.len()); path_width = path_width.max(path.len()); rows.push((plugin.id.clone(), state, installed_version, path)); } if index > 0 { println!(); } println!("Marketplace `{}`", marketplace.name); println!("{}", marketplace.path.as_path().display()); println!(); println!( "{:, args: RemovePluginArgs, ) -> Result<()> { let PluginCommandContext { manager, .. } = load_plugin_command_context(overrides).await?; let selection = parse_plugin_selection(args.plugin, args.marketplace_name)?; manager.uninstall_plugin(selection.plugin_key).await?; println!( "Removed plugin `{}` from marketplace `{}`.", selection.plugin_name, selection.marketplace_name ); Ok(()) } struct PluginCommandContext { codex_home: PathBuf, plugins_input: PluginsConfigInput, manager: PluginsManager, } async fn load_plugin_command_context( overrides: Vec<(String, toml::Value)>, ) -> Result { let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?; let config = Config::load_with_cli_overrides(overrides) .await .context("failed to load configuration")?; let plugins_input = config.plugins_config_input(); let manager = PluginsManager::new(codex_home.to_path_buf()); Ok(PluginCommandContext { codex_home: codex_home.to_path_buf(), plugins_input, manager, }) } struct PluginSelection { plugin_name: String, marketplace_name: String, plugin_key: String, } impl PluginSelection { fn from_plugin_id(plugin_id: PluginId) -> Self { let plugin_key = plugin_id.as_key(); Self { plugin_name: plugin_id.plugin_name, marketplace_name: plugin_id.marketplace_name, plugin_key, } } } fn parse_plugin_selection( plugin: String, marketplace_name: Option, ) -> Result { match (PluginId::parse(&plugin), marketplace_name) { (Ok(plugin_id), None) => Ok(PluginSelection::from_plugin_id(plugin_id)), (Ok(plugin_id), Some(marketplace_name)) => { if plugin_id.marketplace_name != marketplace_name { bail!( "plugin id `{}` belongs to marketplace `{}`, but --marketplace specified `{}`", plugin, plugin_id.marketplace_name, marketplace_name ); } Ok(PluginSelection::from_plugin_id(plugin_id)) } (Err(_), Some(marketplace_name)) => Ok(PluginSelection::from_plugin_id(PluginId::new( plugin, marketplace_name, )?)), (Err(_), None) => { bail!("plugin requires --marketplace unless passed as @") } } } fn find_marketplace_for_plugin( manager: &PluginsManager, codex_home: &std::path::Path, plugins_input: &PluginsConfigInput, marketplace_name: &str, plugin_name: &str, ) -> Result { let outcome = manager .list_marketplaces_for_config(plugins_input, &[]) .context("failed to list marketplace plugins")?; ensure_configured_marketplace_snapshots_loaded( codex_home, plugins_input, &outcome.errors, Some(marketplace_name), )?; let matches = outcome .marketplaces .into_iter() .filter(|marketplace| marketplace.name == marketplace_name) .filter(|marketplace| { marketplace .plugins .iter() .any(|plugin| plugin.name == plugin_name) }) .collect::>(); match matches.as_slice() { [] => bail!("plugin `{plugin_name}` was not found in marketplace `{marketplace_name}`"), [marketplace] => Ok(marketplace.clone()), _ => bail!( "plugin `{plugin_name}` in marketplace `{marketplace_name}` matched multiple marketplace roots" ), } } pub(crate) struct ConfiguredMarketplaceSnapshotIssue { pub(crate) marketplace_name: String, pub(crate) path: PathBuf, pub(crate) message: String, } fn ensure_configured_marketplace_snapshots_loaded( codex_home: &std::path::Path, plugins_input: &PluginsConfigInput, load_errors: &[MarketplaceListError], marketplace_name: Option<&str>, ) -> Result<()> { let issues = configured_marketplace_snapshot_issues( codex_home, plugins_input, load_errors, marketplace_name, ); if issues.is_empty() { return Ok(()); } let issue_lines = issues .iter() .map(|issue| { format!( "- `{}` at {}: {}", issue.marketplace_name, issue.path.display(), issue.message ) }) .collect::>() .join("\n"); bail!("failed to load configured marketplace snapshot(s):\n{issue_lines}"); } pub(crate) fn configured_marketplace_snapshot_issues( codex_home: &std::path::Path, plugins_input: &PluginsConfigInput, load_errors: &[MarketplaceListError], marketplace_name: Option<&str>, ) -> Vec { let Some(user_config) = plugins_input.config_layer_stack.effective_user_config() else { return Vec::new(); }; let Some(configured_marketplaces) = user_config .get("marketplaces") .and_then(toml::Value::as_table) else { return Vec::new(); }; let default_install_root = marketplace_install_root(codex_home); let mut manifest_paths = Vec::new(); let mut issues = Vec::new(); for (configured_name, marketplace) in configured_marketplaces { if marketplace_name.is_some_and(|name| configured_name != name) { continue; } if !marketplace.is_table() { issues.push(ConfiguredMarketplaceSnapshotIssue { marketplace_name: configured_name.clone(), path: PathBuf::from(""), message: "configured marketplace entry must be a table".to_string(), }); continue; } if let Err(err) = validate_plugin_segment(configured_name, "marketplace name") { issues.push(ConfiguredMarketplaceSnapshotIssue { marketplace_name: configured_name.clone(), path: PathBuf::from(""), message: err.to_string(), }); continue; } if marketplace.get("source_type").and_then(toml::Value::as_str) == Some("local") && marketplace .get("source") .and_then(toml::Value::as_str) .is_none_or(str::is_empty) { issues.push(ConfiguredMarketplaceSnapshotIssue { marketplace_name: configured_name.clone(), path: PathBuf::from(""), message: "configured local marketplace source is missing or empty".to_string(), }); continue; } let Some(root) = resolve_configured_marketplace_root( configured_name, marketplace, &default_install_root, ) else { continue; }; match find_marketplace_manifest_path(&root) { Some(path) => manifest_paths.push((configured_name.clone(), path)), None => { if is_implicit_system_marketplace_root(configured_name, codex_home, &root) { continue; } issues.push(ConfiguredMarketplaceSnapshotIssue { marketplace_name: configured_name.clone(), path: root, message: "marketplace root does not contain a supported manifest".to_string(), }); } } } for error in load_errors { if let Some((configured_name, _)) = manifest_paths .iter() .find(|(_, path)| path.as_path() == error.path.as_path()) { issues.push(ConfiguredMarketplaceSnapshotIssue { marketplace_name: configured_name.clone(), path: error.path.to_path_buf(), message: error.message.clone(), }); } } issues } fn is_implicit_system_marketplace_root( marketplace_name: &str, _codex_home: &Path, root: &Path, ) -> bool { if matches!( marketplace_name, OPENAI_BUNDLED_MARKETPLACE_NAME | OPENAI_BUNDLED_ALPHA_MARKETPLACE_NAME ) && path_ends_with(root, &[".tmp", "bundled-marketplaces", marketplace_name]) { return true; } marketplace_name == OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME && path_ends_with( root, &[ "codex-runtimes", "codex-primary-runtime", "plugins", marketplace_name, ], ) } fn path_ends_with(path: &Path, suffix: &[&str]) -> bool { let path_components = path .components() .map(|component| component.as_os_str().to_string_lossy().into_owned()) .collect::>(); path_components.as_slice().ends_with( &suffix .iter() .map(std::string::ToString::to_string) .collect::>(), ) }