@hustle-together/api-dev-tools 3.11.1 โ†’ 3.12.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 (139) hide show
  1. package/.claude/agents/code-reviewer.md +170 -0
  2. package/.claude/agents/docs-generator.md +80 -0
  3. package/.claude/agents/implementation-reviewer.md +119 -0
  4. package/.claude/agents/parallel-researcher.md +52 -0
  5. package/.claude/agents/research-validator.md +116 -0
  6. package/.claude/agents/schema-generator.md +70 -0
  7. package/.claude/agents/test-writer.md +104 -0
  8. package/.claude/api-dev-state.json +305 -56
  9. package/.claude/commands/README.md +21 -10
  10. package/.claude/commands/add-command.md +8 -5
  11. package/.claude/commands/api-create.md +36 -25
  12. package/.claude/commands/api-env.md +1 -0
  13. package/.claude/commands/api-interview.md +32 -19
  14. package/.claude/commands/api-research.md +47 -21
  15. package/.claude/commands/api-status.md +21 -1
  16. package/.claude/commands/api-verify.md +14 -13
  17. package/.claude/commands/beepboop.md +4 -5
  18. package/.claude/commands/busycommit.md +2 -3
  19. package/.claude/commands/commit.md +2 -3
  20. package/.claude/commands/cycle.md +2 -7
  21. package/.claude/commands/gap.md +2 -3
  22. package/.claude/commands/green.md +2 -7
  23. package/.claude/commands/issue.md +3 -8
  24. package/.claude/commands/ntfy-setup.md +91 -0
  25. package/.claude/commands/ntfy-test.md +74 -0
  26. package/.claude/commands/plan.md +2 -3
  27. package/.claude/commands/pr.md +2 -3
  28. package/.claude/commands/publish.md +40 -0
  29. package/.claude/commands/red.md +2 -7
  30. package/.claude/commands/refactor.md +2 -7
  31. package/.claude/commands/spike.md +2 -7
  32. package/.claude/commands/summarize.md +2 -3
  33. package/.claude/commands/tdd.md +2 -7
  34. package/.claude/commands/worktree-add.md +208 -216
  35. package/.claude/commands/worktree-cleanup.md +172 -178
  36. package/.claude/settings.json +63 -12
  37. package/.claude/settings.local.json +2 -1
  38. package/.claude-plugin/marketplace.json +2 -11
  39. package/.skills/README.md +55 -53
  40. package/.skills/_shared/settings.json +1 -1
  41. package/.skills/add-command/SKILL.md +10 -5
  42. package/.skills/api-create/SKILL.md +146 -35
  43. package/.skills/api-env/SKILL.md +1 -0
  44. package/.skills/api-interview/SKILL.md +32 -19
  45. package/.skills/api-research/SKILL.md +47 -21
  46. package/.skills/api-status/SKILL.md +21 -1
  47. package/.skills/api-verify/SKILL.md +14 -13
  48. package/.skills/beepboop/SKILL.md +6 -5
  49. package/.skills/busycommit/SKILL.md +4 -3
  50. package/.skills/commit/SKILL.md +4 -3
  51. package/.skills/cycle/SKILL.md +4 -7
  52. package/.skills/gap/SKILL.md +4 -3
  53. package/.skills/green/SKILL.md +4 -7
  54. package/.skills/issue/SKILL.md +5 -8
  55. package/.skills/plan/SKILL.md +4 -3
  56. package/.skills/pr/SKILL.md +4 -3
  57. package/.skills/publish/SKILL.md +160 -0
  58. package/.skills/red/SKILL.md +4 -7
  59. package/.skills/refactor/SKILL.md +4 -7
  60. package/.skills/spike/SKILL.md +4 -7
  61. package/.skills/summarize/SKILL.md +4 -3
  62. package/.skills/tdd/SKILL.md +4 -7
  63. package/.skills/update-todos/SKILL.md +22 -0
  64. package/.skills/worktree-add/SKILL.md +210 -216
  65. package/.skills/worktree-cleanup/SKILL.md +183 -187
  66. package/CHANGELOG.md +97 -79
  67. package/README.md +161 -7142
  68. package/bin/cli.js +448 -805
  69. package/commands/README.md +66 -31
  70. package/commands/add-command.md +8 -5
  71. package/commands/beepboop.md +4 -5
  72. package/commands/busycommit.md +2 -3
  73. package/commands/commit.md +2 -3
  74. package/commands/cycle.md +2 -7
  75. package/commands/gap.md +2 -3
  76. package/commands/green.md +2 -7
  77. package/commands/hustle-api-continue.md +8 -5
  78. package/commands/hustle-api-create.md +70 -29
  79. package/commands/hustle-api-env.md +1 -0
  80. package/commands/hustle-api-interview.md +32 -19
  81. package/commands/hustle-api-research.md +47 -21
  82. package/commands/hustle-api-sessions.md +8 -7
  83. package/commands/hustle-api-status.md +21 -1
  84. package/commands/hustle-api-verify.md +14 -13
  85. package/commands/hustle-combine.md +488 -241
  86. package/commands/hustle-ui-create-page.md +113 -50
  87. package/commands/hustle-ui-create.md +179 -26
  88. package/commands/issue.md +3 -8
  89. package/commands/plan.md +2 -3
  90. package/commands/pr.md +2 -3
  91. package/commands/red.md +2 -7
  92. package/commands/refactor.md +2 -7
  93. package/commands/spike.md +2 -7
  94. package/commands/summarize.md +2 -3
  95. package/commands/tdd.md +2 -7
  96. package/commands/worktree-add.md +208 -216
  97. package/commands/worktree-cleanup.md +172 -178
  98. package/hooks/api-workflow-check.py +5 -3
  99. package/hooks/enforce-component-type-confirm.py +97 -0
  100. package/hooks/lib/__init__.py +1 -0
  101. package/hooks/lib/greptile.py +355 -0
  102. package/hooks/lib/ntfy.py +209 -0
  103. package/hooks/notify-input-needed.py +73 -0
  104. package/hooks/notify-phase-complete.py +90 -0
  105. package/hooks/run-code-review.py +246 -0
  106. package/hooks/track-token-usage.py +121 -0
  107. package/package.json +13 -3
  108. package/scripts/collect-test-results.ts +102 -77
  109. package/scripts/extract-parameters.ts +112 -70
  110. package/scripts/generate-test-manifest.ts +118 -77
  111. package/templates/.env.example +57 -0
  112. package/templates/BRAND_GUIDE.md +92 -52
  113. package/templates/CLAUDE-SECTION.md +40 -37
  114. package/templates/SPEC.json +186 -38
  115. package/templates/api-dev-state.json +33 -4
  116. package/templates/api-showcase/_components/APICard.tsx +22 -18
  117. package/templates/api-showcase/_components/APIModal.tsx +110 -64
  118. package/templates/api-showcase/_components/APIShowcase.tsx +53 -35
  119. package/templates/api-showcase/_components/APITester.tsx +128 -67
  120. package/templates/api-showcase/page.tsx +4 -4
  121. package/templates/api-test/page.tsx +51 -30
  122. package/templates/api-test/test-structure/route.ts +43 -34
  123. package/templates/component/Component.stories.tsx +41 -39
  124. package/templates/component/Component.test.tsx +96 -78
  125. package/templates/component/Component.tsx +63 -52
  126. package/templates/component/Component.types.ts +10 -6
  127. package/templates/component/Component.visual.spec.ts +170 -0
  128. package/templates/component/index.ts +2 -2
  129. package/templates/dev-tools/_components/DevToolsLanding.tsx +8 -8
  130. package/templates/dev-tools/page.tsx +4 -3
  131. package/templates/mcp-servers.json +30 -2
  132. package/templates/page/page.e2e.test.ts +56 -48
  133. package/templates/page/page.tsx +3 -3
  134. package/templates/shared/HeroHeader.tsx +16 -15
  135. package/templates/shared/index.ts +1 -1
  136. package/templates/ui-showcase/_components/PreviewCard.tsx +20 -20
  137. package/templates/ui-showcase/_components/PreviewModal.tsx +149 -108
  138. package/templates/ui-showcase/_components/UIShowcase.tsx +43 -35
  139. package/templates/ui-showcase/page.tsx +4 -4
