gitarsenal-cli 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitarsenal-cli",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -223,6 +223,18 @@ def root():
223
223
  """Root endpoint for basic connectivity testing"""
224
224
  return jsonify({"status": "ok", "message": "Modal proxy service is running"})
225
225
 
226
+ @app.route('/api/modal-tokens', methods=['GET'])
227
+ def get_modal_tokens():
228
+ """Get Modal tokens (protected by API key)"""
229
+ if not authenticate_request():
230
+ return jsonify({"error": "Unauthorized"}), 401
231
+
232
+ # Return the server's Modal token
233
+ return jsonify({
234
+ "token_id": MODAL_TOKEN,
235
+ "token_secret": MODAL_TOKEN # For compatibility, use the same token
236
+ })
237
+
226
238
  @app.route('/api/create-api-key', methods=['POST'])
227
239
  def create_api_key():
228
240
  """Create a new API key (protected by admin key)"""
@@ -2181,17 +2181,113 @@ def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_co
2181
2181
  subprocess.run(["service", "ssh", "start"], check=True)
2182
2182
 
2183
2183
  # Now modify the create_modal_ssh_container function to use the standalone ssh_container_function
2184
+
2184
2185
  def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_commands=None,
2185
2186
  volume_name=None, timeout_minutes=60, ssh_password=None):
2186
- """Create a Modal SSH container with GPU support and proper tunneling"""
2187
+ """Create a Modal SSH container with GPU support and tunneling"""
2187
2188
 
2188
- # Check Modal authentication
2189
+ # Check if Modal is authenticated
2189
2190
  try:
2190
- modal.config.get_current_workspace_name()
2191
- print("āœ… Modal authentication verified")
2191
+ # Print all environment variables for debugging
2192
+ print("šŸ” DEBUG: Checking environment variables")
2193
+ modal_token_id = os.environ.get("MODAL_TOKEN_ID")
2194
+ modal_token = os.environ.get("MODAL_TOKEN")
2195
+ print(f"šŸ” MODAL_TOKEN_ID exists: {'Yes' if modal_token_id else 'No'}")
2196
+ print(f"šŸ” MODAL_TOKEN exists: {'Yes' if modal_token else 'No'}")
2197
+ if modal_token_id:
2198
+ print(f"šŸ” MODAL_TOKEN_ID length: {len(modal_token_id)}")
2199
+ if modal_token:
2200
+ print(f"šŸ” MODAL_TOKEN length: {len(modal_token)}")
2201
+
2202
+ # Try to access Modal token to check authentication
2203
+ try:
2204
+ # Check if token is set in environment
2205
+ modal_token_id = os.environ.get("MODAL_TOKEN_ID")
2206
+ if not modal_token_id:
2207
+ print("āš ļø MODAL_TOKEN_ID not found in environment.")
2208
+ # Try to get from MODAL_TOKEN
2209
+ modal_token = os.environ.get("MODAL_TOKEN")
2210
+ if modal_token:
2211
+ print("āœ… Found token in MODAL_TOKEN environment variable")
2212
+ os.environ["MODAL_TOKEN_ID"] = modal_token
2213
+ modal_token_id = modal_token
2214
+ print(f"āœ… Set MODAL_TOKEN_ID from MODAL_TOKEN (length: {len(modal_token)})")
2215
+
2216
+ if modal_token_id:
2217
+ print(f"āœ… Modal token found (length: {len(modal_token_id)})")
2218
+
2219
+ # Use the comprehensive fix_modal_token script
2220
+ try:
2221
+ # Execute the fix_modal_token.py script
2222
+ import subprocess
2223
+ print(f"šŸ”„ Running fix_modal_token.py to set up Modal token...")
2224
+ result = subprocess.run(
2225
+ ["python", os.path.join(os.path.dirname(__file__), "fix_modal_token.py")],
2226
+ capture_output=True,
2227
+ text=True
2228
+ )
2229
+
2230
+ # Print the output
2231
+ print(result.stdout)
2232
+
2233
+ if result.returncode != 0:
2234
+ print(f"āš ļø Warning: fix_modal_token.py exited with code {result.returncode}")
2235
+ if result.stderr:
2236
+ print(f"Error: {result.stderr}")
2237
+
2238
+ print(f"āœ… Modal token setup completed")
2239
+ except Exception as e:
2240
+ print(f"āš ļø Error running fix_modal_token.py: {e}")
2241
+ else:
2242
+ print("āŒ No Modal token found in environment variables")
2243
+ # Try to get from file as a last resort
2244
+ try:
2245
+ home_dir = os.path.expanduser("~")
2246
+ modal_dir = os.path.join(home_dir, ".modal")
2247
+ token_file = os.path.join(modal_dir, "token.json")
2248
+ if os.path.exists(token_file):
2249
+ print(f"šŸ” Found Modal token file at {token_file}")
2250
+ with open(token_file, 'r') as f:
2251
+ import json
2252
+ token_data = json.load(f)
2253
+ if "token_id" in token_data:
2254
+ modal_token_id = token_data["token_id"]
2255
+ os.environ["MODAL_TOKEN_ID"] = modal_token_id
2256
+ os.environ["MODAL_TOKEN"] = modal_token_id
2257
+ print(f"āœ… Loaded token from file (length: {len(modal_token_id)})")
2258
+ else:
2259
+ print("āŒ Token file does not contain token_id")
2260
+ else:
2261
+ print("āŒ Modal token file not found")
2262
+ except Exception as e:
2263
+ print(f"āŒ Error loading token from file: {e}")
2264
+
2265
+ if not os.environ.get("MODAL_TOKEN_ID"):
2266
+ print("āŒ Could not find Modal token in any location")
2267
+ return None
2268
+
2269
+ except Exception as e:
2270
+ print(f"āš ļø Error checking Modal token: {e}")
2271
+ # Try to use the token from environment
2272
+ modal_token_id = os.environ.get("MODAL_TOKEN_ID")
2273
+ modal_token = os.environ.get("MODAL_TOKEN")
2274
+ if modal_token_id:
2275
+ print(f"šŸ”„ Using MODAL_TOKEN_ID from environment (length: {len(modal_token_id)})")
2276
+ elif modal_token:
2277
+ print(f"šŸ”„ Using MODAL_TOKEN from environment (length: {len(modal_token)})")
2278
+ os.environ["MODAL_TOKEN_ID"] = modal_token
2279
+ modal_token_id = modal_token
2280
+ else:
2281
+ print("āŒ No Modal token available. Cannot proceed.")
2282
+ return None
2283
+
2284
+ # Set it in both environment variables
2285
+ os.environ["MODAL_TOKEN_ID"] = modal_token_id
2286
+ os.environ["MODAL_TOKEN"] = modal_token_id
2287
+ print("āœ… Set both MODAL_TOKEN_ID and MODAL_TOKEN environment variables")
2192
2288
  except Exception as e:
