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 +1 -1
- package/bin/gitarsenal.js +119 -73
- package/lib/sandbox.js +12 -1
- package/package.json +1 -1
- package/python/test_modalSandboxScript.py +11 -3
package/.venv_status.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"created":"2025-08-
|
|
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
|
|
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
|
-
|
|
145
|
-
|
|
163
|
+
let data = null;
|
|
164
|
+
let lastErrorText = '';
|
|
146
165
|
|
|
147
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
-
|
|
201
|
+
if (showSummary) {
|
|
202
|
+
printGpuTorchCudaSummary(data);
|
|
203
|
+
}
|
|
198
204
|
return data;
|
|
199
205
|
} catch (e) {
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
527
|
-
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
type: 'input',
|
|
539
|
+
name: 'userName',
|
|
528
540
|
message: 'Enter your full name:',
|
|
529
|
-
|
|
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
|
-
|
|
552
|
-
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
|
|
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;
|
|
757
|
+
// Attempt full fetch first to get both commands and recommendations; now start preview concurrently
|
|
746
758
|
if (useApi && repoUrl) {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
}
|
|
756
|
-
|
|
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
|
@@ -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
|
-
#
|
|
2153
|
-
if
|
|
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.")
|