gitarsenal-cli 1.9.58 ā 1.9.60
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 +15 -115
- package/config.json +5 -0
- package/lib/sandbox.js +6 -4
- package/package.json +1 -1
- package/python/__pycache__/auth_manager.cpython-312.pyc +0 -0
- package/python/__pycache__/fetch_modal_tokens.cpython-312.pyc +0 -0
- package/python/llm_debugging.py +7 -11
- package/python/shell.py +7 -3
- package/python/test_modalSandboxScript.py +60 -298
package/.venv_status.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"created":"2025-08-
|
|
1
|
+
{"created":"2025-08-13T13:05:38.083Z","packages":["modal","gitingest","requests","anthropic"],"uv_version":"uv 0.8.4 (Homebrew 2025-07-30)"}
|
package/bin/gitarsenal.js
CHANGED
|
@@ -22,9 +22,6 @@ function activateVirtualEnvironment() {
|
|
|
22
22
|
const venvPath = path.join(__dirname, '..', '.venv');
|
|
23
23
|
const statusFile = path.join(__dirname, '..', '.venv_status.json');
|
|
24
24
|
|
|
25
|
-
// Debug: Log the path we're looking for
|
|
26
|
-
// console.log(chalk.gray(`š Looking for virtual environment at: ${venvPath}`));
|
|
27
|
-
|
|
28
25
|
// Check if virtual environment exists
|
|
29
26
|
if (!fs.existsSync(venvPath)) {
|
|
30
27
|
console.log(chalk.red('ā Virtual environment not found. Please reinstall the package:'));
|
|
@@ -41,7 +38,6 @@ function activateVirtualEnvironment() {
|
|
|
41
38
|
if (fs.existsSync(statusFile)) {
|
|
42
39
|
try {
|
|
43
40
|
const status = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
|
|
44
|
-
// console.log(chalk.gray(`š¦ Packages: ${status.packages.join(', ')}`));
|
|
45
41
|
} catch (error) {
|
|
46
42
|
console.log(chalk.gray('ā
Virtual environment found'));
|
|
47
43
|
}
|
|
@@ -103,6 +99,7 @@ function activateVirtualEnvironment() {
|
|
|
103
99
|
return true;
|
|
104
100
|
}
|
|
105
101
|
|
|
102
|
+
|
|
106
103
|
// Lightweight preview of GPU/Torch/CUDA recommendations prior to GPU selection
|
|
107
104
|
async function previewRecommendations(repoUrl, optsOrShowSummary = true) {
|
|
108
105
|
const showSummary = typeof optsOrShowSummary === 'boolean' ? optsOrShowSummary : (optsOrShowSummary?.showSummary ?? true);
|
|
@@ -214,46 +211,6 @@ async function previewRecommendations(repoUrl, optsOrShowSummary = true) {
|
|
|
214
211
|
}
|
|
215
212
|
}
|
|
216
213
|
|
|
217
|
-
function httpPostJson(urlString, body) {
|
|
218
|
-
return new Promise((resolve) => {
|
|
219
|
-
try {
|
|
220
|
-
const urlObj = new URL(urlString);
|
|
221
|
-
const data = JSON.stringify(body);
|
|
222
|
-
const options = {
|
|
223
|
-
hostname: urlObj.hostname,
|
|
224
|
-
port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
|
|
225
|
-
path: urlObj.pathname,
|
|
226
|
-
method: 'POST',
|
|
227
|
-
headers: {
|
|
228
|
-
'Content-Type': 'application/json',
|
|
229
|
-
'Content-Length': Buffer.byteLength(data),
|
|
230
|
-
'User-Agent': 'GitArsenal-CLI/1.0'
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
const client = urlObj.protocol === 'https:' ? https : http;
|
|
234
|
-
const req = client.request(options, (res) => {
|
|
235
|
-
let responseData = '';
|
|
236
|
-
res.on('data', (chunk) => {
|
|
237
|
-
responseData += chunk;
|
|
238
|
-
});
|
|
239
|
-
res.on('end', () => {
|
|
240
|
-
try {
|
|
241
|
-
const parsed = JSON.parse(responseData);
|
|
242
|
-
resolve(parsed);
|
|
243
|
-
} catch (err) {
|
|
244
|
-
resolve(null);
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
req.on('error', () => resolve(null));
|
|
249
|
-
req.write(data);
|
|
250
|
-
req.end();
|
|
251
|
-
} catch (e) {
|
|
252
|
-
resolve(null);
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
214
|
function printGpuTorchCudaSummary(result) {
|
|
258
215
|
try {
|
|
259
216
|
console.log(chalk.bold('\nš RESULT SUMMARY (GPU/Torch/CUDA)'));
|
|
@@ -307,66 +264,6 @@ function printGpuTorchCudaSummary(result) {
|
|
|
307
264
|
} catch {}
|
|
308
265
|
}
|
|
309
266
|
|
|
310
|
-
// Full fetch to get both setup commands and recommendations in one request
|
|
311
|
-
async function fetchFullSetupAndRecs(repoUrl) {
|
|
312
|
-
const envUrl = process.env.GITARSENAL_API_URL;
|
|
313
|
-
const endpoints = envUrl ? [envUrl] : ['https://www.gitarsenal.dev/api/best_gpu'];
|
|
314
|
-
const payload = {
|
|
315
|
-
repoUrl,
|
|
316
|
-
gitingestData: {
|
|
317
|
-
system_info: {
|
|
318
|
-
platform: process.platform,
|
|
319
|
-
python_version: process.version,
|
|
320
|
-
detected_language: 'Unknown',
|
|
321
|
-
detected_technologies: [],
|
|
322
|
-
file_count: 0,
|
|
323
|
-
repo_stars: 0,
|
|
324
|
-
repo_forks: 0,
|
|
325
|
-
primary_package_manager: 'Unknown',
|
|
326
|
-
complexity_level: 'Unknown'
|
|
327
|
-
},
|
|
328
|
-
repository_analysis: {
|
|
329
|
-
summary: `Repository: ${repoUrl}`,
|
|
330
|
-
tree: '',
|
|
331
|
-
content_preview: ''
|
|
332
|
-
},
|
|
333
|
-
success: true
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
const timeoutMs = Number(process.env.GITARSENAL_FULL_TIMEOUT_MS || 180000);
|
|
337
|
-
|
|
338
|
-
const fetchWithTimeout = async (url, body, timeout) => {
|
|
339
|
-
const controller = new AbortController();
|
|
340
|
-
const id = setTimeout(() => controller.abort(), timeout);
|
|
341
|
-
try {
|
|
342
|
-
const res = await fetch(url, {
|
|
343
|
-
method: 'POST',
|
|
344
|
-
headers: { 'Content-Type': 'application/json', 'User-Agent': 'GitArsenal-CLI/1.0' },
|
|
345
|
-
body: JSON.stringify(body),
|
|
346
|
-
redirect: 'follow',
|
|
347
|
-
signal: controller.signal
|
|
348
|
-
});
|
|
349
|
-
clearTimeout(id);
|
|
350
|
-
return res;
|
|
351
|
-
} catch (e) {
|
|
352
|
-
clearTimeout(id);
|
|
353
|
-
throw e;
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
for (const url of endpoints) {
|
|
358
|
-
try {
|
|
359
|
-
const res = await fetchWithTimeout(url, payload, timeoutMs);
|
|
360
|
-
if (!res.ok) continue;
|
|
361
|
-
const data = await res.json().catch(() => null);
|
|
362
|
-
if (data) return data;
|
|
363
|
-
} catch (_e) {
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
return null;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
267
|
// Helper to derive a default volume name from the repository URL
|
|
371
268
|
function getDefaultVolumeName(repoUrl) {
|
|
372
269
|
try {
|
|
@@ -423,13 +320,20 @@ function getDefaultVolumeName(repoUrl) {
|
|
|
423
320
|
}
|
|
424
321
|
}
|
|
425
322
|
|
|
323
|
+
// Full fetch to get both setup commands and recommendations in one request
|
|
324
|
+
async function fetchFullSetupAndRecs(repoUrl) {
|
|
325
|
+
// For now, just use the preview function but don't show summary to avoid duplicates
|
|
326
|
+
// The Python implementation will handle setup commands
|
|
327
|
+
return await previewRecommendations(repoUrl, { showSummary: false, hideSpinner: true });
|
|
328
|
+
}
|
|
329
|
+
|
|
426
330
|
// Function to send user data to web application
|
|
427
331
|
async function sendUserData(userId, userName, userEmail) {
|
|
428
332
|
try {
|
|
429
|
-
console.log(chalk.blue(`š Attempting to register user: ${userName} (${
|
|
333
|
+
console.log(chalk.blue(`š Attempting to register user: ${userName} (${userId})`));
|
|
430
334
|
|
|
431
335
|
const userData = {
|
|
432
|
-
email: userEmail, // Use
|
|
336
|
+
email: userEmail, // Use userId as email (assuming it's an email)
|
|
433
337
|
name: userName,
|
|
434
338
|
username: userId
|
|
435
339
|
};
|
|
@@ -453,8 +357,7 @@ async function sendUserData(userId, userName, userEmail) {
|
|
|
453
357
|
if (process.env.GITARSENAL_WEBHOOK_URL) {
|
|
454
358
|
webhookUrl = process.env.GITARSENAL_WEBHOOK_URL;
|
|
455
359
|
}
|
|
456
|
-
|
|
457
|
-
// console.log(chalk.gray(`š” Sending to: ${webhookUrl}`));
|
|
360
|
+
|
|
458
361
|
console.log(chalk.gray(`š¦ Data: ${data}`));
|
|
459
362
|
|
|
460
363
|
const urlObj = new URL(webhookUrl);
|
|
@@ -521,11 +424,10 @@ async function collectUserCredentials(options) {
|
|
|
521
424
|
let userName = options.userName;
|
|
522
425
|
let userEmail = options.userEmail;
|
|
523
426
|
|
|
524
|
-
// Check for
|
|
427
|
+
// Check for config file first
|
|
525
428
|
const os = require('os');
|
|
526
429
|
const userConfigDir = path.join(os.homedir(), '.gitarsenal');
|
|
527
430
|
const userConfigPath = path.join(userConfigDir, 'user-config.json');
|
|
528
|
-
|
|
529
431
|
if (fs.existsSync(userConfigPath)) {
|
|
530
432
|
try {
|
|
531
433
|
const config = JSON.parse(fs.readFileSync(userConfigPath, 'utf8'));
|
|
@@ -639,7 +541,7 @@ async function collectUserCredentials(options) {
|
|
|
639
541
|
if (input !== answers.password) return 'Passwords do not match';
|
|
640
542
|
return true;
|
|
641
543
|
}
|
|
642
|
-
|
|
544
|
+
}
|
|
643
545
|
]);
|
|
644
546
|
|
|
645
547
|
userId = credentials.userId;
|
|
@@ -947,7 +849,7 @@ async function runContainerCommand(options) {
|
|
|
947
849
|
]);
|
|
948
850
|
|
|
949
851
|
if (volumeAnswers.useVolume) {
|
|
950
|
-
volumeName =
|
|
852
|
+
volumeName = getDefaultVolumeName(repoUrl);
|
|
951
853
|
}
|
|
952
854
|
} else if (!volumeName && skipConfirmation) {
|
|
953
855
|
// If --yes flag is used and no volume specified, use default
|
|
@@ -1001,7 +903,6 @@ async function runContainerCommand(options) {
|
|
|
1001
903
|
}
|
|
1002
904
|
|
|
1003
905
|
// Confirm settings (configuration will be shown by Python script after GPU selection)
|
|
1004
|
-
// console.log(chalk.gray(`š Debug: skipConfirmation = ${skipConfirmation}, options.yes = ${options.yes}`));
|
|
1005
906
|
if (!skipConfirmation) {
|
|
1006
907
|
const confirmAnswers = await inquirer.prompt([
|
|
1007
908
|
{
|
|
@@ -1023,7 +924,6 @@ async function runContainerCommand(options) {
|
|
|
1023
924
|
|
|
1024
925
|
// Run the container
|
|
1025
926
|
try {
|
|
1026
|
-
// console.log(chalk.gray(`š Debug: skipConfirmation = ${skipConfirmation}`));
|
|
1027
927
|
await runContainer({
|
|
1028
928
|
repoUrl,
|
|
1029
929
|
gpuType,
|
|
@@ -1287,4 +1187,4 @@ async function handleKeysDelete(options) {
|
|
|
1287
1187
|
console.error(chalk.red(`Error: ${error.message}`));
|
|
1288
1188
|
process.exit(1);
|
|
1289
1189
|
}
|
|
1290
|
-
}
|
|
1190
|
+
}
|
package/config.json
ADDED
package/lib/sandbox.js
CHANGED
|
@@ -72,8 +72,9 @@ async function runContainer(options) {
|
|
|
72
72
|
|
|
73
73
|
// Run the Python script with show examples flag
|
|
74
74
|
const pythonExecutable = process.env.PYTHON_EXECUTABLE || 'python';
|
|
75
|
-
const pythonProcess = spawn(pythonExecutable, args, {
|
|
76
|
-
stdio: 'inherit' // Inherit stdio to show real-time output
|
|
75
|
+
const pythonProcess = spawn(pythonExecutable, ['-u', ...args], {
|
|
76
|
+
stdio: 'inherit', // Inherit stdio to show real-time output
|
|
77
|
+
env: { ...process.env, PYTHONUNBUFFERED: '1' } // Force unbuffered output
|
|
77
78
|
});
|
|
78
79
|
|
|
79
80
|
return new Promise((resolve, reject) => {
|
|
@@ -138,8 +139,9 @@ async function runContainer(options) {
|
|
|
138
139
|
try {
|
|
139
140
|
// Run the Python script
|
|
140
141
|
const pythonExecutable = process.env.PYTHON_EXECUTABLE || 'python';
|
|
141
|
-
const pythonProcess = spawn(pythonExecutable, args, {
|
|
142
|
-
stdio: 'inherit' // Inherit stdio to show real-time output
|
|
142
|
+
const pythonProcess = spawn(pythonExecutable, ['-u', ...args], {
|
|
143
|
+
stdio: 'inherit', // Inherit stdio to show real-time output
|
|
144
|
+
env: { ...process.env, PYTHONUNBUFFERED: '1' } // Force unbuffered output
|
|
143
145
|
});
|
|
144
146
|
|
|
145
147
|
// Handle process completion
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
package/python/llm_debugging.py
CHANGED
|
@@ -9,15 +9,11 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
def get_stored_credentials():
|
|
11
11
|
"""Load stored credentials from ~/.gitarsenal/credentials.json"""
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return {}
|
|
18
|
-
except Exception as e:
|
|
19
|
-
print(f"ā ļø Error loading stored credentials: {e}")
|
|
20
|
-
return {}
|
|
12
|
+
credentials_file = Path.home() / ".gitarsenal" / "credentials.json"
|
|
13
|
+
if credentials_file.exists():
|
|
14
|
+
with open(credentials_file, 'r') as f:
|
|
15
|
+
return json.load(f)
|
|
16
|
+
return {}
|
|
21
17
|
|
|
22
18
|
|
|
23
19
|
def generate_auth_context(stored_credentials):
|
|
@@ -35,7 +31,7 @@ def generate_auth_context(stored_credentials):
|
|
|
35
31
|
|
|
36
32
|
def get_current_debug_model():
|
|
37
33
|
"""Get the currently configured debugging model preference"""
|
|
38
|
-
return os.environ.get("GITARSENAL_DEBUG_MODEL", "
|
|
34
|
+
return os.environ.get("GITARSENAL_DEBUG_MODEL", "anthropic")
|
|
39
35
|
|
|
40
36
|
|
|
41
37
|
def _to_str(maybe_bytes):
|
|
@@ -536,7 +532,7 @@ def make_groq_request(api_key, prompt, retries=2):
|
|
|
536
532
|
|
|
537
533
|
def get_provider_rotation_order(preferred=None):
|
|
538
534
|
"""Return provider rotation order starting with preferred if valid."""
|
|
539
|
-
default_order = ["
|
|
535
|
+
default_order = ["anthropic", "openai", "groq", "openrouter"]
|
|
540
536
|
if preferred and preferred in default_order:
|
|
541
537
|
return [preferred] + [p for p in default_order if p != preferred]
|
|
542
538
|
return default_order
|
package/python/shell.py
CHANGED
|
@@ -180,7 +180,7 @@ class PersistentShell:
|
|
|
180
180
|
self.command_counter += 1
|
|
181
181
|
marker = f"CMD_DONE_{self.command_counter}_{uuid.uuid4().hex[:8]}"
|
|
182
182
|
|
|
183
|
-
print(f"š§ Executing: {command}")
|
|
183
|
+
print(f"š§ Executing: {command}", flush=True)
|
|
184
184
|
|
|
185
185
|
# Clear any existing output
|
|
186
186
|
self._clear_lines()
|
|
@@ -266,6 +266,8 @@ class PersistentShell:
|
|
|
266
266
|
command_stdout.append(line)
|
|
267
267
|
elif line.strip() and not line.startswith("$"): # Skip empty lines and prompt lines
|
|
268
268
|
command_stdout.append(line)
|
|
269
|
+
# Print line immediately for real-time streaming
|
|
270
|
+
print(line, flush=True)
|
|
269
271
|
|
|
270
272
|
if found_marker:
|
|
271
273
|
break
|
|
@@ -278,6 +280,8 @@ class PersistentShell:
|
|
|
278
280
|
for line in current_stderr:
|
|
279
281
|
if line.strip(): # Skip empty lines
|
|
280
282
|
command_stderr.append(line)
|
|
283
|
+
# Print stderr immediately for real-time streaming
|
|
284
|
+
print(f"{line}", flush=True)
|
|
281
285
|
|
|
282
286
|
# Check if command is waiting for user input
|
|
283
287
|
if not found_marker and time.time() - start_time > 5: # Wait at least 5 seconds before checking
|
|
@@ -313,8 +317,8 @@ class PersistentShell:
|
|
|
313
317
|
success = exit_code == 0 if exit_code is not None else len(command_stderr) == 0
|
|
314
318
|
|
|
315
319
|
if success:
|
|
316
|
-
|
|
317
|
-
|
|
320
|
+
# Don't print the summary output since we already streamed it line by line
|
|
321
|
+
pass
|
|
318
322
|
# Track virtual environment activation
|
|
319
323
|
if command.strip().startswith("source ") and "/bin/activate" in command:
|
|
320
324
|
venv_path = command.replace("source ", "").replace("/bin/activate", "").strip()
|
|
@@ -235,23 +235,9 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
235
235
|
|
|
236
236
|
# Check if Modal is authenticated
|
|
237
237
|
try:
|
|
238
|
-
# Print all environment variables for debugging
|
|
239
|
-
# print("š DEBUG: Checking environment variables")
|
|
240
238
|
modal_token_id = os.environ.get("MODAL_TOKEN_ID")
|
|
241
239
|
modal_token = os.environ.get("MODAL_TOKEN")
|
|
242
240
|
openai_api_key = os.environ.get("OPENAI_API_KEY")
|
|
243
|
-
# print(f"š token exists: {'Yes' if modal_token_id else 'No'}")
|
|
244
|
-
# print(f"š token exists: {'Yes' if modal_token else 'No'}")
|
|
245
|
-
# print(f"š openai_api_key exists: {'Yes' if openai_api_key else 'No'}")
|
|
246
|
-
if modal_token_id:
|
|
247
|
-
# print(f"š token length: {len(modal_token_id)}")
|
|
248
|
-
pass
|
|
249
|
-
if modal_token:
|
|
250
|
-
# print(f"š token length: {len(modal_token)}")
|
|
251
|
-
pass
|
|
252
|
-
if openai_api_key:
|
|
253
|
-
# print(f"š openai_api_key length: {len(openai_api_key)}")
|
|
254
|
-
pass
|
|
255
241
|
# Try to access Modal token to check authentication
|
|
256
242
|
try:
|
|
257
243
|
# Check if token is set in environment
|
|
@@ -342,62 +328,54 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
342
328
|
print("ā ļø Continuing without persistent volume")
|
|
343
329
|
volume = None
|
|
344
330
|
|
|
345
|
-
# Print debug info for authentication
|
|
346
|
-
# print("š Modal authentication debug info:")
|
|
347
331
|
modal_token = os.environ.get("MODAL_TOKEN_ID")
|
|
348
|
-
# print(f" - token in env: {'Yes' if modal_token else 'No'}")
|
|
349
|
-
# print(f" - Token length: {len(modal_token) if modal_token else 'N/A'}")
|
|
350
332
|
|
|
351
333
|
# Create SSH-enabled image
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
"gpg", "ca-certificates", "software-properties-common"
|
|
367
|
-
)
|
|
368
|
-
.uv_pip_install("uv", "modal", "gitingest", "requests", "openai", "anthropic", "exa-py") # Remove problematic CUDA packages
|
|
369
|
-
.run_commands(
|
|
370
|
-
# Create SSH directory
|
|
371
|
-
"mkdir -p /var/run/sshd",
|
|
372
|
-
"mkdir -p /root/.ssh",
|
|
373
|
-
"chmod 700 /root/.ssh",
|
|
374
|
-
|
|
375
|
-
# Configure SSH server
|
|
376
|
-
"sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
|
|
377
|
-
"sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
|
|
378
|
-
"sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
|
|
379
|
-
|
|
380
|
-
# SSH keep-alive settings
|
|
381
|
-
"echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
|
|
382
|
-
"echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
|
|
383
|
-
|
|
384
|
-
# Generate SSH host keys
|
|
385
|
-
"ssh-keygen -A",
|
|
386
|
-
|
|
387
|
-
# Set up a nice bash prompt
|
|
388
|
-
"echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
|
|
389
|
-
)
|
|
390
|
-
.add_local_file(os.path.join(current_dir, "shell.py"), "/python/shell.py") # Mount shell.py
|
|
391
|
-
.add_local_file(os.path.join(current_dir, "command_manager.py"), "/python/command_manager.py") # Mount command_manager.py
|
|
392
|
-
.add_local_file(os.path.join(current_dir, "fetch_modal_tokens.py"), "/python/fetch_modal_tokens.py") # Mount fetch_modal_token.py
|
|
393
|
-
.add_local_file(os.path.join(current_dir, "llm_debugging.py"), "/python/llm_debugging.py") # Mount llm_debugging.py
|
|
394
|
-
.add_local_file(os.path.join(current_dir, "credentials_manager.py"), "/python/credentials_manager.py") # Mount credentials_manager.py
|
|
395
|
-
|
|
334
|
+
print("š¦ Building SSH-enabled image...")
|
|
335
|
+
|
|
336
|
+
# Get the current directory path for mounting local Python sources
|
|
337
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
338
|
+
# print(f"š Current directory for mounting: {current_dir}")
|
|
339
|
+
|
|
340
|
+
# Use a more stable CUDA base image and avoid problematic packages
|
|
341
|
+
ssh_image = (
|
|
342
|
+
# modal.Image.from_registry("nvidia/cuda:12.4.0-devel-ubuntu22.04", add_python="3.11")
|
|
343
|
+
modal.Image.debian_slim()
|
|
344
|
+
.apt_install(
|
|
345
|
+
"openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
|
|
346
|
+
"python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
|
|
347
|
+
"gpg", "ca-certificates", "software-properties-common"
|
|
396
348
|
)
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
349
|
+
.uv_pip_install("uv", "modal", "gitingest", "requests", "openai", "anthropic", "exa-py") # Remove problematic CUDA packages
|
|
350
|
+
.run_commands(
|
|
351
|
+
# Create SSH directory
|
|
352
|
+
"mkdir -p /var/run/sshd",
|
|
353
|
+
"mkdir -p /root/.ssh",
|
|
354
|
+
"chmod 700 /root/.ssh",
|
|
355
|
+
|
|
356
|
+
# Configure SSH server
|
|
357
|
+
"sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
|
|
358
|
+
"sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
|
|
359
|
+
"sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
|
|
360
|
+
|
|
361
|
+
# SSH keep-alive settings
|
|
362
|
+
"echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
|
|
363
|
+
"echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
|
|
364
|
+
|
|
365
|
+
# Generate SSH host keys
|
|
366
|
+
"ssh-keygen -A",
|
|
367
|
+
|
|
368
|
+
# Set up a nice bash prompt
|
|
369
|
+
"echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
|
|
370
|
+
)
|
|
371
|
+
.add_local_file(os.path.join(current_dir, "shell.py"), "/python/shell.py") # Mount shell.py
|
|
372
|
+
.add_local_file(os.path.join(current_dir, "command_manager.py"), "/python/command_manager.py") # Mount command_manager.py
|
|
373
|
+
.add_local_file(os.path.join(current_dir, "fetch_modal_tokens.py"), "/python/fetch_modal_tokens.py") # Mount fetch_modal_token.py
|
|
374
|
+
.add_local_file(os.path.join(current_dir, "llm_debugging.py"), "/python/llm_debugging.py") # Mount llm_debugging.py
|
|
375
|
+
.add_local_file(os.path.join(current_dir, "credentials_manager.py"), "/python/credentials_manager.py") # Mount credentials_manager.py
|
|
376
|
+
|
|
377
|
+
)
|
|
378
|
+
print("ā
SSH image built successfully")
|
|
401
379
|
|
|
402
380
|
# Configure volumes if available
|
|
403
381
|
volumes_config = {}
|
|
@@ -498,9 +476,6 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
498
476
|
# Start SSH service
|
|
499
477
|
subprocess.run(["service", "ssh", "start"], check=True)
|
|
500
478
|
|
|
501
|
-
# Fetch setup commands from API if repo_url is provided and no commands exist
|
|
502
|
-
setup_commands = get_setup_commands_from_gitingest(repo_url)
|
|
503
|
-
|
|
504
479
|
# Preprocess setup commands using LLM to inject credentials
|
|
505
480
|
if setup_commands:
|
|
506
481
|
print(f"š§ Preprocessing {len(setup_commands)} setup commands with LLM to inject credentials...")
|
|
@@ -839,7 +814,7 @@ def fetch_setup_commands_from_api(repo_url):
|
|
|
839
814
|
|
|
840
815
|
# Define API endpoints to try in order - using only online endpoints
|
|
841
816
|
api_endpoints = [
|
|
842
|
-
|
|
817
|
+
"https://www.gitarsenal.dev/api/analyze-with-gitingest" # Working endpoint with www prefix
|
|
843
818
|
]
|
|
844
819
|
|
|
845
820
|
print(f"š Fetching setup commands from API for repository: {repo_url}")
|
|
@@ -874,6 +849,12 @@ def fetch_setup_commands_from_api(repo_url):
|
|
|
874
849
|
# Use gitingest CLI tool to analyze the repository directly from URL
|
|
875
850
|
print(f"š Running GitIngest analysis on {repo_url}...")
|
|
876
851
|
|
|
852
|
+
# Based on the help output, the correct format is:
|
|
853
|
+
# gitingest [OPTIONS] [SOURCE]
|
|
854
|
+
# With options:
|
|
855
|
+
# -o, --output TEXT Output file path
|
|
856
|
+
# --format TEXT Output format (json)
|
|
857
|
+
|
|
877
858
|
# Run gitingest command with proper parameters
|
|
878
859
|
gitingest_run_cmd = [
|
|
879
860
|
gitingest_cmd_name,
|
|
@@ -1103,115 +1084,7 @@ def fetch_setup_commands_from_api(repo_url):
|
|
|
1103
1084
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
1104
1085
|
|
|
1105
1086
|
def generate_fallback_commands(gitingest_data):
|
|
1106
|
-
|
|
1107
|
-
print("\n" + "="*80)
|
|
1108
|
-
print("š GENERATING FALLBACK SETUP COMMANDS")
|
|
1109
|
-
print("="*80)
|
|
1110
|
-
print("Using basic repository analysis to generate setup commands")
|
|
1111
|
-
|
|
1112
|
-
# Default commands that work for most repositories
|
|
1113
|
-
default_commands = [
|
|
1114
|
-
"apt-get update -y",
|
|
1115
|
-
"apt-get install -y git curl wget",
|
|
1116
|
-
"pip install --upgrade pip setuptools wheel"
|
|
1117
|
-
]
|
|
1118
|
-
|
|
1119
|
-
# If we don't have any analysis data, return default commands
|
|
1120
|
-
if not gitingest_data:
|
|
1121
|
-
print("ā ļø No repository analysis data available. Using default commands.")
|
|
1122
|
-
return default_commands
|
|
1123
|
-
|
|
1124
|
-
# Extract language and technologies information
|
|
1125
|
-
detected_language = gitingest_data.get("system_info", {}).get("detected_language", "Unknown")
|
|
1126
|
-
detected_technologies = gitingest_data.get("system_info", {}).get("detected_technologies", [])
|
|
1127
|
-
primary_package_manager = gitingest_data.get("system_info", {}).get("primary_package_manager", "Unknown")
|
|
1128
|
-
|
|
1129
|
-
# Add language-specific commands
|
|
1130
|
-
language_commands = []
|
|
1131
|
-
|
|
1132
|
-
print(f"š Detected primary language: {detected_language}")
|
|
1133
|
-
print(f"š Detected technologies: {', '.join(detected_technologies) if detected_technologies else 'None'}")
|
|
1134
|
-
print(f"š Detected package manager: {primary_package_manager}")
|
|
1135
|
-
|
|
1136
|
-
# Python-specific commands
|
|
1137
|
-
if detected_language == "Python" or primary_package_manager == "pip":
|
|
1138
|
-
print("š¦ Adding Python-specific setup commands")
|
|
1139
|
-
|
|
1140
|
-
# Check for requirements.txt
|
|
1141
|
-
requirements_check = [
|
|
1142
|
-
"if [ -f requirements.txt ]; then",
|
|
1143
|
-
" echo 'Installing from requirements.txt'",
|
|
1144
|
-
" pip install -r requirements.txt",
|
|
1145
|
-
"elif [ -f setup.py ]; then",
|
|
1146
|
-
" echo 'Installing from setup.py'",
|
|
1147
|
-
" pip install -e .",
|
|
1148
|
-
"fi"
|
|
1149
|
-
]
|
|
1150
|
-
language_commands.extend(requirements_check)
|
|
1151
|
-
|
|
1152
|
-
# Add common Python packages
|
|
1153
|
-
language_commands.append("pip install pytest numpy pandas matplotlib")
|
|
1154
|
-
|
|
1155
|
-
# JavaScript/Node.js specific commands
|
|
1156
|
-
elif detected_language in ["JavaScript", "TypeScript"] or primary_package_manager in ["npm", "yarn", "pnpm"]:
|
|
1157
|
-
print("š¦ Adding JavaScript/Node.js-specific setup commands")
|
|
1158
|
-
|
|
1159
|
-
# Install Node.js if not available
|
|
1160
|
-
language_commands.append("apt-get install -y nodejs npm")
|
|
1161
|
-
|
|
1162
|
-
# Check for package.json
|
|
1163
|
-
package_json_check = [
|
|
1164
|
-
"if [ -f package.json ]; then",
|
|
1165
|
-
" echo 'Installing from package.json'",
|
|
1166
|
-
" npm install",
|
|
1167
|
-
"fi"
|
|
1168
|
-
]
|
|
1169
|
-
language_commands.extend(package_json_check)
|
|
1170
|
-
|
|
1171
|
-
# Java specific commands
|
|
1172
|
-
elif detected_language == "Java" or primary_package_manager in ["maven", "gradle"]:
|
|
1173
|
-
print("š¦ Adding Java-specific setup commands")
|
|
1174
|
-
|
|
1175
|
-
language_commands.append("apt-get install -y openjdk-11-jdk maven gradle")
|
|
1176
|
-
|
|
1177
|
-
# Check for Maven or Gradle
|
|
1178
|
-
build_check = [
|
|
1179
|
-
"if [ -f pom.xml ]; then",
|
|
1180
|
-
" echo 'Building with Maven'",
|
|
1181
|
-
" mvn clean install -DskipTests",
|
|
1182
|
-
"elif [ -f build.gradle ]; then",
|
|
1183
|
-
" echo 'Building with Gradle'",
|
|
1184
|
-
" gradle build --no-daemon",
|
|
1185
|
-
"fi"
|
|
1186
|
-
]
|
|
1187
|
-
language_commands.extend(build_check)
|
|
1188
|
-
|
|
1189
|
-
# Go specific commands
|
|
1190
|
-
elif detected_language == "Go" or primary_package_manager == "go":
|
|
1191
|
-
print("š¦ Adding Go-specific setup commands")
|
|
1192
|
-
|
|
1193
|
-
language_commands.append("apt-get install -y golang-go")
|
|
1194
|
-
language_commands.append("go mod tidy")
|
|
1195
|
-
|
|
1196
|
-
# Rust specific commands
|
|
1197
|
-
elif detected_language == "Rust" or primary_package_manager == "cargo":
|
|
1198
|
-
print("š¦ Adding Rust-specific setup commands")
|
|
1199
|
-
|
|
1200
|
-
language_commands.append("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y")
|
|
1201
|
-
language_commands.append("source $HOME/.cargo/env")
|
|
1202
|
-
language_commands.append("cargo build")
|
|
1203
|
-
|
|
1204
|
-
# Combine all commands
|
|
1205
|
-
all_commands = default_commands + language_commands
|
|
1206
|
-
|
|
1207
|
-
# Fix the commands
|
|
1208
|
-
fixed_commands = fix_setup_commands(all_commands)
|
|
1209
|
-
|
|
1210
|
-
print("\nš Generated fallback setup commands:")
|
|
1211
|
-
for i, cmd in enumerate(fixed_commands, 1):
|
|
1212
|
-
print(f" {i}. {cmd}")
|
|
1213
|
-
|
|
1214
|
-
return fixed_commands
|
|
1087
|
+
return True
|
|
1215
1088
|
|
|
1216
1089
|
def generate_basic_repo_analysis_from_url(repo_url):
|
|
1217
1090
|
"""Generate basic repository analysis data from a repository URL."""
|
|
@@ -1263,120 +1136,7 @@ def generate_basic_repo_analysis_from_url(repo_url):
|
|
|
1263
1136
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
1264
1137
|
|
|
1265
1138
|
def generate_basic_repo_analysis(repo_dir):
|
|
1266
|
-
|
|
1267
|
-
import os
|
|
1268
|
-
import subprocess
|
|
1269
|
-
|
|
1270
|
-
# Detect language and technologies based on file extensions
|
|
1271
|
-
file_extensions = {}
|
|
1272
|
-
file_count = 0
|
|
1273
|
-
|
|
1274
|
-
for root, _, files in os.walk(repo_dir):
|
|
1275
|
-
for file in files:
|
|
1276
|
-
file_count += 1
|
|
1277
|
-
ext = os.path.splitext(file)[1].lower()
|
|
1278
|
-
if ext:
|
|
1279
|
-
file_extensions[ext] = file_extensions.get(ext, 0) + 1
|
|
1280
|
-
|
|
1281
|
-
# Determine primary language
|
|
1282
|
-
language_map = {
|
|
1283
|
-
'.py': 'Python',
|
|
1284
|
-
'.js': 'JavaScript',
|
|
1285
|
-
'.ts': 'TypeScript',
|
|
1286
|
-
'.jsx': 'JavaScript',
|
|
1287
|
-
'.tsx': 'TypeScript',
|
|
1288
|
-
'.java': 'Java',
|
|
1289
|
-
'.cpp': 'C++',
|
|
1290
|
-
'.c': 'C',
|
|
1291
|
-
'.go': 'Go',
|
|
1292
|
-
'.rs': 'Rust',
|
|
1293
|
-
'.rb': 'Ruby',
|
|
1294
|
-
'.php': 'PHP',
|
|
1295
|
-
'.swift': 'Swift',
|
|
1296
|
-
'.kt': 'Kotlin',
|
|
1297
|
-
'.cs': 'C#'
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
# Count files by language
|
|
1301
|
-
language_counts = {}
|
|
1302
|
-
for ext, count in file_extensions.items():
|
|
1303
|
-
if ext in language_map:
|
|
1304
|
-
lang = language_map[ext]
|
|
1305
|
-
language_counts[lang] = language_counts.get(lang, 0) + count
|
|
1306
|
-
|
|
1307
|
-
# Determine primary language
|
|
1308
|
-
primary_language = max(language_counts.items(), key=lambda x: x[1])[0] if language_counts else "Unknown"
|
|
1309
|
-
|
|
1310
|
-
# Detect package managers
|
|
1311
|
-
package_managers = []
|
|
1312
|
-
package_files = {
|
|
1313
|
-
'requirements.txt': 'pip',
|
|
1314
|
-
'setup.py': 'pip',
|
|
1315
|
-
'pyproject.toml': 'pip',
|
|
1316
|
-
'package.json': 'npm',
|
|
1317
|
-
'yarn.lock': 'yarn',
|
|
1318
|
-
'pnpm-lock.yaml': 'pnpm',
|
|
1319
|
-
'Cargo.toml': 'cargo',
|
|
1320
|
-
'go.mod': 'go',
|
|
1321
|
-
'Gemfile': 'bundler',
|
|
1322
|
-
'pom.xml': 'maven',
|
|
1323
|
-
'build.gradle': 'gradle',
|
|
1324
|
-
'composer.json': 'composer'
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
for file, manager in package_files.items():
|
|
1328
|
-
if os.path.exists(os.path.join(repo_dir, file)):
|
|
1329
|
-
package_managers.append(manager)
|
|
1330
|
-
|
|
1331
|
-
primary_package_manager = package_managers[0] if package_managers else "Unknown"
|
|
1332
|
-
|
|
1333
|
-
# Get README content
|
|
1334
|
-
readme_content = ""
|
|
1335
|
-
for readme_name in ['README.md', 'README', 'README.txt', 'readme.md']:
|
|
1336
|
-
readme_path = os.path.join(repo_dir, readme_name)
|
|
1337
|
-
if os.path.exists(readme_path):
|
|
1338
|
-
with open(readme_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
1339
|
-
readme_content = f.read()
|
|
1340
|
-
break
|
|
1341
|
-
|
|
1342
|
-
# Try to get repository info
|
|
1343
|
-
repo_info = {}
|
|
1344
|
-
try:
|
|
1345
|
-
# Get remote origin URL
|
|
1346
|
-
cmd = ["git", "config", "--get", "remote.origin.url"]
|
|
1347
|
-
result = subprocess.run(cmd, cwd=repo_dir, capture_output=True, text=True)
|
|
1348
|
-
if result.returncode == 0:
|
|
1349
|
-
repo_info["url"] = result.stdout.strip()
|
|
1350
|
-
|
|
1351
|
-
# Get commit count as a proxy for activity
|
|
1352
|
-
cmd = ["git", "rev-list", "--count", "HEAD"]
|
|
1353
|
-
result = subprocess.run(cmd, cwd=repo_dir, capture_output=True, text=True)
|
|
1354
|
-
if result.returncode == 0:
|
|
1355
|
-
repo_info["commit_count"] = int(result.stdout.strip())
|
|
1356
|
-
except Exception:
|
|
1357
|
-
pass
|
|
1358
|
-
|
|
1359
|
-
# Build the analysis data
|
|
1360
|
-
return {
|
|
1361
|
-
"system_info": {
|
|
1362
|
-
"platform": "linux", # Assuming Linux for container environment
|
|
1363
|
-
"python_version": "3.10", # Common Python version
|
|
1364
|
-
"detected_language": primary_language,
|
|
1365
|
-
"detected_technologies": list(language_counts.keys()),
|
|
1366
|
-
"file_count": file_count,
|
|
1367
|
-
"repo_stars": repo_info.get("stars", 0),
|
|
1368
|
-
"repo_forks": repo_info.get("forks", 0),
|
|
1369
|
-
"primary_package_manager": primary_package_manager,
|
|
1370
|
-
"complexity_level": "medium" # Default assumption
|
|
1371
|
-
},
|
|
1372
|
-
"repository_analysis": {
|
|
1373
|
-
"summary": f"Repository analysis for {repo_dir}",
|
|
1374
|
-
"readme_content": readme_content[:5000] if readme_content else "No README found",
|
|
1375
|
-
"package_managers": package_managers,
|
|
1376
|
-
"file_extensions": list(file_extensions.keys())
|
|
1377
|
-
},
|
|
1378
|
-
"success": True
|
|
1379
|
-
}
|
|
1139
|
+
return True
|
|
1380
1140
|
|
|
1381
1141
|
def fix_setup_commands(commands):
|
|
1382
1142
|
"""Fix setup commands by removing placeholders and comments."""
|
|
@@ -1556,8 +1316,8 @@ def make_api_request_with_retry(url, payload, max_retries=2, timeout=180):
|
|
|
1556
1316
|
if attempt > 0:
|
|
1557
1317
|
print(f"š Retry attempt {attempt}/{max_retries}...")
|
|
1558
1318
|
|
|
1559
|
-
print(f"š Making POST request to: {url}")
|
|
1560
|
-
print(f"ā³ Waiting up to {timeout//60} minutes for response...")
|
|
1319
|
+
# print(f"š Making POST request to: {url}")
|
|
1320
|
+
# print(f"ā³ Waiting up to {timeout//60} minutes for response...")
|
|
1561
1321
|
|
|
1562
1322
|
# Set allow_redirects=True to follow redirects automatically
|
|
1563
1323
|
response = requests.post(
|
|
@@ -1617,6 +1377,8 @@ def get_setup_commands_from_gitingest(repo_url):
|
|
|
1617
1377
|
api_endpoints = [
|
|
1618
1378
|
"https://www.gitarsenal.dev/api/gitingest-setup-commands",
|
|
1619
1379
|
"https://gitarsenal.dev/api/gitingest-setup-commands",
|
|
1380
|
+
"https://www.gitarsenal.dev/api/analyze-with-gitingest",
|
|
1381
|
+
"http://localhost:3000/api/gitingest-setup-commands"
|
|
1620
1382
|
]
|
|
1621
1383
|
|
|
1622
1384
|
# Generate basic gitingest data
|
|
@@ -1709,7 +1471,7 @@ def get_setup_commands_from_gitingest(repo_url):
|
|
|
1709
1471
|
# Try each API endpoint
|
|
1710
1472
|
for api_url in api_endpoints:
|
|
1711
1473
|
try:
|
|
1712
|
-
print(f"Trying API endpoint: {api_url}")
|
|
1474
|
+
# print(f"Trying API endpoint: {api_url}")
|
|
1713
1475
|
|
|
1714
1476
|
# Load stored credentials
|
|
1715
1477
|
stored_credentials = get_stored_credentials()
|
|
@@ -1947,7 +1709,7 @@ Return only the JSON array, no other text.
|
|
|
1947
1709
|
client = openai.OpenAI(api_key=api_key)
|
|
1948
1710
|
|
|
1949
1711
|
response = client.chat.completions.create(
|
|
1950
|
-
model="gpt-4.1",
|
|
1712
|
+
model="gpt-4.1",
|
|
1951
1713
|
messages=[
|
|
1952
1714
|
{"role": "system", "content": "You are a command preprocessing assistant that modifies setup commands to use available credentials and make them non-interactive."},
|
|
1953
1715
|
{"role": "user", "content": prompt}
|
|
@@ -2404,7 +2166,7 @@ if __name__ == "__main__":
|
|
|
2404
2166
|
|
|
2405
2167
|
# Use gitingest by default unless --no-gitingest is set
|
|
2406
2168
|
if args.repo_url and (args.use_gitingest and not args.no_gitingest):
|
|
2407
|
-
print("š Using gitingest approach to fetch setup commands (default)")
|
|
2169
|
+
# print("š Using gitingest approach to fetch setup commands (default)")
|
|
2408
2170
|
api_commands = get_setup_commands_from_gitingest(args.repo_url)
|
|
2409
2171
|
if api_commands:
|
|
2410
2172
|
setup_commands = api_commands
|