@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.
- package/.claude/api-dev-state.json +224 -165
- package/CHANGELOG.md +29 -0
- package/README.md +58 -1
- package/bin/cli.js +1303 -89
- package/hooks/update-api-showcase.py +13 -1
- package/hooks/update-ui-showcase.py +13 -1
- package/package.json +7 -3
- package/scripts/extract-schema-docs.cjs +322 -0
- package/templates/api-showcase/_components/APIModal.tsx +100 -8
- package/templates/api-showcase/_components/APIShowcase.tsx +36 -4
- package/templates/api-showcase/_components/APITester.tsx +132 -45
- package/templates/typedoc.json +19 -0
- package/templates/ui-showcase/_components/UIShowcase.tsx +1 -1
- package/templates/ui-showcase/page.tsx +1 -1
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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-
|
|
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
|
|
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
|
|
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.
|
|
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: {},
|