gitarsenal-cli 1.1.1 → 1.1.3

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.
@@ -9,6 +9,76 @@ import argparse
9
9
  import sys
10
10
  import os
11
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 check_proxy_config():
63
+ """Check if Modal proxy is configured"""
64
+ try:
65
+ # Import the proxy client
66
+ from gitarsenal_proxy_client import GitArsenalProxyClient
67
+
68
+ # Create client and load config
69
+ client = GitArsenalProxyClient()
70
+ config = client.load_config()
71
+
72
+ # Check if proxy URL and API key are configured
73
+ if "proxy_url" in config and "api_key" in config:
74
+ return True
75
+ else:
76
+ return False
77
+ except ImportError:
78
+ return False
79
+ except Exception as e:
80
+ print(f"āš ļø Error checking proxy configuration: {e}")
81
+ return False
12
82
 
13
83
  def main():
14
84
  parser = argparse.ArgumentParser(description="GitArsenal CLI - GPU-accelerated cloud environments")
@@ -47,8 +117,10 @@ def main():
47
117
  sandbox_parser.add_argument("--repo-name", type=str, help="Repository name override")
48
118
  sandbox_parser.add_argument("--setup-commands", type=str, nargs="+", help="Setup commands to run")
49
119
  sandbox_parser.add_argument("--volume-name", type=str, help="Name of the Modal volume for persistent storage")
120
+ sandbox_parser.add_argument("--use-api", action="store_true", help="Use API for setup commands")
121
+ sandbox_parser.add_argument("--use-proxy", action="store_true", help="Use Modal proxy service instead of direct Modal access")
50
122
 
51
- # SSH container command
123
+ # SSH command
52
124
  ssh_parser = subparsers.add_parser("ssh", help="Create a Modal SSH container")
53
125
  ssh_parser.add_argument("--gpu", type=str, default="A10G", choices=["A10G", "A100", "H100", "T4", "V100"],
54
126
  help="GPU type (default: A10G)")
@@ -57,90 +129,387 @@ def main():
57
129
  ssh_parser.add_argument("--setup-commands", type=str, nargs="+", help="Setup commands to run")
58
130
  ssh_parser.add_argument("--volume-name", type=str, help="Name of the Modal volume for persistent storage")
59
131
  ssh_parser.add_argument("--timeout", type=int, default=60, help="Container timeout in minutes (default: 60)")
132
+ ssh_parser.add_argument("--use-api", action="store_true", help="Use API for setup commands")
133
+ ssh_parser.add_argument("--use-proxy", action="store_true", help="Use Modal proxy service instead of direct Modal access")
134
+
135
+ # Proxy command
136
+ proxy_parser = subparsers.add_parser("proxy", help="Configure and use Modal proxy service")
137
+ proxy_subparsers = proxy_parser.add_subparsers(dest="proxy_command", help="Proxy command")
138
+
139
+ # Proxy configure command
140
+ proxy_configure_parser = proxy_subparsers.add_parser("configure", help="Configure Modal proxy service")
141
+
142
+ # Proxy status command
143
+ proxy_status_parser = proxy_subparsers.add_parser("status", help="Check Modal proxy service status")
144
+
145
+ # Proxy sandbox command
146
+ proxy_sandbox_parser = proxy_subparsers.add_parser("sandbox", help="Create a sandbox through Modal proxy")
147
+ proxy_sandbox_parser.add_argument("--gpu", type=str, default="A10G", choices=["A10G", "A100", "H100", "T4", "V100"],
148
+ help="GPU type (default: A10G)")
149
+ proxy_sandbox_parser.add_argument("--repo-url", type=str, help="Repository URL to clone")
150
+ proxy_sandbox_parser.add_argument("--repo-name", type=str, help="Repository name override")
151
+ proxy_sandbox_parser.add_argument("--setup-commands", type=str, nargs="+", help="Setup commands to run")
152
+ proxy_sandbox_parser.add_argument("--volume-name", type=str, help="Name of the Modal volume for persistent storage")
153
+ proxy_sandbox_parser.add_argument("--wait", action="store_true", help="Wait for sandbox to be ready")
154
+
155
+ # Proxy ssh command
156
+ proxy_ssh_parser = proxy_subparsers.add_parser("ssh", help="Create an SSH container through Modal proxy")
157
+ proxy_ssh_parser.add_argument("--gpu", type=str, default="A10G", choices=["A10G", "A100", "H100", "T4", "V100"],
158
+ help="GPU type (default: A10G)")
159
+ proxy_ssh_parser.add_argument("--repo-url", type=str, help="Repository URL to clone")
160
+ proxy_ssh_parser.add_argument("--repo-name", type=str, help="Repository name override")
161
+ proxy_ssh_parser.add_argument("--setup-commands", type=str, nargs="+", help="Setup commands to run")
162
+ proxy_ssh_parser.add_argument("--volume-name", type=str, help="Name of the Modal volume for persistent storage")
163
+ proxy_ssh_parser.add_argument("--timeout", type=int, default=60, help="Container timeout in minutes (default: 60)")
164
+ proxy_ssh_parser.add_argument("--wait", action="store_true", help="Wait for container to be ready")
60
165
 
