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