@dotrino/vaultd 0.7.2 → 0.7.4
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 +31 -3
- package/package.json +1 -1
- package/src/ctl.js +27 -5
- package/src/daemon.js +8 -2
- package/src/tui/app.js +292 -139
- package/src/tui/i18n.js +376 -0
- package/src/vaultControl.js +19 -9
package/src/tui/app.js
CHANGED
|
@@ -11,10 +11,20 @@
|
|
|
11
11
|
* Cada "bóveda" es un PERFIL (maestra propia, dir propio, dispositivos y secretos
|
|
12
12
|
* propios). Las acciones operan sobre la bóveda ACTIVA; para operar otra, cámbiala
|
|
13
13
|
* en la pantalla de bóvedas.
|
|
14
|
+
*
|
|
15
|
+
* BILINGÜE (CONVENCIONES §9): todo el texto sale de `i18n.js` según `st.lang`; la
|
|
16
|
+
* tecla `l` conmuta es/en en cualquier pantalla y recuerda la elección.
|
|
17
|
+
*
|
|
18
|
+
* LAS TECLAS NO CAMBIAN CON EL IDIOMA: son mnemónicos en INGLÉS y son las mismas
|
|
19
|
+
* en español (lo que se traduce es la palabra que las explica en la barra de
|
|
20
|
+
* ayuda). new · rename · delete · password · unlock · locK · pair · approve ·
|
|
21
|
+
* reject · reVoke · refresh · back · language · quit. Por eso el candado dejó de
|
|
22
|
+
* ser `l` (hoy idioma) y es `k`, la contraseña es `p` y emparejar es `p`.
|
|
14
23
|
*/
|
|
15
24
|
import { execFile } from 'node:child_process'
|
|
16
|
-
import { createTerm } from './term.js'
|
|
25
|
+
import { createTerm, widthOf } from './term.js'
|
|
17
26
|
import { qrToString } from '../qr.js'
|
|
27
|
+
import { dict, otherLang, loadLang, saveLang } from './i18n.js'
|
|
18
28
|
import * as vc from '../vaultControl.js'
|
|
19
29
|
|
|
20
30
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
@@ -25,9 +35,24 @@ const KEY_RE = /^[A-Z0-9_]{1,64}$/
|
|
|
25
35
|
|
|
26
36
|
// ------------------------------- utilidades --------------------------------
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
/** Diccionario del idioma activo (español si el estado aún no lo trae). */
|
|
39
|
+
const L = (st) => dict(st?.lang)
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Errores en el idioma de la TUI. Los que nacen aquí o en `vaultControl` tienen
|
|
43
|
+
* `code` y se traducen; los que REENVÍA el daemon llegan como texto y se muestran
|
|
44
|
+
* tal cual (son diagnósticos del servicio, no copy de la interfaz).
|
|
45
|
+
*/
|
|
46
|
+
function humanErr (e, st) {
|
|
47
|
+
const t = L(st)
|
|
48
|
+
const byCode = {
|
|
49
|
+
DAEMON_DOWN: t.errDaemonDown,
|
|
50
|
+
NO_REPLY: t.errNoReply,
|
|
51
|
+
NOT_APPLIED: t.errNotApplied,
|
|
52
|
+
NOT_DELETED: t.errNotDeleted,
|
|
53
|
+
PAIR_FAILED: t.errPairFailed
|
|
54
|
+
}
|
|
55
|
+
return byCode[e?.code] || e?.message || String(e)
|
|
31
56
|
}
|
|
32
57
|
|
|
33
58
|
function flash (st, text, kind = 'ok') { st.flash = { text, kind, at: Date.now() } }
|
|
@@ -91,52 +116,99 @@ function renderList (rows, selIdx, height, cols, t, scrollRef) {
|
|
|
91
116
|
return out
|
|
92
117
|
}
|
|
93
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Barra de ayuda que SIEMPRE deja ver lo global (idioma y salir): si los segmentos
|
|
121
|
+
* no caben, recorta desde el MEDIO y marca el corte con «…». Sin esto, en 80
|
|
122
|
+
* columnas la ayuda se cortaba por la derecha y las teclas del final (justo las
|
|
123
|
+
* globales) no existían para quien no las supiera de memoria.
|
|
124
|
+
*/
|
|
125
|
+
function fitHelp (segs, cols) {
|
|
126
|
+
const join = (a) => a.join(' · ')
|
|
127
|
+
if (widthOf(join(segs)) + 1 <= cols) return join(segs)
|
|
128
|
+
const head = segs.slice(0, 1)
|
|
129
|
+
const tail = segs.slice(-2)
|
|
130
|
+
const mid = segs.slice(1, -2)
|
|
131
|
+
while (mid.length) {
|
|
132
|
+
mid.pop()
|
|
133
|
+
const cand = join([...head, ...mid, '…', ...tail])
|
|
134
|
+
if (widthOf(cand) + 1 <= cols) return cand
|
|
135
|
+
}
|
|
136
|
+
return join([...head, '…', ...tail])
|
|
137
|
+
}
|
|
138
|
+
|
|
94
139
|
// --------------------------------- pantallas -------------------------------
|
|
95
140
|
|
|
96
141
|
function profileRows (st, t) {
|
|
142
|
+
const i = L(st)
|
|
97
143
|
const list = st.profiles?.profiles || []
|
|
98
144
|
return list.map((p) => {
|
|
99
145
|
const mark = p.current ? t.accent('●') : ' '
|
|
100
|
-
const lk = !p.protected ? t.muted(
|
|
101
|
-
const name = p.current ? t.bold(p.name ||
|
|
146
|
+
const lk = !p.protected ? t.muted(i.noPassword) : (p.locked ? t.warn(i.locked) : t.ok(i.unlocked))
|
|
147
|
+
const name = p.current ? t.bold(p.name || i.noName) : (p.name || i.noName)
|
|
102
148
|
return { text: ` ${mark} ${name} ${t.muted(p.id)} ${t.muted(p.fingerprint || '—')} ${lk}`, sel: true, meta: p }
|
|
103
149
|
})
|
|
104
150
|
}
|
|
105
151
|
|
|
106
152
|
function deviceRows (st, t) {
|
|
153
|
+
const i = L(st)
|
|
107
154
|
const rows = []
|
|
108
155
|
const pend = st.pending
|
|
109
156
|
if (pend) {
|
|
110
|
-
rows.push({ text: t.warn(
|
|
157
|
+
rows.push({ text: t.warn(i.pendingDevice(pend.deviceId)) + t.muted(i.pendingHint), sel: false })
|
|
111
158
|
rows.push({ text: '', sel: false })
|
|
112
159
|
}
|
|
113
160
|
const issued = st.devices?.issued || []
|
|
114
161
|
if (!issued.length) {
|
|
115
|
-
rows.push({ text: t.muted(
|
|
162
|
+
rows.push({ text: t.muted(i.noDevices), sel: false })
|
|
116
163
|
}
|
|
117
164
|
for (const d of issued) {
|
|
118
|
-
const label = d.label || t.muted(
|
|
165
|
+
const label = d.label || t.muted(i.noLabel)
|
|
119
166
|
const line = ` ${t.bold(d.deviceId)} ${label} ${t.muted('scope:' + shortScope(d.scope))} ${t.muted('exp:' + fmtExp(d.exp))} ${t.muted('nonce:' + (d.nonce ?? '—'))}`
|
|
120
167
|
rows.push({ text: line, sel: true, meta: d })
|
|
121
168
|
}
|
|
122
169
|
const revoked = st.devices?.revoked || []
|
|
123
170
|
if (revoked.length) {
|
|
124
171
|
rows.push({ text: '', sel: false })
|
|
125
|
-
rows.push({ text: t.muted(
|
|
172
|
+
rows.push({ text: t.muted(i.revokedCount(revoked.length)), sel: false })
|
|
126
173
|
}
|
|
127
174
|
return rows
|
|
128
175
|
}
|
|
129
176
|
|
|
177
|
+
/**
|
|
178
|
+
* LA PREGUNTA DEL EMPAREJAMIENTO. La decisión es del vault (es quien lo inicia) y
|
|
179
|
+
* este daemon puede tener varias cuentas: antes de mostrar el QR hay que decir a
|
|
180
|
+
* cuál entra el dispositivo. Hoy se responde con las dos formas que existen —una
|
|
181
|
+
* cuenta que ya vive aquí, o una nueva que se estrena para él—; la tercera
|
|
182
|
+
* («adoptar la que trae el aparato») necesita el protocolo de adopción y se
|
|
183
|
+
* muestra desactivada para no prometer lo que todavía no hace
|
|
184
|
+
* (docs/vinculacion-de-cuentas.md §5).
|
|
185
|
+
*/
|
|
186
|
+
function pairModeRows (st, t) {
|
|
187
|
+
const i = L(st)
|
|
188
|
+
const ap = activeProfile(st)
|
|
189
|
+
const rows = [{ text: t.muted(' ' + i.pairModeIntro), sel: false }, { text: '', sel: false }]
|
|
190
|
+
rows.push({ text: ` ${t.bold(i.pairModeHere(ap?.name || ap?.id || '—'))}`, sel: true, meta: { mode: 'here' } })
|
|
191
|
+
rows.push({ text: t.muted(' ' + i.pairModeHereHint), sel: false })
|
|
192
|
+
rows.push({ text: '', sel: false })
|
|
193
|
+
rows.push({ text: ` ${t.bold(i.pairModeNew)}`, sel: true, meta: { mode: 'new' } })
|
|
194
|
+
rows.push({ text: t.muted(' ' + i.pairModeNewHint), sel: false })
|
|
195
|
+
rows.push({ text: '', sel: false })
|
|
196
|
+
rows.push({ text: ' ' + t.muted(i.pairModeAdopt), sel: false })
|
|
197
|
+
rows.push({ text: t.muted(' (' + i.pairModeAdoptSoon + ')'), sel: false })
|
|
198
|
+
return rows
|
|
199
|
+
}
|
|
200
|
+
|
|
130
201
|
function secretRows (st, t) {
|
|
202
|
+
const i = L(st)
|
|
131
203
|
const ns = st.secrets || {}
|
|
132
204
|
const names = Object.keys(ns).sort()
|
|
133
205
|
const rows = []
|
|
134
206
|
if (!names.length) {
|
|
135
|
-
rows.push({ text: t.muted(
|
|
207
|
+
rows.push({ text: t.muted(i.noScopes), sel: false })
|
|
136
208
|
return rows
|
|
137
209
|
}
|
|
138
210
|
for (const n of names) {
|
|
139
|
-
rows.push({ text: t.accent(` ▸ ${n}`) + t.muted(
|
|
211
|
+
rows.push({ text: t.accent(` ▸ ${n}`) + t.muted(i.scopeOf(n)), sel: true, meta: { ns: n, key: null } })
|
|
140
212
|
for (const k of ns[n].slice().sort()) {
|
|
141
213
|
rows.push({ text: ` ${k} ${t.muted('••••••')}`, sel: true, meta: { ns: n, key: k } })
|
|
142
214
|
}
|
|
@@ -156,11 +228,11 @@ function setConfirm (st, opts) { st.confirm = { ...opts } }
|
|
|
156
228
|
async function guard (term, st, msg, fn) {
|
|
157
229
|
st.busy = msg
|
|
158
230
|
render(term, st)
|
|
159
|
-
try { const v = await fn(); st.busy = null; return { ok: true, v } } catch (e) { st.busy = null; flash(st, humanErr(e), 'danger'); return { ok: false, e } }
|
|
231
|
+
try { const v = await fn(); st.busy = null; return { ok: true, v } } catch (e) { st.busy = null; flash(st, humanErr(e, st), 'danger'); return { ok: false, e } }
|
|
160
232
|
}
|
|
161
233
|
|
|
162
234
|
async function refreshAll (term, st) {
|
|
163
|
-
const r = await guard(term, st,
|
|
235
|
+
const r = await guard(term, st, L(st).loading, () => vc.snapshot(activeId(st)))
|
|
164
236
|
if (!r.ok) return
|
|
165
237
|
const { devices, secrets, profiles } = r.v
|
|
166
238
|
if (profiles) st.profiles = profiles
|
|
@@ -172,28 +244,29 @@ async function refreshAll (term, st) {
|
|
|
172
244
|
}
|
|
173
245
|
|
|
174
246
|
async function refreshDevices (term, st) {
|
|
175
|
-
const r = await guard(term, st,
|
|
247
|
+
const r = await guard(term, st, L(st).loadingDevices, () => vc.listDevices(activeId(st)))
|
|
176
248
|
if (r.ok) st.devices = r.v
|
|
177
249
|
}
|
|
178
250
|
async function refreshSecrets (term, st) {
|
|
179
|
-
const r = await guard(term, st,
|
|
251
|
+
const r = await guard(term, st, L(st).loadingSecrets, () => vc.listSecrets(activeId(st)))
|
|
180
252
|
if (r.ok) st.secrets = r.v
|
|
181
253
|
}
|
|
182
254
|
async function refreshProfiles (term, st) {
|
|
183
|
-
const r = await guard(term, st,
|
|
255
|
+
const r = await guard(term, st, L(st).loadingVaults, () => vc.listProfiles())
|
|
184
256
|
if (r.ok) st.profiles = r.v
|
|
185
257
|
}
|
|
186
258
|
|
|
187
259
|
/** Asegura la bóveda desbloqueada antes de EDITARLA (rename/rm/password). */
|
|
188
260
|
async function ensureUnlocked (term, st, p, thenFn) {
|
|
189
261
|
if (!p.protected || !p.locked) return thenFn()
|
|
262
|
+
const i = L(st)
|
|
190
263
|
setInput(st, {
|
|
191
|
-
label:
|
|
264
|
+
label: i.passwordOf(p.name || p.id),
|
|
192
265
|
mask: true,
|
|
193
|
-
hint:
|
|
266
|
+
hint: i.passwordToEdit,
|
|
194
267
|
onSubmit: async (pwd) => {
|
|
195
268
|
st.input = null
|
|
196
|
-
const r = await guard(term, st,
|
|
269
|
+
const r = await guard(term, st, i.unlocking, () => vc.unlockProfile(p.id, pwd))
|
|
197
270
|
if (!r.ok) return
|
|
198
271
|
await refreshProfiles(term, st)
|
|
199
272
|
const fresh = (st.profiles.profiles || []).find((x) => x.id === p.id) || p
|
|
@@ -218,7 +291,15 @@ function moveSel (st, key, screen, count) {
|
|
|
218
291
|
else if (key.name === 'end') st.sel[screen] = count - 1
|
|
219
292
|
}
|
|
220
293
|
|
|
294
|
+
/** Conmuta es⇄en, lo recuerda y lo dice en el idioma NUEVO. */
|
|
295
|
+
function toggleLang (st) {
|
|
296
|
+
st.lang = otherLang(st.lang)
|
|
297
|
+
saveLang(st.lang)
|
|
298
|
+
flash(st, L(st).langChanged)
|
|
299
|
+
}
|
|
300
|
+
|
|
221
301
|
async function onKeyProfiles (term, st, key) {
|
|
302
|
+
const i = L(st)
|
|
222
303
|
const rows = profileRows(st, term.t)
|
|
223
304
|
const sels = rows.filter((r) => r.sel).map((r) => r.meta)
|
|
224
305
|
moveSel(st, key, 'profiles', sels.length)
|
|
@@ -229,65 +310,65 @@ async function onKeyProfiles (term, st, key) {
|
|
|
229
310
|
// Entrar a la bóveda: la activa (si no lo estaba ya) y pasa a sus pestañas
|
|
230
311
|
// (Dispositivos/Scopes) — así siempre es explícito de qué bóveda son los ítems.
|
|
231
312
|
if (!cur.current) {
|
|
232
|
-
const r = await guard(term, st,
|
|
313
|
+
const r = await guard(term, st, i.switchingVault, () => vc.useProfile(cur.id))
|
|
233
314
|
if (!r.ok) return true
|
|
234
|
-
flash(st,
|
|
315
|
+
flash(st, i.vaultNowActive(cur.name || cur.id))
|
|
235
316
|
await refreshAll(term, st)
|
|
236
317
|
}
|
|
237
318
|
st.screen = 'devices'
|
|
238
319
|
await refreshDevices(term, st)
|
|
239
320
|
} else if (ch === 'n') {
|
|
240
321
|
setInput(st, {
|
|
241
|
-
label:
|
|
242
|
-
hint:
|
|
322
|
+
label: i.newVaultLabel,
|
|
323
|
+
hint: i.newVaultHint,
|
|
243
324
|
onSubmit: async (name) => {
|
|
244
325
|
st.input = null
|
|
245
|
-
if (!name.trim()) { flash(st,
|
|
246
|
-
const r = await guard(term, st,
|
|
247
|
-
if (r.ok) { flash(st,
|
|
326
|
+
if (!name.trim()) { flash(st, i.nameEmpty, 'danger'); return }
|
|
327
|
+
const r = await guard(term, st, i.creatingVault, () => vc.addProfile(name.trim()))
|
|
328
|
+
if (r.ok) { flash(st, i.vaultCreated(name.trim())); await refreshProfiles(term, st) }
|
|
248
329
|
},
|
|
249
330
|
onCancel: () => { st.input = null }
|
|
250
331
|
})
|
|
251
332
|
} else if (ch === 'r' && cur) {
|
|
252
333
|
await ensureUnlocked(term, st, cur, (p = cur) => setInput(st, {
|
|
253
|
-
label:
|
|
334
|
+
label: i.renameLabel(p.name || p.id),
|
|
254
335
|
value: p.name || '',
|
|
255
336
|
onSubmit: async (name) => {
|
|
256
337
|
st.input = null
|
|
257
|
-
if (!name.trim()) { flash(st,
|
|
258
|
-
const r = await guard(term, st,
|
|
259
|
-
if (r.ok) { flash(st,
|
|
338
|
+
if (!name.trim()) { flash(st, i.nameEmpty, 'danger'); return }
|
|
339
|
+
const r = await guard(term, st, i.renaming, () => vc.renameProfile(p.id, name.trim()))
|
|
340
|
+
if (r.ok) { flash(st, i.vaultRenamed); await refreshProfiles(term, st) }
|
|
260
341
|
},
|
|
261
342
|
onCancel: () => { st.input = null }
|
|
262
343
|
}))
|
|
263
344
|
} else if ((key.name === 'delete' || ch === 'd') && cur) {
|
|
264
|
-
if ((st.profiles.profiles || []).length <= 1) { flash(st,
|
|
345
|
+
if ((st.profiles.profiles || []).length <= 1) { flash(st, i.cantDeleteLast, 'danger'); return true }
|
|
265
346
|
await ensureUnlocked(term, st, cur, (p = cur) => setInput(st, {
|
|
266
|
-
label:
|
|
267
|
-
hint:
|
|
347
|
+
label: i.deleteLabel(p.name || p.id),
|
|
348
|
+
hint: i.deleteHint,
|
|
268
349
|
onSubmit: async (typed) => {
|
|
269
350
|
st.input = null
|
|
270
|
-
if (typed.trim() !== (p.name || p.id)) { flash(st,
|
|
271
|
-
const r = await guard(term, st,
|
|
272
|
-
if (r.ok) { flash(st,
|
|
351
|
+
if (typed.trim() !== (p.name || p.id)) { flash(st, i.deleteMismatch, 'warn'); return }
|
|
352
|
+
const r = await guard(term, st, i.deletingVault, () => vc.removeProfile(p.id))
|
|
353
|
+
if (r.ok) { flash(st, i.vaultDeleted); st.sel.profiles = 0; await refreshAll(term, st) }
|
|
273
354
|
},
|
|
274
355
|
onCancel: () => { st.input = null }
|
|
275
356
|
}))
|
|
276
|
-
} else if (ch === '
|
|
357
|
+
} else if (ch === 'p' && cur) { // password
|
|
277
358
|
await ensureUnlocked(term, st, cur, (p = cur) => setInput(st, {
|
|
278
|
-
label:
|
|
359
|
+
label: i.newPasswordLabel(p.name || p.id),
|
|
279
360
|
mask: true,
|
|
280
361
|
onSubmit: async (pwd) => {
|
|
281
362
|
st.input = null
|
|
282
|
-
if (pwd.length < 4) { flash(st,
|
|
363
|
+
if (pwd.length < 4) { flash(st, i.passwordTooShort, 'danger'); return }
|
|
283
364
|
setInput(st, {
|
|
284
|
-
label:
|
|
365
|
+
label: i.repeatPassword,
|
|
285
366
|
mask: true,
|
|
286
367
|
onSubmit: async (again) => {
|
|
287
368
|
st.input = null
|
|
288
|
-
if (again !== pwd) { flash(st,
|
|
289
|
-
const r = await guard(term, st,
|
|
290
|
-
if (r.ok) { flash(st,
|
|
369
|
+
if (again !== pwd) { flash(st, i.passwordMismatch, 'danger'); return }
|
|
370
|
+
const r = await guard(term, st, i.savingPassword, () => vc.setProfilePassword(p.id, pwd))
|
|
371
|
+
if (r.ok) { flash(st, i.passwordSaved); await refreshProfiles(term, st) }
|
|
291
372
|
},
|
|
292
373
|
onCancel: () => { st.input = null }
|
|
293
374
|
})
|
|
@@ -295,24 +376,25 @@ async function onKeyProfiles (term, st, key) {
|
|
|
295
376
|
onCancel: () => { st.input = null }
|
|
296
377
|
}))
|
|
297
378
|
} else if (ch === 'x' && cur) { // quitar contraseña
|
|
298
|
-
if (!cur.protected) { flash(st,
|
|
379
|
+
if (!cur.protected) { flash(st, i.noPasswordSet, 'warn'); return true }
|
|
299
380
|
await ensureUnlocked(term, st, cur, async (p = cur) => {
|
|
300
|
-
const r = await guard(term, st,
|
|
301
|
-
if (r.ok) { flash(st,
|
|
381
|
+
const r = await guard(term, st, i.removingPassword, () => vc.removeProfilePassword(p.id))
|
|
382
|
+
if (r.ok) { flash(st, i.passwordRemoved); await refreshProfiles(term, st) }
|
|
302
383
|
})
|
|
303
384
|
} else if (ch === 'u' && cur) {
|
|
304
|
-
if (!cur.protected) { flash(st,
|
|
305
|
-
if (!cur.locked) { flash(st,
|
|
306
|
-
await ensureUnlocked(term, st, cur, async () => { flash(st,
|
|
307
|
-
} else if (ch === '
|
|
308
|
-
if (!cur.protected) { flash(st,
|
|
309
|
-
const r = await guard(term, st,
|
|
310
|
-
if (r.ok) { flash(st,
|
|
385
|
+
if (!cur.protected) { flash(st, i.noPasswordSet, 'warn'); return true }
|
|
386
|
+
if (!cur.locked) { flash(st, i.alreadyUnlocked, 'warn'); return true }
|
|
387
|
+
await ensureUnlocked(term, st, cur, async () => { flash(st, i.vaultUnlocked); await refreshProfiles(term, st) })
|
|
388
|
+
} else if (ch === 'k' && cur) { // locK (antes `l`, que ahora es el idioma)
|
|
389
|
+
if (!cur.protected) { flash(st, i.noPasswordSet, 'warn'); return true }
|
|
390
|
+
const r = await guard(term, st, i.lockingVault, () => vc.lockProfile(cur.id))
|
|
391
|
+
if (r.ok) { flash(st, i.vaultLocked); await refreshProfiles(term, st) }
|
|
311
392
|
}
|
|
312
393
|
return true
|
|
313
394
|
}
|
|
314
395
|
|
|
315
396
|
async function onKeyDevices (term, st, key) {
|
|
397
|
+
const i = L(st)
|
|
316
398
|
// Sondea el dispositivo pendiente en cada tick (uno puede conectarse mientras
|
|
317
399
|
// estás en esta pantalla, no solo en la de emparejamiento).
|
|
318
400
|
if (key.name === 'tick') { st.pending = vc.pendingEnroll(); return true }
|
|
@@ -323,23 +405,24 @@ async function onKeyDevices (term, st, key) {
|
|
|
323
405
|
const cur = sels[Math.min(st.sel.devices, sels.length - 1)]
|
|
324
406
|
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
325
407
|
|
|
326
|
-
if (ch === '
|
|
327
|
-
|
|
328
|
-
|
|
408
|
+
if (ch === 'p') { // pair → primero LA PREGUNTA (a qué cuenta entra), luego el QR
|
|
409
|
+
st.sel.pairmode = 0
|
|
410
|
+
st.scroll.pairmode = { value: 0 }
|
|
411
|
+
st.screen = 'pairmode'
|
|
329
412
|
} else if (ch === 'a') { // aprobar el pendiente
|
|
330
|
-
if (!st.pending) { flash(st,
|
|
413
|
+
if (!st.pending) { flash(st, i.noPending, 'warn'); return true }
|
|
331
414
|
promptApprove(term, st)
|
|
332
415
|
} else if (ch === 'x') { // rechazar el pendiente
|
|
333
|
-
if (!st.pending) { flash(st,
|
|
334
|
-
const r = await guard(term, st,
|
|
335
|
-
if (r.ok) { flash(st,
|
|
416
|
+
if (!st.pending) { flash(st, i.noPendingToReject, 'warn'); return true }
|
|
417
|
+
const r = await guard(term, st, i.rejecting, () => vc.rejectPending(st.pending.deviceId, activeId(st)))
|
|
418
|
+
if (r.ok) { flash(st, i.deviceRejected); st.pending = null }
|
|
336
419
|
} else if ((ch === 'v' || key.name === 'delete') && cur?.nonce != null) { // revocar el enrolado seleccionado
|
|
337
420
|
setConfirm(st, {
|
|
338
|
-
text:
|
|
421
|
+
text: i.revokeConfirm(cur.deviceId),
|
|
339
422
|
onYes: async () => {
|
|
340
423
|
st.confirm = null
|
|
341
|
-
const r = await guard(term, st,
|
|
342
|
-
if (r.ok) { flash(st,
|
|
424
|
+
const r = await guard(term, st, i.revoking, () => vc.revokeDevice(cur.nonce, activeId(st)))
|
|
425
|
+
if (r.ok) { flash(st, i.deviceRevoked(cur.deviceId)); st.devices = r.v; st.sel.devices = 0 }
|
|
343
426
|
},
|
|
344
427
|
onNo: () => { st.confirm = null }
|
|
345
428
|
})
|
|
@@ -349,21 +432,67 @@ async function onKeyDevices (term, st, key) {
|
|
|
349
432
|
return true
|
|
350
433
|
}
|
|
351
434
|
|
|
435
|
+
/** Abre el emparejamiento contra `profile` y salta a la pantalla del QR. */
|
|
436
|
+
async function beginPairing (term, st, profile) {
|
|
437
|
+
const r = await guard(term, st, L(st).startingPairing, () => vc.startPairing({ profile }))
|
|
438
|
+
if (r.ok) { st.pairing = r.v; st.pending = null; st.screen = 'pairing' }
|
|
439
|
+
return r.ok
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function onKeyPairMode (term, st, key) {
|
|
443
|
+
const i = L(st)
|
|
444
|
+
const rows = pairModeRows(st, term.t)
|
|
445
|
+
const sels = rows.filter((r) => r.sel).map((r) => r.meta)
|
|
446
|
+
moveSel(st, key, 'pairmode', sels.length)
|
|
447
|
+
const cur = sels[Math.min(st.sel.pairmode, sels.length - 1)]
|
|
448
|
+
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
449
|
+
|
|
450
|
+
if (key.name === 'escape' || ch === 'b') { st.screen = 'devices'; return true }
|
|
451
|
+
if (key.name !== 'enter' || !cur) return true
|
|
452
|
+
|
|
453
|
+
if (cur.mode === 'here') { await beginPairing(term, st, activeId(st)); return true }
|
|
454
|
+
|
|
455
|
+
// Cuenta nueva: se crea aquí, se ACTIVA (así aprobar/rechazar y las listas miran
|
|
456
|
+
// a la misma que el QR) y recién entonces se abre el emparejamiento contra ella.
|
|
457
|
+
setInput(st, {
|
|
458
|
+
label: i.newAccountLabel,
|
|
459
|
+
hint: i.newAccountHint,
|
|
460
|
+
onSubmit: async (name) => {
|
|
461
|
+
st.input = null
|
|
462
|
+
const nombre = name.trim()
|
|
463
|
+
if (!nombre) { flash(st, i.nameEmpty, 'danger'); return }
|
|
464
|
+
const r = await guard(term, st, i.creatingVault, () => vc.addProfile(nombre))
|
|
465
|
+
if (!r.ok) return
|
|
466
|
+
const nuevo = r.v?.id || (r.v?.profiles || []).find((p) => p.name === nombre)?.id
|
|
467
|
+
if (!nuevo) { flash(st, i.errNoReply, 'danger'); return }
|
|
468
|
+
const u = await guard(term, st, i.switchingVault, () => vc.useProfile(nuevo))
|
|
469
|
+
if (!u.ok) return
|
|
470
|
+
await refreshAll(term, st)
|
|
471
|
+
flash(st, i.accountCreated(nombre))
|
|
472
|
+
await beginPairing(term, st, nuevo)
|
|
473
|
+
},
|
|
474
|
+
onCancel: () => { st.input = null }
|
|
475
|
+
})
|
|
476
|
+
return true
|
|
477
|
+
}
|
|
478
|
+
|
|
352
479
|
function promptApprove (term, st) {
|
|
480
|
+
const i = L(st)
|
|
353
481
|
setInput(st, {
|
|
354
|
-
label:
|
|
355
|
-
hint:
|
|
482
|
+
label: i.approveLabel(st.pending?.deviceId || ''),
|
|
483
|
+
hint: i.approveHint,
|
|
356
484
|
onSubmit: async (code) => {
|
|
357
485
|
st.input = null
|
|
358
|
-
if (!code.trim()) { flash(st,
|
|
359
|
-
const r = await guard(term, st,
|
|
360
|
-
if (r.ok) { flash(st,
|
|
486
|
+
if (!code.trim()) { flash(st, i.codeMissing, 'danger'); return }
|
|
487
|
+
const r = await guard(term, st, i.approving, () => vc.approvePending(code.trim(), activeId(st)))
|
|
488
|
+
if (r.ok) { flash(st, i.deviceApproved); st.devices = r.v; st.pending = null; st.screen = 'devices' }
|
|
361
489
|
},
|
|
362
490
|
onCancel: () => { st.input = null }
|
|
363
491
|
})
|
|
364
492
|
}
|
|
365
493
|
|
|
366
494
|
async function onKeyPairing (term, st, key) {
|
|
495
|
+
const i = L(st)
|
|
367
496
|
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
368
497
|
if (key.name === 'tick') {
|
|
369
498
|
const pend = vc.pendingEnroll()
|
|
@@ -372,12 +501,12 @@ async function onKeyPairing (term, st, key) {
|
|
|
372
501
|
}
|
|
373
502
|
if (ch === 'a' && st.pending) { promptApprove(term, st); return true }
|
|
374
503
|
if (ch === 'x' && st.pending) {
|
|
375
|
-
const r = await guard(term, st,
|
|
376
|
-
if (r.ok) { flash(st,
|
|
504
|
+
const r = await guard(term, st, i.rejecting, () => vc.rejectPending(st.pending.deviceId, activeId(st)))
|
|
505
|
+
if (r.ok) { flash(st, i.deviceRejected); st.pending = null }
|
|
377
506
|
return true
|
|
378
507
|
}
|
|
379
|
-
if (ch === '
|
|
380
|
-
const r = await guard(term, st,
|
|
508
|
+
if (ch === 'r') { // restart: reiniciar el emparejamiento
|
|
509
|
+
const r = await guard(term, st, i.restartingPairing, () => vc.startPairing({ profile: activeId(st) }))
|
|
381
510
|
if (r.ok) { st.pairing = r.v; st.pending = null }
|
|
382
511
|
return true
|
|
383
512
|
}
|
|
@@ -386,6 +515,7 @@ async function onKeyPairing (term, st, key) {
|
|
|
386
515
|
}
|
|
387
516
|
|
|
388
517
|
async function onKeySecrets (term, st, key) {
|
|
518
|
+
const i = L(st)
|
|
389
519
|
const rows = secretRows(st, term.t)
|
|
390
520
|
const sels = rows.filter((r) => r.sel).map((r) => r.meta)
|
|
391
521
|
moveSel(st, key, 'secrets', sels.length)
|
|
@@ -397,22 +527,22 @@ async function onKeySecrets (term, st, key) {
|
|
|
397
527
|
} else if ((ch === 'x' || key.name === 'delete') && cur) {
|
|
398
528
|
if (cur.key) {
|
|
399
529
|
setConfirm(st, {
|
|
400
|
-
text:
|
|
530
|
+
text: i.removeVarConfirm(cur.ns, cur.key),
|
|
401
531
|
onYes: async () => {
|
|
402
532
|
st.confirm = null
|
|
403
|
-
const r = await guard(term, st,
|
|
404
|
-
if (r.ok) { flash(st,
|
|
533
|
+
const r = await guard(term, st, i.removingVar, () => vc.deleteSecret(cur.ns, cur.key, activeId(st)))
|
|
534
|
+
if (r.ok) { flash(st, i.varRemoved); st.secrets = r.v; st.sel.secrets = Math.max(0, st.sel.secrets - 1) }
|
|
405
535
|
},
|
|
406
536
|
onNo: () => { st.confirm = null }
|
|
407
537
|
})
|
|
408
538
|
} else {
|
|
409
539
|
const count = (st.secrets?.[cur.ns] || []).length
|
|
410
540
|
setConfirm(st, {
|
|
411
|
-
text:
|
|
541
|
+
text: i.removeScopeConfirm(cur.ns, count),
|
|
412
542
|
onYes: async () => {
|
|
413
543
|
st.confirm = null
|
|
414
|
-
const r = await guard(term, st,
|
|
415
|
-
if (r.ok) { flash(st,
|
|
544
|
+
const r = await guard(term, st, i.removingScope, () => vc.deleteScope(cur.ns, activeId(st)))
|
|
545
|
+
if (r.ok) { flash(st, i.scopeRemoved(cur.ns)); st.secrets = r.v; st.sel.secrets = 0 }
|
|
416
546
|
},
|
|
417
547
|
onNo: () => { st.confirm = null }
|
|
418
548
|
})
|
|
@@ -424,30 +554,31 @@ async function onKeySecrets (term, st, key) {
|
|
|
424
554
|
}
|
|
425
555
|
|
|
426
556
|
function promptNewVariable (term, st) {
|
|
557
|
+
const i = L(st)
|
|
427
558
|
const existing = Object.keys(st.secrets || {})
|
|
428
559
|
setInput(st, {
|
|
429
|
-
label:
|
|
430
|
-
hint: existing.length ?
|
|
560
|
+
label: i.nsLabel,
|
|
561
|
+
hint: existing.length ? i.nsHintExisting(existing.join(', ')) : i.nsHint,
|
|
431
562
|
onSubmit: (ns) => {
|
|
432
563
|
const nsv = ns.trim()
|
|
433
|
-
if (!NS_RE.test(nsv)) { flash(st,
|
|
564
|
+
if (!NS_RE.test(nsv)) { flash(st, i.nsInvalid, 'danger'); promptNewVariable(term, st); return }
|
|
434
565
|
st.input = null
|
|
435
566
|
setInput(st, {
|
|
436
|
-
label:
|
|
437
|
-
hint:
|
|
567
|
+
label: i.keyLabel(nsv),
|
|
568
|
+
hint: i.keyHint,
|
|
438
569
|
onSubmit: (key) => {
|
|
439
570
|
const kv = key.trim()
|
|
440
|
-
if (!KEY_RE.test(kv)) { flash(st,
|
|
571
|
+
if (!KEY_RE.test(kv)) { flash(st, i.keyInvalid, 'danger'); return }
|
|
441
572
|
st.input = null
|
|
442
573
|
setInput(st, {
|
|
443
|
-
label:
|
|
574
|
+
label: i.valueLabel(nsv, kv),
|
|
444
575
|
mask: true,
|
|
445
|
-
hint:
|
|
576
|
+
hint: i.valueHint,
|
|
446
577
|
onSubmit: async (value) => {
|
|
447
578
|
st.input = null
|
|
448
|
-
if (!value) { flash(st,
|
|
449
|
-
const r = await guard(term, st,
|
|
450
|
-
if (r.ok) { flash(st,
|
|
579
|
+
if (!value) { flash(st, i.valueEmpty, 'danger'); return }
|
|
580
|
+
const r = await guard(term, st, i.savingVar, () => vc.setSecret(nsv, kv, value, activeId(st)))
|
|
581
|
+
if (r.ok) { flash(st, i.varSaved(nsv, kv)); st.secrets = r.v }
|
|
451
582
|
},
|
|
452
583
|
onCancel: () => { st.input = null }
|
|
453
584
|
})
|
|
@@ -475,6 +606,8 @@ async function onInputKey (st, key) {
|
|
|
475
606
|
async function onConfirmKey (st, key) {
|
|
476
607
|
const cf = st.confirm
|
|
477
608
|
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
609
|
+
// La tecla es `y` (yes) en los dos idiomas —como el resto, mnemónico inglés—;
|
|
610
|
+
// `s` (sí) se sigue aceptando por costumbre, pero no se anuncia en la ayuda.
|
|
478
611
|
if (ch === 's' || ch === 'y') { const f = cf.onYes; st.confirm = null; await f?.() }
|
|
479
612
|
else if (ch === 'n' || key.name === 'escape' || key.name === 'enter' || key.name === 'ctrl-c') { const f = cf.onNo; st.confirm = null; await f?.() }
|
|
480
613
|
}
|
|
@@ -484,62 +617,74 @@ async function onConfirmKey (st, key) {
|
|
|
484
617
|
// Pestañas INTERNAS de una bóveda ya elegida: se cambian con ←→. La lista de
|
|
485
618
|
// bóvedas (profiles) es el nivel de arriba (se entra con Enter, no es una pestaña).
|
|
486
619
|
const INNER_TABS = ['devices', 'secrets']
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
profiles:
|
|
491
|
-
devices:
|
|
492
|
-
secrets:
|
|
493
|
-
pairing:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
620
|
+
const tabLabel = (i, k) => (k === 'devices' ? i.tabDevices : i.tabSecrets)
|
|
621
|
+
|
|
622
|
+
const helpSegs = (i, screen) => ({
|
|
623
|
+
profiles: i.helpProfiles,
|
|
624
|
+
devices: i.helpDevices,
|
|
625
|
+
secrets: i.helpSecrets,
|
|
626
|
+
pairing: i.helpPairing,
|
|
627
|
+
pairmode: i.helpPairMode
|
|
628
|
+
})[screen] || []
|
|
629
|
+
|
|
630
|
+
const title = (i, screen) => ({
|
|
631
|
+
profiles: i.titleProfiles,
|
|
632
|
+
pairing: i.titlePairing,
|
|
633
|
+
pairmode: i.titlePairMode
|
|
634
|
+
})[screen] || ''
|
|
499
635
|
|
|
500
636
|
/** Barra de pestañas horizontal (Dispositivos | Scopes y variables) de la bóveda entrada. */
|
|
501
637
|
function renderTabs (st, t) {
|
|
638
|
+
const i = L(st)
|
|
502
639
|
return INNER_TABS.map((k) => {
|
|
503
640
|
const active = st.screen === k
|
|
504
|
-
return active ? t.bold(t.accent('▐ ' +
|
|
505
|
-
}).join(' ') + t.muted(
|
|
641
|
+
return active ? t.bold(t.accent('▐ ' + tabLabel(i, k) + ' ▌')) : t.muted(' ' + tabLabel(i, k) + ' ')
|
|
642
|
+
}).join(' ') + t.muted(i.tabsHint)
|
|
506
643
|
}
|
|
507
644
|
|
|
508
645
|
function pairingBody (st, t, cols, height) {
|
|
646
|
+
const i = L(st)
|
|
509
647
|
const info = st.pairing
|
|
510
648
|
const lines = []
|
|
649
|
+
// QUÉ CUENTA se está compartiendo: el vault puede tener varias y el QR sale de
|
|
650
|
+
// UNA (la bóveda en la que entraste). Decirlo aquí evita enrolar un dispositivo
|
|
651
|
+
// en la cuenta equivocada sin enterarse.
|
|
652
|
+
const ap = activeProfile(st)
|
|
653
|
+
const acct = info.profileName || ap?.name || info.profile || ap?.id || '—'
|
|
654
|
+
lines.push(t.bold(i.pairAccount(acct)))
|
|
511
655
|
const left = Math.max(0, Math.round((info.expiresAt - Date.now()) / 60000))
|
|
512
|
-
lines.push(t.muted(
|
|
656
|
+
lines.push(t.muted(i.pairValid(left)))
|
|
513
657
|
lines.push('')
|
|
514
658
|
// QR solo si entra cómodo (es "alto": ~ (módulos+8)/2 filas).
|
|
515
659
|
let qr = ''
|
|
516
660
|
try { qr = qrToString(info.url) } catch (_) {}
|
|
517
661
|
const qrLines = qr ? qr.replace(/\n$/, '').split('\n') : []
|
|
518
662
|
const qrWidth = qrLines.length ? Math.max(...qrLines.map((l) => l.replace(/\x1b\[[0-9;]*m/g, '').length)) : 0
|
|
519
|
-
const reserved =
|
|
663
|
+
const reserved = 9 // encabezado + cuenta + URL + payload + aviso
|
|
520
664
|
if (qrLines.length && qrWidth <= cols && qrLines.length <= height - reserved) {
|
|
521
665
|
for (const l of qrLines) lines.push(l)
|
|
522
666
|
lines.push('')
|
|
523
667
|
}
|
|
524
|
-
lines.push(t.bold(
|
|
668
|
+
lines.push(t.bold(i.pairUrl) + info.url)
|
|
525
669
|
lines.push('')
|
|
526
|
-
lines.push(t.muted(
|
|
670
|
+
lines.push(t.muted(i.pairPaste))
|
|
527
671
|
lines.push(info.payload)
|
|
528
672
|
lines.push('')
|
|
529
|
-
lines.push(t.danger(
|
|
673
|
+
lines.push(t.danger(i.pairWarning))
|
|
530
674
|
lines.push('')
|
|
531
|
-
if (st.pending) lines.push(t.warn(
|
|
532
|
-
else lines.push(t.muted(
|
|
675
|
+
if (st.pending) lines.push(t.warn(i.pairConnected(st.pending.deviceId)))
|
|
676
|
+
else lines.push(t.muted(i.pairWaiting))
|
|
533
677
|
return lines
|
|
534
678
|
}
|
|
535
679
|
|
|
536
680
|
function render (term, st) {
|
|
537
681
|
const t = term.t
|
|
682
|
+
const i = L(st)
|
|
538
683
|
const { cols, rows } = term.size()
|
|
539
684
|
// La distribución necesita: header+contexto (5) + 1 de contenido + estado + ayuda.
|
|
540
685
|
// En un terminal más chico, en vez de escribir en índices fuera de rango, avisamos.
|
|
541
686
|
if (rows < 9 || cols < 24) {
|
|
542
|
-
term.render([t.warn(
|
|
687
|
+
term.render([t.warn(i.tooSmall), i.tooSmallHint(cols, rows)])
|
|
543
688
|
return
|
|
544
689
|
}
|
|
545
690
|
const lines = new Array(rows).fill('')
|
|
@@ -547,16 +692,16 @@ function render (term, st) {
|
|
|
547
692
|
const s = st.state
|
|
548
693
|
const up = st.daemonUp
|
|
549
694
|
const ver = s?.version || 'dev'
|
|
550
|
-
const daemonTxt = up ?
|
|
695
|
+
const daemonTxt = up ? i.daemonRunning : i.daemonStopped
|
|
551
696
|
lines[0] = t.bar(`dotrino-vault ${ver} daemon: ${daemonTxt} ${vc.vaultDir()}`, cols)
|
|
552
697
|
|
|
553
698
|
const ap = activeProfile(st)
|
|
554
|
-
const apTxt = ap ? `${t.accent('●')} ${t.bold(ap.name ||
|
|
555
|
-
lines[1] = '
|
|
699
|
+
const apTxt = ap ? `${t.accent('●')} ${t.bold(ap.name || i.noName)} ${lockGlyph(ap)} ${t.muted('· ' + (ap.fingerprint || '—'))}` : t.muted('—')
|
|
700
|
+
lines[1] = ' ' + i.activeVault + apTxt
|
|
556
701
|
lines[2] = ''
|
|
557
702
|
// Dispositivos/Scopes son pestañas de la bóveda activa (se entra desde Bóvedas);
|
|
558
703
|
// el resto muestra su título simple.
|
|
559
|
-
lines[3] = INNER_TABS.includes(st.screen) ? ' ' + renderTabs(st, t) : ' ' + t.title('» ' + (
|
|
704
|
+
lines[3] = INNER_TABS.includes(st.screen) ? ' ' + renderTabs(st, t) : ' ' + t.title('» ' + title(i, st.screen))
|
|
560
705
|
lines[4] = ''
|
|
561
706
|
|
|
562
707
|
const top = 5
|
|
@@ -568,12 +713,13 @@ function render (term, st) {
|
|
|
568
713
|
if (st.screen === 'profiles') body = renderList(profileRows(st, t), st.sel.profiles, contentH, cols, t, scrollRef)
|
|
569
714
|
else if (st.screen === 'devices') body = renderList(deviceRows(st, t), st.sel.devices, contentH, cols, t, scrollRef)
|
|
570
715
|
else if (st.screen === 'secrets') body = renderList(secretRows(st, t), st.sel.secrets, contentH, cols, t, scrollRef)
|
|
716
|
+
else if (st.screen === 'pairmode') body = renderList(pairModeRows(st, t), st.sel.pairmode, contentH, cols, t, scrollRef)
|
|
571
717
|
else if (st.screen === 'pairing') {
|
|
572
718
|
const pb = pairingBody(st, t, cols, contentH)
|
|
573
719
|
body = pb.slice(0, contentH)
|
|
574
720
|
while (body.length < contentH) body.push('')
|
|
575
721
|
}
|
|
576
|
-
for (let
|
|
722
|
+
for (let n = 0; n < contentH; n++) lines[top + n] = body[n] ?? ''
|
|
577
723
|
|
|
578
724
|
// línea de estado: input / confirm / flash / busy
|
|
579
725
|
const statusRow = rows - 2
|
|
@@ -584,7 +730,7 @@ function render (term, st) {
|
|
|
584
730
|
const hint = inp.hint ? t.muted(' [' + inp.hint + ']') : ''
|
|
585
731
|
lines[statusRow] = ' ' + t.bold(inp.label + ': ') + shown + t.accent('▏') + hint
|
|
586
732
|
} else if (st.confirm) {
|
|
587
|
-
lines[statusRow] = ' ' + t.warn(st.confirm.text) + t.muted(
|
|
733
|
+
lines[statusRow] = ' ' + t.warn(st.confirm.text) + t.muted(i.confirmKeys)
|
|
588
734
|
} else if (st.flash) {
|
|
589
735
|
const kind = st.flash.kind
|
|
590
736
|
const style = kind === 'danger' ? t.danger : kind === 'warn' ? t.warn : t.ok
|
|
@@ -592,9 +738,9 @@ function render (term, st) {
|
|
|
592
738
|
} else lines[statusRow] = ''
|
|
593
739
|
|
|
594
740
|
// barra de ayuda
|
|
595
|
-
let help =
|
|
596
|
-
if (st.input) help =
|
|
597
|
-
else if (st.confirm) help =
|
|
741
|
+
let help = fitHelp(helpSegs(i, st.screen), cols)
|
|
742
|
+
if (st.input) help = i.helpInput
|
|
743
|
+
else if (st.confirm) help = i.helpConfirm
|
|
598
744
|
lines[rows - 1] = t.bar(help, cols)
|
|
599
745
|
|
|
600
746
|
term.render(lines)
|
|
@@ -605,39 +751,42 @@ function render (term, st) {
|
|
|
605
751
|
async function daemonDownScreen (term, st) {
|
|
606
752
|
while (true) {
|
|
607
753
|
const t = term.t
|
|
754
|
+
const i = L(st)
|
|
608
755
|
const { cols, rows } = term.size()
|
|
609
756
|
const lines = new Array(Math.max(rows, 2)).fill('')
|
|
610
757
|
// Contenido en orden; se coloca desde la fila 2 y se corta si no cabe (no se
|
|
611
758
|
// escribe nunca en índices fijos que se salgan de un terminal pequeño).
|
|
612
759
|
const content = [
|
|
613
|
-
t.danger(
|
|
760
|
+
t.danger(i.downTitle),
|
|
614
761
|
'',
|
|
615
|
-
|
|
616
|
-
|
|
762
|
+
i.downBody1,
|
|
763
|
+
i.downBody2,
|
|
617
764
|
'',
|
|
618
|
-
t.bold('S') +
|
|
619
|
-
t.bold('R') +
|
|
620
|
-
t.bold('
|
|
765
|
+
t.bold('S') + i.downStart + t.muted('systemctl --user start dotrino-vault'),
|
|
766
|
+
t.bold('R') + i.downRecheck,
|
|
767
|
+
t.bold('l') + i.downLang,
|
|
768
|
+
t.bold('Q') + i.downQuit,
|
|
621
769
|
'',
|
|
622
|
-
t.muted(
|
|
770
|
+
t.muted(i.downDev)
|
|
623
771
|
]
|
|
624
772
|
if (st.flash) content.push('', (st.flash.kind === 'danger' ? t.danger : t.warn)(st.flash.text))
|
|
625
|
-
lines[0] = t.bar(
|
|
626
|
-
for (let
|
|
627
|
-
lines[rows - 1] = t.bar(
|
|
773
|
+
lines[0] = t.bar(i.downHeader, cols)
|
|
774
|
+
for (let n = 0; n < content.length && 2 + n < rows - 1; n++) lines[2 + n] = ' ' + content[n]
|
|
775
|
+
lines[rows - 1] = t.bar(fitHelp(i.downHelp, cols), cols)
|
|
628
776
|
term.render(lines)
|
|
629
777
|
|
|
630
778
|
const key = await term.readKey()
|
|
631
779
|
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
632
780
|
if (ch === 'q' || key.name === 'ctrl-c') return false
|
|
633
|
-
if (ch === '
|
|
781
|
+
if (ch === 'l') { toggleLang(st); continue }
|
|
782
|
+
if (ch === 'r') { if (vc.daemonAlive()) return true; flash(st, L(st).stillDown, 'warn') }
|
|
634
783
|
if (ch === 's') {
|
|
635
|
-
st.busy =
|
|
636
|
-
flash(st,
|
|
784
|
+
st.busy = L(st).starting // (no re-render aquí; mensaje simple)
|
|
785
|
+
flash(st, L(st).startingShort, 'warn'); term.render(lines)
|
|
637
786
|
const r = await startDaemonService()
|
|
638
787
|
await sleep(1500)
|
|
639
788
|
if (vc.daemonAlive()) return true
|
|
640
|
-
flash(st, r.ok ?
|
|
789
|
+
flash(st, r.ok ? L(st).startedNotReady : L(st).startFailed(r.err), 'danger')
|
|
641
790
|
st.busy = null
|
|
642
791
|
}
|
|
643
792
|
}
|
|
@@ -649,7 +798,8 @@ export async function runTui () {
|
|
|
649
798
|
const term = createTerm()
|
|
650
799
|
const st = {
|
|
651
800
|
screen: 'profiles', // se arranca en la lista de bóvedas: hay que ENTRAR a una
|
|
652
|
-
|
|
801
|
+
lang: loadLang(), // es/en — se conmuta con `l` y se recuerda en prefs.json
|
|
802
|
+
sel: { profiles: 0, devices: 0, secrets: 0, pairmode: 0 },
|
|
653
803
|
scroll: {},
|
|
654
804
|
profiles: null,
|
|
655
805
|
devices: null,
|
|
@@ -696,10 +846,12 @@ export async function runTui () {
|
|
|
696
846
|
const ch = key.name === 'char' ? key.ch.toLowerCase() : null
|
|
697
847
|
// 'q' global sale.
|
|
698
848
|
if (ch === 'q') { running = false; continue }
|
|
849
|
+
// 'l' global: idioma es⇄en en cualquier pantalla (por eso el candado es 'c').
|
|
850
|
+
if (ch === 'l') { toggleLang(st); continue }
|
|
699
851
|
// ←→ cambia entre las pestañas de la bóveda entrada (Dispositivos/Scopes).
|
|
700
852
|
if ((key.name === 'left' || key.name === 'right') && INNER_TABS.includes(st.screen)) {
|
|
701
|
-
const
|
|
702
|
-
st.screen = INNER_TABS[(
|
|
853
|
+
const n = INNER_TABS.indexOf(st.screen)
|
|
854
|
+
st.screen = INNER_TABS[(n + (key.name === 'right' ? 1 : -1) + INNER_TABS.length) % INNER_TABS.length]
|
|
703
855
|
continue
|
|
704
856
|
}
|
|
705
857
|
// Esc/'b' desde una pestaña vuelve a la lista de bóvedas (salir de la bóveda
|
|
@@ -711,6 +863,7 @@ export async function runTui () {
|
|
|
711
863
|
if (st.screen === 'profiles') running = await onKeyProfiles(term, st, key)
|
|
712
864
|
else if (st.screen === 'devices') running = await onKeyDevices(term, st, key)
|
|
713
865
|
else if (st.screen === 'secrets') running = await onKeySecrets(term, st, key)
|
|
866
|
+
else if (st.screen === 'pairmode') running = await onKeyPairMode(term, st, key)
|
|
714
867
|
else if (st.screen === 'pairing') running = await onKeyPairing(term, st, key)
|
|
715
868
|
}
|
|
716
869
|
} finally {
|
|
@@ -719,4 +872,4 @@ export async function runTui () {
|
|
|
719
872
|
}
|
|
720
873
|
|
|
721
874
|
// Solo para pruebas headless (render sin terminal real). No usar en runtime.
|
|
722
|
-
export const __test = { render, profileRows, deviceRows, secretRows, pairingBody }
|
|
875
|
+
export const __test = { render, profileRows, deviceRows, secretRows, pairModeRows, pairingBody, fitHelp, toggleLang }
|