mirror of
https://github.com/openai/codex.git
synced 2026-05-16 09:12:54 +00:00
Load plugin manifests through a shared discoverable-path helper so manifest reads, installs, and skill names all see the same alternate manifest location.
122 lines
4.2 KiB
Rust
122 lines
4.2 KiB
Rust
//! Resolve plugin namespace from skill file paths by walking ancestors for `plugin.json`.
|
|
|
|
use codex_exec_server::ExecutorFileSystem;
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
const DISCOVERABLE_PLUGIN_MANIFEST_PATHS: &[&str] =
|
|
&[".codex-plugin/plugin.json", ".claude-plugin/plugin.json"];
|
|
|
|
pub fn find_plugin_manifest_path(plugin_root: &Path) -> Option<PathBuf> {
|
|
DISCOVERABLE_PLUGIN_MANIFEST_PATHS
|
|
.iter()
|
|
.map(|relative_path| plugin_root.join(relative_path))
|
|
.find(|manifest_path| manifest_path.is_file())
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct RawPluginManifestName {
|
|
#[serde(default)]
|
|
name: String,
|
|
}
|
|
|
|
async fn plugin_manifest_name(
|
|
fs: &dyn ExecutorFileSystem,
|
|
plugin_root: &AbsolutePathBuf,
|
|
) -> Option<String> {
|
|
let mut manifest_path = None;
|
|
for relative_path in DISCOVERABLE_PLUGIN_MANIFEST_PATHS {
|
|
let candidate = plugin_root.join(relative_path);
|
|
match fs.get_metadata(&candidate, /*sandbox*/ None).await {
|
|
Ok(metadata) if metadata.is_file => {
|
|
manifest_path = Some(candidate);
|
|
break;
|
|
}
|
|
Ok(_) | Err(_) => {}
|
|
}
|
|
}
|
|
let manifest_path = manifest_path?;
|
|
let contents = fs
|
|
.read_file_text(&manifest_path, /*sandbox*/ None)
|
|
.await
|
|
.ok()?;
|
|
let RawPluginManifestName { name: raw_name } = serde_json::from_str(&contents).ok()?;
|
|
Some(
|
|
plugin_root
|
|
.file_name()
|
|
.and_then(|entry| entry.to_str())
|
|
.filter(|_| raw_name.trim().is_empty())
|
|
.unwrap_or(raw_name.as_str())
|
|
.to_string(),
|
|
)
|
|
}
|
|
|
|
/// Returns the plugin manifest `name` for the nearest ancestor of `path` that contains a valid
|
|
/// plugin manifest (same `name` rules as full manifest loading in codex-core).
|
|
pub async fn plugin_namespace_for_skill_path(
|
|
fs: &dyn ExecutorFileSystem,
|
|
path: &AbsolutePathBuf,
|
|
) -> Option<String> {
|
|
for ancestor in path.ancestors() {
|
|
if let Some(name) = plugin_manifest_name(fs, &ancestor).await {
|
|
return Some(name);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::find_plugin_manifest_path;
|
|
use super::plugin_namespace_for_skill_path;
|
|
use codex_exec_server::LOCAL_FS;
|
|
use codex_utils_absolute_path::test_support::PathBufExt;
|
|
use std::fs;
|
|
use tempfile::tempdir;
|
|
|
|
const ALTERNATE_PLUGIN_MANIFEST_RELATIVE_PATH: &str = ".claude-plugin/plugin.json";
|
|
|
|
#[tokio::test]
|
|
async fn uses_manifest_name() {
|
|
let tmp = tempdir().expect("tempdir");
|
|
let plugin_root = tmp.path().join("plugins/sample");
|
|
let skill_path = plugin_root.join("skills/search/SKILL.md");
|
|
|
|
fs::create_dir_all(skill_path.parent().expect("parent")).expect("mkdir");
|
|
fs::create_dir_all(plugin_root.join(".codex-plugin")).expect("mkdir manifest");
|
|
fs::write(
|
|
plugin_root.join(".codex-plugin/plugin.json"),
|
|
r#"{"name":"sample"}"#,
|
|
)
|
|
.expect("write manifest");
|
|
fs::write(&skill_path, "---\ndescription: search\n---\n").expect("write skill");
|
|
|
|
assert_eq!(
|
|
plugin_namespace_for_skill_path(LOCAL_FS.as_ref(), &skill_path.abs()).await,
|
|
Some("sample".to_string())
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn uses_name_from_alternate_discoverable_manifest_path() {
|
|
let tmp = tempdir().expect("tempdir");
|
|
let plugin_root = tmp.path().join("plugins/sample");
|
|
let skill_path = plugin_root.join("skills/search/SKILL.md");
|
|
let manifest_path = plugin_root.join(ALTERNATE_PLUGIN_MANIFEST_RELATIVE_PATH);
|
|
|
|
fs::create_dir_all(skill_path.parent().expect("parent")).expect("mkdir");
|
|
fs::create_dir_all(manifest_path.parent().expect("manifest parent"))
|
|
.expect("mkdir manifest");
|
|
fs::write(&manifest_path, r#"{"name":"sample"}"#).expect("write manifest");
|
|
fs::write(&skill_path, "---\ndescription: search\n---\n").expect("write skill");
|
|
|
|
assert_eq!(
|
|
plugin_namespace_for_skill_path(LOCAL_FS.as_ref(), &skill_path.abs()).await,
|
|
Some("sample".to_string())
|
|
);
|
|
assert_eq!(find_plugin_manifest_path(&plugin_root), Some(manifest_path));
|
|
}
|
|
}
|