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    long_version = env!("TEAMS_LONG_VERSION"),
11    about = "Microsoft Teams from your terminal, for humans and agents",
12    after_help = "Get started:\n  teams init                         Guided setup\n  teams init --backend desktop --desktop-client browser-core
13                                     Lightweight browser, personal account
14  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"
15)]
16pub struct Cli {
17    /// Config profile to use
18    #[arg(long, global = true, env = "TEAMS_PROFILE")]
19    pub profile: Option<String>,
20    /// Output format; auto is text on a terminal and JSON when piped
21    #[arg(short = 'o', long, global = true, value_enum, default_value = "auto")]
22    pub output: OutputFormat,
23    /// Suppress progress and informational messages
24    #[arg(long, global = true)]
25    pub quiet: bool,
26    /// Skip confirmation prompts for destructive operations
27    #[arg(long, short = 'y', global = true)]
28    pub yes: bool,
29    /// Alias for --output json
30    #[arg(long, global = true, hide = true)]
31    pub json: bool,
32    #[command(subcommand)]
33    pub command: Option<Command>,
34}
35
36#[derive(Debug, Subcommand)]
37pub enum Command {
38    /// Configure a Graph or experimental desktop profile
39    Init(InitArgs),
40    /// Manage delegated Microsoft authentication
41    Auth {
42        #[command(subcommand)]
43        command: AuthCommand,
44    },
45    /// List, select, or remove configuration profiles
46    Profile {
47        #[command(subcommand)]
48        command: ProfileCommand,
49    },
50    /// Inspect resolved, secret-free configuration
51    Config {
52        #[command(subcommand)]
53        command: ConfigCommand,
54    },
55    /// Show the signed-in Microsoft 365 identity
56    Whoami,
57    /// List teams joined by the signed-in user
58    List(OffsetPageArgs),
59    /// Work with team channels
60    Channels {
61        #[command(subcommand)]
62        command: ChannelsCommand,
63    },
64    /// Work with one-to-one and group chats
65    Chats {
66        #[command(subcommand)]
67        command: ChatsCommand,
68    },
69    /// Find people to start a conversation with
70    People {
71        #[command(subcommand)]
72        command: PeopleCommand,
73    },
74    /// Read and send messages
75    Messages {
76        #[command(subcommand)]
77        command: MessagesCommand,
78    },
79    /// Open the keyboard-first terminal interface
80    Tui(TuiArgs),
81    /// Check the selected backend's configuration and access
82    Doctor {
83        /// Check configuration/platform without contacting Graph or the desktop endpoint
84        #[arg(long)]
85        offline: bool,
86    },
87    /// Manage a recoverable Teams desktop debugging session
88    Desktop {
89        #[command(subcommand)]
90        command: DesktopCommand,
91    },
92    /// Describe supported and deliberately unsupported capabilities
93    Capabilities,
94    /// Emit the offline CLI Spec v0.3 contract
95    Schema {
96        #[arg(long)]
97        command: Option<String>,
98    },
99    /// Generate shell completions
100    Completions { shell: Shell },
101    /// Serve desktop reads for the Linux CLI under WSL over stdin and stdout
102    #[command(hide = true, name = "desktop-helper")]
103    DesktopHelper,
104}
105
106#[derive(Debug, Args)]
107pub struct InitArgs {
108    /// Connection method (desktop is experimental: chats, chat messages and opt-in chat sending)
109    #[arg(long, value_enum, default_value = "graph")]
110    pub backend: crate::config::BackendKind,
111    /// Existing loopback debugging port for a desktop profile
112    #[arg(long, value_parser = clap::value_parser!(u16).range(1024..))]
113    pub desktop_port: Option<u16>,
114    /// Select the personal or work/school Teams surface; auto refuses mixed surfaces
115    #[arg(long, value_enum)]
116    pub desktop_account: Option<crate::config::DesktopAccount>,
117    /// Use the native Teams app or a dedicated Chrome/Edge browser profile
118    #[arg(long, value_enum)]
119    pub desktop_client: Option<crate::config::DesktopClientKind>,
120    /// Override the maintained teams-cli public-client application ID
121    #[arg(long)]
122    pub client_id: Option<String>,
123    /// Tenant ID/domain, or `organizations`
124    #[arg(long)]
125    pub tenant: Option<String>,
126    /// Save configuration without sign-in or probing the desktop endpoint
127    #[arg(long)]
128    pub no_login: bool,
129    /// Use device-code sign-in instead of browser PKCE
130    #[arg(long)]
131    pub device_code: bool,
132    /// Request admin-consented access to read channel history
133    #[arg(long)]
134    pub channel_history: bool,
135    /// Block remote write operations for this profile
136    #[arg(long, env = "TEAMS_READ_ONLY")]
137    pub read_only: bool,
138    /// Allow a desktop profile to send chat messages (desktop profiles are read-only otherwise)
139    #[arg(long)]
140    pub allow_send: bool,
141}
142
143#[derive(Debug, Subcommand)]
144pub enum ConfigCommand {
145    /// Print the resolved profile without credentials
146    Show,
147    /// Print the absolute configuration file path
148    Path,
149}
150
151#[derive(Debug, Subcommand)]
152pub enum AuthCommand {
153    /// Sign in with delegated OAuth
154    Login {
155        /// Use device-code sign-in for headless and remote environments
156        #[arg(long)]
157        device_code: bool,
158        /// Request admin-consented access to read channel history
159        #[arg(long)]
160        channel_history: bool,
161    },
162    /// Remove locally stored credentials
163    Logout,
164    /// Verify authentication and show the selected profile's status
165    Status {
166        /// Inspect configuration without contacting Graph or the desktop endpoint
167        #[arg(long)]
168        offline: bool,
169    },
170}
171
172#[derive(Debug, Subcommand)]
173pub enum ProfileCommand {
174    /// List configured profiles and identify the active one
175    List,
176    /// Select the default profile for future commands
177    Use { name: String },
178    /// Remove a profile and its stored credential
179    Remove { name: String },
180}
181
182#[derive(Debug, Subcommand)]
183pub enum ChannelsCommand {
184    /// List channels in a team
185    List {
186        team: String,
187        #[command(flatten)]
188        page: OffsetPageArgs,
189    },
190}
191
192#[derive(Debug, Subcommand)]
193pub enum ChatsCommand {
194    List(PageArgs),
195    /// Create or reuse a one-to-one conversation; does not send a message
196    Create {
197        /// Exact person ID returned by `people search`
198        #[arg(long)]
199        recipient: String,
200    },
201}
202
203#[derive(Debug, Subcommand)]
204pub enum PeopleCommand {
205    /// Search people by name or email, without sending anything
206    Search {
207        query: String,
208        #[arg(long, default_value_t = 20, value_parser = clap::value_parser!(u16).range(1..=50))]
209        limit: u16,
210    },
211}
212
213#[derive(Debug, Args, Clone)]
214pub struct PageArgs {
215    /// Maximum records in this page
216    #[arg(long, default_value_t = 50, value_parser = clap::value_parser!(u16).range(1..=50))]
217    pub limit: u16,
218    /// Opaque continuation URL from a previous response
219    #[arg(long)]
220    pub cursor: Option<String>,
221    /// Comma-separated output fields
222    #[arg(long)]
223    pub fields: Option<String>,
224}
225
226#[derive(Debug, Args, Clone)]
227pub struct OffsetPageArgs {
228    /// Maximum records in this page
229    #[arg(long, default_value_t = 50, value_parser = clap::value_parser!(u16).range(1..=100))]
230    pub limit: u16,
231    /// Number of records to skip
232    #[arg(long, default_value_t = 0)]
233    pub offset: usize,
234    /// Comma-separated output fields
235    #[arg(long)]
236    pub fields: Option<String>,
237}
238
239#[derive(Debug, Subcommand)]
240pub enum MessagesCommand {
241    /// Check one send's exact client ID in the newest 50 cached desktop messages
242    Status {
243        #[arg(long)]
244        chat: String,
245        #[arg(long)]
246        client_message_id: String,
247    },
248    /// List messages from a chat or channel
249    List {
250        #[arg(long, conflicts_with = "team", required_unless_present = "team")]
251        chat: Option<String>,
252        #[arg(long, requires = "channel", required_unless_present = "chat")]
253        team: Option<String>,
254        #[arg(long, requires = "team")]
255        channel: Option<String>,
256        #[command(flatten)]
257        page: PageArgs,
258    },
259    /// Send a message as the signed-in user
260    Send {
261        #[arg(long, conflicts_with = "team", required_unless_present = "team")]
262        chat: Option<String>,
263        #[arg(long, requires = "channel", required_unless_present = "chat")]
264        team: Option<String>,
265        #[arg(long, requires = "team")]
266        channel: Option<String>,
267        /// Plain-text message body; use '-' to read stdin
268        #[arg(long)]
269        body: String,
270    },
271}
272
273#[derive(Debug, Args)]
274pub struct TuiArgs {
275    /// Use deterministic sample data; no account or network required
276    #[arg(long)]
277    pub demo: bool,
278    /// Print one ANSI-free frame instead of taking over the terminal
279    #[arg(long, requires = "demo")]
280    pub snapshot: bool,
281    #[arg(long, default_value_t = 100, hide = true)]
282    pub width: u16,
283    #[arg(long, default_value_t = 30, hide = true)]
284    pub height: u16,
285}
286
287#[derive(Debug, Subcommand)]
288pub enum DesktopCommand {
289    /// Inspect endpoint metadata and managed-session recovery state
290    Status(DesktopArgs),
291    /// Open a debugging session (native Teams restart or dedicated browser launch)
292    Connect(DesktopArgs),
293    /// Sign in visibly, then automatically continue headless (browser only)
294    Login(DesktopArgs),
295    /// Close the dedicated browser, or restart native Teams normally
296    Disconnect(DesktopArgs),
297    /// Recover interrupted native settings or close a managed browser session
298    Recover(DesktopArgs),
299    /// Remove remote-debugging flags from the user-wide WebView2 setting (Windows/WSL)
300    ClearFlags(DesktopArgs),
301}
302
303#[derive(Debug, Args)]
304pub struct DesktopArgs {
305    /// Override the configured desktop client
306    #[arg(long, value_enum)]
307    pub desktop_client: Option<crate::config::DesktopClientKind>,
308    /// Open the browser visibly for sign-in (browser connect only)
309    #[arg(long)]
310    pub visible: bool,
311    /// Override the selected profile's desktop port
312    #[arg(long, value_parser = clap::value_parser!(u16).range(1024..))]
313    pub port: Option<u16>,
314}