Skip to main content

wapm_cli/commands/
execute.rs

1//! Module for wax, executes a module immediately
2
3//use crate::constants::RFC3339_FORMAT_STRING_WITH_TIMEZONE;
4use crate::config;
5use crate::data::wax_index;
6use crate::dataflow::find_command_result::FindCommandResult;
7use crate::dataflow::installed_packages::{InstalledPackages, RegistryInstaller};
8use crate::dataflow::lockfile_packages::{LockfilePackages, LockfileResult};
9use crate::dataflow::merged_lockfile_packages::MergedLockfilePackages;
10use crate::dataflow::resolved_packages::ResolvedPackages;
11use crate::dataflow::retained_lockfile_packages::RetainedLockfilePackages;
12use crate::dataflow::WapmPackageKey;
13use crate::graphql::{execute_query, DateTime};
14//use crate::keys;
15use crate::util;
16use anyhow::Context;
17use thiserror::Error;
18
19use graphql_client::*;
20
21use std::convert::From;
22use std::ffi::OsString;
23use std::path::PathBuf;
24use std::str::FromStr;
25use structopt::{clap::AppSettings, StructOpt};
26
27#[derive(StructOpt, Debug)]
28pub enum ExecuteOpt {
29    /// The inner execute command
30    #[structopt(external_subcommand)]
31    ExecArgs(Vec<String>),
32}
33
34impl ExecuteOpt {
35    fn args(&self) -> &[String] {
36        match self {
37            ExecuteOpt::ExecArgs(args) => args.as_slice(),
38        }
39    }
40}
41
42// NOTE: we only derive structopt for the help text, we don't actually
43// use it to parse anything!!
44#[derive(StructOpt, Debug, Clone, Default)]
45#[structopt(settings = &[AppSettings::ColoredHelp])]
46struct ExecuteOptInner {
47    /// Run unsandboxed emscripten modules too.
48    #[structopt(long = "emscripten")]
49    enable_emscripten: bool,
50
51    /// Agree to all prompts. Useful for non-interactive uses. (WARNING: this may cause undesired behavior).
52    #[structopt(long = "force-yes", short = "y")]
53    force_yes: bool,
54
55    /// Package will be verified by its signature. Off by default
56    #[structopt(long = "verify", short = "v")]
57    verify_signature: bool,
58
59    /// Run `wax` in offline mode.
60    #[structopt(long = "offline")]
61    offline: bool,
62
63    /// Pre-open a directory for WASI.
64    #[structopt(
65        long = "dir",
66        multiple = true,
67        group = "wasi",
68        value_name = "DIRECTORY"
69    )]
70    pre_opened_directories: Vec<String>,
71
72    /// Prevent the current directory from being preopened by default.
73    #[structopt(long = "no-default-preopen")]
74    no_default_preopen: bool,
75
76    /// The command to run.
77    #[structopt(long = "which", value_name = "COMMAND")]
78    which: Option<String>,
79
80    /// The command to run.
81    #[structopt(conflicts_with_all(&["which"]), index = 1, value_name = "COMMAND")]
82    command: Option<String>,
83
84    /// Arguments that the command will get.
85    #[structopt(multiple = true, parse(from_os_str), last(true), value_name = "ARGS")]
86    args: Vec<OsString>,
87}
88
89impl ExecuteOptInner {
90    fn print_help_text() {
91        // simple hack to force a help message and have StructOpt
92        // manage our help text without doing our parsing
93        ExecuteOptInner::clap()
94            .bin_name("wax")
95            .usage("wax [FLAGS] [OPTIONS] <COMMAND> [<ARGS>...]")
96            .print_help()
97            .expect("could not print help text");
98        // pad with extra new line
99        println!();
100    }
101}
102
103#[derive(Debug, Error)]
104enum ExecuteError {
105    #[error(
106        "Command `{0}` not found in the registry {registry:?} or in the current directory",
107        name
108    )]
109    CommandNotFound { name: String, registry: String },
110    #[error(
111        "The command `{0}` is using the Emscripten ABI which may be implmented in a way that is partially unsandbooxed. To opt-in to executing Emscripten Wasm modules run the command again with the `--emscripten` flag",
112        name
113    )]
114    EmscriptenDisabled { name: String },
115    #[error("Error with the Wax index: {0}")]
116    WaxIndexError(wax_index::WaxIndexError),
117    #[error("Error parsing data from register: {0}")]
118    ErrorInDataFromRegistry(String),
119    #[error("An error occured during installation: {0}")]
120    InstallationError(String),
121    #[error("Please specify a command to run.")]
122    NoCommandGiven,
123    #[error(
124        "The command `{0}` was not found.\nIf you are offline you will need to reconnect to the Internet for this command to succeed.\nOtherwise there may be an error with the wapm registry that you're connected to: run `wapm config get registry.url` to print the URL that we tried to connect to.",
125    )]
126    CommandNotFoundOfflineMode(String),
127    #[error(
128        "The command `{0}` was not found.\nPlease try to run this command again without the `--offline` flag.",
129    )]
130    CommandNotFoundOfflineModeOfflineFlag(String),
131}
132
133#[derive(Debug, Error)]
134enum ExecuteArgParsingError {
135    #[error("Argument `{arg_name}` expects a value `{expected}` but none was found.")]
136    MissingValue { arg_name: String, expected: String },
137    #[error("Unrecognized argument `{arg_name}`")]
138    UnrecognizedArgument { arg_name: String },
139}
140
141#[derive(GraphQLQuery)]
142#[graphql(
143    schema_path = "graphql/schema.graphql",
144    query_path = "graphql/queries/wax_get_command.graphql",
145    response_derives = "Debug"
146)]
147struct WaxGetCommandQuery;
148
149#[derive(GraphQLQuery)]
150#[graphql(
151    schema_path = "graphql/schema.graphql",
152    query_path = "graphql/queries/wax_get_package_command.graphql",
153    response_derives = "Debug"
154)]
155struct WaxGetPackageCommandQuery;
156
157use crate::commands::add::get_package_version_query;
158
159use super::add::GetPackageVersionQuery;
160
161#[derive(Debug)]
162struct QueriedPackage {
163    package: String,
164    command_to_exec: String,
165    version: semver::Version,
166    download_url: String,
167}
168
169impl QueriedPackage {
170    pub fn query(opt: &ExecuteOptInner, package_name: &str) -> Option<Self> {
171        if opt.offline {
172            return None;
173        }
174
175        let split = package_name.split('@').collect::<Vec<_>>();
176        let (mut package_name, mut version) = match split.as_slice() {
177            [n, v] => (n.to_string(), Some(v.to_string())),
178            [n] => (n.to_string(), None),
179            _ => {
180                return None;
181            }
182        };
183
184        let command_name;
185        if let Some(v) = version.clone() {
186            let split2 = v.split(':').collect::<Vec<_>>();
187            let (v, c) = match split2.as_slice() {
188                [n, v] => (n.to_string(), Some(v.to_string())),
189                [n] => (n.to_string(), None),
190                _ => {
191                    return None;
192                }
193            };
194            version = Some(v);
195            command_name = c;
196        } else {
197            let split2 = package_name.split(':').collect::<Vec<_>>();
198            let (v, c) = match split2.as_slice() {
199                [n, v] => (n.to_string(), Some(v.to_string())),
200                [n] => (n.to_string(), None),
201                _ => {
202                    return None;
203                }
204            };
205            package_name = v;
206            command_name = c;
207        }
208
209        match version {
210            None => {
211                let q2 = WaxGetPackageCommandQuery::build_query(
212                    wax_get_package_command_query::Variables { name: package_name },
213                );
214
215                let response: wax_get_package_command_query::ResponseData =
216                    execute_query(&q2).ok()?;
217                let download_url = response
218                    .package
219                    .as_ref()?
220                    .last_version
221                    .as_ref()?
222                    .distribution
223                    .download_url
224                    .clone();
225                let version = semver::Version::from_str(
226                    &response.package.as_ref()?.last_version.as_ref()?.version,
227                )
228                .map_err(|e| ExecuteError::ErrorInDataFromRegistry(e.to_string()))
229                .ok()?;
230                let manifest = wapm_toml::Manifest::parse(
231                    &response.package.as_ref()?.last_version.as_ref()?.manifest,
232                )
233                .ok()?;
234                let command_to_exec = match command_name {
235                    Some(s) => s,
236                    None => manifest
237                        .command
238                        .unwrap_or_default()
239                        .iter()
240                        .map(|v| v.get_name())
241                        .next()?,
242                };
243                let name = response.package.as_ref()?.name.clone();
244                Some(QueriedPackage {
245                    package: name,
246                    command_to_exec,
247                    version,
248                    download_url,
249                })
250            }
251            Some(v) => {
252                let q3 =
253                    GetPackageVersionQuery::build_query(get_package_version_query::Variables {
254                        name: package_name,
255                        version: Some(v),
256                    });
257                let response: get_package_version_query::ResponseData = execute_query(&q3).ok()?;
258                let download_url = response
259                    .package_version
260                    .as_ref()?
261                    .distribution
262                    .download_url
263                    .clone();
264                let version =
265                    semver::Version::from_str(&response.package_version.as_ref()?.version)
266                        .map_err(|e| ExecuteError::ErrorInDataFromRegistry(e.to_string()))
267                        .ok()?;
268                let manifest =
269                    wapm_toml::Manifest::parse(&response.package_version.as_ref()?.manifest)
270                        .ok()?;
271                let command_to_exec = match command_name {
272                    Some(s) => s,
273                    None => manifest
274                        .command
275                        .unwrap_or_default()
276                        .iter()
277                        .map(|v| v.get_name())
278                        .next()?,
279                };
280                let name = response.package_version.as_ref()?.package.name.clone();
281                Some(QueriedPackage {
282                    package: name,
283                    command_to_exec,
284                    version,
285                    download_url,
286                })
287            }
288        }
289    }
290}
291
292/// Do the real argument parsing into [`ExecuteOptInner`].
293fn transform_args(arg_stream: &[String]) -> Result<ExecuteOptInner, ExecuteArgParsingError> {
294    let mut idx = 0;
295    let mut out = ExecuteOptInner::default();
296    let parse_which = |which_arg: Option<String>| -> Result<String, ExecuteArgParsingError> {
297        let val: String = which_arg.ok_or_else(|| ExecuteArgParsingError::MissingValue {
298            arg_name: "--which".to_string(),
299            expected: "<COMMAND NAME>".to_string(),
300        })?;
301        Ok(val)
302    };
303    let parse_dir = |dir_arg: Option<String>| -> Result<String, ExecuteArgParsingError> {
304        let val: String = dir_arg.ok_or_else(|| ExecuteArgParsingError::MissingValue {
305            arg_name: "--dir".to_string(),
306            expected: "<DIRECTORY>".to_string(),
307        })?;
308        Ok(val)
309    };
310    while idx < arg_stream.len() {
311        match arg_stream[idx].as_ref() {
312            "--emscripten" => out.enable_emscripten = true,
313            "--force_yes" | "-y" => out.force_yes = true,
314            "--verify" | "-v" => out.verify_signature = true,
315            "--no-default-preopen" => out.no_default_preopen = true,
316            "--offline" => out.offline = true,
317            "--which" => {
318                out.which = Some(parse_which(arg_stream.get(idx + 1).cloned())?);
319                idx += 1;
320            }
321            "--dir" => {
322                out.pre_opened_directories
323                    .push(parse_dir(arg_stream.get(idx + 1).cloned())?);
324                idx += 1;
325            }
326            "help" | "--help" | "-h" => {
327                ExecuteOptInner::print_help_text();
328                std::process::exit(0);
329            }
330            misc => {
331                if misc.contains('=') {
332                    // if it has a `=`, then it's an argument
333                    let mut splitter = misc.split('=');
334                    let arg = splitter.next().unwrap();
335                    let val = splitter.next().map(|x| x.to_string());
336                    match arg {
337                        "--which" => out.which = Some(parse_which(val)?),
338                        "--dir" => {
339                            out.pre_opened_directories.push(parse_dir(val)?);
340                        }
341                        otherwise => {
342                            return Err(ExecuteArgParsingError::UnrecognizedArgument {
343                                arg_name: otherwise.to_string(),
344                            })
345                        }
346                    }
347                } else {
348                    //otherwise, this is a command
349                    out.command = Some(misc.to_string());
350                    if let Some(after_command) = arg_stream.get(idx + 1) {
351                        // eat `--` after command if it exists
352                        if after_command == "--" {
353                            idx += 1;
354                        }
355                    }
356                    out.args = arg_stream[idx + 1..].iter().map(Into::into).collect();
357                    break;
358                }
359            }
360        }
361        idx += 1;
362    }
363
364    Ok(out)
365}
366
367pub fn execute(opt: ExecuteOpt) -> anyhow::Result<()> {
368    let mut opt = transform_args(opt.args())?;
369    if !opt.no_default_preopen {
370        opt.pre_opened_directories.push(".".into());
371    }
372    let opt = opt;
373    trace!("Execute {:?}", &opt);
374    let current_dir = crate::config::Config::get_current_dir()?;
375    let config = crate::config::Config::from_file()?;
376    if let Some(which) = opt.which {
377        let mut wax_index = wax_index::WaxIndex::open()?;
378        let dir = if let Ok((package_name, version, _)) = wax_index.search_for_entry(which.clone())
379        {
380            wax_index
381                .base_path()
382                .join(format!("{}@{}", package_name, version))
383        } else if let FindCommandResult::CommandFound { manifest_dir, .. } =
384            FindCommandResult::find_command_in_directory(&current_dir, &which)
385        {
386            manifest_dir
387        } else {
388            return Err(ExecuteError::CommandNotFound {
389                name: which,
390                registry: config.registry.get_current_registry(),
391            }
392            .into());
393        };
394
395        println!("{}", dir.to_string_lossy());
396        return Ok(());
397    }
398    let command = if let Some(command) = &opt.command {
399        command.clone()
400    } else {
401        ExecuteOptInner::print_help_text();
402        return Err(ExecuteError::NoCommandGiven.into());
403    };
404    let command_name = command.as_str();
405    let _value = util::set_wapm_should_accept_all_prompts(opt.force_yes);
406    debug_assert!(
407        _value.is_some(),
408        "this function should only be called once!"
409    );
410
411    // first search for locally installed command
412    match FindCommandResult::find_command_in_directory(&current_dir, command_name) {
413        FindCommandResult::CommandNotFound { .. } => {
414            // go to normal wax flow
415            debug!(
416                "Wax: Command \"{}\" not found locally in directory {}",
417                &command_name,
418                current_dir.to_string_lossy()
419            );
420        }
421        FindCommandResult::CommandFound {
422            source,
423            manifest_dir,
424            args: _,
425            module_name,
426            prehashed_cache_key,
427        } => {
428            debug!(
429                "Wax command found locally in {}",
430                current_dir.to_string_lossy()
431            );
432            // run it and return
433            crate::commands::run::do_run(
434                current_dir,
435                source,
436                manifest_dir,
437                command_name,
438                &module_name,
439                &opt.pre_opened_directories,
440                &opt.args,
441                prehashed_cache_key,
442            )?;
443            return Ok(());
444        }
445        FindCommandResult::Error(e) => {
446            warn!(
447                "Error in Wax when looking for command locally: {}... Continuing execution",
448                e
449            );
450        }
451    }
452
453    // get wax info from the index and find out if the entry is stale
454    let mut wax_index = wax_index::WaxIndex::open()?;
455    let (wax_info, exists_and_recently_updated): (Option<(String, semver::Version)>, bool) =
456        if let Ok((package_name, version, last_seen)) =
457            wax_index.search_for_entry(command_name.to_string())
458        {
459            let wax_cooldown = get_wax_cooldown();
460            trace!("Using wax cooldown: {}", wax_cooldown);
461            let now = time::now_utc().to_timespec();
462            let cooldown_duration = time::Duration::seconds(wax_cooldown as i64);
463            let time_to_update = last_seen + cooldown_duration;
464
465            // if we haven't yet hit the time to update, then we've recently updated
466            let recently_updated = now < time_to_update;
467            debug!("The package was recently updated? {}", recently_updated);
468
469            (Some((package_name, version)), recently_updated)
470        } else {
471            (None, false)
472        };
473
474    if exists_and_recently_updated {
475        let (package_name, version) = wax_info
476            .clone()
477            .expect("critical internal logic error in `wapm execute`");
478        let package_version_str = format!("{}@{}", &package_name, &version);
479        let location = wax_index.base_path().join(&package_version_str);
480        if !location
481            .join("wapm_packages")
482            .join(&package_version_str)
483            .join("wapm.toml")
484            .exists()
485        {
486            debug!("Package found in index, but cache has been cleared! continuing to logic that should reinstall");
487        } else {
488            // do execute unless it fails then continue
489            wax_index.save()?;
490
491            match run(
492                command_name,
493                location,
494                &opt.pre_opened_directories,
495                &opt.args,
496            ) {
497                Ok(()) => return Ok(()),
498                Err(e) => {
499                    // REVIEW: does this even make sense?
500                    // maybe if we force a reinstall but eh
501                    debug!("Failed to run when cached due to `{}`. continuing...", e);
502                }
503            }
504        }
505    }
506
507    // if not found, query the server and check if we already have it installed
508    let q = WaxGetCommandQuery::build_query(wax_get_command_query::Variables {
509        command: command_name.to_string(),
510    });
511    let response: wax_get_command_query::ResponseData = if !opt.offline {
512        debug!("Querying server for package info");
513        let response: Result<wax_get_command_query::ResponseData, _> = execute_query(&q);
514        if response.is_err() {
515            info!("Failed to connect to the wapm registry. Continuning in offline mode.");
516            return do_offline_run(command_name, &opt);
517        }
518        response?
519    } else {
520        return do_offline_run(command_name, &opt);
521    };
522
523    trace!("Wax get command query: {:?}", response);
524
525    let mut run_command_name = command_name.to_string();
526    let reg_ver;
527    let package_name;
528    let mut command_is_package = false;
529
530    let (mut install_loc, resolved_packages) = if let Some(command) = response.command {
531        // command found, check if it's installed
532        if let Some(abi) = command.module.abi.as_ref() {
533            if abi == "emscripten" && !opt.enable_emscripten {
534                return Err(ExecuteError::EmscriptenDisabled {
535                    name: command_name.to_string(),
536                }
537                .into());
538            }
539        }
540        let registry_version = semver::Version::from_str(&command.package_version.version)
541            .map_err(|e| ExecuteError::ErrorInDataFromRegistry(e.to_string()))?;
542
543        if let Some((package_name, version)) = wax_info {
544            let package_version_str = format!("{}@{}", &package_name, &version);
545            let location = wax_index.base_path().join(&package_version_str);
546            if registry_version > version
547                || !location
548                    .join("wapm_packages")
549                    .join(&package_version_str)
550                    .join("wapm.toml")
551                    .exists()
552            {
553                debug!(
554                    "Found version {} locally in Wax but version {} from the registry: upgrading",
555                    version, registry_version
556                );
557            } else {
558                debug!(
559                    "Command found in Wax index, executing version {} directly",
560                    version
561                );
562                wax_index.insert_entry(
563                    command_name.to_string(),
564                    registry_version,
565                    command.package_version.package.name,
566                );
567                wax_index.save()?;
568
569                run(
570                    command_name,
571                    location,
572                    &opt.pre_opened_directories,
573                    &opt.args,
574                )?;
575                return Ok(());
576            }
577        } else {
578            debug!("Entry not found in wax index");
579        }
580
581        // ===================
582        // Perform the install
583        // if we made it this far, it means we haven't executed the command yet,
584        // so we install the package and run it
585        trace!("Installing Wax package from registry");
586        let install_loc = wax_index.base_path().join(format!(
587            "{}@{}",
588            &command.package_version.package.name, &registry_version
589        ));
590
591        reg_ver = Some(registry_version.clone());
592        package_name = Some(command.package_version.package.name.clone());
593
594        (
595            install_loc,
596            ResolvedPackages {
597                packages: vec![(
598                    WapmPackageKey {
599                        name: command.package_version.package.name.into(),
600                        version: registry_version,
601                    },
602                    (
603                        command.package_version.distribution.download_url,
604                        None, /*
605                                  // package signing disabled for `wapm execute` for now
606                                  command
607                                  .package_version
608                                  .signature
609                                  .map(|sig| keys::WapmPackageSignature {
610                                  public_key_id: sig.public_key.key_id.clone(),
611                                  public_key: sig.public_key.key.clone(),
612                                  signature_data: sig.data.clone(),
613                                  date_created: time::strptime(
614                                  &sig.public_key.uploaded_at,
615                                  RFC3339_FORMAT_STRING_WITH_TIMEZONE,
616                              )
617                                  .unwrap_or_else(|err| {
618                                  panic!("Failed to parse time string: {}", err)
619                              })
620                                  .to_timespec(),
621                                  revoked: sig.public_key.revoked,
622                                  owner: sig.public_key.owner.username.clone(),
623                              })*/
624                    ),
625                )],
626            },
627        )
628    } else if let Some(package) = QueriedPackage::query(&opt, command_name).as_ref() {
629        command_is_package = true;
630        run_command_name = package.command_to_exec.clone();
631        let install_loc = wax_index
632            .base_path()
633            .join(format!("{}@{}", &package.package, &package.version));
634
635        reg_ver = Some(package.version.clone());
636        package_name = Some(package.package.clone());
637
638        (
639            install_loc,
640            ResolvedPackages {
641                packages: vec![(
642                    WapmPackageKey {
643                        name: package.package.clone().into(),
644                        version: package.version.clone(),
645                    },
646                    (package.download_url.clone(), None),
647                )],
648            },
649        )
650    } else {
651        return Err(ExecuteError::CommandNotFound {
652            name: command_name.to_string(),
653            registry: config.registry.get_current_registry(),
654        }
655        .into());
656    };
657
658    let registry_version = reg_ver.context("no registry version")?;
659    let package_name = package_name.context("no package name")?;
660
661    // perform the install and generate the lockfile (like a simpler version of dataflow::update updating without a manifest)
662    let lockfile_result = LockfileResult::find_in_directory(&install_loc);
663    let lockfile_packages = LockfilePackages::new_from_result(lockfile_result)
664        .map_err(|e| ExecuteError::InstallationError(e.to_string()))?;
665    let installed_packages = InstalledPackages::install::<RegistryInstaller>(
666        &install_loc,
667        resolved_packages,
668        !opt.verify_signature,
669    )?;
670    let added_lockfile_data = LockfilePackages::from_installed_packages(&installed_packages)
671        .map_err(|e| ExecuteError::InstallationError(e.to_string()))?;
672
673    let retained_lockfile_packages =
674        RetainedLockfilePackages::from_lockfile_packages(lockfile_packages);
675    let final_lockfile_data =
676        MergedLockfilePackages::merge(added_lockfile_data, retained_lockfile_packages);
677    final_lockfile_data
678        .generate_lockfile(&install_loc)
679        .map_err(|e| ExecuteError::InstallationError(e.to_string()))?;
680
681    debug!("Wax package installed to {}", install_loc.to_string_lossy());
682
683    wax_index.insert_entry(
684        run_command_name.to_string(),
685        registry_version.clone(),
686        package_name.clone(),
687    );
688    wax_index.save()?;
689
690    if command_is_package {
691        let install_loc_original = install_loc.clone();
692        let mut name = package_name.split('/');
693        install_loc = install_loc.join("wapm_packages");
694        if let Some(s) = name.next() {
695            install_loc = install_loc.join(s)
696        }
697        if let Some(s) = name.next() {
698            install_loc = install_loc.join(format!("{s}@{registry_version}"));
699        }
700
701        let _ = std::fs::copy(
702            install_loc_original.join("wapm.lock"),
703            install_loc.clone().join("wapm.lock"),
704        );
705    }
706
707    run(
708        &run_command_name,
709        install_loc,
710        &opt.pre_opened_directories,
711        &opt.args,
712    )?;
713
714    Ok(())
715}
716
717fn run(
718    command_name: &str,
719    location: PathBuf,
720    pre_opened_directories: &[String],
721    args: &[OsString],
722) -> anyhow::Result<()> {
723    match FindCommandResult::find_command_in_directory(&location, command_name) {
724        FindCommandResult::CommandNotFound { error, extended: _ } => {
725            // this should only happen if the package is deleted immediately
726            // after being installed to the temp directory or the package is
727            // corrupt
728            todo!(
729                "Implement command not found logic!: {}, {}: {}",
730                location.to_string_lossy(),
731                &command_name,
732                error,
733            );
734        }
735        FindCommandResult::CommandFound {
736            source,
737            manifest_dir,
738            args: _,
739            module_name,
740            prehashed_cache_key,
741        } => {
742            crate::logging::clear_stdout()?;
743            crate::commands::run::do_run(
744                location,
745                source,
746                manifest_dir,
747                command_name,
748                &module_name,
749                pre_opened_directories,
750                args,
751                prehashed_cache_key,
752            )
753        }
754        FindCommandResult::Error(e) => Err(e),
755    }
756}
757
758fn do_offline_run(command_name: &str, opt: &ExecuteOptInner) -> anyhow::Result<()> {
759    let mut wax_index = wax_index::WaxIndex::open()?;
760    if let Ok((package_name, version, _)) = wax_index.search_for_entry(command_name.to_string()) {
761        let package_version_str = format!("{}@{}", &package_name, &version);
762        let location = wax_index.base_path().join(package_version_str);
763
764        wax_index.save()?;
765
766        crate::logging::clear_stdout()?;
767        run(
768            command_name,
769            location,
770            &opt.pre_opened_directories,
771            &opt.args,
772        )
773    } else if opt.offline {
774        Err(ExecuteError::CommandNotFoundOfflineModeOfflineFlag(command_name.to_string()).into())
775    } else {
776        Err(ExecuteError::CommandNotFoundOfflineMode(command_name.to_string()).into())
777    }
778}
779
780impl From<wax_index::WaxIndexError> for ExecuteError {
781    fn from(other: wax_index::WaxIndexError) -> Self {
782        ExecuteError::WaxIndexError(other)
783    }
784}
785
786fn get_wax_cooldown() -> i32 {
787    config::Config::from_file()
788        .ok()
789        .map(|c| c.wax_cooldown)
790        .unwrap_or(config::wax_default_cooldown())
791}