wasmer_cli/utils/
prompts.rs

1use anyhow::Context;
2use colored::Colorize;
3use dialoguer::{Select, theme::ColorfulTheme};
4use wasmer_backend_api::WasmerClient;
5use wasmer_config::package::NamedPackageIdent;
6
7pub fn prompt_for_ident(message: &str, default: Option<&str>) -> Result<String, anyhow::Error> {
8    loop {
9        let theme = ColorfulTheme::default();
10        let diag = dialoguer::Input::with_theme(&theme)
11            .with_prompt(message)
12            .with_initial_text(default.unwrap_or_default());
13
14        // if let Some(val) = &validator {
15        //     diag.validate_with(val);
16        // }
17
18        let raw: String = diag.interact_text()?;
19        let val = raw.trim();
20        if !val.is_empty() {
21            break Ok(val.to_string());
22        }
23    }
24}
25
26/// Ask a user for an application name.
27///
28/// Will continue looping until the user provides a valid name that contains
29/// neither dots nor spaces. Returns an error if there are issues with
30/// the input interaction.
31pub fn prompt_for_app_ident(message: &str, default: Option<&str>) -> Result<String, anyhow::Error> {
32    loop {
33        let theme = ColorfulTheme::default();
34        let diag = dialoguer::Input::with_theme(&theme)
35            .with_prompt(message)
36            .with_initial_text(default.unwrap_or_default());
37
38        let raw: String = diag.interact_text()?;
39        let val = raw.trim();
40        if val.is_empty() {
41            continue;
42        }
43        if val.contains('.') || val.contains(' ') {
44            eprintln!("The name must not contain dots or spaces. Please try again.");
45            continue;
46        }
47        return Ok(val.to_string());
48    }
49}
50
51/// Ask a user for a package name.
52///
53/// Will continue looping until the user provides a valid name.
54pub fn prompt_for_package_ident(
55    message: &str,
56    default: Option<&str>,
57) -> Result<NamedPackageIdent, anyhow::Error> {
58    loop {
59        let theme = ColorfulTheme::default();
60        let raw: String = dialoguer::Input::with_theme(&theme)
61            .with_prompt(message)
62            .with_initial_text(default.unwrap_or_default())
63            .interact_text()
64            .context("could not read user input")?;
65
66        match raw.parse::<NamedPackageIdent>() {
67            Ok(p) => break Ok(p),
68            Err(err) => {
69                eprintln!("invalid package name: {err}");
70            }
71        }
72    }
73}
74
75/// Defines how to check for a package.
76pub enum PackageCheckMode {
77    /// The package must exist in the registry.
78    MustExist,
79    /// The package must NOT exist in the registry.
80    #[allow(dead_code)]
81    MustNotExist,
82}
83
84/// Ask a user for a package version.
85///
86/// Will continue looping until the user provides a valid version.
87pub fn prompt_for_package_version(
88    message: &str,
89    default: Option<&str>,
90) -> Result<semver::Version, anyhow::Error> {
91    loop {
92        let theme = ColorfulTheme::default();
93        let raw: String = dialoguer::Input::with_theme(&theme)
94            .with_prompt(message)
95            .with_initial_text(default.unwrap_or_default())
96            .interact_text()
97            .context("could not read user input")?;
98
99        match raw.parse::<semver::Version>() {
100            Ok(p) => break Ok(p),
101            Err(err) => {
102                eprintln!("invalid package version: {err}");
103            }
104        }
105    }
106}
107
108/// Ask for a package name.
109///
110/// Will continue looping until the user provides a valid name.
111///
112/// If an API is provided, will check if the package exists.
113pub async fn prompt_for_package(
114    message: &str,
115    default: Option<&str>,
116    check: Option<PackageCheckMode>,
117    client: Option<&WasmerClient>,
118) -> Result<
119    (
120        NamedPackageIdent,
121        Option<wasmer_backend_api::types::Package>,
122    ),
123    anyhow::Error,
124> {
125    loop {
126        let ident = prompt_for_package_ident(message, default)?;
127
128        if let Some(check) = &check {
129            let api = client.expect("Check mode specified, but no API provided");
130
131            let pkg = if let Some(v) = ident.version_opt() {
132                wasmer_backend_api::query::get_package_version(
133                    api,
134                    ident.full_name(),
135                    v.to_string(),
136                )
137                .await
138                .context("could not query backend for package")?
139                .map(|p| p.package)
140            } else {
141                wasmer_backend_api::query::get_package(api, ident.to_string())
142                    .await
143                    .context("could not query backend for package")?
144            };
145
146            match check {
147                PackageCheckMode::MustExist => {
148                    if let Some(pkg) = pkg {
149                        let mut ident = ident;
150                        if let Some(v) = &pkg.last_version {
151                            ident.tag =
152                                Some(wasmer_config::package::Tag::VersionReq(v.version.parse()?));
153                        }
154                        break Ok((ident, Some(pkg)));
155                    } else {
156                        eprintln!("Package '{ident}' does not exist");
157                    }
158                }
159                PackageCheckMode::MustNotExist => {
160                    if pkg.is_none() {
161                        break Ok((ident, None));
162                    } else {
163                        eprintln!("Package '{ident}' already exists");
164                    }
165                }
166            }
167        } else {
168            break Ok((ident, None));
169        }
170    }
171}
172
173/// Prompt for a namespace.
174///
175/// Will either show a select with all available namespaces based on the `user`
176/// argument, or present a basic text input.
177///
178/// The username will be included as an option.
179pub fn prompt_for_namespace(
180    message: &str,
181    default: Option<&str>,
182    user: Option<&wasmer_backend_api::types::UserWithNamespaces>,
183) -> Result<String, anyhow::Error> {
184    if let Some(user) = user {
185        let namespaces = user
186            .namespaces
187            .edges
188            .clone()
189            .into_iter()
190            .flatten()
191            .filter_map(|e| e.node)
192            .collect::<Vec<_>>();
193
194        let labels = [user.username.clone()]
195            .into_iter()
196            .chain(namespaces.iter().map(|ns| ns.global_name.clone()))
197            .collect::<Vec<_>>();
198
199        let selection_index = Select::with_theme(&ColorfulTheme::default())
200            .with_prompt(message)
201            .default(0)
202            .items(&labels)
203            .interact()
204            .context("could not read user input")?;
205
206        Ok(labels[selection_index].clone())
207    } else {
208        loop {
209            let theme = ColorfulTheme::default();
210            let value = dialoguer::Input::<String>::with_theme(&theme)
211                .with_prompt(message)
212                .with_initial_text(default.map(|x| x.trim().to_string()).unwrap_or_default())
213                .interact_text()
214                .context("could not read user input")?
215                .trim()
216                .to_string();
217
218            if !value.is_empty() {
219                break Ok(value);
220            }
221        }
222    }
223}
224
225/// Prompt for an app name.
226/// If an api provided, will check if an app with the givne alias already exists.
227#[allow(dead_code)]
228pub async fn prompt_new_app_name(
229    message: &str,
230    default: Option<&str>,
231    namespace: &str,
232    api: Option<&WasmerClient>,
233) -> Result<String, anyhow::Error> {
234    loop {
235        let ident = prompt_for_ident(message, default)?;
236
237        if ident.len() < 5 {
238            eprintln!(
239                "{}: Name is too short. It must be longer than 5 characters.",
240                "WARN".bold().yellow()
241            )
242        } else if let Some(api) = &api {
243            let app = wasmer_backend_api::query::get_app(api, namespace.to_string(), ident.clone())
244                .await?;
245            eprint!("Checking name availability... ");
246            if app.is_some() {
247                eprintln!(
248                    "{}",
249                    format!(
250                        "app {} already exists in namespace {}",
251                        ident.bold(),
252                        namespace.bold()
253                    )
254                    .yellow()
255                );
256            } else {
257                eprintln!("{}", "available!".bold().green());
258                break Ok(ident);
259            }
260        }
261    }
262}
263
264/// Prompt for an app name.
265/// If an api provided, will check if an app with the givne alias already exists.
266#[allow(dead_code)]
267pub async fn prompt_new_app_alias(
268    message: &str,
269    default: Option<&str>,
270    api: Option<&WasmerClient>,
271) -> Result<String, anyhow::Error> {
272    loop {
273        let ident = prompt_for_ident(message, default)?;
274
275        if let Some(api) = &api {
276            let app = wasmer_backend_api::query::get_app_by_alias(api, ident.clone()).await?;
277            eprintln!("Checking name availability...");
278            if app.is_some() {
279                eprintln!(
280                    "{}: alias '{}' already exists - pick a different name",
281                    "WARN:".yellow(),
282                    ident
283                );
284            } else {
285                break Ok(ident);
286            }
287        }
288    }
289}