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.
@@ -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
- # Get API key from user
60
- api_key = handle_interactive_input(
61
- "🔑 WEIGHTS & BIASES API KEY REQUIRED\n" +
62
- "Please paste your W&B API key below:\n" +
63
- "(Your API key should be 40 characters long)",
64
- is_password=True
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
- print("\n" + "="*60)
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
- api_key = getpass.getpass("OpenAI API Key: ").strip()
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
- print("✅ API key received successfully!")
243
- except KeyboardInterrupt:
244
- print("\n API key input cancelled by user.")
245
- return None
246
- except Exception as e:
247
- print(f" Error getting API key: {e}")
248
- return None
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
- 'T4': {'gpu': 'T4', 'memory': 16},
460
- 'V100': {'gpu': 'V100-SXM2-16GB', 'memory': 16}
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
- # Additional check: verify if there's actually a nested directory with this name
756
- # This prevents skipping legitimate navigation to nested directories
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
- # The nested directory exists, so this is NOT redundant
763
- print(f"🔍 Detected nested directory '{target_dir}' exists in current location")
764
- print(f"📂 Current: {current_dir}")
765
- print(f"🎯 Target: {target_dir}")
766
- print(f"🔄 Proceeding with navigation to nested directory...")
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
- 'T4': {'gpu': 't4', 'memory': 16},
1875
- 'V100': {'gpu': 'v100', 'memory': 16}
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
- ssh_image = (
1918
- modal.Image.debian_slim()
1919
- .apt_install(
1920
- "openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
1921
- "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
1922
- "gpg", "ca-certificates", "software-properties-common"
1923
- )
1924
- .pip_install("uv") # Fast Python package installer
1925
- .run_commands(
1926
- # Create SSH directory
1927
- "mkdir -p /var/run/sshd",
1928
- "mkdir -p /root/.ssh",
1929
- "chmod 700 /root/.ssh",
1930
-
1931
- # Configure SSH server
1932
- "sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
1933
- "sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
1934
- "sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
1935
-
1936
- # SSH keep-alive settings
1937
- "echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
1938
- "echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
1939
-
1940
- # Generate SSH host keys
1941
- "ssh-keygen -A",
1942
-
1943
- # Set up a nice bash prompt
1944
- "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
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
- # Create Modal app
1949
- with modal.enable_output():
1950
- print(f"🚀 Creating Modal app: {app_name}")
1951
- app = modal.App.lookup(app_name, create_if_missing=True)
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
- # Setup volume mount if available
1954
- volumes = {}
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
- @app.function(
1960
- image=ssh_image,
1961
- timeout=timeout_minutes * 60, # Convert to seconds
1962
- gpu=gpu_spec['gpu'],
1963
- cpu=2,
1964
- memory=8192,
1965
- serialized=True,
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
- # Clone repository if provided
1983
- if repo_url:
1984
- repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
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(["git", "clone", repo_url], check=True, cwd="/root")
1989
- print(f"✅ Repository cloned successfully: {repo_name_from_url}")
1990
-
1991
- # Change to repository directory
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"❌ Failed to clone repository: {e}")
1999
-
2000
- # Run setup commands if provided
2001
- if setup_commands:
2002
- print(f"⚙️ Running {len(setup_commands)} setup commands...")
2003
- for i, cmd in enumerate(setup_commands, 1):
2004
- print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
2005
- try:
2006
- result = subprocess.run(cmd, shell=True, check=True,
2007
- capture_output=True, text=True)
2008
- if result.stdout:
2009
- print(f" Output: {result.stdout}")
2010
- except subprocess.CalledProcessError as e:
2011
- print(f" Command failed: {e}")
2012
- if e.stderr:
2013
- print(f"❌ Error: {e.stderr}")
2014
-
2015
- # Get container info
2016
- print("🔍 Container started successfully!")
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
- print("🚀 Starting SSH container...")
2032
-
2033
- # Use spawn to run the container in the background
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
- # Try to get SSH connection details
2089
- try:
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 = "http://localhost:3000/api/analyze-with-gitingest"
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
- response = requests.post(api_url, json=payload, timeout=60)
2252
-
2253
- print(f"📥 API Response status code: {response.status_code}")
2254
-
2255
- if response.status_code == 200:
2256
- try:
2257
- data = response.json()
2258
- print(f"📄 API Response data received")
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
- return commands
2270
- else:
2271
- print("⚠️ API response did not contain setupInstructions.commands field")
2272
- print("📋 Available fields in response:")
2273
- for key in data.keys():
2274
- print(f" - {key}")
2275
- return []
2276
- except json.JSONDecodeError as e:
2277
- print(f" Failed to parse API response as JSON: {e}")
2278
- print(f"Raw response: {response.text[:500]}...")
2279
- return []
2280
- else:
2281
- print(f"❌ API request failed with status code: {response.status_code}")
2282
- print(f"Error response: {response.text[:500]}...")
2283
- return []
2284
- except requests.exceptions.ConnectionError:
2285
- print(f" Connection error: Could not connect to {api_url}")
2286
- print("⚠️ Make sure the API server is running at localhost:3000")
2287
- return []
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
- return []
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
- return commands
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: