aube 2.2.0

Aube — a fast Node.js package manager
Documentation
//! Stubs for npm-only commands that pnpm claims at the CLI surface.
//!
//! pnpm doesn't implement `whoami`, `token`, `owner`, `search`, `pkg`, or
//! `set-script` — it parses the command name, prints a "not implemented,
//! use npm" error, and exits non-zero. We match that behavior so these
//! names don't fall through aube's implicit-script runner (`External`),
//! where `aube whoami` would otherwise try to run a `whoami` script from
//! `package.json` and produce a confusing "script not found" error.

use miette::{Context, IntoDiagnostic, miette};

#[derive(Debug, usage_rs::Args)]
pub struct FallbackArgs {
    /// Unused; captured so `aube <cmd> foo bar` parses instead of
    /// erroring on unexpected args before the fallback message prints.
    #[usage(arg, double_dash = "automatic", hide)]
    pub args: Vec<String>,
    #[usage(flatten)]
    pub network: crate::cli_args::NetworkArgs,
}

macro_rules! fallback_alias_args {
    ($($name:ident),+ $(,)?) => {
        $(
            #[derive(Debug, usage_rs::Args)]
            pub struct $name {
                #[usage(flatten)]
                pub inner: FallbackArgs,
            }

            impl std::ops::Deref for $name {
                type Target = FallbackArgs;

                fn deref(&self) -> &Self::Target {
                    &self.inner
                }
            }
        )+
    };
}

fallback_alias_args!(
    OwnerArgs,
    PkgArgs,
    SearchArgs,
    SetScriptArgs,
    StageArgs,
    TokenArgs,
    WhoamiArgs,
);

pub fn run(name: &str, args: &FallbackArgs) -> miette::Result<i32> {
    args.network.install_overrides();
    let cwd = crate::dirs::cwd()?;
    let files = crate::commands::FileSources::load(&cwd);
    let empty_ws = std::collections::BTreeMap::new();
    let env = aube_settings::values::process_env();
    let ctx = files.ctx(&empty_ws, env, &[]);

    if let Some(npm_path) = aube_settings::resolved::npm_path(&ctx) {
        let mut cmd = std::process::Command::new(&npm_path);
        cmd.arg(name)
            .args(&args.args)
            .stderr(aube_scripts::child_stderr());
        if let Some(registry) = args.network.registry.as_deref() {
            cmd.arg(format!("--registry={registry}"));
        }
        let status = cmd
            .status()
            .into_diagnostic()
            .wrap_err_with(|| format!("failed to run configured npmPath `{npm_path}`"))?;
        return Ok(child_exit_code(status));
    }

    Err(miette!(
        code = aube_codes::errors::ERR_AUBE_NPM_ONLY_COMMAND,
        "`aube {name}` is not implemented. This is an npm-only command — \
         run it with `npm {name}` instead, or set `npmPath` to let aube delegate it."
    ))
}

fn child_exit_code(status: std::process::ExitStatus) -> i32 {
    if let Some(code) = status.code() {
        return code;
    }

    #[cfg(unix)]
    {
        use std::os::unix::process::ExitStatusExt;

        if let Some(signal) = status.signal() {
            return 128 + signal;
        }
    }

    1
}

#[cfg(test)]
mod tests {
    #[cfg(unix)]
    use super::child_exit_code;

    #[cfg(unix)]
    #[test]
    fn child_signal_exit_uses_shell_convention() {
        use std::os::unix::process::ExitStatusExt;

        let status = std::process::ExitStatus::from_raw(2);

        assert_eq!(child_exit_code(status), 130);
    }

    #[cfg(unix)]
    #[test]
    fn child_normal_exit_code_is_preserved() {
        use std::os::unix::process::ExitStatusExt;

        let status = std::process::ExitStatus::from_raw(7 << 8);

        assert_eq!(child_exit_code(status), 7);
    }
}