gssh-agent 1.0.0 → 1.0.2

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.
@@ -10,6 +10,7 @@ import (
10
10
  "gssh/internal/protocol"
11
11
 
12
12
  "github.com/google/uuid"
13
+ "golang.org/x/crypto/ssh"
13
14
  )
14
15
 
15
16
  // Manager manages SSH sessions
@@ -23,17 +24,17 @@ type Manager struct {
23
24
 
24
25
  // ManagedSession wraps a session with additional state
25
26
  type ManagedSession struct {
26
- ID string
27
- Host string
28
- User string
29
- Port int
30
- Status string
31
- Password string
32
- KeyPath string
33
- SSHClient *client.SSHClient
34
- LastCmd string
35
- Forwards map[string]*portforward.Forwarder
36
- mu sync.RWMutex
27
+ ID string
28
+ Host string
29
+ User string
30
+ Port int
31
+ Status string
32
+ Password string
33
+ KeyPath string
34
+ SSHClient *client.SSHClient
35
+ LastCmd string
36
+ Forwards map[string]*portforward.Forwarder
37
+ mu sync.RWMutex
37
38
  }
38
39
 
39
40
  // NewManager creates a new session manager
@@ -180,23 +181,47 @@ func (m *Manager) Exec(sessionID, command string) (*protocol.ExecResult, error)
180
181
  }
181
182
 
182
183
  ms.mu.RLock()
183
- defer ms.mu.RUnlock()
184
-
185
184
  if ms.SSHClient == nil {
185
+ ms.mu.RUnlock()
186
186
  return nil, fmt.Errorf("session not connected")
187
187
  }
188
+ ms.mu.RUnlock()
188
189
 
189
- stdout, stderr, exitCode, err := ms.SSHClient.Exec(command)
190
- if err != nil && exitCode == 0 {
191
- return nil, err
190
+ // 复用 SSH 连接,创建新的 session 执行命令
191
+ session, err := ms.SSHClient.Client.NewSession()
192
+ if err != nil {
193
+ return nil, fmt.Errorf("failed to create session: %w", err)
192
194
  }
195
+ defer session.Close()
193
196
 
194
- ms.LastCmd = command
197
+ // 直接执行命令,不通过 shell
198
+
199
+ fullCmd := command
200
+
201
+ output, err := session.CombinedOutput(fullCmd)
202
+ if err != nil {
203
+ exitErr, ok := err.(*ssh.ExitError)
204
+ if ok {
205
+ ms.LastCmd = command
206
+ return &protocol.ExecResult{
207
+ Stdout: string(output),
208
+ Stderr: "",
209
+ ExitCode: exitErr.ExitStatus(),
210
+ }, nil
211
+ }
212
+ ms.LastCmd = command
213
+ return &protocol.ExecResult{
214
+ Stdout: string(output),
215
+ Stderr: "",
216
+ ExitCode: 0,
217
+ }, nil
218
+ }
195
219
 
220
+ ms.LastCmd = command
196
221
  return &protocol.ExecResult{
197
- Stdout: stdout,
198
- Stderr: stderr,
199
- ExitCode: exitCode,
222
+ Stdout: string(output),
223
+ Stderr: "",
224
+ ExitCode: 0,
200
225
  }, nil
201
226
  }
202
227
 
@@ -314,6 +339,164 @@ func (m *Manager) CloseForward(forwardID string) error {
314
339
  return nil
315
340
  }
316
341
 
