@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 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
- async function promptSelectTemplate(templates) {
105
- const rl = createInterface({ input: process.stdin, output: process.stdout });
106
- console.log("Available templates:");
107
- templates.forEach((template, index) => {
108
- console.log(` ${index + 1}) ${template.key}`);
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
- const answer = await rl.question("Select a template number: ");
112
- rl.close();
136
+ async function promptSelectTemplateTree(templates) {
137
+ assertInteractive();
138
+ const tree = buildTemplateTree(templates);
139
+ const parents = [];
140
+ let current = tree;
113
141
 
114
- const choice = Number.parseInt(answer.trim(), 10);
115
- if (!Number.isInteger(choice) || choice < 1 || choice > templates.length) {
116
- throw new Error("Invalid template selection.");
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
- return templates[choice - 1];
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
- const rl = createInterface({ input: process.stdin, output: process.stdout });
124
- const answer = await rl.question("Project folder name: ");
125
- rl.close();
126
- const trimmed = answer.trim();
127
- if (!trimmed) {
128
- throw new Error("Project folder name is required.");
129
- }
130
- return trimmed;
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 promptSelectTemplate(templates);
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(" npm install");
235
- console.log(" npm run dev");
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.4",
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
  }