gitarsenal-cli 1.9.38 → 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:15:52.693Z","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
 
@@ -357,13 +369,14 @@ async function fetchFullSetupAndRecs(repoUrl) {
357
369
  }
358
370
 
359
371
  // Function to send user data to web application
360
- async function sendUserData(userId, userName) {
372
+ async function sendUserData(userId, userName, userEmail) {
361
373
  try {
362
- console.log(chalk.blue(`🔗 Attempting to register user: ${userName} (${userId})`));
374
+ console.log(chalk.blue(`🔗 Attempting to register user: ${userName} (${userEmail})`));
363
375
 
364
376
  const userData = {
365
- email: userId, // Use userId as email (assuming it's an email)
366
- name: userName
377
+ email: userEmail, // Use actual email address
378
+ name: userName,
379
+ username: userId
367
380
  };
368
381
 
369
382
  const data = JSON.stringify(userData);
@@ -453,70 +466,153 @@ async function sendUserData(userId, userName) {
453
466
  async function collectUserCredentials(options) {
454
467
  let userId = options.userId;
455
468
  let userName = options.userName;
469
+ let userEmail = options.userEmail;
456
470
 
457
- // Check for config file first
458
- const configPath = path.join(__dirname, '..', 'config.json');
459
- if (fs.existsSync(configPath)) {
471
+ // Check for user-specific config file first (in user's home directory)
472
+ const os = require('os');
473
+ const userConfigDir = path.join(os.homedir(), '.gitarsenal');
474
+ const userConfigPath = path.join(userConfigDir, 'user-config.json');
475
+
476
+ if (fs.existsSync(userConfigPath)) {
460
477
  try {
461
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
462
- if (config.userId && config.userName) {
478
+ const config = JSON.parse(fs.readFileSync(userConfigPath, 'utf8'));
479
+ if (config.userId && config.userName && config.userEmail) {
463
480
  userId = config.userId;
464
481
  userName = config.userName;
482
+ userEmail = config.userEmail;
483
+ console.log(chalk.green(`✅ Welcome back, ${userName}!`));
484
+ return { userId, userName, userEmail };
465
485
  }
466
486
  } catch (error) {
467
- console.log(chalk.yellow('⚠️ Could not read config file'));
487
+ console.log(chalk.yellow('⚠️ Could not read user config file'));
468
488
  }
469
489
  }
470
490
 
471
491
  // If not provided via CLI or config, prompt for them
472
- if (!userId || !userName) {
473
- console.log(chalk.blue('\n🔐 GitArsenal User Identification'));
474
- console.log(chalk.gray('Help us track your usage and improve GitArsenal!'));
492
+ if (!userId || !userName || !userEmail) {
493
+ console.log(chalk.blue('\n🔐 GitArsenal Authentication'));
494
+ console.log(chalk.gray('Create an account or login to use GitArsenal'));
475
495
  console.log(chalk.gray('Your credentials will be saved locally for future use.'));
476
496
 
497
+ const authChoice = await inquirer.prompt([
498
+ {
499
+ type: 'list',
500
+ name: 'action',
501
+ message: 'What would you like to do?',
502
+ choices: [
503
+ { name: 'Create new account', value: 'register' },
504
+ { name: 'Login with existing account', value: 'login' }
505
+ ]
506
+ }
507
+ ]);
508
+
509
+ if (authChoice.action === 'register') {
510
+ console.log(chalk.blue('\n📝 Create New Account'));
477
511
  const credentials = await inquirer.prompt([
478
512
  {
479
513
  type: 'input',
480
514
  name: 'userId',
481
- message: 'Enter your user ID (or email):',
482
- default: userId || 'anonymous',
483
- validate: (input) => input.trim() !== '' ? true : 'User ID is required'
515
+ message: 'Choose a username:',
516
+ validate: (input) => {
517
+ const username = input.trim();
518
+ if (username === '') return 'Username is required';
519
+ if (username.length < 3) return 'Username must be at least 3 characters';
520
+ if (!/^[a-zA-Z0-9_-]+$/.test(username)) return 'Username can only contain letters, numbers, _ and -';
521
+ return true;
522
+ }
523
+ },
524
+ {
525
+ type: 'input',
526
+ name: 'userEmail',
527
+ message: 'Enter your email address:',
528
+ validate: (input) => {
529
+ const email = input.trim();
530
+ if (email === '') return 'Email address is required';
531
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
532
+ return 'Please enter a valid email address (e.g., user@example.com)';
533
+ }
534
+ return true;
535
+ }
484
536
  },
485
537
  {
486
538
  type: 'input',
487
539
  name: 'userName',
488
- message: 'Enter your name:',
489
- default: userName || 'Anonymous User',
540
+ message: 'Enter your full name:',
490
541
  validate: (input) => input.trim() !== '' ? true : 'Name is required'
491
542
  },
492
543
  {
493
- type: 'confirm',
494
- name: 'saveConfig',
495
- message: 'Save these credentials for future use?',
496
- default: true
544
+ type: 'password',
545
+ name: 'password',
546
+ message: 'Create a password (min 8 characters):',
547
+ validate: (input) => {
548
+ if (input.length < 8) return 'Password must be at least 8 characters';
549
+ return true;
550
+ }
551
+ },
552
+ {
553
+ type: 'password',
554
+ name: 'confirmPassword',
555
+ message: 'Confirm your password:',
556
+ validate: (input, answers) => {
557
+ if (input !== answers.password) return 'Passwords do not match';
558
+ return true;
559
+ }
497
560
  }
498
561
  ]);
499
562
 
500
563
  userId = credentials.userId;
501
564
  userName = credentials.userName;
565
+ userEmail = credentials.userEmail;
566
+
567
+ console.log(chalk.green('✅ Account created successfully!'));
568
+ } else {
569
+ console.log(chalk.blue('\n🔑 Login'));
570
+ const credentials = await inquirer.prompt([
571
+ {
572
+ type: 'input',
573
+ name: 'userIdentifier',
574
+ message: 'Enter your username or email:',
575
+ validate: (input) => input.trim() !== '' ? true : 'Username/email is required'
576
+ },
577
+ {
578
+ type: 'password',
579
+ name: 'password',
580
+ message: 'Enter your password:',
581
+ validate: (input) => input.trim() !== '' ? true : 'Password is required'
582
+ }
583
+ ]);
584
+
585
+ // For now, we'll simulate successful login
586
+ // In a real implementation, you'd validate against a user database
587
+ const identifier = credentials.userIdentifier;
588
+ userId = identifier.includes('@') ? identifier.split('@')[0] : identifier;
589
+ userName = `User ${userId}`; // Would be fetched from database
590
+ userEmail = identifier.includes('@') ? identifier : `${identifier}@example.com`;
591
+
592
+ console.log(chalk.green('✅ Login successful!'));
593
+ }
502
594
 
503
- // Save to config file if requested
504
- if (credentials.saveConfig) {
505
- try {
595
+ // Save credentials to user-specific config file
596
+ try {
597
+ // Ensure user config directory exists
598
+ if (!fs.existsSync(userConfigDir)) {
599
+ fs.mkdirSync(userConfigDir, { recursive: true });
600
+ }
601
+
506
602
  const config = {
507
603
  userId,
508
604
  userName,
509
- webhookUrl: 'https://www.gitarsenal.dev/api/users'
605
+ userEmail,
606
+ savedAt: new Date().toISOString()
510
607
  };
511
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
512
- console.log(chalk.green('✅ Credentials saved to config file'));
608
+ fs.writeFileSync(userConfigPath, JSON.stringify(config, null, 2));
609
+ console.log(chalk.green('✅ Credentials saved locally'));
513
610
  } catch (error) {
514
- console.log(chalk.yellow('⚠️ Could not save config file'));
515
- }
611
+ console.log(chalk.yellow('⚠️ Could not save credentials locally'));
516
612
  }
517
613
  }
518
614
 
519
- return { userId, userName };
615
+ return { userId, userName, userEmail };
520
616
  }
521
617
 
522
618
  // Activate virtual environment
@@ -621,12 +717,12 @@ async function runContainerCommand(options) {
621
717
 
622
718
  // Collect user credentials
623
719
  const userCredentials = await collectUserCredentials(options);
624
- const { userId, userName } = userCredentials;
720
+ const { userId, userName, userEmail } = userCredentials;
625
721
 
626
722
  // Register user on dashboard immediately after collecting credentials
627
723
  console.log(chalk.blue('\n📝 Registering user on GitArsenal dashboard...'));
628
724
  // Send user data immediately so the dashboard records users
629
- await sendUserData(userId, userName);
725
+ await sendUserData(userId, userName, userEmail);
630
726
 
631
727
  // Check for required dependencies
632
728
  const spinner = ora('Checking dependencies...').start();
@@ -658,18 +754,49 @@ async function runContainerCommand(options) {
658
754
  repoUrl = answers.repoUrl;
659
755
  }
660
756
 
661
- // 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
662
758
  if (useApi && repoUrl) {
663
- const fullData = await fetchFullSetupAndRecs(repoUrl).catch(() => null);
664
- if (fullData) {
665
- printGpuTorchCudaSummary(fullData);
666
- if (Array.isArray(fullData.commands) && fullData.commands.length) {
667
- setupCommands = fullData.commands;
668
- // Disable auto-detection since we already have commands
669
- 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
+ }
670
795
  }
671
- } else {
672
- 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.'));
673
800
  }
674
801
  }
675
802
 
@@ -799,7 +926,10 @@ async function runContainerCommand(options) {
799
926
  volumeName,
800
927
  setupCommands,
801
928
  useApi,
802
- yes: skipConfirmation
929
+ yes: skipConfirmation,
930
+ userId,
931
+ userName,
932
+ userEmail
803
933
  });
804
934
 
805
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.38",
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.")