gitarsenal-cli 1.0.5 → 1.0.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitarsenal-cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -8,6 +8,8 @@ import re
8
8
  import datetime
9
9
  import getpass
10
10
  import requests
11
+ import secrets
12
+ import string
11
13
 
12
14
  def handle_interactive_input(prompt, is_password=False):
13
15
  """Handle interactive input from the user with optional password masking"""
@@ -1808,6 +1810,324 @@ cd "{current_dir}"
1808
1810
  "sandbox_id": sandbox_id
1809
1811
  }
1810
1812
 
1813
+
1814
+ def handle_interactive_input(prompt, is_password=False):
1815
+ """Handle interactive input from the user with optional password masking"""
1816
+ print("\n" + "="*60)
1817
+ print(f"{prompt}")
1818
+ print("="*60)
1819
+
1820
+ try:
1821
+ if is_password:
1822
+ user_input = getpass.getpass("Input (hidden): ").strip()
1823
+ else:
1824
+ user_input = input("Input: ").strip()
1825
+
1826
+ if not user_input:
1827
+ print("❌ No input provided.")
1828
+ return None
1829
+ print("✅ Input received successfully!")
1830
+ return user_input
1831
+ except KeyboardInterrupt:
1832
+ print("\n❌ Input cancelled by user.")
1833
+ return None
1834
+ except Exception as e:
1835
+ print(f"❌ Error getting input: {e}")
1836
+ return None
1837
+
1838
+ def generate_random_password(length=16):
1839
+ """Generate a random password for SSH access"""
1840
+ alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
1841
+ password = ''.join(secrets.choice(alphabet) for i in range(length))
1842
+ return password
1843
+
1844
+
1845
+
1846
+ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_commands=None,
1847
+ volume_name=None, timeout_minutes=60, ssh_password=None):
1848
+ """Create a Modal SSH container with GPU support"""
1849
+
1850
+ # Generate a unique app name with timestamp to avoid conflicts
1851
+ timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
1852
+ app_name = f"ssh-container-{timestamp}"
1853
+
1854
+ gpu_configs = {
1855
+ 'A10G': {'gpu': 'a10g', 'memory': 24},
1856
+ 'A100': {'gpu': 'a100', 'memory': 40},
1857
+ 'H100': {'gpu': 'h100', 'memory': 80},
1858
+ 'T4': {'gpu': 't4', 'memory': 16},
1859
+ 'V100': {'gpu': 'v100', 'memory': 16}
1860
+ }
1861
+
1862
+ if gpu_type not in gpu_configs:
1863
+ print(f"⚠️ Unknown GPU type: {gpu_type}. Using A10G as default.")
1864
+ gpu_type = 'A10G'
1865
+
1866
+ gpu_spec = gpu_configs[gpu_type]
1867
+ print(f"🚀 Creating Modal SSH container with {gpu_spec['gpu']} GPU ({gpu_spec['memory']}GB VRAM)")
1868
+
1869
+ # Generate or use provided SSH password
1870
+ if not ssh_password:
1871
+ ssh_password = generate_random_password()
1872
+ print(f"🔐 Generated SSH password: {ssh_password}")
1873
+
1874
+ # Setup volume if specified
1875
+ volume = None
1876
+ volume_mount_path = "/persistent"
1877
+
1878
+ if volume_name:
1879
+ print(f"📦 Setting up volume: {volume_name}")
1880
+ try:
1881
+ volume = modal.Volume.from_name(volume_name, create_if_missing=True)
1882
+ print(f"✅ Volume '{volume_name}' ready for use")
1883
+ except Exception as e:
1884
+ print(f"⚠️ Could not setup volume '{volume_name}': {e}")
1885
+ print("⚠️ Continuing without persistent volume")
1886
+ volume = None
1887
+ else:
1888
+ # Create a default volume for this session
1889
+ default_volume_name = f"ssh-vol-{timestamp}"
1890
+ print(f"📦 Creating default volume: {default_volume_name}")
1891
+ try:
1892
+ volume = modal.Volume.from_name(default_volume_name, create_if_missing=True)
1893
+ volume_name = default_volume_name
1894
+ print(f"✅ Default volume '{default_volume_name}' created")
1895
+ except Exception as e:
1896
+ print(f"⚠️ Could not create default volume: {e}")
1897
+ print("⚠️ Continuing without persistent volume")
1898
+ volume = None
1899
+
1900
+ # Create SSH-enabled image
1901
+ ssh_image = (
1902
+ modal.Image.debian_slim()
1903
+ .apt_install(
1904
+ "openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
1905
+ "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
1906
+ "gpg", "ca-certificates", "software-properties-common"
1907
+ )
1908
+ .pip_install("uv") # Fast Python package installer
1909
+ .run_commands(
1910
+ # Create SSH directory
1911
+ "mkdir -p /var/run/sshd",
1912
+ "mkdir -p /root/.ssh",
1913
+ "chmod 700 /root/.ssh",
1914
+
1915
+ # Configure SSH server
1916
+ "sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
1917
+ "sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
1918
+ "sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
1919
+
1920
+ # SSH keep-alive settings
1921
+ "echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
1922
+ "echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
1923
+
1924
+ # Generate SSH host keys
1925
+ "ssh-keygen -A",
1926
+
1927
+ # Set up a nice bash prompt
1928
+ "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
1929
+ )
1930
+ )
1931
+
1932
+ # Create Modal app
1933
+ with modal.enable_output():
1934
+ print(f"🚀 Creating Modal app: {app_name}")
1935
+ app = modal.App.lookup(app_name, create_if_missing=True)
1936
+
1937
+ # Setup volume mount if available
1938
+ volumes = {}
1939
+ if volume:
1940
+ volumes[volume_mount_path] = volume
1941
+ print(f"📦 Mounting volume '{volume_name}' at {volume_mount_path}")
1942
+
1943
+ @app.function(
1944
+ image=ssh_image,
1945
+ timeout=timeout_minutes * 60, # Convert to seconds
1946
+ gpu=gpu_spec['gpu'],
1947
+ cpu=2,
1948
+ memory=8192,
1949
+ serialized=True,
1950
+ volumes=volumes if volumes else None,
1951
+ )
1952
+ def ssh_container():
1953
+ import subprocess
1954
+ import time
1955
+ import os
1956
+
1957
+ # Set root password
1958
+ subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
1959
+
1960
+ # Start SSH service
1961
+ subprocess.run(["service", "ssh", "start"], check=True)
1962
+
1963
+ # Setup environment
1964
+ os.environ['PS1'] = r'\[\e[1;32m\]modal:\[\e[1;34m\]\w\[\e[0m\]$ '
1965
+
1966
+ # Clone repository if provided
1967
+ if repo_url:
1968
+ repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
1969
+ print(f"📥 Cloning repository: {repo_url}")
1970
+
1971
+ try:
1972
+ subprocess.run(["git", "clone", repo_url], check=True, cwd="/root")
1973
+ print(f"✅ Repository cloned successfully: {repo_name_from_url}")
1974
+
1975
+ # Change to repository directory
1976
+ repo_dir = f"/root/{repo_name_from_url}"
1977
+ if os.path.exists(repo_dir):
1978
+ os.chdir(repo_dir)
1979
+ print(f"📂 Changed to repository directory: {repo_dir}")
1980
+
1981
+ except subprocess.CalledProcessError as e:
1982
+ print(f"❌ Failed to clone repository: {e}")
1983
+
1984
+ # Run setup commands if provided
1985
+ if setup_commands:
1986
+ print(f"⚙️ Running {len(setup_commands)} setup commands...")
1987
+ for i, cmd in enumerate(setup_commands, 1):
1988
+ print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
1989
+ try:
1990
+ result = subprocess.run(cmd, shell=True, check=True,
1991
+ capture_output=True, text=True)
1992
+ if result.stdout:
1993
+ print(f"✅ Output: {result.stdout}")
1994
+ except subprocess.CalledProcessError as e:
1995
+ print(f"❌ Command failed: {e}")
1996
+ if e.stderr:
1997
+ print(f"❌ Error: {e.stderr}")
1998
+
1999
+ # Get container info
2000
+ print("🔍 Container started successfully!")
2001
+ print(f"🆔 Container ID: {os.environ.get('MODAL_TASK_ID', 'unknown')}")
2002
+
2003
+ # Keep the container running
2004
+ while True:
2005
+ time.sleep(30)
2006
+ # Check if SSH service is still running
2007
+ try:
2008
+ subprocess.run(["service", "ssh", "status"], check=True,
2009
+ capture_output=True)
2010
+ except subprocess.CalledProcessError:
2011
+ print("⚠️ SSH service stopped, restarting...")
2012
+ subprocess.run(["service", "ssh", "start"], check=True)
2013
+
2014
+ # Start the container
2015
+ print("🚀 Starting SSH container...")
2016
+
2017
+ # Use spawn to run the container in the background
2018
+ container_handle = ssh_container.spawn()
2019
+
2020
+ # Wait a moment for the container to start
2021
+ print("⏳ Waiting for container to initialize...")
2022
+ time.sleep(10)
2023
+
2024
+ # Get container information
2025
+ try:
2026
+ # Try to get the container ID from Modal
2027
+ container_id = None
2028
+
2029
+ # Get container list to find our container
2030
+ print("🔍 Looking for container information...")
2031
+ result = subprocess.run(["modal", "container", "list", "--json"],
2032
+ capture_output=True, text=True)
2033
+
2034
+ if result.returncode == 0:
2035
+ try:
2036
+ containers = json.loads(result.stdout)
2037
+ if containers and isinstance(containers, list):
2038
+ # Find the most recent container
2039
+ for container in containers:
2040
+ if container.get("App") == app_name:
2041
+ container_id = container.get("Container ID")
2042
+ break
2043
+
2044
+ if not container_id and containers:
2045
+ # Fall back to the first container
2046
+ container_id = containers[0].get("Container ID")
2047
+
2048
+ except json.JSONDecodeError:
2049
+ pass
2050
+
2051
+ if not container_id:
2052
+ # Try text parsing
2053
+ result = subprocess.run(["modal", "container", "list"],
2054
+ capture_output=True, text=True)
2055
+ if result.returncode == 0:
2056
+ lines = result.stdout.split('\n')
2057
+ for line in lines:
2058
+ if app_name in line or ('ta-' in line and '│' in line):
2059
+ parts = line.split('│')
2060
+ if len(parts) >= 2:
2061
+ possible_id = parts[1].strip()
2062
+ if possible_id.startswith('ta-'):
2063
+ container_id = possible_id
2064
+ break
2065
+
2066
+ if container_id:
2067
+ print(f"📋 Container ID: {container_id}")
2068
+
2069
+ # Get the external IP for SSH access
2070
+ print("🔍 Getting container connection info...")
2071
+
2072
+ # Try to get SSH connection details
2073
+ try:
2074
+ # Modal containers typically expose SSH on port 22
2075
+ ssh_info = f"ssh root@{container_id}.modal.run"
2076
+
2077
+ print("\n" + "="*80)
2078
+ print("🚀 SSH CONTAINER READY!")
2079
+ print("="*80)
2080
+ print(f"🆔 Container ID: {container_id}")
2081
+ print(f"🔐 SSH Password: {ssh_password}")
2082
+ print(f"📱 App Name: {app_name}")
2083
+ if volume:
2084
+ print(f"💾 Volume: {volume_name} (mounted at {volume_mount_path})")
2085
+ print("\n🔗 SSH Connection:")
2086
+ print(f" {ssh_info}")
2087
+ print(f" Password: {ssh_password}")
2088
+ print("\n💡 Alternative connection methods:")
2089
+ print(f" modal container exec --pty {container_id} bash")
2090
+ print(f" modal shell {container_id}")
2091
+ print("="*80)
2092
+
2093
+ # Try to open SSH connection in a new terminal
2094
+ try:
2095
+ terminal_script = f'''
2096
+ tell application "Terminal"
2097
+ do script "echo 'Connecting to Modal SSH container...'; echo 'Password: {ssh_password}'; {ssh_info}"
2098
+ activate
2099
+ end tell
2100
+ '''
2101
+
2102
+ subprocess.run(['osascript', '-e', terminal_script],
2103
+ capture_output=True, text=True, timeout=30)
2104
+ print("✅ New terminal window opened with SSH connection")
2105
+
2106
+ except Exception as e:
2107
+ print(f"⚠️ Could not open terminal window: {e}")
2108
+ print("📝 You can manually connect using the SSH command above")
2109
+
2110
+ except Exception as e:
2111
+ print(f"⚠️ Error getting SSH connection info: {e}")
2112
+ print("📝 You can connect using:")
2113
+ print(f" modal container exec --pty {container_id} bash")
2114
+ else:
2115
+ print("⚠️ Could not determine container ID")
2116
+ print("📝 Check running containers with: modal container list")
2117
+
2118
+ except Exception as e:
2119
+ print(f"❌ Error getting container information: {e}")
2120
+
2121
+ # Return container information
2122
+ return {
2123
+ "container_handle": container_handle,
2124
+ "container_id": container_id,
2125
+ "app_name": app_name,
2126
+ "ssh_password": ssh_password,
2127
+ "volume_name": volume_name,
2128
+ "volume_mount_path": volume_mount_path if volume else None
2129
+ }
2130
+
1811
2131
  def fetch_setup_commands_from_api(repo_url):
