gitarsenal-cli 1.1.2 → 1.1.4

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.
@@ -59,6 +59,55 @@ def check_modal_auth():
59
59
  print(f"⚠️ Error checking Modal authentication: {e}")
60
60
  return False
61
61
 
62
+ def check_proxy_config():
63
+ """Check if Modal proxy configuration exists and is valid"""
64
+ try:
65
+ from gitarsenal_proxy_client import GitArsenalProxyClient
66
+
67
+ # Initialize client
68
+ client = GitArsenalProxyClient()
69
+
70
+ # Check if configuration exists
71
+ config = client.load_config()
72
+
73
+ if not config.get("proxy_url") or not config.get("api_key"):
74
+ print("⚠️ Modal proxy not configured. Setting up with default values...")
75
+
76
+ # Set default proxy URL to the ngrok URL
77
+ default_url = "https://e74889c63199.ngrok-free.app"
78
+
79
+ # Update configuration with default URL
80
+ config["proxy_url"] = default_url
81
+ client.save_config(config)
82
+ client.base_url = default_url
83
+
84
+ print(f"✅ Set default proxy URL: {default_url}")
85
+
86
+ # If API key is missing, prompt for configuration
87
+ if not config.get("api_key"):
88
+ print("⚠️ API key is still missing. Please configure:")
89
+ client.configure(interactive=True)
90
+
91
+ return False
92
+
93
+ # Verify connection to proxy
94
+ health = client.health_check()
95
+ if not health["success"]:
96
+ print(f"⚠️ Could not connect to Modal proxy at {config.get('proxy_url')}")
97
+ print(f" Error: {health.get('error', 'Unknown error')}")
98
+ print(" Run './gitarsenal.py proxy configure' to update configuration.")
99
+ return False
100
+
101
+ print(f"✅ Connected to Modal proxy at {config.get('proxy_url')}")
102
+ return True
103
+
104
+ except ImportError:
105
+ print("⚠️ GitArsenalProxyClient module not found")
106
+ return False
107
+ except Exception as e:
108
+ print(f"⚠️ Error checking proxy configuration: {e}")
109
+ return False
110
+
62
111
  def main():
63
112
  parser = argparse.ArgumentParser(description="GitArsenal CLI - GPU-accelerated cloud environments")
64
113
  subparsers = parser.add_subparsers(dest="command", help="Command to run")
@@ -96,8 +145,10 @@ def main():
96
145
  sandbox_parser.add_argument("--repo-name", type=str, help="Repository name override")
97
146
  sandbox_parser.add_argument("--setup-commands", type=str, nargs="+", help="Setup commands to run")
98
147
  sandbox_parser.add_argument("--volume-name", type=str, help="Name of the Modal volume for persistent storage")
148
+ sandbox_parser.add_argument("--use-api", action="store_true", help="Use API for setup commands")
149
+ sandbox_parser.add_argument("--use-proxy", action="store_true", help="Use Modal proxy service instead of direct Modal access")
99
150
 
100
- # SSH container command
151
+ # SSH command
101
152
  ssh_parser = subparsers.add_parser("ssh", help="Create a Modal SSH container")
102
153
  ssh_parser.add_argument("--gpu", type=str, default="A10G", choices=["A10G", "A100", "H100", "T4", "V100"],
103
154
  help="GPU type (default: A10G)")
@@ -106,123 +157,437 @@ def main():
106
157
  ssh_parser.add_argument("--setup-commands", type=str, nargs="+", help="Setup commands to run")
107
158
  ssh_parser.add_argument("--volume-name", type=str, help="Name of the Modal volume for persistent storage")
108
159
  ssh_parser.add_argument("--timeout", type=int, default=60, help="Container timeout in minutes (default: 60)")
