gitarsenal-cli 1.1.0 → 1.1.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/README.md CHANGED
@@ -1,105 +1,74 @@
1
1
  # GitArsenal CLI
2
2
 
3
- A command-line tool for easily creating Modal sandboxes with GitHub repositories.
3
+ A tool for creating and managing GPU-accelerated development environments using Modal.
4
4
 
5
5
  ## Features
6
6
 
7
- - Interactive prompts to configure your sandbox
8
- - Support for custom GPU types (A10G, A100, H100, T4, V100)
9
- - Persistent volume support for faster installations
10
- - Custom setup commands
11
- - Automatic repository cloning
7
+ - Create Modal containers with GPU support
8
+ - Clone repositories and run setup commands
9
+ - Persistent storage with Modal volumes
10
+ - SSH access to containers
11
+ - API key management for various services
12
12
 
13
- ## Installation
13
+ ## API Key Management
14
14
 
15
- ### Prerequisites
15
+ The CLI now supports secure storage of API keys for various services. Keys are stored in `~/.gitarsenal/keys/` with proper permissions (only readable by the current user).
16
16
 
17
- - Node.js 14 or higher
18
- - Python 3.8 or higher
19
- - Modal CLI (`pip install modal`)
20
- - Git
17
+ ### Supported Services
21
18
 
22
- ### Global Installation
19
+ - `openai` - OpenAI API keys for debugging and assistance
20
+ - `wandb` - Weights & Biases API keys for experiment tracking
21
+ - `huggingface` - Hugging Face tokens for model access
23
22
 
24
- ```bash
25
- npm install -g gitarsenal-cli
26
- ```
23
+ ### Managing API Keys
27
24
 
28
- ### Local Installation
25
+ #### Adding a new API key
29
26
 
30
27
  ```bash
31
- npm install gitarsenal-cli
32
- ```
28
+ # Add a key interactively (will prompt for the key)
29
+ python test_modalSandboxScript.py keys add --service openai
33
30
 
34
- ## Usage
35
-
36
- ### Command Line Interface
31
+ # Add a key directly (not recommended for security)
32
+ python test_modalSandboxScript.py keys add --service wandb --key YOUR_API_KEY
33
+ ```
37
34
 
38
- Simply run the command:
35
+ #### Listing saved API keys
39
36
 
40
37
  ```bash
41
- gitarsenal
38
+ python test_modalSandboxScript.py keys list
42
39
  ```
43
40
 
44
- The CLI will guide you through the process with interactive prompts:
45
-
46
- 1. Enter the GitHub repository URL
47
- 2. Select a GPU type
48
- 3. Choose whether to use a persistent volume
49
- 4. Optionally provide custom setup commands
50
- 5. Confirm your settings
41
+ #### Viewing a specific API key (masked)
51
42
 
52
- ### Command Line Options
53
-
54
- ```
55
- Usage: gitarsenal [options]
56
-
57
- Options:
58
- -V, --version output the version number
59
- -r, --repo <url> GitHub repository URL
60
- -g, --gpu <type> GPU type (A10G, A100, H100, T4, V100) (default: "A10G")
61
- -v, --volume <name> Name of persistent volume
62
- -y, --yes Skip confirmation prompts
63
- -h, --help display help for command
43
+ ```bash
44
+ python test_modalSandboxScript.py keys view --service huggingface
64
45
  ```
65
46
 
66
- ### Example
47
+ #### Deleting an API key
67
48
 
68
49
  ```bash
69
- gitarsenal --repo https://github.com/username/repo-name --gpu A10G --volume my-volume
50
+ python test_modalSandboxScript.py keys delete --service openai
70
51
  ```
71
52
 
72
- ## Programmatic Usage
53
+ ## Creating a Modal Container
73
54
 
74
- You can also use GitArsenal programmatically in your Node.js applications:
75
-
76
- ```javascript
77
- const { runModalSandbox } = require('gitarsenal-cli');
55
+ ```bash
56
+ # Basic container creation
57
+ python test_modalSandboxScript.py container --gpu A10G --repo-url https://github.com/username/repo.git
78
58
 
79
- async function main() {
80
- await runModalSandbox({
81
- repoUrl: 'https://github.com/username/repo-name',
82
- gpuType: 'A10G',
83
- volumeName: 'my-volume',
84
- setupCommands: [
85
- 'pip install -r requirements.txt',
86
- 'python setup.py install'
87
- ]
88
- });
89
- }
59
+ # With setup commands
60
+ python test_modalSandboxScript.py container --gpu A100 --repo-url https://github.com/username/repo.git --setup-commands "pip install -r requirements.txt" "python setup.py install"
90
61
 
