mod backend;
mod check;
mod commands;
mod daemon;
mod dap;
mod ghcprof;
mod init;
mod inspector;
mod jitdasm;
mod phpprofile;
mod profile;
mod pty;
mod resolve;
mod transport_common;
use std::time::Duration;
use anyhow::{Result, bail};
use clap::Parser;
use nix::unistd::{ForkResult, fork};
use backend::Registry;
const SUBCOMMAND_HELP: &str = "\
Common commands (forwarded to the session daemon):
Session lifecycle:
start <type> <target> Launch a debugger/profiler session
status Show active session details
kill Stop the active session
sessions [--group] List saved / live sessions
save [label] Persist the active session to .dbg/sessions/
replay <label> Re-open a saved session read-only
prune [--older-than D] Delete auto-saved sessions past age D
diff <other> Compare active session against another
cross <symbol> Aggregate all captured evidence for a symbol
Debugger control:
break <loc> [if <cond>] [log <msg>]
continue | step | next | finish | pause | restart
run [args...]
stack | frame <n> | locals | print <expr> | set <lval> <expr>
threads | thread <n> | watch <expr> | list [loc] | catch <evt>
Captured evidence (works live or in replay):
hits <loc> [--group-by F] [--count-by F --top N]
hit-diff <loc> <a> <b>
hit-trend <loc> <field>
source <symbol> [radius]
disasm <symbol> [--refresh]
disasm-diff <a> <b>
Adapter escape hatch:
raw <native-command> Send a literal command to the underlying tool
tool Print which underlying tool is driving the session
Run `dbg help <verb>` inside a session for backend-specific details.";
#[derive(Parser)]
#[command(
name = "dbg",
version,
about = "AI can read your code. Now it can live debug it too.",
after_help = SUBCOMMAND_HELP,
)]
struct Cli {
#[arg(long)]
init: Option<String>,
#[arg(long, alias = "language")]
backend: Option<String>,
#[arg(long, hide = true)]
jitdasm_repl: Option<String>,
#[arg(long, hide = true, default_value = "")]
jitdasm_pattern: String,
#[arg(long, hide = true)]
phpprofile_repl: Option<String>,
#[arg(long, hide = true, num_args = 2, value_names = &["PROF", "OUT"])]
ghcprof_convert: Option<Vec<String>>,
#[arg(long, hide = true, default_value = "php-profile> ")]
profile_prompt: String,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
}
fn is_kill_alias(verb: &str) -> bool {
matches!(verb, "kill" | "stop" | "quit" | "exit")
}
fn main() -> Result<()> {
let cli = Cli::parse();
let mut registry = Registry::new();
registry.register(Box::new(backend::lldb::LldbBackend));
registry.register(Box::new(backend::lldb_dap_proto::LldbDapProtoBackend));
registry.register(Box::new(backend::pdb::PdbBackend));
registry.register(Box::new(backend::debugpy_proto::DebugpyProtoBackend));
registry.register(Box::new(backend::netcoredbg::NetCoreDbgBackend));
registry.register(Box::new(backend::netcoredbg_proto::NetCoreDbgProtoBackend));
registry.register(Box::new(backend::delve::DelveBackend));
registry.register(Box::new(backend::delve_proto::DelveProtoBackend));
registry.register(Box::new(backend::jdb::JdbBackend));
registry.register(Box::new(backend::pprof::PprofBackend));
registry.register(Box::new(backend::perf::PerfBackend));
registry.register(Box::new(backend::callgrind::CallgrindBackend));
registry.register(Box::new(backend::pstats::PstatsBackend));
registry.register(Box::new(backend::memcheck::MemcheckBackend));
registry.register(Box::new(backend::massif::MassifBackend));
registry.register(Box::new(backend::dotnettrace::DotnetTraceBackend));
registry.register(Box::new(backend::jitdasm::JitDasmBackend));
registry.register(Box::new(backend::phpdbg::PhpdbgBackend));
registry.register(Box::new(backend::xdebug::XdebugProfileBackend));
registry.register(Box::new(backend::rdbg::RdbgBackend));
registry.register(Box::new(backend::stackprof::StackprofBackend));
registry.register(Box::new(backend::ghci::GhciBackend));
registry.register(Box::new(backend::ghcprof::GhcProfBackend));
registry.register(Box::new(backend::ocamldebug::OcamlDebugBackend));
registry.register(Box::new(backend::node_proto::NodeProtoBackend));
registry.register(Box::new(backend::nodeprof::NodeProfBackend));
init::auto_update(®istry);
if let Some(asm_path) = &cli.jitdasm_repl {
return jitdasm::run_repl(asm_path, &cli.jitdasm_pattern).map_err(Into::into);
}
if let Some(cg_path) = &cli.phpprofile_repl {
return phpprofile::run_repl(cg_path, &cli.profile_prompt).map_err(Into::into);
}
if let Some(paths) = &cli.ghcprof_convert {
return ghcprof::convert(&paths[0], &paths[1]);
}
if let Some(target) = &cli.init {
return init::run_init(target, ®istry);
}
if let Some(types_str) = &cli.backend {
let types: Vec<&str> = types_str.split(',').map(|s| s.trim()).collect();
let (results, unknown) = check::check_backends(®istry, &types);
if !unknown.is_empty() {
bail!(
"unknown type(s): {} (available: {})",
unknown.join(", "),
registry.available_types().join(", ")
);
}
print!("{}", check::format_results(&results));
let all_ok = results.iter().all(|(_, deps)| deps.iter().all(|d| d.ok));
if !all_ok {
std::process::exit(1);
}
return Ok(());
}
if cli.args.is_empty() {
println!("dbg — AI can read your code. Now it can live debug it too.\n");
println!(" dbg start <type> <target> [--break spec] [--args ...] [--run]");
println!(" dbg <any debugger command>");
println!(" dbg help list available commands");
println!(" dbg help <command> ask the debugger what a command does");
println!(" dbg kill\n");
println!("backends:");
for backend in registry.all_backends() {
let (results, _) = check::check_backends(®istry, &[backend.name()]);
let missing: Vec<&str> = results
.iter()
.flat_map(|(_, statuses)| statuses.iter().filter(|s| !s.ok).map(|s| s.name))
.collect();
let status = if missing.is_empty() {
"ready".to_string()
} else {
format!("missing: {}", missing.join(", "))
};
println!(" {:<14} {} [{}]", backend.name(), backend.description(), status);
}
return Ok(());
}
let first = cli.args[0].as_str();
if cli.args.iter().any(|a| a == "--help" || a == "-h") {
if let Some(text) = daemon::dbg_verb_help(first) {
println!("{text}");
return Ok(());
}
}
match first {
"start" => cmd_start(®istry, &cli.args[1..]),
"attach" => {
eprintln!(
"`dbg attach` is not a verb. Did you mean:\n \
dbg start <type> <target> --attach-pid <PID> (attach live to a process)\n \
dbg replay <label> (re-open a saved session; see `dbg sessions`)"
);
std::process::exit(2);
}
v if is_kill_alias(v) => {
let msg = daemon::kill_daemon()?;
println!("{msg}");
Ok(())
}
"status" if !daemon::is_running() => {
println!("no session");
Ok(())
}
"sessions" if !daemon::is_running() => {
print_live_daemon_peers();
let cwd = std::env::current_dir()?;
let ctx = commands::lifecycle::LifeCtx { cwd: &cwd, active: None };
let l = commands::lifecycle::Lifecycle::Sessions { group_only: false };
println!("{}", commands::lifecycle::run(&l, &ctx));
Ok(())
}
"sessions" => {
print_live_daemon_peers();
let cmd = cli.args.join(" ");
let resp = daemon::send_command(&cmd)?;
println!("{resp}");
Ok(())
}
"replay" => cmd_replay(&cli.args[1..]),
"help" => {
if cli.args.len() > 1 {
let topic = cli.args[1..].join(" ");
if let Some(text) = daemon::dbg_verb_help(topic.trim()) {
println!("{text}");
return Ok(());
}
ensure_running()?;
let resp = daemon::send_command(&format!("help {topic}"))?;
println!("{resp}");
Ok(())
} else if daemon::is_running() {
let resp = daemon::send_command("help")?;
println!("{resp}");
Ok(())
} else {
println!("dbg — unified debug CLI\n");
println!(" dbg start <type> <target> [--break spec] [--args ...] [--run]");
println!(" dbg <any debugger command>");
println!(" dbg help list available commands");
println!(" dbg help <command> help for a specific verb\n");
println!("session lifecycle: start, run, continue, step, next, finish, kill, status, cancel");
println!("inspection: break, locals, stack, print");
println!("crosstrack (DB): hits, hit-diff, hit-trend, cross, disasm, source");
println!("persistence: sessions, save, replay");
println!("timeline: events\n");
println!("types: {}", registry.available_types().join(", "));
Ok(())
}
}
_ => {
ensure_running()?;
let cmd = cli.args.join(" ");
let resp = daemon::send_command(&cmd)?;
println!("{resp}");
Ok(())
}
}
}
fn cmd_replay(args: &[String]) -> Result<()> {
use std::io::{BufRead, Write};
if args.is_empty() {
bail!("usage: dbg replay <label> (see `dbg sessions` for labels)");
}
daemon::clean_stale_runtime_files();
if daemon::is_running() {
bail!(
"a live session is running in this cwd — `dbg kill` it first, then \
`dbg replay {}`",
args[0]
);
}
let cwd = std::env::current_dir()?;
let sessions_dir = dbg_cli::session_db::sessions_dir(&cwd);
let label = &args[0];
let path = if std::path::Path::new(label).exists() {
std::path::PathBuf::from(label)
} else {
sessions_dir.join(format!("{label}.db"))
};
let path = if path.exists() {
path
} else if let Some(by_label) = find_session_by_label(&sessions_dir, label) {
by_label
} else {
bail!(
"no session matching `{label}` — got neither a file `{}` nor any \
saved DB whose `label` column matches. `dbg sessions` lists \
what's available.",
path.display()
);
};
let conn = rusqlite::Connection::open_with_flags(
&path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
)?;
let v: i64 = conn.query_row("PRAGMA user_version", [], |r| r.get(0)).unwrap_or(-1);
if v != dbg_cli::session_db::SCHEMA_VERSION {
bail!(
"session `{}` has schema_version={v}, expected {} — re-collect to replay",
path.display(),
dbg_cli::session_db::SCHEMA_VERSION
);
}
let db = dbg_cli::session_db::SessionDb::open(&path)?;
let (target, target_class): (String, String) = db
.conn()
.query_row(
"SELECT target, target_class FROM sessions LIMIT 1",
[],
|r| Ok((r.get(0)?, r.get(1)?)),
)
.unwrap_or_else(|_| ("?".into(), "?".into()));
eprintln!(
"replay `{label}` (target={target}, class={target_class}) — read-only crosstrack REPL"
);
eprintln!("supported: hits, hit-diff, hit-trend, cross, disasm, source, sessions");
eprintln!("type `quit` or EOF to exit");
use std::str::FromStr;
let target_class_enum = dbg_cli::session_db::TargetClass::from_str(&target_class)
.unwrap_or(dbg_cli::session_db::TargetClass::NativeCpu);
if args.len() > 1 {
let cmd = args[1..].join(" ");
let out = replay_eval(&cmd, &db, &cwd, &target, target_class_enum);
println!("{out}");
return Ok(());
}
let stdin = std::io::stdin();
let mut stdout = std::io::stdout();
let mut line = String::new();
loop {
write!(stdout, "replay> ")?;
stdout.flush()?;
line.clear();
if stdin.lock().read_line(&mut line)? == 0 {
break;
}
let cmd = line.trim();
if cmd.is_empty() {
continue;
}
if matches!(cmd, "quit" | "exit" | "q") {
break;
}
let out = replay_eval(cmd, &db, &cwd, &target, target_class_enum);
println!("{out}");
}
Ok(())
}
fn replay_eval(
cmd: &str,
db: &dbg_cli::session_db::SessionDb,
cwd: &std::path::Path,
target: &str,
target_class: dbg_cli::session_db::TargetClass,
) -> String {
match commands::dispatch_no_backend(cmd) {
Some(commands::Dispatched::Immediate(s)) => s,
Some(commands::Dispatched::Query(q)) => {
let ctx = commands::crosstrack::RunCtx {
target,
target_class,
cwd,
live: None,
};
commands::crosstrack::run(&q, db, &ctx)
}
Some(commands::Dispatched::Lifecycle(l)) => {
let ctx = commands::lifecycle::LifeCtx { cwd, active: Some(db) };
commands::lifecycle::run(&l, &ctx)
}
_ => {
"replay only supports crosstrack + lifecycle verbs (hits, hit-diff, \
hit-trend, cross, disasm, source, sessions, status). Live debugger \
verbs (step, continue, break, …) aren't available — start a new \
session with `dbg start` for those."
.to_string()
}
}
}
fn find_session_by_label(dir: &std::path::Path, label: &str) -> Option<std::path::PathBuf> {
if !dir.exists() {
return None;
}
for entry in std::fs::read_dir(dir).ok()?.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("db") {
continue;
}
let conn = match rusqlite::Connection::open_with_flags(
&path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
) {
Ok(c) => c,
Err(_) => continue,
};
let got: Result<String, _> =
conn.query_row("SELECT label FROM sessions LIMIT 1", [], |r| r.get(0));
if got.ok().as_deref() == Some(label) {
return Some(path);
}
}
None
}
fn ensure_running() -> Result<()> {
if !daemon::is_running() {
bail!("no session running — use: dbg start <type> <target>");
}
Ok(())
}
fn print_live_daemon_peers() {
let peers = daemon::live_slugs_in_cwd();
if peers.len() <= 1 {
return;
}
let active = std::env::var("DBG_SESSION")
.ok()
.or_else(|| {
std::fs::read_to_string(daemon::latest_pointer_path())
.ok()
.map(|s| s.trim().to_string())
});
println!("live daemons in this cwd:");
for slug in &peers {
let marker = if active.as_deref() == Some(slug.as_str()) { "*" } else { " " };
println!(" {marker} {slug}");
}
println!(" (set DBG_SESSION=<slug> to target a specific one)\n");
}
fn autodetect_backend(target: &str) -> Option<&'static str> {
let lower = target.to_ascii_lowercase();
if lower.ends_with(".py") {
Some("pdb")
} else if lower.ends_with(".go") {
Some("delve-proto")
} else if lower.ends_with(".java") {
Some("jdb")
} else if lower.ends_with(".rb") {
Some("rdbg")
} else if lower.ends_with(".php") {
Some("phpdbg")
} else if lower.ends_with(".csproj") {
Some("netcoredbg")
} else if lower.ends_with(".js") || lower.ends_with(".mjs") || lower.ends_with(".ts") {
Some("node-proto")
} else if lower.ends_with(".hs") {
Some("ghci")
} else if lower.ends_with(".ml") {
Some("ocamldebug")
} else {
None
}
}
fn cmd_start(registry: &Registry, args: &[String]) -> Result<()> {
if args.is_empty() {
bail!("usage: dbg start <type> <target> [--break spec] [--args ...] [--run]");
}
let args: Vec<String> = if args.len() == 1 {
match autodetect_backend(&args[0]) {
Some(t) => {
let mut v = vec![t.to_string()];
v.extend_from_slice(args);
v
}
None => bail!(
"usage: dbg start <type> <target> [--break spec] [--args ...] [--run]\n\
(no type given and couldn't infer one from `{}` — supported extensions: \
.py .go .java .rb .php .csproj .js .ts .hs .ml)",
args[0]
),
}
} else if args.len() >= 2 && registry.get(&args[0]).is_none() {
if let Some(t) = autodetect_backend(&args[0]) {
let mut v = vec![t.to_string()];
v.extend_from_slice(args);
v
} else {
args.to_vec()
}
} else {
args.to_vec()
};
let args = args.as_slice();
if args.len() < 2 {
bail!("usage: dbg start <type> <target> [--break spec] [--args ...] [--run]");
}
daemon::clean_stale_runtime_files();
let slug = daemon::allocate_slug()?;
unsafe { std::env::set_var("DBG_SESSION", &slug); }
daemon::write_latest_pointer(&slug);
let peers = daemon::live_slugs_in_cwd();
if !peers.is_empty() {
eprintln!(
"session: {slug} (coexisting with: {})",
peers.iter().filter(|s| *s != &slug).cloned().collect::<Vec<_>>().join(", "),
);
eprintln!(" other shells: set DBG_SESSION={slug} to target this session");
} else {
eprintln!("session: {slug}");
}
let backend_type = &args[0];
let target_raw = &args[1];
match backend_type.as_str() {
"gdbg" | "gpu" | "cuda" | "pytorch" | "triton"
| "tensorflow" | "tf" | "jax" | "mxnet" | "cupy" => {
eprintln!("GPU profiling uses gdbg, not dbg.");
eprintln!();
eprintln!(" gdbg {target_raw} # collect + analyze");
eprintln!(" gdbg --from <name> # reload saved session");
eprintln!(" gdbg check # verify nsys/ncu installed");
eprintln!();
eprintln!("gdbg auto-detects the target type (CUDA, PyTorch, Triton).");
eprintln!("It collects GPU timeline (nsys), hardware metrics (ncu),");
eprintln!("and op mapping (torch.profiler) into a single session,");
eprintln!("then opens an interactive REPL with 30+ analysis commands.");
bail!("use gdbg instead of dbg for GPU profiling");
}
_ => {}
}
let backend = registry
.get(backend_type)
.ok_or_else(|| {
anyhow::anyhow!(
"unknown type: {backend_type} (available: {})",
registry.available_types().join(", ")
)
})?;
if let Err(e) = backend.preflight() {
bail!(e);
}
let (results, _) = check::check_backends(registry, &[backend_type]);
let missing: Vec<_> = results
.iter()
.flat_map(|(_, deps)| deps.iter().filter(|d| !d.ok))
.collect();
if !missing.is_empty() {
eprintln!("missing dependencies:");
for d in &missing {
eprintln!(" {}: {}", d.name, d.install);
}
bail!("install missing dependencies and retry");
}
let mut breakpoints = Vec::new();
let mut run_args = Vec::new();
let mut do_run = false;
let mut attach_pid: Option<u32> = None;
let mut attach_host_port: Option<String> = None;
let mut i = 2;
while i < args.len() {
match args[i].as_str() {
"--break" | "-b" => {
i += 1;
if i < args.len() {
breakpoints.push(args[i].clone());
}
}
"--args" | "-a" => {
i += 1;
while i < args.len() && !args[i].starts_with("--") {
run_args.push(args[i].clone());
i += 1;
}
continue;
}
"--run" | "-r" => do_run = true,
"--attach-pid" => {
i += 1;
if i < args.len() {
attach_pid = args[i].parse().ok();
}
}
"--attach-port" => {
i += 1;
if i < args.len() {
attach_host_port = Some(args[i].clone());
}
}
other if !other.starts_with("--") => {
run_args.push(other.to_string());
}
_ => {}
}
i += 1;
}
let attach = if attach_pid.is_some() || attach_host_port.is_some() {
Some(backend::AttachSpec {
pid: attach_pid,
host_port: attach_host_port,
})
} else {
None
};
let resolved = if attach.is_some() {
target_raw.clone()
} else {
resolve::resolve(backend_type, target_raw)?
};
eprintln!("target: {resolved}");
let log_path = daemon::startup_log_path();
let _ = std::fs::remove_file(&log_path);
let fork_result = unsafe { fork() }?;
match fork_result {
ForkResult::Child => {
let _ = nix::unistd::setsid();
if let Ok(f) = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&log_path)
{
use std::os::unix::io::AsRawFd;
let _ = nix::unistd::dup2(f.as_raw_fd(), 2);
}
if let Err(e) = daemon::run_daemon(backend, &resolved, &run_args, attach.as_ref()) {
eprintln!("daemon error: {e:#}");
std::process::exit(1);
}
std::process::exit(0);
}
ForkResult::Parent { .. } => {
if !daemon::wait_for_socket(Duration::from_secs(30)) {
let log = std::fs::read_to_string(&log_path).unwrap_or_default();
if log.trim().is_empty() {
bail!("daemon failed to start");
} else {
bail!("daemon failed to start:\n{}", log.trim());
}
}
std::thread::sleep(Duration::from_millis(150));
let healthy = daemon::is_running()
&& daemon::send_command("status").is_ok();
if !healthy {
let log = std::fs::read_to_string(&log_path).unwrap_or_default();
let log = log.trim();
if log.is_empty() {
bail!("daemon started but exited before the debugger was ready");
} else {
bail!("daemon started but exited before the debugger was ready:\n{log}");
}
}
let mut bp_ok = true;
for bp in &breakpoints {
let cmd = if backend.canonical_ops().is_some() {
format!("break {bp}")
} else {
backend.format_breakpoint(bp)
};
let resp = daemon::send_command(&cmd)?;
println!("{resp}");
let lc = resp.to_lowercase();
if lc.contains("[error")
|| lc.contains("could not")
|| lc.contains("cannot find")
|| lc.contains("no source")
|| lc.contains("unable to set")
|| lc.contains("blank or comment")
{
bp_ok = false;
if lc.contains("blank or comment") {
eprintln!(
"dbg: `{bp}` points at a blank/comment line — pdb won't stop there. \
Pick an executable line (or use `--break <function_name>`)."
);
}
}
}
if do_run {
if !bp_ok && !breakpoints.is_empty() {
eprintln!(
"dbg: skipping --run because a breakpoint failed to register. \
Fix the breakpoint or omit --break and drive with `dbg run` manually."
);
} else {
let cmd = if backend.canonical_ops().is_some() {
"run".to_string()
} else {
backend.run_command().to_string()
};
let resp = daemon::send_command(&cmd)?;
println!("{resp}");
}
}
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn autodetect_go_prefers_dap() {
assert_eq!(autodetect_backend("main.go"), Some("delve-proto"));
assert_eq!(autodetect_backend("MAIN.GO"), Some("delve-proto"));
}
#[test]
fn autodetect_unambiguous_extensions() {
assert_eq!(autodetect_backend("broken.py"), Some("pdb"));
assert_eq!(autodetect_backend("App.java"), Some("jdb"));
assert_eq!(autodetect_backend("script.rb"), Some("rdbg"));
assert_eq!(autodetect_backend("site.php"), Some("phpdbg"));
assert_eq!(autodetect_backend("proj.csproj"), Some("netcoredbg"));
assert_eq!(autodetect_backend("app.js"), Some("node-proto"));
assert_eq!(autodetect_backend("app.ts"), Some("node-proto"));
assert_eq!(autodetect_backend("foo.hs"), Some("ghci"));
assert_eq!(autodetect_backend("bin/no-ext"), None);
}
#[test]
fn kill_aliases_cover_common_stop_verbs() {
for alias in ["kill", "stop", "quit", "exit"] {
assert!(is_kill_alias(alias), "`{alias}` must end the session");
}
for non_alias in ["start", "break", "sessions", "hits", "continue"] {
assert!(
!is_kill_alias(non_alias),
"`{non_alias}` must not be treated as kill"
);
}
}
#[test]
fn find_session_by_label_matches_stored_label_not_filename_stem() {
let tmp = tempfile::TempDir::new().unwrap();
let dir = tmp.path();
let path = dir.join("session-10.db");
let conn = rusqlite::Connection::open(&path).unwrap();
conn.execute("CREATE TABLE sessions (label TEXT)", []).unwrap();
conn.execute(
"INSERT INTO sessions (label) VALUES (?1)",
["broken-20260419-154358"],
)
.unwrap();
drop(conn);
let got = find_session_by_label(dir, "broken-20260419-154358");
assert_eq!(got.as_deref(), Some(path.as_path()));
assert!(find_session_by_label(dir, "nope").is_none());
}
#[test]
fn top_level_help_lists_subcommand_vocabulary() {
use clap::CommandFactory;
let rendered = Cli::command().render_help().to_string();
for verb in [
"start", "kill", "sessions", "replay",
"break", "continue", "stack", "locals",
"hits", "hit-diff", "hit-trend",
"cross", "disasm", "raw",
] {
assert!(
rendered.contains(verb),
"`dbg --help` is missing `{verb}` — the after_help \
subcommand listing regressed:\n{rendered}"
);
}
}
#[test]
fn help_flag_short_circuits_on_known_verb() {
for verb in ["hits", "start", "replay", "save"] {
assert!(
daemon::dbg_verb_help(verb).is_some(),
"dbg_verb_help missing entry for `{verb}`"
);
}
}
#[test]
fn dbg_verb_help_is_publicly_callable() {
assert!(daemon::dbg_verb_help("start").is_some());
assert!(daemon::dbg_verb_help("replay").is_some());
assert!(daemon::dbg_verb_help("not-a-verb").is_none());
}
}