gitarsenal-cli 1.4.5 → 1.4.7

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.4.5",
3
+ "version": "1.4.7",
4
4
  "description": "CLI tool for creating Modal sandboxes with GitHub repositories",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -126,6 +126,7 @@ def main():
126
126
  ssh_parser.add_argument("--timeout", type=int, default=60, help="Container timeout in minutes (default: 60)")
127
127
  ssh_parser.add_argument("--use-proxy", action="store_true", help="Use Modal proxy service instead of direct Modal API")
128
128
  ssh_parser.add_argument("--wait", action="store_true", help="Wait for container to be ready")
129
+ ssh_parser.add_argument("--interactive", action="store_true", help="Run in interactive mode with prompts")
129
130
 
130
131
  # Sandbox command
131
132
  sandbox_parser = subparsers.add_parser("sandbox", help="Create a Modal sandbox")
@@ -195,7 +196,8 @@ def main():
195
196
  repo_url=args.repo_url,
196
197
  repo_name=args.repo_name,
197
198
  volume_name=args.volume_name,
198
- timeout_minutes=args.timeout
199
+ timeout_minutes=args.timeout,
200
+ interactive=args.interactive
199
201
  )
200
202
  if result:
201
203
  print("✅ SSH container created successfully")
@@ -13,7 +13,7 @@ import argparse
13
13
  from pathlib import Path
14
14
 
15
15
  # Parse command-line arguments
16
- parser = argparse.ArgumentParser(description='Launch a Modal sandbox')
16
+ parser = argparse.ArgumentParser()
17
17
  parser.add_argument('--proxy-url', help='URL of the proxy server')
18
18
  parser.add_argument('--proxy-api-key', help='API key for the proxy server')
19
19
  parser.add_argument('--gpu', default='A10G', help='GPU type to use')
@@ -720,85 +720,85 @@ Do not provide any explanations, just the exact command to run.
720
720
  print(f"❌ All model attempts failed. Last error: {last_error}")
721
721
  return None
722
722
 
723
- # Process the response
724
- try:
725
- fix_command = result["choices"][0]["message"]["content"].strip()
726
-
727
- # Save the original response for debugging
728
- original_response = fix_command
723
+ # Process the response
724
+ try:
725
+ fix_command = result["choices"][0]["message"]["content"].strip()
726
+
727
+ # Save the original response for debugging
728
+ original_response = fix_command
729
+
730
+ # Extract just the command if it's wrapped in backticks or explanation
731
+ if "```" in fix_command:
732
+ # Extract content between backticks
733
+ import re
734
+ code_blocks = re.findall(r'```(?:bash|sh)?\s*(.*?)\s*```', fix_command, re.DOTALL)
735
+ if code_blocks:
736
+ fix_command = code_blocks[0].strip()
737
+ print(f"✅ Extracted command from code block: {fix_command}")
738
+
739
+ # If the response still has explanatory text, try to extract just the command
740
+ if len(fix_command.split('\n')) > 1:
741
+ # First try to find lines that look like commands (start with common command prefixes)
742
+ command_prefixes = ['sudo', 'apt', 'pip', 'npm', 'yarn', 'git', 'cd', 'mv', 'cp', 'rm', 'mkdir', 'touch',
743
+ 'chmod', 'chown', 'echo', 'cat', 'python', 'python3', 'node', 'export',
744
+ 'curl', 'wget', 'docker', 'make', 'gcc', 'g++', 'javac', 'java',
745
+ 'conda', 'uv', 'poetry', 'nvm', 'rbenv', 'pyenv', 'rustup']
729
746
 
730
- # Extract just the command if it's wrapped in backticks or explanation
731
- if "```" in fix_command:
732
- # Extract content between backticks
733
- import re
734
- code_blocks = re.findall(r'```(?:bash|sh)?\s*(.*?)\s*```', fix_command, re.DOTALL)
735
- if code_blocks:
736
- fix_command = code_blocks[0].strip()
737
- print(f"✅ Extracted command from code block: {fix_command}")
747
+ # Check for lines that start with common command prefixes
748
+ command_lines = [line.strip() for line in fix_command.split('\n')
749
+ if any(line.strip().startswith(prefix) for prefix in command_prefixes)]
738
750
 
