botversion-sdk 1.1.6 → 2.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.
package/cli/detector.js DELETED
@@ -1,1149 +0,0 @@
1
- // botversion-sdk/cli/detector.js
2
- "use strict";
3
-
4
- const fs = require("fs");
5
- const path = require("path");
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
-
23
- // ─── PACKAGE JSON ────────────────────────────────────────────────────────────
24
-
25
- function readPackageJson(cwd) {
26
- const pkgPath = path.join(cwd, "package.json");
27
- if (!fs.existsSync(pkgPath)) return null;
28
- try {
29
- return JSON.parse(fs.readFileSync(pkgPath, "utf8"));
30
- } catch {
31
- return null;
32
- }
33
- }
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
-
304
- // ─── MONOREPO DETECTION ──────────────────────────────────────────────────────
305
-
306
- function detectMonorepo(cwd) {
307
- const rootPkg = readPackageJson(cwd);
308
- if (rootPkg && rootPkg.workspaces) {
309
- const workspaceDirs = [];
310
- const patterns = Array.isArray(rootPkg.workspaces)
311
- ? rootPkg.workspaces
312
- : rootPkg.workspaces.packages || [];
313
-
314
- patterns.forEach((pattern) => {
315
- const base = pattern.replace(/\/\*$/, "");
316
- const fullBase = path.join(cwd, base);
317
- if (fs.existsSync(fullBase)) {
318
- fs.readdirSync(fullBase).forEach((dir) => {
319
- const dirPath = path.join(fullBase, dir);
320
- if (
321
- fs.statSync(dirPath).isDirectory() &&
322
- fs.existsSync(path.join(dirPath, "package.json"))
323
- ) {
324
- workspaceDirs.push(dirPath);
325
- }
326
- });
327
- }
328
- });
329
-
330
- if (workspaceDirs.length > 0) {
331
- return { isMonorepo: true, packages: workspaceDirs };
332
- }
333
- }
334
-
335
- // Check for common monorepo structures
336
- const monorepoDirs = ["packages", "apps", "services"];
337
- for (const dir of monorepoDirs) {
338
- const dirPath = path.join(cwd, dir);
339
- if (fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
340
- const subDirs = fs
341
- .readdirSync(dirPath)
342
- .map((d) => path.join(dirPath, d))
343
- .filter(
344
- (d) =>
345
- fs.statSync(d).isDirectory() &&
346
- fs.existsSync(path.join(d, "package.json")),
347
- );
348
- if (subDirs.length > 0) {
349
- return { isMonorepo: true, packages: subDirs };
350
- }
351
- }
352
- }
353
-
354
- return { isMonorepo: false };
355
- }
356
-
357
- // ─── FRAMEWORK DETECTION ─────────────────────────────────────────────────────
358
-
359
- const SUPPORTED_FRAMEWORKS = ["next", "express"];
360
- const UNSUPPORTED_FRAMEWORKS = [
361
- "fastify",
362
- "koa",
363
- "@hapi/hapi",
364
- "nestjs",
365
- "@nestjs/core",
366
- ];
367
-
368
- function detectFramework(pkg) {
369
- if (!pkg) return { name: null, supported: false };
370
-
371
- const deps = {
372
- ...(pkg.dependencies || {}),
373
- ...(pkg.devDependencies || {}),
374
- };
375
-
376
- for (const fw of UNSUPPORTED_FRAMEWORKS) {
377
- if (deps[fw]) return { name: fw, supported: false };
378
- }
379
-
380
- // If BOTH next and express exist, always pick next
381
- // because express is just being used as a custom server wrapper
382
- if (deps["next"]) return { name: "next", supported: true };
383
-
384
- for (const fw of SUPPORTED_FRAMEWORKS) {
385
- if (deps[fw]) return { name: fw, supported: true };
386
- }
387
-
388
- return { name: null, supported: false };
389
- }
390
-
391
- // ─── MODULE SYSTEM DETECTION ─────────────────────────────────────────────────
392
-
393
- function detectModuleSystem(pkg) {
394
- if (pkg && pkg.type === "module") return "esm";
395
- return "cjs";
396
- }
397
-
398
- // ─── TYPESCRIPT DETECTION ────────────────────────────────────────────────────
399
-
400
- function detectTypeScript(cwd) {
401
- return (
402
- fs.existsSync(path.join(cwd, "tsconfig.json")) ||
403
- fs.existsSync(path.join(cwd, "tsconfig.base.json"))
404
- );
405
- }
406
-
407
- function readTsConfig(cwd) {
408
- const tsconfigPath = path.join(cwd, "tsconfig.json");
409
- if (!fs.existsSync(tsconfigPath)) return null;
410
- try {
411
- const raw = fs.readFileSync(tsconfigPath, "utf8");
412
- const stripped = raw
413
- .replace(/\/\/.*$/gm, "")
414
- .replace(/\/\*[\s\S]*?\*\//g, "");
415
- return JSON.parse(stripped);
416
- } catch {
417
- return null;
418
- }
419
- }
420
-
421
- function shouldGenerateTs(cwd, isTypeScript) {
422
- if (!isTypeScript) return false;
423
- const tsconfig = readTsConfig(cwd);
424
- if (!tsconfig) return false;
425
- const allowJs = tsconfig.compilerOptions && tsconfig.compilerOptions.allowJs;
426
- return allowJs === false;
427
- }
428
-
429
- // ─── SRC DIRECTORY DETECTION ─────────────────────────────────────────────────
430
-
431
- function detectSrcDir(cwd) {
432
- return fs.existsSync(path.join(cwd, "src"));
433
- }
434
-
435
- // ─── NEXT.JS ROUTER DETECTION ────────────────────────────────────────────────
436
-
437
- function detectNextRouter(cwd) {
438
- const hasSrc = detectSrcDir(cwd);
439
- const bases = hasSrc ? [path.join(cwd, "src"), cwd] : [cwd];
440
-
441
- let hasPages = false;
442
- let hasApp = false;
443
-
444
- for (const base of bases) {
445
- if (fs.existsSync(path.join(base, "pages", "api"))) hasPages = true;
446
- if (fs.existsSync(path.join(base, "pages"))) hasPages = true;
447
- if (fs.existsSync(path.join(base, "app"))) hasApp = true;
448
- }
449
-
450
- return {
451
- pagesRouter: hasPages,
452
- appRouter: hasApp,
453
- srcDir: hasSrc,
454
- baseDir: hasSrc ? path.join(cwd, "src") : cwd,
455
- };
456
- }
457
-
458
- // ─── NEXT.JS VERSION DETECTION ───────────────────────────────────────────────
459
-
460
- function detectNextVersion(pkg) {
461
- if (!pkg) return null;
462
- const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
463
- const version = deps["next"];
464
- if (!version) return null;
465
- const major = parseInt(version.replace(/[^0-9]/, ""), 10);
466
- return { raw: version, major };
467
- }
468
-
469
- // ─── SCORING FUNCTIONS ────────────────────────────────────────────────────────
470
-
471
- function scoreExpressFile(content, filePath) {
472
- let score = 0;
473
- const filename = path.basename(filePath);
474
- const normalizedPath = filePath.replace(/\\/g, "/");
475
-
476
- // ── EXISTING POSITIVE SIGNALS ──────────────────────────────────
477
- if (/express\s*\(\s*\)/.test(content)) score += 10;
478
- if (/class\s+\w+\s+extends\s+express/.test(content)) score += 10;
479
- if (/(?:function|const|let|var)\s+(create|make|build|setup)App/.test(content))
480
- score += 8;
481
- if (/\.listen\s*\(/.test(content)) score += 8;
482
- if (/\.use\s*\(\s*['"`\/]/.test(content)) score += 5;
483
- if (/app\.use\s*\(/.test(content)) score += 4;
484
- if (
485
- /require\s*\(\s*['"]express['"]\s*\)|from\s+['"]express['"]/.test(content)
486
- )
487
- score += 1;
488
-
489
- // ── NEW POSITIVE SIGNALS — real API server ─────────────────────
490
- const hasApiRoutes = /\.(get|post|put|delete|patch)\s*\(\s*['"`]\//.test(
491
- content,
492
- );
493
- const returnsJson = /res\.json\s*\(/.test(content);
494
- const hasRouterFiles = /require\s*\(['"`]\.\/routes/.test(content);
495
- const hasControllers = /require\s*\(['"`]\.\/controllers/.test(content);
496
-
497
- if (hasApiRoutes) score += 6;
498
- if (returnsJson) score += 5;
499
- if (hasRouterFiles) score += 4;
500
- if (hasControllers) score += 3;
501
-
502
- // ── EXISTING PENALTIES ─────────────────────────────────────────
503
- if (/^(?:const|let|var)\s+router\s*=\s*express\.Router\s*\(/m.test(content))
504
- score -= 8;
505
- if (/(test_|_test|\.test\.|\.spec\.|conftest)/.test(filename)) score -= 10;
506
- if (/dist\/|build\//.test(filePath)) score -= 10;
507
-
508
- // ── NEW PENALTIES ──────────────────────────────────────────────
509
-
510
- // Category 1: PHP servers
511
- if (content.includes("php-express")) score -= 20;
512
- if (content.includes("phpExpress")) score -= 20;
513
- if (/app\.engine\s*\(\s*['"]php['"]/.test(content)) score -= 20;
514
-
515
- // Category 2: View-only servers (any template engine, no API)
516
- const hasViewEngine = /app\.set\s*\(\s*['"]view engine['"]/.test(content);
517
- if (hasViewEngine && !hasApiRoutes && !returnsJson) score -= 15;
518
-
519
- // Category 3: Static-only servers
520
- const hasStatic = /express\.static\s*\(/.test(content);
521
- const hasOnlyStatic = hasStatic && !hasApiRoutes && !returnsJson;
522
- if (hasOnlyStatic) score -= 15;
523
-
524
- // Category 4: Proxy servers
525
- if (
526
- content.includes("http-proxy-middleware") ||
527
- content.includes("createProxyMiddleware")
528
- )
529
- score -= 15;
530
-
531
- // Category 5: WebSocket-only servers
532
- const hasSocketIO =
533
- content.includes("socket.io") || content.includes("new Server(");
534
- if (hasSocketIO && !hasApiRoutes) score -= 15;
535
-
536
- // Category 6: Job/Queue servers
537
- if (
538
- (content.includes("require('bull')") ||
539
- content.includes('require("bull")') ||
540
- content.includes("bullmq") ||
541
- content.includes("agenda")) &&
542
- !hasApiRoutes
543
- )
544
- score -= 15;
545
-
546
- // Category 7: Webhook-only servers
547
- const hasWebhook = /['"`]\/webhook['"`]/.test(content);
548
- const onlyWebhook = hasWebhook && !hasApiRoutes;
549
- if (onlyWebhook) score -= 10;
550
-
551
- // Category 8: Dev tooling servers
552
- if (
553
- content.includes("webpack-dev-middleware") ||
554
- content.includes("webpackDevMiddleware") ||
555
- content.includes("storybook")
556
- )
557
- score -= 15;
558
-
559
- // Category 9: Test/mock servers (path-based)
560
- if (/__mocks__|fixtures\/|test\/|__tests__\//.test(normalizedPath))
561
- score -= 15;
562
-
563
- // ── FILENAME BONUSES ───────────────────────────────────────────
564
- if (["server.js", "server.ts", "app.js", "app.ts"].includes(filename))
565
- score += 3;
566
- if (["index.js", "index.ts", "main.js", "main.ts"].includes(filename))
567
- score += 2;
568
-
569
- return score;
570
- }
571
-
572
- // ─── PARSE ENTRY FROM CONFIG FILES ───────────────────────────────────────────
573
-
574
- function parseEntryFromConfigFiles(cwd) {
575
- /**
576
- * Extracts likely entry point from Procfile or Dockerfile.
577
- * Covers patterns like:
578
- * Procfile: web: node server.js
579
- * Procfile: web: nodemon src/index.js
580
- * Dockerfile: CMD ["node", "server.js"]
581
- * Dockerfile: CMD ["nodemon", "src/index.js"]
582
- */
583
-
584
- function resolveFile(cwd, filePath) {
585
- const full = path.join(cwd, filePath);
586
- if (fs.existsSync(full)) return full;
587
- return null;
588
- }
589
-
590
- // ── 1. Check Procfile ─────────────────────────────────────────────────────
591
- const procfilePath = path.join(cwd, "Procfile");
592
- if (fs.existsSync(procfilePath)) {
593
- try {
594
- const lines = fs.readFileSync(procfilePath, "utf8").split("\n");
595
- for (const line of lines) {
596
- if (!line.trim() || line.startsWith("#")) continue;
597
-
598
- // node/nodemon/ts-node/tsx server.js or src/index.ts
599
- const match = line.match(
600
- /(?:node|nodemon|ts-node|ts-node-dev|tsx)\s+([\w./\\-]+\.[jt]s)/,
601
- );
602
- if (match) {
603
- const result = resolveFile(cwd, match[1]);
604
- if (result) return result;
605
- }
606
- }
607
- } catch {}
608
- }
609
-
610
- // ── 2. Check Dockerfile ───────────────────────────────────────────────────
611
- const dockerfilePath = path.join(cwd, "Dockerfile");
612
- if (fs.existsSync(dockerfilePath)) {
613
- try {
614
- const lines = fs.readFileSync(dockerfilePath, "utf8").split("\n");
615
- for (const line of lines) {
616
- if (!line.trim() || line.startsWith("#")) continue;
617
-
618
- // CMD ["node", "server.js"]
619
- // CMD ["nodemon", "src/index.js"]
620
- const cmdMatch = line.match(
621
- /(?:CMD|ENTRYPOINT)\s*\[.*?"(?:node|nodemon|ts-node|tsx)",\s*"([\w./\\-]+\.[jt]s)"/,
622
- );
623
- if (cmdMatch) {
624
- const result = resolveFile(cwd, cmdMatch[1]);
625
- if (result) return result;
626
- }
627
-
628
- // CMD node server.js (shell form)
629
- const shellMatch = line.match(
630
- /(?:CMD|ENTRYPOINT)\s+(?:node|nodemon|ts-node|tsx)\s+([\w./\\-]+\.[jt]s)/,
631
- );
632
- if (shellMatch) {
633
- const result = resolveFile(cwd, shellMatch[1]);
634
- if (result) return result;
635
- }
636
- }
637
- } catch {}
638
- }
639
-
640
- return null;
641
- }
642
-
643
- function detectExpressEntry(cwd, pkg) {
644
- const SKIP_ENTRY_DIRS = ["dist", "build", "out", ".next", "coverage"];
645
-
646
- function isCompiledPath(filePath) {
647
- const normalized = filePath.replace(/\\/g, "/");
648
- return SKIP_ENTRY_DIRS.some(
649
- (dir) =>
650
- normalized.includes(`/${dir}/`) ||
651
- normalized.endsWith(`/${dir}`) ||
652
- normalized.startsWith(`${dir}/`),
653
- );
654
- }
655
-
656
- // ── Strategy 1: package.json "main" field ─────────────────────────────────
657
- if (pkg && pkg.main) {
658
- const mainPath = path.join(cwd, pkg.main);
659
- if (fs.existsSync(mainPath) && !isCompiledPath(pkg.main)) {
660
- return mainPath;
661
- }
662
- }
663
-
664
- // ── Strategy 2: scripts.start / scripts.dev ───────────────────────────────
665
- if (pkg && pkg.scripts) {
666
- const scripts = [
667
- pkg.scripts.dev,
668
- pkg.scripts.serve,
669
- pkg.scripts["dev-server"],
670
- pkg.scripts.start,
671
- ];
672
- for (const script of scripts) {
673
- if (!script) continue;
674
- const match = script.match(
675
- /(?:node|nodemon|ts-node|ts-node-dev|tsx)\s+([^\s]+\.(js|ts))/,
676
- );
677
- if (match) {
678
- const scriptPath = match[1];
679
- if (isCompiledPath(scriptPath)) continue;
680
- const filePath = path.join(cwd, scriptPath);
681
- if (fs.existsSync(filePath)) return filePath;
682
- }
683
- }
684
-
685
- // Strategy 2b: compiled path fallback — find TS source equivalent
686
- for (const script of scripts) {
687
- if (!script) continue;
688
- const match = script.match(
689
- /(?:node|nodemon|ts-node|ts-node-dev|tsx)\s+([^\s]+\.(js|ts))/,
690
- );
691
- if (match) {
692
- const tsEquivalent = match[1]
693
- .replace(/^backend\/dist\//, "backend/")
694
- .replace(/^dist\//, "")
695
- .replace(/\.js$/, ".ts");
696
- const filePath = path.join(cwd, tsEquivalent);
697
- if (fs.existsSync(filePath)) return filePath;
698
- }
699
- }
700
- }
701
-
702
- // ── Strategy 3: Scoring — scan all JS/TS files ────────────────────────────
703
- const skipDirs = new Set([
704
- ...SKIP_DIRS,
705
- "dist",
706
- "build",
707
- "out",
708
- "coverage",
709
- "tests",
710
- "test",
711
- "__tests__",
712
- "scripts",
713
- "docs",
714
- ]);
715
-
716
- const scoredCandidates = [];
717
-
718
- function walk(directory, depth) {
719
- if (depth > 3) return;
720
- let entries;
721
- try {
722
- entries = fs.readdirSync(directory);
723
- } catch {
724
- return;
725
- }
726
-
727
- for (const entry of entries) {
728
- if (skipDirs.has(entry) || entry.startsWith(".")) continue;
729
-
730
- const fullPath = path.join(directory, entry);
731
- let stat;
732
- try {
733
- stat = fs.statSync(fullPath);
734
- } catch {
735
- continue;
736
- }
737
-
738
- if (stat.isDirectory()) {
739
- walk(fullPath, depth + 1);
740
- } else if (/\.(js|ts)$/.test(entry) && !/\.d\.ts$/.test(entry)) {
741
- try {
742
- const content = fs.readFileSync(fullPath, "utf8");
743
- const score = scoreExpressFile(content, fullPath);
744
- if (score > 0) {
745
- scoredCandidates.push({ score, filePath: fullPath });
746
- }
747
- } catch {
748
- continue;
749
- }
750
- }
751
- }
752
- }
753
-
754
- walk(cwd, 0);
755
-
756
- if (scoredCandidates.length > 0) {
757
- scoredCandidates.sort((a, b) => b.score - a.score);
758
- const best = scoredCandidates[0];
759
-
760
- // If best score is weak, cross-check with Procfile/Dockerfile
761
- if (best.score <= 3) {
762
- const configEntry = parseEntryFromConfigFiles(cwd);
763
- if (configEntry) return configEntry;
764
- }
765
-
766
- return best.filePath;
767
- }
768
-
769
- // ── Strategy 4: Procfile/Dockerfile fallback ──────────────────────────────
770
- const configEntry = parseEntryFromConfigFiles(cwd);
771
- if (configEntry) return configEntry;
772
-
773
- // ── Strategy 5: Last resort deep scan ────────────────────────────────────
774
- return findFileWithContent(cwd, ".listen(", [".js", ".ts"], 2);
775
- }
776
-
777
- // ─── app.listen() LOCATION ───────────────────────────────────────────────────
778
-
779
- function findListenCall(filePath, appVarName) {
780
- appVarName = appVarName || "app";
781
- const content = fs.readFileSync(filePath, "utf8");
782
- const lines = content.split("\n");
783
- const regex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
784
-
785
- for (let i = 0; i < lines.length; i++) {
786
- if (regex.test(lines[i])) {
787
- return { lineIndex: i, lineNumber: i + 1, content: lines[i] };
788
- }
789
- }
790
- return null;
791
- }
792
-
793
- function findModuleExportsApp(filePath) {
794
- const content = fs.readFileSync(filePath, "utf8");
795
- const lines = content.split("\n");
796
- for (let i = 0; i < lines.length; i++) {
797
- if (/module\.exports\s*=\s*app/.test(lines[i])) {
798
- return { lineIndex: i, lineNumber: i + 1, content: lines[i] };
799
- }
800
- }
801
- return null;
802
- }
803
-
804
- function findListenInsideCallback(filePath, appVarName) {
805
- appVarName = appVarName || "app";
806
- const content = fs.readFileSync(filePath, "utf8");
807
- const lines = content.split("\n");
808
- const regex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
809
-
810
- for (let i = 0; i < lines.length; i++) {
811
- if (regex.test(lines[i])) {
812
- const indentation = lines[i].match(/^(\s*)/)[1].length;
813
- if (indentation > 0) {
814
- return { lineIndex: i, lineNumber: i + 1, insideCallback: true };
815
- }
816
- }
817
- }
818
- return null;
819
- }
820
-
821
- function findCreateServer(filePath) {
822
- const content = fs.readFileSync(filePath, "utf8");
823
- const lines = content.split("\n");
824
- for (let i = 0; i < lines.length; i++) {
825
- if (/createServer\s*\(\s*app\s*\)/.test(lines[i])) {
826
- return { lineIndex: i, lineNumber: i + 1, content: lines[i] };
827
- }
828
- }
829
- return null;
830
- }
831
-
832
- // ─── PACKAGE MANAGER DETECTION ───────────────────────────────────────────────
833
-
834
- function detectPackageManager(cwd) {
835
- if (fs.existsSync(path.join(cwd, "bun.lockb"))) return "bun";
836
- if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
837
- if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
838
- return "npm";
839
- }
840
-
841
- // ─── EXISTING BOTVERSION DETECTION ───────────────────────────────────────────
842
-
843
- function detectExistingBotVersion(filePath) {
844
- if (!filePath || !fs.existsSync(filePath)) return false;
845
- const content = fs.readFileSync(filePath, "utf8");
846
- return content.includes("botversion-sdk") || content.includes("BotVersion");
847
- }
848
-
849
- // ─── HELPER: find file containing a string ───────────────────────────────────
850
-
851
- function findFileWithContent(dir, searchString, extensions, maxDepth) {
852
- maxDepth = maxDepth || 2;
853
-
854
- function walk(currentDir, depth) {
855
- if (depth > maxDepth) return null;
856
-
857
- let entries;
858
- try {
859
- entries = fs.readdirSync(currentDir);
860
- } catch {
861
- return null;
862
- }
863
-
864
- for (const entry of entries) {
865
- if (SKIP_DIRS.includes(entry)) continue;
866
-
867
- const fullPath = path.join(currentDir, entry);
868
- let stat;
869
- try {
870
- stat = fs.statSync(fullPath);
871
- } catch {
872
- continue;
873
- }
874
-
875
- if (stat.isDirectory()) {
876
- const result = walk(fullPath, depth + 1);
877
- if (result) return result;
878
- } else if (extensions.some((ext) => entry.endsWith(ext))) {
879
- try {
880
- const content = fs.readFileSync(fullPath, "utf8");
881
- if (content.includes(searchString)) return fullPath;
882
- } catch {
883
- continue;
884
- }
885
- }
886
- }
887
-
888
- return null;
889
- }
890
-
891
- return walk(dir, 0);
892
- }
893
-
894
- function detectAppVarName(filePath) {
895
- try {
896
- const content = fs.readFileSync(filePath, "utf8");
897
-
898
- // Pattern 1: const app = express()
899
- const match1 = content.match(
900
- /(?:const|let|var)\s+(\w+)\s*=\s*express\s*\(/,
901
- );
902
- if (match1) return match1[1];
903
-
904
- // Pattern 2: const app = require('express')()
905
- const match2 = content.match(
906
- /(?:const|let|var)\s+(\w+)\s*=\s*require\s*\(\s*['"]express['"]\s*\)\s*\(/,
907
- );
908
- if (match2) return match2[1];
909
-
910
- // Pattern 3: factory function — createApp(), makeApp() etc.
911
- // const app = createApp()
912
- const match3 = content.match(
913
- /(?:const|let|var)\s+(\w+)\s*=\s*(?:create|make|build|setup)App\s*\(/,
914
- );
915
- if (match3) return match3[1];
916
-
917
- // Pattern 4: subclass — class MyApp extends express
918
- // const app = new MyApp()
919
- const subclassNames = [
920
- ...content.matchAll(/class\s+(\w+)\s+extends\s+express/g),
921
- ].map((m) => m[1]);
922
- for (const name of subclassNames) {
923
- const subMatch = content.match(
924
- new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=\\s*new\\s+${name}\\s*\\(`),
925
- );
926
- if (subMatch) return subMatch[1];
927
- }
928
-
929
- // Pattern 5: const server = app.listen( → return "app"
930
- const match5 = content.match(
931
- /(?:const|let|var)\s+(\w+)\s*=\s*(\w+)\.listen\s*\(/,
932
- );
933
- if (match5) return match5[2];
934
-
935
- // Pattern 6: app.listen( or server.listen(
936
- const match6 = content.match(
937
- /\b(app|server|router|handler|httpServer)\s*\.listen\s*\(/,
938
- );
939
- if (match6) return match6[1];
940
-
941
- return "app";
942
- } catch {
943
- return "app";
944
- }
945
- }
946
-
947
- function detectDotenv(cwd, pkg, entryPoint) {
948
- // Check if dotenv is in dependencies
949
- const deps = {
950
- ...(pkg.dependencies || {}),
951
- ...(pkg.devDependencies || {}),
952
- };
953
- const hasDotenvPackage = !!deps["dotenv"];
954
-
955
- // Check if dotenv is actually being called in entry file
956
- let hasDotenvCall = false;
957
- if (entryPoint && fs.existsSync(entryPoint)) {
958
- try {
959
- const content = fs.readFileSync(entryPoint, "utf8");
960
- hasDotenvCall =
961
- content.includes("dotenv") ||
962
- content.includes("require('dotenv')") ||
963
- content.includes('require("dotenv")');
964
- } catch {}
965
- }
966
-
967
- return hasDotenvPackage || hasDotenvCall;
968
- }
969
-
970
- // ─── MAIN DETECT FUNCTION ────────────────────────────────────────────────────
971
-
972
- function detect(cwd) {
973
- const pkg = readPackageJson(cwd);
974
- const monorepo = detectMonorepo(cwd);
975
- let framework = detectFramework(pkg);
976
-
977
- // ── If framework not found in root, scan ALL package.json files ───────────
978
- let backendDir = cwd;
979
- let frontendDir = null;
980
- let frontendPkg = null;
981
-
982
- if (!framework.name) {
983
- const allPackages = scanAllPackageJsons(cwd);
984
-
985
- for (const { dir, pkg: subPkg } of allPackages) {
986
- // Skip the root package.json — already checked
987
- if (dir === cwd) continue;
988
-
989
- const classification = classifyPackageJson(subPkg);
990
-
991
- // AFTER
992
- if (
993
- (classification === "backend" || classification === "fullstack") &&
994
- !framework.name
995
- ) {
996
- framework = detectFramework(subPkg);
997
- backendDir = dir;
998
- }
999
-
1000
- if (
1001
- (classification === "frontend" || classification === "fullstack") &&
1002
- !frontendDir &&
1003
- dir !== backendDir
1004
- ) {
1005
- frontendDir = dir;
1006
- frontendPkg = subPkg;
1007
- }
1008
- }
1009
- } else {
1010
- // Framework found in root — scan for separate frontend folder
1011
- const allPackages = scanAllPackageJsons(cwd);
1012
- for (const { dir, pkg: subPkg } of allPackages) {
1013
- if (dir === cwd) continue;
1014
- if (dir === backendDir) continue;
1015
- const classification = classifyPackageJson(subPkg);
1016
- if (classification === "frontend" || classification === "fullstack") {
1017
- frontendDir = dir;
1018
- frontendPkg = subPkg;
1019
- break;
1020
- }
1021
- }
1022
- }
1023
-
1024
- // ── Use backendDir for all backend-specific detection ─────────────────────
1025
- // Guard: if frontendDir ended up being the same as backendDir
1026
- // (e.g. a fullstack Next.js folder detected as both), clear frontendDir
1027
- // so we don't try to inject script tag into the wrong place
1028
- if (frontendDir && frontendDir === backendDir) {
1029
- frontendDir = null;
1030
- frontendPkg = null;
1031
- }
1032
- const backendPkg = readPackageJson(backendDir) || pkg;
1033
- const moduleSystem = detectModuleSystem(backendPkg);
1034
- const isTypeScript = detectTypeScript(backendDir);
1035
- const hasSrc = detectSrcDir(backendDir);
1036
- const generateTs = shouldGenerateTs(backendDir, isTypeScript);
1037
-
1038
- // ── Find frontend main file ───────────────────────────────────────────────
1039
- let frontendMainFile = null;
1040
- if (frontendDir && frontendPkg) {
1041
- frontendMainFile = findMainFrontendFile(frontendDir, frontendPkg);
1042
- } else if (framework.name === "next") {
1043
- frontendMainFile = findMainFrontendFile(backendDir, backendPkg);
1044
- } else if (framework.name === "express" && !frontendDir) {
1045
- // Express + frontend in same root folder (e.g. public/index.html)
1046
- frontendMainFile = findMainFrontendFile(backendDir, backendPkg);
1047
- }
1048
-
1049
- const packageManager =
1050
- detectPackageManager(backendDir) !== "npm"
1051
- ? detectPackageManager(backendDir)
1052
- : detectPackageManager(cwd);
1053
-
1054
- const result = {
1055
- cwd: backendDir, // use backend dir as working dir
1056
- rootCwd: cwd, // keep original root for reference
1057
- pkg: backendPkg,
1058
- monorepo,
1059
- framework,
1060
- moduleSystem,
1061
- isTypeScript,
1062
- generateTs,
1063
- hasSrc,
1064
- packageManager,
1065
- ext: generateTs ? ".ts" : ".js",
1066
- // Frontend info
1067
- frontendDir,
1068
- frontendPkg,
1069
- frontendMainFile,
1070
- };
1071
-
1072
- // ── Framework-specific detection ──────────────────────────────────────────
1073
- if (framework.name === "next") {
1074
- result.next = detectNextRouter(backendDir);
1075
- result.nextVersion = detectNextVersion(backendPkg);
1076
- }
1077
-
1078
- if (framework.name === "express") {
1079
- result.entryPoint = detectExpressEntry(backendDir, backendPkg);
1080
- result.hasDotenv = detectDotenv(backendDir, backendPkg, result.entryPoint);
1081
- if (result.entryPoint) {
1082
- result.appVarName = detectAppVarName(result.entryPoint); // detect FIRST
1083
- result.listenCall = findListenCall(result.entryPoint, result.appVarName);
1084
- result.listenInsideCallback = findListenInsideCallback(
1085
- result.entryPoint,
1086
- result.appVarName,
1087
- );
1088
- result.moduleExportsApp = findModuleExportsApp(result.entryPoint);
1089
- result.createServer = findCreateServer(result.entryPoint);
1090
-
1091
- const appFileCandidates = [
1092
- "src/app.js",
1093
- "src/app.ts",
1094
- "app.js",
1095
- "app.ts",
1096
- ];
1097
- for (const candidate of appFileCandidates) {
1098
- const fullPath = path.join(backendDir, candidate);
1099
- if (fs.existsSync(fullPath) && fullPath !== result.entryPoint) {
1100
- const exportCall = findModuleExportsApp(fullPath);
1101
- if (exportCall) {
1102
- result.appFile = fullPath;
1103
- result.appFileExport = exportCall;
1104
- break;
1105
- }
1106
- }
1107
- }
1108
- }
1109
- }
1110
-
1111
- // ── Already initialized check ─────────────────────────────────────────────
1112
- result.alreadyInitialized =
1113
- detectExistingBotVersion(result.entryPoint) ||
1114
- (framework.name === "next" &&
1115
- (fs.existsSync(path.join(backendDir, "instrumentation.js")) ||
1116
- fs.existsSync(path.join(backendDir, "instrumentation.ts")) ||
1117
- fs.existsSync(path.join(backendDir, "src", "instrumentation.js")) ||
1118
- fs.existsSync(path.join(backendDir, "src", "instrumentation.ts"))));
1119
-
1120
- return result;
1121
- }
1122
-
1123
- module.exports = {
1124
- detect,
1125
- readPackageJson,
1126
- scanAllPackageJsons,
1127
- classifyPackageJson,
1128
- detectFrontendFramework,
1129
- findMainFrontendFile,
1130
- detectMonorepo,
1131
- detectFramework,
1132
- detectModuleSystem,
1133
- detectTypeScript,
1134
- readTsConfig,
1135
- shouldGenerateTs,
1136
- detectSrcDir,
1137
- detectNextRouter,
1138
- detectPackageManager,
1139
- detectExistingBotVersion,
1140
- findFileWithContent,
1141
- findListenCall,
1142
- findModuleExportsApp,
1143
- findListenInsideCallback,
1144
- findCreateServer,
1145
- detectAppVarName,
1146
- detectDotenv,
1147
- parseEntryFromConfigFiles,
1148
- scoreExpressFile,
1149
- };