91
- main().catch(console.error);
62
+ # With volume for persistent storage
63
+ python test_modalSandboxScript.py container --gpu A10G --repo-url https://github.com/username/repo.git --volume-name my-persistent-volume
92
64
  ```
93
65
 
94
- ## How It Works
95
-
96
- GitArsenal CLI is a Node.js wrapper around a Python script that creates Modal sandboxes. The CLI:
66
+ ## Automatic API Key Usage
97
67
 
98
- 1. Checks for required dependencies (Python, Modal, Git)
99
- 2. Prompts for configuration options
100
- 3. Executes the Python script with the provided options
101
- 4. Handles errors and provides feedback
68
+ When using commands that require API keys (like `wandb login` or `huggingface-cli login`), the system will:
102
69
 
103
- ## License
70
+ 1. Check if a saved API key exists for the service
71
+ 2. If found, use the saved key automatically
72
+ 3. If not found, prompt for the key and offer to save it for future use
104
73
 
105
- MIT
74
+ This makes it easy to work with multiple projects that require the same API keys without having to re-enter them each time.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitarsenal-cli",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,140 @@
1
+ # GitArsenal CLI
2
+
3
+ GitArsenal CLI is a powerful tool for setting up and running GPU-accelerated environments in the cloud using Modal.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Clone the repository
9
+ git clone https://github.com/yourusername/gitarsenal-cli.git
10
+ cd gitarsenal-cli/python
11
+
12
+ # Install dependencies
13
+ pip install -r requirements.txt
14
+ ```
15
+
16
+ ## First-Time Setup
17
+
18
+ Before using GitArsenal CLI, you need to set up your credentials and authenticate with Modal:
19
+
20
+ ### 1. Install Modal
21
+
22
+ ```bash
23
+ pip install modal
24
+ ```
25
+
26
+ ### 2. Create a Modal Account and Get a Token
27
+
28
+ If you don't have a Modal account:
29
+ 1. Go to https://modal.com and create an account
30
+ 2. Run the following command to authenticate:
31
+ ```bash
32
+ modal token new
33
+ ```
34
+ 3. Follow the instructions to complete authentication
35
+
36
+ ### 3. Set Up GitArsenal Credentials
37
+
38
+ ```bash
39
+ # Set up all required credentials
40
+ ./gitarsenal.py credentials setup
41
+ ```
42
+
43
+ This will guide you through setting up:
44
+ - **Modal Token**: Required for creating cloud environments
45
+ - **OpenAI API Key**: Used for debugging failed commands (optional)
46
+ - **Hugging Face Token**: Used for accessing Hugging Face models (optional)
47
+ - **Weights & Biases API Key**: Used for experiment tracking (optional)
48
+
49
+ ## Usage
50
+
51
+ ### Creating a Modal Sandbox
52
+
53
+ ```bash
54
+ ./gitarsenal.py sandbox --gpu A10G --repo-url "https://github.com/username/repo.git"
55
+ ```
56
+
57
+ ### Creating an SSH Container
58
+
59
+ ```bash
60
+ ./gitarsenal.py ssh --gpu A10G --repo-url "https://github.com/username/repo.git"
61
+ ```
62
+
63
+ ### Options
64
+
65
+ - `--gpu`: GPU type (A10G, A100, H100, T4, V100)
66
+ - `--repo-url`: Repository URL to clone
67
+ - `--repo-name`: Repository name override
68
+ - `--setup-commands`: Setup commands to run
69
+ - `--volume-name`: Name of the Modal volume for persistent storage
70
+ - `--timeout`: Container timeout in minutes (SSH mode only, default: 60)
71
+
72
+ ## Managing Credentials
73
+
74
+ You can manage your credentials using the following commands:
75
+
76
+ ```bash
77
+ # Set up all credentials
78
+ ./gitarsenal.py credentials setup
79
+
80
+ # Set a specific credential
81
+ ./gitarsenal.py credentials set modal_token
82
+
83
+ # View a credential (masked for security)
84
+ ./gitarsenal.py credentials get modal_token
85
+
86
+ # Clear a specific credential
87
+ ./gitarsenal.py credentials clear huggingface_token
88
+
89
+ # Clear all credentials
90
+ ./gitarsenal.py credentials clear
91
+
92
+ # List all saved credentials (without showing values)
93
+ ./gitarsenal.py credentials list
94
+ ```
95
+
96
+ ## Security
97
+
98
+ Your credentials are stored securely in `~/.gitarsenal/credentials.json` with restrictive file permissions. The file is only readable by your user account.
99
+
100
+ ## Troubleshooting
101
+
102
+ ### Modal Authentication Issues
103
+
104
+ If you see errors like "Token missing" or "Could not authenticate client":
105
+
106
+ 1. Ensure Modal is installed:
107
+ ```bash
108
+ pip install modal
109
+ ```
110
+
111
+ 2. Get a new Modal token:
112
+ ```bash
113
+ modal token new
114
+ ```
115
+
116
+ 3. Save the token in GitArsenal:
117
+ ```bash
118
+ ./gitarsenal.py credentials set modal_token
119
+ ```
120
+
121
+ ### API Timeout Issues
122
+
123
+ If the GitArsenal API times out when analyzing repositories, the tool will automatically use fallback setup commands based on the detected programming language and technologies.
124
+
125
+ ### Other Issues
126
+
127
+ 1. Check that your credentials are set up correctly:
128
+ ```bash
129
+ ./gitarsenal.py credentials list
130
+ ```
131
+
132
+ 2. If needed, clear and reset your credentials:
133
+ ```bash
134
+ ./gitarsenal.py credentials clear
135
+ ./gitarsenal.py credentials setup
136
+ ```
137
+
138
+ ## License
139
+
140
+ [MIT License](LICENSE)
@@ -0,0 +1,164 @@
1
+ import os
2
+ import json
3
+ import getpass
4
+ from pathlib import Path
5
+
6
+ class CredentialsManager:
7
+ """
8
+ Manages API keys and tokens for GitArsenal CLI.
9
+ Provides secure storage and retrieval of credentials.
10
+ """
11
+
12
+ def __init__(self, config_dir=None):
13
+ """Initialize the credentials manager with optional custom config directory"""
14
+ if config_dir:
15
+ self.config_dir = Path(config_dir)
16
+ else:
17
+ self.config_dir = Path.home() / ".gitarsenal"
18
+
19
+ self.credentials_file = self.config_dir / "credentials.json"
20
+ self.ensure_config_dir()
21
+
22
+ def ensure_config_dir(self):
23
+ """Ensure the configuration directory exists"""
24
+ if not self.config_dir.exists():
25
+ self.config_dir.mkdir(parents=True)
26
+ # Set restrictive permissions on Unix-like systems
27
+ if os.name == 'posix':
28
+ self.config_dir.chmod(0o700) # Only owner can read/write/execute
29
+
30
+ def load_credentials(self):
31
+ """Load credentials from the credentials file"""
32
+ if not self.credentials_file.exists():
33
+ return {}
34
+
35
+ try:
36
+ with open(self.credentials_file, 'r') as f:
37
+ return json.load(f)
38
+ except (json.JSONDecodeError, IOError):
39
+ print("⚠️ Error reading credentials file. Using empty credentials.")
40
+ return {}
41
+
42
+ def save_credentials(self, credentials):
43
+ """Save credentials to the credentials file"""
44
+ try:
45
+ with open(self.credentials_file, 'w') as f:
46
+ json.dump(credentials, f)
47
+
48
+ # Set restrictive permissions on Unix-like systems
49
+ if os.name == 'posix':
50
+ self.credentials_file.chmod(0o600) # Only owner can read/write
51
+
52
+ return True
53
+ except IOError as e:
54
+ print(f"❌ Error saving credentials: {e}")
55
+ return False
56
+
57
+ def get_credential(self, key, prompt=None, is_password=False, validate_func=None):
58
+ """
59
+ Get a credential by key. If not found, prompt the user.
60
+
61
+ Args:
62
+ key: The credential key
63
+ prompt: Custom prompt message
64
+ is_password: Whether to mask input
65
+ validate_func: Optional function to validate the credential
66
+
67
+ Returns:
68
+ The credential value
69
+ """
70
+ credentials = self.load_credentials()
71
+
72
+ # Check if credential exists and is valid
73
+ if key in credentials:
74
+ value = credentials[key]
75
+ if not validate_func or validate_func(value):
76
+ return value
77
+
78
+ # Credential not found or invalid, prompt user
79
+ if not prompt:
80
+ prompt = f"Please enter your {key}:"
81
+
82
+ print("\n" + "="*60)
83
+ print(f"🔑 {key.upper()} REQUIRED")
84
+ print("="*60)
85
+ print(prompt)
86
+ print("-" * 60)
87
+
88
+ try:
89
+ if is_password:
90
+ value = getpass.getpass("Input (hidden): ").strip()
91
+ else:
92
+ value = input("Input: ").strip()
93
+
94
+ if not value:
95
+ print("❌ No input provided.")
96
+ return None
97
+
98
+ # Validate if function provided
99
+ if validate_func and not validate_func(value):
100
+ print("❌ Invalid input.")
101
+ return None
102
+
103
+ # Save the credential
104
+ credentials[key] = value
105
+ self.save_credentials(credentials)
106
+
107
+ print("✅ Input received and saved successfully!")
108
+ return value
109
+
110
+ except KeyboardInterrupt:
111
+ print("\n❌ Input cancelled by user.")
112
+ return None
113
+ except Exception as e:
114
+ print(f"❌ Error getting input: {e}")
115
+ return None
116
+
117
+ def get_openai_api_key(self):
118
+ """Get OpenAI API key with validation"""
119
+ def validate_openai_key(key):
120
+ # Basic validation - OpenAI keys usually start with "sk-" and are 51 chars
121
+ return key.startswith("sk-") and len(key) > 40
122
+
123
+ prompt = "To debug failed commands, an OpenAI API key is needed.\nYou can get your API key from: https://platform.openai.com/api-keys"
124
+ return self.get_credential("openai_api_key", prompt, is_password=True, validate_func=validate_openai_key)
125
+
126
+ def get_modal_token(self):
127
+ """Get Modal token with basic validation"""
128
+ def validate_modal_token(token):
129
+ # Modal tokens are typically non-empty strings
130
+ return bool(token) and len(token) > 10
131
+
132
+ prompt = "A Modal token is required to create cloud environments.\nYou can get your token by running 'modal token new' in your terminal."
133
+ return self.get_credential("modal_token", prompt, is_password=True, validate_func=validate_modal_token)
134
+
135
+ def get_huggingface_token(self):
136
+ """Get Hugging Face token with basic validation"""
137
+ def validate_hf_token(token):
138
+ # HF tokens are typically non-empty strings
139
+ return bool(token) and len(token) > 8
140
+
141
+ prompt = "A Hugging Face token is required.\nYou can get your token from: https://huggingface.co/settings/tokens"
142
+ return self.get_credential("huggingface_token", prompt, is_password=True, validate_func=validate_hf_token)
143
+
144
+ def get_wandb_api_key(self):
145
+ """Get Weights & Biases API key with validation"""
146
+ def validate_wandb_key(key):
147
+ # W&B API keys are typically 40 characters
148
+ return len(key) == 40
149
+
150
+ prompt = "A Weights & Biases API key is required.\nYou can get your API key from: https://wandb.ai/authorize"
151
+ return self.get_credential("wandb_api_key", prompt, is_password=True, validate_func=validate_wandb_key)
152
+
153
+ def clear_credential(self, key):
154
+ """Remove a specific credential"""
155
+ credentials = self.load_credentials()
156
+ if key in credentials:
157
+ del credentials[key]
158
+ self.save_credentials(credentials)
159
+ return True
160
+ return False
161
+
162
+ def clear_all_credentials(self):
163
+ """Clear all saved credentials"""
164
+ return self.save_credentials({})
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ GitArsenal CLI - Main entry point
4
+
5
+ This script provides a user-friendly interface to the GitArsenal CLI tools.
6
+ """
7
+
8
+ import argparse
9
+ import sys
10
+ import os
11
+ import subprocess
12
+ import json
13
+ from pathlib import Path
14
+
15
+ def check_modal_auth():
16
+ """Check if Modal is authenticated and guide user if not"""
17
+ try:
18
+ # Try to run a simple Modal command to check authentication
19
+ result = subprocess.run(["modal", "app", "list"],
20
+ capture_output=True, text=True, timeout=10)
21
+
22
+ # If the command was successful, Modal is authenticated
23
+ if result.returncode == 0:
24
+ return True
25
+
26
+ # Check for specific authentication errors
27
+ if "Token missing" in result.stderr or "Could not authenticate" in result.stderr:
28
+ print("\n" + "="*80)
29
+ print("🔑 MODAL AUTHENTICATION REQUIRED")
30
+ print("="*80)
31
+ print("GitArsenal requires Modal authentication to create cloud environments.")
32
+ print("\nTo authenticate with Modal, you need to:")
33
+ print("1. Create a Modal account at https://modal.com if you don't have one")
34
+ print("2. Run the following command to get a token:")
35
+ print(" modal token new")
36
+ print("3. Then set up your credentials in GitArsenal:")
37
+ print(" ./gitarsenal.py credentials set modal_token")
38
+ print("\nAfter completing these steps, try your command again.")
39
+ print("="*80)
40
+ return False
41
+
42
+ # Other errors
43
+ print(f"⚠️ Modal command returned error: {result.stderr}")
44
+ return False
45
+
46
+ except FileNotFoundError:
47
+ print("\n" + "="*80)
48
+ print("❌ MODAL CLI NOT INSTALLED")
49
+ print("="*80)
50
+ print("GitArsenal requires the Modal CLI to be installed.")
51
+ print("\nTo install Modal CLI, run:")
52
+ print(" pip install modal")
53
+ print("\nAfter installation, set up your credentials:")
54
+ print("1. Run 'modal token new' to authenticate")
55
+ print("2. Run './gitarsenal.py credentials set modal_token'")
56
+ print("="*80)
57
+ return False
58
+ except Exception as e:
59
+ print(f"⚠️ Error checking Modal authentication: {e}")
60
+ return False
61
+
62
+ def main():
63
+ parser = argparse.ArgumentParser(description="GitArsenal CLI - GPU-accelerated cloud environments")
64
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
65
+
66
+ # Credentials command
67
+ cred_parser = subparsers.add_parser("credentials", help="Manage credentials")
68
+ cred_subparsers = cred_parser.add_subparsers(dest="cred_command", help="Credentials command")
69
+
70
+ # Credentials setup command
71
+ cred_setup_parser = cred_subparsers.add_parser("setup", help="Set up all credentials")
72
+
73
+ # Credentials set command
74
+ cred_set_parser = cred_subparsers.add_parser("set", help="Set a specific credential")
75
+ cred_set_parser.add_argument("key", choices=["openai_api_key", "modal_token", "huggingface_token", "wandb_api_key"],
76
+ help="The credential to set")
77
+
78
+ # Credentials get command
79
+ cred_get_parser = cred_subparsers.add_parser("get", help="Get a specific credential")
80
+ cred_get_parser.add_argument("key", choices=["openai_api_key", "modal_token", "huggingface_token", "wandb_api_key"],
81
+ help="The credential to get")
82
+
83
+ # Credentials clear command
84
+ cred_clear_parser = cred_subparsers.add_parser("clear", help="Clear credentials")
85
+ cred_clear_parser.add_argument("key", nargs="?", choices=["openai_api_key", "modal_token", "huggingface_token", "wandb_api_key", "all"],
86
+ default="all", help="The credential to clear (default: all)")
87
+
88
+ # Credentials list command
89
+ cred_list_parser = cred_subparsers.add_parser("list", help="List all saved credentials")
90
+
91
+ # Sandbox command
92
+ sandbox_parser = subparsers.add_parser("sandbox", help="Create a Modal sandbox")
93
+ sandbox_parser.add_argument("--gpu", type=str, default="A10G", choices=["A10G", "A100", "H100", "T4", "V100"],
94
+ help="GPU type (default: A10G)")
95
+ sandbox_parser.add_argument("--repo-url", type=str, help="Repository URL to clone")
96
+ sandbox_parser.add_argument("--repo-name", type=str, help="Repository name override")
97
+ sandbox_parser.add_argument("--setup-commands", type=str, nargs="+", help="Setup commands to run")
98
+ sandbox_parser.add_argument("--volume-name", type=str, help="Name of the Modal volume for persistent storage")
99
+
100
+ # SSH container command
101
+ ssh_parser = subparsers.add_parser("ssh", help="Create a Modal SSH container")
102
+ ssh_parser.add_argument("--gpu", type=str, default="A10G", choices=["A10G", "A100", "H100", "T4", "V100"],
103
+ help="GPU type (default: A10G)")
104
+ ssh_parser.add_argument("--repo-url", type=str, help="Repository URL to clone")
105
+ ssh_parser.add_argument("--repo-name", type=str, help="Repository name override")
106
+ ssh_parser.add_argument("--setup-commands", type=str, nargs="+", help="Setup commands to run")
107
+ ssh_parser.add_argument("--volume-name", type=str, help="Name of the Modal volume for persistent storage")
108
+ ssh_parser.add_argument("--timeout", type=int, default=60, help="Container timeout in minutes (default: 60)")
109
+
110
+ args = parser.parse_args()
111
+
112
+ # If no command is provided, show help
113
+ if not args.command:
114
+ parser.print_help()
115
+ return 1
116
+
117
+ # Handle credentials commands
118
+ if args.command == "credentials":
119
+ cred_args = []
120
+
121
+ if not args.cred_command:
122
+ cred_parser.print_help()
123
+ return 1
124
+
125
+ cred_args.append(args.cred_command)
126
+
127
+ if args.cred_command == "set" or args.cred_command == "get":
128
+ cred_args.append(args.key)
129
+ elif args.cred_command == "clear" and args.key != "all":
130
+ cred_args.append(args.key)
131
+
132
+ return run_script("manage_credentials.py", cred_args)
133
+
134
+ # For sandbox and SSH commands, check Modal authentication first
135
+ if args.command in ["sandbox", "ssh"]:
136
+ # Check if Modal is authenticated
137
+ if not check_modal_auth():
138
+ print("\n⚠️ Please authenticate with Modal before proceeding.")
139
+ return 1
140
+
141
+ # Try to load credentials and set Modal token if available
142
+ try:
143
+ from credentials_manager import CredentialsManager
144
+ credentials_manager = CredentialsManager()
145
+ credentials = credentials_manager.load_credentials()
146
+
147
+ if "modal_token" in credentials:
148
+ # Set the Modal token in the environment
149
+ os.environ["MODAL_TOKEN_ID"] = credentials["modal_token"]
150
+ print("✅ Using Modal token from credentials")
151
+
152
+ # Try to authenticate with the token
153
+ try:
154
+ token_result = subprocess.run(
155
+ ["modal", "token", "set", credentials["modal_token"]],
156
+ capture_output=True, text=True
157
+ )
158
+ if token_result.returncode == 0:
159
+ print("✅ Successfully authenticated with Modal")
160
+ except Exception as e:
161
+ print(f"⚠️ Error setting Modal token: {e}")
162
+ except ImportError:
163
+ print("⚠️ Could not load credentials manager")
164
+ except Exception as e:
165
+ print(f"⚠️ Error loading credentials: {e}")
166
+
167
+ # Handle sandbox command
168
+ if args.command == "sandbox":
169
+ sandbox_args = []
170
+
171
+ if args.gpu:
172
+ sandbox_args.extend(["--gpu", args.gpu])
173
+ if args.repo_url:
174
+ sandbox_args.extend(["--repo-url", args.repo_url])
175
+ if args.repo_name:
176
+ sandbox_args.extend(["--repo-name", args.repo_name])
177
+ if args.setup_commands:
178
+ sandbox_args.extend(["--setup-commands"] + args.setup_commands)
179
+ if args.volume_name:
180
+ sandbox_args.extend(["--volume-name", args.volume_name])
181
+
182
+ return run_script("test_modalSandboxScript.py", sandbox_args)
183
+
184
+ # Handle SSH container command
185
+ elif args.command == "ssh":
186
+ ssh_args = []
187
+
188
+ if args.gpu:
189
+ ssh_args.extend(["--gpu", args.gpu])
190
+ if args.repo_url:
191
+ ssh_args.extend(["--repo-url", args.repo_url])
192
+ if args.repo_name:
193
+ ssh_args.extend(["--repo-name", args.repo_name])
194
+ if args.setup_commands:
195
+ ssh_args.extend(["--setup-commands"] + args.setup_commands)
196
+ if args.volume_name:
197
+ ssh_args.extend(["--volume-name", args.volume_name])
198
+ if args.timeout:
199
+ ssh_args.extend(["--timeout", str(args.timeout)])
200
+
201
+ # Use test_modalSandboxScript.py with SSH mode
202
+ ssh_args.extend(["--ssh"])
203
+ return run_script("test_modalSandboxScript.py", ssh_args)
204
+
205
+ return 0
206
+
207
+ def run_script(script_name, args):
208
+ """Run a Python script with the given arguments"""
209
+ # Get the directory of the current script
210
+ script_dir = os.path.dirname(os.path.abspath(__file__))
211
+ script_path = os.path.join(script_dir, script_name)
212
+
213
+ # Build the command
214
+ cmd = [sys.executable, script_path] + args
215
+
216
+ try:
217
+ # Run the command
218
+ result = subprocess.run(cmd)
219
+ return result.returncode
220
+ except KeyboardInterrupt:
221
+ print("\n⚠️ Command interrupted by user")
222
+ return 130
223
+ except Exception as e:
224
+ print(f"❌ Error running {script_name}: {e}")
225
+ return 1
226
+
227
+ if __name__ == "__main__":
228
+ sys.exit(main())
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ GitArsenal Credentials Manager CLI
4
+
5
+ This script allows users to manage their API keys and tokens for GitArsenal CLI.
6
+ """
7
+
8
+ import argparse
9
+ import sys
10
+ from credentials_manager import CredentialsManager
11
+
12
+ def main():
13
+ parser = argparse.ArgumentParser(description="Manage credentials for GitArsenal CLI")
14
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
15
+
16
+ # Setup command
17
+ setup_parser = subparsers.add_parser("setup", help="Set up all credentials")
18
+
19
+ # Set command
20
+ set_parser = subparsers.add_parser("set", help="Set a specific credential")
21
+ set_parser.add_argument("key", choices=["openai_api_key", "modal_token", "huggingface_token", "wandb_api_key"],
22
+ help="The credential to set")
23
+
24
+ # Get command
25
+ get_parser = subparsers.add_parser("get", help="Get a specific credential")
26
+ get_parser.add_argument("key", choices=["openai_api_key", "modal_token", "huggingface_token", "wandb_api_key"],
27
+ help="The credential to get")
28
+
29
+ # Clear command
30
+ clear_parser = subparsers.add_parser("clear", help="Clear credentials")
31
+ clear_parser.add_argument("key", nargs="?", choices=["openai_api_key", "modal_token", "huggingface_token", "wandb_api_key", "all"],
32
+ default="all", help="The credential to clear (default: all)")
33
+
34
+ # List command
35
+ list_parser = subparsers.add_parser("list", help="List all saved credentials (shows only existence, not values)")
36
+
37
+ args = parser.parse_args()
38
+
39
+ # Create credentials manager
40
+ credentials_manager = CredentialsManager()
41
+
42
+ if args.command == "setup":
43
+ print("📋 Setting up all credentials for GitArsenal CLI")
44
+ print("You'll be prompted for each credential. Press Ctrl+C to skip any credential.")
45
+
46
+ try:
47
+ # OpenAI API key
48
+ print("\n🔑 Setting up OpenAI API key")
49
+ credentials_manager.get_openai_api_key()
50
+
51
+ # Modal token
52
+ print("\n🔑 Setting up Modal token")
53
+ credentials_manager.get_modal_token()
54
+
55
+ # Hugging Face token
56
+ print("\n🔑 Setting up Hugging Face token")
57
+ credentials_manager.get_huggingface_token()
58
+
59
+ # Weights & Biases API key
60
+ print("\n🔑 Setting up Weights & Biases API key")
61
+ credentials_manager.get_wandb_api_key()
62
+
63
+ print("\n✅ Credentials setup complete!")
64
+
65
+ except KeyboardInterrupt:
66
+ print("\n\n⚠️ Setup interrupted. Some credentials may not have been set.")
67
+ return 1
68
+
69
+ elif args.command == "set":
70
+ print(f"🔑 Setting {args.key}")
71
+ if args.key == "openai_api_key":
72
+ credentials_manager.get_openai_api_key()
73
+ elif args.key == "modal_token":
74
+ credentials_manager.get_modal_token()
75
+ elif args.key == "huggingface_token":
76
+ credentials_manager.get_huggingface_token()
77
+ elif args.key == "wandb_api_key":
78
+ credentials_manager.get_wandb_api_key()
79
+
80
+ elif args.command == "get":
81
+ credentials = credentials_manager.load_credentials()
82
+ if args.key in credentials:
83
+ # Show only first and last few characters for security
84
+ value = credentials[args.key]
85
+ masked_value = value[:4] + "*" * (len(value) - 8) + value[-4:] if len(value) > 8 else "****"
86
+ print(f"{args.key}: {masked_value}")
87
+ else:
88
+ print(f"❌ {args.key} not found in saved credentials")
89
+ return 1
90
+
91
+ elif args.command == "clear":
92
+ if args.key == "all":
93
+ credentials_manager.clear_all_credentials()
94
+ print("✅ All credentials cleared")
95
+ else:
96
+ if credentials_manager.clear_credential(args.key):
97
+ print(f"✅ {args.key} cleared")
98
+ else:
99
+ print(f"❌ {args.key} not found in saved credentials")
100
+ return 1
101
+
102
+ elif args.command == "list":
103
+ credentials = credentials_manager.load_credentials()
104
+ if not credentials:
105
+ print("No credentials saved")
106
+ return 0
107
+
108
+ print("📋 Saved credentials:")
109
+ for key in credentials:
110
+ print(f"- {key}: {'*' * 8}")
111
+
112
+ else:
113
+ parser.print_help()
114
+ return 1
115
+
116
+ return 0
117
+
118
+ if __name__ == "__main__":
119
+ sys.exit(main())
@@ -0,0 +1,4 @@
1
+ modal>=0.56.4
2
+ requests>=2.31.0
3
+ pathlib>=1.0.1
4
+ python-dotenv>=1.0.0
@@ -56,13 +56,25 @@ def handle_wandb_login(sandbox, current_dir):
56
56
  print("Setting up Weights & Biases credentials")
