@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 +35 -0
- package/bin/cli.js +16 -0
- package/lib/scaffold.js +207 -0
- package/package.json +18 -0
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
|
+
}
|
package/lib/scaffold.js
ADDED
|
@@ -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
|
+
}
|