@hustle-together/api-dev-tools 3.10.1 → 3.12.1

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 (178) 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 +331 -0
  9. package/.claude/commands/README.md +196 -0
  10. package/.claude/commands/add-command.md +212 -0
  11. package/.claude/commands/api-create.md +510 -0
  12. package/.claude/commands/api-env.md +51 -0
  13. package/.claude/commands/api-interview.md +344 -0
  14. package/.claude/commands/api-research.md +357 -0
  15. package/.claude/commands/api-status.md +279 -0
  16. package/.claude/commands/api-verify.md +232 -0
  17. package/.claude/commands/beepboop.md +96 -0
  18. package/.claude/commands/busycommit.md +111 -0
  19. package/.claude/commands/commit.md +82 -0
  20. package/.claude/commands/cycle.md +137 -0
  21. package/.claude/commands/gap.md +85 -0
  22. package/.claude/commands/green.md +137 -0
  23. package/.claude/commands/issue.md +187 -0
  24. package/.claude/commands/ntfy-setup.md +91 -0
  25. package/.claude/commands/ntfy-test.md +74 -0
  26. package/.claude/commands/plan.md +167 -0
  27. package/.claude/commands/pr.md +121 -0
  28. package/.claude/commands/publish.md +40 -0
  29. package/.claude/commands/red.md +137 -0
  30. package/.claude/commands/refactor.md +137 -0
  31. package/.claude/commands/spike.md +137 -0
  32. package/.claude/commands/summarize.md +93 -0
  33. package/.claude/commands/tdd.md +139 -0
  34. package/.claude/commands/worktree-add.md +307 -0
  35. package/.claude/commands/worktree-cleanup.md +275 -0
  36. package/.claude/hooks/api-workflow-check.py +227 -0
  37. package/.claude/hooks/enforce-deep-research.py +185 -0
  38. package/.claude/hooks/enforce-disambiguation.py +155 -0
  39. package/.claude/hooks/enforce-documentation.py +192 -0
  40. package/.claude/hooks/enforce-environment.py +253 -0
  41. package/.claude/hooks/enforce-external-research.py +328 -0
  42. package/.claude/hooks/enforce-interview.py +421 -0
  43. package/.claude/hooks/enforce-refactor.py +189 -0
  44. package/.claude/hooks/enforce-research.py +159 -0
  45. package/.claude/hooks/enforce-schema.py +186 -0
  46. package/.claude/hooks/enforce-scope.py +160 -0
  47. package/.claude/hooks/enforce-tdd-red.py +250 -0
  48. package/.claude/hooks/enforce-verify.py +186 -0
  49. package/.claude/hooks/periodic-reground.py +154 -0
  50. package/.claude/hooks/session-startup.py +151 -0
  51. package/.claude/hooks/track-tool-use.py +626 -0
  52. package/.claude/hooks/verify-after-green.py +282 -0
  53. package/.claude/hooks/verify-implementation.py +225 -0
  54. package/.claude/research/index.json +6 -0
  55. package/.claude/settings.json +144 -0
  56. package/.claude/settings.local.json +12 -0
  57. package/.claude-plugin/marketplace.json +103 -0
  58. package/.skills/README.md +293 -0
  59. package/.skills/_shared/convert-commands.py +192 -0
  60. package/.skills/_shared/hooks/api-workflow-check.py +227 -0
  61. package/.skills/_shared/hooks/enforce-deep-research.py +185 -0
  62. package/.skills/_shared/hooks/enforce-disambiguation.py +155 -0
  63. package/.skills/_shared/hooks/enforce-documentation.py +192 -0
  64. package/.skills/_shared/hooks/enforce-environment.py +253 -0
  65. package/.skills/_shared/hooks/enforce-external-research.py +328 -0
  66. package/.skills/_shared/hooks/enforce-interview.py +421 -0
  67. package/.skills/_shared/hooks/enforce-refactor.py +189 -0
  68. package/.skills/_shared/hooks/enforce-research.py +159 -0
  69. package/.skills/_shared/hooks/enforce-schema.py +186 -0
  70. package/.skills/_shared/hooks/enforce-scope.py +160 -0
  71. package/.skills/_shared/hooks/enforce-tdd-red.py +250 -0
  72. package/.skills/_shared/hooks/enforce-verify.py +186 -0
  73. package/.skills/_shared/hooks/periodic-reground.py +154 -0
  74. package/.skills/_shared/hooks/session-startup.py +151 -0
  75. package/.skills/_shared/hooks/track-tool-use.py +626 -0
  76. package/.skills/_shared/hooks/verify-after-green.py +282 -0
  77. package/.skills/_shared/hooks/verify-implementation.py +225 -0
  78. package/.skills/_shared/install.sh +114 -0
  79. package/.skills/_shared/settings.json +93 -0
  80. package/.skills/add-command/SKILL.md +227 -0
  81. package/.skills/api-create/SKILL.md +623 -0
  82. package/.skills/api-env/SKILL.md +64 -0
  83. package/.skills/api-interview/SKILL.md +357 -0
  84. package/.skills/api-research/SKILL.md +370 -0
  85. package/.skills/api-status/SKILL.md +292 -0
  86. package/.skills/api-verify/SKILL.md +245 -0
  87. package/.skills/beepboop/SKILL.md +111 -0
  88. package/.skills/busycommit/SKILL.md +126 -0
  89. package/.skills/commit/SKILL.md +97 -0
  90. package/.skills/cycle/SKILL.md +152 -0
  91. package/.skills/gap/SKILL.md +100 -0
  92. package/.skills/green/SKILL.md +152 -0
  93. package/.skills/issue/SKILL.md +202 -0
  94. package/.skills/plan/SKILL.md +182 -0
  95. package/.skills/pr/SKILL.md +136 -0
  96. package/.skills/publish/SKILL.md +160 -0
  97. package/.skills/red/SKILL.md +152 -0
  98. package/.skills/refactor/SKILL.md +152 -0
  99. package/.skills/spike/SKILL.md +152 -0
  100. package/.skills/summarize/SKILL.md +108 -0
  101. package/.skills/tdd/SKILL.md +154 -0
  102. package/.skills/update-todos/SKILL.md +250 -0
  103. package/.skills/worktree-add/SKILL.md +322 -0
  104. package/.skills/worktree-cleanup/SKILL.md +290 -0
  105. package/CHANGELOG.md +115 -0
  106. package/README.md +161 -7101
  107. package/bin/cli.js +448 -805
  108. package/commands/README.md +66 -31
  109. package/commands/add-command.md +8 -5
  110. package/commands/beepboop.md +4 -5
  111. package/commands/busycommit.md +2 -3
  112. package/commands/commit.md +2 -3
  113. package/commands/cycle.md +2 -7
  114. package/commands/gap.md +2 -3
  115. package/commands/green.md +2 -7
  116. package/commands/hustle-api-continue.md +8 -5
  117. package/commands/hustle-api-create.md +70 -29
  118. package/commands/hustle-api-env.md +1 -0
  119. package/commands/hustle-api-interview.md +32 -19
  120. package/commands/hustle-api-research.md +47 -21
  121. package/commands/hustle-api-sessions.md +8 -7
  122. package/commands/hustle-api-status.md +21 -1
  123. package/commands/hustle-api-verify.md +14 -13
  124. package/commands/hustle-combine.md +488 -241
  125. package/commands/hustle-ui-create-page.md +113 -50
  126. package/commands/hustle-ui-create.md +179 -26
  127. package/commands/issue.md +3 -8
  128. package/commands/plan.md +2 -3
  129. package/commands/pr.md +2 -3
  130. package/commands/red.md +2 -7
  131. package/commands/refactor.md +2 -7
  132. package/commands/spike.md +2 -7
  133. package/commands/summarize.md +2 -3
  134. package/commands/tdd.md +2 -7
  135. package/commands/worktree-add.md +208 -216
  136. package/commands/worktree-cleanup.md +172 -178
  137. package/hooks/api-workflow-check.py +5 -3
  138. package/hooks/enforce-component-type-confirm.py +97 -0
  139. package/hooks/lib/__init__.py +1 -0
  140. package/hooks/lib/greptile.py +355 -0
  141. package/hooks/lib/ntfy.py +209 -0
  142. package/hooks/notify-input-needed.py +73 -0
  143. package/hooks/notify-phase-complete.py +90 -0
  144. package/hooks/run-code-review.py +246 -0
  145. package/hooks/track-token-usage.py +121 -0
  146. package/package.json +33 -12
  147. package/scripts/collect-test-results.ts +102 -77
  148. package/scripts/extract-parameters.ts +112 -70
  149. package/scripts/generate-test-manifest.ts +118 -77
  150. package/templates/.env.example +57 -0
  151. package/templates/BRAND_GUIDE.md +92 -52
  152. package/templates/CLAUDE-SECTION.md +40 -37
  153. package/templates/SPEC.json +186 -38
  154. package/templates/api-dev-state.json +33 -4
  155. package/templates/api-showcase/_components/APICard.tsx +22 -18
  156. package/templates/api-showcase/_components/APIModal.tsx +110 -64
  157. package/templates/api-showcase/_components/APIShowcase.tsx +53 -35
  158. package/templates/api-showcase/_components/APITester.tsx +128 -67
  159. package/templates/api-showcase/page.tsx +4 -4
  160. package/templates/api-test/page.tsx +51 -30
  161. package/templates/api-test/test-structure/route.ts +43 -34
  162. package/templates/component/Component.stories.tsx +41 -39
  163. package/templates/component/Component.test.tsx +96 -78
  164. package/templates/component/Component.tsx +63 -52
  165. package/templates/component/Component.types.ts +10 -6
  166. package/templates/component/Component.visual.spec.ts +170 -0
  167. package/templates/component/index.ts +2 -2
  168. package/templates/dev-tools/_components/DevToolsLanding.tsx +8 -8
  169. package/templates/dev-tools/page.tsx +4 -3
  170. package/templates/mcp-servers.json +30 -2
  171. package/templates/page/page.e2e.test.ts +56 -48
  172. package/templates/page/page.tsx +3 -3
  173. package/templates/shared/HeroHeader.tsx +16 -15
  174. package/templates/shared/index.ts +1 -1
  175. package/templates/ui-showcase/_components/PreviewCard.tsx +20 -20
  176. package/templates/ui-showcase/_components/PreviewModal.tsx +149 -108
  177. package/templates/ui-showcase/_components/UIShowcase.tsx +43 -35
  178. package/templates/ui-showcase/page.tsx +4 -4
