package cli import ( "context" "fmt" "io" "os" "os/exec" "path/filepath" "strings" "time" "banger/internal/api" "banger/internal/daemon" "banger/internal/guest" "banger/internal/paths" "banger/internal/rpc" "banger/internal/system" "banger/internal/toolingplan" ) // deps holds the function seams production code dispatches through and // tests replace with fakes. Keeping these on a per-invocation struct // (instead of package-level mutable vars) makes the CLI's external // surface explicit and lets tests run in parallel without leaking fakes // across test cases. // // Every command builder, orchestrator, and helper that touches the RPC // socket, spawns a subprocess, or reads host state hangs off a *deps // receiver. Pure helpers (formatters, path resolvers, arg-count // validators) stay package-level because they hold no references to // external systems. type deps struct { bangerdPath func() (string, error) daemonExePath func(pid int) string doctor func(ctx context.Context) (system.Report, error) sshExec func(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, args []string) error hostCommandOutput func(ctx context.Context, name string, args ...string) ([]byte, error) vmHealth func(ctx context.Context, socketPath, idOrName string) (api.VMHealthResult, error) vmSSH func(ctx context.Context, socketPath, idOrName string) (api.VMSSHResult, error) vmDelete func(ctx context.Context, socketPath, idOrName string) error vmList func(ctx context.Context, socketPath string) (api.VMListResult, error) daemonPing func(ctx context.Context, socketPath string) (api.PingResult, error) vmCreateBegin func(ctx context.Context, socketPath string, params api.VMCreateParams) (api.VMCreateBeginResult, error) vmCreateStatus func(ctx context.Context, socketPath, operationID string) (api.VMCreateStatusResult, error) vmCreateCancel func(ctx context.Context, socketPath, operationID string) error vmPorts func(ctx context.Context, socketPath, idOrName string) (api.VMPortsResult, error) vmWorkspacePrepare func(ctx context.Context, socketPath string, params api.VMWorkspacePrepareParams) (api.VMWorkspacePrepareResult, error) vmWorkspaceExport func(ctx context.Context, socketPath string, params api.WorkspaceExportParams) (api.WorkspaceExportResult, error) guestWaitForSSH func(ctx context.Context, address, privateKeyPath string, interval time.Duration) error guestDial func(ctx context.Context, address, privateKeyPath string) (vmRunGuestClient, error) buildVMRunToolingPlan func(ctx context.Context, repoRoot string) toolingplan.Plan cwd func() (string, error) completionLister func(ctx context.Context, socketPath, method string) ([]string, error) } func defaultDeps() *deps { return &deps{ bangerdPath: paths.BangerdPath, daemonExePath: func(pid int) string { return filepath.Join("/proc", fmt.Sprintf("%d", pid), "exe") }, doctor: daemon.Doctor, sshExec: func(ctx context.Context, stdin io.Reader, stdout, stderr io.Writer, args []string) error { sshCmd := exec.CommandContext(ctx, "ssh", args...) sshCmd.Stdout = stdout sshCmd.Stderr = stderr sshCmd.Stdin = stdin return sshCmd.Run() }, hostCommandOutput: func(ctx context.Context, name string, args ...string) ([]byte, error) { cmd := exec.CommandContext(ctx, name, args...) output, err := cmd.CombinedOutput() if err == nil { return output, nil } command := strings.TrimSpace(strings.Join(append([]string{name}, args...), " ")) detail := strings.TrimSpace(string(output)) if detail == "" { return output, fmt.Errorf("%s: %w", command, err) } return output, fmt.Errorf("%s: %w: %s", command, err, detail) }, vmHealth: func(ctx context.Context, socketPath, idOrName string) (api.VMHealthResult, error) { return rpc.Call[api.VMHealthResult](ctx, socketPath, "vm.health", api.VMRefParams{IDOrName: idOrName}) }, vmSSH: func(ctx context.Context, socketPath, idOrName string) (api.VMSSHResult, error) { return rpc.Call[api.VMSSHResult](ctx, socketPath, "vm.ssh", api.VMRefParams{IDOrName: idOrName}) }, vmDelete: func(ctx context.Context, socketPath, idOrName string) error { _, err := rpc.Call[api.VMShowResult](ctx, socketPath, "vm.delete", api.VMRefParams{IDOrName: idOrName}) return err }, vmList: func(ctx context.Context, socketPath string) (api.VMListResult, error) { return rpc.Call[api.VMListResult](ctx, socketPath, "vm.list", api.Empty{}) }, daemonPing: func(ctx context.Context, socketPath string) (api.PingResult, error) { return rpc.Call[api.PingResult](ctx, socketPath, "ping", api.Empty{}) }, vmCreateBegin: func(ctx context.Context, socketPath string, params api.VMCreateParams) (api.VMCreateBeginResult, error) { return rpc.Call[api.VMCreateBeginResult](ctx, socketPath, "vm.create.begin", params) }, vmCreateStatus: func(ctx context.Context, socketPath, operationID string) (api.VMCreateStatusResult, error) { return rpc.Call[api.VMCreateStatusResult](ctx, socketPath, "vm.create.status", api.VMCreateStatusParams{ID: operationID}) }, vmCreateCancel: func(ctx context.Context, socketPath, operationID string) error { _, err := rpc.Call[api.Empty](ctx, socketPath, "vm.create.cancel", api.VMCreateStatusParams{ID: operationID}) return err }, vmPorts: func(ctx context.Context, socketPath, idOrName string) (api.VMPortsResult, error) { return rpc.Call[api.VMPortsResult](ctx, socketPath, "vm.ports", api.VMRefParams{IDOrName: idOrName}) }, vmWorkspacePrepare: func(ctx context.Context, socketPath string, params api.VMWorkspacePrepareParams) (api.VMWorkspacePrepareResult, error) { return rpc.Call[api.VMWorkspacePrepareResult](ctx, socketPath, "vm.workspace.prepare", params) }, vmWorkspaceExport: func(ctx context.Context, socketPath string, params api.WorkspaceExportParams) (api.WorkspaceExportResult, error) { return rpc.Call[api.WorkspaceExportResult](ctx, socketPath, "vm.workspace.export", params) }, guestWaitForSSH: func(ctx context.Context, address, privateKeyPath string, interval time.Duration) error { knownHosts, _ := bangerKnownHostsPath() return guest.WaitForSSH(ctx, address, privateKeyPath, knownHosts, interval) }, guestDial: func(ctx context.Context, address, privateKeyPath string) (vmRunGuestClient, error) { knownHosts, _ := bangerKnownHostsPath() return guest.Dial(ctx, address, privateKeyPath, knownHosts) }, buildVMRunToolingPlan: toolingplan.Build, cwd: os.Getwd, completionLister: defaultCompletionLister, } }