@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.
- package/README.md +800 -35
- package/cli/brand-manager.js +277 -78
- package/cli/config.js +22 -1
- package/cli/copy.js +4 -18
- package/cli/db-setup.js +170 -49
- package/cli/doctor.js +393 -0
- package/cli/fetch-core-modules.js +130 -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 +274 -112
- package/cli/info.js +115 -0
- package/cli/init-options.js +209 -0
- package/cli/module-manager.js +168 -0
- package/cli/parse-args.js +177 -0
- package/cli/progress.js +33 -0
- package/cli/project-stamp.js +69 -0
- package/cli/prompt.js +19 -0
- package/cli/template-resolver.js +48 -0
- package/cli/theme-manager.js +28 -2
- package/cli/update.js +227 -0
- package/cli/version-manager.js +518 -0
- package/package.json +31 -4
- package/packages/core/modules/README.md +72 -0
- package/packages/core/modules/blog-compose/app/admin/components/blog-compose-form.ts +44 -0
- package/packages/core/modules/blog-compose/app/admin/hooks/useBlogPosts.ts +103 -0
- package/packages/core/modules/blog-compose/app/admin/panels/BlogComposePanel.tsx +388 -0
- package/packages/core/modules/blog-compose/app/api/blog-posts/[id]/route.ts +29 -0
- package/packages/core/modules/blog-compose/app/api/blog-posts/route.ts +21 -0
- package/packages/core/modules/blog-compose/app/blog/[slug]/page.tsx +79 -0
- package/packages/core/modules/blog-compose/app/blog/layout.tsx +22 -0
- package/packages/core/modules/blog-compose/app/blog/page.tsx +102 -0
- package/packages/core/modules/blog-compose/app/components/BlogSection.tsx +74 -0
- package/packages/core/modules/blog-compose/app/components/BlogShell.tsx +36 -0
- package/packages/core/modules/blog-compose/app/components/BlogSidebar.tsx +112 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/annotate-text.tsx +103 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/blocks/media-blocks.tsx +97 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/blocks/text-blocks.tsx +115 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/index.ts +21 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/interpreter.tsx +55 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/registry.ts +276 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/types.ts +128 -0
- package/packages/core/modules/blog-compose/lib/blog/register-sitemap.ts +18 -0
- package/packages/core/modules/blog-compose/lib/controllers/BlogComposeController.ts +2 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.controller.ts +78 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.repository.ts +78 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.service.ts +72 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.types.ts +20 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/index.ts +7 -0
- package/packages/core/modules/blog-compose/prisma/blog-compose.prisma +13 -0
- package/packages/core/modules/enquiry-modal/app/components/EnquiryModal.tsx +308 -0
- package/packages/core/modules/footer/app/components/Footer.tsx +127 -0
- package/packages/core/modules/gallery/app/admin/components/GalleryFormModal.tsx +132 -0
- package/packages/core/modules/gallery/app/admin/components/GalleryList.tsx +142 -0
- package/packages/core/modules/gallery/app/admin/hooks/useGallery.ts +72 -0
- package/packages/core/modules/gallery/app/api/gallery/[id]/route.ts +29 -0
- package/packages/core/modules/gallery/app/api/gallery/route.ts +22 -0
- package/packages/core/modules/gallery/app/components/GallerySection.tsx +92 -0
- package/packages/core/modules/gallery/app/gallery/layout.tsx +12 -0
- package/packages/core/modules/gallery/app/gallery/page.tsx +179 -0
- package/packages/core/modules/gallery/lib/controllers/GalleryController.ts +2 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.controller.ts +78 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.repository.ts +55 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.service.ts +43 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.types.ts +17 -0
- package/packages/core/modules/gallery/lib/features/gallery/index.ts +4 -0
- package/packages/core/modules/gallery/prisma/gallery.prisma +11 -0
- package/packages/core/modules/hero-simple/app/components/Hero.tsx +160 -0
- package/packages/core/modules/legal-pages/app/components/LegalDocumentPage.tsx +61 -0
- package/packages/core/modules/legal-pages/app/privacy-policy/page.tsx +19 -0
- package/packages/core/modules/legal-pages/app/terms-and-conditions/page.tsx +19 -0
- package/packages/core/modules/reviews/app/admin/components/ReviewFormModal.tsx +110 -0
- package/packages/core/modules/reviews/app/admin/components/ReviewsList.tsx +116 -0
- package/packages/core/modules/reviews/app/admin/hooks/useReviews.ts +70 -0
- package/packages/core/modules/reviews/app/api/reviews/[id]/route.ts +29 -0
- package/packages/core/modules/reviews/app/api/reviews/route.ts +17 -0
- package/packages/core/modules/reviews/app/components/ReviewsSection.tsx +100 -0
- package/packages/core/modules/reviews/lib/controllers/ReviewController.ts +2 -0
- package/packages/core/modules/reviews/lib/features/reviews/index.ts +3 -0
- package/packages/core/modules/reviews/lib/features/reviews/review.controller.ts +64 -0
- package/packages/core/modules/reviews/lib/features/reviews/review.repository.ts +50 -0
- package/packages/core/modules/reviews/lib/features/reviews/review.service.ts +38 -0
- package/packages/core/modules/reviews/prisma/reviews.prisma +9 -0
- package/packages/core/modules/seo/app/components/SiteJsonLd.tsx +16 -0
- package/packages/core/modules/seo/app/robots.ts +30 -0
- package/packages/core/modules/seo/app/sitemap.ts +9 -0
- package/packages/core/modules/seo/lib/seo/index.ts +5 -0
- package/packages/core/modules/seo/lib/seo/json-ld.ts +128 -0
- package/packages/core/modules/seo/lib/seo/metadata.ts +65 -0
- package/packages/core/modules/seo/lib/seo/sitemap.ts +82 -0
- package/packages/core/modules/seo/lib/seo/types.ts +30 -0
- package/packages/core/modules/seo/lib/seo/urls.ts +27 -0
- package/packages/core/modules/theme-modes/app/components/ThemeModeInit.tsx +24 -0
- package/packages/core/modules/theme-modes/app/components/ThemeModeToggle.tsx +58 -0
- package/packages/core/modules.json +140 -0
- package/packages/core/prisma/schema.prisma +33 -0
- package/scripts/module-installer-core.mjs +657 -0
- package/scripts/template-modules-admin.mjs +366 -0
- package/scripts/template-modules-home.mjs +120 -0
- 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
|
-
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {{
|
|
11
|
+
* yes?: boolean,
|
|
12
|
+
* dbHost?: string,
|
|
13
|
+
* dbPort?: string,
|
|
14
|
+
* dbUser?: string,
|
|
15
|
+
* dbPassword?: string,
|
|
16
|
+
* dbName?: string,
|
|
17
|
+
* adminUser?: string,
|
|
18
|
+
* adminPassword?: string,
|
|
19
|
+
* skipDbPush?: boolean,
|
|
20
|
+
* dbPush?: boolean
|
|
21
|
+
* }} DbSetupOptions
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} targetDir
|
|
26
|
+
* @param {DbSetupOptions} options
|
|
27
|
+
*/
|
|
28
|
+
export async function setupDb(targetDir, options = {}) {
|
|
10
29
|
const envPath = join(targetDir, ".env");
|
|
11
30
|
const envExamplePath = join(targetDir, ".env.example");
|
|
12
31
|
|
|
13
|
-
// Load existing env values as defaults if .env exists
|
|
14
32
|
let existingEnv = "";
|
|
15
33
|
if (existsSync(envPath)) {
|
|
16
34
|
try {
|
|
@@ -20,22 +38,23 @@ export async function promptAndSetupDb(targetDir) {
|
|
|
20
38
|
}
|
|
21
39
|
}
|
|
22
40
|
|
|
23
|
-
// Helper to extract env var
|
|
24
41
|
const getEnvVal = (key, fallback) => {
|
|
25
42
|
const match = existingEnv.match(new RegExp(`^${key}\\s*=\\s*["']?([^"'\r\n]+)["']?`, "m"));
|
|
26
43
|
return match ? match[1] : fallback;
|
|
27
44
|
};
|
|
28
45
|
|
|
29
|
-
// Parse existing DB URL if any
|
|
30
46
|
const currentDbUrl = getEnvVal("DATABASE_URL", "");
|
|
31
47
|
let defaultHost = "localhost";
|
|
32
48
|
let defaultPort = "3306";
|
|
33
49
|
let defaultUser = "root";
|
|
34
50
|
let defaultPass = "";
|
|
35
|
-
let defaultDbName =
|
|
51
|
+
let defaultDbName =
|
|
52
|
+
basename(resolve(targetDir)).toLowerCase().replace(/[^a-z0-9_]/g, "_") + "_db";
|
|
36
53
|
|
|
37
54
|
if (currentDbUrl) {
|
|
38
|
-
const urlMatch = currentDbUrl.match(
|
|
55
|
+
const urlMatch = currentDbUrl.match(
|
|
56
|
+
/mysql:\/\/([^:@]+)(?::([^@]+))?@([^:/]+)(?::(\d+))?\/([^?]+)/
|
|
57
|
+
);
|
|
39
58
|
if (urlMatch) {
|
|
40
59
|
defaultUser = urlMatch[1];
|
|
41
60
|
defaultPass = urlMatch[2] || "";
|
|
@@ -48,71 +67,173 @@ export async function promptAndSetupDb(targetDir) {
|
|
|
48
67
|
const defaultAdminUser = getEnvVal("ADMIN_USER", "admin");
|
|
49
68
|
const defaultAdminPass = getEnvVal("ADMIN_PASSWORD", crypto.randomBytes(4).toString("hex"));
|
|
50
69
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
const host = options.dbHost?.trim() || defaultHost;
|
|
71
|
+
const port = options.dbPort?.trim() || defaultPort;
|
|
72
|
+
const user = options.dbUser?.trim() || defaultUser;
|
|
73
|
+
const pass = options.dbPassword !== undefined ? options.dbPassword : defaultPass;
|
|
74
|
+
const dbName = options.dbName?.trim() || defaultDbName;
|
|
75
|
+
const adminUser = options.adminUser?.trim() || defaultAdminUser;
|
|
76
|
+
const adminPass = options.adminPassword?.trim() || defaultAdminPass;
|
|
54
77
|
|
|
55
|
-
|
|
56
|
-
const port = (await rl.question(`MySQL Database Port [${defaultPort}]: `)).trim() || defaultPort;
|
|
57
|
-
const user = (await rl.question(`MySQL Database Username [${defaultUser}]: `)).trim() || defaultUser;
|
|
58
|
-
const pass = (await rl.question(`MySQL Database Password [${defaultPass ? "*****" : "(empty)"}]: `)).trim() || defaultPass;
|
|
59
|
-
const dbName = (await rl.question(`MySQL Database Name [${defaultDbName}]: `)).trim() || defaultDbName;
|
|
78
|
+
const dbUrl = `mysql://${user}${pass ? `:${encodeURIComponent(pass)}` : ""}@${host}:${port}/${dbName}`;
|
|
60
79
|
|
|
61
|
-
|
|
62
|
-
|
|
80
|
+
let envContent = "";
|
|
81
|
+
if (existsSync(envExamplePath)) {
|
|
82
|
+
envContent = await readFile(envExamplePath, "utf8");
|
|
83
|
+
}
|
|
63
84
|
|
|
64
|
-
|
|
65
|
-
const dbUrl = `mysql://${user}${pass ? `:${encodeURIComponent(pass)}` : ""}@${host}:${port}/${dbName}`;
|
|
85
|
+
const randomSecret = crypto.randomBytes(32).toString("base64");
|
|
66
86
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
|
|
87
|
+
const updateEnvKey = (content, key, value) => {
|
|
88
|
+
const regex = new RegExp(`^#?\\s*${key}\\s*=.*$`, "m");
|
|
89
|
+
if (regex.test(content)) {
|
|
90
|
+
return content.replace(regex, `${key}="${value}"`);
|
|
71
91
|
}
|
|
92
|
+
return `${content.trim()}\n${key}="${value}"\n`;
|
|
93
|
+
};
|
|
72
94
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return content.replace(regex, `${key}="${value}"`);
|
|
81
|
-
}
|
|
82
|
-
return `${content.trim()}\n${key}="${value}"\n`;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
if (envContent) {
|
|
86
|
-
envContent = updateEnvKey(envContent, "DATABASE_URL", dbUrl);
|
|
87
|
-
envContent = updateEnvKey(envContent, "AUTH_SECRET", randomSecret);
|
|
88
|
-
envContent = updateEnvKey(envContent, "ADMIN_USER", adminUser);
|
|
89
|
-
envContent = updateEnvKey(envContent, "ADMIN_PASSWORD", adminPass);
|
|
90
|
-
} else {
|
|
91
|
-
envContent = `DATABASE_URL="${dbUrl}"
|
|
95
|
+
if (envContent) {
|
|
96
|
+
envContent = updateEnvKey(envContent, "DATABASE_URL", dbUrl);
|
|
97
|
+
envContent = updateEnvKey(envContent, "AUTH_SECRET", randomSecret);
|
|
98
|
+
envContent = updateEnvKey(envContent, "ADMIN_USER", adminUser);
|
|
99
|
+
envContent = updateEnvKey(envContent, "ADMIN_PASSWORD", adminPass);
|
|
100
|
+
} else {
|
|
101
|
+
envContent = `DATABASE_URL="${dbUrl}"
|
|
92
102
|
AUTH_SECRET="${randomSecret}"
|
|
93
103
|
ADMIN_USER="${adminUser}"
|
|
94
104
|
ADMIN_PASSWORD="${adminPass}"
|
|
95
105
|
AUTH_TRUST_HOST="true"
|
|
96
106
|
`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await writeFile(envPath, envContent, "utf8");
|
|
110
|
+
console.log("Successfully created/updated .env configuration file.");
|
|
111
|
+
|
|
112
|
+
const shouldPush =
|
|
113
|
+
options.dbPush ||
|
|
114
|
+
(options.yes && !options.skipDbPush);
|
|
115
|
+
|
|
116
|
+
if (shouldPush) {
|
|
117
|
+
console.log("\nRunning Prisma Database Push (npx prisma db push)...");
|
|
118
|
+
try {
|
|
119
|
+
execSync("npx prisma db push", { cwd: targetDir, stdio: "inherit" });
|
|
120
|
+
console.log("\nSuccessfully synchronized database schemas.");
|
|
121
|
+
} catch {
|
|
122
|
+
console.error(
|
|
123
|
+
"\nWarning: Database synchronization failed. Make sure your MySQL server is running and the database details are correct."
|
|
124
|
+
);
|
|
125
|
+
console.error("You can run this manually later inside the project folder via: npx prisma db push");
|
|
97
126
|
}
|
|
127
|
+
} else if (options.yes && options.skipDbPush) {
|
|
128
|
+
console.log("\nSkipped database table synchronization (--skip-db-push).");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {string} targetDir
|
|
134
|
+
* @param {DbSetupOptions} options
|
|
135
|
+
*/
|
|
136
|
+
export async function promptAndSetupDb(targetDir, options = {}) {
|
|
137
|
+
if (options.yes) {
|
|
138
|
+
await setupDb(targetDir, options);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
98
141
|
|
|
99
|
-
|
|
100
|
-
|
|
142
|
+
const envPath = join(targetDir, ".env");
|
|
143
|
+
const envExamplePath = join(targetDir, ".env.example");
|
|
144
|
+
|
|
145
|
+
let existingEnv = "";
|
|
146
|
+
if (existsSync(envPath)) {
|
|
147
|
+
try {
|
|
148
|
+
existingEnv = await readFile(envPath, "utf8");
|
|
149
|
+
} catch {
|
|
150
|
+
// Ignore
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const getEnvVal = (key, fallback) => {
|
|
155
|
+
const match = existingEnv.match(new RegExp(`^${key}\\s*=\\s*["']?([^"'\r\n]+)["']?`, "m"));
|
|
156
|
+
return match ? match[1] : fallback;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const currentDbUrl = getEnvVal("DATABASE_URL", "");
|
|
160
|
+
let defaultHost = "localhost";
|
|
161
|
+
let defaultPort = "3306";
|
|
162
|
+
let defaultUser = "root";
|
|
163
|
+
let defaultPass = "";
|
|
164
|
+
let defaultDbName =
|
|
165
|
+
basename(resolve(targetDir)).toLowerCase().replace(/[^a-z0-9_]/g, "_") + "_db";
|
|
166
|
+
|
|
167
|
+
if (currentDbUrl) {
|
|
168
|
+
const urlMatch = currentDbUrl.match(
|
|
169
|
+
/mysql:\/\/([^:@]+)(?::([^@]+))?@([^:/]+)(?::(\d+))?\/([^?]+)/
|
|
170
|
+
);
|
|
171
|
+
if (urlMatch) {
|
|
172
|
+
defaultUser = urlMatch[1];
|
|
173
|
+
defaultPass = urlMatch[2] || "";
|
|
174
|
+
defaultHost = urlMatch[3];
|
|
175
|
+
defaultPort = urlMatch[4] || "3306";
|
|
176
|
+
defaultDbName = urlMatch[5];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const defaultAdminUser = getEnvVal("ADMIN_USER", "admin");
|
|
181
|
+
const defaultAdminPass = getEnvVal("ADMIN_PASSWORD", crypto.randomBytes(4).toString("hex"));
|
|
182
|
+
|
|
183
|
+
const rl = createInterface({ input, output });
|
|
184
|
+
try {
|
|
185
|
+
console.log("\n--- Configure Database & Environment Variables ---");
|
|
101
186
|
|
|
102
|
-
|
|
103
|
-
|
|
187
|
+
const host =
|
|
188
|
+
(await rl.question(`MySQL Database Host [${defaultHost}]: `)).trim() || defaultHost;
|
|
189
|
+
const port =
|
|
190
|
+
(await rl.question(`MySQL Database Port [${defaultPort}]: `)).trim() || defaultPort;
|
|
191
|
+
const user =
|
|
192
|
+
(await rl.question(`MySQL Database Username [${defaultUser}]: `)).trim() || defaultUser;
|
|
193
|
+
const pass =
|
|
194
|
+
(await rl.question(`MySQL Database Password [${defaultPass ? "*****" : "(empty)"}]: `)).trim() ||
|
|
195
|
+
defaultPass;
|
|
196
|
+
const dbName =
|
|
197
|
+
(await rl.question(`MySQL Database Name [${defaultDbName}]: `)).trim() || defaultDbName;
|
|
198
|
+
const adminUser =
|
|
199
|
+
(await rl.question(`Admin Portal Username [${defaultAdminUser}]: `)).trim() ||
|
|
200
|
+
defaultAdminUser;
|
|
201
|
+
const adminPass =
|
|
202
|
+
(await rl.question(`Admin Portal Password [${defaultAdminPass}]: `)).trim() ||
|
|
203
|
+
defaultAdminPass;
|
|
204
|
+
|
|
205
|
+
await setupDb(targetDir, {
|
|
206
|
+
dbHost: host,
|
|
207
|
+
dbPort: port,
|
|
208
|
+
dbUser: user,
|
|
209
|
+
dbPassword: pass,
|
|
210
|
+
dbName,
|
|
211
|
+
adminUser,
|
|
212
|
+
adminPassword: adminPass,
|
|
213
|
+
skipDbPush: true,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const runMigrate = await rl.question(
|
|
217
|
+
"\nDo you want to initialize the database tables now using Prisma? (y/n) [y]: "
|
|
218
|
+
);
|
|
104
219
|
const choice = runMigrate.trim().toLowerCase();
|
|
105
220
|
if (choice === "" || choice === "y" || choice === "yes") {
|
|
106
221
|
console.log("\nRunning Prisma Database Push (npx prisma db push)...");
|
|
107
222
|
try {
|
|
108
223
|
execSync("npx prisma db push", { cwd: targetDir, stdio: "inherit" });
|
|
109
224
|
console.log("\nSuccessfully synchronized database schemas.");
|
|
110
|
-
} catch
|
|
111
|
-
console.error(
|
|
112
|
-
|
|
225
|
+
} catch {
|
|
226
|
+
console.error(
|
|
227
|
+
"\nWarning: Database synchronization failed. Make sure your MySQL server is running and the database details are correct."
|
|
228
|
+
);
|
|
229
|
+
console.error(
|
|
230
|
+
"You can run this manually later inside the project folder via: npx prisma db push"
|
|
231
|
+
);
|
|
113
232
|
}
|
|
114
233
|
} else {
|
|
115
|
-
console.log(
|
|
234
|
+
console.log(
|
|
235
|
+
"\nSkipped database table synchronization. Remember to run 'npx prisma db push' before running the app."
|
|
236
|
+
);
|
|
116
237
|
}
|
|
117
238
|
} finally {
|
|
118
239
|
rl.close();
|
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
|
+
}
|