mirror of
https://github.com/joshuadavidthomas/dotfiles.git
synced 2025-12-23 05:36:53 +00:00
add two new hooks and widen the rg hook
This commit is contained in:
parent
746b53e88f
commit
c0f95e339a
7 changed files with 357 additions and 3 deletions
133
.claude/hooks/bash_shebang_check.py
Executable file
133
.claude/hooks/bash_shebang_check.py
Executable file
|
|
@ -0,0 +1,133 @@
|
|||
#!/usr/bin/env python
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Shell(str, Enum):
|
||||
SH = "sh"
|
||||
BASH = "bash"
|
||||
ZSH = "zsh"
|
||||
KSH = "ksh"
|
||||
FISH = "fish"
|
||||
COMMAND = "command"
|
||||
|
||||
@property
|
||||
def as_ext(self) -> str:
|
||||
return f".{self.value}"
|
||||
|
||||
@property
|
||||
def shell_name(self) -> str:
|
||||
if self == Shell.COMMAND:
|
||||
return "bash"
|
||||
return self.value
|
||||
|
||||
|
||||
@dataclass
|
||||
class ShellInfo:
|
||||
file_path: str
|
||||
filename: str
|
||||
expected_shell: str | None
|
||||
|
||||
@classmethod
|
||||
def from_path(cls, file_path: str) -> ShellInfo:
|
||||
"""Create ShellInfo from a file path."""
|
||||
filename = file_path.split("/")[-1] if "/" in file_path else file_path
|
||||
|
||||
# Determine expected shell based on extension
|
||||
expected_shell = None
|
||||
for shell in Shell:
|
||||
if filename.endswith(shell.as_ext):
|
||||
expected_shell = shell.shell_name
|
||||
break
|
||||
|
||||
return cls(
|
||||
file_path=file_path, filename=filename, expected_shell=expected_shell
|
||||
)
|
||||
|
||||
|
||||
def is_likely_shell_script(file_path: str) -> bool:
|
||||
for shell in Shell:
|
||||
if file_path.endswith(shell.as_ext):
|
||||
return True
|
||||
|
||||
filename = file_path.split("/")[-1] if "/" in file_path else file_path
|
||||
|
||||
if "." not in filename or filename.startswith("."):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def check_shebang(content: str, file_path: str) -> list[str]:
|
||||
issues = []
|
||||
shell_info = ShellInfo.from_path(file_path)
|
||||
|
||||
if shell_info.expected_shell is None:
|
||||
if content.strip().startswith("#!/bin/bash") or re.search(
|
||||
r"^#!/bin/bash", content, re.MULTILINE
|
||||
):
|
||||
issues.append(
|
||||
"Use '#!/usr/bin/env bash' instead of '#!/bin/bash' for better portability"
|
||||
)
|
||||
else:
|
||||
shebang_pattern = rf"^#!\s*/bin/{shell_info.expected_shell}\b"
|
||||
if re.search(shebang_pattern, content, re.MULTILINE):
|
||||
issues.append(
|
||||
f"Use '#!/usr/bin/env {shell_info.expected_shell}' instead of '#!/bin/{shell_info.expected_shell}' for better portability"
|
||||
)
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
try:
|
||||
input_data = json.load(sys.stdin)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
tool_name = input_data.get("tool_name", "")
|
||||
tool_input = input_data.get("tool_input", {})
|
||||
file_path = tool_input.get("file_path", "")
|
||||
|
||||
if not is_likely_shell_script(file_path):
|
||||
return 0
|
||||
|
||||
issues = []
|
||||
if tool_name == "Write":
|
||||
content = tool_input.get("content", "")
|
||||
issues = check_shebang(content, file_path)
|
||||
elif tool_name == "Edit":
|
||||
new_string = tool_input.get("new_string", "")
|
||||
issues = check_shebang(new_string, file_path)
|
||||
elif tool_name == "MultiEdit":
|
||||
edits = tool_input.get("edits", [])
|
||||
for edit in edits:
|
||||
new_string = edit.get("new_string", "")
|
||||
issues.extend(check_shebang(new_string, file_path))
|
||||
|
||||
if issues:
|
||||
seen = set()
|
||||
unique_issues = []
|
||||
for issue in issues:
|
||||
if issue not in seen:
|
||||
seen.add(issue)
|
||||
unique_issues.append(issue)
|
||||
|
||||
for message in unique_issues:
|
||||
print(f"• {message}", file=sys.stderr)
|
||||
|
||||
# Exit code 2 blocks tool call and shows stderr to Claude
|
||||
return 2
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
115
.claude/hooks/python_deprecated_imports.py
Executable file
115
.claude/hooks/python_deprecated_imports.py
Executable file
|
|
@ -0,0 +1,115 @@
|
|||
#!/usr/bin/env python
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections.abc import Sequence as ABCSequence
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImportRule:
|
||||
pattern: str
|
||||
old_usage: str
|
||||
new_usage: str
|
||||
|
||||
|
||||
class DeprecatedImport(Enum):
|
||||
OPTIONAL = ImportRule(
|
||||
pattern=r"from\s+typing\s+import\s+.*\bOptional\b",
|
||||
old_usage="Optional[X]",
|
||||
new_usage="X | None",
|
||||
)
|
||||
DICT = ImportRule(
|
||||
pattern=r"from\s+typing\s+import\s+.*\bDict\b",
|
||||
old_usage="Dict",
|
||||
new_usage="dict",
|
||||
)
|
||||
TUPLE = ImportRule(
|
||||
pattern=r"from\s+typing\s+import\s+.*\bTuple\b",
|
||||
old_usage="Tuple",
|
||||
new_usage="tuple",
|
||||
)
|
||||
SEQUENCE = ImportRule(
|
||||
pattern=r"from\s+typing\s+import\s+.*\bSequence\b",
|
||||
old_usage="typing.Sequence",
|
||||
new_usage="collections.abc.Sequence",
|
||||
)
|
||||
|
||||
def format_message(self) -> str:
|
||||
return f"Use '{self.value.new_usage}' instead of '{self.value.old_usage}' (from typing import {self.name.title()})"
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileInfo:
|
||||
file_path: str
|
||||
filename: str
|
||||
|
||||
@classmethod
|
||||
def from_path(cls, file_path: str) -> FileInfo:
|
||||
filename = file_path.split("/")[-1] if "/" in file_path else file_path
|
||||
return cls(file_path=file_path, filename=filename)
|
||||
|
||||
|
||||
def is_python_file(file_path: str) -> bool:
|
||||
return file_path.endswith(".py") or file_path.endswith(".pyi")
|
||||
|
||||
|
||||
def check_deprecated_imports(content: str) -> list[str]:
|
||||
issues = []
|
||||
|
||||
for deprecated in DeprecatedImport:
|
||||
if re.search(deprecated.value.pattern, content, re.MULTILINE):
|
||||
issues.append(deprecated.format_message())
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def main(argv: ABCSequence[str] | None = None) -> int:
|
||||
try:
|
||||
input_data = json.load(sys.stdin)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
tool_name = input_data.get("tool_name", "")
|
||||
tool_input = input_data.get("tool_input", {})
|
||||
file_path = tool_input.get("file_path", "")
|
||||
|
||||
if not is_python_file(file_path):
|
||||
return 0
|
||||
|
||||
issues = []
|
||||
if tool_name == "Write":
|
||||
content = tool_input.get("content", "")
|
||||
issues = check_deprecated_imports(content)
|
||||
elif tool_name == "Edit":
|
||||
new_string = tool_input.get("new_string", "")
|
||||
issues = check_deprecated_imports(new_string)
|
||||
elif tool_name == "MultiEdit":
|
||||
edits = tool_input.get("edits", [])
|
||||
for edit in edits:
|
||||
new_string = edit.get("new_string", "")
|
||||
issues.extend(check_deprecated_imports(new_string))
|
||||
|
||||
if issues:
|
||||
seen = set()
|
||||
unique_issues = []
|
||||
for issue in issues:
|
||||
if issue not in seen:
|
||||
seen.add(issue)
|
||||
unique_issues.append(issue)
|
||||
|
||||
for message in unique_issues:
|
||||
print(f"• {message}", file=sys.stderr)
|
||||
|
||||
# Exit code 2 blocks tool call and shows stderr to Claude
|
||||
return 2
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -8,12 +8,12 @@ from collections.abc import Sequence
|
|||
|
||||
VALIDATION_RULES = [
|
||||
(
|
||||
r"\bgrep\b(?!.*\|)",
|
||||
r"\bgrep\b",
|
||||
"Use 'rg' (ripgrep) instead of 'grep' for better performance and features",
|
||||
),
|
||||
(
|
||||
r"\bfind\s+\S+\s+-name\b",
|
||||
"Use 'rg --files | rg pattern' or 'rg --files -g pattern' instead of 'find -name' for better performance",
|
||||
r"\bfind\s+",
|
||||
"Use 'rg --files' with appropriate filters instead of 'find' for better performance",
|
||||
),
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,24 @@
|
|||
"command": "$HOME/.claude/hooks/jina_reader_check.py"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Write|Edit|MultiEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$HOME/.claude/hooks/bash_shebang_check.py"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "Write|Edit|MultiEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$HOME/.claude/hooks/python_deprecated_imports.py"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
7
.config/rustic/backup.sh
Executable file
7
.config/rustic/backup.sh
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
rustic backup
|
||||
rustic forget
|
||||
rustic repoinfo
|
||||
1
.config/rustic/rustic-password
Normal file
1
.config/rustic/rustic-password
Normal file
|
|
@ -0,0 +1 @@
|
|||
bgNvM33ciEmBfhPfrWQyLKBXCQUDeG7LBEZCyK6gRKru6fEr
|
||||
80
.config/rustic/rustic.toml
Normal file
80
.config/rustic/rustic.toml
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
[repository]
|
||||
repository = "opendal:s3"
|
||||
password-file = "/home/josh/.config/rustic/rustic-password"
|
||||
|
||||
[repository.options]
|
||||
endpoint = "https://0d52c99870b49aa52ccc7d5feca16714.r2.cloudflarestorage.com"
|
||||
bucket = "homedir"
|
||||
region = "auto"
|
||||
batch_max_operations = "700"
|
||||
enable_exact_buf_write = "true"
|
||||
root = "/backups"
|
||||
|
||||
[backup]
|
||||
label = "homedir"
|
||||
tags = ["home", "laptop"]
|
||||
git-ignore = false
|
||||
exclude-if-present = [".nobackup", "CACHEDIR.TAG"]
|
||||
one-file-system = false
|
||||
|
||||
globs = []
|
||||
iglobs = [
|
||||
# System caches
|
||||
"/home/josh/.cache/**",
|
||||
"/home/josh/.local/share/Trash/**",
|
||||
"/home/josh/snap/**",
|
||||
|
||||
# Browser caches
|
||||
"/home/josh/.config/BraveSoftware/*/*Cache/**",
|
||||
"/home/josh/.config/chromium/*/*Cache/**",
|
||||
"/home/josh/.config/google-chrome/*/*Cache/**",
|
||||
"/home/josh/.config/vivaldi/*/*Cache/**",
|
||||
"/home/josh/.config/vivaldi-snapshot/*/*Cache/**",
|
||||
"/home/josh/.mozilla/firefox/*/*Cache/**",
|
||||
|
||||
# Package caches
|
||||
"/home/josh/.cargo/registry/**",
|
||||
"/home/josh/.cargo/git/db/**",
|
||||
"/home/josh/.cargo/bin/**",
|
||||
"/home/josh/.go/pkg/mod/**",
|
||||
"/home/josh/.npm/**",
|
||||
"/home/josh/.pnpm-store/**",
|
||||
"/home/josh/.yarn/cache/**",
|
||||
|
||||
# Build outputs & dependencies
|
||||
"**/node_modules/**",
|
||||
"**/target/**",
|
||||
"**/dist/**",
|
||||
"**/build/**",
|
||||
"**/__pycache__/**",
|
||||
"**/*.pyc",
|
||||
"**/.venv/**",
|
||||
"**/venv/**",
|
||||
|
||||
# Large media/gaming
|
||||
"/home/josh/.local/share/Steam/**",
|
||||
"/home/josh/videos/**",
|
||||
"/home/josh/music/**",
|
||||
|
||||
# Temp files
|
||||
"**/*.tmp",
|
||||
"**/*.temp",
|
||||
"**/*.swp",
|
||||
"**/*.swo",
|
||||
"**/core",
|
||||
"**/core.*",
|
||||
|
||||
# Downloads (usually duplicates)
|
||||
"/home/josh/downloads/**",
|
||||
"/home/josh/Downloads/**",
|
||||
]
|
||||
|
||||
[[backup.snapshots]]
|
||||
sources = ["/home/josh"]
|
||||
|
||||
[forget]
|
||||
keep-daily = 7
|
||||
keep-weekly = 4
|
||||
keep-monthly = 6
|
||||
keep-yearly = 2
|
||||
prune = true
|
||||
Loading…
Add table
Add a link
Reference in a new issue