gitarsenal-cli 1.9.107 → 1.9.111
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/package.json +3 -2
- package/scripts/ensure-dependencies.sh +46 -0
- package/scripts/postinstall.js +22 -7
- package/tui-app/index.jsx +570 -9
package/.venv_status.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"created":"2025-10-
|
|
1
|
+
{"created":"2025-10-15T13:59:40.768Z","packages":["modal","gitingest","requests","anthropic"],"uv_version":"uv 0.8.4 (Homebrew 2025-07-30)"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitarsenal-cli",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.111",
|
|
4
4
|
"description": "CLI tool for creating Modal sandboxes with GitHub repositories",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"gitarsenal-tui": "./bin/gitarsenal-tui.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"postinstall": "node scripts/postinstall.js"
|
|
11
|
+
"postinstall": "node scripts/postinstall.js",
|
|
12
|
+
"ensure-deps": "bash scripts/ensure-dependencies.sh"
|
|
12
13
|
},
|
|
13
14
|
"keywords": [
|
|
14
15
|
"modal",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Ensure all Python dependencies are installed in the virtual environment
|
|
3
|
+
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
PACKAGE_DIR="$(dirname "$SCRIPT_DIR")"
|
|
8
|
+
|
|
9
|
+
echo "📦 Ensuring Python dependencies are installed..."
|
|
10
|
+
echo "📁 Package directory: $PACKAGE_DIR"
|
|
11
|
+
|
|
12
|
+
# Check if virtual environment exists
|
|
13
|
+
if [ ! -d "$PACKAGE_DIR/.venv" ]; then
|
|
14
|
+
echo "❌ Virtual environment not found. Please run: npm install"
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# Activate virtual environment
|
|
19
|
+
source "$PACKAGE_DIR/.venv/bin/activate"
|
|
20
|
+
|
|
21
|
+
# Check if uv is available
|
|
22
|
+
if ! command -v uv &> /dev/null; then
|
|
23
|
+
echo "❌ uv is not installed. Please install it first."
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Install from requirements.txt
|
|
28
|
+
if [ -f "$PACKAGE_DIR/python/requirements.txt" ]; then
|
|
29
|
+
echo "📦 Installing packages from requirements.txt..."
|
|
30
|
+
uv pip install -r "$PACKAGE_DIR/python/requirements.txt"
|
|
31
|
+
else
|
|
32
|
+
echo "📦 Installing core packages..."
|
|
33
|
+
uv pip install modal requests pathlib python-dotenv flask flask-cors pexpect anthropic gitingest exa-py e2b-code-interpreter
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Verify critical packages
|
|
37
|
+
echo ""
|
|
38
|
+
echo "🔍 Verifying installations..."
|
|
39
|
+
python -c "import modal; print('✅ Modal installed')"
|
|
40
|
+
python -c "import e2b_code_interpreter; print('✅ E2B Code Interpreter installed')"
|
|
41
|
+
python -c "import gitingest; print('✅ Gitingest installed')"
|
|
42
|
+
python -c "import anthropic; print('✅ Anthropic installed')"
|
|
43
|
+
|
|
44
|
+
echo ""
|
|
45
|
+
echo "✅ All dependencies installed successfully!"
|
|
46
|
+
|
package/scripts/postinstall.js
CHANGED
|
@@ -138,21 +138,37 @@ async function createVirtualEnvironment() {
|
|
|
138
138
|
|
|
139
139
|
console.log(chalk.gray(`🔄 Installing packages in virtual environment with uv...`));
|
|
140
140
|
|
|
141
|
+
// Determine the activation command based on platform
|
|
142
|
+
const isWindows = process.platform === 'win32';
|
|
143
|
+
const activateCmd = isWindows ?
|
|
144
|
+
'call .venv\\Scripts\\activate.bat && ' :
|
|
145
|
+
'source .venv/bin/activate && ';
|
|
146
|
+
|
|
141
147
|
// Install packages using uv pip from requirements.txt
|
|
142
148
|
const requirementsPath = path.join(packageDir, 'python', 'requirements.txt');
|
|
143
149
|
if (await fs.pathExists(requirementsPath)) {
|
|
144
150
|
console.log(chalk.gray(`📦 Installing packages from requirements.txt...`));
|
|
145
|
-
|
|
151
|
+
const installCmd = isWindows ?
|
|
152
|
+
`${activateCmd}uv pip install -r ${requirementsPath}` :
|
|
153
|
+
`bash -c "${activateCmd}uv pip install -r ${requirementsPath}"`;
|
|
154
|
+
|
|
155
|
+
await execAsync(installCmd, {
|
|
146
156
|
cwd: packageDir,
|
|
147
157
|
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
|
148
|
-
stdio: 'inherit'
|
|
158
|
+
stdio: 'inherit',
|
|
159
|
+
shell: isWindows ? 'cmd.exe' : '/bin/bash'
|
|
149
160
|
});
|
|
150
161
|
} else {
|
|
151
162
|
console.log(chalk.gray(`📦 Installing packages: ${packages.join(', ')}`));
|
|
152
|
-
|
|
163
|
+
const installCmd = isWindows ?
|
|
164
|
+
`${activateCmd}uv pip install ${packages.join(' ')}` :
|
|
165
|
+
`bash -c "${activateCmd}uv pip install ${packages.join(' ')}"`;
|
|
166
|
+
|
|
167
|
+
await execAsync(installCmd, {
|
|
153
168
|
cwd: packageDir,
|
|
154
169
|
env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
|
|
155
|
-
stdio: 'inherit'
|
|
170
|
+
stdio: 'inherit',
|
|
171
|
+
shell: isWindows ? 'cmd.exe' : '/bin/bash'
|
|
156
172
|
});
|
|
157
173
|
}
|
|
158
174
|
|
|
@@ -160,7 +176,7 @@ async function createVirtualEnvironment() {
|
|
|
160
176
|
|
|
161
177
|
// Verify packages are installed
|
|
162
178
|
const pythonPath = path.join(venvPath, 'bin', 'python');
|
|
163
|
-
const packagesToVerify = [...packages, 'anthropic']; // Ensure anthropic
|
|
179
|
+
const packagesToVerify = [...packages, 'anthropic', 'e2b_code_interpreter']; // Ensure anthropic and e2b are checked
|
|
164
180
|
for (const pkg of packagesToVerify) {
|
|
165
181
|
try {
|
|
166
182
|
await execAsync(`${pythonPath} -c "import ${pkg}; print('${pkg} imported successfully')"`);
|
|
@@ -171,8 +187,6 @@ async function createVirtualEnvironment() {
|
|
|
171
187
|
}
|
|
172
188
|
|
|
173
189
|
// Create a script to activate the virtual environment
|
|
174
|
-
const isWindows = process.platform === 'win32';
|
|
175
|
-
|
|
176
190
|
const activateScript = isWindows ?
|
|
177
191
|
`@echo off
|
|
178
192
|
cd /d "%~dp0"
|
|
@@ -391,6 +405,7 @@ if __name__ == "__main__":
|
|
|
391
405
|
• GitArsenal CLI (npm package)
|
|
392
406
|
• Virtual environment with Python packages:
|
|
393
407
|
- Modal
|
|
408
|
+
- E2B Code Interpreter
|
|
394
409
|
- GitIngest
|
|
395
410
|
- Requests
|
|
396
411
|
- Anthropic (for Claude fallback)
|
package/tui-app/index.jsx
CHANGED
|
@@ -6,10 +6,119 @@ import { spawn } from 'child_process';
|
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import { dirname } from 'path';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import os from 'os';
|
|
9
11
|
|
|
10
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
13
|
const __dirname = dirname(__filename);
|
|
12
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
|
+
// Helper functions for API keys management
|
|
58
|
+
// Uses the same storage format as gitarsenal-cli Python CredentialsManager
|
|
59
|
+
const getCredentialsPath = () => {
|
|
60
|
+
const configDir = join(os.homedir(), '.gitarsenal');
|
|
61
|
+
const credentialsPath = join(configDir, 'credentials.json');
|
|
62
|
+
return { configDir, credentialsPath };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const loadApiKeys = () => {
|
|
66
|
+
const { credentialsPath } = getCredentialsPath();
|
|
67
|
+
if (fs.existsSync(credentialsPath)) {
|
|
68
|
+
try {
|
|
69
|
+
const credentials = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'));
|
|
70
|
+
return credentials;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error('Could not read credentials:', error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const saveApiKey = (serviceName, apiKey) => {
|
|
79
|
+
const { configDir, credentialsPath } = getCredentialsPath();
|
|
80
|
+
try {
|
|
81
|
+
if (!fs.existsSync(configDir)) {
|
|
82
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Load existing credentials
|
|
86
|
+
let credentials = loadApiKeys();
|
|
87
|
+
|
|
88
|
+
// Update or delete the key
|
|
89
|
+
if (apiKey && apiKey.trim()) {
|
|
90
|
+
credentials[serviceName] = apiKey.trim();
|
|
91
|
+
} else {
|
|
92
|
+
delete credentials[serviceName];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fs.writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2));
|
|
96
|
+
|
|
97
|
+
// Set restrictive permissions on Unix-like systems
|
|
98
|
+
if (process.platform !== 'win32') {
|
|
99
|
+
fs.chmodSync(credentialsPath, 0o600);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return true;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('Could not save credentials:', error);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const deleteApiKey = (serviceName) => {
|
|
110
|
+
const { credentialsPath } = getCredentialsPath();
|
|
111
|
+
try {
|
|
112
|
+
let credentials = loadApiKeys();
|
|
113
|
+
delete credentials[serviceName];
|
|
114
|
+
fs.writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2));
|
|
115
|
+
return true;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('Could not delete credential:', error);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
13
122
|
const menuItems = [
|
|
14
123
|
'Create New Sandbox',
|
|
15
124
|
'View Running Sandboxes',
|
|
@@ -229,6 +338,215 @@ const Confirmation = ({ config }) => {
|
|
|
229
338
|
);
|
|
230
339
|
};
|
|
231
340
|
|
|
341
|
+
const AuthChoice = ({ selectedIndex }) => {
|
|
342
|
+
const authOptions = ['Create new account', 'Login with existing account'];
|
|
343
|
+
return (
|
|
344
|
+
<box style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
345
|
+
<box borderStyle="single" padding={1} marginBottom={1}>
|
|
346
|
+
<text bold>GitArsenal Authentication</text>
|
|
347
|
+
</box>
|
|
348
|
+
<box marginBottom={1} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
349
|
+
<text dimColor fg="gray">Create an account or login to use GitArsenal</text>
|
|
350
|
+
<text dimColor fg="gray">Your credentials will be saved locally</text>
|
|
351
|
+
</box>
|
|
352
|
+
{authOptions.map((option, index) => (
|
|
353
|
+
<box key={index} marginY={0} marginBottom={0}>
|
|
354
|
+
{index === selectedIndex ? (
|
|
355
|
+
<box borderStyle="single" borderColor="cyan" paddingX={2} paddingY={0} width={42} style={{ justifyContent: 'center' }}>
|
|
356
|
+
<text bold fg="cyan">{option}</text>
|
|
357
|
+
</box>
|
|
358
|
+
) : (
|
|
359
|
+
<box borderStyle="single" borderColor="gray" paddingX={2} paddingY={0} width={42} style={{ justifyContent: 'center' }}>
|
|
360
|
+
<text dimColor>{option}</text>
|
|
361
|
+
</box>
|
|
362
|
+
)}
|
|
363
|
+
</box>
|
|
364
|
+
))}
|
|
365
|
+
<box marginTop={1}>
|
|
366
|
+
<text dimColor fg="gray">Press Enter to select • Esc to exit</text>
|
|
367
|
+
</box>
|
|
368
|
+
</box>
|
|
369
|
+
);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const LoginForm = ({ values, onInput, onSubmit }) => {
|
|
373
|
+
const fields = ['username', 'email', 'fullName', 'password'];
|
|
374
|
+
const labels = ['Username:', 'Email Address:', 'Full Name:', 'Password:'];
|
|
375
|
+
|
|
376
|
+
return (
|
|
377
|
+
<box style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
378
|
+
<box borderStyle="single" padding={1} marginBottom={1}>
|
|
379
|
+
<text bold>Login</text>
|
|
380
|
+
</box>
|
|
381
|
+
{fields.map((field, index) => (
|
|
382
|
+
<box key={field} marginBottom={1} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
383
|
+
<text bold>{labels[index]}</text>
|
|
384
|
+
<input
|
|
385
|
+
value={values[field] || ''}
|
|
386
|
+
onInput={(value) => onInput(field, value)}
|
|
387
|
+
onSubmit={index === fields.length - 1 ? onSubmit : undefined}
|
|
388
|
+
placeholder={field === 'password' ? '••••••••' : ''}
|
|
389
|
+
focused={index === 0}
|
|
390
|
+
width={50}
|
|
391
|
+
/>
|
|
392
|
+
</box>
|
|
393
|
+
))}
|
|
394
|
+
<box marginTop={1}>
|
|
395
|
+
<text dimColor fg="gray">Fill all fields and press Enter • Esc to go back</text>
|
|
396
|
+
</box>
|
|
397
|
+
</box>
|
|
398
|
+
);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const RegisterForm = ({ values, onInput, onSubmit }) => {
|
|
402
|
+
const fields = ['username', 'email', 'fullName', 'password', 'confirmPassword'];
|
|
403
|
+
const labels = ['Username:', 'Email Address:', 'Full Name:', 'Password (min 8 chars):', 'Confirm Password:'];
|
|
404
|
+
|
|
405
|
+
return (
|
|
406
|
+
<box style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
407
|
+
<box borderStyle="single" padding={1} marginBottom={1}>
|
|
408
|
+
<text bold>Create New Account</text>
|
|
409
|
+
</box>
|
|
410
|
+
{fields.map((field, index) => (
|
|
411
|
+
<box key={field} marginBottom={1} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
412
|
+
<text bold>{labels[index]}</text>
|
|
413
|
+
<input
|
|
414
|
+
value={values[field] || ''}
|
|
415
|
+
onInput={(value) => onInput(field, value)}
|
|
416
|
+
onSubmit={index === fields.length - 1 ? onSubmit : undefined}
|
|
417
|
+
placeholder={field.includes('password') ? '••••••••' : ''}
|
|
418
|
+
focused={index === 0}
|
|
419
|
+
width={50}
|
|
420
|
+
/>
|
|
421
|
+
</box>
|
|
422
|
+
))}
|
|
423
|
+
<box marginTop={1}>
|
|
424
|
+
<text dimColor fg="gray">Fill all fields and press Enter • Esc to go back</text>
|
|
425
|
+
</box>
|
|
426
|
+
</box>
|
|
427
|
+
);
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const ApiKeysManagement = ({ apiKeys, selectedIndex }) => {
|
|
431
|
+
// Get all stored keys as an array
|
|
432
|
+
const storedKeyNames = Object.keys(apiKeys).sort();
|
|
433
|
+
const hasStoredKeys = storedKeyNames.length > 0;
|
|
434
|
+
|
|
435
|
+
return (
|
|
436
|
+
<box style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
437
|
+
<box borderStyle="single" padding={1} marginBottom={1}>
|
|
438
|
+
<text bold>API Keys Management</text>
|
|
439
|
+
</box>
|
|
440
|
+
<box marginBottom={2} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
441
|
+
<text dimColor fg="gray">Universal support for any service API key</text>
|
|
442
|
+
{hasStoredKeys ? (
|
|
443
|
+
<text fg="green">{storedKeyNames.length} key{storedKeyNames.length > 1 ? 's' : ''} stored</text>
|
|
444
|
+
) : (
|
|
445
|
+
<text dimColor fg="gray">No API keys stored yet</text>
|
|
446
|
+
)}
|
|
447
|
+
</box>
|
|
448
|
+
|
|
449
|
+
{/* Add New button */}
|
|
450
|
+
<box marginY={0} marginBottom={1}>
|
|
451
|
+
{selectedIndex === 0 ? (
|
|
452
|
+
<box borderStyle="single" borderColor="cyan" paddingX={2} paddingY={0} width={54} style={{ justifyContent: 'center' }}>
|
|
453
|
+
<text bold fg="cyan">+ Add New API Key</text>
|
|
454
|
+
</box>
|
|
455
|
+
) : (
|
|
456
|
+
<box borderStyle="single" borderColor="gray" paddingX={2} paddingY={0} width={54} style={{ justifyContent: 'center' }}>
|
|
457
|
+
<text dimColor>+ Add New API Key</text>
|
|
458
|
+
</box>
|
|
459
|
+
)}
|
|
460
|
+
</box>
|
|
461
|
+
|
|
462
|
+
{/* Stored keys */}
|
|
463
|
+
{storedKeyNames.map((serviceName, index) => {
|
|
464
|
+
const itemIndex = index + 1; // +1 because "Add New" is at index 0
|
|
465
|
+
return (
|
|
466
|
+
<box key={serviceName} marginY={0} marginBottom={0}>
|
|
467
|
+
{itemIndex === selectedIndex ? (
|
|
468
|
+
<box borderStyle="single" borderColor="cyan" paddingX={2} paddingY={0} width={54} style={{ justifyContent: 'center' }}>
|
|
469
|
+
<text bold fg="cyan">{serviceName}</text>
|
|
470
|
+
</box>
|
|
471
|
+
) : (
|
|
472
|
+
<box borderStyle="single" borderColor="gray" paddingX={2} paddingY={0} width={54} style={{ justifyContent: 'center' }}>
|
|
473
|
+
<text dimColor>{serviceName}</text>
|
|
474
|
+
</box>
|
|
475
|
+
)}
|
|
476
|
+
</box>
|
|
477
|
+
);
|
|
478
|
+
})}
|
|
479
|
+
|
|
480
|
+
<box marginTop={2} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
481
|
+
<text bold>Controls:</text>
|
|
482
|
+
<text dimColor fg="gray">↑↓ - Navigate • Enter - {selectedIndex === 0 ? 'Add' : 'Modify'}</text>
|
|
483
|
+
{selectedIndex > 0 && <text dimColor fg="gray">D - Delete selected key</text>}
|
|
484
|
+
<text dimColor fg="gray">Esc - Back to menu</text>
|
|
485
|
+
</box>
|
|
486
|
+
</box>
|
|
487
|
+
);
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const ApiKeyForm = ({ serviceName, apiKey, onServiceInput, onKeyInput, onSubmit, isEditing }) => {
|
|
491
|
+
return (
|
|
492
|
+
<box style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
493
|
+
<box borderStyle="single" padding={1} marginBottom={1}>
|
|
494
|
+
<text bold>{isEditing ? 'Modify' : 'Add'} API Key</text>
|
|
495
|
+
</box>
|
|
496
|
+
<box marginBottom={2} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
497
|
+
{isEditing ? (
|
|
498
|
+
<>
|
|
499
|
+
<text fg="green">Modifying existing API key</text>
|
|
500
|
+
<text dimColor fg="gray">Update the API key value below</text>
|
|
501
|
+
</>
|
|
502
|
+
) : (
|
|
503
|
+
<>
|
|
504
|
+
<text dimColor fg="gray">Add API key for any service</text>
|
|
505
|
+
<text dimColor fg="gray">Examples: openai_api_key, WANDB_API_KEY, modal_token</text>
|
|
506
|
+
</>
|
|
507
|
+
)}
|
|
508
|
+
</box>
|
|
509
|
+
|
|
510
|
+
{!isEditing && (
|
|
511
|
+
<box marginBottom={2} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
512
|
+
<text bold>Service Name:</text>
|
|
513
|
+
<input
|
|
514
|
+
value={serviceName || ''}
|
|
515
|
+
onInput={onServiceInput}
|
|
516
|
+
placeholder="e.g., openai_api_key"
|
|
517
|
+
focused={true}
|
|
518
|
+
width={70}
|
|
519
|
+
/>
|
|
520
|
+
</box>
|
|
521
|
+
)}
|
|
522
|
+
|
|
523
|
+
{isEditing && (
|
|
524
|
+
<box marginBottom={2} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
525
|
+
<text bold>Service:</text>
|
|
526
|
+
<text fg="cyan">{serviceName}</text>
|
|
527
|
+
</box>
|
|
528
|
+
)}
|
|
529
|
+
|
|
530
|
+
<box marginBottom={1} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
531
|
+
<text bold>API Key:</text>
|
|
532
|
+
<input
|
|
533
|
+
value={apiKey || ''}
|
|
534
|
+
onInput={onKeyInput}
|
|
535
|
+
onSubmit={onSubmit}
|
|
536
|
+
placeholder="Paste your API key here..."
|
|
537
|
+
focused={isEditing}
|
|
538
|
+
width={70}
|
|
539
|
+
/>
|
|
540
|
+
</box>
|
|
541
|
+
|
|
542
|
+
<box marginTop={1} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
543
|
+
<text dimColor fg="gray">Enter to save • Esc to cancel</text>
|
|
544
|
+
{isEditing && <text dimColor fg="gray">Clear field to remove the key</text>}
|
|
545
|
+
</box>
|
|
546
|
+
</box>
|
|
547
|
+
);
|
|
548
|
+
};
|
|
549
|
+
|
|
232
550
|
const SandboxList = ({ sandboxes, selectedIndex }) => {
|
|
233
551
|
const maxVisibleItems = 10;
|
|
234
552
|
const totalSandboxes = sandboxes.length;
|
|
@@ -292,11 +610,8 @@ const SandboxList = ({ sandboxes, selectedIndex }) => {
|
|
|
292
610
|
)}
|
|
293
611
|
<box marginTop={2} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
294
612
|
<text bold>Controls:</text>
|
|
295
|
-
<
|
|
296
|
-
<
|
|
297
|
-
<box><text fg="cyan">D</text><text> - Delete sandbox</text></box>
|
|
298
|
-
<box><text fg="cyan">R</text><text> - Refresh list</text></box>
|
|
299
|
-
<box><text fg="cyan">Esc</text><text> - Back to menu</text></box>
|
|
613
|
+
<text dimColor fg="gray">↑↓ - Navigate • Enter - View logs • D - Delete</text>
|
|
614
|
+
<text dimColor fg="gray">R - Refresh • Esc - Back to menu</text>
|
|
300
615
|
</box>
|
|
301
616
|
</>
|
|
302
617
|
)}
|
|
@@ -333,8 +648,8 @@ const SandboxLogs = ({ sandbox }) => {
|
|
|
333
648
|
</scrollbox>
|
|
334
649
|
<box marginTop={1} style={{ flexDirection: 'column', alignItems: 'center' }}>
|
|
335
650
|
<text bold>Controls:</text>
|
|
336
|
-
<
|
|
337
|
-
<
|
|
651
|
+
<text dimColor fg="gray">Mouse Wheel or Arrow Keys - Scroll logs</text>
|
|
652
|
+
<text dimColor fg="gray">Esc - Back to sandbox list</text>
|
|
338
653
|
</box>
|
|
339
654
|
</box>
|
|
340
655
|
);
|
|
@@ -354,6 +669,29 @@ const App = () => {
|
|
|
354
669
|
const [sandboxIdCounter, setSandboxIdCounter] = useState(1);
|
|
355
670
|
const [statusMessage, setStatusMessage] = useState('');
|
|
356
671
|
const [viewingSandboxId, setViewingSandboxId] = useState(null);
|
|
672
|
+
const [userCredentials, setUserCredentials] = useState(null);
|
|
673
|
+
const [authFormValues, setAuthFormValues] = useState({});
|
|
674
|
+
const [apiKeys, setApiKeys] = useState({});
|
|
675
|
+
const [editingServiceName, setEditingServiceName] = useState('');
|
|
676
|
+
const [serviceNameInput, setServiceNameInput] = useState('');
|
|
677
|
+
const [apiKeyInput, setApiKeyInput] = useState('');
|
|
678
|
+
const [isEditingExisting, setIsEditingExisting] = useState(false);
|
|
679
|
+
|
|
680
|
+
// Load user credentials on mount
|
|
681
|
+
useEffect(() => {
|
|
682
|
+
const credentials = loadUserCredentials();
|
|
683
|
+
if (credentials) {
|
|
684
|
+
setUserCredentials(credentials);
|
|
685
|
+
setStatusMessage(`Welcome back, ${credentials.userName}!`);
|
|
686
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
687
|
+
}
|
|
688
|
+
}, []);
|
|
689
|
+
|
|
690
|
+
// Load API keys on mount
|
|
691
|
+
useEffect(() => {
|
|
692
|
+
const keys = loadApiKeys();
|
|
693
|
+
setApiKeys(keys);
|
|
694
|
+
}, []);
|
|
357
695
|
|
|
358
696
|
|
|
359
697
|
const createSandbox = () => {
|
|
@@ -464,6 +802,19 @@ const App = () => {
|
|
|
464
802
|
return;
|
|
465
803
|
}
|
|
466
804
|
|
|
805
|
+
if (screen === 'apiKeyForm') {
|
|
806
|
+
if (key.name === 'escape') {
|
|
807
|
+
setEditingServiceName('');
|
|
808
|
+
setServiceNameInput('');
|
|
809
|
+
setApiKeyInput('');
|
|
810
|
+
setIsEditingExisting(false);
|
|
811
|
+
setScreen('apiKeysManagement');
|
|
812
|
+
setSelectedIndex(0);
|
|
813
|
+
}
|
|
814
|
+
// Let the input component handle all other keys
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
467
818
|
if (screen === 'menu') {
|
|
468
819
|
if (key.name === 'up') {
|
|
469
820
|
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : menuItems.length - 1));
|
|
@@ -476,6 +827,9 @@ const App = () => {
|
|
|
476
827
|
} else if (selectedIndex === 1) {
|
|
477
828
|
setScreen('sandboxList');
|
|
478
829
|
setSelectedIndex(0);
|
|
830
|
+
} else if (selectedIndex === 2) {
|
|
831
|
+
setScreen('apiKeysManagement');
|
|
832
|
+
setSelectedIndex(0);
|
|
479
833
|
} else if (selectedIndex === menuItems.length - 1) {
|
|
480
834
|
process.exit(0);
|
|
481
835
|
}
|
|
@@ -523,11 +877,51 @@ const App = () => {
|
|
|
523
877
|
}
|
|
524
878
|
} else if (screen === 'confirmation') {
|
|
525
879
|
if (key.name === 'return' || key.name === 'enter') {
|
|
526
|
-
|
|
880
|
+
// Check if user is logged in before creating sandbox
|
|
881
|
+
if (!userCredentials) {
|
|
882
|
+
setScreen('authChoice');
|
|
883
|
+
setSelectedIndex(0);
|
|
884
|
+
} else {
|
|
885
|
+
createSandbox();
|
|
886
|
+
}
|
|
527
887
|
} else if (key.name === 'escape') {
|
|
528
|
-
|
|
888
|
+
if (config.sandboxProvider === 'modal') {
|
|
889
|
+
setScreen('gpuCountSelection');
|
|
890
|
+
} else {
|
|
891
|
+
setScreen('providerSelection');
|
|
892
|
+
}
|
|
529
893
|
setSelectedIndex(0);
|
|
530
894
|
}
|
|
895
|
+
} else if (screen === 'authChoice') {
|
|
896
|
+
if (key.name === 'up') {
|
|
897
|
+
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : 1));
|
|
898
|
+
} else if (key.name === 'down') {
|
|
899
|
+
setSelectedIndex((prev) => (prev < 1 ? prev + 1 : 0));
|
|
900
|
+
} else if (key.name === 'return' || key.name === 'enter') {
|
|
901
|
+
if (selectedIndex === 0) {
|
|
902
|
+
setScreen('register');
|
|
903
|
+
setAuthFormValues({});
|
|
904
|
+
} else {
|
|
905
|
+
setScreen('login');
|
|
906
|
+
setAuthFormValues({});
|
|
907
|
+
}
|
|
908
|
+
} else if (key.name === 'escape') {
|
|
909
|
+
process.exit(0);
|
|
910
|
+
}
|
|
911
|
+
} else if (screen === 'login') {
|
|
912
|
+
if (key.name === 'escape') {
|
|
913
|
+
setScreen('authChoice');
|
|
914
|
+
setSelectedIndex(0);
|
|
915
|
+
setAuthFormValues({});
|
|
916
|
+
}
|
|
917
|
+
// Submit is handled by input component's onSubmit
|
|
918
|
+
} else if (screen === 'register') {
|
|
919
|
+
if (key.name === 'escape') {
|
|
920
|
+
setScreen('authChoice');
|
|
921
|
+
setSelectedIndex(0);
|
|
922
|
+
setAuthFormValues({});
|
|
923
|
+
}
|
|
924
|
+
// Submit is handled by input component's onSubmit
|
|
531
925
|
} else if (screen === 'sandboxList') {
|
|
532
926
|
if (key.name === 'up' && sandboxes.length > 0) {
|
|
533
927
|
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : sandboxes.length - 1));
|
|
@@ -557,6 +951,50 @@ const App = () => {
|
|
|
557
951
|
setViewingSandboxId(null);
|
|
558
952
|
setScreen('sandboxList');
|
|
559
953
|
}
|
|
954
|
+
} else if (screen === 'apiKeysManagement') {
|
|
955
|
+
// Calculate items: 1 "Add New" + all stored keys
|
|
956
|
+
const storedKeyNames = Object.keys(apiKeys).sort();
|
|
957
|
+
const totalItems = 1 + storedKeyNames.length; // 1 for "Add New" + stored keys
|
|
958
|
+
|
|
959
|
+
if (key.name === 'up') {
|
|
960
|
+
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : totalItems - 1));
|
|
961
|
+
} else if (key.name === 'down') {
|
|
962
|
+
setSelectedIndex((prev) => (prev < totalItems - 1 ? prev + 1 : 0));
|
|
963
|
+
} else if (key.name === 'return' || key.name === 'enter') {
|
|
964
|
+
if (selectedIndex === 0) {
|
|
965
|
+
// "Add New API Key" selected
|
|
966
|
+
setServiceNameInput('');
|
|
967
|
+
setApiKeyInput('');
|
|
968
|
+
setIsEditingExisting(false);
|
|
969
|
+
setScreen('apiKeyForm');
|
|
970
|
+
} else {
|
|
971
|
+
// One of the stored keys selected - edit it
|
|
972
|
+
const serviceName = storedKeyNames[selectedIndex - 1];
|
|
973
|
+
setEditingServiceName(serviceName);
|
|
974
|
+
setServiceNameInput(serviceName);
|
|
975
|
+
setApiKeyInput(apiKeys[serviceName] || '');
|
|
976
|
+
setIsEditingExisting(true);
|
|
977
|
+
setScreen('apiKeyForm');
|
|
978
|
+
}
|
|
979
|
+
} else if (key.name === 'd') {
|
|
980
|
+
if (selectedIndex > 0) {
|
|
981
|
+
// Delete a stored key
|
|
982
|
+
const serviceName = storedKeyNames[selectedIndex - 1];
|
|
983
|
+
const success = deleteApiKey(serviceName);
|
|
984
|
+
if (success) {
|
|
985
|
+
setApiKeys(loadApiKeys());
|
|
986
|
+
setStatusMessage(`${serviceName} removed`);
|
|
987
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
988
|
+
// Adjust selection if needed
|
|
989
|
+
if (selectedIndex >= totalItems - 1 && selectedIndex > 0) {
|
|
990
|
+
setSelectedIndex(selectedIndex - 1);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
} else if (key.name === 'escape') {
|
|
995
|
+
setScreen('menu');
|
|
996
|
+
setSelectedIndex(0);
|
|
997
|
+
}
|
|
560
998
|
}
|
|
561
999
|
});
|
|
562
1000
|
|
|
@@ -572,6 +1010,124 @@ const App = () => {
|
|
|
572
1010
|
}
|
|
573
1011
|
};
|
|
574
1012
|
|
|
1013
|
+
const handleAuthFormInput = (field, value) => {
|
|
1014
|
+
setAuthFormValues(prev => ({ ...prev, [field]: value }));
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
const handleLoginSubmit = () => {
|
|
1018
|
+
const { username, email, fullName, password } = authFormValues;
|
|
1019
|
+
if (username && email && fullName && password) {
|
|
1020
|
+
// Save credentials
|
|
1021
|
+
const saved = saveUserCredentials(username, fullName, email);
|
|
1022
|
+
if (saved) {
|
|
1023
|
+
setUserCredentials({ userId: username, userName: fullName, userEmail: email });
|
|
1024
|
+
setStatusMessage(`Welcome, ${fullName}!`);
|
|
1025
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
1026
|
+
createSandbox();
|
|
1027
|
+
} else {
|
|
1028
|
+
setStatusMessage('Failed to save credentials');
|
|
1029
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
const handleRegisterSubmit = () => {
|
|
1035
|
+
const { username, email, fullName, password, confirmPassword } = authFormValues;
|
|
1036
|
+
|
|
1037
|
+
// Validation
|
|
1038
|
+
if (!username || username.length < 3) {
|
|
1039
|
+
setStatusMessage('Username must be at least 3 characters');
|
|
1040
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
1044
|
+
setStatusMessage('Please enter a valid email address');
|
|
1045
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (!fullName) {
|
|
1049
|
+
setStatusMessage('Full name is required');
|
|
1050
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
if (!password || password.length < 8) {
|
|
1054
|
+
setStatusMessage('Password must be at least 8 characters');
|
|
1055
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
if (password !== confirmPassword) {
|
|
1059
|
+
setStatusMessage('Passwords do not match');
|
|
1060
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Save credentials
|
|
1065
|
+
const saved = saveUserCredentials(username, fullName, email);
|
|
1066
|
+
if (saved) {
|
|
1067
|
+
setUserCredentials({ userId: username, userName: fullName, userEmail: email });
|
|
1068
|
+
setStatusMessage(`Account created! Welcome, ${fullName}!`);
|
|
1069
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
1070
|
+
createSandbox();
|
|
1071
|
+
} else {
|
|
1072
|
+
setStatusMessage('Failed to save credentials');
|
|
1073
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
const handleServiceNameInput = (value) => {
|
|
1078
|
+
setServiceNameInput(value);
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
const handleApiKeyInput = (value) => {
|
|
1082
|
+
setApiKeyInput(value);
|
|
1083
|
+
};
|
|
1084
|
+
|
|
1085
|
+
const handleApiKeyFormSubmit = () => {
|
|
1086
|
+
const serviceName = isEditingExisting ? editingServiceName : serviceNameInput.trim();
|
|
1087
|
+
const apiKey = apiKeyInput.trim();
|
|
1088
|
+
|
|
1089
|
+
// Validation
|
|
1090
|
+
if (!isEditingExisting && !serviceName) {
|
|
1091
|
+
setStatusMessage('Service name is required');
|
|
1092
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
if (!apiKey && !isEditingExisting) {
|
|
1097
|
+
setStatusMessage('API key is required');
|
|
1098
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Save the API key
|
|
1103
|
+
const saved = saveApiKey(serviceName, apiKey);
|
|
1104
|
+
|
|
1105
|
+
if (saved) {
|
|
1106
|
+
// Reload all keys
|
|
1107
|
+
setApiKeys(loadApiKeys());
|
|
1108
|
+
|
|
1109
|
+
if (!apiKey) {
|
|
1110
|
+
setStatusMessage(`${serviceName} removed successfully!`);
|
|
1111
|
+
} else if (isEditingExisting) {
|
|
1112
|
+
setStatusMessage(`${serviceName} updated successfully!`);
|
|
1113
|
+
} else {
|
|
1114
|
+
setStatusMessage(`${serviceName} added successfully!`);
|
|
1115
|
+
}
|
|
1116
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
1117
|
+
|
|
1118
|
+
// Reset and go back
|
|
1119
|
+
setEditingServiceName('');
|
|
1120
|
+
setServiceNameInput('');
|
|
1121
|
+
setApiKeyInput('');
|
|
1122
|
+
setIsEditingExisting(false);
|
|
1123
|
+
setScreen('apiKeysManagement');
|
|
1124
|
+
setSelectedIndex(0);
|
|
1125
|
+
} else {
|
|
1126
|
+
setStatusMessage('Failed to save API key');
|
|
1127
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
1128
|
+
}
|
|
1129
|
+
};
|
|
1130
|
+
|
|
575
1131
|
const viewingSandbox = viewingSandboxId ? sandboxes.find(s => s.id === viewingSandboxId) : null;
|
|
576
1132
|
|
|
577
1133
|
return (
|
|
@@ -588,8 +1144,13 @@ const App = () => {
|
|
|
588
1144
|
{screen === 'gpuSelection' && <GpuSelection selectedIndex={selectedIndex} repoUrl={config.repoUrl} provider={config.sandboxProvider} />}
|
|
589
1145
|
{screen === 'gpuCountSelection' && <GpuCountSelection selectedIndex={selectedIndex} gpuType={config.gpuType} />}
|
|
590
1146
|
{screen === 'confirmation' && <Confirmation config={config} />}
|
|
1147
|
+
{screen === 'authChoice' && <AuthChoice selectedIndex={selectedIndex} />}
|
|
1148
|
+
{screen === 'login' && <LoginForm values={authFormValues} onInput={handleAuthFormInput} onSubmit={handleLoginSubmit} />}
|
|
1149
|
+
{screen === 'register' && <RegisterForm values={authFormValues} onInput={handleAuthFormInput} onSubmit={handleRegisterSubmit} />}
|
|
591
1150
|
{screen === 'sandboxList' && <SandboxList sandboxes={sandboxes} selectedIndex={selectedIndex} />}
|
|
592
1151
|
{screen === 'sandboxLogs' && viewingSandbox && <SandboxLogs sandbox={viewingSandbox} />}
|
|
1152
|
+
{screen === 'apiKeysManagement' && <ApiKeysManagement apiKeys={apiKeys} selectedIndex={selectedIndex} />}
|
|
1153
|
+
{screen === 'apiKeyForm' && <ApiKeyForm serviceName={serviceNameInput} apiKey={apiKeyInput} onServiceInput={handleServiceNameInput} onKeyInput={handleApiKeyInput} onSubmit={handleApiKeyFormSubmit} isEditing={isEditingExisting} />}
|
|
593
1154
|
</box>
|
|
594
1155
|
);
|
|
595
1156
|
};
|