gitarsenal-cli 1.1.1 → 1.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitarsenal-cli",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
package/python/README.md CHANGED
@@ -13,50 +13,51 @@ cd gitarsenal-cli/python
13
13
  pip install -r requirements.txt
14
14
  ```
15
15
 
16
- ## Credentials Setup
16
+ ## First-Time Setup
17
17
 
18
- GitArsenal CLI requires several API keys and tokens to function properly:
18
+ Before using GitArsenal CLI, you need to set up your credentials and authenticate with Modal:
19
19
 
20
- - **OpenAI API Key**: Used for debugging failed commands
21
- - **Modal Token**: Used to create cloud environments
22
- - **Hugging Face Token**: Used for accessing Hugging Face models
23
- - **Weights & Biases API Key**: Used for experiment tracking
24
-
25
- You can set up your credentials using the credentials manager:
20
+ ### 1. Install Modal
26
21
 
27
22
  ```bash
28
- python manage_credentials.py setup
23
+ pip install modal
29
24
  ```
30
25
 
31
- This will guide you through setting up all required credentials, which will be stored securely in your home directory.
26
+ ### 2. Create a Modal Account and Get a Token
32
27
 
33
- ### Managing Individual Credentials
28
+ If you don't have a Modal account:
29
+ 1. Go to https://modal.com and create an account
30
+ 2. Run the following command to authenticate:
31
+ ```bash
32
+ modal token new
33
+ ```
34
+ 3. Follow the instructions to complete authentication
34
35
 
35
- You can also manage individual credentials:
36
+ ### 3. Set Up GitArsenal Credentials
36
37
 
37
38
  ```bash
38
- # Set a specific credential
39
- python manage_credentials.py set openai_api_key
39
+ # Set up all required credentials
40
+ ./gitarsenal.py credentials setup
41
+ ```
40
42
 
41
- # View a credential (masked for security)
42
- python manage_credentials.py get modal_token
43
+ This will guide you through setting up:
44
+ - **Modal Token**: Required for creating cloud environments
45
+ - **OpenAI API Key**: Used for debugging failed commands (optional)
46
+ - **Hugging Face Token**: Used for accessing Hugging Face models (optional)
47
+ - **Weights & Biases API Key**: Used for experiment tracking (optional)
43
48
 
44
- # Clear a specific credential
45
- python manage_credentials.py clear huggingface_token
49
+ ## Usage
46
50
 
47
- # Clear all credentials
48
- python manage_credentials.py clear
51
+ ### Creating a Modal Sandbox
49
52
 
50
- # List all saved credentials (without showing values)
51
- python manage_credentials.py list
53
+ ```bash
54
+ ./gitarsenal.py sandbox --gpu A10G --repo-url "https://github.com/username/repo.git"
52
55
  ```
53
56
 
54
- ## Usage
55
-
56
- ### Creating a Modal Sandbox
57
+ ### Creating an SSH Container
57
58
 
58
59
  ```bash
59
- python test_modalSandboxScript.py --gpu A10G --repo-url "https://github.com/username/repo.git"
60
+ ./gitarsenal.py ssh --gpu A10G --repo-url "https://github.com/username/repo.git"
60
61
  ```
61
62
 
62
63
  ### Options
@@ -66,6 +67,31 @@ python test_modalSandboxScript.py --gpu A10G --repo-url "https://github.com/user
66
67
  - `--repo-name`: Repository name override
67
68
  - `--setup-commands`: Setup commands to run
68
69
  - `--volume-name`: Name of the Modal volume for persistent storage
70
+ - `--timeout`: Container timeout in minutes (SSH mode only, default: 60)
71
+
72
+ ## Managing Credentials
73
+
74
+ You can manage your credentials using the following commands:
75
+
76
+ ```bash
77
+ # Set up all credentials
78
+ ./gitarsenal.py credentials setup
79
+
80
+ # Set a specific credential
81
+ ./gitarsenal.py credentials set modal_token
82
+
83
+ # View a credential (masked for security)
84
+ ./gitarsenal.py credentials get modal_token
85
+
86
+ # Clear a specific credential
87
+ ./gitarsenal.py credentials clear huggingface_token
88
+
89
+ # Clear all credentials
90
+ ./gitarsenal.py credentials clear
91
+
92
+ # List all saved credentials (without showing values)
93
+ ./gitarsenal.py credentials list
94
+ ```
69
95
 
70
96
  ## Security
71
97
 
@@ -73,23 +99,40 @@ Your credentials are stored securely in `~/.gitarsenal/credentials.json` with re
73
99
 
74
100
  ## Troubleshooting
75
101
 
76
- If you encounter authentication issues:
102
+ ### Modal Authentication Issues
77
103
 
78
- 1. Check that your credentials are set up correctly:
104
+ If you see errors like "Token missing" or "Could not authenticate client":
105
+
106
+ 1. Ensure Modal is installed:
79
107
  ```bash
