Coverage for src/lilbee/runtime/launcher.py: 100%
37 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"""Thin CLI entry point: shows splash animation while heavy deps load.
3This module imports only ``splash`` (which uses only stdlib + subprocess),
4launches the animation process, then performs the heavy
5``from lilbee.cli import app`` import while the bee animates on stderr.
6"""
8from __future__ import annotations
10import contextlib
11import io
12import os
13import sys
15# Shells fetch tab completions by running the console script with
16# ``_<PROG>_COMPLETE=...`` set (and an empty argv); click answers those itself.
17_COMPLETION_ENV_SUFFIX = "_COMPLETE"
20def _shell_completion_active() -> bool:
21 """True when this process was spawned by a shell's tab-completion."""
22 return any(
23 name.startswith("_") and name.endswith(_COMPLETION_ENV_SUFFIX) for name in os.environ
24 )
27def _force_utf8_stdio() -> None:
28 """Make stdio UTF-8 so a no-locale (GUI-spawned) launch can't crash on non-ASCII output."""
29 for stream in (sys.stdout, sys.stderr):
30 if isinstance(stream, io.TextIOWrapper):
31 # Swallow on an already-detached stream; non-TextIOWrapper streams
32 # (StringIO redirects, capture shims) need no reconfiguration.
33 with contextlib.suppress(ValueError, OSError):
34 stream.reconfigure(encoding="utf-8", errors="backslashreplace")
37def main() -> None:
38 """Entry point for the ``lilbee`` console script."""
39 _force_utf8_stdio()
40 args = sys.argv[1:]
41 # Help exits before the TUI, so it must not print through the animation.
42 wants_help = any(arg in ("--help", "-h") for arg in args)
43 is_interactive = (
44 (not args or args[0] in ("chat", "")) and not wants_help and not _shell_completion_active()
45 )
47 if not is_interactive:
48 from lilbee.cli import app
50 app()
51 return
53 from lilbee.app.services import mark_interactive_session
54 from lilbee.runtime.splash import start, stop
56 # A person is at the TUI for its whole lifetime, so hold the engine resident
57 # (no idle-unload) until they close lilbee. Recorded here, on the sole
58 # interactive entry point and before anything builds the services container,
59 # so a one-shot CLI or the MCP server never trips it.
60 mark_interactive_session()
62 handle = start()
64 try:
65 from lilbee.cli import app
66 except BaseException:
67 stop(handle)
68 raise
70 # The splash keeps animating through the CLI dispatch and the TUI stack's
71 # own imports (Textual and every screen), which on a cold disk take seconds;
72 # run_tui dismisses it right before Textual takes over the terminal.
73 try:
74 app()
75 except KeyboardInterrupt:
76 sys.stderr.write("\033[?25h")
77 sys.stderr.flush()
78 raise SystemExit(130) from None