diff --git a/crates/vite_task/src/cli/mod.rs b/crates/vite_task/src/cli/mod.rs index dca722db..15adc85f 100644 --- a/crates/vite_task/src/cli/mod.rs +++ b/crates/vite_task/src/cli/mod.rs @@ -14,7 +14,7 @@ pub enum CacheSubcommand { } /// Flags that control how a `run` command selects tasks. -#[derive(Debug, Clone, clap::Args)] +#[derive(Debug, Clone, PartialEq, Eq, clap::Args)] pub struct RunFlags { #[clap(flatten)] pub package_query: PackageQueryArgs, @@ -36,10 +36,10 @@ pub struct RunFlags { /// /// Contains the `--last-details` flag which is resolved into a separate /// `ResolvedCommand::RunLastDetails` variant internally. -#[derive(Debug, clap::Args)] +#[derive(Debug, clap::Parser)] pub struct RunCommand { /// `packageName#taskName` or `taskName`. If omitted, lists all available tasks. - pub(crate) task_specifier: Option, + pub(crate) task_specifier: Option, #[clap(flatten)] pub(crate) flags: RunFlags, @@ -109,10 +109,10 @@ pub enum ResolvedCommand { /// /// Does not contain `last_details` — that case is represented by /// [`ResolvedCommand::RunLastDetails`] instead. -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolvedRunCommand { /// `packageName#taskName` or `taskName`. If omitted, lists all available tasks. - pub task_specifier: Option, + pub task_specifier: Option, pub flags: RunFlags, @@ -151,21 +151,25 @@ impl ResolvedRunCommand { pub fn into_query_plan_request( self, cwd: &Arc, - ) -> Result { - let task_specifier = self.task_specifier.ok_or(CLITaskQueryError::MissingTaskSpecifier)?; + ) -> Result<(QueryPlanRequest, bool), CLITaskQueryError> { + let raw_specifier = self.task_specifier.ok_or(CLITaskQueryError::MissingTaskSpecifier)?; + let task_specifier = TaskSpecifier::parse_raw(&raw_specifier); - let (package_query, _is_cwd_only) = + let (package_query, is_cwd_only) = self.flags.package_query.into_package_query(task_specifier.package_name, cwd)?; let include_explicit_deps = !self.flags.ignore_depends_on; - Ok(QueryPlanRequest { - query: TaskQuery { - package_query, - task_name: task_specifier.task_name, - include_explicit_deps, + Ok(( + QueryPlanRequest { + query: TaskQuery { + package_query, + task_name: task_specifier.task_name, + include_explicit_deps, + }, + plan_options: PlanOptions { extra_args: self.additional_args.into() }, }, - plan_options: PlanOptions { extra_args: self.additional_args.into() }, - }) + is_cwd_only, + )) } } diff --git a/crates/vite_task/src/session/mod.rs b/crates/vite_task/src/session/mod.rs index 6d7cd5ba..833c2e8f 100644 --- a/crates/vite_task/src/session/mod.rs +++ b/crates/vite_task/src/session/mod.rs @@ -8,6 +8,7 @@ use std::{ffi::OsStr, fmt::Debug, io::IsTerminal, sync::Arc}; use cache::ExecutionCache; pub use cache::{CacheMiss, FingerprintMismatch}; +use clap::Parser as _; use once_cell::sync::OnceCell; pub use reporter::ExitStatus; use reporter::{ @@ -19,7 +20,7 @@ use vite_path::{AbsolutePath, AbsolutePathBuf}; use vite_select::SelectItem; use vite_str::Str; use vite_task_graph::{ - IndexedTaskGraph, TaskGraph, TaskGraphLoadError, TaskSpecifier, config::user::UserCacheConfig, + IndexedTaskGraph, TaskGraph, TaskGraphLoadError, config::user::UserCacheConfig, loader::UserConfigLoader, }; use vite_task_plan::{ @@ -29,7 +30,26 @@ use vite_task_plan::{ }; use vite_workspace::{WorkspaceRoot, find_workspace_root}; -use crate::cli::{CacheSubcommand, Command, ResolvedCommand, RunCommand, RunFlags}; +use crate::cli::{CacheSubcommand, Command, ResolvedCommand, ResolvedRunCommand, RunCommand}; + +/// Error type for [`Session::main`]. +/// +/// `EarlyExit` represents a non-error exit (e.g. printing a task list) and +/// the caller should exit with the contained status without printing an error. +/// It exists only for easier `?` control flow. +enum SessionError { + Anyhow(anyhow::Error), + EarlyExit(ExitStatus), +} + +impl From for SessionError +where + anyhow::Error: From, +{ + fn from(err: T) -> Self { + Self::Anyhow(anyhow::Error::from(err)) + } +} #[derive(Debug)] enum LazyTaskGraph<'a> { @@ -109,7 +129,9 @@ impl vite_task_plan::PlanRequestParser for PlanRequestParser<'_> { } ResolvedCommand::Run(run_command) => { match run_command.into_query_plan_request(&command.cwd) { - Ok(query_plan_request) => Ok(Some(PlanRequest::Query(query_plan_request))), + Ok((query_plan_request, _)) => { + Ok(Some(PlanRequest::Query(query_plan_request))) + } Err(crate::cli::CLITaskQueryError::MissingTaskSpecifier) => { Ok(Some(PlanRequest::Synthetic( command.to_synthetic_plan_request(UserCacheConfig::disabled()), @@ -232,68 +254,94 @@ impl<'a> Session<'a> { reason = "session is single-threaded, futures do not need to be Send" )] pub async fn main(mut self, command: Command) -> anyhow::Result { + match self.main_inner(command).await { + Ok(()) => Ok(ExitStatus::SUCCESS), + Err(SessionError::EarlyExit(status)) => Ok(status), + Err(SessionError::Anyhow(err)) => Err(err), + } + } + + /// # Panics + /// + /// Panics if parsing a hardcoded bare `RunCommand` fails (should never happen). + #[expect( + clippy::future_not_send, + reason = "session is single-threaded, futures do not need to be Send" + )] + async fn main_inner(&mut self, command: Command) -> Result<(), SessionError> { match command.into_resolved() { ResolvedCommand::Cache { ref subcmd } => self.handle_cache_command(subcmd), ResolvedCommand::RunLastDetails => self.show_last_run_details(), - ResolvedCommand::Run(run_command) => { - let cwd = Arc::clone(&self.cwd); + ResolvedCommand::Run(mut run_command) => { let is_interactive = std::io::stdin().is_terminal() && std::io::stdout().is_terminal(); - // Save task name and flags before consuming run_command - let task_name = run_command.task_specifier.as_ref().map(|s| s.task_name.clone()); - let show_details = run_command.flags.verbose; - let flags = run_command.flags.clone(); - let additional_args = run_command.additional_args.clone(); - - match self.plan_from_cli_run_resolved(cwd, run_command).await { - Ok(ref graph) if graph.node_count() == 0 => { - // No tasks matched the query — show task selector / "did you mean" - self.handle_no_task( - is_interactive, - task_name.as_deref(), - flags, - additional_args, - ) - .await - } - Ok(graph) => { - let builder = LabeledReporterBuilder::new( - self.workspace_path(), - Box::new(tokio::io::stdout()), - show_details, - Some(self.make_summary_writer()), - ); - Ok(self - .execute_graph(graph, Box::new(builder)) - .await - .err() - .unwrap_or(ExitStatus::SUCCESS)) + let graph = if let Some(ref task_specifier) = run_command.task_specifier { + // Task specifier provided — plan it. + let cwd = Arc::clone(&self.cwd); + let (graph, is_cwd_only) = + self.plan_from_cli_run_resolved(cwd, run_command.clone()).await?; + + if graph.node_count() == 0 { + // No tasks matched. With is_cwd_only (no scope flags) the + // task name is a typo — show the selector. Otherwise error. + if is_cwd_only { + self.handle_no_task(is_interactive, &mut run_command).await?; + let cwd = Arc::clone(&self.cwd); + self.plan_from_cli_run_resolved(cwd, run_command.clone()).await?.0 + } else { + return Err(vite_task_plan::Error::NoTasksMatched( + task_specifier.clone(), + ) + .into()); + } + } else { + graph } - Err(err) if err.is_missing_task_specifier() => { - self.handle_no_task(is_interactive, None, flags, additional_args).await + } else { + // No task specifier (e.g. `vp run` or `vp run --verbose`). + // Only bare `vp run` enters the selector; with extra flags, error. + let bare = RunCommand::try_parse_from::<_, &str>([]) + .expect("parsing hardcoded bare command should never fail") + .into_resolved(); + if run_command != bare { + return Err(vite_task_plan::Error::MissingTaskSpecifier.into()); } - Err(err) => Err(err.into()), - } + self.handle_no_task(is_interactive, &mut run_command).await?; + let cwd = Arc::clone(&self.cwd); + self.plan_from_cli_run_resolved(cwd, run_command.clone()).await?.0 + }; + + let builder = LabeledReporterBuilder::new( + self.workspace_path(), + Box::new(tokio::io::stdout()), + run_command.flags.verbose, + Some(self.make_summary_writer()), + ); + self.execute_graph(graph, Box::new(builder)).await.map_err(SessionError::EarlyExit) } } } - fn handle_cache_command(&self, subcmd: &CacheSubcommand) -> anyhow::Result { + fn handle_cache_command(&self, subcmd: &CacheSubcommand) -> Result<(), SessionError> { match subcmd { CacheSubcommand::Clean => { if self.cache_path.as_path().exists() { std::fs::remove_dir_all(&self.cache_path)?; } - Ok(ExitStatus::SUCCESS) } } + Ok(()) } - /// Handle the case where no task was specified or a task name was not found. + /// Show the task selector or list, and update the run command with the selected task. + /// + /// In interactive mode, shows a fuzzy-searchable selection list. On selection, + /// updates `run_command.task_specifier` and returns `Ok(())` so the caller + /// can plan and execute the selected task. /// - /// In interactive mode, shows a fuzzy-searchable selection list. - /// In non-interactive mode, prints the task list or "did you mean" suggestions. + /// In non-interactive mode, prints the task list (or "did you mean" suggestions) + /// and returns `Err(SessionError::EarlyExit(_))` — no further execution needed. #[expect( clippy::future_not_send, reason = "session is single-threaded, futures do not need to be Send" @@ -301,10 +349,9 @@ impl<'a> Session<'a> { async fn handle_no_task( &mut self, is_interactive: bool, - not_found_name: Option<&str>, - flags: RunFlags, - additional_args: Vec, - ) -> anyhow::Result { + run_command: &mut ResolvedRunCommand, + ) -> Result<(), SessionError> { + let not_found_name = run_command.task_specifier.as_deref(); let cwd = Arc::clone(&self.cwd); let task_graph = self.ensure_task_graph_loaded().await?; let current_package_path = task_graph.get_package_path_from_cwd(&cwd).cloned(); @@ -379,34 +426,31 @@ impl<'a> Session<'a> { )?; let Some(selected_index) = selected_index else { - // Non-interactive: if no task was found, return failure. Otherwise, print the list and return - return if not_found_name.is_some() { - Ok(ExitStatus::FAILURE) + // Non-interactive, the list was printed. + return Err(SessionError::EarlyExit(if not_found_name.is_some() { + // For `vp run typo`, return FAILURE status + ExitStatus::FAILURE } else { - Ok(ExitStatus::SUCCESS) - }; + // For bare `vp run`, return SUCCESS status + ExitStatus::SUCCESS + })); }; - // Interactive: run the selected task + // Interactive: print selected task and run it let selected_label = &select_items[selected_index].label; - let task_specifier = TaskSpecifier::parse_raw(selected_label); - let show_details = flags.verbose; - let run_command = RunCommand { - task_specifier: Some(task_specifier), - flags, - additional_args, - last_details: false, - }; - - let cwd = Arc::clone(&self.cwd); - let graph = self.plan_from_cli_run(cwd, run_command).await?; - let builder = LabeledReporterBuilder::new( - self.workspace_path(), - Box::new(tokio::io::stdout()), - show_details, - Some(self.make_summary_writer()), - ); - Ok(self.execute_graph(graph, Box::new(builder)).await.err().unwrap_or(ExitStatus::SUCCESS)) + { + use std::io::Write as _; + + use owo_colors::{OwoColorize as _, Stream}; + writeln!( + stdout, + "{}{}", + "Selected task: ".if_supports_color(Stream::Stdout, |s| s.bold()), + selected_label, + )?; + } + run_command.task_specifier = Some(selected_label.clone()); + Ok(()) } /// Lazily initializes and returns the execution cache. @@ -447,7 +491,7 @@ impl<'a> Session<'a> { clippy::print_stderr, reason = "--last-details error messages are user-facing diagnostics, not debug output" )] - fn show_last_run_details(&self) -> anyhow::Result { + fn show_last_run_details(&self) -> Result<(), SessionError> { let path = self.summary_file_path(); match LastRunSummary::read_from_path(&path) { Ok(Some(summary)) => { @@ -458,18 +502,18 @@ impl<'a> Session<'a> { stdout.write_all(&buf)?; stdout.flush()?; } - Ok(ExitStatus(summary.exit_code)) + Err(SessionError::EarlyExit(ExitStatus(summary.exit_code))) } Ok(None) => { eprintln!("No previous run summary found. Run a task first to generate a summary."); - Ok(ExitStatus::FAILURE) + Err(SessionError::EarlyExit(ExitStatus::FAILURE)) } Err(ReadSummaryError::IncompatibleVersion) => { eprintln!( "Summary data was saved by a different version of vite-task and cannot be read. \ Run a task to generate a new summary." ); - Ok(ExitStatus::FAILURE) + Err(SessionError::EarlyExit(ExitStatus::FAILURE)) } Err(ReadSummaryError::Io(err)) => Err(err.into()), } @@ -578,7 +622,8 @@ impl<'a> Session<'a> { cwd: Arc, command: RunCommand, ) -> Result { - self.plan_from_cli_run_resolved(cwd, command.into_resolved()).await + let (graph, _) = self.plan_from_cli_run_resolved(cwd, command.into_resolved()).await?; + Ok(graph) } /// Internal: plans execution from a resolved run command. @@ -591,9 +636,9 @@ impl<'a> Session<'a> { &mut self, cwd: Arc, command: crate::cli::ResolvedRunCommand, - ) -> Result { - let query_plan_request = match command.into_query_plan_request(&cwd) { - Ok(query_plan_request) => query_plan_request, + ) -> Result<(ExecutionGraph, bool), vite_task_plan::Error> { + let (query_plan_request, is_cwd_only) = match command.into_query_plan_request(&cwd) { + Ok(result) => result, Err(crate::cli::CLITaskQueryError::MissingTaskSpecifier) => { return Err(vite_task_plan::Error::MissingTaskSpecifier); } @@ -606,7 +651,7 @@ impl<'a> Session<'a> { }); } }; - vite_task_plan::plan_query( + let graph = vite_task_plan::plan_query( query_plan_request, &self.workspace_path, &cwd, @@ -614,6 +659,7 @@ impl<'a> Session<'a> { &mut self.plan_request_parser, &mut self.lazy_task_graph, ) - .await + .await?; + Ok((graph, is_cwd_only)) } } diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-list/snapshots/vp run in script.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-list/snapshots/vp run in script.snap index 3e933d32..15546912 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-list/snapshots/vp run in script.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-list/snapshots/vp run in script.snap @@ -14,5 +14,6 @@ Search task (↑/↓ to move, enter to select): lib#build: echo build lib @ write-key: enter $ vp run ⊘ cache disabled +Selected task: hello $ echo hello from root ⊘ cache disabled hello from root diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select-truncate/snapshots/interactive long command truncated.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select-truncate/snapshots/interactive long command truncated.snap index 7c021ed6..f1bd1d22 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select-truncate/snapshots/interactive long command truncated.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select-truncate/snapshots/interactive long command truncated.snap @@ -40,5 +40,6 @@ Search task (↑/↓ to move, enter to select): > long-cmd: echo aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa… test: echo test app @ write-key: enter +Selected task: long-cmd ~/packages/app$ echo aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa ⊘ cache disabled aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots.toml index ddea03ae..1d0fd7ed 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots.toml +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots.toml @@ -12,9 +12,9 @@ steps = [ "echo '' | vp run buid", ] -# Non-interactive: typo with -r flag +# Non-interactive: typo with -r flag errors (not cwd_only) [[e2e]] -name = "non-interactive did you mean with recursive" +name = "non-interactive recursive typo errors" steps = [ "echo '' | vp run -r buid", ] @@ -51,20 +51,20 @@ steps = [ { command = "vp run", interactions = [{ "expect-milestone" = "task-select::0" }, { "write" = "lin" }, { "expect-milestone" = "task-select:lin:0" }, { "write-key" = "escape" }, { "expect-milestone" = "task-select::0" }, { "write-key" = "enter" }] }, ] -# Interactive: -r flag preserved +# -r flag without task errors (not bare) [[e2e]] -name = "interactive select with recursive" +name = "recursive without task errors" cwd = "packages/app" steps = [ - { command = "vp run -r", interactions = [{ "expect-milestone" = "task-select::0" }, { "write-key" = "enter" }] }, + "vp run -r", ] -# Interactive: -t flag + typo +# -t flag + typo errors (not cwd_only) [[e2e]] -name = "interactive select with typo and transitive" +name = "transitive typo errors" cwd = "packages/app" steps = [ - { command = "vp run -t buid", interactions = [{ "expect-milestone" = "task-select:buid:0" }, { "write-key" = "enter" }] }, + "vp run -t buid", ] # Interactive: scroll down past visible page, then select a task beyond the initial viewport @@ -131,3 +131,19 @@ name = "typo in task script fails without list" steps = [ "vp run run-typo-task", ] + +# --verbose without task: not bare, errors with "no task specifier provided" +[[e2e]] +name = "verbose without task errors" +cwd = "packages/app" +steps = [ + "vp run --verbose", +] + +# --verbose with typo: is_cwd_only is true, shows interactive selector +[[e2e]] +name = "verbose with typo enters selector" +cwd = "packages/app" +steps = [ + { command = "vp run buid --verbose", interactions = [{ "expect-milestone" = "task-select:buid:0" }, { "write-key" = "enter" }] }, +] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive enter with no results does nothing.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive enter with no results does nothing.snap index f8fb9bf7..63568d19 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive enter with no results does nothing.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive enter with no results does nothing.snap @@ -42,5 +42,6 @@ Search task (↑/↓ to move, enter to select): task-select-test#format: echo format root (…3 more) @ write-key: enter +Selected task: build ~/packages/app$ echo build app ⊘ cache disabled build app diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive escape clears query.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive escape clears query.snap index a3169915..14e49503 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive escape clears query.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive escape clears query.snap @@ -42,5 +42,6 @@ Search task (↑/↓ to move, enter to select): task-select-test#format: echo format root (…3 more) @ write-key: enter +Selected task: build ~/packages/app$ echo build app ⊘ cache disabled build app diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive scroll long list.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive scroll long list.snap index 668f4a4a..ee81188a 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive scroll long list.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive scroll long list.snap @@ -67,5 +67,6 @@ Search task (↑/↓ to move, enter to select): task-select-test#format: echo format root (…3 more) @ write-key: enter +Selected task: build ~/packages/app$ echo build app ⊘ cache disabled build app diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search other package task.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search other package task.snap index 72975311..65655cc3 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search other package task.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search other package task.snap @@ -25,5 +25,6 @@ Search task (↑/↓ to move, enter to select): Search task (↑/↓ to move, enter to select): typec > lib#typecheck: echo typecheck lib @ write-key: enter +Selected task: lib#typecheck ~/packages/lib$ echo typecheck lib ⊘ cache disabled typecheck lib diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search preserves rating within package.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search preserves rating within package.snap index 8eaa73f7..71c5a7b7 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search preserves rating within package.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search preserves rating within package.snap @@ -37,5 +37,6 @@ Search task (↑/↓ to move, enter to select): t app#test: echo test app (…1 more) @ write-key: enter +Selected task: test ~/packages/lib$ echo test lib ⊘ cache disabled test lib diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search then select.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search then select.snap index 618400a0..fd4dc098 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search then select.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search then select.snap @@ -26,5 +26,6 @@ Search task (↑/↓ to move, enter to select): lin > lint: echo lint app lib#lint: echo lint lib @ write-key: enter +Selected task: lint ~/packages/app$ echo lint app ⊘ cache disabled lint app diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search with hash skips reorder.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search with hash skips reorder.snap index 97e14b1b..d9e0f83b 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search with hash skips reorder.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive search with hash skips reorder.snap @@ -28,5 +28,6 @@ Search task (↑/↓ to move, enter to select): lib# lib#test: echo test lib lib#typecheck: echo typecheck lib @ write-key: enter +Selected task: lib#build ~/packages/lib$ echo build lib ⊘ cache disabled build lib diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task from lib.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task from lib.snap index c216f31f..8d1b475a 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task from lib.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task from lib.snap @@ -21,5 +21,6 @@ Search task (↑/↓ to move, enter to select): task-select-test#format: echo format root (…3 more) @ write-key: enter +Selected task: build ~/packages/lib$ echo build lib ⊘ cache disabled build lib diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task.snap index ba29e6a6..3dacc4d2 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select task.snap @@ -37,5 +37,6 @@ Search task (↑/↓ to move, enter to select): task-select-test#format: echo format root (…3 more) @ write-key: enter +Selected task: lint ~/packages/app$ echo lint app ⊘ cache disabled lint app diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select with recursive.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select with recursive.snap deleted file mode 100644 index 5380ece4..00000000 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select with recursive.snap +++ /dev/null @@ -1,31 +0,0 @@ ---- -source: crates/vite_task_bin/tests/e2e_snapshots/main.rs -expression: e2e_outputs -info: - cwd: packages/app ---- -> vp run -r -@ expect-milestone: task-select::0 -Search task (↑/↓ to move, enter to select): -> build: echo build app - lint: echo lint app - test: echo test app - lib#build: echo build lib - lib#lint: echo lint lib - lib#test: echo test lib - lib#typecheck: echo typecheck lib - task-select-test#check: echo check root - task-select-test#clean: echo clean root - task-select-test#deploy: echo deploy root - task-select-test#docs: echo docs root - task-select-test#format: echo format root - (…3 more) -@ write-key: enter -~/packages/lib$ echo build lib ⊘ cache disabled -build lib - -~/packages/app$ echo build app ⊘ cache disabled -build app - ---- -[vp run] 0/2 cache hit (0%). (Run `vp run --last-details` for full details) diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select with typo and transitive.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select with typo and transitive.snap deleted file mode 100644 index 3d33262e..00000000 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select with typo and transitive.snap +++ /dev/null @@ -1,21 +0,0 @@ ---- -source: crates/vite_task_bin/tests/e2e_snapshots/main.rs -expression: e2e_outputs -info: - cwd: packages/app ---- -> vp run -t buid -@ expect-milestone: task-select:buid:0 -Task "buid" not found. -Search task (↑/↓ to move, enter to select): buid -> build: echo build app - lib#build: echo build lib -@ write-key: enter -~/packages/lib$ echo build lib ⊘ cache disabled -build lib - -~/packages/app$ echo build app ⊘ cache disabled -build app - ---- -[vp run] 0/2 cache hit (0%). (Run `vp run --last-details` for full details) diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select with typo.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select with typo.snap index 8a43077b..34bae938 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select with typo.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/interactive select with typo.snap @@ -11,5 +11,6 @@ Search task (↑/↓ to move, enter to select): buid > build: echo build app lib#build: echo build lib @ write-key: enter +Selected task: build ~/packages/app$ echo build app ⊘ cache disabled build app diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/non-interactive did you mean with recursive.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/non-interactive recursive typo errors.snap similarity index 56% rename from crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/non-interactive did you mean with recursive.snap rename to crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/non-interactive recursive typo errors.snap index f8940b84..47776655 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/non-interactive did you mean with recursive.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/non-interactive recursive typo errors.snap @@ -3,6 +3,4 @@ source: crates/vite_task_bin/tests/e2e_snapshots/main.rs expression: e2e_outputs --- [1]> echo '' | vp run -r buid -Task "buid" not found. Did you mean: - app#build: echo build app - lib#build: echo build lib +Error: Task "buid" not found 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 new file mode 100644 index 00000000..c67ca9a9 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/recursive without task errors.snap @@ -0,0 +1,8 @@ +--- +source: crates/vite_task_bin/tests/e2e_snapshots/main.rs +expression: e2e_outputs +info: + cwd: packages/app +--- +[1]> vp run -r +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.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/transitive typo errors.snap new file mode 100644 index 00000000..8c41bed6 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/transitive typo errors.snap @@ -0,0 +1,8 @@ +--- +source: crates/vite_task_bin/tests/e2e_snapshots/main.rs +expression: e2e_outputs +info: + cwd: packages/app +--- +[1]> vp run -t buid +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.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/typo in task script fails without list.snap index 51c5d7aa..88faadab 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 @@ -6,4 +6,4 @@ expression: e2e_outputs Error: Failed to plan tasks from `vp run nonexistent-xyz` in task task-select-test#run-typo-task Caused by: - no tasks matched the query + Task "nonexistent-xyz" not found diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/verbose with typo enters selector.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/verbose with typo enters selector.snap new file mode 100644 index 00000000..7fe98fbe --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/verbose with typo enters selector.snap @@ -0,0 +1,30 @@ +--- +source: crates/vite_task_bin/tests/e2e_snapshots/main.rs +expression: e2e_outputs +info: + cwd: packages/app +--- +> vp run buid --verbose +@ expect-milestone: task-select:buid:0 +Task "buid" not found. +Search task (↑/↓ to move, enter to select): buid +> build: echo build app + lib#build: echo build lib +@ write-key: enter +Selected task: build +~/packages/app$ echo build app ⊘ cache disabled +build app + + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 0 cache hits • 0 cache misses • 1 cache disabled +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] app#build: ~/packages/app$ echo build app ✓ + → Cache disabled for built-in command +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 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 new file mode 100644 index 00000000..a270a238 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-select/snapshots/verbose without task errors.snap @@ -0,0 +1,8 @@ +--- +source: crates/vite_task_bin/tests/e2e_snapshots/main.rs +expression: e2e_outputs +info: + cwd: packages/app +--- +[1]> vp run --verbose +Error: No task specifier provided for 'run' command diff --git a/crates/vite_task_plan/src/error.rs b/crates/vite_task_plan/src/error.rs index cd5d9e2a..c2dd9090 100644 --- a/crates/vite_task_plan/src/error.rs +++ b/crates/vite_task_plan/src/error.rs @@ -144,8 +144,8 @@ pub enum Error { /// /// At the top level, an empty execution graph triggers the task selector UI. /// In a nested context there is no UI, so this is returned as an error instead. - #[error("no tasks matched the query")] - NoTasksMatched, + #[error("Task \"{0}\" not found")] + NoTasksMatched(Str), /// A cycle was detected in the task dependency graph during planning. /// @@ -155,10 +155,3 @@ pub enum Error { #[error("Cycle dependency detected: {}", _0.iter().map(std::string::ToString::to_string).collect::>().join(" -> "))] CycleDependencyDetected(Vec), } - -impl Error { - #[must_use] - pub const fn is_missing_task_specifier(&self) -> bool { - matches!(self, Self::MissingTaskSpecifier) - } -} diff --git a/crates/vite_task_plan/src/plan.rs b/crates/vite_task_plan/src/plan.rs index 3c8535de..56841017 100644 --- a/crates/vite_task_plan/src/plan.rs +++ b/crates/vite_task_plan/src/plan.rs @@ -177,6 +177,8 @@ async fn plan_task_as_execution_node( let execution_item_kind: ExecutionItemKind = match plan_request { // Expand task query like `vp run -r build` Some(PlanRequest::Query(query_plan_request)) => { + // Save task name before consuming the request + let task_name = query_plan_request.query.task_name.clone(); // Add prefix envs to the context context.add_envs(and_item.envs.iter()); let execution_graph = plan_query_request(query_plan_request, context) @@ -193,7 +195,7 @@ async fn plan_task_as_execution_node( return Err(Error::NestPlan { task_display: task_node.task_display.clone(), command: Str::from(&command_str[add_item_span]), - error: Box::new(Error::NoTasksMatched), + error: Box::new(Error::NoTasksMatched(task_name)), }); } ExecutionItemKind::Expanded(execution_graph) diff --git a/crates/vite_workspace/src/package_filter.rs b/crates/vite_workspace/src/package_filter.rs index a4dc414a..7cc9a41b 100644 --- a/crates/vite_workspace/src/package_filter.rs +++ b/crates/vite_workspace/src/package_filter.rs @@ -214,7 +214,7 @@ pub enum PackageQueryError { /// /// Use `#[clap(flatten)]` to embed these in a parent clap struct. /// Call [`into_package_query`](Self::into_package_query) to convert into an opaque [`PackageQuery`]. -#[derive(Debug, Clone, clap::Args)] +#[derive(Debug, Clone, PartialEq, Eq, clap::Args)] pub struct PackageQueryArgs { /// Select all packages in the workspace. #[clap(default_value = "false", short, long)]