Tool abstraction layer with :download / :path / :install / :update / :uninstall pattern#28
Tool abstraction layer with :download / :path / :install / :update / :uninstall pattern#28munezaclovis merged 16 commits intomainfrom
Conversation
…e pattern Introduce `internal/tools/` package with a `Tool` registry that standardizes how binaries are stored (internal/bin/) and exposed to PATH (bin/) via shims or symlinks. Each tool follows a consistent command pattern: - :download — fetch binary to private storage - :path — expose/unexpose from user PATH (--remove flag) - :install — orchestrator: :download + :path - :update — check for newer version, re-download, re-expose if on PATH Key changes: - Move mago binary to ~/.pv/internal/bin/, symlinked from ~/.pv/bin/ - Move composer.phar to ~/.pv/internal/bin/ - Move shim logic from phpenv/shim.go to tools/shims.go - Add tools.Expose/Unexpose/ExposeAll for managing PATH entries - Refactor :install commands to delegate to :download (no duplicated logic) - Add :update commands for all tools (php, mago, composer, colima) - Refactor `pv update` to orchestrate per-tool :update commands - Add pv self-update with syscall.Exec re-exec for consistent tool updates - Migrate daemon commands to colon-namespace (daemon:enable, daemon:disable) - Update CLAUDE.md with tool command pattern rules
…hestrator Each tool now has a :uninstall command that cleans up its own binary and PATH entry with spinner UI. The main `pv uninstall` delegates tool cleanup to these commands, then handles global concerns (daemon, DNS, CA, ~/.pv). Also: - Add colima:path for power users who want colima on PATH - Change colima from ExposureNone to ExposureSymlink (AutoExpose stays false) - colima:update re-exposes if already on PATH - All uninstall steps use ui.Step spinners
- Extract daemon:restart as standalone colon-namespaced command - Delegate pv restart → daemon:restart in daemon mode - Rewrite CLAUDE.md from documentation to concise rules/conventions - Expand README with tool management, services, daemon, architecture
Update three e2e scripts that checked ~/.pv/data/composer.phar to use the new path ~/.pv/internal/bin/composer.phar.
PR Review Summary:
|
| Priority | Gap | Severity | Effort |
|---|---|---|---|
| 1 | selfupdate.NeedsUpdate version logic |
9/10 | Low — pure function |
| 2 | Expose with nil WriteShim error path |
8/10 | Low — 5-line test |
| 3 | Expose symlink overwrite/re-expose |
8/10 | Low — extend existing |
| 4 | ExposeFunc callback wiring in phpenv |
7/10 | Medium — needs mock |
| 5 | :path command structure tests |
7/10 | Low — follow pattern |
| 6 | ExposeAll error propagation |
6/10 | Low — inject failing WriteShim |
| 7 | Orchestrator delegation verification | 6/10 | Medium |
Strengths
- Clean tool abstraction — The 5-command pattern is well-designed and consistently applied across all tools
- Good test isolation — All tests use
t.TempDir()andt.Setenv("HOME", ...)per conventions selfupdatepackage encapsulation — Clean 2-function public API with well-structured internalsInternalPathas a function — Smart design for dynamic path resolution (PHP version-dependent paths)- Proper UI helper usage —
ui.Step,ui.StepProgress,ui.Header/ui.Footerused consistently - CLAUDE.md rewrite — Comprehensive and mostly accurate project documentation
- All commands use
RunE— No use ofRun, errors propagate correctly (except the swallowed ones above) - Correct binary storage —
internal/bin/for real binaries,bin/for shims/symlinks
Recommended Action
- Fix critical issues 1-4 — Propagate or handle errors from
Expose/Unexpose/Stop/Delete. Makepv updatereturn non-zero on failures. - Address important issues 5-6 — Fix the import pattern and CLAUDE.md accuracy.
- Add tests for
selfupdate.NeedsUpdate— Pure function, highly testable, high impact. - Consider suggestions 15-22 — Registry encapsulation and shim deduplication are low-effort, high-value improvements.
- Stop discarding errors from tools.Expose() and tools.Unexpose() in 7 locations across install/update/uninstall commands - Handle colima Stop/Delete errors properly — don't remove binary if VM can't be stopped - Log per-tool errors in pv uninstall orchestrator - Track failures in pv update and return non-zero exit code
- Rename All to unexported registry, derive Tool.Name from map key in init() so they can never diverge - Add MustGet(name) that panics on unknown tools — use for compile-time constant names throughout cmd/ - Add TestRegistryIntegrity: validates DisplayName, InternalPath, ExposureShim implies WriteShim - Add TestMustGet: verifies panic on unknown tools - Handle os.Remove error before symlink creation in Expose() - Replace all tools.Get with tools.MustGet where names are constants, remove redundant nil guards
mago:update, composer:update, and colima:update now delegate their download logic to the corresponding :download command instead of inlining it. Only version checking and re-expose logic remains.
…mString - Add tests for NeedsUpdate (newer, same, dev, empty, v-prefix) - Add tests for downloadURL and platformString - Extract githubAPIURL as testable variable - Wrap bare io.ReadAll error with context - Remove no-op identity switch in platformString() - Fix misleading "unreachable" comment in update.go
- Renumber install.go steps 5-8 to 5-9 (was duplicate Step 5) - Show absolute paths for symlinks in README architecture diagram
- Rewrite `pv install` as pure orchestrator with --with flag for version overrides and services (e.g., --with="php:8.2,mago,service[redis:7]") - Non-negotiables: PHP, Composer. Mago is opt-in via --with. - Colima removed from install/setup — lazy-installed on first service:add - Fix `pv setup` gaps: add sudo, DNS resolver, CA trust, shell PATH, and actually spin up selected services - Extract shared bootstrap logic into cmd/bootstrap.go - Make php:install auto-resolve latest version when no arg given - Remove self-test (internal/setup/selftest.go) - Silence usage output on errors, use ui.ErrAlreadyPrinted for clean output - Add -f shorthand for --force - Add parseWith tests
- install.sh: --php 8.4 → --with="php:8.4" - update.sh: remove mago check (no longer installed by default)
Composer.phar is self-executable — no need for a bash shim that resolves PHP and sets env vars. Replace with a simple symlink to composer.phar and export COMPOSER_HOME/COMPOSER_CACHE_DIR via `pv env` instead. - Change composer ExposureType from ExposureShim to ExposureSymlink - Remove composerShimScript and writeComposerShim from shims.go - Add COMPOSER_HOME and COMPOSER_CACHE_DIR exports to `pv env` - Update bootstrap.go shell config to use `eval "$(pv env)"` - Make composer.phar executable (chmod 0755) for symlink approach - Delete composer_e2e_test.go (tested shim behavior) - Update shim_test.go, tool_test.go, env_test.go, e2e/composer.sh
Composer.phar needs php on PATH. The verify script wasn't sourcing pv env, so the php shim wasn't available when running composer.
… composer Doctor's composer isolation check was executing composer.phar which needs php on PATH. Switch to checking COMPOSER_HOME/COMPOSER_CACHE_DIR env vars directly since isolation is now handled by `pv env`. Also rename "Composer shim" to "Composer symlink" and eval pv env in e2e doctor script.
Two bugs: 1. php:install returned early when already installed without checking if global_php was set — now sets it if missing. 2. pv install --force overwrote settings.json with empty GlobalPHP — now loads existing settings and preserves GlobalPHP.
Summary
Toolabstraction (internal/tools/) with five standardized commands per managed binary::download,:path,:install,:update,:uninstall~/.pv/internal/bin/(private), exposed to~/.pv/bin/(user PATH) via shims or symlinksinstall,update,uninstall) delegate to per-tool RunE functions — no duplicated logicpv updateself-updates the pv binary viasyscall.Execre-exec before updating toolspv uninstalldelegates cleanup to each tool's:uninstall, then handles global concerns (daemon, DNS, CA cert)daemon:enable,daemon:disable,daemon:restartpv restartdelegates todaemon:restartin daemon modecolima:pathadded for power users to expose colima on PATHChanges
internal/tools/Toolstruct, registry,Expose/Unexpose/IsExposed/ExposeAllinternal/tools/shims.gophpenv/shim.go)internal/selfupdate/NeedsUpdate,Updatefor pv binary self-updatecmd/*_download.go:downloadcommands for php, mago, composer, colimacmd/*_path.go:pathcommands with--removeflagcmd/*_update.go:updatecommands usingIsExposed()for re-exposurecmd/*_uninstall.go:uninstallcommands with spinner UIcmd/*_install.go:downloadRunEcmd/update.gosyscall.Execre-execcmd/uninstall.go:uninstallcmd/daemon.godaemon:enable,daemon:disable,daemon:restartinternal/config/paths.goComposerPharPath→InternalBinDir, addedMagoPathinternal/phpenv/shim.gotools.ExposeAll()CLAUDE.mdREADME.mdTest plan
go test ./...passesgo build -o pv .builds cleanpv mago:download→ binary in~/.pv/internal/bin/magopv mago:path→ symlink in~/.pv/bin/magopv mago:path --remove→ symlink removedpv mago:install→ download + symlinkpv install --force→ full install with new pathspv update --no-self-update→ updates tools without self-updatepv daemon:restart→ restarts daemon when loaded