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 arg(name: &str, kind: &str, description: &str) -> Value {
15    json!({"name": name, "type": kind, "description": description})
16}
17fn required_arg(name: &str, kind: &str, description: &str) -> Value {
18    json!({"name": name, "type": kind, "description": description, "required": true})
19}
20
21fn single(name: &str, description: &str, effects: &str, fields: Vec<Value>) -> Value {
22    json!({"name": name, "description": description, "effects": effects, "cardinality": "single", "output_fields": fields})
23}
24
25fn paged(name: &str, description: &str, fields: Vec<Value>) -> Value {
26    json!({
27        "name": name, "description": description, "effects": "read_only", "cardinality": "unbounded",
28        "pagination": {"style": "cursor", "cursor_field": "next_cursor", "cursor_arg": "--cursor", "limit_arg": "--limit"},
29        "fields_arg": "--fields",
30        "args": [
31            arg("--limit", "integer", "Maximum records in this page"),
32            arg("--cursor", "string", "Opaque continuation URL from the previous page"),
33            arg("--fields", "string", "Comma-separated output fields")
34        ],
35        "output_fields": fields
36    })
37}
38
39fn offset_paged(name: &str, description: &str, fields: Vec<Value>) -> Value {
40    json!({
41        "name": name, "description": description, "effects": "read_only", "cardinality": "unbounded",
42        "pagination": {"style": "offset", "offset_arg": "--offset", "limit_arg": "--limit"},
43        "fields_arg": "--fields",
44        "args": [
45            arg("--limit", "integer", "Maximum records in this page"),
46            arg("--offset", "integer", "Number of records to skip"),
47            arg("--fields", "string", "Comma-separated output fields")
48        ],
49        "output_fields": fields
50    })
51}
52
53pub fn generate(command_filter: Option<&str>) -> Value {
54    let identity = vec![
55        field("id", "string"),
56        field("displayName", "string"),
57        field("userPrincipalName", "string"),
58        nullable_field("mail", "string"),
59    ];
60    let mut commands = vec![
61        {
62            let mut value = single(
63                "init",
64                "Configure an Entra public client and optionally sign in",
65                "idempotent",
66                vec![
67                    field("profile", "string"),
68                    field("config_path", "string"),
69                    field("signed_in", "boolean"),
70                ],
71            );
72            value["args"] = json!([
73                arg("--client-id", "string", "Public-client application ID"),
74                arg("--tenant", "string", "Tenant ID, domain, or organizations"),
75                arg(
76                    "--no-login",
77                    "boolean",
78                    "Save configuration without signing in"
79                ),
80                arg("--device-code", "boolean", "Use device-code sign-in")
81            ]);
82            value
83        },
84        {
85            let mut value = single(
86                "auth login",
87                "Sign in with delegated Microsoft OAuth and protect credentials in the OS store",
88                "idempotent",
89                vec![
90                    field("profile", "string"),
91                    field("expires_at", "integer"),
92                    nullable_field("scope", "string"),
93                ],
94            );
95            value["args"] = json!([arg("--device-code", "boolean", "Use device-code sign-in")]);
96            value
97        },
98        single(
99            "auth logout",
100            "Remove locally stored delegated credentials",
101            "idempotent",
102            vec![field("profile", "string"), field("signed_in", "boolean")],
103        ),
104        single(
105            "auth status",
106            "Inspect local configuration and credential presence without network access",
107            "read_only",
108            vec![
109                field("profile", "string"),
110                field("configured", "boolean"),
111                field("signed_in", "boolean"),
112                field("config_path", "string"),
113            ],
114        ),
115        single(
116            "whoami",
117            "Show the signed-in Microsoft 365 identity",
118            "read_only",
119            identity,
120        ),
121        offset_paged(
122            "list",
123            "List teams joined by the signed-in user",
124            vec![
125                field("id", "string"),
126                field("displayName", "string"),
127                nullable_field("description", "string"),
128            ],
129        ),
130        {
131            let mut value = offset_paged(
132                "channels list",
133                "List channels in a team",
134                vec![
135                    field("id", "string"),
136                    field("displayName", "string"),
137                    nullable_field("description", "string"),
138                    field("membershipType", "string"),
139                ],
140            );
141            value["args"]
142                .as_array_mut()
143                .unwrap()
144                .insert(0, required_arg("team", "string", "Team ID"));
145            value
146        },
147        paged(
148            "chats list",
149            "List one-to-one, group, and meeting chats",
150            vec![
151                field("id", "string"),
152                field("chatType", "string"),
153                nullable_field("topic", "string"),
154                field("lastUpdatedDateTime", "string"),
155            ],
156        ),
157        {
158            let mut value = paged(
159                "messages list",
160                "List messages from one chat or channel",
161                vec![
162                    field("id", "string"),
163                    field("createdDateTime", "string"),
164                    field("from", "object"),
165                    field("body", "object"),
166                ],
167            );
168            let args = value["args"].as_array_mut().unwrap();
169            args.splice(
170                0..0,
171                [
172                    arg("--chat", "string", "Chat ID"),
173                    arg("--team", "string", "Team ID"),
174                    arg("--channel", "string", "Channel ID"),
175                ],
176            );
177            value
178        },
179        {
180            let mut value = single(
181                "messages send",
182                "Send one plain-text message as the signed-in user",
183                "non_idempotent",
184                vec![
185                    field("id", "string"),
186                    field("createdDateTime", "string"),
187                    nullable_field("webUrl", "string"),
188                ],
189            );
190            value["args"] = json!([
191                arg("--chat", "string", "Chat ID"),
192                arg("--team", "string", "Team ID"),
193                arg("--channel", "string", "Channel ID"),
194                required_arg("--body", "string", "Message body, or - for stdin")
195            ]);
196            value
197        },
198        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")]}),
199        single(
200            "doctor",
201            "Check configuration, credential storage, and Graph access",
202            "read_only",
203            vec![array_field("checks", "object"), field("healthy", "boolean")],
204        ),
205        single(
206            "capabilities",
207            "Describe supported and deliberately unsupported capabilities",
208            "read_only",
209            vec![
210                array_field("supported", "string"),
211                array_field("unsupported", "string"),
212                array_field("account_types", "string"),
213                field("api", "string"),
214                array_field("notes", "string"),
215            ],
216        ),
217        json!({
218            "name":"schema",
219            "description":"Emit the offline CLI Spec v0.3 contract",
220            "effects":"read_only",
221            "cardinality":"single",
222            "stdout_schema":{},
223            "args":[arg("--command", "string", "Return only one complete command path")]
224        }),
225        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")]}),
226    ];
227    if let Some(filter) = command_filter {
228        commands.retain(|value| value["name"] == filter);
229    }
230    json!({
231        "$schema": "https://clispec.dev/schema/v0.3.json",
232        "clispec": "0.3",
233        "name": "teams",
234        "version": env!("CARGO_PKG_VERSION"),
235        "description": "A Microsoft Teams CLI and TUI designed equally for humans and agents",
236        "output": {"tty": "text", "piped": "json"},
237        "global_args": [
238            {"name":"--output", "short":"-o", "type":"string", "enum":["auto","text","json"], "default":"auto", "description":"Output format"},
239            {"name":"--profile", "type":"string", "description":"Configuration profile"},
240            {"name":"--quiet", "type":"boolean", "description":"Suppress informational stderr output"}
241        ],
242        "commands": commands,
243        "errors": error::ALL.iter().map(|e| json!({"kind":e.kind,"exit_code":e.exit_code,"retryable":e.retryable,"description":e.description})).collect::<Vec<_>>(),
244        "extensions": {"authentication":"delegated_oauth", "api":"Microsoft Graph v1.0"}
245    })
246}