diff --git a/.cargo/config.toml b/.cargo/config.toml index 6e74eb940d..6660ac95d4 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -4,10 +4,17 @@ WORKSPACE_DIR = { value = "rolldown", relative = true } [build] rustflags = ["--cfg", "tokio_unstable"] # also update .github/workflows/ci.yml + # fix sqlite build error on linux [target.'cfg(target_os = "linux")'] rustflags = ["--cfg", "tokio_unstable", "-C", "link-args=-Wl,--warn-unresolved-symbols"] +# Increase stack size on Windows to avoid stack overflow +[target.'cfg(all(windows, target_env = "msvc"))'] +rustflags = ["--cfg", "tokio_unstable", "-C", "link-arg=/STACK:8388608"] +[target.'cfg(all(windows, target_env = "gnu"))'] +rustflags = ["--cfg", "tokio_unstable", "-C", "link-arg=-Wl,--stack,8388608"] + [unstable] bindeps = true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2940514dfc..c7a2067714 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,6 +109,8 @@ jobs: # Test all crates/* packages. New crates are automatically included. # Also test vite-plus-cli (lives outside crates/) to catch type sync issues. - run: cargo test $(for d in crates/*/; do echo -n "-p $(basename $d) "; done) -p vite-plus-cli + env: + RUST_MIN_STACK: 8388608 lint: needs: detect-changes @@ -399,6 +401,8 @@ jobs: git diff exit 1 fi + env: + RUST_MIN_STACK: 8388608 # Upgrade tests (merged from separate job to avoid duplicate build) - name: Test upgrade (bash) diff --git a/Cargo.lock b/Cargo.lock index 9c3b9e37ef..337c91fcc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -859,6 +859,15 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_complete" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19c9f1dde76b736e3681f28cec9d5a61299cbaae0fce80a68e43724ad56031eb" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.5.55" @@ -7260,6 +7269,7 @@ dependencies = [ "base64-simd", "chrono", "clap", + "clap_complete", "crossterm", "directories", "flate2", diff --git a/Cargo.toml b/Cargo.toml index 48ce30a032..73282a0db2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,6 +71,7 @@ brush-parser = "0.3.0" blake3 = "1.8.2" chrono = { version = "0.4", features = ["serde"] } clap = "4.5.40" +clap_complete = "4.6.0" commondir = "1.0.0" cow-utils = "0.1.3" criterion = { version = "0.7", features = ["html_reports"] } diff --git a/crates/vite_global_cli/Cargo.toml b/crates/vite_global_cli/Cargo.toml index b3699f9989..fade553225 100644 --- a/crates/vite_global_cli/Cargo.toml +++ b/crates/vite_global_cli/Cargo.toml @@ -15,6 +15,7 @@ path = "src/main.rs" base64-simd = { workspace = true } chrono = { workspace = true } clap = { workspace = true, features = ["derive"] } +clap_complete = { workspace = true } directories = { workspace = true } flate2 = { workspace = true } serde = { workspace = true } diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index 29c784e55c..b4d1fab58c 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -15,10 +15,11 @@ use std::process::ExitStatus; +use clap::CommandFactory; use owo_colors::OwoColorize; use super::config::{get_bin_dir, get_vite_plus_home}; -use crate::{error::Error, help}; +use crate::{cli::Args, error::Error, help}; /// Tools to create shims for (node, npm, npx, vpx) const SHIM_TOOLS: &[&str] = &["node", "npm", "npx", "vpx"]; @@ -38,6 +39,9 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result // Ensure home directory exists (env files are written here) tokio::fs::create_dir_all(&vite_plus_home).await?; + // Generate completion scripts + generate_completion_scripts(&vite_plus_home).await?; + // Create env files with PATH guard (prevents duplicate PATH entries) create_env_files(&vite_plus_home).await?; @@ -283,6 +287,39 @@ exec "$VITE_PLUS_HOME/current/bin/vp.exe" env exec {} -- "$@" Ok(()) } +/// Creates completion scripts in `~/.vite-plus/completion/`: +/// - `vp.bash` (bash) +/// - `_vp` (zsh, following zsh convention) +/// - `vp.fish` (fish shell) +/// - `vp.ps1` (PowerShell) +async fn generate_completion_scripts( + vite_plus_home: &vite_path::AbsolutePath, +) -> Result<(), Error> { + let mut cmd = Args::command(); + + // Create completion directory + let completion_dir = vite_plus_home.join("completion"); + tokio::fs::create_dir_all(&completion_dir).await?; + + // Generate shell completion scripts + let completions = [ + (clap_complete::Shell::Bash, "vp.bash"), + (clap_complete::Shell::Zsh, "_vp"), + (clap_complete::Shell::Fish, "vp.fish"), + (clap_complete::Shell::PowerShell, "vp.ps1"), + ]; + + for (shell, filename) in completions { + let path = completion_dir.join(filename); + let mut file = std::fs::File::create(&path)?; + clap_complete::generate(shell, &mut cmd, "vp", &mut file); + } + + tracing::debug!("Generated completion scripts in {:?}", completion_dir); + + Ok(()) +} + /// Create env files with PATH guard (prevents duplicate PATH entries). /// /// Creates: @@ -292,23 +329,28 @@ exec "$VITE_PLUS_HOME/current/bin/vp.exe" env exec {} -- "$@" /// - `~/.vite-plus/bin/vp-use.cmd` (cmd.exe wrapper for `vp env use`) async fn create_env_files(vite_plus_home: &vite_path::AbsolutePath) -> Result<(), Error> { let bin_path = vite_plus_home.join("bin"); + let completion_path = vite_plus_home.join("completion"); // Use $HOME-relative path if install dir is under HOME (like rustup's ~/.cargo/env) // This makes the env file portable across sessions where HOME may differ - let bin_path_ref = if let Some(home_dir) = vite_shared::EnvConfig::get().user_home { - if let Ok(suffix) = bin_path.as_path().strip_prefix(&home_dir) { - format!("$HOME/{}", suffix.display()) - } else { - bin_path.as_path().display().to_string() - } - } else { - bin_path.as_path().display().to_string() + let home_dir = vite_shared::EnvConfig::get().user_home; + let to_ref = |path: &vite_path::AbsolutePath| -> String { + home_dir + .as_ref() + .and_then(|h| path.as_path().strip_prefix(h).ok()) + .map(|s| { + // Normalize to forward slashes for $HOME/... paths (POSIX-style) + format!("$HOME/{}", s.display().to_string().replace('\\', "/")) + }) + .unwrap_or_else(|| path.as_path().display().to_string()) }; + let bin_path_ref = to_ref(&bin_path); // POSIX env file (bash/zsh) // When sourced multiple times, removes existing entry and re-prepends to front // Uses parameter expansion to split PATH around the bin entry in O(1) operations // Includes vp() shell function wrapper for `vp env use` (evals stdout) + // Includes shell completion support let env_content = r#"#!/bin/sh # Vite+ environment setup (https://viteplus.dev) __vp_bin="__VP_BIN__" @@ -339,8 +381,29 @@ vp() { command vp "$@" fi } + +# Shell completion for bash/zsh +# Source appropriate completion script based on current shell +# Only load completion in interactive shells with required builtins +if [ -n "$BASH_VERSION" ] && type complete >/dev/null 2>&1; then + # Bash shell with completion support + __vp_completion="__VP_COMPLETION_BASH__" + if [ -f "$__vp_completion" ]; then + . "$__vp_completion" + fi + unset __vp_completion +elif [ -n "$ZSH_VERSION" ] && type compdef >/dev/null 2>&1; then + # Zsh shell with completion support + __vp_completion="__VP_COMPLETION_ZSH__" + if [ -f "$__vp_completion" ]; then + . "$__vp_completion" + fi + unset __vp_completion +fi "# - .replace("__VP_BIN__", &bin_path_ref); + .replace("__VP_BIN__", &bin_path_ref) + .replace("__VP_COMPLETION_BASH__", &to_ref(&completion_path.join("vp.bash"))) + .replace("__VP_COMPLETION_ZSH__", &to_ref(&completion_path.join("_vp"))); let env_file = vite_plus_home.join("env"); tokio::fs::write(&env_file, env_content).await?; @@ -364,8 +427,15 @@ function vp command vp $argv end end + +# Shell completion for fish +set -l __vp_completion "__VP_COMPLETION_FISH__" +if test -f "$__vp_completion" + source "$__vp_completion" +end "# - .replace("__VP_BIN__", &bin_path_ref); + .replace("__VP_BIN__", &bin_path_ref) + .replace("__VP_COMPLETION_FISH__", &to_ref(&completion_path.join("vp.fish"))); let env_fish_file = vite_plus_home.join("env.fish"); tokio::fs::write(&env_fish_file, env_fish_content).await?; @@ -399,11 +469,20 @@ function vp { & (Join-Path $__vp_bin "vp.exe") @args } } + +# Shell completion for PowerShell +$__vp_completion = "__VP_COMPLETION_PS1__" +if (Test-Path $__vp_completion) { + . $__vp_completion +} "#; // For PowerShell, use the actual absolute path (not $HOME-relative) let bin_path_win = bin_path.as_path().display().to_string(); - let env_ps1_content = env_ps1_content.replace("__VP_BIN_WIN__", &bin_path_win); + let completion_ps1_win = completion_path.join("vp.ps1").as_path().display().to_string(); + let env_ps1_content = env_ps1_content + .replace("__VP_BIN_WIN__", &bin_path_win) + .replace("__VP_COMPLETION_PS1__", &completion_ps1_win); let env_ps1_file = vite_plus_home.join("env.ps1"); tokio::fs::write(&env_ps1_file, env_ps1_content).await?; @@ -734,4 +813,76 @@ mod tests { assert!(fresh_home.join("env.fish").exists(), "env.fish file should be created"); assert!(fresh_home.join("env.ps1").exists(), "env.ps1 file should be created"); } + + #[tokio::test] + async fn test_generate_completion_scripts_creates_all_files() { + let temp_dir = TempDir::new().unwrap(); + let home = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + + generate_completion_scripts(&home).await.unwrap(); + + let completion_dir = home.join("completion"); + + // Verify all completion scripts are created + let bash_completion = completion_dir.join("vp.bash"); + let zsh_completion = completion_dir.join("_vp"); + let fish_completion = completion_dir.join("vp.fish"); + let ps1_completion = completion_dir.join("vp.ps1"); + + assert!(bash_completion.as_path().exists(), "bash completion (vp.bash) should be created"); + assert!(zsh_completion.as_path().exists(), "zsh completion (_vp) should be created"); + assert!(fish_completion.as_path().exists(), "fish completion (vp.fish) should be created"); + assert!( + ps1_completion.as_path().exists(), + "PowerShell completion (vp.ps1) should be created" + ); + } + + #[tokio::test] + async fn test_create_env_files_contains_completion() { + let temp_dir = TempDir::new().unwrap(); + let home = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let _guard = home_guard(temp_dir.path()); + + create_env_files(&home).await.unwrap(); + + let env_content = tokio::fs::read_to_string(home.join("env")).await.unwrap(); + let fish_content = tokio::fs::read_to_string(home.join("env.fish")).await.unwrap(); + let ps1_content = tokio::fs::read_to_string(home.join("env.ps1")).await.unwrap(); + + assert!( + env_content.contains("Shell completion") + && env_content.contains("/completion/vp.bash\""), + "env file should contain bash completion" + ); + assert!( + fish_content.contains("Shell completion") + && fish_content.contains("/completion/vp.fish\""), + "env.fish file should contain fish completion" + ); + assert!( + ps1_content.contains("Shell completion") + && ps1_content.contains(&format!( + "{}completion{}vp.ps1\"", + std::path::MAIN_SEPARATOR_STR, + std::path::MAIN_SEPARATOR_STR + )), + "env.ps1 file should contain PowerShell completion" + ); + + // Verify placeholders are replaced + assert!( + !env_content.contains("__VP_COMPLETION_BASH__") + && !env_content.contains("__VP_COMPLETION_ZSH__"), + "env file should not contain __VP_COMPLETION_* placeholders" + ); + assert!( + !fish_content.contains("__VP_COMPLETION_FISH__"), + "env.fish file should not contain __VP_COMPLETION_FISH__ placeholder" + ); + assert!( + !ps1_content.contains("__VP_COMPLETION_PS1__"), + "env.ps1 file should not contain __VP_COMPLETION_PS1__ placeholder" + ); + } }