fwinit 1.3.0 → 1.3.2
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 +64 -9
- 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
|
@@ -6,7 +6,7 @@ import ora from "ora";
|
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
import { readFileSync } from "fs";
|
|
8
8
|
import { mkdir as mkdir2, cp as cp2, access, readFile as readFile2 } from "fs/promises";
|
|
9
|
-
import { join as join2 } from "path";
|
|
9
|
+
import { join as join2, basename } from "path";
|
|
10
10
|
|
|
11
11
|
// src/templates.ts
|
|
12
12
|
var TEMPLATES = [
|
|
@@ -243,6 +243,17 @@ async function selectTemplateFromLayer(layer) {
|
|
|
243
243
|
]);
|
|
244
244
|
return template;
|
|
245
245
|
}
|
|
246
|
+
async function askGitInit() {
|
|
247
|
+
const { initGit } = await inquirer.prompt([
|
|
248
|
+
{
|
|
249
|
+
type: "confirm",
|
|
250
|
+
name: "initGit",
|
|
251
|
+
message: "\xBFInicializar un repositorio git?",
|
|
252
|
+
default: true
|
|
253
|
+
}
|
|
254
|
+
]);
|
|
255
|
+
return initGit;
|
|
256
|
+
}
|
|
246
257
|
async function selectPackageManager() {
|
|
247
258
|
const { pm } = await inquirer.prompt([
|
|
248
259
|
{
|
|
@@ -431,10 +442,9 @@ async function adaptToNodeRuntime(projectPath) {
|
|
|
431
442
|
} catch {
|
|
432
443
|
}
|
|
433
444
|
}
|
|
434
|
-
async function
|
|
445
|
+
async function writeRootGitignore(projectPath) {
|
|
435
446
|
const { writeFile: writeFile2, access: access2 } = await import("fs/promises");
|
|
436
447
|
const { join: join3 } = await import("path");
|
|
437
|
-
const projectPath = resolve(process.cwd(), info.projectName);
|
|
438
448
|
const gitignorePath = join3(projectPath, ".gitignore");
|
|
439
449
|
try {
|
|
440
450
|
await access2(gitignorePath);
|
|
@@ -444,6 +454,28 @@ async function writeRootFiles(info) {
|
|
|
444
454
|
"# AI dev state\n.atl/\nodd\n.opencode/\n"
|
|
445
455
|
);
|
|
446
456
|
}
|
|
457
|
+
}
|
|
458
|
+
async function gitInit(projectPath) {
|
|
459
|
+
const { execFile } = await import("child_process");
|
|
460
|
+
try {
|
|
461
|
+
await new Promise((resolve2, reject) => {
|
|
462
|
+
execFile(
|
|
463
|
+
"git",
|
|
464
|
+
["init"],
|
|
465
|
+
{ cwd: projectPath },
|
|
466
|
+
(err) => err ? reject(err) : resolve2()
|
|
467
|
+
);
|
|
468
|
+
});
|
|
469
|
+
return true;
|
|
470
|
+
} catch {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async function writeRootFiles(info) {
|
|
475
|
+
const { writeFile: writeFile2 } = await import("fs/promises");
|
|
476
|
+
const { join: join3 } = await import("path");
|
|
477
|
+
const projectPath = resolve(process.cwd(), info.projectName);
|
|
478
|
+
await writeRootGitignore(projectPath);
|
|
447
479
|
const installB = info.backend.postInit?.install ? resolveCommand(info.backend.postInit.install, info.pm) : `${info.pm} install`;
|
|
448
480
|
const devB = info.backend.postInit?.dev ? resolveCommand(info.backend.postInit.dev, info.pm) : `${info.pm} run dev`;
|
|
449
481
|
const installF = info.frontend.postInit?.install ? resolveCommand(info.frontend.postInit.install, info.pm) : `${info.pm} install`;
|
|
@@ -526,9 +558,9 @@ async function readTemplatePostInit(dir) {
|
|
|
526
558
|
return void 0;
|
|
527
559
|
}
|
|
528
560
|
}
|
|
529
|
-
async function createFullStackProject(
|
|
561
|
+
async function createFullStackProject(projectPath, backendTpl, frontendTpl, pm) {
|
|
530
562
|
const { tempDir, repoRoot } = await downloadAndExtractRepo();
|
|
531
|
-
const
|
|
563
|
+
const projectName = basename(projectPath);
|
|
532
564
|
const backendPath = join2(projectPath, "backend");
|
|
533
565
|
const frontendPath = join2(projectPath, "frontend");
|
|
534
566
|
await copyTemplate(
|
|
@@ -595,10 +627,10 @@ program.command("list").description("Mostrar templates disponibles").action(() =
|
|
|
595
627
|
console.log();
|
|
596
628
|
}
|
|
597
629
|
});
|
|
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(
|
|
630
|
+
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)").option("--git", "Inicializar un repositorio git (saltea la pregunta)").option("--no-git", "No inicializar git (saltea la pregunta)").action(
|
|
599
631
|
async (templateArg, projectNameArg) => {
|
|
600
632
|
try {
|
|
601
|
-
const { pm: pmArg, backend, backendTemplate, frontendTemplate } = program.opts();
|
|
633
|
+
const { pm: pmArg, backend, backendTemplate, frontendTemplate, git } = program.opts();
|
|
602
634
|
if (pmArg && !PACKAGE_MANAGERS.includes(pmArg)) {
|
|
603
635
|
console.error(
|
|
604
636
|
chalk.red(
|
|
@@ -688,6 +720,7 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
|
|
|
688
720
|
if (!pm && needsPm) {
|
|
689
721
|
pm = await selectPackageManager();
|
|
690
722
|
}
|
|
723
|
+
const wantsGit = git === void 0 ? await askGitInit() : git;
|
|
691
724
|
if (projectExists(projectName)) {
|
|
692
725
|
console.error(
|
|
693
726
|
chalk.red(
|
|
@@ -697,21 +730,29 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
|
|
|
697
730
|
);
|
|
698
731
|
process.exit(1);
|
|
699
732
|
}
|
|
733
|
+
const projectPath = getProjectPath(projectName);
|
|
700
734
|
const spinner = ora(
|
|
701
735
|
isFullStack ? "Descargando templates..." : `Descargando template ${template.name}...`
|
|
702
736
|
).start();
|
|
703
737
|
if (isFullStack) {
|
|
704
738
|
spinner.text = "Creando proyecto...";
|
|
705
|
-
await createFullStackProject(
|
|
739
|
+
await createFullStackProject(
|
|
740
|
+
projectPath,
|
|
741
|
+
backendTpl,
|
|
742
|
+
frontendTpl,
|
|
743
|
+
pm ?? "npm"
|
|
744
|
+
);
|
|
706
745
|
spinner.succeed("Template descargado");
|
|
707
746
|
} else {
|
|
708
747
|
const { tempDir, templatePath, repoRoot } = await downloadAndExtract(template.folder);
|
|
709
|
-
const projectPath = getProjectPath(projectName);
|
|
710
748
|
const codePath = useLayerLayout ? join2(projectPath, layer) : projectPath;
|
|
711
749
|
spinner.text = "Creando proyecto...";
|
|
712
750
|
await copyTemplate(templatePath, codePath);
|
|
713
751
|
await substituteTemplate(codePath, projectName, template.folder);
|
|
714
752
|
await installOpenCodeFiles(repoRoot, projectPath);
|
|
753
|
+
if (useLayerLayout) {
|
|
754
|
+
await writeRootGitignore(projectPath);
|
|
755
|
+
}
|
|
715
756
|
if (template.runtime === "bun" && pm && pm !== "bun") {
|
|
716
757
|
await adaptToNodeRuntime(codePath);
|
|
717
758
|
}
|
|
@@ -733,6 +774,20 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
|
|
|
733
774
|
);
|
|
734
775
|
}
|
|
735
776
|
}
|
|
777
|
+
if (wantsGit) {
|
|
778
|
+
const ok = await gitInit(projectPath);
|
|
779
|
+
if (ok) {
|
|
780
|
+
console.log(
|
|
781
|
+
chalk.dim(" Git: repositorio inicializado\n")
|
|
782
|
+
);
|
|
783
|
+
} else {
|
|
784
|
+
console.log(
|
|
785
|
+
chalk.yellow(
|
|
786
|
+
" \u26A0 No se pudo inicializar git (\xBFest\xE1 instalado?)\n"
|
|
787
|
+
)
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
736
791
|
} catch (error) {
|
|
737
792
|
console.error(
|
|
738
793
|
chalk.red(
|