Skip to main content

agent_first_mail/runner/
dispatch.rs

1use super::lock::{command_lock, workspace_progress_command};
2use super::purge::{resolve_purge_action, ResolvedPurgeAction};
3use super::push::push_confirmed;
4use crate::cli::{
5    ArchiveAction, ArchiveCaseAction, ArchiveCaseNotesAction, ArchiveListAction,
6    ArchiveMessageCommand, ArchiveMessageNotesAction, CaseCommand, CaseDraftAction,
7    CaseNotesAction, Command, ConfigAction, DoctorAction, LogAction, MessageAction,
8    MessageAttachmentAction, PushAction, RemoteAction, RenderAction, TriageAction,
9};
10use crate::error::{AppError, Result};
11use crate::progress::{ProgressCallback, WorkspaceProgressSink};
12use crate::store::Workspace;
13use crate::workspace_lock::{LockMode, WorkspaceLock};
14use serde_json::{json, Value};
15use std::path::Path;
16
17pub fn execute_command(command: Command) -> Result<Value> {
18    execute_command_with_progress(command, None)
19}
20
21pub(super) fn execute_command_with_progress(
22    command: Command,
23    progress: Option<&mut ProgressCallback<'_>>,
24) -> Result<Value> {
25    match command {
26        Command::Skill { action } => crate::skill_admin::handle_action(action),
27        Command::Status => {
28            let cwd = std::env::current_dir().map_err(|e| AppError::io("current dir", &e))?;
29            execute_status(&cwd)
30        }
31        command => {
32            let cwd = std::env::current_dir().map_err(|e| AppError::io("current dir", &e))?;
33            let lock = command_lock(&command, &cwd)?;
34            let _lock = WorkspaceLock::acquire(&lock.root, lock.mode)?;
35            let mut workspace_progress = workspace_progress_command(&command)
36                .map(|name| WorkspaceProgressSink::start(&lock.root, name));
37            if lock.mode == LockMode::Exclusive
38                && !matches!(command, Command::Init | Command::Doctor { .. })
39            {
40                if let Err(err) = Workspace::at(&lock.root).ensure_no_incomplete_transactions() {
41                    if let Some(sink) = &mut workspace_progress {
42                        sink.finish_failure(&err);
43                    }
44                    return Err(err);
45                }
46            }
47            let mut progress = progress;
48            let should_emit_progress = progress.is_some() || workspace_progress.is_some();
49            let result = {
50                let mut emit_progress = |phase: &str, fields: Value| {
51                    if let Some(callback) = progress.as_deref_mut() {
52                        callback(phase, fields.clone());
53                    }
54                    if let Some(sink) = &mut workspace_progress {
55                        sink.update(phase, fields);
56                    }
57                };
58                let progress = if should_emit_progress {
59                    Some(&mut emit_progress as &mut ProgressCallback<'_>)
60                } else {
61                    None
62                };
63                execute_command_unlocked(command, &cwd, progress)
64            };
65            if let Some(sink) = &mut workspace_progress {
66                match &result {
67                    Ok(value) => sink.finish_success(value),
68                    Err(err) => sink.finish_failure(err),
69                }
70            }
71            result
72        }
73    }
74}
75
76fn execute_command_unlocked(
77    command: Command,
78    cwd: &Path,
79    progress: Option<&mut ProgressCallback<'_>>,
80) -> Result<Value> {
81    match command {
82        Command::Init => Workspace::at(cwd).init(),
83        Command::Pull { ids } => Workspace::discover(cwd)?.pull_with_progress(&ids, progress),
84        Command::Config { action } => {
85            let ws = Workspace::discover(cwd)?;
86            match action {
87                ConfigAction::Show => ws.config_show(),
88                ConfigAction::Get { key } => ws.config_get(&key),
89                ConfigAction::Set { key, values } => ws.config_set(&key, &values),
90            }
91        }
92        Command::Remote { action } => {
93            let ws = Workspace::discover(cwd)?;
94            match action {
95                RemoteAction::Test => ws.remote_test(),
96                RemoteAction::Folders => ws.remote_folders(),
97            }
98        }
99        Command::Push {
100            dry_run,
101            confirm,
102            action,
103        } => {
104            let ws = Workspace::discover(cwd)?;
105            if dry_run && confirm {
106                return Err(AppError::new(
107                    "invalid_request",
108                    "--dry-run cannot be used with --confirm",
109                ));
110            }
111            match action {
112                Some(PushAction::List) => ws.push_list(),
113                Some(PushAction::Drafts {
114                    dry_run: child_dry_run,
115                    confirm: child_confirm,
116                }) => {
117                    let confirmed = push_confirmed(dry_run, confirm, child_dry_run, child_confirm)?;
118                    ws.push_with_progress(crate::push_queue::PushMode::Drafts, confirmed, progress)
119                }
120                Some(PushAction::DraftsSend {
121                    dry_run: child_dry_run,
122                    confirm: child_confirm,
123                }) => {
124                    let confirmed = push_confirmed(dry_run, confirm, child_dry_run, child_confirm)?;
125                    ws.push_with_progress(
126                        crate::push_queue::PushMode::DraftsSend,
127                        confirmed,
128                        progress,
129                    )
130                }
131                Some(PushAction::Archive {
132                    dry_run: child_dry_run,
133                    confirm: child_confirm,
134                }) => {
135                    let confirmed = push_confirmed(dry_run, confirm, child_dry_run, child_confirm)?;
136                    ws.push_with_progress(crate::push_queue::PushMode::Archive, confirmed, progress)
137                }
138                Some(PushAction::Spam {
139                    dry_run: child_dry_run,
140                    confirm: child_confirm,
141                }) => {
142                    let confirmed = push_confirmed(dry_run, confirm, child_dry_run, child_confirm)?;
143                    ws.push_with_progress(crate::push_queue::PushMode::Spam, confirmed, progress)
144                }
145                Some(PushAction::Trash {
146                    dry_run: child_dry_run,
147                    confirm: child_confirm,
148                }) => {
149                    let confirmed = push_confirmed(dry_run, confirm, child_dry_run, child_confirm)?;
150                    ws.push_with_progress(crate::push_queue::PushMode::Trash, confirmed, progress)
151                }
152                None => {
153                    let confirmed = push_confirmed(dry_run, confirm, false, false)?;
154                    ws.push_with_progress(crate::push_queue::PushMode::All, confirmed, progress)
155                }
156            }
157        }
158        Command::Status => execute_status(cwd),
159        Command::Doctor { action } => {
160            let ws = Workspace::discover(cwd)?;
161            match action {
162                None => ws.doctor(),
163                Some(DoctorAction::Repair { confirm }) => ws.doctor_repair(confirm),
164            }
165        }
166        Command::Purge {
167            action,
168            older_than_days,
169        } => {
170            let ws = Workspace::discover(cwd)?;
171            match resolve_purge_action(action, older_than_days)? {
172                ResolvedPurgeAction::Spam { older_than_days } => ws.purge_spam(older_than_days),
173                ResolvedPurgeAction::Trash { older_than_days } => ws.purge_trash(older_than_days),
174                ResolvedPurgeAction::Deleted { older_than_days } => {
175                    ws.purge_deleted(older_than_days)
176                }
177                ResolvedPurgeAction::Discards { older_than_days } => {
178                    ws.purge_discards(older_than_days)
179                }
180            }
181        }
182        Command::Skill { action } => crate::skill_admin::handle_action(action),
183        Command::Triage { action } => {
184            let ws = Workspace::discover(cwd)?;
185            match action {
186                TriageAction::List => ws.triage_list(),
187            }
188        }
189        Command::Message { action } => {
190            let ws = Workspace::discover(cwd)?;
191            match action {
192                MessageAction::Show { message_id } => ws.message_show(&message_id),
193                MessageAction::Archive {
194                    message_id,
195                    archive_ref,
196                    summary,
197                    reason,
198                } => ws.archive_message(
199                    &message_id,
200                    &archive_ref,
201                    Some(summary.as_str()),
202                    reason.as_deref(),
203                ),
204                MessageAction::Spam { message_id, reason } => {
205                    ws.spam_message(&message_id, reason.as_deref())
206                }
207                MessageAction::Unspam { message_id, reason } => {
208                    ws.unspam_message(&message_id, reason.as_deref())
209                }
210                MessageAction::Trash { message_id, reason } => {
211                    ws.trash_message(&message_id, reason.as_deref())
212                }
213                MessageAction::Untrash { message_id, reason } => {
214                    ws.untrash_message(&message_id, reason.as_deref())
215                }
216                MessageAction::Unarchive { message_id, reason } => {
217                    ws.unarchive_message(&message_id, reason.as_deref())
218                }
219                MessageAction::Attachment { action } => match action {
220                    MessageAttachmentAction::Fetch {
221                        message_id,
222                        part_id,
223                    } => ws.fetch_message_attachment(&message_id, part_id.as_deref()),
224                },
225            }
226        }
227        Command::Case { action } => {
228            let ws = Workspace::discover(cwd)?;
229            match action {
230                CaseCommand::Create(args) => ws.create_case(
231                    &args.name,
232                    args.group.as_deref(),
233                    args.message.as_deref(),
234                    args.reason.as_deref(),
235                ),
236                CaseCommand::List => ws.case_list(),
237                CaseCommand::Show { case_ref } => ws.active_case_show(&case_ref),
238                CaseCommand::Add {
239                    case_ref,
240                    message_id,
241                    reason,
242                } => ws.add_message_to_case(&case_ref, &message_id, reason.as_deref()),
243                CaseCommand::Move { case_ref, group } => ws.move_case(&case_ref, &group),
244                CaseCommand::Rename {
245                    case_ref,
246                    name,
247                    reason,
248                } => ws.rename_active_case(&case_ref, &name, reason.as_deref()),
249                CaseCommand::Notes { action } => match action {
250                    CaseNotesAction::Show { case_ref } => ws.active_case_notes_show(&case_ref),
251                    CaseNotesAction::Append { case_ref, text } => {
252                        ws.active_case_notes_append(&case_ref, &text)
253                    }
254                    CaseNotesAction::Replace { case_ref, text } => {
255                        ws.active_case_notes_replace(&case_ref, &text)
256                    }
257                },
258                CaseCommand::Archive { case_ref, reason } => {
259                    ws.archive_case(&case_ref, reason.as_deref())
260                }
261                CaseCommand::Reopen { case_ref, reason } => {
262                    ws.reopen_case(&case_ref, reason.as_deref())
263                }
264                CaseCommand::Tag {
265                    case_ref,
266                    tag,
267                    reason,
268                } => ws.tag_case(&case_ref, &tag, reason.as_deref()),
269                CaseCommand::Untag {
270                    case_ref,
271                    tag,
272                    reason,
273                } => ws.untag_case(&case_ref, &tag, reason.as_deref()),
274                CaseCommand::Draft { action } => match action {
275                    CaseDraftAction::New {
276                        case_ref,
277                        to,
278                        cc,
279                        subject,
280                    } => ws.create_draft(&case_ref, &to, &cc, subject.as_deref()),
281                    CaseDraftAction::Validate {
282                        case_ref,
283                        draft_name,
284                    } => ws.validate_draft(&case_ref, &draft_name),
285                    CaseDraftAction::Attach {
286                        case_ref,
287                        draft_name,
288                        path,
289                    } => ws.attach_file_to_draft(&case_ref, &draft_name, &path),
290                    CaseDraftAction::Remove {
291                        case_ref,
292                        draft_name,
293                        reason,
294                    } => ws.remove_draft(&case_ref, &draft_name, reason.as_deref()),
295                },
296                CaseCommand::Compose {
297                    case_ref,
298                    draft_name,
299                } => ws.compose_draft(&case_ref, &draft_name),
300                CaseCommand::Reply {
301                    case_ref,
302                    message_id,
303                    all,
304                } => ws.reply_to_message(&case_ref, &message_id, all),
305                CaseCommand::Merge {
306                    case_ref,
307                    other_case_ref,
308                    reason,
309                } => ws.merge_case(&case_ref, &other_case_ref, reason.as_deref()),
310            }
311        }
312        Command::Archive { action } => {
313            let ws = Workspace::discover(cwd)?;
314            match action {
315                ArchiveAction::List { target: None } => ws.archive_list(),
316                ArchiveAction::List {
317                    target: Some(ArchiveListAction::Cases),
318                } => ws.archive_list_cases(),
319                ArchiveAction::List {
320                    target: Some(ArchiveListAction::Messages),
321                } => ws.archive_list_messages(),
322                ArchiveAction::Message { action } => match action {
323                    ArchiveMessageCommand::Create(args) => ws.create_archive_message_category(
324                        &args.name,
325                        args.message.as_deref(),
326                        args.summary.as_deref(),
327                        args.reason.as_deref(),
328                    ),
329                    ArchiveMessageCommand::Show { archive_ref } => {
330                        ws.archive_message_show(&archive_ref)
331                    }
332                    ArchiveMessageCommand::Restore {
333                        archive_ref,
334                        message_id,
335                        reason,
336                    } => ws.archive_message_restore(&archive_ref, &message_id, reason.as_deref()),
337                    ArchiveMessageCommand::Move {
338                        archive_ref,
339                        message_id,
340                        new_archive_ref,
341                        reason,
342                    } => ws.archive_message_move(
343                        &archive_ref,
344                        &message_id,
345                        &new_archive_ref,
346                        reason.as_deref(),
347                    ),
348                    ArchiveMessageCommand::Rename {
349                        archive_ref,
350                        name,
351                        reason,
352                    } => ws.archive_message_rename(&archive_ref, &name, reason.as_deref()),
353                    ArchiveMessageCommand::SetSummary {
354                        archive_ref,
355                        message_id,
356                        summary,
357                        reason,
358                    } => ws.archive_message_set_summary(
359                        &archive_ref,
360                        &message_id,
361                        &summary,
362                        reason.as_deref(),
363                    ),
364                    ArchiveMessageCommand::Notes { action } => match action {
365                        ArchiveMessageNotesAction::Show { archive_ref } => {
366                            ws.archive_message_notes_show(&archive_ref)
367                        }
368                        ArchiveMessageNotesAction::Append { archive_ref, text } => {
369                            ws.archive_message_notes_append(&archive_ref, &text)
370                        }
371                        ArchiveMessageNotesAction::Replace { archive_ref, text } => {
372                            ws.archive_message_notes_replace(&archive_ref, &text)
373                        }
374                    },
375                },
376                ArchiveAction::Case { action } => match action {
377                    ArchiveCaseAction::Show { case_ref } => ws.archive_case_show(&case_ref),
378                    ArchiveCaseAction::Restore {
379                        case_ref,
380                        group,
381                        reason,
382                    } => ws.archive_case_restore(&case_ref, &group, reason.as_deref()),
383                    ArchiveCaseAction::Rename {
384                        case_ref,
385                        name,
386                        reason,
387                    } => ws.archive_case_rename(&case_ref, &name, reason.as_deref()),
388                    ArchiveCaseAction::Notes { action } => match action {
389                        ArchiveCaseNotesAction::Show { case_ref } => {
390                            ws.archive_case_notes_show(&case_ref)
391                        }
392                        ArchiveCaseNotesAction::Append { case_ref, text } => {
393                            ws.archive_case_notes_append(&case_ref, &text)
394                        }
395                        ArchiveCaseNotesAction::Replace { case_ref, text } => {
396                            ws.archive_case_notes_replace(&case_ref, &text)
397                        }
398                    },
399                },
400            }
401        }
402        Command::Render { action } => {
403            let ws = Workspace::discover(cwd)?;
404            match action {
405                RenderAction::Refresh => ws.render_refresh(),
406                RenderAction::Templates { force } => ws.render_templates(force),
407            }
408        }
409        Command::Log { action } => {
410            let ws = Workspace::discover(cwd)?;
411            match action {
412                LogAction::List { limit } => ws.log_list(limit),
413                LogAction::Tail => ws.log_tail(),
414                LogAction::Message { message_id } => ws.log_message(&message_id),
415                LogAction::Case { case_uid } => ws.log_case(&case_uid),
416                LogAction::Archive { archive_uid } => ws.log_archive(&archive_uid),
417            }
418        }
419    }
420}
421
422fn execute_status(cwd: &Path) -> Result<Value> {
423    let ws = Workspace::discover(cwd)?;
424    let progress = crate::progress::workspace_status_progress(ws.root())?;
425    let Some(_lock) = WorkspaceLock::try_acquire(ws.root(), LockMode::Shared)? else {
426        return Ok(json!({
427            "code": "status",
428            "workspace_locked": true,
429            "progress": progress,
430            "hint": "Workspace counts are omitted while another afmail command is using this workspace; retry after it finishes for full counts."
431        }));
432    };
433    let mut status = ws.status()?;
434    if let Value::Object(map) = &mut status {
435        map.insert("progress".to_string(), progress);
436    }
437    Ok(status)
438}