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