gitarsenal-cli 1.3.1 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/gitarsenal.js +22 -2
- package/lib/sandbox.js +48 -4
- package/package.json +1 -1
- package/test_modalSandboxScript.py +1219 -416
@@ -1,6 +1,5 @@
|
|
1
1
|
import os
|
2
2
|
import sys
|
3
|
-
import modal
|
4
3
|
import time
|
5
4
|
import subprocess
|
6
5
|
import json
|
@@ -10,6 +9,126 @@ import getpass
|
|
10
9
|
import requests
|
11
10
|
import secrets
|
12
11
|
import string
|
12
|
+
import argparse
|
13
|
+
from pathlib import Path
|
14
|
+
|
15
|
+
# Parse command-line arguments
|
16
|
+
parser = argparse.ArgumentParser(description='Launch a Modal sandbox')
|
17
|
+
parser.add_argument('--proxy-url', help='URL of the proxy server')
|
18
|
+
parser.add_argument('--proxy-api-key', help='API key for the proxy server')
|
19
|
+
parser.add_argument('--gpu', default='A10G', help='GPU type to use')
|
20
|
+
parser.add_argument('--repo-url', help='Repository URL')
|
21
|
+
parser.add_argument('--volume-name', help='Volume name')
|
22
|
+
parser.add_argument('--use-api', action='store_true', help='Use API to fetch setup commands')
|
23
|
+
|
24
|
+
# Parse only known args to avoid conflicts with other arguments
|
25
|
+
args, unknown = parser.parse_known_args()
|
26
|
+
|
27
|
+
# Set proxy URL and API key in environment variables if provided
|
28
|
+
if args.proxy_url:
|
29
|
+
os.environ["MODAL_PROXY_URL"] = args.proxy_url
|
30
|
+
print(f"✅ Set MODAL_PROXY_URL from command line")
|
31
|
+
|
32
|
+
if args.proxy_api_key:
|
33
|
+
os.environ["MODAL_PROXY_API_KEY"] = args.proxy_api_key
|
34
|
+
print(f"✅ Set MODAL_PROXY_API_KEY from command line")
|
35
|
+
|
36
|
+
# First, try to fetch tokens from the proxy server
|
37
|
+
try:
|
38
|
+
# Import the fetch_modal_tokens module
|
39
|
+
print("🔄 Fetching Modal tokens from proxy server...")
|
40
|
+
from fetch_modal_tokens import get_tokens
|
41
|
+
token_id, token_secret = get_tokens()
|
42
|
+
print(f"✅ Modal tokens fetched successfully")
|
43
|
+
|
44
|
+
# Explicitly set the environment variables again to be sure
|
45
|
+
os.environ["MODAL_TOKEN_ID"] = token_id
|
46
|
+
os.environ["MODAL_TOKEN_SECRET"] = token_secret
|
47
|
+
|
48
|
+
# Also set the old environment variable for backward compatibility
|
49
|
+
os.environ["MODAL_TOKEN"] = token_id
|
50
|
+
|
51
|
+
# Set token variables for later use
|
52
|
+
token = token_id # For backward compatibility
|
53
|
+
except Exception as e:
|
54
|
+
print(f"⚠️ Error fetching Modal tokens: {e}")
|
55
|
+
|
56
|
+
# Apply the comprehensive Modal token solution as fallback
|
57
|
+
try:
|
58
|
+
# Import the comprehensive solution module
|
59
|
+
print("🔄 Applying comprehensive Modal token solution...")
|
60
|
+
import modal_token_solution
|
61
|
+
print("✅ Comprehensive Modal token solution applied")
|
62
|
+
|
63
|
+
# Set token variables for later use
|
64
|
+
token = modal_token_solution.TOKEN_ID # For backward compatibility
|
65
|
+
except Exception as e:
|
66
|
+
print(f"⚠️ Error applying comprehensive Modal token solution: {e}")
|
67
|
+
|
68
|
+
# Fall back to the authentication patch
|
69
|
+
try:
|
70
|
+
# Import the patch module
|
71
|
+
print("🔄 Falling back to Modal authentication patch...")
|
72
|
+
import modal_auth_patch
|
73
|
+
print("✅ Modal authentication patch applied")
|
74
|
+
|
75
|
+
# Set token variables for later use
|
76
|
+
token = modal_auth_patch.TOKEN_ID # For backward compatibility
|
77
|
+
except Exception as e:
|
78
|
+
print(f"⚠️ Error applying Modal authentication patch: {e}")
|
79
|
+
|
80
|
+
# Fall back to fix_modal_token.py
|
81
|
+
try:
|
82
|
+
# Execute the fix_modal_token.py script
|
83
|
+
print("🔄 Falling back to fix_modal_token.py...")
|
84
|
+
result = subprocess.run(
|
85
|
+
["python", os.path.join(os.path.dirname(__file__), "fix_modal_token.py")],
|
86
|
+
capture_output=True,
|
87
|
+
text=True
|
88
|
+
)
|
89
|
+
|
90
|
+
# Print the output but hide sensitive information
|
91
|
+
output_lines = result.stdout.split('\n')
|
92
|
+
for line in output_lines:
|
93
|
+
if 'TOKEN_ID' in line or 'TOKEN_SECRET' in line or 'token_id' in line or 'token_secret' in line:
|
94
|
+
# Hide the actual token values
|
95
|
+
if '=' in line:
|
96
|
+
parts = line.split('=', 1)
|
97
|
+
if len(parts) == 2:
|
98
|
+
print(f"{parts[0]}= [HIDDEN]")
|
99
|
+
else:
|
100
|
+
print(line.replace('ak-sLhYqCjkvixiYcb9LAuCHp', '[HIDDEN]').replace('as-fPzD0Zm0dl6IFAEkhaH9pq', '[HIDDEN]'))
|
101
|
+
else:
|
102
|
+
print(line)
|
103
|
+
|
104
|
+
if result.returncode != 0:
|
105
|
+
print(f"⚠️ Warning: fix_modal_token.py exited with code {result.returncode}")
|
106
|
+
if result.stderr:
|
107
|
+
print(f"Error: {result.stderr}")
|
108
|
+
|
109
|
+
# Set token variables for later use
|
110
|
+
token = "ak-sLhYqCjkvixiYcb9LAuCHp" # Default token ID
|
111
|
+
except Exception as e:
|
112
|
+
print(f"⚠️ Error running fix_modal_token.py: {e}")
|
113
|
+
|
114
|
+
# Last resort: use hardcoded tokens
|
115
|
+
token = "ak-sLhYqCjkvixiYcb9LAuCHp" # Default token ID
|
116
|
+
|
117
|
+
# Print debug info
|
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'}")
|
122
|
+
if os.environ.get('MODAL_TOKEN_ID'):
|
123
|
+
print(f"🔍 MODAL_TOKEN_ID length: {len(os.environ.get('MODAL_TOKEN_ID'))}")
|
124
|
+
if os.environ.get('MODAL_TOKEN_SECRET'):
|
125
|
+
print(f"🔍 MODAL_TOKEN_SECRET length: {len(os.environ.get('MODAL_TOKEN_SECRET'))}")
|
126
|
+
if os.environ.get('MODAL_TOKEN'):
|
127
|
+
print(f"🔍 MODAL_TOKEN length: {len(os.environ.get('MODAL_TOKEN'))}")
|
128
|
+
print(f"✅ Modal token setup completed")
|
129
|
+
|
130
|
+
# Import modal after token setup
|
131
|
+
import modal
|
13
132
|
|
14
133
|
def handle_interactive_input(prompt, is_password=False):
|
15
134
|
"""Handle interactive input from the user with optional password masking"""
|
@@ -56,13 +175,25 @@ def handle_wandb_login(sandbox, current_dir):
|
|
56
175
|
print("Setting up Weights & Biases credentials")
|
57
176
|
print("You can get your API key from: https://wandb.ai/authorize")
|
58
177
|
|
59
|
-
#
|
60
|
-
api_key =
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
178
|
+
# Try to use credentials manager first
|
179
|
+
api_key = None
|
180
|
+
try:
|
181
|
+
from credentials_manager import CredentialsManager
|
182
|
+
credentials_manager = CredentialsManager()
|
183
|
+
api_key = credentials_manager.get_wandb_api_key()
|
184
|
+
except ImportError:
|
185
|
+
# Fall back to direct input if credentials_manager is not available
|
186
|
+
pass
|
187
|
+
|
188
|
+
# If credentials manager didn't provide a key, use direct input
|
189
|
+
if not api_key:
|
190
|
+
# Get API key from user
|
191
|
+
api_key = handle_interactive_input(
|
192
|
+
"🔑 WEIGHTS & BIASES API KEY REQUIRED\n" +
|
193
|
+
"Please paste your W&B API key below:\n" +
|
194
|
+
"(Your API key should be 40 characters long)",
|
195
|
+
is_password=True
|
196
|
+
)
|
66
197
|
|
67
198
|
if not api_key:
|
68
199
|
print("❌ No API key provided. Cannot continue with W&B login.")
|
@@ -226,26 +357,36 @@ def call_openai_for_debug(command, error_output, api_key=None, current_dir=None,
|
|
226
357
|
api_key = os.environ.get("OPENAI_API_KEY")
|
227
358
|
|
228
359
|
if not api_key:
|
229
|
-
|
230
|
-
print("🔑 OPENAI API KEY REQUIRED FOR DEBUGGING")
|
231
|
-
print("="*60)
|
232
|
-
print("To debug failed commands, an OpenAI API key is needed.")
|
233
|
-
print("📝 Please paste your OpenAI API key below:")
|
234
|
-
print(" (Your input will be hidden for security)")
|
235
|
-
print("-" * 60)
|
236
|
-
|
360
|
+
# Use the CredentialsManager to get the API key
|
237
361
|
try:
|
238
|
-
|
362
|
+
from credentials_manager import CredentialsManager
|
363
|
+
credentials_manager = CredentialsManager()
|
364
|
+
api_key = credentials_manager.get_openai_api_key()
|
239
365
|
if not api_key:
|
240
366
|
print("❌ No API key provided. Skipping debugging.")
|
241
367
|
return None
|
242
|
-
|
243
|
-
|
244
|
-
print("\n
|
245
|
-
|
246
|
-
|
247
|
-
print(
|
248
|
-
|
368
|
+
except ImportError:
|
369
|
+
# Fall back to direct input if credentials_manager module is not available
|
370
|
+
print("\n" + "="*60)
|
371
|
+
print("🔑 OPENAI API KEY REQUIRED FOR DEBUGGING")
|
372
|
+
print("="*60)
|
373
|
+
print("To debug failed commands, an OpenAI API key is needed.")
|
374
|
+
print("📝 Please paste your OpenAI API key below:")
|
375
|
+
print(" (Your input will be hidden for security)")
|
376
|
+
print("-" * 60)
|
377
|
+
|
378
|
+
try:
|
379
|
+
api_key = getpass.getpass("OpenAI API Key: ").strip()
|
380
|
+
if not api_key:
|
381
|
+
print("❌ No API key provided. Skipping debugging.")
|
382
|
+
return None
|
383
|
+
print("✅ API key received successfully!")
|
384
|
+
except KeyboardInterrupt:
|
385
|
+
print("\n❌ API key input cancelled by user.")
|
386
|
+
return None
|
387
|
+
except Exception as e:
|
388
|
+
print(f"❌ Error getting API key: {e}")
|
389
|
+
return None
|
249
390
|
|
250
391
|
# Get current directory context
|
251
392
|
directory_context = ""
|
@@ -410,6 +551,18 @@ Do not provide any explanations, just the exact command to run.
|
|
410
551
|
|
411
552
|
def prompt_for_hf_token():
|
412
553
|
"""Prompt user for Hugging Face token when needed"""
|
554
|
+
# Try to use credentials manager first
|
555
|
+
try:
|
556
|
+
from credentials_manager import CredentialsManager
|
557
|
+
credentials_manager = CredentialsManager()
|
558
|
+
token = credentials_manager.get_huggingface_token()
|
559
|
+
if token:
|
560
|
+
return token
|
561
|
+
except ImportError:
|
562
|
+
# Fall back to direct input if credentials_manager is not available
|
563
|
+
pass
|
564
|
+
|
565
|
+
# Traditional direct input method as fallback
|
413
566
|
print("\n" + "="*60)
|
414
567
|
print("🔑 HUGGING FACE TOKEN REQUIRED")
|
415
568
|
print("="*60)
|
@@ -434,6 +587,87 @@ def prompt_for_hf_token():
|
|
434
587
|
return None
|
435
588
|
|
436
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
|
+
|
437
671
|
# Execution history for tracking all commands and their results in this session
|
438
672
|
execution_history = []
|
439
673
|
|
@@ -453,11 +687,15 @@ def create_modal_sandbox(gpu_type, repo_url=None, repo_name=None, setup_commands
|
|
453
687
|
app_name = f"sandbox-{timestamp}"
|
454
688
|
|
455
689
|
gpu_configs = {
|
690
|
+
'T4': {'gpu': 'T4', 'memory': 16},
|
691
|
+
'L4': {'gpu': 'L4', 'memory': 24},
|
456
692
|
'A10G': {'gpu': 'A10G', 'memory': 24},
|
457
|
-
'A100': {'gpu': 'A100-SXM4-40GB', 'memory': 40},
|
693
|
+
'A100-40GB': {'gpu': 'A100-SXM4-40GB', 'memory': 40},
|
694
|
+
'A100-80GB': {'gpu': 'A100-80GB', 'memory': 80},
|
695
|
+
'L40S': {'gpu': 'L40S', 'memory': 48},
|
458
696
|
'H100': {'gpu': 'H100', 'memory': 80},
|
459
|
-
'
|
460
|
-
'
|
697
|
+
'H200': {'gpu': 'H200', 'memory': 141},
|
698
|
+
'B200': {'gpu': 'B200', 'memory': 96}
|
461
699
|
}
|
462
700
|
|
463
701
|
if gpu_type not in gpu_configs:
|
@@ -752,18 +990,52 @@ def create_modal_sandbox(gpu_type, repo_url=None, repo_name=None, setup_commands
|
|
752
990
|
not target_dir.startswith("/") and not target_dir.startswith("./") and
|
753
991
|
not target_dir.startswith("../") and current_dir.endswith("/" + target_dir)):
|
754
992
|
|
755
|
-
#
|
756
|
-
|
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
|
757
1003
|
test_cmd = f"test -d \"{target_dir}\""
|
758
1004
|
test_result = sandbox.exec("bash", "-c", test_cmd)
|
759
1005
|
test_result.wait()
|
760
1006
|
|
761
1007
|
if test_result.returncode == 0:
|
762
|
-
#
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
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...")
|
767
1039
|
else:
|
768
1040
|
# No nested directory exists, so this is truly redundant
|
769
1041
|
print(f"⚠️ Detected redundant directory navigation: {cmd}")
|
@@ -1857,22 +2129,234 @@ def generate_random_password(length=16):
|
|
1857
2129
|
password = ''.join(secrets.choice(alphabet) for i in range(length))
|
1858
2130
|
return password
|
1859
2131
|
|
2132
|
+
# First, add the standalone ssh_container function at the module level, before the create_modal_ssh_container function
|
2133
|
+
|
2134
|
+
# Define a module-level ssh container function
|
2135
|
+
ssh_app = modal.App("ssh-container-app")
|
2136
|
+
|
2137
|
+
@ssh_app.function(
|
2138
|
+
image=modal.Image.debian_slim()
|
2139
|
+
.apt_install(
|
2140
|
+
"openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
|
2141
|
+
"python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
|
2142
|
+
"gpg", "ca-certificates", "software-properties-common"
|
2143
|
+
)
|
2144
|
+
.pip_install("uv", "modal") # Fast Python package installer and Modal
|
2145
|
+
.run_commands(
|
2146
|
+
# Create SSH directory
|
2147
|
+
"mkdir -p /var/run/sshd",
|
2148
|
+
"mkdir -p /root/.ssh",
|
2149
|
+
"chmod 700 /root/.ssh",
|
2150
|
+
|
2151
|
+
# Configure SSH server
|
2152
|
+
"sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
|
2153
|
+
"sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
|
2154
|
+
"sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
|
2155
|
+
|
2156
|
+
# SSH keep-alive settings
|
2157
|
+
"echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
|
2158
|
+
"echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
|
2159
|
+
|
2160
|
+
# Generate SSH host keys
|
2161
|
+
"ssh-keygen -A",
|
2162
|
+
|
2163
|
+
# Install Modal CLI
|
2164
|
+
"pip install modal",
|
2165
|
+
|
2166
|
+
# Set up a nice bash prompt
|
2167
|
+
"echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
|
2168
|
+
),
|
2169
|
+
timeout=3600, # Default 1 hour timeout
|
2170
|
+
gpu="a10g", # Default GPU - this will be overridden when called
|
2171
|
+
cpu=2,
|
2172
|
+
memory=8192,
|
2173
|
+
serialized=True,
|
2174
|
+
)
|
2175
|
+
def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_commands=None):
|
2176
|
+
import subprocess
|
2177
|
+
import time
|
2178
|
+
import os
|
2179
|
+
|
2180
|
+
# Set root password
|
2181
|
+
subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
|
2182
|
+
|
2183
|
+
# Start SSH service
|
2184
|
+
subprocess.run(["service", "ssh", "start"], check=True)
|
2185
|
+
|
2186
|
+
# Setup environment
|
2187
|
+
os.environ['PS1'] = r'\[\e[1;32m\]modal:\[\e[1;34m\]\w\[\e[0m\]$ '
|
2188
|
+
|
2189
|
+
# Clone repository if provided
|
2190
|
+
if repo_url:
|
2191
|
+
repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
|
2192
|
+
print(f"📥 Cloning repository: {repo_url}")
|
2193
|
+
|
2194
|
+
try:
|
2195
|
+
subprocess.run(["git", "clone", repo_url], check=True, cwd="/root")
|
2196
|
+
print(f"✅ Repository cloned successfully: {repo_name_from_url}")
|
2197
|
+
|
2198
|
+
# Change to repository directory
|
2199
|
+
repo_dir = f"/root/{repo_name_from_url}"
|
2200
|
+
if os.path.exists(repo_dir):
|
2201
|
+
os.chdir(repo_dir)
|
2202
|
+
print(f"📂 Changed to repository directory: {repo_dir}")
|
2203
|
+
|
2204
|
+
except subprocess.CalledProcessError as e:
|
2205
|
+
print(f"❌ Failed to clone repository: {e}")
|
2206
|
+
|
2207
|
+
# Run setup commands if provided
|
2208
|
+
if setup_commands:
|
2209
|
+
print(f"⚙️ Running {len(setup_commands)} setup commands...")
|
2210
|
+
for i, cmd in enumerate(setup_commands, 1):
|
2211
|
+
print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
|
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}")
|
2221
|
+
|
2222
|
+
# Get container info
|
2223
|
+
print("🔍 Container started successfully!")
|
2224
|
+
print(f"🆔 Container ID: {os.environ.get('MODAL_TASK_ID', 'unknown')}")
|
2225
|
+
|
2226
|
+
# Keep the container running
|
2227
|
+
while True:
|
2228
|
+
time.sleep(30)
|
2229
|
+
# Check if SSH service is still running
|
2230
|
+
try:
|
2231
|
+
subprocess.run(["service", "ssh", "status"], check=True,
|
2232
|
+
capture_output=True)
|
2233
|
+
except subprocess.CalledProcessError:
|
2234
|
+
print("⚠️ SSH service stopped, restarting...")
|
2235
|
+
subprocess.run(["service", "ssh", "start"], check=True)
|
1860
2236
|
|
2237
|
+
# Now modify the create_modal_ssh_container function to use the standalone ssh_container_function
|
1861
2238
|
|
1862
2239
|
def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_commands=None,
|
1863
2240
|
volume_name=None, timeout_minutes=60, ssh_password=None):
|
1864
|
-
"""Create a Modal SSH container with GPU support"""
|
2241
|
+
"""Create a Modal SSH container with GPU support and tunneling"""
|
2242
|
+
|
2243
|
+
# Check if Modal is authenticated
|
2244
|
+
try:
|
2245
|
+
# Print all environment variables for debugging
|
2246
|
+
print("🔍 DEBUG: Checking environment variables")
|
2247
|
+
modal_token_id = os.environ.get("MODAL_TOKEN_ID")
|
2248
|
+
modal_token = os.environ.get("MODAL_TOKEN")
|
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'}")
|
2251
|
+
if modal_token_id:
|
2252
|
+
print(f"🔍 MODAL_TOKEN_ID length: {len(modal_token_id)}")
|
2253
|
+
if modal_token:
|
2254
|
+
print(f"🔍 MODAL_TOKEN length: {len(modal_token)}")
|
2255
|
+
|
2256
|
+
# Try to access Modal token to check authentication
|
2257
|
+
try:
|
2258
|
+
# Check if token is set in environment
|
2259
|
+
modal_token_id = os.environ.get("MODAL_TOKEN_ID")
|
2260
|
+
if not modal_token_id:
|
2261
|
+
print("⚠️ MODAL_TOKEN_ID not found in environment.")
|
2262
|
+
# Try to get from MODAL_TOKEN
|
2263
|
+
modal_token = os.environ.get("MODAL_TOKEN")
|
2264
|
+
if modal_token:
|
2265
|
+
print("✅ Found token in MODAL_TOKEN environment variable")
|
2266
|
+
os.environ["MODAL_TOKEN_ID"] = modal_token
|
2267
|
+
modal_token_id = modal_token
|
2268
|
+
print(f"✅ Set MODAL_TOKEN_ID from MODAL_TOKEN (length: {len(modal_token)})")
|
2269
|
+
|
2270
|
+
if modal_token_id:
|
2271
|
+
print(f"✅ Modal token found (length: {len(modal_token_id)})")
|
2272
|
+
|
2273
|
+
# Use the comprehensive fix_modal_token script
|
2274
|
+
try:
|
2275
|
+
# Execute the fix_modal_token.py script
|
2276
|
+
import subprocess
|
2277
|
+
print(f"🔄 Running fix_modal_token.py to set up Modal token...")
|
2278
|
+
result = subprocess.run(
|
2279
|
+
["python", os.path.join(os.path.dirname(__file__), "fix_modal_token.py")],
|
2280
|
+
capture_output=True,
|
2281
|
+
text=True
|
2282
|
+
)
|
2283
|
+
|
2284
|
+
# Print the output
|
2285
|
+
print(result.stdout)
|
2286
|
+
|
2287
|
+
if result.returncode != 0:
|
2288
|
+
print(f"⚠️ Warning: fix_modal_token.py exited with code {result.returncode}")
|
2289
|
+
if result.stderr:
|
2290
|
+
print(f"Error: {result.stderr}")
|
2291
|
+
|
2292
|
+
print(f"✅ Modal token setup completed")
|
2293
|
+
except Exception as e:
|
2294
|
+
print(f"⚠️ Error running fix_modal_token.py: {e}")
|
2295
|
+
else:
|
2296
|
+
print("❌ No Modal token found in environment variables")
|
2297
|
+
# Try to get from file as a last resort
|
2298
|
+
try:
|
2299
|
+
home_dir = os.path.expanduser("~")
|
2300
|
+
modal_dir = os.path.join(home_dir, ".modal")
|
2301
|
+
token_file = os.path.join(modal_dir, "token.json")
|
2302
|
+
if os.path.exists(token_file):
|
2303
|
+
print(f"🔍 Found Modal token file at {token_file}")
|
2304
|
+
with open(token_file, 'r') as f:
|
2305
|
+
import json
|
2306
|
+
token_data = json.load(f)
|
2307
|
+
if "token_id" in token_data:
|
2308
|
+
modal_token_id = token_data["token_id"]
|
2309
|
+
os.environ["MODAL_TOKEN_ID"] = modal_token_id
|
2310
|
+
os.environ["MODAL_TOKEN"] = modal_token_id
|
2311
|
+
print(f"✅ Loaded token from file (length: {len(modal_token_id)})")
|
2312
|
+
else:
|
2313
|
+
print("❌ Token file does not contain token_id")
|
2314
|
+
else:
|
2315
|
+
print("❌ Modal token file not found")
|
2316
|
+
except Exception as e:
|
2317
|
+
print(f"❌ Error loading token from file: {e}")
|
2318
|
+
|
2319
|
+
if not os.environ.get("MODAL_TOKEN_ID"):
|
2320
|
+
print("❌ Could not find Modal token in any location")
|
2321
|
+
return None
|
2322
|
+
|
2323
|
+
except Exception as e:
|
2324
|
+
print(f"⚠️ Error checking Modal token: {e}")
|
2325
|
+
# Try to use the token from environment
|
2326
|
+
modal_token_id = os.environ.get("MODAL_TOKEN_ID")
|
2327
|
+
modal_token = os.environ.get("MODAL_TOKEN")
|
2328
|
+
if modal_token_id:
|
2329
|
+
print(f"🔄 Using MODAL_TOKEN_ID from environment (length: {len(modal_token_id)})")
|
2330
|
+
elif modal_token:
|
2331
|
+
print(f"🔄 Using MODAL_TOKEN from environment (length: {len(modal_token)})")
|
2332
|
+
os.environ["MODAL_TOKEN_ID"] = modal_token
|
2333
|
+
modal_token_id = modal_token
|
2334
|
+
else:
|
2335
|
+
print("❌ No Modal token available. Cannot proceed.")
|
2336
|
+
return None
|
2337
|
+
|
2338
|
+
# Set it in both environment variables
|
2339
|
+
os.environ["MODAL_TOKEN_ID"] = modal_token_id
|
2340
|
+
os.environ["MODAL_TOKEN"] = modal_token_id
|
2341
|
+
print("✅ Set both MODAL_TOKEN_ID and MODAL_TOKEN environment variables")
|
2342
|
+
except Exception as e:
|
2343
|
+
print(f"⚠️ Error checking Modal authentication: {e}")
|
2344
|
+
print("Continuing anyway, but Modal operations may fail")
|
1865
2345
|
|
1866
2346
|
# Generate a unique app name with timestamp to avoid conflicts
|
1867
2347
|
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
1868
2348
|
app_name = f"ssh-container-{timestamp}"
|
1869
2349
|
|
1870
2350
|
gpu_configs = {
|
2351
|
+
'T4': {'gpu': 't4', 'memory': 16},
|
2352
|
+
'L4': {'gpu': 'l4', 'memory': 24},
|
1871
2353
|
'A10G': {'gpu': 'a10g', 'memory': 24},
|
1872
|
-
'A100': {'gpu': 'a100', 'memory': 40},
|
2354
|
+
'A100-40GB': {'gpu': 'a100', 'memory': 40},
|
2355
|
+
'A100-80GB': {'gpu': 'a100-80gb', 'memory': 80},
|
2356
|
+
'L40S': {'gpu': 'l40s', 'memory': 48},
|
1873
2357
|
'H100': {'gpu': 'h100', 'memory': 80},
|
1874
|
-
'
|
1875
|
-
'
|
2358
|
+
'H200': {'gpu': 'h200', 'memory': 141},
|
2359
|
+
'B200': {'gpu': 'b200', 'memory': 96}
|
1876
2360
|
}
|
1877
2361
|
|
1878
2362
|
if gpu_type not in gpu_configs:
|
@@ -1912,109 +2396,135 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
1912
2396
|
print(f"⚠️ Could not create default volume: {e}")
|
1913
2397
|
print("⚠️ Continuing without persistent volume")
|
1914
2398
|
volume = None
|
2399
|
+
|
2400
|
+
# Print debug info for authentication
|
2401
|
+
print("🔍 Modal authentication debug info:")
|
2402
|
+
modal_token = os.environ.get("MODAL_TOKEN_ID")
|
2403
|
+
print(f" - MODAL_TOKEN_ID in env: {'Yes' if modal_token else 'No'}")
|
2404
|
+
print(f" - Token length: {len(modal_token) if modal_token else 'N/A'}")
|
2405
|
+
|
2406
|
+
# Verify we can create a Modal app
|
2407
|
+
try:
|
2408
|
+
print("🔍 Testing Modal app creation...")
|
2409
|
+
app = modal.App(app_name)
|
2410
|
+
print("✅ Created Modal app successfully")
|
2411
|
+
except Exception as e:
|
2412
|
+
print(f"❌ Error creating Modal app: {e}")
|
2413
|
+
return None
|
1915
2414
|
|
1916
2415
|
# Create SSH-enabled image
|
1917
|
-
|
1918
|
-
|
1919
|
-
|
1920
|
-
|
1921
|
-
|
1922
|
-
|
1923
|
-
|
1924
|
-
|
1925
|
-
|
1926
|
-
#
|
1927
|
-
|
1928
|
-
|
1929
|
-
|
1930
|
-
|
1931
|
-
|
1932
|
-
|
1933
|
-
|
1934
|
-
|
1935
|
-
|
1936
|
-
|
1937
|
-
|
1938
|
-
|
1939
|
-
|
1940
|
-
|
1941
|
-
|
1942
|
-
|
1943
|
-
|
1944
|
-
|
2416
|
+
try:
|
2417
|
+
print("📦 Building SSH-enabled image...")
|
2418
|
+
ssh_image = (
|
2419
|
+
modal.Image.debian_slim()
|
2420
|
+
.apt_install(
|
2421
|
+
"openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
|
2422
|
+
"python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
|
2423
|
+
"gpg", "ca-certificates", "software-properties-common"
|
2424
|
+
)
|
2425
|
+
.pip_install("uv", "modal") # Fast Python package installer and Modal
|
2426
|
+
.run_commands(
|
2427
|
+
# Create SSH directory
|
2428
|
+
"mkdir -p /var/run/sshd",
|
2429
|
+
"mkdir -p /root/.ssh",
|
2430
|
+
"chmod 700 /root/.ssh",
|
2431
|
+
|
2432
|
+
# Configure SSH server
|
2433
|
+
"sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
|
2434
|
+
"sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
|
2435
|
+
"sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
|
2436
|
+
|
2437
|
+
# SSH keep-alive settings
|
2438
|
+
"echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
|
2439
|
+
"echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
|
2440
|
+
|
2441
|
+
# Generate SSH host keys
|
2442
|
+
"ssh-keygen -A",
|
2443
|
+
|
2444
|
+
# Set up a nice bash prompt
|
2445
|
+
"echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
|
2446
|
+
)
|
1945
2447
|
)
|
2448
|
+
print("✅ SSH image built successfully")
|
2449
|
+
except Exception as e:
|
2450
|
+
print(f"❌ Error building SSH image: {e}")
|
2451
|
+
return None
|
2452
|
+
|
2453
|
+
# Configure volumes if available
|
2454
|
+
volumes_config = {}
|
2455
|
+
if volume:
|
2456
|
+
volumes_config[volume_mount_path] = volume
|
2457
|
+
|
2458
|
+
# Define the SSH container function
|
2459
|
+
@app.function(
|
2460
|
+
image=ssh_image,
|
2461
|
+
timeout=timeout_minutes * 60, # Convert to seconds
|
2462
|
+
gpu=gpu_spec['gpu'],
|
2463
|
+
cpu=2,
|
2464
|
+
memory=8192,
|
2465
|
+
serialized=True,
|
2466
|
+
volumes=volumes_config if volumes_config else None,
|
1946
2467
|
)
|
1947
|
-
|
1948
|
-
|
1949
|
-
|
1950
|
-
|
1951
|
-
|
2468
|
+
def ssh_container_function():
|
2469
|
+
"""Start SSH container with password authentication and optional setup."""
|
2470
|
+
import subprocess
|
2471
|
+
import time
|
2472
|
+
import os
|
1952
2473
|
|
1953
|
-
#
|
1954
|
-
|
1955
|
-
if volume:
|
1956
|
-
volumes[volume_mount_path] = volume
|
1957
|
-
print(f"📦 Mounting volume '{volume_name}' at {volume_mount_path}")
|
2474
|
+
# Set root password
|
2475
|
+
subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
|
1958
2476
|
|
1959
|
-
|
1960
|
-
|
1961
|
-
|
1962
|
-
|
1963
|
-
|
1964
|
-
|
1965
|
-
|
1966
|
-
volumes=volumes if volumes else None,
|
1967
|
-
)
|
1968
|
-
def ssh_container():
|
1969
|
-
import subprocess
|
1970
|
-
import time
|
1971
|
-
import os
|
1972
|
-
|
1973
|
-
# Set root password
|
1974
|
-
subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
|
1975
|
-
|
1976
|
-
# Start SSH service
|
1977
|
-
subprocess.run(["service", "ssh", "start"], check=True)
|
1978
|
-
|
1979
|
-
# Setup environment
|
1980
|
-
os.environ['PS1'] = r'\[\e[1;32m\]modal:\[\e[1;34m\]\w\[\e[0m\]$ '
|
2477
|
+
# Start SSH service
|
2478
|
+
subprocess.run(["service", "ssh", "start"], check=True)
|
2479
|
+
|
2480
|
+
# Clone repository if provided
|
2481
|
+
if repo_url:
|
2482
|
+
repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
|
2483
|
+
print(f"📥 Cloning repository: {repo_url}")
|
1981
2484
|
|
1982
|
-
|
1983
|
-
|
1984
|
-
|
1985
|
-
print(f"📥 Cloning repository: {repo_url}")
|
2485
|
+
try:
|
2486
|
+
subprocess.run(["git", "clone", repo_url], check=True, cwd="/root")
|
2487
|
+
print(f"✅ Repository cloned successfully: {repo_name_from_url}")
|
1986
2488
|
|
2489
|
+
# Change to repository directory
|
2490
|
+
repo_dir = f"/root/{repo_name_from_url}"
|
2491
|
+
if os.path.exists(repo_dir):
|
2492
|
+
os.chdir(repo_dir)
|
2493
|
+
print(f"📂 Changed to repository directory: {repo_dir}")
|
2494
|
+
|
2495
|
+
except subprocess.CalledProcessError as e:
|
2496
|
+
print(f"❌ Failed to clone repository: {e}")
|
2497
|
+
|
2498
|
+
# Run setup commands if provided
|
2499
|
+
if setup_commands:
|
2500
|
+
print(f"⚙️ Running {len(setup_commands)} setup commands...")
|
2501
|
+
for i, cmd in enumerate(setup_commands, 1):
|
2502
|
+
print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
|
1987
2503
|
try:
|
1988
|
-
subprocess.run(
|
1989
|
-
|
1990
|
-
|
1991
|
-
|
1992
|
-
repo_dir = f"/root/{repo_name_from_url}"
|
1993
|
-
if os.path.exists(repo_dir):
|
1994
|
-
os.chdir(repo_dir)
|
1995
|
-
print(f"📂 Changed to repository directory: {repo_dir}")
|
1996
|
-
|
2504
|
+
result = subprocess.run(cmd, shell=True, check=True,
|
2505
|
+
capture_output=True, text=True)
|
2506
|
+
if result.stdout:
|
2507
|
+
print(f"✅ Output: {result.stdout}")
|
1997
2508
|
except subprocess.CalledProcessError as e:
|
1998
|
-
print(f"❌
|
1999
|
-
|
2000
|
-
|
2001
|
-
|
2002
|
-
|
2003
|
-
|
2004
|
-
|
2005
|
-
|
2006
|
-
|
2007
|
-
|
2008
|
-
|
2009
|
-
|
2010
|
-
|
2011
|
-
|
2012
|
-
|
2013
|
-
|
2014
|
-
|
2015
|
-
|
2016
|
-
print("
|
2017
|
-
print(f"🆔 Container ID: {os.environ.get('MODAL_TASK_ID', 'unknown')}")
|
2509
|
+
print(f"❌ Command failed: {e}")
|
2510
|
+
if e.stderr:
|
2511
|
+
print(f"❌ Error: {e.stderr}")
|
2512
|
+
|
2513
|
+
# Create SSH tunnel
|
2514
|
+
with modal.forward(22, unencrypted=True) as tunnel:
|
2515
|
+
host, port = tunnel.tcp_socket
|
2516
|
+
|
2517
|
+
print("\n" + "=" * 80)
|
2518
|
+
print("🎉 SSH CONTAINER IS READY!")
|
2519
|
+
print("=" * 80)
|
2520
|
+
print(f"🌐 SSH Host: {host}")
|
2521
|
+
print(f"🔌 SSH Port: {port}")
|
2522
|
+
print(f"👤 Username: root")
|
2523
|
+
print(f"🔐 Password: {ssh_password}")
|
2524
|
+
print()
|
2525
|
+
print("🔗 CONNECT USING THIS COMMAND:")
|
2526
|
+
print(f"ssh -p {port} root@{host}")
|
2527
|
+
print("=" * 80)
|
2018
2528
|
|
2019
2529
|
# Keep the container running
|
2020
2530
|
while True:
|
@@ -2026,123 +2536,27 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
2026
2536
|
except subprocess.CalledProcessError:
|
2027
2537
|
print("⚠️ SSH service stopped, restarting...")
|
2028
2538
|
subprocess.run(["service", "ssh", "start"], check=True)
|
2539
|
+
|
2540
|
+
# Run the container
|
2541
|
+
try:
|
2542
|
+
print("⏳ Starting container... This may take 1-2 minutes...")
|
2029
2543
|
|
2030
|
-
# Start the container
|
2031
|
-
|
2032
|
-
|
2033
|
-
|
2034
|
-
container_handle = ssh_container.spawn()
|
2035
|
-
|
2036
|
-
# Wait a moment for the container to start
|
2037
|
-
print("⏳ Waiting for container to initialize...")
|
2038
|
-
time.sleep(10)
|
2039
|
-
|
2040
|
-
# Get container information
|
2041
|
-
try:
|
2042
|
-
# Try to get the container ID from Modal
|
2043
|
-
container_id = None
|
2044
|
-
|
2045
|
-
# Get container list to find our container
|
2046
|
-
print("🔍 Looking for container information...")
|
2047
|
-
result = subprocess.run(["modal", "container", "list", "--json"],
|
2048
|
-
capture_output=True, text=True)
|
2049
|
-
|
2050
|
-
if result.returncode == 0:
|
2051
|
-
try:
|
2052
|
-
containers = json.loads(result.stdout)
|
2053
|
-
if containers and isinstance(containers, list):
|
2054
|
-
# Find the most recent container
|
2055
|
-
for container in containers:
|
2056
|
-
if container.get("App") == app_name:
|
2057
|
-
container_id = container.get("Container ID")
|
2058
|
-
break
|
2059
|
-
|
2060
|
-
if not container_id and containers:
|
2061
|
-
# Fall back to the first container
|
2062
|
-
container_id = containers[0].get("Container ID")
|
2063
|
-
|
2064
|
-
except json.JSONDecodeError:
|
2065
|
-
pass
|
2066
|
-
|
2067
|
-
if not container_id:
|
2068
|
-
# Try text parsing
|
2069
|
-
result = subprocess.run(["modal", "container", "list"],
|
2070
|
-
capture_output=True, text=True)
|
2071
|
-
if result.returncode == 0:
|
2072
|
-
lines = result.stdout.split('\n')
|
2073
|
-
for line in lines:
|
2074
|
-
if app_name in line or ('ta-' in line and '│' in line):
|
2075
|
-
parts = line.split('│')
|
2076
|
-
if len(parts) >= 2:
|
2077
|
-
possible_id = parts[1].strip()
|
2078
|
-
if possible_id.startswith('ta-'):
|
2079
|
-
container_id = possible_id
|
2080
|
-
break
|
2081
|
-
|
2082
|
-
if container_id:
|
2083
|
-
print(f"📋 Container ID: {container_id}")
|
2084
|
-
|
2085
|
-
# Get the external IP for SSH access
|
2086
|
-
print("🔍 Getting container connection info...")
|
2544
|
+
# Start the container in a new thread to avoid blocking
|
2545
|
+
with modal.enable_output():
|
2546
|
+
with app.run():
|
2547
|
+
ssh_container_function.remote()
|
2087
2548
|
|
2088
|
-
|
2089
|
-
|
2090
|
-
# Modal containers typically expose SSH on port 22
|
2091
|
-
ssh_info = f"ssh root@{container_id}.modal.run"
|
2092
|
-
|
2093
|
-
print("\n" + "="*80)
|
2094
|
-
print("🚀 SSH CONTAINER READY!")
|
2095
|
-
print("="*80)
|
2096
|
-
print(f"🆔 Container ID: {container_id}")
|
2097
|
-
print(f"🔐 SSH Password: {ssh_password}")
|
2098
|
-
print(f"📱 App Name: {app_name}")
|
2099
|
-
if volume:
|
2100
|
-
print(f"💾 Volume: {volume_name} (mounted at {volume_mount_path})")
|
2101
|
-
print("\n🔗 SSH Connection:")
|
2102
|
-
print(f" {ssh_info}")
|
2103
|
-
print(f" Password: {ssh_password}")
|
2104
|
-
print("\n💡 Alternative connection methods:")
|
2105
|
-
print(f" modal container exec --pty {container_id} bash")
|
2106
|
-
print(f" modal shell {container_id}")
|
2107
|
-
print("="*80)
|
2108
|
-
|
2109
|
-
# Try to open SSH connection in a new terminal
|
2110
|
-
try:
|
2111
|
-
terminal_script = f'''
|
2112
|
-
tell application "Terminal"
|
2113
|
-
do script "echo 'Connecting to Modal SSH container...'; echo 'Password: {ssh_password}'; {ssh_info}"
|
2114
|
-
activate
|
2115
|
-
end tell
|
2116
|
-
'''
|
2117
|
-
|
2118
|
-
subprocess.run(['osascript', '-e', terminal_script],
|
2119
|
-
capture_output=True, text=True, timeout=30)
|
2120
|
-
print("✅ New terminal window opened with SSH connection")
|
2121
|
-
|
2122
|
-
except Exception as e:
|
2123
|
-
print(f"⚠️ Could not open terminal window: {e}")
|
2124
|
-
print("📝 You can manually connect using the SSH command above")
|
2125
|
-
|
2126
|
-
except Exception as e:
|
2127
|
-
print(f"⚠️ Error getting SSH connection info: {e}")
|
2128
|
-
print("📝 You can connect using:")
|
2129
|
-
print(f" modal container exec --pty {container_id} bash")
|
2130
|
-
else:
|
2131
|
-
print("⚠️ Could not determine container ID")
|
2132
|
-
print("📝 Check running containers with: modal container list")
|
2133
|
-
|
2134
|
-
except Exception as e:
|
2135
|
-
print(f"❌ Error getting container information: {e}")
|
2549
|
+
# Clean up Modal token after container is successfully created
|
2550
|
+
cleanup_modal_token()
|
2136
2551
|
|
2137
|
-
# Return container information
|
2138
2552
|
return {
|
2139
|
-
"container_handle": container_handle,
|
2140
|
-
"container_id": container_id,
|
2141
2553
|
"app_name": app_name,
|
2142
2554
|
"ssh_password": ssh_password,
|
2143
|
-
"volume_name": volume_name
|
2144
|
-
"volume_mount_path": volume_mount_path if volume else None
|
2555
|
+
"volume_name": volume_name
|
2145
2556
|
}
|
2557
|
+
except Exception as e:
|
2558
|
+
print(f"❌ Error running container: {e}")
|
2559
|
+
return None
|
2146
2560
|
|
2147
2561
|
def fetch_setup_commands_from_api(repo_url):
|
2148
2562
|
"""Fetch setup commands from the GitIngest API using real repository analysis."""
|
@@ -2152,7 +2566,7 @@ def fetch_setup_commands_from_api(repo_url):
|
|
2152
2566
|
import shutil
|
2153
2567
|
import json
|
2154
2568
|
|
2155
|
-
api_url = "
|
2569
|
+
api_url = "https://git-arsenal.vercel.app/api/analyze-with-gitingest"
|
2156
2570
|
|
2157
2571
|
print(f"🔍 Fetching setup commands from API for repository: {repo_url}")
|
2158
2572
|
|
@@ -2248,53 +2662,201 @@ def fetch_setup_commands_from_api(repo_url):
|
|
2248
2662
|
|
2249
2663
|
# Make the API request
|
2250
2664
|
print(f"🌐 Making POST request to: {api_url}")
|
2251
|
-
|
2252
|
-
|
2253
|
-
|
2254
|
-
|
2255
|
-
|
2256
|
-
|
2257
|
-
|
2258
|
-
|
2259
|
-
|
2260
|
-
# Extract setup commands from the response
|
2261
|
-
if "setupInstructions" in data and "commands" in data["setupInstructions"]:
|
2262
|
-
commands = data["setupInstructions"]["commands"]
|
2263
|
-
print(f"✅ Successfully fetched {len(commands)} setup commands from API")
|
2264
|
-
|
2265
|
-
# Print the commands for reference
|
2266
|
-
for i, cmd in enumerate(commands, 1):
|
2267
|
-
print(f" {i}. {cmd}")
|
2665
|
+
try:
|
2666
|
+
response = requests.post(api_url, json=payload, timeout=60)
|
2667
|
+
|
2668
|
+
print(f"📥 API Response status code: {response.status_code}")
|
2669
|
+
|
2670
|
+
if response.status_code == 200:
|
2671
|
+
try:
|
2672
|
+
data = response.json()
|
2673
|
+
print(f"📄 API Response data received")
|
2268
2674
|
|
2269
|
-
|
2270
|
-
|
2271
|
-
|
2272
|
-
|
2273
|
-
|
2274
|
-
|
2275
|
-
|
2276
|
-
|
2277
|
-
|
2278
|
-
|
2279
|
-
|
2280
|
-
|
2281
|
-
|
2282
|
-
|
2283
|
-
|
2284
|
-
|
2285
|
-
|
2286
|
-
|
2287
|
-
|
2675
|
+
# Extract setup commands from the response
|
2676
|
+
if "setupInstructions" in data and "commands" in data["setupInstructions"]:
|
2677
|
+
commands = data["setupInstructions"]["commands"]
|
2678
|
+
print(f"✅ Successfully fetched {len(commands)} setup commands from API")
|
2679
|
+
|
2680
|
+
# Print the original commands for reference
|
2681
|
+
print("📋 Original commands from API:")
|
2682
|
+
for i, cmd in enumerate(commands, 1):
|
2683
|
+
print(f" {i}. {cmd}")
|
2684
|
+
|
2685
|
+
# Fix the commands by removing placeholders and comments
|
2686
|
+
fixed_commands = fix_setup_commands(commands)
|
2687
|
+
|
2688
|
+
# If we have a temp_dir with the cloned repo, try to find the entry point
|
2689
|
+
# and replace any placeholder entry points
|
2690
|
+
for i, cmd in enumerate(fixed_commands):
|
2691
|
+
if "python main.py" in cmd or "python3 main.py" in cmd:
|
2692
|
+
try:
|
2693
|
+
entry_point = find_entry_point(temp_dir)
|
2694
|
+
if entry_point and entry_point != "main.py":
|
2695
|
+
fixed_commands[i] = cmd.replace("main.py", entry_point)
|
2696
|
+
print(f"🔄 Replaced main.py with detected entry point: {entry_point}")
|
2697
|
+
except Exception as e:
|
2698
|
+
print(f"⚠️ Error finding entry point: {e}")
|
2699
|
+
|
2700
|
+
# Print the fixed commands
|
2701
|
+
print("\n📋 Fixed commands:")
|
2702
|
+
for i, cmd in enumerate(fixed_commands, 1):
|
2703
|
+
print(f" {i}. {cmd}")
|
2704
|
+
|
2705
|
+
return fixed_commands
|
2706
|
+
else:
|
2707
|
+
print("⚠️ API response did not contain setupInstructions.commands field")
|
2708
|
+
print("📋 Available fields in response:")
|
2709
|
+
for key in data.keys():
|
2710
|
+
print(f" - {key}")
|
2711
|
+
# Return fallback commands
|
2712
|
+
return generate_fallback_commands(gitingest_data)
|
2713
|
+
except json.JSONDecodeError as e:
|
2714
|
+
print(f"❌ Failed to parse API response as JSON: {e}")
|
2715
|
+
print(f"Raw response: {response.text[:500]}...")
|
2716
|
+
# Return fallback commands
|
2717
|
+
return generate_fallback_commands(gitingest_data)
|
2718
|
+
elif response.status_code == 504:
|
2719
|
+
print(f"❌ API request timed out (504 Gateway Timeout)")
|
2720
|
+
print("⚠️ The server took too long to respond. Using fallback commands instead.")
|
2721
|
+
# Return fallback commands
|
2722
|
+
return generate_fallback_commands(gitingest_data)
|
2723
|
+
else:
|
2724
|
+
print(f"❌ API request failed with status code: {response.status_code}")
|
2725
|
+
print(f"Error response: {response.text[:500]}...")
|
2726
|
+
# Return fallback commands
|
2727
|
+
return generate_fallback_commands(gitingest_data)
|
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}")
|
2735
|
+
print("⚠️ Using fallback commands instead")
|
2736
|
+
# Return fallback commands
|
2737
|
+
return generate_fallback_commands(gitingest_data)
|
2288
2738
|
except Exception as e:
|
2289
2739
|
print(f"❌ Error fetching setup commands from API: {e}")
|
2290
2740
|
import traceback
|
2291
2741
|
traceback.print_exc()
|
2292
|
-
|
2742
|
+
# Return fallback commands
|
2743
|
+
return generate_fallback_commands(None)
|
2293
2744
|
finally:
|
2294
2745
|
# Clean up the temporary directory
|
2295
2746
|
print(f"🧹 Cleaning up temporary directory...")
|
2296
2747
|
shutil.rmtree(temp_dir, ignore_errors=True)
|
2297
2748
|
|
2749
|
+
def generate_fallback_commands(gitingest_data):
|
2750
|
+
"""Generate fallback setup commands based on repository analysis"""
|
2751
|
+
print("\n" + "="*80)
|
2752
|
+
print("📋 GENERATING FALLBACK SETUP COMMANDS")
|
2753
|
+
print("="*80)
|
2754
|
+
print("Using basic repository analysis to generate setup commands")
|
2755
|
+
|
2756
|
+
# Default commands that work for most repositories
|
2757
|
+
default_commands = [
|
2758
|
+
"apt-get update -y",
|
2759
|
+
"apt-get install -y git curl wget",
|
2760
|
+
"pip install --upgrade pip setuptools wheel"
|
2761
|
+
]
|
2762
|
+
|
2763
|
+
# If we don't have any analysis data, return default commands
|
2764
|
+
if not gitingest_data:
|
2765
|
+
print("⚠️ No repository analysis data available. Using default commands.")
|
2766
|
+
return default_commands
|
2767
|
+
|
2768
|
+
# Extract language and technologies information
|
2769
|
+
detected_language = gitingest_data.get("system_info", {}).get("detected_language", "Unknown")
|
2770
|
+
detected_technologies = gitingest_data.get("system_info", {}).get("detected_technologies", [])
|
2771
|
+
primary_package_manager = gitingest_data.get("system_info", {}).get("primary_package_manager", "Unknown")
|
2772
|
+
|
2773
|
+
# Add language-specific commands
|
2774
|
+
language_commands = []
|
2775
|
+
|
2776
|
+
print(f"📋 Detected primary language: {detected_language}")
|
2777
|
+
print(f"📋 Detected technologies: {', '.join(detected_technologies) if detected_technologies else 'None'}")
|
2778
|
+
print(f"📋 Detected package manager: {primary_package_manager}")
|
2779
|
+
|
2780
|
+
# Python-specific commands
|
2781
|
+
if detected_language == "Python" or primary_package_manager == "pip":
|
2782
|
+
print("📦 Adding Python-specific setup commands")
|
2783
|
+
|
2784
|
+
# Check for requirements.txt
|
2785
|
+
requirements_check = [
|
2786
|
+
"if [ -f requirements.txt ]; then",
|
2787
|
+
" echo 'Installing from requirements.txt'",
|
2788
|
+
" pip install -r requirements.txt",
|
2789
|
+
"elif [ -f setup.py ]; then",
|
2790
|
+
" echo 'Installing from setup.py'",
|
2791
|
+
" pip install -e .",
|
2792
|
+
"fi"
|
2793
|
+
]
|
2794
|
+
language_commands.extend(requirements_check)
|
2795
|
+
|
2796
|
+
# Add common Python packages
|
2797
|
+
language_commands.append("pip install pytest numpy pandas matplotlib")
|
2798
|
+
|
2799
|
+
# JavaScript/Node.js specific commands
|
2800
|
+
elif detected_language in ["JavaScript", "TypeScript"] or primary_package_manager in ["npm", "yarn", "pnpm"]:
|
2801
|
+
print("📦 Adding JavaScript/Node.js-specific setup commands")
|
2802
|
+
|
2803
|
+
# Install Node.js if not available
|
2804
|
+
language_commands.append("apt-get install -y nodejs npm")
|
2805
|
+
|
2806
|
+
# Check for package.json
|
2807
|
+
package_json_check = [
|
2808
|
+
"if [ -f package.json ]; then",
|
2809
|
+
" echo 'Installing from package.json'",
|
2810
|
+
" npm install",
|
2811
|
+
"fi"
|
2812
|
+
]
|
2813
|
+
language_commands.extend(package_json_check)
|
2814
|
+
|
2815
|
+
# Java specific commands
|
2816
|
+
elif detected_language == "Java" or primary_package_manager in ["maven", "gradle"]:
|
2817
|
+
print("📦 Adding Java-specific setup commands")
|
2818
|
+
|
2819
|
+
language_commands.append("apt-get install -y openjdk-11-jdk maven gradle")
|
2820
|
+
|
2821
|
+
# Check for Maven or Gradle
|
2822
|
+
build_check = [
|
2823
|
+
"if [ -f pom.xml ]; then",
|
2824
|
+
" echo 'Building with Maven'",
|
2825
|
+
" mvn clean install -DskipTests",
|
2826
|
+
"elif [ -f build.gradle ]; then",
|
2827
|
+
" echo 'Building with Gradle'",
|
2828
|
+
" gradle build --no-daemon",
|
2829
|
+
"fi"
|
2830
|
+
]
|
2831
|
+
language_commands.extend(build_check)
|
2832
|
+
|
2833
|
+
# Go specific commands
|
2834
|
+
elif detected_language == "Go" or primary_package_manager == "go":
|
2835
|
+
print("📦 Adding Go-specific setup commands")
|
2836
|
+
|
2837
|
+
language_commands.append("apt-get install -y golang-go")
|
2838
|
+
language_commands.append("go mod tidy")
|
2839
|
+
|
2840
|
+
# Rust specific commands
|
2841
|
+
elif detected_language == "Rust" or primary_package_manager == "cargo":
|
2842
|
+
print("📦 Adding Rust-specific setup commands")
|
2843
|
+
|
2844
|
+
language_commands.append("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y")
|
2845
|
+
language_commands.append("source $HOME/.cargo/env")
|
2846
|
+
language_commands.append("cargo build")
|
2847
|
+
|
2848
|
+
# Combine all commands
|
2849
|
+
all_commands = default_commands + language_commands
|
2850
|
+
|
2851
|
+
# Fix the commands
|
2852
|
+
fixed_commands = fix_setup_commands(all_commands)
|
2853
|
+
|
2854
|
+
print("\n📋 Generated fallback setup commands:")
|
2855
|
+
for i, cmd in enumerate(fixed_commands, 1):
|
2856
|
+
print(f" {i}. {cmd}")
|
2857
|
+
|
2858
|
+
return fixed_commands
|
2859
|
+
|
2298
2860
|
def generate_basic_repo_analysis_from_url(repo_url):
|
2299
2861
|
"""Generate basic repository analysis data from a repository URL."""
|
2300
2862
|
import tempfile
|
@@ -2481,17 +3043,327 @@ def get_setup_commands_from_local_api(repo_url, gitingest_data):
|
|
2481
3043
|
if "setupInstructions" in data and "commands" in data["setupInstructions"]:
|
2482
3044
|
commands = data["setupInstructions"]["commands"]
|
2483
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:")
|
2484
3049
|
for i, cmd in enumerate(commands, 1):
|
2485
3050
|
print(f" {i}. {cmd}")
|
2486
|
-
|
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
|
2487
3061
|
except Exception as e:
|
2488
3062
|
print(f"❌ Error connecting to local API: {e}")
|
2489
3063
|
|
2490
3064
|
return None
|
2491
3065
|
|
3066
|
+
# Define a function to create and return a properly configured ssh container function
|
3067
|
+
def create_ssh_container_function(gpu_type="a10g", timeout_minutes=60, volume=None, volume_mount_path="/persistent"):
|
3068
|
+
# Create a new app for this specific container
|
3069
|
+
app_name = f"ssh-container-{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
3070
|
+
ssh_app = modal.App.lookup(app_name, create_if_missing=True)
|
3071
|
+
|
3072
|
+
# Create SSH-enabled image
|
3073
|
+
ssh_image = (
|
3074
|
+
modal.Image.debian_slim()
|
3075
|
+
.apt_install(
|
3076
|
+
"openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
|
3077
|
+
"python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
|
3078
|
+
"gpg", "ca-certificates", "software-properties-common"
|
3079
|
+
)
|
3080
|
+
.pip_install("uv", "modal") # Fast Python package installer and Modal
|
3081
|
+
.run_commands(
|
3082
|
+
# Create SSH directory
|
3083
|
+
"mkdir -p /var/run/sshd",
|
3084
|
+
"mkdir -p /root/.ssh",
|
3085
|
+
"chmod 700 /root/.ssh",
|
3086
|
+
|
3087
|
+
# Configure SSH server
|
3088
|
+
"sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
|
3089
|
+
"sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
|
3090
|
+
"sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
|
3091
|
+
|
3092
|
+
# SSH keep-alive settings
|
3093
|
+
"echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
|
3094
|
+
"echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
|
3095
|
+
|
3096
|
+
# Generate SSH host keys
|
3097
|
+
"ssh-keygen -A",
|
3098
|
+
|
3099
|
+
# Set up a nice bash prompt
|
3100
|
+
"echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
|
3101
|
+
)
|
3102
|
+
)
|
3103
|
+
|
3104
|
+
# Setup volume mount if available
|
3105
|
+
volumes = {}
|
3106
|
+
if volume:
|
3107
|
+
volumes[volume_mount_path] = volume
|
3108
|
+
|
3109
|
+
# Define the function with the specific configuration
|
3110
|
+
@ssh_app.function(
|
3111
|
+
image=ssh_image,
|
3112
|
+
timeout=timeout_minutes * 60, # Convert to seconds
|
3113
|
+
gpu=gpu_type,
|
3114
|
+
cpu=2,
|
3115
|
+
memory=8192,
|
3116
|
+
serialized=True,
|
3117
|
+
volumes=volumes if volumes else None,
|
3118
|
+
)
|
3119
|
+
def ssh_container(ssh_password, repo_url=None, repo_name=None, setup_commands=None):
|
3120
|
+
import subprocess
|
3121
|
+
import time
|
3122
|
+
import os
|
3123
|
+
|
3124
|
+
# Set root password
|
3125
|
+
subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
|
3126
|
+
|
3127
|
+
# Start SSH service
|
3128
|
+
subprocess.run(["service", "ssh", "start"], check=True)
|
3129
|
+
|
3130
|
+
# Setup environment
|
3131
|
+
os.environ['PS1'] = r'\[\e[1;32m\]modal:\[\e[1;34m\]\w\[\e[0m\]$ '
|
3132
|
+
|
3133
|
+
# Clone repository if provided
|
3134
|
+
if repo_url:
|
3135
|
+
repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
|
3136
|
+
print(f"📥 Cloning repository: {repo_url}")
|
3137
|
+
|
3138
|
+
try:
|
3139
|
+
subprocess.run(["git", "clone", repo_url], check=True, cwd="/root")
|
3140
|
+
print(f"✅ Repository cloned successfully: {repo_name_from_url}")
|
3141
|
+
|
3142
|
+
# Change to repository directory
|
3143
|
+
repo_dir = f"/root/{repo_name_from_url}"
|
3144
|
+
if os.path.exists(repo_dir):
|
3145
|
+
os.chdir(repo_dir)
|
3146
|
+
print(f"📂 Changed to repository directory: {repo_dir}")
|
3147
|
+
|
3148
|
+
except subprocess.CalledProcessError as e:
|
3149
|
+
print(f"❌ Failed to clone repository: {e}")
|
3150
|
+
|
3151
|
+
# Run setup commands if provided
|
3152
|
+
if setup_commands:
|
3153
|
+
print(f"⚙️ Running {len(setup_commands)} setup commands...")
|
3154
|
+
for i, cmd in enumerate(setup_commands, 1):
|
3155
|
+
print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
|
3156
|
+
try:
|
3157
|
+
result = subprocess.run(cmd, shell=True, check=True,
|
3158
|
+
capture_output=True, text=True)
|
3159
|
+
if result.stdout:
|
3160
|
+
print(f"✅ Output: {result.stdout}")
|
3161
|
+
except subprocess.CalledProcessError as e:
|
3162
|
+
print(f"❌ Command failed: {e}")
|
3163
|
+
if e.stderr:
|
3164
|
+
print(f"❌ Error: {e.stderr}")
|
3165
|
+
|
3166
|
+
# Get container info
|
3167
|
+
print("🔍 Container started successfully!")
|
3168
|
+
print(f"🆔 Container ID: {os.environ.get('MODAL_TASK_ID', 'unknown')}")
|
3169
|
+
|
3170
|
+
# Keep the container running
|
3171
|
+
while True:
|
3172
|
+
time.sleep(30)
|
3173
|
+
# Check if SSH service is still running
|
3174
|
+
try:
|
3175
|
+
subprocess.run(["service", "ssh", "status"], check=True,
|
3176
|
+
capture_output=True)
|
3177
|
+
except subprocess.CalledProcessError:
|
3178
|
+
print("⚠️ SSH service stopped, restarting...")
|
3179
|
+
subprocess.run(["service", "ssh", "start"], check=True)
|
3180
|
+
|
3181
|
+
# Return the configured function
|
3182
|
+
return ssh_container, app_name
|
3183
|
+
|
3184
|
+
def fix_setup_commands(commands):
|
3185
|
+
"""Fix setup commands by removing placeholders and comments."""
|
3186
|
+
fixed_commands = []
|
3187
|
+
|
3188
|
+
for cmd in commands:
|
3189
|
+
# Remove placeholders like "(or the appropriate entry point...)"
|
3190
|
+
cmd = re.sub(r'\([^)]*\)', '', cmd).strip()
|
3191
|
+
|
3192
|
+
# Skip empty commands or pure comments
|
3193
|
+
if not cmd or cmd.startswith('#'):
|
3194
|
+
continue
|
3195
|
+
|
3196
|
+
# Remove trailing comments
|
3197
|
+
cmd = re.sub(r'#.*$', '', cmd).strip()
|
3198
|
+
|
3199
|
+
if cmd:
|
3200
|
+
fixed_commands.append(cmd)
|
3201
|
+
|
3202
|
+
return fixed_commands
|
3203
|
+
|
3204
|
+
def find_entry_point(repo_dir):
|
3205
|
+
"""Find the entry point script for a repository."""
|
3206
|
+
# Common entry point files to check
|
3207
|
+
common_entry_points = [
|
3208
|
+
"main.py", "app.py", "run.py", "train.py", "start.py",
|
3209
|
+
"server.py", "cli.py", "demo.py", "example.py"
|
3210
|
+
]
|
3211
|
+
|
3212
|
+
# Check if any of the common entry points exist
|
3213
|
+
for entry_point in common_entry_points:
|
3214
|
+
if os.path.exists(os.path.join(repo_dir, entry_point)):
|
3215
|
+
return entry_point
|
3216
|
+
|
3217
|
+
# Look for Python files in the root directory
|
3218
|
+
python_files = [f for f in os.listdir(repo_dir) if f.endswith('.py')]
|
3219
|
+
if python_files:
|
3220
|
+
# Prioritize files with main function or if_name_main pattern
|
3221
|
+
for py_file in python_files:
|
3222
|
+
file_path = os.path.join(repo_dir, py_file)
|
3223
|
+
try:
|
3224
|
+
with open(file_path, 'r') as f:
|
3225
|
+
content = f.read()
|
3226
|
+
if "def main" in content or "if __name__ == '__main__'" in content or 'if __name__ == "__main__"' in content:
|
3227
|
+
return py_file
|
3228
|
+
except:
|
3229
|
+
pass
|
3230
|
+
|
3231
|
+
# If no main function found, return the first Python file
|
3232
|
+
return python_files[0]
|
3233
|
+
|
3234
|
+
return None
|
3235
|
+
|
3236
|
+
def analyze_directory_navigation_with_llm(current_dir, target_dir, current_contents, target_contents, api_key=None):
|
3237
|
+
"""Use LLM to analyze if directory navigation makes sense"""
|
3238
|
+
if not api_key:
|
3239
|
+
# Try to get API key from environment
|
3240
|
+
api_key = os.environ.get("OPENAI_API_KEY")
|
3241
|
+
|
3242
|
+
if not api_key:
|
3243
|
+
print("⚠️ No OpenAI API key available for directory analysis")
|
3244
|
+
return None
|
3245
|
+
|
3246
|
+
# Create analysis prompt
|
3247
|
+
analysis_prompt = f"""
|
3248
|
+
I'm trying to determine if a 'cd {target_dir}' command makes sense.
|
3249
|
+
|
3250
|
+
CURRENT DIRECTORY: {current_dir}
|
3251
|
+
Current directory contents:
|
3252
|
+
{current_contents}
|
3253
|
+
|
3254
|
+
TARGET DIRECTORY: {target_dir}
|
3255
|
+
Target directory contents:
|
3256
|
+
{target_contents}
|
3257
|
+
|
3258
|
+
Please analyze if navigating to the target directory makes sense by considering:
|
3259
|
+
1. Are the contents significantly different?
|
3260
|
+
2. Does the target directory contain important files (like source code, config files, etc.)?
|
3261
|
+
3. Is this likely a nested project directory or just a duplicate?
|
3262
|
+
4. Would navigating provide access to different functionality or files?
|
3263
|
+
|
3264
|
+
Respond with only 'NAVIGATE' if navigation makes sense, or 'SKIP' if it's redundant.
|
3265
|
+
"""
|
3266
|
+
|
3267
|
+
# Prepare the API request
|
3268
|
+
headers = {
|
3269
|
+
"Content-Type": "application/json",
|
3270
|
+
"Authorization": f"Bearer {api_key}"
|
3271
|
+
}
|
3272
|
+
|
3273
|
+
payload = {
|
3274
|
+
"model": "gpt-4",
|
3275
|
+
"messages": [
|
3276
|
+
{"role": "system", "content": "You are a directory navigation assistant. Analyze if navigating to a target directory makes sense based on the contents of both directories. Respond with only 'NAVIGATE' or 'SKIP'."},
|
3277
|
+
{"role": "user", "content": analysis_prompt}
|
3278
|
+
],
|
3279
|
+
"temperature": 0.1,
|
3280
|
+
"max_tokens": 50
|
3281
|
+
}
|
3282
|
+
|
3283
|
+
try:
|
3284
|
+
print("🤖 Calling OpenAI for directory navigation analysis...")
|
3285
|
+
response = requests.post(
|
3286
|
+
"https://api.openai.com/v1/chat/completions",
|
3287
|
+
headers=headers,
|
3288
|
+
json=payload,
|
3289
|
+
timeout=30
|
3290
|
+
)
|
3291
|
+
|
3292
|
+
if response.status_code == 200:
|
3293
|
+
result = response.json()
|
3294
|
+
llm_response = result["choices"][0]["message"]["content"].strip()
|
3295
|
+
print(f"🤖 LLM Response: {llm_response}")
|
3296
|
+
return llm_response
|
3297
|
+
else:
|
3298
|
+
print(f"❌ OpenAI API error: {response.status_code} - {response.text}")
|
3299
|
+
return None
|
3300
|
+
except Exception as e:
|
3301
|
+
print(f"❌ Error calling OpenAI API: {e}")
|
3302
|
+
return None
|
3303
|
+
|
3304
|
+
def cleanup_modal_token():
|
3305
|
+
"""Delete Modal token files and environment variables after SSH container is started"""
|
3306
|
+
print("🧹 Cleaning up Modal token for security...")
|
3307
|
+
|
3308
|
+
try:
|
3309
|
+
# Remove token from environment variables
|
3310
|
+
if "MODAL_TOKEN_ID" in os.environ:
|
3311
|
+
del os.environ["MODAL_TOKEN_ID"]
|
3312
|
+
print("✅ Removed MODAL_TOKEN_ID from environment")
|
3313
|
+
|
3314
|
+
if "MODAL_TOKEN" in os.environ:
|
3315
|
+
del os.environ["MODAL_TOKEN"]
|
3316
|
+
print("✅ Removed MODAL_TOKEN from environment")
|
3317
|
+
|
3318
|
+
if "MODAL_TOKEN_SECRET" in os.environ:
|
3319
|
+
del os.environ["MODAL_TOKEN_SECRET"]
|
3320
|
+
print("✅ Removed MODAL_TOKEN_SECRET from environment")
|
3321
|
+
|
3322
|
+
# Delete ~/.modal.toml file
|
3323
|
+
home_dir = os.path.expanduser("~")
|
3324
|
+
modal_toml = os.path.join(home_dir, ".modal.toml")
|
3325
|
+
if os.path.exists(modal_toml):
|
3326
|
+
os.remove(modal_toml)
|
3327
|
+
print(f"✅ Deleted Modal token file at {modal_toml}")
|
3328
|
+
|
3329
|
+
print("✅ Modal token cleanup completed successfully")
|
3330
|
+
except Exception as e:
|
3331
|
+
print(f"❌ Error during Modal token cleanup: {e}")
|
3332
|
+
|
3333
|
+
def show_usage_examples():
|
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")
|
3360
|
+
print(" T4, L4, A10G, A100-40GB, A100-80GB, L40S, H100, H200, B200")
|
3361
|
+
print("")
|
3362
|
+
|
2492
3363
|
if __name__ == "__main__":
|
2493
3364
|
# Parse command line arguments when script is run directly
|
2494
3365
|
import argparse
|
3366
|
+
import sys
|
2495
3367
|
|
2496
3368
|
parser = argparse.ArgumentParser(description='Create a Modal SSH container with GPU')
|
2497
3369
|
parser.add_argument('--gpu', type=str, default='A10G', help='GPU type (default: A10G)')
|
@@ -2506,12 +3378,79 @@ if __name__ == "__main__":
|
|
2506
3378
|
parser.add_argument('--timeout', type=int, default=60, help='Container timeout in minutes (default: 60)')
|
2507
3379
|
parser.add_argument('--ssh-password', type=str, help='SSH password (random if not provided)')
|
2508
3380
|
parser.add_argument('--use-api', action='store_true', help='Fetch setup commands from API')
|
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')
|
2509
3383
|
|
2510
3384
|
args = parser.parse_args()
|
2511
3385
|
|
3386
|
+
# If no arguments or only --show-examples is provided, show usage examples
|
3387
|
+
if len(sys.argv) == 1 or args.show_examples:
|
3388
|
+
show_usage_examples()
|
3389
|
+
sys.exit(0)
|
3390
|
+
|
2512
3391
|
# Get setup commands from file if specified
|
2513
3392
|
setup_commands = args.setup_commands or []
|
2514
3393
|
|
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.")
|
3401
|
+
sys.exit(1)
|
3402
|
+
|
3403
|
+
# Ask about persistent volume
|
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
|
3412
|
+
|
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
|
3419
|
+
|
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}")
|
3425
|
+
|
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:
|
3430
|
+
try:
|
3431
|
+
gpu_index = int(gpu_choice) - 1
|
3432
|
+
if 0 <= gpu_index < len(gpu_options):
|
3433
|
+
args.gpu = gpu_options[gpu_index]
|
3434
|
+
else:
|
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
|
+
|
2515
3454
|
# If --use-api flag is set and repo_url is provided, fetch setup commands from API
|
2516
3455
|
if args.use_api and args.repo_url:
|
2517
3456
|
print("🔄 Using API to fetch setup commands")
|
@@ -2575,70 +3514,7 @@ if __name__ == "__main__":
|
|
2575
3514
|
|
2576
3515
|
except Exception as e:
|
2577
3516
|
print(f"⚠️ Error reading commands file: {e}")
|
2578
|
-
|
2579
|
-
# Execute setup script if provided
|
2580
|
-
if args.setup_script:
|
2581
|
-
print(f"📜 Setup script path: {args.setup_script}")
|
2582
|
-
|
2583
|
-
# Verify script exists
|
2584
|
-
if os.path.exists(args.setup_script):
|
2585
|
-
print(f"✅ Script exists at: {args.setup_script}")
|
2586
|
-
|
2587
|
-
# Check if script is executable
|
2588
|
-
if not os.access(args.setup_script, os.X_OK):
|
2589
|
-
print(f"⚠️ Script is not executable, setting permissions...")
|
2590
|
-
try:
|
2591
|
-
os.chmod(args.setup_script, 0o755)
|
2592
|
-
print(f"✅ Set executable permissions on script")
|
2593
|
-
except Exception as e:
|
2594
|
-
print(f"❌ Failed to set permissions: {e}")
|
2595
|
-
|
2596
|
-
working_dir = args.working_dir or os.getcwd()
|
2597
|
-
print(f"📂 Using working directory: {working_dir}")
|
2598
|
-
|
2599
|
-
# Execute the script directly instead of through container
|
2600
|
-
try:
|
2601
|
-
print(f"🔄 Executing script directly: bash {args.setup_script} {working_dir}")
|
2602
|
-
result = subprocess.run(['bash', args.setup_script, working_dir],
|
2603
|
-
capture_output=True, text=True)
|
2604
|
-
|
2605
|
-
print(f"📋 Script output:")
|
2606
|
-
print(result.stdout)
|
2607
|
-
|
2608
|
-
if result.returncode != 0:
|
2609
|
-
print(f"❌ Script execution failed with error code {result.returncode}")
|
2610
|
-
print(f"Error output: {result.stderr}")
|
2611
|
-
else:
|
2612
|
-
print(f"✅ Script executed successfully")
|
2613
|
-
|
2614
|
-
# Skip the regular setup commands since we executed the script directly
|
2615
|
-
setup_commands = []
|
2616
|
-
except Exception as e:
|
2617
|
-
print(f"❌ Failed to execute script: {e}")
|
2618
|
-
# Fall back to running the script through container
|
2619
|
-
setup_commands = [f"bash {args.setup_script} {working_dir}"]
|
2620
|
-
print("🔄 Falling back to running script through container")
|
2621
|
-
else:
|
2622
|
-
print(f"❌ Script not found at: {args.setup_script}")
|
2623
|
-
# Try to find the script in common locations
|
2624
|
-
possible_paths = [
|
2625
|
-
os.path.join(os.path.expanduser('~'), os.path.basename(args.setup_script)),
|
2626
|
-
os.path.join('/tmp', os.path.basename(args.setup_script)),
|
2627
|
-
os.path.join('/var/tmp', os.path.basename(args.setup_script))
|
2628
|
-
]
|
2629
|
-
|
2630
|
-
found = False
|
2631
|
-
for test_path in possible_paths:
|
2632
|
-
if os.path.exists(test_path):
|
2633
|
-
print(f"🔍 Found script at alternative location: {test_path}")
|
2634
|
-
setup_commands = [f"bash {test_path} {args.working_dir or os.getcwd()}"]
|
2635
|
-
found = True
|
2636
|
-
break
|
2637
3517
|
|
2638
|
-
if not found:
|
2639
|
-
print("❌ Could not find script in any location")
|
2640
|
-
setup_commands = []
|
2641
|
-
|
2642
3518
|
try:
|
2643
3519
|
result = create_modal_ssh_container(
|
2644
3520
|
args.gpu,
|
@@ -2657,79 +3533,6 @@ if __name__ == "__main__":
|
|
2657
3533
|
print(".", end="", flush=True)
|
2658
3534
|
except KeyboardInterrupt:
|
2659
3535
|
print("\n👋 Script exited. The SSH container will continue running.")
|
2660
|
-
if 'result' in locals() and result:
|
2661
|
-
container_id = None
|
2662
|
-
ssh_password = None
|
2663
|
-
|
2664
|
-
# Try to get container ID and SSH password from the result dictionary
|
2665
|
-
if isinstance(result, dict):
|
2666
|
-
container_id = result.get('container_id')
|
2667
|
-
ssh_password = result.get('ssh_password')
|
2668
|
-
elif hasattr(result, 'container_id'):
|
2669
|
-
container_id = result.container_id
|
2670
|
-
ssh_password = getattr(result, 'ssh_password', None)
|
2671
|
-
|
2672
|
-
# If we still don't have the container ID, try to read it from the file
|
2673
|
-
if not container_id:
|
2674
|
-
try:
|
2675
|
-
with open(os.path.expanduser("~/.modal_last_container_id"), "r") as f:
|
2676
|
-
container_id = f.read().strip()
|
2677
|
-
print(f"📋 Retrieved container ID from file: {container_id}")
|
2678
|
-
except Exception as e:
|
2679
|
-
print(f"⚠️ Could not read container ID from file: {e}")
|
2680
|
-
|
2681
|
-
if container_id:
|
2682
|
-
print(f"🚀 SSH connection information:")
|
2683
|
-
print(f" ssh root@{container_id}.modal.run")
|
2684
|
-
if ssh_password:
|
2685
|
-
print(f" Password: {ssh_password}")
|
2686
|
-
|
2687
|
-
# Try to open a new terminal window with SSH connection
|
2688
|
-
try:
|
2689
|
-
terminal_script = f'''
|
2690
|
-
tell application "Terminal"
|
2691
|
-
do script "echo 'Connecting to Modal SSH container...'; echo 'Password: {ssh_password or 'unknown'}'; ssh root@{container_id}.modal.run"
|
2692
|
-
activate
|
2693
|
-
end tell
|
2694
|
-
'''
|
2695
|
-
|
2696
|
-
subprocess.run(['osascript', '-e', terminal_script],
|
2697
|
-
capture_output=True, text=True, timeout=30)
|
2698
|
-
print("✅ New terminal window opened with SSH connection")
|
2699
|
-
|
2700
|
-
except Exception as e:
|
2701
|
-
print(f"⚠️ Failed to open terminal window: {e}")
|
2702
|
-
|
2703
|
-
# Try alternative approach with iTerm2
|
2704
|
-
try:
|
2705
|
-
iterm_script = f'''
|
2706
|
-
tell application "iTerm"
|
2707
|
-
create window with default profile
|
2708
|
-
tell current session of current window
|
2709
|
-
write text "echo 'Connecting to Modal SSH container...'; echo 'Password: {ssh_password or 'unknown'}'; ssh root@{container_id}.modal.run"
|
2710
|
-
end tell
|
2711
|
-
end tell
|
2712
|
-
'''
|
2713
|
-
|
2714
|
-
subprocess.run(['osascript', '-e', iterm_script],
|
2715
|
-
capture_output=True, text=True, timeout=30)
|
2716
|
-
print("✅ New iTerm2 window opened with SSH connection")
|
2717
|
-
|
2718
|
-
except Exception as e2:
|
2719
|
-
print(f"⚠️ Failed to open iTerm2 window: {e2}")
|
2720
|
-
print("📝 You can manually connect using:")
|
2721
|
-
print(f" ssh root@{container_id}.modal.run")
|
2722
|
-
if ssh_password:
|
2723
|
-
print(f" Password: {ssh_password}")
|
2724
|
-
print(" Or use Modal exec:")
|
2725
|
-
print(f" modal container exec --pty {container_id} bash")
|
2726
|
-
else:
|
2727
|
-
print("⚠️ Could not determine container ID")
|
2728
|
-
print("📝 You can manually connect using:")
|
2729
|
-
print(" modal container list")
|
2730
|
-
print(" modal container exec --pty <CONTAINER_ID> bash")
|
2731
|
-
|
2732
|
-
# Exit cleanly
|
2733
3536
|
sys.exit(0)
|
2734
3537
|
|
2735
3538
|
except KeyboardInterrupt:
|