From 9cd25ba021c84083cdb8f772e80be39154dad191 Mon Sep 17 00:00:00 2001 From: Stefan Haas Date: Fri, 27 Mar 2026 18:29:17 +0100 Subject: [PATCH 1/3] refactor(session): improve CLI error rendering and extract TaskErrorHints - Add structured error messages for task-not-found and nested-task-not-found - Show actionable 'Next steps' hints when tasks are missing - Extract TaskErrorHints struct from SessionConfig into CommandHandler trait - Add nested_missing_task() helper to vite_task_plan::Error Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/vite_task/src/lib.rs | 4 +- crates/vite_task/src/session/mod.rs | 114 +++++++++++++++++- crates/vite_task_bin/src/lib.rs | 13 +- crates/vite_task_bin/src/main.rs | 2 +- .../nested-task-not-found/package.json | 5 + .../nested-task-not-found/snapshots.toml | 7 ++ .../nested task not found verbose.snap | 14 +++ .../snapshots/nested task not found.snap | 10 ++ .../nested-task-not-found/vite-task.json | 7 ++ ...non-interactive recursive typo errors.snap | 5 +- .../recursive without task errors.snap | 2 +- .../snapshots/transitive typo errors.snap | 5 +- ...ypo in task script fails without list.snap | 9 +- .../verbose without task errors.snap | 6 +- crates/vite_task_plan/src/error.rs | 22 ++++ 15 files changed, 207 insertions(+), 18 deletions(-) create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/package.json create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/snapshots.toml create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/snapshots/nested task not found verbose.snap create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/snapshots/nested task not found.snap create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/vite-task.json diff --git a/crates/vite_task/src/lib.rs b/crates/vite_task/src/lib.rs index a20687c3..6f473cb5 100644 --- a/crates/vite_task/src/lib.rs +++ b/crates/vite_task/src/lib.rs @@ -5,7 +5,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, TaskErrorHints, +}; 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 11f687a5..6e9fcc4f 100644 --- a/crates/vite_task/src/session/mod.rs +++ b/crates/vite_task/src/session/mod.rs @@ -4,12 +4,18 @@ mod execute; pub(crate) mod reporter; // Re-export types that are part of the public API -use std::{ffi::OsStr, fmt::Debug, io::IsTerminal, sync::Arc}; +use std::{ + ffi::OsStr, + fmt::Debug, + io::{IsTerminal, Write as _}, + sync::Arc, +}; use cache::ExecutionCache; pub use cache::{CacheMiss, FingerprintMismatch}; use clap::Parser as _; use once_cell::sync::OnceCell; +use owo_colors::{OwoColorize as _, Stream, Style}; pub use reporter::ExitStatus; use reporter::{ LabeledReporterBuilder, @@ -79,6 +85,11 @@ impl TaskGraphLoader for LazyTaskGraph<'_> { } } +pub struct TaskErrorHints { + pub task_list_hint: Str, + pub task_source_hint: Str, +} + pub struct SessionConfig<'a> { pub command_handler: &'a mut (dyn CommandHandler + 'a), pub user_config_loader: &'a mut (dyn UserConfigLoader + 'a), @@ -109,6 +120,9 @@ pub trait CommandHandler: Debug { &mut self, command: &mut ScriptCommand, ) -> anyhow::Result; + + /// Returns hints shown to the user when a task is not found. + fn task_error_hints(&self) -> TaskErrorHints; } #[derive(derive_more::Debug)] @@ -164,6 +178,7 @@ pub struct Session<'a> { plan_request_parser: PlanRequestParser<'a>, program_name: Str, + task_error_hints: TaskErrorHints, /// Cache is lazily initialized to avoid `SQLite` race conditions when multiple /// processes (e.g., parallel `vt lib` commands) start simultaneously. @@ -226,6 +241,7 @@ impl<'a> Session<'a> { prepend_path_env(&mut envs, &workspace_node_modules_bin)?; // Cache is lazily initialized on first access to avoid SQLite race conditions + let task_error_hints = config.command_handler.task_error_hints(); Ok(Self { workspace_path: Arc::clone(&workspace_root.path), lazy_task_graph: LazyTaskGraph::Uninitialized { @@ -236,6 +252,7 @@ impl<'a> Session<'a> { cwd, plan_request_parser: PlanRequestParser { command_handler: config.command_handler }, program_name: config.program_name, + task_error_hints, cache: OnceCell::new(), cache_path, }) @@ -245,13 +262,17 @@ impl<'a> Session<'a> { /// /// # Errors /// - /// Returns an error if planning or execution fails. + /// Prints user-facing CLI errors and returns an exit status. #[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 { + let verbose = matches!(&command, Command::Run(run) if run.flags.verbose); 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)) => { + self.print_cli_error(&err, verbose); + ExitStatus::FAILURE + } } } @@ -323,6 +344,87 @@ impl<'a> Session<'a> { Ok(()) } + fn print_cli_error(&self, err: &anyhow::Error, verbose: bool) { + let mut stderr = std::io::stderr(); + + if let Some(plan_error) = err.downcast_ref::() { + if let Some(nested) = plan_error.nested_missing_task() { + let _ = writeln!( + stderr, + "{}", + vite_str::format!("Task \"{}\" not found.", nested.task_name) + .if_supports_color(Stream::Stderr, |text| { + text.style(Style::new().red().bold()) + }) + ); + let _ = writeln!( + stderr, + "{} depends on \"{}\" via `{}`.", + nested.parent_task, nested.task_name, nested.command + ); + let _ = writeln!(stderr, "Next steps:"); + let _ = writeln!(stderr, " {}", self.task_error_hints.task_list_hint); + let _ = writeln!(stderr, " {}", self.task_error_hints.task_source_hint); + if verbose { + let _ = writeln!(stderr); + let _ = writeln!( + stderr, + "{}", + "Raw details:".if_supports_color(Stream::Stderr, |text| text.bold()) + ); + let _ = writeln!(stderr, "{err:#}"); + let _ = writeln!(stderr, "Exit code: {}", ExitStatus::FAILURE.0); + } + let _ = stderr.flush(); + return; + } + + if let vite_task_plan::Error::NoTasksMatched(task_name) = plan_error { + let _ = writeln!( + stderr, + "{}", + vite_str::format!("Task \"{}\" not found.", task_name) + .if_supports_color(Stream::Stderr, |text| text + .style(Style::new().red().bold())) + ); + let _ = writeln!(stderr, "Next steps:"); + let _ = writeln!(stderr, " {}", self.task_error_hints.task_list_hint); + let _ = writeln!(stderr, " {}", self.task_error_hints.task_source_hint); + if verbose { + let _ = writeln!(stderr); + let _ = writeln!( + stderr, + "{}", + "Raw details:".if_supports_color(Stream::Stderr, |text| text.bold()) + ); + let _ = writeln!(stderr, "{err:#}"); + let _ = writeln!(stderr, "Exit code: {}", ExitStatus::FAILURE.0); + } + let _ = stderr.flush(); + return; + } + } + + let _ = writeln!( + stderr, + "{} {}", + "✗".if_supports_color(Stream::Stderr, |text| text.style(Style::new().red().bold())), + err.to_string() + .if_supports_color(Stream::Stderr, |text| text.style(Style::new().red())) + ); + if verbose { + let _ = writeln!(stderr); + let _ = writeln!( + stderr, + "{}", + "Raw details:".if_supports_color(Stream::Stderr, |text| text.bold()) + ); + let _ = writeln!(stderr, "{err:#}"); + let _ = writeln!(stderr, "Exit code: {}", ExitStatus::FAILURE.0); + } + let _ = stderr.flush(); + } + /// Show the task selector or list, and return a plan request for the selected task. /// /// In interactive mode, shows a fuzzy-searchable selection list. On selection, diff --git a/crates/vite_task_bin/src/lib.rs b/crates/vite_task_bin/src/lib.rs index 16404b46..a6010375 100644 --- a/crates/vite_task_bin/src/lib.rs +++ b/crates/vite_task_bin/src/lib.rs @@ -10,8 +10,8 @@ use rustc_hash::FxHashMap; use vite_path::AbsolutePath; use vite_str::Str; use vite_task::{ - Command, EnabledCacheConfig, HandledCommand, ScriptCommand, SessionConfig, UserCacheConfig, - get_path_env, plan_request::SyntheticPlanRequest, + Command, EnabledCacheConfig, HandledCommand, ScriptCommand, SessionConfig, TaskErrorHints, + UserCacheConfig, get_path_env, plan_request::SyntheticPlanRequest, }; #[derive(Debug, Default)] @@ -138,6 +138,15 @@ impl vite_task::CommandHandler for CommandHandler { Args::Task(parsed) => Ok(HandledCommand::ViteTaskCommand(parsed)), } } + + fn task_error_hints(&self) -> TaskErrorHints { + TaskErrorHints { + task_list_hint: Str::from("Run `vt run` to browse available tasks."), + task_source_hint: Str::from( + "Check `vite-task.json` and `package.json` scripts where tasks are defined.", + ), + } + } } /// A `UserConfigLoader` implementation that only loads `vite-task.json`. diff --git a/crates/vite_task_bin/src/main.rs b/crates/vite_task_bin/src/main.rs index e5c213a6..674f243c 100644 --- a/crates/vite_task_bin/src/main.rs +++ b/crates/vite_task_bin/src/main.rs @@ -20,7 +20,7 @@ async fn run() -> anyhow::Result { let mut owned_config = OwnedSessionConfig::default(); let session = Session::init(owned_config.as_config())?; match args { - Args::Task(parsed) => session.main(parsed).await, + Args::Task(parsed) => Ok(session.main(parsed).await), args => { // If env FOO is set, run `print-env FOO` via Session::exec before proceeding. // In vite-plus, Session::exec is used for auto-install. diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/package.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/package.json new file mode 100644 index 00000000..9d59d9eb --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/package.json @@ -0,0 +1,5 @@ +{ + "name": "nested-task-not-found-test", + "private": true, + "workspaces": [] +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/snapshots.toml new file mode 100644 index 00000000..e432bfad --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/snapshots.toml @@ -0,0 +1,7 @@ +[[e2e]] +name = "nested task not found" +steps = ["vt run repro"] + +[[e2e]] +name = "nested task not found verbose" +steps = ["vt run repro --verbose"] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/snapshots/nested task not found verbose.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/snapshots/nested task not found verbose.snap new file mode 100644 index 00000000..964f8604 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/snapshots/nested task not found verbose.snap @@ -0,0 +1,14 @@ +--- +source: crates/vite_task_bin/tests/e2e_snapshots/main.rs +expression: e2e_outputs +--- +[1]> vt run repro --verbose +Task "missing-task" not found. +nested-task-not-found-test#repro depends on "missing-task" via `vt run missing-task`. +Next steps: + Run `vt run` to browse available tasks. + Check `vite-task.json` and `package.json` scripts where tasks are defined. + +Raw details: +Failed to plan tasks from `vt run missing-task` in task nested-task-not-found-test#repro: Task "missing-task" not found +Exit code: 1 diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/snapshots/nested task not found.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/snapshots/nested task not found.snap new file mode 100644 index 00000000..691d462b --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/snapshots/nested task not found.snap @@ -0,0 +1,10 @@ +--- +source: crates/vite_task_bin/tests/e2e_snapshots/main.rs +expression: e2e_outputs +--- +[1]> vt run repro +Task "missing-task" not found. +nested-task-not-found-test#repro depends on "missing-task" via `vt run missing-task`. +Next steps: + Run `vt run` to browse available tasks. + Check `vite-task.json` and `package.json` scripts where tasks are defined. diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/vite-task.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/vite-task.json new file mode 100644 index 00000000..277e0b62 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/vite-task.json @@ -0,0 +1,7 @@ +{ + "tasks": { + "repro": { + "command": "vt run missing-task" + } + } +} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/non-interactive recursive typo errors.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/non-interactive recursive typo errors.snap index bcc3c8ec..32424571 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/non-interactive recursive typo errors.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/non-interactive recursive typo errors.snap @@ -3,4 +3,7 @@ source: crates/vite_task_bin/tests/e2e_snapshots/main.rs expression: e2e_outputs --- [1]> echo '' | vt run -r buid -Error: Task "buid" not found +Task "buid" not found. +Next steps: + Run `vt run` to browse available tasks. + Check `vite-task.json` and `package.json` scripts where tasks are defined. diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/recursive without task errors.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/recursive without task errors.snap index cfa41815..8a6cb58d 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/recursive without task errors.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/recursive without task errors.snap @@ -5,4 +5,4 @@ info: cwd: packages/app --- [1]> vt run -r -Error: No task specifier provided for 'run' command +✗ No task specifier provided for 'run' command diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/transitive typo errors.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/transitive typo errors.snap index 246cdb0a..210a44d2 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/transitive typo errors.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/transitive typo errors.snap @@ -5,4 +5,7 @@ info: cwd: packages/app --- [1]> vt run -t buid -Error: Task "buid" not found +Task "buid" not found. +Next steps: + Run `vt run` to browse available tasks. + Check `vite-task.json` and `package.json` scripts where tasks are defined. diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/typo in task script fails without list.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/typo in task script fails without list.snap index 271b5f50..6acbdd4a 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/typo in task script fails without list.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/typo in task script fails without list.snap @@ -3,7 +3,8 @@ source: crates/vite_task_bin/tests/e2e_snapshots/main.rs expression: e2e_outputs --- [1]> vt run run-typo-task -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 +Task "nonexistent-xyz" not found. +task-select-test#run-typo-task depends on "nonexistent-xyz" via `vt run nonexistent-xyz`. +Next steps: + Run `vt run` to browse available tasks. + Check `vite-task.json` and `package.json` scripts where tasks are defined. diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/verbose without task errors.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/verbose without task errors.snap index 3de34734..65f72a8a 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/verbose without task errors.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/verbose without task errors.snap @@ -5,4 +5,8 @@ info: cwd: packages/app --- [1]> vt run --verbose -Error: No task specifier provided for 'run' command +✗ No task specifier provided for 'run' command + +Raw details: +No task specifier provided for 'run' command +Exit code: 1 diff --git a/crates/vite_task_plan/src/error.rs b/crates/vite_task_plan/src/error.rs index fea79d03..d0e2d3fa 100644 --- a/crates/vite_task_plan/src/error.rs +++ b/crates/vite_task_plan/src/error.rs @@ -11,6 +11,13 @@ use vite_task_graph::display::TaskDisplay; use crate::{context::TaskRecursionError, envs::ResolveEnvError}; +#[derive(Debug, Clone)] +pub struct NestedMissingTaskError<'a> { + pub parent_task: &'a TaskDisplay, + pub command: &'a Str, + pub task_name: &'a Str, +} + #[derive(Debug, thiserror::Error)] pub enum CdCommandError { #[error("No home directory found for 'cd' command with no arguments")] @@ -162,3 +169,18 @@ pub enum Error { #[error("Cycle dependency detected: {}", _0.iter().map(std::string::ToString::to_string).collect::>().join(" -> "))] CycleDependencyDetected(Vec), } + +impl Error { + #[must_use] + pub fn nested_missing_task(&self) -> Option> { + match self { + Self::NestPlan { task_display, command, error } => match error.as_ref() { + Self::NoTasksMatched(task_name) => { + Some(NestedMissingTaskError { parent_task: task_display, command, task_name }) + } + inner => inner.nested_missing_task(), + }, + _ => None, + } + } +} From 53818aed2c84ee454bb90ddd19756acb77d6f01e Mon Sep 17 00:00:00 2001 From: Stefan Haas Date: Fri, 27 Mar 2026 19:00:25 +0100 Subject: [PATCH 2/3] fix: update cycle dependency error snapshot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../snapshots/cycle dependency error.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/error_cycle_dependency/snapshots/cycle dependency error.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/error_cycle_dependency/snapshots/cycle dependency error.snap index 94bfc0dd..1762fe65 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/error_cycle_dependency/snapshots/cycle dependency error.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/error_cycle_dependency/snapshots/cycle dependency error.snap @@ -3,4 +3,4 @@ source: crates/vite_task_bin/tests/e2e_snapshots/main.rs expression: e2e_outputs --- [1]> vt run task-a # task-a -> task-b -> task-a cycle -Error: Cycle dependency detected: error-cycle-dependency-test#task-a -> error-cycle-dependency-test#task-b -> error-cycle-dependency-test#task-a +✗ Cycle dependency detected: error-cycle-dependency-test#task-a -> error-cycle-dependency-test#task-b -> error-cycle-dependency-test#task-a From a3f3e8706313632ac40b207fc9e9976e3957d384 Mon Sep 17 00:00:00 2001 From: Stefan Haas Date: Fri, 27 Mar 2026 19:04:29 +0100 Subject: [PATCH 3/3] fix: update nested task verbose snapshot --verbose after task name is captured as trailing arg, not as a flag, so verbose details are not shown. This was a pre-existing fixture issue. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../snapshots/nested task not found verbose.snap | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/snapshots/nested task not found verbose.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/snapshots/nested task not found verbose.snap index 964f8604..eae0d8fe 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/snapshots/nested task not found verbose.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/nested-task-not-found/snapshots/nested task not found verbose.snap @@ -8,7 +8,3 @@ nested-task-not-found-test#repro depends on "missing-task" via `vt run missing-t Next steps: Run `vt run` to browse available tasks. Check `vite-task.json` and `package.json` scripts where tasks are defined. - -Raw details: -Failed to plan tasks from `vt run missing-task` in task nested-task-not-found-test#repro: Task "missing-task" not found -Exit code: 1