botversion-sdk 1.1.7 → 2.1.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-backend.js +75 -0
- package/bin/scan-frontend.js +66 -0
- package/client.js +13 -7
- package/index.js +152 -24
- package/interceptor.js +190 -14
- package/package.json +4 -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/cli/writer.js
DELETED
|
@@ -1,511 +0,0 @@
|
|
|
1
|
-
// botversion-sdk/cli/writer.js
|
|
2
|
-
|
|
3
|
-
"use strict";
|
|
4
|
-
|
|
5
|
-
const fs = require("fs");
|
|
6
|
-
const path = require("path");
|
|
7
|
-
|
|
8
|
-
// ─── SAFE FILE WRITE ─────────────────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
function writeFile(filePath, content) {
|
|
11
|
-
const dir = path.dirname(filePath);
|
|
12
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
13
|
-
fs.writeFileSync(filePath, content, "utf8");
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// ─── BACKUP A FILE BEFORE MODIFYING ─────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
function backupFile(filePath) {
|
|
19
|
-
if (!fs.existsSync(filePath)) return null;
|
|
20
|
-
const backupPath = filePath + ".backup-before-botversion";
|
|
21
|
-
fs.copyFileSync(filePath, backupPath);
|
|
22
|
-
return backupPath;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ─── INJECT CODE BEFORE app.listen() ────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
function injectBeforeListen(filePath, codeToInject, appVarName) {
|
|
28
|
-
appVarName = appVarName || "app";
|
|
29
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
30
|
-
const lines = content.split("\n");
|
|
31
|
-
const listenRegex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
|
|
32
|
-
|
|
33
|
-
// Check if BotVersion already exists in file
|
|
34
|
-
if (content.includes("botversion-sdk") || content.includes("BotVersion")) {
|
|
35
|
-
return { success: false, reason: "already_exists" };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
let listenLineIndex = -1;
|
|
39
|
-
for (let i = 0; i < lines.length; i++) {
|
|
40
|
-
if (listenRegex.test(lines[i])) {
|
|
41
|
-
listenLineIndex = i;
|
|
42
|
-
break;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (listenLineIndex === -1) {
|
|
47
|
-
return {
|
|
48
|
-
success: false,
|
|
49
|
-
reason: "no_listen",
|
|
50
|
-
suggestion: "append",
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ── NEW: Check if app.listen() is inside if (require.main === module) ──
|
|
55
|
-
// If so, inject before that if block instead of inside it
|
|
56
|
-
let insertIndex = listenLineIndex;
|
|
57
|
-
|
|
58
|
-
for (let i = listenLineIndex; i >= 0; i--) {
|
|
59
|
-
const trimmed = lines[i].trim();
|
|
60
|
-
|
|
61
|
-
// Found if (require.main === module) block
|
|
62
|
-
if (
|
|
63
|
-
/if\s*\(\s*require\.main\s*===?\s*module\s*\)/.test(trimmed) ||
|
|
64
|
-
/if\s*\(\s*module\s*===?\s*require\.main\s*\)/.test(trimmed)
|
|
65
|
-
) {
|
|
66
|
-
insertIndex = i;
|
|
67
|
-
// Keep walking back to check for 404/error handlers before this
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Found a 404 handler — inject before this instead
|
|
72
|
-
if (
|
|
73
|
-
/app\.use\s*\(\s*\(req,\s*res\)\s*=>/.test(trimmed) ||
|
|
74
|
-
/app\.use\s*\(\s*function\s*\(req,\s*res\)/.test(trimmed)
|
|
75
|
-
) {
|
|
76
|
-
insertIndex = i;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Found a global error handler — inject before this instead
|
|
81
|
-
if (
|
|
82
|
-
/app\.use\s*\(\s*\(err,\s*req,\s*res,\s*next\)/.test(trimmed) ||
|
|
83
|
-
/app\.use\s*\(\s*function\s*\(err,\s*req,\s*res,\s*next\)/.test(trimmed)
|
|
84
|
-
) {
|
|
85
|
-
insertIndex = i;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Stop walking back if we hit a top-level non-empty statement
|
|
90
|
-
if (
|
|
91
|
-
i < listenLineIndex &&
|
|
92
|
-
trimmed !== "" &&
|
|
93
|
-
trimmed !== "}" &&
|
|
94
|
-
trimmed !== "})" &&
|
|
95
|
-
trimmed !== "});" &&
|
|
96
|
-
lines[i].match(/^[^\s]/)
|
|
97
|
-
) {
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Insert the code block at the correct position
|
|
103
|
-
const before = lines.slice(0, insertIndex);
|
|
104
|
-
const after = lines.slice(insertIndex);
|
|
105
|
-
const injectedLines = ["", ...codeToInject.split("\n"), ""];
|
|
106
|
-
const newContent = [...before, ...injectedLines, ...after].join("\n");
|
|
107
|
-
|
|
108
|
-
const backup = backupFile(filePath);
|
|
109
|
-
fs.writeFileSync(filePath, newContent, "utf8");
|
|
110
|
-
|
|
111
|
-
return { success: true, lineNumber: insertIndex + 1, backup };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// ─── APPEND CODE TO END OF FILE ──────────────────────────────────────────────
|
|
115
|
-
|
|
116
|
-
function appendToFile(filePath, codeToAppend) {
|
|
117
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
118
|
-
|
|
119
|
-
if (content.includes("botversion-sdk") || content.includes("BotVersion")) {
|
|
120
|
-
return { success: false, reason: "already_exists" };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
backupFile(filePath);
|
|
124
|
-
const newContent = content.trimEnd() + "\n\n" + codeToAppend + "\n";
|
|
125
|
-
fs.writeFileSync(filePath, newContent, "utf8");
|
|
126
|
-
return { success: true };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// ─── CREATE A NEW FILE (won't overwrite unless forced) ───────────────────────
|
|
130
|
-
|
|
131
|
-
function createFile(filePath, content, force) {
|
|
132
|
-
if (fs.existsSync(filePath) && !force) {
|
|
133
|
-
return { success: false, reason: "already_exists", path: filePath };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (fs.existsSync(filePath) && force) {
|
|
137
|
-
backupFile(filePath);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
writeFile(filePath, content);
|
|
141
|
-
return { success: true, path: filePath };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function injectBeforeExport(filePath, codeToInject, appVarName) {
|
|
145
|
-
appVarName = appVarName || "app";
|
|
146
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
147
|
-
const lines = content.split("\n");
|
|
148
|
-
|
|
149
|
-
if (content.includes("botversion-sdk") || content.includes("BotVersion")) {
|
|
150
|
-
return { success: false, reason: "already_exists" };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
let insertIndex = -1;
|
|
154
|
-
|
|
155
|
-
// Find module.exports = app
|
|
156
|
-
let exportsLine = -1;
|
|
157
|
-
for (let i = 0; i < lines.length; i++) {
|
|
158
|
-
if (/module\.exports\s*=\s*app/.test(lines[i])) {
|
|
159
|
-
exportsLine = i;
|
|
160
|
-
break;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (exportsLine !== -1) {
|
|
165
|
-
// Walk backwards from module.exports to skip error handler lines
|
|
166
|
-
// Skip lines that are: blank, closing braces, or known error middleware
|
|
167
|
-
let i = exportsLine - 1;
|
|
168
|
-
while (i >= 0) {
|
|
169
|
-
const line = lines[i].trim();
|
|
170
|
-
if (
|
|
171
|
-
line === "" ||
|
|
172
|
-
line === "})" ||
|
|
173
|
-
line === "});" ||
|
|
174
|
-
line === "}," ||
|
|
175
|
-
line === "}" ||
|
|
176
|
-
line === ");" ||
|
|
177
|
-
line === "next();" ||
|
|
178
|
-
/app\.use\s*\(\s*errorHandler/.test(line) ||
|
|
179
|
-
/app\.use\s*\(\s*errorConverter/.test(line) ||
|
|
180
|
-
/app\.use\s*\(\s*\(req,\s*res,\s*next\)/.test(line) ||
|
|
181
|
-
/next\(new/.test(line) ||
|
|
182
|
-
/NOT_FOUND/.test(line) ||
|
|
183
|
-
/Not found/i.test(line) ||
|
|
184
|
-
/\/\/ (handle|convert|send back) error/.test(lines[i]) ||
|
|
185
|
-
/\/\/ send back a 404/.test(lines[i])
|
|
186
|
-
) {
|
|
187
|
-
i--;
|
|
188
|
-
} else {
|
|
189
|
-
break;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
insertIndex = i + 1;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Fallback: before app.listen()
|
|
196
|
-
if (insertIndex === -1) {
|
|
197
|
-
for (let i = 0; i < lines.length; i++) {
|
|
198
|
-
const listenRegex = new RegExp(`${appVarName}\\.listen\\s*\\(`);
|
|
199
|
-
if (listenRegex.test(lines[i])) {
|
|
200
|
-
insertIndex = i;
|
|
201
|
-
break;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Final fallback: append to end of file
|
|
207
|
-
if (insertIndex === -1) {
|
|
208
|
-
const backup = backupFile(filePath);
|
|
209
|
-
const newContent = content.trimEnd() + "\n\n" + codeToInject + "\n";
|
|
210
|
-
fs.writeFileSync(filePath, newContent, "utf8");
|
|
211
|
-
return { success: true, backup };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const before = lines.slice(0, insertIndex);
|
|
215
|
-
const after = lines.slice(insertIndex);
|
|
216
|
-
const injectedLines = ["", ...codeToInject.split("\n"), ""];
|
|
217
|
-
const newContent = [...before, ...injectedLines, ...after].join("\n");
|
|
218
|
-
|
|
219
|
-
const backup = backupFile(filePath);
|
|
220
|
-
fs.writeFileSync(filePath, newContent, "utf8");
|
|
221
|
-
return { success: true, backup };
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// ─── WRITE SUMMARY OF ALL CHANGES ────────────────────────────────────────────
|
|
225
|
-
|
|
226
|
-
function writeSummary(changes) {
|
|
227
|
-
const lines = [
|
|
228
|
-
"",
|
|
229
|
-
"┌─────────────────────────────────────────────┐",
|
|
230
|
-
"│ BotVersion Setup Complete! │",
|
|
231
|
-
"└─────────────────────────────────────────────┘",
|
|
232
|
-
"",
|
|
233
|
-
];
|
|
234
|
-
|
|
235
|
-
if (changes.modified && changes.modified.length > 0) {
|
|
236
|
-
lines.push(" Modified files:");
|
|
237
|
-
changes.modified.forEach((f) => lines.push(` [Modified] ${f}`));
|
|
238
|
-
lines.push("");
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (changes.created && changes.created.length > 0) {
|
|
242
|
-
lines.push(" Created files:");
|
|
243
|
-
changes.created.forEach((f) => lines.push(` [Created] ${f}`));
|
|
244
|
-
lines.push("");
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (changes.backups && changes.backups.length > 0) {
|
|
248
|
-
lines.push(" Backups created:");
|
|
249
|
-
changes.backups.forEach((f) => lines.push(` [Backup] ${f}`));
|
|
250
|
-
lines.push("");
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (changes.manual && changes.manual.length > 0) {
|
|
254
|
-
lines.push(" Manual steps needed:");
|
|
255
|
-
changes.manual.forEach((m) => lines.push(` [Manual] ${m}`));
|
|
256
|
-
lines.push("");
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
lines.push(" Next: Restart your server and test the chat widget.");
|
|
260
|
-
lines.push(" Docs: https://botversion.com/docs");
|
|
261
|
-
lines.push("");
|
|
262
|
-
|
|
263
|
-
return lines.join("\n");
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// ─── INJECT SCRIPT TAG INTO FRONTEND FILE ────────────────────────────────────
|
|
267
|
-
|
|
268
|
-
function injectScriptTag(filePath, fileType, scriptTag, force) {
|
|
269
|
-
if (!fs.existsSync(filePath)) {
|
|
270
|
-
return { success: false, reason: "file_not_found" };
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
274
|
-
|
|
275
|
-
// Already exists check
|
|
276
|
-
if (content.includes("botversion-loader")) {
|
|
277
|
-
if (!force) return { success: false, reason: "already_exists" };
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const backup = backupFile(filePath);
|
|
281
|
-
|
|
282
|
-
// ── HTML file — inject before </body> ──────────────────────────────────
|
|
283
|
-
if (fileType === "html") {
|
|
284
|
-
if (!content.includes("</body>")) {
|
|
285
|
-
return { success: false, reason: "no_body_tag" };
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const newContent = content.replace("</body>", ` ${scriptTag}\n</body>`);
|
|
289
|
-
fs.writeFileSync(filePath, newContent, "utf8");
|
|
290
|
-
return { success: true, backup };
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// ── Next.js _app.js — inject Script component ──────────────────────────
|
|
294
|
-
if (fileType === "next") {
|
|
295
|
-
const fileName = path.basename(filePath);
|
|
296
|
-
|
|
297
|
-
// pages/_app.js
|
|
298
|
-
if (fileName.startsWith("_app")) {
|
|
299
|
-
return injectIntoNextApp(filePath, content, scriptTag, backup);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// app/layout.js
|
|
303
|
-
if (fileName.startsWith("layout")) {
|
|
304
|
-
return injectIntoNextLayout(filePath, content, scriptTag, backup);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return { success: false, reason: "unsupported_file_type" };
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// ─── INJECT INTO NEXT.JS _app.js ─────────────────────────────────────────────
|
|
312
|
-
|
|
313
|
-
function injectIntoNextApp(filePath, content, scriptTag, backup) {
|
|
314
|
-
let newContent = content;
|
|
315
|
-
|
|
316
|
-
if (!content.includes("next/script")) {
|
|
317
|
-
newContent = newContent.replace(
|
|
318
|
-
/^(import .+)/m,
|
|
319
|
-
`import Script from 'next/script';\n$1`,
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const scriptComponent = `
|
|
324
|
-
<Script
|
|
325
|
-
id="botversion-loader"
|
|
326
|
-
src="${extractAttr(scriptTag, "src")}"
|
|
327
|
-
data-api-url="${extractAttr(scriptTag, "data-api-url")}"
|
|
328
|
-
data-project-id="${extractAttr(scriptTag, "data-project-id")}"
|
|
329
|
-
data-public-key="${extractAttr(scriptTag, "data-public-key")}"
|
|
330
|
-
strategy="afterInteractive"
|
|
331
|
-
/>`;
|
|
332
|
-
|
|
333
|
-
const lines = newContent.split("\n");
|
|
334
|
-
|
|
335
|
-
// Find ALL return statements and pick the one whose root JSX
|
|
336
|
-
// is a multi-child wrapper (not a simple single-element return)
|
|
337
|
-
// Strategy: find the return ( that is followed by the most lines
|
|
338
|
-
// before its closing ) — that's the main render return
|
|
339
|
-
|
|
340
|
-
let bestReturnIndex = -1;
|
|
341
|
-
let bestRootJsxIndex = -1;
|
|
342
|
-
let bestLineCount = 0;
|
|
343
|
-
|
|
344
|
-
for (let i = 0; i < lines.length; i++) {
|
|
345
|
-
if (!/^\s*return\s*\(/.test(lines[i])) continue;
|
|
346
|
-
|
|
347
|
-
// Find the root JSX tag after this return
|
|
348
|
-
let rootJsx = -1;
|
|
349
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
350
|
-
const trimmed = lines[j].trim();
|
|
351
|
-
if (!trimmed) continue;
|
|
352
|
-
if (trimmed.startsWith("<")) {
|
|
353
|
-
rootJsx = j;
|
|
354
|
-
break;
|
|
355
|
-
}
|
|
356
|
-
break; // non-empty, non-JSX line means this isn't a JSX return
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (rootJsx === -1) continue;
|
|
360
|
-
|
|
361
|
-
// Find the closing ) of this return block
|
|
362
|
-
let depth = 1;
|
|
363
|
-
let closingLine = -1;
|
|
364
|
-
for (let j = rootJsx; j < lines.length; j++) {
|
|
365
|
-
for (const ch of lines[j]) {
|
|
366
|
-
if (ch === "(") depth++;
|
|
367
|
-
if (ch === ")") depth--;
|
|
368
|
-
}
|
|
369
|
-
if (depth === 0) {
|
|
370
|
-
closingLine = j;
|
|
371
|
-
break;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const lineCount = closingLine - i;
|
|
376
|
-
if (lineCount > bestLineCount) {
|
|
377
|
-
bestLineCount = lineCount;
|
|
378
|
-
bestReturnIndex = i;
|
|
379
|
-
bestRootJsxIndex = rootJsx;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
if (bestRootJsxIndex !== -1) {
|
|
384
|
-
lines.splice(bestRootJsxIndex + 1, 0, scriptComponent);
|
|
385
|
-
newContent = lines.join("\n");
|
|
386
|
-
} else {
|
|
387
|
-
// Final fallback
|
|
388
|
-
newContent = newContent.replace(
|
|
389
|
-
/([ \t]*<\/div>\s*\n\s*\))/,
|
|
390
|
-
`${scriptComponent}\n$1`,
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
fs.writeFileSync(filePath, newContent, "utf8");
|
|
395
|
-
return { success: true, backup };
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// ─── INJECT INTO NEXT.JS layout.js ───────────────────────────────────────────
|
|
399
|
-
|
|
400
|
-
function injectIntoNextLayout(filePath, content, scriptTag, backup) {
|
|
401
|
-
let newContent = content;
|
|
402
|
-
|
|
403
|
-
if (!content.includes("next/script")) {
|
|
404
|
-
newContent = newContent.replace(
|
|
405
|
-
/^(import .+)/m,
|
|
406
|
-
`import Script from 'next/script';\n$1`,
|
|
407
|
-
);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const scriptComponent = `
|
|
411
|
-
<Script
|
|
412
|
-
id="botversion-loader"
|
|
413
|
-
src="${extractAttr(scriptTag, "src")}"
|
|
414
|
-
data-api-url="${extractAttr(scriptTag, "data-api-url")}"
|
|
415
|
-
data-project-id="${extractAttr(scriptTag, "data-project-id")}"
|
|
416
|
-
data-public-key="${extractAttr(scriptTag, "data-public-key")}"
|
|
417
|
-
strategy="afterInteractive"
|
|
418
|
-
/>`;
|
|
419
|
-
|
|
420
|
-
// Inject before </body> in layout
|
|
421
|
-
if (content.includes("</body>")) {
|
|
422
|
-
newContent = newContent.replace(
|
|
423
|
-
"</body>",
|
|
424
|
-
`${scriptComponent}\n </body>`,
|
|
425
|
-
);
|
|
426
|
-
} else {
|
|
427
|
-
// Fallback — before last closing tag
|
|
428
|
-
newContent = newContent.replace(
|
|
429
|
-
/(<\/\w+>\s*\)[\s;]*$)/m,
|
|
430
|
-
`${scriptComponent}\n $1`,
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
fs.writeFileSync(filePath, newContent, "utf8");
|
|
435
|
-
return { success: true, backup };
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function injectAtTop(filePath, codeToInject) {
|
|
439
|
-
const content = fs.readFileSync(filePath, "utf8");
|
|
440
|
-
|
|
441
|
-
if (content.includes("dotenv")) {
|
|
442
|
-
return { success: false, reason: "already_exists" };
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const newContent = codeToInject + "\n" + content;
|
|
446
|
-
backupFile(filePath);
|
|
447
|
-
fs.writeFileSync(filePath, newContent, "utf8");
|
|
448
|
-
return { success: true };
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// ─── HELPER: extract attribute value from script tag string ──────────────────
|
|
452
|
-
|
|
453
|
-
function extractAttr(scriptTag, attr) {
|
|
454
|
-
const match = scriptTag.match(new RegExp(`${attr}="([^"]+)"`));
|
|
455
|
-
return match ? match[1] : "";
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// ─── WRITE API KEY TO .ENV FILE ───────────────────────────────────────────────
|
|
459
|
-
|
|
460
|
-
function writeEnvKey(cwd, key, value, framework) {
|
|
461
|
-
const envCandidates =
|
|
462
|
-
framework === "next"
|
|
463
|
-
? [".env.local", ".env", ".env.development", ".env.development.local"]
|
|
464
|
-
: [".env", ".env.development", ".env.local"];
|
|
465
|
-
|
|
466
|
-
let envPath = null;
|
|
467
|
-
|
|
468
|
-
// Use existing .env file if found
|
|
469
|
-
for (const candidate of envCandidates) {
|
|
470
|
-
const fullPath = path.join(cwd, candidate);
|
|
471
|
-
if (fs.existsSync(fullPath)) {
|
|
472
|
-
envPath = fullPath;
|
|
473
|
-
break;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// No .env file found — create .env
|
|
478
|
-
if (!envPath) {
|
|
479
|
-
envPath = path.join(cwd, ".env");
|
|
480
|
-
fs.writeFileSync(envPath, "", "utf8");
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const content = fs.readFileSync(envPath, "utf8");
|
|
484
|
-
|
|
485
|
-
// Key already exists — skip
|
|
486
|
-
if (content.includes(key)) {
|
|
487
|
-
return { success: false, reason: "already_exists", path: envPath };
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Append key to file
|
|
491
|
-
const newLine =
|
|
492
|
-
content === "" || content.endsWith("\n")
|
|
493
|
-
? `${key}=${value}\n`
|
|
494
|
-
: `\n${key}=${value}\n`;
|
|
495
|
-
|
|
496
|
-
fs.appendFileSync(envPath, newLine, "utf8");
|
|
497
|
-
return { success: true, path: envPath };
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
module.exports = {
|
|
501
|
-
writeFile,
|
|
502
|
-
backupFile,
|
|
503
|
-
injectBeforeListen,
|
|
504
|
-
appendToFile,
|
|
505
|
-
createFile,
|
|
506
|
-
writeSummary,
|
|
507
|
-
injectBeforeExport,
|
|
508
|
-
injectScriptTag,
|
|
509
|
-
injectAtTop,
|
|
510
|
-
writeEnvKey,
|
|
511
|
-
};
|