gitarsenal-cli 1.9.108 → 1.9.112

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-15T13:20:17.803Z","packages":["modal","gitingest","requests","anthropic"],"uv_version":"uv 0.8.4 (Homebrew 2025-07-30)"}
1
+ {"created":"2025-10-15T14:03:01.884Z","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.108",
3
+ "version": "1.9.112",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
package/tui-app/index.jsx CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { render, useKeyboard, useTerminalDimensions } from '@opentui/react';
4
4
  import { useState, useEffect } from 'react';
5
- import { spawn } from 'child_process';
5
+ import { spawn, exec } from 'child_process';
6
6
  import { join } from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
  import { dirname } from 'path';
@@ -12,6 +12,17 @@ import os from 'os';
12
12
  const __filename = fileURLToPath(import.meta.url);
13
13
  const __dirname = dirname(__filename);
14
14
 
15
+ // Helper function to open URLs in default browser
16
+ const openUrl = (url) => {
17
+ const command = process.platform === 'darwin' ? 'open' :
18
+ process.platform === 'win32' ? 'start' : 'xdg-open';
19
+ exec(`${command} ${url}`, (error) => {
20
+ if (error) {
21
+ console.error('Could not open URL:', error);
22
+ }
23
+ });
24
+ };
25
+
15
26
  // Helper functions for user credentials
16
27
  const getUserConfigPath = () => {
17
28
  const userConfigDir = join(os.homedir(), '.gitarsenal');
@@ -54,6 +65,71 @@ const saveUserCredentials = (userId, userName, userEmail) => {
54
65
  }
55
66
  };
56
67
 
68
+ // Helper functions for API keys management
69
+ // Uses the same storage format as gitarsenal-cli Python CredentialsManager
70
+ const getCredentialsPath = () => {
71
+ const configDir = join(os.homedir(), '.gitarsenal');
72
+ const credentialsPath = join(configDir, 'credentials.json');
73
+ return { configDir, credentialsPath };
74
+ };
75
+
76
+ const loadApiKeys = () => {
77
+ const { credentialsPath } = getCredentialsPath();
78
+ if (fs.existsSync(credentialsPath)) {
79
+ try {
80
+ const credentials = JSON.parse(fs.readFileSync(credentialsPath, 'utf8'));
81
+ return credentials;
82
+ } catch (error) {
83
+ console.error('Could not read credentials:', error);
84
+ }
85
+ }
86
+ return {};
87
+ };
88
+
89
+ const saveApiKey = (serviceName, apiKey) => {
90
+ const { configDir, credentialsPath } = getCredentialsPath();
91
+ try {
92
+ if (!fs.existsSync(configDir)) {
93
+ fs.mkdirSync(configDir, { recursive: true });
94
+ }
95
+
96
+ // Load existing credentials
97
+ let credentials = loadApiKeys();
98
+
99
+ // Update or delete the key
100
+ if (apiKey && apiKey.trim()) {
101
+ credentials[serviceName] = apiKey.trim();
102
+ } else {
103
+ delete credentials[serviceName];
104
+ }
105
+
106
+ fs.writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2));
107
+
108
+ // Set restrictive permissions on Unix-like systems
109
+ if (process.platform !== 'win32') {
110
+ fs.chmodSync(credentialsPath, 0o600);
111
+ }
112
+
113
+ return true;
114
+ } catch (error) {
115
+ console.error('Could not save credentials:', error);
116
+ return false;
117
+ }
118
+ };
119
+
120
+ const deleteApiKey = (serviceName) => {
121
+ const { credentialsPath } = getCredentialsPath();
122
+ try {
123
+ let credentials = loadApiKeys();
124
+ delete credentials[serviceName];
125
+ fs.writeFileSync(credentialsPath, JSON.stringify(credentials, null, 2));
126
+ return true;
127
+ } catch (error) {
128
+ console.error('Could not delete credential:', error);
129
+ return false;
130
+ }
131
+ };
132
+
57
133
  const menuItems = [
58
134
  'Create New Sandbox',
59
135
  'View Running Sandboxes',
@@ -362,6 +438,126 @@ const RegisterForm = ({ values, onInput, onSubmit }) => {
362
438
  );
363
439
  };
364
440
 
