Skip to content

Commit ef26e73

Browse files
feat: add config diff command to show non-default settings
Closes #248
1 parent c1b4d75 commit ef26e73

3 files changed

Lines changed: 118 additions & 1 deletion

File tree

src/bernstein/cli/aliases.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,18 @@
44
55
Provides a Click Group subclass that resolves short aliases to
66
full command names, plus a registry of built-in aliases.
7+
User-defined aliases can be loaded from ``~/.bernstein/aliases.yaml``.
78
"""
89

910
from __future__ import annotations
1011

12+
import logging
13+
from pathlib import Path
14+
1115
import click
16+
import yaml
17+
18+
logger = logging.getLogger(__name__)
1219

1320
# ---------------------------------------------------------------------------
1421
# Alias registry
@@ -25,6 +32,11 @@
2532
"i": "overture", # init (hidden name: overture)
2633
}
2734

35+
# Track which aliases are user-defined (populated at load time)
36+
_USER_ALIASES: dict[str, str] = {}
37+
38+
_USER_ALIASES_PATH = Path.home() / ".bernstein" / "aliases.yaml"
39+
2840

2941
def get_alias(name: str) -> str | None:
3042
"""Return the full command name for an alias, or None.
@@ -43,6 +55,32 @@ def get_all_aliases() -> dict[str, str]:
4355
return dict(ALIASES)
4456

4557

58+
def _load_user_aliases() -> dict[str, str]:
59+
"""Load user-defined aliases from ~/.bernstein/aliases.yaml."""
60+
if not _USER_ALIASES_PATH.is_file():
61+
return {}
62+
try:
63+
with open(_USER_ALIASES_PATH) as f:
64+
data = yaml.safe_load(f) or {}
65+
if not isinstance(data, dict):
66+
return {}
67+
return {str(k): str(v) for k, v in data.items() if isinstance(k, str) and isinstance(v, str)}
68+
except Exception:
69+
logger.debug("Failed to load user aliases from %s", _USER_ALIASES_PATH, exc_info=True)
70+
return {}
71+
72+
73+
def _merge_aliases() -> None:
74+
"""Merge user aliases into the global registry (user overrides built-in)."""
75+
global _USER_ALIASES
76+
_USER_ALIASES = _load_user_aliases()
77+
ALIASES.update(_USER_ALIASES)
78+
79+
80+
# Call at module load time
81+
_merge_aliases()
82+
83+
4684
class AliasGroup(click.Group):
4785
"""Click Group that resolves short aliases to full command names.
4886
@@ -92,6 +130,7 @@ def aliases_cmd() -> None:
92130
table = Table(title="Command Aliases", show_header=True, header_style="bold cyan")
93131
table.add_column("Alias", style="green", width=10)
94132
table.add_column("Command", style="white", width=20)
133+
table.add_column("Source", style="dim", width=10)
95134
table.add_column("Description", style="dim")
96135

97136
_descriptions: dict[str, str] = {
@@ -107,7 +146,8 @@ def aliases_cmd() -> None:
107146

108147
for alias, command in sorted(ALIASES.items()):
109148
desc = _descriptions.get(alias, "")
110-
table.add_row(alias, command, desc)
149+
source = "[cyan]user[/cyan]" if alias in _USER_ALIASES else "[dim]built-in[/dim]"
150+
table.add_row(alias, command, source, desc)
111151

112152
console.print(table)
113153
console.print("\n[dim]Usage: bernstein <alias> [options][/dim]")
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Config diff command: show settings that differ from defaults.
2+
3+
CFG-010: CLI wrapper around core.config_diff_cmd.
4+
"""
5+
6+
from __future__ import annotations
7+
8+
from pathlib import Path
9+
10+
import yaml
11+
12+
13+
def _load_current_config() -> dict[str, object]:
14+
"""Load the current config from bernstein.yaml/yml."""
15+
for name in ("bernstein.yaml", "bernstein.yml"):
16+
p = Path.cwd() / name
17+
if p.is_file():
18+
with open(p) as f:
19+
data = yaml.safe_load(f) or {}
20+
if isinstance(data, dict):
21+
return data
22+
return {}
23+
24+
25+
def config_diff_cmd() -> None:
26+
"""Show settings that differ from defaults."""
27+
from rich.table import Table
28+
29+
from bernstein.cli.helpers import console
30+
from bernstein.core.config_diff_cmd import diff_against_defaults
31+
32+
current_yaml = _load_current_config()
33+
34+
if not current_yaml:
35+
console.print("[dim]No bernstein.yaml found in current directory.[/dim]")
36+
return
37+
38+
report = diff_against_defaults(current_yaml)
39+
40+
if not report.has_deviations:
41+
console.print("[green]All settings match defaults.[/green]")
42+
return
43+
44+
table = Table(title="Configuration diff", show_header=True, header_style="bold cyan")
45+
table.add_column("Setting", style="cyan")
46+
table.add_column("Kind", style="dim")
47+
table.add_column("Default", style="dim")
48+
table.add_column("Current", style="bold green")
49+
50+
for dev in report.deviations:
51+
kind_style = {
52+
"changed": "yellow",
53+
"added": "green",
54+
"removed": "red",
55+
}.get(dev.kind, "dim")
56+
table.add_row(
57+
dev.key,
58+
f"[{kind_style}]{dev.kind}[/{kind_style}]",
59+
str(dev.default_value) if dev.default_value is not None else "",
60+
str(dev.current_value) if dev.current_value is not None else "",
61+
)
62+
63+
console.print(table)
64+
console.print(
65+
f"\n{report.changed_count} changed, "
66+
f"{report.added_count} added, "
67+
f"{report.removed_count} removed "
68+
f"out of {report.total_keys} total settings."
69+
)

src/bernstein/cli/workspace_cmd.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,14 @@ def config_list(project_dir: str) -> None:
245245
console.print(table)
246246

247247

248+
@config_group.command("diff")
249+
def config_diff() -> None:
250+
"""Show settings that differ from defaults."""
251+
from bernstein.cli.config_diff_cli import config_diff_cmd as _diff_cmd
252+
253+
_diff_cmd()
254+
255+
248256
@config_group.command("validate")
249257
def config_validate() -> None:
250258
"""Validate project configuration (model policy, providers, etc.).

0 commit comments

Comments
 (0)