@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,220 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hook: PostToolUse for AskUserQuestion
|
|
4
|
+
Purpose: Track implemented vs deferred features for scope coverage
|
|
5
|
+
|
|
6
|
+
This hook tracks which features discovered during research are:
|
|
7
|
+
- Implemented (user chose to include)
|
|
8
|
+
- Deferred (user chose to skip for later)
|
|
9
|
+
- Discovered (found in docs but not yet decided)
|
|
10
|
+
|
|
11
|
+
Added in v3.6.7 for feature scope tracking.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
- JSON with scope coverage update info
|
|
15
|
+
"""
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_active_endpoint(state):
|
|
25
|
+
"""Get active endpoint - supports both old and new state formats."""
|
|
26
|
+
if "endpoints" in state and "active_endpoint" in state:
|
|
27
|
+
active = state.get("active_endpoint")
|
|
28
|
+
if active and active in state["endpoints"]:
|
|
29
|
+
return active, state["endpoints"][active]
|
|
30
|
+
return None, None
|
|
31
|
+
|
|
32
|
+
endpoint = state.get("endpoint")
|
|
33
|
+
if endpoint:
|
|
34
|
+
return endpoint, state
|
|
35
|
+
|
|
36
|
+
return None, None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def extract_feature_from_question(question, options):
|
|
40
|
+
"""Try to extract a feature name from the question."""
|
|
41
|
+
# Look for common patterns
|
|
42
|
+
patterns = [
|
|
43
|
+
"implement",
|
|
44
|
+
"include",
|
|
45
|
+
"support",
|
|
46
|
+
"enable",
|
|
47
|
+
"add"
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
question_lower = question.lower()
|
|
51
|
+
for pattern in patterns:
|
|
52
|
+
if pattern in question_lower:
|
|
53
|
+
# Extract the words after the pattern
|
|
54
|
+
idx = question_lower.find(pattern)
|
|
55
|
+
after = question_lower[idx:].split("?")[0]
|
|
56
|
+
# Clean up
|
|
57
|
+
words = after.split()[1:4] # Get 1-3 words after pattern
|
|
58
|
+
if words:
|
|
59
|
+
return " ".join(words).strip(",.?")
|
|
60
|
+
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def is_feature_decision(question, answer, options):
|
|
65
|
+
"""Determine if this was a feature implementation decision."""
|
|
66
|
+
question_lower = question.lower()
|
|
67
|
+
|
|
68
|
+
# Keywords suggesting feature decision
|
|
69
|
+
feature_keywords = [
|
|
70
|
+
"implement", "include", "support", "enable", "add",
|
|
71
|
+
"feature", "functionality", "capability"
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
has_keyword = any(k in question_lower for k in feature_keywords)
|
|
75
|
+
|
|
76
|
+
# Check if answer indicates yes/no/defer decision
|
|
77
|
+
answer_lower = str(answer).lower() if answer else ""
|
|
78
|
+
is_decision = any(word in answer_lower for word in [
|
|
79
|
+
"yes", "no", "skip", "defer", "later", "include", "exclude",
|
|
80
|
+
"implement", "confirm", "reject"
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
return has_keyword and is_decision
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def categorize_decision(answer):
|
|
87
|
+
"""Categorize the decision as implement/defer/skip."""
|
|
88
|
+
answer_lower = str(answer).lower() if answer else ""
|
|
89
|
+
|
|
90
|
+
if any(word in answer_lower for word in ["yes", "include", "implement", "confirm"]):
|
|
91
|
+
return "implement"
|
|
92
|
+
elif any(word in answer_lower for word in ["defer", "later", "phase 2", "future"]):
|
|
93
|
+
return "defer"
|
|
94
|
+
elif any(word in answer_lower for word in ["no", "skip", "exclude", "reject"]):
|
|
95
|
+
return "skip"
|
|
96
|
+
|
|
97
|
+
return "unknown"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def main():
|
|
101
|
+
try:
|
|
102
|
+
input_data = json.load(sys.stdin)
|
|
103
|
+
except json.JSONDecodeError:
|
|
104
|
+
print(json.dumps({"continue": True}))
|
|
105
|
+
sys.exit(0)
|
|
106
|
+
|
|
107
|
+
tool_name = input_data.get("tool_name", "")
|
|
108
|
+
tool_input = input_data.get("tool_input", {})
|
|
109
|
+
tool_result = input_data.get("tool_result", {})
|
|
110
|
+
|
|
111
|
+
if tool_name != "AskUserQuestion":
|
|
112
|
+
print(json.dumps({"continue": True}))
|
|
113
|
+
sys.exit(0)
|
|
114
|
+
|
|
115
|
+
if not STATE_FILE.exists():
|
|
116
|
+
print(json.dumps({"continue": True}))
|
|
117
|
+
sys.exit(0)
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
state = json.loads(STATE_FILE.read_text())
|
|
121
|
+
except json.JSONDecodeError:
|
|
122
|
+
print(json.dumps({"continue": True}))
|
|
123
|
+
sys.exit(0)
|
|
124
|
+
|
|
125
|
+
endpoint, endpoint_data = get_active_endpoint(state)
|
|
126
|
+
if not endpoint or not endpoint_data:
|
|
127
|
+
print(json.dumps({"continue": True}))
|
|
128
|
+
sys.exit(0)
|
|
129
|
+
|
|
130
|
+
# Get question and answer
|
|
131
|
+
question = tool_input.get("question", "")
|
|
132
|
+
options = tool_input.get("options", [])
|
|
133
|
+
|
|
134
|
+
# Get user's answer from result
|
|
135
|
+
answer = None
|
|
136
|
+
if isinstance(tool_result, dict):
|
|
137
|
+
answer = tool_result.get("answer", tool_result.get("value", ""))
|
|
138
|
+
elif isinstance(tool_result, str):
|
|
139
|
+
answer = tool_result
|
|
140
|
+
|
|
141
|
+
# Check if this is a feature decision
|
|
142
|
+
if not is_feature_decision(question, answer, options):
|
|
143
|
+
print(json.dumps({"continue": True}))
|
|
144
|
+
sys.exit(0)
|
|
145
|
+
|
|
146
|
+
# Extract feature name
|
|
147
|
+
feature = extract_feature_from_question(question, options)
|
|
148
|
+
if not feature:
|
|
149
|
+
feature = f"feature_{datetime.now().strftime('%H%M%S')}"
|
|
150
|
+
|
|
151
|
+
# Categorize decision
|
|
152
|
+
category = categorize_decision(answer)
|
|
153
|
+
|
|
154
|
+
# Ensure scope object exists
|
|
155
|
+
if "endpoints" in state:
|
|
156
|
+
if "scope" not in state["endpoints"][endpoint]:
|
|
157
|
+
state["endpoints"][endpoint]["scope"] = {
|
|
158
|
+
"discovered_features": [],
|
|
159
|
+
"implemented_features": [],
|
|
160
|
+
"deferred_features": [],
|
|
161
|
+
"coverage_percent": 0
|
|
162
|
+
}
|
|
163
|
+
scope = state["endpoints"][endpoint]["scope"]
|
|
164
|
+
else:
|
|
165
|
+
if "scope" not in state:
|
|
166
|
+
state["scope"] = {
|
|
167
|
+
"discovered_features": [],
|
|
168
|
+
"implemented_features": [],
|
|
169
|
+
"deferred_features": [],
|
|
170
|
+
"coverage_percent": 0
|
|
171
|
+
}
|
|
172
|
+
scope = state["scope"]
|
|
173
|
+
|
|
174
|
+
# Add to discovered if not already there
|
|
175
|
+
feature_entry = {
|
|
176
|
+
"name": feature,
|
|
177
|
+
"discovered_at": datetime.now().isoformat(),
|
|
178
|
+
"question": question[:100],
|
|
179
|
+
"decision": category
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if feature not in [f.get("name") if isinstance(f, dict) else f for f in scope["discovered_features"]]:
|
|
183
|
+
scope["discovered_features"].append(feature_entry)
|
|
184
|
+
|
|
185
|
+
# Add to appropriate category
|
|
186
|
+
if category == "implement":
|
|
187
|
+
if feature not in scope["implemented_features"]:
|
|
188
|
+
scope["implemented_features"].append(feature)
|
|
189
|
+
elif category == "defer":
|
|
190
|
+
defer_entry = {
|
|
191
|
+
"name": feature,
|
|
192
|
+
"reason": f"User chose to defer: {str(answer)[:50]}",
|
|
193
|
+
"deferred_at": datetime.now().isoformat()
|
|
194
|
+
}
|
|
195
|
+
if feature not in [f.get("name") if isinstance(f, dict) else f for f in scope["deferred_features"]]:
|
|
196
|
+
scope["deferred_features"].append(defer_entry)
|
|
197
|
+
|
|
198
|
+
# Calculate coverage
|
|
199
|
+
total = len(scope["discovered_features"])
|
|
200
|
+
implemented = len(scope["implemented_features"])
|
|
201
|
+
if total > 0:
|
|
202
|
+
scope["coverage_percent"] = round((implemented / total) * 100, 1)
|
|
203
|
+
|
|
204
|
+
# Save state
|
|
205
|
+
STATE_FILE.write_text(json.dumps(state, indent=2))
|
|
206
|
+
|
|
207
|
+
output = {
|
|
208
|
+
"hookSpecificOutput": {
|
|
209
|
+
"featureTracked": feature,
|
|
210
|
+
"decision": category,
|
|
211
|
+
"coveragePercent": scope["coverage_percent"]
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
print(json.dumps(output))
|
|
216
|
+
sys.exit(0)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
if __name__ == "__main__":
|
|
220
|
+
main()
|
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()
|