57
57
  print("You can get your API key from: https://wandb.ai/authorize")
58
58
 
59
- # Get API key from user
60
- api_key = handle_interactive_input(
61
- "🔑 WEIGHTS & BIASES API KEY REQUIRED\n" +
62
- "Please paste your W&B API key below:\n" +
63
- "(Your API key should be 40 characters long)",
64
- is_password=True
65
- )
59
+ # Try to use credentials manager first
60
+ api_key = None
61
+ try:
62
+ from credentials_manager import CredentialsManager
63
+ credentials_manager = CredentialsManager()
64
+ api_key = credentials_manager.get_wandb_api_key()
65
+ except ImportError:
66
+ # Fall back to direct input if credentials_manager is not available
67
+ pass
68
+
69
+ # If credentials manager didn't provide a key, use direct input
70
+ if not api_key:
71
+ # Get API key from user
72
+ api_key = handle_interactive_input(
73
+ "🔑 WEIGHTS & BIASES API KEY REQUIRED\n" +
74
+ "Please paste your W&B API key below:\n" +
75
+ "(Your API key should be 40 characters long)",
76
+ is_password=True
77
+ )
66
78
 
67
79
  if not api_key:
68
80
  print("❌ No API key provided. Cannot continue with W&B login.")
@@ -226,26 +238,36 @@ def call_openai_for_debug(command, error_output, api_key=None, current_dir=None,
226
238
  api_key = os.environ.get("OPENAI_API_KEY")
227
239
 
228
240
  if not api_key:
229
- print("\n" + "="*60)
230
- print("🔑 OPENAI API KEY REQUIRED FOR DEBUGGING")
231
- print("="*60)
232
- print("To debug failed commands, an OpenAI API key is needed.")
233
- print("📝 Please paste your OpenAI API key below:")
234
- print(" (Your input will be hidden for security)")
235
- print("-" * 60)
236
-
241
+ # Use the CredentialsManager to get the API key
237
242
  try:
238
- api_key = getpass.getpass("OpenAI API Key: ").strip()
243
+ from credentials_manager import CredentialsManager
244
+ credentials_manager = CredentialsManager()
245
+ api_key = credentials_manager.get_openai_api_key()
239
246
  if not api_key:
240
247
  print("❌ No API key provided. Skipping debugging.")
241
248
  return None
242
- print("✅ API key received successfully!")
243
- except KeyboardInterrupt:
244
- print("\n API key input cancelled by user.")
245
- return None
246
- except Exception as e:
247
- print(f" Error getting API key: {e}")
248
- return None
249
+ except ImportError:
250
+ # Fall back to direct input if credentials_manager module is not available
251
+ print("\n" + "="*60)
252
+ print("🔑 OPENAI API KEY REQUIRED FOR DEBUGGING")
253
+ print("="*60)
254
+ print("To debug failed commands, an OpenAI API key is needed.")
255
+ print("📝 Please paste your OpenAI API key below:")
256
+ print(" (Your input will be hidden for security)")
257
+ print("-" * 60)
258
+
259
+ try:
260
+ api_key = getpass.getpass("OpenAI API Key: ").strip()
261
+ if not api_key:
262
+ print("❌ No API key provided. Skipping debugging.")
263
+ return None
264
+ print("✅ API key received successfully!")
265
+ except KeyboardInterrupt:
266
+ print("\n❌ API key input cancelled by user.")
267
+ return None
268
+ except Exception as e:
269
+ print(f"❌ Error getting API key: {e}")
270
+ return None
249
271
 
250
272
  # Get current directory context
251
273
  directory_context = ""
@@ -410,6 +432,18 @@ Do not provide any explanations, just the exact command to run.
410
432
 
411
433
  def prompt_for_hf_token():
412
434
  """Prompt user for Hugging Face token when needed"""
435
+ # Try to use credentials manager first
436
+ try:
437
+ from credentials_manager import CredentialsManager
438
+ credentials_manager = CredentialsManager()
439
+ token = credentials_manager.get_huggingface_token()
440
+ if token:
441
+ return token
442
+ except ImportError:
443
+ # Fall back to direct input if credentials_manager is not available
444
+ pass
445
+
446
+ # Traditional direct input method as fallback
413
447
  print("\n" + "="*60)
414
448
  print("🔑 HUGGING FACE TOKEN REQUIRED")
415
449
  print("="*60)
@@ -434,6 +468,87 @@ def prompt_for_hf_token():
434
468
  return None
435
469
 
436
470
  def create_modal_sandbox(gpu_type, repo_url=None, repo_name=None, setup_commands=None, volume_name=None):
471
+ # Import the credentials manager if available
472
+ try:
473
+ from credentials_manager import CredentialsManager
474
+ credentials_manager = CredentialsManager()
475
+ except ImportError:
476
+ credentials_manager = None
477
+ print("⚠️ Credentials manager not found, will use environment variables or prompt for credentials")
478
+
479
+ # Check if Modal is authenticated
480
+ try:
481
+ # Try to import modal first to check if it's installed
482
+ import modal
483
+
484
+ # Try to access Modal token to check authentication
485
+ try:
486
+ # This will raise an exception if not authenticated
487
+ modal.config.get_current_workspace_name()
488
+ print("✅ Modal authentication verified")
489
+ except modal.exception.AuthError:
490
+ print("\n" + "="*80)
491
+ print("🔑 MODAL AUTHENTICATION REQUIRED")
492
+ print("="*80)
493
+ print("GitArsenal requires Modal authentication to create cloud environments.")
494
+
495
+ # Try to get token from credentials manager
496
+ modal_token = None
497
+ if credentials_manager:
498
+ try:
499
+ modal_token = credentials_manager.get_modal_token()
500
+ if modal_token:
501
+ # Set the token in the environment
502
+ os.environ["MODAL_TOKEN_ID"] = modal_token
503
+ print("✅ Modal token set from credentials manager")
504
+
505
+ # Try to authenticate with the token
506
+ try:
507
+ import subprocess
508
+ token_result = subprocess.run(
509
+ ["modal", "token", "set", modal_token],
510
+ capture_output=True, text=True
511
+ )
512
+ if token_result.returncode == 0:
513
+ print("✅ Successfully authenticated with Modal")
514
+ else:
515
+ print(f"⚠️ Failed to authenticate with Modal: {token_result.stderr}")
516
+ print("\nPlease authenticate manually:")
517
+ print("1. Run 'modal token new' to get a new token")
518
+ print("2. Then restart this command")
519
+ return None
520
+ except Exception as e:
521
+ print(f"⚠️ Error setting Modal token: {e}")
522
+ return None
523
+ except Exception as e:
524
+ print(f"⚠️ Error getting Modal token: {e}")
525
+
526
+ if not modal_token:
527
+ print("\nTo authenticate with Modal, you need to:")
528
+ print("1. Create a Modal account at https://modal.com if you don't have one")
529
+ print("2. Run the following command to get a token:")
530
+ print(" modal token new")
531
+ print("3. Then set up your credentials in GitArsenal:")
532
+ print(" ./gitarsenal.py credentials set modal_token")
533
+ print("\nAfter completing these steps, try your command again.")
534
+ print("="*80)
535
+ return None
536
+ except ImportError:
537
+ print("\n" + "="*80)
538
+ print("❌ MODAL PACKAGE NOT INSTALLED")
539
+ print("="*80)
540
+ print("GitArsenal requires the Modal package to be installed.")
541
+ print("\nTo install Modal, run:")
542
+ print(" pip install modal")
543
+ print("\nAfter installation, authenticate with Modal:")
544
+ print("1. Run 'modal token new'")
545
+ print("2. Then run './gitarsenal.py credentials set modal_token'")
546
+ print("="*80)
547
+ return None
548
+ except Exception as e:
549
+ print(f"⚠️ Error checking Modal authentication: {e}")
550
+ print("Continuing anyway, but Modal operations may fail")
551
+
437
552
  # Execution history for tracking all commands and their results in this session
438
553
  execution_history = []
439
554
 
@@ -1853,7 +1968,7 @@ ssh_app = modal.App("ssh-container-app")
1853
1968
  "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
1854
1969
  "gpg", "ca-certificates", "software-properties-common"
1855
1970
  )
