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
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
116 changes: 116 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,6 +537,17 @@ case ":${PATH}:" in
esac
unset __vp_bin

# Start the Rust update checker once for each interactive shell. The checker
# uses a cross-process lock and returns immediately when the cache is fresh.
case $- in
*i*)
(
command vp upgrade --background-check </dev/null >/dev/null 2>&1 &
disown 2>/dev/null || true
)
;;
esac

# Shell function wrapper: intercepts `vp env use` to eval its stdout,
# which sets/unsets VP_NODE_VERSION in the current shell session.
vp() {
Expand Down Expand Up @@ -572,6 +583,11 @@ set -l __vp_idx (contains -i -- __VP_BIN__ $PATH)
and set -e PATH[$__vp_idx]
set -gx PATH __VP_BIN__ $PATH

if status is-interactive
command vp upgrade --background-check </dev/null >/dev/null 2>&1 &
disown 2>/dev/null
end

# Shell function wrapper: intercepts `vp env use` to eval its stdout,
# which sets/unsets VP_NODE_VERSION in the current shell session.
function vp
Expand Down Expand Up @@ -606,6 +622,10 @@ 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__")

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

# 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"] {
Expand Down Expand Up @@ -670,6 +690,16 @@ if ($env:Path -split ';' -notcontains $__vp_bin) {
$env:Path = "$__vp_bin;$env:Path"
}

& {
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 {}
}

# Shell function wrapper: intercepts `vp env use` to eval its stdout,
# which sets/unsets VP_NODE_VERSION in the current shell session.
function vp {
Expand Down Expand Up @@ -1257,6 +1287,92 @@ mod tests {
);
}

#[tokio::test]
async fn test_create_env_files_launch_one_background_upgrade_check() {
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()),
("PowerShell", powershell.as_str()),
] {
assert_eq!(
content.matches("--background-check").count(),
1,
"{shell} integration should check once when the shell starts"
);
assert!(
!content.contains("__vp_background_upgrade_check"),
"{shell} integration should not check 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") && posix.contains("*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}`"
);
}
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"
);
}
}

#[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
17 changes: 4 additions & 13 deletions crates/vp_global_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,13 +436,8 @@ async fn main() -> ExitCode {
// Parse CLI arguments (using custom help formatting)
let parse_result = try_parse_args_from(normalized_args);

// Spawn background upgrade check for eligible commands
let upgrade_handle = match &parse_result {
Ok(args) if upgrade_check::should_run_for_command(args) => {
Some(tokio::spawn(upgrade_check::check_for_update()))
}
_ => None,
};
let should_display_upgrade_notice =
parse_result.as_ref().is_ok_and(upgrade_check::should_display_for_command);
Comment thread
liangmiQwQ marked this conversation as resolved.

let exit_code = match parse_result {
Err(e) => {
Expand Down Expand Up @@ -510,12 +505,8 @@ async fn main() -> ExitCode {
},
};

// Display upgrade notice if a newer version is available
if let Some(handle) = upgrade_handle
&& let Ok(Ok(Some(result))) =
tokio::time::timeout(std::time::Duration::from_millis(500), handle).await
{
upgrade_check::display_upgrade_notice(&result);
if should_display_upgrade_notice {
upgrade_check::display_cached_upgrade_notice();
Comment thread
liangmiQwQ marked this conversation as resolved.
}

exit_code
Expand Down
Loading
Loading