@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,352 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: PostToolUse for Write/Edit
4
+ Purpose: Update .claude/registry.json when workflow completes Phase 13
5
+
6
+ This hook runs AFTER Claude writes/edits files. When it detects that
7
+ the completion phase status was just set to "complete" in api-dev-state.json,
8
+ it automatically updates the registry.json with the new entry.
9
+
10
+ Supports:
11
+ - API workflows (api-create) -> registry.apis
12
+ - Component workflows (ui-create-component) -> registry.components
13
+ - Page workflows (ui-create-page) -> registry.pages
14
+ - Combined workflows (combine-api, combine-ui) -> registry.combined
15
+
16
+ Version: 3.9.0
17
+
18
+ Returns:
19
+ - {"continue": true} - Always continues (logging only, no blocking)
20
+ - For UI workflows, includes notify message with UI Showcase link
21
+ """
22
+ import json
23
+ import sys
24
+ from datetime import datetime
25
+ from pathlib import Path
26
+
27
+ # State file is in .claude/ directory (sibling to hooks/)
28
+ STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
29
+ REGISTRY_FILE = Path(__file__).parent.parent / "registry.json"
30
+
31
+
32
+ def get_active_endpoint(state):
33
+ """Get active endpoint - supports both old and new state formats."""
34
+ # New format (v3.6.7+): endpoints object with active_endpoint pointer
35
+ if "endpoints" in state and "active_endpoint" in state:
36
+ active = state.get("active_endpoint")
37
+ if active and active in state["endpoints"]:
38
+ return active, state["endpoints"][active]
39
+ return None, None
40
+
41
+ # Old format: single endpoint field
42
+ endpoint = state.get("endpoint")
43
+ if endpoint:
44
+ return endpoint, state
45
+
46
+ return None, None
47
+
48
+
49
+ def load_registry():
50
+ """Load existing registry or create default."""
51
+ if REGISTRY_FILE.exists():
52
+ try:
53
+ return json.loads(REGISTRY_FILE.read_text())
54
+ except json.JSONDecodeError:
55
+ pass
56
+
57
+ return {
58
+ "version": "1.0.0",
59
+ "updated_at": "",
60
+ "description": "Central registry tracking all APIs, components, and pages created through Hustle Dev Tools",
61
+ "apis": {},
62
+ "components": {},
63
+ "pages": {},
64
+ "combined": {}
65
+ }
66
+
67
+
68
+ def save_registry(registry):
69
+ """Save registry to file."""
70
+ registry["updated_at"] = datetime.now().isoformat()
71
+ REGISTRY_FILE.write_text(json.dumps(registry, indent=2))
72
+
73
+
74
+ def extract_api_entry(endpoint_name, endpoint_state, state):
75
+ """Extract registry entry from state for a standard API."""
76
+ phases = endpoint_state.get("phases", state.get("phases", {}))
77
+ interview = phases.get("interview", {})
78
+ decisions = interview.get("decisions", {})
79
+
80
+ # Get purpose from scope or interview
81
+ scope = phases.get("scope", {})
82
+ purpose = scope.get("purpose", decisions.get("purpose", {}).get("response", ""))
83
+
84
+ # Get schema file path
85
+ schema = phases.get("schema_creation", {})
86
+ schema_file = schema.get("schema_file", f"src/app/api/v2/{endpoint_name}/schemas.ts")
87
+
88
+ # Get test file path
89
+ tdd_red = phases.get("tdd_red", {})
90
+ test_file = tdd_red.get("test_file", f"src/app/api/v2/{endpoint_name}/__tests__/{endpoint_name}.api.test.ts")
91
+
92
+ # Get implementation file path
93
+ tdd_green = phases.get("tdd_green", {})
94
+ impl_file = tdd_green.get("implementation_file", f"src/app/api/v2/{endpoint_name}/route.ts")
95
+
96
+ # Determine methods from interview decisions or default
97
+ methods = ["POST"]
98
+ if decisions.get("methods"):
99
+ methods = decisions.get("methods", {}).get("value", ["POST"])
100
+
101
+ return {
102
+ "name": endpoint_name.replace("-", " ").title(),
103
+ "description": purpose[:200] if purpose else f"API endpoint for {endpoint_name}",
104
+ "route": impl_file,
105
+ "schemas": schema_file,
106
+ "tests": test_file,
107
+ "methods": methods if isinstance(methods, list) else [methods],
108
+ "created_at": datetime.now().strftime("%Y-%m-%d"),
109
+ "status": "complete"
110
+ }
111
+
112
+
113
+ def extract_combined_entry(endpoint_name, endpoint_state, state):
114
+ """Extract registry entry for a combined API."""
115
+ combine_config = state.get("combine_config", endpoint_state.get("combine_config", {}))
116
+ phases = endpoint_state.get("phases", state.get("phases", {}))
117
+ interview = phases.get("interview", {})
118
+ decisions = interview.get("decisions", {})
119
+
120
+ # Get source APIs
121
+ source_elements = combine_config.get("source_elements", [])
122
+ combines = [elem.get("name") for elem in source_elements if elem.get("type") == "api"]
123
+
124
+ # Get purpose from scope
125
+ scope = phases.get("scope", {})
126
+ purpose = scope.get("purpose", "")
127
+
128
+ # Get flow type from interview
129
+ flow_type = decisions.get("execution_order", decisions.get("flow_type", "sequential"))
130
+ if isinstance(flow_type, dict):
131
+ flow_type = flow_type.get("value", "sequential")
132
+
133
+ return {
134
+ "name": endpoint_name.replace("-", " ").title(),
135
+ "type": "api",
136
+ "description": purpose[:200] if purpose else f"Combined API: {', '.join(combines)}",
137
+ "combines": combines,
138
+ "route": f"src/app/api/v2/{endpoint_name}/route.ts",
139
+ "schemas": f"src/app/api/v2/{endpoint_name}/schemas.ts",
140
+ "tests": f"src/app/api/v2/{endpoint_name}/__tests__/",
141
+ "flow_type": flow_type,
142
+ "created_at": datetime.now().strftime("%Y-%m-%d"),
143
+ "status": "complete"
144
+ }
145
+
146
+
147
+ def extract_component_entry(element_name, element_state, state):
148
+ """Extract registry entry for a UI component."""
149
+ phases = element_state.get("phases", state.get("phases", {}))
150
+ ui_config = state.get("ui_config", element_state.get("ui_config", {}))
151
+ interview = phases.get("interview", {})
152
+ decisions = interview.get("decisions", {})
153
+
154
+ # Get description from scope
155
+ scope = phases.get("scope", {})
156
+ description = scope.get("component_purpose", scope.get("purpose", ""))
157
+
158
+ # Get component type (atom, molecule, organism)
159
+ component_type = ui_config.get("component_type", decisions.get("component_type", {}).get("value", "atom"))
160
+
161
+ # Get variants from interview decisions
162
+ variants = ui_config.get("variants", [])
163
+ if not variants and decisions.get("variants"):
164
+ variants = decisions.get("variants", {}).get("value", [])
165
+
166
+ # Get accessibility level
167
+ accessibility = ui_config.get("accessibility_level", "wcag2aa")
168
+
169
+ # File paths (PascalCase for component name)
170
+ pascal_name = "".join(word.capitalize() for word in element_name.replace("-", " ").split())
171
+ base_path = f"src/components/{pascal_name}"
172
+
173
+ return {
174
+ "name": pascal_name,
175
+ "description": description[:200] if description else f"UI component: {pascal_name}",
176
+ "type": component_type,
177
+ "file": f"{base_path}/{pascal_name}.tsx",
178
+ "story": f"{base_path}/{pascal_name}.stories.tsx",
179
+ "tests": f"{base_path}/{pascal_name}.test.tsx",
180
+ "props_interface": f"{pascal_name}Props",
181
+ "variants": variants if isinstance(variants, list) else [],
182
+ "accessibility": accessibility,
183
+ "responsive": True,
184
+ "status": "complete",
185
+ "created_at": datetime.now().strftime("%Y-%m-%d")
186
+ }
187
+
188
+
189
+ def extract_page_entry(element_name, element_state, state):
190
+ """Extract registry entry for a page."""
191
+ phases = element_state.get("phases", state.get("phases", {}))
192
+ ui_config = state.get("ui_config", element_state.get("ui_config", {}))
193
+ interview = phases.get("interview", {})
194
+ decisions = interview.get("decisions", {})
195
+
196
+ # Get description from scope
197
+ scope = phases.get("scope", {})
198
+ description = scope.get("page_purpose", scope.get("purpose", ""))
199
+
200
+ # Get page type (landing, dashboard, form, list)
201
+ page_type = ui_config.get("page_type", decisions.get("page_type", {}).get("value", "landing"))
202
+
203
+ # Get components used (from component analysis phase)
204
+ component_analysis = phases.get("component_analysis", {})
205
+ uses_components = component_analysis.get("selected_components", [])
206
+ if not uses_components:
207
+ uses_components = ui_config.get("uses_components", [])
208
+
209
+ # Get data fetching type from interview
210
+ data_fetching = decisions.get("data_fetching", {}).get("value", "server")
211
+ if isinstance(data_fetching, dict):
212
+ data_fetching = data_fetching.get("value", "server")
213
+
214
+ # Check auth requirement
215
+ auth_required = decisions.get("auth_required", {}).get("value", False)
216
+ if isinstance(auth_required, dict):
217
+ auth_required = auth_required.get("value", False)
218
+
219
+ # Route path (kebab-case)
220
+ route_path = element_name.lower().replace(" ", "-").replace("_", "-")
221
+
222
+ return {
223
+ "name": element_name.replace("-", " ").title(),
224
+ "description": description[:200] if description else f"Page: {element_name}",
225
+ "type": page_type,
226
+ "file": f"src/app/{route_path}/page.tsx",
227
+ "route": f"/{route_path}",
228
+ "tests": f"tests/e2e/{route_path}.spec.ts",
229
+ "uses_components": uses_components if isinstance(uses_components, list) else [],
230
+ "data_fetching": data_fetching,
231
+ "auth_required": auth_required,
232
+ "status": "complete",
233
+ "created_at": datetime.now().strftime("%Y-%m-%d")
234
+ }
235
+
236
+
237
+ def get_active_element(state):
238
+ """Get active element - supports both API and UI workflows."""
239
+ # UI workflow format: elements object with active_element pointer
240
+ if "elements" in state and "active_element" in state:
241
+ active = state.get("active_element")
242
+ if active and active in state["elements"]:
243
+ return active, state["elements"][active]
244
+
245
+ # Fall back to API endpoint format
246
+ return get_active_endpoint(state)
247
+
248
+
249
+ def main():
250
+ # Read hook input from stdin
251
+ try:
252
+ input_data = json.load(sys.stdin)
253
+ except json.JSONDecodeError:
254
+ print(json.dumps({"continue": True}))
255
+ sys.exit(0)
256
+
257
+ tool_name = input_data.get("tool_name", "")
258
+
259
+ # Only process Write/Edit operations
260
+ if tool_name not in ["Write", "Edit"]:
261
+ print(json.dumps({"continue": True}))
262
+ sys.exit(0)
263
+
264
+ # Check if state file exists
265
+ if not STATE_FILE.exists():
266
+ print(json.dumps({"continue": True}))
267
+ sys.exit(0)
268
+
269
+ # Load state
270
+ try:
271
+ state = json.loads(STATE_FILE.read_text())
272
+ except json.JSONDecodeError:
273
+ print(json.dumps({"continue": True}))
274
+ sys.exit(0)
275
+
276
+ # Determine workflow type
277
+ workflow = state.get("workflow", "api-create")
278
+
279
+ # Get active element based on workflow type
280
+ if workflow in ["ui-create-component", "ui-create-page"]:
281
+ element_name, element_state = get_active_element(state)
282
+ else:
283
+ element_name, element_state = get_active_endpoint(state)
284
+
285
+ if not element_name or not element_state:
286
+ print(json.dumps({"continue": True}))
287
+ sys.exit(0)
288
+
289
+ # Check if completion phase just became "complete"
290
+ phases = element_state.get("phases", state.get("phases", {}))
291
+ completion = phases.get("completion", {})
292
+
293
+ if completion.get("status") != "complete":
294
+ print(json.dumps({"continue": True}))
295
+ sys.exit(0)
296
+
297
+ # Check if already in registry (avoid duplicates)
298
+ registry = load_registry()
299
+
300
+ # Result object - may include notify for UI workflows
301
+ result = {"continue": True}
302
+
303
+ # Route to appropriate handler based on workflow
304
+ if workflow == "ui-create-component":
305
+ # Component workflow
306
+ if element_name in registry.get("components", {}):
307
+ print(json.dumps(result))
308
+ sys.exit(0)
309
+
310
+ entry = extract_component_entry(element_name, element_state, state)
311
+ registry.setdefault("components", {})[element_name] = entry
312
+ result["notify"] = f"🎨 View in UI Showcase: http://localhost:3000/ui-showcase"
313
+
314
+ elif workflow == "ui-create-page":
315
+ # Page workflow
316
+ if element_name in registry.get("pages", {}):
317
+ print(json.dumps(result))
318
+ sys.exit(0)
319
+
320
+ entry = extract_page_entry(element_name, element_state, state)
321
+ registry.setdefault("pages", {})[element_name] = entry
322
+ result["notify"] = f"🎨 View in UI Showcase: http://localhost:3000/ui-showcase"
323
+
324
+ elif workflow in ["combine-api", "combine-ui"]:
325
+ # Combined workflow
326
+ if element_name in registry.get("combined", {}):
327
+ print(json.dumps(result))
328
+ sys.exit(0)
329
+
330
+ entry = extract_combined_entry(element_name, element_state, state)
331
+ registry.setdefault("combined", {})[element_name] = entry
332
+
333
+ else:
334
+ # Default: API workflow
335
+ if element_name in registry.get("apis", {}):
336
+ print(json.dumps(result))
337
+ sys.exit(0)
338
+
339
+ entry = extract_api_entry(element_name, element_state, state)
340
+ registry.setdefault("apis", {})[element_name] = entry
341
+ result["notify"] = f"🔌 View in API Showcase: http://localhost:3000/api-showcase"
342
+
343
+ # Save registry
344
+ save_registry(registry)
345
+
346
+ # Return success (with optional notify for UI workflows)
347
+ print(json.dumps(result))
348
+ sys.exit(0)
349
+
350
+
351
+ if __name__ == "__main__":
352
+ main()
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: PostToolUse for Write/Edit
4
+ Purpose: Auto-create UI Showcase page when first component/page is created
5
+ and auto-populate showcase data from registry.
6
+
7
+ This hook monitors for new component or page registrations. When the first
8
+ UI element is added to registry.json, it creates the UI Showcase page
9
+ at src/app/ui-showcase/ if it doesn't exist.
10
+
11
+ Also generates src/app/ui-showcase/data.json from registry for auto-population.
12
+
13
+ Version: 3.10.0
14
+
15
+ Returns:
16
+ - {"continue": true} - Always continues
17
+ - May include "notify" about showcase creation
18
+ """
19
+ import json
20
+ import sys
21
+ from pathlib import Path
22
+ import shutil
23
+ from datetime import datetime
24
+
25
+ # State and registry files in .claude/ directory
26
+ STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
27
+ REGISTRY_FILE = Path(__file__).parent.parent / "registry.json"
28
+
29
+
30
+ def generate_showcase_data(registry, cwd):
31
+ """Generate showcase data file from registry for auto-population.
32
+
33
+ Creates src/app/ui-showcase/data.json with component/page listings.
34
+ """
35
+ components = registry.get("components", {})
36
+ pages = registry.get("pages", {})
37
+
38
+ showcase_data = {
39
+ "version": "3.10.0",
40
+ "generated_at": datetime.now().isoformat(),
41
+ "components": [],
42
+ "pages": []
43
+ }
44
+
45
+ # Process components
46
+ for name, comp in components.items():
47
+ showcase_data["components"].append({
48
+ "id": name,
49
+ "name": comp.get("name", name),
50
+ "description": comp.get("description", ""),
51
+ "type": comp.get("type", "atom"),
52
+ "path": comp.get("path", f"src/components/{name}/{name}.tsx"),
53
+ "storybook_url": comp.get("storybook_url", f"/?path=/story/{name.lower()}--default"),
54
+ "variants": comp.get("variants", []),
55
+ "props": list(comp.get("props", {}).keys()) if isinstance(comp.get("props"), dict) else [],
56
+ "created_at": comp.get("created_at", ""),
57
+ "status": comp.get("status", "ready")
58
+ })
59
+
60
+ # Process pages
61
+ for name, page in pages.items():
62
+ showcase_data["pages"].append({
63
+ "id": name,
64
+ "name": page.get("name", name),
65
+ "description": page.get("description", ""),
66
+ "route": page.get("route", f"/{name}"),
67
+ "page_type": page.get("page_type", "landing"),
68
+ "path": page.get("path", f"src/app/{name}/page.tsx"),
69
+ "requires_auth": page.get("requires_auth", False),
70
+ "data_sources": page.get("data_sources", []),
71
+ "created_at": page.get("created_at", ""),
72
+ "status": page.get("status", "ready")
73
+ })
74
+
75
+ # Write data file
76
+ data_file = cwd / "src" / "app" / "ui-showcase" / "data.json"
77
+ data_file.parent.mkdir(parents=True, exist_ok=True)
78
+ data_file.write_text(json.dumps(showcase_data, indent=2))
79
+
80
+ return str(data_file.relative_to(cwd))
81
+
82
+
83
+ def copy_showcase_templates(cwd):
84
+ """Copy UI showcase templates to src/app/ui-showcase/."""
85
+ # Source templates (installed by CLI)
86
+ templates_dir = Path(__file__).parent.parent / "templates" / "ui-showcase"
87
+
88
+ # Destination
89
+ showcase_dir = cwd / "src" / "app" / "ui-showcase"
90
+
91
+ # Create directory if needed
92
+ showcase_dir.mkdir(parents=True, exist_ok=True)
93
+
94
+ # Copy template files
95
+ templates_to_copy = [
96
+ ("page.tsx", "page.tsx"),
97
+ ("UIShowcase.tsx", "_components/UIShowcase.tsx"),
98
+ ("PreviewCard.tsx", "_components/PreviewCard.tsx"),
99
+ ("PreviewModal.tsx", "_components/PreviewModal.tsx"),
100
+ ]
101
+
102
+ created_files = []
103
+ for src_name, dest_name in templates_to_copy:
104
+ src_path = templates_dir / src_name
105
+ dest_path = showcase_dir / dest_name
106
+
107
+ # Create subdirectories if needed
108
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
109
+
110
+ if src_path.exists() and not dest_path.exists():
111
+ shutil.copy2(src_path, dest_path)
112
+ created_files.append(str(dest_path.relative_to(cwd)))
113
+
114
+ return created_files
115
+
116
+
117
+ def main():
118
+ # Read hook input from stdin
119
+ try:
120
+ input_data = json.load(sys.stdin)
121
+ except json.JSONDecodeError:
122
+ print(json.dumps({"continue": True}))
123
+ sys.exit(0)
124
+
125
+ tool_name = input_data.get("tool_name", "")
126
+
127
+ # Only process Write/Edit operations
128
+ if tool_name not in ["Write", "Edit"]:
129
+ print(json.dumps({"continue": True}))
130
+ sys.exit(0)
131
+
132
+ # Check if state file exists
133
+ if not STATE_FILE.exists():
134
+ print(json.dumps({"continue": True}))
135
+ sys.exit(0)
136
+
137
+ # Load state
138
+ try:
139
+ state = json.loads(STATE_FILE.read_text())
140
+ except json.JSONDecodeError:
141
+ print(json.dumps({"continue": True}))
142
+ sys.exit(0)
143
+
144
+ workflow = state.get("workflow", "")
145
+
146
+ # Only apply for UI workflows
147
+ if workflow not in ["ui-create-component", "ui-create-page"]:
148
+ print(json.dumps({"continue": True}))
149
+ sys.exit(0)
150
+
151
+ # Check if completion phase is complete
152
+ active_element = state.get("active_element", "")
153
+ elements = state.get("elements", {})
154
+
155
+ if active_element and active_element in elements:
156
+ phases = elements[active_element].get("phases", {})
157
+ else:
158
+ phases = state.get("phases", {})
159
+
160
+ completion = phases.get("completion", {})
161
+ if completion.get("status") != "complete":
162
+ print(json.dumps({"continue": True}))
163
+ sys.exit(0)
164
+
165
+ # Check if showcase already exists
166
+ cwd = Path.cwd()
167
+ showcase_page = cwd / "src" / "app" / "ui-showcase" / "page.tsx"
168
+
169
+ if showcase_page.exists():
170
+ print(json.dumps({"continue": True}))
171
+ sys.exit(0)
172
+
173
+ # Check if we have components or pages in registry
174
+ if not REGISTRY_FILE.exists():
175
+ print(json.dumps({"continue": True}))
176
+ sys.exit(0)
177
+
178
+ try:
179
+ registry = json.loads(REGISTRY_FILE.read_text())
180
+ except json.JSONDecodeError:
181
+ print(json.dumps({"continue": True}))
182
+ sys.exit(0)
183
+
184
+ components = registry.get("components", {})
185
+ pages = registry.get("pages", {})
186
+
187
+ # Create showcase if we have at least one component or page
188
+ if components or pages:
189
+ created_files = copy_showcase_templates(cwd)
190
+
191
+ # Always update data.json from registry
192
+ data_file = generate_showcase_data(registry, cwd)
193
+
194
+ if created_files:
195
+ print(json.dumps({
196
+ "continue": True,
197
+ "notify": f"Created UI Showcase at /ui-showcase ({len(created_files)} files) + data.json"
198
+ }))
199
+ else:
200
+ # Just updated data.json
201
+ print(json.dumps({
202
+ "continue": True,
203
+ "notify": f"Updated UI Showcase data ({len(components)} components, {len(pages)} pages)"
204
+ }))
205
+ else:
206
+ print(json.dumps({"continue": True}))
207
+
208
+ sys.exit(0)
209
+
210
+
211
+ if __name__ == "__main__":
212
+ main()
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hustle-together/api-dev-tools",
3
- "version": "3.6.5",
4
- "description": "Interview-driven, research-first API development workflow with continuous verification loops for Claude Code",
3
+ "version": "3.10.0",
4
+ "description": "Interview-driven, research-first API and UI development workflow with continuous verification loops for Claude Code",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {
7
7
  "api-dev-tools": "./bin/cli.js"
@@ -12,7 +12,6 @@
12
12
  "hooks/",
13
13
  "scripts/",
14
14
  "templates/",
15
- "demo/",
16
15
  "README.md",
17
16
  "LICENSE"
18
17
  ],
@@ -23,10 +22,16 @@
23
22
  "claude",
24
23
  "claude-code",
25
24
  "api-development",
25
+ "ui-development",
26
+ "components",
27
+ "storybook",
28
+ "playwright",
26
29
  "tdd",
27
30
  "test-driven-development",
28
31
  "interview-driven",
29
32
  "api-testing",
33
+ "api-orchestration",
34
+ "combine",
30
35
  "documentation",
31
36
  "workflow",
32
37
  "automation"