From 3a9175e5d79c6231b24ace077c51b729f1885852 Mon Sep 17 00:00:00 2001 From: Joshua Herring Date: Tue, 12 May 2026 09:30:19 -0400 Subject: [PATCH] run commands inside TUI and return to list --- commands/run_command.go | 32 +++++++-------- commands/upload_file.go | 47 +++++++++------------ commands/utils.go | 23 +++++------ main.go | 90 +++++++++++++++++++++++++---------------- 4 files changed, 102 insertions(+), 90 deletions(-) diff --git a/commands/run_command.go b/commands/run_command.go index c15decf..7e738af 100644 --- a/commands/run_command.go +++ b/commands/run_command.go @@ -2,15 +2,17 @@ package commands import ( "fmt" + "io" "os" ) type RunCommandArgs struct { - Command string `json:"command"` - RemoteCommand string `json:"remote_command"` + Command string `json:"command"` + RemoteCommand string `json:"remote_command"` PrivateKeyFile string `json:"private_key_file"` - Username string `json:"username"` - Host string `json:"host"` + Username string `json:"username"` + Host string `json:"host"` + Sudo bool `json:"sudo"` } func (rca *RunCommandArgs) ReadArgs() { @@ -28,27 +30,25 @@ func (rca *RunCommandArgs) ReadArgs() { } -func RunCommand(args RunCommandArgs) { +func RunCommand(args RunCommandArgs, stdout, stderr io.Writer) error { - //args := RunCommandArgs{} - //args.ReadArgs() - - client := GetClient(args.PrivateKeyFile, args.Username, args.Host) + client, err := GetClient(args.PrivateKeyFile, args.Username, args.Host) + if err != nil { + return err + } defer client.Close() session, err := client.NewSession() if err != nil { - fmt.Fprintf(os.Stderr, "Error creating SSH session: %v\n", err) - os.Exit(1) + return fmt.Errorf("error creating SSH session: %w", err) } defer session.Close() - session.Stdout = os.Stdout - session.Stderr = os.Stderr + session.Stdout = stdout + session.Stderr = stderr if err := session.Run(args.RemoteCommand); err != nil { - fmt.Fprintf(os.Stderr, "Error running command: %v\n", err) - os.Exit(1) + return fmt.Errorf("error running command: %w", err) } - return + return nil } diff --git a/commands/upload_file.go b/commands/upload_file.go index 08b6f59..c56ded9 100644 --- a/commands/upload_file.go +++ b/commands/upload_file.go @@ -4,17 +4,16 @@ import ( "fmt" "io" "os" - "path/filepath" ) type UploadFileArgs struct { - Command string `json:"command"` - LocalFilepath string `json:"local_filepath"` + Command string `json:"command"` + LocalFilepath string `json:"local_filepath"` DestinationFilepath string `json:"destination_filepath"` - PrivateKeyFile string `json:"private_key_file"` - Username string `json:"username"` - Host string `json:"host"` + PrivateKeyFile string `json:"private_key_file"` + Username string `json:"username"` + Host string `json:"host"` } func (ufa *UploadFileArgs) ReadArgs() { @@ -33,58 +32,52 @@ func (ufa *UploadFileArgs) ReadArgs() { } -func UploadFile(args UploadFileArgs) { - - //args := UploadFileArgs{} - //args.ReadArgs() +func UploadFile(args UploadFileArgs, stdout, stderr io.Writer) error { - client := GetClient(args.PrivateKeyFile, args.Username, args.Host) + client, err := GetClient(args.PrivateKeyFile, args.Username, args.Host) + if err != nil { + return err + } defer client.Close() session, err := client.NewSession() if err != nil { - fmt.Fprintf(os.Stderr, "Error creating SSH session: %v\n", err) - os.Exit(1) + return fmt.Errorf("error creating SSH session: %w", err) } defer session.Close() source_file, err := os.Open(args.LocalFilepath) if err != nil { - fmt.Fprintf(os.Stderr, "Error opening local file: %v\n", err) - os.Exit(1) + return fmt.Errorf("error opening local file: %w", err) } defer source_file.Close() info, err := source_file.Stat() if err != nil { - fmt.Fprintf(os.Stderr, "Error stating local file: %v\n", err) - os.Exit(1) + return fmt.Errorf("error stating local file: %w", err) } w, err := session.StdinPipe() if err != nil { - fmt.Fprintf(os.Stderr, "Error creating stdin pipe: %v\n", err) - os.Exit(1) + return fmt.Errorf("error creating stdin pipe: %w", err) } if err := session.Start("scp -t " + args.DestinationFilepath); err != nil { - fmt.Fprintf(os.Stderr, "Error starting scp: %v\n", err) - os.Exit(1) + return fmt.Errorf("error starting scp: %w", err) } fmt.Fprintf(w, "C0644 %d %s\n", info.Size(), filepath.Base(args.LocalFilepath)) if _, err := io.Copy(w, source_file); err != nil { - fmt.Fprintf(os.Stderr, "Error sending file: %v\n", err) - os.Exit(1) + return fmt.Errorf("error sending file: %w", err) } fmt.Fprint(w, "\x00") w.Close() if err := session.Wait(); err != nil { - fmt.Fprintf(os.Stderr, "Error during scp transfer: %v\n", err) - os.Exit(1) + return fmt.Errorf("error during scp transfer: %w", err) } - fmt.Println("Success") -} + fmt.Fprintln(stdout, "Success") + return nil +} diff --git a/commands/utils.go b/commands/utils.go index 698fc7c..c1cda9a 100644 --- a/commands/utils.go +++ b/commands/utils.go @@ -29,27 +29,28 @@ func ResolvePort(host string) string { } -func GetSigner(private_key_file string) ssh.Signer { +func GetSigner(private_key_file string) (ssh.Signer, error) { key_bytes, err := os.ReadFile(private_key_file) if err != nil { - fmt.Fprintf(os.Stderr, "Error reading private key from file %s: %v\n", private_key_file, err) - os.Exit(1) + return nil, fmt.Errorf("error reading private key from file %s: %w", private_key_file, err) } signer, err := ssh.ParsePrivateKey(key_bytes) if err != nil { - fmt.Fprintf(os.Stderr, "Error parsing private key: %v\n", err) - os.Exit(1) + return nil, fmt.Errorf("error parsing private key: %w", err) } - return signer + return signer, nil } -func GetClient(private_key_file, username, host string) *ssh.Client { +func GetClient(private_key_file, username, host string) (*ssh.Client, error) { - signer := GetSigner(private_key_file) + signer, err := GetSigner(private_key_file) + if err != nil { + return nil, err + } config := &ssh.ClientConfig{ User: username, @@ -63,10 +64,8 @@ func GetClient(private_key_file, username, host string) *ssh.Client { client, err := ssh.Dial("tcp", addr, config) if err != nil { - fmt.Fprintf(os.Stderr, "Error connecting to server: %v\n", err) - os.Exit(1) + return nil, fmt.Errorf("error connecting to server: %w", err) } - return client + return client, nil } - diff --git a/main.go b/main.go index d44add1..269a408 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,8 @@ package main import ( + "bytes" "fmt" - "io" "os" "github.com/charmbracelet/bubbles/list" @@ -11,37 +11,34 @@ import ( "periodic/prddeploy/commands" ) -type sshExecCmd struct { - run func() -} - -func (c *sshExecCmd) Run() error { c.run(); return nil } -func (c *sshExecCmd) SetStdin(_ io.Reader) {} -func (c *sshExecCmd) SetStdout(_ io.Writer) {} -func (c *sshExecCmd) SetStderr(_ io.Writer) {} - type appState int const ( stateList appState = iota stateForm + stateOutput ) +type cmdResultMsg struct { + output string +} + type CommandItem struct { - ItemTitle string `json:"title"` + ItemTitle string `json:"title"` ItemDescription string `json:"description"` } func (i CommandItem) FilterValue() string { return i.ItemTitle } func (i CommandItem) Title() string { return i.ItemTitle } func (i CommandItem) Description() string { return i.ItemDescription } -func (i CommandItem) String() string { return i.ItemTitle } +func (i CommandItem) String() string { return i.ItemTitle } type model struct { list list.Model form *huh.Form state appState choice int + output string RemoteCommand string PrivateKeyFile string @@ -71,8 +68,8 @@ func (m *model) ReadForm() { func newModel() model { items := []list.Item{ - CommandItem{ItemTitle: "Run Command", ItemDescription: "Run a command on the remote host",}, - CommandItem{ItemTitle: "Upload File", ItemDescription: "Upload a file to the remote host",}, + CommandItem{ItemTitle: "Run Command", ItemDescription: "Run a command on the remote host"}, + CommandItem{ItemTitle: "Upload File", ItemDescription: "Upload a file to the remote host"}, } l := list.New(items, list.NewDefaultDelegate(), 80, 20) l.Title = "Commands" @@ -83,6 +80,33 @@ func (m model) Init() tea.Cmd { return nil } +func execCommand(choice int, m model) tea.Cmd { + return func() tea.Msg { + var buf bytes.Buffer + var err error + if choice == 0 { + err = commands.RunCommand(commands.RunCommandArgs{ + RemoteCommand: m.RemoteCommand, + PrivateKeyFile: m.PrivateKeyFile, + Username: m.Username, + Host: m.Host, + }, &buf, &buf) + } else { + err = commands.UploadFile(commands.UploadFileArgs{ + LocalFilepath: m.LocalFilepath, + DestinationFilepath: m.DestinationFilepath, + PrivateKeyFile: m.PrivateKeyFile, + Username: m.Username, + Host: m.Host, + }, &buf, &buf) + } + if err != nil { + fmt.Fprintf(&buf, "\nError: %v", err) + } + return cmdResultMsg{output: buf.String()} + } +} + func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch m.state { case stateList: @@ -127,28 +151,10 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if m.form.State == huh.StateCompleted { m.ReadForm() - var execCmd tea.ExecCommand - if m.choice == 0 { - args := commands.RunCommandArgs{ - RemoteCommand: m.RemoteCommand, - PrivateKeyFile: m.PrivateKeyFile, - Username: m.Username, - Host: m.Host, - } - execCmd = &sshExecCmd{run: func() { commands.RunCommand(args) }} - } else { - args := commands.UploadFileArgs{ - LocalFilepath: m.LocalFilepath, - DestinationFilepath: m.DestinationFilepath, - PrivateKeyFile: m.PrivateKeyFile, - Username: m.Username, - Host: m.Host, - } - execCmd = &sshExecCmd{run: func() { commands.UploadFile(args) }} - } - m.state = stateList + choice := m.choice m.form = nil - return m, tea.Exec(execCmd, func(err error) tea.Msg { return nil }) + m.state = stateOutput + return m, execCommand(choice, m) } if m.form.State == huh.StateAborted { m.state = stateList @@ -156,6 +162,18 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } return m, cmd + + case stateOutput: + if _, ok := msg.(tea.KeyMsg); ok { + m.state = stateList + return m, nil + } + } + + switch msg := msg.(type) { + case cmdResultMsg: + m.output = msg.output + m.state = stateOutput } return m, nil @@ -167,6 +185,8 @@ func (m model) View() string { return m.list.View() case stateForm: return m.form.View() + case stateOutput: + return m.output + "\n\nPress any key to return..." } return "" }