Coverage for src/lilbee/cli/commands/settings.py: 100%
98 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-11-06 08:17 +0000
1"""Settings commands: list, get, set, and unset through the shared write boundary."""
3from __future__ import annotations
5from pathlib import Path
6from typing import TYPE_CHECKING, Any
8import typer
9from rich.table import Column, Table
10from rich.text import Text
12from lilbee.cli import theme
13from lilbee.cli.app import apply_overrides, console, data_dir_option, global_option
14from lilbee.cli.commands._shared import REBUILD_HINT, emit, line, run_or_fail, shown_value
15from lilbee.cli.tui import messages as msg
16from lilbee.core.config import cfg
18if TYPE_CHECKING:
19 from lilbee.app.settings import SettingInfo, SettingsUpdateResult
20 from lilbee.server.models import ConfigUpdateResponse
22settings_app = typer.Typer(
23 help="Show and change settings, with each value's source.", no_args_is_help=True
24)
26_key_argument = typer.Argument(help="A setting's name, as shown by 'lilbee settings list'.")
27_keys_argument = typer.Argument(
28 help="One or more settings to remove your value of; env, profile or default applies."
29)
32def _is_secret(key: str) -> bool:
33 from lilbee.app.settings_map import SETTINGS_MAP
35 definition = SETTINGS_MAP.get(key)
36 return definition is not None and definition.secret
39def _render_list(infos: list[SettingInfo]) -> None:
40 table = Table(Column("Key", no_wrap=True), "Group", "Value", "Source")
41 for info in infos:
42 source = info.source.value.replace("_", " ")
43 table.add_row(info.key, info.group.value, Text(shown_value(info.value)), source)
44 console.print(table)
47def _render_get(info: SettingInfo) -> None:
48 source = info.source.value.replace("_", " ")
49 line(f"{info.key} = {shown_value(info.value)}", theme.ACCENT)
50 line(f"source: {source}")
51 line(info.help_text, theme.MUTED)
54def _render_update(result: SettingsUpdateResult) -> None:
55 if result.reindex_required:
56 line(REBUILD_HINT, theme.WARNING)
57 for warning in result.warnings:
58 line(warning, theme.WARNING)
61def _update_response(result: SettingsUpdateResult) -> ConfigUpdateResponse:
62 from lilbee.server.models import ConfigUpdateResponse
64 return ConfigUpdateResponse.from_result(result)
67@settings_app.command(name="list")
68def settings_list(
69 group: str | None = typer.Option(
70 None, "--group", help="Filter to one group (case-insensitive); omit for every group."
71 ),
72 advanced: bool = typer.Option(
73 False,
74 "--advanced",
75 help="Also list the advanced settings (engine internals, timeouts, thresholds).",
76 ),
77 data_dir: Path | None = data_dir_option,
78 use_global: bool = global_option,
79) -> None:
80 """List the writable settings with their current value and source.
82 The advanced settings are left out unless you pass --advanced.
83 'get', 'set' and 'unset' work on any setting, advanced or not.
84 """
85 from lilbee.app.settings import config_read_failure_message, list_settings
86 from lilbee.server.models import SettingsListResponse
88 apply_overrides(data_dir=data_dir, use_global=use_global)
89 listed = run_or_fail(lambda: list_settings(group), config_read_failure_message)
90 infos = [info for info in listed if advanced or not info.advanced]
91 emit(SettingsListResponse.from_infos(infos), lambda: _render_list(infos))
94@settings_app.command(name="get")
95def settings_get(
96 key: str = _key_argument,
97 data_dir: Path | None = data_dir_option,
98 use_global: bool = global_option,
99) -> None:
100 """Show one setting's current value and source."""
101 from lilbee.app.settings import config_read_failure_message, get_setting
102 from lilbee.server.models import SettingValueResponse
104 apply_overrides(data_dir=data_dir, use_global=use_global)
105 info = run_or_fail(lambda: get_setting(key), config_read_failure_message)
106 emit(SettingValueResponse.from_info(info), lambda: _render_get(info))
109def _parse_cli_value(key: str, raw: str) -> Any:
110 """A list setting is one item per line; any other type is left to pydantic."""
111 from lilbee.app.settings import list_setting_value
112 from lilbee.core.config.schema import field_type_name
114 try:
115 is_list = field_type_name(key) == "list"
116 except KeyError:
117 is_list = False
118 return list_setting_value(raw) if is_list else raw
121def _stored_text(key: str, typed: str) -> str:
122 """What the write stored, on one line: a list joins its items, a blank value reads auto.
124 A bool shows as true, false or auto; any other setting echoes *typed*.
125 """
126 from lilbee.app.settings_map import SETTINGS_MAP
128 stored = getattr(cfg, key)
129 if isinstance(stored, list):
130 return ", ".join(str(item) for item in stored)
131 if stored is None and not typed.strip():
132 return "auto"
133 definition = SETTINGS_MAP.get(key)
134 if definition is None or definition.type is not bool:
135 return typed
136 return "auto" if stored is None else shown_value(stored)
139@settings_app.command(name="set")
140def settings_set(
141 key: str = _key_argument,
142 value: str = typer.Argument(help="The new value."),
143 data_dir: Path | None = data_dir_option,
144 use_global: bool = global_option,
145) -> None:
146 """Set a writable setting; model roles are refused (use 'lilbee model' instead)."""
147 from lilbee.app.settings import apply_settings_update, config_write_failure_message
149 apply_overrides(data_dir=data_dir, use_global=use_global)
150 parsed = _parse_cli_value(key, value)
151 result = run_or_fail(
152 lambda: apply_settings_update({key: parsed}, allow_model_roles=False),
153 config_write_failure_message,
154 )
155 shown = msg.MASKED_VALUE if _is_secret(key) and value else _stored_text(key, value)
157 def _render() -> None:
158 line(f"Set {key} to {shown}.", theme.ACCENT)
159 _render_update(result)
161 emit(_update_response(result), _render)
164@settings_app.command(name="unset")
165def settings_unset(
166 keys: list[str] = _keys_argument,
167 data_dir: Path | None = data_dir_option,
168 use_global: bool = global_option,
169) -> None:
170 """Remove your value of one or more settings; the next source applies."""
171 from lilbee.app.settings import config_write_failure_message, get_setting, reset_settings
173 apply_overrides(data_dir=data_dir, use_global=use_global)
174 result = run_or_fail(
175 lambda: reset_settings(keys, allow_model_roles=False), config_write_failure_message
176 )
178 def _render() -> None:
179 for key in result.updated:
180 try:
181 info = get_setting(key)
182 except ValueError:
183 line(f"{key}: removed your value (write-only; new value not shown)")
184 continue
185 source = info.source.value.replace("_", " ")
186 line(f"{key} = {shown_value(info.value)} ({source})")
187 _render_update(result)
189 emit(_update_response(result), _render)