[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`
This commit is contained in:
mpc-oai
2026-06-08 13:37:55 -05:00
committed by GitHub
Unverified
parent 6d8e12ac42
commit 0aa9931aea
3 changed files with 158 additions and 5 deletions
+49 -2
View File
@@ -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<codex_core_plugins::marketplace::Marketplace>) -> Self {
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()
@@ -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<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(
+2 -2
View File
@@ -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<String, JsonMarketplaceSource> {
let Some(user_config) = plugins_input.config_layer_stack.effective_user_config() else {
+107 -1
View File
@@ -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",
},
},
],
})