gitarsenal-cli 1.9.107 → 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 CHANGED
@@ -1 +1 @@
1
- {"created":"2025-10-15T12:10:37.722Z","packages":["modal","gitingest","requests","anthropic"],"uv_version":"uv 0.8.4 (Homebrew 2025-07-30)"}
1
+ {"created":"2025-10-15T13:20:17.803Z","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.107",
3
+ "version": "1.9.108",
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
+
@@ -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
- await execAsync(`uv pip install -r ${requirementsPath}`, {
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
- await execAsync(`uv pip install ${packages.join(' ')}`, {
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 is checked
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,54 @@ 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
+
13
57
  const menuItems = [
14
58
  'Create New Sandbox',
15
59
  'View Running Sandboxes',
@@ -229,6 +273,95 @@ const Confirmation = ({ config }) => {
229
273
  );
230
274
  };
231
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
+
232
365
  const SandboxList = ({ sandboxes, selectedIndex }) => {
233
366
  const maxVisibleItems = 10;
234
367
  const totalSandboxes = sandboxes.length;
@@ -292,11 +425,8 @@ const SandboxList = ({ sandboxes, selectedIndex }) => {
292
425
  )}
293
426
  <box marginTop={2} style={{ flexDirection: 'column', alignItems: 'center' }}>
294
427
  <text bold>Controls:</text>
295
- <box><text fg="cyan">↑↓</text><text> - Navigate sandboxes</text></box>
296
- <box><text fg="cyan">Enter</text><text> - View logs</text></box>
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>
428
+ <text dimColor fg="gray">↑↓ - Navigate • Enter - View logs • D - Delete</text>
429
+ <text dimColor fg="gray">R - Refresh • Esc - Back to menu</text>
300
430
  </box>
301
431
  </>
302
432
  )}
@@ -333,8 +463,8 @@ const SandboxLogs = ({ sandbox }) => {
333
463
  </scrollbox>
334
464
  <box marginTop={1} style={{ flexDirection: 'column', alignItems: 'center' }}>
335
465
  <text bold>Controls:</text>
336
- <box><text fg="cyan">Mouse Wheel</text><text> or </text><text fg="cyan">↑↓</text><text> - Scroll logs</text></box>
337
- <box><text fg="cyan">Esc</text><text> - Back to sandbox list</text></box>
466
+ <text dimColor fg="gray">Mouse Wheel or Arrow Keys - Scroll logs</text>
467
+ <text dimColor fg="gray">Esc - Back to sandbox list</text>
338
468
  </box>
339
469
  </box>
340
470
  );
@@ -354,6 +484,18 @@ const App = () => {
354
484
  const [sandboxIdCounter, setSandboxIdCounter] = useState(1);
355
485
  const [statusMessage, setStatusMessage] = useState('');
356
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
+ }, []);
357
499
 
358
500
 
359
501
  const createSandbox = () => {
@@ -523,11 +665,51 @@ const App = () => {
523
665
  }
524
666
  } else if (screen === 'confirmation') {
525
667
  if (key.name === 'return' || key.name === 'enter') {
526
- createSandbox();
668
+ // Check if user is logged in before creating sandbox
669
+ if (!userCredentials) {
670
+ setScreen('authChoice');
671
+ setSelectedIndex(0);
672
+ } else {
673
+ createSandbox();
674
+ }
527
675
  } else if (key.name === 'escape') {
528
- setScreen('gpuCountSelection');
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');
529
702
  setSelectedIndex(0);
703
+ setAuthFormValues({});
530
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
531
713
  } else if (screen === 'sandboxList') {
532
714
  if (key.name === 'up' && sandboxes.length > 0) {
533
715
  setSelectedIndex((prev) => (prev > 0 ? prev - 1 : sandboxes.length - 1));
@@ -572,6 +754,70 @@ const App = () => {
572
754
  }
573
755
  };
574
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
+
575
821
  const viewingSandbox = viewingSandboxId ? sandboxes.find(s => s.id === viewingSandboxId) : null;
576
822
 
577
823
  return (
@@ -588,6 +834,9 @@ const App = () => {
588
834
  {screen === 'gpuSelection' && <GpuSelection selectedIndex={selectedIndex} repoUrl={config.repoUrl} provider={config.sandboxProvider} />}
589
835
  {screen === 'gpuCountSelection' && <GpuCountSelection selectedIndex={selectedIndex} gpuType={config.gpuType} />}
590
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} />}
591
840
  {screen === 'sandboxList' && <SandboxList sandboxes={sandboxes} selectedIndex={selectedIndex} />}
592
841
  {screen === 'sandboxLogs' && viewingSandbox && <SandboxLogs sandbox={viewingSandbox} />}
593
842
  </box>