support plugin/list. (#13540)

Introduce a plugin/list which reads from local marketplace.json.
Also update the signature for plugin/install.
This commit is contained in:
xl-openai
2026-03-05 21:58:50 -05:00
committed by GitHub
parent 56420da857
commit 520ed724d2
25 changed files with 1645 additions and 145 deletions

View File

@@ -1,5 +1,7 @@
use super::load_plugin_manifest;
use super::marketplace::MarketplaceError;
use super::marketplace::MarketplacePluginSourceSummary;
use super::marketplace::list_marketplaces;
use super::marketplace::resolve_marketplace_plugin;
use super::plugin_manifest_name;
use super::store::DEFAULT_PLUGIN_VERSION;
@@ -26,6 +28,7 @@ use serde_json::Map as JsonMap;
use serde_json::Value as JsonValue;
use serde_json::json;
use std::collections::HashMap;
use std::collections::HashSet;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
@@ -42,8 +45,21 @@ pub struct AppConnectorId(pub String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginInstallRequest {
pub plugin_name: String,
pub marketplace_name: String,
pub cwd: PathBuf,
pub marketplace_path: AbsolutePathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfiguredMarketplaceSummary {
pub name: String,
pub path: PathBuf,
pub plugins: Vec<ConfiguredMarketplacePluginSummary>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfiguredMarketplacePluginSummary {
pub name: String,
pub source: MarketplacePluginSourceSummary,
pub enabled: bool,
}
#[derive(Debug, Clone, PartialEq)]
@@ -234,11 +250,7 @@ impl PluginsManager {
&self,
request: PluginInstallRequest,
) -> Result<PluginInstallResult, PluginInstallError> {
let resolved = resolve_marketplace_plugin(
&request.cwd,
&request.plugin_name,
&request.marketplace_name,
)?;
let resolved = resolve_marketplace_plugin(&request.marketplace_path, &request.plugin_name)?;
let store = self.store.clone();
let result = tokio::task::spawn_blocking(move || {
store.install(resolved.source_path.into_path_buf(), resolved.plugin_id)
@@ -262,6 +274,56 @@ impl PluginsManager {
Ok(result)
}
pub fn list_marketplaces_for_config(
&self,
config: &Config,
additional_roots: &[AbsolutePathBuf],
) -> Result<Vec<ConfiguredMarketplaceSummary>, MarketplaceError> {
let configured_plugins = self
.plugins_for_config(config)
.plugins()
.iter()
.map(|plugin| (plugin.config_name.clone(), plugin.enabled))
.collect::<HashMap<String, bool>>();
let marketplaces = list_marketplaces(additional_roots)?;
let mut seen_plugin_keys = HashSet::new();
Ok(marketplaces
.into_iter()
.filter_map(|marketplace| {
let marketplace_name = marketplace.name.clone();
let plugins = marketplace
.plugins
.into_iter()
.filter_map(|plugin| {
let plugin_key = format!("{}@{marketplace_name}", plugin.name);
if !seen_plugin_keys.insert(plugin_key.clone()) {
return None;
}
Some(ConfiguredMarketplacePluginSummary {
// Enabled state is keyed by `<plugin>@<marketplace>`, so duplicate
// plugin entries from duplicate marketplace files intentionally
// resolve to the first discovered source.
enabled: configured_plugins
.get(&plugin_key)
.copied()
.unwrap_or(false),
name: plugin.name,
source: plugin.source,
})
})
.collect::<Vec<_>>();
(!plugins.is_empty()).then_some(ConfiguredMarketplaceSummary {
name: marketplace.name,
path: marketplace.path,
plugins,
})
})
.collect())
}
}
#[derive(Debug, thiserror::Error)]
@@ -288,9 +350,9 @@ impl PluginInstallError {
matches!(
self,
Self::Marketplace(
MarketplaceError::InvalidMarketplaceFile { .. }
MarketplaceError::MarketplaceNotFound { .. }
| MarketplaceError::InvalidMarketplaceFile { .. }
| MarketplaceError::PluginNotFound { .. }
| MarketplaceError::DuplicatePlugin { .. }
| MarketplaceError::InvalidPlugin(_)
) | Self::Store(PluginStoreError::Invalid(_))
)
@@ -1086,8 +1148,10 @@ mod tests {
let result = PluginsManager::new(tmp.path().to_path_buf())
.install_plugin(PluginInstallRequest {
plugin_name: "sample-plugin".to_string(),
marketplace_name: "debug".to_string(),
cwd: repo_root.clone(),
marketplace_path: AbsolutePathBuf::try_from(
repo_root.join(".agents/plugins/marketplace.json"),
)
.unwrap(),
})
.await
.unwrap();
@@ -1106,4 +1170,207 @@ mod tests {
assert!(config.contains(r#"[plugins."sample-plugin@debug"]"#));
assert!(config.contains("enabled = true"));
}
#[tokio::test]
async fn list_marketplaces_for_config_includes_enabled_state() {
let tmp = tempfile::tempdir().unwrap();
let repo_root = tmp.path().join("repo");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "debug",
"plugins": [
{
"name": "enabled-plugin",
"source": {
"source": "local",
"path": "./enabled-plugin"
}
},
{
"name": "disabled-plugin",
"source": {
"source": "local",
"path": "./disabled-plugin"
}
}
]
}"#,
)
.unwrap();
write_file(
&tmp.path().join(CONFIG_TOML_FILE),
r#"[features]
plugins = true
[plugins."enabled-plugin@debug"]
enabled = true
[plugins."disabled-plugin@debug"]
enabled = false
"#,
);
let config = ConfigBuilder::default()
.codex_home(tmp.path().to_path_buf())
.build()
.await
.expect("config should load");
let marketplaces = PluginsManager::new(tmp.path().to_path_buf())
.list_marketplaces_for_config(&config, &[AbsolutePathBuf::try_from(repo_root).unwrap()])
.unwrap();
let marketplace = marketplaces
.into_iter()
.find(|marketplace| {
marketplace.path == tmp.path().join("repo/.agents/plugins/marketplace.json")
})
.expect("expected repo marketplace entry");
assert_eq!(
marketplace,
ConfiguredMarketplaceSummary {
name: "debug".to_string(),
path: tmp.path().join("repo/.agents/plugins/marketplace.json"),
plugins: vec![
ConfiguredMarketplacePluginSummary {
name: "enabled-plugin".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: tmp.path().join("repo/.agents/plugins/enabled-plugin"),
},
enabled: true,
},
ConfiguredMarketplacePluginSummary {
name: "disabled-plugin".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: tmp.path().join("repo/.agents/plugins/disabled-plugin"),
},
enabled: false,
},
],
}
);
}
#[tokio::test]
async fn list_marketplaces_for_config_uses_first_duplicate_plugin_entry() {
let tmp = tempfile::tempdir().unwrap();
let repo_a_root = tmp.path().join("repo-a");
let repo_b_root = tmp.path().join("repo-b");
fs::create_dir_all(repo_a_root.join(".git")).unwrap();
fs::create_dir_all(repo_b_root.join(".git")).unwrap();
fs::create_dir_all(repo_a_root.join(".agents/plugins")).unwrap();
fs::create_dir_all(repo_b_root.join(".agents/plugins")).unwrap();
fs::write(
repo_a_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "debug",
"plugins": [
{
"name": "dup-plugin",
"source": {
"source": "local",
"path": "./from-a"
}
}
]
}"#,
)
.unwrap();
fs::write(
repo_b_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "debug",
"plugins": [
{
"name": "dup-plugin",
"source": {
"source": "local",
"path": "./from-b"
}
},
{
"name": "b-only-plugin",
"source": {
"source": "local",
"path": "./from-b-only"
}
}
]
}"#,
)
.unwrap();
write_file(
&tmp.path().join(CONFIG_TOML_FILE),
r#"[features]
plugins = true
[plugins."dup-plugin@debug"]
enabled = true
[plugins."b-only-plugin@debug"]
enabled = false
"#,
);
let config = ConfigBuilder::default()
.codex_home(tmp.path().to_path_buf())
.build()
.await
.expect("config should load");
let marketplaces = PluginsManager::new(tmp.path().to_path_buf())
.list_marketplaces_for_config(
&config,
&[
AbsolutePathBuf::try_from(repo_a_root).unwrap(),
AbsolutePathBuf::try_from(repo_b_root).unwrap(),
],
)
.unwrap();
let repo_a_marketplace = marketplaces
.iter()
.find(|marketplace| {
marketplace.path == tmp.path().join("repo-a/.agents/plugins/marketplace.json")
})
.expect("repo-a marketplace should be listed");
assert_eq!(
repo_a_marketplace.plugins,
vec![ConfiguredMarketplacePluginSummary {
name: "dup-plugin".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: tmp.path().join("repo-a/.agents/plugins/from-a"),
},
enabled: true,
}]
);
let repo_b_marketplace = marketplaces
.iter()
.find(|marketplace| {
marketplace.path == tmp.path().join("repo-b/.agents/plugins/marketplace.json")
})
.expect("repo-b marketplace should be listed");
assert_eq!(
repo_b_marketplace.plugins,
vec![ConfiguredMarketplacePluginSummary {
name: "b-only-plugin".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: tmp.path().join("repo-b/.agents/plugins/from-b-only"),
},
enabled: false,
}]
);
let duplicate_plugin_count = marketplaces
.iter()
.flat_map(|marketplace| marketplace.plugins.iter())
.filter(|plugin| plugin.name == "dup-plugin")
.count();
assert_eq!(duplicate_plugin_count, 1);
}
}

View File

@@ -18,6 +18,24 @@ pub struct ResolvedMarketplacePlugin {
pub source_path: AbsolutePathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MarketplaceSummary {
pub name: String,
pub path: PathBuf,
pub plugins: Vec<MarketplacePluginSummary>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MarketplacePluginSummary {
pub name: String,
pub source: MarketplacePluginSourceSummary,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MarketplacePluginSourceSummary {
Local { path: PathBuf },
}
#[derive(Debug, thiserror::Error)]
pub enum MarketplaceError {
#[error("{context}: {source}")]
@@ -27,6 +45,9 @@ pub enum MarketplaceError {
source: io::Error,
},
#[error("marketplace file `{path}` does not exist")]
MarketplaceNotFound { path: PathBuf },
#[error("invalid marketplace file `{path}`: {message}")]
InvalidMarketplaceFile { path: PathBuf, message: String },
@@ -36,14 +57,6 @@ pub enum MarketplaceError {
marketplace_name: String,
},
#[error(
"multiple marketplace plugin entries matched `{plugin_name}` in marketplace `{marketplace_name}`"
)]
DuplicatePlugin {
plugin_name: String,
marketplace_name: String,
},
#[error("{0}")]
InvalidPlugin(String),
}
@@ -54,77 +67,97 @@ impl MarketplaceError {
}
}
// For now, marketplace discovery always reads from disk so installs see the latest
// marketplace.json contents without any in-memory cache invalidation.
// Always read the specified marketplace file from disk so installs see the
// latest marketplace.json contents without any in-memory cache invalidation.
pub fn resolve_marketplace_plugin(
cwd: &Path,
marketplace_path: &AbsolutePathBuf,
plugin_name: &str,
marketplace_name: &str,
) -> Result<ResolvedMarketplacePlugin, MarketplaceError> {
resolve_marketplace_plugin_from_paths(
&discover_marketplace_paths(cwd),
plugin_name,
marketplace_name,
)
}
let marketplace = load_marketplace(marketplace_path.as_path())?;
let marketplace_name = marketplace.name;
let plugin = marketplace
.plugins
.into_iter()
.find(|plugin| plugin.name == plugin_name);
fn resolve_marketplace_plugin_from_paths(
marketplace_paths: &[PathBuf],
plugin_name: &str,
marketplace_name: &str,
) -> Result<ResolvedMarketplacePlugin, MarketplaceError> {
for marketplace_path in marketplace_paths {
let marketplace = load_marketplace(marketplace_path)?;
let discovered_marketplace_name = marketplace.name;
let mut matches = marketplace
.plugins
.into_iter()
.filter(|plugin| plugin.name == plugin_name)
.collect::<Vec<_>>();
let Some(plugin) = plugin else {
return Err(MarketplaceError::PluginNotFound {
plugin_name: plugin_name.to_string(),
marketplace_name,
});
};
if discovered_marketplace_name != marketplace_name || matches.is_empty() {
continue;
}
if matches.len() > 1 {
return Err(MarketplaceError::DuplicatePlugin {
plugin_name: plugin_name.to_string(),
marketplace_name: marketplace_name.to_string(),
});
}
if let Some(plugin) = matches.pop() {
let plugin_id = PluginId::new(plugin.name, marketplace_name.to_string()).map_err(
|err| match err {
PluginIdError::Invalid(message) => MarketplaceError::InvalidPlugin(message),
},
)?;
return Ok(ResolvedMarketplacePlugin {
plugin_id,
source_path: resolve_plugin_source_path(marketplace_path, plugin.source)?,
});
}
}
Err(MarketplaceError::PluginNotFound {
plugin_name: plugin_name.to_string(),
marketplace_name: marketplace_name.to_string(),
let plugin_id = PluginId::new(plugin.name, marketplace_name).map_err(|err| match err {
PluginIdError::Invalid(message) => MarketplaceError::InvalidPlugin(message),
})?;
Ok(ResolvedMarketplacePlugin {
plugin_id,
source_path: resolve_plugin_source_path(marketplace_path.as_path(), plugin.source)?,
})
}
fn discover_marketplace_paths(cwd: &Path) -> Vec<PathBuf> {
pub fn list_marketplaces(
additional_roots: &[AbsolutePathBuf],
) -> Result<Vec<MarketplaceSummary>, MarketplaceError> {
list_marketplaces_with_home(additional_roots, home_dir().as_deref())
}
fn list_marketplaces_with_home(
additional_roots: &[AbsolutePathBuf],
home_dir: Option<&Path>,
) -> Result<Vec<MarketplaceSummary>, MarketplaceError> {
let mut marketplaces = Vec::new();
for marketplace_path in discover_marketplace_paths_from_roots(additional_roots, home_dir) {
let marketplace = load_marketplace(marketplace_path.as_path())?;
let mut plugins = Vec::new();
for plugin in marketplace.plugins {
let source = match plugin.source {
MarketplacePluginSource::Local { path } => MarketplacePluginSourceSummary::Local {
path: resolve_plugin_source_path(
marketplace_path.as_path(),
MarketplacePluginSource::Local { path },
)?
.into_path_buf(),
},
};
plugins.push(MarketplacePluginSummary {
name: plugin.name,
source,
});
}
marketplaces.push(MarketplaceSummary {
name: marketplace.name,
path: marketplace_path,
plugins,
});
}
Ok(marketplaces)
}
fn discover_marketplace_paths_from_roots(
additional_roots: &[AbsolutePathBuf],
home_dir: Option<&Path>,
) -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Some(repo_root) = get_git_repo_root(cwd) {
let path = repo_root.join(MARKETPLACE_RELATIVE_PATH);
if let Some(home) = home_dir {
let path = home.join(MARKETPLACE_RELATIVE_PATH);
if path.is_file() {
paths.push(path);
}
}
if let Some(home) = home_dir() {
let path = home.join(MARKETPLACE_RELATIVE_PATH);
if path.is_file() {
paths.push(path);
for root in additional_roots {
if let Some(repo_root) = get_git_repo_root(root.as_path()) {
let path = repo_root.join(MARKETPLACE_RELATIVE_PATH);
if path.is_file() && !paths.contains(&path) {
paths.push(path);
}
}
}
@@ -132,8 +165,15 @@ fn discover_marketplace_paths(cwd: &Path) -> Vec<PathBuf> {
}
fn load_marketplace(path: &Path) -> Result<MarketplaceFile, MarketplaceError> {
let contents = fs::read_to_string(path)
.map_err(|err| MarketplaceError::io("failed to read marketplace file", err))?;
let contents = fs::read_to_string(path).map_err(|err| {
if err.kind() == io::ErrorKind::NotFound {
MarketplaceError::MarketplaceNotFound {
path: path.to_path_buf(),
}
} else {
MarketplaceError::io("failed to read marketplace file", err)
}
})?;
serde_json::from_str(&contents).map_err(|err| MarketplaceError::InvalidMarketplaceFile {
path: path.to_path_buf(),
message: err.to_string(),
@@ -233,9 +273,11 @@ mod tests {
)
.unwrap();
let resolved =
resolve_marketplace_plugin(&repo_root.join("nested"), "local-plugin", "codex-curated")
.unwrap();
let resolved = resolve_marketplace_plugin(
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
"local-plugin",
)
.unwrap();
assert_eq!(
resolved,
@@ -260,7 +302,11 @@ mod tests {
)
.unwrap();
let err = resolve_marketplace_plugin(&repo_root, "missing", "codex-curated").unwrap_err();
let err = resolve_marketplace_plugin(
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
"missing",
)
.unwrap_err();
assert_eq!(
err.to_string(),
@@ -269,7 +315,112 @@ mod tests {
}
#[test]
fn resolve_marketplace_plugin_prefers_repo_over_home_for_same_plugin() {
fn list_marketplaces_returns_home_and_repo_marketplaces() {
let tmp = tempdir().unwrap();
let home_root = tmp.path().join("home");
let repo_root = tmp.path().join("repo");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(home_root.join(".agents/plugins")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::write(
home_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "shared-plugin",
"source": {
"source": "local",
"path": "./home-shared"
}
},
{
"name": "home-only",
"source": {
"source": "local",
"path": "./home-only"
}
}
]
}"#,
)
.unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "shared-plugin",
"source": {
"source": "local",
"path": "./repo-shared"
}
},
{
"name": "repo-only",
"source": {
"source": "local",
"path": "./repo-only"
}
}
]
}"#,
)
.unwrap();
let marketplaces = list_marketplaces_with_home(
&[AbsolutePathBuf::try_from(repo_root.clone()).unwrap()],
Some(&home_root),
)
.unwrap();
assert_eq!(
marketplaces,
vec![
MarketplaceSummary {
name: "codex-curated".to_string(),
path: home_root.join(".agents/plugins/marketplace.json"),
plugins: vec![
MarketplacePluginSummary {
name: "shared-plugin".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: home_root.join(".agents/plugins/home-shared"),
},
},
MarketplacePluginSummary {
name: "home-only".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: home_root.join(".agents/plugins/home-only"),
},
},
],
},
MarketplaceSummary {
name: "codex-curated".to_string(),
path: repo_root.join(".agents/plugins/marketplace.json"),
plugins: vec![
MarketplacePluginSummary {
name: "shared-plugin".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: repo_root.join(".agents/plugins/repo-shared"),
},
},
MarketplacePluginSummary {
name: "repo-only".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: repo_root.join(".agents/plugins/repo-only"),
},
},
],
},
]
);
}
#[test]
fn list_marketplaces_keeps_distinct_entries_for_same_name() {
let tmp = tempdir().unwrap();
let home_root = tmp.path().join("home");
let repo_root = tmp.path().join("repo");
@@ -313,23 +464,97 @@ mod tests {
)
.unwrap();
let resolved = resolve_marketplace_plugin_from_paths(
&[repo_marketplace, home_marketplace],
"local-plugin",
"codex-curated",
let marketplaces = list_marketplaces_with_home(
&[AbsolutePathBuf::try_from(repo_root.clone()).unwrap()],
Some(&home_root),
)
.unwrap();
assert_eq!(
resolved,
ResolvedMarketplacePlugin {
plugin_id: PluginId::new("local-plugin".to_string(), "codex-curated".to_string())
.unwrap(),
source_path: AbsolutePathBuf::try_from(
repo_root.join(".agents/plugins/repo-plugin"),
)
.unwrap(),
}
marketplaces,
vec![
MarketplaceSummary {
name: "codex-curated".to_string(),
path: home_marketplace,
plugins: vec![MarketplacePluginSummary {
name: "local-plugin".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: home_root.join(".agents/plugins/home-plugin"),
},
}],
},
MarketplaceSummary {
name: "codex-curated".to_string(),
path: repo_marketplace.clone(),
plugins: vec![MarketplacePluginSummary {
name: "local-plugin".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: repo_root.join(".agents/plugins/repo-plugin"),
},
}],
},
]
);
let resolved = resolve_marketplace_plugin(
&AbsolutePathBuf::try_from(repo_marketplace).unwrap(),
"local-plugin",
)
.unwrap();
assert_eq!(
resolved.source_path,
AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/repo-plugin")).unwrap()
);
}
#[test]
fn list_marketplaces_dedupes_multiple_roots_in_same_repo() {
let tmp = tempdir().unwrap();
let repo_root = tmp.path().join("repo");
let nested_root = repo_root.join("nested/project");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::create_dir_all(&nested_root).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "local-plugin",
"source": {
"source": "local",
"path": "./plugin"
}
}
]
}"#,
)
.unwrap();
let marketplaces = list_marketplaces_with_home(
&[
AbsolutePathBuf::try_from(repo_root.clone()).unwrap(),
AbsolutePathBuf::try_from(nested_root).unwrap(),
],
None,
)
.unwrap();
assert_eq!(
marketplaces,
vec![MarketplaceSummary {
name: "codex-curated".to_string(),
path: repo_root.join(".agents/plugins/marketplace.json"),
plugins: vec![MarketplacePluginSummary {
name: "local-plugin".to_string(),
source: MarketplacePluginSourceSummary::Local {
path: repo_root.join(".agents/plugins/plugin"),
},
}],
}]
);
}
@@ -356,8 +581,11 @@ mod tests {
)
.unwrap();
let err =
resolve_marketplace_plugin(&repo_root, "local-plugin", "codex-curated").unwrap_err();
let err = resolve_marketplace_plugin(
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
"local-plugin",
)
.unwrap_err();
assert_eq!(
err.to_string(),
@@ -367,4 +595,46 @@ mod tests {
)
);
}
#[test]
fn resolve_marketplace_plugin_uses_first_duplicate_entry() {
let tmp = tempdir().unwrap();
let repo_root = tmp.path().join("repo");
fs::create_dir_all(repo_root.join(".git")).unwrap();
fs::create_dir_all(repo_root.join(".agents/plugins")).unwrap();
fs::write(
repo_root.join(".agents/plugins/marketplace.json"),
r#"{
"name": "codex-curated",
"plugins": [
{
"name": "local-plugin",
"source": {
"source": "local",
"path": "./first"
}
},
{
"name": "local-plugin",
"source": {
"source": "local",
"path": "./second"
}
}
]
}"#,
)
.unwrap();
let resolved = resolve_marketplace_plugin(
&AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json")).unwrap(),
"local-plugin",
)
.unwrap();
assert_eq!(
resolved.source_path,
AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/first")).unwrap()
);
}
}

View File

@@ -7,6 +7,8 @@ mod store;
pub(crate) use injection::build_plugin_injections;
pub use manager::AppConnectorId;
pub use manager::ConfiguredMarketplacePluginSummary;
pub use manager::ConfiguredMarketplaceSummary;
pub use manager::LoadedPlugin;
pub use manager::PluginCapabilitySummary;
pub use manager::PluginInstallError;
@@ -16,6 +18,8 @@ pub use manager::PluginsManager;
pub(crate) use manager::plugin_namespace_for_skill_path;
pub(crate) use manifest::load_plugin_manifest;
pub(crate) use manifest::plugin_manifest_name;
pub use marketplace::MarketplaceError;
pub use marketplace::MarketplacePluginSourceSummary;
pub(crate) use render::render_explicit_plugin_instructions;
pub(crate) use render::render_plugins_section;
pub use store::PluginId;