@hustle-together/api-dev-tools 2.0.7 → 3.1.0

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 (55) hide show
  1. package/README.md +343 -467
  2. package/bin/cli.js +229 -15
  3. package/commands/README.md +124 -251
  4. package/commands/api-create.md +318 -136
  5. package/commands/api-interview.md +252 -256
  6. package/commands/api-research.md +209 -234
  7. package/commands/api-verify.md +231 -0
  8. package/demo/audio/generate-all-narrations.js +581 -0
  9. package/demo/audio/generate-narration.js +120 -56
  10. package/demo/audio/generate-voice-previews.js +140 -0
  11. package/demo/audio/narration-adam-timing.json +4675 -0
  12. package/demo/audio/narration-adam.mp3 +0 -0
  13. package/demo/audio/narration-creature-timing.json +4675 -0
  14. package/demo/audio/narration-creature.mp3 +0 -0
  15. package/demo/audio/narration-gaming-timing.json +4675 -0
  16. package/demo/audio/narration-gaming.mp3 +0 -0
  17. package/demo/audio/narration-hope-timing.json +4675 -0
  18. package/demo/audio/narration-hope.mp3 +0 -0
  19. package/demo/audio/narration-mark-timing.json +4675 -0
  20. package/demo/audio/narration-mark.mp3 +0 -0
  21. package/demo/audio/previews/manifest.json +30 -0
  22. package/demo/audio/previews/preview-creature.mp3 +0 -0
  23. package/demo/audio/previews/preview-gaming.mp3 +0 -0
  24. package/demo/audio/previews/preview-hope.mp3 +0 -0
  25. package/demo/audio/previews/preview-mark.mp3 +0 -0
  26. package/demo/audio/voices-manifest.json +50 -0
  27. package/demo/hustle-together/blog/gemini-vs-claude-widgets.html +30 -28
  28. package/demo/hustle-together/blog/interview-driven-api-development.html +37 -23
  29. package/demo/hustle-together/index.html +142 -109
  30. package/demo/workflow-demo.html +2618 -1036
  31. package/hooks/api-workflow-check.py +2 -0
  32. package/hooks/enforce-deep-research.py +180 -0
  33. package/hooks/enforce-disambiguation.py +149 -0
  34. package/hooks/enforce-documentation.py +187 -0
  35. package/hooks/enforce-environment.py +249 -0
  36. package/hooks/enforce-refactor.py +187 -0
  37. package/hooks/enforce-research.py +93 -46
  38. package/hooks/enforce-schema.py +186 -0
  39. package/hooks/enforce-scope.py +156 -0
  40. package/hooks/enforce-tdd-red.py +246 -0
  41. package/hooks/enforce-verify.py +186 -0
  42. package/hooks/periodic-reground.py +154 -0
  43. package/hooks/session-startup.py +151 -0
  44. package/hooks/track-tool-use.py +109 -17
  45. package/hooks/verify-after-green.py +282 -0
  46. package/package.json +3 -2
  47. package/scripts/collect-test-results.ts +404 -0
  48. package/scripts/extract-parameters.ts +483 -0
  49. package/scripts/generate-test-manifest.ts +520 -0
  50. package/templates/CLAUDE-SECTION.md +84 -0
  51. package/templates/api-dev-state.json +83 -8
  52. package/templates/api-test/page.tsx +315 -0
  53. package/templates/api-test/test-structure/route.ts +269 -0
  54. package/templates/research-index.json +6 -0
  55. package/templates/settings.json +59 -0
