Skip to main content

agent_berth/
cli.rs

1use anyhow::Result;
2use clap::{Parser, Subcommand};
3
4use crate::duration::parse_idle;
5use crate::paths::Context;
6use crate::{attach, doctor, list, notify, resume, rm, server, service, setup, tui};
7
8/// Monitor coding agents and resume their sessions.
9#[derive(Debug, Parser)]
10#[command(name = "agent-berth", version, about, propagate_version = true)]
11struct Cli {
12    #[command(subcommand)]
13    command: Option<Command>,
14}
15
16#[derive(Debug, Subcommand)]
17enum Command {
18    /// Launch the interactive TUI (default when no subcommand is given)
19    Tui,
20    /// Start the server
21    Server,
22    /// Install the user service and agent hooks
23    Setup {
24        /// Install hooks only; do not install the user service
25        #[arg(long)]
26        no_service: bool,
27    },
28    /// Stop the service and remove agent hooks
29    Teardown,
30    /// Manage the background server service
31    Service {
32        #[command(subcommand)]
33        action: ServiceAction,
34    },
35    /// List sessions
36    List {
37        /// Print JSON
38        #[arg(long)]
39        json: bool,
40        /// Show sessions that resume would start
41        #[arg(long)]
42        resumable: bool,
43        /// Only include idle sessions within this window (default: 20m)
44        #[arg(
45            long,
46            value_name = "DURATION",
47            requires = "resumable",
48            num_args = 0..=1,
49            default_missing_value = "20m"
50        )]
51        idle: Option<String>,
52        /// Restrict to sessions in the current directory
53        #[arg(long, requires = "resumable")]
54        here: bool,
55    },
56    /// Report agent status to the server (used by hooks)
57    Notify {
58        #[arg(long)]
59        provider: String,
60    },
61    /// Attach to a running agent in tmux
62    Attach {
63        /// Initial fzf query
64        query: Option<String>,
65        /// Show the pane preview (toggle with ctrl-t)
66        #[arg(short, long)]
67        preview: bool,
68        /// Only consider panes in the current tmux session
69        #[arg(short, long)]
70        session: bool,
71        /// Print candidate panes without attaching
72        #[arg(long)]
73        dry_run: bool,
74    },
75    /// Check server, service, and agent hooks
76    Doctor,
77    /// Resume sessions after the server or host restarts
78    Resume {
79        /// Select the session matching this pattern with fzf
80        pattern: Option<String>,
81        /// Include idle sessions within this window (default: 20m)
82        #[arg(
83            long,
84            value_name = "DURATION",
85            num_args = 0..=1,
86            default_missing_value = "20m"
87        )]
88        idle: Option<String>,
89        /// Restrict to sessions in the current directory
90        #[arg(long)]
91        here: bool,
92        /// Print actions without starting agents
93        #[arg(long)]
94        dry_run: bool,
95    },
96    /// Remove sessions so they are hidden and never resumed
97    #[command(alias = "remove")]
98    Rm {
99        /// Only consider sessions matching these terms
100        patterns: Vec<String>,
101    },
102}
103
104#[derive(Debug, Subcommand)]
105enum ServiceAction {
106    /// Start the background server
107    Start,
108    /// Stop the background server
109    Stop,
110    /// Restart the background server
111    Restart,
112}
113
114pub fn run() -> Result<()> {
115    let cli = Cli::parse();
116    let ctx = Context::from_env()?;
117    let Some(command) = cli.command else {
118        if !std::io::IsTerminal::is_terminal(&std::io::stdout()) {
119            return list::run(&ctx, false, false, None, false);
120        }
121        return tui::run(&ctx);
122    };
123    match command {
124        Command::Tui => {
125            if !std::io::IsTerminal::is_terminal(&std::io::stdout()) {
126                anyhow::bail!("tui requires a terminal");
127            }
128            tui::run(&ctx)
129        }
130        Command::Server => server::run(&ctx),
131        Command::Setup { no_service } => setup::setup(&ctx, no_service),
132        Command::Teardown => setup::teardown(&ctx),
133        Command::Service { action } => match action {
134            ServiceAction::Start => service::start(&ctx),
135            ServiceAction::Stop => service::stop(&ctx),
136            ServiceAction::Restart => service::restart(&ctx),
137        },
138        Command::List {
139            json,
140            resumable,
141            idle,
142            here,
143        } => {
144            let idle = if resumable {
145                parse_idle(idle.as_deref())?
146            } else {
147                None
148            };
149            list::run(&ctx, json, resumable, idle, here)
150        }
151        Command::Notify { provider } => notify::run(&ctx, provider),
152        Command::Attach {
153            query,
154            preview,
155            session,
156            dry_run,
157        } => attach::run(&ctx, query, preview, session, dry_run),
158        Command::Doctor => doctor::run(&ctx),
159        Command::Resume {
160            pattern,
161            idle,
162            here,
163            dry_run,
164        } => resume::run(&ctx, parse_idle(idle.as_deref())?, pattern, here, dry_run),
165        Command::Rm { patterns } => rm::run(&ctx, patterns),
166    }
167}