INSTALLIEREN.command
Rama main · réplica de lectura del repositorio original.
#!/bin/bash
# ============================================================================
# PBP — Persoenliches Bewerbungs-Portal
# Doppelklick-Installer fuer macOS
# ============================================================================
# Einfach doppelklicken — kein Terminal-Wissen noetig.
# ============================================================================
set -e
cd "$(dirname "$0")"
PROJECT_DIR="$(pwd)"
VENV_DIR="$PROJECT_DIR/.venv"
export BA_DATA_DIR="${BA_DATA_DIR:-$HOME/.bewerbungs-assistent}"
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
ok() { echo -e " ${GREEN}✓ $1${NC}"; }
warn() { echo -e " ${YELLOW}⚠ $1${NC}"; }
fail() { echo -e " ${RED}✗ $1${NC}"; exit 1; }
info() { echo -e " ${CYAN}→ $1${NC}"; }
clear
echo ""
echo -e "${CYAN}╔══════════════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║ PBP — Persoenliches Bewerbungs-Portal ║${NC}"
echo -e "${CYAN}║ macOS Installer ║${NC}"
echo -e "${CYAN}╚══════════════════════════════════════════════════╝${NC}"
echo ""
# Erkennen ob Erstinstallation oder Update
if [ -f "$VENV_DIR/bin/python" ]; then
MODE="update"
info "Bestehende Installation erkannt — Update-Modus"
else
MODE="install"
info "Erstinstallation"
fi
# ── 1. Python pruefen ──────────────────────────────────────────
echo ""
echo -e "${YELLOW}[1/6] Python pruefen...${NC}"
PYTHON=""
for cmd in python3.13 python3.12 python3.11 python3 python; do
if command -v $cmd &>/dev/null; then
ver=$($cmd --version 2>&1)
# #1149: der Ausdruck war `s/.*3\.\([0-9]*\).*/\1/p`. Das `.*` greift so weit wie moeglich und
# nimmt damit das LETZTE "3." im Text: bei "Python 3.13.5" die "3.5" statt der "3.13" —
# jede Python-3.13-Fassung galt als zu alt (minor=5), obwohl 3.13 neuer ist als die
# verlangte 3.11. Jetzt am Anfang verankert: Zahl nach dem ersten "3.".
minor=$(echo "$ver" | sed -n 's/^[^0-9]*3\.\([0-9][0-9]*\).*/\1/p' | head -1)
if [ -n "$minor" ] && [ "$minor" -ge 11 ] 2>/dev/null; then
PYTHON=$cmd
ok "$ver"
break
fi
fi
done
if [ -z "$PYTHON" ]; then
echo ""
echo -e " ${RED}Python 3.11 oder neuer wird benoetigt.${NC}"
echo ""
echo -e " ${BOLD}So installierst du Python:${NC}"
echo ""
echo -e " Option 1 — Homebrew (empfohlen):"
echo -e " ${CYAN}brew install python@3.12${NC}"
echo ""
echo -e " Option 2 — python.org:"
echo -e " Lade Python von ${CYAN}https://www.python.org/downloads/${NC}"
echo ""
echo -e " Danach dieses Skript erneut doppelklicken."
echo ""
read -p " Druecke Enter zum Schliessen..."
exit 1
fi
# ── 2. Virtuelle Umgebung ──────────────────────────────────────
echo ""
echo -e "${YELLOW}[2/6] Virtuelle Umgebung...${NC}"
if [ "$MODE" = "install" ]; then
info "Erstelle .venv..."
"$PYTHON" -m venv "$VENV_DIR"
ok "Erstellt"
else
ok "Existiert bereits"
fi
PYTHON="$VENV_DIR/bin/python"
# ── 3. Pakete installieren ─────────────────────────────────────
echo ""
echo -e "${YELLOW}[3/6] Pakete installieren...${NC}"
# #1216: "$PYTHON" in Anfuehrungszeichen - ein Leerzeichen im Ordnernamen (macOS: "PBP-1.8.0 2") liess den Befehl zerfallen,
# und unter `set -e` endete der Installer ohne ein Wort. #1219: ein gescheitertes pip-Update (kein Netz) beendet ihn nicht mehr.
"$PYTHON" -m pip install --upgrade pip -q 2>/dev/null || warn "pip liess sich nicht aktualisieren - kein Problem"
"$PYTHON" -m pip install -e "$PROJECT_DIR" -q
ok "Core installiert"
"$PYTHON" -m pip install -e "$PROJECT_DIR[all]" -q 2>/dev/null && \
ok "Alle Module installiert" || \
warn "Optionale Module teilweise fehlgeschlagen (kein Problem)"
# #697: Playwright-Chromium wie in installer/install.sh — ohne diesen
# Schritt erscheint zur Laufzeit "Executable doesn't exist".
info "Installiere Chromium fuer Browser-Quellen..."
if "$PYTHON" -m playwright install chromium 2>/dev/null; then
ok "Chromium-Browser installiert"
else
warn "Chromium-Download fehlgeschlagen — Browser-Quellen eingeschraenkt, Rest funktioniert"
fi
# ── 4. Frontend bauen ──────────────────────────────────────────
echo ""
echo -e "${YELLOW}[4/6] Dashboard (React UI) bauen...${NC}"
FRONTEND_DIR="$PROJECT_DIR/frontend"
PNPM_OK=false
if command -v pnpm &>/dev/null; then
PNPM_OK=true
elif command -v npm &>/dev/null; then
info "Installiere pnpm..."
npm install -g pnpm 2>/dev/null && PNPM_OK=true || true
fi
if [ "$PNPM_OK" = true ] && [ -d "$FRONTEND_DIR" ]; then
# #1149: unter `set -e` beendete ein fehlgeschlagenes pnpm den Installer ohne ein
# Wort (stderr geht nach /dev/null): Datenordner und Claude-Eintrag kamen nie. Das
# fertig gebaute Dashboard liegt dem ZIP bei; der Bau ist eine Zugabe.
if pnpm --dir "$FRONTEND_DIR" install --quiet 2>/dev/null && pnpm --dir "$FRONTEND_DIR" run build 2>/dev/null; then
ok "Dashboard gebaut"
else
warn "Dashboard-Bau fehlgeschlagen — kein Problem: das fertig gebaute Dashboard liegt bei, PBP laeuft damit"
fi
elif command -v node &>/dev/null; then
warn "pnpm nicht verfuegbar — ueberspringe Frontend-Build"
else
warn "Node.js nicht installiert — Dashboard-Build uebersprungen"
echo " Installiere mit: ${CYAN}brew install node${NC}"
fi
# ── 5. Datenverzeichnis ───────────────────────────────────────
echo ""
echo -e "${YELLOW}[5/6] Datenverzeichnis...${NC}"
mkdir -p "$BA_DATA_DIR"/{dokumente,export,logs}
ok "$BA_DATA_DIR"
# ── 6. Claude Desktop konfigurieren ──────────────────────────
echo ""
echo -e "${YELLOW}[6/6] Claude Desktop konfigurieren...${NC}"
CLAUDE_CONFIG="$HOME/Library/Application Support/Claude/claude_desktop_config.json"
CLAUDE_DIR="$(dirname "$CLAUDE_CONFIG")"
if [ -d "$CLAUDE_DIR" ]; then
# v1.8.0 (#1219): `if` statt `$?` - unter `set -e` beendete ein Fehlschlag das Skript an dieser Stelle, und der Zweig
# darunter lief nie. Die Ausgabe wird gelesen: eine unlesbare Konfiguration wird ersetzt (mit Kopie), und das gehoert in den Schluss.
if SETUP_AUSGABE="$("$PYTHON" "$PROJECT_DIR/_setup_claude.py" 2>&1)"; then
printf '%s\n' "$SETUP_AUSGABE"
ok "Claude Desktop konfiguriert"
else
printf '%s\n' "$SETUP_AUSGABE"
CLAUDE_FEHLER=1
warn "Konfiguration fehlgeschlagen — manuell ausfuehren: \"$PYTHON\" \"$PROJECT_DIR/_setup_claude.py\""
fi
CLAUDE_KOPIE="$(printf '%s\n' "$SETUP_AUSGABE" | sed -n 's/.*Kopie gesichert: //p' | head -n 1)"
else
warn "Claude Desktop nicht installiert"
echo " Lade Claude Desktop: ${CYAN}https://claude.ai/download${NC}"
echo " Nach der Installation dieses Skript erneut doppelklicken."
fi
# ── Fertig ─────────────────────────────────────────────────────
echo ""
if [ "${CLAUDE_FEHLER:-0}" = "1" ]; then
# #1219: keine gruene Erfolgsmeldung, wenn Claude Desktop nicht eingerichtet werden konnte
echo -e "${YELLOW}╔══════════════════════════════════════════════════╗${NC}"
echo -e "${YELLOW}║ Installiert - Claude Desktop fehlt noch ║${NC}"
echo -e "${YELLOW}╚══════════════════════════════════════════════════╝${NC}"
else
echo -e "${GREEN}╔══════════════════════════════════════════════════╗${NC}"
if [ "$MODE" = "install" ]; then
echo -e "${GREEN}║ Installation erfolgreich! ║${NC}"
else
echo -e "${GREEN}║ Update erfolgreich! ║${NC}"
fi
echo -e "${GREEN}╚══════════════════════════════════════════════════╝${NC}"
fi
echo ""
if [ -n "${CLAUDE_KOPIE:-}" ]; then
echo -e " ${YELLOW}⚠ Deine bisherige Claude-Konfiguration war nicht lesbar und wurde ersetzt.${NC}"
echo " Die alte Datei liegt als Kopie vor: $CLAUDE_KOPIE"
echo " Andere MCP-Server musst du von dort zurueckkopieren."
echo ""
fi
if [ -d "$CLAUDE_DIR" ]; then
echo -e " ${BOLD}So geht's weiter:${NC}"
echo ""
echo -e " 1. Claude Desktop beenden (Menueleiste → Claude → Beenden)"
echo -e " 2. Claude Desktop neu starten"
echo -e " 3. Eintippen: ${CYAN}Starte den Bewerbungs-Assistenten${NC}"
echo ""
fi
# v1.7.0-beta.38 (#600): Auto-Start Dashboard + Browser oeffnen
echo -e " ${BOLD}PBP-Dashboard wird gestartet...${NC}"
# --- dashboard-funktionen Anfang
# v1.8.0 (#1149 Punkt 6): Beim Update laeuft haeufig noch das Dashboard der ALTEN Fassung und haelt den Port. Der neue Start
# meldete dann "laeuft bereits", und der Health-Check bestaetigte die alte Fassung: der Installer sagte "laeuft", der Mensch
# sah weiter das Alte. Beendet wird nur, was sich als start_dashboard.py von PBP ausweist - nie der MCP-Prozess von
# Claude Desktop und nie ein fremdes Programm auf dem Port. Der Health-Check fragt /api/health und vergleicht die Fassung;
# ein fremdes Programm, das HTTP 200 liefert, gilt nicht mehr als "laeuft".
# v1.8.0 (#1218): Der Name allein traf auch ein FREMDES start_dashboard.py und jeden Testlauf, der ihn als Argument traegt
# (`pytest tests/test_start_dashboard.py`). Das Muster verlangt jetzt ein Wort- oder Pfadende davor, und ein Prozess, dessen
# Skript NACHWEISLICH nicht neben einem PBP-Quellbaum liegt, bleibt unberuehrt (`skript_ist_fremd`).
DASH_PORT="${BA_DASHBOARD_PORT:-8200}"
DASH_MUSTER='(^|/)[Pp]ython[0-9.]*[[:space:]](.*[[:space:]/])?start_dashboard\.py'
NEUE_FASSUNG="$(sed -n 's/^__version__[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' "$PROJECT_DIR/src/bewerbungs_assistent/__init__.py" 2>/dev/null | head -n 1)"
# Gehoert das Skript in dieser Befehlszeile NACHWEISLICH nicht zu PBP? Fremd heisst: ein absoluter Pfad, die Datei ist da,
# und daneben liegt weder ein Quellbaum (src/bewerbungs_assistent) noch der Programmordner der Auto-Update-Fassung
# (versions). Ein relativer oder nicht lesbarer Pfad gilt NICHT als fremd: dann bleibt es beim Verhalten aus #1149 Punkt 6.
# Die Befehlszeile traegt den Pfad samt Leerzeichen ("PBP-1.8.0 2"): von links wird Wort fuer Wort gekuerzt, bis der Rest
# eine Datei ist.
skript_ist_fremd() {
local kandidat="$1" ordner
case "$kandidat" in
*start_dashboard.py*) kandidat="${kandidat%%start_dashboard.py*}start_dashboard.py" ;;
*) return 1 ;;
esac
while [ -n "$kandidat" ]; do
case "$kandidat" in
/*)
if [ -f "$kandidat" ] && [ "${kandidat##*/}" = "start_dashboard.py" ]; then
ordner="${kandidat%/*}"
if [ -d "$ordner/src/bewerbungs_assistent" ] || [ -d "$ordner/versions" ]; then
return 1
fi
return 0
fi
;;
esac
case "$kandidat" in
*[[:space:]]*) kandidat="${kandidat#*[[:space:]]}" ;;
*) return 1 ;;
esac
done
return 1
}
dashboard_pids() {
command -v pgrep >/dev/null 2>&1 || return 0
local pid zeile
for pid in $(pgrep -f -i "$DASH_MUSTER" 2>/dev/null | grep -v -x "$$" || true); do
zeile="$(ps -p "$pid" -o args= 2>/dev/null || true)"
if skript_ist_fremd "$zeile"; then
continue
fi
echo "$pid"
done
return 0
}
# Die Fassung, die auf dem Port als PBP antwortet - leer, wenn dort nichts oder ein anderes Programm antwortet.
dashboard_stand() {
curl -sf --max-time 2 "http://127.0.0.1:${DASH_PORT}/api/health" 2>/dev/null \
| grep -o '"pbp_version"[[:space:]]*:[[:space:]]*"[^"]*"' | head -n 1 \
| sed 's/.*:[[:space:]]*"\(.*\)"$/\1/' || true
}
alte_dashboards_beenden() {
local pids n=0
pids="$(dashboard_pids)"
if [ -z "$pids" ]; then
return 0
fi
info "Ein laufendes PBP-Dashboard wird beendet, damit die neue Fassung startet"
kill $pids 2>/dev/null || true
while [ "$n" -lt 5 ]; do
sleep 1
pids="$(dashboard_pids)"
if [ -z "$pids" ]; then
return 0
fi
n=$((n + 1))
done
kill -9 $pids 2>/dev/null || true
sleep 1
return 0
}
# ok = die installierte Fassung antwortet, alt = eine andere Fassung antwortet, keins = kein PBP antwortet,
# unbekannt = PBP antwortet, aber die installierte Fassung liess sich nicht lesen (#1220): dann gilt NICHTS als "ok".
dashboard_urteil() {
local stand="$1"
if [ -z "$stand" ]; then
echo keins
elif [ -z "$NEUE_FASSUNG" ]; then
echo unbekannt
elif [ "$stand" != "$NEUE_FASSUNG" ]; then
echo alt
else
echo ok
fi
}
# --- dashboard-funktionen Ende
# Zuerst das alte Dashboard (Update), dann im Hintergrund starten
alte_dashboards_beenden
DASH_LAUNCHER="$PROJECT_DIR/Dashboard starten.command"
if [ -f "$DASH_LAUNCHER" ]; then
chmod +x "$DASH_LAUNCHER" 2>/dev/null
nohup "$DASH_LAUNCHER" > /dev/null 2>&1 &
echo -e " [OK] Dashboard-Prozess gestartet."
else
echo -e " [!!] Dashboard-Launcher nicht gefunden: $DASH_LAUNCHER"
fi
# Health-Check bis der Port als PBP antwortet, max 30 Sekunden
echo -n " Warte auf Dashboard auf http://localhost:${DASH_PORT} "
DASH_STAND=""
for i in $(seq 1 30); do
DASH_STAND="$(dashboard_stand)"
if [ -n "$DASH_STAND" ]; then
break
fi
echo -n "."
sleep 1
done
echo ""
DASH_URTEIL="$(dashboard_urteil "$DASH_STAND")"
if [ "$DASH_URTEIL" = "ok" ]; then
echo -e " [OK] Dashboard laeuft. Oeffne Browser..."
open "http://localhost:${DASH_PORT}/"
elif [ "$DASH_URTEIL" = "unbekannt" ]; then
echo -e " [!!] Auf dem Port antwortet PBP $DASH_STAND - ob das die neue Fassung ist, liess sich nicht pruefen"
echo -e " (die installierte Fassung war nicht lesbar). Die Versionsnummer steht unten links im Dashboard."
open "http://localhost:${DASH_PORT}/" 2>/dev/null || true
elif [ "$DASH_URTEIL" = "alt" ]; then
echo -e " [!!] Auf dem Port laeuft noch PBP $DASH_STAND - installiert wurde $NEUE_FASSUNG."
echo -e " Meist ist das der Prozess, den Claude Desktop gestartet hat: Claude Desktop komplett beenden"
echo -e " und neu starten, dann zeigt das Dashboard die neue Fassung."
open "http://localhost:${DASH_PORT}/" 2>/dev/null || true
else
echo -e " [!!] Dashboard antwortet nicht nach 30 Sekunden."
echo -e " Oeffne Browser trotzdem — falls eine alte Instanz laeuft."
open "http://localhost:${DASH_PORT}/" 2>/dev/null || true
fi
echo ""
echo -e " ${CYAN}Daten:${NC} $BA_DATA_DIR"
echo -e " ${CYAN}Projekt:${NC} $PROJECT_DIR"
# #975 (I11): Symmetrie zum Windows-Installer — wer installiert,
# soll auch wissen, wo der Rueckweg liegt.
echo -e " ${CYAN}Entfernen:${NC} DEINSTALLIEREN.command in diesem Ordner"
echo ""
read -p " Druecke Enter zum Schliessen..."