61
166
  args = parser.parse_args()
62
167
 
63
- # If no command is provided, show help
64
168
  if not args.command:
65
169
  parser.print_help()
66
- return 1
170
+ return 0
171
+
172
+ # For sandbox and SSH commands, check Modal authentication first
173
+ if args.command in ["sandbox", "ssh"]:
174
+ # Check if using proxy service
175
+ if hasattr(args, 'use_proxy') and args.use_proxy:
176
+ # Check if proxy is configured
177
+ if not check_proxy_config():
178
+ print("\nāš ļø Modal proxy service is not configured.")
179
+ print("Please run './gitarsenal.py proxy configure' first.")
180
+ return 1
181
+ else:
182
+ # Check if Modal is authenticated
183
+ if not check_modal_auth():
184
+ print("\nāš ļø Please authenticate with Modal before proceeding.")
185
+ return 1
186
+
187
+ # Try to load credentials and set Modal token if available
188
+ try:
189
+ from credentials_manager import CredentialsManager
190
+ credentials_manager = CredentialsManager()
191
+ credentials = credentials_manager.load_credentials()
192
+
193
+ if "modal_token" in credentials:
194
+ # Set the Modal token in the environment
195
+ os.environ["MODAL_TOKEN_ID"] = credentials["modal_token"]
196
+ print("āœ… Using Modal token from credentials")
197
+
198
+ # Try to authenticate with the token
199
+ try:
200
+ token_result = subprocess.run(
201
+ ["modal", "token", "set", credentials["modal_token"]],
202
+ capture_output=True, text=True
203
+ )
204
+ if token_result.returncode == 0:
205
+ print("āœ… Successfully authenticated with Modal")
206
+ except Exception as e:
207
+ print(f"āš ļø Error setting Modal token: {e}")
208
+ except ImportError:
209
+ print("āš ļø Could not load credentials manager")
210
+ except Exception as e:
211
+ print(f"āš ļø Error loading credentials: {e}")
67
212
 
68
213
  # Handle credentials commands
69
214
  if args.command == "credentials":
70
- cred_args = []
71
-
72
- if not args.cred_command:
73
- cred_parser.print_help()
74
- return 1
215
+ try:
216
+ from credentials_manager import CredentialsManager
217
+ credentials_manager = CredentialsManager()
75
218
 
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)
219
+ if args.cred_command == "setup":
220
+ # Set up all credentials
221
+ print("šŸ”§ Setting up all credentials")
222
+
223
+ # Modal token
224
+ modal_token = credentials_manager.get_modal_token()
225
+ if modal_token:
226
+ print("āœ… Modal token set")
227
+ else:
228
+ print("āš ļø Modal token not set")
229
+
230
+ # OpenAI API key
231
+ openai_api_key = credentials_manager.get_openai_api_key()
232
+ if openai_api_key:
233
+ print("āœ… OpenAI API key set")
234
+ else:
235
+ print("āš ļø OpenAI API key not set")
236
+
237
+ # Hugging Face token
238
+ huggingface_token = credentials_manager.get_huggingface_token()
239
+ if huggingface_token:
240
+ print("āœ… Hugging Face token set")
241
+ else:
242
+ print("āš ļø Hugging Face token not set")
243
+
244
+ # Weights & Biases API key
245
+ wandb_api_key = credentials_manager.get_wandb_api_key()
246
+ if wandb_api_key:
247
+ print("āœ… Weights & Biases API key set")
248
+ else:
249
+ print("āš ļø Weights & Biases API key not set")
250
+
251
+ elif args.cred_command == "set":
252
+ # Set a specific credential
253
+ if args.key == "modal_token":
254
+ modal_token = credentials_manager.get_modal_token()
255
+ if modal_token:
256
+ print("āœ… Modal token set")
257
+ else:
258
+ print("āš ļø Modal token not set")
259
+ elif args.key == "openai_api_key":
260
+ openai_api_key = credentials_manager.get_openai_api_key()
261
+ if openai_api_key:
262
+ print("āœ… OpenAI API key set")
263
+ else:
264
+ print("āš ļø OpenAI API key not set")
265
+ elif args.key == "huggingface_token":
266
+ huggingface_token = credentials_manager.get_huggingface_token()
267
+ if huggingface_token:
268
+ print("āœ… Hugging Face token set")
269
+ else:
270
+ print("āš ļø Hugging Face token not set")
271
+ elif args.key == "wandb_api_key":
272
+ wandb_api_key = credentials_manager.get_wandb_api_key()
273
+ if wandb_api_key:
274
+ print("āœ… Weights & Biases API key set")
275
+ else:
276
+ print("āš ļø Weights & Biases API key not set")
277
+
278
+ elif args.cred_command == "get":
279
+ # Get a specific credential
280
+ credentials = credentials_manager.load_credentials()
281
+ if args.key in credentials:
282
+ # Mask the credential for security
283
+ value = credentials[args.key]
284
+ masked_value = value[:4] + "*" * (len(value) - 8) + value[-4:] if len(value) > 8 else "****"
285
+ print(f"{args.key}: {masked_value}")
286
+ else:
287
+ print(f"āš ļø {args.key} not found")
288
+
289
+ elif args.cred_command == "clear":
290
+ # Clear credentials
291
+ if args.key == "all":
292
+ credentials_manager.clear_all_credentials()
293
+ print("āœ… All credentials cleared")
294
+ else:
295
+ if credentials_manager.clear_credential(args.key):
296
+ print(f"āœ… {args.key} cleared")
297
+ else:
298
+ print(f"āš ļø {args.key} not found")
299
+
300
+ elif args.cred_command == "list":
301
+ # List all credentials
302
+ credentials = credentials_manager.load_credentials()
303
+ if credentials:
304
+ print("šŸ“‹ Saved credentials:")
305
+ for key in credentials:
306
+ print(f" - {key}")
307
+ else:
308
+ print("āš ļø No credentials found")
82
309
 
