gitarsenal-cli 1.9.108 → 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 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-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.108",
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": {
package/tui-app/index.jsx CHANGED
@@ -54,6 +54,71 @@ const saveUserCredentials = (userId, userName, userEmail) => {
54
54
  }
55
55
  };
56
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
+
57
122
  const menuItems = [
58
123
  'Create New Sandbox',
59
124
  'View Running Sandboxes',
@@ -362,6 +427,126 @@ const RegisterForm = ({ values, onInput, onSubmit }) => {
362
427
  );
363
428
  };
364
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
+
365
550
  const SandboxList = ({ sandboxes, selectedIndex }) => {
366
551
  const maxVisibleItems = 10;
367
552
  const totalSandboxes = sandboxes.length;
@@ -486,6 +671,11 @@ const App = () => {
486
671
  const [viewingSandboxId, setViewingSandboxId] = useState(null);
487
672
  const [userCredentials, setUserCredentials] = useState(null);
488
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);
489
679
 
490
680
  // Load user credentials on mount
491
681
  useEffect(() => {
@@ -497,6 +687,12 @@ const App = () => {
497
687
  }
498
688
  }, []);
499
689
 
690
+ // Load API keys on mount
691
+ useEffect(() => {
692
+ const keys = loadApiKeys();
693
+ setApiKeys(keys);
694
+ }, []);
695
+
500
696
 
501
697
  const createSandbox = () => {
502
698
  const sandboxId = sandboxIdCounter;
@@ -606,6 +802,19 @@ const App = () => {
606
802
  return;
607
803
  }
608
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
+
609
818
  if (screen === 'menu') {
610
819
  if (key.name === 'up') {
611
820
  setSelectedIndex((prev) => (prev > 0 ? prev - 1 : menuItems.length - 1));
@@ -618,6 +827,9 @@ const App = () => {
618
827
  } else if (selectedIndex === 1) {
619
828
  setScreen('sandboxList');
620
829
  setSelectedIndex(0);
830
+ } else if (selectedIndex === 2) {
831
+ setScreen('apiKeysManagement');
832
+ setSelectedIndex(0);
621
833
  } else if (selectedIndex === menuItems.length - 1) {
622
834
  process.exit(0);
623
835
  }
@@ -739,6 +951,50 @@ const App = () => {
739
951
  setViewingSandboxId(null);
740
952
  setScreen('sandboxList');
741
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
+ }
742
998
  }
743
999
  });
744
1000
 
@@ -818,6 +1074,60 @@ const App = () => {
818
1074
  }
819
1075
  };
820
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
+
821
1131
  const viewingSandbox = viewingSandboxId ? sandboxes.find(s => s.id === viewingSandboxId) : null;
822
1132
 
823
1133
  return (
@@ -839,6 +1149,8 @@ const App = () => {
839
1149
  {screen === 'register' && <RegisterForm values={authFormValues} onInput={handleAuthFormInput} onSubmit={handleRegisterSubmit} />}
840
1150
  {screen === 'sandboxList' && <SandboxList sandboxes={sandboxes} selectedIndex={selectedIndex} />}
841
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} />}
842
1154
  </box>
843
1155
  );
844
1156
  };