Files
SkillMate/main.py
2025-09-27 13:11:39 +02:00

268 Zeilen
10 KiB
Python

#!/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 im Browser
time.sleep(3)
webbrowser.open(f"http://localhost:{self.frontend_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()