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/writer.js CHANGED
@@ -17,21 +17,22 @@ function writeFile(filePath, content) {
17
17
 
18
18
  function backupFile(filePath) {
19
19
  if (!fs.existsSync(filePath)) return null;
20
- const backupPath = filePath + ".botversion.bak";
20
+ const backupPath = filePath + ".backup-before-botversion";
21
21
  fs.copyFileSync(filePath, backupPath);
22
22
  return backupPath;
23
23
  }
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
  }
@@ -58,10 +59,10 @@ function injectBeforeListen(filePath, codeToInject) {
58
59
  const injectedLines = ["", ...codeToInject.split("\n"), ""];
59
60
 
60
61
  const newContent = [...before, ...injectedLines, ...after].join("\n");
61
- backupFile(filePath);
62
+ const backup = backupFile(filePath);
62
63
  fs.writeFileSync(filePath, newContent, "utf8");
63
64
 
64
- return { success: true, lineNumber: listenLineIndex + 1 };
65
+ return { success: true, lineNumber: listenLineIndex + 1, backup };
65
66
  }
66
67
 
67
68
  // ─── APPEND CODE TO END OF FILE ──────────────────────────────────────────────
@@ -94,6 +95,86 @@ function createFile(filePath, content, force) {
94
95
  return { success: true, path: filePath };
95
96
  }
96
97
 
