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,281 @@
1
+ package ipc
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "io"
7
+ "log"
8
+ "os"
9
+ "sync"
10
+
11
+ "github.com/adishm/heyvm/internal/auth"
12
+ "github.com/adishm/heyvm/internal/sftp"
13
+ "github.com/adishm/heyvm/internal/ssh"
14
+ "github.com/adishm/heyvm/internal/vm"
15
+ )
16
+
17
+ // Handler manages IPC communication and VM operations
18
+ type Handler struct {
19
+ registry *vm.Registry
20
+ sshManagers map[string]*ssh.Manager // vmID -> SSH manager
21
+ sftpManagers map[string]*sftp.Manager // vmID -> SFTP manager
22
+ authProviders map[string]auth.Provider // vmID -> auth provider
23
+ mu sync.RWMutex
24
+ input io.Reader
25
+ output io.Writer
26
+ outputMu sync.Mutex // Protect output writes
27
+ }
28
+
29
+ // NewHandler creates a new IPC handler
30
+ func NewHandler(registry *vm.Registry) *Handler {
31
+ return &Handler{
32
+ registry: registry,
33
+ sshManagers: make(map[string]*ssh.Manager),
34
+ sftpManagers: make(map[string]*sftp.Manager),
35
+ authProviders: make(map[string]auth.Provider),
36
+ input: os.Stdin,
37
+ output: os.Stdout,
38
+ }
39
+ }
40
+
41
+ // Emit sends an event to the UI (non-blocking, fire-and-forget)
42
+ func (h *Handler) Emit(event string, data interface{}) {
43
+ h.outputMu.Lock()
44
+ defer h.outputMu.Unlock()
45
+
46
+ encoder := json.NewEncoder(h.output)
47
+ response := NewEvent(event, data)
48
+
49
+ if err := encoder.Encode(response); err != nil {
50
+ log.Printf("IPC handler: error emitting event %s: %v", event, err)
51
+ }
52
+ }
53
+
54
+ // Start begins the IPC request/response loop
55
+ func (h *Handler) Start() error {
56
+ decoder := json.NewDecoder(h.input)
57
+ encoder := json.NewEncoder(h.output)
58
+
59
+ log.Println("IPC handler started, listening for requests...")
60
+
61
+ for {
62
+ var req Request
63
+ if err := decoder.Decode(&req); err != nil {
64
+ if err == io.EOF {
65
+ log.Println("IPC handler: received EOF, shutting down")
66
+ break
67
+ }
68
+ log.Printf("IPC handler: error decoding request: %v", err)
69
+ continue
70
+ }
71
+
72
+ log.Printf("IPC handler: received request: %s", req.Action)
73
+
74
+ // Handle request
75
+ resp := h.HandleRequest(req)
76
+
77
+ // Skip sending response for fire-and-forget actions (empty response)
78
+ if resp.Status == "" {
79
+ log.Printf("IPC handler: fire-and-forget action, no response sent")
80
+ continue
81
+ }
82
+
83
+ // Send response
84
+ h.outputMu.Lock()
85
+ if err := encoder.Encode(resp); err != nil {
86
+ log.Printf("IPC handler: error encoding response: %v", err)
87
+ h.outputMu.Unlock()
88
+ continue
89
+ }
90
+ h.outputMu.Unlock()
91
+
92
+ log.Printf("IPC handler: sent response: status=%s", resp.Status)
93
+ }
94
+
95
+ // Cleanup on shutdown
96
+ h.cleanup()
97
+ return nil
98
+ }
99
+
100
+ // HandleRequest routes a request to the appropriate action handler
101
+ func (h *Handler) HandleRequest(req Request) Response {
102
+ var resp Response
103
+
104
+ switch req.Action {
105
+ case ActionListVMs:
106
+ resp = h.handleListVMs(req.Params)
107
+ case ActionAddVM:
108
+ resp = h.handleAddVM(req.Params)
109
+ case ActionRemoveVM:
110
+ resp = h.handleRemoveVM(req.Params)
111
+ case ActionConnectVM:
112
+ resp = h.handleConnectVM(req.Params)
113
+ case ActionDisconnectVM:
114
+ resp = h.handleDisconnectVM(req.Params)
115
+ case ActionExecuteCommand:
116
+ resp = h.handleExecuteCommand(req.Params)
117
+ case ActionListFiles:
118
+ resp = h.handleListFiles(req.Params)
119
+ case ActionUploadFile:
120
+ resp = h.handleUploadFile(req.Params)
121
+ case ActionDownloadFile:
122
+ resp = h.handleDownloadFile(req.Params)
123
+ case ActionDeleteFile:
124
+ resp = h.handleDeleteFile(req.Params)
125
+ case ActionRenameFile:
126
+ resp = h.handleRenameFile(req.Params)
127
+ case ActionStorePassword:
128
+ resp = h.handleStorePassword(req.Params)
129
+ case ActionTestConnection:
130
+ resp = h.handleTestConnection(req.Params)
131
+ case ActionStartPTY:
132
+ // Fire-and-forget: handle but don't send response
133
+ go h.handleStartPTY(req.Params)
134
+ return Response{} // Return empty response (won't be sent)
135
+ case ActionWriteToPTY:
136
+ // Fire-and-forget: handle but don't send response
137
+ go h.handleWriteToPTY(req.Params)
138
+ return Response{} // Return empty response (won't be sent)
139
+ case ActionResizePTY:
140
+ // Fire-and-forget: handle but don't send response
141
+ go h.handleResizePTY(req.Params)
142
+ return Response{} // Return empty response (won't be sent)
143
+ case ActionClosePTY:
144
+ // Fire-and-forget: handle but don't send response
145
+ go h.handleClosePTY(req.Params)
146
+ return Response{} // Return empty response (won't be sent)
147
+ default:
148
+ resp = NewErrorResponseWithMessage(fmt.Sprintf("unknown action: %s", req.Action))
149
+ }
150
+
151
+ // Add request ID to response for proper matching
152
+ resp.RequestID = req.RequestID
153
+ return resp
154
+ }
155
+
156
+ // cleanup closes all active connections
157
+ func (h *Handler) cleanup() {
158
+ log.Println("IPC handler: cleaning up connections...")
159
+
160
+ h.mu.Lock()
161
+ defer h.mu.Unlock()
162
+
163
+ // Close all SFTP managers
164
+ for vmID, sftpMgr := range h.sftpManagers {
165
+ if err := sftpMgr.Close(); err != nil {
166
+ log.Printf("Error closing SFTP manager for VM %s: %v", vmID, err)
167
+ }
168
+ }
169
+
170
+ // Close all SSH managers
171
+ for vmID, sshMgr := range h.sshManagers {
172
+ if err := sshMgr.Disconnect(); err != nil {
173
+ log.Printf("Error disconnecting SSH manager for VM %s: %v", vmID, err)
174
+ }
175
+ }
176
+
177
+ // Disconnect all auth providers
178
+ for vmID, provider := range h.authProviders {
179
+ if err := provider.Disconnect(); err != nil {
180
+ log.Printf("Error disconnecting auth provider for VM %s: %v", vmID, err)
181
+ }
182
+ }
183
+
184
+ log.Println("IPC handler: cleanup complete")
185
+ }
186
+
187
+ // getSSHManager gets or creates an SSH manager for a VM
188
+ func (h *Handler) getSSHManager(vmID string) (*ssh.Manager, error) {
189
+ h.mu.RLock()
190
+ manager, exists := h.sshManagers[vmID]
191
+ h.mu.RUnlock()
192
+
193
+ if exists && manager.IsConnected() {
194
+ return manager, nil
195
+ }
196
+
197
+ // Get VM from registry
198
+ v, err := h.registry.Get(vmID)
199
+ if err != nil {
200
+ return nil, fmt.Errorf("VM not found: %w", err)
201
+ }
202
+
203
+ // Get or create auth provider
204
+ provider, err := h.getAuthProvider(vmID, v)
205
+ if err != nil {
206
+ return nil, fmt.Errorf("failed to get auth provider: %w", err)
207
+ }
208
+
209
+ // Create SSH manager
210
+ manager = ssh.NewManager(v, provider)
211
+
212
+ h.mu.Lock()
213
+ h.sshManagers[vmID] = manager
214
+ h.mu.Unlock()
215
+
216
+ return manager, nil
217
+ }
218
+
219
+ // getSFTPManager gets or creates an SFTP manager for a VM
220
+ func (h *Handler) getSFTPManager(vmID string) (*sftp.Manager, error) {
221
+ h.mu.RLock()
222
+ manager, exists := h.sftpManagers[vmID]
223
+ h.mu.RUnlock()
224
+
225
+ if exists {
226
+ return manager, nil
227
+ }
228
+
229
+ // Get SSH manager first
230
+ sshMgr, err := h.getSSHManager(vmID)
231
+ if err != nil {
232
+ return nil, fmt.Errorf("failed to get SSH manager: %w", err)
233
+ }
234
+
235
+ // Ensure connected
236
+ if !sshMgr.IsConnected() {
237
+ if err := sshMgr.Connect(); err != nil {
238
+ return nil, fmt.Errorf("failed to connect SSH: %w", err)
239
+ }
240
+ }
241
+
242
+ // Create SFTP manager
243
+ client := sshMgr.GetClient()
244
+ if client == nil {
245
+ return nil, fmt.Errorf("SSH client is nil")
246
+ }
247
+
248
+ manager, err = sftp.NewManager(client)
249
+ if err != nil {
250
+ return nil, fmt.Errorf("failed to create SFTP manager: %w", err)
251
+ }
252
+
253
+ h.mu.Lock()
254
+ h.sftpManagers[vmID] = manager
255
+ h.mu.Unlock()
256
+
257
+ return manager, nil
258
+ }
259
+
260
+ // getAuthProvider gets or creates an auth provider for a VM
261
+ func (h *Handler) getAuthProvider(vmID string, v *vm.VM) (auth.Provider, error) {
262
+ h.mu.RLock()
263
+ provider, exists := h.authProviders[vmID]
264
+ h.mu.RUnlock()
265
+
266
+ if exists {
267
+ return provider, nil
268
+ }
269
+
270
+ // Create new provider
271
+ provider, err := auth.NewProvider(v)
272
+ if err != nil {
273
+ return nil, err
274
+ }
275
+
276
+ h.mu.Lock()
277
+ h.authProviders[vmID] = provider
278
+ h.mu.Unlock()
279
+
280
+ return provider, nil
281
+ }
@@ -0,0 +1,126 @@
1
+ package ipc
2
+
3
+ // Request represents an IPC request from the UI
4
+ type Request struct {
5
+ RequestID int `json:"request_id"`
6
+ Action string `json:"action"`
7
+ Params map[string]interface{} `json:"params,omitempty"`
8
+ }
9
+
10
+ // Response represents an IPC response to the UI
11
+ type Response struct {
12
+ RequestID int `json:"request_id"`
13
+ Status string `json:"status"` // "success" | "error"
14
+ Message string `json:"message,omitempty"`
15
+ Data interface{} `json:"data,omitempty"`
16
+ Event string `json:"event,omitempty"` // For event-based messages
17
+ }
18
+
19
+ // NewSuccessResponse creates a success response
20
+ func NewSuccessResponse(data interface{}) Response {
21
+ return Response{
22
+ Status: "success",
23
+ Data: data,
24
+ }
25
+ }
26
+
27
+ // NewSuccessResponseWithID creates a success response with request ID
28
+ func NewSuccessResponseWithID(requestID int, data interface{}) Response {
29
+ return Response{
30
+ RequestID: requestID,
31
+ Status: "success",
32
+ Data: data,
33
+ }
34
+ }
35
+
36
+ // NewSuccessResponseWithMessage creates a success response with a message
37
+ func NewSuccessResponseWithMessage(message string, data interface{}) Response {
38
+ return Response{
39
+ Status: "success",
40
+ Message: message,
41
+ Data: data,
42
+ }
43
+ }
44
+
45
+ // NewSuccessResponseWithMessageAndID creates a success response with message and request ID
46
+ func NewSuccessResponseWithMessageAndID(requestID int, message string, data interface{}) Response {
47
+ return Response{
48
+ RequestID: requestID,
49
+ Status: "success",
50
+ Message: message,
51
+ Data: data,
52
+ }
53
+ }
54
+
55
+ // NewErrorResponse creates an error response
56
+ func NewErrorResponse(err error) Response {
57
+ return Response{
58
+ Status: "error",
59
+ Message: err.Error(),
60
+ }
61
+ }
62
+
63
+ // NewErrorResponseWithID creates an error response with request ID
64
+ func NewErrorResponseWithID(requestID int, err error) Response {
65
+ return Response{
66
+ RequestID: requestID,
67
+ Status: "error",
68
+ Message: err.Error(),
69
+ }
70
+ }
71
+
72
+ // NewErrorResponseWithMessage creates an error response with a custom message
73
+ func NewErrorResponseWithMessage(message string) Response {
74
+ return Response{
75
+ Status: "error",
76
+ Message: message,
77
+ }
78
+ }
79
+
80
+ // NewErrorResponseWithMessageAndID creates an error response with custom message and request ID
81
+ func NewErrorResponseWithMessageAndID(requestID int, message string) Response {
82
+ return Response{
83
+ RequestID: requestID,
84
+ Status: "error",
85
+ Message: message,
86
+ }
87
+ }
88
+
89
+ // Action constants
90
+ const (
91
+ ActionListVMs = "list_vms"
92
+ ActionAddVM = "add_vm"
93
+ ActionRemoveVM = "remove_vm"
94
+ ActionConnectVM = "connect_vm"
95
+ ActionDisconnectVM = "disconnect_vm"
96
+ ActionExecuteCommand = "execute_command"
97
+ ActionListFiles = "list_files"
98
+ ActionUploadFile = "upload_file"
99
+ ActionDownloadFile = "download_file"
100
+ ActionDeleteFile = "delete_file"
101
+ ActionRenameFile = "rename_file"
102
+ ActionStorePassword = "store_password"
103
+ ActionTestConnection = "test_connection"
104
+ ActionStartPTY = "start_pty"
105
+ ActionWriteToPTY = "write_to_pty"
106
+ ActionResizePTY = "resize_pty"
107
+ ActionClosePTY = "close_pty"
108
+ )
109
+
110
+ // Event constants
111
+ const (
112
+ EventPTYReady = "PTY_READY"
113
+ EventPTYOutput = "PTY_OUTPUT"
114
+ EventPTYExit = "PTY_EXIT"
115
+ EventPTYError = "PTY_ERROR"
116
+ EventTransferProgress = "TRANSFER_PROGRESS"
117
+ )
118
+
119
+ // NewEvent creates an event response (no request ID needed)
120
+ func NewEvent(event string, data interface{}) Response {
121
+ return Response{
122
+ Status: "success",
123
+ Event: event,
124
+ Data: data,
125
+ }
126
+ }
@@ -0,0 +1,29 @@
1
+ package sftp
2
+
3
+ import "errors"
4
+
5
+ var (
6
+ // ErrSFTPClientNotInitialized is returned when SFTP client is not initialized
7
+ ErrSFTPClientNotInitialized = errors.New("SFTP client not initialized")
8
+
9
+ // ErrFileNotFound is returned when a file doesn't exist
10
+ ErrFileNotFound = errors.New("file not found")
11
+
12
+ // ErrPermissionDenied is returned when permission is denied
13
+ ErrPermissionDenied = errors.New("permission denied")
14
+
15
+ // ErrFileAlreadyExists is returned when trying to create a file that already exists
16
+ ErrFileAlreadyExists = errors.New("file already exists")
17
+
18
+ // ErrInvalidPath is returned when a path is invalid
19
+ ErrInvalidPath = errors.New("invalid path")
20
+
21
+ // ErrIsDirectory is returned when expecting a file but got a directory
22
+ ErrIsDirectory = errors.New("path is a directory")
23
+
24
+ // ErrNotDirectory is returned when expecting a directory but got a file
25
+ ErrNotDirectory = errors.New("path is not a directory")
26
+
27
+ // ErrTransferFailed is returned when file transfer fails
28
+ ErrTransferFailed = errors.New("file transfer failed")
29
+ )