@hustle-together/api-dev-tools 3.6.4 → 3.9.2

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 (61) hide show
  1. package/README.md +5307 -258
  2. package/bin/cli.js +348 -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} +22 -2
  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.md +825 -0
  14. package/hooks/api-workflow-check.py +385 -19
  15. package/hooks/cache-research.py +337 -0
  16. package/hooks/check-playwright-setup.py +103 -0
  17. package/hooks/check-storybook-setup.py +81 -0
  18. package/hooks/detect-interruption.py +165 -0
  19. package/hooks/enforce-brand-guide.py +131 -0
  20. package/hooks/enforce-documentation.py +60 -8
  21. package/hooks/enforce-freshness.py +184 -0
  22. package/hooks/enforce-questions-sourced.py +146 -0
  23. package/hooks/enforce-schema-from-interview.py +248 -0
  24. package/hooks/enforce-ui-disambiguation.py +108 -0
  25. package/hooks/enforce-ui-interview.py +130 -0
  26. package/hooks/generate-manifest-entry.py +981 -0
  27. package/hooks/session-logger.py +297 -0
  28. package/hooks/session-startup.py +65 -10
  29. package/hooks/track-scope-coverage.py +220 -0
  30. package/hooks/track-tool-use.py +81 -1
  31. package/hooks/update-api-showcase.py +149 -0
  32. package/hooks/update-registry.py +352 -0
  33. package/hooks/update-ui-showcase.py +148 -0
  34. package/package.json +8 -2
  35. package/templates/BRAND_GUIDE.md +299 -0
  36. package/templates/CLAUDE-SECTION.md +56 -24
  37. package/templates/SPEC.json +640 -0
  38. package/templates/api-dev-state.json +179 -161
  39. package/templates/api-showcase/APICard.tsx +153 -0
  40. package/templates/api-showcase/APIModal.tsx +375 -0
  41. package/templates/api-showcase/APIShowcase.tsx +231 -0
  42. package/templates/api-showcase/APITester.tsx +522 -0
  43. package/templates/api-showcase/page.tsx +41 -0
  44. package/templates/component/Component.stories.tsx +172 -0
  45. package/templates/component/Component.test.tsx +237 -0
  46. package/templates/component/Component.tsx +86 -0
  47. package/templates/component/Component.types.ts +55 -0
  48. package/templates/component/index.ts +15 -0
  49. package/templates/dev-tools/_components/DevToolsLanding.tsx +320 -0
  50. package/templates/dev-tools/page.tsx +10 -0
  51. package/templates/page/page.e2e.test.ts +218 -0
  52. package/templates/page/page.tsx +42 -0
  53. package/templates/performance-budgets.json +58 -0
  54. package/templates/registry.json +13 -0
  55. package/templates/settings.json +74 -0
  56. package/templates/shared/HeroHeader.tsx +261 -0
  57. package/templates/shared/index.ts +1 -0
  58. package/templates/ui-showcase/PreviewCard.tsx +315 -0
  59. package/templates/ui-showcase/PreviewModal.tsx +676 -0
  60. package/templates/ui-showcase/UIShowcase.tsx +262 -0
  61. package/templates/ui-showcase/page.tsx +26 -0
@@ -10,7 +10,11 @@ It logs each research action to api-dev-state.json for:
10
10
  - Providing visibility to the user
11
11
  - Tracking turn counts for periodic re-grounding
12
12
 
13
- Version: 3.0.0
13
+ Version: 3.6.7
14
+
15
+ Updated in v3.6.7:
16
+ - Support multi-API state structure
17
+ - Populate .claude/research/index.json for freshness tracking
14
18
 
15
19
  Returns:
16
20
  - {"continue": true} - Always continues (logging only, no blocking)
@@ -22,11 +26,82 @@ from pathlib import Path
22
26
 
23
27
  # State file is in .claude/ directory (sibling to hooks/)
24
28
  STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
29
+ RESEARCH_DIR = Path(__file__).parent.parent / "research"
30
+ RESEARCH_INDEX = RESEARCH_DIR / "index.json"
25
31
 
26
32
  # Re-grounding interval (also used by periodic-reground.py)
27
33
  REGROUND_INTERVAL = 7
28
34
 
29
35
 
