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    // A URL that points at a `repo.yaml` document identifies the registry
181    // completely: resolve it now so the handle, name and description are
182    // recorded up front, and so the user learns immediately whether the URL is
183    // usable. Only the content is deferred to `zoi sync`.
184    if pkg::sync::is_repo_yaml_url(handle_or_url)
185        && let Ok(registry) =
186            pkg::sync::resolve_registry_from_url(handle_or_url)
187    {
188        pkg::config::add_resolved_registry(registry.clone())?;
189
190        let handle = registry.handle.cyan();
191        println!("Registry {handle} added.");
192        if let Some(name) = &registry.name {
193            println!("  Name: {name}");
194        }
195        if let Some(description) = &registry.description {
196            println!("  Description: {description}");
197        }
198        println!("  Source: {}", registry.url.cyan());
199        println!("It will be synced on the next 'zoi sync' run.");
200        return Ok(());
201    }
202
203    // Fallback: treat as a raw URL. Sync re-resolves the handle from the
204    // remote on the next run.
205    pkg::config::add_added_registry(handle_or_url)?;
206    let url_cyan = handle_or_url.cyan();
207    println!("Registry '{url_cyan}' added.");
208    println!("It will be synced on the next 'zoi sync' run.");
209    Ok(())
210}
211
212/// Remove a registry by its handle or URL.
213///
214/// # Errors
215///
216/// Returns an error if the registry cannot be removed from the configuration.
217pub fn remove_registry(handle: &str) -> Result<()> {
218    pkg::config::remove_added_registry(handle)?;
219    let handle_cyan = handle.cyan();
220    println!("Registry '{handle_cyan}' removed.");
221    Ok(())
222}
223
224/// List all configured registries and show which built-in registries are
225/// available but not yet added.
226///
227/// Output format:
228///
229/// ```text
230/// :: Configured Registries
231/// [Set] zoidberg: Zoidberg
232///       - Official Zoi packages repository
233///       - Git: https://gitlab.com/zillowe/zillwen/zusty/zoidberg
234/// [Added] registry-handle: Registry Name
235///         - Registry description
236///         - Git: https://github/registry/database
237///
238/// :: Available Registries
239/// [Unadded] registry-handle: Registry Name
240///           - Registry description
241///           - Git: https://github/registry/database
242/// ```
243///
244/// # Errors
245///
246/// Returns an error if the configuration cannot be read.
247pub fn list_registries() -> Result<()> {
248    let config = pkg::config::read_config()?;
249    let all_builtins = pkg::builtin_registries::load_all();
250
251    // Collect handles that are already configured (set or added)
252    let mut configured_handles: Vec<&str> = Vec::new();
253
254    println!("{} Configured Registries", "::".bold().blue());
255
256    // --- set registry (default_registry) ---
257    if let Some(ref default) = config.default_registry {
258        let handle = &default.handle;
259        let display_name = default.name.as_deref().unwrap_or(handle);
260        let display_desc = default.description.as_deref();
261
262        configured_handles.push(handle.as_str());
263
264        let handle_str = if handle.is_empty() {
265            "<not synced>".italic().to_string()
266        } else {
267            handle.cyan().to_string()
268        };
269        println!("[Set] {handle_str}: {}", display_name.white());
270        if let Some(desc) = display_desc {
271            println!("      - {desc}");
272        }
273        println!("      - Git: {}", default.url);
274    } else {
275        println!("[Set] {} not set", "<none>".dimmed());
276    }
277
278    // --- added registries ---
279    for reg in &config.added_registries {
280        let handle = &reg.handle;
281        let display_name = reg.name.as_deref().unwrap_or(handle);
282        let display_desc = reg.description.as_deref();
283
284        configured_handles.push(handle.as_str());
285
286        let handle_str = if handle.is_empty() {
287            "<not synced>".italic().to_string()
288        } else {
289            handle.cyan().to_string()
290        };
291        println!();
292        println!("[Added] {handle_str}: {}", display_name.white());
293        if let Some(desc) = display_desc {
294            println!("          - {desc}");
295        }
296        println!("          - Git: {}", reg.url);
297    }
298
299    // --- available but unadded builtins ---
300    let unadded: Vec<_> = all_builtins
301        .iter()
302        .filter(|r| !configured_handles.contains(&r.handle.as_str()))
303        .collect();
304
305    if !unadded.is_empty() {
306        println!();
307        println!("{} Available Registries", "::".bold().blue());
308        for reg in unadded {
309            let handle_str = reg.handle.cyan().to_string();
310            println!("[Unadded] {handle_str}: {}", reg.name.white());
311            println!("          - {}", reg.description);
312            println!("          - Git: {}", reg.git);
313        }
314    }
315
316    Ok(())
317}