diff --git a/README.md b/README.md index 1a5543c..2af23bf 100644 --- a/README.md +++ b/README.md @@ -122,13 +122,13 @@ echo "8.3" > .phpvmrc Then run `phpvm auto` from anywhere in the project — phpvm walks up to find the nearest `.phpvmrc` and prepends that version to your shell `PATH` (session only, your global `phpvm use` is untouched). -For hands-off switching, install the PowerShell prompt hook: +For hands-off switching, enable the PowerShell prompt hook: ```powershell -phpvm hook install # adds a snippet to $PROFILE +phpvm hook enable # adds a snippet to $PROFILE # restart PowerShell, then `cd` between projects - phpvm auto-switches per directory -phpvm hook status # check whether the hook is installed -phpvm hook uninstall # remove the hook +phpvm hook status # check whether the hook is enabled +phpvm hook disable # remove the hook ``` `.phpvmrc` accepts a full semver (`8.3.0`), a major.minor (`8.3` — picks the highest installed patch), or a leading `v` (`v8.3.0`). Lines starting with `#` are comments. If the version is not installed locally, phpvm warns but never auto-installs. @@ -203,6 +203,14 @@ phpvm composer # installs a single global composer that follows The installer signature is verified against `composer.github.io/installer.sig` (SHA-384) before execution. Composer is installed **once** — `composer.phar` in `~/.phpvm/` and a shim in `~/.phpvm/bin/` (on PATH) that runs whatever PHP is active. Switch versions with `phpvm use ` and the same `composer` keeps working; no need to re-run `phpvm composer`. (Composer 2.x requires PHP ≥ 7.2.5, so an extremely old active version won't run the latest composer.) +### WP-CLI + +```bash +phpvm wp-cli # installs a single global `wp` command that follows the active PHP version +``` + +Same global-install model as Composer: `wp-cli.phar` lands in `~/.phpvm/` and a `wp` shim in `~/.phpvm/bin/` (on PATH) runs whatever PHP is active — switch with `phpvm use ` and `wp` keeps working. The phar is verified against the upstream `wp-cli.phar.sha512` checksum (SHA-512 — WP-CLI publishes SHA-512, unlike Composer's SHA-384 installer signature). WP-CLI itself requires PHP ≥ 7.2.24 and reports clearly if the active version is older. + ### Fix `php.ini` extension_dir ```bash @@ -343,7 +351,7 @@ your distro, and check the tail of `~/.phpvm/build.log` for the first error. ### `.phpvmrc` doesn't auto-switch -The shell hook isn't active. Windows: `phpvm hook install`, then open a new +The shell hook isn't active. Windows: `phpvm hook enable`, then open a new terminal. Linux: make sure your `~/.bashrc` / `~/.zshrc` sources `~/.phpvm/phpvm.sh`. Note that auto-switch never installs missing versions — it only switches between installed ones. diff --git a/linux/install.sh b/linux/install.sh index a7bdab4..02938a8 100644 --- a/linux/install.sh +++ b/linux/install.sh @@ -6,7 +6,7 @@ set -e -PHPVM_VERSION="1.10.0" +PHPVM_VERSION="1.11.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 43405df..5e96a2d 100644 --- a/linux/phpvm.sh +++ b/linux/phpvm.sh @@ -10,7 +10,7 @@ # phpvm use 8.3.0 # ============================================================================== -PHPVM_VERSION="1.10.0" +PHPVM_VERSION="1.11.0" PHPVM_DIR="${PHPVM_DIR:-$HOME/.phpvm}" PHPVM_VERSIONS="$PHPVM_DIR/versions" PHPVM_CURRENT="$PHPVM_DIR/current" @@ -1094,6 +1094,73 @@ EOF _dim "Composer follows your active PHP version — no need to re-run after 'phpvm use'." } +# ============================================================================== +# WP-CLI (one global wp that follows the active PHP version) +# ============================================================================== +phpvm_wp_cli() { + local cur + cur=$(_phpvm_current_version) + [[ -z "$cur" ]] && { _err "No active PHP version. Run: phpvm use "; return 1; } + + local php_bin="$PHPVM_VERSIONS/$cur/bin/php" + [[ ! -x "$php_bin" ]] && { _err "php binary not found: $php_bin"; return 1; } + + local phar="$PHPVM_DIR/wp-cli.phar" + local shim="$PHPVM_BIN/wp" + + if [[ -x "$shim" && -f "$phar" ]]; then + _warn "WP-CLI already installed at $shim" + _dim "It follows your active PHP version automatically." + _dim "Run: wp --version" + return 0 + fi + + local phar_url="https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar" + local hash_url="$phar_url.sha512" + + _step "Downloading WP-CLI ..." + if command -v curl &>/dev/null; then + curl -fsSL "$phar_url" -o "$phar" || { _err "Download failed."; rm -f "$phar"; return 1; } + elif command -v wget &>/dev/null; then + wget -qO "$phar" "$phar_url" || { _err "Download failed."; rm -f "$phar"; return 1; } + else + _err "curl or wget is required."; return 1 + fi + + _step "Verifying SHA-512 ..." + local expected actual + if command -v curl &>/dev/null; then + expected=$(curl -fsSL "$hash_url" 2>/dev/null | awk '{print $1}') + else + expected=$(wget -qO- "$hash_url" 2>/dev/null | awk '{print $1}') + fi + # Hash through PHP itself (always present) — no sha512sum/shasum coreutils + # split between GNU and BSD/macOS. + actual=$("$php_bin" -r "echo hash_file('sha512', '$phar');" 2>/dev/null) + + if [[ -z "$expected" || "$actual" != "$expected" ]]; then + _err "SHA-512 mismatch! Phar may be corrupt or tampered." + rm -f "$phar" + return 1 + fi + _ok "SHA-512 verified." + + mkdir -p "$PHPVM_BIN" + cat > "$shim" </dev/null | sed 's/^/ /' + echo "" + _dim "WP-CLI follows your active PHP version — no need to re-run after 'phpvm use'." +} + # ============================================================================== # FIX-INI (sync extension_dir in active php.ini) # ============================================================================== @@ -1177,8 +1244,9 @@ phpvm_help() { phpvm fix-ini Sync extension_dir in active php.ini phpvm deps Print dependency install command - COMPOSER + COMPOSER / WP-CLI phpvm composer Install Composer for active PHP version + phpvm wp-cli Install WP-CLI (global 'wp' command) AUTO-SWITCH (.phpvmrc) phpvm auto Switch to the version named in .phpvmrc @@ -1290,13 +1358,13 @@ phpvm_hook() { local sub="${1:-status}" local flag="$PHPVM_DIR/.auto-hook" case "$sub" in - enable|install) + enable) mkdir -p "$PHPVM_DIR" touch "$flag" _ok "Hook enabled. Restart your shell, or run:" _dim " source \"$PHPVM_DIR/phpvm.sh\"" ;; - disable|uninstall|remove) + disable) if [[ -f "$flag" ]]; then rm -f "$flag" _ok "Hook disabled. Restart your shell to fully unregister." @@ -1352,7 +1420,7 @@ _phpvm_levenshtein() { } # Canonical command list (includes aliases) for suggestions. -_PHPVM_COMMANDS="install use list ls current uninstall remove which ini deps ext composer fix-ini auto hook upgrade update version help" +_PHPVM_COMMANDS="install use list ls current uninstall remove which ini deps ext composer wp-cli fix-ini auto hook upgrade update version help" # Unknown command: suggest the nearest match instead of dumping the full help. _phpvm_unknown() { @@ -1389,6 +1457,7 @@ phpvm() { deps) phpvm_deps ;; ext) phpvm_ext "$@" ;; composer) phpvm_composer ;; + wp-cli) phpvm_wp_cli ;; fix-ini) phpvm_fix_ini ;; auto) phpvm_auto ;; hook) phpvm_hook "$@" ;; diff --git a/tests/linux/commands.bats b/tests/linux/commands.bats index 3db9d5b..5be7740 100644 --- a/tests/linux/commands.bats +++ b/tests/linux/commands.bats @@ -82,6 +82,71 @@ EOF [[ "$output" == *"already installed"* ]] } +# ---------- phpvm_wp_cli ---------- + +# Shell-function stub for curl: serves a fake phar for the download and +# $FAKE_WP_SHA for the .sha512 URL. `command -v curl` resolves functions too. +_stub_curl() { + curl() { + if [[ "$*" == *".sha512"* ]]; then + printf '%s wp-cli.phar\n' "$FAKE_WP_SHA" + return 0 + fi + local out="" prev="" + for a in "$@"; do + [[ "$prev" == "-o" ]] && out="$a" + prev="$a" + done + [[ -n "$out" ]] && echo "fake phar" > "$out" + return 0 + } +} + +@test "wp-cli: errors when no active version" { + eval "_phpvm_current_version() { echo ''; }" + run phpvm_wp_cli + [ "$status" -ne 0 ] + [[ "$output" == *"No active PHP version"* ]] +} + +@test "wp-cli: no-op when global shim + phar already present" { + _fake_php_install 8.3.0 + mkdir -p "$PHPVM_BIN" + touch "$PHPVM_DIR/wp-cli.phar" + cat > "$PHPVM_BIN/wp" <<'EOF' +#!/usr/bin/env sh +EOF + chmod +x "$PHPVM_BIN/wp" + run phpvm_wp_cli + [ "$status" -eq 0 ] + [[ "$output" == *"already installed"* ]] +} + +@test "wp-cli: downloads phar, verifies hash, writes wp shim" { + _fake_php_install 8.3.0 + _stub_curl + export FAKE_WP_SHA="cafe123" + export FAKE_PHP_HASH="cafe123" + run phpvm_wp_cli + [ "$status" -eq 0 ] + [[ "$output" == *"WP-CLI installed"* ]] + [ -f "$PHPVM_DIR/wp-cli.phar" ] + [ -x "$PHPVM_BIN/wp" ] + grep -q "wp-cli.phar" "$PHPVM_BIN/wp" +} + +@test "wp-cli: removes phar and writes no shim on hash mismatch" { + _fake_php_install 8.3.0 + _stub_curl + export FAKE_WP_SHA="cafe123" + export FAKE_PHP_HASH="deadbeef" + run phpvm_wp_cli + [ "$status" -ne 0 ] + [[ "$output" == *"SHA-512 mismatch"* ]] + [ ! -f "$PHPVM_DIR/wp-cli.phar" ] + [ ! -f "$PHPVM_BIN/wp" ] +} + # ---------- phpvm_fix_ini ---------- @test "fix-ini: errors when no active version" { @@ -169,6 +234,13 @@ EOF [[ "$output" == *"No active PHP version"* ]] } +@test "dispatch: 'phpvm wp-cli' routes to phpvm_wp_cli" { + eval "_phpvm_current_version() { echo ''; }" + run phpvm wp-cli + [ "$status" -ne 0 ] + [[ "$output" == *"No active PHP version"* ]] +} + @test "dispatch: 'phpvm fix-ini' routes to phpvm_fix_ini" { eval "_phpvm_current_version() { echo ''; }" run phpvm fix-ini diff --git a/tests/windows/WpCli.Tests.ps1 b/tests/windows/WpCli.Tests.ps1 new file mode 100644 index 0000000..ae31d0b --- /dev/null +++ b/tests/windows/WpCli.Tests.ps1 @@ -0,0 +1,69 @@ +Describe 'Invoke-WpCli' { + BeforeAll { + $env:PHPVM_DIR = Join-Path $TestDrive '.phpvm' + New-Item -ItemType Directory -Path $env:PHPVM_DIR -Force | Out-Null + . $PSScriptRoot/Common.ps1 + + # Test double for php.exe: any invocation prints $env:FAKE_PHP_HASH, + # standing in for hash_file('sha512', ...). + $script:FakePhp = Join-Path $TestDrive 'php.bat' + "@echo off`r`necho:%FAKE_PHP_HASH%" | Set-Content $FakePhp -Encoding ASCII + } + + AfterAll { + Remove-Item Env:PHPVM_DIR -ErrorAction SilentlyContinue + Remove-Item Env:FAKE_PHP_HASH -ErrorAction SilentlyContinue + Remove-Item Env:PHPVM_SKIP_HASH -ErrorAction SilentlyContinue + } + + BeforeEach { + $env:FAKE_PHP_HASH = 'cafe123' + Remove-Item Env:PHPVM_SKIP_HASH -ErrorAction SilentlyContinue + Remove-Item "$PHPVM_DIR\wp-cli.phar", "$PHPVM_BIN\wp.bat" -Force -ErrorAction SilentlyContinue + + Mock -CommandName Get-PHPBuildInfo -MockWith { @{ Exe = $FakePhp } } + Mock -CommandName Invoke-WebRequest -MockWith { 'fake phar' | Set-Content $OutFile } + Mock -CommandName Get-WebString -MockWith { "$env:FAKE_PHP_HASH wp-cli.phar" } + Mock -CommandName Write-Err -MockWith {} + Mock -CommandName Write-Warn -MockWith {} + } + + It 'Downloads the phar, verifies the hash, and writes the wp.bat shim' { + Invoke-WpCli + + Test-Path "$PHPVM_DIR\wp-cli.phar" | Should -BeTrue + Test-Path "$PHPVM_BIN\wp.bat" | Should -BeTrue + Get-Content "$PHPVM_BIN\wp.bat" -Raw | Should -Match ([regex]::Escape("$PHPVM_DIR\wp-cli.phar")) + Should -Invoke Get-WebString -Times 1 + Should -Invoke Write-Err -Times 0 + } + + It 'Is a no-op when the shim already exists' { + New-Item -ItemType Directory -Path $PHPVM_BIN -Force | Out-Null + '@echo off' | Set-Content "$PHPVM_BIN\wp.bat" + + Invoke-WpCli + + Should -Invoke Write-Warn -Times 1 -ParameterFilter { $m -like '*already installed*' } + Should -Invoke Invoke-WebRequest -Times 0 + } + + It 'Removes the phar and writes no shim on hash mismatch' { + Mock -CommandName Get-WebString -MockWith { 'deadbeef wp-cli.phar' } + + Invoke-WpCli + + Should -Invoke Write-Err -Times 1 -ParameterFilter { $m -like 'SHA-512 mismatch*' } + Test-Path "$PHPVM_DIR\wp-cli.phar" | Should -BeFalse + Test-Path "$PHPVM_BIN\wp.bat" | Should -BeFalse + } + + It 'Skips verification when PHPVM_SKIP_HASH is set' { + $env:PHPVM_SKIP_HASH = '1' + + Invoke-WpCli + + Should -Invoke Get-WebString -Times 0 + Test-Path "$PHPVM_BIN\wp.bat" | Should -BeTrue + } +} diff --git a/version.txt b/version.txt index 81c871d..169f19b 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.10.0 +1.11.0 \ No newline at end of file diff --git a/windows/install.ps1 b/windows/install.ps1 index e77d0a2..c12fb60 100644 --- a/windows/install.ps1 +++ b/windows/install.ps1 @@ -8,7 +8,7 @@ Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" -$PHPVM_VERSION = "1.10.0" +$PHPVM_VERSION = "1.11.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 1ff38b6..3f5dc0b 100644 --- a/windows/phpvm.ps1 +++ b/windows/phpvm.ps1 @@ -15,7 +15,7 @@ Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" # -- Constants ----------------------------------------------------------------- -$PHPVM_VERSION = "1.10.0" +$PHPVM_VERSION = "1.11.0" $PHPVM_DIR = if ($env:PHPVM_DIR) { $env:PHPVM_DIR } else { "$env:USERPROFILE\.phpvm" } $VERSIONS_DIR = "$PHPVM_DIR\versions" $CURRENT_LINK = "$PHPVM_DIR\current" @@ -858,15 +858,14 @@ function Show-PHPVMHookStatus { if ($content -and $content.Contains($script:PHPVM_HOOK_MARKER)) { Write-Ok "Hook installed in $profilePath" } else { - Write-Dim "Hook not installed. Run: phpvm hook install" + Write-Dim "Hook not installed. Run: phpvm hook enable" } } function Invoke-Hook ([string]$sub) { - # enable/disable match the Linux verbs; install/uninstall kept as aliases. switch ($sub.ToLower()) { - { $_ -in 'enable', 'install' } { Install-PHPVMHook } - { $_ -in 'disable', 'uninstall', 'remove' } { Uninstall-PHPVMHook } + 'enable' { Install-PHPVMHook } + 'disable' { Uninstall-PHPVMHook } 'status' { Show-PHPVMHookStatus } default { Write-Host "" @@ -1413,6 +1412,61 @@ php "$composerPhar" %* Write-Dim "Composer follows your active PHP version - no need to re-run after 'phpvm use'." } +function Invoke-WpCli { + $info = Get-PHPBuildInfo + + # Same global-phar-plus-shim shape as Composer: phar in $PHPVM_DIR, shim in + # $PHPVM_BIN calls whatever `php` resolves to, so wp follows `phpvm use`. + $wpPhar = "$PHPVM_DIR\wp-cli.phar" + $wpBat = "$PHPVM_BIN\wp.bat" + + if (Test-Path $wpBat) { + Write-Warn "WP-CLI already installed at $wpBat" + Write-Dim "It follows your active PHP version automatically." + Write-Dim "Run: wp --version" + return + } + + $pharUrl = "https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar" + $hashUrl = "$pharUrl.sha512" + + Write-Step "Downloading WP-CLI ..." + try { + $ProgressPreference = "SilentlyContinue" + Invoke-WebRequest -Uri $pharUrl -OutFile $wpPhar -UseBasicParsing + } catch { + Write-Err "Download failed: $_" + return + } + + if (-not $env:PHPVM_SKIP_HASH) { + Write-Step "Verifying SHA-512 ..." + try { $expectedHash = ((Get-WebString $hashUrl).Trim() -split '\s+')[0] } + catch { Write-Err "Could not fetch checksum: $_"; Remove-Item $wpPhar -Force; return } + $actualHash = (& $info.Exe -r "echo hash_file('sha512', '$($wpPhar -replace '\\','\\\\')');") + if ($actualHash -ne $expectedHash) { + Write-Err "SHA-512 mismatch! Phar may be corrupt or tampered." + Remove-Item $wpPhar -Force + return + } + Write-Ok "SHA-512 verified." + } + + if (-not (Test-Path $PHPVM_BIN)) { New-Item -ItemType Directory -Path $PHPVM_BIN -Force | Out-Null } + $bat = @" +@echo off +php "$wpPhar" %* +"@ + $bat | Set-Content $wpBat -Encoding ASCII + Write-Ok "WP-CLI installed (global)!" + Write-Ok " phar : $wpPhar" + Write-Ok " shim : $wpBat" + Write-Host "" + & $info.Exe $wpPhar --version 2>$null | ForEach-Object { Write-Host " $_" } + Write-Host "" + Write-Dim "WP-CLI follows your active PHP version - no need to re-run after 'phpvm use'." +} + function Show-Help { Write-Host @" @@ -1432,8 +1486,9 @@ function Show-Help { phpvm fix-ini Sync extension_dir & CA bundle in active php.ini phpvm cacert [status|update] Manage the shared CA bundle (HTTPS/TLS) - COMPOSER + COMPOSER / WP-CLI phpvm composer Install Composer for active PHP version + phpvm wp-cli Install WP-CLI (global 'wp' command) AUTO-SWITCH (.phpvmrc) phpvm auto Switch to the version named in .phpvmrc @@ -1600,7 +1655,7 @@ function Get-Levenshtein ([string]$a, [string]$b) { # Unknown command: suggest the nearest match instead of dumping the full help. function Invoke-Unknown ([string]$cmd) { $cmds = @("install","use","list","ls","current","uninstall","remove", - "which","ini","fix-ini","cacert","ext","composer","auto","hook", + "which","ini","fix-ini","cacert","ext","composer","wp-cli","auto","hook", "upgrade","update","version","help") $best = ""; $bestd = 99 foreach ($c in $cmds) { @@ -1635,6 +1690,7 @@ if (-not $env:PHPVM_NO_ENTRY) { "auto" { Invoke-Auto } "hook" { Invoke-Hook $SubOrVer } "composer" { Invoke-Composer } + "wp-cli" { Invoke-WpCli } { $_ -in "upgrade", "update" } { Invoke-Upgrade } { $_ -in "version", "-v" } { Write-Ok "phpvm $PHPVM_VERSION" } { $_ -in "help", "--help" } { Show-Help }