Skip to main content

rust_bucket/
cli.rs

1// CLI argument parsing and command dispatch
2
3use clap::{Parser, Subcommand};
4use std::io::{self, BufRead, Write};
5use thiserror::Error;
6
7/// Rust-first project bootstrapper for AI-first engineering
8#[derive(Debug, Parser)]
9#[command(name = "rust-bucket")]
10#[command(about = "Rust-first project bootstrapper for AI-first engineering")]
11#[command(version)]
12pub struct Cli {
13    #[command(subcommand)]
14    pub command: Commands,
15}
16
17/// Available subcommands
18#[derive(Debug, Subcommand)]
19pub enum Commands {
20    /// Apply rust-bucket to the current directory
21    Apply {
22        /// Force overwrite of existing managed files
23        #[arg(long)]
24        force: bool,
25    },
26}
27
28/// CLI-related errors
29#[derive(Debug, Error)]
30pub enum CliError {
31    /// IO error during interactive prompting
32    #[error("IO error: {0}")]
33    Io(#[from] io::Error),
34
35    /// Invalid input provided by the user
36    #[error("Invalid input: {0}")]
37    InvalidInput(String),
38}
39
40/// Prompt the user for a test timeout value
41///
42/// Reads from stdin and validates the input is a positive integer.
43/// Returns 120 as the default if the user provides empty input.
44///
45/// # Errors
46///
47/// Returns `CliError::Io` if reading from stdin fails.
48/// Returns `CliError::InvalidInput` if the input cannot be parsed as a positive integer.
49pub fn prompt_test_timeout() -> Result<u32, CliError> {
50    let stdout = io::stdout();
51    let mut handle = stdout.lock();
52
53    write!(handle, "Enter test timeout in seconds (default: 120): ")?;
54    handle.flush()?;
55
56    let stdin = io::stdin();
57    let mut line = String::new();
58    stdin.lock().read_line(&mut line)?;
59
60    let trimmed = line.trim();
61
62    // Empty input defaults to 120
63    if trimmed.is_empty() {
64        return Ok(120);
65    }
66
67    // Parse the input as u32
68    let timeout = trimmed.parse::<u32>().map_err(|_| {
69        CliError::InvalidInput(format!("'{}' is not a valid positive integer", trimmed))
70    })?;
71
72    // Validate it's positive (non-zero)
73    if timeout == 0 {
74        return Err(CliError::InvalidInput(
75            "Timeout must be a positive integer (greater than 0)".to_string(),
76        ));
77    }
78
79    Ok(timeout)
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn test_cli_parsing() {
88        // Test parsing the apply command
89        let cli = Cli::parse_from(["rust-bucket", "apply"]);
90        match cli.command {
91            Commands::Apply { force } => assert!(!force),
92        }
93
94        // Test parsing the apply command with --force
95        let cli = Cli::parse_from(["rust-bucket", "apply", "--force"]);
96        match cli.command {
97            Commands::Apply { force } => assert!(force),
98        }
99    }
100
101    #[test]
102    fn test_version_flag() {
103        // Test that --version flag is recognized (clap will exit with code 0)
104        let result = Cli::try_parse_from(["rust-bucket", "--version"]);
105        // --version causes clap to print version and exit, which returns an error
106        // of kind DisplayVersion
107        assert!(result.is_err());
108        let err = result.unwrap_err();
109        assert_eq!(err.kind(), clap::error::ErrorKind::DisplayVersion);
110    }
111}