@getparallel/parx 0.4.4 → 0.6.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,15 +6,18 @@ 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 { correoVisible, crearMision, ErrorDeParallel, leerBandeja, leerCuenta, quienEs, responderBandeja, verMision } from "./api.js";
13
- import { comoHasEntrado, credencialesActivas, entrar, formaDeLaOpcion, salir } from "./cuenta.js";
14
- import { hacerTarea, mensajeDeError, nuevaSesion } from "./agente.js";
12
+ import { cicloDeMision, compartirMision, correoVisible, crearMision, equipo, ErrorDeParallel, leerBandeja, leerConexiones, leerCuenta, leerMisionDeLaCuenta, leerMisiones, leerPlantillas, quienEs, responderBandeja, verMision, } from "./api.js";
13
+ import { abrirNavegador, comoHasEntrado, credencialesActivas, entrar, formaDeLaOpcion, salir } from "./cuenta.js";
14
+ import { accionesDeUnaMision, filtrarPorTitulo, hace } from "./comandos.js";
15
+ import { hacerTarea, mensajeDeError, nuevaSesion, reiniciarConversacion } from "./agente.js";
15
16
  import { deshacerUltimo, permitirCarpeta } from "./herramientas.js";
16
17
  import { Registro } from "./registro.js";
17
18
  import { Pantalla } from "./ui.js";
19
+ import { Remoto } from "./remoto.js";
20
+ import { qrEnTexto } from "./qr.js";
18
21
  import { acciones, bloqueDeBienvenida, bloqueDeLaPersona, bloqueDeTabla, duracion, Superficie } from "./tui.js";
19
22
  import { CONSEJOS, cambiarConsejos, consejo, elegirConsejos, ordenDeConsejos } from "./consejos.js";
20
23
  import { anchoDe, recortarMedio } from "./texto.js";
@@ -23,9 +26,9 @@ import { avisarEnElEscritorio, debeAvisar } from "./notificar.js";
23
26
  import { copiarAlPortapapeles, escribirInforme, textoParaPegar } from "./informe.js";
24
27
  import { objetivoDeMision } from "./mision.js";
25
28
  import { ejecutarCarriles, MAX_CARRILES } from "./carriles.js";
26
- import { spawnSync } from 'node:child_process';
27
29
  import { ejecutarBash, textoParaElAgente } from "./bash.js";
