@navneet_25/tempjs 1.0.3 → 1.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/README.md +7 -1
- package/cli/brand-manager.js +108 -0
- package/cli/db-setup.js +120 -0
- package/cli/index.js +58 -8
- package/cli/theme-manager.js +356 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,12 @@ tempjs list
|
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
36
|
tempjs list # show available templates
|
|
37
|
-
tempjs hotel # create project from hotel template
|
|
37
|
+
tempjs hotel # create project from hotel template using default theme
|
|
38
|
+
tempjs hotel config # create project and run full interactive configuration (theme, font, brand, database)
|
|
39
|
+
tempjs theme # change/reset the theme of an initialized project
|
|
40
|
+
tempjs font # change/reset the font pairing of an initialized project
|
|
41
|
+
tempjs brand # configure brand identity & contact info of an initialized project
|
|
42
|
+
tempjs init-db # set up .env file and run database schema sync on an initialized project
|
|
38
43
|
tempjs real-estate --force # overwrite existing files
|
|
39
44
|
tempjs --help # show help
|
|
40
45
|
```
|
|
@@ -43,6 +48,7 @@ tempjs --help # show help
|
|
|
43
48
|
|
|
44
49
|
| Option | Description |
|
|
45
50
|
|---------------|-------------|
|
|
51
|
+
| `--config` | Prompt for full configuration (theme, font, brand, db) during template initialization |
|
|
46
52
|
| `--force` | Overwrite files in the current directory without prompting |
|
|
47
53
|
| `--remote` | Fetch from GitHub even when a local template copy exists |
|
|
48
54
|
| `--init-git` | Run `git init` after copying (optional) |
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
export async function promptAndApplyBrand(targetDir) {
|
|
7
|
+
const siteTsPath = await findSiteTs(targetDir);
|
|
8
|
+
if (!siteTsPath) {
|
|
9
|
+
console.error("Error: Could not locate constants/site.ts. Make sure you are inside an initialized tempjs template directory.");
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Load current values if possible to provide defaults
|
|
14
|
+
let siteContent = await readFile(siteTsPath, "utf8");
|
|
15
|
+
|
|
16
|
+
// Parse existing values using simple regex
|
|
17
|
+
const currentBrandName = (siteContent.match(/name:\s*"([^"]+)"/) || [])[1] || "Chanakya Resort";
|
|
18
|
+
const currentShortName = (siteContent.match(/shortName:\s*"([^"]+)"/) || [])[1] || "Chanakya";
|
|
19
|
+
const currentBaseUrl = (siteContent.match(/baseUrl:\s*"([^"]+)"/) || [])[1] || "https://chanakyaresort.com";
|
|
20
|
+
const currentPhone = (siteContent.match(/phone:\s*"([^"]+)"/) || [])[1] || "9876543210";
|
|
21
|
+
const currentPhoneDisplay = (siteContent.match(/phoneDisplay:\s*"([^"]+)"/) || [])[1] || "+91 98765 43210";
|
|
22
|
+
const currentCountryCode = (siteContent.match(/countryCode:\s*"([^"]+)"/) || [])[1] || "91";
|
|
23
|
+
const currentEmail = (siteContent.match(/email:\s*"([^"]+)"/) || [])[1] || "reservations@chanakyaresort.com";
|
|
24
|
+
const currentAddress = (siteContent.match(/full:\s*"([^"]+)"/) || [])[1] || "Lonavala, Maharashtra, India";
|
|
25
|
+
|
|
26
|
+
const rl = createInterface({ input, output });
|
|
27
|
+
try {
|
|
28
|
+
console.log("\n--- Configure Brand & Contact Details ---");
|
|
29
|
+
|
|
30
|
+
const brandName = (await rl.question(`Brand Name [${currentBrandName}]: `)).trim() || currentBrandName;
|
|
31
|
+
const shortName = (await rl.question(`Short/Display Name [${currentShortName}]: `)).trim() || currentShortName;
|
|
32
|
+
const baseUrl = (await rl.question(`Base URL [${currentBaseUrl}]: `)).trim() || currentBaseUrl;
|
|
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: {
|
|
52
|
+
name: "${brandName}",
|
|
53
|
+
shortName: "${shortName}",
|
|
54
|
+
tagline: "Where Nature Meets Refined Comfort",
|
|
55
|
+
developerName: "${brandName}",
|
|
56
|
+
channelPartner: "${brandName} Partner",
|
|
57
|
+
copyright: "${brandName}. All Rights Reserved.",
|
|
58
|
+
managedBy: "Managed by ${brandName}.",
|
|
59
|
+
}`;
|
|
60
|
+
|
|
61
|
+
const newDomainBlock = `domain: {
|
|
62
|
+
baseUrl: "${baseUrl}",
|
|
63
|
+
wwwHost: "${wwwHost}",
|
|
64
|
+
}`;
|
|
65
|
+
|
|
66
|
+
// Extract address locality/region/country or keep defaults
|
|
67
|
+
let locality = "Lonavala";
|
|
68
|
+
let region = "MH";
|
|
69
|
+
const addressParts = address.split(",");
|
|
70
|
+
if (addressParts.length >= 2) {
|
|
71
|
+
locality = addressParts[0].trim();
|
|
72
|
+
region = addressParts[1].trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const newContactBlock = `contact: {
|
|
76
|
+
phone: "${phone}",
|
|
77
|
+
phoneDisplay: "${phoneDisplay}",
|
|
78
|
+
countryCode: "${countryCode}",
|
|
79
|
+
email: "${email}",
|
|
80
|
+
address: {
|
|
81
|
+
locality: "${locality}",
|
|
82
|
+
region: "${region}",
|
|
83
|
+
country: "IN",
|
|
84
|
+
full: "${address}",
|
|
85
|
+
},
|
|
86
|
+
}`;
|
|
87
|
+
|
|
88
|
+
// Apply replacements using exact patterns matching our templates
|
|
89
|
+
const brandRegex = /brand:\s*\{[\s\S]*?\n\s*\},/;
|
|
90
|
+
const domainRegex = /domain:\s*\{[\s\S]*?\n\s*\},/;
|
|
91
|
+
const contactRegex = /contact:\s*\{[\s\S]*?address:\s*\{[\s\S]*?\}[\s\S]*?\n\s*\},/;
|
|
92
|
+
|
|
93
|
+
if (brandRegex.test(siteContent)) {
|
|
94
|
+
siteContent = siteContent.replace(brandRegex, newBrandBlock + ",");
|
|
95
|
+
}
|
|
96
|
+
if (domainRegex.test(siteContent)) {
|
|
97
|
+
siteContent = siteContent.replace(domainRegex, newDomainBlock + ",");
|
|
98
|
+
}
|
|
99
|
+
if (contactRegex.test(siteContent)) {
|
|
100
|
+
siteContent = siteContent.replace(contactRegex, newContactBlock + ",");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await writeFile(siteTsPath, siteContent, "utf8");
|
|
104
|
+
console.log("Successfully updated brand and contact configurations in constants/site.ts.");
|
|
105
|
+
} finally {
|
|
106
|
+
rl.close();
|
|
107
|
+
}
|
|
108
|
+
}
|
package/cli/db-setup.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
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
|
+
export async function promptAndSetupDb(targetDir) {
|
|
10
|
+
const envPath = join(targetDir, ".env");
|
|
11
|
+
const envExamplePath = join(targetDir, ".env.example");
|
|
12
|
+
|
|
13
|
+
// Load existing env values as defaults if .env exists
|
|
14
|
+
let existingEnv = "";
|
|
15
|
+
if (existsSync(envPath)) {
|
|
16
|
+
try {
|
|
17
|
+
existingEnv = await readFile(envPath, "utf8");
|
|
18
|
+
} catch {
|
|
19
|
+
// Ignore
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Helper to extract env var
|
|
24
|
+
const getEnvVal = (key, fallback) => {
|
|
25
|
+
const match = existingEnv.match(new RegExp(`^${key}\\s*=\\s*["']?([^"'\r\n]+)["']?`, "m"));
|
|
26
|
+
return match ? match[1] : fallback;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Parse existing DB URL if any
|
|
30
|
+
const currentDbUrl = getEnvVal("DATABASE_URL", "");
|
|
31
|
+
let defaultHost = "localhost";
|
|
32
|
+
let defaultPort = "3306";
|
|
33
|
+
let defaultUser = "root";
|
|
34
|
+
let defaultPass = "";
|
|
35
|
+
let defaultDbName = basename(resolve(targetDir)).toLowerCase().replace(/[^a-z0-9_]/g, "_") + "_db";
|
|
36
|
+
|
|
37
|
+
if (currentDbUrl) {
|
|
38
|
+
const urlMatch = currentDbUrl.match(/mysql:\/\/([^:@]+)(?::([^@]+))?@([^:/]+)(?::(\d+))?\/([^?]+)/);
|
|
39
|
+
if (urlMatch) {
|
|
40
|
+
defaultUser = urlMatch[1];
|
|
41
|
+
defaultPass = urlMatch[2] || "";
|
|
42
|
+
defaultHost = urlMatch[3];
|
|
43
|
+
defaultPort = urlMatch[4] || "3306";
|
|
44
|
+
defaultDbName = urlMatch[5];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const defaultAdminUser = getEnvVal("ADMIN_USER", "admin");
|
|
49
|
+
const defaultAdminPass = getEnvVal("ADMIN_PASSWORD", crypto.randomBytes(4).toString("hex"));
|
|
50
|
+
|
|
51
|
+
const rl = createInterface({ input, output });
|
|
52
|
+
try {
|
|
53
|
+
console.log("\n--- Configure Database & Environment Variables ---");
|
|
54
|
+
|
|
55
|
+
const host = (await rl.question(`MySQL Database Host [${defaultHost}]: `)).trim() || defaultHost;
|
|
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;
|
|
60
|
+
|
|
61
|
+
const adminUser = (await rl.question(`Admin Portal Username [${defaultAdminUser}]: `)).trim() || defaultAdminUser;
|
|
62
|
+
const adminPass = (await rl.question(`Admin Portal Password [${defaultAdminPass}]: `)).trim() || defaultAdminPass;
|
|
63
|
+
|
|
64
|
+
// Construct MySQL Connection URL
|
|
65
|
+
const dbUrl = `mysql://${user}${pass ? `:${encodeURIComponent(pass)}` : ""}@${host}:${port}/${dbName}`;
|
|
66
|
+
|
|
67
|
+
// Read template or write fresh environment file
|
|
68
|
+
let envContent = "";
|
|
69
|
+
if (existsSync(envExamplePath)) {
|
|
70
|
+
envContent = await readFile(envExamplePath, "utf8");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Generate fresh secret
|
|
74
|
+
const randomSecret = crypto.randomBytes(32).toString("base64");
|
|
75
|
+
|
|
76
|
+
// Replace or construct env keys
|
|
77
|
+
const updateEnvKey = (content, key, value) => {
|
|
78
|
+
const regex = new RegExp(`^#?\\s*${key}\\s*=.*$`, "m");
|
|
79
|
+
if (regex.test(content)) {
|
|
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}"
|
|
92
|
+
AUTH_SECRET="${randomSecret}"
|
|
93
|
+
ADMIN_USER="${adminUser}"
|
|
94
|
+
ADMIN_PASSWORD="${adminPass}"
|
|
95
|
+
AUTH_TRUST_HOST="true"
|
|
96
|
+
`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await writeFile(envPath, envContent, "utf8");
|
|
100
|
+
console.log("Successfully created/updated .env configuration file.");
|
|
101
|
+
|
|
102
|
+
// Prompt to run prisma database push
|
|
103
|
+
const runMigrate = await rl.question("\nDo you want to initialize the database tables now using Prisma? (y/n) [y]: ");
|
|
104
|
+
const choice = runMigrate.trim().toLowerCase();
|
|
105
|
+
if (choice === "" || choice === "y" || choice === "yes") {
|
|
106
|
+
console.log("\nRunning Prisma Database Push (npx prisma db push)...");
|
|
107
|
+
try {
|
|
108
|
+
execSync("npx prisma db push", { cwd: targetDir, stdio: "inherit" });
|
|
109
|
+
console.log("\nSuccessfully synchronized database schemas.");
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error("\nWarning: Database synchronization failed. Make sure your MySQL server is running and the database details are correct.");
|
|
112
|
+
console.error("You can run this manually later inside the project folder via: npx prisma db push");
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
console.log("\nSkipped database table synchronization. Remember to run 'npx prisma db push' before running the app.");
|
|
116
|
+
}
|
|
117
|
+
} finally {
|
|
118
|
+
rl.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
package/cli/index.js
CHANGED
|
@@ -4,6 +4,14 @@ import { execSync } from "node:child_process";
|
|
|
4
4
|
import { createInterface } from "node:readline/promises";
|
|
5
5
|
import { stdin as input, stdout as output } from "node:process";
|
|
6
6
|
import { loadManifest, getPackageRoot, resolveRepositoryConfig } from "./config.js";
|
|
7
|
+
import {
|
|
8
|
+
promptTheme,
|
|
9
|
+
promptFont,
|
|
10
|
+
applyThemeAndFont,
|
|
11
|
+
getSavedConfig,
|
|
12
|
+
} from "./theme-manager.js";
|
|
13
|
+
import { promptAndApplyBrand } from "./brand-manager.js";
|
|
14
|
+
import { promptAndSetupDb } from "./db-setup.js";
|
|
7
15
|
import {
|
|
8
16
|
copyTemplate,
|
|
9
17
|
findConflictingPaths,
|
|
@@ -20,9 +28,15 @@ tempjs — instantiate project templates from GitHub
|
|
|
20
28
|
USAGE
|
|
21
29
|
tempjs list
|
|
22
30
|
tempjs <template-id> [options]
|
|
31
|
+
tempjs <template-id> config Initialize with interactive theme, typography, brand & database setup
|
|
32
|
+
tempjs theme Change the project's theme in an initialized directory
|
|
33
|
+
tempjs font Change the project's font styling in an initialized directory
|
|
34
|
+
tempjs brand Configure the brand & contact info in an initialized directory
|
|
35
|
+
tempjs init-db Configure .env and sync database schema in an initialized directory
|
|
23
36
|
tempjs --help
|
|
24
37
|
|
|
25
38
|
OPTIONS
|
|
39
|
+
--config Prompt for full interactive setup (theme, font, brand, db) during initialization
|
|
26
40
|
--force Overwrite existing files in the current directory
|
|
27
41
|
--remote Fetch from GitHub even if a local template copy exists
|
|
28
42
|
--init-git Run git init after copying the template
|
|
@@ -30,8 +44,9 @@ OPTIONS
|
|
|
30
44
|
|
|
31
45
|
EXAMPLES
|
|
32
46
|
mkdir hotel-client && cd hotel-client
|
|
33
|
-
tempjs hotel
|
|
34
|
-
|
|
47
|
+
tempjs hotel config
|
|
48
|
+
tempjs brand
|
|
49
|
+
tempjs init-db
|
|
35
50
|
|
|
36
51
|
ENVIRONMENT
|
|
37
52
|
TEMPLATES_REPO_URL GitHub repo URL or owner/repo (overrides templates.json)
|
|
@@ -72,6 +87,7 @@ function parseArgs(argv) {
|
|
|
72
87
|
remote: false,
|
|
73
88
|
initGit: false,
|
|
74
89
|
help: false,
|
|
90
|
+
config: false,
|
|
75
91
|
};
|
|
76
92
|
const positionals = [];
|
|
77
93
|
|
|
@@ -80,6 +96,7 @@ function parseArgs(argv) {
|
|
|
80
96
|
else if (arg === "--remote") flags.remote = true;
|
|
81
97
|
else if (arg === "--init-git") flags.initGit = true;
|
|
82
98
|
else if (arg === "--help" || arg === "-h") flags.help = true;
|
|
99
|
+
else if (arg === "--config") flags.config = true;
|
|
83
100
|
else if (arg.startsWith("-")) {
|
|
84
101
|
throw new Error(`Unknown option: ${arg}`);
|
|
85
102
|
} else {
|
|
@@ -124,8 +141,9 @@ async function confirmOverwrite(force) {
|
|
|
124
141
|
* @param {string} targetDir
|
|
125
142
|
* @param {string} templateId
|
|
126
143
|
* @param {{ force: boolean, remote: boolean, initGit: boolean }} flags
|
|
144
|
+
* @param {boolean} runWithConfig
|
|
127
145
|
*/
|
|
128
|
-
async function runTemplate(targetDir, templateId, flags) {
|
|
146
|
+
async function runTemplate(targetDir, templateId, flags, runWithConfig = false) {
|
|
129
147
|
const manifest = loadManifest();
|
|
130
148
|
const entry = manifest.templates[templateId];
|
|
131
149
|
|
|
@@ -198,6 +216,19 @@ async function runTemplate(targetDir, templateId, flags) {
|
|
|
198
216
|
execSync("git init", { cwd: targetDir, stdio: "inherit" });
|
|
199
217
|
}
|
|
200
218
|
|
|
219
|
+
if (runWithConfig) {
|
|
220
|
+
console.log("\nConfiguring project theme and typography...");
|
|
221
|
+
const selectedTheme = await promptTheme("theme1");
|
|
222
|
+
const selectedFont = await promptFont("default");
|
|
223
|
+
await applyThemeAndFont(targetDir, selectedTheme, selectedFont);
|
|
224
|
+
|
|
225
|
+
// Interactive Brand & Contact Details configuration
|
|
226
|
+
await promptAndApplyBrand(targetDir);
|
|
227
|
+
|
|
228
|
+
// Environment & Database auto-setup
|
|
229
|
+
await promptAndSetupDb(targetDir);
|
|
230
|
+
}
|
|
231
|
+
|
|
201
232
|
console.log(`\nTemplate "${entry.name}" created successfully in ${targetDir}`);
|
|
202
233
|
console.log("\nNext steps:");
|
|
203
234
|
console.log(" pnpm install # or npm install");
|
|
@@ -227,22 +258,41 @@ async function runTemplate(targetDir, templateId, flags) {
|
|
|
227
258
|
*/
|
|
228
259
|
async function main(argv) {
|
|
229
260
|
const { flags, positionals } = parseArgs(argv);
|
|
230
|
-
|
|
261
|
+
let command = positionals[0];
|
|
231
262
|
|
|
232
263
|
if (flags.help || command === "help" || (!command && argv.length === 0)) {
|
|
233
264
|
console.log(HELP_TEXT.trim());
|
|
234
265
|
return;
|
|
235
266
|
}
|
|
236
267
|
|
|
268
|
+
if (command === "theme" || command === "font" || command === "brand" || command === "init-db") {
|
|
269
|
+
const targetDir = process.cwd();
|
|
270
|
+
const currentConfig = getSavedConfig(targetDir);
|
|
271
|
+
|
|
272
|
+
if (command === "theme") {
|
|
273
|
+
const selectedTheme = await promptTheme(currentConfig.theme || "theme1");
|
|
274
|
+
await applyThemeAndFont(targetDir, selectedTheme, currentConfig.font || "default");
|
|
275
|
+
} else if (command === "font") {
|
|
276
|
+
const selectedFont = await promptFont(currentConfig.font || "default");
|
|
277
|
+
await applyThemeAndFont(targetDir, currentConfig.theme || "theme1", selectedFont);
|
|
278
|
+
} else if (command === "brand") {
|
|
279
|
+
await promptAndApplyBrand(targetDir);
|
|
280
|
+
} else if (command === "init-db") {
|
|
281
|
+
await promptAndSetupDb(targetDir);
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
237
286
|
const manifest = loadManifest();
|
|
238
287
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
288
|
+
let runWithConfig = flags.config;
|
|
289
|
+
if (positionals.length >= 2 && positionals[1] === "config") {
|
|
290
|
+
runWithConfig = true;
|
|
291
|
+
positionals.splice(1, 1);
|
|
242
292
|
}
|
|
243
293
|
|
|
244
294
|
const targetDir = process.cwd();
|
|
245
|
-
await runTemplate(targetDir, command, flags);
|
|
295
|
+
await runTemplate(targetDir, command, flags, runWithConfig);
|
|
246
296
|
}
|
|
247
297
|
|
|
248
298
|
main(process.argv.slice(2)).catch((error) => {
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
3
|
+
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
export const THEMES = [
|
|
8
|
+
{
|
|
9
|
+
id: "theme1",
|
|
10
|
+
name: "Theme 1 (Slate / Blue)",
|
|
11
|
+
colors: {
|
|
12
|
+
primary: "#2563EB",
|
|
13
|
+
primaryHover: "#1D4ED8",
|
|
14
|
+
accent: "#38BDF8",
|
|
15
|
+
accentDark: "#0284C7",
|
|
16
|
+
accentLight: "#EFF6FF",
|
|
17
|
+
textMain: "#0F172A",
|
|
18
|
+
textMuted: "#64748B",
|
|
19
|
+
bgMain: "#F8FAFC",
|
|
20
|
+
bgLight: "#EFF6FF",
|
|
21
|
+
bgCard: "#FFFFFF",
|
|
22
|
+
footerBg: "#E2E8F0",
|
|
23
|
+
ctaPrimary: "#2563EB",
|
|
24
|
+
ctaPrimaryHover: "#1D4ED8",
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "theme2",
|
|
29
|
+
name: "Theme 2 (Forest / Green)",
|
|
30
|
+
colors: {
|
|
31
|
+
primary: "#58812F",
|
|
32
|
+
primaryHover: "#466725",
|
|
33
|
+
accent: "#8BC34A",
|
|
34
|
+
accentDark: "#689F38",
|
|
35
|
+
accentLight: "#F1F5EA",
|
|
36
|
+
textMain: "#1D3108",
|
|
37
|
+
textMuted: "#4A5441",
|
|
38
|
+
bgMain: "#F9FAF7",
|
|
39
|
+
bgLight: "#F1F5EA",
|
|
40
|
+
bgCard: "#FFFFFF",
|
|
41
|
+
footerBg: "#E6EBDC",
|
|
42
|
+
ctaPrimary: "#58812F",
|
|
43
|
+
ctaPrimaryHover: "#466725",
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "theme3",
|
|
48
|
+
name: "Theme 3 (Purple / Violet)",
|
|
49
|
+
colors: {
|
|
50
|
+
primary: "#7C3AED",
|
|
51
|
+
primaryHover: "#6D28D9",
|
|
52
|
+
accent: "#A78BFA",
|
|
53
|
+
accentDark: "#8B5CF6",
|
|
54
|
+
accentLight: "#F5F3FF",
|
|
55
|
+
textMain: "#2E1065",
|
|
56
|
+
textMuted: "#6B6382",
|
|
57
|
+
bgMain: "#FAF9FF",
|
|
58
|
+
bgLight: "#F5F3FF",
|
|
59
|
+
bgCard: "#FFFFFF",
|
|
60
|
+
footerBg: "#E9E3FF",
|
|
61
|
+
ctaPrimary: "#7C3AED",
|
|
62
|
+
ctaPrimaryHover: "#6D28D9",
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "theme4",
|
|
67
|
+
name: "Theme 4 (Red / Crimson)",
|
|
68
|
+
colors: {
|
|
69
|
+
primary: "#DC2626",
|
|
70
|
+
primaryHover: "#B91C1C",
|
|
71
|
+
accent: "#F87171",
|
|
72
|
+
accentDark: "#EF4444",
|
|
73
|
+
accentLight: "#FEF2F2",
|
|
74
|
+
textMain: "#450A0A",
|
|
75
|
+
textMuted: "#7F1D1D",
|
|
76
|
+
bgMain: "#FFFBFB",
|
|
77
|
+
bgLight: "#FEF2F2",
|
|
78
|
+
bgCard: "#FFFFFF",
|
|
79
|
+
footerBg: "#FEE2E2",
|
|
80
|
+
ctaPrimary: "#DC2626",
|
|
81
|
+
ctaPrimaryHover: "#B91C1C",
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "theme5",
|
|
86
|
+
name: "Theme 5 (Amber / Gold)",
|
|
87
|
+
colors: {
|
|
88
|
+
primary: "#D97706",
|
|
89
|
+
primaryHover: "#B45309",
|
|
90
|
+
accent: "#FBBF24",
|
|
91
|
+
accentDark: "#F59E0B",
|
|
92
|
+
accentLight: "#FFFBEB",
|
|
93
|
+
textMain: "#1C1917",
|
|
94
|
+
textMuted: "#78350F",
|
|
95
|
+
bgMain: "#FFFCF5",
|
|
96
|
+
bgLight: "#FFFBEB",
|
|
97
|
+
bgCard: "#FFFFFF",
|
|
98
|
+
footerBg: "#FEF3C7",
|
|
99
|
+
ctaPrimary: "#D97706",
|
|
100
|
+
ctaPrimaryHover: "#B45309",
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
export const FONTS = [
|
|
106
|
+
{
|
|
107
|
+
id: "default",
|
|
108
|
+
name: "Playfair Display (Serif) + Outfit (Sans-serif) [Default]",
|
|
109
|
+
serif: "'Playfair Display', Georgia, serif",
|
|
110
|
+
sans: "'Outfit', system-ui, sans-serif",
|
|
111
|
+
importUrl: "https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "inter",
|
|
115
|
+
name: "Inter (Sans-serif) + Inter (Sans-serif)",
|
|
116
|
+
serif: "'Inter', system-ui, sans-serif",
|
|
117
|
+
sans: "'Inter', system-ui, sans-serif",
|
|
118
|
+
importUrl: "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "lora-montserrat",
|
|
122
|
+
name: "Lora (Serif) + Montserrat (Sans-serif)",
|
|
123
|
+
serif: "'Lora', Georgia, serif",
|
|
124
|
+
sans: "'Montserrat', system-ui, sans-serif",
|
|
125
|
+
importUrl: "https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,700;1,400&family=Montserrat:wght@300;400;500;600;700&display=swap"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: "merriweather-open-sans",
|
|
129
|
+
name: "Merriweather (Serif) + Open Sans (Sans-serif)",
|
|
130
|
+
serif: "'Merriweather', Georgia, serif",
|
|
131
|
+
sans: "'Open Sans', system-ui, sans-serif",
|
|
132
|
+
importUrl: "https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@0,300;0,400;0,700;1,300&family=Open+Sans:wght@300;400;500;600;700&display=swap"
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: "cinzel-montserrat",
|
|
136
|
+
name: "Cinzel (Serif) + Montserrat (Sans-serif)",
|
|
137
|
+
serif: "'Cinzel', serif",
|
|
138
|
+
sans: "'Montserrat', system-ui, sans-serif",
|
|
139
|
+
importUrl: "https://fonts.googleapis.com/css2?family=Cinzel:wght@400;700&family=Montserrat:wght@300;400;500;600;700&display=swap"
|
|
140
|
+
}
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
export function getSavedConfig(targetDir) {
|
|
144
|
+
const configPath = join(targetDir, ".tempjsrc");
|
|
145
|
+
if (existsSync(configPath)) {
|
|
146
|
+
try {
|
|
147
|
+
return JSON.parse(readFileSync(configPath, "utf8"));
|
|
148
|
+
} catch {
|
|
149
|
+
return {};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return {};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function promptSelection(options, promptText, defaultValue) {
|
|
156
|
+
const rl = createInterface({ input, output });
|
|
157
|
+
try {
|
|
158
|
+
console.log(`\n${promptText}`);
|
|
159
|
+
for (let i = 0; i < options.length; i++) {
|
|
160
|
+
const isDefault = options[i].id === defaultValue ? " (current default)" : "";
|
|
161
|
+
console.log(` [${i + 1}] ${options[i].name}${isDefault}`);
|
|
162
|
+
}
|
|
163
|
+
const defaultIndex = options.findIndex(o => o.id === defaultValue) + 1;
|
|
164
|
+
const placeholder = defaultIndex > 0 ? defaultIndex : 1;
|
|
165
|
+
const actualDefault = defaultIndex > 0 ? defaultValue : options[0].id;
|
|
166
|
+
|
|
167
|
+
while (true) {
|
|
168
|
+
const answer = await rl.question(`Choose an option (1-${options.length}) [${placeholder}]: `);
|
|
169
|
+
const trimmed = answer.trim();
|
|
170
|
+
if (trimmed === "") return actualDefault;
|
|
171
|
+
const num = parseInt(trimmed, 10);
|
|
172
|
+
if (num >= 1 && num <= options.length) {
|
|
173
|
+
return options[num - 1].id;
|
|
174
|
+
}
|
|
175
|
+
console.log("Invalid option. Please try again.");
|
|
176
|
+
}
|
|
177
|
+
} finally {
|
|
178
|
+
rl.close();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function promptTheme(currentThemeId = "theme1") {
|
|
183
|
+
return promptSelection(THEMES, "Available Themes:", currentThemeId);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function promptFont(currentFontId = "default") {
|
|
187
|
+
return promptSelection(FONTS, "Available Font combinations:", currentFontId);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function findGlobalsCss(dir) {
|
|
191
|
+
const commonPaths = [
|
|
192
|
+
join(dir, "app/globals.css"),
|
|
193
|
+
join(dir, "src/app/globals.css"),
|
|
194
|
+
join(dir, "src/globals.css"),
|
|
195
|
+
join(dir, "globals.css"),
|
|
196
|
+
];
|
|
197
|
+
for (const p of commonPaths) {
|
|
198
|
+
if (existsSync(p)) return p;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function search(currentDir) {
|
|
202
|
+
let entries;
|
|
203
|
+
try {
|
|
204
|
+
entries = await readdir(currentDir, { withFileTypes: true });
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
if (entry.isDirectory()) {
|
|
210
|
+
if (
|
|
211
|
+
entry.name === "node_modules" ||
|
|
212
|
+
entry.name === ".next" ||
|
|
213
|
+
entry.name === ".git" ||
|
|
214
|
+
entry.name === "dist" ||
|
|
215
|
+
entry.name === "build"
|
|
216
|
+
) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const found = await search(join(currentDir, entry.name));
|
|
220
|
+
if (found) return found;
|
|
221
|
+
} else if (entry.name === "globals.css") {
|
|
222
|
+
return join(currentDir, entry.name);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
return search(dir);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function findSiteTs(dir) {
|
|
231
|
+
const commonPaths = [
|
|
232
|
+
join(dir, "constants/site.ts"),
|
|
233
|
+
join(dir, "src/constants/site.ts"),
|
|
234
|
+
join(dir, "constants/site.js"),
|
|
235
|
+
join(dir, "src/constants/site.js"),
|
|
236
|
+
];
|
|
237
|
+
for (const p of commonPaths) {
|
|
238
|
+
if (existsSync(p)) return p;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function search(currentDir) {
|
|
242
|
+
let entries;
|
|
243
|
+
try {
|
|
244
|
+
entries = await readdir(currentDir, { withFileTypes: true });
|
|
245
|
+
} catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
for (const entry of entries) {
|
|
249
|
+
if (entry.isDirectory()) {
|
|
250
|
+
if (
|
|
251
|
+
entry.name === "node_modules" ||
|
|
252
|
+
entry.name === ".next" ||
|
|
253
|
+
entry.name === ".git" ||
|
|
254
|
+
entry.name === "dist" ||
|
|
255
|
+
entry.name === "build"
|
|
256
|
+
) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const found = await search(join(currentDir, entry.name));
|
|
260
|
+
if (found) return found;
|
|
261
|
+
} else if (entry.name === "site.ts" || entry.name === "site.js") {
|
|
262
|
+
return join(currentDir, entry.name);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
return search(dir);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function applyThemeAndFont(targetDir, themeId, fontId) {
|
|
271
|
+
const theme = THEMES.find(t => t.id === themeId) || THEMES[0];
|
|
272
|
+
const font = FONTS.find(f => f.id === fontId) || FONTS[0];
|
|
273
|
+
|
|
274
|
+
// 1. Write metadata config file .tempjsrc
|
|
275
|
+
const configPath = join(targetDir, ".tempjsrc");
|
|
276
|
+
const config = {
|
|
277
|
+
theme: theme.id,
|
|
278
|
+
font: font.id,
|
|
279
|
+
updatedAt: new Date().toISOString()
|
|
280
|
+
};
|
|
281
|
+
await writeFile(configPath, JSON.stringify(config, null, 2), "utf8");
|
|
282
|
+
|
|
283
|
+
// 2. Find and update globals.css
|
|
284
|
+
const globalsPath = await findGlobalsCss(targetDir);
|
|
285
|
+
if (globalsPath) {
|
|
286
|
+
const cssDir = join(globalsPath, "..");
|
|
287
|
+
const themeCssPath = join(cssDir, "tempjs-theme.css");
|
|
288
|
+
|
|
289
|
+
// Construct the theme css content
|
|
290
|
+
const cssContent = `/* tempjs generated theme settings */
|
|
291
|
+
@import url('${font.importUrl}');
|
|
292
|
+
|
|
293
|
+
:root {
|
|
294
|
+
--primary: ${theme.colors.primary} !important;
|
|
295
|
+
--primary-hover: ${theme.colors.primaryHover} !important;
|
|
296
|
+
--accent-gold: ${theme.colors.accent} !important;
|
|
297
|
+
--accent-gold-dark: ${theme.colors.accentDark} !important;
|
|
298
|
+
--accent-gold-light: ${theme.colors.accentLight} !important;
|
|
299
|
+
--text-main: ${theme.colors.textMain} !important;
|
|
300
|
+
--text-muted: ${theme.colors.textMuted} !important;
|
|
301
|
+
--bg-tan: ${theme.colors.bgMain} !important;
|
|
302
|
+
--bg-light: ${theme.colors.bgLight} !important;
|
|
303
|
+
--bg-card: ${theme.colors.bgCard} !important;
|
|
304
|
+
--footer-bg: ${theme.colors.footerBg} !important;
|
|
305
|
+
--cta-primary: ${theme.colors.ctaPrimary} !important;
|
|
306
|
+
--cta-primary-hover: ${theme.colors.ctaPrimaryHover} !important;
|
|
307
|
+
|
|
308
|
+
--font-serif: ${font.serif} !important;
|
|
309
|
+
--font-sans: ${font.sans} !important;
|
|
310
|
+
}
|
|
311
|
+
`;
|
|
312
|
+
await writeFile(themeCssPath, cssContent, "utf8");
|
|
313
|
+
|
|
314
|
+
// Ensure it's imported in globals.css
|
|
315
|
+
let globalsContent = await readFile(globalsPath, "utf8");
|
|
316
|
+
if (!globalsContent.includes("tempjs-theme.css")) {
|
|
317
|
+
// Prepend import at the top of the file
|
|
318
|
+
globalsContent = `@import "./tempjs-theme.css";\n` + globalsContent;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Always append/update a timestamp touch comment to force bundler/Tailwind CSS recompilation
|
|
322
|
+
globalsContent = globalsContent.replace(/\/\* tempjs-touch: \d+ \*\/\s*$/, "");
|
|
323
|
+
globalsContent = globalsContent.trim() + `\n/* tempjs-touch: ${Date.now()} */\n`;
|
|
324
|
+
await writeFile(globalsPath, globalsContent, "utf8");
|
|
325
|
+
} else {
|
|
326
|
+
console.warn("Could not locate globals.css file in the project. CSS variables and font imports were not applied.");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 3. Find and update site.ts
|
|
330
|
+
const siteTsPath = await findSiteTs(targetDir);
|
|
331
|
+
if (siteTsPath) {
|
|
332
|
+
let siteContent = await readFile(siteTsPath, "utf8");
|
|
333
|
+
const colorsRegex = /colors:\s*\{[\s\S]*?\}/;
|
|
334
|
+
const replacement = `colors: {
|
|
335
|
+
primary: "${theme.colors.primary}",
|
|
336
|
+
primaryHover: "${theme.colors.primaryHover}",
|
|
337
|
+
accent: "${theme.colors.accent}",
|
|
338
|
+
accentDark: "${theme.colors.accentDark}",
|
|
339
|
+
accentLight: "${theme.colors.accentLight}",
|
|
340
|
+
textMain: "${theme.colors.textMain}",
|
|
341
|
+
textMuted: "${theme.colors.textMuted}",
|
|
342
|
+
bgMain: "${theme.colors.bgMain}",
|
|
343
|
+
bgLight: "${theme.colors.bgLight}",
|
|
344
|
+
bgCard: "${theme.colors.bgCard}",
|
|
345
|
+
footerBg: "${theme.colors.footerBg}",
|
|
346
|
+
ctaPrimary: "${theme.colors.ctaPrimary}",
|
|
347
|
+
ctaPrimaryHover: "${theme.colors.ctaPrimaryHover}",
|
|
348
|
+
}`;
|
|
349
|
+
if (colorsRegex.test(siteContent)) {
|
|
350
|
+
siteContent = siteContent.replace(colorsRegex, replacement);
|
|
351
|
+
await writeFile(siteTsPath, siteContent, "utf8");
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
console.log(`\nSuccessfully applied theme "${theme.name}" and font pairing "${font.name}".`);
|
|
356
|
+
}
|