use crate::console::{icon_fail, icon_info, icon_ok, icon_play, icon_warn};
use colored::Colorize;
use std::fs;
use std::path::Path;
pub fn run() {
println!(
"\n{}",
" Tina4 Upgrade — v2 → v3 ".on_bright_black().white()
);
println!();
let lang = detect_v2_project();
if lang.is_none() {
eprintln!(
"{} No Tina4 v2 project detected in current directory",
icon_fail().red()
);
eprintln!(
"{} This command upgrades v2 projects. If this is already v3, no action needed.",
icon_info().blue()
);
std::process::exit(1);
}
let lang = lang.unwrap();
println!(
"{} Detected v2 {} project — upgrading to v3",
icon_play().green(),
lang.cyan()
);
let mut changes = 0;
changes += move_dir_into_src("routes");
changes += move_dir_into_src("orm");
changes += move_dir_into_src("templates");
changes += move_dir_into_src("scss");
changes += move_dir_into_src("public");
changes += move_dir_into_src("services");
changes += move_dir_into_src("app");
changes += move_dir_into_src("locales");
changes += move_dir_into_src("seeds");
if !Path::new("src").exists() {
fs::create_dir_all("src").ok();
}
changes += upgrade_manifest(&lang);
delegate_upgrade(&lang);
println!();
if changes > 0 {
println!(
"{} Upgrade complete — {} changes applied",
icon_ok().green(),
changes.to_string().cyan()
);
} else {
println!(
"{} Project already appears to be v3 structure — no changes needed",
icon_info().blue()
);
}
println!(
"{} Review the changes and run your test suite to verify",
icon_info().blue()
);
println!();
}
fn detect_v2_project() -> Option<String> {
let has_toplevel_routes = Path::new("routes").is_dir() && !Path::new("src/routes").is_dir();
let has_toplevel_orm = Path::new("orm").is_dir() && !Path::new("src/orm").is_dir();
let has_toplevel_templates =
Path::new("templates").is_dir() && !Path::new("src/templates").is_dir();
if !has_toplevel_routes && !has_toplevel_orm && !has_toplevel_templates {
return None;
}
if let Ok(content) = fs::read_to_string("composer.json") {
if content.contains("tina4") {
return Some("php".into());
}
}
if Path::new("pyproject.toml").exists() || Path::new("requirements.txt").exists() {
return Some("python".into());
}
if Path::new("Gemfile").exists() {
return Some("ruby".into());
}
if Path::new("package.json").exists() {
return Some("nodejs".into());
}
Some("unknown".into())
}
fn move_dir_into_src(dir_name: &str) -> usize {
let src = Path::new(dir_name);
let dest = Path::new("src").join(dir_name);
if !src.is_dir() {
return 0;
}
if dest.exists() {
println!(
" {} {} — src/{} already exists, skipping",
icon_warn().yellow(),
dir_name,
dir_name
);
return 0;
}
fs::create_dir_all("src").ok();
match fs::rename(src, &dest) {
Ok(_) => {
println!(
" {} Moved {}/ → src/{}/",
icon_ok().green(),
dir_name,
dir_name
);
1
}
Err(e) => {
if copy_dir_recursive(src, &dest).is_ok() {
fs::remove_dir_all(src).ok();
println!(
" {} Moved {}/ → src/{}/",
icon_ok().green(),
dir_name,
dir_name
);
1
} else {
eprintln!(
" {} Failed to move {}/: {}",
icon_fail().red(),
dir_name,
e
);
0
}
}
}
}
fn copy_dir_recursive(src: &Path, dest: &Path) -> std::io::Result<()> {
fs::create_dir_all(dest)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let src_path = entry.path();
let dest_path = dest.join(entry.file_name());
if src_path.is_dir() {
copy_dir_recursive(&src_path, &dest_path)?;
} else {
fs::copy(&src_path, &dest_path)?;
}
}
Ok(())
}
fn upgrade_manifest(lang: &str) -> usize {
match lang {
"php" => upgrade_composer_json(),
"python" => upgrade_pyproject_toml(),
"ruby" => upgrade_gemfile(),
"nodejs" => upgrade_package_json(),
_ => 0,
}
}
fn upgrade_composer_json() -> usize {
let path = "composer.json";
let Ok(content) = fs::read_to_string(path) else { return 0 };
let updated = content
.replace("\"tina4stack/tina4php\": \"^2", "\"tina4stack/tina4php\": \"^3")
.replace("\"tina4stack/tina4php\": \"~2", "\"tina4stack/tina4php\": \"^3")
.replace("\"tina4stack/tina4php-core\"", "\"_removed_tina4php-core\"")
.replace("\"tina4stack/tina4php-database\"", "\"_removed_tina4php-database\"")
.replace("\"tina4stack/tina4php-orm\"", "\"_removed_tina4php-orm\"");
if updated != content && fs::write(path, &updated).is_ok() {
println!(" {} Updated composer.json — tina4php ^3.0", icon_ok().green());
if updated.contains("_removed_") {
println!(
" {} Removed old split packages (tina4php-core, -database, -orm) — v3 is unified",
icon_info().blue()
);
}
return 1;
}
0
}
fn upgrade_pyproject_toml() -> usize {
let path = "pyproject.toml";
if let Ok(content) = fs::read_to_string(path) {
let updated = content
.replace("tina4-python>=2", "tina4-python>=3")
.replace("tina4-python~=2", "tina4-python>=3")
.replace("tina4-python==2", "tina4-python>=3");
if updated != content && fs::write(path, &updated).is_ok() {
println!(" {} Updated pyproject.toml — tina4-python >=3", icon_ok().green());
return 1;
}
}
let req_path = "requirements.txt";
if let Ok(content) = fs::read_to_string(req_path) {
let updated = content
.replace("tina4-python>=2", "tina4-python>=3")
.replace("tina4-python~=2", "tina4-python>=3")
.replace("tina4-python==2", "tina4-python>=3");
if updated != content && fs::write(req_path, &updated).is_ok() {
println!(" {} Updated requirements.txt — tina4-python >=3", icon_ok().green());
return 1;
}
}
0
}
fn upgrade_gemfile() -> usize {
let path = "Gemfile";
let Ok(content) = fs::read_to_string(path) else { return 0 };
let updated = content
.replace("'tina4', '~> 2", "'tina4', '~> 3")
.replace("\"tina4\", \"~> 2", "\"tina4\", \"~> 3");
if updated != content && fs::write(path, &updated).is_ok() {
println!(" {} Updated Gemfile — tina4 ~> 3.0", icon_ok().green());
return 1;
}
0
}
fn upgrade_package_json() -> usize {
let path = "package.json";
let Ok(content) = fs::read_to_string(path) else { return 0 };
let updated = content
.replace("\"@tina4/core\": \"^2", "\"@tina4/core\": \"^3")
.replace("\"@tina4/core\": \"~2", "\"@tina4/core\": \"^3")
.replace("\"@tina4/orm\": \"^2", "\"@tina4/orm\": \"^3")
.replace("\"@tina4/orm\": \"~2", "\"@tina4/orm\": \"^3");
if updated != content && fs::write(path, &updated).is_ok() {
println!(" {} Updated package.json — @tina4/* ^3.0", icon_ok().green());
return 1;
}
0
}
fn delegate_upgrade(lang: &str) {
let (cmd, cli_path) = match lang {
"php" => {
let vendor_path = crate::console::php_vendor_bin("tina4php");
if Path::new(&vendor_path).exists() {
("php".to_string(), vendor_path)
} else {
return;
}
}
"python" => {
if which::which("tina4python").is_ok() {
("tina4python".to_string(), String::new())
} else {
return;
}
}
"ruby" => {
if which::which("tina4ruby").is_ok() {
("tina4ruby".to_string(), String::new())
} else {
return;
}
}
"nodejs" => {
if which::which("tina4nodejs").is_ok() {
("tina4nodejs".to_string(), String::new())
} else {
return;
}
}
_ => return,
};
println!(
"\n {} Running language-specific upgrade via {} ...",
icon_play().green(),
cmd.cyan()
);
let status = if lang == "php" {
std::process::Command::new(&cmd)
.args([cli_path.as_str(), "upgrade"])
.status()
} else {
std::process::Command::new(&cmd)
.args(["upgrade"])
.status()
};
match status {
Ok(s) if s.success() => {
println!(
" {} Language-specific upgrade complete",
icon_ok().green()
);
}
_ => {
println!(
" {} Language CLI upgrade not available — structural migration done",
icon_info().blue()
);
}
}
}