Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "servel",
"private": true,
"version": "1.4.1",
"version": "1.4.3",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 <dev.hardiyanto@gmail.com>"]
edition = "2021"
Expand Down
16 changes: 15 additions & 1 deletion src-tauri/src/commands/prereq.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ async fn probe_version(cmd: &str, args: &[&str]) -> Option<String> {
};

#[cfg(not(target_os = "windows"))]
let output = Command::new(cmd)
let output = silent_command(cmd)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
Expand Down Expand Up @@ -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()
}
}
19 changes: 3 additions & 16 deletions src-tauri/src/commands/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};



Expand Down Expand Up @@ -162,17 +162,11 @@ pub(crate) async fn load_services_internal(app: &AppHandle) -> Result<Vec<Servic

/// Inti services_status tanpa #[tauri::command], bisa dipanggil dari polling.
pub(crate) async fn services_status_internal() -> Result<Vec<ServiceStatus>, 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
Expand All @@ -188,14 +182,7 @@ pub(crate) async fn services_status_internal() -> Result<Vec<ServiceStatus>, 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")
}


Expand Down
12 changes: 3 additions & 9 deletions src-tauri/src/commands/stats.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -84,18 +85,11 @@ pub fn parse_docker_stats_json(stdout: &str) -> Vec<ContainerMemStat> {
/// 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<Vec<ContainerMemStat>, 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
Expand Down
84 changes: 84 additions & 0 deletions src-tauri/src/commands/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = 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<String> = 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::<Vec<_>>()
.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<String> {
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.
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
85 changes: 64 additions & 21 deletions src/views/Onboarding.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
<script setup lang="ts">
import { inject, computed } from 'vue'
import { inject, computed, ref, onMounted } from 'vue'
import { SetViewKey } from '../types/navigation'
import { usePrereq } from '@/composables/usePrereq'
import { useTauri } from '@/composables/useTauri'
import PrereqCard from '@/components/PrereqCard.vue'
import type { PrereqAction } from '@/components/PrereqCard.vue'
import SkeletonBox from '@/components/ui/SkeletonBox.vue'

const setView = inject(SetViewKey)!

// Platform dipakai untuk menampilkan instruksi prasyarat yang sesuai OS.
// Default "windows" (target utama) sampai command resolve.
const { call } = useTauri()
const platform = ref<string>('windows')
onMounted(async () => {
const p = await call<string>('get_platform')
if (p) platform.value = p
})
const isLinux = computed<boolean>(() => platform.value === 'linux')

const {
status,
checking,
Expand All @@ -33,25 +44,57 @@ const totalCount = 3

const progressPct = computed<number>(() => (readyCount.value / totalCount) * 100)

const isLinuxDockerError = computed<boolean>(() =>
startDockerError.value?.includes('systemctl') ?? false
)

function openUrl(url: string): void {
window.open(url, '_blank', 'noopener')
}

const dockerActions = computed<PrereqAction[]>(() => [
{
label: '⬇ Download Docker Desktop',
primary: true,
onClick: () => openUrl('https://www.docker.com/products/docker-desktop/'),
},
{
label: 'WSL2 + Docker Engine Guide ↗',
onClick: () => openUrl('https://docs.docker.com/desktop/wsl/'),
},
])
const dockerActions = computed<PrereqAction[]>(() => {
if (isLinux.value) {
return [
{
label: '⬇ Install Docker Engine',
primary: true,
onClick: () => openUrl('https://docs.docker.com/engine/install/'),
},
{
label: 'Post-install (grup docker / rootless) ↗',
onClick: () => openUrl('https://docs.docker.com/engine/install/linux-postinstall/'),
},
]
}
if (platform.value === 'macos') {
return [
{
label: '⬇ Download Docker Desktop',
primary: true,
onClick: () => openUrl('https://www.docker.com/products/docker-desktop/'),
},
]
}
return [
{
label: '⬇ Download Docker Desktop',
primary: true,
onClick: () => openUrl('https://www.docker.com/products/docker-desktop/'),
},
{
label: 'WSL2 + Docker Engine Guide ↗',
onClick: () => openUrl('https://docs.docker.com/desktop/wsl/'),
},
]
})

// Perintah install fnm per-OS untuk ditampilkan di code-block prereq card.
const fnmInstallCmd = computed<string>(() => {
switch (platform.value) {
case 'macos':
return 'brew install fnm'
case 'linux':
return 'curl -fsSL https://fnm.vercel.app/install | bash'
default:
return 'winget install Schniz.fnm'
}
})

const phpvmActions = computed<PrereqAction[]>(() => [
{
Expand Down Expand Up @@ -149,25 +192,25 @@ const fnmOkDesc = computed<string>(() => {
:ok-desc="fnmOkDesc"
not-found-desc="Fast Node version manager, built in Rust"
:actions="fnmActions"
code="winget install Schniz.fnm"
:code="fnmInstallCmd"
/>
</div>

<div v-if="startDockerError && !isLinuxDockerError" class="ob-error">
<div v-if="startDockerError && !isLinux" class="ob-error">
{{ startDockerError }}
</div>

<div v-if="isLinuxDockerError" class="ob-instruction">
<div v-if="isLinux && dockerInstalledButNotRunning" class="ob-instruction">
<p class="ob-instruction__title">Start Docker manually:</p>
<code class="ob-instruction__cmd">sudo systemctl start docker</code>
<p class="ob-instruction__hint">Then click "Refresh checks" to refresh the status.</p>
<p class="ob-instruction__hint">Lalu klik "Refresh checks" untuk perbarui status.</p>
</div>

<p class="ob-note">Already installed everything? Click Refresh to re-check.</p>

<div class="ob-cta-row">
<button
v-if="dockerInstalledButNotRunning"
v-if="dockerInstalledButNotRunning && !isLinux"
class="ob-btn ob-btn--primary"
:disabled="startingDocker"
@click="startDocker"
Expand Down