wasmer_cli/commands/package/
get.rs

1use std::path::Path;
2
3use anyhow::{Context, bail};
4use dialoguer::console::style;
5use wasmer_backend_api::WasmerClient;
6use wasmer_config::package::{Manifest, PackageIdent, PackageSource};
7
8use super::common::{
9    get_manifest, manifest_from_webc_metadata, package_web_url, registry_web_host,
10};
11use crate::commands::AsyncCliCommand;
12use crate::config::WasmerEnv;
13
14/// Show basic metadata of a package without unpacking or downloading it.
15///
16/// The package can be a directory containing a `wasmer.toml` file, a `.webc`
17/// file, or a package name from the registry. If no argument is given, the
18/// current directory is used.
19#[derive(clap::Parser, Debug)]
20pub struct PackageGet {
21    #[clap(flatten)]
22    pub env: WasmerEnv,
23
24    /// The package to show.
25    ///
26    /// This can be a path to a package directory or a `.webc` file, or the name
27    /// of a package in the registry (e.g. `wasmer/hello@=0.1.0`).
28    #[clap(default_value = ".")]
29    pub package: String,
30}
31
32#[async_trait::async_trait]
33impl AsyncCliCommand for PackageGet {
34    type Output = ();
35
36    async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
37        let (manifest, web_url) = self.load_manifest().await?;
38
39        let print_field = |label: &str, value: &str| {
40            println!("{:<13} {value}", style(format!("{label}:")).bold().dim());
41        };
42
43        if let Some(package) = manifest.package.as_ref() {
44            print_field("Name", package.name.as_deref().unwrap_or("<unnamed>"));
45            print_field(
46                "Version",
47                &package
48                    .version
49                    .as_ref()
50                    .map(|v| v.to_string())
51                    .unwrap_or_else(|| "<none>".to_string()),
52            );
53
54            if let Some(description) = &package.description {
55                print_field("Description", description);
56            }
57            if let Some(license) = &package.license {
58                print_field("License", license);
59            }
60            if let Some(homepage) = &package.homepage {
61                print_field("Homepage", homepage);
62            }
63            if let Some(repository) = &package.repository {
64                print_field("Repository", repository);
65            }
66            if let Some(entrypoint) = &package.entrypoint {
67                print_field("Entrypoint", entrypoint);
68            }
69            if package.private {
70                print_field("Private", "true");
71            }
72        } else {
73            println!("{}", style("Package has no metadata.").dim());
74        }
75
76        if !manifest.commands.is_empty() {
77            let commands = manifest
78                .commands
79                .iter()
80                .map(|c| c.get_name())
81                .collect::<Vec<_>>()
82                .join(", ");
83            print_field("Commands", &commands);
84        }
85
86        if !manifest.dependencies.is_empty() {
87            print_field("Dependencies", &manifest.dependencies.len().to_string());
88            for (name, version) in &manifest.dependencies {
89                println!("  {name} = {version}");
90            }
91        }
92
93        if let Some(web_url) = web_url {
94            print_field("URL", &web_url);
95        }
96
97        Ok(())
98    }
99}
100
101impl PackageGet {
102    /// Resolve the `package` argument into a [`Manifest`], whether it points to a
103    /// local package or a package in the registry.
104    ///
105    /// The second tuple element is a link to the package's page on the registry
106    /// web frontend, present only when the package was resolved from a named
107    /// registry lookup (local files and hashes have no such page).
108    async fn load_manifest(&self) -> anyhow::Result<(Manifest, Option<String>)> {
109        // A path on disk (directory with a `wasmer.toml`, or a `.webc` file)
110        // takes precedence over registry resolution.
111        let path = Path::new(&self.package);
112        if path.exists() {
113            let (_, manifest) = get_manifest(path)?;
114            return Ok((manifest, None));
115        }
116
117        let source: PackageSource = self.package.parse().with_context(|| {
118            format!(
119                "'{}' is not a file or directory on disk, or a valid package name",
120                self.package
121            )
122        })?;
123
124        match source {
125            PackageSource::Ident(PackageIdent::Named(id)) => {
126                let client = self.env.client_unauthennticated()?;
127
128                let version = id.version_or_default().to_string();
129                let version = if version == "*" {
130                    String::from("latest")
131                } else {
132                    version
133                };
134                let full_name = id.full_name();
135
136                let package = match wasmer_backend_api::query::get_package_version(
137                    &client,
138                    full_name.clone(),
139                    version.clone(),
140                )
141                .await?
142                {
143                    Some(package) => package,
144                    None => {
145                        // Echo the version the user actually typed rather than
146                        // the semver requirement it parsed into (`4.4.4` would
147                        // otherwise render as `^4.4.4`). `None` means the user
148                        // gave no version (we resolved `latest`).
149                        let requested = (version != "latest").then(|| {
150                            self.package
151                                .rsplit_once('@')
152                                .map_or(version.as_str(), |(_, v)| v)
153                        });
154                        return Err(version_not_found_error(&client, &full_name, requested).await);
155                    }
156                };
157
158                let json = package
159                    .pirita_manifest
160                    .as_ref()
161                    .context("the registry did not return a manifest for this package")?;
162                let webc_manifest: webc::metadata::Manifest = serde_json::from_str(&json.0)
163                    .context("could not parse the manifest returned by the registry")?;
164                let mut manifest = manifest_from_webc_metadata(&webc_manifest)?;
165
166                // Link to the package's page on the registry web frontend,
167                // using the concrete version the registry resolved for us.
168                let web_url = package_web_url(&client, &full_name, Some(&package.version));
169
170                // Prefer the authoritative metadata reported by the registry.
171                //
172                // `Package::from_manifest` strips name/version/description from
173                // the WAPM annotation when building the webc, so the manifest
174                // reconstructed from `pirita_manifest` is missing them. The
175                // registry exposes these fields directly, so re-inject them.
176                if let Some(pkg) = manifest.package.as_mut() {
177                    pkg.name = Some(full_name);
178                    pkg.version = Some(package.version.parse().with_context(|| {
179                        format!(
180                            "invalid version returned by the registry: '{}'",
181                            package.version
182                        )
183                    })?);
184                    // Take each registry field when present, otherwise keep
185                    // whatever the webc carried (license/homepage/repository
186                    // survive in the WAPM annotation; description does not).
187                    if !package.description.is_empty() {
188                        pkg.description = Some(package.description);
189                    }
190                    if package.license.is_some() {
191                        pkg.license = package.license;
192                    }
193                    if package.homepage.is_some() {
194                        pkg.homepage = package.homepage;
195                    }
196                    if package.repository.is_some() {
197                        pkg.repository = package.repository;
198                    }
199                }
200
201                Ok((manifest, Some(web_url)))
202            }
203            PackageSource::Ident(PackageIdent::Hash(hash)) => {
204                let client = self.env.client_unauthennticated()?;
205
206                let pkg = wasmer_backend_api::query::get_package_release(
207                    &client,
208                    &hash.to_string(),
209                )
210                .await?
211                .with_context(|| {
212                    format!(
213                        "Package with {hash} does not exist in the registry, or is not accessible"
214                    )
215                })?;
216
217                let image = pkg
218                    .webc_v3
219                    .or(pkg.webc)
220                    .context("the registry did not return a WebC image for this package")?;
221                let webc_manifest: webc::metadata::Manifest =
222                    serde_json::from_str(&image.manifest.0)
223                        .context("could not parse the manifest returned by the registry")?;
224
225                Ok((manifest_from_webc_metadata(&webc_manifest)?, None))
226            }
227            PackageSource::Path(p) => {
228                // Local paths will have already short-circuited at the very
229                // start of the command, so we should never get here,
230                // unless the local path is invalid or inaccessible.
231                bail!("no file or directory found at '{p}'")
232            }
233            PackageSource::Url(url) => {
234                bail!("showing a package directly from a URL is not supported: '{url}'")
235            }
236        }
237    }
238}
239
240/// Build a helpful error for when [`get_package_version`] returns nothing.
241///
242/// If the package exists but the requested version doesn't, the error lists the
243/// versions that are available; otherwise it reports the package as not found.
244///
245/// [`get_package_version`]: wasmer_backend_api::query::get_package_version
246async fn version_not_found_error(
247    client: &WasmerClient,
248    full_name: &str,
249    requested: Option<&str>,
250) -> anyhow::Error {
251    let registry = registry_web_host(client);
252
253    match wasmer_backend_api::query::get_package_version_numbers(client, full_name.to_string())
254        .await
255    {
256        Ok(Some(mut versions)) if !versions.is_empty() => {
257            sort_versions(&mut versions);
258            let available = versions.join(", ");
259            match requested {
260                Some(version) => anyhow::anyhow!(
261                    "package '{full_name}' has no version matching '{version}' in registry '{registry}'.\nAvailable versions: {available}"
262                ),
263                None => anyhow::anyhow!(
264                    "could not resolve the latest version of package '{full_name}' from registry '{registry}'.\nAvailable versions: {available}"
265                ),
266            }
267        }
268        // The package exists but has no published versions, or doesn't exist.
269        Ok(_) => {
270            anyhow::anyhow!("package '{full_name}' was not found in registry '{registry}'")
271        }
272        // Couldn't list versions; fall back to a generic message.
273        Err(_) => anyhow::anyhow!(
274            "could not retrieve package information for package '{full_name}' from registry '{registry}'"
275        ),
276    }
277}
278
279/// Sort version strings ascending, parsing them as semver where possible and
280/// falling back to lexical order for anything that doesn't parse.
281fn sort_versions(versions: &mut [String]) {
282    versions.sort_by(
283        |a, b| match (semver::Version::parse(a), semver::Version::parse(b)) {
284            (Ok(a), Ok(b)) => a.cmp(&b),
285            _ => a.cmp(b),
286        },
287    );
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[tokio::test]
295    async fn test_cmd_package_get_dir() {
296        let dir = tempfile::tempdir().unwrap();
297        std::fs::write(
298            dir.path().join("wasmer.toml"),
299            r#"
300[package]
301name = "wasmer/test"
302version = "1.2.3"
303description = "A test package"
304license = "MIT"
305"#,
306        )
307        .unwrap();
308
309        let cmd = PackageGet {
310            env: WasmerEnv::new(
311                crate::config::DEFAULT_WASMER_CACHE_DIR.clone(),
312                crate::config::DEFAULT_WASMER_CACHE_DIR.clone(),
313                None,
314                None,
315            ),
316            package: dir.path().to_str().unwrap().to_owned(),
317        };
318
319        cmd.run_async().await.unwrap();
320    }
321}