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.
@@ -22,6 +22,13 @@ except ImportError:
22
22
  except Exception as e:
23
23
  print(f"⚠️ Warning: Error setting up Modal token: {e}")
24
24
 
25
+ # Import authentication manager
26
+ try:
27
+ from auth_manager import AuthManager
28
+ except ImportError:
29
+ print("⚠️ Warning: auth_manager module not found. Authentication will be disabled.")
30
+ AuthManager = None
31
+
25
32
  def check_modal_auth():
26
33
  """Check if Modal is authenticated and set up the built-in token if needed"""
27
34
  try:
@@ -112,11 +119,176 @@ def check_proxy_config():
112
119
  print(f"⚠️ Error checking proxy configuration: {e}")
113
120
  return False
114
121
 
122
+ def _check_authentication(auth_manager):
123
+ """Check if user is authenticated, prompt for login if not"""
124
+ if auth_manager.is_authenticated():
125
+ user = auth_manager.get_current_user()
126
+ print(f"✅ Authenticated as: {user['username']}")
127
+ return True
128
+
129
+ print("\n🔐 Authentication required")
130
+ return auth_manager.interactive_auth_flow()
131
+
132
+ def _handle_auth_commands(auth_manager, args):
133
+ """Handle authentication-related commands"""
134
+ import getpass
135
+
136
+ if args.login:
137
+ print("\n🔐 LOGIN")
138
+ username = input("Username: ").strip()
139
+ password = getpass.getpass("Password: ").strip()
140
+ if auth_manager.login_user(username, password):
141
+ print("✅ Login successful!")
142
+ else:
143
+ print("❌ Login failed.")
144
+
145
+ elif args.register:
146
+ print("\n🔐 REGISTRATION")
147
+ username = input("Username (min 3 characters): ").strip()
148
+ email = input("Email: ").strip()
149
+ password = getpass.getpass("Password (min 8 characters): ").strip()
150
+ confirm_password = getpass.getpass("Confirm password: ").strip()
151
+
152
+ if password != confirm_password:
153
+ print("❌ Passwords do not match.")
154
+ return
155
+
156
+ if auth_manager.register_user(username, email, password):
157
+ print("✅ Registration successful!")
158
+ # Auto-login after registration
159
+ if auth_manager.login_user(username, password):
160
+ print("✅ Auto-login successful!")
161
+ else:
162
+ print("❌ Registration failed.")
163
+
164
+ elif args.logout:
165
+ auth_manager.logout_user()
166
+
167
+ elif args.user_info:
168
+ auth_manager.show_user_info()
169
+
170
+ elif args.change_password:
171
+ if not auth_manager.is_authenticated():
172
+ print("❌ Not logged in. Please login first.")
173
+ return
174
+
175
+ current_password = getpass.getpass("Current password: ").strip()
176
+ new_password = getpass.getpass("New password (min 8 characters): ").strip()
177
+ confirm_password = getpass.getpass("Confirm new password: ").strip()
178
+
179
+ if new_password != confirm_password:
180
+ print("❌ New passwords do not match.")
181
+ return
182
+
183
+ if auth_manager.change_password(current_password, new_password):
184
+ print("✅ Password changed successfully!")
185
+ else:
186
+ print("❌ Failed to change password.")
187
+
188
+ elif args.delete_account:
189
+ if not auth_manager.is_authenticated():
190
+ print("❌ Not logged in. Please login first.")
191
+ return
192
+
193
+ password = getpass.getpass("Enter your password to confirm deletion: ").strip()
194
+ if auth_manager.delete_account(password):
195
+ print("✅ Account deleted successfully!")
196
+ else:
197
+ print("❌ Failed to delete account.")
198
+
199
+ elif args.store_api_key:
200
+ if not auth_manager.is_authenticated():
201
+ print("❌ Not logged in. Please login first.")
202
+ return
203
+
204
+ service = args.store_api_key
205
+ api_key = getpass.getpass(f"Enter {service} API key: ").strip()
206
+
207
+ if auth_manager.store_api_key(service, api_key):
208
+ print(f"✅ {service} API key stored successfully!")
209
+ else:
210
+ print(f"❌ Failed to store {service} API key.")
211
+
212
+ elif args.auth:
213
+ # Interactive authentication management
214
+ while True:
215
+ print("\n" + "="*60)
216
+ print("🔐 AUTHENTICATION MANAGEMENT")
217
+ print("="*60)
218
+ print("1. Login")
219
+ print("2. Register")
220
+ print("3. Show user info")
221
+ print("4. Change password")
222
+ print("5. Store API key")
223
+ print("6. Delete account")
224
+ print("7. Logout")
225
+ print("8. Exit")
226
+
227
+ choice = input("\nSelect an option (1-8): ").strip()
228
+
229
+ if choice == "1":
230
+ username = input("Username: ").strip()
231
+ password = getpass.getpass("Password: ").strip()
232
+ auth_manager.login_user(username, password)
233
+ elif choice == "2":
234
+ username = input("Username (min 3 characters): ").strip()
235
+ email = input("Email: ").strip()
236
+ password = getpass.getpass("Password (min 8 characters): ").strip()
237
+ confirm_password = getpass.getpass("Confirm password: ").strip()
238
+ if password == confirm_password:
239
+ auth_manager.register_user(username, email, password)
240
+ else:
241
+ print("❌ Passwords do not match.")
242
+ elif choice == "3":
243
+ auth_manager.show_user_info()
244
+ elif choice == "4":
245
+ if auth_manager.is_authenticated():
246
+ current_password = getpass.getpass("Current password: ").strip()
247
+ new_password = getpass.getpass("New password (min 8 characters): ").strip()
248
+ confirm_password = getpass.getpass("Confirm new password: ").strip()
249
+ if new_password == confirm_password:
250
+ auth_manager.change_password(current_password, new_password)
251
+ else:
252
+ print("❌ New passwords do not match.")
253
+ else:
254
+ print("❌ Not logged in.")
255
+ elif choice == "5":
256
+ if auth_manager.is_authenticated():
257
+ service = input("Service name (e.g., openai, modal): ").strip()
258
+ api_key = getpass.getpass(f"Enter {service} API key: ").strip()
259
+ auth_manager.store_api_key(service, api_key)
260
+ else:
261
+ print("❌ Not logged in.")
262
+ elif choice == "6":
263
+ if auth_manager.is_authenticated():
264
+ password = getpass.getpass("Enter your password to confirm deletion: ").strip()
265
+ auth_manager.delete_account(password)
266
+ else:
267
+ print("❌ Not logged in.")
268
+ elif choice == "7":
269
+ auth_manager.logout_user()
270
+ elif choice == "8":
271
+ print("👋 Goodbye!")
272
+ break
273
+ else:
274
+ print("❌ Invalid option. Please try again.")
275
+
115
276
  def main():
