use anyhow::Result;
use clap::Parser;
use console::style;
use sqlx::postgres::PgPoolOptions;
use sqlx::Row;
use std::path::Path;
#[derive(Parser)]
pub struct CheckCommand {
#[arg(short, long, default_value = "forge.toml")]
pub config: String,
#[arg(long, default_value = "true")]
pub db: bool,
}
impl CheckCommand {
pub async fn execute(self) -> Result<()> {
println!();
println!("{} Checking FORGE project...", style("🔍").cyan());
println!();
let mut all_passed = true;
let mut warnings = Vec::new();
let config_path = Path::new(&self.config);
if config_path.exists() {
println!(" {} forge.toml found", style("✓").green());
let content = std::fs::read_to_string(config_path)?;
match toml::from_str::<toml::Value>(&content) {
Ok(config) => {
println!(" {} forge.toml is valid TOML", style("✓").green());
if config.get("project").is_some() {
println!(" {} [project] section found", style("✓").green());
} else {
println!(" {} [project] section missing", style("⚠").yellow());
warnings.push("Add [project] section to forge.toml");
}
if config.get("database").is_some() {
println!(" {} [database] section found", style("✓").green());
} else {
println!(" {} [database] section missing", style("⚠").yellow());
warnings.push("Add [database] section to forge.toml");
}
}
Err(e) => {
println!(" {} forge.toml parse error: {}", style("✗").red(), e);
all_passed = false;
}
}
} else {
println!(" {} forge.toml not found", style("✗").red());
println!(" {} Run 'forge init' to create one", style("→").dim());
all_passed = false;
}
let schema_dir = Path::new("src/schema");
let functions_dir = Path::new("src/functions");
let migrations_dir = Path::new("migrations");
if schema_dir.exists() {
println!(" {} src/schema/ directory exists", style("✓").green());
} else {
println!(" {} src/schema/ directory missing", style("✗").red());
all_passed = false;
}
if functions_dir.exists() {
println!(" {} src/functions/ directory exists", style("✓").green());
} else {
println!(" {} src/functions/ directory missing", style("✗").red());
all_passed = false;
}
if migrations_dir.exists() {
println!(" {} migrations/ directory exists", style("✓").green());
let count = std::fs::read_dir(migrations_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "sql"))
.count();
println!(" {} {} migration file(s) found", style("→").dim(), count);
} else {
println!(" {} migrations/ directory missing", style("⚠").yellow());
warnings.push("Create migrations/ directory for database migrations");
}
let env_path = Path::new(".env");
if env_path.exists() {
println!(" {} .env file found", style("✓").green());
} else {
println!(" {} .env file missing", style("⚠").yellow());
warnings.push("Create .env file with DATABASE_URL");
}
match std::env::var("DATABASE_URL") {
Ok(url) => {
println!(" {} DATABASE_URL is set", style("✓").green());
let masked = mask_database_url(&url);
println!(" {} {}", style("→").dim(), style(masked).dim());
if self.db {
println!();
println!(" {} Checking database connection...", style("⋯").cyan());
match check_database_connection(&url).await {
Ok(version) => {
println!(" {} Database connection successful", style("✓").green());
println!(" {} PostgreSQL {}", style("→").dim(), version);
}
Err(e) => {
println!(" {} Database connection failed: {}", style("✗").red(), e);
all_passed = false;
}
}
}
}
Err(_) => {
if env_path.exists() {
if let Ok(content) = std::fs::read_to_string(env_path) {
for line in content.lines() {
if line.starts_with("DATABASE_URL=") {
println!(" {} DATABASE_URL found in .env", style("✓").green());
let url = line.trim_start_matches("DATABASE_URL=");
let masked = mask_database_url(url);
println!(" {} {}", style("→").dim(), style(masked).dim());
if self.db {
println!();
println!(
" {} Checking database connection...",
style("⋯").cyan()
);
match check_database_connection(url).await {
Ok(version) => {
println!(
" {} Database connection successful",
style("✓").green()
);
println!(
" {} PostgreSQL {}",
style("→").dim(),
version
);
}
Err(e) => {
println!(
" {} Database connection failed: {}",
style("✗").red(),
e
);
all_passed = false;
}
}
}
break;
}
}
}
} else {
println!(" {} DATABASE_URL not set", style("⚠").yellow());
warnings.push("Set DATABASE_URL in .env or environment");
}
}
}
let frontend_dir = Path::new("frontend");
if frontend_dir.exists() {
println!(" {} frontend/ directory exists", style("✓").green());
let package_json = frontend_dir.join("package.json");
if package_json.exists() {
println!(" {} frontend/package.json found", style("✓").green());
} else {
println!(" {} frontend/package.json missing", style("⚠").yellow());
warnings.push("Run 'cd frontend && bun install' to set up frontend");
}
let node_modules = frontend_dir.join("node_modules");
if node_modules.exists() {
println!(" {} frontend dependencies installed", style("✓").green());
} else {
println!(
" {} frontend dependencies not installed",
style("⚠").yellow()
);
warnings.push("Run 'cd frontend && bun install' to install dependencies");
}
}
println!();
if all_passed && warnings.is_empty() {
println!("{} All checks passed! Ready to run.", style("✅").green());
println!();
println!("Next steps:");
println!(" {} Start development", style("forge dev").cyan());
println!(
" {} Check migrations",
style("forge migrate status").cyan()
);
} else if all_passed {
println!(
"{} Checks passed with {} warning(s)",
style("⚠").yellow(),
warnings.len()
);
println!();
for warning in &warnings {
println!(" {} {}", style("→").dim(), warning);
}
} else {
println!(
"{} Some checks failed. Fix the issues above and run 'forge check' again.",
style("❌").red()
);
return Err(anyhow::anyhow!("Project check failed"));
}
println!();
Ok(())
}
}
fn mask_database_url(url: &str) -> String {
if let Some(at_pos) = url.find('@') {
if let Some(colon_pos) = url[..at_pos].rfind(':') {
let protocol_end = url.find("://").map(|p| p + 3).unwrap_or(0);
if colon_pos > protocol_end {
let before_password = &url[..colon_pos + 1];
let after_password = &url[at_pos..];
return format!("{}****{}", before_password, after_password);
}
}
}
url.to_string()
}
async fn check_database_connection(url: &str) -> Result<String> {
let pool = PgPoolOptions::new()
.max_connections(1)
.acquire_timeout(std::time::Duration::from_secs(5))
.connect(url)
.await?;
let row = sqlx::query("SELECT version()").fetch_one(&pool).await?;
let version_str: String = row.get(0);
let version = version_str
.split_whitespace()
.nth(1)
.unwrap_or(&version_str)
.to_string();
pool.close().await;
Ok(version)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mask_database_url() {
assert_eq!(
mask_database_url("postgres://user:secret@localhost/db"),
"postgres://user:****@localhost/db"
);
assert_eq!(
mask_database_url("postgres://localhost/db"),
"postgres://localhost/db"
);
}
}