botversion-sdk 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/init.js 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
  }
@@ -42,6 +45,22 @@ function step(msg) {
42
45
  console.log(`\n${c.bold}${c.white} → ${msg}${c.reset}`);
43
46
  }
44
47
 
48
+ // ─── FETCH PROJECT INFO ───────────────────────────────────────────────────────
49
+
50
+ async function fetchProjectInfo(apiKey, platformUrl) {
51
+ const url = `${platformUrl}/api/sdk/project-info?workspaceKey=${encodeURIComponent(apiKey)}`;
52
+ try {
53
+ const response = await fetch(url);
54
+ if (!response.ok) {
55
+ throw new Error("Invalid API key or project not found");
56
+ }
57
+ return await response.json();
58
+ // returns { projectId, publicKey, apiUrl, cdnUrl }
59
+ } catch (err) {
60
+ throw new Error(`Could not fetch project info: ${err.message}`);
61
+ }
62
+ }
63
+
45
64
  // ─── PARSE ARGS ───────────────────────────────────────────────────────────────
46
65
 
47
66
  function parseArgs(argv) {
@@ -90,26 +109,33 @@ async function main() {
90
109
  const cwd = args.cwd;
91
110
  const changes = { modified: [], created: [], backups: [], manual: [] };
92
111
 
112
+ // ── Fetch project info from platform ──────────────────────────────────────
113
+ step("Fetching project info from platform...");
114
+ let projectInfo;
115
+ try {
116
+ projectInfo = await fetchProjectInfo(args.key, "http://localhost:3000");
117
+ success(`Project found — ID: ${projectInfo.projectId}`);
118
+ } catch (err) {
119
+ error(err.message);
120
+ process.exit(1);
121
+ }
122
+
93
123
  // ── Detect environment ────────────────────────────────────────────────────
94
124
  step("Scanning your project...");
95
125
 
96
126
  // Handle monorepo
97
- let workingDir = cwd;
98
127
  const monorepoInfo = detector.detectMonorepo(cwd);
99
128
  if (monorepoInfo.isMonorepo) {
100
- warn("Monorepo detected.");
101
- workingDir = await prompts.promptMonorepoPackage(
102
- monorepoInfo.packages,
103
- cwd,
129
+ warn(
130
+ "Monorepo detected will scan all packages for frontend and backend.",
104
131
  );
105
- info(`Using package: ${path.relative(cwd, workingDir) || "root"}`);
106
132
  }
107
133
 
108
- // Run full detection
109
- const info2 = detector.detect(workingDir);
134
+ // Always detect from root so frontend/backend split works correctly
135
+ const detected = detector.detect(cwd);
110
136
 
111
137
  // ── Check if already initialized ─────────────────────────────────────────
112
- if (info2.alreadyInitialized && !args.force) {
138
+ if (detected.alreadyInitialized && !args.force) {
113
139
  warn("BotVersion SDK is already initialized in this project.");
114
140
  log(`\n To reinitialize, run with --force flag:\n`);
115
141
  log(` npx botversion-sdk init --key ${args.key} --force\n`);
@@ -119,69 +145,112 @@ async function main() {
119
145
  // ── Framework check ───────────────────────────────────────────────────────
120
146
  step("Detecting framework...");
121
147
 
122
- if (!info2.framework.name) {
148
+ if (!detected.framework.name) {
123
149
  error("Could not detect a supported framework.");
124
150
  log(`\n Supported: Express.js, Next.js`);
125
151
  log(` Make sure you have them listed in package.json\n`);
126
152
  process.exit(1);
127
153
  }
128
154
 
129
- if (!info2.framework.supported) {
155
+ if (!detected.framework.supported) {
130
156
  warn(
131
- `Detected: ${info2.framework.name} (not yet supported for auto-setup)`,
157
+ `Detected: ${detected.framework.name} (not yet supported for auto-setup)`,
132
158
  );
133
159
  log("");
134
- log(generator.generateManualInstructions(info2.framework.name, args.key));
160
+ log(
161
+ generator.generateManualInstructions(detected.framework.name, args.key),
162
+ );
135
163
  process.exit(0);
136
164
  }
137
165
 
138
- success(`Framework: ${info2.framework.name}`);
166
+ success(`Framework: ${detected.framework.name}`);
139
167
  info(
140
- `Module system: ${info2.moduleSystem === "esm" ? "ES Modules" : "CommonJS"}`,
168
+ `Module system: ${detected.moduleSystem === "esm" ? "ES Modules" : "CommonJS"}`,
141
169
  );
142
- info(`Language: ${info2.isTypeScript ? "TypeScript" : "JavaScript"}`);
170
+ info(`Language: ${detected.isTypeScript ? "TypeScript" : "JavaScript"}`);
143
171
 
144
172
  // ── Auth detection ────────────────────────────────────────────────────────
145
173
  step("Detecting auth library...");
146
174
 
147
- let auth = info2.auth;
175
+ let auth = detected.auth;
148
176
 
149
177
  if (!auth.name) {
150
178
  warn("No auth library detected automatically.");
151
179
  auth = await prompts.promptAuthLibrary();
152
- info2.auth = auth;
180
+ detected.auth = auth;
153
181
  } else if (!auth.supported) {
154
182
  warn(`Detected auth: ${auth.name} (not yet supported for auto-setup)`);
155
183
  warn("Will set up without user context — you can add it manually later.");
156
184
  const proceed = await prompts.confirm("Continue without auth?", true);
157
185
  if (!proceed) process.exit(0);
158
186
  auth = { name: auth.name, supported: false };
159
- info2.auth = auth;
187
+ detected.auth = auth;
160
188
  } else {
161
189
  const versionLabel = auth.version ? ` (${auth.version})` : "";
162
190
  success(`Auth: ${auth.name}${versionLabel}`);
163
191
  }
164
192
 
165
193
  // ── Package manager ───────────────────────────────────────────────────────
166
- info(`Package manager: ${info2.packageManager}`);
194
+ info(`Package manager: ${detected.packageManager}`);
167
195
 
168
196
  // ─────────────────────────────────────────────────────────────────────────
169
197
  // FRAMEWORK: EXPRESS
170
198
  // ─────────────────────────────────────────────────────────────────────────
171
- if (info2.framework.name === "express") {
172
- await setupExpress(info2, args, changes);
199
+ if (detected.framework.name === "express") {
200
+ await setupExpress(detected, args, changes, projectInfo);
201
+
202
+ // ── Inject script tag into frontend (Express only) ──────────────────
203
+ // Next.js handles its own script tag injection inside setupNextJs
204
+ if (detected.frontendMainFile && projectInfo) {
205
+ const scriptTag = generator.generateScriptTag(projectInfo);
206
+ const result = writer.injectScriptTag(
207
+ detected.frontendMainFile.file,
208
+ detected.frontendMainFile.type,
209
+ scriptTag,
210
+ args.force,
211
+ );
212
+
213
+ if (result.success) {
214
+ success(
215
+ `Injected script tag into ${path.relative(detected.cwd, detected.frontendMainFile.file)}`,
216
+ );
217
+ changes.modified.push(
218
+ path.relative(detected.cwd, detected.frontendMainFile.file),
219
+ );
220
+ if (result.backup) changes.backups.push(result.backup);
221
+ } else if (result.reason === "already_exists") {
222
+ warn(
223
+ "BotVersion script tag already exists in frontend file — skipping.",
224
+ );
225
+ } else {
226
+ warn(
227
+ "Could not auto-inject script tag. Add this manually to your frontend HTML:",
228
+ );
229
+ console.log("\n" + scriptTag + "\n");
230
+ changes.manual.push(
231
+ `Add to your frontend HTML before </body>:\n\n${scriptTag}`,
232
+ );
233
+ }
234
+ } else if (!detected.frontendMainFile) {
235
+ warn("Could not find frontend main file automatically.");
236
+ const scriptTag = generator.generateScriptTag(projectInfo);
237
+ changes.manual.push(
238
+ `Add to your frontend HTML before </body>:\n\n${scriptTag}`,
239
+ );
240
+ }
173
241
  }
174
242
 
175
243
  // ─────────────────────────────────────────────────────────────────────────
176
244
  // FRAMEWORK: NEXT.JS
177
245
  // ─────────────────────────────────────────────────────────────────────────
178
- else if (info2.framework.name === "next") {
179
- await setupNextJs(info2, args, changes);
246
+ else if (detected.framework.name === "next") {
247
+ await setupNextJs(detected, args, changes, projectInfo);
180
248
  }
181
249
 
182
250
  // ── Write API key to .env / .env.local ────────────────────────────────────
183
- const envFileName = info2.framework.name === "next" ? ".env.local" : ".env";
184
- const envPath = path.join(workingDir, envFileName);
251
+ const envFileName =
252
+ detected.framework.name === "next" ? ".env.local" : ".env";
253
+ const envPath = path.join(detected.cwd, envFileName);
185
254
  const envLine = `BOTVERSION_API_KEY=${args.key}`;
186
255
  const envContent = fs.existsSync(envPath)
187
256
  ? fs.readFileSync(envPath, "utf8")
@@ -216,16 +285,16 @@ async function main() {
216
285
 
217
286
  // ─── EXPRESS SETUP ────────────────────────────────────────────────────────────
218
287
 
219
- async function setupExpress(info, args, changes) {
288
+ async function setupExpress(detected, args, changes, projectInfo) {
220
289
  step("Setting up Express...");
221
290
 
222
291
  // Find entry point
223
- let entryPoint = info.entryPoint;
292
+ let entryPoint = detected.entryPoint;
224
293
 
225
294
  if (!entryPoint || !fs.existsSync(entryPoint)) {
226
295
  warn("Could not find your server entry point automatically.");
227
296
  const manualPath = await prompts.promptEntryPoint();
228
- entryPoint = path.resolve(info.cwd, manualPath);
297
+ entryPoint = path.resolve(detected.cwd, manualPath);
229
298
 
230
299
  if (!fs.existsSync(entryPoint)) {
231
300
  error(`File not found: ${entryPoint}`);
@@ -233,42 +302,73 @@ async function setupExpress(info, args, changes) {
233
302
  }
234
303
  }
235
304
 
236
- success(`Entry point: ${path.relative(info.cwd, entryPoint)}`);
305
+ success(`Entry point: ${path.relative(detected.cwd, entryPoint)}`);
237
306
 
238
307
  // Generate the init code
239
- const generated = generator.generateExpressInit(info, args.key);
308
+ const generated = generator.generateExpressInit(detected, args.key);
309
+
310
+ // PATTERN 2: Separate app file with module.exports = app
311
+ if (detected.appFile) {
312
+ info(`Found app file: ${path.relative(detected.cwd, detected.appFile)}`);
313
+ const generated2 = generator.generateExpressInit(detected, args.key);
314
+ const result = writer.injectBeforeExport(
315
+ detected.appFile,
316
+ generated2.initBlock,
317
+ detected.appVarName,
318
+ );
240
319
 
241
- // Find app.listen() and inject before it
242
- const listenCall = detector.findListenCall(entryPoint);
320
+ if (result.success) {
321
+ success(
322
+ `Injected BotVersion.init() into ${path.relative(detected.cwd, detected.appFile)}`,
323
+ );
324
+ changes.modified.push(path.relative(detected.cwd, detected.appFile));
325
+ } else if (result.reason === "already_exists") {
326
+ warn("BotVersion already found — skipping injection.");
327
+ }
328
+ }
243
329
 
244
- if (listenCall) {
245
- info(`Found app.listen() at line ${listenCall.lineNumber}`);
246
- const result = writer.injectBeforeListen(entryPoint, generated.initBlock);
330
+ // PATTERN 1 & 3 & 4: app.listen() or server.listen() in entry file
331
+ else if (
332
+ detected.listenCall ||
333
+ detected.listenInsideCallback ||
334
+ detected.createServer
335
+ ) {
336
+ const result = writer.injectBeforeListen(
337
+ entryPoint,
338
+ generated.initBlock,
339
+ detected.appVarName,
340
+ );
247
341
 
248
342
  if (result.success) {
249
343
  success(`Injected BotVersion.init() before app.listen()`);
250
- changes.modified.push(path.relative(info.cwd, entryPoint));
344
+ changes.modified.push(path.relative(detected.cwd, entryPoint));
251
345
  if (result.backup) changes.backups.push(result.backup);
252
346
  } else if (result.reason === "already_exists") {
253
347
  warn("BotVersion already found in entry point — skipping injection.");
254
348
  }
255
- } else {
256
- // app.listen() not found
257
- warn("Could not find app.listen() in entry point.");
349
+ }
350
+
351
+ // LAST RESORT: ask the user
352
+ else {
353
+ warn("Could not find the right place to inject automatically.");
258
354
  const response = await prompts.promptMissingListenCall(
259
- path.relative(info.cwd, entryPoint),
355
+ path.relative(detected.cwd, entryPoint),
260
356
  );
261
357
 
262
358
  if (response.action === "append") {
263
359
  const result = writer.appendToFile(entryPoint, generated.initBlock);
264
360
  if (result.success) {
265
361
  success("Appended BotVersion setup to end of file.");
266
- changes.modified.push(path.relative(info.cwd, entryPoint));
362
+ changes.modified.push(path.relative(detected.cwd, entryPoint));
267
363
  }
268
364
  } else if (response.action === "manual_path") {
269
- const altPath = path.resolve(info.cwd, response.filePath);
365
+ const altPath = path.resolve(detected.cwd, response.filePath);
270
366
  if (fs.existsSync(altPath)) {
271
- const result = writer.injectBeforeListen(altPath, generated.initBlock);
367
+ const result = writer.injectBeforeListen(
368
+ altPath,
369
+ generated.initBlock,
370
+ detected.appVarName,
371
+ );
272
372
  if (result.success) {
273
373
  success(`Injected into ${response.filePath}`);
274
374
  changes.modified.push(response.filePath);
@@ -280,7 +380,6 @@ async function setupExpress(info, args, changes) {
280
380
  );
281
381
  }
282
382
  } else {
283
- // skip — print manual instructions
284
383
  changes.manual.push(
285
384
  `Add this to your server file before app.listen():\n\n${generated.initBlock}`,
286
385
  );
@@ -291,10 +390,10 @@ async function setupExpress(info, args, changes) {
291
390
 
292
391
  // ─── NEXT.JS SETUP ────────────────────────────────────────────────────────────
293
392
 
294
- async function setupNextJs(info, args, changes) {
393
+ async function setupNextJs(detected, args, changes, projectInfo) {
295
394
  step("Setting up Next.js...");
296
395
 
297
- const nextInfo = info.next;
396
+ const nextInfo = detected.next;
298
397
  const baseDir = nextInfo.baseDir;
299
398
 
300
399
  info2(
@@ -302,20 +401,23 @@ async function setupNextJs(info, args, changes) {
302
401
  );
303
402
 
304
403
  // ── next-auth config location ─────────────────────────────────────────────
305
- if (info.auth.name === "next-auth" && !info.nextAuthConfig) {
404
+ if (detected.auth.name === "next-auth" && !detected.nextAuthConfig) {
306
405
  warn("Could not find authOptions location automatically.");
307
406
  const configPath = await prompts.promptNextAuthConfigPath();
308
- info.nextAuthConfig = {
309
- path: path.resolve(info.cwd, configPath),
407
+ detected.nextAuthConfig = {
408
+ path: path.resolve(detected.cwd, configPath),
310
409
  relativePath: configPath,
311
410
  };
312
411
  }
313
412
 
314
413
  // ── 1. Create instrumentation.js ──────────────────────────────────────────
315
- const instrExt = info.generateTs ? ".ts" : ".js";
316
- const instrFile = path.join(info.cwd, `instrumentation${instrExt}`);
414
+ const instrExt = detected.generateTs ? ".ts" : ".js";
415
+ const instrFile = path.join(detected.cwd, `instrumentation${instrExt}`);
317
416
 
318
- const instrContent = generator.generateInstrumentationFile(info, args.key);
417
+ const instrContent = generator.generateInstrumentationFile(
418
+ detected,
419
+ args.key,
420
+ );
319
421
  const instrResult = writer.createFile(instrFile, instrContent, args.force);
320
422
 
321
423
  if (instrResult.success) {
@@ -333,15 +435,18 @@ async function setupNextJs(info, args, changes) {
333
435
  }
334
436
 
335
437
  // ── 2. Patch next.config.js ───────────────────────────────────────────────
336
- const configPatch = generator.generateNextConfigPatch(info.cwd);
438
+ const configPatch = generator.generateNextConfigPatch(
439
+ detected.cwd,
440
+ detected.nextVersion,
441
+ );
337
442
 
338
443
  if (configPatch) {
339
444
  if (configPatch.alreadyPatched) {
340
445
  info("next.config.js already has instrumentationHook — skipping.");
341
446
  } else {
342
447
  fs.writeFileSync(configPatch.path, configPatch.content, "utf8");
343
- success(`Updated ${path.relative(info.cwd, configPatch.path)}`);
344
- changes.modified.push(path.relative(info.cwd, configPatch.path));
448
+ success(`Updated ${path.relative(detected.cwd, configPatch.path)}`);
449
+ changes.modified.push(path.relative(detected.cwd, configPatch.path));
345
450
  }
346
451
  } else {
347
452
  warn("Could not find next.config.js — please add this manually:");
@@ -356,7 +461,7 @@ async function setupNextJs(info, args, changes) {
356
461
  const chatDir = path.join(pagesBase, "api", "botversion");
357
462
  const chatFile = path.join(chatDir, `chat${instrExt}`);
358
463
 
359
- const chatContent = generator.generateNextPagesChatRoute(info);
464
+ const chatContent = generator.generateNextPagesChatRoute(detected);
360
465
  const chatResult = writer.createFile(chatFile, chatContent, args.force);
361
466
 
362
467
  const relPath = `${nextInfo.srcDir ? "src/" : ""}pages/api/botversion/chat${instrExt}`;
@@ -382,7 +487,7 @@ async function setupNextJs(info, args, changes) {
382
487
  const chatDir = path.join(appBase, "api", "botversion", "chat");
383
488
  const chatFile = path.join(chatDir, `route${instrExt}`);
384
489
 
385
- const chatContent = generator.generateNextAppChatRoute(info);
490
+ const chatContent = generator.generateNextAppChatRoute(detected);
386
491
  const relPath = `${nextInfo.srcDir ? "src/" : ""}app/api/botversion/chat/route${instrExt}`;
387
492
 
388
493
  const chatResult = writer.createFile(chatFile, chatContent, args.force);
@@ -401,11 +506,45 @@ async function setupNextJs(info, args, changes) {
401
506
  }
402
507
  }
403
508
  }
404
- }
405
509
 
406
- // ─── helper used inside setupNextJs ──────────────────────────────────────────
407
- function info2(msg) {
408
- console.log(`${c.cyan} ℹ${c.reset} ${msg}`);
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
513
+ if (detected.frontendMainFile && projectInfo) {
514
+ const scriptTag = generator.generateScriptTag(projectInfo);
515
+ const result = writer.injectScriptTag(
516
+ detected.frontendMainFile.file,
517
+ detected.frontendMainFile.type,
518
+ scriptTag,
519
+ args.force,
520
+ );
521
+
522
+ if (result.success) {
523
+ success(
524
+ `Injected script tag into ${path.relative(detected.cwd, detected.frontendMainFile.file)}`,
525
+ );
526
+ changes.modified.push(
527
+ path.relative(detected.cwd, detected.frontendMainFile.file),
528
+ );
529
+ if (result.backup) changes.backups.push(result.backup);
530
+ } else if (result.reason === "already_exists") {
531
+ warn("BotVersion script tag already exists — skipping.");
532
+ } else {
533
+ warn(
534
+ "Could not auto-inject script tag. Add this manually to your frontend file:",
535
+ );
536
+ console.log("\n" + scriptTag + "\n");
537
+ changes.manual.push(
538
+ `Add to your frontend HTML before </body>:\n\n${scriptTag}`,
539
+ );
540
+ }
541
+ } else if (!detected.frontendMainFile) {
542
+ warn("Could not find frontend file automatically.");
543
+ const scriptTag = generator.generateScriptTag(projectInfo);
544
+ changes.manual.push(
545
+ `Add to your frontend HTML before </body>:\n\n${scriptTag}`,
546
+ );
547
+ }
409
548
  }
410
549
 
411
550
  // ─── RUN ──────────────────────────────────────────────────────────────────────