fractalia-faro 0.1.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/README.md +95 -0
- package/bin/crear.mjs +477 -0
- package/package.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# fractalia-faro · Wizard inicial de repos Fractalia
|
|
2
|
+
|
|
3
|
+
> CLI **PRE-harness** e independiente de él: el harness vive DENTRO de cada repo ya creado; este wizard es quien **crea** el repo. Pensado para usuarios no técnicos: un solo comando, sin paths ni git a mano.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npx fractalia-faro
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Qué hace, paso a paso
|
|
10
|
+
|
|
11
|
+
1. **Sesión GitHub**: si no estás logueado, te guía (device flow con código en el navegador, o token personal). Se hace UNA vez por máquina; queda guardado en `~/.fractalia-cli.json`.
|
|
12
|
+
2. **Valida la organización**: comprueba vía API que eres **miembro activo** de la organización destino (p. ej. `CAS-IA`). Si no lo eres, te dice a quién pedir acceso y para. Como los templates son privados de la org, sin membresía no hay acceso posible.
|
|
13
|
+
3. **Clasifica** lo que arrancas, explicándote las diferencias:
|
|
14
|
+
|
|
15
|
+
| Tipo | Prefijo de repo | Qué es |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| Servicio de la plataforma Core | `core-serv-` | Operación continua de Core; no se cierra, se opera y evoluciona |
|
|
18
|
+
| Funcionalidad de la plataforma Core | `core-func-` | Funcionalidad acotada que se añade a Core; se entrega y se integra |
|
|
19
|
+
| Proyecto | `prj-` | Fin definido, entregables y fechas (grande o pequeño = más o menos fases) |
|
|
20
|
+
| Iniciativa | `ini-` | Exploración/mejora con alcance aún abierto |
|
|
21
|
+
|
|
22
|
+
4. **Naturaleza → template**: documental/gestión → `template-faro-doc` · código (worker Cloudflare) → `template-faro-worker`. El catálogo puede sobreescribirse desde la org (`fractalia-templates/catalog.json`) sin republicar el CLI.
|
|
23
|
+
5. **Nomenclatura**: aplica el prefijo al nombre (`core-serv-notificaciones`), valida que el repo no exista.
|
|
24
|
+
6. **Crea el repo** privado en la org desde el template (API nativa `generate`), crea la rama `develop` y la deja **por defecto** (main queda como rama de aprobación).
|
|
25
|
+
7. **Clona en local** en la carpeta que elijas (default recordado), con el remote limpio (el token no queda en la URL).
|
|
26
|
+
8. **ClickUp API key personal** (opcional): la guarda en `git config --global clickup.token` — vale para todos tus repos y da atribución correcta.
|
|
27
|
+
9. **Abre VS Code** en la carpeta. Al abrir Claude Code dentro, el harness del template arranca su onboarding (fasear, funcional, riesgos), ya con conciencia del **tipo** por el prefijo del repo.
|
|
28
|
+
|
|
29
|
+
## Probar sin crear nada
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
node bin/crear.mjs --dry-run
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Simula todo el asistente (sin login ni API) y muestra el plan.
|
|
36
|
+
|
|
37
|
+
## Primer uso — guía para el usuario nuevo, paso a paso
|
|
38
|
+
|
|
39
|
+
**Dónde "vive" el wizard**: el código fuente está en este repo (privado de la org); el paquete ejecutable se **publica en npm** (registro público, sin secretos dentro). `npx` no va a GitHub: busca `fractalia-faro` en npm, lo descarga a una caché temporal y lo ejecuta. No se instala nada del wizard y siempre corre la última versión publicada. *(Alternativa sin npm: hacer público solo este repo y usar `npx github:CAS-IA/faro-cli`.)*
|
|
40
|
+
|
|
41
|
+
**Paso 0 · Prerrequisitos de la máquina (una sola vez).** Tres herramientas; en Windows se instalan pegando esto en PowerShell (o ejecutando `kit/instalar-fractalia.ps1`, que lo hace solo):
|
|
42
|
+
|
|
43
|
+
```powershell
|
|
44
|
+
winget install OpenJS.NodeJS.LTS # Node (trae npx)
|
|
45
|
+
winget install Git.Git # git (para clonar)
|
|
46
|
+
winget install Microsoft.VisualStudioCode # si no lo tiene ya
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Paso 1 · Invitación a la organización.** El administrador te invita (GitHub → org → People → Invite member); aceptas el email con tu cuenta GitHub (si no tienes, créala gratis en github.com).
|
|
50
|
+
|
|
51
|
+
**Paso 2 · Ejecutar el wizard.** Abre VS Code → Terminal → New Terminal y pega:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
npx fractalia-faro
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
La primera vez npx pregunta `Ok to proceed? (y)` → responde `y`.
|
|
58
|
+
|
|
59
|
+
**Paso 3 · Login GitHub (solo la primera vez en cada máquina).** El wizard te da un código y abre `github.com/login/device`: inicias sesión con tu cuenta corporativa, tecleas el código y autorizas. Queda guardado en `~/.fractalia-cli.json`.
|
|
60
|
+
|
|
61
|
+
**Paso 4 · Contesta al asistente.** Qué es (te explica servicio Core / funcionalidad Core / proyecto / iniciativa), nombre en lenguaje normal (él lo sanitiza y aplica el prefijo), carpeta local, y ClickUp key si quieres.
|
|
62
|
+
|
|
63
|
+
**Paso 5 · A trabajar.** El wizard deja el repo creado en la org (main + develop, develop por defecto), clonado en tu carpeta **ya en develop**, y VS Code abierto. Dentro, abre Claude Code: el harness del repo arranca su onboarding y desde ahí te gobierna el trabajo.
|
|
64
|
+
|
|
65
|
+
Las siguientes veces: solo el paso 2 (mismo comando, sin login ni preguntas de npx).
|
|
66
|
+
|
|
67
|
+
## Requisitos del usuario (resumen)
|
|
68
|
+
|
|
69
|
+
- Node 18+ y git instalados; VS Code con comando `code` (lo instala VS Code por defecto).
|
|
70
|
+
- Cuenta GitHub **dentro de la organización** (el wizard te dice si no lo estás).
|
|
71
|
+
|
|
72
|
+
## Configuración de administrador (una vez)
|
|
73
|
+
|
|
74
|
+
1. Subir `template-faro-doc` y `template-faro-worker` a la organización y marcarlos como **Template repository** (Settings → General).
|
|
75
|
+
2. (Opcional, mejor UX) Crear una **OAuth App** en la org para el device flow y distribuir su `client_id` (`FRACTALIA_CLIENT_ID` o `client_id` en `~/.fractalia-cli.json`). Sin ella, el wizard pide un PAT.
|
|
76
|
+
3. (Opcional) Repo `fractalia-templates` con `catalog.json` (`{ "tipos": {...}, "naturalezas": {...} }`) para gobernar tipos/prefijos/templates sin tocar el CLI.
|
|
77
|
+
4. Publicar este paquete en npm (`npm publish --access public`): no contiene secretos, solo lógica.
|
|
78
|
+
5. **Secrets globales** (p. ej. `DIRECTUS_TOKEN`): como *organization secrets* de GitHub Actions (write-only, nadie puede leerlos) — los usan los workflows de los repos creados, nunca la máquina del usuario.
|
|
79
|
+
|
|
80
|
+
## ⚠ Requisito de plan GitHub (verificado contra la documentación)
|
|
81
|
+
|
|
82
|
+
En el plan **Free**, los repos **privados** NO tienen: (a) acceso a *organization secrets* desde Actions, ni (b) *branch protection* / rulesets. Es decir: con org Free, la publicación a Directus con secret de org y la protección en servidor de `main` **no funcionan** (el harness sigue protegiendo main en local, eso no depende del plan). Para el diseño completo hace falta **GitHub Team**. Verifícalo antes del despliegue.
|
|
83
|
+
|
|
84
|
+
## Sincronización del estándar (anti-deriva)
|
|
85
|
+
|
|
86
|
+
La API `generate` de GitHub crea una **copia congelada**: los repos derivados no se enteran de las mejoras del template. Por eso el wizard estampa en cada repo creado un `.fractalia-template.json` (de qué template y de qué commit salió — patrón `copier`/`.copier-answers`). Con ese metadato, el plan de sync es añadir a los templates el workflow **actions-template-sync**, que abre PRs automáticos en los repos derivados cuando el template evoluciona.
|
|
87
|
+
|
|
88
|
+
## Roadmap
|
|
89
|
+
|
|
90
|
+
- Workflow `actions-template-sync` en los templates (cierra el ciclo anti-deriva de arriba).
|
|
91
|
+
- Registro central de repos generados (índice ligero en la org, estilo catálogo Backstage).
|
|
92
|
+
- Modo **no interactivo** por flags (`--tipo= --nombre= --naturaleza= --si`) para CI y agentes, y cancelación limpia con Ctrl-C (patrón create-vite).
|
|
93
|
+
- Adaptador GitLab (misma capa de proveedor).
|
|
94
|
+
- Broker Cloudflare Worker para secretos compartidos usables desde local (Directus push bajo demanda).
|
|
95
|
+
- Extensión VS Code fina que envuelva este CLI con formulario gráfico.
|
package/bin/crear.mjs
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// fractalia-faro — Wizard INICIAL de creación de repos (PRE-harness).
|
|
3
|
+
// Pieza independiente: el harness vive DENTRO de cada repo ya creado; este CLI es quien lo crea.
|
|
4
|
+
// Flujo: clasificar (con explicaciones) -> nomenclatura -> login GitHub -> validar organización
|
|
5
|
+
// -> crear repo desde el template -> ramas main+develop -> clonar en local -> ClickUp key -> abrir VS Code.
|
|
6
|
+
// Sin dependencias. Node >= 18 (usa fetch nativo).
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import readline from 'node:readline/promises';
|
|
13
|
+
import { execFileSync } from 'node:child_process';
|
|
14
|
+
|
|
15
|
+
// ---------- Catálogo de tipos (nomenclatura + explicación para clasificar) ----------
|
|
16
|
+
// El prefijo del repo garantiza la coherencia del catálogo de la organización.
|
|
17
|
+
// Si existe fractalia-templates/catalog.json en la org, SOBREESCRIBE este catálogo embebido.
|
|
18
|
+
// PLATAFORMAS de producto Fractalia. Core = plataforma PRINCIPAL (primera); el resto son
|
|
19
|
+
// soluciones transversales (Orbital, y las que vengan). EXTENSIBLE: añade una entrada aquí (o en
|
|
20
|
+
// el catalog.json de la org) y aparece con su -serv y -func. Los tipos de plataforma llevan
|
|
21
|
+
// `soloOwner: true` -> en el menú solo se ofrecen a owners/admins de la org (crear una capacidad
|
|
22
|
+
// de plataforma es estratégico); siguen accesibles por flag (--tipo) para automatización.
|
|
23
|
+
const PLATAFORMAS = [
|
|
24
|
+
{ id: 'core', nombre: 'Core', nota: 'plataforma principal' },
|
|
25
|
+
{ id: 'orbital', nombre: 'Orbital', nota: 'solución transversal' },
|
|
26
|
+
];
|
|
27
|
+
const CATALOGO = {};
|
|
28
|
+
for (const p of PLATAFORMAS) {
|
|
29
|
+
CATALOGO[`${p.id}-serv`] = {
|
|
30
|
+
etiqueta: `Servicio de ${p.nombre}`, prefijo: `${p.id}-serv-`, soloOwner: true,
|
|
31
|
+
explica: `Servicio de la plataforma ${p.nombre} (${p.nota}): operación CONTINUA, sin fecha de fin. No "se termina": se opera y evoluciona.`,
|
|
32
|
+
ejemplos: `servicio de notificaciones de ${p.nombre}, motor de ingesta de ${p.nombre}`,
|
|
33
|
+
};
|
|
34
|
+
CATALOGO[`${p.id}-func`] = {
|
|
35
|
+
etiqueta: `Funcionalidad de ${p.nombre}`, prefijo: `${p.id}-func-`, soloOwner: true,
|
|
36
|
+
explica: `Funcionalidad CONCRETA que se añade a la plataforma ${p.nombre}. Alcance acotado: se entrega y se integra en ${p.nombre}.`,
|
|
37
|
+
ejemplos: `exportación a Excel en ${p.nombre}, nueva vista en ${p.nombre}`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Tipos transversales (para todos los miembros).
|
|
41
|
+
CATALOGO.proyecto = {
|
|
42
|
+
etiqueta: 'Proyecto',
|
|
43
|
+
prefijo: 'prj-',
|
|
44
|
+
explica: 'Trabajo con FIN definido: objetivo, entregables y fechas; se cierra al terminar. Grande o pequeño solo cambia el número de fases. Si YA sabes que acabará en producto, empieza aquí.',
|
|
45
|
+
ejemplos: 'implantar CRM, migración a proveedor X, onboarding de cliente Y',
|
|
46
|
+
};
|
|
47
|
+
CATALOGO.iniciativa = {
|
|
48
|
+
etiqueta: 'Iniciativa / investigación',
|
|
49
|
+
prefijo: 'ini-',
|
|
50
|
+
explica: 'Exploración, estudio o investigación con alcance AÚN abierto (mercado, técnica, viabilidad). Si cristaliza en producto, se CONVIERTE en proyecto o servicio (se registra la decisión como ADR y, si cambia la naturaleza, se crea el repo destino). Úsala cuando el resultado es CONOCIMIENTO, no todavía un producto.',
|
|
51
|
+
ejemplos: 'estudiar automatización de altas, investigación de mercado, piloto de agentes IA',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Naturaleza del trabajo -> repo template a clonar (independiente del tipo/nomenclatura).
|
|
55
|
+
const NATURALEZAS = {
|
|
56
|
+
documental: {
|
|
57
|
+
etiqueta: 'Documental / gestión (sin código)',
|
|
58
|
+
template: 'template-faro-doc',
|
|
59
|
+
explica: 'Documentación funcional, specs, faseo, informes. El estándar FARO documental.',
|
|
60
|
+
},
|
|
61
|
+
codigo: {
|
|
62
|
+
etiqueta: 'Código: aplicación / worker Cloudflare',
|
|
63
|
+
template: 'template-faro-worker',
|
|
64
|
+
explica: 'App Cloudflare Workers con gestión faseada encima (harness FARO de código: gates, openspec, deploy).',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const CONFIG_PATH = path.join(os.homedir(), '.fractalia-cli.json');
|
|
69
|
+
const API = 'https://api.github.com';
|
|
70
|
+
|
|
71
|
+
// ---------- CLI args ----------
|
|
72
|
+
const args = process.argv.slice(2);
|
|
73
|
+
const DRY = args.includes('--dry-run');
|
|
74
|
+
const argVal = (name) => {
|
|
75
|
+
const a = args.find((x) => x.startsWith(`--${name}=`));
|
|
76
|
+
return a ? a.split('=').slice(1).join('=') : null;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const argFlag = (name) => args.includes(`--${name}`);
|
|
80
|
+
const NO_OPEN = argFlag('no-open');
|
|
81
|
+
const YES = argFlag('yes') || argFlag('si');
|
|
82
|
+
|
|
83
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
84
|
+
console.log(`
|
|
85
|
+
fractalia-faro — asistente de nuevo proyecto/iniciativa/servicio Fractalia
|
|
86
|
+
|
|
87
|
+
Uso: npx fractalia-faro [opciones]
|
|
88
|
+
|
|
89
|
+
Interactivo (por defecto): npx fractalia-faro
|
|
90
|
+
|
|
91
|
+
No interactivo (para CI o power users): pasa todo por flags
|
|
92
|
+
npx fractalia-faro --org=CAS-IA --tipo=core-serv --naturaleza=documental \\
|
|
93
|
+
--nombre="Notificaciones Core" --dir=H:/PROYECTOS --yes
|
|
94
|
+
|
|
95
|
+
Opciones:
|
|
96
|
+
--org=<nombre> Organización GitHub destino (si no, se pregunta/recuerda).
|
|
97
|
+
--tipo=<id> core-serv | core-func | orbital-serv | orbital-func | proyecto | iniciativa
|
|
98
|
+
--naturaleza=<id> documental | codigo
|
|
99
|
+
--nombre=<texto> Nombre del trabajo (se sanitiza y se le aplica el prefijo).
|
|
100
|
+
--dir=<ruta> Carpeta donde clonar (si no, se pregunta/recuerda).
|
|
101
|
+
--team=<slug> Asigna el repo a un equipo GitHub (control de visibilidad).
|
|
102
|
+
--yes / --si No pide confirmaciones (asume "sí").
|
|
103
|
+
--no-open No abre VS Code al terminar.
|
|
104
|
+
--dry-run Simula todo SIN crear nada (ni login): valida el flujo.
|
|
105
|
+
--help Esta ayuda.
|
|
106
|
+
|
|
107
|
+
Qué hace: clasifica lo que arrancas, aplica la nomenclatura oficial, inicia sesión
|
|
108
|
+
en GitHub (device flow), valida tu pertenencia a la organización, crea el repo desde
|
|
109
|
+
el template, crea main+develop (develop por defecto), intenta proteger main en
|
|
110
|
+
servidor (si el plan lo permite), lo clona, guarda tu ClickUp key y abre VS Code.
|
|
111
|
+
`);
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Cancelacion limpia (Ctrl-C) — no deja el terminal a medias (patron create-vite).
|
|
116
|
+
let _rlRef = null;
|
|
117
|
+
const cancelar = () => { if (_rlRef) { try { _rlRef.close(); } catch {} } console.log('\n\n Cancelado. No se ha creado nada nuevo.\n'); process.exit(130); };
|
|
118
|
+
process.on('SIGINT', cancelar);
|
|
119
|
+
|
|
120
|
+
// ---------- UI helpers (colores ANSI + caracteres de caja, sin dependencias; se apagan sin TTY) ----------
|
|
121
|
+
const C = process.stdout.isTTY
|
|
122
|
+
? { g: '\x1b[32m', y: '\x1b[33m', r: '\x1b[31m', c: '\x1b[36m', b: '\x1b[1m', d: '\x1b[2m', x: '\x1b[0m' }
|
|
123
|
+
: { g: '', y: '', r: '', c: '', b: '', d: '', x: '' };
|
|
124
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
125
|
+
_rlRef = rl;
|
|
126
|
+
const say = (s = '') => console.log(s);
|
|
127
|
+
const ok = (s) => console.log(` ${C.g}✔${C.x} ${s}`);
|
|
128
|
+
const warn = (s) => console.log(` ${C.y}⚠${C.x} ${s}`);
|
|
129
|
+
const fail = (s) => { console.error(` ${C.r}✖${C.x} ${s}`); process.exitCode = 1; };
|
|
130
|
+
|
|
131
|
+
// Ancho visible de un texto (ignora los codigos ANSI para alinear cajas).
|
|
132
|
+
const vis = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
133
|
+
const W = 66;
|
|
134
|
+
// Caja con titulo opcional y lineas dentro (centra el marco, respeta el color).
|
|
135
|
+
function caja(lineas, { color = C.c, titulo = '' } = {}) {
|
|
136
|
+
const top = titulo ? `╭─ ${C.b}${titulo}${C.x}${color} ${'─'.repeat(Math.max(0, W - 4 - vis(titulo).length))}╮` : `╭${'─'.repeat(W)}╮`;
|
|
137
|
+
say(`${color}${top}${C.x}`);
|
|
138
|
+
for (const l of lineas) { const pad = Math.max(0, W - 2 - vis(l).length); say(`${color}│${C.x} ${l}${' '.repeat(pad)} ${color}│${C.x}`); }
|
|
139
|
+
say(`${color}╰${'─'.repeat(W)}╯${C.x}`);
|
|
140
|
+
}
|
|
141
|
+
// Encabezado de paso: "Paso N/T · titulo" con separacion visual.
|
|
142
|
+
let _pasoN = 0; const _pasoT = 6;
|
|
143
|
+
function paso(titulo) { _pasoN++; say(''); say(`${C.c}${C.b}▸ Paso ${_pasoN}/${_pasoT}${C.x} ${C.b}${titulo}${C.x}`); say(`${C.d}${'─'.repeat(W + 2)}${C.x}`); }
|
|
144
|
+
|
|
145
|
+
async function pregunta(q, defecto) {
|
|
146
|
+
const suf = defecto ? ` ${C.d}[${defecto}]${C.x}` : '';
|
|
147
|
+
const r = (await rl.question(`${C.b}?${C.x} ${q}${suf}: `)).trim();
|
|
148
|
+
return r || defecto || '';
|
|
149
|
+
}
|
|
150
|
+
// Confirmación s/n (respeta --yes).
|
|
151
|
+
async function confirma(q, def = 's') {
|
|
152
|
+
if (YES) return true;
|
|
153
|
+
return (await pregunta(`${q} (s/n)`, def)).toLowerCase().startsWith('s');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Menú numerado con explicación por opción. Si `preseleccion` (de un flag) casa con una clave, la usa sin preguntar.
|
|
157
|
+
async function elige(titulo, opciones, preseleccion) {
|
|
158
|
+
const keys = Object.keys(opciones);
|
|
159
|
+
if (preseleccion) {
|
|
160
|
+
if (keys.includes(preseleccion)) return preseleccion;
|
|
161
|
+
fail(`Valor '${preseleccion}' no válido. Opciones: ${keys.join(', ')}.`); rl.close(); process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
say(`\n${C.b}${titulo}${C.x}\n`);
|
|
164
|
+
keys.forEach((k, i) => {
|
|
165
|
+
const o = opciones[k];
|
|
166
|
+
say(` ${C.c}${C.b}${i + 1}${C.x} ${C.b}${o.etiqueta}${C.x}${o.prefijo ? ` ${C.d}→${C.x} ${C.g}${o.prefijo}<nombre>${C.x}` : ''}`);
|
|
167
|
+
say(` ${C.d}${o.explica}${C.x}`);
|
|
168
|
+
if (o.ejemplos) say(` ${C.d}Ej.: ${o.ejemplos}${C.x}`);
|
|
169
|
+
say('');
|
|
170
|
+
});
|
|
171
|
+
for (;;) {
|
|
172
|
+
const r = (await rl.question(`${C.b}?${C.x} Elige 1-${keys.length}: `)).trim();
|
|
173
|
+
const n = Number.parseInt(r, 10);
|
|
174
|
+
if (n >= 1 && n <= keys.length) return keys[n - 1];
|
|
175
|
+
say(' Respuesta no válida.');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Comprobación de prerrequisitos de la máquina (una vez, antes de nada).
|
|
180
|
+
function tieneCmd(bins) { for (const b of bins) { try { execFileSync(b, ['--version'], { stdio: 'ignore', timeout: 8000 }); return true; } catch {} } return false; }
|
|
181
|
+
function checkPrerrequisitos() {
|
|
182
|
+
const falta = [];
|
|
183
|
+
if (!tieneCmd(['git'])) falta.push('Git (winget install Git.Git)');
|
|
184
|
+
if (!tieneCmd(['code', 'code.cmd'])) warn('VS Code (comando "code") no detectado — al final tendrás que abrir la carpeta a mano.');
|
|
185
|
+
if (falta.length) { fail(`Falta instalar: ${falta.join(' · ')}. Instálalo y vuelve a ejecutar (o usa kit/instalar-fractalia.ps1).`); rl.close(); process.exit(1); }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const slugify = (s) => s
|
|
189
|
+
.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
190
|
+
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
191
|
+
|
|
192
|
+
// ---------- config local (~/.fractalia-cli.json) ----------
|
|
193
|
+
function loadConfig() {
|
|
194
|
+
try { return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); } catch { return {}; }
|
|
195
|
+
}
|
|
196
|
+
function saveConfig(cfg) {
|
|
197
|
+
try { fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), { mode: 0o600 }); } catch {}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---------- GitHub API ----------
|
|
201
|
+
async function gh(token, ruta, init = {}) {
|
|
202
|
+
const res = await fetch(`${API}${ruta}`, {
|
|
203
|
+
...init,
|
|
204
|
+
headers: {
|
|
205
|
+
Accept: 'application/vnd.github+json',
|
|
206
|
+
Authorization: `Bearer ${token}`,
|
|
207
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
208
|
+
...(init.body ? { 'Content-Type': 'application/json' } : {}),
|
|
209
|
+
...(init.headers || {}),
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
const text = await res.text();
|
|
213
|
+
let body = null;
|
|
214
|
+
try { body = text ? JSON.parse(text) : null; } catch { body = text; }
|
|
215
|
+
return { status: res.status, ok: res.ok, body };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Login SIN contraseñas: GitHub Device Flow (requiere una OAuth App de la org; solo client_id, sin secreto).
|
|
219
|
+
async function deviceFlow(clientId) {
|
|
220
|
+
const form = (o) => Object.entries(o).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
|
|
221
|
+
const post = async (url, data) => {
|
|
222
|
+
const res = await fetch(url, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, body: form(data) });
|
|
223
|
+
return res.json();
|
|
224
|
+
};
|
|
225
|
+
const d = await post('https://github.com/login/device/code', { client_id: clientId, scope: 'repo read:org' });
|
|
226
|
+
if (!d.device_code) throw new Error(`device flow: ${JSON.stringify(d)}`);
|
|
227
|
+
say(`\n 1) Abre en el navegador: ${d.verification_uri}`);
|
|
228
|
+
say(` 2) Inicia sesión con tu cuenta corporativa y teclea el código: ${d.user_code}\n`);
|
|
229
|
+
const intervalo = (d.interval || 5) * 1000;
|
|
230
|
+
for (;;) {
|
|
231
|
+
await new Promise((r) => setTimeout(r, intervalo));
|
|
232
|
+
const t = await post('https://github.com/login/oauth/access_token', {
|
|
233
|
+
client_id: clientId, device_code: d.device_code, grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
234
|
+
});
|
|
235
|
+
if (t.access_token) return t.access_token;
|
|
236
|
+
if (t.error && t.error !== 'authorization_pending' && t.error !== 'slow_down') throw new Error(`device flow: ${t.error}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Cascada de autenticación: env -> token guardado -> gh CLI -> device flow (si hay client_id) -> pegar PAT.
|
|
241
|
+
async function obtenerToken(cfg) {
|
|
242
|
+
const candidatos = [];
|
|
243
|
+
if (process.env.FRACTALIA_GITHUB_TOKEN) candidatos.push(['variable de entorno FRACTALIA_GITHUB_TOKEN', process.env.FRACTALIA_GITHUB_TOKEN]);
|
|
244
|
+
if (process.env.GITHUB_TOKEN) candidatos.push(['variable de entorno GITHUB_TOKEN', process.env.GITHUB_TOKEN]);
|
|
245
|
+
if (cfg.token) candidatos.push(['sesión guardada (~/.fractalia-cli.json)', cfg.token]);
|
|
246
|
+
try {
|
|
247
|
+
const t = execFileSync('gh', ['auth', 'token'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 8000 }).trim();
|
|
248
|
+
if (t) candidatos.push(['GitHub CLI (gh auth token)', t]);
|
|
249
|
+
} catch {}
|
|
250
|
+
for (const [via, tok] of candidatos) {
|
|
251
|
+
const me = await gh(tok, '/user');
|
|
252
|
+
if (me.ok) { ok(`Sesión GitHub válida vía ${via}: @${me.body.login}`); return { token: tok, login: me.body.login }; }
|
|
253
|
+
}
|
|
254
|
+
const clientId = cfg.client_id || process.env.FRACTALIA_CLIENT_ID;
|
|
255
|
+
if (clientId) {
|
|
256
|
+
say('\nNecesitas iniciar sesión en GitHub (una sola vez en esta máquina).');
|
|
257
|
+
const tok = await deviceFlow(clientId);
|
|
258
|
+
const me = await gh(tok, '/user');
|
|
259
|
+
if (!me.ok) throw new Error('el token del device flow no valida');
|
|
260
|
+
cfg.token = tok; saveConfig(cfg);
|
|
261
|
+
ok(`Sesión iniciada: @${me.body.login} (guardada para próximas veces)`);
|
|
262
|
+
return { token: tok, login: me.body.login };
|
|
263
|
+
}
|
|
264
|
+
say('\nNo hay sesión de GitHub. Crea un token personal (PAT clásico, scopes: repo + read:org)');
|
|
265
|
+
say('en https://github.com/settings/tokens y pégalo aquí (queda guardado en tu máquina).');
|
|
266
|
+
const tok = await pregunta('Token de GitHub (ghp_...)');
|
|
267
|
+
const me = await gh(tok, '/user');
|
|
268
|
+
if (!me.ok) throw new Error('el token pegado no es válido');
|
|
269
|
+
cfg.token = tok; saveConfig(cfg);
|
|
270
|
+
ok(`Sesión iniciada: @${me.body.login} (guardada para próximas veces)`);
|
|
271
|
+
return { token: tok, login: me.body.login };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Catálogo remoto opcional: <org>/fractalia-templates:catalog.json manda sobre el embebido.
|
|
275
|
+
async function cargarCatalogo(token, org) {
|
|
276
|
+
try {
|
|
277
|
+
const r = await gh(token, `/repos/${org}/fractalia-templates/contents/catalog.json`);
|
|
278
|
+
if (r.ok && r.body && r.body.content) {
|
|
279
|
+
const remoto = JSON.parse(Buffer.from(r.body.content, 'base64').toString('utf8'));
|
|
280
|
+
if (remoto.tipos) { ok('Catálogo de tipos cargado desde la organización (fractalia-templates/catalog.json).'); return { tipos: remoto.tipos, naturalezas: remoto.naturalezas || NATURALEZAS }; }
|
|
281
|
+
}
|
|
282
|
+
} catch {}
|
|
283
|
+
return { tipos: CATALOGO, naturalezas: NATURALEZAS };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const run = (cmd, argv, opts = {}) => execFileSync(cmd, argv, { stdio: 'inherit', ...opts });
|
|
287
|
+
|
|
288
|
+
// Clona el repo en local, estampa metadatos de origen, guarda ClickUp key y abre VS Code.
|
|
289
|
+
// Compartida por el flujo normal y por el RESUME (repo ya existente).
|
|
290
|
+
async function finalizarLocal(auth, org, repoName, baseDir, destino, nat, tipo, naturaleza) {
|
|
291
|
+
fs.mkdirSync(baseDir, { recursive: true });
|
|
292
|
+
if (fs.existsSync(destino)) { warn(`La carpeta ${destino} ya existe: no la sobrescribo. Abre ahí a mano si es el clon.`); }
|
|
293
|
+
else {
|
|
294
|
+
run('git', ['clone', `https://x-access-token:${auth.token}@github.com/${org}/${repoName}.git`, destino]);
|
|
295
|
+
run('git', ['-C', destino, 'remote', 'set-url', 'origin', `https://github.com/${org}/${repoName}.git`]);
|
|
296
|
+
ok(`Clonado en ${destino}`);
|
|
297
|
+
// Metadatos de ORIGEN del template (patron copier): de que template y commit salio -> habilita sync futuro.
|
|
298
|
+
try {
|
|
299
|
+
const head = await gh(auth.token, `/repos/${org}/${nat.template}/commits?per_page=1`);
|
|
300
|
+
const meta = { template: `${org}/${nat.template}`, template_commit: (head.ok && head.body[0] && head.body[0].sha) || null, tipo, naturaleza, organizacion: org, generador: 'fractalia-faro' };
|
|
301
|
+
const mp = path.join(destino, '.fractalia-template.json');
|
|
302
|
+
if (!fs.existsSync(mp)) {
|
|
303
|
+
fs.writeFileSync(mp, JSON.stringify(meta, null, 2) + '\n', 'utf8');
|
|
304
|
+
run('git', ['-C', destino, 'add', '.fractalia-template.json'], { stdio: 'ignore' });
|
|
305
|
+
run('git', ['-C', destino, 'commit', '-m', 'chore: metadatos de origen del template (habilita sync del estandar)'], { stdio: 'ignore' });
|
|
306
|
+
run('git', ['-C', destino, 'push'], { stdio: 'ignore' });
|
|
307
|
+
ok('Metadatos de template guardados (.fractalia-template.json).');
|
|
308
|
+
}
|
|
309
|
+
} catch { warn('No pude estampar los metadatos del template (no bloquea).'); }
|
|
310
|
+
}
|
|
311
|
+
// ClickUp API key personal (opcional; el harness del repo la usará).
|
|
312
|
+
let cuToken = null;
|
|
313
|
+
try { cuToken = execFileSync('git', ['config', '--global', 'clickup.token'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch {}
|
|
314
|
+
if (!cuToken && !YES) {
|
|
315
|
+
const cu = await pregunta('ClickUp API key personal (pk_..., Enter para omitir)');
|
|
316
|
+
if (cu) { run('git', ['config', '--global', 'clickup.token', cu]); ok('ClickUp key guardada en tu git config global (vale para todos tus repos).'); }
|
|
317
|
+
} else if (cuToken) ok('ClickUp key personal ya configurada en esta máquina.');
|
|
318
|
+
// Abrir VS Code (salvo --no-open).
|
|
319
|
+
if (NO_OPEN) { ok(`Listo. Abre la carpeta: ${destino}`); return; }
|
|
320
|
+
let abierto = false;
|
|
321
|
+
for (const bin of ['code', 'code.cmd']) { try { execFileSync(bin, [destino], { stdio: 'ignore' }); abierto = true; break; } catch {} }
|
|
322
|
+
if (abierto) ok('Abriendo VS Code…');
|
|
323
|
+
else warn(`No pude abrir VS Code automáticamente: abre la carpeta ${destino} a mano.`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ---------- flujo principal ----------
|
|
327
|
+
async function main() {
|
|
328
|
+
say('');
|
|
329
|
+
caja([
|
|
330
|
+
`${C.b}🧭 Fractalia · asistente de nuevo repositorio${C.x}`,
|
|
331
|
+
`${C.d}Crea el repo con la nomenclatura oficial, ramas main+develop${C.x}`,
|
|
332
|
+
`${C.d}y te lo deja clonado y listo para trabajar con Claude Code.${C.x}`,
|
|
333
|
+
]);
|
|
334
|
+
if (DRY) { say(''); warn(`${C.y}MODO SIMULACIÓN (--dry-run): no se crea nada.${C.x}`); }
|
|
335
|
+
if (!DRY) checkPrerrequisitos();
|
|
336
|
+
|
|
337
|
+
const cfg = loadConfig();
|
|
338
|
+
|
|
339
|
+
// 1. Sesión GitHub + organización (en dry-run se omite el login).
|
|
340
|
+
let auth = { token: null, login: '(dry-run)' };
|
|
341
|
+
let org = argVal('org') || cfg.org || '';
|
|
342
|
+
if (!DRY) {
|
|
343
|
+
auth = await obtenerToken(cfg);
|
|
344
|
+
if (!org) {
|
|
345
|
+
const orgs = await gh(auth.token, '/user/orgs');
|
|
346
|
+
const nombres = (orgs.ok && orgs.body.map((o) => o.login)) || [];
|
|
347
|
+
if (nombres.length === 1) org = nombres[0];
|
|
348
|
+
else if (nombres.length > 1) {
|
|
349
|
+
const opciones = Object.fromEntries(nombres.map((n) => [n, { etiqueta: n, explica: 'organización GitHub de la que eres miembro' }]));
|
|
350
|
+
org = await elige('¿En qué organización se crea el repo?', opciones);
|
|
351
|
+
} else org = await pregunta('Organización GitHub destino');
|
|
352
|
+
}
|
|
353
|
+
// 2. Validar pertenencia a la organización (los templates son privados: sin membresía no hay acceso).
|
|
354
|
+
const memb = await gh(auth.token, `/user/memberships/orgs/${org}`);
|
|
355
|
+
if (!memb.ok || memb.body.state !== 'active') {
|
|
356
|
+
fail(`No eres miembro activo de la organización '${org}'. Pide acceso al administrador y vuelve a ejecutar el asistente.`);
|
|
357
|
+
rl.close(); return;
|
|
358
|
+
}
|
|
359
|
+
auth.esOwner = memb.body.role === 'admin'; // 'admin' = owner de la organización
|
|
360
|
+
ok(`Miembro de '${org}' verificado (@${auth.login}) · rol: ${auth.esOwner ? 'OWNER/admin' : 'member'}.`);
|
|
361
|
+
cfg.org = org; saveConfig(cfg);
|
|
362
|
+
} else if (!org) {
|
|
363
|
+
org = await pregunta('Organización GitHub destino (simulación)', 'fractalia');
|
|
364
|
+
}
|
|
365
|
+
const esOwner = DRY ? true : !!auth.esOwner; // en dry-run se muestran todas las opciones
|
|
366
|
+
|
|
367
|
+
// 3. Clasificación con explicaciones (¿qué estás arrancando?). Los flags --tipo/--naturaleza saltan la pregunta.
|
|
368
|
+
const catalogo = DRY ? { tipos: CATALOGO, naturalezas: NATURALEZAS } : await cargarCatalogo(auth.token, org);
|
|
369
|
+
const tipoFlag = argVal('tipo');
|
|
370
|
+
// Menú filtrado por rol: los tipos de plataforma (soloOwner) solo se MUESTRAN a owners.
|
|
371
|
+
// Con --tipo (flag) se usa el catálogo completo (guía, no candado).
|
|
372
|
+
const menuTipos = tipoFlag ? catalogo.tipos
|
|
373
|
+
: Object.fromEntries(Object.entries(catalogo.tipos).filter(([, v]) => esOwner || !v.soloOwner));
|
|
374
|
+
if (!esOwner && !tipoFlag) say(`\n${C.d}(Los tipos de plataforma —servicios/funcionalidades de Core, Orbital…— los crean owners de la organización; no aparecen abajo.)${C.x}`);
|
|
375
|
+
const tipo = await elige('¿Qué vas a arrancar? (elige por lo que ES, no por el tamaño)', menuTipos, tipoFlag);
|
|
376
|
+
const t = catalogo.tipos[tipo];
|
|
377
|
+
const naturaleza = await elige('¿Qué naturaleza tiene el trabajo?', catalogo.naturalezas, argVal('naturaleza'));
|
|
378
|
+
const nat = catalogo.naturalezas[naturaleza];
|
|
379
|
+
|
|
380
|
+
// 4. Nombre + nomenclatura oficial (--nombre lo pasa directo).
|
|
381
|
+
let repoName = '';
|
|
382
|
+
const nombreFlag = argVal('nombre');
|
|
383
|
+
for (;;) {
|
|
384
|
+
const bruto = nombreFlag || await pregunta(`Nombre corto (sin prefijo; ej. "${(t.ejemplos || 'mi-tema').split(',')[0].trim().split(' ').slice(-2).join('-')}")`);
|
|
385
|
+
const slug = slugify(bruto);
|
|
386
|
+
if (!slug) { if (nombreFlag) { fail('El --nombre no produce un slug válido.'); rl.close(); return; } say(' Nombre vacío o inválido.'); continue; }
|
|
387
|
+
repoName = `${t.prefijo}${slug}`;
|
|
388
|
+
say(` → nombre de repo según nomenclatura: ${C.g}${org}/${repoName}${C.x}`);
|
|
389
|
+
if (nombreFlag || await confirma('¿Correcto?')) break;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// 5. Carpeta local de trabajo (--dir lo pasa directo).
|
|
393
|
+
const defDir = cfg.workdir || path.join(os.homedir(), 'PROYECTOS');
|
|
394
|
+
const baseDir = argVal('dir') || await pregunta('Carpeta donde clonarlo', defDir);
|
|
395
|
+
const destino = path.join(baseDir, repoName);
|
|
396
|
+
cfg.workdir = baseDir; saveConfig(cfg);
|
|
397
|
+
|
|
398
|
+
// Resumen previo en caja.
|
|
399
|
+
say('');
|
|
400
|
+
caja([
|
|
401
|
+
`${C.d}Tipo${C.x} ${t.etiqueta} ${C.d}(${tipo})${C.x}`,
|
|
402
|
+
`${C.d}Naturaleza${C.x} ${nat.etiqueta}`,
|
|
403
|
+
`${C.d}Template${C.x} ${org}/${nat.template}`,
|
|
404
|
+
`${C.d}Repo nuevo${C.x} ${C.g}${org}/${repoName}${C.x} ${C.d}· privado · main + develop${C.x}`,
|
|
405
|
+
`${C.d}Clon en${C.x} ${destino}`,
|
|
406
|
+
], { titulo: 'Resumen', color: C.g });
|
|
407
|
+
say('');
|
|
408
|
+
if (DRY) { ok('(dry-run) Flujo validado — no se ha creado nada.'); say(''); rl.close(); return; }
|
|
409
|
+
if (!(await confirma('¿Adelante?'))) { rl.close(); return; }
|
|
410
|
+
say('');
|
|
411
|
+
|
|
412
|
+
// 6. Crear el repo desde el template (API nativa de GitHub "generate").
|
|
413
|
+
// RESUME: si ya existe (ejecucion previa interrumpida), ofrecer clonarlo en vez de morir.
|
|
414
|
+
const libre = await gh(auth.token, `/repos/${org}/${repoName}`);
|
|
415
|
+
if (libre.status !== 404) {
|
|
416
|
+
if (libre.ok && await confirma(`Ya existe ${org}/${repoName}. ¿Solo clonarlo en local (reanudar)?`)) {
|
|
417
|
+
await finalizarLocal(auth, org, repoName, baseDir, destino, nat, tipo, naturaleza);
|
|
418
|
+
say(`\n${C.g}✅ Reanudado.${C.x} Trabaja en develop/feature; main = aprobación.\n`); rl.close(); return;
|
|
419
|
+
}
|
|
420
|
+
fail(`Ya existe ${org}/${repoName} — elige otro nombre.`); rl.close(); return;
|
|
421
|
+
}
|
|
422
|
+
const gen = await gh(auth.token, `/repos/${org}/${nat.template}/generate`, {
|
|
423
|
+
method: 'POST',
|
|
424
|
+
body: JSON.stringify({ owner: org, name: repoName, private: true, description: `${t.etiqueta} · creado con fractalia-faro` }),
|
|
425
|
+
});
|
|
426
|
+
if (!gen.ok) { fail(`No se pudo crear el repo desde el template: ${JSON.stringify(gen.body && gen.body.message)}. ¿Está ${org}/${nat.template} marcado como "Template repository"?`); rl.close(); return; }
|
|
427
|
+
ok(`Repo creado: ${gen.body.html_url}`);
|
|
428
|
+
|
|
429
|
+
// 6b. Asignar a un equipo GitHub (control de VISIBILIDAD): quien no este en el equipo no lo ve.
|
|
430
|
+
const team = argVal('team') || cfg.team;
|
|
431
|
+
if (team) {
|
|
432
|
+
const asg = await gh(auth.token, `/orgs/${org}/teams/${team}/repos/${org}/${repoName}`, { method: 'PUT', body: JSON.stringify({ permission: 'push' }) });
|
|
433
|
+
if (asg.ok || asg.status === 204) { ok(`Repo asignado al equipo '${team}' (visibilidad restringida a sus miembros).`); cfg.team = team; saveConfig(cfg); }
|
|
434
|
+
else warn(`No pude asignar el repo al equipo '${team}' (¿existe? ¿tienes permiso?): continúo.`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 7. Esperar a que el contenido esté listo y crear develop (rama por defecto).
|
|
438
|
+
let defBranch = gen.body.default_branch || 'main';
|
|
439
|
+
let sha = null;
|
|
440
|
+
for (let i = 0; i < 20 && !sha; i++) {
|
|
441
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
442
|
+
const ref = await gh(auth.token, `/repos/${org}/${repoName}/git/ref/heads/${defBranch}`);
|
|
443
|
+
if (ref.ok) sha = ref.body.object.sha;
|
|
444
|
+
}
|
|
445
|
+
if (!sha) { warn('El repo tarda en estar listo; crea develop a mano más tarde (git switch -c develop && git push -u origin develop).'); }
|
|
446
|
+
else {
|
|
447
|
+
const dev = await gh(auth.token, `/repos/${org}/${repoName}/git/refs`, { method: 'POST', body: JSON.stringify({ ref: 'refs/heads/develop', sha }) });
|
|
448
|
+
if (dev.ok) {
|
|
449
|
+
const pat = await gh(auth.token, `/repos/${org}/${repoName}`, { method: 'PATCH', body: JSON.stringify({ default_branch: 'develop' }) });
|
|
450
|
+
ok(`Rama develop creada${pat.ok ? ' y fijada como rama por defecto' : ''} (main queda como rama de aprobación).`);
|
|
451
|
+
} else warn('No se pudo crear develop automáticamente; créala tras el clon.');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// 7b. Proteger main en SERVIDOR (2ª capa, sobre el gate local del harness). Requiere plan Team en
|
|
455
|
+
// repos privados: si el plan no lo permite, GitHub responde 403 y avisamos sin romper (Free = solo gate local).
|
|
456
|
+
const prot = await gh(auth.token, `/repos/${org}/${repoName}/branches/main/protection`, {
|
|
457
|
+
method: 'PUT',
|
|
458
|
+
body: JSON.stringify({ required_pull_request_reviews: { required_approving_review_count: 1 }, enforce_admins: false, required_status_checks: null, restrictions: null }),
|
|
459
|
+
});
|
|
460
|
+
if (prot.ok) ok('main protegida en servidor (solo se llega por PR/merge = aprobación).');
|
|
461
|
+
else warn('No pude proteger main en servidor (normal en plan GitHub Free para repos privados) — el harness la protege igual en local. Para el candado de servidor: GitHub Team.');
|
|
462
|
+
|
|
463
|
+
// 8-10. Clonar en local, metadatos, ClickUp y abrir VS Code.
|
|
464
|
+
await finalizarLocal(auth, org, repoName, baseDir, destino, nat, tipo, naturaleza);
|
|
465
|
+
|
|
466
|
+
say('');
|
|
467
|
+
caja([
|
|
468
|
+
`${C.g}${C.b}✅ Repositorio listo${C.x}`,
|
|
469
|
+
`${C.d}Al abrir Claude Code dentro, el harness arranca el onboarding${C.x}`,
|
|
470
|
+
`${C.d}(fasear el trabajo, funcional, riesgos).${C.x}`,
|
|
471
|
+
`Trabaja en ${C.b}develop/feature${C.x}${C.d}; el merge a ${C.x}main${C.d} = aprobación.${C.x}`,
|
|
472
|
+
], { color: C.g });
|
|
473
|
+
say('');
|
|
474
|
+
rl.close();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
main().catch((e) => { fail(e.message || String(e)); rl.close(); });
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fractalia-faro",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Wizard FARO: crea un repo de trabajo (servicio/funcionalidad de plataforma, proyecto o iniciativa) desde el template oficial, con nomenclatura, ramas main+develop y clon local listo.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"faro": "bin/crear.mjs"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/"
|
|
14
|
+
],
|
|
15
|
+
"license": "UNLICENSED",
|
|
16
|
+
"private": false
|
|
17
|
+
}
|