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,84 @@
1
+ package vm
2
+
3
+ import (
4
+ "time"
5
+ )
6
+
7
+ // VMStatus represents the current state of a VM connection
8
+ type VMStatus string
9
+
10
+ const (
11
+ VMStatusUnknown VMStatus = "unknown"
12
+ VMStatusConnected VMStatus = "connected"
13
+ VMStatusDisconnected VMStatus = "disconnected"
14
+ VMStatusConnecting VMStatus = "connecting"
15
+ VMStatusError VMStatus = "error"
16
+ )
17
+
18
+ // AuthType represents the authentication method
19
+ type AuthType string
20
+
21
+ const (
22
+ AuthTypeKey AuthType = "key"
23
+ AuthTypePassword AuthType = "password"
24
+ )
25
+
26
+ // AuthConfig contains authentication configuration for a VM
27
+ type AuthConfig struct {
28
+ Type AuthType `yaml:"type" json:"type"`
29
+ KeyPath string `yaml:"key_path,omitempty" json:"keyPath,omitempty"`
30
+ RememberPassword bool `yaml:"remember_password" json:"rememberPassword"`
31
+ }
32
+
33
+ // VM represents a virtual machine configuration
34
+ type VM struct {
35
+ ID string `yaml:"id" json:"id"`
36
+ Name string `yaml:"name" json:"name"`
37
+ Host string `yaml:"host" json:"host"`
38
+ Port int `yaml:"port" json:"port"`
39
+ Username string `yaml:"username" json:"username"`
40
+ Auth AuthConfig `yaml:"auth" json:"auth"`
41
+ LastSeen time.Time `yaml:"last_seen" json:"lastSeen"`
42
+ Status VMStatus `yaml:"status" json:"status"`
43
+ }
44
+
45
+ // Validate checks if the VM configuration is valid
46
+ func (v *VM) Validate() error {
47
+ if v.Name == "" {
48
+ return ErrInvalidVMName
49
+ }
50
+ if v.Host == "" {
51
+ return ErrInvalidVMHost
52
+ }
53
+ if v.Port <= 0 || v.Port > 65535 {
54
+ return ErrInvalidVMPort
55
+ }
56
+ if v.Username == "" {
57
+ return ErrInvalidVMUsername
58
+ }
59
+ if v.Auth.Type != AuthTypeKey && v.Auth.Type != AuthTypePassword {
60
+ return ErrInvalidAuthType
61
+ }
62
+ if v.Auth.Type == AuthTypeKey && v.Auth.KeyPath == "" {
63
+ return ErrMissingKeyPath
64
+ }
65
+ return nil
66
+ }
67
+
68
+ // Clone creates a deep copy of the VM
69
+ func (v *VM) Clone() *VM {
70
+ return &VM{
71
+ ID: v.ID,
72
+ Name: v.Name,
73
+ Host: v.Host,
74
+ Port: v.Port,
75
+ Username: v.Username,
76
+ Auth: AuthConfig{
77
+ Type: v.Auth.Type,
78
+ KeyPath: v.Auth.KeyPath,
79
+ RememberPassword: v.Auth.RememberPassword,
80
+ },
81
+ LastSeen: v.LastSeen,
82
+ Status: v.Status,
83
+ }
84
+ }
@@ -0,0 +1,240 @@
1
+ package vm
2
+
3
+ import (
4
+ "crypto/rand"
5
+ "encoding/hex"
6
+ "fmt"
7
+ "os"
8
+ "path/filepath"
9
+ "sync"
10
+ "time"
11
+
12
+ "gopkg.in/yaml.v3"
13
+ )
14
+
15
+ // Config represents the YAML configuration file structure
16
+ type Config struct {
17
+ VMs []VM `yaml:"vms"`
18
+ }
19
+
20
+ // Registry manages the collection of VMs
21
+ type Registry struct {
22
+ vms map[string]*VM
23
+ configDir string
24
+ mu sync.RWMutex
25
+ }
26
+
27
+ // NewRegistry creates a new VM registry
28
+ func NewRegistry(configDir string) (*Registry, error) {
29
+ if err := os.MkdirAll(configDir, 0700); err != nil {
30
+ return nil, fmt.Errorf("failed to create config directory: %w", err)
31
+ }
32
+
33
+ return &Registry{
34
+ vms: make(map[string]*VM),
35
+ configDir: configDir,
36
+ }, nil
37
+ }
38
+
39
+ // Add adds a VM to the registry
40
+ func (r *Registry) Add(vm *VM) error {
41
+ if vm == nil {
42
+ return fmt.Errorf("cannot add nil VM")
43
+ }
44
+
45
+ // Validate VM
46
+ if err := vm.Validate(); err != nil {
47
+ return fmt.Errorf("VM validation failed: %w", err)
48
+ }
49
+
50
+ // Generate ID if not provided
51
+ if vm.ID == "" {
52
+ vm.ID = r.generateID()
53
+ }
54
+
55
+ r.mu.Lock()
56
+ defer r.mu.Unlock()
57
+
58
+ // Check if VM already exists
59
+ if _, exists := r.vms[vm.ID]; exists {
60
+ return ErrVMAlreadyExists
61
+ }
62
+
63
+ // Set initial status
64
+ if vm.Status == "" {
65
+ vm.Status = VMStatusDisconnected
66
+ }
67
+
68
+ // Store VM (clone to prevent external modifications)
69
+ r.vms[vm.ID] = vm.Clone()
70
+
71
+ return nil
72
+ }
73
+
74
+ // Get retrieves a VM by ID
75
+ func (r *Registry) Get(id string) (*VM, error) {
76
+ r.mu.RLock()
77
+ defer r.mu.RUnlock()
78
+
79
+ vm, exists := r.vms[id]
80
+ if !exists {
81
+ return nil, ErrVMNotFound
82
+ }
83
+
84
+ return vm.Clone(), nil
85
+ }
86
+
87
+ // List returns all VMs in the registry
88
+ func (r *Registry) List() []*VM {
89
+ r.mu.RLock()
90
+ defer r.mu.RUnlock()
91
+
92
+ vms := make([]*VM, 0, len(r.vms))
93
+ for _, vm := range r.vms {
94
+ vms = append(vms, vm.Clone())
95
+ }
96
+
97
+ return vms
98
+ }
99
+
100
+ // Remove removes a VM from the registry
101
+ func (r *Registry) Remove(id string) error {
102
+ r.mu.Lock()
103
+ defer r.mu.Unlock()
104
+
105
+ if _, exists := r.vms[id]; !exists {
106
+ return ErrVMNotFound
107
+ }
108
+
109
+ delete(r.vms, id)
110
+ return nil
111
+ }
112
+
113
+ // Update updates a VM's information
114
+ func (r *Registry) Update(vm *VM) error {
115
+ if vm == nil {
116
+ return fmt.Errorf("cannot update with nil VM")
117
+ }
118
+
119
+ if err := vm.Validate(); err != nil {
120
+ return fmt.Errorf("VM validation failed: %w", err)
121
+ }
122
+
123
+ r.mu.Lock()
124
+ defer r.mu.Unlock()
125
+
126
+ if _, exists := r.vms[vm.ID]; !exists {
127
+ return ErrVMNotFound
128
+ }
129
+
130
+ r.vms[vm.ID] = vm.Clone()
131
+ return nil
132
+ }
133
+
134
+ // UpdateStatus updates a VM's status
135
+ func (r *Registry) UpdateStatus(id string, status VMStatus) error {
136
+ r.mu.Lock()
137
+ defer r.mu.Unlock()
138
+
139
+ vm, exists := r.vms[id]
140
+ if !exists {
141
+ return ErrVMNotFound
142
+ }
143
+
144
+ vm.Status = status
145
+ vm.LastSeen = time.Now()
146
+ return nil
147
+ }
148
+
149
+ // Save persists the registry to the config file
150
+ func (r *Registry) Save() error {
151
+ r.mu.RLock()
152
+ defer r.mu.RUnlock()
153
+
154
+ config := Config{
155
+ VMs: make([]VM, 0, len(r.vms)),
156
+ }
157
+
158
+ for _, vm := range r.vms {
159
+ config.VMs = append(config.VMs, *vm)
160
+ }
161
+
162
+ data, err := yaml.Marshal(&config)
163
+ if err != nil {
164
+ return fmt.Errorf("failed to marshal config: %w", err)
165
+ }
166
+
167
+ configPath := filepath.Join(r.configDir, "config.yaml")
168
+
169
+ // Write with secure permissions (0600)
170
+ if err := os.WriteFile(configPath, data, 0600); err != nil {
171
+ return fmt.Errorf("failed to write config file: %w", err)
172
+ }
173
+
174
+ return nil
175
+ }
176
+
177
+ // Load reads the registry from the config file
178
+ func (r *Registry) Load() error {
179
+ configPath := filepath.Join(r.configDir, "config.yaml")
180
+
181
+ // Check if config file exists
182
+ if _, err := os.Stat(configPath); os.IsNotExist(err) {
183
+ // Config doesn't exist - this is OK for first run
184
+ return nil
185
+ }
186
+
187
+ data, err := os.ReadFile(configPath)
188
+ if err != nil {
189
+ return fmt.Errorf("failed to read config file: %w", err)
190
+ }
191
+
192
+ var config Config
193
+ if err := yaml.Unmarshal(data, &config); err != nil {
194
+ return ErrConfigCorrupted
195
+ }
196
+
197
+ r.mu.Lock()
198
+ defer r.mu.Unlock()
199
+
200
+ // Clear existing VMs
201
+ r.vms = make(map[string]*VM)
202
+
203
+ // Load VMs from config
204
+ for i := range config.VMs {
205
+ vm := &config.VMs[i]
206
+
207
+ // Validate each VM
208
+ if err := vm.Validate(); err != nil {
209
+ // Log warning but continue loading other VMs
210
+ fmt.Fprintf(os.Stderr, "Warning: skipping invalid VM %s: %v\n", vm.Name, err)
211
+ continue
212
+ }
213
+
214
+ // Generate ID if missing
215
+ if vm.ID == "" {
216
+ vm.ID = r.generateID()
217
+ }
218
+
219
+ // Reset status to disconnected on load
220
+ vm.Status = VMStatusDisconnected
221
+
222
+ r.vms[vm.ID] = vm
223
+ }
224
+
225
+ return nil
226
+ }
227
+
228
+ // Count returns the number of VMs in the registry
229
+ func (r *Registry) Count() int {
230
+ r.mu.RLock()
231
+ defer r.mu.RUnlock()
232
+ return len(r.vms)
233
+ }
234
+
235
+ // generateID generates a unique ID for a VM
236
+ func (r *Registry) generateID() string {
237
+ b := make([]byte, 8)
238
+ rand.Read(b)
239
+ return hex.EncodeToString(b)
240
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "heyvm",
3
+ "version": "1.0.0",
4
+ "description": "A zero-config, interactive terminal UI to connect, manage, and transfer files to VMs over SSH",
5
+ "type": "module",
6
+ "bin": {
7
+ "heyvm": "./bin/heyvm.js"
8
+ },
9
+ "scripts": {
10
+ "postinstall": "node scripts/install.js",
11
+ "prepublishOnly": "npm run build",
12
+ "build": "npm run build:ui",
13
+ "build:ui": "cd ui && npm install && npm run build",
14
+ "version": "echo \"Version updated to $(node -p 'require(\"./package.json\").version')\"",
15
+ "publish:public": "npm publish --access public",
16
+ "dev": "make dev",
17
+ "test": "make test"
18
+ },
19
+ "keywords": [
20
+ "ssh",
21
+ "vm",
22
+ "terminal",
23
+ "tui",
24
+ "cli",
25
+ "sftp",
26
+ "file-transfer",
27
+ "remote-management",
28
+ "devops",
29
+ "server-management"
30
+ ],
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/adishm/heyvm.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/adishm/heyvm/issues"
37
+ },
38
+ "homepage": "https://github.com/adishm/heyvm#readme",
39
+ "author": "Adish M",
40
+ "license": "MIT",
41
+ "engines": {
42
+ "node": ">=18.0.0"
43
+ },
44
+ "files": [
45
+ "bin/",
46
+ "ui/dist/",
47
+ "ui/scripts/",
48
+ "ui/package.json",
49
+ "core/",
50
+ "scripts/",
51
+ "LICENSE",
52
+ "README.md"
53
+ ],
54
+ "os": [
55
+ "darwin",
56
+ "linux",
57
+ "win32"
58
+ ]
59
+ }
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Post-install script for heyvm
5
+ * Builds the Go backend when installed globally via npm
6
+ */
7
+
8
+ import { spawn } from 'child_process';
9
+ import { existsSync, mkdirSync } from 'fs';
10
+ import { dirname, join } from 'path';
11
+ import { fileURLToPath } from 'url';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+ const rootDir = join(__dirname, '..');
16
+
17
+ console.log('🚀 Setting up heyvm...\n');
18
+
19
+ // Check if Go is installed
20
+ function checkGo() {
21
+ return new Promise((resolve) => {
22
+ const goVersion = spawn('go', ['version']);
23
+
24
+ goVersion.on('error', () => {
25
+ console.log('⚠️ Go not found - you will need Go >= 1.21 to use heyvm');
26
+ console.log(' Download from: https://go.dev/dl/\n');
27
+ resolve(false);
28
+ });
29
+
30
+ goVersion.on('close', (code) => {
31
+ if (code === 0) {
32
+ console.log('✓ Go is installed');
33
+ resolve(true);
34
+ } else {
35
+ console.log('⚠️ Could not verify Go installation');
36
+ resolve(false);
37
+ }
38
+ });
39
+ });
40
+ }
41
+
42
+ // Build Go backend
43
+ function buildCore() {
44
+ return new Promise((resolve, reject) => {
45
+ console.log('📦 Building Go backend (heyvm-core)...');
46
+
47
+ // Ensure bin directory exists
48
+ const binDir = join(rootDir, 'bin');
49
+ if (!existsSync(binDir)) {
50
+ mkdirSync(binDir, { recursive: true });
51
+ }
52
+
53
+ const coreDir = join(rootDir, 'core');
54
+ const buildProcess = spawn('go', ['build', '-o', '../bin/heyvm-core', './cmd/heyvm-core'], {
55
+ cwd: coreDir,
56
+ stdio: 'inherit',
57
+ });
58
+
59
+ buildProcess.on('error', (err) => {
60
+ reject(new Error(`Failed to build Go backend: ${err.message}`));
61
+ });
62
+
63
+ buildProcess.on('close', (code) => {
64
+ if (code === 0) {
65
+ console.log('✓ Go backend built successfully\n');
66
+ resolve();
67
+ } else {
68
+ reject(new Error(`Go build failed with exit code ${code}`));
69
+ }
70
+ });
71
+ });
72
+ }
73
+
74
+ // Run installation
75
+ async function install() {
76
+ try {
77
+ const hasGo = await checkGo();
78
+
79
+ if (!hasGo) {
80
+ console.log('❌ Installation incomplete: Go is required');
81
+ console.log('\nPlease install Go >= 1.21 from https://go.dev/dl/');
82
+ console.log('Then run: npm install -g heyvm\n');
83
+ process.exit(1);
84
+ }
85
+
86
+ await buildCore();
87
+
88
+ console.log('✅ heyvm installed successfully!');
89
+ console.log('\nRun "heyvm" to start managing your VMs\n');
90
+ } catch (error) {
91
+ console.error('\n❌ Installation failed:', error.message);
92
+ console.error('\nRequirements:');
93
+ console.error(' - Node.js >= 18.0.0');
94
+ console.error(' - Go >= 1.21');
95
+ console.error('\nFor help, visit: https://github.com/adishm/heyvm');
96
+ process.exit(1);
97
+ }
98
+ }
99
+
100
+ install();
package/ui/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # heyvm UI
2
+
3
+ Interactive terminal UI for heyvm, built with Ink (React for CLIs).
4
+
5
+ ## Development
6
+
7
+ ```bash
8
+ # Install dependencies
9
+ npm install
10
+
11
+ # Run in development mode
12
+ npm run dev
13
+
14
+ # Run with core backend
15
+ npm run dev:with-core
16
+
17
+ # Build for production
18
+ npm run build
19
+
20
+ # Run production build
21
+ npm start
22
+ ```
23
+
24
+ ## Architecture
25
+
26
+ - **Framework**: Ink (React for CLIs)
27
+ - **Language**: TypeScript
28
+ - **Communication**: JSON-RPC over stdio with heyvm-core
29
+ - **State Management**: React hooks + context
30
+
31
+ ## Directory Structure
32
+
33
+ ```
34
+ ui/
35
+ ├── src/
36
+ │ ├── components/ # Reusable UI components
37
+ │ ├── screens/ # Main screen components
38
+ │ ├── hooks/ # Custom React hooks
39
+ │ ├── core/ # IPC client and types
40
+ │ └── index.tsx # Entry point
41
+ ├── scripts/ # Build and utility scripts
42
+ └── package.json # Dependencies and scripts
43
+ ```
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ export default function App(): React.JSX.Element;
3
+ //# sourceMappingURL=App.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../src/App.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAgC,MAAM,OAAO,CAAC;AAQrD,MAAM,CAAC,OAAO,UAAU,GAAG,sBAqM1B"}
package/ui/dist/App.js ADDED
@@ -0,0 +1,142 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import { Box, Text, useApp, useInput } from 'ink';
3
+ import VMListScreen from './screens/VMListScreen.js';
4
+ import AddVMScreen from './screens/AddVMScreen.js';
5
+ import VMDetailScreen from './screens/VMDetailScreen.js';
6
+ import { ipcClient } from './core/ipc.js';
7
+ export default function App() {
8
+ // Split-pane state model
9
+ const [selectedVM, setSelectedVM] = useState(null);
10
+ const [activeTab, setActiveTab] = useState('terminal');
11
+ const [showAddVMModal, setShowAddVMModal] = useState(false);
12
+ const [activePaneSide, setActivePaneSide] = useState('left');
13
+ const [vmListVersion, setVMListVersion] = useState(0);
14
+ const { exit } = useApp();
15
+ // Refresh selected VM after state changes
16
+ const refreshSelectedVM = useCallback(async () => {
17
+ if (!selectedVM)
18
+ return;
19
+ try {
20
+ const vms = await ipcClient.listVMs();
21
+ const updatedVM = vms.find(v => v.id === selectedVM.id);
22
+ if (updatedVM) {
23
+ setSelectedVM(updatedVM);
24
+ }
25
+ else {
26
+ // VM was deleted, deselect it
27
+ setSelectedVM(null);
28
+ setActivePaneSide('left');
29
+ }
30
+ // Trigger VM list refresh
31
+ setVMListVersion(v => v + 1);
32
+ }
33
+ catch (err) {
34
+ console.error('Failed to refresh VM:', err);
35
+ }
36
+ }, [selectedVM]);
37
+ // Handlers
38
+ const handleSelectVM = (vm) => {
39
+ setSelectedVM(vm);
40
+ setActivePaneSide('right');
41
+ setActiveTab('overview'); // Always default to overview
42
+ // No auto-connect - user must explicitly connect via 'c' key
43
+ };
44
+ const handleAddVMSubmit = async (vm, password) => {
45
+ try {
46
+ const newVM = await ipcClient.addVM(vm);
47
+ console.log('[App] VM added successfully:', newVM.id);
48
+ // Store password in keychain after VM is added (now we have ID)
49
+ if (password && newVM.id) {
50
+ console.log('[App] Storing password for VM:', newVM.id);
51
+ try {
52
+ await ipcClient.storePassword(newVM.id, password);
53
+ console.log('[App] Password stored successfully');
54
+ }
55
+ catch (passErr) {
56
+ console.error('[App] Failed to store password:', passErr);
57
+ // Show error to user but don't fail VM creation
58
+ // Password can be re-entered later
59
+ }
60
+ }
61
+ setShowAddVMModal(false);
62
+ setSelectedVM(newVM);
63
+ setActivePaneSide('right');
64
+ setActiveTab('overview');
65
+ // No auto-connect - user must explicitly connect via 'c' key
66
+ }
67
+ catch (err) {
68
+ console.error('Failed to add VM:', err);
69
+ // Error is handled in AddVMScreen, this is just a fallback
70
+ }
71
+ };
72
+ const handleVMDeleted = () => {
73
+ setSelectedVM(null);
74
+ setActivePaneSide('left');
75
+ };
76
+ // Global keyboard handler
77
+ useInput((input, key) => {
78
+ // Modal takes precedence
79
+ if (showAddVMModal)
80
+ return;
81
+ // Ctrl+O from terminal: switch to overview tab
82
+ if (key.ctrl && input === 'o' && activePaneSide === 'right' && selectedVM && activeTab === 'terminal') {
83
+ setActiveTab('overview');
84
+ return;
85
+ }
86
+ // Terminal has complete input priority - don't intercept ANYTHING when terminal is active
87
+ if (activePaneSide === 'right' && selectedVM && activeTab === 'terminal') {
88
+ return; // Let terminal handle all input
89
+ }
90
+ // Global quit (only from left pane with no selection)
91
+ if (input === 'q' && activePaneSide === 'left' && !selectedVM) {
92
+ exit();
93
+ }
94
+ // Tab switches panes (only when VM selected)
95
+ // BUT: Don't intercept Tab in Files tab (it needs Tab for local/remote switching)
96
+ if (key.tab && selectedVM && activeTab !== 'files') {
97
+ setActivePaneSide(prev => prev === 'left' ? 'right' : 'left');
98
+ return;
99
+ }
100
+ // Escape deselects VM (from right pane)
101
+ if (key.escape && activePaneSide === 'right' && selectedVM) {
102
+ setSelectedVM(null);
103
+ setActivePaneSide('left');
104
+ return;
105
+ }
106
+ // Tab switching via number keys (only from right pane, NOT in terminal)
107
+ if (activePaneSide === 'right' && selectedVM && activeTab !== 'terminal') {
108
+ if (input === '1') {
109
+ setActiveTab('overview');
110
+ return;
111
+ }
112
+ if (input === '2') {
113
+ setActiveTab('terminal');
114
+ return;
115
+ }
116
+ if (input === '3') {
117
+ setActiveTab('files');
118
+ return;
119
+ }
120
+ }
121
+ // Add VM (only from left pane)
122
+ if (input === 'a' && activePaneSide === 'left') {
123
+ setShowAddVMModal(true);
124
+ return;
125
+ }
126
+ });
127
+ return (React.createElement(Box, { flexDirection: "column", height: "100%" }, showAddVMModal ? (
128
+ // Modal mode: full-screen Add VM form
129
+ React.createElement(Box, { flexDirection: "column", padding: 2 },
130
+ React.createElement(AddVMScreen, { onCancel: () => setShowAddVMModal(false), onSubmit: handleAddVMSubmit }))) : (
131
+ // Split pane mode: VM list + detail
132
+ React.createElement(Box, { flexGrow: 1 },
133
+ React.createElement(Box, { width: "35%", flexDirection: "column", borderStyle: "single", borderColor: "gray" },
134
+ React.createElement(VMListScreen, { selectedVM: selectedVM, onSelectVM: handleSelectVM, isActive: activePaneSide === 'left', onVMDeleted: handleVMDeleted, vmListVersion: vmListVersion })),
135
+ React.createElement(Box, { width: "65%", flexDirection: "column", borderStyle: "single", borderColor: "gray" }, selectedVM ? (React.createElement(VMDetailScreen, { vm: selectedVM, activeTab: activeTab, onTabChange: setActiveTab, isActive: activePaneSide === 'right', onVMUpdated: refreshSelectedVM })) : (React.createElement(Box, { flexDirection: "column", padding: 2, justifyContent: "center", alignItems: "center", height: "100%" },
136
+ React.createElement(Text, { dimColor: true }, "No VM selected"),
137
+ React.createElement(Box, { marginTop: 1 },
138
+ React.createElement(Text, { dimColor: true }, "Select a VM from the list to view details")),
139
+ React.createElement(Box, { marginTop: 1 },
140
+ React.createElement(Text, { dimColor: true }, "Press 'a' to add a new VM")))))))));
141
+ }
142
+ //# sourceMappingURL=App.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"App.js","sourceRoot":"","sources":["../src/App.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ interface ConfirmDialogProps {
3
+ message: string;
4
+ onConfirm: () => void;
5
+ onCancel: () => void;
6
+ }
7
+ export default function ConfirmDialog({ message, onConfirm, onCancel }: ConfirmDialogProps): React.JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=ConfirmDialog.d.ts.map