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