@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.
- package/README.md +343 -467
- package/bin/cli.js +229 -15
- package/commands/README.md +124 -251
- package/commands/api-create.md +318 -136
- package/commands/api-interview.md +252 -256
- package/commands/api-research.md +209 -234
- package/commands/api-verify.md +231 -0
- package/demo/audio/generate-all-narrations.js +581 -0
- package/demo/audio/generate-narration.js +120 -56
- package/demo/audio/generate-voice-previews.js +140 -0
- package/demo/audio/narration-adam-timing.json +4675 -0
- package/demo/audio/narration-adam.mp3 +0 -0
- package/demo/audio/narration-creature-timing.json +4675 -0
- package/demo/audio/narration-creature.mp3 +0 -0
- package/demo/audio/narration-gaming-timing.json +4675 -0
- package/demo/audio/narration-gaming.mp3 +0 -0
- package/demo/audio/narration-hope-timing.json +4675 -0
- package/demo/audio/narration-hope.mp3 +0 -0
- package/demo/audio/narration-mark-timing.json +4675 -0
- package/demo/audio/narration-mark.mp3 +0 -0
- package/demo/audio/previews/manifest.json +30 -0
- package/demo/audio/previews/preview-creature.mp3 +0 -0
- package/demo/audio/previews/preview-gaming.mp3 +0 -0
- package/demo/audio/previews/preview-hope.mp3 +0 -0
- package/demo/audio/previews/preview-mark.mp3 +0 -0
- package/demo/audio/voices-manifest.json +50 -0
- package/demo/hustle-together/blog/gemini-vs-claude-widgets.html +30 -28
- package/demo/hustle-together/blog/interview-driven-api-development.html +37 -23
- package/demo/hustle-together/index.html +142 -109
- package/demo/workflow-demo.html +2618 -1036
- package/hooks/api-workflow-check.py +2 -0
- package/hooks/enforce-deep-research.py +180 -0
- package/hooks/enforce-disambiguation.py +149 -0
- package/hooks/enforce-documentation.py +187 -0
- package/hooks/enforce-environment.py +249 -0
- package/hooks/enforce-refactor.py +187 -0
- package/hooks/enforce-research.py +93 -46
- package/hooks/enforce-schema.py +186 -0
- package/hooks/enforce-scope.py +156 -0
- package/hooks/enforce-tdd-red.py +246 -0
- package/hooks/enforce-verify.py +186 -0
- package/hooks/periodic-reground.py +154 -0
- package/hooks/session-startup.py +151 -0
- package/hooks/track-tool-use.py +109 -17
- package/hooks/verify-after-green.py +282 -0
- package/package.json +3 -2
- package/scripts/collect-test-results.ts +404 -0
- package/scripts/extract-parameters.ts +483 -0
- package/scripts/generate-test-manifest.ts +520 -0
- package/templates/CLAUDE-SECTION.md +84 -0
- package/templates/api-dev-state.json +83 -8
- package/templates/api-test/page.tsx +315 -0
- package/templates/api-test/test-structure/route.ts +269 -0
- package/templates/research-index.json +6 -0
- 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
|
+
}
|
package/templates/settings.json
CHANGED
|
@@ -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
|
}
|