1812
2132
  """Fetch setup commands from the GitIngest API using real repository analysis."""
1813
2133
  import tempfile
@@ -2157,7 +2477,7 @@ if __name__ == "__main__":
2157
2477
  # Parse command line arguments when script is run directly
2158
2478
  import argparse
2159
2479
 
2160
- parser = argparse.ArgumentParser(description='Create a Modal sandbox with GPU')
2480
+ parser = argparse.ArgumentParser(description='Create a Modal SSH container with GPU')
2161
2481
  parser.add_argument('--gpu', type=str, default='A10G', help='GPU type (default: A10G)')
2162
2482
  parser.add_argument('--repo-url', type=str, help='Repository URL to clone')
2163
2483
  parser.add_argument('--repo-name', type=str, help='Repository name override')
@@ -2167,6 +2487,8 @@ if __name__ == "__main__":
2167
2487
  parser.add_argument('--setup-script', type=str, help='Path to bash script containing setup commands')
2168
2488
  parser.add_argument('--working-dir', type=str, help='Working directory for the setup script')
2169
2489
  parser.add_argument('--volume-name', type=str, help='Name of the Modal volume for persistent storage')
2490
+ parser.add_argument('--timeout', type=int, default=60, help='Container timeout in minutes (default: 60)')
2491
+ parser.add_argument('--ssh-password', type=str, help='SSH password (random if not provided)')
2170
2492
  parser.add_argument('--use-api', action='store_true', help='Fetch setup commands from API')
