@dotrino/vaultd 0.155.0 → 0.157.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/README.md +51 -14
- package/bin/sea-entry.js +4 -0
- package/lib/src/browser.js +167 -0
- package/{src → lib/src/core}/approvals.js +1 -1
- package/{src → lib/src/core}/blocked.js +7 -10
- package/lib/src/core/browserDisk.js +109 -0
- package/{src → lib/src/core}/commKey.js +5 -8
- package/lib/src/core/disk.js +65 -0
- package/lib/src/core/libVersion.js +8 -0
- package/{src → lib/src/core}/machines.js +7 -10
- package/{src → lib/src/core}/notices.js +8 -12
- package/{src → lib/src/core}/notifiable.js +7 -10
- package/{src → lib/src/core}/sealKey.js +5 -8
- package/{src → lib/src/core}/sealer.js +12 -10
- package/{src → lib/src/core}/secretsStore.js +8 -11
- package/lib/src/core/sha256.js +69 -0
- package/{src → lib/src/core}/store.js +6 -9
- package/{src → lib/src/core}/subacta.js +5 -6
- package/{src → lib/src/core}/threadStore.js +5 -8
- package/{src → lib/src/core}/transport.js +4 -5
- package/lib/src/core/vault.js +4414 -0
- package/lib/src/proc.js +2 -84
- package/lib/src/procContext.js +97 -0
- package/package.json +3 -3
- package/src/ctl.js +41 -3
- package/src/daemon.js +1 -1
- package/src/disk.js +54 -0
- package/src/platform.js +139 -0
- package/src/service.js +247 -0
- package/src/tui/app.js +319 -127
- package/src/tui/gui.js +214 -0
- package/src/tui/i18n.js +25 -6
- package/src/tui/term.js +7 -2
- package/src/update.js +5 -1
- package/src/vault.js +10 -4455
- /package/{src → lib/src/core}/passwords.js +0 -0
- /package/{src → lib/src/core}/sealers.js +0 -0
package/src/tui/app.js
CHANGED
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
* que entrar antes), el candado es `k` (la `l` es el idioma) y la contraseña `c`.
|
|
31
31
|
*/
|
|
32
32
|
import fs from 'node:fs'
|
|
33
|
-
import {
|
|
33
|
+
import { spawnSync } from 'node:child_process'
|
|
34
34
|
import { parseEnvInput } from '../../lib/src/envtext.js'
|
|
35
|
-
import { createTerm, widthOf } from './term.js'
|
|
35
|
+
import { createTerm, widthOf, makeTheme } from './term.js'
|
|
36
36
|
import { qrToString } from '../qr.js'
|
|
37
37
|
import { dict, otherLang, loadLang, saveLang } from './i18n.js'
|
|
38
38
|
import * as vc from '../vaultControl.js'
|
|
@@ -40,6 +40,8 @@ import { readSettings, writeSettings } from '../settings.js'
|
|
|
40
40
|
import * as hw from '../hwkeys.js'
|
|
41
41
|
import { VERSION } from '../version.js'
|
|
42
42
|
import * as up from '../update.js'
|
|
43
|
+
import * as service from '../service.js'
|
|
44
|
+
import { startDaemonService, restartDaemonService } from '../service.js'
|
|
43
45
|
import { DEVICE_CAPS } from '@dotrino/identity/acta'
|
|
44
46
|
import { machineLine } from '@dotrino/identity/machine'
|
|
45
47
|
|
|
@@ -189,14 +191,6 @@ function lockGlyph (p) {
|
|
|
189
191
|
return p.locked ? '🔒' : '🔓'
|
|
190
192
|
}
|
|
191
193
|
|
|
192
|
-
function restartDaemonService () {
|
|
193
|
-
return new Promise((res, rej) => {
|
|
194
|
-
execFile('systemctl', ['--user', 'restart', 'dotrino-vault'], { timeout: 20000 }, (err, so, se) => {
|
|
195
|
-
if (err) rej(new Error(String(se || '').trim() || err.message)); else res()
|
|
196
|
-
})
|
|
197
|
-
})
|
|
198
|
-
}
|
|
199
|
-
|
|
200
194
|
/**
|
|
201
195
|
* AL ENTRAR SE DICE SI HAY VERSIÓN NUEVA, Y SE OFRECE INSTALARLA (dueño, 2026-10-13: como
|
|
202
196
|
* hace la app de Terminal). El servicio ya se actualiza solo; esto es para quien está
|
|
@@ -239,7 +233,7 @@ async function installUpdate (term, st, release, { install = up.installUserRelea
|
|
|
239
233
|
setConfirm(st, {
|
|
240
234
|
text: i.updateRestartOffer(r.v.version),
|
|
241
235
|
onYes: async () => {
|
|
242
|
-
const rr = await guard(term, st, i.updateRestarting, () => restartDaemonService())
|
|
236
|
+
const rr = await guard(term, st, i.updateRestarting, () => restartDaemonService({ pid: st.state?.pid, alive: vc.daemonAlive }))
|
|
243
237
|
if (!rr.ok) return
|
|
244
238
|
st.relaunch = true
|
|
245
239
|
st.quit = true
|
|
@@ -248,14 +242,6 @@ async function installUpdate (term, st, release, { install = up.installUserRelea
|
|
|
248
242
|
})
|
|
249
243
|
}
|
|
250
244
|
|
|
251
|
-
function startDaemonService () {
|
|
252
|
-
return new Promise((res) => {
|
|
253
|
-
execFile('systemctl', ['--user', 'start', 'dotrino-vault'], { timeout: 8000 }, (err, so, se) => {
|
|
254
|
-
res({ ok: !err, err: err ? (String(se || '').trim() || err.message) : '' })
|
|
255
|
-
})
|
|
256
|
-
})
|
|
257
|
-
}
|
|
258
|
-
|
|
259
245
|
// -------------------- render: modelo de filas + listas ---------------------
|
|
260
246
|
|
|
261
247
|
/**
|
|
@@ -428,12 +414,45 @@ function settingsRows (st, t) {
|
|
|
428
414
|
rows.push({ text: ` ${t.bold(frase)} ${t.accent(i.settingChange)}`, sel: true, meta: { setting: 'updateApproval' } })
|
|
429
415
|
rows.push({ text: t.muted(' ' + i.settingUpdateHint1), sel: false })
|
|
430
416
|
rows.push({ text: t.muted(' ' + i.settingUpdateHint2), sel: false })
|
|
417
|
+
// ARRANCAR AL INICIAR SESIÓN. En Linux lo deja la unidad de systemd; en Windows y macOS no
|
|
418
|
+
// había forma de pedirlo sin la línea de comandos, y una bóveda que hay que arrancar a mano
|
|
419
|
+
// cada día es una bóveda que un día no está.
|
|
420
|
+
const as = st.autostart
|
|
421
|
+
if (as) {
|
|
422
|
+
rows.push({ text: '', sel: false })
|
|
423
|
+
if (as.how === 'none') {
|
|
424
|
+
rows.push({ text: ' ' + t.muted(i.settingAutostartNoUnit), sel: false })
|
|
425
|
+
} else {
|
|
426
|
+
const f = as.error ? i.settingAutostartUnknown : as.on ? i.settingAutostartOn : i.settingAutostartOff
|
|
427
|
+
rows.push({ text: ` ${t.bold(f)} ${t.accent(i.settingChange)}`, sel: true, meta: { setting: 'autostart' } })
|
|
428
|
+
rows.push({ text: t.muted(' ' + i.settingAutostartHint), sel: false })
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
431
|
return rows
|
|
432
432
|
}
|
|
433
|
+
|
|
434
|
+
/** Mira si la bóveda arranca sola al iniciar sesión (`service.js`). `st.autostartApi`: pruebas. */
|
|
435
|
+
async function loadAutostart (st) {
|
|
436
|
+
const api = st.autostartApi || service
|
|
437
|
+
try { st.autostart = await api.autostartStatus() } catch (e) { st.autostart = { error: e?.message || String(e) } }
|
|
438
|
+
}
|
|
433
439
|
async function onKeySettings (term, st, key) {
|
|
434
440
|
const i = L(st)
|
|
435
441
|
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
436
442
|
if (key.name === 'escape' || ch === 'b') { st.screen = 'profiles'; return true }
|
|
443
|
+
const selectable = settingsRows(st, makeTheme()).filter((r) => r.sel)
|
|
444
|
+
if (['up', 'down', 'pageup', 'pagedown', 'home', 'end'].includes(key.name)) { moveSel(st, key, 'settings', selectable.length); return true }
|
|
445
|
+
const cur = selectable[Math.min(st.sel.settings || 0, selectable.length - 1)]?.meta
|
|
446
|
+
if (cur?.setting === 'autostart' && (key.name === 'enter' || (key.name === 'char' && key.ch === ' '))) {
|
|
447
|
+
const api = st.autostartApi || service
|
|
448
|
+
const on = !st.autostart?.on
|
|
449
|
+
const r = await guard(term, st, i.settingAutostartSaving, () => (on ? api.enableAutostart() : api.disableAutostart()))
|
|
450
|
+
if (!r.ok) return true
|
|
451
|
+
if (!r.v?.ok) { flash(st, i.settingSaveFailed(r.v?.err || '?'), 'danger'); return true }
|
|
452
|
+
await loadAutostart(st)
|
|
453
|
+
flash(st, st.autostart?.on ? i.settingAutostartNowOn : i.settingAutostartNowOff)
|
|
454
|
+
return true
|
|
455
|
+
}
|
|
437
456
|
if (key.name === 'enter' || (key.name === 'char' && key.ch === ' ')) {
|
|
438
457
|
// Con el ajuste ilegible, Enter lo ESCRIBE de nuevo (encendido: es lo prudente cuando
|
|
439
458
|
// no se sabe qué había); así la pantalla también es la salida del atasco.
|
|
@@ -1311,6 +1330,7 @@ async function onKeyProfiles (term, st, key) {
|
|
|
1311
1330
|
}))
|
|
1312
1331
|
} else if (ch === 's') { // ajustes de la bóveda (no de un perfil: no hace falta elegir uno)
|
|
1313
1332
|
loadSettings(st)
|
|
1333
|
+
await loadAutostart(st)
|
|
1314
1334
|
st.sel.settings = 0
|
|
1315
1335
|
st.scroll.settings = { value: 0 }
|
|
1316
1336
|
st.screen = 'settings'
|
|
@@ -2291,6 +2311,8 @@ function promptLoadVars (term, st, { ns = null, pub = null, where }) {
|
|
|
2291
2311
|
setInput(st, {
|
|
2292
2312
|
label: i.loadLabel(where),
|
|
2293
2313
|
hint: i.loadHint,
|
|
2314
|
+
// La ventana ofrece además elegir el `.env` con el diálogo del sistema.
|
|
2315
|
+
file: true,
|
|
2294
2316
|
onSubmit: async (raw) => {
|
|
2295
2317
|
const text = raw.trim()
|
|
2296
2318
|
st.input = null
|
|
@@ -2476,7 +2498,7 @@ function renderTabs (st, t) {
|
|
|
2476
2498
|
}).join(' ') + t.muted(i.tabsHint)
|
|
2477
2499
|
}
|
|
2478
2500
|
|
|
2479
|
-
function pairingBody (st, t, cols, height) {
|
|
2501
|
+
function pairingBody (st, t, cols, height, { qr: withQr = true } = {}) {
|
|
2480
2502
|
const i = L(st)
|
|
2481
2503
|
const info = st.pairing
|
|
2482
2504
|
const lines = []
|
|
@@ -2492,8 +2514,9 @@ function pairingBody (st, t, cols, height) {
|
|
|
2492
2514
|
if (info.service) lines.push(t.warn(i.pairService(info.service)))
|
|
2493
2515
|
// QR: se dibuja siempre que quepa de ancho; si es más alto que la pantalla se
|
|
2494
2516
|
// puede hacer scroll hacia arriba/abajo para verlo completo.
|
|
2517
|
+
// La ventana dibuja el QR como imagen: aquí solo le hacen falta los textos.
|
|
2495
2518
|
let qr = ''
|
|
2496
|
-
try { qr = qrToString(info.url) } catch (_) {}
|
|
2519
|
+
if (withQr) { try { qr = qrToString(info.url) } catch (_) {} }
|
|
2497
2520
|
const qrLines = qr ? qr.replace(/\n$/, '').split('\n') : []
|
|
2498
2521
|
const qrWidth = qrLines.length ? Math.max(...qrLines.map((l) => l.replace(/\x1b\[[0-9;]*m/g, '').length)) : 0
|
|
2499
2522
|
if (qrLines.length && qrWidth <= cols) {
|
|
@@ -2539,6 +2562,9 @@ function contentHeight (st, cols, rows, help = helpLines(st, L(st), cols, rows))
|
|
|
2539
2562
|
}
|
|
2540
2563
|
|
|
2541
2564
|
function render (term, st) {
|
|
2565
|
+
// LA VENTANA dibuja el mismo estado con sus propios widgets (`gui.js`): aquí solo se le
|
|
2566
|
+
// avisa de que hay algo nuevo que pintar.
|
|
2567
|
+
if (term.gui) { term.gui(st); return }
|
|
2542
2568
|
const t = term.t
|
|
2543
2569
|
const i = L(st)
|
|
2544
2570
|
const { cols, rows } = term.size()
|
|
@@ -2625,6 +2651,120 @@ function render (term, st) {
|
|
|
2625
2651
|
term.render(lines)
|
|
2626
2652
|
}
|
|
2627
2653
|
|
|
2654
|
+
// ------------------------- modelo para la ventana --------------------------
|
|
2655
|
+
|
|
2656
|
+
/** Las filas de cada pantalla de lista, y qué índice de selección usa. */
|
|
2657
|
+
const LIST_SCREENS = {
|
|
2658
|
+
profiles: [profileRows, 'profiles'],
|
|
2659
|
+
devices: [deviceRows, 'devices'],
|
|
2660
|
+
secrets: [secretRows, 'secrets'],
|
|
2661
|
+
logins: [loginRows, 'logins'],
|
|
2662
|
+
me: [meRows, null],
|
|
2663
|
+
caps: [capsRows, 'caps'],
|
|
2664
|
+
devvars: [devVarRows, 'devvars'],
|
|
2665
|
+
pairmode: [pairModeRows, 'pairmode'],
|
|
2666
|
+
doors: [doorRows, 'doors'],
|
|
2667
|
+
settings: [settingsRows, 'settings'],
|
|
2668
|
+
paircaps: [pairCapsRows, 'paircaps']
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
/**
|
|
2672
|
+
* LO MISMO QUE PINTA `render`, pero como datos: la ventana de escritorio (`gui.js` y la app
|
|
2673
|
+
* de `desktop/`) lo dibuja con sus propios controles. Las filas, los títulos, los avisos y
|
|
2674
|
+
* las acciones salen de las MISMAS funciones que usa la TUI, con sus estilos como escapes
|
|
2675
|
+
* que el puente convierte en tramos. Así no hay una segunda copia de ninguna pantalla.
|
|
2676
|
+
*
|
|
2677
|
+
* `t` es el tema con color forzado (`makeTheme({ color: true })`).
|
|
2678
|
+
*/
|
|
2679
|
+
export function viewModel (st, t) {
|
|
2680
|
+
const i = L(st)
|
|
2681
|
+
const s = st.state
|
|
2682
|
+
const up = st.daemonUp
|
|
2683
|
+
const ap = activeProfile(st)
|
|
2684
|
+
const vm = {
|
|
2685
|
+
lang: st.lang,
|
|
2686
|
+
version: s?.version || VERSION,
|
|
2687
|
+
uiVersion: VERSION,
|
|
2688
|
+
daemonUp: !!up,
|
|
2689
|
+
daemonText: up ? i.daemonRunning : i.daemonStopped,
|
|
2690
|
+
dir: vc.vaultDir(),
|
|
2691
|
+
screen: st.screen,
|
|
2692
|
+
active: ap ? { id: ap.id, name: ap.name || i.noName, protected: !!ap.protected, locked: !!ap.locked, fingerprint: ap.fingerprint || '' } : null,
|
|
2693
|
+
activeLabel: i.activeVault,
|
|
2694
|
+
notice: null,
|
|
2695
|
+
tabs: null,
|
|
2696
|
+
title: '',
|
|
2697
|
+
rows: null,
|
|
2698
|
+
selected: -1,
|
|
2699
|
+
body: null,
|
|
2700
|
+
qr: null,
|
|
2701
|
+
status: null,
|
|
2702
|
+
help: [],
|
|
2703
|
+
input: null,
|
|
2704
|
+
confirm: null
|
|
2705
|
+
}
|
|
2706
|
+
// El aviso de la segunda línea de la TUI, con el mismo orden de prioridad.
|
|
2707
|
+
if (up && s?.version && VERSION !== 'dev' && s.version !== VERSION) vm.notice = { text: i.daemonStale(s.version, VERSION), kind: 'warn' }
|
|
2708
|
+
else if (up && s?.updateAsk?.version) {
|
|
2709
|
+
const u = s.updateAsk
|
|
2710
|
+
const dia = (ms) => new Date(ms).toISOString().slice(0, 16).replace('T', ' ')
|
|
2711
|
+
vm.notice = u.result === 'pending'
|
|
2712
|
+
? { text: i.updateWaiting(u.version, dia(u.askedAt + 24 * 60 * 60 * 1000)), kind: 'muted' }
|
|
2713
|
+
: { text: i.updateNotApproved(u.version, dia(u.askedAt)), kind: 'warn' }
|
|
2714
|
+
} else if (st.update?.release) vm.notice = { text: i.updateAvailable(st.update.release.version), kind: 'warn' }
|
|
2715
|
+
|
|
2716
|
+
if (INNER_TABS.includes(st.screen)) vm.tabs = INNER_TABS.map((k) => ({ key: k, label: tabLabel(i, k), active: st.screen === k }))
|
|
2717
|
+
else vm.title = title(i, st.screen)
|
|
2718
|
+
|
|
2719
|
+
const list = LIST_SCREENS[st.screen]
|
|
2720
|
+
if (list) {
|
|
2721
|
+
const [rowsOf, selKey] = list
|
|
2722
|
+
const rows = rowsOf(st, t)
|
|
2723
|
+
let n = 0
|
|
2724
|
+
vm.rows = rows.map((r) => ({ text: r.text, sel: !!r.sel, idx: r.sel ? n++ : -1 }))
|
|
2725
|
+
vm.selected = selKey ? Math.max(0, Math.min(st.sel[selKey] || 0, n - 1)) : -1
|
|
2726
|
+
if (!n) vm.selected = -1
|
|
2727
|
+
} else if (st.screen === 'pairing' && st.pairing) {
|
|
2728
|
+
vm.body = pairingBody(st, t, 999, 999, { qr: false })
|
|
2729
|
+
vm.qr = st.pairing.url
|
|
2730
|
+
vm.code = st.pairing.b64 || st.pairing.payload || ''
|
|
2731
|
+
} else if (st.screen === 'join') vm.body = joinBody(st, t, 999)
|
|
2732
|
+
|
|
2733
|
+
if (st.busy) vm.status = { text: st.busy, kind: 'busy' }
|
|
2734
|
+
else if (st.flash) vm.status = { text: st.flash.text, kind: st.flash.kind || 'ok' }
|
|
2735
|
+
if (st.input) {
|
|
2736
|
+
const inp = st.input
|
|
2737
|
+
vm.input = { label: inp.label || '', hint: inp.hint || '', mask: !!inp.mask, value: inp.value || '', file: !!inp.file }
|
|
2738
|
+
} else if (st.confirm) vm.confirm = { text: st.confirm.text, defaultYes: !!st.confirm.defaultYes }
|
|
2739
|
+
else vm.help = helpSegs(i, st.screen, st)
|
|
2740
|
+
return vm
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
/** La pantalla de «el servicio no está», como datos. */
|
|
2744
|
+
export function downModel (st) {
|
|
2745
|
+
const i = L(st)
|
|
2746
|
+
return {
|
|
2747
|
+
lang: st.lang,
|
|
2748
|
+
down: true,
|
|
2749
|
+
uiVersion: VERSION,
|
|
2750
|
+
dir: vc.vaultDir(),
|
|
2751
|
+
title: i.downTitle,
|
|
2752
|
+
body: [i.downBody1 + ' ' + i.downBody2],
|
|
2753
|
+
help: i.downHelp,
|
|
2754
|
+
status: st.busy ? { text: st.busy, kind: 'busy' } : st.flash ? { text: st.flash.text, kind: st.flash.kind || 'warn' } : null
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
/** Seleccionar la fila `idx` (entre las seleccionables) de la pantalla actual. */
|
|
2759
|
+
export function selectRow (st, idx) {
|
|
2760
|
+
const list = LIST_SCREENS[st.screen]
|
|
2761
|
+
if (!list || !list[1]) return false
|
|
2762
|
+
st.sel[list[1]] = Math.max(0, idx | 0)
|
|
2763
|
+
return true
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
export { onKeyDown }
|
|
2767
|
+
|
|
2628
2768
|
// --------------------------- pantalla daemon caído -------------------------
|
|
2629
2769
|
|
|
2630
2770
|
async function daemonDownScreen (term, st) {
|
|
@@ -2641,7 +2781,7 @@ async function daemonDownScreen (term, st) {
|
|
|
2641
2781
|
i.downBody1,
|
|
2642
2782
|
i.downBody2,
|
|
2643
2783
|
'',
|
|
2644
|
-
t.bold('S') + i.downStart + t.muted('systemctl --user start dotrino-vault'),
|
|
2784
|
+
t.bold('S') + i.downStart + t.muted(process.platform === 'linux' ? 'systemctl --user start dotrino-vault' : ''),
|
|
2645
2785
|
t.bold('R') + i.downRecheck,
|
|
2646
2786
|
t.bold('l') + i.downLang,
|
|
2647
2787
|
t.bold('Q') + i.downQuit,
|
|
@@ -2656,27 +2796,39 @@ async function daemonDownScreen (term, st) {
|
|
|
2656
2796
|
term.render(lines)
|
|
2657
2797
|
|
|
2658
2798
|
const key = await term.readKey()
|
|
2659
|
-
const
|
|
2660
|
-
if (
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2799
|
+
const r = await onKeyDown(st, key, () => term.render(lines))
|
|
2800
|
+
if (r !== null) return r
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
/**
|
|
2805
|
+
* Una tecla en la pantalla de «el servicio no está». Devuelve `false` (salir), `true` (ya
|
|
2806
|
+
* está: seguir a la TUI) o `null` (seguir aquí). La comparten la TUI y la ventana.
|
|
2807
|
+
*/
|
|
2808
|
+
async function onKeyDown (st, key, redraw = () => {}) {
|
|
2809
|
+
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
2810
|
+
if (ch === 'q' || key.name === 'ctrl-c') return false
|
|
2811
|
+
if (ch === 'l') { toggleLang(st); return null }
|
|
2812
|
+
if (ch === 'r') { if (vc.daemonAlive()) return true; flash(st, L(st).stillDown, 'warn') }
|
|
2813
|
+
if (ch === 's') {
|
|
2814
|
+
st.busy = L(st).starting
|
|
2815
|
+
flash(st, L(st).startingShort, 'warn'); redraw()
|
|
2816
|
+
const r = await startDaemonService()
|
|
2817
|
+
// Un daemon recién lanzado tarda en escribir su `state.json`: se le espera un poco, no
|
|
2818
|
+
// un tiempo fijo, para no dar por fallido uno que solo iba lento.
|
|
2819
|
+
for (let n = 0; n < 20 && !vc.daemonAlive(); n++) await sleep(250)
|
|
2820
|
+
st.busy = null
|
|
2821
|
+
if (vc.daemonAlive()) return true
|
|
2822
|
+
flash(st, r.ok ? L(st).startedNotReady : L(st).startFailed(r.err), 'danger')
|
|
2672
2823
|
}
|
|
2824
|
+
return null
|
|
2673
2825
|
}
|
|
2674
2826
|
|
|
2675
2827
|
// ---------------------------------- loop -----------------------------------
|
|
2676
2828
|
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2829
|
+
/** El estado de una sesión de la pantalla de control (TUI o ventana). */
|
|
2830
|
+
export function newSession () {
|
|
2831
|
+
return {
|
|
2680
2832
|
screen: 'profiles', // se arranca en la lista de bóvedas: hay que ENTRAR a una
|
|
2681
2833
|
lang: loadLang(), // es/en — se conmuta con `l` y se recuerda en prefs.json
|
|
2682
2834
|
sel: { profiles: 0, devices: 0, secrets: 0, pairmode: 0, paircaps: 0, devvars: 0, logins: 0, doors: 0, settings: 0 },
|
|
@@ -2684,7 +2836,7 @@ export async function runTui () {
|
|
|
2684
2836
|
capsDrafts: {},
|
|
2685
2837
|
// Las bóvedas que ha abierto ESTA sesión, para volver a cerrarlas al salir.
|
|
2686
2838
|
unlockedHere: new Set(),
|
|
2687
|
-
// Su contraseña, SOLO en memoria y SOLO mientras la
|
|
2839
|
+
// Su contraseña, SOLO en memoria y SOLO mientras la pantalla esté abierta: sirve para
|
|
2688
2840
|
// reabrir sin volver a preguntar si el daemon pierde el estado (ver
|
|
2689
2841
|
// `reunlockSilently`). Se olvida con `k`, al quitar la contraseña y al salir.
|
|
2690
2842
|
sessionPwd: new Map(),
|
|
@@ -2704,107 +2856,147 @@ export async function runTui () {
|
|
|
2704
2856
|
input: null,
|
|
2705
2857
|
confirm: null
|
|
2706
2858
|
}
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
/** Con el daemon ya vivo: la primera foto y la búsqueda de versión nueva. */
|
|
2862
|
+
export async function startSession (term, st) {
|
|
2863
|
+
st.flash = null
|
|
2864
|
+
st.state = vc.readState()
|
|
2865
|
+
st.daemonUp = true
|
|
2866
|
+
await refreshAll(term, st)
|
|
2867
|
+
checkForUpdate(st)
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
/** Lo que se mira antes de cada pintado: estado del daemon, aviso de versión, candado, flash. */
|
|
2871
|
+
export async function beforeRender (term, st) {
|
|
2872
|
+
st.state = vc.readState()
|
|
2873
|
+
st.daemonUp = vc.daemonAlive()
|
|
2874
|
+
// El aviso de versión nueva sale cuando no hay otra cosa preguntando.
|
|
2875
|
+
if (st.update?.offer && !st.input && !st.confirm) offerUpdate(term, st)
|
|
2876
|
+
// ¿Se cerró sola mientras nadie miraba? Olvida su contraseña y sal de su contenido.
|
|
2877
|
+
await forgetAutoLocked(term, st)
|
|
2878
|
+
// caducar el flash a los ~4 s
|
|
2879
|
+
if (st.flash && Date.now() - st.flash.at > 4000) st.flash = null
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
/** Cada cuánto despertar sin tecla (0 = solo con tecla). */
|
|
2883
|
+
export function tickFor (st) {
|
|
2884
|
+
const base = (st.screen === 'pairing' || st.screen === 'join' || (st.screen === 'devices' && !st.input && !st.confirm)) ? 800 : 0
|
|
2885
|
+
// Despertar a tiempo del bloqueo automático, además del sondeo de la pantalla.
|
|
2886
|
+
const wake = autoLockWakeIn(st)
|
|
2887
|
+
let tick = base && wake ? Math.min(base, wake) : (base || wake)
|
|
2888
|
+
// Mientras se mira si hay versión nueva hay que despertar sin tecla, o el aviso no
|
|
2889
|
+
// saldría hasta que tocaras algo.
|
|
2890
|
+
if (st.update?.checking) tick = tick ? Math.min(tick, 500) : 500
|
|
2891
|
+
return tick
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
/**
|
|
2895
|
+
* UNA TECLA. Devuelve `false` si hay que salir. Es el corazón de la pantalla de control y
|
|
2896
|
+
* lo comparten la TUI y la ventana: una acción nueva aparece en las dos sin tocar la otra.
|
|
2897
|
+
*/
|
|
2898
|
+
export async function handleKey (term, st, key) {
|
|
2899
|
+
if (key.name === 'resize') return true
|
|
2900
|
+
// Un despertar del bloqueo automático no es una tecla: ya se atendió arriba, y las
|
|
2901
|
+
// pantallas que NO esperan `tick` (todas menos aparatos y emparejamiento) no tienen
|
|
2902
|
+
// por qué verlo.
|
|
2903
|
+
if (key.name === 'tick' && !['pairing', 'join', 'devices'].includes(st.screen)) return true
|
|
2904
|
+
// input/confirm se AWAITan: serializa las ops contra el daemon (ver onInputKey).
|
|
2905
|
+
// Ctrl-C dentro de un modal lo CANCELA (no sale); fuera de un modal, sale.
|
|
2906
|
+
if (st.input) { await onInputKey(st, key); return true }
|
|
2907
|
+
if (st.confirm) { await onConfirmKey(st, key); return !st.quit }
|
|
2908
|
+
if (key.name === 'ctrl-c') return false
|
|
2909
|
+
|
|
2910
|
+
// TECLEAR ES USO. El candado se cierra a los 5 min de no usarse, y hasta ahora
|
|
2911
|
+
// «usar» solo contaba cuando algo pedía al daemon: podías estar media hora
|
|
2912
|
+
// navegando esta pantalla y se cerraba igual, que es lo contrario de lo que se
|
|
2913
|
+
// quería. Se avisa como mucho una vez por minuto —el plazo son cinco, así que
|
|
2914
|
+
// sobra— para no escribir un archivo por tecla.
|
|
2915
|
+
seguirAqui(st)
|
|
2916
|
+
|
|
2917
|
+
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
2918
|
+
// 'q' global sale — preguntando antes si hay permisos sin guardar.
|
|
2919
|
+
if (ch === 'q') {
|
|
2920
|
+
if (draftChanges(st).length) { unlessDrafts(st, () => { st.quit = true }); return true }
|
|
2921
|
+
return false
|
|
2922
|
+
}
|
|
2923
|
+
// 'l' global: idioma es⇄en en cualquier pantalla (por eso el candado es 'c').
|
|
2924
|
+
if (ch === 'l') { toggleLang(st); return true }
|
|
2925
|
+
// ←→ cambia entre las pestañas de la bóveda entrada (Dispositivos/Scopes).
|
|
2926
|
+
if ((key.name === 'left' || key.name === 'right') && INNER_TABS.includes(st.screen)) {
|
|
2927
|
+
const n = INNER_TABS.indexOf(st.screen)
|
|
2928
|
+
await openTab(term, st, INNER_TABS[(n + (key.name === 'right' ? 1 : -1) + INNER_TABS.length) % INNER_TABS.length])
|
|
2929
|
+
return true
|
|
2930
|
+
}
|
|
2931
|
+
// Esc/'b' desde una pestaña vuelve a la lista de bóvedas (salir de la bóveda
|
|
2932
|
+
// entrada). La pantalla de emparejamiento maneja su propio Esc (va a Dispositivos).
|
|
2933
|
+
if ((key.name === 'escape' || ch === 'b') && INNER_TABS.includes(st.screen)) {
|
|
2934
|
+
// El borrador es de ESTA bóveda: al salir de ella se guarda o se tira.
|
|
2935
|
+
unlessDrafts(st, () => { st.capsDrafts = {}; st.screen = 'profiles' })
|
|
2936
|
+
return true
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
if (st.screen === 'profiles') return onKeyProfiles(term, st, key)
|
|
2940
|
+
if (st.screen === 'devices') return onKeyDevices(term, st, key)
|
|
2941
|
+
if (st.screen === 'secrets') return onKeySecrets(term, st, key)
|
|
2942
|
+
if (st.screen === 'logins') return onKeyLogins(term, st, key)
|
|
2943
|
+
if (st.screen === 'me') return onKeyMe(term, st, key)
|
|
2944
|
+
if (st.screen === 'caps') return onKeyCaps(term, st, key)
|
|
2945
|
+
if (st.screen === 'devvars') return onKeyDevVars(term, st, key)
|
|
2946
|
+
if (st.screen === 'pairmode') return onKeyPairMode(term, st, key)
|
|
2947
|
+
if (st.screen === 'doors') return onKeyDoors(term, st, key)
|
|
2948
|
+
if (st.screen === 'settings') return onKeySettings(term, st, key)
|
|
2949
|
+
if (st.screen === 'paircaps') return onKeyPairCaps(term, st, key)
|
|
2950
|
+
if (st.screen === 'pairing') return onKeyPairing(term, st, key)
|
|
2951
|
+
if (st.screen === 'join') return onKeyJoin(term, st, key)
|
|
2952
|
+
return true
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
/**
|
|
2956
|
+
* Ir a una pestaña de la bóveda entrada. El perfil y los inicios de sesión se piden al
|
|
2957
|
+
* ENTRAR en su pestaña, no al arrancar: es contenido del usuario y no hay por qué sacarlo
|
|
2958
|
+
* del cifrado si nadie lo está mirando.
|
|
2959
|
+
*/
|
|
2960
|
+
export async function openTab (term, st, tab) {
|
|
2961
|
+
if (!INNER_TABS.includes(tab) || !INNER_TABS.includes(st.screen)) return
|
|
2962
|
+
st.screen = tab
|
|
2963
|
+
if (st.screen === 'me' && st.me === undefined) await refreshMe(term, st)
|
|
2964
|
+
if (st.screen === 'logins' && st.logins === null) await refreshLogins(term, st)
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
/**
|
|
2968
|
+
* AL SALIR SE VUELVE A CERRAR lo que se abrió aquí. Sin esto, teclear la contraseña una
|
|
2969
|
+
* vez dejaba la bóveda abierta para todo el que pasara por esta máquina hasta el
|
|
2970
|
+
* siguiente reinicio del servicio — un candado que solo se cierra reiniciando no es un
|
|
2971
|
+
* candado. (Si la pantalla muere de un tirón —kill, ventana cerrada— no hay quien lo haga:
|
|
2972
|
+
* ahí el cierre lo pone el reinicio, como antes.)
|
|
2973
|
+
*/
|
|
2974
|
+
export async function endSession (st) {
|
|
2975
|
+
st.sessionPwd.clear()
|
|
2976
|
+
for (const id of st.unlockedHere) { try { await vc.lockProfile(id) } catch (_) {} }
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
export async function runTui () {
|
|
2980
|
+
const term = createTerm()
|
|
2981
|
+
const st = newSession()
|
|
2707
2982
|
|
|
2708
2983
|
try {
|
|
2709
2984
|
// Arranque: exige daemon vivo.
|
|
2710
2985
|
if (!vc.daemonAlive()) {
|
|
2711
2986
|
const cont = await daemonDownScreen(term, st)
|
|
2712
2987
|
if (!cont) { term.close(); return }
|
|
2713
|
-
st.flash = null
|
|
2714
2988
|
}
|
|
2715
|
-
|
|
2716
|
-
st.daemonUp = true
|
|
2717
|
-
await refreshAll(term, st)
|
|
2718
|
-
checkForUpdate(st)
|
|
2989
|
+
await startSession(term, st)
|
|
2719
2990
|
|
|
2720
2991
|
let running = true
|
|
2721
2992
|
while (running) {
|
|
2722
|
-
|
|
2723
|
-
st.daemonUp = vc.daemonAlive()
|
|
2724
|
-
// El aviso de versión nueva sale cuando no hay otra cosa preguntando.
|
|
2725
|
-
if (st.update?.offer && !st.input && !st.confirm) offerUpdate(term, st)
|
|
2726
|
-
// ¿Se cerró sola mientras nadie miraba? Olvida su contraseña y sal de su contenido.
|
|
2727
|
-
await forgetAutoLocked(term, st)
|
|
2728
|
-
// caducar el flash a los ~4 s
|
|
2729
|
-
if (st.flash && Date.now() - st.flash.at > 4000) st.flash = null
|
|
2993
|
+
await beforeRender(term, st)
|
|
2730
2994
|
render(term, st)
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
// Despertar a tiempo del bloqueo automático, además del sondeo de la pantalla.
|
|
2734
|
-
const wake = autoLockWakeIn(st)
|
|
2735
|
-
let tick = base && wake ? Math.min(base, wake) : (base || wake)
|
|
2736
|
-
// Mientras se mira si hay versión nueva hay que despertar sin tecla, o el aviso no
|
|
2737
|
-
// saldría hasta que tocaras algo.
|
|
2738
|
-
if (st.update?.checking) tick = tick ? Math.min(tick, 500) : 500
|
|
2739
|
-
const key = await term.readKey(tick)
|
|
2740
|
-
|
|
2741
|
-
if (key.name === 'resize') continue
|
|
2742
|
-
// Un despertar del bloqueo automático no es una tecla: ya se atendió arriba, y las
|
|
2743
|
-
// pantallas que NO esperan `tick` (todas menos aparatos y emparejamiento) no tienen
|
|
2744
|
-
// por qué verlo.
|
|
2745
|
-
if (key.name === 'tick' && !['pairing', 'join', 'devices'].includes(st.screen)) continue
|
|
2746
|
-
// input/confirm se AWAITan: serializa las ops contra el daemon (ver onInputKey).
|
|
2747
|
-
// Ctrl-C dentro de un modal lo CANCELA (no sale); fuera de un modal, sale.
|
|
2748
|
-
if (st.input) { await onInputKey(st, key); continue }
|
|
2749
|
-
if (st.confirm) { await onConfirmKey(st, key); if (st.quit) running = false; continue }
|
|
2750
|
-
if (key.name === 'ctrl-c') { running = false; continue }
|
|
2751
|
-
|
|
2752
|
-
// TECLEAR ES USO. El candado se cierra a los 5 min de no usarse, y hasta ahora
|
|
2753
|
-
// «usar» solo contaba cuando algo pedía al daemon: podías estar media hora
|
|
2754
|
-
// navegando esta pantalla y se cerraba igual, que es lo contrario de lo que se
|
|
2755
|
-
// quería. Se avisa como mucho una vez por minuto —el plazo son cinco, así que
|
|
2756
|
-
// sobra— para no escribir un archivo por tecla.
|
|
2757
|
-
seguirAqui(st)
|
|
2758
|
-
|
|
2759
|
-
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
2760
|
-
// 'q' global sale — preguntando antes si hay permisos sin guardar.
|
|
2761
|
-
if (ch === 'q') {
|
|
2762
|
-
if (draftChanges(st).length) unlessDrafts(st, () => { st.quit = true })
|
|
2763
|
-
else running = false
|
|
2764
|
-
continue
|
|
2765
|
-
}
|
|
2766
|
-
// 'l' global: idioma es⇄en en cualquier pantalla (por eso el candado es 'c').
|
|
2767
|
-
if (ch === 'l') { toggleLang(st); continue }
|
|
2768
|
-
// ←→ cambia entre las pestañas de la bóveda entrada (Dispositivos/Scopes).
|
|
2769
|
-
if ((key.name === 'left' || key.name === 'right') && INNER_TABS.includes(st.screen)) {
|
|
2770
|
-
const n = INNER_TABS.indexOf(st.screen)
|
|
2771
|
-
st.screen = INNER_TABS[(n + (key.name === 'right' ? 1 : -1) + INNER_TABS.length) % INNER_TABS.length]
|
|
2772
|
-
// El perfil se pide al ENTRAR en su pestaña, no al arrancar: es contenido del
|
|
2773
|
-
// usuario y no hay por qué sacarlo del cifrado si nadie lo está mirando.
|
|
2774
|
-
if (st.screen === 'me' && st.me === undefined) await refreshMe(term, st)
|
|
2775
|
-
if (st.screen === 'logins' && st.logins === null) await refreshLogins(term, st)
|
|
2776
|
-
continue
|
|
2777
|
-
}
|
|
2778
|
-
// Esc/'b' desde una pestaña vuelve a la lista de bóvedas (salir de la bóveda
|
|
2779
|
-
// entrada). La pantalla de emparejamiento maneja su propio Esc (va a Dispositivos).
|
|
2780
|
-
if ((key.name === 'escape' || ch === 'b') && INNER_TABS.includes(st.screen)) {
|
|
2781
|
-
// El borrador es de ESTA bóveda: al salir de ella se guarda o se tira.
|
|
2782
|
-
unlessDrafts(st, () => { st.capsDrafts = {}; st.screen = 'profiles' })
|
|
2783
|
-
continue
|
|
2784
|
-
}
|
|
2785
|
-
|
|
2786
|
-
if (st.screen === 'profiles') running = await onKeyProfiles(term, st, key)
|
|
2787
|
-
else if (st.screen === 'devices') running = await onKeyDevices(term, st, key)
|
|
2788
|
-
else if (st.screen === 'secrets') running = await onKeySecrets(term, st, key)
|
|
2789
|
-
else if (st.screen === 'logins') running = await onKeyLogins(term, st, key)
|
|
2790
|
-
else if (st.screen === 'me') running = await onKeyMe(term, st, key)
|
|
2791
|
-
else if (st.screen === 'caps') running = await onKeyCaps(term, st, key)
|
|
2792
|
-
else if (st.screen === 'devvars') running = await onKeyDevVars(term, st, key)
|
|
2793
|
-
else if (st.screen === 'pairmode') running = await onKeyPairMode(term, st, key)
|
|
2794
|
-
else if (st.screen === 'doors') running = await onKeyDoors(term, st, key)
|
|
2795
|
-
else if (st.screen === 'settings') running = await onKeySettings(term, st, key)
|
|
2796
|
-
else if (st.screen === 'paircaps') running = await onKeyPairCaps(term, st, key)
|
|
2797
|
-
else if (st.screen === 'pairing') running = await onKeyPairing(term, st, key)
|
|
2798
|
-
else if (st.screen === 'join') running = await onKeyJoin(term, st, key)
|
|
2995
|
+
const key = await term.readKey(tickFor(st))
|
|
2996
|
+
running = await handleKey(term, st, key)
|
|
2799
2997
|
}
|
|
2800
2998
|
} finally {
|
|
2801
|
-
|
|
2802
|
-
// vez dejaba la bóveda abierta para todo el que pasara por esta máquina hasta el
|
|
2803
|
-
// siguiente reinicio del servicio — un candado que solo se cierra reiniciando no es un
|
|
2804
|
-
// candado. (Si la TUI muere de un tirón —kill, ventana cerrada— no hay quien lo haga:
|
|
2805
|
-
// ahí el cierre lo pone el reinicio, como antes.)
|
|
2806
|
-
st.sessionPwd.clear()
|
|
2807
|
-
for (const id of st.unlockedHere) { try { await vc.lockProfile(id) } catch (_) {} }
|
|
2999
|
+
await endSession(st)
|
|
2808
3000
|
term.close()
|
|
2809
3001
|
}
|
|
2810
3002
|
// TRAS ACTUALIZAR Y REINICIAR, esta pantalla también se vuelve a abrir: es el binario
|