feat!: command-flow overhaul, persistent default, non-interactive installs + de-bloat - #40
Conversation
Bash and Zsh emitted byte-identical path/set_env_var/wrapper_fn strings from two copies. Extract posix_path, posix_set_env_var and posix_wrapper_fn free functions; both impls now delegate. Only the use_on_cd hooks differ per shell.
utils::sort_versions carried a 3-way semver/non-semver fallback comparator, but its only caller sorts installed-version directory names, which are always full semver. One sort_by_cached_key on semver::Version::parse covers it; delete src/utils.rs.
list_installed_versions already returns the list semver-sorted; the inline parts-based re-sort was a third comparator doing the same work again.
futures-util existed only for StreamExt on bytes_stream(). reqwest::Response::chunk() does the same natively. One dependency fewer.
Two dependencies served a single log::debug! call on a path where the error is intentionally swallowed (best-effort update check). Delete the call and both crates.
Nothing ever sets PVM_SHELL_PID: the shell wrappers pass per-session uniqueness via PVM_ENV_UPDATE_PATH. The suffix branch could never execute.
Every caller passed None; the PVM_ENV_UPDATE_PATH env var is the real override mechanism.
Only tokio::main is used directly; reqwest enables the tokio features it needs itself. 'full' pulled signal, net, io and process drivers into a size-optimized binary for nothing.
It only forwarded to execute_install_with(v, true); callers now call that directly.
get_versions_dir derives from the same root and create_dir_all creates the full path.
…e -u Only supported build hosts are Linux/macOS where date(1) exists; build.rs already shells out to git. Extract cmd_stdout helper for the three command-to-string call sites.
…sions Integration tests can now exercise version resolution and install logic without network access by pre-filling remote_cache-<target>.json and fake versions/<v>/bin layouts.
pvm ls-remote now prints the remote versions (with installed markers and package tags) instead of opening an install picker, so scripts and non-TTY callers can consume it. The interactive picker moved to 'pvm install' without arguments, which already delegated here; it now bails with a usage hint when stdin is not a terminal. BREAKING CHANGE: 'pvm ls-remote' no longer prompts to install a selected version. Use 'pvm install' without arguments for the interactive picker.
…Y defaults - pvm install gains --packages cli,fpm,micro (validated against the remote index) and -y/--yes; without a terminal the package selection defaults to cli instead of failing in dialoguer. - pvm use gains -y/--yes for the install-missing and patch-update prompts; patch-update offers are now skipped entirely without a terminal so scripts never trigger surprise downloads. - Patch updates reuse the package set of the version being replaced instead of re-prompting. - New prompt::confirm helper centralizes Confirm handling (assume-yes, non-TTY returns the default); uninstall now uses it too.
Installing an already-present version now prints what is installed, preselects only the missing packages in the interactive selection, and becomes an idempotent no-op for non-interactive callers when cli is already there. Explicit --packages still reinstalls (repair).
pvm default <version> stores the version in PVM_DIR/default; pvm env activates it on shell startup, so a chosen version finally survives opening a new terminal. pvm default without argument shows an interactive picker (or prints the current default when non-TTY), and pvm default system clears it. Also added to the interactive menu.
pvm use system writes a deactivation snippet to the env-update file: it clears PVM_MULTISHELL_PATH and filters every PVM_DIR/versions entry out of PATH (tr/grep/paste for bash and zsh, string match for fish). Until now there was no way back to the system PHP without opening a new shell.
… offer pvm ls on a terminal now shows the installed versions as a picker: Enter switches the shell to the selection, Esc just exits. After a picker-driven switch (ls or pvm use without arguments) pvm offers to save the choice to .php-version. Scripts and pipes still get the plain list. The activation tail of pvm use moved into a shared activate() used by both flows, and a missing version without a terminal now fails with a usage hint instead of a dialoguer error.
pvm init no longer overwrites an existing .php-version silently: it shows the current content and asks first. The selection list now offers locally installed versions (marked) before the remote major.minor lines, and falls back to installed-only with a warning when the remote index is unreachable. Without a terminal init fails with a hint instead of a dialoguer error.
pvm cache clear deletes the cached remote_cache-<target>.json files so a freshly published upstream patch becomes visible before the 24h cache expiry, without hand-deleting files in PVM_DIR.
pvm which resolves the active version (or an explicit argument like 8.3) and prints the full path of its php binary - a debugging aid for "which php am I actually running".
pvm exec <version> <cmd...> prepends the version bin directory to PATH for a single child process and propagates its exit code - testing across versions without switching the shell.
pvm prune [-y] deletes every installed patch that is no longer the newest of its minor line, keeping the currently active version. If the persisted default version gets pruned, it is re-pointed to the kept patch of the same minor.
New cases 15-18 exercise pvm default persistence + env activation, pvm use system PATH stripping, install --packages/-y idempotency and prune with default re-pointing. The uninstall case moves to slot 19 and removes LATEST, since prune already dropped PREVIOUS. Cases guard the single-upstream-patch situation where LATEST == PREVIOUS.
Covers default/use system, which/exec/prune/cache clear, the non-interactive install flags, the interactive ls picker and plain output when piped, and replaces the stale fs4 reference with OS file locks.
Previously untracked. Documents command-dispatch conventions incl. the non-interactive requirements (prompt::confirm, IsTerminal guards), the default-version and cache files in PVM_DIR, the shared activate() tail, Shell::deactivate, and the offline test helpers.
Uninstalling the persisted default version now clears the default file with a hint (prune already re-points it); pvm env warns on stderr when the stored default is not installed instead of silently falling back to system PHP. Tests cover both paths plus the non-TTY prune default.
Non-TTY 'pvm use <missing>' without --yes now fails with a hint instead of auto-confirming a network install (both the argument and .php-version paths), matching the existing patch-update gate. A .php-version containing 'system' deactivates instead of offering to install 'PHP system', --silent suppresses the deactivation message for cd-hooks, and Esc in the version picker exits quietly as the ls prompt promises.
-y now also skips the package MultiSelect (defaults to cli, like a missing terminal); without a terminal and without -y the trailing 'use now?' question no longer auto-answers yes, which printed a 'Switched to PHP' message although no shell evaluates the env file.
Signal-killed children previously collapsed to exit code 1; now they follow the shell convention so callers can distinguish SIGTERM from a plain failure.
Both the picker and explicit-argument flows carried the same confirm-write-report block; save_question() now picks the wording and one shared block does the writing.
init.rs and prune.rs carried identical private helpers; fs.rs, update.rs and the install picker had three more inline split variants of the same logic.
Windows is an unsupported target (get_target_triple bails before any install), so the .exe fallbacks could never match.
Every arm bound the command to a temporary before calling it; call directly on the struct literal instead.
Claude Code drops runtime files (locks, worktrees) there; they are machine-local and must not land in commits.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (4)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThis PR expands ChangesCLI and shell workflows
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (7)
src/commands/default_cmd.rs (1)
42-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePre-select the current default in the interactive picker.
current_defaultis computed at line 42 and used to label the matching entry(default), but.default(0)at line 56 always highlights the first item instead of the current default's index. Minor UX inconsistency for an otherwise well-guarded interactive flow.💡 Proposed fix
let current_default = fs::get_default_version()?.unwrap_or_default(); + let default_idx = items + .iter() + .position(|i| i.version == current_default) + .unwrap_or(0); let displays: Vec<String> = items .iter() .map(|i| { if i.version == current_default { format!("{} {}", i.display, "(default)".cyan()) } else { i.display.clone() } }) .collect(); let selection = Select::with_theme(&ColorfulTheme::default()) .with_prompt("Select the default PHP version for new shells") - .default(0) + .default(default_idx) .items(&displays) .interact_opt()?;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/commands/default_cmd.rs` around lines 42 - 58, Update the interactive picker around current_default and Select::with_theme so the default selection index is the position of the item whose version matches current_default, rather than always 0. Preserve the existing "(default)" display labeling and use the computed matching index when configuring .default(...).src/commands/install.rs (2)
63-71: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winIdempotent short-circuit ignores
--yes, sopvm install <v> -yon a TTY re-downloads an already-installedcli.
select_packagestreatsassume_yesexactly like a missing terminal (line 202), but the early return here only covers the non-TTY case. Gating both on the same condition keeps-yidempotent.♻️ Proposed change
// Idempotent no-op for scripts: nothing was explicitly requested and // the default cli package is already present. if packages.is_empty() - && !std::io::stdin().is_terminal() + && (assume_yes || !std::io::stdin().is_terminal()) && already_installed.iter().any(|p| p == "cli") { return Ok(Some(resolved_version)); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/commands/install.rs` around lines 63 - 71, Update the early-return condition in the install selection flow around select_packages to treat assume_yes the same as a non-terminal stdin. Preserve the existing empty-package and already-installed cli checks, so `pvm install <v> -y` returns the resolved version without re-downloading.
234-238: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
interact_opt()for the package picker.
dialoguer0.12 supportsMultiSelect::interact_opt(), and returningNoneis already handled as an operation cancellation; usinginteract()prevents Esc from producing that same quiet cancel path.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/commands/install.rs` around lines 234 - 238, Update the package picker’s MultiSelect interaction to call interact_opt() instead of interact(), preserving the existing selections.is_empty() cancellation handling and allowing Esc to return None quietly.src/commands/use_cmd.rs (1)
75-117: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated "missing version → gate → confirm → install" flow; consider extracting a helper.
Lines 34-65 and this block differ only in the message wording and the source of the version string. A small
async fn install_missing(version: &str, source: Option<&str>, silent: bool, yes: bool) -> Result<Option<String>>would remove the duplication and flatten the 6-level nesting here.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/commands/use_cmd.rs` around lines 75 - 117, Extract the duplicated missing-version handling into a small async helper, such as install_missing, covering the silent/noninteractive gate, confirmation prompt, installation, cancellation, and optional source-specific messaging. Replace the nested flow in the current command handler and the corresponding flow around the earlier missing-version block with this helper, preserving their differing version sources and message wording while continuing to set resolved_version from a successful installation.Cargo.toml (1)
28-28: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAlign the
tokiofeatures with the repository guideline.The crate only needs
["macros", "rt-multi-thread"]for#[tokio::main], so droppingfulldoes not break compilation. Since the guideline still requiresfeatures = ["full"], update this one dependency line to that convention unless the project intentionally diverges.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Cargo.toml` at line 28, Update the tokio dependency declaration in Cargo.toml to use features = ["full"], replacing the current ["macros", "rt-multi-thread"] feature list and preserving the existing version.Source: Coding guidelines
src/fs.rs (1)
98-107: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winDistinguish "not set" from real I/O errors in
get_default_version.
Err(_) => Ok(None)treats every read failure (permission denied, disk error, etc.) the same as "no default configured." Every caller (env.rs,uninstall.rs,prune.rs,default_cmd.rs) will silently behave as if no default exists, masking real problems.cache.rs'sclear()in this same PR already distinguishesErrorKind::NotFoundfrom other I/O errors — worth applying the same pattern here for consistency.♻️ Proposed fix
pub fn get_default_version() -> Result<Option<String>> { let path = get_pvm_dir()?.join(crate::constants::DEFAULT_VERSION_FILE); match std::fs::read_to_string(path) { Ok(content) => { let trimmed = content.trim().to_string(); Ok((!trimmed.is_empty()).then_some(trimmed)) } - Err(_) => Ok(None), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(e) => Err(e).context("Failed to read default version file"), } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/fs.rs` around lines 98 - 107, Update get_default_version to return Ok(None) only when reading DEFAULT_VERSION_FILE fails with ErrorKind::NotFound; propagate all other I/O errors through the function’s existing Result error path, matching the error distinction used by cache.rs::clear().src/commands/cache.rs (1)
27-65: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winCache-clear deletion bypasses the remote-cache locking protocol.
network.rs's cache read/write paths uselock_shared/lockaround the cache file, butclear()unlinks matching files without acquiring any lock first. A concurrent writer holdingfile.lock()on the cache file could have its write silently lost once the file is unlinked mid-operation. As per coding guidelines, "Use file locking (std::fs::File::lock / lock_shared / unlock) when writing to env update files or the remote cache; follow the fs::write_env_file_locked pattern."♻️ Proposed fix
- if name.starts_with("remote_cache") - && name.ends_with(".json") - && entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) - { - std::fs::remove_file(entry.path()) - .with_context(|| format!("Failed to remove cache file '{}'", name))?; - removed += 1; - } + if name.starts_with("remote_cache") + && name.ends_with(".json") + && entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) + { + if let Ok(file) = std::fs::OpenOptions::new().write(true).open(entry.path()) { + let _ = file.lock(); + std::fs::remove_file(entry.path()) + .with_context(|| format!("Failed to remove cache file '{}'", name))?; + let _ = file.unlock(); + } + removed += 1; + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/commands/cache.rs` around lines 27 - 65, Update clear() to acquire the same exclusive file lock used by the remote-cache write path before removing each matching cache file. Open the cache file, call the established lock API, remove it only after the lock is held, and unlock/close it appropriately, preserving the existing filtering, error context, and removal count behavior.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/commands/install.rs`:
- Around line 334-342: Update the install flow around the installed check and
execute_install_with so an explicitly supplied packages selection is still
applied when the chosen version is already installed. Do not short-circuit to
the “already installed” message when packages need installation; invoke
execute_install_with with the selected version and existing package/assume-yes
arguments, while preserving the message for installed versions with no requested
package work.
In `@src/commands/prune.rs`:
- Around line 50-54: Update the confirmation flow in the prune command around
prompt::confirm so non-interactive stdin cannot proceed with the default
affirmative response. Require self.yes to be explicitly set before allowing
removal when stdin is not a terminal, while preserving the existing interactive
confirmation behavior and cancellation path.
In `@src/commands/use_cmd.rs`:
- Around line 273-290: Update the save prompt condition in the `use` command
flow around `save_question` so it is not evaluated when `opts.quiet` is true.
Preserve the existing prompt and `.php-version` write behavior for non-quiet
invocations, while ensuring silent shell-hook activation never prompts.
---
Nitpick comments:
In `@Cargo.toml`:
- Line 28: Update the tokio dependency declaration in Cargo.toml to use features
= ["full"], replacing the current ["macros", "rt-multi-thread"] feature list and
preserving the existing version.
In `@src/commands/cache.rs`:
- Around line 27-65: Update clear() to acquire the same exclusive file lock used
by the remote-cache write path before removing each matching cache file. Open
the cache file, call the established lock API, remove it only after the lock is
held, and unlock/close it appropriately, preserving the existing filtering,
error context, and removal count behavior.
In `@src/commands/default_cmd.rs`:
- Around line 42-58: Update the interactive picker around current_default and
Select::with_theme so the default selection index is the position of the item
whose version matches current_default, rather than always 0. Preserve the
existing "(default)" display labeling and use the computed matching index when
configuring .default(...).
In `@src/commands/install.rs`:
- Around line 63-71: Update the early-return condition in the install selection
flow around select_packages to treat assume_yes the same as a non-terminal
stdin. Preserve the existing empty-package and already-installed cli checks, so
`pvm install <v> -y` returns the resolved version without re-downloading.
- Around line 234-238: Update the package picker’s MultiSelect interaction to
call interact_opt() instead of interact(), preserving the existing
selections.is_empty() cancellation handling and allowing Esc to return None
quietly.
In `@src/commands/use_cmd.rs`:
- Around line 75-117: Extract the duplicated missing-version handling into a
small async helper, such as install_missing, covering the silent/noninteractive
gate, confirmation prompt, installation, cancellation, and optional
source-specific messaging. Replace the nested flow in the current command
handler and the corresponding flow around the earlier missing-version block with
this helper, preserving their differing version sources and message wording
while continuing to set resolved_version from a successful installation.
In `@src/fs.rs`:
- Around line 98-107: Update get_default_version to return Ok(None) only when
reading DEFAULT_VERSION_FILE fails with ErrorKind::NotFound; propagate all other
I/O errors through the function’s existing Result error path, matching the error
distinction used by cache.rs::clear().
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: d14a4876-e28a-40d4-995a-fc0b068b23d8
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (35)
.gitignoreCLAUDE.mdCargo.tomlREADME.mdbuild.rssrc/cli.rssrc/commands/cache.rssrc/commands/default_cmd.rssrc/commands/env.rssrc/commands/exec_cmd.rssrc/commands/init.rssrc/commands/install.rssrc/commands/ls.rssrc/commands/ls_remote.rssrc/commands/mod.rssrc/commands/prune.rssrc/commands/uninstall.rssrc/commands/use_cmd.rssrc/commands/which_cmd.rssrc/constants.rssrc/fs.rssrc/interactive.rssrc/main.rssrc/network.rssrc/prompt.rssrc/shell.rssrc/update.rssrc/utils.rstests/cli.rstests/e2e/cases/15_default.shtests/e2e/cases/15_uninstall.shtests/e2e/cases/16_use_system.shtests/e2e/cases/17_install_packages.shtests/e2e/cases/18_prune.shtests/e2e/cases/19_uninstall.sh
💤 Files with no reviewable changes (2)
- src/utils.rs
- tests/e2e/cases/15_uninstall.sh
… cd-hook - pvm prune without a terminal now requires --yes before mass deletion, matching the non-TTY guards on use and install. - Explicit --packages survives the already-installed short-circuit in the install picker, so packages can be added to an existing version. - The silent cd-hook never evaluates the .php-version save prompt; a file holding a partial version like 8.3 would otherwise prompt on every cd.
|
@coderabbitai review |
✅ Action performedReview finished.
|
## [2.0.0](v1.3.2...v2.0.0) (2026-07-24) ### ⚠ BREAKING CHANGES * 'pvm ls-remote' no longer prompts to install a selected version. Use 'pvm install' without arguments for the interactive picker. * feat(install): non-interactive installs via --packages, -y and non-TTY defaults - pvm install gains --packages cli,fpm,micro (validated against the remote index) and -y/--yes; without a terminal the package selection defaults to cli instead of failing in dialoguer. - pvm use gains -y/--yes for the install-missing and patch-update prompts; patch-update offers are now skipped entirely without a terminal so scripts never trigger surprise downloads. - Patch updates reuse the package set of the version being replaced instead of re-prompting. - New prompt::confirm helper centralizes Confirm handling (assume-yes, non-TTY returns the default); uninstall now uses it too. * feat(install): detect installed packages and preselect missing ones Installing an already-present version now prints what is installed, preselects only the missing packages in the interactive selection, and becomes an idempotent no-op for non-interactive callers when cli is already there. Explicit --packages still reinstalls (repair). * feat(default): persistent default version for new shells pvm default <version> stores the version in PVM_DIR/default; pvm env activates it on shell startup, so a chosen version finally survives opening a new terminal. pvm default without argument shows an interactive picker (or prints the current default when non-TTY), and pvm default system clears it. Also added to the interactive menu. * feat(use): support switching back to system PHP pvm use system writes a deactivation snippet to the env-update file: it clears PVM_MULTISHELL_PATH and filters every PVM_DIR/versions entry out of PATH (tr/grep/paste for bash and zsh, string match for fish). Until now there was no way back to the system PHP without opening a new shell. * feat(ls): interactive version switch from list with .php-version save offer pvm ls on a terminal now shows the installed versions as a picker: Enter switches the shell to the selection, Esc just exits. After a picker-driven switch (ls or pvm use without arguments) pvm offers to save the choice to .php-version. Scripts and pipes still get the plain list. The activation tail of pvm use moved into a shared activate() used by both flows, and a missing version without a terminal now fails with a usage hint instead of a dialoguer error. * feat(init): confirm .php-version overwrite and prefer installed versions pvm init no longer overwrites an existing .php-version silently: it shows the current content and asks first. The selection list now offers locally installed versions (marked) before the remote major.minor lines, and falls back to installed-only with a warning when the remote index is unreachable. Without a terminal init fails with a hint instead of a dialoguer error. * feat(cache): add cache clear command for the remote version index pvm cache clear deletes the cached remote_cache-<target>.json files so a freshly published upstream patch becomes visible before the 24h cache expiry, without hand-deleting files in PVM_DIR. * feat(which): print the path of the active or given PHP binary pvm which resolves the active version (or an explicit argument like 8.3) and prints the full path of its php binary - a debugging aid for "which php am I actually running". * feat(exec): run a command under a specific PHP version pvm exec <version> <cmd...> prepends the version bin directory to PATH for a single child process and propagates its exit code - testing across versions without switching the shell. * feat(prune): remove superseded patch versions pvm prune [-y] deletes every installed patch that is no longer the newest of its minor line, keeping the currently active version. If the persisted default version gets pruned, it is re-pointed to the kept patch of the same minor. * test(e2e): cover default, use system, non-interactive install and prune New cases 15-18 exercise pvm default persistence + env activation, pvm use system PATH stripping, install --packages/-y idempotency and prune with default re-pointing. The uninstall case moves to slot 19 and removes LATEST, since prune already dropped PREVIOUS. Cases guard the single-upstream-patch situation where LATEST == PREVIOUS. * docs(readme): document new commands and flows Covers default/use system, which/exec/prune/cache clear, the non-interactive install flags, the interactive ls picker and plain output when piped, and replaces the stale fs4 reference with OS file locks. * docs(claude): track CLAUDE.md with current architecture Previously untracked. Documents command-dispatch conventions incl. the non-interactive requirements (prompt::confirm, IsTerminal guards), the default-version and cache files in PVM_DIR, the shared activate() tail, Shell::deactivate, and the offline test helpers. * fix(default): keep default lifecycle consistent on uninstall Uninstalling the persisted default version now clears the default file with a hint (prune already re-points it); pvm env warns on stderr when the stored default is not installed instead of silently falling back to system PHP. Tests cover both paths plus the non-TTY prune default. * fix(use): no surprise installs without a terminal Non-TTY 'pvm use <missing>' without --yes now fails with a hint instead of auto-confirming a network install (both the argument and .php-version paths), matching the existing patch-update gate. A .php-version containing 'system' deactivates instead of offering to install 'PHP system', --silent suppresses the deactivation message for cd-hooks, and Esc in the version picker exits quietly as the ls prompt promises. * fix(install): make -y fully non-interactive and honest without a TTY -y now also skips the package MultiSelect (defaults to cli, like a missing terminal); without a terminal and without -y the trailing 'use now?' question no longer auto-answers yes, which printed a 'Switched to PHP' message although no shell evaluates the env file. * fix(exec): propagate signal terminations as 128 plus signal number Signal-killed children previously collapsed to exit code 1; now they follow the shell convention so callers can distinguish SIGTERM from a plain failure. * refactor(use): deduplicate the .php-version save blocks in activate Both the picker and explicit-argument flows carried the same confirm-write-report block; save_question() now picks the wording and one shared block does the writing. * refactor(fs): consolidate minor-version extraction into fs::minor_of init.rs and prune.rs carried identical private helpers; fs.rs, update.rs and the install picker had three more inline split variants of the same logic. * refactor(fs): drop dead .exe checks in get_installed_packages Windows is an unsupported target (get_target_triple bails before any install), so the .exe fallbacks could never match. * refactor(interactive): construct and call commands inline in menu arms Every arm bound the command to a temporary before calling it; call directly on the struct literal instead. * chore(gitignore): ignore local .claude directory Claude Code drops runtime files (locks, worktrees) there; they are machine-local and must not land in commits. * fix(review): address CodeRabbit findings on prune, install picker and cd-hook - pvm prune without a terminal now requires --yes before mass deletion, matching the non-TTY guards on use and install. - Explicit --packages survives the already-installed short-circuit in the install picker, so packages can be added to an existing version. - The silent cd-hook never evaluates the .php-version save prompt; a file holding a partial version like 8.3 would otherwise prompt on every cd. ### Features * command-flow overhaul, persistent default, non-interactive installs + de-bloat ([#40](#40)) ([46ca57d](46ca57d))
|
🎉 This PR is included in version 2.0.0 🎉 The release is available on:
Your semantic-release bot 📦🚀 |
What
36 commits in three blocks: a repo-wide de-bloat pass, a command-flow overhaul with six new commands, and review-driven fixes.
1. Ponytail cuts (refactor/chore)
futures-util,log,env_logger,chronobuild-dep) and trimmedtokiofromfulltomacros + rt-multi-thread.reqwest::Response::chunk()instead ofStreamExt,date -uinstead of chrono.PVM_SHELL_PIDbranch, always-Noneparameter,.exechecks, unused bindings, duplicated Bash/Zsh shell emission, five copies of minor-version extraction.2. Command flows (feat)
pvm default <version>— persistent default;pvm envactivates it in every new shell.pvm default systemclears it.pvm use system— switches the shell back to the system PHP (newShell::deactivatestrips pvm entries from PATH; bash/zsh/fish).pvm lsinteractive — Enter switches to the selected version and offers to save it to.php-version; Esc just exits. Piped output stays plain.pvm install --packages cli,fpm -y— fully non-interactive installs; without a terminal the package selection defaults tocli. Already-installed versions short-circuit (idempotent for scripts) and the MultiSelect preselects missing packages.pvm which [version],pvm exec <version> <cmd...>,pvm prune [-y],pvm cache clear— path inspection, one-off runs under a version, superseded-patch cleanup (re-points the default), cache invalidation.pvm init— confirms before overwriting.php-version, offers installed versions first, degrades gracefully offline.pvm ls-remoteis now a plain listing (script-friendly); the interactive install picker lives inpvm installwithout arguments.3. Review fixes (fix)
pvm use <missing>without--yesfails with a hint instead of auto-installing.pvm envwarns on stderr when the default is missing.-ynow covers the package MultiSelect; no false "Switched to PHP" without a terminal;execpropagates signal deaths as128+n;.php-versioncontainingsystemdeactivates instead of prompting to "install PHP system".Tests
cargo clippy -- -D warnings,cargo fmt --check,cargo test(24 unit + 42 integration).Notes for review
feat!commit carries the BREAKING CHANGE footer for semantic-release).e2e tests - linuxjob in CI.