1use serde::{Deserialize, Serialize};
2
3#[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#[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#[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 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#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct ExitCodeInfo {
289 pub code: i32,
290 pub description: String,
291}
292
293#[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#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct ExampleInfo {
305 pub description: String,
306 pub command: String,
307}
308
309#[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}