Skip to main content

xcom_rs/
cli.rs

1use clap::{Parser, Subcommand};
2
3/// X.com CLI tool for agent-friendly interactions
4#[derive(Parser, Debug)]
5#[command(author, version, about, long_about = None, disable_help_subcommand = true)]
6pub struct Cli {
7    /// Output format
8    #[arg(long, global = true, default_value = "text")]
9    pub output: String,
10
11    /// Run in non-interactive mode (no prompts)
12    #[arg(long, global = true)]
13    pub non_interactive: bool,
14
15    /// Trace ID for request correlation
16    #[arg(long, global = true)]
17    pub trace_id: Option<String>,
18
19    /// Log format (json or text)
20    #[arg(long, global = true, default_value = "text")]
21    pub log_format: String,
22
23    /// Maximum cost in credits for a single operation (fail if exceeded)
24    #[arg(long, global = true)]
25    pub max_cost_credits: Option<u32>,
26
27    /// Daily budget in credits (fail if daily total would exceed)
28    #[arg(long, global = true)]
29    pub budget_daily_credits: Option<u32>,
30
31    /// Dry run mode - estimate costs without executing
32    #[arg(long, global = true)]
33    pub dry_run: bool,
34
35    #[command(subcommand)]
36    pub command: Option<Commands>,
37}
38
39#[derive(Subcommand, Debug)]
40pub enum Commands {
41    /// List all available commands with metadata
42    Commands,
43
44    /// Get JSON schema for command input/output
45    Schema {
46        /// Command name to get schema for
47        #[arg(long)]
48        command: String,
49    },
50
51    /// Get detailed help for a command
52    Help {
53        /// Command name to get help for
54        command: String,
55    },
56
57    /// Demo command that requires interaction (for testing non-interactive mode)
58    DemoInteractive,
59
60    /// Tweet operations
61    Tweets {
62        #[command(subcommand)]
63        command: TweetsCommands,
64    },
65
66    /// Authentication commands
67    Auth {
68        #[command(subcommand)]
69        command: AuthCommands,
70    },
71
72    /// Billing commands
73    Billing {
74        #[command(subcommand)]
75        command: BillingCommands,
76    },
77
78    /// Diagnostic information about configuration and runtime state
79    Doctor,
80}
81
82#[derive(Subcommand, Debug)]
83pub enum TweetsCommands {
84    /// Create a new tweet
85    Create {
86        /// Tweet text content
87        text: String,
88
89        /// Client request ID for idempotency (auto-generated if not provided)
90        #[arg(long)]
91        client_request_id: Option<String>,
92
93        /// Policy when operation with same client_request_id exists
94        #[arg(long, default_value = "return")]
95        if_exists: String,
96    },
97
98    /// List tweets
99    List {
100        /// Fields to include in response (comma-separated: id,text,author_id,created_at)
101        #[arg(long)]
102        fields: Option<String>,
103
104        /// Maximum number of tweets to return
105        #[arg(long)]
106        limit: Option<usize>,
107
108        /// Pagination cursor
109        #[arg(long)]
110        cursor: Option<String>,
111    },
112}
113
114#[derive(Subcommand, Debug)]
115pub enum AuthCommands {
116    /// Get current authentication status
117    Status,
118
119    /// Export authentication data
120    Export,
121
122    /// Import authentication data
123    Import {
124        /// Authentication data to import
125        data: String,
126
127        /// Dry run mode - show what would be changed without saving
128        #[arg(long)]
129        dry_run: bool,
130    },
131}
132
133#[derive(Subcommand, Debug)]
134pub enum BillingCommands {
135    /// Estimate cost for an operation
136    Estimate {
137        /// Operation to estimate (e.g., "tweets.create")
138        operation: String,
139
140        /// Optional parameters (key=value format)
141        #[arg(long)]
142        text: Option<String>,
143    },
144
145    /// Get billing report
146    Report,
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_cli_parsing() {
155        let cli = Cli::parse_from(["xcom-rs", "commands"]);
156        assert!(matches!(cli.command, Some(Commands::Commands)));
157    }
158
159    #[test]
160    fn test_cli_with_output_format() {
161        let cli = Cli::parse_from(["xcom-rs", "--output", "json", "commands"]);
162        assert_eq!(cli.output, "json");
163    }
164
165    #[test]
166    fn test_cli_with_trace_id() {
167        let cli = Cli::parse_from(["xcom-rs", "--trace-id", "test-123", "commands"]);
168        assert_eq!(cli.trace_id, Some("test-123".to_string()));
169    }
170
171    #[test]
172    fn test_schema_command() {
173        let cli = Cli::parse_from(["xcom-rs", "schema", "--command", "commands"]);
174        if let Some(Commands::Schema { command, .. }) = cli.command {
175            assert_eq!(command, "commands");
176        } else {
177            panic!("Expected Schema command");
178        }
179    }
180
181    #[test]
182    fn test_help_command() {
183        let cli = Cli::parse_from(["xcom-rs", "help", "commands"]);
184        if let Some(Commands::Help { command }) = cli.command {
185            assert_eq!(command, "commands");
186        } else {
187            panic!("Expected Help command");
188        }
189    }
190
191    #[test]
192    fn test_cli_without_subcommand() {
193        let cli = Cli::parse_from(["xcom-rs"]);
194        assert!(cli.command.is_none());
195    }
196}