From 170d5674872fe030777ceb97e18b38a0e9ddbc2f Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Sat, 16 May 2026 17:29:37 +0300 Subject: [PATCH] fix: respect integration config dirs refs #121 --- .pi/docs/CHANGELOG.md | 1 + .pi/docs/INTEGRATIONS.md | 29 +++++---- src/integration/mod.rs | 129 ++++++++++++++++++++++++++++++++++++++- src/workspace.rs | 7 ++- 4 files changed, 149 insertions(+), 17 deletions(-) diff --git a/.pi/docs/CHANGELOG.md b/.pi/docs/CHANGELOG.md index 6655806e..2af756a5 100644 --- a/.pi/docs/CHANGELOG.md +++ b/.pi/docs/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed - GitHub Copilot is now correctly detected when its process name is `copilot`. +- Integration installs now respect `PI_CODING_AGENT_DIR`, `CLAUDE_CONFIG_DIR`, and `CODEX_HOME` when choosing Pi, Claude Code, and Codex config paths. - Split pane resize hit areas no longer overlap the first content column or row, making text selection work from the start of right and bottom panes. ## [0.5.9] - 2026-05-15 diff --git a/.pi/docs/INTEGRATIONS.md b/.pi/docs/INTEGRATIONS.md index ec4e4884..3905e834 100644 --- a/.pi/docs/INTEGRATIONS.md +++ b/.pi/docs/INTEGRATIONS.md @@ -55,6 +55,14 @@ this writes the bundled pi extension to: ~/.pi/agent/extensions/herdr-agent-state.ts ``` +if `PI_CODING_AGENT_DIR` is set, herdr uses that agent directory instead and writes to: + +```text +$PI_CODING_AGENT_DIR/extensions/herdr-agent-state.ts +``` + +`~` is expanded in `PI_CODING_AGENT_DIR`. + pi is the cleanest integration. it already has an authoritative hook model, so herdr can get direct state reports over the socket api without guessing as much from the terminal. bundled source: [`src/integration/assets/pi/herdr-agent-state.ts`](./src/integration/assets/pi/herdr-agent-state.ts) @@ -65,11 +73,7 @@ uninstall: herdr integration uninstall pi ``` -this removes: - -```text -~/.pi/agent/extensions/herdr-agent-state.ts -``` +this removes the same extension path herdr would install, using `PI_CODING_AGENT_DIR` when it is set. ## claude code @@ -84,6 +88,8 @@ this: - writes the hook script to `~/.claude/hooks/herdr-agent-state.sh` - updates `~/.claude/settings.json` +if `CLAUDE_CONFIG_DIR` is set, herdr uses that directory instead, for example `$CLAUDE_CONFIG_DIR/hooks/herdr-agent-state.sh` and `$CLAUDE_CONFIG_DIR/settings.json`. `~` is expanded in `CLAUDE_CONFIG_DIR`. + bundled source: [`src/integration/assets/claude/herdr-agent-state.sh`](./src/integration/assets/claude/herdr-agent-state.sh) current hook mapping: @@ -109,10 +115,7 @@ uninstall: herdr integration uninstall claude ``` -this: - -- removes `~/.claude/hooks/herdr-agent-state.sh` -- removes herdr-owned hook entries from `~/.claude/settings.json` +this removes the same hook path and settings entries herdr would install, using `CLAUDE_CONFIG_DIR` when it is set. ## codex @@ -129,6 +132,8 @@ this: - ensures `hooks = true` under `[features]` in `~/.codex/config.toml` - migrates the deprecated top-level `[features] codex_hooks = true` setting to `hooks = true` +if `CODEX_HOME` is set, herdr uses that directory instead, for example `$CODEX_HOME/herdr-agent-state.sh`, `$CODEX_HOME/hooks.json`, and `$CODEX_HOME/config.toml`. `~` is expanded in `CODEX_HOME`. + bundled source: [`src/integration/assets/codex/herdr-agent-state.sh`](./src/integration/assets/codex/herdr-agent-state.sh) current hook mapping: @@ -153,9 +158,9 @@ herdr integration uninstall codex this: -- removes `~/.codex/herdr-agent-state.sh` -- removes herdr-owned hook entries from `~/.codex/hooks.json` -- intentionally leaves `~/.codex/config.toml` alone +- removes the same hook path herdr would install, using `CODEX_HOME` when it is set +- removes herdr-owned hook entries from the matching `hooks.json` +- intentionally leaves the matching `config.toml` alone that last point is deliberate: herdr does **not** try to guess whether `hooks = true` is still needed for some other codex hook setup. diff --git a/src/integration/mod.rs b/src/integration/mod.rs index fe92f66c..88f673ed 100644 --- a/src/integration/mod.rs +++ b/src/integration/mod.rs @@ -13,12 +13,15 @@ pub(crate) const HERDR_PANE_ID_ENV_VAR: &str = "HERDR_PANE_ID"; const PI_EXTENSION_INSTALL_NAME: &str = "herdr-agent-state.ts"; const PI_EXTENSION_ASSET: &str = include_str!("assets/pi/herdr-agent-state.ts"); const PI_INTEGRATION_VERSION: u32 = 1; +const PI_CODING_AGENT_DIR_ENV_VAR: &str = "PI_CODING_AGENT_DIR"; const CLAUDE_HOOK_INSTALL_NAME: &str = "herdr-agent-state.sh"; const CLAUDE_HOOK_ASSET: &str = include_str!("assets/claude/herdr-agent-state.sh"); const CLAUDE_INTEGRATION_VERSION: u32 = 1; +const CLAUDE_CONFIG_DIR_ENV_VAR: &str = "CLAUDE_CONFIG_DIR"; const CODEX_HOOK_INSTALL_NAME: &str = "herdr-agent-state.sh"; const CODEX_HOOK_ASSET: &str = include_str!("assets/codex/herdr-agent-state.sh"); const CODEX_INTEGRATION_VERSION: u32 = 2; +const CODEX_HOME_ENV_VAR: &str = "CODEX_HOME"; const OPENCODE_PLUGIN_INSTALL_NAME: &str = "herdr-agent-state.js"; const OPENCODE_PLUGIN_ASSET: &str = include_str!("assets/opencode/herdr-agent-state.js"); const OPENCODE_INTEGRATION_VERSION: u32 = 1; @@ -997,15 +1000,53 @@ fn make_executable(path: &Path) -> io::Result<()> { } fn pi_extension_dir() -> io::Result { - Ok(home_dir()?.join(".pi/agent/extensions")) + Ok( + config_dir_from_env_or_home(PI_CODING_AGENT_DIR_ENV_VAR, &[".pi", "agent"])? + .join("extensions"), + ) } fn claude_dir() -> io::Result { - Ok(home_dir()?.join(".claude")) + config_dir_from_env_or_home(CLAUDE_CONFIG_DIR_ENV_VAR, &[".claude"]) } fn codex_dir() -> io::Result { - Ok(home_dir()?.join(".codex")) + config_dir_from_env_or_home(CODEX_HOME_ENV_VAR, &[".codex"]) +} + +fn config_dir_from_env_or_home( + env_var: &str, + home_relative_segments: &[&str], +) -> io::Result { + if let Some(value) = std::env::var_os(env_var).filter(|value| !value.is_empty()) { + return expand_tilde_path(PathBuf::from(value)); + } + + let mut path = home_dir()?; + for segment in home_relative_segments { + path.push(segment); + } + Ok(path) +} + +fn expand_tilde_path(path: PathBuf) -> io::Result { + let Some(raw) = path.to_str() else { + return Ok(path); + }; + + if raw == "~" { + return home_dir(); + } + + if let Some(rest) = raw + .strip_prefix("~/") + .or_else(|| raw.strip_prefix("~\\")) + .or_else(|| raw.strip_prefix('~')) + { + return Ok(home_dir()?.join(rest)); + } + + Ok(path) } fn opencode_dir() -> io::Result { @@ -1028,7 +1069,14 @@ pub(crate) fn integration_env_lock() -> MutexGuard<'static, ()> { mod tests { use super::*; + fn clear_integration_path_env() { + std::env::remove_var(PI_CODING_AGENT_DIR_ENV_VAR); + std::env::remove_var(CLAUDE_CONFIG_DIR_ENV_VAR); + std::env::remove_var(CODEX_HOME_ENV_VAR); + } + fn unique_base() -> PathBuf { + clear_integration_path_env(); std::env::temp_dir().join(format!( "herdr-integration-install-test-{}-{}", std::process::id(), @@ -1058,6 +1106,42 @@ mod tests { let _ = fs::remove_dir_all(base); } + #[test] + fn install_pi_uses_pi_coding_agent_dir_env() { + let _lock = integration_env_lock(); + let base = unique_base(); + let agent_dir = base.join("custom-pi-agent"); + let ext_dir = agent_dir.join("extensions"); + fs::create_dir_all(&ext_dir).unwrap(); + std::env::set_var(PI_CODING_AGENT_DIR_ENV_VAR, &agent_dir); + + let path = install_pi().unwrap(); + + assert_eq!(path, ext_dir.join(PI_EXTENSION_INSTALL_NAME)); + + clear_integration_path_env(); + let _ = fs::remove_dir_all(base); + } + + #[test] + fn install_pi_expands_tilde_in_pi_coding_agent_dir_env() { + let _lock = integration_env_lock(); + let base = unique_base(); + let home = base.join("home"); + let ext_dir = home.join("custom-pi-agent/extensions"); + fs::create_dir_all(&ext_dir).unwrap(); + std::env::set_var("HOME", &home); + std::env::set_var(PI_CODING_AGENT_DIR_ENV_VAR, "~/custom-pi-agent"); + + let path = install_pi().unwrap(); + + assert_eq!(path, ext_dir.join(PI_EXTENSION_INSTALL_NAME)); + + std::env::remove_var("HOME"); + clear_integration_path_env(); + let _ = fs::remove_dir_all(base); + } + #[test] fn uninstall_pi_removes_embedded_extension_when_present() { let _lock = integration_env_lock(); @@ -1208,6 +1292,26 @@ mod tests { let _ = fs::remove_dir_all(base); } + #[test] + fn install_claude_uses_claude_config_dir_env() { + let _lock = integration_env_lock(); + let base = unique_base(); + let claude_dir = base.join("custom-claude"); + fs::create_dir_all(&claude_dir).unwrap(); + std::env::set_var(CLAUDE_CONFIG_DIR_ENV_VAR, &claude_dir); + + let installed = install_claude().unwrap(); + + assert_eq!(installed.settings_path, claude_dir.join("settings.json")); + assert_eq!( + installed.hook_path, + claude_dir.join("hooks").join(CLAUDE_HOOK_INSTALL_NAME) + ); + + clear_integration_path_env(); + let _ = fs::remove_dir_all(base); + } + #[test] fn install_claude_is_idempotent_for_hook_entries() { let _lock = integration_env_lock(); @@ -1377,6 +1481,25 @@ mod tests { let _ = fs::remove_dir_all(base); } + #[test] + fn install_codex_uses_codex_home_env() { + let _lock = integration_env_lock(); + let base = unique_base(); + let codex_dir = base.join("custom-codex"); + fs::create_dir_all(&codex_dir).unwrap(); + fs::write(codex_dir.join("config.toml"), "model = \"gpt-5.4\"\n").unwrap(); + std::env::set_var(CODEX_HOME_ENV_VAR, &codex_dir); + + let installed = install_codex().unwrap(); + + assert_eq!(installed.hook_path, codex_dir.join(CODEX_HOOK_INSTALL_NAME)); + assert_eq!(installed.hooks_path, codex_dir.join("hooks.json")); + assert_eq!(installed.config_path, codex_dir.join("config.toml")); + + clear_integration_path_env(); + let _ = fs::remove_dir_all(base); + } + #[test] fn install_codex_is_idempotent_for_hook_entries_and_feature_flag() { let _lock = integration_env_lock(); diff --git a/src/workspace.rs b/src/workspace.rs index 54f94e38..de030a80 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -575,10 +575,13 @@ mod tests { let root_pane = ws.tabs[0].root_pane; ws.tabs[0] .pane_cwds - .insert(root_pane, PathBuf::from("/tmp/pion")); + .insert(root_pane, PathBuf::from("/herdr-test/pion")); assert_eq!(ws.display_name(), "pion"); - assert_eq!(ws.resolved_identity_cwd(), Some(PathBuf::from("/tmp/pion"))); + assert_eq!( + ws.resolved_identity_cwd(), + Some(PathBuf::from("/herdr-test/pion")) + ); } #[test]