Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions crates/aube/src/commands/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ pub enum FixMode {
Override,
}

pub async fn run(args: AuditArgs) -> miette::Result<()> {
pub async fn run(args: AuditArgs) -> miette::Result<Option<i32>> {
args.network.install_overrides();
let cwd = crate::dirs::project_root()?;

Expand Down Expand Up @@ -172,7 +172,7 @@ pub async fn run(args: AuditArgs) -> miette::Result<()> {
} else {
println!("No dependencies to audit.");
}
return Ok(());
return Ok(None);
}

let client = build_client(&cwd, args.network.registry.as_deref());
Expand Down Expand Up @@ -209,7 +209,10 @@ pub async fn run(args: AuditArgs) -> miette::Result<()> {
"audit degraded: advisory fetch failed, vulnerability status unknown"
);
}
std::process::exit(2);
// Degraded status exits 2. Return the code for the binary's
// single `std::process::exit` rather than terminating here,
// keeping the command embed-safe.
return Ok(Some(2));
}
return Err(miette!("advisory fetch failed: {e}"));
}
Expand Down Expand Up @@ -263,14 +266,17 @@ pub async fn run(args: AuditArgs) -> miette::Result<()> {
)
.await?;
if remaining.is_empty() {
return Ok(());
return Ok(None);
}
render_fix_remaining(&selected, &remaining);
std::process::exit(1);
// Some advisories remain unfixed: exit 1. Return the code for
// the binary's single `std::process::exit` rather than
// terminating here, keeping the command embed-safe.
return Ok(Some(1));
}
FixMode::Override => {
write_fix_overrides(&cwd, &selected, &client).await?;
return Ok(());
return Ok(None);
}
}
}
Expand All @@ -286,10 +292,12 @@ pub async fn run(args: AuditArgs) -> miette::Result<()> {
}

if rows.is_empty() {
Ok(())
Ok(None)
} else {
// pnpm-compat: exit 1 when any advisory matches the threshold.
std::process::exit(1);
// pnpm-compat: exit 1 when any advisory matches the threshold. Return
// the code for the binary's single `std::process::exit` rather than
// terminating here, keeping the command embed-safe.
Ok(Some(1))
}
}

Expand Down
9 changes: 6 additions & 3 deletions crates/aube/src/commands/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ pub struct CheckArgs {
pub json: bool,
}

pub async fn run(args: CheckArgs) -> miette::Result<()> {
pub async fn run(args: CheckArgs) -> miette::Result<Option<i32>> {
// `project_root_or_cwd` falls back to the current directory when
// nothing above it has a `package.json`. `run_report` is already a
// no-op when `node_modules/.aube/` doesn't exist, so running
Expand All @@ -65,9 +65,12 @@ pub async fn run(args: CheckArgs) -> miette::Result<()> {
}

if !report.issues.is_empty() {
std::process::exit(1);
// Exit 1 when the store check found broken links. Return the code
// for the binary's single `std::process::exit` rather than
// terminating here, keeping the command embed-safe.
return Ok(Some(1));
}
Ok(())
Ok(None)
}

/// Result of scanning the virtual store.
Expand Down
8 changes: 4 additions & 4 deletions crates/aube/src/commands/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ fn lockfile_names() -> [&'static str; 6] {
]
}

