Initial commit: Clean DSS implementation
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
This commit is contained in:
179
dss-claude-plugin/hooks/scripts/storybook-reminder.py
Executable file
179
dss-claude-plugin/hooks/scripts/storybook-reminder.py
Executable file
@@ -0,0 +1,179 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user