441
+ const ApiKeysManagement = ({ apiKeys, selectedIndex }) => {
442
+ // Get all stored keys as an array
443
+ const storedKeyNames = Object.keys(apiKeys).sort();
444
+ const hasStoredKeys = storedKeyNames.length > 0;
445
+
446
+ return (
447
+ <box style={{ flexDirection: 'column', alignItems: 'center' }}>
448
+ <box borderStyle="single" padding={1} marginBottom={1}>
449
+ <text bold>API Keys Management</text>
450
+ </box>
451
+ <box marginBottom={2} style={{ flexDirection: 'column', alignItems: 'center' }}>
452
+ <text dimColor fg="gray">Universal support for any service API key</text>
453
+ {hasStoredKeys ? (
454
+ <text fg="green">{storedKeyNames.length} key{storedKeyNames.length > 1 ? 's' : ''} stored</text>
455
+ ) : (
456
+ <text dimColor fg="gray">No API keys stored yet</text>
457
+ )}
458
+ </box>
459
+
460
+ {/* Add New button */}
461
+ <box marginY={0} marginBottom={1}>
462
+ {selectedIndex === 0 ? (
463
+ <box borderStyle="single" borderColor="cyan" paddingX={2} paddingY={0} width={54} style={{ justifyContent: 'center' }}>
464
+ <text bold fg="cyan">+ Add New API Key</text>
465
+ </box>
466
+ ) : (
467
+ <box borderStyle="single" borderColor="gray" paddingX={2} paddingY={0} width={54} style={{ justifyContent: 'center' }}>
468
+ <text dimColor>+ Add New API Key</text>
469
+ </box>
470
+ )}
471
+ </box>
472
+
473
+ {/* Stored keys */}
474
+ {storedKeyNames.map((serviceName, index) => {
475
+ const itemIndex = index + 1; // +1 because "Add New" is at index 0
476
+ return (
477
+ <box key={serviceName} marginY={0} marginBottom={0}>
478
+ {itemIndex === selectedIndex ? (
479
+ <box borderStyle="single" borderColor="cyan" paddingX={2} paddingY={0} width={54} style={{ justifyContent: 'center' }}>
480
+ <text bold fg="cyan">{serviceName}</text>
481
+ </box>
482
+ ) : (
483
+ <box borderStyle="single" borderColor="gray" paddingX={2} paddingY={0} width={54} style={{ justifyContent: 'center' }}>
484
+ <text dimColor>{serviceName}</text>
485
+ </box>
486
+ )}
487
+ </box>
488
+ );
489
+ })}
490
+
491
+ <box marginTop={2} style={{ flexDirection: 'column', alignItems: 'center' }}>
492
+ <text bold>Controls:</text>
493
+ <text dimColor fg="gray">↑↓ - Navigate • Enter - {selectedIndex === 0 ? 'Add' : 'Modify'}</text>
494
+ {selectedIndex > 0 && <text dimColor fg="gray">D - Delete selected key</text>}
495
+ <text dimColor fg="gray">Esc - Back to menu</text>
496
+ </box>
497
+ </box>
498
+ );
499
+ };
500
+
501
+ const ApiKeyForm = ({ serviceName, apiKey, onServiceInput, onKeyInput, onSubmit, isEditing }) => {
502
+ return (
503
+ <box style={{ flexDirection: 'column', alignItems: 'center' }}>
504
+ <box borderStyle="single" padding={1} marginBottom={1}>
505
+ <text bold>{isEditing ? 'Modify' : 'Add'} API Key</text>
506
+ </box>
507
+ <box marginBottom={2} style={{ flexDirection: 'column', alignItems: 'center' }}>
508
+ {isEditing ? (
509
+ <>
510
+ <text fg="green">Modifying existing API key</text>
511
+ <text dimColor fg="gray">Update the API key value below</text>
512
+ </>
513
+ ) : (
514
+ <>
515
+ <text dimColor fg="gray">Add API key for any service</text>
516
+ <text dimColor fg="gray">Examples: openai_api_key, WANDB_API_KEY, modal_token</text>
517
+ </>
518
+ )}
519
+ </box>
520
+
521
+ {!isEditing && (
522
+ <box marginBottom={2} style={{ flexDirection: 'column', alignItems: 'center' }}>
523
+ <text bold>Service Name:</text>
524
+ <input
525
+ value={serviceName || ''}
526
+ onInput={onServiceInput}
527
+ placeholder="e.g., openai_api_key"
528
+ focused={true}
529
+ width={70}
530
+ />
531
+ </box>
532
+ )}
533
+
534
+ {isEditing && (
535
+ <box marginBottom={2} style={{ flexDirection: 'column', alignItems: 'center' }}>
536
+ <text bold>Service:</text>
537
+ <text fg="cyan">{serviceName}</text>
538
+ </box>
539
+ )}
540
+
541
+ <box marginBottom={1} style={{ flexDirection: 'column', alignItems: 'center' }}>
542
+ <text bold>API Key:</text>
543
+ <input
544
+ value={apiKey || ''}
545
+ onInput={onKeyInput}
546
+ onSubmit={onSubmit}
547
+ placeholder="Paste your API key here..."
548
+ focused={isEditing}
549
+ width={70}
550
+ />
551
+ </box>
552
+
553
+ <box marginTop={1} style={{ flexDirection: 'column', alignItems: 'center' }}>
554
+ <text dimColor fg="gray">Enter to save • Esc to cancel</text>
555
+ {isEditing && <text dimColor fg="gray">Clear field to remove the key</text>}
556
+ </box>
557
+ </box>
558
+ );
559
+ };
560
+
365
561
  const SandboxList = ({ sandboxes, selectedIndex }) => {
366
562
  const maxVisibleItems = 10;
367
563
  const totalSandboxes = sandboxes.length;
@@ -486,6 +682,11 @@ const App = () => {
486
682
  const [viewingSandboxId, setViewingSandboxId] = useState(null);
487
683
  const [userCredentials, setUserCredentials] = useState(null);
488
684
  const [authFormValues, setAuthFormValues] = useState({});
685
+ const [apiKeys, setApiKeys] = useState({});
686
+ const [editingServiceName, setEditingServiceName] = useState('');
687
+ const [serviceNameInput, setServiceNameInput] = useState('');
688
+ const [apiKeyInput, setApiKeyInput] = useState('');
689
+ const [isEditingExisting, setIsEditingExisting] = useState(false);
489
690
 
490
691
  // Load user credentials on mount
491
692
  useEffect(() => {
@@ -497,6 +698,12 @@ const App = () => {
497
698
  }
498
699
  }, []);
499
700
 
701
+ // Load API keys on mount
702
+ useEffect(() => {
703
+ const keys = loadApiKeys();
704
+ setApiKeys(keys);
705
+ }, []);
706
+
500
707
 
501
708
  const createSandbox = () => {
502
709
  const sandboxId = sandboxIdCounter;
@@ -606,6 +813,19 @@ const App = () => {
606
813
  return;
607
814
  }
608
815
 
816
+ if (screen === 'apiKeyForm') {
817
+ if (key.name === 'escape') {
818
+ setEditingServiceName('');
819
+ setServiceNameInput('');
820
+ setApiKeyInput('');
821
+ setIsEditingExisting(false);
822
+ setScreen('apiKeysManagement');
823
+ setSelectedIndex(0);
824
+ }
825
+ // Let the input component handle all other keys
826
+ return;
827
+ }
828
+
609
829
  if (screen === 'menu') {
610
830
  if (key.name === 'up') {
611
831
  setSelectedIndex((prev) => (prev > 0 ? prev - 1 : menuItems.length - 1));
@@ -618,6 +838,14 @@ const App = () => {
618
838
  } else if (selectedIndex === 1) {
619
839
  setScreen('sandboxList');
620
840
  setSelectedIndex(0);
841
+ } else if (selectedIndex === 2) {
842
+ setScreen('apiKeysManagement');
843
+ setSelectedIndex(0);
844
+ } else if (selectedIndex === 4) {
845
+ // Help & Examples - open docs page
846
+ openUrl('https://gitarsenal.dev/docs');
847
+ setStatusMessage('Opening documentation in your browser...');
848
+ setTimeout(() => setStatusMessage(''), 3000);
621
849
  } else if (selectedIndex === menuItems.length - 1) {
622
850
  process.exit(0);
623
851
  }
@@ -739,6 +967,50 @@ const App = () => {
739
967
  setViewingSandboxId(null);
740
968
  setScreen('sandboxList');
741
969
  }
970
+ } else if (screen === 'apiKeysManagement') {
971
+ // Calculate items: 1 "Add New" + all stored keys
972
+ const storedKeyNames = Object.keys(apiKeys).sort();
973
+ const totalItems = 1 + storedKeyNames.length; // 1 for "Add New" + stored keys
974
+
975
+ if (key.name === 'up') {
976
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : totalItems - 1));
977
+ } else if (key.name === 'down') {
978
+ setSelectedIndex((prev) => (prev < totalItems - 1 ? prev + 1 : 0));
979
+ } else if (key.name === 'return' || key.name === 'enter') {
980
+ if (selectedIndex === 0) {
981
+ // "Add New API Key" selected
982
+ setServiceNameInput('');
983
+ setApiKeyInput('');
984
+ setIsEditingExisting(false);
985
+ setScreen('apiKeyForm');
986
+ } else {
987
+ // One of the stored keys selected - edit it
988
+ const serviceName = storedKeyNames[selectedIndex - 1];
989
+ setEditingServiceName(serviceName);
990
+ setServiceNameInput(serviceName);
991
+ setApiKeyInput(apiKeys[serviceName] || '');
992
+ setIsEditingExisting(true);
993
+ setScreen('apiKeyForm');
994
+ }
995
+ } else if (key.name === 'd') {
996
+ if (selectedIndex > 0) {
997
+ // Delete a stored key
998
+ const serviceName = storedKeyNames[selectedIndex - 1];
999
+ const success = deleteApiKey(serviceName);
1000
+ if (success) {
1001
+ setApiKeys(loadApiKeys());
1002
+ setStatusMessage(`${serviceName} removed`);
1003
+ setTimeout(() => setStatusMessage(''), 3000);
1004
+ // Adjust selection if needed
1005
+ if (selectedIndex >= totalItems - 1 && selectedIndex > 0) {
1006
+ setSelectedIndex(selectedIndex - 1);
1007
+ }
1008
+ }
1009
+ }
1010
+ } else if (key.name === 'escape') {
1011
+ setScreen('menu');
1012
+ setSelectedIndex(0);
1013
+ }
742
1014
  }
743
1015
  });
