/ proxy.py
proxy.py
1 #!/usr/bin/env python3 2 import sys 3 import subprocess 4 import re 5 import os 6 import glob 7 8 def strip_ansi(text): 9 ansi_escape = re.compile(r'\x1B(?:[@-Z\-_]|[0-?]*[@-~])') 10 return ansi_escape.sub('', text) 11 12 def detect_stacks(): 13 stacks = set() 14 cwd = os.getcwd() 15 16 # 1. File-based detection 17 if os.path.exists(os.path.join(cwd, 'Cargo.toml')): 18 stacks.add('rust') 19 if os.path.exists(os.path.join(cwd, 'package.json')): 20 stacks.add('node') 21 if os.path.exists(os.path.join(cwd, 'go.mod')): 22 stacks.add('go') 23 if os.path.exists(os.path.join(cwd, 'requirements.txt')) or \ 24 os.path.exists(os.path.join(cwd, 'pyproject.toml')) or \ 25 os.path.exists(os.path.join(cwd, 'Pipfile')): 26 stacks.add('python') 27 if os.path.exists(os.path.join(cwd, 'pom.xml')) or \ 28 os.path.exists(os.path.join(cwd, 'build.gradle')): 29 stacks.add('java') 30 31 # 2. Extension-based detection 32 if 'python' not in stacks and glob.glob(os.path.join(cwd, '*.py')): 33 stacks.add('python') 34 if 'rust' not in stacks and glob.glob(os.path.join(cwd, 'src', '*.rs')): 35 stacks.add('rust') 36 37 # 3. CI/Workflow detection 38 workflow_files = glob.glob(os.path.join(cwd, '.forgejo', 'workflows', '*.yml')) + \ 39 glob.glob(os.path.join(cwd, '.github', 'workflows', '*.yml')) 40 41 for wf in workflow_files: 42 try: 43 with open(wf, 'r', encoding='utf-8') as f: 44 content = f.read() 45 if re.search(r'\bcargo\b', content): 46 stacks.add('rust') 47 if re.search(r'\b(npm|yarn|pnpm|bun)\b', content): 48 stacks.add('node') 49 if re.search(r'\b(go build|go test)\b', content): 50 stacks.add('go') 51 if re.search(r'\b(pip|python|pytest|poetry)\b', content): 52 stacks.add('python') 53 except Exception: 54 pass 55 56 if not stacks: 57 stacks.add('generic') 58 59 return stacks 60 61 def process_line(line, stacks): 62 clean = strip_ansi(line).strip() 63 if not clean: 64 return None 65 66 # Boilerplate suppression 67 suppress_pattern = r'^(Downloading|Compiling|Fetching|Running|Created|Started|Updated|Installing|Waiting)\b' 68 if re.match(suppress_pattern, clean, re.IGNORECASE): 69 return None 70 71 # Success patterns 72 success_pattern = r'^(ok|passed|finished|success|completed)$' 73 if re.match(success_pattern, clean, re.IGNORECASE): 74 return '.' 75 76 # --- Stack Specific Logic --- 77 78 if 'rust' in stacks: 79 err_match = re.search(r'error\[(E\d+)\]', clean) 80 if err_match: 81 return f"!ERR:{err_match.group(1)} {clean[match_end(err_match):].strip()}" 82 loc_match = re.search(r'-->\s+(.+:\d+)', clean) 83 if loc_match: 84 return f"@ {loc_match.group(1)}" 85 86 if 'node' in stacks: 87 err_match = re.match(r'^(\w+Error):', clean) 88 if err_match: 89 return f"!ERR:{err_match.group(1)} {clean[match_end(err_match):].strip()}" 90 loc_match = re.search(r'\((.+:\d+:\d+)\)', clean) 91 if loc_match: 92 return f"@ {loc_match.group(1)}" 93 94 if 'go' in stacks: 95 match = re.match(r'^(.+\.go:\d+:\d+):\s+(.+)', clean) 96 if match: 97 return f"!ERR:GoError @ {match.group(1)} {match.group(2)}" 98 99 if 'python' in stacks: 100 if clean.startswith('Traceback (most recent call last):'): 101 return "!ERR:Traceback" 102 loc_match = re.match(r'File "([^"]+)", line (\d+)(?:, in (.+))?', clean) 103 if loc_match: 104 file_path = loc_match.group(1) 105 line_num = loc_match.group(2) 106 context = loc_match.group(3) or "" 107 return f"@ {file_path}:{line_num} {context}" 108 err_match = re.match(r'^(\w+Error):', clean) 109 if err_match: 110 return f"!ERR:{err_match.group(1)} {clean[match_end(err_match):].strip()}" 111 112 # Generic Error Fallback 113 if re.search(r'\b(Error|Exception|Fail|Fatal)\b', clean, re.IGNORECASE): 114 file_match = re.search(r'(\S+\.\w+:\d+)', clean) 115 loc = f" @ {file_match.group(1)}" if file_match else "" 116 return f"!ERR:GENERIC{loc} {clean}" 117 118 return clean 119 120 def match_end(match): 121 return match.end() if match else 0 122 123 def main(): 124 if len(sys.argv) < 2: 125 sys.exit(0) 126 127 command = sys.argv[1:] 128 129 # --- Configurable Docker Restriction --- 130 if command and command[0] == 'docker': 131 cwd = os.getcwd() 132 # Check for .allow_docker file 133 if not os.path.exists(os.path.join(cwd, '.allow_docker')): 134 sys.stderr.write("!ERR:RESTRICTED Docker is forbidden (Native Runner Only). Create .allow_docker to override.\n") 135 sys.exit(1) 136 137 stacks = detect_stacks() 138 139 process = None 140 try: 141 process = subprocess.Popen( 142 command, 143 stdout=subprocess.PIPE, 144 stderr=subprocess.STDOUT, 145 text=True, 146 bufsize=1, 147 encoding='utf-8', 148 errors='replace' 149 ) 150 151 while True: 152 line = process.stdout.readline() 153 if not line and process.poll() is not None: 154 break 155 if not line: 156 continue 157 158 result = process_line(line, stacks) 159 if result: 160 if result == '.': 161 sys.stdout.write('.') 162 elif result.startswith('!ERR'): 163 sys.stdout.write('\n' + result + '\n') 164 elif result.startswith('@'): 165 sys.stdout.write(result + '\n') 166 else: 167 sys.stdout.write(result + '\n') 168 169 sys.stdout.flush() 170 171 process.wait() 172 sys.exit(process.returncode) 173 174 except KeyboardInterrupt: 175 if process: 176 process.terminate() 177 sys.exit(130) 178 except Exception as e: 179 sys.stderr.write(f"Proxy Error: {e}\n") 180 sys.exit(1) 181 182 if __name__ == "__main__": 183 main()