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. // 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 [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 }