gitarsenal-cli 1.0.6 → 1.0.8

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.6",
3
+ "version": "1.0.8",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1841,11 +1841,113 @@ def generate_random_password(length=16):
1841
1841
  password = ''.join(secrets.choice(alphabet) for i in range(length))
1842
1842
  return password
1843
1843
 
1844
+ # First, add the standalone ssh_container function at the module level, before the create_modal_ssh_container function
1844
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
1845
1947
 
1846
1948
  def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_commands=None,
1847
1949
  volume_name=None, timeout_minutes=60, ssh_password=None):
1848
- """Create a Modal SSH container with GPU support"""
1950
+ """Create a Modal SSH container with GPU support and tunneling"""
1849
1951
 
1850
1952
  # Generate a unique app name with timestamp to avoid conflicts
1851
1953
  timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
@@ -1896,7 +1998,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1896
1998
  print(f"āš ļø Could not create default volume: {e}")
1897
1999
  print("āš ļø Continuing without persistent volume")
1898
2000
  volume = None
1899
-
2001
+
1900
2002
  # Create SSH-enabled image
1901
2003
  ssh_image = (
1902
2004
  modal.Image.debian_slim()
@@ -1928,205 +2030,180 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1928
2030
  "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
1929
2031
  )
1930
2032
  )
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,
2033
+
2034
+ # Create the Modal app
2035
+ app = modal.App(app_name)
2036
+
2037
+ # Configure volumes if available
2038
+ volumes_config = {}
2039
+ if volume:
2040
+ volumes_config[volume_mount_path] = volume
2041
+
2042
+ @app.function(
2043
+ image=ssh_image,
2044
+ timeout=timeout_minutes * 60, # Convert to seconds
2045
+ gpu=gpu_spec['gpu'],
2046
+ cpu=2,
2047
+ memory=8192,
2048
+ serialized=True,
2049
+ volumes=volumes_config if volumes_config else None,
2050
+ )
2051
+ def run_ssh_container():
2052
+ """Start SSH container with password authentication and optional setup."""
2053
+ import subprocess
2054
+ import time
2055
+ import os
2056
+
2057
+ # Set root password
2058
+ print("šŸ” Setting up authentication...")
2059
+ subprocess.run(
2060
+ ["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"],
2061
+ check=True
1951
2062
  )
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
2063
 
2014
- # Start the container
2015
- print("šŸš€ Starting SSH container...")
2064
+ print("āœ… Root password configured")
2016
2065
 
2017
- # Use spawn to run the container in the background
2018
- container_handle = ssh_container.spawn()
2066
+ # Start SSH daemon in background
2067
+ print("šŸ”„ Starting SSH daemon...")
2068
+ ssh_process = subprocess.Popen(["/usr/sbin/sshd", "-D"])
2019
2069
 
2020
- # Wait a moment for the container to start
2021
- print("ā³ Waiting for container to initialize...")
2022
- time.sleep(10)
2070
+ # Give SSH daemon a moment to start
2071
+ time.sleep(2)
2023
2072
 
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
2073
+ # Clone repository if provided
2074
+ if repo_url:
2075
+ repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
2076
+ print(f"šŸ“„ Cloning repository: {repo_url}")
2065
2077
 
2066
- if container_id:
2067
- print(f"šŸ“‹ Container ID: {container_id}")
2078
+ try:
2079
+ subprocess.run(["git", "clone", repo_url, "/root/repo"], check=True)
2080
+ print(f"āœ… Repository cloned successfully: {repo_name_from_url}")
2081
+
2082
+ # Change to repository directory
2083
+ repo_dir = f"/root/repo"
2084
+ os.chdir(repo_dir)
2085
+ print(f"šŸ“‚ Changed to repository directory: {repo_dir}")
2068
2086
 
