use rustbasic_core::colored::*;
use rustbasic_core::serde::{Deserialize, Serialize};
use rustbasic_core::serde_json;
use std::process::Command;
const MANIFEST_FILE: &str = ".rustbasic_packages.json";
struct PackageInfo {
version: &'static str,
description: &'static str,
setup_command: Option<&'static str>,
remove_command: Option<&'static str>,
}
fn known_packages(name: &str) -> Option<PackageInfo> {
match name {
"rustbasic-breeze" => Some(PackageInfo {
version: "0.0",
description: "Authentication scaffolding (login, register, reset password)",
setup_command: Some("breeze:install"),
remove_command: Some("breeze:remove"),
}),
"rustbasic-activitylog" => Some(PackageInfo {
version: "0.0",
description: "Activity logging package for tracking actions and HTTP requests",
setup_command: Some("activitylog:install"),
remove_command: Some("activitylog:remove"),
}),
"rustbasic-jwt" => Some(PackageInfo {
version: "0.0",
description: "JWT authentication package (tokens, claims, blacklist)",
setup_command: None,
remove_command: Some("jwt:remove"),
}),
"rustbasic-medialibrary" => Some(PackageInfo {
version: "0.0",
description: "Advanced media library management (upload, WebP compression, S3 integration)",
setup_command: None,
remove_command: None,
}),
"rustbasic-permission" => Some(PackageInfo {
version: "0.0",
description: "Role and Permission management package (RBAC)",
setup_command: Some("permission:install"),
remove_command: Some("permission:remove"),
}),
"rustbasic-translatable" => Some(PackageInfo {
version: "0.0",
description: "Multi-language JSON translation and localization package",
setup_command: None,
remove_command: None,
}),
"rustbasic-webp" => Some(PackageInfo {
version: "0.0",
description: "High-performance WebP image conversion and resizing package",
setup_command: None,
remove_command: None,
}),
"rustbasic-native" => Some(PackageInfo {
version: "0.0",
description: "Native platform wrapper package for running RustBasic server inside Mobile (Android/iOS) & Desktop apps",
setup_command: Some("native:install"),
remove_command: Some("native:remove"),
}),
_ => None,
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(crate = "rustbasic_core::serde")]
pub struct InstalledPackage {
pub name: String,
pub version: String,
pub installed_at: String,
pub source: String, pub description: String,
}
#[derive(Debug, Serialize, Deserialize, Default)]
#[serde(crate = "rustbasic_core::serde")]
pub struct PackageManifest {
pub packages: Vec<InstalledPackage>,
}
pub fn read_manifest() -> PackageManifest {
if let Ok(content) = std::fs::read_to_string(MANIFEST_FILE) {
serde_json::from_str(&content).unwrap_or_default()
} else {
PackageManifest::default()
}
}
fn write_manifest(manifest: &PackageManifest) {
if let Ok(json) = serde_json::to_string_pretty(manifest) {
std::fs::write(MANIFEST_FILE, json).ok();
}
}
fn cargo_toml_path() -> &'static str {
"Cargo.toml"
}
fn read_cargo_toml() -> Option<String> {
std::fs::read_to_string(cargo_toml_path()).ok()
}
fn write_cargo_toml(content: &str) {
std::fs::write(cargo_toml_path(), content).ok();
}
fn cargo_has_package(name: &str) -> bool {
read_cargo_toml()
.map(|c| c.contains(name))
.unwrap_or(false)
}
fn cargo_add_package(name: &str, version: &str) -> bool {
let Some(mut content) = read_cargo_toml() else {
println!("{}", "❌ Tidak dapat membaca Cargo.toml".red().bold());
return false;
};
if content.contains(name) {
return true; }
let use_local_path = std::path::Path::new(&format!("../{}", name)).exists();
let dep_line = if use_local_path {
format!("{} = {{ path = \"../{}\", version = \"{}\" }}\n", name, name, version)
} else {
format!("{} = \"{}\"\n", name, version)
};
if let Some(pos) = content.find("[dependencies]") {
let insert_at = pos + "[dependencies]".len();
let after = &content[insert_at..];
let newline_pos = after.find('\n').map(|p| insert_at + p + 1).unwrap_or(insert_at);
content.insert_str(newline_pos, &dep_line);
write_cargo_toml(&content);
true
} else {
println!("{}", "❌ Tidak dapat menemukan section [dependencies] di Cargo.toml".red().bold());
false
}
}
fn cargo_remove_package(name: &str) {
if let Some(content) = read_cargo_toml() {
let filtered: String = content
.lines()
.filter(|line| !line.contains(name))
.collect::<Vec<_>>()
.join("\n");
let result = if content.ends_with('\n') {
format!("{}\n", filtered)
} else {
filtered
};
write_cargo_toml(&result);
}
}
pub fn sync_manual_packages(manifest: &mut PackageManifest) {
let Some(content) = read_cargo_toml() else { return };
let known_in_manifest: Vec<String> = manifest.packages.iter().map(|p| p.name.clone()).collect();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with('#') { continue; }
if let Some(idx) = trimmed.find("rustbasic-") {
let rest = &trimmed[idx..];
let name_end = rest.find(|c: char| !c.is_alphanumeric() && c != '-')
.unwrap_or(rest.len());
let pkg_name = &rest[..name_end];
if pkg_name == "rustbasic-core" || pkg_name == "rustbasic-cli" {
continue;
}
if !known_in_manifest.contains(&pkg_name.to_string()) {
let version = extract_version_from_line(trimmed).unwrap_or_else(|| "?".to_string());
let desc = known_packages(pkg_name)
.map(|p| p.description.to_string())
.unwrap_or_else(|| "Package eksternal".to_string());
manifest.packages.push(InstalledPackage {
name: pkg_name.to_string(),
version,
installed_at: "—".to_string(),
source: "manual".to_string(),
description: desc,
});
}
}
}
}
fn extract_version_from_line(line: &str) -> Option<String> {
if let Some(start) = line.find("version = \"") {
let rest = &line[start + 11..];
if let Some(end) = rest.find('"') {
return Some(rest[..end].to_string());
}
}
if let Some(eq) = line.find(" = \"") {
let rest = &line[eq + 4..];
if let Some(end) = rest.find('"') {
return Some(rest[..end].to_string());
}
}
None
}
fn run_cargo_build() -> bool {
println!(" {} Mengunduh dan mengkompilasi dependensi...", "📦".bold());
let mut cmd = Command::new("cargo");
cmd.arg("build");
let status = crate::utils::run_cargo_with_progress(cmd);
match status {
Ok(s) if s.success() => true,
Ok(s) => {
println!("{} cargo build gagal dengan exit code: {}", "❌".red().bold(), s);
false
}
Err(e) => {
println!("{} Gagal menjalankan cargo build: {}", "❌".red().bold(), e);
false
}
}
}
fn run_setup_command(package_name: &str, command: &str) {
let bin_dir = "src/bin";
std::fs::create_dir_all(bin_dir).ok();
let script_path = format!("{}/temp_pkg_setup.rs", bin_dir);
let script = match command {
"breeze:install" => {
r#"use rustbasic_core::dotenvy::dotenv;
fn main() {
rustbasic_core::tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
dotenv().ok();
rustbasic_breeze::make_auth().await;
});
}
"#.to_string()
}
"breeze:remove" => {
r#"use rustbasic_core::dotenvy::dotenv;
fn main() {
rustbasic_core::tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
dotenv().ok();
rustbasic_breeze::remove_auth().await;
});
}
"#.to_string()
}
"activitylog:remove" => {
r##"use std::fs;
use std::path::Path;
fn main() {
println!("⚙️ Membersihkan scaffolding Activity Log...");
// 1. Hapus model file
let path = "src/app/models/activity_log.rs";
if Path::new(path).exists() {
let _ = fs::remove_file(path);
println!(" 🗑️ Dihapus: {}", path);
}
// 2. Bersihkan src/app/models/mod.rs
let mod_path = "src/app/models/mod.rs";
if Path::new(mod_path).exists() {
if let Ok(content) = fs::read_to_string(mod_path) {
let filtered: Vec<String> = content
.lines()
.filter(|line| {
!line.contains("pub mod activity_log;") &&
!line.contains("pub use activity_log::Model as ActivityLog;")
})
.map(|s| s.to_string())
.collect();
let _ = fs::write(mod_path, filtered.join("\n") + "\n");
println!(" 📝 Diperbarui: {}", mod_path);
}
}
// 3. Hapus migrations
let migrations_dir = "database/migrations";
if let Ok(entries) = fs::read_dir(migrations_dir) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.contains("create_activity_log_table") {
let path = entry.path();
let _ = fs::remove_file(&path);
println!(" 🗑️ Dihapus: {}", path.display());
// Bersihkan database/migrations/mod.rs
let mod_name = name.strip_suffix(".rs").unwrap_or(&name);
let migrations_mod_path = "database/migrations/mod.rs";
if Path::new(migrations_mod_path).exists() {
if let Ok(content) = fs::read_to_string(migrations_mod_path) {
let filtered: Vec<String> = content
.lines()
.filter(|line| {
!line.contains(&format!("pub mod {};", mod_name)) &&
!line.contains(&format!("Box::new({}::Migration)", mod_name))
})
.map(|s| s.to_string())
.collect();
let _ = fs::write(migrations_mod_path, filtered.join("\n") + "\n");
}
}
}
}
}
}
"##.to_string()
}
"jwt:remove" => {
r##"use std::fs;
use std::path::Path;
fn main() {
println!("⚙️ Membersihkan scaffolding RustBasic JWT...");
// 1. Hapus model files
let models = ["user.rs", "jwt_blacklist.rs"];
for model in &models {
let path = format!("src/app/models/{}", model);
if Path::new(&path).exists() {
let _ = fs::remove_file(&path);
println!(" 🗑️ Dihapus: {}", path);
}
}
// 2. Bersihkan src/app/models/mod.rs
let mod_path = "src/app/models/mod.rs";
if Path::new(mod_path).exists() {
if let Ok(content) = fs::read_to_string(mod_path) {
let filtered: Vec<String> = content
.lines()
.filter(|line| {
!line.contains("pub mod user;") &&
!line.contains("pub use user::Entity as User;") &&
!line.contains("pub use user::Model as User;") &&
!line.contains("pub mod jwt_blacklist;") &&
!line.contains("pub use jwt_blacklist::Entity as JwtBlacklist;") &&
!line.contains("pub use jwt_blacklist::Model as JwtBlacklist;")
})
.map(|s| s.to_string())
.collect();
let _ = fs::write(mod_path, filtered.join("\n") + "\n");
println!(" 📝 Diperbarui: {}", mod_path);
}
}
// 3. Hapus migrations
let migrations_dir = "database/migrations";
if let Ok(entries) = fs::read_dir(migrations_dir) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if (name.contains("create_users_table") && name != "m20260501_000002_create_users_table.rs") || name.contains("create_jwt_blacklists_table") {
let path = entry.path();
let _ = fs::remove_file(&path);
println!(" 🗑️ Dihapus: {}", path.display());
// Bersihkan database/migrations/mod.rs
let mod_name = name.strip_suffix(".rs").unwrap_or(&name);
let migrations_mod_path = "database/migrations/mod.rs";
if Path::new(migrations_mod_path).exists() {
if let Ok(content) = fs::read_to_string(migrations_mod_path) {
let filtered: Vec<String> = content
.lines()
.filter(|line| {
!line.contains(&format!("pub mod {};", mod_name)) &&
!line.contains(&format!("Box::new({}::Migration)", mod_name))
})
.map(|s| s.to_string())
.collect();
let _ = fs::write(migrations_mod_path, filtered.join("\n") + "\n");
}
}
}
}
}
// 4. Bersihkan .env dari konfigurasi JWT
let env_path = ".env";
if Path::new(env_path).exists() {
if let Ok(content) = fs::read_to_string(env_path) {
let filtered: Vec<String> = content
.lines()
.filter(|line| {
!line.starts_with("JWT_") &&
!line.contains("# --- JWT CONFIG ---")
})
.map(|s| s.to_string())
.collect();
let _ = fs::write(env_path, filtered.join("\n") + "\n");
println!(" 📝 Diperbarui: {}", env_path);
}
}
}
"##.to_string()
}
"permission:remove" => {
r##"use std::fs;
use std::path::Path;
fn main() {
println!("⚙️ Membersihkan scaffolding RBAC Permission...");
// 1. Hapus model files
let models = [
"role.rs",
"permission.rs",
"model_has_role.rs",
"model_has_permission.rs",
"role_has_permission.rs"
];
for model in &models {
let path = format!("src/app/models/{}", model);
if Path::new(&path).exists() {
let _ = fs::remove_file(&path);
println!(" 🗑️ Dihapus: {}", path);
}
}
// 2. Bersihkan src/app/models/mod.rs
let mod_path = "src/app/models/mod.rs";
if Path::new(mod_path).exists() {
if let Ok(content) = fs::read_to_string(mod_path) {
let filtered: Vec<String> = content
.lines()
.filter(|line| {
!line.contains("pub mod role;") &&
!line.contains("pub use role::Model as Role;") &&
!line.contains("pub use role::Entity as Role;") &&
!line.contains("pub mod permission;") &&
!line.contains("pub use permission::Model as Permission;") &&
!line.contains("pub use permission::Entity as Permission;") &&
!line.contains("pub mod model_has_role;") &&
!line.contains("pub use model_has_role::Model as ModelHasRole;") &&
!line.contains("pub use model_has_role::Entity as ModelHasRole;") &&
!line.contains("pub mod model_has_permission;") &&
!line.contains("pub use model_has_permission::Model as ModelHasPermission;") &&
!line.contains("pub use model_has_permission::Entity as ModelHasPermission;") &&
!line.contains("pub mod role_has_permission;") &&
!line.contains("pub use role_has_permission::Model as RoleHasPermission;") &&
!line.contains("pub use role_has_permission::Entity as RoleHasPermission;")
})
.map(|s| s.to_string())
.collect();
let _ = fs::write(mod_path, filtered.join("\n") + "\n");
println!(" 📝 Diperbarui: {}", mod_path);
}
}
// 3. Hapus migrations
let migrations_dir = "database/migrations";
if let Ok(entries) = fs::read_dir(migrations_dir) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.contains("create_rbac_tables") {
let path = entry.path();
let _ = fs::remove_file(&path);
println!(" 🗑️ Dihapus: {}", path.display());
// Bersihkan database/migrations/mod.rs
let mod_name = name.strip_suffix(".rs").unwrap_or(&name);
let migrations_mod_path = "database/migrations/mod.rs";
if Path::new(migrations_mod_path).exists() {
if let Ok(content) = fs::read_to_string(migrations_mod_path) {
let filtered: Vec<String> = content
.lines()
.filter(|line| {
!line.contains(&format!("pub mod {};", mod_name)) &&
!line.contains(&format!("Box::new({}::Migration)", mod_name))
})
.map(|s| s.to_string())
.collect();
let _ = fs::write(migrations_mod_path, filtered.join("\n") + "\n");
}
}
}
}
}
}
"##.to_string()
}
"activitylog:install" => {
r#"fn main() {
println!("⚙️ Menjalankan generator scaffolding Activity Log...");
rustbasic_activitylog::scaffolding::make_activitylog_scaffolding();
}
"#.to_string()
}
"permission:install" => {
r#"fn main() {
println!("⚙️ Menjalankan generator scaffolding RBAC Permission...");
rustbasic_permission::scaffolding::make_permission_scaffolding();
}
"#.to_string()
}
"native:install" => {
r#"fn main() {
println!("⚙️ Menjalankan generator scaffolding RustBasic Native...");
rustbasic_native::scaffolding::make_native_scaffolding();
}
"#.to_string()
}
"native:remove" => {
r#"fn main() {
println!("⚙️ Membersihkan scaffolding RustBasic Native...");
rustbasic_native::scaffolding::remove_native_scaffolding();
}
"#.to_string()
}
_ => return,
};
std::fs::write(&script_path, &script).ok();
println!(" {} Menjalankan setup {}...", "⚙️".bold(), package_name.cyan().bold());
let mut cmd = Command::new("cargo");
cmd.args(["run", "--bin", "temp_pkg_setup"]);
let status = crate::utils::run_cargo_with_progress(cmd);
std::fs::remove_file(&script_path).ok();
if let Ok(entries) = std::fs::read_dir(bin_dir)
&& entries.count() == 0 {
std::fs::remove_dir(bin_dir).ok();
}
match status {
Ok(s) if s.success() => {}
Ok(s) => println!("{} Setup command gagal (exit {})", "⚠️".yellow().bold(), s),
Err(e) => println!("{} Gagal menjalankan setup: {}", "⚠️".yellow().bold(), e),
}
}
pub fn install_package(name: &str) {
println!("\n{} {}", "📦 Installing:".magenta().bold(), name.cyan().bold());
let mut manifest = read_manifest();
if manifest.packages.iter().any(|p| p.name == name) {
println!("{} Package '{}' sudah terinstall.", "⚠️".yellow().bold(), name.yellow());
println!(" Gunakan '{}' untuk melihat daftar package.", "rustbasic list packages".cyan());
return;
}
let (version, description, setup_cmd) = if let Some(info) = known_packages(name) {
(info.version.to_string(), info.description.to_string(), info.setup_command.map(|s| s.to_string()))
} else {
println!("{} Package '{}' tidak dikenali dalam registry RustBasic.", "⚠️".yellow().bold(), name.yellow());
println!(" Gunakan versi spesifik dengan menambahkan ke Cargo.toml secara manual.");
return;
};
println!(" {} Menambahkan ke Cargo.toml...", "📝".bold());
if !cargo_add_package(name, &version) {
return;
}
if !run_cargo_build() {
cargo_remove_package(name);
return;
}
if let Some(cmd) = &setup_cmd {
run_setup_command(name, cmd);
}
let now = rustbasic_core::chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string();
manifest.packages.push(InstalledPackage {
name: name.to_string(),
version,
installed_at: now,
source: "install".to_string(),
description,
});
write_manifest(&manifest);
println!("\n{} Package '{}' berhasil diinstall!", "✅".green().bold(), name.cyan().bold());
println!(" Gunakan '{}' untuk melihat daftar package.", "rustbasic list packages".cyan());
}
pub fn list_packages() {
let mut manifest = read_manifest();
sync_manual_packages(&mut manifest);
println!("\n{}", "📦 RustBasic Package Manager".magenta().bold());
println!("{}", "═══════════════════════════════════════════════════════════════════════════".magenta());
println!("{}", "💻 Package Terinstal (Installed Packages):".bold());
if manifest.packages.is_empty() {
println!("{}", " Belum ada package tambahan yang terinstall.".dimmed());
println!(" Gunakan '{}' untuk menginstall package.", "rustbasic install <nama-package>".cyan());
} else {
let header_pkg = format!("{:<28}", "PACKAGE").bold();
let header_ver = format!("{:<10}", "VERSION").bold();
let header_source = format!("{:<18}", "SOURCE").bold();
let header_installed_at = format!("{:<22}", "INSTALLED AT").bold();
let header_desc = "DESCRIPTION".bold();
println!(
" {}{}{}{} {}",
header_pkg,
header_ver,
header_source,
header_installed_at,
header_desc
);
println!("{}", " ─────────────────────────────────────────────────────────────────────".dimmed());
for pkg in &manifest.packages {
let source_padded = match pkg.source.as_str() {
"manual" => format!("{:<18}", "manual").yellow(),
_ => format!("{:<18}", "install").green(),
};
let installed_at_padded = if pkg.installed_at == "—" {
format!("{:<22}", "—").dimmed()
} else {
format!("{:<22}", pkg.installed_at).dimmed()
};
let name_display = format!("{:<28}", pkg.name).cyan();
let version_display = format!("{:<10}", pkg.version);
println!(
" {}{}{}{} {}",
name_display,
version_display,
source_padded,
installed_at_padded,
pkg.description.dimmed()
);
}
}
println!("\n{}", "═══════════════════════════════════════════════════════════════════════════".magenta());
println!("{}", "✨ Package yang Tersedia untuk Diinstal (Available Packages):".bold());
println!("{}", " ─────────────────────────────────────────────────────────────────────".dimmed());
let header_pkg = format!("{:<28}", "PACKAGE").bold();
let header_ver = format!("{:<10}", "VERSION").bold();
let header_status = format!("{:<18}", "STATUS").bold();
let header_desc = "DESCRIPTION".bold();
println!(
" {}{}{} {}",
header_pkg,
header_ver,
header_status,
header_desc
);
println!("{}", " ─────────────────────────────────────────────────────────────────────".dimmed());
let all_packages = &[
"rustbasic-breeze",
"rustbasic-activitylog",
"rustbasic-jwt",
"rustbasic-medialibrary",
"rustbasic-permission",
"rustbasic-translatable",
"rustbasic-webp",
"rustbasic-native",
];
for &name in all_packages {
if let Some(info) = known_packages(name) {
let is_installed = manifest.packages.iter().any(|p| p.name == name);
let status_padded = if is_installed {
format!("{:<18}", "Terinstal").green()
} else {
format!("{:<18}", "Tersedia").yellow()
};
let name_display = format!("{:<28}", name).cyan();
let version_display = format!("{:<10}", info.version);
println!(
" {}{}{} {}",
name_display,
version_display,
status_padded,
info.description.dimmed()
);
}
}
println!("{}", "═══════════════════════════════════════════════════════════════════════════".magenta());
println!(
" {} Total Terinstal: {} | Total Tersedia: {}\n",
"📊".bold(),
manifest.packages.len().to_string().cyan().bold(),
all_packages.len().to_string().yellow().bold()
);
}
pub fn uninstall_package(name: &str) {
println!("\n{} {}", "🗑️ Uninstalling:".red().bold(), name.cyan().bold());
let mut manifest = read_manifest();
let pkg_idx = manifest.packages.iter().position(|p| p.name == name);
let in_cargo = cargo_has_package(name);
if pkg_idx.is_none() && !in_cargo {
println!("{} Package '{}' tidak ditemukan.", "❌".red().bold(), name.yellow());
return;
}
if let Some(info) = known_packages(name)
&& let Some(remove_cmd) = info.remove_command {
if in_cargo {
run_setup_command(name, remove_cmd);
}
}
println!(" {} Menghapus dari Cargo.toml...", "📝".bold());
cargo_remove_package(name);
println!(" {} Memperbarui dependencies...", "📦".bold());
let mut cmd = Command::new("cargo");
cmd.arg("build");
let _ = crate::utils::run_cargo_with_progress(cmd);
if let Some(idx) = pkg_idx {
manifest.packages.remove(idx);
write_manifest(&manifest);
}
println!("\n{} Package '{}' berhasil diuninstall!", "✅".green().bold(), name.cyan().bold());
}