heyvm 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.
Files changed (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +621 -0
  3. package/bin/file-browser +0 -0
  4. package/bin/heyvm +0 -0
  5. package/bin/heyvm-core +0 -0
  6. package/bin/heyvm.js +61 -0
  7. package/core/README.md +51 -0
  8. package/core/cmd/heyvm-core/main.go +73 -0
  9. package/core/go.mod +25 -0
  10. package/core/go.sum +47 -0
  11. package/core/internal/auth/errors.go +29 -0
  12. package/core/internal/auth/password.go +187 -0
  13. package/core/internal/auth/provider.go +33 -0
  14. package/core/internal/auth/ssh_key.go +142 -0
  15. package/core/internal/config/config.go +111 -0
  16. package/core/internal/ipc/actions.go +738 -0
  17. package/core/internal/ipc/handler.go +281 -0
  18. package/core/internal/ipc/protocol.go +126 -0
  19. package/core/internal/sftp/errors.go +29 -0
  20. package/core/internal/sftp/manager.go +303 -0
  21. package/core/internal/sftp/types.go +30 -0
  22. package/core/internal/ssh/errors.go +23 -0
  23. package/core/internal/ssh/manager.go +226 -0
  24. package/core/internal/ssh/session.go +105 -0
  25. package/core/internal/vm/errors.go +35 -0
  26. package/core/internal/vm/models.go +84 -0
  27. package/core/internal/vm/registry.go +240 -0
  28. package/package.json +59 -0
  29. package/scripts/install.js +100 -0
  30. package/ui/README.md +43 -0
  31. package/ui/dist/App.d.ts +3 -0
  32. package/ui/dist/App.d.ts.map +1 -0
  33. package/ui/dist/App.js +142 -0
  34. package/ui/dist/App.js.map +1 -0
  35. package/ui/dist/components/ConfirmDialog.d.ts +9 -0
  36. package/ui/dist/components/ConfirmDialog.d.ts.map +1 -0
  37. package/ui/dist/components/ConfirmDialog.js +22 -0
  38. package/ui/dist/components/ConfirmDialog.js.map +1 -0
  39. package/ui/dist/components/ErrorMessage.d.ts +8 -0
  40. package/ui/dist/components/ErrorMessage.d.ts.map +1 -0
  41. package/ui/dist/components/ErrorMessage.js +10 -0
  42. package/ui/dist/components/ErrorMessage.js.map +1 -0
  43. package/ui/dist/components/Header.d.ts +8 -0
  44. package/ui/dist/components/Header.d.ts.map +1 -0
  45. package/ui/dist/components/Header.js +10 -0
  46. package/ui/dist/components/Header.js.map +1 -0
  47. package/ui/dist/components/LoadingSpinner.d.ts +7 -0
  48. package/ui/dist/components/LoadingSpinner.d.ts.map +1 -0
  49. package/ui/dist/components/LoadingSpinner.js +7 -0
  50. package/ui/dist/components/LoadingSpinner.js.map +1 -0
  51. package/ui/dist/components/StatusBar.d.ts +11 -0
  52. package/ui/dist/components/StatusBar.d.ts.map +1 -0
  53. package/ui/dist/components/StatusBar.js +11 -0
  54. package/ui/dist/components/StatusBar.js.map +1 -0
  55. package/ui/dist/core/ipc.d.ts +96 -0
  56. package/ui/dist/core/ipc.d.ts.map +1 -0
  57. package/ui/dist/core/ipc.js +310 -0
  58. package/ui/dist/core/ipc.js.map +1 -0
  59. package/ui/dist/core/types.d.ts +45 -0
  60. package/ui/dist/core/types.d.ts.map +1 -0
  61. package/ui/dist/core/types.js +3 -0
  62. package/ui/dist/core/types.js.map +1 -0
  63. package/ui/dist/hooks/useFiles.d.ts +14 -0
  64. package/ui/dist/hooks/useFiles.d.ts.map +1 -0
  65. package/ui/dist/hooks/useFiles.js +102 -0
  66. package/ui/dist/hooks/useFiles.js.map +1 -0
  67. package/ui/dist/hooks/useVM.d.ts +10 -0
  68. package/ui/dist/hooks/useVM.d.ts.map +1 -0
  69. package/ui/dist/hooks/useVM.js +54 -0
  70. package/ui/dist/hooks/useVM.js.map +1 -0
  71. package/ui/dist/hooks/useVMList.d.ts +10 -0
  72. package/ui/dist/hooks/useVMList.d.ts.map +1 -0
  73. package/ui/dist/hooks/useVMList.js +56 -0
  74. package/ui/dist/hooks/useVMList.js.map +1 -0
  75. package/ui/dist/index.d.ts +3 -0
  76. package/ui/dist/index.d.ts.map +1 -0
  77. package/ui/dist/index.js +7488 -0
  78. package/ui/dist/index.js.map +1 -0
  79. package/ui/dist/keybindings.d.ts +146 -0
  80. package/ui/dist/keybindings.d.ts.map +1 -0
  81. package/ui/dist/keybindings.js +96 -0
  82. package/ui/dist/keybindings.js.map +1 -0
  83. package/ui/dist/screens/AddVMScreen.d.ts +9 -0
  84. package/ui/dist/screens/AddVMScreen.d.ts.map +1 -0
  85. package/ui/dist/screens/AddVMScreen.js +163 -0
  86. package/ui/dist/screens/AddVMScreen.js.map +1 -0
  87. package/ui/dist/screens/VMDetailScreen.d.ts +12 -0
  88. package/ui/dist/screens/VMDetailScreen.d.ts.map +1 -0
  89. package/ui/dist/screens/VMDetailScreen.js +96 -0
  90. package/ui/dist/screens/VMDetailScreen.js.map +1 -0
  91. package/ui/dist/screens/VMListScreen.d.ts +12 -0
  92. package/ui/dist/screens/VMListScreen.d.ts.map +1 -0
  93. package/ui/dist/screens/VMListScreen.js +158 -0
  94. package/ui/dist/screens/VMListScreen.js.map +1 -0
  95. package/ui/dist/screens/tabs/FilesTab.d.ts +9 -0
  96. package/ui/dist/screens/tabs/FilesTab.d.ts.map +1 -0
  97. package/ui/dist/screens/tabs/FilesTab.js +374 -0
  98. package/ui/dist/screens/tabs/FilesTab.js.map +1 -0
  99. package/ui/dist/screens/tabs/OverviewTab.d.ts +10 -0
  100. package/ui/dist/screens/tabs/OverviewTab.d.ts.map +1 -0
  101. package/ui/dist/screens/tabs/OverviewTab.js +110 -0
  102. package/ui/dist/screens/tabs/OverviewTab.js.map +1 -0
  103. package/ui/dist/screens/tabs/TerminalTab.d.ts +9 -0
  104. package/ui/dist/screens/tabs/TerminalTab.d.ts.map +1 -0
  105. package/ui/dist/screens/tabs/TerminalTab.js +270 -0
  106. package/ui/dist/screens/tabs/TerminalTab.js.map +1 -0
  107. package/ui/dist/utils/terminalEmulator.d.ts +49 -0
  108. package/ui/dist/utils/terminalEmulator.d.ts.map +1 -0
  109. package/ui/dist/utils/terminalEmulator.js +88 -0
  110. package/ui/dist/utils/terminalEmulator.js.map +1 -0
  111. package/ui/package.json +34 -0
  112. package/ui/scripts/start-with-core.js +81 -0
@@ -0,0 +1,738 @@
1
+ package ipc
2
+
3
+ import (
4
+ "encoding/base64"
5
+ "fmt"
6
+ "io"
7
+ "log"
8
+
9
+ "github.com/adishm/heyvm/internal/auth"
10
+ "github.com/adishm/heyvm/internal/sftp"
11
+ "github.com/adishm/heyvm/internal/vm"
12
+ )
13
+
14
+ // handleListVMs returns all VMs in the registry
15
+ func (h *Handler) handleListVMs(params map[string]interface{}) Response {
16
+ vms := h.registry.List()
17
+ return NewSuccessResponse(vms)
18
+ }
19
+
20
+ // handleAddVM adds a new VM to the registry
21
+ func (h *Handler) handleAddVM(params map[string]interface{}) Response {
22
+ // Parse VM from params
23
+ vmData, ok := params["vm"].(map[string]interface{})
24
+ if !ok {
25
+ return NewErrorResponseWithMessage("invalid VM data")
26
+ }
27
+
28
+ // Extract fields
29
+ name, _ := vmData["name"].(string)
30
+ host, _ := vmData["host"].(string)
31
+ port, _ := vmData["port"].(float64) // JSON numbers are float64
32
+ username, _ := vmData["username"].(string)
33
+
34
+ if port == 0 {
35
+ port = 22 // Default SSH port
36
+ }
37
+
38
+ // Extract auth config
39
+ authData, ok := vmData["auth"].(map[string]interface{})
40
+ if !ok {
41
+ return NewErrorResponseWithMessage("invalid auth configuration")
42
+ }
43
+
44
+ authTypeStr, _ := authData["type"].(string)
45
+ keyPath, _ := authData["keyPath"].(string)
46
+ rememberPassword, _ := authData["rememberPassword"].(bool)
47
+
48
+ var authType vm.AuthType
49
+ switch authTypeStr {
50
+ case "key":
51
+ authType = vm.AuthTypeKey
52
+ case "password":
53
+ authType = vm.AuthTypePassword
54
+ default:
55
+ return NewErrorResponseWithMessage("invalid auth type")
56
+ }
57
+
58
+ // Create VM
59
+ newVM := &vm.VM{
60
+ Name: name,
61
+ Host: host,
62
+ Port: int(port),
63
+ Username: username,
64
+ Auth: vm.AuthConfig{
65
+ Type: authType,
66
+ KeyPath: keyPath,
67
+ RememberPassword: rememberPassword,
68
+ },
69
+ Status: vm.VMStatusDisconnected,
70
+ }
71
+
72
+ // Add to registry
73
+ if err := h.registry.Add(newVM); err != nil {
74
+ return NewErrorResponse(err)
75
+ }
76
+
77
+ // Save registry
78
+ if err := h.registry.Save(); err != nil {
79
+ log.Printf("Warning: failed to save registry: %v", err)
80
+ }
81
+
82
+ return NewSuccessResponseWithMessage("VM added successfully", newVM)
83
+ }
84
+
85
+ // handleRemoveVM removes a VM from the registry
86
+ func (h *Handler) handleRemoveVM(params map[string]interface{}) Response {
87
+ vmID, ok := params["vm_id"].(string)
88
+ if !ok {
89
+ return NewErrorResponseWithMessage("vm_id parameter required")
90
+ }
91
+
92
+ // Disconnect if connected
93
+ h.mu.Lock()
94
+ if sshMgr, exists := h.sshManagers[vmID]; exists {
95
+ sshMgr.Disconnect()
96
+ delete(h.sshManagers, vmID)
97
+ }
98
+ if sftpMgr, exists := h.sftpManagers[vmID]; exists {
99
+ sftpMgr.Close()
100
+ delete(h.sftpManagers, vmID)
101
+ }
102
+ if provider, exists := h.authProviders[vmID]; exists {
103
+ provider.Disconnect()
104
+ delete(h.authProviders, vmID)
105
+ }
106
+ h.mu.Unlock()
107
+
108
+ // Remove from registry
109
+ if err := h.registry.Remove(vmID); err != nil {
110
+ return NewErrorResponse(err)
111
+ }
112
+
113
+ // Save registry
114
+ if err := h.registry.Save(); err != nil {
115
+ log.Printf("Warning: failed to save registry: %v", err)
116
+ }
117
+
118
+ return NewSuccessResponseWithMessage("VM removed successfully", nil)
119
+ }
120
+
121
+ // handleConnectVM establishes SSH connection to a VM
122
+ func (h *Handler) handleConnectVM(params map[string]interface{}) Response {
123
+ vmID, ok := params["vm_id"].(string)
124
+ if !ok {
125
+ return NewErrorResponseWithMessage("vm_id parameter required")
126
+ }
127
+
128
+ // Get SSH manager
129
+ sshMgr, err := h.getSSHManager(vmID)
130
+ if err != nil {
131
+ return NewErrorResponse(err)
132
+ }
133
+
134
+ // Check if already connected (idempotent)
135
+ if sshMgr.IsConnected() {
136
+ // Already connected - just ensure status is correct
137
+ if err := h.registry.UpdateStatus(vmID, vm.VMStatusConnected); err != nil {
138
+ log.Printf("Warning: failed to update VM status: %v", err)
139
+ }
140
+ if err := h.registry.Save(); err != nil {
141
+ log.Printf("Warning: failed to save registry: %v", err)
142
+ }
143
+ return NewSuccessResponseWithMessage("Already connected", nil)
144
+ }
145
+
146
+ // Set status to connecting
147
+ if err := h.registry.UpdateStatus(vmID, vm.VMStatusConnecting); err != nil {
148
+ log.Printf("Warning: failed to update VM status: %v", err)
149
+ }
150
+ if err := h.registry.Save(); err != nil {
151
+ log.Printf("Warning: failed to save registry: %v", err)
152
+ }
153
+
154
+ // Connect (this is now idempotent)
155
+ if err := sshMgr.Connect(); err != nil {
156
+ h.registry.UpdateStatus(vmID, vm.VMStatusError)
157
+ h.registry.Save()
158
+ return NewErrorResponse(err)
159
+ }
160
+
161
+ // Update status to connected
162
+ if err := h.registry.UpdateStatus(vmID, vm.VMStatusConnected); err != nil {
163
+ log.Printf("Warning: failed to update VM status: %v", err)
164
+ }
165
+
166
+ // Save registry
167
+ if err := h.registry.Save(); err != nil {
168
+ log.Printf("Warning: failed to save registry: %v", err)
169
+ }
170
+
171
+ return NewSuccessResponseWithMessage("Connected successfully", nil)
172
+ }
173
+
174
+ // handleDisconnectVM closes SSH connection to a VM
175
+ func (h *Handler) handleDisconnectVM(params map[string]interface{}) Response {
176
+ vmID, ok := params["vm_id"].(string)
177
+ if !ok {
178
+ return NewErrorResponseWithMessage("vm_id parameter required")
179
+ }
180
+
181
+ h.mu.Lock()
182
+
183
+ // Close SFTP if exists
184
+ if sftpMgr, exists := h.sftpManagers[vmID]; exists {
185
+ sftpMgr.Close()
186
+ delete(h.sftpManagers, vmID)
187
+ }
188
+
189
+ // Disconnect SSH if exists
190
+ if sshMgr, exists := h.sshManagers[vmID]; exists {
191
+ if err := sshMgr.Disconnect(); err != nil {
192
+ h.mu.Unlock()
193
+ return NewErrorResponse(err)
194
+ }
195
+ delete(h.sshManagers, vmID)
196
+ }
197
+
198
+ h.mu.Unlock()
199
+
200
+ // Update status
201
+ if err := h.registry.UpdateStatus(vmID, vm.VMStatusDisconnected); err != nil {
202
+ log.Printf("Warning: failed to update VM status: %v", err)
203
+ }
204
+
205
+ // Save registry
206
+ if err := h.registry.Save(); err != nil {
207
+ log.Printf("Warning: failed to save registry: %v", err)
208
+ }
209
+
210
+ return NewSuccessResponseWithMessage("Disconnected successfully", nil)
211
+ }
212
+
213
+ // handleExecuteCommand executes a command on a VM
214
+ func (h *Handler) handleExecuteCommand(params map[string]interface{}) Response {
215
+ vmID, ok := params["vm_id"].(string)
216
+ if !ok {
217
+ return NewErrorResponseWithMessage("vm_id parameter required")
218
+ }
219
+
220
+ command, ok := params["command"].(string)
221
+ if !ok {
222
+ return NewErrorResponseWithMessage("command parameter required")
223
+ }
224
+
225
+ // Get SSH manager
226
+ sshMgr, err := h.getSSHManager(vmID)
227
+ if err != nil {
228
+ return NewErrorResponse(err)
229
+ }
230
+
231
+ // Ensure connected
232
+ if !sshMgr.IsConnected() {
233
+ if err := sshMgr.Connect(); err != nil {
234
+ return NewErrorResponse(err)
235
+ }
236
+ }
237
+
238
+ // Execute command
239
+ output, err := sshMgr.ExecuteCommand(command)
240
+ if err != nil {
241
+ return NewErrorResponse(err)
242
+ }
243
+
244
+ return NewSuccessResponse(map[string]interface{}{
245
+ "output": output,
246
+ })
247
+ }
248
+
249
+ // handleListFiles lists files in a directory on a VM
250
+ func (h *Handler) handleListFiles(params map[string]interface{}) Response {
251
+ vmID, ok := params["vm_id"].(string)
252
+ if !ok {
253
+ return NewErrorResponseWithMessage("vm_id parameter required")
254
+ }
255
+
256
+ path, ok := params["path"].(string)
257
+ if !ok {
258
+ path = "/" // Default to root
259
+ }
260
+
261
+ // Get SFTP manager
262
+ sftpMgr, err := h.getSFTPManager(vmID)
263
+ if err != nil {
264
+ return NewErrorResponse(err)
265
+ }
266
+
267
+ // List files
268
+ files, err := sftpMgr.List(path)
269
+ if err != nil {
270
+ return NewErrorResponse(err)
271
+ }
272
+
273
+ return NewSuccessResponse(files)
274
+ }
275
+
276
+ // handleUploadFile uploads a file to a VM
277
+ func (h *Handler) handleUploadFile(params map[string]interface{}) Response {
278
+ vmID, ok := params["vm_id"].(string)
279
+ if !ok {
280
+ return NewErrorResponseWithMessage("vm_id parameter required")
281
+ }
282
+
283
+ localPath, ok := params["local_path"].(string)
284
+ if !ok {
285
+ return NewErrorResponseWithMessage("local_path parameter required")
286
+ }
287
+
288
+ remotePath, ok := params["remote_path"].(string)
289
+ if !ok {
290
+ return NewErrorResponseWithMessage("remote_path parameter required")
291
+ }
292
+
293
+ // Get SFTP manager
294
+ sftpMgr, err := h.getSFTPManager(vmID)
295
+ if err != nil {
296
+ return NewErrorResponse(err)
297
+ }
298
+
299
+ // Upload file with progress reporting
300
+ opts := &sftp.TransferOptions{
301
+ Overwrite: true,
302
+ PreservePermissions: true,
303
+ OnProgress: func(bytesTransferred, totalBytes int64) {
304
+ // Emit progress event to UI
305
+ h.Emit(EventTransferProgress, map[string]interface{}{
306
+ "vm_id": vmID,
307
+ "file": remotePath,
308
+ "direction": "upload",
309
+ "bytes_transferred": bytesTransferred,
310
+ "total_bytes": totalBytes,
311
+ "percent": float64(bytesTransferred) / float64(totalBytes) * 100,
312
+ })
313
+ },
314
+ }
315
+
316
+ if err := sftpMgr.Upload(localPath, remotePath, opts); err != nil {
317
+ return NewErrorResponse(err)
318
+ }
319
+
320
+ return NewSuccessResponseWithMessage("File uploaded successfully", nil)
321
+ }
322
+
323
+ // handleDownloadFile downloads a file from a VM
324
+ func (h *Handler) handleDownloadFile(params map[string]interface{}) Response {
325
+ vmID, ok := params["vm_id"].(string)
326
+ if !ok {
327
+ return NewErrorResponseWithMessage("vm_id parameter required")
328
+ }
329
+
330
+ remotePath, ok := params["remote_path"].(string)
331
+ if !ok {
332
+ return NewErrorResponseWithMessage("remote_path parameter required")
333
+ }
334
+
335
+ localPath, ok := params["local_path"].(string)
336
+ if !ok {
337
+ return NewErrorResponseWithMessage("local_path parameter required")
338
+ }
339
+
340
+ // Get SFTP manager
341
+ sftpMgr, err := h.getSFTPManager(vmID)
342
+ if err != nil {
343
+ return NewErrorResponse(err)
344
+ }
345
+
346
+ // Download file with progress reporting
347
+ opts := &sftp.TransferOptions{
348
+ Overwrite: true,
349
+ PreservePermissions: true,
350
+ OnProgress: func(bytesTransferred, totalBytes int64) {
351
+ // Emit progress event to UI
352
+ h.Emit(EventTransferProgress, map[string]interface{}{
353
+ "vm_id": vmID,
354
+ "file": remotePath,
355
+ "direction": "download",
356
+ "bytes_transferred": bytesTransferred,
357
+ "total_bytes": totalBytes,
358
+ "percent": float64(bytesTransferred) / float64(totalBytes) * 100,
359
+ })
360
+ },
361
+ }
362
+
363
+ if err := sftpMgr.Download(remotePath, localPath, opts); err != nil {
364
+ return NewErrorResponse(err)
365
+ }
366
+
367
+ return NewSuccessResponseWithMessage("File downloaded successfully", nil)
368
+ }
369
+
370
+ // handleDeleteFile deletes a file on a VM
371
+ func (h *Handler) handleDeleteFile(params map[string]interface{}) Response {
372
+ vmID, ok := params["vm_id"].(string)
373
+ if !ok {
374
+ return NewErrorResponseWithMessage("vm_id parameter required")
375
+ }
376
+
377
+ path, ok := params["path"].(string)
378
+ if !ok {
379
+ return NewErrorResponseWithMessage("path parameter required")
380
+ }
381
+
382
+ // Get SFTP manager
383
+ sftpMgr, err := h.getSFTPManager(vmID)
384
+ if err != nil {
385
+ return NewErrorResponse(err)
386
+ }
387
+
388
+ // Delete file
389
+ if err := sftpMgr.Delete(path); err != nil {
390
+ return NewErrorResponse(err)
391
+ }
392
+
393
+ return NewSuccessResponseWithMessage("File deleted successfully", nil)
394
+ }
395
+
396
+ // handleRenameFile renames/moves a file on a VM
397
+ func (h *Handler) handleRenameFile(params map[string]interface{}) Response {
398
+ vmID, ok := params["vm_id"].(string)
399
+ if !ok {
400
+ return NewErrorResponseWithMessage("vm_id parameter required")
401
+ }
402
+
403
+ oldPath, ok := params["old_path"].(string)
404
+ if !ok {
405
+ return NewErrorResponseWithMessage("old_path parameter required")
406
+ }
407
+
408
+ newPath, ok := params["new_path"].(string)
409
+ if !ok {
410
+ return NewErrorResponseWithMessage("new_path parameter required")
411
+ }
412
+
413
+ // Get SFTP manager
414
+ sftpMgr, err := h.getSFTPManager(vmID)
415
+ if err != nil {
416
+ return NewErrorResponse(err)
417
+ }
418
+
419
+ // Rename file
420
+ if err := sftpMgr.Rename(oldPath, newPath); err != nil {
421
+ return NewErrorResponse(err)
422
+ }
423
+
424
+ return NewSuccessResponseWithMessage("File renamed successfully", nil)
425
+ }
426
+
427
+ // handleStorePassword stores a password in the keychain
428
+ func (h *Handler) handleStorePassword(params map[string]interface{}) Response {
429
+ vmID, ok := params["vm_id"].(string)
430
+ if !ok {
431
+ return NewErrorResponseWithMessage("vm_id parameter required")
432
+ }
433
+
434
+ password, ok := params["password"].(string)
435
+ if !ok {
436
+ return NewErrorResponseWithMessage("password parameter required")
437
+ }
438
+
439
+ // Create password auth provider
440
+ passwordAuth, err := auth.NewPasswordAuth()
441
+ if err != nil {
442
+ return NewErrorResponse(err)
443
+ }
444
+
445
+ // Store password
446
+ if err := passwordAuth.StorePassword(vmID, password); err != nil {
447
+ return NewErrorResponse(err)
448
+ }
449
+
450
+ return NewSuccessResponseWithMessage("Password stored successfully", nil)
451
+ }
452
+
453
+ // handleTestConnection tests SSH connection to a VM without saving it
454
+ func (h *Handler) handleTestConnection(params map[string]interface{}) Response {
455
+ // Parse VM from params (similar to handleAddVM)
456
+ vmData, ok := params["vm"].(map[string]interface{})
457
+ if !ok {
458
+ return NewErrorResponseWithMessage("invalid VM data")
459
+ }
460
+
461
+ name, _ := vmData["name"].(string)
462
+ host, _ := vmData["host"].(string)
463
+ port, _ := vmData["port"].(float64)
464
+ username, _ := vmData["username"].(string)
465
+
466
+ if port == 0 {
467
+ port = 22
468
+ }
469
+
470
+ authData, ok := vmData["auth"].(map[string]interface{})
471
+ if !ok {
472
+ return NewErrorResponseWithMessage("invalid auth configuration")
473
+ }
474
+
475
+ authTypeStr, _ := authData["type"].(string)
476
+ keyPath, _ := authData["keyPath"].(string)
477
+
478
+ // Get temporary password from params (for testing only)
479
+ tempPassword, _ := params["password"].(string)
480
+
481
+ var authType vm.AuthType
482
+ switch authTypeStr {
483
+ case "key":
484
+ authType = vm.AuthTypeKey
485
+ case "password":
486
+ authType = vm.AuthTypePassword
487
+ default:
488
+ return NewErrorResponseWithMessage("invalid auth type")
489
+ }
490
+
491
+ // Create temporary VM
492
+ testVM := &vm.VM{
493
+ ID: "test",
494
+ Name: name,
495
+ Host: host,
496
+ Port: int(port),
497
+ Username: username,
498
+ Auth: vm.AuthConfig{
499
+ Type: authType,
500
+ KeyPath: keyPath,
501
+ },
502
+ }
503
+
504
+ // Create auth provider with temporary password if provided
505
+ var provider auth.Provider
506
+ var err error
507
+
508
+ if authType == vm.AuthTypePassword && tempPassword != "" {
509
+ // Use temporary password for testing
510
+ provider, err = auth.NewPasswordAuthWithTemp(tempPassword)
511
+ } else {
512
+ provider, err = auth.NewProvider(testVM)
513
+ }
514
+
515
+ if err != nil {
516
+ return NewErrorResponse(err)
517
+ }
518
+ defer provider.Disconnect()
519
+
520
+ // Test connection
521
+ client, err := provider.Connect(testVM)
522
+ if err != nil {
523
+ return NewErrorResponse(fmt.Errorf("connection test failed: %w", err))
524
+ }
525
+ defer client.Close()
526
+
527
+ return NewSuccessResponseWithMessage("Connection test successful", nil)
528
+ }
529
+
530
+ // handleStartPTY starts an interactive PTY session
531
+ func (h *Handler) handleStartPTY(params map[string]interface{}) Response {
532
+ vmID, ok := params["vm_id"].(string)
533
+ if !ok {
534
+ return NewErrorResponseWithMessage("vm_id parameter required")
535
+ }
536
+
537
+ rows, ok := params["rows"].(float64)
538
+ if !ok {
539
+ rows = 24 // Default rows
540
+ }
541
+
542
+ cols, ok := params["cols"].(float64)
543
+ if !ok {
544
+ cols = 80 // Default cols
545
+ }
546
+
547
+ // Get SSH manager
548
+ sshMgr, err := h.getSSHManager(vmID)
549
+ if err != nil {
550
+ h.Emit(EventPTYError, map[string]interface{}{
551
+ "vm_id": vmID,
552
+ "error": err.Error(),
553
+ })
554
+ return NewErrorResponse(err)
555
+ }
556
+
557
+ // Ensure connected
558
+ if !sshMgr.IsConnected() {
559
+ if err := sshMgr.Connect(); err != nil {
560
+ h.Emit(EventPTYError, map[string]interface{}{
561
+ "vm_id": vmID,
562
+ "error": err.Error(),
563
+ })
564
+ return NewErrorResponse(err)
565
+ }
566
+ }
567
+
568
+ // Start PTY session
569
+ session, err := sshMgr.StartPTY(int(rows), int(cols))
570
+ if err != nil {
571
+ h.Emit(EventPTYError, map[string]interface{}{
572
+ "vm_id": vmID,
573
+ "error": err.Error(),
574
+ })
575
+ return NewErrorResponse(err)
576
+ }
577
+
578
+ sessionID := session.ID()
579
+
580
+ // Emit PTY_READY event
581
+ h.Emit(EventPTYReady, map[string]interface{}{
582
+ "vm_id": vmID,
583
+ "session_id": sessionID,
584
+ })
585
+
586
+ // Start THE ONLY goroutine that reads from combined stdout+stderr
587
+ // This keeps stdin alive and ensures all output is captured
588
+ go func() {
589
+ combined := session.CombinedOutput()
590
+ buf := make([]byte, 4096)
591
+ for {
592
+ // Blocking read from combined output - this is the only reader
593
+ n, err := combined.Read(buf)
594
+ if n > 0 {
595
+ // Base64 encode PTY output to keep JSON safe
596
+ encoded := base64.StdEncoding.EncodeToString(buf[:n])
597
+ // Emit PTY_OUTPUT event with base64-encoded data
598
+ h.Emit(EventPTYOutput, map[string]interface{}{
599
+ "session_id": sessionID,
600
+ "data": encoded,
601
+ })
602
+ }
603
+ if err != nil {
604
+ if err == io.EOF {
605
+ log.Printf("PTY session %s: combined output EOF (shell exited)", sessionID)
606
+ } else {
607
+ log.Printf("PTY session %s: read error: %v", sessionID, err)
608
+ }
609
+ // Emit PTY_EXIT event
610
+ h.Emit(EventPTYExit, map[string]interface{}{
611
+ "session_id": sessionID,
612
+ })
613
+ break
614
+ }
615
+ }
616
+ }()
617
+
618
+ // No response needed - events are emitted instead
619
+ return Response{}
620
+ }
621
+
622
+ // handleWriteToPTY writes data to a PTY session (fire-and-forget)
623
+ func (h *Handler) handleWriteToPTY(params map[string]interface{}) Response {
624
+ vmID, ok := params["vm_id"].(string)
625
+ if !ok {
626
+ return NewErrorResponseWithMessage("vm_id parameter required")
627
+ }
628
+
629
+ sessionID, ok := params["session_id"].(string)
630
+ if !ok {
631
+ return NewErrorResponseWithMessage("session_id parameter required")
632
+ }
633
+
634
+ data, ok := params["data"].(string)
635
+ if !ok {
636
+ return NewErrorResponseWithMessage("data parameter required")
637
+ }
638
+
639
+ // Get SSH manager
640
+ sshMgr, err := h.getSSHManager(vmID)
641
+ if err != nil {
642
+ return NewErrorResponse(err)
643
+ }
644
+
645
+ // Get session
646
+ session, err := sshMgr.GetSession(sessionID)
647
+ if err != nil {
648
+ return NewErrorResponse(err)
649
+ }
650
+
651
+ // Write data (non-blocking from IPC perspective - fire and forget)
652
+ go func() {
653
+ _, err := session.Write([]byte(data))
654
+ if err != nil {
655
+ log.Printf("PTY session %s: write error: %v", sessionID, err)
656
+ }
657
+ }()
658
+
659
+ // No response needed
660
+ return Response{}
661
+ }
662
+
663
+ // handleResizePTY resizes a PTY session
664
+ func (h *Handler) handleResizePTY(params map[string]interface{}) Response {
665
+ vmID, ok := params["vm_id"].(string)
666
+ if !ok {
667
+ log.Printf("PTY resize: vm_id parameter required")
668
+ return Response{}
669
+ }
670
+
671
+ sessionID, ok := params["session_id"].(string)
672
+ if !ok {
673
+ log.Printf("PTY resize: session_id parameter required")
674
+ return Response{}
675
+ }
676
+
677
+ rows, ok := params["rows"].(float64)
678
+ if !ok {
679
+ log.Printf("PTY resize: rows parameter required")
680
+ return Response{}
681
+ }
682
+
683
+ cols, ok := params["cols"].(float64)
684
+ if !ok {
685
+ log.Printf("PTY resize: cols parameter required")
686
+ return Response{}
687
+ }
688
+
689
+ // Get SSH manager
690
+ sshMgr, err := h.getSSHManager(vmID)
691
+ if err != nil {
692
+ log.Printf("PTY resize error for VM %s: %v", vmID, err)
693
+ return Response{}
694
+ }
695
+
696
+ // Get session
697
+ session, err := sshMgr.GetSession(sessionID)
698
+ if err != nil {
699
+ log.Printf("PTY resize error getting session %s: %v", sessionID, err)
700
+ return Response{}
701
+ }
702
+
703
+ // Resize
704
+ if err := session.Resize(int(rows), int(cols)); err != nil {
705
+ log.Printf("PTY resize error for session %s: %v", sessionID, err)
706
+ }
707
+
708
+ // No response needed
709
+ return Response{}
710
+ }
711
+
712
+ // handleClosePTY closes a PTY session
713
+ func (h *Handler) handleClosePTY(params map[string]interface{}) Response {
714
+ vmID, ok := params["vm_id"].(string)
715
+ if !ok {
716
+ return NewErrorResponseWithMessage("vm_id parameter required")
717
+ }
718
+
719
+ sessionID, ok := params["session_id"].(string)
720
+ if !ok {
721
+ return NewErrorResponseWithMessage("session_id parameter required")
722
+ }
723
+
724
+ // Get SSH manager
725
+ sshMgr, err := h.getSSHManager(vmID)
726
+ if err != nil {
727
+ log.Printf("PTY close error for VM %s: %v", vmID, err)
728
+ return Response{}
729
+ }
730
+
731
+ // Close session
732
+ if err := sshMgr.CloseSession(sessionID); err != nil {
733
+ log.Printf("PTY close error for session %s: %v", sessionID, err)
734
+ }
735
+
736
+ // No response needed
737
+ return Response{}
738
+ }