mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Expose configured marketplace source in plugin list JSON (#26417)
## Summary - Follow-up to #25330 - Add `marketplaceSource` to `codex plugin list --json` entries for configured marketplaces - Keep the existing per-plugin `source` field unchanged; this still reports the local plugin source path - Include only the configured marketplace `sourceType` and `source` from `config.toml` - Keep human-readable output unchanged - Add CLI coverage for configured local and git marketplace sources Example: ```json { "source": { "source": "local", "path": "/path/to/.codex/.tmp/marketplaces/debug/plugins/sample" }, "marketplaceSource": { "sourceType": "git", "source": "https://example.com/acme/agent-skills.git" } } ``` ## Validation - `just fmt` - `just fix -p codex-cli` - `just test -p codex-cli plugin_list`
This commit is contained in:
committed by
GitHub
Unverified
parent
cbf62f64cb
commit
cdc1d592df
@@ -20,6 +20,7 @@ use codex_plugin::PluginId;
|
||||
use codex_plugin::validate_plugin_segment;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -177,9 +178,14 @@ pub async fn run_plugin_list(
|
||||
.is_none_or(|name| marketplace.name == *name)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let marketplace_sources = configured_marketplace_sources(&plugins_input);
|
||||
|
||||
if args.json {
|
||||
let output = JsonPluginListOutput::from_marketplaces(marketplaces, args.available);
|
||||
let output = JsonPluginListOutput::from_marketplaces(
|
||||
marketplaces,
|
||||
args.available,
|
||||
&marketplace_sources,
|
||||
);
|
||||
println!("{}", serde_json::to_string_pretty(&output)?);
|
||||
return Ok(());
|
||||
}
|
||||
@@ -269,13 +275,19 @@ impl JsonPluginListOutput {
|
||||
fn from_marketplaces(
|
||||
marketplaces: Vec<codex_core_plugins::ConfiguredMarketplace>,
|
||||
include_available: bool,
|
||||
marketplace_sources: &HashMap<String, JsonMarketplaceSource>,
|
||||
) -> Self {
|
||||
let mut installed = Vec::new();
|
||||
let mut available = Vec::new();
|
||||
|
||||
for marketplace in marketplaces {
|
||||
let marketplace_source = marketplace_sources.get(&marketplace.name).cloned();
|
||||
for plugin in marketplace.plugins {
|
||||
let entry = JsonPluginListEntry::from_configured_plugin(&marketplace.name, plugin);
|
||||
let entry = JsonPluginListEntry::from_configured_plugin(
|
||||
&marketplace.name,
|
||||
marketplace_source.clone(),
|
||||
plugin,
|
||||
);
|
||||
if entry.installed {
|
||||
installed.push(entry);
|
||||
} else if include_available {
|
||||
@@ -301,6 +313,8 @@ struct JsonPluginListEntry {
|
||||
installed: bool,
|
||||
enabled: bool,
|
||||
source: JsonPluginSource,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
marketplace_source: Option<JsonMarketplaceSource>,
|
||||
install_policy: &'static str,
|
||||
auth_policy: &'static str,
|
||||
}
|
||||
@@ -308,6 +322,7 @@ struct JsonPluginListEntry {
|
||||
impl JsonPluginListEntry {
|
||||
fn from_configured_plugin(
|
||||
marketplace_name: &str,
|
||||
marketplace_source: Option<JsonMarketplaceSource>,
|
||||
plugin: codex_core_plugins::ConfiguredMarketplacePlugin,
|
||||
) -> Self {
|
||||
let version = plugin.installed_version.or(plugin.local_version);
|
||||
@@ -319,6 +334,7 @@ impl JsonPluginListEntry {
|
||||
installed: plugin.installed,
|
||||
enabled: plugin.enabled,
|
||||
source: JsonPluginSource::from_marketplace_source(plugin.source),
|
||||
marketplace_source,
|
||||
install_policy: install_policy_label(plugin.policy.installation),
|
||||
auth_policy: auth_policy_label(plugin.policy.authentication),
|
||||
}
|
||||
@@ -375,6 +391,44 @@ impl JsonPluginSource {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct JsonMarketplaceSource {
|
||||
source_type: String,
|
||||
source: String,
|
||||
}
|
||||
|
||||
fn configured_marketplace_sources(
|
||||
plugins_input: &PluginsConfigInput,
|
||||
) -> HashMap<String, JsonMarketplaceSource> {
|
||||
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();
|
||||
};
|
||||
|
||||
marketplaces
|
||||
.iter()
|
||||
.filter_map(|(marketplace_name, marketplace)| {
|
||||
let source_type = marketplace
|
||||
.get("source_type")
|
||||
.and_then(toml::Value::as_str)?;
|
||||
let source = marketplace.get("source").and_then(toml::Value::as_str)?;
|
||||
Some((
|
||||
marketplace_name.clone(),
|
||||
JsonMarketplaceSource {
|
||||
source_type: source_type.to_string(),
|
||||
source: source.to_string(),
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn install_policy_label(policy: MarketplacePluginInstallPolicy) -> &'static str {
|
||||
match policy {
|
||||
MarketplacePluginInstallPolicy::NotAvailable => "NOT_AVAILABLE",
|
||||
|
||||
@@ -2,6 +2,7 @@ use anyhow::Result;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::MarketplaceConfigUpdate;
|
||||
use codex_config::record_user_marketplace;
|
||||
use codex_utils_absolute_path::canonicalize_existing_preserving_symlinks;
|
||||
use predicates::prelude::PredicateBooleanExt;
|
||||
use predicates::str::contains;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -495,6 +496,7 @@ async fn plugin_list_prints_plugins_in_a_table() -> Result<()> {
|
||||
async fn plugin_list_json_prints_available_plugins_when_requested() -> Result<()> {
|
||||
let (codex_home, source) = setup_local_marketplace()?;
|
||||
let plugin_path = source.path().join("plugins").join("sample");
|
||||
let source_path = source.path().to_string_lossy().into_owned();
|
||||
|
||||
let assert = codex_command(codex_home.path())?
|
||||
.args(["plugin", "list", "--available", "--json"])
|
||||
@@ -519,6 +521,69 @@ async fn plugin_list_json_prints_available_plugins_when_requested() -> Result<()
|
||||
"source": "local",
|
||||
"path": plugin_path.display().to_string(),
|
||||
},
|
||||
"marketplaceSource": {
|
||||
"sourceType": "local",
|
||||
"source": source_path,
|
||||
},
|
||||
"installPolicy": "AVAILABLE",
|
||||
"authPolicy": "ON_INSTALL",
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_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 plugin_path = marketplace_root.join("plugins").join("sample");
|
||||
let normalized_plugin_path = canonicalize_existing_preserving_symlinks(&plugin_path)?;
|
||||
|
||||
let assert = codex_command(codex_home.path())?
|
||||
.args(["plugin", "list", "--available", "--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!({
|
||||
"installed": [],
|
||||
"available": [
|
||||
{
|
||||
"pluginId": "sample@debug",
|
||||
"name": "sample",
|
||||
"marketplaceName": "debug",
|
||||
"version": "1.2.3",
|
||||
"installed": false,
|
||||
"enabled": false,
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": normalized_plugin_path.display().to_string(),
|
||||
},
|
||||
"marketplaceSource": {
|
||||
"sourceType": "git",
|
||||
"source": "https://example.com/acme/agent-skills.git",
|
||||
},
|
||||
"installPolicy": "AVAILABLE",
|
||||
"authPolicy": "ON_INSTALL",
|
||||
},
|
||||
@@ -533,6 +598,7 @@ async fn plugin_list_json_prints_available_plugins_when_requested() -> Result<()
|
||||
async fn plugin_list_json_prints_installed_plugins() -> Result<()> {
|
||||
let (codex_home, source) = setup_local_marketplace()?;
|
||||
let plugin_path = source.path().join("plugins").join("sample");
|
||||
let source_path = source.path().to_string_lossy().into_owned();
|
||||
|
||||
codex_command(codex_home.path())?
|
||||
.args(["plugin", "add", "sample@debug"])
|
||||
@@ -561,6 +627,10 @@ async fn plugin_list_json_prints_installed_plugins() -> Result<()> {
|
||||
"source": "local",
|
||||
"path": plugin_path.display().to_string(),
|
||||
},
|
||||
"marketplaceSource": {
|
||||
"sourceType": "local",
|
||||
"source": source_path,
|
||||
},
|
||||
"installPolicy": "AVAILABLE",
|
||||
"authPolicy": "ON_INSTALL",
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user