@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/README.md +752 -35
- package/cli/brand-manager.js +178 -66
- package/cli/config.js +14 -1
- package/cli/copy.js +4 -18
- package/cli/db-setup.js +170 -49
- package/cli/fetch.js +47 -29
- package/cli/file-tree.js +113 -0
- package/cli/fs-ignore.js +60 -0
- package/cli/index.js +138 -107
- 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 +28 -2
- package/cli/update.js +212 -0
- package/package.json +6 -2
- package/templates.json +33 -3
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 {
|
|
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
|
|
32
|
-
tempjs
|
|
33
|
-
tempjs
|
|
34
|
-
tempjs
|
|
35
|
-
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
|
|
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
|
|
40
|
-
--
|
|
41
|
-
--
|
|
42
|
-
--
|
|
43
|
-
--
|
|
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
|
-
|
|
49
|
-
tempjs
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
165
|
-
let cleanupDir = null;
|
|
141
|
+
let resolved = null;
|
|
166
142
|
|
|
167
143
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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(
|
|
163
|
+
const conflicts = findConflictingPaths(resolved.templateRoot, targetDir);
|
|
195
164
|
if (conflicts.length > 0) {
|
|
196
165
|
printConflictWarning(conflicts);
|
|
197
|
-
const confirmed = await
|
|
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
|
|
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(
|
|
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
|
-
|
|
226
|
-
await
|
|
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 (
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
+
}
|
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
|
+
}
|