create-surf-app 0.1.5 → 0.1.7

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 (77) hide show
  1. package/dist/chunk-AMAMASIV.js +1016 -0
  2. package/dist/cli.js +51 -0
  3. package/dist/index.js +5 -387
  4. package/dist/templates/default/CLAUDE.md +44 -0
  5. package/dist/templates/default/backend/db/index.js +23 -0
  6. package/dist/templates/default/backend/db/schema.js +20 -0
  7. package/dist/templates/{backend → default/backend}/eslint.config.mjs +1 -1
  8. package/dist/templates/default/backend/lib/db.js +67 -0
  9. package/dist/templates/default/backend/package.json +25 -0
  10. package/dist/templates/default/backend/routes/proxy.js +66 -0
  11. package/dist/templates/default/backend/server.js +444 -0
  12. package/dist/templates/default/frontend/components.json +21 -0
  13. package/{templates → dist/templates/default}/frontend/eslint.config.js +1 -1
  14. package/dist/templates/default/frontend/package.json +82 -0
  15. package/dist/templates/default/frontend/src/App.tsx +23 -0
  16. package/dist/templates/default/frontend/src/ErrorBoundary.tsx +106 -0
  17. package/dist/templates/default/frontend/src/components/ui/accordion.tsx +55 -0
  18. package/dist/templates/default/frontend/src/components/ui/alert.tsx +59 -0
  19. package/dist/templates/default/frontend/src/components/ui/aspect-ratio.tsx +5 -0
  20. package/dist/templates/default/frontend/src/components/ui/avatar.tsx +48 -0
  21. package/dist/templates/default/frontend/src/components/ui/badge.tsx +36 -0
  22. package/dist/templates/default/frontend/src/components/ui/breadcrumb.tsx +115 -0
  23. package/dist/templates/default/frontend/src/components/ui/button.tsx +57 -0
  24. package/dist/templates/default/frontend/src/components/ui/calendar.tsx +211 -0
  25. package/dist/templates/default/frontend/src/components/ui/card.tsx +76 -0
  26. package/dist/templates/default/frontend/src/components/ui/carousel.tsx +262 -0
  27. package/dist/templates/default/frontend/src/components/ui/checkbox.tsx +30 -0
  28. package/dist/templates/default/frontend/src/components/ui/collapsible.tsx +9 -0
  29. package/dist/templates/default/frontend/src/components/ui/command.tsx +153 -0
  30. package/dist/templates/default/frontend/src/components/ui/context-menu.tsx +200 -0
  31. package/dist/templates/default/frontend/src/components/ui/dialog.tsx +120 -0
  32. package/dist/templates/default/frontend/src/components/ui/drawer.tsx +118 -0
  33. package/dist/templates/default/frontend/src/components/ui/dropdown-menu.tsx +201 -0
  34. package/dist/templates/default/frontend/src/components/ui/form.tsx +176 -0
  35. package/dist/templates/default/frontend/src/components/ui/hover-card.tsx +29 -0
  36. package/dist/templates/default/frontend/src/components/ui/input.tsx +22 -0
  37. package/dist/templates/default/frontend/src/components/ui/label.tsx +26 -0
  38. package/dist/templates/default/frontend/src/components/ui/menubar.tsx +256 -0
  39. package/dist/templates/default/frontend/src/components/ui/navigation-menu.tsx +128 -0
  40. package/dist/templates/default/frontend/src/components/ui/popover.tsx +33 -0
  41. package/dist/templates/default/frontend/src/components/ui/progress.tsx +26 -0
  42. package/dist/templates/default/frontend/src/components/ui/radio-group.tsx +42 -0
  43. package/dist/templates/default/frontend/src/components/ui/resizable.tsx +43 -0
  44. package/dist/templates/default/frontend/src/components/ui/scroll-area.tsx +46 -0
  45. package/dist/templates/default/frontend/src/components/ui/select.tsx +157 -0
  46. package/dist/templates/default/frontend/src/components/ui/separator.tsx +31 -0
  47. package/dist/templates/default/frontend/src/components/ui/sheet.tsx +140 -0
  48. package/dist/templates/default/frontend/src/components/ui/skeleton.tsx +15 -0
  49. package/dist/templates/default/frontend/src/components/ui/slider.tsx +26 -0
  50. package/dist/templates/default/frontend/src/components/ui/sonner.tsx +29 -0
  51. package/dist/templates/default/frontend/src/components/ui/switch.tsx +29 -0
  52. package/dist/templates/default/frontend/src/components/ui/table.tsx +120 -0
  53. package/dist/templates/default/frontend/src/components/ui/tabs.tsx +53 -0
  54. package/dist/templates/default/frontend/src/components/ui/textarea.tsx +22 -0
  55. package/dist/templates/default/frontend/src/components/ui/toast.tsx +129 -0
  56. package/dist/templates/default/frontend/src/components/ui/toaster.tsx +35 -0
  57. package/dist/templates/default/frontend/src/components/ui/toggle-group.tsx +59 -0
  58. package/dist/templates/default/frontend/src/components/ui/toggle.tsx +43 -0
  59. package/dist/templates/default/frontend/src/components/ui/tooltip.tsx +30 -0
  60. package/dist/templates/default/frontend/src/db/schema.ts +16 -0
  61. package/{templates → dist/templates/default}/frontend/src/entry-client.tsx +11 -8
  62. package/dist/templates/default/frontend/src/hooks/use-toast.ts +95 -0
  63. package/dist/templates/default/frontend/src/index.css +314 -0
  64. package/dist/templates/default/frontend/src/lib/api.ts +31 -0
  65. package/dist/templates/default/frontend/src/lib/fetch.ts +38 -0
  66. package/dist/templates/default/frontend/src/lib/utils.ts +6 -0
  67. package/dist/templates/default/frontend/src/vite-env.d.ts +11 -0
  68. package/dist/templates/default/frontend/tsconfig.json +22 -0
  69. package/dist/templates/default/frontend/vite.config.ts +162 -0
  70. package/package.json +7 -7
  71. package/dist/templates/frontend/eslint.config.js +0 -42
  72. package/dist/templates/frontend/src/entry-client.tsx +0 -109
  73. package/templates/backend/eslint.config.mjs +0 -21
  74. package/templates/frontend/index.html +0 -43
  75. package/templates/frontend/src/entry-server.tsx +0 -13
  76. /package/dist/templates/{frontend → default/frontend}/index.html +0 -0
  77. /package/dist/templates/{frontend → default/frontend}/src/entry-server.tsx +0 -0
