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 +184 -135
- package/cli/detector.js +360 -173
- package/cli/generator.js +18 -476
- package/cli/prompts.js +40 -42
- package/cli/writer.js +118 -42
- package/client.js +214 -36
- package/index.js +74 -391
- package/interceptor.js +112 -124
- package/package.json +3 -2
- package/scanner.js +792 -27
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
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
|
-
//
|
|
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()
|
|
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
|
-
|
|
400
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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.
|
|
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(
|