aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/src/integrations.rs
blob: d7cbd346bda5dd688eba3fcb96322a9ed45a7be5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
//! Agent integrations: hook extensions that let agents report their own
//! state (herdr's integration model), so detection does not rely on screen
//! scraping for the agents that support it.

use std::path::PathBuf;

use detect_core::Agent;

/// The embedded pi extension, adapted from herdr's `herdr-agent-state.ts`
/// (Apache-2.0, Copyright herdr contributors).
const PI_EXTENSION: &str = include_str!("../assets/pi/pi-agent-state.ts");

fn pi_extension_dir() -> Result<PathBuf, String> {
    let base = std::env::var_os("PI_CODING_AGENT_DIR")
        .map(PathBuf::from)
        .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".pi").join("agent")))
        .ok_or("cannot determine pi agent dir (no HOME)")?;
    Ok(base.join("extensions"))
}

/// zsh hook: records the command line running in each tmux pane exactly as
/// typed (aliases, pipes, quoting), for restore to retype.
const ZSH_HOOK: &str = r#"# installed by dingger; `dingger install zsh` overwrites this file.
#
# Records the command line running in this tmux pane, as typed, in the pane
# option @dingger_cmd, so `dingger restore` can type it again (without
# running it) after a crash or shutdown.
if [[ -n $TMUX_PANE ]]; then
  _dingger_preexec() { command tmux set-option -p -t "$TMUX_PANE" @dingger_cmd "$1" 2>/dev/null &! }
  _dingger_precmd() { command tmux set-option -p -u -t "$TMUX_PANE" @dingger_cmd 2>/dev/null &! }
  autoload -Uz add-zsh-hook
  add-zsh-hook preexec _dingger_preexec
  add-zsh-hook precmd _dingger_precmd
fi
"#;

fn config_dir() -> Result<PathBuf, String> {
    std::env::var_os("DINGGER_CONFIG_DIR")
        .map(PathBuf::from)
        .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".config").join("dingger")))
        .ok_or_else(|| "cannot determine the config dir (no HOME)".to_string())
}

fn install_zsh() -> Result<(), String> {
    let dir = config_dir()?;
    std::fs::create_dir_all(&dir).map_err(|err| format!("mkdir failed: {err}"))?;
    let path = dir.join("dingger.zsh");
    std::fs::write(&path, ZSH_HOOK).map_err(|err| format!("write failed: {err}"))?;
    println!(
        "installed {}\nadd this line to your .zshrc (dingger does not edit it):\n\n  [[ -r {0} ]] && source {0}\n",
        path.display()
    );
    Ok(())
}

/// Install an integration: an agent's state hooks, or `zsh` for retyping
/// command lines on restore.
pub fn install(agent: &str) -> Result<(), String> {
    if agent == "zsh" {
        return install_zsh();
    }
    let parsed = detect_core::parse_agent_label(agent)
        .ok_or_else(|| format!("unknown agent: {agent}"))?;
    match parsed {
        Agent::Pi => install_pi(),
        other => Err(format!(
            "no hook integration for {} yet — screen detection is used",
            detect_core::agent_label(other)
        )),
    }
}

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}"))?;
    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",
        path.display()
    );
    Ok(())
}