Skip to main content

xcom_rs/
introspection.rs

1use serde::{Deserialize, Serialize};
2
3/// Command metadata for introspection
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct CommandInfo {
6    pub name: String,
7    pub description: String,
8    pub arguments: Vec<ArgumentInfo>,
9    pub risk: RiskLevel,
10    #[serde(rename = "hasCost")]
11    pub has_cost: bool,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ArgumentInfo {
16    pub name: String,
17    pub description: String,
18    pub required: bool,
19    #[serde(rename = "type")]
20    pub arg_type: String,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub default: Option<String>,
23}
24
25#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
26#[serde(rename_all = "lowercase")]
27pub enum RiskLevel {
28    Safe,
29    Low,
30    Medium,
31    High,
32}
33
34/// List of all available commands
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct CommandsList {
37    pub commands: Vec<CommandInfo>,
38}
39
40impl CommandsList {
41    pub fn new() -> Self {
42        Self {
43            commands: vec![
44                CommandInfo {
45                    name: "commands".to_string(),
46                    description: "List all available commands with metadata".to_string(),
47                    arguments: vec![],
48                    risk: RiskLevel::Safe,
49                    has_cost: false,
50                },
51                CommandInfo {
52                    name: "schema".to_string(),
53                    description: "Get JSON schema for command input/output".to_string(),
54                    arguments: vec![ArgumentInfo {
55                        name: "command".to_string(),
56                        description: "Command name to get schema for".to_string(),
57                        required: true,
58                        arg_type: "string".to_string(),
59                        default: None,
60                    }],
61                    risk: RiskLevel::Safe,
62                    has_cost: false,
63                },
64                CommandInfo {
65                    name: "help".to_string(),
66                    description: "Get detailed help for a command including exit codes".to_string(),
67                    arguments: vec![ArgumentInfo {
68                        name: "command".to_string(),
69                        description: "Command name to get help for".to_string(),
70                        required: true,
71                        arg_type: "string".to_string(),
72                        default: None,
73                    }],
74                    risk: RiskLevel::Safe,
75                    has_cost: false,
76                },
77                CommandInfo {
78                    name: "demo-interactive".to_string(),
79                    description:
80                        "Demo command that requires interaction (for testing non-interactive mode)"
81                            .to_string(),
82                    arguments: vec![],
83                    risk: RiskLevel::Low,
84                    has_cost: false,
85                },
86            ],
87        }
88    }
89}
90
91impl Default for CommandsList {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97/// JSON Schema for a command
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct CommandSchema {
100    pub command: String,
101    #[serde(rename = "inputSchema")]
102    pub input_schema: serde_json::Value,
103    #[serde(rename = "outputSchema")]
104    pub output_schema: serde_json::Value,
105}
106
107impl CommandSchema {
108    /// Helper to create envelope schema wrapping the data schema
109    fn wrap_in_envelope_schema(data_schema: serde_json::Value) -> serde_json::Value {
110        serde_json::json!({
111            "type": "object",
112            "required": ["ok", "type", "schemaVersion"],
113            "properties": {
114                "ok": { "type": "boolean" },
115                "type": { "type": "string" },
116                "schemaVersion": { "type": "integer", "const": 1 },
117                "data": data_schema,
118                "error": {
119                    "type": "object",
120                    "required": ["code", "message", "isRetryable"],
121                    "properties": {
122                        "code": { "type": "string" },
123                        "message": { "type": "string" },
124                        "isRetryable": { "type": "boolean" },
125                        "details": { "type": "object" }
126                    }
127                },
128                "meta": {
129                    "type": "object",
130                    "properties": {
131                        "traceId": { "type": "string" }
132                    }
133                }
134            }
135        })
136    }
137
138    pub fn for_command(command: &str) -> Self {
139        match command {
140            "commands" => Self {
141                command: command.to_string(),
142                input_schema: serde_json::json!({
143                    "type": "object",
144                    "properties": {},
145                    "additionalProperties": false
146                }),
147                output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
148                    "type": "object",
149                    "required": ["commands"],
150                    "properties": {
151                        "commands": {
152                            "type": "array",
153                            "items": {
154                                "type": "object",
155                                "required": ["name", "description", "arguments", "risk", "hasCost"],
156                                "properties": {
157                                    "name": { "type": "string" },
158                                    "description": { "type": "string" },
159                                    "arguments": {
160                                        "type": "array",
161                                        "items": {
162                                            "type": "object",
163                                            "required": ["name", "description", "required", "type"],
164                                            "properties": {
165                                                "name": { "type": "string" },
166                                                "description": { "type": "string" },
167                                                "required": { "type": "boolean" },
168                                                "type": { "type": "string" },
169                                                "default": { "type": "string" }
170                                            }
171                                        }
172                                    },
173                                    "risk": {
174                                        "type": "string",
175                                        "enum": ["safe", "low", "medium", "high"]
176                                    },
177                                    "hasCost": { "type": "boolean" }
178                                }
179                            }
180                        }
181                    }
182                })),
183            },
184            "schema" => Self {
185                command: command.to_string(),
186                input_schema: serde_json::json!({
187                    "type": "object",
188                    "required": ["command"],
189                    "properties": {
190                        "command": { "type": "string" }
191                    }
192                }),
193                output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
194                    "type": "object",
195                    "required": ["command", "inputSchema", "outputSchema"],
196                    "properties": {
197                        "command": { "type": "string" },
198                        "inputSchema": { "type": "object" },
199                        "outputSchema": { "type": "object" }
200                    }
201                })),
202            },
203            "help" => Self {
204                command: command.to_string(),
205                input_schema: serde_json::json!({
206                    "type": "object",
207                    "required": ["command"],
208                    "properties": {
209                        "command": { "type": "string" }
210                    }
211                }),
212                output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
213                    "type": "object",
214                    "required": ["command", "description", "usage", "exitCodes", "errorVocabulary", "examples"],
215                    "properties": {
216                        "command": { "type": "string" },
217                        "description": { "type": "string" },
218                        "usage": { "type": "string" },
219                        "exitCodes": {
220                            "type": "array",
221                            "items": {
222                                "type": "object",
223                                "required": ["code", "description"],
224                                "properties": {
225                                    "code": { "type": "integer" },
226                                    "description": { "type": "string" }
227                                }
228                            }
229                        },
230                        "errorVocabulary": {
231                            "type": "array",
232                            "items": {
233                                "type": "object",
234                                "required": ["code", "description", "isRetryable"],
235                                "properties": {
236                                    "code": { "type": "string" },
237                                    "description": { "type": "string" },
238                                    "isRetryable": { "type": "boolean" }
239                                }
240                            }
241                        },
242                        "examples": {
243                            "type": "array",
244                            "items": {
245                                "type": "object",
246                                "required": ["description", "command"],
247                                "properties": {
248                                    "description": { "type": "string" },
249                                    "command": { "type": "string" }
250                                }
251                            }
252                        }
253                    }
254                })),
255            },
256            "demo-interactive" => Self {
257                command: command.to_string(),
258                input_schema: serde_json::json!({
259                    "type": "object",
260                    "properties": {}
261                }),
262                output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
263                    "type": "object",
264                    "required": ["message", "confirmed"],
265                    "properties": {
266                        "message": { "type": "string" },
267                        "confirmed": { "type": "boolean" }
268                    }
269                })),
270            },
271            _ => Self {
272                command: command.to_string(),
273                input_schema: serde_json::json!({
274                    "type": "object",
275                    "properties": {}
276                }),
277                output_schema: Self::wrap_in_envelope_schema(serde_json::json!({
278                    "type": "object",
279                    "properties": {}
280                })),
281            },
282        }
283    }
284}
285
286/// Exit code information
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct ExitCodeInfo {
289    pub code: i32,
290    pub description: String,
291}
292
293/// Error code information
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct ErrorCodeInfo {
296    pub code: String,
297    pub description: String,
298    #[serde(rename = "isRetryable")]
299    pub is_retryable: bool,
300}
301
302/// Example usage of a command
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct ExampleInfo {
305    pub description: String,
306    pub command: String,
307}
308
309/// Detailed help for a command
310#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct CommandHelp {
312    pub command: String,
313    pub description: String,
314    pub usage: String,
315    #[serde(rename = "exitCodes")]
316    pub exit_codes: Vec<ExitCodeInfo>,
317    #[serde(rename = "errorVocabulary")]
318    pub error_vocabulary: Vec<ErrorCodeInfo>,
319    pub examples: Vec<ExampleInfo>,
320}
321
322impl CommandHelp {
323    pub fn for_command(command: &str) -> Self {
324        let exit_codes = vec![
325            ExitCodeInfo {
326                code: 0,
327                description: "Success".to_string(),
328            },
329            ExitCodeInfo {
330                code: 2,
331                description: "Invalid argument or missing required argument".to_string(),
332            },
333            ExitCodeInfo {
334                code: 3,
335                description: "Authentication or authorization failed".to_string(),
336            },
337            ExitCodeInfo {
338                code: 4,
339                description: "Operation failed (network, rate limit, service unavailable, etc.)"
340                    .to_string(),
341            },
342        ];
343
344        let error_vocabulary = vec![
345            ErrorCodeInfo {
346                code: "INVALID_ARGUMENT".to_string(),
347                description: "Invalid argument provided".to_string(),
348                is_retryable: false,
349            },
350            ErrorCodeInfo {
351                code: "MISSING_ARGUMENT".to_string(),
352                description: "Required argument missing".to_string(),
353                is_retryable: false,
354            },
355            ErrorCodeInfo {
356                code: "UNKNOWN_COMMAND".to_string(),
357                description: "Command not recognized".to_string(),
358                is_retryable: false,
359            },
360            ErrorCodeInfo {
361                code: "AUTHENTICATION_FAILED".to_string(),
362                description: "Authentication credentials invalid or expired".to_string(),
363                is_retryable: false,
364            },
365            ErrorCodeInfo {
366                code: "AUTHORIZATION_FAILED".to_string(),
367                description: "Insufficient permissions".to_string(),
368                is_retryable: false,
369            },
370            ErrorCodeInfo {
371                code: "RATE_LIMIT_EXCEEDED".to_string(),
372                description: "Rate limit exceeded, retry after delay".to_string(),
373                is_retryable: true,
374            },
375            ErrorCodeInfo {
376                code: "NETWORK_ERROR".to_string(),
377                description: "Network connection failed".to_string(),
378                is_retryable: true,
379            },
380            ErrorCodeInfo {
381                code: "SERVICE_UNAVAILABLE".to_string(),
382                description: "Service temporarily unavailable".to_string(),
383                is_retryable: true,
384            },
385            ErrorCodeInfo {
386                code: "INTERNAL_ERROR".to_string(),
387                description: "Internal error occurred".to_string(),
388                is_retryable: false,
389            },
390            ErrorCodeInfo {
391                code: "INTERACTION_REQUIRED".to_string(),
392                description: "User interaction required but --non-interactive mode is enabled"
393                    .to_string(),
394                is_retryable: false,
395            },
396        ];
397
398        match command {
399            "commands" => Self {
400                command: command.to_string(),
401                description: "List all available commands with metadata".to_string(),
402                usage: "xcom-rs commands [--output json|yaml|text]".to_string(),
403                exit_codes,
404                error_vocabulary,
405                examples: vec![
406                    ExampleInfo {
407                        description: "List commands in JSON format".to_string(),
408                        command: "xcom-rs commands --output json".to_string(),
409                    },
410                    ExampleInfo {
411                        description: "List commands in text format".to_string(),
412                        command: "xcom-rs commands --output text".to_string(),
413                    },
414                ],
415            },
416            "schema" => Self {
417                command: command.to_string(),
418                description: "Get JSON schema for command input/output".to_string(),
419                usage: "xcom-rs schema --command <name> [--output json|yaml|text]".to_string(),
420                exit_codes,
421                error_vocabulary,
422                examples: vec![
423                    ExampleInfo {
424                        description: "Get schema for commands command".to_string(),
425                        command: "xcom-rs schema --command commands --output json".to_string(),
426                    },
427                    ExampleInfo {
428                        description: "Get schema for help command".to_string(),
429                        command: "xcom-rs schema --command help --output json".to_string(),
430                    },
431                ],
432            },
433            "help" => Self {
434                command: command.to_string(),
435                description: "Get detailed help for a command including exit codes".to_string(),
436                usage: "xcom-rs help <command> [--output json|yaml|text]".to_string(),
437                exit_codes,
438                error_vocabulary,
439                examples: vec![
440                    ExampleInfo {
441                        description: "Get help for commands command".to_string(),
442                        command: "xcom-rs help commands --output json".to_string(),
443                    },
444                    ExampleInfo {
445                        description: "Get help for schema command".to_string(),
446                        command: "xcom-rs help schema --output json".to_string(),
447                    },
448                ],
449            },
450            "demo-interactive" => Self {
451                command: command.to_string(),
452                description:
453                    "Demo command that requires interaction (for testing non-interactive mode)"
454                        .to_string(),
455                usage: "xcom-rs demo-interactive [--non-interactive] [--output json|yaml|text]"
456                    .to_string(),
457                exit_codes,
458                error_vocabulary,
459                examples: vec![
460                    ExampleInfo {
461                        description: "Run in interactive mode".to_string(),
462                        command: "xcom-rs demo-interactive".to_string(),
463                    },
464                    ExampleInfo {
465                        description:
466                            "Run in non-interactive mode (will fail with INTERACTION_REQUIRED)"
467                                .to_string(),
468                        command: "xcom-rs demo-interactive --non-interactive --output json"
469                            .to_string(),
470                    },
471                ],
472            },
473            _ => Self {
474                command: command.to_string(),
475                description: format!("Help for {}", command),
476                usage: format!("xcom-rs {} [options]", command),
477                exit_codes,
478                error_vocabulary,
479                examples: vec![],
480            },
481        }
482    }
483}
484
485#[cfg(test)]
486mod tests {
487    use super::*;
488
489    #[test]
490    fn test_commands_list() {
491        let list = CommandsList::new();
492        assert!(!list.commands.is_empty());
493        assert!(list.commands.iter().any(|c| c.name == "commands"));
494        assert!(list.commands.iter().any(|c| c.name == "schema"));
495        assert!(list.commands.iter().any(|c| c.name == "help"));
496    }
497
498    #[test]
499    fn test_command_schema() {
500        let schema = CommandSchema::for_command("commands");
501        assert_eq!(schema.command, "commands");
502        assert!(schema.input_schema.is_object());
503        assert!(schema.output_schema.is_object());
504    }
505
506    #[test]
507    fn test_command_help() {
508        let help = CommandHelp::for_command("commands");
509        assert_eq!(help.command, "commands");
510        assert!(!help.exit_codes.is_empty());
511        assert!(!help.error_vocabulary.is_empty());
512        assert!(!help.examples.is_empty());
513    }
514}