Cuts the daemon-managed guest-session machinery (start/list/show/
logs/stop/kill/attach/send). The feature shipped aimed at agent-
orchestration workflows (programmatic stdin piping into a long-lived
guest process) that aren't driving any concrete user today, and the
~2.3K LOC of daemon surface area — attach bridge, FIFO keepalive,
controller registry, sessionstream framing, SQLite persistence — was
locking in an API we'd have to keep through v0.1.0.
Anything session-flavoured that people actually need today can be
done with `vm ssh + tmux` or `vm run -- cmd`.
Deleted:
- internal/cli/commands_vm_session.go
- internal/daemon/{guest_sessions,session_lifecycle,session_attach,session_stream,session_controller}.go
- internal/daemon/session/ (guest-session helpers package)
- internal/sessionstream/ (framing package)
- internal/daemon/guest_sessions_test.go
- internal/store/guest_session_test.go
- GuestSession* types from internal/{api,model}
- Store UpsertGuestSession/GetGuestSession/ListGuestSessionsByVM/DeleteGuestSession + scanner helpers
- guest.session.* RPC dispatch entries
- 5 CLI session tests, 2 completion tests, 2 printer tests
Extracted:
- ShellQuote + FormatStepError lifted to internal/daemon/workspace/util.go
(only non-session consumer); workspace package now self-contained
- internal/daemon/guest_ssh.go keeps guestSSHClient + dialGuest +
waitForGuestSSH — still used by workspace prepare/export
- internal/daemon/fake_firecracker_test.go preserves the test helper
that used to live in guest_sessions_test.go
Store schema: CREATE TABLE guest_sessions and its column migrations
removed. Existing dev DBs keep an orphan table (harmless, pre-v0.1.0).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
293 lines
6.6 KiB
Go
293 lines
6.6 KiB
Go
package cli
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"text/tabwriter"
|
|
|
|
"banger/internal/api"
|
|
"banger/internal/model"
|
|
"banger/internal/system"
|
|
)
|
|
|
|
// anyWriter is the minimal writer surface every printer needs. Split
|
|
// out from io.Writer because some of our callers already hold a
|
|
// tabwriter/bytes.Buffer by value.
|
|
type anyWriter interface {
|
|
Write(p []byte) (n int, err error)
|
|
}
|
|
|
|
// -- small helpers --------------------------------------------------
|
|
|
|
func humanSize(bytes int64) string {
|
|
if bytes <= 0 {
|
|
return "-"
|
|
}
|
|
const (
|
|
kib = 1024
|
|
mib = 1024 * kib
|
|
gib = 1024 * mib
|
|
)
|
|
switch {
|
|
case bytes >= gib:
|
|
return fmt.Sprintf("%.1fGiB", float64(bytes)/float64(gib))
|
|
case bytes >= mib:
|
|
return fmt.Sprintf("%.1fMiB", float64(bytes)/float64(mib))
|
|
case bytes >= kib:
|
|
return fmt.Sprintf("%.1fKiB", float64(bytes)/float64(kib))
|
|
default:
|
|
return fmt.Sprintf("%dB", bytes)
|
|
}
|
|
}
|
|
|
|
func dashIfEmpty(s string) string {
|
|
if strings.TrimSpace(s) == "" {
|
|
return "-"
|
|
}
|
|
return s
|
|
}
|
|
|
|
func emptyDash(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return "-"
|
|
}
|
|
return value
|
|
}
|
|
|
|
// -- generic printers -----------------------------------------------
|
|
|
|
func printJSON(out anyWriter, v any) error {
|
|
data, err := json.MarshalIndent(v, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = fmt.Fprintln(out, string(data))
|
|
return err
|
|
}
|
|
|
|
// -- VM printers ----------------------------------------------------
|
|
|
|
func printVMSummary(out anyWriter, vm model.VMRecord) error {
|
|
_, err := fmt.Fprintf(
|
|
out,
|
|
"%s\t%s\t%s\t%s\t%s\t%s\n",
|
|
shortID(vm.ID),
|
|
vm.Name,
|
|
vm.State,
|
|
vm.Runtime.GuestIP,
|
|
model.FormatSizeBytes(vm.Spec.WorkDiskSizeBytes),
|
|
vm.Runtime.DNSName,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func printVMIDList(out anyWriter, vms []model.VMRecord) error {
|
|
for _, vm := range vms {
|
|
if _, err := fmt.Fprintln(out, vm.ID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func printVMListTable(out anyWriter, vms []model.VMRecord, imageNames map[string]string) error {
|
|
w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0)
|
|
if _, err := fmt.Fprintln(w, "ID\tNAME\tSTATE\tIMAGE\tIP\tVCPU\tMEM\tDISK\tCREATED"); err != nil {
|
|
return err
|
|
}
|
|
for _, vm := range vms {
|
|
if _, err := fmt.Fprintf(
|
|
w,
|
|
"%s\t%s\t%s\t%s\t%s\t%d\t%d MiB\t%s\t%s\n",
|
|
shortID(vm.ID),
|
|
vm.Name,
|
|
vm.State,
|
|
vmImageLabel(vm.ImageID, imageNames),
|
|
vm.Runtime.GuestIP,
|
|
vm.Spec.VCPUCount,
|
|
vm.Spec.MemoryMiB,
|
|
model.FormatSizeBytes(vm.Spec.WorkDiskSizeBytes),
|
|
relativeTime(vm.CreatedAt),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return w.Flush()
|
|
}
|
|
|
|
func printVMPortsTable(out anyWriter, result api.VMPortsResult) error {
|
|
type portRow struct {
|
|
Proto string
|
|
Endpoint string
|
|
Process string
|
|
Command string
|
|
Port int
|
|
}
|
|
rows := make([]portRow, 0, len(result.Ports))
|
|
for _, port := range result.Ports {
|
|
rows = append(rows, portRow{
|
|
Proto: port.Proto,
|
|
Endpoint: port.Endpoint,
|
|
Process: port.Process,
|
|
Command: port.Command,
|
|
Port: port.Port,
|
|
})
|
|
}
|
|
sort.Slice(rows, func(i, j int) bool {
|
|
if rows[i].Proto != rows[j].Proto {
|
|
return rows[i].Proto < rows[j].Proto
|
|
}
|
|
if rows[i].Port != rows[j].Port {
|
|
return rows[i].Port < rows[j].Port
|
|
}
|
|
if rows[i].Process != rows[j].Process {
|
|
return rows[i].Process < rows[j].Process
|
|
}
|
|
return rows[i].Command < rows[j].Command
|
|
})
|
|
if len(rows) == 0 {
|
|
return nil
|
|
}
|
|
|
|
w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0)
|
|
if _, err := fmt.Fprintln(w, "PROTO\tENDPOINT\tPROCESS\tCOMMAND"); err != nil {
|
|
return err
|
|
}
|
|
for _, row := range rows {
|
|
if _, err := fmt.Fprintf(
|
|
w,
|
|
"%s\t%s\t%s\t%s\n",
|
|
row.Proto,
|
|
emptyDash(row.Endpoint),
|
|
emptyDash(row.Process),
|
|
emptyDash(row.Command),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return w.Flush()
|
|
}
|
|
|
|
// -- image printers -------------------------------------------------
|
|
|
|
func printImageSummary(out anyWriter, image model.Image) error {
|
|
_, err := fmt.Fprintf(out, "%s\t%s\t%t\t%s\n", shortID(image.ID), image.Name, image.Managed, image.RootfsPath)
|
|
return err
|
|
}
|
|
|
|
func imageNameIndex(images []model.Image) map[string]string {
|
|
index := make(map[string]string, len(images))
|
|
for _, image := range images {
|
|
index[image.ID] = image.Name
|
|
}
|
|
return index
|
|
}
|
|
|
|
func vmImageLabel(imageID string, imageNames map[string]string) string {
|
|
if name := strings.TrimSpace(imageNames[imageID]); name != "" {
|
|
return name
|
|
}
|
|
return shortID(imageID)
|
|
}
|
|
|
|
func printImageListTable(out anyWriter, images []model.Image) error {
|
|
w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0)
|
|
if _, err := fmt.Fprintln(w, "ID\tNAME\tMANAGED\tROOTFS SIZE\tCREATED"); err != nil {
|
|
return err
|
|
}
|
|
for _, image := range images {
|
|
if _, err := fmt.Fprintf(
|
|
w,
|
|
"%s\t%s\t%t\t%s\t%s\n",
|
|
shortID(image.ID),
|
|
image.Name,
|
|
image.Managed,
|
|
rootfsSizeLabel(image.RootfsPath),
|
|
relativeTime(image.CreatedAt),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return w.Flush()
|
|
}
|
|
|
|
func rootfsSizeLabel(path string) string {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return "-"
|
|
}
|
|
if info.Size() <= 0 {
|
|
return "0"
|
|
}
|
|
return model.FormatSizeBytes(info.Size())
|
|
}
|
|
|
|
// -- kernel printers ------------------------------------------------
|
|
|
|
func printKernelListTable(out anyWriter, entries []api.KernelEntry) error {
|
|
w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0)
|
|
if _, err := fmt.Fprintln(w, "NAME\tDISTRO\tARCH\tKERNEL\tIMPORTED"); err != nil {
|
|
return err
|
|
}
|
|
for _, entry := range entries {
|
|
if _, err := fmt.Fprintf(
|
|
w,
|
|
"%s\t%s\t%s\t%s\t%s\n",
|
|
entry.Name,
|
|
dashIfEmpty(entry.Distro),
|
|
dashIfEmpty(entry.Arch),
|
|
dashIfEmpty(entry.KernelVersion),
|
|
dashIfEmpty(entry.ImportedAt),
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return w.Flush()
|
|
}
|
|
|
|
func printKernelCatalogTable(out anyWriter, entries []api.KernelCatalogEntry) error {
|
|
w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0)
|
|
if _, err := fmt.Fprintln(w, "NAME\tDISTRO\tARCH\tKERNEL\tSIZE\tSTATE"); err != nil {
|
|
return err
|
|
}
|
|
for _, entry := range entries {
|
|
state := "available"
|
|
if entry.Pulled {
|
|
state = "pulled"
|
|
}
|
|
if _, err := fmt.Fprintf(
|
|
w,
|
|
"%s\t%s\t%s\t%s\t%s\t%s\n",
|
|
entry.Name,
|
|
dashIfEmpty(entry.Distro),
|
|
dashIfEmpty(entry.Arch),
|
|
dashIfEmpty(entry.KernelVersion),
|
|
humanSize(entry.SizeBytes),
|
|
state,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return w.Flush()
|
|
}
|
|
|
|
// -- doctor printer -------------------------------------------------
|
|
|
|
func printDoctorReport(out anyWriter, report system.Report) error {
|
|
for _, check := range report.Checks {
|
|
status := strings.ToUpper(string(check.Status))
|
|
if _, err := fmt.Fprintf(out, "%s\t%s\n", status, check.Name); err != nil {
|
|
return err
|
|
}
|
|
for _, detail := range check.Details {
|
|
if _, err := fmt.Fprintf(out, " - %s\n", detail); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|