gitarsenal-cli 1.0.7 → 1.0.9

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.7",
3
+ "version": "1.0.9",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -1947,7 +1947,7 @@ def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_co
1947
1947
 
1948
1948
  def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_commands=None,
1949
1949
  volume_name=None, timeout_minutes=60, ssh_password=None):
1950
- """Create a Modal SSH container with GPU support"""
1950
+ """Create a Modal SSH container with GPU support and tunneling"""
1951
1951
 
1952
1952
  # Generate a unique app name with timestamp to avoid conflicts
1953
1953
  timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
@@ -1998,137 +1998,212 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
1998
1998
  print(f"⚠️ Could not create default volume: {e}")
1999
1999
  print("⚠️ Continuing without persistent volume")
2000
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
2001
+
2002
+ # Create SSH-enabled image
2003
+ ssh_image = (
2004
+ modal.Image.debian_slim()
2005
+ .apt_install(
2006
+ "openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
2007
+ "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
2008
+ "gpg", "ca-certificates", "software-properties-common"
2009
+ )
2010
+ .pip_install("uv") # Fast Python package installer
2011
+ .run_commands(
2012
+ # Create SSH directory
2013
+ "mkdir -p /var/run/sshd",
2014
+ "mkdir -p /root/.ssh",
2015
+ "chmod 700 /root/.ssh",
2016
+
2017
+ # Configure SSH server
2018
+ "sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
2019
+ "sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
2020
+ "sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
2021
+
2022
+ # SSH keep-alive settings
2023
+ "echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
2024
+ "echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
2025
+
2026
+ # Generate SSH host keys
2027
+ "ssh-keygen -A",
2028
+
2029
+ # Set up a nice bash prompt
2030
+ "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
2031
+ )
2032
+ )
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
2014
2062
  )
2015
2063
 
2016
- # Start the container
2017
- print("🚀 Starting SSH container...")
2064
+ print("✅ Root password configured")
2018
2065
 
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)
2066
+ # Start SSH daemon in background
2067
+ print("🔄 Starting SSH daemon...")
2068
+ ssh_process = subprocess.Popen(["/usr/sbin/sshd", "-D"])
2021
2069
 
2022
- # Wait a moment for the container to start
2023
- print("⏳ Waiting for container to initialize...")
2024
- time.sleep(10)
2070
+ # Give SSH daemon a moment to start
2071
+ time.sleep(2)
2025
2072
 
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
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}")
2067
2077
 
2068
- if container_id:
2069
- 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}")
2070
2086
 
2071
- # Get the external IP for SSH access
2072
- 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)
2073
2091
 
2074
- # 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}")
2075
2100
  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)
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()
2094
2159
 
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
- '''
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)
2103
2166
 
2104
- subprocess.run(['osascript', '-e', terminal_script],
2105
- capture_output=True, text=True, timeout=30)
2106
- 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")
2107
2172
 
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")
2173
+ last_check = current_time
2111
2174
 
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
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
+
2124
2195
  return {
2125
- "container_handle": container_handle,
2126
- "container_id": container_id,
2127
- "app_name": "ssh-container-app",
2196
+ "app_name": app_name,
2128
2197
  "ssh_password": ssh_password,
2129
2198
  "volume_name": volume_name,
2130
2199
  "volume_mount_path": volume_mount_path if volume else None
2131
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
2132
2207
 
2133
2208
  def fetch_setup_commands_from_api(repo_url):
2134
2209
  """Fetch setup commands from the GitIngest API using real repository analysis."""
@@ -2138,7 +2213,7 @@ def fetch_setup_commands_from_api(repo_url):
2138
2213
  import shutil
2139
2214
  import json
2140
2215
 
2141
- api_url = "http://localhost:3000/api/analyze-with-gitingest"
2216
+ api_url = "http://git-arsenal.vercel.app/api/analyze-with-gitingest"
2142
2217
 
2143
2218
  print(f"🔍 Fetching setup commands from API for repository: {repo_url}")
2144
2219
 
@@ -2475,6 +2550,124 @@ def get_setup_commands_from_local_api(repo_url, gitingest_data):
2475
2550
 
2476
2551
  return None
2477
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
+
2478
2671
  if __name__ == "__main__":
2479
2672
  # Parse command line arguments when script is run directly
2480
2673
  import argparse