dbg-cli 0.2.2

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

pub struct PdbBackend;

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

    fn description(&self) -> &'static str {
        "Python debugger"
    }

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

    fn spawn_config(&self, target: &str, args: &[String]) -> anyhow::Result<SpawnConfig> {
        let python = std::env::var("PYTHON").unwrap_or_else(|_| "python3".into());
        let mut spawn_args = vec!["-u".into(), "-m".into(), "pdb".into(), target.into()];
        spawn_args.extend(args.iter().cloned());

        Ok(SpawnConfig {
            bin: python,
            args: spawn_args,
            env: vec![("PYTHONDONTWRITEBYTECODE".into(), "1".into())],
            init_commands: vec![],
        })
    }

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

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

    fn format_breakpoint(&self, spec: &str) -> String {
        format!("break {spec}")
    }

    fn run_command(&self) -> &'static str {
        "continue"
    }

    fn parse_help(&self, raw: &str) -> String {
        let mut cmds: Vec<String> = Vec::new();
        for line in raw.lines() {
            let line = line.trim();
            if line.is_empty()
                || line.starts_with('=')
                || line.starts_with("Documented")
                || line.starts_with("Undocumented")
                || line.starts_with("Miscellaneous")
            {
                continue;
            }
            for tok in line.split_whitespace() {
                if tok.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
                    && tok.len() < 20
                    && !tok.is_empty()
                {
                    cmds.push(tok.to_string());
                }
            }
        }
        cmds.sort();
        cmds.dedup();
        format!("pdb: {}", cmds.join(", "))
    }

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

    fn clean(&self, cmd: &str, output: &str) -> CleanResult {
        let trimmed = cmd.trim();
        let output = if trimmed == "where" || trimmed == "bt" {
            output
                .lines()
                .filter(|l| !l.contains("bdb.py") && !l.contains("<string>(1)"))
                .collect::<Vec<_>>()
                .join("\n")
        } else {
            output.to_string()
        };
        CleanResult {
            output,
            events: vec![],
        }
    }
}

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

    #[test]
    fn format_breakpoint() {
        assert_eq!(PdbBackend.format_breakpoint("test.py:10"), "break test.py:10");
    }

    #[test]
    fn clean_where_filters_bdb() {
        let input = "> script.py(5)main()\n  bdb.py(123)dispatch()\n> script.py(10)<module>()\n  <string>(1)<module>()";
        let r = PdbBackend.clean("where", input);
        assert!(!r.output.contains("bdb.py"));
        assert!(!r.output.contains("<string>(1)"));
        assert!(r.output.contains("script.py(5)"));
        assert!(r.output.contains("script.py(10)"));
    }

    #[test]
    fn clean_other_cmd_passthrough() {
        let input = "bdb.py line should stay";
        let r = PdbBackend.clean("p x", input);
        assert!(r.output.contains("bdb.py"));
    }

    #[test]
    fn spawn_config_includes_target_and_args() {
        let cfg = PdbBackend
            .spawn_config("test.py", &["--verbose".into()])
            .unwrap();
        assert!(cfg.args.contains(&"test.py".to_string()));
        assert!(cfg.args.contains(&"--verbose".to_string()));
        assert!(cfg.args.contains(&"-m".to_string()));
    }

    #[test]
    fn parse_help_extracts_and_deduplicates() {
        let raw = "Documented commands:\n========\nbreak  continue  help\nbreak  next  step";
        let result = PdbBackend.parse_help(raw);
        assert!(result.contains("break"));
        assert!(result.contains("continue"));
        let count = result.matches("break").count();
        assert_eq!(count, 1);
    }
}