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: 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#[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 dep_line = format!("{} = {{ path = \"../{}\", version = \"{}\" }}\n", name, name, version);
147
148 if let Some(pos) = content.find("[dependencies]") {
150 let insert_at = pos + "[dependencies]".len();
151 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
163fn 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 let result = if content.ends_with('\n') {
173 format!("{}\n", filtered)
174 } else {
175 filtered
176 };
177 write_cargo_toml(&result);
178 }
179}
180
181pub 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 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 if pkg_name == "rustbasic-core" || pkg_name == "rustbasic-cli" {
198 continue;
199 }
200
201 if !known_in_manifest.contains(&pkg_name.to_string()) {
202 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 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 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
238fn 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
258fn 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 std::fs::remove_file(&script_path).ok();
348 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
361pub fn install_package(name: &str) {
365 println!("\n{} {}", "📦 Installing:".magenta().bold(), name.cyan().bold());
366
367 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 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 println!(" {} Menambahkan ke Cargo.toml...", "📝".bold());
386 if !cargo_add_package(name, &version) {
387 return;
388 }
389
390 if !run_cargo_build() {
392 cargo_remove_package(name);
394 return;
395 }
396
397 if let Some(cmd) = &setup_cmd {
399 run_setup_command(name, cmd);
400 }
401
402 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
417pub 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 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
469pub 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 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 if let Some(info) = known_packages(name)
485 && let Some(remove_cmd) = info.remove_command {
486 if in_cargo {
488 run_setup_command(name, remove_cmd);
489 }
490 }
491
492 println!(" {} Menghapus dari Cargo.toml...", "📝".bold());
494 cargo_remove_package(name);
495
496 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 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}