Añade launchers de macOS/Linux con un click y hace runtime_control cross-platform

Equivalentes .command (doble-clic en Finder) de los .bat de Windows:
- setup_mac.command: bootstrap con un click (detecta Python 3.10+, crea .venv,
  instala requirements + Chromium de Playwright, copia .env.example -> .env).
- start/stop/restart/start_persistent_profile.command: espejo de los .bat,
  lanzan el server con nohup usando el python del .venv.
- mp_common.sh: helper compartido (raíz, venv, banners).

runtime_control.py ahora es cross-platform (IS_WINDOWS): lsof/ps/pgrep/kill en
POSIX, netstat/PowerShell/taskkill en Windows. _kill_tree_posix mata el árbol
padre+worker de uvicorn con SIGTERM.

.venv/ añadido a .gitignore. Docs actualizadas (CLAUDE.md, AGENTS.md,
PLAYWRIGHT_SESSION.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 15:02:47 -06:00
parent a35d26fac0
commit 2a37a4ffbf
11 changed files with 525 additions and 26 deletions
+3
View File
@@ -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/
+1
View File
@@ -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 <query>` or `python scripts\mp_opportunity_search.py <query-or-status>`.
- 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.
+4
View File
@@ -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\<nombre>.py [args]`. La mayoría leen `generated/data/mp_manager.sqlite` (vía `paths.DB_PATH`) y asumen una sync previa.
- Variables de entorno:
+1 -1
View File
@@ -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.
Executable
+96
View File
@@ -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
}
+78
View File
@@ -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
+118 -25
View File
@@ -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 "<ruta absoluta>/main.py"`,
Fuente secundaria (cmdline): si los launchers lanzan `python "<ruta absoluta>/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}):")
+83
View File
@@ -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
Executable
+60
View File
@@ -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
+58
View File
@@ -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
Executable
+23
View File
@@ -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"