Skip to content

Commit 93bb95d

Browse files
branchseerclaude
andcommitted
feat: show task list when vp run is called without arguments
Instead of a clap usage error, `vp run` now loads the task graph and prints all available tasks grouped by current package vs other packages. When `vp run` appears inside a task script, it falls back to a synthetic plan request so it executes as a normal process. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent caec672 commit 93bb95d

File tree

16 files changed

+276
-19
lines changed

16 files changed

+276
-19
lines changed

crates/vite_task/src/cli/mod.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ pub enum CacheSubcommand {
1515
/// Arguments for the `run` subcommand.
1616
#[derive(Debug, clap::Args)]
1717
pub struct RunCommand {
18-
/// `packageName#taskName` or `taskName`.
19-
pub task_specifier: TaskSpecifier,
18+
/// `packageName#taskName` or `taskName`. If omitted, lists all available tasks.
19+
pub task_specifier: Option<TaskSpecifier>,
2020

2121
/// Run tasks found in all packages in the workspace, in topological order based on package dependencies.
2222
#[clap(default_value = "false", short, long)]
@@ -49,6 +49,9 @@ pub enum Command {
4949

5050
#[derive(thiserror::Error, Debug)]
5151
pub enum CLITaskQueryError {
52+
#[error("no task specifier provided")]
53+
MissingTaskSpecifier,
54+
5255
#[error("--recursive and --transitive cannot be used together")]
5356
RecursiveTransitiveConflict,
5457

@@ -70,6 +73,8 @@ impl RunCommand {
7073
let Self { task_specifier, recursive, transitive, ignore_depends_on, additional_args } =
7174
self;
7275

76+
let task_specifier = task_specifier.ok_or(CLITaskQueryError::MissingTaskSpecifier)?;
77+
7378
let include_explicit_deps = !ignore_depends_on;
7479

7580
let query_kind = if recursive {

crates/vite_task/src/session/mod.rs

Lines changed: 93 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,15 @@ impl vite_task_plan::PlanRequestParser for PlanRequestParser<'_> {
101101
Command::Cache { .. } => Ok(Some(PlanRequest::Synthetic(
102102
command.to_synthetic_plan_request(UserCacheConfig::disabled()),
103103
))),
104-
Command::Run(run_command) => Ok(Some(run_command.into_plan_request(&command.cwd)?)),
104+
Command::Run(run_command) => match run_command.into_plan_request(&command.cwd) {
105+
Ok(plan_request) => Ok(Some(plan_request)),
106+
Err(crate::cli::CLITaskQueryError::MissingTaskSpecifier) => {
107+
Ok(Some(PlanRequest::Synthetic(
108+
command.to_synthetic_plan_request(UserCacheConfig::disabled()),
109+
)))
110+
}
111+
Err(err) => Err(err.into()),
112+
},
105113
},
106114
HandledCommand::Verbatim => Ok(None),
107115
}
@@ -220,13 +228,19 @@ impl<'a> Session<'a> {
220228
Command::Cache { ref subcmd } => self.handle_cache_command(subcmd),
221229
Command::Run(run_command) => {
222230
let cwd = Arc::clone(&self.cwd);
223-
let plan = self.plan_from_cli(cwd, run_command).await?;
224-
let reporter = LabeledReporter::new(std::io::stdout(), self.workspace_path());
225-
Ok(self
226-
.execute(plan, Box::new(reporter))
227-
.await
228-
.err()
229-
.unwrap_or(ExitStatus::SUCCESS))
231+
match self.plan_from_cli(cwd, run_command).await {
232+
Ok(plan) => {
233+
let reporter =
234+
LabeledReporter::new(std::io::stdout(), self.workspace_path());
235+
Ok(self
236+
.execute(plan, Box::new(reporter))
237+
.await
238+
.err()
239+
.unwrap_or(ExitStatus::SUCCESS))
240+
}
241+
Err(err) if err.is_missing_task_specifier() => self.print_task_list().await,
242+
Err(err) => Err(err.into()),
243+
}
230244
}
231245
}
232246
}
@@ -242,6 +256,63 @@ impl<'a> Session<'a> {
242256
}
243257
}
244258

259+
#[expect(
260+
clippy::future_not_send,
261+
reason = "session is single-threaded, futures do not need to be Send"
262+
)]
263+
async fn print_task_list(&mut self) -> anyhow::Result<ExitStatus> {
264+
use std::io::Write;
265+
266+
let cwd = Arc::clone(&self.cwd);
267+
let task_graph = self.ensure_task_graph_loaded().await?;
268+
let mut entries = task_graph.list_tasks();
269+
entries.sort_unstable_by(|a, b| {
270+
a.task_display
271+
.package_name
272+
.cmp(&b.task_display.package_name)
273+
.then_with(|| a.task_display.task_name.cmp(&b.task_display.task_name))
274+
});
275+
276+
// Find the most specific package containing the CWD (longest matching path)
277+
let current_package_path = entries
278+
.iter()
279+
.map(|e| &e.task_display.package_path)
280+
.filter(|p| cwd.as_path().starts_with(p.as_path()))
281+
.max_by_key(|p| p.as_path().as_os_str().len());
282+
283+
let (current, others): (Vec<_>, Vec<_>) = entries
284+
.iter()
285+
.partition(|e| current_package_path == Some(&e.task_display.package_path));
286+
287+
let mut stdout = std::io::stdout().lock();
288+
289+
if !current.is_empty() {
290+
let package_name = &current[0].task_display.package_name;
291+
if package_name.is_empty() {
292+
writeln!(stdout, "Tasks in the current package")?;
293+
} else {
294+
writeln!(stdout, "Tasks in the current package ({package_name})")?;
295+
}
296+
for entry in &current {
297+
writeln!(stdout, " {}", entry.task_display.task_name)?;
298+
writeln!(stdout, " {}", entry.command)?;
299+
}
300+
}
301+
302+
if !others.is_empty() {
303+
if !current.is_empty() {
304+
writeln!(stdout)?;
305+
}
306+
writeln!(stdout, "Tasks in other packages")?;
307+
for entry in &others {
308+
writeln!(stdout, " {}", entry.task_display)?;
309+
writeln!(stdout, " {}", entry.command)?;
310+
}
311+
}
312+
313+
Ok(ExitStatus::SUCCESS)
314+
}
315+
245316
/// Lazily initializes and returns the execution cache.
246317
/// The cache is only created when first accessed to avoid `SQLite` race conditions
247318
/// when multiple processes start simultaneously.
@@ -320,15 +391,21 @@ impl<'a> Session<'a> {
320391
cwd: Arc<AbsolutePath>,
321392
command: RunCommand,
322393
) -> Result<ExecutionPlan, vite_task_plan::Error> {
323-
let plan_request = command.into_plan_request(&cwd).map_err(|error| {
324-
TaskPlanErrorKind::ParsePlanRequestError {
325-
error: error.into(),
326-
program: Str::from("vp"),
327-
args: Arc::default(),
328-
cwd: Arc::clone(&cwd),
394+
let plan_request = match command.into_plan_request(&cwd) {
395+
Ok(plan_request) => plan_request,
396+
Err(crate::cli::CLITaskQueryError::MissingTaskSpecifier) => {
397+
return Err(TaskPlanErrorKind::MissingTaskSpecifier.with_empty_call_stack());
398+
}
399+
Err(error) => {
400+
return Err(TaskPlanErrorKind::ParsePlanRequestError {
401+
error: error.into(),
402+
program: Str::from("vp"),
403+
args: Arc::default(),
404+
cwd: Arc::clone(&cwd),
405+
}
406+
.with_empty_call_stack());
329407
}
330-
.with_empty_call_stack()
331-
})?;
408+
};
332409
let plan = ExecutionPlan::plan(
333410
plan_request,
334411
&self.workspace_path,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"name": "task-list-test",
3+
"private": true
4+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "app",
3+
"scripts": {
4+
"build": "echo build app",
5+
"test": "echo test app"
6+
},
7+
"dependencies": {
8+
"lib": "workspace:*"
9+
}
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"tasks": {
3+
"build": {},
4+
"test": {},
5+
"lint": {
6+
"command": "echo lint app"
7+
}
8+
}
9+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"name": "lib",
3+
"scripts": {
4+
"build": "echo build lib"
5+
}
6+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"tasks": {
3+
"build": {}
4+
}
5+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
packages:
2+
- packages/*
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[[e2e]]
2+
name = "list tasks from package dir"
3+
cwd = "packages/app"
4+
steps = [
5+
"vp run",
6+
]
7+
8+
[[e2e]]
9+
name = "list tasks from workspace root"
10+
steps = [
11+
"vp run",
12+
]
13+
14+
[[e2e]]
15+
name = "vp run in script"
16+
steps = [
17+
"vp run list-tasks",
18+
]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
source: crates/vite_task_bin/tests/e2e_snapshots/main.rs
3+
expression: e2e_outputs
4+
input_file: crates/vite_task_bin/tests/e2e_snapshots/fixtures/task-list
5+
---
6+
> vp run
7+
Tasks in the current package (app)
8+
build
9+
echo build app
10+
lint
11+
echo lint app
12+
test
13+
echo test app
14+
15+
Tasks in other packages
16+
lib#build
17+
echo build lib
18+
task-list-test#hello
19+
echo hello from root
20+
task-list-test#list-tasks
21+
vp run

0 commit comments

Comments
 (0)