package cli import ( "errors" "fmt" "path/filepath" "strings" "banger/internal/api" "banger/internal/buildinfo" "github.com/spf13/cobra" ) // NewBangerCommand builds the top-level cobra tree with production // defaults wired into the dependency struct. Tests reach into the // package directly — see newRootCommand + defaultDeps. func NewBangerCommand() *cobra.Command { return defaultDeps().newRootCommand() } func (d *deps) newRootCommand() *cobra.Command { root := &cobra.Command{ Use: "banger", Version: formatVersionLine(buildinfo.Current()), 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, } // Drop cobra's default "{{.Name}} version {{.Version}}" wrapper — // our Version string is already a complete sentence. root.SetVersionTemplate("{{.Version}}\n") root.AddCommand( d.newDaemonCommand(), d.newDoctorCommand(), d.newImageCommand(), d.newInternalCommand(), d.newKernelCommand(), newSSHConfigCommand(), d.newSystemCommand(), d.newUpdateCommand(), newVersionCommand(), d.newPSCommand(), d.newVMCommand(), ) return root } func (d *deps) newDoctorCommand() *cobra.Command { var verbose bool cmd := &cobra.Command{ Use: "doctor", Short: "Check host and runtime readiness", 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 By default, prints failing and warning checks only and a summary footer; a healthy host collapses to a single line. Pass --verbose to print every check with its details. 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 { return err } if err := printDoctorReport(cmd.OutOrStdout(), report, verbose); err != nil { return err } if report.HasFailures() { return errors.New("doctor found failing checks") } return nil }, } cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "show every check (default: only failures and warnings)") return cmd } func newVersionCommand() *cobra.Command { return &cobra.Command{ Use: "version", Short: "Show banger build information", Args: noArgsUsage("usage: banger version"), RunE: func(cmd *cobra.Command, args []string) error { _, err := fmt.Fprint(cmd.OutOrStdout(), formatBuildInfoBlock(buildinfo.Current())) return err }, } } func helpNoArgs(cmd *cobra.Command, args []string) error { if len(args) != 0 { return fmt.Errorf("unknown arguments: %s", strings.Join(args, " ")) } return cmd.Help() } func noArgsUsage(usage string) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) != 0 { return errors.New(usage) } return nil } } func exactArgsUsage(n int, usage string) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) != n { return errors.New(usage) } return nil } } func minArgsUsage(n int, usage string) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) < n { return errors.New(usage) } return nil } } func maxArgsUsage(n int, usage string) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) > n { return errors.New(usage) } return nil } } func shellQuote(value string) string { return "'" + strings.ReplaceAll(value, "'", `'"'"'`) + "'" } func absolutizeImageRegisterPaths(params *api.ImageRegisterParams) error { return absolutizePaths( ¶ms.RootfsPath, ¶ms.WorkSeedPath, ¶ms.KernelPath, ¶ms.InitrdPath, ¶ms.ModulesDir, ) } func absolutizePaths(values ...*string) error { var err error for _, value := range values { if *value == "" || filepath.IsAbs(*value) { continue } *value, err = filepath.Abs(*value) if err != nil { return err } } return nil } func formatBuildInfoBlock(info buildinfo.Info) string { return fmt.Sprintf("version: %s\ncommit: %s\nbuilt_at: %s\n", info.Version, info.Commit, info.BuiltAt) } // formatVersionLine renders a buildinfo.Info as a single line — // "banger v0.1.0 (commit abcd1234, built 2026-04-28T20:45:50Z)" — for // the `--version` flag. Long commit strings are truncated to the // first 8 hex chars so the line stays scannable. The verbose // multi-line form lives on `banger version` for callers that want // the full SHA / built_at on separate lines. func formatVersionLine(info buildinfo.Info) string { commit := info.Commit if len(commit) > 8 { commit = commit[:8] } return fmt.Sprintf("banger %s (commit %s, built %s)", info.Version, commit, info.BuiltAt) }