160
+ ssh_parser.add_argument("--use-api", action="store_true", help="Use API for setup commands")
161
+ ssh_parser.add_argument("--use-proxy", action="store_true", help="Use Modal proxy service instead of direct Modal access")
162
+
163
+ # Proxy command
164
+ proxy_parser = subparsers.add_parser("proxy", help="Configure and use Modal proxy service")
165
+ proxy_subparsers = proxy_parser.add_subparsers(dest="proxy_command", help="Proxy command")
166
+
167
+ # Proxy configure command
168
+ proxy_configure_parser = proxy_subparsers.add_parser("configure", help="Configure Modal proxy service")
169
+
170
+ # Proxy status command
171
+ proxy_status_parser = proxy_subparsers.add_parser("status", help="Check Modal proxy service status")
172
+
173
+ # Proxy sandbox command
174
+ proxy_sandbox_parser = proxy_subparsers.add_parser("sandbox", help="Create a sandbox through Modal proxy")
175
+ proxy_sandbox_parser.add_argument("--gpu", type=str, default="A10G", choices=["A10G", "A100", "H100", "T4", "V100"],
176
+ help="GPU type (default: A10G)")
177
+ proxy_sandbox_parser.add_argument("--repo-url", type=str, help="Repository URL to clone")
178
+ proxy_sandbox_parser.add_argument("--repo-name", type=str, help="Repository name override")
179
+ proxy_sandbox_parser.add_argument("--setup-commands", type=str, nargs="+", help="Setup commands to run")
180
+ proxy_sandbox_parser.add_argument("--volume-name", type=str, help="Name of the Modal volume for persistent storage")
181
+ proxy_sandbox_parser.add_argument("--wait", action="store_true", help="Wait for sandbox to be ready")
182
+
183
+ # Proxy ssh command
184
+ proxy_ssh_parser = proxy_subparsers.add_parser("ssh", help="Create an SSH container through Modal proxy")
185
+ proxy_ssh_parser.add_argument("--gpu", type=str, default="A10G", choices=["A10G", "A100", "H100", "T4", "V100"],
186
+ help="GPU type (default: A10G)")
187
+ proxy_ssh_parser.add_argument("--repo-url", type=str, help="Repository URL to clone")
188
+ proxy_ssh_parser.add_argument("--repo-name", type=str, help="Repository name override")
189
+ proxy_ssh_parser.add_argument("--setup-commands", type=str, nargs="+", help="Setup commands to run")
190
+ proxy_ssh_parser.add_argument("--volume-name", type=str, help="Name of the Modal volume for persistent storage")
191
+ proxy_ssh_parser.add_argument("--timeout", type=int, default=60, help="Container timeout in minutes (default: 60)")
192
+ proxy_ssh_parser.add_argument("--wait", action="store_true", help="Wait for container to be ready")
109
193
 
110
194
  args = parser.parse_args()
111
195
 
112
- # If no command is provided, show help
113
196
  if not args.command:
114
197
  parser.print_help()
115
- return 1
116
-
117
- # Handle credentials commands
118
- if args.command == "credentials":
119
- cred_args = []
120
-
121
- if not args.cred_command:
122
- cred_parser.print_help()
123
- return 1
124
-
125
- cred_args.append(args.cred_command)
126
-
127
- if args.cred_command == "set" or args.cred_command == "get":
128
- cred_args.append(args.key)
129
- elif args.cred_command == "clear" and args.key != "all":
130
- cred_args.append(args.key)
131
-
132
- return run_script("manage_credentials.py", cred_args)
198
+ return 0
133
199
 
134
200
  # For sandbox and SSH commands, check Modal authentication first
135
201
  if args.command in ["sandbox", "ssh"]:
136
- # Check if Modal is authenticated
137
- if not check_modal_auth():
138
- print("\n⚠️ Please authenticate with Modal before proceeding.")
139
- return 1
140
-
141
- # Try to load credentials and set Modal token if available
202
+ # Check if using proxy service
203
+ if hasattr(args, 'use_proxy') and args.use_proxy:
204
+ # Check if proxy is configured
205
+ if not check_proxy_config():
206
+ print("\n⚠️ Modal proxy service is not configured.")
207
+ print("Please run './gitarsenal.py proxy configure' first.")
208
+ return 1
209
+ else:
210
+ # Check if Modal is authenticated
211
+ if not check_modal_auth():
212
+ print("\n⚠️ Please authenticate with Modal before proceeding.")
213
+ return 1
214
+
215
+ # Try to load credentials and set Modal token if available
216
+ try:
217
+ from credentials_manager import CredentialsManager
218
+ credentials_manager = CredentialsManager()
219
+ credentials = credentials_manager.load_credentials()
220
+
221
+ if "modal_token" in credentials:
222
+ # Set the Modal token in the environment
223
+ os.environ["MODAL_TOKEN_ID"] = credentials["modal_token"]
224
+ print("✅ Using Modal token from credentials")
225
+
226
+ # Try to authenticate with the token
227
+ try:
228
+ token_result = subprocess.run(
229
+ ["modal", "token", "set", credentials["modal_token"]],
230
+ capture_output=True, text=True
231
+ )
232
+ if token_result.returncode == 0:
233
+ print("✅ Successfully authenticated with Modal")
234
+ except Exception as e:
235
+ print(f"⚠️ Error setting Modal token: {e}")
236
+ except ImportError:
237
+ print("⚠️ Could not load credentials manager")
238
+ except Exception as e:
239
+ print(f"⚠️ Error loading credentials: {e}")
240
+
241
+ # Handle credentials commands
242
+ if args.command == "credentials":
142
243
  try:
