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: None,
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: None,
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: None,
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 dep_line = format!("{} = {{ path = \"../{}\", version = \"{}\" }}\n", name, name, version);
147
148    // Sisipkan setelah [dependencies]
149    if let Some(pos) = content.find("[dependencies]") {
150        let insert_at = pos + "[dependencies]".len();
151        // Cari akhir baris [dependencies]
152        let after = &content[insert_at..];
153        let newline_pos = after.find('\n').map(|p| insert_at + p + 1).unwrap_or(insert_at);
154        content.insert_str(newline_pos, &dep_line);
155        write_cargo_toml(&content);
156        true
157    } else {
158        println!("{}", "❌ Tidak dapat menemukan section [dependencies] di Cargo.toml".red().bold());
159        false
160    }
161}
162
163/// Hapus dependency dari Cargo.toml
164fn cargo_remove_package(name: &str) {
165    if let Some(content) = read_cargo_toml() {
166        let filtered: String = content
167            .lines()
168            .filter(|line| !line.contains(name))
169            .collect::<Vec<_>>()
170            .join("\n");
171        // Pertahankan trailing newline
172        let result = if content.ends_with('\n') {
173            format!("{}\n", filtered)
174        } else {
175            filtered
176        };
177        write_cargo_toml(&result);
178    }
179}
180
181/// Scan Cargo.toml untuk package rustbasic-* yang belum ada di manifest
182pub fn sync_manual_packages(manifest: &mut PackageManifest) {
183    let Some(content) = read_cargo_toml() else { return };
184    let known_in_manifest: Vec<String> = manifest.packages.iter().map(|p| p.name.clone()).collect();
185
186    for line in content.lines() {
187        let trimmed = line.trim();
188        if trimmed.starts_with('#') { continue; }
189        if let Some(idx) = trimmed.find("rustbasic-") {
190            let rest = &trimmed[idx..];
191            // Ekstrak nama package: ambil sampai karakter non-alfanumerik/dash
192            let name_end = rest.find(|c: char| !c.is_alphanumeric() && c != '-')
193                .unwrap_or(rest.len());
194            let pkg_name = &rest[..name_end];
195
196            // Skip core dan cli
197            if pkg_name == "rustbasic-core" || pkg_name == "rustbasic-cli" {
198                continue;
199            }
200
201            if !known_in_manifest.contains(&pkg_name.to_string()) {
202                // Ekstrak versi jika ada
203                let version = extract_version_from_line(trimmed).unwrap_or_else(|| "?".to_string());
204                let desc = known_packages(pkg_name)
205                    .map(|p| p.description.to_string())
206                    .unwrap_or_else(|| "Package eksternal".to_string());
207
208                manifest.packages.push(InstalledPackage {
209                    name: pkg_name.to_string(),
210                    version,
211                    installed_at: "—".to_string(),
212                    source: "manual".to_string(),
213                    description: desc,
214                });
215            }
216        }
217    }
218}
219
220fn extract_version_from_line(line: &str) -> Option<String> {
221    // Coba cari 'version = "x.x"' atau '"x.x"' pattern
222    if let Some(start) = line.find("version = \"") {
223        let rest = &line[start + 11..];
224        if let Some(end) = rest.find('"') {
225            return Some(rest[..end].to_string());
226        }
227    }
228    // Bentuk ringkas: package = "x.x"
229    if let Some(eq) = line.find(" = \"") {
230        let rest = &line[eq + 4..];
231        if let Some(end) = rest.find('"') {
232            return Some(rest[..end].to_string());
233        }
234    }
235    None
236}
237
238// ─── Cargo Build ──────────────────────────────────────────────────────────────
239
240fn run_cargo_build() -> bool {
241    println!("   {} Mengunduh dan mengkompilasi dependensi...", "📦".bold());
242    let mut cmd = Command::new("cargo");
243    cmd.arg("build");
244    let status = crate::utils::run_cargo_with_progress(cmd);
245    match status {
246        Ok(s) if s.success() => true,
247        Ok(s) => {
248            println!("{} cargo build gagal dengan exit code: {}", "❌".red().bold(), s);
249            false
250        }
251        Err(e) => {
252            println!("{} Gagal menjalankan cargo build: {}", "❌".red().bold(), e);
253            false
254        }
255    }
256}
257
258// ─── Setup / Remove via Temp Binary ───────────────────────────────────────────
259
260fn run_setup_command(package_name: &str, command: &str) {
261    let bin_dir = "src/bin";
262    std::fs::create_dir_all(bin_dir).ok();
263    let script_path = format!("{}/temp_pkg_setup.rs", bin_dir);
264
265    let script = match command {
266        "breeze:install" => {
267            r#"use rustbasic_core::dotenvy::dotenv;
268#[tokio::main]
269async fn main() {
270    dotenv().ok();
271    rustbasic_breeze::make_auth().await;
272}
273"#.to_string()
274        }
275        "breeze:remove" => {
276            r#"use rustbasic_core::dotenvy::dotenv;
277#[tokio::main]
278async fn main() {
279    dotenv().ok();
280    rustbasic_breeze::remove_auth().await;
281}
282"#.to_string()
283        }
284        "activitylog:install" => {
285            r#"fn main() {
286    println!("⚙️ Menjalankan generator scaffolding Activity Log...");
287    let mut cmd = std::process::Command::new("cargo");
288    cmd.args(["run", "--bin", "rustbasic-activitylog", "--", "install"]);
289    if let Ok(status) = cmd.status() {
290        if !status.success() {
291            eprintln!("❌ Scaffolding Activity Log gagal");
292        }
293    }
294}
295"#.to_string()
296        }
297        "permission:install" => {
298            r#"fn main() {
299    println!("⚙️ Menjalankan generator scaffolding RBAC Permission...");
300    let mut cmd = std::process::Command::new("cargo");
301    cmd.args(["run", "--bin", "rustbasic-permission", "--", "install"]);
302    if let Ok(status) = cmd.status() {
303        if !status.success() {
304            eprintln!("❌ Scaffolding RBAC Permission gagal");
305        }
306    }
307}
308"#.to_string()
309        }
310        "native:install" => {
311            r#"fn main() {
312    println!("⚙️ Menjalankan generator scaffolding RustBasic Native...");
313    let mut cmd = std::process::Command::new("cargo");
314    cmd.args(["run", "--manifest-path", "../rustbasic-native/Cargo.toml", "--", "install"]);
315    if let Ok(status) = cmd.status() {
316        if !status.success() {
317            eprintln!("❌ Scaffolding RustBasic Native gagal");
318        }
319    }
320}
321"#.to_string()
322        }
323        "native:remove" => {
324            r#"fn main() {
325    println!("⚙️ Membersihkan scaffolding RustBasic Native...");
326    let mut cmd = std::process::Command::new("cargo");
327    cmd.args(["run", "--manifest-path", "../rustbasic-native/Cargo.toml", "--", "uninstall"]);
328    if let Ok(status) = cmd.status() {
329        if !status.success() {
330            eprintln!("❌ Pembersihan scaffolding RustBasic Native gagal");
331        }
332    }
333}
334"#.to_string()
335        }
336        _ => return,
337    };
338
339    std::fs::write(&script_path, &script).ok();
340
341    println!("   {} Menjalankan setup {}...", "⚙️".bold(), package_name.cyan().bold());
342    let mut cmd = Command::new("cargo");
343    cmd.args(["run", "--bin", "temp_pkg_setup"]);
344    let status = crate::utils::run_cargo_with_progress(cmd);
345
346    // Cleanup script
347    std::fs::remove_file(&script_path).ok();
348    // Hapus folder bin jika kosong
349    if let Ok(entries) = std::fs::read_dir(bin_dir)
350        && entries.count() == 0 {
351        std::fs::remove_dir(bin_dir).ok();
352    }
353
354    match status {
355        Ok(s) if s.success() => {}
356        Ok(s) => println!("{} Setup command gagal (exit {})", "⚠️".yellow().bold(), s),
357        Err(e) => println!("{} Gagal menjalankan setup: {}", "⚠️".yellow().bold(), e),
358    }
359}
360
361// ─── Public API ───────────────────────────────────────────────────────────────
362
363/// Install sebuah package ke project
364pub fn install_package(name: &str) {
365    println!("\n{} {}", "📦 Installing:".magenta().bold(), name.cyan().bold());
366
367    // 1. Cek apakah package sudah terinstall
368    let mut manifest = read_manifest();
369    if manifest.packages.iter().any(|p| p.name == name) {
370        println!("{} Package '{}' sudah terinstall.", "⚠️".yellow().bold(), name.yellow());
371        println!("   Gunakan '{}' untuk melihat daftar package.", "rustbasic list packages".cyan());
372        return;
373    }
374
375    // 2. Tentukan versi
376    let (version, description, setup_cmd) = if let Some(info) = known_packages(name) {
377        (info.version.to_string(), info.description.to_string(), info.setup_command.map(|s| s.to_string()))
378    } else {
379        println!("{} Package '{}' tidak dikenali dalam registry RustBasic.", "⚠️".yellow().bold(), name.yellow());
380        println!("   Gunakan versi spesifik dengan menambahkan ke Cargo.toml secara manual.");
381        return;
382    };
383
384    // 3. Tambahkan ke Cargo.toml
385    println!("   {} Menambahkan ke Cargo.toml...", "📝".bold());
386    if !cargo_add_package(name, &version) {
387        return;
388    }
389
390    // 4. cargo build
391    if !run_cargo_build() {
392        // Rollback
393        cargo_remove_package(name);
394        return;
395    }
396
397    // 5. Jalankan setup function
398    if let Some(cmd) = &setup_cmd {
399        run_setup_command(name, cmd);
400    }
401
402    // 6. Catat di manifest
403    let now = rustbasic_core::chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string();
404    manifest.packages.push(InstalledPackage {
405        name: name.to_string(),
406        version,
407        installed_at: now,
408        source: "install".to_string(),
409        description,
410    });
411    write_manifest(&manifest);
412
413    println!("\n{} Package '{}' berhasil diinstall!", "✅".green().bold(), name.cyan().bold());
414    println!("   Gunakan '{}' untuk melihat daftar package.", "rustbasic list packages".cyan());
415}
416
417/// Tampilkan daftar package yang terinstall
418pub fn list_packages() {
419    let mut manifest = read_manifest();
420    sync_manual_packages(&mut manifest);
421
422    println!("\n{}", "📦 RustBasic Package Manager".magenta().bold());
423    println!("{}", "═══════════════════════════════════════════════════════════════════════════".magenta());
424
425    if manifest.packages.is_empty() {
426        println!("{}", "   Belum ada package tambahan yang terinstall.".dimmed());
427        println!("   Gunakan '{}' untuk menginstall package.", "rustbasic install <nama-package>".cyan());
428    } else {
429        // Header tabel
430        println!(
431            "  {:<28} {:<10} {:<10} {:<22} {}",
432            "PACKAGE".bold(),
433            "VERSION".bold(),
434            "SOURCE".bold(),
435            "INSTALLED AT".bold(),
436            "DESCRIPTION".bold()
437        );
438        println!("{}", "  ─────────────────────────────────────────────────────────────────────".dimmed());
439
440        for pkg in &manifest.packages {
441            let source_display = match pkg.source.as_str() {
442                "manual" => "manual".yellow().to_string(),
443                _ => "install".green().to_string(),
444            };
445            let installed_at = if pkg.installed_at == "—" {
446                "—".dimmed().to_string()
447            } else {
448                pkg.installed_at.clone().dimmed().to_string()
449            };
450            println!(
451                "  {:<28} {:<10} {:<18} {:<22} {}",
452                pkg.name.cyan(),
453                pkg.version,
454                source_display,
455                installed_at,
456                pkg.description.dimmed()
457            );
458        }
459    }
460
461    println!("{}", "═══════════════════════════════════════════════════════════════════════════".magenta());
462    println!(
463        "  {} Total: {} package\n",
464        "📊".bold(),
465        manifest.packages.len().to_string().cyan().bold()
466    );
467}
468
469/// Uninstall sebuah package dari project
470pub fn uninstall_package(name: &str) {
471    println!("\n{} {}", "🗑️  Uninstalling:".red().bold(), name.cyan().bold());
472
473    let mut manifest = read_manifest();
474    let pkg_idx = manifest.packages.iter().position(|p| p.name == name);
475
476    // Cek jika tidak ada di manifest tapi ada di Cargo.toml
477    let in_cargo = cargo_has_package(name);
478    if pkg_idx.is_none() && !in_cargo {
479        println!("{} Package '{}' tidak ditemukan.", "❌".red().bold(), name.yellow());
480        return;
481    }
482
483    // 1. Jalankan remove function (cleanup file scaffolding)
484    if let Some(info) = known_packages(name)
485        && let Some(remove_cmd) = info.remove_command {
486        // Pastikan package ada di Cargo.toml sebelum menjalankan remove
487        if in_cargo {
488            run_setup_command(name, remove_cmd);
489        }
490    }
491
492    // 2. Hapus dari Cargo.toml
493    println!("   {} Menghapus dari Cargo.toml...", "📝".bold());
494    cargo_remove_package(name);
495
496    // 3. cargo build untuk update lock file
497    println!("   {} Memperbarui dependencies...", "📦".bold());
498    let mut cmd = Command::new("cargo");
499    cmd.arg("build");
500    let _ = crate::utils::run_cargo_with_progress(cmd);
501
502    // 4. Hapus dari manifest
503    if let Some(idx) = pkg_idx {
504        manifest.packages.remove(idx);
505        write_manifest(&manifest);
506    }
507
508    println!("\n{} Package '{}' berhasil diuninstall!", "✅".green().bold(), name.cyan().bold());
509}