116
277
  """Main entry point for the GitArsenal CLI"""
117
278
  parser = argparse.ArgumentParser(description="GitArsenal CLI - Tools for AI development")
118
279
  subparsers = parser.add_subparsers(dest="command", help="Command to run")
119
280
 
281
+ # Authentication arguments (global)
282
+ parser.add_argument('--skip-auth', action='store_true', help='Skip authentication check (for development)')
283
+ parser.add_argument('--login', action='store_true', help='Login to GitArsenal')
284
+ parser.add_argument('--register', action='store_true', help='Register new account')
285
+ parser.add_argument('--logout', action='store_true', help='Logout from GitArsenal')
286
+ parser.add_argument('--user-info', action='store_true', help='Show current user information')
287
+ parser.add_argument('--change-password', action='store_true', help='Change password')
288
+ parser.add_argument('--delete-account', action='store_true', help='Delete account')
289
+ parser.add_argument('--store-api-key', type=str, help='Store API key for a service (e.g., openai, modal)')
290
+ parser.add_argument('--auth', action='store_true', help='Manage authentication (login, register, logout)')
291
+
120
292
  # SSH command
121
293
  ssh_parser = subparsers.add_parser("ssh", help="Create a Modal SSH container")
122
294
  ssh_parser.add_argument("--gpu", type=str, default="A10G", help="GPU type (default: A10G)")
@@ -159,6 +331,22 @@ def main():
159
331
  # Parse arguments
160
332
  args = parser.parse_args()
161
333
 
334
+ # Handle authentication commands first
335
+ if AuthManager and (args.login or args.register or args.logout or args.user_info or
336
+ args.change_password or args.delete_account or args.store_api_key or args.auth):
337
+ auth_manager = AuthManager()
338
+ _handle_auth_commands(auth_manager, args)
339
+ return
340
+
341
+ # Check authentication for main commands (unless skipped)
342
+ if AuthManager and not args.skip_auth and args.command:
343
+ auth_manager = AuthManager()
344
+ if not _check_authentication(auth_manager):
345
+ print("\n❌ Authentication required. Please login or register first.")
346
+ print("Use --login to login or --register to create an account.")
347
+ print("Use --skip-auth to bypass authentication for development.")
348
+ return
349
+
162
350
  if args.command == "ssh":
163
351
  if args.use_proxy:
164
352
  # Use proxy service for SSH container
