botversion-sdk 1.0.1 → 1.0.3
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 +130 -19
- package/cli/detector.js +409 -79
- package/cli/generator.js +30 -5
- package/cli/writer.js +190 -5
- package/index.js +1 -2
- package/package.json +1 -1
package/bin/init.js
CHANGED
|
@@ -45,6 +45,22 @@ function step(msg) {
|
|
|
45
45
|
console.log(`\n${c.bold}${c.white} → ${msg}${c.reset}`);
|
|
46
46
|
}
|
|
47
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
|
+
|
|
48
64
|
// ─── PARSE ARGS ───────────────────────────────────────────────────────────────
|
|
49
65
|
|
|
50
66
|
function parseArgs(argv) {
|
|
@@ -93,23 +109,30 @@ async function main() {
|
|
|
93
109
|
const cwd = args.cwd;
|
|
94
110
|
const changes = { modified: [], created: [], backups: [], manual: [] };
|
|
95
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
|
+
|
|
96
123
|
// ── Detect environment ────────────────────────────────────────────────────
|
|
97
124
|
step("Scanning your project...");
|
|
98
125
|
|
|
99
126
|
// Handle monorepo
|
|
100
|
-
let workingDir = cwd;
|
|
101
127
|
const monorepoInfo = detector.detectMonorepo(cwd);
|
|
102
128
|
if (monorepoInfo.isMonorepo) {
|
|
103
|
-
warn(
|
|
104
|
-
|
|
105
|
-
monorepoInfo.packages,
|
|
106
|
-
cwd,
|
|
129
|
+
warn(
|
|
130
|
+
"Monorepo detected — will scan all packages for frontend and backend.",
|
|
107
131
|
);
|
|
108
|
-
info(`Using package: ${path.relative(cwd, workingDir) || "root"}`);
|
|
109
132
|
}
|
|
110
133
|
|
|
111
|
-
//
|
|
112
|
-
const detected = detector.detect(
|
|
134
|
+
// Always detect from root so frontend/backend split works correctly
|
|
135
|
+
const detected = detector.detect(cwd);
|
|
113
136
|
|
|
114
137
|
// ── Check if already initialized ─────────────────────────────────────────
|
|
115
138
|
if (detected.alreadyInitialized && !args.force) {
|
|
@@ -174,20 +197,60 @@ async function main() {
|
|
|
174
197
|
// FRAMEWORK: EXPRESS
|
|
175
198
|
// ─────────────────────────────────────────────────────────────────────────
|
|
176
199
|
if (detected.framework.name === "express") {
|
|
177
|
-
await setupExpress(detected, args, changes);
|
|
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
|
+
}
|
|
178
241
|
}
|
|
179
242
|
|
|
180
243
|
// ─────────────────────────────────────────────────────────────────────────
|
|
181
244
|
// FRAMEWORK: NEXT.JS
|
|
182
245
|
// ─────────────────────────────────────────────────────────────────────────
|
|
183
246
|
else if (detected.framework.name === "next") {
|
|
184
|
-
await setupNextJs(detected, args, changes);
|
|
247
|
+
await setupNextJs(detected, args, changes, projectInfo);
|
|
185
248
|
}
|
|
186
249
|
|
|
187
250
|
// ── Write API key to .env / .env.local ────────────────────────────────────
|
|
188
251
|
const envFileName =
|
|
189
252
|
detected.framework.name === "next" ? ".env.local" : ".env";
|
|
190
|
-
const envPath = path.join(
|
|
253
|
+
const envPath = path.join(detected.cwd, envFileName);
|
|
191
254
|
const envLine = `BOTVERSION_API_KEY=${args.key}`;
|
|
192
255
|
const envContent = fs.existsSync(envPath)
|
|
193
256
|
? fs.readFileSync(envPath, "utf8")
|
|
@@ -222,7 +285,7 @@ async function main() {
|
|
|
222
285
|
|
|
223
286
|
// ─── EXPRESS SETUP ────────────────────────────────────────────────────────────
|
|
224
287
|
|
|
225
|
-
async function setupExpress(detected, args, changes) {
|
|
288
|
+
async function setupExpress(detected, args, changes, projectInfo) {
|
|
226
289
|
step("Setting up Express...");
|
|
227
290
|
|
|
228
291
|
// Find entry point
|
|
@@ -244,9 +307,6 @@ async function setupExpress(detected, args, changes) {
|
|
|
244
307
|
// Generate the init code
|
|
245
308
|
const generated = generator.generateExpressInit(detected, args.key);
|
|
246
309
|
|
|
247
|
-
// Find app.listen() and inject before it
|
|
248
|
-
const listenCall = detector.findListenCall(entryPoint);
|
|
249
|
-
|
|
250
310
|
// PATTERN 2: Separate app file with module.exports = app
|
|
251
311
|
if (detected.appFile) {
|
|
252
312
|
info(`Found app file: ${path.relative(detected.cwd, detected.appFile)}`);
|
|
@@ -254,6 +314,7 @@ async function setupExpress(detected, args, changes) {
|
|
|
254
314
|
const result = writer.injectBeforeExport(
|
|
255
315
|
detected.appFile,
|
|
256
316
|
generated2.initBlock,
|
|
317
|
+
detected.appVarName,
|
|
257
318
|
);
|
|
258
319
|
|
|
259
320
|
if (result.success) {
|
|
@@ -272,7 +333,11 @@ async function setupExpress(detected, args, changes) {
|
|
|
272
333
|
detected.listenInsideCallback ||
|
|
273
334
|
detected.createServer
|
|
274
335
|
) {
|
|
275
|
-
const result = writer.injectBeforeListen(
|
|
336
|
+
const result = writer.injectBeforeListen(
|
|
337
|
+
entryPoint,
|
|
338
|
+
generated.initBlock,
|
|
339
|
+
detected.appVarName,
|
|
340
|
+
);
|
|
276
341
|
|
|
277
342
|
if (result.success) {
|
|
278
343
|
success(`Injected BotVersion.init() before app.listen()`);
|
|
@@ -299,7 +364,11 @@ async function setupExpress(detected, args, changes) {
|
|
|
299
364
|
} else if (response.action === "manual_path") {
|
|
300
365
|
const altPath = path.resolve(detected.cwd, response.filePath);
|
|
301
366
|
if (fs.existsSync(altPath)) {
|
|
302
|
-
const result = writer.injectBeforeListen(
|
|
367
|
+
const result = writer.injectBeforeListen(
|
|
368
|
+
altPath,
|
|
369
|
+
generated.initBlock,
|
|
370
|
+
detected.appVarName,
|
|
371
|
+
);
|
|
303
372
|
if (result.success) {
|
|
304
373
|
success(`Injected into ${response.filePath}`);
|
|
305
374
|
changes.modified.push(response.filePath);
|
|
@@ -321,7 +390,7 @@ async function setupExpress(detected, args, changes) {
|
|
|
321
390
|
|
|
322
391
|
// ─── NEXT.JS SETUP ────────────────────────────────────────────────────────────
|
|
323
392
|
|
|
324
|
-
async function setupNextJs(detected, args, changes) {
|
|
393
|
+
async function setupNextJs(detected, args, changes, projectInfo) {
|
|
325
394
|
step("Setting up Next.js...");
|
|
326
395
|
|
|
327
396
|
const nextInfo = detected.next;
|
|
@@ -366,7 +435,10 @@ async function setupNextJs(detected, args, changes) {
|
|
|
366
435
|
}
|
|
367
436
|
|
|
368
437
|
// ── 2. Patch next.config.js ───────────────────────────────────────────────
|
|
369
|
-
const configPatch = generator.generateNextConfigPatch(
|
|
438
|
+
const configPatch = generator.generateNextConfigPatch(
|
|
439
|
+
detected.cwd,
|
|
440
|
+
detected.nextVersion,
|
|
441
|
+
);
|
|
370
442
|
|
|
371
443
|
if (configPatch) {
|
|
372
444
|
if (configPatch.alreadyPatched) {
|
|
@@ -434,6 +506,45 @@ async function setupNextJs(detected, args, changes) {
|
|
|
434
506
|
}
|
|
435
507
|
}
|
|
436
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
|
|
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
|
+
}
|
|
437
548
|
}
|
|
438
549
|
|
|
439
550
|
// ─── RUN ──────────────────────────────────────────────────────────────────────
|
package/cli/detector.js
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
// botversion-sdk/cli/detector.js
|
|
2
|
-
|
|
3
2
|
"use strict";
|
|
4
3
|
|
|
5
4
|
const fs = require("fs");
|
|
6
5
|
const path = require("path");
|
|
7
6
|
|
|
7
|
+
// ─── SKIP DIRS (used everywhere) ─────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const SKIP_DIRS = [
|
|
10
|
+
"node_modules",
|
|
11
|
+
".git",
|
|
12
|
+
".next",
|
|
13
|
+
"dist",
|
|
14
|
+
"build",
|
|
15
|
+
".cache",
|
|
16
|
+
"coverage",
|
|
17
|
+
".turbo",
|
|
18
|
+
"out",
|
|
19
|
+
".output",
|
|
20
|
+
".svelte-kit",
|
|
21
|
+
];
|
|
22
|
+
|
|
8
23
|
// ─── PACKAGE JSON ────────────────────────────────────────────────────────────
|
|
9
24
|
|
|
10
25
|
function readPackageJson(cwd) {
|
|
@@ -17,22 +32,286 @@ function readPackageJson(cwd) {
|
|
|
17
32
|
}
|
|
18
33
|
}
|
|
19
34
|
|
|
35
|
+
// ─── SCAN ALL PACKAGE.JSON FILES ─────────────────────────────────────────────
|
|
36
|
+
// Recursively finds ALL package.json files in the project
|
|
37
|
+
|
|
38
|
+
function scanAllPackageJsons(cwd) {
|
|
39
|
+
const results = []; // [{ dir, pkg }]
|
|
40
|
+
|
|
41
|
+
function walk(currentDir, depth) {
|
|
42
|
+
if (depth > 5) return;
|
|
43
|
+
|
|
44
|
+
let entries;
|
|
45
|
+
try {
|
|
46
|
+
entries = fs.readdirSync(currentDir);
|
|
47
|
+
} catch {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (SKIP_DIRS.includes(entry)) continue;
|
|
53
|
+
|
|
54
|
+
const fullPath = path.join(currentDir, entry);
|
|
55
|
+
let stat;
|
|
56
|
+
try {
|
|
57
|
+
stat = fs.statSync(fullPath);
|
|
58
|
+
} catch {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (stat.isDirectory()) {
|
|
63
|
+
walk(fullPath, depth + 1);
|
|
64
|
+
} else if (entry === "package.json") {
|
|
65
|
+
try {
|
|
66
|
+
const pkg = JSON.parse(fs.readFileSync(fullPath, "utf8"));
|
|
67
|
+
results.push({ dir: currentDir, pkg });
|
|
68
|
+
} catch {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
walk(cwd, 0);
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── CLASSIFY PACKAGE.JSON ────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
const BACKEND_PACKAGES = [
|
|
82
|
+
"express",
|
|
83
|
+
"fastify",
|
|
84
|
+
"koa",
|
|
85
|
+
"@nestjs/core",
|
|
86
|
+
"@hapi/hapi",
|
|
87
|
+
"restify",
|
|
88
|
+
"polka",
|
|
89
|
+
"micro",
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const FULLSTACK_PACKAGES = ["next", "@sveltejs/kit"];
|
|
93
|
+
|
|
94
|
+
const FRONTEND_PACKAGES = [
|
|
95
|
+
"react",
|
|
96
|
+
"react-dom",
|
|
97
|
+
"vue",
|
|
98
|
+
"@angular/core",
|
|
99
|
+
"svelte",
|
|
100
|
+
"@sveltejs/kit",
|
|
101
|
+
"solid-js",
|
|
102
|
+
"preact",
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
function classifyPackageJson(pkg) {
|
|
106
|
+
if (!pkg) return "unknown";
|
|
107
|
+
|
|
108
|
+
const deps = {
|
|
109
|
+
...(pkg.dependencies || {}),
|
|
110
|
+
...(pkg.devDependencies || {}),
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const isFullstack = FULLSTACK_PACKAGES.some((p) => !!deps[p]);
|
|
114
|
+
if (isFullstack) return "fullstack";
|
|
115
|
+
|
|
116
|
+
const isBackend = BACKEND_PACKAGES.some((p) => !!deps[p]);
|
|
117
|
+
const isFrontend = FRONTEND_PACKAGES.some((p) => !!deps[p]);
|
|
118
|
+
|
|
119
|
+
if (isBackend && isFrontend) return "fullstack";
|
|
120
|
+
if (isBackend) return "backend";
|
|
121
|
+
if (isFrontend) return "frontend";
|
|
122
|
+
return "unknown";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ─── DETECT FRONTEND FRAMEWORK ───────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
function detectFrontendFramework(pkg) {
|
|
128
|
+
if (!pkg) return null;
|
|
129
|
+
|
|
130
|
+
const deps = {
|
|
131
|
+
...(pkg.dependencies || {}),
|
|
132
|
+
...(pkg.devDependencies || {}),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
if (deps["next"]) return "next";
|
|
136
|
+
if (deps["@sveltejs/kit"]) return "sveltekit";
|
|
137
|
+
if (deps["svelte"]) return "svelte";
|
|
138
|
+
if (deps["@angular/core"]) return "angular";
|
|
139
|
+
if (deps["vue"]) return "vue";
|
|
140
|
+
if (deps["react-dom"] || deps["react"]) {
|
|
141
|
+
// Distinguish CRA vs Vite
|
|
142
|
+
if (deps["vite"] || deps["@vitejs/plugin-react"]) return "react-vite";
|
|
143
|
+
return "react-cra";
|
|
144
|
+
}
|
|
145
|
+
if (deps["solid-js"]) return "solid";
|
|
146
|
+
if (deps["preact"]) return "preact";
|
|
147
|
+
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─── FIND MAIN FRONTEND FILE ──────────────────────────────────────────────────
|
|
152
|
+
// Returns { file, type } or null
|
|
153
|
+
|
|
154
|
+
function findMainFrontendFile(dir, pkg) {
|
|
155
|
+
const framework = detectFrontendFramework(pkg);
|
|
156
|
+
|
|
157
|
+
// ── Next.js ───────────────────────────────────────────────────────────────
|
|
158
|
+
// For Next.js we inject into _app.js (Pages) or layout.js (App Router)
|
|
159
|
+
if (framework === "next") {
|
|
160
|
+
const candidates = [
|
|
161
|
+
"pages/_app.js",
|
|
162
|
+
"pages/_app.tsx",
|
|
163
|
+
"pages/_app.ts",
|
|
164
|
+
"src/pages/_app.js",
|
|
165
|
+
"src/pages/_app.tsx",
|
|
166
|
+
"src/pages/_app.ts",
|
|
167
|
+
"app/layout.js",
|
|
168
|
+
"app/layout.tsx",
|
|
169
|
+
"src/app/layout.js",
|
|
170
|
+
"src/app/layout.tsx",
|
|
171
|
+
];
|
|
172
|
+
for (const candidate of candidates) {
|
|
173
|
+
const fullPath = path.join(dir, candidate);
|
|
174
|
+
if (fs.existsSync(fullPath)) {
|
|
175
|
+
return { file: fullPath, type: "next" };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── Angular ───────────────────────────────────────────────────────────────
|
|
182
|
+
if (framework === "angular") {
|
|
183
|
+
const candidate = path.join(dir, "src", "index.html");
|
|
184
|
+
if (fs.existsSync(candidate)) {
|
|
185
|
+
return { file: candidate, type: "html" };
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── React Vite / Vue Vite / Svelte / SvelteKit / Solid / Preact ──────────
|
|
191
|
+
// All Vite-based projects have index.html in root of the project folder
|
|
192
|
+
if (
|
|
193
|
+
framework === "react-vite" ||
|
|
194
|
+
framework === "vue" ||
|
|
195
|
+
framework === "svelte" ||
|
|
196
|
+
framework === "sveltekit" ||
|
|
197
|
+
framework === "solid" ||
|
|
198
|
+
framework === "preact"
|
|
199
|
+
) {
|
|
200
|
+
// Check root index.html first
|
|
201
|
+
const rootHtml = path.join(dir, "index.html");
|
|
202
|
+
if (fs.existsSync(rootHtml)) {
|
|
203
|
+
return { file: rootHtml, type: "html" };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Fallback: public/index.html
|
|
207
|
+
const publicHtml = path.join(dir, "public", "index.html");
|
|
208
|
+
if (fs.existsSync(publicHtml)) {
|
|
209
|
+
return { file: publicHtml, type: "html" };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── React CRA ─────────────────────────────────────────────────────────────
|
|
216
|
+
if (framework === "react-cra") {
|
|
217
|
+
// CRA always puts index.html in public/
|
|
218
|
+
const publicHtml = path.join(dir, "public", "index.html");
|
|
219
|
+
if (fs.existsSync(publicHtml)) {
|
|
220
|
+
return { file: publicHtml, type: "html" };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Fallback: root index.html (custom CRA config)
|
|
224
|
+
const rootHtml = path.join(dir, "index.html");
|
|
225
|
+
if (fs.existsSync(rootHtml)) {
|
|
226
|
+
return { file: rootHtml, type: "html" };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── Unknown frontend — scan for any index.html ────────────────────────────
|
|
233
|
+
const htmlCandidates = [
|
|
234
|
+
"index.html",
|
|
235
|
+
"public/index.html",
|
|
236
|
+
"src/index.html",
|
|
237
|
+
"static/index.html",
|
|
238
|
+
"www/index.html",
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
for (const candidate of htmlCandidates) {
|
|
242
|
+
const fullPath = path.join(dir, candidate);
|
|
243
|
+
if (fs.existsSync(fullPath)) {
|
|
244
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
245
|
+
// Make sure it's a real HTML file with a body tag
|
|
246
|
+
if (content.includes("<body") || content.includes("<html")) {
|
|
247
|
+
return { file: fullPath, type: "html" };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Last resort — deep scan for any .html file
|
|
253
|
+
const found = findHtmlFile(dir);
|
|
254
|
+
if (found) return { file: found, type: "html" };
|
|
255
|
+
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ─── DEEP SCAN FOR HTML FILE ─────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
function findHtmlFile(dir) {
|
|
262
|
+
function walk(currentDir, depth) {
|
|
263
|
+
if (depth > 3) return null;
|
|
264
|
+
|
|
265
|
+
let entries;
|
|
266
|
+
try {
|
|
267
|
+
entries = fs.readdirSync(currentDir);
|
|
268
|
+
} catch {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const entry of entries) {
|
|
273
|
+
if (SKIP_DIRS.includes(entry)) continue;
|
|
274
|
+
|
|
275
|
+
const fullPath = path.join(currentDir, entry);
|
|
276
|
+
let stat;
|
|
277
|
+
try {
|
|
278
|
+
stat = fs.statSync(fullPath);
|
|
279
|
+
} catch {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (stat.isDirectory()) {
|
|
284
|
+
const result = walk(fullPath, depth + 1);
|
|
285
|
+
if (result) return result;
|
|
286
|
+
} else if (entry.endsWith(".html")) {
|
|
287
|
+
try {
|
|
288
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
289
|
+
if (content.includes("<body") || content.includes("<html")) {
|
|
290
|
+
return fullPath;
|
|
291
|
+
}
|
|
292
|
+
} catch {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return walk(dir, 0);
|
|
302
|
+
}
|
|
303
|
+
|
|
20
304
|
// ─── MONOREPO DETECTION ──────────────────────────────────────────────────────
|
|
21
305
|
|
|
22
306
|
function detectMonorepo(cwd) {
|
|
23
|
-
const entries = fs.readdirSync(cwd);
|
|
24
|
-
|
|
25
|
-
// Check for workspaces in root package.json
|
|
26
307
|
const rootPkg = readPackageJson(cwd);
|
|
27
308
|
if (rootPkg && rootPkg.workspaces) {
|
|
28
|
-
// Find all workspace package.json files
|
|
29
309
|
const workspaceDirs = [];
|
|
30
310
|
const patterns = Array.isArray(rootPkg.workspaces)
|
|
31
311
|
? rootPkg.workspaces
|
|
32
312
|
: rootPkg.workspaces.packages || [];
|
|
33
313
|
|
|
34
314
|
patterns.forEach((pattern) => {
|
|
35
|
-
// Handle simple patterns like "packages/*"
|
|
36
315
|
const base = pattern.replace(/\/\*$/, "");
|
|
37
316
|
const fullBase = path.join(cwd, base);
|
|
38
317
|
if (fs.existsSync(fullBase)) {
|
|
@@ -94,17 +373,12 @@ function detectFramework(pkg) {
|
|
|
94
373
|
...(pkg.devDependencies || {}),
|
|
95
374
|
};
|
|
96
375
|
|
|
97
|
-
// Check unsupported first so we can warn clearly
|
|
98
376
|
for (const fw of UNSUPPORTED_FRAMEWORKS) {
|
|
99
|
-
if (deps[fw]) {
|
|
100
|
-
return { name: fw, supported: false };
|
|
101
|
-
}
|
|
377
|
+
if (deps[fw]) return { name: fw, supported: false };
|
|
102
378
|
}
|
|
103
379
|
|
|
104
380
|
for (const fw of SUPPORTED_FRAMEWORKS) {
|
|
105
|
-
if (deps[fw]) {
|
|
106
|
-
return { name: fw, supported: true };
|
|
107
|
-
}
|
|
381
|
+
if (deps[fw]) return { name: fw, supported: true };
|
|
108
382
|
}
|
|
109
383
|
|
|
110
384
|
return { name: null, supported: false };
|
|
@@ -131,7 +405,6 @@ function readTsConfig(cwd) {
|
|
|
131
405
|
if (!fs.existsSync(tsconfigPath)) return null;
|
|
132
406
|
try {
|
|
133
407
|
const raw = fs.readFileSync(tsconfigPath, "utf8");
|
|
134
|
-
// Strip comments — tsconfig supports JSON with comments
|
|
135
408
|
const stripped = raw
|
|
136
409
|
.replace(/\/\/.*$/gm, "")
|
|
137
410
|
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
@@ -141,11 +414,6 @@ function readTsConfig(cwd) {
|
|
|
141
414
|
}
|
|
142
415
|
}
|
|
143
416
|
|
|
144
|
-
// Decide whether to generate .ts or .js files
|
|
145
|
-
// - Not TypeScript → always .js
|
|
146
|
-
// - TypeScript + allowJs: true (Next.js default) → .js is fine
|
|
147
|
-
// - TypeScript + allowJs: false (manually set by user) → must use .ts
|
|
148
|
-
// - TypeScript + allowJs not set → Next.js default is true → .js is fine
|
|
149
417
|
function shouldGenerateTs(cwd, isTypeScript) {
|
|
150
418
|
if (!isTypeScript) return false;
|
|
151
419
|
const tsconfig = readTsConfig(cwd);
|
|
@@ -179,7 +447,6 @@ function detectNextRouter(cwd) {
|
|
|
179
447
|
pagesRouter: hasPages,
|
|
180
448
|
appRouter: hasApp,
|
|
181
449
|
srcDir: hasSrc,
|
|
182
|
-
// Resolve the actual base directory
|
|
183
450
|
baseDir: hasSrc ? path.join(cwd, "src") : cwd,
|
|
184
451
|
};
|
|
185
452
|
}
|
|
@@ -209,7 +476,6 @@ function detectExpressEntry(cwd, pkg) {
|
|
|
209
476
|
const scripts = [pkg.scripts.start, pkg.scripts.dev, pkg.scripts.serve];
|
|
210
477
|
for (const script of scripts) {
|
|
211
478
|
if (!script) continue;
|
|
212
|
-
// e.g. "node server.js" or "nodemon src/index.js" or "ts-node index.ts"
|
|
213
479
|
const match = script.match(
|
|
214
480
|
/(?:node|nodemon|ts-node|tsx)\s+([^\s]+\.(js|ts))/,
|
|
215
481
|
);
|
|
@@ -220,7 +486,7 @@ function detectExpressEntry(cwd, pkg) {
|
|
|
220
486
|
}
|
|
221
487
|
}
|
|
222
488
|
|
|
223
|
-
// Strategy 3: common file names
|
|
489
|
+
// Strategy 3: common file names
|
|
224
490
|
const candidates = [
|
|
225
491
|
"server.js",
|
|
226
492
|
"server.ts",
|
|
@@ -243,7 +509,6 @@ function detectExpressEntry(cwd, pkg) {
|
|
|
243
509
|
for (const candidate of candidates) {
|
|
244
510
|
const filePath = path.join(cwd, candidate);
|
|
245
511
|
if (fs.existsSync(filePath)) {
|
|
246
|
-
// Verify it actually contains express
|
|
247
512
|
const content = fs.readFileSync(filePath, "utf8");
|
|
248
513
|
if (content.includes("express") || content.includes("app.listen")) {
|
|
249
514
|
return filePath;
|
|
@@ -252,17 +517,19 @@ function detectExpressEntry(cwd, pkg) {
|
|
|
252
517
|
}
|
|
253
518
|
|
|
254
519
|
// Strategy 4: any .js/.ts file containing app.listen()
|
|
255
|
-
return findFileWithContent(cwd, "
|
|
520
|
+
return findFileWithContent(cwd, ".listen(", [".js", ".ts"], 2);
|
|
256
521
|
}
|
|
257
522
|
|
|
258
523
|
// ─── app.listen() LOCATION ───────────────────────────────────────────────────
|
|
259
524
|
|
|
260
|
-
function findListenCall(filePath) {
|
|
525
|
+
function findListenCall(filePath, appVarName) {
|
|
526
|
+
appVarName = appVarName || "app";
|
|
261
527
|
const content = fs.readFileSync(filePath, "utf8");
|
|
262
528
|
const lines = content.split("\n");
|
|
529
|
+
const regex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
|
|
263
530
|
|
|
264
531
|
for (let i = 0; i < lines.length; i++) {
|
|
265
|
-
if (
|
|
532
|
+
if (regex.test(lines[i])) {
|
|
266
533
|
return { lineIndex: i, lineNumber: i + 1, content: lines[i] };
|
|
267
534
|
}
|
|
268
535
|
}
|
|
@@ -280,12 +547,14 @@ function findModuleExportsApp(filePath) {
|
|
|
280
547
|
return null;
|
|
281
548
|
}
|
|
282
549
|
|
|
283
|
-
function findListenInsideCallback(filePath) {
|
|
550
|
+
function findListenInsideCallback(filePath, appVarName) {
|
|
551
|
+
appVarName = appVarName || "app";
|
|
284
552
|
const content = fs.readFileSync(filePath, "utf8");
|
|
285
553
|
const lines = content.split("\n");
|
|
554
|
+
const regex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
|
|
555
|
+
|
|
286
556
|
for (let i = 0; i < lines.length; i++) {
|
|
287
|
-
if (
|
|
288
|
-
// Check if it's inside a callback (indented or preceded by .then)
|
|
557
|
+
if (regex.test(lines[i])) {
|
|
289
558
|
const indentation = lines[i].match(/^(\s*)/)[1].length;
|
|
290
559
|
if (indentation > 0) {
|
|
291
560
|
return { lineIndex: i, lineNumber: i + 1, insideCallback: true };
|
|
@@ -392,12 +661,7 @@ function detectAuth(pkg) {
|
|
|
392
661
|
"jwt",
|
|
393
662
|
"express-session",
|
|
394
663
|
].includes(lib.name);
|
|
395
|
-
return {
|
|
396
|
-
name: lib.name,
|
|
397
|
-
version,
|
|
398
|
-
package: pkg2,
|
|
399
|
-
supported,
|
|
400
|
-
};
|
|
664
|
+
return { name: lib.name, version, package: pkg2, supported };
|
|
401
665
|
}
|
|
402
666
|
}
|
|
403
667
|
|
|
@@ -407,7 +671,6 @@ function detectAuth(pkg) {
|
|
|
407
671
|
// ─── NEXT-AUTH CONFIG LOCATION ───────────────────────────────────────────────
|
|
408
672
|
|
|
409
673
|
function findNextAuthConfig(cwd) {
|
|
410
|
-
// Common locations for authOptions
|
|
411
674
|
const candidates = [
|
|
412
675
|
"pages/api/auth/[...nextauth].js",
|
|
413
676
|
"pages/api/auth/[...nextauth].ts",
|
|
@@ -424,7 +687,7 @@ function findNextAuthConfig(cwd) {
|
|
|
424
687
|
"utils/auth.js",
|
|
425
688
|
"utils/auth.ts",
|
|
426
689
|
"auth.js",
|
|
427
|
-
"auth.ts",
|
|
690
|
+
"auth.ts",
|
|
428
691
|
];
|
|
429
692
|
|
|
430
693
|
for (const candidate of candidates) {
|
|
@@ -434,13 +697,9 @@ function findNextAuthConfig(cwd) {
|
|
|
434
697
|
}
|
|
435
698
|
}
|
|
436
699
|
|
|
437
|
-
// Search for authOptions in files
|
|
438
700
|
const found = findFileWithContent(cwd, "authOptions", [".js", ".ts"], 3);
|
|
439
701
|
if (found) {
|
|
440
|
-
return {
|
|
441
|
-
path: found,
|
|
442
|
-
relativePath: path.relative(cwd, found),
|
|
443
|
-
};
|
|
702
|
+
return { path: found, relativePath: path.relative(cwd, found) };
|
|
444
703
|
}
|
|
445
704
|
|
|
446
705
|
return null;
|
|
@@ -471,16 +730,6 @@ function findFileWithContent(dir, searchString, extensions, maxDepth) {
|
|
|
471
730
|
function walk(currentDir, depth) {
|
|
472
731
|
if (depth > maxDepth) return null;
|
|
473
732
|
|
|
474
|
-
// Skip node_modules, .git, .next, dist, build
|
|
475
|
-
const skipDirs = [
|
|
476
|
-
"node_modules",
|
|
477
|
-
".git",
|
|
478
|
-
".next",
|
|
479
|
-
"dist",
|
|
480
|
-
"build",
|
|
481
|
-
".cache",
|
|
482
|
-
];
|
|
483
|
-
|
|
484
733
|
let entries;
|
|
485
734
|
try {
|
|
486
735
|
entries = fs.readdirSync(currentDir);
|
|
@@ -489,7 +738,7 @@ function findFileWithContent(dir, searchString, extensions, maxDepth) {
|
|
|
489
738
|
}
|
|
490
739
|
|
|
491
740
|
for (const entry of entries) {
|
|
492
|
-
if (
|
|
741
|
+
if (SKIP_DIRS.includes(entry)) continue;
|
|
493
742
|
|
|
494
743
|
const fullPath = path.join(currentDir, entry);
|
|
495
744
|
let stat;
|
|
@@ -523,7 +772,7 @@ function detectAppVarName(filePath) {
|
|
|
523
772
|
const content = fs.readFileSync(filePath, "utf8");
|
|
524
773
|
const match = content.match(/(?:const|let|var)\s+(\w+)\s*=\s*express\s*\(/);
|
|
525
774
|
return match ? match[1] : "app";
|
|
526
|
-
} catch
|
|
775
|
+
} catch {
|
|
527
776
|
return "app";
|
|
528
777
|
}
|
|
529
778
|
}
|
|
@@ -533,18 +782,90 @@ function detectAppVarName(filePath) {
|
|
|
533
782
|
function detect(cwd) {
|
|
534
783
|
const pkg = readPackageJson(cwd);
|
|
535
784
|
const monorepo = detectMonorepo(cwd);
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
785
|
+
let framework = detectFramework(pkg);
|
|
786
|
+
|
|
787
|
+
// ── If framework not found in root, scan ALL package.json files ───────────
|
|
788
|
+
let backendDir = cwd;
|
|
789
|
+
let frontendDir = null;
|
|
790
|
+
let frontendPkg = null;
|
|
791
|
+
|
|
792
|
+
if (!framework.name) {
|
|
793
|
+
const allPackages = scanAllPackageJsons(cwd);
|
|
794
|
+
|
|
795
|
+
for (const { dir, pkg: subPkg } of allPackages) {
|
|
796
|
+
// Skip the root package.json — already checked
|
|
797
|
+
if (dir === cwd) continue;
|
|
798
|
+
|
|
799
|
+
const classification = classifyPackageJson(subPkg);
|
|
800
|
+
|
|
801
|
+
// AFTER
|
|
802
|
+
if (
|
|
803
|
+
(classification === "backend" || classification === "fullstack") &&
|
|
804
|
+
!framework.name
|
|
805
|
+
) {
|
|
806
|
+
framework = detectFramework(subPkg);
|
|
807
|
+
backendDir = dir;
|
|
808
|
+
}
|
|
542
809
|
|
|
543
|
-
|
|
810
|
+
if (
|
|
811
|
+
(classification === "frontend" || classification === "fullstack") &&
|
|
812
|
+
!frontendDir &&
|
|
813
|
+
dir !== backendDir
|
|
814
|
+
) {
|
|
815
|
+
frontendDir = dir;
|
|
816
|
+
frontendPkg = subPkg;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
} else {
|
|
820
|
+
// Framework found in root — scan for separate frontend folder
|
|
821
|
+
const allPackages = scanAllPackageJsons(cwd);
|
|
822
|
+
for (const { dir, pkg: subPkg } of allPackages) {
|
|
823
|
+
if (dir === cwd) continue;
|
|
824
|
+
if (dir === backendDir) continue;
|
|
825
|
+
const classification = classifyPackageJson(subPkg);
|
|
826
|
+
if (classification === "frontend" || classification === "fullstack") {
|
|
827
|
+
frontendDir = dir;
|
|
828
|
+
frontendPkg = subPkg;
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// ── Use backendDir for all backend-specific detection ─────────────────────
|
|
835
|
+
// Guard: if frontendDir ended up being the same as backendDir
|
|
836
|
+
// (e.g. a fullstack Next.js folder detected as both), clear frontendDir
|
|
837
|
+
// so we don't try to inject script tag into the wrong place
|
|
838
|
+
if (frontendDir && frontendDir === backendDir) {
|
|
839
|
+
frontendDir = null;
|
|
840
|
+
frontendPkg = null;
|
|
841
|
+
}
|
|
842
|
+
const backendPkg = readPackageJson(backendDir) || pkg;
|
|
843
|
+
const moduleSystem = detectModuleSystem(backendPkg);
|
|
844
|
+
const isTypeScript = detectTypeScript(backendDir);
|
|
845
|
+
const hasSrc = detectSrcDir(backendDir);
|
|
846
|
+
const auth = detectAuth(backendPkg);
|
|
847
|
+
const generateTs = shouldGenerateTs(backendDir, isTypeScript);
|
|
848
|
+
|
|
849
|
+
// ── Find frontend main file ───────────────────────────────────────────────
|
|
850
|
+
let frontendMainFile = null;
|
|
851
|
+
if (frontendDir && frontendPkg) {
|
|
852
|
+
frontendMainFile = findMainFrontendFile(frontendDir, frontendPkg);
|
|
853
|
+
} else if (framework.name === "next") {
|
|
854
|
+
frontendMainFile = findMainFrontendFile(backendDir, backendPkg);
|
|
855
|
+
} else if (framework.name === "express" && !frontendDir) {
|
|
856
|
+
// Express + frontend in same root folder (e.g. public/index.html)
|
|
857
|
+
frontendMainFile = findMainFrontendFile(backendDir, backendPkg);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const packageManager =
|
|
861
|
+
detectPackageManager(backendDir) !== "npm"
|
|
862
|
+
? detectPackageManager(backendDir)
|
|
863
|
+
: detectPackageManager(cwd);
|
|
544
864
|
|
|
545
865
|
const result = {
|
|
546
|
-
cwd,
|
|
547
|
-
|
|
866
|
+
cwd: backendDir, // use backend dir as working dir
|
|
867
|
+
rootCwd: cwd, // keep original root for reference
|
|
868
|
+
pkg: backendPkg,
|
|
548
869
|
monorepo,
|
|
549
870
|
framework,
|
|
550
871
|
moduleSystem,
|
|
@@ -553,30 +874,34 @@ function detect(cwd) {
|
|
|
553
874
|
hasSrc,
|
|
554
875
|
auth,
|
|
555
876
|
packageManager,
|
|
556
|
-
// generateTs: true means user has allowJs:false — must use .ts
|
|
557
|
-
// generateTs: false means .js files are fine (most users)
|
|
558
877
|
ext: generateTs ? ".ts" : ".js",
|
|
878
|
+
// Frontend info
|
|
879
|
+
frontendDir,
|
|
880
|
+
frontendPkg,
|
|
881
|
+
frontendMainFile,
|
|
559
882
|
};
|
|
560
883
|
|
|
561
|
-
// Framework-specific detection
|
|
884
|
+
// ── Framework-specific detection ──────────────────────────────────────────
|
|
562
885
|
if (framework.name === "next") {
|
|
563
|
-
result.next = detectNextRouter(
|
|
564
|
-
result.nextVersion = detectNextVersion(
|
|
886
|
+
result.next = detectNextRouter(backendDir);
|
|
887
|
+
result.nextVersion = detectNextVersion(backendPkg);
|
|
565
888
|
if (auth.name === "next-auth") {
|
|
566
|
-
result.nextAuthConfig = findNextAuthConfig(
|
|
889
|
+
result.nextAuthConfig = findNextAuthConfig(backendDir);
|
|
567
890
|
}
|
|
568
891
|
}
|
|
569
892
|
|
|
570
893
|
if (framework.name === "express") {
|
|
571
|
-
result.entryPoint = detectExpressEntry(
|
|
894
|
+
result.entryPoint = detectExpressEntry(backendDir, backendPkg);
|
|
572
895
|
if (result.entryPoint) {
|
|
573
|
-
result.
|
|
896
|
+
result.appVarName = detectAppVarName(result.entryPoint); // detect FIRST
|
|
897
|
+
result.listenCall = findListenCall(result.entryPoint, result.appVarName);
|
|
898
|
+
result.listenInsideCallback = findListenInsideCallback(
|
|
899
|
+
result.entryPoint,
|
|
900
|
+
result.appVarName,
|
|
901
|
+
);
|
|
574
902
|
result.moduleExportsApp = findModuleExportsApp(result.entryPoint);
|
|
575
|
-
result.listenInsideCallback = findListenInsideCallback(result.entryPoint);
|
|
576
903
|
result.createServer = findCreateServer(result.entryPoint);
|
|
577
|
-
result.appVarName = detectAppVarName(result.entryPoint);
|
|
578
904
|
|
|
579
|
-
// Also check for app file separately (pattern 2)
|
|
580
905
|
const appFileCandidates = [
|
|
581
906
|
"src/app.js",
|
|
582
907
|
"src/app.ts",
|
|
@@ -584,7 +909,7 @@ function detect(cwd) {
|
|
|
584
909
|
"app.ts",
|
|
585
910
|
];
|
|
586
911
|
for (const candidate of appFileCandidates) {
|
|
587
|
-
const fullPath = path.join(
|
|
912
|
+
const fullPath = path.join(backendDir, candidate);
|
|
588
913
|
if (fs.existsSync(fullPath) && fullPath !== result.entryPoint) {
|
|
589
914
|
const exportCall = findModuleExportsApp(fullPath);
|
|
590
915
|
if (exportCall) {
|
|
@@ -597,13 +922,14 @@ function detect(cwd) {
|
|
|
597
922
|
}
|
|
598
923
|
}
|
|
599
924
|
|
|
925
|
+
// ── Already initialized check ─────────────────────────────────────────────
|
|
600
926
|
result.alreadyInitialized =
|
|
601
927
|
detectExistingBotVersion(result.entryPoint) ||
|
|
602
928
|
(framework.name === "next" &&
|
|
603
|
-
(fs.existsSync(path.join(
|
|
604
|
-
fs.existsSync(path.join(
|
|
605
|
-
fs.existsSync(path.join(
|
|
606
|
-
fs.existsSync(path.join(
|
|
929
|
+
(fs.existsSync(path.join(backendDir, "instrumentation.js")) ||
|
|
930
|
+
fs.existsSync(path.join(backendDir, "instrumentation.ts")) ||
|
|
931
|
+
fs.existsSync(path.join(backendDir, "src", "instrumentation.js")) ||
|
|
932
|
+
fs.existsSync(path.join(backendDir, "src", "instrumentation.ts"))));
|
|
607
933
|
|
|
608
934
|
return result;
|
|
609
935
|
}
|
|
@@ -611,6 +937,10 @@ function detect(cwd) {
|
|
|
611
937
|
module.exports = {
|
|
612
938
|
detect,
|
|
613
939
|
readPackageJson,
|
|
940
|
+
scanAllPackageJsons,
|
|
941
|
+
classifyPackageJson,
|
|
942
|
+
detectFrontendFramework,
|
|
943
|
+
findMainFrontendFile,
|
|
614
944
|
detectMonorepo,
|
|
615
945
|
detectFramework,
|
|
616
946
|
detectModuleSystem,
|
package/cli/generator.js
CHANGED
|
@@ -366,7 +366,7 @@ export async function POST(req${typeAnnotation}) {
|
|
|
366
366
|
};
|
|
367
367
|
|
|
368
368
|
// Forward to BotVersion platform directly
|
|
369
|
-
const response = await fetch(\`\${process.env.BOTVERSION_PLATFORM_URL || '
|
|
369
|
+
const response = await fetch(\`\${process.env.BOTVERSION_PLATFORM_URL || 'http://localhost:3000'}/api/chatbot/widget-chat\`, {
|
|
370
370
|
method: 'POST',
|
|
371
371
|
headers: { 'Content-Type': 'application/json' },
|
|
372
372
|
body: JSON.stringify({
|
|
@@ -405,7 +405,7 @@ export async function POST(req${typeAnnotation}) {
|
|
|
405
405
|
const { userId } = await auth();
|
|
406
406
|
const body = await req.json();
|
|
407
407
|
|
|
408
|
-
const response = await fetch(\`\${process.env.BOTVERSION_PLATFORM_URL || '
|
|
408
|
+
const response = await fetch(\`\${process.env.BOTVERSION_PLATFORM_URL || 'http://localhost:3000'}/api/chatbot/widget-chat\`, {
|
|
409
409
|
method: 'POST',
|
|
410
410
|
headers: { 'Content-Type': 'application/json' },
|
|
411
411
|
body: JSON.stringify({
|
|
@@ -444,7 +444,7 @@ export async function POST(req${typeAnnotation}) {
|
|
|
444
444
|
const { data: { session } } = await supabase.auth.getSession();
|
|
445
445
|
const body = await req.json();
|
|
446
446
|
|
|
447
|
-
const response = await fetch(\`\${process.env.BOTVERSION_PLATFORM_URL || '
|
|
447
|
+
const response = await fetch(\`\${process.env.BOTVERSION_PLATFORM_URL || 'http://localhost:3000'}/api/chatbot/widget-chat\`, {
|
|
448
448
|
method: 'POST',
|
|
449
449
|
headers: { 'Content-Type': 'application/json' },
|
|
450
450
|
body: JSON.stringify({
|
|
@@ -491,7 +491,7 @@ export async function POST(req${typeAnnotation}) {
|
|
|
491
491
|
// Add userContext here if needed:
|
|
492
492
|
// const userContext = { userId: '...', email: '...' };
|
|
493
493
|
|
|
494
|
-
const response = await fetch(\`\${process.env.BOTVERSION_PLATFORM_URL || '
|
|
494
|
+
const response = await fetch(\`\${process.env.BOTVERSION_PLATFORM_URL || 'http://localhost:3000'}/api/chatbot/widget-chat\`, {
|
|
495
495
|
method: 'POST',
|
|
496
496
|
headers: { 'Content-Type': 'application/json' },
|
|
497
497
|
body: JSON.stringify({
|
|
@@ -516,7 +516,7 @@ export async function POST(req${typeAnnotation}) {
|
|
|
516
516
|
|
|
517
517
|
// ─── NEXT.JS CONFIG PATCH ─────────────────────────────────────────────────────
|
|
518
518
|
|
|
519
|
-
function generateNextConfigPatch(cwd) {
|
|
519
|
+
function generateNextConfigPatch(cwd, nextVersion) {
|
|
520
520
|
const candidates = ["next.config.js", "next.config.mjs", "next.config.ts"];
|
|
521
521
|
|
|
522
522
|
let configPath = null;
|
|
@@ -533,6 +533,17 @@ function generateNextConfigPatch(cwd) {
|
|
|
533
533
|
|
|
534
534
|
if (!configPath) return null;
|
|
535
535
|
|
|
536
|
+
// Skip instrumentationHook for Next.js 14.1+ (enabled by default)
|
|
537
|
+
if (nextVersion && nextVersion.major >= 14) {
|
|
538
|
+
// Check minor version too
|
|
539
|
+
const rawVersion = nextVersion.raw || "";
|
|
540
|
+
const match = rawVersion.match(/(\d+)\.(\d+)/);
|
|
541
|
+
const minor = match ? parseInt(match[2], 10) : 0;
|
|
542
|
+
if (nextVersion.major > 14 || (nextVersion.major === 14 && minor >= 1)) {
|
|
543
|
+
return { path: configPath, alreadyPatched: true };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
536
547
|
if (configContent.includes("instrumentationHook")) {
|
|
537
548
|
return { path: configPath, alreadyPatched: true };
|
|
538
549
|
}
|
|
@@ -610,6 +621,19 @@ Visit https://docs.botversion.com for manual setup instructions.
|
|
|
610
621
|
);
|
|
611
622
|
}
|
|
612
623
|
|
|
624
|
+
// ─── SCRIPT TAG GENERATION ────────────────────────────────────────────────────
|
|
625
|
+
|
|
626
|
+
function generateScriptTag(projectInfo) {
|
|
627
|
+
return `<script
|
|
628
|
+
id="botversion-loader"
|
|
629
|
+
src="${projectInfo.cdnUrl}"
|
|
630
|
+
data-api-url="${projectInfo.apiUrl}"
|
|
631
|
+
data-project-id="${projectInfo.projectId}"
|
|
632
|
+
data-public-key="${projectInfo.publicKey}"
|
|
633
|
+
data-proxy-url="/api/botversion/chat"
|
|
634
|
+
></script>`;
|
|
635
|
+
}
|
|
636
|
+
|
|
613
637
|
module.exports = {
|
|
614
638
|
generateExpressInit,
|
|
615
639
|
generateInstrumentationFile,
|
|
@@ -617,4 +641,5 @@ module.exports = {
|
|
|
617
641
|
generateNextAppChatRoute,
|
|
618
642
|
generateManualInstructions,
|
|
619
643
|
generateNextConfigPatch,
|
|
644
|
+
generateScriptTag,
|
|
620
645
|
};
|
package/cli/writer.js
CHANGED
|
@@ -24,14 +24,15 @@ function backupFile(filePath) {
|
|
|
24
24
|
|
|
25
25
|
// ─── INJECT CODE BEFORE app.listen() ────────────────────────────────────────
|
|
26
26
|
|
|
27
|
-
function injectBeforeListen(filePath, codeToInject) {
|
|
27
|
+
function injectBeforeListen(filePath, codeToInject, appVarName) {
|
|
28
|
+
appVarName = appVarName || "app";
|
|
28
29
|
const content = fs.readFileSync(filePath, "utf8");
|
|
29
30
|
const lines = content.split("\n");
|
|
31
|
+
const listenRegex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
|
|
30
32
|
|
|
31
|
-
// Find app.listen() line
|
|
32
33
|
let listenLineIndex = -1;
|
|
33
34
|
for (let i = 0; i < lines.length; i++) {
|
|
34
|
-
if (
|
|
35
|
+
if (listenRegex.test(lines[i])) {
|
|
35
36
|
listenLineIndex = i;
|
|
36
37
|
break;
|
|
37
38
|
}
|
|
@@ -94,7 +95,8 @@ function createFile(filePath, content, force) {
|
|
|
94
95
|
return { success: true, path: filePath };
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
function injectBeforeExport(filePath, codeToInject) {
|
|
98
|
+
function injectBeforeExport(filePath, codeToInject, appVarName) {
|
|
99
|
+
appVarName = appVarName || "app";
|
|
98
100
|
const content = fs.readFileSync(filePath, "utf8");
|
|
99
101
|
const lines = content.split("\n");
|
|
100
102
|
|
|
@@ -147,7 +149,8 @@ function injectBeforeExport(filePath, codeToInject) {
|
|
|
147
149
|
// Fallback: before app.listen()
|
|
148
150
|
if (insertIndex === -1) {
|
|
149
151
|
for (let i = 0; i < lines.length; i++) {
|
|
150
|
-
|
|
152
|
+
const listenRegex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
|
|
153
|
+
if (listenRegex.test(lines[i])) {
|
|
151
154
|
insertIndex = i;
|
|
152
155
|
break;
|
|
153
156
|
}
|
|
@@ -238,6 +241,187 @@ function writeSummary(changes) {
|
|
|
238
241
|
return lines.join("\n");
|
|
239
242
|
}
|
|
240
243
|
|
|
244
|
+
// ─── INJECT SCRIPT TAG INTO FRONTEND FILE ────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
function injectScriptTag(filePath, fileType, scriptTag, force) {
|
|
247
|
+
if (!fs.existsSync(filePath)) {
|
|
248
|
+
return { success: false, reason: "file_not_found" };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
252
|
+
|
|
253
|
+
// Already exists check
|
|
254
|
+
if (content.includes("botversion-loader")) {
|
|
255
|
+
if (!force) return { success: false, reason: "already_exists" };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const backup = backupFile(filePath);
|
|
259
|
+
|
|
260
|
+
// ── HTML file — inject before </body> ──────────────────────────────────
|
|
261
|
+
if (fileType === "html") {
|
|
262
|
+
if (!content.includes("</body>")) {
|
|
263
|
+
return { success: false, reason: "no_body_tag" };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const newContent = content.replace("</body>", ` ${scriptTag}\n</body>`);
|
|
267
|
+
fs.writeFileSync(filePath, newContent, "utf8");
|
|
268
|
+
return { success: true, backup };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Next.js _app.js — inject Script component ──────────────────────────
|
|
272
|
+
if (fileType === "next") {
|
|
273
|
+
const fileName = path.basename(filePath);
|
|
274
|
+
|
|
275
|
+
// pages/_app.js
|
|
276
|
+
if (fileName.startsWith("_app")) {
|
|
277
|
+
return injectIntoNextApp(filePath, content, scriptTag, backup);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// app/layout.js
|
|
281
|
+
if (fileName.startsWith("layout")) {
|
|
282
|
+
return injectIntoNextLayout(filePath, content, scriptTag, backup);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { success: false, reason: "unsupported_file_type" };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ─── INJECT INTO NEXT.JS _app.js ─────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
function injectIntoNextApp(filePath, content, scriptTag, backup) {
|
|
292
|
+
let newContent = content;
|
|
293
|
+
|
|
294
|
+
if (!content.includes("next/script")) {
|
|
295
|
+
newContent = newContent.replace(
|
|
296
|
+
/^(import .+)/m,
|
|
297
|
+
`import Script from 'next/script';\n$1`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const scriptComponent = `
|
|
302
|
+
<Script
|
|
303
|
+
id="botversion-loader"
|
|
304
|
+
src="${extractAttr(scriptTag, "src")}"
|
|
305
|
+
data-api-url="${extractAttr(scriptTag, "data-api-url")}"
|
|
306
|
+
data-project-id="${extractAttr(scriptTag, "data-project-id")}"
|
|
307
|
+
data-public-key="${extractAttr(scriptTag, "data-public-key")}"
|
|
308
|
+
data-proxy-url="/api/botversion/chat"
|
|
309
|
+
strategy="afterInteractive"
|
|
310
|
+
/>`;
|
|
311
|
+
|
|
312
|
+
const lines = newContent.split("\n");
|
|
313
|
+
|
|
314
|
+
// Find ALL return statements and pick the one whose root JSX
|
|
315
|
+
// is a multi-child wrapper (not a simple single-element return)
|
|
316
|
+
// Strategy: find the return ( that is followed by the most lines
|
|
317
|
+
// before its closing ) — that's the main render return
|
|
318
|
+
|
|
319
|
+
let bestReturnIndex = -1;
|
|
320
|
+
let bestRootJsxIndex = -1;
|
|
321
|
+
let bestLineCount = 0;
|
|
322
|
+
|
|
323
|
+
for (let i = 0; i < lines.length; i++) {
|
|
324
|
+
if (!/^\s*return\s*\(/.test(lines[i])) continue;
|
|
325
|
+
|
|
326
|
+
// Find the root JSX tag after this return
|
|
327
|
+
let rootJsx = -1;
|
|
328
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
329
|
+
const trimmed = lines[j].trim();
|
|
330
|
+
if (!trimmed) continue;
|
|
331
|
+
if (trimmed.startsWith("<")) {
|
|
332
|
+
rootJsx = j;
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
break; // non-empty, non-JSX line means this isn't a JSX return
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (rootJsx === -1) continue;
|
|
339
|
+
|
|
340
|
+
// Find the closing ) of this return block
|
|
341
|
+
let depth = 1;
|
|
342
|
+
let closingLine = -1;
|
|
343
|
+
for (let j = rootJsx; j < lines.length; j++) {
|
|
344
|
+
for (const ch of lines[j]) {
|
|
345
|
+
if (ch === "(") depth++;
|
|
346
|
+
if (ch === ")") depth--;
|
|
347
|
+
}
|
|
348
|
+
if (depth === 0) {
|
|
349
|
+
closingLine = j;
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const lineCount = closingLine - i;
|
|
355
|
+
if (lineCount > bestLineCount) {
|
|
356
|
+
bestLineCount = lineCount;
|
|
357
|
+
bestReturnIndex = i;
|
|
358
|
+
bestRootJsxIndex = rootJsx;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (bestRootJsxIndex !== -1) {
|
|
363
|
+
lines.splice(bestRootJsxIndex + 1, 0, scriptComponent);
|
|
364
|
+
newContent = lines.join("\n");
|
|
365
|
+
} else {
|
|
366
|
+
// Final fallback
|
|
367
|
+
newContent = newContent.replace(
|
|
368
|
+
/([ \t]*<\/div>\s*\n\s*\))/,
|
|
369
|
+
`${scriptComponent}\n$1`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
fs.writeFileSync(filePath, newContent, "utf8");
|
|
374
|
+
return { success: true, backup };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ─── INJECT INTO NEXT.JS layout.js ───────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
function injectIntoNextLayout(filePath, content, scriptTag, backup) {
|
|
380
|
+
let newContent = content;
|
|
381
|
+
|
|
382
|
+
if (!content.includes("next/script")) {
|
|
383
|
+
newContent = newContent.replace(
|
|
384
|
+
/^(import .+)/m,
|
|
385
|
+
`import Script from 'next/script';\n$1`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const scriptComponent = `
|
|
390
|
+
<Script
|
|
391
|
+
id="botversion-loader"
|
|
392
|
+
src="${extractAttr(scriptTag, "src")}"
|
|
393
|
+
data-api-url="${extractAttr(scriptTag, "data-api-url")}"
|
|
394
|
+
data-project-id="${extractAttr(scriptTag, "data-project-id")}"
|
|
395
|
+
data-public-key="${extractAttr(scriptTag, "data-public-key")}"
|
|
396
|
+
data-proxy-url="/api/botversion/chat"
|
|
397
|
+
strategy="afterInteractive"
|
|
398
|
+
/>`;
|
|
399
|
+
|
|
400
|
+
// Inject before </body> in layout
|
|
401
|
+
if (content.includes("</body>")) {
|
|
402
|
+
newContent = newContent.replace(
|
|
403
|
+
"</body>",
|
|
404
|
+
`${scriptComponent}\n </body>`,
|
|
405
|
+
);
|
|
406
|
+
} else {
|
|
407
|
+
// Fallback — before last closing tag
|
|
408
|
+
newContent = newContent.replace(
|
|
409
|
+
/(<\/\w+>\s*\)[\s;]*$)/m,
|
|
410
|
+
`${scriptComponent}\n $1`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
fs.writeFileSync(filePath, newContent, "utf8");
|
|
415
|
+
return { success: true, backup };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ─── HELPER: extract attribute value from script tag string ──────────────────
|
|
419
|
+
|
|
420
|
+
function extractAttr(scriptTag, attr) {
|
|
421
|
+
const match = scriptTag.match(new RegExp(`${attr}="([^"]+)"`));
|
|
422
|
+
return match ? match[1] : "";
|
|
423
|
+
}
|
|
424
|
+
|
|
241
425
|
module.exports = {
|
|
242
426
|
writeFile,
|
|
243
427
|
backupFile,
|
|
@@ -247,4 +431,5 @@ module.exports = {
|
|
|
247
431
|
mergeIntoMiddleware,
|
|
248
432
|
writeSummary,
|
|
249
433
|
injectBeforeExport,
|
|
434
|
+
injectScriptTag,
|
|
250
435
|
};
|
package/index.js
CHANGED
|
@@ -61,8 +61,7 @@ var BotVersion = {
|
|
|
61
61
|
|
|
62
62
|
this._client = new BotVersionClient({
|
|
63
63
|
apiKey: options.apiKey,
|
|
64
|
-
platformUrl:
|
|
65
|
-
options.platformUrl || "https://chatbusiness-two.vercel.app/",
|
|
64
|
+
platformUrl: options.platformUrl || "http://localhost:3000/",
|
|
66
65
|
debug: options.debug || false,
|
|
67
66
|
timeout: options.timeout || 30000,
|
|
68
67
|
});
|