@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.
@@ -0,0 +1,156 @@
1
+ /**
2
+ * @typedef {ReturnType<typeof createDefaultFlags>} CliFlags
3
+ */
4
+
5
+ export function createDefaultFlags() {
6
+ return {
7
+ force: false,
8
+ remote: false,
9
+ initGit: false,
10
+ help: false,
11
+ config: false,
12
+ yes: false,
13
+ theme: undefined,
14
+ font: undefined,
15
+ name: undefined,
16
+ shortName: undefined,
17
+ baseUrl: undefined,
18
+ phone: undefined,
19
+ phoneDisplay: undefined,
20
+ countryCode: undefined,
21
+ email: undefined,
22
+ address: undefined,
23
+ dbHost: undefined,
24
+ dbPort: undefined,
25
+ dbUser: undefined,
26
+ dbPassword: undefined,
27
+ dbName: undefined,
28
+ adminUser: undefined,
29
+ adminPassword: undefined,
30
+ skipDbPush: false,
31
+ dbPush: false,
32
+ updateCheck: false,
33
+ updateMerge: false,
34
+ };
35
+ }
36
+
37
+ const FLAG_MAP = {
38
+ "--force": { key: "force", type: "boolean" },
39
+ "-f": { key: "force", type: "boolean" },
40
+ "--remote": { key: "remote", type: "boolean" },
41
+ "--init-git": { key: "initGit", type: "boolean" },
42
+ "--help": { key: "help", type: "boolean" },
43
+ "-h": { key: "help", type: "boolean" },
44
+ "--config": { key: "config", type: "boolean" },
45
+ "--yes": { key: "yes", type: "boolean" },
46
+ "-y": { key: "yes", type: "boolean" },
47
+ "--no-prompt": { key: "yes", type: "boolean" },
48
+ "--skip-db-push": { key: "skipDbPush", type: "boolean" },
49
+ "--db-push": { key: "dbPush", type: "boolean" },
50
+ "--check": { key: "updateCheck", type: "boolean" },
51
+ "--merge": { key: "updateMerge", type: "boolean" },
52
+ "--theme": { key: "theme", type: "string" },
53
+ "--font": { key: "font", type: "string" },
54
+ "--name": { key: "name", type: "string" },
55
+ "--short-name": { key: "shortName", type: "string" },
56
+ "--base-url": { key: "baseUrl", type: "string" },
57
+ "--phone": { key: "phone", type: "string" },
58
+ "--phone-display": { key: "phoneDisplay", type: "string" },
59
+ "--country-code": { key: "countryCode", type: "string" },
60
+ "--email": { key: "email", type: "string" },
61
+ "--address": { key: "address", type: "string" },
62
+ "--db-host": { key: "dbHost", type: "string" },
63
+ "--db-port": { key: "dbPort", type: "string" },
64
+ "--db-user": { key: "dbUser", type: "string" },
65
+ "--db-password": { key: "dbPassword", type: "string" },
66
+ "--db-name": { key: "dbName", type: "string" },
67
+ "--admin-user": { key: "adminUser", type: "string" },
68
+ "--admin-password": { key: "adminPassword", type: "string" },
69
+ };
70
+
71
+ /**
72
+ * @param {string[]} argv
73
+ */
74
+ export function parseArgs(argv) {
75
+ const flags = createDefaultFlags();
76
+ const positionals = [];
77
+
78
+ for (let i = 0; i < argv.length; i++) {
79
+ const arg = argv[i];
80
+
81
+ if (arg.includes("=") && arg.startsWith("--")) {
82
+ const eqIndex = arg.indexOf("=");
83
+ const flagName = arg.slice(0, eqIndex);
84
+ const value = arg.slice(eqIndex + 1);
85
+ applyFlag(flags, flagName, value);
86
+ continue;
87
+ }
88
+
89
+ const spec = FLAG_MAP[arg];
90
+ if (spec) {
91
+ if (spec.type === "boolean") {
92
+ flags[spec.key] = true;
93
+ } else {
94
+ const value = argv[++i];
95
+ if (!value || value.startsWith("-")) {
96
+ throw new Error(`Option ${arg} requires a value`);
97
+ }
98
+ flags[spec.key] = value;
99
+ }
100
+ continue;
101
+ }
102
+
103
+ if (arg.startsWith("-")) {
104
+ throw new Error(`Unknown option: ${arg}`);
105
+ }
106
+
107
+ positionals.push(arg);
108
+ }
109
+
110
+ return { flags, positionals };
111
+ }
112
+
113
+ /**
114
+ * @param {CliFlags} flags
115
+ * @param {string} flagName
116
+ * @param {string} value
117
+ */
118
+ function applyFlag(flags, flagName, value) {
119
+ const spec = FLAG_MAP[flagName];
120
+ if (!spec) {
121
+ throw new Error(`Unknown option: ${flagName}`);
122
+ }
123
+ if (spec.type === "boolean") {
124
+ flags[spec.key] = value === "true" || value === "1";
125
+ } else {
126
+ flags[spec.key] = value;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * @param {CliFlags} flags
132
+ */
133
+ export function toCliOptions(flags) {
134
+ return {
135
+ yes: flags.yes,
136
+ theme: flags.theme,
137
+ font: flags.font,
138
+ name: flags.name,
139
+ shortName: flags.shortName,
140
+ baseUrl: flags.baseUrl,
141
+ phone: flags.phone,
142
+ phoneDisplay: flags.phoneDisplay,
143
+ countryCode: flags.countryCode,
144
+ email: flags.email,
145
+ address: flags.address,
146
+ dbHost: flags.dbHost,
147
+ dbPort: flags.dbPort,
148
+ dbUser: flags.dbUser,
149
+ dbPassword: flags.dbPassword,
150
+ dbName: flags.dbName,
151
+ adminUser: flags.adminUser,
152
+ adminPassword: flags.adminPassword,
153
+ skipDbPush: flags.skipDbPush,
154
+ dbPush: flags.dbPush,
155
+ };
156
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @param {number} bytes
3
+ * @returns {string}
4
+ */
5
+ export function formatBytes(bytes) {
6
+ if (bytes < 1024) return `${bytes} B`;
7
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
8
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
9
+ }
10
+
11
+ /**
12
+ * @param {number} ms
13
+ * @returns {string}
14
+ */
15
+ export function formatDuration(ms) {
16
+ if (ms < 1000) return `${Math.round(ms)}ms`;
17
+ return `${(ms / 1000).toFixed(1)}s`;
18
+ }
19
+
20
+ /**
21
+ * @param {{
22
+ * templateId: string,
23
+ * stats: { bytes: number, files: number, durationMs: number, source?: string }
24
+ * }} options
25
+ */
26
+ export function printFetchComplete(options) {
27
+ const { templateId, stats } = options;
28
+ const sourceLabel = stats.source === "local" ? "local copy" : "download";
29
+ const sizePart = stats.bytes > 0 ? `${formatBytes(stats.bytes)}, ` : "";
30
+ console.log(
31
+ `Fetching template "${templateId}"... done (${sizePart}${stats.files} files, ${formatDuration(stats.durationMs)}, ${sourceLabel})`
32
+ );
33
+ }
@@ -0,0 +1,69 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { collectFileHashes } from "./file-tree.js";
5
+
6
+ export const STAMP_FILENAME = ".tempjs.json";
7
+
8
+ /**
9
+ * @typedef {{
10
+ * template: string,
11
+ * templateVersion: string,
12
+ * templateDirectory: string,
13
+ * generatedAt: string,
14
+ * updatedAt?: string,
15
+ * repository?: string,
16
+ * branch?: string,
17
+ * fileHashes: Record<string, string>
18
+ * }} ProjectStamp
19
+ */
20
+
21
+ /**
22
+ * @param {string} projectDir
23
+ * @returns {ProjectStamp | null}
24
+ */
25
+ export function readProjectStamp(projectDir) {
26
+ const stampPath = join(projectDir, STAMP_FILENAME);
27
+ if (!existsSync(stampPath)) return null;
28
+
29
+ try {
30
+ return JSON.parse(readFileSync(stampPath, "utf8"));
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * @param {string} projectDir
38
+ * @param {{
39
+ * templateId: string,
40
+ * templateVersion: string,
41
+ * templateDirectory: string,
42
+ * repository?: string,
43
+ * branch?: string,
44
+ * sourceDir: string,
45
+ * isUpdate?: boolean
46
+ * }} options
47
+ */
48
+ export async function writeProjectStamp(projectDir, options) {
49
+ const existing = readProjectStamp(projectDir);
50
+ const fileHashes = collectFileHashes(options.sourceDir);
51
+
52
+ /** @type {ProjectStamp} */
53
+ const stamp = {
54
+ template: options.templateId,
55
+ templateVersion: options.templateVersion,
56
+ templateDirectory: options.templateDirectory,
57
+ generatedAt: existing?.generatedAt ?? new Date().toISOString(),
58
+ updatedAt: options.isUpdate ? new Date().toISOString() : existing?.updatedAt,
59
+ repository: options.repository,
60
+ branch: options.branch,
61
+ fileHashes,
62
+ };
63
+
64
+ if (options.isUpdate) {
65
+ stamp.updatedAt = new Date().toISOString();
66
+ }
67
+
68
+ await writeFile(join(projectDir, STAMP_FILENAME), JSON.stringify(stamp, null, 2) + "\n", "utf8");
69
+ }
package/cli/prompt.js ADDED
@@ -0,0 +1,19 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+
4
+ /**
5
+ * @param {string} message
6
+ * @param {boolean} autoConfirm
7
+ */
8
+ export async function confirmYesNo(message, autoConfirm = false) {
9
+ if (autoConfirm) return true;
10
+
11
+ const rl = createInterface({ input, output });
12
+ try {
13
+ const answer = await rl.question(message);
14
+ const trimmed = answer.trim().toLowerCase();
15
+ return trimmed === "y" || trimmed === "yes";
16
+ } finally {
17
+ rl.close();
18
+ }
19
+ }
@@ -0,0 +1,48 @@
1
+ import { removeDirectory } from "./copy.js";
2
+ import { fetchTemplateFromGitHub, loadLocalTemplate } from "./fetch.js";
3
+
4
+ /**
5
+ * @typedef {{
6
+ * templateRoot: string,
7
+ * cleanupDir: string,
8
+ * stats: import('./fetch.js').FetchStats,
9
+ * release: () => void
10
+ * }} ResolvedTemplate
11
+ */
12
+
13
+ /**
14
+ * Resolve template source from local package or remote GitHub tarball.
15
+ * @param {{
16
+ * repo: import('./config.js').RepositoryConfig,
17
+ * packageRoot: string,
18
+ * templateDirectory: string,
19
+ * useRemote: boolean
20
+ * }} options
21
+ * @returns {Promise<ResolvedTemplate>}
22
+ */
23
+ export async function resolveTemplateSource(options) {
24
+ const { repo, packageRoot, templateDirectory, useRemote } = options;
25
+
26
+ let result = null;
27
+
28
+ if (!useRemote) {
29
+ result = await loadLocalTemplate(packageRoot, repo.templatesPath, templateDirectory);
30
+ }
31
+
32
+ if (!result) {
33
+ result = await fetchTemplateFromGitHub(repo, templateDirectory);
34
+ }
35
+
36
+ const cleanupDir = result.cleanupDir;
37
+
38
+ return {
39
+ templateRoot: result.templateRoot,
40
+ cleanupDir,
41
+ stats: result.stats,
42
+ release: () => {
43
+ if (cleanupDir) {
44
+ removeDirectory(cleanupDir);
45
+ }
46
+ },
47
+ };
48
+ }
@@ -152,6 +152,26 @@ export function getSavedConfig(targetDir) {
152
152
  return {};
153
153
  }
154
154
 
155
+ export function resolveThemeId(id) {
156
+ const theme = THEMES.find((t) => t.id === id);
157
+ if (!theme) {
158
+ throw new Error(
159
+ `Unknown theme: ${id}. Valid options: ${THEMES.map((t) => t.id).join(", ")}`
160
+ );
161
+ }
162
+ return theme.id;
163
+ }
164
+
165
+ export function resolveFontId(id) {
166
+ const font = FONTS.find((f) => f.id === id);
167
+ if (!font) {
168
+ throw new Error(
169
+ `Unknown font: ${id}. Valid options: ${FONTS.map((f) => f.id).join(", ")}`
170
+ );
171
+ }
172
+ return font.id;
173
+ }
174
+
155
175
  async function promptSelection(options, promptText, defaultValue) {
156
176
  const rl = createInterface({ input, output });
157
177
  try {
@@ -179,11 +199,17 @@ async function promptSelection(options, promptText, defaultValue) {
179
199
  }
180
200
  }
181
201
 
182
- export async function promptTheme(currentThemeId = "theme1") {
202
+ export async function promptTheme(currentThemeId = "theme1", options = {}) {
203
+ if (options.yes || options.theme) {
204
+ return resolveThemeId(options.theme || currentThemeId || "theme1");
205
+ }
183
206
  return promptSelection(THEMES, "Available Themes:", currentThemeId);
184
207
  }
185
208
 
186
- export async function promptFont(currentFontId = "default") {
209
+ export async function promptFont(currentFontId = "default", options = {}) {
210
+ if (options.yes || options.font) {
211
+ return resolveFontId(options.font || currentFontId || "default");
212
+ }
187
213
  return promptSelection(FONTS, "Available Font combinations:", currentFontId);
188
214
  }
189
215
 
@@ -316,8 +342,12 @@ export async function applyThemeAndFont(targetDir, themeId, fontId) {
316
342
  if (!globalsContent.includes("tempjs-theme.css")) {
317
343
  // Prepend import at the top of the file
318
344
  globalsContent = `@import "./tempjs-theme.css";\n` + globalsContent;
319
- await writeFile(globalsPath, globalsContent, "utf8");
320
345
  }
346
+
347
+ // Always append/update a timestamp touch comment to force bundler/Tailwind CSS recompilation
348
+ globalsContent = globalsContent.replace(/\/\* tempjs-touch: \d+ \*\/\s*$/, "");
349
+ globalsContent = globalsContent.trim() + `\n/* tempjs-touch: ${Date.now()} */\n`;
350
+ await writeFile(globalsPath, globalsContent, "utf8");
321
351
  } else {
322
352
  console.warn("Could not locate globals.css file in the project. CSS variables and font imports were not applied.");
323
353
  }
package/cli/update.js ADDED
@@ -0,0 +1,212 @@
1
+ import { cpSync, existsSync, mkdirSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { loadManifest, resolveRepositoryConfig, getPackageRoot } from "./config.js";
4
+ import { isUpdateProtected, shouldSkipFileName } from "./fs-ignore.js";
5
+ import {
6
+ collectFileHashes,
7
+ diffTemplateTrees,
8
+ hashFileContents,
9
+ } from "./file-tree.js";
10
+ import { readProjectStamp, writeProjectStamp } from "./project-stamp.js";
11
+ import { confirmYesNo } from "./prompt.js";
12
+ import { resolveTemplateSource } from "./template-resolver.js";
13
+
14
+ /**
15
+ * @param {import('./file-tree.js').UpdateDiff} diff
16
+ * @param {{
17
+ * projectVersion: string,
18
+ * latestVersion: string,
19
+ * templateName: string
20
+ * }} meta
21
+ */
22
+ export function printUpdateReport(diff, meta) {
23
+ console.log(`\nTemplate update report: ${meta.templateName}`);
24
+ console.log(` Project version: ${meta.projectVersion}`);
25
+ console.log(` Latest version: ${meta.latestVersion}`);
26
+ console.log("");
27
+
28
+ if (
29
+ diff.newFiles.length === 0 &&
30
+ diff.safeUpdates.length === 0 &&
31
+ diff.conflicts.length === 0 &&
32
+ diff.removedFromTemplate.length === 0
33
+ ) {
34
+ console.log("Your project is up to date with the latest template.");
35
+ return;
36
+ }
37
+
38
+ if (diff.newFiles.length > 0) {
39
+ console.log(`New files in template (${diff.newFiles.length}):`);
40
+ diff.newFiles.slice(0, 25).forEach((p) => console.log(` + ${p}`));
41
+ if (diff.newFiles.length > 25) {
42
+ console.log(` ... and ${diff.newFiles.length - 25} more`);
43
+ }
44
+ console.log("");
45
+ }
46
+
47
+ if (diff.safeUpdates.length > 0) {
48
+ console.log(`Safe updates (${diff.safeUpdates.length}) — template changed, you did not edit:`);
49
+ diff.safeUpdates.slice(0, 25).forEach((p) => console.log(` ~ ${p}`));
50
+ if (diff.safeUpdates.length > 25) {
51
+ console.log(` ... and ${diff.safeUpdates.length - 25} more`);
52
+ }
53
+ console.log("");
54
+ }
55
+
56
+ if (diff.conflicts.length > 0) {
57
+ console.log(`Conflicts (${diff.conflicts.length}) — you modified these files:`);
58
+ diff.conflicts.slice(0, 25).forEach((p) => console.log(` ! ${p}`));
59
+ if (diff.conflicts.length > 25) {
60
+ console.log(` ... and ${diff.conflicts.length - 25} more`);
61
+ }
62
+ console.log("");
63
+ }
64
+
65
+ if (diff.removedFromTemplate.length > 0) {
66
+ console.log(`Removed from template (${diff.removedFromTemplate.length}) — not deleted locally:`);
67
+ diff.removedFromTemplate.slice(0, 15).forEach((p) => console.log(` - ${p}`));
68
+ if (diff.removedFromTemplate.length > 15) {
69
+ console.log(` ... and ${diff.removedFromTemplate.length - 15} more`);
70
+ }
71
+ console.log("");
72
+ }
73
+
74
+ if (diff.upToDate.length > 0) {
75
+ console.log(`${diff.upToDate.length} file(s) already match the latest template.\n`);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * @param {string} sourcePath
81
+ * @param {string} targetPath
82
+ */
83
+ export function copyTemplateFile(sourcePath, targetPath) {
84
+ mkdirSync(dirname(targetPath), { recursive: true });
85
+ cpSync(sourcePath, targetPath, { force: true });
86
+ }
87
+
88
+ /**
89
+ * @param {string} projectDir
90
+ * @param {import('./parse-args.js').CliFlags} flags
91
+ * @param {{ checkOnly: boolean }} mode
92
+ */
93
+ export async function runUpdate(projectDir, flags, mode) {
94
+ const stamp = readProjectStamp(projectDir);
95
+
96
+ if (!stamp) {
97
+ console.error("No .tempjs.json found in this directory.");
98
+ console.error("Run `tempjs <template-id>` here first, or this project was not created with tempjs.");
99
+ process.exitCode = 1;
100
+ return;
101
+ }
102
+
103
+ const manifest = loadManifest();
104
+ const entry = manifest.templates[stamp.template];
105
+ if (!entry) {
106
+ console.error(`Unknown template in .tempjs.json: ${stamp.template}`);
107
+ process.exitCode = 1;
108
+ return;
109
+ }
110
+
111
+ const repo = resolveRepositoryConfig(manifest.repository);
112
+ const packageRoot = getPackageRoot();
113
+ const useRemote =
114
+ flags.remote ||
115
+ process.env.TEMPLATE_USE_REMOTE === "1" ||
116
+ process.env.TEMPLATE_USE_REMOTE === "true";
117
+
118
+ const latestVersion = entry.version ?? "0.0.0";
119
+ let resolved = null;
120
+
121
+ try {
122
+ resolved = await resolveTemplateSource({
123
+ repo,
124
+ packageRoot,
125
+ templateDirectory: entry.directory,
126
+ useRemote,
127
+ });
128
+
129
+ const latestHashes = collectFileHashes(resolved.templateRoot);
130
+ const baselineHashes = stamp.fileHashes ?? {};
131
+ const currentHashes = {};
132
+
133
+ for (const path of Object.keys(latestHashes)) {
134
+ const projectPath = join(projectDir, path);
135
+ if (existsSync(projectPath) && !shouldSkipFileName(path.split("/").pop() ?? "")) {
136
+ currentHashes[path] = hashFileContents(projectPath);
137
+ }
138
+ }
139
+
140
+ const diff = diffTemplateTrees(baselineHashes, currentHashes, latestHashes);
141
+
142
+ printUpdateReport(diff, {
143
+ projectVersion: stamp.templateVersion,
144
+ latestVersion,
145
+ templateName: entry.name,
146
+ });
147
+
148
+ if (mode.checkOnly) {
149
+ if (stamp.templateVersion !== latestVersion) {
150
+ console.log("Run `tempjs update --merge` to apply non-conflicting template updates.");
151
+ }
152
+ return;
153
+ }
154
+
155
+ const pathsToApply = [...diff.newFiles, ...diff.safeUpdates].filter(
156
+ (path) => !isUpdateProtected(path)
157
+ );
158
+
159
+ const skippedProtected = [...diff.newFiles, ...diff.safeUpdates].filter((path) =>
160
+ isUpdateProtected(path)
161
+ );
162
+ if (skippedProtected.length > 0) {
163
+ console.log(`Skipping ${skippedProtected.length} protected path(s) (brand, .env, etc.).`);
164
+ }
165
+
166
+ if (pathsToApply.length === 0) {
167
+ if (diff.conflicts.length > 0) {
168
+ console.log("\nNo safe updates to apply. Resolve conflicts manually.");
169
+ }
170
+ return;
171
+ }
172
+
173
+ const confirmed = await confirmYesNo(
174
+ `\nApply ${pathsToApply.length} non-conflicting update(s)? [y/N] `,
175
+ flags.yes
176
+ );
177
+
178
+ if (!confirmed) {
179
+ console.log("Aborted.");
180
+ return;
181
+ }
182
+
183
+ for (const relPath of pathsToApply) {
184
+ const from = join(resolved.templateRoot, relPath);
185
+ const to = join(projectDir, relPath);
186
+ copyTemplateFile(from, to);
187
+ }
188
+
189
+ await writeProjectStamp(projectDir, {
190
+ templateId: stamp.template,
191
+ templateVersion: latestVersion,
192
+ templateDirectory: entry.directory,
193
+ repository: `${repo.owner}/${repo.repo}`,
194
+ branch: repo.branch,
195
+ sourceDir: resolved.templateRoot,
196
+ isUpdate: true,
197
+ });
198
+
199
+ console.log(`\nApplied ${pathsToApply.length} update(s). Project stamped at template v${latestVersion}.`);
200
+
201
+ if (diff.conflicts.length > 0) {
202
+ console.log(`${diff.conflicts.length} conflict(s) still need manual review.`);
203
+ }
204
+ } catch (error) {
205
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
206
+ process.exitCode = 1;
207
+ } finally {
208
+ if (resolved) {
209
+ resolved.release();
210
+ }
211
+ }
212
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@navneet_25/tempjs",
3
- "version": "1.0.4",
3
+ "version": "2.0.0",
4
4
  "description": "CLI to instantiate website project templates from a single GitHub repository",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,5 +20,9 @@
20
20
  "website",
21
21
  "nextjs"
22
22
  ],
23
- "license": "MIT"
23
+ "license": "MIT",
24
+ "scripts": {
25
+ "sync-templates": "node scripts/sync-templates.mjs",
26
+ "sync-templates:check": "node scripts/sync-templates.mjs --check"
27
+ }
24
28
  }
package/templates.json CHANGED
@@ -9,12 +9,42 @@
9
9
  "hotel": {
10
10
  "directory": "hotel-website-template",
11
11
  "name": "Hotel Website",
12
- "description": "Modern hotel and resort website with admin panel, gallery, and booking features"
12
+ "description": "Modern hotel and resort website with admin panel, gallery, and booking features",
13
+ "version": "1.2.0",
14
+ "stack": ["Next.js 16", "React 19", "TypeScript", "Tailwind CSS 4", "Prisma", "MariaDB", "NextAuth"],
15
+ "packageManager": "pnpm",
16
+ "node": ">=20",
17
+ "setupTime": "~10 min",
18
+ "docker": true,
19
+ "tags": ["admin", "cms", "gallery", "booking", "leads"],
20
+ "features": [
21
+ "Admin dashboard for rooms, facilities, reviews, and leads",
22
+ "Gallery and promo banner management",
23
+ "FTP asset upload pipeline",
24
+ "LeadRat CRM integration",
25
+ "Docker Compose for local MariaDB"
26
+ ],
27
+ "docs": "DEVELOPER_GUIDE.md"
13
28
  },
14
29
  "real-estate": {
15
30
  "directory": "real-estate-website-template",
16
31
  "name": "Real Estate Website",
17
- "description": "Real estate and property listing website with admin panel and property management"
32
+ "description": "Real estate and property listing website with admin panel and property management",
33
+ "version": "1.2.0",
34
+ "stack": ["Next.js 16", "React 19", "TypeScript", "Tailwind CSS 4", "Prisma", "MariaDB", "NextAuth"],
35
+ "packageManager": "pnpm",
36
+ "node": ">=20",
37
+ "setupTime": "~10 min",
38
+ "docker": true,
39
+ "tags": ["admin", "cms", "properties", "leads"],
40
+ "features": [
41
+ "Admin dashboard for property listings and leads",
42
+ "Dynamic project pages with slug routing",
43
+ "FTP asset upload pipeline",
44
+ "LeadRat CRM integration",
45
+ "Docker Compose for local MariaDB"
46
+ ],
47
+ "docs": "DEVELOPER_GUIDE.md"
18
48
  }
19
49
  }
20
- }
50
+ }