Codex Handbook
utils/plugins/src/plugin_namespace.rs 125 lines
//! 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 codex_utils_path_uri::PathUri;use std::path::Path;use std::path::PathBuf;/// Ordered plugin manifest paths recognized beneath a plugin root.pub 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);        let candidate_uri = PathUri::from_abs_path(&candidate);        match fs.get_metadata(&candidate_uri, /*sandbox*/ None).await {            Ok(metadata) if metadata.is_file => {                manifest_path = Some(candidate);                break;            }            Ok(_) | Err(_) => {}        }    }    let manifest_path = manifest_path?;    let manifest_path_uri = PathUri::from_abs_path(&manifest_path);    let contents = fs        .read_file_text(&manifest_path_uri, /*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));    }}