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 _ => None,
71 }
72}
73
74#[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, 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
92pub 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
108fn 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
122fn cargo_has_package(name: &str) -> bool {
124 read_cargo_toml()
125 .map(|c| c.contains(name))
126 .unwrap_or(false)
127}
128
129fn 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; }
139
140 let dep_line = format!("{} = {{ path = \"../{}\", version = \"{}\" }}\n", name, name, version);
141
142 if let Some(pos) = content.find("[dependencies]") {
144 let insert_at = pos + "[dependencies]".len();
145 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
157fn 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 let result = if content.ends_with('\n') {
167 format!("{}\n", filtered)
168 } else {
169 filtered
170 };
171 write_cargo_toml(&result);
172 }
173}
174
175pub 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 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 if pkg_name == "rustbasic-core" || pkg_name == "rustbasic-cli" {
192 continue;
193 }
194
195 if !known_in_manifest.contains(&pkg_name.to_string()) {
196 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 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 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
232fn 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
252fn 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 std::fs::remove_file(&script_path).ok();
316 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
329pub fn install_package(name: &str) {
333 println!("\n{} {}", "📦 Installing:".magenta().bold(), name.cyan().bold());
334
335 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 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 println!(" {} Menambahkan ke Cargo.toml...", "📝".bold());
354 if !cargo_add_package(name, &version) {
355 return;
356 }
357
358 if !run_cargo_build() {
360 cargo_remove_package(name);
362 return;
363 }
364
365 if let Some(cmd) = &setup_cmd {
367 run_setup_command(name, cmd);
368 }
369
370 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
385pub 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 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
437pub 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 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 if let Some(info) = known_packages(name)
453 && let Some(remove_cmd) = info.remove_command {
454 if in_cargo {
456 run_setup_command(name, remove_cmd);
457 }
458 }
459
460 println!(" {} Menghapus dari Cargo.toml...", "📝".bold());
462 cargo_remove_package(name);
463
464 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 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}