feat: detect stale config and prompt merge-or-overwrite on save
Records the file mtime at load time; before write_config() fires, re-checks it. If it changed (e.g. `claude mcp add`, a second BCC window, or Claude itself writing ~/.claude.json), StaleDialog prompts with the changed top-level key names and a masked server-section diff. "Merge & save" applies the user's in-memory server edits on top of the current on-disk file (preserving external non-server changes); "Overwrite anyway" proceeds as before. - bcc_core: config_mtime(), external_change_summary(), _server_sections() helper extracted from backup_diff for reuse - bcc.py: StaleDialog, MainWindow._loaded_mtime tracked through load/save - tests: 7 new tests (config_mtime, external_change_summary variants, AC merge test) Closes #4 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+48
-6
@@ -290,6 +290,14 @@ def _redact_servers_block(block: dict | None) -> dict:
|
||||
return {name: _redact_server_data(data) for name, data in block.items()}
|
||||
|
||||
|
||||
def _server_sections(cfg: dict) -> dict:
|
||||
"""Return the masked server sections of a config dict, safe for diff display."""
|
||||
out: dict = {"mcpServers": _redact_servers_block(cfg.get("mcpServers"))}
|
||||
if DISABLED_KEY in cfg:
|
||||
out[DISABLED_KEY] = _redact_servers_block(cfg.get(DISABLED_KEY))
|
||||
return out
|
||||
|
||||
|
||||
def backup_diff(
|
||||
config_path: Path | str,
|
||||
backup_path: Path | str,
|
||||
@@ -318,12 +326,6 @@ def backup_diff(
|
||||
after_cfg = dict(current_cfg)
|
||||
apply_servers(after_cfg, backup_servers)
|
||||
|
||||
def _server_sections(cfg: dict) -> dict:
|
||||
out: dict = {"mcpServers": _redact_servers_block(cfg.get("mcpServers"))}
|
||||
if DISABLED_KEY in cfg:
|
||||
out[DISABLED_KEY] = _redact_servers_block(cfg.get(DISABLED_KEY))
|
||||
return out
|
||||
|
||||
before_lines = (
|
||||
json.dumps(_server_sections(current_cfg), indent=2, ensure_ascii=False) + "\n"
|
||||
).splitlines(keepends=True)
|
||||
@@ -370,6 +372,46 @@ def restore_backup(
|
||||
return write_config(p, cfg)
|
||||
|
||||
|
||||
def config_mtime(path: Path | str) -> float | None:
|
||||
"""Return the file's mtime, or None if the file does not exist."""
|
||||
try:
|
||||
return Path(path).stat().st_mtime
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def external_change_summary(original_cfg: dict, path: Path | str) -> tuple[list[str], str]:
|
||||
"""
|
||||
Compare original_cfg (what BCC loaded) with the current on-disk state.
|
||||
|
||||
Returns:
|
||||
changed_keys — sorted list of top-level keys whose values differ.
|
||||
server_diff — masked unified diff of server sections (empty if unchanged
|
||||
or the file cannot be read).
|
||||
"""
|
||||
try:
|
||||
disk_cfg = load_config(Path(path))
|
||||
except Exception:
|
||||
return [], ""
|
||||
|
||||
all_keys = set(original_cfg) | set(disk_cfg)
|
||||
changed_keys = sorted(k for k in all_keys if original_cfg.get(k) != disk_cfg.get(k))
|
||||
|
||||
server_diff = ""
|
||||
if any(k in {"mcpServers", DISABLED_KEY} for k in changed_keys):
|
||||
before_lines = (
|
||||
json.dumps(_server_sections(original_cfg), indent=2, ensure_ascii=False) + "\n"
|
||||
).splitlines(keepends=True)
|
||||
after_lines = (
|
||||
json.dumps(_server_sections(disk_cfg), indent=2, ensure_ascii=False) + "\n"
|
||||
).splitlines(keepends=True)
|
||||
server_diff = "".join(
|
||||
difflib.unified_diff(before_lines, after_lines, fromfile="loaded", tofile="on disk now")
|
||||
)
|
||||
|
||||
return changed_keys, server_diff
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Paste / import parsing
|
||||
# --------------------------------------------------------------------------- #
|
||||
|
||||
Reference in New Issue
Block a user