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