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.
- package/bin/crear.mjs +77 -16
- 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 (!
|
|
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
|
-
|
|
228
|
-
say(
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
say('
|
|
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
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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.
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const
|
|
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.
|
|
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"
|