fwinit 1.2.0 → 1.3.1
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 +84 -22
- package/dist/index.js +283 -88
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# fwinit
|
|
2
2
|
|
|
3
|
-
CLI para crear proyectos desde templates. Instalás una vez y scaffoldás proyectos con un comando.
|
|
3
|
+
CLI para crear proyectos desde templates. Instalás una vez y scaffoldás proyectos con un comando: un solo template, o un proyecto full stack completo (backend + frontend + base de datos).
|
|
4
4
|
|
|
5
5
|
## Instalación
|
|
6
6
|
|
|
@@ -17,51 +17,112 @@ bun add -g fwinit
|
|
|
17
17
|
|
|
18
18
|
## Uso
|
|
19
19
|
|
|
20
|
-
### Modo directo
|
|
21
|
-
|
|
22
20
|
```bash
|
|
21
|
+
# Menú interactivo: elegís entre full stack, solo backend o solo frontend
|
|
22
|
+
fwinit
|
|
23
|
+
|
|
24
|
+
# Crear un solo template (backend o frontend)
|
|
23
25
|
fwinit fastify mi-api
|
|
24
26
|
fwinit aspnet mi-app
|
|
25
27
|
fwinit react-native mi-mobile
|
|
28
|
+
|
|
29
|
+
# Crear un proyecto completo: backend + frontend (+ db incluida)
|
|
30
|
+
fwinit fullstack mi-proyecto
|
|
31
|
+
|
|
32
|
+
# Elegir los templates sin preguntas (modo scripting/CI)
|
|
33
|
+
fwinit fullstack mi-proyecto -b fastify -f react-native -p pnpm
|
|
34
|
+
|
|
35
|
+
# Ver templates disponibles (agrupados por capa)
|
|
36
|
+
fwinit list
|
|
37
|
+
|
|
38
|
+
# Ayuda
|
|
39
|
+
fwinit --help
|
|
26
40
|
```
|
|
27
41
|
|
|
28
42
|
### Modo interactivo
|
|
29
43
|
|
|
30
|
-
|
|
31
|
-
|
|
44
|
+
`fwinit` sin argumentos abre el menú principal:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
? ¿Qué querés crear?
|
|
48
|
+
> Full stack (backend + frontend)
|
|
49
|
+
Solo backend (API)
|
|
50
|
+
Solo frontend (app)
|
|
32
51
|
```
|
|
33
52
|
|
|
34
|
-
|
|
53
|
+
- **Full stack**: te pregunta primero el backend y después el frontend, y arma el monorepo completo.
|
|
54
|
+
- **Solo backend / Solo frontend**: te pregunta el template de la capa, el nombre, el layout y el package manager.
|
|
35
55
|
|
|
36
|
-
###
|
|
56
|
+
### Proyecto full stack
|
|
37
57
|
|
|
38
58
|
```bash
|
|
39
|
-
fwinit
|
|
59
|
+
fwinit fullstack mi-proyecto
|
|
40
60
|
```
|
|
41
61
|
|
|
42
|
-
|
|
62
|
+
Pregunta qué backend querés (ASP.NET, Express, Fastify, Node.js Vanilla) y qué frontend (React Native). Genera:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
mi-proyecto/
|
|
66
|
+
├── backend/ # API (template elegido — trae su DB: Prisma o EF)
|
|
67
|
+
├── frontend/ # App (template frontend elegido)
|
|
68
|
+
├── .gitignore # .atl/ · odd · .opencode/
|
|
69
|
+
├── .opencode/ # Skills de opencode (create-specs incluida)
|
|
70
|
+
├── README.md # Cómo correr backend y frontend
|
|
71
|
+
└── specs/ # La creás con /create-specs
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
La base de datos no se configura aparte: viene incluida en el template backend (Prisma en los templates JS/TS, EF Core en ASP.NET).
|
|
75
|
+
|
|
76
|
+
### Layout de capa (monorepo liviano)
|
|
77
|
+
|
|
78
|
+
Al crear un solo template, el CLI pregunta si querés empaquetar el código en una carpeta de capa (default: sí):
|
|
43
79
|
|
|
44
80
|
```bash
|
|
45
|
-
|
|
81
|
+
# backend/ + .opencode/ + specs/ al mismo nivel
|
|
82
|
+
fwinit express mi-api
|
|
83
|
+
# raíz limpia (todo el código en mi-api/)
|
|
84
|
+
fwinit express mi-api --no-backend
|
|
85
|
+
# forzar el layout de capa sin preguntas
|
|
86
|
+
fwinit express mi-api --backend
|
|
46
87
|
```
|
|
47
88
|
|
|
89
|
+
Igual para frontend: `fwinit react-native mi-app` pregunta por `frontend/`.
|
|
90
|
+
|
|
91
|
+
## Flags
|
|
92
|
+
|
|
93
|
+
| Flag | Descripción |
|
|
94
|
+
|------|-------------|
|
|
95
|
+
| `-p, --pm <manager>` | Package manager: `npm`, `pnpm` o `bun` (saltea la pregunta) |
|
|
96
|
+
| `--backend` | Empaquetar el código en `backend/` (solo modo single, saltea la pregunta) |
|
|
97
|
+
| `--no-backend` | Dejar el código en la raíz del proyecto (saltea la pregunta) |
|
|
98
|
+
| `-b, --backend-template <tpl>` | Backend a usar en fullstack (saltea la pregunta) |
|
|
99
|
+
| `-f, --frontend-template <tpl>` | Frontend a usar en fullstack (saltea la pregunta) |
|
|
100
|
+
|
|
48
101
|
## Templates disponibles
|
|
49
102
|
|
|
50
|
-
| Template | Descripción | Runtime |
|
|
51
|
-
|
|
52
|
-
| `aspnet` | API REST con ASP.NET Core, Clean Architecture y C# | dotnet |
|
|
53
|
-
| `express` | API REST con Express, Prisma, TypeScript y Bun | bun |
|
|
54
|
-
| `fastify` | API REST con Fastify, Prisma, TypeScript y Bun | bun |
|
|
55
|
-
| `nodejs` | API con Node.js puro, Prisma, TypeScript y tsx | node |
|
|
56
|
-
| `react-native` | App móvil con React Native, Expo y TypeScript | node |
|
|
103
|
+
| Template | Capa | Descripción | Runtime |
|
|
104
|
+
|----------|------|-------------|---------|
|
|
105
|
+
| `aspnet` | backend | API REST con ASP.NET Core, Clean Architecture y C# | dotnet |
|
|
106
|
+
| `express` | backend | API REST con Express, Prisma, TypeScript y Bun | bun |
|
|
107
|
+
| `fastify` | backend | API REST con Fastify, Prisma, TypeScript y Bun | bun |
|
|
108
|
+
| `nodejs` | backend | API con Node.js puro, Prisma, TypeScript y tsx | node |
|
|
109
|
+
| `react-native` | frontend | App móvil con React Native, Expo y TypeScript | node |
|
|
110
|
+
|
|
111
|
+
La capa es metadata de cada template (`layer` en `templates/<TEMPLATE>/template.json`): para sumar un frontend nuevo (Astro, Next.js...), alcanza con agregar la carpeta del template y su `layer`. Los templates se clasifican por capa en la metadata, no por estructura de carpetas.
|
|
112
|
+
|
|
113
|
+
El runtime `bun` de un template se porta automáticamente a `node` (npm/pnpm) si elegís npm o pnpm: scripts con `tsx`, tests con `vitest` y sin `bun-types`.
|
|
57
114
|
|
|
58
115
|
## Qué hace
|
|
59
116
|
|
|
60
|
-
1. Descarga el ZIP del repositorio de templates desde GitHub
|
|
61
|
-
2. Extrae el template
|
|
62
|
-
3.
|
|
63
|
-
4.
|
|
64
|
-
5.
|
|
117
|
+
1. Descarga el ZIP del repositorio de templates desde GitHub (una sola vez, también en fullstack)
|
|
118
|
+
2. Extrae y copia el template (o los dos, `backend/` + `frontend/`) al proyecto
|
|
119
|
+
3. Sustituye el nombre del proyecto (package.json, app.json de Expo, proyectos ASP.NET con "Example")
|
|
120
|
+
4. Si corresponde, porta el template de runtime bun a node (npm/pnpm)
|
|
121
|
+
5. Instala `.opencode/` con la skill `create-specs` y el comando `/create-specs`
|
|
122
|
+
6. Limpia los lock files (cada proyecto instala los suyos)
|
|
123
|
+
7. Te muestra los próximos pasos (install y dev de cada carpeta)
|
|
124
|
+
|
|
125
|
+
En modo fullstack, además crea `.gitignore` y `README.md` en la raíz del monorepo.
|
|
65
126
|
|
|
66
127
|
## Desarrollo
|
|
67
128
|
|
|
@@ -93,6 +154,7 @@ bun run typecheck
|
|
|
93
154
|
```bash
|
|
94
155
|
node dist/index.js list
|
|
95
156
|
node dist/index.js fastify mi-api
|
|
157
|
+
node dist/index.js fullstack mi-proyecto -b fastify -f react-native -p pnpm
|
|
96
158
|
```
|
|
97
159
|
|
|
98
160
|
### Link global para desarrollo
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Command } from "commander";
|
|
|
5
5
|
import ora from "ora";
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
import { readFileSync } from "fs";
|
|
8
|
-
import { mkdir as mkdir2, cp as cp2, access } from "fs/promises";
|
|
8
|
+
import { mkdir as mkdir2, cp as cp2, access, readFile as readFile2 } from "fs/promises";
|
|
9
9
|
import { join as join2 } from "path";
|
|
10
10
|
|
|
11
11
|
// src/templates.ts
|
|
@@ -15,37 +15,45 @@ var TEMPLATES = [
|
|
|
15
15
|
alias: ["aspnet", "dotnet", "csharp"],
|
|
16
16
|
description: "API REST con ASP.NET Core, Clean Architecture y C#",
|
|
17
17
|
folder: "ASPNET",
|
|
18
|
-
runtime: "dotnet"
|
|
18
|
+
runtime: "dotnet",
|
|
19
|
+
layer: "backend"
|
|
19
20
|
},
|
|
20
21
|
{
|
|
21
22
|
name: "Express",
|
|
22
23
|
alias: ["express"],
|
|
23
24
|
description: "API REST con Express, Prisma, TypeScript y Bun",
|
|
24
25
|
folder: "EXPRESS",
|
|
25
|
-
runtime: "bun"
|
|
26
|
+
runtime: "bun",
|
|
27
|
+
layer: "backend"
|
|
26
28
|
},
|
|
27
29
|
{
|
|
28
30
|
name: "Fastify",
|
|
29
31
|
alias: ["fastify"],
|
|
30
32
|
description: "API REST con Fastify, Prisma, TypeScript y Bun",
|
|
31
33
|
folder: "FASTIFY",
|
|
32
|
-
runtime: "bun"
|
|
34
|
+
runtime: "bun",
|
|
35
|
+
layer: "backend"
|
|
33
36
|
},
|
|
34
37
|
{
|
|
35
38
|
name: "Node.js Vanilla",
|
|
36
39
|
alias: ["nodejs", "vanilla", "node"],
|
|
37
40
|
description: "API con Node.js puro, Prisma, TypeScript y tsx",
|
|
38
41
|
folder: "NODEJS-VANILLA",
|
|
39
|
-
runtime: "node"
|
|
42
|
+
runtime: "node",
|
|
43
|
+
layer: "backend"
|
|
40
44
|
},
|
|
41
45
|
{
|
|
42
46
|
name: "React Native",
|
|
43
47
|
alias: ["react-native", "rn", "expo"],
|
|
44
48
|
description: "App m\xF3vil con React Native, Expo y TypeScript",
|
|
45
49
|
folder: "REACT-NATIVE",
|
|
46
|
-
runtime: "node"
|
|
50
|
+
runtime: "node",
|
|
51
|
+
layer: "frontend"
|
|
47
52
|
}
|
|
48
53
|
];
|
|
54
|
+
function getTemplatesByLayer(layer) {
|
|
55
|
+
return TEMPLATES.filter((t) => t.layer === layer);
|
|
56
|
+
}
|
|
49
57
|
function findTemplate(input) {
|
|
50
58
|
const normalized = input.toLowerCase().trim();
|
|
51
59
|
return TEMPLATES.find(
|
|
@@ -61,7 +69,7 @@ import { tmpdir } from "os";
|
|
|
61
69
|
import { randomBytes } from "crypto";
|
|
62
70
|
var GITHUB_REPO = "orlandotellez/fwinit";
|
|
63
71
|
var GITHUB_BRANCH = "main";
|
|
64
|
-
async function
|
|
72
|
+
async function downloadAndExtractRepo() {
|
|
65
73
|
const url = `https://github.com/${GITHUB_REPO}/archive/refs/heads/${GITHUB_BRANCH}.zip`;
|
|
66
74
|
const response = await fetch(url);
|
|
67
75
|
if (!response.ok) {
|
|
@@ -76,6 +84,10 @@ async function downloadAndExtract(templateFolder) {
|
|
|
76
84
|
await mkdir(tempDir, { recursive: true });
|
|
77
85
|
zip.extractAllTo(tempDir, true);
|
|
78
86
|
const repoRoot = join(tempDir, `fwinit-${GITHUB_BRANCH}`);
|
|
87
|
+
return { tempDir, repoRoot };
|
|
88
|
+
}
|
|
89
|
+
async function downloadAndExtract(templateFolder) {
|
|
90
|
+
const { tempDir, repoRoot } = await downloadAndExtractRepo();
|
|
79
91
|
const templatePath = join(repoRoot, "templates", templateFolder);
|
|
80
92
|
try {
|
|
81
93
|
await readdir(templatePath);
|
|
@@ -202,13 +214,28 @@ function toPascalCase(str) {
|
|
|
202
214
|
|
|
203
215
|
// src/prompts.ts
|
|
204
216
|
import inquirer from "inquirer";
|
|
205
|
-
async function
|
|
217
|
+
async function selectScope() {
|
|
218
|
+
const { scope } = await inquirer.prompt([
|
|
219
|
+
{
|
|
220
|
+
type: "list",
|
|
221
|
+
name: "scope",
|
|
222
|
+
message: "\xBFQu\xE9 quer\xE9s crear?",
|
|
223
|
+
choices: [
|
|
224
|
+
{ name: "Full stack (backend + frontend)", value: "fullstack" },
|
|
225
|
+
{ name: "Solo backend (API)", value: "backend" },
|
|
226
|
+
{ name: "Solo frontend (app)", value: "frontend" }
|
|
227
|
+
]
|
|
228
|
+
}
|
|
229
|
+
]);
|
|
230
|
+
return scope;
|
|
231
|
+
}
|
|
232
|
+
async function selectTemplateFromLayer(layer) {
|
|
206
233
|
const { template } = await inquirer.prompt([
|
|
207
234
|
{
|
|
208
235
|
type: "list",
|
|
209
236
|
name: "template",
|
|
210
|
-
message: "\xBFQu\xE9
|
|
211
|
-
choices:
|
|
237
|
+
message: layer === "backend" ? "\xBFQu\xE9 backend quer\xE9s usar?" : "\xBFQu\xE9 frontend quer\xE9s usar?",
|
|
238
|
+
choices: getTemplatesByLayer(layer).map((t) => ({
|
|
212
239
|
name: `${t.name} \u2014 ${t.description}`,
|
|
213
240
|
value: t
|
|
214
241
|
}))
|
|
@@ -249,12 +276,12 @@ async function askProjectName() {
|
|
|
249
276
|
]);
|
|
250
277
|
return name.trim();
|
|
251
278
|
}
|
|
252
|
-
async function
|
|
279
|
+
async function askLayerLayout(layer) {
|
|
253
280
|
const { layout } = await inquirer.prompt([
|
|
254
281
|
{
|
|
255
282
|
type: "confirm",
|
|
256
283
|
name: "layout",
|
|
257
|
-
message: "\xBFEmpaquetar el c\xF3digo del template en una carpeta backend/?",
|
|
284
|
+
message: layer === "backend" ? "\xBFEmpaquetar el c\xF3digo del template en una carpeta backend/?" : "\xBFEmpaquetar el c\xF3digo del template en una carpeta frontend/?",
|
|
258
285
|
default: true
|
|
259
286
|
}
|
|
260
287
|
]);
|
|
@@ -337,6 +364,23 @@ function printSuccess(projectName, templateName, pm, postInit, codeDir) {
|
|
|
337
364
|
\x1B[36m${devCmd}\x1B[0m
|
|
338
365
|
`);
|
|
339
366
|
}
|
|
367
|
+
function printSuccessFullStack(projectName, layers, pm) {
|
|
368
|
+
const blocks = layers.map((l) => {
|
|
369
|
+
const installCmd = l.postInit?.install ? resolveCommand(l.postInit.install, pm) : `${pm} install`;
|
|
370
|
+
const devCmd = l.postInit?.dev ? resolveCommand(l.postInit.dev, pm) : `${pm} run dev`;
|
|
371
|
+
return ` \x1B[36mcd ${projectName}/${l.label}\x1B[0m
|
|
372
|
+
\x1B[36m${installCmd}\x1B[0m
|
|
373
|
+
\x1B[36m${devCmd}\x1B[0m`;
|
|
374
|
+
}).join("\n\n");
|
|
375
|
+
console.log(`
|
|
376
|
+
\x1B[32m\u2714\x1B[0m Template descargado
|
|
377
|
+
\x1B[32m\u2714\x1B[0m Proyecto creado en ./${projectName}
|
|
378
|
+
|
|
379
|
+
\x1B[1mPr\xF3ximos pasos:\x1B[0m
|
|
380
|
+
|
|
381
|
+
${blocks}
|
|
382
|
+
`);
|
|
383
|
+
}
|
|
340
384
|
function adaptScriptToNode(script) {
|
|
341
385
|
let s = script.trim();
|
|
342
386
|
if (/^bun test\b/.test(s)) {
|
|
@@ -346,10 +390,10 @@ function adaptScriptToNode(script) {
|
|
|
346
390
|
return s;
|
|
347
391
|
}
|
|
348
392
|
async function adaptToNodeRuntime(projectPath) {
|
|
349
|
-
const { readFile:
|
|
393
|
+
const { readFile: readFile3, writeFile: writeFile2, readdir: readdir2 } = await import("fs/promises");
|
|
350
394
|
const { join: join3 } = await import("path");
|
|
351
395
|
const pkgPath = join3(projectPath, "package.json");
|
|
352
|
-
const pkg = JSON.parse(await
|
|
396
|
+
const pkg = JSON.parse(await readFile3(pkgPath, "utf-8"));
|
|
353
397
|
for (const key of Object.keys(pkg.scripts ?? {})) {
|
|
354
398
|
pkg.scripts[key] = adaptScriptToNode(pkg.scripts[key]);
|
|
355
399
|
}
|
|
@@ -365,7 +409,7 @@ async function adaptToNodeRuntime(projectPath) {
|
|
|
365
409
|
if (entry.isDirectory()) {
|
|
366
410
|
if (entry.name !== "node_modules") await walk(full);
|
|
367
411
|
} else if (entry.name.endsWith(".test.ts")) {
|
|
368
|
-
const content = await
|
|
412
|
+
const content = await readFile3(full, "utf-8");
|
|
369
413
|
if (content.includes("bun:test")) {
|
|
370
414
|
await writeFile2(full, content.replaceAll('from "bun:test"', 'from "vitest"'));
|
|
371
415
|
}
|
|
@@ -379,7 +423,7 @@ async function adaptToNodeRuntime(projectPath) {
|
|
|
379
423
|
}
|
|
380
424
|
const tsconfigPath = join3(projectPath, "tsconfig.json");
|
|
381
425
|
try {
|
|
382
|
-
const tsconfig = JSON.parse(await
|
|
426
|
+
const tsconfig = JSON.parse(await readFile3(tsconfigPath, "utf-8"));
|
|
383
427
|
if (tsconfig.compilerOptions?.types?.includes("bun-types")) {
|
|
384
428
|
delete tsconfig.compilerOptions.types;
|
|
385
429
|
await writeFile2(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
|
|
@@ -387,6 +431,54 @@ async function adaptToNodeRuntime(projectPath) {
|
|
|
387
431
|
} catch {
|
|
388
432
|
}
|
|
389
433
|
}
|
|
434
|
+
async function writeRootFiles(info) {
|
|
435
|
+
const { writeFile: writeFile2, access: access2 } = await import("fs/promises");
|
|
436
|
+
const { join: join3 } = await import("path");
|
|
437
|
+
const projectPath = resolve(process.cwd(), info.projectName);
|
|
438
|
+
const gitignorePath = join3(projectPath, ".gitignore");
|
|
439
|
+
try {
|
|
440
|
+
await access2(gitignorePath);
|
|
441
|
+
} catch {
|
|
442
|
+
await writeFile2(
|
|
443
|
+
gitignorePath,
|
|
444
|
+
"# AI dev state\n.atl/\nodd\n.opencode/\n"
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
const installB = info.backend.postInit?.install ? resolveCommand(info.backend.postInit.install, info.pm) : `${info.pm} install`;
|
|
448
|
+
const devB = info.backend.postInit?.dev ? resolveCommand(info.backend.postInit.dev, info.pm) : `${info.pm} run dev`;
|
|
449
|
+
const installF = info.frontend.postInit?.install ? resolveCommand(info.frontend.postInit.install, info.pm) : `${info.pm} install`;
|
|
450
|
+
const devF = info.frontend.postInit?.dev ? resolveCommand(info.frontend.postInit.dev, info.pm) : `${info.pm} run dev`;
|
|
451
|
+
const readme = [
|
|
452
|
+
`# ${info.projectName}`,
|
|
453
|
+
"",
|
|
454
|
+
"Proyecto full stack generado con [fwinit](https://github.com/orlandotellez/fwinit).",
|
|
455
|
+
"",
|
|
456
|
+
"## Estructura",
|
|
457
|
+
"",
|
|
458
|
+
`- \`backend/\` \u2014 ${info.backend.description}`,
|
|
459
|
+
`- \`frontend/\` \u2014 ${info.frontend.description}`,
|
|
460
|
+
"- `.opencode/` \u2014 skills de opencode (us\xE1 `/create-specs` para generar las specs)",
|
|
461
|
+
"- `specs/` \u2014 especificaciones del proyecto",
|
|
462
|
+
"",
|
|
463
|
+
"## Backend",
|
|
464
|
+
"",
|
|
465
|
+
"```bash",
|
|
466
|
+
"cd backend",
|
|
467
|
+
installB,
|
|
468
|
+
devB,
|
|
469
|
+
"```",
|
|
470
|
+
"",
|
|
471
|
+
"## Frontend",
|
|
472
|
+
"",
|
|
473
|
+
"```bash",
|
|
474
|
+
"cd frontend",
|
|
475
|
+
installF,
|
|
476
|
+
devF,
|
|
477
|
+
"```",
|
|
478
|
+
""
|
|
479
|
+
].join("\n");
|
|
480
|
+
await writeFile2(join3(projectPath, "README.md"), readme);
|
|
481
|
+
}
|
|
390
482
|
|
|
391
483
|
// src/index.ts
|
|
392
484
|
var program = new Command();
|
|
@@ -424,22 +516,89 @@ async function installOpenCodeFiles(repoRoot, projectPath) {
|
|
|
424
516
|
join2(projectPath, ".opencode", "commands", "create-specs.md")
|
|
425
517
|
);
|
|
426
518
|
}
|
|
519
|
+
async function readTemplatePostInit(dir) {
|
|
520
|
+
try {
|
|
521
|
+
const tj = JSON.parse(
|
|
522
|
+
await readFile2(join2(dir, "template.json"), "utf-8")
|
|
523
|
+
);
|
|
524
|
+
return tj.postInit;
|
|
525
|
+
} catch {
|
|
526
|
+
return void 0;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
async function createFullStackProject(projectName, backendTpl, frontendTpl, pm) {
|
|
530
|
+
const { tempDir, repoRoot } = await downloadAndExtractRepo();
|
|
531
|
+
const projectPath = getProjectPath(projectName);
|
|
532
|
+
const backendPath = join2(projectPath, "backend");
|
|
533
|
+
const frontendPath = join2(projectPath, "frontend");
|
|
534
|
+
await copyTemplate(
|
|
535
|
+
join2(repoRoot, "templates", backendTpl.folder),
|
|
536
|
+
backendPath
|
|
537
|
+
);
|
|
538
|
+
await copyTemplate(
|
|
539
|
+
join2(repoRoot, "templates", frontendTpl.folder),
|
|
540
|
+
frontendPath
|
|
541
|
+
);
|
|
542
|
+
await substituteTemplate(backendPath, projectName, backendTpl.folder);
|
|
543
|
+
await substituteTemplate(frontendPath, projectName, frontendTpl.folder);
|
|
544
|
+
await installOpenCodeFiles(repoRoot, projectPath);
|
|
545
|
+
if (backendTpl.runtime === "bun" && pm !== "bun") {
|
|
546
|
+
await adaptToNodeRuntime(backendPath);
|
|
547
|
+
}
|
|
548
|
+
const backendPost = await readTemplatePostInit(backendPath);
|
|
549
|
+
const frontendPost = await readTemplatePostInit(frontendPath);
|
|
550
|
+
await writeRootFiles({
|
|
551
|
+
projectName,
|
|
552
|
+
backend: {
|
|
553
|
+
label: "backend",
|
|
554
|
+
description: backendTpl.description,
|
|
555
|
+
postInit: backendPost
|
|
556
|
+
},
|
|
557
|
+
frontend: {
|
|
558
|
+
label: "frontend",
|
|
559
|
+
description: frontendTpl.description,
|
|
560
|
+
postInit: frontendPost
|
|
561
|
+
},
|
|
562
|
+
pm
|
|
563
|
+
});
|
|
564
|
+
await cleanup(tempDir);
|
|
565
|
+
printSuccessFullStack(
|
|
566
|
+
projectName,
|
|
567
|
+
[
|
|
568
|
+
{ label: "backend", postInit: backendPost },
|
|
569
|
+
{ label: "frontend", postInit: frontendPost }
|
|
570
|
+
],
|
|
571
|
+
pm
|
|
572
|
+
);
|
|
573
|
+
console.log(
|
|
574
|
+
chalk.dim(
|
|
575
|
+
" Estructura: backend/ (API) \xB7 frontend/ (app) \xB7 .opencode/ y specs/ (al mismo nivel)\n"
|
|
576
|
+
)
|
|
577
|
+
);
|
|
578
|
+
}
|
|
427
579
|
program.name("fwinit").description("CLI para crear proyectos desde templates").version(getCliVersion());
|
|
428
580
|
program.command("list").description("Mostrar templates disponibles").action(() => {
|
|
429
581
|
console.log(
|
|
430
582
|
chalk.bold("\nTemplates disponibles:\n")
|
|
431
583
|
);
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
584
|
+
for (const [label, layer] of [
|
|
585
|
+
["Backend", "backend"],
|
|
586
|
+
["Frontend", "frontend"]
|
|
587
|
+
]) {
|
|
588
|
+
console.log(chalk.bold(`${label}:
|
|
589
|
+
`));
|
|
590
|
+
getTemplatesByLayer(layer).forEach((t) => {
|
|
591
|
+
console.log(
|
|
592
|
+
` ${chalk.cyan(t.folder.toLowerCase())} \u2014 ${t.description}`
|
|
593
|
+
);
|
|
594
|
+
});
|
|
595
|
+
console.log();
|
|
596
|
+
}
|
|
438
597
|
});
|
|
439
|
-
program.argument("[template]", "Template a usar").argument("[project-name]", "Nombre del proyecto").option("-p, --pm <package-manager>", "Package manager a usar: npm, pnpm o bun").option("--backend", "Empaquetar el c\xF3digo en backend/ (saltea la pregunta)").option("--no-backend", "Dejar el c\xF3digo en la ra\xEDz del proyecto (saltea la pregunta)").action(
|
|
598
|
+
program.argument("[template]", "Template a usar (o 'fullstack' para backend + frontend)").argument("[project-name]", "Nombre del proyecto").option("-p, --pm <package-manager>", "Package manager a usar: npm, pnpm o bun").option("--backend", "Empaquetar el c\xF3digo en backend/ (saltea la pregunta)").option("--no-backend", "Dejar el c\xF3digo en la ra\xEDz del proyecto (saltea la pregunta)").option("-b, --backend-template <tpl>", "Backend a usar en fullstack (saltea la pregunta)").option("-f, --frontend-template <tpl>", "Frontend a usar en fullstack (saltea la pregunta)").action(
|
|
440
599
|
async (templateArg, projectNameArg) => {
|
|
441
600
|
try {
|
|
442
|
-
const { pm: pmArg, backend } = program.opts();
|
|
601
|
+
const { pm: pmArg, backend, backendTemplate, frontendTemplate } = program.opts();
|
|
443
602
|
if (pmArg && !PACKAGE_MANAGERS.includes(pmArg)) {
|
|
444
603
|
console.error(
|
|
445
604
|
chalk.red(
|
|
@@ -450,31 +609,83 @@ program.argument("[template]", "Template a usar").argument("[project-name]", "No
|
|
|
450
609
|
);
|
|
451
610
|
process.exit(1);
|
|
452
611
|
}
|
|
453
|
-
let template = templateArg ? findTemplate(templateArg) : void 0;
|
|
454
612
|
let projectName = projectNameArg;
|
|
455
613
|
let pm = pmArg;
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
614
|
+
let isFullStack = false;
|
|
615
|
+
let template;
|
|
616
|
+
const rawArg = templateArg?.toLowerCase();
|
|
617
|
+
if (rawArg === void 0) {
|
|
618
|
+
const scope = await selectScope();
|
|
619
|
+
if (scope === "fullstack") {
|
|
620
|
+
isFullStack = true;
|
|
621
|
+
} else {
|
|
622
|
+
template = await selectTemplateFromLayer(scope);
|
|
623
|
+
}
|
|
624
|
+
} else if (rawArg === "fullstack") {
|
|
625
|
+
isFullStack = true;
|
|
626
|
+
} else {
|
|
627
|
+
template = findTemplate(rawArg);
|
|
628
|
+
if (!template) {
|
|
629
|
+
console.error(
|
|
630
|
+
chalk.red(
|
|
631
|
+
`
|
|
460
632
|
\u2716 Template "${templateArg}" no encontrado.
|
|
461
633
|
`
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
634
|
+
)
|
|
635
|
+
);
|
|
636
|
+
console.log("Templates disponibles:");
|
|
637
|
+
for (const [label, layer2] of [
|
|
638
|
+
["Backend", "backend"],
|
|
639
|
+
["Frontend", "frontend"]
|
|
640
|
+
]) {
|
|
641
|
+
console.log(chalk.bold(`${label}:`));
|
|
642
|
+
getTemplatesByLayer(layer2).forEach(
|
|
643
|
+
(t) => console.log(` - ${t.folder.toLowerCase()}`)
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
469
648
|
}
|
|
470
|
-
|
|
471
|
-
|
|
649
|
+
let backendTpl;
|
|
650
|
+
let frontendTpl;
|
|
651
|
+
if (isFullStack) {
|
|
652
|
+
backendTpl = backendTemplate ? findTemplate(backendTemplate) : void 0;
|
|
653
|
+
frontendTpl = frontendTemplate ? findTemplate(frontendTemplate) : void 0;
|
|
654
|
+
if (backendTemplate && !backendTpl) {
|
|
655
|
+
console.error(
|
|
656
|
+
chalk.red(
|
|
657
|
+
`
|
|
658
|
+
\u2716 Backend "${backendTemplate}" no encontrado.
|
|
659
|
+
`
|
|
660
|
+
)
|
|
661
|
+
);
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
if (frontendTemplate && !frontendTpl) {
|
|
665
|
+
console.error(
|
|
666
|
+
chalk.red(
|
|
667
|
+
`
|
|
668
|
+
\u2716 Frontend "${frontendTemplate}" no encontrado.
|
|
669
|
+
`
|
|
670
|
+
)
|
|
671
|
+
);
|
|
672
|
+
process.exit(1);
|
|
673
|
+
}
|
|
674
|
+
if (!backendTpl) {
|
|
675
|
+
backendTpl = await selectTemplateFromLayer("backend");
|
|
676
|
+
}
|
|
677
|
+
if (!frontendTpl) {
|
|
678
|
+
frontendTpl = await selectTemplateFromLayer("frontend");
|
|
679
|
+
}
|
|
472
680
|
}
|
|
473
681
|
if (!projectName) {
|
|
474
682
|
projectName = await askProjectName();
|
|
475
683
|
}
|
|
476
|
-
const
|
|
477
|
-
|
|
684
|
+
const layer = template?.layer;
|
|
685
|
+
const useLayerLayout = !template ? false : backend === void 0 ? await askLayerLayout(layer) : backend;
|
|
686
|
+
const runtimes = template ? [template.runtime] : [backendTpl.runtime, frontendTpl.runtime];
|
|
687
|
+
const needsPm = runtimes.some((r) => r !== "dotnet");
|
|
688
|
+
if (!pm && needsPm) {
|
|
478
689
|
pm = await selectPackageManager();
|
|
479
690
|
}
|
|
480
691
|
if (projectExists(projectName)) {
|
|
@@ -487,56 +698,40 @@ program.argument("[template]", "Template a usar").argument("[project-name]", "No
|
|
|
487
698
|
process.exit(1);
|
|
488
699
|
}
|
|
489
700
|
const spinner = ora(
|
|
490
|
-
`Descargando template ${template.name}...`
|
|
701
|
+
isFullStack ? "Descargando templates..." : `Descargando template ${template.name}...`
|
|
491
702
|
).start();
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
templatePath,
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
codePath
|
|
502
|
-
projectName,
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
try {
|
|
516
|
-
const { readFile: readFile2 } = await import("fs/promises");
|
|
517
|
-
const { join: join3 } = await import("path");
|
|
518
|
-
const tj = JSON.parse(
|
|
519
|
-
await readFile2(
|
|
520
|
-
join3(codePath, "template.json"),
|
|
521
|
-
"utf-8"
|
|
522
|
-
)
|
|
523
|
-
);
|
|
524
|
-
postInit = tj.postInit;
|
|
525
|
-
} catch {
|
|
526
|
-
}
|
|
527
|
-
printSuccess(
|
|
528
|
-
projectName,
|
|
529
|
-
template.name,
|
|
530
|
-
pm ?? "npm",
|
|
531
|
-
postInit,
|
|
532
|
-
useBackendLayout ? "backend" : void 0
|
|
533
|
-
);
|
|
534
|
-
if (useBackendLayout) {
|
|
535
|
-
console.log(
|
|
536
|
-
chalk.dim(
|
|
537
|
-
" Estructura: backend/ (c\xF3digo) \xB7 .opencode/ y specs/ (al mismo nivel)\n"
|
|
538
|
-
)
|
|
703
|
+
if (isFullStack) {
|
|
704
|
+
spinner.text = "Creando proyecto...";
|
|
705
|
+
await createFullStackProject(projectName, backendTpl, frontendTpl, pm ?? "npm");
|
|
706
|
+
spinner.succeed("Template descargado");
|
|
707
|
+
} else {
|
|
708
|
+
const { tempDir, templatePath, repoRoot } = await downloadAndExtract(template.folder);
|
|
709
|
+
const projectPath = getProjectPath(projectName);
|
|
710
|
+
const codePath = useLayerLayout ? join2(projectPath, layer) : projectPath;
|
|
711
|
+
spinner.text = "Creando proyecto...";
|
|
712
|
+
await copyTemplate(templatePath, codePath);
|
|
713
|
+
await substituteTemplate(codePath, projectName, template.folder);
|
|
714
|
+
await installOpenCodeFiles(repoRoot, projectPath);
|
|
715
|
+
if (template.runtime === "bun" && pm && pm !== "bun") {
|
|
716
|
+
await adaptToNodeRuntime(codePath);
|
|
717
|
+
}
|
|
718
|
+
await cleanup(tempDir);
|
|
719
|
+
spinner.succeed("Template descargado");
|
|
720
|
+
printSuccess(
|
|
721
|
+
projectName,
|
|
722
|
+
template.name,
|
|
723
|
+
pm ?? "npm",
|
|
724
|
+
await readTemplatePostInit(codePath),
|
|
725
|
+
useLayerLayout ? layer : void 0
|
|
539
726
|
);
|
|
727
|
+
if (useLayerLayout) {
|
|
728
|
+
console.log(
|
|
729
|
+
chalk.dim(
|
|
730
|
+
` Estructura: ${layer}/ (c\xF3digo) \xB7 .opencode/ y specs/ (al mismo nivel)
|
|
731
|
+
`
|
|
732
|
+
)
|
|
733
|
+
);
|
|
734
|
+
}
|
|
540
735
|
}
|
|
541
736
|
} catch (error) {
|
|
542
737
|
console.error(
|