gitarsenal-cli 1.5.9 → 1.5.10
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.
@@ -13,7 +13,7 @@ import argparse
|
|
13
13
|
from pathlib import Path
|
14
14
|
|
15
15
|
# Parse command-line arguments
|
16
|
-
parser = argparse.ArgumentParser()
|
16
|
+
parser = argparse.ArgumentParser(description='Launch a Modal sandbox')
|
17
17
|
parser.add_argument('--proxy-url', help='URL of the proxy server')
|
18
18
|
parser.add_argument('--proxy-api-key', help='API key for the proxy server')
|
19
19
|
parser.add_argument('--gpu', default='A10G', help='GPU type to use')
|
@@ -27,35 +27,24 @@ args, unknown = parser.parse_known_args()
|
|
27
27
|
# Set proxy URL and API key in environment variables if provided
|
28
28
|
if args.proxy_url:
|
29
29
|
os.environ["MODAL_PROXY_URL"] = args.proxy_url
|
30
|
-
|
30
|
+
print(f"✅ Set MODAL_PROXY_URL from command line")
|
31
31
|
|
32
32
|
if args.proxy_api_key:
|
33
33
|
os.environ["MODAL_PROXY_API_KEY"] = args.proxy_api_key
|
34
|
-
|
34
|
+
print(f"✅ Set MODAL_PROXY_API_KEY from command line")
|
35
35
|
|
36
36
|
# First, try to fetch tokens from the proxy server
|
37
37
|
try:
|
38
38
|
# Import the fetch_modal_tokens module
|
39
|
-
print("🔄 Fetching tokens from proxy server...")
|
39
|
+
print("🔄 Fetching Modal tokens from proxy server...")
|
40
40
|
from fetch_modal_tokens import get_tokens
|
41
|
-
token_id, token_secret
|
42
|
-
|
43
|
-
# Debug print the tokens
|
44
|
-
# print("🔍 DEBUG: Modal Tokens Fetched:")
|
45
|
-
# print(f"🔍 DEBUG: Token ID: {token_id}")
|
46
|
-
# print(f"🔍 DEBUG: Token Secret: {token_secret}")
|
47
|
-
# print(f"🔍 DEBUG: OpenAI API Key: {openai_api_key}")
|
48
|
-
|
49
|
-
# Check if we got valid tokens
|
50
|
-
if token_id is None or token_secret is None:
|
51
|
-
raise ValueError("Could not get valid tokens")
|
52
|
-
|
53
|
-
print(f"✅ Tokens fetched successfully")
|
41
|
+
token_id, token_secret = get_tokens()
|
42
|
+
print(f"✅ Modal tokens fetched successfully")
|
54
43
|
|
55
44
|
# Explicitly set the environment variables again to be sure
|
56
45
|
os.environ["MODAL_TOKEN_ID"] = token_id
|
57
46
|
os.environ["MODAL_TOKEN_SECRET"] = token_secret
|
58
|
-
|
47
|
+
|
59
48
|
# Also set the old environment variable for backward compatibility
|
60
49
|
os.environ["MODAL_TOKEN"] = token_id
|
61
50
|
|
@@ -67,26 +56,26 @@ except Exception as e:
|
|
67
56
|
# Apply the comprehensive Modal token solution as fallback
|
68
57
|
try:
|
69
58
|
# Import the comprehensive solution module
|
70
|
-
|
59
|
+
print("🔄 Applying comprehensive Modal token solution...")
|
71
60
|
import modal_token_solution
|
72
|
-
|
61
|
+
print("✅ Comprehensive Modal token solution applied")
|
73
62
|
|
74
63
|
# Set token variables for later use
|
75
64
|
token = modal_token_solution.TOKEN_ID # For backward compatibility
|
76
65
|
except Exception as e:
|
77
|
-
|
66
|
+
print(f"⚠️ Error applying comprehensive Modal token solution: {e}")
|
78
67
|
|
79
68
|
# Fall back to the authentication patch
|
80
69
|
try:
|
81
70
|
# Import the patch module
|
82
|
-
|
71
|
+
print("🔄 Falling back to Modal authentication patch...")
|
83
72
|
import modal_auth_patch
|
84
|
-
|
73
|
+
print("✅ Modal authentication patch applied")
|
85
74
|
|
86
75
|
# Set token variables for later use
|
87
76
|
token = modal_auth_patch.TOKEN_ID # For backward compatibility
|
88
77
|
except Exception as e:
|
89
|
-
|
78
|
+
print(f"⚠️ Error applying Modal authentication patch: {e}")
|
90
79
|
|
91
80
|
# Fall back to fix_modal_token.py
|
92
81
|
try:
|
@@ -126,17 +115,17 @@ except Exception as e:
|
|
126
115
|
token = "ak-sLhYqCjkvixiYcb9LAuCHp" # Default token ID
|
127
116
|
|
128
117
|
# Print debug info
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
118
|
+
print(f"🔍 DEBUG: Checking environment variables")
|
119
|
+
print(f"🔍 MODAL_TOKEN_ID exists: {'Yes' if os.environ.get('MODAL_TOKEN_ID') else 'No'}")
|
120
|
+
print(f"🔍 MODAL_TOKEN_SECRET exists: {'Yes' if os.environ.get('MODAL_TOKEN_SECRET') else 'No'}")
|
121
|
+
print(f"🔍 MODAL_TOKEN exists: {'Yes' if os.environ.get('MODAL_TOKEN') else 'No'}")
|
133
122
|
if os.environ.get('MODAL_TOKEN_ID'):
|
134
|
-
print(f"🔍
|
123
|
+
print(f"🔍 MODAL_TOKEN_ID length: {len(os.environ.get('MODAL_TOKEN_ID'))}")
|
135
124
|
if os.environ.get('MODAL_TOKEN_SECRET'):
|
136
|
-
print(f"🔍
|
125
|
+
print(f"🔍 MODAL_TOKEN_SECRET length: {len(os.environ.get('MODAL_TOKEN_SECRET'))}")
|
137
126
|
if os.environ.get('MODAL_TOKEN'):
|
138
|
-
print(f"🔍
|
139
|
-
|
127
|
+
print(f"🔍 MODAL_TOKEN length: {len(os.environ.get('MODAL_TOKEN'))}")
|
128
|
+
print(f"✅ Modal token setup completed")
|
140
129
|
|
141
130
|
# Import modal after token setup
|
142
131
|
import modal
|
@@ -326,15 +315,17 @@ def handle_huggingface_login(sandbox, current_dir):
|
|
326
315
|
|
327
316
|
return exit_code == 0, stdout_buffer, stderr_buffer
|
328
317
|
|
318
|
+
def handle_interactive_command(cmd, sandbox, current_dir):
|
319
|
+
"""Handle interactive commands by prompting the user for input"""
|
320
|
+
print(f"⚠️ Interactive command detected: {cmd}")
|
321
|
+
print("⚠️ Some prompts may not be visible. If the command appears stuck, it may be waiting for input.")
|
322
|
+
|
323
|
+
# This is a placeholder for more sophisticated interactive command handling
|
324
|
+
# In a real implementation, you would need to handle specific interactive commands differently
|
325
|
+
return None
|
329
326
|
|
330
327
|
def call_openai_for_debug(command, error_output, api_key=None, current_dir=None, sandbox=None):
|
331
328
|
"""Call OpenAI to debug a failed command and suggest a fix"""
|
332
|
-
print("\n🔍 DEBUG: Starting LLM debugging...")
|
333
|
-
print(f"🔍 DEBUG: Command: {command}")
|
334
|
-
print(f"🔍 DEBUG: Error output length: {len(error_output) if error_output else 0}")
|
335
|
-
print(f"🔍 DEBUG: Current directory: {current_dir}")
|
336
|
-
print(f"🔍 DEBUG: Sandbox available: {sandbox is not None}")
|
337
|
-
|
338
329
|
# Define _to_str function locally to avoid NameError
|
339
330
|
def _to_str(maybe_bytes):
|
340
331
|
try:
|
@@ -360,86 +351,22 @@ def call_openai_for_debug(command, error_output, api_key=None, current_dir=None,
|
|
360
351
|
print("⚠️ Error output is empty. Cannot effectively debug the command.")
|
361
352
|
print("⚠️ Skipping OpenAI debugging due to lack of error information.")
|
362
353
|
return None
|
363
|
-
|
364
|
-
# Try to get API key from multiple sources
|
365
|
-
if not api_key:
|
366
|
-
print("🔍 DEBUG: No API key provided, searching for one...")
|
367
354
|
|
368
|
-
|
355
|
+
if not api_key:
|
356
|
+
# Try to get API key from environment
|
369
357
|
api_key = os.environ.get("OPENAI_API_KEY")
|
370
|
-
print(f"🔍 DEBUG: API key from environment: {'Found' if api_key else 'Not found'}")
|
371
|
-
if api_key:
|
372
|
-
print(f"🔍 DEBUG: Environment API key value: {api_key}")
|
373
|
-
|
374
|
-
# If not in environment, try to fetch from server using fetch_modal_tokens
|
375
|
-
if not api_key:
|
376
|
-
try:
|
377
|
-
print("🔍 DEBUG: Trying to fetch API key from server...")
|
378
|
-
from fetch_modal_tokens import get_tokens
|
379
|
-
_, _, api_key = get_tokens()
|
380
|
-
if api_key:
|
381
|
-
# print("✅ Successfully fetched OpenAI API key from server")
|
382
|
-
# print(f"🔍 DEBUG: Fetched OpenAI API key value: {api_key}")
|
383
|
-
# Set in environment for this session
|
384
|
-
os.environ["OPENAI_API_KEY"] = api_key
|
385
|
-
else:
|
386
|
-
print("⚠️ Could not fetch OpenAI API key from server")
|
387
|
-
except Exception as e:
|
388
|
-
print(f"⚠️ Error fetching API key from server: {e}")
|
389
|
-
|
390
|
-
# Store the API key in a persistent file if found
|
391
|
-
if api_key:
|
392
|
-
try:
|
393
|
-
os.makedirs(os.path.expanduser("~/.gitarsenal"), exist_ok=True)
|
394
|
-
with open(os.path.expanduser("~/.gitarsenal/openai_key"), "w") as f:
|
395
|
-
f.write(api_key)
|
396
|
-
print("✅ Saved OpenAI API key for future use")
|
397
|
-
except Exception as e:
|
398
|
-
print(f"⚠️ Could not save API key: {e}")
|
399
|
-
|
400
|
-
# Try to load from saved file if not in environment
|
401
|
-
if not api_key:
|
402
|
-
try:
|
403
|
-
key_file = os.path.expanduser("~/.gitarsenal/openai_key")
|
404
|
-
print(f"🔍 DEBUG: Checking for saved API key at: {key_file}")
|
405
|
-
if os.path.exists(key_file):
|
406
|
-
with open(key_file, "r") as f:
|
407
|
-
api_key = f.read().strip()
|
408
|
-
if api_key:
|
409
|
-
print("✅ Loaded OpenAI API key from saved file")
|
410
|
-
print(f"🔍 DEBUG: API key from file: {api_key}")
|
411
|
-
print(f"🔍 DEBUG: API key length: {len(api_key)}")
|
412
|
-
# Also set in environment for this session
|
413
|
-
os.environ["OPENAI_API_KEY"] = api_key
|
414
|
-
else:
|
415
|
-
print("🔍 DEBUG: Saved file exists but is empty")
|
416
|
-
else:
|
417
|
-
print("🔍 DEBUG: No saved API key file found")
|
418
|
-
except Exception as e:
|
419
|
-
print(f"⚠️ Could not load saved API key: {e}")
|
420
358
|
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
os.environ["OPENAI_API_KEY"] = api_key
|
433
|
-
else:
|
434
|
-
print(f"🔍 DEBUG: API key from credentials manager: Not found")
|
435
|
-
except ImportError as e:
|
436
|
-
print(f"🔍 DEBUG: Credentials manager not available: {e}")
|
437
|
-
# Fall back to direct input if credentials_manager is not available
|
438
|
-
pass
|
439
|
-
|
440
|
-
# Finally, prompt the user if still no API key
|
441
|
-
if not api_key:
|
442
|
-
print("🔍 DEBUG: No API key found in any source, prompting user...")
|
359
|
+
if not api_key:
|
360
|
+
# Use the CredentialsManager to get the API key
|
361
|
+
try:
|
362
|
+
from credentials_manager import CredentialsManager
|
363
|
+
credentials_manager = CredentialsManager()
|
364
|
+
api_key = credentials_manager.get_openai_api_key()
|
365
|
+
if not api_key:
|
366
|
+
print("❌ No API key provided. Skipping debugging.")
|
367
|
+
return None
|
368
|
+
except ImportError:
|
369
|
+
# Fall back to direct input if credentials_manager module is not available
|
443
370
|
print("\n" + "="*60)
|
444
371
|
print("🔑 OPENAI API KEY REQUIRED FOR DEBUGGING")
|
445
372
|
print("="*60)
|
@@ -454,9 +381,6 @@ def call_openai_for_debug(command, error_output, api_key=None, current_dir=None,
|
|
454
381
|
print("❌ No API key provided. Skipping debugging.")
|
455
382
|
return None
|
456
383
|
print("✅ API key received successfully!")
|
457
|
-
print(f"🔍 DEBUG: User-provided API key: {api_key}")
|
458
|
-
# Save the API key to environment for future use in this session
|
459
|
-
os.environ["OPENAI_API_KEY"] = api_key
|
460
384
|
except KeyboardInterrupt:
|
461
385
|
print("\n❌ API key input cancelled by user.")
|
462
386
|
return None
|
@@ -464,19 +388,9 @@ def call_openai_for_debug(command, error_output, api_key=None, current_dir=None,
|
|
464
388
|
print(f"❌ Error getting API key: {e}")
|
465
389
|
return None
|
466
390
|
|
467
|
-
#
|
468
|
-
if not api_key:
|
469
|
-
print("❌ No OpenAI API key available. Cannot perform LLM debugging.")
|
470
|
-
print("💡 To enable LLM debugging, set the OPENAI_API_KEY environment variable")
|
471
|
-
return None
|
472
|
-
|
473
|
-
print(f"✅ OpenAI API key available (length: {len(api_key)})")
|
474
|
-
|
475
|
-
# Gather additional context to help with debugging
|
391
|
+
# Get current directory context
|
476
392
|
directory_context = ""
|
477
393
|
system_info = ""
|
478
|
-
command_history = ""
|
479
|
-
file_context = ""
|
480
394
|
|
481
395
|
if sandbox:
|
482
396
|
try:
|
@@ -490,7 +404,6 @@ def call_openai_for_debug(command, error_output, api_key=None, current_dir=None,
|
|
490
404
|
uname -a
|
491
405
|
echo -e "\nPython Information:"
|
492
406
|
python --version
|
493
|
-
pip --version
|
494
407
|
echo -e "\nPackage Manager:"
|
495
408
|
which apt 2>/dev/null && echo "apt available" || echo "apt not available"
|
496
409
|
which yum 2>/dev/null && echo "yum available" || echo "yum not available"
|
@@ -545,72 +458,6 @@ Directory contents:
|
|
545
458
|
{parent_context}
|
546
459
|
"""
|
547
460
|
print("✅ Directory context gathered successfully")
|
548
|
-
|
549
|
-
# Check for relevant files that might provide additional context
|
550
|
-
# For example, if error mentions a specific file, try to get its content
|
551
|
-
relevant_files = []
|
552
|
-
error_files = re.findall(r'(?:No such file or directory|cannot open|not found): ([^\s:]+)', error_output)
|
553
|
-
if error_files:
|
554
|
-
for file_path in error_files:
|
555
|
-
# Clean up the file path
|
556
|
-
file_path = file_path.strip("'\"")
|
557
|
-
if not os.path.isabs(file_path):
|
558
|
-
file_path = os.path.join(current_dir, file_path)
|
559
|
-
|
560
|
-
# Try to get the parent directory if the file doesn't exist
|
561
|
-
if '/' in file_path:
|
562
|
-
parent_file_dir = os.path.dirname(file_path)
|
563
|
-
relevant_files.append(parent_file_dir)
|
564
|
-
|
565
|
-
# Look for package.json, requirements.txt, etc.
|
566
|
-
common_config_files = ["package.json", "requirements.txt", "pyproject.toml", "setup.py",
|
567
|
-
"Pipfile", "Dockerfile", "docker-compose.yml", "Makefile"]
|
568
|
-
|
569
|
-
for config_file in common_config_files:
|
570
|
-
check_cmd = f"test -f {current_dir}/{config_file}"
|
571
|
-
check_result = sandbox.exec("bash", "-c", check_cmd)
|
572
|
-
check_result.wait()
|
573
|
-
if check_result.returncode == 0:
|
574
|
-
relevant_files.append(f"{current_dir}/{config_file}")
|
575
|
-
|
576
|
-
# Get content of relevant files
|
577
|
-
if relevant_files:
|
578
|
-
file_context = "\nRelevant file contents:\n"
|
579
|
-
for file_path in relevant_files[:2]: # Limit to 2 files to avoid too much context
|
580
|
-
try:
|
581
|
-
file_check_cmd = f"test -f {file_path}"
|
582
|
-
file_check = sandbox.exec("bash", "-c", file_check_cmd)
|
583
|
-
file_check.wait()
|
584
|
-
|
585
|
-
if file_check.returncode == 0:
|
586
|
-
# It's a file, get its content
|
587
|
-
cat_cmd = f"cat {file_path}"
|
588
|
-
cat_result = sandbox.exec("bash", "-c", cat_cmd)
|
589
|
-
file_content = ""
|
590
|
-
for line in cat_result.stdout:
|
591
|
-
file_content += _to_str(line)
|
592
|
-
cat_result.wait()
|
593
|
-
|
594
|
-
# Truncate if too long
|
595
|
-
if len(file_content) > 1000:
|
596
|
-
file_content = file_content[:1000] + "\n... (truncated)"
|
597
|
-
|
598
|
-
file_context += f"\n--- {file_path} ---\n{file_content}\n"
|
599
|
-
else:
|
600
|
-
# It's a directory, list its contents
|
601
|
-
ls_cmd = f"ls -la {file_path}"
|
602
|
-
ls_dir_result = sandbox.exec("bash", "-c", ls_cmd)
|
603
|
-
dir_content = ""
|
604
|
-
for line in ls_dir_result.stdout:
|
605
|
-
dir_content += _to_str(line)
|
606
|
-
ls_dir_result.wait()
|
607
|
-
|
608
|
-
file_context += f"\n--- Directory: {file_path} ---\n{dir_content}\n"
|
609
|
-
except Exception as e:
|
610
|
-
print(f"⚠️ Error getting content of {file_path}: {e}")
|
611
|
-
|
612
|
-
print(f"✅ Additional file context gathered from {len(relevant_files)} relevant files")
|
613
|
-
|
614
461
|
except Exception as e:
|
615
462
|
print(f"⚠️ Error getting directory context: {e}")
|
616
463
|
directory_context = f"\nCurrent directory: {current_dir}\n"
|
@@ -642,262 +489,64 @@ But it failed with this error:
|
|
642
489
|
```
|
643
490
|
{system_info}
|
644
491
|
{directory_context}
|
645
|
-
{file_context}
|
646
|
-
|
647
492
|
Please analyze the error and provide ONLY a single terminal command that would fix the issue.
|
648
493
|
Consider the current directory, system information, and directory contents carefully before suggesting a solution.
|
649
494
|
|
650
|
-
IMPORTANT
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
- For other commands: check their documentation for the appropriate non-interactive flag
|
656
|
-
|
657
|
-
2. If the error indicates a file is not found (e.g., "No such file or directory", "cannot open", "not found"):
|
658
|
-
- FIRST try to search for the file using: find . -name "filename" -type f 2>/dev/null
|
659
|
-
- If found, navigate to that directory using: cd /path/to/directory
|
660
|
-
- If not found, then consider creating the file or installing missing packages
|
661
|
-
|
662
|
-
3. For missing packages or dependencies:
|
663
|
-
- Use pip install for Python packages
|
664
|
-
- Use apt-get install for system packages
|
665
|
-
- Use npm install for Node.js packages
|
666
|
-
|
667
|
-
4. For authentication issues:
|
668
|
-
- For wandb login: suggest 'wandb login YOUR_API_KEY' (system will prompt for actual key)
|
669
|
-
- For huggingface: suggest 'huggingface-cli login' (system will prompt for token)
|
495
|
+
IMPORTANT: For any commands that might ask for yes/no confirmation, use the appropriate non-interactive flag:
|
496
|
+
- For apt/apt-get: use -y or --yes
|
497
|
+
- For pip: use --no-input
|
498
|
+
- For rm: use -f or --force
|
499
|
+
- For other commands: check their documentation for the appropriate non-interactive flag
|
670
500
|
|
671
501
|
Do not provide any explanations, just the exact command to run.
|
672
502
|
"""
|
673
503
|
|
674
504
|
# Prepare the API request payload
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
if preferred_model:
|
685
|
-
# Insert the preferred model at the beginning of the list
|
686
|
-
models_to_try.insert(0, preferred_model)
|
687
|
-
print(f"✅ Using preferred model from environment: {preferred_model}")
|
688
|
-
|
689
|
-
# Remove duplicates while preserving order
|
690
|
-
models_to_try = list(dict.fromkeys(models_to_try))
|
691
|
-
print(f"🔍 DEBUG: Models to try: {models_to_try}")
|
692
|
-
|
693
|
-
# Function to make the API call with a specific model
|
694
|
-
def try_api_call(model_name, retries=2, backoff_factor=1.5):
|
695
|
-
print(f"🔍 DEBUG: Attempting API call with model: {model_name}")
|
696
|
-
print(f"🔍 DEBUG: API key available: {'Yes' if api_key else 'No'}")
|
697
|
-
# if api_key:
|
698
|
-
# print(f"🔍 DEBUG: API key length: {len(api_key)}")
|
699
|
-
# print(f"🔍 DEBUG: API key starts with: {api_key[:10]}...")
|
700
|
-
|
701
|
-
payload = {
|
702
|
-
"model": model_name,
|
703
|
-
"messages": [
|
704
|
-
{"role": "system", "content": "You are a debugging assistant. Provide only the terminal command to fix the issue. Analyze the issue first, understand why it's happening, then provide the command to fix it. For file not found errors, first search for the file using 'find . -name filename -type f' and navigate to the directory if found. For missing packages, use appropriate package managers (pip, apt-get, npm). For authentication, suggest login commands with placeholders."},
|
705
|
-
{"role": "user", "content": prompt}
|
706
|
-
],
|
707
|
-
"temperature": 0.2,
|
708
|
-
"max_tokens": 300
|
709
|
-
}
|
710
|
-
|
711
|
-
print(f"🔍 DEBUG: Payload prepared, prompt length: {len(prompt)}")
|
712
|
-
|
713
|
-
# Add specific handling for common errors
|
714
|
-
last_error = None
|
715
|
-
for attempt in range(retries + 1):
|
716
|
-
try:
|
717
|
-
if attempt > 0:
|
718
|
-
# Exponential backoff
|
719
|
-
wait_time = backoff_factor * (2 ** (attempt - 1))
|
720
|
-
print(f"⏱️ Retrying in {wait_time:.1f} seconds... (attempt {attempt+1}/{retries+1})")
|
721
|
-
time.sleep(wait_time)
|
722
|
-
|
723
|
-
print(f"🤖 Calling OpenAI with {model_name} model to debug the failed command...")
|
724
|
-
print(f"🔍 DEBUG: Making POST request to OpenAI API...")
|
725
|
-
response = requests.post(
|
726
|
-
"https://api.openai.com/v1/chat/completions",
|
727
|
-
headers=headers,
|
728
|
-
json=payload,
|
729
|
-
timeout=45 # Increased timeout for reliability
|
730
|
-
)
|
731
|
-
|
732
|
-
print(f"🔍 DEBUG: Response received, status code: {response.status_code}")
|
733
|
-
|
734
|
-
# Handle specific status codes
|
735
|
-
if response.status_code == 200:
|
736
|
-
print(f"🔍 DEBUG: Success! Response length: {len(response.text)}")
|
737
|
-
return response.json(), None
|
738
|
-
elif response.status_code == 401:
|
739
|
-
error_msg = "Authentication error: Invalid API key"
|
740
|
-
print(f"❌ {error_msg}")
|
741
|
-
print(f"🔍 DEBUG: Response text: {response.text}")
|
742
|
-
# Don't retry auth errors
|
743
|
-
return None, error_msg
|
744
|
-
elif response.status_code == 429:
|
745
|
-
error_msg = "Rate limit exceeded or quota reached"
|
746
|
-
print(f"⚠️ {error_msg}")
|
747
|
-
print(f"🔍 DEBUG: Response text: {response.text}")
|
748
|
-
# Always retry rate limit errors with increasing backoff
|
749
|
-
last_error = error_msg
|
750
|
-
continue
|
751
|
-
elif response.status_code == 500:
|
752
|
-
error_msg = "OpenAI server error"
|
753
|
-
print(f"⚠️ {error_msg}")
|
754
|
-
print(f"🔍 DEBUG: Response text: {response.text}")
|
755
|
-
# Retry server errors
|
756
|
-
last_error = error_msg
|
757
|
-
continue
|
758
|
-
else:
|
759
|
-
error_msg = f"Status code: {response.status_code}, Response: {response.text}"
|
760
|
-
print(f"⚠️ OpenAI API error: {error_msg}")
|
761
|
-
print(f"🔍 DEBUG: Full response text: {response.text}")
|
762
|
-
last_error = error_msg
|
763
|
-
# Only retry if we have attempts left
|
764
|
-
if attempt < retries:
|
765
|
-
continue
|
766
|
-
return None, error_msg
|
767
|
-
except requests.exceptions.Timeout:
|
768
|
-
error_msg = "Request timed out"
|
769
|
-
print(f"⚠️ {error_msg}")
|
770
|
-
print(f"🔍 DEBUG: Timeout after 45 seconds")
|
771
|
-
last_error = error_msg
|
772
|
-
# Always retry timeouts
|
773
|
-
continue
|
774
|
-
except requests.exceptions.ConnectionError:
|
775
|
-
error_msg = "Connection error"
|
776
|
-
print(f"⚠️ {error_msg}")
|
777
|
-
print(f"🔍 DEBUG: Connection failed to api.openai.com")
|
778
|
-
last_error = error_msg
|
779
|
-
# Always retry connection errors
|
780
|
-
continue
|
781
|
-
except Exception as e:
|
782
|
-
error_msg = str(e)
|
783
|
-
print(f"⚠️ Unexpected error: {error_msg}")
|
784
|
-
print(f"🔍 DEBUG: Exception type: {type(e).__name__}")
|
785
|
-
print(f"🔍 DEBUG: Exception details: {str(e)}")
|
786
|
-
last_error = error_msg
|
787
|
-
# Only retry if we have attempts left
|
788
|
-
if attempt < retries:
|
789
|
-
continue
|
790
|
-
return None, error_msg
|
791
|
-
|
792
|
-
# If we get here, all retries failed
|
793
|
-
return None, last_error
|
794
|
-
|
795
|
-
# Try each model in sequence until one works
|
796
|
-
result = None
|
797
|
-
last_error = None
|
798
|
-
|
799
|
-
for model in models_to_try:
|
800
|
-
result, error = try_api_call(model)
|
801
|
-
if result:
|
802
|
-
# print(f"✅ Successfully got response from {model}")
|
803
|
-
break
|
804
|
-
else:
|
805
|
-
print(f"⚠️ Failed to get response from {model}: {error}")
|
806
|
-
last_error = error
|
807
|
-
|
808
|
-
if not result:
|
809
|
-
print(f"❌ All model attempts failed. Last error: {last_error}")
|
810
|
-
return None
|
505
|
+
payload = {
|
506
|
+
"model": "gpt-4.1",
|
507
|
+
"messages": [
|
508
|
+
{"role": "system", "content": "You are a debugging assistant. Provide only the terminal command to fix the issue, analyze the issue first understand why its happening and then provide the command to fix the issue. If you see missing pytest errors, suggest 'pip install pytest'. For wandb login issues, suggest 'wandb login YOUR_API_KEY' and the system will handle prompting for the actual key."},
|
509
|
+
{"role": "user", "content": prompt}
|
510
|
+
],
|
511
|
+
"temperature": 0.2,
|
512
|
+
"max_tokens": 300
|
513
|
+
}
|
811
514
|
|
812
|
-
# Process the response
|
813
515
|
try:
|
814
|
-
print(
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
821
|
-
# Save the original response for debugging
|
822
|
-
original_response = fix_command
|
823
|
-
|
824
|
-
# Extract just the command if it's wrapped in backticks or explanation
|
825
|
-
if "```" in fix_command:
|
826
|
-
# Extract content between backticks
|
827
|
-
import re
|
828
|
-
code_blocks = re.findall(r'```(?:bash|sh)?\s*(.*?)\s*```', fix_command, re.DOTALL)
|
829
|
-
if code_blocks:
|
830
|
-
fix_command = code_blocks[0].strip()
|
831
|
-
print(f"✅ Extracted command from code block: {fix_command}")
|
832
|
-
|
833
|
-
# If the response still has explanatory text, try to extract just the command
|
834
|
-
if len(fix_command.split('\n')) > 1:
|
835
|
-
# First try to find lines that look like commands (start with common command prefixes)
|
836
|
-
command_prefixes = ['sudo', 'apt', 'pip', 'npm', 'yarn', 'git', 'cd', 'mv', 'cp', 'rm', 'mkdir', 'touch',
|
837
|
-
'chmod', 'chown', 'echo', 'cat', 'python', 'python3', 'node', 'export',
|
838
|
-
'curl', 'wget', 'docker', 'make', 'gcc', 'g++', 'javac', 'java',
|
839
|
-
'conda', 'uv', 'poetry', 'nvm', 'rbenv', 'pyenv', 'rustup']
|
840
|
-
|
841
|
-
# Check for lines that start with common command prefixes
|
842
|
-
command_lines = [line.strip() for line in fix_command.split('\n')
|
843
|
-
if any(line.strip().startswith(prefix) for prefix in command_prefixes)]
|
844
|
-
|
845
|
-
if command_lines:
|
846
|
-
# Use the first command line found
|
847
|
-
fix_command = command_lines[0]
|
848
|
-
print(f"✅ Identified command by prefix: {fix_command}")
|
849
|
-
else:
|
850
|
-
# Try to find lines that look like commands (contain common shell patterns)
|
851
|
-
shell_patterns = [' | ', ' > ', ' >> ', ' && ', ' || ', ' ; ', '$(', '`', ' -y ', ' --yes ']
|
852
|
-
command_lines = [line.strip() for line in fix_command.split('\n')
|
853
|
-
if any(pattern in line for pattern in shell_patterns)]
|
854
|
-
|
855
|
-
if command_lines:
|
856
|
-
# Use the first command line found
|
857
|
-
fix_command = command_lines[0]
|
858
|
-
print(f"✅ Identified command by shell pattern: {fix_command}")
|
859
|
-
else:
|
860
|
-
# Fall back to the shortest non-empty line as it's likely the command
|
861
|
-
lines = [line.strip() for line in fix_command.split('\n') if line.strip()]
|
862
|
-
if lines:
|
863
|
-
# Exclude very short lines that are likely not commands
|
864
|
-
valid_lines = [line for line in lines if len(line) > 5]
|
865
|
-
if valid_lines:
|
866
|
-
fix_command = min(valid_lines, key=len)
|
867
|
-
else:
|
868
|
-
fix_command = min(lines, key=len)
|
869
|
-
print(f"✅ Selected shortest line as command: {fix_command}")
|
870
|
-
|
871
|
-
# Clean up the command - remove any trailing periods or quotes
|
872
|
-
fix_command = fix_command.rstrip('.;"\'')
|
873
|
-
|
874
|
-
# Remove common prefixes that LLMs sometimes add
|
875
|
-
prefixes_to_remove = [
|
876
|
-
"Run: ", "Execute: ", "Try: ", "Command: ", "Fix: ", "Solution: ",
|
877
|
-
"You should run: ", "You can run: ", "You need to run: "
|
878
|
-
]
|
879
|
-
for prefix in prefixes_to_remove:
|
880
|
-
if fix_command.startswith(prefix):
|
881
|
-
fix_command = fix_command[len(prefix):].strip()
|
882
|
-
print(f"✅ Removed prefix: {prefix}")
|
883
|
-
break
|
884
|
-
|
885
|
-
# If the command is still multi-line or very long, it might not be a valid command
|
886
|
-
if len(fix_command.split('\n')) > 1 or len(fix_command) > 500:
|
887
|
-
print("⚠️ Extracted command appears invalid (multi-line or too long)")
|
888
|
-
print("🔍 Original response from LLM:")
|
889
|
-
print("-" * 60)
|
890
|
-
print(original_response)
|
891
|
-
print("-" * 60)
|
892
|
-
print("⚠️ Using best guess for command")
|
516
|
+
print("🤖 Calling OpenAI to debug the failed command...")
|
517
|
+
response = requests.post(
|
518
|
+
"https://api.openai.com/v1/chat/completions",
|
519
|
+
headers=headers,
|
520
|
+
json=payload,
|
521
|
+
timeout=30
|
522
|
+
)
|
893
523
|
|
894
|
-
|
895
|
-
|
896
|
-
|
524
|
+
if response.status_code == 200:
|
525
|
+
result = response.json()
|
526
|
+
fix_command = result["choices"][0]["message"]["content"].strip()
|
527
|
+
|
528
|
+
# Extract just the command if it's wrapped in backticks or explanation
|
529
|
+
if "```" in fix_command:
|
530
|
+
# Extract content between backticks
|
531
|
+
import re
|
532
|
+
code_blocks = re.findall(r'```(?:bash|sh)?\s*(.*?)\s*```', fix_command, re.DOTALL)
|
533
|
+
if code_blocks:
|
534
|
+
fix_command = code_blocks[0].strip()
|
535
|
+
|
536
|
+
# If the response still has explanatory text, try to extract just the command
|
537
|
+
if len(fix_command.split('\n')) > 1:
|
538
|
+
# Take the shortest non-empty line as it's likely the command
|
539
|
+
lines = [line.strip() for line in fix_command.split('\n') if line.strip()]
|
540
|
+
if lines:
|
541
|
+
fix_command = min(lines, key=len)
|
542
|
+
|
543
|
+
print(f"🔧 Suggested fix: {fix_command}")
|
544
|
+
return fix_command
|
545
|
+
else:
|
546
|
+
print(f"❌ OpenAI API error: {response.status_code} - {response.text}")
|
547
|
+
return None
|
897
548
|
except Exception as e:
|
898
|
-
print(f"❌ Error
|
899
|
-
print(f"🔍 DEBUG: Exception type: {type(e).__name__}")
|
900
|
-
print(f"🔍 DEBUG: Exception details: {str(e)}")
|
549
|
+
print(f"❌ Error calling OpenAI API: {e}")
|
901
550
|
return None
|
902
551
|
|
903
552
|
def prompt_for_hf_token():
|
@@ -937,6 +586,1518 @@ def prompt_for_hf_token():
|
|
937
586
|
print(f"❌ Error getting token: {e}")
|
938
587
|
return None
|
939
588
|
|
589
|
+
def create_modal_sandbox(gpu_type, repo_url=None, repo_name=None, setup_commands=None, volume_name=None):
|
590
|
+
# Import the credentials manager if available
|
591
|
+
try:
|
592
|
+
from credentials_manager import CredentialsManager
|
593
|
+
credentials_manager = CredentialsManager()
|
594
|
+
except ImportError:
|
595
|
+
credentials_manager = None
|
596
|
+
print("⚠️ Credentials manager not found, will use environment variables or prompt for credentials")
|
597
|
+
|
598
|
+
# Check if Modal is authenticated
|
599
|
+
try:
|
600
|
+
# Try to import modal first to check if it's installed
|
601
|
+
import modal
|
602
|
+
|
603
|
+
# Try to access Modal token to check authentication
|
604
|
+
try:
|
605
|
+
# This will raise an exception if not authenticated
|
606
|
+
modal.config.get_current_workspace_name()
|
607
|
+
print("✅ Modal authentication verified")
|
608
|
+
except modal.exception.AuthError:
|
609
|
+
print("\n" + "="*80)
|
610
|
+
print("🔑 MODAL AUTHENTICATION REQUIRED")
|
611
|
+
print("="*80)
|
612
|
+
print("GitArsenal requires Modal authentication to create cloud environments.")
|
613
|
+
|
614
|
+
# Try to get token from credentials manager
|
615
|
+
modal_token = None
|
616
|
+
if credentials_manager:
|
617
|
+
try:
|
618
|
+
modal_token = credentials_manager.get_modal_token()
|
619
|
+
if modal_token:
|
620
|
+
# Set the token in the environment
|
621
|
+
os.environ["MODAL_TOKEN_ID"] = modal_token
|
622
|
+
print("✅ Modal token set from credentials manager")
|
623
|
+
|
624
|
+
# Try to authenticate with the token
|
625
|
+
try:
|
626
|
+
import subprocess
|
627
|
+
token_result = subprocess.run(
|
628
|
+
["modal", "token", "set", "--from-env"],
|
629
|
+
capture_output=True, text=True
|
630
|
+
)
|
631
|
+
if token_result.returncode == 0:
|
632
|
+
print("✅ Successfully authenticated with Modal")
|
633
|
+
else:
|
634
|
+
print(f"⚠️ Failed to authenticate with Modal: {token_result.stderr}")
|
635
|
+
print("\nPlease authenticate manually:")
|
636
|
+
print("1. Run 'modal token new' to get a new token")
|
637
|
+
print("2. Then restart this command")
|
638
|
+
return None
|
639
|
+
except Exception as e:
|
640
|
+
print(f"⚠️ Error setting Modal token: {e}")
|
641
|
+
return None
|
642
|
+
except Exception as e:
|
643
|
+
print(f"⚠️ Error getting Modal token: {e}")
|
644
|
+
|
645
|
+
if not modal_token:
|
646
|
+
print("\nTo authenticate with Modal, you need to:")
|
647
|
+
print("1. Create a Modal account at https://modal.com if you don't have one")
|
648
|
+
print("2. Run the following command to get a token:")
|
649
|
+
print(" modal token new")
|
650
|
+
print("3. Then set up your credentials in GitArsenal:")
|
651
|
+
print(" ./gitarsenal.py credentials set modal_token")
|
652
|
+
print("\nAfter completing these steps, try your command again.")
|
653
|
+
print("="*80)
|
654
|
+
return None
|
655
|
+
except ImportError:
|
656
|
+
print("\n" + "="*80)
|
657
|
+
print("❌ MODAL PACKAGE NOT INSTALLED")
|
658
|
+
print("="*80)
|
659
|
+
print("GitArsenal requires the Modal package to be installed.")
|
660
|
+
print("\nTo install Modal, run:")
|
661
|
+
print(" pip install modal")
|
662
|
+
print("\nAfter installation, authenticate with Modal:")
|
663
|
+
print("1. Run 'modal token new'")
|
664
|
+
print("2. Then run './gitarsenal.py credentials set modal_token'")
|
665
|
+
print("="*80)
|
666
|
+
return None
|
667
|
+
except Exception as e:
|
668
|
+
print(f"⚠️ Error checking Modal authentication: {e}")
|
669
|
+
print("Continuing anyway, but Modal operations may fail")
|
670
|
+
|
671
|
+
# Execution history for tracking all commands and their results in this session
|
672
|
+
execution_history = []
|
673
|
+
|
674
|
+
# Track session start time
|
675
|
+
session_start = datetime.datetime.now().isoformat()
|
676
|
+
|
677
|
+
# Track previous errors to detect repeated failures
|
678
|
+
previous_errors = {}
|
679
|
+
|
680
|
+
# Track Python version management
|
681
|
+
conda_installed = False
|
682
|
+
python_version_switched = False
|
683
|
+
current_python_version = None
|
684
|
+
|
685
|
+
# Generate a unique app name with timestamp to avoid conflicts
|
686
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
687
|
+
app_name = f"sandbox-{timestamp}"
|
688
|
+
|
689
|
+
gpu_configs = {
|
690
|
+
'T4': {'gpu': 'T4', 'memory': 16},
|
691
|
+
'L4': {'gpu': 'L4', 'memory': 24},
|
692
|
+
'A10G': {'gpu': 'A10G', 'memory': 24},
|
693
|
+
'A100-40GB': {'gpu': 'A100-SXM4-40GB', 'memory': 40},
|
694
|
+
'A100-80GB': {'gpu': 'A100-80GB', 'memory': 80},
|
695
|
+
'L40S': {'gpu': 'L40S', 'memory': 48},
|
696
|
+
'H100': {'gpu': 'H100', 'memory': 80},
|
697
|
+
'H200': {'gpu': 'H200', 'memory': 141},
|
698
|
+
'B200': {'gpu': 'B200', 'memory': 96}
|
699
|
+
}
|
700
|
+
|
701
|
+
if gpu_type not in gpu_configs:
|
702
|
+
print(f"⚠️ Unknown GPU type: {gpu_type}. Using A10G as default.")
|
703
|
+
gpu_type = 'A10G'
|
704
|
+
|
705
|
+
gpu_spec = gpu_configs[gpu_type]
|
706
|
+
print(f"🚀 Creating Modal sandbox with {gpu_spec['gpu']} GPU ({gpu_spec['memory']}GB VRAM)")
|
707
|
+
|
708
|
+
# Initialize uv_path variable
|
709
|
+
uv_path = ""
|
710
|
+
|
711
|
+
# Setup volume if specified
|
712
|
+
volume = None
|
713
|
+
volume_mount_path = "/persistent"
|
714
|
+
|
715
|
+
if volume_name:
|
716
|
+
print(f"📦 Setting up volume: {volume_name}")
|
717
|
+
try:
|
718
|
+
# Try to get existing volume or create new one
|
719
|
+
volume = modal.Volume.from_name(volume_name, create_if_missing=True)
|
720
|
+
print(f"✅ Volume '{volume_name}' ready for use")
|
721
|
+
except Exception as e:
|
722
|
+
print(f"⚠️ Could not setup volume '{volume_name}': {e}")
|
723
|
+
print("⚠️ Continuing without persistent volume")
|
724
|
+
volume = None
|
725
|
+
else:
|
726
|
+
# Create a default volume for this session
|
727
|
+
default_volume_name = f"sandbox-vol-{timestamp}"
|
728
|
+
print(f"📦 Creating default volume: {default_volume_name}")
|
729
|
+
try:
|
730
|
+
volume = modal.Volume.from_name(default_volume_name, create_if_missing=True)
|
731
|
+
volume_name = default_volume_name
|
732
|
+
print(f"✅ Default volume '{default_volume_name}' created")
|
733
|
+
except Exception as e:
|
734
|
+
print(f"⚠️ Could not create default volume: {e}")
|
735
|
+
print("⚠️ Continuing without persistent volume")
|
736
|
+
volume = None
|
737
|
+
|
738
|
+
# Enable output for image building
|
739
|
+
with modal.enable_output():
|
740
|
+
# Create a Modal app and sandbox
|
741
|
+
print(f"🚀 Creating Modal sandbox with GPU: {gpu_type.lower()} (App: {app_name})...")
|
742
|
+
# Always use lookup with create_if_missing=True to properly initialize the app
|
743
|
+
app = modal.App.lookup(app_name, create_if_missing=True)
|
744
|
+
print(f"Created app: {app_name}")
|
745
|
+
|
746
|
+
# Create the sandbox with increased timeout for long-running operations
|
747
|
+
print("⏱️ Setting 30-minute timeout for long-running installations...")
|
748
|
+
|
749
|
+
# Setup volume mount if available
|
750
|
+
volumes = {}
|
751
|
+
if volume:
|
752
|
+
volumes[volume_mount_path] = volume
|
753
|
+
print(f"📦 Mounting volume '{volume_name}' at {volume_mount_path}")
|
754
|
+
|
755
|
+
cuda_image = modal.Image.from_registry("nvidia/cuda:12.8.1-devel-ubuntu24.04", add_python="3.12")
|
756
|
+
|
757
|
+
sandbox = modal.Sandbox.create(
|
758
|
+
"sleep", "infinity",
|
759
|
+
app=app,
|
760
|
+
gpu=gpu_type.lower(),
|
761
|
+
image=cuda_image,
|
762
|
+
timeout=3600, # 40 minutes instead of 15 minutes
|
763
|
+
volumes=volumes if volumes else None
|
764
|
+
)
|
765
|
+
|
766
|
+
# Get the sandbox ID for reference
|
767
|
+
sandbox_id = sandbox.object_id
|
768
|
+
print(f"📋 Sandbox ID: {sandbox_id}")
|
769
|
+
|
770
|
+
# Wait a moment for the container to be registered
|
771
|
+
print("⏳ Waiting for container to be registered...")
|
772
|
+
time.sleep(5) # Increased wait time
|
773
|
+
|
774
|
+
# Function to extract container ID from text output
|
775
|
+
def extract_container_id_from_text(output):
|
776
|
+
print("Extracting container ID from text output...")
|
777
|
+
|
778
|
+
# First, try to find lines with the app name
|
779
|
+
lines = output.split('\n')
|
780
|
+
app_lines = [line for line in lines if app_name in line]
|
781
|
+
|
782
|
+
if app_lines:
|
783
|
+
# Get the first line with the app name
|
784
|
+
app_line = app_lines[0]
|
785
|
+
print(f"Found line with app name: {app_line}")
|
786
|
+
|
787
|
+
# Try to extract the container ID
|
788
|
+
if '│' in app_line:
|
789
|
+
parts = app_line.split('│')
|
790
|
+
if len(parts) >= 2:
|
791
|
+
container_id_part = parts[1].strip()
|
792
|
+
if container_id_part.startswith('ta-'):
|
793
|
+
return container_id_part
|
794
|
+
|
795
|
+
# If that didn't work, try regex pattern matching
|
796
|
+
container_matches = re.findall(r'ta-[A-Z0-9]+', output)
|
797
|
+
if container_matches:
|
798
|
+
return container_matches[0]
|
799
|
+
|
800
|
+
return None
|
801
|
+
|
802
|
+
# Get the container ID using multiple approaches
|
803
|
+
print("📋 Getting container ID...")
|
804
|
+
container_id = None
|
805
|
+
|
806
|
+
# Approach 1: Use modal container list --json
|
807
|
+
try:
|
808
|
+
print("Trying JSON approach...")
|
809
|
+
result = subprocess.run(["modal", "container", "list", "--json"], capture_output=True, text=True)
|
810
|
+
output = result.stdout
|
811
|
+
print(f"JSON output: {output}")
|
812
|
+
|
813
|
+
import json
|
814
|
+
try:
|
815
|
+
containers = json.loads(output)
|
816
|
+
print(f"Parsed JSON: {containers}")
|
817
|
+
if containers and isinstance(containers, list) and len(containers) > 0:
|
818
|
+
# The container ID is in the "Container ID" field, not "id"
|
819
|
+
container_id = containers[0].get("Container ID")
|
820
|
+
if container_id:
|
821
|
+
print(f"📋 Found container ID from JSON: {container_id}")
|
822
|
+
else:
|
823
|
+
# Try lowercase keys as a fallback
|
824
|
+
container_id = containers[0].get("container_id") or containers[0].get("container id")
|
825
|
+
if container_id:
|
826
|
+
print(f"📋 Found container ID from JSON with lowercase keys: {container_id}")
|
827
|
+
except json.JSONDecodeError as json_err:
|
828
|
+
print(f"JSON parse error: {json_err}")
|
829
|
+
except Exception as e:
|
830
|
+
print(f"Error with JSON approach: {e}")
|
831
|
+
|
832
|
+
# Approach 2: Use modal container list with text parsing
|
833
|
+
if not container_id:
|
834
|
+
try:
|
835
|
+
print("Trying text output approach...")
|
836
|
+
result = subprocess.run(["modal", "container", "list"], capture_output=True, text=True)
|
837
|
+
output = result.stdout
|
838
|
+
print("Modal container list output:")
|
839
|
+
print(output)
|
840
|
+
|
841
|
+
container_id = extract_container_id_from_text(output)
|
842
|
+
if container_id:
|
843
|
+
print(f"📋 Found container ID from text: {container_id}")
|
844
|
+
except Exception as e:
|
845
|
+
print(f"Error with text approach: {e}")
|
846
|
+
|
847
|
+
# Approach 3: Use shell command to get first container
|
848
|
+
if not container_id:
|
849
|
+
try:
|
850
|
+
print("Trying shell command approach...")
|
851
|
+
cmd = "modal container list | grep -v Container | grep -v '─' | head -1 | awk '{print $1}'"
|
852
|
+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
853
|
+
output = result.stdout.strip()
|
854
|
+
print(f"Shell command output: {output}")
|
855
|
+
|
856
|
+
if output and output.startswith('ta-'):
|
857
|
+
container_id = output
|
858
|
+
print(f"📋 Found container ID from shell command: {container_id}")
|
859
|
+
except Exception as e:
|
860
|
+
print(f"Error with shell command approach: {e}")
|
861
|
+
|
862
|
+
# Approach 4: Get all containers and find the one with our app
|
863
|
+
if not container_id:
|
864
|
+
try:
|
865
|
+
print("Trying app matching approach...")
|
866
|
+
cmd = "modal container list"
|
867
|
+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
868
|
+
output = result.stdout
|
869
|
+
|
870
|
+
# Look for our app name in the output
|
871
|
+
if app_name in output:
|
872
|
+
print(f"Found {app_name} in container list")
|
873
|
+
# Try to get the container ID from the same line
|
874
|
+
lines = output.split('\n')
|
875
|
+
for line in lines:
|
876
|
+
if app_name in line:
|
877
|
+
print(f"Found line: {line}")
|
878
|
+
# Try to extract the first column
|
879
|
+
if '│' in line:
|
880
|
+
container_id_part = line.split('│')[1].strip()
|
881
|
+
if container_id_part.startswith('ta-'):
|
882
|
+
container_id = container_id_part
|
883
|
+
print(f"📋 Found container ID from app matching: {container_id}")
|
884
|
+
break
|
885
|
+
except Exception as e:
|
886
|
+
print(f"Error with app matching approach: {e}")
|
887
|
+
|
888
|
+
# Final fallback: Use sandbox ID to create a container ID
|
889
|
+
if not container_id:
|
890
|
+
print("⚠️ All approaches failed to find container ID")
|
891
|
+
# Use sandbox ID as container prefix
|
892
|
+
short_id = sandbox_id.split('-')[1][:8] if '-' in sandbox_id else sandbox_id[:8]
|
893
|
+
container_id = f"ta-{short_id.upper()}"
|
894
|
+
print(f"📋 Using derived container ID: {container_id}")
|
895
|
+
|
896
|
+
# Ensure we have a non-None container ID
|
897
|
+
if not container_id:
|
898
|
+
print("⚠️ Critical error: Failed to determine container ID")
|
899
|
+
print("⚠️ Using a placeholder container ID")
|
900
|
+
container_id = "ta-UNKNOWN"
|
901
|
+
|
902
|
+
# Try to verify the container ID exists
|
903
|
+
print("🔍 Verifying container ID...")
|
904
|
+
verify_cmd = f"modal container logs {container_id} --tail 1 2>/dev/null || echo 'Container not found'"
|
905
|
+
verify_result = subprocess.run(verify_cmd, shell=True, capture_output=True, text=True)
|
906
|
+
if "Container not found" in verify_result.stdout:
|
907
|
+
print(f"⚠️ Container ID verification failed: {container_id}")
|
908
|
+
|
909
|
+
# Last resort: Try to find any valid container
|
910
|
+
print("🔍 Looking for any valid container as last resort...")
|
911
|
+
list_cmd = "modal container list | grep -v Container | grep -v '─' | grep -v '┏' | grep -v '┃' | head -1"
|
912
|
+
list_result = subprocess.run(list_cmd, shell=True, capture_output=True, text=True)
|
913
|
+
if list_result.stdout.strip():
|
914
|
+
print(f"Found container line: {list_result.stdout.strip()}")
|
915
|
+
# Try to extract the ID from the first column
|
916
|
+
container_line = list_result.stdout.strip()
|
917
|
+
if '│' in container_line:
|
918
|
+
possible_id = container_line.split('│')[1].strip()
|
919
|
+
if possible_id.startswith('ta-'):
|
920
|
+
container_id = possible_id
|
921
|
+
print(f"📋 Using container ID from list as last resort: {container_id}")
|
922
|
+
|
923
|
+
# Verify this container
|
924
|
+
verify_cmd = f"modal container logs {container_id} --tail 1 2>/dev/null || echo 'Container not found'"
|
925
|
+
verify_result = subprocess.run(verify_cmd, shell=True, capture_output=True, text=True)
|
926
|
+
if "Container not found" not in verify_result.stdout:
|
927
|
+
print(f"✅ Last resort container ID verified: {container_id}")
|
928
|
+
else:
|
929
|
+
print("⚠️ Last resort container ID also failed verification")
|
930
|
+
|
931
|
+
print("⚠️ Container connection may fail. You may need to connect manually.")
|
932
|
+
else:
|
933
|
+
print(f"✅ Container ID verified: {container_id}")
|
934
|
+
|
935
|
+
# Function to convert bytes to string
|
936
|
+
def _to_str(maybe_bytes):
|
937
|
+
try:
|
938
|
+
return (maybe_bytes.decode('utf-8') if isinstance(maybe_bytes, (bytes, bytearray)) else maybe_bytes)
|
939
|
+
except UnicodeDecodeError:
|
940
|
+
# Handle non-UTF-8 bytes by replacing invalid characters
|
941
|
+
if isinstance(maybe_bytes, (bytes, bytearray)):
|
942
|
+
return maybe_bytes.decode('utf-8', errors='replace')
|
943
|
+
else:
|
944
|
+
return str(maybe_bytes)
|
945
|
+
except Exception:
|
946
|
+
# Last resort fallback
|
947
|
+
return str(maybe_bytes)
|
948
|
+
|
949
|
+
# Skip the persistent shell approach for now due to async stream complexity
|
950
|
+
print("🔍 Modal's async streams require complex async handling")
|
951
|
+
print("🔄 Switching to individual command execution approach for reliability...")
|
952
|
+
|
953
|
+
# Initialize state tracking variables
|
954
|
+
current_dir = "/"
|
955
|
+
execution_history = []
|
956
|
+
|
957
|
+
# Function to run commands using individual sandbox.exec calls
|
958
|
+
def run_command(cmd, show_output=True, retry_count=0, max_retries=3, debug_with_llm=True, timeout=600):
|
959
|
+
"""
|
960
|
+
Execute a command in the sandbox with error handling and automatic retries.
|
961
|
+
|
962
|
+
When a command fails and is fixed by the LLM debugging system, the retry count
|
963
|
+
is reset to 0, so successful fixes don't count against the maximum retry limit.
|
964
|
+
This ensures that a command that's been fixed gets a fresh set of retry attempts.
|
965
|
+
"""
|
966
|
+
# Use the outer scope variables
|
967
|
+
nonlocal current_dir, execution_history, sandbox, previous_errors
|
968
|
+
nonlocal conda_installed, python_version_switched, current_python_version
|
969
|
+
|
970
|
+
# Record command start time
|
971
|
+
command_start_time = datetime.datetime.now().isoformat()
|
972
|
+
start_time = time.time()
|
973
|
+
|
974
|
+
# Prevent infinite retry loops
|
975
|
+
if retry_count >= max_retries:
|
976
|
+
print(f"⚠️ Maximum retry count ({max_retries}) reached. Stopping retries.")
|
977
|
+
return False, "", f"Maximum retry count ({max_retries}) reached"
|
978
|
+
|
979
|
+
# Special handling for cd commands to prevent common navigation errors
|
980
|
+
if cmd.strip().startswith("cd "):
|
981
|
+
# Extract the target directory from the cd command
|
982
|
+
cd_parts = cmd.split(None, 1)
|
983
|
+
if len(cd_parts) >= 2:
|
984
|
+
target_dir = cd_parts[1].strip().strip('"\'')
|
985
|
+
|
986
|
+
# Check if this is a repo name that matches the end of current_dir
|
987
|
+
# This prevents errors like "cd repo-name" when already in "/root/repo-name"
|
988
|
+
# BUT we need to be careful about nested directories like /root/litex/litex
|
989
|
+
if (target_dir != "/" and target_dir != "." and target_dir != ".." and
|
990
|
+
not target_dir.startswith("/") and not target_dir.startswith("./") and
|
991
|
+
not target_dir.startswith("../") and current_dir.endswith("/" + target_dir)):
|
992
|
+
|
993
|
+
# Advanced check: analyze directory contents to determine if navigation makes sense
|
994
|
+
print(f"🔍 Analyzing directory contents to determine navigation necessity...")
|
995
|
+
|
996
|
+
# Get current directory contents
|
997
|
+
current_contents_cmd = "ls -la"
|
998
|
+
current_result = sandbox.exec("bash", "-c", current_contents_cmd)
|
999
|
+
current_result.wait()
|
1000
|
+
current_contents = _to_str(current_result.stdout) if current_result.stdout else ""
|
1001
|
+
|
1002
|
+
# Check if target directory exists
|
1003
|
+
test_cmd = f"test -d \"{target_dir}\""
|
1004
|
+
test_result = sandbox.exec("bash", "-c", test_cmd)
|
1005
|
+
test_result.wait()
|
1006
|
+
|
1007
|
+
if test_result.returncode == 0:
|
1008
|
+
# Target directory exists, get its contents
|
1009
|
+
target_contents_cmd = f"ls -la \"{target_dir}\""
|
1010
|
+
target_result = sandbox.exec("bash", "-c", target_contents_cmd)
|
1011
|
+
target_result.wait()
|
1012
|
+
target_contents = _to_str(target_result.stdout) if target_result.stdout else ""
|
1013
|
+
|
1014
|
+
try:
|
1015
|
+
# Call LLM for analysis with the dedicated function
|
1016
|
+
llm_response = analyze_directory_navigation_with_llm(current_dir, target_dir, current_contents, target_contents, api_key)
|
1017
|
+
|
1018
|
+
# Extract decision from LLM response
|
1019
|
+
if llm_response and "NAVIGATE" in llm_response.upper():
|
1020
|
+
print(f"🤖 LLM Analysis: Navigation makes sense - contents are different")
|
1021
|
+
print(f"📂 Current: {current_dir}")
|
1022
|
+
print(f"🎯 Target: {target_dir}")
|
1023
|
+
print(f"🔄 Proceeding with navigation...")
|
1024
|
+
else:
|
1025
|
+
print(f"🤖 LLM Analysis: Navigation is redundant - contents are similar")
|
1026
|
+
print(f"⚠️ Detected redundant directory navigation: {cmd}")
|
1027
|
+
print(f"📂 Already in the correct directory: {current_dir}")
|
1028
|
+
print(f"✅ Skipping unnecessary navigation command")
|
1029
|
+
return True, f"Already in directory {current_dir}", ""
|
1030
|
+
|
1031
|
+
except Exception as e:
|
1032
|
+
print(f"⚠️ LLM analysis failed: {e}")
|
1033
|
+
print(f"🔄 Falling back to simple directory existence check...")
|
1034
|
+
# Fallback to simple check
|
1035
|
+
print(f"🔍 Detected nested directory '{target_dir}' exists in current location")
|
1036
|
+
print(f"📂 Current: {current_dir}")
|
1037
|
+
print(f"🎯 Target: {target_dir}")
|
1038
|
+
print(f"🔄 Proceeding with navigation to nested directory...")
|
1039
|
+
else:
|
1040
|
+
# No nested directory exists, so this is truly redundant
|
1041
|
+
print(f"⚠️ Detected redundant directory navigation: {cmd}")
|
1042
|
+
print(f"📂 Already in the correct directory: {current_dir}")
|
1043
|
+
print(f"✅ Skipping unnecessary navigation command")
|
1044
|
+
return True, f"Already in directory {current_dir}", ""
|
1045
|
+
|
1046
|
+
# Remove any parenthetical text that could cause syntax errors in bash
|
1047
|
+
if '(' in cmd:
|
1048
|
+
original_cmd = cmd
|
1049
|
+
cmd = re.sub(r'\([^)]*\)', '', cmd).strip()
|
1050
|
+
print(f"🔄 Removing parenthetical text:")
|
1051
|
+
print(f" Original: {original_cmd}")
|
1052
|
+
print(f" Cleaned: {cmd}")
|
1053
|
+
|
1054
|
+
# Convert pip install commands to use uv for faster installation
|
1055
|
+
original_cmd = cmd
|
1056
|
+
if 'uv_path' in globals() and uv_path and ('pip install' in cmd or 'pip3 install' in cmd) and not cmd.startswith(uv_path):
|
1057
|
+
# Replace pip/pip3 install with uv pip install, but only if not already using uv
|
1058
|
+
cmd = cmd.replace('pip install', f'{uv_path} pip install')
|
1059
|
+
cmd = cmd.replace('pip3 install', f'{uv_path} pip install')
|
1060
|
+
print(f"🚀 Converting to uv for faster installation:")
|
1061
|
+
print(f" Original: {original_cmd}")
|
1062
|
+
print(f" Converted: {cmd}")
|
1063
|
+
|
1064
|
+
print(f"\n▶ {cmd}\n")
|
1065
|
+
|
1066
|
+
# Check if this is a potentially long-running command
|
1067
|
+
long_running_patterns = [
|
1068
|
+
'pip install', 'apt install', 'yum install',
|
1069
|
+
'wget', 'curl', 'git clone', 'npm install', 'yarn install',
|
1070
|
+
'cmake', 'make', 'gcc', 'g++', 'python setup.py'
|
1071
|
+
]
|
1072
|
+
|
1073
|
+
is_long_running = any(pattern in cmd.lower() for pattern in long_running_patterns)
|
1074
|
+
if is_long_running:
|
1075
|
+
print(f"⏱️ Detected potentially long-running command. This may take several minutes...")
|
1076
|
+
print(f"📦 Large packages (like PyTorch) can take 5-10 minutes to download and install.")
|
1077
|
+
print(f"🔄 The container has a 30-minute timeout to accommodate this.")
|
1078
|
+
|
1079
|
+
# Use the original command without modification for interactivity
|
1080
|
+
cmd_to_execute = cmd
|
1081
|
+
|
1082
|
+
# Special handling for huggingface-cli login command
|
1083
|
+
if "huggingface-cli login" in cmd_to_execute:
|
1084
|
+
print("🔍 Detected huggingface-cli login command")
|
1085
|
+
print("🔄 Using non-interactive login approach with token instead")
|
1086
|
+
|
1087
|
+
# Check if the command already has a token
|
1088
|
+
if "--token" in cmd_to_execute:
|
1089
|
+
print("✅ Command already includes token parameter")
|
1090
|
+
else:
|
1091
|
+
# Prompt for HF token
|
1092
|
+
hf_token = prompt_for_hf_token()
|
1093
|
+
if hf_token:
|
1094
|
+
# Replace with non-interactive command
|
1095
|
+
cmd_to_execute = f"huggingface-cli login --token {hf_token} --add-to-git-credential"
|
1096
|
+
print(f"🔄 Using non-interactive command: {cmd_to_execute}")
|
1097
|
+
else:
|
1098
|
+
print("❌ No token provided. Cannot continue with Hugging Face login.")
|
1099
|
+
return False, "", "No Hugging Face token provided"
|
1100
|
+
|
1101
|
+
# Special handling for wandb login command
|
1102
|
+
elif "wandb login" in cmd_to_execute and "YOUR_API_KEY" not in cmd_to_execute:
|
1103
|
+
print("🔍 Detected Weights & Biases login command")
|
1104
|
+
print("🔄 Using API key approach for non-interactive login")
|
1105
|
+
|
1106
|
+
# Check if the command already includes an API key
|
1107
|
+
has_api_key = False
|
1108
|
+
cmd_parts = cmd_to_execute.split()
|
1109
|
+
for part in cmd_parts:
|
1110
|
+
if part != "wandb" and part != "login" and not part.startswith("-"):
|
1111
|
+
has_api_key = True
|
1112
|
+
break
|
1113
|
+
|
1114
|
+
if not has_api_key:
|
1115
|
+
# Prompt for W&B API key
|
1116
|
+
print("\n" + "="*60)
|
1117
|
+
print("🔑 WEIGHTS & BIASES API KEY REQUIRED")
|
1118
|
+
print("="*60)
|
1119
|
+
print("You can get your API key from: https://wandb.ai/authorize")
|
1120
|
+
print("📝 Please paste your W&B API key below:")
|
1121
|
+
print(" (Your input will be hidden for security)")
|
1122
|
+
print("-" * 60)
|
1123
|
+
|
1124
|
+
try:
|
1125
|
+
api_key = getpass.getpass("W&B API Key: ").strip()
|
1126
|
+
if not api_key:
|
1127
|
+
print("❌ No API key provided. Cannot continue with W&B login.")
|
1128
|
+
return False, "", "No W&B API key provided"
|
1129
|
+
|
1130
|
+
# Validate API key length (typically 40 characters)
|
1131
|
+
if len(api_key) != 40:
|
1132
|
+
print(f"⚠️ Warning: API key should be 40 characters long, yours was {len(api_key)}")
|
1133
|
+
confirm = input("Continue anyway? (yes/no): ").strip().lower()
|
1134
|
+
if confirm not in ["yes", "y"]:
|
1135
|
+
print("❌ W&B login cancelled.")
|
1136
|
+
return False, "", "W&B login cancelled"
|
1137
|
+
|
1138
|
+
print("✅ API key received successfully!")
|
1139
|
+
|
1140
|
+
# Replace with non-interactive command
|
1141
|
+
cmd_to_execute = f"wandb login {api_key}"
|
1142
|
+
print(f"🔄 Using non-interactive command: wandb login [API_KEY_HIDDEN]")
|
1143
|
+
except KeyboardInterrupt:
|
1144
|
+
print("\n❌ API key input cancelled by user.")
|
1145
|
+
return False, "", "W&B API key input cancelled"
|
1146
|
+
except Exception as e:
|
1147
|
+
print(f"❌ Error getting API key: {e}")
|
1148
|
+
return False, "", f"Error getting W&B API key: {e}"
|
1149
|
+
|
1150
|
+
# Validate the command before execution
|
1151
|
+
if not cmd_to_execute or cmd_to_execute.strip() == "":
|
1152
|
+
print("⚠️ Empty command detected, skipping execution")
|
1153
|
+
return False, "", "Empty command"
|
1154
|
+
|
1155
|
+
# Sanitize command to prevent issues with special characters
|
1156
|
+
# Remove any null bytes or other problematic characters
|
1157
|
+
cmd_to_execute = cmd_to_execute.replace('\x00', '').strip()
|
1158
|
+
|
1159
|
+
if len(cmd_to_execute) > 10000: # Prevent extremely long commands
|
1160
|
+
print("⚠️ Command too long, truncating")
|
1161
|
+
cmd_to_execute = cmd_to_execute[:10000]
|
1162
|
+
|
1163
|
+
# Prepare the command with environment variables and error handling
|
1164
|
+
full_command = f"""
|
1165
|
+
# Change to current directory
|
1166
|
+
cd "{current_dir}"
|
1167
|
+
|
1168
|
+
# Execute the command
|
1169
|
+
{cmd_to_execute}
|
1170
|
+
"""
|
1171
|
+
|
1172
|
+
# Execute the command using sandbox.exec
|
1173
|
+
try:
|
1174
|
+
print(f"🔄 Executing command in directory: {current_dir}")
|
1175
|
+
|
1176
|
+
# Use sandbox.exec for individual command execution
|
1177
|
+
result = sandbox.exec("bash", "-c", full_command.strip())
|
1178
|
+
|
1179
|
+
# Collect output in real-time - Modal streams are already set up for line-by-line streaming
|
1180
|
+
stdout_lines = []
|
1181
|
+
stderr_lines = []
|
1182
|
+
|
1183
|
+
# Process output streams in real-time - Modal handles this natively
|
1184
|
+
# We don't need to use threading here as Modal's streams are designed to be consumed directly
|
1185
|
+
if show_output:
|
1186
|
+
print("\n--- Command Output ---")
|
1187
|
+
|
1188
|
+
# Track if we've shown timeout warnings
|
1189
|
+
timeout_warnings = set()
|
1190
|
+
last_output_time = time.time()
|
1191
|
+
|
1192
|
+
# Read stdout in real-time
|
1193
|
+
for line in result.stdout:
|
1194
|
+
# Check for timeout
|
1195
|
+
current_time = time.time()
|
1196
|
+
elapsed = current_time - start_time
|
1197
|
+
time_since_output = current_time - last_output_time
|
1198
|
+
|
1199
|
+
# Show timeout warning every 30 seconds if no output for 30+ seconds
|
1200
|
+
if time_since_output > 30 and int(time_since_output) // 30 not in timeout_warnings:
|
1201
|
+
warning_time = int(time_since_output) // 30 * 30
|
1202
|
+
timeout_warnings.add(int(time_since_output) // 30)
|
1203
|
+
print(f"Still running after {int(elapsed)} seconds...")
|
1204
|
+
|
1205
|
+
# If total time exceeds timeout, break
|
1206
|
+
if elapsed > timeout:
|
1207
|
+
print(f"⚠️ Command timed out after {timeout} seconds")
|
1208
|
+
# Force terminate the command
|
1209
|
+
try:
|
1210
|
+
result.terminate()
|
1211
|
+
except:
|
1212
|
+
pass
|
1213
|
+
return False, "Command timed out", f"Command execution exceeded timeout of {timeout} seconds"
|
1214
|
+
|
1215
|
+
# Process the line
|
1216
|
+
line_str = _to_str(line)
|
1217
|
+
stdout_lines.append(line_str)
|
1218
|
+
if show_output:
|
1219
|
+
# Print immediately with flush to ensure real-time display
|
1220
|
+
print(line_str, end="", flush=True)
|
1221
|
+
|
1222
|
+
# Update last output time
|
1223
|
+
last_output_time = time.time()
|
1224
|
+
|
1225
|
+
# Read stderr in real-time
|
1226
|
+
for line in result.stderr:
|
1227
|
+
# Check for timeout
|
1228
|
+
current_time = time.time()
|
1229
|
+
elapsed = current_time - start_time
|
1230
|
+
time_since_output = current_time - last_output_time
|
1231
|
+
|
1232
|
+
# Show timeout warning every 30 seconds if no output for 30+ seconds
|
1233
|
+
if time_since_output > 30 and int(time_since_output) // 30 not in timeout_warnings:
|
1234
|
+
warning_time = int(time_since_output) // 30 * 30
|
1235
|
+
timeout_warnings.add(int(time_since_output) // 30)
|
1236
|
+
print(f"Still running after {int(elapsed)} seconds...")
|
1237
|
+
|
1238
|
+
# If total time exceeds timeout, break
|
1239
|
+
if elapsed > timeout:
|
1240
|
+
print(f"⚠️ Command timed out after {timeout} seconds")
|
1241
|
+
# Force terminate the command
|
1242
|
+
try:
|
1243
|
+
result.terminate()
|
1244
|
+
except:
|
1245
|
+
pass
|
1246
|
+
return False, "Command timed out", f"Command execution exceeded timeout of {timeout} seconds"
|
1247
|
+
|
1248
|
+
# Process the line
|
1249
|
+
line_str = _to_str(line)
|
1250
|
+
stderr_lines.append(line_str)
|
1251
|
+
if show_output:
|
1252
|
+
# Print immediately with flush to ensure real-time display
|
1253
|
+
print(line_str, end="", file=sys.stderr, flush=True)
|
1254
|
+
|
1255
|
+
# Update last output time
|
1256
|
+
last_output_time = time.time()
|
1257
|
+
|
1258
|
+
if show_output:
|
1259
|
+
print("--- End Output ---\n")
|
1260
|
+
|
1261
|
+
stdout_buffer = ''.join(stdout_lines)
|
1262
|
+
stderr_buffer = ''.join(stderr_lines)
|
1263
|
+
|
1264
|
+
# Wait for the process to complete before accessing returncode
|
1265
|
+
result.wait()
|
1266
|
+
exit_code = result.returncode
|
1267
|
+
|
1268
|
+
except Exception as e:
|
1269
|
+
print(f"❌ Error executing command: {e}")
|
1270
|
+
return False, "", str(e)
|
1271
|
+
|
1272
|
+
# Record command completion time
|
1273
|
+
command_end_time = datetime.datetime.now().isoformat()
|
1274
|
+
|
1275
|
+
# Calculate duration in seconds
|
1276
|
+
start_dt = datetime.datetime.fromisoformat(command_start_time)
|
1277
|
+
end_dt = datetime.datetime.fromisoformat(command_end_time)
|
1278
|
+
duration = (end_dt - start_dt).total_seconds()
|
1279
|
+
|
1280
|
+
# Record this command execution in history
|
1281
|
+
execution_record = {
|
1282
|
+
"command": cmd_to_execute,
|
1283
|
+
"original_command": cmd if cmd != cmd_to_execute else None,
|
1284
|
+
"start_time": command_start_time,
|
1285
|
+
"end_time": command_end_time,
|
1286
|
+
"duration_seconds": duration,
|
1287
|
+
"exit_code": exit_code,
|
1288
|
+
"stdout": stdout_buffer,
|
1289
|
+
"stderr": stderr_buffer,
|
1290
|
+
"directory": current_dir
|
1291
|
+
}
|
1292
|
+
execution_history.append(execution_record)
|
1293
|
+
|
1294
|
+
# Update current directory if this was a cd command and it succeeded
|
1295
|
+
if cmd_to_execute.strip().startswith("cd ") and exit_code == 0:
|
1296
|
+
# Extract the target directory from the cd command
|
1297
|
+
cd_parts = cmd_to_execute.split(None, 1)
|
1298
|
+
if len(cd_parts) >= 2:
|
1299
|
+
target_dir = cd_parts[1].strip('"\'')
|
1300
|
+
|
1301
|
+
# Store the previous directory for logging
|
1302
|
+
previous_dir = current_dir
|
1303
|
+
|
1304
|
+
# Handle different types of paths
|
1305
|
+
if target_dir.startswith('/'):
|
1306
|
+
# Absolute path
|
1307
|
+
current_dir = target_dir
|
1308
|
+
elif target_dir == '..':
|
1309
|
+
# Parent directory
|
1310
|
+
current_dir = '/'.join(current_dir.rstrip('/').split('/')[:-1]) or '/'
|
1311
|
+
elif target_dir == '.':
|
1312
|
+
# Current directory - no change
|
1313
|
+
pass
|
1314
|
+
else:
|
1315
|
+
# Relative path - handle special case where target is already at the end of current_dir
|
1316
|
+
if current_dir.endswith('/' + target_dir):
|
1317
|
+
print(f"📂 Already in directory {current_dir}, no change needed")
|
1318
|
+
else:
|
1319
|
+
current_dir = f"{current_dir.rstrip('/')}/{target_dir}"
|
1320
|
+
|
1321
|
+
print(f"📂 Updated current directory: {previous_dir} -> {current_dir}")
|
1322
|
+
execution_record["new_current_dir"] = current_dir
|
1323
|
+
|
1324
|
+
# Verify the directory actually exists
|
1325
|
+
verify_cmd = f"test -d \"{current_dir}\""
|
1326
|
+
verify_result = sandbox.exec("bash", "-c", verify_cmd)
|
1327
|
+
verify_result.wait()
|
1328
|
+
|
1329
|
+
if verify_result.returncode != 0:
|
1330
|
+
print(f"⚠️ Warning: Directory {current_dir} does not exist")
|
1331
|
+
print(f"⚠️ Reverting to previous directory: {previous_dir}")
|
1332
|
+
current_dir = previous_dir
|
1333
|
+
execution_record["new_current_dir"] = current_dir
|
1334
|
+
|
1335
|
+
# Check for errors and handle Hugging Face token issues
|
1336
|
+
if exit_code != 0:
|
1337
|
+
# Check for specific Hugging Face token errors
|
1338
|
+
hf_token_error_patterns = [
|
1339
|
+
"Token is required",
|
1340
|
+
"LocalTokenNotFoundError",
|
1341
|
+
"Invalid user token",
|
1342
|
+
"401 Client Error: Unauthorized",
|
1343
|
+
"Invalid credentials in Authorization header",
|
1344
|
+
"HF_TOKEN environment variable is invalid"
|
1345
|
+
]
|
1346
|
+
|
1347
|
+
is_hf_token_error = any(pattern in stderr_buffer for pattern in hf_token_error_patterns)
|
1348
|
+
|
1349
|
+
if is_hf_token_error:
|
1350
|
+
print(f"🔑 Detected Hugging Face token authentication error!")
|
1351
|
+
print(f"🔍 Error details: {stderr_buffer}")
|
1352
|
+
|
1353
|
+
# Prompt for the real token
|
1354
|
+
real_token = prompt_for_hf_token()
|
1355
|
+
|
1356
|
+
if real_token:
|
1357
|
+
print(f"🔄 Setting HF_TOKEN and retrying command...")
|
1358
|
+
|
1359
|
+
# Retry with the token set
|
1360
|
+
token_command = f"export HF_TOKEN='{real_token}'; {cmd_to_execute}"
|
1361
|
+
return run_command(token_command, show_output, retry_count + 1, max_retries)
|
1362
|
+
else:
|
1363
|
+
print("❌ No token provided. Cannot continue with Hugging Face operations.")
|
1364
|
+
return False, stdout_buffer, "No Hugging Face token provided"
|
1365
|
+
|
1366
|
+
# Check for "No such file or directory" errors with cd commands
|
1367
|
+
if "cd " in cmd_to_execute and "No such file or directory" in stderr_buffer:
|
1368
|
+
print("⚠️ Directory navigation error detected")
|
1369
|
+
|
1370
|
+
# Extract the target directory from the cd command
|
1371
|
+
cd_parts = cmd_to_execute.split(None, 1)
|
1372
|
+
if len(cd_parts) >= 2:
|
1373
|
+
target_dir = cd_parts[1].strip('"\'')
|
1374
|
+
|
1375
|
+
# Check if this might be a repository name that's already in the path
|
1376
|
+
if not target_dir.startswith('/') and '/' + target_dir in current_dir:
|
1377
|
+
print(f"🔍 The directory '{target_dir}' appears to be part of the current path: {current_dir}")
|
1378
|
+
print(f"⚠️ This is likely a redundant navigation attempt")
|
1379
|
+
|
1380
|
+
# If we're already in a directory that ends with the target, consider it a success
|
1381
|
+
if current_dir.endswith('/' + target_dir):
|
1382
|
+
print(f"✅ Already in the correct directory: {current_dir}")
|
1383
|
+
return True, f"Already in directory {current_dir}", ""
|
1384
|
+
|
1385
|
+
print(f"⚠️ Command failed with exit code {exit_code}")
|
1386
|
+
if stderr_buffer.strip():
|
1387
|
+
print(f"Error output: {stderr_buffer}")
|
1388
|
+
|
1389
|
+
# If command failed and we're debugging with LLM
|
1390
|
+
if debug_with_llm:
|
1391
|
+
print("🔍 Attempting to debug the failed command with OpenAI...")
|
1392
|
+
|
1393
|
+
# Check if the command is a hanging huggingface-cli login
|
1394
|
+
if "huggingface-cli login" in cmd_to_execute and not stderr_buffer.strip():
|
1395
|
+
print("🔍 Detected hanging huggingface-cli login command")
|
1396
|
+
print("🔄 Using non-interactive login approach with HF_TOKEN instead")
|
1397
|
+
|
1398
|
+
# Prompt for HF token
|
1399
|
+
hf_token = prompt_for_hf_token()
|
1400
|
+
if hf_token:
|
1401
|
+
# Set the token as environment variable and create .huggingface folder
|
1402
|
+
print("✅ Token received, setting up non-interactive authentication")
|
1403
|
+
setup_commands = [
|
1404
|
+
"mkdir -p ~/.huggingface",
|
1405
|
+
f"echo '{hf_token}' > ~/.huggingface/token",
|
1406
|
+
f"export HF_TOKEN='{hf_token}'",
|
1407
|
+
"echo 'HF_TOKEN and token file have been set up'"
|
1408
|
+
]
|
1409
|
+
|
1410
|
+
for setup_cmd in setup_commands:
|
1411
|
+
setup_success, setup_stdout, _ = run_command(setup_cmd, show_output=True, debug_with_llm=False)
|
1412
|
+
if not setup_success:
|
1413
|
+
print(f"⚠️ Setup command failed: {setup_cmd}")
|
1414
|
+
|
1415
|
+
print("✅ Hugging Face authentication set up non-interactively")
|
1416
|
+
return True, "Hugging Face authentication set up successfully", ""
|
1417
|
+
else:
|
1418
|
+
print("❌ No token provided. Cannot set up Hugging Face authentication.")
|
1419
|
+
return False, "", "No Hugging Face token provided"
|
1420
|
+
|
1421
|
+
# Check if the error is related to missing pytest
|
1422
|
+
if "ModuleNotFoundError: No module named 'pytest'" in stderr_buffer or "ImportError: No module named pytest" in stderr_buffer:
|
1423
|
+
print("🔍 Detected missing pytest module, installing it automatically...")
|
1424
|
+
pytest_install_success, _, _ = run_command("pip install pytest", show_output=True, debug_with_llm=False)
|
1425
|
+
if pytest_install_success:
|
1426
|
+
print("✅ Successfully installed pytest, retrying original command...")
|
1427
|
+
return run_command(cmd, show_output, retry_count + 1, max_retries)
|
1428
|
+
|
1429
|
+
# Check for Python version-specific errors
|
1430
|
+
python_version_errors = [
|
1431
|
+
# Python 3.13 distutils issue
|
1432
|
+
("ModuleNotFoundError: No module named 'distutils'", "3.13"),
|
1433
|
+
# Add more version-specific error patterns here
|
1434
|
+
("ImportError: cannot import name 'soft_unicode' from 'markupsafe'", None),
|
1435
|
+
("AttributeError: module 'setuptools.dist' has no attribute 'check_specifier'", None)
|
1436
|
+
]
|
1437
|
+
|
1438
|
+
# Check if any of the error patterns match
|
1439
|
+
for error_pattern, problematic_version in python_version_errors:
|
1440
|
+
if error_pattern in stderr_buffer:
|
1441
|
+
print(f"🔍 Detected Python version-specific error: {error_pattern}")
|
1442
|
+
|
1443
|
+
# Get current Python version if not already known
|
1444
|
+
if not current_python_version:
|
1445
|
+
version_cmd = "python --version"
|
1446
|
+
version_success, version_stdout, _ = run_command(version_cmd, show_output=False, debug_with_llm=False)
|
1447
|
+
if version_success:
|
1448
|
+
current_python_version = version_stdout.strip()
|
1449
|
+
print(f"🐍 Current Python version: {current_python_version}")
|
1450
|
+
|
1451
|
+
# Check if we've already tried switching Python versions
|
1452
|
+
if python_version_switched:
|
1453
|
+
print("⚠️ Already attempted to switch Python versions once, not trying again")
|
1454
|
+
break
|
1455
|
+
|
1456
|
+
print("🔄 Attempting to fix by switching Python version...")
|
1457
|
+
|
1458
|
+
# Install conda if not already installed
|
1459
|
+
if not conda_installed:
|
1460
|
+
print("📦 Installing Miniconda to manage Python versions...")
|
1461
|
+
conda_install_cmds = [
|
1462
|
+
"apt-get update -y",
|
1463
|
+
"apt-get install -y wget bzip2",
|
1464
|
+
"wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh",
|
1465
|
+
"bash /tmp/miniconda.sh -b -p /opt/conda",
|
1466
|
+
"rm /tmp/miniconda.sh",
|
1467
|
+
"echo 'export PATH=/opt/conda/bin:$PATH' >> ~/.bashrc",
|
1468
|
+
"export PATH=/opt/conda/bin:$PATH",
|
1469
|
+
"conda init bash",
|
1470
|
+
"source ~/.bashrc",
|
1471
|
+
"conda activate base"
|
1472
|
+
]
|
1473
|
+
|
1474
|
+
for conda_cmd in conda_install_cmds:
|
1475
|
+
print(f"🔄 Running: {conda_cmd}")
|
1476
|
+
conda_success, _, _ = run_command(conda_cmd, show_output=True, debug_with_llm=False)
|
1477
|
+
if not conda_success:
|
1478
|
+
print("⚠️ Failed to install conda, continuing with system Python")
|
1479
|
+
break
|
1480
|
+
|
1481
|
+
# Check if conda was successfully installed
|
1482
|
+
conda_check_cmd = "conda --version"
|
1483
|
+
conda_check_success, conda_check_stdout, _ = run_command(conda_check_cmd, show_output=True, debug_with_llm=False)
|
1484
|
+
conda_installed = conda_check_success
|
1485
|
+
|
1486
|
+
if conda_installed:
|
1487
|
+
print(f"✅ Successfully installed conda: {conda_check_stdout.strip()}")
|
1488
|
+
else:
|
1489
|
+
print("⚠️ Failed to verify conda installation")
|
1490
|
+
break
|
1491
|
+
|
1492
|
+
# Determine target Python version
|
1493
|
+
target_version = "3.10" # Default to a stable version
|
1494
|
+
if problematic_version == "3.13":
|
1495
|
+
# If we're on 3.13 and having issues, go to 3.10
|
1496
|
+
target_version = "3.10"
|
1497
|
+
elif "3.13" in str(current_python_version):
|
1498
|
+
# If we're on 3.13 for any other error, try 3.10
|
1499
|
+
target_version = "3.10"
|
1500
|
+
elif "3.10" in str(current_python_version):
|
1501
|
+
# If we're on 3.10 and having issues, try 3.9
|
1502
|
+
target_version = "3.9"
|
1503
|
+
|
1504
|
+
print(f"🐍 Switching from {current_python_version} to Python {target_version}...")
|
1505
|
+
|
1506
|
+
# Create and activate a conda environment with the target Python version
|
1507
|
+
conda_cmds = [
|
1508
|
+
f"conda create -y -n py{target_version} python={target_version}",
|
1509
|
+
f"echo 'conda activate py{target_version}' >> ~/.bashrc",
|
1510
|
+
f"conda init bash",
|
1511
|
+
f"source ~/.bashrc",
|
1512
|
+
f"conda activate py{target_version}"
|
1513
|
+
]
|
1514
|
+
|
1515
|
+
for conda_cmd in conda_cmds:
|
1516
|
+
print(f"🔄 Running: {conda_cmd}")
|
1517
|
+
conda_success, _, _ = run_command(conda_cmd, show_output=True, debug_with_llm=False)
|
1518
|
+
if not conda_success:
|
1519
|
+
print(f"⚠️ Failed to run conda command: {conda_cmd}")
|
1520
|
+
|
1521
|
+
# Verify Python version changed
|
1522
|
+
verify_cmd = "python --version"
|
1523
|
+
verify_success, verify_stdout, _ = run_command(verify_cmd, show_output=True, debug_with_llm=False)
|
1524
|
+
|
1525
|
+
if verify_success and target_version in verify_stdout:
|
1526
|
+
print(f"✅ Successfully switched to Python {verify_stdout.strip()}")
|
1527
|
+
python_version_switched = True
|
1528
|
+
current_python_version = verify_stdout.strip()
|
1529
|
+
|
1530
|
+
# Reinstall pip and setuptools in the new environment
|
1531
|
+
print("📦 Installing pip and setuptools in new environment...")
|
1532
|
+
run_command("pip install --upgrade pip setuptools wheel", show_output=True, debug_with_llm=False)
|
1533
|
+
|
1534
|
+
# Retry the original command with the new Python version
|
1535
|
+
print(f"🔄 Retrying original command with Python {target_version}...")
|
1536
|
+
# Reset the retry counter since we've made a significant change
|
1537
|
+
return run_command(cmd, show_output, 0, max_retries)
|
1538
|
+
else:
|
1539
|
+
print("⚠️ Failed to switch Python version, continuing with current version")
|
1540
|
+
|
1541
|
+
break
|
1542
|
+
|
1543
|
+
# Check if stderr is empty, try to use stdout as fallback
|
1544
|
+
debug_output = stderr_buffer
|
1545
|
+
if not debug_output or not debug_output.strip():
|
1546
|
+
print("⚠️ stderr is empty, checking if stdout contains error information...")
|
1547
|
+
if stdout_buffer and stdout_buffer.strip():
|
1548
|
+
print("✅ Using stdout for debugging as stderr is empty")
|
1549
|
+
debug_output = stdout_buffer
|
1550
|
+
else:
|
1551
|
+
print("⚠️ Both stderr and stdout are empty. Limited debugging information available.")
|
1552
|
+
debug_output = f"Command failed with exit code {exit_code}, but no error output was captured."
|
1553
|
+
|
1554
|
+
# Print debug output for verification
|
1555
|
+
print(f"🔍 Debug output to be sent to OpenAI ({len(debug_output)} chars):")
|
1556
|
+
print("="*60)
|
1557
|
+
print(debug_output if debug_output else "[EMPTY]")
|
1558
|
+
print("="*60)
|
1559
|
+
|
1560
|
+
fix_command = call_openai_for_debug(cmd_to_execute, debug_output, current_dir=current_dir, sandbox=sandbox)
|
1561
|
+
|
1562
|
+
if fix_command:
|
1563
|
+
print(f"🔧 OpenAI suggested fix command: {fix_command}")
|
1564
|
+
|
1565
|
+
# Check if the suggested command is "wandb login YOUR_API_KEY" or similar
|
1566
|
+
if "wandb login" in fix_command and ("YOUR_API_KEY" in fix_command or "[your_api_key]" in fix_command):
|
1567
|
+
print("🔍 Detected placeholder API key in suggested command")
|
1568
|
+
print("🔄 Prompting for actual W&B API key instead")
|
1569
|
+
|
1570
|
+
# Prompt for W&B API key
|
1571
|
+
print("\n" + "="*60)
|
1572
|
+
print("🔑 WEIGHTS & BIASES API KEY REQUIRED")
|
1573
|
+
print("="*60)
|
1574
|
+
print("You can get your API key from: https://wandb.ai/authorize")
|
1575
|
+
print("📝 Please paste your W&B API key below:")
|
1576
|
+
print(" (Your input will be hidden for security)")
|
1577
|
+
print("-" * 60)
|
1578
|
+
|
1579
|
+
try:
|
1580
|
+
api_key = getpass.getpass("W&B API Key: ").strip()
|
1581
|
+
if api_key:
|
1582
|
+
# Replace placeholder with actual API key
|
1583
|
+
fix_command = f"wandb login {api_key}"
|
1584
|
+
print(f"🔄 Using actual API key: wandb login [API_KEY_HIDDEN]")
|
1585
|
+
else:
|
1586
|
+
print("❌ No API key provided. Cannot continue with W&B login.")
|
1587
|
+
return False, stdout_buffer, stderr_buffer
|
1588
|
+
except Exception as e:
|
1589
|
+
print(f"❌ Error getting API key: {e}")
|
1590
|
+
return False, stdout_buffer, stderr_buffer
|
1591
|
+
|
1592
|
+
# Special handling for cd commands to prevent directory navigation loops
|
1593
|
+
if fix_command.strip().startswith("cd "):
|
1594
|
+
# Extract the target directory from the cd command
|
1595
|
+
cd_parts = fix_command.split(None, 1)
|
1596
|
+
if len(cd_parts) >= 2:
|
1597
|
+
target_dir = cd_parts[1].strip('"\'')
|
1598
|
+
|
1599
|
+
# Check if this is trying to navigate to a directory we're already in
|
1600
|
+
if target_dir.endswith(current_dir.split('/')[-1]) or current_dir.endswith('/' + target_dir):
|
1601
|
+
print(f"⚠️ Detected potential directory navigation loop")
|
1602
|
+
print(f"🔍 Current directory: {current_dir}")
|
1603
|
+
print(f"🔍 Suggested navigation: {target_dir}")
|
1604
|
+
|
1605
|
+
# Check if we're already in the target directory or a directory that contains it
|
1606
|
+
if current_dir.endswith('/' + target_dir) or ('/' + target_dir + '/' in current_dir):
|
1607
|
+
print(f"✅ Already in or past the target directory")
|
1608
|
+
print(f"🔄 Skipping redundant navigation and retrying the original command")
|
1609
|
+
return run_command(cmd, show_output, retry_count + 1, max_retries)
|
1610
|
+
|
1611
|
+
# Automatically run the fix command without asking for permission
|
1612
|
+
print(f"🔄 Running suggested fix command: {fix_command}")
|
1613
|
+
# Run the fix command with debugging disabled to prevent infinite loop
|
1614
|
+
fix_success, fix_stdout, fix_stderr = run_command(fix_command, show_output=True, debug_with_llm=False)
|
1615
|
+
|
1616
|
+
if fix_success:
|
1617
|
+
print("✅ Fix command succeeded!")
|
1618
|
+
# Retry the original command with reset retry count
|
1619
|
+
print(f"🔄 Retrying original command: {cmd}")
|
1620
|
+
|
1621
|
+
# Create a key for tracking this error
|
1622
|
+
error_key = f"{cmd}:{stderr_buffer[:100]}"
|
1623
|
+
|
1624
|
+
# Check if we've seen this error before
|
1625
|
+
if error_key in previous_errors:
|
1626
|
+
# We've seen this error before, don't reset the retry count
|
1627
|
+
previous_errors[error_key] += 1
|
1628
|
+
print(f"⚠️ Same error encountered {previous_errors[error_key]} times. Not resetting retry count.")
|
1629
|
+
return run_command(cmd, show_output, retry_count + 1, max_retries)
|
1630
|
+
else:
|
1631
|
+
# First time seeing this error, track it and reset retry count
|
1632
|
+
previous_errors[error_key] = 1
|
1633
|
+
print(f"🔄 Resetting retry count to 0 after successful fix")
|
1634
|
+
return run_command(cmd, show_output, 0, max_retries) # Reset retry count to 0
|
1635
|
+
else:
|
1636
|
+
print("❌ Fix command failed.")
|
1637
|
+
return False, stdout_buffer, stderr_buffer
|
1638
|
+
|
1639
|
+
return exit_code == 0, stdout_buffer, stderr_buffer
|
1640
|
+
|
1641
|
+
# Initialize the environment with basic commands
|
1642
|
+
print("🔄 Initializing environment...")
|
1643
|
+
init_commands = [
|
1644
|
+
"export PS1='$ '", # Set a simple prompt
|
1645
|
+
"export TERM=xterm-256color", # Set terminal type
|
1646
|
+
"source ~/.bashrc 2>/dev/null || true" # Source bashrc if available
|
1647
|
+
]
|
1648
|
+
|
1649
|
+
# Add volume-specific initialization if volume is available
|
1650
|
+
if volume:
|
1651
|
+
volume_commands = [
|
1652
|
+
f"mkdir -p {volume_mount_path}/venvs", # Create virtual environments directory
|
1653
|
+
f"mkdir -p {volume_mount_path}/cache", # Create cache directory
|
1654
|
+
f"export PIP_CACHE_DIR={volume_mount_path}/cache/pip", # Pip cache
|
1655
|
+
f"export UV_CACHE_DIR={volume_mount_path}/cache/uv", # UV cache
|
1656
|
+
]
|
1657
|
+
init_commands.extend(volume_commands)
|
1658
|
+
print(f"📦 Setting up persistent storage directories in {volume_mount_path}")
|
1659
|
+
|
1660
|
+
# Run initialization commands
|
1661
|
+
for i, init_cmd in enumerate(init_commands, 1):
|
1662
|
+
print(f"📋 Running init command {i}/{len(init_commands)}: {init_cmd}")
|
1663
|
+
success, stdout, stderr = run_command(init_cmd, show_output=False)
|
1664
|
+
if not success:
|
1665
|
+
print(f"⚠️ Init command failed: {stderr}")
|
1666
|
+
|
1667
|
+
print("✅ Environment initialization completed")
|
1668
|
+
|
1669
|
+
print("📦 Installing basic tools...")
|
1670
|
+
run_command("apt-get update && apt-get install -y git curl wget")
|
1671
|
+
|
1672
|
+
print("📦 Installing uv with pip...")
|
1673
|
+
run_command("pip install uv")
|
1674
|
+
|
1675
|
+
# Set uv path to system installation
|
1676
|
+
uv_path = "uv"
|
1677
|
+
|
1678
|
+
# Test if uv is available and working
|
1679
|
+
test_uv_cmd = f"{uv_path} --version || echo 'uv not found'"
|
1680
|
+
test_success, test_stdout, test_stderr = run_command(test_uv_cmd)
|
1681
|
+
if not test_success or 'uv not found' in test_stdout:
|
1682
|
+
print("⚠️ uv installation not found in system path, trying alternative installation...")
|
1683
|
+
# Try alternative installation method
|
1684
|
+
print("📦 Installing uv using the official installer...")
|
1685
|
+
run_command("curl -LsSf https://astral.sh/uv/install.sh | sh")
|
1686
|
+
run_command("source $HOME/.local/bin/env")
|
1687
|
+
run_command('export PATH="$HOME/.local/bin:$PATH"')
|
1688
|
+
|
1689
|
+
# Update path to the local installation
|
1690
|
+
uv_path = "$HOME/.local/bin/uv"
|
1691
|
+
|
1692
|
+
# Test again
|
1693
|
+
test_uv_cmd = f"{uv_path} --version || echo 'uv not found'"
|
1694
|
+
test_success, test_stdout, test_stderr = run_command(test_uv_cmd)
|
1695
|
+
if not test_success or 'uv not found' in test_stdout:
|
1696
|
+
print("⚠️ uv installation still failed, using standard pip")
|
1697
|
+
uv_path = ""
|
1698
|
+
else:
|
1699
|
+
print(f"✅ uv installed successfully via alternative method: {test_stdout.strip()}")
|
1700
|
+
else:
|
1701
|
+
print(f"✅ uv installed successfully via pip: {test_stdout.strip()}")
|
1702
|
+
|
1703
|
+
# Initialize repo_clone_dir for use throughout the function
|
1704
|
+
repo_clone_dir = "/root" # Always use home directory for repositories
|
1705
|
+
|
1706
|
+
# Clone repository if URL is provided
|
1707
|
+
if repo_url:
|
1708
|
+
try:
|
1709
|
+
# Extract repo name from URL
|
1710
|
+
repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
|
1711
|
+
|
1712
|
+
print(f"📥 Cloning repository in Modal container: {repo_url}")
|
1713
|
+
|
1714
|
+
# Determine the best location for the repository
|
1715
|
+
repo_clone_dir = "/root" # Always use home directory for repositories
|
1716
|
+
print(f"📦 Using home directory for repository: {repo_clone_dir}")
|
1717
|
+
|
1718
|
+
# Ensure we're in the home directory and update current directory tracking
|
1719
|
+
cd_success, cd_stdout, cd_stderr = run_command(f"cd {repo_clone_dir}", show_output=False)
|
1720
|
+
if cd_success:
|
1721
|
+
current_dir = repo_clone_dir
|
1722
|
+
print(f"📂 Successfully changed to: {repo_clone_dir}")
|
1723
|
+
else:
|
1724
|
+
print(f"⚠️ Failed to change to {repo_clone_dir}: {cd_stderr}")
|
1725
|
+
current_dir = "/"
|
1726
|
+
|
1727
|
+
# First, list current directory contents for debugging
|
1728
|
+
print("📂 Current directory contents before cloning:")
|
1729
|
+
run_command("pwd && ls -la", show_output=True)
|
1730
|
+
|
1731
|
+
# Check if repository already exists in current location
|
1732
|
+
print(f"🔍 Checking if {repo_name_from_url} directory exists...")
|
1733
|
+
|
1734
|
+
# First ensure we're in the right directory and check with absolute path
|
1735
|
+
check_cmd = f"cd {repo_clone_dir} && test -d {repo_name_from_url}"
|
1736
|
+
success, stdout, stderr = run_command(check_cmd, show_output=False, retry_count=0, max_retries=0)
|
1737
|
+
|
1738
|
+
# The directory exists if the test command succeeds (exit code 0)
|
1739
|
+
repo_exists = success
|
1740
|
+
print(f"📂 Repository check result: exists={repo_exists} (exit code: {0 if success else 1})")
|
1741
|
+
print(f"📂 Checking in directory: {repo_clone_dir}/{repo_name_from_url}")
|
1742
|
+
|
1743
|
+
if repo_exists:
|
1744
|
+
print(f"📂 Repository directory already exists: {repo_name_from_url}")
|
1745
|
+
# Check if it's actually a git repository - disable retries to avoid bad debugging
|
1746
|
+
git_check_cmd = f"cd {repo_clone_dir}/{repo_name_from_url} && git status"
|
1747
|
+
git_check_success, git_stdout, git_stderr = run_command(git_check_cmd, show_output=False, retry_count=0, max_retries=0)
|
1748
|
+
if git_check_success:
|
1749
|
+
print(f"✅ Valid git repository found, using existing: {repo_name_from_url}")
|
1750
|
+
else:
|
1751
|
+
print(f"⚠️ Directory exists but is not a valid git repository, removing and re-cloning...")
|
1752
|
+
remove_cmd = f"cd {repo_clone_dir} && rm -rf {repo_name_from_url}"
|
1753
|
+
run_command(remove_cmd, show_output=False)
|
1754
|
+
repo_exists = False
|
1755
|
+
|
1756
|
+
if not repo_exists:
|
1757
|
+
print(f"📥 Repository does not exist, proceeding with clone...")
|
1758
|
+
print(f"📥 Cloning repository: {repo_url}")
|
1759
|
+
print(f"📥 Repository name will be: {repo_name_from_url}")
|
1760
|
+
print(f"📥 Clone location: {repo_clone_dir}")
|
1761
|
+
|
1762
|
+
# Ensure we're in the right directory before cloning
|
1763
|
+
run_command(f"cd {repo_clone_dir}", show_output=False)
|
1764
|
+
|
1765
|
+
# Execute the git clone command with verbose output - use absolute path, disable retries
|
1766
|
+
clone_cmd = f"cd {repo_clone_dir} && git clone {repo_url}"
|
1767
|
+
clone_success, clone_stdout, clone_stderr = run_command(clone_cmd, show_output=True, retry_count=0, max_retries=0)
|
1768
|
+
|
1769
|
+
print(f"📥 Clone command completed. Success: {clone_success}")
|
1770
|
+
if clone_stdout.strip():
|
1771
|
+
print(f"📥 Clone stdout: {clone_stdout.strip()}")
|
1772
|
+
if clone_stderr.strip():
|
1773
|
+
print(f"📥 Clone stderr: {clone_stderr.strip()}")
|
1774
|
+
|
1775
|
+
if not clone_success:
|
1776
|
+
print(f"❌ Failed to clone repository: {clone_stderr}")
|
1777
|
+
print("🔄 Trying alternative clone methods...")
|
1778
|
+
|
1779
|
+
# Try with different git options - use absolute path, disable retries
|
1780
|
+
print("🔄 Attempting shallow clone...")
|
1781
|
+
shallow_clone_cmd = f"cd {repo_clone_dir} && git clone --depth 1 {repo_url}"
|
1782
|
+
clone_success, clone_stdout, clone_stderr = run_command(shallow_clone_cmd, show_output=True, retry_count=0, max_retries=0)
|
1783
|
+
|
1784
|
+
print(f"📥 Shallow clone command completed. Success: {clone_success}")
|
1785
|
+
if clone_stdout.strip():
|
1786
|
+
print(f"📥 Shallow clone stdout: {clone_stdout.strip()}")
|
1787
|
+
if clone_stderr.strip():
|
1788
|
+
print(f"📥 Shallow clone stderr: {clone_stderr.strip()}")
|
1789
|
+
|
1790
|
+
if not clone_success:
|
1791
|
+
print(f"❌ Alternative clone also failed: {clone_stderr}")
|
1792
|
+
print("⚠️ Continuing without repository...")
|
1793
|
+
repo_name_from_url = None
|
1794
|
+
else:
|
1795
|
+
print(f"✅ Repository cloned successfully with shallow clone")
|
1796
|
+
else:
|
1797
|
+
print(f"✅ Repository cloned successfully")
|
1798
|
+
else:
|
1799
|
+
print(f"📂 Repository already exists, skipping clone")
|
1800
|
+
|
1801
|
+
# Verify repository directory exists and change to it
|
1802
|
+
if repo_name_from_url:
|
1803
|
+
print("📂 Verifying repository directory...")
|
1804
|
+
|
1805
|
+
# List available directories for debugging
|
1806
|
+
print("📂 Available directories after cloning:")
|
1807
|
+
run_command("ls -la", show_output=True)
|
1808
|
+
|
1809
|
+
# Check if the repository directory exists using simple test
|
1810
|
+
check_success, _, _ = run_command(f"test -d {repo_name_from_url}", show_output=False)
|
1811
|
+
|
1812
|
+
if check_success:
|
1813
|
+
print(f"📂 Repository directory confirmed: {repo_name_from_url}")
|
1814
|
+
# Change to the repository directory
|
1815
|
+
cd_success, cd_stdout, cd_stderr = run_command(f"cd {repo_name_from_url}")
|
1816
|
+
if cd_success:
|
1817
|
+
print(f"📂 Successfully changed to repository directory: {repo_name_from_url}")
|
1818
|
+
repo_dir_name = f"{repo_clone_dir}/{repo_name_from_url}" if repo_clone_dir != "/" else repo_name_from_url
|
1819
|
+
else:
|
1820
|
+
print(f"⚠️ Failed to change to repository directory: {cd_stderr}")
|
1821
|
+
repo_dir_name = repo_clone_dir
|
1822
|
+
else:
|
1823
|
+
print(f"⚠️ Repository directory not found after cloning: {repo_name_from_url}")
|
1824
|
+
print("🔍 Looking for alternative directories...")
|
1825
|
+
|
1826
|
+
# Look for any git repositories
|
1827
|
+
search_success, search_stdout, search_stderr = run_command("find . -maxdepth 1 -type d -name '.git' -exec dirname {} \\;", show_output=False)
|
1828
|
+
|
1829
|
+
if search_success and search_stdout.strip():
|
1830
|
+
found_dirs = [d.replace('./', '') for d in search_stdout.strip().split('\n') if d.strip() and d != '.']
|
1831
|
+
if found_dirs:
|
1832
|
+
repo_dir_name = f"{repo_clone_dir}/{found_dirs[0]}" if repo_clone_dir != "/" else found_dirs[0]
|
1833
|
+
print(f"📂 Found git repository: {repo_dir_name}")
|
1834
|
+
run_command(f"cd {found_dirs[0]}")
|
1835
|
+
else:
|
1836
|
+
repo_dir_name = repo_clone_dir
|
1837
|
+
print("📂 Using current directory")
|
1838
|
+
else:
|
1839
|
+
repo_dir_name = repo_clone_dir
|
1840
|
+
print("📂 Using current directory")
|
1841
|
+
else:
|
1842
|
+
repo_dir_name = repo_clone_dir
|
1843
|
+
print("📂 No valid repository, using current directory")
|
1844
|
+
|
1845
|
+
# Show final directory status
|
1846
|
+
print("📂 Final directory status:")
|
1847
|
+
run_command("pwd && ls -la", show_output=True)
|
1848
|
+
|
1849
|
+
except Exception as e:
|
1850
|
+
print(f"❌ Error during repository cloning: {e}")
|
1851
|
+
print(f"❌ Exception type: {type(e).__name__}")
|
1852
|
+
print("⚠️ Continuing without repository...")
|
1853
|
+
repo_dir_name = repo_clone_dir
|
1854
|
+
run_command("pwd && ls -la", show_output=True)
|
1855
|
+
else:
|
1856
|
+
repo_dir_name = repo_clone_dir
|
1857
|
+
print("📂 No repository URL provided, using current directory")
|
1858
|
+
run_command("pwd && ls -la", show_output=True)
|
1859
|
+
|
1860
|
+
# Run setup commands if provided - now we're already in the repository directory
|
1861
|
+
if setup_commands:
|
1862
|
+
print("⚙️ Running user setup commands in Modal container...")
|
1863
|
+
|
1864
|
+
# Check if git clone is already in the setup commands
|
1865
|
+
has_git_clone = any('git clone' in cmd for cmd in setup_commands)
|
1866
|
+
|
1867
|
+
# Only add git clone if:
|
1868
|
+
# 1. No git clone in setup commands AND
|
1869
|
+
# 2. We have a repo URL AND
|
1870
|
+
# 3. Repository was NOT already cloned successfully
|
1871
|
+
if not has_git_clone and repo_url and not repo_exists:
|
1872
|
+
print("📥 Git clone not found in setup commands and repository not yet cloned, adding it...")
|
1873
|
+
clone_cmd = f"git clone {repo_url}"
|
1874
|
+
setup_commands = [clone_cmd] + setup_commands
|
1875
|
+
print(f"📥 Added git clone command: {clone_cmd}")
|
1876
|
+
elif has_git_clone and repo_exists:
|
1877
|
+
print("⚠️ Repository already cloned successfully, removing duplicate git clone from setup commands...")
|
1878
|
+
# Remove git clone commands since repository is already cloned
|
1879
|
+
setup_commands = [cmd for cmd in setup_commands if 'git clone' not in cmd]
|
1880
|
+
print(f"📥 Removed duplicate git clone commands")
|
1881
|
+
elif repo_exists:
|
1882
|
+
print("📂 Repository already cloned successfully, skipping git clone in setup commands")
|
1883
|
+
|
1884
|
+
# Print all commands that will be executed
|
1885
|
+
print("📋 Setup commands to execute in container:")
|
1886
|
+
for i, cmd in enumerate(setup_commands, 1):
|
1887
|
+
print(f" {i}. {cmd}")
|
1888
|
+
|
1889
|
+
print(f"\n🚀 Executing commands in container directory: {repo_dir_name}")
|
1890
|
+
|
1891
|
+
# Ensure we start in the /root directory and reset current_dir
|
1892
|
+
current_dir = "/root"
|
1893
|
+
print(f"📂 Resetting working directory to: {current_dir}")
|
1894
|
+
|
1895
|
+
# Verify we can access /root directory
|
1896
|
+
verify_success, verify_output, _ = run_command("pwd", show_output=True)
|
1897
|
+
if verify_success:
|
1898
|
+
print(f"✅ Current directory verified: {verify_output.strip()}")
|
1899
|
+
|
1900
|
+
# Execute each command individually in the repository directory within the container
|
1901
|
+
for i, cmd in enumerate(setup_commands, 1):
|
1902
|
+
print(f"\n📋 Executing command {i}/{len(setup_commands)} in container: {cmd}")
|
1903
|
+
|
1904
|
+
# If this is a cd command, just run it directly
|
1905
|
+
if cmd.strip().startswith('cd '):
|
1906
|
+
# Execute the command directly (we're already in the right directory)
|
1907
|
+
success, stdout, stderr = run_command(cmd)
|
1908
|
+
continue
|
1909
|
+
|
1910
|
+
# For git clone commands, handle as before
|
1911
|
+
if 'git clone' in cmd:
|
1912
|
+
# Execute the command directly
|
1913
|
+
success, stdout, stderr = run_command(cmd)
|
1914
|
+
|
1915
|
+
if success:
|
1916
|
+
print(f"✅ Command executed successfully in container: {cmd}")
|
1917
|
+
if stdout.strip():
|
1918
|
+
print(f"📄 Output: {stdout.strip()}")
|
1919
|
+
|
1920
|
+
# Handle repository directory change as before
|
1921
|
+
print("📂 Git clone detected, attempting to change to repository directory...")
|
1922
|
+
# Extract repository name from the clone command
|
1923
|
+
parts = cmd.split()
|
1924
|
+
if len(parts) >= 3:
|
1925
|
+
clone_url = parts[2] # git clone <url>
|
1926
|
+
target_dir = clone_url.split('/')[-1].replace('.git', '')
|
1927
|
+
|
1928
|
+
# Check if we're already in the target directory
|
1929
|
+
if current_dir.endswith(f"/{target_dir}") or current_dir == f"/{target_dir}":
|
1930
|
+
print(f"📂 Already in target directory: {current_dir}")
|
1931
|
+
else:
|
1932
|
+
# The repository should now be at current_dir/target_dir
|
1933
|
+
repo_full_path = f"{current_dir.rstrip('/')}/{target_dir}"
|
1934
|
+
|
1935
|
+
# Check if directory exists using absolute path
|
1936
|
+
dir_check_success, _, _ = run_command(f"test -d '{repo_full_path}'", show_output=False)
|
1937
|
+
if dir_check_success:
|
1938
|
+
current_dir = repo_full_path
|
1939
|
+
print(f"📂 Successfully changed current directory to: {current_dir}")
|
1940
|
+
# Verify the change worked
|
1941
|
+
verify_success, verify_output, _ = run_command("pwd", show_output=True)
|
1942
|
+
if verify_success:
|
1943
|
+
print(f"✅ Directory change verified: {verify_output.strip()}")
|
1944
|
+
# List contents to confirm we're in the right place
|
1945
|
+
run_command("ls -la", show_output=True)
|
1946
|
+
|
1947
|
+
# Initialize git submodules if they exist
|
1948
|
+
print("📦 Checking for git submodules...")
|
1949
|
+
submodule_check_success, _, _ = run_command("test -f .gitmodules", show_output=False)
|
1950
|
+
if submodule_check_success:
|
1951
|
+
print("📦 Git submodules found, initializing...")
|
1952
|
+
run_command("git submodule update --init --recursive", show_output=True)
|
1953
|
+
print("✅ Git submodules initialized")
|
1954
|
+
else:
|
1955
|
+
print("📦 No git submodules found")
|
1956
|
+
else:
|
1957
|
+
print("⚠️ Directory change verification failed")
|
1958
|
+
else:
|
1959
|
+
print(f"⚠️ Repository directory {repo_full_path} not found after clone")
|
1960
|
+
print("🔍 Checking what was actually created:")
|
1961
|
+
run_command("find . -maxdepth 2 -name '*.git' -type d", show_output=True)
|
1962
|
+
run_command("ls -la", show_output=True)
|
1963
|
+
else:
|
1964
|
+
# For Python commands, make sure we're in the correct directory first
|
1965
|
+
if cmd.startswith('python '):
|
1966
|
+
# Fix the directory path issue - ensure we're in the correct repository directory
|
1967
|
+
# Check if we're in a nested directory that matches the repo name
|
1968
|
+
repo_dir_parts = current_dir.split('/')
|
1969
|
+
if len(repo_dir_parts) >= 2 and repo_dir_parts[-1] == repo_dir_parts[-2]:
|
1970
|
+
# We're in a nested directory like /root/nanoGPT/nanoGPT
|
1971
|
+
# Move up one level to /root/nanoGPT
|
1972
|
+
print(f"⚠️ Detected nested directory structure: {current_dir}")
|
1973
|
+
parent_dir = '/'.join(repo_dir_parts[:-1])
|
1974
|
+
print(f"🔄 Moving to parent directory: {parent_dir}")
|
1975
|
+
cd_success, _, _ = run_command(f"cd {parent_dir}", show_output=False)
|
1976
|
+
if cd_success:
|
1977
|
+
current_dir = parent_dir
|
1978
|
+
print(f"📂 Updated current directory to: {current_dir}")
|
1979
|
+
|
1980
|
+
# Execute the command directly (we're already in the right directory)
|
1981
|
+
success, stdout, stderr = run_command(cmd)
|
1982
|
+
|
1983
|
+
if success:
|
1984
|
+
print(f"✅ Command executed successfully in container: {cmd}")
|
1985
|
+
if stdout.strip():
|
1986
|
+
print(f"📄 Output: {stdout.strip()}")
|
1987
|
+
else:
|
1988
|
+
print(f"❌ Command failed in container: {cmd}")
|
1989
|
+
print(f"❌ Error: {stderr}")
|
1990
|
+
# Continue with next command even if this one failed
|
1991
|
+
|
1992
|
+
# Show final status of the repository directory in container
|
1993
|
+
print(f"\n📂 Final directory contents in container ({repo_dir_name}):")
|
1994
|
+
run_command("pwd && ls -la")
|
1995
|
+
|
1996
|
+
else:
|
1997
|
+
print("⚠️ No setup commands provided.")
|
1998
|
+
|
1999
|
+
# If no setup commands but we have a repo URL, at least try to clone it
|
2000
|
+
if repo_url and not repo_exists:
|
2001
|
+
print("📥 No setup commands provided, but cloning repository anyway...")
|
2002
|
+
clone_success, _, _ = run_command(f"git clone {repo_url}", show_output=True)
|
2003
|
+
if clone_success:
|
2004
|
+
print(f"✅ Repository cloned successfully")
|
2005
|
+
# Try to change to the repository directory
|
2006
|
+
if repo_name_from_url:
|
2007
|
+
run_command(f"cd {repo_name_from_url}")
|
2008
|
+
print("📂 Final directory status after clone:")
|
2009
|
+
run_command("pwd && ls -la", show_output=True)
|
2010
|
+
|
2011
|
+
# Write container ID to file for future reference
|
2012
|
+
with open(os.path.expanduser("~/.modal_last_container_id"), "w") as f:
|
2013
|
+
f.write(container_id)
|
2014
|
+
|
2015
|
+
# Print connection instructions
|
2016
|
+
print(f"✅ Modal sandbox created successfully!")
|
2017
|
+
print(f"📋 Sandbox ID: {sandbox_id}")
|
2018
|
+
print(f"📋 Container ID: {container_id}")
|
2019
|
+
if volume:
|
2020
|
+
print(f"📦 Volume: {volume_name} (mounted at {volume_mount_path})")
|
2021
|
+
print(f"💾 Persistent storage available for pip and uv caches")
|
2022
|
+
print(f"📂 Repositories will be cloned in home directory (/root) for faster access")
|
2023
|
+
print("🔗 To connect to this container, run:")
|
2024
|
+
print(f"modal container exec --pty {container_id} bash")
|
2025
|
+
print("⏳ Sandbox will remain running until you terminate it with:")
|
2026
|
+
print(f"modal sandbox terminate {sandbox_id}")
|
2027
|
+
|
2028
|
+
# Try to open a new terminal window and connect to the container
|
2029
|
+
if container_id:
|
2030
|
+
print("🖥️ Attempting to open new terminal window...")
|
2031
|
+
# Use osascript to open a new terminal with the modal shell command
|
2032
|
+
terminal_script = f'''
|
2033
|
+
tell application "Terminal"
|
2034
|
+
do script "modal shell {container_id}"
|
2035
|
+
activate
|
2036
|
+
end tell
|
2037
|
+
'''
|
2038
|
+
|
2039
|
+
try:
|
2040
|
+
result = subprocess.run(['osascript', '-e', terminal_script],
|
2041
|
+
capture_output=True, text=True, timeout=30)
|
2042
|
+
if result.returncode == 0:
|
2043
|
+
print("✅ New terminal window opened successfully")
|
2044
|
+
else:
|
2045
|
+
print(f"⚠️ Failed to open terminal window: {result.stderr}")
|
2046
|
+
|
2047
|
+
# Try alternative approach with iTerm2 if Terminal failed
|
2048
|
+
print("🔄 Trying with iTerm2 instead...")
|
2049
|
+
iterm_script = f'''
|
2050
|
+
tell application "iTerm"
|
2051
|
+
create window with default profile
|
2052
|
+
tell current session of current window
|
2053
|
+
write text "modal shell {container_id}"
|
2054
|
+
end tell
|
2055
|
+
end tell
|
2056
|
+
'''
|
2057
|
+
|
2058
|
+
try:
|
2059
|
+
iterm_result = subprocess.run(['osascript', '-e', iterm_script],
|
2060
|
+
capture_output=True, text=True, timeout=30)
|
2061
|
+
if iterm_result.returncode == 0:
|
2062
|
+
print("✅ New iTerm2 window opened successfully")
|
2063
|
+
else:
|
2064
|
+
print(f"⚠️ Failed to open iTerm2 window: {iterm_result.stderr}")
|
2065
|
+
print("📝 You can manually connect using:")
|
2066
|
+
print(f" modal shell {container_id}")
|
2067
|
+
except Exception as e:
|
2068
|
+
print(f"⚠️ Error opening iTerm2: {e}")
|
2069
|
+
print("📝 You can manually connect using:")
|
2070
|
+
print(f" modal shell {container_id}")
|
2071
|
+
except subprocess.TimeoutExpired:
|
2072
|
+
print("⚠️ Terminal opening timed out")
|
2073
|
+
except Exception as e:
|
2074
|
+
print(f"⚠️ Error opening terminal: {e}")
|
2075
|
+
print("📝 You can manually connect using:")
|
2076
|
+
print(f" modal shell {container_id}")
|
2077
|
+
|
2078
|
+
# Also provide manual connection instructions
|
2079
|
+
print("\n" + "="*60)
|
2080
|
+
print("🚀 SANDBOX READY!")
|
2081
|
+
print("="*60)
|
2082
|
+
print(f"📋 Sandbox ID: {sandbox_id}")
|
2083
|
+
print(f"🆔 Container ID: {container_id}")
|
2084
|
+
if volume:
|
2085
|
+
print(f"💾 Volume: {volume_name} mounted at {volume_mount_path}")
|
2086
|
+
print("📁 Persistent storage available for caches and repositories")
|
2087
|
+
print("\n🔗 To connect to your container, run:")
|
2088
|
+
print(f" modal shell {container_id}")
|
2089
|
+
print("="*60)
|
2090
|
+
else:
|
2091
|
+
print("❌ No container ID available for connection")
|
2092
|
+
|
2093
|
+
return {
|
2094
|
+
"run_command": run_command,
|
2095
|
+
"current_dir": current_dir,
|
2096
|
+
"execution_history": execution_history,
|
2097
|
+
"container_id": container_id,
|
2098
|
+
"sandbox_id": sandbox_id
|
2099
|
+
}
|
2100
|
+
|
940
2101
|
|
941
2102
|
def handle_interactive_input(prompt, is_password=False):
|
942
2103
|
"""Handle interactive input from the user with optional password masking"""
|
@@ -980,7 +2141,7 @@ ssh_app = modal.App("ssh-container-app")
|
|
980
2141
|
"python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
|
981
2142
|
"gpg", "ca-certificates", "software-properties-common"
|
982
2143
|
)
|
983
|
-
.pip_install("uv", "modal"
|
2144
|
+
.pip_install("uv", "modal") # Fast Python package installer and Modal
|
984
2145
|
.run_commands(
|
985
2146
|
# Create SSH directory
|
986
2147
|
"mkdir -p /var/run/sshd",
|
@@ -1011,7 +2172,7 @@ ssh_app = modal.App("ssh-container-app")
|
|
1011
2172
|
memory=8192,
|
1012
2173
|
serialized=True,
|
1013
2174
|
)
|
1014
|
-
def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_commands=None
|
2175
|
+
def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_commands=None):
|
1015
2176
|
import subprocess
|
1016
2177
|
import time
|
1017
2178
|
import os
|
@@ -1025,13 +2186,6 @@ def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_co
|
|
1025
2186
|
# Setup environment
|
1026
2187
|
os.environ['PS1'] = r'\[\e[1;32m\]modal:\[\e[1;34m\]\w\[\e[0m\]$ '
|
1027
2188
|
|
1028
|
-
# Set OpenAI API key if provided
|
1029
|
-
if openai_api_key:
|
1030
|
-
os.environ['OPENAI_API_KEY'] = openai_api_key
|
1031
|
-
print(f"✅ Set OpenAI API key in container environment (length: {len(openai_api_key)})")
|
1032
|
-
else:
|
1033
|
-
print("⚠️ No OpenAI API key provided to container")
|
1034
|
-
|
1035
2189
|
# Clone repository if provided
|
1036
2190
|
if repo_url:
|
1037
2191
|
repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
|
@@ -1053,118 +2207,17 @@ def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_co
|
|
1053
2207
|
# Run setup commands if provided
|
1054
2208
|
if setup_commands:
|
1055
2209
|
print(f"⚙️ Running {len(setup_commands)} setup commands...")
|
1056
|
-
|
1057
|
-
# First, let's check the current directory structure
|
1058
|
-
print("🔍 Checking current directory structure before running setup commands...")
|
1059
|
-
try:
|
1060
|
-
result = subprocess.run("pwd && ls -la", shell=True, check=True,
|
1061
|
-
capture_output=True, text=True)
|
1062
|
-
print(f"📂 Current directory: {result.stdout}")
|
1063
|
-
except subprocess.CalledProcessError as e:
|
1064
|
-
print(f"⚠️ Could not check directory structure: {e}")
|
1065
|
-
|
1066
|
-
# Define a simple run_command function for SSH container
|
1067
|
-
def run_command_with_llm_debug(cmd, show_output=True, retry_count=0, max_retries=3):
|
1068
|
-
"""Execute a command with LLM debugging enabled"""
|
1069
|
-
print(f"🔧 Executing: {cmd}")
|
1070
|
-
try:
|
1071
|
-
# Handle special case for source command which doesn't work with subprocess.run
|
1072
|
-
if cmd.strip().startswith("source ") or " source " in cmd:
|
1073
|
-
print("⚠️ Detected 'source' command which doesn't work with subprocess.run")
|
1074
|
-
print("🔄 Converting to bash -c with dot (.) instead of source")
|
1075
|
-
# Replace source with . (dot) which is the same as source but works in sh
|
1076
|
-
modified_cmd = cmd.replace("source ", ". ")
|
1077
|
-
# Wrap in bash -c to ensure it runs in bash
|
1078
|
-
bash_cmd = f"bash -c '{modified_cmd}'"
|
1079
|
-
print(f"🔄 Modified command: {bash_cmd}")
|
1080
|
-
result = subprocess.run(bash_cmd, shell=True, check=True,
|
1081
|
-
capture_output=True, text=True)
|
1082
|
-
else:
|
1083
|
-
result = subprocess.run(cmd, shell=True, check=True,
|
1084
|
-
capture_output=True, text=True)
|
1085
|
-
|
1086
|
-
if result.stdout and show_output:
|
1087
|
-
print(f"✅ Output: {result.stdout}")
|
1088
|
-
return True, result.stdout, ""
|
1089
|
-
except subprocess.CalledProcessError as e:
|
1090
|
-
error_output = e.stderr if e.stderr else str(e)
|
1091
|
-
print(f"❌ Command failed: {e}")
|
1092
|
-
print(f"❌ Error: {error_output}")
|
1093
|
-
|
1094
|
-
# Call OpenAI for debugging
|
1095
|
-
print("🔍 Attempting to debug the failed command with OpenAI...")
|
1096
|
-
try:
|
1097
|
-
# Get the current directory for context
|
1098
|
-
current_dir = os.getcwd()
|
1099
|
-
|
1100
|
-
# Call OpenAI for debugging
|
1101
|
-
print(f"🔍 DEBUG: About to call call_openai_for_debug...")
|
1102
|
-
print(f"🔍 DEBUG: Command: {cmd}")
|
1103
|
-
print(f"🔍 DEBUG: Error output length: {len(error_output)}")
|
1104
|
-
print(f"🔍 DEBUG: Current directory: {current_dir}")
|
1105
|
-
|
1106
|
-
# Get the API key from environment or use the one that was fetched earlier
|
1107
|
-
api_key = os.environ.get("OPENAI_API_KEY")
|
1108
|
-
fix_command = call_openai_for_debug(cmd, error_output, api_key=api_key, current_dir=current_dir)
|
1109
|
-
|
1110
|
-
print(f"🔍 DEBUG: call_openai_for_debug returned: {fix_command}")
|
1111
|
-
|
1112
|
-
if fix_command:
|
1113
|
-
print(f"🔧 OpenAI suggested fix command: {fix_command}")
|
1114
|
-
|
1115
|
-
# Run the fix command
|
1116
|
-
print(f"🔄 Running suggested fix command: {fix_command}")
|
1117
|
-
try:
|
1118
|
-
fix_result = subprocess.run(fix_command, shell=True, check=True,
|
1119
|
-
capture_output=True, text=True)
|
1120
|
-
if fix_result.stdout:
|
1121
|
-
print(f"✅ Fix command output: {fix_result.stdout}")
|
1122
|
-
|
1123
|
-
# Retry the original command
|
1124
|
-
print(f"🔄 Retrying original command: {cmd}")
|
1125
|
-
return run_command_with_llm_debug(cmd, show_output, retry_count + 1, max_retries)
|
1126
|
-
except subprocess.CalledProcessError as fix_e:
|
1127
|
-
print(f"❌ Fix command also failed: {fix_e}")
|
1128
|
-
return False, "", error_output
|
1129
|
-
else:
|
1130
|
-
print("❌ No fix suggested by OpenAI")
|
1131
|
-
return False, "", error_output
|
1132
|
-
|
1133
|
-
except Exception as debug_e:
|
1134
|
-
print(f"❌ LLM debugging failed: {debug_e}")
|
1135
|
-
return False, "", error_output
|
1136
|
-
|
1137
2210
|
for i, cmd in enumerate(setup_commands, 1):
|
1138
2211
|
print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
1144
|
-
|
1145
|
-
|
1146
|
-
|
1147
|
-
|
1148
|
-
capture_output=True, text=True)
|
1149
|
-
if check_result.returncode != 0:
|
1150
|
-
print(f"⚠️ Directory does not exist: {target_dir}")
|
1151
|
-
print(f"🔍 Current directory contents:")
|
1152
|
-
subprocess.run("pwd && ls -la", shell=True, check=False)
|
1153
|
-
|
1154
|
-
# Try to find similar directories
|
1155
|
-
print(f"🔍 Looking for similar directories...")
|
1156
|
-
subprocess.run("find . -type d -name '*llama*' -o -name '*nano*' 2>/dev/null | head -10", shell=True, check=False)
|
1157
|
-
except Exception as e:
|
1158
|
-
print(f"⚠️ Could not check directory: {e}")
|
1159
|
-
|
1160
|
-
success, stdout, stderr = run_command_with_llm_debug(cmd, show_output=True)
|
1161
|
-
if not success:
|
1162
|
-
print(f"⚠️ Command {i} failed, but continuing with remaining commands...")
|
1163
|
-
|
1164
|
-
# If this was a cd command that failed, try to understand the directory structure
|
1165
|
-
if cmd.strip().startswith("cd ") and "No such file or directory" in stderr:
|
1166
|
-
print(f"🔍 Analyzing directory structure after failed cd command...")
|
1167
|
-
subprocess.run("pwd && ls -la && echo '--- Parent directory ---' && ls -la ..", shell=True, check=False)
|
2212
|
+
try:
|
2213
|
+
result = subprocess.run(cmd, shell=True, check=True,
|
2214
|
+
capture_output=True, text=True)
|
2215
|
+
if result.stdout:
|
2216
|
+
print(f"✅ Output: {result.stdout}")
|
2217
|
+
except subprocess.CalledProcessError as e:
|
2218
|
+
print(f"❌ Command failed: {e}")
|
2219
|
+
if e.stderr:
|
2220
|
+
print(f"❌ Error: {e.stderr}")
|
1168
2221
|
|
1169
2222
|
# Get container info
|
1170
2223
|
print("🔍 Container started successfully!")
|
@@ -1182,56 +2235,23 @@ def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_co
|
|
1182
2235
|
subprocess.run(["service", "ssh", "start"], check=True)
|
1183
2236
|
|
1184
2237
|
# Now modify the create_modal_ssh_container function to use the standalone ssh_container_function
|
2238
|
+
|
1185
2239
|
def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_commands=None,
|
1186
|
-
volume_name=None, timeout_minutes=60, ssh_password=None
|
2240
|
+
volume_name=None, timeout_minutes=60, ssh_password=None):
|
1187
2241
|
"""Create a Modal SSH container with GPU support and tunneling"""
|
1188
2242
|
|
1189
|
-
# Use interactive mode if specified
|
1190
|
-
if interactive:
|
1191
|
-
# If GPU type is not specified, prompt for it
|
1192
|
-
if not gpu_type:
|
1193
|
-
gpu_type = prompt_for_gpu()
|
1194
|
-
|
1195
|
-
# If repo URL is not specified, prompt for it
|
1196
|
-
if not repo_url:
|
1197
|
-
try:
|
1198
|
-
repo_url = input("? Enter GitHub repository URL: ").strip()
|
1199
|
-
if not repo_url:
|
1200
|
-
print("❌ Repository URL is required.")
|
1201
|
-
return None
|
1202
|
-
except KeyboardInterrupt:
|
1203
|
-
print("\n🛑 Setup cancelled.")
|
1204
|
-
return None
|
1205
|
-
|
1206
|
-
# If volume name is not specified, ask about persistent volume
|
1207
|
-
if not volume_name:
|
1208
|
-
try:
|
1209
|
-
use_volume = input("? Use persistent volume for faster installs? (Y/n): ").strip().lower()
|
1210
|
-
if use_volume in ('', 'y', 'yes'):
|
1211
|
-
volume_name = input("? Enter volume name: ").strip()
|
1212
|
-
if not volume_name:
|
1213
|
-
volume_name = "gitarsenal-volume"
|
1214
|
-
print(f"Using default volume name: {volume_name}")
|
1215
|
-
except KeyboardInterrupt:
|
1216
|
-
print("\n🛑 Setup cancelled.")
|
1217
|
-
return None
|
1218
|
-
|
1219
2243
|
# Check if Modal is authenticated
|
1220
2244
|
try:
|
1221
2245
|
# Print all environment variables for debugging
|
1222
2246
|
print("🔍 DEBUG: Checking environment variables")
|
1223
2247
|
modal_token_id = os.environ.get("MODAL_TOKEN_ID")
|
1224
2248
|
modal_token = os.environ.get("MODAL_TOKEN")
|
1225
|
-
|
1226
|
-
print(f"🔍
|
1227
|
-
print(f"🔍 token exists: {'Yes' if modal_token else 'No'}")
|
1228
|
-
print(f"🔍 openai_api_key exists: {'Yes' if openai_api_key else 'No'}")
|
2249
|
+
print(f"🔍 MODAL_TOKEN_ID exists: {'Yes' if modal_token_id else 'No'}")
|
2250
|
+
print(f"🔍 MODAL_TOKEN exists: {'Yes' if modal_token else 'No'}")
|
1229
2251
|
if modal_token_id:
|
1230
|
-
print(f"🔍
|
2252
|
+
print(f"🔍 MODAL_TOKEN_ID length: {len(modal_token_id)}")
|
1231
2253
|
if modal_token:
|
1232
|
-
print(f"🔍
|
1233
|
-
if openai_api_key:
|
1234
|
-
print(f"🔍 openai_api_key length: {len(openai_api_key)}")
|
2254
|
+
print(f"🔍 MODAL_TOKEN length: {len(modal_token)}")
|
1235
2255
|
|
1236
2256
|
# Try to access Modal token to check authentication
|
1237
2257
|
try:
|
@@ -1242,13 +2262,13 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
1242
2262
|
# Try to get from MODAL_TOKEN
|
1243
2263
|
modal_token = os.environ.get("MODAL_TOKEN")
|
1244
2264
|
if modal_token:
|
1245
|
-
print("✅ Found token in environment variable")
|
2265
|
+
print("✅ Found token in MODAL_TOKEN environment variable")
|
1246
2266
|
os.environ["MODAL_TOKEN_ID"] = modal_token
|
1247
2267
|
modal_token_id = modal_token
|
1248
|
-
print(f"✅ Set
|
2268
|
+
print(f"✅ Set MODAL_TOKEN_ID from MODAL_TOKEN (length: {len(modal_token)})")
|
1249
2269
|
|
1250
2270
|
if modal_token_id:
|
1251
|
-
print(f"✅ token found (length: {len(modal_token_id)})")
|
2271
|
+
print(f"✅ Modal token found (length: {len(modal_token_id)})")
|
1252
2272
|
|
1253
2273
|
# Use the comprehensive fix_modal_token script
|
1254
2274
|
try:
|
@@ -1269,11 +2289,11 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
1269
2289
|
if result.stderr:
|
1270
2290
|
print(f"Error: {result.stderr}")
|
1271
2291
|
|
1272
|
-
print(f"✅ token setup completed")
|
2292
|
+
print(f"✅ Modal token setup completed")
|
1273
2293
|
except Exception as e:
|
1274
2294
|
print(f"⚠️ Error running fix_modal_token.py: {e}")
|
1275
2295
|
else:
|
1276
|
-
print("❌ No token found in environment variables")
|
2296
|
+
print("❌ No Modal token found in environment variables")
|
1277
2297
|
# Try to get from file as a last resort
|
1278
2298
|
try:
|
1279
2299
|
home_dir = os.path.expanduser("~")
|
@@ -1292,7 +2312,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
1292
2312
|
else:
|
1293
2313
|
print("❌ Token file does not contain token_id")
|
1294
2314
|
else:
|
1295
|
-
print("❌ token file not found")
|
2315
|
+
print("❌ Modal token file not found")
|
1296
2316
|
except Exception as e:
|
1297
2317
|
print(f"❌ Error loading token from file: {e}")
|
1298
2318
|
|
@@ -1306,9 +2326,9 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
1306
2326
|
modal_token_id = os.environ.get("MODAL_TOKEN_ID")
|
1307
2327
|
modal_token = os.environ.get("MODAL_TOKEN")
|
1308
2328
|
if modal_token_id:
|
1309
|
-
print(f"🔄 Using
|
2329
|
+
print(f"🔄 Using MODAL_TOKEN_ID from environment (length: {len(modal_token_id)})")
|
1310
2330
|
elif modal_token:
|
1311
|
-
print(f"🔄 Using
|
2331
|
+
print(f"🔄 Using MODAL_TOKEN from environment (length: {len(modal_token)})")
|
1312
2332
|
os.environ["MODAL_TOKEN_ID"] = modal_token
|
1313
2333
|
modal_token_id = modal_token
|
1314
2334
|
else:
|
@@ -1318,7 +2338,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
1318
2338
|
# Set it in both environment variables
|
1319
2339
|
os.environ["MODAL_TOKEN_ID"] = modal_token_id
|
1320
2340
|
os.environ["MODAL_TOKEN"] = modal_token_id
|
1321
|
-
print("✅ Set both
|
2341
|
+
print("✅ Set both MODAL_TOKEN_ID and MODAL_TOKEN environment variables")
|
1322
2342
|
except Exception as e:
|
1323
2343
|
print(f"⚠️ Error checking Modal authentication: {e}")
|
1324
2344
|
print("Continuing anyway, but Modal operations may fail")
|
@@ -1344,7 +2364,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
1344
2364
|
gpu_type = 'A10G'
|
1345
2365
|
|
1346
2366
|
gpu_spec = gpu_configs[gpu_type]
|
1347
|
-
print(f"🚀 Creating SSH container with {gpu_spec['gpu']} GPU ({gpu_spec['memory']}GB VRAM)")
|
2367
|
+
print(f"🚀 Creating Modal SSH container with {gpu_spec['gpu']} GPU ({gpu_spec['memory']}GB VRAM)")
|
1348
2368
|
|
1349
2369
|
# Generate or use provided SSH password
|
1350
2370
|
if not ssh_password:
|
@@ -1380,16 +2400,16 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
1380
2400
|
# Print debug info for authentication
|
1381
2401
|
print("🔍 Modal authentication debug info:")
|
1382
2402
|
modal_token = os.environ.get("MODAL_TOKEN_ID")
|
1383
|
-
print(f" -
|
2403
|
+
print(f" - MODAL_TOKEN_ID in env: {'Yes' if modal_token else 'No'}")
|
1384
2404
|
print(f" - Token length: {len(modal_token) if modal_token else 'N/A'}")
|
1385
2405
|
|
1386
2406
|
# Verify we can create a Modal app
|
1387
2407
|
try:
|
1388
|
-
print("🔍 Testing app creation...")
|
2408
|
+
print("🔍 Testing Modal app creation...")
|
1389
2409
|
app = modal.App(app_name)
|
1390
|
-
print("✅ Created app successfully")
|
2410
|
+
print("✅ Created Modal app successfully")
|
1391
2411
|
except Exception as e:
|
1392
|
-
print(f"❌ Error creating app: {e}")
|
2412
|
+
print(f"❌ Error creating Modal app: {e}")
|
1393
2413
|
return None
|
1394
2414
|
|
1395
2415
|
# Create SSH-enabled image
|
@@ -1402,7 +2422,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
1402
2422
|
"python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
|
1403
2423
|
"gpg", "ca-certificates", "software-properties-common"
|
1404
2424
|
)
|
1405
|
-
.pip_install("uv", "modal"
|
2425
|
+
.pip_install("uv", "modal") # Fast Python package installer and Modal
|
1406
2426
|
.run_commands(
|
1407
2427
|
# Create SSH directory
|
1408
2428
|
"mkdir -p /var/run/sshd",
|
@@ -1445,7 +2465,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
1445
2465
|
serialized=True,
|
1446
2466
|
volumes=volumes_config if volumes_config else None,
|
1447
2467
|
)
|
1448
|
-
def ssh_container_function(
|
2468
|
+
def ssh_container_function():
|
1449
2469
|
"""Start SSH container with password authentication and optional setup."""
|
1450
2470
|
import subprocess
|
1451
2471
|
import time
|
@@ -1454,13 +2474,6 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
1454
2474
|
# Set root password
|
1455
2475
|
subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
|
1456
2476
|
|
1457
|
-
# Set OpenAI API key if provided
|
1458
|
-
if openai_api_key:
|
1459
|
-
os.environ['OPENAI_API_KEY'] = openai_api_key
|
1460
|
-
print(f"✅ Set OpenAI API key in container environment (length: {len(openai_api_key)})")
|
1461
|
-
else:
|
1462
|
-
print("⚠️ No OpenAI API key provided to container")
|
1463
|
-
|
1464
2477
|
# Start SSH service
|
1465
2478
|
subprocess.run(["service", "ssh", "start"], check=True)
|
1466
2479
|
|
@@ -1485,110 +2498,17 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
1485
2498
|
# Run setup commands if provided
|
1486
2499
|
if setup_commands:
|
1487
2500
|
print(f"⚙️ Running {len(setup_commands)} setup commands...")
|
1488
|
-
|
1489
|
-
|
1490
|
-
def run_command_with_basic_error_handling(cmd, show_output=True, retry_count=0, max_retries=2):
|
1491
|
-
"""Execute a command with LLM debugging enabled"""
|
1492
|
-
print(f"🔧 Executing: {cmd}")
|
2501
|
+
for i, cmd in enumerate(setup_commands, 1):
|
2502
|
+
print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
|
1493
2503
|
try:
|
1494
|
-
|
1495
|
-
|
1496
|
-
|
1497
|
-
print("🔄 Converting to bash -c with dot (.) instead of source")
|
1498
|
-
# Replace source with . (dot) which is the same as source but works in sh
|
1499
|
-
modified_cmd = cmd.replace("source ", ". ")
|
1500
|
-
# Wrap in bash -c to ensure it runs in bash
|
1501
|
-
bash_cmd = f"bash -c '{modified_cmd}'"
|
1502
|
-
print(f"🔄 Modified command: {bash_cmd}")
|
1503
|
-
result = subprocess.run(bash_cmd, shell=True, check=True,
|
1504
|
-
capture_output=True, text=True)
|
1505
|
-
else:
|
1506
|
-
result = subprocess.run(cmd, shell=True, check=True,
|
1507
|
-
capture_output=True, text=True)
|
1508
|
-
|
1509
|
-
if result.stdout and show_output:
|
2504
|
+
result = subprocess.run(cmd, shell=True, check=True,
|
2505
|
+
capture_output=True, text=True)
|
2506
|
+
if result.stdout:
|
1510
2507
|
print(f"✅ Output: {result.stdout}")
|
1511
|
-
return True, result.stdout, ""
|
1512
2508
|
except subprocess.CalledProcessError as e:
|
1513
|
-
error_output = e.stderr if e.stderr else str(e)
|
1514
2509
|
print(f"❌ Command failed: {e}")
|
1515
|
-
|
1516
|
-
|
1517
|
-
# Call OpenAI for debugging
|
1518
|
-
print("🔍 Attempting to debug the failed command with OpenAI...")
|
1519
|
-
try:
|
1520
|
-
# Get the current directory for context
|
1521
|
-
current_dir = os.getcwd()
|
1522
|
-
|
1523
|
-
# Call OpenAI for debugging
|
1524
|
-
print(f"🔍 DEBUG: About to call call_openai_for_debug...")
|
1525
|
-
print(f"🔍 DEBUG: Command: {cmd}")
|
1526
|
-
print(f"🔍 DEBUG: Error output length: {len(error_output)}")
|
1527
|
-
print(f"🔍 DEBUG: Current directory: {current_dir}")
|
1528
|
-
|
1529
|
-
# Get the API key from environment or use the one that was fetched earlier
|
1530
|
-
api_key = os.environ.get("OPENAI_API_KEY")
|
1531
|
-
fix_command = call_openai_for_debug(cmd, error_output, api_key=api_key, current_dir=current_dir)
|
1532
|
-
|
1533
|
-
print(f"🔍 DEBUG: call_openai_for_debug returned: {fix_command}")
|
1534
|
-
|
1535
|
-
if fix_command:
|
1536
|
-
print(f"🔧 OpenAI suggested fix command: {fix_command}")
|
1537
|
-
|
1538
|
-
# Run the fix command
|
1539
|
-
print(f"🔄 Running suggested fix command: {fix_command}")
|
1540
|
-
try:
|
1541
|
-
fix_result = subprocess.run(fix_command, shell=True, check=True,
|
1542
|
-
capture_output=True, text=True)
|
1543
|
-
if fix_result.stdout:
|
1544
|
-
print(f"✅ Fix command output: {fix_result.stdout}")
|
1545
|
-
|
1546
|
-
# Retry the original command
|
1547
|
-
print(f"🔄 Retrying original command: {cmd}")
|
1548
|
-
return run_command_with_basic_error_handling(cmd, show_output, retry_count + 1, max_retries)
|
1549
|
-
except subprocess.CalledProcessError as fix_e:
|
1550
|
-
print(f"❌ Fix command also failed: {fix_e}")
|
1551
|
-
return False, "", error_output
|
1552
|
-
else:
|
1553
|
-
print("❌ No fix suggested by OpenAI")
|
1554
|
-
return False, "", error_output
|
1555
|
-
|
1556
|
-
except Exception as debug_e:
|
1557
|
-
print(f"❌ LLM debugging failed: {debug_e}")
|
1558
|
-
return False, "", error_output
|
1559
|
-
|
1560
|
-
# Run each setup command
|
1561
|
-
for i, cmd in enumerate(setup_commands, 1):
|
1562
|
-
print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
|
1563
|
-
|
1564
|
-
# Check if this is a cd command and if the directory exists
|
1565
|
-
if cmd.strip().startswith("cd "):
|
1566
|
-
cd_parts = cmd.split(None, 1)
|
1567
|
-
if len(cd_parts) >= 2:
|
1568
|
-
target_dir = cd_parts[1].strip('"\'')
|
1569
|
-
print(f"🔍 Checking if directory exists: {target_dir}")
|
1570
|
-
try:
|
1571
|
-
check_result = subprocess.run(f"test -d '{target_dir}'", shell=True,
|
1572
|
-
capture_output=True, text=True)
|
1573
|
-
if check_result.returncode != 0:
|
1574
|
-
print(f"⚠️ Directory does not exist: {target_dir}")
|
1575
|
-
print(f"🔍 Current directory contents:")
|
1576
|
-
subprocess.run("pwd && ls -la", shell=True, check=False)
|
1577
|
-
|
1578
|
-
# Try to find similar directories
|
1579
|
-
print(f"🔍 Looking for similar directories...")
|
1580
|
-
subprocess.run("find . -type d -name '*llama*' -o -name '*nano*' 2>/dev/null | head -10", shell=True, check=False)
|
1581
|
-
except Exception as e:
|
1582
|
-
print(f"⚠️ Could not check directory: {e}")
|
1583
|
-
|
1584
|
-
success, stdout, stderr = run_command_with_basic_error_handling(cmd, show_output=True)
|
1585
|
-
if not success:
|
1586
|
-
print(f"⚠️ Command {i} failed, but continuing with remaining commands...")
|
1587
|
-
|
1588
|
-
# If this was a cd command that failed, try to understand the directory structure
|
1589
|
-
if cmd.strip().startswith("cd ") and "No such file or directory" in stderr:
|
1590
|
-
print(f"🔍 Analyzing directory structure after failed cd command...")
|
1591
|
-
subprocess.run("pwd && ls -la && echo '--- Parent directory ---' && ls -la ..", shell=True, check=False)
|
2510
|
+
if e.stderr:
|
2511
|
+
print(f"❌ Error: {e.stderr}")
|
1592
2512
|
|
1593
2513
|
# Create SSH tunnel
|
1594
2514
|
with modal.forward(22, unencrypted=True) as tunnel:
|
@@ -1624,9 +2544,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
1624
2544
|
# Start the container in a new thread to avoid blocking
|
1625
2545
|
with modal.enable_output():
|
1626
2546
|
with app.run():
|
1627
|
-
|
1628
|
-
api_key = os.environ.get("OPENAI_API_KEY")
|
1629
|
-
ssh_container_function.remote(ssh_password, repo_url, repo_name, setup_commands, api_key)
|
2547
|
+
ssh_container_function.remote()
|
1630
2548
|
|
1631
2549
|
# Clean up Modal token after container is successfully created
|
1632
2550
|
cleanup_modal_token()
|
@@ -1640,15 +2558,6 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
1640
2558
|
print(f"❌ Error running container: {e}")
|
1641
2559
|
return None
|
1642
2560
|
|
1643
|
-
def is_local_server_running(url, timeout=2):
|
1644
|
-
"""Check if a local server is running by attempting a connection."""
|
1645
|
-
import requests
|
1646
|
-
try:
|
1647
|
-
response = requests.get(url, timeout=timeout)
|
1648
|
-
return True
|
1649
|
-
except requests.exceptions.RequestException:
|
1650
|
-
return False
|
1651
|
-
|
1652
2561
|
def fetch_setup_commands_from_api(repo_url):
|
1653
2562
|
"""Fetch setup commands from the GitIngest API using real repository analysis."""
|
1654
2563
|
import tempfile
|
@@ -1656,13 +2565,8 @@ def fetch_setup_commands_from_api(repo_url):
|
|
1656
2565
|
import os
|
1657
2566
|
import shutil
|
1658
2567
|
import json
|
1659
|
-
import time
|
1660
|
-
import requests
|
1661
2568
|
|
1662
|
-
|
1663
|
-
api_endpoints = [
|
1664
|
-
"https://www.gitarsenal.dev/api/analyze-with-gitingest" # Working endpoint with www prefix
|
1665
|
-
]
|
2569
|
+
api_url = "https://git-arsenal.vercel.app/api/analyze-with-gitingest"
|
1666
2570
|
|
1667
2571
|
print(f"🔍 Fetching setup commands from API for repository: {repo_url}")
|
1668
2572
|
|
@@ -1684,13 +2588,6 @@ def fetch_setup_commands_from_api(repo_url):
|
|
1684
2588
|
temp_dir = tempfile.mkdtemp(prefix="repo_analysis_")
|
1685
2589
|
output_file = os.path.join(temp_dir, "digest.json")
|
1686
2590
|
|
1687
|
-
# Create a directory to save GitIngest results
|
1688
|
-
save_dir = os.path.join(os.path.expanduser("~"), "gitarsenal_results")
|
1689
|
-
os.makedirs(save_dir, exist_ok=True)
|
1690
|
-
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
1691
|
-
repo_name = repo_url.split("/")[-1].replace(".git", "")
|
1692
|
-
save_file = os.path.join(save_dir, f"gitingest_{repo_name}_{timestamp}.txt")
|
1693
|
-
|
1694
2591
|
try:
|
1695
2592
|
if has_gitingest_cli:
|
1696
2593
|
# Use gitingest CLI tool to analyze the repository directly from URL
|
@@ -1727,65 +2624,16 @@ def fetch_setup_commands_from_api(repo_url):
|
|
1727
2624
|
try:
|
1728
2625
|
with open(output_file, 'r', encoding='utf-8') as f:
|
1729
2626
|
content = f.read()
|
1730
|
-
|
1731
|
-
# Save the GitIngest output to the results directory
|
1732
|
-
with open(save_file, 'w', encoding='utf-8') as save_f:
|
1733
|
-
save_f.write(content)
|
1734
|
-
print(f"📁 GitIngest output saved to: {save_file}")
|
1735
|
-
|
1736
2627
|
try:
|
1737
2628
|
gitingest_data = json.loads(content)
|
1738
2629
|
print(f"✅ GitIngest data loaded as JSON from {output_file}")
|
1739
2630
|
except json.JSONDecodeError:
|
1740
2631
|
# If not JSON, convert the text output to a basic structure
|
1741
2632
|
print(f"⚠️ GitIngest output is not in JSON format, converting text to structure")
|
1742
|
-
|
1743
|
-
# Process the text to extract useful information
|
1744
|
-
import re
|
1745
|
-
|
1746
|
-
# Try to identify language
|
1747
|
-
language_match = re.search(r"(?i)language[s]?:?\s*(\w+)", content)
|
1748
|
-
detected_language = language_match.group(1) if language_match else "Unknown"
|
1749
|
-
|
1750
|
-
# Try to identify technologies with stronger evidence requirements
|
1751
|
-
tech_patterns = {
|
1752
|
-
"python": r"(?i)(python|\.py\b|pip\b|requirements\.txt|setup\.py)",
|
1753
|
-
"javascript": r"(?i)(javascript|\.js\b|node|npm|yarn|package\.json)",
|
1754
|
-
"typescript": r"(?i)(typescript|\.ts\b|tsc\b|tsconfig\.json)",
|
1755
|
-
"go": r"(?i)(\bgo\b|golang|\.go\b|go\.mod|go\.sum)",
|
1756
|
-
"rust": r"(?i)(rust|\.rs\b|cargo|Cargo\.toml)",
|
1757
|
-
"java": r"(?i)(java\b|\.java\b|maven|gradle|pom\.xml)",
|
1758
|
-
"c++": r"(?i)(c\+\+|\.cpp\b|\.hpp\b|cmake\b|CMakeLists\.txt)",
|
1759
|
-
"pytorch": r"(?i)(pytorch|torch\b|nn\.Module)",
|
1760
|
-
"tensorflow": r"(?i)(tensorflow|tf\.|keras\b)",
|
1761
|
-
}
|
1762
|
-
|
1763
|
-
# Count occurrences to filter out false positives
|
1764
|
-
tech_counts = {}
|
1765
|
-
for tech, pattern in tech_patterns.items():
|
1766
|
-
matches = re.findall(pattern, content)
|
1767
|
-
if matches:
|
1768
|
-
tech_counts[tech] = len(matches)
|
1769
|
-
|
1770
|
-
# Filter technologies based on threshold
|
1771
|
-
thresholds = {
|
1772
|
-
"javascript": 3, # Higher threshold for JavaScript
|
1773
|
-
"go": 3, # Higher threshold for Go
|
1774
|
-
"default": 2 # Default threshold
|
1775
|
-
}
|
1776
|
-
|
1777
|
-
detected_technologies = []
|
1778
|
-
for tech, count in tech_counts.items():
|
1779
|
-
threshold = thresholds.get(tech, thresholds["default"])
|
1780
|
-
if count >= threshold:
|
1781
|
-
detected_technologies.append(tech)
|
1782
|
-
print(f"📊 Detected {tech} with confidence score {count}")
|
1783
|
-
|
1784
|
-
# Create a structured representation
|
1785
2633
|
gitingest_data = {
|
1786
2634
|
"system_info": {
|
1787
|
-
"detected_language":
|
1788
|
-
"detected_technologies":
|
2635
|
+
"detected_language": "Unknown",
|
2636
|
+
"detected_technologies": [],
|
1789
2637
|
},
|
1790
2638
|
"repository_analysis": {
|
1791
2639
|
"summary": content[:5000], # First 5000 chars as summary
|
@@ -1793,12 +2641,6 @@ def fetch_setup_commands_from_api(repo_url):
|
|
1793
2641
|
},
|
1794
2642
|
"success": True
|
1795
2643
|
}
|
1796
|
-
|
1797
|
-
# Save the processed data
|
1798
|
-
processed_file = os.path.join(save_dir, f"gitingest_processed_{repo_name}_{timestamp}.json")
|
1799
|
-
with open(processed_file, 'w', encoding='utf-8') as proc_f:
|
1800
|
-
json.dump(gitingest_data, proc_f, indent=2)
|
1801
|
-
print(f"📁 Processed GitIngest data saved to: {processed_file}")
|
1802
2644
|
except FileNotFoundError:
|
1803
2645
|
print(f"⚠️ Output file not found at {output_file}")
|
1804
2646
|
gitingest_data = generate_basic_repo_analysis_from_url(repo_url)
|
@@ -1818,46 +2660,17 @@ def fetch_setup_commands_from_api(repo_url):
|
|
1818
2660
|
|
1819
2661
|
print(f"📤 API Request payload prepared (GitIngest data size: {len(json.dumps(gitingest_data))} bytes)")
|
1820
2662
|
|
1821
|
-
#
|
1822
|
-
|
1823
|
-
for api_url in api_endpoints:
|
1824
|
-
# Use the retry mechanism for more reliable requests
|
1825
|
-
response = make_api_request_with_retry(
|
1826
|
-
url=api_url,
|
1827
|
-
payload=payload,
|
1828
|
-
max_retries=2,
|
1829
|
-
timeout=180 # 3 minute timeout
|
1830
|
-
)
|
1831
|
-
|
1832
|
-
# If we got a response and it's successful, break out of the loop
|
1833
|
-
if response and response.status_code == 200:
|
1834
|
-
print(f"✅ Successful response from {api_url}")
|
1835
|
-
break
|
1836
|
-
|
1837
|
-
if response:
|
1838
|
-
print(f"⚠️ Endpoint {api_url} returned status code {response.status_code}, trying next endpoint...")
|
1839
|
-
else:
|
1840
|
-
print(f"⚠️ Failed to connect to {api_url}, trying next endpoint...")
|
1841
|
-
|
1842
|
-
# If we've tried all endpoints and still don't have a response, use fallback
|
1843
|
-
if response is None:
|
1844
|
-
print("❌ All API endpoints failed")
|
1845
|
-
return generate_fallback_commands(gitingest_data)
|
1846
|
-
|
1847
|
-
# Continue with the response we got from the successful endpoint
|
1848
|
-
if not response:
|
1849
|
-
print("❌ No valid response received from any endpoint")
|
1850
|
-
return generate_fallback_commands(gitingest_data)
|
1851
|
-
|
2663
|
+
# Make the API request
|
2664
|
+
print(f"🌐 Making POST request to: {api_url}")
|
1852
2665
|
try:
|
2666
|
+
response = requests.post(api_url, json=payload, timeout=60)
|
2667
|
+
|
1853
2668
|
print(f"📥 API Response status code: {response.status_code}")
|
1854
2669
|
|
1855
2670
|
if response.status_code == 200:
|
1856
2671
|
try:
|
1857
2672
|
data = response.json()
|
1858
2673
|
print(f"📄 API Response data received")
|
1859
|
-
print(f"📄 Response size: {len(response.text)} bytes")
|
1860
|
-
print(f"📄 Response URL: {response.url}")
|
1861
2674
|
|
1862
2675
|
# Extract setup commands from the response
|
1863
2676
|
if "setupInstructions" in data and "commands" in data["setupInstructions"]:
|
@@ -1909,13 +2722,16 @@ def fetch_setup_commands_from_api(repo_url):
|
|
1909
2722
|
return generate_fallback_commands(gitingest_data)
|
1910
2723
|
else:
|
1911
2724
|
print(f"❌ API request failed with status code: {response.status_code}")
|
1912
|
-
print(f"
|
1913
|
-
print(f"❌ Response headers: {dict(response.headers)}")
|
1914
|
-
print(f"❌ Error response: {response.text[:500]}...")
|
2725
|
+
print(f"Error response: {response.text[:500]}...")
|
1915
2726
|
# Return fallback commands
|
1916
2727
|
return generate_fallback_commands(gitingest_data)
|
1917
|
-
except
|
1918
|
-
print(
|
2728
|
+
except requests.exceptions.Timeout:
|
2729
|
+
print("❌ API request timed out after 60 seconds")
|
2730
|
+
print("⚠️ Using fallback commands instead")
|
2731
|
+
# Return fallback commands
|
2732
|
+
return generate_fallback_commands(gitingest_data)
|
2733
|
+
except requests.exceptions.ConnectionError:
|
2734
|
+
print(f"❌ Connection error: Could not connect to {api_url}")
|
1919
2735
|
print("⚠️ Using fallback commands instead")
|
1920
2736
|
# Return fallback commands
|
1921
2737
|
return generate_fallback_commands(gitingest_data)
|
@@ -2207,11 +3023,8 @@ def generate_basic_repo_analysis(repo_dir):
|
|
2207
3023
|
}
|
2208
3024
|
|
2209
3025
|
def get_setup_commands_from_local_api(repo_url, gitingest_data):
|
2210
|
-
"""Try to get setup commands from the API."""
|
2211
|
-
|
2212
|
-
api_endpoints = [
|
2213
|
-
"https://www.gitarsenal.dev/api/analyze-with-gitingest" # Working endpoint with www prefix
|
2214
|
-
]
|
3026
|
+
"""Try to get setup commands from the local API."""
|
3027
|
+
api_url = "http://localhost:3000/api/analyze-with-gitingest"
|
2215
3028
|
|
2216
3029
|
# Prepare the request payload
|
2217
3030
|
payload = {
|
@@ -2220,49 +3033,34 @@ def get_setup_commands_from_local_api(repo_url, gitingest_data):
|
|
2220
3033
|
"userRequest": "Setup and run the repository"
|
2221
3034
|
}
|
2222
3035
|
|
2223
|
-
|
2224
|
-
|
2225
|
-
|
2226
|
-
response =
|
2227
|
-
url=api_url,
|
2228
|
-
payload=payload,
|
2229
|
-
max_retries=2,
|
2230
|
-
timeout=180 # 3 minute timeout
|
2231
|
-
)
|
3036
|
+
try:
|
3037
|
+
# Make the API request
|
3038
|
+
print(f"🌐 Making POST request to local API: {api_url}")
|
3039
|
+
response = requests.post(api_url, json=payload, timeout=60)
|
2232
3040
|
|
2233
|
-
if response
|
2234
|
-
|
2235
|
-
|
2236
|
-
|
2237
|
-
print(f"
|
2238
|
-
|
2239
|
-
|
2240
|
-
|
2241
|
-
|
2242
|
-
|
2243
|
-
|
2244
|
-
|
2245
|
-
|
2246
|
-
|
2247
|
-
|
2248
|
-
|
2249
|
-
|
2250
|
-
|
2251
|
-
|
2252
|
-
|
2253
|
-
|
2254
|
-
|
2255
|
-
return fixed_commands
|
2256
|
-
else:
|
2257
|
-
print("⚠️ API response did not contain setupInstructions.commands field")
|
2258
|
-
except json.JSONDecodeError:
|
2259
|
-
print(f"❌ Failed to parse API response as JSON")
|
2260
|
-
elif response:
|
2261
|
-
print(f"❌ API request failed with status code: {response.status_code}")
|
2262
|
-
else:
|
2263
|
-
print(f"❌ Failed to connect to {api_url}")
|
3041
|
+
if response.status_code == 200:
|
3042
|
+
data = response.json()
|
3043
|
+
if "setupInstructions" in data and "commands" in data["setupInstructions"]:
|
3044
|
+
commands = data["setupInstructions"]["commands"]
|
3045
|
+
print(f"✅ Successfully fetched {len(commands)} setup commands from local API")
|
3046
|
+
|
3047
|
+
# Print the original commands
|
3048
|
+
print("📋 Original commands from local API:")
|
3049
|
+
for i, cmd in enumerate(commands, 1):
|
3050
|
+
print(f" {i}. {cmd}")
|
3051
|
+
|
3052
|
+
# Fix the commands
|
3053
|
+
fixed_commands = fix_setup_commands(commands)
|
3054
|
+
|
3055
|
+
# Print the fixed commands
|
3056
|
+
print("\n📋 Fixed commands:")
|
3057
|
+
for i, cmd in enumerate(fixed_commands, 1):
|
3058
|
+
print(f" {i}. {cmd}")
|
3059
|
+
|
3060
|
+
return fixed_commands
|
3061
|
+
except Exception as e:
|
3062
|
+
print(f"❌ Error connecting to local API: {e}")
|
2264
3063
|
|
2265
|
-
print("❌ All API endpoints failed")
|
2266
3064
|
return None
|
2267
3065
|
|
2268
3066
|
# Define a function to create and return a properly configured ssh container function
|
@@ -2273,13 +3071,13 @@ def create_ssh_container_function(gpu_type="a10g", timeout_minutes=60, volume=No
|
|
2273
3071
|
|
2274
3072
|
# Create SSH-enabled image
|
2275
3073
|
ssh_image = (
|
2276
|
-
modal.Image.
|
3074
|
+
modal.Image.debian_slim()
|
2277
3075
|
.apt_install(
|
2278
3076
|
"openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
|
2279
3077
|
"python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
|
2280
3078
|
"gpg", "ca-certificates", "software-properties-common"
|
2281
3079
|
)
|
2282
|
-
.pip_install("uv", "modal"
|
3080
|
+
.pip_install("uv", "modal") # Fast Python package installer and Modal
|
2283
3081
|
.run_commands(
|
2284
3082
|
# Create SSH directory
|
2285
3083
|
"mkdir -p /var/run/sshd",
|
@@ -2318,7 +3116,7 @@ def create_ssh_container_function(gpu_type="a10g", timeout_minutes=60, volume=No
|
|
2318
3116
|
serialized=True,
|
2319
3117
|
volumes=volumes if volumes else None,
|
2320
3118
|
)
|
2321
|
-
def ssh_container(ssh_password, repo_url=None, repo_name=None, setup_commands=None
|
3119
|
+
def ssh_container(ssh_password, repo_url=None, repo_name=None, setup_commands=None):
|
2322
3120
|
import subprocess
|
2323
3121
|
import time
|
2324
3122
|
import os
|
@@ -2326,13 +3124,6 @@ def create_ssh_container_function(gpu_type="a10g", timeout_minutes=60, volume=No
|
|
2326
3124
|
# Set root password
|
2327
3125
|
subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
|
2328
3126
|
|
2329
|
-
# Set OpenAI API key if provided
|
2330
|
-
if openai_api_key:
|
2331
|
-
os.environ['OPENAI_API_KEY'] = openai_api_key
|
2332
|
-
print(f"✅ Set OpenAI API key in container environment (length: {len(openai_api_key)})")
|
2333
|
-
else:
|
2334
|
-
print("⚠️ No OpenAI API key provided to container")
|
2335
|
-
|
2336
3127
|
# Start SSH service
|
2337
3128
|
subprocess.run(["service", "ssh", "start"], check=True)
|
2338
3129
|
|
@@ -2511,430 +3302,71 @@ Respond with only 'NAVIGATE' if navigation makes sense, or 'SKIP' if it's redund
|
|
2511
3302
|
return None
|
2512
3303
|
|
2513
3304
|
def cleanup_modal_token():
|
2514
|
-
"""Delete token files and environment variables after SSH container is started"""
|
2515
|
-
print("🧹 Cleaning up
|
3305
|
+
"""Delete Modal token files and environment variables after SSH container is started"""
|
3306
|
+
print("🧹 Cleaning up Modal token for security...")
|
2516
3307
|
|
2517
3308
|
try:
|
2518
3309
|
# Remove token from environment variables
|
2519
3310
|
if "MODAL_TOKEN_ID" in os.environ:
|
2520
3311
|
del os.environ["MODAL_TOKEN_ID"]
|
2521
|
-
|
3312
|
+
print("✅ Removed MODAL_TOKEN_ID from environment")
|
2522
3313
|
|
2523
3314
|
if "MODAL_TOKEN" in os.environ:
|
2524
3315
|
del os.environ["MODAL_TOKEN"]
|
2525
|
-
|
3316
|
+
print("✅ Removed MODAL_TOKEN from environment")
|
2526
3317
|
|
2527
3318
|
if "MODAL_TOKEN_SECRET" in os.environ:
|
2528
3319
|
del os.environ["MODAL_TOKEN_SECRET"]
|
2529
|
-
|
3320
|
+
print("✅ Removed MODAL_TOKEN_SECRET from environment")
|
2530
3321
|
|
2531
3322
|
# Delete ~/.modal.toml file
|
2532
3323
|
home_dir = os.path.expanduser("~")
|
2533
3324
|
modal_toml = os.path.join(home_dir, ".modal.toml")
|
2534
3325
|
if os.path.exists(modal_toml):
|
2535
3326
|
os.remove(modal_toml)
|
2536
|
-
|
3327
|
+
print(f"✅ Deleted Modal token file at {modal_toml}")
|
2537
3328
|
|
2538
|
-
|
3329
|
+
print("✅ Modal token cleanup completed successfully")
|
2539
3330
|
except Exception as e:
|
2540
|
-
print(f"❌ Error during token cleanup: {e}")
|
3331
|
+
print(f"❌ Error during Modal token cleanup: {e}")
|
2541
3332
|
|
2542
3333
|
def show_usage_examples():
|
2543
|
-
"""Display usage examples for the
|
2544
|
-
print("
|
2545
|
-
|
2546
|
-
print("
|
2547
|
-
print("
|
2548
|
-
print("
|
2549
|
-
print("└────────────────────────────────────────────────────────────────────────┘\
|
2550
|
-
|
2551
|
-
print("
|
2552
|
-
print("
|
2553
|
-
print("
|
2554
|
-
print("
|
2555
|
-
print("└────────────────────────────────────────────────────────────────────────────────────────────────────┘\
|
2556
|
-
|
2557
|
-
print("
|
2558
|
-
print("
|
2559
|
-
print("
|
2560
|
-
print("
|
2561
|
-
print("└────────────────────────────────────────────────────────────────────────────────────┘\
|
2562
|
-
|
2563
|
-
print("
|
2564
|
-
print("
|
2565
|
-
print("
|
2566
|
-
print("
|
2567
|
-
|
2568
|
-
print("
|
2569
|
-
print("┌────────────────────────────────────────────────────────────────────────────────────┐")
|
2570
|
-
print("│ gitarsenal --gpu A10G --repo-url https://github.com/username/repo.git \\ │")
|
2571
|
-
print("│ --no-gitingest │")
|
2572
|
-
print("└────────────────────────────────────────────────────────────────────────────────────┘\n")
|
2573
|
-
|
2574
|
-
print("With Original API")
|
2575
|
-
print("┌────────────────────────────────────────────────────────────────────────────────────┐")
|
2576
|
-
print("│ gitarsenal --gpu A10G --repo-url https://github.com/username/repo.git \\ │")
|
2577
|
-
print("│ --use-api │")
|
2578
|
-
print("└────────────────────────────────────────────────────────────────────────────────────┘\n")
|
2579
|
-
|
2580
|
-
print("Available GPU Options:")
|
3334
|
+
"""Display usage examples for the command-line interface."""
|
3335
|
+
print("\033[92mUsage Examples\033[0m")
|
3336
|
+
print("")
|
3337
|
+
print("\033[92mBasic Container Creation\033[0m")
|
3338
|
+
print("\033[90m┌────────────────────────────────────────────────────────────────────────┐\033[0m")
|
3339
|
+
print("\033[90m│\033[0m \033[92mgitarsenal --gpu A10G --repo-url https://github.com/username/repo.git\033[0m \033[90m│\033[0m")
|
3340
|
+
print("\033[90m└────────────────────────────────────────────────────────────────────────┘\033[0m")
|
3341
|
+
print("")
|
3342
|
+
print("\033[92mWith Setup Commands\033[0m")
|
3343
|
+
print("\033[90m┌────────────────────────────────────────────────────────────────────────────────────────────────────┐\033[0m")
|
3344
|
+
print("\033[90m│\033[0m \033[92mgitarsenal --gpu A100 --repo-url https://github.com/username/repo.git \\\033[0m \033[90m│\033[0m")
|
3345
|
+
print("\033[90m│\033[0m \033[92m --setup-commands \"pip install -r requirements.txt\" \"python setup.py install\"\033[0m \033[90m│\033[0m")
|
3346
|
+
print("\033[90m└────────────────────────────────────────────────────────────────────────────────────────────────────┘\033[0m")
|
3347
|
+
print("")
|
3348
|
+
print("\033[92mWith Persistent Storage\033[0m")
|
3349
|
+
print("\033[90m┌────────────────────────────────────────────────────────────────────────────────────┐\033[0m")
|
3350
|
+
print("\033[90m│\033[0m \033[92mgitarsenal --gpu A10G --repo-url https://github.com/username/repo.git \\\033[0m \033[90m│\033[0m")
|
3351
|
+
print("\033[90m│\033[0m \033[92m --volume-name my-persistent-volume\033[0m \033[90m│\033[0m")
|
3352
|
+
print("\033[90m└────────────────────────────────────────────────────────────────────────────────────┘\033[0m")
|
3353
|
+
print("")
|
3354
|
+
print("\033[92mInteractive Mode\033[0m")
|
3355
|
+
print("\033[90m┌────────────────────────────┐\033[0m")
|
3356
|
+
print("\033[90m│\033[0m \033[92mgitarsenal --interactive\033[0m \033[90m│\033[0m")
|
3357
|
+
print("\033[90m└────────────────────────────┘\033[0m")
|
3358
|
+
print("")
|
3359
|
+
print("\033[92mAvailable GPU Options:\033[0m")
|
2581
3360
|
print(" T4, L4, A10G, A100-40GB, A100-80GB, L40S, H100, H200, B200")
|
3361
|
+
print("")
|
2582
3362
|
|
2583
|
-
def make_api_request_with_retry(url, payload, max_retries=2, timeout=180):
|
2584
|
-
"""Make an API request with retry mechanism."""
|
2585
|
-
import requests
|
2586
|
-
import time
|
2587
|
-
|
2588
|
-
for attempt in range(max_retries + 1):
|
2589
|
-
try:
|
2590
|
-
if attempt > 0:
|
2591
|
-
print(f"🔄 Retry attempt {attempt}/{max_retries}...")
|
2592
|
-
|
2593
|
-
print(f"🌐 Making POST request to: {url}")
|
2594
|
-
print(f"⏳ Waiting up to {timeout//60} minutes for response...")
|
2595
|
-
|
2596
|
-
# Set allow_redirects=True to follow redirects automatically
|
2597
|
-
response = requests.post(
|
2598
|
-
url,
|
2599
|
-
json=payload,
|
2600
|
-
timeout=timeout,
|
2601
|
-
allow_redirects=True,
|
2602
|
-
headers={
|
2603
|
-
'Content-Type': 'application/json',
|
2604
|
-
'User-Agent': 'GitArsenal-CLI/1.0'
|
2605
|
-
}
|
2606
|
-
)
|
2607
|
-
|
2608
|
-
# Print redirect info if any
|
2609
|
-
if response.history:
|
2610
|
-
print(f"✅ Request was redirected {len(response.history)} times")
|
2611
|
-
for resp in response.history:
|
2612
|
-
print(f" - Redirect: {resp.status_code} from {resp.url}")
|
2613
|
-
print(f"✅ Final URL: {response.url}")
|
2614
|
-
|
2615
|
-
return response
|
2616
|
-
except requests.exceptions.RequestException as e:
|
2617
|
-
if attempt < max_retries:
|
2618
|
-
retry_delay = 2 ** attempt # Exponential backoff
|
2619
|
-
print(f"⚠️ Request failed: {str(e)}")
|
2620
|
-
print(f"⏳ Waiting {retry_delay} seconds before retrying...")
|
2621
|
-
time.sleep(retry_delay)
|
2622
|
-
else:
|
2623
|
-
print(f"❌ All retry attempts failed: {str(e)}")
|
2624
|
-
return None
|
2625
|
-
|
2626
|
-
return None
|
2627
|
-
|
2628
|
-
def get_setup_commands_from_gitingest(repo_url):
|
2629
|
-
"""
|
2630
|
-
Get repository setup commands using the gitingest approach.
|
2631
|
-
|
2632
|
-
This function is inspired by gitingest_setup_client.py and provides a more
|
2633
|
-
robust way to get setup commands for a repository.
|
2634
|
-
|
2635
|
-
Args:
|
2636
|
-
repo_url: URL of the repository to set up
|
2637
|
-
|
2638
|
-
Returns:
|
2639
|
-
List of setup commands or None if failed
|
2640
|
-
"""
|
2641
|
-
import requests
|
2642
|
-
import json
|
2643
|
-
import os
|
2644
|
-
import sys
|
2645
|
-
import tempfile
|
2646
|
-
import subprocess
|
2647
|
-
|
2648
|
-
print(f"🔍 Getting setup commands for repository: {repo_url}")
|
2649
|
-
|
2650
|
-
# Define API endpoints to try in order
|
2651
|
-
api_endpoints = [
|
2652
|
-
"https://www.gitarsenal.dev/api/gitingest-setup-commands",
|
2653
|
-
"https://gitarsenal.dev/api/gitingest-setup-commands",
|
2654
|
-
"https://www.gitarsenal.dev/api/analyze-with-gitingest",
|
2655
|
-
"http://localhost:3000/api/gitingest-setup-commands"
|
2656
|
-
]
|
2657
|
-
|
2658
|
-
# Generate basic gitingest data
|
2659
|
-
def generate_basic_gitingest_data():
|
2660
|
-
# Extract repo name from URL
|
2661
|
-
repo_name = repo_url.split('/')[-1].replace('.git', '')
|
2662
|
-
|
2663
|
-
return {
|
2664
|
-
"system_info": {
|
2665
|
-
"platform": "Unknown",
|
2666
|
-
"python_version": "Unknown",
|
2667
|
-
"detected_language": "Unknown",
|
2668
|
-
"detected_technologies": [],
|
2669
|
-
"file_count": 0,
|
2670
|
-
"repo_stars": 0,
|
2671
|
-
"repo_forks": 0,
|
2672
|
-
"primary_package_manager": "Unknown",
|
2673
|
-
"complexity_level": "Unknown"
|
2674
|
-
},
|
2675
|
-
"repository_analysis": {
|
2676
|
-
"summary": f"Repository: {repo_name}",
|
2677
|
-
"tree": "",
|
2678
|
-
"content_preview": ""
|
2679
|
-
},
|
2680
|
-
"success": True
|
2681
|
-
}
|
2682
|
-
|
2683
|
-
# Try to generate gitingest data using CLI if available
|
2684
|
-
def generate_gitingest_data_from_cli():
|
2685
|
-
try:
|
2686
|
-
# Check if gitingest CLI is available
|
2687
|
-
subprocess.run(["gitingest", "--help"], check=True, capture_output=True, text=True)
|
2688
|
-
|
2689
|
-
# Create a temporary file for the output
|
2690
|
-
with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as tmp:
|
2691
|
-
output_file = tmp.name
|
2692
|
-
|
2693
|
-
# Run gitingest command
|
2694
|
-
print(f"Running gitingest analysis on {repo_url}...")
|
2695
|
-
gitingest_cmd = ["gitingest", repo_url, "-o", output_file]
|
2696
|
-
result = subprocess.run(gitingest_cmd, capture_output=True, text=True)
|
2697
|
-
|
2698
|
-
if result.returncode != 0:
|
2699
|
-
print(f"GitIngest CLI failed: {result.stderr}")
|
2700
|
-
return None
|
2701
|
-
|
2702
|
-
# Read the output file
|
2703
|
-
try:
|
2704
|
-
with open(output_file, 'r', encoding='utf-8') as f:
|
2705
|
-
content = f.read()
|
2706
|
-
try:
|
2707
|
-
data = json.loads(content)
|
2708
|
-
return data
|
2709
|
-
except json.JSONDecodeError:
|
2710
|
-
# If not JSON, convert the text output to a basic structure
|
2711
|
-
return {
|
2712
|
-
"system_info": {
|
2713
|
-
"platform": "Unknown",
|
2714
|
-
"python_version": "Unknown",
|
2715
|
-
"detected_language": "Unknown",
|
2716
|
-
"detected_technologies": []
|
2717
|
-
},
|
2718
|
-
"repository_analysis": {
|
2719
|
-
"summary": content[:5000], # First 5000 chars as summary
|
2720
|
-
"tree": "",
|
2721
|
-
"content_preview": content[:10000] # First 10000 chars as preview
|
2722
|
-
},
|
2723
|
-
"success": True
|
2724
|
-
}
|
2725
|
-
except Exception as e:
|
2726
|
-
print(f"Error reading gitingest output: {e}")
|
2727
|
-
return None
|
2728
|
-
finally:
|
2729
|
-
# Clean up the temporary file
|
2730
|
-
if os.path.exists(output_file):
|
2731
|
-
os.unlink(output_file)
|
2732
|
-
|
2733
|
-
except (subprocess.SubprocessError, FileNotFoundError):
|
2734
|
-
print("GitIngest CLI not found")
|
2735
|
-
return None
|
2736
|
-
|
2737
|
-
# First try to get data from CLI
|
2738
|
-
gitingest_data = generate_gitingest_data_from_cli()
|
2739
|
-
|
2740
|
-
# If CLI failed, use basic data
|
2741
|
-
if not gitingest_data:
|
2742
|
-
print("Using basic gitingest data")
|
2743
|
-
gitingest_data = generate_basic_gitingest_data()
|
2744
|
-
|
2745
|
-
# Try each API endpoint
|
2746
|
-
for api_url in api_endpoints:
|
2747
|
-
try:
|
2748
|
-
print(f"Trying API endpoint: {api_url}")
|
2749
|
-
|
2750
|
-
payload = {
|
2751
|
-
"repoUrl": repo_url,
|
2752
|
-
"gitingestData": gitingest_data
|
2753
|
-
}
|
2754
|
-
|
2755
|
-
# Use the retry mechanism for more reliable requests
|
2756
|
-
response = make_api_request_with_retry(
|
2757
|
-
url=api_url,
|
2758
|
-
payload=payload,
|
2759
|
-
max_retries=2,
|
2760
|
-
timeout=180 # 3 minute timeout
|
2761
|
-
)
|
2762
|
-
|
2763
|
-
if not response:
|
2764
|
-
print(f"Failed to connect to {api_url}")
|
2765
|
-
continue
|
2766
|
-
|
2767
|
-
if response.status_code != 200:
|
2768
|
-
print(f"API request failed with status code: {response.status_code}")
|
2769
|
-
continue
|
2770
|
-
|
2771
|
-
try:
|
2772
|
-
result = response.json()
|
2773
|
-
|
2774
|
-
# Check if we have commands in the response
|
2775
|
-
commands = None
|
2776
|
-
|
2777
|
-
# Check for different response formats
|
2778
|
-
if "commands" in result:
|
2779
|
-
commands = result["commands"]
|
2780
|
-
elif "setupInstructions" in result and "commands" in result["setupInstructions"]:
|
2781
|
-
commands = result["setupInstructions"]["commands"]
|
2782
|
-
|
2783
|
-
if commands:
|
2784
|
-
print(f"✅ Successfully fetched {len(commands)} setup commands from API at {api_url}")
|
2785
|
-
|
2786
|
-
# Print the commands
|
2787
|
-
print("\n📋 Setup Commands:")
|
2788
|
-
for i, cmd in enumerate(commands, 1):
|
2789
|
-
print(f" {i}. {cmd}")
|
2790
|
-
|
2791
|
-
# Fix the commands
|
2792
|
-
fixed_commands = fix_setup_commands(commands)
|
2793
|
-
|
2794
|
-
# Print the fixed commands
|
2795
|
-
print("\n📋 Fixed commands:")
|
2796
|
-
for i, cmd in enumerate(fixed_commands, 1):
|
2797
|
-
print(f" {i}. {cmd}")
|
2798
|
-
|
2799
|
-
return fixed_commands
|
2800
|
-
else:
|
2801
|
-
print("No commands found in API response")
|
2802
|
-
except json.JSONDecodeError:
|
2803
|
-
print(f"Failed to parse API response as JSON")
|
2804
|
-
except Exception as e:
|
2805
|
-
print(f"Error with API endpoint {api_url}: {e}")
|
2806
|
-
|
2807
|
-
print("❌ All API endpoints failed")
|
2808
|
-
return generate_fallback_commands(gitingest_data)
|
2809
|
-
|
2810
|
-
def prompt_for_gpu():
|
2811
|
-
"""
|
2812
|
-
Prompt the user to select a GPU type from available options using arrow keys.
|
2813
|
-
Returns the selected GPU type.
|
2814
|
-
"""
|
2815
|
-
import sys
|
2816
|
-
import tty
|
2817
|
-
import termios
|
2818
|
-
|
2819
|
-
# Define available GPU types and their specifications
|
2820
|
-
gpu_specs = {
|
2821
|
-
'T4': {'gpu': 'T4', 'memory': '16GB'},
|
2822
|
-
'L4': {'gpu': 'L4', 'memory': '24GB'},
|
2823
|
-
'A10G': {'gpu': 'A10G', 'memory': '24GB'},
|
2824
|
-
'A100-40': {'gpu': 'A100-40GB', 'memory': '40GB'},
|
2825
|
-
'A100-80': {'gpu': 'A100-80GB', 'memory': '80GB'},
|
2826
|
-
'L40S': {'gpu': 'L40S', 'memory': '48GB'},
|
2827
|
-
'H100': {'gpu': 'H100', 'memory': '80GB'},
|
2828
|
-
'H200': {'gpu': 'H200', 'memory': '141GB'},
|
2829
|
-
'B200': {'gpu': 'B200', 'memory': '141GB'}
|
2830
|
-
}
|
2831
|
-
|
2832
|
-
# Create a list of options
|
2833
|
-
options = list(gpu_specs.keys())
|
2834
|
-
selected_index = 2 # Default to A10G (index 2)
|
2835
|
-
|
2836
|
-
def get_key():
|
2837
|
-
"""Get a single keypress from the user."""
|
2838
|
-
fd = sys.stdin.fileno()
|
2839
|
-
old_settings = termios.tcgetattr(fd)
|
2840
|
-
try:
|
2841
|
-
tty.setraw(sys.stdin.fileno())
|
2842
|
-
ch = sys.stdin.read(1)
|
2843
|
-
if ch == '\x1b': # Escape sequence
|
2844
|
-
ch2 = sys.stdin.read(1)
|
2845
|
-
if ch2 == '[':
|
2846
|
-
ch3 = sys.stdin.read(1)
|
2847
|
-
if ch3 == 'A':
|
2848
|
-
return 'UP'
|
2849
|
-
elif ch3 == 'B':
|
2850
|
-
return 'DOWN'
|
2851
|
-
elif ch == '\r' or ch == '\n':
|
2852
|
-
return 'ENTER'
|
2853
|
-
elif ch == '\x03': # Ctrl+C
|
2854
|
-
return 'CTRL_C'
|
2855
|
-
return ch
|
2856
|
-
finally:
|
2857
|
-
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
2858
|
-
|
2859
|
-
def display_menu():
|
2860
|
-
"""Display the GPU selection menu with current selection highlighted."""
|
2861
|
-
print("\n📊 Available GPU Options:")
|
2862
|
-
print("┌──────────────┬─────────┐")
|
2863
|
-
print("│ GPU Type │ VRAM │")
|
2864
|
-
print("├──────────────┼─────────┤")
|
2865
|
-
|
2866
|
-
for i, gpu_type in enumerate(options):
|
2867
|
-
specs = gpu_specs[gpu_type]
|
2868
|
-
# Calculate proper spacing for alignment
|
2869
|
-
number_part = f"{i+1}."
|
2870
|
-
if i == selected_index:
|
2871
|
-
prefix = "> "
|
2872
|
-
suffix = " ←"
|
2873
|
-
else:
|
2874
|
-
prefix = " "
|
2875
|
-
suffix = ""
|
2876
|
-
|
2877
|
-
# Ensure consistent width for GPU type column
|
2878
|
-
gpu_display = f"{prefix}{number_part} {gpu_type}"
|
2879
|
-
gpu_padded = f"{gpu_display:<12}" # Fixed width for GPU column
|
2880
|
-
|
2881
|
-
print(f"│ {gpu_padded} │ {specs['memory']:<7} │{suffix}")
|
2882
|
-
|
2883
|
-
print("└──────────────┴─────────┘")
|
2884
|
-
print("Use ↑/↓ arrows to select, Enter to confirm, Ctrl+C to cancel")
|
2885
|
-
|
2886
|
-
# Clear screen and show initial menu
|
2887
|
-
print("\033[2J\033[H", end="") # Clear screen and move cursor to top
|
2888
|
-
display_menu()
|
2889
|
-
|
2890
|
-
while True:
|
2891
|
-
try:
|
2892
|
-
key = get_key()
|
2893
|
-
|
2894
|
-
if key == 'UP':
|
2895
|
-
selected_index = (selected_index - 1) % len(options)
|
2896
|
-
print("\033[2J\033[H", end="") # Clear screen
|
2897
|
-
display_menu()
|
2898
|
-
elif key == 'DOWN':
|
2899
|
-
selected_index = (selected_index + 1) % len(options)
|
2900
|
-
print("\033[2J\033[H", end="") # Clear screen
|
2901
|
-
display_menu()
|
2902
|
-
elif key == 'ENTER':
|
2903
|
-
selected_gpu = options[selected_index]
|
2904
|
-
print(f"\n✅ Selected GPU: {selected_gpu}")
|
2905
|
-
return selected_gpu
|
2906
|
-
elif key == 'CTRL_C':
|
2907
|
-
print("\n🛑 Selection cancelled.")
|
2908
|
-
sys.exit(1)
|
2909
|
-
|
2910
|
-
except KeyboardInterrupt:
|
2911
|
-
print("\n🛑 Selection cancelled.")
|
2912
|
-
sys.exit(1)
|
2913
|
-
except Exception as e:
|
2914
|
-
print(f"\n❌ Error: {e}")
|
2915
|
-
# Fall back to simple input method
|
2916
|
-
try:
|
2917
|
-
choice = input("\n🔍 Select GPU type (number or name, default is A10G): ").strip()
|
2918
|
-
if not choice:
|
2919
|
-
return "A10G"
|
2920
|
-
if choice.isdigit():
|
2921
|
-
index = int(choice) - 1
|
2922
|
-
if 0 <= index < len(options):
|
2923
|
-
return options[index]
|
2924
|
-
elif choice in options:
|
2925
|
-
return choice
|
2926
|
-
return "A10G"
|
2927
|
-
except:
|
2928
|
-
return "A10G"
|
2929
|
-
|
2930
|
-
# Replace the existing GPU argument parsing in the main section
|
2931
3363
|
if __name__ == "__main__":
|
2932
3364
|
# Parse command line arguments when script is run directly
|
2933
3365
|
import argparse
|
2934
3366
|
import sys
|
2935
3367
|
|
2936
|
-
parser = argparse.ArgumentParser()
|
2937
|
-
parser.add_argument('--gpu', type=str, help='GPU type (
|
3368
|
+
parser = argparse.ArgumentParser(description='Create a Modal SSH container with GPU')
|
3369
|
+
parser.add_argument('--gpu', type=str, default='A10G', help='GPU type (default: A10G)')
|
2938
3370
|
parser.add_argument('--repo-url', type=str, help='Repository URL to clone')
|
2939
3371
|
parser.add_argument('--repo-name', type=str, help='Repository name override')
|
2940
3372
|
parser.add_argument('--setup-commands', type=str, nargs='+', help='Setup commands to run (deprecated)')
|
@@ -2945,257 +3377,169 @@ if __name__ == "__main__":
|
|
2945
3377
|
parser.add_argument('--volume-name', type=str, help='Name of the Modal volume for persistent storage')
|
2946
3378
|
parser.add_argument('--timeout', type=int, default=60, help='Container timeout in minutes (default: 60)')
|
2947
3379
|
parser.add_argument('--ssh-password', type=str, help='SSH password (random if not provided)')
|
2948
|
-
parser.add_argument('--use-api', action='store_true', help='Fetch setup commands from
|
2949
|
-
parser.add_argument('--use-gitingest', action='store_true', default=True, help='Use gitingest approach to fetch setup commands (default)')
|
2950
|
-
parser.add_argument('--no-gitingest', action='store_true', help='Disable gitingest approach for setup commands')
|
2951
|
-
parser.add_argument('--show-examples', action='store_true', help='Show usage examples')
|
2952
|
-
parser.add_argument('--list-gpus', action='store_true', help='List available GPU types with their specifications')
|
3380
|
+
parser.add_argument('--use-api', action='store_true', help='Fetch setup commands from API')
|
2953
3381
|
parser.add_argument('--interactive', action='store_true', help='Run in interactive mode with prompts')
|
3382
|
+
parser.add_argument('--show-examples', action='store_true', help='Show usage examples')
|
2954
3383
|
|
2955
3384
|
args = parser.parse_args()
|
2956
3385
|
|
2957
|
-
# If --list-gpus is specified, just show GPU options and exit
|
2958
|
-
if args.list_gpus:
|
2959
|
-
prompt_for_gpu()
|
2960
|
-
sys.exit(0)
|
2961
|
-
|
2962
3386
|
# If no arguments or only --show-examples is provided, show usage examples
|
2963
3387
|
if len(sys.argv) == 1 or args.show_examples:
|
2964
3388
|
show_usage_examples()
|
2965
3389
|
sys.exit(0)
|
3390
|
+
|
3391
|
+
# Get setup commands from file if specified
|
3392
|
+
setup_commands = args.setup_commands or []
|
2966
3393
|
|
2967
|
-
#
|
2968
|
-
|
2969
|
-
|
2970
|
-
|
2971
|
-
|
2972
|
-
|
2973
|
-
|
2974
|
-
|
2975
|
-
# Check Modal CLI
|
2976
|
-
try:
|
2977
|
-
subprocess.run(["modal", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
2978
|
-
print("✓ Modal CLI found")
|
2979
|
-
except (subprocess.SubprocessError, FileNotFoundError):
|
2980
|
-
print("❌ Modal CLI not found. Please install with: pip install modal")
|
2981
|
-
|
2982
|
-
# Check Gitingest CLI
|
2983
|
-
try:
|
2984
|
-
subprocess.run(["gitingest", "--help"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
2985
|
-
print("✓ Gitingest CLI found")
|
2986
|
-
except (subprocess.SubprocessError, FileNotFoundError):
|
2987
|
-
print("⚠️ Gitingest CLI not found (optional)")
|
2988
|
-
|
2989
|
-
# Check Git
|
2990
|
-
try:
|
2991
|
-
subprocess.run(["git", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
2992
|
-
print("✓ Git found")
|
2993
|
-
except (subprocess.SubprocessError, FileNotFoundError):
|
2994
|
-
print("❌ Git not found. Please install Git.")
|
2995
|
-
|
2996
|
-
print("------------------------")
|
2997
|
-
print("\n✔ Dependencies checked")
|
2998
|
-
|
2999
|
-
# Always prompt for GPU selection regardless of interactive mode
|
3000
|
-
gpu_type = prompt_for_gpu()
|
3001
|
-
args.gpu = gpu_type
|
3002
|
-
|
3003
|
-
# Display configuration after GPU selection
|
3004
|
-
print("\n📋 Container Configuration:")
|
3005
|
-
print(f"Repository URL: {args.repo_url or 'Not specified'}")
|
3006
|
-
print(f"GPU Type: {gpu_type}")
|
3007
|
-
print(f"Volume: {args.volume_name or 'None'}")
|
3008
|
-
if args.use_api:
|
3009
|
-
print("Setup Commands: Auto-detect from repository")
|
3010
|
-
elif args.setup_commands:
|
3011
|
-
print(f"Setup Commands: {len(args.setup_commands)} custom commands")
|
3012
|
-
else:
|
3013
|
-
print("Setup Commands: Auto-detect from repository")
|
3014
|
-
|
3015
|
-
# Confirm settings
|
3016
|
-
try:
|
3017
|
-
proceed = input("Proceed with these settings? (Y/n): ").strip().lower()
|
3018
|
-
if proceed in ('n', 'no'):
|
3019
|
-
print("🛑 Operation cancelled by user.")
|
3020
|
-
sys.exit(0)
|
3021
|
-
except KeyboardInterrupt:
|
3022
|
-
print("\n🛑 Operation cancelled by user.")
|
3023
|
-
sys.exit(0)
|
3024
|
-
|
3025
|
-
# Interactive mode or missing required arguments
|
3026
|
-
if args.interactive or not args.repo_url or not args.volume_name:
|
3027
|
-
# Get repository URL if not provided
|
3028
|
-
repo_url = args.repo_url
|
3029
|
-
if not repo_url:
|
3030
|
-
try:
|
3031
|
-
repo_url = input("? Enter GitHub repository URL: ").strip()
|
3032
|
-
if not repo_url:
|
3033
|
-
print("❌ Repository URL is required.")
|
3034
|
-
sys.exit(1)
|
3035
|
-
except KeyboardInterrupt:
|
3036
|
-
print("\n🛑 Setup cancelled.")
|
3394
|
+
# If interactive mode is enabled, prompt for options
|
3395
|
+
if args.interactive:
|
3396
|
+
# If repo URL wasn't provided via command line, ask for it
|
3397
|
+
if not args.repo_url:
|
3398
|
+
args.repo_url = input("✔ Dependencies checked\n? Enter GitHub repository URL: ").strip()
|
3399
|
+
if not args.repo_url:
|
3400
|
+
print("❌ No repository URL provided. Exiting.")
|
3037
3401
|
sys.exit(1)
|
3038
3402
|
|
3039
3403
|
# Ask about persistent volume
|
3040
|
-
|
3041
|
-
if not
|
3042
|
-
|
3043
|
-
|
3044
|
-
if
|
3045
|
-
volume_name =
|
3046
|
-
|
3047
|
-
|
3048
|
-
print(f"Using default volume name: {volume_name}")
|
3049
|
-
except KeyboardInterrupt:
|
3050
|
-
print("\n🛑 Setup cancelled.")
|
3051
|
-
sys.exit(1)
|
3404
|
+
use_volume = input("? Use persistent volume for faster installs? (Yes/No) [Yes]: ").strip().lower()
|
3405
|
+
if not use_volume or use_volume.startswith('y'):
|
3406
|
+
if not args.volume_name:
|
3407
|
+
args.volume_name = input("? Enter volume name [gitarsenal-volume]: ").strip()
|
3408
|
+
if not args.volume_name:
|
3409
|
+
args.volume_name = "gitarsenal-volume"
|
3410
|
+
else:
|
3411
|
+
args.volume_name = None
|
3052
3412
|
|
3053
|
-
# Ask about setup commands
|
3054
|
-
|
3055
|
-
if not
|
3056
|
-
|
3057
|
-
|
3058
|
-
|
3059
|
-
use_gitingest = False
|
3060
|
-
except KeyboardInterrupt:
|
3061
|
-
print("\n🛑 Setup cancelled.")
|
3062
|
-
sys.exit(1)
|
3413
|
+
# Ask about auto-detecting setup commands
|
3414
|
+
use_api = input("? Automatically detect setup commands for this repository? (Yes/No) [Yes]: ").strip().lower()
|
3415
|
+
if not use_api or use_api.startswith('y'):
|
3416
|
+
args.use_api = True
|
3417
|
+
else:
|
3418
|
+
args.use_api = False
|
3063
3419
|
|
3064
|
-
#
|
3065
|
-
|
3066
|
-
|
3067
|
-
|
3068
|
-
|
3069
|
-
try:
|
3070
|
-
# Get setup commands from file if specified
|
3071
|
-
setup_commands = args.setup_commands or []
|
3072
|
-
|
3073
|
-
# Use gitingest by default unless --no-gitingest is set
|
3074
|
-
if args.repo_url and (args.use_gitingest and not args.no_gitingest):
|
3075
|
-
print("🔄 Using gitingest approach to fetch setup commands (default)")
|
3076
|
-
api_commands = get_setup_commands_from_gitingest(args.repo_url)
|
3077
|
-
if api_commands:
|
3078
|
-
setup_commands = api_commands
|
3079
|
-
print(f"📋 Using {len(setup_commands)} commands from gitingest API")
|
3080
|
-
else:
|
3081
|
-
print("⚠️ Failed to get commands from gitingest API")
|
3082
|
-
if not args.use_api:
|
3083
|
-
print("⚠️ Falling back to basic setup commands")
|
3084
|
-
setup_commands = generate_fallback_commands(None)
|
3085
|
-
else:
|
3086
|
-
setup_commands = []
|
3087
|
-
# If --use-api flag is set and repo_url is provided, fetch setup commands from API
|
3088
|
-
elif args.use_api and args.repo_url:
|
3089
|
-
print("🔄 Using original API to fetch setup commands")
|
3090
|
-
api_commands = fetch_setup_commands_from_api(args.repo_url)
|
3091
|
-
if api_commands:
|
3092
|
-
setup_commands = api_commands
|
3093
|
-
print(f"📋 Using {len(setup_commands)} commands from original API")
|
3094
|
-
else:
|
3095
|
-
print("⚠️ Failed to get commands from API, no fallback commands will be used")
|
3096
|
-
# Do not fall back to basic setup commands
|
3097
|
-
setup_commands = []
|
3420
|
+
# GPU selection
|
3421
|
+
gpu_options = ['T4', 'L4', 'A10G', 'A100-40GB', 'A100-80GB', 'L40S', 'H100', 'H200', 'B200']
|
3422
|
+
print("\n? Select GPU type:")
|
3423
|
+
for i, gpu in enumerate(gpu_options, 1):
|
3424
|
+
print(f" {i}. {gpu}")
|
3098
3425
|
|
3099
|
-
|
3100
|
-
if
|
3426
|
+
gpu_choice = input(f"Enter choice (1-{len(gpu_options)}) [default: 3 for A10G]: ").strip()
|
3427
|
+
if not gpu_choice:
|
3428
|
+
args.gpu = 'A10G' # Default
|
3429
|
+
else:
|
3101
3430
|
try:
|
3102
|
-
|
3103
|
-
if
|
3104
|
-
|
3105
|
-
print(f"📋 Parsed {len(setup_commands)} commands from JSON:")
|
3106
|
-
for i, cmd in enumerate(setup_commands, 1):
|
3107
|
-
print(f" {i}. {cmd}")
|
3431
|
+
gpu_index = int(gpu_choice) - 1
|
3432
|
+
if 0 <= gpu_index < len(gpu_options):
|
3433
|
+
args.gpu = gpu_options[gpu_index]
|
3108
3434
|
else:
|
3109
|
-
print(
|
3110
|
-
|
3111
|
-
|
3112
|
-
print(
|
3113
|
-
|
3114
|
-
|
3115
|
-
|
3116
|
-
|
3117
|
-
|
3118
|
-
|
3435
|
+
print("⚠️ Invalid choice. Using default: A10G")
|
3436
|
+
args.gpu = 'A10G'
|
3437
|
+
except ValueError:
|
3438
|
+
print("⚠️ Invalid input. Using default: A10G")
|
3439
|
+
args.gpu = 'A10G'
|
3440
|
+
|
3441
|
+
# Show configuration summary
|
3442
|
+
print("\n📋 Container Configuration:")
|
3443
|
+
print(f"Repository URL: {args.repo_url}")
|
3444
|
+
print(f"GPU Type: {args.gpu}")
|
3445
|
+
print(f"Volume: {args.volume_name if args.volume_name else 'None'}")
|
3446
|
+
print(f"Setup Commands: {'Auto-detect from repository' if args.use_api else 'None'}")
|
3447
|
+
|
3448
|
+
# Confirm settings
|
3449
|
+
confirm = input("? Proceed with these settings? (Yes/No) [Yes]: ").strip().lower()
|
3450
|
+
if confirm and not confirm.startswith('y'):
|
3451
|
+
print("❌ Setup cancelled by user.")
|
3452
|
+
sys.exit(0)
|
3453
|
+
|
3454
|
+
# If --use-api flag is set and repo_url is provided, fetch setup commands from API
|
3455
|
+
if args.use_api and args.repo_url:
|
3456
|
+
print("🔄 Using API to fetch setup commands")
|
3457
|
+
api_commands = fetch_setup_commands_from_api(args.repo_url)
|
3458
|
+
if api_commands:
|
3459
|
+
setup_commands = api_commands
|
3460
|
+
print(f"📋 Using {len(setup_commands)} commands from API")
|
3461
|
+
else:
|
3462
|
+
print("⚠️ Failed to get commands from API, no fallback commands will be used")
|
3463
|
+
# Do not fall back to basic setup commands
|
3464
|
+
setup_commands = []
|
3465
|
+
|
3466
|
+
# Parse setup commands from JSON if provided
|
3467
|
+
if args.setup_commands_json:
|
3468
|
+
try:
|
3469
|
+
json_commands = json.loads(args.setup_commands_json)
|
3470
|
+
if isinstance(json_commands, list):
|
3471
|
+
setup_commands = json_commands
|
3472
|
+
print(f"📋 Parsed {len(setup_commands)} commands from JSON:")
|
3473
|
+
for i, cmd in enumerate(setup_commands, 1):
|
3474
|
+
print(f" {i}. {cmd}")
|
3475
|
+
else:
|
3476
|
+
print(f"⚠️ Invalid JSON format for setup commands: not a list")
|
3477
|
+
except json.JSONDecodeError as e:
|
3478
|
+
print(f"⚠️ Error parsing JSON setup commands: {e}")
|
3479
|
+
print(f"Received JSON string: {args.setup_commands_json}")
|
3480
|
+
|
3481
|
+
# Print received setup commands for debugging
|
3482
|
+
if setup_commands:
|
3483
|
+
print(f"📋 Using {len(setup_commands)} setup commands:")
|
3484
|
+
for i, cmd in enumerate(setup_commands, 1):
|
3485
|
+
print(f" {i}. {cmd}")
|
3486
|
+
|
3487
|
+
# Load commands from file if specified
|
3488
|
+
if args.commands_file and os.path.exists(args.commands_file):
|
3489
|
+
try:
|
3490
|
+
with open(args.commands_file, 'r') as f:
|
3491
|
+
# Check if the file contains JSON or line-by-line commands
|
3492
|
+
content = f.read().strip()
|
3119
3493
|
|
3120
|
-
|
3121
|
-
|
3122
|
-
|
3123
|
-
|
3124
|
-
|
3125
|
-
|
3126
|
-
|
3127
|
-
|
3128
|
-
|
3129
|
-
|
3130
|
-
|
3131
|
-
|
3132
|
-
setup_commands.extend(json_commands)
|
3133
|
-
print(f"📋 Loaded {len(json_commands)} commands from JSON file {args.commands_file}")
|
3134
|
-
else:
|
3135
|
-
print(f"⚠️ Invalid JSON format in commands file: not a list")
|
3136
|
-
except json.JSONDecodeError as json_err:
|
3137
|
-
print(f"⚠️ Error parsing JSON commands file: {json_err}")
|
3138
|
-
# Fall back to line-by-line parsing
|
3139
|
-
file_commands = [line.strip() for line in content.split('\n') if line.strip()]
|
3140
|
-
setup_commands.extend(file_commands)
|
3141
|
-
print(f"📋 Loaded {len(file_commands)} commands from file (line-by-line fallback)")
|
3142
|
-
else:
|
3143
|
-
# Line-by-line format
|
3494
|
+
if content.startswith('[') and content.endswith(']'):
|
3495
|
+
# JSON format
|
3496
|
+
try:
|
3497
|
+
json_commands = json.loads(content)
|
3498
|
+
if isinstance(json_commands, list):
|
3499
|
+
setup_commands.extend(json_commands)
|
3500
|
+
print(f"📋 Loaded {len(json_commands)} commands from JSON file {args.commands_file}")
|
3501
|
+
else:
|
3502
|
+
print(f"⚠️ Invalid JSON format in commands file: not a list")
|
3503
|
+
except json.JSONDecodeError as json_err:
|
3504
|
+
print(f"⚠️ Error parsing JSON commands file: {json_err}")
|
3505
|
+
# Fall back to line-by-line parsing
|
3144
3506
|
file_commands = [line.strip() for line in content.split('\n') if line.strip()]
|
3145
3507
|
setup_commands.extend(file_commands)
|
3146
|
-
print(f"📋 Loaded {len(file_commands)} commands from file (line-by-line
|
3147
|
-
|
3148
|
-
|
3149
|
-
|
3150
|
-
|
3151
|
-
|
3152
|
-
|
3153
|
-
|
3154
|
-
|
3155
|
-
|
3156
|
-
|
3157
|
-
|
3158
|
-
|
3159
|
-
|
3160
|
-
|
3161
|
-
|
3162
|
-
|
3163
|
-
|
3164
|
-
|
3165
|
-
print(f"🔑 Using provided SSH password")
|
3166
|
-
ssh_password = args.ssh_password
|
3167
|
-
else:
|
3168
|
-
ssh_password = generate_random_password()
|
3169
|
-
print(f"🔑 Generated random SSH password: {ssh_password}")
|
3170
|
-
|
3171
|
-
# Extract repository name from URL if not provided
|
3172
|
-
repo_name = args.repo_name
|
3173
|
-
if not repo_name and args.repo_url:
|
3174
|
-
# Try to extract repo name from URL
|
3175
|
-
url_parts = args.repo_url.rstrip('/').split('/')
|
3176
|
-
if url_parts:
|
3177
|
-
repo_name = url_parts[-1]
|
3178
|
-
if repo_name.endswith('.git'):
|
3179
|
-
repo_name = repo_name[:-4]
|
3180
|
-
|
3181
|
-
# Create the container
|
3182
|
-
create_modal_ssh_container(
|
3183
|
-
gpu_type=args.gpu,
|
3184
|
-
repo_url=args.repo_url,
|
3185
|
-
repo_name=repo_name,
|
3186
|
-
setup_commands=setup_commands,
|
3187
|
-
volume_name=args.volume_name,
|
3188
|
-
timeout_minutes=args.timeout,
|
3189
|
-
ssh_password=ssh_password,
|
3190
|
-
interactive=args.interactive
|
3508
|
+
print(f"📋 Loaded {len(file_commands)} commands from file (line-by-line fallback)")
|
3509
|
+
else:
|
3510
|
+
# Line-by-line format
|
3511
|
+
file_commands = [line.strip() for line in content.split('\n') if line.strip()]
|
3512
|
+
setup_commands.extend(file_commands)
|
3513
|
+
print(f"📋 Loaded {len(file_commands)} commands from file (line-by-line format)")
|
3514
|
+
|
3515
|
+
except Exception as e:
|
3516
|
+
print(f"⚠️ Error reading commands file: {e}")
|
3517
|
+
|
3518
|
+
try:
|
3519
|
+
result = create_modal_ssh_container(
|
3520
|
+
args.gpu,
|
3521
|
+
args.repo_url,
|
3522
|
+
args.repo_name,
|
3523
|
+
setup_commands,
|
3524
|
+
getattr(args, 'volume_name', None),
|
3525
|
+
args.timeout,
|
3526
|
+
args.ssh_password
|
3191
3527
|
)
|
3528
|
+
|
3529
|
+
print("\n⏳ Keeping the SSH container alive. Press Ctrl+C to exit (container will continue running)...")
|
3530
|
+
try:
|
3531
|
+
while True:
|
3532
|
+
time.sleep(90)
|
3533
|
+
print(".", end="", flush=True)
|
3534
|
+
except KeyboardInterrupt:
|
3535
|
+
print("\n👋 Script exited. The SSH container will continue running.")
|
3536
|
+
sys.exit(0)
|
3537
|
+
|
3192
3538
|
except KeyboardInterrupt:
|
3193
|
-
#
|
3194
|
-
|
3195
|
-
|
3196
|
-
sys.exit(
|
3539
|
+
# Handle Ctrl+C during container creation
|
3540
|
+
print("\n👋 Script interrupted during container creation.")
|
3541
|
+
print("📝 You may need to check if a container was created with: modal container list")
|
3542
|
+
sys.exit(0)
|
3197
3543
|
except Exception as e:
|
3198
|
-
|
3199
|
-
# print("🧹 Cleaning up resources...")
|
3200
|
-
cleanup_modal_token()
|
3544
|
+
print(f"❌ Error: {e}")
|
3201
3545
|
sys.exit(1)
|