@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,483 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Extract Parameters Script
|
|
4
|
+
*
|
|
5
|
+
* Programmatically extracts ALL parameters from:
|
|
6
|
+
* 1. Zod schemas (request/response validation)
|
|
7
|
+
* 2. Route files (query params, headers)
|
|
8
|
+
* 3. Test files (parameter usage in tests)
|
|
9
|
+
*
|
|
10
|
+
* Generates a parameter matrix for each endpoint showing:
|
|
11
|
+
* - Name, type, required/optional
|
|
12
|
+
* - Valid values, defaults, constraints
|
|
13
|
+
* - Test coverage (which params are tested)
|
|
14
|
+
*
|
|
15
|
+
* IMPORTANT: This is 100% programmatic - NO LLM involvement.
|
|
16
|
+
*
|
|
17
|
+
* @generated by @hustle-together/api-dev-tools v3.0
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'fs';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
|
|
23
|
+
// ============================================
|
|
24
|
+
// Types
|
|
25
|
+
// ============================================
|
|
26
|
+
|
|
27
|
+
interface ParameterDefinition {
|
|
28
|
+
name: string;
|
|
29
|
+
location: 'query' | 'body' | 'header' | 'path';
|
|
30
|
+
type: string;
|
|
31
|
+
required: boolean;
|
|
32
|
+
description?: string;
|
|
33
|
+
default?: unknown;
|
|
34
|
+
enum?: string[];
|
|
35
|
+
min?: number;
|
|
36
|
+
max?: number;
|
|
37
|
+
pattern?: string;
|
|
38
|
+
example?: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface EndpointParameters {
|
|
42
|
+
endpoint: string;
|
|
43
|
+
method: string;
|
|
44
|
+
parameters: ParameterDefinition[];
|
|
45
|
+
sourceFiles: string[];
|
|
46
|
+
testedParameters: string[];
|
|
47
|
+
untestedParameters: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ParameterMatrix {
|
|
51
|
+
version: string;
|
|
52
|
+
generatedAt: string;
|
|
53
|
+
endpoints: EndpointParameters[];
|
|
54
|
+
coverage: {
|
|
55
|
+
totalParameters: number;
|
|
56
|
+
testedParameters: number;
|
|
57
|
+
coveragePercent: number;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================
|
|
62
|
+
// File Discovery
|
|
63
|
+
// ============================================
|
|
64
|
+
|
|
65
|
+
function findFiles(baseDir: string, pattern: RegExp, exclude: string[] = ['node_modules', '.git', 'dist']): string[] {
|
|
66
|
+
const files: string[] = [];
|
|
67
|
+
|
|
68
|
+
function walk(dir: string) {
|
|
69
|
+
try {
|
|
70
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (exclude.includes(entry.name)) continue;
|
|
73
|
+
|
|
74
|
+
const fullPath = path.join(dir, entry.name);
|
|
75
|
+
if (entry.isDirectory()) {
|
|
76
|
+
walk(fullPath);
|
|
77
|
+
} else if (entry.isFile() && pattern.test(entry.name)) {
|
|
78
|
+
files.push(fullPath);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// Skip unreadable directories
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
walk(baseDir);
|
|
87
|
+
return files;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================
|
|
91
|
+
// Zod Schema Extractor
|
|
92
|
+
// ============================================
|
|
93
|
+
|
|
94
|
+
function extractFromZodSchema(content: string): ParameterDefinition[] {
|
|
95
|
+
const params: ParameterDefinition[] = [];
|
|
96
|
+
|
|
97
|
+
// Match z.object definitions with their field names
|
|
98
|
+
// This handles nested objects and various Zod types
|
|
99
|
+
const zodObjectRegex = /z\.object\s*\(\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/gs;
|
|
100
|
+
|
|
101
|
+
let objectMatch;
|
|
102
|
+
while ((objectMatch = zodObjectRegex.exec(content)) !== null) {
|
|
103
|
+
const objectBody = objectMatch[1];
|
|
104
|
+
|
|
105
|
+
// Parse individual fields
|
|
106
|
+
// Pattern handles: fieldName: z.type().chain().chain()
|
|
107
|
+
const fieldRegex = /(\w+)\s*:\s*(z\.(?:[^,\n]+(?:\([^)]*\))?)+)/g;
|
|
108
|
+
|
|
109
|
+
let fieldMatch;
|
|
110
|
+
while ((fieldMatch = fieldRegex.exec(objectBody)) !== null) {
|
|
111
|
+
const [, name, zodChain] = fieldMatch;
|
|
112
|
+
|
|
113
|
+
const param = parseZodChain(name, zodChain);
|
|
114
|
+
if (param) {
|
|
115
|
+
params.push(param);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return params;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseZodChain(name: string, chain: string): ParameterDefinition | null {
|
|
124
|
+
// Determine base type
|
|
125
|
+
const typeMatch = chain.match(/z\.(\w+)/);
|
|
126
|
+
if (!typeMatch) return null;
|
|
127
|
+
|
|
128
|
+
const baseType = typeMatch[1];
|
|
129
|
+
|
|
130
|
+
const param: ParameterDefinition = {
|
|
131
|
+
name,
|
|
132
|
+
location: 'body', // Default, will be refined based on context
|
|
133
|
+
type: mapZodType(baseType),
|
|
134
|
+
required: true
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Check for optional
|
|
138
|
+
if (chain.includes('.optional()') || chain.includes('.nullable()')) {
|
|
139
|
+
param.required = false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Extract description
|
|
143
|
+
const descMatch = chain.match(/\.describe\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/);
|
|
144
|
+
if (descMatch) {
|
|
145
|
+
param.description = descMatch[1];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Extract default value
|
|
149
|
+
const defaultMatch = chain.match(/\.default\s*\(\s*([^)]+)\s*\)/);
|
|
150
|
+
if (defaultMatch) {
|
|
151
|
+
try {
|
|
152
|
+
param.default = JSON.parse(defaultMatch[1].replace(/'/g, '"'));
|
|
153
|
+
} catch {
|
|
154
|
+
param.default = defaultMatch[1].replace(/['"]/g, '');
|
|
155
|
+
}
|
|
156
|
+
param.required = false; // Has default = not required
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Extract enum values
|
|
160
|
+
if (baseType === 'enum') {
|
|
161
|
+
const enumMatch = chain.match(/z\.enum\s*\(\s*\[([^\]]+)\]/);
|
|
162
|
+
if (enumMatch) {
|
|
163
|
+
param.enum = enumMatch[1]
|
|
164
|
+
.split(',')
|
|
165
|
+
.map(s => s.trim().replace(/['"`]/g, ''));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Extract min/max for numbers
|
|
170
|
+
const minMatch = chain.match(/\.min\s*\(\s*(\d+)\s*\)/);
|
|
171
|
+
if (minMatch) param.min = parseInt(minMatch[1]);
|
|
172
|
+
|
|
173
|
+
const maxMatch = chain.match(/\.max\s*\(\s*(\d+)\s*\)/);
|
|
174
|
+
if (maxMatch) param.max = parseInt(maxMatch[1]);
|
|
175
|
+
|
|
176
|
+
// Extract pattern for strings
|
|
177
|
+
const regexMatch = chain.match(/\.regex\s*\(\s*\/([^/]+)\//);
|
|
178
|
+
if (regexMatch) param.pattern = regexMatch[1];
|
|
179
|
+
|
|
180
|
+
return param;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function mapZodType(zodType: string): string {
|
|
184
|
+
const typeMap: Record<string, string> = {
|
|
185
|
+
'string': 'string',
|
|
186
|
+
'number': 'number',
|
|
187
|
+
'boolean': 'boolean',
|
|
188
|
+
'array': 'array',
|
|
189
|
+
'object': 'object',
|
|
190
|
+
'enum': 'enum',
|
|
191
|
+
'literal': 'literal',
|
|
192
|
+
'union': 'union',
|
|
193
|
+
'date': 'string (ISO date)',
|
|
194
|
+
'coerce': 'coerced'
|
|
195
|
+
};
|
|
196
|
+
return typeMap[zodType] || zodType;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ============================================
|
|
200
|
+
// Route File Extractor
|
|
201
|
+
// ============================================
|
|
202
|
+
|
|
203
|
+
function extractFromRouteFile(content: string, existingParams: ParameterDefinition[]): ParameterDefinition[] {
|
|
204
|
+
const params: ParameterDefinition[] = [];
|
|
205
|
+
|
|
206
|
+
// Extract query parameters from searchParams usage
|
|
207
|
+
// Pattern: searchParams.get('paramName')
|
|
208
|
+
const queryParamRegex = /searchParams\.get\s*\(\s*['"`](\w+)['"`]\s*\)/g;
|
|
209
|
+
let match;
|
|
210
|
+
while ((match = queryParamRegex.exec(content)) !== null) {
|
|
211
|
+
const name = match[1];
|
|
212
|
+
if (!existingParams.some(p => p.name === name) && !params.some(p => p.name === name)) {
|
|
213
|
+
params.push({
|
|
214
|
+
name,
|
|
215
|
+
location: 'query',
|
|
216
|
+
type: 'string',
|
|
217
|
+
required: false // Query params are typically optional
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Extract header access
|
|
223
|
+
// Pattern: request.headers.get('X-Header-Name')
|
|
224
|
+
const headerRegex = /headers\.get\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
|
|
225
|
+
while ((match = headerRegex.exec(content)) !== null) {
|
|
226
|
+
const name = match[1];
|
|
227
|
+
if (!existingParams.some(p => p.name === name) && !params.some(p => p.name === name)) {
|
|
228
|
+
params.push({
|
|
229
|
+
name,
|
|
230
|
+
location: 'header',
|
|
231
|
+
type: 'string',
|
|
232
|
+
required: false
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Extract path parameters from dynamic routes
|
|
238
|
+
// Pattern: params.paramName or { paramName } = params
|
|
239
|
+
const pathParamRegex = /params\.(\w+)|{\s*(\w+)\s*}\s*=\s*params/g;
|
|
240
|
+
while ((match = pathParamRegex.exec(content)) !== null) {
|
|
241
|
+
const name = match[1] || match[2];
|
|
242
|
+
if (!existingParams.some(p => p.name === name) && !params.some(p => p.name === name)) {
|
|
243
|
+
params.push({
|
|
244
|
+
name,
|
|
245
|
+
location: 'path',
|
|
246
|
+
type: 'string',
|
|
247
|
+
required: true // Path params are always required
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return params;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ============================================
|
|
256
|
+
// Test File Analyzer
|
|
257
|
+
// ============================================
|
|
258
|
+
|
|
259
|
+
function extractTestedParams(content: string): string[] {
|
|
260
|
+
const testedParams: Set<string> = new Set();
|
|
261
|
+
|
|
262
|
+
// Find parameters in fetch/request calls
|
|
263
|
+
// Pattern: body: { param: value } or ?param=value
|
|
264
|
+
const bodyParamRegex = /body\s*:\s*(?:JSON\.stringify\s*\()?\s*\{([^}]+)\}/g;
|
|
265
|
+
let match;
|
|
266
|
+
|
|
267
|
+
while ((match = bodyParamRegex.exec(content)) !== null) {
|
|
268
|
+
const bodyContent = match[1];
|
|
269
|
+
const paramNames = bodyContent.match(/(\w+)\s*:/g);
|
|
270
|
+
if (paramNames) {
|
|
271
|
+
paramNames.forEach(p => testedParams.add(p.replace(':', '').trim()));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Find query params in URLs
|
|
276
|
+
const queryRegex = /[?&](\w+)=/g;
|
|
277
|
+
while ((match = queryRegex.exec(content)) !== null) {
|
|
278
|
+
testedParams.add(match[1]);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Find header params
|
|
282
|
+
const headerRegex = /['"`](X-[\w-]+|Authorization|Content-Type)['"`]\s*:/gi;
|
|
283
|
+
while ((match = headerRegex.exec(content)) !== null) {
|
|
284
|
+
testedParams.add(match[1]);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return Array.from(testedParams);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ============================================
|
|
291
|
+
// Main Extractor
|
|
292
|
+
// ============================================
|
|
293
|
+
|
|
294
|
+
function extractAllParameters(baseDir: string): ParameterMatrix {
|
|
295
|
+
console.log('๐ Scanning for parameter sources...');
|
|
296
|
+
|
|
297
|
+
// Find files
|
|
298
|
+
const schemaFiles = findFiles(baseDir, /schema.*\.ts$|schemas?\/.*\.ts$/);
|
|
299
|
+
const routeFiles = findFiles(baseDir, /route\.(ts|tsx)$/);
|
|
300
|
+
const testFiles = findFiles(baseDir, /\.(test|spec)\.(ts|tsx)$/);
|
|
301
|
+
|
|
302
|
+
console.log(` Found ${schemaFiles.length} schema files`);
|
|
303
|
+
console.log(` Found ${routeFiles.length} route files`);
|
|
304
|
+
console.log(` Found ${testFiles.length} test files`);
|
|
305
|
+
|
|
306
|
+
const endpointsMap = new Map<string, EndpointParameters>();
|
|
307
|
+
|
|
308
|
+
// Process schema files
|
|
309
|
+
for (const schemaFile of schemaFiles) {
|
|
310
|
+
const content = fs.readFileSync(schemaFile, 'utf-8');
|
|
311
|
+
const params = extractFromZodSchema(content);
|
|
312
|
+
|
|
313
|
+
if (params.length > 0) {
|
|
314
|
+
const relativePath = path.relative(baseDir, schemaFile);
|
|
315
|
+
|
|
316
|
+
// Determine endpoint from file path
|
|
317
|
+
const pathParts = relativePath.split(path.sep);
|
|
318
|
+
const apiIndex = pathParts.findIndex(p => p === 'api');
|
|
319
|
+
let endpoint = '/api/unknown';
|
|
320
|
+
|
|
321
|
+
if (apiIndex >= 0) {
|
|
322
|
+
endpoint = '/' + pathParts.slice(apiIndex).join('/').replace(/\/schema.*\.ts$/, '');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const existing = endpointsMap.get(endpoint) || {
|
|
326
|
+
endpoint,
|
|
327
|
+
method: 'POST',
|
|
328
|
+
parameters: [],
|
|
329
|
+
sourceFiles: [],
|
|
330
|
+
testedParameters: [],
|
|
331
|
+
untestedParameters: []
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
existing.parameters.push(...params);
|
|
335
|
+
existing.sourceFiles.push(relativePath);
|
|
336
|
+
endpointsMap.set(endpoint, existing);
|
|
337
|
+
|
|
338
|
+
console.log(`\n๐ Schema: ${relativePath}`);
|
|
339
|
+
console.log(` Extracted ${params.length} parameters for ${endpoint}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Process route files
|
|
344
|
+
for (const routeFile of routeFiles) {
|
|
345
|
+
const content = fs.readFileSync(routeFile, 'utf-8');
|
|
346
|
+
const relativePath = path.relative(baseDir, routeFile);
|
|
347
|
+
|
|
348
|
+
// Determine endpoint
|
|
349
|
+
const pathParts = relativePath.split(path.sep);
|
|
350
|
+
const apiIndex = pathParts.findIndex(p => p === 'api');
|
|
351
|
+
let endpoint = '/api/unknown';
|
|
352
|
+
|
|
353
|
+
if (apiIndex >= 0) {
|
|
354
|
+
endpoint = '/' + pathParts.slice(apiIndex, -1).join('/');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const existing = endpointsMap.get(endpoint) || {
|
|
358
|
+
endpoint,
|
|
359
|
+
method: 'GET',
|
|
360
|
+
parameters: [],
|
|
361
|
+
sourceFiles: [],
|
|
362
|
+
testedParameters: [],
|
|
363
|
+
untestedParameters: []
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// Determine method
|
|
367
|
+
if (content.includes('export async function POST') || content.includes('export const POST')) {
|
|
368
|
+
existing.method = 'POST';
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const routeParams = extractFromRouteFile(content, existing.parameters);
|
|
372
|
+
existing.parameters.push(...routeParams);
|
|
373
|
+
if (!existing.sourceFiles.includes(relativePath)) {
|
|
374
|
+
existing.sourceFiles.push(relativePath);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
endpointsMap.set(endpoint, existing);
|
|
378
|
+
|
|
379
|
+
if (routeParams.length > 0) {
|
|
380
|
+
console.log(`\n๐ Route: ${relativePath}`);
|
|
381
|
+
console.log(` Extracted ${routeParams.length} additional parameters`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Process test files to find tested parameters
|
|
386
|
+
for (const testFile of testFiles) {
|
|
387
|
+
const content = fs.readFileSync(testFile, 'utf-8');
|
|
388
|
+
const relativePath = path.relative(baseDir, testFile);
|
|
389
|
+
|
|
390
|
+
// Determine endpoint from test file
|
|
391
|
+
const endpointMatch = content.match(/(?:\/api\/[\w\/-]+)/);
|
|
392
|
+
if (!endpointMatch) continue;
|
|
393
|
+
|
|
394
|
+
const endpoint = endpointMatch[0];
|
|
395
|
+
const existing = endpointsMap.get(endpoint);
|
|
396
|
+
|
|
397
|
+
if (existing) {
|
|
398
|
+
const testedParams = extractTestedParams(content);
|
|
399
|
+
existing.testedParameters = [...new Set([...existing.testedParameters, ...testedParams])];
|
|
400
|
+
|
|
401
|
+
console.log(`\n๐งช Test: ${relativePath}`);
|
|
402
|
+
console.log(` Found ${testedParams.length} tested parameters`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Calculate untested parameters and coverage
|
|
407
|
+
let totalParams = 0;
|
|
408
|
+
let testedParams = 0;
|
|
409
|
+
|
|
410
|
+
const endpoints = Array.from(endpointsMap.values()).map(ep => {
|
|
411
|
+
const allParamNames = ep.parameters.map(p => p.name);
|
|
412
|
+
ep.untestedParameters = allParamNames.filter(p => !ep.testedParameters.includes(p));
|
|
413
|
+
|
|
414
|
+
totalParams += ep.parameters.length;
|
|
415
|
+
testedParams += ep.testedParameters.length;
|
|
416
|
+
|
|
417
|
+
return ep;
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
version: '3.0.0',
|
|
422
|
+
generatedAt: new Date().toISOString(),
|
|
423
|
+
endpoints,
|
|
424
|
+
coverage: {
|
|
425
|
+
totalParameters: totalParams,
|
|
426
|
+
testedParameters: testedParams,
|
|
427
|
+
coveragePercent: totalParams > 0 ? Math.round((testedParams / totalParams) * 100) : 0
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ============================================
|
|
433
|
+
// CLI Entry Point
|
|
434
|
+
// ============================================
|
|
435
|
+
|
|
436
|
+
function main() {
|
|
437
|
+
const args = process.argv.slice(2);
|
|
438
|
+
const baseDir = args[0] || process.cwd();
|
|
439
|
+
const outputPath = args[1] || path.join(baseDir, 'src', 'app', 'api-test', 'parameter-matrix.json');
|
|
440
|
+
|
|
441
|
+
console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
442
|
+
console.log(' ๐ Parameter Matrix Extractor');
|
|
443
|
+
console.log(' @hustle-together/api-dev-tools v3.0');
|
|
444
|
+
console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
445
|
+
console.log(`\n๐ Base directory: ${baseDir}`);
|
|
446
|
+
console.log(`๐ Output file: ${outputPath}\n`);
|
|
447
|
+
|
|
448
|
+
const matrix = extractAllParameters(baseDir);
|
|
449
|
+
|
|
450
|
+
// Ensure output directory exists
|
|
451
|
+
const outputDir = path.dirname(outputPath);
|
|
452
|
+
if (!fs.existsSync(outputDir)) {
|
|
453
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Write matrix
|
|
457
|
+
fs.writeFileSync(outputPath, JSON.stringify(matrix, null, 2));
|
|
458
|
+
|
|
459
|
+
console.log('\nโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
460
|
+
console.log(' โ
Parameter matrix generated successfully!');
|
|
461
|
+
console.log('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ');
|
|
462
|
+
console.log(`\n๐ Coverage Summary:`);
|
|
463
|
+
console.log(` โข Total parameters: ${matrix.coverage.totalParameters}`);
|
|
464
|
+
console.log(` โข Tested parameters: ${matrix.coverage.testedParameters}`);
|
|
465
|
+
console.log(` โข Coverage: ${matrix.coverage.coveragePercent}%`);
|
|
466
|
+
|
|
467
|
+
// List untested parameters
|
|
468
|
+
const untested = matrix.endpoints.flatMap(ep =>
|
|
469
|
+
ep.untestedParameters.map(p => `${ep.endpoint}: ${p}`)
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
if (untested.length > 0) {
|
|
473
|
+
console.log(`\nโ ๏ธ Untested parameters (${untested.length}):`);
|
|
474
|
+
untested.slice(0, 10).forEach(p => console.log(` โข ${p}`));
|
|
475
|
+
if (untested.length > 10) {
|
|
476
|
+
console.log(` ... and ${untested.length - 10} more`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
console.log(`\n๐ Output: ${outputPath}\n`);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
main();
|