@hustle-together/api-dev-tools 3.12.3 → 3.12.16
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/.claude/commands/hustle-build.md +259 -0
- package/.claude/commands/hustle-combine.md +1089 -0
- package/.claude/commands/hustle-ui-create-page.md +1078 -0
- package/.claude/commands/hustle-ui-create.md +1058 -0
- package/.claude/hooks/auto-answer.py +305 -0
- package/.claude/hooks/cache-research.py +337 -0
- package/.claude/hooks/check-api-routes.py +168 -0
- package/.claude/hooks/check-playwright-setup.py +103 -0
- package/.claude/hooks/check-storybook-setup.py +81 -0
- package/.claude/hooks/check-update.py +132 -0
- package/.claude/hooks/completion-promise-detector.py +293 -0
- package/.claude/hooks/context-capacity-warning.py +171 -0
- package/.claude/hooks/detect-interruption.py +165 -0
- package/.claude/hooks/docs-update-check.py +120 -0
- package/.claude/hooks/enforce-a11y-audit.py +202 -0
- package/.claude/hooks/enforce-brand-guide.py +241 -0
- package/.claude/hooks/enforce-component-type-confirm.py +97 -0
- package/.claude/hooks/enforce-dry-run.py +134 -0
- package/.claude/hooks/enforce-freshness.py +184 -0
- package/.claude/hooks/enforce-page-components.py +186 -0
- package/.claude/hooks/enforce-page-data-schema.py +155 -0
- package/.claude/hooks/enforce-questions-sourced.py +146 -0
- package/.claude/hooks/enforce-schema-from-interview.py +248 -0
- package/.claude/hooks/enforce-ui-disambiguation.py +108 -0
- package/.claude/hooks/enforce-ui-interview.py +130 -0
- package/.claude/hooks/generate-adr-options.py +282 -0
- package/.claude/hooks/generate-manifest-entry.py +1161 -0
- package/.claude/hooks/hook_utils.py +609 -0
- package/.claude/hooks/lib/__init__.py +1 -0
- package/.claude/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
- package/.claude/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
- package/.claude/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
- package/.claude/hooks/lib/greptile.py +355 -0
- package/.claude/hooks/lib/ntfy.py +209 -0
- package/.claude/hooks/notify-input-needed.py +73 -0
- package/.claude/hooks/notify-phase-complete.py +90 -0
- package/.claude/hooks/ntfy-on-question.py +240 -0
- package/.claude/hooks/orchestrator-completion.py +313 -0
- package/.claude/hooks/orchestrator-handoff.py +267 -0
- package/.claude/hooks/orchestrator-session-startup.py +146 -0
- package/.claude/hooks/parallel-orchestrator.py +451 -0
- package/.claude/hooks/project-document-prompt.py +302 -0
- package/.claude/hooks/remote-question-proxy.py +284 -0
- package/.claude/hooks/remote-question-server.py +1224 -0
- package/.claude/hooks/run-code-review.py +393 -0
- package/.claude/hooks/run-visual-qa.py +338 -0
- package/.claude/hooks/session-logger.py +323 -0
- package/.claude/hooks/test-orchestrator-reground.py +248 -0
- package/.claude/hooks/track-scope-coverage.py +220 -0
- package/.claude/hooks/track-token-usage.py +121 -0
- package/.claude/hooks/update-adr-decision.py +236 -0
- package/.claude/hooks/update-api-showcase.py +161 -0
- package/.claude/hooks/update-registry.py +352 -0
- package/.claude/hooks/update-testing-checklist.py +195 -0
- package/.claude/hooks/update-ui-showcase.py +224 -0
- package/.claude/settings.local.json +7 -1
- package/.claude/test-auto-answer-bot.py +183 -0
- package/.claude/test-completion-detector.py +263 -0
- package/.claude/test-orchestrator-state.json +20 -0
- package/.claude/test-orchestrator.sh +271 -0
- package/.skills/api-create/SKILL.md +88 -3
- package/.skills/docs-sync/SKILL.md +260 -0
- package/.skills/hustle-build/SKILL.md +459 -0
- package/.skills/hustle-build-review/SKILL.md +518 -0
- package/CHANGELOG.md +87 -0
- package/README.md +86 -9
- package/bin/cli.js +1302 -88
- package/commands/hustle-api-create.md +22 -0
- package/commands/hustle-combine.md +81 -2
- package/commands/hustle-ui-create-page.md +84 -2
- package/commands/hustle-ui-create.md +82 -2
- package/hooks/auto-answer.py +228 -0
- package/hooks/check-update.py +132 -0
- package/hooks/ntfy-on-question.py +227 -0
- package/hooks/orchestrator-completion.py +313 -0
- package/hooks/orchestrator-handoff.py +189 -0
- package/hooks/orchestrator-session-startup.py +146 -0
- package/hooks/periodic-reground.py +230 -67
- package/hooks/update-api-showcase.py +13 -1
- package/hooks/update-ui-showcase.py +13 -1
- package/package.json +7 -3
- package/scripts/extract-schema-docs.cjs +322 -0
- package/templates/CLAUDE-SECTION.md +89 -64
- package/templates/api-showcase/_components/APIModal.tsx +100 -8
- package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
- package/templates/api-showcase/_components/APITester.tsx +367 -58
- package/templates/docs/page.tsx +230 -0
- package/templates/hustle-build-defaults.json +84 -0
- package/templates/hustle-dev-dashboard/page.tsx +365 -0
- package/templates/playwright-report/page.tsx +258 -0
- package/templates/settings.json +88 -7
- package/templates/test-results/page.tsx +237 -0
- package/templates/typedoc.json +19 -0
- package/templates/ui-showcase/_components/UIShowcase.tsx +1 -1
- package/templates/ui-showcase/page.tsx +1 -1
- package/.claude/api-dev-state.json +0 -466
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
ADR Decision Updater Hook
|
|
4
|
+
|
|
5
|
+
Updates Architecture Decision Records when user makes a decision during interview.
|
|
6
|
+
Changes status from PROPOSED to ACCEPTED and records the decision with reasoning.
|
|
7
|
+
|
|
8
|
+
Hook Type: PostToolUse (matcher: AskUserQuestion)
|
|
9
|
+
|
|
10
|
+
Flow:
|
|
11
|
+
1. Interview phase presents options to user (referencing ADR)
|
|
12
|
+
2. User selects an option
|
|
13
|
+
3. Hook detects the answer relates to a PROPOSED ADR
|
|
14
|
+
4. Updates ADR with decision, reasoning, and consequences
|
|
15
|
+
5. Updates registry with decision
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_config():
|
|
26
|
+
"""Load ADR configuration"""
|
|
27
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
|
|
28
|
+
|
|
29
|
+
config_file = Path(project_dir) / ".claude" / "hustle-build-defaults.json"
|
|
30
|
+
if config_file.exists():
|
|
31
|
+
try:
|
|
32
|
+
config = json.loads(config_file.read_text())
|
|
33
|
+
return config.get("adr", {})
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
return {"enabled": True}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_registry():
|
|
41
|
+
"""Load current registry"""
|
|
42
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
|
|
43
|
+
registry_file = Path(project_dir) / ".claude" / "registry.json"
|
|
44
|
+
|
|
45
|
+
if registry_file.exists():
|
|
46
|
+
try:
|
|
47
|
+
return json.loads(registry_file.read_text())
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
return {}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def save_registry(registry):
|
|
54
|
+
"""Save registry"""
|
|
55
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
|
|
56
|
+
registry_file = Path(project_dir) / ".claude" / "registry.json"
|
|
57
|
+
registry_file.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
registry_file.write_text(json.dumps(registry, indent=2))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def find_matching_adr(question_text, answer_text, registry):
|
|
62
|
+
"""
|
|
63
|
+
Find a PROPOSED ADR that matches the question/answer.
|
|
64
|
+
Matches based on category keywords in question and answer options.
|
|
65
|
+
"""
|
|
66
|
+
adrs = registry.get("adrs", {})
|
|
67
|
+
|
|
68
|
+
for adr_key, adr in adrs.items():
|
|
69
|
+
if adr.get("status") != "proposed":
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
category = adr.get("category", "")
|
|
73
|
+
options = adr.get("options_considered", [])
|
|
74
|
+
|
|
75
|
+
# Check if question mentions the category
|
|
76
|
+
if category.lower() in question_text.lower():
|
|
77
|
+
# Check if answer matches one of the options
|
|
78
|
+
for opt in options:
|
|
79
|
+
if opt.lower() in answer_text.lower():
|
|
80
|
+
return adr_key, adr, opt
|
|
81
|
+
|
|
82
|
+
# Check if answer directly matches an option
|
|
83
|
+
for opt in options:
|
|
84
|
+
if opt.lower() in answer_text.lower():
|
|
85
|
+
# Verify category is relevant to question
|
|
86
|
+
category_keywords = {
|
|
87
|
+
"database": ["database", "storage", "data", "db"],
|
|
88
|
+
"auth": ["auth", "authentication", "login", "security"],
|
|
89
|
+
"cache": ["cache", "caching", "performance"],
|
|
90
|
+
"hosting": ["host", "deploy", "platform"],
|
|
91
|
+
"state": ["state", "store", "management"],
|
|
92
|
+
"styling": ["style", "css", "design", "ui"],
|
|
93
|
+
}
|
|
94
|
+
keywords = category_keywords.get(category, [category])
|
|
95
|
+
if any(kw in question_text.lower() for kw in keywords):
|
|
96
|
+
return adr_key, adr, opt
|
|
97
|
+
|
|
98
|
+
return None, None, None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def update_adr_file(adr, decision):
|
|
102
|
+
"""Update the ADR markdown file with the decision"""
|
|
103
|
+
project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
|
|
104
|
+
adr_file = Path(project_dir) / adr.get("file", "")
|
|
105
|
+
|
|
106
|
+
if not adr_file.exists():
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
content = adr_file.read_text()
|
|
110
|
+
|
|
111
|
+
# Update status
|
|
112
|
+
content = re.sub(
|
|
113
|
+
r"\*\*Status:\*\* PROPOSED",
|
|
114
|
+
"**Status:** ACCEPTED",
|
|
115
|
+
content
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Update decision section
|
|
119
|
+
decision_section = f"""## Decision
|
|
120
|
+
|
|
121
|
+
We will use **{decision.title()}** based on user selection during interview.
|
|
122
|
+
|
|
123
|
+
**Reasoning:** User prioritized this option based on project requirements.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
content = re.sub(
|
|
127
|
+
r"## Decision\n\n_Pending user selection during interview phase\._",
|
|
128
|
+
decision_section,
|
|
129
|
+
content
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Update consequences section
|
|
133
|
+
consequences_section = f"""## Consequences
|
|
134
|
+
|
|
135
|
+
### Positive
|
|
136
|
+
- Decision has been made, enabling implementation to proceed
|
|
137
|
+
- Choice aligns with user's stated requirements
|
|
138
|
+
|
|
139
|
+
### Negative
|
|
140
|
+
- Alternative options were not selected (may revisit if requirements change)
|
|
141
|
+
|
|
142
|
+
### Implementation Notes
|
|
143
|
+
- Proceed with {decision.title()} integration
|
|
144
|
+
- Update environment variables as needed
|
|
145
|
+
- Follow {decision.title()} best practices
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
content = re.sub(
|
|
149
|
+
r"## Consequences\n\n_To be documented after decision is made\._",
|
|
150
|
+
consequences_section,
|
|
151
|
+
content
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Add decision timestamp
|
|
155
|
+
content = content.replace(
|
|
156
|
+
"_This ADR was auto-generated during research.",
|
|
157
|
+
f"_Decision recorded: {datetime.now().strftime('%Y-%m-%d %H:%M')}_\n\n_This ADR was auto-generated during research."
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
adr_file.write_text(content)
|
|
161
|
+
return True
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def main():
|
|
165
|
+
# Get tool info
|
|
166
|
+
tool_name = os.environ.get("CLAUDE_TOOL_NAME", "")
|
|
167
|
+
tool_output = os.environ.get("CLAUDE_TOOL_OUTPUT", "")
|
|
168
|
+
tool_input = os.environ.get("CLAUDE_TOOL_INPUT", "{}")
|
|
169
|
+
|
|
170
|
+
# Only process AskUserQuestion results
|
|
171
|
+
if tool_name != "AskUserQuestion":
|
|
172
|
+
print(json.dumps({"continue": True}))
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
# Load config
|
|
176
|
+
config = load_config()
|
|
177
|
+
if not config.get("enabled", True):
|
|
178
|
+
print(json.dumps({"continue": True}))
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
# Parse question and answer
|
|
182
|
+
try:
|
|
183
|
+
input_data = json.loads(tool_input)
|
|
184
|
+
questions = input_data.get("questions", [])
|
|
185
|
+
if not questions:
|
|
186
|
+
print(json.dumps({"continue": True}))
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
question_text = questions[0].get("question", "")
|
|
190
|
+
except Exception:
|
|
191
|
+
question_text = ""
|
|
192
|
+
|
|
193
|
+
# The answer is in tool_output
|
|
194
|
+
answer_text = tool_output
|
|
195
|
+
|
|
196
|
+
if not question_text or not answer_text:
|
|
197
|
+
print(json.dumps({"continue": True}))
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
# Load registry and find matching ADR
|
|
201
|
+
registry = load_registry()
|
|
202
|
+
adr_key, adr, decision = find_matching_adr(question_text, answer_text, registry)
|
|
203
|
+
|
|
204
|
+
if not adr_key:
|
|
205
|
+
print(json.dumps({"continue": True}))
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
# Update ADR file
|
|
209
|
+
update_adr_file(adr, decision)
|
|
210
|
+
|
|
211
|
+
# Update registry
|
|
212
|
+
registry["adrs"][adr_key]["status"] = "accepted"
|
|
213
|
+
registry["adrs"][adr_key]["decision"] = decision
|
|
214
|
+
registry["adrs"][adr_key]["phase"] = "interview"
|
|
215
|
+
registry["adrs"][adr_key]["decided_at"] = datetime.now().isoformat()
|
|
216
|
+
save_registry(registry)
|
|
217
|
+
|
|
218
|
+
# Notify about ADR update
|
|
219
|
+
result = {
|
|
220
|
+
"continue": True,
|
|
221
|
+
"additionalContext": f"""## ADR Updated
|
|
222
|
+
|
|
223
|
+
**ADR-{adr.get('number', 0):04d}: {adr.get('title', '')}** has been updated.
|
|
224
|
+
|
|
225
|
+
- **Status:** PROPOSED → ACCEPTED
|
|
226
|
+
- **Decision:** {decision.title()}
|
|
227
|
+
- **File:** {adr.get('file', '')}
|
|
228
|
+
|
|
229
|
+
This decision is now recorded for future reference.
|
|
230
|
+
"""
|
|
231
|
+
}
|
|
232
|
+
print(json.dumps(result))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
if __name__ == "__main__":
|
|
236
|
+
main()
|
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
shared_templates_dir = Path(__file__).parent.parent / "templates" / "shared"
|
|
31
|
+
|
|
32
|
+
# Destination
|
|
33
|
+
showcase_dir = cwd / "src" / "app" / "api-showcase"
|
|
34
|
+
shared_dir = cwd / "src" / "app" / "shared"
|
|
35
|
+
|
|
36
|
+
# Create directories if needed
|
|
37
|
+
showcase_dir.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
shared_dir.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
|
|
40
|
+
# Copy template files
|
|
41
|
+
templates_to_copy = [
|
|
42
|
+
("page.tsx", "page.tsx"),
|
|
43
|
+
("APIShowcase.tsx", "_components/APIShowcase.tsx"),
|
|
44
|
+
("APICard.tsx", "_components/APICard.tsx"),
|
|
45
|
+
("APIModal.tsx", "_components/APIModal.tsx"),
|
|
46
|
+
("APITester.tsx", "_components/APITester.tsx"),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
created_files = []
|
|
50
|
+
for src_name, dest_name in templates_to_copy:
|
|
51
|
+
src_path = templates_dir / src_name
|
|
52
|
+
dest_path = showcase_dir / dest_name
|
|
53
|
+
|
|
54
|
+
# Create subdirectories if needed
|
|
55
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
|
|
57
|
+
if src_path.exists() and not dest_path.exists():
|
|
58
|
+
shutil.copy2(src_path, dest_path)
|
|
59
|
+
created_files.append(str(dest_path.relative_to(cwd)))
|
|
60
|
+
|
|
61
|
+
# Also copy shared components (HeroHeader, etc.)
|
|
62
|
+
if shared_templates_dir.exists():
|
|
63
|
+
for src_file in shared_templates_dir.iterdir():
|
|
64
|
+
if src_file.is_file():
|
|
65
|
+
dest_path = shared_dir / src_file.name
|
|
66
|
+
if not dest_path.exists():
|
|
67
|
+
shutil.copy2(src_file, dest_path)
|
|
68
|
+
created_files.append(str(dest_path.relative_to(cwd)))
|
|
69
|
+
|
|
70
|
+
return created_files
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def main():
|
|
74
|
+
# Read hook input from stdin
|
|
75
|
+
try:
|
|
76
|
+
input_data = json.load(sys.stdin)
|
|
77
|
+
except json.JSONDecodeError:
|
|
78
|
+
print(json.dumps({"continue": True}))
|
|
79
|
+
sys.exit(0)
|
|
80
|
+
|
|
81
|
+
tool_name = input_data.get("tool_name", "")
|
|
82
|
+
|
|
83
|
+
# Only process Write/Edit operations
|
|
84
|
+
if tool_name not in ["Write", "Edit"]:
|
|
85
|
+
print(json.dumps({"continue": True}))
|
|
86
|
+
sys.exit(0)
|
|
87
|
+
|
|
88
|
+
# Check if state file exists
|
|
89
|
+
if not STATE_FILE.exists():
|
|
90
|
+
print(json.dumps({"continue": True}))
|
|
91
|
+
sys.exit(0)
|
|
92
|
+
|
|
93
|
+
# Load state
|
|
94
|
+
try:
|
|
95
|
+
state = json.loads(STATE_FILE.read_text())
|
|
96
|
+
except json.JSONDecodeError:
|
|
97
|
+
print(json.dumps({"continue": True}))
|
|
98
|
+
sys.exit(0)
|
|
99
|
+
|
|
100
|
+
workflow = state.get("workflow", "")
|
|
101
|
+
|
|
102
|
+
# Only apply for API workflows
|
|
103
|
+
if workflow not in ["api-create", "combine-api"]:
|
|
104
|
+
print(json.dumps({"continue": True}))
|
|
105
|
+
sys.exit(0)
|
|
106
|
+
|
|
107
|
+
# Check if completion phase is complete
|
|
108
|
+
active_endpoint = state.get("active_endpoint", "")
|
|
109
|
+
endpoints = state.get("endpoints", {})
|
|
110
|
+
|
|
111
|
+
if active_endpoint and active_endpoint in endpoints:
|
|
112
|
+
phases = endpoints[active_endpoint].get("phases", {})
|
|
113
|
+
else:
|
|
114
|
+
phases = state.get("phases", {})
|
|
115
|
+
|
|
116
|
+
completion = phases.get("completion", {})
|
|
117
|
+
if completion.get("status") != "complete":
|
|
118
|
+
print(json.dumps({"continue": True}))
|
|
119
|
+
sys.exit(0)
|
|
120
|
+
|
|
121
|
+
# Check if showcase already exists
|
|
122
|
+
cwd = Path.cwd()
|
|
123
|
+
showcase_page = cwd / "src" / "app" / "api-showcase" / "page.tsx"
|
|
124
|
+
|
|
125
|
+
if showcase_page.exists():
|
|
126
|
+
print(json.dumps({"continue": True}))
|
|
127
|
+
sys.exit(0)
|
|
128
|
+
|
|
129
|
+
# Check if we have APIs in registry
|
|
130
|
+
if not REGISTRY_FILE.exists():
|
|
131
|
+
print(json.dumps({"continue": True}))
|
|
132
|
+
sys.exit(0)
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
registry = json.loads(REGISTRY_FILE.read_text())
|
|
136
|
+
except json.JSONDecodeError:
|
|
137
|
+
print(json.dumps({"continue": True}))
|
|
138
|
+
sys.exit(0)
|
|
139
|
+
|
|
140
|
+
apis = registry.get("apis", {})
|
|
141
|
+
combined = registry.get("combined", {})
|
|
142
|
+
|
|
143
|
+
# Create showcase if we have at least one API
|
|
144
|
+
if apis or combined:
|
|
145
|
+
created_files = copy_showcase_templates(cwd)
|
|
146
|
+
|
|
147
|
+
if created_files:
|
|
148
|
+
print(json.dumps({
|
|
149
|
+
"continue": True,
|
|
150
|
+
"notify": f"Created API Showcase at /api-showcase ({len(created_files)} files)"
|
|
151
|
+
}))
|
|
152
|
+
else:
|
|
153
|
+
print(json.dumps({"continue": True}))
|
|
154
|
+
else:
|
|
155
|
+
print(json.dumps({"continue": True}))
|
|
156
|
+
|
|
157
|
+
sys.exit(0)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
main()
|