143
244
  from credentials_manager import CredentialsManager
144
245
  credentials_manager = CredentialsManager()
145
- credentials = credentials_manager.load_credentials()
146
246
 
147
- if "modal_token" in credentials:
148
- # Set the Modal token in the environment
149
- os.environ["MODAL_TOKEN_ID"] = credentials["modal_token"]
150
- print("✅ Using Modal token from credentials")
247
+ if args.cred_command == "setup":
248
+ # Set up all credentials
249
+ print("🔧 Setting up all credentials")
250
+
251
+ # Modal token
252
+ modal_token = credentials_manager.get_modal_token()
253
+ if modal_token:
254
+ print("✅ Modal token set")
255
+ else:
256
+ print("⚠️ Modal token not set")
257
+
258
+ # OpenAI API key
259
+ openai_api_key = credentials_manager.get_openai_api_key()
260
+ if openai_api_key:
261
+ print("✅ OpenAI API key set")
262
+ else:
263
+ print("⚠️ OpenAI API key not set")
264
+
265
+ # Hugging Face 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
+
272
+ # Weights & Biases API key
273
+ wandb_api_key = credentials_manager.get_wandb_api_key()
274
+ if wandb_api_key:
275
+ print("✅ Weights & Biases API key set")
276
+ else:
277
+ print("⚠️ Weights & Biases API key not set")
278
+
279
+ elif args.cred_command == "set":
280
+ # Set a specific credential
281
+ if args.key == "modal_token":
282
+ modal_token = credentials_manager.get_modal_token()
283
+ if modal_token:
284
+ print("✅ Modal token set")
285
+ else:
286
+ print("⚠️ Modal token not set")
287
+ elif args.key == "openai_api_key":
288
+ openai_api_key = credentials_manager.get_openai_api_key()
289
+ if openai_api_key:
290
+ print("✅ OpenAI API key set")
291
+ else:
292
+ print("⚠️ OpenAI API key not set")
293
+ elif args.key == "huggingface_token":
294
+ huggingface_token = credentials_manager.get_huggingface_token()
295
+ if huggingface_token:
296
+ print("✅ Hugging Face token set")
297
+ else:
298
+ print("⚠️ Hugging Face token not set")
299
+ elif args.key == "wandb_api_key":
300
+ wandb_api_key = credentials_manager.get_wandb_api_key()
301
+ if wandb_api_key:
302
+ print("✅ Weights & Biases API key set")
303
+ else:
304
+ print("⚠️ Weights & Biases API key not set")
305
+
306
+ elif args.cred_command == "get":
307
+ # Get a specific credential
308
+ credentials = credentials_manager.load_credentials()
309
+ if args.key in credentials:
310
+ # Mask the credential for security
311
+ value = credentials[args.key]
312
+ masked_value = value[:4] + "*" * (len(value) - 8) + value[-4:] if len(value) > 8 else "****"
313
+ print(f"{args.key}: {masked_value}")
314
+ else:
315
+ print(f"⚠️ {args.key} not found")
316
+
317
+ elif args.cred_command == "clear":
318
+ # Clear credentials
319
+ if args.key == "all":
320
+ credentials_manager.clear_all_credentials()
321
+ print("✅ All credentials cleared")
322
+ else:
323
+ if credentials_manager.clear_credential(args.key):
324
+ print(f"✅ {args.key} cleared")
325
+ else:
326
+ print(f"⚠️ {args.key} not found")
327
+
328
+ elif args.cred_command == "list":
329
+ # List all credentials
330
+ credentials = credentials_manager.load_credentials()
331
+ if credentials:
332
+ print("📋 Saved credentials:")
333
+ for key in credentials:
334
+ print(f" - {key}")
335
+ else:
336
+ print("⚠️ No credentials found")
337
+
338
+ else:
339
+ print("⚠️ Unknown credentials command")
340
+ return 1
151
341
 
