Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
180 lines
5.6 KiB
Python
Executable File
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()
|