1use clap::CommandFactory;
4use clap::Parser;
5use clap_complete::generate;
6
7use crate::common::{Colors, color_init};
8use crate::daemon::{DaemonError, start_daemon};
9use crate::ipc::{ClientError, DaemonClient, ensure_daemon};
10
11use crate::attach::AttachError;
12use crate::commands::{Cli, Commands, DaemonCommand, DebugCommand, RecordAction};
13use crate::handlers::{self, HandlerContext};
14
15mod exit_codes {
17 pub const SUCCESS: i32 = 0;
18 pub const GENERAL_ERROR: i32 = 1;
19 pub const USAGE: i32 = 64; pub const UNAVAILABLE: i32 = 69; pub const CANTCREAT: i32 = 73; pub const IOERR: i32 = 74; pub const TEMPFAIL: i32 = 75; }
25
26pub struct Application;
28
29impl Application {
30 pub fn new() -> Self {
32 Self
33 }
34
35 pub fn run(&self) -> i32 {
37 match self.execute() {
38 Ok(()) => exit_codes::SUCCESS,
39 Err(e) => self.handle_error(e),
40 }
41 }
42
43 fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
44 let cli = Cli::parse();
45 color_init(cli.no_color);
46
47 if self.handle_standalone_commands(&cli)? {
49 return Ok(());
50 }
51
52 let mut client = self.connect_to_daemon()?;
54
55 if !matches!(cli.command, Commands::Daemon(_) | Commands::Version) {
57 check_version_mismatch(&mut client);
58 }
59
60 let format = cli.effective_format();
62 let mut ctx = HandlerContext::new(&mut client, cli.session, format);
63 self.dispatch_command(&mut ctx, &cli.command)
64 }
65
66 fn handle_standalone_commands(&self, cli: &Cli) -> Result<bool, Box<dyn std::error::Error>> {
69 match &cli.command {
70 Commands::Daemon(DaemonCommand::Start { foreground: true }) => {
71 start_daemon()?;
72 Ok(true)
73 }
74 Commands::Daemon(DaemonCommand::Start { foreground: false }) => {
75 crate::ipc::start_daemon_background()?;
76 println!("Daemon started in background");
77 Ok(true)
78 }
79 Commands::Completions { shell } => {
80 let mut cmd = Cli::command();
81 generate(*shell, &mut cmd, "agent-tui", &mut std::io::stdout());
82 Ok(true)
83 }
84 _ => Ok(false),
85 }
86 }
87
88 fn connect_to_daemon(&self) -> Result<impl DaemonClient, Box<dyn std::error::Error>> {
89 ensure_daemon().map_err(|e| {
90 eprintln!(
91 "{} Failed to connect to daemon: {}",
92 Colors::error("Error:"),
93 e
94 );
95 eprintln!();
96 eprintln!("Troubleshooting:");
97 eprintln!(" 1. Check if socket directory is writable (usually /tmp)");
98 eprintln!(" 2. Try starting daemon manually: agent-tui daemon");
99 eprintln!(" 3. Check current configuration: agent-tui env");
100 e.into()
101 })
102 }
103
104 fn dispatch_command<C: DaemonClient>(
105 &self,
106 ctx: &mut HandlerContext<C>,
107 command: &Commands,
108 ) -> Result<(), Box<dyn std::error::Error>> {
109 match command {
110 Commands::Daemon(daemon_cmd) => match daemon_cmd {
111 DaemonCommand::Start { .. } => unreachable!("Handled in standalone"),
112 DaemonCommand::Stop { force } => handlers::handle_daemon_stop(ctx, *force)?,
113 DaemonCommand::Status => handlers::handle_daemon_status(ctx)?,
114 DaemonCommand::Restart => handlers::handle_daemon_restart(ctx)?,
115 },
116 Commands::Completions { .. } => unreachable!("Handled in standalone"),
117
118 Commands::Run {
119 command,
120 args,
121 cwd,
122 cols,
123 rows,
124 } => handlers::handle_spawn(
125 ctx,
126 command.clone(),
127 args.clone(),
128 cwd.clone(),
129 *cols,
130 *rows,
131 )?,
132
133 Commands::Snap {
134 elements,
135 accessibility,
136 interactive_only,
137 region,
138 strip_ansi,
139 include_cursor,
140 } => {
141 if *accessibility {
142 handlers::handle_accessibility_snapshot(ctx, *interactive_only)?
143 } else {
144 handlers::handle_snapshot(
145 ctx,
146 *elements,
147 region.clone(),
148 *strip_ansi,
149 *include_cursor,
150 )?
151 }
152 }
153
154 Commands::Click {
155 element_ref,
156 double,
157 } => {
158 if *double {
159 handlers::handle_dbl_click(ctx, element_ref.clone())?
160 } else {
161 handlers::handle_click(ctx, element_ref.clone())?
162 }
163 }
164 Commands::Fill { element_ref, value } => {
165 handlers::handle_fill(ctx, element_ref.clone(), value.clone())?
166 }
167
168 Commands::Key {
169 key,
170 text,
171 hold,
172 release,
173 } => {
174 if let Some(text) = text {
175 handlers::handle_type(ctx, text.clone())?
176 } else if let Some(key) = key {
177 if *hold {
178 handlers::handle_keydown(ctx, key.clone())?
179 } else if *release {
180 handlers::handle_keyup(ctx, key.clone())?
181 } else {
182 handlers::handle_press(ctx, key.clone())?
183 }
184 }
185 }
186
187 Commands::Wait { params } => handlers::handle_wait(ctx, params.clone())?,
188 Commands::Kill => handlers::handle_kill(ctx)?,
189 Commands::Restart => handlers::handle_restart(ctx)?,
190 Commands::Ls => handlers::handle_sessions(ctx)?,
191 Commands::Status { verbose } => handlers::handle_health(ctx, *verbose)?,
192
193 Commands::Select {
194 element_ref,
195 option,
196 } => handlers::handle_select(ctx, element_ref.clone(), option.clone())?,
197 Commands::MultiSelect {
198 element_ref,
199 options,
200 } => handlers::handle_multiselect(ctx, element_ref.clone(), options.clone())?,
201
202 Commands::Scroll {
203 direction,
204 amount,
205 element: _,
206 to_ref,
207 } => {
208 if let Some(element_ref) = to_ref {
209 handlers::handle_scroll_into_view(ctx, element_ref.clone())?
210 } else if let Some(dir) = direction {
211 handlers::handle_scroll(ctx, *dir, *amount)?
212 }
213 }
214
215 Commands::Focus { element_ref } => handlers::handle_focus(ctx, element_ref.clone())?,
216 Commands::Clear { element_ref } => handlers::handle_clear(ctx, element_ref.clone())?,
217 Commands::SelectAll { element_ref } => {
218 handlers::handle_select_all(ctx, element_ref.clone())?
219 }
220
221 Commands::Count { role, name, text } => {
222 handlers::handle_count(ctx, role.clone(), name.clone(), text.clone())?
223 }
224
225 Commands::Toggle {
226 element_ref,
227 on,
228 off,
229 } => {
230 let state = if *on {
231 Some(true)
232 } else if *off {
233 Some(false)
234 } else {
235 None
236 };
237 handlers::handle_toggle(ctx, element_ref.clone(), state)?
238 }
239
240 Commands::RecordStart => handlers::handle_record_start(ctx)?,
241 Commands::RecordStop {
242 output,
243 record_format,
244 } => handlers::handle_record_stop(ctx, output.clone(), *record_format)?,
245 Commands::RecordStatus => handlers::handle_record_status(ctx)?,
246
247 Commands::Trace { count, start, stop } => {
248 handlers::handle_trace(ctx, *count, *start, *stop)?
249 }
250 Commands::Console { lines, clear } => handlers::handle_console(ctx, *lines, *clear)?,
251 Commands::Errors { count, clear } => handlers::handle_errors(ctx, *count, *clear)?,
252
253 Commands::Resize { cols, rows } => handlers::handle_resize(ctx, *cols, *rows)?,
254 Commands::Attach {
255 session_id,
256 interactive,
257 } => handlers::handle_attach(ctx, session_id.clone(), *interactive)?,
258
259 Commands::Version => handlers::handle_version(ctx)?,
260 Commands::Env => handlers::handle_env(ctx)?,
261 Commands::Assert { condition } => handlers::handle_assert(ctx, condition.clone())?,
262 Commands::Cleanup { all } => handlers::handle_cleanup(ctx, *all)?,
263 Commands::Find { params } => handlers::handle_find(ctx, params.clone())?,
264
265 Commands::Debug(debug_cmd) => match debug_cmd {
266 DebugCommand::Record(action) => match action {
267 RecordAction::Start => handlers::handle_record_start(ctx)?,
268 RecordAction::Stop { output, format } => {
269 handlers::handle_record_stop(ctx, output.clone(), *format)?
270 }
271 RecordAction::Status => handlers::handle_record_status(ctx)?,
272 },
273 DebugCommand::Trace { count, start, stop } => {
274 handlers::handle_trace(ctx, *count, *start, *stop)?
275 }
276 DebugCommand::Console { lines, clear } => {
277 handlers::handle_console(ctx, *lines, *clear)?
278 }
279 DebugCommand::Errors { count, clear } => {
280 handlers::handle_errors(ctx, *count, *clear)?
281 }
282 DebugCommand::Env => handlers::handle_env(ctx)?,
283 },
284 }
285 Ok(())
286 }
287
288 fn handle_error(&self, e: Box<dyn std::error::Error>) -> i32 {
289 if let Some(client_error) = e.downcast_ref::<ClientError>() {
290 eprintln!("{} {}", Colors::error("Error:"), client_error);
291 if let Some(suggestion) = client_error.suggestion() {
292 eprintln!("{} {}", Colors::dim("Suggestion:"), suggestion);
293 }
294 if client_error.is_retryable() {
295 eprintln!(
296 "{}",
297 Colors::dim("(This error may be transient - retry may succeed)")
298 );
299 }
300 exit_code_for_client_error(client_error)
301 } else if let Some(attach_error) = e.downcast_ref::<AttachError>() {
302 eprintln!("{} {}", Colors::error("Error:"), attach_error);
303 eprintln!(
304 "{} {}",
305 Colors::dim("Suggestion:"),
306 attach_error.suggestion()
307 );
308 if attach_error.is_retryable() {
309 eprintln!(
310 "{}",
311 Colors::dim("(This error may be transient - retry may succeed)")
312 );
313 }
314 attach_error.exit_code()
315 } else if let Some(daemon_error) = e.downcast_ref::<DaemonError>() {
316 eprintln!("{} {}", Colors::error("Error:"), daemon_error);
317 eprintln!(
318 "{} {}",
319 Colors::dim("Suggestion:"),
320 daemon_error.suggestion()
321 );
322 if daemon_error.is_retryable() {
323 eprintln!(
324 "{}",
325 Colors::dim("(This error may be transient - retry may succeed)")
326 );
327 }
328 exit_codes::IOERR
329 } else {
330 eprintln!("{} {}", Colors::error("Error:"), e);
331 exit_codes::GENERAL_ERROR
332 }
333 }
334}
335
336impl Default for Application {
337 fn default() -> Self {
338 Self::new()
339 }
340}
341
342fn check_version_mismatch<C: DaemonClient>(client: &mut C) {
344 use crate::ipc::version::{VersionCheckResult, check_version};
345
346 match check_version(client, env!("CARGO_PKG_VERSION")) {
347 VersionCheckResult::Match => {}
348 VersionCheckResult::Mismatch(mismatch) => {
349 eprintln!(
350 "{} CLI version ({}) differs from daemon version ({})",
351 Colors::warning("Warning:"),
352 mismatch.cli_version,
353 mismatch.daemon_version
354 );
355 eprintln!(
356 "{} Run '{}' to update the daemon.",
357 Colors::dim("Hint:"),
358 Colors::info("agent-tui daemon restart")
359 );
360 eprintln!();
361 }
362 VersionCheckResult::CheckFailed(err) => {
363 eprintln!(
364 "{} Could not check daemon version: {}",
365 Colors::dim("Note:"),
366 err
367 );
368 }
369 }
370}
371
372fn exit_code_for_client_error(error: &ClientError) -> i32 {
373 use crate::ipc::error_codes::ErrorCategory;
374
375 match error.category() {
376 Some(ErrorCategory::InvalidInput) => exit_codes::USAGE,
377 Some(ErrorCategory::NotFound) => exit_codes::UNAVAILABLE,
378 Some(ErrorCategory::Busy) => exit_codes::CANTCREAT,
379 Some(ErrorCategory::External) => exit_codes::IOERR,
380 Some(ErrorCategory::Internal) => exit_codes::IOERR,
381 Some(ErrorCategory::Timeout) => exit_codes::TEMPFAIL,
382 None => exit_codes::GENERAL_ERROR,
383 }
384}