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/bin/scan-frontend.js +49 -0
- package/client.js +12 -6
- package/index.js +150 -23
- package/interceptor.js +332 -86
- package/package.json +2 -2
- package/scanner.js +7 -106
- package/bin/init.js +0 -605
- package/cli/detector.js +0 -1149
- package/cli/generator.js +0 -187
- package/cli/prompts.js +0 -163
- package/cli/writer.js +0 -511
package/bin/init.js
DELETED
|
@@ -1,605 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// botversion-sdk/bin/init.js
|
|
3
|
-
"use strict";
|
|
4
|
-
|
|
5
|
-
const path = require("path");
|
|
6
|
-
const fs = require("fs");
|
|
7
|
-
|
|
8
|
-
const detector = require("../cli/detector");
|
|
9
|
-
const generator = require("../cli/generator");
|
|
10
|
-
const writer = require("../cli/writer");
|
|
11
|
-
const prompts = require("../cli/prompts");
|
|
12
|
-
|
|
13
|
-
// ─── COLORS ──────────────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
const c = {
|
|
16
|
-
reset: "\x1b[0m",
|
|
17
|
-
bold: "\x1b[1m",
|
|
18
|
-
green: "\x1b[32m",
|
|
19
|
-
yellow: "\x1b[33m",
|
|
20
|
-
red: "\x1b[31m",
|
|
21
|
-
cyan: "\x1b[36m",
|
|
22
|
-
gray: "\x1b[90m",
|
|
23
|
-
white: "\x1b[37m",
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
function log(msg) {
|
|
27
|
-
console.log(msg);
|
|
28
|
-
}
|
|
29
|
-
function info(msg) {
|
|
30
|
-
console.log(`${c.cyan} ℹ${c.reset} ${msg}`);
|
|
31
|
-
}
|
|
32
|
-
function info2(msg) {
|
|
33
|
-
console.log(`${c.cyan} ℹ${c.reset} ${msg}`);
|
|
34
|
-
}
|
|
35
|
-
function success(msg) {
|
|
36
|
-
console.log(`${c.green} ✔${c.reset} ${msg}`);
|
|
37
|
-
}
|
|
38
|
-
function warn(msg) {
|
|
39
|
-
console.log(`${c.yellow} ⚠${c.reset} ${msg}`);
|
|
40
|
-
}
|
|
41
|
-
function error(msg) {
|
|
42
|
-
console.log(`${c.red} ✖${c.reset} ${msg}`);
|
|
43
|
-
}
|
|
44
|
-
function step(msg) {
|
|
45
|
-
console.log(`\n${c.bold}${c.white} → ${msg}${c.reset}`);
|
|
46
|
-
}
|
|
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
|
-
|
|
64
|
-
// ─── PARSE ARGS ───────────────────────────────────────────────────────────────
|
|
65
|
-
|
|
66
|
-
function parseArgs(argv) {
|
|
67
|
-
const args = { key: null, force: false, cwd: process.cwd() };
|
|
68
|
-
|
|
69
|
-
for (let i = 0; i < argv.length; i++) {
|
|
70
|
-
if (argv[i] === "--key" && argv[i + 1]) {
|
|
71
|
-
args.key = argv[i + 1];
|
|
72
|
-
i++;
|
|
73
|
-
} else if (argv[i] === "--force") {
|
|
74
|
-
args.force = true;
|
|
75
|
-
} else if (argv[i] === "--cwd" && argv[i + 1]) {
|
|
76
|
-
args.cwd = path.resolve(argv[i + 1]);
|
|
77
|
-
i++;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return args;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ─── BANNER ───────────────────────────────────────────────────────────────────
|
|
85
|
-
|
|
86
|
-
function printBanner() {
|
|
87
|
-
log("");
|
|
88
|
-
log(`${c.cyan}${c.bold} ╔══════════════════════════════════════╗${c.reset}`);
|
|
89
|
-
log(`${c.cyan}${c.bold} ║ BotVersion SDK Setup CLI ║${c.reset}`);
|
|
90
|
-
log(`${c.cyan}${c.bold} ╚══════════════════════════════════════╝${c.reset}`);
|
|
91
|
-
log("");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// ─── MAIN ─────────────────────────────────────────────────────────────────────
|
|
95
|
-
|
|
96
|
-
async function main() {
|
|
97
|
-
const args = parseArgs(process.argv.slice(2));
|
|
98
|
-
|
|
99
|
-
printBanner();
|
|
100
|
-
|
|
101
|
-
// ── Validate API key ───────────────────────────────────────────────────────
|
|
102
|
-
if (!args.key) {
|
|
103
|
-
error("API key is required.");
|
|
104
|
-
log(`\n Usage: npx botversion-sdk init --key YOUR_WORKSPACE_KEY\n`);
|
|
105
|
-
log(` Get your key from: https://botversion.com/settings\n`);
|
|
106
|
-
process.exit(1);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const cwd = args.cwd;
|
|
110
|
-
const changes = { modified: [], created: [], backups: [], manual: [] };
|
|
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, "https://botversion.com");
|
|
117
|
-
success(`Project found — ID: ${projectInfo.projectId}`);
|
|
118
|
-
} catch (err) {
|
|
119
|
-
error(err.message);
|
|
120
|
-
process.exit(1);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ── Detect environment ────────────────────────────────────────────────────
|
|
124
|
-
step("Scanning your project...");
|
|
125
|
-
|
|
126
|
-
const monorepoInfo = detector.detectMonorepo(cwd);
|
|
127
|
-
if (monorepoInfo.isMonorepo) {
|
|
128
|
-
warn(
|
|
129
|
-
"Monorepo detected — will scan all packages for frontend and backend.",
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const detected = detector.detect(cwd);
|
|
134
|
-
|
|
135
|
-
// ── Check if already initialized ─────────────────────────────────────────
|
|
136
|
-
if (detected.alreadyInitialized && !args.force) {
|
|
137
|
-
warn("BotVersion SDK is already initialized in this project.");
|
|
138
|
-
log(`\n To reinitialize, run with --force flag:\n`);
|
|
139
|
-
log(` npx botversion-sdk init --key ${args.key} --force\n`);
|
|
140
|
-
process.exit(0);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ── Framework check ───────────────────────────────────────────────────────
|
|
144
|
-
step("Detecting framework...");
|
|
145
|
-
|
|
146
|
-
if (!detected.framework.name) {
|
|
147
|
-
error("Could not detect a supported framework.");
|
|
148
|
-
log(`\n Supported: Express.js, Next.js`);
|
|
149
|
-
log(` Make sure you have them listed in package.json\n`);
|
|
150
|
-
process.exit(1);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (!detected.framework.supported) {
|
|
154
|
-
warn(
|
|
155
|
-
`Detected: ${detected.framework.name} (not yet supported for auto-setup)`,
|
|
156
|
-
);
|
|
157
|
-
log("");
|
|
158
|
-
log(
|
|
159
|
-
generator.generateManualInstructions(detected.framework.name, args.key),
|
|
160
|
-
);
|
|
161
|
-
process.exit(0);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
success(`Framework: ${detected.framework.name}`);
|
|
165
|
-
info(
|
|
166
|
-
`Module system: ${detected.moduleSystem === "esm" ? "ES Modules" : "CommonJS"}`,
|
|
167
|
-
);
|
|
168
|
-
info(`Language: ${detected.isTypeScript ? "TypeScript" : "JavaScript"}`);
|
|
169
|
-
info(`Package manager: ${detected.packageManager}`);
|
|
170
|
-
|
|
171
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
172
|
-
// FRAMEWORK: NEXT.JS
|
|
173
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
174
|
-
if (detected.framework.name === "next") {
|
|
175
|
-
await setupNextJs(detected, args, changes, projectInfo);
|
|
176
|
-
|
|
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
|
-
}
|
|
244
|
-
|
|
245
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
246
|
-
// FRAMEWORK: EXPRESS ONLY
|
|
247
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
248
|
-
else if (detected.framework.name === "express") {
|
|
249
|
-
await setupExpress(detected, args, changes, projectInfo);
|
|
250
|
-
|
|
251
|
-
if (detected.frontendMainFile && projectInfo) {
|
|
252
|
-
const scriptTag = generator.generateScriptTag(projectInfo);
|
|
253
|
-
const result = writer.injectScriptTag(
|
|
254
|
-
detected.frontendMainFile.file,
|
|
255
|
-
detected.frontendMainFile.type,
|
|
256
|
-
scriptTag,
|
|
257
|
-
args.force,
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
if (result.success) {
|
|
261
|
-
success(
|
|
262
|
-
`Injected script tag into ${path.relative(detected.cwd, detected.frontendMainFile.file)}`,
|
|
263
|
-
);
|
|
264
|
-
changes.modified.push(
|
|
265
|
-
path.relative(detected.cwd, detected.frontendMainFile.file),
|
|
266
|
-
);
|
|
267
|
-
if (result.backup) changes.backups.push(result.backup);
|
|
268
|
-
} else if (result.reason === "already_exists") {
|
|
269
|
-
warn(
|
|
270
|
-
"BotVersion script tag already exists in frontend file — skipping.",
|
|
271
|
-
);
|
|
272
|
-
} else {
|
|
273
|
-
warn(
|
|
274
|
-
"Could not auto-inject script tag. Add this manually to your frontend HTML:",
|
|
275
|
-
);
|
|
276
|
-
console.log("\n" + scriptTag + "\n");
|
|
277
|
-
changes.manual.push(
|
|
278
|
-
`Add to your frontend HTML before </body>:\n\n${scriptTag}`,
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
} else if (!detected.frontendMainFile) {
|
|
282
|
-
warn("Could not find frontend main file automatically.");
|
|
283
|
-
const scriptTag = generator.generateScriptTag(projectInfo);
|
|
284
|
-
changes.manual.push(
|
|
285
|
-
`Add to your frontend HTML before </body>:\n\n${scriptTag}`,
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// ── Print summary ─────────────────────────────────────────────────────────
|
|
291
|
-
log(writer.writeSummary(changes));
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// ─── EXPRESS SETUP ────────────────────────────────────────────────────────────
|
|
295
|
-
|
|
296
|
-
async function setupExpress(detected, args, changes, projectInfo) {
|
|
297
|
-
step("Setting up Express...");
|
|
298
|
-
|
|
299
|
-
let entryPoint = detected.entryPoint;
|
|
300
|
-
|
|
301
|
-
if (!entryPoint || !fs.existsSync(entryPoint)) {
|
|
302
|
-
warn("Could not find your server entry point automatically.");
|
|
303
|
-
const manualPath = await prompts.promptEntryPoint();
|
|
304
|
-
entryPoint = path.resolve(detected.cwd, manualPath);
|
|
305
|
-
|
|
306
|
-
if (!fs.existsSync(entryPoint)) {
|
|
307
|
-
error(`File not found: ${entryPoint}`);
|
|
308
|
-
process.exit(1);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
success(`Entry point: ${path.relative(detected.cwd, entryPoint)}`);
|
|
313
|
-
|
|
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
|
-
|
|
388
|
-
const generated = generator.generateExpressInit(detected, args.key);
|
|
389
|
-
|
|
390
|
-
// PATTERN 2: Separate app file with module.exports = app
|
|
391
|
-
if (detected.appFile) {
|
|
392
|
-
info(`Found app file: ${path.relative(detected.cwd, detected.appFile)}`);
|
|
393
|
-
const generated2 = generator.generateExpressInit(detected, args.key);
|
|
394
|
-
const result = writer.injectBeforeExport(
|
|
395
|
-
detected.appFile,
|
|
396
|
-
generated2.initBlock,
|
|
397
|
-
detected.appVarName,
|
|
398
|
-
);
|
|
399
|
-
|
|
400
|
-
if (result.success) {
|
|
401
|
-
success(
|
|
402
|
-
`Injected BotVersion.init() into ${path.relative(detected.cwd, detected.appFile)}`,
|
|
403
|
-
);
|
|
404
|
-
changes.modified.push(path.relative(detected.cwd, detected.appFile));
|
|
405
|
-
} else if (result.reason === "already_exists") {
|
|
406
|
-
warn("BotVersion already found — skipping injection.");
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// PATTERN 1 & 3 & 4: app.listen() in entry file
|
|
411
|
-
else if (
|
|
412
|
-
detected.listenCall ||
|
|
413
|
-
detected.listenInsideCallback ||
|
|
414
|
-
detected.createServer
|
|
415
|
-
) {
|
|
416
|
-
const result = writer.injectBeforeListen(
|
|
417
|
-
entryPoint,
|
|
418
|
-
generated.initBlock,
|
|
419
|
-
detected.appVarName,
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
if (result.success) {
|
|
423
|
-
success(`Injected BotVersion.init() before app.listen()`);
|
|
424
|
-
changes.modified.push(path.relative(detected.cwd, entryPoint));
|
|
425
|
-
if (result.backup) changes.backups.push(result.backup);
|
|
426
|
-
} else if (result.reason === "already_exists") {
|
|
427
|
-
warn("BotVersion already found in entry point — skipping injection.");
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// LAST RESORT: ask the user
|
|
432
|
-
else {
|
|
433
|
-
warn("Could not find the right place to inject automatically.");
|
|
434
|
-
const response = await prompts.promptMissingListenCall(
|
|
435
|
-
path.relative(detected.cwd, entryPoint),
|
|
436
|
-
);
|
|
437
|
-
|
|
438
|
-
if (response.action === "append") {
|
|
439
|
-
const result = writer.appendToFile(entryPoint, generated.initBlock);
|
|
440
|
-
if (result.success) {
|
|
441
|
-
success("Appended BotVersion setup to end of file.");
|
|
442
|
-
changes.modified.push(path.relative(detected.cwd, entryPoint));
|
|
443
|
-
}
|
|
444
|
-
} else if (response.action === "manual_path") {
|
|
445
|
-
const altPath = path.resolve(detected.cwd, response.filePath);
|
|
446
|
-
if (fs.existsSync(altPath)) {
|
|
447
|
-
const result = writer.injectBeforeListen(
|
|
448
|
-
altPath,
|
|
449
|
-
generated.initBlock,
|
|
450
|
-
detected.appVarName,
|
|
451
|
-
);
|
|
452
|
-
if (result.success) {
|
|
453
|
-
success(`Injected into ${response.filePath}`);
|
|
454
|
-
changes.modified.push(response.filePath);
|
|
455
|
-
}
|
|
456
|
-
} else {
|
|
457
|
-
error(`File not found: ${altPath}`);
|
|
458
|
-
changes.manual.push(
|
|
459
|
-
`Add this to your server file:\n${generated.initBlock}`,
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
} else {
|
|
463
|
-
changes.manual.push(
|
|
464
|
-
`Add this to your server file before app.listen():\n\n${generated.initBlock}`,
|
|
465
|
-
);
|
|
466
|
-
warn("Skipped — see manual steps below.");
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// ─── NEXT.JS SETUP ────────────────────────────────────────────────────────────
|
|
472
|
-
|
|
473
|
-
async function setupNextJs(detected, args, changes, projectInfo) {
|
|
474
|
-
step("Setting up Next.js...");
|
|
475
|
-
|
|
476
|
-
const nextInfo = detected.next;
|
|
477
|
-
const baseDir = nextInfo.baseDir;
|
|
478
|
-
|
|
479
|
-
// ── Write API key to .env ──────────────────────────────────────────────
|
|
480
|
-
const shouldWriteEnv = await prompts.confirm(
|
|
481
|
-
" Add BOTVERSION_API_KEY to your .env file automatically?",
|
|
482
|
-
true,
|
|
483
|
-
);
|
|
484
|
-
|
|
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
|
-
);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
info2(
|
|
513
|
-
`Router: ${nextInfo.pagesRouter ? "Pages" : ""}${nextInfo.pagesRouter && nextInfo.appRouter ? " + " : ""}${nextInfo.appRouter ? "App" : ""}`,
|
|
514
|
-
);
|
|
515
|
-
|
|
516
|
-
// ── 1. Create instrumentation.js ──────────────────────────────────────────
|
|
517
|
-
const instrExt = detected.generateTs ? ".ts" : ".js";
|
|
518
|
-
const instrFile = path.join(detected.cwd, `instrumentation${instrExt}`);
|
|
519
|
-
|
|
520
|
-
const instrContent = generator.generateInstrumentationFile(
|
|
521
|
-
detected,
|
|
522
|
-
args.key,
|
|
523
|
-
);
|
|
524
|
-
const instrResult = writer.createFile(instrFile, instrContent, args.force);
|
|
525
|
-
|
|
526
|
-
if (instrResult.success) {
|
|
527
|
-
success(`Created instrumentation${instrExt}`);
|
|
528
|
-
changes.created.push(`instrumentation${instrExt}`);
|
|
529
|
-
} else if (instrResult.reason === "already_exists") {
|
|
530
|
-
const overwrite = await prompts.promptForce(`instrumentation${instrExt}`);
|
|
531
|
-
if (overwrite) {
|
|
532
|
-
writer.createFile(instrFile, instrContent, true);
|
|
533
|
-
success(`Overwrote instrumentation${instrExt}`);
|
|
534
|
-
changes.modified.push(`instrumentation${instrExt}`);
|
|
535
|
-
} else {
|
|
536
|
-
warn(`Skipped instrumentation${instrExt}`);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// ── 2. Patch next.config.js ───────────────────────────────────────────────
|
|
541
|
-
const configPatch = generator.generateNextConfigPatch(
|
|
542
|
-
detected.cwd,
|
|
543
|
-
detected.nextVersion,
|
|
544
|
-
);
|
|
545
|
-
|
|
546
|
-
if (configPatch) {
|
|
547
|
-
if (configPatch.alreadyPatched) {
|
|
548
|
-
info("next.config.js already has instrumentationHook — skipping.");
|
|
549
|
-
} else {
|
|
550
|
-
fs.writeFileSync(configPatch.path, configPatch.content, "utf8");
|
|
551
|
-
success(`Updated ${path.relative(detected.cwd, configPatch.path)}`);
|
|
552
|
-
changes.modified.push(path.relative(detected.cwd, configPatch.path));
|
|
553
|
-
}
|
|
554
|
-
} else {
|
|
555
|
-
warn("Could not find next.config.js — please add this manually:");
|
|
556
|
-
changes.manual.push(
|
|
557
|
-
`Add to your next.config.js:\n\n experimental: {\n instrumentationHook: true,\n }`,
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// ── 3. Inject script tag into frontend ───────────────────────────────────
|
|
562
|
-
if (detected.frontendMainFile && projectInfo) {
|
|
563
|
-
const scriptTag = generator.generateScriptTag(projectInfo);
|
|
564
|
-
const result = writer.injectScriptTag(
|
|
565
|
-
detected.frontendMainFile.file,
|
|
566
|
-
detected.frontendMainFile.type,
|
|
567
|
-
scriptTag,
|
|
568
|
-
args.force,
|
|
569
|
-
);
|
|
570
|
-
|
|
571
|
-
if (result.success) {
|
|
572
|
-
success(
|
|
573
|
-
`Injected script tag into ${path.relative(detected.cwd, detected.frontendMainFile.file)}`,
|
|
574
|
-
);
|
|
575
|
-
changes.modified.push(
|
|
576
|
-
path.relative(detected.cwd, detected.frontendMainFile.file),
|
|
577
|
-
);
|
|
578
|
-
if (result.backup) changes.backups.push(result.backup);
|
|
579
|
-
} else if (result.reason === "already_exists") {
|
|
580
|
-
warn("BotVersion script tag already exists — skipping.");
|
|
581
|
-
} else {
|
|
582
|
-
warn(
|
|
583
|
-
"Could not auto-inject script tag. Add this manually to your frontend file:",
|
|
584
|
-
);
|
|
585
|
-
console.log("\n" + scriptTag + "\n");
|
|
586
|
-
changes.manual.push(
|
|
587
|
-
`Add to your frontend HTML before </body>:\n\n${scriptTag}`,
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
} else if (!detected.frontendMainFile) {
|
|
591
|
-
warn("Could not find frontend file automatically.");
|
|
592
|
-
const scriptTag = generator.generateScriptTag(projectInfo);
|
|
593
|
-
changes.manual.push(
|
|
594
|
-
`Add to your frontend HTML before </body>:\n\n${scriptTag}`,
|
|
595
|
-
);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// ─── RUN ──────────────────────────────────────────────────────────────────────
|
|
600
|
-
|
|
601
|
-
main().catch((err) => {
|
|
602
|
-
console.error(`\n${c.red} ✖ Unexpected error:${c.reset}`, err.message);
|
|
603
|
-
if (process.env.DEBUG) console.error(err.stack);
|
|
604
|
-
process.exit(1);
|
|
605
|
-
});
|