Implemented
Add a standalone vp-setup.exe Windows installer binary, distributed via GitHub Releases, that installs the vp CLI without requiring PowerShell. This complements the existing irm https://vite.plus/ps1 | iex script-based installer. Modeled after rustup-init.exe.
The current Windows installation requires running a PowerShell command:
irm https://vite.plus/ps1 | iexThis has several friction points:
- Execution policy barriers: Many corporate/enterprise Windows machines restrict PowerShell script execution (
Set-ExecutionPolicychanges required). - No cmd.exe support: Users in
cmd.exeor Git Bash cannot use theirm | iexidiom without first opening PowerShell. - No double-click install: Users following documentation cannot simply download-and-run an installer.
- CI friction: GitHub Actions using
shell: cmdorshell: bashon Windows need workarounds to invoke PowerShell. - PowerShell version fragmentation: PowerShell 5.1 (built-in) and PowerShell 7+ (pwsh) have subtle differences that the script must handle.
rustup provides rustup-init.exe — a single console binary that users download and run from any shell or by double-clicking. Key characteristics:
- Console-only (no GUI), interactive prompts with numbered menu
- Silent mode via
-yflag for CI - Single binary that is both installer and main tool (detects behavior from
argv[0]) - Modifies Windows User PATH via registry
- Registers in Add/Remove Programs
- DLL security mitigations for download-folder execution
- Provide a single
.exethat installs vp from any Windows shell or double-click - Support silent/unattended installation for CI environments
- Reuse existing installation logic from the
vp upgradecommand - Keep the installer binary small (target: 3-5 MB)
- Replicate the exact same installation result as
install.ps1
- GUI installer (MSI, NSIS, Inno Setup) — console-only like rustup
- Cross-platform installer binary (Linux/macOS are well-served by
install.sh) - winget/chocolatey/scoop package submission (future work)
- Code signing (required for GA, but out of scope for this RFC)
rustup uses one binary for everything — rustup-init.exe copies itself to ~/.cargo/bin/rustup.exe and changes behavior based on argv[0]. This works because rustup IS the toolchain manager.
Not suitable for vp because:
vp.exeis downloaded from the npm registry as a platform-specific package- The installer cannot copy itself as
vp.exe— they are fundamentally different binaries vp.exelinksvite_js_runtime,vite_workspace,oxc_resolver(~15-20 MB) — the installer needs none of these
Create two new crates:
crates/vite_setup/ — shared installation logic (library)
crates/vite_installer/ — standalone installer binary
vite_setup extracts the reusable installation logic currently in vite_global_cli/src/commands/upgrade/. Both vp upgrade and vp-setup.exe call into vite_setup.
Benefits:
- Installer binary stays small (3-5 MB)
vp upgradeandvp-setup.exeshare identical installation logic — no drift- Clear separation of concerns
Original location in upgrade/ |
Extracted to vite_setup:: |
Purpose |
|---|---|---|
platform.rs |
platform |
OS/arch detection |
registry.rs |
registry |
npm registry queries |
integrity.rs |
integrity |
SHA-512 verification |
install.rs (all functions) |
install |
Tarball extraction, package.json generation, .npmrc overrides, dep install, symlink/junction swap, version cleanup, rollback support |
- CLI argument parsing for
vp upgrade - Version comparison (current vs available)
- Rollback logic
- Output formatting specific to upgrade UX
- Interactive installation prompts (numbered menu)
- Windows User PATH modification via registry
- Node.js version manager setup prompt
- Shell env file creation
- Existing installation detection
- DLL security mitigations (for download-folder execution)
vite_installer (binary, ~3-5 MB)
├── vite_setup (shared installation logic)
├── vite_install (HTTP client)
├── vite_shared (home dir resolution)
├── vite_path (typed path wrappers)
├── clap (CLI parsing)
├── tokio (async runtime)
├── indicatif (progress bars)
└── owo-colors (terminal colors)
vite_global_cli (existing)
├── vite_setup (replaces inline upgrade code)
└── ... (all existing deps)
When run without flags (double-click or plain vp-setup.exe):
Welcome to Vite+ Installer!
This will install the vp CLI and monorepo task runner.
Install directory: C:\Users\alice\.vite-plus
PATH modification: C:\Users\alice\.vite-plus\bin → User PATH
Version: latest
Node.js manager: enabled
1) Proceed with installation (default)
2) Customize installation
3) Cancel
>
The Node.js manager value is pre-computed via auto-detection before the menu is shown (see Node.js Manager Auto-Detection). The user can override it in the customize submenu before proceeding.
Customization submenu:
Customize installation:
1) Version: [latest]
2) npm registry: [(default)]
3) Node.js manager: [enabled]
4) Modify PATH: [yes]
Enter option number to change, or press Enter to go back:
>
The installer auto-detects CI environments (CI=true) and skips interactive prompts, so -y is not required in CI:
# CI environments are automatically non-interactive
vp-setup.exe
# Explicit silent mode (outside CI)
vp-setup.exe -y
# Customize
vp-setup.exe --version 0.3.0 --no-node-manager --registry https://registry.npmmirror.com| Flag | Description | Default |
|---|---|---|
-y / --yes |
Accept defaults, no prompts | interactive |
-q / --quiet |
Suppress output except errors | false |
--version <VER> |
Install specific version | latest |
--tag <TAG> |
npm dist-tag | latest |
--install-dir <PATH> |
Installation directory | %USERPROFILE%\.vite-plus |
--registry <URL> |
npm registry URL | https://registry.npmjs.org |
--no-node-manager |
Skip Node.js manager setup | auto-detect |
--no-modify-path |
Don't modify User PATH | modify |
| Variable | Maps to |
|---|---|
VP_VERSION |
--version |
VP_HOME |
--install-dir |
NPM_CONFIG_REGISTRY |
--registry |
VP_NODE_MANAGER=yes|no |
--no-node-manager |
CLI flags take precedence over environment variables.
The installer replicates the same result as install.ps1, implemented in Rust via vite_setup.
┌─────────────────────────────────────────────────────────────┐
│ RESOLVE │
│ │
│ ┌─ detect platform ──────── win32-x64-msvc │
│ │ win32-arm64-msvc │
│ │ │
│ ├─ check existing ──────── read %VP_HOME%\current │
│ │ │
│ └─ resolve version ──────── resolve_version_string() │
│ 1 HTTP call: "latest" → "0.3.0" │
│ same version? → skip to │
│ CONFIGURE (repair path) │
└─────────────────────────────────────────────────────────────┘
│
(only if version differs)
│
▼
┌─────────────────────────────────────────────────────────────┐
│ DOWNLOAD & VERIFY │
│ │
│ ┌─ resolve platform pkg ── resolve_platform_package() │
│ │ 2nd HTTP call: tarball URL + SRI │
│ │ │
│ ├─ download tarball ─────── GET tarball URL from registry │
│ │ progress spinner via indicatif │
│ │ │
│ └─ verify integrity ─────── SHA-512 SRI hash comparison │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ INSTALL │
│ │
│ ┌─ extract binary ──────── %VP_HOME%\{version}\bin\ │
│ │ vp.exe + vp-shim.exe │
│ │ │
│ ├─ generate package.json ─ wrapper with vite-plus dep │
│ │ pins pnpm@10.33.0 │
│ │ │
│ ├─ write .npmrc ────────── minimum-release-age=0 │
│ │ │
│ └─ install deps ────────── spawn: vp install --silent │
│ installs vite-plus + transitive │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ ACTIVATE ◄── point of no │
│ return │
│ ┌─ save previous version ── .previous-version (rollback) │
│ │ (only if upgrading existing) │
│ │ │
│ ├─ swap current ────────── mklink /J current → {version} │
│ │ (junction on Windows, │
│ │ atomic symlink on Unix) │
│ │ │
│ └─ cleanup old versions ── keep last 5 by creation time │
│ protects new + previous version │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ CONFIGURE (best-effort, always runs, │
│ even for same-version repair) │
│ │
│ ┌─ create bin shims ────── copy vp-shim.exe → bin\vp.exe │
│ │ (rename-to-.old if running) │
│ │ │
│ ├─ Node.js manager ────── if enabled (pre-computed): │
│ │ spawn: vp env setup --refresh │
│ │ if disabled: │
│ │ spawn: vp env setup --env-only │
│ │ │
│ └─ modify User PATH ────── if --no-modify-path not set: │
│ HKCU\Environment\Path │
│ prepend %VP_HOME%\bin │
│ broadcast WM_SETTINGCHANGE │
└─────────────────────────────────────────────────────────────┘
│
▼
✔ Print success
Each phase maps to vite_setup library functions shared with vp upgrade:
| Phase | Key function | Crate |
|---|---|---|
| Resolve | platform::detect_platform_suffix() |
vite_setup |
| Resolve | install::read_current_version() |
vite_setup |
| Resolve | registry::resolve_version_string() |
vite_setup |
| Download & Verify | registry::resolve_platform_package() |
vite_setup |
| Download & Verify | HttpClient::get_bytes() |
vite_install |
| Download & Verify | integrity::verify_integrity() |
vite_setup |
| Install | install::extract_platform_package() |
vite_setup |
| Install | install::generate_wrapper_package_json() |
vite_setup |
| Install | install::write_release_age_overrides() |
vite_setup |
| Install | install::install_production_deps() |
vite_setup |
| Activate | install::save_previous_version() |
vite_setup |
| Activate | install::swap_current_link() |
vite_setup |
| Activate | install::cleanup_old_versions() |
vite_setup |
| Configure | install::refresh_shims() |
vite_setup |
| Configure | windows_path::add_to_user_path() |
vite_installer |
Same-version repair: When the resolved version matches the installed version, the DOWNLOAD/INSTALL/ACTIVATE phases are skipped entirely (saving 1 HTTP request + all I/O). The CONFIGURE phase always runs to repair shims, env files, and PATH if needed.
Failure recovery: Before the Activate phase, failures clean up the version directory and leave the existing installation untouched. After Activate, all CONFIGURE steps are best-effort — failures log a warning but do not cause exit code 1. Rerunning the installer always retries CONFIGURE.
The Node.js manager decision (enabled/disabled) is pre-computed before the interactive menu is shown, so the user sees the resolved value and can override it via the customize submenu. No prompts occur during the installation phase.
The auto-detection logic matches install.ps1/install.sh:
| Priority | Condition | Result |
|---|---|---|
| 1 | --no-node-manager CLI flag |
disabled |
| 2 | VP_NODE_MANAGER=yes |
enabled |
| 3 | VP_NODE_MANAGER=no |
disabled |
| 4 | bin/node.exe shim already exists |
enabled |
| 5 | CI / Codespaces / DevContainer / DevPod | enabled |
| 6 | No system node found |
enabled |
| 7 | System node present, interactive mode |
enabled |
| 8 | System node present, silent mode (-y) |
disabled |
In interactive mode (rules 7), the default matches install.ps1's Y/n prompt where pressing Enter enables it. The user can disable it in the customize menu before installation begins. In silent mode (rule 8), shims are not created unless explicitly requested, avoiding silently taking over an existing Node toolchain.
Same approach as rustup and install.ps1, using the winreg crate for registry access:
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let env = hkcu.open_subkey_with_flags("Environment", KEY_READ | KEY_WRITE)?;
let current: String = env.get_value("Path").unwrap_or_default();
// ... check if already present (case-insensitive, handles trailing backslash)
// ... prepend bin_dir, write back as REG_EXPAND_SZ
// ... broadcast WM_SETTINGCHANGE via SendMessageTimeoutW (raw FFI, single call)See crates/vite_installer/src/windows_path.rs for the full implementation.
Following rustup's approach — when the .exe is downloaded to Downloads/ and double-clicked, malicious DLLs in the same folder could be loaded. Two mitigations, both using raw FFI (no windows-sys crate):
// build.rs — linker-time: restrict DLL search at load time
#[cfg(windows)]
println!("cargo:rustc-link-arg=/DEPENDENTLOADFLAG:0x800");
// main.rs — runtime: restrict DLL search via Win32 API
#[cfg(windows)]
fn init_dll_security() {
unsafe extern "system" {
fn SetDefaultDllDirectories(directory_flags: u32) -> i32;
}
const LOAD_LIBRARY_SEARCH_SYSTEM32: u32 = 0x0000_0800;
unsafe { SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32); }
}The binary uses the console subsystem (default for Rust binaries on Windows). When double-clicked, Windows allocates a console window automatically. No special handling needed.
| Scenario | Behavior |
|---|---|
| No existing install | Fresh install |
| Same version installed | Skip download, rerun CONFIGURE phase (repair shims/PATH/env) |
| Different version installed | Upgrade to target version |
| Corrupt/partial install (broken junction) | Recreate directory structure |
Running vp.exe in bin/ |
Rename to .old, copy new (same as trampoline pattern) |
Phase 1: Skip. vp implode already handles full uninstallation.
Phase 2: Register. Write to HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\VitePlus:
DisplayName = "Vite+"
UninstallString = "C:\Users\alice\.vite-plus\current\bin\vp.exe implode --yes"
DisplayVersion = "0.3.0"
Publisher = "VoidZero"
InstallLocation = "C:\Users\alice\.vite-plus"
Attach installer binaries to each GitHub Release:
vp-setup-x86_64-pc-windows-msvc.exevp-setup-aarch64-pc-windows-msvc.exe
The release workflow already creates GitHub Releases. Add build + upload steps for the init binary.
https://viteplus.dev/vp-setup redirects (302) to https://setup.viteplus.dev via Netlify redirect in netlify.toml. Installation docs link to the user-facing viteplus.dev URL.
Submit to winget, chocolatey, scoop. Each has its own manifest format and review process.
In build-upstream/action.yml, the installer binary is built and cached alongside the CLI:
- name: Build installer binary (Windows only)
if: contains(inputs.target, 'windows')
run: cargo build --release --target ${{ inputs.target }} -p vite_installerIn release.yml, installer artifacts are uploaded per-target, renamed with the target triple, and attached to the GitHub Release:
- name: Upload installer binary artifact (Windows only)
if: contains(matrix.settings.target, 'windows')
uses: actions/upload-artifact@v4
with:
name: vp-setup-${{ matrix.settings.target }}
path: ./target/${{ matrix.settings.target }}/release/vp-setup.exetest-standalone-install.yml includes a test-vp-setup-exe job that builds the installer from source, installs via pwsh, and verifies from all three shells (pwsh, cmd, bash):
test-vp-setup-exe:
name: Test vp-setup.exe (pwsh)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: oxc-project/setup-rust@v1
- name: Build vp-setup.exe
run: cargo build --release -p vite_installer
- name: Install via vp-setup.exe (silent)
shell: pwsh
run: ./target/release/vp-setup.exe
env:
VP_VERSION: alpha
- name: Verify installation (pwsh/cmd/bash)
# verifies from all three shells after a single installThe workflow triggers on changes to crates/vite_installer/** and crates/vite_setup/**.
Windows Defender SmartScreen flags unsigned executables downloaded from the internet. This is a significant UX problem for a download-and-run installer.
Recommendation: Obtain an EV (Extended Validation) code signing certificate before GA release. EV certificates immediately remove SmartScreen warnings (no reputation building period needed).
This is an organizational decision (cost: ~$300-500/year) and out of scope for the implementation, but critical for user experience.
Target: 3-5 MB (release, stripped, LTO).
Key dependencies and their approximate contribution:
| Dependency | Purpose | Size impact |
|---|---|---|
reqwest + native-tls-vendored |
HTTP + TLS | ~1.5 MB |
flate2 + tar |
Tarball extraction | ~200 KB |
clap |
CLI parsing | ~300 KB |
tokio (minimal features) |
Async runtime | ~400 KB |
indicatif |
Progress bars | ~100 KB |
sha2 |
Integrity verification | ~50 KB |
serde_json |
Registry JSON parsing | ~200 KB |
winreg + windows-sys |
Windows registry | ~50-100 KB |
| Rust std + overhead | ~500 KB |
Use opt-level = "z" (optimize for size) in package profile override, matching the trampoline approach.
Traditional Windows installers provide GUI, Add/Remove Programs, and Start Menu integration. However:
- Adds build-time dependency on external tooling (WiX, NSIS)
- GUI is unnecessary for a developer CLI tool
- MSI has complex authoring requirements
- rustup chose console-only and it works well for the developer audience
Like rustup, make vp.exe detect when called as vp-setup.exe and switch to installer mode.
- Would bloat the installer to ~15-20 MB (all of vp's dependencies)
- vp.exe is downloaded FROM the installer — circular dependency
- The installation payload (vp.exe) and the installer are fundamentally different
Embed the PowerShell script in a self-extracting exe. Fragile, still requires PowerShell runtime.
winregcrate: Higher-level safe API, ~50-100 KB after LTO, significantly less code (~80 lines vs ~225 lines)- Raw Win32 FFI: Zero dependencies but 225 lines of unsafe code with manual UTF-16 encoding and registry choreography
- PowerShell subprocess: Proven in
install.ps1but adds process spawn overhead and PowerShell dependency - Decision: Use
winregfor registry access — the zero-dependency pattern makes sense forvite_trampoline(copied 5-10 times as shims) but not for a single downloadable installer where readability matters more.WM_SETTINGCHANGEbroadcast still uses a single raw FFI call sincewinregdoesn't wrap it.
- Created
crates/vite_setup/withplatform,registry,integrity,installmodules - Moved shared code from
vite_global_cli/src/commands/upgrade/intovite_setup - Updated
vite_global_clito import fromvite_setup - All 353 existing tests pass
- Created
crates/vite_installer/with[[bin]] name = "vp-setup" - Implemented CLI argument parsing (clap) with env var merging
- Implemented installation flow calling
vite_setupwith same-version repair path - Implemented Windows PATH modification via
winregcrate - Implemented interactive prompts with customization submenu
- Implemented Node.js manager auto-detection (pre-computed, no mid-install prompts)
- Implemented progress spinner for downloads
- Added DLL security mitigations (build.rs linker flag + runtime
SetDefaultDllDirectories) - Post-activation steps are best-effort (non-fatal on error)
- Added installer binary build to
build-upstream/action.yml(Windows targets only) - Added artifact upload and GitHub Release attachment in
release.yml - Added
test-vp-setup-exejob totest-standalone-install.yml(cmd, pwsh, bash) - Updated release body with
vp-setup.exedownload mention
- Updated installation docs on website (
docs/guide/index.md) - Added
viteplus.dev/vp-setup.exeredirect via Netlify (netlify.toml) - winget, chocolatey, scoop submission deferred to future work
- Platform detection (mock different architectures)
- PATH modification logic (registry read/write)
- Version comparison and existing install detection
- Fresh install from cmd.exe, PowerShell, Git Bash
- Silent mode (
-y) installation - Custom registry, custom install dir
- Upgrade over existing installation
- Verify
vp --versionworks after install - Verify PATH is modified correctly
- Double-click from Downloads folder
- SmartScreen behavior (signed vs unsigned)
- Windows Defender scan behavior
- ARM64 Windows (if available)
- Binary name:
vp-setup.exe - Uninstall: Rely on
vp implode— no--uninstallflag in the installer - Minimum Windows version: Windows 10 version 1809 (October 2018 Update) or later, same as Rust's
x86_64-pc-windows-msvctarget requirement
- rustup-init.exe source — single-binary installer model
- rustup self_update.rs — installation flow
- rustup windows.rs — Windows PATH/registry handling
- RFC: Windows Trampoline — existing Windows .exe shim approach
- RFC: Self-Update Command — existing upgrade logic to share