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    /// Read and send messages
70    Messages {
71        #[command(subcommand)]
72        command: MessagesCommand,
73    },
74    /// Open the keyboard-first terminal interface
75    Tui(TuiArgs),
76    /// Check the selected backend's configuration and access
77    Doctor {
78        /// Check configuration/platform without contacting Graph or the desktop endpoint
79        #[arg(long)]
80        offline: bool,
81    },
82    /// Manage a recoverable Teams desktop debugging session
83    Desktop {
84        #[command(subcommand)]
85        command: DesktopCommand,
86    },
87    /// Describe supported and deliberately unsupported capabilities
88    Capabilities,
89    /// Emit the offline CLI Spec v0.3 contract
90    Schema {
91        #[arg(long)]
92        command: Option<String>,
93    },
94    /// Generate shell completions
95    Completions { shell: Shell },
96    /// Serve desktop reads for the Linux CLI under WSL over stdin and stdout
97    #[command(hide = true, name = "desktop-helper")]
98    DesktopHelper,
99}
100
101#[derive(Debug, Args)]
102pub struct InitArgs {
103    /// Connection method (desktop is experimental: chats, chat messages and opt-in chat sending)
104    #[arg(long, value_enum, default_value = "graph")]
105    pub backend: crate::config::BackendKind,
106    /// Existing loopback debugging port for a desktop profile
107    #[arg(long, value_parser = clap::value_parser!(u16).range(1024..))]
108    pub desktop_port: Option<u16>,
109    /// Select the personal or work/school Teams surface; auto refuses mixed surfaces
110    #[arg(long, value_enum)]
111    pub desktop_account: Option<crate::config::DesktopAccount>,
112    /// Use the native Teams app or a dedicated Chrome/Edge browser profile
113    #[arg(long, value_enum)]
114    pub desktop_client: Option<crate::config::DesktopClientKind>,
115    /// Override the maintained teams-cli public-client application ID
116    #[arg(long)]
117    pub client_id: Option<String>,
118    /// Tenant ID/domain, or `organizations`
119    #[arg(long)]
120    pub tenant: Option<String>,
121    /// Save configuration without sign-in or probing the desktop endpoint
122    #[arg(long)]
123    pub no_login: bool,
124    /// Use device-code sign-in instead of browser PKCE
125    #[arg(long)]
126    pub device_code: bool,
127    /// Request admin-consented access to read channel history
128    #[arg(long)]
129    pub channel_history: bool,
130    /// Block remote write operations for this profile
131    #[arg(long, env = "TEAMS_READ_ONLY")]
132    pub read_only: bool,
133    /// Allow a desktop profile to send chat messages (desktop profiles are read-only otherwise)
134    #[arg(long)]
135    pub allow_send: bool,
136}
137
138#[derive(Debug, Subcommand)]
139pub enum ConfigCommand {
140    /// Print the resolved profile without credentials
141    Show,
142    /// Print the absolute configuration file path
143    Path,
144}
145
146#[derive(Debug, Subcommand)]
147pub enum AuthCommand {
148    /// Sign in with delegated OAuth
149    Login {
150        /// Use device-code sign-in for headless and remote environments
151        #[arg(long)]
152        device_code: bool,
153        /// Request admin-consented access to read channel history
154        #[arg(long)]
155        channel_history: bool,
156    },
157    /// Remove locally stored credentials
158    Logout,
159    /// Verify authentication and show the selected profile's status
160    Status {
161        /// Inspect configuration without contacting Graph or the desktop endpoint
162        #[arg(long)]
163        offline: bool,
164    },
165}
166
167#[derive(Debug, Subcommand)]
168pub enum ProfileCommand {
169    /// List configured profiles and identify the active one
170    List,
171    /// Select the default profile for future commands
172    Use { name: String },
173    /// Remove a profile and its stored credential
174    Remove { name: String },
175}
176
177#[derive(Debug, Subcommand)]
178pub enum ChannelsCommand {
179    /// List channels in a team
180    List {
181        team: String,
182        #[command(flatten)]
183        page: OffsetPageArgs,
184    },
185}
186
187#[derive(Debug, Subcommand)]
188pub enum ChatsCommand {
189    List(PageArgs),
190}
191
192#[derive(Debug, Args, Clone)]
193pub struct PageArgs {
194    /// Maximum records in this page
195    #[arg(long, default_value_t = 50, value_parser = clap::value_parser!(u16).range(1..=50))]
196    pub limit: u16,
197    /// Opaque continuation URL from a previous response
198    #[arg(long)]
199    pub cursor: Option<String>,
200    /// Comma-separated output fields
201    #[arg(long)]
202    pub fields: Option<String>,
203}
204
205#[derive(Debug, Args, Clone)]
206pub struct OffsetPageArgs {
207    /// Maximum records in this page
208    #[arg(long, default_value_t = 50, value_parser = clap::value_parser!(u16).range(1..=100))]
209    pub limit: u16,
210    /// Number of records to skip
211    #[arg(long, default_value_t = 0)]
212    pub offset: usize,
213    /// Comma-separated output fields
214    #[arg(long)]
215    pub fields: Option<String>,
216}
217
218#[derive(Debug, Subcommand)]
219pub enum MessagesCommand {
220    /// Check one send's exact client ID in the newest 50 cached desktop messages
221    Status {
222        #[arg(long)]
223        chat: String,
224        #[arg(long)]
225        client_message_id: String,
226    },
227    /// List messages from a chat or channel
228    List {
229        #[arg(long, conflicts_with = "team", required_unless_present = "team")]
230        chat: Option<String>,
231        #[arg(long, requires = "channel", required_unless_present = "chat")]
232        team: Option<String>,
233        #[arg(long, requires = "team")]
234        channel: Option<String>,
235        #[command(flatten)]
236        page: PageArgs,
237    },
238    /// Send a message as the signed-in user
239    Send {
240        #[arg(long, conflicts_with = "team", required_unless_present = "team")]
241        chat: Option<String>,
242        #[arg(long, requires = "channel", required_unless_present = "chat")]
243        team: Option<String>,
244        #[arg(long, requires = "team")]
245        channel: Option<String>,
246        /// Plain-text message body; use '-' to read stdin
247        #[arg(long)]
248        body: String,
249    },
250}
251
252#[derive(Debug, Args)]
253pub struct TuiArgs {
254    /// Use deterministic sample data; no account or network required
255    #[arg(long)]
256    pub demo: bool,
257    /// Print one ANSI-free frame instead of taking over the terminal
258    #[arg(long, requires = "demo")]
259    pub snapshot: bool,
260    #[arg(long, default_value_t = 100, hide = true)]
261    pub width: u16,
262    #[arg(long, default_value_t = 30, hide = true)]
263    pub height: u16,
264}
265
266#[derive(Debug, Subcommand)]
267pub enum DesktopCommand {
268    /// Inspect endpoint metadata and managed-session recovery state
269    Status(DesktopArgs),
270    /// Open a debugging session (native Teams restart or dedicated browser launch)
271    Connect(DesktopArgs),
272    /// Sign in visibly, then automatically continue headless (browser only)
273    Login(DesktopArgs),
274    /// Close the dedicated browser, or restart native Teams normally
275    Disconnect(DesktopArgs),
276    /// Recover interrupted native settings or close a managed browser session
277    Recover(DesktopArgs),
278}
279
280#[derive(Debug, Args)]
281pub struct DesktopArgs {
282    /// Override the configured desktop client
283    #[arg(long, value_enum)]
284    pub desktop_client: Option<crate::config::DesktopClientKind>,
285    /// Open the browser visibly for sign-in (browser connect only)
286    #[arg(long)]
287    pub visible: bool,
288    /// Override the selected profile's desktop port
289    #[arg(long, value_parser = clap::value_parser!(u16).range(1024..))]
290    pub port: Option<u16>,
291}