botversion-sdk 1.1.6 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/writer.js DELETED
@@ -1,511 +0,0 @@
1
- // botversion-sdk/cli/writer.js
2
-
3
- "use strict";
4
-
5
- const fs = require("fs");
6
- const path = require("path");
7
-
8
- // ─── SAFE FILE WRITE ─────────────────────────────────────────────────────────
9
-
10
- function writeFile(filePath, content) {
11
- const dir = path.dirname(filePath);
12
- fs.mkdirSync(dir, { recursive: true });
13
- fs.writeFileSync(filePath, content, "utf8");
14
- }
15
-
16
- // ─── BACKUP A FILE BEFORE MODIFYING ─────────────────────────────────────────
17
-
18
- function backupFile(filePath) {
19
- if (!fs.existsSync(filePath)) return null;
20
- const backupPath = filePath + ".backup-before-botversion";
21
- fs.copyFileSync(filePath, backupPath);
22
- return backupPath;
23
- }
24
-
25
- // ─── INJECT CODE BEFORE app.listen() ────────────────────────────────────────
26
-
27
- function injectBeforeListen(filePath, codeToInject, appVarName) {
28
- appVarName = appVarName || "app";
29
- const content = fs.readFileSync(filePath, "utf8");
30
- const lines = content.split("\n");
31
- const listenRegex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
32
-
33
- // Check if BotVersion already exists in file
34
- if (content.includes("botversion-sdk") || content.includes("BotVersion")) {
35
- return { success: false, reason: "already_exists" };
36
- }
37
-
38
- let listenLineIndex = -1;
39
- for (let i = 0; i < lines.length; i++) {
40
- if (listenRegex.test(lines[i])) {
41
- listenLineIndex = i;
42
- break;
43
- }
44
- }
45
-
46
- if (listenLineIndex === -1) {
47
- return {
48
- success: false,
49
- reason: "no_listen",
50
- suggestion: "append",
51
- };
52
- }
53
-
54
- // ── NEW: Check if app.listen() is inside if (require.main === module) ──
55
- // If so, inject before that if block instead of inside it
56
- let insertIndex = listenLineIndex;
57
-
58
- for (let i = listenLineIndex; i >= 0; i--) {
59
- const trimmed = lines[i].trim();
60
-
61
- // Found if (require.main === module) block
62
- if (
63
- /if\s*\(\s*require\.main\s*===?\s*module\s*\)/.test(trimmed) ||
64
- /if\s*\(\s*module\s*===?\s*require\.main\s*\)/.test(trimmed)
65
- ) {
66
- insertIndex = i;
67
- // Keep walking back to check for 404/error handlers before this
68
- continue;
69
- }
70
-
71
- // Found a 404 handler — inject before this instead
72
- if (
73
- /app\.use\s*\(\s*\(req,\s*res\)\s*=>/.test(trimmed) ||
74
- /app\.use\s*\(\s*function\s*\(req,\s*res\)/.test(trimmed)
75
- ) {
76
- insertIndex = i;
77
- continue;
78
- }
79
-
80
- // Found a global error handler — inject before this instead
81
- if (
82
- /app\.use\s*\(\s*\(err,\s*req,\s*res,\s*next\)/.test(trimmed) ||
83
- /app\.use\s*\(\s*function\s*\(err,\s*req,\s*res,\s*next\)/.test(trimmed)
84
- ) {
85
- insertIndex = i;
86
- continue;
87
- }
88
-
89
- // Stop walking back if we hit a top-level non-empty statement
90
- if (
91
- i < listenLineIndex &&
92
- trimmed !== "" &&
93
- trimmed !== "}" &&
94
- trimmed !== "})" &&
95
- trimmed !== "});" &&
96
- lines[i].match(/^[^\s]/)
97
- ) {
98
- break;
99
- }
100
- }
101
-
102
- // Insert the code block at the correct position
103
- const before = lines.slice(0, insertIndex);
104
- const after = lines.slice(insertIndex);
105
- const injectedLines = ["", ...codeToInject.split("\n"), ""];
106
- const newContent = [...before, ...injectedLines, ...after].join("\n");
107
-
108
- const backup = backupFile(filePath);
109
- fs.writeFileSync(filePath, newContent, "utf8");
110
-
111
- return { success: true, lineNumber: insertIndex + 1, backup };
112
- }
113
-
114
- // ─── APPEND CODE TO END OF FILE ──────────────────────────────────────────────
115
-
116
- function appendToFile(filePath, codeToAppend) {
117
- const content = fs.readFileSync(filePath, "utf8");
118
-
119
- if (content.includes("botversion-sdk") || content.includes("BotVersion")) {
120
- return { success: false, reason: "already_exists" };
121
- }
122
-
123
- backupFile(filePath);
124
- const newContent = content.trimEnd() + "\n\n" + codeToAppend + "\n";
125
- fs.writeFileSync(filePath, newContent, "utf8");
126
- return { success: true };
127
- }
128
-
129
- // ─── CREATE A NEW FILE (won't overwrite unless forced) ───────────────────────
130
-
131
- function createFile(filePath, content, force) {
132
- if (fs.existsSync(filePath) && !force) {
133
- return { success: false, reason: "already_exists", path: filePath };
134
- }
135
-
136
- if (fs.existsSync(filePath) && force) {
137
- backupFile(filePath);
138
- }
139
-
140
- writeFile(filePath, content);
141
- return { success: true, path: filePath };
142
- }
143
-
144
- function injectBeforeExport(filePath, codeToInject, appVarName) {
145
- appVarName = appVarName || "app";
146
- const content = fs.readFileSync(filePath, "utf8");
147
- const lines = content.split("\n");
148
-
149
- if (content.includes("botversion-sdk") || content.includes("BotVersion")) {
150
- return { success: false, reason: "already_exists" };
151
- }
152
-
153
- let insertIndex = -1;
154
-
155
- // Find module.exports = app
156
- let exportsLine = -1;
157
- for (let i = 0; i < lines.length; i++) {
158
- if (/module\.exports\s*=\s*app/.test(lines[i])) {
159
- exportsLine = i;
160
- break;
161
- }
162
- }
163
-
164
- if (exportsLine !== -1) {
165
- // Walk backwards from module.exports to skip error handler lines
166
- // Skip lines that are: blank, closing braces, or known error middleware
167
- let i = exportsLine - 1;
168
- while (i >= 0) {
169
- const line = lines[i].trim();
170
- if (
171
- line === "" ||
172
- line === "})" ||
173
- line === "});" ||
174
- line === "}," ||
175
- line === "}" ||
176
- line === ");" ||
177
- line === "next();" ||
178
- /app\.use\s*\(\s*errorHandler/.test(line) ||
179
- /app\.use\s*\(\s*errorConverter/.test(line) ||
180
- /app\.use\s*\(\s*\(req,\s*res,\s*next\)/.test(line) ||
181
- /next\(new/.test(line) ||
182
- /NOT_FOUND/.test(line) ||
183
- /Not found/i.test(line) ||
184
- /\/\/ (handle|convert|send back) error/.test(lines[i]) ||
185
- /\/\/ send back a 404/.test(lines[i])
186
- ) {
187
- i--;
188
- } else {
189
- break;
190
- }
191
- }
192
- insertIndex = i + 1;
193
- }
194
-
195
- // Fallback: before app.listen()
196
- if (insertIndex === -1) {
197
- for (let i = 0; i < lines.length; i++) {
198
- const listenRegex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
199
- if (listenRegex.test(lines[i])) {
200
- insertIndex = i;
201
- break;
202
- }
203
- }
204
- }
205
-
206
- // Final fallback: append to end of file
207
- if (insertIndex === -1) {
208
- const backup = backupFile(filePath);
209
- const newContent = content.trimEnd() + "\n\n" + codeToInject + "\n";
210
- fs.writeFileSync(filePath, newContent, "utf8");
211
- return { success: true, backup };
212
- }
213
-
214
- const before = lines.slice(0, insertIndex);
215
- const after = lines.slice(insertIndex);
216
- const injectedLines = ["", ...codeToInject.split("\n"), ""];
217
- const newContent = [...before, ...injectedLines, ...after].join("\n");
218
-
219
- const backup = backupFile(filePath);
220
- fs.writeFileSync(filePath, newContent, "utf8");
221
- return { success: true, backup };
222
- }
223
-
224
- // ─── WRITE SUMMARY OF ALL CHANGES ────────────────────────────────────────────
225
-
226
- function writeSummary(changes) {
227
- const lines = [
228
- "",
229
- "┌─────────────────────────────────────────────┐",
230
- "│ BotVersion Setup Complete! │",
231
- "└─────────────────────────────────────────────┘",
232
- "",
233
- ];
234
-
235
- if (changes.modified && changes.modified.length > 0) {
236
- lines.push(" Modified files:");
237
- changes.modified.forEach((f) => lines.push(` [Modified] ${f}`));
238
- lines.push("");
239
- }
240
-
241
- if (changes.created && changes.created.length > 0) {
242
- lines.push(" Created files:");
243
- changes.created.forEach((f) => lines.push(` [Created] ${f}`));
244
- lines.push("");
245
- }
246
-
247
- if (changes.backups && changes.backups.length > 0) {
248
- lines.push(" Backups created:");
249
- changes.backups.forEach((f) => lines.push(` [Backup] ${f}`));
250
- lines.push("");
251
- }
252
-
253
- if (changes.manual && changes.manual.length > 0) {
254
- lines.push(" Manual steps needed:");
255
- changes.manual.forEach((m) => lines.push(` [Manual] ${m}`));
256
- lines.push("");
257
- }
258
-
259
- lines.push(" Next: Restart your server and test the chat widget.");
260
- lines.push(" Docs: https://botversion.com/docs");
261
- lines.push("");
262
-
263
- return lines.join("\n");
264
- }
265
-
266
- // ─── INJECT SCRIPT TAG INTO FRONTEND FILE ────────────────────────────────────
267
-
268
- function injectScriptTag(filePath, fileType, scriptTag, force) {
269
- if (!fs.existsSync(filePath)) {
270
- return { success: false, reason: "file_not_found" };
271
- }
272
-
273
- const content = fs.readFileSync(filePath, "utf8");
274
-
275
- // Already exists check
276
- if (content.includes("botversion-loader")) {
277
- if (!force) return { success: false, reason: "already_exists" };
278
- }
279
-
280
- const backup = backupFile(filePath);
281
-
282
- // ── HTML file — inject before </body> ──────────────────────────────────
283
- if (fileType === "html") {
284
- if (!content.includes("</body>")) {
285
- return { success: false, reason: "no_body_tag" };
286
- }
287
-
288
- const newContent = content.replace("</body>", ` ${scriptTag}\n</body>`);
289
- fs.writeFileSync(filePath, newContent, "utf8");
290
- return { success: true, backup };
291
- }
292
-
293
- // ── Next.js _app.js — inject Script component ──────────────────────────
294
- if (fileType === "next") {
295
- const fileName = path.basename(filePath);
296
-
297
- // pages/_app.js
298
- if (fileName.startsWith("_app")) {
299
- return injectIntoNextApp(filePath, content, scriptTag, backup);
300
- }
301
-
302
- // app/layout.js
303
- if (fileName.startsWith("layout")) {
304
- return injectIntoNextLayout(filePath, content, scriptTag, backup);
305
- }
306
- }
307
-
308
- return { success: false, reason: "unsupported_file_type" };
309
- }
310
-
311
- // ─── INJECT INTO NEXT.JS _app.js ─────────────────────────────────────────────
312
-
313
- function injectIntoNextApp(filePath, content, scriptTag, backup) {
314
- let newContent = content;
315
-
316
- if (!content.includes("next/script")) {
317
- newContent = newContent.replace(
318
- /^(import .+)/m,
319
- `import Script from 'next/script';\n$1`,
320
- );
321
- }
322
-
323
- const scriptComponent = `
324
- <Script
325
- id="botversion-loader"
326
- src="${extractAttr(scriptTag, "src")}"
327
- data-api-url="${extractAttr(scriptTag, "data-api-url")}"
328
- data-project-id="${extractAttr(scriptTag, "data-project-id")}"
329
- data-public-key="${extractAttr(scriptTag, "data-public-key")}"
330
- strategy="afterInteractive"
331
- />`;
332
-
333
- const lines = newContent.split("\n");
334
-
335
- // Find ALL return statements and pick the one whose root JSX
336
- // is a multi-child wrapper (not a simple single-element return)
337
- // Strategy: find the return ( that is followed by the most lines
338
- // before its closing ) — that's the main render return
339
-
340
- let bestReturnIndex = -1;
341
- let bestRootJsxIndex = -1;
342
- let bestLineCount = 0;
343
-
344
- for (let i = 0; i < lines.length; i++) {
345
- if (!/^\s*return\s*\(/.test(lines[i])) continue;
346
-
347
- // Find the root JSX tag after this return
348
- let rootJsx = -1;
349
- for (let j = i + 1; j < lines.length; j++) {
350
- const trimmed = lines[j].trim();
351
- if (!trimmed) continue;
352
- if (trimmed.startsWith("<")) {
353
- rootJsx = j;
354
- break;
355
- }
356
- break; // non-empty, non-JSX line means this isn't a JSX return
357
- }
358
-
359
- if (rootJsx === -1) continue;
360
-
361
- // Find the closing ) of this return block
362
- let depth = 1;
363
- let closingLine = -1;
364
- for (let j = rootJsx; j < lines.length; j++) {
365
- for (const ch of lines[j]) {
366
- if (ch === "(") depth++;
367
- if (ch === ")") depth--;
368
- }
369
- if (depth === 0) {
370
- closingLine = j;
371
- break;
372
- }
373
- }
374
-
375
- const lineCount = closingLine - i;
376
- if (lineCount > bestLineCount) {
377
- bestLineCount = lineCount;
378
- bestReturnIndex = i;
379
- bestRootJsxIndex = rootJsx;
380
- }
381
- }
382
-
383
- if (bestRootJsxIndex !== -1) {
384
- lines.splice(bestRootJsxIndex + 1, 0, scriptComponent);
385
- newContent = lines.join("\n");
386
- } else {
387
- // Final fallback
388
- newContent = newContent.replace(
389
- /([ \t]*<\/div>\s*\n\s*\))/,
390
- `${scriptComponent}\n$1`,
391
- );
392
- }
393
-
394
- fs.writeFileSync(filePath, newContent, "utf8");
395
- return { success: true, backup };
396
- }
397
-
398
- // ─── INJECT INTO NEXT.JS layout.js ───────────────────────────────────────────
399
-
400
- function injectIntoNextLayout(filePath, content, scriptTag, backup) {
401
- let newContent = content;
402
-
403
- if (!content.includes("next/script")) {
404
- newContent = newContent.replace(
405
- /^(import .+)/m,
406
- `import Script from 'next/script';\n$1`,
407
- );
408
- }
409
-
410
- const scriptComponent = `
411
- <Script
412
- id="botversion-loader"
413
- src="${extractAttr(scriptTag, "src")}"
414
- data-api-url="${extractAttr(scriptTag, "data-api-url")}"
415
- data-project-id="${extractAttr(scriptTag, "data-project-id")}"
416
- data-public-key="${extractAttr(scriptTag, "data-public-key")}"
417
- strategy="afterInteractive"
418
- />`;
419
-
420
- // Inject before </body> in layout
421
- if (content.includes("</body>")) {
422
- newContent = newContent.replace(
423
- "</body>",
424
- `${scriptComponent}\n </body>`,
425
- );
426
- } else {
427
- // Fallback — before last closing tag
428
- newContent = newContent.replace(
429
- /(<\/\w+>\s*\)[\s;]*$)/m,
430
- `${scriptComponent}\n $1`,
431
- );
432
- }
433
-
434
- fs.writeFileSync(filePath, newContent, "utf8");
435
- return { success: true, backup };
436
- }
437
-
438
- function injectAtTop(filePath, codeToInject) {
439
- const content = fs.readFileSync(filePath, "utf8");
440
-
441
- if (content.includes("dotenv")) {
442
- return { success: false, reason: "already_exists" };
443
- }
444
-
445
- const newContent = codeToInject + "\n" + content;
446
- backupFile(filePath);
447
- fs.writeFileSync(filePath, newContent, "utf8");
448
- return { success: true };
449
- }
450
-
451
- // ─── HELPER: extract attribute value from script tag string ──────────────────
452
-
453
- function extractAttr(scriptTag, attr) {
454
- const match = scriptTag.match(new RegExp(`${attr}="([^"]+)"`));
455
- return match ? match[1] : "";
456
- }
457
-
458
- // ─── WRITE API KEY TO .ENV FILE ───────────────────────────────────────────────
459
-
460
- function writeEnvKey(cwd, key, value, framework) {
461
- const envCandidates =
462
- framework === "next"
463
- ? [".env.local", ".env", ".env.development", ".env.development.local"]
464
- : [".env", ".env.development", ".env.local"];
465
-
466
- let envPath = null;
467
-
468
- // Use existing .env file if found
469
- for (const candidate of envCandidates) {
470
- const fullPath = path.join(cwd, candidate);
471
- if (fs.existsSync(fullPath)) {
472
- envPath = fullPath;
473
- break;
474
- }
475
- }
476
-
477
- // No .env file found — create .env
478
- if (!envPath) {
479
- envPath = path.join(cwd, ".env");
480
- fs.writeFileSync(envPath, "", "utf8");
481
- }
482
-
483
- const content = fs.readFileSync(envPath, "utf8");
484
-
485
- // Key already exists — skip
486
- if (content.includes(key)) {
487
- return { success: false, reason: "already_exists", path: envPath };
488
- }
489
-
490
- // Append key to file
491
- const newLine =
492
- content === "" || content.endsWith("\n")
493
- ? `${key}=${value}\n`
494
- : `\n${key}=${value}\n`;
495
-
496
- fs.appendFileSync(envPath, newLine, "utf8");
497
- return { success: true, path: envPath };
498
- }
499
-
500
- module.exports = {
501
- writeFile,
502
- backupFile,
503
- injectBeforeListen,
504
- appendToFile,
505
- createFile,
506
- writeSummary,
507
- injectBeforeExport,
508
- injectScriptTag,
509
- injectAtTop,
510
- writeEnvKey,
511
- };