@getparallel/parx 0.5.0 → 0.7.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/cli.js CHANGED
@@ -6,16 +6,20 @@ import { basename, resolve } from 'node:path';
6
6
  import { parseArgs } from 'node:util';
7
7
  import { randomUUID } from 'node:crypto';
8
8
  import { ALIAS_COMANDO, ALIAS_SI_NO, COMANDOS, t, USO } from "./textos.js";
9
- import { c, colorDeModo, fijarCapacidades, sep, signo, SIMBOLOS } from "./marca.js";
9
+ import { c, capacidades, colorDeModo, fijarCapacidades, sep, signo, sinColor, SIMBOLOS } from "./marca.js";
10
10
  import { guardarAjustes, leerAjustes, migrarCarpetaVieja, VERSION } from "./config.js";
11
11
  import { MODELO_POR_DEFECTO, MODELOS, modeloDe, nombreDeModelo } from "./modelos.js";
12
- import { cicloDeMision, compartirMision, correoVisible, crearMision, ErrorDeParallel, leerBandeja, leerConexiones, leerCuenta, leerMisionDeLaCuenta, leerMisiones, leerPlantillas, quienEs, responderBandeja, verMision, } from "./api.js";
12
+ import { cicloDeMision, compartirMision, correoVisible, crearMision, equipo, ErrorDeParallel, leerBandeja, leerConexiones, leerCuenta, leerMisionDeLaCuenta, leerMisiones, leerPlantillas, quienEs, responderBandeja, verMision, } from "./api.js";
13
13
  import { abrirNavegador, comoHasEntrado, credencialesActivas, entrar, formaDeLaOpcion, salir } from "./cuenta.js";
14
14
  import { accionesDeUnaMision, filtrarPorTitulo, hace } from "./comandos.js";
15
- import { hacerTarea, mensajeDeError, nuevaSesion } from "./agente.js";
15
+ import { hacerTarea, mensajeDeError, nuevaSesion, reiniciarConversacion } from "./agente.js";
16
16
  import { deshacerUltimo, permitirCarpeta } from "./herramientas.js";
17
17
  import { Registro } from "./registro.js";
18
18
  import { Pantalla } from "./ui.js";
19
+ import { Remoto } from "./remoto.js";
20
+ import { VistaPrevia } from "./vista.js";
21
+ import { anadirCarpeta, carpetasDelServicio, instalarServicio, quitarCarpeta, quitarServicio, reiniciarServicio, rutaDelRegistro, servicioEnMarcha, servicioInstalado, servir } from "./servicio.js";
22
+ import { qrEnTexto } from "./qr.js";
19
23
  import { acciones, bloqueDeBienvenida, bloqueDeLaPersona, bloqueDeTabla, duracion, Superficie } from "./tui.js";
20
24
  import { CONSEJOS, cambiarConsejos, consejo, elegirConsejos, ordenDeConsejos } from "./consejos.js";
21
25
  import { anchoDe, recortarMedio } from "./texto.js";
@@ -24,9 +28,9 @@ import { avisarEnElEscritorio, debeAvisar } from "./notificar.js";
24
28
  import { copiarAlPortapapeles, escribirInforme, textoParaPegar } from "./informe.js";
25
29
  import { objetivoDeMision } from "./mision.js";
26
30
  import { ejecutarCarriles, MAX_CARRILES } from "./carriles.js";
27
- import { spawnSync } from 'node:child_process';
28
31
  import { ejecutarBash, textoParaElAgente } from "./bash.js";
