Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 82 additions & 9 deletions crates/vite_task/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub enum CacheSubcommand {
/// Extracted as a separate struct so they can be cheaply `Copy`-ed
/// before `RunCommand` is consumed.
#[derive(Debug, Clone, Copy, clap::Args)]
#[expect(clippy::struct_excessive_bools, reason = "CLI flags are naturally boolean")]
pub struct RunFlags {
/// Run tasks found in all packages in the workspace, in topological order based on package dependencies.
#[clap(default_value = "false", short, long)]
Expand All @@ -29,34 +30,106 @@ pub struct RunFlags {
/// Do not run dependencies specified in `dependsOn` fields.
#[clap(default_value = "false", long)]
pub ignore_depends_on: bool,

/// Show full detailed summary after execution.
#[clap(default_value = "false", short = 'v', long)]
pub verbose: bool,
}

/// Arguments for the `run` subcommand.
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Clap-parsed types (used only at the parsing boundary)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

/// Arguments for the `run` subcommand as parsed by clap.
///
/// Contains the `--last-details` flag which is resolved into a separate
/// [`Command::RunLastDetails`] variant via [`ParsedCommand::into_command`].
#[derive(Debug, clap::Args)]
pub struct RunCommand {
pub struct ParsedRunCommand {
/// `packageName#taskName` or `taskName`. If omitted, lists all available tasks.
pub task_specifier: Option<TaskSpecifier>,
task_specifier: Option<TaskSpecifier>,

#[clap(flatten)]
pub flags: RunFlags,
flags: RunFlags,

/// Additional arguments to pass to the tasks
#[clap(trailing_var_arg = true, allow_hyphen_values = true)]
pub additional_args: Vec<Str>,
additional_args: Vec<Str>,

/// Display the detailed summary of the last run.
#[clap(long, exclusive = true)]
last_details: bool,
}

/// vite task CLI subcommands
/// vite task CLI subcommands as parsed by clap.
///
/// Use [`ParsedCommand::into_command`] to resolve into the dispatched [`Command`]
/// enum, which makes `--last-details` exclusive at the type level.
#[derive(Debug, Parser)]
pub enum Command {
pub enum ParsedCommand {
/// Run tasks
Run(RunCommand),
Run(ParsedRunCommand),
/// Manage the task cache
Cache {
#[clap(subcommand)]
subcmd: CacheSubcommand,
},
}

impl ParsedCommand {
/// Resolve the clap-parsed command into the dispatched [`Command`] enum.
///
/// When `--last-details` is set on the `run` subcommand, this produces
/// [`Command::RunLastDetails`] instead of [`Command::Run`], making the
/// exclusivity enforced at the type level.
#[must_use]
pub fn into_command(self) -> Command {
match self {
Self::Run(run) if run.last_details => Command::RunLastDetails,
Self::Run(run) => Command::Run(RunCommand {
task_specifier: run.task_specifier,
flags: run.flags,
additional_args: run.additional_args,
}),
Self::Cache { subcmd } => Command::Cache { subcmd },
}
}
}

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Resolved types (used for dispatch — `--last-details` is a separate variant)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

/// Resolved CLI command for dispatch.
///
/// Unlike [`ParsedCommand`], this enum makes `--last-details` a separate variant
/// ([`Command::RunLastDetails`]) so that it is exclusive at the type level —
/// there is no way to combine it with task execution fields.
#[derive(Debug)]
pub enum Command {
/// Run tasks with the given parameters.
Run(RunCommand),
/// Display the saved detailed summary of the last run (`--last-details`).
RunLastDetails,
/// Manage the task cache.
Cache { subcmd: CacheSubcommand },
}

/// Resolved arguments for executing tasks.
///
/// Does not contain `last_details` — that case is represented by
/// [`Command::RunLastDetails`] instead.
#[derive(Debug)]
pub struct RunCommand {
/// `packageName#taskName` or `taskName`. If omitted, lists all available tasks.
pub task_specifier: Option<TaskSpecifier>,

pub flags: RunFlags,

/// Additional arguments to pass to the tasks.
pub additional_args: Vec<Str>,
}

#[derive(thiserror::Error, Debug)]
pub enum CLITaskQueryError {
#[error("no task specifier provided")]
Expand All @@ -82,7 +155,7 @@ impl RunCommand {
) -> Result<QueryPlanRequest, CLITaskQueryError> {
let Self {
task_specifier,
flags: RunFlags { recursive, transitive, ignore_depends_on },
flags: RunFlags { recursive, transitive, ignore_depends_on, .. },
additional_args,
} = self;

Expand Down
2 changes: 1 addition & 1 deletion crates/vite_task/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod maybe_str;
pub mod session;

// Public exports for vite_task_bin
pub use cli::{CacheSubcommand, Command, RunCommand, RunFlags};
pub use cli::{CacheSubcommand, Command, ParsedCommand, RunCommand, RunFlags};
pub use session::{CommandHandler, ExitStatus, HandledCommand, Session, SessionCallbacks};
pub use vite_task_graph::{
config::{
Expand Down
136 changes: 40 additions & 96 deletions crates/vite_task/src/session/cache/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
//! Coloring is handled by the reporter to respect `NO_COLOR` environment variable.

use rustc_hash::FxHashSet;
use serde::{Deserialize, Serialize};
use vite_str::Str;
use vite_task_plan::cache_metadata::SpawnFingerprint;

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

/// Describes a single atomic change between two spawn fingerprints
enum SpawnFingerprintChange {
/// Describes a single atomic change between two spawn fingerprints.
///
/// Used both for live cache status display and for persisted summary data.
#[derive(Serialize, Deserialize)]
pub enum SpawnFingerprintChange {
// Environment variable changes
/// Environment variable added
EnvAdded { key: Str, value: Str },
Expand Down Expand Up @@ -43,8 +47,40 @@ enum SpawnFingerprintChange {
FingerprintIgnoreRemoved { pattern: Str },
}

/// Compare two spawn fingerprints and return all changes
fn detect_spawn_fingerprint_changes(
/// Format a single spawn fingerprint change as human-readable text.
///
/// Used by both the live cache status display and the persisted summary rendering.
pub fn format_spawn_change(change: &SpawnFingerprintChange) -> Str {
match change {
SpawnFingerprintChange::EnvAdded { key, value } => {
vite_str::format!("env {key}={value} added")
}
SpawnFingerprintChange::EnvRemoved { key, value } => {
vite_str::format!("env {key}={value} removed")
}
SpawnFingerprintChange::EnvValueChanged { key, old_value, new_value } => {
vite_str::format!("env {key} value changed from '{old_value}' to '{new_value}'")
}
SpawnFingerprintChange::PassThroughEnvAdded { name } => {
vite_str::format!("pass-through env '{name}' added")
}
SpawnFingerprintChange::PassThroughEnvRemoved { name } => {
vite_str::format!("pass-through env '{name}' removed")
}
SpawnFingerprintChange::ProgramChanged => Str::from("program changed"),
SpawnFingerprintChange::ArgsChanged => Str::from("args changed"),
SpawnFingerprintChange::CwdChanged => Str::from("working directory changed"),
SpawnFingerprintChange::FingerprintIgnoreAdded { pattern } => {
vite_str::format!("fingerprint ignore '{pattern}' added")
}
SpawnFingerprintChange::FingerprintIgnoreRemoved { pattern } => {
vite_str::format!("fingerprint ignore '{pattern}' removed")
}
}
}

/// Compare two spawn fingerprints and return all changes.
pub fn detect_spawn_fingerprint_changes(
old: &SpawnFingerprint,
new: &SpawnFingerprint,
) -> Vec<SpawnFingerprintChange> {
Expand Down Expand Up @@ -190,95 +226,3 @@ pub fn format_cache_status_inline(cache_status: &CacheStatus) -> Option<Str> {
}
}
}

/// Format cache status for summary display (post-execution).
///
/// Returns a formatted string showing detailed cache information.
/// - Cache Hit: Shows saved time
/// - Cache Miss (NotFound): Indicates first-time execution
/// - Cache Miss (with mismatch): Shows specific reason with details
/// - Cache Disabled: Shows user-friendly reason message
///
/// Note: Returns plain text without styling. The reporter applies colors.
pub fn format_cache_status_summary(cache_status: &CacheStatus) -> Str {
match cache_status {
CacheStatus::Hit { replayed_duration } => {
// Show saved time for cache hits
vite_str::format!("→ Cache hit - output replayed - {replayed_duration:.2?} saved")
}
CacheStatus::Miss(CacheMiss::NotFound) => {
// First time running this task - no previous cache entry
Str::from("→ Cache miss: no previous cache entry found")
}
CacheStatus::Miss(CacheMiss::FingerprintMismatch(mismatch)) => {
// Show specific reason why cache was invalidated
match mismatch {
FingerprintMismatch::SpawnFingerprintMismatch { old, new } => {
let changes = detect_spawn_fingerprint_changes(old, new);
let formatted: Vec<Str> = changes
.iter()
.map(|c| match c {
SpawnFingerprintChange::EnvAdded { key, value } => {
vite_str::format!("env {key}={value} added")
}
SpawnFingerprintChange::EnvRemoved { key, value } => {
vite_str::format!("env {key}={value} removed")
}
SpawnFingerprintChange::EnvValueChanged {
key,
old_value,
new_value,
} => {
vite_str::format!(
"env {key} value changed from '{old_value}' to '{new_value}'"
)
}
SpawnFingerprintChange::PassThroughEnvAdded { name } => {
vite_str::format!("pass-through env '{name}' added")
}
SpawnFingerprintChange::PassThroughEnvRemoved { name } => {
vite_str::format!("pass-through env '{name}' removed")
}
SpawnFingerprintChange::ProgramChanged => Str::from("program changed"),
SpawnFingerprintChange::ArgsChanged => Str::from("args changed"),
SpawnFingerprintChange::CwdChanged => {
Str::from("working directory changed")
}
SpawnFingerprintChange::FingerprintIgnoreAdded { pattern } => {
vite_str::format!("fingerprint ignore '{pattern}' added")
}
SpawnFingerprintChange::FingerprintIgnoreRemoved { pattern } => {
vite_str::format!("fingerprint ignore '{pattern}' removed")
}
})
.collect();

if formatted.is_empty() {
Str::from("→ Cache miss: configuration changed")
} else {
let joined =
formatted.iter().map(Str::as_str).collect::<Vec<_>>().join("; ");
vite_str::format!("→ Cache miss: {joined}")
}
}
FingerprintMismatch::PostRunFingerprintMismatch(diff) => {
// Post-run mismatch has specific path information
use crate::session::execute::fingerprint::PostRunFingerprintMismatch;
match diff {
PostRunFingerprintMismatch::InputContentChanged { path } => {
vite_str::format!("→ Cache miss: content of input '{path}' changed")
}
}
}
}
}
CacheStatus::Disabled(reason) => {
// Display user-friendly message for each disabled reason
let message = match reason {
CacheDisabledReason::InProcessExecution => "Cache disabled for built-in command",
CacheDisabledReason::NoCacheMetadata => "Cache disabled in task configuration",
};
vite_str::format!("→ {message}")
}
}
}
3 changes: 2 additions & 1 deletion crates/vite_task/src/session/cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ use std::{fmt::Display, fs::File, io::Write, sync::Arc, time::Duration};

use bincode::{Decode, Encode, decode_from_slice, encode_to_vec};
// Re-export display functions for convenience
pub use display::{format_cache_status_inline, format_cache_status_summary};
pub use display::format_cache_status_inline;
pub use display::{SpawnFingerprintChange, detect_spawn_fingerprint_changes, format_spawn_change};
use rusqlite::{Connection, OptionalExtension as _, config::DbConfig};
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
Expand Down
Loading