@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.
@@ -0,0 +1,113 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+ import { shouldSkipPath } from "./fs-ignore.js";
5
+
6
+ /**
7
+ * @param {string} filePath
8
+ * @returns {string}
9
+ */
10
+ export function hashFileContents(filePath) {
11
+ return createHash("sha256").update(readFileSync(filePath)).digest("hex");
12
+ }
13
+
14
+ /**
15
+ * @param {string} rootDir
16
+ * @returns {Record<string, string>}
17
+ */
18
+ export function collectFileHashes(rootDir) {
19
+ const hashes = {};
20
+ collectRecursive(rootDir, rootDir, hashes);
21
+ return hashes;
22
+ }
23
+
24
+ /**
25
+ * @param {string} rootDir
26
+ * @param {string} currentDir
27
+ * @param {Record<string, string>} hashes
28
+ */
29
+ function collectRecursive(rootDir, currentDir, hashes) {
30
+ if (!existsSync(currentDir)) return;
31
+
32
+ for (const name of readdirSync(currentDir)) {
33
+ const fullPath = join(currentDir, name);
34
+ const relPath = relative(rootDir, fullPath).replace(/\\/g, "/");
35
+
36
+ if (shouldSkipPath(relPath, name)) continue;
37
+
38
+ const stat = statSync(fullPath);
39
+ if (stat.isDirectory()) {
40
+ collectRecursive(rootDir, fullPath, hashes);
41
+ } else {
42
+ hashes[relPath] = hashFileContents(fullPath);
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * @param {string} rootDir
49
+ * @returns {number}
50
+ */
51
+ export function countFiles(rootDir) {
52
+ return Object.keys(collectFileHashes(rootDir)).length;
53
+ }
54
+
55
+ /**
56
+ * @typedef {{
57
+ * newFiles: string[],
58
+ * upToDate: string[],
59
+ * safeUpdates: string[],
60
+ * conflicts: string[],
61
+ * protectedSkips: string[],
62
+ * removedFromTemplate: string[]
63
+ * }} UpdateDiff
64
+ */
65
+
66
+ /**
67
+ * Compare project against baseline (at generation) and latest template hashes.
68
+ * @param {Record<string, string>} baselineHashes
69
+ * @param {Record<string, string>} currentHashes
70
+ * @param {Record<string, string>} latestHashes
71
+ * @returns {UpdateDiff}
72
+ */
73
+ export function diffTemplateTrees(baselineHashes, currentHashes, latestHashes) {
74
+ const diff = {
75
+ newFiles: [],
76
+ upToDate: [],
77
+ safeUpdates: [],
78
+ conflicts: [],
79
+ protectedSkips: [],
80
+ removedFromTemplate: [],
81
+ };
82
+
83
+ for (const path of Object.keys(latestHashes).sort()) {
84
+ const latest = latestHashes[path];
85
+ const current = currentHashes[path];
86
+ const baseline = baselineHashes[path];
87
+
88
+ if (current === undefined) {
89
+ diff.newFiles.push(path);
90
+ continue;
91
+ }
92
+
93
+ if (current === latest) {
94
+ diff.upToDate.push(path);
95
+ continue;
96
+ }
97
+
98
+ if (baseline !== undefined && current === baseline && latest !== baseline) {
99
+ diff.safeUpdates.push(path);
100
+ continue;
101
+ }
102
+
103
+ diff.conflicts.push(path);
104
+ }
105
+
106
+ for (const path of Object.keys(baselineHashes).sort()) {
107
+ if (!latestHashes[path]) {
108
+ diff.removedFromTemplate.push(path);
109
+ }
110
+ }
111
+
112
+ return diff;
113
+ }
@@ -0,0 +1,60 @@
1
+ /** Files and directories never copied from templates or merged on update. */
2
+ export const NEVER_COPY_NAMES = new Set([".git", ".gitignore.bak"]);
3
+
4
+ /** Paths relative to project root that must not be overwritten by template update. */
5
+ export const UPDATE_PROTECTED_PATHS = new Set([
6
+ ".env",
7
+ ".tempjsrc",
8
+ ".tempjs.json",
9
+ "constants/site.ts",
10
+ "app/tempjs-theme.css",
11
+ ]);
12
+
13
+ /** Directory names skipped when walking trees. */
14
+ export const SKIP_DIR_NAMES = new Set([
15
+ "node_modules",
16
+ ".next",
17
+ ".git",
18
+ "dist",
19
+ "build",
20
+ "coverage",
21
+ ]);
22
+
23
+ /**
24
+ * @param {string} name
25
+ * @returns {boolean}
26
+ */
27
+ export function shouldSkipFileName(name) {
28
+ if (NEVER_COPY_NAMES.has(name)) return true;
29
+ if (name === ".env" || name.startsWith(".env.")) {
30
+ return name !== ".env.example";
31
+ }
32
+ return false;
33
+ }
34
+
35
+ /**
36
+ * @param {string} relativePath
37
+ * @param {string} [name]
38
+ * @returns {boolean}
39
+ */
40
+ export function shouldSkipPath(relativePath, name) {
41
+ const base = name ?? relativePath.split("/").pop() ?? "";
42
+ if (shouldSkipFileName(base)) return true;
43
+
44
+ const parts = relativePath.split("/");
45
+ for (const part of parts) {
46
+ if (SKIP_DIR_NAMES.has(part)) return true;
47
+ }
48
+ return false;
49
+ }
50
+
51
+ /**
52
+ * @param {string} relativePath
53
+ * @returns {boolean}
54
+ */
55
+ export function isUpdateProtected(relativePath) {
56
+ const normalized = relativePath.replace(/\\/g, "/");
57
+ if (UPDATE_PROTECTED_PATHS.has(normalized)) return true;
58
+ if (normalized.startsWith(".env.") && normalized !== ".env.example") return true;
59
+ return false;
60
+ }
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 { fetchTemplateFromGitHub, resolveLocalTemplate } from "./fetch.js";
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 Initialize with interactive theme & typography setup
30
- tempjs theme Change the project's theme in an initialized directory
31
- tempjs font Change the project's font styling in an initialized directory
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 Prompt for theme and font pairings during initialization
36
- --force Overwrite existing files in the current directory
37
- --remote Fetch from GitHub even if a local template copy exists
38
- --init-git Run git init after copying the template
39
- --help Show this help message
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
- tempjs theme
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 (overrides templates.json)
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 Optional — required only for private repositories
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, { name: string, description: string }>} templates
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
- console.log(`${id.padEnd(idWidth + 2)}${entry.name}`);
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
- console.log("");
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
- return { flags, positionals };
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 {{ force: boolean, remote: boolean, initGit: boolean }} flags
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 sourceDir = null;
160
- let cleanupDir = null;
141
+ let resolved = null;
161
142
 
162
143
  try {
163
- if (!useRemote) {
164
- sourceDir = await resolveLocalTemplate(
165
- packageRoot,
166
- repo.templatesPath,
167
- entry.directory
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(sourceDir, targetDir);
163
+ const conflicts = findConflictingPaths(resolved.templateRoot, targetDir);
190
164
  if (conflicts.length > 0) {
191
165
  printConflictWarning(conflicts);
192
- const confirmed = await confirmOverwrite(false);
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 confirmOverwrite(false);
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(sourceDir, targetDir, { force: allowOverwrite });
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 (cleanupDir) {
240
- removeDirectory(cleanupDir);
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
- let command = positionals[0];
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
- if (command === "theme" || command === "font") {
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 applyThemeAndFont(targetDir, selectedTheme, currentConfig.font || "default");
264
- } else {
265
- const selectedFont = await promptFont(currentConfig.font || "default");
266
- await applyThemeAndFont(targetDir, currentConfig.theme || "theme1", selectedFont);
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
+ }