cli.rs 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. use tauri::{AppHandle, Manager, path::BaseDirectory};
  2. use tauri_plugin_shell::{ShellExt, process::Command};
  3. const CLI_INSTALL_DIR: &str = ".opencode/bin";
  4. const CLI_BINARY_NAME: &str = "opencode";
  5. #[derive(serde::Deserialize)]
  6. pub struct ServerConfig {
  7. pub hostname: Option<String>,
  8. pub port: Option<u32>,
  9. }
  10. #[derive(serde::Deserialize)]
  11. pub struct Config {
  12. pub server: Option<ServerConfig>,
  13. }
  14. pub async fn get_config(app: &AppHandle) -> Option<Config> {
  15. create_command(app, "debug config")
  16. .output()
  17. .await
  18. .inspect_err(|e| eprintln!("Failed to read OC config: {e}"))
  19. .ok()
  20. .and_then(|out| String::from_utf8(out.stdout.to_vec()).ok())
  21. .and_then(|s| serde_json::from_str::<Config>(&s).ok())
  22. }
  23. fn get_cli_install_path() -> Option<std::path::PathBuf> {
  24. std::env::var("HOME").ok().map(|home| {
  25. std::path::PathBuf::from(home)
  26. .join(CLI_INSTALL_DIR)
  27. .join(CLI_BINARY_NAME)
  28. })
  29. }
  30. pub fn get_sidecar_path(app: &tauri::AppHandle) -> std::path::PathBuf {
  31. // Get binary with symlinks support
  32. tauri::process::current_binary(&app.env())
  33. .expect("Failed to get current binary")
  34. .parent()
  35. .expect("Failed to get parent dir")
  36. .join("opencode-cli")
  37. }
  38. fn is_cli_installed() -> bool {
  39. get_cli_install_path()
  40. .map(|path| path.exists())
  41. .unwrap_or(false)
  42. }
  43. const INSTALL_SCRIPT: &str = include_str!("../../../../install");
  44. #[tauri::command]
  45. pub fn install_cli(app: tauri::AppHandle) -> Result<String, String> {
  46. if cfg!(not(unix)) {
  47. return Err("CLI installation is only supported on macOS & Linux".to_string());
  48. }
  49. let sidecar = get_sidecar_path(&app);
  50. if !sidecar.exists() {
  51. return Err("Sidecar binary not found".to_string());
  52. }
  53. let temp_script = std::env::temp_dir().join("opencode-install.sh");
  54. std::fs::write(&temp_script, INSTALL_SCRIPT)
  55. .map_err(|e| format!("Failed to write install script: {}", e))?;
  56. #[cfg(unix)]
  57. {
  58. use std::os::unix::fs::PermissionsExt;
  59. std::fs::set_permissions(&temp_script, std::fs::Permissions::from_mode(0o755))
  60. .map_err(|e| format!("Failed to set script permissions: {}", e))?;
  61. }
  62. let output = std::process::Command::new(&temp_script)
  63. .arg("--binary")
  64. .arg(&sidecar)
  65. .output()
  66. .map_err(|e| format!("Failed to run install script: {}", e))?;
  67. let _ = std::fs::remove_file(&temp_script);
  68. if !output.status.success() {
  69. let stderr = String::from_utf8_lossy(&output.stderr);
  70. return Err(format!("Install script failed: {}", stderr));
  71. }
  72. let install_path =
  73. get_cli_install_path().ok_or_else(|| "Could not determine install path".to_string())?;
  74. Ok(install_path.to_string_lossy().to_string())
  75. }
  76. pub fn sync_cli(app: tauri::AppHandle) -> Result<(), String> {
  77. if cfg!(debug_assertions) {
  78. println!("Skipping CLI sync for debug build");
  79. return Ok(());
  80. }
  81. if !is_cli_installed() {
  82. println!("No CLI installation found, skipping sync");
  83. return Ok(());
  84. }
  85. let cli_path =
  86. get_cli_install_path().ok_or_else(|| "Could not determine CLI install path".to_string())?;
  87. let output = std::process::Command::new(&cli_path)
  88. .arg("--version")
  89. .output()
  90. .map_err(|e| format!("Failed to get CLI version: {}", e))?;
  91. if !output.status.success() {
  92. return Err("Failed to get CLI version".to_string());
  93. }
  94. let cli_version_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
  95. let cli_version = semver::Version::parse(&cli_version_str)
  96. .map_err(|e| format!("Failed to parse CLI version '{}': {}", cli_version_str, e))?;
  97. let app_version = app.package_info().version.clone();
  98. if cli_version >= app_version {
  99. println!(
  100. "CLI version {} is up to date (app version: {}), skipping sync",
  101. cli_version, app_version
  102. );
  103. return Ok(());
  104. }
  105. println!(
  106. "CLI version {} is older than app version {}, syncing",
  107. cli_version, app_version
  108. );
  109. install_cli(app)?;
  110. println!("Synced installed CLI");
  111. Ok(())
  112. }
  113. fn get_user_shell() -> String {
  114. std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
  115. }
  116. pub fn create_command(app: &tauri::AppHandle, args: &str) -> Command {
  117. let state_dir = app
  118. .path()
  119. .resolve("", BaseDirectory::AppLocalData)
  120. .expect("Failed to resolve app local data dir");
  121. #[cfg(target_os = "windows")]
  122. return app
  123. .shell()
  124. .sidecar("opencode-cli")
  125. .unwrap()
  126. .args(args.split_whitespace())
  127. .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
  128. .env("OPENCODE_CLIENT", "desktop")
  129. .env("XDG_STATE_HOME", &state_dir);
  130. #[cfg(not(target_os = "windows"))]
  131. return {
  132. let sidecar = get_sidecar_path(app);
  133. let shell = get_user_shell();
  134. let cmd = if shell.ends_with("/nu") {
  135. format!("^\"{}\" {}", sidecar.display(), args)
  136. } else {
  137. format!("\"{}\" {}", sidecar.display(), args)
  138. };
  139. app.shell()
  140. .command(&shell)
  141. .env("OPENCODE_EXPERIMENTAL_ICON_DISCOVERY", "true")
  142. .env("OPENCODE_CLIENT", "desktop")
  143. .env("XDG_STATE_HOME", &state_dir)
  144. .args(["-il", "-c", &cmd])
  145. };
  146. }