pub async fn run(args: CleanArgs) -> miette::Result<()> {
pub async fn run(args: CleanArgs) -> miette::Result<Option<i32>> {
run_as("clean", args).await
}

pub async fn run_purge(args: CleanArgs) -> miette::Result<()> {
pub async fn run_purge(args: CleanArgs) -> miette::Result<Option<i32>> {
run_as("purge", args).await
}

async fn run_as(invoked_as: &str, args: CleanArgs) -> miette::Result<()> {
async fn run_as(invoked_as: &str, args: CleanArgs) -> miette::Result<Option<i32>> {
let cwd = crate::dirs::project_root()?;
let _lock = super::take_project_lock(&cwd)?;

Expand Down Expand Up @@ -154,5 +154,5 @@ async fn run_as(invoked_as: &str, args: CleanArgs) -> miette::Result<()> {
}
}

Ok(())
Ok(None)
}
4 changes: 2 additions & 2 deletions crates/aube/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pub struct CreateArgs {
/// Scaffold a project from a `create-*` starter kit. Matches pnpm/npm
/// semantics: `aube create foo` runs the `create-foo` package via dlx,
/// `aube create @scope/foo` runs `@scope/create-foo`, etc.
pub async fn run(args: CreateArgs) -> miette::Result<()> {
pub async fn run(args: CreateArgs) -> miette::Result<Option<i32>> {
args.network.install_overrides();
let CreateArgs { params, network } = args;

Expand All @@ -47,7 +47,7 @@ pub async fn run(args: CreateArgs) -> miette::Result<()> {
.print_help()
.map_err(|e| miette!("failed to render help: {e}"))?;
println!();
return Ok(());
return Ok(None);
}

let (template, rest) = params.split_first().expect("checked non-empty above");
Expand Down
11 changes: 7 additions & 4 deletions crates/aube/src/commands/dlx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ pub struct DlxArgs {
/// an already-removed scratch dir.
/// 3. Exec `<tmp>/node_modules/.bin/<command>` from the user's original cwd.
/// 4. tempfile removes the scratch dir on drop.
pub async fn run(args: DlxArgs) -> miette::Result<()> {
pub async fn run(args: DlxArgs) -> miette::Result<Option<i32>> {
args.network.install_overrides();
args.lockfile.install_overrides();
args.virtual_store.install_overrides();
Expand All @@ -104,7 +104,7 @@ pub async fn run(args: DlxArgs) -> miette::Result<()> {
.print_help()
.map_err(|e| miette!("failed to render help: {e}"))?;
println!();
return Ok(());
return Ok(None);
}

// When only `-p` is given, dlx needs at least one arg to serve as the
Expand Down Expand Up @@ -285,9 +285,12 @@ pub async fn run(args: DlxArgs) -> miette::Result<()> {
drop(tmp);

if !status.success() {
std::process::exit(aube_scripts::exit_code_from_status(status));
// Propagate the dlx binary's non-zero exit up to the binary's single
// `std::process::exit` rather than terminating here, keeping the
// command embed-safe. The scratch project (`tmp`) is already dropped.
return Ok(Some(aube_scripts::exit_code_from_status(status)));
}
Ok(())
Ok(None)
}

fn dlx_manifest(install_specs: &[String], allow_build: &[String]) -> serde_json::Value {
Expand Down
9 changes: 6 additions & 3 deletions crates/aube/src/commands/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub struct DoctorArgs {
pub json: bool,
}

pub async fn run(args: DoctorArgs) -> miette::Result<()> {
pub async fn run(args: DoctorArgs) -> miette::Result<Option<i32>> {
let cwd = crate::dirs::cwd()?;
let project_root = crate::dirs::find_project_root(&cwd);
let anchor = project_root.clone().unwrap_or_else(|| cwd.clone());
Expand All @@ -67,9 +67,12 @@ pub async fn run(args: DoctorArgs) -> miette::Result<()> {
crate::update_check::check_and_notify(&anchor).await;

if !report.errors.is_empty() {
std::process::exit(1);
// Exit 1 when the doctor found errors. Return the code for the
// binary's single `std::process::exit` rather than terminating
// here, keeping the command embed-safe.
return Ok(Some(1));
}
Ok(())
Ok(None)
}

#[derive(Debug, Default)]
Expand Down
34 changes: 23 additions & 11 deletions crates/aube/src/commands/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ pub struct ExecArgs {
pub async fn run(
exec_args: ExecArgs,
filter: aube_workspace::selector::EffectiveFilter,
) -> miette::Result<()> {
) -> miette::Result<Option<i32>> {
exec_args.network.install_overrides();
exec_args.lockfile.install_overrides();
exec_args.virtual_store.install_overrides();
Expand Down Expand Up @@ -129,7 +129,7 @@ async fn run_filtered(
parallel: bool,
filter: &aube_workspace::selector::EffectiveFilter,
recursive: super::run::RecursiveOpts,
) -> miette::Result<()> {
) -> miette::Result<Option<i32>> {
let (_root, matched) = super::select_workspace_packages(cwd, filter, "exec")?;
let matched = super::run::order_matched_packages(matched, &recursive)?;

Expand All @@ -150,9 +150,13 @@ async fn run_filtered(

for pkg in matched {
let bin_path = super::project_modules_dir(&pkg.dir).join(".bin").join(bin);
exec_bin(&pkg.dir, &bin_path, bin, args, shell_mode).await?;
// Sequential fanout bails on the first non-zero exit, matching the
// previous behavior where the inner `exec` terminated the process.
if let Some(code) = exec_bin(&pkg.dir, &bin_path, bin, args, shell_mode).await? {
return Ok(Some(code));
}
}
Ok(())
Ok(None)
}

#[allow(clippy::too_many_arguments)]
Expand All @@ -164,7 +168,7 @@ async fn run_filtered_parallel(
concurrency: usize,
reporter_hide_prefix: bool,
reverse: bool,
) -> miette::Result<()> {
) -> miette::Result<Option<i32>> {
use std::sync::Arc;
use tokio::sync::Semaphore;

Expand Down Expand Up @@ -265,12 +269,15 @@ async fn run_filtered_parallel(
}
}
if let Some(code) = first_exit {
std::process::exit(code);
// Propagate the first non-zero child exit up to the binary's single
// `std::process::exit` rather than terminating here, so an embedder
// driving aube in-process isn't hard-killed by a failing task.
return Ok(Some(code));
}
if let Some(e) = first_err {
return Err(e);
}
Ok(())
Ok(None)
}

pub(crate) async fn exec_bin(
Expand All @@ -279,18 +286,23 @@ pub(crate) async fn exec_bin(
bin: &str,
args: &[String],
shell_mode: bool,
) -> miette::Result<()> {
) -> miette::Result<Option<i32>> {
exec_bin_with_node_args(cwd, bin_path, bin, args, &[], shell_mode).await
}

/// Run a project-local binary. On a non-zero child exit, returns
/// `Ok(Some(code))` so the caller can propagate the code up to the binary's
/// single `std::process::exit` instead of terminating in place — keeping the
/// command layer embed-safe for a host driving aube as a library.
/// `Ok(None)` means the binary succeeded.
pub(crate) async fn exec_bin_with_node_args(
cwd: &Path,
bin_path: &Path,
bin: &str,
args: &[String],
node_args: &[String],
shell_mode: bool,
) -> miette::Result<()> {
) -> miette::Result<Option<i32>> {
if !shell_mode && !bin_path.exists() {
return Err(miette!(
"binary not found: {bin}\nTry running `{}` first, or check that the package providing '{bin}' is in your dependencies.",
Expand Down Expand Up @@ -327,10 +339,10 @@ pub(crate) async fn exec_bin_with_node_args(
.wrap_err("failed to execute binary")?;

if !status.success() {
std::process::exit(aube_scripts::exit_code_from_status(status));
return Ok(Some(aube_scripts::exit_code_from_status(status)));
}

Ok(())
Ok(None)
}

pub(crate) async fn exec_bin_status(
Expand Down
2 changes: 1 addition & 1 deletion crates/aube/src/commands/install_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use miette::miette;
/// alone is equivalent. We emit a one-line hint pointing users at the shorter
/// form and still honor the explicit install so the command behaves the way
/// a pnpm muscle-memory user expects.
pub async fn run(script_args: ScriptArgs) -> miette::Result<()> {
pub async fn run(script_args: ScriptArgs) -> miette::Result<Option<i32>> {
script_args.network.install_overrides();
script_args.lockfile.install_overrides();
script_args.virtual_store.install_overrides();
Expand Down
36 changes: 21 additions & 15 deletions crates/aube/src/commands/outdated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ fn serialize_dep_type<S: serde::Serializer>(dt: &DepType, s: S) -> Result<S::Ok,
pub async fn run(
args: OutdatedArgs,
mut filter: aube_workspace::selector::EffectiveFilter,
) -> miette::Result<()> {
) -> miette::Result<Option<i32>> {
args.network.install_overrides();
let mut cwd = crate::dirs::project_root()?;
if !filter.is_empty() {
Expand All @@ -144,15 +144,22 @@ pub async fn run(
{
cwd = root;
}
run_one(&cwd, args, None).await?;
Ok(())
// Match pnpm: exit 1 when any dependency is outdated so CI patterns
// like `aube outdated || exit 1` behave the same. The code is
// returned for the binary's single `std::process::exit` rather than
// exited in place, keeping the command embed-safe.
if run_one(&cwd, args, None).await? {
Ok(Some(1))
} else {
Ok(None)
}
}

async fn run_filtered(
cwd: &Path,
args: OutdatedArgs,
filter: &aube_workspace::selector::EffectiveFilter,
) -> miette::Result<()> {
) -> miette::Result<Option<i32>> {
let (root, matched) = super::select_workspace_packages(cwd, filter, "outdated")?;
let manifest = super::load_manifest(&root.join("package.json"))?;
let graph = match aube_lockfile::parse_lockfile(&root, &manifest) {
Expand All @@ -162,7 +169,7 @@ async fn run_filtered(
"No lockfile found. Run `{}` first.",
aube_util::cmd("install")
);
return Ok(());
return Ok(None);
}
Err(e) => return Err(miette::Report::new(e)).wrap_err("failed to parse lockfile"),
};
Expand Down Expand Up @@ -200,9 +207,11 @@ async fn run_filtered(
}
}
if any_drift {
std::process::exit(1);
// Return the code for the binary's single `std::process::exit`
// rather than exiting in place, keeping the command embed-safe.
return Ok(Some(1));
}
Ok(())
Ok(None)
}

async fn run_one(cwd: &Path, args: OutdatedArgs, importer: Option<String>) -> miette::Result<bool> {
Expand Down Expand Up @@ -352,14 +361,11 @@ async fn run_graph(
render_table(&rows, args.long);
}

// Match pnpm: exit 1 when any dependency is outdated so CI patterns like
// `aube outdated || exit 1` and bare `aube outdated && echo ok` behave
// the same as with pnpm. `std::process::exit` is fine here because the
// command has no resources to clean up beyond what the OS handles.
if has_drift && importer.is_none() {
std::process::exit(1);
}

// Return the drift flag to the caller. The single-project caller (`run`)
// maps `true` to exit code 1 (pnpm parity: `aube outdated || exit 1`),
// and the recursive caller (`run_filtered`) aggregates drift across
// importers — the exit decision lives at the top so the command layer
// stays embed-safe (no in-place `std::process::exit`).
Ok(has_drift)
}

Expand Down
Loading
Loading