@@ -1,52 +1,107 @@
1
1
  {
2
- "version": "1.1.0",
2
+ "version": "3.0.0",
3
3
  "created_at": null,
4
4
  "endpoint": null,
5
5
  "library": null,
6
+ "session_id": null,
7
+ "turn_count": 0,
8
+ "last_turn_timestamp": null,
6
9
  "research_queries": [],
7
10
  "prompt_detections": [],
8
11
  "phases": {
12
+ "disambiguation": {
13
+ "status": "not_started",
14
+ "clarified": null,
15
+ "search_variations": [],
16
+ "user_question_asked": false,
17
+ "user_selected": null,
18
+ "description": "Pre-research disambiguation to clarify ambiguous requests"
19
+ },
9
20
  "scope": {
10
21
  "status": "not_started",
11
- "description": "Initial scope understanding"
22
+ "confirmed": false,
23
+ "user_question_asked": false,
24
+ "user_confirmed": false,
25
+ "description": "Initial scope understanding and confirmation"
12
26
  },
13
27
  "research_initial": {
14
28
  "status": "not_started",
15
29
  "sources": [],
30
+ "summary_shown": false,
31
+ "user_question_asked": false,
32
+ "user_approved": false,
16
33
  "description": "Context7/WebSearch research for live documentation"
17
34
  },
18
35
  "interview": {
19
36
  "status": "not_started",
20
37
  "questions": [],
21
- "description": "Structured interview about requirements"
38
+ "user_question_count": 0,
39
+ "structured_question_count": 0,
40
+ "decisions": {},
41
+ "user_question_asked": false,
42
+ "user_completed": false,
43
+ "description": "Structured interview about requirements (generated FROM research)"
22
44
  },
23
45
  "research_deep": {
24
46
  "status": "not_started",
25
47
  "sources": [],
26
- "description": "Deep dive based on interview answers"
48
+ "proposed_searches": [],
49
+ "approved_searches": [],
50
+ "executed_searches": [],
51
+ "skipped_searches": [],
52
+ "proposals_shown": false,
53
+ "user_question_asked": false,
54
+ "user_approved": false,
55
+ "description": "Deep dive based on interview answers (adaptive, not shotgun)"
27
56
  },
28
57
  "schema_creation": {
29
58
  "status": "not_started",
30
59
  "schema_file": null,
60
+ "fields_count": 0,
61
+ "schema_shown": false,
62
+ "user_question_asked": false,
63
+ "user_confirmed": false,
31
64
  "description": "Zod schema creation from research"
32
65
  },
33
66
  "environment_check": {
34
67
  "status": "not_started",
35
- "keys_verified": [],
68
+ "keys_required": [],
69
+ "keys_found": [],
36
70
  "keys_missing": [],
71
+ "env_shown": false,
72
+ "user_question_asked": false,
73
+ "user_ready": false,
37
74
  "description": "API key and environment verification"
38
75
  },
39
76
  "tdd_red": {
40
77
  "status": "not_started",
41
78
  "test_file": null,
42
79
  "test_count": 0,
80
+ "test_scenarios": [],
81
+ "matrix_shown": false,
82
+ "user_question_asked": false,
83
+ "user_approved": false,
43
84
  "description": "Write failing tests first"
44
85
  },
45
86
  "tdd_green": {
46
87
  "status": "not_started",
47
88
  "implementation_file": null,
89
+ "all_tests_passing": false,
48
90
  "description": "Minimal implementation to pass tests"
49
91
  },
92
+ "verify": {
93
+ "status": "not_started",
94
+ "gaps_found": 0,
95
+ "gaps_fixed": 0,
96
+ "gaps_skipped": 0,
97
+ "intentional_omissions": [],
98
+ "re_research_done": false,
99
+ "gap_analysis_shown": false,
100
+ "user_question_asked": false,
101
+ "user_decided": false,
102
+ "user_decision": null,
103
+ "description": "Re-research after Green to verify implementation matches docs"
104
+ },
50
105
  "tdd_refactor": {
51
106
  "status": "not_started",
52
107
  "description": "Code cleanup while keeping tests green"
@@ -54,7 +109,24 @@
54
109
  "documentation": {
55
110
  "status": "not_started",
56
111
  "files_updated": [],
57
- "description": "Update manifests, OpenAPI, examples"
112
+ "manifest_updated": false,
113
+ "openapi_updated": false,
114
+ "research_cached": false,
115
+ "checklist_shown": false,
116
+ "user_question_asked": false,
117
+ "user_confirmed": false,
118
+ "description": "Update manifests, OpenAPI, cache research"
119
+ }
120
+ },
121
+ "manifest_generation": {
122
+ "last_run": null,
123
+ "manifest_generated": false,
124
+ "parameters_extracted": false,
125
+ "test_results_collected": false,
126
+ "output_files": {
127
+ "manifest": "src/app/api-test/api-tests-manifest.json",
128
+ "parameters": "src/app/api-test/parameter-matrix.json",
129
+ "results": "src/app/api-test/test-results.json"
58
130
  }
59
131
  },
60
132
  "verification": {
@@ -62,6 +134,9 @@
62
134
  "schema_matches_docs": false,
63
135
  "tests_cover_params": false,
64
136
  "all_tests_passing": false,
65
- "coverage_percent": null
66
- }
137
+ "coverage_percent": null,
138
+ "post_green_verification": false
139
+ },
140
+ "research_index": {},
141
+ "reground_history": []
67
142
  }
