gitarsenal-cli 1.9.39 → 1.9.40

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-08-11T11:27:54.801Z","packages":["modal","gitingest","requests","anthropic"],"uv_version":"uv 0.8.4 (Homebrew 2025-07-30)"}
1
+ {"created":"2025-08-11T12:44:14.984Z","packages":["modal","gitingest","requests","anthropic"],"uv_version":"uv 0.8.4 (Homebrew 2025-07-30)"}
package/bin/gitarsenal.js CHANGED
@@ -105,19 +105,39 @@ function activateVirtualEnvironment() {
105
105
  }
106
106
 
107
107
  // Lightweight preview of GPU/Torch/CUDA recommendations prior to GPU selection
108
- async function previewRecommendations(repoUrl) {
109
- const spinner = ora('Analyzing repository for GPU/Torch/CUDA recommendations...').start();
108
+ async function previewRecommendations(repoUrl, optsOrShowSummary = true) {
109
+ const showSummary = typeof optsOrShowSummary === 'boolean' ? optsOrShowSummary : (optsOrShowSummary?.showSummary ?? true);
110
+ const externalSignal = typeof optsOrShowSummary === 'object' ? optsOrShowSummary.abortSignal : undefined;
111
+ const hideSpinner = typeof optsOrShowSummary === 'object' ? optsOrShowSummary.hideSpinner : false;
112
+
113
+ const spinner = hideSpinner ? null : ora('Analyzing repository for GPU/Torch/CUDA recommendations...').start();
114
+ const previewTimeoutMs = Number(process.env.GITARSENAL_PREVIEW_TIMEOUT_MS || 90000);
115
+ const controller = new AbortController();
116
+ const abortOnExternal = () => controller.abort();
117
+ const timeoutId = setTimeout(() => controller.abort(), previewTimeoutMs);
118
+
119
+ // Add periodic spinner updates to show progress (only if we have a spinner)
120
+ let elapsedTime = 0;
121
+ const progressInterval = spinner ? setInterval(() => {
122
+ elapsedTime += 10;
123
+ const minutes = Math.floor(elapsedTime / 60);
124
+ const seconds = elapsedTime % 60;
125
+ const timeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
126
+ spinner.text = `Analyzing repository for GPU/Torch/CUDA recommendations... (${timeStr})`;
127
+ }, 10000) : null; // Update every 10 seconds
128
+
110
129
  try {
130
+ // Bridge external abort signal to our controller (for stopping spinner when full fetch returns)
131
+ if (externalSignal) {
132
+ if (externalSignal.aborted) controller.abort();
133
+ else externalSignal.addEventListener('abort', abortOnExternal, { once: true });
134
+ }
135
+
111
136
  const envUrl = process.env.GITARSENAL_API_URL;
112
- const endpoints = envUrl
113
- ? [envUrl]
114
- : [
115
- 'https://www.gitarsenal.dev/api/best_gpu'
116
- ];
137
+ const endpoints = envUrl ? [envUrl] : ['https://www.gitarsenal.dev/api/best_gpu'];
117
138
 
118
139
  const payload = {
119
140
  repoUrl,
120
- // Minimal GitIngest data to allow backend to run LLM analysis
121
141
  gitingestData: {
122
142
  system_info: {
123
143
  platform: process.platform,
@@ -137,42 +157,22 @@ async function previewRecommendations(repoUrl) {
137
157
  },
138
158
  success: true
139
159
  },
140
- // Hint server to do lightweight preview if supported
141
160
  preview: true
142
161
  };
143
162
 
144
- // Increase timeout to allow server to compute recommendations before GPU selection
145
- const previewTimeoutMs = Number(process.env.GITARSENAL_PREVIEW_TIMEOUT_MS || 90000);
163
+ let data = null;
164
+ let lastErrorText = '';
146
165
 
147
- const fetchWithTimeout = async (url, body, timeoutMs = previewTimeoutMs) => {
148
- const controller = new AbortController();
149
- const id = setTimeout(() => controller.abort(), timeoutMs);
166
+ for (const url of endpoints) {
150
167
  try {
168
+ if (spinner) spinner.text = `Analyzing (preview): ${url}`;
151
169
  const res = await fetch(url, {
152
170
  method: 'POST',
153
- headers: {
154
- 'Content-Type': 'application/json',
155
- 'User-Agent': 'GitArsenal-CLI/1.0'
156
- },
157
- body: JSON.stringify(body),
171
+ headers: { 'Content-Type': 'application/json', 'User-Agent': 'GitArsenal-CLI/1.0' },
172
+ body: JSON.stringify(payload),
158
173
  redirect: 'follow',
159
174
  signal: controller.signal
160
175
  });
161
- clearTimeout(id);
162
- return res;
163
- } catch (e) {
164
- clearTimeout(id);
165
- throw e;
166
- }
167
- };
168
-
169
- let data = null;
170
- let lastErrorText = '';
171
-
172
- for (const url of endpoints) {
173
- try {
174
- spinner.text = `Analyzing (preview): ${url}`;
175
- const res = await fetchWithTimeout(url, payload, previewTimeoutMs);
176
176
  if (!res.ok) {
177
177
  const text = await res.text().catch(() => '');
178
178
  lastErrorText = `${res.status} ${text.slice(0, 300)}`;
@@ -181,25 +181,37 @@ async function previewRecommendations(repoUrl) {
181
181
  data = await res.json().catch(() => null);
182
182
  if (data) break;
183
183
  } catch (err) {
184
+ if (err && (err.name === 'AbortError' || err.code === 'ABORT_ERR')) {
185
+ // Silent stop on external abort (e.g., full fetch succeeded)
186
+ return null;
187
+ }
184
188
  lastErrorText = err && err.message ? err.message : 'request failed';
185
189
  continue;
186
190
  }
187
191
  }
188
192
 
189
- spinner.stop();
190
-
191
193
  if (!data) {
192
- console.log(chalk.yellow('⚠️ Preview unavailable (timeout or server error).'));
193
- if (lastErrorText) console.log(chalk.gray(`Reason: ${lastErrorText}`));
194
+ if (!hideSpinner) {
195
+ console.log(chalk.yellow('⚠️ Preview unavailable (timeout or server error).'));
196
+ if (lastErrorText) console.log(chalk.gray(`Reason: ${lastErrorText}`));
197
+ }
194
198
  return null;
195
199
  }
196
200
 
197
- printGpuTorchCudaSummary(data);
201
+ if (showSummary) {
202
+ printGpuTorchCudaSummary(data);
203
+ }
198
204
  return data;
199
205
  } catch (e) {
200
- spinner.stop();
201
- console.log(chalk.yellow(`⚠️ Preview failed: ${e.message}`));
206
+ if (!(e && (e.name === 'AbortError' || e.code === 'ABORT_ERR')) && !hideSpinner) {
207
+ console.log(chalk.yellow(`⚠️ Preview failed: ${e.message}`));
208
+ }
202
209
  return null;
210
+ } finally {
211
+ clearTimeout(timeoutId);
212
+ if (progressInterval) clearInterval(progressInterval);
213
+ if (spinner) spinner.stop();
214
+ if (externalSignal) externalSignal.removeEventListener('abort', abortOnExternal);
203
215
  }
204
216
  }
205
217
 
@@ -496,10 +508,10 @@ async function collectUserCredentials(options) {
496
508
 
497
509
  if (authChoice.action === 'register') {
498
510
  console.log(chalk.blue('\n📝 Create New Account'));
499
- const credentials = await inquirer.prompt([
500
- {
501
- type: 'input',
502
- name: 'userId',
511
+ const credentials = await inquirer.prompt([
512
+ {
513
+ type: 'input',
514
+ name: 'userId',
503
515
  message: 'Choose a username:',
504
516
  validate: (input) => {
505
517
  const username = input.trim();
@@ -521,14 +533,14 @@ async function collectUserCredentials(options) {
521
533
  }
522
534
  return true;
523
535
  }
524
- },
525
- {
526
- type: 'input',
527
- name: 'userName',
536
+ },
537
+ {
538
+ type: 'input',
539
+ name: 'userName',
528
540
  message: 'Enter your full name:',
529
- validate: (input) => input.trim() !== '' ? true : 'Name is required'
530
- },
531
- {
541
+ validate: (input) => input.trim() !== '' ? true : 'Name is required'
542
+ },
543
+ {
532
544
  type: 'password',
533
545
  name: 'password',
534
546
  message: 'Create a password (min 8 characters):',
@@ -545,11 +557,11 @@ async function collectUserCredentials(options) {
545
557
  if (input !== answers.password) return 'Passwords do not match';
546
558
  return true;
547
559
  }
548
- }
549
- ]);
550
-
551
- userId = credentials.userId;
552
- userName = credentials.userName;
560
+ }
561
+ ]);
562
+
563
+ userId = credentials.userId;
564
+ userName = credentials.userName;
553
565
  userEmail = credentials.userEmail;
554
566
 
555
567
  console.log(chalk.green('✅ Account created successfully!'));
@@ -587,15 +599,15 @@ async function collectUserCredentials(options) {
587
599
  fs.mkdirSync(userConfigDir, { recursive: true });
588
600
  }
589
601
 
590
- const config = {
591
- userId,
592
- userName,
602
+ const config = {
603
+ userId,
604
+ userName,
593
605
  userEmail,
594
606
  savedAt: new Date().toISOString()
595
- };
607
+ };
596
608
  fs.writeFileSync(userConfigPath, JSON.stringify(config, null, 2));
597
609
  console.log(chalk.green('✅ Credentials saved locally'));
598
- } catch (error) {
610
+ } catch (error) {
599
611
  console.log(chalk.yellow('⚠️ Could not save credentials locally'));
600
612
  }
601
613
  }
@@ -742,18 +754,49 @@ async function runContainerCommand(options) {
742
754
  repoUrl = answers.repoUrl;
743
755
  }
744
756
 
745
- // Attempt full fetch first to get both commands and recommendations; fallback to preview on failure
757
+ // Attempt full fetch first to get both commands and recommendations; now start preview concurrently
746
758
  if (useApi && repoUrl) {
747
- const fullData = await fetchFullSetupAndRecs(repoUrl).catch(() => null);
748
- if (fullData) {
749
- printGpuTorchCudaSummary(fullData);
750
- if (Array.isArray(fullData.commands) && fullData.commands.length) {
751
- setupCommands = fullData.commands;
752
- // Disable auto-detection since we already have commands
753
- useApi = false;
759
+ // Start a main spinner that will show overall progress
760
+ const mainSpinner = ora('Analyzing repository...').start();
761
+
762
+ try {
763
+ // Start preview immediately so we get early feedback; suppress summary here to avoid duplicates.
764
+ // Provide an AbortController so we can stop the preview spinner as soon as full fetch returns.
765
+ const previewAbort = new AbortController();
766
+ mainSpinner.text = 'Analyzing repository for GPU/Torch/CUDA recommendations...';
767
+ const previewPromise = previewRecommendations(repoUrl, { showSummary: false, abortSignal: previewAbort.signal, hideSpinner: true }).catch(() => null);
768
+
769
+ // Run full fetch in parallel; prefer its results if available.
770
+ mainSpinner.text = 'Finding the best machine for your code...';
771
+ const fullData = await fetchFullSetupAndRecs(repoUrl).catch(() => null);
772
+
773
+ if (fullData) {
774
+ // Stop preview spinner immediately since we have a response
775
+ previewAbort.abort();
776
+ mainSpinner.succeed('Analysis complete!');
777
+ printGpuTorchCudaSummary(fullData);
778
+ if (Array.isArray(fullData.commands) && fullData.commands.length) {
779
+ setupCommands = fullData.commands;
780
+ // Disable auto-detection since we already have commands
781
+ useApi = false;
782
+ }
783
+ } else {
784
+ // Full fetch failed, wait for preview and show its results
785
+ mainSpinner.text = 'Waiting for preview analysis to complete...';
786
+ const previewData = await previewPromise;
787
+ if (previewData) {
788
+ mainSpinner.succeed('Preview analysis complete!');
789
+ printGpuTorchCudaSummary(previewData);
790
+ } else {
791
+ mainSpinner.fail('Analysis failed - both preview and full analysis timed out or failed');
792
+ console.log(chalk.yellow('⚠️ Unable to analyze repository automatically.'));
793
+ console.log(chalk.gray('You can still proceed with manual setup commands.'));
794
+ }
754
795
  }
755
- } else {
756
- await previewRecommendations(repoUrl);
796
+ } catch (error) {
797
+ mainSpinner.fail(`Analysis failed: ${error.message}`);
798
+ console.log(chalk.yellow('⚠️ Unable to analyze repository automatically.'));
799
+ console.log(chalk.gray('You can still proceed with manual setup commands.'));
757
800
  }
758
801
  }
759
802
 
@@ -883,7 +926,10 @@ async function runContainerCommand(options) {
883
926
  volumeName,
884
927
  setupCommands,
885
928
  useApi,
886
- yes: skipConfirmation
929
+ yes: skipConfirmation,
930
+ userId,
931
+ userName,
932
+ userEmail
887
933
  });
888
934
 
889
935
  } catch (containerError) {
package/lib/sandbox.js CHANGED
@@ -44,7 +44,10 @@ async function runContainer(options) {
44
44
  setupCommands = [],
45
45
  useApi = true,
46
46
  showExamples = false,
47
- yes = false
47
+ yes = false,
48
+ userId,
49
+ userName,
50
+ userEmail
48
51
  } = options;
49
52
 
50
53
  // Get the path to the Python script
@@ -107,6 +110,14 @@ async function runContainer(options) {
107
110
  console.log(chalk.gray(`🔍 Debug: Adding --yes flag to Python script`));
108
111
  }
109
112
 
113
+ // Add user credentials if provided
114
+ if (userId && userEmail && userName) {
115
+ args.push('--user-id', userEmail);
116
+ args.push('--user-name', userId);
117
+ args.push('--display-name', userName);
118
+ console.log(chalk.gray(`🔍 Debug: Passing user credentials to Python script`));
119
+ }
120
+
110
121
  // Handle manual setup commands if provided
111
122
  if (setupCommands.length > 0) {
112
123
  // Create a temporary file to store setup commands
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitarsenal-cli",
3
- "version": "1.9.39",
3
+ "version": "1.9.40",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1611,7 +1611,7 @@ def get_setup_commands_from_gitingest(repo_url):
1611
1611
  url=api_url,
1612
1612
  payload=payload,
1613
1613
  max_retries=2,
1614
- timeout=180 # 3 minute timeout
1614
+ timeout=180, # 3 minute timeout
1615
1615
  )
1616
1616
 
1617
1617
  if not response:
@@ -2115,6 +2115,11 @@ if __name__ == "__main__":
2115
2115
  parser.add_argument('--store-api-key', type=str, help='Store API key for a service (e.g., openai, modal)')
2116
2116
  parser.add_argument('--skip-auth', action='store_true', help='Skip authentication check (for development)')
2117
2117
 
2118
+ # User credential arguments (passed from JavaScript CLI)
2119
+ parser.add_argument('--user-id', type=str, help='User email address (passed from JavaScript CLI)')
2120
+ parser.add_argument('--user-name', type=str, help='Username (passed from JavaScript CLI)')
2121
+ parser.add_argument('--display-name', type=str, help='Display name (passed from JavaScript CLI)')
2122
+
2118
2123
  args = parser.parse_args()
2119
2124
 
2120
2125
  # Initialize authentication manager
@@ -2149,8 +2154,11 @@ if __name__ == "__main__":
2149
2154
  show_usage_examples()
2150
2155
  sys.exit(0)
2151
2156
 
2152
- # Check authentication (unless skipped for development)
2153
- if not args.skip_auth:
2157
+ # Authentication is handled by the JavaScript CLI when credentials are passed
2158
+ if args.user_id and args.user_name and args.display_name:
2159
+ print(f"✅ Authenticated as: {args.display_name} ({args.user_id})")
2160
+ elif not args.skip_auth:
2161
+ # Only perform authentication check if running Python script directly (not from CLI)
2154
2162
  if not _check_authentication(auth_manager):
2155
2163
  print("\n❌ Authentication required. Please login or register first.")
2156
2164
  print("Use --login to login or --register to create an account.")