@@ -2,6 +2,7 @@ package ssh
22
33import (
44 "context"
5+ "errors"
56 "fmt"
67 "os"
78 "os/exec"
@@ -34,6 +35,10 @@ func NewCmdSSH(f cmdutil.Factory, ioStreams cmdutil.IOStreams) *cobra.Command {
3435 SSH connection.
3536
3637 Any arguments after -- are passed directly to the ssh command.
38+
39+ Supports piping commands via stdin. When stdin is not a terminal,
40+ the input is forwarded to the remote host and pseudo-TTY allocation
41+ is disabled automatically.
3742 ` ),
3843 Example : cmdutil .Examples (`
3944 # SSH by hostname
@@ -47,6 +52,12 @@ func NewCmdSSH(f cmdutil.Factory, ioStreams cmdutil.IOStreams) *cobra.Command {
4752
4853 # Pass extra ssh arguments
4954 verda ssh gpu-runner -- -L 8080:localhost:8080
55+
56+ # Pipe a command to run remotely
57+ echo "nvidia-smi" | verda ssh gpu-runner
58+
59+ # Pipe a script
60+ cat setup.sh | verda ssh gpu-runner
5061 ` ),
5162 Args : cobra .ArbitraryArgs , // extra args after "--" are passed to ssh
5263 DisableFlagParsing : false ,
@@ -98,35 +109,14 @@ func runSSH(cmd *cobra.Command, f cmdutil.Factory, ioStreams cmdutil.IOStreams,
98109
99110 // Interactive picker when no target is provided.
100111 if target == "" {
101- running := filterRunning (instances )
102- if len (running ) == 0 {
103- _ , _ = fmt .Fprintln (ioStreams .ErrOut , "No running instances found." )
104- return nil
105- }
106-
107- labels := make ([]string , 0 , len (running )+ 1 )
108- for i := range running {
109- ip := ""
110- if running [i ].IP != nil && * running [i ].IP != "" {
111- ip = * running [i ].IP
112- }
113- labels = append (labels , fmt .Sprintf ("%-20s %-18s %s %s" ,
114- running [i ].Hostname ,
115- running [i ].InstanceType ,
116- running [i ].Location ,
117- ip ,
118- ))
119- }
120- labels = append (labels , "Cancel" )
121-
122- idx , err := f .Prompter ().Select (cmd .Context (), "Select instance to SSH into" , labels )
112+ picked , err := pickInstance (cmd .Context (), f , ioStreams , instances )
123113 if err != nil {
124- return nil // User pressed Esc/Ctrl+C during prompt.
114+ return err
125115 }
126- if idx == len ( running ) {
127- return nil // Cancel selected.
116+ if picked == "" {
117+ return nil // canceled or no running instances
128118 }
129- target = running [ idx ]. Hostname
119+ target = picked
130120 }
131121
132122 inst := resolveInstance (instances , target )
@@ -150,6 +140,9 @@ func runSSH(cmd *cobra.Command, f cmdutil.Factory, ioStreams cmdutil.IOStreams,
150140 }
151141
152142 sshArgs := []string {"ssh" }
143+ if ! isTerminal (os .Stdin ) {
144+ sshArgs = append (sshArgs , "-T" )
145+ }
153146 if opts .KeyFile != "" {
154147 sshArgs = append (sshArgs , "-i" , opts .KeyFile )
155148 }
@@ -163,6 +156,44 @@ func runSSH(cmd *cobra.Command, f cmdutil.Factory, ioStreams cmdutil.IOStreams,
163156 return syscall .Exec (sshPath , sshArgs , os .Environ ()) //nolint:gosec // Intentional: replace process with ssh using user-provided host and args.
164157}
165158
159+ // pickInstance shows an interactive picker for running instances.
160+ // Returns the selected hostname, or "" if canceled/no running instances.
161+ func pickInstance (ctx context.Context , f cmdutil.Factory , ioStreams cmdutil.IOStreams , instances []verda.Instance ) (string , error ) {
162+ if ! isTerminal (os .Stdin ) {
163+ return "" , errors .New ("instance ID or hostname is required when piping stdin" )
164+ }
165+
166+ running := filterRunning (instances )
167+ if len (running ) == 0 {
168+ _ , _ = fmt .Fprintln (ioStreams .ErrOut , "No running instances found." )
169+ return "" , nil
170+ }
171+
172+ labels := make ([]string , 0 , len (running )+ 1 )
173+ for i := range running {
174+ ip := ""
175+ if running [i ].IP != nil && * running [i ].IP != "" {
176+ ip = * running [i ].IP
177+ }
178+ labels = append (labels , fmt .Sprintf ("%-20s %-18s %s %s" ,
179+ running [i ].Hostname ,
180+ running [i ].InstanceType ,
181+ running [i ].Location ,
182+ ip ,
183+ ))
184+ }
185+ labels = append (labels , "Cancel" )
186+
187+ idx , err := f .Prompter ().Select (ctx , "Select instance to SSH into" , labels )
188+ if err != nil {
189+ return "" , err
190+ }
191+ if idx == len (running ) {
192+ return "" , nil // Cancel selected.
193+ }
194+ return running [idx ].Hostname , nil
195+ }
196+
166197// filterRunning returns only instances with status running.
167198func filterRunning (instances []verda.Instance ) []verda.Instance {
168199 var running []verda.Instance
@@ -174,6 +205,15 @@ func filterRunning(instances []verda.Instance) []verda.Instance {
174205 return running
175206}
176207
208+ // isTerminal reports whether f is a terminal.
209+ func isTerminal (f * os.File ) bool {
210+ fi , err := f .Stat ()
211+ if err != nil {
212+ return false
213+ }
214+ return fi .Mode ()& os .ModeCharDevice != 0
215+ }
216+
177217// resolveInstance finds an instance by exact ID match first, then by hostname.
178218func resolveInstance (instances []verda.Instance , target string ) * verda.Instance {
179219 // Exact ID match.
0 commit comments