vm run: add -d/--detach + transparent tooling bootstrap

The mise tooling bootstrap was failing silently when --nat wasn't
set: the VM came up, the user landed in ssh, and tools were missing
with no obvious cause. Two coupled fixes:

* `-d`/`--detach`: create + prep + bootstrap, exit without attaching
  to ssh. Reconnect later with `banger vm ssh <name>`. Rejects the
  ambiguous combos `-d --rm` and `-d -- <cmd>`.

* NAT precondition: when the workspace has a .mise.toml or
  .tool-versions, vm run now refuses before VM creation if --nat
  isn't set. Error message points at --nat or --no-bootstrap.

* `--no-bootstrap`: explicit opt-out for users who want a vanilla
  VM with their workspace and no tooling install.

Detached bootstrap runs synchronously (foreground tee'd to the log
file) so the CLI only returns once installs finish. Interactive
mode keeps today's nohup'd background behaviour so the ssh session
starts promptly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-05-01 14:51:16 -03:00
parent 9b5cbed32d
commit aaf49fc1b1
No known key found for this signature in database
GPG key ID: 33112E6833C34679
5 changed files with 394 additions and 13 deletions

View file

@ -1324,6 +1324,8 @@ func TestRunVMRunWorkspacePreparesAndAttaches(t *testing.T) {
&repo,
nil,
false,
false,
false,
)
if err != nil {
t.Fatalf("d.runVMRun: %v", err)
@ -1400,6 +1402,8 @@ func TestVMRunPrintsPostCreateProgress(t *testing.T) {
&repo,
nil,
false,
false,
false,
)
if err != nil {
t.Fatalf("d.runVMRun: %v", err)
@ -1475,6 +1479,8 @@ func TestRunVMRunWarnsWhenToolingHarnessStartFails(t *testing.T) {
&repo,
nil,
false,
false,
false,
)
if err != nil {
t.Fatalf("d.runVMRun: %v", err)
@ -1526,6 +1532,8 @@ func TestRunVMRunBareModeSkipsWorkspaceAndTooling(t *testing.T) {
nil,
nil,
false,
false,
false,
)
if err != nil {
t.Fatalf("d.runVMRun: %v", err)
@ -1569,7 +1577,9 @@ func TestRunVMRunRMDeletesAfterSessionExits(t *testing.T) {
api.VMCreateParams{Name: "tmpbox"},
nil,
nil,
true, // --rm
true, // --rm,
false,
false,
)
if err != nil {
t.Fatalf("d.runVMRun: %v", err)
@ -1619,7 +1629,9 @@ func TestRunVMRunRMSkipsDeleteOnSSHWaitTimeout(t *testing.T) {
api.VMCreateParams{Name: "slowvm"},
nil,
nil,
true, // --rm
true, // --rm,
false,
false,
)
if err == nil {
t.Fatal("want timeout error")
@ -1662,6 +1674,8 @@ func TestRunVMRunSSHTimeoutReturnsActionableError(t *testing.T) {
nil,
nil,
false,
false,
false,
)
if err == nil {
t.Fatal("want timeout error")
@ -1711,6 +1725,8 @@ func TestRunVMRunCommandModePropagatesExitCode(t *testing.T) {
nil,
[]string{"false"},
false,
false,
false,
)
var exitErr ExitCodeError
if !errors.As(err, &exitErr) || exitErr.Code != 7 {