feat: add dependency manager and setup wizard
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1 +1,53 @@
|
||||
// Dependency check and install commands — implemented in Task 2
|
||||
use serde::Serialize;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::process::{Command, Stdio};
|
||||
use tauri::ipc::Channel;
|
||||
|
||||
use crate::models::DependencyStatus;
|
||||
use crate::services::dependency_manager;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn check_dependencies() -> Vec<DependencyStatus> {
|
||||
dependency_manager::get_all_dependency_statuses()
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "event", content = "data")]
|
||||
pub enum InstallEvent {
|
||||
Output { line: String },
|
||||
Finished { success: bool },
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_dependency(
|
||||
name: String,
|
||||
on_event: Channel<InstallEvent>,
|
||||
) -> Result<(), String> {
|
||||
let args = dependency_manager::get_install_args(&name)?;
|
||||
let (cmd, cmd_args) = args.split_first().ok_or("Empty install command")?;
|
||||
|
||||
let mut child = Command::new(cmd)
|
||||
.args(cmd_args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to start install: {e}"))?;
|
||||
|
||||
if let Some(stdout) = child.stdout.take() {
|
||||
let reader = BufReader::new(stdout);
|
||||
for line in reader.lines().map_while(Result::ok) {
|
||||
let _ = on_event.send(InstallEvent::Output { line });
|
||||
}
|
||||
}
|
||||
|
||||
let status = child.wait().map_err(|e| format!("Install failed: {e}"))?;
|
||||
let _ = on_event.send(InstallEvent::Finished {
|
||||
success: status.success(),
|
||||
});
|
||||
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Install command exited with non-zero status".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ mod commands;
|
||||
mod models;
|
||||
mod services;
|
||||
|
||||
use commands::dependencies;
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
@@ -9,6 +11,10 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
dependencies::check_dependencies,
|
||||
dependencies::install_dependency,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
@@ -1 +1,131 @@
|
||||
// Implemented in Task 2
|
||||
use crate::models::DependencyStatus;
|
||||
use std::process::Command;
|
||||
|
||||
pub fn check_tool_exists(name: &str) -> Option<String> {
|
||||
let output = Command::new("which").arg(name).output().ok()?;
|
||||
|
||||
if output.status.success() {
|
||||
let version_output = Command::new(name).arg("--version").output().ok()?;
|
||||
let version = String::from_utf8_lossy(&version_output.stdout)
|
||||
.lines()
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string();
|
||||
Some(version)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_js_runtime() -> Option<String> {
|
||||
check_tool_exists("deno").or_else(|| check_tool_exists("node"))
|
||||
}
|
||||
|
||||
pub fn check_pot_plugin() -> bool {
|
||||
let output = Command::new("yt-dlp")
|
||||
.args(["-v", "--simulate", "https://www.youtube.com/watch?v=dQw4w9WgXcQ"])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(out) => {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
stderr.contains("GetPOT")
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_all_dependency_statuses() -> Vec<DependencyStatus> {
|
||||
let ffmpeg = check_tool_exists("ffmpeg");
|
||||
let ytdlp = check_tool_exists("yt-dlp");
|
||||
let js_runtime = check_js_runtime();
|
||||
let pot_plugin = if ytdlp.is_some() {
|
||||
check_pot_plugin()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
vec![
|
||||
DependencyStatus {
|
||||
name: "ffmpeg".to_string(),
|
||||
installed: ffmpeg.is_some(),
|
||||
version: ffmpeg,
|
||||
install_command: "brew install ffmpeg".to_string(),
|
||||
},
|
||||
DependencyStatus {
|
||||
name: "yt-dlp".to_string(),
|
||||
installed: ytdlp.is_some(),
|
||||
version: ytdlp,
|
||||
install_command: "brew install yt-dlp".to_string(),
|
||||
},
|
||||
DependencyStatus {
|
||||
name: "JavaScript runtime (deno/node)".to_string(),
|
||||
installed: js_runtime.is_some(),
|
||||
version: js_runtime,
|
||||
install_command: "brew install deno".to_string(),
|
||||
},
|
||||
DependencyStatus {
|
||||
name: "bgutil-ytdlp-pot-provider".to_string(),
|
||||
installed: pot_plugin,
|
||||
version: None,
|
||||
install_command: "python3 -m pip install -U bgutil-ytdlp-pot-provider".to_string(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
pub fn get_install_args(dep_name: &str) -> Result<Vec<String>, String> {
|
||||
match dep_name {
|
||||
"ffmpeg" => Ok(vec![
|
||||
"brew".into(), "install".into(), "ffmpeg".into(),
|
||||
]),
|
||||
"yt-dlp" => Ok(vec![
|
||||
"brew".into(), "install".into(), "yt-dlp".into(),
|
||||
]),
|
||||
"JavaScript runtime (deno/node)" => Ok(vec![
|
||||
"brew".into(), "install".into(), "deno".into(),
|
||||
]),
|
||||
"bgutil-ytdlp-pot-provider" => Ok(vec![
|
||||
"python3".into(), "-m".into(), "pip".into(), "install".into(), "-U".into(), "bgutil-ytdlp-pot-provider".into(),
|
||||
]),
|
||||
_ => Err(format!("Unknown dependency: {dep_name}")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_install_args_ffmpeg() {
|
||||
let args = get_install_args("ffmpeg").unwrap();
|
||||
assert_eq!(args, vec!["brew", "install", "ffmpeg"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_install_args_ytdlp() {
|
||||
let args = get_install_args("yt-dlp").unwrap();
|
||||
assert_eq!(args, vec!["brew", "install", "yt-dlp"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_install_args_pot_provider() {
|
||||
let args = get_install_args("bgutil-ytdlp-pot-provider").unwrap();
|
||||
assert_eq!(args, vec!["python3", "-m", "pip", "install", "-U", "bgutil-ytdlp-pot-provider"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_install_args_unknown() {
|
||||
assert!(get_install_args("unknown-tool").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dependency_statuses_has_four_entries() {
|
||||
let statuses = get_all_dependency_statuses();
|
||||
assert_eq!(statuses.len(), 4);
|
||||
assert_eq!(statuses[0].name, "ffmpeg");
|
||||
assert_eq!(statuses[1].name, "yt-dlp");
|
||||
assert_eq!(statuses[2].name, "JavaScript runtime (deno/node)");
|
||||
assert_eq!(statuses[3].name, "bgutil-ytdlp-pot-provider");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user