9e8f1bbeed
Dashboard / frontend (push) Failing after 0s
Dashboard / api (push) Failing after 0s
Lint PowerShell / powershell-lint (ubuntu-latest) (push) Failing after 1s
Python Lint / Lint Python with Ruff (push) Failing after 1s
ShellCheck / Lint shell scripts (push) Failing after 1s
Matrix Smoke / linux-smoke (push) Failing after 1s
Matrix Smoke / distro: cachyos (push) Failing after 15s
Matrix Smoke / distro: linux-mint-21.3 (push) Failing after 15s
Matrix Smoke / distro: debian-12 (push) Failing after 5m21s
Matrix Smoke / distro: fedora-41 (push) Failing after 4m56s
Matrix Smoke / distro: ubuntu-24.04 (push) Failing after 2m13s
Matrix Smoke / distro: rocky-9 (push) Failing after 10m39s
Matrix Smoke / distro: manjaro (push) Failing after 12m11s
Matrix Smoke / distro: opensuse-tw (push) Failing after 11m53s
Matrix Smoke / distro: archlinux (push) Failing after 20m3s
Matrix Smoke / distro: ubuntu-22.04 (push) Failing after 13m49s
Validate .env Schema / tier-1-env-validation (push) Successful in 52s
Validate .env Schema / tier-2-env-validation (push) Successful in 44s
Validate .env Schema / tier-3-env-validation (push) Successful in 52s
Validate .env Schema / tier-4-env-validation (push) Successful in 51s
Validate Extensions Catalog / Check catalog is up-to-date (push) Failing after 9m47s
Secret Scan / Scan for secrets (push) Failing after 21m4s
Validate Docker Compose / Validate Docker Compose files (push) Has been cancelled
Python Type Check / Type check with mypy (push) Has been cancelled
Validate .env Schema / tier-0-env-validation (push) Has been cancelled
Test Linux / integration-smoke (push) Has been cancelled
Lint PowerShell / powershell-lint (windows-latest) (push) Has been cancelled
Matrix Smoke / macos-smoke (push) Has been cancelled
302 lines
8.3 KiB
Rust
302 lines
8.3 KiB
Rust
use crate::state::{GpuInfo, InstallPhase, InstallState};
|
|
use crate::{docker, gpu, installer, platform};
|
|
use serde::Serialize;
|
|
use std::sync::Mutex;
|
|
|
|
const ALLOWED_FEATURES: &[&str] = &["voice", "workflows", "rag", "image_gen", "all"];
|
|
|
|
// ---- System Check ----
|
|
|
|
#[derive(Serialize)]
|
|
pub struct SystemCheckResult {
|
|
pub system: platform::SystemInfo,
|
|
pub requirements: Vec<platform::RequirementCheck>,
|
|
pub docker: docker::DockerStatus,
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn check_system() -> SystemCheckResult {
|
|
let system = platform::check_system();
|
|
let requirements = platform::check_requirements(&system);
|
|
let docker = docker::check();
|
|
|
|
SystemCheckResult {
|
|
system,
|
|
requirements,
|
|
docker,
|
|
}
|
|
}
|
|
|
|
// ---- Prerequisites ----
|
|
|
|
#[derive(Serialize)]
|
|
pub struct PrerequisiteStatus {
|
|
pub git_installed: bool,
|
|
pub docker_installed: bool,
|
|
pub docker_running: bool,
|
|
pub wsl2_needed: bool,
|
|
pub wsl2_installed: bool,
|
|
pub all_met: bool,
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn check_prerequisites() -> PrerequisiteStatus {
|
|
let git = which::which("git").is_ok();
|
|
let docker_status = docker::check();
|
|
let wsl2_needed = cfg!(target_os = "windows");
|
|
let wsl2_installed = if wsl2_needed {
|
|
std::process::Command::new("wsl")
|
|
.args(["--status"])
|
|
.output()
|
|
.map(|o| o.status.success())
|
|
.unwrap_or(false)
|
|
} else {
|
|
true
|
|
};
|
|
|
|
let all_met = git
|
|
&& docker_status.installed
|
|
&& docker_status.running
|
|
&& docker_status.compose_installed
|
|
&& (!wsl2_needed || wsl2_installed);
|
|
|
|
PrerequisiteStatus {
|
|
git_installed: git,
|
|
docker_installed: docker_status.installed,
|
|
docker_running: docker_status.running,
|
|
wsl2_needed,
|
|
wsl2_installed,
|
|
all_met,
|
|
}
|
|
}
|
|
|
|
// ---- Install Prerequisites ----
|
|
|
|
#[derive(Serialize)]
|
|
pub struct InstallPrereqResult {
|
|
pub success: bool,
|
|
pub message: String,
|
|
pub reboot_required: bool,
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn install_prerequisites(component: String) -> InstallPrereqResult {
|
|
match component.as_str() {
|
|
"docker" => match docker::install_docker().await {
|
|
Ok(msg) => InstallPrereqResult {
|
|
success: true,
|
|
message: msg,
|
|
reboot_required: false,
|
|
},
|
|
Err(msg) => InstallPrereqResult {
|
|
success: false,
|
|
message: msg,
|
|
reboot_required: false,
|
|
},
|
|
},
|
|
#[cfg(target_os = "windows")]
|
|
"wsl2" => match crate::platform::windows::install_wsl2() {
|
|
Ok(needs_reboot) => InstallPrereqResult {
|
|
success: true,
|
|
message: if needs_reboot {
|
|
"WSL2 installed. A restart is required to complete setup.".into()
|
|
} else {
|
|
"WSL2 is ready.".into()
|
|
},
|
|
reboot_required: needs_reboot,
|
|
},
|
|
Err(msg) => InstallPrereqResult {
|
|
success: false,
|
|
message: msg,
|
|
reboot_required: false,
|
|
},
|
|
},
|
|
_ => InstallPrereqResult {
|
|
success: false,
|
|
message: format!("Unknown component: {}", component),
|
|
reboot_required: false,
|
|
},
|
|
}
|
|
}
|
|
|
|
// ---- GPU Detection ----
|
|
|
|
#[derive(Serialize)]
|
|
pub struct GpuResult {
|
|
pub gpu: GpuInfo,
|
|
pub recommended_tier: u8,
|
|
pub tier_description: String,
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn detect_gpu() -> GpuResult {
|
|
let gpu = gpu::detect();
|
|
let tier = gpu::recommend_tier(&gpu);
|
|
let desc = tier_description(tier);
|
|
|
|
GpuResult {
|
|
gpu,
|
|
recommended_tier: tier,
|
|
tier_description: desc,
|
|
}
|
|
}
|
|
|
|
fn tier_description(tier: u8) -> String {
|
|
match tier {
|
|
0 => "Cloud Mode — No local GPU detected. Uses cloud AI providers.".into(),
|
|
1 => "Tier 1 — Qwen3-8B (8GB VRAM). Great for chat, code help, and general tasks.".into(),
|
|
2 => "Tier 2 — Qwen3-14B (12GB+ VRAM). Stronger reasoning and longer context.".into(),
|
|
3 => "Tier 3 — Qwen3-32B (24GB+ VRAM). Professional-grade for complex tasks.".into(),
|
|
4 => "Tier 4 — Qwen3-72B (48GB+ VRAM). Enterprise-level, best quality.".into(),
|
|
_ => "Unknown tier".into(),
|
|
}
|
|
}
|
|
|
|
// ---- Installation ----
|
|
|
|
#[tauri::command]
|
|
pub async fn start_install(
|
|
tier: u8,
|
|
features: Vec<String>,
|
|
install_dir: Option<String>,
|
|
) -> Result<String, String> {
|
|
validate_install_request(tier, &features)?;
|
|
|
|
let dir = install_dir
|
|
.map(std::path::PathBuf::from)
|
|
.unwrap_or_else(installer::default_install_dir);
|
|
|
|
let state = std::sync::Arc::new(Mutex::new(InstallState {
|
|
phase: InstallPhase::Installing,
|
|
install_dir: Some(dir.to_string_lossy().to_string()),
|
|
selected_tier: Some(tier),
|
|
selected_features: features.clone(),
|
|
..Default::default()
|
|
}));
|
|
|
|
let state_clone = state.clone();
|
|
|
|
// Run installation in a blocking thread
|
|
tokio::task::spawn_blocking(move || installer::run_install(state_clone, dir, tier, features))
|
|
.await
|
|
.map_err(|e| format!("Install task failed: {}", e))?
|
|
.map(|_| "Installation complete!".to_string())
|
|
}
|
|
|
|
fn validate_install_request(tier: u8, features: &[String]) -> Result<(), String> {
|
|
if tier > 4 {
|
|
return Err(format!("Unsupported install tier: {}", tier));
|
|
}
|
|
|
|
for feature in features {
|
|
if !ALLOWED_FEATURES.contains(&feature.as_str()) {
|
|
return Err(format!("Unsupported feature: {}", feature));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// ---- Progress ----
|
|
|
|
#[tauri::command]
|
|
pub fn get_install_progress() -> ProgressInfo {
|
|
// Read from persisted state
|
|
let state_path = state_file_path();
|
|
if let Ok(data) = std::fs::read_to_string(&state_path) {
|
|
if let Ok(state) = serde_json::from_str::<InstallState>(&data) {
|
|
return ProgressInfo {
|
|
phase: format!("{:?}", state.phase),
|
|
percent: state.progress_pct,
|
|
message: state.progress_message,
|
|
error: state.error,
|
|
};
|
|
}
|
|
}
|
|
|
|
ProgressInfo {
|
|
phase: "unknown".into(),
|
|
percent: 0,
|
|
message: "Waiting for installer...".into(),
|
|
error: None,
|
|
}
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct ProgressInfo {
|
|
pub phase: String,
|
|
pub percent: u8,
|
|
pub message: String,
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
// ---- State ----
|
|
|
|
#[tauri::command]
|
|
pub fn get_install_state() -> InstallState {
|
|
let state_path = state_file_path();
|
|
if let Ok(data) = std::fs::read_to_string(&state_path) {
|
|
if let Ok(state) = serde_json::from_str::<InstallState>(&data) {
|
|
return state;
|
|
}
|
|
}
|
|
InstallState::default()
|
|
}
|
|
|
|
// ---- Open ODS ----
|
|
|
|
#[tauri::command]
|
|
pub fn open_ods() -> Result<(), String> {
|
|
let url = "http://localhost:3000";
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
std::process::Command::new("cmd")
|
|
.args(["/C", "start", url])
|
|
.spawn()
|
|
.map_err(|e| format!("Failed to open browser: {}", e))?;
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
std::process::Command::new("open")
|
|
.arg(url)
|
|
.spawn()
|
|
.map_err(|e| format!("Failed to open browser: {}", e))?;
|
|
}
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
std::process::Command::new("xdg-open")
|
|
.arg(url)
|
|
.spawn()
|
|
.map_err(|e| format!("Failed to open browser: {}", e))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// ---- Helpers ----
|
|
|
|
fn state_file_path() -> std::path::PathBuf {
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
let base = std::env::var("LOCALAPPDATA").unwrap_or_else(|_| "C:\\ProgramData".into());
|
|
std::path::PathBuf::from(base)
|
|
.join("ods")
|
|
.join("installer-state.json")
|
|
}
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
|
|
std::path::PathBuf::from(home)
|
|
.join("Library/Application Support/ods/installer-state.json")
|
|
}
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
let base = std::env::var("XDG_DATA_HOME").unwrap_or_else(|_| {
|
|
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
|
|
format!("{}/.local/share", home)
|
|
});
|
|
std::path::PathBuf::from(base)
|
|
.join("ods")
|
|
.join("installer-state.json")
|
|
}
|
|
}
|