@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.
Files changed (96) hide show
  1. package/.claude/commands/hustle-build.md +259 -0
  2. package/.claude/commands/hustle-combine.md +1089 -0
  3. package/.claude/commands/hustle-ui-create-page.md +1078 -0
  4. package/.claude/commands/hustle-ui-create.md +1058 -0
  5. package/.claude/hooks/auto-answer.py +305 -0
  6. package/.claude/hooks/cache-research.py +337 -0
  7. package/.claude/hooks/check-api-routes.py +168 -0
  8. package/.claude/hooks/check-playwright-setup.py +103 -0
  9. package/.claude/hooks/check-storybook-setup.py +81 -0
  10. package/.claude/hooks/check-update.py +132 -0
  11. package/.claude/hooks/completion-promise-detector.py +293 -0
  12. package/.claude/hooks/context-capacity-warning.py +171 -0
  13. package/.claude/hooks/detect-interruption.py +165 -0
  14. package/.claude/hooks/docs-update-check.py +120 -0
  15. package/.claude/hooks/enforce-a11y-audit.py +202 -0
  16. package/.claude/hooks/enforce-brand-guide.py +241 -0
  17. package/.claude/hooks/enforce-component-type-confirm.py +97 -0
  18. package/.claude/hooks/enforce-dry-run.py +134 -0
  19. package/.claude/hooks/enforce-freshness.py +184 -0
  20. package/.claude/hooks/enforce-page-components.py +186 -0
  21. package/.claude/hooks/enforce-page-data-schema.py +155 -0
  22. package/.claude/hooks/enforce-questions-sourced.py +146 -0
  23. package/.claude/hooks/enforce-schema-from-interview.py +248 -0
  24. package/.claude/hooks/enforce-ui-disambiguation.py +108 -0
  25. package/.claude/hooks/enforce-ui-interview.py +130 -0
  26. package/.claude/hooks/generate-adr-options.py +282 -0
  27. package/.claude/hooks/generate-manifest-entry.py +1161 -0
  28. package/.claude/hooks/hook_utils.py +609 -0
  29. package/.claude/hooks/lib/__init__.py +1 -0
  30. package/.claude/hooks/lib/__pycache__/__init__.cpython-314.pyc +0 -0
  31. package/.claude/hooks/lib/__pycache__/greptile.cpython-314.pyc +0 -0
  32. package/.claude/hooks/lib/__pycache__/ntfy.cpython-314.pyc +0 -0
  33. package/.claude/hooks/lib/greptile.py +355 -0
  34. package/.claude/hooks/lib/ntfy.py +209 -0
  35. package/.claude/hooks/notify-input-needed.py +73 -0
  36. package/.claude/hooks/notify-phase-complete.py +90 -0
  37. package/.claude/hooks/ntfy-on-question.py +240 -0
  38. package/.claude/hooks/orchestrator-completion.py +313 -0
  39. package/.claude/hooks/orchestrator-handoff.py +267 -0
  40. package/.claude/hooks/orchestrator-session-startup.py +146 -0
  41. package/.claude/hooks/parallel-orchestrator.py +451 -0
  42. package/.claude/hooks/project-document-prompt.py +302 -0
  43. package/.claude/hooks/remote-question-proxy.py +284 -0
  44. package/.claude/hooks/remote-question-server.py +1224 -0
  45. package/.claude/hooks/run-code-review.py +393 -0
  46. package/.claude/hooks/run-visual-qa.py +338 -0
  47. package/.claude/hooks/session-logger.py +323 -0
  48. package/.claude/hooks/test-orchestrator-reground.py +248 -0
  49. package/.claude/hooks/track-scope-coverage.py +220 -0
  50. package/.claude/hooks/track-token-usage.py +121 -0
  51. package/.claude/hooks/update-adr-decision.py +236 -0
  52. package/.claude/hooks/update-api-showcase.py +161 -0
  53. package/.claude/hooks/update-registry.py +352 -0
  54. package/.claude/hooks/update-testing-checklist.py +195 -0
  55. package/.claude/hooks/update-ui-showcase.py +224 -0
  56. package/.claude/settings.local.json +7 -1
  57. package/.claude/test-auto-answer-bot.py +183 -0
  58. package/.claude/test-completion-detector.py +263 -0
  59. package/.claude/test-orchestrator-state.json +20 -0
  60. package/.claude/test-orchestrator.sh +271 -0
  61. package/.skills/api-create/SKILL.md +88 -3
  62. package/.skills/docs-sync/SKILL.md +260 -0
  63. package/.skills/hustle-build/SKILL.md +459 -0
  64. package/.skills/hustle-build-review/SKILL.md +518 -0
  65. package/CHANGELOG.md +87 -0
  66. package/README.md +86 -9
  67. package/bin/cli.js +1302 -88
  68. package/commands/hustle-api-create.md +22 -0
  69. package/commands/hustle-combine.md +81 -2
  70. package/commands/hustle-ui-create-page.md +84 -2
  71. package/commands/hustle-ui-create.md +82 -2
  72. package/hooks/auto-answer.py +228 -0
  73. package/hooks/check-update.py +132 -0
  74. package/hooks/ntfy-on-question.py +227 -0
  75. package/hooks/orchestrator-completion.py +313 -0
  76. package/hooks/orchestrator-handoff.py +189 -0
  77. package/hooks/orchestrator-session-startup.py +146 -0
  78. package/hooks/periodic-reground.py +230 -67
  79. package/hooks/update-api-showcase.py +13 -1
  80. package/hooks/update-ui-showcase.py +13 -1
  81. package/package.json +7 -3
  82. package/scripts/extract-schema-docs.cjs +322 -0
  83. package/templates/CLAUDE-SECTION.md +89 -64
  84. package/templates/api-showcase/_components/APIModal.tsx +100 -8
  85. package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
  86. package/templates/api-showcase/_components/APITester.tsx +367 -58
  87. package/templates/docs/page.tsx +230 -0
  88. package/templates/hustle-build-defaults.json +84 -0
  89. package/templates/hustle-dev-dashboard/page.tsx +365 -0
  90. package/templates/playwright-report/page.tsx +258 -0
  91. package/templates/settings.json +88 -7
  92. package/templates/test-results/page.tsx +237 -0
  93. package/templates/typedoc.json +19 -0
  94. package/templates/ui-showcase/_components/UIShowcase.tsx +1 -1
  95. package/templates/ui-showcase/page.tsx +1 -1
  96. 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()