Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
1 change: 1 addition & 0 deletions crates/vite_global_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
175 changes: 163 additions & 12 deletions crates/vite_global_cli/src/commands/env/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand All @@ -38,6 +39,9 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result<ExitStatus, Error>
// 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?;

Expand Down Expand Up @@ -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:
Expand All @@ -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__"
Expand Down Expand Up @@ -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?;

Expand All @@ -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?;

Expand Down Expand Up @@ -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?;

Expand Down Expand Up @@ -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"
);
}
}
Loading