83
- return run_script("manage_credentials.py", cred_args)
310
+ else:
311
+ print("āš ļø Unknown credentials command")
312
+ return 1
313
+
314
+ except ImportError:
315
+ print("āŒ Could not import credentials_manager module")
316
+ return 1
317
+ except Exception as e:
318
+ print(f"āŒ Error: {e}")
319
+ return 1
84
320
 
85
321
  # Handle sandbox command
86
322
  elif args.command == "sandbox":
87
- sandbox_args = []
323
+ try:
324
+ # Check if using proxy service
325
+ if args.use_proxy:
326
+ # Import proxy client
327
+ try:
328
+ from gitarsenal_proxy_client import GitArsenalProxyClient
329
+ client = GitArsenalProxyClient()
330
+
331
+ # Create sandbox through proxy
332
+ result = client.create_sandbox(
333
+ gpu_type=args.gpu,
334
+ repo_url=args.repo_url,
335
+ repo_name=args.repo_name,
336
+ setup_commands=args.setup_commands,
337
+ volume_name=args.volume_name,
338
+ wait=True # Always wait for sandbox to be ready
339
+ )
340
+
341
+ if not result:
342
+ print("āŒ Failed to create sandbox through proxy service")
343
+ return 1
344
+
345
+ print("āœ… Sandbox created successfully through proxy service")
346
+
347
+ except ImportError:
348
+ print("āŒ Could not import gitarsenal_proxy_client module")
349
+ print("Please make sure gitarsenal_proxy_client.py is in the same directory")
350
+ return 1
351
+ else:
352
+ # Import sandbox creation function
353
+ from test_modalSandboxScript import create_modal_sandbox
354
+
355
+ # Create sandbox directly
356
+ result = create_modal_sandbox(
357
+ args.gpu,
358
+ repo_url=args.repo_url,
359
+ repo_name=args.repo_name,
360
+ setup_commands=args.setup_commands,
361
+ volume_name=args.volume_name
362
+ )
363
+
364
+ if not result:
365
+ print("āŒ Failed to create sandbox")
366
+ return 1
367
+
368
+ print("āœ… Sandbox created successfully")
88
369
 
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)
370
+ except ImportError as e:
371
+ print(f"āŒ Could not import required module: {e}")
372
+ return 1
373
+ except Exception as e:
374
+ print(f"āŒ Error: {e}")
375
+ return 1
101
376
 
102
- # Handle SSH container command
377
+ # Handle SSH command
103
378
  elif args.command == "ssh":
