@guchen_0521/create-temp 0.1.4 → 0.1.6
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 +6 -0
- package/lib/scaffold.js +116 -24
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -23,6 +23,12 @@ Use with npm create:
|
|
|
23
23
|
npm create @guchen_0521/temp@latest -- backend/express/express-no-ts my-api
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
Interactive selection (TUI):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm create @guchen_0521/temp@latest
|
|
30
|
+
```
|
|
31
|
+
|
|
26
32
|
## Requirements
|
|
27
33
|
|
|
28
34
|
- Git installed and available in PATH
|
package/lib/scaffold.js
CHANGED
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
stat,
|
|
8
8
|
} from "node:fs/promises";
|
|
9
9
|
import { execFile } from "node:child_process";
|
|
10
|
-
import { createInterface } from "node:readline/promises";
|
|
11
10
|
import { promisify } from "node:util";
|
|
12
11
|
import os from "node:os";
|
|
13
12
|
import path from "node:path";
|
|
13
|
+
import prompts from "prompts";
|
|
14
14
|
|
|
15
15
|
const IGNORE_NAMES = new Set([
|
|
16
16
|
".git",
|
|
@@ -27,6 +27,16 @@ const TEMPLATE_REPO_URL =
|
|
|
27
27
|
const CLONE_RETRIES = 3;
|
|
28
28
|
const GIT_SSH_COMMAND =
|
|
29
29
|
"ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=6";
|
|
30
|
+
const PROMPT_CANCELLED_MESSAGE = "Prompt cancelled.";
|
|
31
|
+
const PROMPT_BACK_VALUE = "__back";
|
|
32
|
+
|
|
33
|
+
function assertInteractive() {
|
|
34
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"Interactive prompts require a TTY. Pass template and target arguments."
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
30
40
|
|
|
31
41
|
async function cloneTemplateRepo() {
|
|
32
42
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), "template-cli-"));
|
|
@@ -101,33 +111,105 @@ function normalizeTemplateKey(templateKey) {
|
|
|
101
111
|
.replace(/^\.\/+/, "");
|
|
102
112
|
}
|
|
103
113
|
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
function buildTemplateTree(templates) {
|
|
115
|
+
const root = { name: "", children: new Map(), template: null };
|
|
116
|
+
templates.forEach((template) => {
|
|
117
|
+
const parts = template.key.split("/");
|
|
118
|
+
let node = root;
|
|
119
|
+
parts.forEach((part, index) => {
|
|
120
|
+
if (!node.children.has(part)) {
|
|
121
|
+
node.children.set(part, {
|
|
122
|
+
name: part,
|
|
123
|
+
children: new Map(),
|
|
124
|
+
template: null,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
node = node.children.get(part);
|
|
128
|
+
if (index === parts.length - 1) {
|
|
129
|
+
node.template = template;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
109
132
|
});
|
|
133
|
+
return root;
|
|
134
|
+
}
|
|
110
135
|
|
|
111
|
-
|
|
112
|
-
|
|
136
|
+
async function promptSelectTemplateTree(templates) {
|
|
137
|
+
assertInteractive();
|
|
138
|
+
const tree = buildTemplateTree(templates);
|
|
139
|
+
const parents = [];
|
|
140
|
+
let current = tree;
|
|
113
141
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
142
|
+
while (true) {
|
|
143
|
+
const choices = Array.from(current.children.values())
|
|
144
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
145
|
+
.map((node) => ({
|
|
146
|
+
title: node.name,
|
|
147
|
+
value: node.name,
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
if (parents.length > 0) {
|
|
151
|
+
choices.unshift({ title: "<- Back", value: PROMPT_BACK_VALUE });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const { selection } = await prompts(
|
|
155
|
+
{
|
|
156
|
+
type: "select",
|
|
157
|
+
name: "selection",
|
|
158
|
+
message:
|
|
159
|
+
parents.length === 0
|
|
160
|
+
? "Select a template category:"
|
|
161
|
+
: "Select a folder:",
|
|
162
|
+
choices,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
onCancel: () => {
|
|
166
|
+
throw new Error(PROMPT_CANCELLED_MESSAGE);
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
);
|
|
118
170
|
|
|
119
|
-
|
|
171
|
+
if (!selection) {
|
|
172
|
+
throw new Error("Invalid template selection.");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (selection === PROMPT_BACK_VALUE) {
|
|
176
|
+
current = parents.pop();
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const next = current.children.get(selection);
|
|
181
|
+
if (!next) {
|
|
182
|
+
throw new Error("Invalid template selection.");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (next.template) {
|
|
186
|
+
return next.template;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
parents.push(current);
|
|
190
|
+
current = next;
|
|
191
|
+
}
|
|
120
192
|
}
|
|
121
193
|
|
|
122
194
|
async function promptTargetDir() {
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
195
|
+
assertInteractive();
|
|
196
|
+
const response = await prompts(
|
|
197
|
+
{
|
|
198
|
+
type: "text",
|
|
199
|
+
name: "targetDir",
|
|
200
|
+
message: "Project folder name:",
|
|
201
|
+
validate: (value) =>
|
|
202
|
+
value && value.trim().length > 0
|
|
203
|
+
? true
|
|
204
|
+
: "Project folder name is required.",
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
onCancel: () => {
|
|
208
|
+
throw new Error(PROMPT_CANCELLED_MESSAGE);
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
return response.targetDir.trim();
|
|
131
213
|
}
|
|
132
214
|
|
|
133
215
|
async function ensureEmptyDir(targetDir) {
|
|
@@ -215,7 +297,7 @@ export async function scaffold({ templateKey, targetDir }) {
|
|
|
215
297
|
throw new Error(`Template not found. Available: ${keys}`);
|
|
216
298
|
}
|
|
217
299
|
} else {
|
|
218
|
-
selectedTemplate = await
|
|
300
|
+
selectedTemplate = await promptSelectTemplateTree(templates);
|
|
219
301
|
}
|
|
220
302
|
|
|
221
303
|
const resolvedTargetDir = targetDir || (await promptTargetDir());
|
|
@@ -228,9 +310,19 @@ export async function scaffold({ templateKey, targetDir }) {
|
|
|
228
310
|
await rm(tempDir, { recursive: true, force: true });
|
|
229
311
|
}
|
|
230
312
|
|
|
313
|
+
const userAgent = process.env.npm_config_user_agent || "";
|
|
314
|
+
let packageManager = "npm";
|
|
315
|
+
if (userAgent.includes("pnpm")) {
|
|
316
|
+
packageManager = "pnpm";
|
|
317
|
+
} else if (userAgent.includes("yarn")) {
|
|
318
|
+
packageManager = "yarn";
|
|
319
|
+
} else if (userAgent.includes("bun")) {
|
|
320
|
+
packageManager = "bun";
|
|
321
|
+
}
|
|
322
|
+
|
|
231
323
|
console.log(`Created project in ${targetPath}`);
|
|
232
324
|
console.log("Next steps:");
|
|
233
325
|
console.log(` cd ${resolvedTargetDir}`);
|
|
234
|
-
console.log(
|
|
235
|
-
console.log(
|
|
326
|
+
console.log(` ${packageManager} install`);
|
|
327
|
+
console.log(` ${packageManager} run dev`);
|
|
236
328
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guchen_0521/create-temp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Project scaffolding CLI (templates pulled from GitHub)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,5 +14,8 @@
|
|
|
14
14
|
],
|
|
15
15
|
"engines": {
|
|
16
16
|
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"prompts": "^2.4.2"
|
|
17
20
|
}
|
|
18
21
|
}
|