1use crate::pkg::{local, resolve, types};
2use crate::utils;
3use anyhow::{Result, anyhow};
4use colored::*;
5use std::fs;
6
7fn print_dependency_group(group: &types::DependencyGroup, indent: usize) {
8 let prefix = " ".repeat(indent * 2);
9 let mut count = 0;
10
11 let required = group.required();
12 if !required.is_empty() {
13 for dep in required {
14 println!("{}- {} (required)", prefix, dep);
15 count += 1;
16 }
17 }
18
19 let options = group.options();
20 if !options.is_empty() {
21 for opt_group in options {
22 println!(
23 "{}{}: {} (choose {})",
24 prefix,
25 opt_group.name.bold(),
26 opt_group.desc,
27 if opt_group.all { "any" } else { "one" }
28 );
29 for dep in &opt_group.depends {
30 println!("{} - {}", prefix, dep);
31 count += 1;
32 }
33 }
34 }
35
36 let optional = group.optional();
37 if !optional.is_empty() {
38 for dep in optional {
39 println!("{}- {} (optional)", prefix, dep);
40 count += 1;
41 }
42 }
43
44 if let types::DependencyGroup::Complex(complex) = group
45 && let Some(subs) = &complex.sub_packages
46 {
47 for (sub_name, sub_group) in subs {
48 println!("{}{}:", prefix, sub_name.bold().cyan());
49 print_dependency_group(sub_group, indent + 1);
50 count += 1;
51 }
52 }
53
54 if count == 0 {
55 println!("{}- {}", prefix, "None".italic());
56 }
57}
58
59pub fn run(source: &str, raw: bool, purl: bool) -> Result<()> {
60 let mut source_str = source.trim().to_string();
61 if purl {
62 println!(
63 "{} Fetching PURL package '{}'...",
64 "::".bold().blue(),
65 source_str
66 );
67 source_str = crate::pkg::purl::fetch_and_store_purl_package(&source_str)?;
68 }
69 let source = source_str.as_str();
70 let resolved_source = resolve::resolve_source(source, None, false, false)?;
71
72 if raw {
73 let content = fs::read_to_string(&resolved_source.path)?;
74 println!("{content}");
75 return Ok(());
76 }
77 let mut pkg: types::Package = crate::pkg::lua::parser::parse_lua_package(
78 resolved_source.path.to_str().ok_or_else(|| {
79 anyhow!(
80 "Path contains invalid UTF-8 characters: {:?}",
81 resolved_source.path
82 )
83 })?,
84 None,
85 None,
86 false,
87 )?;
88 if let Some(repo_name) = resolved_source.repo_name {
89 pkg.repo = repo_name;
90 }
91 pkg.version = Some(
92 resolve::get_default_version(&pkg, resolved_source.registry_handle.as_deref())
93 .unwrap_or_else(|_| "N/A".to_string()),
94 );
95
96 let request = resolve::parse_source_string(source)?;
97 let installed_manifest = match find_installed_manifest(&request) {
98 Ok(manifest) => manifest,
99 Err(e) => {
100 eprintln!("Warning: could not check installation status: {}", e);
101 None
102 }
103 };
104
105 print_beautiful(&pkg, installed_manifest.as_ref());
106 Ok(())
107}
108
109fn find_installed_manifest(
110 request: &crate::pkg::resolve::PackageRequest,
111) -> Result<Option<types::InstallManifest>> {
112 let mut candidates = Vec::new();
113 for scope in [
114 types::Scope::User,
115 types::Scope::System,
116 types::Scope::Project,
117 ] {
118 candidates.extend(local::find_installed_manifests_matching(request, scope)?);
119 }
120
121 if candidates.is_empty() {
122 return Ok(None);
123 }
124
125 let package_name = if let Some(sub) = &request.sub_package {
126 format!("{}:{}", request.name, sub)
127 } else {
128 request.name.clone()
129 };
130 Ok(Some(
131 crate::cmd::installed_select::choose_installed_manifest(&package_name, &candidates, false)?,
132 ))
133}
134
135fn print_beautiful(
136 pkg: &crate::pkg::types::Package,
137 installed_manifest: Option<&types::InstallManifest>,
138) {
139 let mut version_display = if pkg.epoch > 0 {
140 format!("{}:", pkg.epoch)
141 } else {
142 String::new()
143 };
144
145 version_display.push_str(pkg.version.as_deref().unwrap_or("N/A"));
146
147 if pkg.revision != "1" {
148 version_display = format!("{}-{}", version_display, pkg.revision);
149 }
150
151 let repo_display = if pkg.repo.starts_with("git/") {
152 format!("#git@{}", &pkg.repo[4..])
153 } else {
154 pkg.repo.clone()
155 };
156
157 println!(
158 "{} {} - {}",
159 pkg.name.bold().green(),
160 version_display.dimmed(),
161 repo_display
162 );
163 if let Some(website) = &pkg.website {
164 println!("Website: {}", website.cyan().underline());
165 }
166 if !pkg.git.is_empty() {
167 println!("Git Repo: {}", pkg.git.cyan().underline());
168 }
169 println!("{}", pkg.description);
170
171 if let Some(subs) = &pkg.sub_packages {
172 println!("{}: {}", "Sub-packages".bold(), subs.join(", "));
173 if let Some(main_subs) = &pkg.main_subs {
174 println!("{}: {}", "Main sub-packages".bold(), main_subs.join(", "));
175 }
176 }
177
178 if let Some(manifest) = installed_manifest {
179 let status_text = if let Some(sub) = &manifest.sub_package {
180 format!("Installed ({})", sub)
181 } else {
182 "Installed".to_string()
183 };
184
185 let installed_version_display = if manifest.revision != "1" {
186 format!("{}-{}", manifest.version, manifest.revision)
187 } else {
188 manifest.version.clone()
189 };
190
191 println!(
192 "{}: {} ({})",
193 "Status".bold(),
194 status_text.green(),
195 installed_version_display
196 );
197 } else {
198 println!("{}: {}", "Status".bold(), "Not Installed".red());
199 }
200
201 if !pkg.license.is_empty() {
202 println!("{}: {}", "License".bold(), pkg.license);
203 utils::check_license(&pkg.license);
204 }
205
206 let mut maintainer_line = format!(
207 "{}: {} <{}>",
208 "Maintainer".bold(),
209 pkg.maintainer.name,
210 pkg.maintainer.email
211 );
212 if let Some(website) = &pkg.maintainer.website {
213 maintainer_line.push_str(&format!(" - {}", website.cyan().underline()));
214 }
215 println!("{}", maintainer_line);
216
217 if let Some(author) = &pkg.author {
218 let mut author_line = format!("{}: {}", "Author".bold(), author.name);
219 if let Some(email) = &author.email {
220 author_line.push_str(&format!(" <{}>", email));
221 }
222 if let Some(website) = &author.website {
223 author_line.push_str(&format!(" - {}", website.cyan().underline()));
224 }
225 println!("{}", author_line);
226 }
227
228 let type_display = match pkg.package_type {
229 crate::pkg::types::PackageType::Package => "Package",
230 crate::pkg::types::PackageType::Collection => "Collection",
231 crate::pkg::types::PackageType::App => "App",
232 crate::pkg::types::PackageType::Extension => "Extension",
233 };
234 println!("{}: {}", "Type".bold(), type_display);
235
236 let scope_display = match pkg.scope {
237 crate::pkg::types::Scope::User => "User",
238 crate::pkg::types::Scope::System => "System",
239 crate::pkg::types::Scope::Project => "Project",
240 };
241 println!("{}: {}", "Scope".bold(), scope_display);
242
243 if !pkg.tags.is_empty() {
244 println!("{}: {}", "Tags".bold(), pkg.tags.join(", "));
245 }
246
247 if let Some(bins) = &pkg.bins
248 && !bins.is_empty()
249 {
250 println!("{}: {}", "Provides".bold(), bins.join(", ").green());
251 }
252
253 if let Some(conflicts) = &pkg.conflicts
254 && !conflicts.is_empty()
255 {
256 println!("{}: {}", "Conflicts".bold(), conflicts.join(", ").red());
257 }
258
259 if pkg.package_type == crate::pkg::types::PackageType::Package {
260 println!("{}: {}", "Available types".bold(), pkg.types.join(", "));
261 }
262
263 if let Some(service) = &pkg.service {
264 println!("\n{}:", "Service".bold());
265 println!(" Run: {}", service.run.cyan());
266 println!(
267 " Run at load: {}",
268 if service.run_at_load {
269 "Yes".green()
270 } else {
271 "No".yellow()
272 }
273 );
274 }
275
276 if let Some(deps) = &pkg.dependencies {
277 println!("\n{}:", "Dependencies".bold());
278
279 if let Some(runtime) = &deps.runtime {
280 println!(" Runtime:");
281 print_dependency_group(runtime, 2);
282 }
283
284 if let Some(build_deps) = &deps.build {
285 println!(" Build Dependencies:");
286 match build_deps {
287 types::BuildDependencies::Group(group) => {
288 print_dependency_group(group, 2);
289 }
290 types::BuildDependencies::Typed(typed_build_deps) => {
291 for (name, group) in &typed_build_deps.types {
292 println!(" {}:", name.cyan());
293 print_dependency_group(group, 3);
294 }
295 }
296 }
297 }
298
299 if let Some(test_deps) = &deps.test {
300 println!(" Test Dependencies:");
301 print_dependency_group(test_deps, 2);
302 }
303 }
304}