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                ],
77            );
78            value["args"] = json!([
79                arg(
80                    "--client-id",
81                    "string",
82                    "Override the maintained teams-cli public-client application ID"
83                ),
84                arg("--tenant", "string", "Tenant ID, domain, or organizations"),
85                arg(
86                    "--no-login",
87                    "boolean",
88                    "Save configuration without signing in"
89                ),
90                arg("--device-code", "boolean", "Use device-code sign-in"),
91                arg(
92                    "--channel-history",
93                    "boolean",
94                    "Request admin-consented channel-history access"
95                )
96            ]);
97            value
98        },
99        {
100            let mut value = single(
101                "auth login",
102                "Sign in with delegated Microsoft OAuth and protect credentials in the OS store",
103                "idempotent",
104                vec![
105                    field("profile", "string"),
106                    field("expires_at", "integer"),
107                    nullable_field("scope", "string"),
108                    field("channel_history_requested", "boolean"),
109                ],
110            );
111            value["args"] = json!([
112                arg(
113                    "--device-code",
114                    "boolean",
115                    "Use device-code sign-in for headless and remote environments"
116                ),
117                arg(
118                    "--channel-history",
119                    "boolean",
120                    "Request admin-consented channel-history access"
121                )
122            ]);
123            value
124        },
125        single(
126            "auth logout",
127            "Remove locally stored delegated credentials",
128            "idempotent",
129            vec![field("profile", "string"), field("signed_in", "boolean")],
130        ),
131        single(
132            "auth status",
133            "Inspect local configuration and credential presence without network access",
134            "read_only",
135            vec![
136                field("profile", "string"),
137                field("configured", "boolean"),
138                field("signed_in", "boolean"),
139                field("config_path", "string"),
140                nullable_array_field("granted_scopes", "string"),
141                nullable_field("channel_history", "boolean"),
142            ],
143        ),
144        single(
145            "whoami",
146            "Show the signed-in Microsoft 365 identity",
147            "read_only",
148            identity,
149        ),
150        offset_paged(
151            "list",
152            "List teams joined by the signed-in user",
153            vec![
154                field("id", "string"),
155                field("displayName", "string"),
156                nullable_field("description", "string"),
157            ],
158        ),
159        {
160            let mut value = offset_paged(
161                "channels list",
162                "List channels in a team",
163                vec![
164                    field("id", "string"),
165                    field("displayName", "string"),
166                    nullable_field("description", "string"),
167                    field("membershipType", "string"),
168                ],
169            );
170            value["args"]
171                .as_array_mut()
172                .unwrap()
173                .insert(0, required_arg("team", "string", "Team ID"));
174            value
175        },
176        paged(
177            "chats list",
178            "List one-to-one, group, and meeting chats",
179            vec![
180                field("id", "string"),
181                field("chatType", "string"),
182                nullable_field("topic", "string"),
183                field("lastUpdatedDateTime", "string"),
184            ],
185        ),
186        {
187            let mut value = paged(
188                "messages list",
189                "List messages from one chat, or from a channel with optional admin-consented access",
190                vec![
191                    field("id", "string"),
192                    field("createdDateTime", "string"),
193                    field("from", "object"),
194                    field("body", "object"),
195                ],
196            );
197            let args = value["args"].as_array_mut().unwrap();
198            args.splice(
199                0..0,
200                [
201                    arg("--chat", "string", "Chat ID"),
202                    arg("--team", "string", "Team ID"),
203                    arg("--channel", "string", "Channel ID"),
204                ],
205            );
206            value
207        },
208        {
209            let mut value = single(
210                "messages send",
211                "Send one plain-text message as the signed-in user",
212                "non_idempotent",
213                vec![
214                    field("id", "string"),
215                    field("createdDateTime", "string"),
216                    nullable_field("webUrl", "string"),
217                ],
218            );
219            value["args"] = json!([
220                arg("--chat", "string", "Chat ID"),
221                arg("--team", "string", "Team ID"),
222                arg("--channel", "string", "Channel ID"),
223                required_arg("--body", "string", "Message body, or - for stdin")
224            ]);
225            value
226        },
227        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")]}),
228        single(
229            "doctor",
230            "Check configuration, credential storage, Microsoft identity, and Teams provisioning",
231            "read_only",
232            vec![array_field("checks", "object"), field("healthy", "boolean")],
233        ),
234        single(
235            "capabilities",
236            "Describe supported and deliberately unsupported capabilities",
237            "read_only",
238            vec![
239                array_field("supported", "string"),
240                array_field("unsupported", "string"),
241                array_field("account_types", "string"),
242                field("api", "string"),
243                array_field("notes", "string"),
244            ],
245        ),
246        json!({
247            "name":"schema",
248            "description":"Emit the offline CLI Spec v0.3 contract",
249            "effects":"read_only",
250            "cardinality":"single",
251            "stdout_schema":{},
252            "args":[arg("--command", "string", "Return only one complete command path")]
253        }),
254        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")]}),
255    ];
256    if let Some(filter) = command_filter {
257        commands.retain(|value| value["name"] == filter);
258    }
259    json!({
260        "$schema": "https://clispec.dev/schema/v0.3.json",
261        "clispec": "0.3",
262        "name": "teams",
263        "version": env!("CARGO_PKG_VERSION"),
264        "description": "A Microsoft Teams CLI and TUI designed equally for humans and agents",
265        "output": {"tty": "text", "piped": "json"},
266        "global_args": [
267            {"name":"--output", "short":"-o", "type":"string", "enum":["auto","text","json"], "default":"auto", "description":"Output format"},
268            {"name":"--profile", "type":"string", "description":"Configuration profile"},
269            {"name":"--quiet", "type":"boolean", "description":"Suppress informational stderr output"}
270        ],
271        "commands": commands,
272        "errors": error::ALL.iter().map(|e| json!({"kind":e.kind,"exit_code":e.exit_code,"retryable":e.retryable,"description":e.description})).collect::<Vec<_>>(),
273        "extensions": {
274            "authentication":"delegated_oauth",
275            "api":"Microsoft Graph v1.0",
276            "default_client_id": crate::config::DEFAULT_CLIENT_ID,
277            "login_modes":["browser_pkce","device_code"],
278            "baseline_scopes": crate::auth::BASE_SCOPES.split_whitespace().collect::<Vec<_>>(),
279            "privileged_scopes":[crate::auth::CHANNEL_HISTORY_SCOPE]
280        }
281    })
282}