gitarsenal-cli 1.2.8 → 1.3.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.
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ This script patches the test_modalSandboxScript.py file to replace "modal" with "container" in the logs.
4
+ """
5
+
6
+ import os
7
+ import re
8
+ import sys
9
+ import shutil
10
+ from pathlib import Path
11
+
12
+ def patch_modal_logs(script_path):
13
+ """
14
+ Patch the Python script to replace "modal" with "container" in the logs.
15
+ """
16
+ print(f"Patching {script_path} to replace 'modal' with 'container' in logs...")
17
+
18
+ # Make a backup of the original file
19
+ backup_path = f"{script_path}.bak"
20
+ shutil.copy2(script_path, backup_path)
21
+
22
+ # Read the file
23
+ with open(script_path, 'r', encoding='utf-8') as f:
24
+ content = f.read()
25
+
26
+ # Define replacements
27
+ replacements = [
28
+ # Function names
29
+ (r'def create_modal_sandbox', r'def create_container'),
30
+ (r'def create_modal_ssh_container', r'def create_ssh_container'),
31
+
32
+ # Log messages - case sensitive replacements
33
+ (r'Modal sandbox', r'Container'),
34
+ (r'Modal authentication', r'Container authentication'),
35
+ (r'Modal token', r'Container token'),
36
+ (r'Modal package', r'Container package'),
37
+ (r'Modal operations', r'Container operations'),
38
+
39
+ # Log messages - case insensitive replacements (lowercase)
40
+ (r'modal sandbox', r'container'),
41
+ (r'modal authentication', r'container authentication'),
42
+ (r'modal token', r'container token'),
43
+ (r'modal package', r'container package'),
44
+ (r'modal operations', r'container operations'),
45
+
46
+ # Keep function calls to modal library intact but change logs
47
+ (r'print\([\'"](.*)modal(.*)[\'"]', r'print(\1container\2'),
48
+ (r'log\([\'"](.*)modal(.*)[\'"]', r'log(\1container\2'),
49
+ (r'logger\.info\([\'"](.*)modal(.*)[\'"]', r'logger.info(\1container\2'),
50
+ (r'logger\.error\([\'"](.*)modal(.*)[\'"]', r'logger.error(\1container\2'),
51
+ (r'logger\.warning\([\'"](.*)modal(.*)[\'"]', r'logger.warning(\1container\2'),
52
+ (r'logger\.debug\([\'"](.*)modal(.*)[\'"]', r'logger.debug(\1container\2'),
53
+ ]
54
+
55
+ # Apply replacements
56
+ for pattern, replacement in replacements:
57
+ content = re.sub(pattern, replacement, content, flags=re.IGNORECASE)
58
+
59
+ # Write the modified content back to the file
60
+ with open(script_path, 'w', encoding='utf-8') as f:
61
+ f.write(content)
62
+
63
+ print(f"Patching complete. Original file backed up to {backup_path}")
64
+ return True
65
+
66
+ def main():
67
+ """
68
+ Main entry point for the script.
69
+ """
70
+ # Get the path to the script
71
+ script_dir = Path(__file__).parent
72
+ script_path = script_dir / "test_modalSandboxScript.py"
73
+
74
+ if not script_path.exists():
75
+ print(f"Error: Script not found at {script_path}")
76
+ return 1
77
+
78
+ try:
79
+ patch_modal_logs(script_path)
80
+ return 0
81
+ except Exception as e:
82
+ print(f"Error patching file: {e}")
83
+ return 1
84
+
85
+ if __name__ == "__main__":
86
+ sys.exit(main())
@@ -687,11 +687,15 @@ def create_modal_sandbox(gpu_type, repo_url=None, repo_name=None, setup_commands
687
687
  app_name = f"sandbox-{timestamp}"
688
688
 
689
689
  gpu_configs = {
690
+ 'T4': {'gpu': 'T4', 'memory': 16},
691
+ 'L4': {'gpu': 'L4', 'memory': 24},
690
692
  'A10G': {'gpu': 'A10G', 'memory': 24},
691
- 'A100': {'gpu': 'A100-SXM4-40GB', 'memory': 40},
693
+ 'A100-40GB': {'gpu': 'A100-SXM4-40GB', 'memory': 40},
694
+ 'A100-80GB': {'gpu': 'A100-80GB', 'memory': 80},
695
+ 'L40S': {'gpu': 'L40S', 'memory': 48},
692
696
  'H100': {'gpu': 'H100', 'memory': 80},
693
- 'T4': {'gpu': 'T4', 'memory': 16},
694
- 'V100': {'gpu': 'V100-SXM2-16GB', 'memory': 16}
697
+ 'H200': {'gpu': 'H200', 'memory': 141},
698
+ 'B200': {'gpu': 'B200', 'memory': 96}
695
699
  }
696
700
 
697
701
  if gpu_type not in gpu_configs:
@@ -2163,7 +2167,7 @@ ssh_app = modal.App("ssh-container-app")
2163
2167
  "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
2164
2168
  ),
2165
2169
  timeout=3600, # Default 1 hour timeout
2166
- gpu="a10g", # Default GPU
2170
+ gpu="a10g", # Default GPU - this will be overridden when called
2167
2171
  cpu=2,
2168
2172
  memory=8192,
2169
2173
  serialized=True,
@@ -2344,11 +2348,15 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2344
2348
  app_name = f"ssh-container-{timestamp}"
2345
2349
 
2346
2350
  gpu_configs = {
2351
+ 'T4': {'gpu': 't4', 'memory': 16},
2352
+ 'L4': {'gpu': 'l4', 'memory': 24},
2347
2353
  'A10G': {'gpu': 'a10g', 'memory': 24},
2348
- 'A100': {'gpu': 'a100', 'memory': 40},
2354
+ 'A100-40GB': {'gpu': 'a100', 'memory': 40},
2355
+ 'A100-80GB': {'gpu': 'a100-80gb', 'memory': 80},
2356
+ 'L40S': {'gpu': 'l40s', 'memory': 48},
2349
2357
  'H100': {'gpu': 'h100', 'memory': 80},
2350
- 'T4': {'gpu': 't4', 'memory': 16},
2351
- 'V100': {'gpu': 'v100', 'memory': 16}
2358
+ 'H200': {'gpu': 'h200', 'memory': 141},
2359
+ 'B200': {'gpu': 'b200', 'memory': 96}
2352
2360
  }
2353
2361
 
2354
2362
  if gpu_type not in gpu_configs:
@@ -3322,9 +3330,40 @@ def cleanup_modal_token():
3322
3330
  except Exception as e:
3323
3331
  print(f"āŒ Error during Modal token cleanup: {e}")
3324
3332
 
3333
+ def show_usage_examples():
3334
+ """Display usage examples for the command-line interface."""
3335
+ print("\033[92mUsage Examples\033[0m")
3336
+ print("")
3337
+ print("\033[92mBasic Container Creation\033[0m")
3338
+ print("\033[90mā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”\033[0m")
3339
+ print("\033[90m│\033[0m \033[92mgitarsenal --gpu A10G --repo-url https://github.com/username/repo.git\033[0m \033[90m│\033[0m")
3340
+ print("\033[90mā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜\033[0m")
3341
+ print("")
3342
+ print("\033[92mWith Setup Commands\033[0m")
3343
+ print("\033[90mā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”\033[0m")
3344
+ print("\033[90m│\033[0m \033[92mgitarsenal --gpu A100 --repo-url https://github.com/username/repo.git \\\033[0m \033[90m│\033[0m")
3345
+ print("\033[90m│\033[0m \033[92m --setup-commands \"pip install -r requirements.txt\" \"python setup.py install\"\033[0m \033[90m│\033[0m")
3346
+ print("\033[90mā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜\033[0m")
3347
+ print("")
3348
+ print("\033[92mWith Persistent Storage\033[0m")
3349
+ print("\033[90mā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”\033[0m")
3350
+ print("\033[90m│\033[0m \033[92mgitarsenal --gpu A10G --repo-url https://github.com/username/repo.git \\\033[0m \033[90m│\033[0m")
3351
+ print("\033[90m│\033[0m \033[92m --volume-name my-persistent-volume\033[0m \033[90m│\033[0m")
3352
+ print("\033[90mā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜\033[0m")
3353
+ print("")
3354
+ print("\033[92mInteractive Mode\033[0m")
3355
+ print("\033[90mā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”\033[0m")
3356
+ print("\033[90m│\033[0m \033[92mgitarsenal --interactive\033[0m \033[90m│\033[0m")
3357
+ print("\033[90mā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜\033[0m")
3358
+ print("")
3359
+ print("\033[92mAvailable GPU Options:\033[0m")
3360
+ print(" T4, L4, A10G, A100-40GB, A100-80GB, L40S, H100, H200, B200")
3361
+ print("")
3362
+
3325
3363
  if __name__ == "__main__":
3326
3364
  # Parse command line arguments when script is run directly
3327
3365
  import argparse
3366
+ import sys
3328
3367
 
3329
3368
  parser = argparse.ArgumentParser(description='Create a Modal SSH container with GPU')
3330
3369
  parser.add_argument('--gpu', type=str, default='A10G', help='GPU type (default: A10G)')
@@ -3339,12 +3378,79 @@ if __name__ == "__main__":
3339
3378
  parser.add_argument('--timeout', type=int, default=60, help='Container timeout in minutes (default: 60)')
3340
3379
  parser.add_argument('--ssh-password', type=str, help='SSH password (random if not provided)')
3341
3380
  parser.add_argument('--use-api', action='store_true', help='Fetch setup commands from API')
3381
+ parser.add_argument('--interactive', action='store_true', help='Run in interactive mode with prompts')
3382
+ parser.add_argument('--show-examples', action='store_true', help='Show usage examples')
3342
3383
 
3343
3384
  args = parser.parse_args()
3344
3385
 
3386
+ # If no arguments or only --show-examples is provided, show usage examples
3387
+ if len(sys.argv) == 1 or args.show_examples:
3388
+ show_usage_examples()
3389
+ sys.exit(0)
3390
+
3345
3391
  # Get setup commands from file if specified
3346
3392
  setup_commands = args.setup_commands or []
3347
3393
 
3394
+ # If interactive mode is enabled, prompt for options
3395
+ if args.interactive:
3396
+ # If repo URL wasn't provided via command line, ask for it
3397
+ if not args.repo_url:
3398
+ args.repo_url = input("āœ” Dependencies checked\n? Enter GitHub repository URL: ").strip()
3399
+ if not args.repo_url:
3400
+ print("āŒ No repository URL provided. Exiting.")
3401
+ sys.exit(1)
3402
+
3403
+ # Ask about persistent volume
3404
+ use_volume = input("? Use persistent volume for faster installs? (Yes/No) [Yes]: ").strip().lower()
3405
+ if not use_volume or use_volume.startswith('y'):
3406
+ if not args.volume_name:
3407
+ args.volume_name = input("? Enter volume name [gitarsenal-volume]: ").strip()
3408
+ if not args.volume_name:
3409
+ args.volume_name = "gitarsenal-volume"
3410
+ else:
3411
+ args.volume_name = None
3412
+
3413
+ # Ask about auto-detecting setup commands
3414
+ use_api = input("? Automatically detect setup commands for this repository? (Yes/No) [Yes]: ").strip().lower()
3415
+ if not use_api or use_api.startswith('y'):
3416
+ args.use_api = True
3417
+ else:
3418
+ args.use_api = False
3419
+
3420
+ # GPU selection
3421
+ gpu_options = ['T4', 'L4', 'A10G', 'A100-40GB', 'A100-80GB', 'L40S', 'H100', 'H200', 'B200']
3422
+ print("\n? Select GPU type:")
3423
+ for i, gpu in enumerate(gpu_options, 1):
3424
+ print(f" {i}. {gpu}")
3425
+
3426
+ gpu_choice = input(f"Enter choice (1-{len(gpu_options)}) [default: 3 for A10G]: ").strip()
3427
+ if not gpu_choice:
3428
+ args.gpu = 'A10G' # Default
3429
+ else:
3430
+ try:
3431
+ gpu_index = int(gpu_choice) - 1
3432
+ if 0 <= gpu_index < len(gpu_options):
3433
+ args.gpu = gpu_options[gpu_index]
3434
+ else:
3435
+ print("āš ļø Invalid choice. Using default: A10G")
3436
+ args.gpu = 'A10G'
3437
+ except ValueError:
3438
+ print("āš ļø Invalid input. Using default: A10G")
3439
+ args.gpu = 'A10G'
3440
+
3441
+ # Show configuration summary
3442
+ print("\nšŸ“‹ Container Configuration:")
3443
+ print(f"Repository URL: {args.repo_url}")
3444
+ print(f"GPU Type: {args.gpu}")
3445
+ print(f"Volume: {args.volume_name if args.volume_name else 'None'}")
3446
+ print(f"Setup Commands: {'Auto-detect from repository' if args.use_api else 'None'}")
3447
+
3448
+ # Confirm settings
3449
+ confirm = input("? Proceed with these settings? (Yes/No) [Yes]: ").strip().lower()
3450
+ if confirm and not confirm.startswith('y'):
3451
+ print("āŒ Setup cancelled by user.")
3452
+ sys.exit(0)
3453
+
3348
3454
  # If --use-api flag is set and repo_url is provided, fetch setup commands from API
3349
3455
  if args.use_api and args.repo_url:
3350
3456
  print("šŸ”„ Using API to fetch setup commands")
@@ -3408,70 +3514,7 @@ if __name__ == "__main__":
3408
3514
 
3409
3515
  except Exception as e:
3410
3516
  print(f"āš ļø Error reading commands file: {e}")
3411
-
3412
- # Execute setup script if provided
3413
- if args.setup_script:
3414
- print(f"šŸ“œ Setup script path: {args.setup_script}")
3415
-
3416
- # Verify script exists
3417
- if os.path.exists(args.setup_script):
3418
- print(f"āœ… Script exists at: {args.setup_script}")
3419
-
3420
- # Check if script is executable
3421
- if not os.access(args.setup_script, os.X_OK):
3422
- print(f"āš ļø Script is not executable, setting permissions...")
3423
- try:
3424
- os.chmod(args.setup_script, 0o755)
3425
- print(f"āœ… Set executable permissions on script")
3426
- except Exception as e:
3427
- print(f"āŒ Failed to set permissions: {e}")
3428
-
3429
- working_dir = args.working_dir or os.getcwd()
3430
- print(f"šŸ“‚ Using working directory: {working_dir}")
3431
-
3432
- # Execute the script directly instead of through container
3433
- try:
3434
- print(f"šŸ”„ Executing script directly: bash {args.setup_script} {working_dir}")
3435
- result = subprocess.run(['bash', args.setup_script, working_dir],
3436
- capture_output=True, text=True)
3437
-
3438
- print(f"šŸ“‹ Script output:")
3439
- print(result.stdout)
3440
-
3441
- if result.returncode != 0:
3442
- print(f"āŒ Script execution failed with error code {result.returncode}")
3443
- print(f"Error output: {result.stderr}")
3444
- else:
3445
- print(f"āœ… Script executed successfully")
3446
-
3447
- # Skip the regular setup commands since we executed the script directly
3448
- setup_commands = []
3449
- except Exception as e:
3450
- print(f"āŒ Failed to execute script: {e}")
3451
- # Fall back to running the script through container
3452
- setup_commands = [f"bash {args.setup_script} {working_dir}"]
3453
- print("šŸ”„ Falling back to running script through container")
3454
- else:
3455
- print(f"āŒ Script not found at: {args.setup_script}")
3456
- # Try to find the script in common locations
3457
- possible_paths = [
3458
- os.path.join(os.path.expanduser('~'), os.path.basename(args.setup_script)),
3459
- os.path.join('/tmp', os.path.basename(args.setup_script)),
3460
- os.path.join('/var/tmp', os.path.basename(args.setup_script))
3461
- ]
3462
3517
 
3463
- found = False
3464
- for test_path in possible_paths:
3465
- if os.path.exists(test_path):
3466
- print(f"šŸ” Found script at alternative location: {test_path}")
3467
- setup_commands = [f"bash {test_path} {args.working_dir or os.getcwd()}"]
3468
- found = True
3469
- break
3470
-
3471
- if not found:
3472
- print("āŒ Could not find script in any location")
3473
- setup_commands = []
3474
-
3475
3518
  try:
3476
3519
  result = create_modal_ssh_container(
3477
3520
  args.gpu,
@@ -3490,79 +3533,6 @@ if __name__ == "__main__":
3490
3533
  print(".", end="", flush=True)
3491
3534
  except KeyboardInterrupt:
3492
3535
  print("\nšŸ‘‹ Script exited. The SSH container will continue running.")
3493
- if 'result' in locals() and result:
3494
- container_id = None
3495
- ssh_password = None
3496
-
3497
- # Try to get container ID and SSH password from the result dictionary
3498
- if isinstance(result, dict):
3499
- container_id = result.get('container_id')
3500
- ssh_password = result.get('ssh_password')
3501
- elif hasattr(result, 'container_id'):
3502
- container_id = result.container_id
3503
- ssh_password = getattr(result, 'ssh_password', None)
3504
-
3505
- # If we still don't have the container ID, try to read it from the file
3506
- if not container_id:
3507
- try:
3508
- with open(os.path.expanduser("~/.modal_last_container_id"), "r") as f:
3509
- container_id = f.read().strip()
3510
- print(f"šŸ“‹ Retrieved container ID from file: {container_id}")
3511
- except Exception as e:
3512
- print(f"āš ļø Could not read container ID from file: {e}")
3513
-
3514
- if container_id:
3515
- print(f"šŸš€ SSH connection information:")
3516
- print(f" ssh root@{container_id}.modal.run")
3517
- if ssh_password:
3518
- print(f" Password: {ssh_password}")
3519
-
3520
- # Try to open a new terminal window with SSH connection
3521
- try:
3522
- terminal_script = f'''
3523
- tell application "Terminal"
3524
- do script "echo 'Connecting to Modal SSH container...'; echo 'Password: {ssh_password or 'unknown'}'; ssh root@{container_id}.modal.run"
3525
- activate
3526
- end tell
3527
- '''
3528
-
3529
- subprocess.run(['osascript', '-e', terminal_script],
3530
- capture_output=True, text=True, timeout=30)
3531
- print("āœ… New terminal window opened with SSH connection")
3532
-
3533
- except Exception as e:
3534
- print(f"āš ļø Failed to open terminal window: {e}")
3535
-
3536
- # Try alternative approach with iTerm2
3537
- try:
3538
- iterm_script = f'''
3539
- tell application "iTerm"
3540
- create window with default profile
3541
- tell current session of current window
3542
- write text "echo 'Connecting to Modal SSH container...'; echo 'Password: {ssh_password or 'unknown'}'; ssh root@{container_id}.modal.run"
3543
- end tell
3544
- end tell
3545
- '''
3546
-
3547
- subprocess.run(['osascript', '-e', iterm_script],
3548
- capture_output=True, text=True, timeout=30)
3549
- print("āœ… New iTerm2 window opened with SSH connection")
3550
-
3551
- except Exception as e2:
3552
- print(f"āš ļø Failed to open iTerm2 window: {e2}")
3553
- print("šŸ“ You can manually connect using:")
3554
- print(f" ssh root@{container_id}.modal.run")
3555
- if ssh_password:
3556
- print(f" Password: {ssh_password}")
3557
- print(" Or use Modal exec:")
3558
- print(f" modal container exec --pty {container_id} bash")
3559
- else:
3560
- print("āš ļø Could not determine container ID")
3561
- print("šŸ“ You can manually connect using:")
3562
- print(" modal container list")
3563
- print(" modal container exec --pty <CONTAINER_ID> bash")
3564
-
3565
- # Exit cleanly
3566
3536
  sys.exit(0)
3567
3537
 
3568
3538
  except KeyboardInterrupt: