1use 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
14struct PackageInfo {
16 version: &'static str,
18 description: &'static str,
20 setup_command: Option<&'static str>,
22 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#[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, 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
98pub 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
114fn 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
128fn cargo_has_package(name: &str) -> bool {
130 read_cargo_toml()
131 .map(|c| c.contains(name))
132 .unwrap_or(false)
133}
134
135fn 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; }
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 if let Some(pos) = content.find("[dependencies]") {
155 let insert_at = pos + "[dependencies]".len();
156 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
168fn 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 let result = if content.ends_with('\n') {
178 format!("{}\n", filtered)
179 } else {
180 filtered
181 };
182 write_cargo_toml(&result);
183 }
184}
185
186pub 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 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 if pkg_name == "rustbasic-core" || pkg_name == "rustbasic-cli" {
203 continue;
204 }
205
206 if !known_in_manifest.contains(&pkg_name.to_string()) {
207 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 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 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
243fn 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
263fn 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 std::fs::remove_file(&script_path).ok();
574 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
587pub fn install_package(name: &str) {
591 println!("\n{} {}", "📦 Installing:".magenta().bold(), name.cyan().bold());
592
593 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 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 println!(" {} Menambahkan ke Cargo.toml...", "📝".bold());
612 if !cargo_add_package(name, &version) {
613 return;
614 }
615
616 if !run_cargo_build() {
618 cargo_remove_package(name);
620 return;
621 }
622
623 if let Some(cmd) = &setup_cmd {
625 run_setup_command(name, cmd);
626 }
627
628 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
643pub 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 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
753pub 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 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 if let Some(info) = known_packages(name)
769 && let Some(remove_cmd) = info.remove_command {
770 if in_cargo {
772 run_setup_command(name, remove_cmd);
773 }
774 }
775
776 println!(" {} Menghapus dari Cargo.toml...", "📝".bold());
778 cargo_remove_package(name);
779
780 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 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}