@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.
- package/README.md +5599 -258
- package/bin/cli.js +395 -20
- package/commands/README.md +459 -71
- package/commands/hustle-api-continue.md +158 -0
- package/commands/{api-create.md → hustle-api-create.md} +35 -15
- package/commands/{api-env.md → hustle-api-env.md} +4 -4
- package/commands/{api-interview.md → hustle-api-interview.md} +1 -1
- package/commands/{api-research.md → hustle-api-research.md} +3 -3
- package/commands/hustle-api-sessions.md +149 -0
- package/commands/{api-status.md → hustle-api-status.md} +16 -16
- package/commands/{api-verify.md → hustle-api-verify.md} +2 -2
- package/commands/hustle-combine.md +763 -0
- package/commands/hustle-ui-create-page.md +933 -0
- package/commands/hustle-ui-create.md +825 -0
- package/hooks/api-workflow-check.py +545 -21
- package/hooks/cache-research.py +337 -0
- package/hooks/check-api-routes.py +168 -0
- package/hooks/check-playwright-setup.py +103 -0
- package/hooks/check-storybook-setup.py +81 -0
- package/hooks/detect-interruption.py +165 -0
- package/hooks/enforce-a11y-audit.py +202 -0
- package/hooks/enforce-brand-guide.py +241 -0
- package/hooks/enforce-documentation.py +60 -8
- package/hooks/enforce-freshness.py +184 -0
- package/hooks/enforce-page-components.py +186 -0
- package/hooks/enforce-page-data-schema.py +155 -0
- package/hooks/enforce-questions-sourced.py +146 -0
- package/hooks/enforce-schema-from-interview.py +248 -0
- package/hooks/enforce-ui-disambiguation.py +108 -0
- package/hooks/enforce-ui-interview.py +130 -0
- package/hooks/generate-manifest-entry.py +1161 -0
- package/hooks/session-logger.py +297 -0
- package/hooks/session-startup.py +160 -15
- package/hooks/track-scope-coverage.py +220 -0
- package/hooks/track-tool-use.py +81 -1
- package/hooks/update-api-showcase.py +149 -0
- package/hooks/update-registry.py +352 -0
- package/hooks/update-ui-showcase.py +212 -0
- package/package.json +8 -3
- package/templates/BRAND_GUIDE.md +299 -0
- package/templates/CLAUDE-SECTION.md +56 -24
- package/templates/SPEC.json +640 -0
- package/templates/api-dev-state.json +217 -161
- package/templates/api-showcase/_components/APICard.tsx +153 -0
- package/templates/api-showcase/_components/APIModal.tsx +375 -0
- package/templates/api-showcase/_components/APIShowcase.tsx +231 -0
- package/templates/api-showcase/_components/APITester.tsx +522 -0
- package/templates/api-showcase/page.tsx +41 -0
- package/templates/component/Component.stories.tsx +172 -0
- package/templates/component/Component.test.tsx +237 -0
- package/templates/component/Component.tsx +86 -0
- package/templates/component/Component.types.ts +55 -0
- package/templates/component/index.ts +15 -0
- package/templates/dev-tools/_components/DevToolsLanding.tsx +320 -0
- package/templates/dev-tools/page.tsx +10 -0
- package/templates/page/page.e2e.test.ts +218 -0
- package/templates/page/page.tsx +42 -0
- package/templates/performance-budgets.json +58 -0
- package/templates/registry.json +13 -0
- package/templates/settings.json +90 -0
- package/templates/shared/HeroHeader.tsx +261 -0
- package/templates/shared/index.ts +1 -0
- package/templates/ui-showcase/_components/PreviewCard.tsx +315 -0
- package/templates/ui-showcase/_components/PreviewModal.tsx +676 -0
- package/templates/ui-showcase/_components/UIShowcase.tsx +262 -0
- package/templates/ui-showcase/page.tsx +26 -0
- 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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Hook: PreToolUse for Write/Edit (and Stop)
|
|
4
4
|
Purpose: Block completion until documentation confirmed WITH USER REVIEW
|
|
5
5
|
|
|
6
|
-
Phase
|
|
6
|
+
Phase 12 (Documentation) requires:
|
|
7
7
|
1. Update api-tests-manifest.json
|
|
8
8
|
2. Cache research to .claude/research/
|
|
9
9
|
3. Update OpenAPI spec if applicable
|
|
@@ -14,12 +14,50 @@ Phase 21 (Documentation) requires:
|
|
|
14
14
|
Returns:
|
|
15
15
|
- {"permissionDecision": "allow"} - Let the tool run
|
|
16
16
|
- {"permissionDecision": "deny", "reason": "..."} - Block with explanation
|
|
17
|
+
|
|
18
|
+
Updated in v3.6.7:
|
|
19
|
+
- Support multi-API state structure
|
|
20
|
+
- Don't block on missing cache files (cache-research.py creates them)
|
|
21
|
+
- Check actual file existence, not just state flags
|
|
17
22
|
"""
|
|
18
23
|
import json
|
|
19
24
|
import sys
|
|
20
25
|
from pathlib import Path
|
|
21
26
|
|
|
22
27
|
STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
|
|
28
|
+
RESEARCH_DIR = Path(__file__).parent.parent / "research"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_active_endpoint(state):
|
|
32
|
+
"""Get active endpoint - supports both old and new state formats."""
|
|
33
|
+
# New format (v3.6.7+): endpoints object with active_endpoint pointer
|
|
34
|
+
if "endpoints" in state and "active_endpoint" in state:
|
|
35
|
+
active = state.get("active_endpoint")
|
|
36
|
+
if active and active in state["endpoints"]:
|
|
37
|
+
return active, state["endpoints"][active]
|
|
38
|
+
return None, None
|
|
39
|
+
|
|
40
|
+
# Old format: single endpoint field
|
|
41
|
+
endpoint = state.get("endpoint")
|
|
42
|
+
if endpoint:
|
|
43
|
+
return endpoint, state
|
|
44
|
+
|
|
45
|
+
return None, None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def check_research_cache_exists(endpoint):
|
|
49
|
+
"""Check if research cache files actually exist."""
|
|
50
|
+
cache_dir = RESEARCH_DIR / endpoint
|
|
51
|
+
if not cache_dir.exists():
|
|
52
|
+
return False, []
|
|
53
|
+
|
|
54
|
+
expected_files = ["sources.json", "interview.json", "schema.json"]
|
|
55
|
+
existing = []
|
|
56
|
+
for f in expected_files:
|
|
57
|
+
if (cache_dir / f).exists():
|
|
58
|
+
existing.append(f)
|
|
59
|
+
|
|
60
|
+
return len(existing) >= 2, existing # At least 2 of 3 files should exist
|
|
23
61
|
|
|
24
62
|
|
|
25
63
|
def main():
|
|
@@ -54,8 +92,13 @@ def main():
|
|
|
54
92
|
print(json.dumps({"permissionDecision": "allow"}))
|
|
55
93
|
sys.exit(0)
|
|
56
94
|
|
|
57
|
-
|
|
58
|
-
|
|
95
|
+
# Get active endpoint (supports both old and new formats)
|
|
96
|
+
endpoint, endpoint_data = get_active_endpoint(state)
|
|
97
|
+
if not endpoint or not endpoint_data:
|
|
98
|
+
print(json.dumps({"permissionDecision": "allow"}))
|
|
99
|
+
sys.exit(0)
|
|
100
|
+
|
|
101
|
+
phases = endpoint_data.get("phases", {})
|
|
59
102
|
tdd_refactor = phases.get("tdd_refactor", {})
|
|
60
103
|
documentation = phases.get("documentation", {})
|
|
61
104
|
|
|
@@ -74,14 +117,19 @@ def main():
|
|
|
74
117
|
user_confirmed = documentation.get("user_confirmed", False)
|
|
75
118
|
checklist_shown = documentation.get("checklist_shown", False)
|
|
76
119
|
manifest_updated = documentation.get("manifest_updated", False)
|
|
77
|
-
research_cached = documentation.get("research_cached", False)
|
|
78
120
|
openapi_updated = documentation.get("openapi_updated", False)
|
|
79
121
|
|
|
122
|
+
# v3.6.7: Check actual file existence for research cache
|
|
123
|
+
# (cache-research.py PostToolUse hook creates these files automatically)
|
|
124
|
+
cache_exists, cache_files = check_research_cache_exists(endpoint)
|
|
125
|
+
research_cached = cache_exists or documentation.get("research_cached", False)
|
|
126
|
+
|
|
80
127
|
missing = []
|
|
81
128
|
if not manifest_updated:
|
|
82
129
|
missing.append("api-tests-manifest.json not updated")
|
|
83
130
|
if not research_cached:
|
|
84
|
-
|
|
131
|
+
# Don't block - cache-research.py will create files when docs are written
|
|
132
|
+
missing.append(f"Research cache pending (will be created automatically)")
|
|
85
133
|
if not checklist_shown:
|
|
86
134
|
missing.append("Documentation checklist not shown to user")
|
|
87
135
|
if not user_question_asked:
|
|
@@ -93,7 +141,7 @@ def main():
|
|
|
93
141
|
|
|
94
142
|
print(json.dumps({
|
|
95
143
|
"permissionDecision": "deny",
|
|
96
|
-
"reason": f"""❌ BLOCKED: Documentation (Phase
|
|
144
|
+
"reason": f"""❌ BLOCKED: Documentation (Phase 12) not complete.
|
|
97
145
|
|
|
98
146
|
Status: {status}
|
|
99
147
|
Manifest updated: {manifest_updated}
|
|
@@ -176,12 +224,16 @@ WHY: Documentation ensures next developer (or future Claude) has context."""
|
|
|
176
224
|
}))
|
|
177
225
|
sys.exit(0)
|
|
178
226
|
|
|
179
|
-
# Documentation complete
|
|
227
|
+
# Documentation complete - check actual file existence for status
|
|
228
|
+
cache_exists, cache_files = check_research_cache_exists(endpoint)
|
|
229
|
+
manifest_updated = documentation.get("manifest_updated", False)
|
|
230
|
+
openapi_updated = documentation.get("openapi_updated", False)
|
|
231
|
+
|
|
180
232
|
print(json.dumps({
|
|
181
233
|
"permissionDecision": "allow",
|
|
182
234
|
"message": f"""✅ Documentation complete for {endpoint}.
|
|
183
235
|
Manifest updated: {manifest_updated}
|
|
184
|
-
Research cached: {
|
|
236
|
+
Research cached: {cache_exists} ({', '.join(cache_files) if cache_files else 'no files'})
|
|
185
237
|
OpenAPI updated: {openapi_updated}
|
|
186
238
|
User confirmed documentation is complete."""
|
|
187
239
|
}))
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hook: PreToolUse (Write|Edit)
|
|
4
|
+
Purpose: Enforce research freshness for the active endpoint
|
|
5
|
+
|
|
6
|
+
This hook blocks Write/Edit operations if:
|
|
7
|
+
1. There is an active endpoint in api-dev-state.json
|
|
8
|
+
2. Research exists for that endpoint
|
|
9
|
+
3. Research is older than 7 days (configurable)
|
|
10
|
+
|
|
11
|
+
The user can:
|
|
12
|
+
- Run /hustle-api-research to refresh the research
|
|
13
|
+
- Set "enforce_freshness": false in the endpoint config to disable
|
|
14
|
+
- Research is only enforced for the ACTIVE endpoint
|
|
15
|
+
|
|
16
|
+
Exit Codes:
|
|
17
|
+
- 0: Continue (no active endpoint, research is fresh, or enforcement disabled)
|
|
18
|
+
- 2: Block with message (research is stale, requires re-research)
|
|
19
|
+
|
|
20
|
+
Added in v3.7.0:
|
|
21
|
+
- User requested enforcement (not just warning) for stale research
|
|
22
|
+
- Only enforces for the active endpoint being worked on
|
|
23
|
+
"""
|
|
24
|
+
import json
|
|
25
|
+
import sys
|
|
26
|
+
import os
|
|
27
|
+
from datetime import datetime
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
# State file is in .claude/ directory (sibling to hooks/)
|
|
31
|
+
STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
|
|
32
|
+
RESEARCH_INDEX = Path(__file__).parent.parent / "research" / "index.json"
|
|
33
|
+
|
|
34
|
+
# Default freshness threshold (days)
|
|
35
|
+
FRESHNESS_THRESHOLD_DAYS = 7
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_active_endpoint(state):
|
|
39
|
+
"""Get active endpoint - supports both old and new state formats."""
|
|
40
|
+
if "endpoints" in state and "active_endpoint" in state:
|
|
41
|
+
active = state.get("active_endpoint")
|
|
42
|
+
if active and active in state["endpoints"]:
|
|
43
|
+
return active, state["endpoints"][active]
|
|
44
|
+
return None, None
|
|
45
|
+
|
|
46
|
+
# Old format
|
|
47
|
+
endpoint = state.get("endpoint")
|
|
48
|
+
if endpoint:
|
|
49
|
+
return endpoint, state
|
|
50
|
+
|
|
51
|
+
return None, None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_research_index():
|
|
55
|
+
"""Load research index from .claude/research/index.json file."""
|
|
56
|
+
if not RESEARCH_INDEX.exists():
|
|
57
|
+
return {}
|
|
58
|
+
try:
|
|
59
|
+
index = json.loads(RESEARCH_INDEX.read_text())
|
|
60
|
+
return index.get("apis", {})
|
|
61
|
+
except (json.JSONDecodeError, IOError):
|
|
62
|
+
return {}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def calculate_days_old(timestamp_str):
|
|
66
|
+
"""Calculate how many days old a timestamp is."""
|
|
67
|
+
if not timestamp_str:
|
|
68
|
+
return 0
|
|
69
|
+
try:
|
|
70
|
+
last_updated = datetime.fromisoformat(timestamp_str.replace('Z', '+00:00'))
|
|
71
|
+
now = datetime.now(last_updated.tzinfo) if last_updated.tzinfo else datetime.now()
|
|
72
|
+
return (now - last_updated).days
|
|
73
|
+
except (ValueError, TypeError):
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def is_api_related_file(file_path):
|
|
78
|
+
"""Check if the file being written is API-related."""
|
|
79
|
+
if not file_path:
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
file_path = file_path.lower()
|
|
83
|
+
|
|
84
|
+
# Files that indicate API development
|
|
85
|
+
api_indicators = [
|
|
86
|
+
'/api/',
|
|
87
|
+
'/route.ts',
|
|
88
|
+
'/route.js',
|
|
89
|
+
'.api.test.',
|
|
90
|
+
'/schemas/',
|
|
91
|
+
'api-tests-manifest',
|
|
92
|
+
'/v2/'
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
return any(indicator in file_path for indicator in api_indicators)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def main():
|
|
99
|
+
# Read hook input from stdin
|
|
100
|
+
try:
|
|
101
|
+
input_data = json.load(sys.stdin)
|
|
102
|
+
except json.JSONDecodeError:
|
|
103
|
+
input_data = {}
|
|
104
|
+
|
|
105
|
+
# Get the file being written (if applicable)
|
|
106
|
+
tool_input = input_data.get("toolInput", {})
|
|
107
|
+
file_path = tool_input.get("file_path", "")
|
|
108
|
+
|
|
109
|
+
# Only enforce for API-related files
|
|
110
|
+
if not is_api_related_file(file_path):
|
|
111
|
+
print(json.dumps({"continue": True}))
|
|
112
|
+
sys.exit(0)
|
|
113
|
+
|
|
114
|
+
# Check if state file exists
|
|
115
|
+
if not STATE_FILE.exists():
|
|
116
|
+
print(json.dumps({"continue": True}))
|
|
117
|
+
sys.exit(0)
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
state = json.loads(STATE_FILE.read_text())
|
|
121
|
+
except json.JSONDecodeError:
|
|
122
|
+
print(json.dumps({"continue": True}))
|
|
123
|
+
sys.exit(0)
|
|
124
|
+
|
|
125
|
+
# Get active endpoint
|
|
126
|
+
endpoint, endpoint_data = get_active_endpoint(state)
|
|
127
|
+
if not endpoint or not endpoint_data:
|
|
128
|
+
# No active endpoint - allow
|
|
129
|
+
print(json.dumps({"continue": True}))
|
|
130
|
+
sys.exit(0)
|
|
131
|
+
|
|
132
|
+
# Check if freshness enforcement is disabled for this endpoint
|
|
133
|
+
if endpoint_data.get("enforce_freshness") is False:
|
|
134
|
+
print(json.dumps({"continue": True}))
|
|
135
|
+
sys.exit(0)
|
|
136
|
+
|
|
137
|
+
# Check research freshness
|
|
138
|
+
research_index = load_research_index()
|
|
139
|
+
|
|
140
|
+
if endpoint not in research_index:
|
|
141
|
+
# No research indexed yet - allow but note this is caught by enforce-research.py
|
|
142
|
+
print(json.dumps({"continue": True}))
|
|
143
|
+
sys.exit(0)
|
|
144
|
+
|
|
145
|
+
entry = research_index[endpoint]
|
|
146
|
+
last_updated = entry.get("last_updated", "")
|
|
147
|
+
days_old = calculate_days_old(last_updated)
|
|
148
|
+
|
|
149
|
+
# Get custom threshold if set
|
|
150
|
+
threshold = endpoint_data.get("freshness_threshold_days", FRESHNESS_THRESHOLD_DAYS)
|
|
151
|
+
|
|
152
|
+
if days_old > threshold:
|
|
153
|
+
# Research is stale - block and require re-research
|
|
154
|
+
output = {
|
|
155
|
+
"decision": "block",
|
|
156
|
+
"reason": f"""🔄 STALE RESEARCH DETECTED
|
|
157
|
+
|
|
158
|
+
Research for '{endpoint}' is {days_old} days old (threshold: {threshold} days).
|
|
159
|
+
|
|
160
|
+
**Action Required:**
|
|
161
|
+
Run `/hustle-api-research {endpoint}` to refresh the research before continuing.
|
|
162
|
+
|
|
163
|
+
**Why This Matters:**
|
|
164
|
+
- API documentation may have changed
|
|
165
|
+
- New parameters or features may be available
|
|
166
|
+
- Breaking changes may have been introduced
|
|
167
|
+
- Your implementation may not match current docs
|
|
168
|
+
|
|
169
|
+
**To Skip (Not Recommended):**
|
|
170
|
+
Set `"enforce_freshness": false` in api-dev-state.json for this endpoint.
|
|
171
|
+
|
|
172
|
+
Last researched: {last_updated or 'Unknown'}
|
|
173
|
+
Research location: .claude/research/{endpoint}/CURRENT.md"""
|
|
174
|
+
}
|
|
175
|
+
print(json.dumps(output))
|
|
176
|
+
sys.exit(2) # Exit code 2 = block with message
|
|
177
|
+
|
|
178
|
+
# Research is fresh - continue
|
|
179
|
+
print(json.dumps({"continue": True}))
|
|
180
|
+
sys.exit(0)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
main()
|
|
@@ -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()
|