Skip to main content

zoi_package/
registry.rs

1//! Registry management and metadata generation.
2//!
3//! This module provides functions for initializing registries, adding packages
4//! and security advisories, and generating the optimized JSON index files used
5//! by Zoi.
6
7use std::collections::HashMap;
8use std::fs;
9use std::path::Path;
10
11use anyhow::{Result, anyhow};
12use chrono::Datelike;
13use colored::Colorize;
14use walkdir::WalkDir;
15use zoi_core::types;
16use zoi_lua;
17
18use crate::{doctor as pkg_doctor, init_lsp};
19
20/// Initializes a new Zoi registry at the given path.
21///
22/// This creates the necessary directory structure, `repo.yaml`,
23/// `packages.json`, and `advisories.json`, and sets up LSP support.
24///
25/// # Errors
26///
27/// Returns an error if:
28/// - The registry directory or its subdirectories cannot be created.
29/// - LSP workspace setup fails.
30/// - Any of the initial configuration files cannot be written.
31pub fn init(path: &Path) -> Result<()> {
32    println!(
33        "{} Initializing new Zoi registry at {}...",
34        "::".bold().blue(),
35        path.display()
36    );
37
38    fs::create_dir_all(path)?;
39
40    let dirs = ["core", "main", "community", "test", "archive"];
41    for dir in &dirs {
42        let dir_path = path.join(dir);
43        if !dir_path.exists() {
44            fs::create_dir_all(&dir_path)?;
45        }
46    }
47
48    init_lsp::setup_lsp_workspace(path)?;
49    println!(
50        "{} LSP support initialized. Created .luarc.json and type definitions.",
51        "::".bold().green()
52    );
53
54    let repo_yaml_path = path.join("repo.yaml");
55    if !repo_yaml_path.exists() {
56        let content = r#"# Zoi Registry Configuration
57# For detailed documentation, visit: https://zillowe.qzz.io/docs/zds/zoi/repositories#the-repoyaml-file
58
59name: "My-Registry"
60description: "A custom Zoi package registry"
61handle: "my-registry"
62advisory_prefix: "RSA" # Prefix for security advisories, e.g. RSA-2026-A0001
63
64# Git mirrors for this registry
65# For more info: https://zillowe.qzz.io/docs/zds/zoi/guides/mirroring
66git:
67  - type: main
68    url: "https://github.com/user/my-registry.git"
69
70# Pre-built package mirrors (optional)
71# pkg:
72#   - type: main
73#     url: "https://example.com/pkgs/{repo}/{os}/{arch}/{version}"
74
75# Trusted PGP keys for this registry
76# pgp:
77#   - name: maintainer-key
78#     key: "https://example.com/keys/maintainer.asc"
79
80# Repository tiers
81repos:
82  - name: core
83    type: official
84    active: true
85  - name: main
86    type: official
87    active: true
88  - name: community
89    type: community
90    active: false
91  - name: test
92    type: test
93    active: false
94  - name: archive
95    type: archive
96    active: false
97"#;
98        fs::write(repo_yaml_path, content)?;
99    }
100
101    let packages_json_path = path.join("packages.json");
102    if !packages_json_path.exists() {
103        let content = r#"{
104  "version": "2",
105  "packages": {}
106}"#;
107        fs::write(packages_json_path, content)?;
108    }
109
110    let advisories_json_path = path.join("advisories.json");
111    if !advisories_json_path.exists() {
112        let current_year = chrono::Utc::now().year();
113        let content = format!(
114            r#"{{
115  "version": "2",
116  "advisories": {{}},
117  "last_id": 0,
118  "year": {current_year}
119}}"#,
120        );
121        fs::write(advisories_json_path, content)?;
122    }
123
124    println!("{}", "Registry initialized successfully.".green());
125    println!(
126        "{} Edit 'repo.yaml' to configure your registry mirrors and \
127         authorities.",
128        "Note:".yellow()
129    );
130    Ok(())
131}
132
133/// Adds a new package definition to the registry.
134///
135/// This creates a new directory for the package and a template `.pkg.lua` file.
136/// If `name` or `repo` are not provided, it prompts the user for input.
137///
138/// # Errors
139///
140/// Returns an error if:
141/// - The path is not a Zoi registry.
142/// - Package name or repository tier are not provided.
143/// - The package directory cannot be created.
144/// - The package already exists.
145pub fn add_package(
146    registry_root: &Path,
147    name: Option<&str>,
148    repo: Option<&str>
149) -> Result<()> {
150    use std::io::{Write, stdin, stdout};
151
152    if !registry_root.join("repo.yaml").exists() {
153        return Err(anyhow!(
154            "Not a Zoi registry (missing repo.yaml). Run 'zoi reg init' first."
155        ));
156    }
157
158    let get_input = |prompt: &str| -> String {
159        print!("{prompt}: ");
160        let _ = stdout().flush();
161        let mut input = String::new();
162        let _ = stdin().read_line(&mut input);
163        input.trim().to_string()
164    };
165
166    let name = match name {
167        Some(n) => n.to_string(),
168        None => get_input("Package name")
169    };
170
171    let repo = match repo {
172        Some(r) => r.to_string(),
173        None => get_input("Repository tier (e.g. community, main)")
174    };
175
176    if name.is_empty() || repo.is_empty() {
177        return Err(anyhow!("Package name and repository tier are required."));
178    }
179
180    let pkg_dir = registry_root.join(&repo).join(&name);
181    fs::create_dir_all(&pkg_dir)?;
182
183    let pkg_lua_path = pkg_dir.join(format!("{name}.pkg.lua"));
184    if pkg_lua_path.exists() {
185        return Err(anyhow!(
186            "Package '{name}' already exists in repo '{repo}'.",
187        ));
188    }
189
190    let content = format!(
191        r#"-- Zoi Package Definition: {name}
192-- For detailed documentation, visit: https://zillowe.qzz.io/docs/zds/zoi/creating-packages
193
194metadata({{
195  name = "{name}",
196  repo = "{repo}",
197  version = "1.0.0",
198  revision = "1",
199  description = "A short description of {name}.",
200  website = "https://example.com",
201  license = "Apache-2.0",
202  maintainer = {{ name = "Your Name", email = "you@example.com" }},
203  bins = {{ "{name}" }},
204  types = {{ "pre-compiled" }}, -- Supports "source", "pre-compiled"
205}})
206
207dependencies({{
208  build = {{
209    -- Build-time dependencies
210    -- For format info: https://zillowe.qzz.io/docs/zds/zoi/dependencies
211  }},
212  runtime = {{
213    -- Runtime dependencies
214  }}
215}})
216
217function prepare()
218  -- Fetch source or binaries
219  -- Example: UTILS.EXTRACT("https://example.com/release.tar.gz", "src")
220end
221
222function package()
223  -- Stage files for the package
224  -- Example: zcp("src/{name}", "${{pkgstore}}/bin/{name}")
225end
226
227-- function verify()
228--   -- Security verification
229--   -- return verifyHash("release.tar.gz", "sha256-...")
230-- end
231
232-- function test()
233--   -- Integration tests (run via zoi package test)
234--   -- local _, _, code = cmd(STAGING_DIR .. "/data/pkgstore/bin/{name} --version")
235--   -- return code == 0
236-- end
237
238function uninstall()
239  -- Cleanup outside the package store
240end
241"#,
242    );
243
244    fs::write(pkg_lua_path, content)?;
245    println!(
246        "{} Package '{}' created in repo '{}'.",
247        "::".bold().green(),
248        name.cyan(),
249        repo.cyan()
250    );
251
252    Ok(())
253}
254
255/// Adds a new security advisory to the registry.
256///
257/// This creates a temporary `.sec.yaml` file for the given package.
258/// The ID will be automatically assigned during `generate_metadata`.
259///
260/// # Errors
261///
262/// Returns an error if:
263/// - The path is not a Zoi registry.
264/// - The package is not found in the registry.
265/// - A temporary advisory already exists.
266pub fn add_advisory(
267    registry_root: &Path,
268    package_name: Option<&str>,
269    repo: Option<&str>
270) -> Result<()> {
271    use std::io::{Write, stdin, stdout};
272
273    if !registry_root.join("repo.yaml").exists() {
274        return Err(anyhow!(
275            "Not a Zoi registry (missing repo.yaml). Run 'zoi reg init' first."
276        ));
277    }
278
279    let get_input = |prompt: &str| -> String {
280        print!("{prompt}: ");
281        let _ = stdout().flush();
282        let mut input = String::new();
283        let _ = stdin().read_line(&mut input);
284        input.trim().to_string()
285    };
286
287    let package_name = match package_name {
288        Some(n) => n.to_string(),
289        None => get_input("Package name")
290    };
291
292    let package_name_str = package_name.as_str();
293
294    let pkg_dir = if let Some(r) = repo {
295        let dir = registry_root.join(r).join(package_name_str);
296        if !dir.join(format!("{package_name_str}.pkg.lua")).exists() {
297            return Err(anyhow!(
298                "Package '{package_name_str}' not found in repo '{r}'.",
299            ));
300        }
301        dir
302    } else {
303        let mut found = None;
304        for entry in WalkDir::new(registry_root)
305            .into_iter()
306            .filter_map(Result::ok)
307        {
308            if entry.file_type().is_dir()
309                && entry.file_name().to_string_lossy() == package_name_str
310                && entry
311                    .path()
312                    .join(format!("{package_name_str}.pkg.lua"))
313                    .exists()
314            {
315                found = Some(entry.path().to_path_buf());
316                break;
317            }
318        }
319        found.ok_or_else(|| {
320            anyhow!(
321                "Package '{package_name_str}' not found in registry. Try \
322                 specifying --repo.",
323            )
324        })?
325    };
326
327    let repo_config_str = fs::read_to_string(registry_root.join("repo.yaml"))?;
328    let repo_config: types::RepoConfig =
329        serde_yaml::from_str(&repo_config_str)?;
330    let prefix = repo_config
331        .advisory_prefix
332        .unwrap_or_else(|| "ZSA".to_string());
333    // Validated for the same reason as in `generate_metadata`: this prefix is
334    // interpolated into the advisory file name below.
335    let prefix = validate_advisory_prefix(&prefix)?;
336
337    let current_year = chrono::Utc::now().year();
338    let adv_file_path =
339        pkg_dir.join(format!("{prefix}-{current_year}-TEMP.sec.yaml"));
340
341    if adv_file_path.exists() {
342        return Err(anyhow!(
343            "A temporary advisory already exists for this package."
344        ));
345    }
346
347    println!(
348        "{} Adding security advisory for package: {}",
349        "::".bold().blue(),
350        package_name_str.cyan()
351    );
352    println!(
353        "For detailed documentation, visit: https://zillowe.qzz.io/docs/zds/zoi/guides/security-advisories\n"
354    );
355
356    let summary = get_input("Summary (short description)");
357    let severity = get_input("Severity (low, medium, high, critical)");
358    let affected_range =
359        get_input("Affected version range (e.g. >=1.0.0, <1.2.3)");
360    let fixed_in = get_input("Fixed in version");
361    let description = get_input("Detailed description");
362    let reference = get_input("Reference URL (optional)");
363
364    let content = format!(
365        r#"# Zoi Security Advisory
366# For schema details: https://zillowe.qzz.io/docs/zds/zoi/guides/security-advisories#advisory-schema
367
368id: "{prefix}-{current_year}-TEMP"
369package: "{package_name}"
370summary: "{summary}"
371severity: "{severity}"
372affected_range: "{affected_range}"
373fixed_in: "{fixed_in}"
374description: |
375  {description}
376references:
377  - "{reference}"
378"#,
379    );
380
381    fs::write(&adv_file_path, content)?;
382    println!(
383        "\n{} Temporary advisory created: {}",
384        "::".bold().green(),
385        adv_file_path.display().to_string().cyan()
386    );
387    println!(
388        "{} ID will be automatically assigned during 'zoi reg gen-meta' or in \
389         CI.",
390        "Note:".yellow()
391    );
392
393    Ok(())
394}
395
396/// Validates the `advisory_prefix` from a registry's `repo.yaml`.
397///
398/// The prefix is interpolated straight into the file name every advisory is
399/// written to (`{prefix}-{year}-{sev}{n}.sec.yaml`). A registry setting a
400/// prefix that contains a separator, a dot segment, or a NUL byte would
401/// therefore make `zoi reg addadvisory` and `zoi reg build` read and write
402/// outside the registry root. An allowlist is used rather than a denylist so
403/// percent-encoding and Unicode look-alikes cannot smuggle a separator past it.
404///
405/// # Errors
406///
407/// Returns an error if the prefix is empty or contains anything outside
408/// ASCII letters, digits, hyphen, or underscore.
409fn validate_advisory_prefix(prefix: &str) -> Result<String> {
410    if prefix.is_empty()
411        || !prefix
412            .chars()
413            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_'))
414    {
415        return Err(anyhow!(
416            "advisory_prefix '{prefix}' in repo.yaml must be non-empty and \
417             contain only ASCII letters, digits, '-' or '_'"
418        ));
419    }
420    Ok(prefix.to_string())
421}
422
423/// Scans the entire registry to generate optimized JSON index files.
424///
425/// This function is the "Build Pipeline" for Zoi registries. It:
426/// - Chronologically assigns permanent IDs to new security advisories.
427/// - Parses every `.pkg.lua` file to extract static metadata.
428/// - Populates `packages.json` (the primary index) and `advisories.json`.
429/// - Enables clients to resolve packages and vulnerabilities without cloning
430///   the entire Git repository or parsing thousands of Lua scripts.
431///
432/// # Errors
433///
434/// Returns an error if:
435/// - The path is not a Zoi registry.
436/// - The registry index files cannot be written.
437/// - Any package or advisory definition is malformed.
438/// - `repo.yaml` declares an `advisory_prefix` that is not a plain name.
439pub fn generate_metadata(registry_root: &Path) -> Result<()> {
440    if !registry_root.join("repo.yaml").exists() {
441        return Err(anyhow!(
442            "Not a Zoi registry (missing repo.yaml). Run 'zoi reg init' first."
443        ));
444    }
445
446    println!("{} Generating registry metadata...", "::".bold().blue());
447
448    let repo_config_str = fs::read_to_string(registry_root.join("repo.yaml"))?;
449    let repo_config: types::RepoConfig =
450        serde_yaml::from_str(&repo_config_str)?;
451    let advisory_prefix = repo_config
452        .advisory_prefix
453        .clone()
454        .unwrap_or_else(|| "ZSA".to_string());
455
456    let advisory_prefix = validate_advisory_prefix(&advisory_prefix)?;
457
458    let advisories_json_path = registry_root.join("advisories.json");
459    let mut adv_registry: types::AdvisoryRegistry =
460        if advisories_json_path.exists() {
461            serde_json::from_str(&fs::read_to_string(&advisories_json_path)?)?
462        } else {
463            types::AdvisoryRegistry::default()
464        };
465
466    let current_year = chrono::Utc::now().year();
467    let current_year_u32 = u32::try_from(current_year).unwrap_or(0);
468    if adv_registry.year != current_year_u32 {
469        adv_registry.year = current_year_u32;
470        adv_registry.last_id = 0;
471    }
472
473    for entry in WalkDir::new(registry_root)
474        .into_iter()
475        .filter_map(Result::ok)
476    {
477        let file_name = entry.file_name().to_string_lossy();
478        if file_name.ends_with("-TEMP.sec.yaml") {
479            let path = entry.path();
480            let content_str = fs::read_to_string(path)?;
481            let mut content: serde_yaml::Value =
482                serde_yaml::from_str(&content_str)?;
483            let severity = content
484                .get("severity")
485                .and_then(|v| v.as_str())
486                .unwrap_or("low")
487                .to_lowercase();
488            let sev_char = match severity.as_str() {
489                "medium" => "B",
490                "high" => "C",
491                "critical" => "D",
492                _ => "A"
493            };
494
495            adv_registry.last_id += 1;
496            let final_id = format!(
497                "{advisory_prefix}-{current_year}-{sev_char}{:04}",
498                adv_registry.last_id
499            );
500
501            if let Some(mapping) = content.as_mapping_mut() {
502                mapping.insert(
503                    serde_yaml::Value::String("id".to_string()),
504                    serde_yaml::Value::String(final_id.clone())
505                );
506            }
507
508            let final_path =
509                path.with_file_name(format!("{final_id}.sec.yaml"));
510            fs::write(&final_path, serde_yaml::to_string(&content)?)?;
511            fs::remove_file(path)?;
512            println!("Assigned ID {} to {}", final_id.green(), path.display());
513        }
514    }
515
516    let mut advisories_map = std::collections::BTreeMap::new();
517    let mut max_id = adv_registry.last_id;
518
519    for entry in WalkDir::new(registry_root)
520        .into_iter()
521        .filter_map(Result::ok)
522    {
523        let file_name = entry.file_name().to_string_lossy();
524        if file_name.ends_with(".sec.yaml")
525            && !file_name.ends_with("-TEMP.sec.yaml")
526        {
527            let content_str = fs::read_to_string(entry.path())?;
528            let content: types::Advisory = serde_yaml::from_str(&content_str)?;
529            if let Some(last_part) = content.id.split('-').next_back() {
530                let id_num_str = if last_part.len() > 4 {
531                    &last_part[1..]
532                } else {
533                    last_part
534                };
535                if let Ok(id_num) = id_num_str.parse::<u32>() {
536                    if id_num > max_id {
537                        max_id = id_num;
538                    }
539                    let rel_path = entry.path().strip_prefix(registry_root)?;
540                    advisories_map.insert(
541                        content.id.clone(),
542                        rel_path.to_string_lossy().to_string()
543                    );
544                }
545            }
546        }
547    }
548
549    adv_registry.last_id = max_id;
550    adv_registry.advisories = advisories_map;
551    adv_registry.version = "2".to_string();
552    fs::write(
553        &advisories_json_path,
554        serde_json::to_string_pretty(&adv_registry)?
555    )?;
556
557    let mut packages_map = std::collections::BTreeMap::new();
558    let repo_types: HashMap<String, String> = repo_config
559        .repos
560        .iter()
561        .map(|r| (r.name.clone(), r.repo_type.clone()))
562        .collect();
563
564    for entry in WalkDir::new(registry_root)
565        .into_iter()
566        .filter_map(Result::ok)
567    {
568        if entry.file_type().is_file()
569            && entry.file_name().to_string_lossy().ends_with(".pkg.lua")
570        {
571            let path = entry.path();
572            let path_str = path.to_string_lossy();
573            if let Ok(pkg) =
574                zoi_lua::parser::parse_lua_package(&path_str, None, None, true)
575            {
576                let rel_path = path.strip_prefix(registry_root)?;
577                let mut repo_parts: Vec<_> = rel_path
578                    .components()
579                    .map(|c| c.as_os_str().to_string_lossy().to_string())
580                    .collect();
581                repo_parts.pop();
582                repo_parts.pop();
583                let repo_path = repo_parts.join("/");
584
585                let major_repo =
586                    repo_path.split('/').next().unwrap_or_default();
587                let repo_type = repo_types
588                    .get(major_repo)
589                    .cloned()
590                    .unwrap_or_else(|| "unofficial".to_string());
591
592                let version = pkg
593                    .version
594                    .clone()
595                    .or_else(|| {
596                        pkg.versions
597                            .as_ref()
598                            .and_then(|v| v.get("stable").cloned())
599                    })
600                    .unwrap_or_else(|| "unknown".to_string());
601
602                let mut vulns = Vec::new();
603                let pkg_dir = path.parent().ok_or_else(|| {
604                    anyhow!("Package path has no parent directory")
605                })?;
606                if let Ok(sec_entries) = fs::read_dir(pkg_dir) {
607                    for sec_entry in sec_entries.flatten() {
608                        if sec_entry
609                            .file_name()
610                            .to_string_lossy()
611                            .ends_with(".sec.yaml")
612                        {
613                            let sec_content_str =
614                                fs::read_to_string(sec_entry.path())?;
615                            if let Ok(adv) =
616                                serde_yaml::from_str::<types::Advisory>(
617                                    &sec_content_str
618                                )
619                            {
620                                vulns.push(
621                                    zoi_core::types::MiniVulnerability {
622                                        id: adv.id,
623                                        severity: format!("{:?}", adv.severity)
624                                            .to_lowercase(),
625                                        affected_range: adv.affected_range,
626                                        fixed_in: adv.fixed_in,
627                                        summary: adv.summary
628                                    }
629                                );
630                            }
631                        }
632                    }
633                }
634
635                let packages_key =
636                    format!("@{repo_path}/{name}", name = pkg.name);
637
638                let dependencies_v2 = pkg.dependencies.map(|deps| {
639                    let mut runtime = Vec::new();
640                    if let Some(r) = deps.runtime {
641                        runtime = match r {
642                            types::DependencyGroup::Simple(d) => d,
643                            types::DependencyGroup::Complex(c) => {
644                                let mut all = c.required;
645                                all.extend(c.optional);
646                                for opt in c.options {
647                                    all.extend(opt.depends);
648                                }
649                                all
650                            }
651                        };
652                    }
653
654                    let mut build = Vec::new();
655                    if let Some(b) = deps.build {
656                        match b {
657                            types::BuildDependencies::Group(g) => {
658                                let packages = match g {
659                                    types::DependencyGroup::Simple(d) => d,
660                                    types::DependencyGroup::Complex(c) => {
661                                        let mut all = c.required;
662                                        all.extend(c.optional);
663                                        for opt in c.options {
664                                            all.extend(opt.depends);
665                                        }
666                                        all
667                                    }
668                                };
669                                build.push(types::BuildDependencyV2 {
670                                    build_type: "source".to_string(),
671                                    packages
672                                });
673                            }
674                            types::BuildDependencies::Typed(t) => {
675                                for (bt, g) in t.types {
676                                    let packages = match g {
677                                        types::DependencyGroup::Simple(d) => d,
678                                        types::DependencyGroup::Complex(c) => {
679                                            let mut all = c.required;
680                                            all.extend(c.optional);
681                                            for opt in c.options {
682                                                all.extend(opt.depends);
683                                            }
684                                            all
685                                        }
686                                    };
687                                    build.push(types::BuildDependencyV2 {
688                                        build_type: bt,
689                                        packages
690                                    });
691                                }
692                            }
693                            types::BuildDependencies::List(l) => {
694                                for entry in l {
695                                    build.push(types::BuildDependencyV2 {
696                                        build_type: entry.build_type,
697                                        packages: entry.packages
698                                    });
699                                }
700                            }
701                        }
702                    }
703
704                    let mut test = Vec::new();
705                    if let Some(t) = deps.test {
706                        test = match t {
707                            types::DependencyGroup::Simple(d) => d,
708                            types::DependencyGroup::Complex(c) => {
709                                let mut all = c.required;
710                                all.extend(c.optional);
711                                for opt in c.options {
712                                    all.extend(opt.depends);
713                                }
714                                all
715                            }
716                        };
717                    }
718
719                    types::DependenciesV2 {
720                        runtime,
721                        build,
722                        test
723                    }
724                });
725
726                packages_map.insert(
727                    packages_key,
728                    types::PurlPackageIndexV2 {
729                        repo: repo_path,
730                        repo_type,
731                        version,
732                        epoch: pkg.epoch,
733                        revision: pkg.revision.clone(),
734                        description: pkg.description,
735                        scope: Some(pkg.scope),
736                        scopes: pkg.scopes.clone(),
737                        dependencies: dependencies_v2,
738                        sub_packages: pkg.sub_packages.unwrap_or_default(),
739                        main_sub_packages: pkg.main_subs.unwrap_or_default(),
740                        vuln: vulns
741                    }
742                );
743            }
744        }
745    }
746
747    let index = types::RegistryIndexV2 {
748        version: "2".to_string(),
749        packages: packages_map
750    };
751    fs::write(
752        registry_root.join("packages.json"),
753        serde_json::to_string_pretty(&index)?
754    )?;
755
756    println!("{}", "Metadata generation complete.".green());
757
758    Ok(())
759}
760
761/// Checks the integrity of all package definitions in the registry.
762///
763/// This runs `zoi doctor` on every `.pkg.lua` file in the registry.
764///
765/// # Errors
766///
767/// Returns an error if any package fails the health check.
768pub fn check(registry_root: &Path) -> Result<()> {
769    if !registry_root.join("repo.yaml").exists() {
770        return Err(anyhow!(
771            "Not a Zoi registry (missing repo.yaml). Run 'zoi reg init' first."
772        ));
773    }
774
775    println!("{} Checking registry integrity...", "::".bold().blue());
776
777    let mut errors = 0;
778    let mut warnings = 0;
779
780    for entry in WalkDir::new(registry_root)
781        .into_iter()
782        .filter_map(Result::ok)
783    {
784        if entry.file_type().is_file()
785            && entry.file_name().to_string_lossy().ends_with(".pkg.lua")
786        {
787            println!(
788                "  Checking {}...",
789                entry.path().display().to_string().cyan()
790            );
791            match pkg_doctor::run(entry.path(), None, None) {
792                Ok(report) => {
793                    for error in &report.errors {
794                        eprintln!("    {} {error}", "Error:".red().bold());
795                        errors += 1;
796                    }
797                    for warning in &report.warnings {
798                        println!(
799                            "    {} {warning}",
800                            "Warning:".yellow().bold()
801                        );
802                        warnings += 1;
803                    }
804                }
805                Err(e) => {
806                    eprintln!(
807                        "    {} Failed to parse package: {e}",
808                        "Error:".red().bold(),
809                    );
810                    errors += 1;
811                }
812            }
813        }
814    }
815
816    if errors > 0 {
817        return Err(anyhow!(
818            "Registry check failed with {errors} error(s) and {warnings} \
819             warning(s).",
820        ));
821    }
822
823    println!(
824        "{} Registry check passed with {} warning(s).",
825        "::".bold().green(),
826        warnings
827    );
828    Ok(())
829}