Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions src/integration/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()?;
Expand Down Expand Up @@ -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 => {
Expand Down
91 changes: 77 additions & 14 deletions src/integration/targets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand Down Expand Up @@ -486,7 +486,43 @@ pub(crate) fn install_hermes() -> io::Result<HermesInstallPaths> {
)));
}

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<Vec<PathBuf>> {
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<PathBuf> {
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),
Expand All @@ -496,7 +532,10 @@ pub(crate) fn install_hermes() -> io::Result<HermesInstallPaths> {
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<PathBuf> {
let config_path = dir.join("config.yaml");
let existing_config = if config_path.is_file() {
fs::read_to_string(&config_path)?
Expand All @@ -507,11 +546,7 @@ pub(crate) fn install_hermes() -> io::Result<HermesInstallPaths> {
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<PiUninstallResult> {
Expand Down Expand Up @@ -836,11 +871,39 @@ pub(crate) fn uninstall_hermes() -> io::Result<HermesUninstallResult> {
}
}

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,
})
}

Expand Down
113 changes: 113 additions & 0 deletions src/integration/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions src/integration/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf>,
pub profile_config_paths: Vec<PathBuf>,
}

#[derive(Debug)]
Expand Down Expand Up @@ -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<PathBuf>,
pub profile_config_paths: Vec<PathBuf>,
pub removed_profile_plugin_dirs: usize,
pub updated_profile_configs: usize,
}

#[derive(Debug)]
Expand Down
Loading