@@ -0,0 +1,1016 @@
1
+ // src/index.ts
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
6
+
7
+ // src/api-codegen.ts
8
+ var INTERNAL_SWAGGER_BASE = process.env.HERMOD_INTERNAL_URL ?? "http://hermod-api.app.svc.cluster.local:8080";
9
+ var BLOCKED_PATHS = /* @__PURE__ */ new Set(["/gateway/v1/onchain/sql"]);
10
+ var TAG_ORDER = [
11
+ "Market",
12
+ "Project",
13
+ "Wallet",
14
+ "Token",
15
+ "Social",
16
+ "News",
17
+ "Onchain",
18
+ "Web",
19
+ "Fund",
20
+ "Search",
21
+ "Prediction Market",
22
+ "Exchange"
23
+ ];
24
+ var CATEGORY_SET = new Set(TAG_ORDER);
25
+ var TYPE_MAP = {
26
+ string: "string",
27
+ integer: "number",
28
+ number: "number",
29
+ boolean: "boolean",
30
+ object: "any"
31
+ };
32
+ var FALLBACK_API_TS = `/**
33
+ * Crypto API client helpers (fallback \u2014 swagger fetch failed).
34
+ */
35
+
36
+ export { API_BASE, normalizeProxyPath, fetchWithRetry, proxyGet, proxyPost } from './fetch'
37
+
38
+ export interface ApiResponse<T> {
39
+ data: T[]
40
+ meta?: { total?: number; limit?: number; offset?: number }
41
+ error?: { code: string; message: string }
42
+ }
43
+
44
+ export interface ApiObjectResponse<T> {
45
+ data: T
46
+ meta?: { total?: number; limit?: number; offset?: number }
47
+ error?: { code: string; message: string }
48
+ }
49
+
50
+ export interface CursorMeta {
51
+ has_more: boolean
52
+ next_cursor?: string
53
+ limit?: number
54
+ cached?: boolean
55
+ credits_used?: number
56
+ }
57
+
58
+ export interface ApiCursorResponse<T> {
59
+ data: T[]
60
+ meta: CursorMeta
61
+ error?: { code: string; message: string }
62
+ }
63
+ `;
64
+ function getSwaggerUrl(override) {
65
+ if (override) return override;
66
+ if (process.env.HERMOD_SWAGGER_URL) return process.env.HERMOD_SWAGGER_URL;
67
+ return `${INTERNAL_SWAGGER_BASE.replace(/\/$/, "")}/gateway/openapi.json`;
68
+ }
69
+ async function fetchSwaggerDocument(swaggerUrl) {
70
+ const url = getSwaggerUrl(swaggerUrl);
71
+ const response = await fetch(url, {
72
+ headers: { Accept: "application/json" }
73
+ });
74
+ if (!response.ok) {
75
+ throw new Error(`Failed to fetch swagger from ${url}: ${response.status} ${response.statusText}`);
76
+ }
77
+ return await response.json();
78
+ }
79
+ function resolveRef(ref) {
80
+ return ref.split("/").pop() ?? ref;
81
+ }
82
+ function tsInterfaceName(defName) {
83
+ const name = defName.split(".").pop() ?? defName;
84
+ return name ? name[0].toUpperCase() + name.slice(1) : name;
85
+ }
86
+ function tagSlug(tag) {
87
+ return tag.toLowerCase().replaceAll(" ", "-");
88
+ }
89
+ function isSchemaType(schema, checkType) {
90
+ const type = schema?.type;
91
+ if (Array.isArray(type)) return type.includes(checkType);
92
+ return type === checkType;
93
+ }
94
+ function extractAllRefs(schema) {
95
+ if (!schema || typeof schema !== "object") return [];
96
+ if (typeof schema.$ref === "string") return [schema.$ref];
97
+ const refs = [];
98
+ if (schema.items) refs.push(...extractAllRefs(schema.items));
99
+ for (const key of ["allOf", "anyOf", "oneOf"]) {
100
+ for (const sub of schema[key] ?? []) refs.push(...extractAllRefs(sub));
101
+ }
102
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
103
+ refs.push(...extractAllRefs(schema.additionalProperties));
104
+ }
105
+ for (const propSchema of Object.values(schema.properties ?? {})) {
106
+ refs.push(...extractAllRefs(propSchema));
107
+ }
108
+ return refs;
109
+ }
110
+ function needsQuoting(name) {
111
+ return Boolean(name) && (name.includes("-") || name.includes(" ") || /^\d/.test(name));
112
+ }
113
+ function openApiPropToTs(prop, definitions) {
114
+ if (prop.$ref) return tsInterfaceName(resolveRef(prop.$ref));
115
+ if (Array.isArray(prop.allOf) && prop.allOf.length > 0) {
116
+ return prop.allOf.map((sub) => openApiPropToTs(sub, definitions)).join(" & ");
117
+ }
118
+ for (const key of ["anyOf", "oneOf"]) {
119
+ if (Array.isArray(prop[key]) && prop[key].length > 0) {
120
+ return prop[key].map((sub) => openApiPropToTs(sub, definitions)).join(" | ");
121
+ }
122
+ }
123
+ if (Array.isArray(prop.enum) && prop.enum.length > 0) {
124
+ return prop.enum.map((value) => typeof value === "string" ? `'${value}'` : String(value)).join(" | ");
125
+ }
126
+ let type = prop.type ?? "unknown";
127
+ let nullable = Boolean(prop.nullable);
128
+ if (Array.isArray(type)) {
129
+ nullable = nullable || type.includes("null");
130
+ type = type.find((value) => value !== "null") ?? "unknown";
131
+ }
132
+ if (type === "array") {
133
+ const itemType = openApiPropToTs(prop.items ?? {}, definitions);
134
+ const base = `${itemType}[]`;
135
+ return nullable ? `${base} | null` : base;
136
+ }
137
+ if (type === "object") {
138
+ let base = "Record<string, unknown>";
139
+ if (prop.additionalProperties && typeof prop.additionalProperties === "object") {
140
+ base = `Record<string, ${openApiPropToTs(prop.additionalProperties, definitions)}>`;
141
+ } else if (prop.properties && typeof prop.properties === "object") {
142
+ const required = new Set(prop.required ?? []);
143
+ const fields = Object.entries(prop.properties).map(([name, schema]) => {
144
+ const key = needsQuoting(name) ? `'${name}'` : name;
145
+ const optional = required.has(name) ? "" : "?";
146
+ return `${key}${optional}: ${openApiPropToTs(schema, definitions)}`;
147
+ });
148
+ base = `{ ${fields.join("; ")} }`;
149
+ }
150
+ return nullable ? `${base} | null` : base;
151
+ }
152
+ const tsType = TYPE_MAP[String(type)] ?? "unknown";
153
+ return nullable ? `${tsType} | null` : tsType;
154
+ }
155
+ function schemaToTsInterface(name, schema, definitions) {
156
+ const tsName = tsInterfaceName(name);
157
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) {
158
+ const values = schema.enum.map((value) => typeof value === "string" ? `'${value}'` : String(value)).join(" | ");
159
+ return `export type ${tsName} = ${values}
160
+ `;
161
+ }
162
+ const properties = { ...schema.properties ?? {} };
163
+ const extendsParts = [];
164
+ const required = new Set(schema.required ?? []);
165
+ for (const sub of schema.allOf ?? []) {
166
+ if (sub.$ref) {
167
+ extendsParts.push(tsInterfaceName(resolveRef(sub.$ref)));
168
+ continue;
169
+ }
170
+ Object.assign(properties, sub.properties ?? {});
171
+ for (const item of sub.required ?? []) required.add(item);
172
+ }
173
+ if (Object.keys(properties).length === 0) {
174
+ let nullable = Boolean(schema.nullable);
175
+ let schemaType = schema.type;
176
+ if (Array.isArray(schemaType)) {
177
+ nullable = nullable || schemaType.includes("null");
178
+ schemaType = schemaType.find((value) => value !== "null");
179
+ }
180
+ const nullSuffix = nullable ? " | null" : "";
181
+ if (extendsParts.length > 0) {
182
+ return `export type ${tsName} = ${extendsParts.join(" & ")}${nullSuffix}
183
+ `;
184
+ }
185
+ if (isSchemaType(schema, "array")) {
186
+ const itemType = openApiPropToTs(schema.items ?? {}, definitions);
187
+ return `export type ${tsName} = ${itemType}[]${nullSuffix}
188
+ `;
189
+ }
190
+ if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
191
+ const valueType = openApiPropToTs(schema.additionalProperties, definitions);
192
+ return `export type ${tsName} = Record<string, ${valueType}>${nullSuffix}
193
+ `;
194
+ }
195
+ if (typeof schemaType === "string" && TYPE_MAP[schemaType] && schemaType !== "object") {
196
+ return `export type ${tsName} = ${TYPE_MAP[schemaType]}${nullSuffix}
197
+ `;
198
+ }
199
+ return `export type ${tsName} = Record<string, unknown>${nullSuffix}
200
+ `;
201
+ }
202
+ const extendsClause = extendsParts.length > 0 ? ` extends ${extendsParts.join(", ")}` : "";
203
+ const lines = [`export interface ${tsName}${extendsClause} {`];
204
+ for (const [propName, propSchema] of Object.entries(properties)) {
205
+ if (propName === "$schema") continue;
206
+ const description = propSchema.description;
207
+ if (description) lines.push(` /** ${String(description).replaceAll("\n", " ").trim()} */`);
208
+ const key = needsQuoting(propName) ? `'${propName}'` : propName;
209
+ const optional = required.has(propName) ? "" : "?";
210
+ lines.push(` ${key}${optional}: ${openApiPropToTs(propSchema, definitions)}`);
211
+ }
212
+ lines.push("}");
213
+ return `${lines.join("\n")}
214
+ `;
215
+ }
216
+ function isDataTypeRef(refName) {
217
+ if (refName.startsWith("DataResponse") || refName.startsWith("DataObjectResponse") || refName.startsWith("SimpleListResponse") || refName.startsWith("CursorDataResponse") || refName.endsWith("InputBody") || ["DataAPIError", "ResponseMeta", "ObjectResponseMeta", "OffsetMeta", "CursorMeta"].includes(refName)) {
218
+ return false;
219
+ }
220
+ return true;
221
+ }
222
+ function isTypedResponse(refName) {
223
+ return !refName.includes("RawJSON") && !refName.includes("MapStringInterface");
224
+ }
225
+ function getResponseModel(pathSpec) {
226
+ const schema = pathSpec.responses?.["200"]?.content?.["application/json"]?.schema ?? pathSpec.responses?.["200"]?.schema ?? {};
227
+ if (schema.$ref) {
228
+ const refName = resolveRef(schema.$ref);
229
+ return [refName, isTypedResponse(refName)];
230
+ }
231
+ return [null, false];
232
+ }
233
+ function isObjectResponse(respModel) {
234
+ return Boolean(respModel && respModel.startsWith("DataObjectResponse"));
235
+ }
236
+ function isCursorResponse(respModel) {
237
+ return Boolean(respModel && respModel.startsWith("CursorDataResponse"));
238
+ }
239
+ function isArrayResponse(respModel) {
240
+ return respModel.startsWith("DataResponse") || respModel.startsWith("SimpleListResponse") || respModel.startsWith("CursorDataResponse");
241
+ }
242
+ function extractDataItemDefName(respModel, definitions) {
243
+ if (!respModel) return null;
244
+ const objectResponse = respModel.startsWith("DataObjectResponse");
245
+ const arrayResponse = isArrayResponse(respModel) && !objectResponse;
246
+ if (!objectResponse && !arrayResponse) return null;
247
+ const modelDef = definitions[respModel] ?? {};
248
+ const dataProp = modelDef.properties?.data ?? {};
249
+ if (objectResponse) {
250
+ if (dataProp.$ref) {
251
+ const refName2 = resolveRef(dataProp.$ref);
252
+ return isTypedResponse(refName2) ? refName2 : null;
253
+ }
254
+ return null;
255
+ }
256
+ const itemRef = dataProp.items?.$ref;
257
+ if (!itemRef) return null;
258
+ const refName = resolveRef(itemRef);
259
+ return isTypedResponse(refName) ? refName : null;
260
+ }
261
+ function extractDataItemType(respModel, definitions) {
262
+ const defName = extractDataItemDefName(respModel, definitions);
263
+ return defName ? tsInterfaceName(defName) : null;
264
+ }
265
+ function getBodyParam(params, spec) {
266
+ for (const param of params) {
267
+ if (param.in === "body") return param;
268
+ }
269
+ const schema = spec?.requestBody?.content?.["application/json"]?.schema;
270
+ if (schema) return { in: "body", schema };
271
+ return null;
272
+ }
273
+ function buildParamsInterface(params) {
274
+ const result = [];
275
+ for (const param of params) {
276
+ if (param.in === "body") continue;
277
+ const schema = param.schema ?? {};
278
+ let paramType = param.type ?? schema.type ?? "string";
279
+ const enumValues = schema.enum;
280
+ let tsType = "string";
281
+ if (Array.isArray(enumValues) && enumValues.length > 0) {
282
+ tsType = enumValues.map((value) => typeof value === "string" ? `'${value}'` : String(value)).join(" | ");
283
+ } else {
284
+ if (Array.isArray(paramType)) {
285
+ paramType = paramType.find((value) => value !== "null") ?? "string";
286
+ }
287
+ tsType = TYPE_MAP[String(paramType)] ?? "string";
288
+ }
289
+ result.push({
290
+ name: param.name,
291
+ type: tsType,
292
+ required: Boolean(param.required),
293
+ in: param.in ?? "query",
294
+ description: schema.description ?? param.description,
295
+ default: schema.default ?? param.default,
296
+ minimum: schema.minimum,
297
+ maximum: schema.maximum
298
+ });
299
+ }
300
+ return result;
301
+ }
302
+ function collectRefsFromSchema(schema, refs) {
303
+ for (const ref of extractAllRefs(schema)) refs.add(tsInterfaceName(resolveRef(ref)));
304
+ }
305
+ function collectBodyTypeRefs(spec) {
306
+ const refs = /* @__PURE__ */ new Set();
307
+ const bodyParam = getBodyParam(spec.parameters ?? [], spec);
308
+ if (!bodyParam) return refs;
309
+ const bodySchema = bodyParam.schema ?? {};
310
+ const definition = bodySchema.$ref ? bodySchema : bodySchema;
311
+ collectRefsFromSchema(definition, refs);
312
+ return refs;
313
+ }
314
+ function collectSubRefs(names, definitions) {
315
+ const queue = [...names];
316
+ while (queue.length > 0) {
317
+ const defName = queue.pop();
318
+ const schema = definitions[defName] ?? {};
319
+ for (const refString of extractAllRefs(schema)) {
320
+ const refName = resolveRef(refString);
321
+ if (isDataTypeRef(refName) && !names.has(refName) && definitions[refName]) {
322
+ names.add(refName);
323
+ queue.push(refName);
324
+ }
325
+ }
326
+ }
327
+ }
328
+ function categorizeSchemas(definitions, paths) {
329
+ const tagSchemas = /* @__PURE__ */ new Map();
330
+ for (const routePath of Object.keys(paths).sort()) {
331
+ const methods = paths[routePath] ?? {};
332
+ for (const [method, spec] of Object.entries(methods)) {
333
+ if (!["get", "post", "put", "delete"].includes(method)) continue;
334
+ const tags = spec.tags ?? [];
335
+ if (!tags[0] || !CATEGORY_SET.has(tags[0])) continue;
336
+ const tag = tags[0];
337
+ const [respModel] = getResponseModel(spec);
338
+ const itemDef = extractDataItemDefName(respModel, definitions);
339
+ if (itemDef && isDataTypeRef(itemDef)) {
340
+ if (!tagSchemas.has(tag)) tagSchemas.set(tag, /* @__PURE__ */ new Set());
341
+ tagSchemas.get(tag).add(itemDef);
342
+ }
343
+ const bodyParam = getBodyParam(spec.parameters ?? [], spec);
344
+ if (bodyParam) {
345
+ const bodySchema = bodyParam.schema ?? {};
346
+ const bodyDef = bodySchema.$ref ? definitions[resolveRef(bodySchema.$ref)] ?? {} : bodySchema;
347
+ for (const refString of extractAllRefs(bodyDef)) {
348
+ const refName = resolveRef(refString);
349
+ if (!isDataTypeRef(refName)) continue;
350
+ if (!tagSchemas.has(tag)) tagSchemas.set(tag, /* @__PURE__ */ new Set());
351
+ tagSchemas.get(tag).add(refName);
352
+ }
353
+ }
354
+ }
355
+ }
356
+ const result = {};
357
+ for (const [tag, names] of tagSchemas.entries()) {
358
+ collectSubRefs(names, definitions);
359
+ result[tag] = [...names].sort();
360
+ }
361
+ return result;
362
+ }
363
+ function topoSortSchemas(names, definitions) {
364
+ const nameSet = new Set(names);
365
+ const deps = /* @__PURE__ */ new Map();
366
+ for (const name of names) {
367
+ const schema = definitions[name] ?? {};
368
+ const refs = /* @__PURE__ */ new Set();
369
+ for (const propSchema of Object.values(schema.properties ?? {})) {
370
+ for (const refString of extractAllRefs(propSchema)) {
371
+ const refName = resolveRef(refString);
372
+ if (nameSet.has(refName) && refName !== name) refs.add(refName);
373
+ }
374
+ }
375
+ deps.set(name, refs);
376
+ }
377
+ const dependents = /* @__PURE__ */ new Map();
378
+ const inDegree = /* @__PURE__ */ new Map();
379
+ for (const name of names) {
380
+ dependents.set(name, /* @__PURE__ */ new Set());
381
+ inDegree.set(name, 0);
382
+ }
383
+ for (const [name, nameDeps] of deps.entries()) {
384
+ for (const dep of nameDeps) {
385
+ dependents.get(dep)?.add(name);
386
+ inDegree.set(name, (inDegree.get(name) ?? 0) + 1);
387
+ }
388
+ }
389
+ const queue = names.filter((name) => (inDegree.get(name) ?? 0) === 0).sort();
390
+ const result = [];
391
+ while (queue.length > 0) {
392
+ const name = queue.shift();
393
+ result.push(name);
394
+ for (const dependent of [...dependents.get(name) ?? []].sort()) {
395
+ const next = (inDegree.get(dependent) ?? 0) - 1;
396
+ inDegree.set(dependent, next);
397
+ if (next === 0) queue.push(dependent);
398
+ }
399
+ }
400
+ for (const name of [...names].sort()) {
401
+ if (!result.includes(name)) result.push(name);
402
+ }
403
+ return result;
404
+ }
405
+ function buildTypesCommon() {
406
+ return `/**
407
+ * Common types \u2014 auto-generated from hermod OpenAPI spec.
408
+ */
409
+
410
+ export interface ResponseMeta {
411
+ total?: number
412
+ limit?: number
413
+ offset?: number
414
+ cached?: boolean
415
+ credits_used?: number
416
+ }
417
+
418
+ export interface ApiResponse<T> {
419
+ data: T[]
420
+ meta?: ResponseMeta
421
+ error?: { code: string; message: string }
422
+ }
423
+
424
+ export interface ApiObjectResponse<T> {
425
+ data: T
426
+ meta?: ResponseMeta
427
+ error?: { code: string; message: string }
428
+ }
429
+
430
+ export interface CursorMeta {
431
+ has_more: boolean
432
+ next_cursor?: string
433
+ limit?: number
434
+ cached?: boolean
435
+ credits_used?: number
436
+ }
437
+
438
+ export interface ApiCursorResponse<T> {
439
+ data: T[]
440
+ meta: CursorMeta
441
+ error?: { code: string; message: string }
442
+ }
443
+ `;
444
+ }
445
+ function buildTypesCategory(tag, schemaNames, definitions) {
446
+ const lines = ["/**", ` * ${tag} types \u2014 auto-generated from hermod OpenAPI spec.`, " */", ""];
447
+ for (const name of schemaNames) lines.push(schemaToTsInterface(name, definitions[name] ?? {}, definitions));
448
+ return lines.join("\n");
449
+ }
450
+ function funcNameFromPath(routePath) {
451
+ const clean = routePath.replace("/gateway/v1/", "").replace("/v1/", "");
452
+ const parts = [];
453
+ for (const segment of clean.split("/")) {
454
+ if (segment.startsWith("{") && segment.endsWith("}")) continue;
455
+ parts.push(segment);
456
+ }
457
+ const name = parts.flatMap((part) => part.split("-")).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
458
+ return `fetch${name}`;
459
+ }
460
+ function hookNameFromFunc(funcName) {
461
+ return `use${funcName.slice(5)}`;
462
+ }
463
+ function swaggerTypeToTs(prop) {
464
+ if (prop.$ref) return tsInterfaceName(resolveRef(prop.$ref));
465
+ if (isSchemaType(prop, "array")) return `${swaggerTypeToTs(prop.items ?? {})}[]`;
466
+ let type = prop.type ?? "any";
467
+ if (Array.isArray(type)) type = type.find((value) => value !== "null") ?? "any";
468
+ return TYPE_MAP[String(type)] ?? "any";
469
+ }
470
+ function buildApiCore(tagsWithContent, typesTags, forBackend = false) {
471
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
472
+ const lines = [
473
+ "/**",
474
+ " * Crypto API client \u2014 auto-generated from hermod OpenAPI spec.",
475
+ ` * Generated at: ${generatedAt}`,
476
+ " *",
477
+ ...forBackend ? [
478
+ " * Backend-only: CommonJS fetch functions, no React Query hooks.",
479
+ " * Usage: const { fetchMarketPrice } = require('./api')"
480
+ ] : [
481
+ " * Core helpers + barrel re-exports from per-category modules.",
482
+ " * Usage: import { useMarketPrice, proxyGet } from '@/lib/api'"
483
+ ],
484
+ " */",
485
+ ""
486
+ ];
487
+ if (forBackend) {
488
+ lines.push(
489
+ "const DATA_PROXY_BASE = process.env.DATA_PROXY_BASE || 'http://127.0.0.1:9999/proxy'",
490
+ "",
491
+ "function normalizeProxyPath(path) {",
492
+ " const trimmed = String(path || '').replace(/^\\/+/, '')",
493
+ " return trimmed.replace(/^(?:proxy\\/)+/, '')",
494
+ "}",
495
+ "",
496
+ "async function fetchWithRetry(url, init, retries = 1) {",
497
+ " for (let attempt = 0; attempt <= retries; attempt++) {",
498
+ " const res = await fetch(url, init)",
499
+ " const text = await res.text()",
500
+ " if (text) return JSON.parse(text)",
501
+ " if (attempt < retries) await new Promise(resolve => setTimeout(resolve, 1000))",
502
+ " }",
503
+ " throw new Error(`Empty response from ${url.replace(DATA_PROXY_BASE, '')}`)",
504
+ "}",
505
+ "",
506
+ "async function proxyGet(path, params) {",
507
+ " const qs = params ? '?' + new URLSearchParams(params).toString() : ''",
508
+ " return fetchWithRetry(`${DATA_PROXY_BASE}/${normalizeProxyPath(path)}${qs}`)",
509
+ "}",
510
+ "",
511
+ "async function proxyPost(path, body) {",
512
+ " return fetchWithRetry(`${DATA_PROXY_BASE}/${normalizeProxyPath(path)}`, {",
513
+ " method: 'POST',",
514
+ " headers: { 'Content-Type': 'application/json' },",
515
+ " body: body ? JSON.stringify(body) : undefined,",
516
+ " })",
517
+ "}",
518
+ "",
519
+ "module.exports.DATA_PROXY_BASE = DATA_PROXY_BASE",
520
+ "module.exports.normalizeProxyPath = normalizeProxyPath",
521
+ "module.exports.fetchWithRetry = fetchWithRetry",
522
+ "module.exports.proxyGet = proxyGet",
523
+ "module.exports.proxyPost = proxyPost",
524
+ ""
525
+ );
526
+ for (const tag of tagsWithContent) {
527
+ lines.push(`Object.assign(module.exports, require('./api-${tagSlug(tag)}'))`);
528
+ }
529
+ return lines.join("\n");
530
+ }
531
+ lines.push("export { API_BASE, normalizeProxyPath, fetchWithRetry, proxyGet, proxyPost } from './fetch'", "");
532
+ lines.push("export * from './types-common'");
533
+ for (const tag of typesTags) lines.push(`export * from './types-${tagSlug(tag)}'`);
534
+ lines.push("");
535
+ for (const tag of tagsWithContent) lines.push(`export * from './api-${tagSlug(tag)}'`);
536
+ return lines.join("\n");
537
+ }
538
+ function buildApiIndex(tagPaths, definitions, forBackend = false) {
539
+ const lines = ["# API Index", ""];
540
+ const ext = forBackend ? "js" : "ts";
541
+ lines.push(`Read \`api-{category}.${ext}\` for full signatures and types.`, "");
542
+ for (const tag of TAG_ORDER) {
543
+ const endpoints = tagPaths[tag] ?? [];
544
+ if (endpoints.length === 0) continue;
545
+ lines.push(`## ${tag} \u2014 \`api-${tagSlug(tag)}.${ext}\``);
546
+ for (const [, routePath, spec] of endpoints) {
547
+ const funcName = funcNameFromPath(routePath);
548
+ const params = buildParamsInterface(spec.parameters ?? []);
549
+ const bodyParam = getBodyParam(spec.parameters ?? [], spec);
550
+ const required = params.filter((param) => param.required).map((param) => param.name);
551
+ if (bodyParam?.schema?.$ref) {
552
+ const bodyDef = definitions[resolveRef(bodyParam.schema.$ref)] ?? {};
553
+ required.push(...Object.keys(bodyDef.properties ?? {}).filter((name) => (bodyDef.required ?? []).includes(name)));
554
+ }
555
+ const signature = required.length > 0 ? `(${required.join(", ")})` : "()";
556
+ const description = spec.description || spec.summary;
557
+ lines.push(`- \`${funcName}${signature}\`${description ? ` \u2014 ${description}` : ""}`);
558
+ }
559
+ lines.push("");
560
+ }
561
+ return lines.join("\n");
562
+ }
563
+ function buildQueryKey(funcName) {
564
+ const parts = funcName.slice(5);
565
+ const tokens = [];
566
+ let current = "";
567
+ for (const char of parts) {
568
+ if (/[A-Z]/.test(char) && current) {
569
+ tokens.push(current.toLowerCase());
570
+ current = char;
571
+ continue;
572
+ }
573
+ current += char;
574
+ }
575
+ if (current) tokens.push(current.toLowerCase());
576
+ return tokens;
577
+ }
578
+ function buildQueryAssignments(queryOnly, paramsOptional) {
579
+ const lines = [];
580
+ for (const param of queryOnly) {
581
+ if (param.required) {
582
+ lines.push(` qs['${param.name}'] = String(params.${param.name})`);
583
+ continue;
584
+ }
585
+ if (param.default !== void 0) {
586
+ const defaultLiteral = typeof param.default === "string" ? `'${param.default}'` : JSON.stringify(param.default);
587
+ lines.push(` qs['${param.name}'] = String(params?.${param.name} ?? ${defaultLiteral})`);
588
+ continue;
589
+ }
590
+ lines.push(` if (params?.${param.name} !== undefined) qs['${param.name}'] = String(params.${param.name})`);
591
+ }
592
+ return lines;
593
+ }
594
+ function buildApiCategory(tag, endpoints, definitions, availableTypes, includeHooks = true) {
595
+ const isBackend = !includeHooks;
596
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
597
+ const lines = [
598
+ "/**",
599
+ ` * ${tag} API \u2014 auto-generated from hermod OpenAPI spec.`,
600
+ ` * Generated at: ${generatedAt}`,
601
+ " */",
602
+ ""
603
+ ];
604
+ const usedTypes = /* @__PURE__ */ new Set();
605
+ let hasApiResponse = false;
606
+ let hasApiObjectResponse = false;
607
+ let hasApiCursorResponse = false;
608
+ for (const [, , spec] of endpoints) {
609
+ const [respModel] = getResponseModel(spec);
610
+ const itemType = extractDataItemType(respModel, definitions);
611
+ if (itemType && availableTypes.has(itemType)) {
612
+ usedTypes.add(itemType);
613
+ if (isObjectResponse(respModel)) hasApiObjectResponse = true;
614
+ else if (isCursorResponse(respModel)) hasApiCursorResponse = true;
615
+ else hasApiResponse = true;
616
+ }
617
+ for (const ref of collectBodyTypeRefs(spec)) {
618
+ if (availableTypes.has(ref)) usedTypes.add(ref);
619
+ }
620
+ }
621
+ if (isBackend) {
622
+ lines.push("'use strict'", "", "const { proxyGet, proxyPost } = require('./api')", "");
623
+ } else {
624
+ const imports = ["proxyGet", "proxyPost"];
625
+ const typeImports = [];
626
+ if (hasApiResponse) typeImports.push("ApiResponse");
627
+ if (hasApiObjectResponse) typeImports.push("ApiObjectResponse");
628
+ if (hasApiCursorResponse) typeImports.push("ApiCursorResponse");
629
+ for (const typeName of [...usedTypes].sort()) typeImports.push(typeName);
630
+ const allImports = [
631
+ ...imports,
632
+ ...typeImports.map((typeName) => `type ${typeName}`)
633
+ ];
634
+ lines.push(`import { ${allImports.join(", ")} } from './api'`);
635
+ if (includeHooks) {
636
+ lines.push(
637
+ "import { useInfiniteQuery, useQuery, type UseInfiniteQueryOptions, type UseQueryOptions } from '@tanstack/react-query'"
638
+ );
639
+ }
640
+ lines.push("");
641
+ }
642
+ const generatedFuncs = [];
643
+ for (const [httpMethod, routePath, spec] of endpoints) {
644
+ const funcName = funcNameFromPath(routePath);
645
+ const params = buildParamsInterface(spec.parameters ?? []);
646
+ const bodyParam = getBodyParam(spec.parameters ?? [], spec);
647
+ const pathParams = params.filter((param) => param.in === "path");
648
+ const queryOnly = params.filter((param) => param.in === "query");
649
+ const hasRequired = params.some((param) => param.required);
650
+ const hasBody = bodyParam !== null;
651
+ const paramsOptional = !hasRequired && !hasBody;
652
+ const [respModel, typedResponse] = getResponseModel(spec);
653
+ const itemType = extractDataItemType(respModel, definitions);
654
+ const isCursor = isCursorResponse(respModel);
655
+ let returnType = "any";
656
+ if (typedResponse && itemType && availableTypes.has(itemType)) {
657
+ if (isObjectResponse(respModel)) returnType = `ApiObjectResponse<${itemType}>`;
658
+ else if (isCursor) returnType = `ApiCursorResponse<${itemType}>`;
659
+ else returnType = `ApiResponse<${itemType}>`;
660
+ }
661
+ const bodyFields = [];
662
+ const bodyRequired = /* @__PURE__ */ new Set();
663
+ if (hasBody) {
664
+ const bodySchema = bodyParam?.schema ?? {};
665
+ const bodyDefinition = bodySchema.$ref ? definitions[resolveRef(bodySchema.$ref)] ?? {} : bodySchema;
666
+ for (const field of bodyDefinition.required ?? []) bodyRequired.add(field);
667
+ for (const [name, schema] of Object.entries(bodyDefinition.properties ?? {})) {
668
+ if (name === "$schema") continue;
669
+ bodyFields.push([name, schema]);
670
+ }
671
+ }
672
+ const paramFields = [];
673
+ for (const param of params) {
674
+ const optional = param.required ? "" : "?";
675
+ paramFields.push(`${param.name}${optional}: ${param.type}`);
676
+ }
677
+ for (const [name, schema] of bodyFields) {
678
+ const optional = bodyRequired.has(name) ? "" : "?";
679
+ paramFields.push(`${name}${optional}: ${swaggerTypeToTs(schema)}`);
680
+ }
681
+ lines.push(`/** ${spec.description || spec.summary || funcName} */`);
682
+ if (isBackend) {
683
+ if (paramFields.length > 0) lines.push(`async function ${funcName}(params) {`);
684
+ else lines.push(`async function ${funcName}() {`);
685
+ } else {
686
+ if (paramFields.length > 0) {
687
+ const paramsType = `{ ${paramFields.join("; ")} }`;
688
+ lines.push(`export async function ${funcName}(params${paramsOptional ? "?" : ""}: ${paramsType}) {`);
689
+ } else {
690
+ lines.push(`export async function ${funcName}() {`);
691
+ }
692
+ }
693
+ for (const param of queryOnly.filter((param2) => param2.minimum !== void 0 || param2.maximum !== void 0)) {
694
+ const accessor = `params${paramsOptional ? "?" : ""}.${param.name}`;
695
+ let expr = accessor;
696
+ if (param.minimum !== void 0 && param.maximum !== void 0) {
697
+ expr = `Math.max(${param.minimum}, Math.min(${param.maximum}, ${accessor}))`;
698
+ } else if (param.minimum !== void 0) {
699
+ expr = `Math.max(${param.minimum}, ${accessor})`;
700
+ } else if (param.maximum !== void 0) {
701
+ expr = `Math.min(${param.maximum}, ${accessor})`;
702
+ }
703
+ if (param.required) lines.push(` params.${param.name} = ${expr}`);
704
+ else lines.push(` if (${accessor} !== undefined) params.${param.name} = ${expr}`);
705
+ }
706
+ let proxyPath = routePath.replace("/gateway/v1/", "").replace("/v1/", "");
707
+ for (const param of pathParams) {
708
+ proxyPath = proxyPath.replace(`{${param.name}}`, `\${encodeURIComponent(params.${param.name})}`);
709
+ }
710
+ if (httpMethod === "POST" || httpMethod === "PUT") {
711
+ let bodyExpr = "params";
712
+ if (queryOnly.length > 0 && bodyFields.length > 0) {
713
+ lines.push(` const { ${queryOnly.map((param) => param.name).join(", ")}, ...body } = params`);
714
+ bodyExpr = "body";
715
+ }
716
+ if (queryOnly.length > 0) {
717
+ lines.push(isBackend ? " const qs = {}" : " const qs: Record<string, string> = {}");
718
+ lines.push(...buildQueryAssignments(queryOnly, paramsOptional));
719
+ lines.push(
720
+ isBackend ? ` return proxyPost(\`${proxyPath}?\${new URLSearchParams(qs)}\`${bodyFields.length > 0 ? `, ${bodyExpr}` : ""})` : ` return proxyPost<${returnType}>(\`${proxyPath}?\${new URLSearchParams(qs)}\`${bodyFields.length > 0 ? `, ${bodyExpr}` : ""})`
721
+ );
722
+ } else if (bodyFields.length > 0) {
723
+ lines.push(isBackend ? ` return proxyPost(\`${proxyPath}\`, params)` : ` return proxyPost<${returnType}>(\`${proxyPath}\`, params)`);
724
+ } else {
725
+ lines.push(isBackend ? ` return proxyPost(\`${proxyPath}\`)` : ` return proxyPost<${returnType}>(\`${proxyPath}\`)`);
726
+ }
727
+ } else {
728
+ if (queryOnly.length > 0) {
729
+ lines.push(isBackend ? " const qs = {}" : " const qs: Record<string, string> = {}");
730
+ lines.push(...buildQueryAssignments(queryOnly, paramsOptional));
731
+ lines.push(
732
+ isBackend ? ` return proxyGet(\`${proxyPath}\`, qs)` : ` return proxyGet<${returnType}>(\`${proxyPath}\`, qs)`
733
+ );
734
+ } else {
735
+ lines.push(isBackend ? ` return proxyGet(\`${proxyPath}\`)` : ` return proxyGet<${returnType}>(\`${proxyPath}\`)`);
736
+ }
737
+ }
738
+ lines.push("}", "");
739
+ const isMutation = (httpMethod === "POST" || httpMethod === "PUT") && (tag === "Onchain" || tag === "Web");
740
+ const queryParamNames = new Set(queryOnly.map((param) => param.name));
741
+ let paginationType = null;
742
+ if (!isMutation && respModel && isArrayResponse(respModel)) {
743
+ if (isCursor && queryParamNames.has("cursor")) paginationType = "cursor";
744
+ else if (queryParamNames.has("limit") && queryParamNames.has("offset")) paginationType = "offset";
745
+ }
746
+ generatedFuncs.push({
747
+ funcName,
748
+ hasParams: paramFields.length > 0,
749
+ paramsOptional,
750
+ returnType,
751
+ isQuery: !isMutation,
752
+ paginationType,
753
+ summary: spec.summary ?? ""
754
+ });
755
+ }
756
+ if (isBackend) {
757
+ lines.push("module.exports = {");
758
+ for (const generated of generatedFuncs) lines.push(` ${generated.funcName},`);
759
+ lines.push("}", "");
760
+ return lines.join("\n");
761
+ }
762
+ const queryFuncs = generatedFuncs.filter((generated) => generated.isQuery);
763
+ if (includeHooks && queryFuncs.length > 0) {
764
+ lines.push("// Hooks", "");
765
+ lines.push("type QueryOpts<T> = Omit<UseQueryOptions<T, Error>, 'queryKey' | 'queryFn'>", "");
766
+ for (const generated of queryFuncs) {
767
+ const hookName = hookNameFromFunc(generated.funcName);
768
+ const keyTokens = buildQueryKey(generated.funcName).map((token) => `'${token}'`).join(", ");
769
+ if (generated.hasParams) {
770
+ lines.push(`export function ${hookName}(params${generated.paramsOptional ? "?" : ""}: Parameters<typeof ${generated.funcName}>[0], opts?: QueryOpts<${generated.returnType}>) {`);
771
+ lines.push(` return useQuery({ queryKey: [${keyTokens}, params], queryFn: () => ${generated.funcName}(params${generated.paramsOptional ? "" : "!"}), ...opts })`);
772
+ } else {
773
+ lines.push(`export function ${hookName}(opts?: QueryOpts<${generated.returnType}>) {`);
774
+ lines.push(` return useQuery({ queryKey: [${keyTokens}], queryFn: () => ${generated.funcName}(), ...opts })`);
775
+ }
776
+ lines.push("}", "");
777
+ }
778
+ const paginated = queryFuncs.filter((generated) => generated.paginationType !== null);
779
+ if (paginated.length > 0) {
780
+ lines.push("// Infinite query hooks", "");
781
+ if (paginated.some((generated) => generated.paginationType === "offset")) {
782
+ lines.push("type OffsetInfiniteOpts<T> = Omit<UseInfiniteQueryOptions<T, Error, T, T, unknown[], number>, 'queryKey' | 'queryFn' | 'initialPageParam' | 'getNextPageParam'>");
783
+ }
784
+ if (paginated.some((generated) => generated.paginationType === "cursor")) {
785
+ lines.push("type CursorInfiniteOpts<T> = Omit<UseInfiniteQueryOptions<T, Error, T, T, unknown[], string>, 'queryKey' | 'queryFn' | 'initialPageParam' | 'getNextPageParam'>");
786
+ }
787
+ lines.push("");
788
+ for (const generated of paginated) {
789
+ const hookName = hookNameFromFunc(generated.funcName).replace(/^use/, "useInfinite");
790
+ const keyTokens = buildQueryKey(generated.funcName).map((token) => `'${token}'`).join(", ");
791
+ if (generated.paginationType === "cursor") {
792
+ lines.push(`export function ${hookName}(params${generated.paramsOptional ? "?" : ""}: Omit<Parameters<typeof ${generated.funcName}>[0], 'cursor'>, opts?: CursorInfiniteOpts<${generated.returnType}>) {`);
793
+ lines.push(" return useInfiniteQuery({");
794
+ lines.push(` queryKey: [${keyTokens}, 'infinite', params],`);
795
+ lines.push(` queryFn: ({ pageParam }) => ${generated.funcName}({ ...params${generated.paramsOptional ? "" : "!"}, ...(pageParam ? { cursor: pageParam } : {}) }),`);
796
+ lines.push(" initialPageParam: '',");
797
+ lines.push(" getNextPageParam: (lastPage) => {");
798
+ lines.push(" const meta = (lastPage as any)?.meta");
799
+ lines.push(" if (!meta?.has_more || !meta?.next_cursor) return undefined");
800
+ lines.push(" return meta.next_cursor");
801
+ lines.push(" },");
802
+ lines.push(" ...opts,");
803
+ lines.push(" })");
804
+ lines.push("}", "");
805
+ } else {
806
+ lines.push(`export function ${hookName}(params${generated.paramsOptional ? "?" : ""}: Omit<Parameters<typeof ${generated.funcName}>[0], 'offset'>, opts?: OffsetInfiniteOpts<${generated.returnType}>) {`);
807
+ lines.push(" return useInfiniteQuery({");
808
+ lines.push(` queryKey: [${keyTokens}, 'infinite', params],`);
809
+ lines.push(` queryFn: ({ pageParam = 0 }) => ${generated.funcName}({ ...params${generated.paramsOptional ? "" : "!"}, offset: pageParam }),`);
810
+ lines.push(" initialPageParam: 0,");
811
+ lines.push(" getNextPageParam: (lastPage) => {");
812
+ lines.push(" const meta = (lastPage as any)?.meta");
813
+ lines.push(" if (!meta?.total || !meta?.limit) return undefined");
814
+ lines.push(" const next = (meta.offset ?? 0) + meta.limit");
815
+ lines.push(" return next < meta.total ? next : undefined");
816
+ lines.push(" },");
817
+ lines.push(" ...opts,");
818
+ lines.push(" })");
819
+ lines.push("}", "");
820
+ }
821
+ }
822
+ }
823
+ }
824
+ return lines.join("\n");
825
+ }
826
+ async function generateApiFiles({
827
+ swaggerUrl,
828
+ logger = () => {
829
+ }
830
+ } = {}) {
831
+ const doc = await fetchSwaggerDocument(swaggerUrl);
832
+ const definitions = doc.components?.schemas ?? {};
833
+ const paths = doc.paths ?? {};
834
+ const tagPaths = {};
835
+ for (const routePath of Object.keys(paths).sort()) {
836
+ const methods = paths[routePath] ?? {};
837
+ for (const [method, spec] of Object.entries(methods)) {
838
+ if (!["get", "post", "put", "delete"].includes(method)) continue;
839
+ const tags = spec.tags ?? [];
840
+ if (!tags[0] || !CATEGORY_SET.has(tags[0])) continue;
841
+ if (BLOCKED_PATHS.has(routePath)) continue;
842
+ if (!tagPaths[tags[0]]) tagPaths[tags[0]] = [];
843
+ tagPaths[tags[0]].push([method.toUpperCase(), routePath, spec]);
844
+ }
845
+ }
846
+ const categorySchemas = categorizeSchemas(definitions, paths);
847
+ const availableTypes = /* @__PURE__ */ new Set();
848
+ for (const names of Object.values(categorySchemas)) {
849
+ for (const name of names) availableTypes.add(tsInterfaceName(name));
850
+ }
851
+ const categoryFiles = {};
852
+ const tagsWithContent = [];
853
+ for (const tag of TAG_ORDER) {
854
+ const endpoints = tagPaths[tag] ?? [];
855
+ if (endpoints.length === 0) continue;
856
+ tagsWithContent.push(tag);
857
+ categoryFiles[`api-${tagSlug(tag)}.ts`] = buildApiCategory(tag, endpoints, definitions, availableTypes, true);
858
+ }
859
+ const typesTags = [];
860
+ const result = {
861
+ "types-common.ts": buildTypesCommon()
862
+ };
863
+ for (const tag of TAG_ORDER) {
864
+ const schemaNames = categorySchemas[tag] ?? [];
865
+ if (schemaNames.length === 0) continue;
866
+ typesTags.push(tag);
867
+ result[`types-${tagSlug(tag)}.ts`] = buildTypesCategory(
868
+ tag,
869
+ topoSortSchemas(schemaNames, definitions),
870
+ definitions
871
+ );
872
+ }
873
+ result["api.ts"] = buildApiCore(tagsWithContent, typesTags, false);
874
+ result["API_INDEX.md"] = buildApiIndex(tagPaths, definitions, false);
875
+ Object.assign(result, categoryFiles);
876
+ result["backend/api.js"] = buildApiCore(tagsWithContent, typesTags, true);
877
+ result["backend/API_INDEX.md"] = buildApiIndex(tagPaths, definitions, true);
878
+ for (const tag of TAG_ORDER) {
879
+ const endpoints = tagPaths[tag] ?? [];
880
+ if (endpoints.length === 0) continue;
881
+ result[`backend/api-${tagSlug(tag)}.js`] = buildApiCategory(tag, endpoints, definitions, availableTypes, false);
882
+ }
883
+ logger(` generated ${Object.keys(result).length} API files from ${getSwaggerUrl(swaggerUrl)}`);
884
+ return result;
885
+ }
886
+
887
+ // src/index.ts
888
+ var DEFAULT_FRONTEND_PORT = "5173";
889
+ var DEFAULT_BACKEND_PORT = "3001";
890
+ var DEFAULT_TEMPLATE = "default";
891
+ async function createSurfApp({
892
+ projectName = ".",
893
+ frontendPort = process.env.VITE_PORT || DEFAULT_FRONTEND_PORT,
894
+ backendPort = process.env.VITE_BACKEND_PORT || DEFAULT_BACKEND_PORT,
895
+ previewBase = process.env.VITE_BASE,
896
+ swaggerUrl,
897
+ logger = console.log
898
+ } = {}) {
899
+ const root = path.resolve(projectName);
900
+ const name = path.basename(root);
901
+ const validatedFrontendPort = validatePort("frontend", frontendPort);
902
+ const validatedBackendPort = validatePort("backend", backendPort);
903
+ const templateDir = resolveTemplateDir(DEFAULT_TEMPLATE);
904
+ logger(`
905
+ Creating Surf app in ${root}
906
+ `);
907
+ fs.mkdirSync(root, { recursive: true });
908
+ copyDir(templateDir, root, root, logger);
909
+ writeEnvFiles(root, validatedFrontendPort, validatedBackendPort, previewBase);
910
+ await generateAndWriteApiFiles(root, swaggerUrl, logger);
911
+ logger(`
912
+ Done! Next steps:
913
+
914
+ cd ${name}
915
+ cd backend && npm install && cd ..
916
+ cd frontend && npm install && cd ..
917
+
918
+ # Start backend
919
+ cd backend && PORT=${validatedBackendPort} npm run dev &
920
+
921
+ # Start frontend
922
+ cd frontend && npm run dev
923
+
924
+ Open http://localhost:${validatedFrontendPort}
925
+ `);
926
+ return root;
927
+ }
928
+ function resolveTemplateDir(templateName) {
929
+ const here = path.dirname(fileURLToPath(import.meta.url));
930
+ const candidates = [
931
+ path.join(here, "templates", templateName),
932
+ path.join(here, "..", "templates", templateName)
933
+ ];
934
+ for (const candidate of candidates) {
935
+ if (fs.existsSync(candidate)) return candidate;
936
+ }
937
+ throw new Error(`Could not find template "${templateName}" near ${here}`);
938
+ }
939
+ function copyDir(src, dest, root, logger) {
940
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
941
+ const srcPath = path.join(src, entry.name);
942
+ const destPath = path.join(dest, entry.name);
943
+ if (entry.isDirectory()) {
944
+ fs.mkdirSync(destPath, { recursive: true });
945
+ copyDir(srcPath, destPath, root, logger);
946
+ continue;
947
+ }
948
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
949
+ fs.writeFileSync(destPath, fs.readFileSync(srcPath));
950
+ logger(` ${path.relative(root, destPath)}`);
951
+ }
952
+ }
953
+ function validatePort(label, value) {
954
+ const port = Number.parseInt(value, 10);
955
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
956
+ throw new Error(`Invalid ${label} port: ${value}`);
957
+ }
958
+ return String(port);
959
+ }
960
+ function writeEnvFiles(root, frontendPort, backendPort, previewBase) {
961
+ const backendEnvPath = path.join(root, "backend", ".env");
962
+ const frontendEnvPath = path.join(root, "frontend", ".env");
963
+ fs.writeFileSync(backendEnvPath, `PORT=${backendPort}${os.EOL}`);
964
+ let frontendEnv = `VITE_PORT=${frontendPort}${os.EOL}VITE_BACKEND_PORT=${backendPort}${os.EOL}`;
965
+ if (previewBase) {
966
+ frontendEnv += `VITE_BASE=${previewBase}${os.EOL}`;
967
+ }
968
+ fs.writeFileSync(frontendEnvPath, frontendEnv);
969
+ }
970
+ async function generateAndWriteApiFiles(root, swaggerUrl, logger) {
971
+ const frontendLibDir = path.join(root, "frontend", "src", "lib");
972
+ const backendLibDir = path.join(root, "backend", "lib");
973
+ fs.mkdirSync(frontendLibDir, { recursive: true });
974
+ fs.mkdirSync(backendLibDir, { recursive: true });
975
+ removeGeneratedApiFiles(frontendLibDir, backendLibDir);
976
+ try {
977
+ const apiFiles = await generateApiFiles({ swaggerUrl, logger });
978
+ for (const [filename, content] of Object.entries(apiFiles)) {
979
+ if (filename.startsWith("backend/")) {
980
+ const relPath = filename.slice("backend/".length);
981
+ const filePath = path.join(backendLibDir, relPath);
982
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
983
+ fs.writeFileSync(filePath, content);
984
+ } else {
985
+ const filePath = path.join(frontendLibDir, filename);
986
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
987
+ fs.writeFileSync(filePath, content);
988
+ }
989
+ }
990
+ } catch (error) {
991
+ logger(` failed to generate API files from swagger, using fallback: ${String(error)}`);
992
+ fs.writeFileSync(path.join(frontendLibDir, "api.ts"), FALLBACK_API_TS);
993
+ }
994
+ }
995
+ function removeGeneratedApiFiles(frontendLibDir, backendLibDir) {
996
+ if (fs.existsSync(frontendLibDir)) {
997
+ for (const entry of fs.readdirSync(frontendLibDir)) {
998
+ if (entry === "fetch.ts" || entry === "utils.ts") continue;
999
+ if (entry === "api.ts" || entry === "API_INDEX.md" || /^api-.*\.ts$/.test(entry) || /^types-.*\.ts$/.test(entry)) {
1000
+ fs.rmSync(path.join(frontendLibDir, entry), { force: true, recursive: true });
1001
+ }
1002
+ }
1003
+ }
1004
+ if (fs.existsSync(backendLibDir)) {
1005
+ for (const entry of fs.readdirSync(backendLibDir)) {
1006
+ if (entry === "db.js") continue;
1007
+ if (entry === "api.js" || entry === "API_INDEX.md" || /^api-.*\.js$/.test(entry)) {
1008
+ fs.rmSync(path.join(backendLibDir, entry), { force: true, recursive: true });
1009
+ }
1010
+ }
1011
+ }
1012
+ }
1013
+
1014
+ export {
1015
+ createSurfApp
1016
+ };