1856
- .pip_install("uv")
1971
+ .pip_install("uv", "modal") # Fast Python package installer and Modal
1857
1972
  .run_commands(
1858
1973
  # Create SSH directory
1859
1974
  "mkdir -p /var/run/sshd",
@@ -1872,6 +1987,9 @@ ssh_app = modal.App("ssh-container-app")
1872
1987
  # Generate SSH host keys
1873
1988
  "ssh-keygen -A",
1874
1989
 
1990
+ # Install Modal CLI
1991
+ "pip install modal",
1992
+
1875
1993
  # Set up a nice bash prompt
1876
1994
  "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
1877
1995
  ),
@@ -2007,7 +2125,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2007
2125
  "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
2008
2126
  "gpg", "ca-certificates", "software-properties-common"
2009
2127
  )
2010
- .pip_install("uv") # Fast Python package installer
2128
+ .pip_install("uv", "modal") # Fast Python package installer and Modal
2011
2129
  .run_commands(
2012
2130
  # Create SSH directory
2013
2131
  "mkdir -p /var/run/sshd",
@@ -2026,6 +2144,9 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
2026
2144
  # Generate SSH host keys
2027
2145
  "ssh-keygen -A",
2028
2146
 
2147
+ # Install Modal CLI
2148
+ "pip install modal",
2149
+
2029
2150
  # Set up a nice bash prompt
2030
2151
  "echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
2031
2152
  )
