Round-1 convergence from all three reviewers, both findings real:
A version string used to be a path fragment: 'box uninstall
../../../.ssh' resolved below versions/ and rm -rf'd wherever it
landed, 'box use' could point current outside the root, and a hostile
flat-tree VERSION could steer the migration's mv the same way. One
strict validator now gates every caller — only [A-Za-z0-9._+-], no
leading '.' or '-' — byte-identical in install.sh and bin/box like
existing_boxes, diff-guarded in the tests, with traversal regressions
on use, uninstall and the migration (which now refuses BEFORE the tree
moves anywhere).
--force is uninstall's installer-family consent, and --purge-host now
forwards it: teardown-host.sh gets --yes under --force/BOX_YES, so the
combined non-interactive uninstall no longer dies at teardown's own
prompt. CI's drill now runs the combined verb with --force alone (no
BOX_YES, no TTY) — the exact invocation that used to abort.
Also grok's polish, taken: current flips by rename (ln to a side name,
mv -Tf over — no window with no current) in both install.sh and 'box
use'; BOX_REINSTALL swaps by two renames and deletes LAST; and the
single-version path refuses while current is dangling (readlink -f
resolves a missing last component, so the guard checks the DIRECTORY,
not just the string).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
test/cli.sh drives REAL installer runs via BOX_INSTALL_SOURCE (temp
BOX_HOME/BOX_BIN, a fabricated second version for upgrades): fresh install
lands versioned, --version answers through the chain, side-by-side installs,
same-version no-op, BOX_REINSTALL, the 0.6.0 flat-tree migration, stale-
symlink healing, single-version and full uninstalls asserted to zero residue
(a planted survivor makes it scream INCOMPLETE), working-tree refusals, and
byte-identity of the existing_boxes copies. The rehearsal job now installs
via install.sh itself — CI proves the installer under review — and ends with
the uninstall drill: revoke --purge, teardown --yes, box uninstall, then
assert nothing is left (networks, profiles, nft, units, files, symlinks).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/var/lib/incus/users/<uid> was removed for releases without being re-checked,
and a bare [ -d ] from a non-root admin answers 'absent' for a directory that
is very much there. Both the removal guard and the absence assert now test
through $SUDO, closing the one residue the purge promised gone but never
proved.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'box versions' lists what is installed (current + running marked); 'box use'
flips the default, refuses under existing boxes (#66), and asserts the
EFFECTIVE result — current resolves to the version asked for and the chain's
own --version answers it. 'box uninstall' is the real uninstall the old
two-rm-rf-lines of prose never were: one version, or everything in the safe
order (boxes first — refuse or --purge-host — then trees and symlinks), and
it ends by re-checking every promised path is gone, exiting 1 naming
leftovers (the revoke --purge discipline). teardown-host grows --yes/BOX_YES
for the unattended paths and points at 'box uninstall'; the drill reads the
installed tree through current/.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each install lands whole at <root>/versions/<v>; a 'current' symlink names
the default and $BINDIR/box points through it, so switching versions is a
symlink flip, not an uninstall. #66's stance survives where it matters: the
FLIP refuses under existing boxes; installing side-by-side is always safe.
A pre-0.7.0 flat tree is migrated in place (two renames, no copy window),
stale/dangling PATH symlinks heal instead of wedging the no-op check, the
other tier's install is named out loud, and BOX_INSTALL_SOURCE installs
from a local tree or tarball so CI and the drill prove the code under
review, offline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>