From 0c0829765752e69ee8de2e6793652f1e4d87cb07 Mon Sep 17 00:00:00 2001 From: xanawer Date: Wed, 5 Aug 2026 05:42:38 +0000 Subject: [PATCH] fix: install Hermes integration for profiles --- src/integration/actions.rs | 27 ++++++++- src/integration/targets.rs | 91 ++++++++++++++++++++++++----- src/integration/tests.rs | 113 +++++++++++++++++++++++++++++++++++++ src/integration/types.rs | 6 ++ 4 files changed, 221 insertions(+), 16 deletions(-) diff --git a/src/integration/actions.rs b/src/integration/actions.rs index 3cb737744f..35a87f1e1a 100644 --- a/src/integration/actions.rs +++ b/src/integration/actions.rs @@ -158,7 +158,7 @@ fn install_target_inner(target: crate::api::schema::IntegrationTarget) -> io::Re } crate::api::schema::IntegrationTarget::Hermes => { let installed = install_hermes()?; - vec![ + let mut messages = vec![ format!( "installed hermes integration plugin to {}", installed.plugin_dir.display() @@ -167,7 +167,18 @@ fn install_target_inner(target: crate::api::schema::IntegrationTarget) -> io::Re "enabled hermes plugin in {}", installed.config_path.display() ), - ] + ]; + if !installed.profile_plugin_dirs.is_empty() { + messages.push(format!( + "installed hermes integration plugin to {} profile(s)", + installed.profile_plugin_dirs.len() + )); + messages.push(format!( + "enabled hermes plugin in {} profile config(s)", + installed.profile_config_paths.len() + )); + } + messages } crate::api::schema::IntegrationTarget::Qodercli => { let installed = install_qodercli()?; @@ -502,6 +513,18 @@ pub(crate) fn uninstall_target( result.config_path.display() )); } + if !result.profile_plugin_dirs.is_empty() { + messages.push(format!( + "removed hermes integration plugin from {} of {} profile(s)", + result.removed_profile_plugin_dirs, + result.profile_plugin_dirs.len() + )); + messages.push(format!( + "disabled hermes plugin in {} of {} profile config(s)", + result.updated_profile_configs, + result.profile_config_paths.len() + )); + } messages } crate::api::schema::IntegrationTarget::Qodercli => { diff --git a/src/integration/targets.rs b/src/integration/targets.rs index b387b960a9..50c3fc608e 100644 --- a/src/integration/targets.rs +++ b/src/integration/targets.rs @@ -42,14 +42,14 @@ use super::{ DEVIN_HOOK_INSTALL_NAME, DEVIN_REMOVED_LIFECYCLE_HOOK_EVENTS, DROID_HOOK_ASSET, DROID_HOOK_EVENTS, DROID_HOOK_INSTALL_NAME, DROID_REMOVED_LIFECYCLE_HOOK_EVENTS, GROK_HOOK_ASSET, GROK_HOOK_CONFIG_INSTALL_NAME, GROK_HOOK_INSTALL_NAME, - HERMES_PLUGIN_INIT_ASSET, HERMES_PLUGIN_INIT_INSTALL_NAME, HERMES_PLUGIN_MANIFEST_ASSET, - HERMES_PLUGIN_MANIFEST_INSTALL_NAME, KILO_PLUGIN_ASSET, KILO_PLUGIN_INSTALL_NAME, - KIMI_HOOK_ASSET, KIMI_HOOK_INSTALL_NAME, MASTRACODE_HOOK_ASSET, MASTRACODE_HOOK_EVENTS, - MASTRACODE_HOOK_INSTALL_NAME, MASTRACODE_HOOK_TIMEOUT_MS, MASTRACODE_REMOVED_HOOK_EVENTS, - OMP_EXTENSION_ASSET, OMP_EXTENSION_INSTALL_NAME, OPENCODE_PLUGIN_ASSET, - OPENCODE_PLUGIN_INSTALL_NAME, PI_EXTENSION_ASSET, PI_EXTENSION_INSTALL_NAME, - QODERCLI_HOOK_ASSET, QODERCLI_HOOK_EVENTS, QODERCLI_HOOK_INSTALL_NAME, - QODERCLI_REMOVED_LIFECYCLE_HOOK_EVENTS, + HERMES_PLUGIN_INIT_ASSET, HERMES_PLUGIN_INIT_INSTALL_NAME, HERMES_PLUGIN_INSTALL_NAME, + HERMES_PLUGIN_MANIFEST_ASSET, HERMES_PLUGIN_MANIFEST_INSTALL_NAME, KILO_PLUGIN_ASSET, + KILO_PLUGIN_INSTALL_NAME, KIMI_HOOK_ASSET, KIMI_HOOK_INSTALL_NAME, MASTRACODE_HOOK_ASSET, + MASTRACODE_HOOK_EVENTS, MASTRACODE_HOOK_INSTALL_NAME, MASTRACODE_HOOK_TIMEOUT_MS, + MASTRACODE_REMOVED_HOOK_EVENTS, OMP_EXTENSION_ASSET, OMP_EXTENSION_INSTALL_NAME, + OPENCODE_PLUGIN_ASSET, OPENCODE_PLUGIN_INSTALL_NAME, PI_EXTENSION_ASSET, + PI_EXTENSION_INSTALL_NAME, QODERCLI_HOOK_ASSET, QODERCLI_HOOK_EVENTS, + QODERCLI_HOOK_INSTALL_NAME, QODERCLI_REMOVED_LIFECYCLE_HOOK_EVENTS, }; fn ensure_extension_dir(dir: &Path, agent: &str) -> io::Result<()> { @@ -486,7 +486,43 @@ pub(crate) fn install_hermes() -> io::Result { ))); } - let plugin_dir = hermes_plugin_dir()?; + let plugin_dir = install_hermes_plugin_for_home(&dir)?; + let config_path = enable_hermes_plugin_for_home(&dir)?; + + let mut profile_plugin_dirs = Vec::new(); + let mut profile_config_paths = Vec::new(); + for profile_dir in hermes_profile_dirs(&dir)? { + profile_plugin_dirs.push(install_hermes_plugin_for_home(&profile_dir)?); + profile_config_paths.push(enable_hermes_plugin_for_home(&profile_dir)?); + } + + Ok(HermesInstallPaths { + plugin_dir, + config_path, + profile_plugin_dirs, + profile_config_paths, + }) +} + +fn hermes_profile_dirs(dir: &Path) -> io::Result> { + let profiles_dir = dir.join("profiles"); + if !profiles_dir.is_dir() { + return Ok(Vec::new()); + } + + let mut profiles = Vec::new(); + for entry in fs::read_dir(profiles_dir)? { + let entry = entry?; + if entry.file_type()?.is_dir() { + profiles.push(entry.path()); + } + } + profiles.sort(); + Ok(profiles) +} + +fn install_hermes_plugin_for_home(dir: &Path) -> io::Result { + let plugin_dir = dir.join("plugins").join(HERMES_PLUGIN_INSTALL_NAME); fs::create_dir_all(&plugin_dir)?; fs::write( plugin_dir.join(HERMES_PLUGIN_MANIFEST_INSTALL_NAME), @@ -496,7 +532,10 @@ pub(crate) fn install_hermes() -> io::Result { plugin_dir.join(HERMES_PLUGIN_INIT_INSTALL_NAME), HERMES_PLUGIN_INIT_ASSET, )?; + Ok(plugin_dir) +} +fn enable_hermes_plugin_for_home(dir: &Path) -> io::Result { let config_path = dir.join("config.yaml"); let existing_config = if config_path.is_file() { fs::read_to_string(&config_path)? @@ -507,11 +546,7 @@ pub(crate) fn install_hermes() -> io::Result { if new_config != existing_config { fs::write(&config_path, new_config)?; } - - Ok(HermesInstallPaths { - plugin_dir, - config_path, - }) + Ok(config_path) } pub(crate) fn uninstall_pi() -> io::Result { @@ -836,11 +871,39 @@ pub(crate) fn uninstall_hermes() -> io::Result { } } + let mut profile_plugin_dirs = Vec::new(); + let mut profile_config_paths = Vec::new(); + let mut removed_profile_plugin_dirs = 0; + let mut updated_profile_configs = 0; + for profile_dir in hermes_profile_dirs(&dir)? { + let profile_plugin_dir = profile_dir.join("plugins").join(HERMES_PLUGIN_INSTALL_NAME); + let profile_config_path = profile_dir.join("config.yaml"); + + if remove_dir_all_if_exists(&profile_plugin_dir)? { + removed_profile_plugin_dirs += 1; + } + if profile_config_path.is_file() { + let existing_config = fs::read_to_string(&profile_config_path)?; + let new_config = remove_hermes_plugin_enabled(&existing_config); + if new_config != existing_config { + fs::write(&profile_config_path, new_config)?; + updated_profile_configs += 1; + } + } + + profile_plugin_dirs.push(profile_plugin_dir); + profile_config_paths.push(profile_config_path); + } + Ok(HermesUninstallResult { plugin_dir, config_path, removed_plugin_dir, updated_config, + profile_plugin_dirs, + profile_config_paths, + removed_profile_plugin_dirs, + updated_profile_configs, }) } diff --git a/src/integration/tests.rs b/src/integration/tests.rs index 6827dae710..a05d35f7d2 100644 --- a/src/integration/tests.rs +++ b/src/integration/tests.rs @@ -2375,6 +2375,64 @@ fn install_hermes_writes_plugin_and_enables_it() { let _ = fs::remove_dir_all(base); } +#[test] +fn install_hermes_writes_plugin_and_enables_it_for_profiles() { + let _lock = integration_env_lock(); + let base = unique_base(); + let home = base.join("home"); + let hermes_dir = home.join(".hermes"); + let profiles_dir = hermes_dir.join("profiles"); + let default_profile = profiles_dir.join("default"); + let work_profile = profiles_dir.join("work"); + fs::create_dir_all(&default_profile).unwrap(); + fs::create_dir_all(&work_profile).unwrap(); + fs::write(hermes_dir.join("config.yaml"), "model:\n provider: auto\n").unwrap(); + fs::write( + default_profile.join("config.yaml"), + "plugins:\n - existing\n", + ) + .unwrap(); + std::env::set_var("HOME", &home); + + let installed = install_hermes().unwrap(); + let default_plugin_dir = default_profile + .join("plugins") + .join(HERMES_PLUGIN_INSTALL_NAME); + let work_plugin_dir = work_profile + .join("plugins") + .join(HERMES_PLUGIN_INSTALL_NAME); + let default_config = fs::read_to_string(default_profile.join("config.yaml")).unwrap(); + let work_config = fs::read_to_string(work_profile.join("config.yaml")).unwrap(); + + assert_eq!( + installed.profile_plugin_dirs, + vec![default_plugin_dir.clone(), work_plugin_dir.clone()] + ); + assert_eq!( + installed.profile_config_paths, + vec![ + default_profile.join("config.yaml"), + work_profile.join("config.yaml") + ] + ); + assert_eq!( + fs::read_to_string(default_plugin_dir.join(HERMES_PLUGIN_INIT_INSTALL_NAME)).unwrap(), + HERMES_PLUGIN_INIT_ASSET + ); + assert_eq!( + fs::read_to_string(work_plugin_dir.join(HERMES_PLUGIN_MANIFEST_INSTALL_NAME)).unwrap(), + HERMES_PLUGIN_MANIFEST_ASSET + ); + assert_eq!( + default_config, + "plugins:\n - herdr-agent-state\n - existing\n" + ); + assert!(work_config.contains("plugins:\n enabled:\n - herdr-agent-state")); + + std::env::remove_var("HOME"); + let _ = fs::remove_dir_all(base); +} + #[test] fn install_hermes_is_idempotent_for_enabled_entry() { let _lock = integration_env_lock(); @@ -2510,6 +2568,61 @@ fn uninstall_hermes_removes_plugin_and_enabled_entry() { let _ = fs::remove_dir_all(base); } +#[test] +fn uninstall_hermes_removes_plugin_and_enabled_entry_for_profiles() { + let _lock = integration_env_lock(); + let base = unique_base(); + let home = base.join("home"); + let hermes_dir = home.join(".hermes"); + let profiles_dir = hermes_dir.join("profiles"); + let default_profile = profiles_dir.join("default"); + let work_profile = profiles_dir.join("work"); + let default_plugin_dir = default_profile + .join("plugins") + .join(HERMES_PLUGIN_INSTALL_NAME); + let work_plugin_dir = work_profile + .join("plugins") + .join(HERMES_PLUGIN_INSTALL_NAME); + fs::create_dir_all(&default_plugin_dir).unwrap(); + fs::create_dir_all(&work_plugin_dir).unwrap(); + fs::create_dir_all(hermes_dir.join("plugins").join(HERMES_PLUGIN_INSTALL_NAME)).unwrap(); + fs::write( + hermes_dir.join("config.yaml"), + "plugins:\n enabled:\n - herdr-agent-state\n", + ) + .unwrap(); + fs::write( + default_profile.join("config.yaml"), + "plugins:\n - other-plugin\n - herdr-agent-state\n", + ) + .unwrap(); + fs::write( + work_profile.join("config.yaml"), + "plugins:\n enabled:\n - herdr-agent-state\n - existing\n", + ) + .unwrap(); + std::env::set_var("HOME", &home); + + let result = uninstall_hermes().unwrap(); + let default_config = fs::read_to_string(default_profile.join("config.yaml")).unwrap(); + let work_config = fs::read_to_string(work_profile.join("config.yaml")).unwrap(); + + assert_eq!( + result.profile_plugin_dirs, + vec![default_plugin_dir.clone(), work_plugin_dir.clone()] + ); + assert_eq!(result.removed_profile_plugin_dirs, 2); + assert_eq!(result.updated_profile_configs, 2); + assert!(!default_plugin_dir.exists()); + assert!(!work_plugin_dir.exists()); + assert_eq!(default_config, "plugins:\n - other-plugin\n"); + assert!(work_config.contains(" - existing")); + assert!(!work_config.contains("herdr-agent-state")); + + std::env::remove_var("HOME"); + let _ = fs::remove_dir_all(base); +} + #[test] fn uninstall_hermes_preserves_flat_plugin_list() { let _lock = integration_env_lock(); diff --git a/src/integration/types.rs b/src/integration/types.rs index 09a235b047..a08399ad94 100644 --- a/src/integration/types.rs +++ b/src/integration/types.rs @@ -59,6 +59,8 @@ pub(crate) struct OmpInstallPaths { pub(crate) struct HermesInstallPaths { pub plugin_dir: PathBuf, pub config_path: PathBuf, + pub profile_plugin_dirs: Vec, + pub profile_config_paths: Vec, } #[derive(Debug)] @@ -240,6 +242,10 @@ pub(crate) struct HermesUninstallResult { pub config_path: PathBuf, pub removed_plugin_dir: bool, pub updated_config: bool, + pub profile_plugin_dirs: Vec, + pub profile_config_paths: Vec, + pub removed_profile_plugin_dirs: usize, + pub updated_profile_configs: usize, } #[derive(Debug)]