Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5b375ce
feat(env): add fallback shim directory and preserve trampoline roots
liangmiQwQ Sep 20, 2026
b60d8e4
feat(env): reconcile shim placement and shell paths from preferences
liangmiQwQ Sep 20, 2026
900f2da
fix(env): resolve system-first tools without shim recursion
liangmiQwQ Sep 20, 2026
08ccbab
test(env): verify shim roots and explicit bypass resolution
liangmiQwQ Sep 20, 2026
46101b1
test(env): cover fallback placement and foreign manager recursion
liangmiQwQ Sep 20, 2026
bf219c0
test(env): update setup and package manager mode snapshots
liangmiQwQ Sep 20, 2026
eabfd5b
docs(env): describe system-first fallback shims
liangmiQwQ Sep 20, 2026
3992c1d
Merge branch 'main' into liang/codex/system-first-shims
liangmiQwQ Sep 20, 2026
bf9a105
docs(env): simplify setup command descriptions
liangmiQwQ Sep 20, 2026
1c63803
test(env): expose fallback scenarios and enable Windows coverage
liangmiQwQ Sep 20, 2026
c9188e4
test(env): keep snapshot inline tables on one line
liangmiQwQ Sep 20, 2026
92135a6
test(env): organize fallback scenarios into annotated steps
liangmiQwQ Sep 20, 2026
7daf416
test(env): keep annotated snapshot steps compact
liangmiQwQ Sep 20, 2026
fc7673a
fix(ci): align checks with system-first shim placement
liangmiQwQ Sep 20, 2026
1564651
fix(env): restore legacy package manager consent prompts
liangmiQwQ Sep 20, 2026
476943d
docs(shim): note follow-up for legacy consent lookup
liangmiQwQ Sep 20, 2026
974d00d
Merge branch 'main' into liang/codex/system-first-shims
liangmiQwQ Sep 20, 2026
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
15 changes: 11 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -440,12 +440,19 @@ jobs:
- name: Install Global CLI vp
run: |
pnpm bootstrap-cli:ci
if [[ "$RUNNER_OS" == "Windows" ]]; then
echo "$USERPROFILE\.vite-plus\bin" >> $GITHUB_PATH
else
echo "$HOME/.vite-plus/bin" >> $GITHUB_PATH
if [[ "$RUNNER_OS" != "Windows" ]]; then
. "$HOME/.vite-plus/env"
# GITHUB_PATH prepends entries, but fallback shims must remain last.
echo "PATH=$PATH" >> "$GITHUB_ENV"
fi

- name: Load Windows CLI environment
if: runner.os == 'Windows'
shell: pwsh
run: |
. (Join-Path $HOME '.vite-plus/env.ps1')
"PATH=$env:Path" >> $env:GITHUB_ENV

- name: Verify vp installation
run: |
which vp
Expand Down
34 changes: 23 additions & 11 deletions .github/workflows/test-standalone-install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ jobs:
test "$CONFIG_DIR" = "$(dump_dir config)"
test "$STATE_DIR" = "$(dump_dir state)"

- name: Custom shared bin refreshes Node even in system-first mode
- name: Custom shared bin preserves Node in system-first mode
run: |
set -euo pipefail
FOREIGN=$(mktemp -d)
Expand All @@ -492,16 +492,18 @@ jobs:
export VP_BIN_DIR="$FOREIGN/.local/bin"
export VP_DATA_DIR="$FOREIGN/.local/share/vite-plus"
export VP_CACHE_DIR="$FOREIGN/.cache/vite-plus"
# Explicitly sharing the Vite+ bin directory accepts replacement there; system-first tools elsewhere stay intact.
# System-first shims belong under data, leaving shared-bin tools intact.
export VP_NODE_MANAGER=no
export PATH="$FOREIGN/user-bin:$PATH"
unset VP_HOME
unset XDG_DATA_HOME XDG_CACHE_HOME XDG_CONFIG_HOME XDG_STATE_HOME
VP_LOCAL_TGZ="$FAKE_TGZ" VP_VERSION=local-foreign-node bash packages/cli/install.sh

test ! -d "$FOREIGN/.vite-plus"
test -f "$FOREIGN/.local/bin/node"
test -L "$FOREIGN/.local/bin/node"
test "$("$FOREIGN/.local/bin/node")" = "foreign-node"
# Probe the installed launcher without downloading a managed runtime.
FALLBACK_BIN=$(VP_DUMP_DIRS=1 "$VP_DATA_DIR/fallback-bin/node" | awk -F '\t' '$1 == "bin" { print $2 }')
test "$FALLBACK_BIN" = "$VP_BIN_DIR"
test "$("$FOREIGN/user-bin/node")" = "foreign-node"

