@hustle-together/api-dev-tools 3.12.16 → 4.5.3

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 (180) hide show
  1. package/.claude/adr-requests/.gitkeep +10 -0
  2. package/.claude/agents/adr-researcher.md +109 -0
  3. package/.claude/agents/visual-analyzer.md +183 -0
  4. package/.claude/api-dev-state.json +10 -0
  5. package/.claude/documentation-audit.json +114 -0
  6. package/.claude/registry.json +289 -0
  7. package/.claude/settings.json +45 -1
  8. package/.claude/settings.local.json +1 -7
  9. package/.claude/workflow-logs/None.json +49 -0
  10. package/.claude/workflow-logs/session-20251230-143727.json +106 -0
  11. package/.skills/adr-deep-research/SKILL.md +351 -0
  12. package/.skills/api-create/SKILL.md +34 -20
  13. package/.skills/api-research/SKILL.md +130 -0
  14. package/.skills/docs-update/SKILL.md +205 -0
  15. package/.skills/hustle-brand/SKILL.md +368 -0
  16. package/.skills/hustle-build/SKILL.md +365 -38
  17. package/.skills/parallel-spawn/SKILL.md +212 -0
  18. package/.skills/ralph-continue/SKILL.md +151 -0
  19. package/.skills/ralph-loop/SKILL.md +341 -0
  20. package/.skills/ralph-status/SKILL.md +87 -0
  21. package/.skills/refactor/SKILL.md +59 -0
  22. package/.skills/shadcn/SKILL.md +522 -0
  23. package/.skills/test-all/SKILL.md +210 -0
  24. package/.skills/test-builds/SKILL.md +208 -0
  25. package/.skills/test-debug/SKILL.md +212 -0
  26. package/.skills/test-e2e/SKILL.md +168 -0
  27. package/.skills/test-review/SKILL.md +707 -0
  28. package/.skills/test-unit/SKILL.md +143 -0
  29. package/.skills/test-visual/SKILL.md +301 -0
  30. package/.skills/token-report/SKILL.md +132 -0
  31. package/CHANGELOG.md +488 -0
  32. package/README.md +346 -53
  33. package/bin/cli.js +359 -123
  34. package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
  35. package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
  36. package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
  37. package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
  38. package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
  39. package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
  40. package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
  41. package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
  42. package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
  43. package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
  44. package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
  45. package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
  46. package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
  47. package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
  48. package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
  49. package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
  50. package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
  51. package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
  52. package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
  53. package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
  54. package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
  55. package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
  56. package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
  57. package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
  58. package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
  59. package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
  60. package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
  61. package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
  62. package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
  63. package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
  64. package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
  65. package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
  66. package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
  67. package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
  68. package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
  69. package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
  70. package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
  71. package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
  72. package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
  73. package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
  74. package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
  75. package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
  76. package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
  77. package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
  78. package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
  79. package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
  80. package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
  81. package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
  82. package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
  83. package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
  84. package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
  85. package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
  86. package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
  87. package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
  88. package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
  89. package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
  90. package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
  91. package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
  92. package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
  93. package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
  94. package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
  95. package/hooks/api-workflow-check.py +34 -0
  96. package/hooks/auto-answer.py +97 -20
  97. package/{.claude/hooks → hooks}/completion-promise-detector.py +0 -0
  98. package/{.claude/hooks → hooks}/context-capacity-warning.py +0 -0
  99. package/{.claude/hooks → hooks}/docs-update-check.py +0 -0
  100. package/{.claude/hooks → hooks}/enforce-dry-run.py +0 -0
  101. package/hooks/enforce-external-research.py +25 -0
  102. package/hooks/enforce-interview.py +20 -0
  103. package/{.claude/hooks → hooks}/generate-adr-options.py +0 -0
  104. package/{.claude/hooks → hooks}/hook_utils.py +0 -0
  105. package/hooks/ntfy-on-question.py +15 -2
  106. package/hooks/orchestrator-handoff.py +81 -3
  107. package/{.claude/hooks → hooks}/parallel-orchestrator.py +0 -0
  108. package/hooks/periodic-reground.py +40 -0
  109. package/{.claude/hooks → hooks}/remote-question-server.py +0 -0
  110. package/hooks/run-code-review.py +176 -29
  111. package/{.claude/hooks → hooks}/run-visual-qa.py +0 -0
  112. package/hooks/session-logger.py +27 -1
  113. package/hooks/session-startup.py +113 -0
  114. package/{.claude/hooks → hooks}/update-adr-decision.py +0 -0
  115. package/package.json +1 -1
  116. package/templates/.skills/hustle-interview/SKILL.md +174 -0
  117. package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
  118. package/templates/api-dev-state.json +33 -1
  119. package/templates/brand-page/page.tsx +645 -0
  120. package/templates/component/Component.visual.spec.ts +30 -24
  121. package/templates/eslint-plugin-zod-schema/index.js +446 -0
  122. package/templates/eslint-plugin-zod-schema/package.json +26 -0
  123. package/templates/github-workflows/security.yml +274 -0
  124. package/templates/hustle-build-defaults.json +53 -1
  125. package/templates/page/page.e2e.test.ts +30 -26
  126. package/templates/performance-budgets.json +63 -5
  127. package/templates/registry.json +279 -3
  128. package/templates/review-dashboard/page.tsx +510 -0
  129. package/templates/settings.json +74 -7
  130. package/templates/ui-showcase/_components/UIShowcase.tsx +47 -0
  131. package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
  132. package/.claude/commands/hustle-combine.md +0 -1089
  133. package/.claude/commands/hustle-ui-create-page.md +0 -1078
  134. package/.claude/commands/hustle-ui-create.md +0 -1058
  135. package/.claude/hooks/auto-answer.py +0 -305
  136. package/.claude/hooks/cache-research.py +0 -337
  137. package/.claude/hooks/check-api-routes.py +0 -168
  138. package/.claude/hooks/check-playwright-setup.py +0 -103
  139. package/.claude/hooks/check-storybook-setup.py +0 -81
  140. package/.claude/hooks/check-update.py +0 -132
  141. package/.claude/hooks/detect-interruption.py +0 -165
  142. package/.claude/hooks/enforce-a11y-audit.py +0 -202
  143. package/.claude/hooks/enforce-brand-guide.py +0 -241
  144. package/.claude/hooks/enforce-component-type-confirm.py +0 -97
  145. package/.claude/hooks/enforce-freshness.py +0 -184
  146. package/.claude/hooks/enforce-page-components.py +0 -186
  147. package/.claude/hooks/enforce-page-data-schema.py +0 -155
  148. package/.claude/hooks/enforce-questions-sourced.py +0 -146
  149. package/.claude/hooks/enforce-schema-from-interview.py +0 -248
  150. package/.claude/hooks/enforce-ui-disambiguation.py +0 -108
  151. package/.claude/hooks/enforce-ui-interview.py +0 -130
  152. package/.claude/hooks/generate-manifest-entry.py +0 -1161
  153. package/.claude/hooks/lib/__init__.py +0 -1
  154. package/.claude/hooks/lib/greptile.py +0 -355
  155. package/.claude/hooks/lib/ntfy.py +0 -209
  156. package/.claude/hooks/notify-input-needed.py +0 -73
  157. package/.claude/hooks/notify-phase-complete.py +0 -90
  158. package/.claude/hooks/ntfy-on-question.py +0 -240
  159. package/.claude/hooks/orchestrator-completion.py +0 -313
  160. package/.claude/hooks/orchestrator-handoff.py +0 -267
  161. package/.claude/hooks/orchestrator-session-startup.py +0 -146
  162. package/.claude/hooks/run-code-review.py +0 -393
  163. package/.claude/hooks/session-logger.py +0 -323
  164. package/.claude/hooks/test-orchestrator-reground.py +0 -248
  165. package/.claude/hooks/track-scope-coverage.py +0 -220
  166. package/.claude/hooks/track-token-usage.py +0 -121
  167. package/.claude/hooks/update-api-showcase.py +0 -161
  168. package/.claude/hooks/update-registry.py +0 -352
  169. package/.claude/hooks/update-ui-showcase.py +0 -224
  170. package/.claude/test-auto-answer-bot.py +0 -183
  171. package/.claude/test-completion-detector.py +0 -263
  172. package/.claude/test-orchestrator-state.json +0 -20
  173. package/.claude/test-orchestrator.sh +0 -271
  174. /package/{.claude/commands → commands}/hustle-build.md +0 -0
  175. /package/{.claude/hooks → hooks}/lib/__pycache__/__init__.cpython-314.pyc +0 -0
  176. /package/{.claude/hooks → hooks}/lib/__pycache__/greptile.cpython-314.pyc +0 -0
  177. /package/{.claude/hooks → hooks}/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
  178. /package/{.claude/hooks → hooks}/project-document-prompt.py +0 -0
  179. /package/{.claude/hooks → hooks}/remote-question-proxy.py +0 -0
  180. /package/{.claude/hooks → hooks}/update-testing-checklist.py +0 -0
