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}