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