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