2171
2493
 
2172
2494
  args = parser.parse_args()
@@ -2207,6 +2529,7 @@ if __name__ == "__main__":
2207
2529
  for i, cmd in enumerate(setup_commands, 1):
2208
2530
  print(f" {i}. {cmd}")
2209
2531
 
2532
+ # Load commands from file if specified
2210
2533
  if args.commands_file and os.path.exists(args.commands_file):
2211
2534
  try:
2212
2535
  with open(args.commands_file, 'r') as f:
@@ -2257,7 +2580,7 @@ if __name__ == "__main__":
2257
2580
  working_dir = args.working_dir or os.getcwd()
2258
2581
  print(f"📂 Using working directory: {working_dir}")
2259
2582
 
2260
- # Execute the script directly instead of through sandbox
2583
+ # Execute the script directly instead of through container
2261
2584
  try:
2262
2585
  print(f"🔄 Executing script directly: bash {args.setup_script} {working_dir}")
2263
2586
  result = subprocess.run(['bash', args.setup_script, working_dir],
@@ -2276,9 +2599,9 @@ if __name__ == "__main__":
2276
2599
  setup_commands = []
2277
2600
  except Exception as e:
2278
2601
  print(f"❌ Failed to execute script: {e}")
2279
- # Fall back to running the script through sandbox
2602
+ # Fall back to running the script through container
2280
2603
  setup_commands = [f"bash {args.setup_script} {working_dir}"]
2281
- print("🔄 Falling back to running script through sandbox")
2604
+ print("🔄 Falling back to running script through container")
2282
2605
  else:
2283
2606
  print(f"❌ Script not found at: {args.setup_script}")
2284
2607
  # Try to find the script in common locations
@@ -2301,32 +2624,34 @@ if __name__ == "__main__":
2301
2624
  setup_commands = []
2302
2625
 
2303
2626
  try:
2304
- result = create_modal_sandbox(
2305
- args.gpu,
2306
- args.repo_url,
2307
- args.repo_name,
2627
+ result = create_modal_ssh_container(
2628
+ args.gpu,
2629
+ args.repo_url,
2630
+ args.repo_name,
2308
2631
  setup_commands,
2309
- getattr(args, 'volume_name', None)
2632
+ getattr(args, 'volume_name', None),
2633
+ args.timeout,
2634
+ args.ssh_password
2310
2635
  )
2311
2636
 
2312
- print("\n⏳ Keeping the sandbox alive. Press Ctrl+C to exit (sandbox will continue running)...")
2637
+ print("\n⏳ Keeping the SSH container alive. Press Ctrl+C to exit (container will continue running)...")
2313
2638
  try:
2314
2639
  while True:
2315
2640
  time.sleep(90)
2316
2641
  print(".", end="", flush=True)
2317
2642
  except KeyboardInterrupt:
2318
- print("\n👋 Script exited. The sandbox will continue running.")
2643
+ print("\n👋 Script exited. The SSH container will continue running.")
2319
2644
  if 'result' in locals() and result:
2320
2645
  container_id = None
2321
- # Try to get container ID from the result dictionary
2646
+ ssh_password = None
2647
+
2648
+ # Try to get container ID and SSH password from the result dictionary
2322
2649
  if isinstance(result, dict):
2323
- # The container ID might be stored in execution_history or elsewhere
2324
- # Let's try to find it in the current_dir which might contain it
2325
- current_dir = result.get('current_dir', '')
2326
- if 'container_id' in result:
2327
- container_id = result['container_id']
2328
- elif hasattr(result, 'container_id'):
2329
- container_id = result.container_id
2650
+ container_id = result.get('container_id')
2651
+ ssh_password = result.get('ssh_password')
2652
+ elif hasattr(result, 'container_id'):
2653
+ container_id = result.container_id
2654
+ ssh_password = getattr(result, 'ssh_password', None)
2330
2655
 
2331
2656
  # If we still don't have the container ID, try to read it from the file
2332
2657
  if not container_id:
@@ -2338,66 +2663,63 @@ if __name__ == "__main__":
2338
2663
  print(f"⚠️ Could not read container ID from file: {e}")
2339
2664
 
2340
2665
  if container_id:
2341
- print(f"🚀 Starting shell in container: {container_id}")
2342
-
2343
- # First try to open a new terminal window
2344
- terminal_script = f'''
2345
- tell application "Terminal"
2346
- do script "modal shell {container_id}"
2347
- activate
2348
- end tell
2349
- '''
2666
+ print(f"🚀 SSH connection information:")
2667
+ print(f" ssh root@{container_id}.modal.run")
2668
+ if ssh_password:
2669
+ print(f" Password: {ssh_password}")
2350
2670
 
2671
+ # Try to open a new terminal window with SSH connection
2351
2672
  try:
2352
- # Run osascript to open a new terminal window
2673
+ terminal_script = f'''
2674
+ tell application "Terminal"
2675
+ do script "echo 'Connecting to Modal SSH container...'; echo 'Password: {ssh_password or 'unknown'}'; ssh root@{container_id}.modal.run"
2676
+ activate
2677
+ end tell
2678
+ '''
2679
+
2353
2680
  subprocess.run(['osascript', '-e', terminal_script],
2354
2681
  capture_output=True, text=True, timeout=30)
2355
- print("✅ New terminal window opened successfully")
2682
+ print("✅ New terminal window opened with SSH connection")
2683
+
2356
2684
  except Exception as e:
2357
2685
  print(f"⚠️ Failed to open terminal window: {e}")
2358
2686
 
2359
- # Try alternative approach with iTerm2 if Terminal failed
2360
- print("🔄 Trying with iTerm2 instead...")
2361
- iterm_script = f'''
2362
- tell application "iTerm"
2363
- create window with default profile
2364
- tell current session of current window
2365
- write text "modal shell {container_id}"
2366
- end tell
2367
- end tell
2368
- '''
2369
-
2687
+ # Try alternative approach with iTerm2
2370
2688
  try:
2371
- iterm_result = subprocess.run(['osascript', '-e', iterm_script],
2372
- capture_output=True, text=True, timeout=30)
2373
- print("✅ New iTerm2 window opened successfully")
2689
+ iterm_script = f'''
2690
+ tell application "iTerm"
2691
+ create window with default profile
2692
+ tell current session of current window
2693
+ write text "echo 'Connecting to Modal SSH container...'; echo 'Password: {ssh_password or 'unknown'}'; ssh root@{container_id}.modal.run"
2694
+ end tell
2695
+ end tell
2696
+ '''
2697
+
2698
+ subprocess.run(['osascript', '-e', iterm_script],
2699
+ capture_output=True, text=True, timeout=30)
2700
+ print("✅ New iTerm2 window opened with SSH connection")
2701
+
2374
2702
  except Exception as e2:
2375
- # As a last resort, try to run the modal shell command directly
2376
2703
  print(f"⚠️ Failed to open iTerm2 window: {e2}")
2377
- print("🔄 Trying direct modal shell command...")
2378
-
2379
- try:
2380
- # Execute modal shell command directly without any flags
2381
- shell_cmd = f"modal shell {container_id}"
2382
- print(f"🔄 Executing: {shell_cmd}")
2383
- subprocess.run(shell_cmd, shell=True)
2384
- print("✅ Shell session completed")
2385
- except Exception as e3:
2386
- print(f"❌ Error starting shell: {e3}")
2387
- print("📝 You can manually connect using:")
2388
- print(f" modal shell {container_id}")
2704
+ print("📝 You can manually connect using:")
2705
+ print(f" ssh root@{container_id}.modal.run")
2706
+ if ssh_password:
2707
+ print(f" Password: {ssh_password}")
2708
+ print(" Or use Modal exec:")
2709
+ print(f" modal container exec --pty {container_id} bash")
2389
2710
  else:
2390
2711
  print("⚠️ Could not determine container ID")
2391
2712
  print("📝 You can manually connect using:")
2392
2713
  print(" modal container list")
2393
- print(" modal shell <CONTAINER_ID>")
2714
+ print(" modal container exec --pty <CONTAINER_ID> bash")
2394
2715
 
2395
2716
  # Exit cleanly
2396
2717
  sys.exit(0)
2718
+
2397
2719
  except KeyboardInterrupt:
2398
- # Handle Ctrl+C during sandbox creation
2399
- print("\n👋 Script interrupted during sandbox creation.")
2400
- print("📝 You may need to check if a sandbox was created with: modal sandbox list")
2720
+ # Handle Ctrl+C during container creation
2721
+ print("\n👋 Script interrupted during container creation.")
2722
+ print("📝 You may need to check if a container was created with: modal container list")
2401
2723
  sys.exit(0)
2402
2724
  except Exception as e:
2403
2725
  print(f"❌ Error: {e}")
@@ -8,6 +8,8 @@ import re
8
8
  import datetime
9
9
  import getpass
10
10
  import requests
11
+ import secrets
12
+ import string
11
13
 
12
14
  def handle_interactive_input(prompt, is_password=False):
13
15
  """Handle interactive input from the user with optional password masking"""
@@ -1808,6 +1810,324 @@ cd "{current_dir}"
1808
1810
  "sandbox_id": sandbox_id
1809
1811
  }
1810
1812
 
1813
+
1814
+ def handle_interactive_input(prompt, is_password=False):
1815
+ """Handle interactive input from the user with optional password masking"""
1816
+ print("\n" + "="*60)
1817
+ print(f"{prompt}")
1818
+ print("="*60)
1819
+
1820
+ try:
1821
+ if is_password:
1822
+ user_input = getpass.getpass("Input (hidden): ").strip()
1823
+ else:
1824
+ user_input = input("Input: ").strip()
1825
+
1826
+ if not user_input:
1827
+ print("❌ No input provided.")
1828
+ return None
1829
+ print("✅ Input received successfully!")
1830
+ return user_input
1831
+ except KeyboardInterrupt:
1832
+ print("\n❌ Input cancelled by user.")
1833
+ return None
1834
+ except Exception as e:
1835
+ print(f"❌ Error getting input: {e}")
1836
+ return None
1837
+
1838
+ def generate_random_password(length=16):
1839
+ """Generate a random password for SSH access"""
1840
+ alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
1841
+ password = ''.join(secrets.choice(alphabet) for i in range(length))
1842
+ return password
1843
+
1844
+
1845
+
1846
+ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_commands=None,
1847
+ volume_name=None, timeout_minutes=60, ssh_password=None):
1848
+ """Create a Modal SSH container with GPU support"""
1849
+
1850
+ # Generate a unique app name with timestamp to avoid conflicts
1851
+ timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
1852
+ app_name = f"ssh-container-{timestamp}"
1853
+
1854
+ gpu_configs = {
1855
+ 'A10G': {'gpu': 'a10g', 'memory': 24},
1856
+ 'A100': {'gpu': 'a100', 'memory': 40},
1857
+ 'H100': {'gpu': 'h100', 'memory': 80},
1858
+ 'T4': {'gpu': 't4', 'memory': 16},
1859
+ 'V100': {'gpu': 'v100', 'memory': 16}
1860
+ }
1861
+
1862
+ if gpu_type not in gpu_configs:
1863
+ print(f"⚠️ Unknown GPU type: {gpu_type}. Using A10G as default.")
1864
+ gpu_type = 'A10G'
1865
+
1866
+ gpu_spec = gpu_configs[gpu_type]
1867
+ print(f"🚀 Creating Modal SSH container with {gpu_spec['gpu']} GPU ({gpu_spec['memory']}GB VRAM)")
1868
+
1869
+ # Generate or use provided SSH password
1870
+ if not ssh_password:
1871
+ ssh_password = generate_random_password()
1872
+ print(f"🔐 Generated SSH password: {ssh_password}")
1873
+
1874
+ # Setup volume if specified
1875
+ volume = None
1876
+ volume_mount_path = "/persistent"
1877
+
1878
+ if volume_name:
1879
+ print(f"📦 Setting up volume: {volume_name}")
1880
+ try:
1881
+ volume = modal.Volume.from_name(volume_name, create_if_missing=True)
1882
+ print(f"✅ Volume '{volume_name}' ready for use")
1883
+ except Exception as e:
1884
+ print(f"⚠️ Could not setup volume '{volume_name}': {e}")
1885
+ print("⚠️ Continuing without persistent volume")
1886
+ volume = None
1887
+ else:
1888
+ # Create a default volume for this session
1889
+ default_volume_name = f"ssh-vol-{timestamp}"
1890
+ print(f"📦 Creating default volume: {default_volume_name}")
1891
+ try:
1892
+ volume = modal.Volume.from_name(default_volume_name, create_if_missing=True)
1893
+ volume_name = default_volume_name
1894
+ print(f"✅ Default volume '{default_volume_name}' created")
1895
+ except Exception as e:
1896
+ print(f"⚠️ Could not create default volume: {e}")
1897
+ print("⚠️ Continuing without persistent volume")
1898
+ volume = None
1899
+
1900
+ # Create SSH-enabled image
1901
+ ssh_image = (
1902
+ modal.Image.debian_slim()
1903
+ .apt_install(
1904
+ "openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
1905
+ "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
1906
+ "gpg", "ca-certificates", "software-properties-common"
1907
+ )
1908
+ .pip_install("uv") # Fast Python package installer
1909
+ .run_commands(
1910
+ # Create SSH directory
1911
+ "mkdir -p /var/run/sshd",
1912
+ "mkdir -p /root/.ssh",
1913
+ "chmod 700 /root/.ssh",
1914
+
1915
+ # Configure SSH server
1916
+ "sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
1917
+ "sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
1918
+ "sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
1919
+
1920
+ # SSH keep-alive settings
1921
+ "echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
1922
+ "echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
1923
+
1924
+ # Generate SSH host keys
1925
+ "ssh-keygen -A",
1926
+
1927
+ # Set up a nice bash prompt
1928
+ "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
1929
+ )
1930
+ )
1931
+
1932
+ # Create Modal app
1933
+ with modal.enable_output():
1934
+ print(f"🚀 Creating Modal app: {app_name}")
1935
+ app = modal.App.lookup(app_name, create_if_missing=True)
1936
+
1937
+ # Setup volume mount if available
1938
+ volumes = {}
1939
+ if volume:
1940
+ volumes[volume_mount_path] = volume
1941
+ print(f"📦 Mounting volume '{volume_name}' at {volume_mount_path}")
1942
+
1943
+ @app.function(
1944
+ image=ssh_image,
1945
+ timeout=timeout_minutes * 60, # Convert to seconds
1946
+ gpu=gpu_spec['gpu'],
1947
+ cpu=2,
1948
+ memory=8192,
1949
+ serialized=True,
1950
+ volumes=volumes if volumes else None,
1951
+ )
1952
+ def ssh_container():
1953
+ import subprocess
1954
+ import time
1955
+ import os
1956
+
1957
+ # Set root password
1958
+ subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
1959
+
1960
+ # Start SSH service
1961
+ subprocess.run(["service", "ssh", "start"], check=True)
1962
+
1963
+ # Setup environment
1964
+ os.environ['PS1'] = r'\[\e[1;32m\]modal:\[\e[1;34m\]\w\[\e[0m\]$ '
1965
+
1966
+ # Clone repository if provided
1967
+ if repo_url:
1968
+ repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
1969
+ print(f"📥 Cloning repository: {repo_url}")
1970
+
1971
+ try:
1972
+ subprocess.run(["git", "clone", repo_url], check=True, cwd="/root")
1973
+ print(f"✅ Repository cloned successfully: {repo_name_from_url}")
1974
+
1975
+ # Change to repository directory
1976
+ repo_dir = f"/root/{repo_name_from_url}"
1977
+ if os.path.exists(repo_dir):
1978
+ os.chdir(repo_dir)
1979
+ print(f"📂 Changed to repository directory: {repo_dir}")
1980
+
1981
+ except subprocess.CalledProcessError as e:
1982
+ print(f"❌ Failed to clone repository: {e}")
1983
+
1984
+ # Run setup commands if provided
1985
+ if setup_commands:
1986
+ print(f"⚙️ Running {len(setup_commands)} setup commands...")
1987
+ for i, cmd in enumerate(setup_commands, 1):
1988
+ print(f"📋 Executing command {i}/{len(setup_commands)}: {cmd}")
1989
+ try:
1990
+ result = subprocess.run(cmd, shell=True, check=True,
1991
+ capture_output=True, text=True)
1992
+ if result.stdout:
1993
+ print(f"✅ Output: {result.stdout}")
1994
+ except subprocess.CalledProcessError as e:
1995
+ print(f"❌ Command failed: {e}")
1996
+ if e.stderr:
1997
+ print(f"❌ Error: {e.stderr}")
1998
+
1999
+ # Get container info
2000
+ print("🔍 Container started successfully!")
2001
+ print(f"🆔 Container ID: {os.environ.get('MODAL_TASK_ID', 'unknown')}")
2002
+
2003
+ # Keep the container running
2004
+ while True:
2005
+ time.sleep(30)
2006
+ # Check if SSH service is still running
2007
+ try:
2008
+ subprocess.run(["service", "ssh", "status"], check=True,
2009
+ capture_output=True)
2010
+ except subprocess.CalledProcessError:
2011
+ print("⚠️ SSH service stopped, restarting...")
2012
+ subprocess.run(["service", "ssh", "start"], check=True)
2013
+
2014
+ # Start the container
2015
+ print("🚀 Starting SSH container...")
2016
+
2017
+ # Use spawn to run the container in the background
2018
+ container_handle = ssh_container.spawn()
2019
+
2020
+ # Wait a moment for the container to start
2021
+ print("⏳ Waiting for container to initialize...")
2022
+ time.sleep(10)
2023
+
2024
+ # Get container information
2025
+ try:
2026
+ # Try to get the container ID from Modal
2027
+ container_id = None
2028
+
2029
+ # Get container list to find our container
2030
+ print("🔍 Looking for container information...")
2031
+ result = subprocess.run(["modal", "container", "list", "--json"],
2032
+ capture_output=True, text=True)
2033
+
2034
+ if result.returncode == 0:
2035
+ try:
2036
+ containers = json.loads(result.stdout)
2037
+ if containers and isinstance(containers, list):
2038
+ # Find the most recent container
2039
+ for container in containers:
2040
+ if container.get("App") == app_name:
2041
+ container_id = container.get("Container ID")
2042
+ break
2043
+
2044
+ if not container_id and containers:
2045
+ # Fall back to the first container
2046
+ container_id = containers[0].get("Container ID")
2047
+
2048
+ except json.JSONDecodeError:
2049
+ pass
2050
+
2051
+ if not container_id:
2052
+ # Try text parsing
2053
+ result = subprocess.run(["modal", "container", "list"],
2054
+ capture_output=True, text=True)
2055
+ if result.returncode == 0:
2056
+ lines = result.stdout.split('\n')
2057
+ for line in lines:
2058
+ if app_name in line or ('ta-' in line and '│' in line):
2059
+ parts = line.split('│')
2060
+ if len(parts) >= 2:
2061
+ possible_id = parts[1].strip()
2062
+ if possible_id.startswith('ta-'):
2063
+ container_id = possible_id
2064
+ break
2065
+
2066
+ if container_id:
2067
+ print(f"📋 Container ID: {container_id}")
2068
+
2069
+ # Get the external IP for SSH access
2070
+ print("🔍 Getting container connection info...")
2071
+
2072
+ # Try to get SSH connection details
2073
+ try:
2074
+ # Modal containers typically expose SSH on port 22
2075
+ ssh_info = f"ssh root@{container_id}.modal.run"
2076
+
2077
+ print("\n" + "="*80)
2078
+ print("🚀 SSH CONTAINER READY!")
2079
+ print("="*80)
2080
+ print(f"🆔 Container ID: {container_id}")
2081
+ print(f"🔐 SSH Password: {ssh_password}")
2082
+ print(f"📱 App Name: {app_name}")
2083
+ if volume:
2084
+ print(f"💾 Volume: {volume_name} (mounted at {volume_mount_path})")
2085
+ print("\n🔗 SSH Connection:")
2086
+ print(f" {ssh_info}")
2087
+ print(f" Password: {ssh_password}")
2088
+ print("\n💡 Alternative connection methods:")
2089
+ print(f" modal container exec --pty {container_id} bash")
2090
+ print(f" modal shell {container_id}")
2091
+ print("="*80)
2092
+
2093
+ # Try to open SSH connection in a new terminal
2094
+ try:
2095
+ terminal_script = f'''
2096
+ tell application "Terminal"
2097
+ do script "echo 'Connecting to Modal SSH container...'; echo 'Password: {ssh_password}'; {ssh_info}"
2098
+ activate
2099
+ end tell
2100
+ '''
2101
+
2102
+ subprocess.run(['osascript', '-e', terminal_script],
2103
+ capture_output=True, text=True, timeout=30)
2104
+ print("✅ New terminal window opened with SSH connection")
2105
+
2106
+ except Exception as e:
2107
+ print(f"⚠️ Could not open terminal window: {e}")
2108
+ print("📝 You can manually connect using the SSH command above")
2109
+
2110
+ except Exception as e:
2111
+ print(f"⚠️ Error getting SSH connection info: {e}")
2112
+ print("📝 You can connect using:")
2113
+ print(f" modal container exec --pty {container_id} bash")
2114
+ else:
2115
+ print("⚠️ Could not determine container ID")
2116
+ print("📝 Check running containers with: modal container list")
2117
+
2118
+ except Exception as e:
2119
+ print(f"❌ Error getting container information: {e}")
2120
+
2121
+ # Return container information
2122
+ return {
2123
+ "container_handle": container_handle,
2124
+ "container_id": container_id,
2125
+ "app_name": app_name,
2126
+ "ssh_password": ssh_password,
2127
+ "volume_name": volume_name,
2128
+ "volume_mount_path": volume_mount_path if volume else None
2129
+ }
2130
+
1811
2131
  def fetch_setup_commands_from_api(repo_url):
