@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.
Files changed (30) hide show
  1. package/README.md +319 -22
  2. package/bin/cli.js +61 -13
  3. package/commands/hustle-api-create.md +13 -13
  4. package/commands/hustle-ui-create-page.md +933 -0
  5. package/hooks/api-workflow-check.py +160 -2
  6. package/hooks/check-api-routes.py +168 -0
  7. package/hooks/enforce-a11y-audit.py +202 -0
  8. package/hooks/enforce-brand-guide.py +115 -5
  9. package/hooks/enforce-page-components.py +186 -0
  10. package/hooks/enforce-page-data-schema.py +155 -0
  11. package/hooks/generate-manifest-entry.py +181 -1
  12. package/hooks/session-startup.py +95 -5
  13. package/hooks/update-ui-showcase.py +67 -3
  14. package/package.json +1 -2
  15. package/templates/api-dev-state.json +39 -1
  16. package/templates/settings.json +16 -0
  17. package/templates/shared/HeroHeader.tsx +1 -1
  18. package/demo/hustle-together/blog/gemini-vs-claude-widgets.html +0 -959
  19. package/demo/hustle-together/blog/interview-driven-api-development.html +0 -1146
  20. package/demo/hustle-together/blog/tdd-for-ai.html +0 -982
  21. package/demo/hustle-together/index.html +0 -1312
  22. package/demo/workflow-demo-v3.5-backup.html +0 -5008
  23. package/demo/workflow-demo.html +0 -6202
  24. /package/templates/api-showcase/{APICard.tsx → _components/APICard.tsx} +0 -0
  25. /package/templates/api-showcase/{APIModal.tsx → _components/APIModal.tsx} +0 -0
  26. /package/templates/api-showcase/{APIShowcase.tsx → _components/APIShowcase.tsx} +0 -0
  27. /package/templates/api-showcase/{APITester.tsx → _components/APITester.tsx} +0 -0
  28. /package/templates/ui-showcase/{PreviewCard.tsx → _components/PreviewCard.tsx} +0 -0
  29. /package/templates/ui-showcase/{PreviewModal.tsx → _components/PreviewModal.tsx} +0 -0
  30. /package/templates/ui-showcase/{UIShowcase.tsx → _components/UIShowcase.tsx} +0 -0
@@ -49,6 +49,127 @@ RECOMMENDED_PHASES = [
49
49
  ("documentation", "Documentation updates"),
50
50
  ]
51
51
 
52
+ # Combine workflow specific phases
53
+ COMBINE_REQUIRED_PHASES = [
54
+ ("selection", "API selection (2+ APIs required)"),
55
+ ("scope", "Scope confirmation"),
56
+ ("research_initial", "Initial research"),
57
+ ("interview", "User interview"),
58
+ ("research_deep", "Deep research"),
59
+ ("schema_creation", "Combined schema creation"),
60
+ ("environment_check", "Environment check"),
61
+ ("tdd_red", "TDD Red phase"),
62
+ ("tdd_green", "TDD Green phase"),
63
+ ("verify", "Verification phase"),
64
+ ("documentation", "Documentation updates"),
65
+ ]
66
+
67
+ # UI workflow specific phases
68
+ UI_REQUIRED_PHASES = [
69
+ ("disambiguation", "Component/Page type disambiguation"),
70
+ ("scope", "Scope confirmation"),
71
+ ("design_research", "Design research"),
72
+ ("interview", "User interview"),
73
+ ("tdd_red", "TDD Red phase"),
74
+ ("tdd_green", "TDD Green phase"),
75
+ ("verify", "Verification phase (4-step)"),
76
+ ("documentation", "Documentation updates"),
77
+ ]
78
+
79
+
80
+ def get_workflow_type(state):
81
+ """Detect the workflow type from state."""
82
+ workflow = state.get("workflow", "")
83
+ if workflow:
84
+ return workflow
85
+
86
+ # Infer from state structure
87
+ if state.get("combine_config"):
88
+ return "combine-api"
89
+ if state.get("ui_config"):
90
+ mode = state.get("ui_config", {}).get("mode", "")
91
+ return f"ui-create-{mode}" if mode else "ui-create-component"
92
+
93
+ return "api-create"
94
+
95
+
96
+ def get_required_phases_for_workflow(workflow_type):
97
+ """Get the required phases list for a given workflow type."""
98
+ if workflow_type == "combine-api":
99
+ return COMBINE_REQUIRED_PHASES
100
+ elif workflow_type.startswith("ui-create"):
101
+ return UI_REQUIRED_PHASES
102
+ else:
103
+ return REQUIRED_PHASES
104
+
105
+
106
+ def validate_combine_workflow(state):
107
+ """Validate combine-specific requirements.
108
+
109
+ Returns list of issues if validation fails, empty list if OK.
110
+ """
111
+ issues = []
112
+
113
+ combine_config = state.get("combine_config", {})
114
+ if not combine_config:
115
+ issues.append("❌ Combine config not found in state")
116
+ return issues
117
+
118
+ # Check that at least 2 APIs are selected
119
+ source_elements = combine_config.get("source_elements", [])
120
+ if len(source_elements) < 2:
121
+ issues.append(f"❌ Combine requires 2+ APIs, found {len(source_elements)}")
122
+ issues.append(" Select more APIs in Phase 1 (SELECTION)")
123
+
124
+ # Verify all source APIs exist in registry
125
+ try:
126
+ registry_path = STATE_FILE.parent / "registry.json"
127
+ if registry_path.exists():
128
+ registry = json.loads(registry_path.read_text())
129
+ apis = registry.get("apis", {})
130
+
131
+ for elem in source_elements:
132
+ elem_name = elem.get("name", "") if isinstance(elem, dict) else str(elem)
133
+ if elem_name and elem_name not in apis:
134
+ issues.append(f"⚠️ Source API '{elem_name}' not found in registry")
135
+ issues.append(f" Run /api-create {elem_name} first")
136
+ except Exception:
137
+ pass
138
+
139
+ # Check flow type is defined
140
+ flow_type = combine_config.get("flow_type", "")
141
+ if not flow_type:
142
+ issues.append("⚠️ Flow type not defined (sequential/parallel/conditional)")
143
+
144
+ return issues
145
+
146
+
147
+ def validate_ui_workflow(state):
148
+ """Validate UI-specific requirements.
149
+
150
+ Returns list of issues if validation fails, empty list if OK.
151
+ """
152
+ issues = []
153
+
154
+ ui_config = state.get("ui_config", {})
155
+ if not ui_config:
156
+ # Try to get from active element
157
+ active = state.get("active_element", "")
158
+ if active:
159
+ elements = state.get("elements", {})
160
+ element = elements.get(active, {})
161
+ ui_config = element.get("ui_config", {})
162
+
163
+ if not ui_config:
164
+ issues.append("⚠️ UI config not found in state")
165
+ return issues
166
+
167
+ # Check brand guide was applied
168
+ if not ui_config.get("use_brand_guide"):
169
+ issues.append("⚠️ Brand guide not applied - design may not match project standards")
170
+
171
+ return issues
172
+
52
173
 
