Skip to content

Commit ddbbb94

Browse files
committed
feat: replace verbose execution summary with compact one-liners
Add --details flag for full summary and --last-details to view saved summary from last run. Normal runs now show a compact one-liner instead of the full summary table: - Single task + no cache hit: no summary - Single task + cache hit: '[vp run] cache hit, {duration} saved.' - Multi-task: '[vp run] {hits}/{total} cache hit ({rate}%)' Summary data is persisted as structured JSON (last-summary.json) in the cache directory using atomic writes. Both live and --last-details rendering share the same code path through LastRunSummary. The TaskResult enum encodes cache status and execution outcome together, making invalid states unrepresentable (e.g. CacheHit+exit_code, InProcess+error). Also fixes a pre-existing flaky test in e2e-lint-cache where oxlint scanned node_modules, causing fspy to fingerprint the cache directory.
1 parent 3d1554b commit ddbbb94

File tree

81 files changed

+1113
-1046
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+1113
-1046
lines changed

crates/vite_task/src/cli/mod.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub enum CacheSubcommand {
1717
/// Extracted as a separate struct so they can be cheaply `Copy`-ed
1818
/// before `RunCommand` is consumed.
1919
#[derive(Debug, Clone, Copy, clap::Args)]
20+
#[expect(clippy::struct_excessive_bools, reason = "CLI flags are naturally boolean")]
2021
pub struct RunFlags {
2122
/// Run tasks found in all packages in the workspace, in topological order based on package dependencies.
2223
#[clap(default_value = "false", short, long)]
@@ -29,6 +30,10 @@ pub struct RunFlags {
2930
/// Do not run dependencies specified in `dependsOn` fields.
3031
#[clap(default_value = "false", long)]
3132
pub ignore_depends_on: bool,
33+
34+
/// Show full detailed summary after execution.
35+
#[clap(default_value = "false", long)]
36+
pub details: bool,
3237
}
3338

3439
/// Arguments for the `run` subcommand.
@@ -43,6 +48,10 @@ pub struct RunCommand {
4348
/// Additional arguments to pass to the tasks
4449
#[clap(trailing_var_arg = true, allow_hyphen_values = true)]
4550
pub additional_args: Vec<Str>,
51+
52+
/// Display the detailed summary of the last run.
53+
#[clap(long, exclusive = true)]
54+
pub last_details: bool,
4655
}
4756

4857
/// vite task CLI subcommands
@@ -82,8 +91,9 @@ impl RunCommand {
8291
) -> Result<QueryPlanRequest, CLITaskQueryError> {
8392
let Self {
8493
task_specifier,
85-
flags: RunFlags { recursive, transitive, ignore_depends_on },
94+
flags: RunFlags { recursive, transitive, ignore_depends_on, .. },
8695
additional_args,
96+
..
8797
} = self;
8898

8999
let task_specifier = task_specifier.ok_or(CLITaskQueryError::MissingTaskSpecifier)?;

crates/vite_task/src/session/cache/display.rs

Lines changed: 40 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@
44
//! Coloring is handled by the reporter to respect `NO_COLOR` environment variable.
55
66
use rustc_hash::FxHashSet;
7+
use serde::{Deserialize, Serialize};
78
use vite_str::Str;
89
use vite_task_plan::cache_metadata::SpawnFingerprint;
910

1011
use super::{CacheMiss, FingerprintMismatch};
1112
use crate::session::event::{CacheDisabledReason, CacheStatus};
1213

13-
/// Describes a single atomic change between two spawn fingerprints
14-
enum SpawnFingerprintChange {
14+
/// Describes a single atomic change between two spawn fingerprints.
15+
///
16+
/// Used both for live cache status display and for persisted summary data.
17+
#[derive(Serialize, Deserialize)]
18+
pub enum SpawnFingerprintChange {
1519
// Environment variable changes
1620
/// Environment variable added
1721
EnvAdded { key: Str, value: Str },
@@ -43,8 +47,40 @@ enum SpawnFingerprintChange {
4347
FingerprintIgnoreRemoved { pattern: Str },
4448
}
4549

46-
/// Compare two spawn fingerprints and return all changes
47-
fn detect_spawn_fingerprint_changes(
50+
/// Format a single spawn fingerprint change as human-readable text.
51+
///
52+
/// Used by both the live cache status display and the persisted summary rendering.
53+
pub fn format_spawn_change(change: &SpawnFingerprintChange) -> Str {
54+
match change {
55+
SpawnFingerprintChange::EnvAdded { key, value } => {
56+
vite_str::format!("env {key}={value} added")
57+
}
58+
SpawnFingerprintChange::EnvRemoved { key, value } => {
59+
vite_str::format!("env {key}={value} removed")
60+
}
61+
SpawnFingerprintChange::EnvValueChanged { key, old_value, new_value } => {
62+
vite_str::format!("env {key} value changed from '{old_value}' to '{new_value}'")
63+
}
64+
SpawnFingerprintChange::PassThroughEnvAdded { name } => {
65+
vite_str::format!("pass-through env '{name}' added")
66+
}
67+
SpawnFingerprintChange::PassThroughEnvRemoved { name } => {
68+
vite_str::format!("pass-through env '{name}' removed")
69+
}
70+
SpawnFingerprintChange::ProgramChanged => Str::from("program changed"),
71+
SpawnFingerprintChange::ArgsChanged => Str::from("args changed"),
72+
SpawnFingerprintChange::CwdChanged => Str::from("working directory changed"),
73+
SpawnFingerprintChange::FingerprintIgnoreAdded { pattern } => {
74+
vite_str::format!("fingerprint ignore '{pattern}' added")
75+
}
76+
SpawnFingerprintChange::FingerprintIgnoreRemoved { pattern } => {
77+
vite_str::format!("fingerprint ignore '{pattern}' removed")
78+
}
79+
}
80+
}
81+
82+
/// Compare two spawn fingerprints and return all changes.
83+
pub fn detect_spawn_fingerprint_changes(
4884
old: &SpawnFingerprint,
4985
new: &SpawnFingerprint,
5086
) -> Vec<SpawnFingerprintChange> {
@@ -190,95 +226,3 @@ pub fn format_cache_status_inline(cache_status: &CacheStatus) -> Option<Str> {
190226
}
191227
}
192228
}
193-
194-
/// Format cache status for summary display (post-execution).
195-
///
196-
/// Returns a formatted string showing detailed cache information.
197-
/// - Cache Hit: Shows saved time
198-
/// - Cache Miss (NotFound): Indicates first-time execution
199-
/// - Cache Miss (with mismatch): Shows specific reason with details
200-
/// - Cache Disabled: Shows user-friendly reason message
201-
///
202-
/// Note: Returns plain text without styling. The reporter applies colors.
203-
pub fn format_cache_status_summary(cache_status: &CacheStatus) -> Str {
204-
match cache_status {
205-
CacheStatus::Hit { replayed_duration } => {
206-
// Show saved time for cache hits
207-
vite_str::format!("→ Cache hit - output replayed - {replayed_duration:.2?} saved")
208-
}
209-
CacheStatus::Miss(CacheMiss::NotFound) => {
210-
// First time running this task - no previous cache entry
211-
Str::from("→ Cache miss: no previous cache entry found")
212-
}
213-
CacheStatus::Miss(CacheMiss::FingerprintMismatch(mismatch)) => {
214-
// Show specific reason why cache was invalidated
215-
match mismatch {
216-
FingerprintMismatch::SpawnFingerprintMismatch { old, new } => {
217-
let changes = detect_spawn_fingerprint_changes(old, new);
218-
let formatted: Vec<Str> = changes
219-
.iter()
220-
.map(|c| match c {
221-
SpawnFingerprintChange::EnvAdded { key, value } => {
222-
vite_str::format!("env {key}={value} added")
223-
}
224-
SpawnFingerprintChange::EnvRemoved { key, value } => {
225-
vite_str::format!("env {key}={value} removed")
226-
}
227-
SpawnFingerprintChange::EnvValueChanged {
228-
key,
229-
old_value,
230-
new_value,
231-
} => {
232-
vite_str::format!(
233-
"env {key} value changed from '{old_value}' to '{new_value}'"
234-
)
235-
}
236-
SpawnFingerprintChange::PassThroughEnvAdded { name } => {
237-
vite_str::format!("pass-through env '{name}' added")
238-
}
239-
SpawnFingerprintChange::PassThroughEnvRemoved { name } => {
240-
vite_str::format!("pass-through env '{name}' removed")
241-
}
242-
SpawnFingerprintChange::ProgramChanged => Str::from("program changed"),
243-
SpawnFingerprintChange::ArgsChanged => Str::from("args changed"),
244-
SpawnFingerprintChange::CwdChanged => {
245-
Str::from("working directory changed")
246-
}
247-
SpawnFingerprintChange::FingerprintIgnoreAdded { pattern } => {
248-
vite_str::format!("fingerprint ignore '{pattern}' added")
249-
}
250-
SpawnFingerprintChange::FingerprintIgnoreRemoved { pattern } => {
251-
vite_str::format!("fingerprint ignore '{pattern}' removed")
252-
}
253-
})
254-
.collect();
255-
256-
if formatted.is_empty() {
257-
Str::from("→ Cache miss: configuration changed")
258-
} else {
259-
let joined =
260-
formatted.iter().map(Str::as_str).collect::<Vec<_>>().join("; ");
261-
vite_str::format!("→ Cache miss: {joined}")
262-
}
263-
}
264-
FingerprintMismatch::PostRunFingerprintMismatch(diff) => {
265-
// Post-run mismatch has specific path information
266-
use crate::session::execute::fingerprint::PostRunFingerprintMismatch;
267-
match diff {
268-
PostRunFingerprintMismatch::InputContentChanged { path } => {
269-
vite_str::format!("→ Cache miss: content of input '{path}' changed")
270-
}
271-
}
272-
}
273-
}
274-
}
275-
CacheStatus::Disabled(reason) => {
276-
// Display user-friendly message for each disabled reason
277-
let message = match reason {
278-
CacheDisabledReason::InProcessExecution => "Cache disabled for built-in command",
279-
CacheDisabledReason::NoCacheMetadata => "Cache disabled in task configuration",
280-
};
281-
vite_str::format!("→ {message}")
282-
}
283-
}
284-
}

crates/vite_task/src/session/cache/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use std::{fmt::Display, fs::File, io::Write, sync::Arc, time::Duration};
66

77
use bincode::{Decode, Encode, decode_from_slice, encode_to_vec};
88
// Re-export display functions for convenience
9-
pub use display::{format_cache_status_inline, format_cache_status_summary};
9+
pub use display::format_cache_status_inline;
10+
pub use display::{SpawnFingerprintChange, detect_spawn_fingerprint_changes, format_spawn_change};
1011
use rusqlite::{Connection, OptionalExtension as _, config::DbConfig};
1112
use serde::{Deserialize, Serialize};
1213
use tokio::sync::Mutex;

crates/vite_task/src/session/mod.rs

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ use cache::ExecutionCache;
1010
pub use cache::{CacheMiss, FingerprintMismatch};
1111
use once_cell::sync::OnceCell;
1212
pub use reporter::ExitStatus;
13-
use reporter::LabeledReporterBuilder;
13+
use reporter::{
14+
LabeledReporterBuilder,
15+
summary::{LastRunSummary, ReadSummaryError, format_full_summary},
16+
};
1417
use rustc_hash::FxHashMap;
1518
use vite_path::{AbsolutePath, AbsolutePathBuf};
1619
use vite_select::SelectItem;
@@ -225,12 +228,18 @@ impl<'a> Session<'a> {
225228
match command {
226229
Command::Cache { ref subcmd } => self.handle_cache_command(subcmd),
227230
Command::Run(run_command) => {
231+
// --last-details: display saved summary and exit (exclusive flag)
232+
if run_command.last_details {
233+
return self.show_last_run_details();
234+
}
235+
228236
let cwd = Arc::clone(&self.cwd);
229237
let is_interactive =
230238
std::io::stdin().is_terminal() && std::io::stdout().is_terminal();
231239

232240
// Save task name and flags before consuming run_command
233241
let task_name = run_command.task_specifier.as_ref().map(|s| s.task_name.clone());
242+
let show_details = run_command.flags.details;
234243
let flags = run_command.flags;
235244
let additional_args = run_command.additional_args.clone();
236245

@@ -249,6 +258,8 @@ impl<'a> Session<'a> {
249258
let builder = LabeledReporterBuilder::new(
250259
self.workspace_path(),
251260
Box::new(tokio::io::stdout()),
261+
show_details,
262+
Some(self.summary_file_path()),
252263
);
253264
Ok(self
254265
.execute_graph(graph, Box::new(builder))
@@ -389,13 +400,22 @@ impl<'a> Session<'a> {
389400
// Interactive: run the selected task
390401
let selected_label = &select_items[selected_index].label;
391402
let task_specifier = TaskSpecifier::parse_raw(selected_label);
392-
let run_command =
393-
RunCommand { task_specifier: Some(task_specifier), flags, additional_args };
403+
let show_details = flags.details;
404+
let run_command = RunCommand {
405+
task_specifier: Some(task_specifier),
406+
flags,
407+
additional_args,
408+
last_details: false,
409+
};
394410

395411
let cwd = Arc::clone(&self.cwd);
396412
let graph = self.plan_from_cli_run(cwd, run_command).await?;
397-
let builder =
398-
LabeledReporterBuilder::new(self.workspace_path(), Box::new(tokio::io::stdout()));
413+
let builder = LabeledReporterBuilder::new(
414+
self.workspace_path(),
415+
Box::new(tokio::io::stdout()),
416+
show_details,
417+
Some(self.summary_file_path()),
418+
);
399419
Ok(self.execute_graph(graph, Box::new(builder)).await.err().unwrap_or(ExitStatus::SUCCESS))
400420
}
401421

@@ -414,6 +434,44 @@ impl<'a> Session<'a> {
414434
Arc::clone(&self.workspace_path)
415435
}
416436

437+
/// Path to the `last-summary.json` file inside the cache directory.
438+
fn summary_file_path(&self) -> AbsolutePathBuf {
439+
self.cache_path.join("last-summary.json")
440+
}
441+
442+
/// Display the saved summary from the last run (`--last-details`).
443+
#[expect(
444+
clippy::print_stderr,
445+
reason = "--last-details error messages are user-facing diagnostics, not debug output"
446+
)]
447+
fn show_last_run_details(&self) -> anyhow::Result<ExitStatus> {
448+
let path = self.summary_file_path();
449+
match LastRunSummary::read_from_path(&path) {
450+
Ok(Some(summary)) => {
451+
let buf = format_full_summary(&summary);
452+
{
453+
use std::io::Write;
454+
let mut stdout = std::io::stdout().lock();
455+
let _ = stdout.write_all(&buf);
456+
let _ = stdout.flush();
457+
}
458+
Ok(ExitStatus(summary.exit_code))
459+
}
460+
Ok(None) => {
461+
eprintln!("No previous run summary found. Run a task first to generate a summary.");
462+
Ok(ExitStatus::FAILURE)
463+
}
464+
Err(ReadSummaryError::IncompatibleVersion) => {
465+
eprintln!(
466+
"Summary data was saved by a different version of vite-task and cannot be read. \
467+
Run a task to generate a new summary."
468+
);
469+
Ok(ExitStatus::FAILURE)
470+
}
471+
Err(ReadSummaryError::Io(err)) => Err(err.into()),
472+
}
473+
}
474+
417475
pub const fn task_graph(&self) -> Option<&TaskGraph> {
418476
match &self.lazy_task_graph {
419477
LazyTaskGraph::Initialized(graph) => Some(graph.task_graph()),

0 commit comments

Comments
 (0)