Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 16 additions & 29 deletions pauldot/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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.")
27 changes: 13 additions & 14 deletions pauldot/commands/alias.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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())
Expand Down
52 changes: 14 additions & 38 deletions pauldot/commands/tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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")
Expand All @@ -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}")
17 changes: 13 additions & 4 deletions pauldot/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,17 +103,26 @@ 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/<profile_name>.toml."""
path = repo_path / "profiles" / f"{profile_name}.toml"
if not path.exists():
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())


Expand All @@ -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())
30 changes: 29 additions & 1 deletion pauldot/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand Down