@navneet_25/tempjs 1.1.0 → 3.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.
Files changed (100) hide show
  1. package/README.md +800 -35
  2. package/cli/brand-manager.js +277 -78
  3. package/cli/config.js +22 -1
  4. package/cli/copy.js +4 -18
  5. package/cli/db-setup.js +170 -49
  6. package/cli/doctor.js +393 -0
  7. package/cli/fetch-core-modules.js +130 -0
  8. package/cli/fetch.js +47 -29
  9. package/cli/file-tree.js +113 -0
  10. package/cli/fs-ignore.js +60 -0
  11. package/cli/index.js +274 -112
  12. package/cli/info.js +115 -0
  13. package/cli/init-options.js +209 -0
  14. package/cli/module-manager.js +168 -0
  15. package/cli/parse-args.js +177 -0
  16. package/cli/progress.js +33 -0
  17. package/cli/project-stamp.js +69 -0
  18. package/cli/prompt.js +19 -0
  19. package/cli/template-resolver.js +48 -0
  20. package/cli/theme-manager.js +28 -2
  21. package/cli/update.js +227 -0
  22. package/cli/version-manager.js +518 -0
  23. package/package.json +31 -4
  24. package/packages/core/modules/README.md +72 -0
  25. package/packages/core/modules/blog-compose/app/admin/components/blog-compose-form.ts +44 -0
  26. package/packages/core/modules/blog-compose/app/admin/hooks/useBlogPosts.ts +103 -0
  27. package/packages/core/modules/blog-compose/app/admin/panels/BlogComposePanel.tsx +388 -0
  28. package/packages/core/modules/blog-compose/app/api/blog-posts/[id]/route.ts +29 -0
  29. package/packages/core/modules/blog-compose/app/api/blog-posts/route.ts +21 -0
  30. package/packages/core/modules/blog-compose/app/blog/[slug]/page.tsx +79 -0
  31. package/packages/core/modules/blog-compose/app/blog/layout.tsx +22 -0
  32. package/packages/core/modules/blog-compose/app/blog/page.tsx +102 -0
  33. package/packages/core/modules/blog-compose/app/components/BlogSection.tsx +74 -0
  34. package/packages/core/modules/blog-compose/app/components/BlogShell.tsx +36 -0
  35. package/packages/core/modules/blog-compose/app/components/BlogSidebar.tsx +112 -0
  36. package/packages/core/modules/blog-compose/lib/blog/compose/annotate-text.tsx +103 -0
  37. package/packages/core/modules/blog-compose/lib/blog/compose/blocks/media-blocks.tsx +97 -0
  38. package/packages/core/modules/blog-compose/lib/blog/compose/blocks/text-blocks.tsx +115 -0
  39. package/packages/core/modules/blog-compose/lib/blog/compose/index.ts +21 -0
  40. package/packages/core/modules/blog-compose/lib/blog/compose/interpreter.tsx +55 -0
  41. package/packages/core/modules/blog-compose/lib/blog/compose/registry.ts +276 -0
  42. package/packages/core/modules/blog-compose/lib/blog/compose/types.ts +128 -0
  43. package/packages/core/modules/blog-compose/lib/blog/register-sitemap.ts +18 -0
  44. package/packages/core/modules/blog-compose/lib/controllers/BlogComposeController.ts +2 -0
  45. package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.controller.ts +78 -0
  46. package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.repository.ts +78 -0
  47. package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.service.ts +72 -0
  48. package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.types.ts +20 -0
  49. package/packages/core/modules/blog-compose/lib/features/blog-compose/index.ts +7 -0
  50. package/packages/core/modules/blog-compose/prisma/blog-compose.prisma +13 -0
  51. package/packages/core/modules/enquiry-modal/app/components/EnquiryModal.tsx +308 -0
  52. package/packages/core/modules/footer/app/components/Footer.tsx +127 -0
  53. package/packages/core/modules/gallery/app/admin/components/GalleryFormModal.tsx +132 -0
  54. package/packages/core/modules/gallery/app/admin/components/GalleryList.tsx +142 -0
  55. package/packages/core/modules/gallery/app/admin/hooks/useGallery.ts +72 -0
  56. package/packages/core/modules/gallery/app/api/gallery/[id]/route.ts +29 -0
  57. package/packages/core/modules/gallery/app/api/gallery/route.ts +22 -0
  58. package/packages/core/modules/gallery/app/components/GallerySection.tsx +92 -0
  59. package/packages/core/modules/gallery/app/gallery/layout.tsx +12 -0
  60. package/packages/core/modules/gallery/app/gallery/page.tsx +179 -0
  61. package/packages/core/modules/gallery/lib/controllers/GalleryController.ts +2 -0
  62. package/packages/core/modules/gallery/lib/features/gallery/gallery.controller.ts +78 -0
  63. package/packages/core/modules/gallery/lib/features/gallery/gallery.repository.ts +55 -0
  64. package/packages/core/modules/gallery/lib/features/gallery/gallery.service.ts +43 -0
  65. package/packages/core/modules/gallery/lib/features/gallery/gallery.types.ts +17 -0
  66. package/packages/core/modules/gallery/lib/features/gallery/index.ts +4 -0
  67. package/packages/core/modules/gallery/prisma/gallery.prisma +11 -0
  68. package/packages/core/modules/hero-simple/app/components/Hero.tsx +160 -0
  69. package/packages/core/modules/legal-pages/app/components/LegalDocumentPage.tsx +61 -0
  70. package/packages/core/modules/legal-pages/app/privacy-policy/page.tsx +19 -0
  71. package/packages/core/modules/legal-pages/app/terms-and-conditions/page.tsx +19 -0
  72. package/packages/core/modules/reviews/app/admin/components/ReviewFormModal.tsx +110 -0
  73. package/packages/core/modules/reviews/app/admin/components/ReviewsList.tsx +116 -0
  74. package/packages/core/modules/reviews/app/admin/hooks/useReviews.ts +70 -0
  75. package/packages/core/modules/reviews/app/api/reviews/[id]/route.ts +29 -0
  76. package/packages/core/modules/reviews/app/api/reviews/route.ts +17 -0
  77. package/packages/core/modules/reviews/app/components/ReviewsSection.tsx +100 -0
  78. package/packages/core/modules/reviews/lib/controllers/ReviewController.ts +2 -0
  79. package/packages/core/modules/reviews/lib/features/reviews/index.ts +3 -0
  80. package/packages/core/modules/reviews/lib/features/reviews/review.controller.ts +64 -0
  81. package/packages/core/modules/reviews/lib/features/reviews/review.repository.ts +50 -0
  82. package/packages/core/modules/reviews/lib/features/reviews/review.service.ts +38 -0
  83. package/packages/core/modules/reviews/prisma/reviews.prisma +9 -0
  84. package/packages/core/modules/seo/app/components/SiteJsonLd.tsx +16 -0
  85. package/packages/core/modules/seo/app/robots.ts +30 -0
  86. package/packages/core/modules/seo/app/sitemap.ts +9 -0
  87. package/packages/core/modules/seo/lib/seo/index.ts +5 -0
  88. package/packages/core/modules/seo/lib/seo/json-ld.ts +128 -0
  89. package/packages/core/modules/seo/lib/seo/metadata.ts +65 -0
  90. package/packages/core/modules/seo/lib/seo/sitemap.ts +82 -0
  91. package/packages/core/modules/seo/lib/seo/types.ts +30 -0
  92. package/packages/core/modules/seo/lib/seo/urls.ts +27 -0
  93. package/packages/core/modules/theme-modes/app/components/ThemeModeInit.tsx +24 -0
  94. package/packages/core/modules/theme-modes/app/components/ThemeModeToggle.tsx +58 -0
  95. package/packages/core/modules.json +140 -0
  96. package/packages/core/prisma/schema.prisma +33 -0
  97. package/scripts/module-installer-core.mjs +657 -0
  98. package/scripts/template-modules-admin.mjs +366 -0
  99. package/scripts/template-modules-home.mjs +120 -0
  100. package/templates.json +188 -3
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
- export async function promptAndSetupDb(targetDir) {
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 = basename(resolve(targetDir)).toLowerCase().replace(/[^a-z0-9_]/g, "_") + "_db";
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(/mysql:\/\/([^:@]+)(?::([^@]+))?@([^:/]+)(?::(\d+))?\/([^?]+)/);
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 rl = createInterface({ input, output });
52
- try {
53
- console.log("\n--- Configure Database & Environment Variables ---");
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
- 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;
78
+ const dbUrl = `mysql://${user}${pass ? `:${encodeURIComponent(pass)}` : ""}@${host}:${port}/${dbName}`;
60
79
 
61
- const adminUser = (await rl.question(`Admin Portal Username [${defaultAdminUser}]: `)).trim() || defaultAdminUser;
62
- const adminPass = (await rl.question(`Admin Portal Password [${defaultAdminPass}]: `)).trim() || defaultAdminPass;
80
+ let envContent = "";
81
+ if (existsSync(envExamplePath)) {
82
+ envContent = await readFile(envExamplePath, "utf8");
83
+ }
63
84
 
64
- // Construct MySQL Connection URL
65
- const dbUrl = `mysql://${user}${pass ? `:${encodeURIComponent(pass)}` : ""}@${host}:${port}/${dbName}`;
85
+ const randomSecret = crypto.randomBytes(32).toString("base64");
66
86
 
67
- // Read template or write fresh environment file
68
- let envContent = "";
69
- if (existsSync(envExamplePath)) {
70
- envContent = await readFile(envExamplePath, "utf8");
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
- // 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}"
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
- await writeFile(envPath, envContent, "utf8");
100
- console.log("Successfully created/updated .env configuration file.");
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
- // 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]: ");
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 (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");
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("\nSkipped database table synchronization. Remember to run 'npx prisma db push' before running the app.");
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();
package/cli/doctor.js ADDED
@@ -0,0 +1,393 @@
1
+ import { pathToFileURL } from "node:url";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { createConnection } from "node:net";
5
+ import { loadManifest } from "./config.js";
6
+ import { readProjectStamp } from "./project-stamp.js";
7
+
8
+ const REQUIRED_ENV_KEYS = [
9
+ "DATABASE_URL",
10
+ "AUTH_SECRET",
11
+ "ADMIN_USER",
12
+ "ADMIN_PASSWORD",
13
+ ];
14
+
15
+ /**
16
+ * @typedef {{
17
+ * name: string,
18
+ * ok: boolean,
19
+ * detail: string,
20
+ * hint?: string,
21
+ * optional?: boolean
22
+ * }} DoctorCheck
23
+ */
24
+
25
+ /**
26
+ * @param {string} requirement e.g. ">=20"
27
+ * @param {number} version
28
+ */
29
+ function satisfiesNodeRequirement(requirement, version) {
30
+ const match = requirement.match(/^>=\s*(\d+)/);
31
+ if (!match) return true;
32
+ return version >= Number(match[1]);
33
+ }
34
+
35
+ /**
36
+ * @param {string} a
37
+ * @param {string} b
38
+ */
39
+ function compareVersions(a, b) {
40
+ const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
41
+ const pb = b.split(".").map((n) => Number.parseInt(n, 10) || 0);
42
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
43
+ const da = pa[i] ?? 0;
44
+ const db = pb[i] ?? 0;
45
+ if (da !== db) return da < db ? -1 : 1;
46
+ }
47
+ return 0;
48
+ }
49
+
50
+ /**
51
+ * @param {string} filePath
52
+ */
53
+ function loadEnvFile(filePath) {
54
+ /** @type {Record<string, string>} */
55
+ const env = {};
56
+ const content = readFileSync(filePath, "utf8");
57
+
58
+ for (const line of content.split("\n")) {
59
+ const trimmed = line.trim();
60
+ if (!trimmed || trimmed.startsWith("#")) continue;
61
+
62
+ const eq = trimmed.indexOf("=");
63
+ if (eq === -1) continue;
64
+
65
+ const key = trimmed.slice(0, eq).trim();
66
+ let value = trimmed.slice(eq + 1).trim();
67
+
68
+ if (
69
+ (value.startsWith('"') && value.endsWith('"')) ||
70
+ (value.startsWith("'") && value.endsWith("'"))
71
+ ) {
72
+ value = value.slice(1, -1);
73
+ }
74
+
75
+ env[key] = value;
76
+ }
77
+
78
+ return env;
79
+ }
80
+
81
+ /**
82
+ * @param {string} databaseUrl
83
+ */
84
+ function parseDatabaseUrl(databaseUrl) {
85
+ const match = databaseUrl.match(
86
+ /^(?:mysql|mariadb):\/\/([^:@]+)(?::([^@]*))?@([^:/]+)(?::(\d+))?\/([^?]+)/
87
+ );
88
+
89
+ if (!match) return null;
90
+
91
+ return {
92
+ user: decodeURIComponent(match[1]),
93
+ password: decodeURIComponent(match[2] ?? ""),
94
+ host: match[3],
95
+ port: Number(match[4] || 3306),
96
+ database: match[5],
97
+ };
98
+ }
99
+
100
+ /**
101
+ * @param {string} host
102
+ * @param {number} port
103
+ * @param {number} timeoutMs
104
+ */
105
+ function probeTcp(host, port, timeoutMs = 4000) {
106
+ return new Promise((resolve) => {
107
+ const socket = createConnection({ host, port });
108
+ const timer = setTimeout(() => {
109
+ socket.destroy();
110
+ resolve(false);
111
+ }, timeoutMs);
112
+
113
+ socket.on("connect", () => {
114
+ clearTimeout(timer);
115
+ socket.end();
116
+ resolve(true);
117
+ });
118
+
119
+ socket.on("error", () => {
120
+ clearTimeout(timer);
121
+ resolve(false);
122
+ });
123
+ });
124
+ }
125
+
126
+ /**
127
+ * @param {string} projectDir
128
+ * @param {Record<string, string>} env
129
+ */
130
+ async function checkDatabase(projectDir, env) {
131
+ const databaseUrl = env.DATABASE_URL?.trim();
132
+ if (!databaseUrl) {
133
+ return {
134
+ ok: false,
135
+ detail: "DATABASE_URL is empty",
136
+ hint: "Set DATABASE_URL in .env (see .env.example)",
137
+ };
138
+ }
139
+
140
+ const parsed = parseDatabaseUrl(databaseUrl);
141
+ if (!parsed) {
142
+ return {
143
+ ok: false,
144
+ detail: "DATABASE_URL format is invalid",
145
+ hint: "Expected mysql://user:pass@host:3306/dbname",
146
+ };
147
+ }
148
+
149
+ const reachable = await probeTcp(parsed.host, parsed.port);
150
+ if (!reachable) {
151
+ return {
152
+ ok: false,
153
+ detail: `Cannot reach ${parsed.host}:${parsed.port}`,
154
+ hint: "Run docker compose up -d or check your database host",
155
+ };
156
+ }
157
+
158
+ const mariadbEntry = join(projectDir, "node_modules", "mariadb", "package.json");
159
+ if (!existsSync(mariadbEntry)) {
160
+ return {
161
+ ok: false,
162
+ detail: "TCP reachable but mariadb driver not installed",
163
+ hint: "Run pnpm install, then tempjs doctor again",
164
+ };
165
+ }
166
+
167
+ try {
168
+ const mariadb = await import(
169
+ pathToFileURL(join(projectDir, "node_modules/mariadb/index.js")).href
170
+ );
171
+ const conn = await mariadb.createConnection({
172
+ host: parsed.host,
173
+ port: parsed.port,
174
+ user: parsed.user,
175
+ password: parsed.password,
176
+ database: parsed.database,
177
+ connectTimeout: 5000,
178
+ });
179
+ await conn.query("SELECT 1");
180
+ await conn.end();
181
+ return { ok: true, detail: `Connected to ${parsed.database} on ${parsed.host}:${parsed.port}` };
182
+ } catch (error) {
183
+ const message = error instanceof Error ? error.message : String(error);
184
+ return {
185
+ ok: false,
186
+ detail: `Auth or query failed: ${message}`,
187
+ hint: "Verify credentials in DATABASE_URL and that the database exists",
188
+ };
189
+ }
190
+ }
191
+
192
+ /**
193
+ * @param {string} projectDir
194
+ */
195
+ async function checkHealthEndpoint(projectDir) {
196
+ const urls = ["http://localhost:3000/api/health", "http://127.0.0.1:3000/api/health"];
197
+
198
+ for (const url of urls) {
199
+ try {
200
+ const response = await fetch(url, { signal: AbortSignal.timeout(2500) });
201
+ const body = await response.json();
202
+
203
+ if (response.ok && body?.status === "ok") {
204
+ return { ok: true, detail: `${url} → status ok` };
205
+ }
206
+
207
+ return {
208
+ ok: false,
209
+ detail: `${url} → ${body?.status ?? response.status}`,
210
+ hint: "Fix env or database issues reported by /api/health",
211
+ optional: true,
212
+ };
213
+ } catch {
214
+ // try next url
215
+ }
216
+ }
217
+
218
+ return {
219
+ ok: true,
220
+ detail: "Dev server not running (optional)",
221
+ optional: true,
222
+ };
223
+ }
224
+
225
+ /**
226
+ * @param {DoctorCheck[]} checks
227
+ */
228
+ function printChecks(checks) {
229
+ for (const check of checks) {
230
+ const icon = check.ok ? "✓" : "✗";
231
+ const label = check.name.padEnd(16);
232
+ console.log(`${icon} ${label}${check.detail}`);
233
+ if (!check.ok && check.hint) {
234
+ console.log(` → ${check.hint}`);
235
+ }
236
+ }
237
+ }
238
+
239
+ /**
240
+ * @param {string} projectDir
241
+ */
242
+ export async function runDoctor(projectDir) {
243
+ /** @type {DoctorCheck[]} */
244
+ const checks = [];
245
+ const manifest = loadManifest();
246
+
247
+ const nodeVersion = process.versions.node;
248
+ const nodeMajor = Number.parseInt(nodeVersion.split(".")[0], 10);
249
+ const stamp = readProjectStamp(projectDir);
250
+ const templateEntry = stamp ? manifest.templates[stamp.template] : null;
251
+ const nodeRequirement = templateEntry?.node ?? ">=20";
252
+
253
+ const nodeOk = satisfiesNodeRequirement(nodeRequirement, nodeMajor);
254
+ checks.push({
255
+ name: "Node.js",
256
+ ok: nodeOk,
257
+ detail: `v${nodeVersion} (template requires ${nodeRequirement})`,
258
+ hint: nodeOk ? undefined : `Upgrade Node.js to ${nodeRequirement}`,
259
+ });
260
+
261
+ const packageJsonPath = join(projectDir, "package.json");
262
+ if (!existsSync(packageJsonPath)) {
263
+ checks.push({
264
+ name: "package.json",
265
+ ok: false,
266
+ detail: "Not found in this directory",
267
+ hint: "Run tempjs from your generated project root",
268
+ });
269
+ } else {
270
+ checks.push({ name: "package.json", ok: true, detail: "found" });
271
+ }
272
+
273
+ const nodeModulesPath = join(projectDir, "node_modules");
274
+ checks.push({
275
+ name: "node_modules",
276
+ ok: existsSync(nodeModulesPath),
277
+ detail: existsSync(nodeModulesPath) ? "installed" : "missing",
278
+ hint: existsSync(nodeModulesPath) ? undefined : "Run pnpm install",
279
+ });
280
+
281
+ const envPath = join(projectDir, ".env");
282
+ const envExamplePath = join(projectDir, ".env.example");
283
+
284
+ if (!existsSync(envPath)) {
285
+ checks.push({
286
+ name: ".env",
287
+ ok: false,
288
+ detail: "missing",
289
+ hint: existsSync(envExamplePath) ? "Run cp .env.example .env" : "Create .env with required variables",
290
+ });
291
+ } else {
292
+ const env = loadEnvFile(envPath);
293
+ const missing = REQUIRED_ENV_KEYS.filter((key) => !env[key]?.trim());
294
+ checks.push({
295
+ name: ".env",
296
+ ok: missing.length === 0,
297
+ detail:
298
+ missing.length === 0
299
+ ? `all required keys set (${REQUIRED_ENV_KEYS.length})`
300
+ : `missing: ${missing.join(", ")}`,
301
+ hint: missing.length > 0 ? "Fill required values in .env" : undefined,
302
+ });
303
+
304
+ if (missing.length === 0) {
305
+ const dbCheck = await checkDatabase(projectDir, env);
306
+ checks.push({
307
+ name: "Database",
308
+ ok: dbCheck.ok,
309
+ detail: dbCheck.detail,
310
+ hint: dbCheck.hint,
311
+ });
312
+ } else {
313
+ checks.push({
314
+ name: "Database",
315
+ ok: false,
316
+ detail: "skipped — fix .env first",
317
+ });
318
+ }
319
+ }
320
+
321
+ if (stamp) {
322
+ const latestVersion = templateEntry?.version ?? "0.0.0";
323
+ const behind = compareVersions(stamp.templateVersion, latestVersion) < 0;
324
+ const changelogPath = join(projectDir, "CHANGELOG.md");
325
+
326
+ let versionDetail = `${stamp.template} v${stamp.templateVersion}`;
327
+ if (templateEntry) {
328
+ versionDetail += behind
329
+ ? ` (latest manifest: v${latestVersion})`
330
+ : ` (matches manifest v${latestVersion})`;
331
+ }
332
+
333
+ checks.push({
334
+ name: ".tempjs.json",
335
+ ok: !behind,
336
+ detail: versionDetail,
337
+ hint: behind
338
+ ? `Run tempjs update --check and read CHANGELOG.md${existsSync(changelogPath) ? "" : " (after update)"}`
339
+ : undefined,
340
+ });
341
+
342
+ if (behind && existsSync(changelogPath)) {
343
+ checks.push({
344
+ name: "CHANGELOG",
345
+ ok: true,
346
+ detail: "CHANGELOG.md present — review before updating",
347
+ optional: true,
348
+ });
349
+ }
350
+ } else if (existsSync(packageJsonPath)) {
351
+ checks.push({
352
+ name: ".tempjs.json",
353
+ ok: false,
354
+ detail: "not found",
355
+ hint: "Project may not have been created with tempjs",
356
+ });
357
+ }
358
+
359
+ const healthCheck = await checkHealthEndpoint(projectDir);
360
+ checks.push({
361
+ name: "API health",
362
+ ok: healthCheck.ok,
363
+ detail: healthCheck.detail,
364
+ hint: healthCheck.hint,
365
+ optional: healthCheck.optional,
366
+ });
367
+
368
+ console.log("tempjs doctor\n");
369
+
370
+ const requiredChecks = checks.filter((c) => !c.optional);
371
+ const failedRequired = requiredChecks.filter((c) => !c.ok);
372
+ const canRun = failedRequired.length === 0;
373
+
374
+ console.log(
375
+ canRun
376
+ ? "Can you run this project? YES"
377
+ : `Can you run this project? NO (${failedRequired.length} check(s) failed)`
378
+ );
379
+ console.log("");
380
+
381
+ printChecks(checks);
382
+
383
+ if (!canRun) {
384
+ console.log("\nFix the items above, then run GETTING_STARTED.md steps or:");
385
+ console.log(" pnpm install && cp .env.example .env && docker compose up -d");
386
+ console.log(" pnpm prisma db push && pnpm prisma db seed && pnpm dev");
387
+ } else {
388
+ console.log("\nYou're ready to run: pnpm dev");
389
+ console.log("Admin: http://localhost:3000/admin");
390
+ }
391
+
392
+ return canRun ? 0 : 1;
393
+ }