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