@hustle-together/api-dev-tools 3.12.3 → 4.5.1

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 (159) 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 +7 -463
  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/workflow-logs/None.json +49 -0
  9. package/.claude/workflow-logs/session-20251230-143727.json +106 -0
  10. package/.skills/adr-deep-research/SKILL.md +351 -0
  11. package/.skills/api-create/SKILL.md +116 -17
  12. package/.skills/api-research/SKILL.md +130 -0
  13. package/.skills/docs-sync/SKILL.md +260 -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 +786 -0
  17. package/.skills/hustle-build-review/SKILL.md +518 -0
  18. package/.skills/parallel-spawn/SKILL.md +212 -0
  19. package/.skills/ralph-continue/SKILL.md +151 -0
  20. package/.skills/ralph-loop/SKILL.md +341 -0
  21. package/.skills/ralph-status/SKILL.md +87 -0
  22. package/.skills/refactor/SKILL.md +59 -0
  23. package/.skills/shadcn/SKILL.md +522 -0
  24. package/.skills/test-all/SKILL.md +210 -0
  25. package/.skills/test-builds/SKILL.md +208 -0
  26. package/.skills/test-debug/SKILL.md +212 -0
  27. package/.skills/test-e2e/SKILL.md +168 -0
  28. package/.skills/test-review/SKILL.md +707 -0
  29. package/.skills/test-unit/SKILL.md +143 -0
  30. package/.skills/test-visual/SKILL.md +301 -0
  31. package/.skills/token-report/SKILL.md +132 -0
  32. package/CHANGELOG.md +575 -0
  33. package/README.md +426 -56
  34. package/bin/cli.js +1538 -88
  35. package/commands/hustle-api-create.md +22 -0
  36. package/commands/hustle-build.md +259 -0
  37. package/commands/hustle-combine.md +81 -2
  38. package/commands/hustle-ui-create-page.md +84 -2
  39. package/commands/hustle-ui-create.md +82 -2
  40. package/hooks/__pycache__/api-workflow-check.cpython-314.pyc +0 -0
  41. package/hooks/__pycache__/auto-answer.cpython-314.pyc +0 -0
  42. package/hooks/__pycache__/cache-research.cpython-314.pyc +0 -0
  43. package/hooks/__pycache__/check-api-routes.cpython-314.pyc +0 -0
  44. package/hooks/__pycache__/check-playwright-setup.cpython-314.pyc +0 -0
  45. package/hooks/__pycache__/check-storybook-setup.cpython-314.pyc +0 -0
  46. package/hooks/__pycache__/check-update.cpython-314.pyc +0 -0
  47. package/hooks/__pycache__/completion-promise-detector.cpython-314.pyc +0 -0
  48. package/hooks/__pycache__/context-capacity-warning.cpython-314.pyc +0 -0
  49. package/hooks/__pycache__/detect-interruption.cpython-314.pyc +0 -0
  50. package/hooks/__pycache__/docs-update-check.cpython-314.pyc +0 -0
  51. package/hooks/__pycache__/enforce-a11y-audit.cpython-314.pyc +0 -0
  52. package/hooks/__pycache__/enforce-brand-guide.cpython-314.pyc +0 -0
  53. package/hooks/__pycache__/enforce-component-type-confirm.cpython-314.pyc +0 -0
  54. package/hooks/__pycache__/enforce-deep-research.cpython-314.pyc +0 -0
  55. package/hooks/__pycache__/enforce-disambiguation.cpython-314.pyc +0 -0
  56. package/hooks/__pycache__/enforce-documentation.cpython-314.pyc +0 -0
  57. package/hooks/__pycache__/enforce-dry-run.cpython-314.pyc +0 -0
  58. package/hooks/__pycache__/enforce-environment.cpython-314.pyc +0 -0
  59. package/hooks/__pycache__/enforce-external-research.cpython-314.pyc +0 -0
  60. package/hooks/__pycache__/enforce-freshness.cpython-314.pyc +0 -0
  61. package/hooks/__pycache__/enforce-interview.cpython-314.pyc +0 -0
  62. package/hooks/__pycache__/enforce-page-components.cpython-314.pyc +0 -0
  63. package/hooks/__pycache__/enforce-page-data-schema.cpython-314.pyc +0 -0
  64. package/hooks/__pycache__/enforce-questions-sourced.cpython-314.pyc +0 -0
  65. package/hooks/__pycache__/enforce-refactor.cpython-314.pyc +0 -0
  66. package/hooks/__pycache__/enforce-research.cpython-314.pyc +0 -0
  67. package/hooks/__pycache__/enforce-schema-from-interview.cpython-314.pyc +0 -0
  68. package/hooks/__pycache__/enforce-schema.cpython-314.pyc +0 -0
  69. package/hooks/__pycache__/enforce-scope.cpython-314.pyc +0 -0
  70. package/hooks/__pycache__/enforce-tdd-red.cpython-314.pyc +0 -0
  71. package/hooks/__pycache__/enforce-ui-disambiguation.cpython-314.pyc +0 -0
  72. package/hooks/__pycache__/enforce-ui-interview.cpython-314.pyc +0 -0
  73. package/hooks/__pycache__/enforce-verify.cpython-314.pyc +0 -0
  74. package/hooks/__pycache__/generate-adr-options.cpython-314.pyc +0 -0
  75. package/hooks/__pycache__/generate-manifest-entry.cpython-314.pyc +0 -0
  76. package/hooks/__pycache__/hook_utils.cpython-314.pyc +0 -0
  77. package/hooks/__pycache__/notify-input-needed.cpython-314.pyc +0 -0
  78. package/hooks/__pycache__/notify-phase-complete.cpython-314.pyc +0 -0
  79. package/hooks/__pycache__/ntfy-on-question.cpython-314.pyc +0 -0
  80. package/hooks/__pycache__/orchestrator-completion.cpython-314.pyc +0 -0
  81. package/hooks/__pycache__/orchestrator-handoff.cpython-314.pyc +0 -0
  82. package/hooks/__pycache__/orchestrator-session-startup.cpython-314.pyc +0 -0
  83. package/hooks/__pycache__/parallel-orchestrator.cpython-314.pyc +0 -0
  84. package/hooks/__pycache__/periodic-reground.cpython-314.pyc +0 -0
  85. package/hooks/__pycache__/project-document-prompt.cpython-314.pyc +0 -0
  86. package/hooks/__pycache__/remote-question-proxy.cpython-314.pyc +0 -0
  87. package/hooks/__pycache__/remote-question-server.cpython-314.pyc +0 -0
  88. package/hooks/__pycache__/run-code-review.cpython-314.pyc +0 -0
  89. package/hooks/__pycache__/run-visual-qa.cpython-314.pyc +0 -0
  90. package/hooks/__pycache__/session-logger.cpython-314.pyc +0 -0
  91. package/hooks/__pycache__/session-startup.cpython-314.pyc +0 -0
  92. package/hooks/__pycache__/track-scope-coverage.cpython-314.pyc +0 -0
  93. package/hooks/__pycache__/track-token-usage.cpython-314.pyc +0 -0
  94. package/hooks/__pycache__/track-tool-use.cpython-314.pyc +0 -0
  95. package/hooks/__pycache__/update-adr-decision.cpython-314.pyc +0 -0
  96. package/hooks/__pycache__/update-api-showcase.cpython-314.pyc +0 -0
  97. package/hooks/__pycache__/update-registry.cpython-314.pyc +0 -0
  98. package/hooks/__pycache__/update-ui-showcase.cpython-314.pyc +0 -0
  99. package/hooks/__pycache__/verify-after-green.cpython-314.pyc +0 -0
  100. package/hooks/__pycache__/verify-implementation.cpython-314.pyc +0 -0
  101. package/hooks/api-workflow-check.py +34 -0
  102. package/hooks/auto-answer.py +305 -0
  103. package/hooks/check-update.py +132 -0
  104. package/hooks/completion-promise-detector.py +293 -0
  105. package/hooks/context-capacity-warning.py +171 -0
  106. package/hooks/docs-update-check.py +120 -0
  107. package/hooks/enforce-dry-run.py +134 -0
  108. package/hooks/enforce-external-research.py +25 -0
  109. package/hooks/enforce-interview.py +20 -0
  110. package/hooks/generate-adr-options.py +282 -0
  111. package/hooks/hook_utils.py +609 -0
  112. package/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
  113. package/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
  114. package/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
  115. package/hooks/ntfy-on-question.py +240 -0
  116. package/hooks/orchestrator-completion.py +313 -0
  117. package/hooks/orchestrator-handoff.py +267 -0
  118. package/hooks/orchestrator-session-startup.py +146 -0
  119. package/hooks/parallel-orchestrator.py +451 -0
  120. package/hooks/periodic-reground.py +270 -67
  121. package/hooks/project-document-prompt.py +302 -0
  122. package/hooks/remote-question-proxy.py +284 -0
  123. package/hooks/remote-question-server.py +1224 -0
  124. package/hooks/run-code-review.py +176 -29
  125. package/hooks/run-visual-qa.py +338 -0
  126. package/hooks/session-logger.py +27 -1
  127. package/hooks/session-startup.py +113 -0
  128. package/hooks/update-adr-decision.py +236 -0
  129. package/hooks/update-api-showcase.py +13 -1
  130. package/hooks/update-testing-checklist.py +195 -0
  131. package/hooks/update-ui-showcase.py +13 -1
  132. package/package.json +7 -3
  133. package/scripts/extract-schema-docs.cjs +322 -0
  134. package/templates/.skills/hustle-interview/SKILL.md +174 -0
  135. package/templates/CLAUDE-SECTION.md +89 -64
  136. package/templates/adr-viewer/_components/ADRViewer.tsx +326 -0
  137. package/templates/api-dev-state.json +33 -1
  138. package/templates/api-showcase/_components/APIModal.tsx +100 -8
  139. package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
  140. package/templates/api-showcase/_components/APITester.tsx +367 -58
  141. package/templates/brand-page/page.tsx +645 -0
  142. package/templates/component/Component.visual.spec.ts +30 -24
  143. package/templates/docs/page.tsx +230 -0
  144. package/templates/eslint-plugin-zod-schema/index.js +446 -0
  145. package/templates/eslint-plugin-zod-schema/package.json +26 -0
  146. package/templates/github-workflows/security.yml +274 -0
  147. package/templates/hustle-build-defaults.json +136 -0
  148. package/templates/hustle-dev-dashboard/page.tsx +365 -0
  149. package/templates/page/page.e2e.test.ts +30 -26
  150. package/templates/performance-budgets.json +63 -5
  151. package/templates/playwright-report/page.tsx +258 -0
  152. package/templates/registry.json +279 -3
  153. package/templates/review-dashboard/page.tsx +510 -0
  154. package/templates/settings.json +155 -7
  155. package/templates/test-results/page.tsx +237 -0
  156. package/templates/typedoc.json +19 -0
  157. package/templates/ui-showcase/_components/UIShowcase.tsx +48 -1
  158. package/templates/ui-showcase/_components/VisualTestingDashboard.tsx +579 -0
  159. package/templates/ui-showcase/page.tsx +1 -1
