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.
- package/README.md +24 -0
- package/bin/daemon +0 -0
- package/bin/gssh +0 -0
- package/bin/gssh-daemon +0 -0
- package/cmd/gssh/main.go +235 -12
- package/go.mod +6 -1
- package/go.sum +46 -0
- package/internal/client/ssh.go +252 -2
- package/internal/portforward/forwarder.go +88 -19
- package/internal/protocol/types.go +23 -0
- package/internal/session/manager.go +203 -20
- package/package.json +21 -6
- package/pkg/rpc/handler.go +28 -0
- package/skill.md +65 -65
- package/gssh-darwin-arm64.tar.gz +0 -0
- package/homebrew/gssh.plist +0 -22
- package/homebrew/install.sh +0 -43
|
@@ -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
|
|
27
|
-
Host
|
|
28
|
-
User
|
|
29
|
-
Port
|
|
30
|
-
Status
|
|
31
|
-
Password
|
|
32
|
-
KeyPath
|
|
33
|
-
SSHClient
|
|
34
|
-
LastCmd
|
|
35
|
-
Forwards
|
|
36
|
-
mu
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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:
|
|
198
|
-
Stderr:
|
|
199
|
-
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.
|
|
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
|
}
|
package/pkg/rpc/handler.go
CHANGED
|
@@ -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, ¶ms); 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, ¶ms); 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, ¶ms); 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, ¶ms); 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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
#
|
|
19
|
-
gssh connect -u
|
|
17
|
+
# 使用密码
|
|
18
|
+
gssh connect -u user -h host -p password
|
|
20
19
|
|
|
21
|
-
#
|
|
22
|
-
gssh
|
|
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` |
|
|
28
|
+
| `connect` | 建立 SSH 连接 |
|
|
39
29
|
| `exec` | 执行命令 |
|
|
40
|
-
| `
|
|
41
|
-
| `
|
|
42
|
-
| `disconnect` | 断开 session |
|
|
43
|
-
| `reconnect` | 重连 session |
|
|
30
|
+
| `scp` | 文件传输 |
|
|
31
|
+
| `sftp` | SFTP 操作 |
|
|
44
32
|
| `forward` | 端口转发 |
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
33
|
+
| `list` | 列出 session |
|
|
34
|
+
| `use` | 切换默认 session |
|
|
47
35
|
|
|
48
|
-
##
|
|
36
|
+
## 执行命令
|
|
49
37
|
|
|
50
38
|
```bash
|
|
51
|
-
#
|
|
52
|
-
gssh
|
|
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
|
-
#
|
|
62
|
-
gssh
|
|
42
|
+
# sudo 命令
|
|
43
|
+
gssh exec -S password "sudo systemctl restart nginx"
|
|
63
44
|
```
|
|
64
45
|
|
|
65
|
-
##
|
|
46
|
+
## 文件传输
|
|
66
47
|
|
|
67
48
|
```bash
|
|
68
|
-
#
|
|
69
|
-
gssh
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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 密码 |
|
package/gssh-darwin-arm64.tar.gz
DELETED
|
Binary file
|
package/homebrew/gssh.plist
DELETED
|
@@ -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>
|
package/homebrew/install.sh
DELETED
|
@@ -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"
|