2069
- # Get the external IP for SSH access
2070
- print("šŸ” Getting container connection info...")
2087
+ # Initialize git submodules if they exist
2088
+ if os.path.exists(".gitmodules"):
2089
+ print("šŸ“¦ Initializing git submodules...")
2090
+ subprocess.run(["git", "submodule", "update", "--init", "--recursive"], check=True)
2071
2091
 
2072
- # Try to get SSH connection details
2092
+ except subprocess.CalledProcessError as e:
2093
+ print(f"āŒ Failed to clone repository: {e}")
2094
+
2095
+ # Run setup commands if provided
2096
+ if setup_commands:
2097
+ print(f"āš™ļø Running {len(setup_commands)} setup commands...")
2098
+ for i, cmd in enumerate(setup_commands, 1):
2099
+ print(f"šŸ“‹ Executing command {i}/{len(setup_commands)}: {cmd}")
2073
2100
  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)
2101
+ # Run in repository directory if it exists, otherwise root
2102
+ work_dir = "/root/repo" if os.path.exists("/root/repo") else "/root"
2103
+ result = subprocess.run(
2104
+ cmd,
2105
+ shell=True,
2106
+ check=True,
2107
+ cwd=work_dir,
2108
+ capture_output=True,
2109
+ text=True
2110
+ )
2111
+ if result.stdout:
2112
+ print(f"āœ… Output: {result.stdout}")
2113
+ except subprocess.CalledProcessError as e:
2114
+ print(f"āŒ Command failed: {e}")
2115
+ if e.stderr:
2116
+ print(f"āŒ Error: {e.stderr}")
2117
+
2118
+ # Create unencrypted tunnel for SSH (port 22)
2119
+ print("🌐 Creating SSH tunnel...")
2120
+ with modal.forward(22, unencrypted=True) as tunnel:
2121
+ # Print connection information
2122
+ host, port = tunnel.tcp_socket
2123
+
2124
+ print("\n" + "=" * 80)
2125
+ print("šŸŽ‰ SSH CONTAINER IS READY!")
2126
+ print("=" * 80)
2127
+ print(f"🌐 SSH Host: {host}")
2128
+ print(f"šŸ”Œ SSH Port: {port}")
2129
+ print(f"šŸ‘¤ Username: root")
2130
+ print(f"šŸ” Password: {ssh_password}")
2131
+ print()
2132
+ print("šŸ”— CONNECT USING THIS COMMAND:")
2133
+ print(f"ssh -p {port} root@{host}")
2134
+ print()
2135
+ print("šŸ’” Connection Tips:")
2136
+ print("• Copy the password above and paste when prompted")
2137
+ print("• Use Ctrl+C to disconnect (container will keep running)")
2138
+ print("• Type 'exit' in the SSH session to close the connection")
2139
+ if repo_url:
2140
+ print("• Your repository is in: /root/repo")
2141
+ if volume:
2142
+ print(f"• Persistent storage is mounted at: {volume_mount_path}")
2143
+ print(f"• Container will auto-terminate in {timeout_minutes} minutes")
2144
+ print()
2145
+ print("šŸ› ļø Available Tools:")
2146
+ print("• Python 3 with pip and uv package manager")
2147
+ print("• Git, curl, wget, vim, nano, htop")
2148
+ print("• tmux and screen for session management")
2149
+ print(f"• {gpu_type} GPU access")
2150
+ print("=" * 80)
2151
+
2152
+ # Keep the container running and monitor SSH daemon
2153
+ try:
2154
+ start_time = time.time()
2155
+ last_check = start_time
2156
+
2157
+ while True:
2158
+ current_time = time.time()
2092
2159
 
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
- '''
2160
+ # Check SSH daemon every 30 seconds
2161
+ if current_time - last_check >= 30:
2162
+ if ssh_process.poll() is not None:
2163
+ print("āš ļø SSH daemon stopped unexpectedly, restarting...")
2164
+ ssh_process = subprocess.Popen(["/usr/sbin/sshd", "-D"])
2165
+ time.sleep(2)
2101
2166
 
2102
- subprocess.run(['osascript', '-e', terminal_script],
2103
- capture_output=True, text=True, timeout=30)
2104
- print("āœ… New terminal window opened with SSH connection")
2167
+ # Print alive status every 5 minutes
2168
+ elapsed_minutes = (current_time - start_time) / 60
2169
+ if int(elapsed_minutes) % 5 == 0 and elapsed_minutes > 0:
2170
+ remaining_minutes = timeout_minutes - elapsed_minutes
2171
+ print(f"ā±ļø Container alive for {elapsed_minutes:.0f} minutes, {remaining_minutes:.0f} minutes remaining")
2105
2172
 
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")
2173
+ last_check = current_time
2109
2174
 
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
2175
+ time.sleep(10)
2176
+
2177
+ except KeyboardInterrupt:
2178
+ print("\nšŸ‘‹ Received shutdown signal...")
2179
+ print("šŸ”„ Stopping SSH daemon...")
2180
+ ssh_process.terminate()
2181
+ ssh_process.wait()
2182
+ print("āœ… Container shutting down gracefully")
2183
+ return
2184
+
2185
+ # Run the container
2186
+ print("ā³ Starting container... This may take 1-2 minutes...")
2187
+ print("šŸ“¦ Building image and allocating resources...")
2188
+
2189
+ try:
2190
+ # Start the container and run the function
2191
+ with modal.enable_output():
2192
+ with app.run():
2193
+ run_ssh_container.remote()
2194
+
2122
2195
  return {
2123
- "container_handle": container_handle,
2124
- "container_id": container_id,
2125
2196
  "app_name": app_name,
2126
2197
  "ssh_password": ssh_password,
2127
2198
  "volume_name": volume_name,
2128
2199
  "volume_mount_path": volume_mount_path if volume else None
2129
2200
  }
2201
+ except KeyboardInterrupt:
2202
+ print("\nšŸ‘‹ Container startup interrupted")
2203
+ return None
2204
+ except Exception as e:
2205
+ print(f"āŒ Error running container: {e}")
2206
+ return None
2130
2207
 
2131
2208
  def fetch_setup_commands_from_api(repo_url):
2132
2209
  """Fetch setup commands from the GitIngest API using real repository analysis."""
@@ -2473,6 +2550,124 @@ def get_setup_commands_from_local_api(repo_url, gitingest_data):
2473
2550
 
2474
2551
  return None
2475
2552
 
2553
+ # Define a function to create and return a properly configured ssh container function
2554
+ def create_ssh_container_function(gpu_type="a10g", timeout_minutes=60, volume=None, volume_mount_path="/persistent"):
2555
+ # Create a new app for this specific container
2556
+ app_name = f"ssh-container-{datetime.datetime.now().strftime('%Y%m%d-%H%M%S')}"
2557
+ ssh_app = modal.App.lookup(app_name, create_if_missing=True)
2558
+
2559
+ # Create SSH-enabled image
2560
+ ssh_image = (
2561
+ modal.Image.debian_slim()
2562
+ .apt_install(
2563
+ "openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
2564
+ "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
2565
+ "gpg", "ca-certificates", "software-properties-common"
2566
+ )
2567
+ .pip_install("uv")
2568
+ .run_commands(
2569
+ # Create SSH directory
2570
+ "mkdir -p /var/run/sshd",
2571
+ "mkdir -p /root/.ssh",
2572
+ "chmod 700 /root/.ssh",
2573
+
2574
+ # Configure SSH server
2575
+ "sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
2576
+ "sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
2577
+ "sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
2578
+
2579
+ # SSH keep-alive settings
2580
+ "echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
2581
+ "echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
2582
+
2583
+ # Generate SSH host keys
2584
+ "ssh-keygen -A",
2585
+
2586
+ # Set up a nice bash prompt
2587
+ "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
2588
+ )
2589
+ )
2590
+
2591
+ # Setup volume mount if available
2592
+ volumes = {}
2593
+ if volume:
2594
+ volumes[volume_mount_path] = volume
2595
+
2596
+ # Define the function with the specific configuration
2597
+ @ssh_app.function(
2598
+ image=ssh_image,
2599
+ timeout=timeout_minutes * 60, # Convert to seconds
2600
+ gpu=gpu_type,
2601
+ cpu=2,
2602
+ memory=8192,
2603
+ serialized=True,
2604
+ volumes=volumes if volumes else None,
2605
+ )
2606
+ def ssh_container(ssh_password, repo_url=None, repo_name=None, setup_commands=None):
2607
+ import subprocess
2608
+ import time
2609
+ import os
2610
+
2611
+ # Set root password
2612
+ subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
2613
+
2614
+ # Start SSH service
2615
+ subprocess.run(["service", "ssh", "start"], check=True)
2616
+
2617
+ # Setup environment
2618
+ os.environ['PS1'] = r'\[\e[1;32m\]modal:\[\e[1;34m\]\w\[\e[0m\]$ '
2619
+
2620
+ # Clone repository if provided
2621
+ if repo_url:
2622
+ repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
2623
+ print(f"šŸ“„ Cloning repository: {repo_url}")
2624
+
2625
+ try:
2626
+ subprocess.run(["git", "clone", repo_url], check=True, cwd="/root")
2627
+ print(f"āœ… Repository cloned successfully: {repo_name_from_url}")
2628
+
2629
+ # Change to repository directory
2630
+ repo_dir = f"/root/{repo_name_from_url}"
2631
+ if os.path.exists(repo_dir):
2632
+ os.chdir(repo_dir)
2633
+ print(f"šŸ“‚ Changed to repository directory: {repo_dir}")
2634
+
2635
+ except subprocess.CalledProcessError as e:
2636
+ print(f"āŒ Failed to clone repository: {e}")
2637
+
2638
+ # Run setup commands if provided
2639
+ if setup_commands:
2640
+ print(f"āš™ļø Running {len(setup_commands)} setup commands...")
2641
+ for i, cmd in enumerate(setup_commands, 1):
2642
+ print(f"šŸ“‹ Executing command {i}/{len(setup_commands)}: {cmd}")
2643
+ try:
2644
+ result = subprocess.run(cmd, shell=True, check=True,
2645
+ capture_output=True, text=True)
2646
+ if result.stdout:
2647
+ print(f"āœ… Output: {result.stdout}")
2648
+ except subprocess.CalledProcessError as e:
2649
+ print(f"āŒ Command failed: {e}")
2650
+ if e.stderr:
2651
+ print(f"āŒ Error: {e.stderr}")
2652
+
2653
+ # Get container info
2654
+ print("šŸ” Container started successfully!")
2655
+ print(f"šŸ†” Container ID: {os.environ.get('MODAL_TASK_ID', 'unknown')}")
2656
+
2657
+ # Keep the container running
2658
+ while True:
2659
+ time.sleep(30)
2660
+ # Check if SSH service is still running
2661
+ try:
2662
+ subprocess.run(["service", "ssh", "status"], check=True,
2663
+ capture_output=True)
2664
+ except subprocess.CalledProcessError:
2665
+ print("āš ļø SSH service stopped, restarting...")
2666
+ subprocess.run(["service", "ssh", "start"], check=True)
2667
+
2668
+ # Return the configured function
2669
+ return ssh_container, app_name
2670
+
2476
2671
  if __name__ == "__main__":
2477
2672
  # Parse command line arguments when script is run directly
2478
2673
  import argparse
@@ -2687,7 +2882,7 @@ if __name__ == "__main__":
2687
2882
  # Try alternative approach with iTerm2
2688
2883
  try:
2689
2884
  iterm_script = f'''
2690
- tell application "iTerm"
2885
+ tell application "iTerm"
2691
2886
  create window with default profile
2692
2887
  tell current session of current window
2693
2888
  write text "echo 'Connecting to Modal SSH container...'; echo 'Password: {ssh_password or 'unknown'}'; ssh root@{container_id}.modal.run"