@guchen_0521/create-temp 0.1.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 ADDED
@@ -0,0 +1,35 @@
1
+ # create-temp
2
+
3
+ Local template scaffolding CLI for this repository. It clones the latest
4
+ templates from GitHub on each run (SSH access required).
5
+
6
+ ## Usage
7
+
8
+ ```bash
9
+ npm link
10
+ create-temp
11
+ ```
12
+
13
+ Or pass args directly:
14
+
15
+ ```bash
16
+ create-temp backend/express/express-no-ts my-api
17
+ create-temp front/web/vue3/vue3-no-ts my-web
18
+ ```
19
+
20
+ Use with npm create:
21
+
22
+ ```bash
23
+ npm create @guchen_0521/temp@latest -- backend/express/express-no-ts my-api
24
+ ```
25
+
26
+ ## Requirements
27
+
28
+ - Git installed and available in PATH
29
+ - SSH access to `ssh://git@ssh.github.com:443/GuChena/template.git`
30
+
31
+ ## Publish
32
+
33
+ ```bash
34
+ npm publish --access public
35
+ ```
package/bin/cli.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { scaffold } from "../lib/scaffold.js";
3
+
4
+ const args = process.argv.slice(2);
5
+ const [templateArg, targetArg] = args;
6
+
7
+ try {
8
+ await scaffold({
9
+ templateKey: templateArg,
10
+ targetDir: targetArg,
11
+ });
12
+ } catch (error) {
13
+ const message = error instanceof Error ? error.message : String(error);
14
+ console.error(message);
15
+ process.exit(1);
16
+ }
@@ -0,0 +1,207 @@
1
+ import {
2
+ copyFile,
3
+ mkdir,
4
+ mkdtemp,
5
+ readdir,
6
+ rm,
7
+ stat,
8
+ } from "node:fs/promises";
9
+ import { execFile } from "node:child_process";
10
+ import { createInterface } from "node:readline/promises";
11
+ import { promisify } from "node:util";
12
+ import os from "node:os";
13
+ import path from "node:path";
14
+
15
+ const IGNORE_NAMES = new Set([
16
+ ".git",
17
+ "node_modules",
18
+ ".DS_Store",
19
+ ".pnpm",
20
+ ".pnpm-store",
21
+ ]);
22
+
23
+ const execFileAsync = promisify(execFile);
24
+ const TEMPLATE_REPO_SSH = "ssh://git@ssh.github.com:443/GuChena/template.git";
25
+
26
+ async function cloneTemplateRepo() {
27
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), "template-cli-"));
28
+ const repoDir = path.join(tempDir, `repo-${Date.now().toString(36)}`);
29
+
30
+ await execFileAsync("git", [
31
+ "clone",
32
+ "--depth",
33
+ "1",
34
+ TEMPLATE_REPO_SSH,
35
+ repoDir,
36
+ ]);
37
+
38
+ return { repoDir, tempDir };
39
+ }
40
+
41
+ async function findTemplateDirs(rootDir) {
42
+ const results = [];
43
+ const entries = await readdir(rootDir, { withFileTypes: true });
44
+
45
+ for (const entry of entries) {
46
+ if (!entry.isDirectory() || IGNORE_NAMES.has(entry.name)) {
47
+ continue;
48
+ }
49
+
50
+ const fullPath = path.join(rootDir, entry.name);
51
+ const packageJsonPath = path.join(fullPath, "package.json");
52
+ try {
53
+ const info = await stat(packageJsonPath);
54
+ if (info.isFile()) {
55
+ results.push(fullPath);
56
+ continue;
57
+ }
58
+ } catch {
59
+ // Not a template root; keep searching.
60
+ }
61
+
62
+ const nested = await findTemplateDirs(fullPath);
63
+ results.push(...nested);
64
+ }
65
+
66
+ return results;
67
+ }
68
+
69
+ function formatTemplateKey(projectRoot, templateDir) {
70
+ return path
71
+ .relative(projectRoot, templateDir)
72
+ .split(path.sep)
73
+ .join("/");
74
+ }
75
+
76
+ async function promptSelectTemplate(templates) {
77
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
78
+ console.log("Available templates:");
79
+ templates.forEach((template, index) => {
80
+ console.log(` ${index + 1}) ${template.key}`);
81
+ });
82
+
83
+ const answer = await rl.question("Select a template number: ");
84
+ rl.close();
85
+
86
+ const choice = Number.parseInt(answer.trim(), 10);
87
+ if (!Number.isInteger(choice) || choice < 1 || choice > templates.length) {
88
+ throw new Error("Invalid template selection.");
89
+ }
90
+
91
+ return templates[choice - 1];
92
+ }
93
+
94
+ async function promptTargetDir() {
95
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
96
+ const answer = await rl.question("Project folder name: ");
97
+ rl.close();
98
+ const trimmed = answer.trim();
99
+ if (!trimmed) {
100
+ throw new Error("Project folder name is required.");
101
+ }
102
+ return trimmed;
103
+ }
104
+
105
+ async function ensureEmptyDir(targetDir) {
106
+ try {
107
+ const items = await readdir(targetDir);
108
+ if (items.length > 0) {
109
+ throw new Error(`Target directory is not empty: ${targetDir}`);
110
+ }
111
+ } catch (error) {
112
+ if (error && error.code === "ENOENT") {
113
+ await mkdir(targetDir, { recursive: true });
114
+ return;
115
+ }
116
+ throw error;
117
+ }
118
+ }
119
+
120
+ async function copyDir(sourceDir, targetDir) {
121
+ await mkdir(targetDir, { recursive: true });
122
+ const entries = await readdir(sourceDir, { withFileTypes: true });
123
+
124
+ for (const entry of entries) {
125
+ if (IGNORE_NAMES.has(entry.name)) {
126
+ continue;
127
+ }
128
+
129
+ const sourcePath = path.join(sourceDir, entry.name);
130
+ const targetPath = path.join(targetDir, entry.name);
131
+
132
+ if (entry.isDirectory()) {
133
+ await copyDir(sourcePath, targetPath);
134
+ } else if (entry.isFile()) {
135
+ await mkdir(path.dirname(targetPath), { recursive: true });
136
+ await copyFile(sourcePath, targetPath);
137
+ }
138
+ }
139
+ }
140
+
141
+ export async function scaffold({ templateKey, targetDir }) {
142
+ let repoDir = null;
143
+ let tempDir = null;
144
+ try {
145
+ const cloneResult = await cloneTemplateRepo();
146
+ repoDir = cloneResult.repoDir;
147
+ tempDir = cloneResult.tempDir;
148
+ } catch (error) {
149
+ const message = error instanceof Error ? error.message : String(error);
150
+ throw new Error(
151
+ `Failed to clone template repo. Ensure SSH access works. ${message}`
152
+ );
153
+ }
154
+
155
+ const templateRoots = [
156
+ path.join(repoDir, "backend"),
157
+ path.join(repoDir, "front"),
158
+ ];
159
+
160
+ const templates = [];
161
+ for (const root of templateRoots) {
162
+ try {
163
+ const dirs = await findTemplateDirs(root);
164
+ dirs.forEach((dir) => {
165
+ templates.push({
166
+ key: formatTemplateKey(repoDir, dir),
167
+ dir,
168
+ });
169
+ });
170
+ } catch {
171
+ // Missing template root is fine.
172
+ }
173
+ }
174
+
175
+ if (templates.length === 0) {
176
+ throw new Error("No templates found under backend/ or front/.");
177
+ }
178
+
179
+ let selectedTemplate = null;
180
+ if (templateKey) {
181
+ selectedTemplate = templates.find(
182
+ (template) => template.key === templateKey
183
+ );
184
+ if (!selectedTemplate) {
185
+ const keys = templates.map((template) => template.key).join(", ");
186
+ throw new Error(`Template not found. Available: ${keys}`);
187
+ }
188
+ } else {
189
+ selectedTemplate = await promptSelectTemplate(templates);
190
+ }
191
+
192
+ const resolvedTargetDir = targetDir || (await promptTargetDir());
193
+ const targetPath = path.resolve(process.cwd(), resolvedTargetDir);
194
+
195
+ await ensureEmptyDir(targetPath);
196
+ await copyDir(selectedTemplate.dir, targetPath);
197
+
198
+ if (tempDir) {
199
+ await rm(tempDir, { recursive: true, force: true });
200
+ }
201
+
202
+ console.log(`Created project in ${targetPath}`);
203
+ console.log("Next steps:");
204
+ console.log(` cd ${resolvedTargetDir}`);
205
+ console.log(" npm install");
206
+ console.log(" npm run dev");
207
+ }
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@guchen_0521/create-temp",
3
+ "version": "0.1.0",
4
+ "description": "Project scaffolding CLI (templates pulled from GitHub)",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-temp": "bin/cli.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "lib",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ }
18
+ }