36
+ def get_active_endpoint(state):
37
+ """Get active endpoint - supports both old and new state formats."""
38
+ # New format (v3.6.7+): endpoints object with active_endpoint pointer
39
+ if "endpoints" in state and "active_endpoint" in state:
40
+ active = state.get("active_endpoint")
41
+ if active and active in state["endpoints"]:
42
+ return active, state["endpoints"][active]
43
+ return None, None
44
+
45
+ # Old format: single endpoint field
46
+ endpoint = state.get("endpoint")
47
+ if endpoint:
48
+ return endpoint, state
49
+
50
+ return None, None
51
+
52
+
53
+ def update_research_index(endpoint, source_entry):
54
+ """Update the research index.json with new research activity."""
55
+ RESEARCH_DIR.mkdir(parents=True, exist_ok=True)
56
+
57
+ # Load existing index
58
+ if RESEARCH_INDEX.exists():
59
+ try:
60
+ index = json.loads(RESEARCH_INDEX.read_text())
61
+ except json.JSONDecodeError:
62
+ index = {"version": "3.6.7", "apis": {}}
63
+ else:
64
+ index = {"version": "3.6.7", "apis": {}}
65
+
66
+ if "apis" not in index:
67
+ index["apis"] = {}
68
+
69
+ # Update endpoint entry
70
+ now = datetime.now().isoformat()
71
+ if endpoint not in index["apis"]:
72
+ index["apis"][endpoint] = {
73
+ "last_updated": now,
74
+ "freshness_days": 0,
75
+ "source_count": 0,
76
+ "sources": []
77
+ }
78
+
79
+ entry = index["apis"][endpoint]
80
+ entry["last_updated"] = now
81
+ entry["freshness_days"] = 0
82
+ entry["source_count"] = entry.get("source_count", 0) + 1
83
+
84
+ # Add source summary (keep last 10)
85
+ sources = entry.get("sources", [])
86
+ source_summary = {
87
+ "type": source_entry.get("type", "unknown"),
88
+ "timestamp": now
89
+ }
90
+ if source_entry.get("query"):
91
+ source_summary["query"] = source_entry.get("query", "")[:100]
92
+ if source_entry.get("url"):
93
+ source_summary["url"] = source_entry.get("url", "")[:200]
94
+ if source_entry.get("library"):
95
+ source_summary["library"] = source_entry.get("library", "")
96
+
97
+ sources.append(source_summary)
98
+ entry["sources"] = sources[-10:] # Keep last 10
99
+
100
+ # Save index
101
+ RESEARCH_INDEX.write_text(json.dumps(index, indent=2))
102
+ return True
103
+
104
+
30
105
  def main():
31
106
  # Read hook input from stdin
32
107
  try:
@@ -261,6 +336,11 @@ def main():
261
336
  # Add to sources list
262
337
  sources.append(source_entry)
263
338
 
339
+ # v3.6.7: Update research index.json for freshness tracking
340
+ endpoint, _ = get_active_endpoint(state)
341
+ if endpoint:
342
+ update_research_index(endpoint, source_entry)
343
+
264
344
  # Also add to research_queries for prompt verification
265
345
  research_queries = state.setdefault("research_queries", [])
