@serve_service.command("install")
@click.option("--host", default=DEFAULT_HOST, show_default=True)
@click.option("--port", default=DEFAULT_PORT, show_default=True, type=int)
@click.option(
"--maillage-reseau",
is_flag=True,
help="Ouvrir un SECOND socket pour que vos autres appareils atteignent "
"ce Mac. Dix routes du maillage y sont exposées, créance d'appareil "
"exigée ; le chat, la voix et Succès restent sur la loopback.",
)
@click.option(
"--lan-port",
default=8001,
show_default=True,
type=int,
help="Port du second socket, celui du maillage.",
)
@click.option("--allow-network", is_flag=True, hidden=True)
def install(
host: str,
port: int,
maillage_reseau: bool,
lan_port: int,
allow_network: bool,
) -> None:
"""Run the API server at login, in the background."""
_require_macos()
from diapason.desktop import launch_agent
if allow_network:
# Cette option mettait les DEUX CENT DIX routes sur le réseau. Elle
# existait pour un besoin légitime — « un téléphone ne peut pas
# joindre 127.0.0.1 » — auquel il n'y avait alors pas d'autre
# réponse. Il y en a une depuis : un second socket qui ne porte que
# neuf routes. Le besoin étant servi, l'échappatoire se referme.
#
# Elle échoue au lieu d'être un alias silencieux : la même commande
# ne doit pas se mettre à faire autre chose sans le dire.
click.echo(
"--allow-network n'existe plus : elle exposait l'API ENTIÈRE au "
"réseau, et c'est ainsi que ce Mac a servi deux cent dix routes "
"sur le Wi-Fi jusqu'au 26 août 2026.\n"
" • Pour que vos autres appareils atteignent ce Mac : "
"--maillage-reseau, qui n'expose que les neuf routes du "
"maillage, créance d'appareil exigée.\n"
" • L'application complète reste sur 127.0.0.1, toujours.",
err=True,
)
sys.exit(1)
if host not in ("127.0.0.1", "localhost", "::1"):
# Plus d'échappatoire. Le seul motif qu'on lui connaissait a
# désormais sa propre porte, plus étroite et mieux gardée.
click.echo(
f"Refusing to bind {host!r} from a background service: it would "
"expose the API beyond this machine.\n"
" • Pour un usage local : gardez 127.0.0.1.\n"
" • Pour que vos autres appareils atteignent ce Mac : gardez "
"127.0.0.1 ET ajoutez --maillage-reseau.",
err=True,
)
sys.exit(1)
if maillage_reseau and lan_port == port:
click.echo(
f"--lan-port et --port valent tous deux {port} : deux serveurs "
"sur le même port se lient en silence sur macOS et échouent sur "
"Linux.",
err=True,
)
sys.exit(1)
if maillage_reseau:
click.echo(
f"⚠ Le maillage écoutera sur 0.0.0.0:{lan_port} : toute machine "
"de votre réseau local pourra l'atteindre. Dix routes, "
"créance d'appareil exigée — mais un réseau partagé reste un "
"réseau partagé.",
err=True,
)
# Installer, c'est LANCER : le plist porte RunAtLoad. Poser un service sur
# un port déjà servi crée exactement le doublon silencieux que le reste de
# ce chantier traque — les deux se disputent le port, le plus spécifique
# gagne le routage, l'autre devient un zombie muet, et rien ne le dit.
#
# Réinstaller par-dessus SON PROPRE service reste permis : launchd remplace
# un job de même étiquette, il n'en empile pas un second.
from diapason.core import ports
etat, detail = ports.port_state(port)
if etat == ports.OCCUPE:
notre_pid = launch_agent.job_pid(launch_agent.SERVE_LABEL)
a_nous = notre_pid is not None and f"PID {notre_pid} " in f"{detail} "
if not a_nous:
click.echo(
f"Port {port} is already served by: {detail}\n"
"Installing would start a second server on the same port, and "
"neither would report an error.\n"
"Stop that one first, or install on another port with --port.",
err=True,
)
sys.exit(1)
click.echo(f"↻ Remplacement du service existant (PID {notre_pid}).", err=True)
elif etat == ports.INCONNU:
# Ici on avertit sans refuser : contrairement à `diapason start`, qui
# est répété au quotidien, une installation est un geste délibéré et
# rare. La bloquer parce que lsof manque empêcherait toute mise en
# place sur un système minimal.
click.echo(
f"⚠ Impossible de vérifier si le port {port} est libre ({detail}).\n"
f" Vérifiez à la main : lsof -nP -iTCP:{port} -sTCP:LISTEN",
err=True,
)
path = launch_agent.install(
label=launch_agent.SERVE_LABEL,
log_prefix="serve",
args=[
sys.executable,
"-m",
"diapason.cli",
"serve",
"--host",
host,
"--port",
str(port),
]
+ (
["--lan-host", "0.0.0.0", "--lan-port", str(lan_port)]
if maillage_reseau
else []
),
)
click.echo(f"Installed LaunchAgent → {path}")
if launch_agent.is_loaded(launch_agent.SERVE_LABEL):
click.echo(f"API server running at http://{host}:{port} — and at every login.")
else:
click.echo(
"WARNING: launchd did not accept the job. Check "
"`diapason serve-service logs`.",
err=True,
)