gssh-agent 1.0.0
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 +175 -0
- package/bin/daemon +0 -0
- package/bin/gssh +0 -0
- package/bin/gssh-daemon +0 -0
- package/cmd/daemon/main.go +45 -0
- package/cmd/gssh/main.go +394 -0
- package/go.mod +10 -0
- package/go.sum +8 -0
- package/gssh-darwin-arm64.tar.gz +0 -0
- package/homebrew/gssh.plist +22 -0
- package/homebrew/install.sh +43 -0
- package/idea.md +271 -0
- package/internal/client/ssh.go +143 -0
- package/internal/client/ssh_test.go +33 -0
- package/internal/portforward/forwarder.go +187 -0
- package/internal/protocol/types.go +90 -0
- package/internal/protocol/types_test.go +87 -0
- package/internal/session/manager.go +380 -0
- package/internal/session/manager_test.go +52 -0
- package/package.json +25 -0
- package/pkg/rpc/handler.go +206 -0
- package/skill.md +96 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
package protocol
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"testing"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func TestSessionJSON(t *testing.T) {
|
|
9
|
+
session := Session{
|
|
10
|
+
ID: "test-id",
|
|
11
|
+
Host: "192.168.1.1",
|
|
12
|
+
User: "admin",
|
|
13
|
+
Port: 22,
|
|
14
|
+
Status: "connected",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
data, err := json.Marshal(session)
|
|
18
|
+
if err != nil {
|
|
19
|
+
t.Fatalf("failed to marshal: %v", err)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
var parsed Session
|
|
23
|
+
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
24
|
+
t.Fatalf("failed to unmarshal: %v", err)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if parsed.ID != session.ID {
|
|
28
|
+
t.Errorf("expected ID %s, got %s", session.ID, parsed.ID)
|
|
29
|
+
}
|
|
30
|
+
if parsed.Host != session.Host {
|
|
31
|
+
t.Errorf("expected Host %s, got %s", session.Host, parsed.Host)
|
|
32
|
+
}
|
|
33
|
+
if parsed.User != session.User {
|
|
34
|
+
t.Errorf("expected User %s, got %s", session.User, parsed.User)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func TestExecResultJSON(t *testing.T) {
|
|
39
|
+
result := ExecResult{
|
|
40
|
+
Stdout: "hello\n",
|
|
41
|
+
Stderr: "error\n",
|
|
42
|
+
ExitCode: 0,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
data, err := json.Marshal(result)
|
|
46
|
+
if err != nil {
|
|
47
|
+
t.Fatalf("failed to marshal: %v", err)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
var parsed ExecResult
|
|
51
|
+
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
52
|
+
t.Fatalf("failed to unmarshal: %v", err)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if parsed.Stdout != result.Stdout {
|
|
56
|
+
t.Errorf("expected stdout %s, got %s", result.Stdout, parsed.Stdout)
|
|
57
|
+
}
|
|
58
|
+
if parsed.ExitCode != result.ExitCode {
|
|
59
|
+
t.Errorf("expected exit code %d, got %d", result.ExitCode, parsed.ExitCode)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func TestForwardJSON(t *testing.T) {
|
|
64
|
+
forward := Forward{
|
|
65
|
+
ID: "fwd-1",
|
|
66
|
+
Type: "local",
|
|
67
|
+
Local: 8080,
|
|
68
|
+
Remote: 80,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
data, err := json.Marshal(forward)
|
|
72
|
+
if err != nil {
|
|
73
|
+
t.Fatalf("failed to marshal: %v", err)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
var parsed Forward
|
|
77
|
+
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
78
|
+
t.Fatalf("failed to unmarshal: %v", err)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if parsed.Type != forward.Type {
|
|
82
|
+
t.Errorf("expected type %s, got %s", forward.Type, parsed.Type)
|
|
83
|
+
}
|
|
84
|
+
if parsed.Local != forward.Local {
|
|
85
|
+
t.Errorf("expected local port %d, got %d", forward.Local, parsed.Local)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
package session
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"sync"
|
|
6
|
+
"time"
|
|
7
|
+
|
|
8
|
+
"gssh/internal/client"
|
|
9
|
+
"gssh/internal/portforward"
|
|
10
|
+
"gssh/internal/protocol"
|
|
11
|
+
|
|
12
|
+
"github.com/google/uuid"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// Manager manages SSH sessions
|
|
16
|
+
type Manager struct {
|
|
17
|
+
sessions map[string]*ManagedSession
|
|
18
|
+
defaultID string
|
|
19
|
+
mu sync.RWMutex
|
|
20
|
+
forwards map[string]*portforward.Forwarder
|
|
21
|
+
forwardMu sync.RWMutex
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ManagedSession wraps a session with additional state
|
|
25
|
+
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
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// NewManager creates a new session manager
|
|
40
|
+
func NewManager() *Manager {
|
|
41
|
+
return &Manager{
|
|
42
|
+
sessions: make(map[string]*ManagedSession),
|
|
43
|
+
forwards: make(map[string]*portforward.Forwarder),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Connect creates a new SSH session
|
|
48
|
+
func (m *Manager) Connect(user, host string, port int, password, keyPath string) (*protocol.Session, error) {
|
|
49
|
+
m.mu.Lock()
|
|
50
|
+
defer m.mu.Unlock()
|
|
51
|
+
|
|
52
|
+
// Check if session already exists
|
|
53
|
+
for _, s := range m.sessions {
|
|
54
|
+
if s.Host == host && s.User == user && s.Port == port {
|
|
55
|
+
if s.Status == "connected" {
|
|
56
|
+
return toProtocolSession(s), fmt.Errorf("session already exists")
|
|
57
|
+
}
|
|
58
|
+
// Try to reconnect
|
|
59
|
+
sshClient, err := client.Connect(user, host, port, password, keyPath)
|
|
60
|
+
if err != nil {
|
|
61
|
+
return nil, err
|
|
62
|
+
}
|
|
63
|
+
s.SSHClient = sshClient
|
|
64
|
+
s.Status = "connected"
|
|
65
|
+
return toProtocolSession(s), nil
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Create new session
|
|
70
|
+
sshClient, err := client.Connect(user, host, port, password, keyPath)
|
|
71
|
+
if err != nil {
|
|
72
|
+
return nil, err
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
id := uuid.New().String()
|
|
76
|
+
ms := &ManagedSession{
|
|
77
|
+
ID: id,
|
|
78
|
+
Host: host,
|
|
79
|
+
User: user,
|
|
80
|
+
Port: port,
|
|
81
|
+
Status: "connected",
|
|
82
|
+
Password: password,
|
|
83
|
+
KeyPath: keyPath,
|
|
84
|
+
SSHClient: sshClient,
|
|
85
|
+
Forwards: make(map[string]*portforward.Forwarder),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
m.sessions[id] = ms
|
|
89
|
+
|
|
90
|
+
// Set as default if first session
|
|
91
|
+
if m.defaultID == "" {
|
|
92
|
+
m.defaultID = id
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Start reconnect monitor
|
|
96
|
+
go m.monitorReconnect(ms)
|
|
97
|
+
|
|
98
|
+
return toProtocolSession(ms), nil
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Disconnect closes a session
|
|
102
|
+
func (m *Manager) Disconnect(sessionID string) error {
|
|
103
|
+
m.mu.Lock()
|
|
104
|
+
defer m.mu.Unlock()
|
|
105
|
+
|
|
106
|
+
ms, ok := m.sessions[sessionID]
|
|
107
|
+
if !ok {
|
|
108
|
+
return fmt.Errorf("session not found")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if ms.SSHClient != nil {
|
|
112
|
+
ms.SSHClient.Close()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
ms.Status = "disconnected"
|
|
116
|
+
|
|
117
|
+
// Clear default ID when disconnecting
|
|
118
|
+
if m.defaultID == sessionID {
|
|
119
|
+
m.defaultID = ""
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return nil
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Reconnect reconnects a session
|
|
126
|
+
func (m *Manager) Reconnect(sessionID string) (*protocol.Session, error) {
|
|
127
|
+
m.mu.Lock()
|
|
128
|
+
ms, ok := m.sessions[sessionID]
|
|
129
|
+
m.mu.Unlock()
|
|
130
|
+
|
|
131
|
+
if !ok {
|
|
132
|
+
return nil, fmt.Errorf("session not found")
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Close existing connection
|
|
136
|
+
if ms.SSHClient != nil {
|
|
137
|
+
ms.SSHClient.Close()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Create new connection
|
|
141
|
+
sshClient, err := client.Connect(ms.User, ms.Host, ms.Port, ms.Password, ms.KeyPath)
|
|
142
|
+
if err != nil {
|
|
143
|
+
ms.mu.Lock()
|
|
144
|
+
ms.Status = "disconnected"
|
|
145
|
+
ms.mu.Unlock()
|
|
146
|
+
return nil, err
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
ms.mu.Lock()
|
|
150
|
+
ms.SSHClient = sshClient
|
|
151
|
+
ms.Status = "connected"
|
|
152
|
+
ms.mu.Unlock()
|
|
153
|
+
|
|
154
|
+
// Restore default ID if it was cleared
|
|
155
|
+
m.mu.Lock()
|
|
156
|
+
if m.defaultID == "" {
|
|
157
|
+
m.defaultID = sessionID
|
|
158
|
+
}
|
|
159
|
+
m.mu.Unlock()
|
|
160
|
+
|
|
161
|
+
// Restore forwards
|
|
162
|
+
m.restoreForwards(ms)
|
|
163
|
+
|
|
164
|
+
return toProtocolSession(ms), nil
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Exec executes a command on a session
|
|
168
|
+
func (m *Manager) Exec(sessionID, command string) (*protocol.ExecResult, error) {
|
|
169
|
+
m.mu.RLock()
|
|
170
|
+
var ms *ManagedSession
|
|
171
|
+
if sessionID != "" {
|
|
172
|
+
ms = m.sessions[sessionID]
|
|
173
|
+
} else if m.defaultID != "" {
|
|
174
|
+
ms = m.sessions[m.defaultID]
|
|
175
|
+
}
|
|
176
|
+
m.mu.RUnlock()
|
|
177
|
+
|
|
178
|
+
if ms == nil {
|
|
179
|
+
return nil, fmt.Errorf("session not found")
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
ms.mu.RLock()
|
|
183
|
+
defer ms.mu.RUnlock()
|
|
184
|
+
|
|
185
|
+
if ms.SSHClient == nil {
|
|
186
|
+
return nil, fmt.Errorf("session not connected")
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
stdout, stderr, exitCode, err := ms.SSHClient.Exec(command)
|
|
190
|
+
if err != nil && exitCode == 0 {
|
|
191
|
+
return nil, err
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
ms.LastCmd = command
|
|
195
|
+
|
|
196
|
+
return &protocol.ExecResult{
|
|
197
|
+
Stdout: stdout,
|
|
198
|
+
Stderr: stderr,
|
|
199
|
+
ExitCode: exitCode,
|
|
200
|
+
}, nil
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// List returns all sessions
|
|
204
|
+
func (m *Manager) List() []*protocol.Session {
|
|
205
|
+
m.mu.RLock()
|
|
206
|
+
defer m.mu.RUnlock()
|
|
207
|
+
|
|
208
|
+
result := make([]*protocol.Session, 0, len(m.sessions))
|
|
209
|
+
for _, s := range m.sessions {
|
|
210
|
+
result = append(result, toProtocolSession(s))
|
|
211
|
+
}
|
|
212
|
+
return result
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Use sets the default session
|
|
216
|
+
func (m *Manager) Use(sessionID string) error {
|
|
217
|
+
m.mu.Lock()
|
|
218
|
+
defer m.mu.Unlock()
|
|
219
|
+
|
|
220
|
+
if _, ok := m.sessions[sessionID]; !ok {
|
|
221
|
+
return fmt.Errorf("session not found")
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
m.defaultID = sessionID
|
|
225
|
+
return nil
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// GetDefaultID returns the default session ID
|
|
229
|
+
func (m *Manager) GetDefaultID() string {
|
|
230
|
+
m.mu.RLock()
|
|
231
|
+
defer m.mu.RUnlock()
|
|
232
|
+
return m.defaultID
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// AddForward adds a port forward
|
|
236
|
+
func (m *Manager) AddForward(sessionID, forwardType string, localPort, remotePort int) (*protocol.Forward, error) {
|
|
237
|
+
m.mu.RLock()
|
|
238
|
+
var ms *ManagedSession
|
|
239
|
+
if sessionID != "" {
|
|
240
|
+
ms = m.sessions[sessionID]
|
|
241
|
+
} else if m.defaultID != "" {
|
|
242
|
+
ms = m.sessions[m.defaultID]
|
|
243
|
+
}
|
|
244
|
+
m.mu.RUnlock()
|
|
245
|
+
|
|
246
|
+
if ms == nil {
|
|
247
|
+
return nil, fmt.Errorf("session not found")
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
ms.mu.RLock()
|
|
251
|
+
sshClient := ms.SSHClient
|
|
252
|
+
ms.mu.RUnlock()
|
|
253
|
+
|
|
254
|
+
if sshClient == nil {
|
|
255
|
+
return nil, fmt.Errorf("session not connected")
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
forwarder, err := portforward.NewForwarder(sshClient.Client, forwardType, localPort, remotePort)
|
|
259
|
+
if err != nil {
|
|
260
|
+
return nil, err
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
id := uuid.New().String()
|
|
264
|
+
forwarder.ID = id
|
|
265
|
+
|
|
266
|
+
m.forwardMu.Lock()
|
|
267
|
+
m.forwards[id] = forwarder
|
|
268
|
+
|
|
269
|
+
ms.mu.Lock()
|
|
270
|
+
ms.Forwards[id] = forwarder
|
|
271
|
+
ms.mu.Unlock()
|
|
272
|
+
m.forwardMu.Unlock()
|
|
273
|
+
|
|
274
|
+
go forwarder.Start()
|
|
275
|
+
|
|
276
|
+
return &protocol.Forward{
|
|
277
|
+
ID: id,
|
|
278
|
+
Type: forwardType,
|
|
279
|
+
Local: localPort,
|
|
280
|
+
Remote: remotePort,
|
|
281
|
+
}, nil
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ListForwards lists all forwards
|
|
285
|
+
func (m *Manager) ListForwards() []*protocol.Forward {
|
|
286
|
+
m.forwardMu.RLock()
|
|
287
|
+
defer m.forwardMu.RUnlock()
|
|
288
|
+
|
|
289
|
+
result := make([]*protocol.Forward, 0, len(m.forwards))
|
|
290
|
+
for _, f := range m.forwards {
|
|
291
|
+
result = append(result, &protocol.Forward{
|
|
292
|
+
ID: f.ID,
|
|
293
|
+
Type: f.Type,
|
|
294
|
+
Local: f.LocalPort,
|
|
295
|
+
Remote: f.RemotePort,
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
return result
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// CloseForward closes a forward
|
|
302
|
+
func (m *Manager) CloseForward(forwardID string) error {
|
|
303
|
+
m.forwardMu.Lock()
|
|
304
|
+
defer m.forwardMu.Unlock()
|
|
305
|
+
|
|
306
|
+
forwarder, ok := m.forwards[forwardID]
|
|
307
|
+
if !ok {
|
|
308
|
+
return fmt.Errorf("forward not found")
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
forwarder.Close()
|
|
312
|
+
delete(m.forwards, forwardID)
|
|
313
|
+
|
|
314
|
+
return nil
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// monitorReconnect monitors connection and auto-reconnects
|
|
318
|
+
func (m *Manager) monitorReconnect(ms *ManagedSession) {
|
|
319
|
+
ticker := time.NewTicker(5 * time.Second)
|
|
320
|
+
defer ticker.Stop()
|
|
321
|
+
|
|
322
|
+
for range ticker.C {
|
|
323
|
+
ms.mu.RLock()
|
|
324
|
+
status := ms.Status
|
|
325
|
+
sshClient := ms.SSHClient
|
|
326
|
+
ms.mu.RUnlock()
|
|
327
|
+
|
|
328
|
+
if status == "disconnected" {
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if sshClient == nil || sshClient.Client == nil {
|
|
333
|
+
ms.mu.Lock()
|
|
334
|
+
ms.Status = "reconnecting"
|
|
335
|
+
ms.mu.Unlock()
|
|
336
|
+
|
|
337
|
+
// Try to reconnect
|
|
338
|
+
newClient, err := client.Connect(ms.User, ms.Host, ms.Port, ms.Password, ms.KeyPath)
|
|
339
|
+
if err != nil {
|
|
340
|
+
continue
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
ms.mu.Lock()
|
|
344
|
+
ms.SSHClient = newClient
|
|
345
|
+
ms.Status = "connected"
|
|
346
|
+
ms.mu.Unlock()
|
|
347
|
+
|
|
348
|
+
// Restore forwards
|
|
349
|
+
m.restoreForwards(ms)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// restoreForwards restores all forwards after reconnection
|
|
355
|
+
func (m *Manager) restoreForwards(ms *ManagedSession) {
|
|
356
|
+
ms.mu.RLock()
|
|
357
|
+
forwards := make(map[string]*portforward.Forwarder)
|
|
358
|
+
for k, v := range ms.Forwards {
|
|
359
|
+
forwards[k] = v
|
|
360
|
+
}
|
|
361
|
+
ms.mu.RUnlock()
|
|
362
|
+
|
|
363
|
+
for _, f := range forwards {
|
|
364
|
+
f.Restart(ms.SSHClient.Client)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
func toProtocolSession(ms *ManagedSession) *protocol.Session {
|
|
369
|
+
ms.mu.RLock()
|
|
370
|
+
defer ms.mu.RUnlock()
|
|
371
|
+
|
|
372
|
+
return &protocol.Session{
|
|
373
|
+
ID: ms.ID,
|
|
374
|
+
Host: ms.Host,
|
|
375
|
+
User: ms.User,
|
|
376
|
+
Port: ms.Port,
|
|
377
|
+
Status: ms.Status,
|
|
378
|
+
KeyPath: ms.KeyPath,
|
|
379
|
+
}
|
|
380
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
package session
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"testing"
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
func TestNewManager(t *testing.T) {
|
|
8
|
+
manager := NewManager()
|
|
9
|
+
if manager == nil {
|
|
10
|
+
t.Error("NewManager returned nil")
|
|
11
|
+
}
|
|
12
|
+
if manager.sessions == nil {
|
|
13
|
+
t.Error("sessions map is nil")
|
|
14
|
+
}
|
|
15
|
+
if manager.forwards == nil {
|
|
16
|
+
t.Error("forwards map is nil")
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func TestManagerList(t *testing.T) {
|
|
21
|
+
manager := NewManager()
|
|
22
|
+
sessions := manager.List()
|
|
23
|
+
if len(sessions) != 0 {
|
|
24
|
+
t.Errorf("expected 0 sessions, got %d", len(sessions))
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func TestManagerUse(t *testing.T) {
|
|
29
|
+
manager := NewManager()
|
|
30
|
+
|
|
31
|
+
// Test using non-existent session
|
|
32
|
+
err := manager.Use("non-existent")
|
|
33
|
+
if err == nil {
|
|
34
|
+
t.Error("expected error for non-existent session")
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func TestManagerGetDefaultID(t *testing.T) {
|
|
39
|
+
manager := NewManager()
|
|
40
|
+
id := manager.GetDefaultID()
|
|
41
|
+
if id != "" {
|
|
42
|
+
t.Errorf("expected empty default ID, got %s", id)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func TestManagerListForwards(t *testing.T) {
|
|
47
|
+
manager := NewManager()
|
|
48
|
+
forwards := manager.ListForwards()
|
|
49
|
+
if len(forwards) != 0 {
|
|
50
|
+
t.Errorf("expected 0 forwards, got %d", len(forwards))
|
|
51
|
+
}
|
|
52
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gssh-agent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SSH Session Manager for Agents",
|
|
5
|
+
"bin": {
|
|
6
|
+
"gssh": "bin/gssh",
|
|
7
|
+
"gssh-daemon": "bin/gssh-daemon"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/forechoandlook/gssh.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"ssh",
|
|
15
|
+
"agent",
|
|
16
|
+
"session-manager",
|
|
17
|
+
"port-forwarding"
|
|
18
|
+
],
|
|
19
|
+
"author": "",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/forechoandlook/gssh/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/forechoandlook/gssh#readme"
|
|
25
|
+
}
|