@@ -2309,53 +2430,177 @@ def fetch_setup_commands_from_api(repo_url):
2309
2430
 
2310
2431
  # Make the API request
2311
2432
  print(f"🌐 Making POST request to: {api_url}")
2312
- response = requests.post(api_url, json=payload, timeout=60)
2313
-
2314
- print(f"📥 API Response status code: {response.status_code}")
2315
-
2316
- if response.status_code == 200:
2317
- try:
2318
- data = response.json()
2319
- print(f"📄 API Response data received")
2320
-
2321
- # Extract setup commands from the response
2322
- if "setupInstructions" in data and "commands" in data["setupInstructions"]:
2323
- commands = data["setupInstructions"]["commands"]
2324
- print(f"✅ Successfully fetched {len(commands)} setup commands from API")
2325
-
2326
- # Print the commands for reference
2327
- for i, cmd in enumerate(commands, 1):
2328
- print(f" {i}. {cmd}")
2433
+ try:
2434
+ response = requests.post(api_url, json=payload, timeout=60)
2435
+
2436
+ print(f"📥 API Response status code: {response.status_code}")
2437
+
2438
+ if response.status_code == 200:
2439
+ try:
2440
+ data = response.json()
2441
+ print(f"📄 API Response data received")
2329
2442
 
2330
- return commands
2331
- else:
2332
- print("⚠️ API response did not contain setupInstructions.commands field")
2333
- print("📋 Available fields in response:")
2334
- for key in data.keys():
2335
- print(f" - {key}")
2336
- return []
2337
- except json.JSONDecodeError as e:
2338
- print(f"❌ Failed to parse API response as JSON: {e}")
2339
- print(f"Raw response: {response.text[:500]}...")
2340
- return []
2341
- else:
2342
- print(f" API request failed with status code: {response.status_code}")
2343
- print(f"Error response: {response.text[:500]}...")
2344
- return []
2345
- except requests.exceptions.ConnectionError:
2346
- print(f"❌ Connection error: Could not connect to {api_url}")
2347
- print("⚠️ Make sure the API server is running at localhost:3000")
2348
- return []
2443
+ # Extract setup commands from the response
2444
+ if "setupInstructions" in data and "commands" in data["setupInstructions"]:
2445
+ commands = data["setupInstructions"]["commands"]
2446
+ print(f" Successfully fetched {len(commands)} setup commands from API")
2447
+
2448
+ # Print the commands for reference
2449
+ for i, cmd in enumerate(commands, 1):
2450
+ print(f" {i}. {cmd}")
2451
+
2452
+ return commands
2453
+ else:
2454
+ print("⚠️ API response did not contain setupInstructions.commands field")
2455
+ print("📋 Available fields in response:")
2456
+ for key in data.keys():
2457
+ print(f" - {key}")
2458
+ # Return fallback commands
2459
+ return generate_fallback_commands(gitingest_data)
2460
+ except json.JSONDecodeError as e:
2461
+ print(f"❌ Failed to parse API response as JSON: {e}")
2462
+ print(f"Raw response: {response.text[:500]}...")
2463
+ # Return fallback commands
2464
+ return generate_fallback_commands(gitingest_data)
2465
+ elif response.status_code == 504:
2466
+ print(f"❌ API request timed out (504 Gateway Timeout)")
2467
+ print("⚠️ The server took too long to respond. Using fallback commands instead.")
2468
+ # Return fallback commands
2469
+ return generate_fallback_commands(gitingest_data)
2470
+ else:
2471
+ print(f"❌ API request failed with status code: {response.status_code}")
2472
+ print(f"Error response: {response.text[:500]}...")
2473
+ # Return fallback commands
2474
+ return generate_fallback_commands(gitingest_data)
2475
+ except requests.exceptions.Timeout:
2476
+ print("❌ API request timed out after 60 seconds")
2477
+ print("⚠️ Using fallback commands instead")
2478
+ # Return fallback commands
2479
+ return generate_fallback_commands(gitingest_data)
2480
+ except requests.exceptions.ConnectionError:
2481
+ print(f"❌ Connection error: Could not connect to {api_url}")
2482
+ print("⚠️ Using fallback commands instead")
2483
+ # Return fallback commands
2484
+ return generate_fallback_commands(gitingest_data)
2349
2485
  except Exception as e:
