diff options
| author | Igor Soarez <igor@soarez.org> | 2026-10-30 17:27:21 +0200 |
|---|---|---|
| committer | Igor Soarez <igor@soarez.org> | 2026-10-30 17:27:21 +0200 |
| commit | b0cfd10b80443d3f67d263ff4db177bb53cc90ed (patch) | |
| tree | 957951350434f38da83457f5467b6c000e892867 /crates/dinggerd/src/integrations.rs | |
| parent | 91436536b69c10aa333c2b2978284b57cca3ed64 (diff) | |
reload pi agents on an old extension: versioned extension, control socket, menu item and reload-agents
Diffstat (limited to 'crates/dinggerd/src/integrations.rs')
| -rw-r--r-- | crates/dinggerd/src/integrations.rs | 62 |
1 files changed, 59 insertions, 3 deletions
diff --git a/crates/dinggerd/src/integrations.rs b/crates/dinggerd/src/integrations.rs index b3daf1d..5d9cf7a 100644 --- a/crates/dinggerd/src/integrations.rs +++ b/crates/dinggerd/src/integrations.rs @@ -10,7 +10,28 @@ use serde_json::{json, Value}; /// The embedded pi extension, adapted from herdr's `herdr-agent-state.ts` /// (Apache-2.0, Copyright herdr contributors). +/// It carries a placeholder for its version, which install fills in. const PI_EXTENSION: &str = include_str!("../assets/pi/pi-agent-state.ts"); +const VERSION_PLACEHOLDER: &str = "__DINGGER_EXTENSION_VERSION__"; + +/// The extension's version: a fingerprint of its text (FNV-1a, so it does +/// not change with the Rust release). A running pi reports the version it +/// loaded; one that differs from this runs an old extension. +pub fn pi_extension_version() -> String { + let hash = PI_EXTENSION.bytes().fold(0xcbf2_9ce4_8422_2325_u64, |hash, byte| { + (hash ^ u64::from(byte)).wrapping_mul(0x0100_0000_01b3) + }); + format!("{hash:016x}") +} + +/// The extension as installed: stamped with its version. +fn pi_extension_text() -> String { + PI_EXTENSION.replace(VERSION_PLACEHOLDER, &pi_extension_version()) +} + +fn pi_extension_path() -> Result<PathBuf, String> { + Ok(pi_extension_dir()?.join("dingger-agent-state.ts")) +} fn pi_extension_dir() -> Result<PathBuf, String> { let base = std::env::var_os("PI_CODING_AGENT_DIR") @@ -76,16 +97,40 @@ pub fn install(agent: &str) -> Result<(), String> { fn install_pi() -> Result<(), String> { let dir = pi_extension_dir()?; std::fs::create_dir_all(&dir).map_err(|err| format!("mkdir failed: {err}"))?; - let path = dir.join("dingger-agent-state.ts"); - std::fs::write(&path, PI_EXTENSION).map_err(|err| format!("write failed: {err}"))?; + let path = pi_extension_path()?; + std::fs::write(&path, pi_extension_text()).map_err(|err| format!("write failed: {err}"))?; tracing::info!(path = %path.display(), "installed pi extension"); println!( - "installed {} — new pi sessions report their state directly;\nrestart running pi agents (or reload them) to pick it up", + "installed {} — new pi sessions report their state directly;\nrunning pi agents pick it up when reloaded (`dingger reload-agents`)", path.display() ); Ok(()) } +/// Whether the installed pi extension is this dingger's: only then does a +/// reload bring an agent up to date (otherwise it loads the old one again). +pub fn pi_extension_installed_is_current() -> bool { + pi_extension_path() + .and_then(|path| std::fs::read_to_string(path).map_err(|err| err.to_string())) + .is_ok_and(|installed| installed == pi_extension_text()) +} + +/// At the daemon's start: if the pi extension is installed and differs +/// from this dingger's, update it, so a dingger update needs no reinstall. +/// Only dingger's own file (it says so on its first line). +pub fn update_pi_extension() { + let Ok(path) = pi_extension_path() else { return }; + let Ok(installed) = std::fs::read_to_string(&path) else { return }; + let current = pi_extension_text(); + if installed == current || !installed.starts_with("// installed by dingger") { + return; + } + match std::fs::write(&path, current) { + Ok(()) => tracing::info!(path = %path.display(), version = %pi_extension_version(), "updated pi extension"), + Err(err) => tracing::warn!(path = %path.display(), %err, "failed to update pi extension"), + } +} + /// Claude Code's user settings, where its hooks are configured. fn claude_settings_path() -> Result<PathBuf, String> { std::env::var_os("CLAUDE_CONFIG_DIR") @@ -217,6 +262,17 @@ mod tests { use super::*; #[test] + fn the_installed_extension_carries_its_version_once() { + let version = pi_extension_version(); + assert_eq!(version.len(), 16); + assert_eq!(PI_EXTENSION.matches(VERSION_PLACEHOLDER).count(), 1, "the asset has one placeholder"); + let text = pi_extension_text(); + assert!(!text.contains(VERSION_PLACEHOLDER)); + assert!(text.contains(&format!("\"{version}\"")), "stamped as a string literal"); + assert!(text.starts_with("// installed by dingger"), "update_pi_extension recognizes its own file"); + } + + #[test] fn claude_hook_is_added_beside_other_hooks_and_keys_in_order() { let settings: Value = serde_json::from_str( r#"{"model":"sonnet","hooks":{"SessionStart":[{"matcher":"^startup$","hooks":[{"type":"command","command":"bash herdr.sh session"}]}],"Stop":[]},"theme":"auto"}"#, |