1812
2132
  """Fetch setup commands from the GitIngest API using real repository analysis."""
1813
2133
  import tempfile
@@ -2157,7 +2477,7 @@ if __name__ == "__main__":
2157
2477
  # Parse command line arguments when script is run directly
2158
2478
  import argparse
2159
2479
 
2160
- parser = argparse.ArgumentParser(description='Create a Modal sandbox with GPU')
2480
+ parser = argparse.ArgumentParser(description='Create a Modal SSH container with GPU')
2161
2481
  parser.add_argument('--gpu', type=str, default='A10G', help='GPU type (default: A10G)')
2162
2482
  parser.add_argument('--repo-url', type=str, help='Repository URL to clone')
2163
2483
  parser.add_argument('--repo-name', type=str, help='Repository name override')
@@ -2167,6 +2487,8 @@ if __name__ == "__main__":
2167
2487
  parser.add_argument('--setup-script', type=str, help='Path to bash script containing setup commands')
2168
2488
  parser.add_argument('--working-dir', type=str, help='Working directory for the setup script')
2169
2489
  parser.add_argument('--volume-name', type=str, help='Name of the Modal volume for persistent storage')
2490
+ parser.add_argument('--timeout', type=int, default=60, help='Container timeout in minutes (default: 60)')
2491
+ parser.add_argument('--ssh-password', type=str, help='SSH password (random if not provided)')
2170
2492
  parser.add_argument('--use-api', action='store_true', help='Fetch setup commands from API')
