gitarsenal-cli 1.9.106 → 1.9.108

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.
@@ -0,0 +1,609 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { execSync, spawn } from 'child_process';
4
+ import { join } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname } from 'path';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ // Helper function to center text
12
+ function center(text, stripAnsi = false) {
13
+ const terminalWidth = process.stdout.columns || 80;
14
+ // Remove ANSI codes for width calculation if needed
15
+ const visibleText = stripAnsi ? text.replace(/\x1b\[[0-9;]*m/g, '') : text;
16
+ const padding = Math.max(0, Math.floor((terminalWidth - visibleText.length) / 2));
17
+ return ' '.repeat(padding) + text;
18
+ }
19
+
20
+ console.clear();
21
+ console.log(center('\x1b[32m╭────────────────────────────────────────────────────────────────────────────────╮\x1b[0m', true));
22
+ console.log(center('\x1b[32m│ │\x1b[0m', true));
23
+ console.log(center('\x1b[32m│ \x1b[1m\x1b[32m██████╗ ██╗████████╗ █████╗ ██████╗ ███████╗███████╗███╗ ██╗ █████╗ ██╗\x1b[0m\x1b[32m │\x1b[0m', true));
24
+ console.log(center('\x1b[32m│ \x1b[1m\x1b[32m██╔════╝ ██║╚══██╔══╝██╔══██╗██╔══██╗██╔════╝██╔════╝████╗ ██║██╔══██╗██║\x1b[0m\x1b[32m │\x1b[0m', true));
25
+ console.log(center('\x1b[32m│ \x1b[1m\x1b[32m██║ ███╗██║ ██║ ███████║██████╔╝███████╗█████╗ ██╔██╗ ██║███████║██║\x1b[0m\x1b[32m │\x1b[0m', true));
26
+ console.log(center('\x1b[32m│ \x1b[1m\x1b[32m██║ ██║██║ ██║ ██╔══██║██╔══██╗╚════██║██╔══╝ ██║╚██╗██║██╔══██║██║\x1b[0m\x1b[32m │\x1b[0m', true));
27
+ console.log(center('\x1b[32m│ \x1b[1m\x1b[32m╚██████╔╝██║ ██║ ██║ ██║██║ ██║███████║███████╗██║ ╚████║██║ ██║███████╗\x1b[0m\x1b[32m│\x1b[0m', true));
28
+ console.log(center('\x1b[32m│ \x1b[1m\x1b[32m╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝\x1b[0m\x1b[32m│\x1b[0m', true));
29
+ console.log(center('\x1b[32m│ │\x1b[0m', true));
30
+ console.log(center('\x1b[32m│ \x1b[1m\x1b[37m GPU-Accelerated Development Environments\x1b[0m\x1b[32m │\x1b[0m', true));
31
+ console.log(center('\x1b[32m│ \x1b[90m 🎨 Powered by OpenTUI Framework\x1b[0m\x1b[32m │\x1b[0m', true));
32
+ console.log(center('\x1b[32m│ │\x1b[0m', true));
33
+ console.log(center('\x1b[32m╰────────────────────────────────────────────────────────────────────────────────╯\x1b[0m', true));
34
+ console.log('');
35
+
36
+ const menuItems = [
37
+ '🚀 Create New Sandbox',
38
+ '📋 View Running Sandboxes',
39
+ '🔑 API Keys Management',
40
+ '⚙️ Settings',
41
+ '❓ Help & Examples',
42
+ '🚪 Exit'
43
+ ];
44
+
45
+ const gpuOptions = [
46
+ { name: 'T4 (16GB VRAM)', value: 'T4' },
47
+ { name: 'L4 (24GB VRAM)', value: 'L4' },
48
+ { name: 'A10G (24GB VRAM)', value: 'A10G' },
49
+ { name: 'A100-40 (40GB VRAM)', value: 'A100-40GB' },
50
+ { name: 'A100-80 (80GB VRAM)', value: 'A100-80GB' },
51
+ { name: 'L40S (48GB VRAM)', value: 'L40S' },
52
+ { name: 'H100 (80GB VRAM)', value: 'H100' },
53
+ { name: 'H200 (141GB VRAM)', value: 'H200' },
54
+ { name: 'B200 (141GB VRAM)', value: 'B200' }
55
+ ];
56
+
57
+ const gpuCountOptions = [
58
+ { name: '1 GPU', value: 1 },
59
+ { name: '2 GPUs', value: 2 },
60
+ { name: '3 GPUs', value: 3 },
61
+ { name: '4 GPUs', value: 4 },
62
+ { name: '6 GPUs', value: 6 },
63
+ { name: '8 GPUs', value: 8 }
64
+ ];
65
+
66
+ const providerOptions = [
67
+ { name: 'Modal (GPU support, persistent volumes)', value: 'modal' },
68
+ { name: 'E2B (Faster startup, no GPU)', value: 'e2b' }
69
+ ];
70
+
71
+ let selectedIndex = 0;
72
+ let currentScreen = 'menu';
73
+ let inputText = '';
74
+ let config = {
75
+ repoUrl: '',
76
+ gpuType: 'A10G',
77
+ gpuCount: 1,
78
+ sandboxProvider: 'modal'
79
+ };
80
+
81
+ // Track running sandboxes
82
+ let sandboxes = [];
83
+ let sandboxIdCounter = 1;
84
+
85
+ // Track currently active process (for logs viewing)
86
+ let activeProcess = null;
87
+
88
+ function renderMenu() {
89
+ console.clear();
90
+ console.log(center('\x1b[32m╭────────────────────────────────────────────────────────────────────────────────╮\x1b[0m', true));
91
+ console.log(center('\x1b[32m│ │\x1b[0m', true));
92
+ console.log(center('\x1b[32m│ \x1b[1m\x1b[32m██████╗ ██╗████████╗ █████╗ ██████╗ ███████╗███████╗███╗ ██╗ █████╗ ██╗\x1b[0m\x1b[32m │\x1b[0m', true));
93
+ console.log(center('\x1b[32m│ \x1b[1m\x1b[32m██╔════╝ ██║╚══██╔══╝██╔══██╗██╔══██╗██╔════╝██╔════╝████╗ ██║██╔══██╗██║\x1b[0m\x1b[32m │\x1b[0m', true));
94
+ console.log(center('\x1b[32m│ \x1b[1m\x1b[32m██║ ███╗██║ ██║ ███████║██████╔╝███████╗█████╗ ██╔██╗ ██║███████║██║\x1b[0m\x1b[32m │\x1b[0m', true));
95
+ console.log(center('\x1b[32m│ \x1b[1m\x1b[32m██║ ██║██║ ██║ ██╔══██║██╔══██╗╚════██║██╔══╝ ██║╚██╗██║██╔══██║██║\x1b[0m\x1b[32m │\x1b[0m', true));
96
+ console.log(center('\x1b[32m│ \x1b[1m\x1b[32m╚██████╔╝██║ ██║ ██║ ██║██║ ██║███████║███████╗██║ ╚████║██║ ██║███████╗\x1b[0m\x1b[32m│\x1b[0m', true));
97
+ console.log(center('\x1b[32m│ \x1b[1m\x1b[32m╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝\x1b[0m\x1b[32m│\x1b[0m', true));
98
+ console.log(center('\x1b[32m│ │\x1b[0m', true));
99
+ console.log(center('\x1b[32m│ \x1b[1m\x1b[37m GPU-Accelerated Development Environments\x1b[0m\x1b[32m │\x1b[0m', true));
100
+ console.log(center('\x1b[32m│ \x1b[90m 🎨 Powered by OpenTUI Framework\x1b[0m\x1b[32m │\x1b[0m', true));
101
+ console.log(center('\x1b[32m│ │\x1b[0m', true));
102
+ console.log(center('\x1b[32m╰────────────────────────────────────────────────────────────────────────────────╯\x1b[0m', true));
103
+ console.log('');
104
+
105
+ // Show active sandboxes count
106
+ const runningSandboxes = sandboxes.filter(s => s.status === 'running' || s.status === 'initializing');
107
+ if (runningSandboxes.length > 0) {
108
+ console.log(center(`\x1b[32m📦 ${runningSandboxes.length} active sandbox${runningSandboxes.length > 1 ? 'es' : ''}\x1b[0m`, true));
109
+ console.log('');
110
+ }
111
+
112
+ menuItems.forEach((item, index) => {
113
+ if (index === selectedIndex) {
114
+ console.log(center(`\x1b[1m\x1b[36m❯ ${item}\x1b[0m`, true));
115
+ } else {
116
+ console.log(center(` ${item}`, false));
117
+ }
118
+ });
119
+
120
+ console.log('');
121
+ console.log(center('\x1b[90mUse ↑↓ arrows to navigate, Enter to select, Ctrl+C to exit\x1b[0m', true));
122
+ }
123
+
124
+ function renderSandboxList() {
125
+ console.clear();
126
+ console.log(center('╔════════════════════════════════════════════════════════════╗', false));
127
+ console.log(center('║ 📋 Running Sandboxes ║', false));
128
+ console.log(center('╚════════════════════════════════════════════════════════════╝', false));
129
+ console.log('');
130
+
131
+ if (sandboxes.length === 0) {
132
+ console.log(center('\x1b[90mNo sandboxes running.\x1b[0m', true));
133
+ console.log('');
134
+ console.log(center('\x1b[90mPress Esc to go back to menu\x1b[0m', true));
135
+ return;
136
+ }
137
+
138
+ sandboxes.forEach((sandbox, index) => {
139
+ const isSelected = index === selectedIndex;
140
+ const statusColor = sandbox.status === 'running' ? '\x1b[32m' :
141
+ sandbox.status === 'initializing' ? '\x1b[33m' :
142
+ '\x1b[31m';
143
+ const statusIcon = sandbox.status === 'running' ? '✅' :
144
+ sandbox.status === 'initializing' ? '🔄' :
145
+ '❌';
146
+
147
+ if (isSelected) {
148
+ console.log(center(`\x1b[1m\x1b[36m❯ #${sandbox.id} ${statusIcon} ${sandbox.repo}\x1b[0m`, true));
149
+ console.log(center(`\x1b[36m${statusColor}${sandbox.status}\x1b[0m \x1b[36m| ${sandbox.provider} | ${sandbox.gpu || 'N/A'}\x1b[0m`, true));
150
+ } else {
151
+ console.log(center(`#${sandbox.id} ${statusIcon} ${sandbox.repo}`, false));
152
+ console.log(center(`${statusColor}${sandbox.status}\x1b[0m | ${sandbox.provider} | ${sandbox.gpu || 'N/A'}`, true));
153
+ }
154
+ console.log('');
155
+ });
156
+
157
+ console.log(center('\x1b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m', true));
158
+ console.log('');
159
+ console.log(center('\x1b[1mControls:\x1b[0m', true));
160
+ console.log(center('\x1b[36mEnter\x1b[0m - View logs / Connect to sandbox', true));
161
+ console.log(center('\x1b[36mD\x1b[0m - Delete sandbox', true));
162
+ console.log(center('\x1b[36mR\x1b[0m - Refresh list', true));
163
+ console.log(center('\x1b[36mEsc\x1b[0m - Back to menu', true));
164
+ }
165
+
166
+ function renderRepoInput() {
167
+ console.clear();
168
+ console.log(center('╔════════════════════════════════════════════════════════════╗', false));
169
+ console.log(center('║ 🚀 Create New Sandbox ║', false));
170
+ console.log(center('╚════════════════════════════════════════════════════════════╝', false));
171
+ console.log('');
172
+
173
+ console.log(center('\x1b[1mEnter GitHub repository URL:\x1b[0m', true));
174
+ console.log('');
175
+ console.log(center(`\x1b[36m${inputText}\x1b[7m \x1b[0m`, true));
176
+
177
+ console.log('');
178
+ console.log(center('\x1b[90mExamples:\x1b[0m', true));
179
+ console.log(center('\x1b[90m • https://github.com/pytorch/examples\x1b[0m', true));
180
+ console.log(center('\x1b[90m • https://github.com/huggingface/transformers\x1b[0m', true));
181
+ console.log(center('\x1b[90m • https://github.com/openai/whisper\x1b[0m', true));
182
+ console.log('');
183
+ console.log(center('\x1b[90mPress Enter to continue • Esc to go back • Ctrl+C to exit\x1b[0m', true));
184
+ }
185
+
186
+ function renderProviderSelection() {
187
+ console.clear();
188
+ console.log(center('╔════════════════════════════════════════════════════════════╗', false));
189
+ console.log(center('║ 🔧 Select Sandbox Provider ║', false));
190
+ console.log(center('╚════════════════════════════════════════════════════════════╝', false));
191
+ console.log('');
192
+
193
+ console.log(center(`\x1b[1mRepository:\x1b[0m \x1b[32m${config.repoUrl}\x1b[0m`, true));
194
+ console.log('');
195
+
196
+ providerOptions.forEach((option, index) => {
197
+ if (index === selectedIndex) {
198
+ console.log(center(`\x1b[1m\x1b[36m❯ ${option.name}\x1b[0m`, true));
199
+ } else {
200
+ console.log(center(` ${option.name}`, false));
201
+ }
202
+ });
203
+
204
+ console.log('');
205
+ console.log(center('\x1b[90mPress Enter to select • Esc to go back\x1b[0m', true));
206
+ }
207
+
208
+ function renderGpuSelection() {
209
+ console.clear();
210
+ console.log(center('╔════════════════════════════════════════════════════════════╗', false));
211
+ console.log(center('║ ⚡ Select GPU Configuration ║', false));
212
+ console.log(center('╚════════════════════════════════════════════════════════════╝', false));
213
+ console.log('');
214
+
215
+ console.log(center(`\x1b[1mRepository:\x1b[0m \x1b[32m${config.repoUrl}\x1b[0m`, true));
216
+ console.log(center(`\x1b[1mProvider:\x1b[0m \x1b[32m${config.sandboxProvider}\x1b[0m`, true));
217
+ console.log('');
218
+
219
+ gpuOptions.forEach((option, index) => {
220
+ if (index === selectedIndex) {
221
+ console.log(center(`\x1b[1m\x1b[36m❯ ${option.name}\x1b[0m`, true));
222
+ } else {
223
+ console.log(center(` ${option.name}`, false));
224
+ }
225
+ });
226
+
227
+ console.log('');
228
+ console.log(center('\x1b[90mPress Enter to select • Esc to go back\x1b[0m', true));
229
+ }
230
+
231
+ function renderGpuCountSelection() {
232
+ console.clear();
233
+ console.log(center('╔════════════════════════════════════════════════════════════╗', false));
234
+ console.log(center('║ ⚡ Select GPU Count ║', false));
235
+ console.log(center('╚════════════════════════════════════════════════════════════╝', false));
236
+ console.log('');
237
+
238
+ console.log(center(`\x1b[1mGPU Type:\x1b[0m \x1b[32m${config.gpuType}\x1b[0m`, true));
239
+ console.log('');
240
+
241
+ gpuCountOptions.forEach((option, index) => {
242
+ if (index === selectedIndex) {
243
+ console.log(center(`\x1b[1m\x1b[36m❯ ${option.name}\x1b[0m`, true));
244
+ } else {
245
+ console.log(center(` ${option.name}`, false));
246
+ }
247
+ });
248
+
249
+ console.log('');
250
+ console.log(center('\x1b[90mPress Enter to select • Esc to go back\x1b[0m', true));
251
+ }
252
+
253
+ function renderConfirmation() {
254
+ console.clear();
255
+ console.log(center('╔════════════════════════════════════════════════════════════╗', false));
256
+ console.log(center('║ 📋 Configuration Summary ║', false));
257
+ console.log(center('╚════════════════════════════════════════════════════════════╝', false));
258
+ console.log('');
259
+
260
+ console.log(center('\x1b[1mRepository URL:\x1b[0m', true));
261
+ console.log(center(`\x1b[36m${config.repoUrl}\x1b[0m`, true));
262
+ console.log('');
263
+
264
+ console.log(center('\x1b[1mSandbox Provider:\x1b[0m', true));
265
+ console.log(center(`\x1b[36m${config.sandboxProvider}\x1b[0m`, true));
266
+ console.log('');
267
+
268
+ if (config.sandboxProvider === 'modal') {
269
+ console.log(center('\x1b[1mGPU Configuration:\x1b[0m', true));
270
+ console.log(center(`\x1b[36m${config.gpuCount > 1 ? config.gpuCount + 'x ' : ''}${config.gpuType}\x1b[0m`, true));
271
+ console.log('');
272
+ }
273
+
274
+ console.log(center('\x1b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m', true));
275
+ console.log('');
276
+
277
+ console.log(center('\x1b[1m\x1b[32m✅ Press Enter to create sandbox in background\x1b[0m', true));
278
+ console.log(center('\x1b[90mPress Esc to go back and change settings\x1b[0m', true));
279
+ }
280
+
281
+ function createSandbox() {
282
+ const sandboxId = sandboxIdCounter++;
283
+ const sandbox = {
284
+ id: sandboxId,
285
+ repo: config.repoUrl.split('/').pop() || config.repoUrl,
286
+ fullRepo: config.repoUrl,
287
+ provider: config.sandboxProvider,
288
+ gpu: config.sandboxProvider === 'modal' ? `${config.gpuCount}x ${config.gpuType}` : null,
289
+ status: 'initializing',
290
+ process: null,
291
+ startTime: new Date()
292
+ };
293
+
294
+ sandboxes.push(sandbox);
295
+
296
+ console.clear();
297
+ console.log('\x1b[1m\x1b[32m🚀 Starting sandbox in background...\x1b[0m\n');
298
+ console.log(`\x1b[36mSandbox ID:\x1b[0m #${sandboxId}`);
299
+ console.log(`\x1b[36mRepository:\x1b[0m ${config.repoUrl}`);
300
+ console.log(`\x1b[36mProvider:\x1b[0m ${config.sandboxProvider}`);
301
+ if (config.sandboxProvider === 'modal') {
302
+ console.log(`\x1b[36mGPU:\x1b[0m ${config.gpuCount}x ${config.gpuType}`);
303
+ }
304
+ console.log();
305
+
306
+ const args = [
307
+ '--yes',
308
+ '--repo', config.repoUrl,
309
+ '--sandbox-provider', config.sandboxProvider
310
+ ];
311
+
312
+ if (config.sandboxProvider === 'modal') {
313
+ args.push('--gpu', config.gpuType);
314
+ args.push('--gpu-count', config.gpuCount.toString());
315
+ }
316
+
317
+ const cliPath = join(__dirname, '..', 'bin', 'gitarsenal.js');
318
+
319
+ // Spawn in background
320
+ const child = spawn('node', [cliPath, ...args], {
321
+ cwd: join(__dirname, '..'),
322
+ detached: true,
323
+ stdio: 'pipe'
324
+ });
325
+
326
+ sandbox.process = child;
327
+
328
+ // Update status based on output
329
+ let output = '';
330
+ child.stdout.on('data', (data) => {
331
+ output += data.toString();
332
+ if (output.includes('Sandbox created') || output.includes('successfully')) {
333
+ sandbox.status = 'running';
334
+ }
335
+ });
336
+
337
+ child.stderr.on('data', (data) => {
338
+ output += data.toString();
339
+ });
340
+
341
+ child.on('close', (code) => {
342
+ if (code === 0) {
343
+ sandbox.status = 'running';
344
+ } else {
345
+ sandbox.status = 'failed';
346
+ }
347
+ });
348
+
349
+ console.log('\x1b[32m✅ Sandbox started in background!\x1b[0m');
350
+ console.log('\x1b[90mCheck status in "View Running Sandboxes"\x1b[0m\n');
351
+ console.log('\x1b[90mPress any key to return to menu...\x1b[0m');
352
+
353
+ currentScreen = 'result';
354
+ }
355
+
356
+ function viewSandboxLogs(sandbox) {
357
+ console.clear();
358
+ console.log(`\x1b[1m\x1b[36m📋 Sandbox #${sandbox.id} - ${sandbox.repo}\x1b[0m\n`);
359
+ console.log(`\x1b[90mStatus: ${sandbox.status} | Provider: ${sandbox.provider}\x1b[0m`);
360
+ console.log('\x1b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m\n');
361
+ console.log('\x1b[33m💡 Press Esc to exit logs and return to sandbox list\x1b[0m');
362
+ console.log('\x1b[33m💡 Press Ctrl+C to exit TUI entirely\x1b[0m\n');
363
+ console.log('\x1b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m\n');
364
+
365
+ if (sandbox.process && !sandbox.process.killed) {
366
+ console.log('\x1b[1mLive Output:\x1b[0m\n');
367
+
368
+ // Switch to raw mode to capture Esc
369
+ process.stdin.setRawMode(true);
370
+
371
+ // Pipe the process output
372
+ sandbox.process.stdout.pipe(process.stdout);
373
+ sandbox.process.stderr.pipe(process.stderr);
374
+
375
+ activeProcess = sandbox.process;
376
+ } else {
377
+ console.log('\x1b[90mNo live output available.\x1b[0m\n');
378
+ console.log('\x1b[90mPress Esc to go back\x1b[0m');
379
+ }
380
+ }
381
+
382
+ function deleteSandbox(index) {
383
+ const sandbox = sandboxes[index];
384
+ if (sandbox.process && !sandbox.process.killed) {
385
+ sandbox.process.kill('SIGTERM');
386
+ }
387
+ sandboxes.splice(index, 1);
388
+
389
+ console.clear();
390
+ console.log('\x1b[32m✅ Sandbox deleted\x1b[0m\n');
391
+ console.log('\x1b[90mPress any key to continue...\x1b[0m');
392
+ currentScreen = 'result-after-delete';
393
+ }
394
+
395
+ function handleMenuSelection() {
396
+ if (selectedIndex === 0) {
397
+ currentScreen = 'repo-input';
398
+ inputText = '';
399
+ selectedIndex = 0;
400
+ renderRepoInput();
401
+ } else if (selectedIndex === 1) {
402
+ currentScreen = 'sandbox-list';
403
+ selectedIndex = 0;
404
+ renderSandboxList();
405
+ } else if (selectedIndex === 2) {
406
+ console.clear();
407
+ console.log('\n\x1b[1m\x1b[33m🔑 API Keys Management\x1b[0m\n');
408
+ console.log('Launching gitarsenal keys command...\n');
409
+ console.log('\x1b[33m💡 Press Ctrl+C to cancel\x1b[0m\n');
410
+
411
+ process.stdin.setRawMode(false);
412
+ const child = spawn('node', [join(__dirname, '..', 'bin', 'gitarsenal.js'), 'keys', 'list'], {
413
+ stdio: 'inherit',
414
+ cwd: join(__dirname, '..')
415
+ });
416
+
417
+ child.on('close', () => {
418
+ console.log('\n\x1b[90mPress any key to return to menu...\x1b[0m');
419
+ process.stdin.setRawMode(true);
420
+ currentScreen = 'result';
421
+ });
422
+ } else if (selectedIndex === menuItems.length - 1) {
423
+ console.clear();
424
+ console.log('\n👋 Goodbye!\n');
425
+ if (sandboxes.length > 0) {
426
+ const count = sandboxes.length;
427
+ const plural = count > 1 ? "es" : "";
428
+ console.log("\x1b[33m⚠️ " + count + " sandbox" + plural + " still running in background\x1b[0m\n"); }
429
+ process.exit(0);
430
+ } else {
431
+ console.clear();
432
+ console.log('\n\x1b[33m⚠️ ' + menuItems[selectedIndex] + '\x1b[0m');
433
+ console.log('\x1b[90mThis feature is coming soon!\x1b[0m');
434
+ console.log('\n\x1b[90mPress any key to return to menu...\x1b[0m');
435
+ currentScreen = 'result';
436
+ }
437
+ }
438
+
439
+ // Initial render
440
+ renderMenu();
441
+
442
+ // Handle keyboard input
443
+ process.stdin.setRawMode(true);
444
+ process.stdin.resume();
445
+ process.stdin.setEncoding('utf8');
446
+
447
+ process.stdin.on('data', (key) => {
448
+ if (key === '\u0003') {
449
+ // Ctrl+C - Kill all sandboxes and exit
450
+ console.clear();
451
+ console.log('\n\x1b[33m⚠️ Stopping all sandboxes...\x1b[0m\n');
452
+ sandboxes.forEach(sandbox => {
453
+ if (sandbox.process && !sandbox.process.killed) {
454
+ sandbox.process.kill('SIGTERM');
455
+ }
456
+ });
457
+ setTimeout(() => {
458
+ console.clear();
459
+ console.log('\n👋 Goodbye!\n');
460
+ process.exit(0);
461
+ }, 500);
462
+ return;
463
+ }
464
+
465
+ if (currentScreen === 'menu') {
466
+ if (key === '\u001b[A') {
467
+ selectedIndex = Math.max(0, selectedIndex - 1);
468
+ renderMenu();
469
+ } else if (key === '\u001b[B') {
470
+ selectedIndex = Math.min(menuItems.length - 1, selectedIndex + 1);
471
+ renderMenu();
472
+ } else if (key === '\r' || key === '\n') {
473
+ handleMenuSelection();
474
+ }
475
+ } else if (currentScreen === 'sandbox-list') {
476
+ if (key === '\u001b') {
477
+ // Esc - back to menu
478
+ currentScreen = 'menu';
479
+ selectedIndex = 0;
480
+ renderMenu();
481
+ } else if (key === '\u001b[A') {
482
+ selectedIndex = Math.max(0, selectedIndex - 1);
483
+ renderSandboxList();
484
+ } else if (key === '\u001b[B') {
485
+ selectedIndex = Math.min(sandboxes.length - 1, selectedIndex + 1);
486
+ renderSandboxList();
487
+ } else if (key === '\r' || key === '\n') {
488
+ if (sandboxes.length > 0) {
489
+ currentScreen = 'viewing-logs';
490
+ viewSandboxLogs(sandboxes[selectedIndex]);
491
+ }
492
+ } else if (key.toLowerCase() === 'd') {
493
+ if (sandboxes.length > 0) {
494
+ deleteSandbox(selectedIndex);
495
+ }
496
+ } else if (key.toLowerCase() === 'r') {
497
+ renderSandboxList();
498
+ }
499
+ } else if (currentScreen === 'viewing-logs') {
500
+ if (key === '\u001b') {
501
+ // Esc - exit logs and return to sandbox list
502
+ if (activeProcess) {
503
+ activeProcess.stdout.unpipe(process.stdout);
504
+ activeProcess.stderr.unpipe(process.stderr);
505
+ activeProcess = null;
506
+ }
507
+ currentScreen = 'sandbox-list';
508
+ renderSandboxList();
509
+ }
510
+ } else if (currentScreen === 'repo-input') {
511
+ if (key === '\u001b') {
512
+ currentScreen = 'menu';
513
+ selectedIndex = 0;
514
+ inputText = '';
515
+ renderMenu();
516
+ } else if (key === '\r' || key === '\n') {
517
+ if (inputText.trim()) {
518
+ config.repoUrl = inputText.trim();
519
+ currentScreen = 'provider-select';
520
+ selectedIndex = 0;
521
+ renderProviderSelection();
522
+ }
523
+ } else if (key === '\u007f' || key === '\b') {
524
+ inputText = inputText.slice(0, -1);
525
+ renderRepoInput();
526
+ } else if (key.charCodeAt(0) >= 32 && key.charCodeAt(0) <= 126) {
527
+ inputText += key;
528
+ renderRepoInput();
529
+ }
530
+ } else if (currentScreen === 'provider-select') {
531
+ if (key === '\u001b') {
532
+ currentScreen = 'repo-input';
533
+ inputText = config.repoUrl;
534
+ renderRepoInput();
535
+ } else if (key === '\u001b[A') {
536
+ selectedIndex = Math.max(0, selectedIndex - 1);
537
+ renderProviderSelection();
538
+ } else if (key === '\u001b[B') {
539
+ selectedIndex = Math.min(providerOptions.length - 1, selectedIndex + 1);
540
+ renderProviderSelection();
541
+ } else if (key === '\r' || key === '\n') {
542
+ config.sandboxProvider = providerOptions[selectedIndex].value;
543
+ if (config.sandboxProvider === 'modal') {
544
+ currentScreen = 'gpu-select';
545
+ selectedIndex = 2;
546
+ renderGpuSelection();
547
+ } else {
548
+ currentScreen = 'confirm';
549
+ renderConfirmation();
550
+ }
551
+ }
552
+ } else if (currentScreen === 'gpu-select') {
553
+ if (key === '\u001b') {
554
+ currentScreen = 'provider-select';
555
+ selectedIndex = 0;
556
+ renderProviderSelection();
557
+ } else if (key === '\u001b[A') {
558
+ selectedIndex = Math.max(0, selectedIndex - 1);
559
+ renderGpuSelection();
560
+ } else if (key === '\u001b[B') {
561
+ selectedIndex = Math.min(gpuOptions.length - 1, selectedIndex + 1);
562
+ renderGpuSelection();
563
+ } else if (key === '\r' || key === '\n') {
564
+ config.gpuType = gpuOptions[selectedIndex].value;
565
+ currentScreen = 'gpu-count';
566
+ selectedIndex = 0;
567
+ renderGpuCountSelection();
568
+ }
569
+ } else if (currentScreen === 'gpu-count') {
570
+ if (key === '\u001b') {
571
+ currentScreen = 'gpu-select';
572
+ selectedIndex = 2;
573
+ renderGpuSelection();
574
+ } else if (key === '\u001b[A') {
575
+ selectedIndex = Math.max(0, selectedIndex - 1);
576
+ renderGpuCountSelection();
577
+ } else if (key === '\u001b[B') {
578
+ selectedIndex = Math.min(gpuCountOptions.length - 1, selectedIndex + 1);
579
+ renderGpuCountSelection();
580
+ } else if (key === '\r' || key === '\n') {
581
+ config.gpuCount = gpuCountOptions[selectedIndex].value;
582
+ currentScreen = 'confirm';
583
+ renderConfirmation();
584
+ }
585
+ } else if (currentScreen === 'confirm') {
586
+ if (key === '\u001b') {
587
+ if (config.sandboxProvider === 'modal') {
588
+ currentScreen = 'gpu-count';
589
+ selectedIndex = 0;
590
+ renderGpuCountSelection();
591
+ } else {
592
+ currentScreen = 'provider-select';
593
+ selectedIndex = 0;
594
+ renderProviderSelection();
595
+ }
596
+ } else if (key === '\r' || key === '\n') {
597
+ createSandbox();
598
+ }
599
+ } else if (currentScreen === 'result' || currentScreen === 'result-after-delete') {
600
+ if (currentScreen === 'result-after-delete') {
601
+ currentScreen = 'sandbox-list';
602
+ renderSandboxList();
603
+ } else {
604
+ currentScreen = 'menu';
605
+ selectedIndex = 0;
606
+ renderMenu();
607
+ }
608
+ }
609
+ });