From f3f662638581011570db901c0c604d797c4cf9af Mon Sep 17 00:00:00 2001 From: Irfan Hardiyanto <52022757+devhardiyanto@users.noreply.github.com> Date: Tue, 14 Jul 2026 01:46:03 +0700 Subject: [PATCH 1/3] fix(windows): drop stale restart hint, trim use output, guard install version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sprint 10 UX feedback pass, phase 1 (tasks 1, 2, 6 of the plan). - upgrade: remove "Restart your terminal" — phpvm.ps1 is re-executed in full on every invocation, so the next command already runs the new code. Consistent with B1 (1.8.2). The Linux counterpart stays: phpvm.sh is sourced into the shell and genuinely needs a re-source. - use: print only the first line of `php --version`, matching Linux. `current` keeps the full banner on both OSes — that parity was already correct. - install: reject anything that is not a full x.y.z before Resolve-PHPURL. `phpvm install composer` used to reach Get-VSVersion's [int] cast and leak a raw PowerShell exception; it now prints "Invalid version" plus a hint toward `phpvm composer`. Same guard added on Linux, where the input did not crash but failed later as a misleading "Download failed". devhardiyanto --- linux/phpvm.sh | 8 +++++++ tests/linux/commands.bats | 22 +++++++++++++++++ tests/windows/Install.Tests.ps1 | 42 +++++++++++++++++++++++++++++++++ windows/phpvm.ps1 | 11 +++++++-- 4 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 tests/windows/Install.Tests.ps1 diff --git a/linux/phpvm.sh b/linux/phpvm.sh index d13a381..892bf1b 100644 --- a/linux/phpvm.sh +++ b/linux/phpvm.sh @@ -349,6 +349,14 @@ phpvm_install() { fi fi + # Reject non-versions up front; otherwise they only fail later as a confusing + # "Download failed" from php.net. + if [[ ! "$ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + _err "Invalid version '$ver'. Usage: phpvm install (e.g. phpvm install 8.3.0)" + [[ "$ver" == "composer" ]] && _dim "Did you mean: phpvm composer" + return 1 + fi + local target="$PHPVM_VERSIONS/$ver" if [[ -d "$target" ]]; then diff --git a/tests/linux/commands.bats b/tests/linux/commands.bats index fb0a900..e5d4d24 100644 --- a/tests/linux/commands.bats +++ b/tests/linux/commands.bats @@ -234,3 +234,25 @@ EOF [ "$status" -eq 0 ] [ "$output" = "8.3.5" ] } + +# ---------- phpvm_install version guard ---------- + +@test "install: rejects a non-version argument before touching the network" { + run phpvm_install composer + [ "$status" -ne 0 ] + [[ "$output" == *"Invalid version 'composer'"* ]] + [[ "$output" == *"Did you mean: phpvm composer"* ]] +} + +@test "install: rejects a malformed version" { + run phpvm_install 8.3.x + [ "$status" -ne 0 ] + [[ "$output" == *"Invalid version"* ]] +} + +@test "install: full x.y.z passes the guard (already-installed path)" { + _fake_php_install 8.3.0 + run phpvm_install 8.3.0 + [ "$status" -eq 0 ] + [[ "$output" == *"already installed"* ]] +} diff --git a/tests/windows/Install.Tests.ps1 b/tests/windows/Install.Tests.ps1 new file mode 100644 index 0000000..6a2d8f3 --- /dev/null +++ b/tests/windows/Install.Tests.ps1 @@ -0,0 +1,42 @@ +Describe 'Invoke-Install version guard' { + BeforeAll { + . $PSScriptRoot/Common.ps1 + } + + It 'Rejects a non-version argument instead of throwing' { + Mock -CommandName Write-Err -MockWith {} + Mock -CommandName Write-Dim -MockWith {} + Mock -CommandName Resolve-PHPURL -MockWith { throw 'must not be reached' } + + { Invoke-Install 'composer' } | Should -Not -Throw + + Should -Invoke Write-Err -Times 1 -ParameterFilter { + $m -like "Invalid version 'composer'*" + } + Should -Invoke Resolve-PHPURL -Times 0 + } + + It 'Hints at the composer command on that specific typo' { + Mock -CommandName Write-Err -MockWith {} + Mock -CommandName Write-Dim -MockWith {} + Mock -CommandName Resolve-PHPURL -MockWith { throw 'must not be reached' } + + Invoke-Install 'composer' + + Should -Invoke Write-Dim -Times 1 -ParameterFilter { + $m -like '*phpvm composer*' + } + } + + It 'Rejects a malformed version' { + Mock -CommandName Write-Err -MockWith {} + Mock -CommandName Resolve-PHPURL -MockWith { throw 'must not be reached' } + + { Invoke-Install '8.3.x' } | Should -Not -Throw + + Should -Invoke Write-Err -Times 1 -ParameterFilter { + $m -like 'Invalid version*' + } + Should -Invoke Resolve-PHPURL -Times 0 + } +} diff --git a/windows/phpvm.ps1 b/windows/phpvm.ps1 index 98845fc..b86232e 100644 --- a/windows/phpvm.ps1 +++ b/windows/phpvm.ps1 @@ -329,6 +329,14 @@ function Invoke-Install ([string]$ver) { $ver = $resolved } + # Anything that isn't a full x.y.z here would blow up later in Get-VSVersion's + # [int] cast with a raw PowerShell exception. + if ($ver -notmatch '^\d+\.\d+\.\d+$') { + Write-Err "Invalid version '$ver'. Usage: phpvm install (e.g. phpvm install 8.3.0)" + if ($ver -eq "composer") { Write-Dim "Did you mean: phpvm composer" } + return + } + $targetDir = "$VERSIONS_DIR\$ver" if (Test-Path $targetDir) { Write-Warn "PHP $ver is already installed. Run: phpvm use $ver" @@ -433,7 +441,7 @@ function Invoke-Use ([string]$ver) { Write-Ok "Now using PHP $ver" try { - & "$CURRENT_LINK\php.exe" --version 2>$null | ForEach-Object { Write-Host " $_" } + & "$CURRENT_LINK\php.exe" --version 2>$null | Select-Object -First 1 | ForEach-Object { Write-Host " $_" } } catch { return } @@ -1330,7 +1338,6 @@ function Invoke-Upgrade { Invoke-WebRequest -Uri $scriptUrl -OutFile $scriptDest -UseBasicParsing Unblock-File $scriptDest Write-Ok "phpvm upgraded to $latest!" - Write-Dim "Restart your terminal to use the new version." } catch { Write-Err "Upgrade failed: $_" Copy-Item $backup $scriptDest -Force From a21210a748e735e6e639c2a3e0f5eeb906be4ff2 Mon Sep 17 00:00:00 2001 From: Irfan Hardiyanto <52022757+devhardiyanto@users.noreply.github.com> Date: Tue, 14 Jul 2026 02:16:13 +0700 Subject: [PATCH 2/3] feat(windows,linux): live install progress and an install --no-use opt-out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sprint 10 UX feedback pass, phases 2 and 3. Bumps 1.8.3 -> 1.9.0. Progress (phase 2). Both sides went quiet for minutes during an install with no sign of life. Modelled on fnm's approach (indicatif over the response stream): byte-level progress, drawn on stderr so stdout stays pipe-clean, and only when the content length is known. - windows: Invoke-Download streams the response in chunks and prints a throttled percent / size / speed / ETA line, replacing $ProgressPreference=SilentlyContinue. Payloads under 5 MB (Xdebug DLL, ext zips) stay silent — a bar there is flicker. Falls back to Invoke-WebRequest when the size is unknown. - linux: new _phpvm_run_logged wraps configure / make / make install with a spinner plus elapsed time, and reports the real exit code. A trap takes the build down on Ctrl+C instead of orphaning it. - Both auto-disable when stderr is not a terminal, so CI and the test suites see the output they saw before. --no-use (phase 3). Auto-use on install (1.8.0) is kept as the default; this only adds an opt-out, so nobody's existing habit breaks. Accepted in either argument position on both OSes; usage, error and hint strings are identical across them. devhardiyanto --- README.md | 9 +++- linux/install.sh | 2 +- linux/phpvm.sh | 76 ++++++++++++++++++++++---- tests/linux/commands.bats | 54 +++++++++++++++++++ tests/windows/Install.Tests.ps1 | 35 ++++++++++++ version.txt | 2 +- windows/install.ps1 | 2 +- windows/phpvm.ps1 | 96 ++++++++++++++++++++++++++++++--- 8 files changed, 256 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 6805b30..e40af01 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ phpvm install 8.3.0 # install a specific PHP version phpvm install 8.3 # install latest 8.3.x patch (auto-resolves) phpvm install 8 # install latest 8.x patch (e.g. 8.5.x) phpvm install 7.4 # works for older lines (7.x, 5.x) +phpvm install 8.4.16 --no-use # install but keep the current version active phpvm use 8.3.0 # switch active version phpvm list # list installed versions phpvm current # show active version @@ -77,7 +78,13 @@ phpvm ini # open php.ini in editor ``` A successful `phpvm install` automatically activates the freshly installed -version, so you can skip a separate `phpvm use` for the common case. +version, so you can skip a separate `phpvm use` for the common case. Pass +`--no-use` to install a version in the background without switching to it. + +Long installs report live progress: a download bar on Windows, and a spinner +with elapsed time over the `configure` / `make` / `make install` steps on Linux. +Both are drawn on stderr and are suppressed automatically when output is not a +terminal, so piping and CI logs stay clean. ### Auto-Switch with `.phpvmrc` (Windows) diff --git a/linux/install.sh b/linux/install.sh index 9a0cc4f..e715912 100644 --- a/linux/install.sh +++ b/linux/install.sh @@ -6,7 +6,7 @@ set -e -PHPVM_VERSION="1.8.3" +PHPVM_VERSION="1.9.0" PHPVM_DIR="${PHPVM_DIR:-$HOME/.phpvm}" PHPVM_REPO="https://raw.githubusercontent.com/devhardiyanto/phpvm/main" diff --git a/linux/phpvm.sh b/linux/phpvm.sh index 892bf1b..9a52326 100644 --- a/linux/phpvm.sh +++ b/linux/phpvm.sh @@ -10,7 +10,7 @@ # phpvm use 8.3.0 # ============================================================================== -PHPVM_VERSION="1.8.3" +PHPVM_VERSION="1.9.0" PHPVM_DIR="${PHPVM_DIR:-$HOME/.phpvm}" PHPVM_VERSIONS="$PHPVM_DIR/versions" PHPVM_CURRENT="$PHPVM_DIR/current" @@ -297,6 +297,52 @@ _phpvm_cpus() { nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 2 } +# Run a long build step with its output appended to $PHPVM_LOG, showing a live +# spinner + elapsed time so a multi-minute `make` never looks hung. +# _phpvm_run_logged "Building with 8 cores" make -j8 +# The spinner is drawn on stderr and only when stderr is a terminal, so CI and +# the bats suite see exactly the output they saw before. Returns the real exit +# code of the command. +_phpvm_run_logged() { + local label="$1"; shift + + if [[ ! -t 2 ]]; then + _step "$label ..." + "$@" >> "$PHPVM_LOG" 2>&1 + return $? + fi + + "$@" >> "$PHPVM_LOG" 2>&1 & + local pid=$! + + # Ctrl+C must take the build down with us, not orphan it. + trap 'kill "$pid" 2>/dev/null; printf "\r\033[K" >&2; trap - INT; return 130' INT + + # Plain ASCII frames + explicit index: ${var:i++:1} is a bashism that bites + # in zsh, and Unicode braille spinners mangle on older terminals. + local frames='|/-\' + local i=0 start=$SECONDS elapsed frame + while kill -0 "$pid" 2>/dev/null; do + elapsed=$((SECONDS - start)) + frame=${frames:$i:1} + i=$(( (i + 1) % 4 )) + printf '\r\033[36m %s %s ... %dm%02ds\033[0m' \ + "$frame" "$label" $((elapsed / 60)) $((elapsed % 60)) >&2 + sleep 0.5 + done + + wait "$pid" + local rc=$? + trap - INT + printf '\r\033[K' >&2 + + elapsed=$((SECONDS - start)) + if [[ $rc -eq 0 ]]; then + _step "$label ... done in $((elapsed / 60))m$((elapsed % 60))s" + fi + return $rc +} + # Resolve a partial version to the highest published patch on php.net. # "8" -> latest 8.x (e.g. 8.5.7) # "8.3" -> latest 8.3.x (e.g. 8.3.31) @@ -332,8 +378,16 @@ _phpvm_resolve_remote() { # phpvm install # ============================================================================== phpvm_install() { - local ver="$1" - [[ -z "$ver" ]] && { _err "Usage: phpvm install (e.g. phpvm install 8.3.0)"; return 1; } + local ver="" no_use=0 + while [[ $# -gt 0 ]]; do + case "$1" in + --no-use) no_use=1 ;; + -*) _err "Unknown option: $1. Usage: phpvm install [--no-use]"; return 1 ;; + *) [[ -z "$ver" ]] && ver="$1" ;; + esac + shift + done + [[ -z "$ver" ]] && { _err "Usage: phpvm install [--no-use] (e.g. phpvm install 8.3.0)"; return 1; } # Partial version: "8" -> latest 8.x, "8.3" -> latest 8.3.x. if [[ "$ver" =~ ^[0-9]+$ || "$ver" =~ ^[0-9]+\.[0-9]+$ ]]; then @@ -401,7 +455,6 @@ phpvm_install() { tar -xzf "$cache_file" -C "$PHPVM_CACHE" # Configure - _step "Configuring (this may take a minute) ..." mkdir -p "$target" local configure_opts=( @@ -446,7 +499,7 @@ phpvm_install() { return 1 } ./buildconf --force &>>"$PHPVM_LOG" 2>&1 || true # needed only for git checkouts - ./configure "${configure_opts[@]}" >> "$PHPVM_LOG" 2>&1 || { + _phpvm_run_logged "Configuring" ./configure "${configure_opts[@]}" || { _err "Configure failed. See log: $PHPVM_LOG" rm -rf "$target" return 1 @@ -455,16 +508,14 @@ phpvm_install() { # Build local cpus cpus=$(_phpvm_cpus) - _step "Building with $cpus cores (grab a coffee, this takes a few minutes) ..." - make -j"$cpus" >> "$PHPVM_LOG" 2>&1 || { + _phpvm_run_logged "Building with $cpus cores" make -j"$cpus" || { _err "Build failed. See log: $PHPVM_LOG" rm -rf "$target" return 1 } # Install - _step "Installing ..." - make install >> "$PHPVM_LOG" 2>&1 || { + _phpvm_run_logged "Installing" make install || { _err "Install failed. See log: $PHPVM_LOG" rm -rf "$target" return 1 @@ -484,7 +535,11 @@ phpvm_install() { _ok "PHP $ver installed successfully." - # Activate the freshly built version right away (point 4 — auto-use). + # Activate the freshly built version right away, unless opted out. + if [[ $no_use -eq 1 ]]; then + _dim "Not switching (--no-use). Run: phpvm use $ver" + return 0 + fi phpvm_use "$ver" } @@ -1069,6 +1124,7 @@ phpvm_help() { VERSION MANAGEMENT phpvm install Build & install a PHP version + --no-use install without switching to it phpvm use Switch the active PHP version phpvm list List installed versions phpvm current Show active version info diff --git a/tests/linux/commands.bats b/tests/linux/commands.bats index e5d4d24..6c3d86d 100644 --- a/tests/linux/commands.bats +++ b/tests/linux/commands.bats @@ -256,3 +256,57 @@ EOF [ "$status" -eq 0 ] [[ "$output" == *"already installed"* ]] } + +# ---------- _phpvm_run_logged ---------- + +@test "run_logged: returns the command's exit code" { + export PHPVM_LOG="$BATS_TEST_TMPDIR/build.log" + run _phpvm_run_logged "Failing" false + [ "$status" -ne 0 ] + run _phpvm_run_logged "Passing" true + [ "$status" -eq 0 ] +} + +@test "run_logged: appends command output to the build log, not stdout" { + export PHPVM_LOG="$BATS_TEST_TMPDIR/build.log" + : > "$PHPVM_LOG" + run _phpvm_run_logged "Echoing" echo "secret-build-noise" + [ "$status" -eq 0 ] + [[ "$output" != *"secret-build-noise"* ]] + grep -q "secret-build-noise" "$PHPVM_LOG" +} + +@test "run_logged: prints the label when stderr is not a tty (no spinner)" { + export PHPVM_LOG="$BATS_TEST_TMPDIR/build.log" + run _phpvm_run_logged "Configuring" true + [ "$status" -eq 0 ] + [[ "$output" == *"Configuring"* ]] + # The spinner frames must never reach a non-tty stream. + [[ "$output" != *$'\r'* ]] +} + +# ---------- phpvm install --no-use ---------- + +@test "install --no-use: flag is stripped, version still parsed (flag last)" { + run phpvm_install 8.3.x --no-use + [ "$status" -ne 0 ] + [[ "$output" == *"Invalid version '8.3.x'"* ]] +} + +@test "install --no-use: flag is stripped, version still parsed (flag first)" { + run phpvm_install --no-use 8.3.x + [ "$status" -ne 0 ] + [[ "$output" == *"Invalid version '8.3.x'"* ]] +} + +@test "install --no-use: rejects an unknown option" { + run phpvm_install 8.3.0 --bogus + [ "$status" -ne 0 ] + [[ "$output" == *"Unknown option: --bogus"* ]] +} + +@test "install: bare --no-use without a version still errors on usage" { + run phpvm_install --no-use + [ "$status" -ne 0 ] + [[ "$output" == *"Usage: phpvm install"* ]] +} diff --git a/tests/windows/Install.Tests.ps1 b/tests/windows/Install.Tests.ps1 index 6a2d8f3..eb6d822 100644 --- a/tests/windows/Install.Tests.ps1 +++ b/tests/windows/Install.Tests.ps1 @@ -40,3 +40,38 @@ Describe 'Invoke-Install version guard' { Should -Invoke Resolve-PHPURL -Times 0 } } + +Describe 'Invoke-Install --no-use parsing' { + BeforeAll { + . $PSScriptRoot/Common.ps1 + } + + BeforeEach { + Mock -CommandName Write-Err -MockWith {} + Mock -CommandName Write-Dim -MockWith {} + Mock -CommandName Resolve-PHPURL -MockWith { throw 'must not be reached' } + } + + # The flag must be stripped in either position, matching the Linux arg loop. + # A deliberately malformed version proves which token was taken as the version. + It 'Strips the flag when it trails the version' { + Invoke-Install '8.3.x' '--no-use' + Should -Invoke Write-Err -Times 1 -ParameterFilter { $m -like "Invalid version '8.3.x'*" } + } + + It 'Strips the flag when it leads the version' { + Invoke-Install '--no-use' '8.3.x' + Should -Invoke Write-Err -Times 1 -ParameterFilter { $m -like "Invalid version '8.3.x'*" } + } + + It 'Errors on usage when --no-use is passed with no version' { + Invoke-Install '--no-use' '' + Should -Invoke Write-Err -Times 1 -ParameterFilter { $m -like 'Usage: phpvm install*' } + } + + It 'Rejects an unknown option' { + Invoke-Install '8.3.0' '--bogus' + Should -Invoke Write-Err -Times 1 -ParameterFilter { $m -like 'Unknown option: --bogus*' } + Should -Invoke Resolve-PHPURL -Times 0 + } +} diff --git a/version.txt b/version.txt index a7ee35a..f8e233b 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.8.3 +1.9.0 diff --git a/windows/install.ps1 b/windows/install.ps1 index 96914f7..8e5fa7f 100644 --- a/windows/install.ps1 +++ b/windows/install.ps1 @@ -7,7 +7,7 @@ Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" -$PHPVM_VERSION = "1.8.3" +$PHPVM_VERSION = "1.9.0" $PHPVM_DIR = if ($env:PHPVM_DIR) { $env:PHPVM_DIR } else { "$env:USERPROFILE\.phpvm" } $PHPVM_BIN = "$PHPVM_DIR\bin" diff --git a/windows/phpvm.ps1 b/windows/phpvm.ps1 index b86232e..dacae99 100644 --- a/windows/phpvm.ps1 +++ b/windows/phpvm.ps1 @@ -15,7 +15,7 @@ Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" # -- Constants ----------------------------------------------------------------- -$PHPVM_VERSION = "1.8.3" +$PHPVM_VERSION = "1.9.0" $PHPVM_DIR = if ($env:PHPVM_DIR) { $env:PHPVM_DIR } else { "$env:USERPROFILE\.phpvm" } $VERSIONS_DIR = "$PHPVM_DIR\versions" $CURRENT_LINK = "$PHPVM_DIR\current" @@ -241,9 +241,77 @@ function Remove-Junction ([string]$path) { } # -- Download helper ----------------------------------------------------------- +# Progress is only worth drawing for the PHP zips (tens of MB); the Xdebug DLL +# and ext zips are small enough that a bar would just flicker. +$script:PROGRESS_MIN_BYTES = 5MB + +function Format-Bytes ([double]$bytes) { + if ($bytes -ge 1GB) { return "{0:N1} GB" -f ($bytes / 1GB) } + if ($bytes -ge 1MB) { return "{0:N1} MB" -f ($bytes / 1MB) } + return "{0:N0} KB" -f ($bytes / 1KB) +} + +function Format-Duration ([double]$seconds) { + if ($seconds -lt 0 -or [double]::IsInfinity($seconds) -or [double]::IsNaN($seconds)) { return "--:--" } + $ts = [TimeSpan]::FromSeconds([Math]::Round($seconds)) + if ($ts.TotalHours -ge 1) { return "{0:d1}:{1:d2}:{2:d2}" -f [int]$ts.TotalHours, $ts.Minutes, $ts.Seconds } + return "{0:d2}:{1:d2}" -f $ts.Minutes, $ts.Seconds +} + +# Streams $url to $dest, drawing a byte-level progress line on stderr so stdout +# stays pipe-clean. Falls back to a plain copy when the size is unknown, the +# payload is small, or stderr is redirected (CI, tests). function Invoke-Download ([string]$url, [string]$dest) { $ProgressPreference = "SilentlyContinue" - Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing + + $resp = $null + try { + $req = [System.Net.HttpWebRequest]::Create($url) + $req.UserAgent = "phpvm/$PHPVM_VERSION" + $resp = $req.GetResponse() + $total = [long]$resp.ContentLength + } catch { + if ($resp) { $resp.Dispose() } + # Anything odd about the response: let Invoke-WebRequest deal with it. + Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing + return + } + + $showProgress = ($total -ge $script:PROGRESS_MIN_BYTES) -and (-not [Console]::IsErrorRedirected) + + $input_ = $resp.GetResponseStream() + $output = [System.IO.File]::Create($dest) + $buffer = New-Object byte[] 81920 + $read = 0 + $sw = [Diagnostics.Stopwatch]::StartNew() + $lastDraw = 0 + + try { + while (($n = $input_.Read($buffer, 0, $buffer.Length)) -gt 0) { + $output.Write($buffer, 0, $n) + $read += $n + + if (-not $showProgress) { continue } + # Throttle redraws; repainting per 80 KB chunk is pure overhead. + if ($sw.ElapsedMilliseconds - $lastDraw -lt 120 -and $read -lt $total) { continue } + $lastDraw = $sw.ElapsedMilliseconds + + $elapsed = [Math]::Max($sw.Elapsed.TotalSeconds, 0.001) + $speed = $read / $elapsed + $eta = if ($speed -gt 0) { ($total - $read) / $speed } else { -1 } + $pct = [int](100 * $read / $total) + + $line = " {0,3}% {1} / {2} ({3}/s, eta {4})" -f ` + $pct, (Format-Bytes $read), (Format-Bytes $total), + (Format-Bytes $speed), (Format-Duration $eta) + [Console]::Error.Write(("`r" + $line.PadRight(70))) + } + } finally { + $output.Dispose() + $input_.Dispose() + $resp.Dispose() + if ($showProgress) { [Console]::Error.Write("`r" + (" " * 70) + "`r") } + } } @@ -313,8 +381,19 @@ function Test-URLExists ([string]$url) { # CORE COMMANDS # ============================================================================== -function Invoke-Install ([string]$ver) { - if (-not $ver) { Write-Err "Usage: phpvm install (e.g. phpvm install 8.3.0)"; return } +function Invoke-Install ([string]$ver, [string]$flag) { + # Accept --no-use in either position, matching the Linux arg loop. + $noUse = $false + $positional = @() + foreach ($a in @($ver, $flag)) { + if (-not $a) { continue } + if ($a -eq "--no-use") { $noUse = $true } + elseif ($a -like "-*") { Write-Err "Unknown option: $a. Usage: phpvm install [--no-use]"; return } + else { $positional += $a } + } + $ver = if ($positional.Count -gt 0) { $positional[0] } else { "" } + + if (-not $ver) { Write-Err "Usage: phpvm install [--no-use] (e.g. phpvm install 8.3.0)"; return } # Allow "8" -> latest 8.x and "8.3" -> latest 8.3.x. if ($ver -match '^\d+(\.\d+)?$') { @@ -408,7 +487,11 @@ function Invoke-Install ([string]$ver) { Write-Ok "PHP $ver installed successfully." - # Activate the freshly installed version right away (point 4 - auto-use). + # Activate the freshly installed version right away, unless opted out. + if ($noUse) { + Write-Dim "Not switching (--no-use). Run: phpvm use $ver" + return + } Invoke-Use $ver } @@ -1232,6 +1315,7 @@ function Show-Help { VERSION MANAGEMENT phpvm install Download & install a PHP version + --no-use install without switching to it phpvm use Switch the active PHP version phpvm list List installed versions phpvm current Show active version info @@ -1394,7 +1478,7 @@ if (-not $env:PHPVM_NO_ENTRY) { } switch ($Command.ToLower()) { - "install" { Invoke-Install $SubOrVer } + "install" { Invoke-Install $SubOrVer $Arg2 } "use" { Invoke-Use $SubOrVer } { $_ -in "list", "ls" } { Invoke-List } "current" { Invoke-Current } From b515e7ad2a069c438d9d223c5e4e46e3e4db38ff Mon Sep 17 00:00:00 2001 From: Irfan Hardiyanto <52022757+devhardiyanto@users.noreply.github.com> Date: Tue, 14 Jul 2026 02:31:49 +0700 Subject: [PATCH 3/3] fix(linux): quote spinner frames so ShellCheck stops flagging SC1003 A trailing backslash inside single quotes reads as an escape attempt. Double quotes with \ produce the same four frames without the ambiguity. devhardiyanto --- linux/phpvm.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linux/phpvm.sh b/linux/phpvm.sh index 9a52326..cde3742 100644 --- a/linux/phpvm.sh +++ b/linux/phpvm.sh @@ -320,7 +320,7 @@ _phpvm_run_logged() { # Plain ASCII frames + explicit index: ${var:i++:1} is a bashism that bites # in zsh, and Unicode braille spinners mangle on older terminals. - local frames='|/-\' + local frames="|/-\\" local i=0 start=$SECONDS elapsed frame while kill -0 "$pid" 2>/dev/null; do elapsed=$((SECONDS - start))