opencode_cloud/
lib.rs

1//! opencode-cloud CLI - Manage your opencode cloud service
2//!
3//! This module contains the shared CLI implementation used by all binaries.
4
5mod 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/// Manage your opencode cloud service
14#[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    /// Increase verbosity level
24    #[arg(short, long, global = true, action = clap::ArgAction::Count)]
25    verbose: u8,
26
27    /// Suppress non-error output
28    #[arg(short, long, global = true)]
29    quiet: bool,
30
31    /// Disable colored output
32    #[arg(long, global = true)]
33    no_color: bool,
34}
35
36#[derive(Subcommand)]
37enum Commands {
38    /// Start the opencode service
39    Start(commands::StartArgs),
40    /// Stop the opencode service
41    Stop(commands::StopArgs),
42    /// Restart the opencode service
43    Restart(commands::RestartArgs),
44    /// Show service status
45    Status(commands::StatusArgs),
46    /// View service logs
47    Logs(commands::LogsArgs),
48    /// Register service to start on boot/login
49    Install(commands::InstallArgs),
50    /// Remove service registration
51    Uninstall(commands::UninstallArgs),
52    /// Manage configuration
53    #[command(subcommand)]
54    Config(ConfigCommands),
55}
56
57#[derive(Subcommand)]
58enum ConfigCommands {
59    /// Show current configuration
60    Show,
61    /// Set a configuration value
62    Set {
63        /// Configuration key to set
64        key: String,
65        /// Value to set
66        value: String,
67    },
68}
69
70/// Get the ASCII banner for help display
71fn get_banner() -> &'static str {
72    r#"
73  ___  _ __   ___ _ __   ___ ___   __| | ___
74 / _ \| '_ \ / _ \ '_ \ / __/ _ \ / _` |/ _ \
75| (_) | |_) |  __/ | | | (_| (_) | (_| |  __/
76 \___/| .__/ \___|_| |_|\___\___/ \__,_|\___|
77      |_|                            cloud
78"#
79}
80
81pub fn run() -> Result<()> {
82    // Initialize tracing
83    tracing_subscriber::fmt::init();
84
85    let cli = Cli::parse();
86
87    // Configure color output
88    if cli.no_color {
89        console::set_colors_enabled(false);
90    }
91
92    // Load config (creates default if missing)
93    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 config was just created, inform the user
99            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            // Display rich error for invalid config
110            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    // Show verbose info if requested
129    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            // No command - show a welcome message and hint to use --help
173            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/// Acquire the singleton lock for service management commands
188///
189/// This should be called before any command that manages the service
190/// (start, stop, restart, status, etc.) to ensure only one instance runs.
191/// Config commands don't need the lock as they're read-only or file-based.
192#[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/// Display a rich error message when another instance is already running
202#[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            // Display current config as formatted JSON
261            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            // Placeholder - not yet implemented
268            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}