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 +40 -71
- package/package.json +1 -1
- package/python/README.md +140 -0
- package/python/credentials_manager.py +164 -0
- package/python/gitarsenal.py +228 -0
- package/python/manage_credentials.py +119 -0
- package/python/requirements.txt +4 -0
- package/python/test_modalSandboxScript.py +308 -63
package/README.md
CHANGED
@@ -1,105 +1,74 @@
|
|
1
1
|
# GitArsenal CLI
|
2
2
|
|
3
|
-
A
|
3
|
+
A tool for creating and managing GPU-accelerated development environments using Modal.
|
4
4
|
|
5
5
|
## Features
|
6
6
|
|
7
|
-
-
|
8
|
-
-
|
9
|
-
- Persistent
|
10
|
-
-
|
11
|
-
-
|
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
|
-
##
|
13
|
+
## API Key Management
|
14
14
|
|
15
|
-
|
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
|
-
|
18
|
-
- Python 3.8 or higher
|
19
|
-
- Modal CLI (`pip install modal`)
|
20
|
-
- Git
|
17
|
+
### Supported Services
|
21
18
|
|
22
|
-
|
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
|
-
|
25
|
-
npm install -g gitarsenal-cli
|
26
|
-
```
|
23
|
+
### Managing API Keys
|
27
24
|
|
28
|
-
|
25
|
+
#### Adding a new API key
|
29
26
|
|
30
27
|
```bash
|
31
|
-
|
32
|
-
|
28
|
+
# Add a key interactively (will prompt for the key)
|
29
|
+
python test_modalSandboxScript.py keys add --service openai
|
33
30
|
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
35
|
+
#### Listing saved API keys
|
39
36
|
|
40
37
|
```bash
|
41
|
-
|
38
|
+
python test_modalSandboxScript.py keys list
|
42
39
|
```
|
43
40
|
|
44
|
-
|
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
|
-
|
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
|
-
|
47
|
+
#### Deleting an API key
|
67
48
|
|
68
49
|
```bash
|
69
|
-
|
50
|
+
python test_modalSandboxScript.py keys delete --service openai
|
70
51
|
```
|
71
52
|
|
72
|
-
##
|
53
|
+
## Creating a Modal Container
|
73
54
|
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
80
|
-
|
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
|
-
|
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
|
-
##
|
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
|
-
|
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
|
-
|
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
|
-
|
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
package/python/README.md
ADDED
@@ -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())
|
@@ -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
|
-
#
|
60
|
-
api_key =
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
243
|
-
|
244
|
-
print("\n
|
245
|
-
|
246
|
-
|
247
|
-
print(
|
248
|
-
|
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
|
-
|
2313
|
-
|
2314
|
-
|
2315
|
-
|
2316
|
-
|
2317
|
-
|
2318
|
-
|
2319
|
-
|
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
|
-
|
2331
|
-
|
2332
|
-
|
2333
|
-
|
2334
|
-
|
2335
|
-
|
2336
|
-
|
2337
|
-
|
2338
|
-
|
2339
|
-
|
2340
|
-
|
2341
|
-
|
2342
|
-
|
2343
|
-
|
2344
|
-
|
2345
|
-
|
2346
|
-
|
2347
|
-
|
2348
|
-
|
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
|
-
|
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",
|