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 _ => None,
35 }
36}
37
38#[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, 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
56pub 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
72fn 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
86fn cargo_has_package(name: &str) -> bool {
88 read_cargo_toml()
89 .map(|c| c.contains(name))
90 .unwrap_or(false)
91}
92
93fn 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; }
103
104 let dep_line = format!("{} = {{ path = \"../{}\", version = \"{}\" }}\n", name, name, version);
105
106 if let Some(pos) = content.find("[dependencies]") {
108 let insert_at = pos + "[dependencies]".len();
109 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
121fn 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 let result = if content.ends_with('\n') {
131 format!("{}\n", filtered)
132 } else {
133 filtered
134 };
135 write_cargo_toml(&result);
136 }
137}
138
139pub 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 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 if pkg_name == "rustbasic-core" || pkg_name == "rustbasic-cli" {
156 continue;
157 }
158
159 if !known_in_manifest.contains(&pkg_name.to_string()) {
160 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 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 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
196fn 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
216fn 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 std::fs::remove_file(&script_path).ok();
254 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
267pub fn install_package(name: &str) {
271 println!("\n{} {}", "📦 Installing:".magenta().bold(), name.cyan().bold());
272
273 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 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 println!(" {} Menambahkan ke Cargo.toml...", "📝".bold());
292 if !cargo_add_package(name, &version) {
293 return;
294 }
295
296 if !run_cargo_build() {
298 cargo_remove_package(name);
300 return;
301 }
302
303 if let Some(cmd) = &setup_cmd {
305 run_setup_command(name, cmd);
306 }
307
308 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
323pub 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 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
375pub 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 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 if let Some(info) = known_packages(name)
391 && let Some(remove_cmd) = info.remove_command {
392 if in_cargo {
394 run_setup_command(name, remove_cmd);
395 }
396 }
397
398 println!(" {} Menghapus dari Cargo.toml...", "📝".bold());
400 cargo_remove_package(name);
401
402 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 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}