Files
dss/dss-claude-plugin/hooks/scripts/storybook-reminder.py
Bruno Sarlo 4de266de61
Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled
Revert "chore: Remove dss-claude-plugin directory"
This reverts commit 72cb7319f5.
2025-12-10 15:54:39 -03:00

180 lines
5.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""
DSS Storybook Reminder Hook
Reminds developers to update Storybook stories when components change.
Written from scratch for DSS.
"""
import json
import os
import re
import sys
from pathlib import Path
def get_config():
"""Load hook configuration."""
config_path = Path.home() / ".dss" / "hooks-config.json"
default_config = {
"storybook_reminder": {
"enabled": True,
"component_patterns": ["**/components/**/*.tsx", "**/ui/**/*.tsx"],
"story_extensions": [".stories.tsx", ".stories.jsx", ".stories.ts", ".stories.js"],
"remind_on_new": True,
"remind_on_props_change": True
}
}
if config_path.exists():
try:
with open(config_path) as f:
user_config = json.load(f)
return {**default_config, **user_config}
except:
pass
return default_config
def is_component_file(file_path: str) -> bool:
"""Check if file is a React component."""
path = Path(file_path)
# Must be a tsx/jsx file
if path.suffix.lower() not in [".tsx", ".jsx"]:
return False
# Skip story files, test files, index files
name = path.stem.lower()
if any(x in name for x in [".stories", ".story", ".test", ".spec", "index"]):
return False
# Check if in component-like directory
parts = str(path).lower()
component_dirs = ["components", "ui", "atoms", "molecules", "organisms", "templates"]
return any(d in parts for d in component_dirs)
def find_story_file(component_path: str) -> tuple:
"""Find corresponding story file for a component."""
path = Path(component_path)
base_name = path.stem
parent = path.parent
story_extensions = [".stories.tsx", ".stories.jsx", ".stories.ts", ".stories.js"]
# Check same directory
for ext in story_extensions:
story_path = parent / f"{base_name}{ext}"
if story_path.exists():
return (True, str(story_path))
# Check __stories__ subdirectory
stories_dir = parent / "__stories__"
if stories_dir.exists():
for ext in story_extensions:
story_path = stories_dir / f"{base_name}{ext}"
if story_path.exists():
return (True, str(story_path))
# Check stories subdirectory
stories_dir = parent / "stories"
if stories_dir.exists():
for ext in story_extensions:
story_path = stories_dir / f"{base_name}{ext}"
if story_path.exists():
return (True, str(story_path))
return (False, None)
def detect_props_change(content: str) -> bool:
"""Detect if content includes prop changes."""
prop_patterns = [
r"interface\s+\w+Props",
r"type\s+\w+Props\s*=",
r"Props\s*=\s*\{",
r"defaultProps\s*=",
r"propTypes\s*="
]
for pattern in prop_patterns:
if re.search(pattern, content):
return True
return False
def format_reminder(file_path: str, has_story: bool, story_path: str, props_changed: bool) -> str:
"""Format the reminder message."""
lines = [f"\n=== DSS Storybook Reminder ===\n"]
component_name = Path(file_path).stem
if not has_story:
lines.append(f"[NEW] Component '{component_name}' has no Storybook story!")
lines.append(f" Consider creating: {component_name}.stories.tsx")
lines.append("")
lines.append(" Quick template:")
lines.append(f" import {{ {component_name} }} from './{component_name}';")
lines.append(f" export default {{ title: 'Components/{component_name}' }};")
lines.append(f" export const Default = () => <{component_name} />;")
elif props_changed:
lines.append(f"[UPDATE] Props changed in '{component_name}'")
lines.append(f" Story file: {story_path}")
lines.append(" Consider updating stories to reflect new props.")
lines.append("")
lines.append("=" * 40)
return "\n".join(lines)
def main():
"""Main hook entry point."""
config = get_config()
if not config.get("storybook_reminder", {}).get("enabled", True):
sys.exit(0)
# Read hook input from stdin
try:
input_data = json.loads(sys.stdin.read())
except json.JSONDecodeError:
sys.exit(0)
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
if tool_name not in ["Edit", "Write"]:
sys.exit(0)
file_path = tool_input.get("file_path", "")
# Only check component files
if not is_component_file(file_path):
sys.exit(0)
# Get content
if tool_name == "Write":
content = tool_input.get("content", "")
elif tool_name == "Edit":
content = tool_input.get("new_string", "")
else:
content = ""
# Check for story file
has_story, story_path = find_story_file(file_path)
# Check for props changes
props_changed = detect_props_change(content) if content else False
reminder_config = config.get("storybook_reminder", {})
# Determine if we should show reminder
should_remind = False
if not has_story and reminder_config.get("remind_on_new", True):
should_remind = True
elif has_story and props_changed and reminder_config.get("remind_on_props_change", True):
should_remind = True
if should_remind:
output = format_reminder(file_path, has_story, story_path, props_changed)
print(output, file=sys.stderr)
sys.exit(0)
if __name__ == "__main__":
main()