dbg-cli 0.6.0

A universal debugger CLI that lets AI agents observe runtime state instead of guessing from source code
Documentation
use super::{Backend, Dependency, DependencyCheck, SpawnConfig, shell_escape};
use crate::daemon::session_tmp;

pub struct MassifBackend;

impl Backend for MassifBackend {
    fn name(&self) -> &'static str {
        "massif"
    }

    fn description(&self) -> &'static str {
        "heap profiler (valgrind)"
    }

    fn types(&self) -> &'static [&'static str] {
        &["massif"]
    }

    fn spawn_config(&self, target: &str, args: &[String]) -> anyhow::Result<SpawnConfig> {
        let out_file = session_tmp("massif.out");
        let out_str = out_file.display().to_string();

        let mut valgrind_cmd = format!(
            "valgrind --tool=massif --massif-out-file={} {}",
            shell_escape(&out_str),
            shell_escape(target)
        );
        for a in args {
            valgrind_cmd.push(' ');
            valgrind_cmd.push_str(&shell_escape(a));
        }

        Ok(SpawnConfig {
            bin: "bash".into(),
            args: vec!["--norc".into(), "--noprofile".into()],
            env: vec![("PS1".into(), "$ ".into()), ("MASSIF_OUT".into(), out_str)],
            init_commands: vec![valgrind_cmd, "echo '--- massif data ready ---'".into()],
        })
    }

    fn prompt_pattern(&self) -> &str {
        r"\$ $"
    }

    fn dependencies(&self) -> Vec<Dependency> {
        vec![
            Dependency {
                name: "valgrind",
                check: DependencyCheck::Binary {
                    name: "valgrind",
                    alternatives: &["valgrind"],
                    version_cmd: None,
                },
                install: "sudo apt install valgrind  # or: brew install valgrind",
            },
            Dependency {
                name: "ms_print",
                check: DependencyCheck::Binary {
                    name: "ms_print",
                    alternatives: &["ms_print"],
                    version_cmd: None,
                },
                install: "sudo apt install valgrind  # included with valgrind",
            },
        ]
    }

    fn run_command(&self) -> &'static str {
        "ms_print \"$MASSIF_OUT\""
    }

    fn is_profile_backend(&self) -> bool {
        true
    }

    fn quit_command(&self) -> &'static str {
        "exit"
    }

    fn parse_help(&self, _raw: &str) -> String {
        "massif: ms_print [--threshold=N] \"$MASSIF_OUT\"".to_string()
    }

    fn clean(&self, _cmd: &str, output: &str) -> String {
        let mut lines = Vec::new();
        for line in output.lines() {
            let trimmed = line.trim();
            if trimmed.starts_with("==")
                && (trimmed.contains("Massif") || trimmed.contains("Copyright"))
            {
                continue;
            }
            lines.push(line);
        }
        lines.join("\n")
    }

    fn adapters(&self) -> Vec<(&'static str, &'static str)> {
        vec![(
            "massif.md",
            include_str!("../../skills/references/adapters/massif.md"),
        )]
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn clean_filters_massif_header() {
        let input = "  == Massif, a heap profiler\n  == Copyright (C) 2022\nactual data\nmore data";
        let r = MassifBackend.clean("ms_print", input);
        assert!(!r.contains("Massif"));
        assert!(!r.contains("Copyright"));
        assert!(r.contains("actual data"));
    }

    #[test]
    fn clean_keeps_non_header_lines() {
        let input = "  == Some other valgrind line\ndata";
        let r = MassifBackend.clean("ms_print", input);
        assert!(r.contains("Some other valgrind line"));
    }

    #[test]
    fn spawn_config_sets_massif_out_env() {
        let cfg = MassifBackend.spawn_config("./app", &[]).unwrap();
        assert!(cfg.env.iter().any(|(k, _)| k == "MASSIF_OUT"));
    }

    #[test]
    fn spawn_config_valgrind_cmd_with_args() {
        let cfg = MassifBackend
            .spawn_config("./app", &["--size".into(), "100".into()])
            .unwrap();
        let cmd = &cfg.init_commands[0];
        assert!(cmd.contains("--tool=massif"));
        assert!(cmd.contains("./app"));
        assert!(cmd.contains("--size"));
        assert!(cmd.contains("100"));
    }

    #[test]
    fn spawn_config_escapes_spaces() {
        let cfg = MassifBackend.spawn_config("./my app", &[]).unwrap();
        let cmd = &cfg.init_commands[0];
        assert!(cmd.contains("'./my app'"), "target not escaped: {cmd}");
    }

    #[test]
    fn session_output_path_is_quoted_in_commands() {
        assert_eq!(MassifBackend.run_command(), "ms_print \"$MASSIF_OUT\"");
        assert!(MassifBackend.parse_help("").contains("\"$MASSIF_OUT\""));
    }
}