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.
- package/dist/index.js +296 -73
- 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
|
|
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,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 ./${
|
|
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 ${
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
459
|
-
|
|
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
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
510
|
-
|
|
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(
|