271 Zeilen
11 KiB
Python
271 Zeilen
11 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 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()
|