@navneet_25/tempjs 2.0.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 +66 -18
- package/cli/brand-manager.js +183 -96
- package/cli/config.js +9 -1
- package/cli/doctor.js +393 -0
- package/cli/fetch-core-modules.js +130 -0
- package/cli/index.js +141 -10
- package/cli/info.js +28 -1
- package/cli/init-options.js +209 -0
- package/cli/module-manager.js +168 -0
- package/cli/parse-args.js +21 -0
- package/cli/update.js +15 -0
- package/cli/version-manager.js +518 -0
- package/package.json +27 -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 +163 -8
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
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createWriteStream, existsSync, statSync } from "node:fs";
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { pipeline } from "node:stream/promises";
|
|
7
|
+
import { Readable } from "node:stream";
|
|
8
|
+
|
|
9
|
+
const CODELOAD_BASE = "https://codeload.github.com";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {{ coreDir: string, cleanupDir: string }} CoreModulesFetchResult
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {import('./config.js').RepositoryConfig} repo
|
|
17
|
+
* @returns {Promise<CoreModulesFetchResult>}
|
|
18
|
+
*/
|
|
19
|
+
export async function fetchCoreModulesFromGitHub(repo) {
|
|
20
|
+
const tempRoot = await mkdtemp(join(tmpdir(), "tempjs-core-"));
|
|
21
|
+
const tarballPath = join(tempRoot, "archive.tar.gz");
|
|
22
|
+
const coreDir = join(tempRoot, "core");
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
await downloadTarball(repo, tarballPath);
|
|
26
|
+
await extractCoreFromTarball(tarballPath, coreDir, repo);
|
|
27
|
+
await readFile(join(coreDir, "modules.json"));
|
|
28
|
+
|
|
29
|
+
return { coreDir, cleanupDir: tempRoot };
|
|
30
|
+
} catch (error) {
|
|
31
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {string} packageRoot
|
|
38
|
+
* @returns {string | null}
|
|
39
|
+
*/
|
|
40
|
+
export function resolveLocalCoreDir(packageRoot) {
|
|
41
|
+
const localPath = join(packageRoot, "packages", "core");
|
|
42
|
+
if (existsSync(join(localPath, "modules.json"))) {
|
|
43
|
+
return localPath;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {import('./config.js').RepositoryConfig} repo
|
|
50
|
+
* @param {string} destPath
|
|
51
|
+
*/
|
|
52
|
+
async function downloadTarball(repo, destPath) {
|
|
53
|
+
const branch = encodeURIComponent(repo.branch);
|
|
54
|
+
const url = `${CODELOAD_BASE}/${repo.owner}/${repo.repo}/tar.gz/${branch}`;
|
|
55
|
+
|
|
56
|
+
const response = await fetch(url, {
|
|
57
|
+
headers: buildHeaders(),
|
|
58
|
+
redirect: "follow",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
let detail = `Could not download ${repo.owner}/${repo.repo}@${repo.branch}`;
|
|
63
|
+
if (response.status === 404) {
|
|
64
|
+
detail += "\nVerify the repository, branch, and templates.json settings.";
|
|
65
|
+
}
|
|
66
|
+
if (response.status === 401 || response.status === 403) {
|
|
67
|
+
detail += "\nFor private repositories, set GITHUB_TOKEN or GH_TOKEN.";
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`${detail} (HTTP ${response.status})`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!response.body) {
|
|
73
|
+
throw new Error("Empty response while downloading repository archive");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(destPath));
|
|
77
|
+
return statSync(destPath).size;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} tarballPath
|
|
82
|
+
* @param {string} destDir
|
|
83
|
+
* @param {import('./config.js').RepositoryConfig} repo
|
|
84
|
+
*/
|
|
85
|
+
async function extractCoreFromTarball(tarballPath, destDir, repo) {
|
|
86
|
+
const listing = execFileSync("tar", ["-tzf", tarballPath], {
|
|
87
|
+
encoding: "utf8",
|
|
88
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const lines = listing.split("\n").filter(Boolean);
|
|
92
|
+
if (lines.length === 0) {
|
|
93
|
+
throw new Error("Repository archive is empty");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const archiveRoot = lines[0].replace(/\/$/, "").split("/")[0];
|
|
97
|
+
const coreArchivePath = `${archiveRoot}/packages/core`;
|
|
98
|
+
const corePrefix = `${coreArchivePath}/`;
|
|
99
|
+
|
|
100
|
+
const hasCore = lines.some((line) => line === corePrefix || line.startsWith(corePrefix));
|
|
101
|
+
|
|
102
|
+
if (!hasCore) {
|
|
103
|
+
throw new Error(`Core package not found in repository: packages/core`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const stripComponents = coreArchivePath.split("/").length;
|
|
107
|
+
|
|
108
|
+
await mkdir(destDir, { recursive: true });
|
|
109
|
+
|
|
110
|
+
execFileSync(
|
|
111
|
+
"tar",
|
|
112
|
+
[
|
|
113
|
+
"-xzf",
|
|
114
|
+
tarballPath,
|
|
115
|
+
"-C",
|
|
116
|
+
destDir,
|
|
117
|
+
`--strip-components=${stripComponents}`,
|
|
118
|
+
coreArchivePath,
|
|
119
|
+
],
|
|
120
|
+
{ stdio: "pipe", maxBuffer: 64 * 1024 * 1024 }
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildHeaders() {
|
|
125
|
+
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
126
|
+
if (token) {
|
|
127
|
+
return { Authorization: `token ${token}` };
|
|
128
|
+
}
|
|
129
|
+
return {};
|
|
130
|
+
}
|