@guchenyo/create-temp 0.0.1 → 0.0.3
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/LICENSE +5 -0
- package/README.md +4 -4
- package/lib/scaffold.js +126 -58
- package/optional-packages.json +47 -2
- package/package.json +3 -2
package/LICENSE
ADDED
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# create-temp
|
|
2
2
|
|
|
3
3
|
Local template scaffolding CLI for this repository. It clones the latest
|
|
4
|
-
templates from GitHub on each run (
|
|
4
|
+
templates from GitHub on each run (HTTPS by default).
|
|
5
5
|
|
|
6
6
|
## Usage
|
|
7
7
|
|
|
@@ -20,19 +20,19 @@ create-temp front/web/vue3/vue3-no-ts my-web
|
|
|
20
20
|
Use with npm create:
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
npm create @
|
|
23
|
+
npm create @guchenyo/temp@latest -- backend/express/express-no-ts my-api
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
Interactive selection (TUI):
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
npm create @
|
|
29
|
+
npm create @guchenyo/temp@latest
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
## Requirements
|
|
33
33
|
|
|
34
34
|
- Git installed and available in PATH
|
|
35
|
-
-
|
|
35
|
+
- Access to `https://github.com/GuChena/template.git` (or override `TEMPLATE_REPO_URL`)
|
|
36
36
|
|
|
37
37
|
## Publish
|
|
38
38
|
|
package/lib/scaffold.js
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
} from "@clack/prompts";
|
|
25
25
|
import gradient from "gradient-string";
|
|
26
26
|
import ora from "ora";
|
|
27
|
+
import pc from "picocolors";
|
|
27
28
|
|
|
28
29
|
const IGNORE_NAMES = new Set([
|
|
29
30
|
".git",
|
|
@@ -43,7 +44,7 @@ const PROMPT_CANCELLED_MESSAGE = "已取消操作。";
|
|
|
43
44
|
const PROMPT_BACK_VALUE = "__back";
|
|
44
45
|
const OPTIONAL_PACKAGES_PATH = new URL(
|
|
45
46
|
"../optional-packages.json",
|
|
46
|
-
import.meta.url
|
|
47
|
+
import.meta.url,
|
|
47
48
|
);
|
|
48
49
|
|
|
49
50
|
function assertInteractive() {
|
|
@@ -71,6 +72,90 @@ function applyGradientPerLine(text, gradientFn) {
|
|
|
71
72
|
.join("\n");
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
function renderHero() {
|
|
76
|
+
const headerGradient = gradient(["#4285f4", "#9b72cb", "#d96570"]);
|
|
77
|
+
const bodyGradient = gradient(["#8ec5ff", "#c9a1f0", "#ffb3ba"]);
|
|
78
|
+
const logo = [
|
|
79
|
+
" ██████╗██████╗ ███████╗ █████╗ ████████╗███████╗ ████████╗███████╗███╗ ███╗██████╗ ",
|
|
80
|
+
" ██╔════╝██╔══██╗██╔════╝██╔══██╗╚══██╔══╝██╔════╝ ╚══██╔══╝██╔════╝████╗ ████║██╔══██╗ ",
|
|
81
|
+
" ██║ ██████╔╝█████╗ ███████║ ██║ █████╗ █████╗ ██║ █████╗ ██╔████╔██║██████╔╝ ",
|
|
82
|
+
" ██║ ██╔══██╗██╔══╝ ██╔══██║ ██║ ██╔══╝ ╚════╝ ██║ ██╔══╝ ██║╚██╔╝██║██╔═══╝ ",
|
|
83
|
+
" ╚██████╗██║ ██║███████╗██║ ██║ ██║ ███████╗ ██║ ███████╗██║ ╚═╝ ██║██║ ",
|
|
84
|
+
" ╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ "
|
|
85
|
+
].join("\n");
|
|
86
|
+
|
|
87
|
+
const header = applyGradientPerLine(
|
|
88
|
+
[
|
|
89
|
+
logo,
|
|
90
|
+
" ✨ 一站式项目模板脚手架 ✨",
|
|
91
|
+
"",
|
|
92
|
+
" 按步骤选择模板、可选依赖和项目目录。",
|
|
93
|
+
].join("\n"),
|
|
94
|
+
headerGradient,
|
|
95
|
+
);
|
|
96
|
+
const hints = applyGradientPerLine(
|
|
97
|
+
[
|
|
98
|
+
" 流程: 1) 模板 2) 可选依赖 3) 项目名称",
|
|
99
|
+
" 支持: backend / front",
|
|
100
|
+
].join("\n"),
|
|
101
|
+
bodyGradient,
|
|
102
|
+
);
|
|
103
|
+
return `${header}\n\n${hints}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildTemplateOverview(templates) {
|
|
107
|
+
const grouped = new Map();
|
|
108
|
+
|
|
109
|
+
templates.forEach((template) => {
|
|
110
|
+
const [root, ...rest] = template.key.split("/");
|
|
111
|
+
if (!grouped.has(root)) {
|
|
112
|
+
grouped.set(root, []);
|
|
113
|
+
}
|
|
114
|
+
grouped.get(root).push(rest.join("/"));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const lines = [];
|
|
118
|
+
const roots = Array.from(grouped.keys()).sort((a, b) => a.localeCompare(b));
|
|
119
|
+
roots.forEach((root) => {
|
|
120
|
+
const items = grouped.get(root).sort((a, b) => a.localeCompare(b));
|
|
121
|
+
lines.push(`${root}/ (${items.length})`);
|
|
122
|
+
items.slice(0, 6).forEach((item) => {
|
|
123
|
+
lines.push(` - ${item}`);
|
|
124
|
+
});
|
|
125
|
+
if (items.length > 6) {
|
|
126
|
+
lines.push(` - ... +${items.length - 6} 更多`);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function buildNextStepsBlock({
|
|
134
|
+
selectedTemplateKey,
|
|
135
|
+
selectedOptionalPackages,
|
|
136
|
+
resolvedTargetDir,
|
|
137
|
+
packageManager,
|
|
138
|
+
}) {
|
|
139
|
+
const lines = [
|
|
140
|
+
`📂 模板: ${pc.cyan(selectedTemplateKey)}`,
|
|
141
|
+
`📁 目录: ${pc.cyan(resolvedTargetDir)}`,
|
|
142
|
+
"",
|
|
143
|
+
"🚀 下一步命令:",
|
|
144
|
+
` ${pc.green(`cd ${resolvedTargetDir}`)}`,
|
|
145
|
+
` ${pc.green(`${packageManager} install`)}`,
|
|
146
|
+
` ${pc.green(`${packageManager} run dev`)}`,
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
if (selectedOptionalPackages.length > 0) {
|
|
150
|
+
const installed = selectedOptionalPackages
|
|
151
|
+
.map((pkg) => pkg.name)
|
|
152
|
+
.join(", ");
|
|
153
|
+
lines.push("", `📦 已预装可选依赖: ${pc.magenta(installed)}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return lines.join("\n");
|
|
157
|
+
}
|
|
158
|
+
|
|
74
159
|
async function cloneTemplateRepo() {
|
|
75
160
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), "template-cli-"));
|
|
76
161
|
const repoDir = path.join(tempDir, `repo-${Date.now().toString(36)}`);
|
|
@@ -85,7 +170,7 @@ async function cloneTemplateRepo() {
|
|
|
85
170
|
await execFileAsync(
|
|
86
171
|
"git",
|
|
87
172
|
["clone", "--depth", "1", TEMPLATE_REPO_URL, repoDir],
|
|
88
|
-
{ env }
|
|
173
|
+
{ env },
|
|
89
174
|
);
|
|
90
175
|
return { repoDir, tempDir };
|
|
91
176
|
} catch (error) {
|
|
@@ -227,30 +312,24 @@ async function promptOptionalPackages(optionalPackages) {
|
|
|
227
312
|
|
|
228
313
|
const choices = optionalPackages.map((pkg) => ({
|
|
229
314
|
value: pkg.name,
|
|
230
|
-
label: pkg.description
|
|
315
|
+
label: pkg.description
|
|
316
|
+
? `${pkg.name}${pkg.dev ? " (dev)" : ""} - ${pkg.description}`
|
|
317
|
+
: `${pkg.name}${pkg.dev ? " (dev)" : ""}`,
|
|
231
318
|
}));
|
|
232
319
|
|
|
233
320
|
const selected = handleCancel(
|
|
234
321
|
await multiselect({
|
|
235
|
-
message: "
|
|
322
|
+
message: "🧩 第 2 步:选择可选预装依赖(空格多选,回车确认)",
|
|
236
323
|
options: choices,
|
|
237
324
|
required: false,
|
|
238
|
-
})
|
|
325
|
+
}),
|
|
239
326
|
);
|
|
240
327
|
|
|
241
328
|
if (!Array.isArray(selected) || selected.length === 0) {
|
|
242
329
|
return [];
|
|
243
330
|
}
|
|
244
331
|
|
|
245
|
-
|
|
246
|
-
const gray = "\x1b[90m";
|
|
247
|
-
const dim = "\x1b[2m";
|
|
248
|
-
const reset = "\x1b[0m";
|
|
249
|
-
process.stdout.write("\x1b[1A\x1b[2K");
|
|
250
|
-
process.stdout.write(
|
|
251
|
-
`${gray}│${reset} ${dim}${selected.join(", ")}${reset}\n`
|
|
252
|
-
);
|
|
253
|
-
}
|
|
332
|
+
note(selected.join("\n"), "已选择可选依赖");
|
|
254
333
|
|
|
255
334
|
const selectedSet = new Set(selected);
|
|
256
335
|
return optionalPackages.filter((pkg) => selectedSet.has(pkg.name));
|
|
@@ -301,10 +380,10 @@ async function promptSelectTemplateTree(templates) {
|
|
|
301
380
|
await select({
|
|
302
381
|
message:
|
|
303
382
|
parents.length === 0
|
|
304
|
-
? "
|
|
305
|
-
:
|
|
383
|
+
? "📁 第 1 步:选择模板类别"
|
|
384
|
+
: `📂 第 1 步:选择模板目录 (${pc.cyan(pathStack.join("/"))})`,
|
|
306
385
|
options: choices,
|
|
307
|
-
})
|
|
386
|
+
}),
|
|
308
387
|
);
|
|
309
388
|
|
|
310
389
|
if (!selection || typeof selection !== "string") {
|
|
@@ -336,11 +415,11 @@ async function promptTargetDir() {
|
|
|
336
415
|
assertInteractive();
|
|
337
416
|
const response = handleCancel(
|
|
338
417
|
await text({
|
|
339
|
-
message: "
|
|
418
|
+
message: "🎯 第 3 步:输入项目名称",
|
|
340
419
|
placeholder: "my-app",
|
|
341
420
|
validate: (value) =>
|
|
342
421
|
value && value.trim().length > 0 ? undefined : "请输入项目名称。",
|
|
343
|
-
})
|
|
422
|
+
}),
|
|
344
423
|
);
|
|
345
424
|
return response.trim();
|
|
346
425
|
}
|
|
@@ -392,11 +471,11 @@ async function applyOptionalPackages(targetPath, packages) {
|
|
|
392
471
|
|
|
393
472
|
const hadDependencies = Object.prototype.hasOwnProperty.call(
|
|
394
473
|
pkgJson,
|
|
395
|
-
"dependencies"
|
|
474
|
+
"dependencies",
|
|
396
475
|
);
|
|
397
476
|
const hadDevDependencies = Object.prototype.hasOwnProperty.call(
|
|
398
477
|
pkgJson,
|
|
399
|
-
"devDependencies"
|
|
478
|
+
"devDependencies",
|
|
400
479
|
);
|
|
401
480
|
|
|
402
481
|
const dependencies = hadDependencies ? { ...pkgJson.dependencies } : {};
|
|
@@ -436,27 +515,9 @@ async function applyOptionalPackages(targetPath, packages) {
|
|
|
436
515
|
export async function scaffold({ templateKey, targetDir }) {
|
|
437
516
|
const interactive = isInteractive();
|
|
438
517
|
if (interactive) {
|
|
439
|
-
intro("create-temp");
|
|
440
|
-
console.log("");
|
|
441
|
-
console.log(
|
|
442
|
-
applyGradientPerLine(
|
|
443
|
-
["轻量模板脚手架", "按提示选择模板并设置项目名称"].join("\n"),
|
|
444
|
-
gradient(["#8ec5ff", "#b6e3ff"])
|
|
445
|
-
)
|
|
446
|
-
);
|
|
518
|
+
intro(gradient(["#8ec5ff", "#b6e3ff"])("create-temp"));
|
|
447
519
|
console.log("");
|
|
448
|
-
console.log(
|
|
449
|
-
applyGradientPerLine(
|
|
450
|
-
[
|
|
451
|
-
"可用模板与用途:",
|
|
452
|
-
" backend/",
|
|
453
|
-
" - express/express-no-ts: Node.js + Express API 服务",
|
|
454
|
-
" front/",
|
|
455
|
-
" - web/vue3/vue3-no-ts: Vue3 前端站点",
|
|
456
|
-
].join("\n"),
|
|
457
|
-
gradient(["#9de7ff", "#b7f7d4"])
|
|
458
|
-
)
|
|
459
|
-
);
|
|
520
|
+
console.log(renderHero());
|
|
460
521
|
console.log("");
|
|
461
522
|
}
|
|
462
523
|
|
|
@@ -464,7 +525,7 @@ export async function scaffold({ templateKey, targetDir }) {
|
|
|
464
525
|
let tempDir = null;
|
|
465
526
|
const cloneSpinner = interactive
|
|
466
527
|
? ora({
|
|
467
|
-
text: "正在获取模板...",
|
|
528
|
+
text: pc.cyan("✨ 正在获取模板..."),
|
|
468
529
|
spinner: "dots",
|
|
469
530
|
interval: 50,
|
|
470
531
|
}).start()
|
|
@@ -474,12 +535,11 @@ export async function scaffold({ templateKey, targetDir }) {
|
|
|
474
535
|
repoDir = cloneResult.repoDir;
|
|
475
536
|
tempDir = cloneResult.tempDir;
|
|
476
537
|
if (cloneSpinner) {
|
|
477
|
-
cloneSpinner.
|
|
478
|
-
console.log(`T ${gradient(["#8ec5ff", "#b6e3ff"])("加载完成")}`);
|
|
538
|
+
cloneSpinner.succeed(pc.green("✨ 模板索引加载完成"));
|
|
479
539
|
}
|
|
480
540
|
} catch (error) {
|
|
481
541
|
if (cloneSpinner) {
|
|
482
|
-
cloneSpinner.fail("模板获取失败");
|
|
542
|
+
cloneSpinner.fail(pc.red("❌ 模板获取失败"));
|
|
483
543
|
}
|
|
484
544
|
const message = error instanceof Error ? error.message : String(error);
|
|
485
545
|
throw new Error(`模板仓库拉取失败,请检查网络或权限。${message}`);
|
|
@@ -509,11 +569,15 @@ export async function scaffold({ templateKey, targetDir }) {
|
|
|
509
569
|
throw new Error("未在 backend/ 或 front/ 下找到模板。");
|
|
510
570
|
}
|
|
511
571
|
|
|
572
|
+
if (interactive) {
|
|
573
|
+
note(buildTemplateOverview(templates), `模板分类总览(共 ${templates.length} 个)`);
|
|
574
|
+
}
|
|
575
|
+
|
|
512
576
|
let selectedTemplate = null;
|
|
513
577
|
if (templateKey) {
|
|
514
578
|
const normalizedKey = normalizeTemplateKey(templateKey);
|
|
515
579
|
selectedTemplate = templates.find(
|
|
516
|
-
(template) => normalizeTemplateKey(template.key) === normalizedKey
|
|
580
|
+
(template) => normalizeTemplateKey(template.key) === normalizedKey,
|
|
517
581
|
);
|
|
518
582
|
if (!selectedTemplate) {
|
|
519
583
|
const keys = templates.map((template) => template.key).join(", ");
|
|
@@ -526,7 +590,7 @@ export async function scaffold({ templateKey, targetDir }) {
|
|
|
526
590
|
const optionalRules = await loadOptionalPackageRules();
|
|
527
591
|
const optionalPackages = collectOptionalPackages(
|
|
528
592
|
optionalRules,
|
|
529
|
-
selectedTemplate.key
|
|
593
|
+
selectedTemplate.key,
|
|
530
594
|
);
|
|
531
595
|
const selectedOptionalPackages =
|
|
532
596
|
optionalPackages.length > 0
|
|
@@ -540,29 +604,29 @@ export async function scaffold({ templateKey, targetDir }) {
|
|
|
540
604
|
try {
|
|
541
605
|
if (interactive) {
|
|
542
606
|
buildSpinner = ora({
|
|
543
|
-
text: "正在生成项目...",
|
|
607
|
+
text: pc.cyan("🚀 正在生成项目..."),
|
|
544
608
|
spinner: "dots",
|
|
545
609
|
interval: 50,
|
|
546
610
|
}).start();
|
|
547
611
|
}
|
|
548
612
|
if (buildSpinner) {
|
|
549
|
-
buildSpinner.text = "
|
|
613
|
+
buildSpinner.text = pc.cyan("📦 正在初始化项目目录 (1/3)");
|
|
550
614
|
}
|
|
551
615
|
await ensureEmptyDir(targetPath);
|
|
552
616
|
if (buildSpinner) {
|
|
553
|
-
buildSpinner.text = "
|
|
617
|
+
buildSpinner.text = pc.cyan("🚚 正在复制模板文件 (2/3)");
|
|
554
618
|
}
|
|
555
619
|
await copyDir(selectedTemplate.dir, targetPath);
|
|
556
620
|
if (buildSpinner) {
|
|
557
|
-
buildSpinner.text = "
|
|
621
|
+
buildSpinner.text = pc.cyan("🔧 正在应用可选依赖 (3/3)");
|
|
558
622
|
}
|
|
559
623
|
await applyOptionalPackages(targetPath, selectedOptionalPackages);
|
|
560
624
|
if (buildSpinner) {
|
|
561
|
-
buildSpinner.succeed("
|
|
625
|
+
buildSpinner.succeed(pc.green("🎉 项目生成完成"));
|
|
562
626
|
}
|
|
563
627
|
} catch (error) {
|
|
564
628
|
if (buildSpinner) {
|
|
565
|
-
buildSpinner.fail("项目生成失败");
|
|
629
|
+
buildSpinner.fail(pc.red("❌ 项目生成失败"));
|
|
566
630
|
}
|
|
567
631
|
throw error;
|
|
568
632
|
}
|
|
@@ -582,12 +646,16 @@ export async function scaffold({ templateKey, targetDir }) {
|
|
|
582
646
|
}
|
|
583
647
|
|
|
584
648
|
if (interactive) {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
649
|
+
note(
|
|
650
|
+
buildNextStepsBlock({
|
|
651
|
+
selectedTemplateKey: selectedTemplate.key,
|
|
652
|
+
selectedOptionalPackages,
|
|
653
|
+
resolvedTargetDir,
|
|
654
|
+
packageManager,
|
|
655
|
+
}),
|
|
656
|
+
"✨ 下一步",
|
|
590
657
|
);
|
|
658
|
+
outro(pc.green(`✅ 完成:${selectedTemplate.key}`));
|
|
591
659
|
} else {
|
|
592
660
|
console.log(`Created project in ${targetPath}`);
|
|
593
661
|
console.log("Next steps:");
|
package/optional-packages.json
CHANGED
|
@@ -11,7 +11,52 @@
|
|
|
11
11
|
{
|
|
12
12
|
"name": "mysql2",
|
|
13
13
|
"dev": false,
|
|
14
|
-
"description": "MySQL
|
|
14
|
+
"description": "MySQL驱动"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"match": "backend/nest",
|
|
20
|
+
"packages": [
|
|
21
|
+
{
|
|
22
|
+
"name": "prisma",
|
|
23
|
+
"dev": true,
|
|
24
|
+
"description": "Prisma CLI"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "@prisma/client",
|
|
28
|
+
"dev": false,
|
|
29
|
+
"description": "Prisma runtime client"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"name": "ioredis",
|
|
33
|
+
"dev": false,
|
|
34
|
+
"description": "Redis client"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "amqplib",
|
|
38
|
+
"dev": false,
|
|
39
|
+
"description": "RabbitMQ client"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "@types/amqplib",
|
|
43
|
+
"dev": true,
|
|
44
|
+
"description": "Type definitions for amqplib"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "class-validator",
|
|
48
|
+
"dev": false,
|
|
49
|
+
"description": "Validation decorators"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "class-transformer",
|
|
53
|
+
"dev": false,
|
|
54
|
+
"description": "Class object transformation"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"name": "axios",
|
|
58
|
+
"dev": false,
|
|
59
|
+
"description": "HTTP client"
|
|
15
60
|
}
|
|
16
61
|
]
|
|
17
62
|
},
|
|
@@ -21,7 +66,7 @@
|
|
|
21
66
|
{
|
|
22
67
|
"name": "axios",
|
|
23
68
|
"dev": false,
|
|
24
|
-
"description": "HTTP
|
|
69
|
+
"description": "HTTP请求库"
|
|
25
70
|
},
|
|
26
71
|
{
|
|
27
72
|
"name": "eslint",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guchenyo/create-temp",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Project scaffolding CLI (templates pulled from GitHub)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@clack/prompts": "^0.7.0",
|
|
21
21
|
"gradient-string": "^2.0.2",
|
|
22
|
-
"ora": "^8.0.1"
|
|
22
|
+
"ora": "^8.0.1",
|
|
23
|
+
"picocolors": "^1.1.1"
|
|
23
24
|
}
|
|
24
25
|
}
|