2350
2486
  print(f"❌ Error fetching setup commands from API: {e}")
2351
2487
  import traceback
2352
2488
  traceback.print_exc()
2353
- return []
2489
+ # Return fallback commands
2490
+ return generate_fallback_commands(None)
2354
2491
  finally:
2355
2492
  # Clean up the temporary directory
2356
2493
  print(f"🧹 Cleaning up temporary directory...")
2357
2494
  shutil.rmtree(temp_dir, ignore_errors=True)
2358
2495
 
2496
+ def generate_fallback_commands(gitingest_data):
2497
+ """Generate fallback setup commands based on repository analysis"""
2498
+ print("\n" + "="*80)
2499
+ print("📋 GENERATING FALLBACK SETUP COMMANDS")
2500
+ print("="*80)
2501
+ print("Using basic repository analysis to generate setup commands")
2502
+
2503
+ # Default commands that work for most repositories
2504
+ default_commands = [
2505
+ "apt-get update -y",
2506
+ "apt-get install -y git curl wget",
2507
+ "pip install --upgrade pip setuptools wheel"
2508
+ ]
2509
+
2510
+ # If we don't have any analysis data, return default commands
2511
+ if not gitingest_data:
2512
+ print("⚠️ No repository analysis data available. Using default commands.")
2513
+ return default_commands
2514
+
2515
+ # Extract language and technologies information
2516
+ detected_language = gitingest_data.get("system_info", {}).get("detected_language", "Unknown")
2517
+ detected_technologies = gitingest_data.get("system_info", {}).get("detected_technologies", [])
2518
+ primary_package_manager = gitingest_data.get("system_info", {}).get("primary_package_manager", "Unknown")
2519
+
2520
+ # Add language-specific commands
2521
+ language_commands = []
2522
+
2523
+ print(f"📋 Detected primary language: {detected_language}")
2524
+ print(f"📋 Detected technologies: {', '.join(detected_technologies) if detected_technologies else 'None'}")
2525
+ print(f"📋 Detected package manager: {primary_package_manager}")
2526
+
2527
+ # Python-specific commands
2528
+ if detected_language == "Python" or primary_package_manager == "pip":
2529
+ print("📦 Adding Python-specific setup commands")
2530
+
2531
+ # Check for requirements.txt
2532
+ requirements_check = [
2533
+ "if [ -f requirements.txt ]; then",
2534
+ " echo 'Installing from requirements.txt'",
2535
+ " pip install -r requirements.txt",
2536
+ "elif [ -f setup.py ]; then",
2537
+ " echo 'Installing from setup.py'",
2538
+ " pip install -e .",
2539
+ "fi"
2540
+ ]
2541
+ language_commands.extend(requirements_check)
2542
+
2543
+ # Add common Python packages
2544
+ language_commands.append("pip install pytest numpy pandas matplotlib")
2545
+
2546
+ # JavaScript/Node.js specific commands
2547
+ elif detected_language in ["JavaScript", "TypeScript"] or primary_package_manager in ["npm", "yarn", "pnpm"]:
2548
+ print("📦 Adding JavaScript/Node.js-specific setup commands")
2549
+
2550
+ # Install Node.js if not available
2551
+ language_commands.append("apt-get install -y nodejs npm")
2552
+
2553
+ # Check for package.json
2554
+ package_json_check = [
2555
+ "if [ -f package.json ]; then",
2556
+ " echo 'Installing from package.json'",
2557
+ " npm install",
2558
+ "fi"
2559
+ ]
2560
+ language_commands.extend(package_json_check)
2561
+
2562
+ # Java specific commands
2563
+ elif detected_language == "Java" or primary_package_manager in ["maven", "gradle"]:
2564
+ print("📦 Adding Java-specific setup commands")
2565
+
2566
+ language_commands.append("apt-get install -y openjdk-11-jdk maven gradle")
2567
+
2568
+ # Check for Maven or Gradle
2569
+ build_check = [
2570
+ "if [ -f pom.xml ]; then",
2571
+ " echo 'Building with Maven'",
2572
+ " mvn clean install -DskipTests",
2573
+ "elif [ -f build.gradle ]; then",
2574
+ " echo 'Building with Gradle'",
2575
+ " gradle build --no-daemon",
2576
+ "fi"
2577
+ ]
2578
+ language_commands.extend(build_check)
2579
+
2580
+ # Go specific commands
2581
+ elif detected_language == "Go" or primary_package_manager == "go":
2582
+ print("📦 Adding Go-specific setup commands")
2583
+
2584
+ language_commands.append("apt-get install -y golang-go")
2585
+ language_commands.append("go mod tidy")
2586
+
2587
+ # Rust specific commands
2588
+ elif detected_language == "Rust" or primary_package_manager == "cargo":
2589
+ print("📦 Adding Rust-specific setup commands")
2590
+
2591
+ language_commands.append("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y")
2592
+ language_commands.append("source $HOME/.cargo/env")
2593
+ language_commands.append("cargo build")
2594
+
2595
+ # Combine all commands
2596
+ all_commands = default_commands + language_commands
2597
+
2598
+ print("\n📋 Generated fallback setup commands:")
2599
+ for i, cmd in enumerate(all_commands, 1):
2600
+ print(f" {i}. {cmd}")
2601
+
2602
+ return all_commands
2603
+
2359
2604
  def generate_basic_repo_analysis_from_url(repo_url):
2360
2605
  """Generate basic repository analysis data from a repository URL."""
2361
2606
  import tempfile
@@ -2564,7 +2809,7 @@ def create_ssh_container_function(gpu_type="a10g", timeout_minutes=60, volume=No
2564
2809
  "python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
2565
2810
  "gpg", "ca-certificates", "software-properties-common"
2566
2811
  )
2567
- .pip_install("uv")
2812
+ .pip_install("uv", "modal") # Fast Python package installer and Modal
2568
2813
  .run_commands(
2569
2814
  # Create SSH directory
2570
2815
  "mkdir -p /var/run/sshd",