circuitpython_deploy/
cli.rs1use 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 #[arg(value_name = "PROJECT_DIR")]
26 pub project_dir: Option<PathBuf>,
27
28 #[arg(short = 'b', long = "board", value_name = "BOARD_PATH")]
30 pub board_path: Option<PathBuf>,
31
32 #[arg(short = 'B', long = "backup", value_name = "BACKUP_DIR")]
34 pub backup_dir: Option<PathBuf>,
35
36 #[arg(short = 'n', long = "dry-run")]
38 pub dry_run: bool,
39
40 #[arg(short = 'v', long = "verbose")]
42 pub verbose: bool,
43
44 #[arg(short = 'f', long = "force")]
46 pub force: bool,
47
48 #[arg(short = 'y', long = "yes")]
50 pub assume_yes: bool,
51
52 #[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 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 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 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}