cli: rewrite help text for AI-driven discovery

Frontier models tend to discover a CLI by running --help, scanning
the Long description, and inferring the dominant workflow from the
examples. Today's banger help reads like a man page index — every
verb has a one-line Short and nothing else. This rewrites the
groups (banger, vm, vm workspace, image, kernel, system,
ssh-config) so each landing page answers "what is this for, what's
the 80% command, what comes next" in three to ten lines, with
runnable examples.

Also disambiguates the near-twin lifecycle commands so a model
reading the subcommand index can tell stop/kill/delete apart at a
glance:

  start    Start a stopped VM
  stop     Stop a running VM gracefully
  restart  Stop then start a VM
  kill     Force-kill a VM (use when 'vm stop' hangs)
  delete   Stop a VM and remove its disks (irreversible)

vm create / vm ssh / vm logs / vm show pick up Long descriptions
and examples for the same reason. No behaviour changes; help text
only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-26 15:02:08 -03:00
parent 41ced66a54
commit 35bfac3f13
No known key found for this signature in database
GPG key ID: 33112E6833C34679
6 changed files with 251 additions and 30 deletions

View file

@ -21,8 +21,31 @@ func NewBangerCommand() *cobra.Command {
func (d *deps) newRootCommand() *cobra.Command {
root := &cobra.Command{
Use: "banger",
Short: "Manage development VMs and images",
Use: "banger",
Short: "Run development sandboxes as Firecracker microVMs",
Long: strings.TrimSpace(`
banger runs disposable development sandboxes as Firecracker microVMs.
Each sandbox boots in a few seconds, gets its own root filesystem and
network, and exits on demand.
The most common workflow is one command:
banger vm run bare sandbox, drops into ssh
banger vm run ./repo ships a repo into /root/repo, drops into ssh
banger vm run ./repo -- make test ships a repo, runs the command, exits with its status
For a longer-lived VM, use 'banger vm create' to provision and
'banger vm ssh <name>' to attach. 'banger ps' lists running VMs;
'banger vm list --all' shows stopped ones too.
First-time setup, in order:
sudo banger system install install the systemd services
banger doctor confirm the host is ready
banger image pull debian-bookworm fetch a default image
Run 'banger <command> --help' for any subcommand. Run 'banger doctor'
to diagnose host readiness problems.
`),
SilenceUsage: true,
SilenceErrors: true,
RunE: helpNoArgs,
@ -46,7 +69,21 @@ func (d *deps) newDoctorCommand() *cobra.Command {
return &cobra.Command{
Use: "doctor",
Short: "Check host and runtime readiness",
Args: noArgsUsage("usage: banger doctor"),
Long: strings.TrimSpace(`
Check that the host has everything banger needs to boot guests:
required tools (mkfs.ext4, debugfs, dmsetup, ip, iptables, ...), KVM
access, daemon reachability, and per-feature preflight (NAT, DNS
routing, work-disk seeding).
Run 'banger doctor':
- after 'banger system install' to confirm the install took
- after upgrading the host kernel or banger itself
- when 'banger vm run' fails with an unclear error
Exit code is non-zero if any check fails. Warnings are reported but
do not fail the run.
`),
Args: noArgsUsage("usage: banger doctor"),
RunE: func(cmd *cobra.Command, args []string) error {
report, err := d.doctor(cmd.Context())
if err != nil {

View file

@ -15,8 +15,28 @@ import (
func (d *deps) newImageCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "image",
Short: "Manage images",
RunE: helpNoArgs,
Short: "Pull and manage banger images (rootfs + kernel + work-seed)",
Long: strings.TrimSpace(`
A banger image bundles a rootfs.ext4, a kernel, an optional initrd
+ modules, and an optional work-seed (the snapshot used to populate
each new VM's /root). Most users only need 'banger image pull
<catalog-name>' for the cataloged paths (see internal/imagecat),
or 'banger image pull <oci-ref>' for an OCI image.
Subcommands:
pull fetch a bundle by catalog name OR pull an OCI image
register point banger at an existing local rootfs (advanced)
promote copy a registered image's files into banger's managed dir
list show what's installed
show print one image's full record as JSON
delete remove an image (no VMs may reference it)
`),
Example: strings.TrimSpace(`
banger image pull debian-bookworm
banger image pull docker.io/library/alpine:3.20 --kernel-ref generic-6.12
banger image list
`),
RunE: helpNoArgs,
}
cmd.AddCommand(
d.newImageRegisterCommand(),

View file

@ -15,8 +15,31 @@ import (
func (d *deps) newKernelCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "kernel",
Short: "Manage the local kernel catalog",
RunE: helpNoArgs,
Short: "Pull and manage Firecracker-compatible kernels",
Long: strings.TrimSpace(`
Banger boots guests with a separate kernel artifact (vmlinux, plus
optional initrd + modules). Kernels are tracked by name in a local
catalog so multiple images can share one.
Most users never run these commands directly: 'banger image pull'
auto-pulls the kernel referenced by the catalog entry. Use these
commands when you want to inspect what's installed, switch a VM to
a different kernel via 'image register --kernel-ref', or import a
kernel built locally with scripts/make-*-kernel.sh.
Subcommands:
pull download a cataloged kernel by name
list show what's installed (or --available for the catalog)
show inspect one entry as JSON
rm remove a local kernel
import register a kernel built from scripts/make-*-kernel.sh
`),
Example: strings.TrimSpace(`
banger kernel list --available
banger kernel pull generic-6.12
banger kernel import void-kernel --from build/manual/void-kernel
`),
RunE: helpNoArgs,
}
cmd.AddCommand(
d.newKernelListCommand(),

View file

@ -2,6 +2,7 @@ package cli
import (
"fmt"
"strings"
"banger/internal/config"
"banger/internal/daemon"
@ -21,7 +22,7 @@ func newSSHConfigCommand() *cobra.Command {
)
cmd := &cobra.Command{
Use: "ssh-config",
Short: "Manage the optional `ssh <name>.vm` shortcut",
Short: "Enable plain 'ssh <name>.vm' from any terminal",
Long: `Banger keeps a self-contained SSH client config under its own config
directory (never touching ~/.ssh/config on its own). Opt in to the
convenience shortcut that lets you run 'ssh <name>.vm' from any
@ -30,7 +31,15 @@ terminal, bypassing 'banger vm ssh':
banger ssh-config # print status + copy-paste snippet
banger ssh-config --install # add an Include line to ~/.ssh/config
banger ssh-config --uninstall # remove banger's Include from ~/.ssh/config
After --install, 'ssh agent.vm' works the same as 'banger vm ssh
agent', including for tools like rsync, scp, and editor remotes.
`,
Example: strings.TrimSpace(`
banger ssh-config --install
ssh agent.vm
rsync -avz ./code agent.vm:/root/repo/
`),
Args: noArgsUsage("usage: banger ssh-config [--install|--uninstall]"),
RunE: func(cmd *cobra.Command, args []string) error {
if install && uninstall {

View file

@ -35,8 +35,34 @@ func (d *deps) newSystemCommand() *cobra.Command {
var purge bool
cmd := &cobra.Command{
Use: "system",
Short: "Install and manage banger's system services",
RunE: helpNoArgs,
Short: "Install banger's owner-daemon and root-helper systemd units",
Long: strings.TrimSpace(`
Banger ships as two services: an owner-user daemon for
orchestration and a narrow root helper for bridge/tap, NAT, and
Firecracker launch. 'banger system' installs, restarts, inspects,
and removes them.
First-run flow (must be run as root):
sudo banger system install --owner $USER install both services
banger system status confirm they're up
banger doctor check host readiness
After 'install', the owner user can run 'banger ...' day to day
without sudo. Subsequent invocations:
sudo banger system restart bounce both services
sudo banger system uninstall remove services + binaries
sudo banger system uninstall --purge also delete /var/lib/banger
See docs/privileges.md for the full trust model.
`),
Example: strings.TrimSpace(`
sudo banger system install --owner alice
banger system status
sudo banger system uninstall --purge
`),
RunE: helpNoArgs,
}
installCmd := &cobra.Command{
Use: "install",

View file

@ -25,18 +25,43 @@ import (
func (d *deps) newVMCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "vm",
Short: "Manage virtual machines",
RunE: helpNoArgs,
Short: "Manage Firecracker microVM sandboxes",
Long: strings.TrimSpace(`
Lifecycle commands for banger's microVMs.
For most cases you want 'banger vm run' it creates, starts,
provisions ssh, and drops you into the guest in one command. Use
'vm create' / 'vm start' / 'vm ssh' separately when you want a
longer-lived VM you'll come back to.
Quick reference:
banger vm run ephemeral sandbox; --rm to delete on exit
banger vm run ./repo -- make test ship a repo, run a command, exit
banger vm create --name dev persistent VM; pair with 'vm ssh'
banger vm ssh <name> open a shell in a running VM
banger vm stop <name> | vm restart graceful lifecycle
banger vm kill <name> force-kill if stop hangs
banger vm delete <name> stop + remove disks
banger ps / banger vm list running / all VMs (use --all)
banger vm logs <name> guest console + daemon log
banger vm workspace prepare/export ship a repo in, pull diffs back
`),
Example: strings.TrimSpace(`
banger vm run -- uname -a
banger vm run ./project -- npm test
banger vm create --name agent && banger vm ssh agent
`),
RunE: helpNoArgs,
}
cmd.AddCommand(
d.newVMCreateCommand(),
d.newVMRunCommand(),
d.newVMListCommand(),
d.newVMShowCommand(),
d.newVMActionCommand("start", "Start a VM", "vm.start"),
d.newVMActionCommand("stop", "Stop a VM", "vm.stop"),
d.newVMActionCommand("start", "Start a stopped VM", "vm.start"),
d.newVMActionCommand("stop", "Stop a running VM gracefully", "vm.stop"),
d.newVMKillCommand(),
d.newVMActionCommand("restart", "Restart a VM", "vm.restart"),
d.newVMActionCommand("restart", "Stop then start a VM", "vm.restart"),
d.newVMDeleteCommand(),
d.newVMPruneCommand(),
d.newVMSetCommand(),
@ -169,8 +194,16 @@ Three modes:
func (d *deps) newVMKillCommand() *cobra.Command {
var signal string
cmd := &cobra.Command{
Use: "kill <id-or-name>...",
Short: "Send a signal to a VM process",
Use: "kill <id-or-name>...",
Short: "Force-kill a VM (use when 'vm stop' hangs)",
Long: strings.TrimSpace(`
Send a signal directly to the firecracker process. Default is
SIGTERM; pass --signal SIGKILL when the VM is stuck and a graceful
'vm stop' has already failed.
This skips the normal stop sequence (no flush, no clean shutdown).
Prefer 'banger vm stop' for routine teardown.
`),
Args: minArgsUsage(1, "usage: banger vm kill [--signal SIGTERM|SIGKILL|...] <id-or-name>..."),
ValidArgsFunction: d.completeVMNames,
RunE: func(cmd *cobra.Command, args []string) error {
@ -320,8 +353,21 @@ func (d *deps) newVMCreateCommand() *cobra.Command {
)
cmd := &cobra.Command{
Use: "create",
Short: "Create a VM",
Args: noArgsUsage("usage: banger vm create"),
Short: "Create a VM (without entering it)",
Long: strings.TrimSpace(`
Create a microVM in the 'running' state and return its summary.
Unlike 'banger vm run', this does NOT open an ssh session pair it
with 'banger vm ssh <name>' when you want to attach.
Use 'vm create' for a longer-lived VM you'll come back to. Use
'vm run' for one-shot sandboxes (especially with --rm).
`),
Example: strings.TrimSpace(`
banger vm create --name agent
banger vm create --name big --vcpu 8 --memory 16384
banger vm create --no-start --name spare # provision but leave stopped
`),
Args: noArgsUsage("usage: banger vm create"),
RunE: func(cmd *cobra.Command, args []string) error {
params, err := vmCreateParamsFromFlags(cmd, name, imageName, vcpu, memory, systemOverlaySize, workDiskSize, natEnabled, noStart)
if err != nil {
@ -427,8 +473,15 @@ func selectVMListVMs(vms []model.VMRecord, showAll, latest bool) []model.VMRecor
func (d *deps) newVMShowCommand() *cobra.Command {
return &cobra.Command{
Use: "show <id-or-name>",
Short: "Show VM details",
Use: "show <id-or-name>",
Short: "Print full VM record as JSON",
Long: strings.TrimSpace(`
Emit the complete VM record (spec, runtime state, image reference,
created/updated timestamps) as a single JSON object. Suitable for
piping into 'jq' or feeding into automation.
For human-readable summaries use 'banger ps' or 'banger vm stats'.
`),
Args: exactArgsUsage(1, "usage: banger vm show <id-or-name>"),
ValidArgsFunction: d.completeVMNameOnlyAtPos0,
RunE: func(cmd *cobra.Command, args []string) error {
@ -477,9 +530,17 @@ func (d *deps) newVMActionCommand(use, short, method string, aliases ...string)
func (d *deps) newVMDeleteCommand() *cobra.Command {
return &cobra.Command{
Use: "delete <id-or-name>...",
Aliases: []string{"rm"},
Short: "Delete a VM",
Use: "delete <id-or-name>...",
Aliases: []string{"rm"},
Short: "Stop a VM and remove its disks (irreversible)",
Long: strings.TrimSpace(`
Stop the VM if it's running, then remove its work disk, system
overlay, snapshot, and metadata. Frees host disk space. The
operation is irreversible anything written inside the guest is
lost.
Use 'banger vm prune' to bulk-delete every VM that isn't running.
`),
Args: minArgsUsage(1, "usage: banger vm delete <id-or-name>..."),
ValidArgsFunction: d.completeVMNames,
RunE: func(cmd *cobra.Command, args []string) error {
@ -559,8 +620,22 @@ func (d *deps) newVMSetCommand() *cobra.Command {
func (d *deps) newVMSSHCommand() *cobra.Command {
return &cobra.Command{
Use: "ssh <id-or-name> [ssh args...]",
Short: "SSH into a running VM",
Use: "ssh <id-or-name> [ssh args...]",
Short: "Open an interactive ssh session to a running VM",
Long: strings.TrimSpace(`
Connect to a running VM as root over the host bridge. Trailing
arguments are passed through to the underlying 'ssh' command, so
'-- -L 8080:localhost:8080' forwards a port and '-- echo hi' runs
a single command and exits.
To run a one-shot command without holding a session, prefer
'banger vm run --rm -- <command>' over 'vm ssh -- <command>'.
`),
Example: strings.TrimSpace(`
banger vm ssh agent
banger vm ssh agent -- uname -a
banger vm ssh agent -- -L 8080:localhost:8080 -N
`),
Args: minArgsUsage(1, "usage: banger vm ssh <id-or-name> [ssh args...]"),
ValidArgsFunction: d.completeVMNameOnlyAtPos0,
RunE: func(cmd *cobra.Command, args []string) error {
@ -587,8 +662,29 @@ func (d *deps) newVMSSHCommand() *cobra.Command {
func (d *deps) newVMWorkspaceCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "workspace",
Short: "Manage repository workspaces inside a running VM",
RunE: helpNoArgs,
Short: "Ship a host repo into a guest, pull diffs back",
Long: strings.TrimSpace(`
Two-step pattern for round-tripping a working tree through a guest
VM:
prepare Copy a local git repo into the guest at /root/repo
(or any path you choose). Default ships tracked files
only; pass --include-untracked to ship the rest.
export Capture every change inside the guest workspace as a
host-readable patch. Non-mutating: the guest's working
tree is left untouched.
This is the supported flow for AI agents and CI runners that want
to evaluate code changes inside a sandbox without touching the
host checkout. 'banger vm run ./repo -- <cmd>' is shorthand for
prepare + run + delete.
`),
Example: strings.TrimSpace(`
banger vm workspace prepare agent ../repo
banger vm ssh agent -- bash -lc 'cd /root/repo && make test'
banger vm workspace export agent --base-commit <commit> > out.patch
`),
RunE: helpNoArgs,
}
cmd.AddCommand(
d.newVMWorkspacePrepareCommand(),
@ -723,8 +819,18 @@ func (d *deps) newVMWorkspaceExportCommand() *cobra.Command {
func (d *deps) newVMLogsCommand() *cobra.Command {
var follow bool
cmd := &cobra.Command{
Use: "logs <id-or-name>",
Short: "Show VM logs",
Use: "logs <id-or-name>",
Short: "Show guest console + per-VM daemon log",
Long: strings.TrimSpace(`
Print the firecracker console log (kernel + early init output) and
the per-VM daemon log (lifecycle stages, errors). Pass -f to follow
new lines as they arrive useful while a VM is starting up or
hanging on boot.
`),
Example: strings.TrimSpace(`
banger vm logs agent
banger vm logs agent -f
`),
Args: exactArgsUsage(1, "usage: banger vm logs [-f] <id-or-name>"),
ValidArgsFunction: d.completeVMNameOnlyAtPos0,
RunE: func(cmd *cobra.Command, args []string) error {