1use crate::resolve::{PackageRequest, get_db_root};
2use anyhow::Result;
3use std::fs;
4use std::path::{Path, PathBuf};
5use walkdir::WalkDir;
6use zoi_core::config;
7use zoi_core::types::{self, InstallManifest, Scope};
8use zoi_core::utils;
9
10pub fn get_store_base_dir(scope: Scope) -> Result<PathBuf> {
11 utils::get_store_base_dir(scope)
12}
13
14pub fn get_package_dir(
15 scope: Scope,
16 registry_handle: &str,
17 repo_path: &str,
18 package_name: &str,
19) -> Result<PathBuf> {
20 let base_dir = get_store_base_dir(scope)?;
21 let package_id = utils::generate_package_id(registry_handle, repo_path, package_name);
22 let package_dir_name = utils::get_package_dir_name(&package_id, package_name);
23 Ok(base_dir.join(package_dir_name))
24}
25
26pub fn get_package_version_dir(
27 scope: Scope,
28 registry_handle: &str,
29 repo_path: &str,
30 package_name: &str,
31 version: &str,
32) -> Result<PathBuf> {
33 let package_dir = get_package_dir(scope, registry_handle, repo_path, package_name)?;
34 Ok(package_dir.join(version))
35}
36
37use rayon::prelude::*;
38
39pub fn get_installed_packages() -> Result<Vec<InstallManifest>> {
40 let scopes = [Scope::User, Scope::System, Scope::Project];
41
42 let installed: Vec<InstallManifest> = scopes
43 .into_par_iter()
44 .map(|scope| {
45 let mut manifests = Vec::new();
46 if let Ok(store_root) = get_store_base_dir(scope)
47 && store_root.exists()
48 && let Ok(entries) = fs::read_dir(store_root)
49 {
50 for entry in entries.flatten() {
51 let path = entry.path();
52 if !path.is_dir() {
53 continue;
54 }
55 let latest_path = path.join("latest");
56 if (latest_path.is_symlink() || latest_path.is_dir())
57 && let Ok(sub_entries) = fs::read_dir(&latest_path)
58 {
59 for sub_entry in sub_entries.flatten() {
60 let file_name = sub_entry.file_name().to_string_lossy().to_string();
61 if file_name.starts_with("manifest") && file_name.ends_with(".yaml") {
62 let manifest_path = sub_entry.path();
63 if manifest_path.exists()
64 && let Ok(content) = fs::read_to_string(manifest_path)
65 && let Ok(manifest) =
66 serde_yaml::from_str::<InstallManifest>(&content)
67 {
68 manifests.push(manifest);
69 }
70 }
71 }
72 }
73 }
74 }
75 manifests
76 })
77 .flatten()
78 .collect();
79
80 let mut sorted_installed = installed;
81 sorted_installed.sort_by(|a, b| a.name.cmp(&b.name));
82 Ok(sorted_installed)
83}
84
85#[derive(Debug)]
86pub struct InstalledPackage {
87 pub name: String,
88 pub sub_package: Option<String>,
89 pub version: String,
90 pub repo: String,
91 pub package_type: zoi_core::types::PackageType,
92}
93
94pub fn get_installed_packages_with_type() -> Result<Vec<InstalledPackage>> {
95 let manifests = get_installed_packages()?;
96 Ok(manifests
97 .into_iter()
98 .map(|m| InstalledPackage {
99 name: m.name,
100 sub_package: m.sub_package,
101 version: m.version,
102 repo: m.repo,
103 package_type: m.package_type,
104 })
105 .collect())
106}
107
108pub fn is_package_installed(
109 package_name: &str,
110 sub_package_name: Option<&str>,
111 scope: Scope,
112) -> Result<Option<InstallManifest>> {
113 let store_root = get_store_base_dir(scope)?;
114 if !store_root.exists() {
115 return Ok(None);
116 }
117
118 for entry in fs::read_dir(store_root)? {
119 let entry = entry?;
120 let path = entry.path();
121 if !path.is_dir() {
122 continue;
123 }
124
125 if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
126 let parts: Vec<&str> = file_name.splitn(2, '-').collect();
127 if parts.len() == 2 && parts[1] == package_name && parts[0].len() == 32 {
128 let latest_path = path.join("latest");
129 if (latest_path.is_symlink() || latest_path.is_dir())
130 && let Ok(entries) = fs::read_dir(&latest_path)
131 {
132 for entry in entries.filter_map(Result::ok) {
133 let file_name = entry.file_name().to_string_lossy().to_string();
134 if file_name.starts_with("manifest") && file_name.ends_with(".yaml") {
135 let manifest_path = entry.path();
136 if manifest_path.exists() {
137 let content = fs::read_to_string(manifest_path)?;
138 let manifest: InstallManifest = serde_yaml::from_str(&content)?;
139 if manifest.name == package_name
140 && manifest.sub_package.as_deref() == sub_package_name
141 {
142 return Ok(Some(manifest));
143 }
144 }
145 }
146 }
147 }
148 }
149 }
150 }
151
152 Ok(None)
153}
154
155pub fn get_installed_manifests_in_scope(scope: Scope) -> Result<Vec<InstallManifest>> {
156 let store_root = get_store_base_dir(scope)?;
157 if !store_root.exists() {
158 return Ok(Vec::new());
159 }
160
161 let mut manifests = Vec::new();
162 for entry in fs::read_dir(store_root)? {
163 let entry = entry?;
164 let path = entry.path();
165 if !path.is_dir() {
166 continue;
167 }
168
169 let latest_path = path.join("latest");
170 if !(latest_path.is_symlink() || latest_path.is_dir()) {
171 continue;
172 }
173
174 let Ok(entries) = fs::read_dir(&latest_path) else {
175 continue;
176 };
177
178 for entry in entries.filter_map(Result::ok) {
179 let file_name = entry.file_name().to_string_lossy().to_string();
180 if !file_name.starts_with("manifest") || !file_name.ends_with(".yaml") {
181 continue;
182 }
183
184 let manifest_path = entry.path();
185 if !manifest_path.exists() {
186 continue;
187 }
188
189 let content = fs::read_to_string(manifest_path)?;
190 let manifest: InstallManifest = serde_yaml::from_str(&content)?;
191 manifests.push(manifest);
192 }
193 }
194
195 Ok(manifests)
196}
197
198pub fn find_installed_manifests_matching(
199 request: &PackageRequest,
200 scope: Scope,
201) -> Result<Vec<InstallManifest>> {
202 let manifests = get_installed_manifests_in_scope(scope)?;
203 Ok(manifests
204 .into_iter()
205 .filter(|manifest| {
206 manifest.name == request.name
207 && manifest.sub_package == request.sub_package
208 && request
209 .handle
210 .as_ref()
211 .is_none_or(|handle| manifest.registry_handle == *handle)
212 && request
213 .repo
214 .as_ref()
215 .is_none_or(|repo| manifest.repo == *repo)
216 && request
217 .version_spec
218 .as_ref()
219 .is_none_or(|version| manifest.version == *version)
220 })
221 .collect())
222}
223
224pub fn package_source_string(
225 registry_handle: &str,
226 repo: &str,
227 name: &str,
228 sub_package: Option<&str>,
229 version: &str,
230) -> String {
231 let registry_handle = registry_handle.trim();
232 let repo = repo.trim();
233 let name = name.trim();
234 let version = version.trim();
235
236 if let Some(sub_package) = sub_package {
237 format!(
238 "#{}@{}/{}:{}@{}",
239 registry_handle,
240 repo,
241 name,
242 sub_package.trim(),
243 version
244 )
245 } else {
246 format!("#{}@{}/{}@{}", registry_handle, repo, name, version)
247 }
248}
249
250pub fn installed_manifest_source(manifest: &InstallManifest) -> String {
251 package_source_string(
252 &manifest.registry_handle,
253 &manifest.repo,
254 &manifest.name,
255 manifest.sub_package.as_deref(),
256 &manifest.version,
257 )
258}
259
260pub fn get_packages_from_repos(repos: &[String]) -> Result<Vec<zoi_core::types::Package>> {
261 let db_root = get_db_root()?;
262 if !db_root.exists() {
263 return Err(anyhow::anyhow!(
264 "Package database not found. Please run 'zoi sync' first."
265 ));
266 }
267
268 let mut available = Vec::new();
269
270 for repo_name in repos {
271 let repo_path = db_root.join(repo_name);
272 if !repo_path.exists() {
273 continue;
274 }
275 for entry in WalkDir::new(repo_path).into_iter().filter_map(Result::ok) {
276 if !entry.file_type().is_dir() {
277 continue;
278 }
279
280 let pkg_name = entry.file_name().to_string_lossy();
281 let pkg_file_path = entry.path().join(format!("{}.pkg.lua", pkg_name));
282
283 if pkg_file_path.is_file() {
284 let pkg_file_path_str = pkg_file_path.to_str().ok_or_else(|| {
285 anyhow::anyhow!("Package path contains invalid UTF-8: {:?}", pkg_file_path)
286 })?;
287 let mut pkg: zoi_core::types::Package =
288 zoi_lua::parser::parse_lua_package(pkg_file_path_str, None, None, true)?;
289
290 if let Ok(repo_subpath) = entry.path().strip_prefix(&db_root) {
291 let mut repo_path = repo_subpath
292 .to_string_lossy()
293 .to_string()
294 .replace('\\', "/");
295 let pkg_name_suffix = format!("/{}", pkg.name);
296 if repo_path.ends_with(&pkg_name_suffix) {
297 repo_path =
298 repo_path[..repo_path.len() - pkg_name_suffix.len()].to_string();
299 } else if repo_path == pkg.name {
300 repo_path = String::new();
301 }
302 pkg.repo = repo_path;
303 }
304
305 available.push(pkg);
306 }
307 }
308 }
309
310 available.sort_by(|a, b| a.name.cmp(&b.name));
311 Ok(available)
312}
313
314pub fn get_all_available_packages() -> Result<Vec<zoi_core::types::Package>> {
315 let config = config::read_config()?;
316 if let Some(handle) = config
317 .default_registry
318 .as_ref()
319 .map(|r| &r.handle)
320 .filter(|h| !h.is_empty())
321 {
322 let repos_with_handle: Vec<String> = config
323 .repos
324 .iter()
325 .map(|repo| format!("{}/{}", handle, repo))
326 .collect();
327 get_packages_from_repos(&repos_with_handle)
328 } else {
329 Ok(Vec::new())
330 }
331}
332
333pub fn add_dependent(package_dir: &Path, dependent_id: &str) -> Result<()> {
334 let dependents_dir = package_dir.join("dependents");
335 fs::create_dir_all(&dependents_dir)?;
336 let dependent_file = dependents_dir.join(hex::encode(dependent_id));
337 fs::write(dependent_file, "")?;
338 Ok(())
339}
340
341pub fn remove_dependent(package_dir: &Path, dependent_id: &str) -> Result<()> {
342 let dependents_dir = package_dir.join("dependents");
343 if dependents_dir.exists() {
344 let dependent_file = dependents_dir.join(hex::encode(dependent_id));
345 if dependent_file.exists() {
346 fs::remove_file(dependent_file)?;
347 } else if let Some(pos) = dependent_id.rfind('@') {
348 let legacy_id = &dependent_id[..pos];
349 let legacy_file = dependents_dir.join(hex::encode(legacy_id));
350 if legacy_file.exists() {
351 fs::remove_file(legacy_file)?;
352 }
353 }
354 }
355 Ok(())
356}
357
358pub fn get_dependents(package_dir: &Path) -> Result<Vec<String>> {
359 let dependents_dir = package_dir.join("dependents");
360 let mut dependents = Vec::new();
361 if dependents_dir.exists() {
362 for entry in fs::read_dir(dependents_dir)? {
363 let entry = entry?;
364 let path = entry.path();
365 if path.is_file()
366 && let Some(file_name) = path.file_name().and_then(|s| s.to_str())
367 && let Ok(decoded) = hex::decode(file_name)
368 && let Ok(dependent_id) = String::from_utf8(decoded)
369 {
370 dependents.push(dependent_id);
371 }
372 }
373 }
374 Ok(dependents)
375}
376
377pub fn write_manifest(manifest: &InstallManifest) -> Result<()> {
378 let version_dir = get_package_version_dir(
379 manifest.scope,
380 &manifest.registry_handle,
381 &manifest.repo,
382 &manifest.name,
383 &manifest.version,
384 )?;
385 fs::create_dir_all(&version_dir)?;
386
387 let manifest_filename = if let Some(sub) = &manifest.sub_package {
388 format!("manifest-{}.yaml", sub)
389 } else {
390 "manifest.yaml".to_string()
391 };
392 let manifest_path = version_dir.join(manifest_filename);
393
394 let content = serde_yaml::to_string(&manifest)?;
395 fs::write(manifest_path, content)?;
396
397 let package_dir = get_package_dir(
398 manifest.scope,
399 &manifest.registry_handle,
400 &manifest.repo,
401 &manifest.name,
402 )?;
403 let latest_symlink_path = package_dir.join("latest");
404 zoi_core::utils::symlink_dir(&version_dir, &latest_symlink_path)?;
405
406 Ok(())
407}
408
409pub fn get_package_source_path(manifest: &InstallManifest) -> Result<PathBuf> {
410 let version_dir = get_package_version_dir(
411 manifest.scope,
412 &manifest.registry_handle,
413 &manifest.repo,
414 &manifest.name,
415 &manifest.version,
416 )?;
417 Ok(version_dir.join("package.pkg.lua"))
418}
419
420pub fn persist_package_source(manifest: &InstallManifest, source_path: &Path) -> Result<()> {
421 let stored_source_path = get_package_source_path(manifest)?;
422 if let Some(parent) = stored_source_path.parent() {
423 fs::create_dir_all(parent)?;
424 }
425 fs::copy(source_path, stored_source_path)?;
426 Ok(())
427}
428
429pub fn update_manifest_reason(
430 manifest: &InstallManifest,
431 new_reason: types::InstallReason,
432) -> Result<()> {
433 let mut updated_manifest = manifest.clone();
434 updated_manifest.reason = new_reason;
435 write_manifest(&updated_manifest)?;
436 Ok(())
437}