botversion-sdk 1.0.1 → 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/bin/init.js CHANGED
@@ -45,6 +45,22 @@ function step(msg) {
45
45
  console.log(`\n${c.bold}${c.white} → ${msg}${c.reset}`);
46
46
  }
47
47
 
48
+ // ─── FETCH PROJECT INFO ───────────────────────────────────────────────────────
49
+
50
+ async function fetchProjectInfo(apiKey, platformUrl) {
51
+ const url = `${platformUrl}/api/sdk/project-info?workspaceKey=${encodeURIComponent(apiKey)}`;
52
+ try {
53
+ const response = await fetch(url);
54
+ if (!response.ok) {
55
+ throw new Error("Invalid API key or project not found");
56
+ }
57
+ return await response.json();
58
+ // returns { projectId, publicKey, apiUrl, cdnUrl }
59
+ } catch (err) {
60
+ throw new Error(`Could not fetch project info: ${err.message}`);
61
+ }
62
+ }
63
+
48
64
  // ─── PARSE ARGS ───────────────────────────────────────────────────────────────
49
65
 
50
66
  function parseArgs(argv) {
@@ -93,23 +109,30 @@ async function main() {
93
109
  const cwd = args.cwd;
94
110
  const changes = { modified: [], created: [], backups: [], manual: [] };
95
111
 
112
+ // ── Fetch project info from platform ──────────────────────────────────────
113
+ step("Fetching project info from platform...");
114
+ let projectInfo;
115
+ try {
116
+ projectInfo = await fetchProjectInfo(args.key, "http://localhost:3000");
117
+ success(`Project found — ID: ${projectInfo.projectId}`);
118
+ } catch (err) {
119
+ error(err.message);
120
+ process.exit(1);
121
+ }
122
+
96
123
  // ── Detect environment ────────────────────────────────────────────────────
97
124
  step("Scanning your project...");
98
125
 
99
126
  // Handle monorepo
100
- let workingDir = cwd;
101
127
  const monorepoInfo = detector.detectMonorepo(cwd);
102
128
  if (monorepoInfo.isMonorepo) {
103
- warn("Monorepo detected.");
104
- workingDir = await prompts.promptMonorepoPackage(
105
- monorepoInfo.packages,
106
- cwd,
129
+ warn(
130
+ "Monorepo detected will scan all packages for frontend and backend.",
107
131
  );
108
- info(`Using package: ${path.relative(cwd, workingDir) || "root"}`);
109
132
  }
110
133
 
111
- // Run full detection
112
- const detected = detector.detect(workingDir);
134
+ // Always detect from root so frontend/backend split works correctly
135
+ const detected = detector.detect(cwd);
113
136
 
114
137
  // ── Check if already initialized ─────────────────────────────────────────
115
138
  if (detected.alreadyInitialized && !args.force) {
@@ -174,20 +197,60 @@ async function main() {
174
197
  // FRAMEWORK: EXPRESS
175
198
  // ─────────────────────────────────────────────────────────────────────────
176
199
  if (detected.framework.name === "express") {
177
- await setupExpress(detected, args, changes);
200
+ await setupExpress(detected, args, changes, projectInfo);
201
+
202
+ // ── Inject script tag into frontend (Express only) ──────────────────
203
+ // Next.js handles its own script tag injection inside setupNextJs
204
+ if (detected.frontendMainFile && projectInfo) {
205
+ const scriptTag = generator.generateScriptTag(projectInfo);
206
+ const result = writer.injectScriptTag(
207
+ detected.frontendMainFile.file,
208
+ detected.frontendMainFile.type,
209
+ scriptTag,
210
+ args.force,
211
+ );
212
+
213
+ if (result.success) {
214
+ success(
215
+ `Injected script tag into ${path.relative(detected.cwd, detected.frontendMainFile.file)}`,
216
+ );
217
+ changes.modified.push(
218
+ path.relative(detected.cwd, detected.frontendMainFile.file),
219
+ );
220
+ if (result.backup) changes.backups.push(result.backup);
221
+ } else if (result.reason === "already_exists") {
222
+ warn(
223
+ "BotVersion script tag already exists in frontend file — skipping.",
224
+ );
225
+ } else {
226
+ warn(
227
+ "Could not auto-inject script tag. Add this manually to your frontend HTML:",
228
+ );
229
+ console.log("\n" + scriptTag + "\n");
230
+ changes.manual.push(
231
+ `Add to your frontend HTML before </body>:\n\n${scriptTag}`,
232
+ );
233
+ }
234
+ } else if (!detected.frontendMainFile) {
235
+ warn("Could not find frontend main file automatically.");
236
+ const scriptTag = generator.generateScriptTag(projectInfo);
237
+ changes.manual.push(
238
+ `Add to your frontend HTML before </body>:\n\n${scriptTag}`,
239
+ );
240
+ }
178
241
  }
179
242
 
180
243
  // ─────────────────────────────────────────────────────────────────────────
181
244
  // FRAMEWORK: NEXT.JS
182
245
  // ─────────────────────────────────────────────────────────────────────────
183
246
  else if (detected.framework.name === "next") {
184
- await setupNextJs(detected, args, changes);
247
+ await setupNextJs(detected, args, changes, projectInfo);
185
248
  }
186
249
 
187
250
  // ── Write API key to .env / .env.local ────────────────────────────────────
188
251
  const envFileName =
189
252
  detected.framework.name === "next" ? ".env.local" : ".env";
190
- const envPath = path.join(workingDir, envFileName);
253
+ const envPath = path.join(detected.cwd, envFileName);
191
254
  const envLine = `BOTVERSION_API_KEY=${args.key}`;
192
255
  const envContent = fs.existsSync(envPath)
193
256
  ? fs.readFileSync(envPath, "utf8")
@@ -222,7 +285,7 @@ async function main() {
222
285
 
223
286
  // ─── EXPRESS SETUP ────────────────────────────────────────────────────────────
224
287
 
225
- async function setupExpress(detected, args, changes) {
288
+ async function setupExpress(detected, args, changes, projectInfo) {
226
289
  step("Setting up Express...");
227
290
 
228
291
  // Find entry point
@@ -244,9 +307,6 @@ async function setupExpress(detected, args, changes) {
244
307
  // Generate the init code
245
308
  const generated = generator.generateExpressInit(detected, args.key);
246
309
 
247
- // Find app.listen() and inject before it
248
- const listenCall = detector.findListenCall(entryPoint);
249
-
250
310
  // PATTERN 2: Separate app file with module.exports = app
251
311
  if (detected.appFile) {
252
312
  info(`Found app file: ${path.relative(detected.cwd, detected.appFile)}`);
@@ -254,6 +314,7 @@ async function setupExpress(detected, args, changes) {
254
314
  const result = writer.injectBeforeExport(
255
315
  detected.appFile,
256
316
  generated2.initBlock,
317
+ detected.appVarName,
257
318
  );
258
319
 
259
320
  if (result.success) {
@@ -272,7 +333,11 @@ async function setupExpress(detected, args, changes) {
272
333
  detected.listenInsideCallback ||
273
334
  detected.createServer
274
335
  ) {
275
- const result = writer.injectBeforeListen(entryPoint, generated.initBlock);
336
+ const result = writer.injectBeforeListen(
337
+ entryPoint,
338
+ generated.initBlock,
339
+ detected.appVarName,
340
+ );
276
341
 
277
342
  if (result.success) {
278
343
  success(`Injected BotVersion.init() before app.listen()`);
@@ -299,7 +364,11 @@ async function setupExpress(detected, args, changes) {
299
364
  } else if (response.action === "manual_path") {
300
365
  const altPath = path.resolve(detected.cwd, response.filePath);
301
366
  if (fs.existsSync(altPath)) {
302
- const result = writer.injectBeforeListen(altPath, generated.initBlock);
367
+ const result = writer.injectBeforeListen(
368
+ altPath,
369
+ generated.initBlock,
370
+ detected.appVarName,
371
+ );
303
372
  if (result.success) {
304
373
  success(`Injected into ${response.filePath}`);
305
374
  changes.modified.push(response.filePath);
@@ -321,7 +390,7 @@ async function setupExpress(detected, args, changes) {
321
390
 
322
391
  // ─── NEXT.JS SETUP ────────────────────────────────────────────────────────────
323
392
 
324
- async function setupNextJs(detected, args, changes) {
393
+ async function setupNextJs(detected, args, changes, projectInfo) {
325
394
  step("Setting up Next.js...");
326
395
 
327
396
  const nextInfo = detected.next;
@@ -366,7 +435,10 @@ async function setupNextJs(detected, args, changes) {
366
435
  }
367
436
 
368
437
  // ── 2. Patch next.config.js ───────────────────────────────────────────────
369
- const configPatch = generator.generateNextConfigPatch(detected.cwd);
438
+ const configPatch = generator.generateNextConfigPatch(
439
+ detected.cwd,
440
+ detected.nextVersion,
441
+ );
370
442
 
371
443
  if (configPatch) {
372
444
  if (configPatch.alreadyPatched) {
@@ -434,6 +506,45 @@ async function setupNextJs(detected, args, changes) {
434
506
  }
435
507
  }
436
508
  }
509
+
510
+ // ── 5. Inject script tag into frontend ───────────────────────────────────
511
+ // For Next.js the frontend IS the same project
512
+ // frontendMainFile will be _app.js or layout.js
513
+ if (detected.frontendMainFile && projectInfo) {
514
+ const scriptTag = generator.generateScriptTag(projectInfo);
515
+ const result = writer.injectScriptTag(
516
+ detected.frontendMainFile.file,
517
+ detected.frontendMainFile.type,
518
+ scriptTag,
519
+ args.force,
520
+ );
521
+
522
+ if (result.success) {
523
+ success(
524
+ `Injected script tag into ${path.relative(detected.cwd, detected.frontendMainFile.file)}`,
525
+ );
526
+ changes.modified.push(
527
+ path.relative(detected.cwd, detected.frontendMainFile.file),
528
+ );
529
+ if (result.backup) changes.backups.push(result.backup);
530
+ } else if (result.reason === "already_exists") {
531
+ warn("BotVersion script tag already exists — skipping.");
532
+ } else {
533
+ warn(
534
+ "Could not auto-inject script tag. Add this manually to your frontend file:",
535
+ );
536
+ console.log("\n" + scriptTag + "\n");
537
+ changes.manual.push(
538
+ `Add to your frontend HTML before </body>:\n\n${scriptTag}`,
539
+ );
540
+ }
541
+ } else if (!detected.frontendMainFile) {
542
+ warn("Could not find frontend file automatically.");
543
+ const scriptTag = generator.generateScriptTag(projectInfo);
544
+ changes.manual.push(
545
+ `Add to your frontend HTML before </body>:\n\n${scriptTag}`,
546
+ );
547
+ }
437
548
  }
438
549
 
439
550
  // ─── RUN ──────────────────────────────────────────────────────────────────────
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,19 @@ 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";
261
527
  const content = fs.readFileSync(filePath, "utf8");
262
528
  const lines = content.split("\n");
529
+ const regex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
263
530
 
264
531
  for (let i = 0; i < lines.length; i++) {
265
- if (/app\.listen\s*\(/.test(lines[i])) {
532
+ if (regex.test(lines[i])) {
266
533
  return { lineIndex: i, lineNumber: i + 1, content: lines[i] };
267
534
  }
268
535
  }
@@ -280,12 +547,14 @@ function findModuleExportsApp(filePath) {
280
547
  return null;
281
548
  }
282
549
 
283
- function findListenInsideCallback(filePath) {
550
+ function findListenInsideCallback(filePath, appVarName) {
551
+ appVarName = appVarName || "app";
284
552
  const content = fs.readFileSync(filePath, "utf8");
285
553
  const lines = content.split("\n");
554
+ const regex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
555
+
286
556
  for (let i = 0; i < lines.length; i++) {
287
- if (/app\.listen\s*\(/.test(lines[i])) {
288
- // Check if it's inside a callback (indented or preceded by .then)
557
+ if (regex.test(lines[i])) {
289
558
  const indentation = lines[i].match(/^(\s*)/)[1].length;
290
559
  if (indentation > 0) {
291
560
  return { lineIndex: i, lineNumber: i + 1, insideCallback: true };
@@ -392,12 +661,7 @@ function detectAuth(pkg) {
392
661
  "jwt",
393
662
  "express-session",
394
663
  ].includes(lib.name);
395
- return {
396
- name: lib.name,
397
- version,
398
- package: pkg2,
399
- supported,
400
- };
664
+ return { name: lib.name, version, package: pkg2, supported };
401
665
  }
402
666
  }
403
667
 
@@ -407,7 +671,6 @@ function detectAuth(pkg) {
407
671
  // ─── NEXT-AUTH CONFIG LOCATION ───────────────────────────────────────────────
408
672
 
409
673
  function findNextAuthConfig(cwd) {
410
- // Common locations for authOptions
411
674
  const candidates = [
412
675
  "pages/api/auth/[...nextauth].js",
413
676
  "pages/api/auth/[...nextauth].ts",
@@ -424,7 +687,7 @@ function findNextAuthConfig(cwd) {
424
687
  "utils/auth.js",
425
688
  "utils/auth.ts",
426
689
  "auth.js",
427
- "auth.ts", // next-auth v5
690
+ "auth.ts",
428
691
  ];
429
692
 
430
693
  for (const candidate of candidates) {
@@ -434,13 +697,9 @@ function findNextAuthConfig(cwd) {
434
697
  }
435
698
  }
436
699
 
437
- // Search for authOptions in files
438
700
  const found = findFileWithContent(cwd, "authOptions", [".js", ".ts"], 3);
439
701
  if (found) {
440
- return {
441
- path: found,
442
- relativePath: path.relative(cwd, found),
443
- };
702
+ return { path: found, relativePath: path.relative(cwd, found) };
444
703
  }
445
704
 
446
705
  return null;
@@ -471,16 +730,6 @@ function findFileWithContent(dir, searchString, extensions, maxDepth) {
471
730
  function walk(currentDir, depth) {
472
731
  if (depth > maxDepth) return null;
473
732
 
474
- // Skip node_modules, .git, .next, dist, build
475
- const skipDirs = [
476
- "node_modules",
477
- ".git",
478
- ".next",
479
- "dist",
480
- "build",
481
- ".cache",
482
- ];
483
-
484
733
  let entries;
485
734
  try {
486
735
  entries = fs.readdirSync(currentDir);
@@ -489,7 +738,7 @@ function findFileWithContent(dir, searchString, extensions, maxDepth) {
489
738
  }
490
739
 
491
740
  for (const entry of entries) {
492
- if (skipDirs.includes(entry)) continue;
741
+ if (SKIP_DIRS.includes(entry)) continue;
493
742
 
494
743
  const fullPath = path.join(currentDir, entry);
495
744
  let stat;
@@ -523,7 +772,7 @@ function detectAppVarName(filePath) {
523
772
  const content = fs.readFileSync(filePath, "utf8");
524
773
  const match = content.match(/(?:const|let|var)\s+(\w+)\s*=\s*express\s*\(/);
525
774
  return match ? match[1] : "app";
526
- } catch (e) {
775
+ } catch {
527
776
  return "app";
528
777
  }
529
778
  }
@@ -533,18 +782,88 @@ function detectAppVarName(filePath) {
533
782
  function detect(cwd) {
534
783
  const pkg = readPackageJson(cwd);
535
784
  const monorepo = detectMonorepo(cwd);
536
- const framework = detectFramework(pkg);
537
- const moduleSystem = detectModuleSystem(pkg);
538
- const isTypeScript = detectTypeScript(cwd);
539
- const hasSrc = detectSrcDir(cwd);
540
- const auth = detectAuth(pkg);
541
- 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
+ }
542
809
 
543
- const generateTs = shouldGenerateTs(cwd, isTypeScript);
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
+ }
857
+
858
+ const packageManager =
859
+ detectPackageManager(backendDir) !== "npm"
860
+ ? detectPackageManager(backendDir)
861
+ : detectPackageManager(cwd);
544
862
 
545
863
  const result = {
546
- cwd,
547
- pkg,
864
+ cwd: backendDir, // use backend dir as working dir
865
+ rootCwd: cwd, // keep original root for reference
866
+ pkg: backendPkg,
548
867
  monorepo,
549
868
  framework,
550
869
  moduleSystem,
@@ -553,30 +872,34 @@ function detect(cwd) {
553
872
  hasSrc,
554
873
  auth,
555
874
  packageManager,
556
- // generateTs: true means user has allowJs:false — must use .ts
557
- // generateTs: false means .js files are fine (most users)
558
875
  ext: generateTs ? ".ts" : ".js",
876
+ // Frontend info
877
+ frontendDir,
878
+ frontendPkg,
879
+ frontendMainFile,
559
880
  };
560
881
 
561
- // Framework-specific detection
882
+ // ── Framework-specific detection ──────────────────────────────────────────
562
883
  if (framework.name === "next") {
563
- result.next = detectNextRouter(cwd);
564
- result.nextVersion = detectNextVersion(pkg);
884
+ result.next = detectNextRouter(backendDir);
885
+ result.nextVersion = detectNextVersion(backendPkg);
565
886
  if (auth.name === "next-auth") {
566
- result.nextAuthConfig = findNextAuthConfig(cwd);
887
+ result.nextAuthConfig = findNextAuthConfig(backendDir);
567
888
  }
568
889
  }
569
890
 
570
891
  if (framework.name === "express") {
571
- result.entryPoint = detectExpressEntry(cwd, pkg);
892
+ result.entryPoint = detectExpressEntry(backendDir, backendPkg);
572
893
  if (result.entryPoint) {
573
- 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
+ );
574
900
  result.moduleExportsApp = findModuleExportsApp(result.entryPoint);
575
- result.listenInsideCallback = findListenInsideCallback(result.entryPoint);
576
901
  result.createServer = findCreateServer(result.entryPoint);
577
- result.appVarName = detectAppVarName(result.entryPoint);
578
902
 
579
- // Also check for app file separately (pattern 2)
580
903
  const appFileCandidates = [
581
904
  "src/app.js",
582
905
  "src/app.ts",
@@ -584,7 +907,7 @@ function detect(cwd) {
584
907
  "app.ts",
585
908
  ];
586
909
  for (const candidate of appFileCandidates) {
587
- const fullPath = path.join(cwd, candidate);
910
+ const fullPath = path.join(backendDir, candidate);
588
911
  if (fs.existsSync(fullPath) && fullPath !== result.entryPoint) {
589
912
  const exportCall = findModuleExportsApp(fullPath);
590
913
  if (exportCall) {
@@ -597,13 +920,14 @@ function detect(cwd) {
597
920
  }
598
921
  }
599
922
 
923
+ // ── Already initialized check ─────────────────────────────────────────────
600
924
  result.alreadyInitialized =
601
925
  detectExistingBotVersion(result.entryPoint) ||
602
926
  (framework.name === "next" &&
603
- (fs.existsSync(path.join(cwd, "instrumentation.js")) ||
604
- fs.existsSync(path.join(cwd, "instrumentation.ts")) ||
605
- fs.existsSync(path.join(cwd, "src", "instrumentation.js")) ||
606
- 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"))));
607
931
 
608
932
  return result;
609
933
  }
@@ -611,6 +935,10 @@ function detect(cwd) {
611
935
  module.exports = {
612
936
  detect,
613
937
  readPackageJson,
938
+ scanAllPackageJsons,
939
+ classifyPackageJson,
940
+ detectFrontendFramework,
941
+ findMainFrontendFile,
614
942
  detectMonorepo,
615
943
  detectFramework,
616
944
  detectModuleSystem,
package/cli/generator.js CHANGED
@@ -366,7 +366,7 @@ export async function POST(req${typeAnnotation}) {
366
366
  };
367
367
 
368
368
  // Forward to BotVersion platform directly
369
- const response = await fetch(\`\${process.env.BOTVERSION_PLATFORM_URL || 'https://chatbusiness-two.vercel.app'}/api/chatbot/widget-chat\`, {
369
+ const response = await fetch(\`\${process.env.BOTVERSION_PLATFORM_URL || 'http://localhost:3000'}/api/chatbot/widget-chat\`, {
370
370
  method: 'POST',
371
371
  headers: { 'Content-Type': 'application/json' },
372
372
  body: JSON.stringify({
@@ -405,7 +405,7 @@ export async function POST(req${typeAnnotation}) {
405
405
  const { userId } = await auth();
406
406
  const body = await req.json();
407
407
 
408
- const response = await fetch(\`\${process.env.BOTVERSION_PLATFORM_URL || 'https://chatbusiness-two.vercel.app'}/api/chatbot/widget-chat\`, {
408
+ const response = await fetch(\`\${process.env.BOTVERSION_PLATFORM_URL || 'http://localhost:3000'}/api/chatbot/widget-chat\`, {
409
409
  method: 'POST',
410
410
  headers: { 'Content-Type': 'application/json' },
411
411
  body: JSON.stringify({
@@ -444,7 +444,7 @@ export async function POST(req${typeAnnotation}) {
444
444
  const { data: { session } } = await supabase.auth.getSession();
445
445
  const body = await req.json();
446
446
 
447
- const response = await fetch(\`\${process.env.BOTVERSION_PLATFORM_URL || 'https://chatbusiness-two.vercel.app'}/api/chatbot/widget-chat\`, {
447
+ const response = await fetch(\`\${process.env.BOTVERSION_PLATFORM_URL || 'http://localhost:3000'}/api/chatbot/widget-chat\`, {
448
448
  method: 'POST',
449
449
  headers: { 'Content-Type': 'application/json' },
450
450
  body: JSON.stringify({
@@ -491,7 +491,7 @@ export async function POST(req${typeAnnotation}) {
491
491
  // Add userContext here if needed:
492
492
  // const userContext = { userId: '...', email: '...' };
493
493
 
494
- const response = await fetch(\`\${process.env.BOTVERSION_PLATFORM_URL || 'https://chatbusiness-two.vercel.app'}/api/chatbot/widget-chat\`, {
494
+ const response = await fetch(\`\${process.env.BOTVERSION_PLATFORM_URL || 'http://localhost:3000'}/api/chatbot/widget-chat\`, {
495
495
  method: 'POST',
496
496
  headers: { 'Content-Type': 'application/json' },
497
497
  body: JSON.stringify({
@@ -516,7 +516,7 @@ export async function POST(req${typeAnnotation}) {
516
516
 
517
517
  // ─── NEXT.JS CONFIG PATCH ─────────────────────────────────────────────────────
518
518
 
519
- function generateNextConfigPatch(cwd) {
519
+ function generateNextConfigPatch(cwd, nextVersion) {
520
520
  const candidates = ["next.config.js", "next.config.mjs", "next.config.ts"];
521
521
 
522
522
  let configPath = null;
@@ -533,6 +533,17 @@ function generateNextConfigPatch(cwd) {
533
533
 
534
534
  if (!configPath) return null;
535
535
 
536
+ // Skip instrumentationHook for Next.js 14.1+ (enabled by default)
537
+ if (nextVersion && nextVersion.major >= 14) {
538
+ // Check minor version too
539
+ const rawVersion = nextVersion.raw || "";
540
+ const match = rawVersion.match(/(\d+)\.(\d+)/);
541
+ const minor = match ? parseInt(match[2], 10) : 0;
542
+ if (nextVersion.major > 14 || (nextVersion.major === 14 && minor >= 1)) {
543
+ return { path: configPath, alreadyPatched: true };
544
+ }
545
+ }
546
+
536
547
  if (configContent.includes("instrumentationHook")) {
537
548
  return { path: configPath, alreadyPatched: true };
538
549
  }
@@ -610,6 +621,19 @@ Visit https://docs.botversion.com for manual setup instructions.
610
621
  );
611
622
  }
612
623
 
624
+ // ─── SCRIPT TAG GENERATION ────────────────────────────────────────────────────
625
+
626
+ function generateScriptTag(projectInfo) {
627
+ return `<script
628
+ id="botversion-loader"
629
+ src="${projectInfo.cdnUrl}"
630
+ data-api-url="${projectInfo.apiUrl}"
631
+ data-project-id="${projectInfo.projectId}"
632
+ data-public-key="${projectInfo.publicKey}"
633
+ data-proxy-url="/api/botversion/chat"
634
+ ></script>`;
635
+ }
636
+
613
637
  module.exports = {
614
638
  generateExpressInit,
615
639
  generateInstrumentationFile,
@@ -617,4 +641,5 @@ module.exports = {
617
641
  generateNextAppChatRoute,
618
642
  generateManualInstructions,
619
643
  generateNextConfigPatch,
644
+ generateScriptTag,
620
645
  };
package/cli/writer.js CHANGED
@@ -24,14 +24,15 @@ function backupFile(filePath) {
24
24
 
25
25
  // ─── INJECT CODE BEFORE app.listen() ────────────────────────────────────────
26
26
 
27
- function injectBeforeListen(filePath, codeToInject) {
27
+ function injectBeforeListen(filePath, codeToInject, appVarName) {
28
+ appVarName = appVarName || "app";
28
29
  const content = fs.readFileSync(filePath, "utf8");
29
30
  const lines = content.split("\n");
31
+ const listenRegex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
30
32
 
31
- // Find app.listen() line
32
33
  let listenLineIndex = -1;
33
34
  for (let i = 0; i < lines.length; i++) {
34
- if (/app\.listen\s*\(/.test(lines[i])) {
35
+ if (listenRegex.test(lines[i])) {
35
36
  listenLineIndex = i;
36
37
  break;
37
38
  }
@@ -94,7 +95,8 @@ function createFile(filePath, content, force) {
94
95
  return { success: true, path: filePath };
95
96
  }
96
97
 
97
- function injectBeforeExport(filePath, codeToInject) {
98
+ function injectBeforeExport(filePath, codeToInject, appVarName) {
99
+ appVarName = appVarName || "app";
98
100
  const content = fs.readFileSync(filePath, "utf8");
99
101
  const lines = content.split("\n");
100
102
 
@@ -147,7 +149,8 @@ function injectBeforeExport(filePath, codeToInject) {
147
149
  // Fallback: before app.listen()
148
150
  if (insertIndex === -1) {
149
151
  for (let i = 0; i < lines.length; i++) {
150
- if (/app\.listen\s*\(/.test(lines[i])) {
152
+ const listenRegex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
153
+ if (listenRegex.test(lines[i])) {
151
154
  insertIndex = i;
152
155
  break;
153
156
  }
@@ -238,6 +241,187 @@ function writeSummary(changes) {
238
241
  return lines.join("\n");
239
242
  }
240
243
 
244
+ // ─── INJECT SCRIPT TAG INTO FRONTEND FILE ────────────────────────────────────
245
+
246
+ function injectScriptTag(filePath, fileType, scriptTag, force) {
247
+ if (!fs.existsSync(filePath)) {
248
+ return { success: false, reason: "file_not_found" };
249
+ }
250
+
251
+ const content = fs.readFileSync(filePath, "utf8");
252
+
253
+ // Already exists check
254
+ if (content.includes("botversion-loader")) {
255
+ if (!force) return { success: false, reason: "already_exists" };
256
+ }
257
+
258
+ const backup = backupFile(filePath);
259
+
260
+ // ── HTML file — inject before </body> ──────────────────────────────────
261
+ if (fileType === "html") {
262
+ if (!content.includes("</body>")) {
263
+ return { success: false, reason: "no_body_tag" };
264
+ }
265
+
266
+ const newContent = content.replace("</body>", ` ${scriptTag}\n</body>`);
267
+ fs.writeFileSync(filePath, newContent, "utf8");
268
+ return { success: true, backup };
269
+ }
270
+
271
+ // ── Next.js _app.js — inject Script component ──────────────────────────
272
+ if (fileType === "next") {
273
+ const fileName = path.basename(filePath);
274
+
275
+ // pages/_app.js
276
+ if (fileName.startsWith("_app")) {
277
+ return injectIntoNextApp(filePath, content, scriptTag, backup);
278
+ }
279
+
280
+ // app/layout.js
281
+ if (fileName.startsWith("layout")) {
282
+ return injectIntoNextLayout(filePath, content, scriptTag, backup);
283
+ }
284
+ }
285
+
286
+ return { success: false, reason: "unsupported_file_type" };
287
+ }
288
+
289
+ // ─── INJECT INTO NEXT.JS _app.js ─────────────────────────────────────────────
290
+
291
+ function injectIntoNextApp(filePath, content, scriptTag, backup) {
292
+ let newContent = content;
293
+
294
+ if (!content.includes("next/script")) {
295
+ newContent = newContent.replace(
296
+ /^(import .+)/m,
297
+ `import Script from 'next/script';\n$1`,
298
+ );
299
+ }
300
+
301
+ const scriptComponent = `
302
+ <Script
303
+ id="botversion-loader"
304
+ src="${extractAttr(scriptTag, "src")}"
305
+ data-api-url="${extractAttr(scriptTag, "data-api-url")}"
306
+ data-project-id="${extractAttr(scriptTag, "data-project-id")}"
307
+ data-public-key="${extractAttr(scriptTag, "data-public-key")}"
308
+ data-proxy-url="/api/botversion/chat"
309
+ strategy="afterInteractive"
310
+ />`;
311
+
312
+ const lines = newContent.split("\n");
313
+
314
+ // Find ALL return statements and pick the one whose root JSX
315
+ // is a multi-child wrapper (not a simple single-element return)
316
+ // Strategy: find the return ( that is followed by the most lines
317
+ // before its closing ) — that's the main render return
318
+
319
+ let bestReturnIndex = -1;
320
+ let bestRootJsxIndex = -1;
321
+ let bestLineCount = 0;
322
+
323
+ for (let i = 0; i < lines.length; i++) {
324
+ if (!/^\s*return\s*\(/.test(lines[i])) continue;
325
+
326
+ // Find the root JSX tag after this return
327
+ let rootJsx = -1;
328
+ for (let j = i + 1; j < lines.length; j++) {
329
+ const trimmed = lines[j].trim();
330
+ if (!trimmed) continue;
331
+ if (trimmed.startsWith("<")) {
332
+ rootJsx = j;
333
+ break;
334
+ }
335
+ break; // non-empty, non-JSX line means this isn't a JSX return
336
+ }
337
+
338
+ if (rootJsx === -1) continue;
339
+
340
+ // Find the closing ) of this return block
341
+ let depth = 1;
342
+ let closingLine = -1;
343
+ for (let j = rootJsx; j < lines.length; j++) {
344
+ for (const ch of lines[j]) {
345
+ if (ch === "(") depth++;
346
+ if (ch === ")") depth--;
347
+ }
348
+ if (depth === 0) {
349
+ closingLine = j;
350
+ break;
351
+ }
352
+ }
353
+
354
+ const lineCount = closingLine - i;
355
+ if (lineCount > bestLineCount) {
356
+ bestLineCount = lineCount;
357
+ bestReturnIndex = i;
358
+ bestRootJsxIndex = rootJsx;
359
+ }
360
+ }
361
+
362
+ if (bestRootJsxIndex !== -1) {
363
+ lines.splice(bestRootJsxIndex + 1, 0, scriptComponent);
364
+ newContent = lines.join("\n");
365
+ } else {
366
+ // Final fallback
367
+ newContent = newContent.replace(
368
+ /([ \t]*<\/div>\s*\n\s*\))/,
369
+ `${scriptComponent}\n$1`,
370
+ );
371
+ }
372
+
373
+ fs.writeFileSync(filePath, newContent, "utf8");
374
+ return { success: true, backup };
375
+ }
376
+
377
+ // ─── INJECT INTO NEXT.JS layout.js ───────────────────────────────────────────
378
+
379
+ function injectIntoNextLayout(filePath, content, scriptTag, backup) {
380
+ let newContent = content;
381
+
382
+ if (!content.includes("next/script")) {
383
+ newContent = newContent.replace(
384
+ /^(import .+)/m,
385
+ `import Script from 'next/script';\n$1`,
386
+ );
387
+ }
388
+
389
+ const scriptComponent = `
390
+ <Script
391
+ id="botversion-loader"
392
+ src="${extractAttr(scriptTag, "src")}"
393
+ data-api-url="${extractAttr(scriptTag, "data-api-url")}"
394
+ data-project-id="${extractAttr(scriptTag, "data-project-id")}"
395
+ data-public-key="${extractAttr(scriptTag, "data-public-key")}"
396
+ data-proxy-url="/api/botversion/chat"
397
+ strategy="afterInteractive"
398
+ />`;
399
+
400
+ // Inject before </body> in layout
401
+ if (content.includes("</body>")) {
402
+ newContent = newContent.replace(
403
+ "</body>",
404
+ `${scriptComponent}\n </body>`,
405
+ );
406
+ } else {
407
+ // Fallback — before last closing tag
408
+ newContent = newContent.replace(
409
+ /(<\/\w+>\s*\)[\s;]*$)/m,
410
+ `${scriptComponent}\n $1`,
411
+ );
412
+ }
413
+
414
+ fs.writeFileSync(filePath, newContent, "utf8");
415
+ return { success: true, backup };
416
+ }
417
+
418
+ // ─── HELPER: extract attribute value from script tag string ──────────────────
419
+
420
+ function extractAttr(scriptTag, attr) {
421
+ const match = scriptTag.match(new RegExp(`${attr}="([^"]+)"`));
422
+ return match ? match[1] : "";
423
+ }
424
+
241
425
  module.exports = {
242
426
  writeFile,
243
427
  backupFile,
@@ -247,4 +431,5 @@ module.exports = {
247
431
  mergeIntoMiddleware,
248
432
  writeSummary,
249
433
  injectBeforeExport,
434
+ injectScriptTag,
250
435
  };
package/index.js CHANGED
@@ -61,8 +61,7 @@ var BotVersion = {
61
61
 
62
62
  this._client = new BotVersionClient({
63
63
  apiKey: options.apiKey,
64
- platformUrl:
65
- options.platformUrl || "https://chatbusiness-two.vercel.app/",
64
+ platformUrl: options.platformUrl || "http://localhost:3000/",
66
65
  debug: options.debug || false,
67
66
  timeout: options.timeout || 30000,
68
67
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botversion-sdk",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "BotVersion SDK — auto-detect and register your API endpoints",
5
5
  "main": "index.js",
6
6
  "bin": {