152
- # Try to authenticate with the token
153
- try:
154
- token_result = subprocess.run(
155
- ["modal", "token", "set", credentials["modal_token"]],
156
- capture_output=True, text=True
157
- )
158
- if token_result.returncode == 0:
159
- print("✅ Successfully authenticated with Modal")
160
- except Exception as e:
161
- print(f"⚠️ Error setting Modal token: {e}")
162
342
  except ImportError:
163
- print("⚠️ Could not load credentials manager")
343
+ print(" Could not import credentials_manager module")
344
+ return 1
164
345
  except Exception as e:
165
- print(f"⚠️ Error loading credentials: {e}")
346
+ print(f" Error: {e}")
347
+ return 1
166
348
 
167
349
  # Handle sandbox command
168
- if args.command == "sandbox":
169
- sandbox_args = []
350
+ elif args.command == "sandbox":
351
+ try:
352
+ # Check if using proxy service
353
+ if args.use_proxy:
354
+ # Import proxy client
355
+ try:
356
+ from gitarsenal_proxy_client import GitArsenalProxyClient
357
+ client = GitArsenalProxyClient()
358
+
359
+ # Create sandbox through proxy
360
+ result = client.create_sandbox(
361
+ gpu_type=args.gpu,
362
+ repo_url=args.repo_url,
363
+ repo_name=args.repo_name,
364
+ setup_commands=args.setup_commands,
365
+ volume_name=args.volume_name,
366
+ wait=True # Always wait for sandbox to be ready
367
+ )
368
+
369
+ if not result:
370
+ print("❌ Failed to create sandbox through proxy service")
371
+ return 1
372
+
373
+ print("✅ Sandbox created successfully through proxy service")
374
+
375
+ except ImportError:
376
+ print("❌ Could not import gitarsenal_proxy_client module")
377
+ print("Please make sure gitarsenal_proxy_client.py is in the same directory")
378
+ return 1
379
+ else:
380
+ # Import sandbox creation function
381
+ from test_modalSandboxScript import create_modal_sandbox
382
+
383
+ # Create sandbox directly
384
+ result = create_modal_sandbox(
385
+ args.gpu,
386
+ repo_url=args.repo_url,
387
+ repo_name=args.repo_name,
388
+ setup_commands=args.setup_commands,
389
+ volume_name=args.volume_name
390
+ )
391
+
392
+ if not result:
393
+ print("❌ Failed to create sandbox")
394
+ return 1
395
+
396
+ print("✅ Sandbox created successfully")
170
397
 
171
- if args.gpu:
172
- sandbox_args.extend(["--gpu", args.gpu])
173
- if args.repo_url:
174
- sandbox_args.extend(["--repo-url", args.repo_url])
175
- if args.repo_name:
176
- sandbox_args.extend(["--repo-name", args.repo_name])
177
- if args.setup_commands:
178
- sandbox_args.extend(["--setup-commands"] + args.setup_commands)
179
- if args.volume_name:
180
- sandbox_args.extend(["--volume-name", args.volume_name])
181
-
182
- return run_script("test_modalSandboxScript.py", sandbox_args)
183
-
184
- # Handle SSH container command
398
+ except ImportError as e:
399
+ print(f"❌ Could not import required module: {e}")
400
+ return 1
401
+ except Exception as e:
402
+ print(f"❌ Error: {e}")
403
+ return 1
404
+
405
+ # Handle SSH command
185
406
  elif args.command == "ssh":
