Skip to main content

rustbasic_core/
cli.rs

1use crate::app::Config;
2use crate::schema::MigratorTrait;
3use crate::seeder::SeederTrait;
4use crate::colored::Colorize;
5
6/// Entry point CLI utama — dipanggil oleh project's cli.rs
7///
8/// M = MigratorTrait  (dari project/database/migrations)
9/// S = SeederTrait    (dari project/database/seeders)
10pub 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    // server --android / --desktop → jalankan native runner
35    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; // fall through ke standard web server
43    }
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    // Perintah database — perlu koneksi pool
90    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
138// ============================================================
139// KEY:GENERATE
140// ============================================================
141fn handle_key_generate() {
142    let key = crate::rand::random_alphanumeric(32);
143    let encoded = crate::base64::encode(key.as_bytes());
144
145    // Baca .env, update APP_KEY, tulis balik
146    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            // pastikan ada newline di akhir
161            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
180// ============================================================
181// ROUTE:LIST
182// ============================================================
183fn handle_route_list() {
184    // Baca route definitions dari src/routes/ secara statis (parse file)
185    let route_files = ["src/routes/web.rs", "src/routes/api.rs"];
186    let mut routes: Vec<(String, String, String)> = Vec::new(); // (method, path, handler)
187
188    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                // Match pola: .route("/path", get(handler)).name("x")
193                // atau: .route("/path", post(handler))
194                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    // Header tabel
208    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    // Cari path string (content antara tanda kutip pertama)
220    let methods = ["get", "post", "put", "patch", "delete"];
221    
222    // Ekstrak path dari .route("path", ...)
223    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    // Ekstrak method dan handler
231    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
246// ============================================================
247// MAKE:* GENERATOR
248// ============================================================
249fn handle_make(args: &[String]) {
250    let subcommand = args[1].as_str(); // e.g. "make:controller"
251    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
280/// Konversi PascalCase → snake_case
281fn 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
292/// Tulis file — buat folder parent jika belum ada
293fn 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
312/// Tambahkan baris `pub mod <name>;` ke mod.rs jika belum ada
313fn 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        // Sisipkan setelah baris label
321        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    // Format timestamp: m{ts}_<name>
518    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
545// ============================================================
546// STORAGE:LINK
547// ============================================================
548fn 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
588// ============================================================
589// RUN NATIVE (server --android/--desktop)
590// ============================================================
591fn setup_java_home() {
592    if std::env::var("JAVA_HOME").is_err() {
593        let mut custom_java_home: Option<String> = None;
594        let os = std::env::consts::OS;
595        if os == "macos" {
596            let paths = vec![
597                "/Applications/Android Studio.app/Contents/jbr/Contents/Home",
598                "/Applications/Android Studio.app/Contents/jre/Contents/Home",
599                "/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home",
600            ];
601            for path in &paths {
602                if std::path::Path::new(path).exists() {
603                    custom_java_home = Some(path.to_string());
604                    break;
605                }
606            }
607        } else if os == "windows" {
608            let win_paths = [
609                "C:\\Program Files\\Android\\Android Studio\\jbr",
610                "C:\\Program Files\\Android\\Android Studio\\jre",
611            ];
612            for path in &win_paths {
613                if std::path::Path::new(path).exists() {
614                    custom_java_home = Some(path.to_string());
615                    break;
616                }
617            }
618        } else {
619            // Linux & other Unix-like OS
620            let unix_paths = [
621                "/opt/android-studio/jbr",
622                "/opt/android-studio/jre",
623                "/snap/android-studio/current/jbr",
624                "/snap/android-studio/current/jre",
625                "/usr/local/android-studio/jbr",
626                "/usr/local/android-studio/jre",
627                "/usr/lib/jvm/default-java",
628            ];
629            for path in &unix_paths {
630                if std::path::Path::new(path).exists() {
631                    custom_java_home = Some(path.to_string());
632                    break;
633                }
634            }
635        }
636        if let Some(jh) = custom_java_home {
637            unsafe {
638                std::env::set_var("JAVA_HOME", &jh);
639            }
640        }
641    }
642}
643
644fn run_native(run_android: bool, run_desktop: bool) {
645    if run_android {
646        println!("šŸš€ Memulai RustBasic Android Wrapper (Native implementation)...");
647
648        // 1. Setup environment
649        let home = std::env::var("HOME").unwrap_or_default();
650        let android_home = std::env::var("ANDROID_HOME")
651            .unwrap_or_else(|_| format!("{}/Library/Android/sdk", home));
652        unsafe { std::env::set_var("ANDROID_HOME", &android_home); }
653
654        setup_java_home();
655
656        let mut devices = get_adb_devices();
657        if devices.is_empty() {
658            println!("šŸ“± Perangkat Android atau emulator tidak terdeteksi aktif.");
659            let emulator_bin = format!("{}/emulator/emulator", android_home);
660            if std::path::Path::new(&emulator_bin).exists() {
661                let avd_output = std::process::Command::new(&emulator_bin).arg("-list-avds").output();
662                if let Ok(avd_out) = avd_output {
663                    let avds_str = String::from_utf8_lossy(&avd_out.stdout);
664                    if let Some(avd_name) = avds_str.lines().next() {
665                        println!("šŸš€ Menyalakan emulator AVD: {}...", avd_name);
666                        let _ = std::process::Command::new(&emulator_bin)
667                            .args(["-avd", avd_name])
668                            .stdout(std::process::Stdio::null())
669                            .stderr(std::process::Stdio::null())
670                            .spawn();
671                        
672                        println!("ā³ Menunggu emulator menyala dan terdeteksi adb...");
673                        let _ = std::process::Command::new("adb").arg("wait-for-device").status();
674                        println!("āœ… Emulator berhasil aktif!");
675                        std::thread::sleep(std::time::Duration::from_secs(3));
676                        devices = get_adb_devices();
677                    }
678                }
679            }
680        }
681
682        let (device_id, device_name) = if devices.len() == 1 {
683            let d = devices[0].clone();
684            println!("šŸ“± Menggunakan perangkat tunggal: {} ({})", d.1, d.0);
685            d
686        } else if devices.len() > 1 {
687            println!("šŸ“± Terdeteksi beberapa perangkat Android. Silakan pilih target:");
688            for (idx, d) in devices.iter().enumerate() {
689                println!("  [{}] {} ({})", idx + 1, d.1, d.0);
690            }
691            let choice = prompt_choice("šŸ‘‰ Pilih nomor perangkat: ", 1, devices.len());
692            devices[choice - 1].clone()
693        } else {
694            println!("āŒ Error: Tidak ada perangkat Android terhubung.");
695            return;
696        };
697
698        // 3. build JNI
699        if !compile_jni_libraries() {
700            return;
701        }
702
703        // 4. local.properties
704        let local_props = std::path::Path::new("native/android/local.properties");
705        if !local_props.exists() {
706            if let Ok(mut file) = std::fs::File::create(local_props) {
707                use std::io::Write;
708                let _ = writeln!(file, "sdk.dir={}", android_home);
709            }
710        }
711
712        // 5. gradlew assembleDebug
713        println!("šŸ”Ø Membangun debug APK menggunakan Gradle...");
714        let gradlew_bin = if cfg!(target_os = "windows") { "gradlew.bat" } else { "./gradlew" };
715        let mut gradle_cmd = std::process::Command::new(gradlew_bin);
716        gradle_cmd.arg("assembleDebug");
717        gradle_cmd.current_dir("native/android");
718
719        if let Ok(jh) = std::env::var("JAVA_HOME") {
720            gradle_cmd.env("JAVA_HOME", jh);
721        }
722
723        let gradle_status = gradle_cmd.status();
724        if gradle_status.is_err() || !gradle_status.unwrap().success() {
725            println!("āŒ Gradle build assembleDebug gagal.");
726            return;
727        }
728
729        // 6. adb install
730        println!("šŸ”Ø Memasang APK ke perangkat {} ({})...", device_name, device_id);
731        let install_status = std::process::Command::new("adb")
732            .args(["-s", &device_id, "install", "-r", "native/android/app/build/outputs/apk/debug/app-debug.apk"])
733            .status();
734
735        if install_status.is_err() || !install_status.unwrap().success() {
736            println!("āŒ Gagal memasang APK ke device.");
737            return;
738        }
739
740        // 7. adb reverse
741        let vite_port = "5173"; // default
742        let reverse_status = std::process::Command::new("adb")
743            .args(["-s", &device_id, "reverse", &format!("tcp:{}", vite_port), &format!("tcp:{}", vite_port)])
744            .status();
745        if reverse_status.is_err() {
746            println!("āš ļø Warning: Gagal melakukan adb reverse port {}", vite_port);
747        }
748
749        // 8. adb shell am start
750        println!("šŸš€ Membuka aplikasi di perangkat {}...", device_name);
751        let _ = std::process::Command::new("adb")
752            .args(["-s", &device_id, "logcat", "-c"])
753            .status();
754        
755        let _ = std::process::Command::new("adb")
756            .args(["-s", &device_id, "shell", "am", "start", "-n", "com.rustbasic.mobile/com.rustbasic.mobile.MainActivity"])
757            .status();
758
759        println!("šŸ“‹ Menampilkan log realtime dari perangkat {} (Tekan Ctrl+C untuk keluar)...", device_name);
760        let mut logcat_cmd = std::process::Command::new("adb");
761        logcat_cmd.args(["-s", &device_id, "logcat", "-s", "RustBasicServer"]);
762        let mut child = logcat_cmd.spawn().expect("Gagal menjalankan adb logcat");
763        let _ = child.wait();
764    } else if run_desktop {
765        println!("šŸš€ Memulai RustBasic Desktop Wrapper...");
766        let mut cmd = std::process::Command::new("cargo");
767        cmd.args(["run", "--bin", "rustbasic-desktop", "--features", "desktop"]);
768        let status = cmd.status();
769        match status {
770            Ok(s) if s.success() => {}
771            _ => {
772                println!("āŒ Gagal menjalankan Desktop Wrapper.");
773            }
774        }
775    }
776}
777
778fn get_adb_devices() -> Vec<(String, String)> {
779    let output = std::process::Command::new("adb").arg("devices").output();
780    let mut devices = Vec::new();
781    if let Ok(out) = output {
782        let stdout = String::from_utf8_lossy(&out.stdout);
783        for line in stdout.lines() {
784            if line.contains("device") && !line.contains("List of devices") {
785                let parts: Vec<&str> = line.split_whitespace().collect();
786                if parts.is_empty() { continue; }
787                let device_id = parts[0].to_string();
788                let model_out = std::process::Command::new("adb")
789                    .args(["-s", &device_id, "shell", "getprop", "ro.product.model"])
790                    .output();
791                let model = if let Ok(m_out) = model_out {
792                    String::from_utf8_lossy(&m_out.stdout).trim().to_string()
793                } else {
794                    "Unknown Device".to_string()
795                };
796                devices.push((device_id, model));
797            }
798        }
799    }
800    devices
801}
802
803fn compile_jni_libraries() -> bool {
804    println!("šŸš€ Building Rust library for Android (Native Rust implementation)...");
805
806    let _ = std::process::Command::new("rustup")
807        .args(["target", "add", "aarch64-linux-android", "armv7-linux-androideabi", "x86_64-linux-android"])
808        .status();
809
810    let home = std::env::var("HOME").unwrap_or_default();
811    let android_ndk_home = if let Ok(val) = std::env::var("ANDROID_NDK_HOME") {
812        val
813    } else {
814        let mac_ndk = format!("{}/Library/Android/sdk/ndk", home);
815        if std::path::Path::new(&mac_ndk).exists() {
816            if let Ok(entries) = std::fs::read_dir(&mac_ndk) {
817                let mut paths: Vec<_> = entries.flatten().map(|e| e.path()).collect();
818                paths.sort();
819                if let Some(highest) = paths.last() {
820                    highest.display().to_string()
821                } else {
822                    "".to_string()
823                }
824            } else {
825                "".to_string()
826            }
827        } else {
828            "".to_string()
829        }
830    };
831
832    if android_ndk_home.is_empty() {
833        println!("āŒ Error: ANDROID_NDK_HOME is not set. Please set ANDROID_NDK_HOME.");
834        return false;
835    }
836
837    println!("Using NDK: {}", android_ndk_home);
838
839    let os = std::env::consts::OS;
840    let toolchain_sub = if os == "macos" { "darwin-x86_64" } else { "linux-x86_64" };
841    let toolchain_bin_path = std::path::Path::new(&android_ndk_home)
842        .join("toolchains/llvm/prebuilt")
843        .join(toolchain_sub)
844        .join("bin");
845
846    if !toolchain_bin_path.exists() {
847        println!("āŒ Error: Toolchain bin path not found: {}", toolchain_bin_path.display());
848        return false;
849    }
850
851    let sqlite_version = "3450100";
852    let sqlite_dir = format!("target/sqlite-amalgamation-{}", sqlite_version);
853    if !std::path::Path::new(&sqlite_dir).exists() {
854        println!("šŸ“„ Downloading SQLite source amalgamation...");
855        std::fs::create_dir_all("target").ok();
856        
857        let zip_path = "target/sqlite.zip";
858        let sqlite_url = format!("https://www.sqlite.org/2024/sqlite-amalgamation-{}.zip", sqlite_version);
859        
860        let curl_status = std::process::Command::new("curl")
861            .args(["-sSLo", zip_path, &sqlite_url])
862            .status();
863        
864        if curl_status.is_err() || !curl_status.unwrap().success() {
865            println!("āŒ Gagal men-download SQLite source.");
866            return false;
867        }
868
869        let unzip_status = std::process::Command::new("unzip")
870            .args(["-q", zip_path, "-d", "target/"])
871            .status();
872
873        let _ = std::fs::remove_file(zip_path);
874
875        if unzip_status.is_err() || !unzip_status.unwrap().success() {
876            println!("āŒ Gagal mengekstrak SQLite source.");
877            return false;
878        }
879    }
880
881    let targets = vec![
882        ("aarch64-linux-android", "arm64-v8a", "aarch64-linux-android21-clang"),
883        ("armv7-linux-androideabi", "armeabi-v7a", "armv7a-linux-androideabi21-clang"),
884        ("x86_64-linux-android", "x86_64", "x86_64-linux-android21-clang"),
885    ];
886
887    let jnilibs_dir = "native/android/app/src/main/jniLibs";
888
889    for (target, arch, clang_name) in targets {
890        println!("šŸ”Ø Preparing SQLite static library for {}...", target);
891        
892        let clang_path = toolchain_bin_path.join(clang_name);
893        let ar_path = toolchain_bin_path.join("llvm-ar");
894
895        if !clang_path.exists() {
896            println!("āŒ Error: Compiler not found: {}", clang_path.display());
897            return false;
898        }
899
900        let sqlite_out = format!("target/{}/sqlite", target);
901        std::fs::create_dir_all(&sqlite_out).ok();
902
903        let libsqlite3_a = format!("{}/libsqlite3.a", sqlite_out);
904        if !std::path::Path::new(&libsqlite3_a).exists() {
905            println!("   Compiling SQLite static lib for {}...", target);
906            let sqlite3_o = format!("{}/sqlite3.o", sqlite_out);
907            let sqlite3_c = format!("{}/sqlite3.c", sqlite_dir);
908            
909            let compile_status = std::process::Command::new(&clang_path)
910                .args(["-O2", "-c", &sqlite3_c, "-o", &sqlite3_o])
911                .status();
912
913            if compile_status.is_err() || !compile_status.unwrap().success() {
914                println!("āŒ Gagal mengompilasi sqlite3.o");
915                return false;
916            }
917
918            let archive_status = std::process::Command::new(&ar_path)
919                .args(["rcs", &libsqlite3_a, &sqlite3_o])
920                .status();
921
922            if archive_status.is_err() || !archive_status.unwrap().success() {
923                println!("āŒ Gagal mengarsip libsqlite3.a");
924                return false;
925            }
926        }
927
928        println!("šŸ”Ø Compiling Rust library for {}...", target);
929        let mut cargo_cmd = std::process::Command::new("cargo");
930        cargo_cmd.args(["build", "--target", target, "--release"]);
931
932        let clang_path_str = clang_path.display().to_string();
933        let ar_path_str = ar_path.display().to_string();
934
935        let target_upper = target.replace("-", "_").to_uppercase();
936        let linker_env = format!("CARGO_TARGET_{}_LINKER", target_upper);
937        let cc_env = format!("CC_{}", target.replace("-", "_"));
938        let ar_env = format!("AR_{}", target.replace("-", "_"));
939
940        cargo_cmd.env(&linker_env, &clang_path_str);
941        cargo_cmd.env(&cc_env, &clang_path_str);
942        cargo_cmd.env(&ar_env, &ar_path_str);
943
944        let cargo_status = cargo_cmd.status();
945        if cargo_status.is_err() || !cargo_status.unwrap().success() {
946            println!("āŒ Gagal mengompilasi library Rust untuk target {}", target);
947            return false;
948        }
949
950        let dest_dir = format!("{}/{}", jnilibs_dir, arch);
951        std::fs::create_dir_all(&dest_dir).ok();
952
953        let src_so = format!("target/{}/release/librustbasic.so", target);
954        let dest_so = format!("{}/librustbasic_mobile.so", dest_dir);
955
956        if let Err(e) = std::fs::copy(&src_so, &dest_so) {
957            println!("āŒ Gagal menyalin {}: {}", src_so, e);
958            return false;
959        }
960    }
961
962    println!("āœ… Android JNI libraries built successfully!");
963    true
964}
965
966// ============================================================
967// BUILD
968// ============================================================
969fn prompt_choice(prompt: &str, min: usize, max: usize) -> usize {
970    use std::io::{self, Write};
971    loop {
972        print!("{}", prompt);
973        let _ = io::stdout().flush();
974        let mut input = String::new();
975        if io::stdin().read_line(&mut input).is_ok() {
976            if let Ok(choice) = input.trim().parse::<usize>() {
977                if choice >= min && choice <= max {
978                    return choice;
979                }
980            }
981        }
982        println!("āš ļø Pilihan tidak valid, silakan coba lagi.");
983    }
984}
985
986pub async fn handle_build(args: &[String]) {
987    let mut build_docker  = args.iter().any(|a| a == "--docker");
988    let mut build_desktop = args.iter().any(|a| a == "--desktop");
989    let mut build_android = args.iter().any(|a| a == "--android");
990    let release_mode  = args.iter().any(|a| a == "--release" || a == "-r");
991    let mut target_type   = String::new();
992    let mut docker_tag    = String::new();
993    let mut docker_platform = String::new();
994
995    for i in 0..args.len() {
996        if args[i] == "--type" && i + 1 < args.len() { target_type = args[i+1].to_lowercase(); }
997        if args[i] == "--tag"  && i + 1 < args.len() { docker_tag  = args[i+1].clone(); }
998        if args[i] == "--platform" && i + 1 < args.len() { docker_platform = args[i+1].clone(); }
999    }
1000
1001    if !build_docker && !build_desktop && !build_android {
1002        let is_native_installed = if let Ok(content) = std::fs::read_to_string(".rustbasic_packages.json") {
1003            content.contains("\"rustbasic-native\"")
1004        } else {
1005            false
1006        };
1007
1008        println!("šŸ› ļø  RustBasic Build CLI");
1009        println!("Pilih platform target untuk di-build:");
1010        println!("  [1] Docker (Container Image)");
1011
1012        let max_choice = if is_native_installed {
1013            println!("  [2] Desktop Wrapper (Windows, macOS, Linux)");
1014            println!("  [3] Android Wrapper (APK, AAB)");
1015            3
1016        } else {
1017            1
1018        };
1019
1020        let prompt_str = format!("šŸ‘‰ Pilih nomor platform (1-{}): ", max_choice);
1021        match prompt_choice(&prompt_str, 1, max_choice) {
1022            1 => build_docker  = true,
1023            2 => build_desktop = true,
1024            3 => build_android = true,
1025            _ => {}
1026        }
1027    }
1028
1029    if build_docker {
1030        let mut extract = args.iter().any(|a| a == "--extract" || a == "-e");
1031        if docker_platform.is_empty() {
1032            println!("\nPilih Platform Target CPU Docker:");
1033            println!("  [1] Current Host Platform (Sesuai OS komputer Anda)");
1034            println!("  [2] Linux AMD64 / x86_64 (Standard VPS Intel/AMD - Umum/Rekomendasi)");
1035            println!("  [3] Linux ARM64 / aarch64 (Server berbasis ARM / AWS Graviton)");
1036            match prompt_choice("šŸ‘‰ Pilih (1-3): ", 1, 3) {
1037                2 => docker_platform = "linux/amd64".to_string(),
1038                3 => docker_platform = "linux/arm64".to_string(),
1039                _ => {}
1040            }
1041        }
1042
1043        // Peringatan Arsitektur CPU Mismatch
1044        let host_arch = get_host_arch();
1045        let is_mismatch = (host_arch == "aarch64" && docker_platform == "linux/amd64")
1046            || (host_arch == "x86_64" && docker_platform == "linux/arm64");
1047
1048        if is_mismatch {
1049            println!("\nāš ļø  {}", "PERINGATAN: Arsitektur CPU Mismatch (Sangat Lambat)".yellow().bold());
1050            println!("   Anda berada di host dengan CPU '{}' tetapi memilih target Docker '{}'.", host_arch, docker_platform);
1051            println!("   Docker akan menggunakan emulasi CPU (QEMU) yang membuat proses kompilasi");
1052            println!("   Rust berjalan {} (bisa memakan waktu 10-30 menit).", "10x-20x LEBIH LAMBAT".red().bold());
1053            println!("   ");
1054            println!("   šŸ’” {} Kami telah mengaktifkan target caching untuk mempercepat build.", "TIPS:".cyan().bold());
1055            println!("      Build pertama tetap lambat, namun build berikutnya akan sangat cepat (1-2 menit)");
1056            println!("      karena target directory dan dependency cache disimpan oleh Docker.");
1057            println!("   ");
1058            let proceed = prompt_string("šŸ‘‰ Apakah Anda ingin melanjutkan proses build? (y/n) [default: y]: ", "y");
1059            if !proceed.to_lowercase().starts_with('y') {
1060                println!("āŒ Build dibatalkan oleh pengguna.");
1061                return;
1062            }
1063        }
1064
1065        if !extract && !args.iter().any(|a| a == "--docker") {
1066            let extract_choice = prompt_string("\nšŸ‘‰ Apakah Anda ingin mengekstrak biner Linux hasil build ke folder './build-output'? (y/n) [default: n]: ", "n");
1067            extract = extract_choice.to_lowercase().starts_with('y');
1068        }
1069
1070        build_docker_image(&docker_tag, &docker_platform, extract).await;
1071    } else if build_desktop {
1072        build_desktop_binary(args, release_mode).await;
1073    } else if build_android {
1074        build_android_apk(args, &target_type, release_mode).await;
1075    }
1076}
1077
1078async fn build_docker_image(custom_tag: &str, platform: &str, extract_binary: bool) {
1079    // Cek Docker tersedia
1080    if !std::process::Command::new("docker").arg("version")
1081        .stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null())
1082        .status().map(|s| s.success()).unwrap_or(false)
1083    {
1084        println!("āŒ Docker tidak ditemukan. Install: https://docs.docker.com/get-docker/");
1085        return;
1086    }
1087
1088    let dockerfile_path = std::path::Path::new("Dockerfile");
1089    if !dockerfile_path.exists() {
1090        println!("šŸ“ Membuat Dockerfile...");
1091        let is_monorepo = std::path::Path::new("../rustbasic-core").exists() || std::path::Path::new("rustbasic-core").exists();
1092        let binary_name = get_cargo_package_name();
1093
1094        let dockerfile_content = if is_monorepo {
1095            r#"# ============================================================
1096# RustBasic Docker Build — Standalone (Cached)
1097# ============================================================
1098
1099# Stage 1: Builder
1100FROM rust:1-slim-bookworm AS builder
1101
1102RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
1103    --mount=type=cache,target=/var/lib/apt,sharing=locked \
1104    apt-get update && apt-get install -y \
1105    pkg-config libssl-dev
1106
1107# Copy rustbasic-core (dari konteks workspace root)
1108WORKDIR /rustbasic-core
1109COPY --from=core . .
1110
1111# Copy proyek utama rustbasic
1112WORKDIR /build
1113COPY . .
1114
1115# Build release binary using Cargo registry, git cache, and target cache
1116ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
1117RUN --mount=type=cache,target=/usr/local/cargo/registry \
1118    --mount=type=cache,target=/usr/local/cargo/git \
1119    --mount=type=cache,target=/build/target \
1120    cargo build --release --bin rustbasic && \
1121    cp target/release/rustbasic /build/rustbasic-bin
1122
1123# Stage 2: Runtime
1124FROM debian:bookworm-slim
1125
1126RUN apt-get update && apt-get install -y \
1127    ca-certificates libssl3 \
1128    && rm -rf /var/lib/apt/lists/*
1129
1130WORKDIR /app
1131
1132# Copy binary dari builder stage
1133COPY --from=builder /build/rustbasic-bin ./rustbasic
1134
1135# Copy assets yang diperlukan dari builder stage (lebih aman dan bersih)
1136COPY --from=builder /build/src/resources/views/ src/resources/views/
1137COPY --from=builder /build/src/dist/ src/dist/
1138COPY --from=builder /build/public/ public/
1139COPY --from=builder /build/database/ database/
1140COPY --from=builder /build/.env.example .env
1141
1142# Expose port aplikasi
1143EXPOSE 4000
1144
1145CMD ["./rustbasic"]
1146"#.to_string()
1147        } else {
1148            format!(r#"# ============================================================
1149# RustBasic Docker Build — Standalone (Cached)
1150# ============================================================
1151
1152# Stage 1: Builder
1153FROM rust:1-slim-bookworm AS builder
1154
1155RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
1156    --mount=type=cache,target=/var/lib/apt,sharing=locked \
1157    apt-get update && apt-get install -y \
1158    pkg-config libssl-dev
1159
1160WORKDIR /build
1161
1162COPY . .
1163
1164# Build release binary using Cargo registry, git cache, and target cache
1165ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse
1166RUN --mount=type=cache,target=/usr/local/cargo/registry \
1167    --mount=type=cache,target=/usr/local/cargo/git \
1168    --mount=type=cache,target=/build/target \
1169    cargo build --release --bin {bin_name} && \
1170    cp target/release/{bin_name} /build/{bin_name}-bin
1171
1172# Stage 2: Runtime
1173FROM debian:bookworm-slim
1174
1175RUN apt-get update && apt-get install -y \
1176    ca-certificates libssl3 \
1177    && rm -rf /var/lib/apt/lists/*
1178
1179WORKDIR /app
1180
1181# Copy binary dari builder stage
1182COPY --from=builder /build/{bin_name}-bin ./{bin_name}
1183
1184# Copy assets yang diperlukan dari builder stage
1185COPY --from=builder /build/src/resources/views/ src/resources/views/
1186COPY --from=builder /build/src/dist/ src/dist/
1187COPY --from=builder /build/public/ public/
1188COPY --from=builder /build/database/ database/
1189COPY --from=builder /build/.env.example .env
1190
1191# Expose port aplikasi
1192EXPOSE 4000
1193
1194CMD ["./{bin_name}"]
1195"#, bin_name = binary_name)
1196        };
1197
1198        if let Err(e) = std::fs::write(dockerfile_path, dockerfile_content) {
1199            println!("āŒ Gagal membuat Dockerfile: {}", e);
1200            return;
1201        }
1202        println!("āœ… Dockerfile berhasil dibuat.");
1203    }
1204
1205    let app_name = std::env::var("APP_NAME").unwrap_or_else(|_| get_cargo_package_name()).to_lowercase();
1206    let image_tag = if custom_tag.is_empty() { format!("{}:latest", app_name) } else { custom_tag.to_string() };
1207
1208    if std::path::Path::new("package.json").exists() {
1209        println!("šŸ“¦ Mengompilasi frontend assets (npm run build)...");
1210        let npm_cmd = if cfg!(windows) { "npm.cmd" } else { "npm" };
1211        let success = match std::process::Command::new(npm_cmd)
1212            .args(&["run", "build"])
1213            .status()
1214        {
1215            Ok(status) => status.success(),
1216            Err(_) => {
1217                println!("āŒ Gagal menjalankan npm. Pastikan Node.js & npm terinstal di sistem Anda.");
1218                return;
1219            }
1220        };
1221
1222        if !success {
1223            println!("āŒ Gagal mengompilasi frontend assets. Silakan periksa error di atas.");
1224            return;
1225        }
1226    }
1227
1228    println!("\n🐳 Docker build dimulai...");
1229    println!("   Image: {}", image_tag);
1230    
1231    let core_context = if std::path::Path::new("../rustbasic-core").exists() {
1232        "core=../rustbasic-core".to_string()
1233    } else {
1234        "core=.".to_string()
1235    };
1236
1237    let mut build_args = vec![
1238        "build".to_string(),
1239        "--build-context".to_string(),
1240        core_context,
1241        "--progress=plain".to_string(),
1242    ];
1243    
1244    if !platform.is_empty() {
1245        build_args.push("--platform".to_string());
1246        build_args.push(platform.to_string());
1247        println!("   Platform: {}", platform);
1248    }
1249    
1250    build_args.push("-t".to_string());
1251    build_args.push(image_tag.clone());
1252    build_args.push(".".to_string());
1253
1254    println!("   Running: docker {}", build_args.join(" "));
1255
1256    let mut cmd = std::process::Command::new("docker")
1257        .args(&build_args)
1258        .stdin(std::process::Stdio::inherit())
1259        .stdout(std::process::Stdio::inherit())
1260        .stderr(std::process::Stdio::inherit())
1261        .spawn().expect("Gagal menjalankan docker build");
1262
1263    let success = cmd.wait().map(|s| s.success()).unwrap_or(false);
1264
1265    if success {
1266        println!("\nāœ… Docker build selesai! Image: {}", image_tag);
1267
1268        if extract_binary {
1269            println!("šŸ“¦ Mengekstrak biner Linux dari image Docker...");
1270            let container_name = format!("temp-extract-{}", std::time::SystemTime::now()
1271                .duration_since(std::time::UNIX_EPOCH)
1272                .unwrap_or_default()
1273                .as_secs());
1274            
1275            // docker create
1276            let create_res = std::process::Command::new("docker")
1277                .args(["create", "--name", &container_name, &image_tag])
1278                .status();
1279
1280            if create_res.is_ok() && create_res.unwrap().success() {
1281                // create build-output directory
1282                let _ = std::fs::create_dir_all("build-output");
1283                
1284                // Get cargo package name (which is the binary name inside container)
1285                let binary_name = get_cargo_package_name();
1286
1287                // docker cp
1288                let cp_status = std::process::Command::new("docker")
1289                    .args(["cp", &format!("{}:/app/{}", container_name, binary_name), &format!("build-output/{}", binary_name)])
1290                    .status();
1291                    
1292                // docker rm
1293                let _ = std::process::Command::new("docker")
1294                    .args(["rm", &container_name])
1295                    .status();
1296                    
1297                if cp_status.is_ok() && cp_status.unwrap().success() {
1298                    println!("āœ… Biner berhasil diekstrak ke: {}", format!("build-output/{}", binary_name).cyan().bold());
1299                } else {
1300                    println!("āŒ Gagal mengekstrak biner dari container.");
1301                }
1302            } else {
1303                println!("āŒ Gagal membuat temporary container untuk ekstraksi.");
1304            }
1305        }
1306
1307        println!("   Jalankan container (Lokal/Development):");
1308        if !platform.is_empty() {
1309            println!("   docker run --platform {} -p 4000:4000 --env-file .env {}", platform, image_tag);
1310        } else {
1311            println!("   docker run -p 4000:4000 --env-file .env {}", image_tag);
1312        }
1313        println!("   Jalankan container (Produksi/Server - Auto Restart):");
1314        if !platform.is_empty() {
1315            println!("   docker run --platform {} -d -p 80:4000 --restart unless-stopped --env-file .env {}", platform, image_tag);
1316        } else {
1317            println!("   docker run -d -p 80:4000 --restart unless-stopped --env-file .env {}", image_tag);
1318        }
1319    } else {
1320        println!("\nāŒ Docker build gagal.");
1321    }
1322}
1323
1324async fn build_desktop_binary(args: &[String], mut release_mode: bool) {
1325    let mut target_triple = "";
1326
1327    for i in 0..args.len() {
1328        if args[i] == "--os" && i + 1 < args.len() {
1329            target_triple = match args[i+1].as_str() {
1330                "macos-intel"   => "x86_64-apple-darwin",
1331                "macos-silicon" => "aarch64-apple-darwin",
1332                "windows"       => "x86_64-pc-windows-msvc",
1333                "windows-gnu"   => "x86_64-pc-windows-gnu",
1334                "linux"         => "x86_64-unknown-linux-gnu",
1335                _               => "",
1336            };
1337        }
1338    }
1339
1340    if target_triple.is_empty() && !args.iter().any(|a| a.starts_with("--os")) {
1341        println!("\nPilih OS Target Desktop:");
1342        println!("  [1] Current OS");
1343        println!("  [2] macOS Intel (x86_64-apple-darwin)");
1344        println!("  [3] macOS Apple Silicon (aarch64-apple-darwin)");
1345        println!("  [4] Windows MSVC (x86_64-pc-windows-msvc)");
1346        println!("  [5] Windows GNU (x86_64-pc-windows-gnu - Rekomendasi Cross-compile dari macOS/Linux)");
1347        println!("  [6] Linux (x86_64-unknown-linux-gnu)");
1348        match prompt_choice("šŸ‘‰ Pilih (1-6): ", 1, 6) {
1349            2 => target_triple = "x86_64-apple-darwin",
1350            3 => target_triple = "aarch64-apple-darwin",
1351            4 => target_triple = "x86_64-pc-windows-msvc",
1352            5 => target_triple = "x86_64-pc-windows-gnu",
1353            6 => target_triple = "x86_64-unknown-linux-gnu",
1354            _ => {}
1355        }
1356    }
1357
1358    if !args.iter().any(|a| a == "--release" || a == "-r" || a == "--debug" || a == "-d") {
1359        println!("\n  [1] Debug\n  [2] Release");
1360        if prompt_choice("šŸ‘‰ Mode (1-2): ", 1, 2) == 2 { release_mode = true; }
1361    }
1362
1363    let mut build_args = vec!["build", "--bin", "rustbasic-desktop", "--features", "desktop"];
1364    if release_mode   { build_args.push("--release"); }
1365    if !target_triple.is_empty() {
1366        build_args.push("--target");
1367        build_args.push(target_triple);
1368        let _ = std::process::Command::new("rustup").args(["target", "add", target_triple]).status();
1369    }
1370
1371    println!("\nšŸ–„ļø  Desktop build: cargo {}", build_args.join(" "));
1372    let mut cmd = std::process::Command::new("cargo")
1373        .args(&build_args)
1374        .stdin(std::process::Stdio::inherit())
1375        .stdout(std::process::Stdio::inherit())
1376        .stderr(std::process::Stdio::inherit())
1377        .spawn().expect("Gagal menjalankan cargo build");
1378
1379    if cmd.wait().map(|s| s.success()).unwrap_or(false) {
1380        println!("\nāœ… Desktop build selesai!");
1381    } else {
1382        println!("\nāŒ Desktop build gagal.");
1383    }
1384}
1385
1386async fn build_android_apk(args: &[String], target_type: &str, mut release_mode: bool) {
1387    let is_aab = if target_type.is_empty() {
1388        println!("\n  [1] APK\n  [2] AAB (Google Play)");
1389        prompt_choice("šŸ‘‰ Format (1-2): ", 1, 2) == 2
1390    } else {
1391        target_type == "aab"
1392    };
1393
1394    if !args.iter().any(|a| a == "--release" || a == "-r" || a == "--debug" || a == "-d") {
1395        println!("\n  [1] Debug\n  [2] Release");
1396        if prompt_choice("šŸ‘‰ Mode (1-2): ", 1, 2) == 2 { release_mode = true; }
1397    }
1398
1399    // Build JNI
1400    println!("\nšŸ”Ø Membangun JNI library (Native implementation)...");
1401    if !compile_jni_libraries() {
1402        println!("āŒ JNI build gagal.");
1403        return;
1404    }
1405
1406    // Setup environment
1407    let home = std::env::var("HOME").unwrap_or_default();
1408    let android_home = std::env::var("ANDROID_HOME")
1409        .unwrap_or_else(|_| format!("{}/Library/Android/sdk", home));
1410    unsafe { std::env::set_var("ANDROID_HOME", &android_home); }
1411
1412    setup_java_home();
1413
1414    let gradle_task = match (is_aab, release_mode) {
1415        (false, false) => "assembleDebug",
1416        (false, true)  => "assembleRelease",
1417        (true,  false) => "bundleDebug",
1418        (true,  true)  => "bundleRelease",
1419    };
1420
1421    println!("\nšŸ”Ø Gradle task: {}", gradle_task);
1422    let gradlew = if std::path::Path::new("native/android/gradlew").exists() { "./gradlew" } else { "gradle" };
1423    let mut cmd = std::process::Command::new(gradlew);
1424    cmd.arg(gradle_task)
1425        .current_dir("native/android");
1426
1427    if let Ok(jh) = std::env::var("JAVA_HOME") {
1428        cmd.env("JAVA_HOME", jh);
1429    }
1430
1431    cmd.stdin(std::process::Stdio::inherit()).stdout(std::process::Stdio::inherit()).stderr(std::process::Stdio::inherit());
1432    let mut child = cmd.spawn().expect("Gagal menjalankan Gradle");
1433
1434    if child.wait().map(|s| s.success()).unwrap_or(false) {
1435        println!("\nāœ… Android build selesai!");
1436    } else {
1437        println!("\nāŒ Android build gagal.");
1438    }
1439}
1440
1441fn prompt_string(prompt: &str, default: &str) -> String {
1442    use std::io::Write;
1443    print!("{}", prompt);
1444    let _ = std::io::stdout().flush();
1445    let mut input = String::new();
1446    if std::io::stdin().read_line(&mut input).is_ok() {
1447        let trimmed = input.trim();
1448        if trimmed.is_empty() {
1449            default.to_string()
1450        } else {
1451            trimmed.to_string()
1452        }
1453    } else {
1454        default.to_string()
1455    }
1456}
1457
1458pub async fn handle_deploy() {
1459    println!("\n{}", "šŸš€ RustBasic Docker Deploy CLI".magenta().bold());
1460    println!("{}", "------------------------------".magenta());
1461
1462    // 1. Konfigurasi Image & Pengiriman
1463    let image_name = prompt_string("šŸ‘‰ Masukkan Nama/Tag Docker Image (default: rustbasic:latest): ", "rustbasic:latest");
1464
1465    // Cek apakah Docker image sudah ada
1466    let inspect = std::process::Command::new("docker")
1467        .args(["image", "inspect", &image_name])
1468        .stdout(std::process::Stdio::null())
1469        .stderr(std::process::Stdio::null())
1470        .status();
1471
1472    match inspect {
1473        Ok(status) if status.success() => {}
1474        _ => {
1475            println!("{}", format!("āš ļø  Peringatan: Image '{}' tidak ditemukan lokal.", image_name).yellow());
1476            let proceed = prompt_string("šŸ‘‰ Tetap lanjutkan proses ekspor? (y/n) [default: n]: ", "n");
1477            if proceed.to_lowercase() != "y" {
1478                println!("āŒ Proses dihentikan.");
1479                return;
1480            }
1481        }
1482    }
1483
1484    // 2. Ekspor ke tar
1485    println!("šŸ“¦ Mengekspor Docker image '{}' ke 'rustbasic.tar'...", image_name);
1486    let save_status = std::process::Command::new("docker")
1487        .args(["save", "-o", "rustbasic.tar", &image_name])
1488        .status();
1489
1490    match save_status {
1491        Ok(status) if status.success() => {
1492            println!("{}", "āœ… Image berhasil diekspor ke rustbasic.tar.".green());
1493        }
1494        _ => {
1495            println!("{}", "āŒ Gagal mengekspor Docker image.".red().bold());
1496            return;
1497        }
1498    }
1499
1500    // 3. Konfigurasi Pengiriman
1501    println!("\n{}", "🌐 Konfigurasi Pengiriman ke Server".cyan().bold());
1502    println!("{}", "-----------------------------------".cyan());
1503    let ssh_user = prompt_string("šŸ‘‰ Masukkan SSH Username Server (contoh: root) [default: root]: ", "root");
1504    let ssh_ip = prompt_string("šŸ‘‰ Masukkan IP Address Server: ", "");
1505    if ssh_ip.is_empty() {
1506        println!("{}", "āŒ IP Address server tidak boleh kosong.".red().bold());
1507        let _ = std::fs::remove_file("rustbasic.tar");
1508        return;
1509    }
1510    let ssh_port = prompt_string("šŸ‘‰ Masukkan SSH Port Server (default: 22): ", "22");
1511    let dest_dir = prompt_string("šŸ‘‰ Masukkan Folder Tujuan di Server (default: ~/app): ", "~/app");
1512    let server_port = prompt_string("šŸ‘‰ Masukkan Port Server Mapping (contoh: 80:4000) [default: 80:4000]: ", "80:4000");
1513    let env_file = prompt_string("šŸ‘‰ Masukkan File Env yang akan dikirim (default: .env): ", ".env");
1514
1515    println!("\nšŸš€ Menyiapkan folder tujuan di server...");
1516    let mkdir_status = std::process::Command::new("ssh")
1517        .args([
1518            "-p", &ssh_port,
1519            &format!("{}@{}", ssh_user, ssh_ip),
1520            &format!("mkdir -p {}", dest_dir)
1521        ])
1522        .status();
1523
1524    match mkdir_status {
1525        Ok(status) if status.success() => {}
1526        _ => {
1527            println!("{}", "āŒ Gagal terhubung ke server menggunakan SSH.".red().bold());
1528            let _ = std::fs::remove_file("rustbasic.tar");
1529            return;
1530        }
1531    }
1532
1533    println!("šŸš€ Mengirimkan berkas rustbasic.tar & {} ke server...", env_file);
1534    let scp_status = std::process::Command::new("scp")
1535        .args([
1536            "-P", &ssh_port,
1537            "rustbasic.tar", &env_file,
1538            &format!("{}@{}:{}", ssh_user, ssh_ip, dest_dir)
1539        ])
1540        .status();
1541
1542    // Hapus file tar lokal
1543    let _ = std::fs::remove_file("rustbasic.tar");
1544
1545    if let Ok(status) = scp_status {
1546        if !status.success() {
1547            println!("{}", "āŒ Gagal mengirimkan berkas via SCP.".red().bold());
1548            return;
1549        }
1550    } else {
1551        println!("{}", "āŒ Gagal menjalankan SCP.".red().bold());
1552        return;
1553    }
1554
1555    println!("{}", "āœ… Pengiriman berkas berhasil!".green());
1556
1557    // 4. Eksekusi SSH otomatis di server jika disetujui
1558    let auto_run = prompt_string("\nšŸ‘‰ Apakah Anda ingin langsung menjalankan container di server secara otomatis? (y/n) [default: y]: ", "y");
1559    if auto_run.to_lowercase() == "y" || auto_run.is_empty() {
1560        println!("\nšŸš€ Memuat image di server (docker load)...");
1561        let load_status = std::process::Command::new("ssh")
1562            .args([
1563                "-p", &ssh_port,
1564                &format!("{}@{}", ssh_user, ssh_ip),
1565                &format!("docker load -i {}/rustbasic.tar", dest_dir)
1566            ])
1567            .status();
1568
1569        match load_status {
1570            Ok(status) if status.success() => {
1571                println!("{}", "āœ… Image berhasil dimuat di server.".green());
1572            }
1573            _ => {
1574                println!("{}", "āŒ Gagal memuat image di server.".red().bold());
1575                return;
1576            }
1577        }
1578
1579        println!("šŸš€ Menghentikan & menghapus container lama 'rustbasic-app' jika ada...");
1580        let stop_status = std::process::Command::new("ssh")
1581            .args([
1582                "-p", &ssh_port,
1583                &format!("{}@{}", ssh_user, ssh_ip),
1584                "docker stop rustbasic-app || true && docker rm rustbasic-app || true"
1585            ])
1586            .status();
1587
1588        if let Err(e) = stop_status {
1589            println!("āš ļø Peringatan saat membersihkan container lama: {}", e);
1590        }
1591
1592        println!("šŸš€ Menjalankan container baru 'rustbasic-app'...");
1593        let run_cmd = format!(
1594            "docker run -d --name rustbasic-app -p {} --restart unless-stopped --env-file {}/.env {}",
1595            server_port, dest_dir, image_name
1596        );
1597        let run_status = std::process::Command::new("ssh")
1598            .args([
1599                "-p", &ssh_port,
1600                &format!("{}@{}", ssh_user, ssh_ip),
1601                &run_cmd
1602            ])
1603            .status();
1604
1605        match run_status {
1606            Ok(status) if status.success() => {
1607                println!("{}", "šŸŽ‰ Container 'rustbasic-app' berhasil dijalankan di server!".green().bold());
1608            }
1609            _ => {
1610                println!("{}", "āŒ Gagal menjalankan container di server.".red().bold());
1611                return;
1612            }
1613        }
1614
1615        println!("šŸš€ Membersihkan file tar di server...");
1616        let rm_status = std::process::Command::new("ssh")
1617            .args([
1618                "-p", &ssh_port,
1619                &format!("{}@{}", ssh_user, ssh_ip),
1620                &format!("rm {}/rustbasic.tar", dest_dir)
1621            ])
1622            .status();
1623
1624        if let Err(e) = rm_status {
1625            println!("āš ļø Peringatan saat membersihkan file tar di server: {}", e);
1626        }
1627
1628        println!("\n{}", "šŸŽ‰ Deployment selesai!".green().bold());
1629        println!("{}", "--------------------------------------------------------".green());
1630        println!("Untuk melihat log aplikasi di server, jalankan:");
1631        println!("ssh -p {} {}@{} \"docker logs -f rustbasic-app\"", ssh_port, ssh_user, ssh_ip);
1632        println!("{}", "--------------------------------------------------------".green());
1633    } else {
1634        println!("\n{}", "šŸ–„ļø  Langkah Selanjutnya di Server Anda:".cyan().bold());
1635        println!("{}", "--------------------------------------------------------".green());
1636        println!("1. Hubungkan ke server via SSH:");
1637        println!("   ssh -p {} {}@{}", ssh_port, ssh_user, ssh_ip);
1638        println!("");
1639        println!("2. Masuk ke folder tujuan:");
1640        println!("   cd {}", dest_dir);
1641        println!("");
1642        println!("3. Muat (load) image Docker dari berkas tar:");
1643        println!("   docker load -i rustbasic.tar");
1644        println!("");
1645        println!("4. Jalankan container dengan fitur auto-restart:");
1646        println!("   docker run -d --name rustbasic-app -p {} --restart unless-stopped --env-file .env {}", server_port, image_name);
1647        println!("");
1648        println!("5. Hapus file tar di server untuk menghemat disk:");
1649        println!("   rm rustbasic.tar");
1650        println!("{}", "--------------------------------------------------------".green());
1651    }
1652}
1653
1654// ============================================================
1655// PUBLISH
1656// ============================================================
1657fn handle_publish(target: &str) {
1658    let mut selected_target = target.to_string();
1659    if selected_target.is_empty() {
1660        println!("šŸ› ļø  RustBasic Configuration Publisher");
1661        println!("Pilih konfigurasi yang ingin dipublikasikan ke proyek Anda:");
1662        println!("  [1] CORS (Cross-Origin Resource Sharing)");
1663        println!("  [2] CSRF (Cross-Site Request Forgery)");
1664        println!("  [3] APP (Application settings & Storage path overrides)");
1665        let choice = prompt_choice("šŸ‘‰ Pilih nomor konfigurasi (1-3): ", 1, 3);
1666        match choice {
1667            1 => selected_target = "cors".to_string(),
1668            2 => selected_target = "csrf".to_string(),
1669            3 => selected_target = "app".to_string(),
1670            _ => return,
1671        }
1672    }
1673
1674    match selected_target.as_str() {
1675        "cors" => {
1676            let path = std::path::Path::new("src/config/cors.rs");
1677            if path.exists() {
1678                println!("ā„¹ļø  File cors.rs sudah ada di src/config/cors.rs");
1679                return;
1680            }
1681            if let Some(parent) = path.parent() {
1682                let _ = std::fs::create_dir_all(parent);
1683            }
1684            let content = r#"/* ---------------------------------------------------------
1685 * šŸ“‘ LABEL: CORS CONFIGURATION (src/config/cors.rs)
1686 * Berkas konfigurasi tambahan untuk kustomisasi CORS.
1687 * --------------------------------------------------------- */
1688
1689pub struct CorsConfig {
1690    pub allowed_origins: Vec<&'static str>,
1691    pub allowed_methods: Vec<&'static str>,
1692    pub allowed_headers: Vec<&'static str>,
1693}
1694
1695impl Default for CorsConfig {
1696    fn default() -> Self {
1697        Self {
1698            allowed_origins: vec!["*"],
1699            allowed_methods: vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"],
1700            allowed_headers: vec!["*"],
1701        }
1702    }
1703}
1704"#;
1705            if std::fs::write(path, content).is_ok() {
1706                println!("{} {}", "āœ… Berhasil mempublikasikan konfigurasi CORS ke:".green().bold(), path.display().to_string().cyan());
1707                println!("šŸ’” File ini sekarang dapat diimpor untuk menyesuaikan aturan CORS lokal.");
1708            } else {
1709                println!("āŒ Gagal menulis file CORS config.");
1710            }
1711        }
1712        "csrf" => {
1713            let path = std::path::Path::new("src/config/csrf.rs");
1714            if path.exists() {
1715                println!("ā„¹ļø  File csrf.rs sudah ada di src/config/csrf.rs");
1716                return;
1717            }
1718            if let Some(parent) = path.parent() {
1719                let _ = std::fs::create_dir_all(parent);
1720            }
1721            let content = r#"/* ---------------------------------------------------------
1722 * šŸ“‘ LABEL: CSRF CONFIGURATION (src/config/csrf.rs)
1723 * Berkas konfigurasi tambahan untuk perlindungan CSRF.
1724 * --------------------------------------------------------- */
1725
1726pub struct CsrfConfig {
1727    pub except_paths: Vec<&'static str>,
1728}
1729
1730impl Default for CsrfConfig {
1731    fn default() -> Self {
1732        Self {
1733            except_paths: vec![], // Masukkan rute yang dikecualikan dari CSRF di sini
1734        }
1735    }
1736}
1737"#;
1738            if std::fs::write(path, content).is_ok() {
1739                println!("{} {}", "āœ… Berhasil mempublikasikan konfigurasi CSRF ke:".green().bold(), path.display().to_string().cyan());
1740                println!("šŸ’” File ini sekarang dapat diimpor untuk mengecualikan rute tertentu dari CSRF.");
1741            } else {
1742                println!("āŒ Gagal menulis file CSRF config.");
1743            }
1744        }
1745        "app" => {
1746            let path = std::path::Path::new("src/config/app.rs");
1747            if path.exists() {
1748                println!("ā„¹ļø  File app.rs sudah ada di src/config/app.rs");
1749                return;
1750            }
1751            if let Some(parent) = path.parent() {
1752                let _ = std::fs::create_dir_all(parent);
1753            }
1754            let content = r#"/* ---------------------------------------------------------
1755 * šŸ“‘ LABEL: APP CONFIGURATION (src/config/app.rs)
1756 * Berkas konfigurasi tambahan untuk kustomisasi lokasi storage lokal.
1757 * --------------------------------------------------------- */
1758
1759pub const STORAGE_TARGET: &str = "public/storage";
1760pub const STORAGE_SOURCE: &str = "storage/app/public";
1761"#;
1762            if std::fs::write(path, content).is_ok() {
1763                println!("{} {}", "āœ… Berhasil mempublikasikan konfigurasi APP ke:".green().bold(), path.display().to_string().cyan());
1764            } else {
1765                println!("āŒ Gagal menulis file APP config.");
1766            }
1767        }
1768        _ => {
1769            println!("āŒ Target '{}' tidak dikenal untuk di-publish.", selected_target);
1770            println!("šŸ’” Target yang didukung: cors, csrf, app");
1771        }
1772    }
1773}
1774
1775fn get_cargo_package_name() -> String {
1776    if let Ok(content) = std::fs::read_to_string("Cargo.toml") {
1777        for line in content.lines() {
1778            let trimmed = line.trim();
1779            if trimmed.starts_with("name =") || trimmed.starts_with("name=") {
1780                let parts: Vec<&str> = trimmed.split('=').collect();
1781                if parts.len() > 1 {
1782                    let name = parts[1].trim().trim_matches('"').trim_matches('\'');
1783                    return name.to_string();
1784                }
1785            }
1786        }
1787    }
1788    "rustbasic".to_string()
1789}
1790
1791fn get_host_arch() -> String {
1792    // 1. Coba deteksi via command 'uname -m' (macOS / Linux)
1793    if cfg!(any(target_os = "macos", target_os = "linux")) {
1794        if let Ok(output) = std::process::Command::new("uname").arg("-m").output() {
1795            let arch = String::from_utf8_lossy(&output.stdout).trim().to_lowercase();
1796            if !arch.is_empty() {
1797                if arch.contains("aarch64") || arch.contains("arm64") {
1798                    return "aarch64".to_string();
1799                } else if arch.contains("x86_64") || arch.contains("amd64") {
1800                    return "x86_64".to_string();
1801                }
1802                return arch;
1803            }
1804        }
1805    }
1806
1807    // 2. Coba deteksi via environment variable (Windows)
1808    if cfg!(target_os = "windows") {
1809        if let Ok(arch) = std::env::var("PROCESSOR_ARCHITECTURE") {
1810            let arch_lower = arch.to_lowercase();
1811            if arch_lower.contains("amd64") || arch_lower.contains("x64") {
1812                return "x86_64".to_string();
1813            } else if arch_lower.contains("arm64") {
1814                return "aarch64".to_string();
1815            }
1816        }
1817    }
1818
1819    // 3. Fallback ke compile-time constant
1820    let const_arch = std::env::consts::ARCH;
1821    if const_arch == "x86_64" {
1822        "x86_64".to_string()
1823    } else if const_arch == "aarch64" {
1824        "aarch64".to_string()
1825    } else {
1826        const_arch.to_string()
1827    }
1828}
1829