2193
- print(f"āŒ Modal authentication failed: {e}")
2194
- return None
2289
+ print(f"āš ļø Error checking Modal authentication: {e}")
2290
+ print("Continuing anyway, but Modal operations may fail")
2195
2291
 
2196
2292
  # Generate a unique app name with timestamp to avoid conflicts
2197
2293
  timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
@@ -2230,86 +2326,99 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2230
2326
  print(f"āš ļø Could not setup volume '{volume_name}': {e}")
2231
2327
  print("āš ļø Continuing without persistent volume")
2232
2328
  volume = None
2329
+ else:
2330
+ # Create a default volume for this session
2331
+ default_volume_name = f"ssh-vol-{timestamp}"
2332
+ print(f"šŸ“¦ Creating default volume: {default_volume_name}")
2333
+ try:
2334
+ volume = modal.Volume.from_name(default_volume_name, create_if_missing=True)
2335
+ volume_name = default_volume_name
2336
+ print(f"āœ… Default volume '{default_volume_name}' created")
2337
+ except Exception as e:
2338
+ print(f"āš ļø Could not create default volume: {e}")
2339
+ print("āš ļø Continuing without persistent volume")
2340
+ volume = None
2233
2341
 
2234
- # Create the Modal app
2235
- app = modal.App(app_name)
2342
+ # Print debug info for authentication
2343
+ print("šŸ” Modal authentication debug info:")
2344
+ modal_token = os.environ.get("MODAL_TOKEN_ID")
2345
+ print(f" - MODAL_TOKEN_ID in env: {'Yes' if modal_token else 'No'}")
2346
+ print(f" - Token length: {len(modal_token) if modal_token else 'N/A'}")
2236
2347
 