342
+ // SCP uploads or downloads files via SFTP
343
+ func (m *Manager) SCP(sessionID, source, dest string, isUpload bool) (*protocol.SCPResult, error) {
344
+ m.mu.RLock()
345
+ var ms *ManagedSession
346
+ if sessionID != "" {
347
+ ms = m.sessions[sessionID]
348
+ } else if m.defaultID != "" {
349
+ ms = m.sessions[m.defaultID]
350
+ }
351
+ m.mu.RUnlock()
352
+
353
+ if ms == nil {
354
+ return nil, fmt.Errorf("session not found")
355
+ }
356
+
357
+ ms.mu.RLock()
358
+ sshClient := ms.SSHClient
359
+ ms.mu.RUnlock()
360
+
361
+ if sshClient == nil {
362
+ return nil, fmt.Errorf("session not connected")
363
+ }
364
+
365
+ // Create SFTP client
366
+ sftpClient, err := sshClient.NewSFTPClient()
367
+ if err != nil {
368
+ return nil, fmt.Errorf("failed to create SFTP client: %w", err)
369
+ }
370
+ defer sftpClient.Close()
371
+
372
+ start := time.Now()
373
+
374
+ var bytes int64
375
+ var transferErr error
376
+
377
+ if isUpload {
378
+ // Upload: source is local, dest is remote
379
+ bytes, transferErr = sftpClient.Upload(source, dest)
380
+ } else {
381
+ // Download: source is remote, dest is local
382
+ bytes, transferErr = sftpClient.Download(source, dest)
383
+ }
384
+
385
+ duration := time.Since(start).Milliseconds()
386
+
387
+ if transferErr != nil {
388
+ return &protocol.SCPResult{
389
+ Success: false,
390
+ Message: transferErr.Error(),
391
+ Bytes: bytes,
392
+ Duration: duration,
393
+ }, nil
394
+ }
395
+
396
+ return &protocol.SCPResult{
397
+ Success: true,
398
+ Message: fmt.Sprintf("Transferred %d bytes in %dms", bytes, duration),
399
+ Bytes: bytes,
400
+ Duration: duration,
401
+ }, nil
402
+ }
403
+
404
+ // SFTPList lists files in a remote directory
405
+ func (m *Manager) SFTPList(sessionID, path string) ([]string, error) {
406
+ m.mu.RLock()
407
+ var ms *ManagedSession
408
+ if sessionID != "" {
409
+ ms = m.sessions[sessionID]
410
+ } else if m.defaultID != "" {
411
+ ms = m.sessions[m.defaultID]
412
+ }
413
+ m.mu.RUnlock()
414
+
415
+ if ms == nil {
416
+ return nil, fmt.Errorf("session not found")
417
+ }
418
+
419
+ ms.mu.RLock()
420
+ sshClient := ms.SSHClient
421
+ ms.mu.RUnlock()
422
+
423
+ if sshClient == nil {
424
+ return nil, fmt.Errorf("session not connected")
425
+ }
426
+
427
+ sftpClient, err := sshClient.NewSFTPClient()
428
+ if err != nil {
429
+ return nil, fmt.Errorf("failed to create SFTP client: %w", err)
430
+ }
431
+ defer sftpClient.Close()
432
+
433
+ return sftpClient.List(path)
434
+ }
435
+
436
+ // SFTPMkdir creates a remote directory
437
+ func (m *Manager) SFTPMkdir(sessionID, path string) error {
438
+ m.mu.RLock()
439
+ var ms *ManagedSession
440
+ if sessionID != "" {
441
+ ms = m.sessions[sessionID]
442
+ } else if m.defaultID != "" {
443
+ ms = m.sessions[m.defaultID]
444
+ }
445
+ m.mu.RUnlock()
446
+
447
+ if ms == nil {
448
+ return fmt.Errorf("session not found")
449
+ }
450
+
451
+ ms.mu.RLock()
452
+ sshClient := ms.SSHClient
453
+ ms.mu.RUnlock()
454
+
455
+ if sshClient == nil {
456
+ return fmt.Errorf("session not connected")
457
+ }
458
+
459
+ sftpClient, err := sshClient.NewSFTPClient()
460
+ if err != nil {
461
+ return fmt.Errorf("failed to create SFTP client: %w", err)
462
+ }
463
+ defer sftpClient.Close()
464
+
465
+ return sftpClient.Mkdir(path)
466
+ }
467
+
468
+ // SFTPRemove removes a remote file
469
+ func (m *Manager) SFTPRemove(sessionID, path string) error {
470
+ m.mu.RLock()
471
+ var ms *ManagedSession
472
+ if sessionID != "" {
473
+ ms = m.sessions[sessionID]
474
+ } else if m.defaultID != "" {
475
+ ms = m.sessions[m.defaultID]
476
+ }
477
+ m.mu.RUnlock()
478
+
479
+ if ms == nil {
480
+ return fmt.Errorf("session not found")
481
+ }
482
+
483
+ ms.mu.RLock()
484
+ sshClient := ms.SSHClient
485
+ ms.mu.RUnlock()
486
+
487
+ if sshClient == nil {
488
+ return fmt.Errorf("session not connected")
489
+ }
490
+
491
+ sftpClient, err := sshClient.NewSFTPClient()
492
+ if err != nil {
493
+ return fmt.Errorf("failed to create SFTP client: %w", err)
494
+ }
495
+ defer sftpClient.Close()
496
+
497
+ return sftpClient.Remove(path)
498
+ }
499
+
317
500
  // monitorReconnect monitors connection and auto-reconnects
