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 +1 -11
- package/lib/sandbox.js +3 -10
- package/package.json +1 -1
- package/python/test_modalSandboxScript.py +130 -172
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.
|
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
|
92
|
-
if (
|
93
|
-
|
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
@@ -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
|
3335
|
-
print("
|
3336
|
-
|
3337
|
-
print("
|
3338
|
-
print("
|
3339
|
-
print("
|
3340
|
-
print("
|
3341
|
-
|
3342
|
-
print("
|
3343
|
-
print("
|
3344
|
-
print("
|
3345
|
-
print("
|
3346
|
-
print("
|
3347
|
-
|
3348
|
-
print("
|
3349
|
-
print("
|
3350
|
-
print("
|
3351
|
-
print("
|
3352
|
-
print("
|
3353
|
-
|
3354
|
-
print("
|
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
|
-
|
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
|
-
#
|
3421
|
-
|
3422
|
-
|
3423
|
-
|
3424
|
-
|
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
|
-
|
3427
|
-
if
|
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
|
-
|
3432
|
-
if
|
3433
|
-
|
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
|
3436
|
-
|
3437
|
-
|
3438
|
-
print("
|
3439
|
-
|
3440
|
-
|
3441
|
-
|
3442
|
-
|
3443
|
-
|
3444
|
-
|
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
|
-
|
3495
|
-
|
3496
|
-
|
3497
|
-
|
3498
|
-
|
3499
|
-
|
3500
|
-
|
3501
|
-
|
3502
|
-
|
3503
|
-
|
3504
|
-
|
3505
|
-
|
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
|
3509
|
-
|
3510
|
-
|
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
|
-
|
3530
|
-
|
3531
|
-
|
3532
|
-
|
3533
|
-
|
3534
|
-
|
3535
|
-
|
3536
|
-
|
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
|
-
|
3540
|
-
print("
|
3541
|
-
|
3542
|
-
sys.exit(
|
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)
|