Skip to content

docs(hooks): document lib/ symlink requirement in installation instru… #17

docs(hooks): document lib/ symlink requirement in installation instru…

docs(hooks): document lib/ symlink requirement in installation instru… #17

name: Clean Environment Install Test
on:
push:
branches: [main]
paths:
- 'claude-dev-toolkit/**'
- 'hooks/**'
- 'scripts/**'
- '.github/workflows/install-clean-env.yml'
pull_request:
branches: [main]
paths:
- 'claude-dev-toolkit/**'
- 'hooks/**'
- 'scripts/**'
workflow_dispatch:
# Feature coverage: 15/20 (75%)
# Not testable in CI: oidc (needs AWS), update (needs registry),
# iTerm2 hooks (needs terminal), shell hooks requiring tty
jobs:
clean-install:
name: "Clean install (${{ matrix.os }}, Node ${{ matrix.node }})"
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
node: ["18", "20"]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: ${{ matrix.node }}
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
- name: Verify clean state (no pre-existing .claude)
run: |
if [ -d "$HOME/.claude" ]; then
echo "WARN: ~/.claude exists, removing for clean test"
rm -rf "$HOME/.claude"
fi
echo "Confirmed: no ~/.claude directory"
# ---------------------------------------------------------------
# Install
# ---------------------------------------------------------------
- name: Clean install and pack npm package
working-directory: claude-dev-toolkit
run: |
rm -rf node_modules package-lock.json
npm install --ignore-scripts
npm pack
TARBALL=$(ls *.tgz)
echo "TARBALL=$TARBALL" >> "$GITHUB_ENV"
echo "Packed: $TARBALL"
- name: Install package globally from tarball
working-directory: claude-dev-toolkit
run: npm install -g "./$TARBALL"
# ---------------------------------------------------------------
# CLI basics
# ---------------------------------------------------------------
- name: "Feature: CLI --version and --help"
run: |
claude-commands --version
claude-commands --help
# ---------------------------------------------------------------
# Setup (creates dirs, commands, hooks, config)
# ---------------------------------------------------------------
- name: "Feature: setup (active commands, basic config)"
run: claude-commands setup --type basic --commands active
- name: Verify directory structure created
run: |
FAIL=0
for dir in "$HOME/.claude" "$HOME/.claude/commands" "$HOME/.claude/hooks"; do
if [ -d "$dir" ]; then
echo "OK: $dir exists"
else
echo "FAIL: $dir missing"
FAIL=1
fi
done
[ $FAIL -eq 0 ] || exit 1
- name: Verify commands installed
run: |
COUNT=$(ls "$HOME/.claude/commands/"*.md 2>/dev/null | wc -l)
echo "Commands installed: $COUNT"
if [ "$COUNT" -lt 10 ]; then
echo "FAIL: expected >= 10 active commands, got $COUNT"
exit 1
fi
echo "OK: $COUNT commands installed"
- name: Verify hooks installed
run: |
SHELL_HOOKS=$(ls "$HOME/.claude/hooks/"*.sh 2>/dev/null | wc -l)
echo "Shell hooks installed: $SHELL_HOOKS"
if [ "$SHELL_HOOKS" -lt 1 ]; then
echo "FAIL: no shell hooks installed"
exit 1
fi
echo "OK: $SHELL_HOOKS shell hooks installed"
- name: Verify settings.json is valid JSON
run: |
SETTINGS="$HOME/.claude/settings.json"
if [ -f "$SETTINGS" ]; then
python3 -c "import json; json.load(open('$SETTINGS'))"
echo "OK: settings.json is valid JSON"
else
echo "WARN: settings.json not created (acceptable for basic template)"
fi
# ---------------------------------------------------------------
# CLI commands: list, status, verify
# ---------------------------------------------------------------
- name: "Feature: list"
run: claude-commands list
- name: "Feature: list --active"
run: claude-commands list --active
- name: "Feature: status"
run: claude-commands status
- name: "Feature: verify"
run: claude-commands verify || true
# ---------------------------------------------------------------
# Config and configure
# ---------------------------------------------------------------
- name: "Feature: config --list"
run: claude-commands config --list
- name: "Feature: configure --validate"
run: claude-commands configure --validate || true
- name: "Feature: configure --template basic"
run: |
claude-commands configure --template basic
SETTINGS="$HOME/.claude/settings.json"
python3 -c "import json; json.load(open('$SETTINGS'))"
echo "OK: settings.json valid after configure --template basic"
- name: "Feature: configure --template security-focused"
run: |
claude-commands configure --template security-focused
SETTINGS="$HOME/.claude/settings.json"
python3 -c "import json; json.load(open('$SETTINGS'))"
echo "OK: settings.json valid after configure --template security-focused"
# ---------------------------------------------------------------
# Subagents
# ---------------------------------------------------------------
- name: "Feature: subagents --list"
run: claude-commands subagents --list
- name: "Feature: subagents --install"
run: claude-commands subagents --install || true
# ---------------------------------------------------------------
# Backup and restore
# ---------------------------------------------------------------
- name: "Feature: backup creates archive"
run: |
claude-commands backup test-snapshot
# Verify backup file was created
if ls "$HOME/.claude/backups/test-snapshot"* 2>/dev/null; then
echo "OK: backup created"
else
echo "FAIL: no backup file found"
ls -la "$HOME/.claude/backups/" 2>/dev/null || echo "No backups dir"
exit 1
fi
- name: "Feature: restore from backup"
run: |
# Restore (may fail due to compressed backup — known limitation)
if claude-commands restore test-snapshot; then
echo "OK: restore succeeded"
else
echo "WARN: restore failed (known issue with compressed backups)"
fi
# ---------------------------------------------------------------
# Python hook smoke tests
# ---------------------------------------------------------------
- name: "Feature: check-complexity.py blocks smelly code"
run: |
# Create a Python file with a long function (>20 lines)
TMPFILE=$(mktemp /tmp/smelly_XXXX.py)
python3 -c "
lines = ['def long_function():']
for i in range(25):
lines.append(f' x_{i} = {i}')
print('\n'.join(lines))
" > "$TMPFILE"
# Pipe a PostToolUse event into check-complexity.py
RESULT=$(echo "{\"tool_input\":{\"file_path\":\"$TMPFILE\"}}" | \
python3 hooks/check-complexity.py 2>/dev/null || true)
rm -f "$TMPFILE"
if echo "$RESULT" | python3 -c "import json,sys; d=json.load(sys.stdin); assert d['decision']=='block'" 2>/dev/null; then
echo "OK: check-complexity.py blocked smelly code"
else
echo "FAIL: check-complexity.py did not block smelly code"
echo "Output: $RESULT"
exit 1
fi
- name: "Feature: check-complexity.py passes clean code"
run: |
TMPFILE=$(mktemp /tmp/clean_XXXX.py)
echo -e "def hello():\n return 1\n" > "$TMPFILE"
RESULT=$(echo "{\"tool_input\":{\"file_path\":\"$TMPFILE\"}}" | \
python3 hooks/check-complexity.py 2>/dev/null || true)
rm -f "$TMPFILE"
if [ -z "$RESULT" ]; then
echo "OK: check-complexity.py passed clean code (no output)"
else
echo "FAIL: check-complexity.py unexpectedly blocked clean code"
echo "Output: $RESULT"
exit 1
fi
- name: "Feature: check-security.py blocks hardcoded secrets"
run: |
TMPFILE=$(mktemp /tmp/secret_XXXX.py)
echo "aws_key = 'AKIAIOSFODNN7ZZZZZZZ'" > "$TMPFILE"
RESULT=$(echo "{\"tool_input\":{\"file_path\":\"$TMPFILE\"}}" | \
python3 hooks/check-security.py 2>/dev/null || true)
rm -f "$TMPFILE"
if echo "$RESULT" | python3 -c "import json,sys; d=json.load(sys.stdin); assert d['decision']=='block'" 2>/dev/null; then
echo "OK: check-security.py blocked hardcoded secret"
else
echo "FAIL: check-security.py did not block hardcoded secret"
echo "Output: $RESULT"
exit 1
fi
- name: "Feature: check-security.py passes clean code"
run: |
TMPFILE=$(mktemp /tmp/safe_XXXX.py)
echo -e "x = 1\nprint(x)\n" > "$TMPFILE"
RESULT=$(echo "{\"tool_input\":{\"file_path\":\"$TMPFILE\"}}" | \
python3 hooks/check-security.py 2>/dev/null || true)
rm -f "$TMPFILE"
if [ -z "$RESULT" ]; then
echo "OK: check-security.py passed clean code (no output)"
else
echo "FAIL: check-security.py unexpectedly blocked clean code"
echo "Output: $RESULT"
exit 1
fi
- name: "Feature: check-commit-signing.py passes non-git commands"
run: |
RESULT=$(echo '{"tool_input":{"command":"ls -la"}}' | \
python3 hooks/check-commit-signing.py 2>/dev/null || true)
if [ -z "$RESULT" ]; then
echo "OK: check-commit-signing.py passed non-git command"
else
echo "FAIL: check-commit-signing.py blocked a non-git command"
echo "Output: $RESULT"
exit 1
fi
- name: "Feature: check-commit-signing.py checks git commit"
run: |
RESULT=$(echo '{"tool_input":{"command":"git commit -m test"}}' | \
python3 hooks/check-commit-signing.py 2>/dev/null || true)
# In CI, signing is not configured, so it should block
if echo "$RESULT" | python3 -c "import json,sys; d=json.load(sys.stdin); assert d['decision']=='block'" 2>/dev/null; then
echo "OK: check-commit-signing.py blocked unsigned commit"
else
echo "WARN: commit signing may be configured in CI (passed)"
fi
# ---------------------------------------------------------------
# Install command directly (separate from setup)
# ---------------------------------------------------------------
- name: "Feature: install --all"
run: |
claude-commands install --all
COUNT=$(ls "$HOME/.claude/commands/"*.md 2>/dev/null | wc -l)
echo "Commands after install --all: $COUNT"
if [ "$COUNT" -lt 40 ]; then
echo "FAIL: expected >= 40 total commands, got $COUNT"
exit 1
fi
echo "OK: $COUNT commands installed with --all"
# ---------------------------------------------------------------
# Idempotent re-install
# ---------------------------------------------------------------
- name: "Test: idempotent re-install (no corruption)"
run: |
BEFORE_CMDS=$(ls "$HOME/.claude/commands/"*.md 2>/dev/null | wc -l)
claude-commands setup --type basic --commands active
AFTER_CMDS=$(ls "$HOME/.claude/commands/"*.md 2>/dev/null | wc -l)
if [ "$BEFORE_CMDS" -ne "$AFTER_CMDS" ]; then
echo "FAIL: command count changed ($BEFORE_CMDS -> $AFTER_CMDS)"
exit 1
fi
echo "OK: idempotent re-install preserved $AFTER_CMDS commands"
SETTINGS="$HOME/.claude/settings.json"
if [ -f "$SETTINGS" ]; then
python3 -c "import json; json.load(open('$SETTINGS'))"
echo "OK: settings.json still valid after re-install"
fi
# ---------------------------------------------------------------
# User config preservation
# ---------------------------------------------------------------
- name: "Test: install preserves pre-existing user config"
run: |
SETTINGS="$HOME/.claude/settings.json"
cat > "$SETTINGS" << 'USERJSON'
{
"permissions": {
"allow": ["Bash(npm test)", "Read"]
},
"userCustomField": "do-not-delete"
}
USERJSON
claude-commands setup --type basic --commands active
python3 -c "
import json
with open('$SETTINGS') as f:
data = json.load(f)
print('Valid JSON: OK')
"
echo "OK: user config survived setup"
# ---------------------------------------------------------------
# Uninstall
# ---------------------------------------------------------------
- name: "Feature: uninstall"
run: |
BEFORE=$(ls "$HOME/.claude/commands/"*.md 2>/dev/null | wc -l)
claude-commands uninstall
AFTER=$(ls "$HOME/.claude/commands/"*.md 2>/dev/null | wc -l)
echo "Commands before: $BEFORE, after: $AFTER"
if [ "$AFTER" -ge "$BEFORE" ]; then
echo "FAIL: uninstall did not remove any commands"
exit 1
fi
echo "OK: uninstall removed $((BEFORE - AFTER)) commands"
# ---------------------------------------------------------------
# Cleanup
# ---------------------------------------------------------------
- name: Clean up global install
if: always()
run: npm uninstall -g @paulduvall/claude-dev-toolkit || true