1use crate::cli;
4use crate::config::{Config, ConfigError};
5use crate::generator::{self, GeneratorError};
6use crate::templates::{self, TemplateError};
7use crate::verify::{self, VerifyError, VerifyReport};
8use std::path::{Path, PathBuf};
9use thiserror::Error;
10
11#[derive(Debug)]
13pub struct ApplyResult {
14 pub files_generated: Vec<PathBuf>,
15 pub verification: VerifyReport,
16}
17
18#[derive(Debug, Error)]
20pub enum ApplyError {
21 #[error("Not a Rust crate: Cargo.toml not found in target directory")]
23 NotRustCrate,
24
25 #[error("Not a git repository: .git/ directory not found")]
27 NotGitRepo,
28
29 #[error("Conflicting files detected: {}", .0.iter().map(|p| p.display().to_string()).collect::<Vec<_>>().join(", "))]
31 ConflictingFiles(Vec<PathBuf>),
32
33 #[error("Configuration error: {0}")]
35 ConfigError(#[from] ConfigError),
36
37 #[error("Generator error: {0}")]
39 GeneratorError(#[from] GeneratorError),
40
41 #[error("Verification error: {0}")]
43 VerifyError(#[from] VerifyError),
44
45 #[error("Template error: {0}")]
47 TemplateError(#[from] TemplateError),
48
49 #[error("CLI error: {0}")]
51 CliError(#[from] cli::CliError),
52}
53
54pub fn apply_init(target_dir: &Path, force: bool) -> Result<ApplyResult, ApplyError> {
71 let cargo_toml = target_dir.join("Cargo.toml");
72 if !cargo_toml.exists() {
73 return Err(ApplyError::NotRustCrate);
74 }
75
76 let git_dir = target_dir.join(".git");
77 if !git_dir.exists() {
78 return Err(ApplyError::NotGitRepo);
79 }
80
81 let conflicts = generator::check_conflicts(target_dir);
82 if !conflicts.is_empty() {
83 if !force {
84 return Err(ApplyError::ConflictingFiles(conflicts));
85 }
86 eprintln!(
87 "Warning: Overwriting {} existing file(s) due to --force flag",
88 conflicts.len()
89 );
90 }
91
92 let test_timeout = cli::prompt_test_timeout()?;
93
94 let config = Config {
95 rust_bucket_version: env!("CARGO_PKG_VERSION").to_string(),
96 test_timeout,
97 project_name: "Rust-Bucket".to_string(),
98 };
99
100 let config_path = target_dir.join("rust-bucket.toml");
101 config.save(&config_path)?;
102
103 let (_temp_dir, temp_path) = templates::extract_to_temp()?;
104
105 let mut files_generated = generator::render(&temp_path, target_dir, &config, force)?;
106
107 let claude_symlink = generator::create_claude_symlink(target_dir)?;
108 files_generated.push(claude_symlink);
109
110 generator::ensure_gitignore(target_dir)?;
111
112 let seeded = generator::seed_files(&temp_path, target_dir, &config)?;
113 files_generated.extend(seeded);
114
115 let verification = verify::run_all(target_dir)?;
116
117 Ok(ApplyResult {
118 files_generated,
119 verification,
120 })
121}
122
123pub fn apply_update(target_dir: &Path) -> Result<ApplyResult, ApplyError> {
139 let cargo_toml = target_dir.join("Cargo.toml");
140 if !cargo_toml.exists() {
141 return Err(ApplyError::NotRustCrate);
142 }
143
144 let git_dir = target_dir.join(".git");
145 if !git_dir.exists() {
146 return Err(ApplyError::NotGitRepo);
147 }
148
149 let config_path = target_dir.join("rust-bucket.toml");
150 let mut config = Config::load(&config_path)?;
151
152 let current_version = env!("CARGO_PKG_VERSION");
153 if config.rust_bucket_version != current_version {
154 eprintln!(
155 "Note: Config was last generated with rust-bucket v{}, updating to v{}",
156 config.rust_bucket_version, current_version
157 );
158 }
159
160 config.rust_bucket_version = current_version.to_string();
161
162 config.save(&config_path)?;
163
164 let (_temp_dir, temp_path) = templates::extract_to_temp()?;
165
166 let mut files_generated = generator::render(&temp_path, target_dir, &config, true)?;
167
168 let claude_symlink = generator::create_claude_symlink(target_dir)?;
169 files_generated.push(claude_symlink);
170
171 generator::ensure_gitignore(target_dir)?;
172
173 let seeded = generator::seed_files(&temp_path, target_dir, &config)?;
174 files_generated.extend(seeded);
175
176 let verification = verify::run_all(target_dir)?;
177
178 Ok(ApplyResult {
179 files_generated,
180 verification,
181 })
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use std::fs;
188 use tempfile::TempDir;
189
190 fn create_test_rust_crate(path: &Path) -> Result<(), Box<dyn std::error::Error>> {
191 fs::write(
193 path.join("Cargo.toml"),
194 r#"[package]
195name = "test-crate"
196version = "0.1.0"
197edition = "2021"
198"#,
199 )?;
200
201 fs::create_dir(path.join(".git"))?;
203
204 let src_dir = path.join("src");
206 fs::create_dir(&src_dir)?;
207 fs::write(src_dir.join("lib.rs"), "// test lib\n")?;
208 Ok(())
209 }
210
211 #[test]
212 fn test_apply_init_not_rust_crate() -> Result<(), Box<dyn std::error::Error>> {
213 let temp_dir = TempDir::new()?;
214 let result = apply_init(temp_dir.path(), false);
215
216 assert!(result.is_err());
217 assert!(
218 matches!(result.unwrap_err(), ApplyError::NotRustCrate),
219 "Expected NotRustCrate error"
220 );
221 Ok(())
222 }
223
224 #[test]
225 fn test_apply_init_not_git_repo() -> Result<(), Box<dyn std::error::Error>> {
226 let temp_dir = TempDir::new()?;
227
228 fs::write(
230 temp_dir.path().join("Cargo.toml"),
231 "[package]\nname = \"test\"",
232 )?;
233
234 let result = apply_init(temp_dir.path(), false);
235
236 assert!(result.is_err());
237 assert!(
238 matches!(result.unwrap_err(), ApplyError::NotGitRepo),
239 "Expected NotGitRepo error"
240 );
241 Ok(())
242 }
243
244 #[test]
245 fn test_apply_init_conflicts_without_force() -> Result<(), Box<dyn std::error::Error>> {
246 let temp_dir = TempDir::new()?;
247 create_test_rust_crate(temp_dir.path())?;
248
249 fs::write(temp_dir.path().join("AGENTS.md"), "existing content")?;
251
252 let result = apply_init(temp_dir.path(), false);
253
254 assert!(result.is_err());
255 let err = result.unwrap_err();
256 assert!(
257 matches!(&err, ApplyError::ConflictingFiles(_)),
258 "Expected ConflictingFiles error"
259 );
260 if let ApplyError::ConflictingFiles(conflicts) = err {
261 assert!(!conflicts.is_empty());
262 assert!(
263 conflicts
264 .iter()
265 .any(|p| p.file_name().is_some_and(|n| n == "AGENTS.md"))
266 );
267 }
268 Ok(())
269 }
270}