@navneet_25/tempjs 1.0.4 → 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 -33
- package/cli/brand-manager.js +220 -0
- package/cli/config.js +14 -1
- package/cli/copy.js +4 -18
- package/cli/db-setup.js +241 -0
- package/cli/fetch.js +47 -29
- package/cli/file-tree.js +113 -0
- package/cli/fs-ignore.js +60 -0
- package/cli/index.js +144 -98
- 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 +33 -3
- package/cli/update.js +212 -0
- package/package.json +6 -2
- package/templates.json +33 -3
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { findSiteTs } from "./theme-manager.js";
|
|
5
|
+
|
|
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
|
+
*/
|
|
19
|
+
|
|
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
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} baseUrl
|
|
38
|
+
*/
|
|
39
|
+
function deriveWwwHost(baseUrl) {
|
|
40
|
+
try {
|
|
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;
|
|
74
|
+
|
|
75
|
+
const wwwHost = deriveWwwHost(baseUrl);
|
|
76
|
+
|
|
77
|
+
const newBrandBlock = `brand: {
|
|
78
|
+
name: "${brandName}",
|
|
79
|
+
shortName: "${shortName}",
|
|
80
|
+
tagline: "Where Nature Meets Refined Comfort",
|
|
81
|
+
developerName: "${brandName}",
|
|
82
|
+
channelPartner: "${brandName} Partner",
|
|
83
|
+
copyright: "${brandName}. All Rights Reserved.",
|
|
84
|
+
managedBy: "Managed by ${brandName}.",
|
|
85
|
+
}`;
|
|
86
|
+
|
|
87
|
+
const newDomainBlock = `domain: {
|
|
88
|
+
baseUrl: "${baseUrl}",
|
|
89
|
+
wwwHost: "${wwwHost}",
|
|
90
|
+
}`;
|
|
91
|
+
|
|
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
|
+
}
|
|
99
|
+
|
|
100
|
+
const newContactBlock = `contact: {
|
|
101
|
+
phone: "${phone}",
|
|
102
|
+
phoneDisplay: "${phoneDisplay}",
|
|
103
|
+
countryCode: "${countryCode}",
|
|
104
|
+
email: "${email}",
|
|
105
|
+
address: {
|
|
106
|
+
locality: "${locality}",
|
|
107
|
+
region: "${region}",
|
|
108
|
+
country: "IN",
|
|
109
|
+
full: "${address}",
|
|
110
|
+
},
|
|
111
|
+
}`;
|
|
112
|
+
|
|
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
|
+
});
|
|
214
|
+
|
|
215
|
+
await writeFile(siteTsPath, siteContent, "utf8");
|
|
216
|
+
console.log("Successfully updated brand and contact configurations in constants/site.ts.");
|
|
217
|
+
} finally {
|
|
218
|
+
rl.close();
|
|
219
|
+
}
|
|
220
|
+
}
|
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
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { join, basename, resolve } from "node:path";
|
|
6
|
+
import crypto from "node:crypto";
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
8
|
+
|
|
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 = {}) {
|
|
29
|
+
const envPath = join(targetDir, ".env");
|
|
30
|
+
const envExamplePath = join(targetDir, ".env.example");
|
|
31
|
+
|
|
32
|
+
let existingEnv = "";
|
|
33
|
+
if (existsSync(envPath)) {
|
|
34
|
+
try {
|
|
35
|
+
existingEnv = await readFile(envPath, "utf8");
|
|
36
|
+
} catch {
|
|
37
|
+
// Ignore
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const getEnvVal = (key, fallback) => {
|
|
42
|
+
const match = existingEnv.match(new RegExp(`^${key}\\s*=\\s*["']?([^"'\r\n]+)["']?`, "m"));
|
|
43
|
+
return match ? match[1] : fallback;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const currentDbUrl = getEnvVal("DATABASE_URL", "");
|
|
47
|
+
let defaultHost = "localhost";
|
|
48
|
+
let defaultPort = "3306";
|
|
49
|
+
let defaultUser = "root";
|
|
50
|
+
let defaultPass = "";
|
|
51
|
+
let defaultDbName =
|
|
52
|
+
basename(resolve(targetDir)).toLowerCase().replace(/[^a-z0-9_]/g, "_") + "_db";
|
|
53
|
+
|
|
54
|
+
if (currentDbUrl) {
|
|
55
|
+
const urlMatch = currentDbUrl.match(
|
|
56
|
+
/mysql:\/\/([^:@]+)(?::([^@]+))?@([^:/]+)(?::(\d+))?\/([^?]+)/
|
|
57
|
+
);
|
|
58
|
+
if (urlMatch) {
|
|
59
|
+
defaultUser = urlMatch[1];
|
|
60
|
+
defaultPass = urlMatch[2] || "";
|
|
61
|
+
defaultHost = urlMatch[3];
|
|
62
|
+
defaultPort = urlMatch[4] || "3306";
|
|
63
|
+
defaultDbName = urlMatch[5];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const defaultAdminUser = getEnvVal("ADMIN_USER", "admin");
|
|
68
|
+
const defaultAdminPass = getEnvVal("ADMIN_PASSWORD", crypto.randomBytes(4).toString("hex"));
|
|
69
|
+
|
|
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;
|
|
77
|
+
|
|
78
|
+
const dbUrl = `mysql://${user}${pass ? `:${encodeURIComponent(pass)}` : ""}@${host}:${port}/${dbName}`;
|
|
79
|
+
|
|
80
|
+
let envContent = "";
|
|
81
|
+
if (existsSync(envExamplePath)) {
|
|
82
|
+
envContent = await readFile(envExamplePath, "utf8");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const randomSecret = crypto.randomBytes(32).toString("base64");
|
|
86
|
+
|
|
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}"`);
|
|
91
|
+
}
|
|
92
|
+
return `${content.trim()}\n${key}="${value}"\n`;
|
|
93
|
+
};
|
|
94
|
+
|
|
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}"
|
|
102
|
+
AUTH_SECRET="${randomSecret}"
|
|
103
|
+
ADMIN_USER="${adminUser}"
|
|
104
|
+
ADMIN_PASSWORD="${adminPass}"
|
|
105
|
+
AUTH_TRUST_HOST="true"
|
|
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");
|
|
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
|
+
}
|
|
141
|
+
|
|
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 ---");
|
|
186
|
+
|
|
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
|
+
);
|
|
219
|
+
const choice = runMigrate.trim().toLowerCase();
|
|
220
|
+
if (choice === "" || choice === "y" || choice === "yes") {
|
|
221
|
+
console.log("\nRunning Prisma Database Push (npx prisma db push)...");
|
|
222
|
+
try {
|
|
223
|
+
execSync("npx prisma db push", { cwd: targetDir, stdio: "inherit" });
|
|
224
|
+
console.log("\nSuccessfully synchronized database schemas.");
|
|
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
|
+
);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
console.log(
|
|
235
|
+
"\nSkipped database table synchronization. Remember to run 'npx prisma db push' before running the app."
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
} finally {
|
|
239
|
+
rl.close();
|
|
240
|
+
}
|
|
241
|
+
}
|
package/cli/fetch.js
CHANGED
|
@@ -1,39 +1,44 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { createWriteStream, existsSync } from "node:fs";
|
|
2
|
+
import { createWriteStream, existsSync, statSync } from "node:fs";
|
|
3
3
|
import { mkdir, mkdtemp, readdir, readFile, rm, stat } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { pipeline } from "node:stream/promises";
|
|
7
7
|
import { Readable } from "node:stream";
|
|
8
|
+
import { shouldSkipFileName } from "./fs-ignore.js";
|
|
9
|
+
import { countFiles } from "./file-tree.js";
|
|
8
10
|
|
|
9
11
|
const CODELOAD_BASE = "https://codeload.github.com";
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*
|
|
14
|
+
* @typedef {{ bytes: number, files: number, durationMs: number, source: string }} FetchStats
|
|
15
|
+
* @typedef {{ templateRoot: string, cleanupDir: string, stats: FetchStats }} FetchResult
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
18
19
|
* @param {import('./config.js').RepositoryConfig} repo
|
|
19
20
|
* @param {string} templateDirectory
|
|
20
|
-
* @returns {Promise<
|
|
21
|
+
* @returns {Promise<FetchResult>}
|
|
21
22
|
*/
|
|
22
23
|
export async function fetchTemplateFromGitHub(repo, templateDirectory) {
|
|
24
|
+
const startMs = performance.now();
|
|
23
25
|
const tempRoot = await mkdtemp(join(tmpdir(), "template-cli-"));
|
|
24
26
|
const tarballPath = join(tempRoot, "archive.tar.gz");
|
|
25
27
|
const templateRoot = join(tempRoot, "template");
|
|
26
28
|
|
|
27
29
|
try {
|
|
28
|
-
await downloadTarball(repo, tarballPath);
|
|
29
|
-
await extractTemplateFromTarball(
|
|
30
|
-
tarballPath,
|
|
31
|
-
templateRoot,
|
|
32
|
-
repo,
|
|
33
|
-
templateDirectory
|
|
34
|
-
);
|
|
30
|
+
const bytes = await downloadTarball(repo, tarballPath);
|
|
31
|
+
await extractTemplateFromTarball(tarballPath, templateRoot, repo, templateDirectory);
|
|
35
32
|
await readFile(join(templateRoot, "package.json"));
|
|
36
|
-
|
|
33
|
+
|
|
34
|
+
const durationMs = performance.now() - startMs;
|
|
35
|
+
const files = countFiles(templateRoot);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
templateRoot,
|
|
39
|
+
cleanupDir: tempRoot,
|
|
40
|
+
stats: { bytes, files, durationMs, source: "remote" },
|
|
41
|
+
};
|
|
37
42
|
} catch (error) {
|
|
38
43
|
await rm(tempRoot, { recursive: true, force: true });
|
|
39
44
|
throw error;
|
|
@@ -43,6 +48,7 @@ export async function fetchTemplateFromGitHub(repo, templateDirectory) {
|
|
|
43
48
|
/**
|
|
44
49
|
* @param {import('./config.js').RepositoryConfig} repo
|
|
45
50
|
* @param {string} destPath
|
|
51
|
+
* @returns {Promise<number>} Downloaded bytes
|
|
46
52
|
*/
|
|
47
53
|
async function downloadTarball(repo, destPath) {
|
|
48
54
|
const branch = encodeURIComponent(repo.branch);
|
|
@@ -69,6 +75,7 @@ async function downloadTarball(repo, destPath) {
|
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
await pipeline(Readable.fromWeb(response.body), createWriteStream(destPath));
|
|
78
|
+
return statSync(destPath).size;
|
|
72
79
|
}
|
|
73
80
|
|
|
74
81
|
/**
|
|
@@ -128,13 +135,12 @@ async function extractTemplateFromTarball(
|
|
|
128
135
|
}
|
|
129
136
|
|
|
130
137
|
/**
|
|
131
|
-
* Remove files that must never appear in generated projects.
|
|
132
138
|
* @param {string} dir
|
|
133
139
|
*/
|
|
134
140
|
async function removeSkippedFiles(dir) {
|
|
135
141
|
if (!existsSync(dir)) return;
|
|
136
142
|
|
|
137
|
-
const entries = await
|
|
143
|
+
const entries = await readDirEntries(dir);
|
|
138
144
|
for (const entry of entries) {
|
|
139
145
|
const fullPath = join(dir, entry.name);
|
|
140
146
|
if (entry.isDirectory) {
|
|
@@ -155,7 +161,7 @@ async function removeSkippedFiles(dir) {
|
|
|
155
161
|
/**
|
|
156
162
|
* @param {string} dir
|
|
157
163
|
*/
|
|
158
|
-
async function
|
|
164
|
+
async function readDirEntries(dir) {
|
|
159
165
|
const names = await readdir(dir);
|
|
160
166
|
const result = [];
|
|
161
167
|
for (const name of names) {
|
|
@@ -166,16 +172,6 @@ async function readDirSafe(dir) {
|
|
|
166
172
|
return result;
|
|
167
173
|
}
|
|
168
174
|
|
|
169
|
-
/**
|
|
170
|
-
* @param {string} name
|
|
171
|
-
*/
|
|
172
|
-
function shouldSkipFileName(name) {
|
|
173
|
-
if (name === ".env" || (name.startsWith(".env.") && name !== ".env.example")) {
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
|
-
return false;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
175
|
function buildHeaders() {
|
|
180
176
|
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
181
177
|
if (token) {
|
|
@@ -199,3 +195,25 @@ export async function resolveLocalTemplate(packageRoot, templatesPath, templateD
|
|
|
199
195
|
return null;
|
|
200
196
|
}
|
|
201
197
|
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @param {string} packageRoot
|
|
201
|
+
* @param {string} templatesPath
|
|
202
|
+
* @param {string} templateDirectory
|
|
203
|
+
* @returns {Promise<FetchResult | null>}
|
|
204
|
+
*/
|
|
205
|
+
export async function loadLocalTemplate(packageRoot, templatesPath, templateDirectory) {
|
|
206
|
+
const localPath = await resolveLocalTemplate(packageRoot, templatesPath, templateDirectory);
|
|
207
|
+
if (!localPath) return null;
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
templateRoot: localPath,
|
|
211
|
+
cleanupDir: "",
|
|
212
|
+
stats: {
|
|
213
|
+
bytes: 0,
|
|
214
|
+
files: countFiles(localPath),
|
|
215
|
+
durationMs: 0,
|
|
216
|
+
source: "local",
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|