Skip to main content

zoom_cli/commands/
mod.rs

1pub mod config;
2pub mod init;
3pub mod meetings;
4pub mod recordings;
5pub mod reports;
6pub mod users;
7pub mod webinars;
8
9fn schema_doc() -> serde_json::Value {
10    serde_json::json!({
11        "clispec": "0.2",
12        "name": "zoom",
13        "version": env!("CARGO_PKG_VERSION"),
14        "description": "CLI for the Zoom API",
15        "global_args": [
16            {
17                "name": "--output",
18                "type": "string",
19                "description": "Output format: auto (default), text, json",
20                "default": "auto"
21            },
22            {
23                "name": "--quiet",
24                "type": "boolean",
25                "description": "Suppress non-data output"
26            },
27            {
28                "name": "--profile",
29                "type": "string",
30                "description": "Config profile to use"
31            },
32            {
33                "name": "--json",
34                "type": "boolean",
35                "description": "Output as JSON (alias for --output=json)"
36            }
37        ],
38        "commands": [
39            {
40                "name": "meetings list",
41                "description": "List meetings for a user",
42                "mutating": false,
43                "output_fields": [
44                    {"name": "id", "type": "integer"},
45                    {"name": "topic", "type": "string"},
46                    {"name": "start_time", "type": "string"},
47                    {"name": "duration", "type": "integer"},
48                    {"name": "join_url", "type": "string"},
49                    {"name": "status", "type": "string"}
50                ],
51                "args": [
52                    {"name": "--user", "type": "string", "default": "me", "description": "User ID or 'me'"},
53                    {"name": "--type", "type": "string", "description": "Filter: scheduled|live|upcoming"},
54                    {"name": "--limit", "type": "integer", "description": "Maximum number of results"},
55                    {"name": "--offset", "type": "integer", "description": "Number of results to skip"},
56                    {"name": "--fields", "type": "string[]", "description": "Fields to include in output"}
57                ]
58            },
59            {
60                "name": "meetings get",
61                "description": "Get a meeting by ID",
62                "mutating": false,
63                "output_fields": [
64                    {"name": "id", "type": "integer"},
65                    {"name": "topic", "type": "string"},
66                    {"name": "start_time", "type": "string"},
67                    {"name": "duration", "type": "integer"},
68                    {"name": "join_url", "type": "string"},
69                    {"name": "start_url", "type": "string"},
70                    {"name": "status", "type": "string"},
71                    {"name": "password", "type": "string"}
72                ],
73                "args": [
74                    {"name": "id", "type": "integer", "required": true, "description": "Meeting ID"}
75                ]
76            },
77            {
78                "name": "meetings create",
79                "description": "Create a meeting",
80                "mutating": true,
81                "output_fields": [
82                    {"name": "id", "type": "integer"},
83                    {"name": "topic", "type": "string"},
84                    {"name": "join_url", "type": "string"},
85                    {"name": "start_url", "type": "string"}
86                ],
87                "args": [
88                    {"name": "--topic", "type": "string", "required": true, "description": "Meeting topic"},
89                    {"name": "--duration", "type": "integer", "description": "Duration in minutes"},
90                    {"name": "--start", "type": "string", "description": "Start time (ISO 8601)"},
91                    {"name": "--password", "type": "string", "description": "Meeting password"}
92                ]
93            },
94            {
95                "name": "meetings update",
96                "description": "Update a meeting",
97                "mutating": true,
98                "args": [
99                    {"name": "id", "type": "integer", "required": true, "description": "Meeting ID"},
100                    {"name": "--topic", "type": "string", "description": "New topic"},
101                    {"name": "--duration", "type": "integer", "description": "New duration in minutes"},
102                    {"name": "--start", "type": "string", "description": "New start time (ISO 8601)"}
103                ]
104            },
105            {
106                "name": "meetings delete",
107                "description": "Delete a meeting",
108                "mutating": true,
109                "args": [
110                    {"name": "id", "type": "integer", "required": true, "description": "Meeting ID"},
111                    {"name": "--yes", "type": "boolean", "default": false, "description": "Skip confirmation prompt"}
112                ]
113            },
114            {
115                "name": "meetings end",
116                "description": "End a live meeting",
117                "mutating": true,
118                "args": [
119                    {"name": "id", "type": "integer", "required": true, "description": "Meeting ID"}
120                ]
121            },
122            {
123                "name": "meetings participants",
124                "description": "List participants from a past meeting",
125                "mutating": false,
126                "output_fields": [
127                    {"name": "name", "type": "string"},
128                    {"name": "user_email", "type": "string"},
129                    {"name": "join_time", "type": "string"},
130                    {"name": "leave_time", "type": "string"},
131                    {"name": "duration", "type": "integer"}
132                ],
133                "args": [
134                    {"name": "meeting_id", "type": "string", "required": true, "description": "Meeting ID or UUID"}
135                ]
136            },
137            {
138                "name": "meetings invite",
139                "description": "Get meeting invitation text",
140                "mutating": false,
141                "output_fields": [
142                    {"name": "invitation", "type": "string"}
143                ],
144                "args": [
145                    {"name": "id", "type": "integer", "required": true, "description": "Meeting ID"}
146                ]
147            },
148            {
149                "name": "recordings list",
150                "description": "List cloud recordings for a user",
151                "mutating": false,
152                "output_fields": [
153                    {"name": "id", "type": "integer"},
154                    {"name": "topic", "type": "string"},
155                    {"name": "start_time", "type": "string"},
156                    {"name": "duration", "type": "integer"},
157                    {"name": "total_size", "type": "integer"},
158                    {"name": "recording_files", "type": "array"}
159                ],
160                "args": [
161                    {"name": "--user", "type": "string", "default": "me", "description": "User ID or 'me'"},
162                    {"name": "--from", "type": "string", "description": "Start date (YYYY-MM-DD)"},
163                    {"name": "--to", "type": "string", "description": "End date (YYYY-MM-DD)"},
164                    {"name": "--limit", "type": "integer", "description": "Maximum number of results"},
165                    {"name": "--offset", "type": "integer", "description": "Number of results to skip"},
166                    {"name": "--fields", "type": "string[]", "description": "Fields to include in output"}
167                ]
168            },
169            {
170                "name": "recordings get",
171                "description": "Get recording details for a meeting",
172                "mutating": false,
173                "output_fields": [
174                    {"name": "id", "type": "integer"},
175                    {"name": "topic", "type": "string"},
176                    {"name": "start_time", "type": "string"},
177                    {"name": "duration", "type": "integer"},
178                    {"name": "total_size", "type": "integer"},
179                    {"name": "recording_files", "type": "array"}
180                ],
181                "args": [
182                    {"name": "meeting_id", "type": "string", "required": true, "description": "Meeting ID or UUID"}
183                ]
184            },
185            {
186                "name": "recordings download",
187                "description": "Download recording files for a meeting",
188                "mutating": false,
189                "args": [
190                    {"name": "meeting_id", "type": "string", "required": true, "description": "Meeting ID or UUID"},
191                    {"name": "--out", "type": "path", "default": ".", "description": "Output directory"}
192                ]
193            },
194            {
195                "name": "recordings delete",
196                "description": "Delete all cloud recordings for a meeting",
197                "mutating": true,
198                "args": [
199                    {"name": "meeting_id", "type": "string", "required": true, "description": "Meeting ID or UUID"},
200                    {"name": "--permanent", "type": "boolean", "default": false, "description": "Permanently delete instead of moving to trash"},
201                    {"name": "--yes", "type": "boolean", "default": false, "description": "Skip confirmation prompt"}
202                ]
203            },
204            {
205                "name": "recordings start",
206                "description": "Start cloud recording for a live meeting",
207                "mutating": true,
208                "args": [
209                    {"name": "meeting_id", "type": "integer", "required": true, "description": "Numeric meeting ID of the live meeting"}
210                ]
211            },
212            {
213                "name": "recordings stop",
214                "description": "Stop cloud recording for a live meeting",
215                "mutating": true,
216                "args": [
217                    {"name": "meeting_id", "type": "integer", "required": true, "description": "Numeric meeting ID of the live meeting"}
218                ]
219            },
220            {
221                "name": "recordings pause",
222                "description": "Pause cloud recording for a live meeting",
223                "mutating": true,
224                "args": [
225                    {"name": "meeting_id", "type": "integer", "required": true, "description": "Numeric meeting ID of the live meeting"}
226                ]
227            },
228            {
229                "name": "recordings resume",
230                "description": "Resume cloud recording for a live meeting",
231                "mutating": true,
232                "args": [
233                    {"name": "meeting_id", "type": "integer", "required": true, "description": "Numeric meeting ID of the live meeting"}
234                ]
235            },
236            {
237                "name": "recordings transcript",
238                "description": "Download transcript files (VTT/chat) for a meeting",
239                "mutating": false,
240                "args": [
241                    {"name": "meeting_id", "type": "string", "required": true, "description": "Meeting ID or UUID"},
242                    {"name": "--out", "type": "path", "default": ".", "description": "Output directory"}
243                ]
244            },
245            {
246                "name": "users list",
247                "description": "List users in the account",
248                "mutating": false,
249                "output_fields": [
250                    {"name": "id", "type": "string"},
251                    {"name": "email", "type": "string"},
252                    {"name": "display_name", "type": "string"},
253                    {"name": "first_name", "type": "string"},
254                    {"name": "last_name", "type": "string"},
255                    {"name": "status", "type": "string"},
256                    {"name": "type", "type": "integer"}
257                ],
258                "args": [
259                    {"name": "--status", "type": "string", "description": "Filter: active|inactive|pending"},
260                    {"name": "--limit", "type": "integer", "description": "Maximum number of results"},
261                    {"name": "--offset", "type": "integer", "description": "Number of results to skip"},
262                    {"name": "--fields", "type": "string[]", "description": "Fields to include in output"}
263                ]
264            },
265            {
266                "name": "users get",
267                "description": "Get a user by ID or email",
268                "mutating": false,
269                "output_fields": [
270                    {"name": "id", "type": "string"},
271                    {"name": "email", "type": "string"},
272                    {"name": "display_name", "type": "string"},
273                    {"name": "first_name", "type": "string"},
274                    {"name": "last_name", "type": "string"},
275                    {"name": "status", "type": "string"},
276                    {"name": "type", "type": "integer"}
277                ],
278                "args": [
279                    {"name": "id_or_email", "type": "string", "required": true, "description": "User ID or email"}
280                ]
281            },
282            {
283                "name": "users me",
284                "description": "Get the current user",
285                "mutating": false,
286                "output_fields": [
287                    {"name": "id", "type": "string"},
288                    {"name": "email", "type": "string"},
289                    {"name": "display_name", "type": "string"},
290                    {"name": "first_name", "type": "string"},
291                    {"name": "last_name", "type": "string"},
292                    {"name": "status", "type": "string"},
293                    {"name": "type", "type": "integer"}
294                ]
295            },
296            {
297                "name": "users create",
298                "description": "Create a new user",
299                "mutating": true,
300                "output_fields": [
301                    {"name": "id", "type": "string"},
302                    {"name": "email", "type": "string"},
303                    {"name": "first_name", "type": "string"},
304                    {"name": "last_name", "type": "string"},
305                    {"name": "status", "type": "string"}
306                ],
307                "args": [
308                    {"name": "--email", "type": "string", "required": true, "description": "User email"},
309                    {"name": "--first-name", "type": "string", "description": "First name"},
310                    {"name": "--last-name", "type": "string", "description": "Last name"},
311                    {"name": "--type", "type": "integer", "default": 1, "description": "User type: 1=Basic, 2=Licensed, 3=On-prem"}
312                ]
313            },
314            {
315                "name": "users deactivate",
316                "description": "Deactivate a user",
317                "mutating": true,
318                "args": [
319                    {"name": "id_or_email", "type": "string", "required": true, "description": "User ID or email"}
320                ]
321            },
322            {
323                "name": "users activate",
324                "description": "Activate (reactivate) a user",
325                "mutating": true,
326                "args": [
327                    {"name": "id_or_email", "type": "string", "required": true, "description": "User ID or email"}
328                ]
329            },
330            {
331                "name": "webinars list",
332                "description": "List webinars for a user",
333                "mutating": false,
334                "output_fields": [
335                    {"name": "id", "type": "integer"},
336                    {"name": "topic", "type": "string"},
337                    {"name": "start_time", "type": "string"},
338                    {"name": "duration", "type": "integer"},
339                    {"name": "join_url", "type": "string"},
340                    {"name": "status", "type": "string"}
341                ],
342                "args": [
343                    {"name": "--user", "type": "string", "default": "me", "description": "User ID or 'me'"},
344                    {"name": "--limit", "type": "integer", "description": "Maximum number of results"},
345                    {"name": "--offset", "type": "integer", "description": "Number of results to skip"},
346                    {"name": "--fields", "type": "string[]", "description": "Fields to include in output"}
347                ]
348            },
349            {
350                "name": "webinars get",
351                "description": "Get a webinar by ID",
352                "mutating": false,
353                "output_fields": [
354                    {"name": "id", "type": "integer"},
355                    {"name": "topic", "type": "string"},
356                    {"name": "start_time", "type": "string"},
357                    {"name": "duration", "type": "integer"},
358                    {"name": "join_url", "type": "string"},
359                    {"name": "status", "type": "string"},
360                    {"name": "agenda", "type": "string"}
361                ],
362                "args": [
363                    {"name": "id", "type": "integer", "required": true, "description": "Webinar ID"}
364                ]
365            },
366            {
367                "name": "reports meetings",
368                "description": "Meeting summary report for a user",
369                "mutating": false,
370                "output_fields": [
371                    {"name": "id", "type": "integer"},
372                    {"name": "uuid", "type": "string"},
373                    {"name": "topic", "type": "string"},
374                    {"name": "start_time", "type": "string"},
375                    {"name": "duration", "type": "integer"},
376                    {"name": "participants_count", "type": "integer"},
377                    {"name": "total_minutes", "type": "integer"}
378                ],
379                "args": [
380                    {"name": "--user", "type": "string", "default": "me", "description": "User ID or 'me'"},
381                    {"name": "--from", "type": "string", "required": true, "description": "Start date (YYYY-MM-DD)"},
382                    {"name": "--to", "type": "string", "description": "End date (YYYY-MM-DD, default: today)"},
383                    {"name": "--limit", "type": "integer", "description": "Maximum number of results"},
384                    {"name": "--offset", "type": "integer", "description": "Number of results to skip"},
385                    {"name": "--fields", "type": "string[]", "description": "Fields to include in output"}
386                ]
387            },
388            {
389                "name": "reports participants",
390                "description": "Participant report for a past meeting",
391                "mutating": false,
392                "output_fields": [
393                    {"name": "name", "type": "string"},
394                    {"name": "user_email", "type": "string"},
395                    {"name": "join_time", "type": "string"},
396                    {"name": "leave_time", "type": "string"},
397                    {"name": "duration", "type": "integer"}
398                ],
399                "args": [
400                    {"name": "meeting_id", "type": "string", "required": true, "description": "Meeting ID or UUID"}
401                ]
402            },
403            {
404                "name": "config show",
405                "description": "Show current configuration",
406                "mutating": false,
407                "output_fields": [
408                    {"name": "profiles", "type": "array"},
409                    {"name": "active_profile", "type": "string"},
410                    {"name": "env_overrides", "type": "object"}
411                ]
412            },
413            {
414                "name": "config delete",
415                "description": "Delete a profile from the config file",
416                "mutating": true,
417                "args": [
418                    {"name": "profile", "type": "string", "required": true, "description": "Profile name to delete"},
419                    {"name": "--force", "type": "boolean", "default": false, "description": "Skip confirmation prompt"}
420                ]
421            },
422            {
423                "name": "init",
424                "description": "Set up credentials interactively",
425                "mutating": true,
426                "args": [
427                    {"name": "--profile", "type": "string", "description": "Profile name to create or update"}
428                ]
429            },
430            {
431                "name": "schema",
432                "description": "Print machine-readable clispec v0.2 schema",
433                "mutating": false
434            },
435            {
436                "name": "completions",
437                "description": "Generate shell completions",
438                "mutating": false,
439                "args": [
440                    {"name": "shell", "type": "string", "required": true, "description": "Shell to generate completions for"}
441                ]
442            }
443        ],
444        "errors": [
445            {"kind": "auth_error", "exit_code": 2, "retryable": false, "description": "Authentication failed or forbidden"},
446            {"kind": "not_found", "exit_code": 3, "retryable": false, "description": "Resource not found"},
447            {"kind": "invalid_input", "exit_code": 2, "retryable": false, "description": "Invalid user input or missing config"},
448            {"kind": "rate_limit", "exit_code": 1, "retryable": true, "description": "Rate limited by Zoom API"},
449            {"kind": "api_error", "exit_code": 1, "retryable": false, "description": "Non-2xx response from Zoom API"},
450            {"kind": "http_error", "exit_code": 1, "retryable": true, "description": "Network or TLS error"},
451            {"kind": "error", "exit_code": 1, "retryable": false, "description": "Unexpected error"},
452            {"kind": "confirmation_required", "exit_code": 2, "retryable": false, "description": "Destructive operation requires explicit confirmation"},
453            {"kind": "conflict", "exit_code": 1, "retryable": false, "description": "Resource already exists or state conflict"}
454        ]
455    })
456}
457
458pub fn schema() {
459    println!(
460        "{}",
461        serde_json::to_string_pretty(&schema_doc()).expect("serialize")
462    );
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468    use jsonschema;
469
470    #[test]
471    fn schema_output_is_valid_json_with_required_fields() {
472        let doc = schema_doc();
473        assert_eq!(doc["clispec"], "0.2");
474        assert_eq!(doc["name"], "zoom");
475        assert!(!doc["version"].as_str().unwrap_or("").is_empty());
476        assert!(doc["commands"].as_array().unwrap().len() > 0);
477        let errors = doc["errors"].as_array().unwrap();
478        assert!(!errors.is_empty());
479        assert!(
480            errors
481                .iter()
482                .all(|e| e["kind"].is_string() && e["exit_code"].is_number())
483        );
484    }
485
486    #[test]
487    fn schema_output_validates_against_clispec_v0_2_json_schema() {
488        let schema_json = include_str!("../../tests/fixtures/clispec-v0.2.json");
489        let meta_schema: serde_json::Value =
490            serde_json::from_str(schema_json).expect("fixture must be valid JSON");
491        let validator =
492            jsonschema::validator_for(&meta_schema).expect("clispec JSON Schema must compile");
493        let doc = schema_doc();
494        let errors: Vec<String> = validator.iter_errors(&doc).map(|e| e.to_string()).collect();
495        assert!(
496            errors.is_empty(),
497            "schema output does not validate against clispec v0.2:\n{}",
498            errors.join("\n")
499        );
500    }
501
502    #[test]
503    fn schema_errors_include_conflict_kind() {
504        let doc = schema_doc();
505        let errors = doc["errors"].as_array().unwrap();
506        let has_conflict = errors.iter().any(|e| e["kind"] == "conflict");
507        assert!(has_conflict, "errors must include 'conflict' kind");
508    }
509
510    #[test]
511    fn schema_all_commands_have_mutating_field() {
512        let doc = schema_doc();
513        if let Some(cmds) = doc["commands"].as_array() {
514            for cmd in cmds {
515                assert!(
516                    cmd["mutating"].is_boolean(),
517                    "command '{}' must have a boolean mutating field",
518                    cmd["name"].as_str().unwrap_or("?")
519                );
520            }
521        }
522    }
523}