botversion-sdk 1.0.4 → 1.0.6
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 +186 -140
- package/cli/detector.js +360 -173
- package/cli/generator.js +21 -479
- package/cli/prompts.js +40 -42
- package/cli/writer.js +119 -43
- package/client.js +33 -69
- package/index.js +54 -515
- package/interceptor.js +57 -132
- package/package.json +3 -1
- package/scanner.js +733 -28
package/bin/init.js
CHANGED
|
@@ -102,7 +102,7 @@ async function main() {
|
|
|
102
102
|
if (!args.key) {
|
|
103
103
|
error("API key is required.");
|
|
104
104
|
log(`\n Usage: npx botversion-sdk init --key YOUR_WORKSPACE_KEY\n`);
|
|
105
|
-
log(` Get your key from: https://
|
|
105
|
+
log(` Get your key from: https://botversion.com/settings\n`);
|
|
106
106
|
process.exit(1);
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -113,10 +113,7 @@ async function main() {
|
|
|
113
113
|
step("Fetching project info from platform...");
|
|
114
114
|
let projectInfo;
|
|
115
115
|
try {
|
|
116
|
-
projectInfo = await fetchProjectInfo(
|
|
117
|
-
args.key,
|
|
118
|
-
"https://chatbusiness-two.vercel.app",
|
|
119
|
-
);
|
|
116
|
+
projectInfo = await fetchProjectInfo(args.key, "https://botversion.com");
|
|
120
117
|
success(`Project found — ID: ${projectInfo.projectId}`);
|
|
121
118
|
} catch (err) {
|
|
122
119
|
error(err.message);
|
|
@@ -126,7 +123,6 @@ async function main() {
|
|
|
126
123
|
// ── Detect environment ────────────────────────────────────────────────────
|
|
127
124
|
step("Scanning your project...");
|
|
128
125
|
|
|
129
|
-
// Handle monorepo
|
|
130
126
|
const monorepoInfo = detector.detectMonorepo(cwd);
|
|
131
127
|
if (monorepoInfo.isMonorepo) {
|
|
132
128
|
warn(
|
|
@@ -134,7 +130,6 @@ async function main() {
|
|
|
134
130
|
);
|
|
135
131
|
}
|
|
136
132
|
|
|
137
|
-
// Always detect from root so frontend/backend split works correctly
|
|
138
133
|
const detected = detector.detect(cwd);
|
|
139
134
|
|
|
140
135
|
// ── Check if already initialized ─────────────────────────────────────────
|
|
@@ -171,39 +166,88 @@ async function main() {
|
|
|
171
166
|
`Module system: ${detected.moduleSystem === "esm" ? "ES Modules" : "CommonJS"}`,
|
|
172
167
|
);
|
|
173
168
|
info(`Language: ${detected.isTypeScript ? "TypeScript" : "JavaScript"}`);
|
|
169
|
+
info(`Package manager: ${detected.packageManager}`);
|
|
174
170
|
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (!auth.name) {
|
|
181
|
-
warn("No auth library detected automatically.");
|
|
182
|
-
auth = await prompts.promptAuthLibrary();
|
|
183
|
-
detected.auth = auth;
|
|
184
|
-
} else if (!auth.supported) {
|
|
185
|
-
warn(`Detected auth: ${auth.name} (not yet supported for auto-setup)`);
|
|
186
|
-
warn("Will set up without user context — you can add it manually later.");
|
|
187
|
-
const proceed = await prompts.confirm("Continue without auth?", true);
|
|
188
|
-
if (!proceed) process.exit(0);
|
|
189
|
-
auth = { name: auth.name, supported: false };
|
|
190
|
-
detected.auth = auth;
|
|
191
|
-
} else {
|
|
192
|
-
const versionLabel = auth.version ? ` (${auth.version})` : "";
|
|
193
|
-
success(`Auth: ${auth.name}${versionLabel}`);
|
|
194
|
-
}
|
|
171
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
172
|
+
// FRAMEWORK: NEXT.JS
|
|
173
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
174
|
+
if (detected.framework.name === "next") {
|
|
175
|
+
await setupNextJs(detected, args, changes, projectInfo);
|
|
195
176
|
|
|
196
|
-
|
|
197
|
-
|
|
177
|
+
// ── Also check for a separate Express backend folder ──────────────────
|
|
178
|
+
step("Checking for separate backend...");
|
|
179
|
+
const backendDirs = ["backend", "server", "api", "services"];
|
|
180
|
+
let expressBackendFound = false;
|
|
181
|
+
|
|
182
|
+
for (const dir of backendDirs) {
|
|
183
|
+
const backendPath = path.join(cwd, dir);
|
|
184
|
+
if (!fs.existsSync(backendPath)) continue;
|
|
185
|
+
|
|
186
|
+
// Check if this folder has its own package.json with express
|
|
187
|
+
const backendPkg = detector.readPackageJson(backendPath);
|
|
188
|
+
const backendDeps = {
|
|
189
|
+
...((backendPkg && backendPkg.dependencies) || {}),
|
|
190
|
+
...((backendPkg && backendPkg.devDependencies) || {}),
|
|
191
|
+
};
|
|
192
|
+
const hasExpress = !!backendDeps["express"];
|
|
193
|
+
|
|
194
|
+
// Or check if any JS file in this folder has express routes
|
|
195
|
+
const hasExpressFiles = fs.readdirSync(backendPath).some((file) => {
|
|
196
|
+
if (!/\.(js|ts)$/.test(file)) return false;
|
|
197
|
+
try {
|
|
198
|
+
const content = fs.readFileSync(path.join(backendPath, file), "utf8");
|
|
199
|
+
return (
|
|
200
|
+
content.includes("express()") ||
|
|
201
|
+
content.includes("express(") || // factory pattern
|
|
202
|
+
/class\s+\w+\s+extends\s+express/.test(content) || // subclass
|
|
203
|
+
/(?:create|make|build|setup)App\s*\(/.test(content) || // factory fn
|
|
204
|
+
/(?:app|router|server)\.(get|post|put|delete|patch|all)\s*\(\s*['"`]\//.test(
|
|
205
|
+
content,
|
|
206
|
+
)
|
|
207
|
+
);
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (hasExpress || hasExpressFiles) {
|
|
214
|
+
expressBackendFound = true;
|
|
215
|
+
warn(`Found Express backend in "${dir}/" folder.`);
|
|
216
|
+
|
|
217
|
+
// Detect the backend separately
|
|
218
|
+
const backendDetected = detector.detect(backendPath);
|
|
219
|
+
|
|
220
|
+
if (backendDetected.entryPoint) {
|
|
221
|
+
success(
|
|
222
|
+
`Backend entry point: ${path.relative(cwd, backendDetected.entryPoint)}`,
|
|
223
|
+
);
|
|
224
|
+
await setupExpress(backendDetected, args, changes, projectInfo);
|
|
225
|
+
} else {
|
|
226
|
+
warn(`Could not find entry point in "${dir}/" automatically.`);
|
|
227
|
+
const manualPath = await prompts.promptEntryPoint();
|
|
228
|
+
const resolvedPath = path.resolve(backendPath, manualPath);
|
|
229
|
+
if (fs.existsSync(resolvedPath)) {
|
|
230
|
+
backendDetected.entryPoint = resolvedPath;
|
|
231
|
+
await setupExpress(backendDetected, args, changes, projectInfo);
|
|
232
|
+
} else {
|
|
233
|
+
error(`File not found: ${resolvedPath}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
break; // only set up one backend
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!expressBackendFound) {
|
|
241
|
+
info("No separate Express backend found — skipping.");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
198
244
|
|
|
199
245
|
// ─────────────────────────────────────────────────────────────────────────
|
|
200
|
-
// FRAMEWORK: EXPRESS
|
|
246
|
+
// FRAMEWORK: EXPRESS ONLY
|
|
201
247
|
// ─────────────────────────────────────────────────────────────────────────
|
|
202
|
-
if (detected.framework.name === "express") {
|
|
248
|
+
else if (detected.framework.name === "express") {
|
|
203
249
|
await setupExpress(detected, args, changes, projectInfo);
|
|
204
250
|
|
|
205
|
-
// ── Inject script tag into frontend (Express only) ──────────────────
|
|
206
|
-
// Next.js handles its own script tag injection inside setupNextJs
|
|
207
251
|
if (detected.frontendMainFile && projectInfo) {
|
|
208
252
|
const scriptTag = generator.generateScriptTag(projectInfo);
|
|
209
253
|
const result = writer.injectScriptTag(
|
|
@@ -243,45 +287,6 @@ async function main() {
|
|
|
243
287
|
}
|
|
244
288
|
}
|
|
245
289
|
|
|
246
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
247
|
-
// FRAMEWORK: NEXT.JS
|
|
248
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
249
|
-
else if (detected.framework.name === "next") {
|
|
250
|
-
await setupNextJs(detected, args, changes, projectInfo);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// ── Write API key to .env / .env.local ────────────────────────────────────
|
|
254
|
-
const envFileName =
|
|
255
|
-
detected.framework.name === "next" ? ".env.local" : ".env";
|
|
256
|
-
const envPath = path.join(detected.cwd, envFileName);
|
|
257
|
-
const envLine = `BOTVERSION_API_KEY=${args.key}`;
|
|
258
|
-
const envContent = fs.existsSync(envPath)
|
|
259
|
-
? fs.readFileSync(envPath, "utf8")
|
|
260
|
-
: "";
|
|
261
|
-
|
|
262
|
-
if (!envContent.includes("BOTVERSION_API_KEY")) {
|
|
263
|
-
const writeEnv = await prompts.confirm(
|
|
264
|
-
`Add BOTVERSION_API_KEY to ${envFileName}?`,
|
|
265
|
-
true,
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
if (writeEnv) {
|
|
269
|
-
const envAddition = "\n\n# BotVersion API key\n" + envLine + "\n";
|
|
270
|
-
fs.writeFileSync(envPath, envContent.trimEnd() + envAddition, "utf8");
|
|
271
|
-
success(`Added BOTVERSION_API_KEY to ${envFileName}`);
|
|
272
|
-
changes.modified.push(envFileName);
|
|
273
|
-
} else {
|
|
274
|
-
warn(`Skipped — add this manually to your ${envFileName}:`);
|
|
275
|
-
log(`\n # BotVersion API key`);
|
|
276
|
-
log(` BOTVERSION_API_KEY=${args.key}\n`);
|
|
277
|
-
changes.manual.push(
|
|
278
|
-
`Add to your ${envFileName}:\n\n # BotVersion API key\n BOTVERSION_API_KEY=${args.key}`,
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
} else {
|
|
282
|
-
info(`BOTVERSION_API_KEY already exists in ${envFileName} — skipping.`);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
290
|
// ── Print summary ─────────────────────────────────────────────────────────
|
|
286
291
|
log(writer.writeSummary(changes));
|
|
287
292
|
}
|
|
@@ -291,7 +296,6 @@ async function main() {
|
|
|
291
296
|
async function setupExpress(detected, args, changes, projectInfo) {
|
|
292
297
|
step("Setting up Express...");
|
|
293
298
|
|
|
294
|
-
// Find entry point
|
|
295
299
|
let entryPoint = detected.entryPoint;
|
|
296
300
|
|
|
297
301
|
if (!entryPoint || !fs.existsSync(entryPoint)) {
|
|
@@ -307,7 +311,80 @@ async function setupExpress(detected, args, changes, projectInfo) {
|
|
|
307
311
|
|
|
308
312
|
success(`Entry point: ${path.relative(detected.cwd, entryPoint)}`);
|
|
309
313
|
|
|
310
|
-
//
|
|
314
|
+
// ── Write API key to .env ──────────────────────────────────────────────
|
|
315
|
+
const shouldWriteEnv = await prompts.confirm(
|
|
316
|
+
" Add BOTVERSION_API_KEY to your .env file?",
|
|
317
|
+
true,
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
if (shouldWriteEnv) {
|
|
321
|
+
const envResult = writer.writeEnvKey(
|
|
322
|
+
detected.cwd,
|
|
323
|
+
"BOTVERSION_API_KEY",
|
|
324
|
+
args.key,
|
|
325
|
+
"express",
|
|
326
|
+
);
|
|
327
|
+
if (envResult.success) {
|
|
328
|
+
success(
|
|
329
|
+
`Added BOTVERSION_API_KEY to ${path.relative(detected.cwd, envResult.path)}`,
|
|
330
|
+
);
|
|
331
|
+
changes.modified.push(path.relative(detected.cwd, envResult.path));
|
|
332
|
+
} else if (envResult.reason === "already_exists") {
|
|
333
|
+
info("BOTVERSION_API_KEY already exists in .env — skipping.");
|
|
334
|
+
} else {
|
|
335
|
+
warn("Could not write to .env — add this manually:");
|
|
336
|
+
changes.manual.push(
|
|
337
|
+
`Add to your .env file:\n\n BOTVERSION_API_KEY=${args.key}`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
} else {
|
|
341
|
+
info("Skipped — add this manually to your .env file:");
|
|
342
|
+
changes.manual.push(
|
|
343
|
+
`Add to your .env file:\n\n BOTVERSION_API_KEY=${args.key}`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (!detected.hasDotenv) {
|
|
348
|
+
warn("dotenv not detected — installing it automatically...");
|
|
349
|
+
|
|
350
|
+
const { execSync } = require("child_process");
|
|
351
|
+
const installCmd =
|
|
352
|
+
detected.packageManager === "yarn"
|
|
353
|
+
? "yarn add dotenv"
|
|
354
|
+
: detected.packageManager === "pnpm"
|
|
355
|
+
? "pnpm add dotenv"
|
|
356
|
+
: detected.packageManager === "bun"
|
|
357
|
+
? "bun add dotenv"
|
|
358
|
+
: "npm install dotenv";
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
execSync(installCmd, { cwd: detected.cwd, stdio: "inherit" });
|
|
362
|
+
success("dotenv installed successfully");
|
|
363
|
+
|
|
364
|
+
const isESM = detected.moduleSystem === "esm";
|
|
365
|
+
const dotenvLine = isESM
|
|
366
|
+
? `import 'dotenv/config';`
|
|
367
|
+
: `require('dotenv').config();`;
|
|
368
|
+
|
|
369
|
+
const injectResult = writer.injectAtTop(entryPoint, dotenvLine);
|
|
370
|
+
if (injectResult.success) {
|
|
371
|
+
success("Added dotenv config to entry file");
|
|
372
|
+
changes.modified.push(path.relative(detected.cwd, entryPoint));
|
|
373
|
+
if (injectResult.backup) changes.backups.push(injectResult.backup);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
detected.hasDotenv = true;
|
|
377
|
+
} catch (err) {
|
|
378
|
+
warn("Could not install dotenv automatically.");
|
|
379
|
+
warn(`Please run manually: ${installCmd}`);
|
|
380
|
+
warn("And add to top of your entry file: require('dotenv').config();");
|
|
381
|
+
changes.manual.push(
|
|
382
|
+
`Install dotenv: ${installCmd}\n` +
|
|
383
|
+
`Add to top of ${path.relative(detected.cwd, entryPoint)}: require('dotenv').config();`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
311
388
|
const generated = generator.generateExpressInit(detected, args.key);
|
|
312
389
|
|
|
313
390
|
// PATTERN 2: Separate app file with module.exports = app
|
|
@@ -330,7 +407,7 @@ async function setupExpress(detected, args, changes, projectInfo) {
|
|
|
330
407
|
}
|
|
331
408
|
}
|
|
332
409
|
|
|
333
|
-
// PATTERN 1 & 3 & 4: app.listen()
|
|
410
|
+
// PATTERN 1 & 3 & 4: app.listen() in entry file
|
|
334
411
|
else if (
|
|
335
412
|
detected.listenCall ||
|
|
336
413
|
detected.listenInsideCallback ||
|
|
@@ -399,20 +476,43 @@ async function setupNextJs(detected, args, changes, projectInfo) {
|
|
|
399
476
|
const nextInfo = detected.next;
|
|
400
477
|
const baseDir = nextInfo.baseDir;
|
|
401
478
|
|
|
402
|
-
|
|
403
|
-
|
|
479
|
+
// ── Write API key to .env ──────────────────────────────────────────────
|
|
480
|
+
const shouldWriteEnv = await prompts.confirm(
|
|
481
|
+
" Add BOTVERSION_API_KEY to your .env file automatically?",
|
|
482
|
+
true,
|
|
404
483
|
);
|
|
405
484
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
485
|
+
if (shouldWriteEnv) {
|
|
486
|
+
const envResult = writer.writeEnvKey(
|
|
487
|
+
detected.cwd,
|
|
488
|
+
"BOTVERSION_API_KEY",
|
|
489
|
+
args.key,
|
|
490
|
+
"next",
|
|
491
|
+
);
|
|
492
|
+
if (envResult.success) {
|
|
493
|
+
success(
|
|
494
|
+
`Added BOTVERSION_API_KEY to ${path.relative(detected.cwd, envResult.path)}`,
|
|
495
|
+
);
|
|
496
|
+
changes.modified.push(path.relative(detected.cwd, envResult.path));
|
|
497
|
+
} else if (envResult.reason === "already_exists") {
|
|
498
|
+
info("BOTVERSION_API_KEY already exists in .env — skipping.");
|
|
499
|
+
} else {
|
|
500
|
+
warn("Could not write to .env — add this manually:");
|
|
501
|
+
changes.manual.push(
|
|
502
|
+
`Add to your .env file:\n\n BOTVERSION_API_KEY=${args.key}`,
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
info("Skipped — add this manually to your .env file:");
|
|
507
|
+
changes.manual.push(
|
|
508
|
+
`Add to your .env file:\n\n BOTVERSION_API_KEY=${args.key}`,
|
|
509
|
+
);
|
|
414
510
|
}
|
|
415
511
|
|
|
512
|
+
info2(
|
|
513
|
+
`Router: ${nextInfo.pagesRouter ? "Pages" : ""}${nextInfo.pagesRouter && nextInfo.appRouter ? " + " : ""}${nextInfo.appRouter ? "App" : ""}`,
|
|
514
|
+
);
|
|
515
|
+
|
|
416
516
|
// ── 1. Create instrumentation.js ──────────────────────────────────────────
|
|
417
517
|
const instrExt = detected.generateTs ? ".ts" : ".js";
|
|
418
518
|
const instrFile = path.join(detected.cwd, `instrumentation${instrExt}`);
|
|
@@ -458,61 +558,7 @@ async function setupNextJs(detected, args, changes, projectInfo) {
|
|
|
458
558
|
);
|
|
459
559
|
}
|
|
460
560
|
|
|
461
|
-
// ── 3.
|
|
462
|
-
if (nextInfo.pagesRouter) {
|
|
463
|
-
const pagesBase = path.join(baseDir, "pages");
|
|
464
|
-
const chatDir = path.join(pagesBase, "api", "botversion");
|
|
465
|
-
const chatFile = path.join(chatDir, `chat${instrExt}`);
|
|
466
|
-
|
|
467
|
-
const chatContent = generator.generateNextPagesChatRoute(detected);
|
|
468
|
-
const chatResult = writer.createFile(chatFile, chatContent, args.force);
|
|
469
|
-
|
|
470
|
-
const relPath = `${nextInfo.srcDir ? "src/" : ""}pages/api/botversion/chat${instrExt}`;
|
|
471
|
-
|
|
472
|
-
if (chatResult.success) {
|
|
473
|
-
success(`Created ${relPath}`);
|
|
474
|
-
changes.created.push(relPath);
|
|
475
|
-
} else if (chatResult.reason === "already_exists") {
|
|
476
|
-
const overwrite = await prompts.promptForce(relPath);
|
|
477
|
-
if (overwrite) {
|
|
478
|
-
writer.createFile(chatFile, chatContent, true);
|
|
479
|
-
success(`Overwrote ${relPath}`);
|
|
480
|
-
changes.modified.push(relPath);
|
|
481
|
-
} else {
|
|
482
|
-
warn(`Skipped ${relPath}`);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// ── 4. App Router chat route ──────────────────────────────────────────────
|
|
488
|
-
if (nextInfo.appRouter) {
|
|
489
|
-
const appBase = path.join(baseDir, "app");
|
|
490
|
-
const chatDir = path.join(appBase, "api", "botversion", "chat");
|
|
491
|
-
const chatFile = path.join(chatDir, `route${instrExt}`);
|
|
492
|
-
|
|
493
|
-
const chatContent = generator.generateNextAppChatRoute(detected);
|
|
494
|
-
const relPath = `${nextInfo.srcDir ? "src/" : ""}app/api/botversion/chat/route${instrExt}`;
|
|
495
|
-
|
|
496
|
-
const chatResult = writer.createFile(chatFile, chatContent, args.force);
|
|
497
|
-
|
|
498
|
-
if (chatResult.success) {
|
|
499
|
-
success(`Created ${relPath}`);
|
|
500
|
-
changes.created.push(relPath);
|
|
501
|
-
} else if (chatResult.reason === "already_exists") {
|
|
502
|
-
const overwrite = await prompts.promptForce(relPath);
|
|
503
|
-
if (overwrite) {
|
|
504
|
-
writer.createFile(chatFile, chatContent, true);
|
|
505
|
-
success(`Overwrote ${relPath}`);
|
|
506
|
-
changes.modified.push(relPath);
|
|
507
|
-
} else {
|
|
508
|
-
warn(`Skipped ${relPath}`);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// ── 5. Inject script tag into frontend ───────────────────────────────────
|
|
514
|
-
// For Next.js the frontend IS the same project
|
|
515
|
-
// frontendMainFile will be _app.js or layout.js
|
|
561
|
+
// ── 3. Inject script tag into frontend ───────────────────────────────────
|
|
516
562
|
if (detected.frontendMainFile && projectInfo) {
|
|
517
563
|
const scriptTag = generator.generateScriptTag(projectInfo);
|
|
518
564
|
const result = writer.injectScriptTag(
|