@hustle-together/api-dev-tools 3.6.5 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +5599 -258
  2. package/bin/cli.js +395 -20
  3. package/commands/README.md +459 -71
  4. package/commands/hustle-api-continue.md +158 -0
  5. package/commands/{api-create.md → hustle-api-create.md} +35 -15
  6. package/commands/{api-env.md → hustle-api-env.md} +4 -4
  7. package/commands/{api-interview.md → hustle-api-interview.md} +1 -1
  8. package/commands/{api-research.md → hustle-api-research.md} +3 -3
  9. package/commands/hustle-api-sessions.md +149 -0
  10. package/commands/{api-status.md → hustle-api-status.md} +16 -16
  11. package/commands/{api-verify.md → hustle-api-verify.md} +2 -2
  12. package/commands/hustle-combine.md +763 -0
  13. package/commands/hustle-ui-create-page.md +933 -0
  14. package/commands/hustle-ui-create.md +825 -0
  15. package/hooks/api-workflow-check.py +545 -21
  16. package/hooks/cache-research.py +337 -0
  17. package/hooks/check-api-routes.py +168 -0
  18. package/hooks/check-playwright-setup.py +103 -0
  19. package/hooks/check-storybook-setup.py +81 -0
  20. package/hooks/detect-interruption.py +165 -0
  21. package/hooks/enforce-a11y-audit.py +202 -0
  22. package/hooks/enforce-brand-guide.py +241 -0
  23. package/hooks/enforce-documentation.py +60 -8
  24. package/hooks/enforce-freshness.py +184 -0
  25. package/hooks/enforce-page-components.py +186 -0
  26. package/hooks/enforce-page-data-schema.py +155 -0
  27. package/hooks/enforce-questions-sourced.py +146 -0
  28. package/hooks/enforce-schema-from-interview.py +248 -0
  29. package/hooks/enforce-ui-disambiguation.py +108 -0
  30. package/hooks/enforce-ui-interview.py +130 -0
  31. package/hooks/generate-manifest-entry.py +1161 -0
  32. package/hooks/session-logger.py +297 -0
  33. package/hooks/session-startup.py +160 -15
  34. package/hooks/track-scope-coverage.py +220 -0
  35. package/hooks/track-tool-use.py +81 -1
  36. package/hooks/update-api-showcase.py +149 -0
  37. package/hooks/update-registry.py +352 -0
  38. package/hooks/update-ui-showcase.py +212 -0
  39. package/package.json +8 -3
  40. package/templates/BRAND_GUIDE.md +299 -0
  41. package/templates/CLAUDE-SECTION.md +56 -24
  42. package/templates/SPEC.json +640 -0
  43. package/templates/api-dev-state.json +217 -161
  44. package/templates/api-showcase/_components/APICard.tsx +153 -0
  45. package/templates/api-showcase/_components/APIModal.tsx +375 -0
  46. package/templates/api-showcase/_components/APIShowcase.tsx +231 -0
  47. package/templates/api-showcase/_components/APITester.tsx +522 -0
  48. package/templates/api-showcase/page.tsx +41 -0
  49. package/templates/component/Component.stories.tsx +172 -0
  50. package/templates/component/Component.test.tsx +237 -0
  51. package/templates/component/Component.tsx +86 -0
  52. package/templates/component/Component.types.ts +55 -0
  53. package/templates/component/index.ts +15 -0
  54. package/templates/dev-tools/_components/DevToolsLanding.tsx +320 -0
  55. package/templates/dev-tools/page.tsx +10 -0
  56. package/templates/page/page.e2e.test.ts +218 -0
  57. package/templates/page/page.tsx +42 -0
  58. package/templates/performance-budgets.json +58 -0
  59. package/templates/registry.json +13 -0
  60. package/templates/settings.json +90 -0
  61. package/templates/shared/HeroHeader.tsx +261 -0
  62. package/templates/shared/index.ts +1 -0
  63. package/templates/ui-showcase/_components/PreviewCard.tsx +315 -0
  64. package/templates/ui-showcase/_components/PreviewModal.tsx +676 -0
  65. package/templates/ui-showcase/_components/UIShowcase.tsx +262 -0
  66. package/templates/ui-showcase/page.tsx +26 -0
  67. package/demo/hustle-together/blog/gemini-vs-claude-widgets.html +0 -959
  68. package/demo/hustle-together/blog/interview-driven-api-development.html +0 -1146
  69. package/demo/hustle-together/blog/tdd-for-ai.html +0 -982
  70. package/demo/hustle-together/index.html +0 -1312
  71. package/demo/workflow-demo-v3.5-backup.html +0 -5008
  72. package/demo/workflow-demo.html +0 -6202
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: SessionStart
4
+ Purpose: Detect and prompt for interrupted workflows
5
+
6
+ This hook runs at session start and checks if there are any
7
+ in-progress workflows that were interrupted. If found, it injects
8
+ a prompt asking the user if they want to resume.
9
+
10
+ Added in v3.6.7 for session continuation support.
11
+
12
+ Returns:
13
+ - JSON with additionalContext about interrupted workflows
14
+ """
15
+ import json
16
+ import sys
17
+ import os
18
+ from datetime import datetime
19
+ from pathlib import Path
20
+
21
+ STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
22
+
23
+
24
+ def get_interrupted_workflows(state):
25
+ """Find all workflows that are in_progress but not active."""
26
+ interrupted = []
27
+
28
+ # New format (v3.6.7+): check endpoints object
29
+ if "endpoints" in state:
30
+ active = state.get("active_endpoint")
31
+ for endpoint_name, endpoint_data in state["endpoints"].items():
32
+ status = endpoint_data.get("status", "not_started")
33
+ if status == "in_progress" and endpoint_name != active:
34
+ # Find the current phase
35
+ phases = endpoint_data.get("phases", {})
36
+ current_phase = None
37
+ for phase_name, phase_data in phases.items():
38
+ if phase_data.get("status") == "in_progress":
39
+ current_phase = phase_name
40
+ break
41
+
42
+ interrupted.append({
43
+ "endpoint": endpoint_name,
44
+ "status": status,
45
+ "current_phase": current_phase,
46
+ "started_at": endpoint_data.get("started_at"),
47
+ "interrupted_at": endpoint_data.get("session", {}).get("interrupted_at"),
48
+ "interrupted_phase": endpoint_data.get("session", {}).get("interrupted_phase")
49
+ })
50
+
51
+ # Also check if active endpoint is not fully started
52
+ if active and active in state["endpoints"]:
53
+ active_data = state["endpoints"][active]
54
+ session = active_data.get("session", {})
55
+ if session.get("interrupted_at"):
56
+ # Active endpoint was previously interrupted
57
+ interrupted.insert(0, {
58
+ "endpoint": active,
59
+ "status": active_data.get("status"),
60
+ "current_phase": session.get("interrupted_phase"),
61
+ "started_at": active_data.get("started_at"),
62
+ "interrupted_at": session.get("interrupted_at"),
63
+ "is_active": True
64
+ })
65
+
66
+ # Old format: single endpoint
67
+ elif state.get("endpoint"):
68
+ endpoint = state.get("endpoint")
69
+ phases = state.get("phases", {})
70
+
71
+ # Check if any phase is in_progress
72
+ for phase_name, phase_data in phases.items():
73
+ if phase_data.get("status") == "in_progress":
74
+ interrupted.append({
75
+ "endpoint": endpoint,
76
+ "status": "in_progress",
77
+ "current_phase": phase_name,
78
+ "started_at": state.get("created_at"),
79
+ "is_legacy": True
80
+ })
81
+ break
82
+
83
+ return interrupted
84
+
85
+
86
+ def format_interrupted_message(interrupted):
87
+ """Format a user-friendly message about interrupted workflows."""
88
+ if not interrupted:
89
+ return None
90
+
91
+ lines = [
92
+ "",
93
+ "=" * 60,
94
+ " INTERRUPTED WORKFLOW DETECTED",
95
+ "=" * 60,
96
+ ""
97
+ ]
98
+
99
+ for i, workflow in enumerate(interrupted, 1):
100
+ endpoint = workflow["endpoint"]
101
+ phase = workflow.get("current_phase", "unknown")
102
+ started = workflow.get("started_at", "unknown")
103
+ interrupted_at = workflow.get("interrupted_at", "")
104
+
105
+ lines.append(f"{i}. **{endpoint}**")
106
+ lines.append(f" - Phase: {phase}")
107
+ lines.append(f" - Started: {started}")
108
+ if interrupted_at:
109
+ lines.append(f" - Interrupted: {interrupted_at}")
110
+ lines.append("")
111
+
112
+ lines.extend([
113
+ "To resume an interrupted workflow, use:",
114
+ " /api-continue [endpoint-name]",
115
+ "",
116
+ "Or start a new workflow with:",
117
+ " /api-create [new-endpoint-name]",
118
+ "",
119
+ "=" * 60
120
+ ])
121
+
122
+ return "\n".join(lines)
123
+
124
+
125
+ def main():
126
+ try:
127
+ input_data = json.load(sys.stdin)
128
+ except json.JSONDecodeError:
129
+ input_data = {}
130
+
131
+ # Check if state file exists
132
+ if not STATE_FILE.exists():
133
+ print(json.dumps({"continue": True}))
134
+ sys.exit(0)
135
+
136
+ try:
137
+ state = json.loads(STATE_FILE.read_text())
138
+ except json.JSONDecodeError:
139
+ print(json.dumps({"continue": True}))
140
+ sys.exit(0)
141
+
142
+ # Find interrupted workflows
143
+ interrupted = get_interrupted_workflows(state)
144
+
145
+ if not interrupted:
146
+ print(json.dumps({"continue": True}))
147
+ sys.exit(0)
148
+
149
+ # Format message
150
+ message = format_interrupted_message(interrupted)
151
+
152
+ output = {
153
+ "hookSpecificOutput": {
154
+ "hookEventName": "SessionStart",
155
+ "additionalContext": message,
156
+ "interruptedWorkflows": interrupted
157
+ }
158
+ }
159
+
160
+ print(json.dumps(output))
161
+ sys.exit(0)
162
+
163
+
164
+ if __name__ == "__main__":
165
+ main()
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: PostToolUse for Write/Edit
4
+ Purpose: Trigger accessibility audit after UI component/page implementation
5
+
6
+ This hook runs after Phase 9 (TDD GREEN) for UI workflows. It notifies Claude
7
+ to run axe-core audit on Storybook stories or pages to verify WCAG compliance.
8
+
9
+ Version: 3.10.0
10
+
11
+ Returns:
12
+ - {"continue": true} - Always continues
13
+ - May include "notify" with accessibility check reminder
14
+ - May include "additionalContext" with accessibility guidelines
15
+ """
16
+ import json
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ # State file is in .claude/ directory (sibling to hooks/)
21
+ STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
22
+
23
+ # WCAG 2.1 Level AA Quick Reference
24
+ WCAG_AA_CHECKLIST = [
25
+ "Color contrast: 4.5:1 for normal text, 3:1 for large text",
26
+ "Focus visible: All interactive elements show focus state",
27
+ "Keyboard nav: All functionality accessible via keyboard",
28
+ "Labels: All form inputs have associated labels",
29
+ "Alt text: All images have meaningful alt text",
30
+ "Headings: Proper heading hierarchy (h1-h6)",
31
+ "Touch targets: Min 44x44px for touch targets",
32
+ "Error messages: Clear error identification and suggestions",
33
+ ]
34
+
35
+
36
+ def get_workflow_type(state):
37
+ """Detect the workflow type from state."""
38
+ workflow = state.get("workflow", "")
39
+ if workflow:
40
+ return workflow
41
+
42
+ if state.get("ui_config"):
43
+ mode = state.get("ui_config", {}).get("mode", "")
44
+ return f"ui-create-{mode}" if mode else "ui-create-component"
45
+
46
+ return "api-create"
47
+
48
+
49
+ def get_active_element(state):
50
+ """Get active element name and data."""
51
+ if "elements" in state and "active_element" in state:
52
+ active = state.get("active_element")
53
+ if active and active in state["elements"]:
54
+ return active, state["elements"][active]
55
+ return None, None
56
+
57
+ active = state.get("active_element")
58
+ if active:
59
+ return active, state
60
+
61
+ return None, None
62
+
63
+
64
+ def is_verify_phase(phases):
65
+ """Check if we're in or just completed the verify phase."""
66
+ verify = phases.get("verify", {})
67
+ tdd_green = phases.get("tdd_green", {})
68
+
69
+ # After green, before or during verify
70
+ return (
71
+ tdd_green.get("status") == "complete" and
72
+ verify.get("status") in ["not_started", "in_progress"]
73
+ )
74
+
75
+
76
+ def get_accessibility_level(state, element_data):
77
+ """Get the accessibility level requirement."""
78
+ ui_config = state.get("ui_config", {})
79
+ if not ui_config and element_data:
80
+ ui_config = element_data.get("ui_config", {})
81
+
82
+ return ui_config.get("accessibility_level", "AA")
83
+
84
+
85
+ def generate_a11y_commands(element_name, workflow_type):
86
+ """Generate accessibility testing commands."""
87
+ commands = []
88
+
89
+ if "component" in workflow_type:
90
+ commands.extend([
91
+ f"# Storybook accessibility check",
92
+ f"pnpm storybook --ci",
93
+ f"# Then run axe in browser or:",
94
+ f"pnpm dlx @storybook/test-runner --url http://localhost:6006",
95
+ f"",
96
+ f"# Or manual axe-core check:",
97
+ f"pnpm dlx @axe-core/cli http://localhost:6006/?path=/story/{element_name.lower()}--default"
98
+ ])
99
+ else:
100
+ commands.extend([
101
+ f"# Page accessibility check",
102
+ f"pnpm dev",
103
+ f"# Then in another terminal:",
104
+ f"pnpm dlx @axe-core/cli http://localhost:3000/{element_name}",
105
+ f"",
106
+ f"# Or use Playwright accessibility tests:",
107
+ f"pnpm test:e2e --grep 'accessibility'"
108
+ ])
109
+
110
+ return "\n".join(commands)
111
+
112
+
113
+ def main():
114
+ # Read hook input from stdin
115
+ try:
116
+ input_data = json.load(sys.stdin)
117
+ except json.JSONDecodeError:
118
+ print(json.dumps({"continue": True}))
119
+ sys.exit(0)
120
+
121
+ tool_name = input_data.get("tool_name", "")
122
+
123
+ # Only process Write/Edit operations
124
+ if tool_name not in ["Write", "Edit"]:
125
+ print(json.dumps({"continue": True}))
126
+ sys.exit(0)
127
+
128
+ # Check if state file exists
129
+ if not STATE_FILE.exists():
130
+ print(json.dumps({"continue": True}))
131
+ sys.exit(0)
132
+
133
+ # Load state
134
+ try:
135
+ state = json.loads(STATE_FILE.read_text())
136
+ except json.JSONDecodeError:
137
+ print(json.dumps({"continue": True}))
138
+ sys.exit(0)
139
+
140
+ workflow_type = get_workflow_type(state)
141
+
142
+ # Only apply for UI workflows
143
+ if not workflow_type.startswith("ui-create"):
144
+ print(json.dumps({"continue": True}))
145
+ sys.exit(0)
146
+
147
+ # Get active element
148
+ element_name, element_data = get_active_element(state)
149
+ if not element_name or not element_data:
150
+ print(json.dumps({"continue": True}))
151
+ sys.exit(0)
152
+
153
+ phases = element_data.get("phases", {}) if element_data else state.get("phases", {})
154
+
155
+ # Check if we should trigger a11y audit (after TDD Green)
156
+ if not is_verify_phase(phases):
157
+ print(json.dumps({"continue": True}))
158
+ sys.exit(0)
159
+
160
+ # Get accessibility level
161
+ a11y_level = get_accessibility_level(state, element_data)
162
+
163
+ # Generate audit commands
164
+ commands = generate_a11y_commands(element_name, workflow_type)
165
+
166
+ # Build accessibility context
167
+ checklist = "\n".join([f" - {item}" for item in WCAG_AA_CHECKLIST])
168
+
169
+ context = f"""
170
+ ## Accessibility Audit Required (WCAG 2.1 {a11y_level})
171
+
172
+ The TDD Green phase is complete. Before marking verify as complete, run an accessibility audit.
173
+
174
+ ### Quick Commands
175
+ ```bash
176
+ {commands}
177
+ ```
178
+
179
+ ### WCAG 2.1 {a11y_level} Checklist
180
+ {checklist}
181
+
182
+ ### 4-Step Verification for UI
183
+ 1. **Responsive**: Test at 320px, 768px, 1024px, 1440px
184
+ 2. **Data Binding**: Verify all data sources load correctly
185
+ 3. **Tests**: All unit/e2e tests pass
186
+ 4. **Accessibility**: Run axe-core, fix any violations
187
+
188
+ If violations are found, fix them before completing the verify phase.
189
+ """
190
+
191
+ output = {
192
+ "continue": True,
193
+ "notify": f"Accessibility audit required for {element_name} (WCAG 2.1 {a11y_level})",
194
+ "additionalContext": context
195
+ }
196
+
197
+ print(json.dumps(output))
198
+ sys.exit(0)
199
+
200
+
201
+ if __name__ == "__main__":
202
+ main()
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: PreToolUse for Write/Edit
4
+ Purpose: Inject brand guide content and validate color compliance during UI implementation
5
+
6
+ This hook runs before writing component/page files. When use_brand_guide=true
7
+ in the state, it logs the brand guide summary to remind Claude to apply
8
+ consistent branding and validates that only approved colors are used.
9
+
10
+ Version: 3.10.0
11
+
12
+ Returns:
13
+ - {"continue": true} - Always continues (notifies on violations)
14
+ - May include "notify" with brand guide summary or color violations
15
+ """
16
+ import json
17
+ import sys
18
+ import re
19
+ from pathlib import Path
20
+
21
+ # State file is in .claude/ directory (sibling to hooks/)
22
+ STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
23
+ BRAND_GUIDE_FILE = Path(__file__).parent.parent / "BRAND_GUIDE.md"
24
+
25
+
26
+ def extract_brand_colors(content):
27
+ """Extract all brand colors from brand guide markdown.
28
+
29
+ Returns a set of allowed colors (hex values, CSS variables, Tailwind classes).
30
+ """
31
+ allowed_colors = set()
32
+
33
+ # Extract hex colors from brand guide
34
+ hex_pattern = r'#[0-9A-Fa-f]{3,8}'
35
+ for match in re.finditer(hex_pattern, content):
36
+ allowed_colors.add(match.group(0).upper())
37
+
38
+ # Extract CSS variable names
39
+ css_var_pattern = r'var\(--([a-zA-Z0-9-]+)\)'
40
+ for match in re.finditer(css_var_pattern, content):
41
+ allowed_colors.add(f"--{match.group(1)}")
42
+
43
+ # Extract Tailwind color classes mentioned in brand guide
44
+ tailwind_pattern = r'(?:bg|text|border|ring)-([a-zA-Z]+-[0-9]+|[a-zA-Z]+)'
45
+ for match in re.finditer(tailwind_pattern, content):
46
+ allowed_colors.add(match.group(0))
47
+
48
+ # Always allow these common values
49
+ allowed_colors.update([
50
+ 'transparent', 'inherit', 'currentColor', 'current',
51
+ 'white', 'black', 'bg-white', 'bg-black', 'text-white', 'text-black',
52
+ 'bg-transparent', 'border-transparent',
53
+ # Common utility colors
54
+ 'bg-background', 'text-foreground', 'border-border',
55
+ 'bg-primary', 'text-primary', 'border-primary',
56
+ 'bg-secondary', 'text-secondary', 'border-secondary',
57
+ 'bg-accent', 'text-accent', 'border-accent',
58
+ 'bg-muted', 'text-muted', 'border-muted',
59
+ 'bg-destructive', 'text-destructive', 'border-destructive',
60
+ ])
61
+
62
+ return allowed_colors
63
+
64
+
65
+ def extract_colors_from_code(code_content):
66
+ """Extract colors used in component code.
67
+
68
+ Returns a list of color usages found.
69
+ """
70
+ used_colors = []
71
+
72
+ # Find hex colors
73
+ hex_pattern = r'#[0-9A-Fa-f]{3,8}'
74
+ for match in re.finditer(hex_pattern, code_content):
75
+ used_colors.append(('hex', match.group(0).upper()))
76
+
77
+ # Find Tailwind color classes (excluding allowed dynamic patterns)
78
+ tailwind_pattern = r'(?:bg|text|border|ring|from|to|via)-([a-zA-Z]+-[0-9]+)'
79
+ for match in re.finditer(tailwind_pattern, code_content):
80
+ # Skip if it's a dynamic value like bg-[#xxx]
81
+ full_match = match.group(0)
82
+ if '[' not in full_match:
83
+ used_colors.append(('tailwind', full_match))
84
+
85
+ # Find inline style colors
86
+ style_pattern = r'(?:color|backgroundColor|borderColor):\s*["\']([^"\']+)["\']'
87
+ for match in re.finditer(style_pattern, code_content):
88
+ value = match.group(1)
89
+ if value.startswith('#'):
90
+ used_colors.append(('style', value.upper()))
91
+
92
+ return used_colors
93
+
94
+
95
+ def validate_color_compliance(code_content, allowed_colors):
96
+ """Check if code uses only brand-approved colors.
97
+
98
+ Returns list of violations found.
99
+ """
100
+ violations = []
101
+ used_colors = extract_colors_from_code(code_content)
102
+
103
+ for color_type, color_value in used_colors:
104
+ # Check if color is allowed
105
+ is_allowed = False
106
+
107
+ if color_type == 'hex':
108
+ is_allowed = color_value in allowed_colors
109
+ elif color_type == 'tailwind':
110
+ is_allowed = color_value in allowed_colors or color_value.split('-')[0] in ['bg', 'text', 'border']
111
+ elif color_type == 'style':
112
+ is_allowed = color_value in allowed_colors
113
+
114
+ if not is_allowed:
115
+ # Check against all allowed colors more loosely
116
+ if color_value not in allowed_colors:
117
+ violations.append(f"{color_type}: {color_value}")
118
+
119
+ return violations
120
+
121
+
122
+ def extract_brand_summary(content):
123
+ """Extract key brand values from brand guide markdown."""
124
+ summary = []
125
+
126
+ lines = content.split("\n")
127
+ current_section = ""
128
+
129
+ for line in lines:
130
+ line = line.strip()
131
+
132
+ # Track section
133
+ if line.startswith("## "):
134
+ current_section = line[3:].lower()
135
+ continue
136
+
137
+ # Extract key values
138
+ if line.startswith("- **") and ":" in line:
139
+ # Parse "- **Key:** Value" format
140
+ try:
141
+ key_part = line.split(":**")[0].replace("- **", "")
142
+ value_part = line.split(":**")[1].strip()
143
+
144
+ # Only include primary brand values
145
+ if current_section == "colors" and key_part in ["Primary", "Accent", "Background"]:
146
+ summary.append(f"{key_part}: {value_part}")
147
+ elif current_section == "typography" and key_part in ["Headings", "Body"]:
148
+ summary.append(f"{key_part}: {value_part}")
149
+ elif current_section == "component styling" and key_part in ["Border Radius", "Focus Ring"]:
150
+ summary.append(f"{key_part}: {value_part}")
151
+ except IndexError:
152
+ continue
153
+
154
+ return summary
155
+
156
+
157
+ def main():
158
+ # Read hook input from stdin
159
+ try:
160
+ input_data = json.load(sys.stdin)
161
+ except json.JSONDecodeError:
162
+ print(json.dumps({"continue": True}))
163
+ sys.exit(0)
164
+
165
+ tool_name = input_data.get("tool_name", "")
166
+ tool_input = input_data.get("tool_input", {})
167
+
168
+ # Only check Write/Edit operations
169
+ if tool_name not in ["Write", "Edit"]:
170
+ print(json.dumps({"continue": True}))
171
+ sys.exit(0)
172
+
173
+ # Check if targeting component or page files
174
+ file_path = tool_input.get("file_path", "")
175
+ is_component = "/components/" in file_path and file_path.endswith(".tsx")
176
+ is_page = "/app/" in file_path and "page.tsx" in file_path
177
+
178
+ if not is_component and not is_page:
179
+ print(json.dumps({"continue": True}))
180
+ sys.exit(0)
181
+
182
+ # Check if state file exists
183
+ if not STATE_FILE.exists():
184
+ print(json.dumps({"continue": True}))
185
+ sys.exit(0)
186
+
187
+ # Load state
188
+ try:
189
+ state = json.loads(STATE_FILE.read_text())
190
+ except json.JSONDecodeError:
191
+ print(json.dumps({"continue": True}))
192
+ sys.exit(0)
193
+
194
+ workflow = state.get("workflow", "")
195
+
196
+ # Only apply for UI workflows
197
+ if workflow not in ["ui-create-component", "ui-create-page"]:
198
+ print(json.dumps({"continue": True}))
199
+ sys.exit(0)
200
+
201
+ # Check if brand guide is enabled
202
+ ui_config = state.get("ui_config", {})
203
+ use_brand_guide = ui_config.get("use_brand_guide", False)
204
+
205
+ if not use_brand_guide:
206
+ print(json.dumps({"continue": True}))
207
+ sys.exit(0)
208
+
209
+ # Check if brand guide file exists
210
+ if not BRAND_GUIDE_FILE.exists():
211
+ print(json.dumps({"continue": True}))
212
+ sys.exit(0)
213
+
214
+ # Extract brand summary
215
+ brand_content = BRAND_GUIDE_FILE.read_text()
216
+ summary = extract_brand_summary(brand_content)
217
+
218
+ # For Edit operations, check color compliance
219
+ tool_input = input_data.get("tool_input", {})
220
+ if tool_name == "Edit":
221
+ new_content = tool_input.get("new_string", "")
222
+ if new_content:
223
+ allowed_colors = extract_brand_colors(brand_content)
224
+ violations = validate_color_compliance(new_content, allowed_colors)
225
+
226
+ if violations:
227
+ notify_msg = f"⚠️ Brand color check: {len(violations)} potential non-brand colors: " + ", ".join(violations[:3])
228
+ print(json.dumps({"continue": True, "notify": notify_msg}))
229
+ sys.exit(0)
230
+
231
+ if summary:
232
+ notify_msg = "Applying brand guide: " + " | ".join(summary[:5])
233
+ print(json.dumps({"continue": True, "notify": notify_msg}))
234
+ else:
235
+ print(json.dumps({"continue": True}))
236
+
237
+ sys.exit(0)
238
+
239
+
240
+ if __name__ == "__main__":
241
+ main()