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