@hustle-together/api-dev-tools 3.6.5 → 3.9.2

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 (61) hide show
  1. package/README.md +5307 -258
  2. package/bin/cli.js +348 -20
  3. package/commands/README.md +459 -71
  4. package/commands/hustle-api-continue.md +158 -0
  5. package/commands/{api-create.md → hustle-api-create.md} +22 -2
  6. package/commands/{api-env.md → hustle-api-env.md} +4 -4
  7. package/commands/{api-interview.md → hustle-api-interview.md} +1 -1
  8. package/commands/{api-research.md → hustle-api-research.md} +3 -3
  9. package/commands/hustle-api-sessions.md +149 -0
  10. package/commands/{api-status.md → hustle-api-status.md} +16 -16
  11. package/commands/{api-verify.md → hustle-api-verify.md} +2 -2
  12. package/commands/hustle-combine.md +763 -0
  13. package/commands/hustle-ui-create.md +825 -0
  14. package/hooks/api-workflow-check.py +385 -19
  15. package/hooks/cache-research.py +337 -0
  16. package/hooks/check-playwright-setup.py +103 -0
  17. package/hooks/check-storybook-setup.py +81 -0
  18. package/hooks/detect-interruption.py +165 -0
  19. package/hooks/enforce-brand-guide.py +131 -0
  20. package/hooks/enforce-documentation.py +60 -8
  21. package/hooks/enforce-freshness.py +184 -0
  22. package/hooks/enforce-questions-sourced.py +146 -0
  23. package/hooks/enforce-schema-from-interview.py +248 -0
  24. package/hooks/enforce-ui-disambiguation.py +108 -0
  25. package/hooks/enforce-ui-interview.py +130 -0
  26. package/hooks/generate-manifest-entry.py +981 -0
  27. package/hooks/session-logger.py +297 -0
  28. package/hooks/session-startup.py +65 -10
  29. package/hooks/track-scope-coverage.py +220 -0
  30. package/hooks/track-tool-use.py +81 -1
  31. package/hooks/update-api-showcase.py +149 -0
  32. package/hooks/update-registry.py +352 -0
  33. package/hooks/update-ui-showcase.py +148 -0
  34. package/package.json +8 -2
  35. package/templates/BRAND_GUIDE.md +299 -0
  36. package/templates/CLAUDE-SECTION.md +56 -24
  37. package/templates/SPEC.json +640 -0
  38. package/templates/api-dev-state.json +179 -161
  39. package/templates/api-showcase/APICard.tsx +153 -0
  40. package/templates/api-showcase/APIModal.tsx +375 -0
  41. package/templates/api-showcase/APIShowcase.tsx +231 -0
  42. package/templates/api-showcase/APITester.tsx +522 -0
  43. package/templates/api-showcase/page.tsx +41 -0
  44. package/templates/component/Component.stories.tsx +172 -0
  45. package/templates/component/Component.test.tsx +237 -0
  46. package/templates/component/Component.tsx +86 -0
  47. package/templates/component/Component.types.ts +55 -0
  48. package/templates/component/index.ts +15 -0
  49. package/templates/dev-tools/_components/DevToolsLanding.tsx +320 -0
  50. package/templates/dev-tools/page.tsx +10 -0
  51. package/templates/page/page.e2e.test.ts +218 -0
  52. package/templates/page/page.tsx +42 -0
  53. package/templates/performance-budgets.json +58 -0
  54. package/templates/registry.json +13 -0
  55. package/templates/settings.json +74 -0
  56. package/templates/shared/HeroHeader.tsx +261 -0
  57. package/templates/shared/index.ts +1 -0
  58. package/templates/ui-showcase/PreviewCard.tsx +315 -0
  59. package/templates/ui-showcase/PreviewModal.tsx +676 -0
  60. package/templates/ui-showcase/UIShowcase.tsx +262 -0
  61. package/templates/ui-showcase/page.tsx +26 -0
