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.
Files changed (3) hide show
  1. package/README.md +84 -22
  2. package/dist/index.js +283 -88
  3. 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
- ```bash
31
- fwinit
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
- Te pregunta qué template usar y el nombre del proyecto.
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
- ### Ver templates disponibles
56
+ ### Proyecto full stack
37
57
 
38
58
  ```bash
39
- fwinit list
59
+ fwinit fullstack mi-proyecto
40
60
  ```
41
61
 
42
- ### Help
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
- fwinit --help
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 seleccionado
62
- 3. Lo copia al directorio del proyecto
63
- 4. Limpia los lock files (cada proyecto instala los suyos)
64
- 5. Te muestra los próximos pasos (install y dev)
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 downloadAndExtract(templateFolder) {
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 selectTemplate() {
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 proyecto quer\xE9s crear?",
211
- choices: TEMPLATES.map((t) => ({
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 askBackendLayout() {
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: readFile2, writeFile: writeFile2, readdir: readdir2 } = await import("fs/promises");
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 readFile2(pkgPath, "utf-8"));
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 readFile2(full, "utf-8");
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 readFile2(tsconfigPath, "utf-8"));
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
- TEMPLATES.forEach((t) => {
433
- console.log(
434
- ` ${chalk.cyan(t.folder.toLowerCase())} \u2014 ${t.description}`
435
- );
436
- });
437
- console.log();
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
- if (templateArg && !template) {
457
- console.error(
458
- chalk.red(
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
- console.log("Templates disponibles:");
465
- TEMPLATES.forEach(
466
- (t) => console.log(` - ${t.folder.toLowerCase()}`)
467
- );
468
- process.exit(1);
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
- if (!template) {
471
- template = await selectTemplate();
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 useBackendLayout = backend === void 0 ? await askBackendLayout() : backend;
477
- if (!pm && template.runtime !== "dotnet") {
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
- const { tempDir, templatePath, repoRoot } = await downloadAndExtract(template.folder);
493
- const projectPath = getProjectPath(projectName);
494
- const codePath = useBackendLayout ? join2(projectPath, "backend") : projectPath;
495
- spinner.text = "Creando proyecto...";
496
- await copyTemplate(
497
- templatePath,
498
- codePath
499
- );
500
- await substituteTemplate(
501
- codePath,
502
- projectName,
503
- template.folder
504
- );
505
- await installOpenCodeFiles(
506
- repoRoot,
507
- projectPath
508
- );
509
- if (template.runtime === "bun" && pm && pm !== "bun") {
510
- await adaptToNodeRuntime(codePath);
511
- }
512
- await cleanup(tempDir);
513
- spinner.succeed("Template descargado");
514
- let postInit;
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(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fwinit",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "CLI para crear proyectos desde templates",
5
5
  "type": "module",
6
6
  "bin": {