Skip to main content

teams_cli/
cli.rs

1use clap::{Args, Parser, Subcommand};
2use clap_complete::Shell;
3
4use crate::output::OutputFormat;
5
6#[derive(Debug, Parser)]
7#[command(
8    name = "teams",
9    version,
10    about = "Microsoft Teams from your terminal, for humans and agents",
11    after_help = "Get started:\n  teams init                         Guided setup\n  teams tui --demo                   Explore without an account\n  teams doctor                       Diagnose configuration and access\n  teams schema --command 'messages send'\n                                     Inspect the automation contract"
12)]
13pub struct Cli {
14    /// Config profile to use
15    #[arg(long, global = true, env = "TEAMS_PROFILE")]
16    pub profile: Option<String>,
17    /// Output format; auto is text on a terminal and JSON when piped
18    #[arg(short = 'o', long, global = true, value_enum, default_value = "auto")]
19    pub output: OutputFormat,
20    /// Suppress progress and informational messages
21    #[arg(long, global = true)]
22    pub quiet: bool,
23    /// Alias for --output json
24    #[arg(long, global = true, hide = true)]
25    pub json: bool,
26    #[command(subcommand)]
27    pub command: Option<Command>,
28}
29
30#[derive(Debug, Subcommand)]
31pub enum Command {
32    /// Guided Microsoft Entra app and account setup
33    Init(InitArgs),
34    /// Manage delegated Microsoft authentication
35    Auth {
36        #[command(subcommand)]
37        command: AuthCommand,
38    },
39    /// Show the signed-in Microsoft 365 identity
40    Whoami,
41    /// List teams joined by the signed-in user
42    List(OffsetPageArgs),
43    /// Work with team channels
44    Channels {
45        #[command(subcommand)]
46        command: ChannelsCommand,
47    },
48    /// Work with one-to-one and group chats
49    Chats {
50        #[command(subcommand)]
51        command: ChatsCommand,
52    },
53    /// Read and send messages
54    Messages {
55        #[command(subcommand)]
56        command: MessagesCommand,
57    },
58    /// Open the keyboard-first terminal interface
59    Tui(TuiArgs),
60    /// Check configuration, credential storage, and Graph access
61    Doctor,
62    /// Describe supported and deliberately unsupported capabilities
63    Capabilities,
64    /// Emit the offline CLI Spec v0.3 contract
65    Schema {
66        #[arg(long)]
67        command: Option<String>,
68    },
69    /// Generate shell completions
70    Completions { shell: Shell },
71}
72
73#[derive(Debug, Args)]
74pub struct InitArgs {
75    /// Public-client application (client) ID
76    #[arg(long, env = "TEAMS_CLIENT_ID")]
77    pub client_id: Option<String>,
78    /// Tenant ID/domain, or `organizations`
79    #[arg(long, default_value = "organizations", env = "TEAMS_TENANT")]
80    pub tenant: String,
81    /// Save configuration without starting sign-in
82    #[arg(long)]
83    pub no_login: bool,
84    /// Use device-code sign-in instead of browser PKCE
85    #[arg(long)]
86    pub device_code: bool,
87}
88
89#[derive(Debug, Subcommand)]
90pub enum AuthCommand {
91    /// Sign in with delegated OAuth
92    Login {
93        #[arg(long)]
94        device_code: bool,
95    },
96    /// Remove locally stored credentials
97    Logout,
98    /// Show local authentication status without network access
99    Status,
100}
101
102#[derive(Debug, Subcommand)]
103pub enum ChannelsCommand {
104    /// List channels in a team
105    List {
106        team: String,
107        #[command(flatten)]
108        page: OffsetPageArgs,
109    },
110}
111
112#[derive(Debug, Subcommand)]
113pub enum ChatsCommand {
114    List(PageArgs),
115}
116
117#[derive(Debug, Args, Clone)]
118pub struct PageArgs {
119    /// Maximum records in this page
120    #[arg(long, default_value_t = 50, value_parser = clap::value_parser!(u16).range(1..=50))]
121    pub limit: u16,
122    /// Opaque continuation URL from a previous response
123    #[arg(long)]
124    pub cursor: Option<String>,
125    /// Comma-separated output fields
126    #[arg(long)]
127    pub fields: Option<String>,
128}
129
130#[derive(Debug, Args, Clone)]
131pub struct OffsetPageArgs {
132    /// Maximum records in this page
133    #[arg(long, default_value_t = 50, value_parser = clap::value_parser!(u16).range(1..=100))]
134    pub limit: u16,
135    /// Number of records to skip
136    #[arg(long, default_value_t = 0)]
137    pub offset: usize,
138    /// Comma-separated output fields
139    #[arg(long)]
140    pub fields: Option<String>,
141}
142
143#[derive(Debug, Subcommand)]
144pub enum MessagesCommand {
145    /// List messages from a chat or channel
146    List {
147        #[arg(long, conflicts_with = "team", required_unless_present = "team")]
148        chat: Option<String>,
149        #[arg(long, requires = "channel", required_unless_present = "chat")]
150        team: Option<String>,
151        #[arg(long, requires = "team")]
152        channel: Option<String>,
153        #[command(flatten)]
154        page: PageArgs,
155    },
156    /// Send a message as the signed-in user
157    Send {
158        #[arg(long, conflicts_with = "team", required_unless_present = "team")]
159        chat: Option<String>,
160        #[arg(long, requires = "channel", required_unless_present = "chat")]
161        team: Option<String>,
162        #[arg(long, requires = "team")]
163        channel: Option<String>,
164        /// Plain-text message body; use '-' to read stdin
165        #[arg(long)]
166        body: String,
167    },
168}
169
170#[derive(Debug, Args)]
171pub struct TuiArgs {
172    /// Use deterministic sample data; no account or network required
173    #[arg(long)]
174    pub demo: bool,
175    /// Print one ANSI-free frame instead of taking over the terminal
176    #[arg(long, requires = "demo")]
177    pub snapshot: bool,
178    #[arg(long, default_value_t = 100, hide = true)]
179    pub width: u16,
180    #[arg(long, default_value_t = 30, hide = true)]
181    pub height: u16,
182}