@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
|
@@ -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.
|
|
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"
|