@navneet_25/tempjs 1.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +800 -35
- package/cli/brand-manager.js +277 -78
- package/cli/config.js +22 -1
- package/cli/copy.js +4 -18
- package/cli/db-setup.js +170 -49
- package/cli/doctor.js +393 -0
- package/cli/fetch-core-modules.js +130 -0
- package/cli/fetch.js +47 -29
- package/cli/file-tree.js +113 -0
- package/cli/fs-ignore.js +60 -0
- package/cli/index.js +274 -112
- package/cli/info.js +115 -0
- package/cli/init-options.js +209 -0
- package/cli/module-manager.js +168 -0
- package/cli/parse-args.js +177 -0
- package/cli/progress.js +33 -0
- package/cli/project-stamp.js +69 -0
- package/cli/prompt.js +19 -0
- package/cli/template-resolver.js +48 -0
- package/cli/theme-manager.js +28 -2
- package/cli/update.js +227 -0
- package/cli/version-manager.js +518 -0
- package/package.json +31 -4
- package/packages/core/modules/README.md +72 -0
- package/packages/core/modules/blog-compose/app/admin/components/blog-compose-form.ts +44 -0
- package/packages/core/modules/blog-compose/app/admin/hooks/useBlogPosts.ts +103 -0
- package/packages/core/modules/blog-compose/app/admin/panels/BlogComposePanel.tsx +388 -0
- package/packages/core/modules/blog-compose/app/api/blog-posts/[id]/route.ts +29 -0
- package/packages/core/modules/blog-compose/app/api/blog-posts/route.ts +21 -0
- package/packages/core/modules/blog-compose/app/blog/[slug]/page.tsx +79 -0
- package/packages/core/modules/blog-compose/app/blog/layout.tsx +22 -0
- package/packages/core/modules/blog-compose/app/blog/page.tsx +102 -0
- package/packages/core/modules/blog-compose/app/components/BlogSection.tsx +74 -0
- package/packages/core/modules/blog-compose/app/components/BlogShell.tsx +36 -0
- package/packages/core/modules/blog-compose/app/components/BlogSidebar.tsx +112 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/annotate-text.tsx +103 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/blocks/media-blocks.tsx +97 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/blocks/text-blocks.tsx +115 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/index.ts +21 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/interpreter.tsx +55 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/registry.ts +276 -0
- package/packages/core/modules/blog-compose/lib/blog/compose/types.ts +128 -0
- package/packages/core/modules/blog-compose/lib/blog/register-sitemap.ts +18 -0
- package/packages/core/modules/blog-compose/lib/controllers/BlogComposeController.ts +2 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.controller.ts +78 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.repository.ts +78 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.service.ts +72 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/blog-compose.types.ts +20 -0
- package/packages/core/modules/blog-compose/lib/features/blog-compose/index.ts +7 -0
- package/packages/core/modules/blog-compose/prisma/blog-compose.prisma +13 -0
- package/packages/core/modules/enquiry-modal/app/components/EnquiryModal.tsx +308 -0
- package/packages/core/modules/footer/app/components/Footer.tsx +127 -0
- package/packages/core/modules/gallery/app/admin/components/GalleryFormModal.tsx +132 -0
- package/packages/core/modules/gallery/app/admin/components/GalleryList.tsx +142 -0
- package/packages/core/modules/gallery/app/admin/hooks/useGallery.ts +72 -0
- package/packages/core/modules/gallery/app/api/gallery/[id]/route.ts +29 -0
- package/packages/core/modules/gallery/app/api/gallery/route.ts +22 -0
- package/packages/core/modules/gallery/app/components/GallerySection.tsx +92 -0
- package/packages/core/modules/gallery/app/gallery/layout.tsx +12 -0
- package/packages/core/modules/gallery/app/gallery/page.tsx +179 -0
- package/packages/core/modules/gallery/lib/controllers/GalleryController.ts +2 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.controller.ts +78 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.repository.ts +55 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.service.ts +43 -0
- package/packages/core/modules/gallery/lib/features/gallery/gallery.types.ts +17 -0
- package/packages/core/modules/gallery/lib/features/gallery/index.ts +4 -0
- package/packages/core/modules/gallery/prisma/gallery.prisma +11 -0
- package/packages/core/modules/hero-simple/app/components/Hero.tsx +160 -0
- package/packages/core/modules/legal-pages/app/components/LegalDocumentPage.tsx +61 -0
- package/packages/core/modules/legal-pages/app/privacy-policy/page.tsx +19 -0
- package/packages/core/modules/legal-pages/app/terms-and-conditions/page.tsx +19 -0
- package/packages/core/modules/reviews/app/admin/components/ReviewFormModal.tsx +110 -0
- package/packages/core/modules/reviews/app/admin/components/ReviewsList.tsx +116 -0
- package/packages/core/modules/reviews/app/admin/hooks/useReviews.ts +70 -0
- package/packages/core/modules/reviews/app/api/reviews/[id]/route.ts +29 -0
- package/packages/core/modules/reviews/app/api/reviews/route.ts +17 -0
- package/packages/core/modules/reviews/app/components/ReviewsSection.tsx +100 -0
- package/packages/core/modules/reviews/lib/controllers/ReviewController.ts +2 -0
- package/packages/core/modules/reviews/lib/features/reviews/index.ts +3 -0
- package/packages/core/modules/reviews/lib/features/reviews/review.controller.ts +64 -0
- package/packages/core/modules/reviews/lib/features/reviews/review.repository.ts +50 -0
- package/packages/core/modules/reviews/lib/features/reviews/review.service.ts +38 -0
- package/packages/core/modules/reviews/prisma/reviews.prisma +9 -0
- package/packages/core/modules/seo/app/components/SiteJsonLd.tsx +16 -0
- package/packages/core/modules/seo/app/robots.ts +30 -0
- package/packages/core/modules/seo/app/sitemap.ts +9 -0
- package/packages/core/modules/seo/lib/seo/index.ts +5 -0
- package/packages/core/modules/seo/lib/seo/json-ld.ts +128 -0
- package/packages/core/modules/seo/lib/seo/metadata.ts +65 -0
- package/packages/core/modules/seo/lib/seo/sitemap.ts +82 -0
- package/packages/core/modules/seo/lib/seo/types.ts +30 -0
- package/packages/core/modules/seo/lib/seo/urls.ts +27 -0
- package/packages/core/modules/theme-modes/app/components/ThemeModeInit.tsx +24 -0
- package/packages/core/modules/theme-modes/app/components/ThemeModeToggle.tsx +58 -0
- package/packages/core/modules.json +140 -0
- package/packages/core/prisma/schema.prisma +33 -0
- package/scripts/module-installer-core.mjs +657 -0
- package/scripts/template-modules-admin.mjs +366 -0
- package/scripts/template-modules-home.mjs +120 -0
- package/templates.json +188 -3
|
@@ -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
|
+
}
|
package/cli/progress.js
ADDED
|
@@ -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
|
+
}
|