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.
- package/.venv_status.json +1 -1
- package/README-TYPING.md +91 -0
- package/START-HERE.md +230 -0
- package/bin/gitarsenal-tui.js +147 -0
- package/bin/gitarsenal.js +58 -15
- package/launch-tui.sh +18 -0
- package/package.json +9 -3
- package/scripts/ensure-dependencies.sh +46 -0
- package/scripts/postinstall.js +22 -7
- package/tui/App.jsx +326 -0
- package/tui/index.js +37 -0
- package/tui/simple-test.js +41 -0
- package/tui-app/bun.lock +200 -0
- package/tui-app/index-manual.js.bak +609 -0
- package/tui-app/index.jsx +848 -0
- package/tui-app/package-lock.json +1720 -0
- package/tui-app/package.json +16 -0
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { render, useKeyboard, useTerminalDimensions } from '@opentui/react';
|
|
4
|
+
import { useState, useEffect } from 'react';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { dirname } from 'path';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
|
|
15
|
+
// Helper functions for user credentials
|
|
16
|
+
const getUserConfigPath = () => {
|
|
17
|
+
const userConfigDir = join(os.homedir(), '.gitarsenal');
|
|
18
|
+
const userConfigPath = join(userConfigDir, 'user-config.json');
|
|
19
|
+
return { userConfigDir, userConfigPath };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const loadUserCredentials = () => {
|
|
23
|
+
const { userConfigPath } = getUserConfigPath();
|
|
24
|
+
if (fs.existsSync(userConfigPath)) {
|
|
25
|
+
try {
|
|
26
|
+
const config = JSON.parse(fs.readFileSync(userConfigPath, 'utf8'));
|
|
27
|
+
if (config.userId && config.userName && config.userEmail && !config.userEmail.includes('@example.com')) {
|
|
28
|
+
return config;
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('Could not read user config:', error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const saveUserCredentials = (userId, userName, userEmail) => {
|
|
38
|
+
const { userConfigDir, userConfigPath } = getUserConfigPath();
|
|
39
|
+
try {
|
|
40
|
+
if (!fs.existsSync(userConfigDir)) {
|
|
41
|
+
fs.mkdirSync(userConfigDir, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
const config = {
|
|
44
|
+
userId,
|
|
45
|
+
userName,
|
|
46
|
+
userEmail,
|
|
47
|
+
savedAt: new Date().toISOString()
|
|
48
|
+
};
|
|
49
|
+
fs.writeFileSync(userConfigPath, JSON.stringify(config, null, 2));
|
|
50
|
+
return true;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Could not save credentials:', error);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const menuItems = [
|
|
58
|
+
'Create New Sandbox',
|
|
59
|
+
'View Running Sandboxes',
|
|
60
|
+
'API Keys Management',
|
|
61
|
+
'Settings',
|
|
62
|
+
'Help & Examples',
|
|
63
|
+
'Exit'
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const gpuOptions = [
|
|
67
|
+
{ name: 'T4 (16GB VRAM)', value: 'T4' },
|
|
68
|
+
{ name: 'L4 (24GB VRAM)', value: 'L4' },
|
|
69
|
+
{ name: 'A10G (24GB VRAM)', value: 'A10G' },
|
|
70
|
+
{ name: 'A100-40 (40GB VRAM)', value: 'A100-40GB' },
|
|
71
|
+
{ name: 'A100-80 (80GB VRAM)', value: 'A100-80GB' },
|
|
72
|
+
{ name: 'L40S (48GB VRAM)', value: 'L40S' },
|
|
73
|
+
{ name: 'H100 (80GB VRAM)', value: 'H100' },
|
|
74
|
+
{ name: 'H200 (141GB VRAM)', value: 'H200' },
|
|
75
|
+
{ name: 'B200 (141GB VRAM)', value: 'B200' }
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const gpuCountOptions = [1, 2, 3, 4, 6, 8];
|
|
79
|
+
|
|
80
|
+
const providerOptions = [
|
|
81
|
+
{ name: 'Modal (GPU support, persistent volumes)', value: 'modal' },
|
|
82
|
+
{ name: 'E2B (Faster startup, no GPU)', value: 'e2b' }
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const Banner = () => {
|
|
86
|
+
return (
|
|
87
|
+
<box style={{ flexDirection: 'column', alignItems: 'center', marginBottom: 1 }}>
|
|
88
|
+
<box borderStyle="single" borderColor="green" paddingX={2} paddingY={0}>
|
|
89
|
+
<box style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
90
|
+
<text bold fg="green"> ██████╗ ██╗████████╗ █████╗ ██████╗ ███████╗███████╗███╗ ██╗ █████╗ ██╗ </text>
|
|
91
|
+
<text bold fg="green">██╔════╝ ██║╚══██╔══╝██╔══██╗██╔══██╗██╔════╝██╔════╝████╗ ██║██╔══██╗██║ </text>
|
|
92
|
+
<text bold fg="green">██║ ███╗██║ ██║ ███████║██████╔╝███████╗█████╗ ██╔██╗ ██║███████║██║ </text>
|
|
93
|
+
<text bold fg="green">██║ ██║██║ ██║ ██╔══██║██╔══██╗╚════██║██╔══╝ ██║╚██╗██║██╔══██║██║ </text>
|
|
94
|
+
<text bold fg="green">╚██████╔╝██║ ██║ ██║ ██║██║ ██║███████║███████╗██║ ╚████║██║ ██║███████╗</text>
|
|
95
|
+
<text bold fg="green"> ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝</text>
|
|
96
|
+
<text bold>GPU-Accelerated Development Environments</text>
|
|
97
|
+
</box>
|
|
98
|
+
</box>
|
|
99
|
+
</box>
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const Menu = ({ selectedIndex }) => {
|
|
104
|
+
return (
|
|
105
|
+
<box style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
106
|
+
{menuItems.map((item, index) => (
|
|
107
|
+
<box key={index} marginY={0} marginBottom={0}>
|
|
108
|
+
{index === selectedIndex ? (
|
|
109
|
+
<box borderStyle="single" borderColor="cyan" paddingX={2} paddingY={0} width={36} style={{ justifyContent: 'center' }}>
|
|
110
|
+
<text bold fg="cyan">{item}</text>
|
|
111
|
+
</box>
|
|
112
|
+
) : (
|
|
113
|
+
<box borderStyle="single" borderColor="gray" paddingX={2} paddingY={0} width={36} style={{ justifyContent: 'center' }}>
|
|
114
|
+
<text dimColor>{item}</text>
|
|
115
|
+
</box>
|
|
116
|
+
)}
|
|
117
|
+
</box>
|
|
118
|
+
))}
|
|
119
|
+
<box marginTop={1}>
|
|
120
|
+
<text dimColor fg="gray">Use ↑↓ arrows to navigate, Enter to select, Ctrl+C to exit</text>
|
|
121
|
+
</box>
|
|
122
|
+
</box>
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const RepoInput = ({ value, onInput, onSubmit }) => {
|
|
127
|
+
return (
|
|
128
|
+
<box style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
129
|
+
<box borderStyle="single" padding={1} marginBottom={1}>
|
|
130
|
+
<text bold>Create New Sandbox</text>
|
|
131
|
+
</box>
|
|
132
|
+
<box marginBottom={1}>
|
|
133
|
+
<text bold>Enter GitHub repository URL:</text>
|
|
134
|
+
</box>
|
|
135
|
+
<input
|
|
136
|
+
value={value}
|
|
137
|
+
onInput={onInput}
|
|
138
|
+
onSubmit={onSubmit}
|
|
139
|
+
placeholder="https://github.com/pytorch/examples"
|
|
140
|
+
focused
|
|
141
|
+
width={60}
|
|
142
|
+
/>
|
|
143
|
+
<box marginTop={2} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
144
|
+
<text dimColor fg="gray">Examples:</text>
|
|
145
|
+
<text dimColor fg="gray"> • https://github.com/pytorch/examples</text>
|
|
146
|
+
<text dimColor fg="gray"> • https://github.com/huggingface/transformers</text>
|
|
147
|
+
<text dimColor fg="gray"> • https://github.com/openai/whisper</text>
|
|
148
|
+
</box>
|
|
149
|
+
<box marginTop={1}>
|
|
150
|
+
<text dimColor fg="gray">Press Enter to continue • Esc to go back • Ctrl+C to exit</text>
|
|
151
|
+
</box>
|
|
152
|
+
</box>
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const ProviderSelection = ({ selectedIndex, repoUrl }) => {
|
|
157
|
+
return (
|
|
158
|
+
<box style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
159
|
+
<box borderStyle="single" padding={1} marginBottom={1}>
|
|
160
|
+
<text bold>Select Sandbox Provider</text>
|
|
161
|
+
</box>
|
|
162
|
+
<box marginBottom={1}>
|
|
163
|
+
<text bold>Repository: </text>
|
|
164
|
+
<text fg="green">{repoUrl}</text>
|
|
165
|
+
</box>
|
|
166
|
+
{providerOptions.map((option, index) => (
|
|
167
|
+
<box key={index} marginY={0} marginBottom={0}>
|
|
168
|
+
{index === selectedIndex ? (
|
|
169
|
+
<box borderStyle="single" borderColor="cyan" paddingX={2} paddingY={0} width={50} style={{ justifyContent: 'center' }}>
|
|
170
|
+
<text bold fg="cyan">{option.name}</text>
|
|
171
|
+
</box>
|
|
172
|
+
) : (
|
|
173
|
+
<box borderStyle="single" borderColor="gray" paddingX={2} paddingY={0} width={50} style={{ justifyContent: 'center' }}>
|
|
174
|
+
<text dimColor>{option.name}</text>
|
|
175
|
+
</box>
|
|
176
|
+
)}
|
|
177
|
+
</box>
|
|
178
|
+
))}
|
|
179
|
+
<box marginTop={1}>
|
|
180
|
+
<text dimColor fg="gray">Press Enter to select • Esc to go back</text>
|
|
181
|
+
</box>
|
|
182
|
+
</box>
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const GpuSelection = ({ selectedIndex, repoUrl, provider }) => {
|
|
187
|
+
return (
|
|
188
|
+
<box style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
189
|
+
<box borderStyle="single" padding={1} marginBottom={1}>
|
|
190
|
+
<text bold>Select GPU Configuration</text>
|
|
191
|
+
</box>
|
|
192
|
+
<box marginBottom={1} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
193
|
+
<box><text bold>Repository: </text><text fg="green">{repoUrl}</text></box>
|
|
194
|
+
<box><text bold>Provider: </text><text fg="green">{provider}</text></box>
|
|
195
|
+
</box>
|
|
196
|
+
{gpuOptions.map((option, index) => (
|
|
197
|
+
<box key={index} marginY={0} marginBottom={0}>
|
|
198
|
+
{index === selectedIndex ? (
|
|
199
|
+
<box borderStyle="single" borderColor="cyan" paddingX={2} paddingY={0} width={30} style={{ justifyContent: 'center' }}>
|
|
200
|
+
<text bold fg="cyan">{option.name}</text>
|
|
201
|
+
</box>
|
|
202
|
+
) : (
|
|
203
|
+
<box borderStyle="single" borderColor="gray" paddingX={2} paddingY={0} width={30} style={{ justifyContent: 'center' }}>
|
|
204
|
+
<text dimColor>{option.name}</text>
|
|
205
|
+
</box>
|
|
206
|
+
)}
|
|
207
|
+
</box>
|
|
208
|
+
))}
|
|
209
|
+
<box marginTop={1}>
|
|
210
|
+
<text dimColor fg="gray">Press Enter to select • Esc to go back</text>
|
|
211
|
+
</box>
|
|
212
|
+
</box>
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const GpuCountSelection = ({ selectedIndex, gpuType }) => {
|
|
217
|
+
return (
|
|
218
|
+
<box style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
219
|
+
<box borderStyle="single" padding={1} marginBottom={1}>
|
|
220
|
+
<text bold>Select GPU Count</text>
|
|
221
|
+
</box>
|
|
222
|
+
<box marginBottom={1}>
|
|
223
|
+
<text bold>GPU Type: </text>
|
|
224
|
+
<text fg="green">{gpuType}</text>
|
|
225
|
+
</box>
|
|
226
|
+
{gpuCountOptions.map((count, index) => (
|
|
227
|
+
<box key={index} marginY={0} marginBottom={0}>
|
|
228
|
+
{index === selectedIndex ? (
|
|
229
|
+
<box borderStyle="single" borderColor="cyan" paddingX={2} paddingY={0} width={16} style={{ justifyContent: 'center' }}>
|
|
230
|
+
<text bold fg="cyan">{count} GPU{count > 1 ? 's' : ''}</text>
|
|
231
|
+
</box>
|
|
232
|
+
) : (
|
|
233
|
+
<box borderStyle="single" borderColor="gray" paddingX={2} paddingY={0} width={16} style={{ justifyContent: 'center' }}>
|
|
234
|
+
<text dimColor>{count} GPU{count > 1 ? 's' : ''}</text>
|
|
235
|
+
</box>
|
|
236
|
+
)}
|
|
237
|
+
</box>
|
|
238
|
+
))}
|
|
239
|
+
<box marginTop={1}>
|
|
240
|
+
<text dimColor fg="gray">Press Enter to select • Esc to go back</text>
|
|
241
|
+
</box>
|
|
242
|
+
</box>
|
|
243
|
+
);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const Confirmation = ({ config }) => {
|
|
247
|
+
return (
|
|
248
|
+
<box style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
249
|
+
<box borderStyle="single" padding={1} marginBottom={1}>
|
|
250
|
+
<text bold>Configuration Summary</text>
|
|
251
|
+
</box>
|
|
252
|
+
<box marginBottom={1} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
253
|
+
<text bold>Repository URL:</text>
|
|
254
|
+
<text fg="cyan">{config.repoUrl}</text>
|
|
255
|
+
</box>
|
|
256
|
+
<box marginBottom={1} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
257
|
+
<text bold>Sandbox Provider:</text>
|
|
258
|
+
<text fg="cyan">{config.sandboxProvider}</text>
|
|
259
|
+
</box>
|
|
260
|
+
{config.sandboxProvider === 'modal' && (
|
|
261
|
+
<box marginBottom={1} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
262
|
+
<text bold>GPU Configuration:</text>
|
|
263
|
+
<text fg="cyan">{config.gpuCount > 1 ? config.gpuCount + 'x ' : ''}{config.gpuType}</text>
|
|
264
|
+
</box>
|
|
265
|
+
)}
|
|
266
|
+
<box marginTop={2} marginBottom={1}>
|
|
267
|
+
<text bold fg="green">Press Enter to create sandbox in background</text>
|
|
268
|
+
</box>
|
|
269
|
+
<box>
|
|
270
|
+
<text dimColor fg="gray">Press Esc to go back and change settings</text>
|
|
271
|
+
</box>
|
|
272
|
+
</box>
|
|
273
|
+
);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const AuthChoice = ({ selectedIndex }) => {
|
|
277
|
+
const authOptions = ['Create new account', 'Login with existing account'];
|
|
278
|
+
return (
|
|
279
|
+
<box style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
280
|
+
<box borderStyle="single" padding={1} marginBottom={1}>
|
|
281
|
+
<text bold>GitArsenal Authentication</text>
|
|
282
|
+
</box>
|
|
283
|
+
<box marginBottom={1} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
284
|
+
<text dimColor fg="gray">Create an account or login to use GitArsenal</text>
|
|
285
|
+
<text dimColor fg="gray">Your credentials will be saved locally</text>
|
|
286
|
+
</box>
|
|
287
|
+
{authOptions.map((option, index) => (
|
|
288
|
+
<box key={index} marginY={0} marginBottom={0}>
|
|
289
|
+
{index === selectedIndex ? (
|
|
290
|
+
<box borderStyle="single" borderColor="cyan" paddingX={2} paddingY={0} width={42} style={{ justifyContent: 'center' }}>
|
|
291
|
+
<text bold fg="cyan">{option}</text>
|
|
292
|
+
</box>
|
|
293
|
+
) : (
|
|
294
|
+
<box borderStyle="single" borderColor="gray" paddingX={2} paddingY={0} width={42} style={{ justifyContent: 'center' }}>
|
|
295
|
+
<text dimColor>{option}</text>
|
|
296
|
+
</box>
|
|
297
|
+
)}
|
|
298
|
+
</box>
|
|
299
|
+
))}
|
|
300
|
+
<box marginTop={1}>
|
|
301
|
+
<text dimColor fg="gray">Press Enter to select • Esc to exit</text>
|
|
302
|
+
</box>
|
|
303
|
+
</box>
|
|
304
|
+
);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const LoginForm = ({ values, onInput, onSubmit }) => {
|
|
308
|
+
const fields = ['username', 'email', 'fullName', 'password'];
|
|
309
|
+
const labels = ['Username:', 'Email Address:', 'Full Name:', 'Password:'];
|
|
310
|
+
|
|
311
|
+
return (
|
|
312
|
+
<box style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
313
|
+
<box borderStyle="single" padding={1} marginBottom={1}>
|
|
314
|
+
<text bold>Login</text>
|
|
315
|
+
</box>
|
|
316
|
+
{fields.map((field, index) => (
|
|
317
|
+
<box key={field} marginBottom={1} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
318
|
+
<text bold>{labels[index]}</text>
|
|
319
|
+
<input
|
|
320
|
+
value={values[field] || ''}
|
|
321
|
+
onInput={(value) => onInput(field, value)}
|
|
322
|
+
onSubmit={index === fields.length - 1 ? onSubmit : undefined}
|
|
323
|
+
placeholder={field === 'password' ? '••••••••' : ''}
|
|
324
|
+
focused={index === 0}
|
|
325
|
+
width={50}
|
|
326
|
+
/>
|
|
327
|
+
</box>
|
|
328
|
+
))}
|
|
329
|
+
<box marginTop={1}>
|
|
330
|
+
<text dimColor fg="gray">Fill all fields and press Enter • Esc to go back</text>
|
|
331
|
+
</box>
|
|
332
|
+
</box>
|
|
333
|
+
);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const RegisterForm = ({ values, onInput, onSubmit }) => {
|
|
337
|
+
const fields = ['username', 'email', 'fullName', 'password', 'confirmPassword'];
|
|
338
|
+
const labels = ['Username:', 'Email Address:', 'Full Name:', 'Password (min 8 chars):', 'Confirm Password:'];
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<box style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
342
|
+
<box borderStyle="single" padding={1} marginBottom={1}>
|
|
343
|
+
<text bold>Create New Account</text>
|
|
344
|
+
</box>
|
|
345
|
+
{fields.map((field, index) => (
|
|
346
|
+
<box key={field} marginBottom={1} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
347
|
+
<text bold>{labels[index]}</text>
|
|
348
|
+
<input
|
|
349
|
+
value={values[field] || ''}
|
|
350
|
+
onInput={(value) => onInput(field, value)}
|
|
351
|
+
onSubmit={index === fields.length - 1 ? onSubmit : undefined}
|
|
352
|
+
placeholder={field.includes('password') ? '••••••••' : ''}
|
|
353
|
+
focused={index === 0}
|
|
354
|
+
width={50}
|
|
355
|
+
/>
|
|
356
|
+
</box>
|
|
357
|
+
))}
|
|
358
|
+
<box marginTop={1}>
|
|
359
|
+
<text dimColor fg="gray">Fill all fields and press Enter • Esc to go back</text>
|
|
360
|
+
</box>
|
|
361
|
+
</box>
|
|
362
|
+
);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const SandboxList = ({ sandboxes, selectedIndex }) => {
|
|
366
|
+
const maxVisibleItems = 10;
|
|
367
|
+
const totalSandboxes = sandboxes.length;
|
|
368
|
+
|
|
369
|
+
// Calculate scroll offset to keep selected item in view
|
|
370
|
+
const scrollOffset = Math.max(0, Math.min(
|
|
371
|
+
selectedIndex - Math.floor(maxVisibleItems / 2),
|
|
372
|
+
Math.max(0, totalSandboxes - maxVisibleItems)
|
|
373
|
+
));
|
|
374
|
+
|
|
375
|
+
const visibleSandboxes = sandboxes.slice(scrollOffset, scrollOffset + maxVisibleItems);
|
|
376
|
+
const showScrollIndicator = totalSandboxes > maxVisibleItems;
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
<box style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
380
|
+
<box borderStyle="single" padding={1} marginBottom={1}>
|
|
381
|
+
<text bold>Running Sandboxes</text>
|
|
382
|
+
</box>
|
|
383
|
+
{sandboxes.length === 0 ? (
|
|
384
|
+
<box style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
385
|
+
<text dimColor fg="gray">No sandboxes running.</text>
|
|
386
|
+
<box marginTop={1}>
|
|
387
|
+
<text dimColor fg="gray">Press Esc to go back to menu</text>
|
|
388
|
+
</box>
|
|
389
|
+
</box>
|
|
390
|
+
) : (
|
|
391
|
+
<>
|
|
392
|
+
{showScrollIndicator && (
|
|
393
|
+
<box marginBottom={1}>
|
|
394
|
+
<text dimColor fg="gray">Showing {scrollOffset + 1}-{Math.min(scrollOffset + maxVisibleItems, totalSandboxes)} of {totalSandboxes}</text>
|
|
395
|
+
</box>
|
|
396
|
+
)}
|
|
397
|
+
<box style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
398
|
+
{visibleSandboxes.map((sandbox, visibleIndex) => {
|
|
399
|
+
const actualIndex = scrollOffset + visibleIndex;
|
|
400
|
+
const statusColor = sandbox.status === 'running' ? 'green' :
|
|
401
|
+
sandbox.status === 'initializing' ? 'yellow' : 'red';
|
|
402
|
+
const statusIcon = sandbox.status === 'running' ? '[OK]' :
|
|
403
|
+
sandbox.status === 'initializing' ? '[...]' : '[FAIL]';
|
|
404
|
+
return (
|
|
405
|
+
<box key={sandbox.id} marginY={0} marginBottom={0} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
406
|
+
{actualIndex === selectedIndex ? (
|
|
407
|
+
<box borderStyle="single" borderColor="cyan" paddingX={2} paddingY={0} width={60} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
408
|
+
<text bold fg="cyan">#{sandbox.id} {statusIcon} {sandbox.repo}</text>
|
|
409
|
+
<text fg="cyan">{sandbox.status} | {sandbox.provider} | {sandbox.gpu || 'N/A'}</text>
|
|
410
|
+
</box>
|
|
411
|
+
) : (
|
|
412
|
+
<box borderStyle="single" borderColor="gray" paddingX={2} paddingY={0} width={60} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
413
|
+
<text dimColor>#{sandbox.id} {statusIcon} {sandbox.repo}</text>
|
|
414
|
+
<text dimColor fg={statusColor}>{sandbox.status}</text>
|
|
415
|
+
</box>
|
|
416
|
+
)}
|
|
417
|
+
</box>
|
|
418
|
+
);
|
|
419
|
+
})}
|
|
420
|
+
</box>
|
|
421
|
+
{showScrollIndicator && (
|
|
422
|
+
<box marginTop={1}>
|
|
423
|
+
<text dimColor fg="gray">↑↓ to scroll • More {scrollOffset + maxVisibleItems < totalSandboxes ? '↓' : ''}{scrollOffset > 0 ? '↑' : ''}</text>
|
|
424
|
+
</box>
|
|
425
|
+
)}
|
|
426
|
+
<box marginTop={2} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
427
|
+
<text bold>Controls:</text>
|
|
428
|
+
<text dimColor fg="gray">↑↓ - Navigate • Enter - View logs • D - Delete</text>
|
|
429
|
+
<text dimColor fg="gray">R - Refresh • Esc - Back to menu</text>
|
|
430
|
+
</box>
|
|
431
|
+
</>
|
|
432
|
+
)}
|
|
433
|
+
</box>
|
|
434
|
+
);
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const SandboxLogs = ({ sandbox }) => {
|
|
438
|
+
const allLogLines = (sandbox.logs || 'No logs yet...').split('\n');
|
|
439
|
+
const totalLines = allLogLines.length;
|
|
440
|
+
|
|
441
|
+
return (
|
|
442
|
+
<box style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
443
|
+
<box borderStyle="single" padding={1} marginBottom={1}>
|
|
444
|
+
<text bold>Sandbox #{sandbox.id} Logs - {sandbox.repo}</text>
|
|
445
|
+
</box>
|
|
446
|
+
<box marginBottom={1} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
447
|
+
<box>
|
|
448
|
+
<text>Status: </text>
|
|
449
|
+
<text fg={sandbox.status === 'running' ? 'green' : sandbox.status === 'initializing' ? 'yellow' : 'red'}>
|
|
450
|
+
{sandbox.status}
|
|
451
|
+
</text>
|
|
452
|
+
</box>
|
|
453
|
+
<box>
|
|
454
|
+
<text dimColor fg="gray">Total lines: {totalLines}</text>
|
|
455
|
+
</box>
|
|
456
|
+
</box>
|
|
457
|
+
<scrollbox width={84} height={17} borderStyle="single" stickyScroll="bottom">
|
|
458
|
+
<box style={{ flexDirection: 'column' }}>
|
|
459
|
+
{allLogLines.map((line, index) => (
|
|
460
|
+
<text key={index} dimColor={!line.trim()}>{line || ' '}</text>
|
|
461
|
+
))}
|
|
462
|
+
</box>
|
|
463
|
+
</scrollbox>
|
|
464
|
+
<box marginTop={1} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
465
|
+
<text bold>Controls:</text>
|
|
466
|
+
<text dimColor fg="gray">Mouse Wheel or Arrow Keys - Scroll logs</text>
|
|
467
|
+
<text dimColor fg="gray">Esc - Back to sandbox list</text>
|
|
468
|
+
</box>
|
|
469
|
+
</box>
|
|
470
|
+
);
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
const App = () => {
|
|
474
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
475
|
+
const [screen, setScreen] = useState('menu');
|
|
476
|
+
const [repoUrl, setRepoUrl] = useState('');
|
|
477
|
+
const [config, setConfig] = useState({
|
|
478
|
+
repoUrl: '',
|
|
479
|
+
gpuType: 'A10G',
|
|
480
|
+
gpuCount: 1,
|
|
481
|
+
sandboxProvider: 'modal'
|
|
482
|
+
});
|
|
483
|
+
const [sandboxes, setSandboxes] = useState([]);
|
|
484
|
+
const [sandboxIdCounter, setSandboxIdCounter] = useState(1);
|
|
485
|
+
const [statusMessage, setStatusMessage] = useState('');
|
|
486
|
+
const [viewingSandboxId, setViewingSandboxId] = useState(null);
|
|
487
|
+
const [userCredentials, setUserCredentials] = useState(null);
|
|
488
|
+
const [authFormValues, setAuthFormValues] = useState({});
|
|
489
|
+
|
|
490
|
+
// Load user credentials on mount
|
|
491
|
+
useEffect(() => {
|
|
492
|
+
const credentials = loadUserCredentials();
|
|
493
|
+
if (credentials) {
|
|
494
|
+
setUserCredentials(credentials);
|
|
495
|
+
setStatusMessage(`Welcome back, ${credentials.userName}!`);
|
|
496
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
497
|
+
}
|
|
498
|
+
}, []);
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
const createSandbox = () => {
|
|
502
|
+
const sandboxId = sandboxIdCounter;
|
|
503
|
+
setSandboxIdCounter(prev => prev + 1);
|
|
504
|
+
|
|
505
|
+
const newSandbox = {
|
|
506
|
+
id: sandboxId,
|
|
507
|
+
repo: config.repoUrl.split('/').pop() || config.repoUrl,
|
|
508
|
+
fullRepo: config.repoUrl,
|
|
509
|
+
provider: config.sandboxProvider,
|
|
510
|
+
gpu: config.sandboxProvider === 'modal' ? `${config.gpuCount}x ${config.gpuType}` : null,
|
|
511
|
+
status: 'initializing',
|
|
512
|
+
startTime: new Date(),
|
|
513
|
+
logs: 'Starting sandbox...\n'
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
setSandboxes(prev => [...prev, newSandbox]);
|
|
517
|
+
|
|
518
|
+
const args = [
|
|
519
|
+
'--yes',
|
|
520
|
+
'--repo', config.repoUrl,
|
|
521
|
+
'--sandbox-provider', config.sandboxProvider
|
|
522
|
+
];
|
|
523
|
+
|
|
524
|
+
if (config.sandboxProvider === 'modal') {
|
|
525
|
+
args.push('--gpu', config.gpuType);
|
|
526
|
+
args.push('--gpu-count', config.gpuCount.toString());
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const cliPath = join(__dirname, '..', 'bin', 'gitarsenal.js');
|
|
530
|
+
|
|
531
|
+
const child = spawn('node', [cliPath, ...args], {
|
|
532
|
+
cwd: join(__dirname, '..'),
|
|
533
|
+
detached: true,
|
|
534
|
+
stdio: 'pipe'
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// Store process reference for cleanup
|
|
538
|
+
setSandboxes(prev => prev.map(s =>
|
|
539
|
+
s.id === sandboxId ? { ...s, process: child } : s
|
|
540
|
+
));
|
|
541
|
+
|
|
542
|
+
let output = '';
|
|
543
|
+
child.stdout.on('data', (data) => {
|
|
544
|
+
const text = data.toString();
|
|
545
|
+
output += text;
|
|
546
|
+
|
|
547
|
+
// Append to logs
|
|
548
|
+
setSandboxes(prev => prev.map(s =>
|
|
549
|
+
s.id === sandboxId ? { ...s, logs: (s.logs || '') + text } : s
|
|
550
|
+
));
|
|
551
|
+
|
|
552
|
+
if (output.includes('Sandbox created') || output.includes('successfully')) {
|
|
553
|
+
setSandboxes(prev => prev.map(s =>
|
|
554
|
+
s.id === sandboxId ? { ...s, status: 'running' } : s
|
|
555
|
+
));
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
child.stderr.on('data', (data) => {
|
|
560
|
+
const text = data.toString();
|
|
561
|
+
output += text;
|
|
562
|
+
|
|
563
|
+
// Append to logs
|
|
564
|
+
setSandboxes(prev => prev.map(s =>
|
|
565
|
+
s.id === sandboxId ? { ...s, logs: (s.logs || '') + text } : s
|
|
566
|
+
));
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
child.on('close', (code) => {
|
|
570
|
+
setSandboxes(prev => prev.map(s =>
|
|
571
|
+
s.id === sandboxId ? {
|
|
572
|
+
...s,
|
|
573
|
+
status: code === 0 ? 'running' : 'failed',
|
|
574
|
+
logs: (s.logs || '') + `\n\n[Process exited with code ${code}]`
|
|
575
|
+
} : s
|
|
576
|
+
));
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
setStatusMessage(`Sandbox #${sandboxId} started in background!`);
|
|
580
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
581
|
+
|
|
582
|
+
setScreen('menu');
|
|
583
|
+
setSelectedIndex(0);
|
|
584
|
+
setRepoUrl('');
|
|
585
|
+
setConfig({
|
|
586
|
+
repoUrl: '',
|
|
587
|
+
gpuType: 'A10G',
|
|
588
|
+
gpuCount: 1,
|
|
589
|
+
sandboxProvider: 'modal'
|
|
590
|
+
});
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
useKeyboard((key) => {
|
|
594
|
+
// Always handle Ctrl+C
|
|
595
|
+
if (key.ctrl && key.name === 'c') {
|
|
596
|
+
process.exit(0);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Don't handle keyboard events when input is focused
|
|
600
|
+
if (screen === 'repoInput') {
|
|
601
|
+
if (key.name === 'escape') {
|
|
602
|
+
setScreen('menu');
|
|
603
|
+
setSelectedIndex(0);
|
|
604
|
+
}
|
|
605
|
+
// Let the input component handle all other keys
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (screen === 'menu') {
|
|
610
|
+
if (key.name === 'up') {
|
|
611
|
+
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : menuItems.length - 1));
|
|
612
|
+
} else if (key.name === 'down') {
|
|
613
|
+
setSelectedIndex((prev) => (prev < menuItems.length - 1 ? prev + 1 : 0));
|
|
614
|
+
} else if (key.name === 'return' || key.name === 'enter') {
|
|
615
|
+
if (selectedIndex === 0) {
|
|
616
|
+
setScreen('repoInput');
|
|
617
|
+
setRepoUrl('');
|
|
618
|
+
} else if (selectedIndex === 1) {
|
|
619
|
+
setScreen('sandboxList');
|
|
620
|
+
setSelectedIndex(0);
|
|
621
|
+
} else if (selectedIndex === menuItems.length - 1) {
|
|
622
|
+
process.exit(0);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
} else if (screen === 'providerSelection') {
|
|
626
|
+
if (key.name === 'up') {
|
|
627
|
+
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : providerOptions.length - 1));
|
|
628
|
+
} else if (key.name === 'down') {
|
|
629
|
+
setSelectedIndex((prev) => (prev < providerOptions.length - 1 ? prev + 1 : 0));
|
|
630
|
+
} else if (key.name === 'return' || key.name === 'enter') {
|
|
631
|
+
setConfig(prev => ({ ...prev, sandboxProvider: providerOptions[selectedIndex].value }));
|
|
632
|
+
if (providerOptions[selectedIndex].value === 'modal') {
|
|
633
|
+
setScreen('gpuSelection');
|
|
634
|
+
setSelectedIndex(0);
|
|
635
|
+
} else {
|
|
636
|
+
setScreen('confirmation');
|
|
637
|
+
}
|
|
638
|
+
} else if (key.name === 'escape') {
|
|
639
|
+
setScreen('repoInput');
|
|
640
|
+
}
|
|
641
|
+
} else if (screen === 'gpuSelection') {
|
|
642
|
+
if (key.name === 'up') {
|
|
643
|
+
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : gpuOptions.length - 1));
|
|
644
|
+
} else if (key.name === 'down') {
|
|
645
|
+
setSelectedIndex((prev) => (prev < gpuOptions.length - 1 ? prev + 1 : 0));
|
|
646
|
+
} else if (key.name === 'return' || key.name === 'enter') {
|
|
647
|
+
setConfig(prev => ({ ...prev, gpuType: gpuOptions[selectedIndex].value }));
|
|
648
|
+
setScreen('gpuCountSelection');
|
|
649
|
+
setSelectedIndex(0);
|
|
650
|
+
} else if (key.name === 'escape') {
|
|
651
|
+
setScreen('providerSelection');
|
|
652
|
+
setSelectedIndex(0);
|
|
653
|
+
}
|
|
654
|
+
} else if (screen === 'gpuCountSelection') {
|
|
655
|
+
if (key.name === 'up') {
|
|
656
|
+
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : gpuCountOptions.length - 1));
|
|
657
|
+
} else if (key.name === 'down') {
|
|
658
|
+
setSelectedIndex((prev) => (prev < gpuCountOptions.length - 1 ? prev + 1 : 0));
|
|
659
|
+
} else if (key.name === 'return' || key.name === 'enter') {
|
|
660
|
+
setConfig(prev => ({ ...prev, gpuCount: gpuCountOptions[selectedIndex] }));
|
|
661
|
+
setScreen('confirmation');
|
|
662
|
+
} else if (key.name === 'escape') {
|
|
663
|
+
setScreen('gpuSelection');
|
|
664
|
+
setSelectedIndex(0);
|
|
665
|
+
}
|
|
666
|
+
} else if (screen === 'confirmation') {
|
|
667
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
668
|
+
// Check if user is logged in before creating sandbox
|
|
669
|
+
if (!userCredentials) {
|
|
670
|
+
setScreen('authChoice');
|
|
671
|
+
setSelectedIndex(0);
|
|
672
|
+
} else {
|
|
673
|
+
createSandbox();
|
|
674
|
+
}
|
|
675
|
+
} else if (key.name === 'escape') {
|
|
676
|
+
if (config.sandboxProvider === 'modal') {
|
|
677
|
+
setScreen('gpuCountSelection');
|
|
678
|
+
} else {
|
|
679
|
+
setScreen('providerSelection');
|
|
680
|
+
}
|
|
681
|
+
setSelectedIndex(0);
|
|
682
|
+
}
|
|
683
|
+
} else if (screen === 'authChoice') {
|
|
684
|
+
if (key.name === 'up') {
|
|
685
|
+
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : 1));
|
|
686
|
+
} else if (key.name === 'down') {
|
|
687
|
+
setSelectedIndex((prev) => (prev < 1 ? prev + 1 : 0));
|
|
688
|
+
} else if (key.name === 'return' || key.name === 'enter') {
|
|
689
|
+
if (selectedIndex === 0) {
|
|
690
|
+
setScreen('register');
|
|
691
|
+
setAuthFormValues({});
|
|
692
|
+
} else {
|
|
693
|
+
setScreen('login');
|
|
694
|
+
setAuthFormValues({});
|
|
695
|
+
}
|
|
696
|
+
} else if (key.name === 'escape') {
|
|
697
|
+
process.exit(0);
|
|
698
|
+
}
|
|
699
|
+
} else if (screen === 'login') {
|
|
700
|
+
if (key.name === 'escape') {
|
|
701
|
+
setScreen('authChoice');
|
|
702
|
+
setSelectedIndex(0);
|
|
703
|
+
setAuthFormValues({});
|
|
704
|
+
}
|
|
705
|
+
// Submit is handled by input component's onSubmit
|
|
706
|
+
} else if (screen === 'register') {
|
|
707
|
+
if (key.name === 'escape') {
|
|
708
|
+
setScreen('authChoice');
|
|
709
|
+
setSelectedIndex(0);
|
|
710
|
+
setAuthFormValues({});
|
|
711
|
+
}
|
|
712
|
+
// Submit is handled by input component's onSubmit
|
|
713
|
+
} else if (screen === 'sandboxList') {
|
|
714
|
+
if (key.name === 'up' && sandboxes.length > 0) {
|
|
715
|
+
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : sandboxes.length - 1));
|
|
716
|
+
} else if (key.name === 'down' && sandboxes.length > 0) {
|
|
717
|
+
setSelectedIndex((prev) => (prev < sandboxes.length - 1 ? prev + 1 : 0));
|
|
718
|
+
} else if ((key.name === 'return' || key.name === 'enter') && sandboxes.length > 0) {
|
|
719
|
+
const sandbox = sandboxes[selectedIndex];
|
|
720
|
+
setViewingSandboxId(sandbox.id);
|
|
721
|
+
setScreen('sandboxLogs');
|
|
722
|
+
} else if (key.name === 'escape') {
|
|
723
|
+
setScreen('menu');
|
|
724
|
+
setSelectedIndex(0);
|
|
725
|
+
} else if (key.name === 'd' && sandboxes.length > 0) {
|
|
726
|
+
const sandbox = sandboxes[selectedIndex];
|
|
727
|
+
if (sandbox.process) {
|
|
728
|
+
sandbox.process.kill();
|
|
729
|
+
}
|
|
730
|
+
setSandboxes(prev => prev.filter(s => s.id !== sandbox.id));
|
|
731
|
+
if (selectedIndex >= sandboxes.length - 1) {
|
|
732
|
+
setSelectedIndex(Math.max(0, sandboxes.length - 2));
|
|
733
|
+
}
|
|
734
|
+
} else if (key.name === 'r') {
|
|
735
|
+
// Refresh - just re-render
|
|
736
|
+
}
|
|
737
|
+
} else if (screen === 'sandboxLogs') {
|
|
738
|
+
if (key.name === 'escape') {
|
|
739
|
+
setViewingSandboxId(null);
|
|
740
|
+
setScreen('sandboxList');
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
const handleRepoInputChange = (value) => {
|
|
746
|
+
setRepoUrl(value);
|
|
747
|
+
setConfig(prev => ({ ...prev, repoUrl: value }));
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
const handleRepoInputSubmit = () => {
|
|
751
|
+
if (repoUrl.trim()) {
|
|
752
|
+
setScreen('providerSelection');
|
|
753
|
+
setSelectedIndex(0);
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
const handleAuthFormInput = (field, value) => {
|
|
758
|
+
setAuthFormValues(prev => ({ ...prev, [field]: value }));
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
const handleLoginSubmit = () => {
|
|
762
|
+
const { username, email, fullName, password } = authFormValues;
|
|
763
|
+
if (username && email && fullName && password) {
|
|
764
|
+
// Save credentials
|
|
765
|
+
const saved = saveUserCredentials(username, fullName, email);
|
|
766
|
+
if (saved) {
|
|
767
|
+
setUserCredentials({ userId: username, userName: fullName, userEmail: email });
|
|
768
|
+
setStatusMessage(`Welcome, ${fullName}!`);
|
|
769
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
770
|
+
createSandbox();
|
|
771
|
+
} else {
|
|
772
|
+
setStatusMessage('Failed to save credentials');
|
|
773
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
const handleRegisterSubmit = () => {
|
|
779
|
+
const { username, email, fullName, password, confirmPassword } = authFormValues;
|
|
780
|
+
|
|
781
|
+
// Validation
|
|
782
|
+
if (!username || username.length < 3) {
|
|
783
|
+
setStatusMessage('Username must be at least 3 characters');
|
|
784
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
788
|
+
setStatusMessage('Please enter a valid email address');
|
|
789
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (!fullName) {
|
|
793
|
+
setStatusMessage('Full name is required');
|
|
794
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
if (!password || password.length < 8) {
|
|
798
|
+
setStatusMessage('Password must be at least 8 characters');
|
|
799
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
if (password !== confirmPassword) {
|
|
803
|
+
setStatusMessage('Passwords do not match');
|
|
804
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Save credentials
|
|
809
|
+
const saved = saveUserCredentials(username, fullName, email);
|
|
810
|
+
if (saved) {
|
|
811
|
+
setUserCredentials({ userId: username, userName: fullName, userEmail: email });
|
|
812
|
+
setStatusMessage(`Account created! Welcome, ${fullName}!`);
|
|
813
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
814
|
+
createSandbox();
|
|
815
|
+
} else {
|
|
816
|
+
setStatusMessage('Failed to save credentials');
|
|
817
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
const viewingSandbox = viewingSandboxId ? sandboxes.find(s => s.id === viewingSandboxId) : null;
|
|
822
|
+
|
|
823
|
+
return (
|
|
824
|
+
<box style={{ flexDirection: 'column', padding: 1, width: '100%' }}>
|
|
825
|
+
<Banner />
|
|
826
|
+
{statusMessage && (
|
|
827
|
+
<box marginBottom={1} style={{ flexDirection: 'row', justifyContent: 'center' }}>
|
|
828
|
+
<text fg="green">{statusMessage}</text>
|
|
829
|
+
</box>
|
|
830
|
+
)}
|
|
831
|
+
{screen === 'menu' && <Menu selectedIndex={selectedIndex} />}
|
|
832
|
+
{screen === 'repoInput' && <RepoInput value={repoUrl} onInput={handleRepoInputChange} onSubmit={handleRepoInputSubmit} />}
|
|
833
|
+
{screen === 'providerSelection' && <ProviderSelection selectedIndex={selectedIndex} repoUrl={config.repoUrl} />}
|
|
834
|
+
{screen === 'gpuSelection' && <GpuSelection selectedIndex={selectedIndex} repoUrl={config.repoUrl} provider={config.sandboxProvider} />}
|
|
835
|
+
{screen === 'gpuCountSelection' && <GpuCountSelection selectedIndex={selectedIndex} gpuType={config.gpuType} />}
|
|
836
|
+
{screen === 'confirmation' && <Confirmation config={config} />}
|
|
837
|
+
{screen === 'authChoice' && <AuthChoice selectedIndex={selectedIndex} />}
|
|
838
|
+
{screen === 'login' && <LoginForm values={authFormValues} onInput={handleAuthFormInput} onSubmit={handleLoginSubmit} />}
|
|
839
|
+
{screen === 'register' && <RegisterForm values={authFormValues} onInput={handleAuthFormInput} onSubmit={handleRegisterSubmit} />}
|
|
840
|
+
{screen === 'sandboxList' && <SandboxList sandboxes={sandboxes} selectedIndex={selectedIndex} />}
|
|
841
|
+
{screen === 'sandboxLogs' && viewingSandbox && <SandboxLogs sandbox={viewingSandbox} />}
|
|
842
|
+
</box>
|
|
843
|
+
);
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
render(<App />, {
|
|
847
|
+
enableMouse: true
|
|
848
|
+
});
|