Coverage for src/lilbee/cli/launchers/config_file.py: 100%
18 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 11:46 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 11:46 +0000
1"""Load and atomically write an agent's on-disk config, refusing to clobber a corrupt file."""
3from __future__ import annotations
5from collections.abc import Callable
6from pathlib import Path
7from typing import Any
9import typer
11from lilbee.core.security import write_private_text
14def load_config_dict(
15 path: Path,
16 *,
17 parse: Callable[[str], Any],
18 parse_error: type[Exception],
19 label: str,
20) -> dict[str, Any]:
21 """Return the parsed mapping at *path*, or ``{}`` when absent or empty.
23 Exits non-zero without writing when the file does not parse, so a corrupt
24 user config is never overwritten."""
25 if not path.exists():
26 return {}
27 raw = path.read_text(encoding="utf-8")
28 try:
29 parsed = parse(raw)
30 except parse_error as exc:
31 typer.secho(
32 f"Your {label} did not parse, so lilbee will not overwrite it. "
33 "Fix or remove it, then retry.",
34 err=True,
35 fg=typer.colors.RED,
36 )
37 raise typer.Exit(1) from exc
38 return parsed if isinstance(parsed, dict) else {}
41def atomic_write_text(path: Path, text: str) -> None:
42 """Write *text* to *path* atomically (temp file + os.replace), creating parents.
44 Agent configs carry the lilbee bearer token, so they get the same
45 owner-only treatment as the other secret files. This already wrote through
46 a temp file, which is created 0600 and keeps that mode across the replace;
47 sharing the one implementation just makes that explicit.
48 """
49 write_private_text(path, text)