2348
+ # Verify we can create a Modal app
2349
+ try:
2350
+ print("šŸ” Testing Modal app creation...")
2351
+ app = modal.App(app_name)
2352
+ print("āœ… Created Modal app successfully")
2353
+ except Exception as e:
2354
+ print(f"āŒ Error creating Modal app: {e}")
2355
+ return None
2356
+
2237
2357
  # Create SSH-enabled image
2238
- ssh_image = (
2239
- modal.Image.debian_slim()
2240
- .apt_install(
2241
- "openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
2242
- "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
2243
- "gpg", "ca-certificates", "software-properties-common"
2244
- )
2245
- .pip_install("uv", "modal")
2246
- .run_commands(
2247
- # Create SSH directory
2248
- "mkdir -p /var/run/sshd",
2249
- "mkdir -p /root/.ssh",
2250
- "chmod 700 /root/.ssh",
2251
-
2252
- # Configure SSH server for password authentication
2253
- "sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
2254
- "sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
2255
- "sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config",
2256
- "sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
2257
-
2258
- # SSH keep-alive settings
2259
- "echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
2260
- "echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
2261
-
2262
- # Allow SSH on port 22
2263
- "echo 'Port 22' >> /etc/ssh/sshd_config",
2264
-
2265
- # Generate SSH host keys
2266
- "ssh-keygen -A",
2267
-
2268
- # Set up a nice bash prompt
2269
- "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
2358
+ try:
2359
+ print("šŸ“¦ Building SSH-enabled image...")
2360
+ ssh_image = (
2361
+ modal.Image.debian_slim()
2362
+ .apt_install(
2363
+ "openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
2364
+ "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
2365
+ "gpg", "ca-certificates", "software-properties-common"
2366
+ )
2367
+ .pip_install("uv", "modal") # Fast Python package installer and Modal
2368
+ .run_commands(
2369
+ # Create SSH directory
2370
+ "mkdir -p /var/run/sshd",
2371
+ "mkdir -p /root/.ssh",
2372
+ "chmod 700 /root/.ssh",
2373
+
2374
+ # Configure SSH server
2375
+ "sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
2376
+ "sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
2377
+ "sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
2378
+
2379
+ # SSH keep-alive settings
2380
+ "echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
2381
+ "echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
2382
+
2383
+ # Generate SSH host keys
2384
+ "ssh-keygen -A",
2385
+
2386
+ # Set up a nice bash prompt
2387
+ "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
2388
+ )
2270
2389
  )
2271
- )
2272
-
2390
+ print("āœ… SSH image built successfully")
2391
+ except Exception as e:
2392
+ print(f"āŒ Error building SSH image: {e}")
2393
+ return None
2394
+
2273
2395
  # Configure volumes if available
2274
2396
  volumes_config = {}
2275
2397
  if volume:
2276
2398
  volumes_config[volume_mount_path] = volume
2277
-
2278
- # Define the SSH container function with proper tunnel setup
2399
+
2400
+ # Define the SSH container function
2279
2401
  @app.function(
2280
2402
  image=ssh_image,
2281
- timeout=timeout_minutes * 60,
2403
+ timeout=timeout_minutes * 60, # Convert to seconds
2282
2404
  gpu=gpu_spec['gpu'],
2283
2405
  cpu=2,
2284
2406
  memory=8192,
2407
+ serialized=True,
2285
2408
  volumes=volumes_config if volumes_config else None,
2286
2409
  )
2287
- def ssh_container():
2288
- """Start SSH container with password authentication and tunnel."""
2410
+ def ssh_container_function():
2411
+ """Start SSH container with password authentication and optional setup."""
2289
2412
  import subprocess
2290
2413
  import time
2291
2414
  import os
2292
2415
 
2293
- print("šŸ”§ Setting up SSH container...")
2294
-
2295
2416
  # Set root password
2296
- print(f"šŸ” Setting root password...")
2297
2417
  subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
2298
2418
 
2299
2419
  # Start SSH service
2300
- print("šŸš€ Starting SSH service...")
2301
2420
  subprocess.run(["service", "ssh", "start"], check=True)
2302
2421
 
2303
- # Verify SSH service is running
2304
- result = subprocess.run(["service", "ssh", "status"], capture_output=True, text=True)
2305
- if result.returncode == 0:
2306
- print("āœ… SSH service is running")
2307
- else:
2308
- print("āŒ SSH service failed to start")
2309
- print(result.stdout)
2310
- print(result.stderr)
2311
- return
2312
-
2313
2422
  # Clone repository if provided
2314
2423
  if repo_url:
2315
2424
  repo_name_from_url = repo_name or repo_url.split('/')[-1].replace('.git', '')
@@ -2343,74 +2452,49 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2343
2452
  if e.stderr:
2344
2453
  print(f"āŒ Error: {e.stderr}")
2345
2454
 
2346
- # CRITICAL: Use unencrypted tunnel for SSH (port 22)
2347
- print("🌐 Creating unencrypted SSH tunnel...")
2348
-
2349
- # Use Modal's unencrypted tunnel for SSH protocol
2455
+ # Create SSH tunnel
2350
2456
  with modal.forward(22, unencrypted=True) as tunnel:
2457
+ host, port = tunnel.tcp_socket
2458
+
2351
2459
  print("\n" + "=" * 80)
2352
2460
  print("šŸŽ‰ SSH CONTAINER IS READY!")
2353
2461
  print("=" * 80)
2354
- print(f"🌐 SSH Host: {tunnel.tcp_socket[0]}")
2355
- print(f"šŸ”Œ SSH Port: {tunnel.tcp_socket[1]}")
2462
+ print(f"🌐 SSH Host: {host}")
2463
+ print(f"šŸ”Œ SSH Port: {port}")
2356
2464
  print(f"šŸ‘¤ Username: root")
2357
2465
  print(f"šŸ” Password: {ssh_password}")
2358
2466
  print()
2359
2467
  print("šŸ”— CONNECT USING THIS COMMAND:")
2360
- print(f"ssh -p {tunnel.tcp_socket[1]} root@{tunnel.tcp_socket[0]}")
2468
+ print(f"ssh -p {port} root@{host}")
2361
2469
  print("=" * 80)
2362
2470
 
2363
- # Store connection info in a file for later reference
2364
- connection_info = {
2365
- "host": tunnel.tcp_socket[0],
2366
- "port": tunnel.tcp_socket[1],
2367
- "username": "root",
2368
- "password": ssh_password,
2369
- "app_name": app_name,
2370
- "timestamp": datetime.datetime.now().isoformat()
2371
- }
2372
-
2373
- try:
2374
- with open(os.path.expanduser("~/.modal_ssh_connection"), "w") as f:
2375
- json.dump(connection_info, f, indent=2)
2376
- print(f"šŸ“‹ Connection info saved to ~/.modal_ssh_connection")
2377
- except Exception as e:
2378
- print(f"āš ļø Could not save connection info: {e}")
2379
-
2380
- # Keep the container and tunnel alive
2381
- print("ā³ Container is running. Press Ctrl+C to stop...")
2382
- try:
2383
- while True:
2384
- time.sleep(30)
2385
- # Check if SSH service is still running
2386
- try:
2387
- subprocess.run(["service", "ssh", "status"], check=True,
2388
- capture_output=True)
2389
- except subprocess.CalledProcessError:
2390
- print("āš ļø SSH service stopped, restarting...")
2391
- subprocess.run(["service", "ssh", "start"], check=True)
2392
- except KeyboardInterrupt:
2393
- print("\nšŸ‘‹ Container stopping...")
2394
- return
2471
+ # Keep the container running
2472
+ while True:
2473
+ time.sleep(30)
2474
+ # Check if SSH service is still running
2475
+ try:
2476
+ subprocess.run(["service", "ssh", "status"], check=True,
2477
+ capture_output=True)
2478
+ except subprocess.CalledProcessError:
2479
+ print("āš ļø SSH service stopped, restarting...")
2480
+ subprocess.run(["service", "ssh", "start"], check=True)
2395
2481
 
2396
2482
  # Run the container
2397
2483
  try:
2398
2484
  print("ā³ Starting container... This may take 1-2 minutes...")
2399
2485
 
2400
- # Run the container function
2401
- with app.run():
2402
- ssh_container.remote()
2403
-
2486
+ # Start the container in a new thread to avoid blocking
2487
+ with modal.enable_output():
2488
+ with app.run():
2489
+ ssh_container_function.remote()
2490
+
2404
2491
  return {
2405
2492
  "app_name": app_name,
2406
2493
  "ssh_password": ssh_password,
2407
2494
  "volume_name": volume_name
2408
2495
  }
2409
-
2410
2496
  except Exception as e:
2411
2497
  print(f"āŒ Error running container: {e}")
2412
- import traceback
2413
- traceback.print_exc()
2414
2498
  return None
2415
2499
 
2416
2500
  def fetch_setup_commands_from_api(repo_url):