1use anyhow::Result;
6use clap::{Parser, Subcommand};
7use console::style;
8use opencode_cloud_core::{Config, InstanceLock, SingletonError, config, get_version, load_config};
9
10#[derive(Parser)]
12#[command(name = "opencode-cloud")]
13#[command(version = env!("CARGO_PKG_VERSION"))]
14#[command(about = "Manage your opencode cloud service", long_about = None)]
15#[command(after_help = get_banner())]
16struct Cli {
17 #[command(subcommand)]
18 command: Option<Commands>,
19
20 #[arg(short, long, global = true, action = clap::ArgAction::Count)]
22 verbose: u8,
23
24 #[arg(short, long, global = true)]
26 quiet: bool,
27
28 #[arg(long, global = true)]
30 no_color: bool,
31}
32
33#[derive(Subcommand)]
34enum Commands {
35 #[command(subcommand)]
37 Config(ConfigCommands),
38 }
44
45#[derive(Subcommand)]
46enum ConfigCommands {
47 Show,
49 Set {
51 key: String,
53 value: String,
55 },
56}
57
58fn get_banner() -> &'static str {
60 r#"
61 ___ _ __ ___ _ __ ___ ___ __| | ___
62 / _ \| '_ \ / _ \ '_ \ / __/ _ \ / _` |/ _ \
63| (_) | |_) | __/ | | | (_| (_) | (_| | __/
64 \___/| .__/ \___|_| |_|\___\___/ \__,_|\___|
65 |_| cloud
66"#
67}
68
69pub fn run() -> Result<()> {
70 tracing_subscriber::fmt::init();
72
73 let cli = Cli::parse();
74
75 if cli.no_color {
77 console::set_colors_enabled(false);
78 }
79
80 let config_path = config::paths::get_config_path()
82 .ok_or_else(|| anyhow::anyhow!("Could not determine config path"))?;
83
84 let config = match load_config() {
85 Ok(config) => {
86 if cli.verbose > 0 {
88 eprintln!(
89 "{} Config loaded from: {}",
90 style("[info]").cyan(),
91 config_path.display()
92 );
93 }
94 config
95 }
96 Err(e) => {
97 eprintln!("{} Configuration error", style("Error:").red().bold());
99 eprintln!();
100 eprintln!(" {}", e);
101 eprintln!();
102 eprintln!(" Config file: {}", style(config_path.display()).yellow());
103 eprintln!();
104 eprintln!(
105 " {} Check the config file for syntax errors or unknown fields.",
106 style("Tip:").cyan()
107 );
108 eprintln!(
109 " {} See schemas/config.example.jsonc for valid configuration.",
110 style("Tip:").cyan()
111 );
112 std::process::exit(1);
113 }
114 };
115
116 if cli.verbose > 0 {
118 let data_dir = config::paths::get_data_dir()
119 .map(|p| p.display().to_string())
120 .unwrap_or_else(|| "unknown".to_string());
121 eprintln!(
122 "{} Config: {}",
123 style("[info]").cyan(),
124 config_path.display()
125 );
126 eprintln!("{} Data: {}", style("[info]").cyan(), data_dir);
127 }
128
129 match cli.command {
130 Some(Commands::Config(cmd)) => handle_config(cmd, &config),
131 None => {
132 if !cli.quiet {
134 println!(
135 "{} {}",
136 style("opencode-cloud").cyan().bold(),
137 style(get_version()).dim()
138 );
139 println!();
140 println!("Run {} for available commands.", style("--help").green());
141 }
142 Ok(())
143 }
144 }
145}
146
147#[allow(dead_code)]
153fn acquire_singleton_lock() -> Result<InstanceLock, SingletonError> {
154 let pid_path = config::paths::get_data_dir()
155 .ok_or(SingletonError::InvalidPath)?
156 .join("opencode-cloud.pid");
157
158 InstanceLock::acquire(pid_path)
159}
160
161#[allow(dead_code)]
163fn display_singleton_error(err: &SingletonError) {
164 match err {
165 SingletonError::AlreadyRunning(pid) => {
166 eprintln!(
167 "{} Another instance is already running",
168 style("Error:").red().bold()
169 );
170 eprintln!();
171 eprintln!(" Process ID: {}", style(pid).yellow());
172 eprintln!();
173 eprintln!(
174 " {} Stop the existing instance first:",
175 style("Tip:").cyan()
176 );
177 eprintln!(" {} stop", style("opencode-cloud").green());
178 eprintln!();
179 eprintln!(
180 " {} If the process is stuck, kill it manually:",
181 style("Tip:").cyan()
182 );
183 eprintln!(" {} {}", style("kill").green(), pid);
184 }
185 SingletonError::CreateDirFailed(msg) => {
186 eprintln!(
187 "{} Failed to create data directory",
188 style("Error:").red().bold()
189 );
190 eprintln!();
191 eprintln!(" {}", msg);
192 eprintln!();
193 if let Some(data_dir) = config::paths::get_data_dir() {
194 eprintln!(" {} Check permissions for:", style("Tip:").cyan());
195 eprintln!(" {}", style(data_dir.display()).yellow());
196 }
197 }
198 SingletonError::LockFailed(msg) => {
199 eprintln!("{} Failed to acquire lock", style("Error:").red().bold());
200 eprintln!();
201 eprintln!(" {}", msg);
202 }
203 SingletonError::InvalidPath => {
204 eprintln!(
205 "{} Could not determine lock file path",
206 style("Error:").red().bold()
207 );
208 eprintln!();
209 eprintln!(
210 " {} Ensure XDG_DATA_HOME or HOME is set.",
211 style("Tip:").cyan()
212 );
213 }
214 }
215}
216
217fn handle_config(cmd: ConfigCommands, config: &Config) -> Result<()> {
218 match cmd {
219 ConfigCommands::Show => {
220 let json = serde_json::to_string_pretty(config)
222 .map_err(|err| anyhow::anyhow!("Config serialization failed: {err}"))?;
223 println!("{}", json);
224 Ok(())
225 }
226 ConfigCommands::Set { key, value } => {
227 eprintln!(
229 "{} config set is not yet implemented",
230 style("Note:").yellow()
231 );
232 eprintln!();
233 eprintln!(
234 " Attempted to set: {} = {}",
235 style(&key).cyan(),
236 style(&value).green()
237 );
238 eprintln!();
239 eprintln!(" For now, edit the config file directly at:");
240 if let Some(path) = config::paths::get_config_path() {
241 eprintln!(" {}", style(path.display()).yellow());
242 }
243 Ok(())
244 }
245 }
246}