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
5use anyhow::Result;
6use clap::{Parser, Subcommand};
7use console::style;
8use opencode_cloud_core::{Config, InstanceLock, SingletonError, config, get_version, load_config};
9
10/// Manage your opencode cloud service
11#[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    /// Increase verbosity level
21    #[arg(short, long, global = true, action = clap::ArgAction::Count)]
22    verbose: u8,
23
24    /// Suppress non-error output
25    #[arg(short, long, global = true)]
26    quiet: bool,
27
28    /// Disable colored output
29    #[arg(long, global = true)]
30    no_color: bool,
31}
32
33#[derive(Subcommand)]
34enum Commands {
35    /// Manage configuration
36    #[command(subcommand)]
37    Config(ConfigCommands),
38    // Future commands (Phase 3+):
39    // Start - Start the opencode service
40    // Stop - Stop the opencode service
41    // Status - Show service status
42    // Restart - Restart the opencode service
43}
44
45#[derive(Subcommand)]
46enum ConfigCommands {
47    /// Show current configuration
48    Show,
49    /// Set a configuration value
50    Set {
51        /// Configuration key to set
52        key: String,
53        /// Value to set
54        value: String,
55    },
56}
57
58/// Get the ASCII banner for help display
59fn get_banner() -> &'static str {
60    r#"
61  ___  _ __   ___ _ __   ___ ___   __| | ___
62 / _ \| '_ \ / _ \ '_ \ / __/ _ \ / _` |/ _ \
63| (_) | |_) |  __/ | | | (_| (_) | (_| |  __/
64 \___/| .__/ \___|_| |_|\___\___/ \__,_|\___|
65      |_|                            cloud
66"#
67}
68
69pub fn run() -> Result<()> {
70    // Initialize tracing
71    tracing_subscriber::fmt::init();
72
73    let cli = Cli::parse();
74
75    // Configure color output
76    if cli.no_color {
77        console::set_colors_enabled(false);
78    }
79
80    // Load config (creates default if missing)
81    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 config was just created, inform the user
87            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            // Display rich error for invalid config
98            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    // Show verbose info if requested
117    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            // No command - show a welcome message and hint to use --help
133            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/// Acquire the singleton lock for service management commands
148///
149/// This should be called before any command that manages the service
150/// (start, stop, restart, status, etc.) to ensure only one instance runs.
151/// Config commands don't need the lock as they're read-only or file-based.
152#[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/// Display a rich error message when another instance is already running
162#[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            // Display current config as formatted JSON
221            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            // Placeholder - not yet implemented
228            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}