gitarsenal-cli 1.3.3 → 1.3.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.
package/bin/gitarsenal.js CHANGED
@@ -38,7 +38,6 @@ const containerCmd = program
38
38
  .option('-s, --setup-commands <commands...>', 'Setup commands to run in the container')
39
39
  .option('-y, --yes', 'Skip confirmation prompts')
40
40
  .option('-m, --manual', 'Disable automatic setup command detection')
41
- .option('-i, --interactive', 'Run in interactive mode with prompts')
42
41
  .option('--show-examples', 'Show usage examples')
43
42
  .action(async (options) => {
44
43
  await runContainerCommand(options);
@@ -88,11 +87,10 @@ program
88
87
  .option('-v, --volume <n>', 'Name of persistent volume')
89
88
  .option('-y, --yes', 'Skip confirmation prompts')
90
89
  .option('-m, --manual', 'Disable automatic setup command detection')
91
- .option('-i, --interactive', 'Run in interactive mode with prompts')
92
90
  .option('--show-examples', 'Show usage examples')
93
91
  .action(async (options) => {
94
92
  // If options are provided directly, run the container command
95
- if (options.repo || options.interactive || options.showExamples || process.argv.length <= 3) {
93
+ if (options.repo || options.showExamples || process.argv.length <= 3) {
96
94
  await runContainerCommand(options);
97
95
  }
98
96
  });
@@ -119,14 +117,6 @@ async function runContainerCommand(options) {
119
117
  }
120
118
  spinner.succeed('Dependencies checked');
121
119
 
122
- // If interactive mode is enabled, let the Python script handle the prompts
123
- if (options.interactive) {
124
- await runContainer({
125
- interactive: true
126
- });
127
- return;
128
- }
129
-
130
120
  // If repo URL not provided, prompt for it
131
121
  let repoUrl = options.repoUrl || options.repo;
132
122
  let gpuType = options.gpu;
package/lib/sandbox.js CHANGED
@@ -33,7 +33,6 @@ function getPythonScriptPath() {
33
33
  * @param {string} options.volumeName - Volume name
34
34
  * @param {Array<string>} options.setupCommands - Setup commands
35
35
  * @param {boolean} options.useApi - Whether to use the API to fetch setup commands
36
- * @param {boolean} options.interactive - Whether to run in interactive mode
37
36
  * @param {boolean} options.showExamples - Whether to show usage examples
38
37
  * @returns {Promise<void>}
39
38
  */
@@ -44,7 +43,6 @@ async function runContainer(options) {
44
43
  volumeName,
45
44
  setupCommands = [],
46
45
  useApi = true,
47
- interactive = false,
48
46
  showExamples = false
49
47
  } = options;
50
48
 
@@ -88,14 +86,9 @@ async function runContainer(options) {
88
86
  });
89
87
  }
90
88
 
91
- // Add interactive flag if specified
92
- if (interactive) {
93
- args.push('--interactive');
94
- } else {
95
- // Only add these arguments in non-interactive mode
96
- if (gpuType) args.push('--gpu', gpuType);
97
- if (repoUrl) args.push('--repo-url', repoUrl);
98
- }
89
+ // Add normal arguments
90
+ if (gpuType) args.push('--gpu', gpuType);
91
+ if (repoUrl) args.push('--repo-url', repoUrl);
99
92
 
100
93
  if (volumeName) {
101
94
  args.push('--volume-name', volumeName);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitarsenal-cli",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -3331,34 +3331,28 @@ def cleanup_modal_token():
3331
3331
  print(f"❌ Error during Modal token cleanup: {e}")
3332
3332
 
3333
3333
  def show_usage_examples():
3334
- """Display usage examples for the command-line interface."""
3335
- print("\033[92mUsage Examples\033[0m")
3336
- print("")
3337
- print("\033[92mBasic Container Creation\033[0m")
3338
- print("\033[90m┌────────────────────────────────────────────────────────────────────────┐\033[0m")
3339
- print("\033[90m│\033[0m \033[92mgitarsenal --gpu A10G --repo-url https://github.com/username/repo.git\033[0m \033[90m│\033[0m")
3340
- print("\033[90m└────────────────────────────────────────────────────────────────────────┘\033[0m")
3341
- print("")
3342
- print("\033[92mWith Setup Commands\033[0m")
3343
- print("\033[90m┌────────────────────────────────────────────────────────────────────────────────────────────────────┐\033[0m")
3344
- print("\033[90m│\033[0m \033[92mgitarsenal --gpu A100 --repo-url https://github.com/username/repo.git \\\033[0m \033[90m│\033[0m")
3345
- print("\033[90m│\033[0m \033[92m --setup-commands \"pip install -r requirements.txt\" \"python setup.py install\"\033[0m \033[90m│\033[0m")
3346
- print("\033[90m└────────────────────────────────────────────────────────────────────────────────────────────────────┘\033[0m")
3347
- print("")
3348
- print("\033[92mWith Persistent Storage\033[0m")
3349
- print("\033[90m┌────────────────────────────────────────────────────────────────────────────────────┐\033[0m")
3350
- print("\033[90m│\033[0m \033[92mgitarsenal --gpu A10G --repo-url https://github.com/username/repo.git \\\033[0m \033[90m│\033[0m")
3351
- print("\033[90m│\033[0m \033[92m --volume-name my-persistent-volume\033[0m \033[90m│\033[0m")
3352
- print("\033[90m└────────────────────────────────────────────────────────────────────────────────────┘\033[0m")
3353
- print("")
3354
- print("\033[92mInteractive Mode\033[0m")
3355
- print("\033[90m┌────────────────────────────┐\033[0m")
3356
- print("\033[90m│\033[0m \033[92mgitarsenal --interactive\033[0m \033[90m│\033[0m")
3357
- print("\033[90m└────────────────────────────┘\033[0m")
3358
- print("")
3359
- print("\033[92mAvailable GPU Options:\033[0m")
3334
+ """Display usage examples for the script."""
3335
+ print("Usage Examples\n")
3336
+
3337
+ print("Basic Container Creation")
3338
+ print("┌────────────────────────────────────────────────────────────────────────┐")
3339
+ print(" gitarsenal --gpu A10G --repo-url https://github.com/username/repo.git ")
3340
+ print("└────────────────────────────────────────────────────────────────────────┘\n")
3341
+
3342
+ print("With Setup Commands")
3343
+ print("┌────────────────────────────────────────────────────────────────────────────────────────────────────┐")
3344
+ print(" gitarsenal --gpu A100 --repo-url https://github.com/username/repo.git \\ ")
3345
+ print("--setup-commands \"pip install -r requirements.txt\" \"python setup.py install\" ")
3346
+ print("└────────────────────────────────────────────────────────────────────────────────────────────────────┘\n")
3347
+
3348
+ print("With Persistent Storage")
3349
+ print("┌────────────────────────────────────────────────────────────────────────────────────┐")
3350
+ print(" gitarsenal --gpu A10G --repo-url https://github.com/username/repo.git \\ ")
3351
+ print("--volume-name my-persistent-volume ")
3352
+ print("└────────────────────────────────────────────────────────────────────────────────────┘\n")
3353
+
3354
+ print("Available GPU Options:")
3360
3355
  print(" T4, L4, A10G, A100-40GB, A100-80GB, L40S, H100, H200, B200")
3361
- print("")
3362
3356
 
3363
3357
  if __name__ == "__main__":
3364
3358
  # Parse command line arguments when script is run directly
@@ -3378,7 +3372,6 @@ if __name__ == "__main__":
3378
3372
  parser.add_argument('--timeout', type=int, default=60, help='Container timeout in minutes (default: 60)')
3379
3373
  parser.add_argument('--ssh-password', type=str, help='SSH password (random if not provided)')
3380
3374
  parser.add_argument('--use-api', action='store_true', help='Fetch setup commands from API')
3381
- parser.add_argument('--interactive', action='store_true', help='Run in interactive mode with prompts')
3382
3375
  parser.add_argument('--show-examples', action='store_true', help='Show usage examples')
3383
3376
 
3384
3377
  args = parser.parse_args()
@@ -3387,159 +3380,124 @@ if __name__ == "__main__":
3387
3380
  if len(sys.argv) == 1 or args.show_examples:
3388
3381
  show_usage_examples()
3389
3382
  sys.exit(0)
3383
+
3384
+ try:
3385
+ # Get setup commands from file if specified
3386
+ setup_commands = args.setup_commands or []
3390
3387
 
3391
- # Get setup commands from file if specified
3392
- setup_commands = args.setup_commands or []
3393
-
3394
- # If interactive mode is enabled, prompt for options
3395
- if args.interactive:
3396
- # If repo URL wasn't provided via command line, ask for it
3397
- if not args.repo_url:
3398
- args.repo_url = input("✔ Dependencies checked\n? Enter GitHub repository URL: ").strip()
3399
- if not args.repo_url:
3400
- print("❌ No repository URL provided. Exiting.")
3401
- sys.exit(1)
3402
-
3403
- # Ask about persistent volume
3404
- use_volume = input("? Use persistent volume for faster installs? (Yes/No) [Yes]: ").strip().lower()
3405
- if not use_volume or use_volume.startswith('y'):
3406
- if not args.volume_name:
3407
- args.volume_name = input("? Enter volume name [gitarsenal-volume]: ").strip()
3408
- if not args.volume_name:
3409
- args.volume_name = "gitarsenal-volume"
3410
- else:
3411
- args.volume_name = None
3412
-
3413
- # Ask about auto-detecting setup commands
3414
- use_api = input("? Automatically detect setup commands for this repository? (Yes/No) [Yes]: ").strip().lower()
3415
- if not use_api or use_api.startswith('y'):
3416
- args.use_api = True
3417
- else:
3418
- args.use_api = False
3388
+ # If --use-api flag is set and repo_url is provided, fetch setup commands from API
3419
3389
 
3420
- # GPU selection
3421
- gpu_options = ['T4', 'L4', 'A10G', 'A100-40GB', 'A100-80GB', 'L40S', 'H100', 'H200', 'B200']
3422
- print("\n? Select GPU type:")
3423
- for i, gpu in enumerate(gpu_options, 1):
3424
- print(f" {i}. {gpu}")
3390
+ # If --use-api flag is set and repo_url is provided, fetch setup commands from API
3391
+ if args.use_api and args.repo_url:
3392
+ print("🔄 Using API to fetch setup commands")
3393
+ api_commands = fetch_setup_commands_from_api(args.repo_url)
3394
+ if api_commands:
3395
+ setup_commands = api_commands
3396
+ print(f"📋 Using {len(setup_commands)} commands from API")
3397
+ else:
3398
+ print("⚠️ Failed to get commands from API, no fallback commands will be used")
3399
+ # Do not fall back to basic setup commands
3400
+ setup_commands = []
3425
3401
 
3426
- gpu_choice = input(f"Enter choice (1-{len(gpu_options)}) [default: 3 for A10G]: ").strip()
3427
- if not gpu_choice:
3428
- args.gpu = 'A10G' # Default
3429
- else:
3402
+ # Parse setup commands from JSON if provided
3403
+ if args.setup_commands_json:
3430
3404
  try:
3431
- gpu_index = int(gpu_choice) - 1
3432
- if 0 <= gpu_index < len(gpu_options):
3433
- args.gpu = gpu_options[gpu_index]
3405
+ json_commands = json.loads(args.setup_commands_json)
3406
+ if isinstance(json_commands, list):
3407
+ setup_commands = json_commands
3408
+ print(f"📋 Parsed {len(setup_commands)} commands from JSON:")
3409
+ for i, cmd in enumerate(setup_commands, 1):
3410
+ print(f" {i}. {cmd}")
3434
3411
  else:
3435
- print("⚠️ Invalid choice. Using default: A10G")
3436
- args.gpu = 'A10G'
3437
- except ValueError:
3438
- print("⚠️ Invalid input. Using default: A10G")
3439
- args.gpu = 'A10G'
3440
-
3441
- # Show configuration summary
3442
- print("\n📋 Container Configuration:")
3443
- print(f"Repository URL: {args.repo_url}")
3444
- print(f"GPU Type: {args.gpu}")
3445
- print(f"Volume: {args.volume_name if args.volume_name else 'None'}")
3446
- print(f"Setup Commands: {'Auto-detect from repository' if args.use_api else 'None'}")
3447
-
3448
- # Confirm settings
3449
- confirm = input("? Proceed with these settings? (Yes/No) [Yes]: ").strip().lower()
3450
- if confirm and not confirm.startswith('y'):
3451
- print("❌ Setup cancelled by user.")
3452
- sys.exit(0)
3453
-
3454
- # If --use-api flag is set and repo_url is provided, fetch setup commands from API
3455
- if args.use_api and args.repo_url:
3456
- print("🔄 Using API to fetch setup commands")
3457
- api_commands = fetch_setup_commands_from_api(args.repo_url)
3458
- if api_commands:
3459
- setup_commands = api_commands
3460
- print(f"📋 Using {len(setup_commands)} commands from API")
3461
- else:
3462
- print("⚠️ Failed to get commands from API, no fallback commands will be used")
3463
- # Do not fall back to basic setup commands
3464
- setup_commands = []
3465
-
3466
- # Parse setup commands from JSON if provided
3467
- if args.setup_commands_json:
3468
- try:
3469
- json_commands = json.loads(args.setup_commands_json)
3470
- if isinstance(json_commands, list):
3471
- setup_commands = json_commands
3472
- print(f"📋 Parsed {len(setup_commands)} commands from JSON:")
3473
- for i, cmd in enumerate(setup_commands, 1):
3474
- print(f" {i}. {cmd}")
3475
- else:
3476
- print(f"⚠️ Invalid JSON format for setup commands: not a list")
3477
- except json.JSONDecodeError as e:
3478
- print(f"⚠️ Error parsing JSON setup commands: {e}")
3479
- print(f"Received JSON string: {args.setup_commands_json}")
3480
-
3481
- # Print received setup commands for debugging
3482
- if setup_commands:
3483
- print(f"📋 Using {len(setup_commands)} setup commands:")
3484
- for i, cmd in enumerate(setup_commands, 1):
3485
- print(f" {i}. {cmd}")
3486
-
3487
- # Load commands from file if specified
3488
- if args.commands_file and os.path.exists(args.commands_file):
3489
- try:
3490
- with open(args.commands_file, 'r') as f:
3491
- # Check if the file contains JSON or line-by-line commands
3492
- content = f.read().strip()
3412
+ print(f"⚠️ Invalid JSON format for setup commands: not a list")
3413
+ except json.JSONDecodeError as e:
3414
+ print(f"⚠️ Error parsing JSON setup commands: {e}")
3415
+ print(f"Received JSON string: {args.setup_commands_json}")
3416
+
3417
+ # Print received setup commands for debugging
3418
+ if setup_commands:
3419
+ print(f"📋 Using {len(setup_commands)} setup commands:")
3420
+ for i, cmd in enumerate(setup_commands, 1):
3421
+ print(f" {i}. {cmd}")
3493
3422
 
3494
- if content.startswith('[') and content.endswith(']'):
3495
- # JSON format
3496
- try:
3497
- json_commands = json.loads(content)
3498
- if isinstance(json_commands, list):
3499
- setup_commands.extend(json_commands)
3500
- print(f"📋 Loaded {len(json_commands)} commands from JSON file {args.commands_file}")
3501
- else:
3502
- print(f"⚠️ Invalid JSON format in commands file: not a list")
3503
- except json.JSONDecodeError as json_err:
3504
- print(f"⚠️ Error parsing JSON commands file: {json_err}")
3505
- # Fall back to line-by-line parsing
3423
+ # Load commands from file if specified
3424
+ if args.commands_file and os.path.exists(args.commands_file):
3425
+ try:
3426
+ with open(args.commands_file, 'r') as f:
3427
+ # Check if the file contains JSON or line-by-line commands
3428
+ content = f.read().strip()
3429
+
3430
+ if content.startswith('[') and content.endswith(']'):
3431
+ # JSON format
3432
+ try:
3433
+ json_commands = json.loads(content)
3434
+ if isinstance(json_commands, list):
3435
+ setup_commands.extend(json_commands)
3436
+ print(f"📋 Loaded {len(json_commands)} commands from JSON file {args.commands_file}")
3437
+ else:
3438
+ print(f"⚠️ Invalid JSON format in commands file: not a list")
3439
+ except json.JSONDecodeError as json_err:
3440
+ print(f"⚠️ Error parsing JSON commands file: {json_err}")
3441
+ # Fall back to line-by-line parsing
3442
+ file_commands = [line.strip() for line in content.split('\n') if line.strip()]
3443
+ setup_commands.extend(file_commands)
3444
+ print(f"📋 Loaded {len(file_commands)} commands from file (line-by-line fallback)")
3445
+ else:
3446
+ # Line-by-line format
3506
3447
  file_commands = [line.strip() for line in content.split('\n') if line.strip()]
3507
3448
  setup_commands.extend(file_commands)
3508
- print(f"📋 Loaded {len(file_commands)} commands from file (line-by-line fallback)")
3509
- else:
3510
- # Line-by-line format
3511
- file_commands = [line.strip() for line in content.split('\n') if line.strip()]
3512
- setup_commands.extend(file_commands)
3513
- print(f"📋 Loaded {len(file_commands)} commands from file (line-by-line format)")
3514
-
3515
- except Exception as e:
3516
- print(f"⚠️ Error reading commands file: {e}")
3517
-
3518
- try:
3519
- result = create_modal_ssh_container(
3520
- args.gpu,
3521
- args.repo_url,
3522
- args.repo_name,
3523
- setup_commands,
3524
- getattr(args, 'volume_name', None),
3525
- args.timeout,
3526
- args.ssh_password
3527
- )
3449
+ print(f"📋 Loaded {len(file_commands)} commands from file (line-by-line format)")
3450
+ except Exception as e:
3451
+ print(f"⚠️ Error loading commands from file: {e}")
3528
3452
 
3529
- print("\n⏳ Keeping the SSH container alive. Press Ctrl+C to exit (container will continue running)...")
3530
- try:
3531
- while True:
3532
- time.sleep(90)
3533
- print(".", end="", flush=True)
3534
- except KeyboardInterrupt:
3535
- print("\n👋 Script exited. The SSH container will continue running.")
3536
- sys.exit(0)
3537
-
3453
+ # Load commands from setup script if specified
3454
+ if args.setup_script and os.path.exists(args.setup_script):
3455
+ try:
3456
+ with open(args.setup_script, 'r') as f:
3457
+ script_content = f.read().strip()
3458
+ # Convert script to individual commands
3459
+ script_commands = [line.strip() for line in script_content.split('\n')
3460
+ if line.strip() and not line.strip().startswith('#')]
3461
+ setup_commands.extend(script_commands)
3462
+ print(f"📋 Loaded {len(script_commands)} commands from script {args.setup_script}")
3463
+ except Exception as e:
3464
+ print(f"⚠️ Error loading commands from script: {e}")
3465
+
3466
+ # Create the container with the specified options
3467
+ if args.ssh_password:
3468
+ print(f"🔑 Using provided SSH password")
3469
+ ssh_password = args.ssh_password
3470
+ else:
3471
+ ssh_password = generate_random_password()
3472
+ print(f"🔑 Generated random SSH password: {ssh_password}")
3473
+
3474
+ # Extract repository name from URL if not provided
3475
+ repo_name = args.repo_name
3476
+ if not repo_name and args.repo_url:
3477
+ # Try to extract repo name from URL
3478
+ url_parts = args.repo_url.rstrip('/').split('/')
3479
+ if url_parts:
3480
+ repo_name = url_parts[-1]
3481
+ if repo_name.endswith('.git'):
3482
+ repo_name = repo_name[:-4]
3483
+
3484
+ # Create the container
3485
+ create_modal_ssh_container(
3486
+ gpu_type=args.gpu,
3487
+ repo_url=args.repo_url,
3488
+ repo_name=repo_name,
3489
+ setup_commands=setup_commands,
3490
+ volume_name=args.volume_name,
3491
+ timeout_minutes=args.timeout,
3492
+ ssh_password=ssh_password
3493
+ )
3538
3494
  except KeyboardInterrupt:
3539
- # Handle Ctrl+C during container creation
3540
- print("\n👋 Script interrupted during container creation.")
3541
- print("📝 You may need to check if a container was created with: modal container list")
3542
- sys.exit(0)
3495
+ print("\n\n🛑 Execution interrupted")
3496
+ print("🧹 Cleaning up resources...")
3497
+ cleanup_modal_token()
3498
+ sys.exit(1)
3543
3499
  except Exception as e:
3544
- print(f"❌ Error: {e}")
3500
+ print(f"\n❌ Error: {e}")
3501
+ print("🧹 Cleaning up resources...")
3502
+ cleanup_modal_token()
3545
3503
  sys.exit(1)