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 `. // - Fail silently. Completion is advisory; any error path returns an // empty suggestion list rather than propagating to the user. // completionListerFunc is the seam used by tests to avoid touching a // real daemon socket. var completionListerFunc = func(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 } // completionSessionListerFunc is the seam for guest-session name lookups // scoped to a VM. var completionSessionListerFunc = func(ctx context.Context, socketPath, vmIDOrName string) ([]string, error) { result, err := rpc.Call[api.GuestSessionListResult](ctx, socketPath, "guest.session.list", api.VMRefParams{IDOrName: vmIDOrName}) if err != nil { return nil, err } names := make([]string, 0, len(result.Sessions)) for _, session := range result.Sessions { if session.Name != "" { names = append(names, session.Name) } } return names, 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 daemonSocketForCompletion(ctx context.Context) (string, bool) { layout, err := paths.Resolve() if err != nil { return "", false } if _, err := daemonPingFunc(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 completeVMNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { socket, ok := daemonSocketForCompletion(cmd.Context()) if !ok { return nil, cobra.ShellCompDirectiveNoFileComp } names, err := completionListerFunc(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 [ssh args...]` // where args after pos 0 are free-form. func completeVMNameOnlyAtPos0(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) > 0 { return nil, cobra.ShellCompDirectiveNoFileComp } return completeVMNames(cmd, args, toComplete) } func completeImageNameOnlyAtPos0(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) > 0 { return nil, cobra.ShellCompDirectiveNoFileComp } return completeImageNames(cmd, args, toComplete) } func completeKernelNameOnlyAtPos0(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) > 0 { return nil, cobra.ShellCompDirectiveNoFileComp } return completeKernelNames(cmd, args, toComplete) } func completeImageNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { socket, ok := daemonSocketForCompletion(cmd.Context()) if !ok { return nil, cobra.ShellCompDirectiveNoFileComp } names, err := completionListerFunc(cmd.Context(), socket, "image.list") if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } return filterPrefix(names, args, toComplete), cobra.ShellCompDirectiveNoFileComp } func completeKernelNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { socket, ok := daemonSocketForCompletion(cmd.Context()) if !ok { return nil, cobra.ShellCompDirectiveNoFileComp } names, err := completionListerFunc(cmd.Context(), socket, "kernel.list") if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } return filterPrefix(names, args, toComplete), cobra.ShellCompDirectiveNoFileComp } // completeSessionNames handles `... ` commands: pos 0 // completes VMs, pos 1 completes sessions owned by args[0], pos 2+ is // silent. func completeSessionNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { switch len(args) { case 0: return completeVMNames(cmd, args, toComplete) case 1: socket, ok := daemonSocketForCompletion(cmd.Context()) if !ok { return nil, cobra.ShellCompDirectiveNoFileComp } names, err := completionSessionListerFunc(cmd.Context(), socket, args[0]) if err != nil { return nil, cobra.ShellCompDirectiveNoFileComp } return filterPrefix(names, nil, toComplete), cobra.ShellCompDirectiveNoFileComp default: return nil, cobra.ShellCompDirectiveNoFileComp } }