diff --git a/pauldot/cli.py b/pauldot/cli.py index 164c48d..68c8b9e 100644 --- a/pauldot/cli.py +++ b/pauldot/cli.py @@ -49,11 +49,22 @@ console = rich_console.Console() +def _maybe_commit(repo_path: pathlib.Path, message: str) -> None: + """Commit repo changes when auto_commit is enabled. Silently no-ops on any error.""" + try: + cfg = config.load_pauldot_config(repo_path) + if cfg.git.auto_commit: + git.commit(repo_path, message) + console.print("✓ Committed to dotfiles repo.") + except (FileNotFoundError, RuntimeError): + pass + + def _print_apply_result(result: pauldot_apply.ApplyResult, dry_run: bool) -> None: display.print_zshrc_result(result.zshrc, dry_run) display.print_dotfile_apply_results(result.dotfiles, dry_run=dry_run) if result.tools: - cmd_tool.print_tool_results(result.tools) + display.print_tool_results(result.tools) def _gather_repo_url() -> str | None: @@ -184,12 +195,7 @@ def status() -> None: if profile.dotfiles: drift = dotfiles.status(profile.dotfiles, home, repo_path) display.print_dotfile_status_results(drift) - except FileNotFoundError: - pass - - try: - ss = state.load_state() - display.print_status_attention(ss) + display.print_status_attention(s) except FileNotFoundError: pass @@ -237,13 +243,7 @@ def track( console.print(f"✓ Tracking ~/{home_rel} in profile '{s.active_profile}'.") console.print(f" Repo copy: {repo_file.relative_to(repo_path)}") - try: - cfg = config.load_pauldot_config(repo_path) - if cfg.git.auto_commit: - git.commit(repo_path, f"pauldot: track {home_rel}") - console.print("✓ Committed to dotfiles repo.") - except FileNotFoundError, RuntimeError: - pass # auto-commit is best-effort + _maybe_commit(repo_path, f"pauldot: track {home_rel}") @app.command() @@ -505,13 +505,7 @@ def absorb( if not result.lines or dry_run: return - try: - cfg = config.load_pauldot_config(repo_path) - if cfg.git.auto_commit: - git.commit(repo_path, "pauldot: absorb zshrc modifications") - console.print("✓ Committed to dotfiles repo.") - except FileNotFoundError, RuntimeError: - pass # auto-commit is best-effort + _maybe_commit(repo_path, "pauldot: absorb zshrc modifications") @app.command() @@ -543,12 +537,5 @@ def migrate( if dry_run: return - try: - cfg = config.load_pauldot_config(repo_path) - if cfg.git.auto_commit: - git.commit(repo_path, "pauldot: migrate existing zshrc") - console.print("✓ Committed to dotfiles repo.") - except FileNotFoundError, RuntimeError: - pass # auto-commit is best-effort - + _maybe_commit(repo_path, "pauldot: migrate existing zshrc") console.print("\nReview the changes, then run `pauldot apply` when ready.") diff --git a/pauldot/commands/alias.py b/pauldot/commands/alias.py index 6f479dd..849cbca 100644 --- a/pauldot/commands/alias.py +++ b/pauldot/commands/alias.py @@ -17,6 +17,17 @@ _ALIAS_PREFIX = "alias " +def _maybe_commit(repo_path: pathlib.Path, message: str) -> None: + """Commit repo changes when auto_commit is enabled. Silently no-ops on any error.""" + try: + cfg = config.load_pauldot_config(repo_path) + if cfg.git.auto_commit: + git.commit(repo_path, message) + console.print("✓ Committed to dotfiles repo.") + except (FileNotFoundError, RuntimeError): + pass + + def _aliases_file(repo_path: pathlib.Path) -> pathlib.Path: return repo_path / "files" / "aliases.zsh" @@ -98,13 +109,7 @@ def alias_add( console.print(f'✓ Added alias {key}="{value}"') - try: - cfg = config.load_pauldot_config(repo_path) - if cfg.git.auto_commit: - git.commit(repo_path, f"pauldot: add alias {key}") - console.print("✓ Committed to dotfiles repo.") - except FileNotFoundError, RuntimeError: - pass # auto-commit is best-effort + _maybe_commit(repo_path, f"pauldot: add alias {key}") try: result = pauldot_apply.run(pathlib.Path.home()) @@ -159,13 +164,7 @@ def alias_remove( console.print(f"✓ Removed alias '{key}' from {', '.join(sources)}") - try: - cfg = config.load_pauldot_config(repo_path) - if cfg.git.auto_commit: - git.commit(repo_path, f"pauldot: remove alias {key}") - console.print("✓ Committed to dotfiles repo.") - except FileNotFoundError, RuntimeError: - pass # auto-commit is best-effort + _maybe_commit(repo_path, f"pauldot: remove alias {key}") try: result = pauldot_apply.run(pathlib.Path.home()) diff --git a/pauldot/commands/tool.py b/pauldot/commands/tool.py index 6fb87b5..3c91798 100644 --- a/pauldot/commands/tool.py +++ b/pauldot/commands/tool.py @@ -8,34 +8,22 @@ from rich import table as rich_table from rich import text as rich_text -from pauldot import config, git, profiles, shell, state, tools +from pauldot import config, display, git, profiles, shell, state, tools tool_app = typer.Typer() console = rich_console.Console() -_TOOL_ACTION_LABELS: dict[str, tuple[str, str]] = { - "installed": ("✓ installed", "green"), - "already_installed": ("✓ already installed", "dim"), - "updated": ("✓ updated", "green"), - "skipped": ("– skipped", "dim"), - "not_installed": ("✗ not installed", "red"), - "failed": ("⚠ failed", "yellow"), -} - -def print_tool_results(tool_results: list[tools.ToolResult]) -> None: - t = rich_table.Table(show_header=False, box=None, padding=(0, 2)) - t.add_column(no_wrap=True) - t.add_column(no_wrap=True) - t.add_column(no_wrap=True) - - for result in tool_results: - label, style = _TOOL_ACTION_LABELS[result.action] - error_text = rich_text.Text(result.error or "", style="dim") - t.add_row(rich_text.Text(result.name), rich_text.Text(label, style=style), error_text) - - console.print(t) +def _maybe_commit(repo_path: pathlib.Path, message: str) -> None: + """Commit repo changes when auto_commit is enabled. Silently no-ops on any error.""" + try: + cfg = config.load_pauldot_config(repo_path) + if cfg.git.auto_commit: + git.commit(repo_path, message) + console.print("✓ Committed to dotfiles repo.") + except (FileNotFoundError, RuntimeError): + pass @tool_app.command("list") @@ -103,7 +91,7 @@ def tool_install( raise typer.Exit(1) from None results = tools.reconcile(tool_names, all_tools, os_name, console=console) - print_tool_results(results) + display.print_tool_results(results) @tool_app.command("update") @@ -130,7 +118,7 @@ def tool_update( raise typer.Exit(1) from None results = [tools.update(all_tools[n], os_name, console=console) for n in tool_names if n in all_tools] - print_tool_results(results) + display.print_tool_results(results) @tool_app.command("add") @@ -188,13 +176,7 @@ def tool_add( except FileNotFoundError: console.print(f"[yellow]⚠[/yellow] Profile '{target_profile}' not found — tool saved to tools.toml only.") - try: - cfg = config.load_pauldot_config(repo_path) - if cfg.git.auto_commit: - git.commit(repo_path, f"pauldot: add tool {name}") - console.print("✓ Committed to dotfiles repo.") - except FileNotFoundError, RuntimeError: - pass # auto-commit is best-effort + _maybe_commit(repo_path, f"pauldot: add tool {name}") @tool_app.command("remove") @@ -211,10 +193,4 @@ def tool_remove(name: str) -> None: config.save_tools(repo_path, updated) console.print(f"✓ Removed '{name}' from tools/tools.toml.") - try: - cfg = config.load_pauldot_config(repo_path) - if cfg.git.auto_commit: - git.commit(repo_path, f"pauldot: remove tool {name}") - console.print("✓ Committed to dotfiles repo.") - except FileNotFoundError, RuntimeError: - pass # auto-commit is best-effort + _maybe_commit(repo_path, f"pauldot: remove tool {name}") diff --git a/pauldot/config.py b/pauldot/config.py index 10dd967..2191d08 100644 --- a/pauldot/config.py +++ b/pauldot/config.py @@ -103,6 +103,15 @@ def save_tools(repo_path: pathlib.Path, tool_list: list[ToolDefinition]) -> None path.write_bytes(tomli_w.dumps(data).encode()) +# TOML field names that correspond to ProfileConfig fields. +# These functions mutate profile TOML via raw dicts (rather than round-tripping +# through ProfileConfig) so that unknown keys added by future pauldot versions +# are preserved. These constants make the coupling to ProfileConfig explicit so +# a field rename is caught at review time rather than silently going wrong. +_PROFILE_FIELD_TOOLS = "tools" # ProfileConfig.tools +_PROFILE_FIELD_DOTFILES = "dotfiles" # ProfileConfig.dotfiles + + def add_tool_to_profile(repo_path: pathlib.Path, profile_name: str, tool_name: str) -> None: """Append tool_name to the tools list in profiles/.toml.""" path = repo_path / "profiles" / f"{profile_name}.toml" @@ -110,10 +119,10 @@ def add_tool_to_profile(repo_path: pathlib.Path, profile_name: str, tool_name: s raise FileNotFoundError(f"Profile '{profile_name}' not found at {path}.") with path.open("rb") as f: data = tomllib.load(f) - tools_list: list[str] = data.get("tools", []) + tools_list: list[str] = data.get(_PROFILE_FIELD_TOOLS, []) if tool_name not in tools_list: tools_list.append(tool_name) - data["tools"] = tools_list + data[_PROFILE_FIELD_TOOLS] = tools_list path.write_bytes(tomli_w.dumps(data).encode()) @@ -124,8 +133,8 @@ def add_dotfile_to_profile(repo_path: pathlib.Path, profile_name: str, home_rel: raise FileNotFoundError(f"Profile '{profile_name}' not found at {path}.") with path.open("rb") as f: data = tomllib.load(f) - dotfiles_list: list[str] = data.get("dotfiles", []) + dotfiles_list: list[str] = data.get(_PROFILE_FIELD_DOTFILES, []) if home_rel not in dotfiles_list: dotfiles_list.append(home_rel) - data["dotfiles"] = dotfiles_list + data[_PROFILE_FIELD_DOTFILES] = dotfiles_list path.write_bytes(tomli_w.dumps(data).encode()) diff --git a/pauldot/display.py b/pauldot/display.py index 0f698c0..c3a5fdc 100644 --- a/pauldot/display.py +++ b/pauldot/display.py @@ -9,7 +9,7 @@ from rich import text as rich_text from pauldot import absorb as pauldot_absorb -from pauldot import dotfiles, state, zshrc +from pauldot import dotfiles, state, tools, zshrc from pauldot import migrate as pauldot_migrate console = rich_console.Console() @@ -116,6 +116,34 @@ def print_dotfile_status_results(results: list[dotfiles.DotfileStatus]) -> None: console.print(t) +# --------------------------------------------------------------------------- +# tools +# --------------------------------------------------------------------------- + +_TOOL_ACTION_LABELS: dict[str, tuple[str, str]] = { + "installed": ("✓ installed", "green"), + "already_installed": ("✓ already installed", "dim"), + "updated": ("✓ updated", "green"), + "skipped": ("– skipped", "dim"), + "not_installed": ("✗ not installed", "red"), + "failed": ("⚠ failed", "yellow"), +} + + +def print_tool_results(tool_results: list[tools.ToolResult]) -> None: + t = rich_table.Table(show_header=False, box=None, padding=(0, 2)) + t.add_column(no_wrap=True) + t.add_column(no_wrap=True) + t.add_column(no_wrap=True) + + for result in tool_results: + label, style = _TOOL_ACTION_LABELS[result.action] + error_text = rich_text.Text(result.error or "", style="dim") + t.add_row(rich_text.Text(result.name), rich_text.Text(label, style=style), error_text) + + console.print(t) + + # --------------------------------------------------------------------------- # doctor # ---------------------------------------------------------------------------