fwinit 1.0.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 +87 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +247 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# fwinit
|
|
2
|
+
|
|
3
|
+
CLI para crear proyectos desde templates. Instalás una vez y scaffoldás proyectos con un comando.
|
|
4
|
+
|
|
5
|
+
## Instalación
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g fwinit
|
|
9
|
+
# o
|
|
10
|
+
bun add -g fwinit
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Uso
|
|
14
|
+
|
|
15
|
+
### Modo directo
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
fwinit fastify mi-api
|
|
19
|
+
fwinit aspnet mi-app
|
|
20
|
+
fwinit react-native mi-mobile
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Modo interactivo
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
fwinit
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Te pregunta qué template usar y el nombre del proyecto.
|
|
30
|
+
|
|
31
|
+
### Ver templates disponibles
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
fwinit list
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Help
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
fwinit --help
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Templates disponibles
|
|
44
|
+
|
|
45
|
+
| Template | Descripción | Runtime |
|
|
46
|
+
|----------|-------------|---------|
|
|
47
|
+
| `aspnet` | API REST con ASP.NET Core, Clean Architecture y C# | dotnet |
|
|
48
|
+
| `express` | API REST con Express, Prisma, TypeScript y Bun | bun |
|
|
49
|
+
| `fastify` | API REST con Fastify, Prisma, TypeScript y Bun | bun |
|
|
50
|
+
| `nodejs` | API con Node.js puro, Prisma, TypeScript y tsx | node |
|
|
51
|
+
| `react-native` | App móvil con React Native, Expo y TypeScript | node |
|
|
52
|
+
|
|
53
|
+
## Qué hace
|
|
54
|
+
|
|
55
|
+
1. Descarga el ZIP del repositorio de templates desde GitHub
|
|
56
|
+
2. Extrae el template seleccionado
|
|
57
|
+
3. Lo copia al directorio del proyecto
|
|
58
|
+
4. Limpia los lock files (cada proyecto instala los suyos)
|
|
59
|
+
5. Te muestra los próximos pasos (install y dev)
|
|
60
|
+
|
|
61
|
+
## Desarrollo
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
bun install
|
|
65
|
+
bun run build # build a dist/
|
|
66
|
+
bun run dev # watch mode
|
|
67
|
+
bun run start # ejecutar desde dist/
|
|
68
|
+
bun run typecheck # type check
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Probar localmente
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
node dist/index.js list
|
|
75
|
+
node dist/index.js fastify mi-api
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Link global para desarrollo
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
bun link
|
|
82
|
+
# ahora `fwinit` está disponible en todo el sistema
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Licencia
|
|
86
|
+
|
|
87
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import ora from "ora";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
|
|
8
|
+
// src/templates.ts
|
|
9
|
+
var TEMPLATES = [
|
|
10
|
+
{
|
|
11
|
+
name: "ASP.NET",
|
|
12
|
+
alias: ["aspnet", "dotnet", "csharp"],
|
|
13
|
+
description: "API REST con ASP.NET Core, Clean Architecture y C#",
|
|
14
|
+
folder: "ASPNET"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "Express",
|
|
18
|
+
alias: ["express"],
|
|
19
|
+
description: "API REST con Express, Prisma, TypeScript y Bun",
|
|
20
|
+
folder: "EXPRESS"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "Fastify",
|
|
24
|
+
alias: ["fastify"],
|
|
25
|
+
description: "API REST con Fastify, Prisma, TypeScript y Bun",
|
|
26
|
+
folder: "FASTIFY"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "Node.js Vanilla",
|
|
30
|
+
alias: ["nodejs", "vanilla", "node"],
|
|
31
|
+
description: "API con Node.js puro, Prisma, TypeScript y tsx",
|
|
32
|
+
folder: "NODEJS-VANILLA"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "React Native",
|
|
36
|
+
alias: ["react-native", "rn", "expo"],
|
|
37
|
+
description: "App m\xF3vil con React Native, Expo y TypeScript",
|
|
38
|
+
folder: "REACT-NATIVE"
|
|
39
|
+
}
|
|
40
|
+
];
|
|
41
|
+
function findTemplate(input) {
|
|
42
|
+
const normalized = input.toLowerCase().trim();
|
|
43
|
+
return TEMPLATES.find(
|
|
44
|
+
(t) => t.folder.toLowerCase() === normalized || t.alias.includes(normalized)
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/downloader.ts
|
|
49
|
+
import AdmZip from "adm-zip";
|
|
50
|
+
import { mkdir, rm, cp, readdir } from "fs/promises";
|
|
51
|
+
import { join } from "path";
|
|
52
|
+
import { tmpdir } from "os";
|
|
53
|
+
import { randomBytes } from "crypto";
|
|
54
|
+
var GITHUB_REPO = "orlandotellez/fwinit";
|
|
55
|
+
var GITHUB_BRANCH = "main";
|
|
56
|
+
async function downloadAndExtract(templateFolder) {
|
|
57
|
+
const url = `https://github.com/${GITHUB_REPO}/archive/refs/heads/${GITHUB_BRANCH}.zip`;
|
|
58
|
+
const response = await fetch(url);
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Failed to download template: ${response.statusText}`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
65
|
+
const zip = new AdmZip(buffer);
|
|
66
|
+
const tempId = randomBytes(8).toString("hex");
|
|
67
|
+
const tempDir = join(tmpdir(), `fwinit-${tempId}`);
|
|
68
|
+
await mkdir(tempDir, { recursive: true });
|
|
69
|
+
zip.extractAllTo(tempDir, true);
|
|
70
|
+
const repoRoot = join(tempDir, `fwinit-${GITHUB_BRANCH}`);
|
|
71
|
+
const templatePath = join(repoRoot, "templates", templateFolder);
|
|
72
|
+
try {
|
|
73
|
+
await readdir(templatePath);
|
|
74
|
+
} catch {
|
|
75
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Template "${templateFolder}" not found in repository`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return { tempDir, templatePath };
|
|
81
|
+
}
|
|
82
|
+
async function copyTemplate(source, destination) {
|
|
83
|
+
await cp(source, destination, { recursive: true });
|
|
84
|
+
await cleanTemplate(destination);
|
|
85
|
+
}
|
|
86
|
+
async function cleanTemplate(dir) {
|
|
87
|
+
const lockFiles = [
|
|
88
|
+
"bun.lock",
|
|
89
|
+
"pnpm-lock.yaml",
|
|
90
|
+
"package-lock.json",
|
|
91
|
+
"yarn.lock"
|
|
92
|
+
];
|
|
93
|
+
for (const file of lockFiles) {
|
|
94
|
+
const path = join(dir, file);
|
|
95
|
+
try {
|
|
96
|
+
await rm(path, { force: true });
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function cleanup(tempDir) {
|
|
102
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/prompts.ts
|
|
106
|
+
import inquirer from "inquirer";
|
|
107
|
+
async function selectTemplate() {
|
|
108
|
+
const { template } = await inquirer.prompt([
|
|
109
|
+
{
|
|
110
|
+
type: "list",
|
|
111
|
+
name: "template",
|
|
112
|
+
message: "What project do you want to create?",
|
|
113
|
+
choices: TEMPLATES.map((t) => ({
|
|
114
|
+
name: `${t.name} \u2014 ${t.description}`,
|
|
115
|
+
value: t
|
|
116
|
+
}))
|
|
117
|
+
}
|
|
118
|
+
]);
|
|
119
|
+
return template;
|
|
120
|
+
}
|
|
121
|
+
async function askProjectName() {
|
|
122
|
+
const { name } = await inquirer.prompt([
|
|
123
|
+
{
|
|
124
|
+
type: "input",
|
|
125
|
+
name: "name",
|
|
126
|
+
message: "Project name:",
|
|
127
|
+
validate: (input) => {
|
|
128
|
+
if (!input.trim()) return "Name cannot be empty";
|
|
129
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(input)) {
|
|
130
|
+
return "Only letters, numbers, hyphens and underscores";
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
]);
|
|
136
|
+
return name.trim();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/utils.ts
|
|
140
|
+
import { existsSync } from "fs";
|
|
141
|
+
import { resolve } from "path";
|
|
142
|
+
function getProjectPath(name) {
|
|
143
|
+
return resolve(process.cwd(), name);
|
|
144
|
+
}
|
|
145
|
+
function projectExists(name) {
|
|
146
|
+
return existsSync(getProjectPath(name));
|
|
147
|
+
}
|
|
148
|
+
function printSuccess(projectName, templateName, postInit) {
|
|
149
|
+
const installCmd = postInit?.install || "npm install";
|
|
150
|
+
const devCmd = postInit?.dev || "npm run dev";
|
|
151
|
+
console.log(`
|
|
152
|
+
\x1B[32m\u2714\x1B[0m Template downloaded
|
|
153
|
+
\x1B[32m\u2714\x1B[0m Project created in ./${projectName}
|
|
154
|
+
|
|
155
|
+
\x1B[1mNext steps:\x1B[0m
|
|
156
|
+
|
|
157
|
+
\x1B[36mcd ${projectName}\x1B[0m
|
|
158
|
+
\x1B[36m${installCmd}\x1B[0m
|
|
159
|
+
\x1B[36m${devCmd}\x1B[0m
|
|
160
|
+
`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/index.ts
|
|
164
|
+
var program = new Command();
|
|
165
|
+
program.name("fwinit").description("CLI to create projects from templates").version("1.0.0");
|
|
166
|
+
program.command("list").description("Show available templates").action(() => {
|
|
167
|
+
console.log(
|
|
168
|
+
chalk.bold("\nAvailable templates:\n")
|
|
169
|
+
);
|
|
170
|
+
TEMPLATES.forEach((t) => {
|
|
171
|
+
console.log(
|
|
172
|
+
` ${chalk.cyan(t.folder.toLowerCase())} \u2014 ${t.description}`
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
console.log();
|
|
176
|
+
});
|
|
177
|
+
program.argument("[template]", "Template to use").argument("[project-name]", "Project name").action(
|
|
178
|
+
async (templateArg, projectNameArg) => {
|
|
179
|
+
try {
|
|
180
|
+
let template = templateArg ? findTemplate(templateArg) : void 0;
|
|
181
|
+
let projectName = projectNameArg;
|
|
182
|
+
if (templateArg && !template) {
|
|
183
|
+
console.error(
|
|
184
|
+
chalk.red(
|
|
185
|
+
`
|
|
186
|
+
\u2716 Template "${templateArg}" not found.
|
|
187
|
+
`
|
|
188
|
+
)
|
|
189
|
+
);
|
|
190
|
+
console.log("Available templates:");
|
|
191
|
+
TEMPLATES.forEach(
|
|
192
|
+
(t) => console.log(` - ${t.folder.toLowerCase()}`)
|
|
193
|
+
);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
if (!template) {
|
|
197
|
+
template = await selectTemplate();
|
|
198
|
+
}
|
|
199
|
+
if (!projectName) {
|
|
200
|
+
projectName = await askProjectName();
|
|
201
|
+
}
|
|
202
|
+
if (projectExists(projectName)) {
|
|
203
|
+
console.error(
|
|
204
|
+
chalk.red(
|
|
205
|
+
`
|
|
206
|
+
\u2716 Directory "${projectName}" already exists.`
|
|
207
|
+
)
|
|
208
|
+
);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
const spinner = ora(
|
|
212
|
+
`Downloading ${template.name} template...`
|
|
213
|
+
).start();
|
|
214
|
+
const { tempDir, templatePath } = await downloadAndExtract(template.folder);
|
|
215
|
+
spinner.text = "Creating project...";
|
|
216
|
+
await copyTemplate(
|
|
217
|
+
templatePath,
|
|
218
|
+
getProjectPath(projectName)
|
|
219
|
+
);
|
|
220
|
+
await cleanup(tempDir);
|
|
221
|
+
spinner.succeed("Template downloaded");
|
|
222
|
+
let postInit;
|
|
223
|
+
try {
|
|
224
|
+
const { readFile } = await import("fs/promises");
|
|
225
|
+
const { join: join2 } = await import("path");
|
|
226
|
+
const tj = JSON.parse(
|
|
227
|
+
await readFile(
|
|
228
|
+
join2(getProjectPath(projectName), "template.json"),
|
|
229
|
+
"utf-8"
|
|
230
|
+
)
|
|
231
|
+
);
|
|
232
|
+
postInit = tj.postInit;
|
|
233
|
+
} catch {
|
|
234
|
+
}
|
|
235
|
+
printSuccess(projectName, template.name, postInit);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.error(
|
|
238
|
+
chalk.red(
|
|
239
|
+
`
|
|
240
|
+
\u2716 Error: ${error.message}`
|
|
241
|
+
)
|
|
242
|
+
);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fwinit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI para crear proyectos desde templates",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"fwinit": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
14
|
+
"dev": "tsup src/index.ts --format esm --watch",
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"prepublishOnly": "chmod +x dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"adm-zip": "^0.5.16",
|
|
21
|
+
"chalk": "^5.4.1",
|
|
22
|
+
"commander": "^13.1.0",
|
|
23
|
+
"inquirer": "^12.4.2",
|
|
24
|
+
"ora": "^8.2.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/adm-zip": "^0.5.7",
|
|
28
|
+
"@types/node": "^22.13.10",
|
|
29
|
+
"tsup": "^8.4.0",
|
|
30
|
+
"typescript": "^5.8.2"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"cli",
|
|
34
|
+
"templates",
|
|
35
|
+
"scaffolding",
|
|
36
|
+
"framework",
|
|
37
|
+
"boilerplate"
|
|
38
|
+
],
|
|
39
|
+
"author": "Orlando Téllez",
|
|
40
|
+
"license": "MIT"
|
|
41
|
+
}
|