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    /// Skip confirmation prompts for destructive operations
24    #[arg(long, short = 'y', global = true)]
25    pub yes: bool,
26    /// Alias for --output json
27    #[arg(long, global = true, hide = true)]
28    pub json: bool,
29    #[command(subcommand)]
30    pub command: Option<Command>,
31}
32
33#[derive(Debug, Subcommand)]
34pub enum Command {
35    /// Guided Microsoft Entra app and account setup
36    Init(InitArgs),
37    /// Manage delegated Microsoft authentication
38    Auth {
39        #[command(subcommand)]
40        command: AuthCommand,
41    },
42    /// List, select, or remove configuration profiles
43    Profile {
44        #[command(subcommand)]
45        command: ProfileCommand,
46    },
47    /// Inspect resolved, secret-free configuration
48    Config {
49        #[command(subcommand)]
50        command: ConfigCommand,
51    },
52    /// Show the signed-in Microsoft 365 identity
53    Whoami,
54    /// List teams joined by the signed-in user
55    List(OffsetPageArgs),
56    /// Work with team channels
57    Channels {
58        #[command(subcommand)]
59        command: ChannelsCommand,
60    },
61    /// Work with one-to-one and group chats
62    Chats {
63        #[command(subcommand)]
64        command: ChatsCommand,
65    },
66    /// Read and send messages
67    Messages {
68        #[command(subcommand)]
69        command: MessagesCommand,
70    },
71    /// Open the keyboard-first terminal interface
72    Tui(TuiArgs),
73    /// Check configuration, credential storage, and Graph access
74    Doctor {
75        /// Check local state without contacting Microsoft Graph
76        #[arg(long)]
77        offline: bool,
78    },
79    /// Describe supported and deliberately unsupported capabilities
80    Capabilities,
81    /// Emit the offline CLI Spec v0.3 contract
82    Schema {
83        #[arg(long)]
84        command: Option<String>,
85    },
86    /// Generate shell completions
87    Completions { shell: Shell },
88}
89
90#[derive(Debug, Args)]
91pub struct InitArgs {
92    /// Override the maintained teams-cli public-client application ID
93    #[arg(long, env = "TEAMS_CLIENT_ID")]
94    pub client_id: Option<String>,
95    /// Tenant ID/domain, or `organizations`
96    #[arg(long, default_value = "organizations", env = "TEAMS_TENANT")]
97    pub tenant: String,
98    /// Save configuration without starting sign-in
99    #[arg(long)]
100    pub no_login: bool,
101    /// Use device-code sign-in instead of browser PKCE
102    #[arg(long)]
103    pub device_code: bool,
104    /// Request admin-consented access to read channel history
105    #[arg(long)]
106    pub channel_history: bool,
107    /// Block remote write operations for this profile
108    #[arg(long, env = "TEAMS_READ_ONLY")]
109    pub read_only: bool,
110}
111
112#[derive(Debug, Subcommand)]
113pub enum ConfigCommand {
114    /// Print the resolved profile without credentials
115    Show,
116    /// Print the absolute configuration file path
117    Path,
118}
119
120#[derive(Debug, Subcommand)]
121pub enum AuthCommand {
122    /// Sign in with delegated OAuth
123    Login {
124        /// Use device-code sign-in for headless and remote environments
125        #[arg(long)]
126        device_code: bool,
127        /// Request admin-consented access to read channel history
128        #[arg(long)]
129        channel_history: bool,
130    },
131    /// Remove locally stored credentials
132    Logout,
133    /// Verify authentication and show the selected profile's status
134    Status {
135        /// Inspect local credential state without contacting Microsoft Graph
136        #[arg(long)]
137        offline: bool,
138    },
139}
140
141#[derive(Debug, Subcommand)]
142pub enum ProfileCommand {
143    /// List configured profiles and identify the active one
144    List,
145    /// Select the default profile for future commands
146    Use { name: String },
147    /// Remove a profile and its stored credential
148    Remove { name: String },
149}
150
151#[derive(Debug, Subcommand)]
152pub enum ChannelsCommand {
153    /// List channels in a team
154    List {
155        team: String,
156        #[command(flatten)]
157        page: OffsetPageArgs,
158    },
159}
160
161#[derive(Debug, Subcommand)]
162pub enum ChatsCommand {
163    List(PageArgs),
164}
165
166#[derive(Debug, Args, Clone)]
167pub struct PageArgs {
168    /// Maximum records in this page
169    #[arg(long, default_value_t = 50, value_parser = clap::value_parser!(u16).range(1..=50))]
170    pub limit: u16,
171    /// Opaque continuation URL from a previous response
172    #[arg(long)]
173    pub cursor: Option<String>,
174    /// Comma-separated output fields
175    #[arg(long)]
176    pub fields: Option<String>,
177}
178
179#[derive(Debug, Args, Clone)]
180pub struct OffsetPageArgs {
181    /// Maximum records in this page
182    #[arg(long, default_value_t = 50, value_parser = clap::value_parser!(u16).range(1..=100))]
183    pub limit: u16,
184    /// Number of records to skip
185    #[arg(long, default_value_t = 0)]
186    pub offset: usize,
187    /// Comma-separated output fields
188    #[arg(long)]
189    pub fields: Option<String>,
190}
191
192#[derive(Debug, Subcommand)]
193pub enum MessagesCommand {
194    /// List messages from a chat or channel
195    List {
196        #[arg(long, conflicts_with = "team", required_unless_present = "team")]
197        chat: Option<String>,
198        #[arg(long, requires = "channel", required_unless_present = "chat")]
199        team: Option<String>,
200        #[arg(long, requires = "team")]
201        channel: Option<String>,
202        #[command(flatten)]
203        page: PageArgs,
204    },
205    /// Send a message as the signed-in user
206    Send {
207        #[arg(long, conflicts_with = "team", required_unless_present = "team")]
208        chat: Option<String>,
209        #[arg(long, requires = "channel", required_unless_present = "chat")]
210        team: Option<String>,
211        #[arg(long, requires = "team")]
212        channel: Option<String>,
213        /// Plain-text message body; use '-' to read stdin
214        #[arg(long)]
215        body: String,
216    },
217}
218
219#[derive(Debug, Args)]
220pub struct TuiArgs {
221    /// Use deterministic sample data; no account or network required
222    #[arg(long)]
223    pub demo: bool,
224    /// Print one ANSI-free frame instead of taking over the terminal
225    #[arg(long, requires = "demo")]
226    pub snapshot: bool,
227    #[arg(long, default_value_t = 100, hide = true)]
228    pub width: u16,
229    #[arg(long, default_value_t = 30, hide = true)]
230    pub height: u16,
231}