From 0aa9931aeadd9cbb1d6d02f854d1402f8db2bec8 Mon Sep 17 00:00:00 2001 From: mpc-oai Date: Mon, 8 Jun 2026 13:37:55 -0500 Subject: [PATCH] [plugins] Expose marketplace source in marketplace list JSON (#27009) ## Summary - Follow-up to #26417 and #26631 - Add `marketplaceSource` to `codex plugin marketplace list --json` entries for configured marketplaces - Reuse the existing `marketplaceSource` shape from `codex plugin list --json` - Keep human-readable marketplace list output unchanged - Add CLI coverage for configured local and git marketplace sources Example: ```json { "marketplaces": [ { "name": "debug", "root": "/path/to/.codex/.tmp/marketplaces/debug", "marketplaceSource": { "sourceType": "git", "source": "https://example.com/acme/agent-skills.git" } } ] } ``` ## Validation - `just fmt` - `just fix -p codex-cli` - `just test -p codex-cli marketplace_list` - `just test -p codex-cli` --- codex-rs/cli/src/marketplace_cmd.rs | 51 ++++++++++++- codex-rs/cli/src/plugin_cmd.rs | 4 +- codex-rs/cli/tests/plugin_cli.rs | 108 +++++++++++++++++++++++++++- 3 files changed, 158 insertions(+), 5 deletions(-) diff --git a/codex-rs/cli/src/marketplace_cmd.rs b/codex-rs/cli/src/marketplace_cmd.rs index fc4d9eef6..b392bb53b 100644 --- a/codex-rs/cli/src/marketplace_cmd.rs +++ b/codex-rs/cli/src/marketplace_cmd.rs @@ -5,7 +5,10 @@ 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; @@ -15,9 +18,14 @@ 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; #[derive(Debug, Parser)] #[command(bin_name = "codex plugin marketplace")] @@ -240,7 +248,10 @@ async fn run_list(overrides: Vec<(String, toml::Value)>, args: ListMarketplaceAr } let marketplaces = marketplace_listing.marketplaces; if args.json { - let output = JsonMarketplaceListOutput::from_marketplaces(marketplaces); + 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(()); } @@ -288,7 +299,10 @@ struct JsonMarketplaceListOutput { } impl JsonMarketplaceListOutput { - fn from_marketplaces(marketplaces: Vec) -> Self { + fn from_marketplaces( + marketplaces: Vec, + marketplace_sources: &HashMap, + ) -> Self { let mut seen_roots = HashSet::new(); let marketplaces = marketplaces .into_iter() @@ -298,6 +312,7 @@ impl JsonMarketplaceListOutput { return None; } Some(JsonMarketplaceListEntry { + marketplace_source: marketplace_sources.get(root.as_path()).cloned(), name: marketplace.name, root: root.display().to_string(), }) @@ -313,6 +328,38 @@ impl JsonMarketplaceListOutput { struct JsonMarketplaceListEntry { name: String, root: String, + #[serde(skip_serializing_if = "Option::is_none")] + marketplace_source: Option, +} + +fn configured_marketplace_sources_by_root( + codex_home: &Path, + plugins_input: &PluginsConfigInput, +) -> HashMap { + 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( diff --git a/codex-rs/cli/src/plugin_cmd.rs b/codex-rs/cli/src/plugin_cmd.rs index 7def7283c..94c388066 100644 --- a/codex-rs/cli/src/plugin_cmd.rs +++ b/codex-rs/cli/src/plugin_cmd.rs @@ -437,12 +437,12 @@ impl JsonPluginSource { #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] -struct JsonMarketplaceSource { +pub(crate) struct JsonMarketplaceSource { source_type: String, source: String, } -fn configured_marketplace_sources( +pub(crate) fn configured_marketplace_sources( plugins_input: &PluginsConfigInput, ) -> HashMap { let Some(user_config) = plugins_input.config_layer_stack.effective_user_config() else { diff --git a/codex-rs/cli/tests/plugin_cli.rs b/codex-rs/cli/tests/plugin_cli.rs index cda79cf9a..c8bd7b542 100644 --- a/codex-rs/cli/tests/plugin_cli.rs +++ b/codex-rs/cli/tests/plugin_cli.rs @@ -341,6 +341,7 @@ async fn marketplace_list_shows_configured_marketplace_names() -> Result<()> { #[tokio::test] async fn marketplace_list_json_prints_configured_marketplaces() -> Result<()> { let (codex_home, source) = setup_local_marketplace()?; + let source_path = source.path().display().to_string(); let assert = codex_command(codex_home.path())? .args(["plugin", "marketplace", "list", "--json"]) @@ -355,7 +356,112 @@ async fn marketplace_list_json_prints_configured_marketplaces() -> Result<()> { "marketplaces": [ { "name": "debug", - "root": source.path().display().to_string(), + "root": source_path, + "marketplaceSource": { + "sourceType": "local", + "source": source_path, + }, + }, + ], + }) + ); + + Ok(()) +} + +#[tokio::test] +async fn marketplace_list_json_includes_configured_git_marketplace_source() -> Result<()> { + let codex_home = TempDir::new()?; + let marketplace_root = codex_home + .path() + .join(".tmp") + .join("marketplaces") + .join("debug"); + write_plugins_enabled_config(codex_home.path())?; + write_marketplace_source(&marketplace_root)?; + let update = MarketplaceConfigUpdate { + last_updated: "2026-06-04T08:39:49Z", + last_revision: Some("abc123"), + source_type: "git", + source: "https://example.com/acme/agent-skills.git", + ref_name: None, + sparse_paths: &[], + }; + record_user_marketplace(codex_home.path(), "debug", &update)?; + let normalized_root = canonicalize_existing_preserving_symlinks(&marketplace_root)?; + + 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": normalized_root.display().to_string(), + "marketplaceSource": { + "sourceType": "git", + "source": "https://example.com/acme/agent-skills.git", + }, + }, + ], + }) + ); + + Ok(()) +} + +#[tokio::test] +async fn marketplace_list_json_keys_configured_source_by_root() -> Result<()> { + let codex_home = TempDir::new()?; + let home = TempDir::new()?; + let marketplace_root = codex_home + .path() + .join(".tmp") + .join("marketplaces") + .join("debug"); + write_plugins_enabled_config(codex_home.path())?; + write_marketplace_source(home.path())?; + write_marketplace_source(&marketplace_root)?; + let update = MarketplaceConfigUpdate { + last_updated: "2026-06-04T08:39:49Z", + last_revision: Some("abc123"), + source_type: "git", + source: "https://example.com/acme/agent-skills.git", + ref_name: None, + sparse_paths: &[], + }; + record_user_marketplace(codex_home.path(), "debug", &update)?; + let normalized_root = canonicalize_existing_preserving_symlinks(&marketplace_root)?; + + let assert = codex_command(codex_home.path())? + .env("HOME", 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": home.path().display().to_string(), + }, + { + "name": "debug", + "root": normalized_root.display().to_string(), + "marketplaceSource": { + "sourceType": "git", + "source": "https://example.com/acme/agent-skills.git", + }, }, ], })