739
- # If the response still has explanatory text, try to extract just the command
740
- if len(fix_command.split('\n')) > 1:
741
- # First try to find lines that look like commands (start with common command prefixes)
742
- command_prefixes = ['sudo', 'apt', 'pip', 'npm', 'yarn', 'git', 'cd', 'mv', 'cp', 'rm', 'mkdir', 'touch',
743
- 'chmod', 'chown', 'echo', 'cat', 'python', 'python3', 'node', 'export',
744
- 'curl', 'wget', 'docker', 'make', 'gcc', 'g++', 'javac', 'java',
745
- 'conda', 'uv', 'poetry', 'nvm', 'rbenv', 'pyenv', 'rustup']
746
-
747
- # Check for lines that start with common command prefixes
751
+ if command_lines:
752
+ # Use the first command line found
753
+ fix_command = command_lines[0]
754
+ print(f"✅ Identified command by prefix: {fix_command}")
755
+ else:
756
+ # Try to find lines that look like commands (contain common shell patterns)
757
+ shell_patterns = [' | ', ' > ', ' >> ', ' && ', ' || ', ' ; ', '$(', '`', ' -y ', ' --yes ']
748
758
  command_lines = [line.strip() for line in fix_command.split('\n')
749
- if any(line.strip().startswith(prefix) for prefix in command_prefixes)]
759
+ if any(pattern in line for pattern in shell_patterns)]
750
760
 
751
761
  if command_lines:
752
762
  # Use the first command line found
753
763
  fix_command = command_lines[0]
754
- print(f"✅ Identified command by prefix: {fix_command}")
764
+ print(f"✅ Identified command by shell pattern: {fix_command}")
755
765
  else:
756
- # Try to find lines that look like commands (contain common shell patterns)
757
- shell_patterns = [' | ', ' > ', ' >> ', ' && ', ' || ', ' ; ', '$(', '`', ' -y ', ' --yes ']
758
- command_lines = [line.strip() for line in fix_command.split('\n')
759
- if any(pattern in line for pattern in shell_patterns)]
760
-
761
- if command_lines:
762
- # Use the first command line found
763
- fix_command = command_lines[0]
764
- print(f"✅ Identified command by shell pattern: {fix_command}")
765
- else:
766
- # Fall back to the shortest non-empty line as it's likely the command
767
- lines = [line.strip() for line in fix_command.split('\n') if line.strip()]
768
- if lines:
769
- # Exclude very short lines that are likely not commands
770
- valid_lines = [line for line in lines if len(line) > 5]
771
- if valid_lines:
772
- fix_command = min(valid_lines, key=len)
773
- else:
774
- fix_command = min(lines, key=len)
775
- print(f"✅ Selected shortest line as command: {fix_command}")
776
-
777
- # Clean up the command - remove any trailing periods or quotes
778
- fix_command = fix_command.rstrip('.;"\'')
779
-
780
- # Remove common prefixes that LLMs sometimes add
781
- prefixes_to_remove = [
782
- "Run: ", "Execute: ", "Try: ", "Command: ", "Fix: ", "Solution: ",
783
- "You should run: ", "You can run: ", "You need to run: "
784
- ]
785
- for prefix in prefixes_to_remove:
786
- if fix_command.startswith(prefix):
787
- fix_command = fix_command[len(prefix):].strip()
788
- print(f" Removed prefix: {prefix}")
789
- break
790
-
791
- # If the command is still multi-line or very long, it might not be a valid command
792
- if len(fix_command.split('\n')) > 1 or len(fix_command) > 500:
793
- print("⚠️ Extracted command appears invalid (multi-line or too long)")
794
- print("🔍 Original response from LLM:")
795
- print("-" * 60)
796
- print(original_response)
797
- print("-" * 60)
798
- print("⚠️ Using best guess for command")
799
-
800
- print(f"🔧 Suggested fix: {fix_command}")
801
- return fix_command
766
+ # Fall back to the shortest non-empty line as it's likely the command
767
+ lines = [line.strip() for line in fix_command.split('\n') if line.strip()]
768
+ if lines:
769
+ # Exclude very short lines that are likely not commands
770
+ valid_lines = [line for line in lines if len(line) > 5]
771
+ if valid_lines:
772
+ fix_command = min(valid_lines, key=len)
773
+ else:
774
+ fix_command = min(lines, key=len)
775
+ print(f"✅ Selected shortest line as command: {fix_command}")
776
+
777
+ # Clean up the command - remove any trailing periods or quotes
778
+ fix_command = fix_command.rstrip('.;"\'')
779
+
780
+ # Remove common prefixes that LLMs sometimes add
781
+ prefixes_to_remove = [
782
+ "Run: ", "Execute: ", "Try: ", "Command: ", "Fix: ", "Solution: ",
783
+ "You should run: ", "You can run: ", "You need to run: "
784
+ ]
785
+ for prefix in prefixes_to_remove:
786
+ if fix_command.startswith(prefix):
787
+ fix_command = fix_command[len(prefix):].strip()
788
+ print(f"✅ Removed prefix: {prefix}")
789
+ break
790
+
791
+ # If the command is still multi-line or very long, it might not be a valid command
792
+ if len(fix_command.split('\n')) > 1 or len(fix_command) > 500:
793
+ print("⚠️ Extracted command appears invalid (multi-line or too long)")
794
+ print("🔍 Original response from LLM:")
795
+ print("-" * 60)
796
+ print(original_response)
797
+ print("-" * 60)
798
+ print("⚠️ Using best guess for command")
799
+
800
+ print(f"🔧 Suggested fix: {fix_command}")
801
+ return fix_command
802
802
  except Exception as e:
803
803
  print(f"❌ Error processing OpenAI response: {e}")
804
804
  return None
@@ -2583,9 +2583,39 @@ def ssh_container_function(ssh_password, repo_url=None, repo_name=None, setup_co
2583
2583
  # Now modify the create_modal_ssh_container function to use the standalone ssh_container_function
2584
2584
 
2585
2585
  def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_commands=None,
2586
- volume_name=None, timeout_minutes=60, ssh_password=None):
2586
+ volume_name=None, timeout_minutes=60, ssh_password=None, interactive=False):
2587
2587
  """Create a Modal SSH container with GPU support and tunneling"""
2588
2588
 
2589
+ # Use interactive mode if specified
2590
+ if interactive:
2591
+ # If GPU type is not specified, prompt for it
2592
+ if not gpu_type:
2593
+ gpu_type = prompt_for_gpu()
2594
+
2595
+ # If repo URL is not specified, prompt for it
2596
+ if not repo_url:
2597
+ try:
2598
+ repo_url = input("? Enter GitHub repository URL: ").strip()
2599
+ if not repo_url:
2600
+ print("❌ Repository URL is required.")
2601
+ return None
2602
+ except KeyboardInterrupt:
2603
+ print("\n🛑 Setup cancelled.")
2604
+ return None
2605
+
2606
+ # If volume name is not specified, ask about persistent volume
2607
+ if not volume_name:
2608
+ try:
2609
+ use_volume = input("? Use persistent volume for faster installs? (Y/n): ").strip().lower()
2610
+ if use_volume in ('', 'y', 'yes'):
2611
+ volume_name = input("? Enter volume name: ").strip()
2612
+ if not volume_name:
2613
+ volume_name = "gitarsenal-volume"
2614
+ print(f"Using default volume name: {volume_name}")
2615
+ except KeyboardInterrupt:
2616
+ print("\n🛑 Setup cancelled.")
2617
+ return None
2618
+
2589
2619
  # Check if Modal is authenticated
2590
2620
  try:
2591
2621
  # Print all environment variables for debugging
@@ -4064,13 +4094,74 @@ def get_setup_commands_from_gitingest(repo_url):
4064
4094
  print("❌ All API endpoints failed")
4065
4095
  return generate_fallback_commands(gitingest_data)
4066
4096
 
