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/bin/init.js +201 -62
- package/cli/detector.js +473 -71
- package/cli/generator.js +298 -137
- package/cli/writer.js +270 -6
- package/client.js +4 -0
- package/index.js +12 -3
- package/interceptor.js +2 -2
- package/package.json +1 -1
- package/scanner.js +21 -22
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
|
|
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 (
|
|
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] ✅
|
|
208
|
+
"[BotVersion SDK] ✅ Endpoints queued —",
|
|
209
209
|
endpoints.length,
|
|
210
|
-
"endpoints
|
|
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:
|
|
271
|
-
port:
|
|
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
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
|
|
329
|
-
if (regexp.source === "^\\/?(?=\\/|$)") return "";
|
|
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
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
//
|
|
343
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
return "/:param";
|
|
354
|
-
}
|
|
349
|
+
if (!src || src === "/") return "";
|
|
350
|
+
if (!src.startsWith("/")) src = "/" + src;
|
|
355
351
|
|
|
356
|
-
|
|
352
|
+
return src;
|
|
353
|
+
} catch (e) {
|
|
354
|
+
return "";
|
|
355
|
+
}
|
|
357
356
|
}
|
|
358
357
|
|
|
359
358
|
module.exports = {
|