Remove the banger TUI

Hard cut the terminal UI so the supported management surface is the daemon-backed CLI only.

Drop the tui subcommand, delete the Bubble Tea implementation and its tests, and keep a regression check that the legacy command is rejected.
Prune the Charmbracelet dependencies with go mod tidy and remove the stale README and AGENTS references.

Validated with go test ./... and GOCACHE=/tmp/banger-gocache go test ./internal/cli.
This commit is contained in:
Thales Maciel 2026-03-19 22:56:16 -03:00
parent 7667249b47
commit 0c80d03081
No known key found for this signature in database
GPG key ID: 33112E6833C34679
8 changed files with 13 additions and 2290 deletions

View file

@ -21,7 +21,6 @@
- `./banger vm stop vm-a vm-b vm-c` and `./banger vm set --nat web-1 web-2` are supported; multi-VM lifecycle and `set` actions fan out concurrently through the CLI.
- `./banger doctor` reports runtime bundle, host tool, feature, and image-build readiness from the same Go checks used by the daemon.
- `./banger image register --name local --rootfs /abs/path/rootfs.ext4` creates or updates an unmanaged image record without changing the default image config; use it for experimental guest iteration paths such as Void.
- `./banger tui` launches the terminal UI.
- `make test` runs `go test ./...`.
- `./verify.sh` runs the smoke test for the Go VM workflow.

View file

