mirror of
https://github.com/JuliusBrussee/caveman.git
synced 2026-08-11 13:21:09 +02:00
Drops top-level entry count from ~26 to ~22 to align with conventions in
mature skill libraries (anthropics/skills, vercel-labs/skills). agents/,
commands/, skills/, and .claude-plugin/plugin.json stay at the repo root
because Claude Code auto-discovers them at the plugin root.
What moved:
hooks/ → src/hooks/
rules/ → src/rules/
tools/ → src/tools/
mcp-servers/ → src/mcp-servers/
Path references updated atomically in:
- bin/install.js (HOOKS_REMOTE, INIT_SCRIPT_URL, repoRoot/sourceDir paths,
user-facing install hints)
- .claude-plugin/plugin.json (CLAUDE_PLUGIN_ROOT/src/hooks/...)
- package.json files: array
- tests/{test_hooks,test_symlink_flag,test_caveman_init,test_mcp_shrink,
verify_repo}: bash/node command paths and require() args
- tests/verify_repo.py: also drops the dead .cursor/.windsurf/.clinerules/
copilot-instructions/caveman dotdir-mirror checks left behind by be714a3
plus adds bin/install.js + bin/lib/settings.js regression guards
- commands/caveman-init.toml prompt
- src/mcp-servers/caveman-shrink/package.json repository.directory
- CLAUDE.md, CONTRIBUTING.md, README.md, src/hooks/install.{sh,ps1},
src/hooks/uninstall.{sh,ps1}, src/tools/caveman-init.js: docstrings,
URL paths, source-of-truth tables
External raw.githubusercontent.com URL updates (referrers, blog posts) are
the maintainer's lane.
162 lines
5.9 KiB
Python
162 lines
5.9 KiB
Python
import json
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
class HookScriptTests(unittest.TestCase):
|
|
def run_cmd(self, cmd, home):
|
|
env = os.environ.copy()
|
|
env["HOME"] = str(home)
|
|
env["USERPROFILE"] = str(home)
|
|
return subprocess.run(
|
|
cmd,
|
|
cwd=REPO_ROOT,
|
|
env=env,
|
|
text=True,
|
|
capture_output=True,
|
|
check=True,
|
|
)
|
|
|
|
def test_install_upgrades_old_two_file_install(self):
|
|
with tempfile.TemporaryDirectory(prefix="caveman-hooks-upgrade-") as tmp:
|
|
home = Path(tmp)
|
|
hooks_dir = home / ".claude" / "hooks"
|
|
hooks_dir.mkdir(parents=True)
|
|
(home / ".claude" / "settings.json").write_text("{}\n")
|
|
(hooks_dir / "caveman-activate.js").write_text("")
|
|
(hooks_dir / "caveman-mode-tracker.js").write_text("")
|
|
|
|
self.run_cmd(["bash", "src/hooks/install.sh"], home)
|
|
|
|
statusline = hooks_dir / "caveman-statusline.sh"
|
|
self.assertTrue(statusline.exists(), "upgrade should install statusline script")
|
|
|
|
settings = json.loads((home / ".claude" / "settings.json").read_text())
|
|
self.assertIn("statusLine", settings)
|
|
self.assertIn(str(statusline), settings["statusLine"]["command"])
|
|
|
|
def test_install_reconfigures_missing_statusline(self):
|
|
with tempfile.TemporaryDirectory(prefix="caveman-hooks-statusline-") as tmp:
|
|
home = Path(tmp)
|
|
claude_dir = home / ".claude"
|
|
hooks_dir = claude_dir / "hooks"
|
|
hooks_dir.mkdir(parents=True)
|
|
|
|
for name in ("caveman-activate.js", "caveman-mode-tracker.js", "caveman-statusline.sh"):
|
|
(hooks_dir / name).write_text("")
|
|
|
|
settings = {
|
|
"hooks": {
|
|
"SessionStart": [
|
|
{
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": f'node "{hooks_dir / "caveman-activate.js"}"',
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"UserPromptSubmit": [
|
|
{
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": f'node "{hooks_dir / "caveman-mode-tracker.js"}"',
|
|
}
|
|
]
|
|
}
|
|
],
|
|
}
|
|
}
|
|
(claude_dir / "settings.json").write_text(json.dumps(settings, indent=2) + "\n")
|
|
|
|
result = self.run_cmd(["bash", "src/hooks/install.sh"], home)
|
|
|
|
self.assertNotIn("Nothing to do", result.stdout)
|
|
|
|
updated = json.loads((claude_dir / "settings.json").read_text())
|
|
self.assertIn("statusLine", updated)
|
|
self.assertIn(str(hooks_dir / "caveman-statusline.sh"), updated["statusLine"]["command"])
|
|
|
|
def test_uninstall_preserves_custom_statusline(self):
|
|
with tempfile.TemporaryDirectory(prefix="caveman-hooks-uninstall-") as tmp:
|
|
home = Path(tmp)
|
|
claude_dir = home / ".claude"
|
|
hooks_dir = claude_dir / "hooks"
|
|
hooks_dir.mkdir(parents=True)
|
|
|
|
for name in ("caveman-activate.js", "caveman-mode-tracker.js", "caveman-statusline.sh"):
|
|
(hooks_dir / name).write_text("")
|
|
|
|
settings = {
|
|
"statusLine": {
|
|
"type": "command",
|
|
"command": "bash /tmp/custom-status-with-caveman.sh",
|
|
},
|
|
"hooks": {
|
|
"SessionStart": [
|
|
{
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": f'node "{hooks_dir / "caveman-activate.js"}"',
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"UserPromptSubmit": [
|
|
{
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": f'node "{hooks_dir / "caveman-mode-tracker.js"}"',
|
|
}
|
|
]
|
|
}
|
|
],
|
|
},
|
|
}
|
|
(claude_dir / "settings.json").write_text(json.dumps(settings, indent=2) + "\n")
|
|
|
|
self.run_cmd(["bash", "src/hooks/uninstall.sh"], home)
|
|
|
|
updated = json.loads((claude_dir / "settings.json").read_text())
|
|
self.assertEqual(
|
|
updated["statusLine"]["command"],
|
|
"bash /tmp/custom-status-with-caveman.sh",
|
|
)
|
|
self.assertNotIn("hooks", updated)
|
|
|
|
def test_activate_does_not_nudge_when_custom_statusline_exists(self):
|
|
with tempfile.TemporaryDirectory(prefix="caveman-hooks-activate-") as tmp:
|
|
home = Path(tmp)
|
|
claude_dir = home / ".claude"
|
|
claude_dir.mkdir(parents=True)
|
|
(claude_dir / "settings.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"statusLine": {
|
|
"type": "command",
|
|
"command": "bash /tmp/my-statusline.sh",
|
|
}
|
|
}
|
|
)
|
|
+ "\n"
|
|
)
|
|
|
|
result = self.run_cmd(["node", "src/hooks/caveman-activate.js"], home)
|
|
|
|
self.assertNotIn("STATUSLINE SETUP NEEDED", result.stdout)
|
|
self.assertEqual((claude_dir / ".caveman-active").read_text(), "full")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|