fractalia-faro 0.1.0 → 0.4.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/bin/crear.mjs +77 -16
  2. package/package.json +1 -1
package/bin/crear.mjs CHANGED
@@ -68,6 +68,13 @@ const NATURALEZAS = {
68
68
  const CONFIG_PATH = path.join(os.homedir(), '.fractalia-cli.json');
69
69
  const API = 'https://api.github.com';
70
70
 
71
+ // LOGIN por NAVEGADOR (device flow) para usuarios no técnicos: sin pegar tokens.
72
+ // El client_id es PÚBLICO (no es secreto) -> se embebe aquí. Lo crea un admin de la org UNA vez:
73
+ // GitHub -> org Settings -> Developer settings -> OAuth Apps -> New OAuth App -> ✅ Enable Device Flow.
74
+ // Mientras esté vacío, el wizard usa la vía de reserva (pegar un PAT).
75
+ const DEFAULT_CLIENT_ID = 'Ov23liKk3T9bWKak2wmH'; // OAuth App "Fractalia FARO CLI" de CAS-IA (Client ID público)
76
+ const DEFAULT_ORG = 'CAS-IA'; // organización por defecto (evita teclearla)
77
+
71
78
  // ---------- CLI args ----------
72
79
  const args = process.argv.slice(2);
73
80
  const DRY = args.includes('--dry-run');
@@ -178,10 +185,25 @@ async function elige(titulo, opciones, preseleccion) {
178
185
 
179
186
  // Comprobación de prerrequisitos de la máquina (una vez, antes de nada).
180
187
  function tieneCmd(bins) { for (const b of bins) { try { execFileSync(b, ['--version'], { stdio: 'ignore', timeout: 8000 }); return true; } catch {} } return false; }
188
+ // Localiza el comando 'code' de VS Code: en PATH o en las rutas de instalación típicas de Windows.
189
+ // (Es común que 'code' no esté en PATH aunque VS Code esté instalado — hay que "Install 'code' command in PATH".)
190
+ function codeBin() {
191
+ for (const b of ['code', 'code.cmd']) { try { execFileSync(b, ['--version'], { stdio: 'ignore', timeout: 8000 }); return b; } catch {} }
192
+ const cand = [
193
+ path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'Microsoft VS Code', 'bin', 'code.cmd'),
194
+ 'C:/Program Files/Microsoft VS Code/bin/code.cmd',
195
+ 'C:/Program Files/Microsoft VS Code Insiders/bin/code-insiders.cmd',
196
+ ];
197
+ for (const c of cand) { if (fs.existsSync(c)) return c; }
198
+ return null;
199
+ }
200
+ const enVSCode = process.env.TERM_PROGRAM === 'vscode';
181
201
  function checkPrerrequisitos() {
182
202
  const falta = [];
183
203
  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.');
204
+ if (!codeBin()) warn(enVSCode
205
+ ? 'El comando "code" no está en el PATH — al final te diré la carpeta para abrirla tú (o instálalo: Ctrl+Shift+P → "Install \'code\' command in PATH").'
206
+ : 'VS Code (comando "code") no detectado — al final tendrás que abrir la carpeta a mano.');
185
207
  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
208
  }
187
209
 
@@ -215,6 +237,16 @@ async function gh(token, ruta, init = {}) {
215
237
  return { status: res.status, ok: res.ok, body };
216
238
  }
217
239
 
240
+ // Abre una URL en el navegador del sistema (sin dependencias, multiplataforma).
241
+ function abrirNavegador(url) {
242
+ try {
243
+ if (process.platform === 'win32') execFileSync('cmd', ['/c', 'start', '', url], { stdio: 'ignore' });
244
+ else if (process.platform === 'darwin') execFileSync('open', [url], { stdio: 'ignore' });
245
+ else execFileSync('xdg-open', [url], { stdio: 'ignore' });
246
+ return true;
247
+ } catch { return false; }
248
+ }
249
+
218
250
  // Login SIN contraseñas: GitHub Device Flow (requiere una OAuth App de la org; solo client_id, sin secreto).
219
251
  async function deviceFlow(clientId) {
220
252
  const form = (o) => Object.entries(o).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
@@ -224,8 +256,15 @@ async function deviceFlow(clientId) {
224
256
  };
225
257
  const d = await post('https://github.com/login/device/code', { client_id: clientId, scope: 'repo read:org' });
226
258
  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`);
259
+ const abierto = abrirNavegador(d.verification_uri);
260
+ say('');
261
+ caja([
262
+ `${C.b}Inicia sesión en GitHub${C.x} ${C.d}(una sola vez en esta máquina)${C.x}`,
263
+ '',
264
+ `${abierto ? 'Te he abierto el navegador en:' : 'Abre en el navegador:'} ${C.c}${d.verification_uri}${C.x}`,
265
+ `Teclea este código: ${C.b}${C.g}${d.user_code}${C.x}`,
266
+ ], { titulo: 'Login', color: C.c });
267
+ say(`${C.d} Esperando a que autorices en el navegador…${C.x}`);
229
268
  const intervalo = (d.interval || 5) * 1000;
230
269
  for (;;) {
231
270
  await new Promise((r) => setTimeout(r, intervalo));
@@ -251,9 +290,9 @@ async function obtenerToken(cfg) {
251
290
  const me = await gh(tok, '/user');
252
291
  if (me.ok) { ok(`Sesión GitHub válida vía ${via}: @${me.body.login}`); return { token: tok, login: me.body.login }; }
253
292
  }
254
- const clientId = cfg.client_id || process.env.FRACTALIA_CLIENT_ID;
293
+ const clientId = cfg.client_id || process.env.FRACTALIA_CLIENT_ID || DEFAULT_CLIENT_ID;
255
294
  if (clientId) {
256
- say('\nNecesitas iniciar sesión en GitHub (una sola vez en esta máquina).');
295
+ // Login por NAVEGADOR (lo normal para todos, sin pegar tokens).
257
296
  const tok = await deviceFlow(clientId);
258
297
  const me = await gh(tok, '/user');
259
298
  if (!me.ok) throw new Error('el token del device flow no valida');
@@ -261,8 +300,10 @@ async function obtenerToken(cfg) {
261
300
  ok(`Sesión iniciada: @${me.body.login} (guardada para próximas veces)`);
262
301
  return { token: tok, login: me.body.login };
263
302
  }
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).');
303
+ // Vía de RESERVA (solo si aún no hay OAuth App configurada): pegar un token personal.
304
+ say('');
305
+ warn('Login por navegador no disponible todavía (falta configurar la OAuth App de la organización).');
306
+ say(`${C.d} Mientras tanto: crea un token en https://github.com/settings/tokens (classic, scopes: repo + read:org) y pégalo (se guarda en tu máquina).${C.x}`);
266
307
  const tok = await pregunta('Token de GitHub (ghp_...)');
267
308
  const me = await gh(tok, '/user');
268
309
  if (!me.ok) throw new Error('el token pegado no es válido');
@@ -289,7 +330,12 @@ const run = (cmd, argv, opts = {}) => execFileSync(cmd, argv, { stdio: 'inherit'
289
330
  // Compartida por el flujo normal y por el RESUME (repo ya existente).
290
331
  async function finalizarLocal(auth, org, repoName, baseDir, destino, nat, tipo, naturaleza) {
291
332
  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.`); }
333
+ // Se puede clonar en una carpeta que YA existe si está VACÍA (caso "esta carpeta como raíz"); si tiene
334
+ // contenido, no se pisa.
335
+ const existe = fs.existsSync(destino);
336
+ let vacio = !existe;
337
+ if (existe) { try { vacio = fs.readdirSync(destino).filter((n) => n !== '.git').length === 0; } catch { vacio = false; } }
338
+ if (existe && !vacio) { warn(`La carpeta ${destino} ya existe y NO está vacía: no la sobrescribo. Abre ahí a mano si es el clon.`); }
293
339
  else {
294
340
  run('git', ['clone', `https://x-access-token:${auth.token}@github.com/${org}/${repoName}.git`, destino]);
295
341
  run('git', ['-C', destino, 'remote', 'set-url', 'origin', `https://github.com/${org}/${repoName}.git`]);
@@ -317,10 +363,9 @@ async function finalizarLocal(auth, org, repoName, baseDir, destino, nat, tipo,
317
363
  } else if (cuToken) ok('ClickUp key personal ya configurada en esta máquina.');
318
364
  // Abrir VS Code (salvo --no-open).
319
365
  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.`);
366
+ const cb = codeBin();
367
+ if (cb) { try { execFileSync(cb, [destino], { stdio: 'ignore' }); ok('Abriendo VS Code…'); } catch { warn(`Abre la carpeta a mano: ${destino}`); } }
368
+ else warn(`VS Code no está en el PATH. Abre esta carpeta a mano: ${destino}`);
324
369
  }
325
370
 
326
371
  // ---------- flujo principal ----------
@@ -389,10 +434,26 @@ async function main() {
389
434
  if (nombreFlag || await confirma('¿Correcto?')) break;
390
435
  }
391
436
 
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);
437
+ // 5. Dónde clonar. Por defecto la carpeta ACTUAL (donde ejecutas el wizard); eliges raíz o subcarpeta,
438
+ // y se detecta si la carpeta ya es un repo o tiene contenido para no pisar nada.
439
+ let destino;
440
+ const dirFlag = argVal('dir');
441
+ const cwd = process.cwd();
442
+ if (dirFlag) destino = path.join(dirFlag, repoName);
443
+ else if (YES) destino = path.join(cwd, repoName);
444
+ else {
445
+ const esRepo = fs.existsSync(path.join(cwd, '.git'));
446
+ let vacia = false; try { vacia = fs.readdirSync(cwd).filter((n) => n !== '.git' && !n.startsWith('.')).length === 0; } catch {}
447
+ const ops = { sub: { etiqueta: `Aquí, en una subcarpeta → ${path.join(cwd, repoName)}`, explica: 'Recomendado: crea la carpeta del proyecto DENTRO de la actual.' } };
448
+ if (!esRepo && vacia) ops.raiz = { etiqueta: `Esta misma carpeta como raíz → ${cwd}`, explica: 'La carpeta actual está vacía: el proyecto será ESTA carpeta, sin subcarpeta.' };
449
+ ops.otra = { etiqueta: 'Otra carpeta…', explica: 'Indicas tú la ruta contenedora.' };
450
+ if (esRepo) warn('La carpeta actual ya es un repositorio git → se usará una subcarpeta (no se puede anidar).');
451
+ const donde = await elige(`¿Dónde creamos el proyecto? ${C.d}(estás en ${cwd})${C.x}`, ops);
452
+ if (donde === 'raiz') destino = cwd;
453
+ else if (donde === 'otra') { const base = await pregunta('Ruta de la carpeta contenedora', cfg.workdir || cwd); destino = path.join(base, repoName); }
454
+ else destino = path.join(cwd, repoName);
455
+ }
456
+ const baseDir = path.dirname(destino);
396
457
  cfg.workdir = baseDir; saveConfig(cfg);
397
458
 
398
459
  // Resumen previo en caja.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fractalia-faro",
3
- "version": "0.1.0",
3
+ "version": "0.4.0",
4
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
5
  "bin": {
6
6
  "faro": "bin/crear.mjs"