diff --git a/.gitignore b/.gitignore index 74ad92f..1f2fd22 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ __pycache__/ *.pyc *.pyo +# Entorno virtual de macOS/Linux (lo crea setup_mac.command) +.venv/ + # Todo lo que generan la app y los scripts vive bajo generated/ (ver paths.py) generated/ diff --git a/AGENTS.md b/AGENTS.md index 70a3c24..2e39740 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,7 @@ - Install deps with `python -m pip install -r requirements.txt`. - Run the local app with `python main.py`; it serves FastAPI/Uvicorn at `http://127.0.0.1:8000` with reload enabled. - On Windows, `start.bat` runs `python main.py` in a new window and opens the browser; `stop.bat` kills any process using port `8000`. +- On macOS/Linux, the double-clickable `.command` files mirror the `.bat`s: run `setup_mac.command` once to bootstrap a `.venv` (Python 3.10+) and deps, then `start.command` / `stop.command` / `restart.command` / `start_persistent_profile.command`. They delegate the safe preflight/stop logic to the now cross-platform `runtime_control.py`. - There is no test, lint, or formatter config in this repo. For a syntax-only check, run `python -m py_compile main.py db.py ghl_client.py sync_engine.py script_runner.py` and add specific scripts as needed. - Run focused utility scripts directly, for example `python scripts\mp_contact_search.py ` or `python scripts\mp_opportunity_search.py `. - Global dashboard sync parallelism is controlled with `SYNC_ENGINE_MAX_WORKERS`; default is `12`, hard maximum is `20`. It affects the `Sincronizar Todo` button and processes multiple GHL locations in parallel. diff --git a/CLAUDE.md b/CLAUDE.md index 16ee24c..eb9e36f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,6 +9,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - `python -m pip install -r requirements.txt` — instalar dependencias. - `python main.py` — levanta FastAPI/Uvicorn en `http://127.0.0.1:8000` con reload. - `start.bat` / `stop.bat` / `restart.bat` (Windows) — lanzan/matan/reinician la app y liberan el puerto 8000. `restart.bat` detecta automáticamente si estaba en modo normal o perfil persistente (lee `generated/runtime/last_mode`), mata Chromium zombies y limpia batch files huérfanos en `generated/runtime/batch/`. +- **macOS / Linux** — equivalentes doble-clickeables en Finder con extensión `.command`: + - `setup_mac.command` — inicialización con un click (NO tiene equivalente Windows): detecta Python 3.10+, crea el venv `.venv`, instala `requirements.txt` y el Chromium de Playwright, y copia `.env.example` a `.env`. Idempotente. + - `start.command` / `stop.command` / `restart.command` / `start_persistent_profile.command` — espejo de los `.bat`. Lanzan el server con `nohup` en segundo plano (logs en `generated/logs/server.out`) usando el python del `.venv`. `stop.command` acepta `--force` igual que el `.bat`. + - `mp_common.sh` — helper compartido (sourced, no se ejecuta solo): resuelve la raíz del proyecto, localiza el python del venv y define los banners/utilidades. La lógica segura de preflight/stop la delegan todos a `runtime_control.py`, que ahora es cross-platform (`lsof`/`ps`/`pgrep`/`kill` en POSIX, `netstat`/PowerShell/`taskkill` en Windows; ver `IS_WINDOWS`). - `python -m py_compile main.py db.py ghl_client.py sync_engine.py script_runner.py paths.py` — único "lint" disponible (no hay test/lint/format configurado). - Scripts de utilidad se corren directo: `python scripts\.py [args]`. La mayoría leen `generated/data/mp_manager.sqlite` (vía `paths.DB_PATH`) y asumen una sync previa. - Variables de entorno: diff --git a/docs/PLAYWRIGHT_SESSION.md b/docs/PLAYWRIGHT_SESSION.md index dd44d23..3db607e 100644 --- a/docs/PLAYWRIGHT_SESSION.md +++ b/docs/PLAYWRIGHT_SESSION.md @@ -31,7 +31,7 @@ Los scripts soportan dos modos de persistencia. Se elige con la variable de ento ### Modo 2 — Perfil de Chrome persistente -- **Cuándo se usa**: cuando defines `GHL_BROWSER_PROFILE_DIR` apuntando a un directorio. Lo más fácil es lanzar el servidor con [start_persistent_profile.bat](../start_persistent_profile.bat). +- **Cuándo se usa**: cuando defines `GHL_BROWSER_PROFILE_DIR` apuntando a un directorio. Lo más fácil es lanzar el servidor con [start_persistent_profile.bat](../start_persistent_profile.bat) (Windows) o [start_persistent_profile.command](../start_persistent_profile.command) (macOS/Linux). - **Cómo funciona**: Playwright usa `launch_persistent_context()` con un perfil completo en disco — igual que un Chrome real. Persiste cookies HttpOnly, IndexedDB, cache, localStorage, service workers. - **Pros**: GHL trata el perfil como un "dispositivo" estable. Sesión mucho más duradera. Login dura semanas en vez de horas/días. - **Cons**: **no puedes correr dos scripts en paralelo** contra el mismo perfil — Chrome bloquea el directorio mientras un proceso lo usa. Si necesitas ejecuciones concurrentes (raro en este repo), no uses este modo. diff --git a/mp_common.sh b/mp_common.sh new file mode 100755 index 0000000..a9138ac --- /dev/null +++ b/mp_common.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------------------- +# mp_common.sh — utilidades compartidas por los launchers de macOS/Linux +# (start.command, stop.command, restart.command, start_persistent_profile.command +# y setup_mac.command). +# +# No se ejecuta directamente: los demás scripts lo cargan con `source`. +# Equivale a la lógica común que en Windows vive embebida en los .bat. +# --------------------------------------------------------------------------- + +# Raíz del proyecto = carpeta donde vive este archivo. Resuelve symlinks para +# que funcione aunque el .command se invoque desde otra ruta. +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do + DIR="$(cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd)" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" +done +PROJECT_DIR="$(cd -P "$(dirname "$SOURCE")" >/dev/null 2>&1 && pwd)" +cd "$PROJECT_DIR" || exit 1 + +VENV_DIR="$PROJECT_DIR/.venv" +VENV_PY="$VENV_DIR/bin/python" + +# Versión mínima de Python que exige el proyecto (3.10+). +PY_MIN_MAJOR=3 +PY_MIN_MINOR=10 + +# Colorea solo si la salida es una terminal interactiva. +if [ -t 1 ]; then + C_RESET="\033[0m"; C_BOLD="\033[1m"; C_RED="\033[31m"; C_YEL="\033[33m"; C_GRN="\033[32m" +else + C_RESET=""; C_BOLD=""; C_RED=""; C_YEL=""; C_GRN="" +fi + +info() { printf "${C_BOLD}[SISTEMA]${C_RESET} %s\n" "$1"; } +ok() { printf "${C_GRN}[OK]${C_RESET} %s\n" "$1"; } +warn() { printf "${C_YEL}[ADVERTENCIA]${C_RESET} %s\n" "$1"; } +err() { printf "${C_RED}[ERROR]${C_RESET} %s\n" "$1" >&2; } + +# Imprime un encabezado tipo banner como los .bat de Windows. +banner() { + echo "===================================================" + echo " $1" + echo "===================================================" + echo +} + +# True si "$1 --version" reporta >= PY_MIN. +_py_is_recent_enough() { + local cmd="$1" + "$cmd" -c "import sys; sys.exit(0 if sys.version_info[:2] >= ($PY_MIN_MAJOR, $PY_MIN_MINOR) else 1)" 2>/dev/null +} + +# Localiza un intérprete del sistema 3.10+ para crear el venv. Prueba nombres +# versionados explícitos (más fiables que 'python3' a secas en macOS, donde el +# 'python3' del sistema suele ser viejo). Imprime la ruta o vacío. +find_system_python() { + local candidates=(python3.13 python3.12 python3.11 python3.10 python3 python) + for c in "${candidates[@]}"; do + if command -v "$c" >/dev/null 2>&1 && _py_is_recent_enough "$c"; then + command -v "$c" + return 0 + fi + done + return 1 +} + +# Devuelve el python del venv si existe; si no, vacío. Los launchers que +# requieren dependencias instaladas deben llamar a require_venv en su lugar. +venv_python() { + if [ -x "$VENV_PY" ]; then + echo "$VENV_PY" + return 0 + fi + return 1 +} + +# Garantiza que el venv exista; si no, da instrucciones y aborta. Imprime la +# ruta del intérprete del venv por stdout (para capturarla con $(...)). +require_venv() { + if [ ! -x "$VENV_PY" ]; then + err "No se encontró el entorno virtual (.venv)." + err "Ejecuta primero (doble clic): setup_mac.command" + return 1 + fi + echo "$VENV_PY" +} + +# Pausa al final cuando el script se abrió con doble clic en Finder, para que la +# ventana de Terminal no se cierre de golpe y el usuario pueda leer el resultado. +# Si se ejecutó desde una terminal interactiva (uso avanzado), no estorba. +hold_window() { + echo + read -r -p "Presiona ENTER para cerrar esta ventana..." _ || true +} diff --git a/restart.command b/restart.command new file mode 100755 index 0000000..db9487b --- /dev/null +++ b/restart.command @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------------------- +# restart.command — Equivalente macOS/Linux de restart.bat. +# Reinicia el servidor detectando automáticamente si estaba en modo normal o +# perfil persistente (lee generated/runtime/last_mode), mata Chromium zombies +# de Playwright y limpia batch files huérfanos. +# --------------------------------------------------------------------------- +set -euo pipefail +source "$(dirname "${BASH_SOURCE[0]}")/mp_common.sh" + +banner "MP Manager - Reiniciando Servidor" + +PY="$(require_venv)" || { hold_window; exit 1; } + +# --- 1. Determinar modo activo (normal / persistent) ---------------------- +MODE="normal" +LAST_MODE_FILE="generated/runtime/last_mode" +if [ -f "$LAST_MODE_FILE" ]; then + MODE="$(tr -d '[:space:]' < "$LAST_MODE_FILE")" +fi +if [ "$MODE" != "normal" ] && [ "$MODE" != "persistent" ]; then + MODE="normal" +fi +info "Modo detectado: $MODE" + +# --- 2. Detener el server actual de forma segura -------------------------- +set +e +"$PY" runtime_control.py stop --force +set -e + +# --- 3. Limpiar Chromium headless huérfanos de Playwright ----------------- +# Solo matamos procesos cuya línea de comando apunta al Chromium de Playwright +# (ms-playwright / chrome-headless-shell), NUNCA el Google Chrome del usuario. +info "Limpiando zombies de Chromium headless de Playwright si los hay..." +pkill -f "ms-playwright.*[Cc]hromium" 2>/dev/null || true +pkill -f "chrome-headless-shell" 2>/dev/null || true + +# --- 4. Limpiar batch files interrumpidos del runtime --------------------- +if compgen -G "generated/runtime/batch/_bulk_batch_*.json" >/dev/null; then + info "Limpiando batch files huérfanos..." + rm -f generated/runtime/batch/_bulk_batch_*.json +fi +rm -f generated/runtime/batch/_test_batch.json 2>/dev/null || true + +# --- 5. Pequeña espera para liberar el puerto ----------------------------- +info "Esperando 2 segundos para liberar puerto..." +sleep 2 + +# --- 6. Relanzar en el modo correcto -------------------------------------- +mkdir -p generated/runtime generated/logs +LOG="$PROJECT_DIR/generated/logs/server.out" + +if [ "$MODE" = "persistent" ]; then + export GHL_BROWSER_PROFILE_DIR="$PROJECT_DIR/generated/browser/profile" + info "Relanzando en modo PERFIL PERSISTENTE." + info "GHL_BROWSER_PROFILE_DIR=$GHL_BROWSER_PROFILE_DIR" + if [ ! -d "$GHL_BROWSER_PROFILE_DIR" ]; then + echo + warn "El perfil persistente aún no existe." + warn 'Cuando arranque, usa "Renovar sesión Bucéfalo" en el dashboard.' + echo + fi + echo "persistent" > "$LAST_MODE_FILE" +else + info "Relanzando en modo NORMAL." + echo "normal" > "$LAST_MODE_FILE" +fi + +nohup "$PY" "$PROJECT_DIR/main.py" >> "$LOG" 2>&1 & +disown || true + +echo +echo "===================================================" +echo " Servidor reiniciado en modo $MODE." +echo " Logs: generated/logs/server.out" +echo " Para detener: stop.command" +echo "===================================================" +hold_window diff --git a/runtime_control.py b/runtime_control.py index d507e8c..0cbe69a 100644 --- a/runtime_control.py +++ b/runtime_control.py @@ -7,9 +7,10 @@ Centraliza la lógica de: * apagar el servidor de forma segura (sin matar procesos ajenos), * esperar a que el server termine de arrancar (server_info.json escrito). -Pensado para que los batch files (`start.bat`, `stop.bat`, `restart.bat`) lo -invoquen como CLI sin tener que hacer parsing de JSON o validaciones complejas -en lenguaje batch. +Pensado para que los launchers (Windows: `start.bat`/`stop.bat`/`restart.bat`; +macOS/Linux: `start.command`/`stop.command`/`restart.command`) lo invoquen como +CLI sin tener que hacer parsing de JSON o validaciones complejas en shell. La +lógica de OS (puertos, PIDs, kill) es cross-platform: ver `IS_WINDOWS`. Uso CLI: python runtime_control.py status # imprime estado actual @@ -22,6 +23,7 @@ from __future__ import annotations import json import os +import signal import socket import subprocess import sys @@ -35,6 +37,11 @@ import paths # proyecto Python). Lo buscamos en la línea de comando del proceso. PROJECT_MARKER = os.path.normcase(paths.BASE_DIR) +# Las primitivas de OS (descubrir quién ocupa un puerto, leer la línea de comando +# de un PID, matarlo) difieren entre Windows y POSIX (macOS/Linux). El resto del +# módulo es agnóstico: dispatcha según esta bandera. +IS_WINDOWS = os.name == "nt" + def is_port_in_use(port: int, host: str = "127.0.0.1") -> bool: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: @@ -47,6 +54,12 @@ def is_port_in_use(port: int, host: str = "127.0.0.1") -> bool: def get_pid_listening_on(port: int) -> Optional[int]: """Devuelve el PID que escucha en `port` (LISTENING), o None si nadie.""" + if IS_WINDOWS: + return _get_pid_listening_on_windows(port) + return _get_pid_listening_on_posix(port) + + +def _get_pid_listening_on_windows(port: int) -> Optional[int]: try: out = subprocess.check_output( ["netstat", "-ano"], text=True, stderr=subprocess.DEVNULL @@ -65,6 +78,27 @@ def get_pid_listening_on(port: int) -> Optional[int]: return None +def _get_pid_listening_on_posix(port: int) -> Optional[int]: + """macOS/Linux: `lsof` lista el/los PID que escuchan en el puerto. + + `-sTCP:LISTEN` acota a sockets en estado LISTEN, `-t` devuelve solo PIDs. + Si hay varios (p.ej. el reloader de uvicorn y su worker comparten socket), + devolvemos el primero; `_kill_tree` se encarga del árbol completo. + """ + try: + out = subprocess.check_output( + ["lsof", "-nP", f"-iTCP:{port}", "-sTCP:LISTEN", "-t"], + text=True, + stderr=subprocess.DEVNULL, + ) + except Exception: + return None + for line in out.split(): + if line.strip().isdigit(): + return int(line.strip()) + return None + + def _run_powershell(ps_command: str) -> Optional[str]: """Ejecuta un comando PowerShell y devuelve stdout (stripped) o None si falla. @@ -85,18 +119,43 @@ def _run_powershell(ps_command: str) -> Optional[str]: def get_process_cmdline(pid: int) -> Optional[str]: - """Devuelve la línea de comando de un PID en Windows, o None si no existe.""" - return _run_powershell( - f"(Get-CimInstance Win32_Process -Filter 'ProcessId={pid}' -ErrorAction SilentlyContinue).CommandLine" - ) + """Devuelve la línea de comando completa de un PID, o None si no existe.""" + if IS_WINDOWS: + return _run_powershell( + f"(Get-CimInstance Win32_Process -Filter 'ProcessId={pid}' -ErrorAction SilentlyContinue).CommandLine" + ) + # POSIX (macOS/Linux): `ps -o command=` imprime la línea de comando completa + # sin encabezado. En macOS el límite por defecto es amplio; suficiente para + # contener la ruta absoluta a main.py que es lo que buscamos. + try: + out = subprocess.check_output( + ["ps", "-p", str(pid), "-o", "command="], + text=True, + stderr=subprocess.DEVNULL, + ) + except Exception: + return None + return out.strip() or None def pid_is_alive(pid: int) -> bool: - """True si el PID existe. Más confiable que parsear tasklist.""" - result = _run_powershell( - f"if (Get-Process -Id {pid} -ErrorAction SilentlyContinue) {{ 'yes' }} else {{ 'no' }}" - ) - return result == "yes" + """True si el PID existe.""" + if IS_WINDOWS: + result = _run_powershell( + f"if (Get-Process -Id {pid} -ErrorAction SilentlyContinue) {{ 'yes' }} else {{ 'no' }}" + ) + return result == "yes" + # POSIX: señal 0 no mata; solo verifica existencia/permisos del proceso. + try: + os.kill(pid, 0) + return True + except ProcessLookupError: + return False + except PermissionError: + # Existe pero es de otro usuario; para nuestros fines, está vivo. + return True + except OSError: + return False def pid_belongs_to_mp_manager(pid: int) -> bool: @@ -106,7 +165,7 @@ def pid_belongs_to_mp_manager(pid: int) -> bool: registró este PID. Como ese archivo SOLO lo escribe `main.py` de este proyecto, el match es prueba inequívoca de identidad. - Fuente secundaria (cmdline): si los .bat lanzan `python "/main.py"`, + Fuente secundaria (cmdline): si los launchers lanzan `python "/main.py"`, el path del proyecto queda embebido en la línea de comando del proceso y podemos reconocerlo aunque el server_info.json esté ausente o stale. """ @@ -130,11 +189,16 @@ def get_process_cwd(pid: int) -> Optional[str]: informativo es la línea de comando completa, así que esta función es secundaria. """ - out = _run_powershell( - f"(Get-CimInstance Win32_Process -Filter 'ProcessId={pid}' -ErrorAction SilentlyContinue).ExecutablePath" - ) - if out: - return os.path.dirname(out) + if IS_WINDOWS: + out = _run_powershell( + f"(Get-CimInstance Win32_Process -Filter 'ProcessId={pid}' -ErrorAction SilentlyContinue).ExecutablePath" + ) + if out: + return os.path.dirname(out) + return None + # POSIX: no hay un cwd fiable sin /proc (Linux) y macOS no lo expone vía ps. + # La línea de comando completa es la pista útil para nosotros, así que esta + # función queda como best-effort y devuelve None. return None @@ -239,11 +303,40 @@ def stop_server(force: bool = False) -> int: def _kill_tree(pid: int) -> None: - subprocess.run( - ["taskkill", "/F", "/T", "/PID", str(pid)], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) + if IS_WINDOWS: + subprocess.run( + ["taskkill", "/F", "/T", "/PID", str(pid)], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + return + _kill_tree_posix(pid) + + +def _kill_tree_posix(pid: int) -> None: + """Mata recursivamente el proceso y sus hijos en macOS/Linux. + + uvicorn arranca con `reload=True`, lo que crea un proceso reloader padre y un + worker hijo. El PID registrado en server_info.json es el del padre, así que + hay que descender por el árbol (`pgrep -P`) para no dejar al worker zombie + aferrado al puerto. Enviamos SIGTERM (apagado limpio); el padre propaga la + señal a uvicorn que cierra el socket ordenadamente. + """ + try: + out = subprocess.check_output( + ["pgrep", "-P", str(pid)], text=True, stderr=subprocess.DEVNULL + ) + children = [int(c) for c in out.split() if c.strip().isdigit()] + except Exception: + children = [] + + for child in children: + _kill_tree_posix(child) + + try: + os.kill(pid, signal.SIGTERM) + except (ProcessLookupError, OSError): + pass def wait_ready(timeout_sec: float = 30.0) -> int: @@ -271,14 +364,14 @@ def preflight_check() -> int: if info and info.get("pid") and pid_is_alive(int(info["pid"])): port = info.get("port", 8000) print(f"[INFO] MP Manager ya esta corriendo en puerto {port} (PID {info['pid']}).") - print(f" Abre http://127.0.0.1:{port}/ o ejecuta stop.bat primero.") + print(f" Abre http://127.0.0.1:{port}/ o detén primero (stop.bat / stop.command).") return 1 port_pid = get_pid_listening_on(8000) if port_pid: if pid_belongs_to_mp_manager(port_pid): print(f"[INFO] Hay un MP Manager huerfano en puerto 8000 (PID {port_pid}).") - print(f" Ejecuta stop.bat o usa: python runtime_control.py stop --force") + print(f" Detén (stop.bat / stop.command) o usa: python runtime_control.py stop --force") return 1 cmd = get_process_cmdline(port_pid) or "(desconocido)" print(f"[ADVERTENCIA] Puerto 8000 ocupado por OTRO proyecto (PID {port_pid}):") diff --git a/setup_mac.command b/setup_mac.command new file mode 100755 index 0000000..6ecd645 --- /dev/null +++ b/setup_mac.command @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------------------- +# setup_mac.command — Inicialización con UN CLICK de MP Manager en macOS/Linux. +# +# Equivalente "de arranque" que NO existe en Windows (allá se asume Python en el +# PATH). En macOS el Python del sistema suele ser 3.9 y está bloqueado para pip +# (PEP 668), así que aquí: +# 1. Detecta un Python 3.10+ del sistema. +# 2. Crea un entorno virtual aislado en .venv (no toca el Python del sistema). +# 3. Instala las dependencias de requirements.txt. +# 4. Instala el navegador Chromium para Playwright. +# 5. Crea el archivo .env a partir de .env.example si no existe. +# +# Es idempotente: puedes volver a ejecutarlo para actualizar dependencias. +# Doble clic en Finder para correrlo. +# --------------------------------------------------------------------------- +set -euo pipefail +source "$(dirname "${BASH_SOURCE[0]}")/mp_common.sh" + +banner "MP Manager - Configuración inicial (macOS / Linux)" + +# --- 1. Buscar Python 3.10+ del sistema ----------------------------------- +info "Buscando un Python ${PY_MIN_MAJOR}.${PY_MIN_MINOR}+ en el sistema..." +SYS_PY="$(find_system_python || true)" +if [ -z "$SYS_PY" ]; then + err "No se encontró Python ${PY_MIN_MAJOR}.${PY_MIN_MINOR} o superior." + echo + echo " Instálalo y reintenta. La forma más simple en macOS:" + echo " brew install python@3.12" + echo " (o descárgalo de https://www.python.org/downloads/macos/)" + hold_window + exit 1 +fi +ok "Python encontrado: $SYS_PY ($("$SYS_PY" --version 2>&1))" + +# --- 2. Crear / reutilizar el entorno virtual ------------------------------ +if [ -x "$VENV_PY" ]; then + info "Entorno virtual ya existe en .venv (se reutiliza)." +else + info "Creando entorno virtual en .venv ..." + "$SYS_PY" -m venv "$VENV_DIR" + ok "Entorno virtual creado." +fi + +# --- 3. Instalar dependencias --------------------------------------------- +info "Actualizando pip ..." +"$VENV_PY" -m pip install --upgrade pip >/dev/null + +info "Instalando dependencias de requirements.txt (puede tardar) ..." +"$VENV_PY" -m pip install -r "$PROJECT_DIR/requirements.txt" +ok "Dependencias de Python instaladas." + +# --- 4. Navegador de Playwright ------------------------------------------- +# Los scripts ghl_browser_*.py automatizan la UI web con Chromium. Sin esto +# fallarían al lanzar el navegador. +info "Instalando Chromium para Playwright ..." +if "$VENV_PY" -m playwright install chromium; then + ok "Chromium instalado." +else + warn "No se pudo instalar Chromium para Playwright." + warn "El dashboard funcionará; solo los scripts de navegador (workflows) fallarán." + warn "Puedes reintentar luego con: .venv/bin/python -m playwright install chromium" +fi + +# --- 5. Archivo .env ------------------------------------------------------- +if [ -f "$PROJECT_DIR/.env" ]; then + info "Archivo .env ya existe (no se sobrescribe)." +elif [ -f "$PROJECT_DIR/.env.example" ]; then + cp "$PROJECT_DIR/.env.example" "$PROJECT_DIR/.env" + ok "Creado .env a partir de .env.example — revísalo y completa tus credenciales." +else + warn "No hay .env.example; omito la creación de .env." +fi + +echo +banner "Configuración completada" +echo " Para arrancar el servidor: doble clic en start.command" +echo " Para detenerlo: doble clic en stop.command" +echo " Para reiniciarlo: doble clic en restart.command" +echo +echo " La primera vez, macOS puede pedir permiso para abrir un .command" +echo " descargado: clic derecho > Abrir, o Ajustes > Privacidad y seguridad." +hold_window diff --git a/start.command b/start.command new file mode 100755 index 0000000..8b71617 --- /dev/null +++ b/start.command @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------------------- +# start.command — Equivalente macOS/Linux de start.bat. +# Arranca el servidor MP Manager en modo NORMAL. +# 1. Verifica que exista el venv (creado por setup_mac.command). +# 2. Pre-vuelo: rechaza relanzar si ya hay una instancia; avisa si el puerto +# 8000 lo tiene otro proyecto (main.py salta al siguiente libre). +# 3. Deja huella del modo (last_mode=normal) para que restart.command sepa +# cómo relanzar. +# 4. Lanza FastAPI en segundo plano (nohup) con la ruta ABSOLUTA a main.py +# —para que runtime_control pueda identificar el proceso— y registra logs. +# main.py escribe server_info.json con el puerto real y abre el navegador solo. +# --------------------------------------------------------------------------- +set -euo pipefail +source "$(dirname "${BASH_SOURCE[0]}")/mp_common.sh" + +banner "MP Manager - Iniciando Servidor Monte Providencia" + +PY="$(require_venv)" || { hold_window; exit 1; } + +# --- Pre-vuelo (misma lógica que Windows, vía runtime_control.py) ---------- +set +e +"$PY" runtime_control.py preflight +PRE=$? +set -e + +if [ "$PRE" -eq 1 ]; then + echo + info "No se relanza. Si quieres reiniciar usa restart.command." + hold_window + exit 1 +fi +if [ "$PRE" -eq 2 ]; then + echo + info "Continuando: MP Manager buscará el siguiente puerto libre." + echo +fi + +# --- Huella de modo activo ------------------------------------------------- +mkdir -p generated/runtime +echo "normal" > generated/runtime/last_mode + +# --- Lanzar servidor en segundo plano -------------------------------------- +mkdir -p generated/logs +LOG="$PROJECT_DIR/generated/logs/server.out" +info "Iniciando servidor FastAPI..." +nohup "$PY" "$PROJECT_DIR/main.py" >> "$LOG" 2>&1 & +disown || true + +echo +echo "===================================================" +echo " El servidor se está iniciando en segundo plano." +echo " El navegador se abrirá solo en el puerto correcto" +echo " (8000 o el siguiente libre)." +echo +echo " Logs: generated/logs/server.out" +echo " Para detener: stop.command" +echo " Reiniciar: restart.command" +echo "===================================================" +hold_window diff --git a/start_persistent_profile.command b/start_persistent_profile.command new file mode 100755 index 0000000..6c8b7c6 --- /dev/null +++ b/start_persistent_profile.command @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------------------- +# start_persistent_profile.command — Equivalente macOS/Linux de +# start_persistent_profile.bat. +# Arranca el servidor con el PERFIL PERSISTENTE de Chrome para Playwright +# (más estable que la sesión shared). Todos los scripts ghl_browser_*.py usarán +# generated/browser/profile en lugar de generated/browser/session.json. +# --------------------------------------------------------------------------- +set -euo pipefail +source "$(dirname "${BASH_SOURCE[0]}")/mp_common.sh" + +banner "MP Manager - Modo Perfil Persistente Bucéfalo" + +PY="$(require_venv)" || { hold_window; exit 1; } + +# --- Pre-vuelo ------------------------------------------------------------- +set +e +"$PY" runtime_control.py preflight +PRE=$? +set -e +if [ "$PRE" -eq 1 ]; then + echo + info "No se relanza. Usa restart.command para reiniciar el modo persistente." + hold_window + exit 1 +fi + +# --- Perfil persistente ---------------------------------------------------- +export GHL_BROWSER_PROFILE_DIR="$PROJECT_DIR/generated/browser/profile" +info "GHL_BROWSER_PROFILE_DIR=$GHL_BROWSER_PROFILE_DIR" + +if [ ! -d "$GHL_BROWSER_PROFILE_DIR" ]; then + echo + warn "El perfil aún no existe. La primera vez tendrás que generar la sesión:" + echo " 1. Ve a la pestaña Workflows y dale a \"Renovar sesión Bucéfalo\"." + echo " 2. Inicia sesión + MFA en la ventana del navegador." + echo " 3. La sesión se persistirá automáticamente en este directorio." + echo +fi + +info "Iniciando servidor FastAPI con perfil persistente activo..." +mkdir -p generated/runtime generated/logs +echo "persistent" > generated/runtime/last_mode + +LOG="$PROJECT_DIR/generated/logs/server.out" +nohup "$PY" "$PROJECT_DIR/main.py" >> "$LOG" 2>&1 & +disown || true + +echo +echo "===================================================" +echo " Servidor iniciado en modo perfil persistente." +echo " Logs: generated/logs/server.out" +echo " Para detener: stop.command" +echo +echo " NOTA: en este modo no puedes correr dos scripts de" +echo " Playwright al mismo tiempo contra el mismo perfil." +echo "===================================================" +hold_window diff --git a/stop.command b/stop.command new file mode 100755 index 0000000..df04cf2 --- /dev/null +++ b/stop.command @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------------------- +# stop.command — Equivalente macOS/Linux de stop.bat. +# Delega TODO a runtime_control.py para garantizar que solo se detiene MP +# Manager (nunca otro proyecto Python que escuche en el puerto 8000). +# +# Acepta los mismos flags que el .bat, p.ej.: +# ./stop.command --force (apaga también instancias huérfanas sin server_info) +# --------------------------------------------------------------------------- +set -euo pipefail +source "$(dirname "${BASH_SOURCE[0]}")/mp_common.sh" + +banner "MP Manager - Deteniendo Servidor Monte Providencia" + +PY="$(require_venv)" || { hold_window; exit 1; } + +set +e +"$PY" runtime_control.py stop "$@" +RC=$? +set -e + +hold_window +exit "$RC"