1use crate::app::Config;
2use crate::schema::MigratorTrait;
3use crate::seeder::SeederTrait;
4use crate::colored::Colorize;
5
6pub async fn handle<
11 M: MigratorTrait + Send + Sync + 'static,
12 S: SeederTrait + Send + Sync + 'static,
13>(
14 args: &[String],
15 cfg: &Config,
16 seeder: Option<S>,
17) -> bool {
18 if args.len() < 2 {
19 return false;
20 }
21
22 let command = args[1].as_str();
23
24 let is_migration_cmd = command.starts_with("migrate") || command == "db:seed";
25 let is_storage_cmd = command == "storage:link";
26 let is_server_cmd = command == "server" || command == "serve";
27 let is_build_cmd = command == "build";
28 let is_make_cmd = command.starts_with("make:");
29 let is_route_cmd = command == "route:list";
30 let is_key_cmd = command == "key:generate";
31 let is_deploy_cmd = command == "deploy";
32 let is_publish_cmd = command == "publish";
33
34 if is_server_cmd {
36 let run_android = args.iter().any(|arg| arg == "--android");
37 let run_desktop = args.iter().any(|arg| arg == "--desktop");
38 if run_android || run_desktop {
39 run_native(run_android, run_desktop);
40 return true;
41 }
42 return false; }
44
45 if !is_migration_cmd && !is_storage_cmd && !is_build_cmd
46 && !is_make_cmd && !is_route_cmd && !is_key_cmd && !is_deploy_cmd
47 && !is_publish_cmd {
48 return false;
49 }
50
51 println!("š ļø RustBasic CLI - Command: {}", command);
52
53 if is_build_cmd {
54 handle_build(args).await;
55 return true;
56 }
57
58 if is_deploy_cmd {
59 handle_deploy().await;
60 return true;
61 }
62
63 if is_storage_cmd {
64 handle_storage_link(cfg);
65 return true;
66 }
67
68 if is_make_cmd {
69 handle_make(args);
70 return true;
71 }
72
73 if is_route_cmd {
74 handle_route_list();
75 return true;
76 }
77
78 if is_key_cmd {
79 handle_key_generate();
80 return true;
81 }
82
83 if is_publish_cmd {
84 let target = args.get(2).map(|s| s.as_str()).unwrap_or("");
85 handle_publish(target);
86 return true;
87 }
88
89 let pool = crate::database::connect(cfg).await;
91
92 match command {
93 "migrate" => {
94 println!("š Menjalankan migrasi database...");
95 if let Err(e) = M::up(&pool, None).await {
96 println!("ā Gagal menjalankan migrasi: {}", e);
97 } else {
98 println!("ā
Migrasi selesai!");
99 }
100 }
101 "migrate:refresh" => {
102 println!("š Mereset dan menjalankan ulang migrasi...");
103 if let Err(e) = M::fresh(&pool).await {
104 println!("ā Gagal refresh migrasi: {}", e);
105 } else {
106 println!("ā
Database berhasil di-refresh!");
107 }
108 }
109 "migrate:back" | "migrate:rollback" => {
110 println!("ā¬
ļø Rollback migrasi terakhir...");
111 if let Err(e) = M::down(&pool, None).await {
112 println!("ā Gagal rollback: {}", e);
113 } else {
114 println!("ā
Rollback berhasil!");
115 }
116 }
117 "db:seed" => {
118 println!("š± Menjalankan database seeder...");
119 match seeder {
120 Some(s) => {
121 if let Err(e) = s.run(&pool).await {
122 println!("ā Seeder gagal: {}", e);
123 } else {
124 println!("ā
Seeder selesai!");
125 }
126 }
127 None => {
128 println!("ā ļø Tidak ada seeder yang terdaftar.");
129 }
130 }
131 }
132 _ => return false,
133 }
134
135 true
136}
137
138fn handle_key_generate() {
142 let key = crate::rand::random_alphanumeric(32);
143 let encoded = crate::base64::encode(key.as_bytes());
144
145 let env_path = std::path::Path::new(".env");
147 if env_path.exists() {
148 let content = std::fs::read_to_string(env_path).unwrap_or_default();
149 let new_content = if content.contains("APP_KEY=") {
150 let mut lines: Vec<String> = content
151 .lines()
152 .map(|line| {
153 if line.starts_with("APP_KEY=") {
154 format!("APP_KEY={}", encoded)
155 } else {
156 line.to_string()
157 }
158 })
159 .collect();
160 if !lines.last().map(|l| l.is_empty()).unwrap_or(true) {
162 lines.push(String::new());
163 }
164 lines.join("\n")
165 } else {
166 format!("{}\nAPP_KEY={}\n", content.trim_end(), encoded)
167 };
168 if let Err(e) = std::fs::write(env_path, new_content) {
169 println!("ā Gagal menulis ke .env: {}", e);
170 return;
171 }
172 println!("ā
APP_KEY berhasil dibuat dan disimpan ke .env");
173 println!(" Key: {}", encoded);
174 } else {
175 println!("ā ļø File .env tidak ditemukan. Key yang dibuat:");
176 println!(" APP_KEY={}", encoded);
177 }
178}
179
180fn handle_route_list() {
184 let route_files = ["src/routes/web.rs", "src/routes/api.rs"];
186 let mut routes: Vec<(String, String, String)> = Vec::new(); for file_path in &route_files {
189 if let Ok(content) = std::fs::read_to_string(file_path) {
190 for line in content.lines() {
191 let line = line.trim();
192 if line.starts_with(".route(") || line.contains(".route(\"") {
195 parse_route_line(line, &mut routes);
196 }
197 }
198 }
199 }
200
201 if routes.is_empty() {
202 println!("ā¹ļø Tidak ada rute ditemukan atau format rute tidak dikenal.");
203 println!(" Cek file src/routes/web.rs dan src/routes/api.rs");
204 return;
205 }
206
207 println!("\n{}", "=".repeat(72));
209 println!(" {:<8} {:<35} {}", "METHOD", "PATH", "HANDLER");
210 println!("{}", "=".repeat(72));
211 for (method, path, handler) in &routes {
212 println!(" {:<8} {:<35} {}", method, path, handler);
213 }
214 println!("{}\n", "=".repeat(72));
215 println!(" Total: {} rute terdaftar", routes.len());
216}
217
218fn parse_route_line(line: &str, routes: &mut Vec<(String, String, String)>) {
219 let methods = ["get", "post", "put", "patch", "delete"];
221
222 let path = if let Some(start) = line.find(".route(\"") {
224 let after = &line[start + 8..];
225 if let Some(end) = after.find('"') {
226 after[..end].to_string()
227 } else { return; }
228 } else { return; };
229
230 for method in &methods {
232 let pattern = format!("{}(", method);
233 if let Some(pos) = line.find(&pattern) {
234 let after = &line[pos + method.len() + 1..];
235 let handler = if let Some(end) = after.find(')') {
236 after[..end].to_string()
237 } else {
238 after.to_string()
239 };
240 routes.push((method.to_uppercase(), path.clone(), handler));
241 break;
242 }
243 }
244}
245
246fn handle_make(args: &[String]) {
250 let subcommand = args[1].as_str(); let name = args.get(2).map(|s| s.as_str()).unwrap_or("");
252
253 if name.is_empty() {
254 println!("ā Nama diperlukan. Contoh: rustbasic {} MyName", subcommand);
255 return;
256 }
257
258 match subcommand {
259 "make:controller" => make_controller(name),
260 "make:model" => {
261 let with_migration = args.iter().any(|a| a == "-m" || a == "--migration");
262 make_model(name, with_migration);
263 }
264 "make:middleware" => make_middleware(name),
265 "make:observer" => {
266 let model = args.iter().find(|a| a.starts_with("--model="))
267 .and_then(|a| a.strip_prefix("--model="))
268 .unwrap_or("Model");
269 make_observer(name, model);
270 }
271 "make:service" => make_service(name),
272 "make:seeder" => make_seeder(name),
273 "make:migration" => make_migration(name),
274 _ => {
275 println!("ā Subcommand tidak dikenal: {}", subcommand);
276 }
277 }
278}
279
280fn to_snake_case(s: &str) -> String {
282 let mut out = String::new();
283 for (i, c) in s.chars().enumerate() {
284 if c.is_uppercase() && i > 0 {
285 out.push('_');
286 }
287 out.push(c.to_ascii_lowercase());
288 }
289 out
290}
291
292fn write_file(path: &str, content: &str) {
294 let p = std::path::Path::new(path);
295 if let Some(parent) = p.parent() {
296 if let Err(e) = std::fs::create_dir_all(parent) {
297 println!("ā Gagal membuat direktori {}: {}", parent.display(), e);
298 return;
299 }
300 }
301 if p.exists() {
302 println!("ā ļø File sudah ada, dilewati: {}", path);
303 return;
304 }
305 if let Err(e) = std::fs::write(p, content) {
306 println!("ā Gagal membuat file {}: {}", path, e);
307 } else {
308 println!("ā
Dibuat: {}", path);
309 }
310}
311
312fn register_mod(mod_file: &str, module_name: &str) {
314 let line = format!("pub mod {};\n", module_name);
315 let content = std::fs::read_to_string(mod_file).unwrap_or_default();
316 if content.contains(&line) { return; }
317
318 let label = "// š LABEL: MODULE";
319 let new_content = if let Some(pos) = content.find(label) {
320 let nl = content[pos..].find('\n').map(|n| pos + n + 1).unwrap_or(content.len());
322 format!("{}{}{}", &content[..nl], &line, &content[nl..])
323 } else {
324 format!("{}{}", content, &line)
325 };
326
327 if let Err(e) = std::fs::write(mod_file, new_content) {
328 println!("ā ļø Gagal mendaftarkan modul di {}: {}", mod_file, e);
329 } else {
330 println!(" š Daftar di: {}", mod_file);
331 }
332}
333
334fn make_controller(name: &str) {
335 let snake = to_snake_case(name);
336 let file_name = if snake.ends_with("_controller") {
337 snake.clone()
338 } else {
339 format!("{}_controller", snake)
340 };
341 let struct_name = name.to_string();
342 let path = format!("src/app/http/controllers/{}.rs", file_name);
343
344 let content = format!(r#"use rustbasic_core::requests::Request;
345use rustbasic_core::IntoResponse;
346
347pub async fn index(_req: Request) -> impl IntoResponse {{
348 "Hello from {struct_name}Controller!"
349}}
350
351pub async fn show(_req: Request) -> impl IntoResponse {{
352 "Show action"
353}}
354
355pub async fn store(_req: Request) -> impl IntoResponse {{
356 "Store action"
357}}
358
359pub async fn update(_req: Request) -> impl IntoResponse {{
360 "Update action"
361}}
362
363pub async fn destroy(_req: Request) -> impl IntoResponse {{
364 "Destroy action"
365}}
366"#);
367
368 write_file(&path, &content);
369 register_mod("src/app/http/controllers/mod.rs", &file_name);
370}
371
372fn make_model(name: &str, with_migration: bool) {
373 let snake = to_snake_case(name);
374 let path = format!("src/app/models/{}.rs", snake);
375
376 let content = format!(r#"use rustbasic_core::model;
377
378model! {{
379 table: "{snake}s",
380 fillable: [],
381 Model {{
382 pub id: i32,
383 pub created_at: Option<String>,
384 pub updated_at: Option<String>,
385 }}
386}}
387
388impl Model {{
389 pub fn to_resource(&self) -> rustbasic_core::serde_json::Value {{
390 rustbasic_core::serde_json::json!({{
391 "id": self.id,
392 }})
393 }}
394}}
395"#);
396
397 write_file(&path, &content);
398 register_mod("src/app/models/mod.rs", &snake);
399
400 if with_migration {
401 make_migration(&format!("create_{}_table", snake));
402 }
403}
404
405fn make_middleware(name: &str) {
406 let snake = to_snake_case(name);
407 let path = format!("src/app/http/middleware/{}.rs", snake);
408
409 let content = format!(r#"use rustbasic_core::requests::Request;
410use rustbasic_core::router::{{Response, Next}};
411
412pub async fn {snake}_middleware(req: Request, next: Next) -> Response {{
413 // Logika middleware sebelum handler
414 let res = next.run(req).await;
415 // Logika middleware setelah handler
416 res
417}}
418"#);
419
420 write_file(&path, &content);
421 register_mod("src/app/http/middleware/mod.rs", &snake);
422}
423
424fn make_observer(name: &str, model: &str) {
425 let snake = to_snake_case(name);
426 let obs_name = if snake.ends_with("_observer") {
427 snake.clone()
428 } else {
429 format!("{}_observer", snake)
430 };
431 let path = format!("src/app/observers/{}.rs", obs_name);
432
433 let content = format!(r#"use crate::app::models::{snake}::Model as {model};
434
435pub struct {name}Observer;
436
437impl {name}Observer {{
438 pub async fn created(&self, model: &{model}) {{
439 // Dipanggil setelah model dibuat
440 let _ = model;
441 }}
442
443 pub async fn updated(&self, model: &{model}) {{
444 // Dipanggil setelah model diupdate
445 let _ = model;
446 }}
447
448 pub async fn deleted(&self, model: &{model}) {{
449 // Dipanggil setelah model dihapus
450 let _ = model;
451 }}
452}}
453"#);
454
455 write_file(&path, &content);
456}
457
458fn make_service(name: &str) {
459 let snake = to_snake_case(name);
460 let svc_name = if snake.ends_with("_service") {
461 snake.clone()
462 } else {
463 format!("{}_service", snake)
464 };
465 let path = format!("src/app/services/{}.rs", svc_name);
466
467 let content = format!(r#"use rustbasic_core::sql::AnyPool;
468
469pub struct {name}Service<'a> {{
470 pub db: &'a AnyPool,
471}}
472
473impl<'a> {name}Service<'a> {{
474 pub fn new(db: &'a AnyPool) -> Self {{
475 Self {{ db }}
476 }}
477
478 pub async fn execute(&self) -> Result<(), String> {{
479 // Implementasi logika bisnis di sini
480 Ok(())
481 }}
482}}
483"#);
484
485 write_file(&path, &content);
486}
487
488fn make_seeder(name: &str) {
489 let snake = to_snake_case(name);
490 let seed_name = if snake.ends_with("_seeder") {
491 snake.clone()
492 } else {
493 format!("{}_seeder", snake)
494 };
495 let path = format!("database/seeders/{}.rs", seed_name);
496
497 let content = format!(r#"use rustbasic_core::sql::AnyPool;
498
499pub async fn run(db: &AnyPool) {{
500 println!("š± Menjalankan {}...");
501 // Contoh:
502 // rustbasic_core::database::execute(db, "INSERT INTO ...", ()).await;
503}}
504"#, seed_name);
505
506 write_file(&path, &content);
507}
508
509fn make_migration(name: &str) {
510 use std::time::{SystemTime, UNIX_EPOCH};
511
512 let ts = SystemTime::now()
513 .duration_since(UNIX_EPOCH)
514 .map(|d| d.as_secs())
515 .unwrap_or(0);
516
517 let migration_name = format!("m{}_{}", ts, to_snake_case(name));
519 let path = format!("database/migrations/{}.rs", migration_name);
520
521 let content = format!(r#"use rustbasic_core::schema::{{Schema, MigrationTrait, DbErr}};
522use rustbasic_core::sql::AnyPool;
523use rustbasic_core::async_trait;
524
525pub struct {migration_name};
526
527#[async_trait]
528impl MigrationTrait for {migration_name} {{
529 async fn up(&self, db: &AnyPool) -> Result<(), DbErr> {{
530 Schema::create("{}", db, |t| {{
531 t.id();
532 t.timestamps();
533 }}).await
534 }}
535
536 async fn down(&self, db: &AnyPool) -> Result<(), DbErr> {{
537 Schema::drop_if_exists("{}", db).await
538 }}
539}}
540"#, to_snake_case(name), to_snake_case(name));
541
542 write_file(&path, &content);
543}
544
545fn handle_storage_link(cfg: &Config) {
549 let target = "public/storage";
550 let source = "storage/app/public";
551
552 if let Err(e) = std::fs::create_dir_all(source) {
553 println!("ā Gagal membuat direktori storage: {}", e);
554 return;
555 }
556
557 let path = std::path::Path::new(target);
558 if path.exists() || path.is_symlink() {
559 println!("ā¹ļø Link 'public/storage' sudah ada.");
560 return;
561 }
562
563 println!("š Membuat symbolic link...");
564
565 #[cfg(unix)]
566 {
567 use std::os::unix::fs::symlink;
568 if let Err(e) = symlink("../storage/app/public", target) {
569 println!("ā Gagal membuat symlink: {}", e);
570 } else {
571 println!("ā
Link storage berhasil! [public/storage -> storage/app/public]");
572 }
573 }
574
575 #[cfg(windows)]
576 {
577 use std::os::windows::fs::symlink_dir;
578 if let Err(e) = symlink_dir("../storage/app/public", target) {
579 println!("ā Gagal membuat symlink: {}", e);
580 } else {
581 println!("ā
Link storage berhasil! [public/storage -> storage/app/public]");
582 }
583 }
584
585 let _ = cfg;
586}
587
588fn run_native(run_android: bool, run_desktop: bool) {
592 if run_android {
593 println!("š Memulai RustBasic Android Wrapper (Native implementation)...");
594
595 let home = std::env::var("HOME").unwrap_or_default();
597 let android_home = std::env::var("ANDROID_HOME")
598 .unwrap_or_else(|_| format!("{}/Library/Android/sdk", home));
599 unsafe { std::env::set_var("ANDROID_HOME", &android_home); }
600
601 let mut custom_java_home: Option<String> = None;
602 if std::env::consts::OS == "macos" {
603 let paths = vec![
604 "/Applications/Android Studio.app/Contents/jbr/Contents/Home",
605 "/Applications/Android Studio.app/Contents/jre/Contents/Home",
606 "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home",
607 ];
608 for path in &paths {
609 if std::path::Path::new(path).exists() {
610 custom_java_home = Some(path.to_string());
611 break;
612 }
613 }
614 }
615
616 let mut devices = get_adb_devices();
617 if devices.is_empty() {
618 println!("š± Perangkat Android atau emulator tidak terdeteksi aktif.");
619 let emulator_bin = format!("{}/emulator/emulator", android_home);
620 if std::path::Path::new(&emulator_bin).exists() {
621 let avd_output = std::process::Command::new(&emulator_bin).arg("-list-avds").output();
622 if let Ok(avd_out) = avd_output {
623 let avds_str = String::from_utf8_lossy(&avd_out.stdout);
624 if let Some(avd_name) = avds_str.lines().next() {
625 println!("š Menyalakan emulator AVD: {}...", avd_name);
626 let _ = std::process::Command::new(&emulator_bin)
627 .args(["-avd", avd_name])
628 .stdout(std::process::Stdio::null())
629 .stderr(std::process::Stdio::null())
630 .spawn();
631
632 println!("ā³ Menunggu emulator menyala dan terdeteksi adb...");
633 let _ = std::process::Command::new("adb").arg("wait-for-device").status();
634 println!("ā
Emulator berhasil aktif!");
635 std::thread::sleep(std::time::Duration::from_secs(3));
636 devices = get_adb_devices();
637 }
638 }
639 }
640 }
641
642 let (device_id, device_name) = if devices.len() == 1 {
643 let d = devices[0].clone();
644 println!("š± Menggunakan perangkat tunggal: {} ({})", d.1, d.0);
645 d
646 } else if devices.len() > 1 {
647 println!("š± Terdeteksi beberapa perangkat Android. Silakan pilih target:");
648 for (idx, d) in devices.iter().enumerate() {
649 println!(" [{}] {} ({})", idx + 1, d.1, d.0);
650 }
651 let choice = prompt_choice("š Pilih nomor perangkat: ", 1, devices.len());
652 devices[choice - 1].clone()
653 } else {
654 println!("ā Error: Tidak ada perangkat Android terhubung.");
655 return;
656 };
657
658 if !compile_jni_libraries() {
660 return;
661 }
662
663 let local_props = std::path::Path::new("native/android/local.properties");
665 if !local_props.exists() {
666 if let Ok(mut file) = std::fs::File::create(local_props) {
667 use std::io::Write;
668 let _ = writeln!(file, "sdk.dir={}", android_home);
669 }
670 }
671
672 println!("šØ Membangun debug APK menggunakan Gradle...");
674 let gradlew_bin = if cfg!(target_os = "windows") { "gradlew.bat" } else { "./gradlew" };
675 let mut gradle_cmd = std::process::Command::new(gradlew_bin);
676 gradle_cmd.arg("assembleDebug");
677 gradle_cmd.current_dir("native/android");
678
679 if let Some(jh) = &custom_java_home {
680 gradle_cmd.env("JAVA_HOME", jh);
681 }
682
683 let gradle_status = gradle_cmd.status();
684 if gradle_status.is_err() || !gradle_status.unwrap().success() {
685 println!("ā Gradle build assembleDebug gagal.");
686 return;
687 }
688
689 println!("šØ Memasang APK ke perangkat {} ({})...", device_name, device_id);
691 let install_status = std::process::Command::new("adb")
692 .args(["-s", &device_id, "install", "-r", "native/android/app/build/outputs/apk/debug/app-debug.apk"])
693 .status();
694
695 if install_status.is_err() || !install_status.unwrap().success() {
696 println!("ā Gagal memasang APK ke device.");
697 return;
698 }
699
700 let vite_port = "5173"; let reverse_status = std::process::Command::new("adb")
703 .args(["-s", &device_id, "reverse", &format!("tcp:{}", vite_port), &format!("tcp:{}", vite_port)])
704 .status();
705 if reverse_status.is_err() {
706 println!("ā ļø Warning: Gagal melakukan adb reverse port {}", vite_port);
707 }
708
709 println!("š Membuka aplikasi di perangkat {}...", device_name);
711 let _ = std::process::Command::new("adb")
712 .args(["-s", &device_id, "logcat", "-c"])
713 .status();
714
715 let _ = std::process::Command::new("adb")
716 .args(["-s", &device_id, "shell", "am", "start", "-n", "com.rustbasic.mobile/com.rustbasic.mobile.MainActivity"])
717 .status();
718
719 println!("š Menampilkan log realtime dari perangkat {} (Tekan Ctrl+C untuk keluar)...", device_name);
720 let mut logcat_cmd = std::process::Command::new("adb");
721 logcat_cmd.args(["-s", &device_id, "logcat", "-s", "RustBasicServer"]);
722 let mut child = logcat_cmd.spawn().expect("Gagal menjalankan adb logcat");
723 let _ = child.wait();
724 } else if run_desktop {
725 println!("š Memulai RustBasic Desktop Wrapper...");
726 let mut cmd = std::process::Command::new("cargo");
727 cmd.args(["run", "--bin", "rustbasic-desktop"]);
728 let status = cmd.status();
729 match status {
730 Ok(s) if s.success() => {}
731 _ => {
732 println!("ā Gagal menjalankan Desktop Wrapper.");
733 }
734 }
735 }
736}
737
738fn get_adb_devices() -> Vec<(String, String)> {
739 let output = std::process::Command::new("adb").arg("devices").output();
740 let mut devices = Vec::new();
741 if let Ok(out) = output {
742 let stdout = String::from_utf8_lossy(&out.stdout);
743 for line in stdout.lines() {
744 if line.contains("device") && !line.contains("List of devices") {
745 let parts: Vec<&str> = line.split_whitespace().collect();
746 if parts.is_empty() { continue; }
747 let device_id = parts[0].to_string();
748 let model_out = std::process::Command::new("adb")
749 .args(["-s", &device_id, "shell", "getprop", "ro.product.model"])
750 .output();
751 let model = if let Ok(m_out) = model_out {
752 String::from_utf8_lossy(&m_out.stdout).trim().to_string()
753 } else {
754 "Unknown Device".to_string()
755 };
756 devices.push((device_id, model));
757 }
758 }
759 }
760 devices
761}
762
763fn compile_jni_libraries() -> bool {
764 println!("š Building Rust library for Android (Native Rust implementation)...");
765
766 let _ = std::process::Command::new("rustup")
767 .args(["target", "add", "aarch64-linux-android", "armv7-linux-androideabi", "x86_64-linux-android"])
768 .status();
769
770 let home = std::env::var("HOME").unwrap_or_default();
771 let android_ndk_home = if let Ok(val) = std::env::var("ANDROID_NDK_HOME") {
772 val
773 } else {
774 let mac_ndk = format!("{}/Library/Android/sdk/ndk", home);
775 if std::path::Path::new(&mac_ndk).exists() {
776 if let Ok(entries) = std::fs::read_dir(&mac_ndk) {
777 let mut paths: Vec<_> = entries.flatten().map(|e| e.path()).collect();
778 paths.sort();
779 if let Some(highest) = paths.last() {
780 highest.display().to_string()
781 } else {
782 "".to_string()
783 }
784 } else {
785 "".to_string()
786 }
787 } else {
788 "".to_string()
789 }
790 };
791
792 if android_ndk_home.is_empty() {
793 println!("ā Error: ANDROID_NDK_HOME is not set. Please set ANDROID_NDK_HOME.");
794 return false;
795 }
796
797 println!("Using NDK: {}", android_ndk_home);
798
799 let os = std::env::consts::OS;
800 let toolchain_sub = if os == "macos" { "darwin-x86_64" } else { "linux-x86_64" };
801 let toolchain_bin_path = std::path::Path::new(&android_ndk_home)
802 .join("toolchains/llvm/prebuilt")
803 .join(toolchain_sub)
804 .join("bin");
805
806 if !toolchain_bin_path.exists() {
807 println!("ā Error: Toolchain bin path not found: {}", toolchain_bin_path.display());
808 return false;
809 }
810
811 let sqlite_version = "3450100";
812 let sqlite_dir = format!("target/sqlite-amalgamation-{}", sqlite_version);
813 if !std::path::Path::new(&sqlite_dir).exists() {
814 println!("š„ Downloading SQLite source amalgamation...");
815 std::fs::create_dir_all("target").ok();
816
817 let zip_path = "target/sqlite.zip";
818 let sqlite_url = format!("https://www.sqlite.org/2024/sqlite-amalgamation-{}.zip", sqlite_version);
819
820 let curl_status = std::process::Command::new("curl")
821 .args(["-sSLo", zip_path, &sqlite_url])
822 .status();
823
824 if curl_status.is_err() || !curl_status.unwrap().success() {
825 println!("ā Gagal men-download SQLite source.");
826 return false;
827 }
828
829 let unzip_status = std::process::Command::new("unzip")
830 .args(["-q", zip_path, "-d", "target/"])
831 .status();
832
833 let _ = std::fs::remove_file(zip_path);
834
835 if unzip_status.is_err() || !unzip_status.unwrap().success() {
836 println!("ā Gagal mengekstrak SQLite source.");
837 return false;
838 }
839 }
840
841 let targets = vec![
842 ("aarch64-linux-android", "arm64-v8a", "aarch64-linux-android21-clang"),
843 ("armv7-linux-androideabi", "armeabi-v7a", "armv7a-linux-androideabi21-clang"),
844 ("x86_64-linux-android", "x86_64", "x86_64-linux-android21-clang"),
845 ];
846
847 let jnilibs_dir = "native/android/app/src/main/jniLibs";
848
849 for (target, arch, clang_name) in targets {
850 println!("šØ Preparing SQLite static library for {}...", target);
851
852 let clang_path = toolchain_bin_path.join(clang_name);
853 let ar_path = toolchain_bin_path.join("llvm-ar");
854
855 if !clang_path.exists() {
856 println!("ā Error: Compiler not found: {}", clang_path.display());
857 return false;
858 }
859
860 let sqlite_out = format!("target/{}/sqlite", target);
861 std::fs::create_dir_all(&sqlite_out).ok();
862
863 let libsqlite3_a = format!("{}/libsqlite3.a", sqlite_out);
864 if !std::path::Path::new(&libsqlite3_a).exists() {
865 println!(" Compiling SQLite static lib for {}...", target);
866 let sqlite3_o = format!("{}/sqlite3.o", sqlite_out);
867 let sqlite3_c = format!("{}/sqlite3.c", sqlite_dir);
868
869 let compile_status = std::process::Command::new(&clang_path)
870 .args(["-O2", "-c", &sqlite3_c, "-o", &sqlite3_o])
871 .status();
872
873 if compile_status.is_err() || !compile_status.unwrap().success() {
874 println!("ā Gagal mengompilasi sqlite3.o");
875 return false;
876 }
877
878 let archive_status = std::process::Command::new(&ar_path)
879 .args(["rcs", &libsqlite3_a, &sqlite3_o])
880 .status();
881
882 if archive_status.is_err() || !archive_status.unwrap().success() {
883 println!("ā Gagal mengarsip libsqlite3.a");
884 return false;
885 }
886 }
887
888 println!("šØ Compiling Rust library for {}...", target);
889 let mut cargo_cmd = std::process::Command::new("cargo");
890 cargo_cmd.args(["build", "--target", target, "--release"]);
891
892 let clang_path_str = clang_path.display().to_string();
893 let ar_path_str = ar_path.display().to_string();
894
895 let target_upper = target.replace("-", "_").to_uppercase();
896 let linker_env = format!("CARGO_TARGET_{}_LINKER", target_upper);
897 let cc_env = format!("CC_{}", target.replace("-", "_"));
898 let ar_env = format!("AR_{}", target.replace("-", "_"));
899
900 cargo_cmd.env(&linker_env, &clang_path_str);
901 cargo_cmd.env(&cc_env, &clang_path_str);
902 cargo_cmd.env(&ar_env, &ar_path_str);
903
904 let cargo_status = cargo_cmd.status();
905 if cargo_status.is_err() || !cargo_status.unwrap().success() {
906 println!("ā Gagal mengompilasi library Rust untuk target {}", target);
907 return false;
908 }
909
910 let dest_dir = format!("{}/{}", jnilibs_dir, arch);
911 std::fs::create_dir_all(&dest_dir).ok();
912
913 let src_so = format!("target/{}/release/librustbasic.so", target);
914 let dest_so = format!("{}/librustbasic_mobile.so", dest_dir);
915
916 if let Err(e) = std::fs::copy(&src_so, &dest_so) {
917 println!("ā Gagal menyalin {}: {}", src_so, e);
918 return false;
919 }
920 }
921
922 println!("ā
Android JNI libraries built successfully!");
923 true
924}
925
926fn prompt_choice(prompt: &str, min: usize, max: usize) -> usize {
930 use std::io::{self, Write};
931 loop {
932 print!("{}", prompt);
933 let _ = io::stdout().flush();
934 let mut input = String::new();
935 if io::stdin().read_line(&mut input).is_ok() {
936 if let Ok(choice) = input.trim().parse::<usize>() {
937 if choice >= min && choice <= max {
938 return choice;
939 }
940 }
941 }
942 println!("ā ļø Pilihan tidak valid, silakan coba lagi.");
943 }
944}
945
946pub async fn handle_build(args: &[String]) {
947 let mut build_docker = args.iter().any(|a| a == "--docker");
948 let mut build_desktop = args.iter().any(|a| a == "--desktop");
949 let mut build_android = args.iter().any(|a| a == "--android");
950 let release_mode = args.iter().any(|a| a == "--release" || a == "-r");
951 let mut target_type = String::new();
952 let mut docker_tag = String::new();
953 let mut docker_platform = String::new();
954
955 for i in 0..args.len() {
956 if args[i] == "--type" && i + 1 < args.len() { target_type = args[i+1].to_lowercase(); }
957 if args[i] == "--tag" && i + 1 < args.len() { docker_tag = args[i+1].clone(); }
958 if args[i] == "--platform" && i + 1 < args.len() { docker_platform = args[i+1].clone(); }
959 }
960
961 if !build_docker && !build_desktop && !build_android {
962 let is_native_installed = if let Ok(content) = std::fs::read_to_string(".rustbasic_packages.json") {
963 content.contains("\"rustbasic-native\"")
964 } else {
965 false
966 };
967
968 println!("š ļø RustBasic Build CLI");
969 println!("Pilih platform target untuk di-build:");
970 println!(" [1] Docker (Container Image)");
971
972 let max_choice = if is_native_installed {
973 println!(" [2] Desktop Wrapper (Windows, macOS, Linux)");
974 println!(" [3] Android Wrapper (APK, AAB)");
975 3
976 } else {
977 1
978 };
979
980 let prompt_str = format!("š Pilih nomor platform (1-{}): ", max_choice);
981 match prompt_choice(&prompt_str, 1, max_choice) {
982 1 => build_docker = true,
983 2 => build_desktop = true,
984 3 => build_android = true,
985 _ => {}
986 }
987 }
988
989 if build_docker {
990 if docker_platform.is_empty() {
991 println!("\nPilih Platform Target CPU Docker:");
992 println!(" [1] Current Host Platform (Sesuai OS komputer Anda)");
993 println!(" [2] Linux AMD64 / x86_64 (Standard VPS Intel/AMD - Umum/Rekomendasi)");
994 println!(" [3] Linux ARM64 / aarch64 (Server berbasis ARM / AWS Graviton)");
995 match prompt_choice("š Pilih (1-3): ", 1, 3) {
996 2 => docker_platform = "linux/amd64".to_string(),
997 3 => docker_platform = "linux/arm64".to_string(),
998 _ => {}
999 }
1000 }
1001 build_docker_image(&docker_tag, &docker_platform).await;
1002 } else if build_desktop {
1003 build_desktop_binary(args, release_mode).await;
1004 } else if build_android {
1005 build_android_apk(args, &target_type, release_mode).await;
1006 }
1007}
1008
1009async fn build_docker_image(custom_tag: &str, platform: &str) {
1010 if !std::process::Command::new("docker").arg("version")
1012 .stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null())
1013 .status().map(|s| s.success()).unwrap_or(false)
1014 {
1015 println!("ā Docker tidak ditemukan. Install: https://docs.docker.com/get-docker/");
1016 return;
1017 }
1018
1019 let app_name = std::env::var("APP_NAME").unwrap_or_else(|_| "rustbasic".to_string()).to_lowercase();
1020 let image_tag = if custom_tag.is_empty() { format!("{}:latest", app_name) } else { custom_tag.to_string() };
1021
1022 if std::path::Path::new("package.json").exists() {
1023 println!("š¦ Mengompilasi frontend assets (npm run build)...");
1024 let npm_cmd = if cfg!(windows) { "npm.cmd" } else { "npm" };
1025 let success = match std::process::Command::new(npm_cmd)
1026 .args(&["run", "build"])
1027 .status()
1028 {
1029 Ok(status) => status.success(),
1030 Err(_) => {
1031 println!("ā Gagal menjalankan npm. Pastikan Node.js & npm terinstal di sistem Anda.");
1032 return;
1033 }
1034 };
1035
1036 if !success {
1037 println!("ā Gagal mengompilasi frontend assets. Silakan periksa error di atas.");
1038 return;
1039 }
1040 }
1041
1042 println!("\nš³ Docker build dimulai...");
1043 println!(" Image: {}", image_tag);
1044
1045 let core_context = if std::path::Path::new("../rustbasic-core").exists() {
1046 "core=../rustbasic-core".to_string()
1047 } else {
1048 "core=.".to_string()
1049 };
1050
1051 let mut build_args = vec![
1052 "build".to_string(),
1053 "--build-context".to_string(),
1054 core_context,
1055 ];
1056
1057 if !platform.is_empty() {
1058 build_args.push("--platform".to_string());
1059 build_args.push(platform.to_string());
1060 println!(" Platform: {}", platform);
1061 }
1062
1063 build_args.push("-t".to_string());
1064 build_args.push(image_tag.clone());
1065 build_args.push(".".to_string());
1066
1067 println!(" Running: docker {}", build_args.join(" "));
1068
1069 let mut cmd = std::process::Command::new("docker")
1070 .args(&build_args)
1071 .stdin(std::process::Stdio::inherit())
1072 .stdout(std::process::Stdio::inherit())
1073 .stderr(std::process::Stdio::inherit())
1074 .spawn().expect("Gagal menjalankan docker build");
1075
1076 let success = cmd.wait().map(|s| s.success()).unwrap_or(false);
1077
1078 if success {
1079 println!("\nā
Docker build selesai! Image: {}", image_tag);
1080 println!(" Jalankan container (Lokal/Development):");
1081 if !platform.is_empty() {
1082 println!(" docker run --platform {} -p 4000:4000 --env-file .env {}", platform, image_tag);
1083 } else {
1084 println!(" docker run -p 4000:4000 --env-file .env {}", image_tag);
1085 }
1086 println!(" Jalankan container (Produksi/Server - Auto Restart):");
1087 if !platform.is_empty() {
1088 println!(" docker run --platform {} -d -p 80:4000 --restart unless-stopped --env-file .env {}", platform, image_tag);
1089 } else {
1090 println!(" docker run -d -p 80:4000 --restart unless-stopped --env-file .env {}", image_tag);
1091 }
1092 } else {
1093 println!("\nā Docker build gagal.");
1094 }
1095}
1096
1097async fn build_desktop_binary(args: &[String], mut release_mode: bool) {
1098 let mut target_triple = "";
1099
1100 for i in 0..args.len() {
1101 if args[i] == "--os" && i + 1 < args.len() {
1102 target_triple = match args[i+1].as_str() {
1103 "macos-intel" => "x86_64-apple-darwin",
1104 "macos-silicon" => "aarch64-apple-darwin",
1105 "windows" => "x86_64-pc-windows-msvc",
1106 "linux" => "x86_64-unknown-linux-gnu",
1107 _ => "",
1108 };
1109 }
1110 }
1111
1112 if target_triple.is_empty() && !args.iter().any(|a| a.starts_with("--os")) {
1113 println!("\nPilih OS Target Desktop:");
1114 println!(" [1] Current OS\n [2] macOS Intel\n [3] macOS Apple Silicon\n [4] Windows\n [5] Linux");
1115 match prompt_choice("š Pilih (1-5): ", 1, 5) {
1116 2 => target_triple = "x86_64-apple-darwin",
1117 3 => target_triple = "aarch64-apple-darwin",
1118 4 => target_triple = "x86_64-pc-windows-msvc",
1119 5 => target_triple = "x86_64-unknown-linux-gnu",
1120 _ => {}
1121 }
1122 }
1123
1124 if !args.iter().any(|a| a == "--release" || a == "-r" || a == "--debug" || a == "-d") {
1125 println!("\n [1] Debug\n [2] Release");
1126 if prompt_choice("š Mode (1-2): ", 1, 2) == 2 { release_mode = true; }
1127 }
1128
1129 let mut build_args = vec!["build", "--bin", "rustbasic-desktop"];
1130 if release_mode { build_args.push("--release"); }
1131 if !target_triple.is_empty() {
1132 build_args.push("--target");
1133 build_args.push(target_triple);
1134 let _ = std::process::Command::new("rustup").args(["target", "add", target_triple]).status();
1135 }
1136
1137 println!("\nš„ļø Desktop build: cargo {}", build_args.join(" "));
1138 let mut cmd = std::process::Command::new("cargo")
1139 .args(&build_args)
1140 .stdin(std::process::Stdio::inherit())
1141 .stdout(std::process::Stdio::inherit())
1142 .stderr(std::process::Stdio::inherit())
1143 .spawn().expect("Gagal menjalankan cargo build");
1144
1145 if cmd.wait().map(|s| s.success()).unwrap_or(false) {
1146 println!("\nā
Desktop build selesai!");
1147 } else {
1148 println!("\nā Desktop build gagal.");
1149 }
1150}
1151
1152async fn build_android_apk(args: &[String], target_type: &str, mut release_mode: bool) {
1153 let is_aab = if target_type.is_empty() {
1154 println!("\n [1] APK\n [2] AAB (Google Play)");
1155 prompt_choice("š Format (1-2): ", 1, 2) == 2
1156 } else {
1157 target_type == "aab"
1158 };
1159
1160 if !args.iter().any(|a| a == "--release" || a == "-r" || a == "--debug" || a == "-d") {
1161 println!("\n [1] Debug\n [2] Release");
1162 if prompt_choice("š Mode (1-2): ", 1, 2) == 2 { release_mode = true; }
1163 }
1164
1165 println!("\nšØ Membangun JNI library (Native implementation)...");
1167 if !compile_jni_libraries() {
1168 println!("ā JNI build gagal.");
1169 return;
1170 }
1171
1172 let home = std::env::var("HOME").unwrap_or_default();
1174 let android_home = std::env::var("ANDROID_HOME")
1175 .unwrap_or_else(|_| format!("{}/Library/Android/sdk", home));
1176 unsafe { std::env::set_var("ANDROID_HOME", &android_home); }
1177
1178 let gradle_task = match (is_aab, release_mode) {
1179 (false, false) => "assembleDebug",
1180 (false, true) => "assembleRelease",
1181 (true, false) => "bundleDebug",
1182 (true, true) => "bundleRelease",
1183 };
1184
1185 println!("\nšØ Gradle task: {}", gradle_task);
1186 let gradlew = if std::path::Path::new("native/android/gradlew").exists() { "./gradlew" } else { "gradle" };
1187 let mut cmd = std::process::Command::new(gradlew)
1188 .arg(gradle_task)
1189 .current_dir("native/android")
1190 .stdin(std::process::Stdio::inherit()).stdout(std::process::Stdio::inherit()).stderr(std::process::Stdio::inherit())
1191 .spawn().expect("Gagal menjalankan Gradle");
1192
1193 if cmd.wait().map(|s| s.success()).unwrap_or(false) {
1194 println!("\nā
Android build selesai!");
1195 } else {
1196 println!("\nā Android build gagal.");
1197 }
1198}
1199
1200fn prompt_string(prompt: &str, default: &str) -> String {
1201 use std::io::Write;
1202 print!("{}", prompt);
1203 let _ = std::io::stdout().flush();
1204 let mut input = String::new();
1205 if std::io::stdin().read_line(&mut input).is_ok() {
1206 let trimmed = input.trim();
1207 if trimmed.is_empty() {
1208 default.to_string()
1209 } else {
1210 trimmed.to_string()
1211 }
1212 } else {
1213 default.to_string()
1214 }
1215}
1216
1217pub async fn handle_deploy() {
1218 println!("\n{}", "š RustBasic Docker Deploy CLI".magenta().bold());
1219 println!("{}", "------------------------------".magenta());
1220
1221 let image_name = prompt_string("š Masukkan Nama/Tag Docker Image (default: rustbasic:latest): ", "rustbasic:latest");
1223
1224 let inspect = std::process::Command::new("docker")
1226 .args(["image", "inspect", &image_name])
1227 .stdout(std::process::Stdio::null())
1228 .stderr(std::process::Stdio::null())
1229 .status();
1230
1231 match inspect {
1232 Ok(status) if status.success() => {}
1233 _ => {
1234 println!("{}", format!("ā ļø Peringatan: Image '{}' tidak ditemukan lokal.", image_name).yellow());
1235 let proceed = prompt_string("š Tetap lanjutkan proses ekspor? (y/n) [default: n]: ", "n");
1236 if proceed.to_lowercase() != "y" {
1237 println!("ā Proses dihentikan.");
1238 return;
1239 }
1240 }
1241 }
1242
1243 println!("š¦ Mengekspor Docker image '{}' ke 'rustbasic.tar'...", image_name);
1245 let save_status = std::process::Command::new("docker")
1246 .args(["save", "-o", "rustbasic.tar", &image_name])
1247 .status();
1248
1249 match save_status {
1250 Ok(status) if status.success() => {
1251 println!("{}", "ā
Image berhasil diekspor ke rustbasic.tar.".green());
1252 }
1253 _ => {
1254 println!("{}", "ā Gagal mengekspor Docker image.".red().bold());
1255 return;
1256 }
1257 }
1258
1259 println!("\n{}", "š Konfigurasi Pengiriman ke Server".cyan().bold());
1261 println!("{}", "-----------------------------------".cyan());
1262 let ssh_user = prompt_string("š Masukkan SSH Username Server (contoh: root) [default: root]: ", "root");
1263 let ssh_ip = prompt_string("š Masukkan IP Address Server: ", "");
1264 if ssh_ip.is_empty() {
1265 println!("{}", "ā IP Address server tidak boleh kosong.".red().bold());
1266 let _ = std::fs::remove_file("rustbasic.tar");
1267 return;
1268 }
1269 let ssh_port = prompt_string("š Masukkan SSH Port Server (default: 22): ", "22");
1270 let dest_dir = prompt_string("š Masukkan Folder Tujuan di Server (default: ~/app): ", "~/app");
1271 let server_port = prompt_string("š Masukkan Port Server Mapping (contoh: 80:4000) [default: 80:4000]: ", "80:4000");
1272 let env_file = prompt_string("š Masukkan File Env yang akan dikirim (default: .env): ", ".env");
1273
1274 println!("\nš Menyiapkan folder tujuan di server...");
1275 let mkdir_status = std::process::Command::new("ssh")
1276 .args([
1277 "-p", &ssh_port,
1278 &format!("{}@{}", ssh_user, ssh_ip),
1279 &format!("mkdir -p {}", dest_dir)
1280 ])
1281 .status();
1282
1283 match mkdir_status {
1284 Ok(status) if status.success() => {}
1285 _ => {
1286 println!("{}", "ā Gagal terhubung ke server menggunakan SSH.".red().bold());
1287 let _ = std::fs::remove_file("rustbasic.tar");
1288 return;
1289 }
1290 }
1291
1292 println!("š Mengirimkan berkas rustbasic.tar & {} ke server...", env_file);
1293 let scp_status = std::process::Command::new("scp")
1294 .args([
1295 "-P", &ssh_port,
1296 "rustbasic.tar", &env_file,
1297 &format!("{}@{}:{}", ssh_user, ssh_ip, dest_dir)
1298 ])
1299 .status();
1300
1301 let _ = std::fs::remove_file("rustbasic.tar");
1303
1304 if let Ok(status) = scp_status {
1305 if !status.success() {
1306 println!("{}", "ā Gagal mengirimkan berkas via SCP.".red().bold());
1307 return;
1308 }
1309 } else {
1310 println!("{}", "ā Gagal menjalankan SCP.".red().bold());
1311 return;
1312 }
1313
1314 println!("{}", "ā
Pengiriman berkas berhasil!".green());
1315
1316 let auto_run = prompt_string("\nš Apakah Anda ingin langsung menjalankan container di server secara otomatis? (y/n) [default: y]: ", "y");
1318 if auto_run.to_lowercase() == "y" || auto_run.is_empty() {
1319 println!("\nš Memuat image di server (docker load)...");
1320 let load_status = std::process::Command::new("ssh")
1321 .args([
1322 "-p", &ssh_port,
1323 &format!("{}@{}", ssh_user, ssh_ip),
1324 &format!("docker load -i {}/rustbasic.tar", dest_dir)
1325 ])
1326 .status();
1327
1328 match load_status {
1329 Ok(status) if status.success() => {
1330 println!("{}", "ā
Image berhasil dimuat di server.".green());
1331 }
1332 _ => {
1333 println!("{}", "ā Gagal memuat image di server.".red().bold());
1334 return;
1335 }
1336 }
1337
1338 println!("š Menghentikan & menghapus container lama 'rustbasic-app' jika ada...");
1339 let stop_status = std::process::Command::new("ssh")
1340 .args([
1341 "-p", &ssh_port,
1342 &format!("{}@{}", ssh_user, ssh_ip),
1343 "docker stop rustbasic-app || true && docker rm rustbasic-app || true"
1344 ])
1345 .status();
1346
1347 if let Err(e) = stop_status {
1348 println!("ā ļø Peringatan saat membersihkan container lama: {}", e);
1349 }
1350
1351 println!("š Menjalankan container baru 'rustbasic-app'...");
1352 let run_cmd = format!(
1353 "docker run -d --name rustbasic-app -p {} --restart unless-stopped --env-file {}/.env {}",
1354 server_port, dest_dir, image_name
1355 );
1356 let run_status = std::process::Command::new("ssh")
1357 .args([
1358 "-p", &ssh_port,
1359 &format!("{}@{}", ssh_user, ssh_ip),
1360 &run_cmd
1361 ])
1362 .status();
1363
1364 match run_status {
1365 Ok(status) if status.success() => {
1366 println!("{}", "š Container 'rustbasic-app' berhasil dijalankan di server!".green().bold());
1367 }
1368 _ => {
1369 println!("{}", "ā Gagal menjalankan container di server.".red().bold());
1370 return;
1371 }
1372 }
1373
1374 println!("š Membersihkan file tar di server...");
1375 let rm_status = std::process::Command::new("ssh")
1376 .args([
1377 "-p", &ssh_port,
1378 &format!("{}@{}", ssh_user, ssh_ip),
1379 &format!("rm {}/rustbasic.tar", dest_dir)
1380 ])
1381 .status();
1382
1383 if let Err(e) = rm_status {
1384 println!("ā ļø Peringatan saat membersihkan file tar di server: {}", e);
1385 }
1386
1387 println!("\n{}", "š Deployment selesai!".green().bold());
1388 println!("{}", "--------------------------------------------------------".green());
1389 println!("Untuk melihat log aplikasi di server, jalankan:");
1390 println!("ssh -p {} {}@{} \"docker logs -f rustbasic-app\"", ssh_port, ssh_user, ssh_ip);
1391 println!("{}", "--------------------------------------------------------".green());
1392 } else {
1393 println!("\n{}", "š„ļø Langkah Selanjutnya di Server Anda:".cyan().bold());
1394 println!("{}", "--------------------------------------------------------".green());
1395 println!("1. Hubungkan ke server via SSH:");
1396 println!(" ssh -p {} {}@{}", ssh_port, ssh_user, ssh_ip);
1397 println!("");
1398 println!("2. Masuk ke folder tujuan:");
1399 println!(" cd {}", dest_dir);
1400 println!("");
1401 println!("3. Muat (load) image Docker dari berkas tar:");
1402 println!(" docker load -i rustbasic.tar");
1403 println!("");
1404 println!("4. Jalankan container dengan fitur auto-restart:");
1405 println!(" docker run -d --name rustbasic-app -p {} --restart unless-stopped --env-file .env {}", server_port, image_name);
1406 println!("");
1407 println!("5. Hapus file tar di server untuk menghemat disk:");
1408 println!(" rm rustbasic.tar");
1409 println!("{}", "--------------------------------------------------------".green());
1410 }
1411}
1412
1413fn handle_publish(target: &str) {
1417 let mut selected_target = target.to_string();
1418 if selected_target.is_empty() {
1419 println!("š ļø RustBasic Configuration Publisher");
1420 println!("Pilih konfigurasi yang ingin dipublikasikan ke proyek Anda:");
1421 println!(" [1] CORS (Cross-Origin Resource Sharing)");
1422 println!(" [2] CSRF (Cross-Site Request Forgery)");
1423 println!(" [3] APP (Application settings & Storage path overrides)");
1424 let choice = prompt_choice("š Pilih nomor konfigurasi (1-3): ", 1, 3);
1425 match choice {
1426 1 => selected_target = "cors".to_string(),
1427 2 => selected_target = "csrf".to_string(),
1428 3 => selected_target = "app".to_string(),
1429 _ => return,
1430 }
1431 }
1432
1433 match selected_target.as_str() {
1434 "cors" => {
1435 let path = std::path::Path::new("src/config/cors.rs");
1436 if path.exists() {
1437 println!("ā¹ļø File cors.rs sudah ada di src/config/cors.rs");
1438 return;
1439 }
1440 if let Some(parent) = path.parent() {
1441 let _ = std::fs::create_dir_all(parent);
1442 }
1443 let content = r#"/* ---------------------------------------------------------
1444 * š LABEL: CORS CONFIGURATION (src/config/cors.rs)
1445 * Berkas konfigurasi tambahan untuk kustomisasi CORS.
1446 * --------------------------------------------------------- */
1447
1448pub struct CorsConfig {
1449 pub allowed_origins: Vec<&'static str>,
1450 pub allowed_methods: Vec<&'static str>,
1451 pub allowed_headers: Vec<&'static str>,
1452}
1453
1454impl Default for CorsConfig {
1455 fn default() -> Self {
1456 Self {
1457 allowed_origins: vec!["*"],
1458 allowed_methods: vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"],
1459 allowed_headers: vec!["*"],
1460 }
1461 }
1462}
1463"#;
1464 if std::fs::write(path, content).is_ok() {
1465 println!("{} {}", "ā
Berhasil mempublikasikan konfigurasi CORS ke:".green().bold(), path.display().to_string().cyan());
1466 println!("š” File ini sekarang dapat diimpor untuk menyesuaikan aturan CORS lokal.");
1467 } else {
1468 println!("ā Gagal menulis file CORS config.");
1469 }
1470 }
1471 "csrf" => {
1472 let path = std::path::Path::new("src/config/csrf.rs");
1473 if path.exists() {
1474 println!("ā¹ļø File csrf.rs sudah ada di src/config/csrf.rs");
1475 return;
1476 }
1477 if let Some(parent) = path.parent() {
1478 let _ = std::fs::create_dir_all(parent);
1479 }
1480 let content = r#"/* ---------------------------------------------------------
1481 * š LABEL: CSRF CONFIGURATION (src/config/csrf.rs)
1482 * Berkas konfigurasi tambahan untuk perlindungan CSRF.
1483 * --------------------------------------------------------- */
1484
1485pub struct CsrfConfig {
1486 pub except_paths: Vec<&'static str>,
1487}
1488
1489impl Default for CsrfConfig {
1490 fn default() -> Self {
1491 Self {
1492 except_paths: vec![], // Masukkan rute yang dikecualikan dari CSRF di sini
1493 }
1494 }
1495}
1496"#;
1497 if std::fs::write(path, content).is_ok() {
1498 println!("{} {}", "ā
Berhasil mempublikasikan konfigurasi CSRF ke:".green().bold(), path.display().to_string().cyan());
1499 println!("š” File ini sekarang dapat diimpor untuk mengecualikan rute tertentu dari CSRF.");
1500 } else {
1501 println!("ā Gagal menulis file CSRF config.");
1502 }
1503 }
1504 "app" => {
1505 let path = std::path::Path::new("src/config/app.rs");
1506 if path.exists() {
1507 println!("ā¹ļø File app.rs sudah ada di src/config/app.rs");
1508 return;
1509 }
1510 if let Some(parent) = path.parent() {
1511 let _ = std::fs::create_dir_all(parent);
1512 }
1513 let content = r#"/* ---------------------------------------------------------
1514 * š LABEL: APP CONFIGURATION (src/config/app.rs)
1515 * Berkas konfigurasi tambahan untuk kustomisasi lokasi storage lokal.
1516 * --------------------------------------------------------- */
1517
1518pub const STORAGE_TARGET: &str = "public/storage";
1519pub const STORAGE_SOURCE: &str = "storage/app/public";
1520"#;
1521 if std::fs::write(path, content).is_ok() {
1522 println!("{} {}", "ā
Berhasil mempublikasikan konfigurasi APP ke:".green().bold(), path.display().to_string().cyan());
1523 } else {
1524 println!("ā Gagal menulis file APP config.");
1525 }
1526 }
1527 _ => {
1528 println!("ā Target '{}' tidak dikenal untuk di-publish.", selected_target);
1529 println!("š” Target yang didukung: cors, csrf, app");
1530 }
1531 }
1532}
1533