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