mirror of
https://github.com/openai/codex.git
synced 2026-04-24 06:35:50 +00:00
Improve external agent plugin migration for configured marketplaces
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
use crate::plugins::MarketplaceAddRequest;
|
||||
use crate::plugins::MarketplacePluginInstallPolicy;
|
||||
use crate::plugins::PluginId;
|
||||
use crate::plugins::PluginInstallRequest;
|
||||
use crate::plugins::PluginsManager;
|
||||
use crate::plugins::add_marketplace;
|
||||
use crate::plugins::find_marketplace_manifest_path;
|
||||
use crate::plugins::load_marketplace;
|
||||
use crate::plugins::marketplace_install_root;
|
||||
use crate::plugins::parse_marketplace_source;
|
||||
use crate::plugins::resolve_configured_marketplace_root;
|
||||
use crate::plugins::validate_plugin_segment;
|
||||
use codex_protocol::protocol::Product;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashSet;
|
||||
@@ -15,8 +23,8 @@ use toml::Value as TomlValue;
|
||||
|
||||
const EXTERNAL_AGENT_CONFIG_DETECT_METRIC: &str = "codex.external_agent_config.detect";
|
||||
const EXTERNAL_AGENT_CONFIG_IMPORT_METRIC: &str = "codex.external_agent_config.import";
|
||||
// Installed marketplace roots always expose their manifest at this relative path.
|
||||
const INSTALLED_MARKETPLACE_MANIFEST_RELATIVE_PATH: &str = ".agents/plugins/marketplace.json";
|
||||
const EXTERNAL_AGENT_DIR: &str = ".claude";
|
||||
const EXTERNAL_AGENT_CONFIG_MD: &str = "CLAUDE.md";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ExternalAgentConfigDetectOptions {
|
||||
@@ -137,22 +145,11 @@ impl ExternalAgentConfigService {
|
||||
let cwd = migration_item.cwd;
|
||||
let details = migration_item.details;
|
||||
tokio::spawn(async move {
|
||||
match service.import_plugins(cwd.as_deref(), details).await {
|
||||
Ok(outcome) => {
|
||||
tracing::info!(
|
||||
succeeded_marketplaces = outcome.succeeded_marketplaces.len(),
|
||||
succeeded_plugin_ids = outcome.succeeded_plugin_ids.len(),
|
||||
failed_marketplaces = outcome.failed_marketplaces.len(),
|
||||
failed_plugin_ids = outcome.failed_plugin_ids.len(),
|
||||
"external agent config plugin import completed"
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"external agent config plugin import failed"
|
||||
);
|
||||
}
|
||||
if let Err(err) = service.import_plugins(cwd.as_deref(), details).await {
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"external agent config plugin import failed"
|
||||
);
|
||||
}
|
||||
});
|
||||
emit_migration_metric(
|
||||
@@ -173,10 +170,12 @@ impl ExternalAgentConfigService {
|
||||
repo_root: Option<&Path>,
|
||||
items: &mut Vec<ExternalAgentConfigMigrationItem>,
|
||||
) -> io::Result<()> {
|
||||
let configured_plugin_ids = configured_plugin_ids(&self.codex_home)?;
|
||||
let configured_marketplace_plugins = configured_marketplace_plugins(&self.codex_home)?;
|
||||
let cwd = repo_root.map(Path::to_path_buf);
|
||||
let source_settings = repo_root.map_or_else(
|
||||
|| self.external_agent_home.join("settings.json"),
|
||||
|repo_root| repo_root.join(".claude").join("settings.json"),
|
||||
|repo_root| repo_root.join(EXTERNAL_AGENT_DIR).join("settings.json"),
|
||||
);
|
||||
let settings = read_external_settings(&source_settings)?;
|
||||
let target_config = repo_root.map_or_else(
|
||||
@@ -223,12 +222,14 @@ impl ExternalAgentConfigService {
|
||||
source_settings.as_path(),
|
||||
cwd.clone(),
|
||||
settings.as_ref(),
|
||||
&configured_plugin_ids,
|
||||
&configured_marketplace_plugins,
|
||||
items,
|
||||
);
|
||||
|
||||
let source_skills = repo_root.map_or_else(
|
||||
|| self.external_agent_home.join("skills"),
|
||||
|repo_root| repo_root.join(".claude").join("skills"),
|
||||
|repo_root| repo_root.join(EXTERNAL_AGENT_DIR).join("skills"),
|
||||
);
|
||||
let target_skills = repo_root.map_or_else(
|
||||
|| self.home_target_skills_dir(),
|
||||
@@ -239,7 +240,7 @@ impl ExternalAgentConfigService {
|
||||
items.push(ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::Skills,
|
||||
description: format!(
|
||||
"Copy skill folders from {} to {}",
|
||||
"Migrate skills from {} to {}",
|
||||
source_skills.display(),
|
||||
target_skills.display()
|
||||
),
|
||||
@@ -256,7 +257,7 @@ impl ExternalAgentConfigService {
|
||||
let source_agents_md = if let Some(repo_root) = repo_root {
|
||||
find_repo_agents_md_source(repo_root)?
|
||||
} else {
|
||||
let path = self.external_agent_home.join("CLAUDE.md");
|
||||
let path = self.external_agent_home.join(EXTERNAL_AGENT_CONFIG_MD);
|
||||
is_non_empty_text_file(&path)?.then_some(path)
|
||||
};
|
||||
let target_agents_md = repo_root.map_or_else(
|
||||
@@ -298,9 +299,17 @@ impl ExternalAgentConfigService {
|
||||
source_settings: &Path,
|
||||
cwd: Option<PathBuf>,
|
||||
settings: Option<&JsonValue>,
|
||||
configured_plugin_ids: &HashSet<String>,
|
||||
configured_marketplace_plugins: &BTreeMap<String, HashSet<String>>,
|
||||
items: &mut Vec<ExternalAgentConfigMigrationItem>,
|
||||
) {
|
||||
let Some(plugin_details) = settings.and_then(extract_plugin_migration_details) else {
|
||||
let Some(plugin_details) = settings.and_then(|settings| {
|
||||
extract_plugin_migration_details(
|
||||
settings,
|
||||
configured_plugin_ids,
|
||||
configured_marketplace_plugins,
|
||||
)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -336,8 +345,13 @@ impl ExternalAgentConfigService {
|
||||
.iter()
|
||||
.map(|plugin_name| format!("{plugin_name}@{marketplace_name}"))
|
||||
.collect::<Vec<_>>();
|
||||
let import_source =
|
||||
read_marketplace_import_source(cwd, &self.external_agent_home, &marketplace_name)?;
|
||||
let source_settings = cwd.map_or_else(
|
||||
|| self.external_agent_home.join("settings.json"),
|
||||
|cwd| cwd.join(EXTERNAL_AGENT_DIR).join("settings.json"),
|
||||
);
|
||||
let import_source = read_external_settings(&source_settings)?.and_then(|settings| {
|
||||
collect_marketplace_import_sources(&settings).remove(&marketplace_name)
|
||||
});
|
||||
let Some(import_source) = import_source else {
|
||||
outcome.failed_marketplaces.push(marketplace_name);
|
||||
outcome.failed_plugin_ids.extend(plugin_ids);
|
||||
@@ -351,12 +365,17 @@ impl ExternalAgentConfigService {
|
||||
let add_marketplace_outcome = add_marketplace(self.codex_home.clone(), request).await;
|
||||
let marketplace_path = match add_marketplace_outcome {
|
||||
Ok(add_marketplace_outcome) => {
|
||||
let Some(marketplace_path) = find_marketplace_manifest_path(
|
||||
add_marketplace_outcome.installed_root.as_path(),
|
||||
) else {
|
||||
outcome.failed_marketplaces.push(marketplace_name);
|
||||
outcome.failed_plugin_ids.extend(plugin_ids);
|
||||
continue;
|
||||
};
|
||||
outcome
|
||||
.succeeded_marketplaces
|
||||
.push(marketplace_name.clone());
|
||||
add_marketplace_outcome
|
||||
.installed_root
|
||||
.join(INSTALLED_MARKETPLACE_MANIFEST_RELATIVE_PATH)
|
||||
marketplace_path
|
||||
}
|
||||
Err(_) => {
|
||||
outcome.failed_marketplaces.push(marketplace_name);
|
||||
@@ -388,7 +407,7 @@ impl ExternalAgentConfigService {
|
||||
fn import_config(&self, cwd: Option<&Path>) -> io::Result<()> {
|
||||
let (source_settings, target_config) = if let Some(repo_root) = find_repo_root(cwd)? {
|
||||
(
|
||||
repo_root.join(".claude").join("settings.json"),
|
||||
repo_root.join(EXTERNAL_AGENT_DIR).join("settings.json"),
|
||||
repo_root.join(".codex").join("config.toml"),
|
||||
)
|
||||
} else if cwd.is_some_and(|cwd| !cwd.as_os_str().is_empty()) {
|
||||
@@ -440,7 +459,7 @@ impl ExternalAgentConfigService {
|
||||
fn import_skills(&self, cwd: Option<&Path>) -> io::Result<usize> {
|
||||
let (source_skills, target_skills) = if let Some(repo_root) = find_repo_root(cwd)? {
|
||||
(
|
||||
repo_root.join(".claude").join("skills"),
|
||||
repo_root.join(EXTERNAL_AGENT_DIR).join("skills"),
|
||||
repo_root.join(".agents").join("skills"),
|
||||
)
|
||||
} else if cwd.is_some_and(|cwd| !cwd.as_os_str().is_empty()) {
|
||||
@@ -487,7 +506,7 @@ impl ExternalAgentConfigService {
|
||||
return Ok(());
|
||||
} else {
|
||||
(
|
||||
self.external_agent_home.join("CLAUDE.md"),
|
||||
self.external_agent_home.join(EXTERNAL_AGENT_CONFIG_MD),
|
||||
self.codex_home.join("AGENTS.md"),
|
||||
)
|
||||
};
|
||||
@@ -508,10 +527,10 @@ impl ExternalAgentConfigService {
|
||||
|
||||
fn default_external_agent_home() -> PathBuf {
|
||||
if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) {
|
||||
return PathBuf::from(home).join(".claude");
|
||||
return PathBuf::from(home).join(EXTERNAL_AGENT_DIR);
|
||||
}
|
||||
|
||||
PathBuf::from(".claude")
|
||||
PathBuf::from(EXTERNAL_AGENT_DIR)
|
||||
}
|
||||
|
||||
fn read_external_settings(path: &Path) -> io::Result<Option<JsonValue>> {
|
||||
@@ -525,12 +544,36 @@ fn read_external_settings(path: &Path) -> io::Result<Option<JsonValue>> {
|
||||
Ok(Some(settings))
|
||||
}
|
||||
|
||||
fn extract_plugin_migration_details(settings: &JsonValue) -> Option<MigrationDetails> {
|
||||
fn extract_plugin_migration_details(
|
||||
settings: &JsonValue,
|
||||
configured_plugin_ids: &HashSet<String>,
|
||||
configured_marketplace_plugins: &BTreeMap<String, HashSet<String>>,
|
||||
) -> Option<MigrationDetails> {
|
||||
let loadable_marketplaces = collect_marketplace_import_sources(settings)
|
||||
.into_iter()
|
||||
.filter_map(|(marketplace_name, source)| {
|
||||
parse_marketplace_source(&source.source, source.ref_name)
|
||||
.ok()
|
||||
.map(|_| marketplace_name)
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
let mut plugins = BTreeMap::new();
|
||||
for plugin_id in collect_enabled_plugins(settings) {
|
||||
for plugin_id in collect_enabled_plugins(settings)
|
||||
.into_iter()
|
||||
.filter(|plugin_id| !configured_plugin_ids.contains(plugin_id))
|
||||
{
|
||||
let Ok(plugin_id) = PluginId::parse(&plugin_id) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(installable_plugins) =
|
||||
configured_marketplace_plugins.get(&plugin_id.marketplace_name)
|
||||
{
|
||||
if !installable_plugins.contains(&plugin_id.plugin_name) {
|
||||
continue;
|
||||
}
|
||||
} else if !loadable_marketplaces.contains(&plugin_id.marketplace_name) {
|
||||
continue;
|
||||
}
|
||||
let plugin_group = plugins
|
||||
.entry(plugin_id.marketplace_name.clone())
|
||||
.or_insert_with(|| PluginsMigration {
|
||||
@@ -579,6 +622,97 @@ fn collect_enabled_plugins(settings: &JsonValue) -> Vec<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn configured_plugin_ids(codex_home: &Path) -> io::Result<HashSet<String>> {
|
||||
let config_path = codex_home.join("config.toml");
|
||||
if !config_path.is_file() {
|
||||
return Ok(HashSet::new());
|
||||
}
|
||||
|
||||
let raw_config = fs::read_to_string(&config_path)?;
|
||||
if raw_config.trim().is_empty() {
|
||||
return Ok(HashSet::new());
|
||||
}
|
||||
|
||||
let config = toml::from_str::<TomlValue>(&raw_config)
|
||||
.map_err(|err| invalid_data_error(format!("invalid config.toml: {err}")))?;
|
||||
let Some(plugins) = config.get("plugins").and_then(TomlValue::as_table) else {
|
||||
return Ok(HashSet::new());
|
||||
};
|
||||
|
||||
Ok(plugins.keys().cloned().collect())
|
||||
}
|
||||
|
||||
fn configured_marketplace_plugins(
|
||||
codex_home: &Path,
|
||||
) -> io::Result<BTreeMap<String, HashSet<String>>> {
|
||||
let default_install_root = marketplace_install_root(codex_home);
|
||||
let mut marketplace_roots = BTreeMap::new();
|
||||
let config_path = codex_home.join("config.toml");
|
||||
if !config_path.is_file() {
|
||||
return Ok(BTreeMap::new());
|
||||
}
|
||||
|
||||
let raw_config = fs::read_to_string(&config_path)?;
|
||||
if raw_config.trim().is_empty() {
|
||||
return Ok(BTreeMap::new());
|
||||
}
|
||||
|
||||
let config = toml::from_str::<TomlValue>(&raw_config)
|
||||
.map_err(|err| invalid_data_error(format!("invalid config.toml: {err}")))?;
|
||||
let Some(marketplaces) = config.get("marketplaces").and_then(TomlValue::as_table) else {
|
||||
return Ok(BTreeMap::new());
|
||||
};
|
||||
|
||||
for (marketplace_name, marketplace) in marketplaces {
|
||||
if validate_plugin_segment(marketplace_name, "marketplace name").is_err()
|
||||
|| !marketplace.is_table()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let Some(root) = resolve_configured_marketplace_root(
|
||||
marketplace_name,
|
||||
marketplace,
|
||||
&default_install_root,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let root = if root.is_relative() {
|
||||
config_path
|
||||
.parent()
|
||||
.unwrap_or_else(|| Path::new("."))
|
||||
.join(root)
|
||||
} else {
|
||||
root
|
||||
};
|
||||
marketplace_roots.insert(marketplace_name.clone(), root);
|
||||
}
|
||||
|
||||
let mut marketplace_plugins = BTreeMap::new();
|
||||
for (marketplace_name, root) in marketplace_roots {
|
||||
let Some(manifest_path) = find_marketplace_manifest_path(&root) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(marketplace) = load_marketplace(&manifest_path) else {
|
||||
continue;
|
||||
};
|
||||
let plugins =
|
||||
marketplace
|
||||
.plugins
|
||||
.into_iter()
|
||||
.filter(|plugin| {
|
||||
plugin.policy.installation != MarketplacePluginInstallPolicy::NotAvailable
|
||||
&& plugin.policy.products.as_deref().is_none_or(|products| {
|
||||
Product::Codex.matches_product_restriction(products)
|
||||
})
|
||||
})
|
||||
.map(|plugin| plugin.name)
|
||||
.collect::<HashSet<_>>();
|
||||
marketplace_plugins.insert(marketplace_name, plugins);
|
||||
}
|
||||
|
||||
Ok(marketplace_plugins)
|
||||
}
|
||||
|
||||
fn collect_marketplace_import_sources(
|
||||
settings: &JsonValue,
|
||||
) -> BTreeMap<String, MarketplaceImportSource> {
|
||||
@@ -631,22 +765,6 @@ struct MarketplaceImportSource {
|
||||
ref_name: Option<String>,
|
||||
}
|
||||
|
||||
fn read_marketplace_import_source(
|
||||
cwd: Option<&Path>,
|
||||
external_agent_home: &Path,
|
||||
marketplace_name: &str,
|
||||
) -> io::Result<Option<MarketplaceImportSource>> {
|
||||
let source_settings = cwd.map_or_else(
|
||||
|| external_agent_home.join("settings.json"),
|
||||
|cwd| cwd.join(".claude").join("settings.json"),
|
||||
);
|
||||
let Some(settings) = read_external_settings(&source_settings)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(collect_marketplace_import_sources(&settings).remove(marketplace_name))
|
||||
}
|
||||
|
||||
fn find_repo_root(cwd: Option<&Path>) -> io::Result<Option<PathBuf>> {
|
||||
let Some(cwd) = cwd.filter(|cwd| !cwd.as_os_str().is_empty()) else {
|
||||
return Ok(None);
|
||||
@@ -729,8 +847,10 @@ fn is_non_empty_text_file(path: &Path) -> io::Result<bool> {
|
||||
|
||||
fn find_repo_agents_md_source(repo_root: &Path) -> io::Result<Option<PathBuf>> {
|
||||
for candidate in [
|
||||
repo_root.join("CLAUDE.md"),
|
||||
repo_root.join(".claude").join("CLAUDE.md"),
|
||||
repo_root.join(EXTERNAL_AGENT_CONFIG_MD),
|
||||
repo_root
|
||||
.join(EXTERNAL_AGENT_DIR)
|
||||
.join(EXTERNAL_AGENT_CONFIG_MD),
|
||||
] {
|
||||
if is_non_empty_text_file(&candidate)? {
|
||||
return Ok(Some(candidate));
|
||||
@@ -779,7 +899,11 @@ fn rewrite_and_copy_text_file(source: &Path, target: &Path) -> io::Result<()> {
|
||||
}
|
||||
|
||||
fn rewrite_external_agent_terms(content: &str) -> String {
|
||||
let mut rewritten = replace_case_insensitive_with_boundaries(content, "claude.md", "AGENTS.md");
|
||||
let mut rewritten = replace_case_insensitive_with_boundaries(
|
||||
content,
|
||||
&EXTERNAL_AGENT_CONFIG_MD.to_ascii_lowercase(),
|
||||
"AGENTS.md",
|
||||
);
|
||||
for from in [
|
||||
"claude code",
|
||||
"claude-code",
|
||||
|
||||
@@ -5,13 +5,16 @@ use tempfile::TempDir;
|
||||
|
||||
fn fixture_paths() -> (TempDir, PathBuf, PathBuf) {
|
||||
let root = TempDir::new().expect("create tempdir");
|
||||
let claude_home = root.path().join(".claude");
|
||||
let external_agent_home = root.path().join(".claude");
|
||||
let codex_home = root.path().join(".codex");
|
||||
(root, claude_home, codex_home)
|
||||
(root, external_agent_home, codex_home)
|
||||
}
|
||||
|
||||
fn service_for_paths(claude_home: PathBuf, codex_home: PathBuf) -> ExternalAgentConfigService {
|
||||
ExternalAgentConfigService::new_for_test(codex_home, claude_home)
|
||||
fn service_for_paths(
|
||||
external_agent_home: PathBuf,
|
||||
codex_home: PathBuf,
|
||||
) -> ExternalAgentConfigService {
|
||||
ExternalAgentConfigService::new_for_test(codex_home, external_agent_home)
|
||||
}
|
||||
|
||||
fn github_plugin_details() -> MigrationDetails {
|
||||
@@ -25,20 +28,21 @@ fn github_plugin_details() -> MigrationDetails {
|
||||
|
||||
#[test]
|
||||
fn detect_home_lists_config_skills_and_agents_md() {
|
||||
let (_root, claude_home, codex_home) = fixture_paths();
|
||||
let (_root, external_agent_home, codex_home) = fixture_paths();
|
||||
let agents_skills = codex_home
|
||||
.parent()
|
||||
.map(|parent| parent.join(".agents").join("skills"))
|
||||
.unwrap_or_else(|| PathBuf::from(".agents").join("skills"));
|
||||
fs::create_dir_all(claude_home.join("skills").join("skill-a")).expect("create skills");
|
||||
fs::write(claude_home.join("CLAUDE.md"), "claude rules").expect("write claude md");
|
||||
fs::create_dir_all(external_agent_home.join("skills").join("skill-a")).expect("create skills");
|
||||
fs::write(external_agent_home.join("CLAUDE.md"), "claude rules")
|
||||
.expect("write external agent md");
|
||||
fs::write(
|
||||
claude_home.join("settings.json"),
|
||||
external_agent_home.join("settings.json"),
|
||||
r#"{"model":"claude","env":{"FOO":"bar"}}"#,
|
||||
)
|
||||
.expect("write settings");
|
||||
|
||||
let items = service_for_paths(claude_home.clone(), codex_home.clone())
|
||||
let items = service_for_paths(external_agent_home.clone(), codex_home.clone())
|
||||
.detect(ExternalAgentConfigDetectOptions {
|
||||
include_home: true,
|
||||
cwds: None,
|
||||
@@ -50,7 +54,7 @@ fn detect_home_lists_config_skills_and_agents_md() {
|
||||
item_type: ExternalAgentConfigMigrationItemType::Config,
|
||||
description: format!(
|
||||
"Migrate {} into {}",
|
||||
claude_home.join("settings.json").display(),
|
||||
external_agent_home.join("settings.json").display(),
|
||||
codex_home.join("config.toml").display()
|
||||
),
|
||||
cwd: None,
|
||||
@@ -59,8 +63,8 @@ fn detect_home_lists_config_skills_and_agents_md() {
|
||||
ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::Skills,
|
||||
description: format!(
|
||||
"Copy skill folders from {} to {}",
|
||||
claude_home.join("skills").display(),
|
||||
"Migrate skills from {} to {}",
|
||||
external_agent_home.join("skills").display(),
|
||||
agents_skills.display()
|
||||
),
|
||||
cwd: None,
|
||||
@@ -70,7 +74,7 @@ fn detect_home_lists_config_skills_and_agents_md() {
|
||||
item_type: ExternalAgentConfigMigrationItemType::AgentsMd,
|
||||
description: format!(
|
||||
"Import {} to {}",
|
||||
claude_home.join("CLAUDE.md").display(),
|
||||
external_agent_home.join("CLAUDE.md").display(),
|
||||
codex_home.join("AGENTS.md").display()
|
||||
),
|
||||
cwd: None,
|
||||
@@ -125,25 +129,32 @@ fn detect_repo_lists_agents_md_for_each_cwd() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn import_home_migrates_supported_config_fields_skills_and_agents_md() {
|
||||
let (_root, claude_home, codex_home) = fixture_paths();
|
||||
let (_root, external_agent_home, codex_home) = fixture_paths();
|
||||
let agents_skills = codex_home
|
||||
.parent()
|
||||
.map(|parent| parent.join(".agents").join("skills"))
|
||||
.unwrap_or_else(|| PathBuf::from(".agents").join("skills"));
|
||||
fs::create_dir_all(claude_home.join("skills").join("skill-a")).expect("create skills");
|
||||
fs::create_dir_all(external_agent_home.join("skills").join("skill-a")).expect("create skills");
|
||||
fs::write(
|
||||
claude_home.join("settings.json"),
|
||||
external_agent_home.join("settings.json"),
|
||||
r#"{"model":"claude","permissions":{"ask":["git push"]},"env":{"FOO":"bar","CI":false,"MAX_RETRIES":3,"MY_TEAM":"codex","IGNORED":null,"LIST":["a","b"],"MAP":{"x":1}},"sandbox":{"enabled":true,"network":{"allowLocalBinding":true}}}"#,
|
||||
)
|
||||
.expect("write settings");
|
||||
fs::write(
|
||||
claude_home.join("skills").join("skill-a").join("SKILL.md"),
|
||||
external_agent_home
|
||||
.join("skills")
|
||||
.join("skill-a")
|
||||
.join("SKILL.md"),
|
||||
"Use Claude Code and CLAUDE utilities.",
|
||||
)
|
||||
.expect("write skill");
|
||||
fs::write(claude_home.join("CLAUDE.md"), "Claude code guidance").expect("write agents");
|
||||
fs::write(
|
||||
external_agent_home.join("CLAUDE.md"),
|
||||
"Claude code guidance",
|
||||
)
|
||||
.expect("write agents");
|
||||
|
||||
service_for_paths(claude_home, codex_home.clone())
|
||||
service_for_paths(external_agent_home, codex_home.clone())
|
||||
.import(vec![
|
||||
ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::AgentsMd,
|
||||
@@ -185,15 +196,15 @@ async fn import_home_migrates_supported_config_fields_skills_and_agents_md() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn import_home_skips_empty_config_migration() {
|
||||
let (_root, claude_home, codex_home) = fixture_paths();
|
||||
fs::create_dir_all(&claude_home).expect("create claude home");
|
||||
let (_root, external_agent_home, codex_home) = fixture_paths();
|
||||
fs::create_dir_all(&external_agent_home).expect("create external agent home");
|
||||
fs::write(
|
||||
claude_home.join("settings.json"),
|
||||
external_agent_home.join("settings.json"),
|
||||
r#"{"model":"claude","sandbox":{"enabled":false}}"#,
|
||||
)
|
||||
.expect("write settings");
|
||||
|
||||
service_for_paths(claude_home, codex_home.clone())
|
||||
service_for_paths(external_agent_home, codex_home.clone())
|
||||
.import(vec![ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::Config,
|
||||
description: String::new(),
|
||||
@@ -208,11 +219,11 @@ async fn import_home_skips_empty_config_migration() {
|
||||
|
||||
#[test]
|
||||
fn detect_home_skips_config_when_target_already_has_supported_fields() {
|
||||
let (_root, claude_home, codex_home) = fixture_paths();
|
||||
fs::create_dir_all(&claude_home).expect("create claude home");
|
||||
let (_root, external_agent_home, codex_home) = fixture_paths();
|
||||
fs::create_dir_all(&external_agent_home).expect("create external agent home");
|
||||
fs::create_dir_all(&codex_home).expect("create codex home");
|
||||
fs::write(
|
||||
claude_home.join("settings.json"),
|
||||
external_agent_home.join("settings.json"),
|
||||
r#"{"env":{"FOO":"bar"},"sandbox":{"enabled":true}}"#,
|
||||
)
|
||||
.expect("write settings");
|
||||
@@ -230,7 +241,7 @@ fn detect_home_skips_config_when_target_already_has_supported_fields() {
|
||||
)
|
||||
.expect("write config");
|
||||
|
||||
let items = service_for_paths(claude_home, codex_home)
|
||||
let items = service_for_paths(external_agent_home, codex_home)
|
||||
.detect(ExternalAgentConfigDetectOptions {
|
||||
include_home: true,
|
||||
cwds: None,
|
||||
@@ -242,15 +253,15 @@ fn detect_home_skips_config_when_target_already_has_supported_fields() {
|
||||
|
||||
#[test]
|
||||
fn detect_home_skips_skills_when_all_skill_directories_exist() {
|
||||
let (_root, claude_home, codex_home) = fixture_paths();
|
||||
let (_root, external_agent_home, codex_home) = fixture_paths();
|
||||
let agents_skills = codex_home
|
||||
.parent()
|
||||
.map(|parent| parent.join(".agents").join("skills"))
|
||||
.unwrap_or_else(|| PathBuf::from(".agents").join("skills"));
|
||||
fs::create_dir_all(claude_home.join("skills").join("skill-a")).expect("create source");
|
||||
fs::create_dir_all(external_agent_home.join("skills").join("skill-a")).expect("create source");
|
||||
fs::create_dir_all(agents_skills.join("skill-a")).expect("create target");
|
||||
|
||||
let items = service_for_paths(claude_home, codex_home)
|
||||
let items = service_for_paths(external_agent_home, codex_home)
|
||||
.detect(ExternalAgentConfigDetectOptions {
|
||||
include_home: true,
|
||||
cwds: None,
|
||||
@@ -333,7 +344,7 @@ async fn import_repo_agents_md_overwrites_empty_targets() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_repo_prefers_non_empty_dot_claude_agents_source() {
|
||||
fn detect_repo_prefers_non_empty_external_agent_agents_source() {
|
||||
let root = TempDir::new().expect("create tempdir");
|
||||
let repo_root = root.path().join("repo");
|
||||
fs::create_dir_all(repo_root.join(".git")).expect("create git");
|
||||
@@ -368,7 +379,7 @@ fn detect_repo_prefers_non_empty_dot_claude_agents_source() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn import_repo_uses_non_empty_dot_claude_agents_source() {
|
||||
async fn import_repo_uses_non_empty_external_agent_agents_source() {
|
||||
let root = TempDir::new().expect("create tempdir");
|
||||
let repo_root = root.path().join("repo");
|
||||
fs::create_dir_all(repo_root.join(".git")).expect("create git");
|
||||
@@ -409,21 +420,26 @@ fn migration_metric_tags_for_skills_include_skills_count() {
|
||||
|
||||
#[test]
|
||||
fn detect_home_lists_enabled_plugins_from_settings() {
|
||||
let (_root, claude_home, codex_home) = fixture_paths();
|
||||
fs::create_dir_all(&claude_home).expect("create claude home");
|
||||
let (_root, external_agent_home, codex_home) = fixture_paths();
|
||||
fs::create_dir_all(&external_agent_home).expect("create external agent home");
|
||||
fs::write(
|
||||
claude_home.join("settings.json"),
|
||||
external_agent_home.join("settings.json"),
|
||||
r#"{
|
||||
"enabledPlugins": {
|
||||
"formatter@acme-tools": true,
|
||||
"deployer@acme-tools": true,
|
||||
"analyzer@security-plugins": false
|
||||
},
|
||||
"extraKnownMarketplaces": {
|
||||
"acme-tools": {
|
||||
"source": "acme-corp/claude-plugins"
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("write settings");
|
||||
|
||||
let items = service_for_paths(claude_home.clone(), codex_home)
|
||||
let items = service_for_paths(external_agent_home.clone(), codex_home)
|
||||
.detect(ExternalAgentConfigDetectOptions {
|
||||
include_home: true,
|
||||
cwds: None,
|
||||
@@ -436,7 +452,7 @@ fn detect_home_lists_enabled_plugins_from_settings() {
|
||||
item_type: ExternalAgentConfigMigrationItemType::Plugins,
|
||||
description: format!(
|
||||
"Import enabled plugins from {}",
|
||||
claude_home.join("settings.json").display()
|
||||
external_agent_home.join("settings.json").display()
|
||||
),
|
||||
cwd: None,
|
||||
details: Some(MigrationDetails {
|
||||
@@ -449,11 +465,153 @@ fn detect_home_lists_enabled_plugins_from_settings() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_repo_skips_plugins_that_are_already_configured_in_codex() {
|
||||
let root = TempDir::new().expect("create tempdir");
|
||||
let external_agent_home = root.path().join(".claude");
|
||||
let codex_home = root.path().join(".codex");
|
||||
let repo_root = root.path().join("repo");
|
||||
fs::create_dir_all(repo_root.join(".git")).expect("create git dir");
|
||||
fs::create_dir_all(repo_root.join(".claude")).expect("create repo external agent dir");
|
||||
fs::create_dir_all(&codex_home).expect("create codex home");
|
||||
fs::write(
|
||||
repo_root.join(".claude").join("settings.json"),
|
||||
r#"{
|
||||
"enabledPlugins": {
|
||||
"formatter@acme-tools": true,
|
||||
"deployer@acme-tools": true
|
||||
},
|
||||
"extraKnownMarketplaces": {
|
||||
"acme-tools": {
|
||||
"source": "acme-corp/claude-plugins"
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("write repo settings");
|
||||
fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
r#"
|
||||
[plugins."formatter@acme-tools"]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.expect("write codex config");
|
||||
|
||||
let items = service_for_paths(external_agent_home, codex_home)
|
||||
.detect(ExternalAgentConfigDetectOptions {
|
||||
include_home: false,
|
||||
cwds: Some(vec![repo_root.clone()]),
|
||||
})
|
||||
.expect("detect");
|
||||
|
||||
assert_eq!(
|
||||
items,
|
||||
vec![ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::Plugins,
|
||||
description: format!(
|
||||
"Import enabled plugins from {}",
|
||||
repo_root.join(".claude").join("settings.json").display()
|
||||
),
|
||||
cwd: Some(repo_root),
|
||||
details: Some(MigrationDetails {
|
||||
plugins: vec![PluginsMigration {
|
||||
marketplace_name: "acme-tools".to_string(),
|
||||
plugin_names: vec!["deployer".to_string()],
|
||||
}],
|
||||
}),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_repo_skips_plugins_that_are_disabled_in_codex() {
|
||||
let root = TempDir::new().expect("create tempdir");
|
||||
let external_agent_home = root.path().join(".claude");
|
||||
let codex_home = root.path().join(".codex");
|
||||
let repo_root = root.path().join("repo");
|
||||
fs::create_dir_all(repo_root.join(".git")).expect("create git dir");
|
||||
fs::create_dir_all(repo_root.join(".claude")).expect("create repo external agent dir");
|
||||
fs::create_dir_all(&codex_home).expect("create codex home");
|
||||
fs::write(
|
||||
repo_root.join(".claude").join("settings.json"),
|
||||
r#"{
|
||||
"enabledPlugins": {
|
||||
"formatter@acme-tools": true
|
||||
},
|
||||
"extraKnownMarketplaces": {
|
||||
"acme-tools": {
|
||||
"source": "acme-corp/claude-plugins"
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("write repo settings");
|
||||
fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
r#"
|
||||
[plugins."formatter@acme-tools"]
|
||||
enabled = false
|
||||
"#,
|
||||
)
|
||||
.expect("write codex config");
|
||||
|
||||
let items = service_for_paths(external_agent_home, codex_home)
|
||||
.detect(ExternalAgentConfigDetectOptions {
|
||||
include_home: false,
|
||||
cwds: Some(vec![repo_root]),
|
||||
})
|
||||
.expect("detect");
|
||||
|
||||
assert_eq!(items, Vec::<ExternalAgentConfigMigrationItem>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_repo_skips_plugins_without_explicit_enabled_in_codex() {
|
||||
let root = TempDir::new().expect("create tempdir");
|
||||
let external_agent_home = root.path().join(".claude");
|
||||
let codex_home = root.path().join(".codex");
|
||||
let repo_root = root.path().join("repo");
|
||||
fs::create_dir_all(repo_root.join(".git")).expect("create git dir");
|
||||
fs::create_dir_all(repo_root.join(".claude")).expect("create repo external agent dir");
|
||||
fs::create_dir_all(&codex_home).expect("create codex home");
|
||||
fs::write(
|
||||
repo_root.join(".claude").join("settings.json"),
|
||||
r#"{
|
||||
"enabledPlugins": {
|
||||
"formatter@acme-tools": true
|
||||
},
|
||||
"extraKnownMarketplaces": {
|
||||
"acme-tools": {
|
||||
"source": "acme-corp/claude-plugins"
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("write repo settings");
|
||||
fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
r#"
|
||||
[plugins."formatter@acme-tools"]
|
||||
"#,
|
||||
)
|
||||
.expect("write codex config");
|
||||
|
||||
let items = service_for_paths(external_agent_home, codex_home)
|
||||
.detect(ExternalAgentConfigDetectOptions {
|
||||
include_home: false,
|
||||
cwds: Some(vec![repo_root]),
|
||||
})
|
||||
.expect("detect");
|
||||
|
||||
assert_eq!(items, Vec::<ExternalAgentConfigMigrationItem>::new());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn import_plugins_requires_details() {
|
||||
let (_root, claude_home, codex_home) = fixture_paths();
|
||||
let (_root, external_agent_home, codex_home) = fixture_paths();
|
||||
|
||||
let err = service_for_paths(claude_home, codex_home)
|
||||
let err = service_for_paths(external_agent_home, codex_home)
|
||||
.import_plugins(/*cwd*/ None, /*details*/ None)
|
||||
.await
|
||||
.expect_err("expected missing details error");
|
||||
@@ -462,12 +620,248 @@ async fn import_plugins_requires_details() {
|
||||
assert_eq!(err.to_string(), "plugins migration item is missing details");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_repo_does_not_skip_plugins_only_configured_in_project_codex() {
|
||||
let root = TempDir::new().expect("create tempdir");
|
||||
let external_agent_home = root.path().join(".claude");
|
||||
let codex_home = root.path().join(".codex");
|
||||
let repo_root = root.path().join("repo");
|
||||
fs::create_dir_all(repo_root.join(".git")).expect("create git dir");
|
||||
fs::create_dir_all(repo_root.join(".claude")).expect("create repo external agent dir");
|
||||
fs::create_dir_all(repo_root.join(".codex")).expect("create repo codex dir");
|
||||
fs::create_dir_all(&codex_home).expect("create codex home");
|
||||
fs::write(
|
||||
repo_root.join(".claude").join("settings.json"),
|
||||
r#"{
|
||||
"enabledPlugins": {
|
||||
"formatter@acme-tools": true
|
||||
},
|
||||
"extraKnownMarketplaces": {
|
||||
"acme-tools": {
|
||||
"source": "acme-corp/claude-plugins"
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("write repo settings");
|
||||
fs::write(
|
||||
repo_root.join(".codex").join("config.toml"),
|
||||
r#"
|
||||
[plugins."formatter@acme-tools"]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.expect("write project codex config");
|
||||
|
||||
let items = service_for_paths(external_agent_home, codex_home)
|
||||
.detect(ExternalAgentConfigDetectOptions {
|
||||
include_home: false,
|
||||
cwds: Some(vec![repo_root.clone()]),
|
||||
})
|
||||
.expect("detect");
|
||||
|
||||
assert_eq!(
|
||||
items,
|
||||
vec![ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::Plugins,
|
||||
description: format!(
|
||||
"Import enabled plugins from {}",
|
||||
repo_root.join(".claude").join("settings.json").display()
|
||||
),
|
||||
cwd: Some(repo_root),
|
||||
details: Some(MigrationDetails {
|
||||
plugins: vec![PluginsMigration {
|
||||
marketplace_name: "acme-tools".to_string(),
|
||||
plugin_names: vec!["formatter".to_string()],
|
||||
}],
|
||||
}),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_home_skips_plugins_without_marketplace_source() {
|
||||
let (_root, external_agent_home, codex_home) = fixture_paths();
|
||||
fs::create_dir_all(&external_agent_home).expect("create external agent home");
|
||||
fs::write(
|
||||
external_agent_home.join("settings.json"),
|
||||
r#"{
|
||||
"enabledPlugins": {
|
||||
"formatter@acme-tools": true
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("write settings");
|
||||
|
||||
let items = service_for_paths(external_agent_home, codex_home)
|
||||
.detect(ExternalAgentConfigDetectOptions {
|
||||
include_home: true,
|
||||
cwds: None,
|
||||
})
|
||||
.expect("detect");
|
||||
|
||||
assert_eq!(items, Vec::<ExternalAgentConfigMigrationItem>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_home_skips_plugins_with_invalid_marketplace_source() {
|
||||
let (_root, external_agent_home, codex_home) = fixture_paths();
|
||||
fs::create_dir_all(&external_agent_home).expect("create external agent home");
|
||||
fs::write(
|
||||
external_agent_home.join("settings.json"),
|
||||
r#"{
|
||||
"enabledPlugins": {
|
||||
"formatter@acme-tools": true
|
||||
},
|
||||
"extraKnownMarketplaces": {
|
||||
"acme-tools": {
|
||||
"source": "github"
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("write settings");
|
||||
|
||||
let items = service_for_paths(external_agent_home, codex_home)
|
||||
.detect(ExternalAgentConfigDetectOptions {
|
||||
include_home: true,
|
||||
cwds: None,
|
||||
})
|
||||
.expect("detect");
|
||||
|
||||
assert_eq!(items, Vec::<ExternalAgentConfigMigrationItem>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_repo_filters_plugins_against_installed_marketplace() {
|
||||
let root = TempDir::new().expect("create tempdir");
|
||||
let external_agent_home = root.path().join(".claude");
|
||||
let codex_home = root.path().join(".codex");
|
||||
let repo_root = root.path().join("repo");
|
||||
let marketplace_root = codex_home.join(".tmp").join("marketplaces").join("debug");
|
||||
fs::create_dir_all(repo_root.join(".git")).expect("create git dir");
|
||||
fs::create_dir_all(repo_root.join(".claude")).expect("create repo external agent dir");
|
||||
fs::create_dir_all(marketplace_root.join(".agents").join("plugins"))
|
||||
.expect("create marketplace manifest dir");
|
||||
fs::create_dir_all(
|
||||
marketplace_root
|
||||
.join("plugins")
|
||||
.join("sample")
|
||||
.join(".codex-plugin"),
|
||||
)
|
||||
.expect("create sample plugin");
|
||||
fs::create_dir_all(
|
||||
marketplace_root
|
||||
.join("plugins")
|
||||
.join("available")
|
||||
.join(".codex-plugin"),
|
||||
)
|
||||
.expect("create available plugin");
|
||||
fs::write(
|
||||
repo_root.join(".claude").join("settings.json"),
|
||||
r#"{
|
||||
"enabledPlugins": {
|
||||
"sample@debug": true,
|
||||
"available@debug": true,
|
||||
"missing@debug": true
|
||||
},
|
||||
"extraKnownMarketplaces": {
|
||||
"debug": {
|
||||
"source": "owner/debug-marketplace"
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("write repo settings");
|
||||
fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
r#"
|
||||
[marketplaces.debug]
|
||||
source_type = "git"
|
||||
source = "owner/debug-marketplace"
|
||||
"#,
|
||||
)
|
||||
.expect("write codex config");
|
||||
fs::write(
|
||||
marketplace_root
|
||||
.join(".agents")
|
||||
.join("plugins")
|
||||
.join("marketplace.json"),
|
||||
r#"{
|
||||
"name": "debug",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "sample",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/sample"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "NOT_AVAILABLE"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "available",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/available"
|
||||
}
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)
|
||||
.expect("write marketplace manifest");
|
||||
fs::write(
|
||||
marketplace_root
|
||||
.join("plugins")
|
||||
.join("sample")
|
||||
.join(".codex-plugin")
|
||||
.join("plugin.json"),
|
||||
r#"{"name":"sample"}"#,
|
||||
)
|
||||
.expect("write sample plugin manifest");
|
||||
fs::write(
|
||||
marketplace_root
|
||||
.join("plugins")
|
||||
.join("available")
|
||||
.join(".codex-plugin")
|
||||
.join("plugin.json"),
|
||||
r#"{"name":"available"}"#,
|
||||
)
|
||||
.expect("write available plugin manifest");
|
||||
|
||||
let items = service_for_paths(external_agent_home, codex_home)
|
||||
.detect(ExternalAgentConfigDetectOptions {
|
||||
include_home: false,
|
||||
cwds: Some(vec![repo_root.clone()]),
|
||||
})
|
||||
.expect("detect");
|
||||
|
||||
assert_eq!(
|
||||
items,
|
||||
vec![ExternalAgentConfigMigrationItem {
|
||||
item_type: ExternalAgentConfigMigrationItemType::Plugins,
|
||||
description: format!(
|
||||
"Import enabled plugins from {}",
|
||||
repo_root.join(".claude").join("settings.json").display()
|
||||
),
|
||||
cwd: Some(repo_root),
|
||||
details: Some(MigrationDetails {
|
||||
plugins: vec![PluginsMigration {
|
||||
marketplace_name: "debug".to_string(),
|
||||
plugin_names: vec!["available".to_string()],
|
||||
}],
|
||||
}),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn import_plugins_requires_source_marketplace_details() {
|
||||
let (_root, claude_home, codex_home) = fixture_paths();
|
||||
fs::create_dir_all(&claude_home).expect("create claude home");
|
||||
let (_root, external_agent_home, codex_home) = fixture_paths();
|
||||
fs::create_dir_all(&external_agent_home).expect("create external agent home");
|
||||
fs::write(
|
||||
claude_home.join("settings.json"),
|
||||
external_agent_home.join("settings.json"),
|
||||
r#"{
|
||||
"enabledPlugins": {
|
||||
"formatter@acme-tools": true
|
||||
@@ -482,7 +876,7 @@ async fn import_plugins_requires_source_marketplace_details() {
|
||||
)
|
||||
.expect("write settings");
|
||||
|
||||
let outcome = service_for_paths(claude_home, codex_home)
|
||||
let outcome = service_for_paths(external_agent_home, codex_home)
|
||||
.import_plugins(
|
||||
/*cwd*/ None,
|
||||
Some(MigrationDetails {
|
||||
@@ -508,10 +902,10 @@ async fn import_plugins_requires_source_marketplace_details() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn import_plugins_defers_marketplace_source_validation_to_add_marketplace() {
|
||||
let (_root, claude_home, codex_home) = fixture_paths();
|
||||
fs::create_dir_all(&claude_home).expect("create claude home");
|
||||
let (_root, external_agent_home, codex_home) = fixture_paths();
|
||||
fs::create_dir_all(&external_agent_home).expect("create external agent home");
|
||||
fs::write(
|
||||
claude_home.join("settings.json"),
|
||||
external_agent_home.join("settings.json"),
|
||||
r#"{
|
||||
"enabledPlugins": {
|
||||
"formatter@acme-tools": true
|
||||
@@ -526,7 +920,7 @@ async fn import_plugins_defers_marketplace_source_validation_to_add_marketplace(
|
||||
)
|
||||
.expect("write settings");
|
||||
|
||||
let outcome = service_for_paths(claude_home, codex_home)
|
||||
let outcome = service_for_paths(external_agent_home, codex_home)
|
||||
.import_plugins(/*cwd*/ None, Some(github_plugin_details()))
|
||||
.await
|
||||
.expect("import plugins");
|
||||
@@ -542,18 +936,96 @@ async fn import_plugins_defers_marketplace_source_validation_to_add_marketplace(
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn import_plugins_supports_external_agent_plugin_marketplace_layout() {
|
||||
let (_root, external_agent_home, codex_home) = fixture_paths();
|
||||
let marketplace_root = external_agent_home.join("my-marketplace");
|
||||
let plugin_root = marketplace_root.join("plugins").join("cloudflare");
|
||||
fs::create_dir_all(marketplace_root.join(".claude-plugin"))
|
||||
.expect("create marketplace manifest dir");
|
||||
fs::create_dir_all(plugin_root.join(".codex-plugin")).expect("create plugin manifest dir");
|
||||
fs::create_dir_all(&codex_home).expect("create codex home");
|
||||
|
||||
fs::write(
|
||||
external_agent_home.join("settings.json"),
|
||||
format!(
|
||||
r#"{{
|
||||
"enabledPlugins": {{
|
||||
"cloudflare@my-plugins": true
|
||||
}},
|
||||
"extraKnownMarketplaces": {{
|
||||
"my-plugins": {{
|
||||
"source": "local",
|
||||
"path": "{}"
|
||||
}}
|
||||
}}
|
||||
}}"#,
|
||||
marketplace_root.display()
|
||||
),
|
||||
)
|
||||
.expect("write settings");
|
||||
fs::write(
|
||||
marketplace_root
|
||||
.join(".claude-plugin")
|
||||
.join("marketplace.json"),
|
||||
r#"{
|
||||
"name": "my-plugins",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "cloudflare",
|
||||
"source": "./plugins/cloudflare"
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)
|
||||
.expect("write marketplace manifest");
|
||||
fs::write(
|
||||
plugin_root.join(".codex-plugin").join("plugin.json"),
|
||||
r#"{"name":"cloudflare","version":"0.1.0"}"#,
|
||||
)
|
||||
.expect("write plugin manifest");
|
||||
|
||||
let outcome = service_for_paths(external_agent_home, codex_home.clone())
|
||||
.import_plugins(
|
||||
/*cwd*/ None,
|
||||
Some(MigrationDetails {
|
||||
plugins: vec![PluginsMigration {
|
||||
marketplace_name: "my-plugins".to_string(),
|
||||
plugin_names: vec!["cloudflare".to_string()],
|
||||
}],
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.expect("import plugins");
|
||||
|
||||
assert_eq!(
|
||||
outcome,
|
||||
PluginImportOutcome {
|
||||
succeeded_marketplaces: vec!["my-plugins".to_string()],
|
||||
succeeded_plugin_ids: vec!["cloudflare@my-plugins".to_string()],
|
||||
failed_marketplaces: Vec::new(),
|
||||
failed_plugin_ids: Vec::new(),
|
||||
}
|
||||
);
|
||||
let config = fs::read_to_string(codex_home.join("config.toml")).expect("read config");
|
||||
assert!(config.contains(r#"[plugins."cloudflare@my-plugins"]"#));
|
||||
assert!(config.contains("enabled = true"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_skills_returns_only_new_skill_directory_count() {
|
||||
let (_root, claude_home, codex_home) = fixture_paths();
|
||||
let (_root, external_agent_home, codex_home) = fixture_paths();
|
||||
let agents_skills = codex_home
|
||||
.parent()
|
||||
.map(|parent| parent.join(".agents").join("skills"))
|
||||
.unwrap_or_else(|| PathBuf::from(".agents").join("skills"));
|
||||
fs::create_dir_all(claude_home.join("skills").join("skill-a")).expect("create source a");
|
||||
fs::create_dir_all(claude_home.join("skills").join("skill-b")).expect("create source b");
|
||||
fs::create_dir_all(external_agent_home.join("skills").join("skill-a"))
|
||||
.expect("create source a");
|
||||
fs::create_dir_all(external_agent_home.join("skills").join("skill-b"))
|
||||
.expect("create source b");
|
||||
fs::create_dir_all(agents_skills.join("skill-a")).expect("create existing target");
|
||||
|
||||
let copied_count = service_for_paths(claude_home, codex_home)
|
||||
let copied_count = service_for_paths(external_agent_home, codex_home)
|
||||
.import_skills(/*cwd*/ None)
|
||||
.expect("import skills");
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ use metadata::find_marketplace_root_by_name;
|
||||
use metadata::installed_marketplace_root_for_source;
|
||||
use metadata::record_added_marketplace_entry;
|
||||
use source::MarketplaceSource;
|
||||
use source::parse_marketplace_source;
|
||||
pub(crate) use source::parse_marketplace_source;
|
||||
use source::stage_marketplace_source;
|
||||
use source::validate_marketplace_source_root;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum MarketplaceSource {
|
||||
pub(crate) enum MarketplaceSource {
|
||||
Git {
|
||||
url: String,
|
||||
ref_name: Option<String>,
|
||||
@@ -15,7 +15,7 @@ pub(super) enum MarketplaceSource {
|
||||
},
|
||||
}
|
||||
|
||||
pub(super) fn parse_marketplace_source(
|
||||
pub(crate) fn parse_marketplace_source(
|
||||
source: &str,
|
||||
explicit_ref: Option<String>,
|
||||
) -> Result<MarketplaceSource, MarketplaceAddError> {
|
||||
|
||||
@@ -31,6 +31,7 @@ pub(crate) use discoverable::list_tool_suggest_discoverable_plugins;
|
||||
pub(crate) use injection::build_plugin_injections;
|
||||
pub use installed_marketplaces::INSTALLED_MARKETPLACES_DIR;
|
||||
pub use installed_marketplaces::marketplace_install_root;
|
||||
pub(crate) use installed_marketplaces::resolve_configured_marketplace_root;
|
||||
pub use manager::ConfiguredMarketplace;
|
||||
pub use manager::ConfiguredMarketplaceListOutcome;
|
||||
pub use manager::ConfiguredMarketplacePlugin;
|
||||
@@ -58,11 +59,14 @@ pub use marketplace::MarketplacePluginAuthPolicy;
|
||||
pub use marketplace::MarketplacePluginInstallPolicy;
|
||||
pub use marketplace::MarketplacePluginPolicy;
|
||||
pub use marketplace::MarketplacePluginSource;
|
||||
pub(crate) use marketplace::find_marketplace_manifest_path;
|
||||
pub(crate) use marketplace::load_marketplace;
|
||||
pub use marketplace::validate_marketplace_root;
|
||||
pub use marketplace_add::MarketplaceAddError;
|
||||
pub use marketplace_add::MarketplaceAddOutcome;
|
||||
pub use marketplace_add::MarketplaceAddRequest;
|
||||
pub use marketplace_add::add_marketplace;
|
||||
pub(crate) use marketplace_add::parse_marketplace_source;
|
||||
pub use remote::RemotePluginFetchError;
|
||||
pub use remote::fetch_remote_featured_plugin_ids;
|
||||
pub(crate) use render::render_explicit_plugin_instructions;
|
||||
|
||||
Reference in New Issue
Block a user