botversion-sdk 1.0.3 → 1.0.5

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 CHANGED
@@ -123,7 +123,6 @@ async function main() {
123
123
  // ── Detect environment ────────────────────────────────────────────────────
124
124
  step("Scanning your project...");
125
125
 
126
- // Handle monorepo
127
126
  const monorepoInfo = detector.detectMonorepo(cwd);
128
127
  if (monorepoInfo.isMonorepo) {
129
128
  warn(
@@ -131,7 +130,6 @@ async function main() {
131
130
  );
132
131
  }
133
132
 
134
- // Always detect from root so frontend/backend split works correctly
135
133
  const detected = detector.detect(cwd);
136
134
 
137
135
  // ── Check if already initialized ─────────────────────────────────────────
@@ -168,39 +166,88 @@ async function main() {
168
166
  `Module system: ${detected.moduleSystem === "esm" ? "ES Modules" : "CommonJS"}`,
169
167
  );
170
168
  info(`Language: ${detected.isTypeScript ? "TypeScript" : "JavaScript"}`);
169
+ info(`Package manager: ${detected.packageManager}`);
171
170
 
172
- // ── Auth detection ────────────────────────────────────────────────────────
173
- step("Detecting auth library...");
174
-
175
- let auth = detected.auth;
176
-
177
- if (!auth.name) {
178
- warn("No auth library detected automatically.");
179
- auth = await prompts.promptAuthLibrary();
180
- detected.auth = auth;
181
- } else if (!auth.supported) {
182
- warn(`Detected auth: ${auth.name} (not yet supported for auto-setup)`);
183
- warn("Will set up without user context — you can add it manually later.");
184
- const proceed = await prompts.confirm("Continue without auth?", true);
185
- if (!proceed) process.exit(0);
186
- auth = { name: auth.name, supported: false };
187
- detected.auth = auth;
188
- } else {
189
- const versionLabel = auth.version ? ` (${auth.version})` : "";
190
- success(`Auth: ${auth.name}${versionLabel}`);
191
- }
171
+ // ─────────────────────────────────────────────────────────────────────────
172
+ // FRAMEWORK: NEXT.JS
173
+ // ─────────────────────────────────────────────────────────────────────────
174
+ if (detected.framework.name === "next") {
175
+ await setupNextJs(detected, args, changes, projectInfo);
192
176
 
193
- // ── Package manager ───────────────────────────────────────────────────────
194
- info(`Package manager: ${detected.packageManager}`);
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
+ }
195
244
 
196
245
  // ─────────────────────────────────────────────────────────────────────────
197
- // FRAMEWORK: EXPRESS
246
+ // FRAMEWORK: EXPRESS ONLY
198
247
  // ─────────────────────────────────────────────────────────────────────────
199
- if (detected.framework.name === "express") {
248
+ else if (detected.framework.name === "express") {
200
249
  await setupExpress(detected, args, changes, projectInfo);
201
250
 
202
- // ── Inject script tag into frontend (Express only) ──────────────────
203
- // Next.js handles its own script tag injection inside setupNextJs
204
251
  if (detected.frontendMainFile && projectInfo) {
205
252
  const scriptTag = generator.generateScriptTag(projectInfo);
206
253
  const result = writer.injectScriptTag(
@@ -240,45 +287,6 @@ async function main() {
240
287
  }
241
288
  }
242
289
 
243
- // ─────────────────────────────────────────────────────────────────────────
244
- // FRAMEWORK: NEXT.JS
245
- // ─────────────────────────────────────────────────────────────────────────
246
- else if (detected.framework.name === "next") {
247
- await setupNextJs(detected, args, changes, projectInfo);
248
- }
249
-
250
- // ── Write API key to .env / .env.local ────────────────────────────────────
251
- const envFileName =
252
- detected.framework.name === "next" ? ".env.local" : ".env";
253
- const envPath = path.join(detected.cwd, envFileName);
254
- const envLine = `BOTVERSION_API_KEY=${args.key}`;
255
- const envContent = fs.existsSync(envPath)
256
- ? fs.readFileSync(envPath, "utf8")
257
- : "";
258
-
259
- if (!envContent.includes("BOTVERSION_API_KEY")) {
260
- const writeEnv = await prompts.confirm(
261
- `Add BOTVERSION_API_KEY to ${envFileName}?`,
262
- true,
263
- );
264
-
265
- if (writeEnv) {
266
- const envAddition = "\n\n# BotVersion API key\n" + envLine + "\n";
267
- fs.writeFileSync(envPath, envContent.trimEnd() + envAddition, "utf8");
268
- success(`Added BOTVERSION_API_KEY to ${envFileName}`);
269
- changes.modified.push(envFileName);
270
- } else {
271
- warn(`Skipped — add this manually to your ${envFileName}:`);
272
- log(`\n # BotVersion API key`);
273
- log(` BOTVERSION_API_KEY=${args.key}\n`);
274
- changes.manual.push(
275
- `Add to your ${envFileName}:\n\n # BotVersion API key\n BOTVERSION_API_KEY=${args.key}`,
276
- );
277
- }
278
- } else {
279
- info(`BOTVERSION_API_KEY already exists in ${envFileName} — skipping.`);
280
- }
281
-
282
290
  // ── Print summary ─────────────────────────────────────────────────────────
283
291
  log(writer.writeSummary(changes));
284
292
  }
@@ -288,7 +296,6 @@ async function main() {
288
296
  async function setupExpress(detected, args, changes, projectInfo) {
289
297
  step("Setting up Express...");
290
298
 
291
- // Find entry point
292
299
  let entryPoint = detected.entryPoint;
293
300
 
294
301
  if (!entryPoint || !fs.existsSync(entryPoint)) {
@@ -304,7 +311,80 @@ async function setupExpress(detected, args, changes, projectInfo) {
304
311
 
305
312
  success(`Entry point: ${path.relative(detected.cwd, entryPoint)}`);
306
313
 
307
- // Generate the init code
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
+
308
388
  const generated = generator.generateExpressInit(detected, args.key);
309
389
 
310
390
  // PATTERN 2: Separate app file with module.exports = app
@@ -327,7 +407,7 @@ async function setupExpress(detected, args, changes, projectInfo) {
327
407
  }
328
408
  }
329
409
 
330
- // PATTERN 1 & 3 & 4: app.listen() or server.listen() in entry file
410
+ // PATTERN 1 & 3 & 4: app.listen() in entry file
331
411
  else if (
332
412
  detected.listenCall ||
333
413
  detected.listenInsideCallback ||
@@ -396,20 +476,43 @@ async function setupNextJs(detected, args, changes, projectInfo) {
396
476
  const nextInfo = detected.next;
397
477
  const baseDir = nextInfo.baseDir;
398
478
 
399
- info2(
400
- `Router: ${nextInfo.pagesRouter ? "Pages" : ""}${nextInfo.pagesRouter && nextInfo.appRouter ? " + " : ""}${nextInfo.appRouter ? "App" : ""}`,
479
+ // ── Write API key to .env ──────────────────────────────────────────────
480
+ const shouldWriteEnv = await prompts.confirm(
481
+ " Add BOTVERSION_API_KEY to your .env file automatically?",
482
+ true,
401
483
  );
402
484
 
403
- // ── next-auth config location ─────────────────────────────────────────────
404
- if (detected.auth.name === "next-auth" && !detected.nextAuthConfig) {
405
- warn("Could not find authOptions location automatically.");
406
- const configPath = await prompts.promptNextAuthConfigPath();
407
- detected.nextAuthConfig = {
408
- path: path.resolve(detected.cwd, configPath),
409
- relativePath: configPath,
410
- };
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
+ );
411
510
  }
412
511
 
512
+ info2(
513
+ `Router: ${nextInfo.pagesRouter ? "Pages" : ""}${nextInfo.pagesRouter && nextInfo.appRouter ? " + " : ""}${nextInfo.appRouter ? "App" : ""}`,
514
+ );
515
+
413
516
  // ── 1. Create instrumentation.js ──────────────────────────────────────────
414
517
  const instrExt = detected.generateTs ? ".ts" : ".js";
415
518
  const instrFile = path.join(detected.cwd, `instrumentation${instrExt}`);
@@ -455,61 +558,7 @@ async function setupNextJs(detected, args, changes, projectInfo) {
455
558
  );
456
559
  }
457
560
 
458
- // ── 3. Pages Router chat route ────────────────────────────────────────────
459
- if (nextInfo.pagesRouter) {
460
- const pagesBase = path.join(baseDir, "pages");
461
- const chatDir = path.join(pagesBase, "api", "botversion");
462
- const chatFile = path.join(chatDir, `chat${instrExt}`);
463
-
464
- const chatContent = generator.generateNextPagesChatRoute(detected);
465
- const chatResult = writer.createFile(chatFile, chatContent, args.force);
466
-
467
- const relPath = `${nextInfo.srcDir ? "src/" : ""}pages/api/botversion/chat${instrExt}`;
468
-
469
- if (chatResult.success) {
470
- success(`Created ${relPath}`);
471
- changes.created.push(relPath);
472
- } else if (chatResult.reason === "already_exists") {
473
- const overwrite = await prompts.promptForce(relPath);
474
- if (overwrite) {
475
- writer.createFile(chatFile, chatContent, true);
476
- success(`Overwrote ${relPath}`);
477
- changes.modified.push(relPath);
478
- } else {
479
- warn(`Skipped ${relPath}`);
480
- }
481
- }
482
- }
483
-
484
- // ── 4. App Router chat route ──────────────────────────────────────────────
485
- if (nextInfo.appRouter) {
486
- const appBase = path.join(baseDir, "app");
487
- const chatDir = path.join(appBase, "api", "botversion", "chat");
488
- const chatFile = path.join(chatDir, `route${instrExt}`);
489
-
490
- const chatContent = generator.generateNextAppChatRoute(detected);
491
- const relPath = `${nextInfo.srcDir ? "src/" : ""}app/api/botversion/chat/route${instrExt}`;
492
-
493
- const chatResult = writer.createFile(chatFile, chatContent, args.force);
494
-
495
- if (chatResult.success) {
496
- success(`Created ${relPath}`);
497
- changes.created.push(relPath);
498
- } else if (chatResult.reason === "already_exists") {
499
- const overwrite = await prompts.promptForce(relPath);
500
- if (overwrite) {
501
- writer.createFile(chatFile, chatContent, true);
502
- success(`Overwrote ${relPath}`);
503
- changes.modified.push(relPath);
504
- } else {
505
- warn(`Skipped ${relPath}`);
506
- }
507
- }
508
- }
509
-
510
- // ── 5. Inject script tag into frontend ───────────────────────────────────
511
- // For Next.js the frontend IS the same project
512
- // frontendMainFile will be _app.js or layout.js
561
+ // ── 3. Inject script tag into frontend ───────────────────────────────────
513
562
  if (detected.frontendMainFile && projectInfo) {
514
563
  const scriptTag = generator.generateScriptTag(projectInfo);
515
564
  const result = writer.injectScriptTag(