@getparallel/parx 0.6.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/CHANGELOG.md +7 -0
- package/dist/api.js +3 -0
- package/dist/cli.js +158 -1
- package/dist/config.js +2 -0
- package/dist/mision.js +4 -3
- package/dist/remoto.js +11 -0
- package/dist/servicio.js +191 -0
- package/dist/textos.js +24 -0
- package/dist/ui.js +10 -0
- package/dist/vista.js +133 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,13 @@ changes). Entries are generated by `npm run release` from the git history of `ap
|
|
|
6
6
|
|
|
7
7
|
<!-- Notes written under «## Unreleased» are used as the next entry instead of the git log. -->
|
|
8
8
|
|
|
9
|
+
## 0.7.0 (2026-10-18)
|
|
10
|
+
|
|
11
|
+
- Un paso corto ya no tumba el plan; «Llévalo a tu negocio» lleva a la misión en marcha y sin palabras que disparen la plantilla de clientes
|
|
12
|
+
- Del código al negocio (x03): «Llévalo a tu negocio» en Sesiones y /mission con el enlace de la web
|
|
13
|
+
- Ver en el móvil la web que construye (x01)
|
|
14
|
+
- El Mac siempre listo (x07): parx service on/off/add/remove/status, tareas abiertas desde el móvil
|
|
15
|
+
|
|
9
16
|
## 0.6.0 (2026-10-18)
|
|
10
17
|
|
|
11
18
|
- parx updates itself. Every few hours it checks npm in the background; a new version is downloaded, verified (npm's registry signature against npm's public key pinned in parx, plus the package's SHA-512) and start-tested, then kept ready in `~/.config/parx/versiones`. It starts with your next session, never mid-session, with one line: «parx 0.5.1 updated · what's new». If a new version fails to start, parx goes back to the previous one on its own. `/autoupdate off` (or `PARX_NO_AUTO_UPDATE=1`) keeps the old behaviour: a line when there is a new version, and `parx update` installs it.
|
package/dist/api.js
CHANGED
|
@@ -128,6 +128,9 @@ export function latidoRemoto(servidor, token, id, cuerpo, señal) {
|
|
|
128
128
|
export function ordenesRemotas(servidor, token, id, esperaMs, señal) {
|
|
129
129
|
return llamar(servidor, `/api/terminal/remoto/${encodeURIComponent(id)}/ordenes?espera=${esperaMs}`, { token, señal, plazoMs: esperaMs + 15_000 });
|
|
130
130
|
}
|
|
131
|
+
export function subirVistaRemota(servidor, token, id, cuerpo) {
|
|
132
|
+
return llamar(servidor, `/api/terminal/remoto/${encodeURIComponent(id)}/vista`, { token, cuerpo, plazoMs: 60_000 });
|
|
133
|
+
}
|
|
131
134
|
export function cerrarRemoto(servidor, token, id, plazoMs = 3000) {
|
|
132
135
|
return llamar(servidor, `/api/terminal/remoto/${encodeURIComponent(id)}`, { token, metodo: 'DELETE', plazoMs });
|
|
133
136
|
}
|
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,8 @@ import { deshacerUltimo, permitirCarpeta } from "./herramientas.js";
|
|
|
17
17
|
import { Registro } from "./registro.js";
|
|
18
18
|
import { Pantalla } from "./ui.js";
|
|
19
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";
|
|
20
22
|
import { qrEnTexto } from "./qr.js";
|
|
21
23
|
import { acciones, bloqueDeBienvenida, bloqueDeLaPersona, bloqueDeTabla, duracion, Superficie } from "./tui.js";
|
|
22
24
|
import { CONSEJOS, cambiarConsejos, consejo, elegirConsejos, ordenDeConsejos } from "./consejos.js";
|
|
@@ -49,6 +51,9 @@ function opciones() {
|
|
|
49
51
|
'no-animation': { type: 'boolean' },
|
|
50
52
|
animation: { type: 'boolean' },
|
|
51
53
|
remote: { type: 'boolean' },
|
|
54
|
+
serve: { type: 'boolean' },
|
|
55
|
+
'remote-child': { type: 'boolean' },
|
|
56
|
+
origen: { type: 'string' },
|
|
52
57
|
version: { type: 'boolean', short: 'v' },
|
|
53
58
|
help: { type: 'boolean', short: 'h' },
|
|
54
59
|
browser: { type: 'boolean' },
|
|
@@ -222,6 +227,62 @@ async function principal() {
|
|
|
222
227
|
pantalla.escribir();
|
|
223
228
|
return 0;
|
|
224
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
|
+
}
|
|
225
286
|
const modoPedido = (v.mode ?? v.modo);
|
|
226
287
|
const pedido = modoDe(modoPedido);
|
|
227
288
|
if (pedido === 'bypass') {
|
|
@@ -302,6 +363,24 @@ async function principal() {
|
|
|
302
363
|
let superficie = null;
|
|
303
364
|
let remoto = null;
|
|
304
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
|
+
};
|
|
305
384
|
let bienvenida;
|
|
306
385
|
let consejosDeEntrada = [];
|
|
307
386
|
const confirmarNuevaEnMarcha = async () => {
|
|
@@ -397,6 +476,12 @@ async function principal() {
|
|
|
397
476
|
superficie?.descansar();
|
|
398
477
|
remoto?.fijarEstado('libre', null);
|
|
399
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'))));
|
|
484
|
+
}
|
|
400
485
|
quizaAvisar(t('aviso.termino', { tarea: texto.replace(/\s+/g, ' ').slice(0, 80) }), Date.now() - inicio);
|
|
401
486
|
return seguir;
|
|
402
487
|
};
|
|
@@ -434,6 +519,7 @@ async function principal() {
|
|
|
434
519
|
try {
|
|
435
520
|
const r = await nuevo.abrir({ equipo: equipo().equipo, version: equipo().version, carpeta: basename(raiz), modelo: nombreDeModelo(sesion.modelo), modo: nombreDeModo(sesion.ctx.modo) });
|
|
436
521
|
remoto = nuevo;
|
|
522
|
+
vistaPrevia = new VistaPrevia();
|
|
437
523
|
registro.apuntar('remoto', { encendido: true });
|
|
438
524
|
enseñarEnlace(r.url, enseñarQR);
|
|
439
525
|
pantalla.espejo = nuevo;
|
|
@@ -529,6 +615,63 @@ async function principal() {
|
|
|
529
615
|
quizaAvisar(t('carriles.hecho', { n: tareas.length }), ms);
|
|
530
616
|
return r.ok;
|
|
531
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
|
+
}
|
|
532
675
|
if (tarea) {
|
|
533
676
|
cabeceraCorta([c.gris([nombreDeModo(modo), nombreDeModelo(modelo), cuenta.plan, rutaCorta(raiz)].filter(Boolean).join(sep()))]);
|
|
534
677
|
if (quiereBypass && (await confirmarBypass()))
|
|
@@ -1088,7 +1231,7 @@ async function principal() {
|
|
|
1088
1231
|
break;
|
|
1089
1232
|
}
|
|
1090
1233
|
case 'mission': {
|
|
1091
|
-
const objetivo = objetivoDeMision(resto, sesion.diario);
|
|
1234
|
+
const objetivo = objetivoDeMision(resto, sesion.diario, vistaPrevia.url);
|
|
1092
1235
|
if (!objetivo) {
|
|
1093
1236
|
pantalla.escribir(t('mision.nada'));
|
|
1094
1237
|
break;
|
|
@@ -1197,6 +1340,20 @@ async function principal() {
|
|
|
1197
1340
|
await encenderRemoto(true);
|
|
1198
1341
|
break;
|
|
1199
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
|
+
}
|
|
1200
1357
|
case 'autoupdate': {
|
|
1201
1358
|
const si = ALIAS_SI_NO[arg] ?? (autoactualizarApagada() ? 'si' : 'no');
|
|
1202
1359
|
guardarAjustes({ autoactualizar: si === 'si' });
|
package/dist/config.js
CHANGED
|
@@ -100,6 +100,8 @@ export function leerAjustes(dir = carpetaDeConfiguracion()) {
|
|
|
100
100
|
out.autoactualizar = a.autoactualizar;
|
|
101
101
|
if (typeof a.remoto === 'boolean')
|
|
102
102
|
out.remoto = a.remoto;
|
|
103
|
+
if (Array.isArray(a.carpetasRemotas))
|
|
104
|
+
out.carpetasRemotas = a.carpetasRemotas.filter((x) => typeof x === 'string').slice(0, 20);
|
|
103
105
|
const k = a.consejos;
|
|
104
106
|
if (k && typeof k === 'object') {
|
|
105
107
|
const n = (x) => (typeof x === 'number' && Number.isFinite(x) && x >= 0 ? Math.floor(x) : 0);
|
package/dist/mision.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
export const MAX_OBJETIVO = 600;
|
|
2
|
-
export function objetivoDeMision(meta, diario) {
|
|
2
|
+
export function objetivoDeMision(meta, diario, vista = null) {
|
|
3
3
|
const ultima = [...diario].reverse().find((d) => d.pedido.trim());
|
|
4
4
|
const que = (meta.trim() || ultima?.pedido.trim() || '').replace(/\s+/g, ' ');
|
|
5
5
|
if (que.length < 4)
|
|
6
6
|
return null;
|
|
7
7
|
const ficheros = [...new Set(diario.flatMap((d) => d.ficheros))].slice(0, 6);
|
|
8
|
-
const
|
|
9
|
-
|
|
8
|
+
const enlace = vista ? `preview of the website (temporary link, valid 24 hours): ${vista}. ` : '';
|
|
9
|
+
const contexto = ultima || vista
|
|
10
|
+
? ` Context from the terminal (parx): ${enlace}${ficheros.length ? `changed ${ficheros.join(', ')}. ` : ''}${ultima?.respuesta ? `Last result: ${ultima.respuesta.replace(/\s+/g, ' ')}` : ''}`
|
|
10
11
|
: '';
|
|
11
12
|
const base = que.length > MAX_OBJETIVO ? `${que.slice(0, MAX_OBJETIVO - 1)}…` : que;
|
|
12
13
|
const sitio = MAX_OBJETIVO - base.length;
|
package/dist/remoto.js
CHANGED
|
@@ -242,9 +242,20 @@ export class Remoto {
|
|
|
242
242
|
return;
|
|
243
243
|
}
|
|
244
244
|
if (orden.tipo === 'parar') {
|
|
245
|
+
for (const [id, p] of this.preguntas) {
|
|
246
|
+
this.preguntas.delete(id);
|
|
247
|
+
p.resolver('no');
|
|
248
|
+
}
|
|
245
249
|
this.o.alParar();
|
|
246
250
|
return;
|
|
247
251
|
}
|
|
252
|
+
if (orden.tipo === 'abrir') {
|
|
253
|
+
const carpeta = typeof orden.datos.carpeta === 'string' ? orden.datos.carpeta : '';
|
|
254
|
+
const texto = typeof orden.datos.texto === 'string' ? orden.datos.texto.trim().slice(0, MAX_TEXTO) : '';
|
|
255
|
+
if (carpeta && texto)
|
|
256
|
+
this.o.alAbrir?.({ orden: orden.id, carpeta, texto });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
248
259
|
if (orden.tipo === 'respuesta') {
|
|
249
260
|
const id = String(orden.datos.pregunta ?? '');
|
|
250
261
|
const valor = orden.datos.valor;
|
package/dist/servicio.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, mkdirSync, openSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { homedir, hostname } from 'node:os';
|
|
5
|
+
import { basename, join, resolve } from 'node:path';
|
|
6
|
+
import { equipo, ErrorDeParallel } from "./api.js";
|
|
7
|
+
import { guardarAjustes, leerAjustes } from "./config.js";
|
|
8
|
+
import { Remoto } from "./remoto.js";
|
|
9
|
+
export const ETIQUETA = 'net.getparallel.parx';
|
|
10
|
+
export const MAX_SESIONES = 3;
|
|
11
|
+
export function idDeCarpeta(ruta) {
|
|
12
|
+
return createHash('sha256').update(ruta).digest('hex').slice(0, 16);
|
|
13
|
+
}
|
|
14
|
+
export function carpetasDelServicio(rutas = leerAjustes().carpetasRemotas ?? []) {
|
|
15
|
+
return rutas
|
|
16
|
+
.filter((r) => {
|
|
17
|
+
try {
|
|
18
|
+
return existsSync(r) && statSync(r).isDirectory();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
.map((ruta) => ({ id: idDeCarpeta(ruta), nombre: basename(ruta) || ruta, ruta }));
|
|
25
|
+
}
|
|
26
|
+
export function anadirCarpeta(ruta) {
|
|
27
|
+
const abs = resolve(ruta);
|
|
28
|
+
if (!existsSync(abs) || !statSync(abs).isDirectory())
|
|
29
|
+
return null;
|
|
30
|
+
const real = realpathSync(abs);
|
|
31
|
+
if (real === homedir() || real === '/' || real === realpathSync(homedir()))
|
|
32
|
+
return null;
|
|
33
|
+
const lista = leerAjustes().carpetasRemotas ?? [];
|
|
34
|
+
if (!lista.includes(real))
|
|
35
|
+
guardarAjustes({ carpetasRemotas: [...lista, real].slice(-20) });
|
|
36
|
+
return real;
|
|
37
|
+
}
|
|
38
|
+
export function quitarCarpeta(ruta) {
|
|
39
|
+
const lista = leerAjustes().carpetasRemotas ?? [];
|
|
40
|
+
let real = resolve(ruta);
|
|
41
|
+
try {
|
|
42
|
+
real = realpathSync(real);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
}
|
|
46
|
+
const queda = lista.filter((x) => x !== real && basename(x) !== ruta);
|
|
47
|
+
guardarAjustes({ carpetasRemotas: queda });
|
|
48
|
+
return queda.length !== lista.length;
|
|
49
|
+
}
|
|
50
|
+
export const rutaDelPlist = () => join(homedir(), 'Library', 'LaunchAgents', `${ETIQUETA}.plist`);
|
|
51
|
+
export const rutaDelRegistro = () => join(homedir(), 'Library', 'Logs', 'parx-service.log');
|
|
52
|
+
const xml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
53
|
+
export function plistDelServicio(o) {
|
|
54
|
+
const args = [o.node, ...o.argumentos, '--serve'].map((a) => ` <string>${xml(a)}</string>`).join('\n');
|
|
55
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
56
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
57
|
+
<plist version="1.0">
|
|
58
|
+
<dict>
|
|
59
|
+
<key>Label</key>
|
|
60
|
+
<string>${ETIQUETA}</string>
|
|
61
|
+
<key>ProgramArguments</key>
|
|
62
|
+
<array>
|
|
63
|
+
${args}
|
|
64
|
+
</array>
|
|
65
|
+
<key>RunAtLoad</key>
|
|
66
|
+
<true/>
|
|
67
|
+
<key>KeepAlive</key>
|
|
68
|
+
<dict>
|
|
69
|
+
<key>SuccessfulExit</key>
|
|
70
|
+
<false/>
|
|
71
|
+
</dict>
|
|
72
|
+
<key>ThrottleInterval</key>
|
|
73
|
+
<integer>30</integer>
|
|
74
|
+
<key>EnvironmentVariables</key>
|
|
75
|
+
<dict>
|
|
76
|
+
<key>PATH</key>
|
|
77
|
+
<string>${xml(o.path)}</string>
|
|
78
|
+
</dict>
|
|
79
|
+
<key>StandardOutPath</key>
|
|
80
|
+
<string>${xml(o.registro)}</string>
|
|
81
|
+
<key>StandardErrorPath</key>
|
|
82
|
+
<string>${xml(o.registro)}</string>
|
|
83
|
+
</dict>
|
|
84
|
+
</plist>
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
const dominio = () => `gui/${process.getuid?.() ?? 501}`;
|
|
88
|
+
export function servicioInstalado() {
|
|
89
|
+
return existsSync(rutaDelPlist());
|
|
90
|
+
}
|
|
91
|
+
export function instalarServicio() {
|
|
92
|
+
if (process.platform !== 'darwin')
|
|
93
|
+
throw new Error('solo_mac');
|
|
94
|
+
const plist = rutaDelPlist();
|
|
95
|
+
mkdirSync(join(homedir(), 'Library', 'LaunchAgents'), { recursive: true });
|
|
96
|
+
mkdirSync(join(homedir(), 'Library', 'Logs'), { recursive: true });
|
|
97
|
+
writeFileSync(plist, plistDelServicio({ node: process.execPath, argumentos: [...process.execArgv, process.argv[1]], path: process.env.PATH ?? '/usr/bin:/bin', registro: rutaDelRegistro() }), { mode: 0o644 });
|
|
98
|
+
try {
|
|
99
|
+
execFileSync('launchctl', ['bootout', `${dominio()}/${ETIQUETA}`], { stdio: 'ignore' });
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
}
|
|
103
|
+
execFileSync('launchctl', ['bootstrap', dominio(), plist], { stdio: 'ignore' });
|
|
104
|
+
}
|
|
105
|
+
export function reiniciarServicio() {
|
|
106
|
+
if (!servicioInstalado())
|
|
107
|
+
return;
|
|
108
|
+
try {
|
|
109
|
+
execFileSync('launchctl', ['kickstart', '-k', `${dominio()}/${ETIQUETA}`], { stdio: 'ignore' });
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export function quitarServicio() {
|
|
115
|
+
try {
|
|
116
|
+
execFileSync('launchctl', ['bootout', `${dominio()}/${ETIQUETA}`], { stdio: 'ignore' });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
}
|
|
120
|
+
rmSync(rutaDelPlist(), { force: true });
|
|
121
|
+
}
|
|
122
|
+
export function servicioEnMarcha() {
|
|
123
|
+
try {
|
|
124
|
+
execFileSync('launchctl', ['print', `${dominio()}/${ETIQUETA}`], { stdio: 'ignore' });
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const log = (...a) => console.log(new Date().toISOString(), ...a);
|
|
132
|
+
const dormir = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
133
|
+
export function lanzarSesion(o) {
|
|
134
|
+
const salida = openSync(rutaDelRegistro(), 'a');
|
|
135
|
+
return spawn(process.execPath, [...process.execArgv, process.argv[1], '--remote-child', '--origen', o.orden, '--dir', o.ruta, '--', o.texto], {
|
|
136
|
+
cwd: o.ruta,
|
|
137
|
+
stdio: ['ignore', salida, salida],
|
|
138
|
+
env: { ...process.env, PARX_NO_UPDATE_CHECK: '1' },
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
export async function servir(cred) {
|
|
142
|
+
const carpetas = carpetasDelServicio();
|
|
143
|
+
if (!carpetas.length) {
|
|
144
|
+
log('no folders: run `parx service add` in a project folder');
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
let vivas = 0;
|
|
148
|
+
let fin = null;
|
|
149
|
+
const terminado = new Promise((r) => (fin = r));
|
|
150
|
+
const remoto = new Remoto({
|
|
151
|
+
servidor: cred.servidor,
|
|
152
|
+
token: cred.token,
|
|
153
|
+
alMensaje: () => { },
|
|
154
|
+
alParar: () => { },
|
|
155
|
+
alAbrir: ({ orden, carpeta, texto }) => {
|
|
156
|
+
const c = carpetasDelServicio().find((x) => x.id === carpeta);
|
|
157
|
+
if (!c)
|
|
158
|
+
return log('open: unknown folder', carpeta);
|
|
159
|
+
if (vivas >= MAX_SESIONES)
|
|
160
|
+
return log('open: too many sessions');
|
|
161
|
+
vivas++;
|
|
162
|
+
log('open', c.nombre);
|
|
163
|
+
const hijo = lanzarSesion({ ruta: c.ruta, orden, texto });
|
|
164
|
+
hijo.on('exit', () => vivas--);
|
|
165
|
+
},
|
|
166
|
+
alApagarse: (motivo) => {
|
|
167
|
+
log('stopped:', motivo);
|
|
168
|
+
fin?.(motivo === 'web' ? 0 : 1);
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
const e = equipo();
|
|
172
|
+
for (let pausa = 5000;; pausa = Math.min(300_000, pausa * 2)) {
|
|
173
|
+
try {
|
|
174
|
+
await remoto.abrir({ tipo: 'servicio', carpetas: carpetas.map(({ id, nombre }) => ({ id, nombre })), equipo: e.equipo || hostname(), version: e.version, carpeta: '', modelo: '', modo: '' });
|
|
175
|
+
log('listening:', carpetas.map((c) => c.nombre).join(', '));
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
if (err instanceof ErrorDeParallel && (err.codigo === 'sesion' || err.codigo === 'sin_acceso')) {
|
|
180
|
+
log('cannot start:', err.codigo);
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
log('no connection, retrying');
|
|
184
|
+
await dormir(pausa);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const salir = () => void remoto.cerrar().finally(() => fin?.(0));
|
|
188
|
+
process.on('SIGTERM', salir);
|
|
189
|
+
process.on('SIGINT', salir);
|
|
190
|
+
return terminado;
|
|
191
|
+
}
|
package/dist/textos.js
CHANGED
|
@@ -237,6 +237,25 @@ export const TEXTOS = {
|
|
|
237
237
|
'remoto.desde_el_movil': 'from your phone',
|
|
238
238
|
'remoto.parado': 'Stopped from your phone.',
|
|
239
239
|
'remoto.solo_interactiva': 'Remote control works in an interactive session: run parx and then /remote.',
|
|
240
|
+
'mision.sugerencia': 'Next: /mission send it to my client · or announce it, or charge for it. It asks before sending anything.',
|
|
241
|
+
'vista.lista': 'Preview on your phone (updates after each task): {url}',
|
|
242
|
+
'vista.sin_remoto': 'Turn on /remote first: the preview opens from your phone.',
|
|
243
|
+
'vista.sin_web': 'No web to preview: there is no index.html here or in dist/, build/ or out/.',
|
|
244
|
+
'vista.no_subida': 'The preview could not be uploaded: {motivo}.',
|
|
245
|
+
'servicio.on': 'Your Mac is ready. From Sessions on your phone you can start a task in: {carpetas}. It starts again every time you log in.',
|
|
246
|
+
'servicio.off': 'The service is off. Your phone can no longer start tasks on this Mac.',
|
|
247
|
+
'servicio.anadida': 'Added {ruta}. Your phone can start tasks there.',
|
|
248
|
+
'servicio.no_anadida': '{ruta} cannot be added: it must be an existing folder, and not your whole home folder.',
|
|
249
|
+
'servicio.quitada': 'Removed {ruta}.',
|
|
250
|
+
'servicio.no_quitada': '{ruta} was not in the list.',
|
|
251
|
+
'servicio.estado': 'Service: {estado}. Folders: {lista}.',
|
|
252
|
+
'servicio.en_marcha': 'running',
|
|
253
|
+
'servicio.parado': 'off',
|
|
254
|
+
'servicio.ninguna': 'none',
|
|
255
|
+
'servicio.sin_carpetas': 'Add a folder first: go to a project folder and run parx service add.',
|
|
256
|
+
'servicio.solo_mac': 'The service is only for macOS for now.',
|
|
257
|
+
'servicio.registro': 'Log: {ruta}',
|
|
258
|
+
'servicio.uso': 'parx service on | off | add [folder] | remove <folder> | status',
|
|
240
259
|
'error.sin_saldo': 'You are out of Actions. Top up or upgrade at getparallel.net/settings/billing',
|
|
241
260
|
'error.sin_acceso': 'Your account does not have parx yet: it is in testing.',
|
|
242
261
|
'error.sesion': 'Your session expired. Run: parx login',
|
|
@@ -325,6 +344,7 @@ export const COMANDOS = [
|
|
|
325
344
|
['folder', 'Let parx work in another folder'],
|
|
326
345
|
['lanes', 'Run 2 or 3 tasks at once'],
|
|
327
346
|
['remote', 'Control this parx from your phone'],
|
|
347
|
+
['preview', 'See the web it is building on your phone'],
|
|
328
348
|
['mission', 'Keep it running on Parallel, phone alert when done'],
|
|
329
349
|
['missions', 'Pick a mission and control it from here'],
|
|
330
350
|
['inbox', 'Answer what your missions are waiting for'],
|
|
@@ -352,6 +372,7 @@ export const ALIAS_COMANDO = {
|
|
|
352
372
|
folder: 'folder', 'add-folder': 'folder', carpeta: 'folder', dir: 'folder',
|
|
353
373
|
lanes: 'lanes', carriles: 'lanes',
|
|
354
374
|
remote: 'remote', 'remote-control': 'remote', rc: 'remote', remoto: 'remote', phone: 'remote',
|
|
375
|
+
preview: 'preview', vista: 'preview', previa: 'preview',
|
|
355
376
|
mission: 'mission', mision: 'mission', misión: 'mission',
|
|
356
377
|
missions: 'missions', misiones: 'missions',
|
|
357
378
|
inbox: 'inbox', bandeja: 'inbox', approvals: 'inbox', waiting: 'inbox',
|
|
@@ -410,6 +431,9 @@ export const USO = [
|
|
|
410
431
|
' /remote link and QR to control this parx from your phone or a browser',
|
|
411
432
|
' /remote always turn it on every time parx opens on this computer',
|
|
412
433
|
' /remote off turn it off here',
|
|
434
|
+
' parx service add let your phone start tasks in this folder (macOS)',
|
|
435
|
+
' parx service on keep your Mac ready at login, even with parx closed',
|
|
436
|
+
' parx service off | status stop it, or see how it is',
|
|
413
437
|
'',
|
|
414
438
|
'Updates:',
|
|
415
439
|
' parx checks npm every few hours, without slowing anything down, and installs new versions',
|
package/dist/ui.js
CHANGED
|
@@ -308,6 +308,16 @@ export class Pantalla {
|
|
|
308
308
|
}
|
|
309
309
|
this.pausarLinea();
|
|
310
310
|
if (!this.tty || !this.rl) {
|
|
311
|
+
if (this.espejo) {
|
|
312
|
+
const op = opcionesDePregunta(tipo);
|
|
313
|
+
const remota = this.espejo.preguntaRemota(sinColor(texto), tipo, op.opciones.map((etiqueta, i) => ({ etiqueta, valor: op.valores[i] })));
|
|
314
|
+
if (remota.deFuera) {
|
|
315
|
+
const r = await remota.deFuera;
|
|
316
|
+
remota.contestada(r, 'movil');
|
|
317
|
+
return r;
|
|
318
|
+
}
|
|
319
|
+
remota.contestada('no', 'ordenador');
|
|
320
|
+
}
|
|
311
321
|
this.aviso(`${texto} → ${t('aviso.rechazado')}`);
|
|
312
322
|
return 'no';
|
|
313
323
|
}
|
package/dist/vista.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { join, relative, sep } from 'node:path';
|
|
4
|
+
import { subirVistaRemota } from "./api.js";
|
|
5
|
+
export const EXTENSIONES = ['html', 'htm', 'css', 'js', 'mjs', 'json', 'webmanifest', 'txt', 'xml', 'svg', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'ico', 'woff', 'woff2', 'ttf', 'otf', 'mp4', 'webm', 'mp3', 'wav', 'map'];
|
|
6
|
+
const EXT = new Set(EXTENSIONES);
|
|
7
|
+
const SALTAR = new Set(['node_modules', '.git', '.next', '.turbo', '.cache', 'coverage', '__pycache__', '.venv', 'venv', 'target']);
|
|
8
|
+
export const MAX_ARCHIVOS = 300;
|
|
9
|
+
export const MAX_ARCHIVO = 5 * 1024 * 1024;
|
|
10
|
+
export const MAX_TOTAL = 25 * 1024 * 1024;
|
|
11
|
+
const LOTE = 2_300_000;
|
|
12
|
+
export function raizWeb(raiz) {
|
|
13
|
+
for (const c of ['dist', 'build', 'out', '.', 'public', 'docs', 'site']) {
|
|
14
|
+
const abs = c === '.' ? raiz : join(raiz, c);
|
|
15
|
+
try {
|
|
16
|
+
if (existsSync(join(abs, 'index.html')) && statSync(join(abs, 'index.html')).isFile())
|
|
17
|
+
return { abs, nombre: c };
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
export function recogerWeb(abs) {
|
|
25
|
+
const out = [];
|
|
26
|
+
let total = 0;
|
|
27
|
+
const recorrer = (dir, profundidad) => {
|
|
28
|
+
if (profundidad > 8 || out.length >= MAX_ARCHIVOS)
|
|
29
|
+
return;
|
|
30
|
+
let entradas;
|
|
31
|
+
try {
|
|
32
|
+
entradas = readdirSync(dir).sort();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (const n of entradas) {
|
|
38
|
+
if (out.length >= MAX_ARCHIVOS)
|
|
39
|
+
return;
|
|
40
|
+
if (n.startsWith('.') || SALTAR.has(n))
|
|
41
|
+
continue;
|
|
42
|
+
const ruta = join(dir, n);
|
|
43
|
+
let st;
|
|
44
|
+
try {
|
|
45
|
+
st = statSync(ruta);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (st.isDirectory()) {
|
|
51
|
+
recorrer(ruta, profundidad + 1);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const ext = (/\.([a-z0-9]+)$/i.exec(n)?.[1] ?? '').toLowerCase();
|
|
55
|
+
if (!st.isFile() || !EXT.has(ext) || st.size > MAX_ARCHIVO || total + st.size > MAX_TOTAL)
|
|
56
|
+
continue;
|
|
57
|
+
const rel = relative(abs, ruta).split(sep).join('/');
|
|
58
|
+
if (!/^[A-Za-z0-9._\-/@+ ()]+$/.test(rel) || rel.length > 200)
|
|
59
|
+
continue;
|
|
60
|
+
const datos = readFileSync(ruta);
|
|
61
|
+
total += st.size;
|
|
62
|
+
out.push({ ruta: rel, abs: ruta, hash: createHash('sha256').update(datos).digest('hex'), tam: st.size });
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
recorrer(abs, 0);
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
export function enTandas(lista, max = LOTE) {
|
|
69
|
+
const tandas = [];
|
|
70
|
+
let actual = [];
|
|
71
|
+
let peso = 0;
|
|
72
|
+
for (const a of lista) {
|
|
73
|
+
if (actual.length && peso + a.tam > max) {
|
|
74
|
+
tandas.push(actual);
|
|
75
|
+
actual = [];
|
|
76
|
+
peso = 0;
|
|
77
|
+
}
|
|
78
|
+
actual.push(a);
|
|
79
|
+
peso += a.tam;
|
|
80
|
+
}
|
|
81
|
+
tandas.push(actual);
|
|
82
|
+
return tandas;
|
|
83
|
+
}
|
|
84
|
+
export class VistaPrevia {
|
|
85
|
+
token = randomBytes(32).toString('base64url');
|
|
86
|
+
subirLote;
|
|
87
|
+
constructor(subirLote = subirVistaRemota) {
|
|
88
|
+
this.subirLote = subirLote;
|
|
89
|
+
}
|
|
90
|
+
subidos = new Map();
|
|
91
|
+
url = null;
|
|
92
|
+
version = 0;
|
|
93
|
+
enMarcha = null;
|
|
94
|
+
async actualizar(o) {
|
|
95
|
+
while (this.enMarcha)
|
|
96
|
+
await this.enMarcha.catch(() => undefined);
|
|
97
|
+
const p = this.subir(o);
|
|
98
|
+
this.enMarcha = p;
|
|
99
|
+
try {
|
|
100
|
+
return await p;
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
this.enMarcha = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async subir(o) {
|
|
107
|
+
const web = raizWeb(o.raiz);
|
|
108
|
+
if (!web)
|
|
109
|
+
return null;
|
|
110
|
+
const lista = recogerWeb(web.abs);
|
|
111
|
+
if (!lista.some((a) => a.ruta === 'index.html'))
|
|
112
|
+
return null;
|
|
113
|
+
const cambiados = lista.filter((a) => this.subidos.get(a.ruta) !== a.hash);
|
|
114
|
+
const iguales = cambiados.length === 0 && lista.length === this.subidos.size;
|
|
115
|
+
if (iguales && this.url)
|
|
116
|
+
return { url: this.url, version: this.version, cambiados: 0, carpeta: web.nombre };
|
|
117
|
+
const tandas = enTandas(cambiados);
|
|
118
|
+
for (let i = 0; i < tandas.length; i++) {
|
|
119
|
+
const ultima = i === tandas.length - 1;
|
|
120
|
+
const archivos = tandas[i].map((a) => ({ ruta: a.ruta, datos: readFileSync(a.abs).toString('base64') }));
|
|
121
|
+
const r = await this.subirLote(o.servidor, o.token, o.remoto, {
|
|
122
|
+
archivos,
|
|
123
|
+
...(ultima ? { raiz: web.nombre, token: this.token, lista: lista.map(({ ruta, hash, tam }) => ({ ruta, hash, tam })) } : {}),
|
|
124
|
+
});
|
|
125
|
+
if (ultima) {
|
|
126
|
+
this.url = r.url;
|
|
127
|
+
this.version = r.version;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
this.subidos = new Map(lista.map((a) => [a.ruta, a.hash]));
|
|
131
|
+
return { url: this.url, version: this.version, cambiados: cambiados.length, carpeta: web.nombre };
|
|
132
|
+
}
|
|
133
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getparallel/parx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "parx: the coding agent of Parallel in your terminal. Reads and edits your project, runs commands and tests, and does coding tasks end to end. Mac, Windows and Linux.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"parx",
|