fwinit 1.4.5 → 1.4.7
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 +15 -0
- package/dist/index.js +358 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,6 +35,20 @@ fwinit fullstack mi-proyecto -b fastify -f react-native -p pnpm
|
|
|
35
35
|
# Ver templates disponibles (agrupados por capa)
|
|
36
36
|
fwinit list
|
|
37
37
|
|
|
38
|
+
# Instalar skills globalmente (OpenCode, Pi, Agent Skills) — interactivo
|
|
39
|
+
fwinit skills
|
|
40
|
+
|
|
41
|
+
# Instalar skills globalmente sin preguntas (modo scripting/CI)
|
|
42
|
+
fwinit skills --global opencode,pi # o: --global all
|
|
43
|
+
|
|
44
|
+
# Instalar solo algunas skills
|
|
45
|
+
fwinit skills --global all --only create-specs,design
|
|
46
|
+
|
|
47
|
+
# Eliminar skills globales (mismos flags; interactivo pregunta qué eliminar)
|
|
48
|
+
fwinit skills --remove
|
|
49
|
+
fwinit skills --global all --remove
|
|
50
|
+
fwinit skills --global pi --remove --only design
|
|
51
|
+
|
|
38
52
|
# Ayuda
|
|
39
53
|
fwinit --help
|
|
40
54
|
```
|
|
@@ -48,6 +62,7 @@ fwinit --help
|
|
|
48
62
|
> Full stack (backend + frontend)
|
|
49
63
|
Solo backend (API)
|
|
50
64
|
Solo frontend (app)
|
|
65
|
+
Skills globales (instalar / eliminar)
|
|
51
66
|
```
|
|
52
67
|
|
|
53
68
|
- **Full stack**: te pregunta primero el backend y después el frontend, y arma el monorepo completo.
|
package/dist/index.js
CHANGED
|
@@ -5,8 +5,8 @@ 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
|
|
9
|
-
import { join as
|
|
8
|
+
import { mkdir as mkdir3, cp as cp3, readFile as readFile2 } from "fs/promises";
|
|
9
|
+
import { join as join3, basename } from "path";
|
|
10
10
|
|
|
11
11
|
// src/templates.ts
|
|
12
12
|
var TEMPLATES = [
|
|
@@ -247,6 +247,15 @@ var AGENT_BUNDLE = [
|
|
|
247
247
|
dir: "create-specs-from-code",
|
|
248
248
|
required: true
|
|
249
249
|
},
|
|
250
|
+
{
|
|
251
|
+
// Editor de prompts: detalla una idea de proyecto nuevo en
|
|
252
|
+
// specs/prompt.md, listo para /create-specs. Opcional a propósito —
|
|
253
|
+
// depende de create-specs (que sí es required) y sin ella el proyecto
|
|
254
|
+
// conserva /create-specs funcional. Degradar, no abortar.
|
|
255
|
+
kind: "skill",
|
|
256
|
+
dir: "create-prompt",
|
|
257
|
+
required: false
|
|
258
|
+
},
|
|
250
259
|
{
|
|
251
260
|
// Biblioteca de estilos: skills/design/ con una subskill por estilo y sus
|
|
252
261
|
// commands (/design, /design-<estilo>) que leen specs/frontend/02-design.md.
|
|
@@ -266,33 +275,33 @@ async function exists(path) {
|
|
|
266
275
|
}
|
|
267
276
|
async function listMarkdown(dir) {
|
|
268
277
|
try {
|
|
269
|
-
const { readdir:
|
|
270
|
-
return (await
|
|
278
|
+
const { readdir: readdir3 } = await import("fs/promises");
|
|
279
|
+
return (await readdir3(dir)).filter((f) => f.endsWith(".md"));
|
|
271
280
|
} catch {
|
|
272
281
|
return [];
|
|
273
282
|
}
|
|
274
283
|
}
|
|
275
284
|
async function planAgentBundle(repoRoot, bundle = AGENT_BUNDLE) {
|
|
276
|
-
const { join:
|
|
285
|
+
const { join: join4 } = await import("path");
|
|
277
286
|
const installs = [];
|
|
278
287
|
const skillDirs = [];
|
|
279
288
|
const missingRequired = [];
|
|
280
289
|
for (const entry of bundle) {
|
|
281
|
-
const src =
|
|
290
|
+
const src = join4(repoRoot, "skills", entry.dir);
|
|
282
291
|
if (entry.kind === "library") {
|
|
283
292
|
if (!await exists(src)) {
|
|
284
293
|
if (entry.required) missingRequired.push(entry.dir);
|
|
285
294
|
continue;
|
|
286
295
|
}
|
|
287
296
|
installs.push({ from: src, to: `skills/${entry.dir}`, recursive: true });
|
|
288
|
-
const cmds =
|
|
297
|
+
const cmds = join4(src, "commands");
|
|
289
298
|
for (const file of await listMarkdown(cmds)) {
|
|
290
|
-
installs.push({ from:
|
|
299
|
+
installs.push({ from: join4(cmds, file), to: `commands/${file}` });
|
|
291
300
|
}
|
|
292
301
|
continue;
|
|
293
302
|
}
|
|
294
|
-
const skill =
|
|
295
|
-
const command =
|
|
303
|
+
const skill = join4(src, "SKILL.md");
|
|
304
|
+
const command = join4(src, "command.md");
|
|
296
305
|
if (!await exists(skill)) {
|
|
297
306
|
if (entry.required) missingRequired.push(entry.dir);
|
|
298
307
|
continue;
|
|
@@ -305,7 +314,7 @@ async function planAgentBundle(repoRoot, bundle = AGENT_BUNDLE) {
|
|
|
305
314
|
missingRequired.push(`${entry.dir}/command.md`);
|
|
306
315
|
}
|
|
307
316
|
for (const extra of entry.extras ?? []) {
|
|
308
|
-
const from =
|
|
317
|
+
const from = join4(src, extra);
|
|
309
318
|
if (await exists(from)) {
|
|
310
319
|
installs.push({
|
|
311
320
|
from,
|
|
@@ -329,7 +338,8 @@ async function selectScope() {
|
|
|
329
338
|
choices: [
|
|
330
339
|
{ name: "Full stack (backend + frontend)", value: "fullstack" },
|
|
331
340
|
{ name: "Solo backend (API)", value: "backend" },
|
|
332
|
-
{ name: "Solo frontend (app)", value: "frontend" }
|
|
341
|
+
{ name: "Solo frontend (app)", value: "frontend" },
|
|
342
|
+
{ name: "Skills globales (instalar / eliminar)", value: "skills" }
|
|
333
343
|
]
|
|
334
344
|
}
|
|
335
345
|
]);
|
|
@@ -404,6 +414,179 @@ async function askLayerLayout(layer) {
|
|
|
404
414
|
]);
|
|
405
415
|
return layout;
|
|
406
416
|
}
|
|
417
|
+
async function selectGlobalTargets(choices) {
|
|
418
|
+
const { targets } = await inquirer.prompt([
|
|
419
|
+
{
|
|
420
|
+
type: "checkbox",
|
|
421
|
+
name: "targets",
|
|
422
|
+
message: "\xBFD\xF3nde quer\xE9s instalar las skills?",
|
|
423
|
+
choices
|
|
424
|
+
}
|
|
425
|
+
]);
|
|
426
|
+
return targets;
|
|
427
|
+
}
|
|
428
|
+
async function selectGlobalTargetsToRemove(choices) {
|
|
429
|
+
const { targets } = await inquirer.prompt([
|
|
430
|
+
{
|
|
431
|
+
type: "checkbox",
|
|
432
|
+
name: "targets",
|
|
433
|
+
message: "\xBFDe d\xF3nde quer\xE9s eliminar las skills?",
|
|
434
|
+
choices
|
|
435
|
+
}
|
|
436
|
+
]);
|
|
437
|
+
return targets;
|
|
438
|
+
}
|
|
439
|
+
async function selectSkillsAction() {
|
|
440
|
+
const { action } = await inquirer.prompt([
|
|
441
|
+
{
|
|
442
|
+
type: "list",
|
|
443
|
+
name: "action",
|
|
444
|
+
message: "\xBFQu\xE9 quer\xE9s hacer con las skills globales?",
|
|
445
|
+
choices: [
|
|
446
|
+
{ name: "Instalar / actualizar", value: "install" },
|
|
447
|
+
{ name: "Eliminar", value: "remove" }
|
|
448
|
+
]
|
|
449
|
+
}
|
|
450
|
+
]);
|
|
451
|
+
return action;
|
|
452
|
+
}
|
|
453
|
+
async function askGlobalSkillsToo() {
|
|
454
|
+
const { global } = await inquirer.prompt([
|
|
455
|
+
{
|
|
456
|
+
type: "confirm",
|
|
457
|
+
name: "global",
|
|
458
|
+
message: "\xBFInstalar las skills tambi\xE9n globalmente (OpenCode/Pi/Agent Skills)?",
|
|
459
|
+
default: false
|
|
460
|
+
}
|
|
461
|
+
]);
|
|
462
|
+
return global;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/global-skills.ts
|
|
466
|
+
import { mkdir as mkdir2, cp as cp2, rm as rm2, readdir as readdir2 } from "fs/promises";
|
|
467
|
+
import { join as join2 } from "path";
|
|
468
|
+
function resolveGlobalTargets() {
|
|
469
|
+
const home = process.env.HOME;
|
|
470
|
+
if (!home) return [];
|
|
471
|
+
return [
|
|
472
|
+
{
|
|
473
|
+
id: "opencode",
|
|
474
|
+
label: `OpenCode global (${home}/.config/opencode)`,
|
|
475
|
+
root: join2(home, ".config", "opencode"),
|
|
476
|
+
skillsDir: "skills",
|
|
477
|
+
commandsDir: "command"
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
id: "pi",
|
|
481
|
+
label: `Pi global (${home}/.pi/agent)`,
|
|
482
|
+
root: join2(home, ".pi", "agent"),
|
|
483
|
+
skillsDir: "skills",
|
|
484
|
+
commandsDir: "prompts"
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
id: "agents",
|
|
488
|
+
label: `Agent Skills global (${home}/.agents/skills)`,
|
|
489
|
+
root: join2(home, ".agents"),
|
|
490
|
+
skillsDir: "skills"
|
|
491
|
+
}
|
|
492
|
+
];
|
|
493
|
+
}
|
|
494
|
+
function filterBundleByNames(only) {
|
|
495
|
+
const set = new Set(only);
|
|
496
|
+
const bundle = AGENT_BUNDLE.filter((e) => set.has(e.dir));
|
|
497
|
+
const known = new Set(AGENT_BUNDLE.map((e) => e.dir));
|
|
498
|
+
return { bundle, unknown: only.filter((n) => !known.has(n)) };
|
|
499
|
+
}
|
|
500
|
+
function mapPlanToTarget(plan, target) {
|
|
501
|
+
const mapped = [];
|
|
502
|
+
for (const item of plan.installs) {
|
|
503
|
+
if (item.to.startsWith("skills/")) {
|
|
504
|
+
mapped.push({
|
|
505
|
+
from: item.from,
|
|
506
|
+
to: join2(target.root, target.skillsDir, item.to.slice("skills/".length)),
|
|
507
|
+
recursive: item.recursive
|
|
508
|
+
});
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (item.to.startsWith("commands/")) {
|
|
512
|
+
if (!target.commandsDir) continue;
|
|
513
|
+
mapped.push({
|
|
514
|
+
from: item.from,
|
|
515
|
+
to: join2(target.root, target.commandsDir, item.to.slice("commands/".length)),
|
|
516
|
+
recursive: item.recursive
|
|
517
|
+
});
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return mapped;
|
|
522
|
+
}
|
|
523
|
+
async function installGlobalSkills(repoRoot, targets, bundle = AGENT_BUNDLE) {
|
|
524
|
+
const plan = await planAgentBundle(repoRoot, bundle);
|
|
525
|
+
const results = [];
|
|
526
|
+
for (const target of targets) {
|
|
527
|
+
try {
|
|
528
|
+
let copied = 0;
|
|
529
|
+
for (const item of mapPlanToTarget(plan, target)) {
|
|
530
|
+
if (item.recursive === true) {
|
|
531
|
+
await rm2(item.to, { recursive: true, force: true });
|
|
532
|
+
}
|
|
533
|
+
await mkdir2(
|
|
534
|
+
item.recursive === true ? item.to : join2(item.to, ".."),
|
|
535
|
+
{ recursive: true }
|
|
536
|
+
);
|
|
537
|
+
await cp2(item.from, item.to, {
|
|
538
|
+
recursive: item.recursive === true
|
|
539
|
+
});
|
|
540
|
+
copied++;
|
|
541
|
+
}
|
|
542
|
+
results.push({ target, copied });
|
|
543
|
+
} catch (error) {
|
|
544
|
+
results.push({
|
|
545
|
+
target,
|
|
546
|
+
copied: 0,
|
|
547
|
+
error: error.message
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return results;
|
|
552
|
+
}
|
|
553
|
+
async function uninstallGlobalSkills(targets, bundle = AGENT_BUNDLE) {
|
|
554
|
+
const skillNames = bundle.map((e) => e.dir);
|
|
555
|
+
const results = [];
|
|
556
|
+
for (const target of targets) {
|
|
557
|
+
try {
|
|
558
|
+
const removed = [];
|
|
559
|
+
for (const name of skillNames) {
|
|
560
|
+
await rm2(join2(target.root, target.skillsDir, name), {
|
|
561
|
+
recursive: true,
|
|
562
|
+
force: true
|
|
563
|
+
});
|
|
564
|
+
removed.push(`${target.skillsDir}/${name}`);
|
|
565
|
+
}
|
|
566
|
+
if (target.commandsDir) {
|
|
567
|
+
const cmdDir = join2(target.root, target.commandsDir);
|
|
568
|
+
for (const name of skillNames) {
|
|
569
|
+
await rm2(join2(cmdDir, `${name}.md`), { force: true });
|
|
570
|
+
removed.push(`${target.commandsDir}/${name}.md`);
|
|
571
|
+
try {
|
|
572
|
+
const prefix = `${name}-`;
|
|
573
|
+
for (const f of await readdir2(cmdDir)) {
|
|
574
|
+
if (f.startsWith(prefix) && f.endsWith(".md")) {
|
|
575
|
+
await rm2(join2(cmdDir, f), { force: true });
|
|
576
|
+
removed.push(`${target.commandsDir}/${f}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
} catch {
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
results.push({ target, removed });
|
|
584
|
+
} catch (error) {
|
|
585
|
+
results.push({ target, removed: [], error: error.message });
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return results;
|
|
589
|
+
}
|
|
407
590
|
|
|
408
591
|
// src/utils.ts
|
|
409
592
|
import { existsSync } from "fs";
|
|
@@ -507,9 +690,9 @@ function adaptScriptToNode(script) {
|
|
|
507
690
|
return s;
|
|
508
691
|
}
|
|
509
692
|
async function adaptToNodeRuntime(projectPath) {
|
|
510
|
-
const { readFile: readFile3, writeFile: writeFile2, readdir:
|
|
511
|
-
const { join:
|
|
512
|
-
const pkgPath =
|
|
693
|
+
const { readFile: readFile3, writeFile: writeFile2, readdir: readdir3 } = await import("fs/promises");
|
|
694
|
+
const { join: join4 } = await import("path");
|
|
695
|
+
const pkgPath = join4(projectPath, "package.json");
|
|
513
696
|
const pkg = JSON.parse(await readFile3(pkgPath, "utf-8"));
|
|
514
697
|
for (const key of Object.keys(pkg.scripts ?? {})) {
|
|
515
698
|
pkg.scripts[key] = adaptScriptToNode(pkg.scripts[key]);
|
|
@@ -520,9 +703,9 @@ async function adaptToNodeRuntime(projectPath) {
|
|
|
520
703
|
delete pkg.devDependencies["bun-types"];
|
|
521
704
|
await writeFile2(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
522
705
|
async function walk(dir) {
|
|
523
|
-
const entries = await
|
|
706
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
524
707
|
for (const entry of entries) {
|
|
525
|
-
const full =
|
|
708
|
+
const full = join4(dir, entry.name);
|
|
526
709
|
if (entry.isDirectory()) {
|
|
527
710
|
if (entry.name !== "node_modules") await walk(full);
|
|
528
711
|
} else if (entry.name.endsWith(".test.ts")) {
|
|
@@ -533,12 +716,12 @@ async function adaptToNodeRuntime(projectPath) {
|
|
|
533
716
|
}
|
|
534
717
|
}
|
|
535
718
|
}
|
|
536
|
-
const srcDir =
|
|
719
|
+
const srcDir = join4(projectPath, "src");
|
|
537
720
|
try {
|
|
538
721
|
await walk(srcDir);
|
|
539
722
|
} catch {
|
|
540
723
|
}
|
|
541
|
-
const tsconfigPath =
|
|
724
|
+
const tsconfigPath = join4(projectPath, "tsconfig.json");
|
|
542
725
|
try {
|
|
543
726
|
const tsconfig = JSON.parse(await readFile3(tsconfigPath, "utf-8"));
|
|
544
727
|
if (tsconfig.compilerOptions?.types?.includes("bun-types")) {
|
|
@@ -550,8 +733,8 @@ async function adaptToNodeRuntime(projectPath) {
|
|
|
550
733
|
}
|
|
551
734
|
async function writeRootGitignore(projectPath) {
|
|
552
735
|
const { writeFile: writeFile2, access } = await import("fs/promises");
|
|
553
|
-
const { join:
|
|
554
|
-
const gitignorePath =
|
|
736
|
+
const { join: join4 } = await import("path");
|
|
737
|
+
const gitignorePath = join4(projectPath, ".gitignore");
|
|
555
738
|
try {
|
|
556
739
|
await access(gitignorePath);
|
|
557
740
|
} catch {
|
|
@@ -579,7 +762,7 @@ async function gitInit(projectPath) {
|
|
|
579
762
|
}
|
|
580
763
|
async function writeRootFiles(info) {
|
|
581
764
|
const { writeFile: writeFile2 } = await import("fs/promises");
|
|
582
|
-
const { join:
|
|
765
|
+
const { join: join4 } = await import("path");
|
|
583
766
|
const projectPath = resolve(process.cwd(), info.projectName);
|
|
584
767
|
await writeRootGitignore(projectPath);
|
|
585
768
|
const installB = info.backend.postInit?.install ? resolveCommand(info.backend.postInit.install, info.pm) : `${info.pm} install`;
|
|
@@ -595,7 +778,7 @@ async function writeRootFiles(info) {
|
|
|
595
778
|
"",
|
|
596
779
|
`- \`backend/\` \u2014 ${info.backend.description}`,
|
|
597
780
|
`- \`frontend/\` \u2014 ${info.frontend.description}`,
|
|
598
|
-
"- `.opencode/`, `.pi/` y `.agents/` \u2014 skills de agentes: `/create-specs` para las specs + biblioteca de dise\xF1o (dark-luxury, minimal-dashboard, minimal-light, neo-brutalist, glassmorphism). Mismo bundle en
|
|
781
|
+
"- `.opencode/`, `.pi/` y `.agents/` \u2014 skills de agentes: `/create-prompt` para detallar tu idea, `/create-specs` para generar las specs, `/create-specs-from-code` para documentar un proyecto que ya existe, + biblioteca de dise\xF1o (dark-luxury, minimal-dashboard, minimal-light, neo-brutalist, glassmorphism). Mismo bundle en las 3 ubicaciones",
|
|
599
782
|
"- `specs/` \u2014 especificaciones del proyecto",
|
|
600
783
|
"",
|
|
601
784
|
"## Backend",
|
|
@@ -615,7 +798,7 @@ async function writeRootFiles(info) {
|
|
|
615
798
|
"```",
|
|
616
799
|
""
|
|
617
800
|
].join("\n");
|
|
618
|
-
await writeFile2(
|
|
801
|
+
await writeFile2(join4(projectPath, "README.md"), readme);
|
|
619
802
|
}
|
|
620
803
|
|
|
621
804
|
// src/index.ts
|
|
@@ -634,15 +817,15 @@ var AGENT_DIRS = [".opencode", ".pi", ".agents"];
|
|
|
634
817
|
async function installAgentSkills(repoRoot, projectPath) {
|
|
635
818
|
const plan = await planAgentBundle(repoRoot);
|
|
636
819
|
for (const agentDir of AGENT_DIRS) {
|
|
637
|
-
const root =
|
|
820
|
+
const root = join3(projectPath, agentDir);
|
|
638
821
|
for (const dir of plan.skillDirs) {
|
|
639
|
-
await
|
|
822
|
+
await mkdir3(join3(root, dir), { recursive: true });
|
|
640
823
|
}
|
|
641
824
|
for (const dir of plan.commandDirs) {
|
|
642
|
-
await
|
|
825
|
+
await mkdir3(join3(root, dir), { recursive: true });
|
|
643
826
|
}
|
|
644
827
|
for (const item of plan.installs) {
|
|
645
|
-
await
|
|
828
|
+
await cp3(item.from, join3(root, item.to), {
|
|
646
829
|
recursive: item.recursive === true
|
|
647
830
|
});
|
|
648
831
|
}
|
|
@@ -656,10 +839,29 @@ async function installAgentSkills(repoRoot, projectPath) {
|
|
|
656
839
|
);
|
|
657
840
|
}
|
|
658
841
|
}
|
|
842
|
+
async function installGlobalSkillsWithSpinner(targets, bundle) {
|
|
843
|
+
const spinner = ora("Descargando bundle de skills...").start();
|
|
844
|
+
const { tempDir, repoRoot } = await downloadAndExtractRepo();
|
|
845
|
+
try {
|
|
846
|
+
spinner.text = "Instalando skills...";
|
|
847
|
+
const results = await installGlobalSkills(repoRoot, targets, bundle);
|
|
848
|
+
spinner.succeed("Skills globales instaladas");
|
|
849
|
+
for (const r of results) {
|
|
850
|
+
if (r.error) {
|
|
851
|
+
console.log(chalk.red(` \u2716 ${r.target.label}: ${r.error}`));
|
|
852
|
+
} else {
|
|
853
|
+
console.log(chalk.dim(` \u2714 ${r.target.label} \u2014 ${r.copied} archivos`));
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
console.log();
|
|
857
|
+
} finally {
|
|
858
|
+
await cleanup(tempDir);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
659
861
|
async function readTemplatePostInit(dir) {
|
|
660
862
|
try {
|
|
661
863
|
const tj = JSON.parse(
|
|
662
|
-
await readFile2(
|
|
864
|
+
await readFile2(join3(dir, "template.json"), "utf-8")
|
|
663
865
|
);
|
|
664
866
|
return tj.postInit;
|
|
665
867
|
} catch {
|
|
@@ -669,14 +871,14 @@ async function readTemplatePostInit(dir) {
|
|
|
669
871
|
async function createFullStackProject(projectPath, backendTpl, frontendTpl, pm) {
|
|
670
872
|
const { tempDir, repoRoot } = await downloadAndExtractRepo();
|
|
671
873
|
const projectName = basename(projectPath);
|
|
672
|
-
const backendPath =
|
|
673
|
-
const frontendPath =
|
|
874
|
+
const backendPath = join3(projectPath, "backend");
|
|
875
|
+
const frontendPath = join3(projectPath, "frontend");
|
|
674
876
|
await copyTemplate(
|
|
675
|
-
|
|
877
|
+
join3(repoRoot, "templates", backendTpl.folder),
|
|
676
878
|
backendPath
|
|
677
879
|
);
|
|
678
880
|
await copyTemplate(
|
|
679
|
-
|
|
881
|
+
join3(repoRoot, "templates", frontendTpl.folder),
|
|
680
882
|
frontendPath
|
|
681
883
|
);
|
|
682
884
|
await substituteTemplate(backendPath, projectName, backendTpl.folder);
|
|
@@ -735,6 +937,102 @@ program.command("list").description("Mostrar templates disponibles").action(() =
|
|
|
735
937
|
console.log();
|
|
736
938
|
}
|
|
737
939
|
});
|
|
940
|
+
async function runSkillsFlow(opts = {}) {
|
|
941
|
+
const all = resolveGlobalTargets();
|
|
942
|
+
if (all.length === 0) {
|
|
943
|
+
console.error(chalk.red("\n\u2716 No se pudo resolver el HOME del usuario\n"));
|
|
944
|
+
process.exit(1);
|
|
945
|
+
}
|
|
946
|
+
console.log(chalk.bold("\nSkills disponibles:\n"));
|
|
947
|
+
console.log(chalk.dim(" create-specs \u2014 specs para proyecto nuevo (/create-specs)"));
|
|
948
|
+
console.log(chalk.dim(" create-specs-from-code \u2014 specs desde c\xF3digo existente (/create-specs-from-code)"));
|
|
949
|
+
console.log(chalk.dim(" design/ \u2014 dark-luxury, glassmorphism, minimal-dashboard, minimal-light, neo-brutalist (/design-<estilo>)\n"));
|
|
950
|
+
let bundle;
|
|
951
|
+
if (opts.only) {
|
|
952
|
+
const names = opts.only.split(",").map((s) => s.trim()).filter(Boolean);
|
|
953
|
+
const { bundle: filtered, unknown } = filterBundleByNames(names);
|
|
954
|
+
if (unknown.length > 0) {
|
|
955
|
+
console.log(chalk.yellow(` \u26A0 Skills desconocidas (ignoradas): ${unknown.join(", ")}
|
|
956
|
+
`));
|
|
957
|
+
}
|
|
958
|
+
if (filtered.length === 0) {
|
|
959
|
+
console.log(chalk.dim("\n Ninguna skill v\xE1lida en --only \u2014 no se hizo nada.\n"));
|
|
960
|
+
return false;
|
|
961
|
+
}
|
|
962
|
+
bundle = filtered;
|
|
963
|
+
}
|
|
964
|
+
const remove = opts.remove === true;
|
|
965
|
+
const interactive = !opts.global;
|
|
966
|
+
let action = remove ? "remove" : "install";
|
|
967
|
+
if (interactive && !remove) {
|
|
968
|
+
action = await selectSkillsAction();
|
|
969
|
+
}
|
|
970
|
+
let targets;
|
|
971
|
+
if (opts.global) {
|
|
972
|
+
const ids = opts.global.split(",").map((s) => s.trim().toLowerCase());
|
|
973
|
+
if (ids.includes("all")) {
|
|
974
|
+
targets = all;
|
|
975
|
+
} else {
|
|
976
|
+
const valid = new Map(all.map((t) => [t.id, t]));
|
|
977
|
+
targets = [];
|
|
978
|
+
for (const id of ids) {
|
|
979
|
+
const t = valid.get(id);
|
|
980
|
+
if (!t) {
|
|
981
|
+
console.error(
|
|
982
|
+
chalk.red(`
|
|
983
|
+
\u2716 Destino "${id}" inv\xE1lido. Us\xE1: ${all.map((x) => x.id).join(", ")} o all
|
|
984
|
+
`)
|
|
985
|
+
);
|
|
986
|
+
process.exit(1);
|
|
987
|
+
}
|
|
988
|
+
targets.push(t);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
} else {
|
|
992
|
+
const pick = action === "remove" ? selectGlobalTargetsToRemove : selectGlobalTargets;
|
|
993
|
+
const selected = await pick(all.map((t) => ({ name: t.label, value: t.id })));
|
|
994
|
+
if (selected.length === 0) {
|
|
995
|
+
console.log(
|
|
996
|
+
chalk.dim(`
|
|
997
|
+
Nada seleccionado \u2014 no se ${action === "remove" ? "elimin\xF3" : "instal\xF3"} nada.
|
|
998
|
+
`)
|
|
999
|
+
);
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
targets = all.filter((t) => selected.includes(t.id));
|
|
1003
|
+
}
|
|
1004
|
+
if (action === "remove") {
|
|
1005
|
+
const results = await uninstallGlobalSkills(targets, bundle);
|
|
1006
|
+
for (const r of results) {
|
|
1007
|
+
console.log(chalk.dim(` \u2714 ${r.target.label} \u2014 ${r.removed.length} eliminados`));
|
|
1008
|
+
}
|
|
1009
|
+
console.log();
|
|
1010
|
+
return true;
|
|
1011
|
+
}
|
|
1012
|
+
await installGlobalSkillsWithSpinner(targets, bundle);
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
program.command("skills").description(
|
|
1016
|
+
"Instalar o eliminar las skills del repo en ubicaciones globales (OpenCode, Pi, Agent Skills)"
|
|
1017
|
+
).option(
|
|
1018
|
+
"-g, --global <targets>",
|
|
1019
|
+
"Destinos separados por coma: opencode, pi, agents (o 'all'). Saltea la pregunta"
|
|
1020
|
+
).option(
|
|
1021
|
+
"-o, --only <skills>",
|
|
1022
|
+
"Solo estas skills, separadas por coma: create-specs, create-specs-from-code, design"
|
|
1023
|
+
).option(
|
|
1024
|
+
"-r, --remove",
|
|
1025
|
+
"Eliminar en vez de instalar (combinar con --global y opcionalmente --only)"
|
|
1026
|
+
).action(async (opts) => {
|
|
1027
|
+
try {
|
|
1028
|
+
await runSkillsFlow(opts);
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
console.error(chalk.red(`
|
|
1031
|
+
\u2716 Error: ${error.message}
|
|
1032
|
+
`));
|
|
1033
|
+
process.exit(1);
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
738
1036
|
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(
|
|
739
1037
|
async (templateArg, projectNameArg) => {
|
|
740
1038
|
try {
|
|
@@ -756,6 +1054,10 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
|
|
|
756
1054
|
const rawArg = templateArg?.toLowerCase();
|
|
757
1055
|
if (rawArg === void 0) {
|
|
758
1056
|
const scope = await selectScope();
|
|
1057
|
+
if (scope === "skills") {
|
|
1058
|
+
await runSkillsFlow();
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
759
1061
|
if (scope === "fullstack") {
|
|
760
1062
|
isFullStack = true;
|
|
761
1063
|
} else {
|
|
@@ -853,7 +1155,7 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
|
|
|
853
1155
|
spinner.succeed("Template descargado");
|
|
854
1156
|
} else {
|
|
855
1157
|
const { tempDir, templatePath, repoRoot } = await downloadAndExtract(template.folder);
|
|
856
|
-
const codePath = useLayerLayout ?
|
|
1158
|
+
const codePath = useLayerLayout ? join3(projectPath, layer) : projectPath;
|
|
857
1159
|
spinner.text = "Creando proyecto...";
|
|
858
1160
|
await copyTemplate(templatePath, codePath);
|
|
859
1161
|
await substituteTemplate(codePath, projectName, template.folder);
|
|
@@ -896,6 +1198,28 @@ program.argument("[template]", "Template a usar (o 'fullstack' para backend + fr
|
|
|
896
1198
|
);
|
|
897
1199
|
}
|
|
898
1200
|
}
|
|
1201
|
+
if (git === void 0) {
|
|
1202
|
+
const wantsGlobalSkills = await askGlobalSkillsToo();
|
|
1203
|
+
if (wantsGlobalSkills) {
|
|
1204
|
+
const all = resolveGlobalTargets();
|
|
1205
|
+
if (all.length === 0) {
|
|
1206
|
+
console.log(
|
|
1207
|
+
chalk.yellow(
|
|
1208
|
+
" \u26A0 No se pudo resolver el HOME \u2014 skills globales omitidas\n"
|
|
1209
|
+
)
|
|
1210
|
+
);
|
|
1211
|
+
} else {
|
|
1212
|
+
const selected = await selectGlobalTargets(
|
|
1213
|
+
all.map((t) => ({ name: t.label, value: t.id }))
|
|
1214
|
+
);
|
|
1215
|
+
if (selected.length > 0) {
|
|
1216
|
+
await installGlobalSkillsWithSpinner(
|
|
1217
|
+
all.filter((t) => selected.includes(t.id))
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
899
1223
|
} catch (error) {
|
|
900
1224
|
console.error(
|
|
901
1225
|
chalk.red(
|