Skip to main content

rusty_stack/bootstrap/
launcher.rs

1//! run_rusty_stack.sh equivalent — TUI launcher.
2//!
3//! Ports `scripts/run_rusty_stack.sh` (70 lines) to native Rust.
4//! Builds the Rust binary (if needed) and launches it.
5//!
6//! # Validation Assertions
7//!
8//! - **VAL-VBA-013**: run_rusty_stack.sh equivalent — TUI launcher
9
10use crate::installers::common::utils::{print_error, print_step, print_success};
11use std::path::PathBuf;
12use std::process::Command;
13
14// ===========================================================================
15// Types
16// ===========================================================================
17
18/// Build mode for the TUI launcher.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum BuildMode {
21    Debug,
22    #[default]
23    Release,
24}
25
26impl std::fmt::Display for BuildMode {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            BuildMode::Debug => write!(f, "debug"),
30            BuildMode::Release => write!(f, "release"),
31        }
32    }
33}
34
35/// Result of the TUI launcher.
36#[derive(Debug, Clone)]
37pub struct LaunchResult {
38    /// Whether the build and launch preparation was successful.
39    pub success: bool,
40    /// Path to the built binary.
41    pub binary_path: Option<String>,
42    /// Any errors encountered.
43    pub errors: Vec<String>,
44}
45
46impl LaunchResult {
47    /// Create a successful result.
48    pub fn success(binary_path: String) -> Self {
49        Self {
50            success: true,
51            binary_path: Some(binary_path),
52            errors: Vec::new(),
53        }
54    }
55
56    /// Create a failure result.
57    pub fn failure(error: String) -> Self {
58        Self {
59            success: false,
60            binary_path: None,
61            errors: vec![error],
62        }
63    }
64}
65
66// ===========================================================================
67// Core Logic
68// ===========================================================================
69
70/// Build and launch the rusty-stack TUI.
71///
72/// This is the Rust equivalent of `run_rusty_stack.sh`:
73/// 1. Detect project root and rusty-stack directory
74/// 2. Build in the specified mode (debug/release)
75/// 3. Return the binary path (caller can exec it)
76///
77/// # Validation
78///
79/// - **VAL-VBA-013**: TUI launcher builds and executes Rust binary
80pub fn launch_tui(build_mode: BuildMode, extra_args: &[String]) -> LaunchResult {
81    // Detect project root
82    let project_root = detect_project_root();
83    let rusty_stack_dir = project_root.join("rusty-stack");
84
85    if !rusty_stack_dir.exists() {
86        let msg = format!(
87            "Rust project directory not found: {}",
88            rusty_stack_dir.display()
89        );
90        print_error(&msg);
91        return LaunchResult::failure(msg);
92    }
93
94    print_step(&format!("Build mode: {build_mode}"));
95
96    // Build the binary
97    let (exec_path, build_args) = match build_mode {
98        BuildMode::Debug => ("target/debug/rusty-stack", vec!["build"]),
99        BuildMode::Release => ("target/release/rusty-stack", vec!["build", "--release"]),
100    };
101
102    print_step(&format!("Building with cargo {}...", build_mode));
103
104    let output = Command::new("cargo")
105        .args(&build_args)
106        .current_dir(&rusty_stack_dir)
107        .stdout(std::process::Stdio::inherit())
108        .stderr(std::process::Stdio::inherit())
109        .output();
110
111    match output {
112        Ok(output) if output.status.success() => {
113            let binary_path = rusty_stack_dir.join(exec_path);
114            if !binary_path.exists() {
115                let msg = format!("Binary not found after build: {}", binary_path.display());
116                print_error(&msg);
117                return LaunchResult::failure(msg);
118            }
119            print_success(&format!("Built successfully: {}", binary_path.display()));
120            print_step(&format!("Launching {}", exec_path));
121
122            // Return the binary path and args for the caller to exec
123            // (In the original script, `exec` replaces the process — in Rust
124            // we return the path so the CLI handler can exec it)
125            let _ = extra_args; // passed through to exec
126            LaunchResult::success(binary_path.to_string_lossy().to_string())
127        }
128        Ok(output) => {
129            let stderr = String::from_utf8_lossy(&output.stderr);
130            let msg = format!("Build failed: {stderr}");
131            print_error(&msg);
132            LaunchResult::failure(msg)
133        }
134        Err(e) => {
135            let msg = format!("Failed to run cargo: {e}");
136            print_error(&msg);
137            LaunchResult::failure(msg)
138        }
139    }
140}
141
142// ===========================================================================
143// Helpers
144// ===========================================================================
145
146/// Detect the project root directory.
147///
148/// Looks for `rusty-stack/` subdirectory in:
149/// 1. Current directory
150/// 2. Parent of current directory
151/// 3. Parent of parent (for scripts/ directory context)
152fn detect_project_root() -> PathBuf {
153    let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
154
155    // Check current dir
156    if cwd.join("rusty-stack").exists() {
157        return cwd;
158    }
159
160    // Check parent
161    if let Some(parent) = cwd.parent() {
162        if parent.join("rusty-stack").exists() {
163            return parent.to_path_buf();
164        }
165
166        // Check grandparent (for scripts/ context)
167        if let Some(grandparent) = parent.parent() {
168            if grandparent.join("rusty-stack").exists() {
169                return grandparent.to_path_buf();
170            }
171        }
172    }
173
174    // Fallback — use current directory
175    cwd
176}
177
178// ===========================================================================
179// Tests
180// ===========================================================================
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_build_mode_default_is_release() {
188        assert_eq!(BuildMode::default(), BuildMode::Release);
189    }
190
191    #[test]
192    fn test_build_mode_display() {
193        assert_eq!(format!("{}", BuildMode::Debug), "debug");
194        assert_eq!(format!("{}", BuildMode::Release), "release");
195    }
196
197    #[test]
198    fn test_launch_result_success() {
199        let result = LaunchResult::success("/path/to/rusty-stack".to_string());
200        assert!(result.success);
201        assert_eq!(result.binary_path, Some("/path/to/rusty-stack".to_string()));
202        assert!(result.errors.is_empty());
203    }
204
205    #[test]
206    fn test_launch_result_failure() {
207        let result = LaunchResult::failure("Build error".to_string());
208        assert!(!result.success);
209        assert!(result.binary_path.is_none());
210        assert_eq!(result.errors.len(), 1);
211    }
212
213    #[test]
214    fn test_detect_project_root_no_panic() {
215        // Should not panic even if rusty-stack dir doesn't exist
216        let root = detect_project_root();
217        // Just verify it returns something
218        assert!(!root.as_os_str().is_empty());
219    }
220
221    #[test]
222    fn test_launch_tui_returns_result() {
223        // This will likely fail if rusty-stack dir doesn't exist in CWD parent,
224        // but should not panic
225        let result = launch_tui(BuildMode::Release, &[]);
226        // Just verify it returns a result without panicking
227        let _ = result.success;
228    }
229}