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}