@@ -0,0 +1,315 @@
1
+ /**
2
+ * API Test UI Page
3
+ *
4
+ * Displays test structure parsed from Vitest test files.
5
+ * Tests are the SOURCE OF TRUTH - this UI only displays them.
6
+ *
7
+ * @generated by @hustle-together/api-dev-tools v3.0
8
+ */
9
+
10
+ 'use client';
11
+
12
+ import { useEffect, useState } from 'react';
13
+
14
+ // ============================================
15
+ // Types (mirror the API types)
16
+ // ============================================
17
+
18
+ interface TestCase {
19
+ name: string;
20
+ line: number;
21
+ status: 'pending' | 'passed' | 'failed' | 'skipped';
22
+ duration?: number;
23
+ error?: string;
24
+ }
25
+
26
+ interface TestGroup {
27
+ name: string;
28
+ line: number;
29
+ tests: TestCase[];
30
+ groups: TestGroup[];
31
+ }
32
+
33
+ interface TestFeature {
34
+ file: string;
35
+ relativePath: string;
36
+ groups: TestGroup[];
37
+ totalTests: number;
38
+ passedTests: number;
39
+ failedTests: number;
40
+ skippedTests: number;
41
+ }
42
+
43
+ interface TestStructure {
44
+ features: TestFeature[];
45
+ totalTests: number;
46
+ passedTests: number;
47
+ failedTests: number;
48
+ skippedTests: number;
49
+ parsedAt: string;
50
+ }
51
+
52
+ // ============================================
53
+ // Components
54
+ // ============================================
55
+
56
+ function StatusBadge({ status }: { status: TestCase['status'] }) {
57
+ const colors = {
58
+ pending: 'bg-gray-500',
59
+ passed: 'bg-green-500',
60
+ failed: 'bg-red-500',
61
+ skipped: 'bg-yellow-500'
62
+ };
63
+
64
+ const icons = {
65
+ pending: '○',
66
+ passed: '✓',
67
+ failed: '✗',
68
+ skipped: '⊘'
69
+ };
70
+
71
+ return (
72
+ <span className={`${colors[status]} text-white text-xs px-2 py-0.5 rounded font-mono`}>
73
+ {icons[status]} {status}
74
+ </span>
75
+ );
76
+ }
77
+
78
+ function TestCaseItem({ test, filePath }: { test: TestCase; filePath: string }) {
79
+ return (
80
+ <div className="flex items-center gap-3 py-2 px-3 hover:bg-gray-800/50 rounded group">
81
+ <StatusBadge status={test.status} />
82
+ <span className="flex-1 text-gray-300">{test.name}</span>
83
+ <span className="text-gray-600 text-xs font-mono opacity-0 group-hover:opacity-100 transition-opacity">
84
+ {filePath}:{test.line}
85
+ </span>
86
+ {test.duration && (
87
+ <span className="text-gray-500 text-xs">{test.duration}ms</span>
88
+ )}
89
+ </div>
90
+ );
91
+ }
92
+
93
+ function TestGroupItem({
94
+ group,
95
+ filePath,
96
+ depth = 0
97
+ }: {
98
+ group: TestGroup;
99
+ filePath: string;
100
+ depth?: number;
101
+ }) {
102
+ const [expanded, setExpanded] = useState(true);
103
+ const totalTests = group.tests.length + group.groups.reduce((sum, g) => sum + g.tests.length, 0);
104
+
105
+ return (
106
+ <div className={`${depth > 0 ? 'ml-4 border-l border-gray-700 pl-3' : ''}`}>
107
+ <button
108
+ onClick={() => setExpanded(!expanded)}
109
+ className="flex items-center gap-2 w-full text-left py-2 px-2 hover:bg-gray-800/30 rounded"
110
+ >
111
+ <span className="text-gray-500">{expanded ? '▼' : '▶'}</span>
112
+ <span className="font-medium text-white">{group.name}</span>
113
+ <span className="text-gray-500 text-sm">({totalTests} tests)</span>
114
+ </button>
115
+
116
+ {expanded && (
117
+ <div className="mt-1">
118
+ {group.tests.map((test, i) => (
119
+ <TestCaseItem key={i} test={test} filePath={filePath} />
120
+ ))}
121
+ {group.groups.map((subgroup, i) => (
122
+ <TestGroupItem
123
+ key={i}
124
+ group={subgroup}
125
+ filePath={filePath}
126
+ depth={depth + 1}
127
+ />
128
+ ))}
129
+ </div>
130
+ )}
131
+ </div>
132
+ );
133
+ }
134
+
135
+ function TestFeatureCard({ feature }: { feature: TestFeature }) {
136
+ const [expanded, setExpanded] = useState(true);
137
+
138
+ const passRate = feature.totalTests > 0
139
+ ? Math.round((feature.passedTests / feature.totalTests) * 100)
140
+ : 0;
141
+
142
+ return (
143
+ <div className="bg-gray-900 border border-gray-800 rounded-lg overflow-hidden">
144
+ <button
145
+ onClick={() => setExpanded(!expanded)}
146
+ className="w-full flex items-center justify-between p-4 hover:bg-gray-800/50"
147
+ >
148
+ <div className="flex items-center gap-3">
149
+ <span className="text-gray-500">{expanded ? '▼' : '▶'}</span>
150
+ <div>
151
+ <h3 className="font-mono text-white">{feature.file}</h3>
152
+ <p className="text-gray-500 text-sm">{feature.relativePath}</p>
153
+ </div>
154
+ </div>
155
+ <div className="flex items-center gap-4">
156
+ <div className="flex items-center gap-2 text-sm">
157
+ <span className="text-green-400">{feature.passedTests} passed</span>
158
+ {feature.failedTests > 0 && (
159
+ <span className="text-red-400">{feature.failedTests} failed</span>
160
+ )}
161
+ {feature.skippedTests > 0 && (
162
+ <span className="text-yellow-400">{feature.skippedTests} skipped</span>
163
+ )}
164
+ </div>
165
+ <div className="w-20 h-2 bg-gray-700 rounded-full overflow-hidden">
166
+ <div
167
+ className="h-full bg-green-500 transition-all"
168
+ style={{ width: `${passRate}%` }}
169
+ />
170
+ </div>
171
+ </div>
172
+ </button>
173
+
174
+ {expanded && (
175
+ <div className="border-t border-gray-800 p-4">
176
+ {feature.groups.map((group, i) => (
177
+ <TestGroupItem
178
+ key={i}
179
+ group={group}
180
+ filePath={feature.relativePath}
181
+ />
182
+ ))}
183
+ </div>
184
+ )}
185
+ </div>
186
+ );
187
+ }
188
+
189
+ function SummaryStats({ structure }: { structure: TestStructure }) {
190
+ return (
191
+ <div className="grid grid-cols-4 gap-4 mb-6">
192
+ <div className="bg-gray-900 border border-gray-800 rounded-lg p-4">
193
+ <div className="text-3xl font-bold text-white">{structure.totalTests}</div>
194
+ <div className="text-gray-500 text-sm">Total Tests</div>
195
+ </div>
196
+ <div className="bg-gray-900 border border-green-900 rounded-lg p-4">
197
+ <div className="text-3xl font-bold text-green-400">{structure.passedTests}</div>
198
+ <div className="text-gray-500 text-sm">Passed</div>
199
+ </div>
200
+ <div className="bg-gray-900 border border-red-900 rounded-lg p-4">
201
+ <div className="text-3xl font-bold text-red-400">{structure.failedTests}</div>
202
+ <div className="text-gray-500 text-sm">Failed</div>
203
+ </div>
204
+ <div className="bg-gray-900 border border-yellow-900 rounded-lg p-4">
205
+ <div className="text-3xl font-bold text-yellow-400">{structure.skippedTests}</div>
206
+ <div className="text-gray-500 text-sm">Skipped</div>
207
+ </div>
208
+ </div>
209
+ );
210
+ }
211
+
212
+ // ============================================
213
+ // Main Page
214
+ // ============================================
215
+
216
+ export default function ApiTestPage() {
217
+ const [structure, setStructure] = useState<TestStructure | null>(null);
218
+ const [loading, setLoading] = useState(true);
219
+ const [error, setError] = useState<string | null>(null);
220
+
221
+ const fetchTestStructure = async () => {
222
+ setLoading(true);
223
+ setError(null);
224
+ try {
225
+ const response = await fetch('/api/test-structure');
226
+ if (!response.ok) {
227
+ throw new Error(`Failed to fetch: ${response.status}`);
228
+ }
229
+ const data = await response.json();
230
+ setStructure(data);
231
+ } catch (err) {
232
+ setError(String(err));
233
+ } finally {
234
+ setLoading(false);
235
+ }
236
+ };
237
+
238
+ useEffect(() => {
239
+ fetchTestStructure();
240
+ }, []);
241
+
242
+ return (
243
+ <div className="min-h-screen bg-black text-white p-8">
244
+ <div className="max-w-6xl mx-auto">
245
+ {/* Header */}
246
+ <div className="flex items-center justify-between mb-8">
247
+ <div>
248
+ <h1 className="text-3xl font-bold">API Test Suite</h1>
249
+ <p className="text-gray-500 mt-1">
250
+ Parsed from Vitest test files (source of truth)
251
+ </p>
252
+ </div>
253
+ <div className="flex items-center gap-3">
254
+ <a
255
+ href="http://localhost:51204/__vitest__/"
256
+ target="_blank"
257
+ rel="noopener noreferrer"
258
+ className="px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded-lg text-sm font-medium transition-colors"
259
+ >
260
+ Open Vitest UI
261
+ </a>
262
+ <button
263
+ onClick={fetchTestStructure}
264
+ disabled={loading}
265
+ className="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg text-sm font-medium transition-colors disabled:opacity-50"
266
+ >
267
+ {loading ? 'Loading...' : 'Refresh'}
268
+ </button>
269
+ </div>
270
+ </div>
271
+
272
+ {/* Error State */}
273
+ {error && (
274
+ <div className="bg-red-900/50 border border-red-700 rounded-lg p-4 mb-6">
275
+ <p className="text-red-400">{error}</p>
276
+ </div>
277
+ )}
278
+
279
+ {/* Loading State */}
280
+ {loading && !structure && (
281
+ <div className="flex items-center justify-center py-20">
282
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
283
+ </div>
284
+ )}
285
+
286
+ {/* Content */}
287
+ {structure && (
288
+ <>
289
+ <SummaryStats structure={structure} />
290
+
291
+ <div className="space-y-4">
292
+ {structure.features.length === 0 ? (
293
+ <div className="text-center py-20 text-gray-500">
294
+ <p className="text-lg">No test files found</p>
295
+ <p className="mt-2 text-sm">
296
+ Create test files matching *.test.ts or *.spec.ts patterns
297
+ </p>
298
+ </div>
299
+ ) : (
300
+ structure.features.map((feature, i) => (
301
+ <TestFeatureCard key={i} feature={feature} />
302
+ ))
303
+ )}
304
+ </div>
305
+
306
+ <div className="mt-6 text-center text-gray-600 text-sm">
307
+ Parsed at {new Date(structure.parsedAt).toLocaleString()} •{' '}
308
+ {structure.features.length} test files
309
+ </div>
310
+ </>
311
+ )}
312
+ </div>
313
+ </div>
314
+ );
315
+ }