- name: Existing ~/.vite-plus is reused
Expand Down Expand Up @@ -1759,7 +1761,7 @@ jobs:
if (Test-Path $legacyRoot) { throw "implode left the monolithic root" }
if ($removingRoots.Count -ne 0) { throw "implode left a renamed monolithic root" }

- name: PowerShell installer refreshes shared-bin Node even in system-first mode
- name: PowerShell installer preserves shared-bin Node in system-first mode
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
Expand Down Expand Up @@ -1787,18 +1789,28 @@ jobs:
$env:VP_LOCAL_TGZ = $fakeTgz
$env:VP_SKIP_DEPS_INSTALL = "1"
$env:VP_VERSION = "local-foreign-node"
# Explicitly sharing the Vite+ bin directory accepts replacement there; system-first tools elsewhere stay intact.
# System-first shims belong under data, leaving shared-bin tools intact.
$env:VP_NODE_MANAGER = "no"
$env:CI = "true"

& ./packages/cli/install.ps1

$trampoline = Join-Path $data "current/bin/vp-shim.exe"
if ((Get-FileHash $node).Hash -ne (Get-FileHash $trampoline).Hash) {
Write-Error "install.ps1 did not refresh node.exe in the configured bin directory"
if ([System.IO.File]::ReadAllText($node) -ne "foreign-node") {
Write-Error "install.ps1 replaced the foreign Node in the shared bin directory"
}
if ([System.IO.File]::ReadAllText($pointer) -ne [System.IO.File]::ReadAllText((Join-Path $bin "vp.shim"))) {
Write-Error "install.ps1 did not refresh the Node shim pointer"
if ([System.IO.File]::ReadAllText($pointer) -ne "$data`n") {
Write-Error "install.ps1 replaced the foreign Node pointer"
}
$fallbackNode = Join-Path $data "fallback-bin/node.exe"
# Probe the installed launcher without downloading a managed runtime.
$env:VP_DUMP_DIRS = "1"
try {
$dirs = @(& $fallbackNode)
if ($LASTEXITCODE -ne 0 -or $dirs -notcontains "bin`t$bin") {
Write-Error "The fallback Node launcher did not preserve the main bin directory"
}
} finally {
Remove-Item Env:VP_DUMP_DIRS
}
if ([System.IO.File]::ReadAllText($externalNode) -ne "external-node") {
Write-Error "install.ps1 replaced node.exe outside the configured bin directory"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ Configuration
PATH
✓ vp ~/.vite-plus/bin/vp
✓ Shim dir ~/.vite-plus/bin
✓ pnpm ~/.vite-plus/bin/pnpm (vp shim)
✓ pnpx ~/.vite-plus/bin/pnpx (vp shim)
✓ pnpm <workspace>/system-bin/pnpm (system)
✓ pnpx ~/.vite-plus/fallback-bin/pnpx (vp shim)

Package Manager Resolution
Source system PATH
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const { symlinkSync } = require('node:fs');

// Model the old layout with owned links, rather than unrelated executable files.
for (const tool of ['node', 'npm', 'pnpm', 'pnpx']) {
symlinkSync('../current/bin/vp', `home/bin/${tool}`);
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,12 @@ steps = [
{ argv = ["vpt", "mkdir", "-p", "external", "home/bin", "user-bin"], snapshot = false },
{ argv = ["vpt", "cp", "$VP_HOME/bin/vp", "external/vp"], snapshot = false },
{ argv = ["vpt", "chmod", "+x", "external/vp"], snapshot = false },
{ argv = ["vpt", "write-file", "home/bin/node", "old-node-shim"], snapshot = false },
{ argv = ["vpt", "write-file", "home/bin/npm", "old-npm-shim"], snapshot = false },
{ argv = ["vpt", "write-file", "home/bin/pnpm", "old-pnpm-shim"], snapshot = false },
{ argv = ["vpt", "write-file", "home/bin/pnpx", "old-pnpx-shim"], snapshot = false },
{ argv = ["node", "seed-owned-shims.cjs"], snapshot = false },
{ argv = ["vpt", "write-file", "user-bin/node", "user-node-shim"], snapshot = false },
{ argv = ["vpt", "write-file", "user-bin/pnpm", "user-pnpm-shim"], snapshot = false },
{ argv = ["./external/vp"], tty = false, envs = [["VP_HOME", "${workspace}/home"], ["VP_VERSION", "mixed-shims"], ["VP_NODE_MANAGER", "no"], ["VP_PM_MANAGER", "no"], ["VP_PNPM_MANAGER", "yes"], ["PATH", "${workspace}/user-bin${PATH_SEPARATOR}${PATH}"]], comment = "Installation refreshes every Vite+ shim regardless of management preferences, leaving user tools elsewhere on PATH untouched", snapshot = false },
["vpt", "stat-file", "home/bin/node", "--assert", "symlink"],
["vpt", "stat-file", "home/bin/npm", "--assert", "symlink"],
{ argv = ["./external/vp"], tty = false, envs = [["VP_HOME", "${workspace}/home"], ["VP_VERSION", "mixed-shims"], ["VP_NODE_MANAGER", "no"], ["VP_PM_MANAGER", "no"], ["VP_PNPM_MANAGER", "yes"], ["PATH", "${workspace}/user-bin${PATH_SEPARATOR}${PATH}"]], comment = "Installation places owned shims according to each management preference, leaving user tools elsewhere on PATH untouched", snapshot = false },
["vpt", "stat-file", "home/fallback-bin/node", "--assert", "symlink"],
["vpt", "stat-file", "home/fallback-bin/npm", "--assert", "symlink"],
["vpt", "stat-file", "home/bin/pnpm", "--assert", "symlink"],
["vpt", "stat-file", "home/bin/pnpx", "--assert", "symlink"],
["vpt", "print-file", "user-bin/node"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,7 @@
## `vpt chmod +x external/vp`


## `vpt write-file home/bin/node old-node-shim`


## `vpt write-file home/bin/npm old-npm-shim`


## `vpt write-file home/bin/pnpm old-pnpm-shim`


## `vpt write-file home/bin/pnpx old-pnpx-shim`
## `node seed-owned-shims.cjs`


## `vpt write-file user-bin/node user-node-shim`
Expand All @@ -29,19 +20,19 @@

## `VP_HOME=${workspace}/home VP_VERSION=mixed-shims VP_NODE_MANAGER=no VP_PM_MANAGER=no VP_PNPM_MANAGER=yes PATH=${workspace}/user-bin${PATH_SEPARATOR}${PATH} ./external/vp`

Installation refreshes every Vite+ shim regardless of management preferences, leaving user tools elsewhere on PATH untouched
Installation places owned shims according to each management preference, leaving user tools elsewhere on PATH untouched


## `vpt stat-file home/bin/node --assert symlink`
## `vpt stat-file home/fallback-bin/node --assert symlink`

```
home/bin/node: symlink
home/fallback-bin/node: symlink
```

## `vpt stat-file home/bin/npm --assert symlink`
## `vpt stat-file home/fallback-bin/npm --assert symlink`

```
home/bin/npm: symlink
home/fallback-bin/npm: symlink
```

## `vpt stat-file home/bin/pnpm --assert symlink`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ brew-prefix/bin/.vp-setup-complete: missing
Cleanup removes user data and leaves the Homebrew package installed

```
✓ Vite+ removed 12 shims from <workspace>/home/bin
✓ Vite+ removed 3 shims from <workspace>/home/bin
✓ Removed <workspace>/home

✓ Vite+ removed its managed files and shell entries from your system.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,13 @@ function createEnvironment(directory) {
VP_HOME: home,
VP_SELF_SETUP_NO_MODIFY_PATH: '1',
NPM_CONFIG_REGISTRY: 'http://127.0.0.1:9',
PATH: [path.join(home, 'bin'), path.join(directory, 'brew/bin'), system, env.PATH].join(
path.delimiter,
),
PATH: [
path.join(home, 'bin'),
path.join(directory, 'brew/bin'),
system,
env.PATH,
path.join(home, 'fallback-bin'),
].join(path.delimiter),
};
}

Expand All @@ -69,6 +73,7 @@ function verifyDoctor(source) {
const env = createEnvironment(directory);
const publicBin = path.join(directory, 'brew/bin');
const shimBin = path.join(env.VP_HOME, 'bin');
const fallbackBin = path.join(env.VP_HOME, 'fallback-bin');
const systemBin = path.join(directory, 'system/bin');
fs.mkdirSync(publicBin, { recursive: true });
fs.symlinkSync(binary, path.join(publicBin, 'vp'));
Expand Down Expand Up @@ -104,7 +109,7 @@ function verifyDoctor(source) {
binary,
['env', 'doctor', 'node'],
directory,
{ ...env, PATH: paths.join(path.delimiter) },
{ ...env, PATH: [...paths, fallbackBin].join(path.delimiter) },
status,
);
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,20 @@ POSIX wrapper and zsh vpr completion keep global -C before env use/run
# Vite+ environment setup (https://viteplus.dev)
export VP_HOME="<workspace>/home"
__vp_bin="<workspace>/home/bin"
while case ":${PATH}:" in *":${__vp_bin}:"*) true ;; *) false ;; esac; do
__vp_tmp=":${PATH}:"
__vp_before="${__vp_tmp%%":${__vp_bin}:"*}"
__vp_before="${__vp_before#:}"
__vp_after="${__vp_tmp#*":${__vp_bin}:"}"
__vp_after="${__vp_after%:}"
PATH="${__vp_before}${__vp_before:+${__vp_after:+:}}${__vp_after}"
__vp_fallback="<workspace>/home/fallback-bin"
for __vp_dir in "$__vp_bin" "$__vp_fallback"; do
while case ":${PATH}:" in *":${__vp_dir}:"*) true ;; *) false ;; esac; do
__vp_tmp=":${PATH}:"
__vp_before="${__vp_tmp%%":${__vp_dir}:"*}"
__vp_before="${__vp_before#:}"
__vp_after="${__vp_tmp#*":${__vp_dir}:"}"
__vp_after="${__vp_after%:}"
PATH="${__vp_before}${__vp_before:+${__vp_after:+:}}${__vp_after}"
done
done
export PATH="${__vp_bin}${PATH:+:${PATH}}"
unset __vp_bin __vp_tmp __vp_before __vp_after
export PATH="${__vp_bin}${PATH:+:${PATH}}:${__vp_fallback}"
unset __vp_bin __vp_fallback __vp_dir __vp_tmp __vp_before __vp_after
hash -r 2>/dev/null || true

# Shell function wrapper: intercepts `vp env use` to eval its stdout,
# which sets/unsets VP_NODE_VERSION in the current shell session.
Expand All @@ -48,7 +52,9 @@ vp() {
eval "$__vp_out"
else
unset __vp_env_use
command vp "$@"
command vp "$@" || return $?
# Mode changes move executables between directories; discard cached command paths.
hash -r 2>/dev/null || true
fi
}

Expand Down Expand Up @@ -101,7 +107,10 @@ set -gx VP_HOME "<workspace>/home"
while set -l __vp_idx (contains -i -- "<workspace>/home/bin" $PATH)
set -e PATH[$__vp_idx]
end
set -gx PATH "<workspace>/home/bin" $PATH
while set -l __vp_idx (contains -i -- "<workspace>/home/fallback-bin" $PATH)
set -e PATH[$__vp_idx]
end
set -gx PATH "<workspace>/home/bin" $PATH "<workspace>/home/fallback-bin"

# Shell function wrapper: intercepts `vp env use` to eval its stdout,
# which sets/unsets VP_NODE_VERSION in the current shell session.
Expand Down Expand Up @@ -164,7 +173,7 @@ Nushell wrapper and vpr completion keep global -C before env use/run
```
# Vite+ environment setup (https://viteplus.dev)
$env.VP_HOME = ("<workspace>/home" | path expand --no-symlink)
$env.PATH = ($env.PATH | where { $in != "<workspace>/home/bin" } | prepend "<workspace>/home/bin")
$env.PATH = ($env.PATH | where { $in != "<workspace>/home/bin" and $in != "<workspace>/home/fallback-bin" } | prepend "<workspace>/home/bin" | append "<workspace>/home/fallback-bin")

# Shell function wrapper: intercepts `vp env use` to parse its stdout,
# which sets/unsets VP_NODE_VERSION in the current shell session.
Expand Down Expand Up @@ -246,9 +255,9 @@ PowerShell wrapper and vpr completion keep global -C before env use/run
# Vite+ environment setup (https://viteplus.dev)
$env:VP_HOME = '<workspace>/home'
$__vp_bin = '<workspace>/home/bin'
if ($env:Path -split ';' -notcontains $__vp_bin) {
$env:Path = "$__vp_bin;$env:Path"
}
$__vp_fallback = '<workspace>/home/fallback-bin'
$__vp_paths = @($env:Path -split ';' | Where-Object { $_ -and $_ -ne $__vp_bin -and $_ -ne $__vp_fallback })
$env:Path = (@($__vp_bin) + $__vp_paths + @($__vp_fallback)) -join ';'

# Shell function wrapper: intercepts `vp env use` to eval its stdout,
# which sets/unsets VP_NODE_VERSION in the current shell session.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ steps = [
{ argv = ["node", "setup-system-npm.cjs"], snapshot = false },
{ argv = ["vp", "env", "off", "npm"], snapshot = false },
{ argv = ["vp", "env", "off", "node"], snapshot = false },
{ argv = ["vp", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-npm${PATH_SEPARATOR}/usr/bin${PATH_SEPARATOR}/bin"]], comment = "An absent system alias resolves normally instead of entering injected-tool passthrough" },
{ argv = ["vp", "install"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-npm${PATH_SEPARATOR}/usr/bin${PATH_SEPARATOR}/bin${PATH_SEPARATOR}${VP_HOME}/fallback-bin"]], comment = "An absent system alias resolves normally instead of entering injected-tool passthrough" },
]

[[case]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
## `vp env off node`


## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-npm${PATH_SEPARATOR}/usr/bin${PATH_SEPARATOR}/bin vp install`
## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-npm${PATH_SEPARATOR}/usr/bin${PATH_SEPARATOR}/bin${PATH_SEPARATOR}${VP_HOME}/fallback-bin vp install`

An absent system alias resolves normally instead of entering injected-tool passthrough

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ steps = [
{ "write-key" = "down" },
{ "write-key" = "enter" },
] },
{ argv = ["vpt", "stat-file", "$VP_HOME/bin/pnpm", "--assert", "missing"], snapshot = false },
{ argv = ["vpt", "stat-file", "$VP_HOME/fallback-bin/pnpm", "--assert", "symlink"], snapshot = false },
{ argv = ["vpt", "print-file", "$VP_HOME/config.json"], comment = "the explicit system choice records only pnpm" },
{ argv = ["pnpm", "--version"], envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"]], comment = "later pnpm invocations use the recorded choice without prompting" },
{ argv = ["yarn", "--version"], snapshot = false, envs = [["PATH", "${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH}"]], interactions = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
## `PATH=${VP_HOME}/bin${PATH_SEPARATOR}${workspace}/system-bin${PATH_SEPARATOR}${PATH} pnpm --version`


## `vpt stat-file $VP_HOME/bin/pnpm --assert missing`


## `vpt stat-file $VP_HOME/fallback-bin/pnpm --assert symlink`


## `vpt print-file $VP_HOME/config.json`

the explicit system choice records only pnpm
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20.18.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const assert = require('node:assert/strict');
const { execFileSync } = require('node:child_process');
const { realpathSync } = require('node:fs');

const output = execFileSync('vp', ['env', 'which', 'node'], { encoding: 'utf8', timeout: 10000 });
assert.equal(realpathSync(output.split('\n')[0].trim()), realpathSync(process.execPath));
console.log('env which selects the same Node as PATH');
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { existsSync, readdirSync } = require('node:fs');
const path = require('node:path');

// Normalize executable suffixes and omit Windows sidecars, keeping placement visible in snapshots.
for (const directory of process.argv.slice(2)) {
const resolved = directory.replace('$VP_HOME', process.env.VP_HOME);
const names = readdirSync(resolved).filter((name) =>
/^(node|npm|npx|pnpm|pnpx|yarn|yarnpkg|bun|bunx|vpx|vpr)(\.exe)?$/.test(name),
);
for (const name of names.sort()) {
if (!existsSync(path.join(resolved, name))) throw new Error(`Broken shim: ${name}`);
}
console.log(`${directory}: ${names.map((name) => name.replace(/\.exe$/, '')).join(', ') || '(empty)'}`);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
set -eu
. "$VP_HOME/env"
original_path=$PATH
mkdir -p system-bin
printf '#!/bin/sh\necho v99.0.0\n' > system-bin/node
chmod +x system-bin/node
system_bin="$PWD/system-bin"

vp env on >/dev/null
node --version >/dev/null # Populate Bash's command cache before moving the shim.
vp env off node >/dev/null
PATH="$system_bin:$PATH"
. "$VP_HOME/env"
. "$VP_HOME/env"
test "${PATH%%:*}" = "$VP_HOME/bin"
test "${PATH##*:}" = "$VP_HOME/fallback-bin"
test "$(command -v node)" = "$system_bin/node"
test "$(node --version)" = v99.0.0
test "$(vp env which node)" = "$system_bin/node"

PATH="$VP_HOME/fallback-bin:$system_bin:$original_path"
resolved=$(vp env which node)
test "${resolved%%$'\n'*}" = "$VP_HOME/js_runtime/node/20.18.0/bin/node"
test "$(node --version)" = v20.18.0
PATH="$system_bin:$original_path"
. "$VP_HOME/env"
node --version >/dev/null
vp env on node >/dev/null
test "$(command -v node)" = "$VP_HOME/bin/node"
test "$(node --version)" = v20.18.0
echo 'Repeated shell setup and mode changes preserve PATH precedence and clear Bash cache'
Loading
Loading