@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
@@ -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
+ }
package/cli/fetch.js CHANGED
@@ -1,39 +1,44 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { createWriteStream, existsSync } from "node:fs";
2
+ import { createWriteStream, existsSync, statSync } from "node:fs";
3
3
  import { mkdir, mkdtemp, readdir, readFile, rm, stat } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import { pipeline } from "node:stream/promises";
7
7
  import { Readable } from "node:stream";
8
+ import { shouldSkipFileName } from "./fs-ignore.js";
9
+ import { countFiles } from "./file-tree.js";
8
10
 
9
11
  const CODELOAD_BASE = "https://codeload.github.com";
10
12
 
11
13
  /**
12
- * Download a single template directory from GitHub without cloning or per-file API calls.
13
- *
14
- * Strategy: one tarball download from codeload.github.com (not the REST API), then
15
- * extract only templates/<templateDirectory>/ from the archive. This uses a single
16
- * HTTP request instead of 60+ blob API calls, avoiding unauthenticated rate limits.
17
- *
14
+ * @typedef {{ bytes: number, files: number, durationMs: number, source: string }} FetchStats
15
+ * @typedef {{ templateRoot: string, cleanupDir: string, stats: FetchStats }} FetchResult
16
+ */
17
+
18
+ /**
18
19
  * @param {import('./config.js').RepositoryConfig} repo
19
20
  * @param {string} templateDirectory
20
- * @returns {Promise<string>} Path to temp directory containing template files
21
+ * @returns {Promise<FetchResult>}
21
22
  */
