gitarsenal-cli 1.0.9 ā 1.1.1
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 +97 -0
- package/python/credentials_manager.py +164 -0
- package/python/gitarsenal.py +146 -0
- package/python/manage_credentials.py +119 -0
- package/python/test_modalSandboxScript.py +112 -28
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,97 @@
|
|
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
|
+
## Credentials Setup
|
17
|
+
|
18
|
+
GitArsenal CLI requires several API keys and tokens to function properly:
|
19
|
+
|
20
|
+
- **OpenAI API Key**: Used for debugging failed commands
|
21
|
+
- **Modal Token**: Used to create cloud environments
|
22
|
+
- **Hugging Face Token**: Used for accessing Hugging Face models
|
23
|
+
- **Weights & Biases API Key**: Used for experiment tracking
|
24
|
+
|
25
|
+
You can set up your credentials using the credentials manager:
|
26
|
+
|
27
|
+
```bash
|
28
|
+
python manage_credentials.py setup
|
29
|
+
```
|
30
|
+
|
31
|
+
This will guide you through setting up all required credentials, which will be stored securely in your home directory.
|
32
|
+
|
33
|
+
### Managing Individual Credentials
|
34
|
+
|
35
|
+
You can also manage individual credentials:
|
36
|
+
|
37
|
+
```bash
|
38
|
+
# Set a specific credential
|
39
|
+
python manage_credentials.py set openai_api_key
|
40
|
+
|
41
|
+
# View a credential (masked for security)
|
42
|
+
python manage_credentials.py get modal_token
|
43
|
+
|
44
|
+
# Clear a specific credential
|
45
|
+
python manage_credentials.py clear huggingface_token
|
46
|
+
|
47
|
+
# Clear all credentials
|
48
|
+
python manage_credentials.py clear
|
49
|
+
|
50
|
+
# List all saved credentials (without showing values)
|
51
|
+
python manage_credentials.py list
|
52
|
+
```
|
53
|
+
|
54
|
+
## Usage
|
55
|
+
|
56
|
+
### Creating a Modal Sandbox
|
57
|
+
|
58
|
+
```bash
|
59
|
+
python test_modalSandboxScript.py --gpu A10G --repo-url "https://github.com/username/repo.git"
|
60
|
+
```
|
61
|
+
|
62
|
+
### Options
|
63
|
+
|
64
|
+
- `--gpu`: GPU type (A10G, A100, H100, T4, V100)
|
65
|
+
- `--repo-url`: Repository URL to clone
|
66
|
+
- `--repo-name`: Repository name override
|
67
|
+
- `--setup-commands`: Setup commands to run
|
68
|
+
- `--volume-name`: Name of the Modal volume for persistent storage
|
69
|
+
|
70
|
+
## Security
|
71
|
+
|
72
|
+
Your credentials are stored securely in `~/.gitarsenal/credentials.json` with restrictive file permissions. The file is only readable by your user account.
|
73
|
+
|
74
|
+
## Troubleshooting
|
75
|
+
|
76
|
+
If you encounter authentication issues:
|
77
|
+
|
78
|
+
1. Check that your credentials are set up correctly:
|
79
|
+
```bash
|
80
|
+
python manage_credentials.py list
|
81
|
+
```
|
82
|
+
|
83
|
+
2. If needed, clear and reset your credentials:
|
84
|
+
```bash
|
85
|
+
python manage_credentials.py clear
|
86
|
+
python manage_credentials.py setup
|
87
|
+
```
|
88
|
+
|
89
|
+
3. Ensure Modal CLI is installed and authenticated:
|
90
|
+
```bash
|
91
|
+
pip install modal
|
92
|
+
modal token new
|
93
|
+
```
|
94
|
+
|
95
|
+
## License
|
96
|
+
|
97
|
+
[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,146 @@
|
|
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
|
+
|
13
|
+
def main():
|
14
|
+
parser = argparse.ArgumentParser(description="GitArsenal CLI - GPU-accelerated cloud environments")
|
15
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
16
|
+
|
17
|
+
# Credentials command
|
18
|
+
cred_parser = subparsers.add_parser("credentials", help="Manage credentials")
|
19
|
+
cred_subparsers = cred_parser.add_subparsers(dest="cred_command", help="Credentials command")
|
20
|
+
|
21
|
+
# Credentials setup command
|
22
|
+
cred_setup_parser = cred_subparsers.add_parser("setup", help="Set up all credentials")
|
23
|
+
|
24
|
+
# Credentials set command
|
25
|
+
cred_set_parser = cred_subparsers.add_parser("set", help="Set a specific credential")
|
26
|
+
cred_set_parser.add_argument("key", choices=["openai_api_key", "modal_token", "huggingface_token", "wandb_api_key"],
|
27
|
+
help="The credential to set")
|
28
|
+
|
29
|
+
# Credentials get command
|
30
|
+
cred_get_parser = cred_subparsers.add_parser("get", help="Get a specific credential")
|
31
|
+
cred_get_parser.add_argument("key", choices=["openai_api_key", "modal_token", "huggingface_token", "wandb_api_key"],
|
32
|
+
help="The credential to get")
|
33
|
+
|
34
|
+
# Credentials clear command
|
35
|
+
cred_clear_parser = cred_subparsers.add_parser("clear", help="Clear credentials")
|
36
|
+
cred_clear_parser.add_argument("key", nargs="?", choices=["openai_api_key", "modal_token", "huggingface_token", "wandb_api_key", "all"],
|
37
|
+
default="all", help="The credential to clear (default: all)")
|
38
|
+
|
39
|
+
# Credentials list command
|
40
|
+
cred_list_parser = cred_subparsers.add_parser("list", help="List all saved credentials")
|
41
|
+
|
42
|
+
# Sandbox command
|
43
|
+
sandbox_parser = subparsers.add_parser("sandbox", help="Create a Modal sandbox")
|
44
|
+
sandbox_parser.add_argument("--gpu", type=str, default="A10G", choices=["A10G", "A100", "H100", "T4", "V100"],
|
45
|
+
help="GPU type (default: A10G)")
|
46
|
+
sandbox_parser.add_argument("--repo-url", type=str, help="Repository URL to clone")
|
47
|
+
sandbox_parser.add_argument("--repo-name", type=str, help="Repository name override")
|
48
|
+
sandbox_parser.add_argument("--setup-commands", type=str, nargs="+", help="Setup commands to run")
|
49
|
+
sandbox_parser.add_argument("--volume-name", type=str, help="Name of the Modal volume for persistent storage")
|
50
|
+
|
51
|
+
# SSH container command
|
52
|
+
ssh_parser = subparsers.add_parser("ssh", help="Create a Modal SSH container")
|
53
|
+
ssh_parser.add_argument("--gpu", type=str, default="A10G", choices=["A10G", "A100", "H100", "T4", "V100"],
|
54
|
+
help="GPU type (default: A10G)")
|
55
|
+
ssh_parser.add_argument("--repo-url", type=str, help="Repository URL to clone")
|
56
|
+
ssh_parser.add_argument("--repo-name", type=str, help="Repository name override")
|
57
|
+
ssh_parser.add_argument("--setup-commands", type=str, nargs="+", help="Setup commands to run")
|
58
|
+
ssh_parser.add_argument("--volume-name", type=str, help="Name of the Modal volume for persistent storage")
|
59
|
+
ssh_parser.add_argument("--timeout", type=int, default=60, help="Container timeout in minutes (default: 60)")
|
60
|
+
|
61
|
+
args = parser.parse_args()
|
62
|
+
|
63
|
+
# If no command is provided, show help
|
64
|
+
if not args.command:
|
65
|
+
parser.print_help()
|
66
|
+
return 1
|
67
|
+
|
68
|
+
# Handle credentials commands
|
69
|
+
if args.command == "credentials":
|
70
|
+
cred_args = []
|
71
|
+
|
72
|
+
if not args.cred_command:
|
73
|
+
cred_parser.print_help()
|
74
|
+
return 1
|
75
|
+
|
76
|
+
cred_args.append(args.cred_command)
|
77
|
+
|
78
|
+
if args.cred_command == "set" or args.cred_command == "get":
|
79
|
+
cred_args.append(args.key)
|
80
|
+
elif args.cred_command == "clear" and args.key != "all":
|
81
|
+
cred_args.append(args.key)
|
82
|
+
|
83
|
+
return run_script("manage_credentials.py", cred_args)
|
84
|
+
|
85
|
+
# Handle sandbox command
|
86
|
+
elif args.command == "sandbox":
|
87
|
+
sandbox_args = []
|
88
|
+
|
89
|
+
if args.gpu:
|
90
|
+
sandbox_args.extend(["--gpu", args.gpu])
|
91
|
+
if args.repo_url:
|
92
|
+
sandbox_args.extend(["--repo-url", args.repo_url])
|
93
|
+
if args.repo_name:
|
94
|
+
sandbox_args.extend(["--repo-name", args.repo_name])
|
95
|
+
if args.setup_commands:
|
96
|
+
sandbox_args.extend(["--setup-commands"] + args.setup_commands)
|
97
|
+
if args.volume_name:
|
98
|
+
sandbox_args.extend(["--volume-name", args.volume_name])
|
99
|
+
|
100
|
+
return run_script("test_modalSandboxScript.py", sandbox_args)
|
101
|
+
|
102
|
+
# Handle SSH container command
|
103
|
+
elif args.command == "ssh":
|
104
|
+
ssh_args = []
|
105
|
+
|
106
|
+
if args.gpu:
|
107
|
+
ssh_args.extend(["--gpu", args.gpu])
|
108
|
+
if args.repo_url:
|
109
|
+
ssh_args.extend(["--repo-url", args.repo_url])
|
110
|
+
if args.repo_name:
|
111
|
+
ssh_args.extend(["--repo-name", args.repo_name])
|
112
|
+
if args.setup_commands:
|
113
|
+
ssh_args.extend(["--setup-commands"] + args.setup_commands)
|
114
|
+
if args.volume_name:
|
115
|
+
ssh_args.extend(["--volume-name", args.volume_name])
|
116
|
+
if args.timeout:
|
117
|
+
ssh_args.extend(["--timeout", str(args.timeout)])
|
118
|
+
|
119
|
+
# Use test_modalSandboxScript.py with SSH mode
|
120
|
+
ssh_args.extend(["--ssh"])
|
121
|
+
return run_script("test_modalSandboxScript.py", ssh_args)
|
122
|
+
|
123
|
+
return 0
|
124
|
+
|
125
|
+
def run_script(script_name, args):
|
126
|
+
"""Run a Python script with the given arguments"""
|
127
|
+
# Get the directory of the current script
|
128
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
129
|
+
script_path = os.path.join(script_dir, script_name)
|
130
|
+
|
131
|
+
# Build the command
|
132
|
+
cmd = [sys.executable, script_path] + args
|
133
|
+
|
134
|
+
try:
|
135
|
+
# Run the command
|
136
|
+
result = subprocess.run(cmd)
|
137
|
+
return result.returncode
|
138
|
+
except KeyboardInterrupt:
|
139
|
+
print("\nā ļø Command interrupted by user")
|
140
|
+
return 130
|
141
|
+
except Exception as e:
|
142
|
+
print(f"ā Error running {script_name}: {e}")
|
143
|
+
return 1
|
144
|
+
|
145
|
+
if __name__ == "__main__":
|
146
|
+
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,50 @@ 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 list apps to check if authentication is working
|
482
|
+
import subprocess
|
483
|
+
result = subprocess.run(["modal", "app", "list"], capture_output=True, text=True)
|
484
|
+
if result.returncode != 0 and "not authenticated" in result.stderr:
|
485
|
+
print("š Modal authentication required")
|
486
|
+
|
487
|
+
# Try to use credentials manager first
|
488
|
+
if credentials_manager:
|
489
|
+
modal_token = credentials_manager.get_modal_token()
|
490
|
+
if modal_token:
|
491
|
+
# Set the token in the environment
|
492
|
+
os.environ["MODAL_TOKEN_ID"] = modal_token
|
493
|
+
print("ā
Modal token set from credentials manager")
|
494
|
+
|
495
|
+
# Try to authenticate with the token
|
496
|
+
try:
|
497
|
+
token_result = subprocess.run(["modal", "token", "set", modal_token],
|
498
|
+
capture_output=True, text=True)
|
499
|
+
if token_result.returncode == 0:
|
500
|
+
print("ā
Successfully authenticated with Modal")
|
501
|
+
else:
|
502
|
+
print(f"ā ļø Failed to authenticate with Modal: {token_result.stderr}")
|
503
|
+
print("Please run 'modal token new' manually and try again")
|
504
|
+
return None
|
505
|
+
except Exception as e:
|
506
|
+
print(f"ā ļø Error setting Modal token: {e}")
|
507
|
+
return None
|
508
|
+
else:
|
509
|
+
print("ā ļø Modal is not authenticated. Please run 'modal token new' first")
|
510
|
+
return None
|
511
|
+
except Exception as e:
|
512
|
+
print(f"ā ļø Error checking Modal authentication: {e}")
|
513
|
+
print("Continuing anyway, but Modal operations may fail")
|
514
|
+
|
437
515
|
# Execution history for tracking all commands and their results in this session
|
438
516
|
execution_history = []
|
439
517
|
|
@@ -1853,7 +1931,7 @@ ssh_app = modal.App("ssh-container-app")
|
|
1853
1931
|
"python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
|
1854
1932
|
"gpg", "ca-certificates", "software-properties-common"
|
1855
1933
|
)
|
1856
|
-
.pip_install("uv")
|
1934
|
+
.pip_install("uv", "modal") # Fast Python package installer and Modal
|
1857
1935
|
.run_commands(
|
1858
1936
|
# Create SSH directory
|
1859
1937
|
"mkdir -p /var/run/sshd",
|
@@ -1872,6 +1950,9 @@ ssh_app = modal.App("ssh-container-app")
|
|
1872
1950
|
# Generate SSH host keys
|
1873
1951
|
"ssh-keygen -A",
|
1874
1952
|
|
1953
|
+
# Install Modal CLI
|
1954
|
+
"pip install modal",
|
1955
|
+
|
1875
1956
|
# Set up a nice bash prompt
|
1876
1957
|
"echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
|
1877
1958
|
),
|
@@ -1998,7 +2079,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
1998
2079
|
print(f"ā ļø Could not create default volume: {e}")
|
1999
2080
|
print("ā ļø Continuing without persistent volume")
|
2000
2081
|
volume = None
|
2001
|
-
|
2082
|
+
|
2002
2083
|
# Create SSH-enabled image
|
2003
2084
|
ssh_image = (
|
2004
2085
|
modal.Image.debian_slim()
|
@@ -2007,7 +2088,7 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
2007
2088
|
"python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
|
2008
2089
|
"gpg", "ca-certificates", "software-properties-common"
|
2009
2090
|
)
|
2010
|
-
.pip_install("uv") # Fast Python package installer
|
2091
|
+
.pip_install("uv", "modal") # Fast Python package installer and Modal
|
2011
2092
|
.run_commands(
|
2012
2093
|
# Create SSH directory
|
2013
2094
|
"mkdir -p /var/run/sshd",
|
@@ -2026,11 +2107,14 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
2026
2107
|
# Generate SSH host keys
|
2027
2108
|
"ssh-keygen -A",
|
2028
2109
|
|
2110
|
+
# Install Modal CLI
|
2111
|
+
"pip install modal",
|
2112
|
+
|
2029
2113
|
# Set up a nice bash prompt
|
2030
2114
|
"echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
|
2031
2115
|
)
|
2032
2116
|
)
|
2033
|
-
|
2117
|
+
|
2034
2118
|
# Create the Modal app
|
2035
2119
|
app = modal.App(app_name)
|
2036
2120
|
|
@@ -2564,7 +2648,7 @@ def create_ssh_container_function(gpu_type="a10g", timeout_minutes=60, volume=No
|
|
2564
2648
|
"python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
|
2565
2649
|
"gpg", "ca-certificates", "software-properties-common"
|
2566
2650
|
)
|
2567
|
-
.pip_install("uv")
|
2651
|
+
.pip_install("uv", "modal") # Fast Python package installer and Modal
|
2568
2652
|
.run_commands(
|
2569
2653
|
# Create SSH directory
|
2570
2654
|
"mkdir -p /var/run/sshd",
|