aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/src/integrations.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/dinggerd/src/integrations.rs')
-rw-r--r--crates/dinggerd/src/integrations.rs62
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"}"#,