zoi/pkg/
resolve.rs

1use crate::pkg::{config, pin, types};
2use anyhow::{Result, anyhow};
3use chrono::Utc;
4use colored::*;
5use dialoguer::{Select, theme::ColorfulTheme};
6use indicatif::{ProgressBar, ProgressStyle};
7use std::collections::HashMap;
8use std::env;
9use std::fs;
10use std::io::Read;
11use std::path::{Path, PathBuf};
12use walkdir::WalkDir;
13
14#[derive(Debug, PartialEq, Eq, Clone)]
15pub enum SourceType {
16    OfficialRepo,
17    UntrustedRepo(String),
18    GitRepo(String),
19    LocalFile,
20    Url,
21}
22
23#[derive(Debug)]
24pub struct ResolvedSource {
25    pub path: PathBuf,
26    pub source_type: SourceType,
27    pub repo_name: Option<String>,
28    pub registry_handle: Option<String>,
29    pub sharable_manifest: Option<types::SharableInstallManifest>,
30}
31
32#[derive(Debug, Default)]
33pub struct PackageRequest {
34    pub handle: Option<String>,
35    pub repo: Option<String>,
36    pub name: String,
37    pub sub_package: Option<String>,
38    pub version_spec: Option<String>,
39}
40
41pub fn get_db_root() -> Result<PathBuf> {
42    let home_dir = home::home_dir().ok_or_else(|| anyhow!("Could not find home directory."))?;
43    Ok(home_dir.join(".zoi").join("pkgs").join("db"))
44}
45
46pub fn parse_source_string(source_str: &str) -> Result<PackageRequest> {
47    if source_str.contains('/')
48        && (source_str.ends_with(".manifest.yaml") || source_str.ends_with(".pkg.lua"))
49    {
50        let path = std::path::Path::new(source_str);
51        let file_stem = path.file_stem().unwrap_or_default().to_string_lossy();
52        let name = if let Some(stripped) = file_stem.strip_suffix(".manifest") {
53            stripped.to_string()
54        } else if let Some(stripped) = file_stem.strip_suffix(".pkg") {
55            stripped.to_string()
56        } else {
57            file_stem.to_string()
58        };
59        return Ok(PackageRequest {
60            handle: None,
61            repo: None,
62            name,
63            sub_package: None,
64            version_spec: None,
65        });
66    }
67
68    let mut handle = None;
69    let mut main_part = source_str;
70
71    if main_part.starts_with('#') {
72        if let Some(at_pos) = main_part.find('@') {
73            if at_pos > 1 {
74                handle = Some(main_part[1..at_pos].to_string());
75                main_part = &main_part[at_pos..];
76            } else {
77                return Err(anyhow!("Invalid format: empty registry handle"));
78            }
79        } else {
80            return Err(anyhow!(
81                "Invalid format: missing '@' after registry handle. Expected format: #handle@repo/package"
82            ));
83        }
84    }
85
86    let mut repo = None;
87    let name: &str;
88    let mut sub_package = None;
89    let mut version_spec = None;
90
91    let mut version_part_str = main_part;
92
93    if let Some(at_pos) = main_part.rfind('@')
94        && at_pos > 0
95    {
96        let (pkg_part, ver_part) = main_part.split_at(at_pos);
97        version_part_str = pkg_part;
98        version_spec = Some(ver_part[1..].to_string());
99    }
100
101    let name_part = if version_part_str.starts_with('@') {
102        let s = version_part_str.trim_start_matches('@');
103        if let Some((repo_str, name_str)) = s.split_once('/') {
104            if !name_str.is_empty() {
105                repo = Some(repo_str.to_lowercase());
106                name_str
107            } else {
108                return Err(anyhow!(
109                    "Invalid format: missing package name after repo path."
110                ));
111            }
112        } else {
113            return Err(anyhow!(
114                "Invalid format: must be in the form @repo/package or @repo/path/to/package"
115            ));
116        }
117    } else {
118        version_part_str
119    };
120
121    if let Some((base_name, sub_name)) = name_part.split_once(':') {
122        name = base_name;
123        sub_package = Some(sub_name.to_string());
124    } else {
125        name = name_part;
126    }
127
128    if name.is_empty() {
129        return Err(anyhow!("Invalid source string: package name is empty."));
130    }
131
132    Ok(PackageRequest {
133        handle,
134        repo,
135        name: name.to_lowercase(),
136        sub_package,
137        version_spec,
138    })
139}
140
141fn find_package_in_db(request: &PackageRequest) -> Result<ResolvedSource> {
142    let db_root = get_db_root()?;
143    let config = config::read_config()?;
144
145    let (registry_db_path, search_repos, is_default_registry, registry_handle) =
146        if let Some(h) = &request.handle {
147            let is_default = config
148                .default_registry
149                .as_ref()
150                .is_some_and(|reg| reg.handle == *h);
151
152            if is_default {
153                let default_registry = config.default_registry.as_ref().unwrap();
154                (
155                    db_root.join(&default_registry.handle),
156                    config.repos,
157                    true,
158                    Some(default_registry.handle.clone()),
159                )
160            } else if let Some(registry) = config.added_registries.iter().find(|r| r.handle == *h) {
161                let repo_path = db_root.join(&registry.handle);
162                let all_sub_repos = if repo_path.exists() {
163                    fs::read_dir(&repo_path)?
164                        .filter_map(Result::ok)
165                        .filter(|entry| entry.path().is_dir() && entry.file_name() != ".git")
166                        .map(|entry| entry.file_name().to_string_lossy().into_owned())
167                        .collect()
168                } else {
169                    Vec::new()
170                };
171                (
172                    repo_path,
173                    all_sub_repos,
174                    false,
175                    Some(registry.handle.clone()),
176                )
177            } else {
178                return Err(anyhow!("Registry with handle '{}' not found.", h));
179            }
180        } else {
181            let default_registry = config
182                .default_registry
183                .as_ref()
184                .ok_or_else(|| anyhow!("No default registry set."))?;
185            (
186                db_root.join(&default_registry.handle),
187                config.repos,
188                true,
189                Some(default_registry.handle.clone()),
190            )
191        };
192
193    let repos_to_search = if let Some(r) = &request.repo {
194        vec![r.clone()]
195    } else {
196        search_repos
197    };
198
199    struct FoundPackage {
200        path: PathBuf,
201        source_type: SourceType,
202        repo_name: String,
203        description: String,
204    }
205
206    let mut found_packages = Vec::new();
207
208    if request.name.contains('/') {
209        let pkg_name = Path::new(&request.name)
210            .file_name()
211            .and_then(|s| s.to_str())
212            .ok_or_else(|| anyhow!("Invalid package path: {}", request.name))?;
213
214        for repo_name in &repos_to_search {
215            let path = registry_db_path
216                .join(repo_name)
217                .join(&request.name)
218                .join(format!("{}.pkg.lua", pkg_name));
219
220            if path.exists() {
221                let pkg: types::Package =
222                    crate::pkg::lua::parser::parse_lua_package(path.to_str().unwrap(), None)?;
223                let major_repo = repo_name.split('/').next().unwrap_or("").to_lowercase();
224
225                let source_type = if is_default_registry {
226                    let repo_config = config::read_repo_config(&registry_db_path).ok();
227                    if let Some(ref cfg) = repo_config {
228                        if let Some(repo_entry) = cfg.repos.iter().find(|r| r.name == major_repo) {
229                            if repo_entry.repo_type == "offical" {
230                                SourceType::OfficialRepo
231                            } else {
232                                SourceType::UntrustedRepo(repo_name.clone())
233                            }
234                        } else {
235                            SourceType::UntrustedRepo(repo_name.clone())
236                        }
237                    } else {
238                        SourceType::UntrustedRepo(repo_name.clone())
239                    }
240                } else {
241                    SourceType::UntrustedRepo(repo_name.clone())
242                };
243
244                found_packages.push(FoundPackage {
245                    path,
246                    source_type,
247                    repo_name: pkg.repo.clone(),
248                    description: pkg.description,
249                });
250            }
251        }
252    } else {
253        for repo_name in &repos_to_search {
254            let repo_path = registry_db_path.join(repo_name);
255            if !repo_path.is_dir() {
256                continue;
257            }
258            for entry in WalkDir::new(&repo_path)
259                .into_iter()
260                .filter_map(|e| e.ok())
261                .filter(|e| e.file_type().is_dir() && e.file_name() == request.name.as_str())
262            {
263                let pkg_dir_path = entry.path();
264
265                if let Ok(relative_path) = pkg_dir_path.strip_prefix(&repo_path) {
266                    if relative_path.components().count() > 1 {
267                        continue;
268                    }
269                } else {
270                    continue;
271                }
272
273                let pkg_file_path = pkg_dir_path.join(format!("{}.pkg.lua", request.name));
274
275                if pkg_file_path.exists() {
276                    let pkg: types::Package = crate::pkg::lua::parser::parse_lua_package(
277                        pkg_file_path.to_str().unwrap(),
278                        None,
279                    )?;
280                    let major_repo = repo_name.split('/').next().unwrap_or("").to_lowercase();
281
282                    let source_type = if is_default_registry {
283                        let repo_config = config::read_repo_config(&registry_db_path).ok();
284                        if let Some(ref cfg) = repo_config {
285                            if let Some(repo_entry) =
286                                cfg.repos.iter().find(|r| r.name == major_repo)
287                            {
288                                if repo_entry.repo_type == "offical" {
289                                    SourceType::OfficialRepo
290                                } else {
291                                    SourceType::UntrustedRepo(repo_name.clone())
292                                }
293                            } else {
294                                SourceType::UntrustedRepo(repo_name.clone())
295                            }
296                        } else {
297                            SourceType::UntrustedRepo(repo_name.clone())
298                        }
299                    } else {
300                        SourceType::UntrustedRepo(repo_name.clone())
301                    };
302
303                    found_packages.push(FoundPackage {
304                        path: pkg_file_path,
305                        source_type,
306                        repo_name: pkg.repo.clone(),
307                        description: pkg.description,
308                    });
309                }
310            }
311        }
312    }
313
314    if found_packages.is_empty() {
315        if let Some(repo) = &request.repo {
316            Err(anyhow!(
317                "Package '{}' not found in repository '@{}'.",
318                request.name,
319                repo
320            ))
321        } else {
322            Err(anyhow!(
323                "Package '{}' not found in any active repositories.",
324                request.name
325            ))
326        }
327    } else if found_packages.len() == 1 {
328        let chosen = &found_packages[0];
329
330        Ok(ResolvedSource {
331            path: chosen.path.clone(),
332            source_type: chosen.source_type.clone(),
333            repo_name: Some(chosen.repo_name.clone()),
334            registry_handle: registry_handle.clone(),
335            sharable_manifest: None,
336        })
337    } else {
338        println!(
339            "Found multiple packages named '{}'. Please choose one:",
340            request.name.cyan()
341        );
342
343        let items: Vec<String> = found_packages
344            .iter()
345            .map(|p| format!("@{} - {}", p.repo_name.bold(), p.description))
346            .collect();
347
348        let selection = Select::with_theme(&ColorfulTheme::default())
349            .with_prompt("Select a package")
350            .items(&items)
351            .default(0)
352            .interact()?;
353
354        let chosen = &found_packages[selection];
355        println!(
356            "Selected package '{}' from repo '{}'",
357            request.name, chosen.repo_name
358        );
359
360        Ok(ResolvedSource {
361            path: chosen.path.clone(),
362            source_type: chosen.source_type.clone(),
363            repo_name: Some(chosen.repo_name.clone()),
364            registry_handle: registry_handle.clone(),
365            sharable_manifest: None,
366        })
367    }
368}
369
370fn download_from_url(url: &str) -> Result<ResolvedSource> {
371    println!("Downloading package definition from URL...");
372    let client = crate::utils::build_blocking_http_client(20)?;
373    let mut attempt = 0u32;
374    let mut response = loop {
375        attempt += 1;
376        match client.get(url).send() {
377            Ok(resp) => break resp,
378            Err(e) => {
379                if attempt < 3 {
380                    eprintln!(
381                        "{}: download failed ({}). Retrying...",
382                        "Network".yellow(),
383                        e
384                    );
385                    crate::utils::retry_backoff_sleep(attempt);
386                    continue;
387                } else {
388                    return Err(anyhow!(
389                        "Failed to download file after {} attempts: {}",
390                        attempt,
391                        e
392                    ));
393                }
394            }
395        }
396    };
397    if !response.status().is_success() {
398        return Err(anyhow!(
399            "Failed to download file (HTTP {}): {}",
400            response.status(),
401            url
402        ));
403    }
404
405    let total_size = response.content_length().unwrap_or(0);
406    let pb = ProgressBar::new(total_size);
407    pb.set_style(ProgressStyle::default_bar()
408        .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec})")?
409        .progress_chars("#>-"));
410
411    let mut downloaded_bytes = Vec::new();
412    let mut buffer = [0; 8192];
413    loop {
414        let bytes_read = response.read(&mut buffer)?;
415        if bytes_read == 0 {
416            break;
417        }
418        downloaded_bytes.extend_from_slice(&buffer[..bytes_read]);
419        pb.inc(bytes_read as u64);
420    }
421    pb.finish_with_message("Download complete.");
422
423    let content = String::from_utf8(downloaded_bytes)?;
424
425    let temp_path = env::temp_dir().join(format!(
426        "zoi-temp-{}.pkg.lua",
427        Utc::now().timestamp_nanos_opt().unwrap_or(0)
428    ));
429    fs::write(&temp_path, content)?;
430
431    Ok(ResolvedSource {
432        path: temp_path,
433        source_type: SourceType::Url,
434        repo_name: None,
435        registry_handle: Some("local".to_string()),
436        sharable_manifest: None,
437    })
438}
439
440fn download_content_from_url(url: &str) -> Result<String> {
441    println!("Downloading from: {}", url.cyan());
442    let client = crate::utils::build_blocking_http_client(20)?;
443    let mut attempt = 0u32;
444    let response = loop {
445        attempt += 1;
446        match client.get(url).send() {
447            Ok(resp) => break resp,
448            Err(e) => {
449                if attempt < 3 {
450                    eprintln!(
451                        "{}: download failed ({}). Retrying...",
452                        "Network".yellow(),
453                        e
454                    );
455                    crate::utils::retry_backoff_sleep(attempt);
456                    continue;
457                } else {
458                    return Err(anyhow!(
459                        "Failed to download from {} after {} attempts: {}",
460                        url,
461                        attempt,
462                        e
463                    ));
464                }
465            }
466        }
467    };
468
469    if !response.status().is_success() {
470        return Err(anyhow!(
471            "Failed to download from {} (HTTP {}). Content: {}",
472            url,
473            response.status(),
474            response
475                .text()
476                .unwrap_or_else(|_| "Could not read response body".to_string())
477        ));
478    }
479
480    Ok(response.text()?)
481}
482
483fn resolve_version_from_url(url: &str, channel: &str) -> Result<String> {
484    println!(
485        "Resolving version for channel '{}' from {}",
486        channel.cyan(),
487        url.cyan()
488    );
489    let client = crate::utils::build_blocking_http_client(15)?;
490    let mut attempt = 0u32;
491    let resp = loop {
492        attempt += 1;
493        match client.get(url).send() {
494            Ok(r) => match r.text() {
495                Ok(t) => break t,
496                Err(e) => {
497                    if attempt < 3 {
498                        eprintln!("{}: read failed ({}). Retrying...", "Network".yellow(), e);
499                        crate::utils::retry_backoff_sleep(attempt);
500                        continue;
501                    } else {
502                        return Err(anyhow!(
503                            "Failed to read response after {} attempts: {}",
504                            attempt,
505                            e
506                        ));
507                    }
508                }
509            },
510            Err(e) => {
511                if attempt < 3 {
512                    eprintln!("{}: fetch failed ({}). Retrying...", "Network".yellow(), e);
513                    crate::utils::retry_backoff_sleep(attempt);
514                    continue;
515                } else {
516                    return Err(anyhow!("Failed to fetch after {} attempts: {}", attempt, e));
517                }
518            }
519        }
520    };
521    let json: serde_json::Value = serde_json::from_str(&resp)?;
522
523    if let Some(version) = json
524        .get("versions")
525        .and_then(|v| v.get(channel))
526        .and_then(|c| c.as_str())
527    {
528        return Ok(version.to_string());
529    }
530
531    Err(anyhow!(
532        "Failed to extract version for channel '{channel}' from JSON URL: {url}"
533    ))
534}
535
536fn resolve_channel(versions: &HashMap<String, String>, channel: &str) -> Result<String> {
537    if let Some(url_or_version) = versions.get(channel) {
538        if url_or_version.starts_with("http") {
539            resolve_version_from_url(url_or_version, channel)
540        } else {
541            Ok(url_or_version.clone())
542        }
543    } else {
544        Err(anyhow!("Channel '@{}' not found in versions map.", channel))
545    }
546}
547
548pub fn get_default_version(pkg: &types::Package, registry_handle: Option<&str>) -> Result<String> {
549    if let Some(handle) = registry_handle {
550        let source = format!("#{}@{}", handle, pkg.repo);
551
552        if let Some(pinned_version) = pin::get_pinned_version(&source)? {
553            println!(
554                "Using pinned version '{}' for {}.",
555                pinned_version.yellow(),
556                source.cyan()
557            );
558            return if pinned_version.starts_with('@') {
559                let channel = pinned_version.trim_start_matches('@');
560                let versions = pkg.versions.as_ref().ok_or_else(|| {
561                    anyhow!(
562                        "Package '{}' has no 'versions' map to resolve pinned channel '{}'.",
563                        pkg.name,
564                        pinned_version
565                    )
566                })?;
567                resolve_channel(versions, channel)
568            } else {
569                Ok(pinned_version)
570            };
571        }
572    }
573
574    if let Some(versions) = &pkg.versions {
575        if versions.contains_key("stable") {
576            return resolve_channel(versions, "stable");
577        }
578        if let Some((channel, _)) = versions.iter().next() {
579            println!(
580                "No 'stable' channel found, using first available channel: '@{}'",
581                channel.cyan()
582            );
583            return resolve_channel(versions, channel);
584        }
585        return Err(anyhow!(
586            "Package has a 'versions' map but no versions were found in it."
587        ));
588    }
589
590    if let Some(ver) = &pkg.version {
591        if ver.starts_with("http") {
592            let client = crate::utils::build_blocking_http_client(15)?;
593            let mut attempt = 0u32;
594            let resp = loop {
595                attempt += 1;
596                match client.get(ver).send() {
597                    Ok(r) => match r.text() {
598                        Ok(t) => break t,
599                        Err(e) => {
600                            if attempt < 3 {
601                                eprintln!(
602                                    "{}: read failed ({}). Retrying...",
603                                    "Network".yellow(),
604                                    e
605                                );
606                                crate::utils::retry_backoff_sleep(attempt);
607                                continue;
608                            } else {
609                                return Err(anyhow!(
610                                    "Failed to read response after {} attempts: {}",
611                                    attempt,
612                                    e
613                                ));
614                            }
615                        }
616                    },
617                    Err(e) => {
618                        if attempt < 3 {
619                            eprintln!("{}: fetch failed ({}). Retrying...", "Network".yellow(), e);
620                            crate::utils::retry_backoff_sleep(attempt);
621                            continue;
622                        } else {
623                            return Err(anyhow!(
624                                "Failed to fetch after {} attempts: {}",
625                                attempt,
626                                e
627                            ));
628                        }
629                    }
630                }
631            };
632            if let Ok(json) = serde_json::from_str::<serde_json::Value>(&resp) {
633                if let Some(version) = json
634                    .get("versions")
635                    .and_then(|v| v.get("stable"))
636                    .and_then(|s| s.as_str())
637                {
638                    return Ok(version.to_string());
639                }
640
641                if let Some(tag) = json
642                    .get("latest")
643                    .and_then(|l| l.get("production"))
644                    .and_then(|p| p.get("tag"))
645                    .and_then(|t| t.as_str())
646                {
647                    return Ok(tag.to_string());
648                }
649                return Err(anyhow!(
650                    "Could not determine a version from the JSON content at {}",
651                    ver
652                ));
653            }
654            return Ok(resp.trim().to_string());
655        } else {
656            return Ok(ver.clone());
657        }
658    }
659
660    Err(anyhow!(
661        "Could not determine a version for package '{}'.",
662        pkg.name
663    ))
664}
665
666fn get_version_for_install(
667    pkg: &types::Package,
668    version_spec: &Option<String>,
669    registry_handle: Option<&str>,
670) -> Result<String> {
671    if let Some(spec) = version_spec {
672        if spec.starts_with('@') {
673            let channel = spec.trim_start_matches('@');
674            let versions = pkg.versions.as_ref().ok_or_else(|| {
675                anyhow!(
676                    "Package '{}' has no 'versions' map to resolve channel '@{}'.",
677                    pkg.name,
678                    channel
679                )
680            })?;
681            return resolve_channel(versions, channel);
682        }
683
684        if let Some(versions) = &pkg.versions
685            && versions.contains_key(spec)
686        {
687            println!("Found '{}' as a channel, resolving...", spec.cyan());
688            return resolve_channel(versions, spec);
689        }
690
691        return Ok(spec.clone());
692    }
693
694    get_default_version(pkg, registry_handle)
695}
696
697pub fn resolve_source(source: &str) -> Result<ResolvedSource> {
698    let resolved = resolve_source_recursive(source, 0)?;
699
700    if let Ok(request) = parse_source_string(source)
701        && !matches!(
702            &resolved.source_type,
703            SourceType::LocalFile | SourceType::Url
704        )
705        && let Some(repo_name) = &resolved.repo_name
706    {
707        println!("Found package '{}' in repo '{}'", request.name, repo_name);
708    }
709
710    Ok(resolved)
711}
712
713pub fn resolve_package_and_version(
714    source_str: &str,
715) -> Result<(
716    types::Package,
717    String,
718    Option<types::SharableInstallManifest>,
719    PathBuf,
720    Option<String>,
721)> {
722    let request = parse_source_string(source_str)?;
723    let resolved_source = resolve_source_recursive(source_str, 0)?;
724    let registry_handle = resolved_source.registry_handle.clone();
725    let pkg_lua_path = resolved_source.path.clone();
726
727    let pkg_template =
728        crate::pkg::lua::parser::parse_lua_package(resolved_source.path.to_str().unwrap(), None)?;
729
730    let mut pkg_with_repo = pkg_template;
731    if let Some(repo_name) = resolved_source.repo_name.clone() {
732        pkg_with_repo.repo = repo_name;
733    }
734
735    let version_string = get_version_for_install(
736        &pkg_with_repo,
737        &request.version_spec,
738        registry_handle.as_deref(),
739    )?;
740
741    let mut pkg = crate::pkg::lua::parser::parse_lua_package(
742        resolved_source.path.to_str().unwrap(),
743        Some(&version_string),
744    )?;
745    if let Some(repo_name) = resolved_source.repo_name.clone() {
746        pkg.repo = repo_name;
747    }
748    pkg.version = Some(version_string.clone());
749
750    let registry_handle = resolved_source.registry_handle.clone();
751
752    Ok((
753        pkg,
754        version_string,
755        resolved_source.sharable_manifest,
756        pkg_lua_path,
757        registry_handle,
758    ))
759}
760
761fn resolve_source_recursive(source: &str, depth: u8) -> Result<ResolvedSource> {
762    if depth > 5 {
763        return Err(anyhow!(
764            "Exceeded max resolution depth, possible circular 'alt' reference."
765        ));
766    }
767
768    if source.ends_with(".manifest.yaml") {
769        let path = PathBuf::from(source);
770        if !path.exists() {
771            return Err(anyhow!("Local file not found at '{source}'"));
772        }
773        println!("Using local sharable manifest file: {}", path.display());
774        let content = fs::read_to_string(&path)?;
775        let sharable_manifest: types::SharableInstallManifest = serde_yaml::from_str(&content)?;
776        let new_source = format!(
777            "#{}@{}/{}@{}",
778            sharable_manifest.registry_handle,
779            sharable_manifest.repo,
780            sharable_manifest.name,
781            sharable_manifest.version
782        );
783        let mut resolved_source = resolve_source_recursive(&new_source, depth + 1)?;
784        resolved_source.sharable_manifest = Some(sharable_manifest);
785        return Ok(resolved_source);
786    }
787
788    let request = parse_source_string(source)?;
789
790    if let Some(handle) = &request.handle
791        && handle.starts_with("git:")
792    {
793        let git_source = handle.strip_prefix("git:").unwrap();
794        println!(
795            "Warning: using remote git repo '{}' not from official Zoi database.",
796            git_source.yellow()
797        );
798
799        let (host, repo_path) = git_source
800            .split_once('/')
801            .ok_or_else(|| anyhow!("Invalid git source format. Expected host/owner/repo."))?;
802
803        let (base_url, branch_sep) = match host {
804            "github.com" => (
805                format!("https://raw.githubusercontent.com/{}", repo_path),
806                "/",
807            ),
808            "gitlab.com" => (format!("https://gitlab.com/{}/-/raw", repo_path), "/"),
809            "codeberg.org" => (
810                format!("https://codeberg.org/{}/raw/branch", repo_path),
811                "/",
812            ),
813            _ => return Err(anyhow!("Unsupported git host: {}", host)),
814        };
815
816        let (_, branch) = {
817            let mut last_error = None;
818            let mut content = None;
819            for b in ["main", "master"] {
820                let repo_yaml_url = format!("{}{}{}/repo.yaml", base_url, branch_sep, b);
821                match download_content_from_url(&repo_yaml_url) {
822                    Ok(c) => {
823                        content = Some((c, b.to_string()));
824                        break;
825                    }
826                    Err(e) => {
827                        last_error = Some(e);
828                    }
829                }
830            }
831            content.ok_or_else(|| {
832                last_error
833                    .unwrap_or_else(|| anyhow!("Could not find repo.yaml on main or master branch"))
834            })?
835        };
836
837        let full_pkg_path = if let Some(r) = &request.repo {
838            format!("{}/{}", r, request.name)
839        } else {
840            request.name.clone()
841        };
842
843        let pkg_name = Path::new(&full_pkg_path)
844            .file_name()
845            .unwrap()
846            .to_str()
847            .unwrap();
848        let pkg_lua_filename = format!("{}.pkg.lua", pkg_name);
849        let pkg_lua_path_in_repo = Path::new(&full_pkg_path).join(pkg_lua_filename);
850
851        let pkg_lua_url = format!(
852            "{}{}{}/{}",
853            base_url,
854            branch_sep,
855            branch,
856            pkg_lua_path_in_repo.to_str().unwrap().replace('\\', "/")
857        );
858
859        let pkg_lua_content = download_content_from_url(&pkg_lua_url)?;
860
861        let temp_path = env::temp_dir().join(format!(
862            "zoi-temp-git-{}.pkg.lua",
863            Utc::now().timestamp_nanos_opt().unwrap_or(0)
864        ));
865        fs::write(&temp_path, pkg_lua_content)?;
866
867        let repo_name = format!("git:{}", git_source);
868
869        return Ok(ResolvedSource {
870            path: temp_path,
871            source_type: SourceType::GitRepo(repo_name.clone()),
872            repo_name: Some(repo_name),
873            registry_handle: None,
874            sharable_manifest: None,
875        });
876    }
877
878    let resolved_source = if source.starts_with("@git/") {
879        let full_path_str = source.trim_start_matches("@git/");
880        let parts: Vec<&str> = full_path_str.split('/').collect();
881
882        if parts.len() < 2 {
883            return Err(anyhow!(
884                "Invalid git source. Use @git/<repo-name>/<path/to/pkg>"
885            ));
886        }
887
888        let repo_name = parts[0];
889        let nested_path_parts = &parts[1..];
890        let pkg_name = nested_path_parts.last().unwrap();
891
892        let home_dir = home::home_dir().ok_or_else(|| anyhow!("Could not find home directory."))?;
893        let mut path = home_dir
894            .join(".zoi")
895            .join("pkgs")
896            .join("git")
897            .join(repo_name);
898
899        for part in nested_path_parts.iter().take(nested_path_parts.len() - 1) {
900            path = path.join(part);
901        }
902
903        path = path.join(format!("{}.pkg.lua", pkg_name));
904
905        if !path.exists() {
906            let nested_path_str = nested_path_parts.join("/");
907            return Err(anyhow!(
908                "Package '{}' not found in git repo '{}' (expected: {})",
909                nested_path_str,
910                repo_name,
911                path.display()
912            ));
913        }
914        println!(
915            "Warning: using external git repo '{}{}' not from official Zoi database.",
916            "@git/".yellow(),
917            repo_name.yellow()
918        );
919        ResolvedSource {
920            path,
921            source_type: SourceType::GitRepo(repo_name.to_string()),
922            repo_name: Some(format!("git/{}", repo_name)),
923            registry_handle: Some("local".to_string()),
924            sharable_manifest: None,
925        }
926    } else if source.starts_with("http://") || source.starts_with("https://") {
927        download_from_url(source)?
928    } else if source.ends_with(".pkg.lua") {
929        let path = PathBuf::from(source);
930        if !path.exists() {
931            return Err(anyhow!("Local file not found at '{source}'"));
932        }
933        println!("Using local package file: {}", path.display());
934        ResolvedSource {
935            path,
936            source_type: SourceType::LocalFile,
937            repo_name: None,
938            registry_handle: Some("local".to_string()),
939            sharable_manifest: None,
940        }
941    } else {
942        find_package_in_db(&request)?
943    };
944
945    let pkg_for_alt_check =
946        crate::pkg::lua::parser::parse_lua_package(resolved_source.path.to_str().unwrap(), None)?;
947
948    if let Some(alt_source) = pkg_for_alt_check.alt {
949        println!("Found 'alt' source. Resolving from: {}", alt_source.cyan());
950
951        let mut alt_resolved_source =
952            if alt_source.starts_with("http://") || alt_source.starts_with("https://") {
953                println!("Downloading 'alt' source from: {}", alt_source.cyan());
954                let client = crate::utils::build_blocking_http_client(20)?;
955                let mut attempt = 0u32;
956                let response = loop {
957                    attempt += 1;
958                    match client.get(&alt_source).send() {
959                        Ok(resp) => break resp,
960                        Err(e) => {
961                            if attempt < 3 {
962                                eprintln!(
963                                    "{}: download failed ({}). Retrying...",
964                                    "Network".yellow(),
965                                    e
966                                );
967                                crate::utils::retry_backoff_sleep(attempt);
968                                continue;
969                            } else {
970                                return Err(anyhow!(
971                                    "Failed to download file after {} attempts: {}",
972                                    attempt,
973                                    e
974                                ));
975                            }
976                        }
977                    }
978                };
979                if !response.status().is_success() {
980                    return Err(anyhow!(
981                        "Failed to download alt source (HTTP {}): {}",
982                        response.status(),
983                        alt_source
984                    ));
985                }
986
987                let content = response.text()?;
988                let temp_path = env::temp_dir().join(format!(
989                    "zoi-alt-{}.pkg.lua",
990                    Utc::now().timestamp_nanos_opt().unwrap_or(0)
991                ));
992                fs::write(&temp_path, &content)?;
993                resolve_source_recursive(temp_path.to_str().unwrap(), depth + 1)?
994            } else {
995                resolve_source_recursive(&alt_source, depth + 1)?
996            };
997
998        if resolved_source.source_type == SourceType::OfficialRepo {
999            alt_resolved_source.source_type = SourceType::OfficialRepo;
1000        }
1001
1002        return Ok(alt_resolved_source);
1003    }
1004
1005    Ok(resolved_source)
1006}