gitarsenal-cli 1.9.71 → 1.9.73
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/.venv_status.json +1 -1
- package/bin/gitarsenal.js +8 -31
- package/kill_claude/prompts/claude-code-system-prompt.md +13 -0
- package/kill_claude/prompts/claude-code-tool-prompts.md +1 -0
- package/kill_claude/tools/__pycache__/task_tool.cpython-313.pyc +0 -0
- package/kill_claude/tools/bash_tool.py +1 -0
- package/lib/sandbox.js +1 -8
- package/package.json +1 -1
- package/python/debug_modal_minimal.py +212 -0
- package/python/test_container.py +108 -17
- package/python/test_modalSandboxScript.py +65 -1097
|
@@ -6,150 +6,34 @@ import json
|
|
|
6
6
|
import re
|
|
7
7
|
import datetime
|
|
8
8
|
import getpass
|
|
9
|
-
import requests
|
|
10
9
|
import secrets
|
|
11
10
|
import string
|
|
12
11
|
import argparse
|
|
13
|
-
import threading
|
|
14
|
-
import uuid
|
|
15
|
-
import signal
|
|
16
12
|
from pathlib import Path
|
|
17
13
|
import modal
|
|
18
|
-
import io
|
|
19
|
-
import contextlib
|
|
20
|
-
import unicodedata
|
|
21
|
-
import shutil
|
|
22
14
|
from auth_manager import AuthManager
|
|
23
15
|
|
|
24
|
-
#
|
|
25
|
-
_BOX_CAPTURE_ACTIVE = False
|
|
16
|
+
# Removed unused boxed output functions since they're no longer used with the Agent-based approach
|
|
26
17
|
|
|
27
18
|
|
|
28
|
-
|
|
29
|
-
# Use NBSP for padding to prevent renderers from trimming trailing spaces
|
|
30
|
-
NBSP = "\u00A0"
|
|
31
|
-
lines = content.splitlines() if content else ["(no output)"]
|
|
19
|
+
# Removed _execute_with_box function as it's no longer used with the Agent-based approach
|
|
32
20
|
|
|
33
|
-
|
|
34
|
-
|
|
21
|
+
# Early argument parsing for proxy settings only
|
|
22
|
+
early_parser = argparse.ArgumentParser(add_help=False)
|
|
23
|
+
early_parser.add_argument('--proxy-url', help='URL of the proxy server')
|
|
24
|
+
early_parser.add_argument('--proxy-api-key', help='API key for the proxy server')
|
|
35
25
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
width = 0
|
|
39
|
-
for ch in s:
|
|
40
|
-
if unicodedata.combining(ch):
|
|
41
|
-
continue
|
|
42
|
-
ea = unicodedata.east_asian_width(ch)
|
|
43
|
-
width += 2 if ea in ("W", "F") else 1
|
|
44
|
-
return width
|
|
45
|
-
|
|
46
|
-
# Compute desired inner width: full terminal width when available
|
|
47
|
-
visible_widths = [_display_width(_strip_ansi(line)) for line in lines]
|
|
48
|
-
term_width = shutil.get_terminal_size(fallback=(120, 24)).columns
|
|
49
|
-
# Borders add 4 chars: left '│ ', right ' │'
|
|
50
|
-
desired_inner = max(20, term_width - 4)
|
|
51
|
-
|
|
52
|
-
# If content has lines longer than max_inner, hard-truncate with ellipsis preserving ANSI
|
|
53
|
-
def _truncate_line(line: str, target: int) -> str:
|
|
54
|
-
plain = _strip_ansi(line)
|
|
55
|
-
if _display_width(plain) <= target:
|
|
56
|
-
return line
|
|
57
|
-
out = []
|
|
58
|
-
acc = 0
|
|
59
|
-
i = 0
|
|
60
|
-
while i < len(line) and acc < max(0, target - 1):
|
|
61
|
-
ch = line[i]
|
|
62
|
-
out.append(ch)
|
|
63
|
-
# Skip ANSI sequences intact
|
|
64
|
-
if ch == "\x1b":
|
|
65
|
-
# consume until letter
|
|
66
|
-
j = i + 1
|
|
67
|
-
while j < len(line) and not (line[j].isalpha() and line[j-1] == 'm'):
|
|
68
|
-
j += 1
|
|
69
|
-
# include the terminator if present
|
|
70
|
-
if j < len(line):
|
|
71
|
-
out.append(line[i+1:j+1])
|
|
72
|
-
i = j + 1
|
|
73
|
-
else:
|
|
74
|
-
i += 1
|
|
75
|
-
continue
|
|
76
|
-
# width accounting
|
|
77
|
-
if not unicodedata.combining(ch):
|
|
78
|
-
ea = unicodedata.east_asian_width(ch)
|
|
79
|
-
acc += 2 if ea in ("W", "F") else 1
|
|
80
|
-
i += 1
|
|
81
|
-
return ''.join(out) + '…'
|
|
82
|
-
|
|
83
|
-
adjusted_lines = [_truncate_line(line, desired_inner) for line in lines]
|
|
84
|
-
inner_width = desired_inner
|
|
85
|
-
|
|
86
|
-
top = "┌" + "─" * (inner_width + 2) + "┐"
|
|
87
|
-
bottom = "└" + "─" * (inner_width + 2) + "┘"
|
|
88
|
-
print(top)
|
|
89
|
-
for line in adjusted_lines:
|
|
90
|
-
vis = _strip_ansi(line)
|
|
91
|
-
pad_len = inner_width - _display_width(vis)
|
|
92
|
-
pad = NBSP * max(pad_len, 0)
|
|
93
|
-
print("│ " + line + pad + " │")
|
|
94
|
-
print(bottom)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
@contextlib.contextmanager
|
|
98
|
-
def _boxed_capture():
|
|
99
|
-
global _BOX_CAPTURE_ACTIVE
|
|
100
|
-
buf = io.StringIO()
|
|
101
|
-
prev = _BOX_CAPTURE_ACTIVE
|
|
102
|
-
_BOX_CAPTURE_ACTIVE = True
|
|
103
|
-
try:
|
|
104
|
-
with contextlib.redirect_stdout(buf):
|
|
105
|
-
yield
|
|
106
|
-
finally:
|
|
107
|
-
_BOX_CAPTURE_ACTIVE = prev
|
|
108
|
-
content = buf.getvalue().rstrip("\n")
|
|
109
|
-
_print_boxed_block(content)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def _execute_with_box(shell, cmd_text: str, timeout: int, title_line: str = None, box: bool = True):
|
|
113
|
-
start_time = time.time()
|
|
114
|
-
if box and not _BOX_CAPTURE_ACTIVE:
|
|
115
|
-
buf = io.StringIO()
|
|
116
|
-
with contextlib.redirect_stdout(buf):
|
|
117
|
-
if title_line:
|
|
118
|
-
print(title_line)
|
|
119
|
-
success, stdout, stderr = shell.execute(cmd_text, timeout=timeout)
|
|
120
|
-
execution_time = time.time() - start_time
|
|
121
|
-
content = buf.getvalue().rstrip("\n")
|
|
122
|
-
_print_boxed_block(content)
|
|
123
|
-
return success, stdout, stderr, execution_time
|
|
124
|
-
else:
|
|
125
|
-
if title_line:
|
|
126
|
-
print(title_line)
|
|
127
|
-
success, stdout, stderr = shell.execute(cmd_text, timeout=timeout)
|
|
128
|
-
execution_time = time.time() - start_time
|
|
129
|
-
return success, stdout, stderr, execution_time
|
|
130
|
-
|
|
131
|
-
# Parse command-line arguments
|
|
132
|
-
parser = argparse.ArgumentParser()
|
|
133
|
-
parser.add_argument('--proxy-url', help='URL of the proxy server')
|
|
134
|
-
parser.add_argument('--proxy-api-key', help='API key for the proxy server')
|
|
135
|
-
parser.add_argument('--gpu', default='A10G', help='GPU type to use')
|
|
136
|
-
parser.add_argument('--repo-url', help='Repository URL')
|
|
137
|
-
parser.add_argument('--volume-name', help='Volume name')
|
|
138
|
-
parser.add_argument('--use-api', action='store_true', help='Use API to fetch setup commands')
|
|
139
|
-
parser.add_argument('--yes', action='store_true', help='Automatically confirm prompts (non-interactive)')
|
|
140
|
-
|
|
141
|
-
# Parse only known args to avoid conflicts with other arguments
|
|
142
|
-
args, unknown = parser.parse_known_args()
|
|
26
|
+
# Parse only proxy args early to avoid conflicts
|
|
27
|
+
early_args, _ = early_parser.parse_known_args()
|
|
143
28
|
|
|
144
29
|
# Set proxy URL and API key in environment variables if provided
|
|
145
|
-
if
|
|
146
|
-
os.environ["MODAL_PROXY_URL"] =
|
|
30
|
+
if early_args.proxy_url:
|
|
31
|
+
os.environ["MODAL_PROXY_URL"] = early_args.proxy_url
|
|
147
32
|
|
|
148
|
-
if
|
|
149
|
-
os.environ["MODAL_PROXY_API_KEY"] =
|
|
33
|
+
if early_args.proxy_api_key:
|
|
34
|
+
os.environ["MODAL_PROXY_API_KEY"] = early_args.proxy_api_key
|
|
150
35
|
|
|
151
36
|
# Import the fetch_modal_tokens module
|
|
152
|
-
# print("🔄 Fetching tokens from proxy server...")
|
|
153
37
|
from fetch_modal_tokens import get_tokens
|
|
154
38
|
token_id, token_secret, openai_api_key, anthropic_api_key, openrouter_api_key, groq_api_key = get_tokens()
|
|
155
39
|
|
|
@@ -157,8 +41,6 @@ token_id, token_secret, openai_api_key, anthropic_api_key, openrouter_api_key, g
|
|
|
157
41
|
if token_id is None or token_secret is None:
|
|
158
42
|
raise ValueError("Could not get valid tokens")
|
|
159
43
|
|
|
160
|
-
# print(f"✅ Tokens fetched successfully")
|
|
161
|
-
|
|
162
44
|
# Explicitly set the environment variables again to be sure
|
|
163
45
|
os.environ["MODAL_TOKEN_ID"] = token_id
|
|
164
46
|
os.environ["MODAL_TOKEN_SECRET"] = token_secret
|
|
@@ -198,9 +80,9 @@ def get_stored_credentials():
|
|
|
198
80
|
return {}
|
|
199
81
|
|
|
200
82
|
|
|
201
|
-
#
|
|
83
|
+
# Create Modal SSH container with GPU support and intelligent repository setup using Agent
|
|
202
84
|
def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_commands=None,
|
|
203
|
-
volume_name=None, timeout_minutes=60, ssh_password=None, interactive=False, gpu_count=1):
|
|
85
|
+
volume_name=None, timeout_minutes=60, ssh_password=None, interactive=False, gpu_count=1, use_cuda_base=False):
|
|
204
86
|
"""Create a Modal SSH container with GPU support and intelligent repository setup.
|
|
205
87
|
|
|
206
88
|
When repo_url is provided, uses Agent for intelligent repository setup.
|
|
@@ -355,47 +237,54 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
355
237
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
356
238
|
# Get the gitarsenal-cli root directory for kill_claude files
|
|
357
239
|
gitarsenal_root = os.path.dirname(current_dir)
|
|
358
|
-
# print(f"🔍 Current directory for mounting: {current_dir}")
|
|
359
240
|
|
|
360
|
-
#
|
|
241
|
+
# Choose base image to avoid CUDA segfault issues
|
|
242
|
+
print("⚠️ Using CUDA base image - this may cause segfaults on some systems")
|
|
243
|
+
# base_image = modal.Image.from_registry("nvidia/cuda:12.4.0-runtime-ubuntu22.04", add_python="3.11")
|
|
244
|
+
base_image = modal.Image.debian_slim()
|
|
245
|
+
pip_cmd = "uv_pip_install"
|
|
246
|
+
|
|
247
|
+
# Build the SSH image with the chosen base
|
|
361
248
|
ssh_image = (
|
|
362
|
-
|
|
363
|
-
modal.Image.debian_slim()
|
|
249
|
+
base_image
|
|
364
250
|
.apt_install(
|
|
365
251
|
"openssh-server", "sudo", "curl", "wget", "vim", "htop", "git",
|
|
366
252
|
"python3", "python3-pip", "build-essential", "tmux", "screen", "nano",
|
|
367
253
|
"gpg", "ca-certificates", "software-properties-common"
|
|
368
254
|
)
|
|
369
|
-
.uv_pip_install("uv", "modal", "gitingest", "requests", "openai", "anthropic", "exa-py") # Remove problematic CUDA packages
|
|
370
|
-
.run_commands(
|
|
371
|
-
# Create SSH directory
|
|
372
|
-
"mkdir -p /var/run/sshd",
|
|
373
|
-
"mkdir -p /root/.ssh",
|
|
374
|
-
"chmod 700 /root/.ssh",
|
|
375
|
-
|
|
376
|
-
# Configure SSH server
|
|
377
|
-
"sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
|
|
378
|
-
"sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
|
|
379
|
-
"sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
|
|
380
|
-
|
|
381
|
-
# SSH keep-alive settings
|
|
382
|
-
"echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
|
|
383
|
-
"echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
|
|
384
|
-
|
|
385
|
-
# Generate SSH host keys
|
|
386
|
-
"ssh-keygen -A",
|
|
387
|
-
|
|
388
|
-
# Set up a nice bash prompt
|
|
389
|
-
"echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
|
|
390
|
-
|
|
391
|
-
# Create base directories (subdirectories will be created automatically when mounting)
|
|
392
|
-
"mkdir -p /python",
|
|
393
|
-
)
|
|
394
|
-
# Mount entire directories instead of individual files
|
|
395
|
-
.add_local_dir(current_dir, "/python", ignore=lambda p: not p.name.endswith('.py')) # Mount all Python files from current directory
|
|
396
|
-
.add_local_dir(os.path.join(gitarsenal_root, "kill_claude"), "/python/kill_claude") # Mount entire kill_claude directory with all subdirectories
|
|
397
|
-
|
|
398
255
|
)
|
|
256
|
+
|
|
257
|
+
# Add Python packages using the appropriate method
|
|
258
|
+
if pip_cmd == "uv_pip_install":
|
|
259
|
+
ssh_image = ssh_image.uv_pip_install("uv", "modal", "gitingest", "requests", "openai", "anthropic", "exa-py")
|
|
260
|
+
else:
|
|
261
|
+
ssh_image = ssh_image.pip_install("modal", "gitingest", "requests", "openai", "anthropic", "exa-py")
|
|
262
|
+
|
|
263
|
+
# Add the rest of the configuration
|
|
264
|
+
ssh_image = ssh_image.run_commands(
|
|
265
|
+
# Create SSH directory
|
|
266
|
+
"mkdir -p /var/run/sshd",
|
|
267
|
+
"mkdir -p /root/.ssh",
|
|
268
|
+
"chmod 700 /root/.ssh",
|
|
269
|
+
|
|
270
|
+
# Configure SSH server
|
|
271
|
+
"sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config",
|
|
272
|
+
"sed -i 's/#PasswordAuthentication yes/PasswordAuthentication yes/' /etc/ssh/sshd_config",
|
|
273
|
+
"sed -i 's/#PubkeyAuthentication yes/PubkeyAuthentication yes/' /etc/ssh/sshd_config",
|
|
274
|
+
|
|
275
|
+
# SSH keep-alive settings
|
|
276
|
+
"echo 'ClientAliveInterval 60' >> /etc/ssh/sshd_config",
|
|
277
|
+
"echo 'ClientAliveCountMax 3' >> /etc/ssh/sshd_config",
|
|
278
|
+
|
|
279
|
+
# Generate SSH host keys
|
|
280
|
+
"ssh-keygen -A",
|
|
281
|
+
|
|
282
|
+
# Set up a nice bash prompt
|
|
283
|
+
"echo 'export PS1=\"\\[\\e[1;32m\\]modal:\\[\\e[1;34m\\]\\w\\[\\e[0m\\]$ \"' >> /root/.bashrc",
|
|
284
|
+
|
|
285
|
+
# Create base directories (subdirectories will be created automatically when mounting)
|
|
286
|
+
"mkdir -p /python",
|
|
287
|
+
).add_local_dir(current_dir, "/python", ignore=lambda p: not p.name.endswith('.py')).add_local_dir(os.path.join(gitarsenal_root, "kill_claude"), "/python/kill_claude")
|
|
399
288
|
print("✅ SSH image built successfully")
|
|
400
289
|
|
|
401
290
|
# Configure volumes if available
|
|
@@ -428,28 +317,11 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
428
317
|
import sys
|
|
429
318
|
|
|
430
319
|
# Add the mounted python directory to the Python path
|
|
431
|
-
sys.path.insert(0, "/python")
|
|
320
|
+
# sys.path.insert(0, "/python")
|
|
432
321
|
|
|
433
|
-
# Import the
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
from shell import PersistentShell
|
|
437
|
-
from llm_debugging import get_stored_credentials, generate_auth_context, call_llm_for_debug, call_llm_for_batch_debug, call_anthropic_for_debug, call_openai_for_debug, call_openai_for_batch_debug, call_anthropic_for_batch_debug, call_openrouter_for_debug, call_openrouter_for_batch_debug, get_current_debug_model
|
|
438
|
-
|
|
439
|
-
print("✅ Successfully imported CommandListManager, PersistentShell, and all llm_debugging functions from mounted modules")
|
|
440
|
-
except ImportError as e:
|
|
441
|
-
print(f"❌ Failed to import modules from mounted directory: {e}")
|
|
442
|
-
print("🔍 Available files in /python:")
|
|
443
|
-
try:
|
|
444
|
-
import os
|
|
445
|
-
if os.path.exists("/python"):
|
|
446
|
-
for file in os.listdir("/python"):
|
|
447
|
-
print(f" - {file}")
|
|
448
|
-
else:
|
|
449
|
-
print(" /python directory does not exist")
|
|
450
|
-
except Exception as list_error:
|
|
451
|
-
print(f" Error listing files: {list_error}")
|
|
452
|
-
raise
|
|
322
|
+
# Import only the modules we actually need (none currently for Agent-based approach)
|
|
323
|
+
# Note: CommandListManager and llm_debugging functions are not used in the Agent-based approach
|
|
324
|
+
print("✅ Container setup complete - using Agent-based repository setup")
|
|
453
325
|
|
|
454
326
|
# Set root password
|
|
455
327
|
subprocess.run(["bash", "-c", f"echo 'root:{ssh_password}' | chpasswd"], check=True)
|
|
@@ -457,7 +329,6 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
457
329
|
# Set OpenAI API key if provided
|
|
458
330
|
if openai_api_key:
|
|
459
331
|
os.environ['OPENAI_API_KEY'] = openai_api_key
|
|
460
|
-
# print(f"✅ Set OpenAI API key in container environment (length: {len(openai_api_key)})")
|
|
461
332
|
else:
|
|
462
333
|
print("⚠️ No OpenAI API key provided to container")
|
|
463
334
|
|
|
@@ -637,8 +508,6 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
637
508
|
process.kill()
|
|
638
509
|
process.wait()
|
|
639
510
|
except Exception as stream_error:
|
|
640
|
-
# print(f"\n⚠️ Error with advanced streaming: {stream_error}")
|
|
641
|
-
# print("🔄 Falling back to simple streaming...")
|
|
642
511
|
pass
|
|
643
512
|
|
|
644
513
|
# Fallback to simple readline approach
|
|
@@ -724,11 +593,9 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
724
593
|
with app.run():
|
|
725
594
|
# Get the API key from environment
|
|
726
595
|
api_key = os.environ.get("OPENAI_API_KEY")
|
|
727
|
-
# print(f"🔐 API key: {api_key}")
|
|
728
596
|
|
|
729
597
|
# Get stored credentials from local file
|
|
730
598
|
stored_credentials = get_stored_credentials()
|
|
731
|
-
# print(f"🔐 Stored credentials: {stored_credentials}")
|
|
732
599
|
if stored_credentials:
|
|
733
600
|
print(f"🔐 Found {len(stored_credentials)} stored credentials to send to container")
|
|
734
601
|
else:
|
|
@@ -748,393 +615,6 @@ def create_modal_ssh_container(gpu_type, repo_url=None, repo_name=None, setup_co
|
|
|
748
615
|
print(f"❌ Error running container: {e}")
|
|
749
616
|
return None
|
|
750
617
|
|
|
751
|
-
def fetch_setup_commands_from_api(repo_url):
|
|
752
|
-
"""Fetch setup commands from the GitIngest API using real repository analysis."""
|
|
753
|
-
import tempfile
|
|
754
|
-
import subprocess
|
|
755
|
-
import os
|
|
756
|
-
import shutil
|
|
757
|
-
import json
|
|
758
|
-
import time
|
|
759
|
-
import requests
|
|
760
|
-
|
|
761
|
-
# Define API endpoints to try in order - using only online endpoints
|
|
762
|
-
api_endpoints = [
|
|
763
|
-
"https://www.gitarsenal.dev/api/analyze-with-gitingest" # Working endpoint with www prefix
|
|
764
|
-
]
|
|
765
|
-
|
|
766
|
-
print(f"🔍 Fetching setup commands from API for repository: {repo_url}")
|
|
767
|
-
|
|
768
|
-
# Check if gitingest command line tool is available - try multiple possible command names
|
|
769
|
-
has_gitingest_cli = False
|
|
770
|
-
gitingest_cmd_name = None
|
|
771
|
-
|
|
772
|
-
# Try the standard command name first
|
|
773
|
-
try:
|
|
774
|
-
print(f"🔍 Checking for GitIngest CLI tool...")
|
|
775
|
-
result = subprocess.run(["gitingest", "--help"], check=True, capture_output=True, text=True)
|
|
776
|
-
has_gitingest_cli = True
|
|
777
|
-
gitingest_cmd_name = "gitingest"
|
|
778
|
-
print(f"✅ GitIngest CLI tool found")
|
|
779
|
-
except (subprocess.SubprocessError, FileNotFoundError) as e:
|
|
780
|
-
print(f" - GitIngest command not found: {str(e)}")
|
|
781
|
-
|
|
782
|
-
# Create a temporary directory for output
|
|
783
|
-
temp_dir = tempfile.mkdtemp(prefix="repo_analysis_")
|
|
784
|
-
output_file = os.path.join(temp_dir, "digest.json")
|
|
785
|
-
|
|
786
|
-
# Create a directory to save GitIngest results
|
|
787
|
-
save_dir = os.path.join(os.path.expanduser("~"), "gitarsenal_results")
|
|
788
|
-
os.makedirs(save_dir, exist_ok=True)
|
|
789
|
-
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
|
790
|
-
repo_name = repo_url.split("/")[-1].replace(".git", "")
|
|
791
|
-
save_file = os.path.join(save_dir, f"gitingest_{repo_name}_{timestamp}.txt")
|
|
792
|
-
|
|
793
|
-
try:
|
|
794
|
-
if has_gitingest_cli:
|
|
795
|
-
# Use gitingest CLI tool to analyze the repository directly from URL
|
|
796
|
-
print(f"🔎 Running GitIngest analysis on {repo_url}...")
|
|
797
|
-
|
|
798
|
-
# Based on the help output, the correct format is:
|
|
799
|
-
# gitingest [OPTIONS] [SOURCE]
|
|
800
|
-
# With options:
|
|
801
|
-
# -o, --output TEXT Output file path
|
|
802
|
-
# --format TEXT Output format (json)
|
|
803
|
-
|
|
804
|
-
# Run gitingest command with proper parameters
|
|
805
|
-
gitingest_run_cmd = [
|
|
806
|
-
gitingest_cmd_name,
|
|
807
|
-
repo_url,
|
|
808
|
-
"-o", output_file, # Use -o for output file
|
|
809
|
-
]
|
|
810
|
-
|
|
811
|
-
print(f"🔄 Executing: {' '.join(gitingest_run_cmd)}")
|
|
812
|
-
|
|
813
|
-
result = subprocess.run(gitingest_run_cmd, capture_output=True, text=True)
|
|
814
|
-
|
|
815
|
-
if result.returncode != 0:
|
|
816
|
-
print(f"⚠️ GitIngest CLI failed with exit code {result.returncode}")
|
|
817
|
-
print(f"⚠️ Error output: {result.stderr}")
|
|
818
|
-
print("Falling back to basic analysis")
|
|
819
|
-
gitingest_data = generate_basic_repo_analysis_from_url(repo_url)
|
|
820
|
-
else:
|
|
821
|
-
print(f"✅ GitIngest analysis completed successfully")
|
|
822
|
-
|
|
823
|
-
# Read the output file - note that the default format might not be JSON
|
|
824
|
-
try:
|
|
825
|
-
# First try to parse as JSON
|
|
826
|
-
try:
|
|
827
|
-
with open(output_file, 'r', encoding='utf-8') as f:
|
|
828
|
-
content = f.read()
|
|
829
|
-
|
|
830
|
-
# Save the GitIngest output to the results directory
|
|
831
|
-
with open(save_file, 'w', encoding='utf-8') as save_f:
|
|
832
|
-
save_f.write(content)
|
|
833
|
-
print(f"📁 GitIngest output saved to: {save_file}")
|
|
834
|
-
|
|
835
|
-
try:
|
|
836
|
-
gitingest_data = json.loads(content)
|
|
837
|
-
print(f"✅ GitIngest data loaded as JSON from {output_file}")
|
|
838
|
-
except json.JSONDecodeError:
|
|
839
|
-
# If not JSON, convert the text output to a basic structure
|
|
840
|
-
print(f"⚠️ GitIngest output is not in JSON format, converting text to structure")
|
|
841
|
-
|
|
842
|
-
# Process the text to extract useful information
|
|
843
|
-
import re
|
|
844
|
-
|
|
845
|
-
# Try to identify language
|
|
846
|
-
language_match = re.search(r"(?i)language[s]?:?\s*(\w+)", content)
|
|
847
|
-
detected_language = language_match.group(1) if language_match else "Unknown"
|
|
848
|
-
|
|
849
|
-
# Try to identify technologies with stronger evidence requirements
|
|
850
|
-
tech_patterns = {
|
|
851
|
-
"python": r"(?i)(python|\.py\b|pip\b|requirements\.txt|setup\.py)",
|
|
852
|
-
"javascript": r"(?i)(javascript|\.js\b|node|npm|yarn|package\.json)",
|
|
853
|
-
"typescript": r"(?i)(typescript|\.ts\b|tsc\b|tsconfig\.json)",
|
|
854
|
-
"go": r"(?i)(\bgo\b|golang|\.go\b|go\.mod|go\.sum)",
|
|
855
|
-
"rust": r"(?i)(rust|\.rs\b|cargo|Cargo\.toml)",
|
|
856
|
-
"java": r"(?i)(java\b|\.java\b|maven|gradle|pom\.xml)",
|
|
857
|
-
"c++": r"(?i)(c\+\+|\.cpp\b|\.hpp\b|cmake\b|CMakeLists\.txt)",
|
|
858
|
-
"pytorch": r"(?i)(pytorch|torch\b|nn\.Module)",
|
|
859
|
-
"tensorflow": r"(?i)(tensorflow|tf\.|keras\b)",
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
# Count occurrences to filter out false positives
|
|
863
|
-
tech_counts = {}
|
|
864
|
-
for tech, pattern in tech_patterns.items():
|
|
865
|
-
matches = re.findall(pattern, content)
|
|
866
|
-
if matches:
|
|
867
|
-
tech_counts[tech] = len(matches)
|
|
868
|
-
|
|
869
|
-
# Filter technologies based on threshold
|
|
870
|
-
thresholds = {
|
|
871
|
-
"javascript": 3, # Higher threshold for JavaScript
|
|
872
|
-
"go": 3, # Higher threshold for Go
|
|
873
|
-
"default": 2 # Default threshold
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
detected_technologies = []
|
|
877
|
-
for tech, count in tech_counts.items():
|
|
878
|
-
threshold = thresholds.get(tech, thresholds["default"])
|
|
879
|
-
if count >= threshold:
|
|
880
|
-
detected_technologies.append(tech)
|
|
881
|
-
print(f"📊 Detected {tech} with confidence score {count}")
|
|
882
|
-
|
|
883
|
-
# Create a structured representation
|
|
884
|
-
gitingest_data = {
|
|
885
|
-
"system_info": {
|
|
886
|
-
"detected_language": detected_language,
|
|
887
|
-
"detected_technologies": detected_technologies,
|
|
888
|
-
},
|
|
889
|
-
"repository_analysis": {
|
|
890
|
-
"summary": content[:5000], # First 5000 chars as summary
|
|
891
|
-
"content_preview": content[:10000] # First 10000 chars as preview
|
|
892
|
-
},
|
|
893
|
-
"success": True
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
# Save the processed data
|
|
897
|
-
processed_file = os.path.join(save_dir, f"gitingest_processed_{repo_name}_{timestamp}.json")
|
|
898
|
-
with open(processed_file, 'w', encoding='utf-8') as proc_f:
|
|
899
|
-
json.dump(gitingest_data, proc_f, indent=2)
|
|
900
|
-
print(f"📁 Processed GitIngest data saved to: {processed_file}")
|
|
901
|
-
except FileNotFoundError:
|
|
902
|
-
print(f"⚠️ Output file not found at {output_file}")
|
|
903
|
-
gitingest_data = generate_basic_repo_analysis_from_url(repo_url)
|
|
904
|
-
except Exception as e:
|
|
905
|
-
print(f"⚠️ Error reading GitIngest output: {e}")
|
|
906
|
-
gitingest_data = generate_basic_repo_analysis_from_url(repo_url)
|
|
907
|
-
else:
|
|
908
|
-
# Fall back to basic analysis if gitingest CLI is not available
|
|
909
|
-
gitingest_data = generate_basic_repo_analysis_from_url(repo_url)
|
|
910
|
-
|
|
911
|
-
# Prepare the request payload with GitIngest data
|
|
912
|
-
payload = {
|
|
913
|
-
"repoUrl": repo_url,
|
|
914
|
-
"gitingestData": gitingest_data,
|
|
915
|
-
"userRequest": "Setup and run the repository"
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
print(f"📤 API Request payload prepared (GitIngest data size: {len(json.dumps(gitingest_data))} bytes)")
|
|
919
|
-
|
|
920
|
-
# Try each endpoint in sequence until one succeeds
|
|
921
|
-
response = None
|
|
922
|
-
for api_url in api_endpoints:
|
|
923
|
-
# Use the retry mechanism for more reliable requests
|
|
924
|
-
response = make_api_request_with_retry(
|
|
925
|
-
url=api_url,
|
|
926
|
-
payload=payload,
|
|
927
|
-
max_retries=2,
|
|
928
|
-
timeout=180 # 3 minute timeout
|
|
929
|
-
)
|
|
930
|
-
|
|
931
|
-
# If we got a response and it's successful, break out of the loop
|
|
932
|
-
if response and response.status_code == 200:
|
|
933
|
-
print(f"✅ Successful response from {api_url}")
|
|
934
|
-
break
|
|
935
|
-
|
|
936
|
-
if response:
|
|
937
|
-
print(f"⚠️ Endpoint {api_url} returned status code {response.status_code}, trying next endpoint...")
|
|
938
|
-
else:
|
|
939
|
-
print(f"⚠️ Failed to connect to {api_url}, trying next endpoint...")
|
|
940
|
-
|
|
941
|
-
# If we've tried all endpoints and still don't have a response, use fallback
|
|
942
|
-
if response is None:
|
|
943
|
-
print("❌ All API endpoints failed")
|
|
944
|
-
return generate_fallback_commands(gitingest_data)
|
|
945
|
-
|
|
946
|
-
# Continue with the response we got from the successful endpoint
|
|
947
|
-
if not response:
|
|
948
|
-
print("❌ No valid response received from any endpoint")
|
|
949
|
-
return generate_fallback_commands(gitingest_data)
|
|
950
|
-
|
|
951
|
-
try:
|
|
952
|
-
print(f"📥 API Response status code: {response.status_code}")
|
|
953
|
-
|
|
954
|
-
if response.status_code == 200:
|
|
955
|
-
try:
|
|
956
|
-
data = response.json()
|
|
957
|
-
print(f"📄 API Response data received")
|
|
958
|
-
print(f"📄 Response size: {len(response.text)} bytes")
|
|
959
|
-
print(f"📄 Response URL: {response.url}")
|
|
960
|
-
|
|
961
|
-
# Extract setup commands from the response
|
|
962
|
-
if "setupInstructions" in data and "commands" in data["setupInstructions"]:
|
|
963
|
-
commands = data["setupInstructions"]["commands"]
|
|
964
|
-
print(f"✅ Successfully fetched {len(commands)} setup commands from API")
|
|
965
|
-
|
|
966
|
-
# Print the original commands for reference
|
|
967
|
-
print("📋 Original commands from API:")
|
|
968
|
-
for i, cmd in enumerate(commands, 1):
|
|
969
|
-
print(f" {i}. {cmd}")
|
|
970
|
-
|
|
971
|
-
# Fix the commands by removing placeholders and comments
|
|
972
|
-
fixed_commands = fix_setup_commands(commands)
|
|
973
|
-
|
|
974
|
-
# If we have a temp_dir with the cloned repo, try to find the entry point
|
|
975
|
-
# and replace any placeholder entry points
|
|
976
|
-
for i, cmd in enumerate(fixed_commands):
|
|
977
|
-
if "python main.py" in cmd or "python3 main.py" in cmd:
|
|
978
|
-
try:
|
|
979
|
-
entry_point = find_entry_point(temp_dir)
|
|
980
|
-
if entry_point and entry_point != "main.py":
|
|
981
|
-
fixed_commands[i] = cmd.replace("main.py", entry_point)
|
|
982
|
-
print(f"🔄 Replaced main.py with detected entry point: {entry_point}")
|
|
983
|
-
except Exception as e:
|
|
984
|
-
print(f"⚠️ Error finding entry point: {e}")
|
|
985
|
-
|
|
986
|
-
# Print the fixed commands
|
|
987
|
-
print("\n📋 Fixed commands:")
|
|
988
|
-
for i, cmd in enumerate(fixed_commands, 1):
|
|
989
|
-
print(f" {i}. {cmd}")
|
|
990
|
-
|
|
991
|
-
return fixed_commands
|
|
992
|
-
else:
|
|
993
|
-
print("⚠️ API response did not contain setupInstructions.commands field")
|
|
994
|
-
print("📋 Available fields in response:")
|
|
995
|
-
for key in data.keys():
|
|
996
|
-
print(f" - {key}")
|
|
997
|
-
# Return fallback commands
|
|
998
|
-
return generate_fallback_commands(gitingest_data)
|
|
999
|
-
except json.JSONDecodeError as e:
|
|
1000
|
-
print(f"❌ Failed to parse API response as JSON: {e}")
|
|
1001
|
-
print(f"Raw response: {response.text[:500]}...")
|
|
1002
|
-
# Return fallback commands
|
|
1003
|
-
return generate_fallback_commands(gitingest_data)
|
|
1004
|
-
elif response.status_code == 504:
|
|
1005
|
-
print(f"❌ API request timed out (504 Gateway Timeout)")
|
|
1006
|
-
print("⚠️ The server took too long to respond. Using fallback commands instead.")
|
|
1007
|
-
# Return fallback commands
|
|
1008
|
-
return generate_fallback_commands(gitingest_data)
|
|
1009
|
-
else:
|
|
1010
|
-
print(f"❌ API request failed with status code: {response.status_code}")
|
|
1011
|
-
print(f"❌ Response URL: {response.url}")
|
|
1012
|
-
print(f"❌ Response headers: {dict(response.headers)}")
|
|
1013
|
-
print(f"❌ Error response: {response.text[:500]}...")
|
|
1014
|
-
# Return fallback commands
|
|
1015
|
-
return generate_fallback_commands(gitingest_data)
|
|
1016
|
-
except Exception as e:
|
|
1017
|
-
print(f"❌ Error processing API response: {str(e)}")
|
|
1018
|
-
print("⚠️ Using fallback commands instead")
|
|
1019
|
-
# Return fallback commands
|
|
1020
|
-
return generate_fallback_commands(gitingest_data)
|
|
1021
|
-
except Exception as e:
|
|
1022
|
-
print(f"❌ Error fetching setup commands from API: {e}")
|
|
1023
|
-
import traceback
|
|
1024
|
-
traceback.print_exc()
|
|
1025
|
-
# Return fallback commands
|
|
1026
|
-
return generate_fallback_commands(None)
|
|
1027
|
-
finally:
|
|
1028
|
-
# Clean up the temporary directory
|
|
1029
|
-
print(f"🧹 Cleaning up temporary directory...")
|
|
1030
|
-
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
1031
|
-
|
|
1032
|
-
def generate_fallback_commands(gitingest_data):
|
|
1033
|
-
return True
|
|
1034
|
-
|
|
1035
|
-
def generate_basic_repo_analysis_from_url(repo_url):
|
|
1036
|
-
"""Generate basic repository analysis data from a repository URL."""
|
|
1037
|
-
import tempfile
|
|
1038
|
-
import subprocess
|
|
1039
|
-
import os
|
|
1040
|
-
import shutil
|
|
1041
|
-
|
|
1042
|
-
# Create a temporary directory for cloning
|
|
1043
|
-
temp_dir = tempfile.mkdtemp(prefix="repo_basic_analysis_")
|
|
1044
|
-
|
|
1045
|
-
try:
|
|
1046
|
-
print(f"📥 Cloning repository to {temp_dir} for basic analysis...")
|
|
1047
|
-
clone_result = subprocess.run(
|
|
1048
|
-
["git", "clone", "--depth", "1", repo_url, temp_dir],
|
|
1049
|
-
capture_output=True,
|
|
1050
|
-
text=True
|
|
1051
|
-
)
|
|
1052
|
-
|
|
1053
|
-
if clone_result.returncode != 0:
|
|
1054
|
-
print(f"❌ Failed to clone repository: {clone_result.stderr}")
|
|
1055
|
-
return {
|
|
1056
|
-
"system_info": {
|
|
1057
|
-
"platform": "linux",
|
|
1058
|
-
"python_version": "3.10",
|
|
1059
|
-
"detected_language": "Unknown",
|
|
1060
|
-
"detected_technologies": [],
|
|
1061
|
-
"file_count": 0,
|
|
1062
|
-
"repo_stars": 0,
|
|
1063
|
-
"repo_forks": 0,
|
|
1064
|
-
"primary_package_manager": "Unknown",
|
|
1065
|
-
"complexity_level": "low"
|
|
1066
|
-
},
|
|
1067
|
-
"repository_analysis": {
|
|
1068
|
-
"summary": f"Repository analysis for {repo_url}",
|
|
1069
|
-
"tree": "Failed to clone repository",
|
|
1070
|
-
"content_preview": "No content available"
|
|
1071
|
-
},
|
|
1072
|
-
"success": False
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
print(f"✅ Repository cloned successfully for basic analysis")
|
|
1076
|
-
|
|
1077
|
-
# Use the existing generate_basic_repo_analysis function
|
|
1078
|
-
return generate_basic_repo_analysis(temp_dir)
|
|
1079
|
-
finally:
|
|
1080
|
-
# Clean up the temporary directory
|
|
1081
|
-
print(f"🧹 Cleaning up temporary directory for basic analysis...")
|
|
1082
|
-
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
1083
|
-
|
|
1084
|
-
def generate_basic_repo_analysis(repo_dir):
|
|
1085
|
-
return True
|
|
1086
|
-
|
|
1087
|
-
def fix_setup_commands(commands):
|
|
1088
|
-
"""Fix setup commands by removing placeholders and comments."""
|
|
1089
|
-
fixed_commands = []
|
|
1090
|
-
|
|
1091
|
-
for cmd in commands:
|
|
1092
|
-
# Remove placeholders like "(or the appropriate entry point...)"
|
|
1093
|
-
cmd = re.sub(r'\([^)]*\)', '', cmd).strip()
|
|
1094
|
-
|
|
1095
|
-
# Skip empty commands or pure comments
|
|
1096
|
-
if not cmd or cmd.startswith('#'):
|
|
1097
|
-
continue
|
|
1098
|
-
|
|
1099
|
-
# Remove trailing comments
|
|
1100
|
-
cmd = re.sub(r'#.*$', '', cmd).strip()
|
|
1101
|
-
|
|
1102
|
-
if cmd:
|
|
1103
|
-
fixed_commands.append(cmd)
|
|
1104
|
-
|
|
1105
|
-
return fixed_commands
|
|
1106
|
-
|
|
1107
|
-
def find_entry_point(repo_dir):
|
|
1108
|
-
"""Find the entry point script for a repository."""
|
|
1109
|
-
# Common entry point files to check
|
|
1110
|
-
common_entry_points = [
|
|
1111
|
-
"main.py", "app.py", "run.py", "train.py", "start.py",
|
|
1112
|
-
"server.py", "cli.py", "demo.py", "example.py"
|
|
1113
|
-
]
|
|
1114
|
-
|
|
1115
|
-
# Check if any of the common entry points exist
|
|
1116
|
-
for entry_point in common_entry_points:
|
|
1117
|
-
if os.path.exists(os.path.join(repo_dir, entry_point)):
|
|
1118
|
-
return entry_point
|
|
1119
|
-
|
|
1120
|
-
# Look for Python files in the root directory
|
|
1121
|
-
python_files = [f for f in os.listdir(repo_dir) if f.endswith('.py')]
|
|
1122
|
-
if python_files:
|
|
1123
|
-
# Prioritize files with main function or if_name_main pattern
|
|
1124
|
-
for py_file in python_files:
|
|
1125
|
-
file_path = os.path.join(repo_dir, py_file)
|
|
1126
|
-
try:
|
|
1127
|
-
with open(file_path, 'r') as f:
|
|
1128
|
-
content = f.read()
|
|
1129
|
-
if "def main" in content or "if __name__ == '__main__'" in content or 'if __name__ == "__main__"' in content:
|
|
1130
|
-
return py_file
|
|
1131
|
-
except:
|
|
1132
|
-
pass
|
|
1133
|
-
|
|
1134
|
-
# If no main function found, return the first Python file
|
|
1135
|
-
return python_files[0]
|
|
1136
|
-
|
|
1137
|
-
return None
|
|
1138
618
|
|
|
1139
619
|
def cleanup_security_tokens():
|
|
1140
620
|
"""Delete all security tokens and API keys after SSH container is started"""
|
|
@@ -1146,27 +626,22 @@ def cleanup_security_tokens():
|
|
|
1146
626
|
for var in modal_env_vars:
|
|
1147
627
|
if var in os.environ:
|
|
1148
628
|
del os.environ[var]
|
|
1149
|
-
# print(f"✅ Removed {var} from environment")
|
|
1150
629
|
|
|
1151
630
|
# Remove OpenAI API key from environment
|
|
1152
631
|
if "OPENAI_API_KEY" in os.environ:
|
|
1153
632
|
del os.environ["OPENAI_API_KEY"]
|
|
1154
|
-
# print("✅ Removed OpenAI API key from environment")
|
|
1155
633
|
|
|
1156
634
|
# Delete ~/.modal.toml file
|
|
1157
635
|
home_dir = os.path.expanduser("~")
|
|
1158
636
|
modal_toml = os.path.join(home_dir, ".modal.toml")
|
|
1159
637
|
if os.path.exists(modal_toml):
|
|
1160
638
|
os.remove(modal_toml)
|
|
1161
|
-
# print(f"✅ Deleted Modal token file at {modal_toml}")
|
|
1162
639
|
|
|
1163
640
|
# Delete ~/.gitarsenal/openai_key file
|
|
1164
641
|
openai_key_file = os.path.join(home_dir, ".gitarsenal", "openai_key")
|
|
1165
642
|
if os.path.exists(openai_key_file):
|
|
1166
643
|
os.remove(openai_key_file)
|
|
1167
|
-
# print(f"✅ Deleted OpenAI API key file at {openai_key_file}")
|
|
1168
644
|
|
|
1169
|
-
# print("✅ Security cleanup completed successfully")
|
|
1170
645
|
except Exception as e:
|
|
1171
646
|
print(f"❌ Error during security cleanup: {e}")
|
|
1172
647
|
|
|
@@ -1175,6 +650,7 @@ def cleanup_modal_token():
|
|
|
1175
650
|
"""Legacy function - now calls the comprehensive cleanup"""
|
|
1176
651
|
cleanup_security_tokens()
|
|
1177
652
|
|
|
653
|
+
|
|
1178
654
|
def show_usage_examples():
|
|
1179
655
|
"""Display usage examples for the script."""
|
|
1180
656
|
print("Usage Examples\n")
|
|
@@ -1254,497 +730,6 @@ def show_usage_examples():
|
|
|
1254
730
|
print(" # Manual setup (advanced users):")
|
|
1255
731
|
print(" gitarsenal --gpu A10G --setup-commands \"pip install torch\" \"python train.py\"")
|
|
1256
732
|
|
|
1257
|
-
def make_api_request_with_retry(url, payload, max_retries=2, timeout=180):
|
|
1258
|
-
"""Make an API request with retry mechanism."""
|
|
1259
|
-
import requests
|
|
1260
|
-
import time
|
|
1261
|
-
|
|
1262
|
-
for attempt in range(max_retries + 1):
|
|
1263
|
-
try:
|
|
1264
|
-
if attempt > 0:
|
|
1265
|
-
print(f"🔄 Retry attempt {attempt}/{max_retries}...")
|
|
1266
|
-
|
|
1267
|
-
# print(f"🌐 Making POST request to: {url}")
|
|
1268
|
-
# print(f"⏳ Waiting up to {timeout//60} minutes for response...")
|
|
1269
|
-
|
|
1270
|
-
# Set allow_redirects=True to follow redirects automatically
|
|
1271
|
-
response = requests.post(
|
|
1272
|
-
url,
|
|
1273
|
-
json=payload,
|
|
1274
|
-
timeout=timeout,
|
|
1275
|
-
allow_redirects=True,
|
|
1276
|
-
headers={
|
|
1277
|
-
'Content-Type': 'application/json',
|
|
1278
|
-
'User-Agent': 'GitArsenal-CLI/1.0'
|
|
1279
|
-
}
|
|
1280
|
-
)
|
|
1281
|
-
|
|
1282
|
-
# Print redirect info if any
|
|
1283
|
-
if response.history:
|
|
1284
|
-
print(f"✅ Request was redirected {len(response.history)} times")
|
|
1285
|
-
for resp in response.history:
|
|
1286
|
-
print(f" - Redirect: {resp.status_code} from {resp.url}")
|
|
1287
|
-
print(f"✅ Final URL: {response.url}")
|
|
1288
|
-
|
|
1289
|
-
return response
|
|
1290
|
-
except requests.exceptions.RequestException as e:
|
|
1291
|
-
if attempt < max_retries:
|
|
1292
|
-
retry_delay = 2 ** attempt # Exponential backoff
|
|
1293
|
-
print(f"⚠️ Request failed: {str(e)}")
|
|
1294
|
-
print(f"⏳ Waiting {retry_delay} seconds before retrying...")
|
|
1295
|
-
time.sleep(retry_delay)
|
|
1296
|
-
else:
|
|
1297
|
-
print(f"❌ All retry attempts failed: {str(e)}")
|
|
1298
|
-
return None
|
|
1299
|
-
|
|
1300
|
-
return None
|
|
1301
|
-
|
|
1302
|
-
def get_setup_commands_from_gitingest(repo_url):
|
|
1303
|
-
"""
|
|
1304
|
-
Get repository setup commands using the gitingest approach.
|
|
1305
|
-
|
|
1306
|
-
This function is inspired by gitingest_setup_client.py and provides a more
|
|
1307
|
-
robust way to get setup commands for a repository.
|
|
1308
|
-
|
|
1309
|
-
Args:
|
|
1310
|
-
repo_url: URL of the repository to set up
|
|
1311
|
-
|
|
1312
|
-
Returns:
|
|
1313
|
-
List of setup commands or None if failed
|
|
1314
|
-
"""
|
|
1315
|
-
import requests
|
|
1316
|
-
import json
|
|
1317
|
-
import os
|
|
1318
|
-
import sys
|
|
1319
|
-
import tempfile
|
|
1320
|
-
import subprocess
|
|
1321
|
-
|
|
1322
|
-
print(f"🔍 Getting setup commands for repository: {repo_url}")
|
|
1323
|
-
|
|
1324
|
-
# Define API endpoints to try in order
|
|
1325
|
-
api_endpoints = [
|
|
1326
|
-
"https://www.gitarsenal.dev/api/gitingest-setup-commands",
|
|
1327
|
-
"https://gitarsenal.dev/api/gitingest-setup-commands",
|
|
1328
|
-
]
|
|
1329
|
-
|
|
1330
|
-
# Generate basic gitingest data
|
|
1331
|
-
def generate_basic_gitingest_data():
|
|
1332
|
-
# Extract repo name from URL
|
|
1333
|
-
repo_name = repo_url.split('/')[-1].replace('.git', '')
|
|
1334
|
-
|
|
1335
|
-
return {
|
|
1336
|
-
"system_info": {
|
|
1337
|
-
"platform": "Unknown",
|
|
1338
|
-
"python_version": "Unknown",
|
|
1339
|
-
"detected_language": "Unknown",
|
|
1340
|
-
"detected_technologies": [],
|
|
1341
|
-
"file_count": 0,
|
|
1342
|
-
"repo_stars": 0,
|
|
1343
|
-
"repo_forks": 0,
|
|
1344
|
-
"primary_package_manager": "Unknown",
|
|
1345
|
-
"complexity_level": "Unknown"
|
|
1346
|
-
},
|
|
1347
|
-
"repository_analysis": {
|
|
1348
|
-
"summary": f"Repository: {repo_name}",
|
|
1349
|
-
"tree": "",
|
|
1350
|
-
"content_preview": ""
|
|
1351
|
-
},
|
|
1352
|
-
"success": True
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
# Try to generate gitingest data using CLI if available
|
|
1356
|
-
def generate_gitingest_data_from_cli():
|
|
1357
|
-
try:
|
|
1358
|
-
# Check if gitingest CLI is available
|
|
1359
|
-
subprocess.run(["gitingest", "--help"], check=True, capture_output=True, text=True)
|
|
1360
|
-
|
|
1361
|
-
# Create a temporary file for the output
|
|
1362
|
-
with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as tmp:
|
|
1363
|
-
output_file = tmp.name
|
|
1364
|
-
|
|
1365
|
-
# Run gitingest command
|
|
1366
|
-
print(f"Running gitingest analysis on {repo_url}...")
|
|
1367
|
-
gitingest_cmd = ["gitingest", repo_url, "-o", output_file]
|
|
1368
|
-
result = subprocess.run(gitingest_cmd, capture_output=True, text=True)
|
|
1369
|
-
|
|
1370
|
-
if result.returncode != 0:
|
|
1371
|
-
print(f"GitIngest CLI failed: {result.stderr}")
|
|
1372
|
-
return None
|
|
1373
|
-
|
|
1374
|
-
# Read the output file
|
|
1375
|
-
try:
|
|
1376
|
-
with open(output_file, 'r', encoding='utf-8') as f:
|
|
1377
|
-
content = f.read()
|
|
1378
|
-
try:
|
|
1379
|
-
data = json.loads(content)
|
|
1380
|
-
return data
|
|
1381
|
-
except json.JSONDecodeError:
|
|
1382
|
-
# If not JSON, convert the text output to a basic structure
|
|
1383
|
-
return {
|
|
1384
|
-
"system_info": {
|
|
1385
|
-
"platform": "Unknown",
|
|
1386
|
-
"python_version": "Unknown",
|
|
1387
|
-
"detected_language": "Unknown",
|
|
1388
|
-
"detected_technologies": []
|
|
1389
|
-
},
|
|
1390
|
-
"repository_analysis": {
|
|
1391
|
-
"summary": content[:5000], # First 5000 chars as summary
|
|
1392
|
-
"tree": "",
|
|
1393
|
-
"content_preview": content[:10000] # First 10000 chars as preview
|
|
1394
|
-
},
|
|
1395
|
-
"success": True
|
|
1396
|
-
}
|
|
1397
|
-
except Exception as e:
|
|
1398
|
-
print(f"Error reading gitingest output: {e}")
|
|
1399
|
-
return None
|
|
1400
|
-
finally:
|
|
1401
|
-
# Clean up the temporary file
|
|
1402
|
-
if os.path.exists(output_file):
|
|
1403
|
-
os.unlink(output_file)
|
|
1404
|
-
|
|
1405
|
-
except (subprocess.SubprocessError, FileNotFoundError):
|
|
1406
|
-
print("GitIngest CLI not found")
|
|
1407
|
-
return None
|
|
1408
|
-
|
|
1409
|
-
# First try to get data from CLI
|
|
1410
|
-
gitingest_data = generate_gitingest_data_from_cli()
|
|
1411
|
-
|
|
1412
|
-
# If CLI failed, use basic data
|
|
1413
|
-
if not gitingest_data:
|
|
1414
|
-
print("Using basic gitingest data")
|
|
1415
|
-
gitingest_data = generate_basic_gitingest_data()
|
|
1416
|
-
|
|
1417
|
-
# Try each API endpoint
|
|
1418
|
-
for api_url in api_endpoints:
|
|
1419
|
-
try:
|
|
1420
|
-
# print(f"Trying API endpoint: {api_url}")
|
|
1421
|
-
|
|
1422
|
-
# Load stored credentials
|
|
1423
|
-
stored_credentials = get_stored_credentials()
|
|
1424
|
-
|
|
1425
|
-
payload = {
|
|
1426
|
-
"repoUrl": repo_url,
|
|
1427
|
-
"gitingestData": gitingest_data,
|
|
1428
|
-
"storedCredentials": stored_credentials, # Add back stored credentials
|
|
1429
|
-
"preview": False
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
# Use the retry mechanism for more reliable requests
|
|
1433
|
-
response = make_api_request_with_retry(
|
|
1434
|
-
url=api_url,
|
|
1435
|
-
payload=payload,
|
|
1436
|
-
max_retries=2,
|
|
1437
|
-
timeout=180, # 3 minute timeout
|
|
1438
|
-
)
|
|
1439
|
-
|
|
1440
|
-
if not response:
|
|
1441
|
-
print(f"Failed to connect to {api_url}")
|
|
1442
|
-
continue
|
|
1443
|
-
|
|
1444
|
-
if response.status_code != 200:
|
|
1445
|
-
print(f"API request failed with status code: {response.status_code}")
|
|
1446
|
-
continue
|
|
1447
|
-
|
|
1448
|
-
try:
|
|
1449
|
-
result = response.json()
|
|
1450
|
-
|
|
1451
|
-
# Check if we have commands in the response
|
|
1452
|
-
commands = None
|
|
1453
|
-
|
|
1454
|
-
# Check for different response formats
|
|
1455
|
-
if "commands" in result:
|
|
1456
|
-
commands = result["commands"]
|
|
1457
|
-
elif "setupInstructions" in result and "commands" in result["setupInstructions"]:
|
|
1458
|
-
commands = result["setupInstructions"]["commands"]
|
|
1459
|
-
|
|
1460
|
-
if commands:
|
|
1461
|
-
print(f"✅ Successfully fetched {len(commands)} setup commands from API at {api_url}")
|
|
1462
|
-
|
|
1463
|
-
# Enhanced response handling for API key detection
|
|
1464
|
-
if "requiredApiKeys" in result:
|
|
1465
|
-
api_keys = result.get("requiredApiKeys", [])
|
|
1466
|
-
if api_keys:
|
|
1467
|
-
print(f"\n🔑 Required API Keys ({len(api_keys)}):")
|
|
1468
|
-
# Load stored GitArsenal credentials
|
|
1469
|
-
stored_credentials = {}
|
|
1470
|
-
try:
|
|
1471
|
-
credentials_file = Path.home() / ".gitarsenal" / "credentials.json"
|
|
1472
|
-
if credentials_file.exists():
|
|
1473
|
-
with open(credentials_file, 'r') as f:
|
|
1474
|
-
stored_credentials = json.load(f)
|
|
1475
|
-
print(f"📋 Found {len(stored_credentials)} stored GitArsenal credentials")
|
|
1476
|
-
else:
|
|
1477
|
-
print("📋 No stored GitArsenal credentials found")
|
|
1478
|
-
except Exception as e:
|
|
1479
|
-
print(f"⚠️ Error loading stored credentials: {e}")
|
|
1480
|
-
|
|
1481
|
-
# Identify missing required API keys
|
|
1482
|
-
missing_required_keys = []
|
|
1483
|
-
available_keys = []
|
|
1484
|
-
|
|
1485
|
-
for i, api_key in enumerate(api_keys, 1):
|
|
1486
|
-
key_name = api_key.get('name', 'Unknown')
|
|
1487
|
-
is_required = api_key.get("required", False)
|
|
1488
|
-
has_stored_key = key_name in stored_credentials
|
|
1489
|
-
|
|
1490
|
-
if is_required:
|
|
1491
|
-
if has_stored_key:
|
|
1492
|
-
status = "✅ Required (Available)"
|
|
1493
|
-
available_keys.append(key_name)
|
|
1494
|
-
else:
|
|
1495
|
-
status = "🔴 Required (Missing)"
|
|
1496
|
-
missing_required_keys.append(api_key)
|
|
1497
|
-
else:
|
|
1498
|
-
status = "🟡 Optional"
|
|
1499
|
-
|
|
1500
|
-
print(f" {i}. {key_name} - {status}")
|
|
1501
|
-
print(f" Service: {api_key.get('service', 'Unknown')}")
|
|
1502
|
-
print(f" Description: {api_key.get('description', 'No description')}")
|
|
1503
|
-
if api_key.get('example'):
|
|
1504
|
-
print(f" Example: {api_key.get('example')}")
|
|
1505
|
-
if api_key.get('documentation_url'):
|
|
1506
|
-
print(f" Docs: {api_key.get('documentation_url')}")
|
|
1507
|
-
print()
|
|
1508
|
-
|
|
1509
|
-
# Prompt for missing required API keys
|
|
1510
|
-
if missing_required_keys:
|
|
1511
|
-
print("🔧 Setting up missing required API keys...")
|
|
1512
|
-
print("Press Enter to continue or Ctrl+C to skip...")
|
|
1513
|
-
|
|
1514
|
-
for api_key in missing_required_keys:
|
|
1515
|
-
key_name = api_key.get('name', 'Unknown')
|
|
1516
|
-
service = api_key.get('service', 'Unknown')
|
|
1517
|
-
description = api_key.get('description', 'No description')
|
|
1518
|
-
example = api_key.get('example', '')
|
|
1519
|
-
docs_url = api_key.get('documentation_url', '')
|
|
1520
|
-
|
|
1521
|
-
print(f"\n📝 Setting up {key_name} for {service}:")
|
|
1522
|
-
print(f" Description: {description}")
|
|
1523
|
-
if example:
|
|
1524
|
-
print(f" Example: {example}")
|
|
1525
|
-
if docs_url:
|
|
1526
|
-
print(f" Documentation: {docs_url}")
|
|
1527
|
-
|
|
1528
|
-
# Prompt user for the API key
|
|
1529
|
-
try:
|
|
1530
|
-
import getpass
|
|
1531
|
-
print(f"\nPlease enter your {key_name} for {service}:")
|
|
1532
|
-
new_key = getpass.getpass(f"{key_name} ({service}) API Key (hidden): ").strip()
|
|
1533
|
-
|
|
1534
|
-
if new_key:
|
|
1535
|
-
# Save to credentials file
|
|
1536
|
-
credentials_file = Path.home() / ".gitarsenal" / "credentials.json"
|
|
1537
|
-
credentials_file.parent.mkdir(parents=True, exist_ok=True)
|
|
1538
|
-
|
|
1539
|
-
# Load existing credentials
|
|
1540
|
-
if credentials_file.exists():
|
|
1541
|
-
with open(credentials_file, 'r') as f:
|
|
1542
|
-
all_credentials = json.load(f)
|
|
1543
|
-
else:
|
|
1544
|
-
all_credentials = {}
|
|
1545
|
-
|
|
1546
|
-
# Add new key
|
|
1547
|
-
all_credentials[key_name] = new_key
|
|
1548
|
-
|
|
1549
|
-
# Save back to file
|
|
1550
|
-
with open(credentials_file, 'w') as f:
|
|
1551
|
-
json.dump(all_credentials, f, indent=2)
|
|
1552
|
-
|
|
1553
|
-
print(f"✅ {key_name} saved successfully!")
|
|
1554
|
-
available_keys.append(key_name)
|
|
1555
|
-
else:
|
|
1556
|
-
print(f"⚠️ Skipping {key_name} (no input provided)")
|
|
1557
|
-
except KeyboardInterrupt:
|
|
1558
|
-
print(f"\n⚠️ Skipping {key_name} (cancelled by user)")
|
|
1559
|
-
except Exception as e:
|
|
1560
|
-
print(f"❌ Error saving {key_name}: {e}")
|
|
1561
|
-
|
|
1562
|
-
# Show summary
|
|
1563
|
-
if available_keys:
|
|
1564
|
-
print(f"✅ Available API keys: {', '.join(available_keys)}")
|
|
1565
|
-
if missing_required_keys:
|
|
1566
|
-
print(f"⚠️ Missing required keys: {', '.join([k.get('name') for k in missing_required_keys])}")
|
|
1567
|
-
else:
|
|
1568
|
-
print("ℹ️ All required API keys are already available.")
|
|
1569
|
-
|
|
1570
|
-
# Display setup complexity if available
|
|
1571
|
-
if "setupComplexity" in result:
|
|
1572
|
-
complexity = result.get("setupComplexity", "medium")
|
|
1573
|
-
estimated_time = result.get("estimatedSetupTime", "Unknown")
|
|
1574
|
-
print(f"📊 Setup Complexity: {complexity.upper()}")
|
|
1575
|
-
print(f"⏱️ Estimated Time: {estimated_time}")
|
|
1576
|
-
|
|
1577
|
-
# Print the commands
|
|
1578
|
-
print("\n📋 Setup Commands:")
|
|
1579
|
-
for i, cmd in enumerate(commands, 1):
|
|
1580
|
-
print(f" {i}. {cmd}")
|
|
1581
|
-
|
|
1582
|
-
# Fix the commands
|
|
1583
|
-
fixed_commands = fix_setup_commands(commands)
|
|
1584
|
-
|
|
1585
|
-
# Print the fixed commands
|
|
1586
|
-
print("\n📋 Fixed commands:")
|
|
1587
|
-
for i, cmd in enumerate(fixed_commands, 1):
|
|
1588
|
-
print(f" {i}. {cmd}")
|
|
1589
|
-
|
|
1590
|
-
return fixed_commands
|
|
1591
|
-
else:
|
|
1592
|
-
print("No commands found in API response")
|
|
1593
|
-
except json.JSONDecodeError:
|
|
1594
|
-
print(f"Failed to parse API response as JSON")
|
|
1595
|
-
except Exception as e:
|
|
1596
|
-
print(f"Error with API endpoint {api_url}: {e}")
|
|
1597
|
-
|
|
1598
|
-
print("❌ All API endpoints failed")
|
|
1599
|
-
return generate_fallback_commands(gitingest_data)
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
def preprocess_commands_with_llm(setup_commands, stored_credentials, api_key=None):
|
|
1606
|
-
"""
|
|
1607
|
-
Use LLM to preprocess setup commands and inject available credentials.
|
|
1608
|
-
|
|
1609
|
-
Args:
|
|
1610
|
-
setup_commands: List of setup commands
|
|
1611
|
-
stored_credentials: Dictionary of stored credentials
|
|
1612
|
-
api_key: OpenAI API key for LLM calls
|
|
1613
|
-
|
|
1614
|
-
Returns:
|
|
1615
|
-
List of processed commands with credentials injected
|
|
1616
|
-
"""
|
|
1617
|
-
if not setup_commands or not stored_credentials:
|
|
1618
|
-
return setup_commands
|
|
1619
|
-
|
|
1620
|
-
try:
|
|
1621
|
-
# Create context for the LLM
|
|
1622
|
-
credentials_info = "\n".join([f"- {key}: {value[:8]}..." for key, value in stored_credentials.items()])
|
|
1623
|
-
|
|
1624
|
-
prompt = f"""
|
|
1625
|
-
You are a command preprocessing assistant. Your task is to modify setup commands to use available credentials and make them non-interactive.
|
|
1626
|
-
|
|
1627
|
-
AVAILABLE CREDENTIALS:
|
|
1628
|
-
{credentials_info}
|
|
1629
|
-
|
|
1630
|
-
ORIGINAL COMMANDS:
|
|
1631
|
-
{chr(10).join([f"{i+1}. {cmd}" for i, cmd in enumerate(setup_commands)])}
|
|
1632
|
-
|
|
1633
|
-
INSTRUCTIONS:
|
|
1634
|
-
1. Replace any authentication commands with token-based versions using available credentials
|
|
1635
|
-
2. Make all commands non-interactive (add --yes, --no-input, -y flags where needed)
|
|
1636
|
-
3. Use environment variables or direct token injection where appropriate
|
|
1637
|
-
4. Skip commands that cannot be made non-interactive due to missing credentials
|
|
1638
|
-
5. Add any necessary environment variable exports
|
|
1639
|
-
|
|
1640
|
-
Return the modified commands as a JSON array of strings. If a command should be skipped, prefix it with "# SKIPPED: ".
|
|
1641
|
-
|
|
1642
|
-
Example transformations:
|
|
1643
|
-
- "huggingface-cli login" → "huggingface-cli login --token $HUGGINGFACE_TOKEN"
|
|
1644
|
-
- "npm install" → "npm install --yes"
|
|
1645
|
-
|
|
1646
|
-
Return only the JSON array, no other text.
|
|
1647
|
-
"""
|
|
1648
|
-
|
|
1649
|
-
if not api_key:
|
|
1650
|
-
print("⚠️ No OpenAI API key available for command preprocessing")
|
|
1651
|
-
return setup_commands
|
|
1652
|
-
|
|
1653
|
-
# Call OpenAI API
|
|
1654
|
-
import openai
|
|
1655
|
-
client = openai.OpenAI(api_key=api_key)
|
|
1656
|
-
|
|
1657
|
-
response = client.chat.completions.create(
|
|
1658
|
-
model="gpt-4.1",
|
|
1659
|
-
messages=[
|
|
1660
|
-
{"role": "system", "content": "You are a command preprocessing assistant that modifies setup commands to use available credentials and make them non-interactive."},
|
|
1661
|
-
{"role": "user", "content": prompt}
|
|
1662
|
-
],
|
|
1663
|
-
temperature=0.1,
|
|
1664
|
-
max_tokens=2000
|
|
1665
|
-
)
|
|
1666
|
-
|
|
1667
|
-
result = response.choices[0].message.content.strip()
|
|
1668
|
-
|
|
1669
|
-
# Debug: Print the raw response
|
|
1670
|
-
print(f"🔍 LLM Response: {result[:200]}...")
|
|
1671
|
-
|
|
1672
|
-
# Parse the JSON response
|
|
1673
|
-
import json
|
|
1674
|
-
try:
|
|
1675
|
-
processed_commands = json.loads(result)
|
|
1676
|
-
if isinstance(processed_commands, list):
|
|
1677
|
-
print(f"🔧 LLM preprocessed {len(processed_commands)} commands")
|
|
1678
|
-
for i, cmd in enumerate(processed_commands):
|
|
1679
|
-
if cmd != setup_commands[i]:
|
|
1680
|
-
print(f" {i+1}. {setup_commands[i]} → {cmd}")
|
|
1681
|
-
return processed_commands
|
|
1682
|
-
else:
|
|
1683
|
-
print("⚠️ LLM returned invalid format, using fallback preprocessing")
|
|
1684
|
-
return fallback_preprocess_commands(setup_commands, stored_credentials)
|
|
1685
|
-
except json.JSONDecodeError as e:
|
|
1686
|
-
print(f"⚠️ Failed to parse LLM response: {e}")
|
|
1687
|
-
print("🔄 Using fallback preprocessing...")
|
|
1688
|
-
return fallback_preprocess_commands(setup_commands, stored_credentials)
|
|
1689
|
-
|
|
1690
|
-
except Exception as e:
|
|
1691
|
-
print(f"⚠️ LLM preprocessing failed: {e}")
|
|
1692
|
-
print("🔄 Using fallback preprocessing...")
|
|
1693
|
-
return fallback_preprocess_commands(setup_commands, stored_credentials)
|
|
1694
|
-
|
|
1695
|
-
def fallback_preprocess_commands(setup_commands, stored_credentials):
|
|
1696
|
-
"""
|
|
1697
|
-
Fallback preprocessing function that manually handles common credential injection patterns.
|
|
1698
|
-
|
|
1699
|
-
Args:
|
|
1700
|
-
setup_commands: List of setup commands
|
|
1701
|
-
stored_credentials: Dictionary of stored credentials
|
|
1702
|
-
|
|
1703
|
-
Returns:
|
|
1704
|
-
List of processed commands with credentials injected
|
|
1705
|
-
"""
|
|
1706
|
-
if not setup_commands or not stored_credentials:
|
|
1707
|
-
return setup_commands
|
|
1708
|
-
|
|
1709
|
-
processed_commands = []
|
|
1710
|
-
|
|
1711
|
-
for i, command in enumerate(setup_commands):
|
|
1712
|
-
processed_command = command
|
|
1713
|
-
|
|
1714
|
-
# Handle Hugging Face login
|
|
1715
|
-
if 'huggingface-cli login' in command and '--token' not in command:
|
|
1716
|
-
if 'HUGGINGFACE_TOKEN' in stored_credentials:
|
|
1717
|
-
processed_command = f"huggingface-cli login --token $HUGGINGFACE_TOKEN"
|
|
1718
|
-
print(f"🔧 Fallback: Injected HF token into command {i+1}")
|
|
1719
|
-
else:
|
|
1720
|
-
processed_command = f"# SKIPPED: {command} (no HF token available)"
|
|
1721
|
-
print(f"🔧 Fallback: Skipped command {i+1} (no HF token)")
|
|
1722
|
-
|
|
1723
|
-
# Handle OpenAI API key
|
|
1724
|
-
elif 'openai' in command.lower() and 'api_key' not in command.lower():
|
|
1725
|
-
if 'OPENAI_API_KEY' in stored_credentials:
|
|
1726
|
-
processed_command = f"export OPENAI_API_KEY=$OPENAI_API_KEY && {command}"
|
|
1727
|
-
print(f"🔧 Fallback: Added OpenAI API key export to command {i+1}")
|
|
1728
|
-
|
|
1729
|
-
# Handle npm install
|
|
1730
|
-
elif 'npm install' in command and '--yes' not in command and '--no-interactive' not in command:
|
|
1731
|
-
processed_command = command.replace('npm install', 'npm install --yes')
|
|
1732
|
-
print(f"🔧 Fallback: Made npm install non-interactive in command {i+1}")
|
|
1733
|
-
|
|
1734
|
-
# Handle git clone
|
|
1735
|
-
elif command.strip().startswith('git clone') and '--depth 1' not in command:
|
|
1736
|
-
processed_command = command.replace('git clone', 'git clone --depth 1')
|
|
1737
|
-
print(f"🔧 Fallback: Made git clone non-interactive in command {i+1}")
|
|
1738
|
-
|
|
1739
|
-
# Handle apt-get install
|
|
1740
|
-
elif 'apt-get install' in command and '-y' not in command:
|
|
1741
|
-
processed_command = command.replace('apt-get install', 'apt-get install -y')
|
|
1742
|
-
print(f"🔧 Fallback: Made apt-get install non-interactive in command {i+1}")
|
|
1743
|
-
|
|
1744
|
-
processed_commands.append(processed_command)
|
|
1745
|
-
|
|
1746
|
-
print(f"🔧 Fallback preprocessing completed: {len(processed_commands)} commands")
|
|
1747
|
-
return processed_commands
|
|
1748
733
|
|
|
1749
734
|
def _check_authentication(auth_manager):
|
|
1750
735
|
"""Check if user is authenticated, prompt for login if not"""
|
|
@@ -1756,6 +741,7 @@ def _check_authentication(auth_manager):
|
|
|
1756
741
|
print("\n🔐 Authentication required")
|
|
1757
742
|
return auth_manager.interactive_auth_flow()
|
|
1758
743
|
|
|
744
|
+
|
|
1759
745
|
def _handle_auth_commands(auth_manager, args):
|
|
1760
746
|
"""Handle authentication-related commands"""
|
|
1761
747
|
if args.login:
|
|
@@ -1914,9 +900,6 @@ if __name__ == "__main__":
|
|
|
1914
900
|
parser.add_argument('--volume-name', type=str, help='Name of the Modal volume for persistent storage')
|
|
1915
901
|
parser.add_argument('--timeout', type=int, default=60, help='Container timeout in minutes (default: 60)')
|
|
1916
902
|
parser.add_argument('--ssh-password', type=str, help='SSH password (random if not provided)')
|
|
1917
|
-
parser.add_argument('--use-api', action='store_true', help='[DEPRECATED] Fetch setup commands from original API (use --repo-url for Agent instead)')
|
|
1918
|
-
parser.add_argument('--use-gitingest', action='store_true', default=True, help='[DEPRECATED] Use gitingest approach (Agent is now used when --repo-url is provided)')
|
|
1919
|
-
parser.add_argument('--no-gitingest', action='store_true', help='[DEPRECATED] Disable gitingest approach (no longer needed with Agent)')
|
|
1920
903
|
parser.add_argument('--show-examples', action='store_true', help='Show usage examples')
|
|
1921
904
|
parser.add_argument('--list-gpus', action='store_true', help='List available GPU types with their specifications')
|
|
1922
905
|
parser.add_argument('--interactive', action='store_true', help='Run in interactive mode with prompts')
|
|
@@ -1927,6 +910,7 @@ if __name__ == "__main__":
|
|
|
1927
910
|
parser.add_argument('--gpu', default='A10G', help='GPU type to use')
|
|
1928
911
|
parser.add_argument('--gpu-count', type=int, default=1, help='Number of GPUs to use (default: 1)')
|
|
1929
912
|
parser.add_argument('--repo-url', help='Repository URL')
|
|
913
|
+
parser.add_argument('--use-cuda-base', action='store_true', help='Use CUDA base image (may cause segfaults, use only if needed for CUDA libraries)')
|
|
1930
914
|
|
|
1931
915
|
# Authentication-related arguments
|
|
1932
916
|
parser.add_argument('--auth', action='store_true', help='Manage authentication (login, register, logout)')
|
|
@@ -2050,8 +1034,6 @@ if __name__ == "__main__":
|
|
|
2050
1034
|
print(f"Volume: {args.volume_name or 'None'}")
|
|
2051
1035
|
if args.repo_url:
|
|
2052
1036
|
print("Repository Setup: Agent (intelligent)")
|
|
2053
|
-
elif args.use_api:
|
|
2054
|
-
print("Setup Commands: Auto-detect from repository")
|
|
2055
1037
|
elif args.setup_commands:
|
|
2056
1038
|
print(f"Setup Commands: {len(args.setup_commands)} custom commands")
|
|
2057
1039
|
else:
|
|
@@ -2068,7 +1050,6 @@ if __name__ == "__main__":
|
|
|
2068
1050
|
print("\n🛑 Operation cancelled by user.")
|
|
2069
1051
|
sys.exit(0)
|
|
2070
1052
|
else:
|
|
2071
|
-
# print("🔍 Debug: yes parameter = true")
|
|
2072
1053
|
print("")
|
|
2073
1054
|
|
|
2074
1055
|
# Interactive mode or missing required arguments
|
|
@@ -2117,25 +1098,13 @@ if __name__ == "__main__":
|
|
|
2117
1098
|
print("\n🛑 Setup cancelled.")
|
|
2118
1099
|
sys.exit(1)
|
|
2119
1100
|
|
|
2120
|
-
# Ask about setup commands
|
|
2121
|
-
use_gitingest = args.use_gitingest and not args.no_gitingest
|
|
2122
|
-
if not args.use_api and not args.setup_commands and not args.setup_commands_json:
|
|
2123
|
-
try:
|
|
2124
|
-
auto_detect = input("? Automatically detect setup commands for this repository? (Y/n): ").strip().lower()
|
|
2125
|
-
if auto_detect in ('n', 'no'):
|
|
2126
|
-
use_gitingest = False
|
|
2127
|
-
except KeyboardInterrupt:
|
|
2128
|
-
print("\n🛑 Setup cancelled.")
|
|
2129
|
-
sys.exit(1)
|
|
2130
|
-
|
|
2131
1101
|
# Update args with interactive values
|
|
2132
1102
|
args.repo_url = repo_url
|
|
2133
1103
|
args.volume_name = volume_name
|
|
2134
1104
|
args.gpu_count = gpu_count
|
|
2135
|
-
args.use_gitingest = use_gitingest
|
|
2136
1105
|
|
|
2137
1106
|
try:
|
|
2138
|
-
# Setup commands are no longer used when repo_url is provided (
|
|
1107
|
+
# Setup commands are no longer used when repo_url is provided (Agent handles setup)
|
|
2139
1108
|
setup_commands = args.setup_commands or []
|
|
2140
1109
|
|
|
2141
1110
|
# Repository setup approach
|
|
@@ -2145,7 +1114,6 @@ if __name__ == "__main__":
|
|
|
2145
1114
|
else:
|
|
2146
1115
|
print("⚠️ No repository URL provided - setup commands may be needed manually")
|
|
2147
1116
|
|
|
2148
|
-
|
|
2149
1117
|
# Parse setup commands from JSON if provided
|
|
2150
1118
|
if args.setup_commands_json:
|
|
2151
1119
|
try:
|
|
@@ -2161,7 +1129,6 @@ if __name__ == "__main__":
|
|
|
2161
1129
|
print(f"⚠️ Error parsing JSON setup commands: {e}")
|
|
2162
1130
|
print(f"Received JSON string: {args.setup_commands_json}")
|
|
2163
1131
|
|
|
2164
|
-
|
|
2165
1132
|
# Load commands from file if specified
|
|
2166
1133
|
if args.commands_file and os.path.exists(args.commands_file):
|
|
2167
1134
|
try:
|
|
@@ -2197,7 +1164,7 @@ if __name__ == "__main__":
|
|
|
2197
1164
|
try:
|
|
2198
1165
|
with open(args.setup_script, 'r') as f:
|
|
2199
1166
|
script_content = f.read().strip()
|
|
2200
|
-
# Convert script to individual
|
|
1167
|
+
# Convert script to individual commands
|
|
2201
1168
|
script_commands = [line.strip() for line in script_content.split('\n')
|
|
2202
1169
|
if line.strip() and not line.strip().startswith('#')]
|
|
2203
1170
|
setup_commands.extend(script_commands)
|
|
@@ -2233,7 +1200,8 @@ if __name__ == "__main__":
|
|
|
2233
1200
|
timeout_minutes=args.timeout,
|
|
2234
1201
|
ssh_password=ssh_password,
|
|
2235
1202
|
interactive=args.interactive,
|
|
2236
|
-
gpu_count=getattr(args, 'gpu_count', 1)
|
|
1203
|
+
gpu_count=getattr(args, 'gpu_count', 1),
|
|
1204
|
+
use_cuda_base=getattr(args, 'use_cuda_base', False)
|
|
2237
1205
|
)
|
|
2238
1206
|
except KeyboardInterrupt:
|
|
2239
1207
|
cleanup_modal_token()
|