botversion-sdk 1.0.0 → 1.0.2

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/cli/detector.js CHANGED
@@ -1,10 +1,25 @@
1
1
  // botversion-sdk/cli/detector.js
2
-
3
2
  "use strict";
4
3
 
5
4
  const fs = require("fs");
6
5
  const path = require("path");
7
6
 
7
+ // ─── SKIP DIRS (used everywhere) ─────────────────────────────────────────────
8
+
9
+ const SKIP_DIRS = [
10
+ "node_modules",
11
+ ".git",
12
+ ".next",
13
+ "dist",
14
+ "build",
15
+ ".cache",
16
+ "coverage",
17
+ ".turbo",
18
+ "out",
19
+ ".output",
20
+ ".svelte-kit",
21
+ ];
22
+
8
23
  // ─── PACKAGE JSON ────────────────────────────────────────────────────────────
9
24
 
10
25
  function readPackageJson(cwd) {
@@ -17,22 +32,286 @@ function readPackageJson(cwd) {
17
32
  }
18
33
  }
19
34
 
35
+ // ─── SCAN ALL PACKAGE.JSON FILES ─────────────────────────────────────────────
36
+ // Recursively finds ALL package.json files in the project
37
+
38
+ function scanAllPackageJsons(cwd) {
39
+ const results = []; // [{ dir, pkg }]
40
+
41
+ function walk(currentDir, depth) {
42
+ if (depth > 5) return;
43
+
44
+ let entries;
45
+ try {
46
+ entries = fs.readdirSync(currentDir);
47
+ } catch {
48
+ return;
49
+ }
50
+
51
+ for (const entry of entries) {
52
+ if (SKIP_DIRS.includes(entry)) continue;
53
+
54
+ const fullPath = path.join(currentDir, entry);
55
+ let stat;
56
+ try {
57
+ stat = fs.statSync(fullPath);
58
+ } catch {
59
+ continue;
60
+ }
61
+
62
+ if (stat.isDirectory()) {
63
+ walk(fullPath, depth + 1);
64
+ } else if (entry === "package.json") {
65
+ try {
66
+ const pkg = JSON.parse(fs.readFileSync(fullPath, "utf8"));
67
+ results.push({ dir: currentDir, pkg });
68
+ } catch {
69
+ continue;
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ walk(cwd, 0);
76
+ return results;
77
+ }
78
+
79
+ // ─── CLASSIFY PACKAGE.JSON ────────────────────────────────────────────────────
80
+
81
+ const BACKEND_PACKAGES = [
82
+ "express",
83
+ "fastify",
84
+ "koa",
85
+ "@nestjs/core",
86
+ "@hapi/hapi",
87
+ "restify",
88
+ "polka",
89
+ "micro",
90
+ ];
91
+
92
+ const FULLSTACK_PACKAGES = ["next", "@sveltejs/kit"];
93
+
94
+ const FRONTEND_PACKAGES = [
95
+ "react",
96
+ "react-dom",
97
+ "vue",
98
+ "@angular/core",
99
+ "svelte",
100
+ "@sveltejs/kit",
101
+ "solid-js",
102
+ "preact",
103
+ ];
104
+
105
+ function classifyPackageJson(pkg) {
106
+ if (!pkg) return "unknown";
107
+
108
+ const deps = {
109
+ ...(pkg.dependencies || {}),
110
+ ...(pkg.devDependencies || {}),
111
+ };
112
+
113
+ const isFullstack = FULLSTACK_PACKAGES.some((p) => !!deps[p]);
114
+ if (isFullstack) return "fullstack";
115
+
116
+ const isBackend = BACKEND_PACKAGES.some((p) => !!deps[p]);
117
+ const isFrontend = FRONTEND_PACKAGES.some((p) => !!deps[p]);
118
+
119
+ if (isBackend && isFrontend) return "fullstack";
120
+ if (isBackend) return "backend";
121
+ if (isFrontend) return "frontend";
122
+ return "unknown";
123
+ }
124
+
125
+ // ─── DETECT FRONTEND FRAMEWORK ───────────────────────────────────────────────
126
+
127
+ function detectFrontendFramework(pkg) {
128
+ if (!pkg) return null;
129
+
130
+ const deps = {
131
+ ...(pkg.dependencies || {}),
132
+ ...(pkg.devDependencies || {}),
133
+ };
134
+
135
+ if (deps["next"]) return "next";
136
+ if (deps["@sveltejs/kit"]) return "sveltekit";
137
+ if (deps["svelte"]) return "svelte";
138
+ if (deps["@angular/core"]) return "angular";
139
+ if (deps["vue"]) return "vue";
140
+ if (deps["react-dom"] || deps["react"]) {
141
+ // Distinguish CRA vs Vite
142
+ if (deps["vite"] || deps["@vitejs/plugin-react"]) return "react-vite";
143
+ return "react-cra";
144
+ }
145
+ if (deps["solid-js"]) return "solid";
146
+ if (deps["preact"]) return "preact";
147
+
148
+ return null;
149
+ }
150
+
151
+ // ─── FIND MAIN FRONTEND FILE ──────────────────────────────────────────────────
152
+ // Returns { file, type } or null
153
+
154
+ function findMainFrontendFile(dir, pkg) {
155
+ const framework = detectFrontendFramework(pkg);
156
+
157
+ // ── Next.js ───────────────────────────────────────────────────────────────
158
+ // For Next.js we inject into _app.js (Pages) or layout.js (App Router)
159
+ if (framework === "next") {
160
+ const candidates = [
161
+ "pages/_app.js",
162
+ "pages/_app.tsx",
163
+ "pages/_app.ts",
164
+ "src/pages/_app.js",
165
+ "src/pages/_app.tsx",
166
+ "src/pages/_app.ts",
167
+ "app/layout.js",
168
+ "app/layout.tsx",
169
+ "src/app/layout.js",
170
+ "src/app/layout.tsx",
171
+ ];
172
+ for (const candidate of candidates) {
173
+ const fullPath = path.join(dir, candidate);
174
+ if (fs.existsSync(fullPath)) {
175
+ return { file: fullPath, type: "next" };
176
+ }
177
+ }
178
+ return null;
179
+ }
180
+
181
+ // ── Angular ───────────────────────────────────────────────────────────────
182
+ if (framework === "angular") {
183
+ const candidate = path.join(dir, "src", "index.html");
184
+ if (fs.existsSync(candidate)) {
185
+ return { file: candidate, type: "html" };
186
+ }
187
+ return null;
188
+ }
189
+
190
+ // ── React Vite / Vue Vite / Svelte / SvelteKit / Solid / Preact ──────────
191
+ // All Vite-based projects have index.html in root of the project folder
192
+ if (
193
+ framework === "react-vite" ||
194
+ framework === "vue" ||
195
+ framework === "svelte" ||
196
+ framework === "sveltekit" ||
197
+ framework === "solid" ||
198
+ framework === "preact"
199
+ ) {
200
+ // Check root index.html first
201
+ const rootHtml = path.join(dir, "index.html");
202
+ if (fs.existsSync(rootHtml)) {
203
+ return { file: rootHtml, type: "html" };
204
+ }
205
+
206
+ // Fallback: public/index.html
207
+ const publicHtml = path.join(dir, "public", "index.html");
208
+ if (fs.existsSync(publicHtml)) {
209
+ return { file: publicHtml, type: "html" };
210
+ }
211
+
212
+ return null;
213
+ }
214
+
215
+ // ── React CRA ─────────────────────────────────────────────────────────────
216
+ if (framework === "react-cra") {
217
+ // CRA always puts index.html in public/
218
+ const publicHtml = path.join(dir, "public", "index.html");
219
+ if (fs.existsSync(publicHtml)) {
220
+ return { file: publicHtml, type: "html" };
221
+ }
222
+
223
+ // Fallback: root index.html (custom CRA config)
224
+ const rootHtml = path.join(dir, "index.html");
225
+ if (fs.existsSync(rootHtml)) {
226
+ return { file: rootHtml, type: "html" };
227
+ }
228
+
229
+ return null;
230
+ }
231
+
232
+ // ── Unknown frontend — scan for any index.html ────────────────────────────
233
+ const htmlCandidates = [
234
+ "index.html",
235
+ "public/index.html",
236
+ "src/index.html",
237
+ "static/index.html",
238
+ "www/index.html",
239
+ ];
240
+
241
+ for (const candidate of htmlCandidates) {
242
+ const fullPath = path.join(dir, candidate);
243
+ if (fs.existsSync(fullPath)) {
244
+ const content = fs.readFileSync(fullPath, "utf8");
245
+ // Make sure it's a real HTML file with a body tag
246
+ if (content.includes("<body") || content.includes("<html")) {
247
+ return { file: fullPath, type: "html" };
248
+ }
249
+ }
250
+ }
251
+
252
+ // Last resort — deep scan for any .html file
253
+ const found = findHtmlFile(dir);
254
+ if (found) return { file: found, type: "html" };
255
+
256
+ return null;
257
+ }
258
+
259
+ // ─── DEEP SCAN FOR HTML FILE ─────────────────────────────────────────────────
260
+
261
+ function findHtmlFile(dir) {
262
+ function walk(currentDir, depth) {
263
+ if (depth > 3) return null;
264
+
265
+ let entries;
266
+ try {
267
+ entries = fs.readdirSync(currentDir);
268
+ } catch {
269
+ return null;
270
+ }
271
+
272
+ for (const entry of entries) {
273
+ if (SKIP_DIRS.includes(entry)) continue;
274
+
275
+ const fullPath = path.join(currentDir, entry);
276
+ let stat;
277
+ try {
278
+ stat = fs.statSync(fullPath);
279
+ } catch {
280
+ continue;
281
+ }
282
+
283
+ if (stat.isDirectory()) {
284
+ const result = walk(fullPath, depth + 1);
285
+ if (result) return result;
286
+ } else if (entry.endsWith(".html")) {
287
+ try {
288
+ const content = fs.readFileSync(fullPath, "utf8");
289
+ if (content.includes("<body") || content.includes("<html")) {
290
+ return fullPath;
291
+ }
292
+ } catch {
293
+ continue;
294
+ }
295
+ }
296
+ }
297
+
298
+ return null;
299
+ }
300
+
301
+ return walk(dir, 0);
302
+ }
303
+
20
304
  // ─── MONOREPO DETECTION ──────────────────────────────────────────────────────
21
305
 
22
306
  function detectMonorepo(cwd) {
23
- const entries = fs.readdirSync(cwd);
24
-
25
- // Check for workspaces in root package.json
26
307
  const rootPkg = readPackageJson(cwd);
27
308
  if (rootPkg && rootPkg.workspaces) {
28
- // Find all workspace package.json files
29
309
  const workspaceDirs = [];
30
310
  const patterns = Array.isArray(rootPkg.workspaces)
31
311
  ? rootPkg.workspaces
32
312
  : rootPkg.workspaces.packages || [];
33
313
 
34
314
  patterns.forEach((pattern) => {
35
- // Handle simple patterns like "packages/*"
36
315
  const base = pattern.replace(/\/\*$/, "");
37
316
  const fullBase = path.join(cwd, base);
38
317
  if (fs.existsSync(fullBase)) {
@@ -94,17 +373,12 @@ function detectFramework(pkg) {
94
373
  ...(pkg.devDependencies || {}),
95
374
  };
96
375
 
97
- // Check unsupported first so we can warn clearly
98
376
  for (const fw of UNSUPPORTED_FRAMEWORKS) {
99
- if (deps[fw]) {
100
- return { name: fw, supported: false };
101
- }
377
+ if (deps[fw]) return { name: fw, supported: false };
102
378
  }
103
379
 
104
380
  for (const fw of SUPPORTED_FRAMEWORKS) {
105
- if (deps[fw]) {
106
- return { name: fw, supported: true };
107
- }
381
+ if (deps[fw]) return { name: fw, supported: true };
108
382
  }
109
383
 
110
384
  return { name: null, supported: false };
@@ -131,7 +405,6 @@ function readTsConfig(cwd) {
131
405
  if (!fs.existsSync(tsconfigPath)) return null;
132
406
  try {
133
407
  const raw = fs.readFileSync(tsconfigPath, "utf8");
134
- // Strip comments — tsconfig supports JSON with comments
135
408
  const stripped = raw
136
409
  .replace(/\/\/.*$/gm, "")
137
410
  .replace(/\/\*[\s\S]*?\*\//g, "");
@@ -141,11 +414,6 @@ function readTsConfig(cwd) {
141
414
  }
142
415
  }
143
416
 
144
- // Decide whether to generate .ts or .js files
145
- // - Not TypeScript → always .js
146
- // - TypeScript + allowJs: true (Next.js default) → .js is fine
147
- // - TypeScript + allowJs: false (manually set by user) → must use .ts
148
- // - TypeScript + allowJs not set → Next.js default is true → .js is fine
149
417
  function shouldGenerateTs(cwd, isTypeScript) {
150
418
  if (!isTypeScript) return false;
151
419
  const tsconfig = readTsConfig(cwd);
@@ -179,7 +447,6 @@ function detectNextRouter(cwd) {
179
447
  pagesRouter: hasPages,
180
448
  appRouter: hasApp,
181
449
  srcDir: hasSrc,
182
- // Resolve the actual base directory
183
450
  baseDir: hasSrc ? path.join(cwd, "src") : cwd,
184
451
  };
185
452
  }
@@ -209,7 +476,6 @@ function detectExpressEntry(cwd, pkg) {
209
476
  const scripts = [pkg.scripts.start, pkg.scripts.dev, pkg.scripts.serve];
210
477
  for (const script of scripts) {
211
478
  if (!script) continue;
212
- // e.g. "node server.js" or "nodemon src/index.js" or "ts-node index.ts"
213
479
  const match = script.match(
214
480
  /(?:node|nodemon|ts-node|tsx)\s+([^\s]+\.(js|ts))/,
215
481
  );
@@ -220,7 +486,7 @@ function detectExpressEntry(cwd, pkg) {
220
486
  }
221
487
  }
222
488
 
223
- // Strategy 3: common file names in root and src/
489
+ // Strategy 3: common file names
224
490
  const candidates = [
225
491
  "server.js",
226
492
  "server.ts",
@@ -243,7 +509,6 @@ function detectExpressEntry(cwd, pkg) {
243
509
  for (const candidate of candidates) {
244
510
  const filePath = path.join(cwd, candidate);
245
511
  if (fs.existsSync(filePath)) {
246
- // Verify it actually contains express
247
512
  const content = fs.readFileSync(filePath, "utf8");
248
513
  if (content.includes("express") || content.includes("app.listen")) {
249
514
  return filePath;
@@ -252,17 +517,58 @@ function detectExpressEntry(cwd, pkg) {
252
517
  }
253
518
 
254
519
  // Strategy 4: any .js/.ts file containing app.listen()
255
- return findFileWithContent(cwd, "app.listen", [".js", ".ts"], 2);
520
+ return findFileWithContent(cwd, ".listen(", [".js", ".ts"], 2);
256
521
  }
257
522
 
258
523
  // ─── app.listen() LOCATION ───────────────────────────────────────────────────
259
524
 
260
- function findListenCall(filePath) {
525
+ function findListenCall(filePath, appVarName) {
526
+ appVarName = appVarName || "app";
527
+ const content = fs.readFileSync(filePath, "utf8");
528
+ const lines = content.split("\n");
529
+ const regex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
530
+
531
+ for (let i = 0; i < lines.length; i++) {
532
+ if (regex.test(lines[i])) {
533
+ return { lineIndex: i, lineNumber: i + 1, content: lines[i] };
534
+ }
535
+ }
536
+ return null;
537
+ }
538
+
539
+ function findModuleExportsApp(filePath) {
540
+ const content = fs.readFileSync(filePath, "utf8");
541
+ const lines = content.split("\n");
542
+ for (let i = 0; i < lines.length; i++) {
543
+ if (/module\.exports\s*=\s*app/.test(lines[i])) {
544
+ return { lineIndex: i, lineNumber: i + 1, content: lines[i] };
545
+ }
546
+ }
547
+ return null;
548
+ }
549
+
550
+ function findListenInsideCallback(filePath, appVarName) {
551
+ appVarName = appVarName || "app";
261
552
  const content = fs.readFileSync(filePath, "utf8");
262
553
  const lines = content.split("\n");
554
+ const regex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
263
555
 
264
556
  for (let i = 0; i < lines.length; i++) {
265
- if (/app\.listen\s*\(/.test(lines[i])) {
557
+ if (regex.test(lines[i])) {
558
+ const indentation = lines[i].match(/^(\s*)/)[1].length;
559
+ if (indentation > 0) {
560
+ return { lineIndex: i, lineNumber: i + 1, insideCallback: true };
561
+ }
562
+ }
563
+ }
564
+ return null;
565
+ }
566
+
567
+ function findCreateServer(filePath) {
568
+ const content = fs.readFileSync(filePath, "utf8");
569
+ const lines = content.split("\n");
570
+ for (let i = 0; i < lines.length; i++) {
571
+ if (/createServer\s*\(\s*app\s*\)/.test(lines[i])) {
266
572
  return { lineIndex: i, lineNumber: i + 1, content: lines[i] };
267
573
  }
268
574
  }
@@ -355,12 +661,7 @@ function detectAuth(pkg) {
355
661
  "jwt",
356
662
  "express-session",
357
663
  ].includes(lib.name);
358
- return {
359
- name: lib.name,
360
- version,
361
- package: pkg2,
362
- supported,
363
- };
664
+ return { name: lib.name, version, package: pkg2, supported };
364
665
  }
365
666
  }
366
667
 
@@ -370,7 +671,6 @@ function detectAuth(pkg) {
370
671
  // ─── NEXT-AUTH CONFIG LOCATION ───────────────────────────────────────────────
371
672
 
372
673
  function findNextAuthConfig(cwd) {
373
- // Common locations for authOptions
374
674
  const candidates = [
375
675
  "pages/api/auth/[...nextauth].js",
376
676
  "pages/api/auth/[...nextauth].ts",
@@ -387,7 +687,7 @@ function findNextAuthConfig(cwd) {
387
687
  "utils/auth.js",
388
688
  "utils/auth.ts",
389
689
  "auth.js",
390
- "auth.ts", // next-auth v5
690
+ "auth.ts",
391
691
  ];
392
692
 
393
693
  for (const candidate of candidates) {
@@ -397,13 +697,9 @@ function findNextAuthConfig(cwd) {
397
697
  }
398
698
  }
399
699
 
400
- // Search for authOptions in files
401
700
  const found = findFileWithContent(cwd, "authOptions", [".js", ".ts"], 3);
402
701
  if (found) {
403
- return {
404
- path: found,
405
- relativePath: path.relative(cwd, found),
406
- };
702
+ return { path: found, relativePath: path.relative(cwd, found) };
407
703
  }
408
704
 
409
705
  return null;
@@ -434,16 +730,6 @@ function findFileWithContent(dir, searchString, extensions, maxDepth) {
434
730
  function walk(currentDir, depth) {
435
731
  if (depth > maxDepth) return null;
436
732
 
437
- // Skip node_modules, .git, .next, dist, build
438
- const skipDirs = [
439
- "node_modules",
440
- ".git",
441
- ".next",
442
- "dist",
443
- "build",
444
- ".cache",
445
- ];
446
-
447
733
  let entries;
448
734
  try {
449
735
  entries = fs.readdirSync(currentDir);
@@ -452,7 +738,7 @@ function findFileWithContent(dir, searchString, extensions, maxDepth) {
452
738
  }
453
739
 
454
740
  for (const entry of entries) {
455
- if (skipDirs.includes(entry)) continue;
741
+ if (SKIP_DIRS.includes(entry)) continue;
456
742
 
457
743
  const fullPath = path.join(currentDir, entry);
458
744
  let stat;
@@ -481,23 +767,103 @@ function findFileWithContent(dir, searchString, extensions, maxDepth) {
481
767
  return walk(dir, 0);
482
768
  }
483
769
 
770
+ function detectAppVarName(filePath) {
771
+ try {
772
+ const content = fs.readFileSync(filePath, "utf8");
773
+ const match = content.match(/(?:const|let|var)\s+(\w+)\s*=\s*express\s*\(/);
774
+ return match ? match[1] : "app";
775
+ } catch {
776
+ return "app";
777
+ }
778
+ }
779
+
484
780
  // ─── MAIN DETECT FUNCTION ────────────────────────────────────────────────────
485
781
 
486
782
  function detect(cwd) {
487
783
  const pkg = readPackageJson(cwd);
488
784
  const monorepo = detectMonorepo(cwd);
489
- const framework = detectFramework(pkg);
490
- const moduleSystem = detectModuleSystem(pkg);
491
- const isTypeScript = detectTypeScript(cwd);
492
- const hasSrc = detectSrcDir(cwd);
493
- const auth = detectAuth(pkg);
494
- const packageManager = detectPackageManager(cwd);
785
+ let framework = detectFramework(pkg);
786
+
787
+ // ── If framework not found in root, scan ALL package.json files ───────────
788
+ let backendDir = cwd;
789
+ let frontendDir = null;
790
+ let frontendPkg = null;
791
+
792
+ if (!framework.name) {
793
+ const allPackages = scanAllPackageJsons(cwd);
794
+
795
+ for (const { dir, pkg: subPkg } of allPackages) {
796
+ // Skip the root package.json — already checked
797
+ if (dir === cwd) continue;
798
+
799
+ const classification = classifyPackageJson(subPkg);
800
+
801
+ // AFTER
802
+ if (
803
+ (classification === "backend" || classification === "fullstack") &&
804
+ !framework.name
805
+ ) {
806
+ framework = detectFramework(subPkg);
807
+ backendDir = dir;
808
+ }
809
+
810
+ if (
811
+ (classification === "frontend" || classification === "fullstack") &&
812
+ !frontendDir &&
813
+ dir !== backendDir
814
+ ) {
815
+ frontendDir = dir;
816
+ frontendPkg = subPkg;
817
+ }
818
+ }
819
+ } else {
820
+ // Framework found in root — scan for separate frontend folder
821
+ const allPackages = scanAllPackageJsons(cwd);
822
+ for (const { dir, pkg: subPkg } of allPackages) {
823
+ if (dir === cwd) continue;
824
+ if (dir === backendDir) continue;
825
+ const classification = classifyPackageJson(subPkg);
826
+ if (classification === "frontend" || classification === "fullstack") {
827
+ frontendDir = dir;
828
+ frontendPkg = subPkg;
829
+ break;
830
+ }
831
+ }
832
+ }
833
+
834
+ // ── Use backendDir for all backend-specific detection ─────────────────────
835
+ // Guard: if frontendDir ended up being the same as backendDir
836
+ // (e.g. a fullstack Next.js folder detected as both), clear frontendDir
837
+ // so we don't try to inject script tag into the wrong place
838
+ if (frontendDir && frontendDir === backendDir) {
839
+ frontendDir = null;
840
+ frontendPkg = null;
841
+ }
842
+ const backendPkg = readPackageJson(backendDir) || pkg;
843
+ const moduleSystem = detectModuleSystem(backendPkg);
844
+ const isTypeScript = detectTypeScript(backendDir);
845
+ const hasSrc = detectSrcDir(backendDir);
846
+ const auth = detectAuth(backendPkg);
847
+ const generateTs = shouldGenerateTs(backendDir, isTypeScript);
848
+
849
+ // ── Find frontend main file ───────────────────────────────────────────────
850
+ let frontendMainFile = null;
851
+ if (frontendDir && frontendPkg) {
852
+ frontendMainFile = findMainFrontendFile(frontendDir, frontendPkg);
853
+ } else if (framework.name === "next") {
854
+ // Next.js is fullstack — find its frontend file in backendDir
855
+ frontendMainFile = findMainFrontendFile(backendDir, backendPkg);
856
+ }
495
857
 
496
- const generateTs = shouldGenerateTs(cwd, isTypeScript);
858
+ const packageManager =
859
+ detectPackageManager(backendDir) !== "npm"
860
+ ? detectPackageManager(backendDir)
861
+ : detectPackageManager(cwd);
497
862
 
498
863
  const result = {
499
- cwd,
500
- pkg,
864
+ cwd: backendDir, // use backend dir as working dir
865
+ rootCwd: cwd, // keep original root for reference
866
+ pkg: backendPkg,
501
867
  monorepo,
502
868
  framework,
503
869
  moduleSystem,
@@ -506,34 +872,62 @@ function detect(cwd) {
506
872
  hasSrc,
507
873
  auth,
508
874
  packageManager,
509
- // generateTs: true means user has allowJs:false — must use .ts
510
- // generateTs: false means .js files are fine (most users)
511
875
  ext: generateTs ? ".ts" : ".js",
876
+ // Frontend info
877
+ frontendDir,
878
+ frontendPkg,
879
+ frontendMainFile,
512
880
  };
513
881
 
514
- // Framework-specific detection
882
+ // ── Framework-specific detection ──────────────────────────────────────────
515
883
  if (framework.name === "next") {
516
- result.next = detectNextRouter(cwd);
517
- result.nextVersion = detectNextVersion(pkg);
884
+ result.next = detectNextRouter(backendDir);
885
+ result.nextVersion = detectNextVersion(backendPkg);
518
886
  if (auth.name === "next-auth") {
519
- result.nextAuthConfig = findNextAuthConfig(cwd);
887
+ result.nextAuthConfig = findNextAuthConfig(backendDir);
520
888
  }
521
889
  }
522
890
 
523
891
  if (framework.name === "express") {
524
- result.entryPoint = detectExpressEntry(cwd, pkg);
892
+ result.entryPoint = detectExpressEntry(backendDir, backendPkg);
525
893
  if (result.entryPoint) {
526
- result.listenCall = findListenCall(result.entryPoint);
894
+ result.appVarName = detectAppVarName(result.entryPoint); // detect FIRST
895
+ result.listenCall = findListenCall(result.entryPoint, result.appVarName);
896
+ result.listenInsideCallback = findListenInsideCallback(
897
+ result.entryPoint,
898
+ result.appVarName,
899
+ );
900
+ result.moduleExportsApp = findModuleExportsApp(result.entryPoint);
901
+ result.createServer = findCreateServer(result.entryPoint);
902
+
903
+ const appFileCandidates = [
904
+ "src/app.js",
905
+ "src/app.ts",
906
+ "app.js",
907
+ "app.ts",
908
+ ];
909
+ for (const candidate of appFileCandidates) {
910
+ const fullPath = path.join(backendDir, candidate);
911
+ if (fs.existsSync(fullPath) && fullPath !== result.entryPoint) {
912
+ const exportCall = findModuleExportsApp(fullPath);
913
+ if (exportCall) {
914
+ result.appFile = fullPath;
915
+ result.appFileExport = exportCall;
916
+ break;
917
+ }
918
+ }
919
+ }
527
920
  }
528
921
  }
529
922
 
923
+ // ── Already initialized check ─────────────────────────────────────────────
530
924
  result.alreadyInitialized =
531
925
  detectExistingBotVersion(result.entryPoint) ||
532
926
  (framework.name === "next" &&
533
- (fs.existsSync(path.join(cwd, "instrumentation.js")) ||
534
- fs.existsSync(path.join(cwd, "instrumentation.ts")) ||
535
- fs.existsSync(path.join(cwd, "src", "instrumentation.js")) ||
536
- fs.existsSync(path.join(cwd, "src", "instrumentation.ts"))));
927
+ (fs.existsSync(path.join(backendDir, "instrumentation.js")) ||
928
+ fs.existsSync(path.join(backendDir, "instrumentation.ts")) ||
929
+ fs.existsSync(path.join(backendDir, "src", "instrumentation.js")) ||
930
+ fs.existsSync(path.join(backendDir, "src", "instrumentation.ts"))));
537
931
 
538
932
  return result;
539
933
  }
@@ -541,6 +935,10 @@ function detect(cwd) {
541
935
  module.exports = {
542
936
  detect,
543
937
  readPackageJson,
938
+ scanAllPackageJsons,
939
+ classifyPackageJson,
940
+ detectFrontendFramework,
941
+ findMainFrontendFile,
544
942
  detectMonorepo,
545
943
  detectFramework,
546
944
  detectModuleSystem,
@@ -555,4 +953,8 @@ module.exports = {
555
953
  detectExistingBotVersion,
556
954
  findFileWithContent,
557
955
  findListenCall,
956
+ findModuleExportsApp,
957
+ findListenInsideCallback,
958
+ findCreateServer,
959
+ detectAppVarName,
558
960
  };