@@ -1,220 +0,0 @@
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()
@@ -1,121 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Hook: PostToolUse
4
- Purpose: Track token usage per phase and display after phase completion
5
-
6
- Logs token usage to state file and outputs summary after each phase.
7
- Integrates with ccusage if available.
8
-
9
- Version: 3.10.0
10
- """
11
- import json
12
- import sys
13
- import subprocess
14
- from pathlib import Path
15
- from datetime import datetime
16
-
17
-
18
- def get_token_usage() -> dict:
19
- """Get current token usage from ccusage."""
20
- try:
21
- result = subprocess.run(
22
- ["ccusage", "--json"],
23
- capture_output=True,
24
- text=True,
25
- timeout=5
26
- )
27
- if result.returncode == 0:
28
- return json.loads(result.stdout)
29
- except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError):
30
- pass
31
- return {}
32
-
33
-
34
- def main():
35
- # Read hook input from stdin
36
- try:
37
- input_data = json.load(sys.stdin)
38
- except json.JSONDecodeError:
39
- sys.exit(0)
40
-
41
- tool_name = input_data.get("tool_name", "")
42
- tool_input = input_data.get("tool_input", {})
43
-
44
- # Only trigger on Write/Edit to state file
45
- if tool_name not in ["Write", "Edit"]:
46
- sys.exit(0)
47
-
48
- file_path = tool_input.get("file_path", "")
49
- if "api-dev-state.json" not in file_path:
50
- sys.exit(0)
51
-
52
- # Get current token usage
53
- usage = get_token_usage()
54
- if not usage:
55
- sys.exit(0)
56
-
57
- # Read state file
58
- cwd = Path.cwd()
59
- state_file = cwd / ".claude" / "api-dev-state.json"
60
-
61
- if not state_file.exists():
62
- sys.exit(0)
63
-
64
- try:
65
- state = json.loads(state_file.read_text())
66
- except (json.JSONDecodeError, IOError):
67
- sys.exit(0)
68
-
69
- # Check for phase completion and log usage
70
- phases = state.get("phases", {})
71
- current_phase = None
72
-
73
- for phase_key, phase_data in phases.items():
74
- if isinstance(phase_data, dict):
75
- status = phase_data.get("status", "")
76
- if status == "complete":
77
- current_phase = phase_key
78
-
79
- if current_phase:
80
- # Initialize token tracking in state if needed
81
- if "token_usage" not in state:
82
- state["token_usage"] = {
83
- "by_phase": {},
84
- "total_at_start": usage.get("total_tokens", 0),
85
- "started_at": datetime.now().isoformat()
86
- }
87
-
88
- # Record phase completion tokens
89
- state["token_usage"]["by_phase"][current_phase] = {
90
- "total_tokens": usage.get("total_tokens", 0),
91
- "total_cost": usage.get("total_cost", 0),
92
- "timestamp": datetime.now().isoformat()
93
- }
94
-
95
- # Calculate phase delta if we have previous data
96
- by_phase = state["token_usage"]["by_phase"]
97
- phase_keys = list(by_phase.keys())
98
-
99
- if len(phase_keys) >= 2:
100
- prev_phase = phase_keys[-2]
101
- prev_tokens = by_phase[prev_phase].get("total_tokens", 0)
102
- current_tokens = usage.get("total_tokens", 0)
103
- delta = current_tokens - prev_tokens
104
-
105
- # Output phase token summary
106
- print(f"\n📊 Phase '{current_phase}' Token Usage:", file=sys.stderr)
107
- print(f" Phase tokens: {delta:,}", file=sys.stderr)
108
- print(f" Total tokens: {current_tokens:,}", file=sys.stderr)
109
- print(f" Total cost: ${usage.get('total_cost', 0):.2f}", file=sys.stderr)
110
-
111
- # Update state file with token tracking
112
- try:
113
- state_file.write_text(json.dumps(state, indent=2))
114
- except IOError:
115
- pass
116
-
117
- sys.exit(0)
118
-
119
-
120
- if __name__ == "__main__":
121
- main()
@@ -1,161 +0,0 @@
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()