744
1016
 
@@ -818,6 +1090,60 @@ const App = () => {
818
1090
  }
819
1091
  };
820
1092
 
1093
+ const handleServiceNameInput = (value) => {
1094
+ setServiceNameInput(value);
1095
+ };
1096
+
1097
+ const handleApiKeyInput = (value) => {
1098
+ setApiKeyInput(value);
1099
+ };
1100
+
1101
+ const handleApiKeyFormSubmit = () => {
1102
+ const serviceName = isEditingExisting ? editingServiceName : serviceNameInput.trim();
1103
+ const apiKey = apiKeyInput.trim();
1104
+
1105
+ // Validation
1106
+ if (!isEditingExisting && !serviceName) {
1107
+ setStatusMessage('Service name is required');
1108
+ setTimeout(() => setStatusMessage(''), 3000);
1109
+ return;
1110
+ }
1111
+
1112
+ if (!apiKey && !isEditingExisting) {
1113
+ setStatusMessage('API key is required');
1114
+ setTimeout(() => setStatusMessage(''), 3000);
1115
+ return;
1116
+ }
1117
+
1118
+ // Save the API key
1119
+ const saved = saveApiKey(serviceName, apiKey);
1120
+
1121
+ if (saved) {
1122
+ // Reload all keys
1123
+ setApiKeys(loadApiKeys());
1124
+
1125
+ if (!apiKey) {
1126
+ setStatusMessage(`${serviceName} removed successfully!`);
1127
+ } else if (isEditingExisting) {
1128
+ setStatusMessage(`${serviceName} updated successfully!`);
1129
+ } else {
1130
+ setStatusMessage(`${serviceName} added successfully!`);
1131
+ }
1132
+ setTimeout(() => setStatusMessage(''), 3000);
1133
+
1134
+ // Reset and go back
1135
+ setEditingServiceName('');
1136
+ setServiceNameInput('');
1137
+ setApiKeyInput('');
1138
+ setIsEditingExisting(false);
1139
+ setScreen('apiKeysManagement');
1140
+ setSelectedIndex(0);
1141
+ } else {
1142
+ setStatusMessage('Failed to save API key');
1143
+ setTimeout(() => setStatusMessage(''), 3000);
1144
+ }
1145
+ };
1146
+
821
1147
  const viewingSandbox = viewingSandboxId ? sandboxes.find(s => s.id === viewingSandboxId) : null;
822
1148
 
823
1149
  return (
@@ -839,6 +1165,8 @@ const App = () => {
839
1165
  {screen === 'register' && <RegisterForm values={authFormValues} onInput={handleAuthFormInput} onSubmit={handleRegisterSubmit} />}
840
1166
  {screen === 'sandboxList' && <SandboxList sandboxes={sandboxes} selectedIndex={selectedIndex} />}
841
1167
  {screen === 'sandboxLogs' && viewingSandbox && <SandboxLogs sandbox={viewingSandbox} />}
1168
+ {screen === 'apiKeysManagement' && <ApiKeysManagement apiKeys={apiKeys} selectedIndex={selectedIndex} />}
1169
+ {screen === 'apiKeyForm' && <ApiKeyForm serviceName={serviceNameInput} apiKey={apiKeyInput} onServiceInput={handleServiceNameInput} onKeyInput={handleApiKeyInput} onSubmit={handleApiKeyFormSubmit} isEditing={isEditingExisting} />}
842
1170
  </box>
843
1171
  );
844
1172
  };