22
23
  export async function fetchTemplateFromGitHub(repo, templateDirectory) {
24
+ const startMs = performance.now();
23
25
  const tempRoot = await mkdtemp(join(tmpdir(), "template-cli-"));
24
26
  const tarballPath = join(tempRoot, "archive.tar.gz");
25
27
  const templateRoot = join(tempRoot, "template");
26
28
 
27
29
  try {
28
- await downloadTarball(repo, tarballPath);
29
- await extractTemplateFromTarball(
30
- tarballPath,
31
- templateRoot,
32
- repo,
33
- templateDirectory
34
- );
30
+ const bytes = await downloadTarball(repo, tarballPath);
31
+ await extractTemplateFromTarball(tarballPath, templateRoot, repo, templateDirectory);
35
32
  await readFile(join(templateRoot, "package.json"));
36
- return templateRoot;
33
+
34
+ const durationMs = performance.now() - startMs;
35
+ const files = countFiles(templateRoot);
36
+
37
+ return {
38
+ templateRoot,
39
+ cleanupDir: tempRoot,
40
+ stats: { bytes, files, durationMs, source: "remote" },
41
+ };
37
42
  } catch (error) {
38
43
  await rm(tempRoot, { recursive: true, force: true });
39
44
  throw error;
@@ -43,6 +48,7 @@ export async function fetchTemplateFromGitHub(repo, templateDirectory) {
43
48
  /**
44
49
  * @param {import('./config.js').RepositoryConfig} repo
45
50
  * @param {string} destPath
51
+ * @returns {Promise<number>} Downloaded bytes
46
52
  */
47
53
  async function downloadTarball(repo, destPath) {
48
54
  const branch = encodeURIComponent(repo.branch);
@@ -69,6 +75,7 @@ async function downloadTarball(repo, destPath) {
69
75
  }
70
76
 
71
77
  await pipeline(Readable.fromWeb(response.body), createWriteStream(destPath));
78
+ return statSync(destPath).size;
72
79
  }
73
80
 
74
81
  /**
@@ -128,13 +135,12 @@ async function extractTemplateFromTarball(
128
135
  }
129
136
 
130
137
  /**
131
- * Remove files that must never appear in generated projects.
132
138
  * @param {string} dir
133
139
  */
134
140
  async function removeSkippedFiles(dir) {
135
141
  if (!existsSync(dir)) return;
136
142
 
137
- const entries = await readDirSafe(dir);
143
+ const entries = await readDirEntries(dir);
138
144
  for (const entry of entries) {
139
145
  const fullPath = join(dir, entry.name);
140
146
  if (entry.isDirectory) {
@@ -155,7 +161,7 @@ async function removeSkippedFiles(dir) {
155
161
  /**
156
162
  * @param {string} dir
157
163
  */
158
- async function readDirSafe(dir) {
164
+ async function readDirEntries(dir) {
159
165
  const names = await readdir(dir);
160
166
  const result = [];
161
167
  for (const name of names) {
@@ -166,16 +172,6 @@ async function readDirSafe(dir) {
166
172
  return result;
167
173
  }
168
174
 
169
- /**
170
- * @param {string} name
171
- */
172
- function shouldSkipFileName(name) {
173
- if (name === ".env" || (name.startsWith(".env.") && name !== ".env.example")) {
174
- return true;
175
- }
176
- return false;
177
- }
178
-
179
175
  function buildHeaders() {
180
176
  const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
181
177
  if (token) {
@@ -199,3 +195,25 @@ export async function resolveLocalTemplate(packageRoot, templatesPath, templateD
199
195
  return null;
200
196
  }
201
197
  }
198
+
199
+ /**
200
+ * @param {string} packageRoot
201
+ * @param {string} templatesPath
202
+ * @param {string} templateDirectory
203
+ * @returns {Promise<FetchResult | null>}
204
+ */
205
+ export async function loadLocalTemplate(packageRoot, templatesPath, templateDirectory) {
206
+ const localPath = await resolveLocalTemplate(packageRoot, templatesPath, templateDirectory);
207
+ if (!localPath) return null;
208
+
209
+ return {
210
+ templateRoot: localPath,
211
+ cleanupDir: "",
212
+ stats: {
213
+ bytes: 0,
214
+ files: countFiles(localPath),
215
+ durationMs: 0,
216
+ source: "local",
217
+ },
218
+ };
219
+ }
@@ -0,0 +1,113 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+ import { shouldSkipPath } from "./fs-ignore.js";
5
+
6
+ /**
7
+ * @param {string} filePath
8
+ * @returns {string}
9
+ */
10
+ export function hashFileContents(filePath) {
11
+ return createHash("sha256").update(readFileSync(filePath)).digest("hex");
12
+ }
13
+
14
+ /**
15
+ * @param {string} rootDir
16
+ * @returns {Record<string, string>}
17
+ */
18
+ export function collectFileHashes(rootDir) {
19
+ const hashes = {};
20
+ collectRecursive(rootDir, rootDir, hashes);
21
+ return hashes;
22
+ }
23
+
24
+ /**
25
+ * @param {string} rootDir
26
+ * @param {string} currentDir
27
+ * @param {Record<string, string>} hashes
28
+ */
29
+ function collectRecursive(rootDir, currentDir, hashes) {
30
+ if (!existsSync(currentDir)) return;
31
+
32
+ for (const name of readdirSync(currentDir)) {
33
+ const fullPath = join(currentDir, name);
34
+ const relPath = relative(rootDir, fullPath).replace(/\\/g, "/");
35
+
36
+ if (shouldSkipPath(relPath, name)) continue;
37
+
38
+ const stat = statSync(fullPath);
39
+ if (stat.isDirectory()) {
40
+ collectRecursive(rootDir, fullPath, hashes);
41
+ } else {
42
+ hashes[relPath] = hashFileContents(fullPath);
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * @param {string} rootDir
49
+ * @returns {number}
50
+ */
51
+ export function countFiles(rootDir) {
52
+ return Object.keys(collectFileHashes(rootDir)).length;
53
+ }
54
+
55
+ /**
56
+ * @typedef {{
57
+ * newFiles: string[],
58
+ * upToDate: string[],
59
+ * safeUpdates: string[],
60
+ * conflicts: string[],
61
+ * protectedSkips: string[],
62
+ * removedFromTemplate: string[]
63
+ * }} UpdateDiff
64
+ */
65
+
66
+ /**
67
+ * Compare project against baseline (at generation) and latest template hashes.
68
+ * @param {Record<string, string>} baselineHashes
69
+ * @param {Record<string, string>} currentHashes
70
+ * @param {Record<string, string>} latestHashes
71
+ * @returns {UpdateDiff}
72
+ */
73
+ export function diffTemplateTrees(baselineHashes, currentHashes, latestHashes) {
74
+ const diff = {
75
+ newFiles: [],
76
+ upToDate: [],
77
+ safeUpdates: [],
78
+ conflicts: [],
79
+ protectedSkips: [],
80
+ removedFromTemplate: [],
81
+ };
82
+
83
+ for (const path of Object.keys(latestHashes).sort()) {
84
+ const latest = latestHashes[path];
85
+ const current = currentHashes[path];
86
+ const baseline = baselineHashes[path];
87
+
88
+ if (current === undefined) {
89
+ diff.newFiles.push(path);
90
+ continue;
91
+ }
92
+
93
+ if (current === latest) {
94
+ diff.upToDate.push(path);
95
+ continue;
96
+ }
97
+
98
+ if (baseline !== undefined && current === baseline && latest !== baseline) {
99
+ diff.safeUpdates.push(path);
100
+ continue;
101
+ }
102
+
103
+ diff.conflicts.push(path);
104
+ }
105
+
106
+ for (const path of Object.keys(baselineHashes).sort()) {
107
+ if (!latestHashes[path]) {
108
+ diff.removedFromTemplate.push(path);
109
+ }
110
+ }
111
+
112
+ return diff;
113
+ }
@@ -0,0 +1,60 @@
1
+ /** Files and directories never copied from templates or merged on update. */
2
+ export const NEVER_COPY_NAMES = new Set([".git", ".gitignore.bak"]);
3
+
4
+ /** Paths relative to project root that must not be overwritten by template update. */
5
+ export const UPDATE_PROTECTED_PATHS = new Set([
6
+ ".env",
7
+ ".tempjsrc",
8
+ ".tempjs.json",
9
+ "constants/site.ts",
10
+ "app/tempjs-theme.css",
11
+ ]);
12
+
13
+ /** Directory names skipped when walking trees. */
14
+ export const SKIP_DIR_NAMES = new Set([
15
+ "node_modules",
16
+ ".next",
17
+ ".git",
18
+ "dist",
19
+ "build",
20
+ "coverage",
21
+ ]);
22
+
23
+ /**
24
+ * @param {string} name
25
+ * @returns {boolean}
26
+ */
27
+ export function shouldSkipFileName(name) {
28
+ if (NEVER_COPY_NAMES.has(name)) return true;
29
+ if (name === ".env" || name.startsWith(".env.")) {
30
+ return name !== ".env.example";
31
+ }
32
+ return false;
33
+ }
34
+
35
+ /**
36
+ * @param {string} relativePath
37
+ * @param {string} [name]
38
+ * @returns {boolean}
39
+ */
40
+ export function shouldSkipPath(relativePath, name) {
41
+ const base = name ?? relativePath.split("/").pop() ?? "";
42
+ if (shouldSkipFileName(base)) return true;
43
+
44
+ const parts = relativePath.split("/");
45
+ for (const part of parts) {
46
+ if (SKIP_DIR_NAMES.has(part)) return true;
47
+ }
48
+ return false;
49
+ }
50
+
51
+ /**
52
+ * @param {string} relativePath
53
+ * @returns {boolean}
54
+ */
55
+ export function isUpdateProtected(relativePath) {
56
+ const normalized = relativePath.replace(/\\/g, "/");
57
+ if (UPDATE_PROTECTED_PATHS.has(normalized)) return true;
58
+ if (normalized.startsWith(".env.") && normalized !== ".env.example") return true;
59
+ return false;
60
+ }