@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
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Test Structure Parser API
3
+ *
4
+ * Parses Vitest test files and returns the test structure for UI display.
5
+ * Vitest tests are the SOURCE OF TRUTH - this API only reads and parses them.
6
+ *
7
+ * @generated by @hustle-together/api-dev-tools v3.0
8
+ */
9
+
10
+ import { NextRequest, NextResponse } from 'next/server';
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+
14
+ // ============================================
15
+ // 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
+ // Parser Logic (brace counting + regex)
54
+ // ============================================
55
+
56
+ /**
57
+ * Parse a single test file and extract describe/it blocks
58
+ * Uses brace counting to handle nested structures
59
+ */
60
+ function parseTestFile(content: string, filePath: string): TestFeature {
61
+ const lines = content.split('\n');
62
+ const rootGroups: TestGroup[] = [];
63
+ const groupStack: TestGroup[] = [];
64
+
65
+ let braceCount = 0;
66
+ let inDescribe = false;
67
+ let currentDescribeBraceStart = 0;
68
+
69
+ for (let i = 0; i < lines.length; i++) {
70
+ const line = lines[i];
71
+ const lineNum = i + 1;
72
+
73
+ // Match describe blocks: describe('name', () => { or describe("name", function() {
74
+ const describeMatch = line.match(/describe\s*\(\s*['"`]([^'"`]+)['"`]/);
75
+ if (describeMatch) {
76
+ const group: TestGroup = {
77
+ name: describeMatch[1],
78
+ line: lineNum,
79
+ tests: [],
80
+ groups: []
81
+ };
82
+
83
+ if (groupStack.length > 0) {
84
+ groupStack[groupStack.length - 1].groups.push(group);
85
+ } else {
86
+ rootGroups.push(group);
87
+ }
88
+
89
+ groupStack.push(group);
90
+ inDescribe = true;
91
+ currentDescribeBraceStart = braceCount;
92
+ }
93
+
94
+ // Match it/test blocks: it('name', ...) or test('name', ...)
95
+ const testMatch = line.match(/(?:it|test)\s*\(\s*['"`]([^'"`]+)['"`]/);
96
+ if (testMatch && groupStack.length > 0) {
97
+ const testCase: TestCase = {
98
+ name: testMatch[1],
99
+ line: lineNum,
100
+ status: 'pending' // Will be updated by test runner results
101
+ };
102
+
103
+ // Check for .skip or .todo
104
+ if (line.includes('.skip') || line.includes('.todo')) {
105
+ testCase.status = 'skipped';
106
+ }
107
+
108
+ groupStack[groupStack.length - 1].tests.push(testCase);
109
+ }
110
+
111
+ // Count braces to track scope
112
+ const openBraces = (line.match(/{/g) || []).length;
113
+ const closeBraces = (line.match(/}/g) || []).length;
114
+ braceCount += openBraces - closeBraces;
115
+
116
+ // Pop group from stack when we close its scope
117
+ if (inDescribe && braceCount <= currentDescribeBraceStart && groupStack.length > 0) {
118
+ groupStack.pop();
119
+ if (groupStack.length === 0) {
120
+ inDescribe = false;
121
+ } else {
122
+ // Update brace start for parent describe
123
+ currentDescribeBraceStart = braceCount;
124
+ }
125
+ }
126
+ }
127
+
128
+ // Count tests
129
+ const countTests = (groups: TestGroup[]): { total: number; passed: number; failed: number; skipped: number } => {
130
+ let total = 0, passed = 0, failed = 0, skipped = 0;
131
+
132
+ for (const group of groups) {
133
+ for (const test of group.tests) {
134
+ total++;
135
+ if (test.status === 'passed') passed++;
136
+ else if (test.status === 'failed') failed++;
137
+ else if (test.status === 'skipped') skipped++;
138
+ }
139
+ const nested = countTests(group.groups);
140
+ total += nested.total;
141
+ passed += nested.passed;
142
+ failed += nested.failed;
143
+ skipped += nested.skipped;
144
+ }
145
+
146
+ return { total, passed, failed, skipped };
147
+ };
148
+
149
+ const counts = countTests(rootGroups);
150
+
151
+ return {
152
+ file: path.basename(filePath),
153
+ relativePath: filePath,
154
+ groups: rootGroups,
155
+ totalTests: counts.total,
156
+ passedTests: counts.passed,
157
+ failedTests: counts.failed,
158
+ skippedTests: counts.skipped
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Find all test files in the project
164
+ */
165
+ function findTestFiles(baseDir: string): string[] {
166
+ const testFiles: string[] = [];
167
+ const testPatterns = [
168
+ '**/*.test.ts',
169
+ '**/*.test.tsx',
170
+ '**/*.spec.ts',
171
+ '**/*.spec.tsx',
172
+ '**/__tests__/**/*.ts',
173
+ '**/__tests__/**/*.tsx'
174
+ ];
175
+
176
+ function walkDir(dir: string) {
177
+ try {
178
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
179
+
180
+ for (const entry of entries) {
181
+ const fullPath = path.join(dir, entry.name);
182
+
183
+ // Skip node_modules and hidden directories
184
+ if (entry.isDirectory()) {
185
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
186
+ continue;
187
+ }
188
+ walkDir(fullPath);
189
+ } else if (entry.isFile()) {
190
+ // Check if file matches test patterns
191
+ if (
192
+ entry.name.endsWith('.test.ts') ||
193
+ entry.name.endsWith('.test.tsx') ||
194
+ entry.name.endsWith('.spec.ts') ||
195
+ entry.name.endsWith('.spec.tsx')
196
+ ) {
197
+ testFiles.push(fullPath);
198
+ }
199
+ }
200
+ }
201
+ } catch (error) {
202
+ // Ignore permission errors
203
+ }
204
+ }
205
+
206
+ walkDir(baseDir);
207
+ return testFiles;
208
+ }
209
+
210
+ // ============================================
211
+ // API Handler
212
+ // ============================================
213
+
214
+ export async function GET(request: NextRequest) {
215
+ try {
216
+ const { searchParams } = new URL(request.url);
217
+ const testPath = searchParams.get('path');
218
+ const baseDir = process.cwd();
219
+
220
+ let testFiles: string[];
221
+
222
+ if (testPath) {
223
+ // Parse specific test file
224
+ const fullPath = path.join(baseDir, testPath);
225
+ if (!fs.existsSync(fullPath)) {
226
+ return NextResponse.json(
227
+ { error: `Test file not found: ${testPath}` },
228
+ { status: 404 }
229
+ );
230
+ }
231
+ testFiles = [fullPath];
232
+ } else {
233
+ // Find all test files
234
+ testFiles = findTestFiles(baseDir);
235
+ }
236
+
237
+ const features: TestFeature[] = [];
238
+
239
+ for (const file of testFiles) {
240
+ try {
241
+ const content = fs.readFileSync(file, 'utf-8');
242
+ const relativePath = path.relative(baseDir, file);
243
+ const feature = parseTestFile(content, relativePath);
244
+ features.push(feature);
245
+ } catch (error) {
246
+ // Skip files that can't be read
247
+ console.error(`Failed to parse ${file}:`, error);
248
+ }
249
+ }
250
+
251
+ // Aggregate counts
252
+ const structure: TestStructure = {
253
+ features,
254
+ totalTests: features.reduce((sum, f) => sum + f.totalTests, 0),
255
+ passedTests: features.reduce((sum, f) => sum + f.passedTests, 0),
256
+ failedTests: features.reduce((sum, f) => sum + f.failedTests, 0),
257
+ skippedTests: features.reduce((sum, f) => sum + f.skippedTests, 0),
258
+ parsedAt: new Date().toISOString()
259
+ };
260
+
261
+ return NextResponse.json(structure);
262
+ } catch (error) {
263
+ console.error('Test structure parser error:', error);
264
+ return NextResponse.json(
265
+ { error: 'Failed to parse test structure', details: String(error) },
266
+ { status: 500 }
267
+ );
268
+ }
269
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "version": "3.0.0",
3
+ "description": "Research cache index with freshness tracking",
4
+ "freshness_threshold_days": 7,
5
+ "apis": {}
6
+ }
@@ -16,6 +16,16 @@
16
16
  ]
17
17
  },
18
18
  "hooks": {
19
+ "SessionStart": [
20
+ {
21
+ "hooks": [
22
+ {
23
+ "type": "command",
24
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-startup.py"
25
+ }
26
+ ]
27
+ }
28
+ ],
19
29
  "UserPromptSubmit": [
20
30
  {
21
31
  "hooks": [
@@ -30,6 +40,14 @@
30
40
  {
31
41
  "matcher": "Write|Edit",
32
42
  "hooks": [
43
+ {
44
+ "type": "command",
45
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-disambiguation.py"
46
+ },
47
+ {
48
+ "type": "command",
49
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-scope.py"
50
+ },
33
51
  {
34
52
  "type": "command",
35
53
  "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-research.py"
@@ -38,9 +56,37 @@
38
56
  "type": "command",
39
57
  "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-interview.py"
40
58
  },
59
+ {
60
+ "type": "command",
61
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-deep-research.py"
62
+ },
63
+ {
64
+ "type": "command",
65
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-schema.py"
66
+ },
67
+ {
68
+ "type": "command",
69
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-environment.py"
70
+ },
71
+ {
72
+ "type": "command",
73
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-tdd-red.py"
74
+ },
41
75
  {
42
76
  "type": "command",
43
77
  "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/verify-implementation.py"
78
+ },
79
+ {
80
+ "type": "command",
81
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-verify.py"
82
+ },
83
+ {
84
+ "type": "command",
85
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-refactor.py"
86
+ },
87
+ {
88
+ "type": "command",
89
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-documentation.py"
44
90
  }
45
91
  ]
46
92
  }
@@ -52,6 +98,19 @@
52
98
  {
53
99
  "type": "command",
54
100
  "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/track-tool-use.py"
101
+ },
102
+ {
103
+ "type": "command",
104
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/periodic-reground.py"
105
+ }
106
+ ]
107
+ },
108
+ {
109
+ "matcher": "Bash",
110
+ "hooks": [
111
+ {
112
+ "type": "command",
113
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/verify-after-green.py"
55
114
  }
56
115
  ]
57
116
  }