Skip to main content

zoi_cli/cmd/
sync.rs

1//! Logic for the `sync` command.
2//!
3//! This module provides commands for syncing package databases from registries,
4//! and managing the list of configured registries.
5
6use anyhow::Result;
7use colored::Colorize;
8
9use crate::cli::SetupScope;
10use crate::pkg;
11
12/// Run the sync command to update package databases.
13///
14/// # Errors
15///
16/// Returns an error if the sync process fails.
17pub fn run(
18    verbose: bool,
19    fallback: bool,
20    no_pm: bool,
21    force: bool,
22    scope: Option<SetupScope>
23) -> Result<()> {
24    println!("{} Syncing package databases...", "::".bold().blue());
25
26    if force {
27        println!(
28            "{} Force sync enabled, removing existing databases...",
29            "::".bold().yellow()
30        );
31    }
32
33    let pkg_scope = match scope {
34        Some(SetupScope::User) => Some(crate::pkg::types::Scope::User),
35        Some(SetupScope::System) => Some(crate::pkg::types::Scope::System),
36        None => None
37    };
38
39    pkg::sync::run(verbose, fallback, no_pm, force, pkg_scope)?;
40
41    println!("{}", "Sync complete.".green());
42    Ok(())
43}
44
45/// Run a project-local sync command.
46///
47/// # Errors
48///
49/// Returns an error if the project-local sync process fails.
50pub fn run_local(
51    verbose: bool,
52    fallback: bool,
53    force: bool,
54    frozen: bool
55) -> Result<()> {
56    if frozen {
57        crate::pkg::frozen::set_frozen(true);
58    }
59    println!(
60        "{} Syncing project-local package databases...",
61        "::".bold().blue()
62    );
63
64    pkg::sync::run_local(verbose, fallback, force, frozen)?;
65
66    println!("{}", "Local sync complete.".green());
67    Ok(())
68}
69
70/// Print a warning for non-official (third-party) registries.
71fn warn_third_party(name: &str, registry_type: &str) {
72    if registry_type != "official" {
73        eprintln!(
74            "{} {} is a third-party registry. Use with caution and verify its \
75             authenticity before installing packages.",
76            "!".yellow().bold(),
77            name.cyan()
78        );
79    }
80}
81
82/// Set the default registry by handle or URL.
83///
84/// When a built-in registry handle is given, the registry is resolved from the
85/// pre-defined YAML and its metadata is used. A local directory or an HTTP URL
86/// is also accepted. The `default` keyword resolves to the built-in `set: true`
87/// registry.
88///
89/// Only one built-in registry may be marked as the "set" (default) registry at
90/// a time.
91///
92/// # Errors
93///
94/// Returns an error if the configuration cannot be updated or the one-set rule
95/// is violated.
96pub fn set_registry(url_or_handle: &str) -> Result<()> {
97    // Resolve the "default" keyword to the built-in `set: true` registry.
98    let handle_or_url_storage;
99    let handle_or_url = if url_or_handle == "default" {
100        handle_or_url_storage = pkg::builtin_registries::get_set()?
101            .map_or_else(|| "zoidberg".to_string(), |r| r.handle);
102        &handle_or_url_storage
103    } else {
104        url_or_handle
105    };
106
107    // --- resolve built-in if possible ---
108    let builtin = pkg::builtin_registries::get(handle_or_url);
109
110    // Print the third-party warning for the resolved registry
111    if let Some(ref reg) = builtin {
112        warn_third_party(&reg.name, &reg.registry_type);
113    }
114
115    // The config helper (set_default_registry) handles writing the user config;
116    // if the handle matches a built-in registry it populates the metadata
117    // fields automatically.
118    pkg::config::set_default_registry(handle_or_url)?;
119
120    // Determine what to display to the user
121    let (display_handle, display_url) = match builtin {
122        Some(ref reg) => (reg.handle.as_str(), reg.git.as_str()),
123        None => (handle_or_url, handle_or_url)
124    };
125
126    println!("Default registry set to: {}", display_handle.cyan());
127    if !display_url.is_empty() && display_url != display_handle {
128        println!("  Git: {display_url}");
129    }
130    println!("The new registry will be used the next time you run 'zoi sync'");
131    Ok(())
132}
133
134/// Add a new registry by handle or URL.
135///
136/// When a built-in registry handle is given (e.g. `docker`), the registry is
137/// resolved from the pre-defined YAML and its metadata is used. A local
138/// directory or an HTTP URL is also accepted. The `default` keyword resolves to
139/// the built-in `set: true` registry.
140///
141/// # Errors
142///
143/// Returns an error if the directory path is invalid or if the configuration
144/// cannot be updated.
145pub fn add_registry(handle_or_url: &str) -> Result<()> {
146    // Resolve the "default" keyword to the built-in `set: true` registry.
147    let resolved_storage;
148    let handle_or_url = if handle_or_url == "default" {
149        resolved_storage = pkg::builtin_registries::get_set()?
150            .map_or_else(|| "zoidberg".to_string(), |r| r.handle);
151        &resolved_storage
152    } else {
153        handle_or_url
154    };
155
156    // Check for local directories first
157    let path = std::path::Path::new(handle_or_url);
158    if path.is_dir() {
159        let final_url =
160            std::fs::canonicalize(path)?.to_string_lossy().to_string();
161        pkg::config::add_added_registry(&final_url)?;
162        let url_cyan = final_url.cyan();
163        println!("Registry '{url_cyan}' added.");
164        println!("It will be synced on the next 'zoi sync' run.");
165        return Ok(());
166    }
167
168    // Try resolving as a built-in handle
169    if let Some(reg) = pkg::builtin_registries::get(handle_or_url) {
170        warn_third_party(&reg.name, &reg.registry_type);
171
172        pkg::config::add_added_registry(handle_or_url)?;
173        println!("Registry {} added.", reg.handle.cyan());
174        println!("  Name: {}", reg.name);
175        println!("  Git: {}", reg.git);
176        println!("It will be synced on the next 'zoi sync' run.");
177        return Ok(());
178    }
179
180    // Fallback: treat as a raw URL
181    pkg::config::add_added_registry(handle_or_url)?;
182    let url_cyan = handle_or_url.cyan();
183    println!("Registry '{url_cyan}' added.");
184    println!("It will be synced on the next 'zoi sync' run.");
185    Ok(())
186}
187
188/// Remove a registry by its handle or URL.
189///
190/// # Errors
191///
192/// Returns an error if the registry cannot be removed from the configuration.
193pub fn remove_registry(handle: &str) -> Result<()> {
194    pkg::config::remove_added_registry(handle)?;
195    let handle_cyan = handle.cyan();
196    println!("Registry '{handle_cyan}' removed.");
197    Ok(())
198}
199
200/// List all configured registries and show which built-in registries are
201/// available but not yet added.
202///
203/// Output format:
204///
205/// ```text
206/// :: Configured Registries
207/// [Set] zoidberg: Zoidberg
208///       - Official Zoi packages repository
209///       - Git: https://gitlab.com/zillowe/zillwen/zusty/zoidberg
210/// [Added] registry-handle: Registry Name
211///         - Registry description
212///         - Git: https://github/registry/database
213///
214/// :: Available Registries
215/// [Unadded] registry-handle: Registry Name
216///           - Registry description
217///           - Git: https://github/registry/database
218/// ```
219///
220/// # Errors
221///
222/// Returns an error if the configuration cannot be read.
223pub fn list_registries() -> Result<()> {
224    let config = pkg::config::read_config()?;
225    let all_builtins = pkg::builtin_registries::load_all();
226
227    // Collect handles that are already configured (set or added)
228    let mut configured_handles: Vec<&str> = Vec::new();
229
230    println!("{} Configured Registries", "::".bold().blue());
231
232    // --- set registry (default_registry) ---
233    if let Some(ref default) = config.default_registry {
234        let handle = &default.handle;
235        let display_name = default.name.as_deref().unwrap_or(handle);
236        let display_desc = default.description.as_deref();
237
238        configured_handles.push(handle.as_str());
239
240        let handle_str = if handle.is_empty() {
241            "<not synced>".italic().to_string()
242        } else {
243            handle.cyan().to_string()
244        };
245        println!("[Set] {handle_str}: {}", display_name.white());
246        if let Some(desc) = display_desc {
247            println!("      - {desc}");
248        }
249        println!("      - Git: {}", default.url);
250    } else {
251        println!("[Set] {} not set", "<none>".dimmed());
252    }
253
254    // --- added registries ---
255    for reg in &config.added_registries {
256        let handle = &reg.handle;
257        let display_name = reg.name.as_deref().unwrap_or(handle);
258        let display_desc = reg.description.as_deref();
259
260        configured_handles.push(handle.as_str());
261
262        let handle_str = if handle.is_empty() {
263            "<not synced>".italic().to_string()
264        } else {
265            handle.cyan().to_string()
266        };
267        println!();
268        println!("[Added] {handle_str}: {}", display_name.white());
269        if let Some(desc) = display_desc {
270            println!("          - {desc}");
271        }
272        println!("          - Git: {}", reg.url);
273    }
274
275    // --- available but unadded builtins ---
276    let unadded: Vec<_> = all_builtins
277        .iter()
278        .filter(|r| !configured_handles.contains(&r.handle.as_str()))
279        .collect();
280
281    if !unadded.is_empty() {
282        println!();
283        println!("{} Available Registries", "::".bold().blue());
284        for reg in unadded {
285            let handle_str = reg.handle.cyan().to_string();
286            println!("[Unadded] {handle_str}: {}", reg.name.white());
287            println!("          - {}", reg.description);
288            println!("          - Git: {}", reg.git);
289        }
290    }
291
292    Ok(())
293}