4097
+ def prompt_for_gpu():
4098
+ """
4099
+ Prompt the user to select a GPU type from available options.
4100
+ Returns the selected GPU type.
4101
+ """
4102
+ # Define available GPU types and their specifications
4103
+ gpu_specs = {
4104
+ 'T4': {'gpu': 'T4', 'memory': '16GB'},
4105
+ 'L4': {'gpu': 'L4', 'memory': '24GB'},
4106
+ 'A10G': {'gpu': 'A10G', 'memory': '24GB'},
4107
+ 'A100-40GB': {'gpu': 'A100-SXM4-40GB', 'memory': '40GB'},
4108
+ 'A100-80GB': {'gpu': 'A100-80GB', 'memory': '80GB'},
4109
+ 'L40S': {'gpu': 'L40S', 'memory': '48GB'},
4110
+ 'H100': {'gpu': 'H100', 'memory': '80GB'},
4111
+ 'H200': {'gpu': 'H200', 'memory': '141GB'},
4112
+ 'B200': {'gpu': 'B200', 'memory': '141GB'}
4113
+ }
4114
+
4115
+ print("\n📊 Available GPU Options:")
4116
+ print("┌─────────────┬──────────┐")
4117
+ print("│ GPU Type │ Memory │")
4118
+ print("├─────────────┼──────────┤")
4119
+
4120
+ # Create a list to keep track of valid options
4121
+ options = []
4122
+
4123
+ # Display GPU options
4124
+ for i, (gpu_type, specs) in enumerate(gpu_specs.items(), 1):
4125
+ options.append(gpu_type)
4126
+ print(f"│ {i}. {gpu_type:<8} │ {specs['memory']:<7} │")
4127
+
4128
+ print("└─────────────┴──────────┘")
4129
+
4130
+ # Prompt for selection
4131
+ while True:
4132
+ try:
4133
+ choice = input("\n🔍 Select GPU type (number or name, default is A10G): ").strip()
4134
+
4135
+ # Default to A10G if empty
4136
+ if not choice:
4137
+ return "A10G"
4138
+
4139
+ # Check if input is a number
4140
+ if choice.isdigit():
4141
+ index = int(choice) - 1
4142
+ if 0 <= index < len(options):
4143
+ return options[index]
4144
+ else:
4145
+ print(f"❌ Invalid selection. Please enter a number between 1 and {len(options)}.")
4146
+ # Check if input is a valid GPU type
4147
+ elif choice in options:
4148
+ return choice
4149
+ else:
4150
+ print(f"❌ Invalid GPU type. Please select from the available options.")
4151
+ except KeyboardInterrupt:
4152
+ print("\n🛑 Selection cancelled.")
4153
+ sys.exit(1)
4154
+ except Exception as e:
4155
+ print(f"❌ Error: {e}")
4156
+
4157
+ # Replace the existing GPU argument parsing in the main section
4067
4158
  if __name__ == "__main__":
4068
4159
  # Parse command line arguments when script is run directly
4069
4160
  import argparse
4070
4161
  import sys
4071
4162
 
4072
- parser = argparse.ArgumentParser(description='Create a Modal SSH container with GPU')
4073
- parser.add_argument('--gpu', type=str, default='A10G', help='GPU type (default: A10G)')
4163
+ parser = argparse.ArgumentParser()
4164
+ parser.add_argument('--gpu', type=str, help='GPU type (e.g., A10G, T4, A100-80GB)')
4074
4165
  parser.add_argument('--repo-url', type=str, help='Repository URL to clone')
4075
4166
  parser.add_argument('--repo-name', type=str, help='Repository name override')
4076
4167
  parser.add_argument('--setup-commands', type=str, nargs='+', help='Setup commands to run (deprecated)')
@@ -4085,20 +4176,105 @@ if __name__ == "__main__":
4085
4176
  parser.add_argument('--use-gitingest', action='store_true', default=True, help='Use gitingest approach to fetch setup commands (default)')
4086
4177
  parser.add_argument('--no-gitingest', action='store_true', help='Disable gitingest approach for setup commands')
4087
4178
  parser.add_argument('--show-examples', action='store_true', help='Show usage examples')
4179
+ parser.add_argument('--list-gpus', action='store_true', help='List available GPU types with their specifications')
4180
+ parser.add_argument('--interactive', action='store_true', help='Run in interactive mode with prompts')
4088
4181
 
4089
4182
  args = parser.parse_args()
4090
4183
 
4184
+ # If --list-gpus is specified, just show GPU options and exit
4185
+ if args.list_gpus:
4186
+ prompt_for_gpu()
4187
+ sys.exit(0)
4188
+
4091
4189
  # If no arguments or only --show-examples is provided, show usage examples
4092
4190
  if len(sys.argv) == 1 or args.show_examples:
4093
4191
  show_usage_examples()
4094
4192
  sys.exit(0)
4095
4193
 
