diff --git a/Cargo.lock b/Cargo.lock index 510eb88ee6..0ecbde48eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7209,6 +7209,7 @@ dependencies = [ "base64-simd", "chrono", "clap", + "crossterm", "flate2", "junction", "node-semver", diff --git a/crates/vite_global_cli/Cargo.toml b/crates/vite_global_cli/Cargo.toml index 80b2f46a7f..82ab505e84 100644 --- a/crates/vite_global_cli/Cargo.toml +++ b/crates/vite_global_cli/Cargo.toml @@ -26,6 +26,7 @@ tokio = { workspace = true, features = ["full"] } tracing = { workspace = true } owo-colors = { workspace = true } oxc_resolver = { workspace = true } +crossterm = { workspace = true } vite_error = { workspace = true } vite_install = { workspace = true } vite_js_runtime = { workspace = true } diff --git a/crates/vite_global_cli/src/command_picker.rs b/crates/vite_global_cli/src/command_picker.rs new file mode 100644 index 0000000000..a2ab2b082f --- /dev/null +++ b/crates/vite_global_cli/src/command_picker.rs @@ -0,0 +1,520 @@ +//! Interactive top-level command picker for `vp`. + +use std::{ + io::{self, IsTerminal, Write}, + ops::ControlFlow, +}; + +use crossterm::{ + cursor, + event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, + execute, + style::{Attribute, Print, ResetColor, SetAttribute, SetForegroundColor}, + terminal::{self, ClearType}, +}; + +const NEWLINE: &str = "\r\n"; +const SELECTED_COLOR: crossterm::style::Color = crossterm::style::Color::Blue; +const SELECTED_MARKER: &str = "›"; +const UNSELECTED_MARKER: &str = " "; +const HELP_LABEL_NOTE: &str = " (view all commands)"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PickedCommand { + pub command: &'static str, + pub append_help: bool, +} + +#[derive(Clone, Copy)] +struct CommandEntry { + label: &'static str, + command: &'static str, + summary: &'static str, + append_help: bool, +} + +const COMMANDS: &[CommandEntry] = &[ + CommandEntry { + label: "create", + command: "create", + summary: "Create a new project from a template.", + append_help: false, + }, + CommandEntry { + label: "migrate", + command: "migrate", + summary: "Migrate an existing project to Vite+.", + append_help: false, + }, + CommandEntry { + label: "dev", + command: "dev", + summary: "Run the development server.", + append_help: false, + }, + CommandEntry { + label: "check", + command: "check", + summary: "Run format, lint, and type checks.", + append_help: false, + }, + CommandEntry { label: "test", command: "test", summary: "Run tests.", append_help: false }, + CommandEntry { + label: "install", + command: "install", + summary: "Install dependencies, or add packages when names are provided.", + append_help: false, + }, + CommandEntry { label: "run", command: "run", summary: "Run tasks.", append_help: false }, + CommandEntry { + label: "build", + command: "build", + summary: "Build for production.", + append_help: false, + }, + CommandEntry { label: "pack", command: "pack", summary: "Build library.", append_help: false }, + CommandEntry { + label: "preview", + command: "preview", + summary: "Preview production build.", + append_help: false, + }, + CommandEntry { + label: "outdated", + command: "outdated", + summary: "Check for outdated packages.", + append_help: false, + }, + CommandEntry { + label: "env", + command: "env", + summary: "Manage Node.js versions.", + append_help: false, + }, + CommandEntry { + label: "help (view all commands)", + command: "help", + summary: "Show the full command list and help details.", + append_help: false, + }, +]; + +const CI_ENV_VARS: &[&str] = &[ + "CI", + "CONTINUOUS_INTEGRATION", + "GITHUB_ACTIONS", + "GITLAB_CI", + "CIRCLECI", + "TRAVIS", + "JENKINS_URL", + "BUILDKITE", + "DRONE", + "CODEBUILD_BUILD_ID", + "TF_BUILD", +]; + +pub fn pick_top_level_command_if_interactive() -> io::Result> { + if !should_enable_picker() { + return Ok(None); + } + + run_picker() +} + +fn should_enable_picker() -> bool { + std::io::stdin().is_terminal() + && std::io::stdout().is_terminal() + && std::env::var("TERM").map_or(true, |term| term != "dumb") + && !is_ci_environment() +} + +fn is_ci_environment() -> bool { + CI_ENV_VARS.iter().any(|key| std::env::var_os(key).is_some()) +} + +fn run_picker() -> io::Result> { + let mut stdout = io::stdout(); + let mut selected_position = 0usize; + let mut viewport_start = 0usize; + let mut query = String::new(); + + terminal::enable_raw_mode()?; + execute!(stdout, cursor::Hide)?; + + let pick_result = loop { + let filtered_indices = filtered_command_indices(&query); + if filtered_indices.is_empty() { + selected_position = 0; + viewport_start = 0; + } else { + if selected_position >= filtered_indices.len() { + selected_position = 0; + } + viewport_start = viewport_start.min(filtered_indices.len().saturating_sub(1)); + } + + let (_, rows) = terminal::size().unwrap_or((80, 24)); + let rows = if rows == 0 { 24 } else { rows }; + let viewport_size = compute_viewport_size(rows.into(), filtered_indices.len()); + viewport_start = align_viewport(viewport_start, selected_position, viewport_size); + render_picker( + &mut stdout, + &query, + &filtered_indices, + selected_position, + viewport_start, + viewport_size, + )?; + + if let Event::Key(KeyEvent { code, modifiers, .. }) = event::read()? { + match handle_key_event( + code, + modifiers, + &mut query, + &mut selected_position, + filtered_indices.len(), + ) { + ControlFlow::Continue(()) => continue, + ControlFlow::Break(Some(())) => { + let Some(index) = filtered_indices.get(selected_position).copied() else { + continue; + }; + break Ok(Some(PickedCommand { + command: COMMANDS[index].command, + append_help: COMMANDS[index].append_help, + })); + } + ControlFlow::Break(None) => break Ok(None), + } + } + }; + + let cleanup_result = cleanup_picker(&mut stdout); + match (pick_result, cleanup_result) { + (Ok(picked), Ok(())) => Ok(picked), + (Err(err), _) => Err(err), + (Ok(_), Err(err)) => Err(err), + } +} + +fn cleanup_picker(stdout: &mut io::Stdout) -> io::Result<()> { + terminal::disable_raw_mode()?; + execute!( + stdout, + cursor::Show, + terminal::Clear(ClearType::All), + cursor::MoveTo(0, 0), + ResetColor + )?; + Ok(()) +} + +fn handle_key_event( + code: KeyCode, + modifiers: KeyModifiers, + query: &mut String, + selected_position: &mut usize, + filtered_len: usize, +) -> ControlFlow> { + match code { + KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) => ControlFlow::Break(None), + KeyCode::Esc => ControlFlow::Break(None), + KeyCode::Backspace => { + if !query.is_empty() { + query.pop(); + *selected_position = 0; + } + ControlFlow::Continue(()) + } + KeyCode::Up => { + *selected_position = selected_position.saturating_sub(1); + ControlFlow::Continue(()) + } + KeyCode::Down => { + if *selected_position + 1 < filtered_len { + *selected_position += 1; + } + ControlFlow::Continue(()) + } + KeyCode::Home => { + *selected_position = 0; + ControlFlow::Continue(()) + } + KeyCode::End => { + *selected_position = filtered_len.saturating_sub(1); + ControlFlow::Continue(()) + } + KeyCode::Enter => { + if filtered_len == 0 { + ControlFlow::Continue(()) + } else { + ControlFlow::Break(Some(())) + } + } + KeyCode::Char(ch) if modifiers.is_empty() || modifiers == KeyModifiers::SHIFT => { + if !ch.is_control() { + query.push(ch); + *selected_position = 0; + } + ControlFlow::Continue(()) + } + _ => ControlFlow::Continue(()), + } +} + +fn render_picker( + stdout: &mut io::Stdout, + query: &str, + filtered_indices: &[usize], + selected_position: usize, + viewport_start: usize, + viewport_size: usize, +) -> io::Result<()> { + let (columns, _) = terminal::size().unwrap_or((80, 24)); + let columns = if columns == 0 { 80 } else { columns }; + let max_width = usize::from(columns).saturating_sub(4); + let viewport_end = (viewport_start + viewport_size).min(filtered_indices.len()); + let instruction = truncate_line( + &format!("Select a command (↑/↓, Enter to run, Esc to cancel): {query}"), + max_width, + ); + + execute!( + stdout, + cursor::MoveTo(0, 0), + terminal::Clear(ClearType::All), + Print(vite_shared::header::vite_plus_header()), + Print(NEWLINE), + Print(NEWLINE), + Print(instruction), + Print(NEWLINE), + Print(NEWLINE) + )?; + + if viewport_start > 0 { + execute!( + stdout, + SetForegroundColor(crossterm::style::Color::DarkGrey), + Print(" ↑ more"), + Print(NEWLINE), + ResetColor + )?; + } + + for (index, command_index) in filtered_indices[viewport_start..viewport_end].iter().enumerate() + { + let actual_position = viewport_start + index; + let is_selected = actual_position == selected_position; + let entry = &COMMANDS[*command_index]; + let marker = if is_selected { SELECTED_MARKER } else { UNSELECTED_MARKER }; + let label = truncate_line(entry.label, max_width); + let (label_main, label_note) = if entry.command == "help" { + if let Some(main) = label.strip_suffix(HELP_LABEL_NOTE) { + (main, Some(HELP_LABEL_NOTE)) + } else { + (label.as_str(), None) + } + } else { + (label.as_str(), None) + }; + + if is_selected { + execute!(stdout, SetForegroundColor(SELECTED_COLOR), SetAttribute(Attribute::Bold),)?; + execute!(stdout, Print(format!(" {marker} {label_main}")))?; + execute!(stdout, SetAttribute(Attribute::Reset), ResetColor)?; + if let Some(note) = label_note { + execute!( + stdout, + SetForegroundColor(crossterm::style::Color::DarkGrey), + Print(note), + ResetColor + )?; + } + execute!(stdout, Print(NEWLINE))?; + } else { + execute!( + stdout, + SetForegroundColor(crossterm::style::Color::DarkGrey), + Print(format!(" {marker} ")), + ResetColor, + Print(label_main), + )?; + if let Some(note) = label_note { + execute!( + stdout, + SetForegroundColor(crossterm::style::Color::DarkGrey), + Print(note), + ResetColor + )?; + } + execute!(stdout, Print(NEWLINE))?; + } + } + + if viewport_end < filtered_indices.len() { + execute!( + stdout, + SetForegroundColor(crossterm::style::Color::DarkGrey), + Print(" ↓ more"), + Print(NEWLINE), + ResetColor + )?; + } + + if let Some(command_index) = filtered_indices.get(selected_position).copied() { + let summary = truncate_line(COMMANDS[command_index].summary, max_width); + execute!( + stdout, + Print(NEWLINE), + SetForegroundColor(crossterm::style::Color::DarkGrey), + Print(" "), + Print(summary), + Print(NEWLINE), + ResetColor + )?; + } else { + let no_match = if query.is_empty() { + "No common commands available. Run `vp help`.".to_string() + } else { + format!("No common command matches '{query}'. Run `vp help`.") + }; + let no_match = truncate_line(&no_match, max_width); + execute!( + stdout, + Print(NEWLINE), + SetForegroundColor(crossterm::style::Color::DarkGrey), + Print(" "), + Print(no_match), + Print(NEWLINE), + ResetColor + )?; + } + + stdout.flush() +} + +fn compute_viewport_size(terminal_rows: usize, total_commands: usize) -> usize { + // Header + instructions + query + spacing + summary takes ~10 rows. + terminal_rows.saturating_sub(10).clamp(6, total_commands.max(6)) +} + +fn align_viewport(current_start: usize, selected_index: usize, viewport_size: usize) -> usize { + if selected_index < current_start { + selected_index + } else if selected_index >= current_start + viewport_size { + selected_index + 1 - viewport_size + } else { + current_start + } +} + +fn truncate_line(line: &str, max_chars: usize) -> String { + if max_chars == 0 { + return String::new(); + } + + let char_count = line.chars().count(); + if char_count <= max_chars { + return line.to_string(); + } + + if max_chars == 1 { + return "…".to_string(); + } + + line.chars().take(max_chars - 1).collect::() + "…" +} + +fn filtered_command_indices(query: &str) -> Vec { + let query = query.trim(); + if query.is_empty() { + return (0..COMMANDS.len()).collect(); + } + + let query = query.to_ascii_lowercase(); + let starts_with_matches = COMMANDS + .iter() + .enumerate() + .filter_map(|(index, command)| { + let command_name = command.command.to_ascii_lowercase(); + command_name.starts_with(&query).then_some(index) + }) + .collect::>(); + + if !starts_with_matches.is_empty() { + return starts_with_matches; + } + + COMMANDS + .iter() + .enumerate() + .filter_map(|(index, command)| { + let command_name = command.command.to_ascii_lowercase(); + command_name.contains(&query).then_some(index) + }) + .collect::>() +} + +#[cfg(test)] +mod tests { + use super::{COMMANDS, align_viewport, compute_viewport_size, filtered_command_indices}; + + #[test] + fn commands_are_unique() { + let mut names = COMMANDS.iter().map(|command| command.command).collect::>(); + names.sort_unstable(); + names.dedup(); + assert_eq!(names.len(), COMMANDS.len()); + } + + #[test] + fn commands_with_required_args_default_to_help() { + let expected: [&str; 0] = []; + let mut actual = COMMANDS + .iter() + .filter(|command| command.append_help) + .map(|command| command.command) + .collect::>(); + actual.sort_unstable(); + assert_eq!(actual, expected); + } + + #[test] + fn viewport_aligns_to_selected_row() { + assert_eq!(align_viewport(0, 0, 8), 0); + assert_eq!(align_viewport(0, 6, 8), 0); + assert_eq!(align_viewport(0, 8, 8), 1); + assert_eq!(align_viewport(5, 2, 8), 2); + } + + #[test] + fn viewport_size_is_clamped() { + assert_eq!(compute_viewport_size(12, 30), 6); + assert_eq!(compute_viewport_size(24, 30), 14); + assert_eq!(compute_viewport_size(100, 8), 8); + } + + #[test] + fn filtering_is_case_insensitive_and_returns_matching_commands_only() { + let run = filtered_command_indices("Ru"); + assert_eq!(run.len(), 1); + assert_eq!(COMMANDS[run[0]].command, "run"); + + let build = filtered_command_indices("b"); + let build_commands = build.iter().map(|index| COMMANDS[*index].command).collect::>(); + assert!(build_commands.contains(&"build")); + } + + #[test] + fn filtering_with_no_matches_returns_empty() { + let no_match = filtered_command_indices("xyz123"); + assert!(no_match.is_empty()); + } + + #[test] + fn filtering_prefers_prefix_matches() { + let help = filtered_command_indices("he"); + assert_eq!(help.len(), 1); + assert_eq!(COMMANDS[help[0]].command, "help"); + } +} diff --git a/crates/vite_global_cli/src/commands/env/doctor.rs b/crates/vite_global_cli/src/commands/env/doctor.rs index c537123cce..610a277922 100644 --- a/crates/vite_global_cli/src/commands/env/doctor.rs +++ b/crates/vite_global_cli/src/commands/env/doctor.rs @@ -217,7 +217,7 @@ async fn check_shim_mode() { print_check( &output::CHECK.green().to_string(), "Shim mode", - &"system-first".cyan().to_string(), + &"system-first".bright_blue().to_string(), ); // Check if system Node.js is available diff --git a/crates/vite_global_cli/src/main.rs b/crates/vite_global_cli/src/main.rs index fa69e36e34..dd4d976bed 100644 --- a/crates/vite_global_cli/src/main.rs +++ b/crates/vite_global_cli/src/main.rs @@ -8,6 +8,7 @@ #![allow(clippy::print_stderr)] mod cli; +mod command_picker; mod commands; mod error; mod help; @@ -227,7 +228,7 @@ async fn main() -> ExitCode { vite_shared::init_tracing(); // Check for shim mode (invoked as node, npm, or npx) - let args: Vec = std::env::args().collect(); + let mut args: Vec = std::env::args().collect(); let argv0 = args.first().map(|s| s.as_str()).unwrap_or("vp"); tracing::debug!("argv0: {argv0}"); @@ -246,6 +247,21 @@ async fn main() -> ExitCode { } }; + if args.len() == 1 { + match command_picker::pick_top_level_command_if_interactive() { + Ok(Some(selection)) => { + args.push(selection.command.to_string()); + if selection.append_help { + args.push("--help".to_string()); + } + } + Ok(None) => {} + Err(err) => { + tracing::debug!("Failed to run top-level command picker: {err}"); + } + } + } + let mut tip_context = tips::TipContext { // Capture user args (excluding argv0) before normalization raw_args: args[1..].to_vec(), diff --git a/crates/vite_install/src/package_manager.rs b/crates/vite_install/src/package_manager.rs index 256e5b935e..851c3ae007 100644 --- a/crates/vite_install/src/package_manager.rs +++ b/crates/vite_install/src/package_manager.rs @@ -597,7 +597,7 @@ fn interactive_package_manager_menu() -> Result { // Highlight selected item execute!( io::stdout(), - SetForegroundColor(Color::Cyan), + SetForegroundColor(Color::Blue), Print("▶ "), Print(format!("[{}] ", i + 1)), Print(name), diff --git a/crates/vite_js_runtime/src/download.rs b/crates/vite_js_runtime/src/download.rs index 3424ec71ca..6cd4955011 100644 --- a/crates/vite_js_runtime/src/download.rs +++ b/crates/vite_js_runtime/src/download.rs @@ -61,7 +61,7 @@ pub async fn download_file( pb.set_style( ProgressStyle::default_bar() .template( - "{msg}\n{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] \ + "{msg}\n{spinner:.green} [{elapsed_precise}] [{bar:40.blue/white}] \ {bytes}/{total_bytes} ({bytes_per_sec}, {eta})", ) .expect("valid progress bar template") diff --git a/packages/cli/install.ps1 b/packages/cli/install.ps1 index 8415d5207d..13fc1d82ef 100644 --- a/packages/cli/install.ps1 +++ b/packages/cli/install.ps1 @@ -231,7 +231,7 @@ function Setup-NodeManager { function Main { Write-Host "" Write-Host "Setting up " -NoNewline - Write-Host "VITE+" -ForegroundColor Cyan -NoNewline + Write-Host "VITE+" -ForegroundColor Blue -NoNewline Write-Host "..." # Suppress progress bars for cleaner output diff --git a/packages/cli/src/utils/terminal.ts b/packages/cli/src/utils/terminal.ts index a05bcbac33..e9e0a1016c 100644 --- a/packages/cli/src/utils/terminal.ts +++ b/packages/cli/src/utils/terminal.ts @@ -6,7 +6,7 @@ export function log(message: string) { } export function accent(text: string) { - return styleText('blueBright', text); + return styleText('blue', text); } export function muted(text: string) { @@ -26,7 +26,7 @@ export function error(text: string) { export function infoMsg(msg: string) { /* oxlint-disable-next-line no-console */ - console.log(styleText(['blueBright', 'bold'], 'info:'), msg); + console.log(styleText(['blue', 'bold'], 'info:'), msg); } export function warnMsg(msg: string) { diff --git a/packages/prompts/src/autocomplete.ts b/packages/prompts/src/autocomplete.ts index 05027a24ba..46ac01b447 100644 --- a/packages/prompts/src/autocomplete.ts +++ b/packages/prompts/src/autocomplete.ts @@ -124,7 +124,7 @@ export const autocomplete = (opts: AutocompleteOptions) => { } default: { - const barColor = this.state === 'error' ? color.yellow : color.blueBright; + const barColor = this.state === 'error' ? color.yellow : color.blue; const guidePrefix = hasGuide ? `${barColor(S_BAR)} ` : ''; const guidePrefixEnd = hasGuide ? barColor(S_BAR_END) : ''; // Display cursor position - show plain text in navigation mode @@ -298,7 +298,7 @@ export const autocompleteMultiselect = (opts: AutocompleteMultiSelectOpti return `${title}${color.gray(S_BAR)} ${color.strikethrough(color.dim(userInput))}`; } default: { - const barColor = this.state === 'error' ? color.yellow : color.blueBright; + const barColor = this.state === 'error' ? color.yellow : color.blue; // Instructions const instructions = [ `${color.dim('↑/↓')} to navigate`, diff --git a/packages/prompts/src/common.ts b/packages/prompts/src/common.ts index 6fde84d4b8..91591b7075 100644 --- a/packages/prompts/src/common.ts +++ b/packages/prompts/src/common.ts @@ -44,7 +44,7 @@ export const symbol = (state: State) => { switch (state) { case 'initial': case 'active': - return color.blueBright(S_STEP_ACTIVE); + return color.blue(S_STEP_ACTIVE); case 'cancel': return color.red(S_STEP_CANCEL); case 'error': @@ -58,7 +58,7 @@ export const symbolBar = (state: State) => { switch (state) { case 'initial': case 'active': - return color.blueBright(S_BAR); + return color.blue(S_BAR); case 'cancel': return color.red(S_BAR); case 'error': diff --git a/packages/prompts/src/confirm.ts b/packages/prompts/src/confirm.ts index 97b4595a85..934c3476e3 100644 --- a/packages/prompts/src/confirm.ts +++ b/packages/prompts/src/confirm.ts @@ -44,13 +44,13 @@ export const confirm = (opts: ConfirmOptions) => { )}${hasGuide ? `\n${color.gray(S_BAR)}` : ''}`; } default: { - const defaultPrefix = hasGuide ? `${color.blueBright(S_BAR)} ` : ''; - const defaultPrefixEnd = hasGuide ? color.blueBright(S_BAR_END) : ''; + const defaultPrefix = hasGuide ? `${color.blue(S_BAR)} ` : ''; + const defaultPrefixEnd = hasGuide ? color.blue(S_BAR_END) : ''; return `${title}${defaultPrefix}${ this.value ? `${color.green(S_RADIO_ACTIVE)} ${active}` : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(active)}` - }${opts.vertical ? (hasGuide ? `\n${color.blueBright(S_BAR)} ` : '\n') : ` ${color.dim('/')} `}${ + }${opts.vertical ? (hasGuide ? `\n${color.blue(S_BAR)} ` : '\n') : ` ${color.dim('/')} `}${ !this.value ? `${color.green(S_RADIO_ACTIVE)} ${inactive}` : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(inactive)}` diff --git a/packages/prompts/src/group-multi-select.ts b/packages/prompts/src/group-multi-select.ts index 193757d5fd..e7d296b827 100644 --- a/packages/prompts/src/group-multi-select.ts +++ b/packages/prompts/src/group-multi-select.ts @@ -43,17 +43,17 @@ export const groupMultiselect = (opts: GroupMultiSelectOptions) => const prefix = isItem ? (selectableGroups ? `${isLast ? S_BAR_END : S_BAR} ` : ' ') : ''; let spacingPrefix = ''; if (groupSpacing > 0 && !isItem) { - const spacingPrefixText = `\n${color.blueBright(S_BAR)}`; + const spacingPrefixText = `\n${color.blue(S_BAR)}`; spacingPrefix = `${spacingPrefixText.repeat(groupSpacing - 1)}${spacingPrefixText} `; } if (state === 'active') { - return `${spacingPrefix}${color.dim(prefix)}${color.blueBright(S_CHECKBOX_ACTIVE)} ${label}${ + return `${spacingPrefix}${color.dim(prefix)}${color.blue(S_CHECKBOX_ACTIVE)} ${label}${ option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' }`; } if (state === 'group-active') { - return `${spacingPrefix}${prefix}${color.blueBright(S_CHECKBOX_ACTIVE)} ${color.dim(label)}`; + return `${spacingPrefix}${prefix}${color.blue(S_CHECKBOX_ACTIVE)} ${color.dim(label)}`; } if (state === 'group-active-selected') { return `${spacingPrefix}${prefix}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; @@ -180,9 +180,9 @@ export const groupMultiselect = (opts: GroupMultiSelectOptions) => const prefix = i !== 0 && !optionText.startsWith('\n') ? ' ' : ''; return `${prefix}${optionText}`; }) - .join(`\n${color.blueBright(S_BAR)}`); + .join(`\n${color.blue(S_BAR)}`); const optionsPrefix = optionsText.startsWith('\n') ? '' : ' '; - return `${title}${color.blueBright(S_BAR)}${optionsPrefix}${optionsText}\n${color.blueBright(S_BAR_END)}\n`; + return `${title}${color.blue(S_BAR)}${optionsPrefix}${optionsText}\n${color.blue(S_BAR_END)}\n`; } } }, diff --git a/packages/prompts/src/multi-select.ts b/packages/prompts/src/multi-select.ts index cec7c7048d..ffa2be174d 100644 --- a/packages/prompts/src/multi-select.ts +++ b/packages/prompts/src/multi-select.ts @@ -48,7 +48,7 @@ export const multiselect = (opts: MultiSelectOptions) => { }`; } if (state === 'active') { - return `${color.blueBright(S_CHECKBOX_ACTIVE)} ${label}${ + return `${color.blue(S_CHECKBOX_ACTIVE)} ${label}${ option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' }`; } @@ -162,7 +162,7 @@ export const multiselect = (opts: MultiSelectOptions) => { }).join(`\n${prefix}`)}\n${footer}\n`; } default: { - const prefix = `${color.blueBright(S_BAR)} `; + const prefix = `${color.blue(S_BAR)} `; // Calculate rowPadding: title lines + footer lines (S_BAR_END + trailing newline) const titleLineCount = title.split('\n').length; const footerLineCount = 2; // S_BAR_END + trailing newline @@ -174,7 +174,7 @@ export const multiselect = (opts: MultiSelectOptions) => { columnPadding: prefix.length, rowPadding: titleLineCount + footerLineCount, style: styleOption, - }).join(`\n${prefix}`)}\n${color.blueBright(S_BAR_END)}\n`; + }).join(`\n${prefix}`)}\n${color.blue(S_BAR_END)}\n`; } } }, diff --git a/packages/prompts/src/password.ts b/packages/prompts/src/password.ts index 8fe26d7c9c..1178c91288 100644 --- a/packages/prompts/src/password.ts +++ b/packages/prompts/src/password.ts @@ -45,8 +45,8 @@ export const password = (opts: PasswordOptions) => { }`; } default: { - const defaultPrefix = hasGuide ? `${color.blueBright(S_BAR)} ` : ''; - const defaultPrefixEnd = hasGuide ? color.blueBright(S_BAR_END) : ''; + const defaultPrefix = hasGuide ? `${color.blue(S_BAR)} ` : ''; + const defaultPrefixEnd = hasGuide ? color.blue(S_BAR_END) : ''; return `${title}${defaultPrefix}${userInput}\n${defaultPrefixEnd}\n`; } } diff --git a/packages/prompts/src/select-key.ts b/packages/prompts/src/select-key.ts index 9988ced11e..30b23dc010 100644 --- a/packages/prompts/src/select-key.ts +++ b/packages/prompts/src/select-key.ts @@ -24,7 +24,7 @@ export const selectKey = (opts: SelectKeyOptions) = return color.strikethrough(color.dim(label)); } if (state === 'active') { - return `${color.bgCyan(color.gray(` ${option.value} `))} ${label}${ + return `${color.bgBlue(color.white(` ${option.value} `))} ${label}${ option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' }`; } @@ -66,8 +66,8 @@ export const selectKey = (opts: SelectKeyOptions) = return `${title}${wrapped}${hasGuide ? `\n${color.gray(S_BAR)}` : ''}`; } default: { - const defaultPrefix = hasGuide ? `${color.blueBright(S_BAR)} ` : ''; - const defaultPrefixEnd = hasGuide ? color.blueBright(S_BAR_END) : ''; + const defaultPrefix = hasGuide ? `${color.blue(S_BAR)} ` : ''; + const defaultPrefixEnd = hasGuide ? color.blue(S_BAR_END) : ''; const wrapped = this.options .map((option, i) => wrapTextWithPrefix( diff --git a/packages/prompts/src/select.ts b/packages/prompts/src/select.ts index 0751c02899..4badc8f70b 100644 --- a/packages/prompts/src/select.ts +++ b/packages/prompts/src/select.ts @@ -145,8 +145,8 @@ export const select = (opts: SelectOptions) => { return `${title}${wrappedLines}${hasGuide ? `\n${color.gray(S_BAR)}` : ''}`; } default: { - const prefix = hasGuide ? `${color.blueBright(S_BAR)} ` : ''; - const prefixEnd = hasGuide ? color.blueBright(S_BAR_END) : ''; + const prefix = hasGuide ? `${color.blue(S_BAR)} ` : ''; + const prefixEnd = hasGuide ? color.blue(S_BAR_END) : ''; // Calculate rowPadding: title lines + footer lines (S_BAR_END + trailing newline) const titleLineCount = title.split('\n').length; const footerLineCount = hasGuide ? 2 : 1; // S_BAR_END + trailing newline (or just trailing newline) diff --git a/packages/prompts/src/text.ts b/packages/prompts/src/text.ts index d54feed40b..5aa21394cf 100644 --- a/packages/prompts/src/text.ts +++ b/packages/prompts/src/text.ts @@ -48,8 +48,8 @@ export const text = (opts: TextOptions) => { return `${title}${cancelPrefix}${valueText}${value.trim() ? `\n${cancelPrefix}` : ''}`; } default: { - const defaultPrefix = hasGuide ? `${color.blueBright(S_BAR)} ` : ''; - const defaultPrefixEnd = hasGuide ? color.blueBright(S_BAR_END) : ''; + const defaultPrefix = hasGuide ? `${color.blue(S_BAR)} ` : ''; + const defaultPrefixEnd = hasGuide ? color.blue(S_BAR_END) : ''; return `${title}${defaultPrefix}${userInput}\n${defaultPrefixEnd}\n`; } } diff --git a/packages/test/build.ts b/packages/test/build.ts index 37586fe908..3affd138d9 100644 --- a/packages/test/build.ts +++ b/packages/test/build.ts @@ -541,7 +541,7 @@ async function brandVitest() { // Use a blue badge for both DEV and RUN. patchString( 'banner color', - /const color = this\.ctx\.config\.watch \? "blue" : "cyan";\n\t\tconst mode = this\.ctx\.config\.watch \? "DEV" : "RUN";/, + /const color = this\.ctx\.config\.watch \? "blue" : "[a-z]+";\n\t\tconst mode = this\.ctx\.config\.watch \? "DEV" : "RUN";/, 'const mode = this.ctx.config.watch ? "DEV" : "RUN";\n\t\tconst label = c.bold(c.inverse(c.blue(` ${mode} `)));', ); diff --git a/packages/tools/src/brand-rolldown-vite.ts b/packages/tools/src/brand-rolldown-vite.ts index 09a49d5769..282adec08d 100644 --- a/packages/tools/src/brand-rolldown-vite.ts +++ b/packages/tools/src/brand-rolldown-vite.ts @@ -52,10 +52,22 @@ function replaceInFile( ); } -function removeAnyInFile(filePath: string, searches: string[]): 'patched' | 'already' { +function removeAnyInFile( + filePath: string, + searches: Array, +): 'patched' | 'already' { const content = readFileSync(filePath, 'utf-8'); for (const search of searches) { - if (content.includes(search)) { + if (typeof search === 'string') { + if (content.includes(search)) { + const newContent = content.replace(search, ''); + writeFileSync(filePath, newContent, 'utf-8'); + return 'patched'; + } + continue; + } + + if (search.test(content)) { const newContent = content.replace(search, ''); writeFileSync(filePath, newContent, 'utf-8'); return 'patched'; @@ -120,8 +132,8 @@ export function brandRolldownVite(rootDir: string = process.cwd()) { const buildFile = join(nodeDir, 'build.ts'); const buildResults = [ removeAnyInFile(buildFile, [ - ' logger.info(\n colors.cyan(\n `vite v${VERSION} ${colors.green(\n `building ${environment.name} environment for ${environment.config.mode}...`,\n )}`,\n ),\n )\n', - ' logger.info(\n colors.cyan(\n `vite+ v${VITE_PLUS_VERSION} ${colors.green(\n `building ${environment.name} environment for ${environment.config.mode}...`,\n )}`,\n ),\n )\n', + / {4}logger\.info\(\n {6}colors\.[a-zA-Z]+\(\n {8}`vite v\$\{VERSION\} \$\{colors\.green\(\n {10}`building \$\{environment\.name\} environment for \$\{environment\.config\.mode\}\.\.\.`,\n {8}\)\}`,\n {6}\),\n {4}\)\n/, + / {4}logger\.info\(\n {6}colors\.[a-zA-Z]+\(\n {8}`vite\+ v\$\{VITE_PLUS_VERSION\} \$\{colors\.green\(\n {10}`building \$\{environment\.name\} environment for \$\{environment\.config\.mode\}\.\.\.`,\n {8}\)\}`,\n {6}\),\n {4}\)\n/, ]), replaceInFile(buildFile, '`[vite]: Rolldown failed', '`[vite+]: Rolldown failed'), ]; diff --git a/rfcs/cli-output-polish.md b/rfcs/cli-output-polish.md index d2634f816e..0614e1c96c 100644 --- a/rfcs/cli-output-polish.md +++ b/rfcs/cli-output-polish.md @@ -161,7 +161,7 @@ This is clean: the rolldown-vite source change is minimal (reads an env var with ```javascript logger.info( - colors.cyan( + colors.blue( `vite v${VERSION} ${colors.green( `building ${environment.name} environment for ${environment.config.mode}...`, )}`, @@ -175,7 +175,7 @@ logger.info( ```javascript logger.info( - colors.cyan( + colors.blue( `vite+ v${VITE_PLUS_VERSION} ${colors.green( `building ${environment.name} environment for ${environment.config.mode}...`, )}`, @@ -236,7 +236,7 @@ pub const ARROW: &str = "\u{2192}"; // → — transitions /// Print an info message to stderr. pub fn info(msg: &str) { - eprintln!("{} {}", "info:".cyan().bold(), msg); + eprintln!("{} {}", "info:".bright_blue().bold(), msg); } /// Print a warning message to stderr. @@ -343,7 +343,7 @@ The JS code in `packages/cli/src/utils/terminal.ts` already has `accent()`, `hea ```typescript export function info(msg: string) { - console.error(styleText(['cyan', 'bold'], 'info:'), msg); + console.error(styleText(['blue', 'bold'], 'info:'), msg); } export function warn(msg: string) { diff --git a/rfcs/env-command.md b/rfcs/env-command.md index ead0dcb0b2..07c1f67f01 100644 --- a/rfcs/env-command.md +++ b/rfcs/env-command.md @@ -1806,7 +1806,7 @@ $ vp env list * v22.13.0 current ``` -- Current version line is highlighted in cyan +- Current version line is highlighted in blue - `current` and `default` markers are shown in dimmed text ### Flags