1use colored::*;
8use serde::{Deserialize, Serialize};
9use std::process::Command;
10
11const MANIFEST_FILE: &str = ".rustbasic_packages.json";
12
13struct PackageInfo {
15 version: &'static str,
17 description: &'static str,
19 setup_command: Option<&'static str>,
21 remove_command: Option<&'static str>,
23}
24
25fn known_packages(name: &str) -> Option<PackageInfo> {
26 match name {
27 "rustbasic-breeze" => Some(PackageInfo {
28 version: "0.0",
29 description: "Authentication scaffolding (login, register, reset password)",
30 setup_command: Some("breeze:install"),
31 remove_command: Some("breeze:remove"),
32 }),
33 _ => None,
34 }
35}
36
37#[derive(Debug, Serialize, Deserialize, Clone)]
40pub struct InstalledPackage {
41 pub name: String,
42 pub version: String,
43 pub installed_at: String,
44 pub source: String, pub description: String,
46}
47
48#[derive(Debug, Serialize, Deserialize, Default)]
49pub struct PackageManifest {
50 pub packages: Vec<InstalledPackage>,
51}
52
53pub fn read_manifest() -> PackageManifest {
56 if let Ok(content) = std::fs::read_to_string(MANIFEST_FILE) {
57 serde_json::from_str(&content).unwrap_or_default()
58 } else {
59 PackageManifest::default()
60 }
61}
62
63fn write_manifest(manifest: &PackageManifest) {
64 if let Ok(json) = serde_json::to_string_pretty(manifest) {
65 std::fs::write(MANIFEST_FILE, json).ok();
66 }
67}
68
69fn cargo_toml_path() -> &'static str {
72 "Cargo.toml"
73}
74
75fn read_cargo_toml() -> Option<String> {
76 std::fs::read_to_string(cargo_toml_path()).ok()
77}
78
79fn write_cargo_toml(content: &str) {
80 std::fs::write(cargo_toml_path(), content).ok();
81}
82
83fn cargo_has_package(name: &str) -> bool {
85 read_cargo_toml()
86 .map(|c| c.contains(name))
87 .unwrap_or(false)
88}
89
90fn cargo_add_package(name: &str, version: &str) -> bool {
92 let Some(mut content) = read_cargo_toml() else {
93 println!("{}", "❌ Tidak dapat membaca Cargo.toml".red().bold());
94 return false;
95 };
96
97 if content.contains(name) {
98 return true; }
100
101 let dep_line = format!("{} = {{ version = \"{}\" }}\n", name, version);
102
103 if let Some(pos) = content.find("[dependencies]") {
105 let insert_at = pos + "[dependencies]".len();
106 let after = &content[insert_at..];
108 let newline_pos = after.find('\n').map(|p| insert_at + p + 1).unwrap_or(insert_at);
109 content.insert_str(newline_pos, &dep_line);
110 write_cargo_toml(&content);
111 true
112 } else {
113 println!("{}", "❌ Tidak dapat menemukan section [dependencies] di Cargo.toml".red().bold());
114 false
115 }
116}
117
118fn cargo_remove_package(name: &str) {
120 if let Some(content) = read_cargo_toml() {
121 let filtered: String = content
122 .lines()
123 .filter(|line| !line.contains(name))
124 .collect::<Vec<_>>()
125 .join("\n");
126 let result = if content.ends_with('\n') {
128 format!("{}\n", filtered)
129 } else {
130 filtered
131 };
132 write_cargo_toml(&result);
133 }
134}
135
136pub fn sync_manual_packages(manifest: &mut PackageManifest) {
138 let Some(content) = read_cargo_toml() else { return };
139 let known_in_manifest: Vec<String> = manifest.packages.iter().map(|p| p.name.clone()).collect();
140
141 for line in content.lines() {
142 let trimmed = line.trim();
143 if trimmed.starts_with('#') { continue; }
144 if let Some(idx) = trimmed.find("rustbasic-") {
145 let rest = &trimmed[idx..];
146 let name_end = rest.find(|c: char| !c.is_alphanumeric() && c != '-')
148 .unwrap_or(rest.len());
149 let pkg_name = &rest[..name_end];
150
151 if pkg_name == "rustbasic-core" || pkg_name == "rustbasic-cli" {
153 continue;
154 }
155
156 if !known_in_manifest.contains(&pkg_name.to_string()) {
157 let version = extract_version_from_line(trimmed).unwrap_or_else(|| "?".to_string());
159 let desc = known_packages(pkg_name)
160 .map(|p| p.description.to_string())
161 .unwrap_or_else(|| "Package eksternal".to_string());
162
163 manifest.packages.push(InstalledPackage {
164 name: pkg_name.to_string(),
165 version,
166 installed_at: "—".to_string(),
167 source: "manual".to_string(),
168 description: desc,
169 });
170 }
171 }
172 }
173}
174
175fn extract_version_from_line(line: &str) -> Option<String> {
176 if let Some(start) = line.find("version = \"") {
178 let rest = &line[start + 11..];
179 if let Some(end) = rest.find('"') {
180 return Some(rest[..end].to_string());
181 }
182 }
183 if let Some(eq) = line.find(" = \"") {
185 let rest = &line[eq + 4..];
186 if let Some(end) = rest.find('"') {
187 return Some(rest[..end].to_string());
188 }
189 }
190 None
191}
192
193fn run_cargo_build() -> bool {
196 println!(" {} Mengunduh dan mengkompilasi dependensi...", "📦".bold());
197 let status = Command::new("cargo")
198 .args(["build", "-q"])
199 .status();
200 match status {
201 Ok(s) if s.success() => true,
202 Ok(s) => {
203 println!("{} cargo build gagal dengan exit code: {}", "❌".red().bold(), s);
204 false
205 }
206 Err(e) => {
207 println!("{} Gagal menjalankan cargo build: {}", "❌".red().bold(), e);
208 false
209 }
210 }
211}
212
213fn run_setup_command(package_name: &str, command: &str) {
216 let bin_dir = "src/bin";
217 std::fs::create_dir_all(bin_dir).ok();
218 let script_path = format!("{}/temp_pkg_setup.rs", bin_dir);
219
220 let script = match command {
221 "breeze:install" => {
222 r#"use rustbasic_core::dotenvy::dotenv;
223#[tokio::main]
224async fn main() {
225 dotenv().ok();
226 rustbasic_breeze::make_auth().await;
227}
228"#.to_string()
229 }
230 "breeze:remove" => {
231 r#"use rustbasic_core::dotenvy::dotenv;
232#[tokio::main]
233async fn main() {
234 dotenv().ok();
235 rustbasic_breeze::remove_auth().await;
236}
237"#.to_string()
238 }
239 _ => return,
240 };
241
242 std::fs::write(&script_path, &script).ok();
243
244 println!(" {} Menjalankan setup {}...", "⚙️".bold(), package_name.cyan().bold());
245 let status = Command::new("cargo")
246 .args(["run", "-q", "--bin", "temp_pkg_setup"])
247 .status();
248
249 std::fs::remove_file(&script_path).ok();
251 if let Ok(entries) = std::fs::read_dir(bin_dir)
253 && entries.count() == 0 {
254 std::fs::remove_dir(bin_dir).ok();
255 }
256
257 match status {
258 Ok(s) if s.success() => {}
259 Ok(s) => println!("{} Setup command gagal (exit {})", "⚠️".yellow().bold(), s),
260 Err(e) => println!("{} Gagal menjalankan setup: {}", "⚠️".yellow().bold(), e),
261 }
262}
263
264pub fn install_package(name: &str) {
268 println!("\n{} {}", "📦 Installing:".magenta().bold(), name.cyan().bold());
269
270 let mut manifest = read_manifest();
272 if manifest.packages.iter().any(|p| p.name == name) {
273 println!("{} Package '{}' sudah terinstall.", "⚠️".yellow().bold(), name.yellow());
274 println!(" Gunakan '{}' untuk melihat daftar package.", "rustbasic list packages".cyan());
275 return;
276 }
277
278 let (version, description, setup_cmd) = if let Some(info) = known_packages(name) {
280 (info.version.to_string(), info.description.to_string(), info.setup_command.map(|s| s.to_string()))
281 } else {
282 println!("{} Package '{}' tidak dikenali dalam registry RustBasic.", "⚠️".yellow().bold(), name.yellow());
283 println!(" Gunakan versi spesifik dengan menambahkan ke Cargo.toml secara manual.");
284 return;
285 };
286
287 println!(" {} Menambahkan ke Cargo.toml...", "📝".bold());
289 if !cargo_add_package(name, &version) {
290 return;
291 }
292
293 if !run_cargo_build() {
295 cargo_remove_package(name);
297 return;
298 }
299
300 if let Some(cmd) = &setup_cmd {
302 run_setup_command(name, cmd);
303 }
304
305 let now = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S").to_string();
307 manifest.packages.push(InstalledPackage {
308 name: name.to_string(),
309 version,
310 installed_at: now,
311 source: "install".to_string(),
312 description,
313 });
314 write_manifest(&manifest);
315
316 println!("\n{} Package '{}' berhasil diinstall!", "✅".green().bold(), name.cyan().bold());
317 println!(" Gunakan '{}' untuk melihat daftar package.", "rustbasic list packages".cyan());
318}
319
320pub fn list_packages() {
322 let mut manifest = read_manifest();
323 sync_manual_packages(&mut manifest);
324
325 println!("\n{}", "📦 RustBasic Package Manager".magenta().bold());
326 println!("{}", "═══════════════════════════════════════════════════════════════════════════".magenta());
327
328 if manifest.packages.is_empty() {
329 println!("{}", " Belum ada package tambahan yang terinstall.".dimmed());
330 println!(" Gunakan '{}' untuk menginstall package.", "rustbasic install <nama-package>".cyan());
331 } else {
332 println!(
334 " {:<28} {:<10} {:<10} {:<22} {}",
335 "PACKAGE".bold(),
336 "VERSION".bold(),
337 "SOURCE".bold(),
338 "INSTALLED AT".bold(),
339 "DESCRIPTION".bold()
340 );
341 println!("{}", " ─────────────────────────────────────────────────────────────────────".dimmed());
342
343 for pkg in &manifest.packages {
344 let source_display = match pkg.source.as_str() {
345 "manual" => "manual".yellow().to_string(),
346 _ => "install".green().to_string(),
347 };
348 let installed_at = if pkg.installed_at == "—" {
349 "—".dimmed().to_string()
350 } else {
351 pkg.installed_at.clone().dimmed().to_string()
352 };
353 println!(
354 " {:<28} {:<10} {:<18} {:<22} {}",
355 pkg.name.cyan(),
356 pkg.version,
357 source_display,
358 installed_at,
359 pkg.description.dimmed()
360 );
361 }
362 }
363
364 println!("{}", "═══════════════════════════════════════════════════════════════════════════".magenta());
365 println!(
366 " {} Total: {} package\n",
367 "📊".bold(),
368 manifest.packages.len().to_string().cyan().bold()
369 );
370}
371
372pub fn uninstall_package(name: &str) {
374 println!("\n{} {}", "🗑️ Uninstalling:".red().bold(), name.cyan().bold());
375
376 let mut manifest = read_manifest();
377 let pkg_idx = manifest.packages.iter().position(|p| p.name == name);
378
379 let in_cargo = cargo_has_package(name);
381 if pkg_idx.is_none() && !in_cargo {
382 println!("{} Package '{}' tidak ditemukan.", "❌".red().bold(), name.yellow());
383 return;
384 }
385
386 if let Some(info) = known_packages(name)
388 && let Some(remove_cmd) = info.remove_command {
389 if in_cargo {
391 run_setup_command(name, remove_cmd);
392 }
393 }
394
395 println!(" {} Menghapus dari Cargo.toml...", "📝".bold());
397 cargo_remove_package(name);
398
399 println!(" {} Memperbarui dependencies...", "📦".bold());
401 let _ = Command::new("cargo").args(["build", "-q"]).status();
402
403 if let Some(idx) = pkg_idx {
405 manifest.packages.remove(idx);
406 write_manifest(&manifest);
407 }
408
409 println!("\n{} Package '{}' berhasil diuninstall!", "✅".green().bold(), name.cyan().bold());
410}