Skip to main content

teams_cli/
schema.rs

1use serde_json::{Value, json};
2
3use crate::error;
4
5fn field(name: &str, kind: &str) -> Value {
6    json!({"name": name, "type": kind})
7}
8fn array_field(name: &str, item_kind: &str) -> Value {
9    json!({"name": name, "type": "array", "items": {"type": item_kind}})
10}
11fn nullable_field(name: &str, kind: &str) -> Value {
12    json!({"name": name, "type": kind, "nullable": true})
13}
14fn nullable_array_field(name: &str, item_kind: &str) -> Value {
15    json!({"name": name, "type": "array", "items": {"type": item_kind}, "nullable": true})
16}
17fn arg(name: &str, kind: &str, description: &str) -> Value {
18    json!({"name": name, "type": kind, "description": description})
19}
20fn required_arg(name: &str, kind: &str, description: &str) -> Value {
21    json!({"name": name, "type": kind, "description": description, "required": true})
22}
23
24fn single(name: &str, description: &str, effects: &str, fields: Vec<Value>) -> Value {
25    json!({"name": name, "description": description, "effects": effects, "cardinality": "single", "output_fields": fields})
26}
27
28fn paged(name: &str, description: &str, fields: Vec<Value>) -> Value {
29    json!({
30        "name": name, "description": description, "effects": "read_only", "cardinality": "unbounded",
31        "pagination": {"style": "cursor", "cursor_field": "next_cursor", "cursor_arg": "--cursor", "limit_arg": "--limit"},
32        "fields_arg": "--fields",
33        "args": [
34            arg("--limit", "integer", "Maximum records in this page"),
35            arg("--cursor", "string", "Opaque continuation URL from the previous page"),
36            arg("--fields", "string", "Comma-separated output fields")
37        ],
38        "output_fields": fields
39    })
40}
41
42fn offset_paged(name: &str, description: &str, fields: Vec<Value>) -> Value {
43    json!({
44        "name": name, "description": description, "effects": "read_only", "cardinality": "unbounded",
45        "pagination": {"style": "offset", "offset_arg": "--offset", "limit_arg": "--limit"},
46        "fields_arg": "--fields",
47        "args": [
48            arg("--limit", "integer", "Maximum records in this page"),
49            arg("--offset", "integer", "Number of records to skip"),
50            arg("--fields", "string", "Comma-separated output fields")
51        ],
52        "output_fields": fields
53    })
54}
55
56pub fn generate(command_filter: Option<&str>) -> Value {
57    let identity = vec![
58        field("id", "string"),
59        field("displayName", "string"),
60        field("userPrincipalName", "string"),
61        nullable_field("mail", "string"),
62    ];
63    let mut commands = vec![
64        {
65            let mut value = single(
66                "init",
67                "Configure an Entra public client and optionally sign in",
68                "idempotent",
69                vec![
70                    field("profile", "string"),
71                    field("config_path", "string"),
72                    field("signed_in", "boolean"),
73                    field("client_id", "string"),
74                    field("tenant", "string"),
75                    field("channel_history_requested", "boolean"),
76                    field("read_only", "boolean"),
77                ],
78            );
79            value["args"] = json!([
80                arg(
81                    "--client-id",
82                    "string",
83                    "Override the maintained teams-cli public-client application ID"
84                ),
85                arg("--tenant", "string", "Tenant ID, domain, or organizations"),
86                arg(
87                    "--no-login",
88                    "boolean",
89                    "Save configuration without signing in"
90                ),
91                arg("--device-code", "boolean", "Use device-code sign-in"),
92                arg(
93                    "--channel-history",
94                    "boolean",
95                    "Request admin-consented channel-history access"
96                ),
97                arg("--read-only", "boolean", "Block remote write operations")
98            ]);
99            value
100        },
101        {
102            let mut value = single(
103                "auth login",
104                "Sign in with delegated Microsoft OAuth and protect credentials in the OS store",
105                "idempotent",
106                vec![
107                    field("profile", "string"),
108                    field("expires_at", "integer"),
109                    nullable_field("scope", "string"),
110                    field("channel_history_requested", "boolean"),
111                ],
112            );
113            value["args"] = json!([
114                arg(
115                    "--device-code",
116                    "boolean",
117                    "Use device-code sign-in for headless and remote environments"
118                ),
119                arg(
120                    "--channel-history",
121                    "boolean",
122                    "Request admin-consented channel-history access"
123                )
124            ]);
125            value
126        },
127        single(
128            "auth logout",
129            "Remove locally stored delegated credentials",
130            "idempotent",
131            vec![field("profile", "string"), field("signed_in", "boolean")],
132        ),
133        {
134            let mut value = single(
135                "auth status",
136                "Verify authentication and inspect credential state",
137                "read_only",
138                vec![
139                    field("profile", "string"),
140                    field("configured", "boolean"),
141                    field("signed_in", "boolean"),
142                    field("verified", "boolean"),
143                    nullable_field("identity", "object"),
144                    field("config_path", "string"),
145                    field("read_only", "boolean"),
146                    nullable_array_field("granted_scopes", "string"),
147                    nullable_field("channel_history", "boolean"),
148                ],
149            );
150            value["args"] = json!([arg(
151                "--offline",
152                "boolean",
153                "Inspect local credential state without contacting Microsoft Graph"
154            )]);
155            value
156        },
157        single(
158            "profile list",
159            "List configured profiles and identify the active one",
160            "read_only",
161            vec![array_field("items", "object"), field("total", "integer")],
162        ),
163        {
164            let mut value = single(
165                "profile use",
166                "Select the default profile for future commands",
167                "idempotent",
168                vec![field("profile", "string"), field("active", "boolean")],
169            );
170            value["args"] = json!([required_arg("name", "string", "Profile name")]);
171            value
172        },
173        {
174            let mut value = single(
175                "profile remove",
176                "Remove a profile and its stored credential",
177                "idempotent",
178                vec![field("profile", "string"), field("removed", "boolean")],
179            );
180            value["args"] = json!([required_arg("name", "string", "Profile name")]);
181            value["confirmation_bypass_arg"] = json!("--yes");
182            value
183        },
184        single(
185            "config show",
186            "Print the resolved profile without credentials",
187            "read_only",
188            vec![
189                field("profile", "string"),
190                field("client_id", "string"),
191                field("tenant", "string"),
192                field("read_only", "boolean"),
193                field("config_path", "string"),
194            ],
195        ),
196        single(
197            "config path",
198            "Print the absolute configuration file path",
199            "read_only",
200            vec![],
201        ),
202        single(
203            "whoami",
204            "Show the signed-in Microsoft 365 identity",
205            "read_only",
206            identity,
207        ),
208        offset_paged(
209            "list",
210            "List teams joined by the signed-in user",
211            vec![
212                field("id", "string"),
213                field("displayName", "string"),
214                nullable_field("description", "string"),
215            ],
216        ),
217        {
218            let mut value = offset_paged(
219                "channels list",
220                "List channels in a team",
221                vec![
222                    field("id", "string"),
223                    field("displayName", "string"),
224                    nullable_field("description", "string"),
225                    field("membershipType", "string"),
226                ],
227            );
228            value["args"]
229                .as_array_mut()
230                .unwrap()
231                .insert(0, required_arg("team", "string", "Team ID"));
232            value
233        },
234        paged(
235            "chats list",
236            "List one-to-one, group, and meeting chats",
237            vec![
238                field("id", "string"),
239                field("chatType", "string"),
240                nullable_field("topic", "string"),
241                field("lastUpdatedDateTime", "string"),
242            ],
243        ),
244        {
245            let mut value = paged(
246                "messages list",
247                "List messages from one chat, or from a channel with optional admin-consented access",
248                vec![
249                    field("id", "string"),
250                    field("createdDateTime", "string"),
251                    field("from", "object"),
252                    field("body", "object"),
253                ],
254            );
255            let args = value["args"].as_array_mut().unwrap();
256            args.splice(
257                0..0,
258                [
259                    arg("--chat", "string", "Chat ID"),
260                    arg("--team", "string", "Team ID"),
261                    arg("--channel", "string", "Channel ID"),
262                ],
263            );
264            value
265        },
266        {
267            let mut value = single(
268                "messages send",
269                "Send one plain-text message as the signed-in user",
270                "non_idempotent",
271                vec![
272                    field("id", "string"),
273                    field("createdDateTime", "string"),
274                    nullable_field("webUrl", "string"),
275                ],
276            );
277            value["args"] = json!([
278                arg("--chat", "string", "Chat ID"),
279                arg("--team", "string", "Team ID"),
280                arg("--channel", "string", "Channel ID"),
281                required_arg("--body", "string", "Message body, or - for stdin")
282            ]);
283            value
284        },
285        json!({"name":"tui", "description":"Open the keyboard-first terminal interface", "effects":"read_only", "output_kind":"opaque", "media_type":"text/plain", "requires_tty":true, "args":[arg("--demo", "boolean", "Use deterministic sample data"), arg("--snapshot", "boolean", "Print one ANSI-free demo frame")]}),
286        {
287            let mut value = single(
288                "doctor",
289                "Check configuration, credential storage, Microsoft identity, and Teams provisioning",
290                "read_only",
291                vec![array_field("checks", "object"), field("healthy", "boolean")],
292            );
293            value["args"] = json!([arg(
294                "--offline",
295                "boolean",
296                "Check local state without contacting Microsoft Graph"
297            )]);
298            value
299        },
300        single(
301            "capabilities",
302            "Describe supported and deliberately unsupported capabilities",
303            "read_only",
304            vec![
305                array_field("supported", "string"),
306                array_field("unsupported", "string"),
307                array_field("account_types", "string"),
308                field("api", "string"),
309                array_field("notes", "string"),
310            ],
311        ),
312        json!({
313            "name":"schema",
314            "description":"Emit the offline CLI Spec v0.3 contract",
315            "effects":"read_only",
316            "cardinality":"single",
317            "stdout_schema":{},
318            "args":[arg("--command", "string", "Return only one complete command path")]
319        }),
320        json!({"name":"completions", "description":"Generate a shell completion script", "effects":"read_only", "output_kind":"opaque", "media_type":"text/plain", "args":[arg("shell", "string", "Shell name")]}),
321    ];
322    if let Some(filter) = command_filter {
323        commands.retain(|value| value["name"] == filter);
324    }
325    json!({
326        "$schema": "https://clispec.dev/schema/v0.3.json",
327        "clispec": "0.3",
328        "name": "teams",
329        "version": env!("CARGO_PKG_VERSION"),
330        "description": "A Microsoft Teams CLI and TUI designed equally for humans and agents",
331        "output": {"tty": "text", "piped": "json"},
332        "global_args": [
333            {"name":"--output", "short":"-o", "type":"string", "enum":["auto","text","json"], "default":"auto", "description":"Output format"},
334            {"name":"--profile", "type":"string", "description":"Configuration profile"},
335            {"name":"--quiet", "type":"boolean", "description":"Suppress informational stderr output"},
336            {"name":"--yes", "short":"-y", "type":"boolean", "description":"Skip confirmation prompts for destructive operations"}
337        ],
338        "commands": commands,
339        "errors": error::ALL.iter().map(|e| json!({"kind":e.kind,"exit_code":e.exit_code,"retryable":e.retryable,"description":e.description})).collect::<Vec<_>>(),
340        "extensions": {
341            "authentication":"delegated_oauth",
342            "api":"Microsoft Graph v1.0",
343            "default_client_id": crate::config::DEFAULT_CLIENT_ID,
344            "login_modes":["browser_pkce","device_code"],
345            "read_only_env":"TEAMS_READ_ONLY",
346            "baseline_scopes": crate::auth::BASE_SCOPES.split_whitespace().collect::<Vec<_>>(),
347            "privileged_scopes":[crate::auth::CHANNEL_HISTORY_SCOPE]
348        }
349    })
350}