@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
package/cli/file-tree.js
ADDED
|
@@ -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
|
+
}
|
package/cli/fs-ignore.js
ADDED
|
@@ -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
|
+
}
|
package/cli/index.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
|
-
import { createInterface } from "node:readline/promises";
|
|
5
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
6
4
|
import { loadManifest, getPackageRoot, resolveRepositoryConfig } from "./config.js";
|
|
7
5
|
import {
|
|
8
6
|
promptTheme,
|
|
@@ -10,53 +8,71 @@ import {
|
|
|
10
8
|
applyThemeAndFont,
|
|
11
9
|
getSavedConfig,
|
|
12
10
|
} from "./theme-manager.js";
|
|
11
|
+
import { promptAndApplyBrand } from "./brand-manager.js";
|
|
12
|
+
import { promptAndSetupDb } from "./db-setup.js";
|
|
13
|
+
import { printTemplateInfo } from "./info.js";
|
|
14
|
+
import { parseArgs, toCliOptions } from "./parse-args.js";
|
|
13
15
|
import {
|
|
14
16
|
copyTemplate,
|
|
15
17
|
findConflictingPaths,
|
|
16
18
|
isDirectoryEmpty,
|
|
17
19
|
listDirectoryEntries,
|
|
18
|
-
removeDirectory,
|
|
19
20
|
targetHasGitRepo,
|
|
20
21
|
} from "./copy.js";
|
|
21
|
-
import {
|
|
22
|
+
import { writeProjectStamp } from "./project-stamp.js";
|
|
23
|
+
import { printFetchComplete } from "./progress.js";
|
|
24
|
+
import { confirmYesNo } from "./prompt.js";
|
|
25
|
+
import { resolveTemplateSource } from "./template-resolver.js";
|
|
26
|
+
import { runUpdate } from "./update.js";
|
|
22
27
|
|
|
23
28
|
const HELP_TEXT = `
|
|
24
29
|
tempjs — instantiate project templates from GitHub
|
|
25
30
|
|
|
26
31
|
USAGE
|
|
27
32
|
tempjs list
|
|
33
|
+
tempjs info <template-id>
|
|
28
34
|
tempjs <template-id> [options]
|
|
29
|
-
tempjs <template-id> config
|
|
30
|
-
tempjs
|
|
31
|
-
tempjs
|
|
35
|
+
tempjs <template-id> config Interactive theme, typography, brand & database setup
|
|
36
|
+
tempjs update [--check | --merge]
|
|
37
|
+
tempjs theme Change theme in an initialized project
|
|
38
|
+
tempjs font Change font styling in an initialized project
|
|
39
|
+
tempjs brand Configure brand & contact info
|
|
40
|
+
tempjs init-db Configure .env and sync database schema
|
|
32
41
|
tempjs --help
|
|
33
42
|
|
|
43
|
+
UPDATE
|
|
44
|
+
tempjs update --check Show diff vs latest template (read-only)
|
|
45
|
+
tempjs update --merge Apply non-conflicting template updates only
|
|
46
|
+
tempjs update --merge --yes Apply without confirmation prompt
|
|
47
|
+
|
|
34
48
|
OPTIONS
|
|
35
|
-
--config
|
|
36
|
-
--
|
|
37
|
-
--
|
|
38
|
-
--
|
|
39
|
-
--
|
|
49
|
+
--config Run full setup (theme, font, brand, database) after copying
|
|
50
|
+
--yes, -y Skip all prompts
|
|
51
|
+
--no-prompt Same as --yes
|
|
52
|
+
--force, -f Overwrite existing files without prompting
|
|
53
|
+
--remote Fetch from GitHub even if a local template copy exists
|
|
54
|
+
--init-git Run git init after copying the template
|
|
55
|
+
|
|
56
|
+
(See tempjs --help for theme, brand, and database flags.)
|
|
40
57
|
|
|
41
58
|
EXAMPLES
|
|
42
59
|
mkdir hotel-client && cd hotel-client
|
|
43
60
|
tempjs hotel config
|
|
44
|
-
|
|
61
|
+
|
|
62
|
+
tempjs update --check
|
|
63
|
+
tempjs update --merge --yes
|
|
45
64
|
|
|
46
65
|
ENVIRONMENT
|
|
47
|
-
TEMPLATES_REPO_URL GitHub repo URL or owner/repo
|
|
66
|
+
TEMPLATES_REPO_URL GitHub repo URL or owner/repo
|
|
48
67
|
TEMPLATES_REPO_OWNER GitHub owner/username
|
|
49
68
|
TEMPLATES_REPO_REPO GitHub repository name
|
|
50
69
|
TEMPLATES_REPO_BRANCH Branch name (default: main)
|
|
51
70
|
TEMPLATE_USE_REMOTE=1 Always fetch from GitHub
|
|
52
|
-
GITHUB_TOKEN / GH_TOKEN
|
|
53
|
-
|
|
54
|
-
CONFIGURATION
|
|
55
|
-
Repository and template mappings live in templates.json at the package root.
|
|
71
|
+
GITHUB_TOKEN / GH_TOKEN Optional — private repositories only
|
|
56
72
|
`;
|
|
57
73
|
|
|
58
74
|
/**
|
|
59
|
-
* @param {Record<string,
|
|
75
|
+
* @param {Record<string, import('./config.js').TemplateEntry>} templates
|
|
60
76
|
*/
|
|
61
77
|
function printTemplateList(templates) {
|
|
62
78
|
console.log("Available templates:\n");
|
|
@@ -65,41 +81,20 @@ function printTemplateList(templates) {
|
|
|
65
81
|
|
|
66
82
|
for (const id of ids) {
|
|
67
83
|
const entry = templates[id];
|
|
68
|
-
|
|
84
|
+
const version = entry.version ? ` v${entry.version}` : "";
|
|
85
|
+
console.log(`${id.padEnd(idWidth + 2)}${entry.name}${version}`);
|
|
69
86
|
if (entry.description) {
|
|
70
87
|
console.log(`${"".padEnd(idWidth + 2)}${entry.description}`);
|
|
71
88
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
* @param {string[]} argv
|
|
78
|
-
*/
|
|
79
|
-
function parseArgs(argv) {
|
|
80
|
-
const flags = {
|
|
81
|
-
force: false,
|
|
82
|
-
remote: false,
|
|
83
|
-
initGit: false,
|
|
84
|
-
help: false,
|
|
85
|
-
config: false,
|
|
86
|
-
};
|
|
87
|
-
const positionals = [];
|
|
88
|
-
|
|
89
|
-
for (const arg of argv) {
|
|
90
|
-
if (arg === "--force") flags.force = true;
|
|
91
|
-
else if (arg === "--remote") flags.remote = true;
|
|
92
|
-
else if (arg === "--init-git") flags.initGit = true;
|
|
93
|
-
else if (arg === "--help" || arg === "-h") flags.help = true;
|
|
94
|
-
else if (arg === "--config") flags.config = true;
|
|
95
|
-
else if (arg.startsWith("-")) {
|
|
96
|
-
throw new Error(`Unknown option: ${arg}`);
|
|
97
|
-
} else {
|
|
98
|
-
positionals.push(arg);
|
|
89
|
+
if (entry.tags?.length) {
|
|
90
|
+
console.log(`${"".padEnd(idWidth + 2)}[${entry.tags.join(", ")}]`);
|
|
91
|
+
}
|
|
92
|
+
if (entry.stack?.length) {
|
|
93
|
+
console.log(`${"".padEnd(idWidth + 2)}${entry.stack.slice(0, 4).join(" · ")}`);
|
|
99
94
|
}
|
|
100
95
|
}
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
console.log("");
|
|
97
|
+
console.log("Run `tempjs info <template-id>` for full details.\n");
|
|
103
98
|
}
|
|
104
99
|
|
|
105
100
|
/**
|
|
@@ -118,24 +113,10 @@ function printConflictWarning(conflicts) {
|
|
|
118
113
|
console.log("");
|
|
119
114
|
}
|
|
120
115
|
|
|
121
|
-
/**
|
|
122
|
-
* @param {boolean} force
|
|
123
|
-
*/
|
|
124
|
-
async function confirmOverwrite(force) {
|
|
125
|
-
if (force) return true;
|
|
126
|
-
const rl = createInterface({ input, output });
|
|
127
|
-
try {
|
|
128
|
-
const answer = await rl.question("Continue? [y/N] ");
|
|
129
|
-
return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
|
|
130
|
-
} finally {
|
|
131
|
-
rl.close();
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
116
|
/**
|
|
136
117
|
* @param {string} targetDir
|
|
137
118
|
* @param {string} templateId
|
|
138
|
-
* @param {
|
|
119
|
+
* @param {import('./parse-args.js').CliFlags} flags
|
|
139
120
|
* @param {boolean} runWithConfig
|
|
140
121
|
*/
|
|
141
122
|
async function runTemplate(targetDir, templateId, flags, runWithConfig = false) {
|
|
@@ -151,30 +132,22 @@ async function runTemplate(targetDir, templateId, flags, runWithConfig = false)
|
|
|
151
132
|
|
|
152
133
|
const repo = resolveRepositoryConfig(manifest.repository);
|
|
153
134
|
const packageRoot = getPackageRoot();
|
|
135
|
+
const cliOptions = toCliOptions(flags);
|
|
154
136
|
const useRemote =
|
|
155
137
|
flags.remote ||
|
|
156
138
|
process.env.TEMPLATE_USE_REMOTE === "1" ||
|
|
157
139
|
process.env.TEMPLATE_USE_REMOTE === "true";
|
|
158
140
|
|
|
159
|
-
let
|
|
160
|
-
let cleanupDir = null;
|
|
141
|
+
let resolved = null;
|
|
161
142
|
|
|
162
143
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (!sourceDir) {
|
|
172
|
-
console.log(`Fetching template "${templateId}" from ${repo.owner}/${repo.repo}...`);
|
|
173
|
-
sourceDir = await fetchTemplateFromGitHub(repo, entry.directory);
|
|
174
|
-
cleanupDir = sourceDir.replace(/[/\\]template$/, "");
|
|
175
|
-
} else {
|
|
176
|
-
console.log(`Using local template "${templateId}"...`);
|
|
177
|
-
}
|
|
144
|
+
resolved = await resolveTemplateSource({
|
|
145
|
+
repo,
|
|
146
|
+
packageRoot,
|
|
147
|
+
templateDirectory: entry.directory,
|
|
148
|
+
useRemote,
|
|
149
|
+
});
|
|
150
|
+
printFetchComplete({ templateId, stats: resolved.stats });
|
|
178
151
|
|
|
179
152
|
if (targetHasGitRepo(targetDir)) {
|
|
180
153
|
console.warn(
|
|
@@ -184,19 +157,20 @@ async function runTemplate(targetDir, templateId, flags, runWithConfig = false)
|
|
|
184
157
|
|
|
185
158
|
const entries = listDirectoryEntries(targetDir);
|
|
186
159
|
const hasContent = entries.length > 0;
|
|
160
|
+
const autoConfirm = flags.force || flags.yes;
|
|
187
161
|
|
|
188
162
|
if (hasContent && !flags.force) {
|
|
189
|
-
const conflicts = findConflictingPaths(
|
|
163
|
+
const conflicts = findConflictingPaths(resolved.templateRoot, targetDir);
|
|
190
164
|
if (conflicts.length > 0) {
|
|
191
165
|
printConflictWarning(conflicts);
|
|
192
|
-
const confirmed = await
|
|
166
|
+
const confirmed = await confirmYesNo("Continue? [y/N] ", autoConfirm);
|
|
193
167
|
if (!confirmed) {
|
|
194
168
|
console.log("Aborted.");
|
|
195
169
|
return;
|
|
196
170
|
}
|
|
197
171
|
} else if (!isDirectoryEmpty(targetDir)) {
|
|
198
172
|
console.log("Current directory is not empty, but no files would be overwritten.");
|
|
199
|
-
const confirmed = await
|
|
173
|
+
const confirmed = await confirmYesNo("Continue? [y/N] ", autoConfirm);
|
|
200
174
|
if (!confirmed) {
|
|
201
175
|
console.log("Aborted.");
|
|
202
176
|
return;
|
|
@@ -205,7 +179,17 @@ async function runTemplate(targetDir, templateId, flags, runWithConfig = false)
|
|
|
205
179
|
}
|
|
206
180
|
|
|
207
181
|
const allowOverwrite = flags.force || hasContent;
|
|
208
|
-
copyTemplate(
|
|
182
|
+
copyTemplate(resolved.templateRoot, targetDir, { force: allowOverwrite });
|
|
183
|
+
|
|
184
|
+
await writeProjectStamp(targetDir, {
|
|
185
|
+
templateId,
|
|
186
|
+
templateVersion: entry.version ?? "0.0.0",
|
|
187
|
+
templateDirectory: entry.directory,
|
|
188
|
+
repository: `${repo.owner}/${repo.repo}`,
|
|
189
|
+
branch: repo.branch,
|
|
190
|
+
sourceDir: resolved.templateRoot,
|
|
191
|
+
isUpdate: false,
|
|
192
|
+
});
|
|
209
193
|
|
|
210
194
|
if (flags.initGit && !targetHasGitRepo(targetDir)) {
|
|
211
195
|
execSync("git init", { cwd: targetDir, stdio: "inherit" });
|
|
@@ -213,12 +197,16 @@ async function runTemplate(targetDir, templateId, flags, runWithConfig = false)
|
|
|
213
197
|
|
|
214
198
|
if (runWithConfig) {
|
|
215
199
|
console.log("\nConfiguring project theme and typography...");
|
|
216
|
-
const selectedTheme = await promptTheme("theme1");
|
|
217
|
-
const selectedFont = await promptFont("default");
|
|
200
|
+
const selectedTheme = await promptTheme("theme1", cliOptions);
|
|
201
|
+
const selectedFont = await promptFont("default", cliOptions);
|
|
218
202
|
await applyThemeAndFont(targetDir, selectedTheme, selectedFont);
|
|
203
|
+
|
|
204
|
+
await promptAndApplyBrand(targetDir, cliOptions);
|
|
205
|
+
await promptAndSetupDb(targetDir, cliOptions);
|
|
219
206
|
}
|
|
220
207
|
|
|
221
208
|
console.log(`\nTemplate "${entry.name}" created successfully in ${targetDir}`);
|
|
209
|
+
console.log(`Stamped .tempjs.json (template v${entry.version ?? "0.0.0"})`);
|
|
222
210
|
console.log("\nNext steps:");
|
|
223
211
|
console.log(" pnpm install # or npm install");
|
|
224
212
|
console.log(" pnpm dev # start development server");
|
|
@@ -236,8 +224,8 @@ async function runTemplate(targetDir, templateId, flags, runWithConfig = false)
|
|
|
236
224
|
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
237
225
|
process.exitCode = 1;
|
|
238
226
|
} finally {
|
|
239
|
-
if (
|
|
240
|
-
|
|
227
|
+
if (resolved) {
|
|
228
|
+
resolved.release();
|
|
241
229
|
}
|
|
242
230
|
}
|
|
243
231
|
}
|
|
@@ -247,29 +235,87 @@ async function runTemplate(targetDir, templateId, flags, runWithConfig = false)
|
|
|
247
235
|
*/
|
|
248
236
|
async function main(argv) {
|
|
249
237
|
const { flags, positionals } = parseArgs(argv);
|
|
250
|
-
|
|
238
|
+
const command = positionals[0];
|
|
251
239
|
|
|
252
240
|
if (flags.help || command === "help" || (!command && argv.length === 0)) {
|
|
253
241
|
console.log(HELP_TEXT.trim());
|
|
254
242
|
return;
|
|
255
243
|
}
|
|
256
244
|
|
|
257
|
-
|
|
245
|
+
const manifest = loadManifest();
|
|
246
|
+
const repo = resolveRepositoryConfig(manifest.repository);
|
|
247
|
+
const cliOptions = toCliOptions(flags);
|
|
248
|
+
|
|
249
|
+
if (command === "list") {
|
|
250
|
+
printTemplateList(manifest.templates);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (command === "info") {
|
|
255
|
+
const templateId = positionals[1];
|
|
256
|
+
if (!templateId) {
|
|
257
|
+
console.error("Usage: tempjs info <template-id>");
|
|
258
|
+
console.error("Run `tempjs list` to see available templates.");
|
|
259
|
+
process.exitCode = 1;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const entry = manifest.templates[templateId];
|
|
263
|
+
if (!entry) {
|
|
264
|
+
console.error(`Unknown template: ${templateId}`);
|
|
265
|
+
console.error("Run `tempjs list` to see available templates.");
|
|
266
|
+
process.exitCode = 1;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
printTemplateInfo(templateId, entry, repo);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (command === "update") {
|
|
274
|
+
const hasCheck = flags.updateCheck || argv.includes("--check");
|
|
275
|
+
const hasMerge = flags.updateMerge || argv.includes("--merge");
|
|
276
|
+
|
|
277
|
+
if (!hasCheck && !hasMerge) {
|
|
278
|
+
console.error("Usage: tempjs update --check | tempjs update --merge [--yes]");
|
|
279
|
+
process.exitCode = 1;
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const targetDir = process.cwd();
|
|
284
|
+
await runUpdate(targetDir, flags, { checkOnly: hasCheck && !hasMerge });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (
|
|
289
|
+
command === "theme" ||
|
|
290
|
+
command === "font" ||
|
|
291
|
+
command === "brand" ||
|
|
292
|
+
command === "init-db"
|
|
293
|
+
) {
|
|
258
294
|
const targetDir = process.cwd();
|
|
259
295
|
const currentConfig = getSavedConfig(targetDir);
|
|
260
296
|
|
|
261
297
|
if (command === "theme") {
|
|
262
|
-
const selectedTheme = await promptTheme(currentConfig.theme || "theme1");
|
|
263
|
-
await
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
298
|
+
const selectedTheme = await promptTheme(currentConfig.theme || "theme1", cliOptions);
|
|
299
|
+
const selectedFont = await promptFont(currentConfig.font || "default", {
|
|
300
|
+
...cliOptions,
|
|
301
|
+
yes: cliOptions.yes || Boolean(cliOptions.font),
|
|
302
|
+
});
|
|
303
|
+
await applyThemeAndFont(targetDir, selectedTheme, selectedFont);
|
|
304
|
+
} else if (command === "font") {
|
|
305
|
+
const selectedFont = await promptFont(currentConfig.font || "default", cliOptions);
|
|
306
|
+
await applyThemeAndFont(
|
|
307
|
+
targetDir,
|
|
308
|
+
currentConfig.theme || "theme1",
|
|
309
|
+
selectedFont
|
|
310
|
+
);
|
|
311
|
+
} else if (command === "brand") {
|
|
312
|
+
await promptAndApplyBrand(targetDir, cliOptions);
|
|
313
|
+
} else if (command === "init-db") {
|
|
314
|
+
await promptAndSetupDb(targetDir, cliOptions);
|
|
267
315
|
}
|
|
268
316
|
return;
|
|
269
317
|
}
|
|
270
318
|
|
|
271
|
-
const manifest = loadManifest();
|
|
272
|
-
|
|
273
319
|
let runWithConfig = flags.config;
|
|
274
320
|
if (positionals.length >= 2 && positionals[1] === "config") {
|
|
275
321
|
runWithConfig = true;
|
package/cli/info.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { loadManifest, resolveRepositoryConfig } from "./config.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} templateId
|
|
5
|
+
* @param {import('./config.js').TemplateEntry} entry
|
|
6
|
+
* @param {import('./config.js').RepositoryConfig} repo
|
|
7
|
+
*/
|
|
8
|
+
export function printTemplateInfo(templateId, entry, repo) {
|
|
9
|
+
const lines = [];
|
|
10
|
+
|
|
11
|
+
lines.push(`${entry.name} (${templateId})`);
|
|
12
|
+
lines.push("=".repeat(Math.min(60, entry.name.length + templateId.length + 4)));
|
|
13
|
+
lines.push("");
|
|
14
|
+
|
|
15
|
+
if (entry.description) {
|
|
16
|
+
lines.push(entry.description);
|
|
17
|
+
lines.push("");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (entry.version) {
|
|
21
|
+
lines.push(`Version: ${entry.version}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (entry.stack?.length) {
|
|
25
|
+
lines.push(`Stack: ${entry.stack.join(", ")}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (entry.node) {
|
|
29
|
+
lines.push(`Node.js: ${entry.node}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (entry.packageManager) {
|
|
33
|
+
lines.push(`Package manager: ${entry.packageManager}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (entry.setupTime) {
|
|
37
|
+
lines.push(`Typical setup: ${entry.setupTime}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (entry.docker !== undefined) {
|
|
41
|
+
lines.push(`Docker support: ${entry.docker ? "Yes (docker-compose.yml)" : "No"}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (entry.tags?.length) {
|
|
45
|
+
lines.push(`Tags: ${entry.tags.join(", ")}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
lines.push(`Source directory: ${repo.templatesPath}/${entry.directory}`);
|
|
49
|
+
lines.push(`Repository: github.com/${repo.owner}/${repo.repo} (${repo.branch})`);
|
|
50
|
+
|
|
51
|
+
if (entry.features?.length) {
|
|
52
|
+
lines.push("");
|
|
53
|
+
lines.push("Features:");
|
|
54
|
+
for (const feature of entry.features) {
|
|
55
|
+
lines.push(` • ${feature}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
lines.push("");
|
|
60
|
+
lines.push("Quick start:");
|
|
61
|
+
lines.push(` mkdir my-project && cd my-project`);
|
|
62
|
+
lines.push(` tempjs ${templateId} config`);
|
|
63
|
+
lines.push(` pnpm install && pnpm dev`);
|
|
64
|
+
lines.push("");
|
|
65
|
+
lines.push("Non-interactive:");
|
|
66
|
+
lines.push(
|
|
67
|
+
` tempjs ${templateId} --config --yes --theme theme1 --name "My Brand" --db-host localhost`
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (entry.docs) {
|
|
71
|
+
lines.push("");
|
|
72
|
+
lines.push(`Docs in generated project: ${entry.docs}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(lines.join("\n"));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {Record<string, import('./config.js').TemplateEntry>} templates
|
|
80
|
+
* @param {import('./config.js').RepositoryConfig} repo
|
|
81
|
+
*/
|
|
82
|
+
export function printAllTemplatesInfo(templates, repo) {
|
|
83
|
+
const ids = Object.keys(templates).sort();
|
|
84
|
+
for (let i = 0; i < ids.length; i++) {
|
|
85
|
+
if (i > 0) console.log("\n");
|
|
86
|
+
printTemplateInfo(ids[i], templates[ids[i]], repo);
|
|
87
|
+
}
|
|
88
|
+
}
|