53
174
  def get_active_endpoint(state):
54
175
  """Get active endpoint - supports both old and new state formats."""
@@ -58,11 +179,23 @@ def get_active_endpoint(state):
58
179
  return active, state["endpoints"][active]
59
180
  return None, None
60
181
 
182
+ # Support for elements (UI workflow)
183
+ if "elements" in state and "active_element" in state:
184
+ active = state.get("active_element")
185
+ if active and active in state["elements"]:
186
+ return active, state["elements"][active]
187
+ return None, None
188
+
61
189
  # Old format: single endpoint
62
190
  endpoint = state.get("endpoint")
63
191
  if endpoint:
64
192
  return endpoint, state
65
193
 
194
+ # Try active_element without elements dict
195
+ active = state.get("active_element")
196
+ if active:
197
+ return active, state
198
+
66
199
  return None, None
67
200
 
68
201
 
@@ -465,6 +598,9 @@ def main():
465
598
  print(json.dumps({"decision": "approve"}))
466
599
  sys.exit(0)
467
600
 
601
+ # Detect workflow type
602
+ workflow_type = get_workflow_type(state)
603
+
468
604
  # Get active endpoint (multi-API support)
469
605
  endpoint, endpoint_data = get_active_endpoint(state)
470
606
 
@@ -476,7 +612,12 @@ def main():
476
612
 
477
613
  # Check if workflow was even started
478
614
  research = phases.get("research_initial", {})
479
- if research.get("status") == "not_started":
615
+ design_research = phases.get("design_research", {}) # For UI workflows
616
+ selection = phases.get("selection", {}) # For combine workflows
617
+
618
+ if (research.get("status") == "not_started" and
619
+ design_research.get("status") == "not_started" and
620
+ selection.get("status") == "not_started"):
480
621
  # Workflow not started, allow stop
481
622
  print(json.dumps({"decision": "approve"}))
482
623
  sys.exit(0)
@@ -484,9 +625,26 @@ def main():
484
625
  # Collect all issues
485
626
  all_issues = []
486
627
 
628
+ # Workflow-specific validation
629
+ if workflow_type == "combine-api":
630
+ combine_issues = validate_combine_workflow(state)
631
+ if combine_issues:
632
+ all_issues.append("❌ COMBINE WORKFLOW VALIDATION FAILED:")
633
+ all_issues.extend(combine_issues)
634
+ all_issues.append("")
635
+
636
+ elif workflow_type.startswith("ui-create"):
637
+ ui_issues = validate_ui_workflow(state)
638
+ if ui_issues:
639
+ all_issues.extend(ui_issues)
640
+ all_issues.append("")
641
+
642
+ # Get the correct required phases for this workflow
643
+ required_phases = get_required_phases_for_workflow(workflow_type)
644
+
487
645
  # Check required phases
488
646
  incomplete_required = []
489
- for phase_key, phase_name in REQUIRED_PHASES:
647
+ for phase_key, phase_name in required_phases:
490
648
  phase = phases.get(phase_key, {})
491
649
  status = phase.get("status", "not_started")