2171
2493
 
2172
2494
  args = parser.parse_args()
@@ -2207,6 +2529,7 @@ if __name__ == "__main__":
2207
2529
  for i, cmd in enumerate(setup_commands, 1):
2208
2530
  print(f" {i}. {cmd}")
2209
2531
 
2532
+ # Load commands from file if specified
2210
2533
  if args.commands_file and os.path.exists(args.commands_file):
2211
2534
  try:
2212
2535
  with open(args.commands_file, 'r') as f:
@@ -2257,7 +2580,7 @@ if __name__ == "__main__":
2257
2580
  working_dir = args.working_dir or os.getcwd()
2258
2581
  print(f"📂 Using working directory: {working_dir}")
2259
2582
 
2260
- # Execute the script directly instead of through sandbox
2583
+ # Execute the script directly instead of through container
2261
2584
  try:
2262
2585
  print(f"🔄 Executing script directly: bash {args.setup_script} {working_dir}")
2263
2586
  result = subprocess.run(['bash', args.setup_script, working_dir],
@@ -2276,9 +2599,9 @@ if __name__ == "__main__":
2276
2599
  setup_commands = []
2277
2600
  except Exception as e:
2278
2601
  print(f"❌ Failed to execute script: {e}")
2279
- # Fall back to running the script through sandbox
2602
+ # Fall back to running the script through container
2280
2603
  setup_commands = [f"bash {args.setup_script} {working_dir}"]
2281
- print("🔄 Falling back to running script through sandbox")
2604
+ print("🔄 Falling back to running script through container")
2282
2605
  else:
2283
2606
  print(f"❌ Script not found at: {args.setup_script}")
2284
2607
  # Try to find the script in common locations
@@ -2301,54 +2624,103 @@ if __name__ == "__main__":
2301
2624
  setup_commands = []
2302
2625
 
2303
2626
  try:
2304
- result = create_modal_sandbox(
2305
- args.gpu,
2306
- args.repo_url,
2307
- args.repo_name,
2627
+ result = create_modal_ssh_container(
2628
+ args.gpu,
2629
+ args.repo_url,
2630
+ args.repo_name,
2308
2631
  setup_commands,
2309
- getattr(args, 'volume_name', None)
2632
+ getattr(args, 'volume_name', None),
2633
+ args.timeout,
2634
+ args.ssh_password
2310
2635
  )
2311
2636
 
2312
- print("\n⏳ Keeping the sandbox alive. Press Ctrl+C to exit (sandbox will continue running)...")
2313
- while True:
2314
- time.sleep(90)
2315
- print(".", end="", flush=True)
2316
- except KeyboardInterrupt:
2317
- print("\n👋 Script exited. The sandbox will continue running.")
2318
- if 'result' in locals() and result:
2319
- container_id = None
2320
- # Try to get container ID from the result dictionary
2321
- if isinstance(result, dict):
2322
- # The container ID might be stored in execution_history or elsewhere
2323
- # Let's try to find it in the current_dir which might contain it
2324
- current_dir = result.get('current_dir', '')
2325
- if 'container_id' in result:
2326
- container_id = result['container_id']
2637
+ print("\n⏳ Keeping the SSH container alive. Press Ctrl+C to exit (container will continue running)...")
2638
+ try:
2639
+ while True:
2640
+ time.sleep(90)
2641
+ print(".", end="", flush=True)
2642
+ except KeyboardInterrupt:
2643
+ print("\n👋 Script exited. The SSH container will continue running.")
2644
+ if 'result' in locals() and result:
2645
+ container_id = None
2646
+ ssh_password = None
2647
+
2648
+ # Try to get container ID and SSH password from the result dictionary
2649
+ if isinstance(result, dict):
2650
+ container_id = result.get('container_id')
2651
+ ssh_password = result.get('ssh_password')
2327
2652
  elif hasattr(result, 'container_id'):
2328
2653
  container_id = result.container_id
2654
+ ssh_password = getattr(result, 'ssh_password', None)
2655
+
2656
+ # If we still don't have the container ID, try to read it from the file
2657
+ if not container_id:
2658
+ try:
2659
+ with open(os.path.expanduser("~/.modal_last_container_id"), "r") as f:
2660
+ container_id = f.read().strip()
2661
+ print(f"📋 Retrieved container ID from file: {container_id}")
2662
+ except Exception as e:
2663
+ print(f"⚠️ Could not read container ID from file: {e}")
2664
+
2665
+ if container_id:
2666
+ print(f"🚀 SSH connection information:")
2667
+ print(f" ssh root@{container_id}.modal.run")
2668
+ if ssh_password:
2669
+ print(f" Password: {ssh_password}")
2670
+
2671
+ # Try to open a new terminal window with SSH connection
2672
+ try:
2673
+ terminal_script = f'''
2674
+ tell application "Terminal"
2675
+ do script "echo 'Connecting to Modal SSH container...'; echo 'Password: {ssh_password or 'unknown'}'; ssh root@{container_id}.modal.run"
2676
+ activate
2677
+ end tell
2678
+ '''
2679
+
2680
+ subprocess.run(['osascript', '-e', terminal_script],
2681
+ capture_output=True, text=True, timeout=30)
2682
+ print("✅ New terminal window opened with SSH connection")
2683
+
2684
+ except Exception as e:
2685
+ print(f"⚠️ Failed to open terminal window: {e}")
2686
+
2687
+ # Try alternative approach with iTerm2
2688
+ try:
2689
+ iterm_script = f'''
2690
+ tell application "iTerm"
2691
+ create window with default profile
2692
+ tell current session of current window
2693
+ write text "echo 'Connecting to Modal SSH container...'; echo 'Password: {ssh_password or 'unknown'}'; ssh root@{container_id}.modal.run"
2694
+ end tell
2695
+ end tell
2696
+ '''
2697
+
2698
+ subprocess.run(['osascript', '-e', iterm_script],
2699
+ capture_output=True, text=True, timeout=30)
2700
+ print("✅ New iTerm2 window opened with SSH connection")
2701
+
2702
+ except Exception as e2:
2703
+ print(f"⚠️ Failed to open iTerm2 window: {e2}")
2704
+ print("📝 You can manually connect using:")
2705
+ print(f" ssh root@{container_id}.modal.run")
2706
+ if ssh_password:
2707
+ print(f" Password: {ssh_password}")
2708
+ print(" Or use Modal exec:")
2709
+ print(f" modal container exec --pty {container_id} bash")
2710
+ else:
2711
+ print("⚠️ Could not determine container ID")
2712
+ print("📝 You can manually connect using:")
2713
+ print(" modal container list")
2714
+ print(" modal container exec --pty <CONTAINER_ID> bash")
2329
2715
 
2330
- # If we still don't have the container ID, try to read it from the file
2331
- if not container_id:
2332
- try:
2333
- with open(os.path.expanduser("~/.modal_last_container_id"), "r") as f:
2334
- container_id = f.read().strip()
2335
- print(f"📋 Retrieved container ID from file: {container_id}")
2336
- except Exception as e:
2337
- print(f"⚠️ Could not read container ID from file: {e}")
2716
+ # Exit cleanly
2717
+ sys.exit(0)
2338
2718
 
2339
- if container_id:
2340
- print(f"🚀 Starting shell in container: {container_id}")
2341
- try:
2342
- # Execute the modal shell command directly
2343
- shell_cmd = f"modal shell {container_id}"
2344
- print(f"🔄 Executing: {shell_cmd}")
2345
- subprocess.run(shell_cmd, shell=True)
2346
- except Exception as e:
2347
- print(f"❌ Error starting shell: {e}")
2348
- print(f"📝 You can manually connect using:")
2349
- print(f" modal shell {container_id}")
2350
- else:
2351
- print("⚠️ Could not determine container ID")
2352
- print("📝 You can manually connect using:")
2353
- print(" modal container list")
2354
- print(" modal shell <CONTAINER_ID>")
2719
+ except KeyboardInterrupt:
2720
+ # Handle Ctrl+C during container creation
2721
+ print("\n👋 Script interrupted during container creation.")
2722
+ print("📝 You may need to check if a container was created with: modal container list")
2723
+ sys.exit(0)
2724
+ except Exception as e:
2725
+ print(f"❌ Error: {e}")
2726
+ sys.exit(1)