104
- ssh_args = []
379
+ try:
380
+ # Check if using proxy service
381
+ if args.use_proxy:
382
+ # Import proxy client
383
+ try:
384
+ from gitarsenal_proxy_client import GitArsenalProxyClient
385
+ client = GitArsenalProxyClient()
386
+
387
+ # Create SSH container through proxy
388
+ result = client.create_ssh_container(
389
+ gpu_type=args.gpu,
390
+ repo_url=args.repo_url,
391
+ repo_name=args.repo_name,
392
+ setup_commands=args.setup_commands,
393
+ volume_name=args.volume_name,
394
+ timeout=args.timeout,
395
+ wait=True # Always wait for container to be ready
396
+ )
397
+
398
+ if not result:
399
+ print("āŒ Failed to create SSH container through proxy service")
400
+ return 1
401
+
402
+ print("āœ… SSH container created successfully through proxy service")
403
+
404
+ except ImportError:
405
+ print("āŒ Could not import gitarsenal_proxy_client module")
406
+ print("Please make sure gitarsenal_proxy_client.py is in the same directory")
407
+ return 1
408
+ else:
409
+ # Import SSH container creation function
410
+ from test_modalSandboxScript import create_modal_ssh_container
411
+
412
+ # Create SSH container directly
413
+ result = create_modal_ssh_container(
414
+ args.gpu,
415
+ repo_url=args.repo_url,
416
+ repo_name=args.repo_name,
417
+ setup_commands=args.setup_commands,
418
+ volume_name=args.volume_name,
419
+ timeout_minutes=args.timeout
420
+ )
421
+
422
+ if not result:
423
+ print("āŒ Failed to create SSH container")
424
+ return 1
425
+
426
+ print("āœ… SSH container created successfully")
105
427
 
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)
428
+ except ImportError as e:
429
+ print(f"āŒ Could not import required module: {e}")
430
+ return 1
431
+ except Exception as e:
432
+ print(f"āŒ Error: {e}")
433
+ return 1
130
434
 
131
- # Build the command
132
- cmd = [sys.executable, script_path] + args
435
+ # Handle proxy commands
436
+ elif args.command == "proxy":
437
+ if not args.proxy_command:
438
+ proxy_parser.print_help()
439
+ return 0
440
+
441
+ try:
442
+ # Import proxy client
443
+ from gitarsenal_proxy_client import GitArsenalProxyClient
444
+ client = GitArsenalProxyClient()
445
+
446
+ if args.proxy_command == "configure":
447
+ # Configure proxy service
448
+ client.configure(interactive=True)
449
+
450
+ elif args.proxy_command == "status":
451
+ # Check proxy service status
452
+ response = client.health_check()
453
+ if response["success"]:
454
+ print(f"āœ… Proxy service is running: {response['data']['message']}")
455
+ else:
456
+ print(f"āŒ Proxy service health check failed: {response['error']}")
457
+ return 1
458
+
459
+ elif args.proxy_command == "sandbox":
460
+ # Create sandbox through proxy
461
+ result = client.create_sandbox(
462
+ gpu_type=args.gpu,
463
+ repo_url=args.repo_url,
464
+ repo_name=args.repo_name,
465
+ setup_commands=args.setup_commands,
466
+ volume_name=args.volume_name,
467
+ wait=args.wait
468
+ )
469
+
470
+ if not result:
471
+ print("āŒ Failed to create sandbox through proxy service")
472
+ return 1
473
+
474
+ print("āœ… Sandbox created successfully through proxy service")
475
+
476
+ elif args.proxy_command == "ssh":
477
+ # Create SSH container through proxy
478
+ result = client.create_ssh_container(
479
+ gpu_type=args.gpu,
480
+ repo_url=args.repo_url,
481
+ repo_name=args.repo_name,
482
+ setup_commands=args.setup_commands,
483
+ volume_name=args.volume_name,
484
+ timeout=args.timeout,
485
+ wait=args.wait
486
+ )
487
+
488
+ if not result:
489
+ print("āŒ Failed to create SSH container through proxy service")
490
+ return 1
491
+
492
+ print("āœ… SSH container created successfully through proxy service")
493
+
494
+ else:
495
+ print(f"āŒ Unknown proxy command: {args.proxy_command}")
496
+ proxy_parser.print_help()
497
+ return 1
498
+
499
+ except ImportError:
500
+ print("āŒ Could not import gitarsenal_proxy_client module")
501
+ print("Please make sure gitarsenal_proxy_client.py is in the same directory")
502
+ return 1
503
+ except Exception as e:
504
+ print(f"āŒ Error: {e}")
505
+ return 1
133
506
 
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}")
507
+ else:
508
+ print(f"āŒ Unknown command: {args.command}")
509
+ parser.print_help()
143
510
  return 1
511
+
512
+ return 0
144
513
 
145
514
  if __name__ == "__main__":
146
515
  sys.exit(main())