High-performance implementation of plcbundle written in Rust
at main 256 lines 8.5 kB view raw
1use anyhow::Result; 2use clap::{Args, ValueHint}; 3use plcbundle::{BundleManager, constants}; 4use std::path::{Path, PathBuf}; 5 6#[derive(Args)] 7#[command( 8 about = "Initialize a new bundle repository", 9 long_about = "Create a new repository for storing PLC bundle data. This command sets up 10the necessary directory structure and creates an empty index file (plc_bundles.json) 11that will track all bundles in the repository. 12 13During initialization, you'll be prompted to select a PLC directory URL (the source 14of bundle data). You can also specify it directly with --plc to skip the prompt. 15The origin URL is stored in the index and used to verify that bundles come from 16the expected source. 17 18After initialization, use 'sync' to fetch bundles from the PLC directory, or 19'clone' to copy bundles from an existing repository. The repository is ready 20to use immediately after initialization.", 21 help_template = crate::clap_help!( 22 examples: " # Initialize in current directory\n \ 23 {bin} init\n\n \ 24 # Initialize in specific directory\n \ 25 {bin} init /path/to/bundles\n\n \ 26 # Set PLC directory URL\n \ 27 {bin} init --plc https://plc.directory\n\n \ 28 # Force reinitialize existing repository\n \ 29 {bin} init --force" 30 ) 31)] 32pub struct InitCommand { 33 /// Directory to initialize (default: current directory) 34 #[arg(default_value = ".", value_hint = ValueHint::DirPath)] 35 pub dir: PathBuf, 36 37 /// PLC Directory URL (if not provided, will prompt interactively) 38 #[arg(long, value_hint = ValueHint::Url)] 39 pub plc: Option<String>, 40 41 /// Origin identifier for this repository (deprecated: use --plc instead) 42 #[arg(long, hide = true, value_hint = ValueHint::Url)] 43 pub origin: Option<String>, 44 45 /// Force initialization even if directory already exists 46 #[arg(short, long)] 47 pub force: bool, 48} 49 50pub fn run(cmd: InitCommand) -> Result<()> { 51 // Get absolute path for display 52 // Normalize the path to avoid trailing dots or other artifacts 53 let dir = if cmd.dir.is_absolute() { 54 cmd.dir.canonicalize().unwrap_or_else(|_| cmd.dir.clone()) 55 } else if cmd.dir == PathBuf::from(".") { 56 // Special case: if dir is ".", just use current directory directly 57 std::env::current_dir()? 58 } else { 59 let joined = std::env::current_dir()?.join(&cmd.dir); 60 joined.canonicalize().unwrap_or(joined) 61 }; 62 63 // Check if directory is already initialized (unless --force is used) 64 let index_path = dir.join("plc_bundles.json"); 65 if index_path.exists() && !cmd.force { 66 return Err(already_initialized_error(&dir)); 67 } 68 69 // Determine PLC Directory URL 70 let plc_url = if let Some(plc) = cmd.plc { 71 // Use provided --plc flag 72 plc 73 } else if let Some(origin) = cmd.origin { 74 // Backward compatibility: use --origin if provided 75 origin 76 } else { 77 // Interactive prompt 78 prompt_plc_directory_url()? 79 }; 80 81 // Initialize repository using BundleManager API 82 let initialized = BundleManager::init_repository(&dir, plc_url.clone(), cmd.force)?; 83 84 if !initialized { 85 // This shouldn't happen since we checked above, but handle it just in case 86 return Err(already_initialized_error(&dir)); 87 } 88 89 // Check if user needs to cd to the directory 90 let current_dir = std::env::current_dir()?; 91 let need_cd = current_dir != dir; 92 93 println!("✓ Initialized PLC bundle repository"); 94 println!(" Location: {}", dir.display()); 95 println!(" Origin: {}", plc_url); 96 println!(" Index: plc_bundles.json"); 97 98 if need_cd { 99 println!("\n⚠ Warning: You initialized in a different directory"); 100 println!(" Please run the following command first:"); 101 println!(" cd {}", dir.display()); 102 } 103 104 println!("\nNext steps:"); 105 println!( 106 " {} sync # Fetch bundles from PLC directory", 107 crate::constants::BINARY_NAME 108 ); 109 println!( 110 " {} info # Show repository info", 111 crate::constants::BINARY_NAME 112 ); 113 println!( 114 " {} mempool status # Check mempool status", 115 crate::constants::BINARY_NAME 116 ); 117 118 Ok(()) 119} 120 121/// Create an error for when repository is already initialized 122fn already_initialized_error(dir: &Path) -> anyhow::Error { 123 anyhow::anyhow!( 124 "Repository already initialized at: {}\n\nUse --force to reinitialize", 125 dir.display() 126 ) 127} 128 129fn prompt_plc_directory_url() -> Result<String> { 130 use dialoguer::{Select, theme::ColorfulTheme}; 131 132 println!("\n┌ Welcome to {}!", constants::BINARY_NAME); 133 println!(""); 134 println!("◆ Which PLC Directory would you like to use?"); 135 println!(""); 136 137 let options = vec![ 138 format!("plc.directory ({})", constants::DEFAULT_PLC_DIRECTORY_URL), 139 "local (for local development/testing)".to_string(), 140 "Custom (enter your own URL)".to_string(), 141 ]; 142 143 let selection = Select::with_theme(&ColorfulTheme::default()) 144 .with_prompt("") 145 .default(0) 146 .items(&options) 147 .interact() 148 .map_err(|e| anyhow::anyhow!("Failed to read user input: {}", e))?; 149 150 let url = match selection { 151 0 => constants::DEFAULT_PLC_DIRECTORY_URL.to_string(), 152 1 => constants::DEFAULT_ORIGIN.to_string(), 153 2 => { 154 use dialoguer::Input; 155 Input::with_theme(&ColorfulTheme::default()) 156 .with_prompt("Enter PLC Directory URL") 157 .validate_with(|input: &String| -> Result<(), &str> { 158 if input.trim().is_empty() { 159 Err("URL cannot be empty") 160 } else if !input.starts_with("http://") && !input.starts_with("https://") { 161 Err("URL must start with http:// or https://") 162 } else { 163 Ok(()) 164 } 165 }) 166 .interact_text() 167 .map_err(|e| anyhow::anyhow!("Failed to read user input: {}", e))? 168 } 169 _ => unreachable!(), 170 }; 171 172 println!(""); 173 println!("\n{}", "".repeat(60)); // Add clear separator line 174 175 Ok(url) 176} 177 178#[cfg(test)] 179mod tests { 180 use super::*; 181 use plcbundle::index::Index; 182 use tempfile::TempDir; 183 184 #[test] 185 fn test_init_creates_index() { 186 let temp = TempDir::new().unwrap(); 187 let cmd = InitCommand { 188 dir: temp.path().to_path_buf(), 189 plc: Some("test".to_string()), 190 origin: None, 191 force: false, 192 }; 193 194 run(cmd).unwrap(); 195 196 let index = Index::load(temp.path()).unwrap(); 197 assert_eq!(index.origin, "test"); 198 assert_eq!(index.last_bundle, 0); 199 } 200 201 #[test] 202 fn test_init_prevents_overwrite() { 203 let temp = TempDir::new().unwrap(); 204 205 // First init 206 let cmd = InitCommand { 207 dir: temp.path().to_path_buf(), 208 plc: Some("first".to_string()), 209 origin: None, 210 force: false, 211 }; 212 run(cmd).unwrap(); 213 214 // Second init without force should fail 215 let cmd = InitCommand { 216 dir: temp.path().to_path_buf(), 217 plc: Some("second".to_string()), 218 origin: None, 219 force: false, 220 }; 221 assert!( 222 run(cmd).is_err(), 223 "Should fail when trying to initialize already-initialized repository without --force" 224 ); 225 226 // Verify the origin is still "first" (not overwritten) 227 let index = Index::load(temp.path()).unwrap(); 228 assert_eq!(index.origin, "first"); 229 } 230 231 #[test] 232 fn test_init_force_overwrites() { 233 let temp = TempDir::new().unwrap(); 234 235 // First init 236 let cmd = InitCommand { 237 dir: temp.path().to_path_buf(), 238 plc: Some("first".to_string()), 239 origin: None, 240 force: false, 241 }; 242 run(cmd).unwrap(); 243 244 // Second init with force 245 let cmd = InitCommand { 246 dir: temp.path().to_path_buf(), 247 plc: Some("second".to_string()), 248 origin: None, 249 force: true, 250 }; 251 run(cmd).unwrap(); 252 253 let index = Index::load(temp.path()).unwrap(); 254 assert_eq!(index.origin, "second"); // Overwritten 255 } 256}