#![deny(clippy::unwrap_used, clippy::expect_used)] #![allow(clippy::cognitive_complexity)] #![allow(clippy::redundant_pattern_matching)] //! FXT -- Foxhunt Operations Platform CLI //! //! Thin gRPC client that talks to the API Gateway for all operations: //! trading, training, tuning, risk, services, cluster, data, and more. use anyhow::Result; use clap::{Parser, Subcommand}; use fxt::auth::token_manager::{AuthTokenManager, FileTokenStorage}; use fxt::auth::LoginClient; use fxt::commands::{ agent, auth, backtest, broker, cluster, config, data, mcp, model, risk, service, trade, train, tune, watch, }; use fxt::grpc::FoxhuntClient; use fxt::output::OutputFormat; /// Foxhunt Operations Platform #[derive(Parser)] #[command(name = "fxt", about = "Foxhunt Operations Platform", version)] struct Cli { /// API Gateway endpoint URL #[arg( long = "api-url", env = "FXT_API_URL", default_value = "https://api.fxhnt.ai" )] api_url: String, /// Output as JSON (for CI/LLM integration) #[arg(long, short)] json: bool, #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { /// Authentication Auth(auth::AuthCommand), /// Order management Trade(trade::TradeCommand), /// ML training lifecycle Train(train::TrainCommand), /// Hyperparameter optimization Tune(tune::TuneCommand), /// Model management Model(model::ModelCommand), /// Trading agent control Agent(agent::AgentCommand), /// Backtesting Backtest(backtest::BacktestCommand), /// Broker connectivity Broker(broker::BrokerCommand), /// Data pipeline management Data(data::DataCommand), /// Service operations Service(service::ServiceCommand), /// Cluster operations Cluster(cluster::ClusterCommand), /// Risk management Risk(risk::RiskCommand), /// System configuration Config(config::ConfigCommand), /// TUI cockpit dashboard Watch(watch::WatchCommand), /// MCP server mode Mcp(mcp::McpCommand), } #[tokio::main] async fn main() -> Result<()> { let cli = Cli::parse(); let format = if cli.json { OutputFormat::Json } else { OutputFormat::Human }; // Commands that do not need a gRPC connection. match &cli.command { Commands::Watch(cmd) => return cmd.execute().await, Commands::Mcp(cmd) => return cmd.execute().await, Commands::Auth(_) | Commands::Trade(_) | Commands::Train(_) | Commands::Tune(_) | Commands::Model(_) | Commands::Agent(_) | Commands::Backtest(_) | Commands::Broker(_) | Commands::Data(_) | Commands::Service(_) | Commands::Cluster(_) | Commands::Risk(_) | Commands::Config(_) => {} } // Connect to API Gateway (lazy -- no TCP until first RPC). let client = FoxhuntClient::connect(&cli.api_url).await?; // Auto-refresh: if the access token is expired but a refresh token exists, // silently regenerate tokens before dispatching the command. // Skip for auth commands (login/logout handle their own token lifecycle). if !matches!(cli.command, Commands::Auth(_)) { let storage = FileTokenStorage::new()?; let manager = AuthTokenManager::new(storage); if manager.needs_refresh().await { let login_client = LoginClient::new(client.channel()); if login_client.silent_login(&manager).await? { eprintln!("Token refreshed."); } } } match &cli.command { Commands::Auth(cmd) => cmd.execute(&client, format).await, Commands::Trade(cmd) => cmd.execute(&client, format).await, Commands::Train(cmd) => cmd.execute(&client, format).await, Commands::Tune(cmd) => cmd.execute(&client, format).await, Commands::Model(cmd) => cmd.execute(&client, format).await, Commands::Agent(cmd) => cmd.execute(&client, format).await, Commands::Backtest(cmd) => cmd.execute(&client, format).await, Commands::Broker(cmd) => cmd.execute(&client, format).await, Commands::Data(cmd) => cmd.execute(&client, format).await, Commands::Service(cmd) => cmd.execute(&client, format).await, Commands::Cluster(cmd) => cmd.execute(&client, format).await, Commands::Risk(cmd) => cmd.execute(&client, format).await, Commands::Config(cmd) => cmd.execute(&client, format).await, // Already handled above -- unreachable. Commands::Watch(_) | Commands::Mcp(_) => Ok(()), } } #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used)] mod tests { use super::*; #[test] fn test_cli_parse_auth_status() { let cli = Cli::parse_from(["fxt", "auth", "status"]); assert!(!cli.json); assert_eq!(cli.api_url, "https://api.fxhnt.ai"); } #[test] fn test_cli_parse_json_flag() { let cli = Cli::parse_from(["fxt", "--json", "auth", "status"]); assert!(cli.json); } #[test] fn test_cli_parse_api_url_override() { let cli = Cli::parse_from(["fxt", "--api-url", "http://localhost:9090", "auth", "login"]); assert_eq!(cli.api_url, "http://localhost:9090"); } #[test] fn test_cli_parse_trade_submit() { let cli = Cli::parse_from([ "fxt", "trade", "submit", "--symbol", "ES.FUT", "--side", "buy", "--qty", "1", ]); assert!(matches!(cli.command, Commands::Trade(_))); } #[test] fn test_cli_parse_service_list() { let cli = Cli::parse_from(["fxt", "service", "list"]); assert!(matches!(cli.command, Commands::Service(_))); } #[test] fn test_cli_parse_watch() { let cli = Cli::parse_from(["fxt", "watch"]); assert!(matches!(cli.command, Commands::Watch(_))); } #[test] fn test_cli_parse_all_subcommands() { // Verify every top-level subcommand parses without error. let commands = [ vec!["fxt", "auth", "status"], vec!["fxt", "trade", "positions"], vec!["fxt", "train", "list"], vec!["fxt", "tune", "status", "some-id"], vec!["fxt", "model", "list"], vec!["fxt", "agent", "status"], vec![ "fxt", "backtest", "run", "--strategy", "s", "--symbol", "ES", "--from", "2025-01-01", "--to", "2025-12-31", ], vec!["fxt", "broker", "status"], vec!["fxt", "data", "status"], vec!["fxt", "service", "health"], vec!["fxt", "cluster", "status"], vec!["fxt", "risk", "status"], vec!["fxt", "config", "env"], vec!["fxt", "watch"], vec!["fxt", "mcp"], ]; for args in &commands { let result = Cli::try_parse_from(args.iter()); assert!( result.is_ok(), "Failed to parse: {:?} -- {}", args, result.err().map_or_else(String::new, |e| e.to_string()) ); } } }