forked from
atscan.net/plcbundle-rs
High-performance implementation of plcbundle written in Rust
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}