summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDaniel Polito <[email protected]>2026-01-05 14:07:46 -0300
committerGitHub <[email protected]>2026-01-06 01:07:46 +0800
commit8e9a0c4ad014eb54d7ddd8de3b5f321de931f2f7 (patch)
tree6d999aaf07fe3382a6aac28c3c47a03648cdd0cd
parentced093e64641de9e4fb7ed100325c3ffe730cc5c (diff)
downloadopencode-8e9a0c4ad014eb54d7ddd8de3b5f321de931f2f7.tar.gz
opencode-8e9a0c4ad014eb54d7ddd8de3b5f321de931f2f7.zip
Desktop: Install CLI (#6526)
Co-authored-by: Brendan Allan <[email protected]>
-rwxr-xr-xinstall234
-rw-r--r--packages/desktop/src-tauri/Cargo.lock1
-rw-r--r--packages/desktop/src-tauri/Cargo.toml1
-rw-r--r--packages/desktop/src-tauri/src/cli.rs116
-rw-r--r--packages/desktop/src-tauri/src/lib.rs179
-rw-r--r--packages/desktop/src/cli.ts13
-rw-r--r--packages/desktop/src/menu.ts5
7 files changed, 364 insertions, 185 deletions
diff --git a/install b/install
index f995e2d43..3b9e6d68c 100755
--- a/install
+++ b/install
@@ -16,16 +16,19 @@ Usage: install.sh [options]
Options:
-h, --help Display this help message
-v, --version <version> Install a specific version (e.g., 1.0.180)
+ -b, --binary <path> Install from a local binary instead of downloading
--no-modify-path Don't modify shell config files (.zshrc, .bashrc, etc.)
Examples:
curl -fsSL https://opencode.ai/install | bash
curl -fsSL https://opencode.ai/install | bash -s -- --version 1.0.180
+ ./install --binary /path/to/opencode
EOF
}
requested_version=${VERSION:-}
no_modify_path=false
+binary_path=""
while [[ $# -gt 0 ]]; do
case "$1" in
@@ -42,6 +45,15 @@ while [[ $# -gt 0 ]]; do
exit 1
fi
;;
+ -b|--binary)
+ if [[ -n "${2:-}" ]]; then
+ binary_path="$2"
+ shift 2
+ else
+ echo -e "${RED}Error: --binary requires a path argument${NC}"
+ exit 1
+ fi
+ ;;
--no-modify-path)
no_modify_path=true
shift
@@ -53,119 +65,128 @@ while [[ $# -gt 0 ]]; do
esac
done
-raw_os=$(uname -s)
-os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
-case "$raw_os" in
- Darwin*) os="darwin" ;;
- Linux*) os="linux" ;;
- MINGW*|MSYS*|CYGWIN*) os="windows" ;;
-esac
-
-arch=$(uname -m)
-if [[ "$arch" == "aarch64" ]]; then
- arch="arm64"
-fi
-if [[ "$arch" == "x86_64" ]]; then
- arch="x64"
-fi
+INSTALL_DIR=$HOME/.opencode/bin
+mkdir -p "$INSTALL_DIR"
-if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then
- rosetta_flag=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)
- if [ "$rosetta_flag" = "1" ]; then
- arch="arm64"
- fi
-fi
+# If --binary is provided, skip all download/detection logic
+if [ -n "$binary_path" ]; then
+ if [ ! -f "$binary_path" ]; then
+ echo -e "${RED}Error: Binary not found at ${binary_path}${NC}"
+ exit 1
+ fi
+ specific_version="local"
+else
+ raw_os=$(uname -s)
+ os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
+ case "$raw_os" in
+ Darwin*) os="darwin" ;;
+ Linux*) os="linux" ;;
+ MINGW*|MSYS*|CYGWIN*) os="windows" ;;
+ esac
-combo="$os-$arch"
-case "$combo" in
- linux-x64|linux-arm64|darwin-x64|darwin-arm64|windows-x64)
- ;;
- *)
- echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}"
- exit 1
- ;;
-esac
+ arch=$(uname -m)
+ if [[ "$arch" == "aarch64" ]]; then
+ arch="arm64"
+ fi
+ if [[ "$arch" == "x86_64" ]]; then
+ arch="x64"
+ fi
-archive_ext=".zip"
-if [ "$os" = "linux" ]; then
- archive_ext=".tar.gz"
-fi
+ if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then
+ rosetta_flag=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)
+ if [ "$rosetta_flag" = "1" ]; then
+ arch="arm64"
+ fi
+ fi
-is_musl=false
-if [ "$os" = "linux" ]; then
- if [ -f /etc/alpine-release ]; then
- is_musl=true
- fi
+ combo="$os-$arch"
+ case "$combo" in
+ linux-x64|linux-arm64|darwin-x64|darwin-arm64|windows-x64)
+ ;;
+ *)
+ echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}"
+ exit 1
+ ;;
+ esac
- if command -v ldd >/dev/null 2>&1; then
- if ldd --version 2>&1 | grep -qi musl; then
- is_musl=true
+ archive_ext=".zip"
+ if [ "$os" = "linux" ]; then
+ archive_ext=".tar.gz"
fi
- fi
-fi
-needs_baseline=false
-if [ "$arch" = "x64" ]; then
- if [ "$os" = "linux" ]; then
- if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then
- needs_baseline=true
- fi
- fi
+ is_musl=false
+ if [ "$os" = "linux" ]; then
+ if [ -f /etc/alpine-release ]; then
+ is_musl=true
+ fi
- if [ "$os" = "darwin" ]; then
- avx2=$(sysctl -n hw.optional.avx2_0 2>/dev/null || echo 0)
- if [ "$avx2" != "1" ]; then
- needs_baseline=true
+ if command -v ldd >/dev/null 2>&1; then
+ if ldd --version 2>&1 | grep -qi musl; then
+ is_musl=true
+ fi
+ fi
fi
- fi
-fi
-target="$os-$arch"
-if [ "$needs_baseline" = "true" ]; then
- target="$target-baseline"
-fi
-if [ "$is_musl" = "true" ]; then
- target="$target-musl"
-fi
-
-filename="$APP-$target$archive_ext"
+ needs_baseline=false
+ if [ "$arch" = "x64" ]; then
+ if [ "$os" = "linux" ]; then
+ if ! grep -qi avx2 /proc/cpuinfo 2>/dev/null; then
+ needs_baseline=true
+ fi
+ fi
+ if [ "$os" = "darwin" ]; then
+ avx2=$(sysctl -n hw.optional.avx2_0 2>/dev/null || echo 0)
+ if [ "$avx2" != "1" ]; then
+ needs_baseline=true
+ fi
+ fi
+ fi
-if [ "$os" = "linux" ]; then
- if ! command -v tar >/dev/null 2>&1; then
- echo -e "${RED}Error: 'tar' is required but not installed.${NC}"
- exit 1
+ target="$os-$arch"
+ if [ "$needs_baseline" = "true" ]; then
+ target="$target-baseline"
fi
-else
- if ! command -v unzip >/dev/null 2>&1; then
- echo -e "${RED}Error: 'unzip' is required but not installed.${NC}"
- exit 1
+ if [ "$is_musl" = "true" ]; then
+ target="$target-musl"
fi
-fi
-INSTALL_DIR=$HOME/.opencode/bin
-mkdir -p "$INSTALL_DIR"
+ filename="$APP-$target$archive_ext"
-if [ -z "$requested_version" ]; then
- url="https://github.com/anomalyco/opencode/releases/latest/download/$filename"
- specific_version=$(curl -s https://api.github.com/repos/anomalyco/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
- if [[ $? -ne 0 || -z "$specific_version" ]]; then
- echo -e "${RED}Failed to fetch version information${NC}"
- exit 1
+ if [ "$os" = "linux" ]; then
+ if ! command -v tar >/dev/null 2>&1; then
+ echo -e "${RED}Error: 'tar' is required but not installed.${NC}"
+ exit 1
+ fi
+ else
+ if ! command -v unzip >/dev/null 2>&1; then
+ echo -e "${RED}Error: 'unzip' is required but not installed.${NC}"
+ exit 1
+ fi
fi
-else
- # Strip leading 'v' if present
- requested_version="${requested_version#v}"
- url="https://github.com/anomalyco/opencode/releases/download/v${requested_version}/$filename"
- specific_version=$requested_version
-
- # Verify the release exists before downloading
- http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/anomalyco/opencode/releases/tag/v${requested_version}")
- if [ "$http_status" = "404" ]; then
- echo -e "${RED}Error: Release v${requested_version} not found${NC}"
- echo -e "${MUTED}Available releases: https://github.com/anomalyco/opencode/releases${NC}"
- exit 1
+
+ if [ -z "$requested_version" ]; then
+ url="https://github.com/anomalyco/opencode/releases/latest/download/$filename"
+ specific_version=$(curl -s https://api.github.com/repos/anomalyco/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
+
+ if [[ $? -ne 0 || -z "$specific_version" ]]; then
+ echo -e "${RED}Failed to fetch version information${NC}"
+ exit 1
+ fi
+ else
+ # Strip leading 'v' if present
+ requested_version="${requested_version#v}"
+ url="https://github.com/anomalyco/opencode/releases/download/v${requested_version}/$filename"
+ specific_version=$requested_version
+
+ # Verify the release exists before downloading
+ http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/anomalyco/opencode/releases/tag/v${requested_version}")
+ if [ "$http_status" = "404" ]; then
+ echo -e "${RED}Error: Release v${requested_version} not found${NC}"
+ echo -e "${MUTED}Available releases: https://github.com/anomalyco/opencode/releases${NC}"
+ exit 1
+ fi
fi
fi
@@ -267,11 +288,11 @@ download_with_progress() {
{
local length=0
local bytes=0
-
+
while IFS=" " read -r -a line; do
[ "${#line[@]}" -lt 2 ] && continue
local tag="${line[0]} ${line[1]}"
-
+
if [ "$tag" = "0000: content-length:" ]; then
length="${line[2]}"
length=$(echo "$length" | tr -d '\r')
@@ -296,7 +317,7 @@ download_and_install() {
print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}version: ${NC}$specific_version"
local tmp_dir="${TMPDIR:-/tmp}/opencode_install_$$"
mkdir -p "$tmp_dir"
-
+
if [[ "$os" == "windows" ]] || ! [ -t 2 ] || ! download_with_progress "$url" "$tmp_dir/$filename"; then
# Fallback to standard curl on Windows, non-TTY environments, or if custom progress fails
curl -# -L -o "$tmp_dir/$filename" "$url"
@@ -307,14 +328,24 @@ download_and_install() {
else
unzip -q "$tmp_dir/$filename" -d "$tmp_dir"
fi
-
+
mv "$tmp_dir/opencode" "$INSTALL_DIR"
chmod 755 "${INSTALL_DIR}/opencode"
rm -rf "$tmp_dir"
}
-check_version
-download_and_install
+install_from_binary() {
+ print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}from: ${NC}$binary_path"
+ cp "$binary_path" "${INSTALL_DIR}/opencode"
+ chmod 755 "${INSTALL_DIR}/opencode"
+}
+
+if [ -n "$binary_path" ]; then
+ install_from_binary
+else
+ check_version
+ download_and_install
+fi
add_to_path() {
@@ -416,4 +447,3 @@ echo -e ""
echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/docs"
echo -e ""
echo -e ""
-
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",
}),