492
650
  if status != "complete":
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: check-api-routes.py
4
+ Trigger: PreToolUse (Write|Edit)
5
+ Purpose: Verify required API routes exist before page implementation
6
+
7
+ For ui-create-page workflow, ensures Phase 7 (ENVIRONMENT) has verified
8
+ that required API routes are available.
9
+ """
10
+
11
+ import json
12
+ import sys
13
+ import os
14
+ import glob
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
+ active = state.get("endpoint", "")
39
+ return active
40
+
41
+ def is_page_implementation(file_path, element_name):
42
+ """Check if the file is a page implementation file"""
43
+ if not file_path or not element_name:
44
+ return False
45
+
46
+ patterns = [
47
+ f"src/app/{element_name}/page.tsx",
48
+ f"app/{element_name}/page.tsx",
49
+ ]
50
+
51
+ return any(pattern in file_path for pattern in patterns)
52
+
53
+ def check_environment_phase(state, element_name):
54
+ """Check if environment phase is complete"""
55
+ elements = state.get("elements", {})
56
+ element = elements.get(element_name, {})
57
+ phases = element.get("phases", {})
58
+
59
+ environment = phases.get("environment_check", {})
60
+ return environment.get("status") == "complete"
61
+
62
+ def get_required_api_routes(state, element_name):
63
+ """Get list of required API routes from interview decisions"""
64
+ elements = state.get("elements", {})
65
+ element = elements.get(element_name, {})
66
+ ui_config = element.get("ui_config", {})
67
+
68
+ # Check if data sources were defined
69
+ data_sources = ui_config.get("data_sources", [])
70
+ return data_sources
71
+
72
+ def find_existing_api_routes():
73
+ """Find all existing API routes in the project"""
74
+ routes = []
75
+
76
+ # Check src/app/api paths
77
+ api_patterns = [
78
+ "src/app/api/**/*.ts",
79
+ "src/app/api/**/*.tsx",
80
+ "app/api/**/*.ts",
81
+ "app/api/**/*.tsx",
82
+ ]
83
+
84
+ for pattern in api_patterns:
85
+ for file_path in glob.glob(pattern, recursive=True):
86
+ if "route.ts" in file_path or "route.tsx" in file_path:
87
+ # Extract route name from path
88
+ route = file_path.replace("src/app/api/", "/api/")
89
+ route = route.replace("app/api/", "/api/")
90
+ route = route.replace("/route.ts", "")
91
+ route = route.replace("/route.tsx", "")
92
+ routes.append(route)
93
+
94
+ return routes
95
+
96
+ def main():
97
+ try:
98
+ # Read tool input from stdin
99
+ input_data = json.loads(sys.stdin.read())
100
+ tool_name = input_data.get("tool_name", "")
101
+ tool_input = input_data.get("tool_input", {})
102
+
103
+ # Only check Write tool
104
+ if tool_name != "Write":
105
+ print(json.dumps({"decision": "allow"}))
106
+ return
107
+
108
+ file_path = tool_input.get("file_path", "")
109
+
110
+ # Load state
111
+ state = load_state()
112
+ if not state:
113
+ print(json.dumps({"decision": "allow"}))
114
+ return
115
+
116
+ # Only apply to ui-create-page workflow
117
+ if not is_page_workflow(state):
118
+ print(json.dumps({"decision": "allow"}))
119
+ return
120
+
121
+ element_name = get_active_element(state)
122
+ if not element_name:
123
+ print(json.dumps({"decision": "allow"}))
124
+ return
125
+
126
+ # Check if writing main page file
127
+ if is_page_implementation(file_path, element_name):
128
+ # Verify environment phase is complete
129
+ if not check_environment_phase(state, element_name):
130
+ # Find existing API routes for reference
131
+ existing_routes = find_existing_api_routes()
132
+ routes_list = "\n".join([f" - {r}" for r in existing_routes[:15]])
133
+ if len(existing_routes) > 15:
134
+ routes_list += f"\n ... and {len(existing_routes) - 15} more"
135
+
136
+ print(json.dumps({
137
+ "decision": "block",
138
+ "reason": f"""
139
+ ENVIRONMENT CHECK REQUIRED (Phase 7)
140
+
141
+ You are implementing the main page, but the Environment phase is not complete.
142
+
143
+ Before implementing page.tsx:
144
+ 1. Verify required API routes exist
145
+ 2. Check authentication configuration
146
+ 3. Verify required packages are installed
147
+ 4. Update state: phases.environment_check.status = "complete"
148
+
149
+ Existing API Routes Found:
150
+ {routes_list if existing_routes else " (No API routes found)"}
151
+
152
+ If you need new API routes, use /api-create to create them first.
153
+ """
154
+ }))
155
+ return
156
+
157
+ # Allow everything else
158
+ print(json.dumps({"decision": "allow"}))
159
+
160
+ except Exception as e:
161
+ # On error, allow to avoid blocking workflow
162
+ print(json.dumps({
163
+ "decision": "allow",
164
+ "error": str(e)
165
+ }))
166
+
167
+ if __name__ == "__main__":
168
+ 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()