186
- ssh_args = []
407
+ try:
408
+ # Check if using proxy service
409
+ if args.use_proxy:
410
+ # Import proxy client
411
+ try:
412
+ from gitarsenal_proxy_client import GitArsenalProxyClient
413
+ client = GitArsenalProxyClient()
414
+
415
+ # Ensure proxy is configured
416
+ config = client.load_config()
417
+ if not config.get("proxy_url") or not config.get("api_key"):
418
+ print("\n⚠️ Modal proxy service is not fully configured.")
419
+ print("Running configuration wizard...")
420
+ client.configure(interactive=True)
421
+ # Reload config after configuration
422
+ config = client.load_config()
423
+ if not config.get("proxy_url") or not config.get("api_key"):
424
+ print("❌ Proxy configuration failed or was cancelled.")
425
+ return 1
426
+ print("✅ Proxy configuration completed successfully.")
427
+
428
+ # Create SSH container through proxy
429
+ print(f"🚀 Creating SSH container through proxy service ({config.get('proxy_url')})")
430
+ result = client.create_ssh_container(
431
+ gpu_type=args.gpu,
432
+ repo_url=args.repo_url,
433
+ repo_name=args.repo_name,
434
+ setup_commands=args.setup_commands,
435
+ volume_name=args.volume_name,
436
+ timeout=args.timeout,
437
+ wait=True # Always wait for container to be ready
438
+ )
439
+
440
+ if not result:
441
+ print("❌ Failed to create SSH container through proxy service")
442
+ return 1
443
+
444
+ print("✅ SSH container created successfully through proxy service")
445
+
446
+ except ImportError:
447
+ print("❌ Could not import gitarsenal_proxy_client module")
448
+ print("Please make sure gitarsenal_proxy_client.py is in the same directory")
449
+ return 1
450
+ else:
451
+ # Import SSH container creation function
452
+ from test_modalSandboxScript import create_modal_ssh_container
453
+
454
+ # Create SSH container directly
455
+ result = create_modal_ssh_container(
456
+ args.gpu,
457
+ repo_url=args.repo_url,
458
+ repo_name=args.repo_name,
459
+ setup_commands=args.setup_commands,
460
+ volume_name=args.volume_name,
461
+ timeout_minutes=args.timeout
462
+ )
463
+
464
+ if not result:
465
+ print("❌ Failed to create SSH container")
466
+ return 1
467
+
468
+ print("✅ SSH container created successfully")
187
469
 
188
- if args.gpu:
189
- ssh_args.extend(["--gpu", args.gpu])
190
- if args.repo_url:
191
- ssh_args.extend(["--repo-url", args.repo_url])
192
- if args.repo_name:
193
- ssh_args.extend(["--repo-name", args.repo_name])
194
- if args.setup_commands:
195
- ssh_args.extend(["--setup-commands"] + args.setup_commands)
196
- if args.volume_name:
197
- ssh_args.extend(["--volume-name", args.volume_name])
198
- if args.timeout:
199
- ssh_args.extend(["--timeout", str(args.timeout)])
200
-
201
- # Use test_modalSandboxScript.py with SSH mode
202
- ssh_args.extend(["--ssh"])
203
- return run_script("test_modalSandboxScript.py", ssh_args)
204
-
205
- return 0
206
-
207
- def run_script(script_name, args):
208
- """Run a Python script with the given arguments"""
209
- # Get the directory of the current script
210
- script_dir = os.path.dirname(os.path.abspath(__file__))
211
- script_path = os.path.join(script_dir, script_name)
470
+ except ImportError as e:
471
+ print(f"❌ Could not import required module: {e}")
472
+ return 1
473
+ except Exception as e:
474
+ print(f"❌ Error: {e}")
475
+ return 1
212
476
 
