@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,209 @@
1
+ /**
2
+ * Per-template init field registry and flag resolution.
3
+ * Maintainers: add fields here once, then reference keys in templates.json `init.brandFields`.
4
+ */
5
+
6
+ /** @typedef {{
7
+ * flag: string,
8
+ * cliKey: string,
9
+ * prompt: string,
10
+ * description: string,
11
+ * siteKey?: string,
12
+ * example?: string
13
+ * }} InitFieldDef */
14
+
15
+ /** @typedef {{
16
+ * flagGroups?: string[],
17
+ * brandFields?: string[],
18
+ * skipBrand?: boolean,
19
+ * skipDatabase?: boolean
20
+ * }} TemplateInitConfig */
21
+
22
+ export const DEFAULT_FLAG_GROUPS = ["generate", "theme", "brand", "database"];
23
+
24
+ export const DEFAULT_BRAND_FIELDS = [
25
+ "name",
26
+ "shortName",
27
+ "baseUrl",
28
+ "phone",
29
+ "phoneDisplay",
30
+ "countryCode",
31
+ "email",
32
+ "address",
33
+ ];
34
+
35
+ /**
36
+ * All supported brand/init fields. Keys match templates.json `init.brandFields` entries.
37
+ * @type {Record<string, InitFieldDef>}
38
+ */
39
+ export const INIT_FIELD_REGISTRY = {
40
+ name: {
41
+ flag: "--name",
42
+ cliKey: "name",
43
+ prompt: "Brand Name",
44
+ description: "Primary business name shown across the site",
45
+ siteKey: "brandName",
46
+ example: "--name \"Lakeside Haven Resort\"",
47
+ },
48
+ shortName: {
49
+ flag: "--short-name",
50
+ cliKey: "shortName",
51
+ prompt: "Short/Display Name",
52
+ description: "Compact label for headers and compact UI",
53
+ example: "--short-name \"Lakeside Haven\"",
54
+ },
55
+ baseUrl: {
56
+ flag: "--base-url",
57
+ cliKey: "baseUrl",
58
+ prompt: "Base URL",
59
+ description: "Production site URL (SEO, sitemap, canonical links)",
60
+ example: "--base-url https://lakesidehaven.com",
61
+ },
62
+ phone: {
63
+ flag: "--phone",
64
+ cliKey: "phone",
65
+ prompt: "Contact Phone Number",
66
+ description: "Raw phone digits without spaces",
67
+ example: "--phone 9876543210",
68
+ },
69
+ phoneDisplay: {
70
+ flag: "--phone-display",
71
+ cliKey: "phoneDisplay",
72
+ prompt: "Display Phone Number",
73
+ description: "Formatted phone for display in UI",
74
+ example: "--phone-display \"+91 98765 43210\"",
75
+ },
76
+ countryCode: {
77
+ flag: "--country-code",
78
+ cliKey: "countryCode",
79
+ prompt: "Country Code",
80
+ description: "Phone country code (no plus sign)",
81
+ example: "--country-code 91",
82
+ },
83
+ email: {
84
+ flag: "--email",
85
+ cliKey: "email",
86
+ prompt: "Contact Email",
87
+ description: "Public contact email",
88
+ example: "--email reservations@example.com",
89
+ },
90
+ address: {
91
+ flag: "--address",
92
+ cliKey: "address",
93
+ prompt: "Full Address",
94
+ description: "Full address string (locality and region are derived for structured fields)",
95
+ example: "--address \"Lonavala, Maharashtra, India\"",
96
+ },
97
+ tagline: {
98
+ flag: "--tagline",
99
+ cliKey: "tagline",
100
+ prompt: "Tagline",
101
+ description: "Short brand tagline under the logo",
102
+ example: "--tagline \"Where Nature Meets Refined Comfort\"",
103
+ },
104
+ developerName: {
105
+ flag: "--developer-name",
106
+ cliKey: "developerName",
107
+ prompt: "Developer / Company Name",
108
+ description: "Legal or developer entity name (real estate) or resort operator name",
109
+ example: "--developer-name \"Greenfield Properties\"",
110
+ },
111
+ channelPartner: {
112
+ flag: "--channel-partner",
113
+ cliKey: "channelPartner",
114
+ prompt: "Channel Partner Name",
115
+ description: "Sales or hospitality partner label used in consent text",
116
+ example: "--channel-partner \"Greenfield Realty Partners\"",
117
+ },
118
+ agentRera: {
119
+ flag: "--agent-rera",
120
+ cliKey: "agentRera",
121
+ prompt: "Agent RERA Number",
122
+ description: "RERA registration for real-estate agent/consent (legal.agentRera)",
123
+ example: "--agent-rera A52100012345",
124
+ },
125
+ templeDistance: {
126
+ flag: "--temple-distance",
127
+ cliKey: "templeDistance",
128
+ prompt: "Nearby Landmark Distance",
129
+ description: "Hotel-specific nearby landmark line (contact.templeDistance)",
130
+ example: "--temple-distance \"5 min walk to Lonavala Lake viewpoint\"",
131
+ },
132
+ company: {
133
+ flag: "--company",
134
+ cliKey: "company",
135
+ prompt: "Company Name",
136
+ description: "Alias for developer/company name when templates use company branding",
137
+ siteKey: "developerName",
138
+ example: "--company \"Mi Plaza Hospitality\"",
139
+ },
140
+ location: {
141
+ flag: "--location",
142
+ cliKey: "location",
143
+ prompt: "Primary Location",
144
+ description: "City or locality label (updates address locality when full address is not set)",
145
+ example: "--location \"Lonavala\"",
146
+ },
147
+ };
148
+
149
+ /**
150
+ * @param {import('./config.js').TemplateEntry} entry
151
+ * @returns {TemplateInitConfig}
152
+ */
153
+ export function resolveTemplateInitConfig(entry) {
154
+ const init = entry.init ?? {};
155
+ return {
156
+ flagGroups: init.flagGroups ?? DEFAULT_FLAG_GROUPS,
157
+ brandFields: init.brandFields ?? DEFAULT_BRAND_FIELDS,
158
+ skipBrand: init.skipBrand ?? false,
159
+ skipDatabase: init.skipDatabase ?? false,
160
+ };
161
+ }
162
+
163
+ /**
164
+ * @param {import('./config.js').TemplateEntry} entry
165
+ * @returns {InitFieldDef[]}
166
+ */
167
+ export function getTemplateBrandFields(entry) {
168
+ const { brandFields } = resolveTemplateInitConfig(entry);
169
+ return brandFields
170
+ .map((key) => INIT_FIELD_REGISTRY[key])
171
+ .filter(Boolean);
172
+ }
173
+
174
+ /**
175
+ * @param {string} templateId
176
+ * @param {import('./config.js').TemplateEntry} entry
177
+ * @param {Record<string, string | undefined>} options
178
+ */
179
+ export function pickBrandOptionsForTemplate(templateId, entry, options) {
180
+ const { brandFields, skipBrand } = resolveTemplateInitConfig(entry);
181
+ if (skipBrand) return {};
182
+
183
+ const picked = { yes: options.yes };
184
+
185
+ for (const fieldKey of brandFields) {
186
+ const def = INIT_FIELD_REGISTRY[fieldKey];
187
+ if (!def) continue;
188
+
189
+ const value = options[def.cliKey];
190
+ if (value !== undefined && value !== "") {
191
+ picked[def.cliKey] = value;
192
+ }
193
+ }
194
+
195
+ // company flag maps to developerName in brand-manager
196
+ if (options.company && !picked.developerName) {
197
+ picked.developerName = options.company;
198
+ }
199
+
200
+ return picked;
201
+ }
202
+
203
+ /**
204
+ * @param {import('./config.js').TemplateEntry} entry
205
+ * @returns {string[]}
206
+ */
207
+ export function getTemplateFlagGroups(entry) {
208
+ return resolveTemplateInitConfig(entry).flagGroups;
209
+ }
@@ -0,0 +1,168 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, relative } from "node:path";
3
+ import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { rm } from "node:fs/promises";
5
+
6
+ import { getPackageRoot } from "./config.js";
7
+ import { fetchCoreModulesFromGitHub, resolveLocalCoreDir } from "./fetch-core-modules.js";
8
+ import { readProjectStamp } from "./project-stamp.js";
9
+
10
+ const scriptsDir = join(dirname(fileURLToPath(import.meta.url)), "..", "scripts");
11
+
12
+ /**
13
+ * @param {string} projectDir
14
+ */
15
+ function assertInitializedProject(projectDir) {
16
+ const stamp = readProjectStamp(projectDir);
17
+ if (!stamp) {
18
+ throw new Error(
19
+ "This directory does not look like a tempjs project (missing .tempjs.json).\n" +
20
+ "Run tempjs <template-id> from an empty directory first."
21
+ );
22
+ }
23
+ }
24
+
25
+ /**
26
+ * @param {string} coreDir
27
+ * @param {string} projectDir
28
+ */
29
+ function writeMergedPrismaSchema(coreDir, projectDir) {
30
+ const coreSchemaPath = join(coreDir, "prisma", "schema.prisma");
31
+ if (!existsSync(coreSchemaPath)) {
32
+ return;
33
+ }
34
+
35
+ let merged = readFileSync(coreSchemaPath, "utf8").trim();
36
+ const domainPath = join(projectDir, "prisma", "domain.prisma");
37
+
38
+ if (existsSync(domainPath)) {
39
+ const domainPart = readFileSync(domainPath, "utf8").trim();
40
+ if (domainPart) {
41
+ merged = `${merged}\n\n${domainPart}`;
42
+ }
43
+ }
44
+
45
+ const destSchema = join(projectDir, "prisma", "schema.prisma");
46
+ mkdirSync(dirname(destSchema), { recursive: true });
47
+ writeFileSync(destSchema, `${merged}\n`, "utf8");
48
+ console.log(" ✓ merged prisma/schema.prisma");
49
+ }
50
+
51
+ /**
52
+ * @param {import('./config.js').RepositoryConfig} repo
53
+ * @param {{ useRemote?: boolean }} [options]
54
+ * @returns {Promise<{ coreDir: string, cleanupDir: string }>}
55
+ */
56
+ async function resolveCoreDir(repo, options = {}) {
57
+ const packageRoot = getPackageRoot();
58
+ const localCore = resolveLocalCoreDir(packageRoot);
59
+
60
+ if (localCore && !options.useRemote) {
61
+ return { coreDir: localCore, cleanupDir: "" };
62
+ }
63
+
64
+ return fetchCoreModulesFromGitHub(repo);
65
+ }
66
+
67
+ /**
68
+ * Add optional core modules to an initialized client project.
69
+ * @param {string} projectDir
70
+ * @param {string[]} moduleIds
71
+ * @param {import('./config.js').RepositoryConfig} repo
72
+ * @param {{ useRemote?: boolean }} [options]
73
+ */
74
+ export async function runAddModule(projectDir, moduleIds, repo, options = {}) {
75
+ assertInitializedProject(projectDir);
76
+
77
+ if (moduleIds.length === 0) {
78
+ throw new Error("No module ids provided. Example: tempjs add-module seo,gallery");
79
+ }
80
+
81
+ const stamp = readProjectStamp(projectDir);
82
+ const displayName = stamp?.template
83
+ ? stamp.template.replace(/-website-template$/, "").replace(/-/g, " ")
84
+ : "Your Site";
85
+
86
+ const { coreDir, cleanupDir } = await resolveCoreDir(repo, {
87
+ useRemote: options.useRemote,
88
+ });
89
+
90
+ const installerPath = join(scriptsDir, "module-installer-core.mjs");
91
+ const { copyModulesIntoProject, listModuleIds, resolveModuleIds, parseModuleSpecs, moduleInstallOptionsFromSpecs } = await import(
92
+ pathToFileURL(installerPath).href
93
+ );
94
+
95
+ const specs = parseModuleSpecs(moduleIds.join(","));
96
+ const resolvedIds = specs.map((s) => s.id);
97
+ const available = listModuleIds(coreDir);
98
+ const resolved = resolveModuleIds(coreDir, resolvedIds);
99
+ const installOptions = moduleInstallOptionsFromSpecs(specs);
100
+
101
+ console.log(`Adding core modules to ${relative(process.cwd(), projectDir) || "."}:`);
102
+ console.log(` modules: ${resolved.join(", ")}`);
103
+
104
+ try {
105
+ const installed = copyModulesIntoProject(projectDir, resolved, {
106
+ coreDir,
107
+ displayName,
108
+ skipHomePage: true,
109
+ skipSeoMetadataReplace: true,
110
+ installOptions,
111
+ writePrismaSchema: () => writeMergedPrismaSchema(coreDir, projectDir),
112
+ });
113
+
114
+ const cmsModules = installed.filter((id) => id === "gallery" || id === "reviews");
115
+ if (cmsModules.length > 0) {
116
+ console.log(
117
+ ` Admin tabs: gallery/reviews wired on /admin via app/admin/registry.ts`
118
+ );
119
+ }
120
+
121
+ console.log(`\n✓ Installed: ${installed.join(", ")}`);
122
+ console.log("\nNext steps:");
123
+ console.log(" pnpm prisma db push");
124
+ console.log(" pnpm dev");
125
+ if (cmsModules.length > 0) {
126
+ console.log(" Open /admin — Gallery and Reviews tabs appear when SITE.features flags are true");
127
+ }
128
+ console.log(`\nAvailable modules: ${available.join(", ")}`);
129
+ } finally {
130
+ if (cleanupDir) {
131
+ await rm(cleanupDir, { recursive: true, force: true });
132
+ }
133
+ }
134
+ }
135
+
136
+ /**
137
+ * @param {import('./config.js').RepositoryConfig} repo
138
+ */
139
+ export async function printAvailableModules(repo) {
140
+ const packageRoot = getPackageRoot();
141
+ const localCore = resolveLocalCoreDir(packageRoot);
142
+
143
+ let coreDir = localCore;
144
+ let cleanupDir = "";
145
+
146
+ if (!coreDir) {
147
+ const fetched = await fetchCoreModulesFromGitHub(repo);
148
+ coreDir = fetched.coreDir;
149
+ cleanupDir = fetched.cleanupDir;
150
+ }
151
+
152
+ try {
153
+ const installerPath = join(scriptsDir, "module-installer-core.mjs");
154
+ const { listModuleIds, loadModulesRegistry } = await import(pathToFileURL(installerPath).href);
155
+ const registry = loadModulesRegistry(coreDir);
156
+ for (const id of listModuleIds(coreDir)) {
157
+ const mod = registry.modules[id];
158
+ console.log(` ${id.padEnd(16)} ${mod.label}`);
159
+ if (mod.description) {
160
+ console.log(` ${mod.description}`);
161
+ }
162
+ }
163
+ } finally {
164
+ if (cleanupDir) {
165
+ await rm(cleanupDir, { recursive: true, force: true });
166
+ }
167
+ }
168
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * @typedef {ReturnType<typeof createDefaultFlags>} CliFlags
3
+ */
4
+
5
+ export function createDefaultFlags() {
6
+ return {
7
+ force: false,
8
+ remote: false,
9
+ initGit: false,
10
+ help: false,
11
+ config: false,
12
+ yes: false,
13
+ theme: undefined,
14
+ font: undefined,
15
+ name: undefined,
16
+ shortName: undefined,
17
+ baseUrl: undefined,
18
+ phone: undefined,
19
+ phoneDisplay: undefined,
20
+ countryCode: undefined,
21
+ email: undefined,
22
+ address: undefined,
23
+ tagline: undefined,
24
+ developerName: undefined,
25
+ channelPartner: undefined,
26
+ agentRera: undefined,
27
+ templeDistance: undefined,
28
+ company: undefined,
29
+ location: undefined,
30
+ dbHost: undefined,
31
+ dbPort: undefined,
32
+ dbUser: undefined,
33
+ dbPassword: undefined,
34
+ dbName: undefined,
35
+ adminUser: undefined,
36
+ adminPassword: undefined,
37
+ skipDbPush: false,
38
+ dbPush: false,
39
+ updateCheck: false,
40
+ updateMerge: false,
41
+ };
42
+ }
43
+
44
+ const FLAG_MAP = {
45
+ "--force": { key: "force", type: "boolean" },
46
+ "-f": { key: "force", type: "boolean" },
47
+ "--remote": { key: "remote", type: "boolean" },
48
+ "--init-git": { key: "initGit", type: "boolean" },
49
+ "--help": { key: "help", type: "boolean" },
50
+ "-h": { key: "help", type: "boolean" },
51
+ "--config": { key: "config", type: "boolean" },
52
+ "--yes": { key: "yes", type: "boolean" },
53
+ "-y": { key: "yes", type: "boolean" },
54
+ "--no-prompt": { key: "yes", type: "boolean" },
55
+ "--skip-db-push": { key: "skipDbPush", type: "boolean" },
56
+ "--db-push": { key: "dbPush", type: "boolean" },
57
+ "--check": { key: "updateCheck", type: "boolean" },
58
+ "--merge": { key: "updateMerge", type: "boolean" },
59
+ "--theme": { key: "theme", type: "string" },
60
+ "--font": { key: "font", type: "string" },
61
+ "--name": { key: "name", type: "string" },
62
+ "--short-name": { key: "shortName", type: "string" },
63
+ "--base-url": { key: "baseUrl", type: "string" },
64
+ "--phone": { key: "phone", type: "string" },
65
+ "--phone-display": { key: "phoneDisplay", type: "string" },
66
+ "--country-code": { key: "countryCode", type: "string" },
67
+ "--email": { key: "email", type: "string" },
68
+ "--address": { key: "address", type: "string" },
69
+ "--tagline": { key: "tagline", type: "string" },
70
+ "--developer-name": { key: "developerName", type: "string" },
71
+ "--channel-partner": { key: "channelPartner", type: "string" },
72
+ "--agent-rera": { key: "agentRera", type: "string" },
73
+ "--temple-distance": { key: "templeDistance", type: "string" },
74
+ "--company": { key: "company", type: "string" },
75
+ "--location": { key: "location", type: "string" },
76
+ "--db-host": { key: "dbHost", type: "string" },
77
+ "--db-port": { key: "dbPort", type: "string" },
78
+ "--db-user": { key: "dbUser", type: "string" },
79
+ "--db-password": { key: "dbPassword", type: "string" },
80
+ "--db-name": { key: "dbName", type: "string" },
81
+ "--admin-user": { key: "adminUser", type: "string" },
82
+ "--admin-password": { key: "adminPassword", type: "string" },
83
+ };
84
+
85
+ /**
86
+ * @param {string[]} argv
87
+ */
88
+ export function parseArgs(argv) {
89
+ const flags = createDefaultFlags();
90
+ const positionals = [];
91
+
92
+ for (let i = 0; i < argv.length; i++) {
93
+ const arg = argv[i];
94
+
95
+ if (arg.includes("=") && arg.startsWith("--")) {
96
+ const eqIndex = arg.indexOf("=");
97
+ const flagName = arg.slice(0, eqIndex);
98
+ const value = arg.slice(eqIndex + 1);
99
+ applyFlag(flags, flagName, value);
100
+ continue;
101
+ }
102
+
103
+ const spec = FLAG_MAP[arg];
104
+ if (spec) {
105
+ if (spec.type === "boolean") {
106
+ flags[spec.key] = true;
107
+ } else {
108
+ const value = argv[++i];
109
+ if (!value || value.startsWith("-")) {
110
+ throw new Error(`Option ${arg} requires a value`);
111
+ }
112
+ flags[spec.key] = value;
113
+ }
114
+ continue;
115
+ }
116
+
117
+ if (arg.startsWith("-")) {
118
+ throw new Error(`Unknown option: ${arg}`);
119
+ }
120
+
121
+ positionals.push(arg);
122
+ }
123
+
124
+ return { flags, positionals };
125
+ }
126
+
127
+ /**
128
+ * @param {CliFlags} flags
129
+ * @param {string} flagName
130
+ * @param {string} value
131
+ */
132
+ function applyFlag(flags, flagName, value) {
133
+ const spec = FLAG_MAP[flagName];
134
+ if (!spec) {
135
+ throw new Error(`Unknown option: ${flagName}`);
136
+ }
137
+ if (spec.type === "boolean") {
138
+ flags[spec.key] = value === "true" || value === "1";
139
+ } else {
140
+ flags[spec.key] = value;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * @param {CliFlags} flags
146
+ */
147
+ export function toCliOptions(flags) {
148
+ return {
149
+ yes: flags.yes,
150
+ theme: flags.theme,
151
+ font: flags.font,
152
+ name: flags.name,
153
+ shortName: flags.shortName,
154
+ baseUrl: flags.baseUrl,
155
+ phone: flags.phone,
156
+ phoneDisplay: flags.phoneDisplay,
157
+ countryCode: flags.countryCode,
158
+ email: flags.email,
159
+ address: flags.address,
160
+ tagline: flags.tagline,
161
+ developerName: flags.developerName,
162
+ channelPartner: flags.channelPartner,
163
+ agentRera: flags.agentRera,
164
+ templeDistance: flags.templeDistance,
165
+ company: flags.company,
166
+ location: flags.location,
167
+ dbHost: flags.dbHost,
168
+ dbPort: flags.dbPort,
169
+ dbUser: flags.dbUser,
170
+ dbPassword: flags.dbPassword,
171
+ dbName: flags.dbName,
172
+ adminUser: flags.adminUser,
173
+ adminPassword: flags.adminPassword,
174
+ skipDbPush: flags.skipDbPush,
175
+ dbPush: flags.dbPush,
176
+ };
177
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @param {number} bytes
3
+ * @returns {string}
4
+ */
5
+ export function formatBytes(bytes) {
6
+ if (bytes < 1024) return `${bytes} B`;
7
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
8
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
9
+ }
10
+
11
+ /**
12
+ * @param {number} ms
13
+ * @returns {string}
14
+ */
15
+ export function formatDuration(ms) {
16
+ if (ms < 1000) return `${Math.round(ms)}ms`;
17
+ return `${(ms / 1000).toFixed(1)}s`;
18
+ }
19
+
20
+ /**
21
+ * @param {{
22
+ * templateId: string,
23
+ * stats: { bytes: number, files: number, durationMs: number, source?: string }
24
+ * }} options
25
+ */
26
+ export function printFetchComplete(options) {
27
+ const { templateId, stats } = options;
28
+ const sourceLabel = stats.source === "local" ? "local copy" : "download";
29
+ const sizePart = stats.bytes > 0 ? `${formatBytes(stats.bytes)}, ` : "";
30
+ console.log(
31
+ `Fetching template "${templateId}"... done (${sizePart}${stats.files} files, ${formatDuration(stats.durationMs)}, ${sourceLabel})`
32
+ );
33
+ }
@@ -0,0 +1,69 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { collectFileHashes } from "./file-tree.js";
5
+
6
+ export const STAMP_FILENAME = ".tempjs.json";
7
+
8
+ /**
9
+ * @typedef {{
10
+ * template: string,
11
+ * templateVersion: string,
12
+ * templateDirectory: string,
13
+ * generatedAt: string,
14
+ * updatedAt?: string,
15
+ * repository?: string,
16
+ * branch?: string,
17
+ * fileHashes: Record<string, string>
18
+ * }} ProjectStamp
19
+ */
20
+
21
+ /**
22
+ * @param {string} projectDir
23
+ * @returns {ProjectStamp | null}
24
+ */
25
+ export function readProjectStamp(projectDir) {
26
+ const stampPath = join(projectDir, STAMP_FILENAME);
27
+ if (!existsSync(stampPath)) return null;
28
+
29
+ try {
30
+ return JSON.parse(readFileSync(stampPath, "utf8"));
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * @param {string} projectDir
38
+ * @param {{
39
+ * templateId: string,
40
+ * templateVersion: string,
41
+ * templateDirectory: string,
42
+ * repository?: string,
43
+ * branch?: string,
44
+ * sourceDir: string,
45
+ * isUpdate?: boolean
46
+ * }} options
47
+ */
48
+ export async function writeProjectStamp(projectDir, options) {
49
+ const existing = readProjectStamp(projectDir);
50
+ const fileHashes = collectFileHashes(options.sourceDir);
51
+
52
+ /** @type {ProjectStamp} */
53
+ const stamp = {
54
+ template: options.templateId,
55
+ templateVersion: options.templateVersion,
56
+ templateDirectory: options.templateDirectory,
57
+ generatedAt: existing?.generatedAt ?? new Date().toISOString(),
58
+ updatedAt: options.isUpdate ? new Date().toISOString() : existing?.updatedAt,
59
+ repository: options.repository,
60
+ branch: options.branch,
61
+ fileHashes,
62
+ };
63
+
64
+ if (options.isUpdate) {
65
+ stamp.updatedAt = new Date().toISOString();
66
+ }
67
+
68
+ await writeFile(join(projectDir, STAMP_FILENAME), JSON.stringify(stamp, null, 2) + "\n", "utf8");
69
+ }
package/cli/prompt.js ADDED
@@ -0,0 +1,19 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+
4
+ /**
5
+ * @param {string} message
6
+ * @param {boolean} autoConfirm
7
+ */
8
+ export async function confirmYesNo(message, autoConfirm = false) {
9
+ if (autoConfirm) return true;
10
+
11
+ const rl = createInterface({ input, output });
12
+ try {
13
+ const answer = await rl.question(message);
14
+ const trimmed = answer.trim().toLowerCase();
15
+ return trimmed === "y" || trimmed === "yes";
16
+ } finally {
17
+ rl.close();
18
+ }
19
+ }