266
346
  query_entry = {
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Hook: PostToolUse for Write/Edit
4
+ Purpose: Auto-create API Showcase page when first API is created
5
+
6
+ This hook monitors for new API registrations. When the first API is added
7
+ to registry.json, it creates the API Showcase page at src/app/api-showcase/
8
+ if it doesn't exist.
9
+
10
+ Version: 3.9.0
11
+
12
+ Returns:
13
+ - {"continue": true} - Always continues
14
+ - May include "notify" about showcase creation
15
+ """
16
+ import json
17
+ import sys
18
+ from pathlib import Path
19
+ import shutil
20
+
21
+ # State and registry files in .claude/ directory
22
+ STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
23
+ REGISTRY_FILE = Path(__file__).parent.parent / "registry.json"
24
+
25
+
26
+ def copy_showcase_templates(cwd):
27
+ """Copy API showcase templates to src/app/api-showcase/."""
28
+ # Source templates (installed by CLI)
29
+ templates_dir = Path(__file__).parent.parent / "templates" / "api-showcase"
30
+
31
+ # Destination
32
+ showcase_dir = cwd / "src" / "app" / "api-showcase"
33
+
34
+ # Create directory if needed
35
+ showcase_dir.mkdir(parents=True, exist_ok=True)
36
+
37
+ # Copy template files
38
+ templates_to_copy = [
39
+ ("page.tsx", "page.tsx"),
40
+ ("APIShowcase.tsx", "_components/APIShowcase.tsx"),
41
+ ("APICard.tsx", "_components/APICard.tsx"),
42
+ ("APIModal.tsx", "_components/APIModal.tsx"),
43
+ ("APITester.tsx", "_components/APITester.tsx"),
44
+ ]
45
+
46
+ created_files = []
47
+ for src_name, dest_name in templates_to_copy:
48
+ src_path = templates_dir / src_name
49
+ dest_path = showcase_dir / dest_name
50
+
51
+ # Create subdirectories if needed
52
+ dest_path.parent.mkdir(parents=True, exist_ok=True)
53
+
54
+ if src_path.exists() and not dest_path.exists():
55
+ shutil.copy2(src_path, dest_path)
56
+ created_files.append(str(dest_path.relative_to(cwd)))
57
+
58
+ return created_files
59
+
60
+
61
+ def main():
62
+ # Read hook input from stdin
63
+ try:
64
+ input_data = json.load(sys.stdin)
65
+ except json.JSONDecodeError:
66
+ print(json.dumps({"continue": True}))
67
+ sys.exit(0)
68
+
69
+ tool_name = input_data.get("tool_name", "")
70
+
71
+ # Only process Write/Edit operations
72
+ if tool_name not in ["Write", "Edit"]:
73
+ print(json.dumps({"continue": True}))
74
+ sys.exit(0)
75
+
76
+ # Check if state file exists
77
+ if not STATE_FILE.exists():
78
+ print(json.dumps({"continue": True}))
79
+ sys.exit(0)
80
+
81
+ # Load state
82
+ try:
83
+ state = json.loads(STATE_FILE.read_text())
84
+ except json.JSONDecodeError:
85
+ print(json.dumps({"continue": True}))
86
+ sys.exit(0)
87
+
88
+ workflow = state.get("workflow", "")
89
+
90
+ # Only apply for API workflows
91
+ if workflow not in ["api-create", "combine-api"]:
92
+ print(json.dumps({"continue": True}))
93
+ sys.exit(0)
94
+
95
+ # Check if completion phase is complete
96
+ active_endpoint = state.get("active_endpoint", "")
97
+ endpoints = state.get("endpoints", {})
98
+
99
+ if active_endpoint and active_endpoint in endpoints:
100
+ phases = endpoints[active_endpoint].get("phases", {})
101
+ else:
102
+ phases = state.get("phases", {})
103
+
104
+ completion = phases.get("completion", {})
105
+ if completion.get("status") != "complete":
106
+ print(json.dumps({"continue": True}))
107
+ sys.exit(0)
108
+
109
+ # Check if showcase already exists
110
+ cwd = Path.cwd()
111
+ showcase_page = cwd / "src" / "app" / "api-showcase" / "page.tsx"
112
+
113
+ if showcase_page.exists():
114
+ print(json.dumps({"continue": True}))
115
+ sys.exit(0)
116
+
117
+ # Check if we have APIs in registry
118
+ if not REGISTRY_FILE.exists():
119
+ print(json.dumps({"continue": True}))
120
+ sys.exit(0)
121
+
122
+ try:
123
+ registry = json.loads(REGISTRY_FILE.read_text())
124
+ except json.JSONDecodeError:
125
+ print(json.dumps({"continue": True}))
126
+ sys.exit(0)
127
+
128
+ apis = registry.get("apis", {})
129
+ combined = registry.get("combined", {})
130
+
131
+ # Create showcase if we have at least one API
132
+ if apis or combined:
133
+ created_files = copy_showcase_templates(cwd)
134
+
135
+ if created_files:
136
+ print(json.dumps({
137
+ "continue": True,
138
+ "notify": f"Created API Showcase at /api-showcase ({len(created_files)} files)"
139
+ }))
140
+ else:
141
+ print(json.dumps({"continue": True}))
142
+ else:
143
+ print(json.dumps({"continue": True}))
144
+
145
+ sys.exit(0)
146
+
147
+
148
+ if __name__ == "__main__":
149
+ main()
@@ -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()