#!/bin/bash # Vite+ CLI Installer # https://vite.plus # # Usage: # curl -fsSL https://vite.plus | bash # # Environment variables: # VP_VERSION - Version to install (default: latest) # VP_HOME - Installation directory (default: ~/.vite-plus) # NPM_CONFIG_REGISTRY - Custom npm registry URL (default: https://registry.npmjs.org) # VP_NODE_MANAGER - Set to "yes" or "no" to skip interactive prompt (for CI/devcontainers) # VP_LOCAL_TGZ - Path to local vite-plus.tgz (for development/testing) set -e VP_VERSION="${VP_VERSION:-latest}" INSTALL_DIR="${VP_HOME:-$HOME/.vite-plus}" # Use $HOME-relative path for shell config references (portable across sessions) if case "$INSTALL_DIR" in "$HOME"/*) true;; *) false;; esac; then INSTALL_DIR_REF_POSIX="\$HOME${INSTALL_DIR#"$HOME"}" INSTALL_DIR_REF_NU="~${INSTALL_DIR#"$HOME"}" else INSTALL_DIR_REF_POSIX="$INSTALL_DIR" INSTALL_DIR_REF_NU="$INSTALL_DIR" fi # npm registry URL (strip trailing slash if present) NPM_REGISTRY="${NPM_CONFIG_REGISTRY:-https://registry.npmjs.org}" NPM_REGISTRY="${NPM_REGISTRY%/}" # Local tarball for development/testing LOCAL_TGZ="${VP_LOCAL_TGZ:-}" # Local binary path (set by install-global-cli.ts for local dev) LOCAL_BINARY="${VP_LOCAL_BINARY:-}" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' BRIGHT_BLUE='\033[0;94m' BOLD='\033[1m' DIM='\033[2m' BOLD_BRIGHT_BLUE='\033[1;94m' NC='\033[0m' # No Color info() { echo -e "${BLUE}info${NC}: $1" } success() { echo -e "${GREEN}success${NC}: $1" } warn() { echo -e "${YELLOW}warn${NC}: $1" } error() { echo -e "${RED}error${NC}: $1" exit 1 } is_release_age_error() { local log_file="$1" [ -f "$log_file" ] || return 1 # This wrapper install path is pinned to pnpm via packageManager, so this # detection follows pnpm's resolver/reporter output rather than npm/yarn. # # pnpm's PnpmError prefixes internal codes with ERR_PNPM_, so # NO_MATURE_MATCHING_VERSION is normally printed as # ERR_PNPM_NO_MATURE_MATCHING_VERSION. npm-resolver emits that code with the # "does not meet the minimumReleaseAge constraint" message when # publishedBy/minimumReleaseAge rejects a matching version. # https://github.com/pnpm/pnpm/blob/16cfde66ec71125d692ea828eba2a5f9b3cc54fc/core/error/src/index.ts#L18-L20 # https://github.com/pnpm/pnpm/blob/16cfde66ec71125d692ea828eba2a5f9b3cc54fc/resolving/npm-resolver/src/index.ts#L76-L84 # # default-reporter may append guidance mentioning minimumReleaseAgeExclude # when the error has an immatureVersion, so that token is also a useful # release-age signal. minimum-release-age is pnpm's .npmrc key; npm's # min-release-age is intentionally not treated as a pnpm signal here. # https://github.com/pnpm/pnpm/blob/16cfde66ec71125d692ea828eba2a5f9b3cc54fc/cli/default-reporter/src/reportError.ts#L163-L164 # https://github.com/pnpm/pnpm/blob/16cfde66ec71125d692ea828eba2a5f9b3cc54fc/config/reader/src/types.ts#L73-L74 grep -Eqi 'ERR_PNPM_NO_MATURE_MATCHING_VERSION|NO_MATURE_MATCHING_VERSION|does not meet the minimumReleaseAge constraint|minimumReleaseAge|minimumReleaseAgeExclude|minimum release age|minimum-release-age' "$log_file" && return 0 # pnpm can also surface ERR_PNPM_NO_MATCHING_VERSION when minimumReleaseAge # filters out all candidates. That code is also used for real missing # versions, so require age-gate context before prompting for a bypass. # https://github.com/pnpm/pnpm/blob/16cfde66ec71125d692ea828eba2a5f9b3cc54fc/deps/inspection/outdated/src/createManifestGetter.ts#L66-L76 if grep -Eq 'ERR_PNPM_NO_MATCHING_VERSION' "$log_file"; then grep -Eqi 'minimumReleaseAge|minimumReleaseAgeExclude|minimum release age|minimum-release-age' "$log_file" return $? fi return 1 } confirm_release_age_override() { [ -e /dev/tty ] && [ -t 1 ] || return 1 echo "" > /dev/tty echo -e "${YELLOW}warn${NC}: Your minimumReleaseAge setting prevented installing vite-plus@${VP_VERSION}." > /dev/tty echo "This setting helps protect against newly published compromised packages." > /dev/tty echo "Proceeding will disable this protection for this Vite+ install only." > /dev/tty printf "Do you want to proceed? (y/N): " > /dev/tty local response read -r response < /dev/tty || return 1 case "$response" in y|Y|yes|YES) return 0 ;; *) return 1 ;; esac } write_release_age_override() { cat > "$VERSION_DIR/.npmrc" <&1) exit_code=$? set -e if [ $exit_code -eq 0 ]; then echo "$output" return 0 fi print_curl_error "$exit_code" "$url" } # Detect libc type on Linux (gnu or musl) detect_libc() { # Prefer positive glibc detection first. # This avoids false musl detection on systems where musl is installed # but the distro itself is glibc-based (common on WSL/Ubuntu). if command -v getconf &> /dev/null; then if getconf GNU_LIBC_VERSION > /dev/null 2>&1; then echo "gnu" return fi fi # Check ldd output for musl/glibc if command -v ldd &> /dev/null; then ldd_out="$(ldd --version 2>&1 || true)" if echo "$ldd_out" | grep -qi musl; then echo "musl" return fi if echo "$ldd_out" | grep -qi 'gnu libc'; then echo "gnu" return fi if echo "$ldd_out" | grep -qi 'glibc'; then echo "gnu" return fi fi # Final fallback: musl loader present usually indicates musl-based distro, # but only check this after glibc detection to avoid false positives. if [ -e /lib/ld-musl-x86_64.so.1 ] || [ -e /lib/ld-musl-aarch64.so.1 ]; then echo "musl" else echo "gnu" fi } # Detect platform detect_platform() { local os arch os="$(uname -s)" arch="$(uname -m)" case "$os" in Darwin) os="darwin" ;; Linux) os="linux" ;; MINGW*|MSYS*|CYGWIN*) os="win32" ;; *) error "Unsupported operating system: $os" ;; esac case "$arch" in x86_64|amd64) arch="x64" ;; arm64|aarch64) arch="arm64" ;; *) error "Unsupported architecture: $arch" ;; esac # For Linux, append libc type to distinguish gnu vs musl if [ "$os" = "linux" ]; then local libc libc=$(detect_libc) echo "${os}-${arch}-${libc}" else echo "${os}-${arch}" fi } # Check for required commands check_requirements() { local missing=() if ! command -v curl &> /dev/null; then missing+=("curl") fi if ! command -v tar &> /dev/null; then missing+=("tar") fi if [ ${#missing[@]} -ne 0 ]; then error "Missing required commands: ${missing[*]}" fi } # Fetch package metadata from npm registry (cached for reuse) # Uses VP_VERSION to fetch the correct version's metadata PACKAGE_METADATA="" fetch_package_metadata() { if [ -z "$PACKAGE_METADATA" ]; then local version_path metadata_url if [ "$VP_VERSION" = "latest" ]; then version_path="latest" else version_path="$VP_VERSION" fi metadata_url="${NPM_REGISTRY}/vite-plus/${version_path}" PACKAGE_METADATA=$(curl_with_error_handling -s "$metadata_url") if [ -z "$PACKAGE_METADATA" ]; then error "Failed to fetch package metadata from: $metadata_url" fi # Check for npm registry error response # npm can return either {"error":"..."} or a plain JSON string like "version not found: test" if echo "$PACKAGE_METADATA" | grep -q '"error"'; then local error_msg error_msg=$(echo "$PACKAGE_METADATA" | grep -o '"error" *: *"[^"]*"' | cut -d'"' -f4) error "Failed to fetch version '${version_path}': ${error_msg:-unknown error}\n URL: $metadata_url" fi # Check if response is a plain error string (not a valid package object) # Use '"version":' to match JSON property, not just the word "version" if ! echo "$PACKAGE_METADATA" | grep -q '"version" *:'; then # Remove surrounding quotes from the error message if present local error_msg error_msg=$(echo "$PACKAGE_METADATA" | sed 's/^"//;s/"$//') error "Failed to fetch version '${version_path}': ${error_msg:-unknown error}\n URL: $metadata_url" fi fi # PACKAGE_METADATA is set as a global variable, no need to echo } # Get the version from package metadata # Sets RESOLVED_VERSION global variable get_version_from_metadata() { # Call fetch_package_metadata to populate PACKAGE_METADATA global # Don't use command substitution as it would swallow the exit from error() fetch_package_metadata RESOLVED_VERSION=$(echo "$PACKAGE_METADATA" | grep -o '"version" *: *"[^"]*"' | head -1 | cut -d'"' -f4) if [ -z "$RESOLVED_VERSION" ]; then error "Failed to extract version from package metadata" fi } # Get platform suffix for CLI package download # Sets PLATFORM_SUFFIX global variable # Platform format from detect_platform(): darwin-arm64, darwin-x64, linux-x64-gnu, linux-arm64-gnu, win32-x64, etc. # CLI package format: @voidzero-dev/vite-plus-cli-darwin-arm64, @voidzero-dev/vite-plus-cli-linux-x64-gnu, etc. get_platform_suffix() { local platform="$1" case "$platform" in win32-*) PLATFORM_SUFFIX="${platform}-msvc" ;; # Windows needs -msvc suffix *) PLATFORM_SUFFIX="$platform" ;; # macOS/Linux map directly esac } # Download and extract file (silent mode - no progress bar) download_and_extract() { local url="$1" local dest_dir="$2" local strip_components="$3" local filter="$4" # Download to temp file (silent mode) local temp_file temp_file=$(mktemp) # Run curl and capture exit code for error handling set +e curl -sL "$url" -o "$temp_file" local exit_code=$? set -e if [ $exit_code -ne 0 ]; then rm -f "$temp_file" print_curl_error "$exit_code" "$url" fi if [ -n "$filter" ]; then tar xzf "$temp_file" -C "$dest_dir" --strip-components="$strip_components" "$filter" 2>/dev/null || \ tar xzf "$temp_file" -C "$dest_dir" --strip-components="$strip_components" else tar xzf "$temp_file" -C "$dest_dir" --strip-components="$strip_components" fi rm -f "$temp_file" } join_by() { local separator="$1" shift local result="" local item for item in "$@"; do if [ -z "$result" ]; then result="$item" else result="${result}${separator}${item}" fi done printf '%s\n' "$result" } abbreviate_path() { local path="$1" if [ "${path#"$HOME"}" != "$path" ]; then printf '~%s\n' "${path#"$HOME"}" else printf '%s\n' "$path" fi } record_shell_summary() { local shell_name="$1" local status="$2" SHELL_CONFIG_SUMMARY+=(" - ${shell_name}: ${status}") } # Add a sourcing line to an existing shell config file. # Returns: 0 = line added, 1 = file missing, 2 = already configured, 3 = failed append_source_to_file() { local shell_config="$1" local source_line="$2" shift 2 local search_patterns=("$@") local pattern if [ ! -f "$shell_config" ]; then return 1 fi if [ ! -w "$shell_config" ]; then warn "Cannot write to $shell_config (permission denied), skipping." return 3 fi for pattern in "${search_patterns[@]}"; do if grep -Fq "$pattern" "$shell_config" 2>/dev/null; then return 2 fi done echo "" >> "$shell_config" echo "# Vite+ bin (https://viteplus.dev)" >> "$shell_config" echo "$source_line" >> "$shell_config" return 0 } # Create or update an installer-managed snippet file. # Returns: 0 = written, 2 = already configured, 3 = failed write_managed_snippet() { local snippet_file="$1" local snippet_content="$2" local snippet_dir snippet_dir=$(dirname "$snippet_file") if ! mkdir -p "$snippet_dir" 2>/dev/null; then warn "Cannot create $snippet_dir, skipping." return 3 fi if [ -f "$snippet_file" ] && [ ! -w "$snippet_file" ]; then warn "Cannot write to $snippet_file (permission denied), skipping." return 3 fi if [ -f "$snippet_file" ] && printf '%s' "$snippet_content" | cmp -s - "$snippet_file"; then return 2 fi if ! printf '%s' "$snippet_content" > "$snippet_file"; then warn "Cannot write to $snippet_file, skipping." return 3 fi return 0 } # Discover Nushell's preferred user-local vendor autoload directory. # Nushell puts the user-local directory at the end of the list. discover_nushell_vendor_autoload_dir() { command -v nu > /dev/null 2>&1 || return 1 local nu_dirs_output nu_dirs_output=$(nu -c '$nu.vendor-autoload-dirs | reverse | each {|dir| $dir } | str join (char nl)' 2>/dev/null) || return 1 while IFS= read -r dir; do [ -n "$dir" ] || continue printf '%s\n' "$dir" return 0 done </dev/null; then warn "Cannot create $zsh_dir, skipping zsh." SHELL_CONFIG_HAS_FAILURE="true" SHELL_CONFIG_FAILED_SHELLS+=("zsh") record_shell_summary "zsh" "failed (could not create $(abbreviate_path "$zsh_dir"))" return fi if [ ! -f "$zshenv" ] && ! touch "$zshenv" 2>/dev/null; then warn "Cannot create $zshenv, skipping zsh." SHELL_CONFIG_HAS_FAILURE="true" SHELL_CONFIG_FAILED_SHELLS+=("zsh") record_shell_summary "zsh" "failed (could not create $(abbreviate_path "$zshenv"))" return fi result=0 append_source_to_file "$zshenv" ". \"$INSTALL_DIR_REF_POSIX/env\"" "$INSTALL_DIR/env" "$INSTALL_DIR_REF_POSIX/env" || result=$? case "$result" in 0) updated+=("$(abbreviate_path "$zshenv")") ;; 2) already+=("$(abbreviate_path "$zshenv")") ;; 3) failed+=("$(abbreviate_path "$zshenv")") ;; esac if [ -f "$zshrc" ]; then result=0 append_source_to_file "$zshrc" ". \"$INSTALL_DIR_REF_POSIX/env\"" "$INSTALL_DIR/env" "$INSTALL_DIR_REF_POSIX/env" || result=$? case "$result" in 0) updated+=("$(abbreviate_path "$zshrc")") ;; 2) already+=("$(abbreviate_path "$zshrc")") ;; 3) failed+=("$(abbreviate_path "$zshrc")") ;; esac fi local details=() if [ ${#updated[@]} -gt 0 ]; then SHELL_CONFIG_HAS_UPDATED="true" SHELL_CONFIG_HAS_CONFIGURED="true" details+=("updated $(join_by ', ' "${updated[@]}")") fi if [ ${#already[@]} -gt 0 ]; then SHELL_CONFIG_HAS_CONFIGURED="true" details+=("already configured $(join_by ', ' "${already[@]}")") fi if [ ${#failed[@]} -gt 0 ]; then SHELL_CONFIG_HAS_FAILURE="true" SHELL_CONFIG_FAILED_SHELLS+=("zsh") details+=("failed $(join_by ', ' "${failed[@]}")") fi if [ ${#details[@]} -eq 0 ]; then record_shell_summary "zsh" "skipped" else record_shell_summary "zsh" "$(join_by '; ' "${details[@]}")" fi } configure_bash_path() { local updated=() local already=() local failed=() local existing=0 local file result for file in "$HOME/.bash_profile" "$HOME/.bashrc" "$HOME/.profile"; do if [ ! -f "$file" ]; then continue fi existing=1 result=0 append_source_to_file "$file" ". \"$INSTALL_DIR_REF_POSIX/env\"" "$INSTALL_DIR/env" "$INSTALL_DIR_REF_POSIX/env" || result=$? case "$result" in 0) updated+=("$(abbreviate_path "$file")") ;; 2) already+=("$(abbreviate_path "$file")") ;; 3) failed+=("$(abbreviate_path "$file")") ;; esac done if [ "$existing" -eq 0 ]; then record_shell_summary "bash" "skipped (no existing rc files)" return fi local details=() if [ ${#updated[@]} -gt 0 ]; then SHELL_CONFIG_HAS_UPDATED="true" SHELL_CONFIG_HAS_CONFIGURED="true" details+=("updated $(join_by ', ' "${updated[@]}")") fi if [ ${#already[@]} -gt 0 ]; then SHELL_CONFIG_HAS_CONFIGURED="true" details+=("already configured $(join_by ', ' "${already[@]}")") fi if [ ${#failed[@]} -gt 0 ]; then SHELL_CONFIG_HAS_FAILURE="true" SHELL_CONFIG_FAILED_SHELLS+=("bash") details+=("failed $(join_by ', ' "${failed[@]}")") fi record_shell_summary "bash" "$(join_by '; ' "${details[@]}")" } configure_fish_path() { local fish_config="${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d/vite-plus.fish" local fish_content="# Vite+ bin (https://viteplus.dev) source \"$INSTALL_DIR_REF_POSIX/env.fish\" " local result=0 write_managed_snippet "$fish_config" "$fish_content" || result=$? case "$result" in 0) SHELL_CONFIG_HAS_UPDATED="true" SHELL_CONFIG_HAS_CONFIGURED="true" record_shell_summary "fish" "updated $(abbreviate_path "$fish_config")" ;; 2) SHELL_CONFIG_HAS_CONFIGURED="true" record_shell_summary "fish" "already configured $(abbreviate_path "$fish_config")" ;; *) SHELL_CONFIG_HAS_FAILURE="true" SHELL_CONFIG_FAILED_SHELLS+=("fish") record_shell_summary "fish" "failed $(abbreviate_path "$fish_config")" ;; esac } configure_nushell_path() { local nushell_dir nushell_dir=$(discover_nushell_vendor_autoload_dir 2>/dev/null) || true if [ -z "$nushell_dir" ]; then SHELL_CONFIG_HAS_FAILURE="true" SHELL_CONFIG_FAILED_SHELLS+=("nushell") record_shell_summary "nushell" "failed (could not determine vendor autoload dir)" return fi local nushell_autoload="$nushell_dir/vite-plus.nu" local nushell_content="# Vite+ bin (https://viteplus.dev) source '$INSTALL_DIR_REF_NU/env.nu' " local result=0 write_managed_snippet "$nushell_autoload" "$nushell_content" || result=$? case "$result" in 0) SHELL_CONFIG_HAS_UPDATED="true" SHELL_CONFIG_HAS_CONFIGURED="true" record_shell_summary "nushell" "updated $(abbreviate_path "$nushell_autoload")" ;; 2) SHELL_CONFIG_HAS_CONFIGURED="true" record_shell_summary "nushell" "already configured $(abbreviate_path "$nushell_autoload")" ;; *) SHELL_CONFIG_HAS_FAILURE="true" SHELL_CONFIG_FAILED_SHELLS+=("nushell") record_shell_summary "nushell" "failed $(abbreviate_path "$nushell_autoload")" ;; esac } # Configure supported shell PATH integrations for all installed shells. configure_shell_path() { SHELL_CONFIG_SUMMARY=() SHELL_CONFIG_FAILED_SHELLS=() SHELL_CONFIG_HAS_UPDATED="false" SHELL_CONFIG_HAS_CONFIGURED="false" SHELL_CONFIG_HAS_FAILURE="false" if command -v zsh > /dev/null 2>&1; then configure_zsh_path else record_shell_summary "zsh" "skipped (not installed)" fi if command -v bash > /dev/null 2>&1; then configure_bash_path else record_shell_summary "bash" "skipped (not installed)" fi if command -v fish > /dev/null 2>&1; then configure_fish_path else record_shell_summary "fish" "skipped (not installed)" fi if command -v nu > /dev/null 2>&1; then configure_nushell_path else record_shell_summary "nushell" "skipped (not installed)" fi } # Run vp env setup --refresh, showing output only on failure # Arguments: vp_bin - path to the vp binary refresh_shims() { local vp_bin="$1" local setup_output if ! setup_output=$("$vp_bin" env setup --refresh 2>&1); then warn "Failed to refresh shims:" echo "$setup_output" >&2 fi } # Setup Node.js version manager (node/npm/npx shims) # Sets NODE_MANAGER_ENABLED global # Arguments: bin_dir - path to the version's bin directory containing vp setup_node_manager() { local bin_dir="$1" local bin_path="$INSTALL_DIR/bin" NODE_MANAGER_ENABLED="false" # Resolve vp binary name (vp on Unix, vp.exe on Windows) local vp_bin="$bin_dir/vp" if [ -f "$bin_dir/vp.exe" ]; then vp_bin="$bin_dir/vp.exe" fi # Explicit override via environment variable if [ "$VP_NODE_MANAGER" = "yes" ]; then refresh_shims "$vp_bin" NODE_MANAGER_ENABLED="true" return 0 elif [ "$VP_NODE_MANAGER" = "no" ]; then NODE_MANAGER_ENABLED="false" return 0 fi # Check if Vite+ is already managing Node.js (bin/node or bin/node.exe exists) if [ -e "$bin_path/node" ] || [ -e "$bin_path/node.exe" ]; then refresh_shims "$vp_bin" NODE_MANAGER_ENABLED="already" return 0 fi # Auto-enable on CI or devcontainer environments # CI: standard CI environment variable (GitHub Actions, Travis, CircleCI, etc.) # CODESPACES: set by GitHub Codespaces (https://docs.github.com/en/codespaces) # REMOTE_CONTAINERS: set by VS Code Dev Containers extension # DEVPOD: set by DevPod (https://devpod.sh) if [ -n "$CI" ] || [ -n "$CODESPACES" ] || [ -n "$REMOTE_CONTAINERS" ] || [ -n "$DEVPOD" ]; then refresh_shims "$vp_bin" NODE_MANAGER_ENABLED="true" return 0 fi # Check if node is available on the system local node_available="false" if command -v node &> /dev/null; then node_available="true" fi # Auto-enable if no node available on system if [ "$node_available" = "false" ]; then refresh_shims "$vp_bin" NODE_MANAGER_ENABLED="true" return 0 fi # Prompt user in interactive mode if [ -e /dev/tty ] && [ -t 1 ]; then echo "" echo "Would you like Vite+ to manage your Node.js versions?" echo "It adds \`node\`, \`npm\`, and \`npx\` shims to ~/.vite-plus/bin/ and automatically uses the right version." echo "Opt out anytime with \`vp env off\`." echo -n "Press Enter to accept (Y/n): " read -r response < /dev/tty if [ -z "$response" ] || [ "$response" = "y" ] || [ "$response" = "Y" ]; then refresh_shims "$vp_bin" NODE_MANAGER_ENABLED="true" fi fi } # Cleanup old versions, keeping only the most recent ones cleanup_old_versions() { local max_versions=5 local versions=() # List version directories (semver format like 0.1.0, 1.2.3-beta.1, 0.0.0-f48af939.20260205-0533) # This excludes 'current' symlink and non-semver directories like 'local-dev' local semver_regex='^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$' for dir in "$INSTALL_DIR"/*/; do local name name=$(basename "$dir") if [ -d "$dir" ] && [[ "$name" =~ $semver_regex ]]; then versions+=("$dir") fi done local count=${#versions[@]} if [ "$count" -le "$max_versions" ]; then return 0 fi # Sort by creation time (oldest first) and delete excess local sorted_versions if [[ "$OSTYPE" == "darwin"* ]]; then # macOS: use stat -f %B for birth time sorted_versions=$(for v in "${versions[@]}"; do echo "$(stat -f %B "$v") $v" done | sort -n | head -n $((count - max_versions)) | cut -d' ' -f2-) else # Linux: use stat -c %W for birth time, fallback to %Y (mtime) sorted_versions=$(for v in "${versions[@]}"; do local btime btime=$(stat -c %W "$v" 2>/dev/null) if [ "$btime" = "0" ] || [ -z "$btime" ]; then btime=$(stat -c %Y "$v") fi echo "$btime $v" done | sort -n | head -n $((count - max_versions)) | cut -d' ' -f2-) fi # Delete oldest versions (silently) for old_version in $sorted_versions; do rm -rf "$old_version" done } main() { echo "" echo -e "Setting up VITE+..." check_requirements local platform platform=$(detect_platform) # Local development mode: use local tgz if [ -n "$LOCAL_TGZ" ]; then # Validate local tgz if [ ! -f "$LOCAL_TGZ" ]; then error "Local tarball not found: $LOCAL_TGZ" fi # Use version as-is (default to "local-dev") if [ "$VP_VERSION" = "latest" ] || [ "$VP_VERSION" = "test" ]; then VP_VERSION="local-dev" fi else # Fetch package metadata and resolve version from npm get_version_from_metadata VP_VERSION="$RESOLVED_VERSION" fi # Set up version-specific directories VERSION_DIR="$INSTALL_DIR/$VP_VERSION" BIN_DIR="$VERSION_DIR/bin" CURRENT_LINK="$INSTALL_DIR/current" local binary_name="vp" if [[ "$platform" == win32* ]]; then binary_name="vp.exe" fi # Create bin directory mkdir -p "$BIN_DIR" if [ -n "$LOCAL_TGZ" ]; then # Local development mode: only need the binary info "Using local tarball: $LOCAL_TGZ" # Copy binary from LOCAL_BINARY env var (set by install-global-cli.ts) if [ -n "$LOCAL_BINARY" ]; then cp "$LOCAL_BINARY" "$BIN_DIR/$binary_name" # On Windows, also copy the trampoline shim binary if available if [[ "$platform" == win32* ]]; then local shim_src shim_src="$(dirname "$LOCAL_BINARY")/vp-shim.exe" if [ -f "$shim_src" ]; then cp "$shim_src" "$BIN_DIR/vp-shim.exe" fi fi else error "VP_LOCAL_BINARY must be set when using VP_LOCAL_TGZ" fi chmod +x "$BIN_DIR/$binary_name" else # Download from npm registry — extract only the vp binary from CLI platform package get_platform_suffix "$platform" local package_name="@voidzero-dev/vite-plus-cli-${PLATFORM_SUFFIX}" local platform_url="${NPM_REGISTRY}/${package_name}/-/vite-plus-cli-${PLATFORM_SUFFIX}-${VP_VERSION}.tgz" # Create temp directory for extraction local platform_temp_dir platform_temp_dir=$(mktemp -d) download_and_extract "$platform_url" "$platform_temp_dir" 1 # Copy binary to BIN_DIR cp "$platform_temp_dir/$binary_name" "$BIN_DIR/" chmod +x "$BIN_DIR/$binary_name" # On Windows, also copy the trampoline shim binary if present in the package if [[ "$platform" == win32* ]] && [ -f "$platform_temp_dir/vp-shim.exe" ]; then cp "$platform_temp_dir/vp-shim.exe" "$BIN_DIR/" fi rm -rf "$platform_temp_dir" fi # Generate wrapper package.json that declares vite-plus as a dependency. # pnpm will install vite-plus and all transitive deps via `vp install`. # The packageManager field pins pnpm to a known-good version, ensuring # consistent behavior regardless of the user's global pnpm version. cat > "$VERSION_DIR/package.json" < "$install_log" 2>&1); then if is_release_age_error "$install_log"; then if confirm_release_age_override; then # Write the override only after explicit consent, then retry once. write_release_age_override if ! (cd "$VERSION_DIR" && CI=true "$vp_install_bin" install > "$install_log" 2>&1); then print_install_failure "$install_log" exit 1 fi else print_release_age_failure "$install_log" exit 1 fi else print_install_failure "$install_log" exit 1 fi fi fi # Create/update current symlink (use relative path for portability) ln -sfn "$VP_VERSION" "$CURRENT_LINK" # Create bin directory and vp entrypoint (always done) mkdir -p "$INSTALL_DIR/bin" if [[ "$platform" == win32* ]]; then # Windows: copy trampoline as vp.exe (matching install.ps1) if [ -f "$INSTALL_DIR/current/bin/vp-shim.exe" ]; then cp "$INSTALL_DIR/current/bin/vp-shim.exe" "$INSTALL_DIR/bin/vp.exe" fi else # Unix: symlink to current/bin/vp ln -sf "../current/bin/vp" "$INSTALL_DIR/bin/vp" fi # Cleanup old versions cleanup_old_versions # Create env files with PATH guard (prevents duplicate PATH entries) # Use current/bin/vp directly (the real binary) instead of bin/vp (trampoline) # to avoid the self-overwrite issue on Windows during --refresh local vp_bin="$INSTALL_DIR/current/bin/vp" if [[ "$platform" == win32* ]]; then vp_bin="$INSTALL_DIR/current/bin/vp.exe" fi "$vp_bin" env setup --env-only > /dev/null # Configure shell PATH (always attempted) configure_shell_path # Setup Node.js version manager (shims) - separate component setup_node_manager "$BIN_DIR" # Use ~ shorthand if install dir is under HOME, otherwise show full path local display_dir="${INSTALL_DIR/#$HOME/~}" local display_location="${display_dir}/bin" # Print success message echo "" echo -e "${GREEN}✔${NC} ${BOLD_BRIGHT_BLUE}VITE+${NC} successfully installed!" echo "" echo " The Unified Toolchain for the Web." echo "" echo -e " ${BOLD}Get started:${NC}" echo -e " ${BRIGHT_BLUE}vp create${NC} Create a new project" echo -e " ${BRIGHT_BLUE}vp env${NC} Manage Node.js versions" echo -e " ${BRIGHT_BLUE}vp install${NC} Install dependencies" echo -e " ${BRIGHT_BLUE}vp migrate${NC} Migrate to Vite+" if [ "$NODE_MANAGER_ENABLED" = "true" ] || [ "$NODE_MANAGER_ENABLED" = "already" ]; then echo "" echo -e " Vite+ is now managing Node.js via ${BRIGHT_BLUE}vp env${NC}." echo -e " Run ${BRIGHT_BLUE}vp env doctor${NC} to verify your setup, or ${BRIGHT_BLUE}vp env off${NC} to opt out." fi echo "" echo -e " Run ${BRIGHT_BLUE}vp help${NC} to see available commands." echo "" echo " Shell configuration:" local summary_line for summary_line in "${SHELL_CONFIG_SUMMARY[@]}"; do echo "$summary_line" done # Show restart note if any shell config was updated if [ "$SHELL_CONFIG_HAS_UPDATED" = "true" ]; then echo "" echo " Note: Restart your terminal to load updated shell configuration." fi # Show manual PATH instructions if no shell was configured or any shell failed if [ "$SHELL_CONFIG_HAS_CONFIGURED" = "false" ] || [ "$SHELL_CONFIG_HAS_FAILURE" = "true" ]; then echo "" echo -e " ${YELLOW}note${NC}: Some shells still need manual setup." echo "" echo -e " vp was installed to: ${BOLD}${display_location}${NC}" echo "" echo " Manual setup instructions:" echo " - Bash/Zsh: add the following to your shell config (~/.bashrc, ~/.zshrc, etc.):" echo " . \"$INSTALL_DIR_REF_POSIX/env\"" echo " - Fish: create ${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d/vite-plus.fish with:" echo " source \"$INSTALL_DIR_REF_POSIX/env.fish\"" echo " - Nushell: create a vendor autoload file with:" echo " source '$INSTALL_DIR_REF_NU/env.nu'" echo "" echo " Or run vp directly:" echo "" echo -e " ${display_location}/vp" fi echo "" } main "$@"