318
501
  func (m *Manager) monitorReconnect(ms *ManagedSession) {
319
502
  ticker := time.NewTicker(5 * time.Second)
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "gssh-agent",
3
- "version": "1.0.0",
4
- "description": "SSH Session Manager for Agents",
3
+ "version": "1.0.2",
4
+ "description": "SSH Session Manager for Agents - Stateless SSH client with SFTP support",
5
5
  "bin": {
6
- "gssh": "bin/gssh",
7
- "gssh-daemon": "bin/gssh-daemon"
6
+ "gssh": "./bin/gssh",
7
+ "gssh-daemon": "./bin/gssh-daemon"
8
8
  },
9
9
  "repository": {
10
10
  "type": "git",
@@ -12,14 +12,29 @@
12
12
  },
13
13
  "keywords": [
14
14
  "ssh",
15
+ "sftp",
16
+ "scp",
15
17
  "agent",
16
18
  "session-manager",
17
- "port-forwarding"
19
+ "port-forwarding",
20
+ "remote-execution"
18
21
  ],
19
22
  "author": "",
20
23
  "license": "MIT",
21
24
  "bugs": {
22
25
  "url": "https://github.com/forechoandlook/gssh/issues"
23
26
  },
24
- "homepage": "https://github.com/forechoandlook/gssh#readme"
27
+ "homepage": "https://github.com/forechoandlook/gssh#readme",
28
+ "preferGlobal": true,
29
+ "os": [
30
+ "darwin",
31
+ "linux"
32
+ ],
33
+ "cpu": [
34
+ "x64",
35
+ "arm64"
36
+ ],
37
+ "engines": {
38
+ "node": ">=14.0.0"
39
+ }
25
40
  }
@@ -89,6 +89,34 @@ func (h *Handler) Handle(data []byte) ([]byte, error) {
89
89
  }
90
90
  err = h.manager.CloseForward(params.ForwardID)
91
91
 
92
+ case "scp":
93
+ var params protocol.SCPParams
94
+ if err := json.Unmarshal(req.Params, &params); err != nil {
95
+ return h.errorResponse(req.ID, -32602, "Invalid params")
96
+ }
97
+ result, err = h.manager.SCP(params.SessionID, params.Source, params.Dest, params.IsUpload)
98
+
99
+ case "sftp_list":
100
+ var params protocol.SFTPParams
101
+ if err := json.Unmarshal(req.Params, &params); err != nil {
102
+ return h.errorResponse(req.ID, -32602, "Invalid params")
103
+ }
104
+ result, err = h.manager.SFTPList(params.SessionID, params.Path)
105
+
106
+ case "sftp_mkdir":
107
+ var params protocol.SFTPParams
108
+ if err := json.Unmarshal(req.Params, &params); err != nil {
109
+ return h.errorResponse(req.ID, -32602, "Invalid params")
110
+ }
111
+ err = h.manager.SFTPMkdir(params.SessionID, params.Path)
112
+
113
+ case "sftp_remove":
114
+ var params protocol.SFTPParams
115
+ if err := json.Unmarshal(req.Params, &params); err != nil {
116
+ return h.errorResponse(req.ID, -32602, "Invalid params")
117
+ }
118
+ err = h.manager.SFTPRemove(params.SessionID, params.Path)
119
+
92
120
  default:
93
121
  return h.errorResponse(req.ID, -32601, fmt.Sprintf("Method not found: %s", req.Method))
