1mod attachments;
2mod auth;
3mod boards;
4mod cards;
5mod checklist_items;
6mod checklists;
7mod comments;
8mod custom_field_items;
9mod custom_fields;
10mod dependencies;
11mod formatting;
12mod imports;
13mod integrations;
14mod lists;
15mod profiles;
16mod raw;
17mod rules;
18mod self_update;
19mod skills;
20mod swimlanes;
21mod users;
22
23use auth::{render_auth_status, render_auth_success, render_logout};
24use boards::{
25 render_board_count, render_board_created, render_board_deleted, render_board_detail,
26 render_board_list, render_board_renamed,
27};
28use cards::{
29 render_card_archived, render_card_collection, render_card_created, render_card_deleted,
30 render_card_detail, render_card_restored, render_card_updated,
31};
32use checklist_items::{
33 render_checklist_item_created, render_checklist_item_deleted, render_checklist_item_detail,
34 render_checklist_item_updated,
35};
36use checklists::{
37 render_checklist_collection, render_checklist_created, render_checklist_deleted,
38 render_checklist_detail,
39};
40use comments::{
41 render_comment_collection, render_comment_created, render_comment_deleted,
42 render_comment_detail,
43};
44use lists::{
45 render_list_collection, render_list_created, render_list_deleted, render_list_detail,
46 render_list_updated,
47};
48use profiles::{render_profile_item, render_profile_list, render_profile_removed};
49use swimlanes::{
50 render_swimlane_collection, render_swimlane_created, render_swimlane_deleted,
51 render_swimlane_detail, render_swimlane_updated,
52};
53use users::{
54 render_user_boards, render_user_cards, render_user_created, render_user_deleted,
55 render_user_detail, render_user_list, render_user_login_change, render_user_ownership,
56};
57
58use std::{
59 ffi::OsString,
60 io::{Read, Write},
61 process::ExitCode,
62};
63
64use clap::ValueEnum;
65use serde::Serialize;
66
67use crate::command_result::CommandSuccess;
68use crate::{
69 error::{AppError, ErrorCode},
70 exit_code::StableExitCode,
71};
72
73pub(crate) use raw::render_human_response as render_api_response_human;
74#[cfg(test)]
75pub(crate) use raw::structured_api_response;
76pub(crate) use self_update::write_update_notice;
77
78#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
79#[value(rename_all = "lower")]
80pub enum OutputFormat {
81 #[default]
82 Human,
83 Json,
84 Raw,
85}
86
87impl OutputFormat {
88 pub fn detect_from_args(args: &[OsString]) -> Self {
89 let mut format = Self::Human;
90 let mut command = crate::command();
91 command.build();
92 let mut current = &command;
93 let mut iter = args.iter().skip(1).peekable();
94 while let Some(argument) = iter.next() {
95 let Some(arg) = argument.to_str() else {
96 continue;
97 };
98 if arg == "--" {
99 break;
100 }
101 if arg == "--output" {
102 format = match iter.next().and_then(|value| value.to_str()) {
103 Some("json") => Self::Json,
104 Some("raw") => Self::Raw,
105 _ => Self::Human,
106 };
107 } else if let Some(value) = arg.strip_prefix("--output=") {
108 format = match value {
109 "json" => Self::Json,
110 "raw" => Self::Raw,
111 _ => Self::Human,
112 };
113 } else if let Some(subcommand) = current.find_subcommand(arg) {
114 current = subcommand;
115 } else if let Some(option) = arg.strip_prefix("--").and_then(|name| {
116 current
117 .get_arguments()
118 .find(|option| option.get_long() == Some(name))
119 }) {
120 if option
123 .get_num_args()
124 .is_some_and(|arity| arity.takes_values())
125 && iter.peek().is_some_and(|value| {
126 option.is_allow_hyphen_values_set()
127 || !value.to_string_lossy().starts_with('-')
128 })
129 {
130 iter.next();
131 }
132 }
133 }
134 format
135 }
136}
137
138#[derive(Serialize)]
139struct SuccessEnvelope<'a, T> {
140 ok: bool,
141 data: &'a T,
142}
143
144#[derive(Serialize)]
145struct ErrorEnvelope<'a> {
146 ok: bool,
147 error: ErrorBody<'a>,
148}
149
150#[derive(Serialize)]
151struct ErrorBody<'a> {
152 code: &'static str,
153 message: &'a str,
154 details: &'a crate::error::ErrorDetails,
155}
156
157pub fn render_success(format: OutputFormat, success: &CommandSuccess) -> String {
158 match (format, success) {
159 (OutputFormat::Human, CommandSuccess::BotRunReported(data)) => {
160 formatting::render_object(data)
161 }
162 (OutputFormat::Json, CommandSuccess::BotRunReported(data)) => render_json_success(data),
163 (OutputFormat::Human, CommandSuccess::InboundEmailSubmitted(data)) => {
164 formatting::render_object(data)
165 }
166 (OutputFormat::Json, CommandSuccess::InboundEmailSubmitted(data)) => {
167 render_json_success(data)
168 }
169 (OutputFormat::Human, CommandSuccess::EmailBounceSubmitted(data)) => {
170 formatting::render_object(data)
171 }
172 (OutputFormat::Json, CommandSuccess::EmailBounceSubmitted(data)) => {
173 render_json_success(data)
174 }
175
176 (OutputFormat::Human, CommandSuccess::OauthProviderSettings(data)) => {
177 formatting::render_object(data)
178 }
179 (OutputFormat::Json, CommandSuccess::OauthProviderSettings(data)) => {
180 render_json_success(data)
181 }
182 (OutputFormat::Human, CommandSuccess::PasswordlessSettings(data)) => {
183 formatting::render_object(data)
184 }
185 (OutputFormat::Json, CommandSuccess::PasswordlessSettings(data)) => {
186 render_json_success(data)
187 }
188 (OutputFormat::Human, CommandSuccess::ProblemsOverview(data)) => {
189 formatting::render_object(data)
190 }
191 (OutputFormat::Json, CommandSuccess::ProblemsOverview(data)) => render_json_success(data),
192 (OutputFormat::Human, CommandSuccess::ProblemStream(data)) => {
193 formatting::render_object(data)
194 }
195 (OutputFormat::Json, CommandSuccess::ProblemStream(data)) => render_json_success(data),
196 (OutputFormat::Human, CommandSuccess::ProblemAcknowledged(data)) => {
197 formatting::render_object(data)
198 }
199 (OutputFormat::Json, CommandSuccess::ProblemAcknowledged(data)) => {
200 render_json_success(data)
201 }
202 (OutputFormat::Human, CommandSuccess::AttachmentSoftDeleted(data)) => {
203 formatting::render_object(data)
204 }
205 (OutputFormat::Json, CommandSuccess::AttachmentSoftDeleted(data)) => {
206 render_json_success(data)
207 }
208 (OutputFormat::Human, CommandSuccess::AttachmentRestored(data)) => {
209 formatting::render_object(data)
210 }
211 (OutputFormat::Json, CommandSuccess::AttachmentRestored(data)) => render_json_success(data),
212 (OutputFormat::Human, CommandSuccess::DeletedAttachments(data)) => {
213 formatting::render_object(data)
214 }
215 (OutputFormat::Json, CommandSuccess::DeletedAttachments(data)) => render_json_success(data),
216 (OutputFormat::Human, CommandSuccess::BoardViewSettings(data)) => {
217 formatting::render_object(data)
218 }
219 (OutputFormat::Json, CommandSuccess::BoardViewSettings(data)) => render_json_success(data),
220 (OutputFormat::Human, CommandSuccess::BoardCardFieldOrder(data)) => {
221 formatting::render_object(data)
222 }
223 (OutputFormat::Json, CommandSuccess::BoardCardFieldOrder(data)) => {
224 render_json_success(data)
225 }
226 (
227 OutputFormat::Human,
228 CommandSuccess::ListArchived(data) | CommandSuccess::ListRestored(data),
229 ) => formatting::render_object(data),
230 (
231 OutputFormat::Json,
232 CommandSuccess::ListArchived(data) | CommandSuccess::ListRestored(data),
233 ) => render_json_success(data),
234 (OutputFormat::Human, CommandSuccess::ChecklistUpdated(data)) => {
235 formatting::render_object(data)
236 }
237 (OutputFormat::Json, CommandSuccess::ChecklistUpdated(data)) => render_json_success(data),
238 (OutputFormat::Human, CommandSuccess::CommentUpdated(data)) => {
239 formatting::render_object(data)
240 }
241 (OutputFormat::Json, CommandSuccess::CommentUpdated(data)) => render_json_success(data),
242 (OutputFormat::Human, CommandSuccess::SelfUpdate(data)) => self_update::render(data),
243 (OutputFormat::Json, CommandSuccess::SelfUpdate(data)) => render_json_success(data),
244 (OutputFormat::Human, CommandSuccess::SkillList(data)) => skills::list(data),
245 (OutputFormat::Json, CommandSuccess::SkillList(data)) => render_json_success(data),
246 (OutputFormat::Human, CommandSuccess::SkillShown(data)) => {
247 data.content.trim_end_matches(['\r', '\n']).to_owned()
248 }
249 (OutputFormat::Json, CommandSuccess::SkillShown(data)) => render_json_success(data),
250 (OutputFormat::Human, CommandSuccess::AuthPreflight(data)) => {
251 formatting::render_object(data)
252 }
253 (OutputFormat::Json, CommandSuccess::AuthPreflight(data)) => render_json_success(data),
254 (OutputFormat::Human, CommandSuccess::SchemaUpgradeStatus(data)) => {
255 formatting::render_object(data)
256 }
257 (OutputFormat::Json, CommandSuccess::SchemaUpgradeStatus(data)) => {
258 render_json_success(data)
259 }
260 (OutputFormat::Human, CommandSuccess::BoardAttachments(data)) => {
261 formatting::render_object(data)
262 }
263 (OutputFormat::Json, CommandSuccess::BoardAttachments(data)) => render_json_success(data),
264 (OutputFormat::Human, CommandSuccess::BoardImported(data)) => imports::board(data),
265 (OutputFormat::Json, CommandSuccess::BoardImported(data)) => render_json_success(data),
266 (OutputFormat::Human, CommandSuccess::TrelloImported(data)) => imports::trello(data),
267 (OutputFormat::Json, CommandSuccess::TrelloImported(data)) => render_json_success(data),
268 (OutputFormat::Human, CommandSuccess::ZipImported(data)) => imports::zip(data),
269 (OutputFormat::Json, CommandSuccess::ZipImported(data)) => render_json_success(data),
270 (OutputFormat::Human, CommandSuccess::IcsImported(data)) => imports::ics(data),
271 (OutputFormat::Json, CommandSuccess::IcsImported(data)) => render_json_success(data),
272 (OutputFormat::Human, CommandSuccess::Exported(data)) => formatting::render_object(data),
273 (OutputFormat::Json, CommandSuccess::Exported(data)) => render_json_success(data),
274 (OutputFormat::Human, CommandSuccess::AdminDomains(data)) => {
275 formatting::render_object(data)
276 }
277 (OutputFormat::Json, CommandSuccess::AdminDomains(data)) => render_json_success(data),
278 (OutputFormat::Human, CommandSuccess::AdminOrganizations(data)) => {
279 formatting::render_object(data)
280 }
281 (OutputFormat::Json, CommandSuccess::AdminOrganizations(data)) => render_json_success(data),
282 (OutputFormat::Human, CommandSuccess::AdminTeams(data)) => formatting::render_object(data),
283 (OutputFormat::Json, CommandSuccess::AdminTeams(data)) => render_json_success(data),
284 (OutputFormat::Human, CommandSuccess::AdminOrganizationUpdated(data)) => {
285 formatting::render_object(data)
286 }
287 (OutputFormat::Json, CommandSuccess::AdminOrganizationUpdated(data)) => {
288 render_json_success(data)
289 }
290 (OutputFormat::Human, CommandSuccess::AdminTeamUpdated(data)) => {
291 formatting::render_object(data)
292 }
293 (OutputFormat::Json, CommandSuccess::AdminTeamUpdated(data)) => render_json_success(data),
294 (OutputFormat::Human, CommandSuccess::AdminOrganizationFeaturesUpdatedAll(data)) => {
295 formatting::render_object(data)
296 }
297 (OutputFormat::Json, CommandSuccess::AdminOrganizationFeaturesUpdatedAll(data)) => {
298 render_json_success(data)
299 }
300 (OutputFormat::Human, CommandSuccess::AdminTeamFeaturesUpdatedAll(data)) => {
301 formatting::render_object(data)
302 }
303 (OutputFormat::Json, CommandSuccess::AdminTeamFeaturesUpdatedAll(data)) => {
304 render_json_success(data)
305 }
306 (OutputFormat::Human, CommandSuccess::GlobalSettings(data)) => {
307 formatting::render_object(data)
308 }
309 (OutputFormat::Json, CommandSuccess::GlobalSettings(data)) => render_json_success(data),
310 (OutputFormat::Human, CommandSuccess::GlobalSettingsUpdated(data)) => {
311 formatting::render_object(data)
312 }
313 (OutputFormat::Json, CommandSuccess::GlobalSettingsUpdated(data)) => {
314 render_json_success(data)
315 }
316 (OutputFormat::Human, CommandSuccess::AttachmentSettings(data)) => {
317 formatting::render_object(data)
318 }
319 (OutputFormat::Json, CommandSuccess::AttachmentSettings(data)) => render_json_success(data),
320 (OutputFormat::Human, CommandSuccess::AttachmentSettingsUpdated(data)) => {
321 formatting::render_object(data)
322 }
323 (OutputFormat::Json, CommandSuccess::AttachmentSettingsUpdated(data)) => {
324 render_json_success(data)
325 }
326
327 (OutputFormat::Human, CommandSuccess::BoardCardSettings(data)) => {
328 formatting::render_object(data)
329 }
330 (OutputFormat::Json, CommandSuccess::BoardCardSettings(data)) => render_json_success(data),
331 (OutputFormat::Human, CommandSuccess::BoardCardSettingsUpdated(data)) => {
332 formatting::render_object(data)
333 }
334 (OutputFormat::Json, CommandSuccess::BoardCardSettingsUpdated(data)) => {
335 render_json_success(data)
336 }
337 (OutputFormat::Human, CommandSuccess::BoardDomains(data)) => {
338 formatting::render_object(data)
339 }
340 (OutputFormat::Json, CommandSuccess::BoardDomains(data)) => render_json_success(data),
341 (OutputFormat::Human, CommandSuccess::BoardDomainChanged(data)) => format!(
342 "Board domains updated:\n{}",
343 formatting::render_object(data)
344 ),
345 (OutputFormat::Json, CommandSuccess::BoardDomainChanged(data)) => render_json_success(data),
346 (OutputFormat::Human, CommandSuccess::RuleCollection(data)) => rules::collection(data),
347 (OutputFormat::Json, CommandSuccess::RuleCollection(data)) => render_json_success(data),
348 (OutputFormat::Human, CommandSuccess::RuleShown(data)) => rules::detail(data),
349 (OutputFormat::Json, CommandSuccess::RuleShown(data)) => render_json_success(data),
350 (OutputFormat::Human, CommandSuccess::RuleCreated(data)) => rules::created(data),
351 (OutputFormat::Json, CommandSuccess::RuleCreated(data)) => render_json_success(data),
352 (OutputFormat::Human, CommandSuccess::RuleUpdated(data)) => rules::updated(data),
353 (OutputFormat::Json, CommandSuccess::RuleUpdated(data)) => render_json_success(data),
354 (OutputFormat::Human, CommandSuccess::RuleDeleted(data)) => rules::deleted(data),
355 (OutputFormat::Json, CommandSuccess::RuleDeleted(data)) => render_json_success(data),
356 (OutputFormat::Human, CommandSuccess::IntegrationCollection(data)) => {
357 integrations::collection(data)
358 }
359 (OutputFormat::Json, CommandSuccess::IntegrationCollection(data)) => {
360 render_json_success(data)
361 }
362 (OutputFormat::Human, CommandSuccess::IntegrationShown(data)) => integrations::detail(data),
363 (OutputFormat::Json, CommandSuccess::IntegrationShown(data)) => render_json_success(data),
364 (OutputFormat::Human, CommandSuccess::IntegrationCreated(data)) => {
365 integrations::created(data)
366 }
367 (OutputFormat::Json, CommandSuccess::IntegrationCreated(data)) => render_json_success(data),
368 (OutputFormat::Human, CommandSuccess::IntegrationUpdated(data)) => {
369 integrations::updated(data)
370 }
371 (OutputFormat::Json, CommandSuccess::IntegrationUpdated(data)) => render_json_success(data),
372 (OutputFormat::Human, CommandSuccess::IntegrationDeleted(data)) => {
373 integrations::deleted(data)
374 }
375 (OutputFormat::Json, CommandSuccess::IntegrationDeleted(data)) => render_json_success(data),
376 (OutputFormat::Human, CommandSuccess::IntegrationActivitiesChanged(data)) => {
377 integrations::activities(data)
378 }
379 (OutputFormat::Json, CommandSuccess::IntegrationActivitiesChanged(data)) => {
380 render_json_success(data)
381 }
382 (OutputFormat::Human, CommandSuccess::BoardCopied(data)) => {
383 format!("Board copied:\n{}", formatting::render_object(data))
384 }
385 (OutputFormat::Json, CommandSuccess::BoardCopied(data)) => render_json_success(data),
386 (OutputFormat::Human, CommandSuccess::CardCopied(data)) => {
387 format!("Card copied:\n{}", formatting::render_object(data))
388 }
389 (OutputFormat::Json, CommandSuccess::CardCopied(data)) => render_json_success(data),
390 (OutputFormat::Human, CommandSuccess::ListCopied(data)) => {
391 format!("List copied:\n{}", formatting::render_object(data))
392 }
393 (OutputFormat::Human, CommandSuccess::ListMoved(data)) => {
394 format!(
395 "{}:\n{}",
396 if data.source_board_id == data.target_board_id {
397 "List moved"
398 } else {
399 "Cross-board list move request accepted (source list retained; position ignored)"
400 },
401 formatting::render_object(data)
402 )
403 }
404 (
405 OutputFormat::Json,
406 CommandSuccess::ListCopied(data) | CommandSuccess::ListMoved(data),
407 ) => render_json_success(data),
408 (OutputFormat::Human, CommandSuccess::SwimlaneCopied(data)) => {
409 format!("Swimlane copied:\n{}", formatting::render_object(data))
410 }
411 (OutputFormat::Human, CommandSuccess::SwimlaneMoved(data)) => {
412 format!("Swimlane moved:\n{}", formatting::render_object(data))
413 }
414 (
415 OutputFormat::Json,
416 CommandSuccess::SwimlaneCopied(data) | CommandSuccess::SwimlaneMoved(data),
417 ) => render_json_success(data),
418 (OutputFormat::Human, CommandSuccess::CardCount(data)) => cards::render_card_count(data),
419 (OutputFormat::Json, CommandSuccess::CardCount(data)) => render_json_success(data),
420 (OutputFormat::Human, CommandSuccess::SwimlaneCards(data)) => {
421 cards::render_swimlane_cards(data)
422 }
423 (OutputFormat::Json, CommandSuccess::SwimlaneCards(data)) => render_json_success(data),
424 (OutputFormat::Human, CommandSuccess::CardBatch(data)) => cards::render_batch(data),
425 (OutputFormat::Json, CommandSuccess::CardBatch(data)) => match data {
426 crate::command_result::CardBatchResult::CardBulkCreate(data) => {
427 render_json_success(data)
428 }
429 crate::command_result::CardBatchResult::CardBulkDelete(data) => {
430 render_json_success(data)
431 }
432 crate::command_result::CardBatchResult::CardBulkLabels(data) => {
433 render_json_success(data)
434 }
435 },
436 (OutputFormat::Human, CommandSuccess::BoardLabels(data)) => formatting::render_object(data),
437 (OutputFormat::Json, CommandSuccess::BoardLabels(data)) => render_json_success(data),
438 (OutputFormat::Human, CommandSuccess::BoardLabelAdded(data)) => {
439 boards::render_label_added(data)
440 }
441 (OutputFormat::Json, CommandSuccess::BoardLabelAdded(data)) => render_json_success(data),
442 (OutputFormat::Human, CommandSuccess::CardLabels(data)) => formatting::render_object(data),
443 (OutputFormat::Json, CommandSuccess::CardLabels(data)) => render_json_success(data),
444 (OutputFormat::Human, CommandSuccess::BoardMembers(data)) => {
445 formatting::render_object(data)
446 }
447 (OutputFormat::Json, CommandSuccess::BoardMembers(data)) => render_json_success(data),
448 (OutputFormat::Human, CommandSuccess::BoardMemberChanged(data)) => {
449 let action = match data.action {
450 crate::command_result::MembershipAction::Add => "Addition",
451 crate::command_result::MembershipAction::Remove => "Removal",
452 };
453 format!(
454 "Board member {action} request accepted:\n{}",
455 formatting::render_object(data)
456 )
457 }
458 (OutputFormat::Json, CommandSuccess::BoardMemberChanged(data)) => render_json_success(data),
459 (OutputFormat::Human, CommandSuccess::BoardMemberUpdated(data)) => {
460 formatting::render_object(data)
461 }
462 (OutputFormat::Json, CommandSuccess::BoardMemberUpdated(data)) => render_json_success(data),
463 (OutputFormat::Human, CommandSuccess::CardMembers(data)) => formatting::render_object(data),
464 (OutputFormat::Json, CommandSuccess::CardMembers(data)) => render_json_success(data),
465 (OutputFormat::Human, CommandSuccess::CardAssignees(data)) => {
466 formatting::render_object(data)
467 }
468 (OutputFormat::Json, CommandSuccess::CardAssignees(data)) => render_json_success(data),
469 (OutputFormat::Human, CommandSuccess::CardMembershipChanged(data)) => {
470 formatting::render_object(data)
471 }
472 (OutputFormat::Json, CommandSuccess::CardMembershipChanged(data)) => {
473 render_json_success(data)
474 }
475 (OutputFormat::Human, CommandSuccess::CardDependencies(data)) => {
476 dependencies::card_collection(data)
477 }
478 (OutputFormat::Json, CommandSuccess::CardDependencies(data)) => render_json_success(data),
479 (OutputFormat::Human, CommandSuccess::BoardDependencies(data)) => {
480 dependencies::board_collection(data)
481 }
482 (OutputFormat::Json, CommandSuccess::BoardDependencies(data)) => render_json_success(data),
483 (OutputFormat::Human, CommandSuccess::DependencyCreated(data)) => {
484 dependencies::created(data)
485 }
486 (OutputFormat::Json, CommandSuccess::DependencyCreated(data)) => render_json_success(data),
487 (OutputFormat::Human, CommandSuccess::DependencyUpdated(data)) => {
488 dependencies::updated(data)
489 }
490 (OutputFormat::Json, CommandSuccess::DependencyUpdated(data)) => render_json_success(data),
491 (OutputFormat::Human, CommandSuccess::DependencyDeleted(data)) => {
492 dependencies::deleted(data)
493 }
494 (OutputFormat::Json, CommandSuccess::DependencyDeleted(data)) => render_json_success(data),
495 (OutputFormat::Human, CommandSuccess::AttachmentList(data)) => attachments::list(data),
496 (OutputFormat::Json, CommandSuccess::AttachmentList(data)) => render_json_success(data),
497 (OutputFormat::Human, CommandSuccess::AttachmentShown(data)) => attachments::detail(data),
498 (OutputFormat::Json, CommandSuccess::AttachmentShown(data)) => render_json_success(data),
499 (OutputFormat::Human, CommandSuccess::AttachmentUploaded(data)) => {
500 format!(
501 "Attachment upload accepted; final availability is unconfirmed:\n{}",
502 attachments::detail(data)
503 )
504 }
505 (OutputFormat::Json, CommandSuccess::AttachmentUploaded(data)) => render_json_success(data),
506 (OutputFormat::Human, CommandSuccess::BackgroundUploaded(data)) => {
507 format!("Board background uploaded:\n{}", attachments::detail(data))
508 }
509 (OutputFormat::Json, CommandSuccess::BackgroundUploaded(data)) => render_json_success(data),
510 (OutputFormat::Human, CommandSuccess::AttachmentCopied(data)) => {
511 format!("Attachment copied:\n{}", attachments::detail(data))
512 }
513 (OutputFormat::Json, CommandSuccess::AttachmentCopied(data)) => render_json_success(data),
514 (OutputFormat::Human, CommandSuccess::AttachmentMoved(data)) => {
515 format!("Attachment moved:\n{}", attachments::detail(data))
516 }
517 (OutputFormat::Json, CommandSuccess::AttachmentMoved(data)) => render_json_success(data),
518 (OutputFormat::Human, CommandSuccess::AttachmentDeleted(data)) => {
519 format!("Attachment deleted:\n{}", attachments::detail(data))
520 }
521 (OutputFormat::Json, CommandSuccess::AttachmentDeleted(data)) => render_json_success(data),
522 (OutputFormat::Human, CommandSuccess::AttachmentDownloaded(data)) => {
523 attachments::detail(data)
524 }
525 (OutputFormat::Json, CommandSuccess::AttachmentDownloaded(data)) => {
526 render_json_success(data)
527 }
528 (_, CommandSuccess::ApiResponse(_)) => {
529 panic!("streaming API responses must be consumed by output::write_success")
530 }
531 (OutputFormat::Raw, CommandSuccess::Cancelled(_)) => String::new(),
532 (OutputFormat::Raw, _) => panic!("raw results must be consumed by output::write_success"),
533 (OutputFormat::Human, CommandSuccess::Cancelled(_)) => {
534 "Cancelled; no changes made.".to_owned()
535 }
536 (OutputFormat::Json, CommandSuccess::Cancelled(data)) => render_json_success(data),
537 (OutputFormat::Human, CommandSuccess::Registration(data)) => {
538 render_auth_success("Registered user", data)
539 }
540 (OutputFormat::Json, CommandSuccess::Registration(data)) => render_json_success(data),
541 (OutputFormat::Human, CommandSuccess::Login(data)) => {
542 render_auth_success("Logged in user", data)
543 }
544 (OutputFormat::Json, CommandSuccess::Login(data)) => render_json_success(data),
545 (OutputFormat::Human, CommandSuccess::Logout(data)) => render_logout(data),
546 (OutputFormat::Json, CommandSuccess::Logout(data)) => render_json_success(data),
547 (OutputFormat::Human, CommandSuccess::AuthStatus(data)) => render_auth_status(data),
548 (OutputFormat::Json, CommandSuccess::AuthStatus(data)) => render_json_success(data),
549 (OutputFormat::Human, CommandSuccess::UserCurrent(data))
550 | (OutputFormat::Human, CommandSuccess::UserShown(data))
551 | (OutputFormat::Human, CommandSuccess::UserUpdated(data)) => render_user_detail(data),
552 (OutputFormat::Json, CommandSuccess::UserCurrent(data))
553 | (OutputFormat::Json, CommandSuccess::UserShown(data))
554 | (OutputFormat::Json, CommandSuccess::UserUpdated(data)) => render_json_success(data),
555 (OutputFormat::Human, CommandSuccess::UserList(data)) => render_user_list(data),
556 (OutputFormat::Json, CommandSuccess::UserList(data)) => render_json_success(data),
557 (OutputFormat::Human, CommandSuccess::UserCards(data)) => render_user_cards(data),
558 (OutputFormat::Json, CommandSuccess::UserCards(data)) => render_json_success(data),
559 (OutputFormat::Human, CommandSuccess::UserCreated(data)) => render_user_created(data),
560 (OutputFormat::Json, CommandSuccess::UserCreated(data)) => render_json_success(data),
561 (OutputFormat::Human, CommandSuccess::UserBoards(data)) => render_user_boards(data),
562 (OutputFormat::Json, CommandSuccess::UserBoards(data)) => render_json_success(data),
563 (OutputFormat::Human, CommandSuccess::UserOwnershipTaken(data)) => {
564 render_user_ownership(data)
565 }
566 (OutputFormat::Json, CommandSuccess::UserOwnershipTaken(data)) => render_json_success(data),
567 (OutputFormat::Human, CommandSuccess::UserLoginChanged(data)) => {
568 render_user_login_change(data)
569 }
570 (OutputFormat::Json, CommandSuccess::UserLoginChanged(data)) => render_json_success(data),
571 (OutputFormat::Human, CommandSuccess::UserDeleted(data)) => render_user_deleted(data),
572 (OutputFormat::Json, CommandSuccess::UserDeleted(data)) => render_json_success(data),
573 (OutputFormat::Human, CommandSuccess::UserTokenCreated(data)) => {
574 users::render_token_created(data)
575 }
576 (OutputFormat::Json, CommandSuccess::UserTokenCreated(data)) => render_json_success(data),
577 (OutputFormat::Human, CommandSuccess::UserTokenDeleted(data)) => {
578 users::render_token_deleted(data)
579 }
580 (OutputFormat::Json, CommandSuccess::UserTokenDeleted(data)) => render_json_success(data),
581 (OutputFormat::Human, CommandSuccess::BoardList(data)) => render_board_list(data),
582 (OutputFormat::Json, CommandSuccess::BoardList(data)) => render_json_success(data),
583 (OutputFormat::Human, CommandSuccess::BoardCount(data)) => render_board_count(data),
584 (OutputFormat::Json, CommandSuccess::BoardCount(data)) => render_json_success(data),
585 (OutputFormat::Human, CommandSuccess::BoardShown(data)) => render_board_detail(data),
586 (OutputFormat::Json, CommandSuccess::BoardShown(data)) => render_json_success(data),
587 (OutputFormat::Human, CommandSuccess::BoardCreated(data)) => render_board_created(data),
588 (OutputFormat::Json, CommandSuccess::BoardCreated(data)) => render_json_success(data),
589 (OutputFormat::Human, CommandSuccess::BoardRenamed(data)) => render_board_renamed(data),
590 (OutputFormat::Json, CommandSuccess::BoardRenamed(data)) => render_json_success(data),
591 (OutputFormat::Human, CommandSuccess::BoardDeleted(data)) => render_board_deleted(data),
592 (OutputFormat::Json, CommandSuccess::BoardDeleted(data)) => render_json_success(data),
593 (OutputFormat::Human, CommandSuccess::ListCollection(data)) => render_list_collection(data),
594 (OutputFormat::Json, CommandSuccess::ListCollection(data)) => render_json_success(data),
595 (OutputFormat::Human, CommandSuccess::ListShown(data)) => render_list_detail(data),
596 (OutputFormat::Json, CommandSuccess::ListShown(data)) => render_json_success(data),
597 (OutputFormat::Human, CommandSuccess::ListCreated(data)) => render_list_created(data),
598 (OutputFormat::Json, CommandSuccess::ListCreated(data)) => render_json_success(data),
599 (OutputFormat::Human, CommandSuccess::ListUpdated(data)) => render_list_updated(data),
600 (OutputFormat::Json, CommandSuccess::ListUpdated(data)) => render_json_success(data),
601 (OutputFormat::Human, CommandSuccess::ListDeleted(data)) => render_list_deleted(data),
602 (OutputFormat::Json, CommandSuccess::ListDeleted(data)) => render_json_success(data),
603 (OutputFormat::Human, CommandSuccess::CardCollection(data)) => render_card_collection(data),
604 (OutputFormat::Json, CommandSuccess::CardCollection(data)) => render_json_success(data),
605 (OutputFormat::Human, CommandSuccess::CardShown(data)) => render_card_detail(data),
606 (OutputFormat::Json, CommandSuccess::CardShown(data)) => render_json_success(data),
607 (OutputFormat::Human, CommandSuccess::CardCreated(data)) => render_card_created(data),
608 (OutputFormat::Json, CommandSuccess::CardCreated(data)) => render_json_success(data),
609 (OutputFormat::Human, CommandSuccess::CardUpdated(data)) => render_card_updated(data),
610 (OutputFormat::Json, CommandSuccess::CardUpdated(data)) => render_json_success(data),
611 (OutputFormat::Human, CommandSuccess::CardArchived(data)) => render_card_archived(data),
612 (OutputFormat::Json, CommandSuccess::CardArchived(data)) => render_json_success(data),
613 (OutputFormat::Human, CommandSuccess::CardRestored(data)) => render_card_restored(data),
614 (OutputFormat::Json, CommandSuccess::CardRestored(data)) => render_json_success(data),
615 (OutputFormat::Human, CommandSuccess::CardDeleted(data)) => render_card_deleted(data),
616 (OutputFormat::Json, CommandSuccess::CardDeleted(data)) => render_json_success(data),
617 (OutputFormat::Human, CommandSuccess::ChecklistCollection(data)) => {
618 render_checklist_collection(data)
619 }
620 (OutputFormat::Json, CommandSuccess::ChecklistCollection(data)) => {
621 render_json_success(data)
622 }
623 (OutputFormat::Human, CommandSuccess::ChecklistShown(data)) => {
624 render_checklist_detail(data)
625 }
626 (OutputFormat::Json, CommandSuccess::ChecklistShown(data)) => render_json_success(data),
627 (OutputFormat::Human, CommandSuccess::ChecklistCreated(data)) => {
628 render_checklist_created(data)
629 }
630 (OutputFormat::Json, CommandSuccess::ChecklistCreated(data)) => render_json_success(data),
631 (OutputFormat::Human, CommandSuccess::ChecklistDeleted(data)) => {
632 render_checklist_deleted(data)
633 }
634 (OutputFormat::Json, CommandSuccess::ChecklistDeleted(data)) => render_json_success(data),
635 (OutputFormat::Human, CommandSuccess::ChecklistItemShown(data)) => {
636 render_checklist_item_detail(data)
637 }
638 (OutputFormat::Json, CommandSuccess::ChecklistItemShown(data)) => render_json_success(data),
639 (OutputFormat::Human, CommandSuccess::ChecklistItemCreated(data)) => {
640 render_checklist_item_created(data)
641 }
642 (OutputFormat::Json, CommandSuccess::ChecklistItemCreated(data)) => {
643 render_json_success(data)
644 }
645 (OutputFormat::Human, CommandSuccess::ChecklistItemUpdated(data)) => {
646 render_checklist_item_updated(data)
647 }
648 (OutputFormat::Json, CommandSuccess::ChecklistItemUpdated(data)) => {
649 render_json_success(data)
650 }
651 (OutputFormat::Human, CommandSuccess::ChecklistItemDeleted(data)) => {
652 render_checklist_item_deleted(data)
653 }
654 (OutputFormat::Json, CommandSuccess::ChecklistItemDeleted(data)) => {
655 render_json_success(data)
656 }
657 (OutputFormat::Human, CommandSuccess::CustomFieldCollection(data)) => {
658 custom_fields::collection(data)
659 }
660 (OutputFormat::Json, CommandSuccess::CustomFieldCollection(data)) => {
661 render_json_success(data)
662 }
663 (OutputFormat::Human, CommandSuccess::CustomFieldShown(data)) => {
664 custom_fields::detail(data)
665 }
666 (OutputFormat::Json, CommandSuccess::CustomFieldShown(data)) => render_json_success(data),
667 (OutputFormat::Human, CommandSuccess::CustomFieldCreated(data)) => {
668 custom_fields::created(data)
669 }
670 (OutputFormat::Json, CommandSuccess::CustomFieldCreated(data)) => render_json_success(data),
671 (OutputFormat::Human, CommandSuccess::CustomFieldUpdated(data)) => {
672 custom_fields::updated(data)
673 }
674 (OutputFormat::Json, CommandSuccess::CustomFieldUpdated(data)) => render_json_success(data),
675 (OutputFormat::Human, CommandSuccess::CustomFieldDeleted(data)) => {
676 custom_fields::deleted(data)
677 }
678 (OutputFormat::Json, CommandSuccess::CustomFieldDeleted(data)) => render_json_success(data),
679 (OutputFormat::Human, CommandSuccess::CustomFieldItemsCreated(data)) => {
680 custom_field_items::created(data)
681 }
682 (OutputFormat::Json, CommandSuccess::CustomFieldItemsCreated(data)) => {
683 render_json_success(data)
684 }
685 (OutputFormat::Human, CommandSuccess::CustomFieldItemUpdated(data)) => {
686 custom_field_items::updated(data)
687 }
688 (OutputFormat::Json, CommandSuccess::CustomFieldItemUpdated(data)) => {
689 render_json_success(data)
690 }
691 (OutputFormat::Human, CommandSuccess::CustomFieldItemDeleted(data)) => {
692 custom_field_items::deleted(data)
693 }
694 (OutputFormat::Json, CommandSuccess::CustomFieldItemDeleted(data)) => {
695 render_json_success(data)
696 }
697 (OutputFormat::Human, CommandSuccess::CardCustomFieldSet(data)) => {
698 cards::render_card_custom_field_set(data)
699 }
700 (OutputFormat::Json, CommandSuccess::CardCustomFieldSet(data)) => render_json_success(data),
701 (OutputFormat::Human, CommandSuccess::CardCustomFieldQuery(data)) => {
702 cards::render_card_custom_field_query(data)
703 }
704 (OutputFormat::Json, CommandSuccess::CardCustomFieldQuery(data)) => {
705 render_json_success(data)
706 }
707 (OutputFormat::Human, CommandSuccess::CommentCollection(data)) => {
708 render_comment_collection(data)
709 }
710 (OutputFormat::Json, CommandSuccess::CommentCollection(data)) => render_json_success(data),
711 (OutputFormat::Human, CommandSuccess::CommentShown(data)) => render_comment_detail(data),
712 (OutputFormat::Json, CommandSuccess::CommentShown(data)) => render_json_success(data),
713 (OutputFormat::Human, CommandSuccess::CommentCreated(data)) => render_comment_created(data),
714 (OutputFormat::Json, CommandSuccess::CommentCreated(data)) => render_json_success(data),
715 (OutputFormat::Human, CommandSuccess::CommentDeleted(data)) => render_comment_deleted(data),
716 (OutputFormat::Json, CommandSuccess::CommentDeleted(data)) => render_json_success(data),
717 (OutputFormat::Human, CommandSuccess::SwimlaneCollection(data)) => {
718 render_swimlane_collection(data)
719 }
720 (OutputFormat::Json, CommandSuccess::SwimlaneCollection(data)) => render_json_success(data),
721 (OutputFormat::Human, CommandSuccess::SwimlaneShown(data)) => render_swimlane_detail(data),
722 (OutputFormat::Json, CommandSuccess::SwimlaneShown(data)) => render_json_success(data),
723 (OutputFormat::Human, CommandSuccess::SwimlaneCreated(data)) => {
724 render_swimlane_created(data)
725 }
726 (OutputFormat::Json, CommandSuccess::SwimlaneCreated(data)) => render_json_success(data),
727 (OutputFormat::Human, CommandSuccess::SwimlaneUpdated(data)) => {
728 render_swimlane_updated(data)
729 }
730 (OutputFormat::Json, CommandSuccess::SwimlaneUpdated(data)) => render_json_success(data),
731 (OutputFormat::Human, CommandSuccess::SwimlaneDeleted(data)) => {
732 render_swimlane_deleted(data)
733 }
734 (OutputFormat::Json, CommandSuccess::SwimlaneDeleted(data)) => render_json_success(data),
735 (OutputFormat::Human, CommandSuccess::ProfileAdded(data)) => {
736 render_profile_item("Added profile", data)
737 }
738 (OutputFormat::Json, CommandSuccess::ProfileAdded(data))
739 | (OutputFormat::Json, CommandSuccess::ProfileShown(data))
740 | (OutputFormat::Json, CommandSuccess::ProfileUsed(data))
741 | (OutputFormat::Json, CommandSuccess::ProfileUpdated(data)) => {
742 serde_json::to_string(&SuccessEnvelope { ok: true, data })
743 .expect("profile success is always serializable")
744 }
745 (OutputFormat::Human, CommandSuccess::ProfileList(data)) => render_profile_list(data),
746 (OutputFormat::Json, CommandSuccess::ProfileList(data)) => {
747 serde_json::to_string(&SuccessEnvelope { ok: true, data })
748 .expect("profile list success is always serializable")
749 }
750 (OutputFormat::Human, CommandSuccess::ProfileShown(data)) => {
751 render_profile_item("Profile", data)
752 }
753 (OutputFormat::Human, CommandSuccess::ProfileUsed(data)) => {
754 render_profile_item("Using profile", data)
755 }
756 (OutputFormat::Human, CommandSuccess::ProfileUpdated(data)) => {
757 render_profile_item("Updated profile", data)
758 }
759 (OutputFormat::Human, CommandSuccess::ProfileRemoved(data)) => render_profile_removed(data),
760 (OutputFormat::Json, CommandSuccess::ProfileRemoved(data)) => {
761 serde_json::to_string(&SuccessEnvelope { ok: true, data })
762 .expect("profile removal success is always serializable")
763 }
764 }
765}
766
767pub async fn write_success(
768 format: OutputFormat,
769 success: CommandSuccess,
770 stdout: &mut dyn Write,
771 stderr: &mut dyn Write,
772) -> Result<ExitCode, AppError> {
773 if let CommandSuccess::ApiResponse(response) = success {
774 return raw::write_response(format, response, stdout, stderr).await;
775 }
776 if format == OutputFormat::Raw {
777 if let CommandSuccess::AttachmentDownloaded(data) = &success {
778 let bytes = data.bytes.as_deref().ok_or_else(|| {
779 AppError::invalid_input("raw attachment output requires --destination -")
780 })?;
781 stdout
782 .write_all(bytes)
783 .and_then(|()| stdout.flush())
784 .map_err(output_write_error)?;
785 return Ok(ExitCode::SUCCESS);
786 }
787 }
788 if format == OutputFormat::Raw {
789 if let CommandSuccess::Exported(data) = &success {
790 let staged = data.staged.as_ref().ok_or_else(|| {
791 AppError::invalid_input("raw export output requires --destination -")
792 })?;
793 let mut file = staged.0.reopen().map_err(export_read_error)?;
794 let mut buffer = [0; 64 * 1024];
795 loop {
796 let read = file.read(&mut buffer).map_err(export_read_error)?;
797 if read == 0 {
798 break;
799 }
800 stdout
801 .write_all(&buffer[..read])
802 .map_err(output_write_error)?;
803 }
804 stdout.flush().map_err(output_write_error)?;
805 return Ok(ExitCode::SUCCESS);
806 }
807 }
808 let rendered = render_success(format, &success);
809 if !rendered.is_empty() {
810 let written = writeln!(stdout, "{rendered}").and_then(|()| {
811 if matches!(&success, CommandSuccess::AttachmentDownloaded(data) if data.file_saved)
812 || matches!(&success, CommandSuccess::Exported(data) if data.file_saved)
813 {
814 stdout.flush()
815 } else {
816 Ok(())
817 }
818 });
819 written.map_err(|e| {
820 let error = output_write_error(e);
821 if let CommandSuccess::Exported(data) = &success {
822 if data.file_saved {
823 return error.with_details(crate::error::ErrorDetails {
824 file_saved: Some(true),
825 destination: Some(data.destination.clone()),
826 ..crate::error::ErrorDetails::default()
827 });
828 }
829 }
830 if let CommandSuccess::AttachmentDownloaded(data) = &success {
831 if data.file_saved {
832 return error.with_details(crate::error::ErrorDetails {
833 file_saved: Some(true),
834 destination: Some(data.destination.clone()),
835 ..crate::error::ErrorDetails::default()
836 });
837 }
838 }
839 error
840 })?;
841 }
842 Ok(ExitCode::SUCCESS)
843}
844fn export_read_error(error: std::io::Error) -> AppError {
845 AppError::new(
846 ErrorCode::FileIoError,
847 format!(
848 "could not read the staged export file: {}",
849 crate::input::escape_terminal_text(&error.to_string())
850 ),
851 StableExitCode::Transport,
852 )
853}
854pub(crate) fn output_write_error(error: std::io::Error) -> AppError {
855 AppError::new(
856 ErrorCode::InternalError,
857 format!("could not write command output: {error}"),
858 StableExitCode::Transport,
859 )
860}
861
862pub(crate) fn render_json_success<T: Serialize>(data: &T) -> String {
863 serde_json::to_string(&SuccessEnvelope { ok: true, data })
864 .expect("command success is always serializable")
865}
866
867pub fn render_error(format: OutputFormat, error: &AppError) -> String {
868 match format {
869 OutputFormat::Human => {
870 let mut rendered = format!(
871 "Error [{}]: {}",
872 error.code().as_str(),
873 error.message().trim()
874 );
875 if let Some(batch) = &error.details().batch_result {
876 rendered.push('\n');
877 rendered.push_str(&cards::render_batch(batch));
878 }
879 if let Some(result) = &error.details().trello_import_result {
880 rendered.push('\n');
881 rendered.push_str(&imports::trello(result));
882 }
883 if let Some(ids) = &error.details().invalid_label_ids {
884 rendered.push_str(&format!(
885 "\nInvalid label IDs: {}",
886 formatting::escape_terminal_controls(&ids.join(", "))
887 ));
888 }
889 if let Some(response) = &error.details().response {
890 rendered.push('\n');
891 rendered.push_str(&render_api_response_human(response));
892 }
893 if error.details().file_saved == Some(true) {
894 if let Some(destination) = &error.details().destination {
895 rendered.push_str(&format!(
896 "\nDownload saved to `{}`; completion output failed.",
897 formatting::escape_terminal_controls(destination)
898 ));
899 }
900 }
901 rendered
902 }
903 OutputFormat::Json => serde_json::to_string(&ErrorEnvelope {
904 ok: false,
905 error: ErrorBody {
906 code: error.code().as_str(),
907 message: error.message(),
908 details: error.details(),
909 },
910 })
911 .expect("application errors are always serializable"),
912 OutputFormat::Raw => format!(
913 "Error [{}]: {}",
914 error.code().as_str(),
915 error.message().trim()
916 ),
917 }
918}
919
920#[cfg(test)]
921mod tests;