Coverage for src/lilbee/cli/launchers/setup_gate.py: 100%

38 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-08-14 11:46 +0000

1"""First-run consent gate shared by launchers that write outside lilbee's dirs.""" 

2 

3from __future__ import annotations 

4 

5import json 

6import os 

7import sys 

8import tempfile 

9from collections.abc import Callable 

10from pathlib import Path 

11 

12import typer 

13 

14from lilbee.core.config import cfg 

15 

16 

17def _marker_path(marker_name: str) -> Path: 

18 """lilbee's record that a client's setup already ran (so launch doesn't re-prompt).""" 

19 return cfg.data_dir / "launchers" / marker_name 

20 

21 

22def _record_setup(marker_name: str) -> None: 

23 """Persist that the user accepted setup; idempotent (atomic write).""" 

24 path = _marker_path(marker_name) 

25 path.parent.mkdir(parents=True, exist_ok=True) 

26 tmp_name: str | None = None 

27 try: 

28 with tempfile.NamedTemporaryFile(dir=path.parent, suffix=".tmp", delete=False) as tmp: 

29 tmp_name = tmp.name 

30 tmp.write(json.dumps({"accepted": True}).encode("utf-8")) 

31 os.replace(tmp_name, path) 

32 except BaseException: 

33 if tmp_name is not None: 

34 Path(tmp_name).unlink(missing_ok=True) 

35 raise 

36 

37 

38def _is_interactive() -> bool: 

39 """True when stdin is a TTY, so a confirmation prompt can be answered.""" 

40 return sys.stdin.isatty() 

41 

42 

43def confirm_first_run_setup( 

44 *, 

45 marker_name: str, 

46 client_name: str, 

47 print_plan: Callable[[], None], 

48 assume_yes: bool, 

49) -> bool: 

50 """Prompt before a client's first setup; True means proceed. 

51 

52 Skipped when already recorded, when *assume_yes* is set, or when stdin is 

53 not a TTY (scripts/CI: invoking the launch is the consent there). The 

54 choice is remembered so later launches don't re-prompt. 

55 """ 

56 if _marker_path(marker_name).exists(): 

57 return True 

58 print_plan() 

59 if assume_yes or not _is_interactive(): 

60 _record_setup(marker_name) 

61 return True 

62 if not typer.confirm(f"Proceed with {client_name} setup?", default=True): 

63 typer.secho(f"Skipped {client_name} setup.", fg=typer.colors.YELLOW) 

64 return False 

65 _record_setup(marker_name) 

66 return True