213
- # Build the command
214
- cmd = [sys.executable, script_path] + args
477
+ # Handle proxy commands
478
+ elif args.command == "proxy":
479
+ if not args.proxy_command:
480
+ proxy_parser.print_help()
481
+ return 0
482
+
483
+ try:
484
+ # Import proxy client
485
+ from gitarsenal_proxy_client import GitArsenalProxyClient
486
+ client = GitArsenalProxyClient()
487
+
488
+ if args.proxy_command == "configure":
489
+ # Configure proxy service
490
+ client.configure(interactive=True)
491
+
492
+ elif args.proxy_command == "status":
493
+ # Check proxy service status
494
+ response = client.health_check()
495
+ if response["success"]:
496
+ print(f"✅ Proxy service is running: {response['data']['message']}")
497
+ else:
498
+ print(f"❌ Proxy service health check failed: {response['error']}")
499
+ return 1
500
+
501
+ elif args.proxy_command == "sandbox":
502
+ # Create sandbox through proxy
503
+ result = client.create_sandbox(
504
+ gpu_type=args.gpu,
505
+ repo_url=args.repo_url,
506
+ repo_name=args.repo_name,
507
+ setup_commands=args.setup_commands,
508
+ volume_name=args.volume_name,
509
+ wait=args.wait
510
+ )
511
+
512
+ if not result:
513
+ print("❌ Failed to create sandbox through proxy service")
514
+ return 1
515
+
516
+ print("✅ Sandbox created successfully through proxy service")
517
+
518
+ elif args.proxy_command == "ssh":
519
+ # Create SSH container through proxy
520
+ print("🚀 Creating SSH container through proxy...")
521
+
522
+ # Verify proxy configuration is complete
523
+ config = client.load_config()
524
+ if not config.get("proxy_url") or not config.get("api_key"):
525
+ print("⚠️ Proxy configuration incomplete. Running configuration wizard...")
526
+ client.configure(interactive=True)
527
+ # Reload config after configuration
528
+ config = client.load_config()
529
+ if not config.get("proxy_url") or not config.get("api_key"):
530
+ print("❌ Proxy configuration failed or was cancelled.")
531
+ return 1
532
+ print("✅ Proxy configuration completed successfully.")
533
+
534
+ # Check proxy service health
535
+ health = client.health_check()
536
+ if not health["success"]:
537
+ print(f"❌ Could not connect to proxy service at {config.get('proxy_url')}")
538
+ print(f" Error: {health.get('error', 'Unknown error')}")
539
+ print(" Please check if the proxy service is running.")
540
+ return 1
541
+
542
+ print(f"✅ Connected to proxy service at {config.get('proxy_url')}")
543
+
544
+ # Create SSH container
545
+ result = client.create_ssh_container(
546
+ gpu_type=args.gpu,
547
+ repo_url=args.repo_url,
548
+ repo_name=args.repo_name,
549
+ setup_commands=args.setup_commands,
550
+ volume_name=args.volume_name,
551
+ timeout=args.timeout,
552
+ wait=args.wait
553
+ )
554
+
555
+ if not result:
556
+ print("❌ Failed to create SSH container through proxy service")
557
+ return 1
558
+
559
+ # Print connection information if available
560
+ if isinstance(result, dict):
561
+ if "container_id" in result:
562
+ print(f"📋 Container ID: {result['container_id']}")
563
+ if "ssh_password" in result:
564
+ print(f"🔐 SSH Password: {result['ssh_password']}")
565
+
566
+ print("✅ SSH container creation process initiated through proxy service")
567
+
568
+ if not args.wait:
569
+ print("\n⚠️ Container creation is asynchronous. Use --wait flag to wait for it to be ready.")
570
+ print(" You can check the status later using the container ID above.")
571
+
572
+ else:
573
+ print(f"❌ Unknown proxy command: {args.proxy_command}")
574
+ proxy_parser.print_help()
575
+ return 1
576
+
577
+ except ImportError:
578
+ print("❌ Could not import gitarsenal_proxy_client module")
579
+ print("Please make sure gitarsenal_proxy_client.py is in the same directory")
580
+ return 1
581
+ except Exception as e:
582
+ print(f"❌ Error: {e}")
583
+ return 1
215
584
 
216
- try:
217
- # Run the command
218
- result = subprocess.run(cmd)
219
- return result.returncode
220
- except KeyboardInterrupt:
221
- print("\n⚠️ Command interrupted by user")
222
- return 130
223
- except Exception as e:
224
- print(f"❌ Error running {script_name}: {e}")
585
+ else:
586
+ print(f"❌ Unknown command: {args.command}")
587
+ parser.print_help()
225
588
  return 1
589
+
590
+ return 0
226
591
 
227
592
  if __name__ == "__main__":
228
593
  sys.exit(main())