@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.
Files changed (90) hide show
  1. package/README.md +66 -18
  2. package/cli/brand-manager.js +183 -96
  3. package/cli/config.js +9 -1
  4. package/cli/doctor.js +393 -0
  5. package/cli/fetch-core-modules.js +130 -0
  6. package/cli/index.js +141 -10
  7. package/cli/info.js +28 -1
  8. package/cli/init-options.js +209 -0
  9. package/cli/module-manager.js +168 -0
  10. package/cli/parse-args.js +21 -0
  11. package/cli/update.js +15 -0
  12. package/cli/version-manager.js +518 -0
  13. package/package.json +27 -4
  14. package/packages/core/modules/README.md +72 -0
  15. package/packages/core/modules/blog-compose/app/admin/components/blog-compose-form.ts +44 -0
  16. package/packages/core/modules/blog-compose/app/admin/hooks/useBlogPosts.ts +103 -0
  17. package/packages/core/modules/blog-compose/app/admin/panels/BlogComposePanel.tsx +388 -0
  18. package/packages/core/modules/blog-compose/app/api/blog-posts/[id]/route.ts +29 -0
  19. package/packages/core/modules/blog-compose/app/api/blog-posts/route.ts +21 -0
  20. package/packages/core/modules/blog-compose/app/blog/[slug]/page.tsx +79 -0
  21. package/packages/core/modules/blog-compose/app/blog/layout.tsx +22 -0
  22. package/packages/core/modules/blog-compose/app/blog/page.tsx +102 -0
  23. package/packages/core/modules/blog-compose/app/components/BlogSection.tsx +74 -0
  24. package/packages/core/modules/blog-compose/app/components/BlogShell.tsx +36 -0
  25. package/packages/core/modules/blog-compose/app/components/BlogSidebar.tsx +112 -0
  26. package/packages/core/modules/blog-compose/lib/blog/compose/annotate-text.tsx +103 -0
  27. package/packages/core/modules/blog-compose/lib/blog/compose/blocks/media-blocks.tsx +97 -0
  28. package/packages/core/modules/blog-compose/lib/blog/compose/blocks/text-blocks.tsx +115 -0
  29. package/packages/core/modules/blog-compose/lib/blog/compose/index.ts +21 -0
  30. package/packages/core/modules/blog-compose/lib/blog/compose/interpreter.tsx +55 -0
  31. package/packages/core/modules/blog-compose/lib/blog/compose/registry.ts +276 -0
  32. package/packages/core/modules/blog-compose/lib/blog/compose/types.ts +128 -0
  33. package/packages/core/modules/blog-compose/lib/blog/register-sitemap.ts +18 -0
  34. package/packages/core/modules/blog-compose/lib/controllers/BlogComposeController.ts +2 -0
  35. package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.controller.ts +78 -0
  36. package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.repository.ts +78 -0
  37. package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.service.ts +72 -0
  38. package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.types.ts +20 -0
  39. package/packages/core/modules/blog-compose/lib/features/blog-compose/index.ts +7 -0
  40. package/packages/core/modules/blog-compose/prisma/blog-compose.prisma +13 -0
  41. package/packages/core/modules/enquiry-modal/app/components/EnquiryModal.tsx +308 -0
  42. package/packages/core/modules/footer/app/components/Footer.tsx +127 -0
  43. package/packages/core/modules/gallery/app/admin/components/GalleryFormModal.tsx +132 -0
  44. package/packages/core/modules/gallery/app/admin/components/GalleryList.tsx +142 -0
  45. package/packages/core/modules/gallery/app/admin/hooks/useGallery.ts +72 -0
  46. package/packages/core/modules/gallery/app/api/gallery/[id]/route.ts +29 -0
  47. package/packages/core/modules/gallery/app/api/gallery/route.ts +22 -0
  48. package/packages/core/modules/gallery/app/components/GallerySection.tsx +92 -0
  49. package/packages/core/modules/gallery/app/gallery/layout.tsx +12 -0
  50. package/packages/core/modules/gallery/app/gallery/page.tsx +179 -0
  51. package/packages/core/modules/gallery/lib/controllers/GalleryController.ts +2 -0
  52. package/packages/core/modules/gallery/lib/features/gallery/gallery.controller.ts +78 -0
  53. package/packages/core/modules/gallery/lib/features/gallery/gallery.repository.ts +55 -0
  54. package/packages/core/modules/gallery/lib/features/gallery/gallery.service.ts +43 -0
  55. package/packages/core/modules/gallery/lib/features/gallery/gallery.types.ts +17 -0
  56. package/packages/core/modules/gallery/lib/features/gallery/index.ts +4 -0
  57. package/packages/core/modules/gallery/prisma/gallery.prisma +11 -0
  58. package/packages/core/modules/hero-simple/app/components/Hero.tsx +160 -0
  59. package/packages/core/modules/legal-pages/app/components/LegalDocumentPage.tsx +61 -0
  60. package/packages/core/modules/legal-pages/app/privacy-policy/page.tsx +19 -0
  61. package/packages/core/modules/legal-pages/app/terms-and-conditions/page.tsx +19 -0
  62. package/packages/core/modules/reviews/app/admin/components/ReviewFormModal.tsx +110 -0
  63. package/packages/core/modules/reviews/app/admin/components/ReviewsList.tsx +116 -0
  64. package/packages/core/modules/reviews/app/admin/hooks/useReviews.ts +70 -0
  65. package/packages/core/modules/reviews/app/api/reviews/[id]/route.ts +29 -0
  66. package/packages/core/modules/reviews/app/api/reviews/route.ts +17 -0
  67. package/packages/core/modules/reviews/app/components/ReviewsSection.tsx +100 -0
  68. package/packages/core/modules/reviews/lib/controllers/ReviewController.ts +2 -0
  69. package/packages/core/modules/reviews/lib/features/reviews/index.ts +3 -0
  70. package/packages/core/modules/reviews/lib/features/reviews/review.controller.ts +64 -0
  71. package/packages/core/modules/reviews/lib/features/reviews/review.repository.ts +50 -0
  72. package/packages/core/modules/reviews/lib/features/reviews/review.service.ts +38 -0
  73. package/packages/core/modules/reviews/prisma/reviews.prisma +9 -0
  74. package/packages/core/modules/seo/app/components/SiteJsonLd.tsx +16 -0
  75. package/packages/core/modules/seo/app/robots.ts +30 -0
  76. package/packages/core/modules/seo/app/sitemap.ts +9 -0
  77. package/packages/core/modules/seo/lib/seo/index.ts +5 -0
  78. package/packages/core/modules/seo/lib/seo/json-ld.ts +128 -0
  79. package/packages/core/modules/seo/lib/seo/metadata.ts +65 -0
  80. package/packages/core/modules/seo/lib/seo/sitemap.ts +82 -0
  81. package/packages/core/modules/seo/lib/seo/types.ts +30 -0
  82. package/packages/core/modules/seo/lib/seo/urls.ts +27 -0
  83. package/packages/core/modules/theme-modes/app/components/ThemeModeInit.tsx +24 -0
  84. package/packages/core/modules/theme-modes/app/components/ThemeModeToggle.tsx +58 -0
  85. package/packages/core/modules.json +140 -0
  86. package/packages/core/prisma/schema.prisma +33 -0
  87. package/scripts/module-installer-core.mjs +657 -0
  88. package/scripts/template-modules-admin.mjs +366 -0
  89. package/scripts/template-modules-home.mjs +120 -0
  90. 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
+ }