@ -1,6 +1,6 @@
# banger
Persistent Firecracker development VMs managed through a Go daemon, CLI, and TUI.
Persistent Firecracker development VMs managed through a Go daemon and CLI.
## Requirements
- Linux host with KVM (`/dev/kvm` access)
@ -137,11 +137,6 @@ banger vm kill --signal KILL aa12bb34 cc56dd78
banger vm set --nat web-1 web-2 web-3
```
Launch the TUI:
```bash
banger tui
```
## Daemon
The CLI auto-starts `bangerd` when needed.

15
go.mod
View file

@ -3,11 +3,7 @@ module banger
go 1.25.0
require (
github.com/charmbracelet/bubbles v0.14.0
github.com/charmbracelet/bubbletea v0.21.1-0.20220623121936-ca32c4c62873
github.com/charmbracelet/lipgloss v0.5.1-0.20220407020210-a86f21a0ae43
github.com/firecracker-microvm/firecracker-go-sdk v1.0.0
github.com/mattn/go-isatty v0.0.20
github.com/miekg/dns v1.1.72
github.com/pelletier/go-toml v1.9.5
github.com/sirupsen/logrus v1.9.4
@ -21,8 +17,6 @@ require (
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/containerd/fifo v1.0.0 // indirect
github.com/containernetworking/cni v1.0.1 // indirect
github.com/containernetworking/plugins v1.0.1 // indirect
@ -43,22 +37,16 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/socket v0.2.0 // indirect
github.com/mdlayher/vsock v1.1.1 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.1 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5 // indirect
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect
@ -67,7 +55,6 @@ require (
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.40.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

38
go.sum
View file

@ -72,8 +72,6 @@ github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:l
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ=
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@ -93,15 +91,6 @@ github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3k
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/bubbles v0.14.0 h1:DJfCwnARfWjZLvMglhSQzo76UZ2gucuHPy9jLWX45Og=
github.com/charmbracelet/bubbles v0.14.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc=
github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4=
github.com/charmbracelet/bubbletea v0.21.1-0.20220623121936-ca32c4c62873 h1:ti/1QRoSzanYHPW4jLgIjCkfJ3beXh2h1nr6nEkWOig=
github.com/charmbracelet/bubbletea v0.21.1-0.20220623121936-ca32c4c62873/go.mod h1:aoVIwlNlr5wbCB26KhxfrqAn0bMp4YpJcoOelbxApjs=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
github.com/charmbracelet/lipgloss v0.5.1-0.20220407020210-a86f21a0ae43 h1:xO5Bh21Ii+0p3EYp1GdFEF/Iax7VhBgMbBVCOFBZ2/Q=
github.com/charmbracelet/lipgloss v0.5.1-0.20220407020210-a86f21a0ae43/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs=
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
@ -132,8 +121,6 @@ github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on
github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=
github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
@ -487,9 +474,6 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@ -502,14 +486,9 @@ github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kN
github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
@ -540,17 +519,6 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/cancelreader v0.2.1 h1:Xzd1B4U5bWQOuSKuN398MyynIGTNT89dxzpEDsalXZs=
github.com/muesli/cancelreader v0.2.1/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI=
github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
@ -651,9 +619,6 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@ -662,7 +627,6 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
@ -952,12 +916,10 @@ golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=

View file

@ -60,7 +60,7 @@ func NewBangerCommand() *cobra.Command {
RunE: helpNoArgs,
}
root.CompletionOptions.DisableDefaultCmd = true
root.AddCommand(newDaemonCommand(), newDoctorCommand(), newVMCommand(), newImageCommand(), newTUICommand(), newInternalCommand())
root.AddCommand(newDaemonCommand(), newDoctorCommand(), newVMCommand(), newImageCommand(), newInternalCommand())
return root
}

View file

@ -25,12 +25,21 @@ func TestNewBangerCommandHasExpectedSubcommands(t *testing.T) {
for _, sub := range cmd.Commands() {
names = append(names, sub.Name())
}
want := []string{"daemon", "doctor", "image", "internal", "tui", "vm"}
want := []string{"daemon", "doctor", "image", "internal", "vm"}
if !reflect.DeepEqual(names, want) {
t.Fatalf("subcommands = %v, want %v", names, want)
}
}
func TestLegacyRemovedCommandIsRejected(t *testing.T) {
cmd := NewBangerCommand()
cmd.SetArgs([]string{"tui"})
err := cmd.Execute()
if err == nil || !strings.Contains(err.Error(), "unknown command \"tui\"") {
t.Fatalf("Execute() error = %v, want unknown legacy command", err)
}
}
func TestDoctorCommandPrintsReportAndFailsOnHardFailures(t *testing.T) {
original := doctorFunc
t.Cleanup(func() {

File diff suppressed because it is too large Load diff

View file

@ -1,396 +0,0 @@
package cli
import (
"context"
"errors"
"os"
"path/filepath"
"strings"
"testing"
"time"
"banger/internal/api"
"banger/internal/model"
"banger/internal/paths"
tea "github.com/charmbracelet/bubbletea"
)
func TestCreateVMFormSubmit(t *testing.T) {
form := newCreateVMForm([]model.Image{{Name: "default"}}, model.DaemonConfig{DefaultImageName: "default"})
form.fields[0].input.SetValue("devbox")
form.fields[2].input.SetValue("4")
form.fields[3].input.SetValue("2048")
form.fields[4].input.SetValue("12G")
form.fields[5].input.SetValue("24G")
form.fields[6].index = 1
action, err := form.submit()
if err != nil {
t.Fatalf("submit: %v", err)
}
if action.kind != actionCreate {
t.Fatalf("kind = %s, want %s", action.kind, actionCreate)
}
if action.create.Name != "devbox" || action.create.ImageName != "default" {
t.Fatalf("unexpected create params: %+v", action.create)
}
if action.create.VCPUCount == nil || *action.create.VCPUCount != 4 || action.create.MemoryMiB == nil || *action.create.MemoryMiB != 2048 {
t.Fatalf("unexpected cpu/memory: %+v", action.create)
}
if action.create.SystemOverlaySize != "12G" || action.create.WorkDiskSize != "24G" {
t.Fatalf("unexpected disk sizes: %+v", action.create)
}
if !action.create.NATEnabled {
t.Fatalf("expected NAT enabled: %+v", action.create)
}
}
func TestEditVMFormSubmit(t *testing.T) {
form := newEditVMForm(model.VMRecord{
ID: "vm-1",
Spec: model.VMSpec{
VCPUCount: 2,
MemoryMiB: 1024,
WorkDiskSizeBytes: 16 * 1024 * 1024 * 1024,
NATEnabled: false,
},
})
form.fields[0].input.SetValue("6")
form.fields[1].input.SetValue("4096")
form.fields[2].input.SetValue("32G")
form.fields[3].index = 1
action, err := form.submit()
if err != nil {
t.Fatalf("submit: %v", err)
}
if action.kind != actionEdit {
t.Fatalf("kind = %s, want %s", action.kind, actionEdit)
}
if action.set.IDOrName != "vm-1" {
t.Fatalf("unexpected vm id: %+v", action.set)
}
if action.set.VCPUCount == nil || *action.set.VCPUCount != 6 {
t.Fatalf("unexpected vcpu: %+v", action.set)
}
if action.set.MemoryMiB == nil || *action.set.MemoryMiB != 4096 {
t.Fatalf("unexpected memory: %+v", action.set)
}
if action.set.WorkDiskSize != "32G" {
t.Fatalf("unexpected disk size: %+v", action.set)
}
if action.set.NATEnabled == nil || !*action.set.NATEnabled {
t.Fatalf("expected nat enabled: %+v", action.set)
}
}
func TestResolveSelectedID(t *testing.T) {
vms := []model.VMRecord{{ID: "one"}, {ID: "two"}}
if got := resolveSelectedID("two", vms); got != "two" {
t.Fatalf("resolveSelectedID existing = %q, want %q", got, "two")
}
if got := resolveSelectedID("missing", vms); got != "one" {
t.Fatalf("resolveSelectedID fallback = %q, want %q", got, "one")
}
if got := resolveSelectedID("anything", nil); got != "" {
t.Fatalf("resolveSelectedID empty = %q, want empty", got)
}
}
func TestNewTUICommandStartsProgramWithoutEnsuringDaemon(t *testing.T) {
origEnsure := tuiEnsureDaemonFunc
origRunner := tuiProgramRunner
origTerminal := tuiIsTerminal
t.Cleanup(func() {
tuiEnsureDaemonFunc = origEnsure
tuiProgramRunner = origRunner
tuiIsTerminal = origTerminal
})
ensureCalled := false
tuiEnsureDaemonFunc = func(ctx context.Context) (paths.Layout, model.DaemonConfig, error) {
ensureCalled = true
return paths.Layout{}, model.DaemonConfig{}, nil
}
tuiProgramRunner = func(model tuiModel) error {
if ensureCalled {
t.Fatal("ensureDaemon should not run before the TUI starts")
}
if !model.daemonPending || !model.loading {
t.Fatalf("startup model = %+v, want pending daemon startup", model)
}
return nil
}
tuiIsTerminal = func(fd uintptr) bool { return true }
cmd := NewBangerCommand()
cmd.SetArgs([]string{"tui"})
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute: %v", err)
}
if ensureCalled {
t.Fatal("ensureDaemon should not have been called")
}
}
func TestTUIViewRendersLayoutImmediately(t *testing.T) {
m := newTUIModel(paths.Layout{}, model.DaemonConfig{})
view := m.View()
if strings.Contains(view, "Loading...") {
t.Fatalf("view = %q, want full layout instead of one-line loading", view)
}
if !strings.Contains(view, "Starting daemon") {
t.Fatalf("view = %q, want startup placeholder", view)
}
}
func TestTUIVMLoadCanCompleteBeforeImages(t *testing.T) {
now := time.Date(2026, time.March, 18, 12, 0, 0, 0, time.UTC)
initial := newTUIModel(paths.Layout{}, model.DaemonConfig{})
updated, _ := initial.Update(daemonReadyMsg{
generation: initial.loadGeneration,
layout: paths.Layout{SocketPath: "/tmp/bangerd.sock"},
cfg: model.DaemonConfig{DefaultImageName: "default"},
duration: 2400 * time.Millisecond,
})
m := updated.(tuiModel)
if !m.daemonReady || !m.vmListPending || !m.imagePending {
t.Fatalf("model after daemonReady = %+v, want pending vm/image loads", m)
}
vm := model.VMRecord{
ID: "vm-1",
Name: "devbox",
State: model.VMStateRunning,
CreatedAt: now,
UpdatedAt: now,
LastTouchedAt: now,
Spec: model.VMSpec{
VCPUCount: 2,
MemoryMiB: 1024,
WorkDiskSizeBytes: 16 * 1024 * 1024 * 1024,
},
Runtime: model.VMRuntime{
GuestIP: "172.16.0.2",
DNSName: "devbox.vm",
},
}
updated, _ = m.Update(vmListLoadedMsg{
generation: m.loadGeneration,
vms: []model.VMRecord{vm},
duration: 20 * time.Millisecond,
})
m = updated.(tuiModel)
if len(m.vms) != 1 || m.selectedID != vm.ID {
t.Fatalf("model after vmListLoaded = %+v, want selected vm", m)
}
if !m.imagePending {
t.Fatalf("image load should still be pending: %+v", m)
}
if strings.Contains(m.View(), "No VMs") {
t.Fatalf("view should render the loaded VM while images are pending: %q", m.View())
}
if !strings.Contains(m.View(), "devbox") {
t.Fatalf("view = %q, want loaded VM name", m.View())
}
}
func TestTUICreateBlockedWhileImagesLoad(t *testing.T) {
m := newTUIModel(paths.Layout{}, model.DaemonConfig{})
m.daemonPending = false
m.daemonReady = true
m.imagePending = true
m.loading = true
updated, _ := m.updateBrowse(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'c'}})
if updated.mode != tuiModeBrowse {
t.Fatalf("mode = %v, want browse", updated.mode)
}
if updated.statusText != "Images are still loading" {
t.Fatalf("status = %q, want image loading warning", updated.statusText)
}
}
func TestTUIStatusIncludesStageDurationsAfterInitialLoad(t *testing.T) {
initial := newTUIModel(paths.Layout{}, model.DaemonConfig{})
updated, _ := initial.Update(daemonReadyMsg{
generation: initial.loadGeneration,
layout: paths.Layout{SocketPath: "/tmp/bangerd.sock"},
duration: 2400 * time.Millisecond,
})
m := updated.(tuiModel)
updated, _ = m.Update(vmListLoadedMsg{
generation: m.loadGeneration,
vms: []model.VMRecord{},
duration: 20 * time.Millisecond,
})
m = updated.(tuiModel)
updated, _ = m.Update(imageListLoadedMsg{
generation: m.loadGeneration,
images: []model.Image{{Name: "default"}},
duration: 15 * time.Millisecond,
})
m = updated.(tuiModel)
if !strings.Contains(m.statusText, "daemon 2.4s") || !strings.Contains(m.statusText, "vm list 20ms") || !strings.Contains(m.statusText, "image list 15ms") {
t.Fatalf("statusText = %q, want stage timings", m.statusText)
}
}
func TestSSHDoneMsgShowsReminderWhenHealthCheckPasses(t *testing.T) {
origHealth := vmHealthFunc
t.Cleanup(func() {
vmHealthFunc = origHealth
})
vmHealthFunc = func(ctx context.Context, socketPath, idOrName string) (api.VMHealthResult, error) {
return api.VMHealthResult{Name: "devbox", Healthy: true}, nil
}
msg := sshDoneMsg(paths.Layout{SocketPath: "/tmp/bangerd.sock"}, actionRequest{id: "devbox", name: "devbox"}, "devbox", nil)
result, ok := msg.(actionResultMsg)
if !ok {
t.Fatalf("msg = %T, want actionResultMsg", msg)
}
if !strings.Contains(result.status, "devbox is still running") {
t.Fatalf("status = %q, want reminder", result.status)
}
}
func TestSSHDoneMsgShowsWarningWhenHealthCheckFails(t *testing.T) {
origHealth := vmHealthFunc
t.Cleanup(func() {
vmHealthFunc = origHealth
})
vmHealthFunc = func(ctx context.Context, socketPath, idOrName string) (api.VMHealthResult, error) {
return api.VMHealthResult{}, errors.New("dial failed")
}
msg := sshDoneMsg(paths.Layout{SocketPath: "/tmp/bangerd.sock"}, actionRequest{id: "devbox", name: "devbox"}, "devbox", nil)
result := msg.(actionResultMsg)
if !strings.Contains(result.status, "failed to check whether devbox is still running") {
t.Fatalf("status = %q, want warning", result.status)
}
}
func TestAggregateRunningVMResources(t *testing.T) {
t.Parallel()
running, vcpus, memoryBytes := aggregateRunningVMResources([]model.VMRecord{
{
State: model.VMStateRunning,
Spec: model.VMSpec{
VCPUCount: 2,
MemoryMiB: 1024,
},
},
{
State: model.VMStateStopped,
Spec: model.VMSpec{
VCPUCount: 8,
MemoryMiB: 8192,
},
},
{
State: model.VMStateRunning,
Spec: model.VMSpec{
VCPUCount: 4,
MemoryMiB: 2048,
},
},
})
if running != 2 || vcpus != 6 || memoryBytes != 3*1024*1024*1024 {
t.Fatalf("aggregateRunningVMResources = (%d, %d, %d), want (2, 6, %d)", running, vcpus, memoryBytes, int64(3*1024*1024*1024))
}
}
func TestTUIViewShowsResourceBar(t *testing.T) {
t.Parallel()
m := newTUIModel(paths.Layout{}, model.DaemonConfig{})
m.hostCPUCount = 32
m.hostMemoryBytes = 125 * 1024 * 1024 * 1024
m.hostDiskBytes = 200 * 1024 * 1024 * 1024
m.daemonPending = false
m.loading = false
stateDir := t.TempDir()
overlayPath := filepath.Join(stateDir, "system.cow")
workDiskPath := filepath.Join(stateDir, "root.ext4")
if err := os.WriteFile(overlayPath, make([]byte, 1024), 0o644); err != nil {
t.Fatalf("WriteFile overlay: %v", err)
}
if err := os.WriteFile(workDiskPath, make([]byte, 2048), 0o644); err != nil {
t.Fatalf("WriteFile work disk: %v", err)
}
m.vms = []model.VMRecord{
{
ID: "vm-1",
Name: "devbox",
State: model.VMStateRunning,
Spec: model.VMSpec{
VCPUCount: 2,
MemoryMiB: 1024,
WorkDiskSizeBytes: 16 * 1024 * 1024 * 1024,
},
Runtime: model.VMRuntime{
SystemOverlay: overlayPath,
WorkDiskPath: workDiskPath,
},
},
{
ID: "vm-2",
Name: "db",
State: model.VMStateStopped,
Spec: model.VMSpec{
VCPUCount: 4,
MemoryMiB: 4096,
WorkDiskSizeBytes: 32 * 1024 * 1024 * 1024,
},
},
}
m.selectedID = "vm-1"
m.rebuildTable()
m.refreshDetail()
view := m.View()
if !strings.Contains(view, "VMs") || !strings.Contains(view, "1/2") {
t.Fatalf("view = %q, want running VM count", view)
}
if !strings.Contains(view, "CPU") || !strings.Contains(view, "2/32") {
t.Fatalf("view = %q, want vcpu aggregate", view)
}
if !strings.Contains(view, "RAM") || !strings.Contains(view, "1.0G/125.0G") {
t.Fatalf("view = %q, want memory aggregate", view)
}
if !strings.Contains(view, "Disk") {
t.Fatalf("view = %q, want disk aggregate", view)
}
if !strings.Contains(view, "█") || !strings.Contains(view, "░") {
t.Fatalf("view = %q, want visual progress bars", view)
}
}
func TestAggregateVMDiskUsage(t *testing.T) {
t.Parallel()
dir := t.TempDir()
overlayPath := filepath.Join(dir, "system.cow")
workDiskPath := filepath.Join(dir, "root.ext4")
if err := os.WriteFile(overlayPath, make([]byte, 4096), 0o644); err != nil {
t.Fatalf("WriteFile overlay: %v", err)
}
if err := os.WriteFile(workDiskPath, make([]byte, 8192), 0o644); err != nil {
t.Fatalf("WriteFile work disk: %v", err)
}
total := aggregateVMDiskUsage([]model.VMRecord{{
Runtime: model.VMRuntime{
SystemOverlay: overlayPath,
WorkDiskPath: workDiskPath,
},
}})
if total <= 0 {
t.Fatalf("aggregateVMDiskUsage = %d, want positive allocated bytes", total)
}
}