Skip to main content

indodax_cli/
lib.rs

1use clap::{Parser, Subcommand};
2use output::{CommandOutput, OutputFormat};
3
4pub mod auth;
5pub mod client;
6pub mod commands;
7pub mod config;
8pub mod errors;
9pub mod mcp;
10pub mod output;
11
12use client::IndodaxClient;
13use errors::IndodaxError;
14
15#[derive(Debug, Parser)]
16#[command(
17    name = "indodax",
18    version,
19    about = "Command-line interface for the Indodax cryptocurrency exchange",
20    long_about = None
21)]
22pub struct Cli {
23    #[command(subcommand)]
24    pub command: Command,
25
26    #[arg(short = 'o', long = "output", default_value = "table", help = "Output format: table or json")]
27    pub output: OutputFormat,
28
29    #[arg(long = "api-key", help = "API key (overrides config file and env var)")]
30    pub api_key: Option<String>,
31
32    #[arg(long = "api-secret", help = "API secret (overrides config file and env var)")]
33    pub api_secret: Option<String>,
34
35    #[arg(short = 'v', long = "verbose", help = "Enable verbose output")]
36    pub verbose: bool,
37}
38
39#[derive(Debug, Subcommand)]
40pub enum Command {
41    #[command(name = "market", about = "Public market data")]
42    Market {
43        #[command(subcommand)]
44        cmd: commands::market::MarketCommand,
45    },
46
47    #[command(name = "account", about = "Account information and balances")]
48    Account {
49        #[command(subcommand)]
50        cmd: commands::account::AccountCommand,
51    },
52
53    #[command(name = "trade", about = "Place and manage orders")]
54    Trade {
55        #[command(subcommand)]
56        cmd: commands::trade::TradeCommand,
57    },
58
59    #[command(name = "funding", about = "Deposit and withdrawal operations")]
60    Funding {
61        #[command(subcommand)]
62        cmd: commands::funding::FundingCommand,
63    },
64
65    #[command(name = "ws", about = "WebSocket streaming")]
66    Ws {
67        #[command(subcommand)]
68        cmd: commands::websocket::WebSocketCommand,
69    },
70
71    #[command(name = "paper", about = "Paper trading (simulated)")]
72    Paper {
73        #[command(subcommand)]
74        cmd: commands::paper::PaperCommand,
75    },
76
77    #[command(name = "auth", about = "Manage API credentials")]
78    Auth {
79        #[command(subcommand)]
80        cmd: commands::auth::AuthCommand,
81    },
82
83    #[command(name = "alert", about = "Price alert management")]
84    Alert {
85        #[command(subcommand)]
86        cmd: commands::alert::AlertCommand,
87    },
88
89    #[command(name = "setup", about = "Interactive setup wizard")]
90    Setup,
91
92    #[command(name = "shell", about = "Start interactive REPL")]
93    Shell,
94
95    #[command(name = "mcp", about = "Start MCP stdio server for AI agent integration")]
96    Mcp {
97        #[arg(short = 's', long = "groups", default_value = "market,account,paper,auth", help = "Comma-separated service groups: market, account, trade, funding, paper, auth")]
98        groups: String,
99        #[arg(long, help = "Allow dangerous operations (trade, funding) without acknowledged flag")]
100        allow_dangerous: bool,
101    },
102}
103
104pub async fn dispatch(
105    cli: Cli,
106    client: &IndodaxClient,
107    config: &mut config::IndodaxConfig,
108) -> Result<CommandOutput, IndodaxError> {
109    let output = match cli.command {
110        Command::Market { cmd } => commands::market::execute(client, &cmd).await
111            .map_err(map_anyhow_error)?,
112        Command::Account { cmd } => commands::account::execute(client, &cmd).await
113            .map_err(map_anyhow_error)?,
114        Command::Trade { cmd } => commands::trade::execute(client, &cmd).await
115            .map_err(map_anyhow_error)?,
116        Command::Funding { cmd } => commands::funding::execute(client, config, &cmd, cli.output).await
117            .map_err(map_anyhow_error)?,
118        Command::Ws { cmd } => commands::websocket::execute(client, &cmd, cli.output).await
119            .map_err(map_anyhow_error)?,
120        Command::Paper { cmd } => commands::paper::execute(client, config, &cmd).await?,
121        Command::Auth { cmd } => commands::auth::execute(client, config, &cmd).await
122            .map_err(map_anyhow_error)?,
123        Command::Alert { cmd } => commands::alert::execute(client, &None, &cmd).await
124            .map_err(map_anyhow_error)?,
125        Command::Setup | Command::Shell | Command::Mcp { .. } => {
126            return Err(IndodaxError::Other("This command is handled separately".into()));
127        }
128    };
129
130    Ok(output.with_format(cli.output))
131}
132
133pub fn map_anyhow_error(e: anyhow::Error) -> IndodaxError {
134    e.downcast::<IndodaxError>()
135        .unwrap_or_else(|e| IndodaxError::Other(e.to_string()))
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_cli_parse_market_ticker() {
144        let args = vec!["indodax", "market", "ticker", "btc_idr"];
145        let cli = Cli::try_parse_from(args).unwrap();
146        match cli.command {
147            Command::Market { cmd: _ } => {
148                // Just verify it parsed
149            }
150            _ => assert!(false, "Expected Market command, got {:?}", cli.command),
151        }
152    }
153
154    #[test]
155    fn test_cli_parse_output_json() {
156        let args = vec!["indodax", "-o", "json", "market", "ticker"];
157        let cli = Cli::try_parse_from(args).unwrap();
158        assert_eq!(cli.output, OutputFormat::Json);
159    }
160
161    #[test]
162    fn test_cli_parse_api_key() {
163        let args = vec!["indodax", "--api-key", "mykey", "market", "ticker"];
164        let cli = Cli::try_parse_from(args).unwrap();
165        assert_eq!(cli.api_key, Some("mykey".into()));
166    }
167
168    #[test]
169    fn test_cli_parse_api_secret() {
170        let args = vec!["indodax", "--api-secret", "mysecret", "market", "ticker"];
171        let cli = Cli::try_parse_from(args).unwrap();
172        assert_eq!(cli.api_secret, Some("mysecret".into()));
173    }
174
175    #[test]
176    fn test_cli_parse_verbose() {
177        let args = vec!["indodax", "-v", "market", "ticker"];
178        let cli = Cli::try_parse_from(args).unwrap();
179        assert!(cli.verbose);
180    }
181
182    #[test]
183    fn test_command_variants() {
184        let _cmd1 = Command::Market { cmd: crate::commands::market::MarketCommand::ServerTime };
185        let _cmd2 = Command::Account { cmd: crate::commands::account::AccountCommand::Info };
186        let _cmd3 = Command::Trade { cmd: crate::commands::trade::TradeCommand::Buy { 
187            pair: "btc_idr".into(), 
188            idr: 100_000.0, 
189            price: None 
190        }};
191        let _cmd4 = Command::Funding { cmd: crate::commands::funding::FundingCommand::WithdrawFee { 
192            currency: "btc".into(), 
193            network: None 
194        }};
195        let _cmd5 = Command::Ws { cmd: crate::commands::websocket::WebSocketCommand::Ticker { 
196            pair: "btc_idr".into() 
197        }};
198        let _cmd6 = Command::Paper { cmd: crate::commands::paper::PaperCommand::Balance };
199        let _cmd7 = Command::Auth { cmd: crate::commands::auth::AuthCommand::Show };
200        let _cmd8 = Command::Setup;
201        let _cmd9 = Command::Shell;
202        let _cmd10 = Command::Mcp { groups: "market,paper".into(), allow_dangerous: false };
203    }
204
205    #[test]
206    fn test_output_format_clap() {
207        // Test that OutputFormat works with clap
208        let args = vec!["indodax", "-o", "table", "market", "ticker"];
209        let cli = Cli::try_parse_from(args).unwrap();
210        assert_eq!(cli.output, OutputFormat::Table);
211    }
212
213    #[test]
214    fn test_cli_parse_default_output() {
215        let args = vec!["indodax", "market", "ticker"];
216        let cli = Cli::try_parse_from(args).unwrap();
217        assert_eq!(cli.output, OutputFormat::Table);
218    }
219
220    #[test]
221    fn test_command_display() {
222        let cli = Cli::try_parse_from(vec!["indodax", "market", "ticker"]).unwrap();
223        // Just verify the struct can be created
224        let _ = format!("{:?}", cli);
225    }
226}