offbyt 1.0.0

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 (103) hide show
  1. package/README.md +2 -0
  2. package/cli/index.js +2 -0
  3. package/cli.js +206 -0
  4. package/core/detector/detectAxios.js +107 -0
  5. package/core/detector/detectFetch.js +148 -0
  6. package/core/detector/detectForms.js +55 -0
  7. package/core/detector/detectSocket.js +341 -0
  8. package/core/generator/generateControllers.js +17 -0
  9. package/core/generator/generateModels.js +25 -0
  10. package/core/generator/generateRoutes.js +17 -0
  11. package/core/generator/generateServer.js +18 -0
  12. package/core/generator/generateSocket.js +160 -0
  13. package/core/index.js +14 -0
  14. package/core/ir/IRTypes.js +25 -0
  15. package/core/ir/buildIR.js +83 -0
  16. package/core/parser/parseJS.js +26 -0
  17. package/core/parser/parseTS.js +27 -0
  18. package/core/rules/relationRules.js +38 -0
  19. package/core/rules/resourceRules.js +32 -0
  20. package/core/rules/schemaInference.js +26 -0
  21. package/core/scanner/scanProject.js +58 -0
  22. package/deploy/cloudflare.js +41 -0
  23. package/deploy/cloudflareWorker.js +122 -0
  24. package/deploy/connect.js +198 -0
  25. package/deploy/flyio.js +51 -0
  26. package/deploy/index.js +322 -0
  27. package/deploy/netlify.js +29 -0
  28. package/deploy/railway.js +215 -0
  29. package/deploy/render.js +195 -0
  30. package/deploy/utils.js +383 -0
  31. package/deploy/vercel.js +29 -0
  32. package/index.js +18 -0
  33. package/lib/generator/advancedCrudGenerator.js +475 -0
  34. package/lib/generator/crudCodeGenerator.js +486 -0
  35. package/lib/generator/irBasedGenerator.js +360 -0
  36. package/lib/ir-builder/index.js +16 -0
  37. package/lib/ir-builder/irBuilder.js +330 -0
  38. package/lib/ir-builder/rulesEngine.js +353 -0
  39. package/lib/ir-builder/templateEngine.js +193 -0
  40. package/lib/ir-builder/templates/index.js +14 -0
  41. package/lib/ir-builder/templates/model.template.js +47 -0
  42. package/lib/ir-builder/templates/routes-generic.template.js +66 -0
  43. package/lib/ir-builder/templates/routes-user.template.js +105 -0
  44. package/lib/ir-builder/templates/routes.template.js +102 -0
  45. package/lib/ir-builder/templates/validation.template.js +15 -0
  46. package/lib/ir-integration.js +349 -0
  47. package/lib/modes/benchmark.js +162 -0
  48. package/lib/modes/configBasedGenerator.js +2258 -0
  49. package/lib/modes/connect.js +1125 -0
  50. package/lib/modes/doctorAi.js +172 -0
  51. package/lib/modes/generateApi.js +435 -0
  52. package/lib/modes/interactiveSetup.js +548 -0
  53. package/lib/modes/offline.clean.js +14 -0
  54. package/lib/modes/offline.enhanced.js +787 -0
  55. package/lib/modes/offline.js +295 -0
  56. package/lib/modes/offline.v2.js +13 -0
  57. package/lib/modes/sync.js +629 -0
  58. package/lib/scanner/apiEndpointExtractor.js +387 -0
  59. package/lib/scanner/authPatternDetector.js +54 -0
  60. package/lib/scanner/frontendScanner.js +642 -0
  61. package/lib/utils/apiClientGenerator.js +242 -0
  62. package/lib/utils/apiScanner.js +95 -0
  63. package/lib/utils/codeInjector.js +350 -0
  64. package/lib/utils/doctor.js +381 -0
  65. package/lib/utils/envGenerator.js +36 -0
  66. package/lib/utils/loadTester.js +61 -0
  67. package/lib/utils/performanceAnalyzer.js +298 -0
  68. package/lib/utils/resourceDetector.js +281 -0
  69. package/package.json +20 -0
  70. package/templates/.env.template +31 -0
  71. package/templates/advanced.model.template.js +201 -0
  72. package/templates/advanced.route.template.js +341 -0
  73. package/templates/auth.middleware.template.js +87 -0
  74. package/templates/auth.routes.template.js +238 -0
  75. package/templates/auth.user.model.template.js +78 -0
  76. package/templates/cache.middleware.js +34 -0
  77. package/templates/chat.models.template.js +260 -0
  78. package/templates/chat.routes.template.js +478 -0
  79. package/templates/compression.middleware.js +19 -0
  80. package/templates/database.config.js +74 -0
  81. package/templates/errorHandler.middleware.js +54 -0
  82. package/templates/express/controller.ejs +26 -0
  83. package/templates/express/model.ejs +9 -0
  84. package/templates/express/route.ejs +18 -0
  85. package/templates/express/server.ejs +16 -0
  86. package/templates/frontend.env.template +14 -0
  87. package/templates/model.template.js +86 -0
  88. package/templates/package.production.json +51 -0
  89. package/templates/package.template.json +41 -0
  90. package/templates/pagination.utility.js +110 -0
  91. package/templates/production.server.template.js +233 -0
  92. package/templates/rateLimiter.middleware.js +36 -0
  93. package/templates/requestLogger.middleware.js +19 -0
  94. package/templates/response.helper.js +179 -0
  95. package/templates/route.template.js +130 -0
  96. package/templates/security.middleware.js +78 -0
  97. package/templates/server.template.js +91 -0
  98. package/templates/socket.server.template.js +433 -0
  99. package/templates/utils.helper.js +157 -0
  100. package/templates/validation.middleware.js +63 -0
  101. package/templates/validation.schema.js +128 -0
  102. package/utils/fileWriter.js +15 -0
  103. package/utils/logger.js +18 -0
