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>
165 lines
5.3 KiB
Go
165 lines
5.3 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
|
|
"banger/internal/api"
|
|
"banger/internal/paths"
|
|
"banger/internal/rpc"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// Completion helpers. Design notes:
|
|
//
|
|
// - Never auto-start the daemon. If it isn't running, return no
|
|
// suggestions + NoFileComp so the shell doesn't fall back to file
|
|
// completion (there are no local files that would plausibly match a
|
|
// VM or image name).
|
|
// - Filter out names already in args — avoids suggesting the same VM
|
|
// twice on variadic commands like `vm stop a b <tab>`.
|
|
// - Fail silently. Completion is advisory; any error path returns an
|
|
// empty suggestion list rather than propagating to the user.
|
|
|
|
// defaultCompletionLister backs the *deps.completionLister field;
|
|
// tests inject their own fake via the struct instead of mutating
|
|
// package-level vars.
|
|
func defaultCompletionLister(ctx context.Context, socketPath, method string) ([]string, error) {
|
|
switch method {
|
|
case "vm.list":
|
|
result, err := rpc.Call[api.VMListResult](ctx, socketPath, method, api.Empty{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
names := make([]string, 0, len(result.VMs))
|
|
for _, vm := range result.VMs {
|
|
if vm.Name != "" {
|
|
names = append(names, vm.Name)
|
|
}
|
|
}
|
|
return names, nil
|
|
case "image.list":
|
|
result, err := rpc.Call[api.ImageListResult](ctx, socketPath, method, api.Empty{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
names := make([]string, 0, len(result.Images))
|
|
for _, image := range result.Images {
|
|
if image.Name != "" {
|
|
names = append(names, image.Name)
|
|
}
|
|
}
|
|
return names, nil
|
|
case "kernel.list":
|
|
result, err := rpc.Call[api.KernelListResult](ctx, socketPath, method, api.Empty{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
names := make([]string, 0, len(result.Entries))
|
|
for _, entry := range result.Entries {
|
|
if entry.Name != "" {
|
|
names = append(names, entry.Name)
|
|
}
|
|
}
|
|
return names, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// daemonSocketForCompletion returns the socket path IFF the daemon is
|
|
// already running. Returns "", false when no daemon is up — completion
|
|
// callers use this as the bail signal.
|
|
func (d *deps) daemonSocketForCompletion(ctx context.Context) (string, bool) {
|
|
layout, err := paths.Resolve()
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
if _, err := d.daemonPing(ctx, layout.SocketPath); err != nil {
|
|
return "", false
|
|
}
|
|
return layout.SocketPath, true
|
|
}
|
|
|
|
// filterPrefix returns the subset of candidates starting with toComplete
|
|
// that aren't in exclude. Comparison is case-sensitive because VM/image
|
|
// names preserve case.
|
|
func filterPrefix(candidates, exclude []string, toComplete string) []string {
|
|
excludeSet := make(map[string]struct{}, len(exclude))
|
|
for _, e := range exclude {
|
|
excludeSet[e] = struct{}{}
|
|
}
|
|
out := make([]string, 0, len(candidates))
|
|
for _, c := range candidates {
|
|
if _, skip := excludeSet[c]; skip {
|
|
continue
|
|
}
|
|
if toComplete == "" || hasPrefix(c, toComplete) {
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func hasPrefix(s, prefix string) bool {
|
|
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
|
|
}
|
|
|
|
func (d *deps) completeVMNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
socket, ok := d.daemonSocketForCompletion(cmd.Context())
|
|
if !ok {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
names, err := d.completionLister(cmd.Context(), socket, "vm.list")
|
|
if err != nil {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
return filterPrefix(names, args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
// completeVMNameOnlyAtPos0 restricts VM-name completion to the first
|
|
// positional argument. Used by commands like `vm ssh <vm> [ssh args...]`
|
|
// where args after pos 0 are free-form.
|
|
func (d *deps) completeVMNameOnlyAtPos0(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
if len(args) > 0 {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
return d.completeVMNames(cmd, args, toComplete)
|
|
}
|
|
|
|
func (d *deps) completeImageNameOnlyAtPos0(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
if len(args) > 0 {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
return d.completeImageNames(cmd, args, toComplete)
|
|
}
|
|
|
|
func (d *deps) completeKernelNameOnlyAtPos0(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
if len(args) > 0 {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
return d.completeKernelNames(cmd, args, toComplete)
|
|
}
|
|
|
|
func (d *deps) completeImageNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
socket, ok := d.daemonSocketForCompletion(cmd.Context())
|
|
if !ok {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
names, err := d.completionLister(cmd.Context(), socket, "image.list")
|
|
if err != nil {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
return filterPrefix(names, args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
|
|
func (d *deps) completeKernelNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
|
socket, ok := d.daemonSocketForCompletion(cmd.Context())
|
|
if !ok {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
names, err := d.completionLister(cmd.Context(), socket, "kernel.list")
|
|
if err != nil {
|
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
|
}
|
|
return filterPrefix(names, args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
|
}
|