mod ide;
mod hooks;
mod fmt;
mod lint;
mod add;
mod upgrade;
mod tree;
use std::path::PathBuf;
use std::time::Instant;
use anyhow::Context;
use clap::{CommandFactory, Parser, Subcommand};
use colored::Colorize;
use cyrce_forge_core::cache::BuildCache;
use cyrce_forge_core::config::ForgeConfig;
use cyrce_forge_deps::maven::MavenResolver;
use cyrce_forge_deps::pypi::PypiResolver;
use cyrce_forge_langs::java::JavaModule;
use cyrce_forge_langs::kotlin::KotlinModule;
use cyrce_forge_langs::python::PythonModule;
#[derive(Parser)]
#[command(
name = "forge",
version,
about = "🔥 FORGE — Build system de nueva generación",
long_about = "FORGE es un build system moderno escrito en Rust.\nSoporta Java, Kotlin y Python con compilación incremental,\nejecución paralela y una configuración simple en TOML.",
after_help = "Ejemplos:\n forge init java Crear proyecto Java\n forge build Compilar el proyecto\n forge run Compilar y ejecutar\n forge test Ejecutar tests\n forge clean Limpiar artefactos\n\n🌐 https://github.com/enri312/forge"
)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(short = 'p', long = "project-dir", global = true)]
project_dir: Option<PathBuf>,
#[arg(short, long, global = true)]
verbose: bool,
}
#[derive(Subcommand)]
enum Commands {
Init {
#[arg(default_value = "java")]
lang: String,
},
New {
name: String,
#[arg(short, long, default_value = "java")]
lang: String,
},
Build {
#[arg(long)]
release: bool,
},
Run,
Test,
Clean,
Deps,
Add {
dep: String,
#[arg(short, long)]
test: bool,
},
Upgrade,
Tree,
Info,
Watch,
Task {
name: String,
},
Doctor,
Stats,
Bench,
Package,
Completions {
#[arg(value_enum)]
shell: clap_complete::Shell,
},
Ide {
target: String,
},
Fmt,
Lint,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive(tracing::Level::WARN.into()),
)
.without_time()
.init();
let cli = Cli::parse();
let project_dir = cli
.project_dir
.unwrap_or_else(|| std::env::current_dir().expect("No se puede obtener el directorio actual"));
let project_dir = std::fs::canonicalize(&project_dir)
.unwrap_or_else(|_| {
if project_dir.is_relative() {
std::env::current_dir()
.expect("No se puede obtener el directorio actual")
.join(&project_dir)
} else {
project_dir
}
});
print_banner();
let start = Instant::now();
let result = match cli.command {
Commands::Init { lang } => cmd_init(&project_dir, &lang).await,
Commands::New { name, lang } => cmd_new(&project_dir, &name, &lang).await,
Commands::Build { release } => cmd_build(&project_dir, cli.verbose, release).await,
Commands::Run => cmd_run(&project_dir, cli.verbose).await,
Commands::Test => cmd_test(&project_dir, cli.verbose).await,
Commands::Clean => cmd_clean(&project_dir).await,
Commands::Deps => cmd_deps(&project_dir).await,
Commands::Add { dep, test } => add::cmd_add(&project_dir, &dep, test).await,
Commands::Upgrade => upgrade::cmd_upgrade(&project_dir).await,
Commands::Tree => tree::cmd_tree(&project_dir).await,
Commands::Info => cmd_info(&project_dir).await,
Commands::Watch => cmd_watch(&project_dir).await,
Commands::Task { name } => cmd_task(&project_dir, &name).await,
Commands::Doctor => cmd_doctor().await,
Commands::Stats => cmd_stats(&project_dir).await,
Commands::Bench => cmd_bench(&project_dir, cli.verbose).await,
Commands::Package => cmd_package(&project_dir).await,
Commands::Ide { target } => ide::cmd_ide(&project_dir, &target).await,
Commands::Fmt => fmt::cmd_fmt(&project_dir).await,
Commands::Lint => lint::cmd_lint(&project_dir).await,
Commands::Completions { shell } => {
let mut cmd = Cli::command();
clap_complete::generate(shell, &mut cmd, "forge", &mut std::io::stdout());
Ok(())
}
};
if let Err(e) = &result {
eprintln!("\n{} {}", "❌ Error:".red().bold(), e);
if let Some(forge_err) = e.downcast_ref::<cyrce_forge_core::error::ForgeError>() {
eprintln!("{}", forge_err.suggestion().yellow());
} else {
eprintln!(
"{}",
" Usa 'forge --help' para ver los comandos disponibles.".dimmed()
);
}
std::process::exit(1);
}
let elapsed = start.elapsed();
if elapsed.as_millis() > 100 {
println!(
"{}",
format!("⏱️ Completado en {:.2}s", elapsed.as_secs_f64()).dimmed()
);
}
Ok(())
}
fn print_banner() {
println!(
"{}",
r#"
███████╗ ██████╗ ██████╗ ██████╗ ███████╗
██╔════╝██╔═══██╗██╔══██╗██╔════╝ ██╔════╝
█████╗ ██║ ██║██████╔╝██║ ███╗█████╗
██╔══╝ ██║ ██║██╔══██╗██║ ██║██╔══╝
██║ ╚██████╔╝██║ ██║╚██████╔╝███████╗
╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝
"#
.cyan()
.bold()
);
}
async fn cmd_init(project_dir: &PathBuf, lang: &str) -> anyhow::Result<()> {
println!(
"{}",
format!("🆕 Inicializando proyecto {} en {:?}...", lang, project_dir).bold()
);
let forge_toml = project_dir.join("forge.toml");
if forge_toml.exists() {
println!(
"{}",
"⚠️ Ya existe un forge.toml en este directorio".yellow()
);
return Ok(());
}
std::fs::create_dir_all(project_dir)?;
let template = ForgeConfig::generate_template(lang)?;
std::fs::write(&forge_toml, &template)?;
println!(" {} forge.toml", "✅ Creado:".green());
let source_dir = match lang {
"java" => "src/main/java",
"kotlin" => "src/main/kotlin",
"python" => "src",
_ => "src",
};
let test_dir = match lang {
"java" => "src/test/java",
"kotlin" => "src/test/kotlin",
"python" => "tests",
_ => "tests",
};
let full_source_dir = project_dir.join(source_dir);
std::fs::create_dir_all(&full_source_dir)?;
println!(
" {} {}",
"✅ Creado:".green(),
source_dir
);
let full_test_dir = project_dir.join(test_dir);
std::fs::create_dir_all(&full_test_dir)?;
println!(
" {} {}",
"✅ Creado:".green(),
test_dir
);
create_example_file(lang, &full_source_dir)?;
create_test_file(lang, &full_test_dir)?;
let gitignore = project_dir.join(".gitignore");
if !gitignore.exists() {
std::fs::write(
&gitignore,
"# FORGE\nbuild/\n.forge/\n\n# IDE\n.idea/\n.vscode/\n*.iml\n\n# OS\n.DS_Store\nThumbs.db\n",
)?;
println!(" {} .gitignore", "✅ Creado:".green());
}
println!();
println!(
"{}",
"🎉 ¡Proyecto inicializado! Próximos pasos:".green().bold()
);
println!(" 1. Edita {} para configurar tu proyecto", "forge.toml".cyan());
println!(" 2. Escribe tu código en {}", source_dir.cyan());
println!(" 3. Ejecuta {} para compilar tu programa", "forge build".cyan());
println!(" 4. Ejecuta {} para validar los tests", "forge test".cyan());
println!(" 5. Ejecuta {} para correr tu programa", "forge run".cyan());
println!();
Ok(())
}
fn create_example_file(lang: &str, source_dir: &PathBuf) -> anyhow::Result<()> {
match lang {
"java" => {
let file = source_dir.join("Main.java");
if !file.exists() {
std::fs::write(
&file,
r#"public class Main {
public static void main(String[] args) {
System.out.println("🔥 ¡Hola desde FORGE! — Proyecto Java");
System.out.println(" Build system de nueva generación");
}
}
"#,
)?;
println!(" {} Main.java (ejemplo)", "✅ Creado:".green());
}
}
"kotlin" => {
let file = source_dir.join("Main.kt");
if !file.exists() {
std::fs::write(
&file,
r#"fun main() {
println("🔥 ¡Hola desde FORGE! — Proyecto Kotlin")
println(" Build system de nueva generación")
}
"#,
)?;
println!(" {} Main.kt (ejemplo)", "✅ Creado:".green());
}
}
"python" => {
let file = source_dir.join("main.py");
if !file.exists() {
std::fs::write(
&file,
r#"#!/usr/bin/env python3
"""🔥 Proyecto de ejemplo FORGE — Python"""
def main():
print("🔥 ¡Hola desde FORGE! — Proyecto Python")
print(" Build system de nueva generación")
if __name__ == "__main__":
main()
"#,
)?;
println!(" {} main.py (ejemplo)", "✅ Creado:".green());
}
}
_ => {}
}
Ok(())
}
fn create_test_file(lang: &str, test_dir: &PathBuf) -> anyhow::Result<()> {
match lang {
"java" => {
let file = test_dir.join("MainTest.java");
if !file.exists() {
std::fs::write(
&file,
r#"import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class MainTest {
@Test
void forgeTestWorks() {
assertEquals(2, 1 + 1, "FORGE Test Runner debería funcionar correctamente");
}
}
"#,
)?;
println!(" {} MainTest.java (ejemplo de test)", "✅ Creado:".green());
}
}
"kotlin" => {
let file = test_dir.join("MainTest.kt");
if !file.exists() {
std::fs::write(
&file,
r#"import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.assertEquals
class MainTest {
@Test
fun `forge test works`() {
assertEquals(2, 1 + 1, "FORGE Test Runner debería funcionar correctamente")
}
}
"#,
)?;
println!(" {} MainTest.kt (ejemplo de test)", "✅ Creado:".green());
}
}
"python" => {
let file = test_dir.join("test_main.py");
if !file.exists() {
std::fs::write(
&file,
r#"def test_forge_works():
assert 1 + 1 == 2, "FORGE Test Runner debería funcionar correctamente"
"#,
)?;
println!(" {} test_main.py (ejemplo de test)", "✅ Creado:".green());
}
}
_ => {}
}
Ok(())
}
async fn cmd_build(project_dir: &PathBuf, _verbose: bool, release: bool) -> anyhow::Result<()> {
let config = ForgeConfig::load(project_dir)?;
if !config.modules.is_empty() {
println!(
"{}",
format!("📦 Workspace detectado: {} sub-módulos", config.modules.len()).cyan().bold()
);
for module_path in &config.modules {
let module_dir = project_dir.join(module_path);
if !module_dir.join("forge.toml").exists() {
println!(
" {}",
format!("⚠️ Módulo '{}' no tiene forge.toml, saltando...", module_path).yellow()
);
continue;
}
println!(
" {}",
format!("🔨 Compilando módulo: {}", module_path).cyan()
);
let module_dir_buf = module_dir.to_path_buf();
Box::pin(cmd_build(&module_dir_buf, _verbose, release)).await?;
}
println!(
" {}",
"✅ Todos los sub-módulos compilados".green()
);
}
let source_dir = project_dir.join(config.source_dir());
let extensions = cyrce_forge_langs::extensions_for_lang(&config.project.lang);
let mut cache = BuildCache::load(project_dir)?;
if !cache.has_changes(&source_dir, extensions)? {
println!(
"{}",
"⚡ Sin cambios detectados — usando caché local".dimmed()
);
return Ok(());
}
let output_dir_name = &config.project.output_dir;
let mut used_remote = false;
if let Some(remote_cfg) = &config.cache {
cache.update_hashes(&source_dir, extensions)?;
if cache.download_from_remote(project_dir, output_dir_name, remote_cfg).await? {
used_remote = true;
cache.save(project_dir)?;
}
}
if !used_remote {
hooks::run_pre_build(&config.hooks, project_dir).await?;
if !config.dependencies.is_empty() {
resolve_dependencies(&config, project_dir).await?;
}
match config.project.lang.as_str() {
"java" => JavaModule::compile(&config, project_dir).await?,
"kotlin" => KotlinModule::compile(&config, project_dir).await?,
"python" => PythonModule::compile(&config, project_dir).await?,
_ => {}
}
cache.update_hashes(&source_dir, extensions)?;
cache.save(project_dir)?;
if let Some(remote_cfg) = &config.cache {
cache.upload_to_remote(project_dir, output_dir_name, remote_cfg).await?;
}
}
hooks::run_post_build(&config.hooks, project_dir).await?;
Ok(())
}
async fn cmd_run(project_dir: &PathBuf, verbose: bool) -> anyhow::Result<()> {
cmd_build(project_dir, verbose, false).await?;
let config = ForgeConfig::load(project_dir)?;
match config.project.lang.as_str() {
"java" => JavaModule::run(&config, project_dir).await?,
"kotlin" => KotlinModule::run(&config, project_dir).await?,
"python" => PythonModule::run(&config, project_dir).await?,
_ => {}
}
Ok(())
}
async fn cmd_test(project_dir: &PathBuf, verbose: bool) -> anyhow::Result<()> {
let config = ForgeConfig::load(project_dir)?;
println!("{}", "🧪 Ejecutando tests...".bold());
hooks::run_pre_test(&config.hooks, project_dir).await?;
match config.project.lang.as_str() {
"java" => {
cmd_build(project_dir, verbose, false).await?;
JavaModule::test(&config, project_dir).await?;
}
"kotlin" => {
cmd_build(project_dir, verbose, false).await?;
KotlinModule::test(&config, project_dir).await?;
}
"python" => PythonModule::test(&config, project_dir).await?,
_ => {}
}
hooks::run_post_test(&config.hooks, project_dir).await?;
Ok(())
}
async fn cmd_clean(project_dir: &PathBuf) -> anyhow::Result<()> {
println!("{}", "🧹 Limpiando artefactos...".bold());
let build_dir = project_dir.join("build");
if build_dir.exists() {
std::fs::remove_dir_all(&build_dir)?;
println!(" {} build/", "🗑️ Eliminado:".green());
}
BuildCache::clean(project_dir)?;
println!(" {} .forge/", "🗑️ Eliminado:".green());
println!("\n{}", "✅ Limpieza completada".green().bold());
Ok(())
}
async fn cmd_deps(project_dir: &PathBuf) -> anyhow::Result<()> {
let config = ForgeConfig::load(project_dir)?;
if config.dependencies.is_empty() {
println!("{}", "📦 No hay dependencias definidas en forge.toml".dimmed());
return Ok(());
}
resolve_dependencies(&config, project_dir).await
}
async fn resolve_dependencies(config: &ForgeConfig, project_dir: &PathBuf) -> anyhow::Result<()> {
match config.project.lang.as_str() {
"java" | "kotlin" => {
let mut resolver = MavenResolver::new(project_dir);
if !config.dependencies.is_empty() {
resolver.resolve_all(&config.dependencies).await?;
}
if !config.test_dependencies.is_empty() {
resolver.resolve_test_deps(&config.test_dependencies).await?;
}
}
"python" => {
let resolver = PypiResolver::new();
if !config.dependencies.is_empty() {
resolver.verify_all(&config.dependencies).await?;
}
}
_ => {}
}
Ok(())
}
async fn cmd_info(project_dir: &PathBuf) -> anyhow::Result<()> {
let config = ForgeConfig::load(project_dir)
.context("No se encontró forge.toml. ¿Estás en un proyecto FORGE?")?;
println!("{}", "ℹ️ Información del Proyecto".bold());
println!(" {} {}", "Nombre:".cyan(), config.project.name);
println!(" {} {}", "Versión:".cyan(), config.project.version);
println!(" {} {}", "Lenguaje:".cyan(), config.project.lang);
println!(" {} {}", "Fuente:".cyan(), config.source_dir());
println!(" {} {}", "Salida:".cyan(), config.project.output_dir);
if let Some(main) = config.main_entry() {
println!(" {} {}", "Entrada:".cyan(), main);
}
if !config.dependencies.is_empty() {
println!("\n {} ({}):", "Dependencias".cyan(), config.dependencies.len());
for (name, version) in &config.dependencies {
println!(" • {} = {}", name, version);
}
}
if !config.tasks.is_empty() {
println!("\n {} ({}):", "Tareas".cyan(), config.tasks.len());
for (name, task) in &config.tasks {
println!(" • {} — {}", name, task.command);
}
}
println!("\n{}", "🔧 Herramientas del Sistema".bold());
print_tool_version("Rust", "rustc", &["--version"]);
match config.project.lang.as_str() {
"java" => {
print_tool_version("Java", "javac", &["--version"]);
print_tool_version("JVM", "java", &["--version"]);
}
"kotlin" => {
print_tool_version("Kotlin", "kotlinc", &["-version"]);
print_tool_version("JVM", "java", &["--version"]);
}
"python" => {
print_tool_version("Python", "python", &["--version"]);
print_tool_version("Pip", "pip", &["--version"]);
}
_ => {}
}
println!();
Ok(())
}
fn print_tool_version(name: &str, cmd: &str, args: &[&str]) {
match std::process::Command::new(cmd).args(args).output() {
Ok(output) => {
let version = String::from_utf8_lossy(&output.stdout);
let version = version.trim();
if version.is_empty() {
let stderr = String::from_utf8_lossy(&output.stderr);
let version = stderr.lines().next().unwrap_or("").trim();
println!(" {} {}", format!("{}:", name).cyan(), version);
} else {
let first_line = version.lines().next().unwrap_or(version);
println!(" {} {}", format!("{}:", name).cyan(), first_line);
}
}
Err(_) => {
println!(
" {} {}",
format!("{}:", name).cyan(),
"No encontrado ❌".red()
);
}
}
}
async fn cmd_new(parent_dir: &PathBuf, name: &str, lang: &str) -> anyhow::Result<()> {
let project_dir = parent_dir.join(name);
if project_dir.exists() {
return Err(anyhow::anyhow!(
"El directorio '{}' ya existe",
project_dir.display()
));
}
println!(
"{}",
format!("📁 Creando proyecto '{}' ({})...", name, lang).bold()
);
std::fs::create_dir_all(&project_dir)?;
cmd_init(&project_dir, lang).await?;
println!(
"\n{}",
format!("💡 Para empezar: cd {} && forge build", name)
.cyan()
.bold()
);
Ok(())
}
async fn cmd_watch(project_dir: &PathBuf) -> anyhow::Result<()> {
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher, Event, EventKind};
use std::sync::mpsc;
let config = ForgeConfig::load(project_dir)?;
let source_dir = project_dir.join(config.source_dir());
if !source_dir.exists() {
return Err(anyhow::anyhow!(
"Directorio de código fuente no encontrado: {}",
source_dir.display()
));
}
println!(
"{}",
format!(
"👁️ Vigilando cambios en {} (Ctrl+C para detener)...",
config.source_dir()
)
.cyan()
.bold()
);
println!("{}", "\n── Build inicial ──".dimmed());
if let Err(e) = cmd_build(project_dir, false, false).await {
eprintln!(" {} {}", "⚠️ Error en build:".yellow(), e);
}
let (tx, rx) = mpsc::channel::<notify::Result<Event>>();
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
watcher.watch(&source_dir, RecursiveMode::Recursive)?;
let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
let r = running.clone();
ctrlc::set_handler(move || {
r.store(false, std::sync::atomic::Ordering::SeqCst);
})?;
println!(
"{}",
"✅ Watcher activo — editá tu código y FORGE recompilará automáticamente\n".green()
);
let extensions = cyrce_forge_langs::extensions_for_lang(&config.project.lang);
while running.load(std::sync::atomic::Ordering::SeqCst) {
match rx.recv_timeout(std::time::Duration::from_millis(500)) {
Ok(Ok(event)) => {
let is_relevant = event.paths.iter().any(|p| {
if let Some(ext) = p.extension() {
extensions.iter().any(|e| ext == *e)
} else {
false
}
});
if is_relevant && matches!(event.kind, EventKind::Modify(_) | EventKind::Create(_)) {
let changed_files: Vec<String> = event
.paths
.iter()
.filter_map(|p| p.file_name())
.map(|f| f.to_string_lossy().to_string())
.collect();
println!(
"\n{}",
format!(
"🔄 Cambios detectados: {} — Recompilando...",
changed_files.join(", ")
)
.yellow()
.bold()
);
let start = Instant::now();
match cmd_build(project_dir, false, false).await {
Ok(_) => {
println!(
"{}",
format!(
"✅ Build exitoso en {:.2}s — Esperando más cambios...\n",
start.elapsed().as_secs_f64()
)
.green()
);
}
Err(e) => {
eprintln!(
"{}",
format!("❌ Error: {} — Corrige y guarda de nuevo\n", e).red()
);
}
}
}
}
Ok(Err(e)) => {
eprintln!(" {} {}", "⚠️ Error del watcher:".yellow(), e);
}
Err(mpsc::RecvTimeoutError::Timeout) => {}
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
}
println!("\n{}", "👋 Watch mode detenido".dimmed());
Ok(())
}
async fn cmd_task(project_dir: &PathBuf, task_name: &str) -> anyhow::Result<()> {
let config = ForgeConfig::load(project_dir)?;
let task = config
.tasks
.get(task_name)
.ok_or_else(|| {
let available: Vec<&String> = config.tasks.keys().collect();
if available.is_empty() {
anyhow::anyhow!(
"No hay tareas definidas en forge.toml. Agrega una sección [tasks.{}]",
task_name
)
} else {
anyhow::anyhow!(
"Tarea '{}' no encontrada. Disponibles: {}",
task_name,
available
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", ")
)
}
})?;
println!(
"{}",
format!("⚙️ Ejecutando tarea: {}", task_name).bold()
);
println!(" {} {}", "Comando:".dimmed(), task.command);
let output = if cfg!(target_os = "windows") {
std::process::Command::new("cmd")
.args(["/C", &task.command])
.current_dir(project_dir)
.output()?
} else {
std::process::Command::new("sh")
.args(["-c", &task.command])
.current_dir(project_dir)
.output()?
};
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if !stdout.is_empty() {
println!("\n{}", stdout.trim());
}
if !stderr.is_empty() {
eprintln!("{}", stderr.trim());
}
if output.status.success() {
println!(
"\n{}",
format!("✅ Tarea '{}' completada", task_name).green().bold()
);
} else {
return Err(anyhow::anyhow!(
"La tarea '{}' falló con código {}",
task_name,
output.status.code().unwrap_or(-1)
));
}
Ok(())
}
async fn cmd_doctor() -> anyhow::Result<()> {
println!("{}", "🩺 Diagnóstico del Sistema FORGE".bold());
println!("{}", "─".repeat(50).dimmed());
let mut all_ok = true;
let mut missing_tools: Vec<(&str, &str)> = Vec::new();
let checks: Vec<(&str, &str, Vec<&str>, bool, &str)> = vec![
("Rust (rustc)", "rustc", vec!["--version"], true,
"https://rustup.rs"),
("Cargo", "cargo", vec!["--version"], true,
"Se instala con Rust: https://rustup.rs"),
("Git", "git", vec!["--version"], true,
"https://git-scm.com/downloads"),
("Java (javac)", "javac", vec!["--version"], false,
"https://adoptium.net (Temurin JDK 21+)"),
("JVM (java)", "java", vec!["--version"], false,
"Se instala con el JDK"),
("Kotlin (kotlinc)", "kotlinc", vec!["-version"], false,
"Descargar de: https://github.com/JetBrains/kotlin/releases\n Extraer y agregar kotlinc/bin al PATH del sistema"),
("Python", "python", vec!["--version"], false,
"https://python.org/downloads"),
("Pip", "pip", vec!["--version"], false,
"Se instala con Python (python -m ensurepip)"),
];
for (label, cmd, args, required, hint) in &checks {
let result = if cfg!(target_os = "windows") && *cmd == "kotlinc" {
std::process::Command::new("cmd")
.arg("/C")
.arg(cmd)
.args(args)
.output()
} else {
std::process::Command::new(cmd).args(args).output()
};
match result {
Ok(output) if output.status.success() => {
let ver = String::from_utf8_lossy(&output.stdout);
let ver = ver.trim();
let ver = if ver.is_empty() {
let stderr = String::from_utf8_lossy(&output.stderr);
stderr.lines().next().unwrap_or("OK").trim().to_string()
} else {
ver.lines().next().unwrap_or("OK").to_string()
};
println!(" {} {} — {}", "✅".green(), label, ver.dimmed());
}
_ => {
if *required {
println!(" {} {} — {}", "❌".red(), label, "No encontrado (REQUERIDO)".red());
all_ok = false;
} else {
println!(" {} {} — {}", "⚠️ ".yellow(), label, "No encontrado (opcional)".yellow());
}
missing_tools.push((label, hint));
}
}
}
println!("\n{}", "📦 FORGE".bold());
println!(" {} {} — {}", "✅".green(), "Versión", env!("CARGO_PKG_VERSION").dimmed());
println!(" {} {} — {}", "✅".green(), "Ubicación", std::env::current_exe().unwrap_or_default().display().to_string().dimmed());
if let Some(home) = dirs::home_dir() {
let forge_cache = home.join(".forge");
let repo_cache = home.join(".forge").join("repository");
if forge_cache.exists() {
let size = dir_size(&forge_cache);
println!(" {} {} — {}", "✅".green(), "Caché global",
format!("{} ({})", forge_cache.display(), format_bytes(size)).dimmed());
} else {
println!(" {} {} — {}", "ℹ️ ".cyan(), "Caché global", "No creada aún".dimmed());
}
if repo_cache.exists() {
let count = std::fs::read_dir(&repo_cache).map(|r| r.count()).unwrap_or(0);
println!(" {} {} — {}", "✅".green(), "Dependencias", format!("{} en caché", count).dimmed());
}
}
if !missing_tools.is_empty() {
println!("\n{}", "💡 Sugerencias de instalación:".yellow().bold());
for (tool, hint) in &missing_tools {
println!(" {} {}", format!("{}:", tool).cyan(), hint);
}
}
println!("\n{}", "─".repeat(50).dimmed());
if all_ok {
println!("{}", "🎉 ¡Sistema listo para FORGE!".green().bold());
} else {
println!("{}", "⚠️ Algunas herramientas requeridas no se encontraron.".yellow().bold());
}
println!();
Ok(())
}
async fn cmd_stats(project_dir: &PathBuf) -> anyhow::Result<()> {
let config = ForgeConfig::load(project_dir)
.context("No se encontró forge.toml. ¿Estás en un proyecto FORGE?")?;
let source_dir = project_dir.join(config.source_dir());
let extensions = cyrce_forge_langs::extensions_for_lang(&config.project.lang);
println!("{}", "📊 Estadísticas del Proyecto".bold());
println!("{}", "─".repeat(45).dimmed());
println!(" {} {}", "Proyecto:".cyan(), config.project.name);
println!(" {} {}", "Lenguaje:".cyan(), config.project.lang);
if !source_dir.exists() {
println!("\n {}", "⚠️ Directorio fuente no encontrado".yellow());
return Ok(());
}
let mut total_files = 0u64;
let mut total_lines = 0u64;
let mut total_bytes = 0u64;
let mut files_by_ext: std::collections::HashMap<String, (u64, u64)> = std::collections::HashMap::new();
for entry in walkdir::WalkDir::new(&source_dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
{
let path = entry.path();
if let Some(ext) = path.extension() {
let ext_str = ext.to_string_lossy().to_string();
let is_relevant = extensions.iter().any(|e| ext_str == *e)
|| matches!(ext_str.as_str(), "toml" | "xml" | "json" | "yaml" | "yml" | "md" | "txt");
if is_relevant {
let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
let lines = std::fs::read_to_string(path)
.map(|content| content.lines().count() as u64)
.unwrap_or(0);
total_files += 1;
total_lines += lines;
total_bytes += size;
let entry = files_by_ext.entry(ext_str).or_insert((0, 0));
entry.0 += 1;
entry.1 += lines;
}
}
}
println!("\n {}", "Código Fuente:".cyan().bold());
println!(" {} {} archivos", "Archivos:".cyan(), total_files);
println!(" {} {} líneas", "Líneas:".cyan(), total_lines);
println!(" {} {}", "Tamaño:".cyan(), format_bytes(total_bytes));
if !files_by_ext.is_empty() {
println!("\n {}", "Por extensión:".cyan().bold());
let mut sorted: Vec<_> = files_by_ext.into_iter().collect();
sorted.sort_by(|a, b| b.1.1.cmp(&a.1.1));
for (ext, (count, lines)) in &sorted {
println!(" .{:<8} {} archivos, {} líneas", ext, count, lines);
}
}
let build_dir = project_dir.join(&config.project.output_dir);
if build_dir.exists() {
let build_size = dir_size(&build_dir);
println!("\n {}", "Build:".cyan().bold());
println!(" {} {}", "Artefactos:".cyan(), format_bytes(build_size));
}
if !config.dependencies.is_empty() {
println!("\n {}", "Dependencias:".cyan().bold());
println!(" {} {} definidas", "Total:".cyan(), config.dependencies.len());
}
if !config.tasks.is_empty() {
println!("\n {}", "Tareas:".cyan().bold());
for (name, task) in &config.tasks {
println!(" ⚙️ {} → {}", name.bold(), task.command.dimmed());
}
}
println!("\n{}", "─".repeat(45).dimmed());
println!();
Ok(())
}
fn dir_size(path: &std::path::Path) -> u64 {
walkdir::WalkDir::new(path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
.map(|e| e.metadata().map(|m| m.len()).unwrap_or(0))
.sum()
}
fn format_bytes(bytes: u64) -> String {
if bytes < 1024 {
format!("{} B", bytes)
} else if bytes < 1024 * 1024 {
format!("{:.1} KB", bytes as f64 / 1024.0)
} else if bytes < 1024 * 1024 * 1024 {
format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
} else {
format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
}
}
async fn cmd_bench(project_dir: &PathBuf, verbose: bool) -> anyhow::Result<()> {
let config = ForgeConfig::load(project_dir)?;
println!("{}", "⏱️ Benchmark de Compilación".bold());
println!("{}", "─".repeat(50).dimmed());
println!(" {} {}", "Proyecto:".cyan(), config.project.name);
println!(" {} {}\n", "Lenguaje:".cyan(), config.project.lang);
let runs = 3;
let mut times: Vec<f64> = Vec::new();
for i in 1..=runs {
let _ = cmd_clean(project_dir).await;
println!(
"{}",
format!(" 🔄 Ejecución {}/{}...", i, runs).dimmed()
);
let start = Instant::now();
cmd_build(project_dir, verbose, false).await?;
let elapsed = start.elapsed().as_secs_f64();
times.push(elapsed);
println!(
" {} {:.3}s\n",
"Tiempo:".cyan(),
elapsed
);
}
let avg = times.iter().sum::<f64>() / times.len() as f64;
let min = times.iter().cloned().fold(f64::MAX, f64::min);
let max = times.iter().cloned().fold(f64::MIN, f64::max);
println!("{}", "─".repeat(50).dimmed());
println!("{}", "📊 Resultados".bold());
println!(" {} {:.3}s", "Promedio:".cyan().bold(), avg);
println!(" {} {:.3}s", "Mínimo: ".green(), min);
println!(" {} {:.3}s", "Máximo: ".red(), max);
println!(" {} {}", "Ejecuciones:".dimmed(), runs);
if avg < 1.0 {
println!("\n {}", "🚀 ¡Velocidad increíble! Sub-segundo.".green().bold());
} else if avg < 5.0 {
println!("\n {}", "⚡ Compilación rápida.".green());
} else if avg < 15.0 {
println!("\n {}", "🔨 Compilación normal.".yellow());
} else {
println!("\n {}", "🐢 Compilación lenta — considera optimizar dependencias.".red());
}
println!();
Ok(())
}
async fn cmd_package(project_dir: &PathBuf) -> anyhow::Result<()> {
let config = ForgeConfig::load(project_dir)?;
println!(
"{}",
format!("📦 Empaquetando {} v{}...", config.project.name, config.project.version).bold()
);
cmd_build(project_dir, false, false).await?;
let dist_dir = project_dir.join("dist");
std::fs::create_dir_all(&dist_dir)?;
let package_name = format!(
"{}-{}-{}",
config.project.name,
config.project.version,
config.project.lang
);
match config.project.lang.as_str() {
"java" | "kotlin" => {
let build_dir = project_dir.join(&config.project.output_dir);
let jar_name = format!("{}.jar", config.project.name);
let jar_src = build_dir.join(&jar_name);
let jar_dst = dist_dir.join(format!("{}.jar", package_name));
if jar_src.exists() {
std::fs::copy(&jar_src, &jar_dst)?;
let size = std::fs::metadata(&jar_dst)?.len();
println!(" {} {} ({})", "✅ JAR:".green(), jar_dst.display(), format_bytes(size));
} else {
let classes_dir = build_dir.join("classes");
if classes_dir.exists() {
let dest = dist_dir.join(format!("{}-classes", package_name));
copy_dir_recursive(&classes_dir, &dest)?;
println!(" {} {}", "✅ Classes:".green(), dest.display());
} else {
println!(" {}", "⚠️ No se encontraron artefactos compilados".yellow());
return Ok(());
}
}
}
"python" => {
let source_dir = project_dir.join(config.source_dir());
let dest = dist_dir.join(&package_name);
std::fs::create_dir_all(&dest)?;
copy_dir_recursive(&source_dir, &dest.join("src"))?;
let forge_toml = project_dir.join("forge.toml");
if forge_toml.exists() {
std::fs::copy(&forge_toml, dest.join("forge.toml"))?;
}
if !config.dependencies.is_empty() {
let reqs: Vec<String> = config
.dependencies
.iter()
.map(|(name, ver)| format!("{}=={}", name, ver))
.collect();
std::fs::write(dest.join("requirements.txt"), reqs.join("\n"))?;
println!(" {} requirements.txt", "✅ Creado:".green());
}
let size = dir_size(&dest);
println!(" {} {} ({})", "✅ Paquete:".green(), dest.display(), format_bytes(size));
}
_ => {}
}
let dist_size = dir_size(&dist_dir);
println!(
"\n{}",
format!("📦 Empaquetado completado en dist/ ({})", format_bytes(dist_size))
.green()
.bold()
);
println!();
Ok(())
}
fn copy_dir_recursive(src: &std::path::Path, dst: &std::path::Path) -> anyhow::Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if src_path.is_dir() {
copy_dir_recursive(&src_path, &dst_path)?;
} else {
std::fs::copy(&src_path, &dst_path)?;
}
}
Ok(())
}