fwinit 1.1.2 → 1.3.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.
Files changed (2) hide show
  1. package/dist/index.js +296 -73
  2. package/package.json +1 -1
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,6 +276,17 @@ async function askProjectName() {
249
276
  ]);
250
277
  return name.trim();
251
278
  }
279
+ async function askLayerLayout(layer) {
280
+ const { layout } = await inquirer.prompt([
281
+ {
282
+ type: "confirm",
283
+ name: "layout",
284
+ message: layer === "backend" ? "\xBFEmpaquetar el c\xF3digo del template en una carpeta backend/?" : "\xBFEmpaquetar el c\xF3digo del template en una carpeta frontend/?",
285
+ default: true
286
+ }
287
+ ]);
288
+ return layout;
289
+ }
252
290
 
253
291
  // src/utils.ts
254
292
  import { existsSync } from "fs";
@@ -311,20 +349,38 @@ function resolveCommand(cmd, pm) {
311
349
  return cmd;
312
350
  }
313
351
  }
314
- function printSuccess(projectName, templateName, pm, postInit) {
352
+ function printSuccess(projectName, templateName, pm, postInit, codeDir) {
315
353
  const installCmd = postInit?.install ? resolveCommand(postInit.install, pm) : `${pm} install`;
316
354
  const devCmd = postInit?.dev ? resolveCommand(postInit.dev, pm) : `${pm} run dev`;
355
+ const target = codeDir ? `${projectName}/${codeDir}` : projectName;
317
356
  console.log(`
318
357
  \x1B[32m\u2714\x1B[0m Template descargado
319
- \x1B[32m\u2714\x1B[0m Proyecto creado en ./${projectName}
358
+ \x1B[32m\u2714\x1B[0m Proyecto creado en ./${target}
320
359
 
321
360
  \x1B[1mPr\xF3ximos pasos:\x1B[0m
322
361
 
323
- \x1B[36mcd ${projectName}\x1B[0m
362
+ \x1B[36mcd ${target}\x1B[0m
324
363
  \x1B[36m${installCmd}\x1B[0m
325
364
  \x1B[36m${devCmd}\x1B[0m
326
365
  `);
327
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
+ }
328
384
  function adaptScriptToNode(script) {
329
385
  let s = script.trim();
330
386
  if (/^bun test\b/.test(s)) {
@@ -334,10 +390,10 @@ function adaptScriptToNode(script) {
334
390
  return s;
335
391
  }
336
392
  async function adaptToNodeRuntime(projectPath) {
337
- const { readFile: readFile2, writeFile: writeFile2, readdir: readdir2 } = await import("fs/promises");
393
+ const { readFile: readFile3, writeFile: writeFile2, readdir: readdir2 } = await import("fs/promises");
338
394
  const { join: join3 } = await import("path");
339
395
  const pkgPath = join3(projectPath, "package.json");
340
- const pkg = JSON.parse(await readFile2(pkgPath, "utf-8"));
396
+ const pkg = JSON.parse(await readFile3(pkgPath, "utf-8"));
341
397
  for (const key of Object.keys(pkg.scripts ?? {})) {
342
398
  pkg.scripts[key] = adaptScriptToNode(pkg.scripts[key]);
343
399
  }
@@ -353,7 +409,7 @@ async function adaptToNodeRuntime(projectPath) {
353
409
  if (entry.isDirectory()) {
354
410
  if (entry.name !== "node_modules") await walk(full);
355
411
  } else if (entry.name.endsWith(".test.ts")) {
356
- const content = await readFile2(full, "utf-8");
412
+ const content = await readFile3(full, "utf-8");
357
413
  if (content.includes("bun:test")) {
358
414
  await writeFile2(full, content.replaceAll('from "bun:test"', 'from "vitest"'));
359
415
  }
@@ -367,7 +423,7 @@ async function adaptToNodeRuntime(projectPath) {
367
423
  }
368
424
  const tsconfigPath = join3(projectPath, "tsconfig.json");
369
425
  try {
370
- const tsconfig = JSON.parse(await readFile2(tsconfigPath, "utf-8"));
426
+ const tsconfig = JSON.parse(await readFile3(tsconfigPath, "utf-8"));
371
427
  if (tsconfig.compilerOptions?.types?.includes("bun-types")) {
372
428
  delete tsconfig.compilerOptions.types;
373
429
  await writeFile2(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
@@ -375,6 +431,54 @@ async function adaptToNodeRuntime(projectPath) {
375
431
  } catch {
376
432
  }
377
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
+ }
378
482
 
379
483
  // src/index.ts
380
484
  var program = new Command();
@@ -412,22 +516,89 @@ async function installOpenCodeFiles(repoRoot, projectPath) {
412
516
  join2(projectPath, ".opencode", "commands", "create-specs.md")
413
517
  );
414
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
+ }
415
579
  program.name("fwinit").description("CLI para crear proyectos desde templates").version(getCliVersion());
416
580
  program.command("list").description("Mostrar templates disponibles").action(() => {
417
581
  console.log(
418
582
  chalk.bold("\nTemplates disponibles:\n")
419
583
  );
420
- TEMPLATES.forEach((t) => {
421
- console.log(
422
- ` ${chalk.cyan(t.folder.toLowerCase())} \u2014 ${t.description}`
423
- );
424
- });
425
- 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
+ }
426
597
  });
427
- 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").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(
428
599
  async (templateArg, projectNameArg) => {
429
600
  try {
430
- const { pm: pmArg } = program.opts();
601
+ const { pm: pmArg, backend, backendTemplate, frontendTemplate } = program.opts();
431
602
  if (pmArg && !PACKAGE_MANAGERS.includes(pmArg)) {
432
603
  console.error(
433
604
  chalk.red(
@@ -438,30 +609,83 @@ program.argument("[template]", "Template a usar").argument("[project-name]", "No
438
609
  );
439
610
  process.exit(1);
440
611
  }
441
- let template = templateArg ? findTemplate(templateArg) : void 0;
442
612
  let projectName = projectNameArg;
443
613
  let pm = pmArg;
444
- if (templateArg && !template) {
445
- console.error(
446
- chalk.red(
447
- `
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
+ `
448
632
  \u2716 Template "${templateArg}" no encontrado.
449
633
  `
450
- )
451
- );
452
- console.log("Templates disponibles:");
453
- TEMPLATES.forEach(
454
- (t) => console.log(` - ${t.folder.toLowerCase()}`)
455
- );
456
- 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
+ }
457
648
  }
458
- if (!template) {
459
- 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
+ }
460
680
  }
461
681
  if (!projectName) {
462
682
  projectName = await askProjectName();
463
683
  }
464
- 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) {
465
689
  pm = await selectPackageManager();
466
690
  }
467
691
  if (projectExists(projectName)) {
@@ -474,42 +698,41 @@ program.argument("[template]", "Template a usar").argument("[project-name]", "No
474
698
  process.exit(1);
475
699
  }
476
700
  const spinner = ora(
477
- `Descargando template ${template.name}...`
701
+ isFullStack ? "Descargando templates..." : `Descargando template ${template.name}...`
478
702
  ).start();
479
- const { tempDir, templatePath, repoRoot } = await downloadAndExtract(template.folder);
480
- spinner.text = "Creando proyecto...";
481
- await copyTemplate(
482
- templatePath,
483
- getProjectPath(projectName)
484
- );
485
- await substituteTemplate(
486
- getProjectPath(projectName),
487
- projectName,
488
- template.folder
489
- );
490
- await installOpenCodeFiles(
491
- repoRoot,
492
- getProjectPath(projectName)
493
- );
494
- if (template.runtime === "bun" && pm && pm !== "bun") {
495
- await adaptToNodeRuntime(getProjectPath(projectName));
496
- }
497
- await cleanup(tempDir);
498
- spinner.succeed("Template descargado");
499
- let postInit;
500
- try {
501
- const { readFile: readFile2 } = await import("fs/promises");
502
- const { join: join3 } = await import("path");
503
- const tj = JSON.parse(
504
- await readFile2(
505
- join3(getProjectPath(projectName), "template.json"),
506
- "utf-8"
507
- )
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
508
726
  );
509
- postInit = tj.postInit;
510
- } catch {
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
+ }
511
735
  }
512
- printSuccess(projectName, template.name, pm ?? "npm", postInit);
513
736
  } catch (error) {
514
737
  console.error(
515
738
  chalk.red(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fwinit",
3
- "version": "1.1.2",
3
+ "version": "1.3.0",
4
4
  "description": "CLI para crear proyectos desde templates",
5
5
  "type": "module",
6
6
  "bin": {