botversion-sdk 1.0.0 → 1.0.1
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 +79 -51
- package/cli/detector.js +74 -0
- package/cli/generator.js +275 -139
- package/cli/writer.js +82 -3
- package/client.js +4 -0
- package/index.js +13 -3
- package/interceptor.js +2 -2
- package/package.json +1 -1
- package/scanner.js +21 -22
package/bin/init.js
CHANGED
|
@@ -29,6 +29,9 @@ function log(msg) {
|
|
|
29
29
|
function info(msg) {
|
|
30
30
|
console.log(`${c.cyan} ℹ${c.reset} ${msg}`);
|
|
31
31
|
}
|
|
32
|
+
function info2(msg) {
|
|
33
|
+
console.log(`${c.cyan} ℹ${c.reset} ${msg}`);
|
|
34
|
+
}
|
|
32
35
|
function success(msg) {
|
|
33
36
|
console.log(`${c.green} ✔${c.reset} ${msg}`);
|
|
34
37
|
}
|
|
@@ -106,10 +109,10 @@ async function main() {
|
|
|
106
109
|
}
|
|
107
110
|
|
|
108
111
|
// Run full detection
|
|
109
|
-
const
|
|
112
|
+
const detected = detector.detect(workingDir);
|
|
110
113
|
|
|
111
114
|
// ── Check if already initialized ─────────────────────────────────────────
|
|
112
|
-
if (
|
|
115
|
+
if (detected.alreadyInitialized && !args.force) {
|
|
113
116
|
warn("BotVersion SDK is already initialized in this project.");
|
|
114
117
|
log(`\n To reinitialize, run with --force flag:\n`);
|
|
115
118
|
log(` npx botversion-sdk init --key ${args.key} --force\n`);
|
|
@@ -119,68 +122,71 @@ async function main() {
|
|
|
119
122
|
// ── Framework check ───────────────────────────────────────────────────────
|
|
120
123
|
step("Detecting framework...");
|
|
121
124
|
|
|
122
|
-
if (!
|
|
125
|
+
if (!detected.framework.name) {
|
|
123
126
|
error("Could not detect a supported framework.");
|
|
124
127
|
log(`\n Supported: Express.js, Next.js`);
|
|
125
128
|
log(` Make sure you have them listed in package.json\n`);
|
|
126
129
|
process.exit(1);
|
|
127
130
|
}
|
|
128
131
|
|
|
129
|
-
if (!
|
|
132
|
+
if (!detected.framework.supported) {
|
|
130
133
|
warn(
|
|
131
|
-
`Detected: ${
|
|
134
|
+
`Detected: ${detected.framework.name} (not yet supported for auto-setup)`,
|
|
132
135
|
);
|
|
133
136
|
log("");
|
|
134
|
-
log(
|
|
137
|
+
log(
|
|
138
|
+
generator.generateManualInstructions(detected.framework.name, args.key),
|
|
139
|
+
);
|
|
135
140
|
process.exit(0);
|
|
136
141
|
}
|
|
137
142
|
|
|
138
|
-
success(`Framework: ${
|
|
143
|
+
success(`Framework: ${detected.framework.name}`);
|
|
139
144
|
info(
|
|
140
|
-
`Module system: ${
|
|
145
|
+
`Module system: ${detected.moduleSystem === "esm" ? "ES Modules" : "CommonJS"}`,
|
|
141
146
|
);
|
|
142
|
-
info(`Language: ${
|
|
147
|
+
info(`Language: ${detected.isTypeScript ? "TypeScript" : "JavaScript"}`);
|
|
143
148
|
|
|
144
149
|
// ── Auth detection ────────────────────────────────────────────────────────
|
|
145
150
|
step("Detecting auth library...");
|
|
146
151
|
|
|
147
|
-
let auth =
|
|
152
|
+
let auth = detected.auth;
|
|
148
153
|
|
|
149
154
|
if (!auth.name) {
|
|
150
155
|
warn("No auth library detected automatically.");
|
|
151
156
|
auth = await prompts.promptAuthLibrary();
|
|
152
|
-
|
|
157
|
+
detected.auth = auth;
|
|
153
158
|
} else if (!auth.supported) {
|
|
154
159
|
warn(`Detected auth: ${auth.name} (not yet supported for auto-setup)`);
|
|
155
160
|
warn("Will set up without user context — you can add it manually later.");
|
|
156
161
|
const proceed = await prompts.confirm("Continue without auth?", true);
|
|
157
162
|
if (!proceed) process.exit(0);
|
|
158
163
|
auth = { name: auth.name, supported: false };
|
|
159
|
-
|
|
164
|
+
detected.auth = auth;
|
|
160
165
|
} else {
|
|
161
166
|
const versionLabel = auth.version ? ` (${auth.version})` : "";
|
|
162
167
|
success(`Auth: ${auth.name}${versionLabel}`);
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
// ── Package manager ───────────────────────────────────────────────────────
|
|
166
|
-
info(`Package manager: ${
|
|
171
|
+
info(`Package manager: ${detected.packageManager}`);
|
|
167
172
|
|
|
168
173
|
// ─────────────────────────────────────────────────────────────────────────
|
|
169
174
|
// FRAMEWORK: EXPRESS
|
|
170
175
|
// ─────────────────────────────────────────────────────────────────────────
|
|
171
|
-
if (
|
|
172
|
-
await setupExpress(
|
|
176
|
+
if (detected.framework.name === "express") {
|
|
177
|
+
await setupExpress(detected, args, changes);
|
|
173
178
|
}
|
|
174
179
|
|
|
175
180
|
// ─────────────────────────────────────────────────────────────────────────
|
|
176
181
|
// FRAMEWORK: NEXT.JS
|
|
177
182
|
// ─────────────────────────────────────────────────────────────────────────
|
|
178
|
-
else if (
|
|
179
|
-
await setupNextJs(
|
|
183
|
+
else if (detected.framework.name === "next") {
|
|
184
|
+
await setupNextJs(detected, args, changes);
|
|
180
185
|
}
|
|
181
186
|
|
|
182
187
|
// ── Write API key to .env / .env.local ────────────────────────────────────
|
|
183
|
-
const envFileName =
|
|
188
|
+
const envFileName =
|
|
189
|
+
detected.framework.name === "next" ? ".env.local" : ".env";
|
|
184
190
|
const envPath = path.join(workingDir, envFileName);
|
|
185
191
|
const envLine = `BOTVERSION_API_KEY=${args.key}`;
|
|
186
192
|
const envContent = fs.existsSync(envPath)
|
|
@@ -216,16 +222,16 @@ async function main() {
|
|
|
216
222
|
|
|
217
223
|
// ─── EXPRESS SETUP ────────────────────────────────────────────────────────────
|
|
218
224
|
|
|
219
|
-
async function setupExpress(
|
|
225
|
+
async function setupExpress(detected, args, changes) {
|
|
220
226
|
step("Setting up Express...");
|
|
221
227
|
|
|
222
228
|
// Find entry point
|
|
223
|
-
let entryPoint =
|
|
229
|
+
let entryPoint = detected.entryPoint;
|
|
224
230
|
|
|
225
231
|
if (!entryPoint || !fs.existsSync(entryPoint)) {
|
|
226
232
|
warn("Could not find your server entry point automatically.");
|
|
227
233
|
const manualPath = await prompts.promptEntryPoint();
|
|
228
|
-
entryPoint = path.resolve(
|
|
234
|
+
entryPoint = path.resolve(detected.cwd, manualPath);
|
|
229
235
|
|
|
230
236
|
if (!fs.existsSync(entryPoint)) {
|
|
231
237
|
error(`File not found: ${entryPoint}`);
|
|
@@ -233,40 +239,65 @@ async function setupExpress(info, args, changes) {
|
|
|
233
239
|
}
|
|
234
240
|
}
|
|
235
241
|
|
|
236
|
-
success(`Entry point: ${path.relative(
|
|
242
|
+
success(`Entry point: ${path.relative(detected.cwd, entryPoint)}`);
|
|
237
243
|
|
|
238
244
|
// Generate the init code
|
|
239
|
-
const generated = generator.generateExpressInit(
|
|
245
|
+
const generated = generator.generateExpressInit(detected, args.key);
|
|
240
246
|
|
|
241
247
|
// Find app.listen() and inject before it
|
|
242
248
|
const listenCall = detector.findListenCall(entryPoint);
|
|
243
249
|
|
|
244
|
-
|
|
245
|
-
|
|
250
|
+
// PATTERN 2: Separate app file with module.exports = app
|
|
251
|
+
if (detected.appFile) {
|
|
252
|
+
info(`Found app file: ${path.relative(detected.cwd, detected.appFile)}`);
|
|
253
|
+
const generated2 = generator.generateExpressInit(detected, args.key);
|
|
254
|
+
const result = writer.injectBeforeExport(
|
|
255
|
+
detected.appFile,
|
|
256
|
+
generated2.initBlock,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
if (result.success) {
|
|
260
|
+
success(
|
|
261
|
+
`Injected BotVersion.init() into ${path.relative(detected.cwd, detected.appFile)}`,
|
|
262
|
+
);
|
|
263
|
+
changes.modified.push(path.relative(detected.cwd, detected.appFile));
|
|
264
|
+
} else if (result.reason === "already_exists") {
|
|
265
|
+
warn("BotVersion already found — skipping injection.");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// PATTERN 1 & 3 & 4: app.listen() or server.listen() in entry file
|
|
270
|
+
else if (
|
|
271
|
+
detected.listenCall ||
|
|
272
|
+
detected.listenInsideCallback ||
|
|
273
|
+
detected.createServer
|
|
274
|
+
) {
|
|
246
275
|
const result = writer.injectBeforeListen(entryPoint, generated.initBlock);
|
|
247
276
|
|
|
248
277
|
if (result.success) {
|
|
249
278
|
success(`Injected BotVersion.init() before app.listen()`);
|
|
250
|
-
changes.modified.push(path.relative(
|
|
279
|
+
changes.modified.push(path.relative(detected.cwd, entryPoint));
|
|
251
280
|
if (result.backup) changes.backups.push(result.backup);
|
|
252
281
|
} else if (result.reason === "already_exists") {
|
|
253
282
|
warn("BotVersion already found in entry point — skipping injection.");
|
|
254
283
|
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// LAST RESORT: ask the user
|
|
287
|
+
else {
|
|
288
|
+
warn("Could not find the right place to inject automatically.");
|
|
258
289
|
const response = await prompts.promptMissingListenCall(
|
|
259
|
-
path.relative(
|
|
290
|
+
path.relative(detected.cwd, entryPoint),
|
|
260
291
|
);
|
|
261
292
|
|
|
262
293
|
if (response.action === "append") {
|
|
263
294
|
const result = writer.appendToFile(entryPoint, generated.initBlock);
|
|
264
295
|
if (result.success) {
|
|
265
296
|
success("Appended BotVersion setup to end of file.");
|
|
266
|
-
changes.modified.push(path.relative(
|
|
297
|
+
changes.modified.push(path.relative(detected.cwd, entryPoint));
|
|
267
298
|
}
|
|
268
299
|
} else if (response.action === "manual_path") {
|
|
269
|
-
const altPath = path.resolve(
|
|
300
|
+
const altPath = path.resolve(detected.cwd, response.filePath);
|
|
270
301
|
if (fs.existsSync(altPath)) {
|
|
271
302
|
const result = writer.injectBeforeListen(altPath, generated.initBlock);
|
|
272
303
|
if (result.success) {
|
|
@@ -280,7 +311,6 @@ async function setupExpress(info, args, changes) {
|
|
|
280
311
|
);
|
|
281
312
|
}
|
|
282
313
|
} else {
|
|
283
|
-
// skip — print manual instructions
|
|
284
314
|
changes.manual.push(
|
|
285
315
|
`Add this to your server file before app.listen():\n\n${generated.initBlock}`,
|
|
286
316
|
);
|
|
@@ -291,10 +321,10 @@ async function setupExpress(info, args, changes) {
|
|
|
291
321
|
|
|
292
322
|
// ─── NEXT.JS SETUP ────────────────────────────────────────────────────────────
|
|
293
323
|
|
|
294
|
-
async function setupNextJs(
|
|
324
|
+
async function setupNextJs(detected, args, changes) {
|
|
295
325
|
step("Setting up Next.js...");
|
|
296
326
|
|
|
297
|
-
const nextInfo =
|
|
327
|
+
const nextInfo = detected.next;
|
|
298
328
|
const baseDir = nextInfo.baseDir;
|
|
299
329
|
|
|
300
330
|
info2(
|
|
@@ -302,20 +332,23 @@ async function setupNextJs(info, args, changes) {
|
|
|
302
332
|
);
|
|
303
333
|
|
|
304
334
|
// ── next-auth config location ─────────────────────────────────────────────
|
|
305
|
-
if (
|
|
335
|
+
if (detected.auth.name === "next-auth" && !detected.nextAuthConfig) {
|
|
306
336
|
warn("Could not find authOptions location automatically.");
|
|
307
337
|
const configPath = await prompts.promptNextAuthConfigPath();
|
|
308
|
-
|
|
309
|
-
path: path.resolve(
|
|
338
|
+
detected.nextAuthConfig = {
|
|
339
|
+
path: path.resolve(detected.cwd, configPath),
|
|
310
340
|
relativePath: configPath,
|
|
311
341
|
};
|
|
312
342
|
}
|
|
313
343
|
|
|
314
344
|
// ── 1. Create instrumentation.js ──────────────────────────────────────────
|
|
315
|
-
const instrExt =
|
|
316
|
-
const instrFile = path.join(
|
|
345
|
+
const instrExt = detected.generateTs ? ".ts" : ".js";
|
|
346
|
+
const instrFile = path.join(detected.cwd, `instrumentation${instrExt}`);
|
|
317
347
|
|
|
318
|
-
const instrContent = generator.generateInstrumentationFile(
|
|
348
|
+
const instrContent = generator.generateInstrumentationFile(
|
|
349
|
+
detected,
|
|
350
|
+
args.key,
|
|
351
|
+
);
|
|
319
352
|
const instrResult = writer.createFile(instrFile, instrContent, args.force);
|
|
320
353
|
|
|
321
354
|
if (instrResult.success) {
|
|
@@ -333,15 +366,15 @@ async function setupNextJs(info, args, changes) {
|
|
|
333
366
|
}
|
|
334
367
|
|
|
335
368
|
// ── 2. Patch next.config.js ───────────────────────────────────────────────
|
|
336
|
-
const configPatch = generator.generateNextConfigPatch(
|
|
369
|
+
const configPatch = generator.generateNextConfigPatch(detected.cwd);
|
|
337
370
|
|
|
338
371
|
if (configPatch) {
|
|
339
372
|
if (configPatch.alreadyPatched) {
|
|
340
373
|
info("next.config.js already has instrumentationHook — skipping.");
|
|
341
374
|
} else {
|
|
342
375
|
fs.writeFileSync(configPatch.path, configPatch.content, "utf8");
|
|
343
|
-
success(`Updated ${path.relative(
|
|
344
|
-
changes.modified.push(path.relative(
|
|
376
|
+
success(`Updated ${path.relative(detected.cwd, configPatch.path)}`);
|
|
377
|
+
changes.modified.push(path.relative(detected.cwd, configPatch.path));
|
|
345
378
|
}
|
|
346
379
|
} else {
|
|
347
380
|
warn("Could not find next.config.js — please add this manually:");
|
|
@@ -356,7 +389,7 @@ async function setupNextJs(info, args, changes) {
|
|
|
356
389
|
const chatDir = path.join(pagesBase, "api", "botversion");
|
|
357
390
|
const chatFile = path.join(chatDir, `chat${instrExt}`);
|
|
358
391
|
|
|
359
|
-
const chatContent = generator.generateNextPagesChatRoute(
|
|
392
|
+
const chatContent = generator.generateNextPagesChatRoute(detected);
|
|
360
393
|
const chatResult = writer.createFile(chatFile, chatContent, args.force);
|
|
361
394
|
|
|
362
395
|
const relPath = `${nextInfo.srcDir ? "src/" : ""}pages/api/botversion/chat${instrExt}`;
|
|
@@ -382,7 +415,7 @@ async function setupNextJs(info, args, changes) {
|
|
|
382
415
|
const chatDir = path.join(appBase, "api", "botversion", "chat");
|
|
383
416
|
const chatFile = path.join(chatDir, `route${instrExt}`);
|
|
384
417
|
|
|
385
|
-
const chatContent = generator.generateNextAppChatRoute(
|
|
418
|
+
const chatContent = generator.generateNextAppChatRoute(detected);
|
|
386
419
|
const relPath = `${nextInfo.srcDir ? "src/" : ""}app/api/botversion/chat/route${instrExt}`;
|
|
387
420
|
|
|
388
421
|
const chatResult = writer.createFile(chatFile, chatContent, args.force);
|
|
@@ -403,11 +436,6 @@ async function setupNextJs(info, args, changes) {
|
|
|
403
436
|
}
|
|
404
437
|
}
|
|
405
438
|
|
|
406
|
-
// ─── helper used inside setupNextJs ──────────────────────────────────────────
|
|
407
|
-
function info2(msg) {
|
|
408
|
-
console.log(`${c.cyan} ℹ${c.reset} ${msg}`);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
439
|
// ─── RUN ──────────────────────────────────────────────────────────────────────
|
|
412
440
|
|
|
413
441
|
main().catch((err) => {
|
package/cli/detector.js
CHANGED
|
@@ -269,6 +269,43 @@ function findListenCall(filePath) {
|
|
|
269
269
|
return null;
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
function findModuleExportsApp(filePath) {
|
|
273
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
274
|
+
const lines = content.split("\n");
|
|
275
|
+
for (let i = 0; i < lines.length; i++) {
|
|
276
|
+
if (/module\.exports\s*=\s*app/.test(lines[i])) {
|
|
277
|
+
return { lineIndex: i, lineNumber: i + 1, content: lines[i] };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function findListenInsideCallback(filePath) {
|
|
284
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
285
|
+
const lines = content.split("\n");
|
|
286
|
+
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)
|
|
289
|
+
const indentation = lines[i].match(/^(\s*)/)[1].length;
|
|
290
|
+
if (indentation > 0) {
|
|
291
|
+
return { lineIndex: i, lineNumber: i + 1, insideCallback: true };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function findCreateServer(filePath) {
|
|
299
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
300
|
+
const lines = content.split("\n");
|
|
301
|
+
for (let i = 0; i < lines.length; i++) {
|
|
302
|
+
if (/createServer\s*\(\s*app\s*\)/.test(lines[i])) {
|
|
303
|
+
return { lineIndex: i, lineNumber: i + 1, content: lines[i] };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
272
309
|
// ─── AUTH DETECTION ──────────────────────────────────────────────────────────
|
|
273
310
|
|
|
274
311
|
const AUTH_LIBS = [
|
|
@@ -481,6 +518,16 @@ function findFileWithContent(dir, searchString, extensions, maxDepth) {
|
|
|
481
518
|
return walk(dir, 0);
|
|
482
519
|
}
|
|
483
520
|
|
|
521
|
+
function detectAppVarName(filePath) {
|
|
522
|
+
try {
|
|
523
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
524
|
+
const match = content.match(/(?:const|let|var)\s+(\w+)\s*=\s*express\s*\(/);
|
|
525
|
+
return match ? match[1] : "app";
|
|
526
|
+
} catch (e) {
|
|
527
|
+
return "app";
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
484
531
|
// ─── MAIN DETECT FUNCTION ────────────────────────────────────────────────────
|
|
485
532
|
|
|
486
533
|
function detect(cwd) {
|
|
@@ -524,6 +571,29 @@ function detect(cwd) {
|
|
|
524
571
|
result.entryPoint = detectExpressEntry(cwd, pkg);
|
|
525
572
|
if (result.entryPoint) {
|
|
526
573
|
result.listenCall = findListenCall(result.entryPoint);
|
|
574
|
+
result.moduleExportsApp = findModuleExportsApp(result.entryPoint);
|
|
575
|
+
result.listenInsideCallback = findListenInsideCallback(result.entryPoint);
|
|
576
|
+
result.createServer = findCreateServer(result.entryPoint);
|
|
577
|
+
result.appVarName = detectAppVarName(result.entryPoint);
|
|
578
|
+
|
|
579
|
+
// Also check for app file separately (pattern 2)
|
|
580
|
+
const appFileCandidates = [
|
|
581
|
+
"src/app.js",
|
|
582
|
+
"src/app.ts",
|
|
583
|
+
"app.js",
|
|
584
|
+
"app.ts",
|
|
585
|
+
];
|
|
586
|
+
for (const candidate of appFileCandidates) {
|
|
587
|
+
const fullPath = path.join(cwd, candidate);
|
|
588
|
+
if (fs.existsSync(fullPath) && fullPath !== result.entryPoint) {
|
|
589
|
+
const exportCall = findModuleExportsApp(fullPath);
|
|
590
|
+
if (exportCall) {
|
|
591
|
+
result.appFile = fullPath;
|
|
592
|
+
result.appFileExport = exportCall;
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
527
597
|
}
|
|
528
598
|
}
|
|
529
599
|
|
|
@@ -555,4 +625,8 @@ module.exports = {
|
|
|
555
625
|
detectExistingBotVersion,
|
|
556
626
|
findFileWithContent,
|
|
557
627
|
findListenCall,
|
|
628
|
+
findModuleExportsApp,
|
|
629
|
+
findListenInsideCallback,
|
|
630
|
+
findCreateServer,
|
|
631
|
+
detectAppVarName,
|
|
558
632
|
};
|
package/cli/generator.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
// botversion-sdk/cli/generator.js
|
|
2
|
-
|
|
3
2
|
"use strict";
|
|
4
3
|
|
|
5
4
|
const path = require("path");
|
|
5
|
+
const fs = require("fs");
|
|
6
6
|
|
|
7
|
-
// ─── EXPRESS CODE GENERATION
|
|
7
|
+
// ─── EXPRESS CODE GENERATION ──────────────────────────────────────────────────
|
|
8
8
|
|
|
9
9
|
function generateExpressInit(info, apiKey) {
|
|
10
10
|
const { moduleSystem, isTypeScript, auth } = info;
|
|
11
11
|
const isESM = moduleSystem === "esm";
|
|
12
12
|
|
|
13
|
+
// FIX #2: Detect the actual app variable name used in the entry file
|
|
14
|
+
const appVarName = info.appVarName || "app";
|
|
15
|
+
|
|
13
16
|
const importLine = isESM
|
|
14
17
|
? `import BotVersion from 'botversion-sdk';`
|
|
15
18
|
: `const BotVersion = require('botversion-sdk');`;
|
|
@@ -20,11 +23,11 @@ function generateExpressInit(info, apiKey) {
|
|
|
20
23
|
// BotVersion AI Agent — auto-added by botversion-sdk init
|
|
21
24
|
${importLine}
|
|
22
25
|
|
|
23
|
-
BotVersion.init(
|
|
24
|
-
apiKey:
|
|
26
|
+
BotVersion.init(${appVarName}, {
|
|
27
|
+
apiKey: process.env.BOTVERSION_API_KEY,
|
|
25
28
|
});
|
|
26
29
|
|
|
27
|
-
|
|
30
|
+
${appVarName}.post('/api/botversion/chat', (req, res) => {
|
|
28
31
|
BotVersion.chat(req, res);
|
|
29
32
|
});
|
|
30
33
|
`;
|
|
@@ -96,30 +99,54 @@ function generateExpressUserContext(auth) {
|
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
101
|
|
|
99
|
-
// ─── NEXT.JS
|
|
102
|
+
// ─── NEXT.JS INSTRUMENTATION FILE ────────────────────────────────────────────
|
|
103
|
+
|
|
100
104
|
function generateInstrumentationFile(info, apiKey) {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
const { next, moduleSystem } = info;
|
|
106
|
+
|
|
107
|
+
// FIX #3: Only include pagesDir if Pages Router actually exists
|
|
108
|
+
// FIX #8: Use dynamic import instead of require() to support ESM projects
|
|
109
|
+
const hasPagesRouter = next?.pagesRouter;
|
|
110
|
+
const hasAppRouter = next?.appRouter;
|
|
111
|
+
|
|
112
|
+
const pagesDirLine = hasPagesRouter
|
|
113
|
+
? next?.srcDir
|
|
114
|
+
? `path.join(process.cwd(), 'src', 'pages')`
|
|
115
|
+
: `path.join(process.cwd(), 'pages')`
|
|
116
|
+
: null;
|
|
117
|
+
|
|
118
|
+
const appDirLine = hasAppRouter
|
|
119
|
+
? next?.srcDir
|
|
120
|
+
? `path.join(process.cwd(), 'src', 'app')`
|
|
121
|
+
: `path.join(process.cwd(), 'app')`
|
|
122
|
+
: null;
|
|
123
|
+
|
|
124
|
+
// Build pagesDir option only if Pages Router exists
|
|
125
|
+
const pagesDirOption = pagesDirLine
|
|
126
|
+
? `\n pagesDir: ${pagesDirLine},`
|
|
127
|
+
: "";
|
|
128
|
+
|
|
129
|
+
// Build appDir option only if App Router exists (for future scanner support)
|
|
130
|
+
const appDirOption = appDirLine ? `\n appDir: ${appDirLine},` : "";
|
|
104
131
|
|
|
105
132
|
return `export async function register() {
|
|
106
133
|
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
|
107
|
-
|
|
108
|
-
const
|
|
134
|
+
// FIX: Use dynamic import to support both CJS and ESM projects
|
|
135
|
+
const { default: BotVersion } = await import('botversion-sdk');
|
|
136
|
+
const { default: path } = await import('path');
|
|
137
|
+
|
|
109
138
|
BotVersion.init({
|
|
110
|
-
apiKey: process.env.BOTVERSION_API_KEY
|
|
111
|
-
pagesDir: ${pagesDir},
|
|
139
|
+
apiKey: process.env.BOTVERSION_API_KEY,${pagesDirOption}${appDirOption}
|
|
112
140
|
});
|
|
113
141
|
}
|
|
114
142
|
}
|
|
115
143
|
`;
|
|
116
144
|
}
|
|
117
145
|
|
|
118
|
-
// ─── NEXT.JS CHAT ROUTE — PAGES ROUTER
|
|
146
|
+
// ─── NEXT.JS CHAT ROUTE — PAGES ROUTER ───────────────────────────────────────
|
|
119
147
|
|
|
120
148
|
function generateNextPagesChatRoute(info) {
|
|
121
|
-
const { auth
|
|
122
|
-
const isESM = moduleSystem === "esm";
|
|
149
|
+
const { auth } = info;
|
|
123
150
|
|
|
124
151
|
switch (auth.name) {
|
|
125
152
|
case "next-auth":
|
|
@@ -137,18 +164,10 @@ function generateNextAuthPagesRoute(info) {
|
|
|
137
164
|
const { nextAuthConfig, auth, next, generateTs } = info;
|
|
138
165
|
const isV5 = auth.version === "v5";
|
|
139
166
|
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
const chatFileDir = path.join(
|
|
143
|
-
next.baseDir, // handles src/ automatically
|
|
144
|
-
"pages",
|
|
145
|
-
"api",
|
|
146
|
-
"botversion",
|
|
147
|
-
);
|
|
167
|
+
// Compute correct relative import path for authOptions
|
|
168
|
+
const chatFileDir = path.join(next.baseDir, "pages", "api", "botversion");
|
|
148
169
|
|
|
149
|
-
// Determine the import path for authOptions
|
|
150
170
|
let authImportPath = "../auth/[...nextauth]";
|
|
151
|
-
|
|
152
171
|
if (nextAuthConfig) {
|
|
153
172
|
const rel = path
|
|
154
173
|
.relative(chatFileDir, nextAuthConfig.path)
|
|
@@ -168,8 +187,7 @@ export default BotVersion.nextHandler({
|
|
|
168
187
|
`;
|
|
169
188
|
}
|
|
170
189
|
|
|
171
|
-
|
|
172
|
-
if (info.generateTs) {
|
|
190
|
+
if (generateTs) {
|
|
173
191
|
return `import BotVersion from 'botversion-sdk';
|
|
174
192
|
import { getServerSession } from 'next-auth';
|
|
175
193
|
import { authOptions } from '${authImportPath}';
|
|
@@ -184,7 +202,6 @@ export default BotVersion.nextHandler({
|
|
|
184
202
|
`;
|
|
185
203
|
}
|
|
186
204
|
|
|
187
|
-
// Plain JS — works for all standard Next.js projects
|
|
188
205
|
return `import BotVersion from 'botversion-sdk';
|
|
189
206
|
import { getServerSession } from 'next-auth';
|
|
190
207
|
import { authOptions } from '${authImportPath}';
|
|
@@ -233,9 +250,7 @@ function generateAuthlessPagesRoute(info) {
|
|
|
233
250
|
|
|
234
251
|
const comment =
|
|
235
252
|
auth.name && !auth.supported
|
|
236
|
-
? `// TODO: We detected ${auth.name} but don't have automatic support yet.
|
|
237
|
-
// Add your own getSession below to pass user context to the agent.
|
|
238
|
-
// See: https://docs.botversion.com/auth\n`
|
|
253
|
+
? `// TODO: We detected ${auth.name} but don't have automatic support yet.\n// Add your own getSession below to pass user context to the agent.\n// See: https://docs.botversion.com/auth\n`
|
|
239
254
|
: "";
|
|
240
255
|
|
|
241
256
|
if (isTypeScript) {
|
|
@@ -269,7 +284,7 @@ export default BotVersion.nextHandler({
|
|
|
269
284
|
// ─── NEXT.JS CHAT ROUTE — APP ROUTER ─────────────────────────────────────────
|
|
270
285
|
|
|
271
286
|
function generateNextAppChatRoute(info) {
|
|
272
|
-
const { auth
|
|
287
|
+
const { auth } = info;
|
|
273
288
|
|
|
274
289
|
switch (auth.name) {
|
|
275
290
|
case "next-auth":
|
|
@@ -284,118 +299,282 @@ function generateNextAppChatRoute(info) {
|
|
|
284
299
|
}
|
|
285
300
|
|
|
286
301
|
function generateNextAuthAppRoute(info) {
|
|
287
|
-
const { auth, isTypeScript } = info;
|
|
302
|
+
const { auth, isTypeScript, next, nextAuthConfig } = info;
|
|
288
303
|
const isV5 = auth.version === "v5";
|
|
289
304
|
|
|
305
|
+
// FIX #5: Compute the correct relative import path instead of hardcoding @/lib/auth
|
|
306
|
+
const chatFileDir = path.join(
|
|
307
|
+
next.baseDir,
|
|
308
|
+
"app",
|
|
309
|
+
"api",
|
|
310
|
+
"botversion",
|
|
311
|
+
"chat",
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
let authImportPath = "@/lib/auth"; // fallback alias
|
|
315
|
+
if (nextAuthConfig) {
|
|
316
|
+
const rel = path
|
|
317
|
+
.relative(chatFileDir, nextAuthConfig.path)
|
|
318
|
+
.replace(/\\/g, "/")
|
|
319
|
+
.replace(/\.(js|ts)$/, "");
|
|
320
|
+
authImportPath = rel.startsWith(".") ? rel : "./" + rel;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const typeAnnotation = isTypeScript ? ": NextRequest" : "";
|
|
324
|
+
const nextRequestImport = isTypeScript
|
|
325
|
+
? `import { NextRequest, NextResponse } from 'next/server';\n`
|
|
326
|
+
: `import { NextResponse } from 'next/server';\n`;
|
|
327
|
+
|
|
290
328
|
if (isV5) {
|
|
291
329
|
return `import BotVersion from 'botversion-sdk';
|
|
292
|
-
import { auth } from '
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
330
|
+
import { auth } from '${authImportPath}';
|
|
331
|
+
${nextRequestImport}
|
|
332
|
+
// FIX #1: appRouterHandler is implemented here directly since App Router
|
|
333
|
+
// does not support the nextHandler() Pages pattern
|
|
334
|
+
export async function POST(req${typeAnnotation}) {
|
|
335
|
+
try {
|
|
336
|
+
const session = await auth();
|
|
337
|
+
const body = await req.json();
|
|
338
|
+
|
|
339
|
+
const result = await BotVersion.nextHandler({
|
|
340
|
+
apiKey: process.env.BOTVERSION_API_KEY,
|
|
341
|
+
getSession: async () => session,
|
|
342
|
+
})({ ...req, body }, { json: (d) => d, status: () => ({ json: (d) => d }) });
|
|
298
343
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
name: session?.user?.name,
|
|
305
|
-
},
|
|
306
|
-
});
|
|
344
|
+
return NextResponse.json(result);
|
|
345
|
+
} catch (err) {
|
|
346
|
+
console.error('[BotVersion] App Router handler error:', err);
|
|
347
|
+
return NextResponse.json({ error: 'Agent error' }, { status: 500 });
|
|
348
|
+
}
|
|
307
349
|
}
|
|
308
350
|
`;
|
|
309
351
|
}
|
|
310
352
|
|
|
311
353
|
return `import BotVersion from 'botversion-sdk';
|
|
312
354
|
import { getServerSession } from 'next-auth';
|
|
313
|
-
import { authOptions } from '
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
355
|
+
import { authOptions } from '${authImportPath}';
|
|
356
|
+
${nextRequestImport}
|
|
357
|
+
export async function POST(req${typeAnnotation}) {
|
|
358
|
+
try {
|
|
359
|
+
const session = await getServerSession(authOptions);
|
|
360
|
+
const body = await req.json();
|
|
319
361
|
|
|
320
|
-
|
|
321
|
-
body,
|
|
322
|
-
userContext: {
|
|
362
|
+
const userContext = {
|
|
323
363
|
userId: session?.user?.id,
|
|
324
364
|
email: session?.user?.email,
|
|
325
365
|
name: session?.user?.name,
|
|
326
|
-
}
|
|
327
|
-
|
|
366
|
+
};
|
|
367
|
+
|
|
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\`, {
|
|
370
|
+
method: 'POST',
|
|
371
|
+
headers: { 'Content-Type': 'application/json' },
|
|
372
|
+
body: JSON.stringify({
|
|
373
|
+
chatbotId: body.chatbotId,
|
|
374
|
+
publicKey: body.publicKey,
|
|
375
|
+
query: body.message,
|
|
376
|
+
previousChats: body.conversationHistory || [],
|
|
377
|
+
pageContext: body.pageContext || {},
|
|
378
|
+
userContext,
|
|
379
|
+
}),
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const data = await response.json();
|
|
383
|
+
return NextResponse.json(data);
|
|
384
|
+
} catch (err) {
|
|
385
|
+
console.error('[BotVersion] App Router handler error:', err);
|
|
386
|
+
return NextResponse.json({ error: 'Agent error' }, { status: 500 });
|
|
387
|
+
}
|
|
328
388
|
}
|
|
329
389
|
`;
|
|
330
390
|
}
|
|
331
391
|
|
|
332
392
|
function generateClerkAppRoute(info) {
|
|
333
|
-
const {
|
|
393
|
+
const { isTypeScript } = info;
|
|
394
|
+
const typeAnnotation = isTypeScript ? ": NextRequest" : "";
|
|
395
|
+
const nextRequestImport = isTypeScript
|
|
396
|
+
? `import { NextRequest, NextResponse } from 'next/server';\n`
|
|
397
|
+
: `import { NextResponse } from 'next/server';\n`;
|
|
334
398
|
|
|
335
399
|
return `import BotVersion from 'botversion-sdk';
|
|
336
400
|
import { auth } from '@clerk/nextjs/server';
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
401
|
+
${nextRequestImport}
|
|
402
|
+
export async function POST(req${typeAnnotation}) {
|
|
403
|
+
try {
|
|
404
|
+
// FIX #6: auth() is async in Clerk v5+ — must be awaited
|
|
405
|
+
const { userId } = await auth();
|
|
406
|
+
const body = await req.json();
|
|
407
|
+
|
|
408
|
+
const response = await fetch(\`\${process.env.BOTVERSION_PLATFORM_URL || 'https://chatbusiness-two.vercel.app'}/api/chatbot/widget-chat\`, {
|
|
409
|
+
method: 'POST',
|
|
410
|
+
headers: { 'Content-Type': 'application/json' },
|
|
411
|
+
body: JSON.stringify({
|
|
412
|
+
chatbotId: body.chatbotId,
|
|
413
|
+
publicKey: body.publicKey,
|
|
414
|
+
query: body.message,
|
|
415
|
+
previousChats: body.conversationHistory || [],
|
|
416
|
+
pageContext: body.pageContext || {},
|
|
417
|
+
userContext: { userId },
|
|
418
|
+
}),
|
|
419
|
+
});
|
|
342
420
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
421
|
+
const data = await response.json();
|
|
422
|
+
return NextResponse.json(data);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
console.error('[BotVersion] App Router handler error:', err);
|
|
425
|
+
return NextResponse.json({ error: 'Agent error' }, { status: 500 });
|
|
426
|
+
}
|
|
347
427
|
}
|
|
348
428
|
`;
|
|
349
429
|
}
|
|
350
430
|
|
|
351
431
|
function generateSupabaseAppRoute(info) {
|
|
352
432
|
const { isTypeScript } = info;
|
|
433
|
+
const typeAnnotation = isTypeScript ? ": NextRequest" : "";
|
|
434
|
+
const nextRequestImport = isTypeScript
|
|
435
|
+
? `import { NextRequest, NextResponse } from 'next/server';\n`
|
|
436
|
+
: `import { NextResponse } from 'next/server';\n`;
|
|
353
437
|
|
|
354
|
-
return `import
|
|
355
|
-
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
|
|
438
|
+
return `import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
|
|
356
439
|
import { cookies } from 'next/headers';
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
440
|
+
${nextRequestImport}
|
|
441
|
+
export async function POST(req${typeAnnotation}) {
|
|
442
|
+
try {
|
|
443
|
+
const supabase = createRouteHandlerClient({ cookies });
|
|
444
|
+
const { data: { session } } = await supabase.auth.getSession();
|
|
445
|
+
const body = await req.json();
|
|
446
|
+
|
|
447
|
+
const response = await fetch(\`\${process.env.BOTVERSION_PLATFORM_URL || 'https://chatbusiness-two.vercel.app'}/api/chatbot/widget-chat\`, {
|
|
448
|
+
method: 'POST',
|
|
449
|
+
headers: { 'Content-Type': 'application/json' },
|
|
450
|
+
body: JSON.stringify({
|
|
451
|
+
chatbotId: body.chatbotId,
|
|
452
|
+
publicKey: body.publicKey,
|
|
453
|
+
query: body.message,
|
|
454
|
+
previousChats: body.conversationHistory || [],
|
|
455
|
+
pageContext: body.pageContext || {},
|
|
456
|
+
userContext: {
|
|
457
|
+
userId: session?.user?.id,
|
|
458
|
+
email: session?.user?.email,
|
|
459
|
+
},
|
|
460
|
+
}),
|
|
461
|
+
});
|
|
363
462
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
});
|
|
463
|
+
const data = await response.json();
|
|
464
|
+
return NextResponse.json(data);
|
|
465
|
+
} catch (err) {
|
|
466
|
+
console.error('[BotVersion] App Router handler error:', err);
|
|
467
|
+
return NextResponse.json({ error: 'Agent error' }, { status: 500 });
|
|
468
|
+
}
|
|
371
469
|
}
|
|
372
470
|
`;
|
|
373
471
|
}
|
|
374
472
|
|
|
375
473
|
function generateAuthlessAppRoute(info) {
|
|
376
474
|
const { auth, isTypeScript } = info;
|
|
475
|
+
const typeAnnotation = isTypeScript ? ": NextRequest" : "";
|
|
476
|
+
const nextRequestImport = isTypeScript
|
|
477
|
+
? `import { NextRequest, NextResponse } from 'next/server';\n`
|
|
478
|
+
: `import { NextResponse } from 'next/server';\n`;
|
|
377
479
|
|
|
378
480
|
const comment =
|
|
379
481
|
auth.name && !auth.supported
|
|
380
|
-
? `// TODO: We detected ${auth.name} but don't have automatic support yet.
|
|
381
|
-
// Add your own user context below.
|
|
382
|
-
// See: https://docs.botversion.com/auth\n\n`
|
|
482
|
+
? `// TODO: We detected ${auth.name} but don't have automatic support yet.\n// Add your own user context below.\n// See: https://docs.botversion.com/auth\n\n`
|
|
383
483
|
: "";
|
|
384
484
|
|
|
385
|
-
return `${comment}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
485
|
+
return `${comment}${nextRequestImport}
|
|
486
|
+
export async function POST(req${typeAnnotation}) {
|
|
487
|
+
try {
|
|
488
|
+
const body = await req.json();
|
|
489
|
+
|
|
490
|
+
// No auth detected — agent works without user context
|
|
491
|
+
// Add userContext here if needed:
|
|
492
|
+
// const userContext = { userId: '...', email: '...' };
|
|
493
|
+
|
|
494
|
+
const response = await fetch(\`\${process.env.BOTVERSION_PLATFORM_URL || 'https://chatbusiness-two.vercel.app'}/api/chatbot/widget-chat\`, {
|
|
495
|
+
method: 'POST',
|
|
496
|
+
headers: { 'Content-Type': 'application/json' },
|
|
497
|
+
body: JSON.stringify({
|
|
498
|
+
chatbotId: body.chatbotId,
|
|
499
|
+
publicKey: body.publicKey,
|
|
500
|
+
query: body.message,
|
|
501
|
+
previousChats: body.conversationHistory || [],
|
|
502
|
+
pageContext: body.pageContext || {},
|
|
503
|
+
userContext: {},
|
|
504
|
+
}),
|
|
505
|
+
});
|
|
390
506
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
507
|
+
const data = await response.json();
|
|
508
|
+
return NextResponse.json(data);
|
|
509
|
+
} catch (err) {
|
|
510
|
+
console.error('[BotVersion] App Router handler error:', err);
|
|
511
|
+
return NextResponse.json({ error: 'Agent error' }, { status: 500 });
|
|
512
|
+
}
|
|
394
513
|
}
|
|
395
514
|
`;
|
|
396
515
|
}
|
|
397
516
|
|
|
398
|
-
// ───
|
|
517
|
+
// ─── NEXT.JS CONFIG PATCH ─────────────────────────────────────────────────────
|
|
518
|
+
|
|
519
|
+
function generateNextConfigPatch(cwd) {
|
|
520
|
+
const candidates = ["next.config.js", "next.config.mjs", "next.config.ts"];
|
|
521
|
+
|
|
522
|
+
let configPath = null;
|
|
523
|
+
let configContent = null;
|
|
524
|
+
|
|
525
|
+
for (const candidate of candidates) {
|
|
526
|
+
const fullPath = path.join(cwd, candidate);
|
|
527
|
+
if (fs.existsSync(fullPath)) {
|
|
528
|
+
configPath = fullPath;
|
|
529
|
+
configContent = fs.readFileSync(fullPath, "utf8");
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (!configPath) return null;
|
|
535
|
+
|
|
536
|
+
if (configContent.includes("instrumentationHook")) {
|
|
537
|
+
return { path: configPath, alreadyPatched: true };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
let patched = configContent;
|
|
541
|
+
|
|
542
|
+
// Add to existing experimental block
|
|
543
|
+
if (configContent.includes("experimental")) {
|
|
544
|
+
patched = configContent.replace(
|
|
545
|
+
/experimental\s*:\s*\{/,
|
|
546
|
+
"experimental: {\n instrumentationHook: true,",
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
// FIX #7: Handle next.config.mjs style — export default { ... }
|
|
550
|
+
} else if (/export\s+default\s+\{/.test(configContent)) {
|
|
551
|
+
patched = configContent.replace(
|
|
552
|
+
/export\s+default\s+\{/,
|
|
553
|
+
"export default {\n experimental: {\n instrumentationHook: true,\n },",
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
// Handle const nextConfig = { ... } style (next.config.js)
|
|
557
|
+
} else if (/const\s+nextConfig\s*=\s*\{/.test(configContent)) {
|
|
558
|
+
patched = configContent.replace(
|
|
559
|
+
/const\s+nextConfig\s*=\s*\{/,
|
|
560
|
+
"const nextConfig = {\n experimental: {\n instrumentationHook: true,\n },",
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
// Handle module.exports = { ... } style
|
|
564
|
+
} else if (/module\.exports\s*=\s*\{/.test(configContent)) {
|
|
565
|
+
patched = configContent.replace(
|
|
566
|
+
/module\.exports\s*=\s*\{/,
|
|
567
|
+
"module.exports = {\n experimental: {\n instrumentationHook: true,\n },",
|
|
568
|
+
);
|
|
569
|
+
} else {
|
|
570
|
+
// Cannot safely patch — return null so caller prompts manual step
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return { path: configPath, content: patched, alreadyPatched: false };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ─── MANUAL INSTRUCTIONS FOR UNSUPPORTED FRAMEWORKS ──────────────────────────
|
|
399
578
|
|
|
400
579
|
function generateManualInstructions(framework, apiKey) {
|
|
401
580
|
const instructions = {
|
|
@@ -431,49 +610,6 @@ Visit https://docs.botversion.com for manual setup instructions.
|
|
|
431
610
|
);
|
|
432
611
|
}
|
|
433
612
|
|
|
434
|
-
function generateNextConfigPatch(cwd) {
|
|
435
|
-
const fs = require("fs");
|
|
436
|
-
const path = require("path");
|
|
437
|
-
|
|
438
|
-
const candidates = ["next.config.js", "next.config.mjs", "next.config.ts"];
|
|
439
|
-
|
|
440
|
-
let configPath = null;
|
|
441
|
-
let configContent = null;
|
|
442
|
-
|
|
443
|
-
for (const candidate of candidates) {
|
|
444
|
-
const fullPath = path.join(cwd, candidate);
|
|
445
|
-
if (fs.existsSync(fullPath)) {
|
|
446
|
-
configPath = fullPath;
|
|
447
|
-
configContent = fs.readFileSync(fullPath, "utf8");
|
|
448
|
-
break;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (!configPath) return null;
|
|
453
|
-
|
|
454
|
-
// Already has instrumentationHook
|
|
455
|
-
if (configContent.includes("instrumentationHook")) {
|
|
456
|
-
return { path: configPath, alreadyPatched: true };
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Add instrumentationHook: true to experimental block if exists
|
|
460
|
-
if (configContent.includes("experimental")) {
|
|
461
|
-
const patched = configContent.replace(
|
|
462
|
-
/experimental\s*:\s*\{/,
|
|
463
|
-
"experimental: {\n instrumentationHook: true,",
|
|
464
|
-
);
|
|
465
|
-
return { path: configPath, content: patched, alreadyPatched: false };
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Add experimental block before the closing of config object
|
|
469
|
-
const patched = configContent.replace(
|
|
470
|
-
/const nextConfig\s*=\s*\{/,
|
|
471
|
-
"const nextConfig = {\n experimental: {\n instrumentationHook: true,\n },",
|
|
472
|
-
);
|
|
473
|
-
|
|
474
|
-
return { path: configPath, content: patched, alreadyPatched: false };
|
|
475
|
-
}
|
|
476
|
-
|
|
477
613
|
module.exports = {
|
|
478
614
|
generateExpressInit,
|
|
479
615
|
generateInstrumentationFile,
|
package/cli/writer.js
CHANGED
|
@@ -17,7 +17,7 @@ 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
|
}
|
|
@@ -58,10 +58,10 @@ function injectBeforeListen(filePath, codeToInject) {
|
|
|
58
58
|
const injectedLines = ["", ...codeToInject.split("\n"), ""];
|
|
59
59
|
|
|
60
60
|
const newContent = [...before, ...injectedLines, ...after].join("\n");
|
|
61
|
-
backupFile(filePath);
|
|
61
|
+
const backup = backupFile(filePath);
|
|
62
62
|
fs.writeFileSync(filePath, newContent, "utf8");
|
|
63
63
|
|
|
64
|
-
return { success: true, lineNumber: listenLineIndex + 1 };
|
|
64
|
+
return { success: true, lineNumber: listenLineIndex + 1, backup };
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// ─── APPEND CODE TO END OF FILE ──────────────────────────────────────────────
|
|
@@ -94,6 +94,84 @@ function createFile(filePath, content, force) {
|
|
|
94
94
|
return { success: true, path: filePath };
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
function injectBeforeExport(filePath, codeToInject) {
|
|
98
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
99
|
+
const lines = content.split("\n");
|
|
100
|
+
|
|
101
|
+
if (content.includes("botversion-sdk") || content.includes("BotVersion")) {
|
|
102
|
+
return { success: false, reason: "already_exists" };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let insertIndex = -1;
|
|
106
|
+
|
|
107
|
+
// Find module.exports = app
|
|
108
|
+
let exportsLine = -1;
|
|
109
|
+
for (let i = 0; i < lines.length; i++) {
|
|
110
|
+
if (/module\.exports\s*=\s*app/.test(lines[i])) {
|
|
111
|
+
exportsLine = i;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (exportsLine !== -1) {
|
|
117
|
+
// Walk backwards from module.exports to skip error handler lines
|
|
118
|
+
// Skip lines that are: blank, closing braces, or known error middleware
|
|
119
|
+
let i = exportsLine - 1;
|
|
120
|
+
while (i >= 0) {
|
|
121
|
+
const line = lines[i].trim();
|
|
122
|
+
if (
|
|
123
|
+
line === "" ||
|
|
124
|
+
line === "})" ||
|
|
125
|
+
line === "});" ||
|
|
126
|
+
line === "}," ||
|
|
127
|
+
line === "}" ||
|
|
128
|
+
line === ");" ||
|
|
129
|
+
line === "next();" ||
|
|
130
|
+
/app\.use\s*\(\s*errorHandler/.test(line) ||
|
|
131
|
+
/app\.use\s*\(\s*errorConverter/.test(line) ||
|
|
132
|
+
/app\.use\s*\(\s*\(req,\s*res,\s*next\)/.test(line) ||
|
|
133
|
+
/next\(new/.test(line) ||
|
|
134
|
+
/NOT_FOUND/.test(line) ||
|
|
135
|
+
/Not found/i.test(line) ||
|
|
136
|
+
/\/\/ (handle|convert|send back) error/.test(lines[i]) ||
|
|
137
|
+
/\/\/ send back a 404/.test(lines[i])
|
|
138
|
+
) {
|
|
139
|
+
i--;
|
|
140
|
+
} else {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
insertIndex = i + 1;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Fallback: before app.listen()
|
|
148
|
+
if (insertIndex === -1) {
|
|
149
|
+
for (let i = 0; i < lines.length; i++) {
|
|
150
|
+
if (/app\.listen\s*\(/.test(lines[i])) {
|
|
151
|
+
insertIndex = i;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Final fallback: append to end of file
|
|
158
|
+
if (insertIndex === -1) {
|
|
159
|
+
const backup = backupFile(filePath);
|
|
160
|
+
const newContent = content.trimEnd() + "\n\n" + codeToInject + "\n";
|
|
161
|
+
fs.writeFileSync(filePath, newContent, "utf8");
|
|
162
|
+
return { success: true, backup };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const before = lines.slice(0, insertIndex);
|
|
166
|
+
const after = lines.slice(insertIndex);
|
|
167
|
+
const injectedLines = ["", ...codeToInject.split("\n"), ""];
|
|
168
|
+
const newContent = [...before, ...injectedLines, ...after].join("\n");
|
|
169
|
+
|
|
170
|
+
const backup = backupFile(filePath);
|
|
171
|
+
fs.writeFileSync(filePath, newContent, "utf8");
|
|
172
|
+
return { success: true, backup };
|
|
173
|
+
}
|
|
174
|
+
|
|
97
175
|
// ─── MERGE INTO EXISTING MIDDLEWARE (Next.js) ────────────────────────────────
|
|
98
176
|
|
|
99
177
|
function mergeIntoMiddleware(middlewarePath, authName) {
|
|
@@ -168,4 +246,5 @@ module.exports = {
|
|
|
168
246
|
createFile,
|
|
169
247
|
mergeIntoMiddleware,
|
|
170
248
|
writeSummary,
|
|
249
|
+
injectBeforeExport,
|
|
171
250
|
};
|
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,8 @@ var BotVersion = {
|
|
|
61
61
|
|
|
62
62
|
this._client = new BotVersionClient({
|
|
63
63
|
apiKey: options.apiKey,
|
|
64
|
-
platformUrl:
|
|
64
|
+
platformUrl:
|
|
65
|
+
options.platformUrl || "https://chatbusiness-two.vercel.app/",
|
|
65
66
|
debug: options.debug || false,
|
|
66
67
|
timeout: options.timeout || 30000,
|
|
67
68
|
});
|
|
@@ -205,9 +206,9 @@ var BotVersion = {
|
|
|
205
206
|
.registerEndpoints(endpoints)
|
|
206
207
|
.then(function () {
|
|
207
208
|
console.log(
|
|
208
|
-
"[BotVersion SDK] ✅
|
|
209
|
+
"[BotVersion SDK] ✅ Endpoints queued —",
|
|
209
210
|
endpoints.length,
|
|
210
|
-
"endpoints
|
|
211
|
+
"endpoints will be sent shortly",
|
|
211
212
|
);
|
|
212
213
|
})
|
|
213
214
|
.catch(function (err) {
|
|
@@ -571,6 +572,14 @@ BotVersion.nextHandler = function (options) {
|
|
|
571
572
|
|
|
572
573
|
BotVersion.nextHandler = BotVersion.nextHandler.bind(BotVersion);
|
|
573
574
|
|
|
575
|
+
BotVersion.appRouterHandler = function () {
|
|
576
|
+
throw new Error(
|
|
577
|
+
"[BotVersion SDK] appRouterHandler is not supported. " +
|
|
578
|
+
"Please run: npx botversion-sdk init --key YOUR_KEY --force " +
|
|
579
|
+
"to regenerate the correct route file.",
|
|
580
|
+
);
|
|
581
|
+
};
|
|
582
|
+
|
|
574
583
|
module.exports = BotVersion;
|
|
575
584
|
module.exports.default = BotVersion;
|
|
576
585
|
module.exports.init = BotVersion.init;
|
|
@@ -578,6 +587,7 @@ module.exports.getEndpoints = BotVersion.getEndpoints;
|
|
|
578
587
|
module.exports.registerEndpoint = BotVersion.registerEndpoint;
|
|
579
588
|
module.exports.chat = BotVersion.chat;
|
|
580
589
|
module.exports.nextHandler = BotVersion.nextHandler;
|
|
590
|
+
module.exports.appRouterHandler = BotVersion.appRouterHandler;
|
|
581
591
|
|
|
582
592
|
// ── Framework detection ──────────────────────────────────────────────────────
|
|
583
593
|
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 = {
|