use colored::Colorize;
use std::process::Command;
use crate::console::{self, icon_fail, icon_info, icon_ok, icon_play, icon_warn};
pub fn run(lang: &str) {
ensure_package_manager();
let lang_norm = lang.to_lowercase();
match lang_norm.as_str() {
"python" | "py" => install_python(),
"php" => install_php(),
"ruby" | "rb" => install_ruby(),
"nodejs" | "node" | "js" => install_nodejs(),
"tina4-js" | "tina4js" | "js-frontend" => install_tina4_js(),
"all" => {
install_python();
install_php();
install_ruby();
install_nodejs();
}
_ => {
eprintln!(
"{} Unknown target: {}. Use: python, php, ruby, nodejs, tina4-js, all",
icon_fail().red(),
lang
);
std::process::exit(1);
}
}
}
fn install_python() {
println!("\n{} Installing Python...", icon_play().green());
if check_exists("python3") || check_exists("python") {
println!(" {} Python already installed", icon_ok().green());
} else {
run_install_commands(&[
("choco", &["install", "python", "-y"]),
("brew", &["install", "python@3.12"]),
("sudo", &["apt-get", "install", "-y", "python3.12", "python3.12-venv"]),
]);
}
if check_exists("uv") {
println!(" {} uv already installed", icon_ok().green());
} else {
println!(" {} Installing uv...", icon_play().green());
if console::is_windows() {
let py = if check_exists("python") { "python" } else { "python3" };
let pip_ok = Command::new(py)
.args(["-m", "pip", "install", "--upgrade", "uv"])
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()
.map(|s| s.success())
.unwrap_or(false);
if pip_ok {
add_python_scripts_to_path_windows(py);
}
if !check_exists("uv") {
let _ = Command::new("powershell")
.args([
"-ExecutionPolicy", "ByPass",
"-NoProfile",
"-Command", "irm https://astral.sh/uv/install.ps1 | iex",
])
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status();
refresh_uv_path_windows();
}
if check_exists("uv") {
println!(" {} uv installed", icon_ok().green());
} else {
eprintln!(
" {} uv not installed — open a new terminal and run: python -m pip install uv",
icon_fail().red()
);
}
} else {
let status = Command::new("sh")
.args(["-c", "curl -LsSf https://astral.sh/uv/install.sh | sh"])
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status();
match status {
Ok(s) if s.success() => {
println!(" {} uv installed", icon_ok().green());
refresh_uv_path_unix();
}
Ok(s) => eprintln!(" {} uv installer exited with {}", icon_fail().red(), s),
Err(e) => eprintln!(" {} Failed to launch shell: {}", icon_fail().red(), e),
}
}
}
install_tina4_cli("tina4python", "uv", &["tool", "install", "tina4-python"]);
}
fn refresh_uv_path_windows() {
let Ok(home) = std::env::var("USERPROFILE") else { return };
let candidates = [
format!("{home}\\.local\\bin"),
format!("{home}\\.cargo\\bin"),
];
let current = std::env::var("PATH").unwrap_or_default();
let mut parts: Vec<String> = current.split(';').map(|s| s.to_string()).collect();
for c in &candidates {
if std::path::Path::new(c).exists() && !parts.iter().any(|p| p.eq_ignore_ascii_case(c)) {
parts.insert(0, c.clone());
}
}
std::env::set_var("PATH", parts.join(";"));
}
fn add_python_scripts_to_path_windows(py: &str) {
let Ok(out) = Command::new(py)
.args(["-c", "import sysconfig; print(sysconfig.get_path('scripts'))"])
.output()
else {
return;
};
if !out.status.success() {
return;
}
let dir = String::from_utf8_lossy(&out.stdout).trim().to_string();
if dir.is_empty() || !std::path::Path::new(&dir).exists() {
return;
}
let current = std::env::var("PATH").unwrap_or_default();
let mut parts: Vec<String> = current.split(';').map(|s| s.to_string()).collect();
if !parts.iter().any(|p| p.eq_ignore_ascii_case(&dir)) {
parts.insert(0, dir);
std::env::set_var("PATH", parts.join(";"));
}
}
fn refresh_uv_path_unix() {
let Ok(home) = std::env::var("HOME") else { return };
let candidates = [format!("{home}/.local/bin"), format!("{home}/.cargo/bin")];
let current = std::env::var("PATH").unwrap_or_default();
let mut parts: Vec<String> = current.split(':').map(|s| s.to_string()).collect();
for c in &candidates {
if std::path::Path::new(c).exists() && !parts.iter().any(|p| p == c) {
parts.insert(0, c.clone());
}
}
std::env::set_var("PATH", parts.join(":"));
}
fn install_php() {
println!("\n{} Installing PHP...", icon_play().green());
if check_exists("php") {
println!(" {} PHP already installed", icon_ok().green());
} else {
run_install_commands(&[
("choco", &["install", "php", "-y"]),
("brew", &["install", "php@8.3"]),
("sudo", &["apt-get", "install", "-y", "php8.3-cli", "php8.3-mbstring", "php8.3-xml", "php8.3-sqlite3"]),
]);
}
if check_exists("composer") {
println!(" {} Composer already installed", icon_ok().green());
} else {
println!(" {} Installing Composer...", icon_play().green());
if console::is_windows() {
run_install_commands(&[("choco", &["install", "composer", "-y"])]);
} else {
let script = r#"php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && php composer-setup.php --install-dir=/usr/local/bin --filename=composer && php -r "unlink('composer-setup.php');" "#;
let _ = console::shell_exec(script);
}
}
println!(
" {} Install tina4php: composer global require tina4stack/tina4-php",
icon_info().blue()
);
}
fn install_ruby() {
println!("\n{} Installing Ruby...", icon_play().green());
if check_exists("ruby") {
let version = get_version("ruby", "--version");
if version.starts_with("ruby 2") {
println!(
" {} System Ruby {} detected — installing modern Ruby...",
icon_warn().yellow(),
version.trim()
);
let _ = Command::new("brew")
.args(["install", "ruby"])
.status();
} else {
println!(" {} Ruby already installed ({})", icon_ok().green(), version.trim());
}
} else {
run_install_commands(&[
("choco", &["install", "ruby", "-y"]),
("brew", &["install", "ruby"]),
("sudo", &["apt-get", "install", "-y", "ruby-full"]),
]);
}
if check_exists("bundle") {
println!(" {} Bundler already installed", icon_ok().green());
} else {
println!(" {} Installing Bundler...", icon_play().green());
let _ = Command::new("gem")
.args(["install", "bundler"])
.status();
}
install_tina4_cli("tina4ruby", "gem", &["install", "tina4ruby"]);
}
fn install_nodejs() {
println!("\n{} Installing Node.js...", icon_play().green());
if check_exists("node") {
println!(" {} Node.js already installed", icon_ok().green());
} else {
run_install_commands(&[
("choco", &["install", "nodejs-lts", "-y"]),
("brew", &["install", "node@22"]),
("sh", &["-c", "curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt-get install -y nodejs"]),
]);
}
if check_exists("npm") {
println!(" {} npm already installed", icon_ok().green());
}
install_tina4_cli("tina4nodejs", "npm", &["install", "-g", "tina4nodejs"]);
}
fn install_tina4_js() {
println!("\n{} Installing tina4-js...", icon_play().green());
let dest = std::path::Path::new("src/public/js");
if !dest.exists() {
std::fs::create_dir_all(dest).unwrap_or_else(|e| {
eprintln!(" {} Failed to create {}: {}", icon_fail().red(), dest.display(), e);
});
}
let target = dest.join("tina4js.min.js");
let url = "https://raw.githubusercontent.com/tina4stack/tina4-js/master/dist/tina4js.min.js";
println!(" {} Downloading from {}", icon_play().green(), "tina4stack/tina4-js".cyan());
let download_status = if console::is_windows() {
Command::new("powershell")
.args([
"-NoProfile",
"-Command",
&format!(
"Invoke-WebRequest -Uri '{}' -OutFile '{}'",
url,
target.display()
),
])
.status()
} else {
Command::new("sh")
.args([
"-c",
&format!("curl -fsSL '{}' -o '{}'", url, target.display()),
])
.status()
};
match download_status {
Ok(s) if s.success() => {
println!(" {} tina4js.min.js installed at {}", icon_ok().green(), target.display());
}
_ => {
let framework_paths = [
"tina4_python/public/js/tina4js.min.js",
"src/public/js/tina4js.min.js",
"lib/tina4/public/js/tina4js.min.js",
"packages/core/public/js/tina4js.min.js",
];
let mut found = false;
for path in &framework_paths {
let p = std::path::Path::new(path);
if p.exists() && std::fs::copy(p, &target).is_ok() {
println!(" {} Copied from framework bundle", icon_ok().green());
found = true;
break;
}
}
if !found {
eprintln!(
" {} Download failed. tina4js.min.js is bundled with the framework at /js/tina4js.min.js",
icon_warn().yellow()
);
}
}
}
println!();
println!(" Usage in your template:");
println!(" {}", "<script src=\"/js/tina4js.min.js\"></script>".cyan());
println!();
}
pub fn ensure_package_manager() -> bool {
if console::is_windows() {
if check_exists("choco") {
println!(" {} Chocolatey already installed", icon_ok().green());
return true;
}
println!(" {} Installing Chocolatey (Windows package manager)...", icon_play().green());
let bootstrap = "Set-ExecutionPolicy Bypass -Scope Process -Force; \
[System.Net.ServicePointManager]::SecurityProtocol = \
[System.Net.ServicePointManager]::SecurityProtocol -bor 3072; \
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))";
let _ = Command::new("powershell")
.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", bootstrap])
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status();
refresh_choco_path_windows();
if check_exists("choco") {
println!(" {} Chocolatey installed", icon_ok().green());
true
} else {
eprintln!(
" {} Chocolatey install failed — re-run this from a terminal opened \
'as Administrator'.",
icon_fail().red()
);
false
}
} else if cfg!(target_os = "macos") {
if check_exists("brew") {
println!(" {} Homebrew already installed", icon_ok().green());
return true;
}
println!(" {} Installing Homebrew (macOS package manager)...", icon_play().green());
let bootstrap = "NONINTERACTIVE=1 /bin/bash -c \"$(curl -fsSL \
https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"";
let _ = Command::new("sh")
.args(["-c", bootstrap])
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status();
refresh_brew_path_macos();
if check_exists("brew") {
println!(" {} Homebrew installed", icon_ok().green());
true
} else {
eprintln!(" {} Homebrew install failed — see https://brew.sh", icon_fail().red());
false
}
} else {
true
}
}
fn refresh_choco_path_windows() {
let dir = std::env::var("ChocolateyInstall")
.map(|p| format!("{p}\\bin"))
.unwrap_or_else(|_| "C:\\ProgramData\\chocolatey\\bin".to_string());
if !std::path::Path::new(&dir).exists() {
return;
}
let current = std::env::var("PATH").unwrap_or_default();
let mut parts: Vec<String> = current.split(';').map(|s| s.to_string()).collect();
if !parts.iter().any(|p| p.eq_ignore_ascii_case(&dir)) {
parts.insert(0, dir);
std::env::set_var("PATH", parts.join(";"));
}
}
fn refresh_brew_path_macos() {
let current = std::env::var("PATH").unwrap_or_default();
let mut parts: Vec<String> = current.split(':').map(|s| s.to_string()).collect();
for dir in ["/opt/homebrew/bin", "/usr/local/bin"] {
if std::path::Path::new(dir).exists() && !parts.iter().any(|p| p == dir) {
parts.insert(0, dir.to_string());
}
}
std::env::set_var("PATH", parts.join(":"));
}
fn check_exists(cmd: &str) -> bool {
which::which(cmd).is_ok()
}
fn get_version(cmd: &str, flag: &str) -> String {
Command::new(crate::console::resolve_cmd(cmd))
.arg(flag)
.output()
.ok()
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
.unwrap_or_default()
}
fn run_install_commands(attempts: &[(&str, &[&str])]) {
for (cmd, args) in attempts {
if check_exists(cmd) {
println!(" {} Running: {} {}", icon_play().green(), cmd, args.join(" "));
let status = Command::new(crate::console::resolve_cmd(cmd)).args(*args).status();
match status {
Ok(s) if s.success() => {
println!(" {} Installed successfully", icon_ok().green());
return;
}
_ => continue,
}
}
}
eprintln!(
" {} Could not install automatically. Please install manually.",
icon_fail().red()
);
}
fn install_tina4_cli(cli_name: &str, pkg_cmd: &str, args: &[&str]) {
if check_exists(cli_name) {
println!(" {} {} already installed", icon_ok().green(), cli_name);
} else if check_exists(pkg_cmd) {
println!(" {} Installing {}...", icon_play().green(), cli_name);
let _ = Command::new(crate::console::resolve_cmd(pkg_cmd)).args(args).status();
} else {
println!(
" {} Cannot install {} — {} not found",
icon_warn().yellow(),
cli_name,
pkg_cmd
);
}
}