1 use anyhow::{anyhow, Result};
2 use clap::{Parser, Subcommand};
3 use std::path::PathBuf;
4 use xcrs::{AndroidDebugBridge, AndroidKit, IosAppTest, XcodeCommandLineTools};
5
6 #[derive(Debug, Parser)]
7 #[command(name = "xcrs")]
8 #[command(version)]
9 #[command(about = "Cross-platform mobile and TV app automation")]
10 struct Cli {
11 /// Run as a standalone Model Context Protocol server over stdio.
12 #[arg(long)]
13 mcp: bool,
14 #[command(subcommand)]
15 command: Option<Commands>,
16 }
17
18 #[derive(Debug, Subcommand)]
19 enum Commands {
20 #[command(subcommand)]
21 Simulator(SimulatorCommand),
22 #[command(subcommand)]
23 AndroidKit(AndroidKitCommand),
24 IosAppTest {
25 #[arg(long, conflicts_with = "simulator_udid")]
26 simulator_name: Option<String>,
27 #[arg(long, conflicts_with = "simulator_name")]
28 simulator_udid: Option<String>,
29 #[arg(long)]
30 app_path: PathBuf,
31 #[arg(long)]
32 bundle_id: String,
33 #[arg(long)]
34 terminate_before_launch: bool,
35 #[arg(long)]
36 json: bool,
37 },
38 }
39
40 #[derive(Debug, Subcommand)]
41 enum AndroidKitCommand {
42 Install {
43 #[arg(long)]
44 serial: String,
45 #[arg(long)]
46 apk: PathBuf,
47 },
48 Start {
49 #[arg(long)]
50 serial: String,
51 },
52 Status {
53 #[arg(long)]
54 serial: String,
55 #[arg(long)]
56 json: bool,
57 },
58 Stop {
59 #[arg(long)]
60 serial: String,
61 },
62 }
63
64 #[derive(Debug, Subcommand)]
65 enum SimulatorCommand {
66 Find {
67 #[arg(long)]
68 name: String,
69 #[arg(long)]
70 json: bool,
71 },
72 List {
73 #[arg(long)]
74 json: bool,
75 },
76 }
77
78 #[tokio::main]
79 async fn main() -> Result<()> {
80 let cli = Cli::parse();
81
82 if cli.mcp {
83 return xcrs::mcp::serve().await;
84 }
85
86 let command = cli
87 .command
88 .ok_or_else(|| anyhow!("a command is required unless --mcp is provided"))?;
89 let tools = XcodeCommandLineTools::new();
90
91 match command {
92 Commands::AndroidKit(AndroidKitCommand::Install { serial, apk }) => {
93 AndroidDebugBridge::new().install_androidkit(&serial, &apk)?;
94 println!("Installed XCRS AndroidKit on {serial}");
95 }
96 Commands::AndroidKit(AndroidKitCommand::Start { serial }) => {
97 AndroidDebugBridge::new().start_androidkit(&serial).await?;
98 println!("Started XCRS AndroidKit on {serial}");
99 }
100 Commands::AndroidKit(AndroidKitCommand::Status { serial, json }) => {
101 let info = AndroidKit::new()
102 .call(serial.clone(), "device.info", serde_json::json!({}))
103 .await?;
104 if json {
105 println!("{}", serde_json::to_string_pretty(&info)?);
106 } else {
107 println!("XCRS AndroidKit is ready on {serial}");
108 }
109 }
110 Commands::AndroidKit(AndroidKitCommand::Stop { serial }) => {
111 AndroidDebugBridge::new().stop_androidkit(&serial)?;
112 println!("Stopped XCRS AndroidKit on {serial}");
113 }
114 Commands::Simulator(SimulatorCommand::Find { name, json }) => {
115 let simulator = tools.simctl().find_simulator_by_name(&name)?;
116 if json {
117 println!("{}", serde_json::to_string_pretty(&simulator)?);
118 } else {
119 println!("{} {}", simulator.name, simulator.udid);
120 }
121 }
122 Commands::Simulator(SimulatorCommand::List { json }) => {
123 let simulators = tools.simctl().list_simulators()?;
124 if json {
125 println!("{}", serde_json::to_string_pretty(&simulators)?);
126 } else {
127 for simulator in simulators {
128 println!("{} {} {}", simulator.name, simulator.udid, simulator.state);
129 }
130 }
131 }
132 Commands::IosAppTest {
133 simulator_name,
134 simulator_udid,
135 app_path,
136 bundle_id,
137 terminate_before_launch,
138 json,
139 } => {
140 let result = tools.run_ios_app_test(&IosAppTest {
141 simulator_name,
142 simulator_udid,
143 app_path,
144 bundle_id,
145 terminate_before_launch,
146 })?;
147
148 if json {
149 println!("{}", serde_json::to_string_pretty(&result)?);
150 } else {
151 println!(
152 "Launched {} on {} ({})",
153 result.bundle_id, result.simulator.name, result.simulator.udid
154 );
155 }
156 }
157 }
158
159 Ok(())
160 }