80
- python manage_credentials.py list
108
+ pip install modal
81
109
  ```
82
110
 
83
- 2. If needed, clear and reset your credentials:
111
+ 2. Get a new Modal token:
84
112
  ```bash
85
- python manage_credentials.py clear
86
- python manage_credentials.py setup
113
+ modal token new
87
114
  ```
88
115
 
89
- 3. Ensure Modal CLI is installed and authenticated:
116
+ 3. Save the token in GitArsenal:
90
117
  ```bash
91
- pip install modal
92
- modal token new
118
+ ./gitarsenal.py credentials set modal_token
119
+ ```
120
+
121
+ ### API Timeout Issues
122
+
123
+ If the GitArsenal API times out when analyzing repositories, the tool will automatically use fallback setup commands based on the detected programming language and technologies.
124
+
125
+ ### Other Issues
126
+
127
+ 1. Check that your credentials are set up correctly:
128
+ ```bash
129
+ ./gitarsenal.py credentials list
130
+ ```
131
+
132
+ 2. If needed, clear and reset your credentials:
133
+ ```bash
134
+ ./gitarsenal.py credentials clear
135
+ ./gitarsenal.py credentials setup
93
136
  ```
94
137
 
95
138
  ## License
@@ -9,6 +9,55 @@ 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
12
61
 
13
62
  def main():
14
63
  parser = argparse.ArgumentParser(description="GitArsenal CLI - GPU-accelerated cloud environments")
@@ -82,8 +131,41 @@ def main():
82
131
 
83
132
  return run_script("manage_credentials.py", cred_args)
84
133
 
134
+ # For sandbox and SSH commands, check Modal authentication first
135
+ 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
142
+ try:
143
+ from credentials_manager import CredentialsManager
144
+ credentials_manager = CredentialsManager()
145
+ credentials = credentials_manager.load_credentials()
146
+
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")
151
+
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
+ except ImportError:
163
+ print("⚠️ Could not load credentials manager")
164
+ except Exception as e:
165
+ print(f"⚠️ Error loading credentials: {e}")
166
+
85
167
  # Handle sandbox command
86
- elif args.command == "sandbox":
168
+ if args.command == "sandbox":
87
169
  sandbox_args = []
88
170
 
89
171
  if args.gpu:
@@ -0,0 +1,4 @@
1
+ modal>=0.56.4
2
+ requests>=2.31.0
3
+ pathlib>=1.0.1
4
+ python-dotenv>=1.0.0
@@ -478,36 +478,73 @@ def create_modal_sandbox(gpu_type, repo_url=None, repo_name=None, setup_commands
478
478
 
479
479
  # Check if Modal is authenticated
480
480
  try:
481
- # Try to list apps to check if authentication is working
482
- import subprocess
483
- result = subprocess.run(["modal", "app", "list"], capture_output=True, text=True)
484
- if result.returncode != 0 and "not authenticated" in result.stderr:
485
- print("🔑 Modal authentication required")
486
-
487
- # Try to use credentials manager first
481
+ # Try to import modal first to check if it's installed
482
+ import modal
483
+
484
+ # Try to access Modal token to check authentication
485
+ try:
486
+ # This will raise an exception if not authenticated
487
+ modal.config.get_current_workspace_name()
488
+ print("✅ Modal authentication verified")
489
+ except modal.exception.AuthError:
490
+ print("\n" + "="*80)
491
+ print("🔑 MODAL AUTHENTICATION REQUIRED")
492
+ print("="*80)
493
+ print("GitArsenal requires Modal authentication to create cloud environments.")
494
+
495
+ # Try to get token from credentials manager
496
+ modal_token = None
488
497
  if credentials_manager:
489
- modal_token = credentials_manager.get_modal_token()
490
- if modal_token:
491
- # Set the token in the environment
492
- os.environ["MODAL_TOKEN_ID"] = modal_token
493
- print(" Modal token set from credentials manager")
494
-
495
- # Try to authenticate with the token
496
- try:
497
- token_result = subprocess.run(["modal", "token", "set", modal_token],
498
- capture_output=True, text=True)
499
- if token_result.returncode == 0:
500
- print(" Successfully authenticated with Modal")
501
- else:
502
- print(f"⚠️ Failed to authenticate with Modal: {token_result.stderr}")
503
- print("Please run 'modal token new' manually and try again")
498
+ try:
499
+ modal_token = credentials_manager.get_modal_token()
500
+ if modal_token:
501
+ # Set the token in the environment
502
+ os.environ["MODAL_TOKEN_ID"] = modal_token
503
+ print("✅ Modal token set from credentials manager")
504
+
505
+ # Try to authenticate with the token
506
+ try:
507
+ import subprocess
508
+ token_result = subprocess.run(
509
+ ["modal", "token", "set", modal_token],
510
+ capture_output=True, text=True
511
+ )
512
+ if token_result.returncode == 0:
513
+ print("✅ Successfully authenticated with Modal")
514
+ else:
515
+ print(f"⚠️ Failed to authenticate with Modal: {token_result.stderr}")
516
+ print("\nPlease authenticate manually:")
517
+ print("1. Run 'modal token new' to get a new token")
518
+ print("2. Then restart this command")
519
+ return None
520
+ except Exception as e:
521
+ print(f"⚠️ Error setting Modal token: {e}")
504
522
  return None
505
- except Exception as e:
506
- print(f"⚠️ Error setting Modal token: {e}")
507
- return None
508
- else:
509
- print("⚠️ Modal is not authenticated. Please run 'modal token new' first")
523
+ except Exception as e:
524
+ print(f"⚠️ Error getting Modal token: {e}")
525
+
526
+ if not modal_token:
527
+ print("\nTo authenticate with Modal, you need to:")
528
+ print("1. Create a Modal account at https://modal.com if you don't have one")
529
+ print("2. Run the following command to get a token:")
530
+ print(" modal token new")
531
+ print("3. Then set up your credentials in GitArsenal:")
532
+ print(" ./gitarsenal.py credentials set modal_token")
533
+ print("\nAfter completing these steps, try your command again.")
534
+ print("="*80)
510
535
  return None
536
+ except ImportError:
537
+ print("\n" + "="*80)
538
+ print("❌ MODAL PACKAGE NOT INSTALLED")
539
+ print("="*80)
540
+ print("GitArsenal requires the Modal package to be installed.")
541
+ print("\nTo install Modal, run:")
542
+ print(" pip install modal")
543
+ print("\nAfter installation, authenticate with Modal:")
544
+ print("1. Run 'modal token new'")
545
+ print("2. Then run './gitarsenal.py credentials set modal_token'")
546
+ print("="*80)
547
+ return None
511
548
  except Exception as e:
512
549
  print(f"⚠️ Error checking Modal authentication: {e}")
513
550
  print("Continuing anyway, but Modal operations may fail")
@@ -2393,53 +2430,177 @@ def fetch_setup_commands_from_api(repo_url):
2393
2430
 
2394
2431
  # Make the API request
2395
2432
  print(f"🌐 Making POST request to: {api_url}")
2396
- response = requests.post(api_url, json=payload, timeout=60)
2397
-
2398
- print(f"📥 API Response status code: {response.status_code}")
2399
-
2400
- if response.status_code == 200:
2401
- try:
2402
- data = response.json()
2403
- print(f"📄 API Response data received")
2404
-
2405
- # Extract setup commands from the response
2406
- if "setupInstructions" in data and "commands" in data["setupInstructions"]:
2407
- commands = data["setupInstructions"]["commands"]
2408
- print(f"✅ Successfully fetched {len(commands)} setup commands from API")
2409
-
2410
- # Print the commands for reference
2411
- for i, cmd in enumerate(commands, 1):
2412
- print(f" {i}. {cmd}")
2433
+ try:
2434
+ response = requests.post(api_url, json=payload, timeout=60)
2435
+
2436
+ print(f"📥 API Response status code: {response.status_code}")
2437
+
2438
+ if response.status_code == 200:
2439
+ try:
2440
+ data = response.json()
2441
+ print(f"📄 API Response data received")
2413
2442
 
2414
- return commands
2415
- else:
2416
- print("⚠️ API response did not contain setupInstructions.commands field")
2417
- print("📋 Available fields in response:")
2418
- for key in data.keys():
2419
- print(f" - {key}")
2420
- return []
2421
- except json.JSONDecodeError as e:
2422
- print(f"❌ Failed to parse API response as JSON: {e}")
2423
- print(f"Raw response: {response.text[:500]}...")
2424
- return []
2425
- else:
2426
- print(f" API request failed with status code: {response.status_code}")
2427
- print(f"Error response: {response.text[:500]}...")
2428
- return []
2429
- except requests.exceptions.ConnectionError:
2430
- print(f"❌ Connection error: Could not connect to {api_url}")
2431
- print("⚠️ Make sure the API server is running at localhost:3000")
2432
- return []
2443
+ # Extract setup commands from the response
2444
+ if "setupInstructions" in data and "commands" in data["setupInstructions"]:
2445
+ commands = data["setupInstructions"]["commands"]
2446
+ print(f" Successfully fetched {len(commands)} setup commands from API")
2447
+
2448
+ # Print the commands for reference
2449
+ for i, cmd in enumerate(commands, 1):
2450
+ print(f" {i}. {cmd}")
2451
+
2452
+ return commands
2453
+ else:
2454
+ print("⚠️ API response did not contain setupInstructions.commands field")
2455
+ print("📋 Available fields in response:")
2456
+ for key in data.keys():
2457
+ print(f" - {key}")
2458
+ # Return fallback commands
2459
+ return generate_fallback_commands(gitingest_data)
2460
+ except json.JSONDecodeError as e:
2461
+ print(f"❌ Failed to parse API response as JSON: {e}")
2462
+ print(f"Raw response: {response.text[:500]}...")
2463
+ # Return fallback commands
2464
+ return generate_fallback_commands(gitingest_data)
2465
+ elif response.status_code == 504:
2466
+ print(f"❌ API request timed out (504 Gateway Timeout)")
2467
+ print("⚠️ The server took too long to respond. Using fallback commands instead.")
2468
+ # Return fallback commands
2469
+ return generate_fallback_commands(gitingest_data)
2470
+ else:
2471
+ print(f"❌ API request failed with status code: {response.status_code}")
2472
+ print(f"Error response: {response.text[:500]}...")
2473
+ # Return fallback commands
2474
+ return generate_fallback_commands(gitingest_data)
2475
+ except requests.exceptions.Timeout:
2476
+ print("❌ API request timed out after 60 seconds")
2477
+ print("⚠️ Using fallback commands instead")
2478
+ # Return fallback commands
2479
+ return generate_fallback_commands(gitingest_data)
2480
+ except requests.exceptions.ConnectionError:
2481
+ print(f"❌ Connection error: Could not connect to {api_url}")
2482
+ print("⚠️ Using fallback commands instead")
2483
+ # Return fallback commands
2484
+ return generate_fallback_commands(gitingest_data)
2433
2485
  except Exception as e:
2434
2486
  print(f"❌ Error fetching setup commands from API: {e}")
2435
2487
  import traceback
2436
2488
  traceback.print_exc()
2437
- return []
2489
+ # Return fallback commands
2490
+ return generate_fallback_commands(None)
2438
2491
  finally:
2439
2492
  # Clean up the temporary directory
2440
2493
  print(f"🧹 Cleaning up temporary directory...")
2441
2494
  shutil.rmtree(temp_dir, ignore_errors=True)
2442
2495
 
2496
+ def generate_fallback_commands(gitingest_data):
2497
+ """Generate fallback setup commands based on repository analysis"""
2498
+ print("\n" + "="*80)
2499
+ print("📋 GENERATING FALLBACK SETUP COMMANDS")
2500
+ print("="*80)
2501
+ print("Using basic repository analysis to generate setup commands")
2502
+
2503
+ # Default commands that work for most repositories
2504
+ default_commands = [
2505
+ "apt-get update -y",
2506
+ "apt-get install -y git curl wget",
2507
+ "pip install --upgrade pip setuptools wheel"
2508
+ ]
2509
+
2510
+ # If we don't have any analysis data, return default commands
2511
+ if not gitingest_data:
2512
+ print("⚠️ No repository analysis data available. Using default commands.")
2513
+ return default_commands
2514
+
2515
+ # Extract language and technologies information
2516
+ detected_language = gitingest_data.get("system_info", {}).get("detected_language", "Unknown")
2517
+ detected_technologies = gitingest_data.get("system_info", {}).get("detected_technologies", [])
2518
+ primary_package_manager = gitingest_data.get("system_info", {}).get("primary_package_manager", "Unknown")
2519
+
2520
+ # Add language-specific commands
2521
+ language_commands = []
2522
+
2523
+ print(f"📋 Detected primary language: {detected_language}")
2524
+ print(f"📋 Detected technologies: {', '.join(detected_technologies) if detected_technologies else 'None'}")
2525
+ print(f"📋 Detected package manager: {primary_package_manager}")
2526
+
2527
+ # Python-specific commands
2528
+ if detected_language == "Python" or primary_package_manager == "pip":
2529
+ print("📦 Adding Python-specific setup commands")
2530
+
2531
+ # Check for requirements.txt
2532
+ requirements_check = [
2533
+ "if [ -f requirements.txt ]; then",
2534
+ " echo 'Installing from requirements.txt'",
2535
+ " pip install -r requirements.txt",
2536
+ "elif [ -f setup.py ]; then",
2537
+ " echo 'Installing from setup.py'",
2538
+ " pip install -e .",
2539
+ "fi"
2540
+ ]
2541
+ language_commands.extend(requirements_check)
2542
+
2543
+ # Add common Python packages
2544
+ language_commands.append("pip install pytest numpy pandas matplotlib")
2545
+
2546
+ # JavaScript/Node.js specific commands
2547
+ elif detected_language in ["JavaScript", "TypeScript"] or primary_package_manager in ["npm", "yarn", "pnpm"]:
2548
+ print("📦 Adding JavaScript/Node.js-specific setup commands")
2549
+
2550
+ # Install Node.js if not available
2551
+ language_commands.append("apt-get install -y nodejs npm")
2552
+
2553
+ # Check for package.json
2554
+ package_json_check = [
2555
+ "if [ -f package.json ]; then",
2556
+ " echo 'Installing from package.json'",
2557
+ " npm install",
2558
+ "fi"
2559
+ ]
2560
+ language_commands.extend(package_json_check)
2561
+
2562
+ # Java specific commands
2563
+ elif detected_language == "Java" or primary_package_manager in ["maven", "gradle"]:
2564
+ print("📦 Adding Java-specific setup commands")
2565
+
2566
+ language_commands.append("apt-get install -y openjdk-11-jdk maven gradle")
2567
+
2568
+ # Check for Maven or Gradle
2569
+ build_check = [
2570
+ "if [ -f pom.xml ]; then",
2571
+ " echo 'Building with Maven'",
2572
+ " mvn clean install -DskipTests",
2573
+ "elif [ -f build.gradle ]; then",
2574
+ " echo 'Building with Gradle'",
2575
+ " gradle build --no-daemon",
2576
+ "fi"
2577
+ ]
2578
+ language_commands.extend(build_check)
2579
+
2580
+ # Go specific commands
2581
+ elif detected_language == "Go" or primary_package_manager == "go":
2582
+ print("📦 Adding Go-specific setup commands")
2583
+
2584
+ language_commands.append("apt-get install -y golang-go")
2585
+ language_commands.append("go mod tidy")
2586
+
2587
+ # Rust specific commands
2588
+ elif detected_language == "Rust" or primary_package_manager == "cargo":
2589
+ print("📦 Adding Rust-specific setup commands")
2590
+
2591
+ language_commands.append("curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y")
2592
+ language_commands.append("source $HOME/.cargo/env")
2593
+ language_commands.append("cargo build")
2594
+
2595
+ # Combine all commands
2596
+ all_commands = default_commands + language_commands
2597
+
2598
+ print("\n📋 Generated fallback setup commands:")
2599
+ for i, cmd in enumerate(all_commands, 1):
2600
+ print(f" {i}. {cmd}")
2601
+
2602
+ return all_commands
2603
+
2443
2604
  def generate_basic_repo_analysis_from_url(repo_url):
2444
2605
  """Generate basic repository analysis data from a repository URL."""
2445
2606
  import tempfile