gitarsenal-cli 1.8.3 → 1.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+ cd "$(dirname "$0")"
3
+ source "/Users/rohansharma/RepoStarter/gitarsenal-cli/venv/bin/activate"
4
+ exec "$@"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitarsenal-cli",
3
- "version": "1.8.3",
3
+ "version": "1.8.5",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,485 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ GitArsenal Authentication Manager
4
+
5
+ Handles user registration, login, and session management for the CLI.
6
+ """
7
+
8
+ import os
9
+ import json
10
+ import hashlib
11
+ import secrets
12
+ import getpass
13
+ import time
14
+ import requests
15
+ from pathlib import Path
16
+ from datetime import datetime, timedelta
17
+ from typing import Optional, Dict, Any
18
+
19
+ class AuthManager:
20
+ """
21
+ Manages user authentication for GitArsenal CLI.
22
+ Handles registration, login, session management, and API key storage.
23
+ """
24
+
25
+ def __init__(self, config_dir=None):
26
+ """Initialize the authentication manager"""
27
+ if config_dir:
28
+ self.config_dir = Path(config_dir)
29
+ else:
30
+ self.config_dir = Path.home() / ".gitarsenal"
31
+
32
+ self.auth_file = self.config_dir / "auth.json"
33
+ self.session_file = self.config_dir / "session.json"
34
+ self.ensure_config_dir()
35
+
36
+ # API endpoints for authentication (can be configured)
37
+ self.auth_api_url = os.getenv("GITARSENAL_AUTH_API", "https://api.gitarsenal.com/auth")
38
+
39
+ def ensure_config_dir(self):
40
+ """Ensure the configuration directory exists with proper permissions"""
41
+ if not self.config_dir.exists():
42
+ self.config_dir.mkdir(parents=True)
43
+ # Set restrictive permissions on Unix-like systems
44
+ if os.name == 'posix':
45
+ self.config_dir.chmod(0o700) # Only owner can read/write/execute
46
+
47
+ def hash_password(self, password: str) -> str:
48
+ """Hash a password using SHA-256 with salt"""
49
+ salt = secrets.token_hex(16)
50
+ hash_obj = hashlib.sha256()
51
+ hash_obj.update((password + salt).encode('utf-8'))
52
+ return f"{salt}${hash_obj.hexdigest()}"
53
+
54
+ def verify_password(self, password: str, hashed_password: str) -> bool:
55
+ """Verify a password against its hash"""
56
+ try:
57
+ salt, hash_value = hashed_password.split('$', 1)
58
+ hash_obj = hashlib.sha256()
59
+ hash_obj.update((password + salt).encode('utf-8'))
60
+ return hash_obj.hexdigest() == hash_value
61
+ except ValueError:
62
+ return False
63
+
64
+ def generate_session_token(self) -> str:
65
+ """Generate a secure session token"""
66
+ return secrets.token_urlsafe(32)
67
+
68
+ def load_auth_data(self) -> Dict[str, Any]:
69
+ """Load authentication data from file"""
70
+ if not self.auth_file.exists():
71
+ return {}
72
+
73
+ try:
74
+ with open(self.auth_file, 'r') as f:
75
+ return json.load(f)
76
+ except (json.JSONDecodeError, IOError):
77
+ print("⚠️ Error reading auth file. Starting fresh.")
78
+ return {}
79
+
80
+ def save_auth_data(self, data: Dict[str, Any]) -> bool:
81
+ """Save authentication data to file"""
82
+ try:
83
+ with open(self.auth_file, 'w') as f:
84
+ json.dump(data, f, indent=2)
85
+
86
+ # Set restrictive permissions on Unix-like systems
87
+ if os.name == 'posix':
88
+ self.auth_file.chmod(0o600) # Only owner can read/write
89
+
90
+ return True
91
+ except IOError as e:
92
+ print(f"❌ Error saving auth data: {e}")
93
+ return False
94
+
95
+ def load_session(self) -> Dict[str, Any]:
96
+ """Load session data from file"""
97
+ if not self.session_file.exists():
98
+ return {}
99
+
100
+ try:
101
+ with open(self.session_file, 'r') as f:
102
+ session_data = json.load(f)
103
+
104
+ # Check if session is still valid
105
+ if session_data.get('expires_at'):
106
+ expires_at = datetime.fromisoformat(session_data['expires_at'])
107
+ if datetime.now() > expires_at:
108
+ print("⚠️ Session expired. Please login again.")
109
+ return {}
110
+
111
+ return session_data
112
+ except (json.JSONDecodeError, IOError):
113
+ return {}
114
+
115
+ def save_session(self, session_data: Dict[str, Any]) -> bool:
116
+ """Save session data to file"""
117
+ try:
118
+ with open(self.session_file, 'w') as f:
119
+ json.dump(session_data, f, indent=2)
120
+
121
+ # Set restrictive permissions on Unix-like systems
122
+ if os.name == 'posix':
123
+ self.session_file.chmod(0o600) # Only owner can read/write
124
+
125
+ return True
126
+ except IOError as e:
127
+ print(f"❌ Error saving session: {e}")
128
+ return False
129
+
130
+ def clear_session(self):
131
+ """Clear the current session"""
132
+ if self.session_file.exists():
133
+ self.session_file.unlink()
134
+
135
+ def is_authenticated(self) -> bool:
136
+ """Check if user is currently authenticated"""
137
+ session = self.load_session()
138
+ return bool(session.get('user_id') and session.get('token'))
139
+
140
+ def get_current_user(self) -> Optional[Dict[str, Any]]:
141
+ """Get current user information"""
142
+ session = self.load_session()
143
+ if not session.get('user_id'):
144
+ return None
145
+
146
+ auth_data = self.load_auth_data()
147
+ return auth_data.get('users', {}).get(session['user_id'])
148
+
149
+ def register_user(self, username: str, email: str, password: str) -> bool:
150
+ """Register a new user"""
151
+ print("\n" + "="*60)
152
+ print("🔐 USER REGISTRATION")
153
+ print("="*60)
154
+
155
+ # Validate input
156
+ if not username or len(username) < 3:
157
+ print("❌ Username must be at least 3 characters long.")
158
+ return False
159
+
160
+ if not email or '@' not in email:
161
+ print("❌ Please provide a valid email address.")
162
+ return False
163
+
164
+ if not password or len(password) < 8:
165
+ print("❌ Password must be at least 8 characters long.")
166
+ return False
167
+
168
+ auth_data = self.load_auth_data()
169
+ users = auth_data.get('users', {})
170
+
171
+ # Check if username already exists
172
+ for user_id, user_data in users.items():
173
+ if user_data.get('username') == username:
174
+ print("❌ Username already exists. Please choose a different username.")
175
+ return False
176
+ if user_data.get('email') == email:
177
+ print("❌ Email already registered. Please use a different email.")
178
+ return False
179
+
180
+ # Create new user
181
+ user_id = secrets.token_hex(16)
182
+ hashed_password = self.hash_password(password)
183
+
184
+ new_user = {
185
+ 'user_id': user_id,
186
+ 'username': username,
187
+ 'email': email,
188
+ 'password_hash': hashed_password,
189
+ 'created_at': datetime.now().isoformat(),
190
+ 'api_keys': {},
191
+ 'settings': {
192
+ 'default_gpu': 'A10G',
193
+ 'timeout_minutes': 60,
194
+ 'interactive_mode': False
195
+ }
196
+ }
197
+
198
+ users[user_id] = new_user
199
+ auth_data['users'] = users
200
+
201
+ if self.save_auth_data(auth_data):
202
+ print("✅ Registration successful!")
203
+ print(f"Username: {username}")
204
+ print(f"Email: {email}")
205
+ print("\nYou can now login with your credentials.")
206
+ return True
207
+ else:
208
+ print("❌ Failed to save registration data.")
209
+ return False
210
+
211
+ def login_user(self, username: str, password: str) -> bool:
212
+ """Login a user"""
213
+ print("\n" + "="*60)
214
+ print("🔐 USER LOGIN")
215
+ print("="*60)
216
+
217
+ auth_data = self.load_auth_data()
218
+ users = auth_data.get('users', {})
219
+
220
+ # Find user by username
221
+ user_id = None
222
+ user_data = None
223
+
224
+ for uid, user in users.items():
225
+ if user.get('username') == username:
226
+ user_id = uid
227
+ user_data = user
228
+ break
229
+
230
+ if not user_data:
231
+ print("❌ Username not found. Please register first.")
232
+ return False
233
+
234
+ # Verify password
235
+ if not self.verify_password(password, user_data['password_hash']):
236
+ print("❌ Invalid password.")
237
+ return False
238
+
239
+ # Create session
240
+ session_token = self.generate_session_token()
241
+ session_data = {
242
+ 'user_id': user_id,
243
+ 'token': session_token,
244
+ 'username': username,
245
+ 'created_at': datetime.now().isoformat(),
246
+ 'expires_at': (datetime.now() + timedelta(days=30)).isoformat()
247
+ }
248
+
249
+ if self.save_session(session_data):
250
+ print("✅ Login successful!")
251
+ print(f"Welcome back, {username}!")
252
+ return True
253
+ else:
254
+ print("❌ Failed to create session.")
255
+ return False
256
+
257
+ def logout_user(self):
258
+ """Logout the current user"""
259
+ self.clear_session()
260
+ print("✅ Logged out successfully.")
261
+
262
+ def change_password(self, current_password: str, new_password: str) -> bool:
263
+ """Change user password"""
264
+ user = self.get_current_user()
265
+ if not user:
266
+ print("❌ Not logged in. Please login first.")
267
+ return False
268
+
269
+ # Verify current password
270
+ if not self.verify_password(current_password, user['password_hash']):
271
+ print("❌ Current password is incorrect.")
272
+ return False
273
+
274
+ # Validate new password
275
+ if not new_password or len(new_password) < 8:
276
+ print("❌ New password must be at least 8 characters long.")
277
+ return False
278
+
279
+ # Update password
280
+ auth_data = self.load_auth_data()
281
+ user_id = user['user_id']
282
+ auth_data['users'][user_id]['password_hash'] = self.hash_password(new_password)
283
+
284
+ if self.save_auth_data(auth_data):
285
+ print("✅ Password changed successfully!")
286
+ return True
287
+ else:
288
+ print("❌ Failed to update password.")
289
+ return False
290
+
291
+ def store_api_key(self, service: str, api_key: str) -> bool:
292
+ """Store an API key for the current user"""
293
+ user = self.get_current_user()
294
+ if not user:
295
+ print("❌ Not logged in. Please login first.")
296
+ return False
297
+
298
+ auth_data = self.load_auth_data()
299
+ user_id = user['user_id']
300
+
301
+ if 'api_keys' not in auth_data['users'][user_id]:
302
+ auth_data['users'][user_id]['api_keys'] = {}
303
+
304
+ auth_data['users'][user_id]['api_keys'][service] = api_key
305
+
306
+ if self.save_auth_data(auth_data):
307
+ print(f"✅ {service} API key stored successfully!")
308
+ return True
309
+ else:
310
+ print(f"❌ Failed to store {service} API key.")
311
+ return False
312
+
313
+ def get_api_key(self, service: str) -> Optional[str]:
314
+ """Get an API key for the current user"""
315
+ user = self.get_current_user()
316
+ if not user:
317
+ return None
318
+
319
+ return user.get('api_keys', {}).get(service)
320
+
321
+ def update_user_settings(self, settings: Dict[str, Any]) -> bool:
322
+ """Update user settings"""
323
+ user = self.get_current_user()
324
+ if not user:
325
+ print("❌ Not logged in. Please login first.")
326
+ return False
327
+
328
+ auth_data = self.load_auth_data()
329
+ user_id = user['user_id']
330
+
331
+ if 'settings' not in auth_data['users'][user_id]:
332
+ auth_data['users'][user_id]['settings'] = {}
333
+
334
+ auth_data['users'][user_id]['settings'].update(settings)
335
+
336
+ if self.save_auth_data(auth_data):
337
+ print("✅ Settings updated successfully!")
338
+ return True
339
+ else:
340
+ print("❌ Failed to update settings.")
341
+ return False
342
+
343
+ def get_user_settings(self) -> Dict[str, Any]:
344
+ """Get current user settings"""
345
+ user = self.get_current_user()
346
+ if not user:
347
+ return {}
348
+
349
+ return user.get('settings', {})
350
+
351
+ def delete_account(self, password: str) -> bool:
352
+ """Delete user account"""
353
+ user = self.get_current_user()
354
+ if not user:
355
+ print("❌ Not logged in. Please login first.")
356
+ return False
357
+
358
+ # Verify password
359
+ if not self.verify_password(password, user['password_hash']):
360
+ print("❌ Password is incorrect.")
361
+ return False
362
+
363
+ # Confirm deletion
364
+ print("\n⚠️ WARNING: This action cannot be undone!")
365
+ print("All your data, including API keys and settings, will be permanently deleted.")
366
+ confirm = input("Type 'DELETE' to confirm: ").strip()
367
+
368
+ if confirm != 'DELETE':
369
+ print("❌ Account deletion cancelled.")
370
+ return False
371
+
372
+ # Delete user data
373
+ auth_data = self.load_auth_data()
374
+ user_id = user['user_id']
375
+
376
+ if user_id in auth_data.get('users', {}):
377
+ del auth_data['users'][user_id]
378
+
379
+ if self.save_auth_data(auth_data):
380
+ self.clear_session()
381
+ print("✅ Account deleted successfully.")
382
+ return True
383
+ else:
384
+ print("❌ Failed to delete account.")
385
+ return False
386
+ else:
387
+ print("❌ User data not found.")
388
+ return False
389
+
390
+ def show_user_info(self):
391
+ """Display current user information"""
392
+ user = self.get_current_user()
393
+ if not user:
394
+ print("❌ Not logged in. Please login first.")
395
+ return
396
+
397
+ print("\n" + "="*60)
398
+ print("👤 USER INFORMATION")
399
+ print("="*60)
400
+ print(f"Username: {user['username']}")
401
+ print(f"Email: {user['email']}")
402
+ print(f"Member since: {user['created_at'][:10]}")
403
+
404
+ # Show API keys (masked)
405
+ api_keys = user.get('api_keys', {})
406
+ if api_keys:
407
+ print("\n🔑 API Keys:")
408
+ for service, key in api_keys.items():
409
+ masked_key = key[:8] + "..." + key[-4:] if len(key) > 12 else "***"
410
+ print(f" {service}: {masked_key}")
411
+ else:
412
+ print("\n🔑 API Keys: None configured")
413
+
414
+ # Show settings
415
+ settings = user.get('settings', {})
416
+ if settings:
417
+ print("\n⚙️ Settings:")
418
+ for key, value in settings.items():
419
+ print(f" {key}: {value}")
420
+
421
+ print("="*60)
422
+
423
+ def interactive_auth_flow(self) -> bool:
424
+ """Interactive authentication flow"""
425
+ if self.is_authenticated():
426
+ user = self.get_current_user()
427
+ print(f"✅ Already logged in as: {user['username']}")
428
+ return True
429
+
430
+ print("\n" + "="*60)
431
+ print("🔐 GITARSENAL AUTHENTICATION")
432
+ print("="*60)
433
+ print("Welcome to GitArsenal CLI!")
434
+ print("You need to create an account or login to continue.")
435
+ print("="*60)
436
+
437
+ while True:
438
+ print("\nOptions:")
439
+ print("1. Login")
440
+ print("2. Register")
441
+ print("3. Exit")
442
+
443
+ choice = input("\nSelect an option (1-3): ").strip()
444
+
445
+ if choice == "1":
446
+ return self._login_flow()
447
+ elif choice == "2":
448
+ return self._register_flow()
449
+ elif choice == "3":
450
+ print("👋 Goodbye!")
451
+ return False
452
+ else:
453
+ print("❌ Invalid option. Please try again.")
454
+
455
+ def _login_flow(self) -> bool:
456
+ """Interactive login flow"""
457
+ print("\n--- LOGIN ---")
458
+ username = input("Username: ").strip()
459
+ password = getpass.getpass("Password: ").strip()
460
+
461
+ if self.login_user(username, password):
462
+ return True
463
+ else:
464
+ retry = input("\nTry again? (y/n): ").strip().lower()
465
+ return retry == 'y' and self._login_flow()
466
+
467
+ def _register_flow(self) -> bool:
468
+ """Interactive registration flow"""
469
+ print("\n--- REGISTRATION ---")
470
+ username = input("Username (min 3 characters): ").strip()
471
+ email = input("Email: ").strip()
472
+ password = getpass.getpass("Password (min 8 characters): ").strip()
473
+ confirm_password = getpass.getpass("Confirm password: ").strip()
474
+
475
+ if password != confirm_password:
476
+ print("❌ Passwords do not match.")
477
+ retry = input("\nTry again? (y/n): ").strip().lower()
478
+ return retry == 'y' and self._register_flow()
479
+
480
+ if self.register_user(username, email, password):
481
+ # Auto-login after registration
482
+ return self.login_user(username, password)
483
+ else:
484
+ retry = input("\nTry again? (y/n): ").strip().lower()
485
+ return retry == 'y' and self._register_flow()
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Debug script for GitArsenal Keys delete functionality
4
+ """
5
+
6
+ import sys
7
+ import os
8
+ from pathlib import Path
9
+
10
+ # Add the current directory to the path
11
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
12
+
13
+ from credentials_manager import CredentialsManager
14
+ from gitarsenal_keys import handle_delete
15
+
16
+ def test_delete_functionality():
17
+ """Test the delete functionality step by step"""
18
+ print("🔍 Debugging GitArsenal Keys Delete Functionality")
19
+ print("=" * 60)
20
+
21
+ # Initialize credentials manager
22
+ credentials_manager = CredentialsManager()
23
+
24
+ print(f"📁 Credentials file: {credentials_manager.credentials_file}")
25
+ print(f"📁 Config directory: {credentials_manager.config_dir}")
26
+
27
+ # Check if credentials file exists
28
+ if credentials_manager.credentials_file.exists():
29
+ print("✅ Credentials file exists")
30
+ else:
31
+ print("❌ Credentials file does not exist")
32
+ return
33
+
34
+ # Load current credentials
35
+ credentials = credentials_manager.load_credentials()
36
+ print(f"📋 Current credentials: {list(credentials.keys())}")
37
+
38
+ # Test different service mappings
39
+ test_services = ['openai', 'wandb', 'huggingface', 'gitarsenal-openai', 'claude']
40
+
41
+ for service in test_services:
42
+ print(f"\n🔍 Testing service: {service}")
43
+
44
+ # Generate credential key from service name
45
+ credential_key = f"{service.replace('-', '_')}_api_key"
46
+
47
+ # Special mappings for backward compatibility
48
+ special_mappings = {
49
+ 'openai': 'openai_api_key',
50
+ 'wandb': 'wandb_api_key',
51
+ 'huggingface': 'huggingface_token',
52
+ 'gitarsenal-openai': 'gitarsenal_openai_api_key'
53
+ }
54
+
55
+ # Use special mapping if it exists, otherwise use generated key
56
+ credential_key = special_mappings.get(service, credential_key)
57
+
58
+ print(f" Service: {service}")
59
+ print(f" Generated key: {service.replace('-', '_')}_api_key")
60
+ print(f" Final key: {credential_key}")
61
+ print(f" Exists in credentials: {credential_key in credentials}")
62
+
63
+ if credential_key in credentials:
64
+ print(f" Current value: {credentials[credential_key][:8]}...")
65
+
66
+ # Test the clear_credential method directly
67
+ print(f"\n🧪 Testing clear_credential method directly")
68
+
69
+ # Try to delete a key that exists
70
+ existing_keys = list(credentials.keys())
71
+ if existing_keys:
72
+ test_key = existing_keys[0]
73
+ print(f" Testing deletion of: {test_key}")
74
+
75
+ # Check if key exists before deletion
76
+ before_delete = test_key in credentials
77
+ print(f" Key exists before deletion: {before_delete}")
78
+
79
+ # Delete the key
80
+ success = credentials_manager.clear_credential(test_key)
81
+ print(f" Deletion success: {success}")
82
+
83
+ # Check if key exists after deletion
84
+ credentials_after = credentials_manager.load_credentials()
85
+ after_delete = test_key in credentials_after
86
+ print(f" Key exists after deletion: {after_delete}")
87
+
88
+ # Restore the key for testing
89
+ if not after_delete and before_delete:
90
+ credentials_after[test_key] = credentials[test_key]
91
+ credentials_manager.save_credentials(credentials_after)
92
+ print(f" ✅ Key restored for further testing")
93
+
94
+ # Test with a non-existent key
95
+ print(f"\n🧪 Testing deletion of non-existent key")
96
+ fake_key = "fake_api_key_12345"
97
+ success = credentials_manager.clear_credential(fake_key)
98
+ print(f" Deletion of non-existent key success: {success}")
99
+
100
+ print(f"\n✅ Debug test completed!")
101
+
102
+ def test_delete_command():
103
+ """Test the delete command with mock arguments"""
104
+ print(f"\n🔧 Testing delete command with mock arguments")
105
+ print("=" * 60)
106
+
107
+ from gitarsenal_keys import handle_delete
108
+ import argparse
109
+
110
+ # Create mock arguments
111
+ class MockArgs:
112
+ def __init__(self, service):
113
+ self.service = service
114
+
115
+ # Test with different services
116
+ test_services = ['openai', 'wandb', 'huggingface']
117
+
118
+ for service in test_services:
119
+ print(f"\n🔍 Testing delete command for: {service}")
120
+
121
+ try:
122
+ args = MockArgs(service)
123
+ handle_delete(None, args) # This will fail because we need a real credentials manager
124
+ except Exception as e:
125
+ print(f" Error: {e}")
126
+
127
+ print(f"\n✅ Delete command test completed!")
128
+
129
+ def show_usage_examples():
130
+ """Show how to use the delete command"""
131
+ print(f"\n📋 Usage Examples")
132
+ print("=" * 60)
133
+
134
+ print("To delete an API key:")
135
+ print(" python gitarsenal_keys.py delete --service openai")
136
+ print(" python gitarsenal_keys.py delete --service wandb")
137
+ print(" python gitarsenal_keys.py delete --service huggingface")
138
+ print(" python gitarsenal_keys.py delete --service gitarsenal-openai")
139
+
140
+ print("\nTo list all stored keys:")
141
+ print(" python gitarsenal_keys.py list")
142
+
143
+ print("\nTo view a specific key (masked):")
144
+ print(" python gitarsenal_keys.py view --service openai")
145
+
146
+ print("\nTo add a new key:")
147
+ print(" python gitarsenal_keys.py add --service openai --key sk-...")
148
+ print(" python gitarsenal_keys.py add --service openai # Interactive mode")
149
+
150
+ if __name__ == "__main__":
151
+ print("GitArsenal Keys Delete Debug Tool")
152
+ print("=" * 60)
153
+
154
+ try:
155
+ test_delete_functionality()
156
+ test_delete_command()
157
+ show_usage_examples()
158
+
159
+ print(f"\n🎉 Debug completed!")
160
+ print(f"\nTo test the actual delete command:")
161
+ print(f" python gitarsenal_keys.py delete --service openai")
162
+
163
+ except Exception as e:
164
+ print(f"\n❌ Debug failed with error: {e}")
165
+ import traceback
166
+ traceback.print_exc()
167
+ sys.exit(1)