@navneet_25/tempjs 1.0.4 → 2.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 +752 -33
- package/cli/brand-manager.js +220 -0
- package/cli/config.js +14 -1
- package/cli/copy.js +4 -18
- package/cli/db-setup.js +241 -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 +144 -98
- package/cli/info.js +88 -0
- package/cli/parse-args.js +156 -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 +33 -3
- package/cli/update.js +212 -0
- package/package.json +6 -2
- package/templates.json +33 -3
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
dbHost: undefined,
|
|
24
|
+
dbPort: undefined,
|
|
25
|
+
dbUser: undefined,
|
|
26
|
+
dbPassword: undefined,
|
|
27
|
+
dbName: undefined,
|
|
28
|
+
adminUser: undefined,
|
|
29
|
+
adminPassword: undefined,
|
|
30
|
+
skipDbPush: false,
|
|
31
|
+
dbPush: false,
|
|
32
|
+
updateCheck: false,
|
|
33
|
+
updateMerge: false,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const FLAG_MAP = {
|
|
38
|
+
"--force": { key: "force", type: "boolean" },
|
|
39
|
+
"-f": { key: "force", type: "boolean" },
|
|
40
|
+
"--remote": { key: "remote", type: "boolean" },
|
|
41
|
+
"--init-git": { key: "initGit", type: "boolean" },
|
|
42
|
+
"--help": { key: "help", type: "boolean" },
|
|
43
|
+
"-h": { key: "help", type: "boolean" },
|
|
44
|
+
"--config": { key: "config", type: "boolean" },
|
|
45
|
+
"--yes": { key: "yes", type: "boolean" },
|
|
46
|
+
"-y": { key: "yes", type: "boolean" },
|
|
47
|
+
"--no-prompt": { key: "yes", type: "boolean" },
|
|
48
|
+
"--skip-db-push": { key: "skipDbPush", type: "boolean" },
|
|
49
|
+
"--db-push": { key: "dbPush", type: "boolean" },
|
|
50
|
+
"--check": { key: "updateCheck", type: "boolean" },
|
|
51
|
+
"--merge": { key: "updateMerge", type: "boolean" },
|
|
52
|
+
"--theme": { key: "theme", type: "string" },
|
|
53
|
+
"--font": { key: "font", type: "string" },
|
|
54
|
+
"--name": { key: "name", type: "string" },
|
|
55
|
+
"--short-name": { key: "shortName", type: "string" },
|
|
56
|
+
"--base-url": { key: "baseUrl", type: "string" },
|
|
57
|
+
"--phone": { key: "phone", type: "string" },
|
|
58
|
+
"--phone-display": { key: "phoneDisplay", type: "string" },
|
|
59
|
+
"--country-code": { key: "countryCode", type: "string" },
|
|
60
|
+
"--email": { key: "email", type: "string" },
|
|
61
|
+
"--address": { key: "address", type: "string" },
|
|
62
|
+
"--db-host": { key: "dbHost", type: "string" },
|
|
63
|
+
"--db-port": { key: "dbPort", type: "string" },
|
|
64
|
+
"--db-user": { key: "dbUser", type: "string" },
|
|
65
|
+
"--db-password": { key: "dbPassword", type: "string" },
|
|
66
|
+
"--db-name": { key: "dbName", type: "string" },
|
|
67
|
+
"--admin-user": { key: "adminUser", type: "string" },
|
|
68
|
+
"--admin-password": { key: "adminPassword", type: "string" },
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {string[]} argv
|
|
73
|
+
*/
|
|
74
|
+
export function parseArgs(argv) {
|
|
75
|
+
const flags = createDefaultFlags();
|
|
76
|
+
const positionals = [];
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < argv.length; i++) {
|
|
79
|
+
const arg = argv[i];
|
|
80
|
+
|
|
81
|
+
if (arg.includes("=") && arg.startsWith("--")) {
|
|
82
|
+
const eqIndex = arg.indexOf("=");
|
|
83
|
+
const flagName = arg.slice(0, eqIndex);
|
|
84
|
+
const value = arg.slice(eqIndex + 1);
|
|
85
|
+
applyFlag(flags, flagName, value);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const spec = FLAG_MAP[arg];
|
|
90
|
+
if (spec) {
|
|
91
|
+
if (spec.type === "boolean") {
|
|
92
|
+
flags[spec.key] = true;
|
|
93
|
+
} else {
|
|
94
|
+
const value = argv[++i];
|
|
95
|
+
if (!value || value.startsWith("-")) {
|
|
96
|
+
throw new Error(`Option ${arg} requires a value`);
|
|
97
|
+
}
|
|
98
|
+
flags[spec.key] = value;
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (arg.startsWith("-")) {
|
|
104
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
positionals.push(arg);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { flags, positionals };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {CliFlags} flags
|
|
115
|
+
* @param {string} flagName
|
|
116
|
+
* @param {string} value
|
|
117
|
+
*/
|
|
118
|
+
function applyFlag(flags, flagName, value) {
|
|
119
|
+
const spec = FLAG_MAP[flagName];
|
|
120
|
+
if (!spec) {
|
|
121
|
+
throw new Error(`Unknown option: ${flagName}`);
|
|
122
|
+
}
|
|
123
|
+
if (spec.type === "boolean") {
|
|
124
|
+
flags[spec.key] = value === "true" || value === "1";
|
|
125
|
+
} else {
|
|
126
|
+
flags[spec.key] = value;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {CliFlags} flags
|
|
132
|
+
*/
|
|
133
|
+
export function toCliOptions(flags) {
|
|
134
|
+
return {
|
|
135
|
+
yes: flags.yes,
|
|
136
|
+
theme: flags.theme,
|
|
137
|
+
font: flags.font,
|
|
138
|
+
name: flags.name,
|
|
139
|
+
shortName: flags.shortName,
|
|
140
|
+
baseUrl: flags.baseUrl,
|
|
141
|
+
phone: flags.phone,
|
|
142
|
+
phoneDisplay: flags.phoneDisplay,
|
|
143
|
+
countryCode: flags.countryCode,
|
|
144
|
+
email: flags.email,
|
|
145
|
+
address: flags.address,
|
|
146
|
+
dbHost: flags.dbHost,
|
|
147
|
+
dbPort: flags.dbPort,
|
|
148
|
+
dbUser: flags.dbUser,
|
|
149
|
+
dbPassword: flags.dbPassword,
|
|
150
|
+
dbName: flags.dbName,
|
|
151
|
+
adminUser: flags.adminUser,
|
|
152
|
+
adminPassword: flags.adminPassword,
|
|
153
|
+
skipDbPush: flags.skipDbPush,
|
|
154
|
+
dbPush: flags.dbPush,
|
|
155
|
+
};
|
|
156
|
+
}
|
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
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { removeDirectory } from "./copy.js";
|
|
2
|
+
import { fetchTemplateFromGitHub, loadLocalTemplate } from "./fetch.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {{
|
|
6
|
+
* templateRoot: string,
|
|
7
|
+
* cleanupDir: string,
|
|
8
|
+
* stats: import('./fetch.js').FetchStats,
|
|
9
|
+
* release: () => void
|
|
10
|
+
* }} ResolvedTemplate
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve template source from local package or remote GitHub tarball.
|
|
15
|
+
* @param {{
|
|
16
|
+
* repo: import('./config.js').RepositoryConfig,
|
|
17
|
+
* packageRoot: string,
|
|
18
|
+
* templateDirectory: string,
|
|
19
|
+
* useRemote: boolean
|
|
20
|
+
* }} options
|
|
21
|
+
* @returns {Promise<ResolvedTemplate>}
|
|
22
|
+
*/
|
|
23
|
+
export async function resolveTemplateSource(options) {
|
|
24
|
+
const { repo, packageRoot, templateDirectory, useRemote } = options;
|
|
25
|
+
|
|
26
|
+
let result = null;
|
|
27
|
+
|
|
28
|
+
if (!useRemote) {
|
|
29
|
+
result = await loadLocalTemplate(packageRoot, repo.templatesPath, templateDirectory);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!result) {
|
|
33
|
+
result = await fetchTemplateFromGitHub(repo, templateDirectory);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const cleanupDir = result.cleanupDir;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
templateRoot: result.templateRoot,
|
|
40
|
+
cleanupDir,
|
|
41
|
+
stats: result.stats,
|
|
42
|
+
release: () => {
|
|
43
|
+
if (cleanupDir) {
|
|
44
|
+
removeDirectory(cleanupDir);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
package/cli/theme-manager.js
CHANGED
|
@@ -152,6 +152,26 @@ export function getSavedConfig(targetDir) {
|
|
|
152
152
|
return {};
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
export function resolveThemeId(id) {
|
|
156
|
+
const theme = THEMES.find((t) => t.id === id);
|
|
157
|
+
if (!theme) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Unknown theme: ${id}. Valid options: ${THEMES.map((t) => t.id).join(", ")}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return theme.id;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function resolveFontId(id) {
|
|
166
|
+
const font = FONTS.find((f) => f.id === id);
|
|
167
|
+
if (!font) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`Unknown font: ${id}. Valid options: ${FONTS.map((f) => f.id).join(", ")}`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return font.id;
|
|
173
|
+
}
|
|
174
|
+
|
|
155
175
|
async function promptSelection(options, promptText, defaultValue) {
|
|
156
176
|
const rl = createInterface({ input, output });
|
|
157
177
|
try {
|
|
@@ -179,11 +199,17 @@ async function promptSelection(options, promptText, defaultValue) {
|
|
|
179
199
|
}
|
|
180
200
|
}
|
|
181
201
|
|
|
182
|
-
export async function promptTheme(currentThemeId = "theme1") {
|
|
202
|
+
export async function promptTheme(currentThemeId = "theme1", options = {}) {
|
|
203
|
+
if (options.yes || options.theme) {
|
|
204
|
+
return resolveThemeId(options.theme || currentThemeId || "theme1");
|
|
205
|
+
}
|
|
183
206
|
return promptSelection(THEMES, "Available Themes:", currentThemeId);
|
|
184
207
|
}
|
|
185
208
|
|
|
186
|
-
export async function promptFont(currentFontId = "default") {
|
|
209
|
+
export async function promptFont(currentFontId = "default", options = {}) {
|
|
210
|
+
if (options.yes || options.font) {
|
|
211
|
+
return resolveFontId(options.font || currentFontId || "default");
|
|
212
|
+
}
|
|
187
213
|
return promptSelection(FONTS, "Available Font combinations:", currentFontId);
|
|
188
214
|
}
|
|
189
215
|
|
|
@@ -316,8 +342,12 @@ export async function applyThemeAndFont(targetDir, themeId, fontId) {
|
|
|
316
342
|
if (!globalsContent.includes("tempjs-theme.css")) {
|
|
317
343
|
// Prepend import at the top of the file
|
|
318
344
|
globalsContent = `@import "./tempjs-theme.css";\n` + globalsContent;
|
|
319
|
-
await writeFile(globalsPath, globalsContent, "utf8");
|
|
320
345
|
}
|
|
346
|
+
|
|
347
|
+
// Always append/update a timestamp touch comment to force bundler/Tailwind CSS recompilation
|
|
348
|
+
globalsContent = globalsContent.replace(/\/\* tempjs-touch: \d+ \*\/\s*$/, "");
|
|
349
|
+
globalsContent = globalsContent.trim() + `\n/* tempjs-touch: ${Date.now()} */\n`;
|
|
350
|
+
await writeFile(globalsPath, globalsContent, "utf8");
|
|
321
351
|
} else {
|
|
322
352
|
console.warn("Could not locate globals.css file in the project. CSS variables and font imports were not applied.");
|
|
323
353
|
}
|
package/cli/update.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { cpSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { loadManifest, resolveRepositoryConfig, getPackageRoot } from "./config.js";
|
|
4
|
+
import { isUpdateProtected, shouldSkipFileName } from "./fs-ignore.js";
|
|
5
|
+
import {
|
|
6
|
+
collectFileHashes,
|
|
7
|
+
diffTemplateTrees,
|
|
8
|
+
hashFileContents,
|
|
9
|
+
} from "./file-tree.js";
|
|
10
|
+
import { readProjectStamp, writeProjectStamp } from "./project-stamp.js";
|
|
11
|
+
import { confirmYesNo } from "./prompt.js";
|
|
12
|
+
import { resolveTemplateSource } from "./template-resolver.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {import('./file-tree.js').UpdateDiff} diff
|
|
16
|
+
* @param {{
|
|
17
|
+
* projectVersion: string,
|
|
18
|
+
* latestVersion: string,
|
|
19
|
+
* templateName: string
|
|
20
|
+
* }} meta
|
|
21
|
+
*/
|
|
22
|
+
export function printUpdateReport(diff, meta) {
|
|
23
|
+
console.log(`\nTemplate update report: ${meta.templateName}`);
|
|
24
|
+
console.log(` Project version: ${meta.projectVersion}`);
|
|
25
|
+
console.log(` Latest version: ${meta.latestVersion}`);
|
|
26
|
+
console.log("");
|
|
27
|
+
|
|
28
|
+
if (
|
|
29
|
+
diff.newFiles.length === 0 &&
|
|
30
|
+
diff.safeUpdates.length === 0 &&
|
|
31
|
+
diff.conflicts.length === 0 &&
|
|
32
|
+
diff.removedFromTemplate.length === 0
|
|
33
|
+
) {
|
|
34
|
+
console.log("Your project is up to date with the latest template.");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (diff.newFiles.length > 0) {
|
|
39
|
+
console.log(`New files in template (${diff.newFiles.length}):`);
|
|
40
|
+
diff.newFiles.slice(0, 25).forEach((p) => console.log(` + ${p}`));
|
|
41
|
+
if (diff.newFiles.length > 25) {
|
|
42
|
+
console.log(` ... and ${diff.newFiles.length - 25} more`);
|
|
43
|
+
}
|
|
44
|
+
console.log("");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (diff.safeUpdates.length > 0) {
|
|
48
|
+
console.log(`Safe updates (${diff.safeUpdates.length}) — template changed, you did not edit:`);
|
|
49
|
+
diff.safeUpdates.slice(0, 25).forEach((p) => console.log(` ~ ${p}`));
|
|
50
|
+
if (diff.safeUpdates.length > 25) {
|
|
51
|
+
console.log(` ... and ${diff.safeUpdates.length - 25} more`);
|
|
52
|
+
}
|
|
53
|
+
console.log("");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (diff.conflicts.length > 0) {
|
|
57
|
+
console.log(`Conflicts (${diff.conflicts.length}) — you modified these files:`);
|
|
58
|
+
diff.conflicts.slice(0, 25).forEach((p) => console.log(` ! ${p}`));
|
|
59
|
+
if (diff.conflicts.length > 25) {
|
|
60
|
+
console.log(` ... and ${diff.conflicts.length - 25} more`);
|
|
61
|
+
}
|
|
62
|
+
console.log("");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (diff.removedFromTemplate.length > 0) {
|
|
66
|
+
console.log(`Removed from template (${diff.removedFromTemplate.length}) — not deleted locally:`);
|
|
67
|
+
diff.removedFromTemplate.slice(0, 15).forEach((p) => console.log(` - ${p}`));
|
|
68
|
+
if (diff.removedFromTemplate.length > 15) {
|
|
69
|
+
console.log(` ... and ${diff.removedFromTemplate.length - 15} more`);
|
|
70
|
+
}
|
|
71
|
+
console.log("");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (diff.upToDate.length > 0) {
|
|
75
|
+
console.log(`${diff.upToDate.length} file(s) already match the latest template.\n`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {string} sourcePath
|
|
81
|
+
* @param {string} targetPath
|
|
82
|
+
*/
|
|
83
|
+
export function copyTemplateFile(sourcePath, targetPath) {
|
|
84
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
85
|
+
cpSync(sourcePath, targetPath, { force: true });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {string} projectDir
|
|
90
|
+
* @param {import('./parse-args.js').CliFlags} flags
|
|
91
|
+
* @param {{ checkOnly: boolean }} mode
|
|
92
|
+
*/
|
|
93
|
+
export async function runUpdate(projectDir, flags, mode) {
|
|
94
|
+
const stamp = readProjectStamp(projectDir);
|
|
95
|
+
|
|
96
|
+
if (!stamp) {
|
|
97
|
+
console.error("No .tempjs.json found in this directory.");
|
|
98
|
+
console.error("Run `tempjs <template-id>` here first, or this project was not created with tempjs.");
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const manifest = loadManifest();
|
|
104
|
+
const entry = manifest.templates[stamp.template];
|
|
105
|
+
if (!entry) {
|
|
106
|
+
console.error(`Unknown template in .tempjs.json: ${stamp.template}`);
|
|
107
|
+
process.exitCode = 1;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const repo = resolveRepositoryConfig(manifest.repository);
|
|
112
|
+
const packageRoot = getPackageRoot();
|
|
113
|
+
const useRemote =
|
|
114
|
+
flags.remote ||
|
|
115
|
+
process.env.TEMPLATE_USE_REMOTE === "1" ||
|
|
116
|
+
process.env.TEMPLATE_USE_REMOTE === "true";
|
|
117
|
+
|
|
118
|
+
const latestVersion = entry.version ?? "0.0.0";
|
|
119
|
+
let resolved = null;
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
resolved = await resolveTemplateSource({
|
|
123
|
+
repo,
|
|
124
|
+
packageRoot,
|
|
125
|
+
templateDirectory: entry.directory,
|
|
126
|
+
useRemote,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const latestHashes = collectFileHashes(resolved.templateRoot);
|
|
130
|
+
const baselineHashes = stamp.fileHashes ?? {};
|
|
131
|
+
const currentHashes = {};
|
|
132
|
+
|
|
133
|
+
for (const path of Object.keys(latestHashes)) {
|
|
134
|
+
const projectPath = join(projectDir, path);
|
|
135
|
+
if (existsSync(projectPath) && !shouldSkipFileName(path.split("/").pop() ?? "")) {
|
|
136
|
+
currentHashes[path] = hashFileContents(projectPath);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const diff = diffTemplateTrees(baselineHashes, currentHashes, latestHashes);
|
|
141
|
+
|
|
142
|
+
printUpdateReport(diff, {
|
|
143
|
+
projectVersion: stamp.templateVersion,
|
|
144
|
+
latestVersion,
|
|
145
|
+
templateName: entry.name,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (mode.checkOnly) {
|
|
149
|
+
if (stamp.templateVersion !== latestVersion) {
|
|
150
|
+
console.log("Run `tempjs update --merge` to apply non-conflicting template updates.");
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const pathsToApply = [...diff.newFiles, ...diff.safeUpdates].filter(
|
|
156
|
+
(path) => !isUpdateProtected(path)
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const skippedProtected = [...diff.newFiles, ...diff.safeUpdates].filter((path) =>
|
|
160
|
+
isUpdateProtected(path)
|
|
161
|
+
);
|
|
162
|
+
if (skippedProtected.length > 0) {
|
|
163
|
+
console.log(`Skipping ${skippedProtected.length} protected path(s) (brand, .env, etc.).`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (pathsToApply.length === 0) {
|
|
167
|
+
if (diff.conflicts.length > 0) {
|
|
168
|
+
console.log("\nNo safe updates to apply. Resolve conflicts manually.");
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const confirmed = await confirmYesNo(
|
|
174
|
+
`\nApply ${pathsToApply.length} non-conflicting update(s)? [y/N] `,
|
|
175
|
+
flags.yes
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (!confirmed) {
|
|
179
|
+
console.log("Aborted.");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const relPath of pathsToApply) {
|
|
184
|
+
const from = join(resolved.templateRoot, relPath);
|
|
185
|
+
const to = join(projectDir, relPath);
|
|
186
|
+
copyTemplateFile(from, to);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await writeProjectStamp(projectDir, {
|
|
190
|
+
templateId: stamp.template,
|
|
191
|
+
templateVersion: latestVersion,
|
|
192
|
+
templateDirectory: entry.directory,
|
|
193
|
+
repository: `${repo.owner}/${repo.repo}`,
|
|
194
|
+
branch: repo.branch,
|
|
195
|
+
sourceDir: resolved.templateRoot,
|
|
196
|
+
isUpdate: true,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
console.log(`\nApplied ${pathsToApply.length} update(s). Project stamped at template v${latestVersion}.`);
|
|
200
|
+
|
|
201
|
+
if (diff.conflicts.length > 0) {
|
|
202
|
+
console.log(`${diff.conflicts.length} conflict(s) still need manual review.`);
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
206
|
+
process.exitCode = 1;
|
|
207
|
+
} finally {
|
|
208
|
+
if (resolved) {
|
|
209
|
+
resolved.release();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@navneet_25/tempjs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "CLI to instantiate website project templates from a single GitHub repository",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,5 +20,9 @@
|
|
|
20
20
|
"website",
|
|
21
21
|
"nextjs"
|
|
22
22
|
],
|
|
23
|
-
"license": "MIT"
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"sync-templates": "node scripts/sync-templates.mjs",
|
|
26
|
+
"sync-templates:check": "node scripts/sync-templates.mjs --check"
|
|
27
|
+
}
|
|
24
28
|
}
|
package/templates.json
CHANGED
|
@@ -9,12 +9,42 @@
|
|
|
9
9
|
"hotel": {
|
|
10
10
|
"directory": "hotel-website-template",
|
|
11
11
|
"name": "Hotel Website",
|
|
12
|
-
"description": "Modern hotel and resort website with admin panel, gallery, and booking features"
|
|
12
|
+
"description": "Modern hotel and resort website with admin panel, gallery, and booking features",
|
|
13
|
+
"version": "1.2.0",
|
|
14
|
+
"stack": ["Next.js 16", "React 19", "TypeScript", "Tailwind CSS 4", "Prisma", "MariaDB", "NextAuth"],
|
|
15
|
+
"packageManager": "pnpm",
|
|
16
|
+
"node": ">=20",
|
|
17
|
+
"setupTime": "~10 min",
|
|
18
|
+
"docker": true,
|
|
19
|
+
"tags": ["admin", "cms", "gallery", "booking", "leads"],
|
|
20
|
+
"features": [
|
|
21
|
+
"Admin dashboard for rooms, facilities, reviews, and leads",
|
|
22
|
+
"Gallery and promo banner management",
|
|
23
|
+
"FTP asset upload pipeline",
|
|
24
|
+
"LeadRat CRM integration",
|
|
25
|
+
"Docker Compose for local MariaDB"
|
|
26
|
+
],
|
|
27
|
+
"docs": "DEVELOPER_GUIDE.md"
|
|
13
28
|
},
|
|
14
29
|
"real-estate": {
|
|
15
30
|
"directory": "real-estate-website-template",
|
|
16
31
|
"name": "Real Estate Website",
|
|
17
|
-
"description": "Real estate and property listing website with admin panel and property management"
|
|
32
|
+
"description": "Real estate and property listing website with admin panel and property management",
|
|
33
|
+
"version": "1.2.0",
|
|
34
|
+
"stack": ["Next.js 16", "React 19", "TypeScript", "Tailwind CSS 4", "Prisma", "MariaDB", "NextAuth"],
|
|
35
|
+
"packageManager": "pnpm",
|
|
36
|
+
"node": ">=20",
|
|
37
|
+
"setupTime": "~10 min",
|
|
38
|
+
"docker": true,
|
|
39
|
+
"tags": ["admin", "cms", "properties", "leads"],
|
|
40
|
+
"features": [
|
|
41
|
+
"Admin dashboard for property listings and leads",
|
|
42
|
+
"Dynamic project pages with slug routing",
|
|
43
|
+
"FTP asset upload pipeline",
|
|
44
|
+
"LeadRat CRM integration",
|
|
45
|
+
"Docker Compose for local MariaDB"
|
|
46
|
+
],
|
|
47
|
+
"docs": "DEVELOPER_GUIDE.md"
|
|
18
48
|
}
|
|
19
49
|
}
|
|
20
|
-
}
|
|
50
|
+
}
|