@@ -0,0 +1,642 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { globSync } from 'glob';
4
+ import ora from 'ora';
5
+ import { scanProject } from '../../core/scanner/scanProject.js';
6
+ import { parseJS } from '../../core/parser/parseJS.js';
7
+ import { parseTS } from '../../core/parser/parseTS.js';
8
+ import { detectFetch } from '../../core/detector/detectFetch.js';
9
+ import { detectAxios } from '../../core/detector/detectAxios.js';
10
+ import { detectForms } from '../../core/detector/detectForms.js';
11
+ import { detectSocket } from '../../core/detector/detectSocket.js';
12
+ import { buildIRFromDetections } from '../../core/ir/buildIR.js';
13
+ import { applyRuleEngine } from '../../core/rules/resourceRules.js';
14
+
15
+ /**
16
+ * Frontend Code Scanner - Enhanced Version
17
+ * Detects ALL API calls including:
18
+ * - Direct fetch/axios calls
19
+ * - Variable-based URLs
20
+ * - Template literals
21
+ * - Query parameters
22
+ * - Nested routing
23
+ */
24
+
25
+ export function scanFrontendCode(projectPath) {
26
+ const spinner = ora('🔍 Scanning frontend code...').start();
27
+
28
+ try {
29
+ const astResult = runAstDetection(projectPath);
30
+
31
+ // Regex fallback scan (legacy compatibility)
32
+ const patterns = [
33
+ 'src/**/*.{html,js,jsx,ts,tsx}',
34
+ 'app/**/*.{html,js,jsx,ts,tsx}',
35
+ 'pages/**/*.{html,js,jsx,ts,tsx}',
36
+ 'components/**/*.{html,js,jsx,ts,tsx}',
37
+ 'services/**/*.{html,js,jsx,ts,tsx}',
38
+ 'frontend/**/*.{html,js,jsx,ts,tsx}',
39
+ 'client/**/*.{html,js,jsx,ts,tsx}',
40
+ 'public/**/*.{html,js,jsx,ts,tsx}',
41
+ '*.{html,js,jsx,ts,tsx}'
42
+ ];
43
+
44
+ const files = [];
45
+ for (const pattern of patterns) {
46
+ files.push(...globSync(pattern, { nodir: true, cwd: projectPath }));
47
+ }
48
+
49
+ const apiCalls = [...astResult.apiCalls];
50
+ const urlVariables = new Map();
51
+
52
+ for (const file of files) {
53
+ try {
54
+ const fullPath = path.join(projectPath, file);
55
+ const content = fs.readFileSync(fullPath, 'utf8');
56
+
57
+ extractUrlVariables(content, urlVariables);
58
+ apiCalls.push(...extractDirectApiCalls(content, file, content));
59
+ apiCalls.push(...extractVariableBasedCalls(content, file, urlVariables, content));
60
+ apiCalls.push(...extractQueryParamCalls(content, file, content));
61
+ } catch {
62
+ // Skip files that can't be read
63
+ }
64
+ }
65
+
66
+ const uniqueCalls = deduplicateApiCalls(apiCalls);
67
+
68
+ spinner.succeed(`✅ Found ${uniqueCalls.length} API calls`);
69
+ return uniqueCalls;
70
+ } catch (error) {
71
+ spinner.fail('Failed to scan code');
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ function extractInlineScripts(htmlContent) {
77
+ const scripts = [];
78
+ const scriptPattern = /<script\b[^>]*>([\s\S]*?)<\/script>/gi;
79
+ let match;
80
+
81
+ while ((match = scriptPattern.exec(htmlContent)) !== null) {
82
+ const scriptCode = match[1]?.trim();
83
+ if (scriptCode) scripts.push(scriptCode);
84
+ }
85
+
86
+ return scripts;
87
+ }
88
+
89
+ function runAstDetection(projectPath) {
90
+ const apiCalls = [];
91
+ const forms = [];
92
+ const socketDetections = [];
93
+
94
+ let files = [];
95
+ try {
96
+ files = scanProject(projectPath);
97
+ } catch {
98
+ return { apiCalls, forms, socketDetections };
99
+ }
100
+
101
+ for (const file of files) {
102
+ const parsedTargets = [];
103
+
104
+ if (file.language === 'html') {
105
+ const scripts = extractInlineScripts(file.content);
106
+ for (const script of scripts) {
107
+ const ast = parseJS(script, file.relativePath);
108
+ if (ast) parsedTargets.push({ ast, content: script });
109
+ }
110
+ } else if (file.language === 'js') {
111
+ const ast = parseJS(file.content, file.relativePath);
112
+ if (ast) parsedTargets.push({ ast, content: file.content });
113
+ } else if (file.language === 'ts') {
114
+ const ast = parseTS(file.content, file.relativePath);
115
+ if (ast) parsedTargets.push({ ast, content: file.content });
116
+ }
117
+
118
+ for (const { ast, content } of parsedTargets) {
119
+ apiCalls.push(...detectFetch(ast, file.relativePath));
120
+ apiCalls.push(...detectAxios(ast, file.relativePath));
121
+ forms.push(...detectForms(ast, file.relativePath));
122
+
123
+ // Detect Socket.io / WebSocket patterns
124
+ const socketDetection = detectSocket(ast, content);
125
+ if (socketDetection.hasSocket || socketDetection.hasChat) {
126
+ socketDetections.push({
127
+ file: file.relativePath,
128
+ ...socketDetection
129
+ });
130
+ }
131
+ }
132
+ }
133
+
134
+ return { apiCalls, forms, socketDetections };
135
+ }
136
+
137
+ export async function buildHybridIR(projectPath) {
138
+ const { apiCalls, forms, socketDetections } = runAstDetection(projectPath);
139
+ const regexCalls = scanFrontendCode(projectPath);
140
+ const merged = deduplicateApiCalls([...apiCalls, ...regexCalls]);
141
+
142
+ const rawIR = buildIRFromDetections(merged, forms);
143
+ const ir = applyRuleEngine(rawIR);
144
+
145
+ // Add socket detection to IR
146
+ ir.socketDetection = mergeSocketDetections(socketDetections);
147
+
148
+ return ir;
149
+ }
150
+
151
+ /**
152
+ * Merge multiple socket detections into one
153
+ */
154
+ function mergeSocketDetections(detections) {
155
+ if (!detections || detections.length === 0) {
156
+ return {
157
+ hasSocket: false,
158
+ hasChat: false,
159
+ socketType: null,
160
+ events: [],
161
+ rooms: false,
162
+ presence: false
163
+ };
164
+ }
165
+
166
+ const merged = {
167
+ hasSocket: false,
168
+ hasChat: false,
169
+ socketType: null,
170
+ events: [],
171
+ rooms: false,
172
+ presence: false,
173
+ endpoints: [],
174
+ files: []
175
+ };
176
+
177
+ for (const detection of detections) {
178
+ if (detection.hasSocket) merged.hasSocket = true;
179
+ if (detection.hasChat) merged.hasChat = true;
180
+ if (detection.socketType) merged.socketType = detection.socketType;
181
+ if (detection.rooms) merged.rooms = true;
182
+ if (detection.presence) merged.presence = true;
183
+
184
+ if (detection.events) {
185
+ merged.events.push(...detection.events);
186
+ }
187
+
188
+ if (detection.endpoints) {
189
+ merged.endpoints.push(...detection.endpoints);
190
+ }
191
+
192
+ if (detection.file) {
193
+ merged.files.push(detection.file);
194
+ }
195
+ }
196
+
197
+ // Deduplicate events
198
+ merged.events = Array.from(
199
+ new Map(merged.events.map(e => [e.name, e])).values()
200
+ );
201
+
202
+ return merged;
203
+ }
204
+
205
+ /**
206
+ * Extract URL variables: const url = `/api/products`;
207
+ */
208
+ function extractUrlVariables(content, urlVariables) {
209
+ // Pattern: const/let/var variableName = `/api/...`;
210
+ const patterns = [
211
+ /(const|let|var)\s+(\w+)\s*=\s*['"`]([^'"`]+)['"`]/g,
212
+ /(\w+)\s*=\s*['"`](\/api[^'"`]+)['"`]/g,
213
+ /(\w+)\s*=\s*`([^`]*\/api[^`]*)`/g
214
+ ];
215
+
216
+ for (const pattern of patterns) {
217
+ let match;
218
+ while ((match = pattern.exec(content)) !== null) {
219
+ const varName = match[2] || match[1];
220
+ const url = match[3] || match[2];
221
+
222
+ if (url && url.includes(`/api`)) {
223
+ urlVariables.set(varName, url);
224
+ }
225
+ }
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Extract direct API calls: fetch(`/api/products`)
231
+ */
232
+ function extractDirectApiCalls(content, file, fileContent) {
233
+ const calls = [];
234
+
235
+ // Pattern 1: fetch with direct string
236
+ const fetchPattern = /fetch\s*\(\s*['"`]([^'"`\n]+)['"`]\s*,?\s*(\{[^}]*\})?/g;
237
+ let match;
238
+
239
+ while ((match = fetchPattern.exec(content)) !== null) {
240
+ const route = match[1];
241
+ const config = match[2] || '';
242
+
243
+ // Skip non-API calls
244
+ if (!route.includes(`/api`) && !route.startsWith('/')) continue;
245
+
246
+ // Extract method and fields
247
+ const method = extractMethod(config, content, match.index) || 'GET';
248
+ const fields = extractFields(config, content, match.index);
249
+
250
+ calls.push({
251
+ file,
252
+ type: 'fetch',
253
+ method,
254
+ route: cleanRoute(route),
255
+ fields,
256
+ hasQueryParams: route.includes('?'),
257
+ line: content.substring(0, match.index).split('\n').length,
258
+ content: fileContent
259
+ });
260
+ }
261
+
262
+ // Pattern 2: fetch with template literal
263
+ const fetchTemplatePattern = /fetch\s*\(\s*`([^`]+)`\s*,?\s*(\{[^}]*\})?/g;
264
+
265
+ while ((match = fetchTemplatePattern.exec(content)) !== null) {
266
+ const route = match[1];
267
+ const config = match[2] || '';
268
+
269
+ // Skip if no API path found
270
+ if (!route.includes(`/api`)) continue;
271
+
272
+ const method = extractMethod(config, content, match.index) || 'GET';
273
+ const fields = extractFields(config, content, match.index);
274
+
275
+ calls.push({
276
+ file,
277
+ type: 'fetch',
278
+ method,
279
+ route: cleanRoute(route),
280
+ fields,
281
+ hasQueryParams: route.includes('?'),
282
+ line: content.substring(0, match.index).split('\n').length,
283
+ content: fileContent
284
+ });
285
+ }
286
+
287
+ // Pattern 3: axios and custom instances (e.g. api, client, request)
288
+ const axiosPattern = /\b(?:axios|api|client|http|request)\.(get|post|put|delete|patch)\s*\(\s*['"`]([^'"`\n]+)['"`]\s*,?\s*(\{[^}]*\})?/g;
289
+
290
+ while ((match = axiosPattern.exec(content)) !== null) {
291
+ const method = match[1].toUpperCase();
292
+ const route = match[2];
293
+ const config = match[3] || '';
294
+
295
+ if (!route.includes(`/api`) && !route.startsWith('/')) continue;
296
+
297
+ const fields = extractFields(config, content, match.index);
298
+
299
+ calls.push({
300
+ file,
301
+ type: 'axios',
302
+ method,
303
+ route: cleanRoute(route),
304
+ fields,
305
+ hasQueryParams: route.includes('?'),
306
+ line: content.substring(0, match.index).split('\n').length,
307
+ content: fileContent
308
+ });
309
+ }
310
+
311
+ return calls;
312
+ }
313
+
314
+ /**
315
+ * Extract variable-based calls: fetch(url)
316
+ */
317
+ function extractVariableBasedCalls(content, file, urlVariables, fileContent) {
318
+ const calls = [];
319
+
320
+ // Pattern: fetch(variableName)
321
+ const fetchVarPattern = /fetch\s*\(\s*(\w+)\s*,?\s*(\{[^}]*\})?/g;
322
+ let match;
323
+
324
+ while ((match = fetchVarPattern.exec(content)) !== null) {
325
+ const varName = match[1];
326
+ const config = match[2] || '';
327
+
328
+ // Check if this variable maps to a URL
329
+ let route = null;
330
+
331
+ // Try to find URL from variable map
332
+ if (urlVariables.has(varName)) {
333
+ route = urlVariables.get(varName);
334
+ } else {
335
+ // Try to find in local scope (look backward in content)
336
+ const beforeContext = content.substring(Math.max(0, match.index - 500), match.index);
337
+ const assignPattern = new RegExp(`${varName}\\s*=\\s*['"\`]([^'"\`]+)['"\`]`);
338
+ const assignMatch = beforeContext.match(assignPattern);
339
+
340
+ if (assignMatch) {
341
+ route = assignMatch[1];
342
+ }
343
+ }
344
+
345
+ if (route && (route.includes(`/api`) || route.startsWith('/'))) {
346
+ const method = extractMethod(config, content, match.index) || 'GET';
347
+ const fields = extractFields(config, content, match.index);
348
+
349
+ calls.push({
350
+ file,
351
+ type: 'fetch',
352
+ method,
353
+ route: cleanRoute(route),
354
+ fields,
355
+ hasQueryParams: route.includes('?'),
356
+ line: content.substring(0, match.index).split('\n').length,
357
+ content: fileContent
358
+ });
359
+ }
360
+ }
361
+
362
+ return calls;
363
+ }
364
+
365
+ /**
366
+ * Extract calls with query parameters
367
+ */
368
+ function extractQueryParamCalls(content, file, fileContent) {
369
+ const calls = [];
370
+
371
+ // Pattern: URLSearchParams usage
372
+ const searchParamsPattern = /new URLSearchParams\(\)([^;]*params\.append[^;]*)+/g;
373
+ let match;
374
+
375
+ while ((match = searchParamsPattern.exec(content)) !== null) {
376
+ const paramsBlock = match[0];
377
+
378
+ // Extract param names
379
+ const paramNames = [];
380
+ const appendPattern = /params\.append\s*\(\s*['"`](\w+)['"`]/g;
381
+ let paramMatch;
382
+
383
+ while ((paramMatch = appendPattern.exec(paramsBlock)) !== null) {
384
+ paramNames.push(paramMatch[1]);
385
+ }
386
+
387
+ // Try to find associated fetch call nearby
388
+ const afterContext = content.substring(match.index, match.index + 300);
389
+ const fetchMatch = afterContext.match(/fetch\s*\(\s*['"`]([^'"`]+)['"`]/);
390
+
391
+ if (fetchMatch) {
392
+ const route = fetchMatch[1];
393
+
394
+ calls.push({
395
+ file,
396
+ type: 'fetch',
397
+ method: 'GET',
398
+ route: cleanRoute(route),
399
+ fields: [],
400
+ hasQueryParams: true,
401
+ queryParams: paramNames,
402
+ line: content.substring(0, match.index).split('\n').length,
403
+ content: fileContent
404
+ });
405
+ }
406
+ }
407
+
408
+ return calls;
409
+ }
410
+
411
+ /**
412
+ * Extract HTTP method from fetch config
413
+ */
414
+ function extractMethod(config, fullContent, startIndex) {
415
+ // Look in config object
416
+ const methodMatch = config.match(/method\s*:\s*['"`]([A-Z]+)['"`]/i);
417
+ if (methodMatch) return methodMatch[1].toUpperCase();
418
+
419
+ // Look ahead in content
420
+ const snippet = fullContent.substring(startIndex, startIndex + 300);
421
+ const snippetMatch = snippet.match(/method\s*:\s*['"`]([A-Z]+)['"`]/i);
422
+ if (snippetMatch) return snippetMatch[1].toUpperCase();
423
+
424
+ // Check for body presence (implies POST)
425
+ if (config.includes('body:') || snippet.includes('body:')) {
426
+ return 'POST';
427
+ }
428
+
429
+ return 'GET';
430
+ }
431
+
432
+ /**
433
+ * Extract fields from request body
434
+ */
435
+ function extractFields(config, fullContent, startIndex) {
436
+ const fields = [];
437
+
438
+ // Pattern 1: body: JSON.stringify({ field1, field2 })
439
+ const jsonStringifyPattern = /body\s*:\s*JSON\.stringify\s*\(\s*\{([^}]+)\}/;
440
+ const jsonMatch = config.match(jsonStringifyPattern);
441
+
442
+ if (jsonMatch) {
443
+ const fieldsStr = jsonMatch[1];
444
+ const fieldMatches = fieldsStr.match(/(\w+)\s*[,:]/g);
445
+ if (fieldMatches) {
446
+ fieldMatches.forEach(f => {
447
+ fields.push(f.replace(/[,:]/g, '').trim());
448
+ });
449
+ }
450
+ }
451
+
452
+ // Pattern 2: Look ahead in content
453
+ const snippet = fullContent.substring(startIndex, startIndex + 500);
454
+ const snippetMatch = snippet.match(/body\s*:\s*JSON\.stringify\s*\(\s*\{([^}]+)\}/);
455
+
456
+ if (snippetMatch) {
457
+ const fieldsStr = snippetMatch[1];
458
+ const fieldMatches = fieldsStr.match(/(\w+)\s*[,:]/g);
459
+ if (fieldMatches) {
460
+ fieldMatches.forEach(f => {
461
+ const fieldName = f.replace(/[,:]/g, '').trim();
462
+ if (!fields.includes(fieldName)) {
463
+ fields.push(fieldName);
464
+ }
465
+ });
466
+ }
467
+ }
468
+
469
+ // Pattern 3: body: JSON.stringify(variableName) - trace the variable
470
+ const varPattern = /body\s*:\s*JSON\.stringify\s*\(\s*(\w+)\s*\)/;
471
+ const varMatch = config.match(varPattern) || snippet.match(varPattern);
472
+
473
+ if (varMatch) {
474
+ const varName = varMatch[1];
475
+
476
+ // Look backward in fullContent to find useState initialization
477
+ const beforeContext = fullContent.substring(0, startIndex);
478
+
479
+ // Pattern: const [formData, setFormData] = useState({ field1: '', field2: '' })
480
+ const statePattern = new RegExp(`const\\s*\\[${varName},\\s*set\\w+\\]\\s*=\\s*useState\\s*\\(\\s*\\{([^}]+)\\}`, 'g');
481
+ const stateMatch = statePattern.exec(beforeContext);
482
+
483
+ if (stateMatch) {
484
+ const stateFields = stateMatch[1];
485
+ const stateFieldMatches = stateFields.match(/(\w+)\s*:/g);
486
+ if (stateFieldMatches) {
487
+ stateFieldMatches.forEach(f => {
488
+ const fieldName = f.replace(':', '').trim();
489
+ if (!fields.includes(fieldName)) {
490
+ fields.push(fieldName);
491
+ }
492
+ });
493
+ }
494
+ }
495
+ }
496
+
497
+ return fields;
498
+ }
499
+
500
+ /**
501
+ * Extract API path from a route that may contain URL variables
502
+ * Examples: "${API_URL}/api/products" -> `/api/products`
503
+ * "${BASE_URL}/api/users/${id}" -> `/api/users/:id`
504
+ */
505
+ function extractApiPath(route) {
506
+ // Try to extract /api/... path (stop at query params or template literal end)
507
+ const apiMatch = route.match(/\/api\/[^\s?`'"()]*/);
508
+ if (apiMatch) {
509
+ let apiPath = apiMatch[0];
510
+ // Replace remaining ${...} with :id for parameterized routes
511
+ apiPath = apiPath.replace(/\$\{[^}]+\}/g, ':id');
512
+ return apiPath;
513
+ }
514
+ // If no /api/ found, return as-is (might be relative path)
515
+ return route;
516
+ }
517
+
518
+ /**
519
+ * Clean and normalize route paths
520
+ */
521
+ function cleanRoute(route) {
522
+ // First extract API path to remove URL prefixes like ${API_URL}
523
+ let cleaned = extractApiPath(route);
524
+
525
+ // Remove query parameters for base route
526
+ cleaned = cleaned.split('?')[0];
527
+
528
+ // Ensure leading slash
529
+ if (!cleaned.startsWith('/')) {
530
+ cleaned = '/' + cleaned;
531
+ }
532
+
533
+ return cleaned;
534
+ }
535
+
536
+ /**
537
+ * Deduplicate API calls
538
+ */
539
+ function deduplicateApiCalls(calls) {
540
+ const seen = new Set();
541
+ const unique = [];
542
+
543
+ for (const call of calls) {
544
+ const key = `${call.method}:${call.route}`;
545
+
546
+ if (!seen.has(key)) {
547
+ seen.add(key);
548
+ unique.push(call);
549
+ } else {
550
+ // Merge fields if same endpoint
551
+ const existing = unique.find(c => c.method === call.method && c.route === call.route);
552
+ if (existing && call.fields) {
553
+ call.fields.forEach(f => {
554
+ if (!existing.fields.includes(f)) {
555
+ existing.fields.push(f);
556
+ }
557
+ });
558
+ }
559
+
560
+ // Merge query params
561
+ if (call.queryParams && existing) {
562
+ if (!existing.queryParams) existing.queryParams = [];
563
+ call.queryParams.forEach(p => {
564
+ if (!existing.queryParams.includes(p)) {
565
+ existing.queryParams.push(p);
566
+ }
567
+ });
568
+ }
569
+ }
570
+ }
571
+
572
+ return unique;
573
+ }
574
+
575
+ /**
576
+ * Smart Route Mapping
577
+ * Maps detected API calls to CRUD operations
578
+ */
579
+
580
+ export function generateRoutesFromAPICalls(apiCalls) {
581
+ const routeMap = new Map();
582
+
583
+ for (const call of apiCalls) {
584
+ // Extract base resource from route (e.g., /auth/signup -> auth)
585
+ const baseResource = getBaseResource(call.route);
586
+ const key = baseResource;
587
+
588
+ if (!routeMap.has(key)) {
589
+ routeMap.set(key, {
590
+ route: `/${baseResource}`,
591
+ model: generateModelName(baseResource),
592
+ operations: [],
593
+ subRoutes: new Set()
594
+ });
595
+ }
596
+
597
+ const routeInfo = routeMap.get(key);
598
+
599
+ // Track sub-routes (e.g., /auth/signup, /auth/login)
600
+ routeInfo.subRoutes.add(call.route);
601
+
602
+ routeInfo.operations.push({
603
+ method: call.method,
604
+ route: call.route,
605
+ fields: call.fields,
606
+ description: getOperationDescription(call.method)
607
+ });
608
+ }
609
+
610
+ return Array.from(routeMap.values());
611
+ }
612
+
613
+ function getBaseResource(route) {
614
+ // /auth/signup -> auth
615
+ // /users -> users
616
+ // /api/products/:id -> products
617
+ return route
618
+ .replace(/^\/+/, '') // Remove leading slashes
619
+ .split('/')[0] // Get first segment
620
+ .replace(/[^\w]/g, ''); // Remove special chars
621
+ }
622
+
623
+ function generateModelName(route) {
624
+ // auth -> Auth, users -> User, products -> Product
625
+ const clean = route
626
+ .replace(/^\//, '')
627
+ .split('/')[0]
628
+ .replace(/s$/, ''); // Remove trailing s
629
+
630
+ return clean.charAt(0).toUpperCase() + clean.slice(1);
631
+ }
632
+
633
+ function getOperationDescription(method) {
634
+ const descriptions = {
635
+ 'GET': 'Fetch data',
636
+ 'POST': 'Create new',
637
+ 'PUT': 'Update data',
638
+ 'PATCH': 'Partial update',
639
+ 'DELETE': 'Remove data'
640
+ };
641
+ return descriptions[method] || method;
642
+ }