@@ -11,6 +11,12 @@ Gap Fixes Applied:
11
11
  - Gap 3: Warns if there are verification_warnings that weren't addressed
12
12
  - Gap 4: Requires explicit verification that implementation matches interview
13
13
 
14
+ v3.6.7 Enhancement:
15
+ - Phase 13 completion output with curl examples, test commands, parameter tables
16
+ - Scope coverage report (discovered vs implemented vs deferred)
17
+ - Research cache location
18
+ - Summary statistics
19
+
14
20
  Returns:
15
21
  - {"decision": "approve"} - Allow stopping
16
22
  - {"decision": "block", "reason": "..."} - Prevent stopping with explanation
@@ -18,10 +24,13 @@ Returns:
18
24
  import json
19
25
  import sys
20
26
  import subprocess
27
+ import re
28
+ from datetime import datetime
21
29
  from pathlib import Path
22
30
 
23
31
  # State file is in .claude/ directory (sibling to hooks/)
24
32
  STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
33
+ RESEARCH_DIR = Path(__file__).parent.parent / "research"
25
34
 
26
35
  # Phases that MUST be complete before stopping
27
36
  REQUIRED_PHASES = [
@@ -41,6 +50,22 @@ RECOMMENDED_PHASES = [
41
50
  ]
42
51
 
43
52
 
53
+ def get_active_endpoint(state):
54
+ """Get active endpoint - supports both old and new state formats."""
55
+ if "endpoints" in state and "active_endpoint" in state:
56
+ active = state.get("active_endpoint")
57
+ if active and active in state["endpoints"]:
58
+ return active, state["endpoints"][active]
59
+ return None, None
60
+
61
+ # Old format: single endpoint
62
+ endpoint = state.get("endpoint")
63
+ if endpoint:
64
+ return endpoint, state
65
+
66
+ return None, None
67
+
68
+
44
69
  def get_git_modified_files() -> list[str]:
45
70
  """Get list of modified files from git.
46
71
 
@@ -76,21 +101,23 @@ def check_verification_warnings(state: dict) -> list[str]:
76
101
  return []
77
102
 
78
103
 
79
- def check_interview_implementation_match(state: dict) -> list[str]:
104
+ def check_interview_implementation_match(state: dict, endpoint_data: dict = None) -> list[str]:
80
105
  """Verify implementation matches interview requirements.
81
106
 
82
107
  Gap 4 Fix: Define specific "done" criteria based on interview.
83
108
  """
84
109
  issues = []
85
110
 
86
- interview = state.get("phases", {}).get("interview", {})
111
+ # Use endpoint_data if provided (multi-API), otherwise use state directly
112
+ data = endpoint_data if endpoint_data else state
113
+ interview = data.get("phases", {}).get("interview", {})
87
114
  questions = interview.get("questions", [])
88
115
 
89
116
  # Extract key requirements from interview
90
117
  all_text = " ".join(str(q) for q in questions)
91
118
 
92
119
  # Check files_created includes expected patterns
93
- files_created = state.get("files_created", [])
120
+ files_created = data.get("files_created", []) or state.get("files_created", [])
94
121
 
95
122
  # Look for route files if interview mentioned endpoints
96
123
  if "endpoint" in all_text.lower() or "/api/" in all_text.lower():
@@ -106,6 +133,324 @@ def check_interview_implementation_match(state: dict) -> list[str]:
106
133
  return issues
107
134
 
108
135
 
136
+ def extract_schema_params(endpoint: str, endpoint_data: dict) -> list[dict]:
137
+ """Extract parameters from schema file for the parameter table."""
138
+ schema_file = endpoint_data.get("phases", {}).get("schema_creation", {}).get("schema_file")
139
+ if not schema_file:
140
+ return []
141
+
142
+ # Try to read the schema file
143
+ try:
144
+ schema_path = STATE_FILE.parent.parent / schema_file
145
+ if not schema_path.exists():
146
+ return []
147
+
148
+ content = schema_path.read_text()
149
+
150
+ # Simple regex to extract Zod field definitions
151
+ # Matches patterns like: fieldName: z.string(), fieldName: z.number().optional()
152
+ params = []
153
+ field_pattern = r'(\w+):\s*z\.(\w+)\(([^)]*)\)(\.[^,\n}]+)?'
154
+
155
+ for match in re.finditer(field_pattern, content):
156
+ name = match.group(1)
157
+ zod_type = match.group(2)
158
+ chain = match.group(4) or ""
159
+
160
+ # Map Zod types to simple types
161
+ type_map = {
162
+ "string": "string",
163
+ "number": "number",
164
+ "boolean": "boolean",
165
+ "array": "array",
166
+ "object": "object",
167
+ "enum": "enum",
168
+ "literal": "literal",
169
+ "union": "union",
170
+ }
171
+
172
+ param_type = type_map.get(zod_type, zod_type)
173
+ required = ".optional()" not in chain
174
+ description = ""
175
+
176
+ # Try to extract description from .describe()
177
+ desc_match = re.search(r'\.describe\(["\']([^"\']+)["\']', chain)
178
+ if desc_match:
179
+ description = desc_match.group(1)
180
+
181
+ params.append({
182
+ "name": name,
183
+ "type": param_type,
184
+ "required": required,
185
+ "description": description
186
+ })
187
+
188
+ return params
189
+ except Exception:
190
+ return []
191
+
192
+
193
+ def generate_curl_examples(endpoint: str, endpoint_data: dict, params: list) -> list[str]:
194
+ """Generate curl command examples for the endpoint."""
195
+ lines = []
196
+
197
+ # Determine HTTP method from route file
198
+ method = "POST" # Default
199
+ files_created = endpoint_data.get("files_created", [])
200
+ for f in files_created:
201
+ if "route.ts" in f:
202
+ try:
203
+ route_path = STATE_FILE.parent.parent / f
204
+ if route_path.exists():
205
+ route_content = route_path.read_text()
206
+ if "export async function GET" in route_content:
207
+ method = "GET"
208
+ elif "export async function DELETE" in route_content:
209
+ method = "DELETE"
210
+ elif "export async function PUT" in route_content:
211
+ method = "PUT"
212
+ elif "export async function PATCH" in route_content:
213
+ method = "PATCH"
214
+ except Exception:
215
+ pass
216
+ break
217
+
218
+ lines.append("## API Usage (curl)")
219
+ lines.append("")
220
+ lines.append("```bash")
221
+ lines.append("# Basic request")
222
+
223
+ # Build example request body from params
224
+ if method in ["POST", "PUT", "PATCH"] and params:
225
+ example_body = {}
226
+ for p in params[:5]: # First 5 params
227
+ if p["type"] == "string":
228
+ example_body[p["name"]] = f"example-{p['name']}"
229
+ elif p["type"] == "number":
230
+ example_body[p["name"]] = 42
231
+ elif p["type"] == "boolean":
232
+ example_body[p["name"]] = True
233
+ elif p["type"] == "array":
234
+ example_body[p["name"]] = []
235
+
236
+ body_json = json.dumps(example_body, indent=2)
237
+ lines.append(f"curl -X {method} http://localhost:3001/api/v2/{endpoint} \\")
238
+ lines.append(" -H \"Content-Type: application/json\" \\")
239
+ lines.append(f" -d '{body_json}'")
240
+ else:
241
+ lines.append(f"curl http://localhost:3001/api/v2/{endpoint}")
242
+
243
+ lines.append("")
244
+
245
+ # With authentication example
246
+ lines.append("# With API key (if required)")
247
+ if method in ["POST", "PUT", "PATCH"]:
248
+ lines.append(f"curl -X {method} http://localhost:3001/api/v2/{endpoint} \\")
249
+ lines.append(" -H \"Content-Type: application/json\" \\")
250
+ lines.append(" -H \"X-API-Key: your-api-key\" \\")
251
+ lines.append(" -d '{\"param\": \"value\"}'")
252
+ else:
253
+ lines.append(f"curl http://localhost:3001/api/v2/{endpoint} \\")
254
+ lines.append(" -H \"X-API-Key: your-api-key\"")
255
+
256
+ lines.append("```")
257
+
258
+ return lines
259
+
260
+
261
+ def generate_test_commands(endpoint: str, endpoint_data: dict) -> list[str]:
262
+ """Generate test commands for running endpoint tests."""
263
+ lines = []
264
+
265
+ lines.append("## Test Commands")
266
+ lines.append("")
267
+ lines.append("```bash")
268
+ lines.append("# Run endpoint tests")
269
+ lines.append(f"pnpm test -- {endpoint}")
270
+ lines.append("")
271
+ lines.append("# Run with coverage")
272
+ lines.append(f"pnpm test:coverage -- {endpoint}")
273
+ lines.append("")
274
+ lines.append("# Run specific test file")
275
+
276
+ # Find test file
277
+ files_created = endpoint_data.get("files_created", [])
278
+ test_file = None
279
+ for f in files_created:
280
+ if ".test." in f or "__tests__" in f:
281
+ test_file = f
282
+ break
283
+
284
+ if test_file:
285
+ lines.append(f"pnpm test:run {test_file}")
286
+ else:
287
+ lines.append(f"pnpm test:run src/app/api/v2/{endpoint}/__tests__/{endpoint}.api.test.ts")
288
+
289
+ lines.append("")
290
+ lines.append("# Full test suite")
291
+ lines.append("pnpm test:run")
292
+ lines.append("```")
293
+
294
+ return lines
295
+
296
+
297
+ def generate_parameter_table(params: list) -> list[str]:
298
+ """Generate markdown parameter table."""
299
+ if not params:
300
+ return []
301
+
302
+ lines = []
303
+ lines.append("## Parameters Discovered")
304
+ lines.append("")
305
+ lines.append("| Name | Type | Required | Description |")
306
+ lines.append("|------|------|----------|-------------|")
307
+
308
+ for p in params:
309
+ req = "✓" if p.get("required") else "-"
310
+ desc = p.get("description", "")[:50] # Truncate long descriptions
311
+ lines.append(f"| {p['name']} | {p['type']} | {req} | {desc} |")
312
+
313
+ return lines
314
+
315
+
316
+ def generate_scope_coverage(endpoint_data: dict) -> list[str]:
317
+ """Generate scope coverage report."""
318
+ scope = endpoint_data.get("scope", {})
319
+ if not scope:
320
+ return []
321
+
322
+ discovered = scope.get("discovered_features", [])
323
+ implemented = scope.get("implemented_features", [])
324
+ deferred = scope.get("deferred_features", [])
325
+ coverage = scope.get("coverage_percent", 0)
326
+
327
+ if not discovered and not implemented and not deferred:
328
+ return []
329
+
330
+ lines = []
331
+ lines.append("## Implementation Scope")
332
+ lines.append("")
333
+
334
+ if implemented:
335
+ lines.append(f"### Implemented ({len(implemented)}/{len(discovered)} features)")
336
+ lines.append("")
337
+ lines.append("| Feature | Status |")
338
+ lines.append("|---------|--------|")
339
+ for feat in implemented:
340
+ if isinstance(feat, dict):
341
+ lines.append(f"| {feat.get('name', feat)} | ✅ |")
342
+ else:
343
+ lines.append(f"| {feat} | ✅ |")
344
+ lines.append("")
345
+
346
+ if deferred:
347
+ lines.append(f"### Deferred ({len(deferred)} features)")
348
+ lines.append("")
349
+ lines.append("| Feature | Reason |")
350
+ lines.append("|---------|--------|")
351
+ for feat in deferred:
352
+ if isinstance(feat, dict):
353
+ reason = feat.get("reason", "User choice")
354
+ lines.append(f"| {feat.get('name', feat)} | {reason} |")
355
+ else:
356
+ lines.append(f"| {feat} | User choice |")
357
+ lines.append("")
358
+
359
+ if discovered:
360
+ total = len(discovered)
361
+ impl_count = len(implemented)
362
+ lines.append(f"**Coverage:** {impl_count}/{total} features ({coverage}%)")
363
+
364
+ return lines
365
+
366
+
367
+ def generate_completion_output(endpoint: str, endpoint_data: dict, state: dict) -> str:
368
+ """Generate comprehensive Phase 13 completion output."""
369
+ lines = []
370
+
371
+ # Header
372
+ lines.append("")
373
+ lines.append("=" * 60)
374
+ lines.append(f"# ✅ API Implementation Complete: {endpoint}")
375
+ lines.append("=" * 60)
376
+ lines.append("")
377
+
378
+ # Summary
379
+ phases = endpoint_data.get("phases", {})
380
+ phases_complete = sum(1 for p in phases.values() if isinstance(p, dict) and p.get("status") == "complete")
381
+ total_phases = len([p for p in phases.values() if isinstance(p, dict)])
382
+
383
+ started_at = endpoint_data.get("started_at", "Unknown")
384
+ files_created = endpoint_data.get("files_created", []) or state.get("files_created", [])
385
+
386
+ # Calculate test count from state
387
+ tdd_red = phases.get("tdd_red", {})
388
+ test_count = tdd_red.get("test_count", 0)
389
+
390
+ lines.append("## Summary")
391
+ lines.append("")
392
+ lines.append(f"- **Status:** PRODUCTION READY")
393
+ lines.append(f"- **Phases:** {phases_complete}/{total_phases} Complete")
394
+ lines.append(f"- **Tests:** {test_count} test scenarios")
395
+ lines.append(f"- **Started:** {started_at}")
396
+ lines.append(f"- **Completed:** {datetime.now().isoformat()}")
397
+ lines.append("")
398
+
399
+ # Files Created
400
+ if files_created:
401
+ lines.append("## Files Created")
402
+ lines.append("")
403
+ for f in files_created:
404
+ lines.append(f"- {f}")
405
+ lines.append("")
406
+
407
+ # Extract schema params
408
+ params = extract_schema_params(endpoint, endpoint_data)
409
+
410
+ # Test Commands
411
+ lines.extend(generate_test_commands(endpoint, endpoint_data))
412
+ lines.append("")
413
+
414
+ # Curl Examples
415
+ lines.extend(generate_curl_examples(endpoint, endpoint_data, params))
416
+ lines.append("")
417
+
418
+ # Parameter Table
419
+ param_lines = generate_parameter_table(params)
420
+ if param_lines:
421
+ lines.extend(param_lines)
422
+ lines.append("")
423
+
424
+ # Scope Coverage
425
+ scope_lines = generate_scope_coverage(endpoint_data)
426
+ if scope_lines:
427
+ lines.extend(scope_lines)
428
+ lines.append("")
429
+
430
+ # Research Cache Location
431
+ research_cache = RESEARCH_DIR / endpoint
432
+ if research_cache.exists():
433
+ lines.append("## Research Cache")
434
+ lines.append("")
435
+ lines.append(f"- `.claude/research/{endpoint}/CURRENT.md`")
436
+ lines.append(f"- `.claude/research/{endpoint}/sources.json`")
437
+ lines.append(f"- `.claude/research/{endpoint}/interview.json`")
438
+ lines.append("")
439
+
440
+ # Next Steps
441
+ lines.append("## Next Steps")
442
+ lines.append("")
443
+ lines.append(f"1. Review tests: `pnpm test -- {endpoint}`")
444
+ lines.append("2. Test manually with curl examples above")
445
+ lines.append("3. Deploy to staging")
446
+ lines.append("4. Update OpenAPI spec if needed")
447
+ lines.append("")
448
+
449
+ lines.append("=" * 60)
450
+
451
+ return "\n".join(lines)
452
+
453
+
109
454
  def main():
110
455
  # If no state file, we're not in an API workflow - allow stop
111
456
  if not STATE_FILE.exists():
@@ -120,7 +465,14 @@ def main():
120
465
  print(json.dumps({"decision": "approve"}))
121
466
  sys.exit(0)
122
467
 
123
- phases = state.get("phases", {})
468
+ # Get active endpoint (multi-API support)
469
+ endpoint, endpoint_data = get_active_endpoint(state)
470
+
471
+ # If no active endpoint, check if using old format
472
+ if not endpoint_data:
473
+ phases = state.get("phases", {})
474
+ else:
475
+ phases = endpoint_data.get("phases", {})
124
476
 
125
477
  # Check if workflow was even started
126
478
  research = phases.get("research_initial", {})
@@ -154,7 +506,8 @@ def main():
154
506
 
155
507
  # Gap 2: Check git diff vs tracked files
156
508
  git_files = get_git_modified_files()
157
- tracked_files = state.get("files_created", []) + state.get("files_modified", [])
509
+ data_for_files = endpoint_data if endpoint_data else state
510
+ tracked_files = (data_for_files.get("files_created", []) or []) + (data_for_files.get("files_modified", []) or [])
158
511
 
159
512
  if git_files and tracked_files:
160
513
  # Find files in git but not tracked
@@ -169,12 +522,12 @@ def main():
169
522
  all_issues.extend([f" - {f}" for f in untracked_changes[:5]])
170
523
 
171
524
  # Gap 3: Check for unaddressed warnings
172
- warning_issues = check_verification_warnings(state)
525
+ warning_issues = check_verification_warnings(endpoint_data if endpoint_data else state)
173
526
  if warning_issues:
174
527
  all_issues.append("\n" + "\n".join(warning_issues))
175
528
 
176
529
  # Gap 4: Check interview-implementation match
177
- match_issues = check_interview_implementation_match(state)
530
+ match_issues = check_interview_implementation_match(state, endpoint_data)
178
531
  if match_issues:
179
532
  all_issues.append("\n⚠️ Gap 4: Implementation verification:")
180
533
  all_issues.extend([f" {i}" for i in match_issues])
@@ -192,22 +545,35 @@ def main():
192
545
  }))
193
546
  sys.exit(0)
194
547
 
195
- # Build completion message
196
- message_parts = ["✅ API workflow completing"]
197
-
548
+ # ================================================================
549
+ # Phase 13: Generate comprehensive completion output (v3.6.7)
550
+ # ================================================================
551
+
552
+ # Build completion message with full output
553
+ message_parts = []
554
+
555
+ # Generate comprehensive output if we have endpoint data
556
+ if endpoint and endpoint_data:
557
+ completion_output = generate_completion_output(endpoint, endpoint_data, state)
558
+ message_parts.append(completion_output)
559
+ else:
560
+ # Fallback for old format
561
+ message_parts.append("✅ API workflow completing")
562
+
563
+ # Show summary of tracked files
564
+ files_created = state.get("files_created", [])
565
+ if files_created:
566
+ message_parts.append(f"\n📁 Files created: {len(files_created)}")
567
+ for f in files_created[:5]:
568
+ message_parts.append(f" - {f}")
569
+ if len(files_created) > 5:
570
+ message_parts.append(f" ... and {len(files_created) - 5} more")
571
+
572
+ # Add warnings if any optional phases were skipped
198
573
  if incomplete_recommended:
199
574
  message_parts.append("\n⚠️ Optional phases skipped:")
200
575
  message_parts.extend(incomplete_recommended)
201
576
 
202
- # Show summary of tracked files
203
- files_created = state.get("files_created", [])
204
- if files_created:
205
- message_parts.append(f"\n📁 Files created: {len(files_created)}")
206
- for f in files_created[:5]:
207
- message_parts.append(f" - {f}")
208
- if len(files_created) > 5:
209
- message_parts.append(f" ... and {len(files_created) - 5} more")
210
-
211
577
  # Show any remaining warnings
212
578
  if warning_issues or match_issues:
213
579
  message_parts.append("\n⚠️ Review suggested:")