gitarsenal-cli 1.9.39 → 1.9.41

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-11T14:42:03.859Z","packages":["modal","gitingest","requests","anthropic"],"uv_version":"uv 0.8.4 (Homebrew 2025-07-30)"}
package/bin/gitarsenal.js CHANGED
@@ -41,7 +41,6 @@ function activateVirtualEnvironment() {
41
41
  if (fs.existsSync(statusFile)) {
42
42
  try {
43
43
  const status = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
44
- // console.log(chalk.gray(`āœ… Virtual environment created: ${status.created}`));
45
44
  console.log(chalk.gray(`šŸ“¦ Packages: ${status.packages.join(', ')}`));
46
45
  } catch (error) {
47
46
  console.log(chalk.gray('āœ… Virtual environment found'));
@@ -105,19 +104,39 @@ function activateVirtualEnvironment() {
105
104
  }
106
105
 
107
106
  // 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();
107
+ async function previewRecommendations(repoUrl, optsOrShowSummary = true) {
108
+ const showSummary = typeof optsOrShowSummary === 'boolean' ? optsOrShowSummary : (optsOrShowSummary?.showSummary ?? true);
109
+ const externalSignal = typeof optsOrShowSummary === 'object' ? optsOrShowSummary.abortSignal : undefined;
110
+ const hideSpinner = typeof optsOrShowSummary === 'object' ? optsOrShowSummary.hideSpinner : false;
111
+
112
+ const spinner = hideSpinner ? null : ora('Analyzing repository for GPU/Torch/CUDA recommendations...').start();
113
+ const previewTimeoutMs = Number(process.env.GITARSENAL_PREVIEW_TIMEOUT_MS || 90000);
114
+ const controller = new AbortController();
115
+ const abortOnExternal = () => controller.abort();
116
+ const timeoutId = setTimeout(() => controller.abort(), previewTimeoutMs);
117
+
118
+ // Add periodic spinner updates to show progress (only if we have a spinner)
119
+ let elapsedTime = 0;
120
+ const progressInterval = spinner ? setInterval(() => {
121
+ elapsedTime += 10;
122
+ const minutes = Math.floor(elapsedTime / 60);
123
+ const seconds = elapsedTime % 60;
124
+ const timeStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
125
+ spinner.text = `Analyzing repository for GPU/Torch/CUDA recommendations... (${timeStr})`;
126
+ }, 10000) : null; // Update every 10 seconds
127
+
110
128
  try {
129
+ // Bridge external abort signal to our controller (for stopping spinner when full fetch returns)
130
+ if (externalSignal) {
131
+ if (externalSignal.aborted) controller.abort();
132
+ else externalSignal.addEventListener('abort', abortOnExternal, { once: true });
133
+ }
134
+
111
135
  const envUrl = process.env.GITARSENAL_API_URL;
112
- const endpoints = envUrl
113
- ? [envUrl]
114
- : [
115
- 'https://www.gitarsenal.dev/api/best_gpu'
116
- ];
136
+ const endpoints = envUrl ? [envUrl] : ['https://www.gitarsenal.dev/api/best_gpu'];
117
137
 
118
138
  const payload = {
119
139
  repoUrl,
120
- // Minimal GitIngest data to allow backend to run LLM analysis
121
140
  gitingestData: {
122
141
  system_info: {
123
142
  platform: process.platform,
@@ -137,42 +156,22 @@ async function previewRecommendations(repoUrl) {
137
156
  },
138
157
  success: true
139
158
  },
140
- // Hint server to do lightweight preview if supported
141
159
  preview: true
142
160
  };
143
161
 
144
- // Increase timeout to allow server to compute recommendations before GPU selection
145
- const previewTimeoutMs = Number(process.env.GITARSENAL_PREVIEW_TIMEOUT_MS || 90000);
162
+ let data = null;
163
+ let lastErrorText = '';
146
164
 
147
- const fetchWithTimeout = async (url, body, timeoutMs = previewTimeoutMs) => {
148
- const controller = new AbortController();
149
- const id = setTimeout(() => controller.abort(), timeoutMs);
165
+ for (const url of endpoints) {
150
166
  try {
167
+ if (spinner) spinner.text = `Analyzing (preview): ${url}`;
151
168
  const res = await fetch(url, {
152
169
  method: 'POST',
153
- headers: {
154
- 'Content-Type': 'application/json',
155
- 'User-Agent': 'GitArsenal-CLI/1.0'
156
- },
157
- body: JSON.stringify(body),
170
+ headers: { 'Content-Type': 'application/json', 'User-Agent': 'GitArsenal-CLI/1.0' },
171
+ body: JSON.stringify(payload),
158
172
  redirect: 'follow',
159
173
  signal: controller.signal
160
174
  });
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
175
  if (!res.ok) {
177
176
  const text = await res.text().catch(() => '');
178
177
  lastErrorText = `${res.status} ${text.slice(0, 300)}`;
@@ -181,25 +180,37 @@ async function previewRecommendations(repoUrl) {
181
180
  data = await res.json().catch(() => null);
182
181
  if (data) break;
183
182
  } catch (err) {
183
+ if (err && (err.name === 'AbortError' || err.code === 'ABORT_ERR')) {
184
+ // Silent stop on external abort (e.g., full fetch succeeded)
185
+ return null;
186
+ }
184
187
  lastErrorText = err && err.message ? err.message : 'request failed';
185
188
  continue;
186
189
  }
187
190
  }
188
191
 
189
- spinner.stop();
190
-
191
192
  if (!data) {
192
- console.log(chalk.yellow('āš ļø Preview unavailable (timeout or server error).'));
193
- if (lastErrorText) console.log(chalk.gray(`Reason: ${lastErrorText}`));
193
+ if (!hideSpinner) {
194
+ console.log(chalk.yellow('āš ļø Preview unavailable (timeout or server error).'));
195
+ if (lastErrorText) console.log(chalk.gray(`Reason: ${lastErrorText}`));
196
+ }
194
197
  return null;
195
198
  }
196
199
 
197
- printGpuTorchCudaSummary(data);
200
+ if (showSummary) {
201
+ printGpuTorchCudaSummary(data);
202
+ }
198
203
  return data;
199
204
  } catch (e) {
200
- spinner.stop();
201
- console.log(chalk.yellow(`āš ļø Preview failed: ${e.message}`));
205
+ if (!(e && (e.name === 'AbortError' || e.code === 'ABORT_ERR')) && !hideSpinner) {
206
+ console.log(chalk.yellow(`āš ļø Preview failed: ${e.message}`));
207
+ }
202
208
  return null;
209
+ } finally {
210
+ clearTimeout(timeoutId);
211
+ if (progressInterval) clearInterval(progressInterval);
212
+ if (spinner) spinner.stop();
213
+ if (externalSignal) externalSignal.removeEventListener('abort', abortOnExternal);
203
214
  }
204
215
  }
205
216
 
@@ -299,7 +310,7 @@ function printGpuTorchCudaSummary(result) {
299
310
  // Full fetch to get both setup commands and recommendations in one request
300
311
  async function fetchFullSetupAndRecs(repoUrl) {
301
312
  const envUrl = process.env.GITARSENAL_API_URL;
302
- const endpoints = envUrl ? [envUrl] : ['https://www.gitarsenal.dev/api/gitingest-setup-commands'];
313
+ const endpoints = envUrl ? [envUrl] : ['https://www.gitarsenal.dev/api/best_gpu'];
303
314
  const payload = {
304
315
  repoUrl,
305
316
  gitingestData: {
@@ -387,7 +398,7 @@ async function sendUserData(userId, userName, userEmail) {
387
398
  webhookUrl = process.env.GITARSENAL_WEBHOOK_URL;
388
399
  }
389
400
 
390
- console.log(chalk.gray(`šŸ“” Sending to: ${webhookUrl}`));
401
+ // console.log(chalk.gray(`šŸ“” Sending to: ${webhookUrl}`));
391
402
  console.log(chalk.gray(`šŸ“¦ Data: ${data}`));
392
403
 
393
404
  const urlObj = new URL(webhookUrl);
@@ -414,8 +425,6 @@ async function sendUserData(userId, userName, userEmail) {
414
425
  responseData += chunk;
415
426
  });
416
427
  res.on('end', () => {
417
- console.log(chalk.gray(`šŸ“Š Response status: ${res.statusCode}`));
418
- console.log(chalk.gray(`šŸ“„ Response: ${responseData}`));
419
428
 
420
429
  if (res.statusCode >= 200 && res.statusCode < 300) {
421
430
  console.log(chalk.green('āœ… User registered on GitArsenal dashboard'));
@@ -465,6 +474,35 @@ async function collectUserCredentials(options) {
465
474
  try {
466
475
  const config = JSON.parse(fs.readFileSync(userConfigPath, 'utf8'));
467
476
  if (config.userId && config.userName && config.userEmail) {
477
+ // Check if the email is a fake one (contains @example.com)
478
+ if (config.userEmail.includes('@example.com')) {
479
+ console.log(chalk.yellow('āš ļø Detected placeholder email address. Please update your credentials.'));
480
+ console.log(chalk.gray('We need your real email address for proper registration.'));
481
+
482
+ // Prompt for real email address
483
+ const emailUpdate = await inquirer.prompt([
484
+ {
485
+ type: 'input',
486
+ name: 'userEmail',
487
+ message: 'Enter your real email address:',
488
+ validate: (input) => {
489
+ const email = input.trim();
490
+ if (email === '') return 'Email address is required';
491
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
492
+ return 'Please enter a valid email address (e.g., user@example.com)';
493
+ }
494
+ return true;
495
+ }
496
+ }
497
+ ]);
498
+
499
+ // Update the config with real email
500
+ config.userEmail = emailUpdate.userEmail;
501
+ config.updatedAt = new Date().toISOString();
502
+ fs.writeFileSync(userConfigPath, JSON.stringify(config, null, 2));
503
+ console.log(chalk.green('āœ… Email address updated successfully!'));
504
+ }
505
+
468
506
  userId = config.userId;
469
507
  userName = config.userName;
470
508
  userEmail = config.userEmail;
@@ -496,10 +534,10 @@ async function collectUserCredentials(options) {
496
534
 
497
535
  if (authChoice.action === 'register') {
498
536
  console.log(chalk.blue('\nšŸ“ Create New Account'));
499
- const credentials = await inquirer.prompt([
500
- {
501
- type: 'input',
502
- name: 'userId',
537
+ const credentials = await inquirer.prompt([
538
+ {
539
+ type: 'input',
540
+ name: 'userId',
503
541
  message: 'Choose a username:',
504
542
  validate: (input) => {
505
543
  const username = input.trim();
@@ -521,14 +559,14 @@ async function collectUserCredentials(options) {
521
559
  }
522
560
  return true;
523
561
  }
524
- },
525
- {
526
- type: 'input',
527
- name: 'userName',
562
+ },
563
+ {
564
+ type: 'input',
565
+ name: 'userName',
528
566
  message: 'Enter your full name:',
529
- validate: (input) => input.trim() !== '' ? true : 'Name is required'
530
- },
531
- {
567
+ validate: (input) => input.trim() !== '' ? true : 'Name is required'
568
+ },
569
+ {
532
570
  type: 'password',
533
571
  name: 'password',
534
572
  message: 'Create a password (min 8 characters):',
@@ -545,11 +583,11 @@ async function collectUserCredentials(options) {
545
583
  if (input !== answers.password) return 'Passwords do not match';
546
584
  return true;
547
585
  }
548
- }
549
- ]);
550
-
551
- userId = credentials.userId;
552
- userName = credentials.userName;
586
+ }
587
+ ]);
588
+
589
+ userId = credentials.userId;
590
+ userName = credentials.userName;
553
591
  userEmail = credentials.userEmail;
554
592
 
555
593
  console.log(chalk.green('āœ… Account created successfully!'));
@@ -558,9 +596,34 @@ async function collectUserCredentials(options) {
558
596
  const credentials = await inquirer.prompt([
559
597
  {
560
598
  type: 'input',
561
- name: 'userIdentifier',
562
- message: 'Enter your username or email:',
563
- validate: (input) => input.trim() !== '' ? true : 'Username/email is required'
599
+ name: 'userId',
600
+ message: 'Enter your username:',
601
+ validate: (input) => {
602
+ const username = input.trim();
603
+ if (username === '') return 'Username is required';
604
+ if (username.length < 3) return 'Username must be at least 3 characters';
605
+ if (!/^[a-zA-Z0-9_-]+$/.test(username)) return 'Username can only contain letters, numbers, _ and -';
606
+ return true;
607
+ }
608
+ },
609
+ {
610
+ type: 'input',
611
+ name: 'userEmail',
612
+ message: 'Enter your email address:',
613
+ validate: (input) => {
614
+ const email = input.trim();
615
+ if (email === '') return 'Email address is required';
616
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
617
+ return 'Please enter a valid email address (e.g., user@example.com)';
618
+ }
619
+ return true;
620
+ }
621
+ },
622
+ {
623
+ type: 'input',
624
+ name: 'userName',
625
+ message: 'Enter your full name:',
626
+ validate: (input) => input.trim() !== '' ? true : 'Name is required'
564
627
  },
565
628
  {
566
629
  type: 'password',
@@ -570,12 +633,9 @@ async function collectUserCredentials(options) {
570
633
  }
571
634
  ]);
572
635
 
573
- // For now, we'll simulate successful login
574
- // In a real implementation, you'd validate against a user database
575
- const identifier = credentials.userIdentifier;
576
- userId = identifier.includes('@') ? identifier.split('@')[0] : identifier;
577
- userName = `User ${userId}`; // Would be fetched from database
578
- userEmail = identifier.includes('@') ? identifier : `${identifier}@example.com`;
636
+ userId = credentials.userId;
637
+ userName = credentials.userName;
638
+ userEmail = credentials.userEmail;
579
639
 
580
640
  console.log(chalk.green('āœ… Login successful!'));
581
641
  }
@@ -587,15 +647,15 @@ async function collectUserCredentials(options) {
587
647
  fs.mkdirSync(userConfigDir, { recursive: true });
588
648
  }
589
649
 
590
- const config = {
591
- userId,
592
- userName,
650
+ const config = {
651
+ userId,
652
+ userName,
593
653
  userEmail,
594
654
  savedAt: new Date().toISOString()
595
- };
655
+ };
596
656
  fs.writeFileSync(userConfigPath, JSON.stringify(config, null, 2));
597
657
  console.log(chalk.green('āœ… Credentials saved locally'));
598
- } catch (error) {
658
+ } catch (error) {
599
659
  console.log(chalk.yellow('āš ļø Could not save credentials locally'));
600
660
  }
601
661
  }
@@ -742,18 +802,49 @@ async function runContainerCommand(options) {
742
802
  repoUrl = answers.repoUrl;
743
803
  }
744
804
 
745
- // Attempt full fetch first to get both commands and recommendations; fallback to preview on failure
805
+ // Attempt full fetch first to get both commands and recommendations; now start preview concurrently
746
806
  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;
807
+ // Start a main spinner that will show overall progress
808
+ const mainSpinner = ora('Analyzing repository...').start();
809
+
810
+ try {
811
+ // Start preview immediately so we get early feedback; suppress summary here to avoid duplicates.
812
+ // Provide an AbortController so we can stop the preview spinner as soon as full fetch returns.
813
+ const previewAbort = new AbortController();
814
+ mainSpinner.text = 'Analyzing repository for GPU/Torch/CUDA recommendations...';
815
+ const previewPromise = previewRecommendations(repoUrl, { showSummary: false, abortSignal: previewAbort.signal, hideSpinner: true }).catch(() => null);
816
+
817
+ // Run full fetch in parallel; prefer its results if available.
818
+ mainSpinner.text = 'Finding the best machine for your code...';
819
+ const fullData = await fetchFullSetupAndRecs(repoUrl).catch(() => null);
820
+
821
+ if (fullData) {
822
+ // Stop preview spinner immediately since we have a response
823
+ previewAbort.abort();
824
+ mainSpinner.succeed('Analysis complete!');
825
+ printGpuTorchCudaSummary(fullData);
826
+ if (Array.isArray(fullData.commands) && fullData.commands.length) {
827
+ setupCommands = fullData.commands;
828
+ // Disable auto-detection since we already have commands
829
+ useApi = false;
830
+ }
831
+ } else {
832
+ // Full fetch failed, wait for preview and show its results
833
+ mainSpinner.text = 'Waiting for preview analysis to complete...';
834
+ const previewData = await previewPromise;
835
+ if (previewData) {
836
+ mainSpinner.succeed('Preview analysis complete!');
837
+ printGpuTorchCudaSummary(previewData);
838
+ } else {
839
+ mainSpinner.fail('Analysis failed - both preview and full analysis timed out or failed');
840
+ console.log(chalk.yellow('āš ļø Unable to analyze repository automatically.'));
841
+ console.log(chalk.gray('You can still proceed with manual setup commands.'));
842
+ }
754
843
  }
755
- } else {
756
- await previewRecommendations(repoUrl);
844
+ } catch (error) {
845
+ mainSpinner.fail(`Analysis failed: ${error.message}`);
846
+ console.log(chalk.yellow('āš ļø Unable to analyze repository automatically.'));
847
+ console.log(chalk.gray('You can still proceed with manual setup commands.'));
757
848
  }
758
849
  }
759
850
 
@@ -883,7 +974,10 @@ async function runContainerCommand(options) {
883
974
  volumeName,
884
975
  setupCommands,
885
976
  useApi,
886
- yes: skipConfirmation
977
+ yes: skipConfirmation,
978
+ userId,
979
+ userName,
980
+ userEmail
887
981
  });
888
982
 
889
983
  } 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.41",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -253,6 +253,24 @@ class CommandListManager:
253
253
 
254
254
  print("="*60)
255
255
 
256
+ def print_all_commands(self):
257
+ """Print all commands in the list for debugging/transparency."""
258
+ if not self.commands:
259
+ print("šŸ“‹ No commands in the list")
260
+ return
261
+
262
+ print("\n" + "="*80)
263
+ print("šŸ“‹ ALL SETUP COMMANDS")
264
+ print("="*80)
265
+ for i, cmd in enumerate(self.commands):
266
+ status_icon = {
267
+ 'pending': 'ā³',
268
+ 'success': 'āœ…',
269
+ 'failed': 'āŒ'
270
+ }.get(cmd['status'], 'ā“')
271
+ print(f" {i+1:2d}. {status_icon} {cmd['command']}")
272
+ print("="*80 + "\n")
273
+
256
274
  def get_failed_commands_for_llm(self):
257
275
  """Get failed commands for LLM analysis."""
258
276
  failed_commands = []
@@ -398,6 +398,14 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
398
398
  processed_commands = preprocess_commands_with_llm(setup_commands, stored_credentials, api_key)
399
399
  print(f"āš™ļø Running {len(processed_commands)} preprocessed setup commands with dynamic command list...")
400
400
 
401
+ # Display all setup commands that will be executed
402
+ print("\n" + "="*80)
403
+ print("šŸ“‹ SETUP COMMANDS TO BE EXECUTED")
404
+ print("="*80)
405
+ for i, cmd in enumerate(processed_commands, 1):
406
+ print(f" {i:2d}. {cmd}")
407
+ print("="*80 + "\n")
408
+
401
409
  # Create command list manager with processed commands
402
410
  cmd_manager = CommandListManager(processed_commands)
403
411
 
@@ -1611,7 +1619,7 @@ def get_setup_commands_from_gitingest(repo_url):
1611
1619
  url=api_url,
1612
1620
  payload=payload,
1613
1621
  max_retries=2,
1614
- timeout=180 # 3 minute timeout
1622
+ timeout=180, # 3 minute timeout
1615
1623
  )
1616
1624
 
1617
1625
  if not response:
@@ -2115,6 +2123,11 @@ if __name__ == "__main__":
2115
2123
  parser.add_argument('--store-api-key', type=str, help='Store API key for a service (e.g., openai, modal)')
2116
2124
  parser.add_argument('--skip-auth', action='store_true', help='Skip authentication check (for development)')
2117
2125
 
2126
+ # User credential arguments (passed from JavaScript CLI)
2127
+ parser.add_argument('--user-id', type=str, help='User email address (passed from JavaScript CLI)')
2128
+ parser.add_argument('--user-name', type=str, help='Username (passed from JavaScript CLI)')
2129
+ parser.add_argument('--display-name', type=str, help='Display name (passed from JavaScript CLI)')
2130
+
2118
2131
  args = parser.parse_args()
2119
2132
 
2120
2133
  # Initialize authentication manager
@@ -2149,8 +2162,11 @@ if __name__ == "__main__":
2149
2162
  show_usage_examples()
2150
2163
  sys.exit(0)
2151
2164
 
2152
- # Check authentication (unless skipped for development)
2153
- if not args.skip_auth:
2165
+ # Authentication is handled by the JavaScript CLI when credentials are passed
2166
+ if args.user_id and args.user_name and args.display_name:
2167
+ print(f"āœ… Authenticated as: {args.display_name} ({args.user_id})")
2168
+ elif not args.skip_auth:
2169
+ # Only perform authentication check if running Python script directly (not from CLI)
2154
2170
  if not _check_authentication(auth_manager):
2155
2171
  print("\nāŒ Authentication required. Please login or register first.")
2156
2172
  print("Use --login to login or --register to create an account.")