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