1mod commands;
6mod output;
7
8use anyhow::Result;
9use clap::{Parser, Subcommand};
10use console::style;
11use opencode_cloud_core::{Config, InstanceLock, SingletonError, config, get_version, load_config};
12
13#[derive(Parser)]
15#[command(name = "opencode-cloud")]
16#[command(version = env!("CARGO_PKG_VERSION"))]
17#[command(about = "Manage your opencode cloud service", long_about = None)]
18#[command(after_help = get_banner())]
19struct Cli {
20 #[command(subcommand)]
21 command: Option<Commands>,
22
23 #[arg(short, long, global = true, action = clap::ArgAction::Count)]
25 verbose: u8,
26
27 #[arg(short, long, global = true)]
29 quiet: bool,
30
31 #[arg(long, global = true)]
33 no_color: bool,
34}
35
36#[derive(Subcommand)]
37enum Commands {
38 Start(commands::StartArgs),
40 Stop(commands::StopArgs),
42 Restart(commands::RestartArgs),
44 Status(commands::StatusArgs),
46 Logs(commands::LogsArgs),
48 Install(commands::InstallArgs),
50 Uninstall(commands::UninstallArgs),
52 #[command(subcommand)]
54 Config(ConfigCommands),
55}
56
57#[derive(Subcommand)]
58enum ConfigCommands {
59 Show,
61 Set {
63 key: String,
65 value: String,
67 },
68}
69
70fn get_banner() -> &'static str {
72 r#"
73 ___ _ __ ___ _ __ ___ ___ __| | ___
74 / _ \| '_ \ / _ \ '_ \ / __/ _ \ / _` |/ _ \
75| (_) | |_) | __/ | | | (_| (_) | (_| | __/
76 \___/| .__/ \___|_| |_|\___\___/ \__,_|\___|
77 |_| cloud
78"#
79}
80
81pub fn run() -> Result<()> {
82 tracing_subscriber::fmt::init();
84
85 let cli = Cli::parse();
86
87 if cli.no_color {
89 console::set_colors_enabled(false);
90 }
91
92 let config_path = config::paths::get_config_path()
94 .ok_or_else(|| anyhow::anyhow!("Could not determine config path"))?;
95
96 let config = match load_config() {
97 Ok(config) => {
98 if cli.verbose > 0 {
100 eprintln!(
101 "{} Config loaded from: {}",
102 style("[info]").cyan(),
103 config_path.display()
104 );
105 }
106 config
107 }
108 Err(e) => {
109 eprintln!("{} Configuration error", style("Error:").red().bold());
111 eprintln!();
112 eprintln!(" {}", e);
113 eprintln!();
114 eprintln!(" Config file: {}", style(config_path.display()).yellow());
115 eprintln!();
116 eprintln!(
117 " {} Check the config file for syntax errors or unknown fields.",
118 style("Tip:").cyan()
119 );
120 eprintln!(
121 " {} See schemas/config.example.jsonc for valid configuration.",
122 style("Tip:").cyan()
123 );
124 std::process::exit(1);
125 }
126 };
127
128 if cli.verbose > 0 {
130 let data_dir = config::paths::get_data_dir()
131 .map(|p| p.display().to_string())
132 .unwrap_or_else(|| "unknown".to_string());
133 eprintln!(
134 "{} Config: {}",
135 style("[info]").cyan(),
136 config_path.display()
137 );
138 eprintln!("{} Data: {}", style("[info]").cyan(), data_dir);
139 }
140
141 match cli.command {
142 Some(Commands::Start(args)) => {
143 let rt = tokio::runtime::Runtime::new()?;
144 rt.block_on(commands::cmd_start(&args, cli.quiet, cli.verbose))
145 }
146 Some(Commands::Stop(args)) => {
147 let rt = tokio::runtime::Runtime::new()?;
148 rt.block_on(commands::cmd_stop(&args, cli.quiet))
149 }
150 Some(Commands::Restart(args)) => {
151 let rt = tokio::runtime::Runtime::new()?;
152 rt.block_on(commands::cmd_restart(&args, cli.quiet, cli.verbose))
153 }
154 Some(Commands::Status(args)) => {
155 let rt = tokio::runtime::Runtime::new()?;
156 rt.block_on(commands::cmd_status(&args, cli.quiet, cli.verbose))
157 }
158 Some(Commands::Logs(args)) => {
159 let rt = tokio::runtime::Runtime::new()?;
160 rt.block_on(commands::cmd_logs(&args, cli.quiet))
161 }
162 Some(Commands::Install(args)) => {
163 let rt = tokio::runtime::Runtime::new()?;
164 rt.block_on(commands::cmd_install(&args, cli.quiet, cli.verbose))
165 }
166 Some(Commands::Uninstall(args)) => {
167 let rt = tokio::runtime::Runtime::new()?;
168 rt.block_on(commands::cmd_uninstall(&args, cli.quiet, cli.verbose))
169 }
170 Some(Commands::Config(cmd)) => handle_config(cmd, &config),
171 None => {
172 if !cli.quiet {
174 println!(
175 "{} {}",
176 style("opencode-cloud").cyan().bold(),
177 style(get_version()).dim()
178 );
179 println!();
180 println!("Run {} for available commands.", style("--help").green());
181 }
182 Ok(())
183 }
184 }
185}
186
187#[allow(dead_code)]
193fn acquire_singleton_lock() -> Result<InstanceLock, SingletonError> {
194 let pid_path = config::paths::get_data_dir()
195 .ok_or(SingletonError::InvalidPath)?
196 .join("opencode-cloud.pid");
197
198 InstanceLock::acquire(pid_path)
199}
200
201#[allow(dead_code)]
203fn display_singleton_error(err: &SingletonError) {
204 match err {
205 SingletonError::AlreadyRunning(pid) => {
206 eprintln!(
207 "{} Another instance is already running",
208 style("Error:").red().bold()
209 );
210 eprintln!();
211 eprintln!(" Process ID: {}", style(pid).yellow());
212 eprintln!();
213 eprintln!(
214 " {} Stop the existing instance first:",
215 style("Tip:").cyan()
216 );
217 eprintln!(" {} stop", style("opencode-cloud").green());
218 eprintln!();
219 eprintln!(
220 " {} If the process is stuck, kill it manually:",
221 style("Tip:").cyan()
222 );
223 eprintln!(" {} {}", style("kill").green(), pid);
224 }
225 SingletonError::CreateDirFailed(msg) => {
226 eprintln!(
227 "{} Failed to create data directory",
228 style("Error:").red().bold()
229 );
230 eprintln!();
231 eprintln!(" {}", msg);
232 eprintln!();
233 if let Some(data_dir) = config::paths::get_data_dir() {
234 eprintln!(" {} Check permissions for:", style("Tip:").cyan());
235 eprintln!(" {}", style(data_dir.display()).yellow());
236 }
237 }
238 SingletonError::LockFailed(msg) => {
239 eprintln!("{} Failed to acquire lock", style("Error:").red().bold());
240 eprintln!();
241 eprintln!(" {}", msg);
242 }
243 SingletonError::InvalidPath => {
244 eprintln!(
245 "{} Could not determine lock file path",
246 style("Error:").red().bold()
247 );
248 eprintln!();
249 eprintln!(
250 " {} Ensure XDG_DATA_HOME or HOME is set.",
251 style("Tip:").cyan()
252 );
253 }
254 }
255}
256
257fn handle_config(cmd: ConfigCommands, config: &Config) -> Result<()> {
258 match cmd {
259 ConfigCommands::Show => {
260 let json = serde_json::to_string_pretty(config)
262 .map_err(|err| anyhow::anyhow!("Config serialization failed: {err}"))?;
263 println!("{}", json);
264 Ok(())
265 }
266 ConfigCommands::Set { key, value } => {
267 eprintln!(
269 "{} config set is not yet implemented",
270 style("Note:").yellow()
271 );
272 eprintln!();
273 eprintln!(
274 " Attempted to set: {} = {}",
275 style(&key).cyan(),
276 style(&value).green()
277 );
278 eprintln!();
279 eprintln!(" For now, edit the config file directly at:");
280 if let Some(path) = config::paths::get_config_path() {
281 eprintln!(" {}", style(path.display()).yellow());
282 }
283 Ok(())
284 }
285 }
286}