/ 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()