Skip to content
Draft
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
1 change: 1 addition & 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 @@ -251,6 +251,7 @@ rolldown-notify = "10.2.0"
rolldown-notify-debouncer-full = "0.7.5"
rustc-hash = "2.1.1"
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
same-file = "1.0.6"
schemars = "1.0.0"
self_cell = "1.2.0"
node-semver = "2.2.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"vite-plus/latest": {
"version": "999.0.0",
"dist": {
"tarball": "unused",
"integrity": "sha512-unused"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,17 @@ skip-platforms = ["windows", { os = "linux", libc = "musl" }]
steps = [
{ argv = ["vp", "upgrade", "--check", "--tag", "alpha"], comment = "alpha tag avoids release-day flake (dev version equals npm latest right after a release, hiding the Update-available branch)", continue-on-failure = true },
]

[[case]]
name = "command_upgrade_background_notice"
vp = "global"
local-registry = true
unset-env = ["VP_CLI_TEST"]
comment = "A background check records an available update without contaminating machine output, then the foreground CLI shows the generic notice at most once per prompt interval."
steps = [
{ argv = ["vp", "upgrade", "--background-check"], snapshot = false },
{ argv = ["vpt", "grep-file", "$VP_HOME/cache/upgrade-check.json", '"status":"available"'], snapshot = false },
{ argv = ["vp", "env", "list", "--json"], comment = "Machine-readable output does not consume the pending notice.", snapshot = false },
{ argv = ["vp", "env", "off"], comment = "The next interactive command displays the cached update notice." },
{ argv = ["vp", "env", "off"], comment = "A subsequent command stays quiet after the notice timestamp is recorded." },
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# command_upgrade_background_notice

A background check records an available update without contaminating machine output, then the foreground CLI shows the generic notice at most once per prompt interval.

## `vp upgrade --background-check`


## `vpt grep-file $VP_HOME/cache/upgrade-check.json '"status":"available"'`


## `vp env list --json`

Machine-readable output does not consume the pending notice.


## `vp env off`

The next interactive command displays the cached update notice.

```
VITE+ - The Unified Toolchain for the Web

✓ Node.js management set to system-first.

All vp commands and shims will now prefer system Node.js, falling back to managed if not found.

Run `vp env on` to always use Vite+ managed Node.js.

A new version of vp is available. Run `vp upgrade` to update.
```

## `vp env off`

A subsequent command stays quiet after the notice timestamp is recorded.

```
VITE+ - The Unified Toolchain for the Web

Node.js management is already set to system-first.
All vp commands and shims will prefer system Node.js, falling back to managed if not found.
```
3 changes: 2 additions & 1 deletion crates/vp_global_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ serde_json = { workspace = true }
node-semver = { workspace = true }
thiserror = { workspace = true }
tar = { workspace = true }
tempfile = { workspace = true }
tokio = { workspace = true, features = ["full"] }
tracing = { workspace = true }
owo-colors = { workspace = true }
same-file = { workspace = true }
oxc_resolver = { workspace = true }
crossterm = { workspace = true }
indexmap = { workspace = true }
Expand All @@ -44,7 +46,6 @@ uuid = { workspace = true, features = ["v4"] }

[dev-dependencies]
serial_test = { workspace = true }
tempfile = { workspace = true }

[lints]
workspace = true
16 changes: 15 additions & 1 deletion crates/vp_global_cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,10 @@ pub enum Commands {
/// Custom npm registry URL
#[arg(long)]
registry: Option<String>,

/// Refresh the cached update status without producing output
#[arg(long, hide = true)]
background_check: bool,
},

/// Remove vp and all related data
Expand Down Expand Up @@ -1045,7 +1049,16 @@ pub async fn run_command_with_options(
Commands::Env(args) => commands::env::execute(cwd, args).await,

// Self-Management
Commands::Upgrade { version, tag, check, rollback, force, silent, registry } => {
Commands::Upgrade {
version,
tag,
check,
rollback,
force,
silent,
registry,
background_check,
} => {
commands::upgrade::execute(commands::upgrade::UpgradeOptions {
version,
tag,
Expand All @@ -1054,6 +1067,7 @@ pub async fn run_command_with_options(
force,
silent,
registry,
background_check,
})
.await
}
Expand Down
165 changes: 165 additions & 0 deletions crates/vp_global_cli/src/commands/env/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -537,9 +537,21 @@ case ":${PATH}:" in
esac
unset __vp_bin

# Start the Rust update checker as a detached shell job. The checker itself
# uses a cross-process lock and returns immediately when the cache is fresh.
__vp_background_upgrade_check() {
case $- in *i*) ;; *) return 0 ;; esac
(
command vp upgrade --background-check </dev/null >/dev/null 2>&1 &
disown 2>/dev/null || true
)
}
__vp_background_upgrade_check

# Shell function wrapper: intercepts `vp env use` to eval its stdout,
# which sets/unsets VP_NODE_VERSION in the current shell session.
vp() {
__vp_background_upgrade_check
Comment thread
liangmiQwQ marked this conversation as resolved.
if [ "$1" = "env" ] && [ "$2" = "use" ]; then
case " $* " in *" -h "*|*" --help "*) command vp "$@"; return; esac
__vp_out="$(VP_ENV_USE_EVAL_ENABLE=1 VP_SHELL=sh command vp "$@")" || return $?
Expand Down Expand Up @@ -572,9 +584,17 @@ set -l __vp_idx (contains -i -- __VP_BIN__ $PATH)
and set -e PATH[$__vp_idx]
set -gx PATH __VP_BIN__ $PATH

function __vp_background_upgrade_check
status is-interactive; or return
command vp upgrade --background-check </dev/null >/dev/null 2>&1 &
disown 2>/dev/null
end
__vp_background_upgrade_check

# Shell function wrapper: intercepts `vp env use` to eval its stdout,
# which sets/unsets VP_NODE_VERSION in the current shell session.
function vp
__vp_background_upgrade_check
if test (count $argv) -ge 2; and test "$argv[1]" = "env"; and test "$argv[2]" = "use"
if contains -- -h $argv; or contains -- --help $argv
command vp $argv; return
Expand Down Expand Up @@ -606,9 +626,17 @@ const ENV_TEMPLATE_NU: &str = r#"# Vite+ environment setup (https://viteplus.dev
$env.VP_HOME = ("__VP_HOME__" | path expand --no-symlink)
$env.PATH = ($env.PATH | where { $in != "__VP_BIN__" } | prepend "__VP_BIN__")

def __vp_background_upgrade_check [] {
if $nu.is-interactive {
job spawn { ^vp upgrade --background-check | complete | ignore } | ignore
}
}
__vp_background_upgrade_check

# Shell function wrapper: intercepts `vp env use` to parse its stdout,
# which sets/unsets VP_NODE_VERSION in the current shell session.
def --env --wrapped vp [...args: string@"nu-complete vp"] {
__vp_background_upgrade_check
if ($args | length) >= 2 and $args.0 == "env" and $args.1 == "use" {
if ("-h" in $args) or ("--help" in $args) {
^vp ...$args
Expand Down Expand Up @@ -670,9 +698,21 @@ if ($env:Path -split ';' -notcontains $__vp_bin) {
$env:Path = "$__vp_bin;$env:Path"
}

function __vp_background_upgrade_check {
if (-not [Environment]::UserInteractive -or [Console]::IsInputRedirected) { return }
$__vp_args = [Environment]::GetCommandLineArgs()
if ($__vp_args -match '^-(NonInteractive|noni|File|f|Command(WithArgs)?|c(wa)?|EncodedCommand|e(c)?)$' -or $__vp_args -match '\.ps1$') { return }
try {
$__vp_null = if ([System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT) { "NUL" } else { "/dev/null" }
Start-Process -FilePath (Join-Path $__vp_bin "vp") -ArgumentList "upgrade", "--background-check" -NoNewWindow -RedirectStandardError $__vp_null -ErrorAction SilentlyContinue | Out-Null
} catch {}
}
__vp_background_upgrade_check

# Shell function wrapper: intercepts `vp env use` to eval its stdout,
# which sets/unsets VP_NODE_VERSION in the current shell session.
function vp {
__vp_background_upgrade_check
if ($args.Count -ge 2 -and $args[0] -eq "env" -and $args[1] -eq "use") {
if ($args -contains "-h" -or $args -contains "--help") {
& (Join-Path $__vp_bin "vp") @args; return
Expand Down Expand Up @@ -1257,6 +1297,131 @@ mod tests {
);
}

#[tokio::test]
async fn test_create_env_files_launch_background_upgrade_checks() {
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 posix = tokio::fs::read_to_string(home.join("env")).await.unwrap();
let fish = tokio::fs::read_to_string(home.join("env.fish")).await.unwrap();
let nu = tokio::fs::read_to_string(home.join("env.nu")).await.unwrap();
let powershell = tokio::fs::read_to_string(home.join("env.ps1")).await.unwrap();

#[cfg(unix)]
assert!(
std::process::Command::new("sh")
.arg("-n")
.arg(home.join("env").as_path())
.status()
.unwrap()
.success(),
"POSIX integration should parse as a shell script"
);

for (shell, content) in
[("POSIX", posix.as_str()), ("Fish", fish.as_str()), ("Nushell", nu.as_str())]
{
assert!(
content.contains("upgrade --background-check"),
"{shell} integration should invoke the hidden check command"
);
assert!(
content.matches("__vp_background_upgrade_check").count() >= 3,
"{shell} integration should check at startup and before each vp command"
);
}

assert!(
posix.contains("(\n command vp upgrade --background-check")
&& posix.contains("&\n disown"),
"POSIX should detach in a subshell without replacing the caller's last background PID"
);
assert!(fish.contains("&\n disown"), "Fish should detach with shell job control");
assert!(nu.contains("job spawn"), "Nushell should use its native job API");
assert!(posix.contains("case $- in *i*)"), "POSIX should require an interactive shell");
assert!(fish.contains("status is-interactive"), "Fish should require an interactive shell");
assert!(
nu.contains("if $nu.is-interactive"),
"Nushell should require an interactive shell"
);
for expected in [
"--background-check",
"Start-Process",
"-NoNewWindow",
"[Environment]::UserInteractive",
"[Console]::IsInputRedirected",
"[Environment]::GetCommandLineArgs()",
"Command(WithArgs)?",
"e(c)?",
"\\.ps1$",
] {
assert!(
powershell.contains(expected),
"PowerShell integration should contain `{expected}`"
);
}
assert!(
powershell.matches("__vp_background_upgrade_check").count() >= 3,
"PowerShell integration should check at startup and before each vp command"
);
for (shell, content) in [
("POSIX", posix.as_str()),
("Fish", fish.as_str()),
("Nushell", nu.as_str()),
("PowerShell", powershell.as_str()),
] {
assert!(
!content.contains("VP_NO_UPDATE_CHECK") && !content.contains("CI"),
"{shell} should leave update-check policy to the hidden command"
);
}
}

#[cfg(unix)]
#[tokio::test]
async fn test_posix_background_upgrade_check_preserves_last_background_pid() {
use std::os::unix::fs::PermissionsExt;

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 vp_path = home.join("bin/vp");
tokio::fs::create_dir_all(vp_path.parent().unwrap()).await.unwrap();
tokio::fs::write(&vp_path, "#!/bin/sh\nexit 0\n").await.unwrap();
std::fs::set_permissions(&vp_path, std::fs::Permissions::from_mode(0o755)).unwrap();

let status = std::process::Command::new("bash")
.env("HOME", temp_dir.path())
.args([
"--noprofile",
"--norc",
"-i",
"-c",
r#"
source "$1"
sleep 30 &
server_pid=$!
vp build
checker_pid=$!
kill "$server_pid"
wait "$server_pid" 2>/dev/null
test "$checker_pid" = "$server_pid"
"#,
"bash",
])
.arg(home.join("env").as_path())
.status()
.unwrap();

assert!(status.success(), "POSIX wrapper should preserve the caller's `$!`");
}

#[tokio::test]
#[cfg(windows)]
#[serial_test::serial]
Expand Down
7 changes: 7 additions & 0 deletions crates/vp_global_cli/src/commands/upgrade/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,18 @@ pub struct UpgradeOptions {
pub silent: bool,
/// Custom npm registry URL
pub registry: Option<String>,
/// Refresh cached update status for shell integrations
pub background_check: bool,
}

/// Execute the upgrade command.
#[allow(clippy::print_stdout, clippy::print_stderr)]
pub async fn execute(options: UpgradeOptions) -> Result<ExitStatus, Error> {
if options.background_check {
crate::upgrade_check::run_background_check().await;
return Ok(ExitStatus::default());
}

let install_dir = get_vp_home()?;

// Handle --rollback
Expand Down
Loading
Loading