diff options
| author | Daniel Polito <[email protected]> | 2026-01-05 14:07:46 -0300 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-01-06 01:07:46 +0800 |
| commit | 8e9a0c4ad014eb54d7ddd8de3b5f321de931f2f7 (patch) | |
| tree | 6d999aaf07fe3382a6aac28c3c47a03648cdd0cd /packages | |
| parent | ced093e64641de9e4fb7ed100325c3ffe730cc5c (diff) | |
| download | opencode-8e9a0c4ad014eb54d7ddd8de3b5f321de931f2f7.tar.gz opencode-8e9a0c4ad014eb54d7ddd8de3b5f321de931f2f7.zip | |
Desktop: Install CLI (#6526)
Co-authored-by: Brendan Allan <[email protected]>
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/desktop/src-tauri/Cargo.lock | 1 | ||||
| -rw-r--r-- | packages/desktop/src-tauri/Cargo.toml | 1 | ||||
| -rw-r--r-- | packages/desktop/src-tauri/src/cli.rs | 116 | ||||
| -rw-r--r-- | packages/desktop/src-tauri/src/lib.rs | 179 | ||||
| -rw-r--r-- | packages/desktop/src/cli.ts | 13 | ||||
| -rw-r--r-- | packages/desktop/src/menu.ts | 5 |
6 files changed, 232 insertions, 83 deletions
diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock index 11afce91e..cd7a4226c 100644 --- a/packages/desktop/src-tauri/Cargo.lock +++ b/packages/desktop/src-tauri/Cargo.lock @@ -2777,6 +2777,7 @@ version = "0.0.0" dependencies = [ "gtk", "listeners", + "semver", "serde", "serde_json", "tauri", diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml index b7c238f06..9afeee945 100644 --- a/packages/desktop/src-tauri/Cargo.toml +++ b/packages/desktop/src-tauri/Cargo.toml @@ -35,6 +35,7 @@ serde_json = "1" tokio = "1.48.0" listeners = "0.3" tauri-plugin-os = "2" +semver = "1.0.27" [target.'cfg(target_os = "linux")'.dependencies] gtk = "0.18.2" diff --git a/packages/desktop/src-tauri/src/cli.rs b/packages/desktop/src-tauri/src/cli.rs new file mode 100644 index 000000000..6b86cbcd2 --- /dev/null +++ b/packages/desktop/src-tauri/src/cli.rs @@ -0,0 +1,116 @@ +const CLI_INSTALL_DIR: &str = ".opencode/bin"; +const CLI_BINARY_NAME: &str = "opencode"; + +fn get_cli_install_path() -> Option<std::path::PathBuf> { + std::env::var("HOME").ok().map(|home| { + std::path::PathBuf::from(home) + .join(CLI_INSTALL_DIR) + .join(CLI_BINARY_NAME) + }) +} + +pub fn get_sidecar_path() -> std::path::PathBuf { + tauri::utils::platform::current_exe() + .expect("Failed to get current exe") + .parent() + .expect("Failed to get parent dir") + .join("opencode-cli") +} + +fn is_cli_installed() -> bool { + get_cli_install_path() + .map(|path| path.exists()) + .unwrap_or(false) +} + +const INSTALL_SCRIPT: &str = include_str!("../../../../install"); + +#[tauri::command] +pub fn install_cli() -> Result<String, String> { + if cfg!(not(unix)) { + return Err("CLI installation is only supported on macOS & Linux".to_string()); + } + + let sidecar = get_sidecar_path(); + if !sidecar.exists() { + return Err("Sidecar binary not found".to_string()); + } + + let temp_script = std::env::temp_dir().join("opencode-install.sh"); + std::fs::write(&temp_script, INSTALL_SCRIPT) + .map_err(|e| format!("Failed to write install script: {}", e))?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&temp_script, std::fs::Permissions::from_mode(0o755)) + .map_err(|e| format!("Failed to set script permissions: {}", e))?; + } + + let output = std::process::Command::new(&temp_script) + .arg("--binary") + .arg(&sidecar) + .output() + .map_err(|e| format!("Failed to run install script: {}", e))?; + + let _ = std::fs::remove_file(&temp_script); + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("Install script failed: {}", stderr)); + } + + let install_path = + get_cli_install_path().ok_or_else(|| "Could not determine install path".to_string())?; + + Ok(install_path.to_string_lossy().to_string()) +} + +pub fn sync_cli(app: tauri::AppHandle) -> Result<(), String> { + if cfg!(debug_assertions) { + println!("Skipping CLI sync for debug build"); + return Ok(()); + } + + if !is_cli_installed() { + println!("No CLI installation found, skipping sync"); + return Ok(()); + } + + let cli_path = + get_cli_install_path().ok_or_else(|| "Could not determine CLI install path".to_string())?; + + let output = std::process::Command::new(&cli_path) + .arg("--version") + .output() + .map_err(|e| format!("Failed to get CLI version: {}", e))?; + + if !output.status.success() { + return Err("Failed to get CLI version".to_string()); + } + + let cli_version_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let cli_version = semver::Version::parse(&cli_version_str) + .map_err(|e| format!("Failed to parse CLI version '{}': {}", cli_version_str, e))?; + + let app_version = app.package_info().version.clone(); + + if cli_version >= app_version { + println!( + "CLI version {} is up to date (app version: {}), skipping sync", + cli_version, app_version + ); + return Ok(()); + } + + println!( + "CLI version {} is older than app version {}, syncing", + cli_version, app_version + ); + + install_cli()?; + + println!("Synced installed CLI"); + + Ok(()) +} diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs index 46c0ab256..4012fe1a5 100644 --- a/packages/desktop/src-tauri/src/lib.rs +++ b/packages/desktop/src-tauri/src/lib.rs @@ -1,12 +1,16 @@ +mod cli; mod window_customizer; +use cli::{get_sidecar_path, install_cli, sync_cli}; use std::{ collections::VecDeque, net::{SocketAddr, TcpListener}, sync::{Arc, Mutex}, time::{Duration, Instant}, }; -use tauri::{AppHandle, LogicalSize, Manager, RunEvent, WebviewUrl, WebviewWindow, path::BaseDirectory}; +use tauri::{ + path::BaseDirectory, AppHandle, LogicalSize, Manager, RunEvent, WebviewUrl, WebviewWindow, +}; use tauri_plugin_clipboard_manager::ClipboardExt; use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogResult}; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; @@ -116,11 +120,7 @@ fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild { #[cfg(not(target_os = "windows"))] let (mut rx, child) = { - let sidecar_path = tauri::utils::platform::current_exe() - .expect("Failed to get current exe") - .parent() - .expect("Failed to get parent dir") - .join("opencode-cli"); + let sidecar = get_sidecar_path(); let shell = get_user_shell(); app.shell() .command(&shell) @@ -130,7 +130,7 @@ fn spawn_sidecar(app: &AppHandle, port: u32) -> CommandChild { .args([ "-il", "-c", - &format!("{} serve --port={}", sidecar_path.display(), port), + &format!("{} serve --port={}", sidecar.display(), port), ]) .spawn() .expect("Failed to spawn opencode") @@ -203,7 +203,8 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ kill_sidecar, copy_logs_to_clipboard, - get_logs + get_logs, + install_cli ]) .setup(move |app| { let app = app.handle().clone(); @@ -211,83 +212,95 @@ pub fn run() { // Initialize log state app.manage(LogState(Arc::new(Mutex::new(VecDeque::new())))); - tauri::async_runtime::spawn(async move { - let port = get_sidecar_port(); - - let should_spawn_sidecar = !is_server_running(port).await; - - let child = if should_spawn_sidecar { - let child = spawn_sidecar(&app, port); - - let timestamp = Instant::now(); - loop { - if timestamp.elapsed() > Duration::from_secs(7) { - let res = app.dialog() - .message("Failed to spawn OpenCode Server. Copy logs using the button below and send them to the team for assistance.") - .title("Startup Failed") - .buttons(MessageDialogButtons::OkCancelCustom("Copy Logs And Exit".to_string(), "Exit".to_string())) - .blocking_show_with_result(); - - if matches!(&res, MessageDialogResult::Custom(name) if name == "Copy Logs And Exit") { - match copy_logs_to_clipboard(app.clone()).await { - Ok(()) => println!("Logs copied to clipboard successfully"), - Err(e) => println!("Failed to copy logs to clipboard: {}", e), - } - } - - app.exit(1); - - return; - } - - tokio::time::sleep(Duration::from_millis(10)).await; - - if is_server_running(port).await { - // give the server a little bit more time to warm up - tokio::time::sleep(Duration::from_millis(10)).await; - - break; - } - } + { + let app = app.clone(); + tauri::async_runtime::spawn(async move { + let port = get_sidecar_port(); + + let should_spawn_sidecar = !is_server_running(port).await; + + let child = if should_spawn_sidecar { + let child = spawn_sidecar(&app, port); + + let timestamp = Instant::now(); + loop { + if timestamp.elapsed() > Duration::from_secs(7) { + let res = app.dialog() + .message("Failed to spawn OpenCode Server. Copy logs using the button below and send them to the team for assistance.") + .title("Startup Failed") + .buttons(MessageDialogButtons::OkCancelCustom("Copy Logs And Exit".to_string(), "Exit".to_string())) + .blocking_show_with_result(); + + if matches!(&res, MessageDialogResult::Custom(name) if name == "Copy Logs And Exit") { + match copy_logs_to_clipboard(app.clone()).await { + Ok(()) => println!("Logs copied to clipboard successfully"), + Err(e) => println!("Failed to copy logs to clipboard: {}", e), + } + } + + app.exit(1); + + return; + } + + tokio::time::sleep(Duration::from_millis(10)).await; + + if is_server_running(port).await { + // give the server a little bit more time to warm up + tokio::time::sleep(Duration::from_millis(10)).await; + + break; + } + } + + println!("Server ready after {:?}", timestamp.elapsed()); + + Some(child) + } else { + None + }; + + let primary_monitor = app.primary_monitor().ok().flatten(); + let size = primary_monitor + .map(|m| m.size().to_logical(m.scale_factor())) + .unwrap_or(LogicalSize::new(1920, 1080)); + + let mut window_builder = + WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into())) + .title("OpenCode") + .inner_size(size.width as f64, size.height as f64) + .decorations(true) + .zoom_hotkeys_enabled(true) + .disable_drag_drop_handler() + .initialization_script(format!( + r#" + window.__OPENCODE__ ??= {{}}; + window.__OPENCODE__.updaterEnabled = {updater_enabled}; + window.__OPENCODE__.port = {port}; + "# + )); + + #[cfg(target_os = "macos")] + { + window_builder = window_builder + .title_bar_style(tauri::TitleBarStyle::Overlay) + .hidden_title(true); + } + + window_builder.build().expect("Failed to create window"); + + app.manage(ServerState(Arc::new(Mutex::new(child)))); + }); + } - println!("Server ready after {:?}", timestamp.elapsed()); - - Some(child) - } else { - None - }; - - let primary_monitor = app.primary_monitor().ok().flatten(); - let size = primary_monitor - .map(|m| m.size().to_logical(m.scale_factor())) - .unwrap_or(LogicalSize::new(1920, 1080)); - - let mut window_builder = - WebviewWindow::builder(&app, "main", WebviewUrl::App("/".into())) - .title("OpenCode") - .inner_size(size.width as f64, size.height as f64) - .decorations(true) - .zoom_hotkeys_enabled(true) - .disable_drag_drop_handler() - .initialization_script(format!( - r#" - window.__OPENCODE__ ??= {{}}; - window.__OPENCODE__.updaterEnabled = {updater_enabled}; - window.__OPENCODE__.port = {port}; - "# - )); - - #[cfg(target_os = "macos")] - { - window_builder = window_builder - .title_bar_style(tauri::TitleBarStyle::Overlay) - .hidden_title(true); + { + let app = app.clone(); + tauri::async_runtime::spawn(async move { + if let Err(e) = sync_cli(app) { + eprintln!("Failed to sync CLI: {e}"); } - - window_builder.build().expect("Failed to create window"); - - app.manage(ServerState(Arc::new(Mutex::new(child)))); - }); + }); + } Ok(()) }); diff --git a/packages/desktop/src/cli.ts b/packages/desktop/src/cli.ts new file mode 100644 index 000000000..965ed6ddc --- /dev/null +++ b/packages/desktop/src/cli.ts @@ -0,0 +1,13 @@ +import { invoke } from "@tauri-apps/api/core" +import { message } from "@tauri-apps/plugin-dialog" + +export async function installCli(): Promise<void> { + try { + const path = await invoke<string>("install_cli") + await message(`CLI installed to ${path}\n\nRestart your terminal to use the 'opencode' command.`, { + title: "CLI Installed", + }) + } catch (e) { + await message(`Failed to install CLI: ${e}`, { title: "Installation Failed" }) + } +} diff --git a/packages/desktop/src/menu.ts b/packages/desktop/src/menu.ts index d1a5fba8e..bf9ca4b8a 100644 --- a/packages/desktop/src/menu.ts +++ b/packages/desktop/src/menu.ts @@ -2,6 +2,7 @@ import { Menu, MenuItem, PredefinedMenuItem, Submenu } from "@tauri-apps/api/men import { type as ostype } from "@tauri-apps/plugin-os" import { runUpdater, UPDATER_ENABLED } from "./updater" +import { installCli } from "./cli" export async function createMenu() { if (ostype() !== "macos") return @@ -19,6 +20,10 @@ export async function createMenu() { action: () => runUpdater({ alertOnFail: true }), text: "Check For Updates...", }), + await MenuItem.new({ + action: () => installCli(), + text: "Install CLI...", + }), await PredefinedMenuItem.new({ item: "Separator", }), |