94
122
  }
package/skill.md CHANGED
@@ -1,96 +1,96 @@
1
- # gssh 使用指南
2
-
3
- ## 快速开始
4
-
5
- ### 1. 安装
6
-
7
- ```bash
8
- git clone <repository-url>
9
- cd gssh
10
- go build -o bin/daemon cmd/daemon/main.go
11
- go build -o bin/gssh cmd/gssh/main.go
12
- ./homebrew/install.sh
13
- ```
14
-
15
- ### 2. 基本使用
1
+ ---
2
+ name: use-gssh
3
+ description: "Manage SSH sessions with gssh CLI for agents"
4
+ allowed-tools: Read, Write, Glob, Grep, Bash
5
+ triggers:
6
+ - gssh
7
+ - ssh session
8
+ - ssh 连接
9
+ - ssh 管理
10
+ - sftp
11
+ - scp 文件传输
12
+ ---
13
+
14
+ ## 连接认证
16
15
 
17
16
  ```bash
18
- # 连接 SSH
19
- gssh connect -u admin1 -h 192.168.1.100 -P 1234
17
+ # 使用密码
18
+ gssh connect -u user -h host -p password
20
19
 
21
- # 执行命令
22
- gssh exec "ls -la"
23
-
24
- # 列出 session
25
- gssh list
20
+ # SSH 密钥
21
+ gssh connect -u user -h host -i ~/.ssh/id_rsa
26
22
  ```
27
23
 
28
- ## 核心概念
29
-
30
- - **daemon**: 后台服务,管理 SSH 连接
31
- - **session**: 一个 SSH 连接会话
32
- - **默认 session**: 不指定 session ID 时使用的会话
33
-
34
- ## 命令列表
24
+ ## 核心命令
35
25
 
36
26
  | 命令 | 说明 |
37
27
  |------|------|
38
- | `connect` | 建立新 SSH 连接 |
28
+ | `connect` | 建立 SSH 连接 |
39
29
  | `exec` | 执行命令 |
40
- | `list` | 列出所有 session |
41
- | `use` | 切换默认 session |
42
- | `disconnect` | 断开 session |
43
- | `reconnect` | 重连 session |
30
+ | `scp` | 文件传输 |
31
+ | `sftp` | SFTP 操作 |
44
32
  | `forward` | 端口转发 |
45
- | `forwards` | 列出转发 |
46
- | `forward-close` | 关闭转发 |
33
+ | `list` | 列出 session |
34
+ | `use` | 切换默认 session |
47
35
 
48
- ## 典型工作流
36
+ ## 执行命令
49
37
 
50
38
  ```bash
51
- # 1. 连接远程主机
52
- gssh connect -u admin -h 192.168.1.100 -P password
53
-
54
- # 2. 执行命令(使用默认 session)
55
- gssh exec "pwd"
56
- gssh exec "uname -a"
57
-
58
- # 3. 断开连接
59
- gssh disconnect
39
+ # 普通命令
40
+ gssh exec "ls -la"
60
41
 
61
- # 4. 重连
62
- gssh reconnect -s <session-id>
42
+ # sudo 命令
43
+ gssh exec -S password "sudo systemctl restart nginx"
63
44
  ```
64
45
 
65
- ## SSH 密钥认证
46
+ ## 文件传输
66
47
 
67
48
  ```bash
68
- # 使用密钥连接
69
- gssh connect -u admin -h 192.168.1.100 -i ~/.ssh/id_rsa
49
+ # 上传本地 -> 远程
50
+ gssh scp -put local.txt /remote/path/
51
+
52
+ # 下载远程 -> 本地
53
+ gssh scp -get /remote/file.txt ./
54
+
55
+ # SFTP 目录操作
56
+ gssh sftp -c ls -p /home/user
57
+ gssh sftp -c mkdir -p /home/user/newdir
58
+ gssh sftp -c rm -p /home/user/file.txt
70
59
  ```
71
60
 
72
61
  ## 端口转发
73
62
 