@@ -17,8 +17,8 @@
17
17
  * @generated by @hustle-together/api-dev-tools v3.0
18
18
  */
19
19
 
20
- import fs from 'fs';
21
- import path from 'path';
20
+ import fs from "fs";
21
+ import path from "path";
22
22
 
23
23
  // ============================================
24
24
  // Types
@@ -26,7 +26,7 @@ import path from 'path';
26
26
 
27
27
  interface ParameterDefinition {
28
28
  name: string;
29
- location: 'query' | 'body' | 'header' | 'path';
29
+ location: "query" | "body" | "header" | "path";
30
30
  type: string;
31
31
  required: boolean;
32
32
  description?: string;
@@ -62,7 +62,11 @@ interface ParameterMatrix {
62
62
  // File Discovery
63
63
  // ============================================
64
64
 
65
- function findFiles(baseDir: string, pattern: RegExp, exclude: string[] = ['node_modules', '.git', 'dist']): string[] {
65
+ function findFiles(
66
+ baseDir: string,
67
+ pattern: RegExp,
68
+ exclude: string[] = ["node_modules", ".git", "dist"],
69
+ ): string[] {
66
70
  const files: string[] = [];
67
71
 
68
72
  function walk(dir: string) {
@@ -120,7 +124,10 @@ function extractFromZodSchema(content: string): ParameterDefinition[] {
120
124
  return params;
121
125
  }
122
126
 
123
- function parseZodChain(name: string, chain: string): ParameterDefinition | null {
127
+ function parseZodChain(
128
+ name: string,
129
+ chain: string,
130
+ ): ParameterDefinition | null {
124
131
  // Determine base type
125
132
  const typeMatch = chain.match(/z\.(\w+)/);
126
133
  if (!typeMatch) return null;
@@ -129,13 +136,13 @@ function parseZodChain(name: string, chain: string): ParameterDefinition | null
129
136
 
130
137
  const param: ParameterDefinition = {
131
138
  name,
132
- location: 'body', // Default, will be refined based on context
139
+ location: "body", // Default, will be refined based on context
133
140
  type: mapZodType(baseType),
134
- required: true
141
+ required: true,
135
142
  };
136
143
 
137
144
  // Check for optional
138
- if (chain.includes('.optional()') || chain.includes('.nullable()')) {
145
+ if (chain.includes(".optional()") || chain.includes(".nullable()")) {
139
146
  param.required = false;
140
147
  }
141
148
 
@@ -151,18 +158,18 @@ function parseZodChain(name: string, chain: string): ParameterDefinition | null
151
158
  try {
152
159
  param.default = JSON.parse(defaultMatch[1].replace(/'/g, '"'));
153
160
  } catch {
154
- param.default = defaultMatch[1].replace(/['"]/g, '');
161
+ param.default = defaultMatch[1].replace(/['"]/g, "");
155
162
  }
156
163
  param.required = false; // Has default = not required
157
164
  }
158
165
 
159
166
  // Extract enum values
160
- if (baseType === 'enum') {
167
+ if (baseType === "enum") {
161
168
  const enumMatch = chain.match(/z\.enum\s*\(\s*\[([^\]]+)\]/);
162
169
  if (enumMatch) {
163
170
  param.enum = enumMatch[1]
164
- .split(',')
165
- .map(s => s.trim().replace(/['"`]/g, ''));
171
+ .split(",")
172
+ .map((s) => s.trim().replace(/['"`]/g, ""));
166
173
  }
167
174
  }
168
175
 
@@ -182,16 +189,16 @@ function parseZodChain(name: string, chain: string): ParameterDefinition | null
182
189
 
183
190
  function mapZodType(zodType: string): string {
184
191
  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'
192
+ string: "string",
193
+ number: "number",
194
+ boolean: "boolean",
195
+ array: "array",
196
+ object: "object",
197
+ enum: "enum",
198
+ literal: "literal",
199
+ union: "union",
200
+ date: "string (ISO date)",
201
+ coerce: "coerced",
195
202
  };
196
203
  return typeMap[zodType] || zodType;
197
204
  }
@@ -200,7 +207,10 @@ function mapZodType(zodType: string): string {
200
207
  // Route File Extractor
201
208
  // ============================================
202
209
 
203
- function extractFromRouteFile(content: string, existingParams: ParameterDefinition[]): ParameterDefinition[] {
210
+ function extractFromRouteFile(
211
+ content: string,
212
+ existingParams: ParameterDefinition[],
213
+ ): ParameterDefinition[] {
204
214
  const params: ParameterDefinition[] = [];
205
215
 
206
216
  // Extract query parameters from searchParams usage
@@ -209,12 +219,15 @@ function extractFromRouteFile(content: string, existingParams: ParameterDefiniti
209
219
  let match;
210
220
  while ((match = queryParamRegex.exec(content)) !== null) {
211
221
  const name = match[1];
212
- if (!existingParams.some(p => p.name === name) && !params.some(p => p.name === name)) {
222
+ if (
223
+ !existingParams.some((p) => p.name === name) &&
224
+ !params.some((p) => p.name === name)
225
+ ) {
213
226
  params.push({
214
227
  name,
215
- location: 'query',
216
- type: 'string',
217
- required: false // Query params are typically optional
228
+ location: "query",
229
+ type: "string",
230
+ required: false, // Query params are typically optional
218
231
  });
219
232
  }
220
233
  }
@@ -224,12 +237,15 @@ function extractFromRouteFile(content: string, existingParams: ParameterDefiniti
224
237
  const headerRegex = /headers\.get\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
225
238
  while ((match = headerRegex.exec(content)) !== null) {
226
239
  const name = match[1];
227
- if (!existingParams.some(p => p.name === name) && !params.some(p => p.name === name)) {
240
+ if (
241
+ !existingParams.some((p) => p.name === name) &&
242
+ !params.some((p) => p.name === name)
243
+ ) {
228
244
  params.push({
229
245
  name,
230
- location: 'header',
231
- type: 'string',
232
- required: false
246
+ location: "header",
247
+ type: "string",
248
+ required: false,
233
249
  });
234
250
  }
235
251
  }
@@ -239,12 +255,15 @@ function extractFromRouteFile(content: string, existingParams: ParameterDefiniti
239
255
  const pathParamRegex = /params\.(\w+)|{\s*(\w+)\s*}\s*=\s*params/g;
240
256
  while ((match = pathParamRegex.exec(content)) !== null) {
241
257
  const name = match[1] || match[2];
242
- if (!existingParams.some(p => p.name === name) && !params.some(p => p.name === name)) {
258
+ if (
259
+ !existingParams.some((p) => p.name === name) &&
260
+ !params.some((p) => p.name === name)
261
+ ) {
243
262
  params.push({
244
263
  name,
245
- location: 'path',
246
- type: 'string',
247
- required: true // Path params are always required
264
+ location: "path",
265
+ type: "string",
266
+ required: true, // Path params are always required
248
267
  });
249
268
  }
250
269
  }
@@ -268,7 +287,7 @@ function extractTestedParams(content: string): string[] {
268
287
  const bodyContent = match[1];
269
288
  const paramNames = bodyContent.match(/(\w+)\s*:/g);
270
289
  if (paramNames) {
271
- paramNames.forEach(p => testedParams.add(p.replace(':', '').trim()));
290
+ paramNames.forEach((p) => testedParams.add(p.replace(":", "").trim()));
272
291
  }
273
292
  }
274
293
 
@@ -292,7 +311,7 @@ function extractTestedParams(content: string): string[] {
292
311
  // ============================================
293
312
 
294
313
  function extractAllParameters(baseDir: string): ParameterMatrix {
295
- console.log('๐Ÿ” Scanning for parameter sources...');
314
+ console.log("๐Ÿ” Scanning for parameter sources...");
296
315
 
297
316
  // Find files
298
317
  const schemaFiles = findFiles(baseDir, /schema.*\.ts$|schemas?\/.*\.ts$/);
@@ -307,7 +326,7 @@ function extractAllParameters(baseDir: string): ParameterMatrix {
307
326
 
308
327
  // Process schema files
309
328
  for (const schemaFile of schemaFiles) {
310
- const content = fs.readFileSync(schemaFile, 'utf-8');
329
+ const content = fs.readFileSync(schemaFile, "utf-8");
311
330
  const params = extractFromZodSchema(content);
312
331
 
313
332
  if (params.length > 0) {
@@ -315,20 +334,25 @@ function extractAllParameters(baseDir: string): ParameterMatrix {
315
334
 
316
335
  // Determine endpoint from file path
317
336
  const pathParts = relativePath.split(path.sep);
318
- const apiIndex = pathParts.findIndex(p => p === 'api');
319
- let endpoint = '/api/unknown';
337
+ const apiIndex = pathParts.findIndex((p) => p === "api");
338
+ let endpoint = "/api/unknown";
320
339
 
321
340
  if (apiIndex >= 0) {
322
- endpoint = '/' + pathParts.slice(apiIndex).join('/').replace(/\/schema.*\.ts$/, '');
341
+ endpoint =
342
+ "/" +
343
+ pathParts
344
+ .slice(apiIndex)
345
+ .join("/")
346
+ .replace(/\/schema.*\.ts$/, "");
323
347
  }
324
348
 
325
349
  const existing = endpointsMap.get(endpoint) || {
326
350
  endpoint,
327
- method: 'POST',
351
+ method: "POST",
328
352
  parameters: [],
329
353
  sourceFiles: [],
330
354
  testedParameters: [],
331
- untestedParameters: []
355
+ untestedParameters: [],
332
356
  };
333
357
 
334
358
  existing.parameters.push(...params);
@@ -342,30 +366,33 @@ function extractAllParameters(baseDir: string): ParameterMatrix {
342
366
 
343
367
  // Process route files
344
368
  for (const routeFile of routeFiles) {
345
- const content = fs.readFileSync(routeFile, 'utf-8');
369
+ const content = fs.readFileSync(routeFile, "utf-8");
346
370
  const relativePath = path.relative(baseDir, routeFile);
347
371
 
348
372
  // Determine endpoint
349
373
  const pathParts = relativePath.split(path.sep);
350
- const apiIndex = pathParts.findIndex(p => p === 'api');
351
- let endpoint = '/api/unknown';
374
+ const apiIndex = pathParts.findIndex((p) => p === "api");
375
+ let endpoint = "/api/unknown";
352
376
 
353
377
  if (apiIndex >= 0) {
354
- endpoint = '/' + pathParts.slice(apiIndex, -1).join('/');
378
+ endpoint = "/" + pathParts.slice(apiIndex, -1).join("/");
355
379
  }
356
380
 
357
381
  const existing = endpointsMap.get(endpoint) || {
358
382
  endpoint,
359
- method: 'GET',
383
+ method: "GET",
360
384
  parameters: [],
361
385
  sourceFiles: [],
362
386
  testedParameters: [],
363
- untestedParameters: []
387
+ untestedParameters: [],
364
388
  };
365
389
 
366
390
  // Determine method
367
- if (content.includes('export async function POST') || content.includes('export const POST')) {
368
- existing.method = 'POST';
391
+ if (
392
+ content.includes("export async function POST") ||
393
+ content.includes("export const POST")
394
+ ) {
395
+ existing.method = "POST";
369
396
  }
370
397
 
371
398
  const routeParams = extractFromRouteFile(content, existing.parameters);
@@ -384,7 +411,7 @@ function extractAllParameters(baseDir: string): ParameterMatrix {
384
411
 
385
412
  // Process test files to find tested parameters
386
413
  for (const testFile of testFiles) {
387
- const content = fs.readFileSync(testFile, 'utf-8');
414
+ const content = fs.readFileSync(testFile, "utf-8");
388
415
  const relativePath = path.relative(baseDir, testFile);
389
416
 
390
417
  // Determine endpoint from test file
@@ -396,7 +423,9 @@ function extractAllParameters(baseDir: string): ParameterMatrix {
396
423
 
397
424
  if (existing) {
398
425
  const testedParams = extractTestedParams(content);
399
- existing.testedParameters = [...new Set([...existing.testedParameters, ...testedParams])];
426
+ existing.testedParameters = [
427
+ ...new Set([...existing.testedParameters, ...testedParams]),
428
+ ];
400
429
 
401
430
  console.log(`\n๐Ÿงช Test: ${relativePath}`);
402
431
  console.log(` Found ${testedParams.length} tested parameters`);
@@ -407,9 +436,11 @@ function extractAllParameters(baseDir: string): ParameterMatrix {
407
436
  let totalParams = 0;
408
437
  let testedParams = 0;
409
438
 
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));
439
+ const endpoints = Array.from(endpointsMap.values()).map((ep) => {
440
+ const allParamNames = ep.parameters.map((p) => p.name);
441
+ ep.untestedParameters = allParamNames.filter(
442
+ (p) => !ep.testedParameters.includes(p),
443
+ );
413
444
 
414
445
  totalParams += ep.parameters.length;
415
446
  testedParams += ep.testedParameters.length;
@@ -418,14 +449,15 @@ function extractAllParameters(baseDir: string): ParameterMatrix {
418
449
  });
419
450
 
420
451
  return {
421
- version: '3.0.0',
452
+ version: "3.0.0",
422
453
  generatedAt: new Date().toISOString(),
423
454
  endpoints,
424
455
  coverage: {
425
456
  totalParameters: totalParams,
426
457
  testedParameters: testedParams,
427
- coveragePercent: totalParams > 0 ? Math.round((testedParams / totalParams) * 100) : 0
428
- }
458
+ coveragePercent:
459
+ totalParams > 0 ? Math.round((testedParams / totalParams) * 100) : 0,
460
+ },
429
461
  };
430
462
  }
431
463
 
@@ -436,12 +468,18 @@ function extractAllParameters(baseDir: string): ParameterMatrix {
436
468
  function main() {
437
469
  const args = process.argv.slice(2);
438
470
  const baseDir = args[0] || process.cwd();
439
- const outputPath = args[1] || path.join(baseDir, 'src', 'app', 'api-test', 'parameter-matrix.json');
471
+ const outputPath =
472
+ args[1] ||
473
+ path.join(baseDir, "src", "app", "api-test", "parameter-matrix.json");
440
474
 
441
- console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•');
442
- console.log(' ๐Ÿ“Š Parameter Matrix Extractor');
443
- console.log(' @hustle-together/api-dev-tools v3.0');
444
- console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•');
475
+ console.log(
476
+ "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•",
477
+ );
478
+ console.log(" ๐Ÿ“Š Parameter Matrix Extractor");
479
+ console.log(" @hustle-together/api-dev-tools v3.0");
480
+ console.log(
481
+ "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•",
482
+ );
445
483
  console.log(`\n๐Ÿ“ Base directory: ${baseDir}`);
446
484
  console.log(`๐Ÿ“„ Output file: ${outputPath}\n`);
447
485
 
@@ -456,22 +494,26 @@ function main() {
456
494
  // Write matrix
457
495
  fs.writeFileSync(outputPath, JSON.stringify(matrix, null, 2));
458
496
 
459
- console.log('\nโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•');
460
- console.log(' โœ… Parameter matrix generated successfully!');
461
- console.log('โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•');
497
+ console.log(
498
+ "\nโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•",
499
+ );
500
+ console.log(" โœ… Parameter matrix generated successfully!");
501
+ console.log(
502
+ "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•",
503
+ );
462
504
  console.log(`\n๐Ÿ“Š Coverage Summary:`);
463
505
  console.log(` โ€ข Total parameters: ${matrix.coverage.totalParameters}`);
464
506
  console.log(` โ€ข Tested parameters: ${matrix.coverage.testedParameters}`);
465
507
  console.log(` โ€ข Coverage: ${matrix.coverage.coveragePercent}%`);
466
508
 
467
509
  // List untested parameters
468
- const untested = matrix.endpoints.flatMap(ep =>
469
- ep.untestedParameters.map(p => `${ep.endpoint}: ${p}`)
510
+ const untested = matrix.endpoints.flatMap((ep) =>
511
+ ep.untestedParameters.map((p) => `${ep.endpoint}: ${p}`),
470
512
  );
471
513
 
472
514
  if (untested.length > 0) {
473
515
  console.log(`\nโš ๏ธ Untested parameters (${untested.length}):`);
474
- untested.slice(0, 10).forEach(p => console.log(` โ€ข ${p}`));
516
+ untested.slice(0, 10).forEach((p) => console.log(` โ€ข ${p}`));
475
517
  if (untested.length > 10) {
476
518
  console.log(` ... and ${untested.length - 10} more`);
477
519
  }