#!/usr/bin/env python3 """ SkillMate - Windows-optimierter Haupteinstiegspunkt """ import os import sys import subprocess import time import webbrowser import signal import atexit import argparse from pathlib import Path from typing import Optional, List class SkillMateStarter: def __init__(self, mode: str = 'dev'): self.processes = [] self.base_dir = Path(__file__).parent.absolute() self.mode = mode # 'dev' oder 'prod' # Ports (Backend/Frontend/Admin) self.backend_port = int(os.environ.get('PORT', '3004')) self.frontend_port = 5173 self.admin_port = 5174 # Registriere Cleanup-Handler atexit.register(self.cleanup) if sys.platform != 'win32': signal.signal(signal.SIGINT, self.signal_handler) signal.signal(signal.SIGTERM, self.signal_handler) def _has_local_binary(self, project_dir: Path, binary: str) -> bool: """Prüft, ob ein lokales NPM-Binary vorhanden ist (Windows .cmd / Unix ohne Endung).""" bin_dir = project_dir / 'node_modules' / '.bin' if sys.platform == 'win32': return (bin_dir / f'{binary}.cmd').exists() or (bin_dir / f'{binary}.CMD').exists() else: return (bin_dir / binary).exists() def _ensure_dependencies(self) -> bool: """Installiert fehlende Node-Abhängigkeiten in backend, frontend, admin-panel. Rückgabe: True bei Erfolg, False wenn Installation irreparabel fehlschlägt. """ projects = [ (self.base_dir / 'backend', ['nodemon', 'ts-node']), (self.base_dir / 'frontend', ['vite']), (self.base_dir / 'admin-panel', ['vite']) ] any_install = False for proj_dir, required_bins in projects: if not proj_dir.exists(): continue needs_install = not (proj_dir / 'node_modules').exists() if not needs_install: # Prüfe konkrete Binaries for b in required_bins: if not self._has_local_binary(proj_dir, b): needs_install = True break if needs_install: any_install = True print(f"🔧 Installiere Abhängigkeiten in {proj_dir.name}...") # Bevorzugt npm ci, fallback auf npm install try: cmd_ci = ["cmd", "/c", "npm ci"] if sys.platform == 'win32' else ["npm", "ci"] result = subprocess.run(cmd_ci, cwd=str(proj_dir), check=False) if result.returncode != 0: cmd_install = ["cmd", "/c", "npm install"] if sys.platform == 'win32' else ["npm", "install"] result = subprocess.run(cmd_install, cwd=str(proj_dir), check=False) if result.returncode != 0: print(f"❌ Konnte Abhängigkeiten in {proj_dir.name} nicht installieren. Prüfen Sie Internet/Proxy.") return False except Exception as e: print(f"❌ Fehler bei der Installation in {proj_dir.name}: {e}") return False if any_install: print("✅ Abhängigkeiten installiert. Fahre mit dem Start fort...\n") else: print("✅ Abhängigkeiten geprüft – alles vorhanden.\n") return True def signal_handler(self, signum, frame): """Handle Strg+C und andere Signale""" print("\n\n🛑 SkillMate wird beendet...") self.cleanup() sys.exit(0) def cleanup(self): """Beende alle gestarteten Prozesse""" for process in self.processes: try: if process.poll() is None: # Prozess läuft noch if sys.platform == 'win32': subprocess.call(['taskkill', '/F', '/T', '/PID', str(process.pid)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) else: process.terminate() process.wait(timeout=5) except: pass # Sicherheitshalber Ports freigeben, falls noch Prozesse hängen if sys.platform == 'win32': for p in [self.backend_port, self.frontend_port, self.admin_port]: self._free_port_windows(p) def _popen_new_console(self, workdir: Path, command: str) -> Optional[subprocess.Popen]: try: creationflag = getattr(subprocess, 'CREATE_NEW_CONSOLE', 0x00000010) p = subprocess.Popen( ["cmd", "/k", command], cwd=str(workdir), creationflags=creationflag, ) self.processes.append(p) return p except Exception as e: print(f"❌ Konnte Konsole nicht starten: {e}") return None def _free_port_windows(self, port: int): """Versucht Prozesse auf Port zu beenden (Windows).""" try: # Finde Zeilen mit Port out = subprocess.check_output(f'netstat -ano | findstr :{port}', shell=True, text=True, stderr=subprocess.DEVNULL) pids: List[str] = [] for line in out.splitlines(): parts = line.split() if parts: pid = parts[-1] if pid.isdigit(): pids.append(pid) # Einzigartige PIDs killen for pid in sorted(set(pids)): try: subprocess.call(["taskkill", "/PID", pid, "/F"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except Exception: pass except subprocess.CalledProcessError: # Kein Treffer → Port frei pass def check_node_npm(self): """Prüfe ob Node.js und npm installiert sind""" try: # Windows-spezifische Prüfung if sys.platform == 'win32': subprocess.run(["node", "--version"], capture_output=True, check=True, shell=True) subprocess.run(["npm", "--version"], capture_output=True, check=True, shell=True) else: subprocess.run(["node", "--version"], capture_output=True, check=True) subprocess.run(["npm", "--version"], capture_output=True, check=True) return True except (subprocess.CalledProcessError, FileNotFoundError): print("❌ Node.js und npm müssen installiert sein!") print(" Bitte installieren Sie Node.js von: https://nodejs.org/") return False def start_services_windows(self): """Starte alle Services in Windows mit separaten CMD-Fenstern""" print("🚀 Starte SkillMate Services...") # Ports freimachen (wenn vorherige Läufe hingen blieben) for p in [self.backend_port, self.frontend_port, self.admin_port]: self._free_port_windows(p) # Abhängigkeiten sicherstellen, bevor Services gestartet werden print(" Prüfe/Installiere Abhängigkeiten...") if not self._ensure_dependencies(): print("❌ Abbruch: Abhängigkeiten konnten nicht installiert werden.") return # Backend backend_dir = self.base_dir / "backend" if self.mode == 'dev': self._popen_new_console(backend_dir, "npm run dev") else: # Produktionsstart: Build & Start self._popen_new_console(backend_dir, "cmd /c npm run build && npm start") # Warte bis Backend bereit ist print(" Warte auf Backend...") time.sleep(5) # Frontend frontend_dir = self.base_dir / "frontend" if self.mode == 'dev': self._popen_new_console(frontend_dir, f"npm run dev -- --strictPort --port {self.frontend_port}") else: self._popen_new_console(frontend_dir, f"cmd /c npm run build && npm run preview -- --port {self.frontend_port}") # Admin Panel (optional) admin_dir = self.base_dir / "admin-panel" if admin_dir.exists(): if self.mode == 'dev': self._popen_new_console(admin_dir, f"npm run dev -- --strictPort --port {self.admin_port}") else: self._popen_new_console(admin_dir, f"cmd /c npm run build && npm run preview -- --port {self.admin_port}") print("\n✨ SkillMate läuft!") print("\n📍 Zugriff:") print(f" - Frontend: http://localhost:{self.frontend_port}") print(f" - Backend: http://localhost:{self.backend_port}") if admin_dir.exists(): print(f" - Admin: http://localhost:{self.admin_port}") # Öffne Frontend und Admin Panel im Browser time.sleep(3) webbrowser.open(f"http://localhost:{self.frontend_port}") if (self.base_dir / "admin-panel").exists(): time.sleep(1) webbrowser.open(f"http://localhost:{self.admin_port}") print("\n⚡ Schließen Sie dieses Fenster, um SkillMate zu beenden") def run(self): """Hauptausführungsmethode""" print("🎯 SkillMate wird gestartet...\n") # Prüfe Voraussetzungen if not self.check_node_npm(): return False # Windows-spezifischer Start if sys.platform == 'win32': self.start_services_windows() # Halte das Hauptfenster offen try: input("\nDrücken Sie Enter zum Beenden...") except KeyboardInterrupt: pass else: print("❌ Dieses Skript ist für Windows optimiert.") print(" Nutzen Sie 'python main.py' für andere Systeme.") return False return True def main(): """Haupteinstiegspunkt""" parser = argparse.ArgumentParser(description='SkillMate Starter') parser.add_argument('--mode', choices=['dev', 'prod'], default='dev', help='Startmodus: dev oder prod') args = parser.parse_args() starter = SkillMateStarter(mode=args.mode) try: success = starter.run() if not success: sys.exit(1) except Exception as e: print(f"\n❌ Unerwarteter Fehler: {e}") starter.cleanup() sys.exit(1) if __name__ == "__main__": main()