@@ -14,7 +14,7 @@ from credentials_manager import CredentialsManager
14
14
  def main():
15
15
  parser = argparse.ArgumentParser(description='GitArsenal Keys Management')
16
16
  parser.add_argument('command', choices=['add', 'list', 'view', 'delete'], help='Command to execute')
17
- parser.add_argument('--service', help='Service name (openai, wandb, huggingface)')
17
+ parser.add_argument('--service', help='Service name (openai_api_key, WANDB_API_KEY, HUGGINGFACE_TOKEN, modal_token)')
18
18
  parser.add_argument('--key', help='API key (for add command)')
19
19
 
20
20
  args = parser.parse_args()
@@ -38,23 +38,30 @@ def handle_add(credentials_manager, args):
38
38
 
39
39
  if not service:
40
40
  print("❌ Service name is required. Use --service option.")
41
+ print("💡 Available service names:")
42
+ print(" openai_api_key")
43
+ print(" WANDB_API_KEY")
44
+ print(" HUGGINGFACE_TOKEN")
45
+ print(" modal_token")
46
+ print(" gitarsenal_openai_api_key")
47
+ print("\n💡 Example: gitarsenal keys add --service openai_api_key")
41
48
  sys.exit(1)
42
49
 
43
50
  if not key:
44
51
  # Prompt for the API key interactively
45
- print(f"\n🔑 {service.upper()} API KEY REQUIRED")
52
+ print(f"\n🔑 {service.upper()} REQUIRED")
46
53
  print("=" * 50)
47
54
 
48
55
  # Get appropriate prompt based on service
49
56
  prompts = {
50
- 'openai': "Please enter your OpenAI API key:\nYou can get your API key from: https://platform.openai.com/api-keys",
51
- 'wandb': "Please enter your Weights & Biases API key:\nYou can get your API key from: https://wandb.ai/authorize",
52
- 'huggingface': "Please enter your Hugging Face token:\nYou can get your token from: https://huggingface.co/settings/tokens",
53
- 'gitarsenal-openai': "Please enter GitArsenal's OpenAI API key for debugging:",
54
- 'claude': "Please enter your Claude API key:\nYou can get your API key from: https://console.anthropic.com/"
57
+ 'openai_api_key': "Please enter your OpenAI API key:\nYou can get your API key from: https://platform.openai.com/api-keys",
58
+ 'WANDB_API_KEY': "Please enter your Weights & Biases API key:\nYou can get your API key from: https://wandb.ai/authorize",
59
+ 'HUGGINGFACE_TOKEN': "Please enter your Hugging Face token:\nYou can get your token from: https://huggingface.co/settings/tokens",
60
+ 'gitarsenal_openai_api_key': "Please enter GitArsenal's OpenAI API key for debugging:",
61
+ 'modal_token': "Please enter your Modal token:\nYou can get your token from: https://modal.com/account/tokens"
55
62
  }
56
63
 
57
- prompt = prompts.get(service, f"Please enter your {service} API key:")
64
+ prompt = prompts.get(service, f"Please enter your {service}:")
58
65
  print(prompt)
59
66
  print("-" * 50)
60
67
 
@@ -72,20 +79,8 @@ def handle_add(credentials_manager, args):
72
79
  print(f"❌ Error getting API key: {e}")
73
80
  sys.exit(1)
74
81
 
75
- # Generate credential key from service name
76
- # Convert service name to a standardized key format
77
- credential_key = f"{service.replace('-', '_')}_api_key"
78
-
79
- # Special mappings for backward compatibility
80
- special_mappings = {
81
- 'openai': 'openai_api_key',
82
- 'wandb': 'wandb_api_key',
83
- 'huggingface': 'huggingface_token',
84
- 'gitarsenal-openai': 'gitarsenal_openai_api_key'
85
- }
86
-
87
- # Use special mapping if it exists, otherwise use generated key
88
- credential_key = special_mappings.get(service, credential_key)
82
+ # Use the service name directly as the credential key
83
+ credential_key = service
89
84
 
90
85
  # Save the credential
91
86
  credentials = credentials_manager.load_credentials()
@@ -113,10 +108,11 @@ def handle_list(credentials_manager):
113
108
  # Map credential keys to display names (for known services)
114
109
  key_mapping = {
115
110
  'openai_api_key': 'OpenAI',
116
- 'wandb_api_key': 'Weights & Biases',
117
- 'huggingface_token': 'Hugging Face',
111
+ 'WANDB_API_KEY': 'Weights & Biases',
112
+ 'HUGGINGFACE_TOKEN': 'Hugging Face',
118
113
  'gitarsenal_openai_api_key': 'GitArsenal OpenAI',
119
- 'claude_api_key': 'Claude'
114
+ 'claude_api_key': 'Claude',
115
+ 'modal_token': 'Modal'
120
116
  }
