//! Pre-installs bundled built-in plugins into the user's installed directory. use std::path::Path; use super::definition::write_if_changed; use crate::Result; /// 随二进制打包的内置插件文件;发布构建没有源码目录,靠这里预装。 const CODEX_AUTH: &[(&str, &str)] = &[ ( "plugin.json", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/codex-auth/plugin.json" )), ), ( "main.ts", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/codex-auth/main.ts" )), ), ( "provider.ts", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/codex-auth/provider.ts" )), ), ( "models.ts", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/codex-auth/models.ts" )), ), ( "oauth.ts", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/codex-auth/oauth.ts" )), ), ( "resources.ts", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/codex-auth/resources.ts" )), ), ( "assets/codex.svg", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/codex-auth/assets/codex.svg" )), ), ]; const GROK_AUTH: &[(&str, &str)] = &[ ( "plugin.json", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/grok-auth/plugin.json" )), ), ( "main.ts", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/grok-auth/main.ts" )), ), ( "provider.ts", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/grok-auth/provider.ts" )), ), ( "models.ts", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/grok-auth/models.ts" )), ), ( "oauth.ts", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/grok-auth/oauth.ts" )), ), ( "resources.ts", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/grok-auth/resources.ts" )), ), ( "assets/grok.svg", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/grok-auth/assets/grok.svg" )), ), ]; const ANTIGRAVITY_AUTH: &[(&str, &str)] = &[ ( "plugin.json", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/antigravity-auth/plugin.json" )), ), ( "main.ts", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/antigravity-auth/main.ts" )), ), ( "provider.ts", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/antigravity-auth/provider.ts" )), ), ( "models.ts", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/antigravity-auth/models.ts" )), ), ( "oauth.ts", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/antigravity-auth/oauth.ts" )), ), ( "google_oauth.ts", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/antigravity-auth/google_oauth.ts" )), ), ( "resources.ts", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/antigravity-auth/resources.ts" )), ), ( "assets/antigravity.svg", include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/plugins/build-in/antigravity-auth/assets/antigravity.svg" )), ), ]; const PLUGINS: &[(&str, &[(&str, &str)])] = &[ ("codex-auth", CODEX_AUTH), ("grok-auth", GROK_AUTH), ("antigravity-auth", ANTIGRAVITY_AUTH), ]; /// 把内置插件预装到 installed 目录。manifest 的 version 是缓存键: /// 版本一致时零写盘;版本变化时整目录同步并清理旧版本残留文件。 pub(super) fn install(installed: &Path) -> Result<()> { for (name, files) in PLUGINS { let directory = installed.join(name); if disk_version(&directory) == Some(embedded_version(files)?) { continue; } write_plugin(&directory, files)?; } Ok(()) } fn embedded_version(files: &[(&str, &str)]) -> Result { let manifest = files .iter() .find(|(name, _)| *name == "plugin.json") .map(|(_, content)| *content) .expect("built-in plugin bundles plugin.json"); let value: serde_json::Value = serde_json::from_str(manifest)?; value .get("version") .and_then(serde_json::Value::as_str) .map(str::to_owned) .ok_or_else(|| crate::Error::Config("built-in plugin manifest requires version".into())) } fn disk_version(directory: &Path) -> Option { let manifest = std::fs::read_to_string(directory.join("plugin.json")).ok()?; let value: serde_json::Value = serde_json::from_str(&manifest).ok()?; Some(value.get("version")?.as_str()?.to_owned()) } fn write_plugin(directory: &Path, files: &[(&str, &str)]) -> Result<()> { for (relative, content) in files { let path = directory.join(relative); let parent = path.parent().expect("plugin file path has a parent"); std::fs::create_dir_all(parent)?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))?; } write_if_changed(&path, content)?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?; } } prune_unknown_files(directory, directory, files)?; Ok(()) } /// 删除插件目录中不在嵌入清单里的文件与空目录(旧版本残留)。 fn prune_unknown_files(root: &Path, directory: &Path, files: &[(&str, &str)]) -> Result<()> { for entry in std::fs::read_dir(directory)? { let entry = entry?; let path = entry.path(); if entry.file_type()?.is_dir() { prune_unknown_files(root, &path, files)?; if std::fs::read_dir(&path)?.next().is_none() { std::fs::remove_dir(&path)?; } continue; } let known = files .iter() .any(|(relative, _)| root.join(relative) == path); if !known { std::fs::remove_file(&path)?; } } Ok(()) } #[cfg(test)] mod tests { use super::*; fn embedded_main() -> &'static str { CODEX_AUTH .iter() .find(|(name, _)| *name == "main.ts") .unwrap() .1 } #[test] fn install_is_version_gated_and_syncs_on_version_change() { let root = tempfile::tempdir().unwrap(); let plugin = root.path().join("codex-auth"); install(root.path()).unwrap(); assert_eq!( std::fs::read_to_string(plugin.join("main.ts")).unwrap(), embedded_main() ); assert!(root .path() .join("antigravity-auth/assets/antigravity.svg") .is_file()); // 版本一致:本地改动与额外文件保持原样,不发生任何写盘。 std::fs::write(plugin.join("main.ts"), "edited").unwrap(); std::fs::write(plugin.join("stale.ts"), "extra").unwrap(); install(root.path()).unwrap(); assert_eq!( std::fs::read_to_string(plugin.join("main.ts")).unwrap(), "edited" ); assert!(plugin.join("stale.ts").exists()); // 版本变化:整目录同步回嵌入内容并清理残留。 let manifest = std::fs::read_to_string(plugin.join("plugin.json")).unwrap(); let mut value: serde_json::Value = serde_json::from_str(&manifest).unwrap(); value["version"] = serde_json::Value::String("0.0.1".into()); std::fs::write(plugin.join("plugin.json"), value.to_string()).unwrap(); install(root.path()).unwrap(); assert_eq!( std::fs::read_to_string(plugin.join("main.ts")).unwrap(), embedded_main() ); assert!(!plugin.join("stale.ts").exists()); } }