29
- import { comprobarDeFondo, esMasNueva, ordenDeActualizar, rutaDelCli, ultimaPublicada, versionNuevaConocida } from "./actualizar.js";
32
+ import { comprobarDeFondo, ENTRE_COMPROBACIONES_MS, esMasNueva, ordenDeActualizar, rutaDelCli, ultimaPublicada, versionNuevaConocida } from "./actualizar.js";
33
+ import { autoactualizarApagada, confirmarArranque, gestorPermiteAutoactualizar, lanzarLaMasNueva, prepararVersion } from "./versiones.js";
30
34
  function opciones() {
31
35
  return parseArgs({
32
36
  allowPositionals: true,
@@ -46,6 +50,10 @@ function opciones() {
46
50
  'no-mouse': { type: 'boolean' },
47
51
  'no-animation': { type: 'boolean' },
48
52
  animation: { type: 'boolean' },
53
+ remote: { type: 'boolean' },
54
+ serve: { type: 'boolean' },
55
+ 'remote-child': { type: 'boolean' },
56
+ origen: { type: 'string' },
49
57
  version: { type: 'boolean', short: 'v' },
50
58
  help: { type: 'boolean', short: 'h' },
51
59
  browser: { type: 'boolean' },
@@ -70,15 +78,38 @@ function rutaCorta(ruta, max = 48) {
70
78
  const corta = ruta.startsWith(casa) ? `~${ruta.slice(casa.length)}` : ruta;
71
79
  return recortarMedio(corta, max);
72
80
  }
81
+ let lineaDeVersiones = null;
82
+ function novedadParaLaPantalla() {
83
+ if (lineaDeVersiones)
84
+ return lineaDeVersiones;
85
+ if (!autoactualizarApagada())
86
+ return undefined;
87
+ const nueva = versionNuevaConocida();
88
+ return nueva ? t('update.disponible', { version: nueva }) : undefined;
89
+ }
73
90
  function cabeceraCorta(lineas = []) {
74
91
  process.stdout.write(`\n ${c.violeta(signo())} ${c.negrita('parx')} ${c.gris(VERSION)}\n`);
75
- const nueva = versionNuevaConocida();
76
- if (nueva)
77
- lineas = [c.gris(t('update.disponible', { version: nueva })), ...lineas];
92
+ const novedad = novedadParaLaPantalla();
93
+ if (novedad)
94
+ lineas = [c.gris(novedad), ...lineas];
78
95
  for (const l of lineas)
79
96
  process.stdout.write(` ${l}\n`);
80
97
  process.stdout.write('\n');
81
98
  }
99
+ async function mantenerAlDia(registro) {
100
+ try {
101
+ const nueva = await comprobarDeFondo();
102
+ if (!nueva || autoactualizarApagada() || !gestorPermiteAutoactualizar(ordenDeActualizar(rutaDelCli()).gestor))
103
+ return;
104
+ const r = await prepararVersion(nueva);
105
+ if (!r.ok)
106
+ registro?.apuntar('actualizacion', { version: nueva, motivo: r.motivo });
107
+ else if (!r.yaEstaba)
108
+ registro?.apuntar('actualizacion', { version: nueva, preparada: true });
109
+ }
110
+ catch {
111
+ }
112
+ }
82
113
  export function nombreDeModo(m) {
83
114
  return t(`modo.${m}`);
84
115
  }
@@ -114,7 +145,7 @@ async function actualizar() {
114
145
  process.stdout.write(`${t('update.npx')}\n`);
115
146
  return 0;
116
147
  }
117
- if (gestor === 'desarrollo' || !orden) {
148
+ if (gestor === 'desarrollo') {
118
149
  process.stdout.write(`${t('update.desarrollo')}\n`);
119
150
  return 0;
120
151
  }
@@ -127,14 +158,13 @@ async function actualizar() {
127
158
  process.stdout.write(`${t('update.al_dia', { version: VERSION })}\n`);
128
159
  return 0;
129
160
  }
130
- const texto = orden.map((p) => (/\s/.test(p) ? `"${p}"` : p)).join(' ');
131
- process.stdout.write(`${c.gris(t('update.lanzando', { orden: texto }))}\n`);
132
- const r = spawnSync(orden[0], orden.slice(1), { stdio: 'inherit', shell: process.platform === 'win32' });
133
- if (r.status !== 0) {
134
- process.stderr.write(`${c.error('✗')} ${t('update.fallo', { orden: texto })}\n`);
161
+ const aMano = (orden ?? ['npm', 'install', '-g', '@getparallel/parx@latest']).map((p) => (/\s/.test(p) ? `"${p}"` : p)).join(' ');
162
+ const r = await prepararVersion(ultima);
163
+ if (!r.ok) {
164
+ process.stderr.write(`${c.error('✗')} ${t(r.motivo === 'firma' ? 'update.firma' : 'update.no_se_pudo', { version: ultima, orden: aMano })}\n`);
135
165
  return 1;
136
166
  }
137
- process.stdout.write(`${c.violeta('✓')} ${t('update.hecho')} (${VERSION} → ${ultima})\n`);
167
+ process.stdout.write(`${c.violeta('✓')} ${t(r.yaEstaba ? 'update.ya_preparada' : 'update.preparada', { version: ultima })}${r.novedad ? c.gris(`${sep()}${r.novedad}`) : ''}\n`);
138
168
  return 0;
139
169
  }
140
170
  async function principal() {
@@ -152,6 +182,9 @@ async function principal() {
152
182
  process.stdout.write(`parx ${VERSION}\n`);
153
183
  return 0;
154
184
  }
185
+ const estreno = confirmarArranque();
186
+ if (estreno)
187
+ lineaDeVersiones = estreno.novedad ? t('update.actualizado_con', { version: estreno.version, novedad: estreno.novedad }) : t('update.actualizado', { version: estreno.version });
155
188
  if (v.help) {
156
189
  cabeceraCorta();
157
190
  process.stdout.write(`${USO}\n`);
@@ -162,7 +195,7 @@ async function principal() {
162
195
  const solo = positionals.length === 1 && !v.lanes;
163
196
  if (solo && ['update', 'upgrade'].includes(primero))
164
197
  return actualizar();
165
- void comprobarDeFondo();
198
+ void mantenerAlDia();
166
199
  if (solo && ['login', 'entrar', 'switch'].includes(primero)) {
167
200
  cabeceraCorta();
168
201
  await entrar((l) => pantalla.escribir(l), { forma: formaPedida(v) });
@@ -194,6 +227,62 @@ async function principal() {
194
227
  pantalla.escribir();
195
228
  return 0;
196
229
  }
230
+ if (v.serve) {
231
+ const c0 = credencialesActivas();
232
+ if (!c0) {
233
+ console.log(t('cuenta.sin_sesion'));
234
+ return 1;
235
+ }
236
+ return servir(c0);
237
+ }
238
+ if (primero === 'service' || primero === 'servicio') {
239
+ const accion = (positionals[1] ?? 'status').toLowerCase();
240
+ const ruta = positionals.slice(2).join(' ') || process.cwd();
241
+ const lista = () => carpetasDelServicio().map((x) => x.nombre).join(', ') || t('servicio.ninguna');
242
+ if (accion === 'add') {
243
+ const r = anadirCarpeta(ruta);
244
+ pantalla.escribir(r ? t('servicio.anadida', { ruta: rutaCorta(r, 60) }) : t('servicio.no_anadida', { ruta }));
245
+ if (r)
246
+ reiniciarServicio();
247
+ return r ? 0 : 1;
248
+ }
249
+ if (accion === 'remove') {
250
+ const ok = quitarCarpeta(positionals.slice(2).join(' '));
251
+ pantalla.escribir(ok ? t('servicio.quitada', { ruta: positionals.slice(2).join(' ') }) : t('servicio.no_quitada', { ruta: positionals.slice(2).join(' ') }));
252
+ if (ok)
253
+ reiniciarServicio();
254
+ return ok ? 0 : 1;
255
+ }
256
+ if (accion === 'on') {
257
+ if (process.platform !== 'darwin') {
258
+ pantalla.error(t('servicio.solo_mac'));
259
+ return 1;
260
+ }
261
+ if (!credencialesActivas()) {
262
+ pantalla.escribir(t('cuenta.sin_sesion'));
263
+ return 1;
264
+ }
265
+ if (!carpetasDelServicio().length && !anadirCarpeta(process.cwd())) {
266
+ pantalla.escribir(t('servicio.sin_carpetas'));
267
+ return 1;
268
+ }
269
+ instalarServicio();
270
+ pantalla.escribir(t('servicio.on', { carpetas: lista() }));
271
+ pantalla.escribir(c.gris(t('servicio.registro', { ruta: rutaDelRegistro() })));
272
+ return 0;
273
+ }
274
+ if (accion === 'off') {
275
+ quitarServicio();
276
+ pantalla.escribir(t('servicio.off'));
277
+ return 0;
278
+ }
279
+ if (accion === 'status') {
280
+ pantalla.escribir(t('servicio.estado', { estado: servicioInstalado() && servicioEnMarcha() ? t('servicio.en_marcha') : t('servicio.parado'), lista: lista() }));
281
+ return 0;
282
+ }
283
+ pantalla.escribir(t('servicio.uso'));
284
+ return 2;
285
+ }
197
286
  const modoPedido = (v.mode ?? v.modo);
198
287
  const pedido = modoDe(modoPedido);
199
288
  if (pedido === 'bypass') {
@@ -242,6 +331,8 @@ async function principal() {
242
331
  lecturas: [],
243
332
  jaula: Boolean(v.sandbox),
244
333
  });
334
+ if (cuenta.tarifa)
335
+ sesion.tarifas = cuenta.tarifa;
245
336
  for (const f of v['add-folder'] ?? []) {
246
337
  const ok = permitirCarpeta(sesion.ctx, f);
247
338
  if (!ok)
@@ -270,6 +361,26 @@ async function principal() {
270
361
  let control = new AbortController();
271
362
  let ocupado = false;
272
363
  let superficie = null;
364
+ let remoto = null;
365
+ let deFueraPendientes = 0;
366
+ let sugeridaMision = false;
367
+ let vistaPrevia = new VistaPrevia();
368
+ const actualizarVista = async (forzar = false) => {
369
+ const r0 = remoto;
370
+ if (!r0?.encendido || !r0.id)
371
+ return null;
372
+ try {
373
+ const r = await vistaPrevia.actualizar({ servidor: cred.servidor, token: cred.token, remoto: r0.id, raiz });
374
+ if (!r)
375
+ return null;
376
+ if (r.cambiados > 0 || forzar)
377
+ r0.evento('vista', { url: r.url, version: r.version, archivos: r.cambiados, carpeta: r.carpeta });
378
+ return { url: r.url };
379
+ }
380
+ catch (e) {
381
+ return { error: e instanceof ErrorDeParallel ? e.codigo : 'red' };
382
+ }
383
+ };
273
384
  let bienvenida;
274
385
  let consejosDeEntrada = [];
275
386
  const confirmarNuevaEnMarcha = async () => {
@@ -283,15 +394,7 @@ async function principal() {
283
394
  control.abort();
284
395
  };
285
396
  const empezarDeNuevo = () => {
286
- sesion.conversacion = [];
287
- sesion.diario = [];
288
- sesion.id = randomUUID();
289
- sesion.ultimaTarea = undefined;
290
- sesion.ctx.hechos = { ficheros: [], comandos: [] };
291
- sesion.ctx.permitidos.clear();
292
- sesion.ctx.deshacer.length = 0;
293
- if (sesion.ctx.lecturas)
294
- sesion.ctx.lecturas.length = 0;
397
+ reiniciarConversacion(sesion);
295
398
  registro.apuntar('nueva', {});
296
399
  pantalla.reiniciar();
297
400
  if (superficie)
@@ -307,6 +410,14 @@ async function principal() {
307
410
  superficie.terminar(despedida());
308
411
  else
309
412
  pantalla.escribir(despedida());
413
+ if (remoto?.encendido) {
414
+ const salida = setTimeout(() => process.exit(0), 1500);
415
+ void remoto.cerrar().finally(() => {
416
+ clearTimeout(salida);
417
+ process.exit(0);
418
+ });
419
+ return;
420
+ }
310
421
  process.exit(0);
311
422
  };
312
423
  const alCtrlC = () => {
@@ -348,6 +459,13 @@ async function principal() {
348
459
  superficie?.trabajar(() => control.abort());
349
460
  const inicio = Date.now();
350
461
  let seguir = true;
462
+ if (remoto?.encendido) {
463
+ remoto.fijarModo(nombreDeModo(sesion.ctx.modo), nombreDeModelo(sesion.modelo));
464
+ remoto.evento('tu', { texto: texto.replace(/^\u2060/, ''), desde: deFueraPendientes > 0 ? 'movil' : 'ordenador' });
465
+ if (deFueraPendientes > 0)
466
+ deFueraPendientes--;
467
+ remoto.fijarEstado('trabajando', texto.replace(/^\u2060/, ''));
468
+ }
351
469
  try {
352
470
  seguir = await hacerTarea(sesion, texto, control.signal, imagenes);
353
471
  if (sesion.disponibles !== undefined && sesion.disponibles !== null)
@@ -356,10 +474,100 @@ async function principal() {
356
474
  finally {
357
475
  ocupado = false;
358
476
  superficie?.descansar();
477
+ remoto?.fijarEstado('libre', null);
478
+ }
479
+ if (remoto?.encendido)
480
+ void actualizarVista();
481
+ if (!sugeridaMision && sesion.diario.some((d) => d.ficheros.length) && superficie) {
482
+ sugeridaMision = true;
483
+ soloAqui(() => pantalla.escribir(c.gris(t('mision.sugerencia'))));
359
484
  }
360
485
  quizaAvisar(t('aviso.termino', { tarea: texto.replace(/\s+/g, ' ').slice(0, 80) }), Date.now() - inicio);
361
486
  return seguir;
362
487
  };
488
+ const encenderRemoto = async (enseñarQR = true) => {
489
+ if (!superficie) {
490
+ pantalla.escribir(t('remoto.solo_interactiva'));
491
+ return;
492
+ }
493
+ if (remoto?.encendido && remoto.url) {
494
+ const url = remoto.url;
495
+ soloAqui(() => enseñarEnlace(url, enseñarQR));
496
+ return;
497
+ }
498
+ const nuevo = new Remoto({
499
+ servidor: cred.servidor,
500
+ token: cred.token,
501
+ alMensaje: (texto) => {
502
+ soloAqui(() => pantalla.escribir(c.gris(`${t('remoto.del_movil')}:`)));
503
+ deFueraPendientes++;
504
+ superficie?.mandarDesdeFuera(texto);
505
+ },
506
+ alParar: () => {
507
+ if (!ocupado)
508
+ return;
509
+ control.abort();
510
+ pantalla.aviso(t('remoto.parado'));
511
+ },
512
+ alApagarse: (motivo) => {
513
+ pantalla.espejo = null;
514
+ remoto = null;
515
+ pantalla.aviso(motivo === 'web' ? t('remoto.cortado_web') : motivo === 'sesion' ? t('remoto.sesion') : t('remoto.no_disponible'));
516
+ superficie?.pintar();
517
+ },
518
+ });
519
+ try {
520
+ const r = await nuevo.abrir({ equipo: equipo().equipo, version: equipo().version, carpeta: basename(raiz), modelo: nombreDeModelo(sesion.modelo), modo: nombreDeModo(sesion.ctx.modo) });
521
+ remoto = nuevo;
522
+ vistaPrevia = new VistaPrevia();
523
+ registro.apuntar('remoto', { encendido: true });
524
+ enseñarEnlace(r.url, enseñarQR);
525
+ pantalla.espejo = nuevo;
526
+ }
527
+ catch (e) {
528
+ if (e instanceof ErrorDeParallel) {
529
+ if (e.codigo === 'sin_acceso')
530
+ pantalla.error(t('remoto.no_disponible'));
531
+ else if (e.codigo === 'sesion')
532
+ pantalla.error(t('remoto.sesion'));
533
+ else if (e.detalle.includes('demasiados'))
534
+ pantalla.error(t('remoto.demasiados'));
535
+ else
536
+ pantalla.error(t('remoto.sin_red', { motivo: e.codigo === 'red' ? 'no connection' : e.detalle || e.codigo }));
537
+ }
538
+ else
539
+ throw e;
540
+ }
541
+ superficie?.pintar();
542
+ };
543
+ const soloAqui = (f) => {
544
+ const e = pantalla.espejo;
545
+ pantalla.espejo = null;
546
+ try {
547
+ f();
548
+ }
549
+ finally {
550
+ pantalla.espejo = e;
551
+ }
552
+ };
553
+ const enseñarEnlace = (url, conQR) => {
554
+ pantalla.escribir(t('remoto.on', { url: c.marca(url) }));
555
+ pantalla.escribir(c.gris(t('remoto.detalle')));
556
+ const caps = capacidades();
557
+ const qr = conQR ? qrEnTexto(url, { color: caps.color > 0, unicode: caps.unicode }) : null;
558
+ if (qr && caps.ancho >= sinColor(qr[0]).length + 4) {
559
+ pantalla.escribir(c.gris(t('remoto.escanea')));
560
+ pantalla.bloque(() => ['', ...qr.map((l) => ` ${l}`), '']);
561
+ }
562
+ };
563
+ const apagarRemoto = async () => {
564
+ const r = remoto;
565
+ remoto = null;
566
+ pantalla.espejo = null;
567
+ if (r)
568
+ await r.cerrar();
569
+ registro.apuntar('remoto', { encendido: false });
570
+ };
363
571
  const carriles = async (tareas) => {
364
572
  if (tareas.length < 2) {
365
573
  pantalla.escribir(t('carriles.min'));
@@ -407,6 +615,63 @@ async function principal() {
407
615
  quizaAvisar(t('carriles.hecho', { n: tareas.length }), ms);
408
616
  return r.ok;
409
617
  };
618
+ if (v['remote-child']) {
619
+ const cola = [];
620
+ let despertar = null;
621
+ let cortada = false;
622
+ const r = new Remoto({
623
+ servidor: cred.servidor,
624
+ token: cred.token,
625
+ alMensaje: (texto) => {
626
+ cola.push(texto);
627
+ despertar?.();
628
+ },
629
+ alParar: () => {
630
+ if (ocupado)
631
+ control.abort();
632
+ },
633
+ alApagarse: () => {
634
+ cortada = true;
635
+ if (ocupado)
636
+ control.abort();
637
+ despertar?.();
638
+ },
639
+ });
640
+ const e = equipo();
641
+ try {
642
+ await r.abrir({ equipo: e.equipo, version: e.version, carpeta: basename(raiz), modelo: nombreDeModelo(sesion.modelo), modo: nombreDeModo(sesion.ctx.modo), ...(typeof v.origen === 'string' ? { origen: v.origen } : {}) });
643
+ }
644
+ catch {
645
+ return 1;
646
+ }
647
+ remoto = r;
648
+ pantalla.espejo = r;
649
+ if (tarea) {
650
+ deFueraPendientes++;
651
+ await hacer(tarea);
652
+ }
653
+ const INACTIVA_MS = 30 * 60 * 1000;
654
+ while (!cortada) {
655
+ const siguiente = cola.shift();
656
+ if (siguiente) {
657
+ deFueraPendientes++;
658
+ await hacer(siguiente);
659
+ continue;
660
+ }
661
+ const hubo = await new Promise((res) => {
662
+ const reloj = setTimeout(() => res(false), INACTIVA_MS);
663
+ despertar = () => {
664
+ clearTimeout(reloj);
665
+ res(true);
666
+ };
667
+ });
668
+ despertar = null;
669
+ if (!hubo)
670
+ break;
671
+ }
672
+ await r.cerrar();
673
+ return 0;
674
+ }
410
675
  if (tarea) {
411
676
  cabeceraCorta([c.gris([nombreDeModo(modo), nombreDeModelo(modelo), cuenta.plan, rutaCorta(raiz)].filter(Boolean).join(sep()))]);
412
677
  if (quiereBypass && (await confirmarBypass()))
@@ -507,15 +772,14 @@ async function principal() {
507
772
  quien: quien.includes('@') ? quien : (quien.split(/\s+/)[0] ?? ''),
508
773
  carpeta: rutaCorta(raiz, 60),
509
774
  plan: cuenta.plan,
510
- novedad: (() => {
511
- const nueva = versionNuevaConocida();
512
- return nueva ? t('update.disponible', { version: nueva }) : undefined;
513
- })(),
775
+ novedad: novedadParaLaPantalla(),
514
776
  peligro: () => sesion.ctx.modo === 'bypass',
515
777
  });
516
778
  superficie.iniciar(bienvenida, consejosDeEntrada);
517
779
  if (quiereBypass)
518
780
  await confirmarBypass();
781
+ if (v.remote || ajustes.remoto)
782
+ void encenderRemoto(Boolean(v.remote));
519
783
  const mirarBandeja = async () => {
520
784
  if (ocupado)
521
785
  return;
@@ -536,6 +800,11 @@ async function principal() {
536
800
  relojBandeja.unref();
537
801
  void mirarBandeja();
538
802
  void cargarConexiones();
803
+ const relojVersiones = setInterval(() => {
804
+ if (!ocupado)
805
+ void mantenerAlDia(registro);
806
+ }, ENTRE_COMPROBACIONES_MS);
807
+ relojVersiones.unref();
539
808
  }
540
809
  const leerLinea = async (ejemplo = null) => {
541
810
  if (superficie)
@@ -962,7 +1231,7 @@ async function principal() {
962
1231
  break;
963
1232
  }
964
1233
  case 'mission': {
965
- const objetivo = objetivoDeMision(resto, sesion.diario);
1234
+ const objetivo = objetivoDeMision(resto, sesion.diario, vistaPrevia.url);
966
1235
  if (!objetivo) {
967
1236
  pantalla.escribir(t('mision.nada'));
968
1237
  break;
@@ -1053,6 +1322,47 @@ async function principal() {
1053
1322
  pantalla.escribir(pantalla.animar ? t('animacion.on') : t('animacion.off'));
1054
1323
  break;
1055
1324
  }
1325
+ case 'remote': {
1326
+ if (arg === 'off' || ALIAS_SI_NO[arg] === 'no' || arg === 'never') {
1327
+ await apagarRemoto();
1328
+ if (leerAjustes().remoto)
1329
+ guardarAjustes({ remoto: false });
1330
+ pantalla.escribir(t('remoto.off'));
1331
+ superficie?.pintar();
1332
+ break;
1333
+ }
1334
+ if (arg === 'always' || arg === 'siempre') {
1335
+ guardarAjustes({ remoto: true });
1336
+ await encenderRemoto(true);
1337
+ pantalla.escribir(c.gris(t('remoto.siempre')));
1338
+ break;
1339
+ }
1340
+ await encenderRemoto(true);
1341
+ break;
1342
+ }
1343
+ case 'preview': {
1344
+ if (!remoto?.encendido) {
1345
+ pantalla.escribir(t('vista.sin_remoto'));
1346
+ break;
1347
+ }
1348
+ const r = await actualizarVista(true);
1349
+ if (!r)
1350
+ pantalla.escribir(t('vista.sin_web'));
1351
+ else if ('error' in r)
1352
+ pantalla.error(t('vista.no_subida', { motivo: r.error }));
1353
+ else
1354
+ pantalla.escribir(t('vista.lista', { url: c.marca(r.url) }));
1355
+ break;
1356
+ }
1357
+ case 'autoupdate': {
1358
+ const si = ALIAS_SI_NO[arg] ?? (autoactualizarApagada() ? 'si' : 'no');
1359
+ guardarAjustes({ autoactualizar: si === 'si' });
1360
+ registro.apuntar('autoactualizar', { encendido: si === 'si' });
1361
+ pantalla.escribir(si === 'si' ? t('autoupdate.on') : t('autoupdate.off'));
1362
+ if (si === 'si')
1363
+ void mantenerAlDia(registro);
1364
+ break;
1365
+ }
1056
1366
  case 'undo': {
1057
1367
  const r2 = deshacerUltimo(sesion.ctx);
1058
1368
  pantalla.escribir(r2 ? t('repl.deshecho', { ruta: r2 }) : t('repl.nada_que_deshacer'));
@@ -1112,10 +1422,16 @@ async function principal() {
1112
1422
  return 0;
1113
1423
  }
1114
1424
  if (!process.env.PARX_PRUEBAS) {
1115
- principal().then((codigo) => process.exit(codigo), (e) => {
1116
- if (e instanceof ErrorDeParallel && e.codigo === 'interrumpido')
1117
- process.exit(130);
1118
- process.stderr.write(`${c.error('✗')} ${e instanceof ErrorDeParallel ? mensajeDeError(e) : e instanceof Error ? e.message : String(e)}\n`);
1119
- process.exit(1);
1425
+ lanzarLaMasNueva().then((lanzamiento) => {
1426
+ if (lanzamiento.lanzada)
1427
+ return;
1428
+ if (lanzamiento.retirada)
1429
+ lineaDeVersiones = t('update.no_arranco', { version: lanzamiento.retirada, actual: VERSION });
1430
+ principal().then((codigo) => process.exit(codigo), (e) => {
1431
+ if (e instanceof ErrorDeParallel && e.codigo === 'interrumpido')
1432
+ process.exit(130);
1433
+ process.stderr.write(`${c.error('✗')} ${e instanceof ErrorDeParallel ? mensajeDeError(e) : e instanceof Error ? e.message : String(e)}\n`);
1434
+ process.exit(1);
1435
+ });
1120
1436
  });
1121
1437
  }
package/dist/config.js CHANGED
@@ -96,6 +96,12 @@ export function leerAjustes(dir = carpetaDeConfiguracion()) {
96
96
  out.raton = a.raton;
97
97
  if (typeof a.animacion === 'boolean')
98
98
  out.animacion = a.animacion;
99
+ if (typeof a.autoactualizar === 'boolean')
100
+ out.autoactualizar = a.autoactualizar;
101
+ if (typeof a.remoto === 'boolean')
102
+ out.remoto = a.remoto;
103
+ if (Array.isArray(a.carpetasRemotas))
104
+ out.carpetasRemotas = a.carpetasRemotas.filter((x) => typeof x === 'string').slice(0, 20);
99
105
  const k = a.consejos;
100
106
  if (k && typeof k === 'object') {
101
107
  const n = (x) => (typeof x === 'number' && Number.isFinite(x) && x >= 0 ? Math.floor(x) : 0);
@@ -0,0 +1,118 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { carpetaDeConfiguracion } from "./config.js";
4
+ import { MODELOS, ORDEN_DE_MODELOS, nombreDeModelo } from "./modelos.js";
5
+ import { t } from "./textos.js";
6
+ export const PERFILES = {
7
+ pregunta: { vueltas: 2, crecimiento: 700, salidaPorVuelta: 80, salidaFinal: 350 },
8
+ pequena: { vueltas: 4, crecimiento: 900, salidaPorVuelta: 350, salidaFinal: 200 },
9
+ media: { vueltas: 8, crecimiento: 1200, salidaPorVuelta: 550, salidaFinal: 250 },
10
+ grande: { vueltas: 16, crecimiento: 1500, salidaPorVuelta: 650, salidaFinal: 350 },
11
+ };
12
+ const GRANDE = /\b(refactor|refactoriz|migrat|migra[r]?\b|rewrite|reescrib|rework|whole (project|app|codebase|repo)|entire|todo el (proyecto|código|repo)|toda la (app|aplicación)|all (the )?files|todos los ficheros|architecture|arquitectura|from scratch|desde cero|full[- ]stack|authentication|autenticaci|database|base de datos|deploy|despliega|integrat|integra[r]?\b|test suite|end[- ]to[- ]end|e2e|multi[- ]?page|varias páginas|dashboard|backend|api rest|microservic)/i;
13
+ const CREA = /\b(create|build|make|write|generate|implement|crea[r]?|construye|monta|escribe|genera|implementa|haz(me)?|new|nuev[oa])\b/i;
14
+ const SUSTANCIAL = /\b(page|página|pagina|website|web ?site|site|web|app|form|formulario|component|componente|feature|funcionalidad|endpoint|screen|pantalla|module|módulo|modulo|api|script|server|servidor|service|servicio|table|tabla|model|modelo|view|vista)\b/i;
15
+ const PEQUENA = /\b(typo|errata|rename|renombra|one[- ]line|una línea|small|pequeñ|minor|mínim|quick|rápid|simple|sencill|tiny|just|solo|sólo|only|fix the|arregla el|arregla la|change the|cambia el|cambia la|add a |añade un|añade una|remove the|quita el|quita la|update the|actualiza el)\b/i;
16
+ const PREGUNTA = /^(what|why|how|where|which|who|when|is|are|does|do|can|could|should|explain|describe|tell me|show me|list|qué|que|por qué|cómo|como|dónde|donde|cuál|cual|quién|quien|cuándo|explica|describe|dime|enséñame|lista|hay)\b/i;
17
+ function palabras(s) {
18
+ return s.trim().split(/\s+/).filter(Boolean).length;
19
+ }
20
+ export function clasificarTarea(pedido) {
21
+ const s = pedido.replace(/\s+/g, ' ').trim();
22
+ const n = palabras(s);
23
+ if (!s)
24
+ return 'pequena';
25
+ if (GRANDE.test(s) || n > 90)
26
+ return 'grande';
27
+ const pregunta = PREGUNTA.test(s) || (s.endsWith('?') && !CREA.test(s));
28
+ if (pregunta && !CREA.test(s))
29
+ return 'pregunta';
30
+ const creaAlgo = CREA.test(s) || SUSTANCIAL.test(s);
31
+ if (n <= 10 && !creaAlgo)
32
+ return 'pequena';
33
+ if (PEQUENA.test(s) && !creaAlgo)
34
+ return 'pequena';
35
+ return 'media';
36
+ }
37
+ export function tokensDe(texto) {
38
+ const chars = typeof texto === 'number' ? texto : texto.length;
39
+ return Math.ceil(chars / 3.8);
40
+ }
41
+ export function accionesEnBruto(clase, baseTokens, tarifa) {
42
+ const p = PERFILES[clase];
43
+ let entrada = 0;
44
+ let salida = 0;
45
+ for (let i = 0; i < p.vueltas; i++) {
46
+ entrada += baseTokens + i * p.crecimiento;
47
+ salida += i === p.vueltas - 1 ? p.salidaFinal : p.salidaPorVuelta;
48
+ }
49
+ return (entrada * tarifa.entrada + salida * tarifa.salida) / 1000;
50
+ }
51
+ export function redondear(n) {
52
+ if (n < 1)
53
+ return 1;
54
+ if (n < 10)
55
+ return Math.round(n);
56
+ const magnitud = 10 ** (Math.floor(Math.log10(n)) - 1);
57
+ return Math.round(n / magnitud) * magnitud;
58
+ }
59
+ export function estimarTarea(e) {
60
+ const clase = clasificarTarea(e.pedido);
61
+ const base = tokensDe(e.contextoChars) + tokensDe(e.pedido);
62
+ const acciones = {};
63
+ for (const m of ORDEN_DE_MODELOS) {
64
+ const bruto = accionesEnBruto(clase, base, e.tarifas[MODELOS[m].servidor]);
65
+ acciones[m] = redondear(bruto * factorDeCalibracion(e.calibracion, clase, m));
66
+ }
67
+ return { clase, acciones };
68
+ }
69
+ export function lineaDeEstimacion(est, modelo) {
70
+ const actual = est.acciones[modelo];
71
+ let sugerido = null;
72
+ if (modelo === 'summit' && est.clase !== 'grande')
73
+ sugerido = 'stride';
74
+ if ((modelo === 'summit' || modelo === 'stride') && (est.clase === 'pregunta' || est.clase === 'pequena'))
75
+ sugerido = 'swift';
76
+ if (sugerido && est.acciones[sugerido] <= actual * 0.6 && actual >= 5) {
77
+ return t('estimacion.con', { n: actual.toLocaleString('en-US'), modelo: nombreDeModelo(sugerido).replace(/ \d+(\.\d+)?$/, ''), otro: est.acciones[sugerido].toLocaleString('en-US') });
78
+ }
79
+ return t('estimacion.linea', { n: actual.toLocaleString('en-US') });
80
+ }
81
+ const FICHERO = 'estimaciones.json';
82
+ const PESO = 0.3;
83
+ export function leerCalibracion(dir = carpetaDeConfiguracion()) {
84
+ try {
85
+ const c = JSON.parse(readFileSync(join(dir, FICHERO), 'utf8'));
86
+ return c && typeof c === 'object' ? c : {};
87
+ }
88
+ catch {
89
+ return {};
90
+ }
91
+ }
92
+ function guardarCalibracion(c, dir) {
93
+ try {
94
+ if (!existsSync(dir))
95
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
96
+ writeFileSync(join(dir, FICHERO), JSON.stringify(c, null, 2), { mode: 0o600 });
97
+ }
98
+ catch {
99
+ }
100
+ }
101
+ export function factorDeCalibracion(c, clase, modelo) {
102
+ const f = c?.[clase]?.[modelo]?.factor;
103
+ if (typeof f !== 'number' || !Number.isFinite(f) || f <= 0)
104
+ return 1;
105
+ return Math.min(2, Math.max(0.5, f));
106
+ }
107
+ export function aprenderDeLaTarea(o) {
108
+ const dir = o.dir ?? carpetaDeConfiguracion();
109
+ const c = o.calibracion ?? leerCalibracion(dir);
110
+ if (!(o.estimada > 0) || !(o.real > 0))
111
+ return c;
112
+ const ratio = Math.min(4, Math.max(0.25, o.real / o.estimada));
113
+ const antes = c[o.clase]?.[o.modelo];
114
+ const factor = antes ? antes.factor * (1 - PESO) + ratio * PESO : ratio;
115
+ c[o.clase] = { ...(c[o.clase] ?? {}), [o.modelo]: { factor: Number(factor.toFixed(3)), n: (antes?.n ?? 0) + 1 } };
116
+ guardarCalibracion(c, dir);
117
+ return c;
118
+ }