#!/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()