diff --git a/package.json b/package.json index 5a2b9fc..f51be3a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "servel", "private": true, - "version": "1.4.1", + "version": "1.4.3", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ce5b7b8..0f0867c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3156,7 +3156,7 @@ dependencies = [ [[package]] name = "servel" -version = "1.4.1" +version = "1.4.3" dependencies = [ "notify", "notify-debouncer-mini", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b97fa66..42dc720 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "servel" -version = "1.4.1" +version = "1.4.3" description = "One app. All your local dev environment." authors = ["devhardiyanto "] edition = "2021" diff --git a/src-tauri/src/commands/prereq.rs b/src-tauri/src/commands/prereq.rs index 4203b56..615100f 100644 --- a/src-tauri/src/commands/prereq.rs +++ b/src-tauri/src/commands/prereq.rs @@ -88,7 +88,7 @@ async fn probe_version(cmd: &str, args: &[&str]) -> Option { }; #[cfg(not(target_os = "windows"))] - let output = Command::new(cmd) + let output = silent_command(cmd) .args(args) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -170,3 +170,17 @@ pub async fn start_docker() -> Result<(), String> { Err("Docker daemon harus distart manual via systemctl start docker.".to_string()) } } + +/// Return OS platform sebagai string: `"windows"` | `"macos"` | `"linux"`. +/// Dipakai frontend untuk menampilkan instruksi prasyarat yang sesuai OS +/// (mis. perintah install fnm, guidance Docker). +#[tauri::command] +pub fn get_platform() -> String { + if cfg!(target_os = "windows") { + "windows".to_string() + } else if cfg!(target_os = "macos") { + "macos".to_string() + } else { + "linux".to_string() + } +} diff --git a/src-tauri/src/commands/services.rs b/src-tauri/src/commands/services.rs index 8fe2e65..f346f25 100644 --- a/src-tauri/src/commands/services.rs +++ b/src-tauri/src/commands/services.rs @@ -4,7 +4,7 @@ use std::process::Stdio; use tauri::{AppHandle, Manager}; use tokio::process::Command; -use crate::commands::util::stream_and_wait_app; +use crate::commands::util::{silent_command, stream_and_wait_app}; @@ -162,17 +162,11 @@ pub(crate) async fn load_services_internal(app: &AppHandle) -> Result Result, String> { - let mut cmd = Command::new("docker"); + let mut cmd = silent_command("docker"); cmd.args(["ps", "-a", "--filter", "name=servel_", "--format", "json"]) .stdout(Stdio::piped()) .stderr(Stdio::piped()); - #[cfg(target_os = "windows")] - { - const CREATE_NO_WINDOW: u32 = 0x08000000; - cmd.creation_flags(CREATE_NO_WINDOW); - } - let output = cmd .output() .await @@ -188,14 +182,7 @@ pub(crate) async fn services_status_internal() -> Result, Str } fn new_docker_cmd() -> Command { - #[cfg_attr(not(target_os = "windows"), allow(unused_mut))] - let mut cmd = Command::new("docker"); - #[cfg(target_os = "windows")] - { - const CREATE_NO_WINDOW: u32 = 0x08000000; - cmd.creation_flags(CREATE_NO_WINDOW); - } - cmd + silent_command("docker") } diff --git a/src-tauri/src/commands/stats.rs b/src-tauri/src/commands/stats.rs index 9b69ac6..b7ab5d8 100644 --- a/src-tauri/src/commands/stats.rs +++ b/src-tauri/src/commands/stats.rs @@ -1,6 +1,7 @@ use serde::Serialize; use std::process::Stdio; -use tokio::process::Command; + +use super::util::silent_command; /// Single container memory snapshot — payload entry untuk event `container-stats-changed`. #[derive(Serialize, Clone, Debug, PartialEq)] @@ -84,18 +85,11 @@ pub fn parse_docker_stats_json(stdout: &str) -> Vec { /// Panggil `docker stats --no-stream --format json` (tanpa `--filter` agar kompatibel /// di Docker lama). Filter prefix `servel_` dilakukan di Rust saat parse. pub async fn fetch_container_stats() -> Result, String> { - #[cfg_attr(not(target_os = "windows"), allow(unused_mut))] - let mut cmd = Command::new("docker"); + let mut cmd = silent_command("docker"); cmd.args(["stats", "--no-stream", "--format", "json"]) .stdout(Stdio::piped()) .stderr(Stdio::piped()); - #[cfg(target_os = "windows")] - { - const CREATE_NO_WINDOW: u32 = 0x08000000; - cmd.creation_flags(CREATE_NO_WINDOW); - } - let output = cmd .output() .await diff --git a/src-tauri/src/commands/util.rs b/src-tauri/src/commands/util.rs index 1ed0fc4..d8d44ac 100644 --- a/src-tauri/src/commands/util.rs +++ b/src-tauri/src/commands/util.rs @@ -19,9 +19,93 @@ pub fn silent_command(program: &str) -> Command { cmd.creation_flags(CREATE_NO_WINDOW); } + // GUI-launched apps (AppImage / .app dari launcher) TIDAK mewarisi PATH shell + // interaktif, jadi tool yang di-install via shell rc (fnm, phpvm → ~/.local/bin, + // ~/.fnm, dll.) tak ketemu. Inject PATH hasil resolusi login-shell + dir umum. + #[cfg(not(target_os = "windows"))] + { + cmd.env("PATH", resolved_path()); + } + cmd } +/// PATH yang sudah di-augment untuk spawn tool eksternal di Linux/macOS. +/// Di-resolve sekali (login-interactive shell → source .bashrc/.zshrc) lalu di-cache. +#[cfg(not(target_os = "windows"))] +pub fn resolved_path() -> &'static str { + use std::sync::OnceLock; + static CACHE: OnceLock = OnceLock::new(); + CACHE.get_or_init(build_resolved_path) +} + +/// Susun PATH: PATH login-shell (bila terbaca) ∪ PATH proses ∪ dir install umum. +/// Dedup jaga urutan; buang segmen kosong. +#[cfg(not(target_os = "windows"))] +fn build_resolved_path() -> String { + use std::collections::HashSet; + + let mut parts: Vec = Vec::new(); + + if let Some(shell_path) = capture_login_shell_path() { + parts.extend(shell_path.split(':').map(str::to_string)); + } + if let Ok(current) = std::env::var("PATH") { + parts.extend(current.split(':').map(str::to_string)); + } + + let home = std::env::var("HOME").unwrap_or_default(); + if !home.is_empty() { + parts.push(format!("{home}/.local/bin")); + parts.push(format!("{home}/.fnm")); + parts.push(format!("{home}/.local/share/fnm")); + parts.push(format!("{home}/.cargo/bin")); + } + parts.push("/usr/local/bin".to_string()); + parts.push("/opt/homebrew/bin".to_string()); + + let mut seen = HashSet::new(); + parts + .into_iter() + .filter(|p| !p.is_empty() && seen.insert(p.clone())) + .collect::>() + .join(":") +} + +/// Jalankan login-interactive shell user untuk membaca PATH final-nya (setelah +/// .bashrc/.zshrc di-source). Timeout 5s + stdin null supaya tak pernah hang. +/// Return None kalau shell gagal / tak ada / kosong (caller fallback ke dir umum). +#[cfg(not(target_os = "windows"))] +fn capture_login_shell_path() -> Option { + use std::process::{Command as StdCommand, Stdio as StdStdio}; + use std::sync::mpsc; + use std::time::Duration; + + let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string()); + let (tx, rx) = mpsc::channel(); + + std::thread::spawn(move || { + let out = StdCommand::new(&shell) + .args(["-lic", "printf '%s' \"$PATH\""]) + .stdin(StdStdio::null()) + .stderr(StdStdio::null()) + .output(); + let _ = tx.send(out); + }); + + match rx.recv_timeout(Duration::from_secs(5)) { + Ok(Ok(out)) if out.status.success() => { + let s = String::from_utf8_lossy(&out.stdout).trim().to_string(); + if s.is_empty() { + None + } else { + Some(s) + } + } + _ => None, + } +} + /// Build a PowerShell command that executes `script_body` as a `-Command` string. /// Flags: -NoProfile -NoLogo -NonInteractive -ExecutionPolicy Bypass. /// Stdout/stderr default to null; caller overrides before spawning. diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 468a346..5d0d252 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -65,6 +65,7 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ commands::prereq::check_prerequisites, commands::prereq::start_docker, + commands::prereq::get_platform, commands::php::php_list_installed, commands::php::php_get_active, commands::php::php_switch, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f0a77ff..3c6d2d9 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Servel", - "version": "1.4.1", + "version": "1.4.3", "identifier": "com.devhardiyanto.servel", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/views/Onboarding.vue b/src/views/Onboarding.vue index 0bb6ad8..d35a76b 100644 --- a/src/views/Onboarding.vue +++ b/src/views/Onboarding.vue @@ -1,13 +1,24 @@