0.22.0

Released 2026-10-27

Added

  • composer <name> through the shim runs the root composer.json script of that name with viv run <name>, as Composer does when no Composer command has that name. The shim used to pass it to a real Composer, and failed where none is installed (#379)
  • A native adapter for automattic/jetpack-autoloader: viv install on a project that requires it runs without --no-plugins, and writes vendor/jetpack-autoloader/ and vendor/autoload_packages.php byte for byte as Composer 2.10.2 does for install and install -o (#389)
  • viv install in a project with a composer.json and no composer.lock prints Composer's "No composer.lock file present" warning, resolves, writes composer.lock and installs, as Composer does, instead of stopping. Only the flags update shares with install carry over; -o, -a, --apcu-autoloader and --adopt take their defaults (#362)
  • viv dump-autoload works without a composer.lock, reading the package list from vendor/composer/installed.json as Composer does, and leaves installed.json untouched in that case. --dev forces dev mode

Changed

  • viv dump-autoload without --dev or --no-dev follows the dev key of vendor/composer/installed.json, as Composer does, so a vendor/ installed with --no-dev dumps without its dev packages. It used to dump in dev mode every time
  • viv --help, the man pages and the crate description call viv a package manager that installs from composer.lock, resolving it first when it is missing, instead of saying install works only from the lock (#366)

Fixed

  • viv update ranks a numbered pre-release such as 4.4.0-RC1 below 4.4.0, as Composer does, so minimum-stability: dev locks the stable release. The semver-php 0.1.0 crate ranked 4.4.0-RC1 above 4.4.0. viv now carries a fixed copy of that crate in src/semver/php/, so the build published to crates.io has the fix too (#386)
  • viv update --lock and viv validate --fix write the lock's minimum-stability, stability-flags and prefer-stable from the current composer.json, as Composer does, and update --lock writes prefer-lowest from its own --prefer-lowest flag instead of keeping the old lock's value (validate --fix keeps it, as it has no such flag). They used to copy all three from the old lock, so a changed minimum-stability, a new prefer-stable or a require with an @beta suffix stayed out of the lock (#390)
  • viv update --lock and viv validate --fix write the lock's platform, platform-dev and platform-overrides from the current composer.json, as Composer does. They used to refresh only content-hash, so a changed php constraint or a new config.platform entry stayed out of the lock (#380)
  • viv install extracts a zip dist that is a local file: a dist.url that is a path relative to the project root (./private/plugin.zip), an absolute path or a file:// URL. It used to stop with invalid dist URL ...: relative URL without a base for an inline package repository, which Composer installs. viv reads the file again on every install instead of keeping a copy in the cache by URL, so a zip replaced under the same URL is not served stale, and it checks dist.shasum when the lock has one (#388)
  • --link-mode works on every command that links packages into vendor/: update, add, rm, new, init, workspace init, workspace add and isolate. The warning "Pass --link-mode copy to silence this warning" names a flag that only viv install accepted, so the other commands answered it with unexpected argument '--link-mode'. viv install --link-mode copy on a project with no composer.lock now passes the flag on to the update it falls back to (#387)
  • viv update finds a version newly published to a composer repository that serves one full packages.json and no /p2/ endpoints, as Satis and SatisPress do. viv used to keep that file for ten minutes without a request, so an update inside the window kept the old list. It now revalidates the file on every update with a conditional request, or downloads it again when the server sends no Last-Modified. Repositories with /p2/ or provider endpoints, such as packagist.org and wpackagist, keep the ten-minute window, and --metadata-ttl still sets a window for the full file (#377)
  • viv update keeps one metadata cache per composer repository URL, as Composer does. Two repositories on the same host, with another path, port or scheme, used to share one packages.json and its provider files, so one repository could read the other's cached package list and send the other's Last-Modified in its conditional request. The first run after upgrading downloads each repository's metadata once, because the cache moves from repo-v0/ to repo-v1/; viv cache prune removes the old directory (#378)
  • viv install, viv update, viv require, viv remove, viv new and viv diagnose read the PHP extensions from the PHP build that config.platform.php pins, the same build viv run uses, instead of the php on PATH. A project that pins PHP 8.4 no longer fails with ext-dom missing when no php is on PATH. These commands never install the pinned build: when it is not installed, or config.platform.php is a version no build exists for, they use the php on PATH, as for a project with no pin. viv lock merge re-solves against the same php. When one of these commands refuses a php, ext-* or lib-* requirement, the error now ends with a line naming the php it read the platform packages from, and when config.platform.php pins a build that is not installed it says how to install it (#385)
  • viv require writes the original composer.json and composer.lock back when the resolution fails, and prints Composer's "Installation failed, reverting ..." line. It used to leave composer.json holding the new requirement, reindented and with its keys reordered. viv rm writes composer.json back when its update fails before the lock is written, and prints "Removal failed, reverting ./composer.json to its original content." Both revert only until the lock is written, as Composer does (#288)
  • viv cache prune keeps the git-v0 bucket of git source mirrors. It deleted the mirrors of source.type: git packages, so the next install cloned each repository again (#360)
  • A partial update that needs a newer version of a package held at its locked version says the package is fixed to that version by the partial update and, unless -W was passed, points to --with-all-dependencies. It used to say the package "could not be found in any version" (#363)
  • A package from a VCS repository gets Composer's lock entry: license and a scripts listener are one-item arrays, and a GitHub repository with issues enabled gets support.issues (#374)
  • installed.json, installed.php, platform_check.php and .vivace-state in vendor/composer/, and the composer.json that viv normalize rewrites, are written with the umask mode instead of 0600, so PHP running as another user can read them. A rewrite keeps the mode of the file it replaces, as Composer's does (#375)