@@ -18,6 +18,12 @@ Updated in v3.6.7:
18
18
  - Support multi-API state structure (endpoints object)
19
19
  - Read research index from .claude/research/index.json file
20
20
  - Calculate freshness from timestamps
21
+
22
+ Updated in v4.5.0:
23
+ - Ensure .claude/ directories exist on session start
24
+ - Create registry.json from template if not exists
25
+ - Log session_start event to workflow logs
26
+ - Detect --dry-run and --resume flags
21
27
  """
22
28
  import json
23
29
  import sys
@@ -25,6 +31,20 @@ import os
25
31
  from datetime import datetime
26
32
  from pathlib import Path
27
33
 
34
+ # Import shared utilities
35
+ try:
36
+ from hook_utils import (
37
+ ensure_directories,
38
+ ensure_registry,
39
+ log_workflow_event,
40
+ load_state as utils_load_state,
41
+ save_state as utils_save_state,
42
+ get_project_dir
43
+ )
44
+ UTILS_AVAILABLE = True
45
+ except ImportError:
46
+ UTILS_AVAILABLE = False
47
+
28
48
  # State file is in .claude/ directory (sibling to hooks/)
29
49
  STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
30
50
  RESEARCH_INDEX = Path(__file__).parent.parent / "research" / "index.json"
@@ -99,6 +119,93 @@ def calculate_days_old(timestamp_str):
99
119
  return 0
100
120
 
101
121
 
122
+ def setup_session():
123
+ """
124
+ Initialize session: create directories, registry, log session start.
125
+ Called at the beginning of every session (v4.5.0).
126
+
127
+ Returns:
128
+ dict: Setup results with directories created, registry status
129
+ """
130
+ results = {
131
+ "directories_created": [],
132
+ "registry_created": False,
133
+ "session_logged": False
134
+ }
135
+
136
+ if not UTILS_AVAILABLE:
137
+ return results
138
+
139
+ # Ensure directories exist
140
+ try:
141
+ results["directories_created"] = ensure_directories()
142
+ except Exception:
143
+ pass
144
+
145
+ # Ensure registry exists
146
+ try:
147
+ success, created = ensure_registry()
148
+ results["registry_created"] = created
149
+ except Exception:
150
+ pass
151
+
152
+ # Log session start event
153
+ try:
154
+ log_workflow_event("session_start", {
155
+ "directories_created": results["directories_created"],
156
+ "registry_created": results["registry_created"]
157
+ })
158
+ results["session_logged"] = True
159
+ except Exception:
160
+ pass
161
+
162
+ return results
163
+
164
+
165
+ def detect_flags(input_data):
166
+ """
167
+ Detect --dry-run, --resume, and other flags from conversation context.
168
+ Sets flags in state for other hooks to use (v4.5.0).
169
+
170
+ Args:
171
+ input_data: Hook input from stdin
172
+
173
+ Returns:
174
+ dict: Detected flags
175
+ """
176
+ flags = {
177
+ "dry_run": False,
178
+ "resume": None,
179
+ "parallel": False
180
+ }
181
+
182
+ if not UTILS_AVAILABLE:
183
+ return flags
184
+
185
+ # The conversation context might contain flags
186
+ # Note: In SessionStart, we have limited context - flags are typically
187
+ # detected by skills reading the user's initial message
188
+ # This function prepares the state structure for flag detection
189
+
190
+ try:
191
+ state = utils_load_state()
192
+
193
+ # Initialize flags structure if not present
194
+ if "flags" not in state:
195
+ state["flags"] = {
196
+ "dry_run": False,
197
+ "resume": None,
198
+ "parallel": False
199
+ }
200
+ utils_save_state(state)
201
+
202
+ flags = state.get("flags", flags)
203
+ except Exception:
204
+ pass
205
+
206
+ return flags
207
+
208
+
102
209
  def main():
103
210
  # Read hook input from stdin
104
211
  try:
@@ -108,6 +215,12 @@ def main():
108
215
 
109
216
  cwd = input_data.get("cwd", os.getcwd())
110
217
 
218
+ # v4.5.0: Initialize session (directories, registry, logging)
219
+ setup_results = setup_session()
220
+
221
+ # v4.5.0: Detect and store flags
222
+ flags = detect_flags(input_data)
223
+
111
224
  # Check if state file exists
112
225
  if not STATE_FILE.exists():
113
226
  # No active workflow - just continue without injection
@@ -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()
@@ -27,12 +27,15 @@ def copy_showcase_templates(cwd):
27
27
  """Copy API showcase templates to src/app/api-showcase/."""
28
28
  # Source templates (installed by CLI)
29
29
  templates_dir = Path(__file__).parent.parent / "templates" / "api-showcase"
30
+ shared_templates_dir = Path(__file__).parent.parent / "templates" / "shared"
30
31
 
31
32
  # Destination
32
33
  showcase_dir = cwd / "src" / "app" / "api-showcase"
34
+ shared_dir = cwd / "src" / "app" / "shared"
33
35
 
34
- # Create directory if needed
36
+ # Create directories if needed
35
37
  showcase_dir.mkdir(parents=True, exist_ok=True)
38
+ shared_dir.mkdir(parents=True, exist_ok=True)
36
39
 
37
40
  # Copy template files
38
41
  templates_to_copy = [
@@ -55,6 +58,15 @@ def copy_showcase_templates(cwd):
55
58
  shutil.copy2(src_path, dest_path)
56
59
  created_files.append(str(dest_path.relative_to(cwd)))
57
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
+
58
70
  return created_files
59
71
 
60
72
 
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Auto-update TESTING_CHECKLIST.md when tests pass.
4
+
5
+ Hook Type: PostToolUse (matcher: Bash)
6
+
7
+ Detects test pass patterns and updates the checklist file with:
8
+ - Test results (PASS/FAIL)
9
+ - Timestamp
10
+ - Comments
11
+
12
+ Works by:
13
+ 1. Detecting test-related Bash commands
14
+ 2. Parsing output for pass/fail patterns
15
+ 3. Updating the corresponding checklist rows
16
+ """
17
+
18
+ import json
19
+ import os
20
+ import re
21
+ import sys
22
+ from datetime import datetime
23
+ from pathlib import Path
24
+
25
+
26
+ def get_tool_result():
27
+ """Get the tool result from environment"""
28
+ result = os.environ.get("CLAUDE_TOOL_RESULT", "")
29
+ return result
30
+
31
+
32
+ def get_tool_input():
33
+ """Get the tool input from environment"""
34
+ try:
35
+ input_json = os.environ.get("CLAUDE_TOOL_INPUT", "{}")
36
+ return json.loads(input_json)
37
+ except Exception:
38
+ return {}
39
+
40
+
41
+ def detect_test_type(command: str, output: str) -> dict:
42
+ """Detect what type of test was run and if it passed"""
43
+ result = {
44
+ "is_test": False,
45
+ "test_type": None,
46
+ "passed": None,
47
+ "hook_name": None,
48
+ "details": None
49
+ }
50
+
51
+ command_lower = command.lower()
52
+
53
+ # Hook compilation test
54
+ if "python3" in command_lower and ".py" in command_lower:
55
+ if "hooks/" in command or ".claude/hooks/" in command:
56
+ result["is_test"] = True
57
+ result["test_type"] = "hook_compile"
58
+ # Extract hook name
59
+ match = re.search(r'(?:hooks/|\.claude/hooks/)([^/\s]+\.py)', command)
60
+ if match:
61
+ result["hook_name"] = match.group(1)
62
+ # Check for pass/fail
63
+ if "Traceback" in output or "Error" in output or "SyntaxError" in output:
64
+ result["passed"] = False
65
+ result["details"] = "Syntax/import error"
66
+ elif "exit code" in output.lower():
67
+ exit_match = re.search(r'exit code[:\s]+(\d+)', output.lower())
68
+ if exit_match:
69
+ result["passed"] = exit_match.group(1) == "0"
70
+ else:
71
+ result["passed"] = True
72
+ result["details"] = "Compiles"
73
+
74
+ # Hook enforcement test
75
+ if "python3" in command_lower and ("enforce" in command_lower or "verify" in command_lower):
76
+ result["is_test"] = True
77
+ result["test_type"] = "hook_enforcement"
78
+ match = re.search(r'(?:hooks/|\.claude/hooks/)([^/\s]+\.py)', command)
79
+ if match:
80
+ result["hook_name"] = match.group(1)
81
+
82
+ # Check for blocking behavior
83
+ if '"permissionDecision": "deny"' in output or "BLOCKED" in output:
84
+ result["passed"] = True
85
+ result["details"] = "BLOCKS correctly"
86
+ elif '"permissionDecision": "allow"' in output:
87
+ result["passed"] = True
88
+ result["details"] = "ALLOWS correctly"
89
+ elif '"continue": true' in output:
90
+ result["passed"] = True
91
+ result["details"] = "Continues"
92
+
93
+ # pnpm test
94
+ if "pnpm test" in command_lower or "npm test" in command_lower:
95
+ result["is_test"] = True
96
+ result["test_type"] = "unit_test"
97
+ if "PASS" in output or "passed" in output.lower():
98
+ result["passed"] = True
99
+ elif "FAIL" in output or "failed" in output.lower():
100
+ result["passed"] = False
101
+
102
+ return result
103
+
104
+
105
+ def update_checklist(hook_name: str, status: str, comment: str):
106
+ """Update the TESTING_CHECKLIST.md file with test results"""
107
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
108
+ checklist_path = Path(project_dir) / "TESTING_CHECKLIST.md"
109
+
110
+ if not checklist_path.exists():
111
+ return False
112
+
113
+ try:
114
+ content = checklist_path.read_text()
115
+ today = datetime.now().strftime("%Y-%m-%d")
116
+
117
+ # Pattern to find hook row in table (with empty Status column)
118
+ # Format: | `hook_name` | Type | Phase/Trigger | | |
119
+ pattern = rf'(\| `{re.escape(hook_name)}` \|[^|]+\|[^|]+\|)\s*\|\s*\|'
120
+ replacement = rf'\1 {status} | {comment} ({today}) |'
121
+
122
+ new_content = re.sub(pattern, replacement, content)
123
+
124
+ if new_content != content:
125
+ checklist_path.write_text(new_content)
126
+ return True
127
+
128
+ # Try alternate pattern for already-filled rows (update existing)
129
+ pattern2 = rf'(\| `{re.escape(hook_name)}` \|[^|]+\|[^|]+\|)[^|]+\|[^|]+\|'
130
+ replacement2 = rf'\1 {status} | {comment} ({today}) |'
131
+
132
+ new_content = re.sub(pattern2, replacement2, content)
133
+ if new_content != content:
134
+ checklist_path.write_text(new_content)
135
+ return True
136
+
137
+ except Exception as e:
138
+ # Log error but don't fail
139
+ pass
140
+
141
+ return False
142
+
143
+
144
+ def main():
145
+ tool_input = get_tool_input()
146
+ command = tool_input.get("command", "")
147
+ output = get_tool_result()
148
+
149
+ # Detect what test was run
150
+ test_info = detect_test_type(command, output)
151
+
152
+ if not test_info["is_test"]:
153
+ print(json.dumps({"continue": True}))
154
+ return
155
+
156
+ # Update checklist if we have a hook name
157
+ if test_info["hook_name"] and test_info["passed"] is not None:
158
+ status = "PASS" if test_info["passed"] else "FAIL"
159
+ comment = test_info["details"] or ("Tested" if test_info["passed"] else "Failed")
160
+
161
+ updated = update_checklist(
162
+ test_info["hook_name"],
163
+ status,
164
+ comment
165
+ )
166
+
167
+ if updated:
168
+ # Log the update
169
+ project_dir = os.environ.get("CLAUDE_PROJECT_DIR", ".")
170
+ logs_dir = Path(project_dir) / ".claude" / "workflow-logs"
171
+ logs_dir.mkdir(parents=True, exist_ok=True)
172
+
173
+ log_file = logs_dir / "checklist-updates.json"
174
+ try:
175
+ if log_file.exists():
176
+ log = json.loads(log_file.read_text())
177
+ else:
178
+ log = {"updates": []}
179
+
180
+ log["updates"].append({
181
+ "timestamp": datetime.now().isoformat(),
182
+ "hook": test_info["hook_name"],
183
+ "status": status,
184
+ "comment": comment
185
+ })
186
+
187
+ log_file.write_text(json.dumps(log, indent=2))
188
+ except Exception:
189
+ pass
190
+
191
+ print(json.dumps({"continue": True}))
192
+
193
+
194
+ if __name__ == "__main__":
195
+ main()
@@ -84,12 +84,15 @@ def copy_showcase_templates(cwd):
84
84
  """Copy UI showcase templates to src/app/ui-showcase/."""
