circuitpython_deploy/
cli.rs

1use clap::Parser;
2use std::path::PathBuf;
3
4#[derive(Parser, Debug)]
5#[command(name = "cpd")]
6#[command(version = "0.1.0")]
7#[command(about = "Fast, reliable CircuitPython project deployment")]
8#[command(long_about = "A command-line tool for deploying CircuitPython projects from your development environment to CircuitPython boards.
9
10Features:
11  • Automatic board detection and smart file filtering
12  • .cpdignore support with gitignore-style patterns  
13  • Backup functionality with progress tracking
14  • Cross-platform support (Windows, macOS, Linux)
15  • High-performance deployment with visual feedback
16
17Examples:
18  cpd                           Deploy current directory to auto-detected board
19  cpd --list-boards            Show all detected CircuitPython boards
20  cpd --dry-run                Preview deployment without copying files
21  cpd --backup ./backup        Create backup before deployment
22  cpd --board /media/CIRCUITPY  Deploy to specific board path")]
23pub struct Cli {
24    /// Path to the project directory to deploy (defaults to current directory)
25    #[arg(value_name = "PROJECT_DIR")]
26    pub project_dir: Option<PathBuf>,
27
28    /// Specify the board drive/mount point manually (e.g., E:\, /media/CIRCUITPY)
29    #[arg(short = 'b', long = "board", value_name = "BOARD_PATH")]
30    pub board_path: Option<PathBuf>,
31
32    /// Backup existing board files before deployment
33    #[arg(short = 'B', long = "backup", value_name = "BACKUP_DIR")]
34    pub backup_dir: Option<PathBuf>,
35
36    /// Preview deployment without copying files (safe mode)
37    #[arg(short = 'n', long = "dry-run")]
38    pub dry_run: bool,
39
40    /// Show detailed information during deployment
41    #[arg(short = 'v', long = "verbose")]
42    pub verbose: bool,
43
44    /// Force deployment even if board validation fails
45    #[arg(short = 'f', long = "force")]
46    pub force: bool,
47
48    /// Skip interactive confirmation prompts
49    #[arg(short = 'y', long = "yes")]
50    pub assume_yes: bool,
51
52    /// List all detected CircuitPython boards and exit
53    #[arg(short = 'l', long = "list-boards")]
54    pub list_boards: bool,
55}
56
57impl Cli {
58    pub fn parse_args() -> Self {
59        Self::parse()
60    }
61
62    pub fn project_dir(&self) -> PathBuf {
63        self.project_dir
64            .clone()
65            .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
66    }
67
68    pub fn validate(&self) -> crate::error::Result<()> {
69        use crate::error::CpdError;
70
71        // Validate project directory exists
72        let project_dir = self.project_dir();
73        if !project_dir.exists() {
74            return Err(CpdError::Configuration {
75                message: format!("Project directory does not exist: {}", project_dir.display()),
76            });
77        }
78
79        if !project_dir.is_dir() {
80            return Err(CpdError::Configuration {
81                message: format!("Project path is not a directory: {}", project_dir.display()),
82            });
83        }
84
85        // Validate board path if specified
86        if let Some(board_path) = &self.board_path {
87            if !board_path.exists() {
88                return Err(CpdError::InvalidBoardPath {
89                    path: board_path.display().to_string(),
90                });
91            }
92
93            if !board_path.is_dir() {
94                return Err(CpdError::InvalidBoardPath {
95                    path: format!("{} is not a directory", board_path.display()),
96                });
97            }
98        }
99
100        // Validate backup directory if specified
101        if let Some(backup_dir) = &self.backup_dir {
102            if backup_dir.exists() && !backup_dir.is_dir() {
103                return Err(CpdError::Configuration {
104                    message: format!("Backup path exists but is not a directory: {}", backup_dir.display()),
105                });
106            }
107        }
108
109        Ok(())
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use std::env;
117
118    #[test]
119    fn test_default_project_dir() {
120        let cli = Cli {
121            project_dir: None,
122            board_path: None,
123            backup_dir: None,
124            dry_run: false,
125            verbose: false,
126            force: false,
127            assume_yes: false,
128            list_boards: false,
129        };
130
131        let current_dir = env::current_dir().unwrap();
132        assert_eq!(cli.project_dir(), current_dir);
133    }
134
135    #[test]
136    fn test_explicit_project_dir() {
137        let test_path = PathBuf::from("/test/path");
138        let cli = Cli {
139            project_dir: Some(test_path.clone()),
140            board_path: None,
141            backup_dir: None,
142            dry_run: false,
143            verbose: false,
144            force: false,
145            assume_yes: false,
146            list_boards: false,
147        };
148
149        assert_eq!(cli.project_dir(), test_path);
150    }
151}