gssh-agent 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/daemon CHANGED
Binary file
package/bin/gssh CHANGED
Binary file
package/cmd/gssh/main.go CHANGED
@@ -18,47 +18,66 @@ import (
18
18
 
19
19
  const (
20
20
  defaultSocketPath = "/tmp/gssh.sock"
21
- )
22
-
23
- var (
24
- socketPath = flag.String("socket", defaultSocketPath, "Unix socket path")
21
+ version = "0.1.0"
25
22
  )
26
23
 
27
24
  func main() {
28
- flag.Parse()
25
+ // Check for version flag before any parsing
26
+ if len(os.Args) > 1 && (os.Args[1] == "-v" || os.Args[1] == "--version") {
27
+ fmt.Printf("gssh version %s\n", version)
28
+ os.Exit(0)
29
+ }
29
30
 
30
- if flag.NArg() < 1 {
31
+ // Check for help flag
32
+ if len(os.Args) > 1 && (os.Args[1] == "-h" || os.Args[1] == "--help") {
33
+ printUsage()
34
+ os.Exit(0)
35
+ }
36
+
37
+ // Parse global -socket flag manually
38
+ socketPath := defaultSocketPath
39
+ args := os.Args[1:]
40
+ for i := 0; i < len(args); i++ {
41
+ if args[i] == "-socket" && i+1 < len(args) {
42
+ socketPath = args[i+1]
43
+ args = append(args[:i], args[i+2:]...)
44
+ i--
45
+ }
46
+ }
47
+
48
+ if len(args) < 1 {
31
49
  printUsage()
32
50
  os.Exit(1)
33
51
  }
34
52
 
35
- cmd := flag.Arg(0)
53
+ cmd := args[0]
54
+ subArgs := args[1:]
36
55
 
37
56
  var err error
38
57
  switch cmd {
39
58
  case "connect":
40
- err = handleConnect()
59
+ err = handleConnect(subArgs, socketPath)
41
60
  case "disconnect":
42
- err = handleDisconnect()
61
+ err = handleDisconnect(subArgs, socketPath)
43
62
  case "reconnect":
44
- err = handleReconnect()
63
+ err = handleReconnect(subArgs, socketPath)
45
64
  case "exec":
46
- err = handleExec()
65
+ err = handleExec(subArgs, socketPath)
47
66
  case "list", "ls":
48
- err = handleList()
67
+ err = handleList(socketPath)
49
68
  case "use":
50
- err = handleUse()
69
+ err = handleUse(subArgs, socketPath)
51
70
  case "forward":
52
- err = handleForward()
71
+ err = handleForward(subArgs, socketPath)
53
72
  case "forwards":
54
- err = handleForwards()
73
+ err = handleForwards(socketPath)
55
74
  case "forward-close":
56
- err = handleForwardClose()
75
+ err = handleForwardClose(subArgs, socketPath)
57
76
  case "scp":
58
- err = handleSCP()
77
+ err = handleSCP(subArgs, socketPath)
59
78
  case "sftp":
60
- err = handleSFTP()
61
- case "help", "-h", "--help":
79
+ err = handleSFTP(subArgs, socketPath)
80
+ case "help":
62
81
  printUsage()
63
82
  default:
64
83
  fmt.Fprintf(os.Stderr, "Unknown command: %s\n", cmd)
@@ -74,9 +93,7 @@ func main() {
74
93
 
75
94
  // readPassword reads password from terminal without echo
76
95
  func readPassword() (string, error) {
77
- // Check if stdin is a terminal
78
96
  if !term.IsTerminal(int(os.Stdin.Fd())) {
79
- // Try to read from stdin
80
97
  reader := bufio.NewReader(os.Stdin)
81
98
  fmt.Print("Password: ")
82
99
  password, err := reader.ReadString('\n')
@@ -116,8 +133,8 @@ func readPassphrase() (string, error) {
116
133
  return string(bytePassphrase), nil
117
134
  }
118
135
 
119
- func sendRequest(method string, params interface{}) ([]byte, error) {
120
- conn, err := net.Dial("unix", *socketPath)
136
+ func sendRequest(socketPath, method string, params interface{}) ([]byte, error) {
137
+ conn, err := net.Dial("unix", socketPath)
121
138
  if err != nil {
122
139
  return nil, fmt.Errorf("failed to connect to daemon: %w", err)
123
140
  }
@@ -140,7 +157,6 @@ func sendRequest(method string, params interface{}) ([]byte, error) {
140
157
  return nil, fmt.Errorf("failed to marshal request: %w", err)
141
158
  }
142
159
 
143
- // Add newline to signal end of request
144
160
  reqData = append(reqData, '\n')
145
161
 
146
162
  _, err = conn.Write(reqData)
@@ -148,14 +164,12 @@ func sendRequest(method string, params interface{}) ([]byte, error) {
148
164
  return nil, fmt.Errorf("failed to send request: %w", err)
149
165
  }
150
166
 
151
- // Read line by line
152
167
  reader := bufio.NewReader(conn)
153
168
  line, err := reader.ReadBytes('\n')
154
169
  if err != nil {
155
170
  return nil, fmt.Errorf("failed to read response: %w", err)
156
171
  }
157
172
 
158
- // Remove trailing newline
159
173
  line = line[:len(line)-1]
160
174
 
161
175
  var resp protocol.Response
@@ -175,22 +189,24 @@ func sendRequest(method string, params interface{}) ([]byte, error) {
175
189
  return result, nil
176
190
  }
177
191
 
178
- func handleConnect() error {
179
- user := flag.String("u", "", "Username")
180
- host := flag.String("h", "", "Host")
181
- port := flag.Int("p", 22, "Port")
182
- password := flag.String("P", "", "Password")
183
- keyPath := flag.String("i", "", "SSH key path")
184
- askPassword := flag.Bool("ask-pass", false, "Ask for password interactively")
185
- askPassphrase := flag.Bool("ask-passphrase", false, "Ask for key passphrase interactively")
192
+ func handleConnect(args []string, socketPath string) error {
193
+ fs := flag.NewFlagSet("connect", flag.ContinueOnError)
194
+ fs.SetOutput(os.Stderr)
195
+
196
+ user := fs.String("u", "", "Username")
197
+ host := fs.String("h", "", "Host")
198
+ port := fs.Int("p", 22, "Port")
199
+ password := fs.String("P", "", "Password")
200
+ keyPath := fs.String("i", "", "SSH key path")
201
+ askPassword := fs.Bool("ask-pass", false, "Ask for password interactively")
202
+ askPassphrase := fs.Bool("ask-passphrase", false, "Ask for key passphrase interactively")
186
203
 
187
- flag.CommandLine.Parse(flag.Args()[1:])
204
+ fs.Parse(args)
188
205
 
189
206
  if *user == "" || *host == "" {
190
207
  return fmt.Errorf("user and host are required")
191
208
  }
192
209
 
193
- // If no password provided via flag and -ask-pass is set, read interactively
194
210
  if *password == "" && *askPassword {
195
211
  p, err := readPassword()
196
212
  if err != nil {
@@ -199,14 +215,11 @@ func handleConnect() error {
199
215
  *password = p
200
216
  }
201
217
 
202
- // If key path provided and -ask-passphrase is set, read interactively
203
218
  if *keyPath != "" && *askPassphrase {
204
219
  passphrase, err := readPassphrase()
205
220
  if err != nil {
206
221
  return fmt.Errorf("failed to read passphrase: %w", err)
207
222
  }
208
- // Note: Passphrase support would require modifying the SSH client
209
- // For now, we'll just warn that it's not supported
210
223
  if passphrase != "" {
211
224
  fmt.Println("Note: Passphrase for keys is not yet supported, ignoring")
212
225
  }
@@ -220,7 +233,7 @@ func handleConnect() error {
220
233
  KeyPath: *keyPath,
221
234
  }
222
235
 
223
- result, err := sendRequest("connect", params)
236
+ result, err := sendRequest(socketPath, "connect", params)
224
237
  if err != nil {
225
238
  return err
226
239
  }
@@ -234,15 +247,24 @@ func handleConnect() error {
234
247
  return nil
235
248
  }
236
249
 
237
- func handleDisconnect() error {
238
- sessionID := flag.String("s", "", "Session ID")
239
- flag.CommandLine.Parse(flag.Args()[1:])
250
+ func handleDisconnect(args []string, socketPath string) error {
251
+ fs := flag.NewFlagSet("disconnect", flag.ContinueOnError)
252
+ fs.SetOutput(os.Stderr)
253
+
254
+ sessionID := fs.String("s", "", "Session ID")
255
+ fs.Parse(args)
256
+
257
+ // Support positional argument
258
+ sessionIDStr := *sessionID
259
+ if sessionIDStr == "" && fs.NArg() > 0 {
260
+ sessionIDStr = fs.Arg(0)
261
+ }
240
262
 
241
263
  params := protocol.DisconnectParams{
242
- SessionID: *sessionID,
264
+ SessionID: sessionIDStr,
243
265
  }
244
266
 
245
- _, err := sendRequest("disconnect", params)
267
+ _, err := sendRequest(socketPath, "disconnect", params)
246
268
  if err != nil {
247
269
  return err
248
270
  }
@@ -251,15 +273,24 @@ func handleDisconnect() error {
251
273
  return nil
252
274
  }
253
275
 
254
- func handleReconnect() error {
255
- sessionID := flag.String("s", "", "Session ID")
256
- flag.CommandLine.Parse(flag.Args()[1:])
276
+ func handleReconnect(args []string, socketPath string) error {
277
+ fs := flag.NewFlagSet("reconnect", flag.ContinueOnError)
278
+ fs.SetOutput(os.Stderr)
279
+
280
+ sessionID := fs.String("s", "", "Session ID")
281
+ fs.Parse(args)
282
+
283
+ // Support positional argument
284
+ sessionIDStr := *sessionID
285
+ if sessionIDStr == "" && fs.NArg() > 0 {
286
+ sessionIDStr = fs.Arg(0)
287
+ }
257
288
 
258
289
  params := protocol.ReconnectParams{
259
- SessionID: *sessionID,
290
+ SessionID: sessionIDStr,
260
291
  }
261
292
 
262
- result, err := sendRequest("reconnect", params)
293
+ result, err := sendRequest(socketPath, "reconnect", params)
263
294
  if err != nil {
264
295
  return err
265
296
  }
@@ -273,22 +304,21 @@ func handleReconnect() error {
273
304
  return nil
274
305
  }
275
306
 
276
- func handleExec() error {
277
- // Get remaining args after "exec"
278
- args := flag.Args()[1:]
307
+ func handleExec(args []string, socketPath string) error {
308
+ fs := flag.NewFlagSet("exec", flag.ContinueOnError)
309
+ fs.SetOutput(os.Stderr)
279
310
 
280
- sessionID := flag.String("s", "", "Session ID")
281
- sudoPassword := flag.String("S", "", "sudo password (will prompt if empty and -s flag used)")
282
- askSudoPassword := flag.Bool("ask-sudo-pass", false, "Interactively ask for sudo password")
311
+ sessionID := fs.String("s", "", "Session ID")
312
+ sudoPassword := fs.String("S", "", "sudo password")
313
+ askSudoPassword := fs.Bool("ask-sudo-pass", false, "Interactively ask for sudo password")
283
314
 
284
- flag.CommandLine.Parse(args)
315
+ fs.Parse(args)
285
316
 
286
- if flag.NArg() < 1 {
317
+ if fs.NArg() < 1 {
287
318
  return fmt.Errorf("command required")
288
319
  }
289
- command := strings.Join(flag.Args(), " ")
320
+ command := strings.Join(fs.Args(), " ")
290
321
 
291
- // If sudo password needed but not provided, prompt for it
292
322
  if *askSudoPassword && *sudoPassword == "" {
293
323
  p, err := readPassword()
294
324
  if err != nil {
@@ -297,9 +327,7 @@ func handleExec() error {
297
327
  *sudoPassword = p
298
328
  }
299
329
 
300
- // If command contains sudo and password provided, wrap the command
301
330
  if *sudoPassword != "" && strings.Contains(command, "sudo") {
302
- // Use printf to pipe password to sudo -S
303
331
  command = fmt.Sprintf("printf '%%s\\n' '%s' | %s -S", *sudoPassword, command)
304
332
  }
305
333
 
@@ -308,7 +336,7 @@ func handleExec() error {
308
336
  Command: command,
309
337
  }
310
338
 
311
- result, err := sendRequest("exec", params)
339
+ result, err := sendRequest(socketPath, "exec", params)
312
340
  if err != nil {
313
341
  return err
314
342
  }
@@ -330,8 +358,8 @@ func handleExec() error {
330
358
  return nil
331
359
  }
332
360
 
333
- func handleList() error {
334
- result, err := sendRequest("list", nil)
361
+ func handleList(socketPath string) error {
362
+ result, err := sendRequest(socketPath, "list", nil)
335
363
  if err != nil {
336
364
  return err
337
365
  }
@@ -354,19 +382,22 @@ func handleList() error {
354
382
  return nil
355
383
  }
356
384
 
357
- func handleUse() error {
358
- flag.CommandLine.Parse(flag.Args()[1:])
385
+ func handleUse(args []string, socketPath string) error {
386
+ fs := flag.NewFlagSet("use", flag.ContinueOnError)
387
+ fs.SetOutput(os.Stderr)
359
388
 
360
- if flag.NArg() < 1 {
389
+ fs.Parse(args)
390
+
391
+ if fs.NArg() < 1 {
361
392
  return fmt.Errorf("session ID required")
362
393
  }
363
- sessionIDStr := flag.Arg(0)
394
+ sessionIDStr := fs.Arg(0)
364
395
 
365
396
  params := protocol.UseParams{
366
397
  SessionID: sessionIDStr,
367
398
  }
368
399
 
369
- _, err := sendRequest("use", params)
400
+ _, err := sendRequest(socketPath, "use", params)
370
401
  if err != nil {
371
402
  return err
372
403
  }
@@ -375,13 +406,16 @@ func handleUse() error {
375
406
  return nil
376
407
  }
377
408
 
378
- func handleForward() error {
379
- sessionID := flag.String("s", "", "Session ID")
380
- local := flag.Int("l", 0, "Local port")
381
- remote := flag.Int("r", 0, "Remote port")
382
- isRemote := flag.Bool("R", false, "Remote port forward")
409
+ func handleForward(args []string, socketPath string) error {
410
+ fs := flag.NewFlagSet("forward", flag.ContinueOnError)
411
+ fs.SetOutput(os.Stderr)
412
+
413
+ sessionID := fs.String("s", "", "Session ID")
414
+ local := fs.Int("l", 0, "Local port")
415
+ remote := fs.Int("r", 0, "Remote port")
416
+ isRemote := fs.Bool("R", false, "Remote port forward")
383
417
 
384
- flag.CommandLine.Parse(flag.Args()[1:])
418
+ fs.Parse(args)
385
419
 
386
420
  if *local == 0 || *remote == 0 {
387
421
  return fmt.Errorf("local and remote ports are required")
@@ -399,7 +433,7 @@ func handleForward() error {
399
433
  Remote: *remote,
400
434
  }
401
435
 
402
- result, err := sendRequest("forward", params)
436
+ result, err := sendRequest(socketPath, "forward", params)
403
437
  if err != nil {
404
438
  return err
405
439
  }
@@ -413,8 +447,8 @@ func handleForward() error {
413
447
  return nil
414
448
  }
415
449
 
416
- func handleForwards() error {
417
- result, err := sendRequest("forwards", nil)
450
+ func handleForwards(socketPath string) error {
451
+ result, err := sendRequest(socketPath, "forwards", nil)
418
452
  if err != nil {
419
453
  return err
420
454
  }
@@ -437,19 +471,22 @@ func handleForwards() error {
437
471
  return nil
438
472
  }
439
473
 
440
- func handleForwardClose() error {
441
- flag.CommandLine.Parse(flag.Args()[1:])
474
+ func handleForwardClose(args []string, socketPath string) error {
475
+ fs := flag.NewFlagSet("forward-close", flag.ContinueOnError)
476
+ fs.SetOutput(os.Stderr)
477
+
478
+ fs.Parse(args)
442
479
 
443
- if flag.NArg() < 1 {
480
+ if fs.NArg() < 1 {
444
481
  return fmt.Errorf("forward ID required")
445
482
  }
446
- forwardID := flag.Arg(0)
483
+ forwardID := fs.Arg(0)
447
484
 
448
485
  params := protocol.ForwardCloseParams{
449
486
  ForwardID: forwardID,
450
487
  }
451
488
 
452
- _, err := sendRequest("forward_close", params)
489
+ _, err := sendRequest(socketPath, "forward_close", params)
453
490
  if err != nil {
454
491
  return err
455
492
  }
@@ -458,19 +495,22 @@ func handleForwardClose() error {
458
495
  return nil
459
496
  }
460
497
 
461
- func handleSCP() error {
462
- sessionID := flag.String("s", "", "Session ID")
463
- isUpload := flag.Bool("put", false, "Upload mode (local->remote)")
464
- isDownload := flag.Bool("get", false, "Download mode (remote->local)")
498
+ func handleSCP(args []string, socketPath string) error {
499
+ fs := flag.NewFlagSet("scp", flag.ContinueOnError)
500
+ fs.SetOutput(os.Stderr)
465
501
 
466
- flag.CommandLine.Parse(flag.Args()[1:])
502
+ sessionID := fs.String("s", "", "Session ID")
503
+ isUpload := fs.Bool("put", false, "Upload mode (local->remote)")
504
+ isDownload := fs.Bool("get", false, "Download mode (remote->local)")
467
505
 
468
- if flag.NArg() < 2 {
506
+ fs.Parse(args)
507
+
508
+ if fs.NArg() < 2 {
469
509
  return fmt.Errorf("source and destination paths required")
470
510
  }
471
511
 
472
- source := flag.Arg(0)
473
- dest := flag.Arg(1)
512
+ source := fs.Arg(0)
513
+ dest := fs.Arg(1)
474
514
 
475
515
  if !*isUpload && !*isDownload {
476
516
  return fmt.Errorf("must specify -put or -get")
@@ -483,7 +523,7 @@ func handleSCP() error {
483
523
  IsUpload: *isUpload,
484
524
  }
485
525
 
486
- result, err := sendRequest("scp", params)
526
+ result, err := sendRequest(socketPath, "scp", params)
487
527
  if err != nil {
488
528
  return err
489
529
  }
@@ -502,12 +542,15 @@ func handleSCP() error {
502
542
  return nil
503
543
  }
504
544
 
505
- func handleSFTP() error {
506
- sessionID := flag.String("s", "", "Session ID")
507
- command := flag.String("c", "", "SFTP command (ls, mkdir, rm)")
508
- path := flag.String("p", ".", "Path")
545
+ func handleSFTP(args []string, socketPath string) error {
546
+ fs := flag.NewFlagSet("sftp", flag.ContinueOnError)
547
+ fs.SetOutput(os.Stderr)
548
+
549
+ sessionID := fs.String("s", "", "Session ID")
550
+ command := fs.String("c", "", "SFTP command (ls, mkdir, rm)")
551
+ path := fs.String("p", ".", "Path")
509
552
 
510
- flag.CommandLine.Parse(flag.Args()[1:])
553
+ fs.Parse(args)
511
554
 
512
555
  if *command == "" {
513
556
  return fmt.Errorf("SFTP command required (-c ls|mkdir|rm)")
@@ -520,7 +563,7 @@ func handleSFTP() error {
520
563
  Command: "ls",
521
564
  Path: *path,
522
565
  }
523
- result, err := sendRequest("sftp_list", params)
566
+ result, err := sendRequest(socketPath, "sftp_list", params)
524
567
  if err != nil {
525
568
  return err
526
569
  }
@@ -540,7 +583,7 @@ func handleSFTP() error {
540
583
  Command: "mkdir",
541
584
  Path: *path,
542
585
  }
543
- _, err := sendRequest("sftp_mkdir", params)
586
+ _, err := sendRequest(socketPath, "sftp_mkdir", params)
544
587
  if err != nil {
545
588
  return err
546
589
  }
@@ -552,7 +595,7 @@ func handleSFTP() error {
552
595
  Command: "rm",
553
596
  Path: *path,
554
597
  }
555
- _, err := sendRequest("sftp_remove", params)
598
+ _, err := sendRequest(socketPath, "sftp_remove", params)
556
599
  if err != nil {
557
600
  return err
558
601
  }
@@ -566,47 +609,54 @@ func handleSFTP() error {
566
609
  }
567
610
 
568
611
  func printUsage() {
569
- fmt.Println(`gssh - SSH Session Manager for Agents (Stateless)
612
+ fmt.Printf(`gssh - SSH Session Manager for Agents v%s
570
613
 
571
614
  Usage:
572
615
  gssh connect -u user -h host [-p port] [-i key_path] [-P password] [--ask-pass]
573
- gssh disconnect [-s session_id]
574
- gssh reconnect [-s session_id]
575
- gssh exec [-s session_id] [-S password | --ask-sudo-pass] "sudo command"
616
+ gssh disconnect [session_id]
617
+ gssh reconnect [session_id]
618
+ gssh exec [-s session_id] [-S password | --ask-sudo-pass] "command"
576
619
  gssh list
577
620
  gssh use <session_id>
578
- gssh forward [-s session_id] -l local_port -r remote_port
621
+ gssh forward [-s session_id] -l local_port -r remote_port [-R]
579
622
  gssh forwards
580
623
  gssh forward-close <forward_id>
581
- gssh scp [-s session_id] [-put|-get] <source> <dest>
582
- gssh sftp [-s session_id] -c <command> -p <path>
583
-
584
- Note: For sudo commands, use key-based authentication or configure passwordless sudo.
624
+ gssh scp [-s session_id] -put <local> <remote>
625
+ gssh scp [-s session_id] -get <remote> <local>
626
+ gssh sftp [-s session_id] -c <ls|mkdir|rm> -p <path>
627
+ gssh -v, --version
585
628
 
586
629
  Options:
587
- -socket path Unix socket path (default: /tmp/gssh.sock)
630
+ -socket path Unix socket path (default: /tmp/gssh.sock)
588
631
  -s session_id Session ID
589
632
  -u user Username
590
633
  -h host Host
591
634
  -p port Port (default: 22)
592
- -P password Password (or use --ask-pass for interactive input)
593
- -i key_path SSH key path
594
- -S password sudo password (for executing sudo commands)
595
- --ask-sudo-pass Interactively ask for sudo password
635
+ -P password Password
636
+ -i key_path SSH key path
637
+ -S password sudo password
638
+ --ask-pass Ask for password interactively
639
+ --ask-sudo-pass Ask for sudo password interactively
596
640
  -l local Local port
597
641
  -r remote Remote port
598
- -R Remote port forward
599
- -put Upload mode (local -> remote)
600
- -get Download mode (remote -> local)
642
+ -R Remote port forward (default: local)
643
+ -put Upload (local -> remote)
644
+ -get Download (remote -> local)
601
645
  -c command SFTP command (ls, mkdir, rm)
602
646
  -p path Path for SFTP command
603
- --ask-pass Interactively ask for SSH password (secure)
604
- --ask-passphrase Interactively ask for key passphrase (secure)`)
647
+ -v, --version Show version
648
+
649
+ Examples:
650
+ gssh connect -u admin -h 192.168.1.1 -P password
651
+ gssh exec "ls -la"
652
+ gssh reconnect 549b6eff-f62c-4dae-a7e9-298815233cf4
653
+ gssh forward -l 8080 -r 80
654
+ gssh scp -put local.txt /home/user/remote.txt
655
+ `, version)
605
656
  }
606
657
 
607
658
  // Restore terminal on exit
608
659
  func init() {
609
- // Setup cleanup to restore terminal state on unexpected exit
610
660
  c := make(chan os.Signal, 1)
611
661
  signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
612
662
  go func() {
@@ -2,6 +2,7 @@ package session
2
2
 
3
3
  import (
4
4
  "fmt"
5
+ "strings"
5
6
  "sync"
6
7
  "time"
7
8
 
@@ -45,6 +46,53 @@ func NewManager() *Manager {
45
46
  }
46
47
  }
47
48
 
49
+ // needsShell returns true if the command needs to be executed through a shell
50
+ func needsShell(cmd string) bool {
51
+ // Check for heredoc
52
+ if strings.Contains(cmd, "<<") {
53
+ return true
54
+ }
55
+
56
+ // Check for newlines (multi-line commands)
57
+ if strings.Contains(cmd, "\n") {
58
+ return true
59
+ }
60
+
61
+ // Check for pipe
62
+ if strings.Contains(cmd, " | ") {
63
+ return true
64
+ }
65
+
66
+ // Check for redirection
67
+ if strings.ContainsAny(cmd, "><") {
68
+ if strings.Contains(cmd, ">") || strings.Contains(cmd, "<") {
69
+ return true
70
+ }
71
+ }
72
+
73
+ // Check for logical operators
74
+ if strings.Contains(cmd, " && ") || strings.Contains(cmd, " || ") {
75
+ return true
76
+ }
77
+
78
+ // Check for background execution
79
+ if strings.HasSuffix(strings.TrimSpace(cmd), "&") {
80
+ return true
81
+ }
82
+
83
+ // Check for command substitution
84
+ if strings.Contains(cmd, "$(") || strings.Contains(cmd, "`") {
85
+ return true
86
+ }
87
+
88
+ // Check for environment variables (but not $$ which is PID)
89
+ if strings.Contains(cmd, "${") || (strings.Contains(cmd, "$") && !strings.Contains(cmd, "$$")) {
90
+ return true
91
+ }
92
+
93
+ return false
94
+ }
95
+
48
96
  // Connect creates a new SSH session
49
97
  func (m *Manager) Connect(user, host string, port int, password, keyPath string) (*protocol.Session, error) {
50
98
  m.mu.Lock()
@@ -101,6 +149,11 @@ func (m *Manager) Connect(user, host string, port int, password, keyPath string)
101
149
 
102
150
  // Disconnect closes a session
103
151
  func (m *Manager) Disconnect(sessionID string) error {
152
+ // Use default session if not specified
153
+ if sessionID == "" {
154
+ sessionID = m.defaultID
155
+ }
156
+
104
157
  m.mu.Lock()
105
158
  defer m.mu.Unlock()
106
159
 
@@ -125,6 +178,11 @@ func (m *Manager) Disconnect(sessionID string) error {
125
178
 
126
179
  // Reconnect reconnects a session
127
180
  func (m *Manager) Reconnect(sessionID string) (*protocol.Session, error) {
181
+ // Use default session if not specified
182
+ if sessionID == "" {
183
+ sessionID = m.defaultID
184
+ }
185
+
128
186
  m.mu.Lock()
129
187
  ms, ok := m.sessions[sessionID]
130
188
  m.mu.Unlock()
@@ -194,9 +252,12 @@ func (m *Manager) Exec(sessionID, command string) (*protocol.ExecResult, error)
194
252
  }
195
253
  defer session.Close()
196
254
 
197
- // 直接执行命令,不通过 shell
198
-
255
+ // 检测是否需要通过 shell 执行
199
256
  fullCmd := command
257
+ if needsShell(command) {
258
+ // 使用 /bin/sh 更通用
259
+ fullCmd = fmt.Sprintf("/bin/sh -c %q", command)
260
+ }
200
261
 
201
262
  output, err := session.CombinedOutput(fullCmd)
202
263
  if err != nil {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gssh-agent",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "SSH Session Manager for Agents - Stateless SSH client with SFTP support",
5
5
  "bin": {
6
6
  "gssh": "./bin/gssh",