gitarsenal-cli 1.0.5 → 1.0.7

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