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