@hustle-together/api-dev-tools 3.12.2 → 3.12.10

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.
@@ -27,12 +27,15 @@ def copy_showcase_templates(cwd):
27
27
  """Copy API showcase templates to src/app/api-showcase/."""
28
28
  # Source templates (installed by CLI)
29
29
  templates_dir = Path(__file__).parent.parent / "templates" / "api-showcase"
30
+ shared_templates_dir = Path(__file__).parent.parent / "templates" / "shared"
30
31
 
31
32
  # Destination
32
33
  showcase_dir = cwd / "src" / "app" / "api-showcase"
34
+ shared_dir = cwd / "src" / "app" / "shared"
33
35
 
34
- # Create directory if needed
36
+ # Create directories if needed
35
37
  showcase_dir.mkdir(parents=True, exist_ok=True)
38
+ shared_dir.mkdir(parents=True, exist_ok=True)
36
39
 
37
40
  # Copy template files
38
41
  templates_to_copy = [
@@ -55,6 +58,15 @@ def copy_showcase_templates(cwd):
55
58
  shutil.copy2(src_path, dest_path)
56
59
  created_files.append(str(dest_path.relative_to(cwd)))
57
60
 
61
+ # Also copy shared components (HeroHeader, etc.)
62
+ if shared_templates_dir.exists():
63
+ for src_file in shared_templates_dir.iterdir():
64
+ if src_file.is_file():
65
+ dest_path = shared_dir / src_file.name
66
+ if not dest_path.exists():
67
+ shutil.copy2(src_file, dest_path)
68
+ created_files.append(str(dest_path.relative_to(cwd)))
69
+
58
70
  return created_files
59
71
 
60
72
 
@@ -84,12 +84,15 @@ def copy_showcase_templates(cwd):
84
84
  """Copy UI showcase templates to src/app/ui-showcase/."""
85
85
  # Source templates (installed by CLI)
86
86
  templates_dir = Path(__file__).parent.parent / "templates" / "ui-showcase"
87
+ shared_templates_dir = Path(__file__).parent.parent / "templates" / "shared"
87
88
 
88
89
  # Destination
89
90
  showcase_dir = cwd / "src" / "app" / "ui-showcase"
91
+ shared_dir = cwd / "src" / "app" / "shared"
90
92
 
91
- # Create directory if needed
93
+ # Create directories if needed
92
94
  showcase_dir.mkdir(parents=True, exist_ok=True)
95
+ shared_dir.mkdir(parents=True, exist_ok=True)
93
96
 
94
97
  # Copy template files
95
98
  templates_to_copy = [
@@ -111,6 +114,15 @@ def copy_showcase_templates(cwd):
111
114
  shutil.copy2(src_path, dest_path)
112
115
  created_files.append(str(dest_path.relative_to(cwd)))
113
116
 
117
+ # Also copy shared components (HeroHeader, etc.)
118
+ if shared_templates_dir.exists():
119
+ for src_file in shared_templates_dir.iterdir():
120
+ if src_file.is_file():
121
+ dest_path = shared_dir / src_file.name
122
+ if not dest_path.exists():
123
+ shutil.copy2(src_file, dest_path)
124
+ created_files.append(str(dest_path.relative_to(cwd)))
125
+
114
126
  return created_files
115
127
 
116
128
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hustle-together/api-dev-tools",
3
- "version": "3.12.2",
3
+ "version": "3.12.10",
4
4
  "description": "Interview-driven, research-first API development toolkit with 14-phase TDD workflow, enforcement hooks, and 23 Agent Skills for cross-platform AI agents",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {
@@ -23,11 +23,15 @@
23
23
  "test": "node bin/cli.js --scope=project",
24
24
  "usage": "ccusage",
25
25
  "format": "prettier --write .",
26
- "lint": "eslint . --fix"
26
+ "lint": "eslint . --fix",
27
+ "typedoc": "typedoc",
28
+ "typedoc:watch": "typedoc --watch"
27
29
  },
28
30
  "devDependencies": {
29
31
  "prettier": "^3.0.0",
30
- "eslint": "^8.0.0"
32
+ "eslint": "^8.0.0",
33
+ "typedoc": "^0.27.0",
34
+ "typedoc-plugin-markdown": "^4.4.0"
31
35
  },
32
36
  "optionalDependencies": {
33
37
  "ccusage": "^1.0.0"
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Extract Schema Documentation
4
+ *
5
+ * Parses Zod schema files and extracts parameter documentation
6
+ * for use in the API Showcase registry.
7
+ *
8
+ * Usage:
9
+ * node scripts/extract-schema-docs.cjs <schema-file-path>
10
+ *
11
+ * Output: JSON with parameter documentation
12
+ *
13
+ * Example:
14
+ * node scripts/extract-schema-docs.cjs src/lib/schemas/unsplash.ts
15
+ *
16
+ * Created with Hustle API Dev Tools (v3.12.10)
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ /**
23
+ * Parse Zod schema file and extract documentation
24
+ * Uses regex-based parsing since we can't import TypeScript directly
25
+ */
26
+ function parseZodSchema(filePath) {
27
+ const content = fs.readFileSync(filePath, 'utf-8');
28
+
29
+ const result = {
30
+ file: filePath,
31
+ actions: [],
32
+ schemas: {},
33
+ enums: {},
34
+ constants: {}
35
+ };
36
+
37
+ // Extract action enum values
38
+ const actionMatch = content.match(/ActionSchema\s*=\s*z\.enum\(\[([^\]]+)\]\)/);
39
+ if (actionMatch) {
40
+ result.actions = actionMatch[1]
41
+ .split(',')
42
+ .map(s => s.trim().replace(/['"]/g, ''))
43
+ .filter(Boolean);
44
+ }
45
+
46
+ // Extract all enums
47
+ const enumRegex = /export const (\w+Schema)\s*=\s*z\.enum\(\[([^\]]+)\]\)/g;
48
+ let enumMatch;
49
+ while ((enumMatch = enumRegex.exec(content)) !== null) {
50
+ const name = enumMatch[1].replace('Schema', '');
51
+ const values = enumMatch[2]
52
+ .split(',')
53
+ .map(s => s.trim().replace(/['"]/g, ''))
54
+ .filter(Boolean);
55
+ result.enums[name] = values;
56
+ }
57
+
58
+ // Extract request schemas (z.object definitions)
59
+ const schemaRegex = /export const (\w+RequestSchema)\s*=\s*z\s*\.?\s*object\(\{([^}]+(?:\{[^}]*\}[^}]*)*)\}\)/gs;
60
+ let schemaMatch;
61
+
62
+ while ((schemaMatch = schemaRegex.exec(content)) !== null) {
63
+ const schemaName = schemaMatch[1];
64
+ const schemaBody = schemaMatch[2];
65
+ const params = parseSchemaParams(schemaBody, result.enums);
66
+
67
+ // Get action name from schema name (e.g., SearchRequestSchema -> search)
68
+ const actionName = schemaName
69
+ .replace('RequestSchema', '')
70
+ .replace(/([A-Z])/g, (m, p1, offset) => offset > 0 ? '_' + p1.toLowerCase() : p1.toLowerCase())
71
+ .replace(/^_/, '');
72
+
73
+ result.schemas[actionName] = {
74
+ name: schemaName,
75
+ params: params
76
+ };
77
+ }
78
+
79
+ return result;
80
+ }
81
+
82
+ /**
83
+ * Parse individual parameters from schema body
84
+ */
85
+ function parseSchemaParams(schemaBody, enums) {
86
+ const params = [];
87
+
88
+ // Split by lines and process each field
89
+ const lines = schemaBody.split('\n');
90
+
91
+ for (const line of lines) {
92
+ // Match field definitions like: fieldName: z.string().min(1)...
93
+ const fieldMatch = line.match(/^\s*(\w+)\s*:\s*z\.(.+)/);
94
+ if (!fieldMatch) continue;
95
+
96
+ const [, name, definition] = fieldMatch;
97
+ const param = {
98
+ name,
99
+ type: 'string',
100
+ required: true,
101
+ description: '',
102
+ default: null,
103
+ enum: null
104
+ };
105
+
106
+ // Determine type
107
+ if (definition.includes('string()') || definition.includes('literal(')) {
108
+ param.type = 'string';
109
+ } else if (definition.includes('number()') || definition.includes('coerce.number()')) {
110
+ param.type = 'number';
111
+ } else if (definition.includes('boolean()') || definition.includes('coerce.boolean()')) {
112
+ param.type = 'boolean';
113
+ } else if (definition.includes('array(')) {
114
+ param.type = 'array';
115
+ }
116
+
117
+ // Check if it references an enum
118
+ for (const [enumName, enumValues] of Object.entries(enums)) {
119
+ if (definition.includes(enumName + 'Schema')) {
120
+ param.type = 'enum';
121
+ param.enum = enumValues;
122
+ break;
123
+ }
124
+ }
125
+
126
+ // Check if optional
127
+ if (definition.includes('.optional()')) {
128
+ param.required = false;
129
+ }
130
+
131
+ // Extract default value
132
+ const defaultMatch = definition.match(/\.default\(([^)]+)\)/);
133
+ if (defaultMatch) {
134
+ let defaultVal = defaultMatch[1].trim();
135
+ // Parse the default value
136
+ if (defaultVal === 'true') param.default = true;
137
+ else if (defaultVal === 'false') param.default = false;
138
+ else if (/^\d+$/.test(defaultVal)) param.default = parseInt(defaultVal);
139
+ else param.default = defaultVal.replace(/['"]/g, '');
140
+ param.required = false;
141
+ }
142
+
143
+ // Extract description from validation messages
144
+ const descMatch = definition.match(/['"]([^'"]+is required|[^'"]+too long|[^'"]+invalid)['"]/i);
145
+ if (descMatch) {
146
+ param.description = descMatch[1];
147
+ }
148
+
149
+ // Extract min/max for numbers
150
+ const minMatch = definition.match(/\.min\((\d+)/);
151
+ const maxMatch = definition.match(/\.max\((\d+)/);
152
+ if (minMatch) param.min = parseInt(minMatch[1]);
153
+ if (maxMatch) param.max = parseInt(maxMatch[1]);
154
+
155
+ params.push(param);
156
+ }
157
+
158
+ return params;
159
+ }
160
+
161
+ /**
162
+ * Generate example value for a parameter
163
+ */
164
+ function generateExample(param) {
165
+ if (param.default !== null && param.default !== undefined) {
166
+ return param.default;
167
+ }
168
+
169
+ if (param.enum && param.enum.length > 0) {
170
+ return param.enum[0];
171
+ }
172
+
173
+ // Generate based on param name and type
174
+ const name = param.name.toLowerCase();
175
+
176
+ if (param.type === 'number') {
177
+ if (param.min !== undefined) return param.min;
178
+ if (name.includes('page')) return 1;
179
+ if (name.includes('per_page') || name.includes('count')) return 10;
180
+ return 10;
181
+ }
182
+
183
+ if (param.type === 'boolean') {
184
+ return true;
185
+ }
186
+
187
+ // String examples based on common param names
188
+ if (name === 'query' || name === 'q' || name === 'search') return 'nature sunset';
189
+ if (name.includes('id')) return 'abc123';
190
+ if (name.includes('url')) return 'https://example.com';
191
+ if (name.includes('color')) return 'blue';
192
+ if (name.includes('orientation')) return 'landscape';
193
+ if (name.includes('size')) return 'regular';
194
+
195
+ return 'example';
196
+ }
197
+
198
+ /**
199
+ * Generate working examples for an endpoint
200
+ */
201
+ function generateExamples(action, params, apiId) {
202
+ const examples = {};
203
+ const baseUrl = `http://localhost:3000/api/v2/${apiId}`;
204
+
205
+ // Build query parts from required params
206
+ const buildQuery = (includeOptional = false) => {
207
+ const parts = [`action=${action}`];
208
+ for (const param of params) {
209
+ if (param.name === 'action') continue;
210
+ if (param.required || includeOptional) {
211
+ const val = generateExample(param);
212
+ if (val !== null && val !== undefined) {
213
+ parts.push(`${param.name}=${encodeURIComponent(String(val))}`);
214
+ }
215
+ }
216
+ }
217
+ return parts.join('&');
218
+ };
219
+
220
+ // Basic example with required params only
221
+ const basicQuery = buildQuery(false);
222
+ examples.basic = {
223
+ description: `Basic ${action} request`,
224
+ query: basicQuery,
225
+ curl: `curl -X GET '${baseUrl}?${basicQuery}'`
226
+ };
227
+
228
+ // Full example with all params
229
+ const fullQuery = buildQuery(true);
230
+ if (fullQuery !== basicQuery) {
231
+ examples.full = {
232
+ description: `${action} with all parameters`,
233
+ query: fullQuery,
234
+ curl: `curl -X GET '${baseUrl}?${fullQuery}'`
235
+ };
236
+ }
237
+
238
+ // If there are enums, generate examples for each enum value
239
+ for (const param of params) {
240
+ if (param.enum && param.enum.length > 1) {
241
+ for (const enumVal of param.enum.slice(0, 3)) { // First 3 enum values
242
+ const enumQuery = basicQuery.replace(
243
+ `${param.name}=${encodeURIComponent(String(generateExample(param)))}`,
244
+ `${param.name}=${encodeURIComponent(enumVal)}`
245
+ );
246
+ // Only add if different from basic
247
+ if (enumQuery !== basicQuery) {
248
+ examples[`${param.name}_${enumVal}`] = {
249
+ description: `${action} with ${param.name}=${enumVal}`,
250
+ query: enumQuery,
251
+ curl: `curl -X GET '${baseUrl}?${enumQuery}'`
252
+ };
253
+ }
254
+ }
255
+ }
256
+ }
257
+
258
+ return examples;
259
+ }
260
+
261
+ /**
262
+ * Format output for registry.json
263
+ */
264
+ function formatForRegistry(parsed, apiId = 'api') {
265
+ const endpoints = {};
266
+
267
+ for (const [action, schema] of Object.entries(parsed.schemas)) {
268
+ const params = schema.params.map(p => ({
269
+ name: p.name,
270
+ type: p.type,
271
+ required: p.required,
272
+ description: p.description || `The ${p.name} parameter`,
273
+ default: p.default,
274
+ enum: p.enum,
275
+ min: p.min,
276
+ max: p.max,
277
+ example: String(generateExample(p))
278
+ })).filter(p => p.name !== 'action'); // Filter out the action param itself
279
+
280
+ endpoints[action] = {
281
+ method: 'GET', // Default, could be enhanced
282
+ description: `${action.charAt(0).toUpperCase() + action.slice(1).replace(/_/g, ' ')} action`,
283
+ params: params,
284
+ examples: generateExamples(action, params, apiId)
285
+ };
286
+ }
287
+
288
+ return {
289
+ actions: parsed.actions,
290
+ endpoints: endpoints
291
+ };
292
+ }
293
+
294
+ // Main execution
295
+ if (require.main === module) {
296
+ const args = process.argv.slice(2);
297
+
298
+ if (args.length === 0) {
299
+ console.error('Usage: node extract-schema-docs.cjs <schema-file-path> [api-id]');
300
+ console.error(' api-id: Optional API identifier for generating curl examples (e.g., "unsplash")');
301
+ process.exit(1);
302
+ }
303
+
304
+ const schemaPath = args[0];
305
+ const apiId = args[1] || path.basename(schemaPath, '.ts');
306
+
307
+ if (!fs.existsSync(schemaPath)) {
308
+ console.error(`File not found: ${schemaPath}`);
309
+ process.exit(1);
310
+ }
311
+
312
+ try {
313
+ const parsed = parseZodSchema(schemaPath);
314
+ const formatted = formatForRegistry(parsed, apiId);
315
+ console.log(JSON.stringify(formatted, null, 2));
316
+ } catch (error) {
317
+ console.error('Error parsing schema:', error.message);
318
+ process.exit(1);
319
+ }
320
+ }
321
+
322
+ module.exports = { parseZodSchema, formatForRegistry, generateExample, generateExamples };
@@ -3,10 +3,29 @@
3
3
  import { useEffect, useCallback, useState } from "react";
4
4
  import { APITester } from "./APITester";
5
5
 
6
+ interface EndpointParam {
7
+ name: string;
8
+ type: string;
9
+ description?: string;
10
+ required?: boolean;
11
+ default?: string | number | boolean;
12
+ enum?: string[];
13
+ example?: string;
14
+ min?: number;
15
+ max?: number;
16
+ }
17
+
18
+ interface EndpointExample {
19
+ description: string;
20
+ query: string;
21
+ curl: string;
22
+ }
23
+
6
24
  interface RegistryAPI {
7
25
  name: string;
8
26
  description?: string;
9
27
  route: string;
28
+ routeFile?: string;
10
29
  schemas?: string;
11
30
  tests?: string;
12
31
  methods?: string[];
@@ -17,8 +36,11 @@ interface RegistryAPI {
17
36
  endpoints?: Record<
18
37
  string,
19
38
  {
20
- methods: string[];
39
+ methods?: string[];
40
+ method?: string;
21
41
  description?: string;
42
+ params?: EndpointParam[];
43
+ examples?: Record<string, EndpointExample>;
22
44
  }
23
45
  >;
24
46
  }
@@ -41,13 +63,15 @@ interface APIModalProps {
41
63
  * - Request/response display
42
64
  * - Curl example generation
43
65
  *
44
- * Created with Hustle API Dev Tools (v3.9.2)
66
+ * Created with Hustle API Dev Tools (v3.12.10)
45
67
  */
46
68
  export function APIModal({ id, type, data, onClose }: APIModalProps) {
47
69
  const [activeTab, setActiveTab] = useState<"try-it" | "docs" | "curl">(
48
70
  "try-it",
49
71
  );
50
72
  const [selectedEndpoint, setSelectedEndpoint] = useState<string | null>(null);
73
+ const [submitRequest, setSubmitRequest] = useState<(() => Promise<void>) | null>(null);
74
+ const [isLoading, setIsLoading] = useState(false);
51
75
 
52
76
  // Close on Escape key
53
77
  const handleKeyDown = useCallback(
@@ -86,14 +110,26 @@ export function APIModal({ id, type, data, onClose }: APIModalProps) {
86
110
  const currentEndpoint = selectedEndpoint
87
111
  ? endpoints[selectedEndpoint]
88
112
  : endpoints[endpointKeys[0]];
89
- const methods = currentEndpoint?.methods || data.methods || ["POST"];
113
+ // Handle both methods array and single method string
114
+ const methods = currentEndpoint?.methods ||
115
+ (currentEndpoint?.method ? [currentEndpoint.method] : null) ||
116
+ data.methods ||
117
+ ["POST"];
90
118
  const baseUrl =
91
119
  typeof window !== "undefined"
92
120
  ? window.location.origin
93
121
  : "http://localhost:3000";
94
122
 
95
- // Build endpoint path
123
+ // Build endpoint path - always use base path for action-based APIs
124
+ // Actions are passed as query parameters, not sub-paths
96
125
  const getEndpointPath = () => {
126
+ // Check if this API uses action-based routing (has params with action)
127
+ const hasActionParam = currentEndpoint?.params?.some(p => p.name === "action");
128
+ if (hasActionParam) {
129
+ // Action-based APIs use query params, not path segments
130
+ return `/api/v2/${id}`;
131
+ }
132
+ // For true sub-endpoint APIs, include the path
97
133
  if (selectedEndpoint && selectedEndpoint !== "default") {
98
134
  return `/api/v2/${id}/${selectedEndpoint}`;
99
135
  }
@@ -102,10 +138,26 @@ export function APIModal({ id, type, data, onClose }: APIModalProps) {
102
138
 
103
139
  const endpoint = getEndpointPath();
104
140
 
105
- // Generate curl example
141
+ // Generate curl example with proper query params
106
142
  const generateCurl = (method: string) => {
143
+ // Build example query string from params
144
+ const params = currentEndpoint?.params || [];
145
+ const queryParts: string[] = [];
146
+
147
+ for (const param of params) {
148
+ if (param.name === "action" && selectedEndpoint) {
149
+ queryParts.push(`action=${selectedEndpoint}`);
150
+ } else if (param.required && param.example) {
151
+ queryParts.push(`${param.name}=${encodeURIComponent(param.example)}`);
152
+ } else if (param.example) {
153
+ queryParts.push(`${param.name}=${encodeURIComponent(param.example)}`);
154
+ }
155
+ }
156
+
157
+ const queryString = queryParts.length > 0 ? `?${queryParts.join("&")}` : "";
158
+
107
159
  if (method === "GET") {
108
- return `curl -X GET "${baseUrl}${endpoint}"`;
160
+ return `curl -X GET "${baseUrl}${endpoint}${queryString}"`;
109
161
  }
110
162
  return `curl -X ${method} "${baseUrl}${endpoint}" \\
111
163
  -H "Content-Type: application/json" \\
@@ -129,7 +181,7 @@ export function APIModal({ id, type, data, onClose }: APIModalProps) {
129
181
  />
130
182
 
131
183
  {/* Modal Content */}
132
- <div className="relative z-10 flex max-h-[90vh] w-full max-w-5xl flex-col overflow-hidden border-2 border-black bg-white shadow-xl dark:border-gray-600 dark:bg-gray-900">
184
+ <div className="relative z-10 flex max-h-[90vh] w-full max-w-7xl flex-col overflow-hidden border-2 border-black bg-white shadow-xl dark:border-gray-600 dark:bg-gray-900">
133
185
  {/* Header */}
134
186
  <header className="flex items-center justify-between border-b-2 border-black px-6 py-4 dark:border-gray-600">
135
187
  <div className="flex items-center gap-4">
@@ -271,6 +323,11 @@ export function APIModal({ id, type, data, onClose }: APIModalProps) {
271
323
  methods={methods}
272
324
  selectedEndpoint={selectedEndpoint}
273
325
  schemaPath={data.schemas}
326
+ endpointParams={data.endpoints?.[selectedEndpoint || "default"]?.params || data.endpoints?.[Object.keys(data.endpoints || {})[0]]?.params}
327
+ apiRoute={data.routeFile || data.route}
328
+ examples={data.endpoints?.[selectedEndpoint || "default"]?.examples || data.endpoints?.[Object.keys(data.endpoints || {})[0]]?.examples}
329
+ onSubmitRef={(fn) => setSubmitRequest(() => fn)}
330
+ onLoadingChange={setIsLoading}
274
331
  />
275
332
  )}
276
333
 
@@ -408,10 +465,45 @@ export function APIModal({ id, type, data, onClose }: APIModalProps) {
408
465
  );
409
466
  }
410
467
  }}
411
- className="border-2 border-black bg-[#BA0C2F] px-3 py-1.5 text-sm font-medium text-white hover:bg-[#8a0923]"
468
+ className="border-2 border-black px-3 py-1.5 text-sm font-medium hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-800"
412
469
  >
413
470
  Copy Schema Import
414
471
  </button>
472
+ {activeTab === "try-it" && submitRequest && (
473
+ <button
474
+ onClick={() => submitRequest()}
475
+ disabled={isLoading}
476
+ className="border-2 border-black bg-[#BA0C2F] px-4 py-1.5 text-sm font-bold text-white transition-colors hover:bg-[#8a0923] disabled:opacity-50"
477
+ >
478
+ {isLoading ? (
479
+ <span className="flex items-center gap-2">
480
+ <svg
481
+ className="h-4 w-4 animate-spin"
482
+ xmlns="http://www.w3.org/2000/svg"
483
+ fill="none"
484
+ viewBox="0 0 24 24"
485
+ >
486
+ <circle
487
+ className="opacity-25"
488
+ cx="12"
489
+ cy="12"
490
+ r="10"
491
+ stroke="currentColor"
492
+ strokeWidth="4"
493
+ />
494
+ <path
495
+ className="opacity-75"
496
+ fill="currentColor"
497
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
498
+ />
499
+ </svg>
500
+ Sending...
501
+ </span>
502
+ ) : (
503
+ "Send Request"
504
+ )}
505
+ </button>
506
+ )}
415
507
  </div>
416
508
  </div>
417
509
  </footer>
@@ -4,6 +4,33 @@ import { useState, useMemo } from "react";
4
4
  import { APICard } from "./APICard";
5
5
  import { APIModal } from "./APIModal";
6
6
 
7
+ // Import registry - this will be updated by the CLI when APIs are created
8
+ import registryData from "@/../.claude/registry.json";
9
+
10
+ /**
11
+ * Parameter documentation from registry.json
12
+ */
13
+ interface EndpointParam {
14
+ name: string;
15
+ type: string;
16
+ description?: string;
17
+ required?: boolean;
18
+ default?: string | number | boolean;
19
+ enum?: string[];
20
+ example?: string;
21
+ min?: number;
22
+ max?: number;
23
+ }
24
+
25
+ /**
26
+ * Example request from registry.json
27
+ */
28
+ interface EndpointExample {
29
+ description: string;
30
+ query: string;
31
+ curl: string;
32
+ }
33
+
7
34
  /**
8
35
  * Registry structure from .claude/registry.json
9
36
  */
@@ -11,16 +38,21 @@ interface RegistryAPI {
11
38
  name: string;
12
39
  description?: string;
13
40
  route: string;
41
+ routeFile?: string;
14
42
  schemas?: string;
15
43
  tests?: string;
16
44
  methods?: string[];
17
45
  created_at?: string;
18
46
  status?: string;
47
+ actions?: string[];
19
48
  endpoints?: Record<
20
49
  string,
21
50
  {
22
- methods: string[];
51
+ methods?: string[];
52
+ method?: string;
23
53
  description?: string;
54
+ params?: EndpointParam[];
55
+ examples?: Record<string, EndpointExample>;
24
56
  }
25
57
  >;
26
58
  }
@@ -46,7 +78,7 @@ interface APIShowcaseProps {
46
78
  *
47
79
  * Data source: .claude/registry.json (apis + combined sections)
48
80
  *
49
- * Created with Hustle API Dev Tools (v3.9.2)
81
+ * Created with Hustle API Dev Tools (v3.12.10)
50
82
  */
51
83
  export function APIShowcase({ registry: propRegistry }: APIShowcaseProps) {
52
84
  const [selectedAPI, setSelectedAPI] = useState<{
@@ -57,8 +89,8 @@ export function APIShowcase({ registry: propRegistry }: APIShowcaseProps) {
57
89
  const [filter, setFilter] = useState<"all" | "api" | "combined">("all");
58
90
  const [search, setSearch] = useState("");
59
91
 
60
- // Use prop registry or default empty structure
61
- const registry: Registry = propRegistry || {
92
+ // Use prop registry, imported registry, or default empty structure
93
+ const registry: Registry = propRegistry || registryData || {
62
94
  version: "1.0.0",
63
95
  apis: {},
64
96
  combined: {},