//! CLI command definitions and dispatch for all 7 La Suite services. use clap::Subcommand; use crate::client::SunbeamClient; use crate::error::Result; use crate::output::{self, OutputFormat}; // ═══════════════════════════════════════════════════════════════════════════ // Helper: build an authenticated La Suite client // ═══════════════════════════════════════════════════════════════════════════ async fn people_client(domain: &str) -> Result { let token = crate::auth::get_token().await?; Ok(super::PeopleClient::connect(domain).with_token(&token)) } async fn docs_client(domain: &str) -> Result { let token = crate::auth::get_token().await?; Ok(super::DocsClient::connect(domain).with_token(&token)) } async fn meet_client(domain: &str) -> Result { let token = crate::auth::get_token().await?; Ok(super::MeetClient::connect(domain).with_token(&token)) } async fn drive_client(domain: &str) -> Result { let token = crate::auth::get_token().await?; Ok(super::DriveClient::connect(domain).with_token(&token)) } async fn messages_client(domain: &str) -> Result { let token = crate::auth::get_token().await?; Ok(super::MessagesClient::connect(domain).with_token(&token)) } async fn calendars_client(domain: &str) -> Result { let token = crate::auth::get_token().await?; Ok(super::CalendarsClient::connect(domain).with_token(&token)) } async fn find_client(domain: &str) -> Result { let token = crate::auth::get_token().await?; Ok(super::FindClient::connect(domain).with_token(&token)) } // ═══════════════════════════════════════════════════════════════════════════ // People // ═══════════════════════════════════════════════════════════════════════════ #[derive(Subcommand, Debug)] pub enum PeopleCommand { /// Contact management. Contact { #[command(subcommand)] action: ContactAction, }, /// Team management. Team { #[command(subcommand)] action: TeamAction, }, /// Service provider listing. ServiceProvider { #[command(subcommand)] action: ServiceProviderAction, }, /// Mail domain listing. MailDomain { #[command(subcommand)] action: MailDomainAction, }, } #[derive(Subcommand, Debug)] pub enum ContactAction { /// List contacts. List { #[arg(long)] page: Option, }, /// Get a contact by ID. Get { #[arg(short, long)] id: String, }, /// Create a contact from JSON. Create { /// JSON body (or "-" to read from stdin). #[arg(short, long)] data: Option, }, /// Update a contact from JSON. Update { #[arg(short, long)] id: String, /// JSON body (or "-" to read from stdin). #[arg(short, long)] data: Option, }, /// Delete a contact. Delete { #[arg(short, long)] id: String, }, } #[derive(Subcommand, Debug)] pub enum TeamAction { /// List teams. List { #[arg(long)] page: Option, }, /// Get a team by ID. Get { #[arg(short, long)] id: String, }, /// Create a team from JSON. Create { /// JSON body (or "-" to read from stdin). #[arg(short, long)] data: Option, }, } #[derive(Subcommand, Debug)] pub enum ServiceProviderAction { /// List service providers. List, } #[derive(Subcommand, Debug)] pub enum MailDomainAction { /// List mail domains. List, } pub async fn dispatch_people( cmd: PeopleCommand, client: &SunbeamClient, fmt: OutputFormat, ) -> Result<()> { let people = people_client(client.domain()).await?; match cmd { PeopleCommand::Contact { action } => match action { ContactAction::List { page } => { let page_data = people.list_contacts(page).await?; output::render_list( &page_data.results, &["ID", "NAME", "EMAIL", "ORGANIZATION"], |c| { vec![ c.id.clone(), format!( "{} {}", c.first_name.as_deref().unwrap_or(""), c.last_name.as_deref().unwrap_or("") ) .trim() .to_string(), c.email.clone().unwrap_or_default(), c.organization.clone().unwrap_or_default(), ] }, fmt, ) } ContactAction::Get { id } => { let item = people.get_contact(&id).await?; output::render(&item, fmt) } ContactAction::Create { data } => { let json = output::read_json_input(data.as_deref())?; let item = people.create_contact(&json).await?; output::render(&item, fmt) } ContactAction::Update { id, data } => { let json = output::read_json_input(data.as_deref())?; let item = people.update_contact(&id, &json).await?; output::render(&item, fmt) } ContactAction::Delete { id } => { people.delete_contact(&id).await?; output::ok(&format!("Deleted contact {id}")); Ok(()) } }, PeopleCommand::Team { action } => match action { TeamAction::List { page } => { let page_data = people.list_teams(page).await?; output::render_list( &page_data.results, &["ID", "NAME", "DESCRIPTION"], |t| { vec![ t.id.clone(), t.name.clone().unwrap_or_default(), t.description.clone().unwrap_or_default(), ] }, fmt, ) } TeamAction::Get { id } => { let item = people.get_team(&id).await?; output::render(&item, fmt) } TeamAction::Create { data } => { let json = output::read_json_input(data.as_deref())?; let item = people.create_team(&json).await?; output::render(&item, fmt) } }, PeopleCommand::ServiceProvider { action } => match action { ServiceProviderAction::List => { let page_data = people.list_service_providers().await?; output::render_list( &page_data.results, &["ID", "NAME", "BASE_URL"], |sp| { vec![ sp.id.clone(), sp.name.clone().unwrap_or_default(), sp.base_url.clone().unwrap_or_default(), ] }, fmt, ) } }, PeopleCommand::MailDomain { action } => match action { MailDomainAction::List => { let page_data = people.list_mail_domains().await?; output::render_list( &page_data.results, &["ID", "NAME", "STATUS"], |md| { vec![ md.id.clone(), md.name.clone().unwrap_or_default(), md.status.clone().unwrap_or_default(), ] }, fmt, ) } }, } } // ═══════════════════════════════════════════════════════════════════════════ // Docs // ═══════════════════════════════════════════════════════════════════════════ #[derive(Subcommand, Debug)] pub enum DocsCommand { /// Document management. Document { #[command(subcommand)] action: DocumentAction, }, /// Template management. Template { #[command(subcommand)] action: TemplateAction, }, /// Version history. Version { #[command(subcommand)] action: VersionAction, }, /// Invite a user to a document. Invite { /// Document ID. #[arg(short, long)] id: String, /// JSON body (or "-" to read from stdin). #[arg(short, long)] data: Option, }, } #[derive(Subcommand, Debug)] pub enum DocumentAction { /// List documents. List { #[arg(long)] page: Option, }, /// Get a document by ID. Get { #[arg(short, long)] id: String, }, /// Create a document from JSON. Create { /// JSON body (or "-" to read from stdin). #[arg(short, long)] data: Option, }, /// Update a document from JSON. Update { #[arg(short, long)] id: String, /// JSON body (or "-" to read from stdin). #[arg(short, long)] data: Option, }, /// Delete a document. Delete { #[arg(short, long)] id: String, }, } #[derive(Subcommand, Debug)] pub enum TemplateAction { /// List templates. List { #[arg(long)] page: Option, }, /// Create a template from JSON. Create { /// JSON body (or "-" to read from stdin). #[arg(short, long)] data: Option, }, } #[derive(Subcommand, Debug)] pub enum VersionAction { /// List versions of a document. List { /// Document ID. #[arg(short, long)] id: String, }, } pub async fn dispatch_docs( cmd: DocsCommand, client: &SunbeamClient, fmt: OutputFormat, ) -> Result<()> { let docs = docs_client(client.domain()).await?; match cmd { DocsCommand::Document { action } => match action { DocumentAction::List { page } => { let page_data = docs.list_documents(page).await?; output::render_list( &page_data.results, &["ID", "TITLE", "PUBLIC", "UPDATED"], |d| { vec![ d.id.clone(), d.title.clone().unwrap_or_default(), d.is_public.map_or("-".into(), |p| p.to_string()), d.updated_at.clone().unwrap_or_default(), ] }, fmt, ) } DocumentAction::Get { id } => { let item = docs.get_document(&id).await?; output::render(&item, fmt) } DocumentAction::Create { data } => { let json = output::read_json_input(data.as_deref())?; let item = docs.create_document(&json).await?; output::render(&item, fmt) } DocumentAction::Update { id, data } => { let json = output::read_json_input(data.as_deref())?; let item = docs.update_document(&id, &json).await?; output::render(&item, fmt) } DocumentAction::Delete { id } => { docs.delete_document(&id).await?; output::ok(&format!("Deleted document {id}")); Ok(()) } }, DocsCommand::Template { action } => match action { TemplateAction::List { page } => { let page_data = docs.list_templates(page).await?; output::render_list( &page_data.results, &["ID", "TITLE", "PUBLIC"], |t| { vec![ t.id.clone(), t.title.clone().unwrap_or_default(), t.is_public.map_or("-".into(), |p| p.to_string()), ] }, fmt, ) } TemplateAction::Create { data } => { let json = output::read_json_input(data.as_deref())?; let item = docs.create_template(&json).await?; output::render(&item, fmt) } }, DocsCommand::Version { action } => match action { VersionAction::List { id } => { let page_data = docs.list_versions(&id).await?; output::render_list( &page_data.results, &["ID", "VERSION", "CREATED"], |v| { vec![ v.id.clone(), v.version_number.map_or("-".into(), |n| n.to_string()), v.created_at.clone().unwrap_or_default(), ] }, fmt, ) } }, DocsCommand::Invite { id, data } => { let json = output::read_json_input(data.as_deref())?; let item = docs.invite_user(&id, &json).await?; output::render(&item, fmt) } } } // ═══════════════════════════════════════════════════════════════════════════ // Meet // ═══════════════════════════════════════════════════════════════════════════ #[derive(Subcommand, Debug)] pub enum MeetCommand { /// Room management. Room { #[command(subcommand)] action: RoomAction, }, /// Recording management. Recording { #[command(subcommand)] action: RecordingAction, }, } #[derive(Subcommand, Debug)] pub enum RoomAction { /// List rooms. List { #[arg(long)] page: Option, }, /// Get a room by ID. Get { #[arg(short, long)] id: String, }, /// Create a room from JSON. Create { /// JSON body (or "-" to read from stdin). #[arg(short, long)] data: Option, }, /// Update a room from JSON. Update { #[arg(short, long)] id: String, /// JSON body (or "-" to read from stdin). #[arg(short, long)] data: Option, }, /// Delete a room. Delete { #[arg(short, long)] id: String, }, } #[derive(Subcommand, Debug)] pub enum RecordingAction { /// List recordings for a room. List { /// Room ID. #[arg(short, long)] id: String, }, } pub async fn dispatch_meet( cmd: MeetCommand, client: &SunbeamClient, fmt: OutputFormat, ) -> Result<()> { let meet = meet_client(client.domain()).await?; match cmd { MeetCommand::Room { action } => match action { RoomAction::List { page } => { let page_data = meet.list_rooms(page).await?; output::render_list( &page_data.results, &["ID", "NAME", "SLUG", "PUBLIC"], |r| { vec![ r.id.clone(), r.name.clone().unwrap_or_default(), r.slug.clone().unwrap_or_default(), r.is_public.map_or("-".into(), |p| p.to_string()), ] }, fmt, ) } RoomAction::Get { id } => { let item = meet.get_room(&id).await?; output::render(&item, fmt) } RoomAction::Create { data } => { let json = output::read_json_input(data.as_deref())?; let item = meet.create_room(&json).await?; output::render(&item, fmt) } RoomAction::Update { id, data } => { let json = output::read_json_input(data.as_deref())?; let item = meet.update_room(&id, &json).await?; output::render(&item, fmt) } RoomAction::Delete { id } => { meet.delete_room(&id).await?; output::ok(&format!("Deleted room {id}")); Ok(()) } }, MeetCommand::Recording { action } => match action { RecordingAction::List { id } => { let page_data = meet.list_recordings(&id).await?; output::render_list( &page_data.results, &["ID", "FILENAME", "DURATION", "CREATED"], |r| { vec![ r.id.clone(), r.filename.clone().unwrap_or_default(), r.duration.map_or("-".into(), |d| format!("{d:.1}s")), r.created_at.clone().unwrap_or_default(), ] }, fmt, ) } }, } } // ═══════════════════════════════════════════════════════════════════════════ // Drive // ═══════════════════════════════════════════════════════════════════════════ #[derive(Subcommand, Debug)] pub enum DriveCommand { /// File management. File { #[command(subcommand)] action: FileAction, }, /// Folder management. Folder { #[command(subcommand)] action: FolderAction, }, /// Share a file with a user. Share { /// File ID. #[arg(short, long)] id: String, /// JSON body (or "-" to read from stdin). #[arg(short, long)] data: Option, }, /// Permission management. Permission { #[command(subcommand)] action: PermissionAction, }, } #[derive(Subcommand, Debug)] pub enum FileAction { /// List files. List { #[arg(long)] page: Option, }, /// Get a file by ID. Get { #[arg(short, long)] id: String, }, /// Upload a file (JSON metadata). Upload { /// JSON body (or "-" to read from stdin). #[arg(short, long)] data: Option, }, /// Delete a file. Delete { #[arg(short, long)] id: String, }, } #[derive(Subcommand, Debug)] pub enum FolderAction { /// List folders. List { #[arg(long)] page: Option, }, /// Create a folder from JSON. Create { /// JSON body (or "-" to read from stdin). #[arg(short, long)] data: Option, }, } #[derive(Subcommand, Debug)] pub enum PermissionAction { /// List permissions for a file. List { /// File ID. #[arg(short, long)] id: String, }, } pub async fn dispatch_drive( cmd: DriveCommand, client: &SunbeamClient, fmt: OutputFormat, ) -> Result<()> { let drive = drive_client(client.domain()).await?; match cmd { DriveCommand::File { action } => match action { FileAction::List { page } => { let page_data = drive.list_files(page).await?; output::render_list( &page_data.results, &["ID", "NAME", "SIZE", "MIME_TYPE"], |f| { vec![ f.id.clone(), f.name.clone().unwrap_or_default(), f.size.map_or("-".into(), |s| s.to_string()), f.mime_type.clone().unwrap_or_default(), ] }, fmt, ) } FileAction::Get { id } => { let item = drive.get_file(&id).await?; output::render(&item, fmt) } FileAction::Upload { data } => { let json = output::read_json_input(data.as_deref())?; let item = drive.upload_file(&json).await?; output::render(&item, fmt) } FileAction::Delete { id } => { drive.delete_file(&id).await?; output::ok(&format!("Deleted file {id}")); Ok(()) } }, DriveCommand::Folder { action } => match action { FolderAction::List { page } => { let page_data = drive.list_folders(page).await?; output::render_list( &page_data.results, &["ID", "NAME", "PARENT_ID"], |f| { vec![ f.id.clone(), f.name.clone().unwrap_or_default(), f.parent_id.clone().unwrap_or_default(), ] }, fmt, ) } FolderAction::Create { data } => { let json = output::read_json_input(data.as_deref())?; let item = drive.create_folder(&json).await?; output::render(&item, fmt) } }, DriveCommand::Share { id, data } => { let json = output::read_json_input(data.as_deref())?; let item = drive.share_file(&id, &json).await?; output::render(&item, fmt) } DriveCommand::Permission { action } => match action { PermissionAction::List { id } => { let page_data = drive.get_permissions(&id).await?; output::render_list( &page_data.results, &["ID", "USER_ID", "ROLE", "READ", "WRITE"], |p| { vec![ p.id.clone(), p.user_id.clone().unwrap_or_default(), p.role.clone().unwrap_or_default(), p.can_read.map_or("-".into(), |v| v.to_string()), p.can_write.map_or("-".into(), |v| v.to_string()), ] }, fmt, ) } }, } } // ═══════════════════════════════════════════════════════════════════════════ // Mail (Messages) // ═══════════════════════════════════════════════════════════════════════════ #[derive(Subcommand, Debug)] pub enum MailCommand { /// Mailbox management. Mailbox { #[command(subcommand)] action: MailboxAction, }, /// Message management. Message { #[command(subcommand)] action: MessageAction, }, /// Folder listing. Folder { #[command(subcommand)] action: MailFolderAction, }, /// Contact listing. Contact { #[command(subcommand)] action: MailContactAction, }, } #[derive(Subcommand, Debug)] pub enum MailboxAction { /// List mailboxes. List, /// Get a mailbox by ID. Get { #[arg(short, long)] id: String, }, } #[derive(Subcommand, Debug)] pub enum MessageAction { /// List messages in a mailbox folder. List { /// Mailbox ID. #[arg(short, long)] id: String, /// Folder name (e.g. "inbox"). #[arg(short, long, default_value = "inbox")] folder: String, }, /// Get a message. Get { /// Mailbox ID. #[arg(short, long)] id: String, /// Message ID. #[arg(short, long)] message_id: String, }, /// Send a message from a mailbox. Send { /// Mailbox ID. #[arg(short, long)] id: String, /// JSON body (or "-" to read from stdin). #[arg(short, long)] data: Option, }, } #[derive(Subcommand, Debug)] pub enum MailFolderAction { /// List folders in a mailbox. List { /// Mailbox ID. #[arg(short, long)] id: String, }, } #[derive(Subcommand, Debug)] pub enum MailContactAction { /// List contacts in a mailbox. List { /// Mailbox ID. #[arg(short, long)] id: String, }, } pub async fn dispatch_mail( cmd: MailCommand, client: &SunbeamClient, fmt: OutputFormat, ) -> Result<()> { let mail = messages_client(client.domain()).await?; match cmd { MailCommand::Mailbox { action } => match action { MailboxAction::List => { let page_data = mail.list_mailboxes().await?; output::render_list( &page_data.results, &["ID", "EMAIL", "DISPLAY_NAME"], |m| { vec![ m.id.clone(), m.email.clone().unwrap_or_default(), m.display_name.clone().unwrap_or_default(), ] }, fmt, ) } MailboxAction::Get { id } => { let item = mail.get_mailbox(&id).await?; output::render(&item, fmt) } }, MailCommand::Message { action } => match action { MessageAction::List { id, folder } => { let page_data = mail.list_messages(&id, &folder).await?; output::render_list( &page_data.results, &["ID", "SUBJECT", "FROM", "READ", "CREATED"], |m| { vec![ m.id.clone(), m.subject.clone().unwrap_or_default(), m.from_address.clone().unwrap_or_default(), m.is_read.map_or("-".into(), |r| r.to_string()), m.created_at.clone().unwrap_or_default(), ] }, fmt, ) } MessageAction::Get { id, message_id } => { let item = mail.get_message(&id, &message_id).await?; output::render(&item, fmt) } MessageAction::Send { id, data } => { let json = output::read_json_input(data.as_deref())?; let item = mail.send_message(&id, &json).await?; output::render(&item, fmt) } }, MailCommand::Folder { action } => match action { MailFolderAction::List { id } => { let page_data = mail.list_folders(&id).await?; output::render_list( &page_data.results, &["ID", "NAME", "MESSAGES", "UNREAD"], |f| { vec![ f.id.clone(), f.name.clone().unwrap_or_default(), f.message_count.map_or("-".into(), |c| c.to_string()), f.unread_count.map_or("-".into(), |c| c.to_string()), ] }, fmt, ) } }, MailCommand::Contact { action } => match action { MailContactAction::List { id } => { let page_data = mail.list_contacts(&id).await?; output::render_list( &page_data.results, &["ID", "EMAIL", "DISPLAY_NAME"], |c| { vec![ c.id.clone(), c.email.clone().unwrap_or_default(), c.display_name.clone().unwrap_or_default(), ] }, fmt, ) } }, } } // ═══════════════════════════════════════════════════════════════════════════ // Calendars // ═══════════════════════════════════════════════════════════════════════════ #[derive(Subcommand, Debug)] pub enum CalCommand { /// Calendar management. Calendar { #[command(subcommand)] action: CalendarAction, }, /// Event management. Event { #[command(subcommand)] action: EventAction, }, /// RSVP to an event. Rsvp { /// Calendar ID. #[arg(short = 'c', long)] calendar_id: String, /// Event ID. #[arg(short = 'e', long)] event_id: String, /// JSON body (or "-" to read from stdin). #[arg(short, long)] data: Option, }, } #[derive(Subcommand, Debug)] pub enum CalendarAction { /// List calendars. List, /// Get a calendar by ID. Get { #[arg(short, long)] id: String, }, /// Create a calendar from JSON. Create { /// JSON body (or "-" to read from stdin). #[arg(short, long)] data: Option, }, } #[derive(Subcommand, Debug)] pub enum EventAction { /// List events in a calendar. List { /// Calendar ID. #[arg(short = 'c', long)] calendar_id: String, }, /// Get an event. Get { /// Calendar ID. #[arg(short = 'c', long)] calendar_id: String, /// Event ID. #[arg(short = 'e', long)] event_id: String, }, /// Create an event from JSON. Create { /// Calendar ID. #[arg(short = 'c', long)] calendar_id: String, /// JSON body (or "-" to read from stdin). #[arg(short, long)] data: Option, }, /// Update an event from JSON. Update { /// Calendar ID. #[arg(short = 'c', long)] calendar_id: String, /// Event ID. #[arg(short = 'e', long)] event_id: String, /// JSON body (or "-" to read from stdin). #[arg(short, long)] data: Option, }, /// Delete an event. Delete { /// Calendar ID. #[arg(short = 'c', long)] calendar_id: String, /// Event ID. #[arg(short = 'e', long)] event_id: String, }, } pub async fn dispatch_cal( cmd: CalCommand, client: &SunbeamClient, fmt: OutputFormat, ) -> Result<()> { let cal = calendars_client(client.domain()).await?; match cmd { CalCommand::Calendar { action } => match action { CalendarAction::List => { let page_data = cal.list_calendars().await?; output::render_list( &page_data.results, &["ID", "NAME", "COLOR", "DEFAULT"], |c| { vec![ c.id.clone(), c.name.clone().unwrap_or_default(), c.color.clone().unwrap_or_default(), c.is_default.map_or("-".into(), |d| d.to_string()), ] }, fmt, ) } CalendarAction::Get { id } => { let item = cal.get_calendar(&id).await?; output::render(&item, fmt) } CalendarAction::Create { data } => { let json = output::read_json_input(data.as_deref())?; let item = cal.create_calendar(&json).await?; output::render(&item, fmt) } }, CalCommand::Event { action } => match action { EventAction::List { calendar_id } => { let page_data = cal.list_events(&calendar_id).await?; output::render_list( &page_data.results, &["ID", "TITLE", "START", "END", "ALL_DAY"], |e| { vec![ e.id.clone(), e.title.clone().unwrap_or_default(), e.start.clone().unwrap_or_default(), e.end.clone().unwrap_or_default(), e.all_day.map_or("-".into(), |a| a.to_string()), ] }, fmt, ) } EventAction::Get { calendar_id, event_id, } => { let item = cal.get_event(&calendar_id, &event_id).await?; output::render(&item, fmt) } EventAction::Create { calendar_id, data } => { let json = output::read_json_input(data.as_deref())?; let item = cal.create_event(&calendar_id, &json).await?; output::render(&item, fmt) } EventAction::Update { calendar_id, event_id, data, } => { let json = output::read_json_input(data.as_deref())?; let item = cal.update_event(&calendar_id, &event_id, &json).await?; output::render(&item, fmt) } EventAction::Delete { calendar_id, event_id, } => { cal.delete_event(&calendar_id, &event_id).await?; output::ok(&format!("Deleted event {event_id}")); Ok(()) } }, CalCommand::Rsvp { calendar_id, event_id, data, } => { let json = output::read_json_input(data.as_deref())?; cal.rsvp(&calendar_id, &event_id, &json).await?; output::ok(&format!("RSVP sent for event {event_id}")); Ok(()) } } } // ═══════════════════════════════════════════════════════════════════════════ // Find // ═══════════════════════════════════════════════════════════════════════════ #[derive(Subcommand, Debug)] pub enum FindCommand { /// Search across La Suite services. Search { /// Search query. #[arg(short, long)] query: String, #[arg(long)] page: Option, }, } pub async fn dispatch_find( cmd: FindCommand, client: &SunbeamClient, fmt: OutputFormat, ) -> Result<()> { let find = find_client(client.domain()).await?; match cmd { FindCommand::Search { query, page } => { let page_data = find.search(&query, page).await?; output::render_list( &page_data.results, &["ID", "TITLE", "SOURCE", "SCORE", "URL"], |r| { vec![ r.id.clone(), r.title.clone().unwrap_or_default(), r.source.clone().unwrap_or_default(), r.score.map_or("-".into(), |s| format!("{s:.2}")), r.url.clone().unwrap_or_default(), ] }, fmt, ) } } }