diff --git a/internal/cli/banger.go b/internal/cli/banger.go index ba3737d..ee0d716 100644 --- a/internal/cli/banger.go +++ b/internal/cli/banger.go @@ -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 ' 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 --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 { diff --git a/internal/cli/commands_image.go b/internal/cli/commands_image.go index 4860a8a..235fbac 100644 --- a/internal/cli/commands_image.go +++ b/internal/cli/commands_image.go @@ -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 +' for the cataloged paths (see internal/imagecat), +or 'banger image pull ' 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(), diff --git a/internal/cli/commands_kernel.go b/internal/cli/commands_kernel.go index 5f7acbc..3026d07 100644 --- a/internal/cli/commands_kernel.go +++ b/internal/cli/commands_kernel.go @@ -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(), diff --git a/internal/cli/commands_ssh_config.go b/internal/cli/commands_ssh_config.go index 51da09a..87001ef 100644 --- a/internal/cli/commands_ssh_config.go +++ b/internal/cli/commands_ssh_config.go @@ -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 .vm` shortcut", + Short: "Enable plain 'ssh .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 .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 { diff --git a/internal/cli/commands_system.go b/internal/cli/commands_system.go index cad7ad1..db9134b 100644 --- a/internal/cli/commands_system.go +++ b/internal/cli/commands_system.go @@ -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", diff --git a/internal/cli/commands_vm.go b/internal/cli/commands_vm.go index 1db668f..68ba852 100644 --- a/internal/cli/commands_vm.go +++ b/internal/cli/commands_vm.go @@ -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 open a shell in a running VM + banger vm stop | vm restart graceful lifecycle + banger vm kill force-kill if stop hangs + banger vm delete stop + remove disks + banger ps / banger vm list running / all VMs (use --all) + banger vm logs 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 ...", - Short: "Send a signal to a VM process", + Use: "kill ...", + 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|...] ..."), 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 ' 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 ", - Short: "Show VM details", + Use: "show ", + 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 "), 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 ...", - Aliases: []string{"rm"}, - Short: "Delete a VM", + Use: "delete ...", + 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 ..."), 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 [ssh args...]", - Short: "SSH into a running VM", + Use: "ssh [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 -- ' over 'vm ssh -- '. +`), + 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 [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 -- ' 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 > 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 ", - Short: "Show VM logs", + Use: "logs ", + 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] "), ValidArgsFunction: d.completeVMNameOnlyAtPos0, RunE: func(cmd *cobra.Command, args []string) error {