98
+ function injectBeforeExport(filePath, codeToInject, appVarName) {
99
+ appVarName = appVarName || "app";
100
+ const content = fs.readFileSync(filePath, "utf8");
101
+ const lines = content.split("\n");
102
+
103
+ if (content.includes("botversion-sdk") || content.includes("BotVersion")) {
104
+ return { success: false, reason: "already_exists" };
105
+ }
106
+
107
+ let insertIndex = -1;
108
+
109
+ // Find module.exports = app
110
+ let exportsLine = -1;
111
+ for (let i = 0; i < lines.length; i++) {
112
+ if (/module\.exports\s*=\s*app/.test(lines[i])) {
113
+ exportsLine = i;
114
+ break;
115
+ }
116
+ }
117
+
118
+ if (exportsLine !== -1) {
119
+ // Walk backwards from module.exports to skip error handler lines
120
+ // Skip lines that are: blank, closing braces, or known error middleware
121
+ let i = exportsLine - 1;
122
+ while (i >= 0) {
123
+ const line = lines[i].trim();
124
+ if (
125
+ line === "" ||
126
+ line === "})" ||
127
+ line === "});" ||
128
+ line === "}," ||
129
+ line === "}" ||
130
+ line === ");" ||
131
+ line === "next();" ||
132
+ /app\.use\s*\(\s*errorHandler/.test(line) ||
133
+ /app\.use\s*\(\s*errorConverter/.test(line) ||
134
+ /app\.use\s*\(\s*\(req,\s*res,\s*next\)/.test(line) ||
135
+ /next\(new/.test(line) ||
136
+ /NOT_FOUND/.test(line) ||
137
+ /Not found/i.test(line) ||
138
+ /\/\/ (handle|convert|send back) error/.test(lines[i]) ||
139
+ /\/\/ send back a 404/.test(lines[i])
140
+ ) {
141
+ i--;
142
+ } else {
143
+ break;
144
+ }
145
+ }
146
+ insertIndex = i + 1;
147
+ }
148
+
149
+ // Fallback: before app.listen()
150
+ if (insertIndex === -1) {
151
+ for (let i = 0; i < lines.length; i++) {
152
+ const listenRegex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
153
+ if (listenRegex.test(lines[i])) {
154
+ insertIndex = i;
155
+ break;
156
+ }
157
+ }
158
+ }
159
+
160
+ // Final fallback: append to end of file
161
+ if (insertIndex === -1) {
162
+ const backup = backupFile(filePath);
163
+ const newContent = content.trimEnd() + "\n\n" + codeToInject + "\n";
164
+ fs.writeFileSync(filePath, newContent, "utf8");
165
+ return { success: true, backup };
166
+ }
167
+
168
+ const before = lines.slice(0, insertIndex);
169
+ const after = lines.slice(insertIndex);
170
+ const injectedLines = ["", ...codeToInject.split("\n"), ""];
171
+ const newContent = [...before, ...injectedLines, ...after].join("\n");
172
+
173
+ const backup = backupFile(filePath);
174
+ fs.writeFileSync(filePath, newContent, "utf8");
175
+ return { success: true, backup };
176
+ }
177
+
97
178
  // ─── MERGE INTO EXISTING MIDDLEWARE (Next.js) ────────────────────────────────
98
179
 
99
180
  function mergeIntoMiddleware(middlewarePath, authName) {
@@ -160,6 +241,187 @@ function writeSummary(changes) {
160
241
  return lines.join("\n");
161
242
  }
162
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
+
163
425
  module.exports = {
164
426
  writeFile,
165
427
  backupFile,
@@ -168,4 +430,6 @@ module.exports = {
168
430
  createFile,
169
431
  mergeIntoMiddleware,
170
432
  writeSummary,
433
+ injectBeforeExport,
434
+ injectScriptTag,
171
435
  };
package/client.js CHANGED
@@ -20,6 +20,10 @@ function BotVersionClient(options) {
20
20
  this._queue = [];
21
21
  this._flushTimer = null;
22
22
  this._flushDelay = options.flushDelay || 3000; // batch every 3 seconds
23
+ var self = this;
24
+ process.on("beforeExit", function () {
25
+ if (self._queue.length > 0) self._flush();
26
+ });
23
27
  }
24
28
 
25
29
  /**
package/index.js CHANGED
@@ -61,7 +61,7 @@ var BotVersion = {
61
61
 
62
62
  this._client = new BotVersionClient({
63
63
  apiKey: options.apiKey,
64
- platformUrl: options.platformUrl || "http://localhost:3000",
64
+ platformUrl: options.platformUrl || "http://localhost:3000/",
65
65
  debug: options.debug || false,
66
66
  timeout: options.timeout || 30000,
67
67
  });
@@ -205,9 +205,9 @@ var BotVersion = {
205
205
  .registerEndpoints(endpoints)
206
206
  .then(function () {
207
207
  console.log(
208
- "[BotVersion SDK] ✅ Static scan complete —",
208
+ "[BotVersion SDK] ✅ Endpoints queued —",
209
209
  endpoints.length,
210
- "endpoints registered",
210
+ "endpoints will be sent shortly",
211
211
  );
212
212
  })
213
213
  .catch(function (err) {
@@ -571,6 +571,14 @@ BotVersion.nextHandler = function (options) {
571
571
 
572
572
  BotVersion.nextHandler = BotVersion.nextHandler.bind(BotVersion);
573
573
 
574
+ BotVersion.appRouterHandler = function () {
575
+ throw new Error(
576
+ "[BotVersion SDK] appRouterHandler is not supported. " +
577
+ "Please run: npx botversion-sdk init --key YOUR_KEY --force " +
578
+ "to regenerate the correct route file.",
579
+ );
580
+ };
581
+
574
582
  module.exports = BotVersion;
575
583
  module.exports.default = BotVersion;
576
584
  module.exports.init = BotVersion.init;
@@ -578,6 +586,7 @@ module.exports.getEndpoints = BotVersion.getEndpoints;
578
586
  module.exports.registerEndpoint = BotVersion.registerEndpoint;
579
587
  module.exports.chat = BotVersion.chat;
580
588
  module.exports.nextHandler = BotVersion.nextHandler;
589
+ module.exports.appRouterHandler = BotVersion.appRouterHandler;
581
590
 
582
591
  // ── Framework detection ──────────────────────────────────────────────────────
583
592
  function detectFramework(app) {
package/interceptor.js CHANGED
@@ -267,8 +267,8 @@ function makeLocalCall(req, call) {
267
267
  var lib = isHttps ? https : http;
268
268
 
269
269
  var options = {
270
- hostname: req.hostname,
271
- port: (req.socket && req.socket.localPort) || (isHttps ? 443 : 80),
270
+ hostname: "127.0.0.1",
271
+ port: process.env.PORT || 3000,
272
272
  path: call.path,
273
273
  method: call.method,
274
274
  headers: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botversion-sdk",
3
- "version": "1.0.0",
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": {
package/scanner.js CHANGED
@@ -8,6 +8,8 @@ function scanExpressRoutes(app) {
8
8
  const endpoints = [];
9
9
  const seen = new Set();
10
10
 
11
+ // Force Express to initialize its router if it hasn't yet
12
+ if (app.lazyrouter) app.lazyrouter();
11
13
  const router = app._router || app.router || (app.stack ? app : null);
12
14
 
13
15
  if (!router) {
@@ -325,35 +327,32 @@ function extractQueryFieldsFromFile(content) {
325
327
  function regexpToPath(regexp, keys) {
326
328
  if (!regexp) return "";
327
329
 
328
- // Express stores the fast path string on the regexp object directly
329
- if (regexp.source === "^\\/?(?=\\/|$)") return ""; // root mount, no prefix
330
+ // Express 4.x stores the original path string directly
331
+ if (regexp.source === "^\\/?(?=\\/|$)") return "";
330
332
 
331
- // Try to extract a clean path from the regexp source
332
333
  try {
333
334
  var src = regexp.source;
334
335
 
335
- // A plain string mount like app.use('/api/v1', ...) produces
336
- // source: "^\\/api\\/v1\\/?(?=\\/|$)" — extract the prefix part
337
- var match = src.match(/^\^\\\/(.+?)\\\/\?\(\?=\\\/\|\$\)$/);
338
- if (match) {
339
- return "/" + match[1].replace(/\\\//g, "/");
340
- }
336
+ // Remove anchors and cleanup
337
+ src = src
338
+ .replace(/^\^/, "")
339
+ .replace(/\\\//g, "/")
340
+ .replace(/\/\?\(\?=\/\|\$\)$/, "")
341
+ .replace(/\/\?\$?$/, "")
342
+ .replace(/\(\?:\(\[\^\/\]\+\?\)\)/g, function (_, i) {
343
+ return keys && keys[i] ? ":" + keys[i].name : ":param";
344
+ });
341
345
 
342
- // Simpler pattern: "^\\/api\\/?(?=\\/|$)"
343
- var match2 = src.match(/^\^(\\.+?)\\\/\?\(\?=\\\/\|\$\)$/);
344
- if (match2) {
345
- return match2[1].replace(/\\\//g, "/");
346
- }
347
- } catch (e) {
348
- // ignore
349
- }
346
+ // Clean up any remaining regex artifacts
347
+ src = src.replace(/\(\?:/g, "").replace(/\)/g, "");
350
348
 
351
- // If keys exist, fall back to building from keys (parameterised mount — rare)
352
- if (keys && keys.length > 0) {
353
- return "/:param";
354
- }
349
+ if (!src || src === "/") return "";
350
+ if (!src.startsWith("/")) src = "/" + src;
355
351
 
356
- return "";
352
+ return src;
353
+ } catch (e) {
354
+ return "";
355
+ }
357
356
  }
358
357
 
359
358
  module.exports = {