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 run_native(run_android: bool, run_desktop: bool) {
592    if run_android {
593        println!("šŸš€ Memulai RustBasic Android Wrapper (Native implementation)...");
594
595        // 1. Setup environment
596        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        // 3. build JNI
659        if !compile_jni_libraries() {
660            return;
661        }
662
663        // 4. local.properties
664        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        // 5. gradlew assembleDebug
673        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        // 6. adb install
690        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        // 7. adb reverse
701        let vite_port = "5173"; // default
702        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        // 8. adb shell am start
710        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
926// ============================================================
927// BUILD
928// ============================================================
929fn 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    // Cek Docker tersedia
1011    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    // Build JNI
1166    println!("\nšŸ”Ø Membangun JNI library (Native implementation)...");
1167    if !compile_jni_libraries() {
1168        println!("āŒ JNI build gagal.");
1169        return;
1170    }
1171
1172    // Setup environment
1173    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    // 1. Konfigurasi Image & Pengiriman
1222    let image_name = prompt_string("šŸ‘‰ Masukkan Nama/Tag Docker Image (default: rustbasic:latest): ", "rustbasic:latest");
1223
1224    // Cek apakah Docker image sudah ada
1225    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    // 2. Ekspor ke tar
1244    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    // 3. Konfigurasi Pengiriman
1260    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    // Hapus file tar lokal
1302    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    // 4. Eksekusi SSH otomatis di server jika disetujui
1317    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
1413// ============================================================
1414// PUBLISH
1415// ============================================================
1416fn 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