"""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 "PUBLIC_EDIT_ENABLED": "true" if has_auth else "", # show edit/upload UI in VPS mode "PUBLIC_MOBILE_APP": "", # Record/Convert tabs off by default "VITE_API_PORT": str(api_port), # picked up by astro.config.mjs } # 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