@hustle-together/api-dev-tools 3.9.2 → 3.10.1
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.
- package/README.md +319 -22
- package/bin/cli.js +61 -13
- package/commands/hustle-api-create.md +13 -13
- package/commands/hustle-ui-create-page.md +933 -0
- package/hooks/api-workflow-check.py +160 -2
- package/hooks/check-api-routes.py +168 -0
- package/hooks/enforce-a11y-audit.py +202 -0
- package/hooks/enforce-brand-guide.py +115 -5
- package/hooks/enforce-page-components.py +186 -0
- package/hooks/enforce-page-data-schema.py +155 -0
- package/hooks/generate-manifest-entry.py +181 -1
- package/hooks/session-startup.py +95 -5
- package/hooks/update-ui-showcase.py +67 -3
- package/package.json +1 -2
- package/templates/api-dev-state.json +39 -1
- package/templates/settings.json +16 -0
- package/templates/shared/HeroHeader.tsx +1 -1
- package/demo/hustle-together/blog/gemini-vs-claude-widgets.html +0 -959
- package/demo/hustle-together/blog/interview-driven-api-development.html +0 -1146
- package/demo/hustle-together/blog/tdd-for-ai.html +0 -982
- package/demo/hustle-together/index.html +0 -1312
- package/demo/workflow-demo-v3.5-backup.html +0 -5008
- package/demo/workflow-demo.html +0 -6202
- /package/templates/api-showcase/{APICard.tsx → _components/APICard.tsx} +0 -0
- /package/templates/api-showcase/{APIModal.tsx → _components/APIModal.tsx} +0 -0
- /package/templates/api-showcase/{APIShowcase.tsx → _components/APIShowcase.tsx} +0 -0
- /package/templates/api-showcase/{APITester.tsx → _components/APITester.tsx} +0 -0
- /package/templates/ui-showcase/{PreviewCard.tsx → _components/PreviewCard.tsx} +0 -0
- /package/templates/ui-showcase/{PreviewModal.tsx → _components/PreviewModal.tsx} +0 -0
- /package/templates/ui-showcase/{UIShowcase.tsx → _components/UIShowcase.tsx} +0 -0
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
3
|
Hook: PreToolUse for Write/Edit
|
|
4
|
-
Purpose: Inject brand guide content during UI implementation
|
|
4
|
+
Purpose: Inject brand guide content and validate color compliance during UI implementation
|
|
5
5
|
|
|
6
6
|
This hook runs before writing component/page files. When use_brand_guide=true
|
|
7
7
|
in the state, it logs the brand guide summary to remind Claude to apply
|
|
8
|
-
consistent branding.
|
|
8
|
+
consistent branding and validates that only approved colors are used.
|
|
9
9
|
|
|
10
|
-
Version: 3.
|
|
10
|
+
Version: 3.10.0
|
|
11
11
|
|
|
12
12
|
Returns:
|
|
13
|
-
- {"continue": true} - Always continues
|
|
14
|
-
- May include "notify" with brand guide summary
|
|
13
|
+
- {"continue": true} - Always continues (notifies on violations)
|
|
14
|
+
- May include "notify" with brand guide summary or color violations
|
|
15
15
|
"""
|
|
16
16
|
import json
|
|
17
17
|
import sys
|
|
18
|
+
import re
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
|
|
20
21
|
# State file is in .claude/ directory (sibling to hooks/)
|
|
@@ -22,6 +23,102 @@ STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
|
|
|
22
23
|
BRAND_GUIDE_FILE = Path(__file__).parent.parent / "BRAND_GUIDE.md"
|
|
23
24
|
|
|
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
|
+
|
|
25
122
|
def extract_brand_summary(content):
|
|
26
123
|
"""Extract key brand values from brand guide markdown."""
|
|
27
124
|
summary = []
|
|
@@ -118,6 +215,19 @@ def main():
|
|
|
118
215
|
brand_content = BRAND_GUIDE_FILE.read_text()
|
|
119
216
|
summary = extract_brand_summary(brand_content)
|
|
120
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
|
+
|
|
121
231
|
if summary:
|
|
122
232
|
notify_msg = "Applying brand guide: " + " | ".join(summary[:5])
|
|
123
233
|
print(json.dumps({"continue": True, "notify": notify_msg}))
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hook: enforce-page-components.py
|
|
4
|
+
Trigger: PreToolUse (Write|Edit)
|
|
5
|
+
Purpose: Check that components from registry are considered before creating new ones
|
|
6
|
+
|
|
7
|
+
For ui-create-page workflow, ensures Phase 5 (PAGE ANALYSIS) is complete and
|
|
8
|
+
encourages reuse of existing components from the registry.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
def load_state():
|
|
17
|
+
"""Load the api-dev-state.json file"""
|
|
18
|
+
state_paths = [
|
|
19
|
+
".claude/api-dev-state.json",
|
|
20
|
+
os.path.join(os.environ.get("CLAUDE_PROJECT_DIR", ""), ".claude/api-dev-state.json")
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
for path in state_paths:
|
|
24
|
+
if os.path.exists(path):
|
|
25
|
+
with open(path, 'r') as f:
|
|
26
|
+
return json.load(f)
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
def load_registry():
|
|
30
|
+
"""Load the registry.json file"""
|
|
31
|
+
registry_paths = [
|
|
32
|
+
".claude/registry.json",
|
|
33
|
+
os.path.join(os.environ.get("CLAUDE_PROJECT_DIR", ""), ".claude/registry.json")
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
for path in registry_paths:
|
|
37
|
+
if os.path.exists(path):
|
|
38
|
+
with open(path, 'r') as f:
|
|
39
|
+
return json.load(f)
|
|
40
|
+
return {}
|
|
41
|
+
|
|
42
|
+
def is_page_workflow(state):
|
|
43
|
+
"""Check if current workflow is ui-create-page"""
|
|
44
|
+
workflow = state.get("workflow", "")
|
|
45
|
+
return workflow == "ui-create-page"
|
|
46
|
+
|
|
47
|
+
def get_active_element(state):
|
|
48
|
+
"""Get the active element being worked on"""
|
|
49
|
+
active = state.get("active_element", "")
|
|
50
|
+
if not active:
|
|
51
|
+
active = state.get("endpoint", "")
|
|
52
|
+
return active
|
|
53
|
+
|
|
54
|
+
def is_creating_new_component(file_path):
|
|
55
|
+
"""Check if the file path suggests creating a new standalone component"""
|
|
56
|
+
if not file_path:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
# Patterns that suggest a new standalone component (not page-specific)
|
|
60
|
+
standalone_patterns = [
|
|
61
|
+
r"src/components/[A-Z]",
|
|
62
|
+
r"components/ui/",
|
|
63
|
+
r"components/shared/",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
return any(re.search(pattern, file_path) for pattern in standalone_patterns)
|
|
67
|
+
|
|
68
|
+
def is_page_specific_component(file_path, element_name):
|
|
69
|
+
"""Check if the file is a page-specific component (allowed)"""
|
|
70
|
+
if not file_path or not element_name:
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
# Page-specific components in _components folder are allowed
|
|
74
|
+
patterns = [
|
|
75
|
+
f"src/app/{element_name}/_components/",
|
|
76
|
+
f"app/{element_name}/_components/",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
return any(pattern in file_path for pattern in patterns)
|
|
80
|
+
|
|
81
|
+
def check_page_analysis_phase(state, element_name):
|
|
82
|
+
"""Check if page analysis phase is complete"""
|
|
83
|
+
elements = state.get("elements", {})
|
|
84
|
+
element = elements.get(element_name, {})
|
|
85
|
+
phases = element.get("phases", {})
|
|
86
|
+
|
|
87
|
+
page_analysis = phases.get("page_analysis", {})
|
|
88
|
+
return page_analysis.get("status") == "complete"
|
|
89
|
+
|
|
90
|
+
def get_available_components(registry):
|
|
91
|
+
"""Get list of available components from registry"""
|
|
92
|
+
components = registry.get("components", {})
|
|
93
|
+
return list(components.keys())
|
|
94
|
+
|
|
95
|
+
def main():
|
|
96
|
+
try:
|
|
97
|
+
# Read tool input from stdin
|
|
98
|
+
input_data = json.loads(sys.stdin.read())
|
|
99
|
+
tool_name = input_data.get("tool_name", "")
|
|
100
|
+
tool_input = input_data.get("tool_input", {})
|
|
101
|
+
|
|
102
|
+
# Only check Write tool
|
|
103
|
+
if tool_name != "Write":
|
|
104
|
+
print(json.dumps({"decision": "allow"}))
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
file_path = tool_input.get("file_path", "")
|
|
108
|
+
|
|
109
|
+
# Load state
|
|
110
|
+
state = load_state()
|
|
111
|
+
if not state:
|
|
112
|
+
print(json.dumps({"decision": "allow"}))
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
# Only apply to ui-create-page workflow
|
|
116
|
+
if not is_page_workflow(state):
|
|
117
|
+
print(json.dumps({"decision": "allow"}))
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
element_name = get_active_element(state)
|
|
121
|
+
if not element_name:
|
|
122
|
+
print(json.dumps({"decision": "allow"}))
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
# Allow page-specific components (in _components folder)
|
|
126
|
+
if is_page_specific_component(file_path, element_name):
|
|
127
|
+
print(json.dumps({"decision": "allow"}))
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
# Check if creating a new standalone component
|
|
131
|
+
if is_creating_new_component(file_path):
|
|
132
|
+
# Check if page analysis phase is complete
|
|
133
|
+
if not check_page_analysis_phase(state, element_name):
|
|
134
|
+
# Load registry to show available components
|
|
135
|
+
registry = load_registry()
|
|
136
|
+
available = get_available_components(registry)
|
|
137
|
+
|
|
138
|
+
component_list = "\n".join([f" - {c}" for c in available[:10]])
|
|
139
|
+
if len(available) > 10:
|
|
140
|
+
component_list += f"\n ... and {len(available) - 10} more"
|
|
141
|
+
|
|
142
|
+
print(json.dumps({
|
|
143
|
+
"decision": "block",
|
|
144
|
+
"reason": f"""
|
|
145
|
+
PAGE ANALYSIS REQUIRED (Phase 5)
|
|
146
|
+
|
|
147
|
+
You are creating a new standalone component, but Page Analysis phase is not complete.
|
|
148
|
+
|
|
149
|
+
Before creating new components:
|
|
150
|
+
1. Check the registry for existing components
|
|
151
|
+
2. Decide which existing components to reuse
|
|
152
|
+
3. Update state: phases.page_analysis.status = "complete"
|
|
153
|
+
|
|
154
|
+
Available Components in Registry:
|
|
155
|
+
{component_list if available else " (No components registered yet)"}
|
|
156
|
+
|
|
157
|
+
If you need a NEW component, consider:
|
|
158
|
+
- Using /ui-create to properly create and document it
|
|
159
|
+
- Or create a page-specific component in src/app/{element_name}/_components/
|
|
160
|
+
"""
|
|
161
|
+
}))
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
# Even if phase is complete, notify about registry
|
|
165
|
+
registry = load_registry()
|
|
166
|
+
available = get_available_components(registry)
|
|
167
|
+
|
|
168
|
+
if available:
|
|
169
|
+
print(json.dumps({
|
|
170
|
+
"decision": "allow",
|
|
171
|
+
"message": f"Note: {len(available)} components available in registry. Consider reusing existing components."
|
|
172
|
+
}))
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
# Allow everything else
|
|
176
|
+
print(json.dumps({"decision": "allow"}))
|
|
177
|
+
|
|
178
|
+
except Exception as e:
|
|
179
|
+
# On error, allow to avoid blocking workflow
|
|
180
|
+
print(json.dumps({
|
|
181
|
+
"decision": "allow",
|
|
182
|
+
"error": str(e)
|
|
183
|
+
}))
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
main()
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hook: enforce-page-data-schema.py
|
|
4
|
+
Trigger: PreToolUse (Write|Edit)
|
|
5
|
+
Purpose: Validate that API response types are defined before page implementation
|
|
6
|
+
|
|
7
|
+
For ui-create-page workflow, ensures Phase 6 (DATA SCHEMA) is complete before
|
|
8
|
+
allowing page implementation in Phase 9.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
def load_state():
|
|
17
|
+
"""Load the api-dev-state.json file"""
|
|
18
|
+
state_paths = [
|
|
19
|
+
".claude/api-dev-state.json",
|
|
20
|
+
os.path.join(os.environ.get("CLAUDE_PROJECT_DIR", ""), ".claude/api-dev-state.json")
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
for path in state_paths:
|
|
24
|
+
if os.path.exists(path):
|
|
25
|
+
with open(path, 'r') as f:
|
|
26
|
+
return json.load(f)
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
def is_page_workflow(state):
|
|
30
|
+
"""Check if current workflow is ui-create-page"""
|
|
31
|
+
workflow = state.get("workflow", "")
|
|
32
|
+
return workflow == "ui-create-page"
|
|
33
|
+
|
|
34
|
+
def get_active_element(state):
|
|
35
|
+
"""Get the active element being worked on"""
|
|
36
|
+
active = state.get("active_element", "")
|
|
37
|
+
if not active:
|
|
38
|
+
# Fall back to endpoint for older state files
|
|
39
|
+
active = state.get("endpoint", "")
|
|
40
|
+
return active
|
|
41
|
+
|
|
42
|
+
def is_page_file(file_path, element_name):
|
|
43
|
+
"""Check if the file being written is a page implementation file"""
|
|
44
|
+
if not file_path or not element_name:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
patterns = [
|
|
48
|
+
f"src/app/{element_name}/page.tsx",
|
|
49
|
+
f"src/app/{element_name}/layout.tsx",
|
|
50
|
+
f"src/app/{element_name}/_components/",
|
|
51
|
+
f"app/{element_name}/page.tsx",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
return any(pattern in file_path for pattern in patterns)
|
|
55
|
+
|
|
56
|
+
def is_types_file(file_path, element_name):
|
|
57
|
+
"""Check if the file being written is the types/schema file"""
|
|
58
|
+
if not file_path or not element_name:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
patterns = [
|
|
62
|
+
f"src/app/{element_name}/_types/",
|
|
63
|
+
f"src/app/{element_name}/types.ts",
|
|
64
|
+
f"src/lib/schemas/{element_name}",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
return any(pattern in file_path for pattern in patterns)
|
|
68
|
+
|
|
69
|
+
def is_test_file(file_path):
|
|
70
|
+
"""Check if file is a test file"""
|
|
71
|
+
return "__tests__" in file_path or ".test." in file_path or ".spec." in file_path
|
|
72
|
+
|
|
73
|
+
def check_data_schema_phase(state, element_name):
|
|
74
|
+
"""Check if data schema phase is complete"""
|
|
75
|
+
elements = state.get("elements", {})
|
|
76
|
+
element = elements.get(element_name, {})
|
|
77
|
+
phases = element.get("phases", {})
|
|
78
|
+
|
|
79
|
+
# Check data_schema phase
|
|
80
|
+
data_schema = phases.get("data_schema", {})
|
|
81
|
+
return data_schema.get("status") == "complete"
|
|
82
|
+
|
|
83
|
+
def main():
|
|
84
|
+
try:
|
|
85
|
+
# Read tool input from stdin
|
|
86
|
+
input_data = json.loads(sys.stdin.read())
|
|
87
|
+
tool_name = input_data.get("tool_name", "")
|
|
88
|
+
tool_input = input_data.get("tool_input", {})
|
|
89
|
+
|
|
90
|
+
# Only check Write and Edit tools
|
|
91
|
+
if tool_name not in ["Write", "Edit"]:
|
|
92
|
+
print(json.dumps({"decision": "allow"}))
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
file_path = tool_input.get("file_path", "")
|
|
96
|
+
|
|
97
|
+
# Load state
|
|
98
|
+
state = load_state()
|
|
99
|
+
if not state:
|
|
100
|
+
print(json.dumps({"decision": "allow"}))
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# Only apply to ui-create-page workflow
|
|
104
|
+
if not is_page_workflow(state):
|
|
105
|
+
print(json.dumps({"decision": "allow"}))
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
element_name = get_active_element(state)
|
|
109
|
+
if not element_name:
|
|
110
|
+
print(json.dumps({"decision": "allow"}))
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
# Allow writing types/schema files (Phase 6)
|
|
114
|
+
if is_types_file(file_path, element_name):
|
|
115
|
+
print(json.dumps({"decision": "allow"}))
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
# Allow writing test files (Phase 8)
|
|
119
|
+
if is_test_file(file_path):
|
|
120
|
+
print(json.dumps({"decision": "allow"}))
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
# Check if writing page implementation file
|
|
124
|
+
if is_page_file(file_path, element_name):
|
|
125
|
+
# Verify data schema phase is complete
|
|
126
|
+
if not check_data_schema_phase(state, element_name):
|
|
127
|
+
print(json.dumps({
|
|
128
|
+
"decision": "block",
|
|
129
|
+
"reason": f"""
|
|
130
|
+
DATA SCHEMA REQUIRED (Phase 6)
|
|
131
|
+
|
|
132
|
+
You are trying to implement page code, but the data schema phase is not complete.
|
|
133
|
+
|
|
134
|
+
Before writing page implementation:
|
|
135
|
+
1. Define TypeScript interfaces for API responses
|
|
136
|
+
2. Create types in src/app/{element_name}/_types/index.ts
|
|
137
|
+
3. Update state: phases.data_schema.status = "complete"
|
|
138
|
+
|
|
139
|
+
Page implementation requires knowing the data structure first.
|
|
140
|
+
"""
|
|
141
|
+
}))
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
# Allow everything else
|
|
145
|
+
print(json.dumps({"decision": "allow"}))
|
|
146
|
+
|
|
147
|
+
except Exception as e:
|
|
148
|
+
# On error, allow to avoid blocking workflow
|
|
149
|
+
print(json.dumps({
|
|
150
|
+
"decision": "allow",
|
|
151
|
+
"error": str(e)
|
|
152
|
+
}))
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
main()
|