74
63
  ```bash
75
- # 本地转发:localhost:8080 -> remote:80
64
+ # 本地转发: localhost:8080 -> remote:80
76
65
  gssh forward -l 8080 -r 80
66
+
67
+ # 远程转发: remote:9000 -> localhost:3000
68
+ gssh forward -R -l 9000 -r 3000
77
69
  ```
78
70
 
79
- ## 服务管理
71
+ ## Session 管理
80
72
 
81
73
  ```bash
82
- # 启动
83
- brew services start gssh
84
-
85
- # 停止
86
- brew services stop gssh
87
-
88
- # 状态
89
- brew services list
74
+ gssh list # 列出所有 session
75
+ gssh use <session-id> # 切换默认 session
76
+ gssh disconnect # 断开
77
+ gssh reconnect -s <id> # 重连
90
78
  ```
91
79
 
92
- ## 注意事项
80
+ ## 选项汇总
93
81
 
94
- - Agent 使用场景:无状态,每次请求独立完成
95
- - sudo 命令:需要配置 passwordless sudo 或使用密钥认证
96
- - Session 状态由 daemon 维护,CLI 只负责发送请求
82
+ | 选项 | 说明 |
83
+ |------|------|
84
+ | `-s` | session ID |
85
+ | `-u` | 用户名 |
86
+ | `-h` | 主机地址 |
87
+ | `-p` | 端口(默认 22) |
88
+ | `-P` | SSH 密码 |
89
+ | `-i` | SSH 密钥 |
90
+ | `-l` | 本地端口 |
91
+ | `-r` | 远程端口 |
92
+ | `-R` | 远程端口转发 |
93
+ | `-put/-get` | 上传/下载 |
94
+ | `-S` | sudo 密码 |
95
+ | `--ask-pass` | 交互输入 SSH 密码 |
96
+ | `--ask-sudo-pass` | 交互输入 sudo 密码 |
Binary file
@@ -1,22 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
- <plist version="1.0">
4
- <dict>
5
- <key>Label</key>
6
- <string>com.gssh.daemon</string>
7
- <key>ProgramArguments</key>
8
- <array>
9
- <string>/usr/local/bin/gssh-daemon</string>
10
- <string>-socket</string>
11
- <string>/tmp/gssh.sock</string>
12
- </array>
13
- <key>RunAtLoad</key>
14
- <true/>
15
- <key>KeepAlive</key>
16
- <true/>
17
- <key>StandardOutPath</key>
18
- <string>/var/log/gssh.log</string>
19
- <key>StandardErrorPath</key>
20
- <string>/var/log/gssh.error.log</string>
21
- </dict>
22
- </plist>
@@ -1,43 +0,0 @@
1
- #!/bin/bash
2
- # gssh service installation script
3
-
4
- set -e
5
-
6
- # Detect architecture
7
- ARCH=$(uname -m)
8
- if [ "$ARCH" = "arm64" ]; then
9
- PREFIX="/opt/homebrew"
10
- else
11
- PREFIX="/usr/local"
12
- fi
13
-
14
- DAEMON_BIN="$PREFIX/bin/gssh-daemon"
15
- CLI_BIN="$PREFIX/bin/gssh"
16
-
17
- echo "Installing gssh to $PREFIX/bin/..."
18
-
19
- # Copy binaries
20
- cp bin/daemon "$DAEMON_BIN"
21
- cp bin/gssh "$CLI_BIN"
22
-
23
- chmod +x "$DAEMON_BIN" "$CLI_BIN"
24
-
25
- # Install launchd plist
26
- PLIST_DIR="$HOME/Library/LaunchAgents"
27
- mkdir -p "$PLIST_DIR"
28
- cp homebrew/gssh.plist "$PLIST_DIR/"
29
-
30
- echo "Loading gssh service..."
31
- launchctl load "$PLIST_DIR/gssh.plist"
32
-
33
- echo "gssh installed successfully!"
34
- echo ""
35
- echo "Usage:"
36
- echo " Start: launchctl start com.gssh.daemon"
37
- echo " Stop: launchctl stop com.gssh.daemon"
38
- echo " Status: launchctl list | grep gssh"
39
- echo ""
40
- echo "Or use Homebrew services:"
41
- echo " brew services start gssh"
42
- echo " brew services stop gssh"
43
- echo " brew services list"