@navneet_25/tempjs 1.1.0 → 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/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,
@@ -12,56 +10,69 @@ import {
12
10
  } from "./theme-manager.js";
13
11
  import { promptAndApplyBrand } from "./brand-manager.js";
14
12
  import { promptAndSetupDb } from "./db-setup.js";
13
+ import { printTemplateInfo } from "./info.js";
14
+ import { parseArgs, toCliOptions } from "./parse-args.js";
15
15
  import {
16
16
  copyTemplate,
17
17
  findConflictingPaths,
18
18
  isDirectoryEmpty,
19
19
  listDirectoryEntries,
20
- removeDirectory,
21
20
  targetHasGitRepo,
22
21
  } from "./copy.js";
23
- 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";
24
27
 
25
28
  const HELP_TEXT = `
26
29
  tempjs — instantiate project templates from GitHub
27
30
 
28
31
  USAGE
29
32
  tempjs list
33
+ tempjs info <template-id>
30
34
  tempjs <template-id> [options]
31
- tempjs <template-id> config Initialize with interactive theme, typography, brand & database setup
32
- tempjs theme Change the project's theme in an initialized directory
33
- tempjs font Change the project's font styling in an initialized directory
34
- tempjs brand Configure the brand & contact info in an initialized directory
35
- tempjs init-db Configure .env and sync database schema 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
36
41
  tempjs --help
37
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
+
38
48
  OPTIONS
39
- --config Prompt for full interactive setup (theme, font, brand, db) during initialization
40
- --force Overwrite existing files in the current directory
41
- --remote Fetch from GitHub even if a local template copy exists
42
- --init-git Run git init after copying the template
43
- --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.)
44
57
 
45
58
  EXAMPLES
46
59
  mkdir hotel-client && cd hotel-client
47
60
  tempjs hotel config
48
- tempjs brand
49
- tempjs init-db
61
+
62
+ tempjs update --check
63
+ tempjs update --merge --yes
50
64
 
51
65
  ENVIRONMENT
52
- TEMPLATES_REPO_URL GitHub repo URL or owner/repo (overrides templates.json)
66
+ TEMPLATES_REPO_URL GitHub repo URL or owner/repo
53
67
  TEMPLATES_REPO_OWNER GitHub owner/username
54
68
  TEMPLATES_REPO_REPO GitHub repository name
55
69
  TEMPLATES_REPO_BRANCH Branch name (default: main)
56
70
  TEMPLATE_USE_REMOTE=1 Always fetch from GitHub
57
- GITHUB_TOKEN / GH_TOKEN Optional — required only for private repositories
58
-
59
- CONFIGURATION
60
- Repository and template mappings live in templates.json at the package root.
71
+ GITHUB_TOKEN / GH_TOKEN Optional — private repositories only
61
72
  `;
62
73
 
63
74
  /**
64
- * @param {Record<string, { name: string, description: string }>} templates
75
+ * @param {Record<string, import('./config.js').TemplateEntry>} templates
65
76
  */
66
77
  function printTemplateList(templates) {
67
78
  console.log("Available templates:\n");
@@ -70,41 +81,20 @@ function printTemplateList(templates) {
70
81
 
71
82
  for (const id of ids) {
72
83
  const entry = templates[id];
73
- 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}`);
74
86
  if (entry.description) {
75
87
  console.log(`${"".padEnd(idWidth + 2)}${entry.description}`);
76
88
  }
77
- }
78
- console.log("");
79
- }
80
-
81
- /**
82
- * @param {string[]} argv
83
- */
84
- function parseArgs(argv) {
85
- const flags = {
86
- force: false,
87
- remote: false,
88
- initGit: false,
89
- help: false,
90
- config: false,
91
- };
92
- const positionals = [];
93
-
94
- for (const arg of argv) {
95
- if (arg === "--force") flags.force = true;
96
- else if (arg === "--remote") flags.remote = true;
97
- else if (arg === "--init-git") flags.initGit = true;
98
- else if (arg === "--help" || arg === "-h") flags.help = true;
99
- else if (arg === "--config") flags.config = true;
100
- else if (arg.startsWith("-")) {
101
- throw new Error(`Unknown option: ${arg}`);
102
- } else {
103
- 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(" · ")}`);
104
94
  }
