Skip to main content

rustbasic_cli/
packages.rs

1/* ---------------------------------------------------------
2 * 📦 LABEL: PACKAGE MANAGER (src/packages.rs)
3 * Menangani install, list, dan uninstall package di project
4 * RustBasic menggunakan manifest .rustbasic_packages.json
5 * --------------------------------------------------------- */
6
7use rustbasic_core::colored::*;
8use rustbasic_core::serde::{Deserialize, Serialize};
9use rustbasic_core::serde_json;
10use std::process::Command;
11
12const MANIFEST_FILE: &str = ".rustbasic_packages.json";
13
14// Registry package yang didukung beserta metadata-nya
15struct PackageInfo {
16    /// Versi default yang akan digunakan saat install via CLI
17    version: &'static str,
18    /// Deskripsi singkat package
19    description: &'static str,
20    /// Command yang dijalankan setelah install (opsional)
21    setup_command: Option<&'static str>,
22    /// Command yang dijalankan sebelum uninstall (opsional)
23    remove_command: Option<&'static str>,
24}
25
26fn known_packages(name: &str) -> Option<PackageInfo> {
27    match name {
28        "rustbasic-breeze" => Some(PackageInfo {
29            version: "0.0",
30            description: "Authentication scaffolding (login, register, reset password)",
31            setup_command: Some("breeze:install"),
32            remove_command: Some("breeze:remove"),
33        }),
34        "rustbasic-activitylog" => Some(PackageInfo {
35            version: "0.0",
36            description: "Activity logging package for tracking actions and HTTP requests",
37            setup_command: Some("activitylog:install"),
38            remove_command: Some("activitylog:remove"),
39        }),
40        "rustbasic-jwt" => Some(PackageInfo {
41            version: "0.0",
42            description: "JWT authentication package (tokens, claims, blacklist)",
43            setup_command: None,
44            remove_command: Some("jwt:remove"),
45        }),
46        "rustbasic-medialibrary" => Some(PackageInfo {
47            version: "0.0",
48            description: "Advanced media library management (upload, WebP compression, S3 integration)",
49            setup_command: None,
50            remove_command: None,
51        }),
52        "rustbasic-permission" => Some(PackageInfo {
53            version: "0.0",
54            description: "Role and Permission management package (RBAC)",
55            setup_command: Some("permission:install"),
56            remove_command: Some("permission:remove"),
57        }),
58        "rustbasic-translatable" => Some(PackageInfo {
59            version: "0.0",
60            description: "Multi-language JSON translation and localization package",
61            setup_command: None,
62            remove_command: None,
63        }),
64        "rustbasic-webp" => Some(PackageInfo {
65            version: "0.0",
66            description: "High-performance WebP image conversion and resizing package",
67            setup_command: None,
68            remove_command: None,
69        }),
70        "rustbasic-native" => Some(PackageInfo {
71            version: "0.0",
72            description: "Native platform wrapper package for running RustBasic server inside Mobile (Android/iOS) & Desktop apps",
73            setup_command: Some("native:install"),
74            remove_command: Some("native:remove"),
75        }),
76        _ => None,
77    }
78}
79
80// ─── Manifest Structs ─────────────────────────────────────────────────────────
81
82#[derive(Debug, Serialize, Deserialize, Clone)]
83#[serde(crate = "rustbasic_core::serde")]
84pub struct InstalledPackage {
85    pub name: String,
86    pub version: String,
87    pub installed_at: String,
88    pub source: String, // "install" | "manual"
89    pub description: String,
90}
91
92#[derive(Debug, Serialize, Deserialize, Default)]
93#[serde(crate = "rustbasic_core::serde")]
94pub struct PackageManifest {
95    pub packages: Vec<InstalledPackage>,
96}
97
98// ─── Manifest I/O ─────────────────────────────────────────────────────────────
99
100pub fn read_manifest() -> PackageManifest {
101    if let Ok(content) = std::fs::read_to_string(MANIFEST_FILE) {
102        serde_json::from_str(&content).unwrap_or_default()
103    } else {
104        PackageManifest::default()
105    }
106}
107
108fn write_manifest(manifest: &PackageManifest) {
109    if let Ok(json) = serde_json::to_string_pretty(manifest) {
110        std::fs::write(MANIFEST_FILE, json).ok();
111    }
112}
113
114// ─── Cargo.toml Helpers ───────────────────────────────────────────────────────
115
116fn cargo_toml_path() -> &'static str {
117    "Cargo.toml"
118}
119
120fn read_cargo_toml() -> Option<String> {
121    std::fs::read_to_string(cargo_toml_path()).ok()
122}
123
124fn write_cargo_toml(content: &str) {
125    std::fs::write(cargo_toml_path(), content).ok();
126}
127
128/// Cek apakah package sudah ada di Cargo.toml
129fn cargo_has_package(name: &str) -> bool {
130    read_cargo_toml()
131        .map(|c| c.contains(name))
132        .unwrap_or(false)
133}
134
135/// Tambahkan dependency ke Cargo.toml
136fn cargo_add_package(name: &str, version: &str) -> bool {
137    let Some(mut content) = read_cargo_toml() else {
138        println!("{}", "❌ Tidak dapat membaca Cargo.toml".red().bold());
139        return false;
140    };
141
142    if content.contains(name) {
143        return true; // sudah ada
144    }
145
146    let use_local_path = std::path::Path::new(&format!("../{}", name)).exists();
147    let dep_line = if use_local_path {
148        format!("{} = {{ path = \"../{}\", version = \"{}\" }}\n", name, name, version)
149    } else {
150        format!("{} = \"{}\"\n", name, version)
151    };
152
153    // Sisipkan setelah [dependencies]
154    if let Some(pos) = content.find("[dependencies]") {
155        let insert_at = pos + "[dependencies]".len();
156        // Cari akhir baris [dependencies]
157        let after = &content[insert_at..];
158        let newline_pos = after.find('\n').map(|p| insert_at + p + 1).unwrap_or(insert_at);
159        content.insert_str(newline_pos, &dep_line);
160        write_cargo_toml(&content);
161        true
162    } else {
163        println!("{}", "❌ Tidak dapat menemukan section [dependencies] di Cargo.toml".red().bold());
164        false
165    }
166}
167
168/// Hapus dependency dari Cargo.toml
169fn cargo_remove_package(name: &str) {
170    if let Some(content) = read_cargo_toml() {
171        let filtered: String = content
172            .lines()
173            .filter(|line| !line.contains(name))
174            .collect::<Vec<_>>()
175            .join("\n");
176        // Pertahankan trailing newline
177        let result = if content.ends_with('\n') {
178            format!("{}\n", filtered)
179        } else {
180            filtered
181        };
182        write_cargo_toml(&result);
183    }
184}
185
186/// Scan Cargo.toml untuk package rustbasic-* yang belum ada di manifest
187pub fn sync_manual_packages(manifest: &mut PackageManifest) {
188    let Some(content) = read_cargo_toml() else { return };
189    let known_in_manifest: Vec<String> = manifest.packages.iter().map(|p| p.name.clone()).collect();
190
191    for line in content.lines() {
192        let trimmed = line.trim();
193        if trimmed.starts_with('#') { continue; }
194        if let Some(idx) = trimmed.find("rustbasic-") {
195            let rest = &trimmed[idx..];
196            // Ekstrak nama package: ambil sampai karakter non-alfanumerik/dash
197            let name_end = rest.find(|c: char| !c.is_alphanumeric() && c != '-')
198                .unwrap_or(rest.len());
199            let pkg_name = &rest[..name_end];
200
201            // Skip core dan cli
202            if pkg_name == "rustbasic-core" || pkg_name == "rustbasic-cli" {
203                continue;
204            }
205
206            if !known_in_manifest.contains(&pkg_name.to_string()) {
207                // Ekstrak versi jika ada
208                let version = extract_version_from_line(trimmed).unwrap_or_else(|| "?".to_string());
209                let desc = known_packages(pkg_name)
210                    .map(|p| p.description.to_string())
211                    .unwrap_or_else(|| "Package eksternal".to_string());
212
213                manifest.packages.push(InstalledPackage {
214                    name: pkg_name.to_string(),
215                    version,
216                    installed_at: "—".to_string(),
217                    source: "manual".to_string(),
218                    description: desc,
219                });
220            }
221        }
222    }
223}
224
225fn extract_version_from_line(line: &str) -> Option<String> {
226    // Coba cari 'version = "x.x"' atau '"x.x"' pattern
227    if let Some(start) = line.find("version = \"") {
228        let rest = &line[start + 11..];
229        if let Some(end) = rest.find('"') {
230            return Some(rest[..end].to_string());
231        }
232    }
233    // Bentuk ringkas: package = "x.x"
234    if let Some(eq) = line.find(" = \"") {
235        let rest = &line[eq + 4..];
236        if let Some(end) = rest.find('"') {
237            return Some(rest[..end].to_string());
238        }
239    }
240    None
241}
242
243// ─── Cargo Build ──────────────────────────────────────────────────────────────
244
245fn run_cargo_build() -> bool {
246    println!("   {} Mengunduh dan mengkompilasi dependensi...", "📦".bold());
247    let mut cmd = Command::new("cargo");
248    cmd.arg("build");
249    let status = crate::utils::run_cargo_with_progress(cmd);
250    match status {
251        Ok(s) if s.success() => true,
252        Ok(s) => {
253            println!("{} cargo build gagal dengan exit code: {}", "❌".red().bold(), s);
254            false
255        }
256        Err(e) => {
257            println!("{} Gagal menjalankan cargo build: {}", "❌".red().bold(), e);
258            false
259        }
260    }
261}
262
263// ─── Setup / Remove via Temp Binary ───────────────────────────────────────────
264
265fn run_setup_command(package_name: &str, command: &str) {
266    let bin_dir = "src/bin";
267    std::fs::create_dir_all(bin_dir).ok();
268    let script_path = format!("{}/temp_pkg_setup.rs", bin_dir);
269
270    let script = match command {
271        "breeze:install" => {
272            r#"use rustbasic_core::dotenvy::dotenv;
273fn main() {
274    rustbasic_core::tokio::runtime::Builder::new_multi_thread()
275        .enable_all()
276        .build()
277        .unwrap()
278        .block_on(async {
279            dotenv().ok();
280            rustbasic_breeze::make_auth().await;
281        });
282}
283"#.to_string()
284        }
285        "breeze:remove" => {
286            r#"use rustbasic_core::dotenvy::dotenv;
287fn main() {
288    rustbasic_core::tokio::runtime::Builder::new_multi_thread()
289        .enable_all()
290        .build()
291        .unwrap()
292        .block_on(async {
293            dotenv().ok();
294            rustbasic_breeze::remove_auth().await;
295        });
296}
297"#.to_string()
298        }
299        "activitylog:remove" => {
300            r##"use std::fs;
301use std::path::Path;
302
303fn main() {
304    println!("⚙️ Membersihkan scaffolding Activity Log...");
305    
306    // 1. Hapus model file
307    let path = "src/app/models/activity_log.rs";
308    if Path::new(path).exists() {
309        let _ = fs::remove_file(path);
310        println!("   🗑️ Dihapus: {}", path);
311    }
312
313    // 2. Bersihkan src/app/models/mod.rs
314    let mod_path = "src/app/models/mod.rs";
315    if Path::new(mod_path).exists() {
316        if let Ok(content) = fs::read_to_string(mod_path) {
317            let filtered: Vec<String> = content
318                .lines()
319                .filter(|line| {
320                    !line.contains("pub mod activity_log;") &&
321                    !line.contains("pub use activity_log::Model as ActivityLog;")
322                })
323                .map(|s| s.to_string())
324                .collect();
325            let _ = fs::write(mod_path, filtered.join("\n") + "\n");
326            println!("   📝 Diperbarui: {}", mod_path);
327        }
328    }
329
330    // 3. Hapus migrations
331    let migrations_dir = "database/migrations";
332    if let Ok(entries) = fs::read_dir(migrations_dir) {
333        for entry in entries.flatten() {
334            let name = entry.file_name().to_string_lossy().to_string();
335            if name.contains("create_activity_log_table") {
336                let path = entry.path();
337                let _ = fs::remove_file(&path);
338                println!("   🗑️ Dihapus: {}", path.display());
339                
340                // Bersihkan database/migrations/mod.rs
341                let mod_name = name.strip_suffix(".rs").unwrap_or(&name);
342                let migrations_mod_path = "database/migrations/mod.rs";
343                if Path::new(migrations_mod_path).exists() {
344                    if let Ok(content) = fs::read_to_string(migrations_mod_path) {
345                        let filtered: Vec<String> = content
346                            .lines()
347                            .filter(|line| {
348                                !line.contains(&format!("pub mod {};", mod_name)) &&
349                                !line.contains(&format!("Box::new({}::Migration)", mod_name))
350                            })
351                            .map(|s| s.to_string())
352                            .collect();
353                        let _ = fs::write(migrations_mod_path, filtered.join("\n") + "\n");
354                    }
355                }
356            }
357        }
358    }
359}
360"##.to_string()
361        }
362        "jwt:remove" => {
363            r##"use std::fs;
364use std::path::Path;
365
366fn main() {
367    println!("⚙️ Membersihkan scaffolding RustBasic JWT...");
368    
369    // 1. Hapus model files
370    let models = ["user.rs", "jwt_blacklist.rs"];
371    for model in &models {
372        let path = format!("src/app/models/{}", model);
373        if Path::new(&path).exists() {
374            let _ = fs::remove_file(&path);
375            println!("   🗑️ Dihapus: {}", path);
376        }
377    }
378
379    // 2. Bersihkan src/app/models/mod.rs
380    let mod_path = "src/app/models/mod.rs";
381    if Path::new(mod_path).exists() {
382        if let Ok(content) = fs::read_to_string(mod_path) {
383            let filtered: Vec<String> = content
384                .lines()
385                .filter(|line| {
386                    !line.contains("pub mod user;") &&
387                    !line.contains("pub use user::Entity as User;") &&
388                    !line.contains("pub use user::Model as User;") &&
389                    !line.contains("pub mod jwt_blacklist;") &&
390                    !line.contains("pub use jwt_blacklist::Entity as JwtBlacklist;") &&
391                    !line.contains("pub use jwt_blacklist::Model as JwtBlacklist;")
392                })
393                .map(|s| s.to_string())
394                .collect();
395            let _ = fs::write(mod_path, filtered.join("\n") + "\n");
396            println!("   📝 Diperbarui: {}", mod_path);
397        }
398    }
399
400    // 3. Hapus migrations
401    let migrations_dir = "database/migrations";
402    if let Ok(entries) = fs::read_dir(migrations_dir) {
403        for entry in entries.flatten() {
404            let name = entry.file_name().to_string_lossy().to_string();
405            if (name.contains("create_users_table") && name != "m20260501_000002_create_users_table.rs") || name.contains("create_jwt_blacklists_table") {
406                let path = entry.path();
407                let _ = fs::remove_file(&path);
408                println!("   🗑️ Dihapus: {}", path.display());
409                
410                // Bersihkan database/migrations/mod.rs
411                let mod_name = name.strip_suffix(".rs").unwrap_or(&name);
412                let migrations_mod_path = "database/migrations/mod.rs";
413                if Path::new(migrations_mod_path).exists() {
414                    if let Ok(content) = fs::read_to_string(migrations_mod_path) {
415                        let filtered: Vec<String> = content
416                            .lines()
417                            .filter(|line| {
418                                !line.contains(&format!("pub mod {};", mod_name)) &&
419                                !line.contains(&format!("Box::new({}::Migration)", mod_name))
420                            })
421                            .map(|s| s.to_string())
422                            .collect();
423                        let _ = fs::write(migrations_mod_path, filtered.join("\n") + "\n");
424                    }
425                }
426            }
427        }
428    }
429
430    // 4. Bersihkan .env dari konfigurasi JWT
431    let env_path = ".env";
432    if Path::new(env_path).exists() {
433        if let Ok(content) = fs::read_to_string(env_path) {
434            let filtered: Vec<String> = content
435                .lines()
436                .filter(|line| {
437                    !line.starts_with("JWT_") &&
438                    !line.contains("# --- JWT CONFIG ---")
439                })
440                .map(|s| s.to_string())
441                .collect();
442            let _ = fs::write(env_path, filtered.join("\n") + "\n");
443            println!("   📝 Diperbarui: {}", env_path);
444        }
445    }
446}
447"##.to_string()
448        }
449        "permission:remove" => {
450            r##"use std::fs;
451use std::path::Path;
452
453fn main() {
454    println!("⚙️ Membersihkan scaffolding RBAC Permission...");
455    
456    // 1. Hapus model files
457    let models = [
458        "role.rs",
459        "permission.rs",
460        "model_has_role.rs",
461        "model_has_permission.rs",
462        "role_has_permission.rs"
463    ];
464    for model in &models {
465        let path = format!("src/app/models/{}", model);
466        if Path::new(&path).exists() {
467            let _ = fs::remove_file(&path);
468            println!("   🗑️ Dihapus: {}", path);
469        }
470    }
471
472    // 2. Bersihkan src/app/models/mod.rs
473    let mod_path = "src/app/models/mod.rs";
474    if Path::new(mod_path).exists() {
475        if let Ok(content) = fs::read_to_string(mod_path) {
476            let filtered: Vec<String> = content
477                .lines()
478                .filter(|line| {
479                    !line.contains("pub mod role;") &&
480                    !line.contains("pub use role::Model as Role;") &&
481                    !line.contains("pub use role::Entity as Role;") &&
482                    !line.contains("pub mod permission;") &&
483                    !line.contains("pub use permission::Model as Permission;") &&
484                    !line.contains("pub use permission::Entity as Permission;") &&
485                    !line.contains("pub mod model_has_role;") &&
486                    !line.contains("pub use model_has_role::Model as ModelHasRole;") &&
487                    !line.contains("pub use model_has_role::Entity as ModelHasRole;") &&
488                    !line.contains("pub mod model_has_permission;") &&
489                    !line.contains("pub use model_has_permission::Model as ModelHasPermission;") &&
490                    !line.contains("pub use model_has_permission::Entity as ModelHasPermission;") &&
491                    !line.contains("pub mod role_has_permission;") &&
492                    !line.contains("pub use role_has_permission::Model as RoleHasPermission;") &&
493                    !line.contains("pub use role_has_permission::Entity as RoleHasPermission;")
494                })
495                .map(|s| s.to_string())
496                .collect();
497            let _ = fs::write(mod_path, filtered.join("\n") + "\n");
498            println!("   📝 Diperbarui: {}", mod_path);
499        }
500    }
501
502    // 3. Hapus migrations
503    let migrations_dir = "database/migrations";
504    if let Ok(entries) = fs::read_dir(migrations_dir) {
505        for entry in entries.flatten() {
506            let name = entry.file_name().to_string_lossy().to_string();
507            if name.contains("create_rbac_tables") {
508                let path = entry.path();
509                let _ = fs::remove_file(&path);
510                println!("   🗑️ Dihapus: {}", path.display());
511                
512                // Bersihkan database/migrations/mod.rs
513                let mod_name = name.strip_suffix(".rs").unwrap_or(&name);
514                let migrations_mod_path = "database/migrations/mod.rs";
515                if Path::new(migrations_mod_path).exists() {
516                    if let Ok(content) = fs::read_to_string(migrations_mod_path) {
517                        let filtered: Vec<String> = content
518                            .lines()
519                            .filter(|line| {
520                                !line.contains(&format!("pub mod {};", mod_name)) &&
521                                !line.contains(&format!("Box::new({}::Migration)", mod_name))
522                            })
523                            .map(|s| s.to_string())
524                            .collect();
525                        let _ = fs::write(migrations_mod_path, filtered.join("\n") + "\n");
526                    }
527                }
528            }
529        }
530    }
531}
532"##.to_string()
533        }
534        "activitylog:install" => {
535            r#"fn main() {
536    println!("⚙️ Menjalankan generator scaffolding Activity Log...");
537    rustbasic_activitylog::scaffolding::make_activitylog_scaffolding();
538}
539"#.to_string()
540        }
541        "permission:install" => {
542            r#"fn main() {
543    println!("⚙️ Menjalankan generator scaffolding RBAC Permission...");
544    rustbasic_permission::scaffolding::make_permission_scaffolding();
545}
546"#.to_string()
547        }
548        "native:install" => {
549            r#"fn main() {
550    println!("⚙️ Menjalankan generator scaffolding RustBasic Native...");
551    rustbasic_native::scaffolding::make_native_scaffolding();
552}
553"#.to_string()
554        }
555        "native:remove" => {
556            r#"fn main() {
557    println!("⚙️ Membersihkan scaffolding RustBasic Native...");
558    rustbasic_native::scaffolding::remove_native_scaffolding();
559}
560"#.to_string()
561        }
562        _ => return,
563    };
564
565    std::fs::write(&script_path, &script).ok();
566
567    println!("   {} Menjalankan setup {}...", "⚙️".bold(), package_name.cyan().bold());
568    let mut cmd = Command::new("cargo");
569    cmd.args(["run", "--bin", "temp_pkg_setup"]);
570    let status = crate::utils::run_cargo_with_progress(cmd);
571
572    // Cleanup script
573    std::fs::remove_file(&script_path).ok();
574    // Hapus folder bin jika kosong
575    if let Ok(entries) = std::fs::read_dir(bin_dir)
576        && entries.count() == 0 {
577        std::fs::remove_dir(bin_dir).ok();
578    }
579
580    match status {
581        Ok(s) if s.success() => {}
582        Ok(s) => println!("{} Setup command gagal (exit {})", "⚠️".yellow().bold(), s),
583        Err(e) => println!("{} Gagal menjalankan setup: {}", "⚠️".yellow().bold(), e),
584    }
585}
586
587// ─── Public API ───────────────────────────────────────────────────────────────
588
589/// Install sebuah package ke project
590pub fn install_package(name: &str) {
591    println!("\n{} {}", "📦 Installing:".magenta().bold(), name.cyan().bold());
592
593    // 1. Cek apakah package sudah terinstall
594    let mut manifest = read_manifest();
595    if manifest.packages.iter().any(|p| p.name == name) {
596        println!("{} Package '{}' sudah terinstall.", "⚠️".yellow().bold(), name.yellow());
597        println!("   Gunakan '{}' untuk melihat daftar package.", "rustbasic list packages".cyan());
598        return;
599    }
600
601    // 2. Tentukan versi
602    let (version, description, setup_cmd) = if let Some(info) = known_packages(name) {
603        (info.version.to_string(), info.description.to_string(), info.setup_command.map(|s| s.to_string()))
604    } else {
605        println!("{} Package '{}' tidak dikenali dalam registry RustBasic.", "⚠️".yellow().bold(), name.yellow());
606        println!("   Gunakan versi spesifik dengan menambahkan ke Cargo.toml secara manual.");
607        return;
608    };
609
610    // 3. Tambahkan ke Cargo.toml
611    println!("   {} Menambahkan ke Cargo.toml...", "📝".bold());
612    if !cargo_add_package(name, &version) {
613        return;
614    }
615
616    // 4. cargo build
617    if !run_cargo_build() {
618        // Rollback
619        cargo_remove_package(name);
620        return;
621    }
622
623    // 5. Jalankan setup function
624    if let Some(cmd) = &setup_cmd {
625        run_setup_command(name, cmd);
626    }
627
628    // 6. Catat di manifest
629    let now = rustbasic_core::chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string();
630    manifest.packages.push(InstalledPackage {
631        name: name.to_string(),
632        version,
633        installed_at: now,
634        source: "install".to_string(),
635        description,
636    });
637    write_manifest(&manifest);
638
639    println!("\n{} Package '{}' berhasil diinstall!", "✅".green().bold(), name.cyan().bold());
640    println!("   Gunakan '{}' untuk melihat daftar package.", "rustbasic list packages".cyan());
641}
642
643/// Tampilkan daftar package yang terinstall
644pub fn list_packages() {
645    let mut manifest = read_manifest();
646    sync_manual_packages(&mut manifest);
647
648    println!("\n{}", "📦 RustBasic Package Manager".magenta().bold());
649    println!("{}", "═══════════════════════════════════════════════════════════════════════════".magenta());
650
651    println!("{}", "💻 Package Terinstal (Installed Packages):".bold());
652    if manifest.packages.is_empty() {
653        println!("{}", "   Belum ada package tambahan yang terinstall.".dimmed());
654        println!("   Gunakan '{}' untuk menginstall package.", "rustbasic install <nama-package>".cyan());
655    } else {
656        // Header tabel
657        let header_pkg = format!("{:<28}", "PACKAGE").bold();
658        let header_ver = format!("{:<10}", "VERSION").bold();
659        let header_source = format!("{:<18}", "SOURCE").bold();
660        let header_installed_at = format!("{:<22}", "INSTALLED AT").bold();
661        let header_desc = "DESCRIPTION".bold();
662        println!(
663            "  {}{}{}{} {}",
664            header_pkg,
665            header_ver,
666            header_source,
667            header_installed_at,
668            header_desc
669        );
670        println!("{}", "  ─────────────────────────────────────────────────────────────────────".dimmed());
671
672        for pkg in &manifest.packages {
673            let source_padded = match pkg.source.as_str() {
674                "manual" => format!("{:<18}", "manual").yellow(),
675                _ => format!("{:<18}", "install").green(),
676            };
677            let installed_at_padded = if pkg.installed_at == "—" {
678                format!("{:<22}", "—").dimmed()
679            } else {
680                format!("{:<22}", pkg.installed_at).dimmed()
681            };
682            let name_display = format!("{:<28}", pkg.name).cyan();
683            let version_display = format!("{:<10}", pkg.version);
684            println!(
685                "  {}{}{}{} {}",
686                name_display,
687                version_display,
688                source_padded,
689                installed_at_padded,
690                pkg.description.dimmed()
691            );
692        }
693    }
694
695    println!("\n{}", "═══════════════════════════════════════════════════════════════════════════".magenta());
696    println!("{}", "✨ Package yang Tersedia untuk Diinstal (Available Packages):".bold());
697    println!("{}", "  ─────────────────────────────────────────────────────────────────────".dimmed());
698    let header_pkg = format!("{:<28}", "PACKAGE").bold();
699    let header_ver = format!("{:<10}", "VERSION").bold();
700    let header_status = format!("{:<18}", "STATUS").bold();
701    let header_desc = "DESCRIPTION".bold();
702    println!(
703        "  {}{}{} {}",
704        header_pkg,
705        header_ver,
706        header_status,
707        header_desc
708    );
709    println!("{}", "  ─────────────────────────────────────────────────────────────────────".dimmed());
710
711    let all_packages = &[
712        "rustbasic-breeze",
713        "rustbasic-activitylog",
714        "rustbasic-jwt",
715        "rustbasic-medialibrary",
716        "rustbasic-permission",
717        "rustbasic-translatable",
718        "rustbasic-webp",
719        "rustbasic-native",
720    ];
721
722    for &name in all_packages {
723        if let Some(info) = known_packages(name) {
724            let is_installed = manifest.packages.iter().any(|p| p.name == name);
725            let status_padded = if is_installed {
726                format!("{:<18}", "Terinstal").green()
727            } else {
728                format!("{:<18}", "Tersedia").yellow()
729            };
730            
731            let name_display = format!("{:<28}", name).cyan();
732            let version_display = format!("{:<10}", info.version);
733            
734            println!(
735                "  {}{}{} {}",
736                name_display,
737                version_display,
738                status_padded,
739                info.description.dimmed()
740            );
741        }
742    }
743
744    println!("{}", "═══════════════════════════════════════════════════════════════════════════".magenta());
745    println!(
746        "  {} Total Terinstal: {} | Total Tersedia: {}\n",
747        "📊".bold(),
748        manifest.packages.len().to_string().cyan().bold(),
749        all_packages.len().to_string().yellow().bold()
750    );
751}
752
753/// Uninstall sebuah package dari project
754pub fn uninstall_package(name: &str) {
755    println!("\n{} {}", "🗑️  Uninstalling:".red().bold(), name.cyan().bold());
756
757    let mut manifest = read_manifest();
758    let pkg_idx = manifest.packages.iter().position(|p| p.name == name);
759
760    // Cek jika tidak ada di manifest tapi ada di Cargo.toml
761    let in_cargo = cargo_has_package(name);
762    if pkg_idx.is_none() && !in_cargo {
763        println!("{} Package '{}' tidak ditemukan.", "❌".red().bold(), name.yellow());
764        return;
765    }
766
767    // 1. Jalankan remove function (cleanup file scaffolding)
768    if let Some(info) = known_packages(name)
769        && let Some(remove_cmd) = info.remove_command {
770        // Pastikan package ada di Cargo.toml sebelum menjalankan remove
771        if in_cargo {
772            run_setup_command(name, remove_cmd);
773        }
774    }
775
776    // 2. Hapus dari Cargo.toml
777    println!("   {} Menghapus dari Cargo.toml...", "📝".bold());
778    cargo_remove_package(name);
779
780    // 3. cargo build untuk update lock file
781    println!("   {} Memperbarui dependencies...", "📦".bold());
782    let mut cmd = Command::new("cargo");
783    cmd.arg("build");
784    let _ = crate::utils::run_cargo_with_progress(cmd);
785
786    // 4. Hapus dari manifest
787    if let Some(idx) = pkg_idx {
788        manifest.packages.remove(idx);
789        write_manifest(&manifest);
790    }
791
792    println!("\n{} Package '{}' berhasil diuninstall!", "✅".green().bold(), name.cyan().bold());
793}