@navneet_25/tempjs 1.1.0 → 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/README.md +752 -35
- package/cli/brand-manager.js +178 -66
- package/cli/config.js +14 -1
- package/cli/copy.js +4 -18
- package/cli/db-setup.js +170 -49
- package/cli/fetch.js +47 -29
- package/cli/file-tree.js +113 -0
- package/cli/fs-ignore.js +60 -0
- package/cli/index.js +138 -107
- package/cli/info.js +88 -0
- package/cli/parse-args.js +156 -0
- package/cli/progress.js +33 -0
- package/cli/project-stamp.js +69 -0
- package/cli/prompt.js +19 -0
- package/cli/template-resolver.js +48 -0
- package/cli/theme-manager.js +28 -2
- package/cli/update.js +212 -0
- package/package.json +6 -2
- package/templates.json +33 -3
package/cli/brand-manager.js
CHANGED
|
@@ -3,52 +3,78 @@ import { stdin as input, stdout as output } from "node:process";
|
|
|
3
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { findSiteTs } from "./theme-manager.js";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {{
|
|
8
|
+
* yes?: boolean,
|
|
9
|
+
* name?: string,
|
|
10
|
+
* shortName?: string,
|
|
11
|
+
* baseUrl?: string,
|
|
12
|
+
* phone?: string,
|
|
13
|
+
* phoneDisplay?: string,
|
|
14
|
+
* countryCode?: string,
|
|
15
|
+
* email?: string,
|
|
16
|
+
* address?: string
|
|
17
|
+
* }} BrandOptions
|
|
18
|
+
*/
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} siteContent
|
|
22
|
+
*/
|
|
23
|
+
function parseCurrentBrandValues(siteContent) {
|
|
24
|
+
return {
|
|
25
|
+
brandName: (siteContent.match(/name:\s*"([^"]+)"/) || [])[1] || "Chanakya Resort",
|
|
26
|
+
shortName: (siteContent.match(/shortName:\s*"([^"]+)"/) || [])[1] || "Chanakya",
|
|
27
|
+
baseUrl: (siteContent.match(/baseUrl:\s*"([^"]+)"/) || [])[1] || "https://chanakyaresort.com",
|
|
28
|
+
phone: (siteContent.match(/phone:\s*"([^"]+)"/) || [])[1] || "9876543210",
|
|
29
|
+
phoneDisplay: (siteContent.match(/phoneDisplay:\s*"([^"]+)"/) || [])[1] || "+91 98765 43210",
|
|
30
|
+
countryCode: (siteContent.match(/countryCode:\s*"([^"]+)"/) || [])[1] || "91",
|
|
31
|
+
email: (siteContent.match(/email:\s*"([^"]+)"/) || [])[1] || "reservations@chanakyaresort.com",
|
|
32
|
+
address: (siteContent.match(/full:\s*"([^"]+)"/) || [])[1] || "Lonavala, Maharashtra, India",
|
|
33
|
+
};
|
|
34
|
+
}
|
|
25
35
|
|
|
26
|
-
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} baseUrl
|
|
38
|
+
*/
|
|
39
|
+
function deriveWwwHost(baseUrl) {
|
|
27
40
|
try {
|
|
28
|
-
|
|
41
|
+
const url = new URL(baseUrl);
|
|
42
|
+
return url.hostname.startsWith("www.") ? url.hostname : `www.${url.hostname}`;
|
|
43
|
+
} catch {
|
|
44
|
+
let wwwHost = baseUrl.replace(/^https?:\/\//, "");
|
|
45
|
+
if (!wwwHost.startsWith("www.")) wwwHost = `www.${wwwHost}`;
|
|
46
|
+
return wwwHost;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} siteContent
|
|
52
|
+
* @param {{
|
|
53
|
+
* brandName: string,
|
|
54
|
+
* shortName: string,
|
|
55
|
+
* baseUrl: string,
|
|
56
|
+
* phone: string,
|
|
57
|
+
* phoneDisplay: string,
|
|
58
|
+
* countryCode: string,
|
|
59
|
+
* email: string,
|
|
60
|
+
* address: string
|
|
61
|
+
* }} values
|
|
62
|
+
*/
|
|
63
|
+
function applyBrandToSiteContent(siteContent, values) {
|
|
64
|
+
const {
|
|
65
|
+
brandName,
|
|
66
|
+
shortName,
|
|
67
|
+
baseUrl,
|
|
68
|
+
phone,
|
|
69
|
+
phoneDisplay,
|
|
70
|
+
countryCode,
|
|
71
|
+
email,
|
|
72
|
+
address,
|
|
73
|
+
} = values;
|
|
29
74
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// Derive wwwHost from baseUrl
|
|
35
|
-
let wwwHost = "www.example.com";
|
|
36
|
-
try {
|
|
37
|
-
const url = new URL(baseUrl);
|
|
38
|
-
wwwHost = url.hostname.startsWith("www.") ? url.hostname : `www.${url.hostname}`;
|
|
39
|
-
} catch {
|
|
40
|
-
wwwHost = baseUrl.replace(/^https?:\/\//, "");
|
|
41
|
-
if (!wwwHost.startsWith("www.")) wwwHost = `www.${wwwHost}`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const phone = (await rl.question(`Contact Phone Number [${currentPhone}]: `)).trim() || currentPhone;
|
|
45
|
-
const phoneDisplay = (await rl.question(`Display Phone Number [${currentPhoneDisplay}]: `)).trim() || currentPhoneDisplay;
|
|
46
|
-
const countryCode = (await rl.question(`Country Code [${currentCountryCode}]: `)).trim() || currentCountryCode;
|
|
47
|
-
const email = (await rl.question(`Contact Email [${currentEmail}]: `)).trim() || currentEmail;
|
|
48
|
-
const address = (await rl.question(`Full Address [${currentAddress}]: `)).trim() || currentAddress;
|
|
49
|
-
|
|
50
|
-
// Build replacement blocks
|
|
51
|
-
const newBrandBlock = `brand: {
|
|
75
|
+
const wwwHost = deriveWwwHost(baseUrl);
|
|
76
|
+
|
|
77
|
+
const newBrandBlock = `brand: {
|
|
52
78
|
name: "${brandName}",
|
|
53
79
|
shortName: "${shortName}",
|
|
54
80
|
tagline: "Where Nature Meets Refined Comfort",
|
|
@@ -58,21 +84,20 @@ export async function promptAndApplyBrand(targetDir) {
|
|
|
58
84
|
managedBy: "Managed by ${brandName}.",
|
|
59
85
|
}`;
|
|
60
86
|
|
|
61
|
-
|
|
87
|
+
const newDomainBlock = `domain: {
|
|
62
88
|
baseUrl: "${baseUrl}",
|
|
63
89
|
wwwHost: "${wwwHost}",
|
|
64
90
|
}`;
|
|
65
91
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
92
|
+
let locality = "Lonavala";
|
|
93
|
+
let region = "MH";
|
|
94
|
+
const addressParts = address.split(",");
|
|
95
|
+
if (addressParts.length >= 2) {
|
|
96
|
+
locality = addressParts[0].trim();
|
|
97
|
+
region = addressParts[1].trim();
|
|
98
|
+
}
|
|
74
99
|
|
|
75
|
-
|
|
100
|
+
const newContactBlock = `contact: {
|
|
76
101
|
phone: "${phone}",
|
|
77
102
|
phoneDisplay: "${phoneDisplay}",
|
|
78
103
|
countryCode: "${countryCode}",
|
|
@@ -85,20 +110,107 @@ export async function promptAndApplyBrand(targetDir) {
|
|
|
85
110
|
},
|
|
86
111
|
}`;
|
|
87
112
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
113
|
+
const brandRegex = /brand:\s*\{[\s\S]*?\n\s*\},/;
|
|
114
|
+
const domainRegex = /domain:\s*\{[\s\S]*?\n\s*\},/;
|
|
115
|
+
const contactRegex = /contact:\s*\{[\s\S]*?address:\s*\{[\s\S]*?\}[\s\S]*?\n\s*\},/;
|
|
116
|
+
|
|
117
|
+
if (brandRegex.test(siteContent)) {
|
|
118
|
+
siteContent = siteContent.replace(brandRegex, newBrandBlock + ",");
|
|
119
|
+
}
|
|
120
|
+
if (domainRegex.test(siteContent)) {
|
|
121
|
+
siteContent = siteContent.replace(domainRegex, newDomainBlock + ",");
|
|
122
|
+
}
|
|
123
|
+
if (contactRegex.test(siteContent)) {
|
|
124
|
+
siteContent = siteContent.replace(contactRegex, newContactBlock + ",");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return siteContent;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {string} targetDir
|
|
132
|
+
* @param {BrandOptions} options
|
|
133
|
+
*/
|
|
134
|
+
export async function applyBrand(targetDir, options = {}) {
|
|
135
|
+
const siteTsPath = await findSiteTs(targetDir);
|
|
136
|
+
if (!siteTsPath) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
"Could not locate constants/site.ts. Make sure you are inside an initialized tempjs template directory."
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let siteContent = await readFile(siteTsPath, "utf8");
|
|
143
|
+
const current = parseCurrentBrandValues(siteContent);
|
|
144
|
+
|
|
145
|
+
const values = {
|
|
146
|
+
brandName: options.name?.trim() || current.brandName,
|
|
147
|
+
shortName: options.shortName?.trim() || current.shortName,
|
|
148
|
+
baseUrl: options.baseUrl?.trim() || current.baseUrl,
|
|
149
|
+
phone: options.phone?.trim() || current.phone,
|
|
150
|
+
phoneDisplay: options.phoneDisplay?.trim() || current.phoneDisplay,
|
|
151
|
+
countryCode: options.countryCode?.trim() || current.countryCode,
|
|
152
|
+
email: options.email?.trim() || current.email,
|
|
153
|
+
address: options.address?.trim() || current.address,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
siteContent = applyBrandToSiteContent(siteContent, values);
|
|
157
|
+
await writeFile(siteTsPath, siteContent, "utf8");
|
|
158
|
+
console.log("Successfully updated brand and contact configurations in constants/site.ts.");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @param {string} targetDir
|
|
163
|
+
* @param {BrandOptions} options
|
|
164
|
+
*/
|
|
165
|
+
export async function promptAndApplyBrand(targetDir, options = {}) {
|
|
166
|
+
if (options.yes) {
|
|
167
|
+
await applyBrand(targetDir, options);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const siteTsPath = await findSiteTs(targetDir);
|
|
172
|
+
if (!siteTsPath) {
|
|
173
|
+
console.error(
|
|
174
|
+
"Error: Could not locate constants/site.ts. Make sure you are inside an initialized tempjs template directory."
|
|
175
|
+
);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let siteContent = await readFile(siteTsPath, "utf8");
|
|
180
|
+
const current = parseCurrentBrandValues(siteContent);
|
|
181
|
+
|
|
182
|
+
const rl = createInterface({ input, output });
|
|
183
|
+
try {
|
|
184
|
+
console.log("\n--- Configure Brand & Contact Details ---");
|
|
185
|
+
|
|
186
|
+
const brandName =
|
|
187
|
+
(await rl.question(`Brand Name [${current.brandName}]: `)).trim() || current.brandName;
|
|
188
|
+
const shortName =
|
|
189
|
+
(await rl.question(`Short/Display Name [${current.shortName}]: `)).trim() || current.shortName;
|
|
190
|
+
const baseUrl =
|
|
191
|
+
(await rl.question(`Base URL [${current.baseUrl}]: `)).trim() || current.baseUrl;
|
|
192
|
+
const phone =
|
|
193
|
+
(await rl.question(`Contact Phone Number [${current.phone}]: `)).trim() || current.phone;
|
|
194
|
+
const phoneDisplay =
|
|
195
|
+
(await rl.question(`Display Phone Number [${current.phoneDisplay}]: `)).trim() ||
|
|
196
|
+
current.phoneDisplay;
|
|
197
|
+
const countryCode =
|
|
198
|
+
(await rl.question(`Country Code [${current.countryCode}]: `)).trim() || current.countryCode;
|
|
199
|
+
const email =
|
|
200
|
+
(await rl.question(`Contact Email [${current.email}]: `)).trim() || current.email;
|
|
201
|
+
const address =
|
|
202
|
+
(await rl.question(`Full Address [${current.address}]: `)).trim() || current.address;
|
|
203
|
+
|
|
204
|
+
siteContent = applyBrandToSiteContent(siteContent, {
|
|
205
|
+
brandName,
|
|
206
|
+
shortName,
|
|
207
|
+
baseUrl,
|
|
208
|
+
phone,
|
|
209
|
+
phoneDisplay,
|
|
210
|
+
countryCode,
|
|
211
|
+
email,
|
|
212
|
+
address,
|
|
213
|
+
});
|
|
102
214
|
|
|
103
215
|
await writeFile(siteTsPath, siteContent, "utf8");
|
|
104
216
|
console.log("Successfully updated brand and contact configurations in constants/site.ts.");
|
package/cli/config.js
CHANGED
|
@@ -5,7 +5,20 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
6
|
|
|
7
7
|
/** @typedef {{ owner: string, repo: string, branch: string, templatesPath: string }} RepositoryConfig */
|
|
8
|
-
/** @typedef {{
|
|
8
|
+
/** @typedef {{
|
|
9
|
+
* directory: string,
|
|
10
|
+
* name: string,
|
|
11
|
+
* description: string,
|
|
12
|
+
* version?: string,
|
|
13
|
+
* stack?: string[],
|
|
14
|
+
* packageManager?: string,
|
|
15
|
+
* node?: string,
|
|
16
|
+
* setupTime?: string,
|
|
17
|
+
* docker?: boolean,
|
|
18
|
+
* tags?: string[],
|
|
19
|
+
* features?: string[],
|
|
20
|
+
* docs?: string
|
|
21
|
+
* }} TemplateEntry */
|
|
9
22
|
/** @typedef {{ repository: RepositoryConfig, templates: Record<string, TemplateEntry> }} Manifest */
|
|
10
23
|
|
|
11
24
|
/**
|
package/cli/copy.js
CHANGED
|
@@ -9,23 +9,10 @@ import {
|
|
|
9
9
|
statSync,
|
|
10
10
|
} from "node:fs";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
+
import { NEVER_COPY_NAMES, shouldSkipFileName } from "./fs-ignore.js";
|
|
12
13
|
|
|
13
|
-
const NEVER_COPY = new Set([".git", ".gitignore.bak"]);
|
|
14
14
|
const NEVER_OVERWRITE = new Set([".git"]);
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
* Files that must never be transferred into a generated project.
|
|
18
|
-
* @param {string} name
|
|
19
|
-
* @returns {boolean}
|
|
20
|
-
*/
|
|
21
|
-
function shouldSkipFile(name) {
|
|
22
|
-
if (NEVER_COPY.has(name)) return true;
|
|
23
|
-
if (name === ".env" || name.startsWith(".env.")) {
|
|
24
|
-
return name !== ".env.example";
|
|
25
|
-
}
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
16
|
/**
|
|
30
17
|
* @param {string} dir
|
|
31
18
|
* @returns {string[]}
|
|
@@ -75,7 +62,7 @@ function collectConflicts(sourceDir, targetDir, relative, conflicts) {
|
|
|
75
62
|
|
|
76
63
|
const sourceNames = readdirSync(currentSource);
|
|
77
64
|
for (const name of sourceNames) {
|
|
78
|
-
if (
|
|
65
|
+
if (shouldSkipFileName(name)) continue;
|
|
79
66
|
|
|
80
67
|
const relPath = relative ? join(relative, name) : name;
|
|
81
68
|
const sourcePath = join(sourceDir, relPath);
|
|
@@ -126,7 +113,7 @@ function copyRecursive(sourceRoot, targetRoot, relative) {
|
|
|
126
113
|
const names = readdirSync(sourcePath);
|
|
127
114
|
|
|
128
115
|
for (const name of names) {
|
|
129
|
-
if (
|
|
116
|
+
if (shouldSkipFileName(name) || NEVER_COPY_NAMES.has(name)) continue;
|
|
130
117
|
|
|
131
118
|
const relPath = relative ? join(relative, name) : name;
|
|
132
119
|
const from = join(sourceRoot, relPath);
|
|
@@ -138,7 +125,6 @@ function copyRecursive(sourceRoot, targetRoot, relative) {
|
|
|
138
125
|
|
|
139
126
|
const stat = lstatSync(from);
|
|
140
127
|
if (stat.isSymbolicLink()) {
|
|
141
|
-
const linkTarget = readlinkSync(from);
|
|
142
128
|
mkdirSync(join(to, ".."), { recursive: true });
|
|
143
129
|
cpSync(from, to, { recursive: true, force: true });
|
|
144
130
|
continue;
|
|
@@ -158,7 +144,7 @@ function copyRecursive(sourceRoot, targetRoot, relative) {
|
|
|
158
144
|
* @param {string} dir
|
|
159
145
|
*/
|
|
160
146
|
export function removeDirectory(dir) {
|
|
161
|
-
if (existsSync(dir)) {
|
|
147
|
+
if (dir && existsSync(dir)) {
|
|
162
148
|
rmSync(dir, { recursive: true, force: true });
|
|
163
149
|
}
|
|
164
150
|
}
|
package/cli/db-setup.js
CHANGED
|
@@ -6,11 +6,29 @@ import { join, basename, resolve } from "node:path";
|
|
|
6
6
|
import crypto from "node:crypto";
|
|
7
7
|
import { execSync } from "node:child_process";
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {{
|
|
11
|
+
* yes?: boolean,
|
|
12
|
+
* dbHost?: string,
|
|
13
|
+
* dbPort?: string,
|
|
14
|
+
* dbUser?: string,
|
|
15
|
+
* dbPassword?: string,
|
|
16
|
+
* dbName?: string,
|
|
17
|
+
* adminUser?: string,
|
|
18
|
+
* adminPassword?: string,
|
|
19
|
+
* skipDbPush?: boolean,
|
|
20
|
+
* dbPush?: boolean
|
|
21
|
+
* }} DbSetupOptions
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} targetDir
|
|
26
|
+
* @param {DbSetupOptions} options
|
|
27
|
+
*/
|
|
28
|
+
export async function setupDb(targetDir, options = {}) {
|
|
10
29
|
const envPath = join(targetDir, ".env");
|
|
11
30
|
const envExamplePath = join(targetDir, ".env.example");
|
|
12
31
|
|
|
13
|
-
// Load existing env values as defaults if .env exists
|
|
14
32
|
let existingEnv = "";
|
|
15
33
|
if (existsSync(envPath)) {
|
|
16
34
|
try {
|
|
@@ -20,22 +38,23 @@ export async function promptAndSetupDb(targetDir) {
|
|
|
20
38
|
}
|
|
21
39
|
}
|
|
22
40
|
|
|
23
|
-
// Helper to extract env var
|
|
24
41
|
const getEnvVal = (key, fallback) => {
|
|
25
42
|
const match = existingEnv.match(new RegExp(`^${key}\\s*=\\s*["']?([^"'\r\n]+)["']?`, "m"));
|
|
26
43
|
return match ? match[1] : fallback;
|
|
27
44
|
};
|
|
28
45
|
|
|
29
|
-
// Parse existing DB URL if any
|
|
30
46
|
const currentDbUrl = getEnvVal("DATABASE_URL", "");
|
|
31
47
|
let defaultHost = "localhost";
|
|
32
48
|
let defaultPort = "3306";
|
|
33
49
|
let defaultUser = "root";
|
|
34
50
|
let defaultPass = "";
|
|
35
|
-
let defaultDbName =
|
|
51
|
+
let defaultDbName =
|
|
52
|
+
basename(resolve(targetDir)).toLowerCase().replace(/[^a-z0-9_]/g, "_") + "_db";
|
|
36
53
|
|
|
37
54
|
if (currentDbUrl) {
|
|
38
|
-
const urlMatch = currentDbUrl.match(
|
|
55
|
+
const urlMatch = currentDbUrl.match(
|
|
56
|
+
/mysql:\/\/([^:@]+)(?::([^@]+))?@([^:/]+)(?::(\d+))?\/([^?]+)/
|
|
57
|
+
);
|
|
39
58
|
if (urlMatch) {
|
|
40
59
|
defaultUser = urlMatch[1];
|
|
41
60
|
defaultPass = urlMatch[2] || "";
|
|
@@ -48,71 +67,173 @@ export async function promptAndSetupDb(targetDir) {
|
|
|
48
67
|
const defaultAdminUser = getEnvVal("ADMIN_USER", "admin");
|
|
49
68
|
const defaultAdminPass = getEnvVal("ADMIN_PASSWORD", crypto.randomBytes(4).toString("hex"));
|
|
50
69
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
const host = options.dbHost?.trim() || defaultHost;
|
|
71
|
+
const port = options.dbPort?.trim() || defaultPort;
|
|
72
|
+
const user = options.dbUser?.trim() || defaultUser;
|
|
73
|
+
const pass = options.dbPassword !== undefined ? options.dbPassword : defaultPass;
|
|
74
|
+
const dbName = options.dbName?.trim() || defaultDbName;
|
|
75
|
+
const adminUser = options.adminUser?.trim() || defaultAdminUser;
|
|
76
|
+
const adminPass = options.adminPassword?.trim() || defaultAdminPass;
|
|
54
77
|
|
|
55
|
-
|
|
56
|
-
const port = (await rl.question(`MySQL Database Port [${defaultPort}]: `)).trim() || defaultPort;
|
|
57
|
-
const user = (await rl.question(`MySQL Database Username [${defaultUser}]: `)).trim() || defaultUser;
|
|
58
|
-
const pass = (await rl.question(`MySQL Database Password [${defaultPass ? "*****" : "(empty)"}]: `)).trim() || defaultPass;
|
|
59
|
-
const dbName = (await rl.question(`MySQL Database Name [${defaultDbName}]: `)).trim() || defaultDbName;
|
|
78
|
+
const dbUrl = `mysql://${user}${pass ? `:${encodeURIComponent(pass)}` : ""}@${host}:${port}/${dbName}`;
|
|
60
79
|
|
|
61
|
-
|
|
62
|
-
|
|
80
|
+
let envContent = "";
|
|
81
|
+
if (existsSync(envExamplePath)) {
|
|
82
|
+
envContent = await readFile(envExamplePath, "utf8");
|
|
83
|
+
}
|
|
63
84
|
|
|
64
|
-
|
|
65
|
-
const dbUrl = `mysql://${user}${pass ? `:${encodeURIComponent(pass)}` : ""}@${host}:${port}/${dbName}`;
|
|
85
|
+
const randomSecret = crypto.randomBytes(32).toString("base64");
|
|
66
86
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
87
|
+
const updateEnvKey = (content, key, value) => {
|
|
88
|
+
const regex = new RegExp(`^#?\\s*${key}\\s*=.*$`, "m");
|
|
89
|
+
if (regex.test(content)) {
|
|
90
|
+
return content.replace(regex, `${key}="${value}"`);
|
|
71
91
|
}
|
|
92
|
+
return `${content.trim()}\n${key}="${value}"\n`;
|
|
93
|
+
};
|
|
72
94
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return content.replace(regex, `${key}="${value}"`);
|
|
81
|
-
}
|
|
82
|
-
return `${content.trim()}\n${key}="${value}"\n`;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
if (envContent) {
|
|
86
|
-
envContent = updateEnvKey(envContent, "DATABASE_URL", dbUrl);
|
|
87
|
-
envContent = updateEnvKey(envContent, "AUTH_SECRET", randomSecret);
|
|
88
|
-
envContent = updateEnvKey(envContent, "ADMIN_USER", adminUser);
|
|
89
|
-
envContent = updateEnvKey(envContent, "ADMIN_PASSWORD", adminPass);
|
|
90
|
-
} else {
|
|
91
|
-
envContent = `DATABASE_URL="${dbUrl}"
|
|
95
|
+
if (envContent) {
|
|
96
|
+
envContent = updateEnvKey(envContent, "DATABASE_URL", dbUrl);
|
|
97
|
+
envContent = updateEnvKey(envContent, "AUTH_SECRET", randomSecret);
|
|
98
|
+
envContent = updateEnvKey(envContent, "ADMIN_USER", adminUser);
|
|
99
|
+
envContent = updateEnvKey(envContent, "ADMIN_PASSWORD", adminPass);
|
|
100
|
+
} else {
|
|
101
|
+
envContent = `DATABASE_URL="${dbUrl}"
|
|
92
102
|
AUTH_SECRET="${randomSecret}"
|
|
93
103
|
ADMIN_USER="${adminUser}"
|
|
94
104
|
ADMIN_PASSWORD="${adminPass}"
|
|
95
105
|
AUTH_TRUST_HOST="true"
|
|
96
106
|
`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await writeFile(envPath, envContent, "utf8");
|
|
110
|
+
console.log("Successfully created/updated .env configuration file.");
|
|
111
|
+
|
|
112
|
+
const shouldPush =
|
|
113
|
+
options.dbPush ||
|
|
114
|
+
(options.yes && !options.skipDbPush);
|
|
115
|
+
|
|
116
|
+
if (shouldPush) {
|
|
117
|
+
console.log("\nRunning Prisma Database Push (npx prisma db push)...");
|
|
118
|
+
try {
|
|
119
|
+
execSync("npx prisma db push", { cwd: targetDir, stdio: "inherit" });
|
|
120
|
+
console.log("\nSuccessfully synchronized database schemas.");
|
|
121
|
+
} catch {
|
|
122
|
+
console.error(
|
|
123
|
+
"\nWarning: Database synchronization failed. Make sure your MySQL server is running and the database details are correct."
|
|
124
|
+
);
|
|
125
|
+
console.error("You can run this manually later inside the project folder via: npx prisma db push");
|
|
97
126
|
}
|
|
127
|
+
} else if (options.yes && options.skipDbPush) {
|
|
128
|
+
console.log("\nSkipped database table synchronization (--skip-db-push).");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {string} targetDir
|
|
134
|
+
* @param {DbSetupOptions} options
|
|
135
|
+
*/
|
|
136
|
+
export async function promptAndSetupDb(targetDir, options = {}) {
|
|
137
|
+
if (options.yes) {
|
|
138
|
+
await setupDb(targetDir, options);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
98
141
|
|
|
99
|
-
|
|
100
|
-
|
|
142
|
+
const envPath = join(targetDir, ".env");
|
|
143
|
+
const envExamplePath = join(targetDir, ".env.example");
|
|
144
|
+
|
|
145
|
+
let existingEnv = "";
|
|
146
|
+
if (existsSync(envPath)) {
|
|
147
|
+
try {
|
|
148
|
+
existingEnv = await readFile(envPath, "utf8");
|
|
149
|
+
} catch {
|
|
150
|
+
// Ignore
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const getEnvVal = (key, fallback) => {
|
|
155
|
+
const match = existingEnv.match(new RegExp(`^${key}\\s*=\\s*["']?([^"'\r\n]+)["']?`, "m"));
|
|
156
|
+
return match ? match[1] : fallback;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const currentDbUrl = getEnvVal("DATABASE_URL", "");
|
|
160
|
+
let defaultHost = "localhost";
|
|
161
|
+
let defaultPort = "3306";
|
|
162
|
+
let defaultUser = "root";
|
|
163
|
+
let defaultPass = "";
|
|
164
|
+
let defaultDbName =
|
|
165
|
+
basename(resolve(targetDir)).toLowerCase().replace(/[^a-z0-9_]/g, "_") + "_db";
|
|
166
|
+
|
|
167
|
+
if (currentDbUrl) {
|
|
168
|
+
const urlMatch = currentDbUrl.match(
|
|
169
|
+
/mysql:\/\/([^:@]+)(?::([^@]+))?@([^:/]+)(?::(\d+))?\/([^?]+)/
|
|
170
|
+
);
|
|
171
|
+
if (urlMatch) {
|
|
172
|
+
defaultUser = urlMatch[1];
|
|
173
|
+
defaultPass = urlMatch[2] || "";
|
|
174
|
+
defaultHost = urlMatch[3];
|
|
175
|
+
defaultPort = urlMatch[4] || "3306";
|
|
176
|
+
defaultDbName = urlMatch[5];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const defaultAdminUser = getEnvVal("ADMIN_USER", "admin");
|
|
181
|
+
const defaultAdminPass = getEnvVal("ADMIN_PASSWORD", crypto.randomBytes(4).toString("hex"));
|
|
182
|
+
|
|
183
|
+
const rl = createInterface({ input, output });
|
|
184
|
+
try {
|
|
185
|
+
console.log("\n--- Configure Database & Environment Variables ---");
|
|
101
186
|
|
|
102
|
-
|
|
103
|
-
|
|
187
|
+
const host =
|
|
188
|
+
(await rl.question(`MySQL Database Host [${defaultHost}]: `)).trim() || defaultHost;
|
|
189
|
+
const port =
|
|
190
|
+
(await rl.question(`MySQL Database Port [${defaultPort}]: `)).trim() || defaultPort;
|
|
191
|
+
const user =
|
|
192
|
+
(await rl.question(`MySQL Database Username [${defaultUser}]: `)).trim() || defaultUser;
|
|
193
|
+
const pass =
|
|
194
|
+
(await rl.question(`MySQL Database Password [${defaultPass ? "*****" : "(empty)"}]: `)).trim() ||
|
|
195
|
+
defaultPass;
|
|
196
|
+
const dbName =
|
|
197
|
+
(await rl.question(`MySQL Database Name [${defaultDbName}]: `)).trim() || defaultDbName;
|
|
198
|
+
const adminUser =
|
|
199
|
+
(await rl.question(`Admin Portal Username [${defaultAdminUser}]: `)).trim() ||
|
|
200
|
+
defaultAdminUser;
|
|
201
|
+
const adminPass =
|
|
202
|
+
(await rl.question(`Admin Portal Password [${defaultAdminPass}]: `)).trim() ||
|
|
203
|
+
defaultAdminPass;
|
|
204
|
+
|
|
205
|
+
await setupDb(targetDir, {
|
|
206
|
+
dbHost: host,
|
|
207
|
+
dbPort: port,
|
|
208
|
+
dbUser: user,
|
|
209
|
+
dbPassword: pass,
|
|
210
|
+
dbName,
|
|
211
|
+
adminUser,
|
|
212
|
+
adminPassword: adminPass,
|
|
213
|
+
skipDbPush: true,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const runMigrate = await rl.question(
|
|
217
|
+
"\nDo you want to initialize the database tables now using Prisma? (y/n) [y]: "
|
|
218
|
+
);
|
|
104
219
|
const choice = runMigrate.trim().toLowerCase();
|
|
105
220
|
if (choice === "" || choice === "y" || choice === "yes") {
|
|
106
221
|
console.log("\nRunning Prisma Database Push (npx prisma db push)...");
|
|
107
222
|
try {
|
|
108
223
|
execSync("npx prisma db push", { cwd: targetDir, stdio: "inherit" });
|
|
109
224
|
console.log("\nSuccessfully synchronized database schemas.");
|
|
110
|
-
} catch
|
|
111
|
-
console.error(
|
|
112
|
-
|
|
225
|
+
} catch {
|
|
226
|
+
console.error(
|
|
227
|
+
"\nWarning: Database synchronization failed. Make sure your MySQL server is running and the database details are correct."
|
|
228
|
+
);
|
|
229
|
+
console.error(
|
|
230
|
+
"You can run this manually later inside the project folder via: npx prisma db push"
|
|
231
|
+
);
|
|
113
232
|
}
|
|
114
233
|
} else {
|
|
115
|
-
console.log(
|
|
234
|
+
console.log(
|
|
235
|
+
"\nSkipped database table synchronization. Remember to run 'npx prisma db push' before running the app."
|
|
236
|
+
);
|
|
116
237
|
}
|
|
117
238
|
} finally {
|
|
118
239
|
rl.close();
|