121
117
 
122
118
  for key, value in credentials.items():
@@ -124,14 +120,16 @@ def handle_list(credentials_manager):
124
120
  if key in key_mapping:
125
121
  display_name = key_mapping[key]
126
122
  else:
127
- # Convert key back to service name for display
128
- service_name = key.replace('_api_key', '').replace('_token', '').replace('_', '-')
129
- display_name = service_name.title()
123
+ # Use the key name directly for unknown services
124
+ display_name = key
130
125
 
131
126
  masked_value = value[:8] + "*" * (len(value) - 12) + value[-4:] if len(value) > 12 else "*" * len(value)
132
127
  print(f" {display_name}: {masked_value}")
133
128
 
134
129
  print(f"\n📁 Storage location: {credentials_manager.credentials_file}")
130
+ print("\n💡 To delete a key, use the exact service name:")
131
+ for key in credentials.keys():
132
+ print(f" gitarsenal keys delete --service {key}")
135
133
 
136
134
  def handle_view(credentials_manager, args):
137
135
  """Handle viewing a specific API key (masked)"""
@@ -139,31 +137,26 @@ def handle_view(credentials_manager, args):
139
137
 
140
138
  if not service:
141
139
  print("❌ Service name is required. Use --service option.")
140
+ print("💡 Available service names:")
141
+ print(" openai_api_key")
142
+ print(" WANDB_API_KEY")
143
+ print(" HUGGINGFACE_TOKEN")
144
+ print(" modal_token")
142
145
  sys.exit(1)
143
146
 
144
- # Generate credential key from service name
145
- credential_key = f"{service.replace('-', '_')}_api_key"
146
-
147
- # Special mappings for backward compatibility
148
- special_mappings = {
149
- 'openai': 'openai_api_key',
150
- 'wandb': 'wandb_api_key',
151
- 'huggingface': 'huggingface_token',
152
- 'gitarsenal-openai': 'gitarsenal_openai_api_key'
153
- }
154
-
155
- # Use special mapping if it exists, otherwise use generated key
156
- credential_key = special_mappings.get(service, credential_key)
147
+ # Use the service name directly as the credential key
148
+ credential_key = service
157
149
  credentials = credentials_manager.load_credentials()
158
150
 
159
151
  if credential_key not in credentials:
160
152
  print(f"❌ No API key found for {service}")
153
+ print(f"💡 Available keys: {list(credentials.keys())}")
161
154
  sys.exit(1)
162
155
 
163
156
  value = credentials[credential_key]
164
157
  masked_value = value[:8] + "*" * (len(value) - 12) + value[-4:] if len(value) > 12 else "*" * len(value)
165
158
 
166
- print(f"🔑 {service.upper()} API Key:")
159
+ print(f"🔑 {service}:")
167
160
  print(f" {masked_value}")
168
161
  print(f"📁 Stored in: {credentials_manager.credentials_file}")
169
162
 
@@ -173,27 +166,22 @@ def handle_delete(credentials_manager, args):
173
166
 
174
167
  if not service:
175
168
  print("❌ Service name is required. Use --service option.")
169
+ print("💡 Available service names:")
170
+ print(" openai_api_key")
171
+ print(" WANDB_API_KEY")
172
+ print(" HUGGINGFACE_TOKEN")
173
+ print(" modal_token")
176
174
  sys.exit(1)
177
175
 
178
- # Generate credential key from service name
179
- credential_key = f"{service.replace('-', '_')}_api_key"
180
-
181
- # Special mappings for backward compatibility
182
- special_mappings = {
183
- 'openai': 'openai_api_key',
184
- 'wandb': 'wandb_api_key',
185
- 'huggingface': 'huggingface_token',
186
- 'gitarsenal-openai': 'gitarsenal_openai_api_key'
187
- }
188
-
189
- # Use special mapping if it exists, otherwise use generated key
190
- credential_key = special_mappings.get(service, credential_key)
176
+ # Use the service name directly as the credential key
177
+ credential_key = service
191
178
  success = credentials_manager.clear_credential(credential_key)
192
179
 
193
180
  if success:
194
181
  print(f"✅ API key for {service} deleted successfully")
195
182
  else:
196
183
  print(f"❌ No API key found for {service}")
184
+ print("💡 Use 'gitarsenal keys list' to see available keys")
197
185
 
198
186
  if __name__ == "__main__":
199
187
  main()