unify single user and multi user behaviour
This commit is contained in:
@@ -17,6 +17,7 @@ from bincio.edit.cli import edit # noqa: E402
|
||||
from bincio.import_.cli import import_group # noqa: E402
|
||||
from bincio.serve.init_cmd import init # noqa: E402
|
||||
from bincio.serve.cli import serve # noqa: E402
|
||||
from bincio.dev import dev # noqa: E402
|
||||
|
||||
main.add_command(extract)
|
||||
main.add_command(render)
|
||||
@@ -24,3 +25,4 @@ main.add_command(edit)
|
||||
main.add_command(import_group)
|
||||
main.add_command(init)
|
||||
main.add_command(serve)
|
||||
main.add_command(dev)
|
||||
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
"""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
|
||||
@@ -342,6 +342,10 @@ def _resolve_config(
|
||||
cfg.input_dirs = [Path(input_dir).expanduser()]
|
||||
if output_dir:
|
||||
cfg.output_dir = Path(output_dir).expanduser()
|
||||
# Always write into {data_root}/{handle}/ so the data dir is always
|
||||
# instance-rooted and single/multi-user share the same layout.
|
||||
if cfg.output_dir.name != cfg.owner_handle:
|
||||
cfg.output_dir = cfg.output_dir / cfg.owner_handle
|
||||
return cfg
|
||||
|
||||
|
||||
|
||||
+11
-32
@@ -70,10 +70,6 @@ def _ensure_npm(site: Path) -> None:
|
||||
subprocess.run(["npm", "install"], cwd=site, check=True)
|
||||
|
||||
|
||||
def _is_multiuser(data: Path) -> bool:
|
||||
return (data / "instance.db").exists()
|
||||
|
||||
|
||||
def _user_dirs(data: Path) -> list[Path]:
|
||||
"""Return all per-user subdirectories (contain an activities/ dir)."""
|
||||
return sorted(
|
||||
@@ -86,21 +82,14 @@ def _merge_edits(data: Path, handle: str | None = None) -> None:
|
||||
"""Run the sidecar merge step for one user or all users."""
|
||||
from bincio.render.merge import merge_all
|
||||
|
||||
if _is_multiuser(data):
|
||||
targets = [data / handle] if handle else _user_dirs(data)
|
||||
total = 0
|
||||
for user_dir in targets:
|
||||
n = merge_all(user_dir)
|
||||
total += n
|
||||
console.print(f" [cyan]{user_dir.name}[/cyan]: {n} sidecar(s) merged")
|
||||
if not total:
|
||||
console.print("No sidecars found — _merged/ dirs mirror extracted data.")
|
||||
else:
|
||||
n = merge_all(data)
|
||||
if n:
|
||||
console.print(f"Merged [cyan]{n}[/cyan] sidecar edit(s) into _merged/")
|
||||
else:
|
||||
console.print("No sidecars found — _merged/ mirrors extracted data.")
|
||||
targets = [data / handle] if handle else _user_dirs(data)
|
||||
total = 0
|
||||
for user_dir in targets:
|
||||
n = merge_all(user_dir)
|
||||
total += n
|
||||
console.print(f" [cyan]{user_dir.name}[/cyan]: {n} sidecar(s) merged")
|
||||
if not total:
|
||||
console.print("No sidecars found — _merged/ dirs mirror extracted data.")
|
||||
|
||||
|
||||
def _write_root_manifest(data: Path) -> None:
|
||||
@@ -138,13 +127,8 @@ def _write_root_manifest(data: Path) -> None:
|
||||
|
||||
|
||||
def _link_data(site: Path, data: Path) -> None:
|
||||
"""Symlink site/public/data → data (multi-user) or data/_merged/ (single-user)."""
|
||||
if _is_multiuser(data):
|
||||
# Multi-user: link to data root directly (each user has their own _merged/)
|
||||
target = data
|
||||
else:
|
||||
merged = data / "_merged"
|
||||
target = merged if merged.exists() else data
|
||||
"""Symlink site/public/data → data root (each user has their own _merged/)."""
|
||||
target = data
|
||||
public_data = site / "public" / "data"
|
||||
public_data.parent.mkdir(parents=True, exist_ok=True)
|
||||
if public_data.is_symlink():
|
||||
@@ -193,14 +177,9 @@ def render(
|
||||
console.print(f"Site: [cyan]{site}[/cyan]")
|
||||
console.print(f"Data: [cyan]{data}[/cyan]")
|
||||
|
||||
multiuser = _is_multiuser(data)
|
||||
if multiuser:
|
||||
console.print("[cyan]Multi-user mode[/cyan]")
|
||||
|
||||
_ensure_npm(site)
|
||||
_merge_edits(data, handle=handle)
|
||||
if multiuser:
|
||||
_write_root_manifest(data)
|
||||
_write_root_manifest(data)
|
||||
_link_data(site, data)
|
||||
|
||||
env = {**os.environ, "BINCIO_DATA_DIR": str(data)}
|
||||
|
||||
+12
-9
@@ -152,7 +152,7 @@ async def me(bincio_session: Optional[str] = Cookie(default=None)) -> JSONRespon
|
||||
|
||||
|
||||
@app.post("/api/auth/login")
|
||||
async def login(request: Request, response: Response) -> JSONResponse:
|
||||
async def login(request: Request) -> JSONResponse:
|
||||
ip = request.client.host if request.client else "unknown"
|
||||
_check_rate_limit(ip)
|
||||
|
||||
@@ -165,22 +165,24 @@ async def login(request: Request, response: Response) -> JSONResponse:
|
||||
raise HTTPException(401, "Invalid credentials")
|
||||
|
||||
token = create_session(_get_db(), handle)
|
||||
_set_session_cookie(response, token)
|
||||
return JSONResponse({"ok": True, "handle": user.handle, "display_name": user.display_name})
|
||||
resp = JSONResponse({"ok": True, "handle": user.handle, "display_name": user.display_name})
|
||||
_set_session_cookie(resp, token)
|
||||
return resp
|
||||
|
||||
|
||||
@app.post("/api/auth/logout")
|
||||
async def logout(response: Response, bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse:
|
||||
async def logout(bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse:
|
||||
if bincio_session:
|
||||
delete_session(_get_db(), bincio_session)
|
||||
response.delete_cookie(_SESSION_COOKIE)
|
||||
return JSONResponse({"ok": True})
|
||||
resp = JSONResponse({"ok": True})
|
||||
resp.delete_cookie(_SESSION_COOKIE)
|
||||
return resp
|
||||
|
||||
|
||||
# ── Registration ──────────────────────────────────────────────────────────────
|
||||
|
||||
@app.post("/api/register")
|
||||
async def register(request: Request, response: Response) -> JSONResponse:
|
||||
async def register(request: Request) -> JSONResponse:
|
||||
body = await request.json()
|
||||
code = body.get("code", "").strip().upper()
|
||||
handle = body.get("handle", "").strip().lower()
|
||||
@@ -207,8 +209,9 @@ async def register(request: Request, response: Response) -> JSONResponse:
|
||||
(dd / handle / "edits").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
token = create_session(_get_db(), handle)
|
||||
_set_session_cookie(response, token)
|
||||
return JSONResponse({"ok": True, "handle": handle})
|
||||
resp = JSONResponse({"ok": True, "handle": handle})
|
||||
_set_session_cookie(resp, token)
|
||||
return resp
|
||||
|
||||
|
||||
# ── Invites ───────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user