diff --git a/crates/vite_task/src/lib.rs b/crates/vite_task/src/lib.rs index c1659756f..8a8f03867 100644 --- a/crates/vite_task/src/lib.rs +++ b/crates/vite_task/src/lib.rs @@ -4,7 +4,9 @@ pub mod session; // Public exports for vite_task_bin pub use cli::{CacheSubcommand, Command, RunCommand, RunFlags}; -pub use session::{CommandHandler, ExitStatus, HandledCommand, Session, SessionConfig}; +pub use session::{ + CommandHandler, ExitStatus, HandledCommand, Session, SessionConfig, print_error, +}; pub use vite_task_graph::{ config::{ self, diff --git a/crates/vite_task/src/session/mod.rs b/crates/vite_task/src/session/mod.rs index ed1fd5cb8..d1d68b1f0 100644 --- a/crates/vite_task/src/session/mod.rs +++ b/crates/vite_task/src/session/mod.rs @@ -244,15 +244,19 @@ impl<'a> Session<'a> { /// Primary entry point for CLI usage. Plans and executes the given command. /// - /// # Errors - /// - /// Returns an error if planning or execution fails. + /// Any error encountered during planning or execution is printed to stderr + /// with a bold red `error:` prefix, with each level of the error chain on + /// its own `* `-prefixed line. Returns the exit status — callers exit the + /// process with it. #[tracing::instrument(level = "debug", skip_all)] - pub async fn main(mut self, command: Command) -> anyhow::Result { + pub async fn main(mut self, command: Command) -> ExitStatus { match self.main_inner(command).await { - Ok(()) => Ok(ExitStatus::SUCCESS), - Err(SessionError::EarlyExit(status)) => Ok(status), - Err(SessionError::Anyhow(err)) => Err(err), + Ok(()) => ExitStatus::SUCCESS, + Err(SessionError::EarlyExit(status)) => status, + Err(SessionError::Anyhow(err)) => { + print_error(&err); + ExitStatus::FAILURE + } } } @@ -795,6 +799,29 @@ impl<'a> Session<'a> { } } +/// Print `error` to stderr formatted as the `vp` CLI does: +/// +/// ```text +/// error: +/// * +/// * +/// ``` +/// +/// The `error:` prefix is bold red when stderr supports ANSI colors. +pub fn print_error(error: &anyhow::Error) { + use std::io::Write as _; + + use owo_colors::{OwoColorize as _, Stream, Style}; + + let prefix = "error:".if_supports_color(Stream::Stderr, |s| s.style(Style::new().red().bold())); + let mut stderr = std::io::stderr().lock(); + let _ = write!(stderr, "{prefix} {error}"); + for source in error.chain().skip(1) { + let _ = write!(stderr, "\n* {source}"); + } + let _ = writeln!(stderr); +} + /// Whether stdout supports ANSI color output for the current process. Honors /// `NO_COLOR`/`FORCE_COLOR` and detects TTY capability via the `supports-color` /// crate. Result is cached for the process lifetime. diff --git a/crates/vite_task_bin/src/main.rs b/crates/vite_task_bin/src/main.rs index d627e2c32..e1599059d 100644 --- a/crates/vite_task_bin/src/main.rs +++ b/crates/vite_task_bin/src/main.rs @@ -3,24 +3,21 @@ use vite_task::{Command, ExitStatus, Session}; use vite_task_bin::OwnedSessionConfig; fn main() -> ! { - let exit_code: i32 = - tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on(async { - match run().await { - Ok(status) => i32::from(status.0), - #[expect(clippy::print_stderr, reason = "top-level error reporting")] - Err(err) => { - eprintln!("Error: {err:?}"); - 1 - } - } - }); + let status: ExitStatus = + tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on(run()); - std::process::exit(exit_code); + std::process::exit(i32::from(status.0)); } -async fn run() -> anyhow::Result { +async fn run() -> ExitStatus { let args = Command::parse(); let mut owned_config = OwnedSessionConfig::default(); - let session = Session::init(owned_config.as_config())?; + let session = match Session::init(owned_config.as_config()) { + Ok(session) => session, + Err(err) => { + vite_task::print_error(&err); + return ExitStatus::FAILURE; + } + }; session.main(args).await } diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/error_cycle_dependency/snapshots/cycle_dependency_error.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/error_cycle_dependency/snapshots/cycle_dependency_error.md index 80dec14ab..b2ec4748d 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/error_cycle_dependency/snapshots/cycle_dependency_error.md +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/error_cycle_dependency/snapshots/cycle_dependency_error.md @@ -9,5 +9,5 @@ task-a -> task-b -> task-a cycle **Exit code:** 1 ``` -Error: Cycle dependency detected: error-cycle-dependency-test#task-a -> error-cycle-dependency-test#task-b -> error-cycle-dependency-test#task-a +error: Cycle dependency detected: error-cycle-dependency-test#task-a -> error-cycle-dependency-test#task-b -> error-cycle-dependency-test#task-a ``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/non_interactive_recursive_typo_errors.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/non_interactive_recursive_typo_errors.md index c3b50a9c9..69e2df4de 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/non_interactive_recursive_typo_errors.md +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/non_interactive_recursive_typo_errors.md @@ -7,5 +7,5 @@ A typoed task name combined with `-r` (not cwd-only) should error without listin **Exit code:** 1 ``` -Error: Task "buid" not found +error: Task "buid" not found ``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/recursive_without_task_errors.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/recursive_without_task_errors.md index fd2c3bd04..818103276 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/recursive_without_task_errors.md +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/recursive_without_task_errors.md @@ -7,5 +7,5 @@ **Exit code:** 1 ``` -Error: No task specifier provided for 'run' command +error: No task specifier provided for 'run' command ``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/transitive_typo_errors.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/transitive_typo_errors.md index ab055b3aa..a79295b1f 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/transitive_typo_errors.md +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/transitive_typo_errors.md @@ -7,5 +7,5 @@ **Exit code:** 1 ``` -Error: Task "buid" not found +error: Task "buid" not found ``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/typo_in_task_script_fails_without_list.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/typo_in_task_script_fails_without_list.md index 6922dc291..386668976 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/typo_in_task_script_fails_without_list.md +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/typo_in_task_script_fails_without_list.md @@ -7,8 +7,6 @@ A typo inside a task's own script (i.e. a nested `vp run` command) should surfac **Exit code:** 1 ``` -Error: Failed to plan tasks from `vt run nonexistent-xyz` in task task-select-test#run-typo-task - -Caused by: - Task "nonexistent-xyz" not found +error: Failed to plan tasks from `vt run nonexistent-xyz` in task task-select-test#run-typo-task +* Task "nonexistent-xyz" not found ``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/verbose_without_task_errors.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/verbose_without_task_errors.md index fb34abe66..083134214 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/verbose_without_task_errors.md +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task_select/snapshots/verbose_without_task_errors.md @@ -7,5 +7,5 @@ **Exit code:** 1 ``` -Error: No task specifier provided for 'run' command +error: No task specifier provided for 'run' command ```