4194
+ # Check for dependencies
4195
+ print("⠏ Checking dependencies...")
4196
+ print("--- Dependency Check ---")
4197
+
4198
+ # Check Python version
4199
+ python_version = sys.version.split()[0]
4200
+ print(f"✓ Python {python_version} found")
4201
+
4202
+ # Check Modal CLI
4203
+ try:
4204
+ subprocess.run(["modal", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
4205
+ print("✓ Modal CLI found")
4206
+ except (subprocess.SubprocessError, FileNotFoundError):
4207
+ print("❌ Modal CLI not found. Please install with: pip install modal")
4208
+
4209
+ # Check Gitingest CLI
4210
+ try:
4211
+ subprocess.run(["gitingest", "--help"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
4212
+ print("✓ Gitingest CLI found")
4213
+ except (subprocess.SubprocessError, FileNotFoundError):
4214
+ print("⚠️ Gitingest CLI not found (optional)")
4215
+
4216
+ # Check Git
4217
+ try:
4218
+ subprocess.run(["git", "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
4219
+ print("✓ Git found")
4220
+ except (subprocess.SubprocessError, FileNotFoundError):
4221
+ print("❌ Git not found. Please install Git.")
4222
+
4223
+ print("------------------------")
4224
+ print("\n✔ Dependencies checked")
4225
+
4226
+ # Always prompt for GPU selection regardless of interactive mode
4227
+ gpu_type = prompt_for_gpu()
4228
+ args.gpu = gpu_type
4229
+
4230
+ # Interactive mode or missing required arguments
4231
+ if args.interactive or not args.repo_url or not args.volume_name:
4232
+ # Get repository URL if not provided
4233
+ repo_url = args.repo_url
4234
+ if not repo_url:
4235
+ try:
4236
+ repo_url = input("? Enter GitHub repository URL: ").strip()
4237
+ if not repo_url:
4238
+ print("❌ Repository URL is required.")
4239
+ sys.exit(1)
4240
+ except KeyboardInterrupt:
4241
+ print("\n🛑 Setup cancelled.")
4242
+ sys.exit(1)
4243
+
4244
+ # Ask about persistent volume
4245
+ volume_name = args.volume_name
4246
+ if not volume_name:
4247
+ try:
4248
+ use_volume = input("? Use persistent volume for faster installs? (Y/n): ").strip().lower()
4249
+ if use_volume in ('', 'y', 'yes'):
4250
+ volume_name = input("? Enter volume name: ").strip()
4251
+ if not volume_name:
4252
+ volume_name = "gitarsenal-volume"
4253
+ print(f"Using default volume name: {volume_name}")
4254
+ except KeyboardInterrupt:
4255
+ print("\n🛑 Setup cancelled.")
4256
+ sys.exit(1)
4257
+
4258
+ # Ask about setup commands
4259
+ use_gitingest = args.use_gitingest and not args.no_gitingest
4260
+ if not args.use_api and not args.setup_commands and not args.setup_commands_json:
4261
+ try:
4262
+ auto_detect = input("? Automatically detect setup commands for this repository? (Y/n): ").strip().lower()
4263
+ if auto_detect in ('n', 'no'):
4264
+ use_gitingest = False
4265
+ except KeyboardInterrupt:
4266
+ print("\n🛑 Setup cancelled.")
4267
+ sys.exit(1)
4268
+
4269
+ # Update args with interactive values
4270
+ args.repo_url = repo_url
4271
+ args.volume_name = volume_name
4272
+ args.use_gitingest = use_gitingest
4273
+
4096
4274
  try:
4097
4275
  # Get setup commands from file if specified
4098
4276
  setup_commands = args.setup_commands or []
4099
4277
 
4100
- # If --use-api flag is set and repo_url is provided, fetch setup commands from API
4101
-
4102
4278
  # Use gitingest by default unless --no-gitingest is set
4103
4279
  if args.repo_url and (args.use_gitingest and not args.no_gitingest):
4104
4280
  print("🔄 Using gitingest approach to fetch setup commands (default)")
@@ -4215,7 +4391,8 @@ if __name__ == "__main__":
4215
4391
  setup_commands=setup_commands,
4216
4392
  volume_name=args.volume_name,
4217
4393
  timeout_minutes=args.timeout,
4218
- ssh_password=ssh_password
4394
+ ssh_password=ssh_password,
4395
+ interactive=args.interactive
4219
4396
  )
4220
4397
  except KeyboardInterrupt:
4221
4398
  # print("\n\n🛑 Execution interrupted")