1use std::fs;
7use std::path::{Path, PathBuf};
8
9use anyhow::Result;
10use walkdir::WalkDir;
11use zoi_core::types::{self, InstallManifest, Scope};
12use zoi_core::{config, utils};
13
14use crate::resolve::{PackageRequest, get_db_root};
15
16pub fn get_store_base_dir(scope: Scope) -> Result<PathBuf> {
22 utils::get_store_base_dir(scope)
23}
24
25pub fn get_package_dir(
31 scope: Scope,
32 registry_handle: &str,
33 repo_path: &str,
34 package_name: &str
35) -> Result<PathBuf> {
36 let base_dir = get_store_base_dir(scope)?;
37 let package_id =
38 utils::generate_package_id(registry_handle, repo_path, package_name);
39 let package_dir_name =
40 utils::get_package_dir_name(&package_id, package_name);
41 Ok(base_dir.join(package_dir_name))
42}
43
44pub fn get_package_version_dir(
51 scope: Scope,
52 registry_handle: &str,
53 repo_path: &str,
54 package_name: &str,
55 version: &str
56) -> Result<PathBuf> {
57 let package_dir =
58 get_package_dir(scope, registry_handle, repo_path, package_name)?;
59 Ok(package_dir.join(version))
60}
61
62use rayon::prelude::*;
63
64pub fn get_installed_packages() -> Result<Vec<InstallManifest>> {
71 let scopes = [Scope::User, Scope::System, Scope::Project];
72
73 let installed: Vec<InstallManifest> = scopes
74 .into_par_iter()
75 .map(|scope| {
76 let mut manifests = Vec::new();
77 if let Ok(store_root) = get_store_base_dir(scope)
78 && store_root.exists()
79 && let Ok(entries) = fs::read_dir(store_root)
80 {
81 for entry in entries.flatten() {
82 let path = entry.path();
83 if !path.is_dir() {
84 continue;
85 }
86 let latest_path = path.join("latest");
87 if (latest_path.is_symlink() || latest_path.is_dir())
88 && let Ok(sub_entries) = fs::read_dir(&latest_path)
89 {
90 for sub_entry in sub_entries.flatten() {
91 let file_name = sub_entry
92 .file_name()
93 .to_string_lossy()
94 .to_string();
95 if file_name.starts_with("manifest")
96 && std::path::Path::new(&file_name)
97 .extension()
98 .is_some_and(|ext| {
99 ext.eq_ignore_ascii_case("yaml")
100 })
101 {
102 let manifest_path = sub_entry.path();
103 if manifest_path.exists()
104 && let Ok(content) =
105 fs::read_to_string(manifest_path)
106 && let Ok(manifest) =
107 serde_yaml::from_str::<InstallManifest>(
108 &content
109 )
110 {
111 manifests.push(manifest);
112 }
113 }
114 }
115 }
116 }
117 }
118 manifests
119 })
120 .flatten()
121 .collect();
122
123 let mut sorted_installed = installed;
124 sorted_installed.sort_by(|a, b| a.name.cmp(&b.name));
125 Ok(sorted_installed)
126}
127
128#[derive(Debug)]
130pub struct InstalledPackage {
131 pub name: String,
133 pub sub_package: Option<String>,
135 pub version: String,
137 pub repo: String,
139 pub package_type: zoi_core::types::PackageType
141}
142
143pub fn get_installed_packages_with_type() -> Result<Vec<InstalledPackage>> {
149 let manifests = get_installed_packages()?;
150 Ok(manifests
151 .into_iter()
152 .map(|m| InstalledPackage {
153 name: m.name,
154 sub_package: m.sub_package,
155 version: m.version,
156 repo: m.repo,
157 package_type: m.package_type
158 })
159 .collect())
160}
161
162pub fn is_package_installed(
170 package_name: &str,
171 sub_package_name: Option<&str>,
172 scope: Scope
173) -> Result<Option<InstallManifest>> {
174 let store_root = get_store_base_dir(scope)?;
175 if !store_root.exists() {
176 return Ok(None);
177 }
178
179 for entry in fs::read_dir(store_root)? {
180 let entry = entry?;
181 let path = entry.path();
182 if !path.is_dir() {
183 continue;
184 }
185
186 if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
187 let parts: Vec<&str> = file_name.splitn(2, '-').collect();
188 if parts.len() == 2
189 && parts.get(1) == Some(&package_name)
190 && parts.first().is_some_and(|p| p.len() == 32)
191 {
192 let latest_path = path.join("latest");
193 if (latest_path.is_symlink() || latest_path.is_dir())
194 && let Ok(entries) = fs::read_dir(&latest_path)
195 {
196 for entry in entries.filter_map(Result::ok) {
197 let file_name =
198 entry.file_name().to_string_lossy().to_string();
199 if file_name.starts_with("manifest")
200 && std::path::Path::new(&file_name)
201 .extension()
202 .is_some_and(|ext| {
203 ext.eq_ignore_ascii_case("yaml")
204 })
205 {
206 let manifest_path = entry.path();
207 if manifest_path.exists() {
208 let content =
209 fs::read_to_string(manifest_path)?;
210 let manifest: InstallManifest =
211 serde_yaml::from_str(&content)?;
212 if manifest.name == package_name
213 && manifest.sub_package.as_deref()
214 == sub_package_name
215 {
216 return Ok(Some(manifest));
217 }
218 }
219 }
220 }
221 }
222 }
223 }
224 }
225
226 Ok(None)
227}
228
229pub fn get_installed_manifests_in_scope(
236 scope: Scope
237) -> Result<Vec<InstallManifest>> {
238 let store_root = get_store_base_dir(scope)?;
239 if !store_root.exists() {
240 return Ok(Vec::new());
241 }
242
243 let mut manifests = Vec::new();
244 for entry in fs::read_dir(store_root)? {
245 let entry = entry?;
246 let path = entry.path();
247 if !path.is_dir() {
248 continue;
249 }
250
251 let latest_path = path.join("latest");
252 if !(latest_path.is_symlink() || latest_path.is_dir()) {
253 continue;
254 }
255
256 let Ok(entries) = fs::read_dir(&latest_path) else {
257 continue;
258 };
259
260 for entry in entries.filter_map(Result::ok) {
261 let file_name = entry.file_name().to_string_lossy().to_string();
262 if !file_name.starts_with("manifest")
263 || !std::path::Path::new(&file_name)
264 .extension()
265 .is_some_and(|ext| ext.eq_ignore_ascii_case("yaml"))
266 {
267 continue;
268 }
269
270 let manifest_path = entry.path();
271 if !manifest_path.exists() {
272 continue;
273 }
274
275 let content = fs::read_to_string(manifest_path)?;
276 let manifest: InstallManifest = serde_yaml::from_str(&content)?;
277 manifests.push(manifest);
278 }
279 }
280
281 Ok(manifests)
282}
283
284pub fn find_installed_manifests_matching(
294 request: &PackageRequest,
295 scope: Scope
296) -> Result<Vec<InstallManifest>> {
297 let manifests = get_installed_manifests_in_scope(scope)?;
298 Ok(manifests
299 .into_iter()
300 .filter(|manifest| {
301 manifest.name.eq_ignore_ascii_case(&request.name)
302 && manifest.sub_package == request.sub_package
303 && request.handle.as_ref().is_none_or(|handle| {
304 manifest.registry_handle.eq_ignore_ascii_case(handle)
305 })
306 && request
307 .repo
308 .as_ref()
309 .is_none_or(|repo| manifest.repo.eq_ignore_ascii_case(repo))
310 && request
311 .version_spec
312 .as_ref()
313 .is_none_or(|version| manifest.version == *version)
314 })
315 .collect())
316}
317
318pub fn package_source_string(
320 registry_handle: &str,
321 repo: &str,
322 name: &str,
323 sub_package: Option<&str>,
324 version: &str
325) -> String {
326 let registry_handle = registry_handle.trim();
327 let repo = repo.trim();
328 let name = name.trim();
329 let version = version.trim();
330
331 if let Some(sub_package) = sub_package {
332 format!(
333 "#{}@{}/{}:{}@{}",
334 registry_handle,
335 repo,
336 name,
337 sub_package.trim(),
338 version
339 )
340 } else {
341 format!("#{registry_handle}@{repo}/{name}@{version}")
342 }
343}
344
345pub fn installed_manifest_source(manifest: &InstallManifest) -> String {
347 package_source_string(
348 &manifest.registry_handle,
349 &manifest.repo,
350 &manifest.name,
351 manifest.sub_package.as_deref(),
352 &manifest.version
353 )
354}
355
356pub fn get_packages_from_repos(
363 repos: &[String]
364) -> Result<Vec<zoi_core::types::Package>> {
365 let db_root = get_db_root()?;
366 if !db_root.exists() {
367 return Err(anyhow::anyhow!(
368 "Package database not found. Please run 'zoi sync' first."
369 ));
370 }
371
372 let mut available = Vec::new();
373
374 for repo_name in repos {
375 let repo_path = db_root.join(repo_name);
376 if !repo_path.exists() {
377 continue;
378 }
379 for entry in WalkDir::new(repo_path).into_iter().filter_map(Result::ok)
380 {
381 if !entry.file_type().is_dir() {
382 continue;
383 }
384
385 let pkg_name = entry.file_name().to_string_lossy();
386 let pkg_file_path =
387 entry.path().join(format!("{pkg_name}.pkg.lua"));
388
389 if pkg_file_path.is_file() {
390 let pkg_file_path_str =
391 pkg_file_path.to_str().ok_or_else(|| {
392 anyhow::anyhow!(
393 "Package path contains invalid UTF-8: {}",
394 pkg_file_path.display()
395 )
396 })?;
397 let mut pkg: zoi_core::types::Package =
398 zoi_lua::parser::parse_lua_package(
399 pkg_file_path_str,
400 None,
401 None,
402 true
403 )?;
404
405 if let Ok(repo_subpath) = entry.path().strip_prefix(&db_root) {
406 let mut repo_path = repo_subpath
407 .to_string_lossy()
408 .to_string()
409 .replace('\\', "/");
410 let pkg_name_suffix = format!("/{}", pkg.name);
411 if repo_path.ends_with(&pkg_name_suffix) {
412 repo_path = repo_path
413 [..repo_path.len() - pkg_name_suffix.len()]
414 .to_string();
415 } else if repo_path == pkg.name {
416 repo_path = String::new();
417 }
418 pkg.repo = repo_path;
419 }
420
421 available.push(pkg);
422 }
423 }
424 }
425
426 available.sort_by(|a, b| a.name.cmp(&b.name));
427 Ok(available)
428}
429
430pub fn get_all_available_packages() -> Result<Vec<zoi_core::types::Package>> {
437 let config = config::read_config()?;
438 if let Some(handle) = config
439 .default_registry
440 .as_ref()
441 .map(|r| &r.handle)
442 .filter(|h| !h.is_empty())
443 {
444 let repos_with_handle: Vec<String> = config
445 .repos
446 .iter()
447 .map(|repo| format!("{handle}/{repo}"))
448 .collect();
449 get_packages_from_repos(&repos_with_handle)
450 } else {
451 Ok(Vec::new())
452 }
453}
454
455pub fn add_dependent(package_dir: &Path, dependent_id: &str) -> Result<()> {
461 let dependents_dir = package_dir.join("dependents");
462 fs::create_dir_all(&dependents_dir)?;
463 let dependent_file = dependents_dir.join(hex::encode(dependent_id));
464 fs::write(dependent_file, "")?;
465 Ok(())
466}
467
468pub fn remove_dependent(package_dir: &Path, dependent_id: &str) -> Result<()> {
474 let dependents_dir = package_dir.join("dependents");
475 if dependents_dir.exists() {
476 let dependent_file = dependents_dir.join(hex::encode(dependent_id));
477 if dependent_file.exists() {
478 fs::remove_file(dependent_file)?;
479 } else if let Some(pos) = dependent_id.rfind('@') {
480 let legacy_id = &dependent_id[..pos];
481 let legacy_file = dependents_dir.join(hex::encode(legacy_id));
482 if legacy_file.exists() {
483 fs::remove_file(legacy_file)?;
484 }
485 }
486 }
487 Ok(())
488}
489
490pub fn get_dependents(package_dir: &Path) -> Result<Vec<String>> {
496 let dependents_dir = package_dir.join("dependents");
497 let mut dependents = Vec::new();
498 if dependents_dir.exists() {
499 for entry in fs::read_dir(dependents_dir)? {
500 let entry = entry?;
501 let path = entry.path();
502 if path.is_file()
503 && let Some(file_name) =
504 path.file_name().and_then(|s| s.to_str())
505 && let Ok(decoded) = hex::decode(file_name)
506 && let Ok(dependent_id) = String::from_utf8(decoded)
507 {
508 dependents.push(dependent_id);
509 }
510 }
511 }
512 Ok(dependents)
513}
514
515pub fn write_manifest(manifest: &InstallManifest) -> Result<()> {
522 let version_dir = get_package_version_dir(
523 manifest.scope,
524 &manifest.registry_handle,
525 &manifest.repo,
526 &manifest.name,
527 &manifest.version
528 )?;
529 fs::create_dir_all(&version_dir)?;
530
531 let manifest_filename = if let Some(sub) = &manifest.sub_package {
532 format!("manifest-{sub}.yaml")
533 } else {
534 "manifest.yaml".to_string()
535 };
536 let manifest_path = version_dir.join(manifest_filename);
537
538 let content = serde_yaml::to_string(&manifest)?;
539 fs::write(manifest_path, content)?;
540
541 let package_dir = get_package_dir(
542 manifest.scope,
543 &manifest.registry_handle,
544 &manifest.repo,
545 &manifest.name
546 )?;
547 let latest_symlink_path = package_dir.join("latest");
548 zoi_core::utils::symlink_dir(&version_dir, &latest_symlink_path)?;
549
550 Ok(())
551}
552
553pub fn get_package_source_path(manifest: &InstallManifest) -> Result<PathBuf> {
559 let version_dir = get_package_version_dir(
560 manifest.scope,
561 &manifest.registry_handle,
562 &manifest.repo,
563 &manifest.name,
564 &manifest.version
565 )?;
566 Ok(version_dir.join("package.pkg.lua"))
567}
568
569pub fn persist_package_source(
575 manifest: &InstallManifest,
576 source_path: &Path
577) -> Result<()> {
578 let stored_source_path = get_package_source_path(manifest)?;
579 if let Some(parent) = stored_source_path.parent() {
580 fs::create_dir_all(parent)?;
581 }
582 fs::copy(source_path, stored_source_path)?;
583 Ok(())
584}
585
586pub fn update_manifest_reason(
592 manifest: &InstallManifest,
593 new_reason: types::InstallReason
594) -> Result<()> {
595 let mut updated_manifest = manifest.clone();
596 updated_manifest.reason = new_reason;
597 write_manifest(&updated_manifest)?;
598 Ok(())
599}