28
- import { comprobarDeFondo, esMasNueva, ordenDeActualizar, rutaDelCli, ultimaPublicada, versionNuevaConocida } from "./actualizar.js";
30
+ import { comprobarDeFondo, ENTRE_COMPROBACIONES_MS, esMasNueva, ordenDeActualizar, rutaDelCli, ultimaPublicada, versionNuevaConocida } from "./actualizar.js";
31
+ import { autoactualizarApagada, confirmarArranque, gestorPermiteAutoactualizar, lanzarLaMasNueva, prepararVersion } from "./versiones.js";
29
32
  function opciones() {
30
33
  return parseArgs({
31
34
  allowPositionals: true,
@@ -45,6 +48,7 @@ function opciones() {
45
48
  'no-mouse': { type: 'boolean' },
46
49
  'no-animation': { type: 'boolean' },
47
50
  animation: { type: 'boolean' },
51
+ remote: { type: 'boolean' },
48
52
  version: { type: 'boolean', short: 'v' },
49
53
  help: { type: 'boolean', short: 'h' },
50
54
  browser: { type: 'boolean' },
@@ -69,15 +73,38 @@ function rutaCorta(ruta, max = 48) {
69
73
  const corta = ruta.startsWith(casa) ? `~${ruta.slice(casa.length)}` : ruta;
70
74
  return recortarMedio(corta, max);
71
75
  }
76
+ let lineaDeVersiones = null;
77
+ function novedadParaLaPantalla() {
78
+ if (lineaDeVersiones)
79
+ return lineaDeVersiones;
80
+ if (!autoactualizarApagada())
81
+ return undefined;
82
+ const nueva = versionNuevaConocida();
83
+ return nueva ? t('update.disponible', { version: nueva }) : undefined;
84
+ }
72
85
  function cabeceraCorta(lineas = []) {
73
86
  process.stdout.write(`\n ${c.violeta(signo())} ${c.negrita('parx')} ${c.gris(VERSION)}\n`);
74
- const nueva = versionNuevaConocida();
75
- if (nueva)
76
- lineas = [c.gris(t('update.disponible', { version: nueva })), ...lineas];
87
+ const novedad = novedadParaLaPantalla();
88
+ if (novedad)
89
+ lineas = [c.gris(novedad), ...lineas];
77
90
  for (const l of lineas)
78
91
  process.stdout.write(` ${l}\n`);
79
92
  process.stdout.write('\n');
80
93
  }
94
+ async function mantenerAlDia(registro) {
95
+ try {
96
+ const nueva = await comprobarDeFondo();
97
+ if (!nueva || autoactualizarApagada() || !gestorPermiteAutoactualizar(ordenDeActualizar(rutaDelCli()).gestor))
98
+ return;
99
+ const r = await prepararVersion(nueva);
100
+ if (!r.ok)
101
+ registro?.apuntar('actualizacion', { version: nueva, motivo: r.motivo });
102
+ else if (!r.yaEstaba)
103
+ registro?.apuntar('actualizacion', { version: nueva, preparada: true });
104
+ }
105
+ catch {
106
+ }
107
+ }
81
108
  export function nombreDeModo(m) {
82
109
  return t(`modo.${m}`);
83
110
  }
@@ -92,7 +119,7 @@ export function modosEnColor() {
92
119
  return `${nombres.slice(0, 3).join(', ')} or ${nombres[3]}`;
93
120
  }
94
121
  function ayudaDeComandos() {
95
- return bloqueDeTabla([['!', t('tip.bash')], ...COMANDOS.map(([k, d]) => [`/${k}`, k === 'mode' ? modosEnColor() : d])], { pie: `${t('tui.atajos')}\n${t('tui.atajos_raton')}` });
122
+ return bloqueDeTabla([['!', t('tip.bash')], ...COMANDOS.map(([k, d]) => [`/${k}`, k === 'mode' ? modosEnColor() : d])], { pie: `${t('tui.atajos')}\n${t('tui.atajos_arroba')}\n${t('tui.atajos_raton')}` });
96
123
  }
97
124
  async function cuentaOError(cred, pantalla) {
98
125
  try {
@@ -113,7 +140,7 @@ async function actualizar() {
113
140
  process.stdout.write(`${t('update.npx')}\n`);
114
141
  return 0;
115
142
  }
116
- if (gestor === 'desarrollo' || !orden) {
143
+ if (gestor === 'desarrollo') {
117
144
  process.stdout.write(`${t('update.desarrollo')}\n`);
118
145
  return 0;
119
146
  }
@@ -126,14 +153,13 @@ async function actualizar() {
126
153
  process.stdout.write(`${t('update.al_dia', { version: VERSION })}\n`);
127
154
  return 0;
128
155
  }
129
- const texto = orden.map((p) => (/\s/.test(p) ? `"${p}"` : p)).join(' ');
130
- process.stdout.write(`${c.gris(t('update.lanzando', { orden: texto }))}\n`);
131
- const r = spawnSync(orden[0], orden.slice(1), { stdio: 'inherit', shell: process.platform === 'win32' });
132
- if (r.status !== 0) {
133
- process.stderr.write(`${c.error('✗')} ${t('update.fallo', { orden: texto })}\n`);
156
+ const aMano = (orden ?? ['npm', 'install', '-g', '@getparallel/parx@latest']).map((p) => (/\s/.test(p) ? `"${p}"` : p)).join(' ');
157
+ const r = await prepararVersion(ultima);
158
+ if (!r.ok) {
159
+ process.stderr.write(`${c.error('✗')} ${t(r.motivo === 'firma' ? 'update.firma' : 'update.no_se_pudo', { version: ultima, orden: aMano })}\n`);
134
160
  return 1;
135
161
  }
136
- process.stdout.write(`${c.violeta('✓')} ${t('update.hecho')} (${VERSION} → ${ultima})\n`);
162
+ process.stdout.write(`${c.violeta('✓')} ${t(r.yaEstaba ? 'update.ya_preparada' : 'update.preparada', { version: ultima })}${r.novedad ? c.gris(`${sep()}${r.novedad}`) : ''}\n`);
137
163
  return 0;
138
164
  }
139
165
  async function principal() {
@@ -151,6 +177,9 @@ async function principal() {
151
177
  process.stdout.write(`parx ${VERSION}\n`);
152
178
  return 0;
153
179
  }
180
+ const estreno = confirmarArranque();
181
+ if (estreno)
182
+ lineaDeVersiones = estreno.novedad ? t('update.actualizado_con', { version: estreno.version, novedad: estreno.novedad }) : t('update.actualizado', { version: estreno.version });
154
183
  if (v.help) {
155
184
  cabeceraCorta();
156
185
  process.stdout.write(`${USO}\n`);
@@ -161,7 +190,7 @@ async function principal() {
161
190
  const solo = positionals.length === 1 && !v.lanes;
162
191
  if (solo && ['update', 'upgrade'].includes(primero))
163
192
  return actualizar();
164
- void comprobarDeFondo();
193
+ void mantenerAlDia();
165
194
  if (solo && ['login', 'entrar', 'switch'].includes(primero)) {
166
195
  cabeceraCorta();
167
196
  await entrar((l) => pantalla.escribir(l), { forma: formaPedida(v) });
@@ -241,6 +270,8 @@ async function principal() {
241
270
  lecturas: [],
242
271
  jaula: Boolean(v.sandbox),
243
272
  });
273
+ if (cuenta.tarifa)
274
+ sesion.tarifas = cuenta.tarifa;
244
275
  for (const f of v['add-folder'] ?? []) {
245
276
  const ok = permitirCarpeta(sesion.ctx, f);
246
277
  if (!ok)
@@ -269,6 +300,8 @@ async function principal() {
269
300
  let control = new AbortController();
270
301
  let ocupado = false;
271
302
  let superficie = null;
303
+ let remoto = null;
304
+ let deFueraPendientes = 0;
272
305
  let bienvenida;
273
306
  let consejosDeEntrada = [];
274
307
  const confirmarNuevaEnMarcha = async () => {
@@ -282,15 +315,7 @@ async function principal() {
282
315
  control.abort();
283
316
  };
284
317
  const empezarDeNuevo = () => {
285
- sesion.conversacion = [];
286
- sesion.diario = [];
287
- sesion.id = randomUUID();
288
- sesion.ultimaTarea = undefined;
289
- sesion.ctx.hechos = { ficheros: [], comandos: [] };
290
- sesion.ctx.permitidos.clear();
291
- sesion.ctx.deshacer.length = 0;
292
- if (sesion.ctx.lecturas)
293
- sesion.ctx.lecturas.length = 0;
318
+ reiniciarConversacion(sesion);
294
319
  registro.apuntar('nueva', {});
295
320
  pantalla.reiniciar();
296
321
  if (superficie)
@@ -306,6 +331,14 @@ async function principal() {
306
331
  superficie.terminar(despedida());
307
332
  else
308
333
  pantalla.escribir(despedida());
334
+ if (remoto?.encendido) {
335
+ const salida = setTimeout(() => process.exit(0), 1500);
336
+ void remoto.cerrar().finally(() => {
337
+ clearTimeout(salida);
338
+ process.exit(0);
339
+ });
340
+ return;
341
+ }
309
342
  process.exit(0);
310
343
  };
311
344
  const alCtrlC = () => {
@@ -347,6 +380,13 @@ async function principal() {
347
380
  superficie?.trabajar(() => control.abort());
348
381
  const inicio = Date.now();
349
382
  let seguir = true;
383
+ if (remoto?.encendido) {
384
+ remoto.fijarModo(nombreDeModo(sesion.ctx.modo), nombreDeModelo(sesion.modelo));
385
+ remoto.evento('tu', { texto: texto.replace(/^\u2060/, ''), desde: deFueraPendientes > 0 ? 'movil' : 'ordenador' });
386
+ if (deFueraPendientes > 0)
387
+ deFueraPendientes--;
388
+ remoto.fijarEstado('trabajando', texto.replace(/^\u2060/, ''));
389
+ }
350
390
  try {
351
391
  seguir = await hacerTarea(sesion, texto, control.signal, imagenes);
352
392
  if (sesion.disponibles !== undefined && sesion.disponibles !== null)
@@ -355,10 +395,93 @@ async function principal() {
355
395
  finally {
356
396
  ocupado = false;
357
397
  superficie?.descansar();
398
+ remoto?.fijarEstado('libre', null);
358
399
  }
359
400
  quizaAvisar(t('aviso.termino', { tarea: texto.replace(/\s+/g, ' ').slice(0, 80) }), Date.now() - inicio);
360
401
  return seguir;
361
402
  };
403
+ const encenderRemoto = async (enseñarQR = true) => {
404
+ if (!superficie) {
405
+ pantalla.escribir(t('remoto.solo_interactiva'));
406
+ return;
407
+ }
408
+ if (remoto?.encendido && remoto.url) {
409
+ const url = remoto.url;
410
+ soloAqui(() => enseñarEnlace(url, enseñarQR));
411
+ return;
412
+ }
413
+ const nuevo = new Remoto({
414
+ servidor: cred.servidor,
415
+ token: cred.token,
416
+ alMensaje: (texto) => {
417
+ soloAqui(() => pantalla.escribir(c.gris(`${t('remoto.del_movil')}:`)));
418
+ deFueraPendientes++;
419
+ superficie?.mandarDesdeFuera(texto);
420
+ },
421
+ alParar: () => {
422
+ if (!ocupado)
423
+ return;
424
+ control.abort();
425
+ pantalla.aviso(t('remoto.parado'));
426
+ },
427
+ alApagarse: (motivo) => {
428
+ pantalla.espejo = null;
429
+ remoto = null;
430
+ pantalla.aviso(motivo === 'web' ? t('remoto.cortado_web') : motivo === 'sesion' ? t('remoto.sesion') : t('remoto.no_disponible'));
431
+ superficie?.pintar();
432
+ },
433
+ });
434
+ try {
435
+ const r = await nuevo.abrir({ equipo: equipo().equipo, version: equipo().version, carpeta: basename(raiz), modelo: nombreDeModelo(sesion.modelo), modo: nombreDeModo(sesion.ctx.modo) });
436
+ remoto = nuevo;
437
+ registro.apuntar('remoto', { encendido: true });
438
+ enseñarEnlace(r.url, enseñarQR);
439
+ pantalla.espejo = nuevo;
440
+ }
441
+ catch (e) {
442
+ if (e instanceof ErrorDeParallel) {
443
+ if (e.codigo === 'sin_acceso')
444
+ pantalla.error(t('remoto.no_disponible'));
445
+ else if (e.codigo === 'sesion')
446
+ pantalla.error(t('remoto.sesion'));
447
+ else if (e.detalle.includes('demasiados'))
448
+ pantalla.error(t('remoto.demasiados'));
449
+ else
450
+ pantalla.error(t('remoto.sin_red', { motivo: e.codigo === 'red' ? 'no connection' : e.detalle || e.codigo }));
451
+ }
452
+ else
453
+ throw e;
454
+ }
455
+ superficie?.pintar();
456
+ };
457
+ const soloAqui = (f) => {
458
+ const e = pantalla.espejo;
459
+ pantalla.espejo = null;
460
+ try {
461
+ f();
462
+ }
463
+ finally {
464
+ pantalla.espejo = e;
465
+ }
466
+ };
467
+ const enseñarEnlace = (url, conQR) => {
468
+ pantalla.escribir(t('remoto.on', { url: c.marca(url) }));
469
+ pantalla.escribir(c.gris(t('remoto.detalle')));
470
+ const caps = capacidades();
471
+ const qr = conQR ? qrEnTexto(url, { color: caps.color > 0, unicode: caps.unicode }) : null;
472
+ if (qr && caps.ancho >= sinColor(qr[0]).length + 4) {
473
+ pantalla.escribir(c.gris(t('remoto.escanea')));
474
+ pantalla.bloque(() => ['', ...qr.map((l) => ` ${l}`), '']);
475
+ }
476
+ };
477
+ const apagarRemoto = async () => {
478
+ const r = remoto;
479
+ remoto = null;
480
+ pantalla.espejo = null;
481
+ if (r)
482
+ await r.cerrar();
483
+ registro.apuntar('remoto', { encendido: false });
484
+ };
362
485
  const carriles = async (tareas) => {
363
486
  if (tareas.length < 2) {
364
487
  pantalla.escribir(t('carriles.min'));
@@ -433,6 +556,25 @@ async function principal() {
433
556
  extra: sesion.ensenar ? 'Teach' : undefined,
434
557
  esperando: esperandoEnBandeja ?? 0,
435
558
  });
559
+ let conexionesAl = 0;
560
+ let conexionesViejas = true;
561
+ let sinConexiones = false;
562
+ const cargarConexiones = async () => {
563
+ if (sinConexiones)
564
+ return;
565
+ conexionesAl = Date.now();
566
+ conexionesViejas = false;
567
+ try {
568
+ const r = await leerConexiones(cred.servidor, cred.token);
569
+ superficie?.fijarConexiones(r.conexiones);
570
+ }
571
+ catch (e) {
572
+ if (e instanceof ErrorDeParallel && (e.codigo === 'sin_acceso' || e.codigo === 'sesion'))
573
+ sinConexiones = true;
574
+ else
575
+ conexionesViejas = true;
576
+ }
577
+ };
436
578
  if (interactiva) {
437
579
  superficie = new Superficie({
438
580
  entrada: process.stdin,
@@ -452,6 +594,17 @@ async function principal() {
452
594
  avisarEnElEscritorio({ titulo: 'parx', texto: t('aviso.te_necesita', { pregunta: texto }) });
453
595
  },
454
596
  alNuevaEnMarcha: () => void confirmarNuevaEnMarcha(),
597
+ alArroba: () => {
598
+ if (conexionesViejas || Date.now() - conexionesAl > 60_000)
599
+ void cargarConexiones();
600
+ },
601
+ alConectar: (x) => {
602
+ abrirNavegador(x.url);
603
+ registro.apuntar('conectar', { provider: x.provider });
604
+ pantalla.escribir(t('arroba.abriendo', { nombre: x.nombre, url: c.marca(x.url) }));
605
+ pantalla.escribir(c.gris(t('arroba.despues')));
606
+ conexionesViejas = true;
607
+ },
455
608
  });
456
609
  const s = superficie;
457
610
  process.on('exit', () => s.restaurarDeUrgencia());
@@ -476,15 +629,14 @@ async function principal() {
476
629
  quien: quien.includes('@') ? quien : (quien.split(/\s+/)[0] ?? ''),
477
630
  carpeta: rutaCorta(raiz, 60),
478
631
  plan: cuenta.plan,
479
- novedad: (() => {
480
- const nueva = versionNuevaConocida();
481
- return nueva ? t('update.disponible', { version: nueva }) : undefined;
482
- })(),
632
+ novedad: novedadParaLaPantalla(),
483
633
  peligro: () => sesion.ctx.modo === 'bypass',
484
634
  });
485
635
  superficie.iniciar(bienvenida, consejosDeEntrada);
486
636
  if (quiereBypass)
487
637
  await confirmarBypass();
638
+ if (v.remote || ajustes.remoto)
639
+ void encenderRemoto(Boolean(v.remote));
488
640
  const mirarBandeja = async () => {
489
641
  if (ocupado)
490
642
  return;
@@ -504,6 +656,12 @@ async function principal() {
504
656
  const relojBandeja = setInterval(() => void mirarBandeja(), 90_000);
505
657
  relojBandeja.unref();
506
658
  void mirarBandeja();
659
+ void cargarConexiones();
660
+ const relojVersiones = setInterval(() => {
661
+ if (!ocupado)
662
+ void mantenerAlDia(registro);
663
+ }, ENTRE_COMPROBACIONES_MS);
664
+ relojVersiones.unref();
507
665
  }
508
666
  const leerLinea = async (ejemplo = null) => {
509
667
  if (superficie)
@@ -640,6 +798,147 @@ async function principal() {
640
798
  elegida = Math.min(elegida, lista.length - 1);
641
799
  }
642
800
  };
801
+ const pedir = async (texto, fn, errores = {}) => {
802
+ ocupado = true;
803
+ control = new AbortController();
804
+ superficie?.trabajar(() => control.abort());
805
+ pantalla.espera(texto, 'ruta', true);
806
+ try {
807
+ return await fn(control.signal);
808
+ }
809
+ catch (e) {
810
+ if (!(e instanceof ErrorDeParallel))
811
+ throw e;
812
+ const clave = errores[e.codigo === 'servidor' ? e.detalle.slice(0, 3) : e.codigo];
813
+ if (clave)
814
+ pantalla.aviso(t(clave));
815
+ else if (e.codigo !== 'interrumpido')
816
+ pantalla.error(mensajeDeError(e));
817
+ return null;
818
+ }
819
+ finally {
820
+ pantalla.pararEspera();
821
+ ocupado = false;
822
+ superficie?.descansar();
823
+ }
824
+ };
825
+ const notaDeMision = (m) => [t('misiones.progreso', { estado: m.texto || m.estado, n: m.progreso }), m.pendientes ? t('misiones.esperan', { n: m.pendientes }) : ''].filter(Boolean).join(sep());
826
+ const erroresDeMision = { sin_acceso: 'misiones.no_existe', '409': 'misiones.no_se_puede', '429': 'misiones.despacio' };
827
+ const unaMision = async (inicial) => {
828
+ let m = inicial;
829
+ let elegida = 0;
830
+ for (;;) {
831
+ const acciones = accionesDeUnaMision(m.estado);
832
+ const j = await superficie.elegir(m.titulo, [...acciones.map((a) => ({ etiqueta: a.etiqueta, nota: a.nota })), { etiqueta: t('misiones.volver') }], {
833
+ vertical: true,
834
+ detalle: notaDeMision(m),
835
+ elegida,
836
+ });
837
+ if (j === null)
838
+ return 'salir';
839
+ if (j >= acciones.length)
840
+ return 'volver';
841
+ elegida = j;
842
+ const a = acciones[j];
843
+ registro.apuntar('mision_accion', { accion: a.slug });
844
+ if (a.id === 'abrir') {
845
+ abrirNavegador(m.url);
846
+ pantalla.escribir(t('misiones.abierta', { url: c.marca(m.url) }));
847
+ }
848
+ else if (a.id === 'pausar' || a.id === 'reanudar') {
849
+ const accion = a.id;
850
+ const r = await pedir(t('misiones.mirando'), (s) => cicloDeMision(cred.servidor, cred.token, m.id, accion, s), erroresDeMision);
851
+ if (r) {
852
+ pantalla.escribir(`${c.ok(SIMBOLOS.hecho)} ${t(accion === 'pausar' ? 'misiones.pausada' : 'misiones.reanudada', { titulo: m.titulo })}`);
853
+ const otra = await leerMisiones(cred.servidor, cred.token).catch(() => null);
854
+ m = otra?.misiones.find((x) => x.id === m.id) ?? { ...m, estado: r.estado };
855
+ elegida = 0;
856
+ }
857
+ }
858
+ else if (a.id === 'compartir') {
859
+ const r = await pedir(t('misiones.mirando'), (s) => compartirMision(cred.servidor, cred.token, m.id, s), { ...erroresDeMision, '503': 'misiones.sin_compartir' });
860
+ if (r)
861
+ pantalla.escribir(t(copiarAlPortapapeles(r.url) ? 'misiones.compartida' : 'misiones.compartida_sin_copiar', { url: c.marca(r.url) }));
862
+ }
863
+ else {
864
+ const r = await pedir(t('misiones.mirando'), (s) => leerMisionDeLaCuenta(cred.servidor, cred.token, m.id, s), erroresDeMision);
865
+ if (!r)
866
+ continue;
867
+ const d = r.mision;
868
+ const titulo = `${a.id === 'resultado' ? t('misiones.resultado') : t('misiones.actividad')}${sep()}${d.titulo}`;
869
+ if (a.id === 'resultado') {
870
+ const filas = [
871
+ [t('misiones.meta'), d.objetivo ? t('misiones.objetivo', { etiqueta: d.objetivo.etiqueta, valor: d.objetivo.valor.toLocaleString('en-US'), meta: d.objetivo.meta.toLocaleString('en-US') }) : t('misiones.sin_objetivo')],
872
+ [t('misiones.el_plan'), t('misiones.plan', { estado: d.texto || d.estado, n: d.progreso })],
873
+ ];
874
+ pantalla.bloque(bloqueDeTabla(filas, { titulo, pie: t('misiones.ver_resultados', { url: d.resultados }), sangria: 2 }));
875
+ }
876
+ else {
877
+ pantalla.bloque(bloqueDeTabla(d.actividad.map((x) => [hace(x.cuando), x.texto]), { titulo, pie: d.actividad.length ? t('misiones.ver_actividad', { url: d.url }) : t('misiones.sin_actividad'), sangria: 2 }));
878
+ }
879
+ }
880
+ }
881
+ };
882
+ const misiones = async (filtro) => {
883
+ const leer = () => pedir(t('misiones.mirando'), (s) => leerMisiones(cred.servidor, cred.token, s), { sin_acceso: 'misiones.no_disponible', '429': 'misiones.despacio' });
884
+ let r = await leer();
885
+ if (!r)
886
+ return;
887
+ if (!r.misiones.length) {
888
+ pantalla.escribir(t('misiones.nada'));
889
+ return;
890
+ }
891
+ let lista = filtrarPorTitulo(r.misiones, filtro);
892
+ if (!lista.length) {
893
+ pantalla.escribir(t('misiones.sin_coincidencias', { q: filtro }));
894
+ return;
895
+ }
896
+ if (!superficie) {
897
+ for (const m of lista)
898
+ pantalla.escribir(`${m.titulo}${c.gris(`${sep()}${notaDeMision(m)}`)} ${c.gris(m.url)}`);
899
+ return;
900
+ }
901
+ let elegida = 0;
902
+ for (;;) {
903
+ const i = await superficie.elegir(t('misiones.titulo'), lista.map((m) => ({ etiqueta: m.titulo, nota: notaDeMision(m) })), { vertical: true, elegida });
904
+ if (i === null)
905
+ return;
906
+ elegida = i;
907
+ if ((await unaMision(lista[i])) === 'salir')
908
+ return;
909
+ r = await leer();
910
+ if (!r)
911
+ return;
912
+ lista = filtrarPorTitulo(r.misiones, filtro);
913
+ if (!lista.length)
914
+ return;
915
+ elegida = Math.min(elegida, lista.length - 1);
916
+ }
917
+ };
918
+ const plantillas = async (filtro) => {
919
+ const r = await pedir(t('plantillas.mirando'), (s) => leerPlantillas(cred.servidor, cred.token, s), { sin_acceso: 'misiones.no_disponible', '429': 'misiones.despacio' });
920
+ if (!r)
921
+ return;
922
+ if (!r.plantillas.length) {
923
+ pantalla.escribir(t('plantillas.nada'));
924
+ return;
925
+ }
926
+ const lista = filtrarPorTitulo(r.plantillas, filtro);
927
+ if (!lista.length) {
928
+ pantalla.escribir(t('plantillas.sin_coincidencias', { q: filtro }));
929
+ return;
930
+ }
931
+ if (!superficie) {
932
+ for (const p of lista)
933
+ pantalla.escribir(`${p.titulo}${p.resumen ? c.gris(`${sep()}${p.resumen}`) : ''}`);
934
+ return;
935
+ }
936
+ const i = await superficie.elegir(t('plantillas.titulo'), lista.map((p) => ({ etiqueta: p.titulo, nota: p.propia ? t('plantillas.propia') : p.resumen })), { vertical: true });
937
+ if (i === null)
938
+ return;
939
+ superficie.ponerEnLaCaja(`/mission ${lista[i].objetivo.replace(/\s+/g, ' ').trim()}`);
940
+ pantalla.escribir(c.gris(t('plantillas.lista')));
941
+ };
643
942
  const bash = async (comando) => {
644
943
  if (!comando)
645
944
  return;
@@ -846,6 +1145,23 @@ async function principal() {
846
1145
  await bandeja();
847
1146
  superficie?.pintar();
848
1147
  break;
1148
+ case 'missions':
1149
+ await misiones(resto);
1150
+ superficie?.pintar();
1151
+ break;
1152
+ case 'templates':
1153
+ await plantillas(resto);
1154
+ superficie?.pintar();
1155
+ break;
1156
+ case 'connections':
1157
+ case 'settings': {
1158
+ const url = `${cred.servidor.replace(/\/$/, '')}/${cmd}`;
1159
+ abrirNavegador(url);
1160
+ if (cmd === 'connections')
1161
+ conexionesViejas = true;
1162
+ pantalla.escribir(t('ir.abriendo', { que: t(cmd === 'connections' ? 'ir.conexiones' : 'ir.ajustes'), url: c.marca(url) }));
1163
+ break;
1164
+ }
849
1165
  case 'mouse': {
850
1166
  const si = ALIAS_SI_NO[arg] ?? (superficie?.conRaton ? 'no' : 'si');
851
1167
  guardarAjustes({ raton: si === 'si' });
@@ -863,6 +1179,33 @@ async function principal() {
863
1179
  pantalla.escribir(pantalla.animar ? t('animacion.on') : t('animacion.off'));
864
1180
  break;
865
1181
  }
1182
+ case 'remote': {
1183
+ if (arg === 'off' || ALIAS_SI_NO[arg] === 'no' || arg === 'never') {
1184
+ await apagarRemoto();
1185
+ if (leerAjustes().remoto)
1186
+ guardarAjustes({ remoto: false });
1187
+ pantalla.escribir(t('remoto.off'));
1188
+ superficie?.pintar();
1189
+ break;
1190
+ }
1191
+ if (arg === 'always' || arg === 'siempre') {
1192
+ guardarAjustes({ remoto: true });
1193
+ await encenderRemoto(true);
1194
+ pantalla.escribir(c.gris(t('remoto.siempre')));
1195
+ break;
1196
+ }
1197
+ await encenderRemoto(true);
1198
+ break;
1199
+ }
1200
+ case 'autoupdate': {
1201
+ const si = ALIAS_SI_NO[arg] ?? (autoactualizarApagada() ? 'si' : 'no');
1202
+ guardarAjustes({ autoactualizar: si === 'si' });
1203
+ registro.apuntar('autoactualizar', { encendido: si === 'si' });
1204
+ pantalla.escribir(si === 'si' ? t('autoupdate.on') : t('autoupdate.off'));
1205
+ if (si === 'si')
1206
+ void mantenerAlDia(registro);
1207
+ break;
1208
+ }
866
1209
  case 'undo': {
867
1210
  const r2 = deshacerUltimo(sesion.ctx);
868
1211
  pantalla.escribir(r2 ? t('repl.deshecho', { ruta: r2 }) : t('repl.nada_que_deshacer'));
@@ -922,10 +1265,16 @@ async function principal() {
922
1265
  return 0;
923
1266
  }
924
1267
  if (!process.env.PARX_PRUEBAS) {
925
- principal().then((codigo) => process.exit(codigo), (e) => {
926
- if (e instanceof ErrorDeParallel && e.codigo === 'interrumpido')
927
- process.exit(130);
928
- process.stderr.write(`${c.error('✗')} ${e instanceof ErrorDeParallel ? mensajeDeError(e) : e instanceof Error ? e.message : String(e)}\n`);
929
- process.exit(1);
1268
+ lanzarLaMasNueva().then((lanzamiento) => {
1269
+ if (lanzamiento.lanzada)
1270
+ return;
1271
+ if (lanzamiento.retirada)
1272
+ lineaDeVersiones = t('update.no_arranco', { version: lanzamiento.retirada, actual: VERSION });
1273
+ principal().then((codigo) => process.exit(codigo), (e) => {
1274
+ if (e instanceof ErrorDeParallel && e.codigo === 'interrumpido')
1275
+ process.exit(130);
1276
+ process.stderr.write(`${c.error('✗')} ${e instanceof ErrorDeParallel ? mensajeDeError(e) : e instanceof Error ? e.message : String(e)}\n`);
1277
+ process.exit(1);
1278
+ });
930
1279
  });
931
1280
  }
@@ -0,0 +1,71 @@
1
+ export const DE_LA_WEB = {
2
+ plantillas: { slug: 'templates', parx: 'templates', como: 'nuevo', porque: 'Pick one and it is written as /mission <goal> in the input, to review and send.' },
3
+ adjuntar: { slug: 'attach', parx: null, como: 'no', porque: 'parx already reads the files of your project; images go in by dragging them into the window.' },
4
+ fuerza: { slug: 'strength', parx: 'model', como: 'alias', porque: 'In parx the strength is the model: Swift, Stride or Summit.' },
5
+ dictar: { slug: 'dictate', parx: null, como: 'no', porque: 'A terminal has no microphone of its own; the dictation of your system still types into it.' },
6
+ nueva: { slug: 'new', parx: 'new', como: 'choque', porque: 'parx keeps /new as a new conversation; a new mission is /mission.' },
7
+ misiones: { slug: 'missions', parx: 'missions', como: 'nuevo', porque: 'Your missions; pick one to open, pause or resume, share, see its result or its activity.' },
8
+ conexiones: { slug: 'connections', parx: 'connections', como: 'nuevo', porque: 'Opens your connections on getparallel.net.' },
9
+ ajustes: { slug: 'settings', parx: 'settings', como: 'nuevo', porque: 'Opens your settings on getparallel.net.' },
10
+ abrir: { slug: 'open', parx: null, como: 'en_misiones', porque: 'Inside /missions.' },
11
+ pausar: { slug: 'pause', parx: null, como: 'en_misiones', porque: 'Inside /missions, only for a running mission.' },
12
+ reanudar: { slug: 'resume', parx: null, como: 'en_misiones', porque: 'Inside /missions, only for a paused mission.' },
13
+ compartir: { slug: 'share', parx: null, como: 'en_misiones', porque: 'Inside /missions.' },
14
+ resultado: { slug: 'result', parx: null, como: 'en_misiones', porque: 'Inside /missions.' },
15
+ actividad: { slug: 'activity', parx: null, como: 'en_misiones', porque: 'Inside /missions.' },
16
+ ayuda: { slug: 'help', parx: 'help', como: 'ya', porque: 'parx already had /help.' },
17
+ };
18
+ export const ACCIONES_DE_MISION = [
19
+ { id: 'abrir', slug: 'open', etiqueta: 'Open', nota: 'In the browser' },
20
+ { id: 'pausar', slug: 'pause', etiqueta: 'Pause', nota: 'Stop the agents' },
21
+ { id: 'reanudar', slug: 'resume', etiqueta: 'Resume', nota: 'Pick up where it was' },
22
+ { id: 'compartir', slug: 'share', etiqueta: 'Share', nota: 'Read-only link, copied' },
23
+ { id: 'resultado', slug: 'result', etiqueta: 'Result', nota: 'How the goal is going' },
24
+ { id: 'actividad', slug: 'activity', etiqueta: 'Activity', nota: 'What the agents did' },
25
+ ];
26
+ export function accionDeMisionPosible(id, estado) {
27
+ if (id === 'pausar')
28
+ return estado === 'active' || estado === 'at_risk';
29
+ if (id === 'reanudar')
30
+ return estado === 'paused';
31
+ return true;
32
+ }
33
+ export function accionesDeUnaMision(estado) {
34
+ return ACCIONES_DE_MISION.filter((a) => accionDeMisionPosible(a.id, estado));
35
+ }
36
+ export function normalizar(texto) {
37
+ return texto.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
38
+ }
39
+ export function filtrarPorTitulo(lista, consulta) {
40
+ const q = normalizar(consulta.trim());
41
+ if (!q)
42
+ return [...lista];
43
+ const puntuados = [];
44
+ lista.forEach((x, i) => {
45
+ const titulo = normalizar(x.titulo);
46
+ let p = -1;
47
+ if (titulo.startsWith(q))
48
+ p = 0;
49
+ else if (titulo.split(/[\s._-]+/).some((w) => w.startsWith(q)))
50
+ p = 1;
51
+ else if (titulo.includes(q))
52
+ p = 2;
53
+ if (p >= 0)
54
+ puntuados.push({ x, p, i });
55
+ });
56
+ return puntuados.sort((a, b) => a.p - b.p || a.i - b.i).map((v) => v.x);
57
+ }
58
+ export function hace(iso, ahora = Date.now()) {
59
+ const ms = ahora - new Date(iso).getTime();
60
+ if (!Number.isFinite(ms))
61
+ return '';
62
+ const min = Math.floor(ms / 60_000);
63
+ if (min < 1)
64
+ return 'just now';
65
+ if (min < 60)
66
+ return `${min}m ago`;
67
+ const h = Math.floor(min / 60);
68
+ if (h < 24)
69
+ return `${h}h ago`;
70
+ return `${Math.floor(h / 24)}d ago`;
71
+ }