@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.
- package/README.md +5307 -258
- package/bin/cli.js +348 -20
- package/commands/README.md +459 -71
- package/commands/hustle-api-continue.md +158 -0
- package/commands/{api-create.md → hustle-api-create.md} +22 -2
- 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.md +825 -0
- package/hooks/api-workflow-check.py +385 -19
- package/hooks/cache-research.py +337 -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-brand-guide.py +131 -0
- package/hooks/enforce-documentation.py +60 -8
- package/hooks/enforce-freshness.py +184 -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 +981 -0
- package/hooks/session-logger.py +297 -0
- package/hooks/session-startup.py +65 -10
- 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 +148 -0
- package/package.json +8 -2
- 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 +179 -161
- package/templates/api-showcase/APICard.tsx +153 -0
- package/templates/api-showcase/APIModal.tsx +375 -0
- package/templates/api-showcase/APIShowcase.tsx +231 -0
- package/templates/api-showcase/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 +74 -0
- package/templates/shared/HeroHeader.tsx +261 -0
- package/templates/shared/index.ts +1 -0
- package/templates/ui-showcase/PreviewCard.tsx +315 -0
- package/templates/ui-showcase/PreviewModal.tsx +676 -0
- package/templates/ui-showcase/UIShowcase.tsx +262 -0
- package/templates/ui-showcase/page.tsx +26 -0
package/hooks/track-tool-use.py
CHANGED
|
@@ -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.
|
|
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()
|