Files
bincio-activity/bincio/dev.py
T
2026-04-09 08:59:40 +02:00

166 lines
5.3 KiB
Python

"""bincio dev — start the full local development environment.
Runs bincio serve (API) in a background thread and astro dev in the
foreground. One command replaces the two-terminal setup.
"""
from __future__ import annotations
import os
import subprocess
import sys
import threading
from pathlib import Path
from typing import Optional
import click
from rich.console import Console
console = Console()
def _find_site_dir(explicit: Optional[str]) -> Path:
if explicit:
p = Path(explicit).expanduser().resolve()
if not (p / "package.json").exists():
raise click.UsageError(f"No package.json in --site-dir {p}")
return p
for candidate in [Path.cwd() / "site", Path.cwd().parent / "site"]:
if (candidate / "package.json").exists():
return candidate
raise click.UsageError(
"Could not find the Astro site directory. Pass --site-dir."
)
def _find_data_dir(explicit: Optional[str]) -> Path:
if explicit:
return Path(explicit).expanduser().resolve()
auto_config = Path.cwd() / "extract_config.yaml"
if auto_config.exists():
import yaml
raw = yaml.safe_load(auto_config.read_text()) or {}
out = raw.get("output", {}).get("dir")
if out:
return Path(out).expanduser().resolve()
for candidate in [Path.cwd() / "bincio_data", Path.cwd().parent / "bincio_data"]:
if candidate.exists() and _user_dirs(candidate):
return candidate
raise click.UsageError(
"Could not find a data directory with user subdirectories. "
"Run `bincio extract` first, or pass --data-dir."
)
def _ensure_npm(site: Path) -> None:
if not (site / "node_modules").exists():
console.print("Running [cyan]npm install[/cyan]…")
subprocess.run(["npm", "install"], cwd=site, check=True)
def _user_dirs(data: Path) -> list[Path]:
return sorted(p for p in data.iterdir() if p.is_dir() and (p / "activities").exists())
def _merge_all_users(data: Path) -> None:
from bincio.render.cli import _merge_edits, _write_root_manifest
_merge_edits(data)
_write_root_manifest(data)
def _start_serve(data: Path, api_port: int, site: Path) -> None:
"""Start bincio serve in a background thread."""
import uvicorn
import bincio.serve.server as srv
srv.data_dir = data
srv.site_dir = site
config = uvicorn.Config(
srv.app,
host="127.0.0.1",
port=api_port,
log_level="warning", # quiet — astro dev output takes priority
)
server = uvicorn.Server(config)
server.run()
@click.command("dev")
@click.option("--data-dir", default=None, help="BAS data directory (must contain instance.db)")
@click.option("--site-dir", default=None, help="Astro project directory (default: ./site)")
@click.option("--port", default=4321, show_default=True, help="Astro dev server port")
@click.option("--api-port", default=4041, show_default=True, help="bincio serve API port")
def dev(
data_dir: Optional[str],
site_dir: Optional[str],
port: int,
api_port: int,
) -> None:
"""Start the local dev environment: bincio serve + astro dev.
Equivalent to running both servers manually in two terminals.
Requires `bincio init` to have been run first.
\b
Quick start:
uv run bincio init --data-dir ./data --handle you --password secret
uv run bincio extract --output ./data/you
uv run bincio dev --data-dir ./data
"""
data = _find_data_dir(data_dir)
site = _find_site_dir(site_dir)
has_auth = (data / "instance.db").exists()
console.print(f"[bold]bincio dev[/bold]")
console.print(f" Data: [cyan]{data}[/cyan]")
console.print(f" Site: [cyan]{site}[/cyan]")
if has_auth:
console.print(f" API: [cyan]http://127.0.0.1:{api_port}[/cyan]")
else:
console.print(f" Auth: [yellow]none[/yellow] (single-user, no instance.db)")
console.print(f" Browser: [cyan]http://localhost:{port}[/cyan]")
console.print()
_ensure_npm(site)
console.print("Merging sidecars…")
_merge_all_users(data)
# Symlink site/public/data → data dir
public_data = site / "public" / "data"
public_data.parent.mkdir(parents=True, exist_ok=True)
if public_data.is_symlink():
if public_data.resolve() != data.resolve():
public_data.unlink()
public_data.symlink_to(data)
elif not public_data.exists():
public_data.symlink_to(data)
# Start bincio serve only when instance.db exists (auth / write API)
if has_auth:
console.print(f"Starting [cyan]bincio serve[/cyan] on port {api_port}")
t = threading.Thread(target=_start_serve, args=(data, api_port, site), daemon=True)
t.start()
# Build env for astro dev
env = {
**os.environ,
"BINCIO_DATA_DIR": str(data),
"PUBLIC_EDIT_URL": "", # empty = proxy /api/* to bincio serve
"VITE_API_PORT": str(api_port), # picked up by astro.config.mjs if needed
}
# Start astro dev in foreground (Ctrl+C stops everything)
console.print(f"Starting [cyan]astro dev[/cyan] on port {port}")
console.print()
try:
subprocess.run(
["npm", "run", "dev", "--", "--port", str(port)],
cwd=site,
env=env,
)
except KeyboardInterrupt:
pass