1use std::collections::HashSet;
4use std::path::{Path, PathBuf};
5
6use secrecy::SecretString;
7use serde::{Deserialize, Serialize};
8
9use crate::errors::Result;
10use crate::output::Printer;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct PackageInfo {
16 pub name: String,
17 pub version: String,
18}
19
20pub trait PackageManager: Send + Sync {
21 fn name(&self) -> &str;
22 fn is_available(&self) -> bool;
23 fn can_bootstrap(&self) -> bool;
24 fn bootstrap(&self, printer: &Printer) -> Result<()>;
25 fn installed_packages(&self) -> Result<HashSet<String>>;
26 fn install(&self, packages: &[String], printer: &Printer) -> Result<()>;
27 fn uninstall(&self, packages: &[String], printer: &Printer) -> Result<()>;
28 fn update(&self, printer: &Printer) -> Result<()>;
29
30 fn available_version(&self, package: &str) -> Result<Option<String>>;
33
34 fn path_dirs(&self) -> Vec<String> {
37 Vec::new()
38 }
39
40 fn installed_packages_with_versions(&self) -> Result<Vec<PackageInfo>> {
43 Ok(self
44 .installed_packages()?
45 .into_iter()
46 .map(|name| PackageInfo {
47 name,
48 version: "unknown".into(),
49 })
50 .collect())
51 }
52
53 fn package_aliases(&self, _canonical_name: &str) -> Result<Vec<String>> {
56 Ok(vec![])
57 }
58}
59
60pub struct SystemDrift {
63 pub key: String,
64 pub expected: String,
65 pub actual: String,
66}
67
68pub trait SystemConfigurator: Send + Sync {
69 fn name(&self) -> &str;
71 fn is_available(&self) -> bool;
72
73 fn current_state(&self) -> Result<serde_yaml::Value>;
75
76 fn diff(&self, desired: &serde_yaml::Value) -> Result<Vec<SystemDrift>>;
78
79 fn apply(&self, desired: &serde_yaml::Value, printer: &Printer) -> Result<()>;
81}
82
83use std::collections::BTreeMap;
86
87#[derive(Debug)]
88pub struct FileLayer {
89 pub source_dir: PathBuf,
90 pub origin_source: String,
91 pub priority: u32,
92}
93
94#[derive(Debug)]
95pub struct FileTree {
96 pub files: BTreeMap<PathBuf, FileEntry>,
97}
98
99#[derive(Debug)]
100pub struct FileEntry {
101 pub content_hash: String,
102 pub permissions: Option<u32>,
103 pub is_template: bool,
104 pub source_path: PathBuf,
105 pub origin_source: String,
106}
107
108#[derive(Debug)]
109pub struct FileDiff {
110 pub target: PathBuf,
111 pub kind: FileDiffKind,
112}
113
114#[derive(Debug)]
115pub enum FileDiffKind {
116 Created { source: PathBuf },
117 Modified { source: PathBuf, diff: String },
118 Deleted,
119 PermissionsChanged { current: u32, desired: u32 },
120 Unchanged,
121}
122
123#[derive(Debug, Serialize)]
124pub enum FileAction {
125 Create {
126 source: PathBuf,
127 target: PathBuf,
128 origin: String,
129 strategy: crate::config::FileStrategy,
130 source_hash: Option<String>,
132 },
133 Update {
134 source: PathBuf,
135 target: PathBuf,
136 diff: String,
137 origin: String,
138 strategy: crate::config::FileStrategy,
139 source_hash: Option<String>,
141 },
142 Delete {
143 target: PathBuf,
144 origin: String,
145 },
146 SetPermissions {
147 target: PathBuf,
148 mode: u32,
149 origin: String,
150 },
151 Skip {
152 target: PathBuf,
153 reason: String,
154 origin: String,
155 },
156}
157
158pub trait FileManager: Send + Sync {
159 fn scan_source(&self, layers: &[FileLayer]) -> Result<FileTree>;
160 fn scan_target(&self, paths: &[PathBuf]) -> Result<FileTree>;
161 fn diff(&self, source: &FileTree, target: &FileTree) -> Result<Vec<FileDiff>>;
162 fn apply(&self, actions: &[FileAction], printer: &Printer) -> Result<()>;
163}
164
165#[derive(Debug, Serialize)]
168pub enum PackageAction {
169 Bootstrap {
170 manager: String,
171 method: String,
172 origin: String,
173 },
174 Install {
175 manager: String,
176 packages: Vec<String>,
177 origin: String,
178 },
179 Uninstall {
180 manager: String,
181 packages: Vec<String>,
182 origin: String,
183 },
184 Skip {
185 manager: String,
186 reason: String,
187 origin: String,
188 },
189}
190
191pub trait SecretBackend: Send + Sync {
194 fn name(&self) -> &str;
195 fn is_available(&self) -> bool;
196 fn encrypt_file(&self, path: &Path) -> Result<()>;
197 fn decrypt_file(&self, path: &Path) -> Result<SecretString>;
198 fn edit_file(&self, path: &Path) -> Result<()>;
199}
200
201pub trait SecretProvider: Send + Sync {
204 fn name(&self) -> &str;
205 fn is_available(&self) -> bool;
206 fn resolve(&self, reference: &str) -> Result<SecretString>;
207}
208
209#[derive(Debug, Serialize)]
212pub enum SecretAction {
213 Decrypt {
214 source: PathBuf,
215 target: PathBuf,
216 backend: String,
217 origin: String,
218 },
219 Resolve {
220 provider: String,
221 reference: String,
222 target: PathBuf,
223 origin: String,
224 },
225 ResolveEnv {
228 provider: String,
229 reference: String,
230 envs: Vec<String>,
231 origin: String,
232 },
233 Skip {
234 source: String,
235 reason: String,
236 origin: String,
237 },
238}
239
240pub struct ProviderRegistry {
243 pub package_managers: Vec<Box<dyn PackageManager>>,
244 pub system_configurators: Vec<Box<dyn SystemConfigurator>>,
245 pub file_manager: Option<Box<dyn FileManager>>,
246 pub secret_backend: Option<Box<dyn SecretBackend>>,
247 pub secret_providers: Vec<Box<dyn SecretProvider>>,
248 pub default_file_strategy: crate::config::FileStrategy,
249}
250
251impl ProviderRegistry {
252 pub fn new() -> Self {
253 Self {
254 package_managers: Vec::new(),
255 system_configurators: Vec::new(),
256 file_manager: None,
257 secret_backend: None,
258 secret_providers: Vec::new(),
259 default_file_strategy: crate::config::FileStrategy::Symlink,
260 }
261 }
262
263 pub fn available_package_managers(&self) -> Vec<&dyn PackageManager> {
264 self.package_managers
265 .iter()
266 .filter(|pm| pm.is_available())
267 .map(|pm| pm.as_ref())
268 .collect()
269 }
270
271 pub fn available_system_configurators(&self) -> Vec<&dyn SystemConfigurator> {
272 self.system_configurators
273 .iter()
274 .filter(|sc| sc.is_available())
275 .map(|sc| sc.as_ref())
276 .collect()
277 }
278}
279
280impl Default for ProviderRegistry {
281 fn default() -> Self {
282 Self::new()
283 }
284}
285
286pub fn parse_secret_reference(source: &str) -> Option<(&str, &str)> {
295 if let Some(rest) = source.strip_prefix("1password://") {
296 Some(("1password", rest))
297 } else if let Some(rest) = source.strip_prefix("bitwarden://") {
298 Some(("bitwarden", rest))
299 } else if let Some(rest) = source.strip_prefix("lastpass://") {
300 Some(("lastpass", rest))
301 } else if let Some(rest) = source.strip_prefix("vault://") {
302 Some(("vault", rest))
303 } else {
304 None
305 }
306}
307
308#[cfg(test)]
310pub(crate) struct StubPackageManager {
311 pub name: String,
312 pub available: bool,
313 pub installed: HashSet<String>,
314 pub versions: std::collections::HashMap<String, String>,
315 pub bootstrap_capable: bool,
316}
317
318#[cfg(test)]
319impl StubPackageManager {
320 pub fn new(name: &str) -> Self {
321 Self {
322 name: name.to_string(),
323 available: true,
324 installed: HashSet::new(),
325 versions: std::collections::HashMap::new(),
326 bootstrap_capable: false,
327 }
328 }
329
330 pub fn unavailable(mut self) -> Self {
331 self.available = false;
332 self
333 }
334
335 pub fn bootstrappable(mut self) -> Self {
336 self.bootstrap_capable = true;
337 self
338 }
339
340 pub fn with_installed(mut self, pkgs: &[&str]) -> Self {
341 for p in pkgs {
342 self.installed.insert((*p).to_string());
343 }
344 self
345 }
346
347 pub fn with_package(mut self, pkg: &str, ver: &str) -> Self {
348 self.versions.insert(pkg.to_string(), ver.to_string());
349 self
350 }
351}
352
353#[cfg(test)]
354impl PackageManager for StubPackageManager {
355 fn name(&self) -> &str {
356 &self.name
357 }
358 fn is_available(&self) -> bool {
359 self.available
360 }
361 fn can_bootstrap(&self) -> bool {
362 self.bootstrap_capable
363 }
364 fn bootstrap(&self, _printer: &Printer) -> Result<()> {
365 Ok(())
366 }
367 fn installed_packages(&self) -> Result<HashSet<String>> {
368 Ok(self.installed.clone())
369 }
370 fn install(&self, _packages: &[String], _printer: &Printer) -> Result<()> {
371 Ok(())
372 }
373 fn uninstall(&self, _packages: &[String], _printer: &Printer) -> Result<()> {
374 Ok(())
375 }
376 fn update(&self, _printer: &Printer) -> Result<()> {
377 Ok(())
378 }
379 fn available_version(&self, package: &str) -> Result<Option<String>> {
380 Ok(self.versions.get(package).cloned())
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 #[test]
389 fn registry_filters_available_managers() {
390 let mut registry = ProviderRegistry::new();
391 registry
392 .package_managers
393 .push(Box::new(StubPackageManager::new("mock")));
394 registry
395 .package_managers
396 .push(Box::new(StubPackageManager::new("mock2").unavailable()));
397
398 let available = registry.available_package_managers();
399 assert_eq!(available.len(), 1);
400 assert_eq!(available[0].name(), "mock");
401 }
402
403 #[test]
404 fn empty_registry() {
405 let registry = ProviderRegistry::new();
406 assert!(registry.available_package_managers().is_empty());
407 assert!(registry.available_system_configurators().is_empty());
408 assert!(registry.file_manager.is_none());
409 assert!(registry.secret_backend.is_none());
410 }
411
412 #[test]
413 fn test_default_installed_packages_with_versions_empty() {
414 let mock = StubPackageManager::new("mock");
415 let pkgs = mock.installed_packages_with_versions().unwrap();
416 assert!(pkgs.is_empty());
417 }
418
419 #[test]
420 fn test_default_package_aliases_empty() {
421 let mock = StubPackageManager::new("mock");
422 let aliases = mock.package_aliases("fd").unwrap();
423 assert!(aliases.is_empty());
424 }
425}