105
95
  }
106
-
107
- return { flags, positionals };
96
+ console.log("");
97
+ console.log("Run `tempjs info <template-id>` for full details.\n");
108
98
  }
109
99
 
110
100
  /**
@@ -123,24 +113,10 @@ function printConflictWarning(conflicts) {
123
113
  console.log("");
124
114
  }
125
115
 
126
- /**
127
- * @param {boolean} force
128
- */
129
- async function confirmOverwrite(force) {
130
- if (force) return true;
131
- const rl = createInterface({ input, output });
132
- try {
133
- const answer = await rl.question("Continue? [y/N] ");
134
- return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
135
- } finally {
136
- rl.close();
137
- }
138
- }
139
-
140
116
  /**
141
117
  * @param {string} targetDir
142
118
  * @param {string} templateId
143
- * @param {{ force: boolean, remote: boolean, initGit: boolean }} flags
119
+ * @param {import('./parse-args.js').CliFlags} flags
144
120
  * @param {boolean} runWithConfig
145
121
  */
146
122
  async function runTemplate(targetDir, templateId, flags, runWithConfig = false) {
@@ -156,30 +132,22 @@ async function runTemplate(targetDir, templateId, flags, runWithConfig = false)
156
132
 
157
133
  const repo = resolveRepositoryConfig(manifest.repository);
158
134
  const packageRoot = getPackageRoot();
135
+ const cliOptions = toCliOptions(flags);
159
136
  const useRemote =
160
137
  flags.remote ||
161
138
  process.env.TEMPLATE_USE_REMOTE === "1" ||
162
139
  process.env.TEMPLATE_USE_REMOTE === "true";
163
140
 
164
- let sourceDir = null;
165
- let cleanupDir = null;
141
+ let resolved = null;
166
142
 
167
143
  try {
168
- if (!useRemote) {
169
- sourceDir = await resolveLocalTemplate(
170
- packageRoot,
171
- repo.templatesPath,
172
- entry.directory
173
- );
174
- }
175
-
176
- if (!sourceDir) {
177
- console.log(`Fetching template "${templateId}" from ${repo.owner}/${repo.repo}...`);
178
- sourceDir = await fetchTemplateFromGitHub(repo, entry.directory);
179
- cleanupDir = sourceDir.replace(/[/\\]template$/, "");
180
- } else {
181
- console.log(`Using local template "${templateId}"...`);
182
- }
144
+ resolved = await resolveTemplateSource({
145
+ repo,
146
+ packageRoot,
147
+ templateDirectory: entry.directory,
148
+ useRemote,
149
+ });
150
+ printFetchComplete({ templateId, stats: resolved.stats });
183
151
 
184
152
  if (targetHasGitRepo(targetDir)) {
185
153
  console.warn(
@@ -189,19 +157,20 @@ async function runTemplate(targetDir, templateId, flags, runWithConfig = false)
189
157
 
190
158
  const entries = listDirectoryEntries(targetDir);
191
159
  const hasContent = entries.length > 0;
160
+ const autoConfirm = flags.force || flags.yes;
192
161
 
193
162
  if (hasContent && !flags.force) {
194
- const conflicts = findConflictingPaths(sourceDir, targetDir);
163
+ const conflicts = findConflictingPaths(resolved.templateRoot, targetDir);
195
164
  if (conflicts.length > 0) {
196
165
  printConflictWarning(conflicts);
197
- const confirmed = await confirmOverwrite(false);
166
+ const confirmed = await confirmYesNo("Continue? [y/N] ", autoConfirm);
198
167
  if (!confirmed) {
199
168
  console.log("Aborted.");
200
169
  return;
201
170
  }
202
171
  } else if (!isDirectoryEmpty(targetDir)) {
203
172
  console.log("Current directory is not empty, but no files would be overwritten.");
204
- const confirmed = await confirmOverwrite(false);
173
+ const confirmed = await confirmYesNo("Continue? [y/N] ", autoConfirm);
205
174
  if (!confirmed) {
206
175
  console.log("Aborted.");
207
176
  return;
@@ -210,7 +179,17 @@ async function runTemplate(targetDir, templateId, flags, runWithConfig = false)
210
179
  }
211
180
 
212
181
  const allowOverwrite = flags.force || hasContent;
213
- 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
+ });
214
193
 
215
194
  if (flags.initGit && !targetHasGitRepo(targetDir)) {
216
195
  execSync("git init", { cwd: targetDir, stdio: "inherit" });
@@ -218,18 +197,16 @@ async function runTemplate(targetDir, templateId, flags, runWithConfig = false)
218
197
 
219
198
  if (runWithConfig) {
220
199
  console.log("\nConfiguring project theme and typography...");
221
- const selectedTheme = await promptTheme("theme1");
222
- const selectedFont = await promptFont("default");
200
+ const selectedTheme = await promptTheme("theme1", cliOptions);
201
+ const selectedFont = await promptFont("default", cliOptions);
223
202
  await applyThemeAndFont(targetDir, selectedTheme, selectedFont);
224
203
 
225
- // Interactive Brand & Contact Details configuration
226
- await promptAndApplyBrand(targetDir);
227
-
228
- // Environment & Database auto-setup
229
- await promptAndSetupDb(targetDir);
204
+ await promptAndApplyBrand(targetDir, cliOptions);
205
+ await promptAndSetupDb(targetDir, cliOptions);
230
206
  }
231
207
 
232
208
  console.log(`\nTemplate "${entry.name}" created successfully in ${targetDir}`);
209
+ console.log(`Stamped .tempjs.json (template v${entry.version ?? "0.0.0"})`);
233
210
  console.log("\nNext steps:");
234
211
  console.log(" pnpm install # or npm install");
235
212
  console.log(" pnpm dev # start development server");
@@ -247,8 +224,8 @@ async function runTemplate(targetDir, templateId, flags, runWithConfig = false)
247
224
  console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
248
225
  process.exitCode = 1;
249
226
  } finally {
250
- if (cleanupDir) {
251
- removeDirectory(cleanupDir);
227
+ if (resolved) {
228
+ resolved.release();
252
229
  }
253
230
  }
254
231
  }
@@ -258,33 +235,87 @@ async function runTemplate(targetDir, templateId, flags, runWithConfig = false)
258
235
  */
259
236
  async function main(argv) {
260
237
  const { flags, positionals } = parseArgs(argv);
261
- let command = positionals[0];
238
+ const command = positionals[0];
262
239
 
263
240
  if (flags.help || command === "help" || (!command && argv.length === 0)) {
264
241
  console.log(HELP_TEXT.trim());
265
242
  return;
266
243
  }
267
244
 
268
- if (command === "theme" || command === "font" || command === "brand" || command === "init-db") {
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
+ ) {
269
294
  const targetDir = process.cwd();
270
295
  const currentConfig = getSavedConfig(targetDir);
271
296
 
272
297
  if (command === "theme") {
273
- const selectedTheme = await promptTheme(currentConfig.theme || "theme1");
274
- await applyThemeAndFont(targetDir, selectedTheme, currentConfig.font || "default");
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);
275
304
  } else if (command === "font") {
276
- const selectedFont = await promptFont(currentConfig.font || "default");
277
- await applyThemeAndFont(targetDir, currentConfig.theme || "theme1", selectedFont);
305
+ const selectedFont = await promptFont(currentConfig.font || "default", cliOptions);
306
+ await applyThemeAndFont(
307
+ targetDir,
308
+ currentConfig.theme || "theme1",
309
+ selectedFont
310
+ );
278
311
  } else if (command === "brand") {
279
- await promptAndApplyBrand(targetDir);
312
+ await promptAndApplyBrand(targetDir, cliOptions);
280
313
  } else if (command === "init-db") {
281
- await promptAndSetupDb(targetDir);
314
+ await promptAndSetupDb(targetDir, cliOptions);
282
315
  }
283
316
  return;
284
317
  }
285
318
 
286
- const manifest = loadManifest();
287
-
288
319
  let runWithConfig = flags.config;
289
320
  if (positionals.length >= 2 && positionals[1] === "config") {
290
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
+ }
@@ -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
+ }
@@ -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
+ }