85
85
  # Source templates (installed by CLI)
86
86
  templates_dir = Path(__file__).parent.parent / "templates" / "ui-showcase"
87
+ shared_templates_dir = Path(__file__).parent.parent / "templates" / "shared"
87
88
 
88
89
  # Destination
89
90
  showcase_dir = cwd / "src" / "app" / "ui-showcase"
91
+ shared_dir = cwd / "src" / "app" / "shared"
90
92
 
91
- # Create directory if needed
93
+ # Create directories if needed
92
94
  showcase_dir.mkdir(parents=True, exist_ok=True)
95
+ shared_dir.mkdir(parents=True, exist_ok=True)
93
96
 
94
97
  # Copy template files
95
98
  templates_to_copy = [
@@ -111,6 +114,15 @@ def copy_showcase_templates(cwd):
111
114
  shutil.copy2(src_path, dest_path)
112
115
  created_files.append(str(dest_path.relative_to(cwd)))
113
116
 
117
+ # Also copy shared components (HeroHeader, etc.)
118
+ if shared_templates_dir.exists():
119
+ for src_file in shared_templates_dir.iterdir():
120
+ if src_file.is_file():
121
+ dest_path = shared_dir / src_file.name
122
+ if not dest_path.exists():
123
+ shutil.copy2(src_file, dest_path)
124
+ created_files.append(str(dest_path.relative_to(cwd)))
125
+
114
126
  return created_files
115
127
 
116
128
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hustle-together/api-dev-tools",
3
- "version": "3.12.3",
3
+ "version": "4.5.1",
4
4
  "description": "Interview-driven, research-first API development toolkit with 14-phase TDD workflow, enforcement hooks, and 23 Agent Skills for cross-platform AI agents",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {
@@ -23,11 +23,15 @@
23
23
  "test": "node bin/cli.js --scope=project",
24
24
  "usage": "ccusage",
25
25
  "format": "prettier --write .",
26
- "lint": "eslint . --fix"
26
+ "lint": "eslint . --fix",
27
+ "typedoc": "typedoc",
28
+ "typedoc:watch": "typedoc --watch"
27
29
  },
28
30
  "devDependencies": {
29
31
  "prettier": "^3.0.0",
30
- "eslint": "^8.0.0"
32
+ "eslint": "^8.0.0",
33
+ "typedoc": "^0.27.0",
34
+ "typedoc-plugin-markdown": "^4.4.0"
31
35
  },
32
36
  "optionalDependencies": {
33
37
  "ccusage": "^1.0.0"