@@ -1,6 +1,6 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
- import { useState, useEffect } from 'react';
3
+ import { useState, useEffect } from "react";
4
4
 
5
5
  interface ParameterDoc {
6
6
  name: string;
@@ -45,15 +45,15 @@ interface ResponseState {
45
45
  // Default request bodies for known APIs
46
46
  const DEFAULT_BODIES: Record<string, Record<string, object>> = {
47
47
  brandfetch: {
48
- default: { domain: 'stripe.com' },
48
+ default: { domain: "stripe.com" },
49
49
  },
50
50
  elevenlabs: {
51
51
  tts: {
52
- text: 'Hello, this is a test of the ElevenLabs text-to-speech API.',
53
- voiceId: '21m00Tcm4TlvDq8ikWAM',
54
- modelId: 'eleven_multilingual_v2',
55
- outputFormat: 'mp3_44100_128',
56
- responseFormat: 'json',
52
+ text: "Hello, this is a test of the ElevenLabs text-to-speech API.",
53
+ voiceId: "21m00Tcm4TlvDq8ikWAM",
54
+ modelId: "eleven_multilingual_v2",
55
+ outputFormat: "mp3_44100_128",
56
+ responseFormat: "json",
57
57
  },
58
58
  voices: {},
59
59
  models: {},
@@ -63,8 +63,8 @@ const DEFAULT_BODIES: Record<string, Record<string, object>> = {
63
63
  // Default query params for GET requests
64
64
  const DEFAULT_QUERY_PARAMS: Record<string, Record<string, string>> = {
65
65
  elevenlabs: {
66
- voices: 'search=&pageSize=10',
67
- models: '',
66
+ voices: "search=&pageSize=10",
67
+ models: "",
68
68
  },
69
69
  };
70
70
 
@@ -82,12 +82,19 @@ const DEFAULT_QUERY_PARAMS: Record<string, Record<string, string>> = {
82
82
  *
83
83
  * Created with Hustle API Dev Tools (v3.9.2)
84
84
  */
85
- export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath, schema }: APITesterProps) {
85
+ export function APITester({
86
+ id,
87
+ endpoint,
88
+ methods,
89
+ selectedEndpoint,
90
+ schemaPath,
91
+ schema,
92
+ }: APITesterProps) {
86
93
  // Get default body for this API/endpoint
87
94
  const getDefaultBody = () => {
88
95
  const apiDefaults = DEFAULT_BODIES[id];
89
96
  if (apiDefaults) {
90
- const endpointDefaults = apiDefaults[selectedEndpoint || 'default'];
97
+ const endpointDefaults = apiDefaults[selectedEndpoint || "default"];
91
98
  if (endpointDefaults && Object.keys(endpointDefaults).length > 0) {
92
99
  return JSON.stringify(endpointDefaults, null, 2);
93
100
  }
@@ -99,24 +106,24 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
99
106
  const getDefaultQueryParams = () => {
100
107
  const apiParams = DEFAULT_QUERY_PARAMS[id];
101
108
  if (apiParams && selectedEndpoint) {
102
- return apiParams[selectedEndpoint] || '';
109
+ return apiParams[selectedEndpoint] || "";
103
110
  }
104
- return '';
111
+ return "";
105
112
  };
106
113
 
107
114
  const [request, setRequest] = useState<RequestState>({
108
- method: methods[0] || 'POST',
115
+ method: methods[0] || "POST",
109
116
  body: getDefaultBody(),
110
117
  queryParams: getDefaultQueryParams(),
111
118
  headers: {
112
- 'Content-Type': 'application/json',
119
+ "Content-Type": "application/json",
113
120
  },
114
121
  });
115
122
 
116
123
  const [response, setResponse] = useState<ResponseState>({
117
124
  status: null,
118
- statusText: '',
119
- body: '',
125
+ statusText: "",
126
+ body: "",
120
127
  time: null,
121
128
  error: null,
122
129
  contentType: null,
@@ -129,26 +136,33 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
129
136
  useEffect(() => {
130
137
  setRequest((prev) => ({
131
138
  ...prev,
132
- method: methods[0] || 'POST',
139
+ method: methods[0] || "POST",
133
140
  body: getDefaultBody(),
134
141
  queryParams: getDefaultQueryParams(),
135
142
  }));
136
143
  // Clear previous response
137
144
  setResponse({
138
145
  status: null,
139
- statusText: '',
140
- body: '',
146
+ statusText: "",
147
+ body: "",
141
148
  time: null,
142
149
  error: null,
143
150
  contentType: null,
144
151
  });
145
152
  setAudioUrl(null);
146
- // eslint-disable-next-line react-hooks/exhaustive-deps
153
+ // eslint-disable-next-line react-hooks/exhaustive-deps
147
154
  }, [selectedEndpoint, id]);
148
155
 
149
156
  const handleSubmit = async () => {
150
157
  setIsLoading(true);
151
- setResponse({ status: null, statusText: '', body: '', time: null, error: null, contentType: null });
158
+ setResponse({
159
+ status: null,
160
+ statusText: "",
161
+ body: "",
162
+ time: null,
163
+ error: null,
164
+ contentType: null,
165
+ });
152
166
  setAudioUrl(null);
153
167
 
154
168
  const startTime = performance.now();
@@ -156,7 +170,7 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
156
170
  try {
157
171
  // Build URL with query params for GET
158
172
  let url = endpoint;
159
- if (request.method === 'GET' && request.queryParams.trim()) {
173
+ if (request.method === "GET" && request.queryParams.trim()) {
160
174
  url = `${endpoint}?${request.queryParams}`;
161
175
  }
162
176
 
@@ -166,31 +180,34 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
166
180
  };
167
181
 
168
182
  // Add body for non-GET requests
169
- if (request.method !== 'GET' && request.body.trim()) {
183
+ if (request.method !== "GET" && request.body.trim()) {
170
184
  fetchOptions.body = request.body;
171
185
  }
172
186
 
173
187
  const res = await fetch(url, fetchOptions);
174
188
  const endTime = performance.now();
175
189
 
176
- const contentType = res.headers.get('content-type') || '';
177
- let responseBody = '';
190
+ const contentType = res.headers.get("content-type") || "";
191
+ let responseBody = "";
178
192
 
179
193
  // Handle different content types
180
- if (contentType.includes('audio/') || contentType.includes('application/octet-stream')) {
194
+ if (
195
+ contentType.includes("audio/") ||
196
+ contentType.includes("application/octet-stream")
197
+ ) {
181
198
  // Binary audio response
182
199
  const blob = await res.blob();
183
200
  const url = URL.createObjectURL(blob);
184
201
  setAudioUrl(url);
185
202
  responseBody = `[Audio Response - ${blob.size} bytes]\nContent-Type: ${contentType}`;
186
- } else if (contentType.includes('application/json')) {
203
+ } else if (contentType.includes("application/json")) {
187
204
  const json = await res.json();
188
205
  responseBody = JSON.stringify(json, null, 2);
189
206
 
190
207
  // Check if JSON contains base64 audio
191
- if (json.audio && typeof json.audio === 'string') {
208
+ if (json.audio && typeof json.audio === "string") {
192
209
  try {
193
- const format = json.format || 'mp3';
210
+ const format = json.format || "mp3";
194
211
  const audioData = atob(json.audio);
195
212
  const bytes = new Uint8Array(audioData.length);
196
213
  for (let i = 0; i < audioData.length; i++) {
@@ -219,10 +236,11 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
219
236
  const endTime = performance.now();
220
237
  setResponse({
221
238
  status: null,
222
- statusText: '',
223
- body: '',
239
+ statusText: "",
240
+ body: "",
224
241
  time: Math.round(endTime - startTime),
225
- error: error instanceof Error ? error.message : 'Unknown error occurred',
242
+ error:
243
+ error instanceof Error ? error.message : "Unknown error occurred",
226
244
  contentType: null,
227
245
  });
228
246
  } finally {
@@ -231,22 +249,26 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
231
249
  };
232
250
 
233
251
  const getStatusColor = (status: number | null) => {
234
- if (!status) return 'text-gray-500';
235
- if (status >= 200 && status < 300) return 'text-green-500';
236
- if (status >= 400 && status < 500) return 'text-yellow-500';
237
- if (status >= 500) return 'text-red-500';
238
- return 'text-gray-500';
252
+ if (!status) return "text-gray-500";
253
+ if (status >= 200 && status < 300) return "text-green-500";
254
+ if (status >= 400 && status < 500) return "text-yellow-500";
255
+ if (status >= 500) return "text-red-500";
256
+ return "text-gray-500";
239
257
  };
240
258
 
241
259
  return (
242
260
  <div className="grid gap-6 lg:grid-cols-2">
243
261
  {/* Request Panel */}
244
262
  <div className="space-y-4">
245
- <h3 className="text-lg font-bold text-black dark:text-white">Request</h3>
263
+ <h3 className="text-lg font-bold text-black dark:text-white">
264
+ Request
265
+ </h3>
246
266
 
247
267
  {/* Method Selection */}
248
268
  <div>
249
- <label className="mb-1 block text-sm font-bold text-black dark:text-white">Method</label>
269
+ <label className="mb-1 block text-sm font-bold text-black dark:text-white">
270
+ Method
271
+ </label>
250
272
  <div className="flex gap-2">
251
273
  {methods.map((method) => (
252
274
  <button
@@ -254,8 +276,8 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
254
276
  onClick={() => setRequest((prev) => ({ ...prev, method }))}
255
277
  className={`border-2 px-4 py-2 text-sm font-medium transition-colors ${
256
278
  request.method === method
257
- ? 'border-[#BA0C2F] bg-[#BA0C2F] text-white'
258
- : 'border-black bg-white text-black hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-800 dark:text-white'
279
+ ? "border-[#BA0C2F] bg-[#BA0C2F] text-white"
280
+ : "border-black bg-white text-black hover:border-[#BA0C2F] dark:border-gray-600 dark:bg-gray-800 dark:text-white"
259
281
  }`}
260
282
  >
261
283
  {method}
@@ -266,20 +288,28 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
266
288
 
267
289
  {/* Endpoint Display */}
268
290
  <div>
269
- <label className="mb-1 block text-sm font-bold text-black dark:text-white">Endpoint</label>
291
+ <label className="mb-1 block text-sm font-bold text-black dark:text-white">
292
+ Endpoint
293
+ </label>
270
294
  <div className="flex items-center border-2 border-black bg-gray-50 px-3 py-2 dark:border-gray-600 dark:bg-gray-800">
271
- <span className="font-mono text-sm text-gray-700 dark:text-gray-300">{endpoint}</span>
295
+ <span className="font-mono text-sm text-gray-700 dark:text-gray-300">
296
+ {endpoint}
297
+ </span>
272
298
  </div>
273
299
  </div>
274
300
 
275
301
  {/* Query Parameters (for GET requests) */}
276
- {request.method === 'GET' && (
302
+ {request.method === "GET" && (
277
303
  <div>
278
- <label className="mb-1 block text-sm font-bold text-black dark:text-white">Query Parameters</label>
304
+ <label className="mb-1 block text-sm font-bold text-black dark:text-white">
305
+ Query Parameters
306
+ </label>
279
307
  <input
280
308
  type="text"
281
309
  value={request.queryParams}
282
- onChange={(e) => setRequest((prev) => ({ ...prev, queryParams: e.target.value }))}
310
+ onChange={(e) =>
311
+ setRequest((prev) => ({ ...prev, queryParams: e.target.value }))
312
+ }
283
313
  className="w-full border-2 border-black bg-white px-3 py-2 font-mono text-sm focus:border-[#BA0C2F] focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
284
314
  placeholder="key1=value1&key2=value2"
285
315
  />
@@ -290,12 +320,16 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
290
320
  )}
291
321
 
292
322
  {/* Body Editor (hide for GET) */}
293
- {request.method !== 'GET' && (
323
+ {request.method !== "GET" && (
294
324
  <div>
295
325
  <div className="mb-1 flex items-center justify-between">
296
- <label className="block text-sm font-bold text-black dark:text-white">Body (JSON)</label>
326
+ <label className="block text-sm font-bold text-black dark:text-white">
327
+ Body (JSON)
328
+ </label>
297
329
  <button
298
- onClick={() => setRequest((prev) => ({ ...prev, body: getDefaultBody() }))}
330
+ onClick={() =>
331
+ setRequest((prev) => ({ ...prev, body: getDefaultBody() }))
332
+ }
299
333
  className="text-xs text-gray-600 hover:text-[#BA0C2F] dark:text-gray-400"
300
334
  >
301
335
  Reset to defaults
@@ -303,7 +337,9 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
303
337
  </div>
304
338
  <textarea
305
339
  value={request.body}
306
- onChange={(e) => setRequest((prev) => ({ ...prev, body: e.target.value }))}
340
+ onChange={(e) =>
341
+ setRequest((prev) => ({ ...prev, body: e.target.value }))
342
+ }
307
343
  className="h-48 w-full border-2 border-black bg-white p-3 font-mono text-sm focus:border-[#BA0C2F] focus:outline-none dark:border-gray-600 dark:bg-gray-800 dark:text-white"
308
344
  placeholder='{"key": "value"}'
309
345
  />
@@ -315,18 +351,24 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
315
351
  <ParameterDocs
316
352
  requestParams={schema.request}
317
353
  queryParams={schema.queryParams}
318
- isGetRequest={request.method === 'GET'}
354
+ isGetRequest={request.method === "GET"}
319
355
  />
320
356
  ) : null}
321
357
 
322
358
  {/* Headers */}
323
359
  <div>
324
- <label className="mb-1 block text-sm font-bold text-black dark:text-white">Headers</label>
360
+ <label className="mb-1 block text-sm font-bold text-black dark:text-white">
361
+ Headers
362
+ </label>
325
363
  <div className="border-2 border-black bg-gray-50 p-3 dark:border-gray-600 dark:bg-gray-800">
326
364
  {Object.entries(request.headers).map(([key, value]) => (
327
365
  <div key={key} className="flex items-center gap-2 text-sm">
328
- <span className="font-bold text-black dark:text-white">{key}:</span>
329
- <span className="text-gray-600 dark:text-gray-400">{value}</span>
366
+ <span className="font-bold text-black dark:text-white">
367
+ {key}:
368
+ </span>
369
+ <span className="text-gray-600 dark:text-gray-400">
370
+ {value}
371
+ </span>
330
372
  </div>
331
373
  ))}
332
374
  </div>
@@ -374,33 +416,45 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
374
416
  {/* Response Panel */}
375
417
  <div className="space-y-4">
376
418
  <div className="flex items-center justify-between">
377
- <h3 className="text-lg font-bold text-black dark:text-white">Response</h3>
419
+ <h3 className="text-lg font-bold text-black dark:text-white">
420
+ Response
421
+ </h3>
378
422
  {response.time !== null && (
379
- <span className="text-sm text-gray-600 dark:text-gray-400">{response.time}ms</span>
423
+ <span className="text-sm text-gray-600 dark:text-gray-400">
424
+ {response.time}ms
425
+ </span>
380
426
  )}
381
427
  </div>
382
428
 
383
429
  {/* Status */}
384
430
  {response.status !== null && (
385
431
  <div className="flex items-center gap-2">
386
- <span className={`text-2xl font-bold ${getStatusColor(response.status)}`}>
432
+ <span
433
+ className={`text-2xl font-bold ${getStatusColor(response.status)}`}
434
+ >
387
435
  {response.status}
388
436
  </span>
389
- <span className="text-gray-600 dark:text-gray-400">{response.statusText}</span>
437
+ <span className="text-gray-600 dark:text-gray-400">
438
+ {response.statusText}
439
+ </span>
390
440
  </div>
391
441
  )}
392
442
 
393
443
  {/* Error */}
394
444
  {response.error && (
395
445
  <div className="border-2 border-red-600 bg-red-50 p-4 dark:bg-red-900/20">
396
- <p className="text-sm text-red-800 dark:text-red-300">{response.error}</p>
446
+ <p className="text-sm text-red-800 dark:text-red-300">
447
+ {response.error}
448
+ </p>
397
449
  </div>
398
450
  )}
399
451
 
400
452
  {/* Audio Player */}
401
453
  {audioUrl && (
402
454
  <div className="border-2 border-black bg-gray-50 p-4 dark:border-gray-600 dark:bg-gray-800">
403
- <p className="mb-2 text-sm font-bold text-black dark:text-white">Audio Response</p>
455
+ <p className="mb-2 text-sm font-bold text-black dark:text-white">
456
+ Audio Response
457
+ </p>
404
458
  <audio controls className="w-full" src={audioUrl}>
405
459
  Your browser does not support the audio element.
406
460
  </audio>
@@ -423,7 +477,9 @@ export function APITester({ id, endpoint, methods, selectedEndpoint, schemaPath,
423
477
  ) : (
424
478
  <div className="flex h-48 items-center justify-center border-2 border-dashed border-black dark:border-gray-600">
425
479
  <p className="text-sm text-gray-600 dark:text-gray-400">
426
- {isLoading ? 'Waiting for response...' : 'Send a request to see the response'}
480
+ {isLoading
481
+ ? "Waiting for response..."
482
+ : "Send a request to see the response"}
427
483
  </p>
428
484
  </div>
429
485
  )}
@@ -457,7 +513,7 @@ function ParameterDocs({
457
513
  className="flex w-full items-center justify-between bg-gray-50 px-3 py-2 text-left dark:bg-gray-800"
458
514
  >
459
515
  <span className="text-sm font-bold text-black dark:text-white">
460
- {isGetRequest ? 'Query Parameters' : 'Request Body'} Documentation
516
+ {isGetRequest ? "Query Parameters" : "Request Body"} Documentation
461
517
  </span>
462
518
  <svg
463
519
  xmlns="http://www.w3.org/2000/svg"
@@ -469,7 +525,7 @@ function ParameterDocs({
469
525
  strokeWidth="2"
470
526
  strokeLinecap="round"
471
527
  strokeLinejoin="round"
472
- className={`text-gray-500 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
528
+ className={`text-gray-500 transition-transform ${isExpanded ? "rotate-180" : ""}`}
473
529
  >
474
530
  <polyline points="6 9 12 15 18 9" />
475
531
  </svg>
@@ -480,7 +536,9 @@ function ParameterDocs({
480
536
  {paramsToShow.map((param) => (
481
537
  <div key={param.name} className="px-3 py-2">
482
538
  <div className="flex items-center gap-2">
483
- <code className="text-sm font-bold text-[#BA0C2F]">{param.name}</code>
539
+ <code className="text-sm font-bold text-[#BA0C2F]">
540
+ {param.name}
541
+ </code>
484
542
  <span className="border border-gray-300 bg-gray-100 px-1.5 py-0.5 text-xs text-gray-600 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400">
485
543
  {param.type}
486
544
  </span>
@@ -510,7 +568,10 @@ function ParameterDocs({
510
568
  )}
511
569
  {param.default !== undefined && (
512
570
  <p className="mt-1 text-xs text-gray-500">
513
- Default: <code className="text-gray-700 dark:text-gray-300">{String(param.default)}</code>
571
+ Default:{" "}
572
+ <code className="text-gray-700 dark:text-gray-300">
573
+ {String(param.default)}
574
+ </code>
514
575
  </p>
515
576
  )}
516
577
  </div>
@@ -1,7 +1,7 @@
1
- 'use client';
1
+ "use client";
2
2
 
3
- import { HeroHeader } from '../shared/HeroHeader';
4
- import { APIShowcase } from './_components/APIShowcase';
3
+ import { HeroHeader } from "../shared/HeroHeader";
4
+ import { APIShowcase } from "./_components/APIShowcase";
5
5
 
6
6
  /**
7
7
  * API Showcase Page
@@ -27,7 +27,7 @@ export default function APIShowcasePage() {
27
27
  badge="API Documentation"
28
28
  description={
29
29
  <>
30
- Interactive testing and documentation for all{' '}
30
+ Interactive testing and documentation for all{" "}
31
31
  <strong>Hustle</strong> API endpoints.
32
32
  </>
33
33
  }
@@ -7,9 +7,9 @@
7
7
  * @generated by @hustle-together/api-dev-tools v3.0
8
8
  */
9
9
 
10
- 'use client';
10
+ "use client";
11
11
 
12
- import { useEffect, useState } from 'react';
12
+ import { useEffect, useState } from "react";
13
13
 
14
14
  // ============================================
15
15
  // Types (mirror the API types)
@@ -18,7 +18,7 @@ import { useEffect, useState } from 'react';
18
18
  interface TestCase {
19
19
  name: string;
20
20
  line: number;
21
- status: 'pending' | 'passed' | 'failed' | 'skipped';
21
+ status: "pending" | "passed" | "failed" | "skipped";
22
22
  duration?: number;
23
23
  error?: string;
24
24
  }
@@ -53,29 +53,37 @@ interface TestStructure {
53
53
  // Components
54
54
  // ============================================
55
55
 
56
- function StatusBadge({ status }: { status: TestCase['status'] }) {
56
+ function StatusBadge({ status }: { status: TestCase["status"] }) {
57
57
  const colors = {
58
- pending: 'bg-gray-500',
59
- passed: 'bg-green-500',
60
- failed: 'bg-red-500',
61
- skipped: 'bg-yellow-500'
58
+ pending: "bg-gray-500",
59
+ passed: "bg-green-500",
60
+ failed: "bg-red-500",
61
+ skipped: "bg-yellow-500",
62
62
  };
63
63
 
64
64
  const icons = {
65
- pending: '',
66
- passed: '',
67
- failed: '',
68
- skipped: ''
65
+ pending: "",
66
+ passed: "",
67
+ failed: "",
68
+ skipped: "",
69
69
  };
70
70
 
71
71
  return (
72
- <span className={`${colors[status]} text-white text-xs px-2 py-0.5 rounded font-mono`}>
72
+ <span
73
+ className={`${colors[status]} text-white text-xs px-2 py-0.5 rounded font-mono`}
74
+ >
73
75
  {icons[status]} {status}
74
76
  </span>
75
77
  );
76
78
  }
77
79
 
78
- function TestCaseItem({ test, filePath }: { test: TestCase; filePath: string }) {
80
+ function TestCaseItem({
81
+ test,
82
+ filePath,
83
+ }: {
84
+ test: TestCase;
85
+ filePath: string;
86
+ }) {
79
87
  return (
80
88
  <div className="flex items-center gap-3 py-2 px-3 hover:bg-gray-800/50 rounded group">
81
89
  <StatusBadge status={test.status} />
@@ -93,22 +101,24 @@ function TestCaseItem({ test, filePath }: { test: TestCase; filePath: string })
93
101
  function TestGroupItem({
94
102
  group,
95
103
  filePath,
96
- depth = 0
104
+ depth = 0,
97
105
  }: {
98
106
  group: TestGroup;
99
107
  filePath: string;
100
108
  depth?: number;
101
109
  }) {
102
110
  const [expanded, setExpanded] = useState(true);
103
- const totalTests = group.tests.length + group.groups.reduce((sum, g) => sum + g.tests.length, 0);
111
+ const totalTests =
112
+ group.tests.length +
113
+ group.groups.reduce((sum, g) => sum + g.tests.length, 0);
104
114
 
105
115
  return (
106
- <div className={`${depth > 0 ? 'ml-4 border-l border-gray-700 pl-3' : ''}`}>
116
+ <div className={`${depth > 0 ? "ml-4 border-l border-gray-700 pl-3" : ""}`}>
107
117
  <button
108
118
  onClick={() => setExpanded(!expanded)}
109
119
  className="flex items-center gap-2 w-full text-left py-2 px-2 hover:bg-gray-800/30 rounded"
110
120
  >
111
- <span className="text-gray-500">{expanded ? '' : ''}</span>
121
+ <span className="text-gray-500">{expanded ? "" : ""}</span>
112
122
  <span className="font-medium text-white">{group.name}</span>
113
123
  <span className="text-gray-500 text-sm">({totalTests} tests)</span>
114
124
  </button>
@@ -135,9 +145,10 @@ function TestGroupItem({
135
145
  function TestFeatureCard({ feature }: { feature: TestFeature }) {
136
146
  const [expanded, setExpanded] = useState(true);
137
147
 
138
- const passRate = feature.totalTests > 0
139
- ? Math.round((feature.passedTests / feature.totalTests) * 100)
140
- : 0;
148
+ const passRate =
149
+ feature.totalTests > 0
150
+ ? Math.round((feature.passedTests / feature.totalTests) * 100)
151
+ : 0;
141
152
 
142
153
  return (
143
154
  <div className="bg-gray-900 border border-gray-800 rounded-lg overflow-hidden">
@@ -146,7 +157,7 @@ function TestFeatureCard({ feature }: { feature: TestFeature }) {
146
157
  className="w-full flex items-center justify-between p-4 hover:bg-gray-800/50"
147
158
  >
148
159
  <div className="flex items-center gap-3">
149
- <span className="text-gray-500">{expanded ? '' : ''}</span>
160
+ <span className="text-gray-500">{expanded ? "" : ""}</span>
150
161
  <div>
151
162
  <h3 className="font-mono text-white">{feature.file}</h3>
152
163
  <p className="text-gray-500 text-sm">{feature.relativePath}</p>
@@ -159,7 +170,9 @@ function TestFeatureCard({ feature }: { feature: TestFeature }) {
159
170
  <span className="text-red-400">{feature.failedTests} failed</span>
160
171
  )}
161
172
  {feature.skippedTests > 0 && (
162
- <span className="text-yellow-400">{feature.skippedTests} skipped</span>
173
+ <span className="text-yellow-400">
174
+ {feature.skippedTests} skipped
175
+ </span>
163
176
  )}
164
177
  </div>
165
178
  <div className="w-20 h-2 bg-gray-700 rounded-full overflow-hidden">
@@ -190,19 +203,27 @@ function SummaryStats({ structure }: { structure: TestStructure }) {
190
203
  return (
191
204
  <div className="grid grid-cols-4 gap-4 mb-6">
192
205
  <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>
206
+ <div className="text-3xl font-bold text-white">
207
+ {structure.totalTests}
208
+ </div>
194
209
  <div className="text-gray-500 text-sm">Total Tests</div>
195
210
  </div>
196
211
  <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>
212
+ <div className="text-3xl font-bold text-green-400">
213
+ {structure.passedTests}
214
+ </div>
198
215
  <div className="text-gray-500 text-sm">Passed</div>
199
216
  </div>
200
217
  <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>
218
+ <div className="text-3xl font-bold text-red-400">
219
+ {structure.failedTests}
220
+ </div>
202
221
  <div className="text-gray-500 text-sm">Failed</div>
203
222
  </div>
204
223
  <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>
224
+ <div className="text-3xl font-bold text-yellow-400">
225
+ {structure.skippedTests}
226
+ </div>
206
227
  <div className="text-gray-500 text-sm">Skipped</div>
207
228
  </div>
208
229
  </div>
@@ -222,7 +243,7 @@ export default function ApiTestPage() {
222
243
  setLoading(true);
223
244
  setError(null);
224
245
  try {
225
- const response = await fetch('/api/test-structure');
246
+ const response = await fetch("/api/test-structure");
226
247
  if (!response.ok) {
227
248
  throw new Error(`Failed to fetch: ${response.status}`);
228
249
  }
@@ -264,7 +285,7 @@ export default function ApiTestPage() {
264
285
  disabled={loading}
265
286
  className="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg text-sm font-medium transition-colors disabled:opacity-50"
266
287
  >
267
- {loading ? 'Loading...' : 'Refresh'}
288
+ {loading ? "Loading..." : "Refresh"}
268
289
  </button>
269
290
  </div>
270
291
  </div>
@@ -304,7 +325,7 @@ export default function ApiTestPage() {
304
325
  </div>
305
326
 
306
327
  <div className="mt-6 text-center text-gray-600 text-sm">
307
- Parsed at {new Date(structure.parsedAt).toLocaleString()} •{' '}
328
+ Parsed at {new Date(structure.parsedAt).toLocaleString()} •{" "}
308
329
  {structure.features.length} test files
309
330
  </div>
310
331
  </>