@dotrino/vaultd 0.156.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/package.json +1 -1
- package/src/ctl.js +39 -1
- 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/tui/gui.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gui.js — el PUENTE entre la ventana de escritorio y la pantalla de control.
|
|
3
|
+
*
|
|
4
|
+
* La ventana (`desktop/`, Rust + iced) no sabe nada de la bóveda: lanza
|
|
5
|
+
* `dotrino-vault gui-bridge` (el mismo binario) y le habla por stdin/stdout, una línea JSON
|
|
6
|
+
* por mensaje. Este proceso corre LA MISMA sesión que la TUI —`newSession`, `handleKey`,
|
|
7
|
+
* `viewModel` de `app.js`— y le manda a la ventana lo que tiene que pintar. Así la ventana
|
|
8
|
+
* tiene exactamente las pantallas, los textos y las acciones de la TUI, y una acción nueva
|
|
9
|
+
* aparece en las dos sin tocar la otra.
|
|
10
|
+
*
|
|
11
|
+
* No abre ningún puerto: el canal son las tuberías del proceso hijo, que solo ve quien lo
|
|
12
|
+
* lanzó. Y como la TUI, no toca la identidad ni la red: le habla al daemon por su carpeta.
|
|
13
|
+
*
|
|
14
|
+
* Mensajes de la ventana (uno por línea):
|
|
15
|
+
* {"key":{"name":"char","ch":"p"}} una tecla, con los mismos nombres que `term.js`
|
|
16
|
+
* {"select":3} señalar la fila 3 (entre las seleccionables)
|
|
17
|
+
* {"activate":3} señalarla y pulsar Enter
|
|
18
|
+
* {"tab":"secrets"} ir a una pestaña de la bóveda entrada
|
|
19
|
+
* {"submit":"texto"} responder la pregunta abierta con ese texto
|
|
20
|
+
* {"confirm":true|false} responder la confirmación abierta
|
|
21
|
+
* {"quit":true} salir (cerrando lo que se abrió aquí)
|
|
22
|
+
*
|
|
23
|
+
* Mensajes a la ventana:
|
|
24
|
+
* {"t":"view","v":{…}} qué pintar (ver `viewModel`/`downModel`)
|
|
25
|
+
* {"t":"bye","relaunch":bool} se terminó; con `relaunch`, volver a lanzar el puente
|
|
26
|
+
* (se instaló una versión nueva)
|
|
27
|
+
*
|
|
28
|
+
* Los textos llevan sus estilos como tramos: `[{"t":"…","c":"muted","b":true}]`.
|
|
29
|
+
*/
|
|
30
|
+
import * as vc from '../vaultControl.js'
|
|
31
|
+
import { makeTheme } from './term.js'
|
|
32
|
+
import {
|
|
33
|
+
newSession, startSession, beforeRender, tickFor, handleKey, endSession,
|
|
34
|
+
viewModel, downModel, selectRow, openTab, onKeyDown
|
|
35
|
+
} from './app.js'
|
|
36
|
+
|
|
37
|
+
// stdout es el canal: cualquier `console.log` suelto lo rompería. Va a stderr.
|
|
38
|
+
console.log = (...a) => console.error(...a)
|
|
39
|
+
|
|
40
|
+
// ----------------------------- estilos → tramos ------------------------------
|
|
41
|
+
|
|
42
|
+
/** Color del tema de la TUI (`makeTheme`) → nombre que entiende la ventana. */
|
|
43
|
+
const COLORS = { 44: 'accent', 114: 'ok', 214: 'warn', 203: 'danger', 244: 'muted', 81: 'title' }
|
|
44
|
+
|
|
45
|
+
/** Un texto con escapes SGR → tramos `{t, c?, b?}`. */
|
|
46
|
+
export function spans (str) {
|
|
47
|
+
const out = []
|
|
48
|
+
let bold = false
|
|
49
|
+
let color = null
|
|
50
|
+
let buf = ''
|
|
51
|
+
const flush = () => {
|
|
52
|
+
if (!buf) return
|
|
53
|
+
const sp = { t: buf }
|
|
54
|
+
if (color) sp.c = color
|
|
55
|
+
if (bold) sp.b = true
|
|
56
|
+
out.push(sp)
|
|
57
|
+
buf = ''
|
|
58
|
+
}
|
|
59
|
+
const re = /\x1b\[([0-9;]*)m/g
|
|
60
|
+
const s = String(str ?? '')
|
|
61
|
+
let last = 0
|
|
62
|
+
let m
|
|
63
|
+
while ((m = re.exec(s))) {
|
|
64
|
+
buf += s.slice(last, m.index)
|
|
65
|
+
last = re.lastIndex
|
|
66
|
+
flush()
|
|
67
|
+
const codes = m[1] === '' ? [0] : m[1].split(';').map(Number)
|
|
68
|
+
for (let k = 0; k < codes.length; k++) {
|
|
69
|
+
const c = codes[k]
|
|
70
|
+
if (c === 0) { bold = false; color = null } else if (c === 1) bold = true
|
|
71
|
+
else if (c === 38 && codes[k + 1] === 5) { color = COLORS[codes[k + 2]] || null; k += 2 }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
buf += s.slice(last)
|
|
75
|
+
flush()
|
|
76
|
+
return out
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Pasa a tramos todos los textos que el modelo trae con estilos. */
|
|
80
|
+
function toWire (vm) {
|
|
81
|
+
const v = { ...vm }
|
|
82
|
+
if (v.rows) v.rows = v.rows.map((r) => ({ ...r, text: spans(r.text) }))
|
|
83
|
+
if (v.body) v.body = v.body.map(spans)
|
|
84
|
+
for (const k of ['title', 'notice', 'status']) {
|
|
85
|
+
if (v[k] && typeof v[k] === 'object' && 'text' in v[k]) v[k] = { ...v[k], text: spans(v[k].text) }
|
|
86
|
+
}
|
|
87
|
+
if (v.confirm) v.confirm = { ...v.confirm, text: spans(v.confirm.text) }
|
|
88
|
+
// «p emparejar» → { key: 'p', label: 'emparejar' }. La tecla es el mnemónico de la TUI, y
|
|
89
|
+
// la ventana la usa como atajo de teclado y la manda al pulsar el botón.
|
|
90
|
+
v.help = (v.help || []).map((seg) => {
|
|
91
|
+
const sp = String(seg).indexOf(' ')
|
|
92
|
+
return sp < 0 ? { key: seg, label: '' } : { key: seg.slice(0, sp), label: seg.slice(sp + 1) }
|
|
93
|
+
})
|
|
94
|
+
return v
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --------------------------------- el bucle ---------------------------------
|
|
98
|
+
|
|
99
|
+
export async function runGuiBridge ({ input = process.stdin, output = process.stdout } = {}) {
|
|
100
|
+
const t = makeTheme({ color: true })
|
|
101
|
+
const st = newSession()
|
|
102
|
+
const queue = []
|
|
103
|
+
let waiter = null
|
|
104
|
+
|
|
105
|
+
const push = (ev) => { if (waiter) { const w = waiter; waiter = null; w(ev) } else queue.push(ev) }
|
|
106
|
+
|
|
107
|
+
let pending = ''
|
|
108
|
+
input.setEncoding('utf8')
|
|
109
|
+
input.on('data', (chunk) => {
|
|
110
|
+
pending += chunk
|
|
111
|
+
let nl
|
|
112
|
+
while ((nl = pending.indexOf('\n')) >= 0) {
|
|
113
|
+
const line = pending.slice(0, nl).trim()
|
|
114
|
+
pending = pending.slice(nl + 1)
|
|
115
|
+
if (!line) continue
|
|
116
|
+
try { push(JSON.parse(line)) } catch (e) { console.error('[gui-bridge] bad message:', e.message) }
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
// La ventana se cerró (o murió): se sale igual que con `q`, cerrando lo abierto aquí.
|
|
120
|
+
input.on('end', () => push({ quit: true }))
|
|
121
|
+
input.on('error', () => push({ quit: true }))
|
|
122
|
+
|
|
123
|
+
const next = (ms) => {
|
|
124
|
+
if (queue.length) return Promise.resolve(queue.shift())
|
|
125
|
+
return new Promise((resolve) => {
|
|
126
|
+
let timer = null
|
|
127
|
+
waiter = (ev) => { if (timer) clearTimeout(timer); resolve(ev) }
|
|
128
|
+
if (ms) timer = setTimeout(() => { waiter = null; resolve({ key: { name: 'tick' } }) }, ms)
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Si la ventana ya no está, escribir da EPIPE: no es un fallo de la sesión, y no puede
|
|
133
|
+
// tumbar el cierre ordenado (que es el que vuelve a cerrar las bóvedas).
|
|
134
|
+
let gone = false
|
|
135
|
+
output.on?.('error', () => { gone = true })
|
|
136
|
+
let lastSent = ''
|
|
137
|
+
const send = (msg) => {
|
|
138
|
+
if (gone) return
|
|
139
|
+
const line = JSON.stringify(msg)
|
|
140
|
+
if (msg.t === 'view' && line === lastSent) return
|
|
141
|
+
if (msg.t === 'view') lastSent = line
|
|
142
|
+
output.write(line + '\n')
|
|
143
|
+
}
|
|
144
|
+
const sendView = () => send({ t: 'view', v: toWire(st.down ? downModel(st) : viewModel(st, t)) })
|
|
145
|
+
|
|
146
|
+
// La sesión pinta también a mitad de una operación (`guard` pone «Guardando…»): el
|
|
147
|
+
// «terminal» de la ventana solo avisa de que hay algo nuevo.
|
|
148
|
+
const term = {
|
|
149
|
+
t,
|
|
150
|
+
gui: () => sendView(),
|
|
151
|
+
size: () => ({ cols: 200, rows: 60 }),
|
|
152
|
+
render: () => sendView(),
|
|
153
|
+
readKey: async () => ({ name: 'tick' }),
|
|
154
|
+
close: () => {}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Un mensaje de la ventana → la tecla (o las teclas) que equivale. */
|
|
158
|
+
const toKeys = (ev) => {
|
|
159
|
+
if (ev.key) return [ev.key]
|
|
160
|
+
if (typeof ev.select === 'number') { selectRow(st, ev.select); return [] }
|
|
161
|
+
if (typeof ev.activate === 'number') { selectRow(st, ev.activate); return [{ name: 'enter' }] }
|
|
162
|
+
if (typeof ev.submit === 'string') {
|
|
163
|
+
if (st.input) st.input.value = ev.submit
|
|
164
|
+
return [{ name: 'enter' }]
|
|
165
|
+
}
|
|
166
|
+
if (typeof ev.confirm === 'boolean') return [{ name: 'char', ch: ev.confirm ? 'y' : 'n' }]
|
|
167
|
+
return []
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let relaunch = false
|
|
171
|
+
let inSession = false
|
|
172
|
+
try {
|
|
173
|
+
while (true) {
|
|
174
|
+
if (!vc.daemonAlive()) {
|
|
175
|
+
// SIN SERVICIO: la pantalla de arrancarlo. Si se cae a mitad de sesión, se vuelve
|
|
176
|
+
// aquí en vez de dejar una ventana que contesta errores a todo.
|
|
177
|
+
st.down = true
|
|
178
|
+
sendView()
|
|
179
|
+
const ev = await next(3000)
|
|
180
|
+
if (ev.quit) break
|
|
181
|
+
if (ev.key?.name === 'tick') continue
|
|
182
|
+
const keys = toKeys(ev)
|
|
183
|
+
let r = null
|
|
184
|
+
for (const k of keys) r = await onKeyDown(st, k, sendView)
|
|
185
|
+
if (r === false) break
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
if (st.down || !inSession) {
|
|
189
|
+
st.down = false
|
|
190
|
+
await startSession(term, st)
|
|
191
|
+
inSession = true
|
|
192
|
+
}
|
|
193
|
+
await beforeRender(term, st)
|
|
194
|
+
sendView()
|
|
195
|
+
const ev = await next(tickFor(st) || 2000)
|
|
196
|
+
if (ev.quit) break
|
|
197
|
+
if (ev.tab) { await openTab(term, st, ev.tab); continue }
|
|
198
|
+
let running = true
|
|
199
|
+
for (const k of toKeys(ev)) {
|
|
200
|
+
// Un tick sin pantalla que lo espere no hace nada; solo sirve para repintar.
|
|
201
|
+
running = await handleKey(term, st, k)
|
|
202
|
+
if (!running) break
|
|
203
|
+
}
|
|
204
|
+
if (!running) { relaunch = !!st.relaunch; break }
|
|
205
|
+
}
|
|
206
|
+
} finally {
|
|
207
|
+
await endSession(st)
|
|
208
|
+
send({ t: 'bye', relaunch })
|
|
209
|
+
// La entrada sigue abierta si quien salió fue la propia sesión (`q`, o tras actualizar):
|
|
210
|
+
// sin soltarla, el proceso se quedaba vivo esperando líneas que ya nadie iba a leer.
|
|
211
|
+
input.pause()
|
|
212
|
+
input.destroy?.()
|
|
213
|
+
}
|
|
214
|
+
}
|
package/src/tui/i18n.js
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
import path from 'node:path'
|
|
16
16
|
import { dataDir, readJson, writeJson } from '../paths.js'
|
|
17
17
|
|
|
18
|
+
// Solo Linux tiene la unidad de systemd: en Windows y macOS nombrarla manda a un comando que no existe.
|
|
19
|
+
const LINUX = process.platform === 'linux'
|
|
20
|
+
|
|
18
21
|
// --------------------------------- español ---------------------------------
|
|
19
22
|
|
|
20
23
|
const es = {
|
|
@@ -37,7 +40,7 @@ const es = {
|
|
|
37
40
|
updateRestartOffer: (version) => `Instalada la ${version}. ¿Reiniciar ahora la bóveda y esta pantalla? Cierra las bóvedas abiertas`,
|
|
38
41
|
updateRestarting: 'Reiniciando la bóveda…',
|
|
39
42
|
updateRestartLater: (version) => `La ${version} queda instalada: la bóveda se reinicia sola en un par de minutos`,
|
|
40
|
-
daemonStale: (running, installed) => `⚠ el servicio corre ${running} y el binario instalado es ${installed}: reinícialo (systemctl --user restart dotrino-vault)`,
|
|
43
|
+
daemonStale: (running, installed) => `⚠ el servicio corre ${running} y el binario instalado es ${installed}: reinícialo${LINUX ? ' (systemctl --user restart dotrino-vault)' : ''}`,
|
|
41
44
|
activeVault: 'Bóveda activa: ',
|
|
42
45
|
noName: '(sin nombre)',
|
|
43
46
|
tooSmall: 'Terminal muy pequeño',
|
|
@@ -190,6 +193,14 @@ const es = {
|
|
|
190
193
|
settingUpdateNowAsks: 'Hecho: pide aprobación antes de actualizarse.',
|
|
191
194
|
settingUpdateNowAlone: 'Hecho: se actualiza sola.',
|
|
192
195
|
settingSaveFailed: (m) => `No se pudo guardar el ajuste: ${m}`,
|
|
196
|
+
settingAutostartOn: 'La bóveda arranca sola al iniciar sesión en esta computadora.',
|
|
197
|
+
settingAutostartOff: 'La bóveda no arranca sola: hay que abrirla cada vez.',
|
|
198
|
+
settingAutostartUnknown: 'No se sabe si la bóveda arranca sola al iniciar sesión.',
|
|
199
|
+
settingAutostartHint: 'Sin ella encendida, tus aparatos no pueden pedirle nada a esta bóveda.',
|
|
200
|
+
settingAutostartNoUnit: 'Para que arranque sola, instálala con el paquete de tu sistema (.deb o .rpm).',
|
|
201
|
+
settingAutostartSaving: 'Guardando…',
|
|
202
|
+
settingAutostartNowOn: 'Hecho: arranca sola al iniciar sesión.',
|
|
203
|
+
settingAutostartNowOff: 'Hecho: ya no arranca sola.',
|
|
193
204
|
doorsIntro: (name) => `Con qué se abre «${name}». Abre cualquiera de estas.`,
|
|
194
205
|
doorsNone: 'Sin candado: se abre sola en esta máquina.',
|
|
195
206
|
doorPassword: 'contraseña',
|
|
@@ -480,7 +491,7 @@ const es = {
|
|
|
480
491
|
|
|
481
492
|
// pantalla "daemon caído"
|
|
482
493
|
downTitle: 'El daemon del vault no está corriendo.',
|
|
483
|
-
downBody1: '
|
|
494
|
+
downBody1: 'Esta pantalla le da órdenes al servicio de la bóveda (el que guarda tu llave). Sin él no puede',
|
|
484
495
|
downBody2: 'crear bóvedas, listar dispositivos ni tocar secretos.',
|
|
485
496
|
downStart: ' intentar arrancarlo: ',
|
|
486
497
|
downRecheck: ' volver a comprobar',
|
|
@@ -496,7 +507,7 @@ const es = {
|
|
|
496
507
|
startFailed: (err) => `No se pudo arrancar: ${err}`,
|
|
497
508
|
|
|
498
509
|
// errores
|
|
499
|
-
errDaemonDown: 'El daemon no está corriendo. Arráncalo: systemctl --user start dotrino-vault (o reinicia la TUI).',
|
|
510
|
+
errDaemonDown: LINUX ? 'El daemon no está corriendo. Arráncalo: systemctl --user start dotrino-vault (o reinicia la TUI).' : 'El servicio de la bóveda no está corriendo. Cierra esta pantalla y vuelve a abrirla para arrancarlo.',
|
|
500
511
|
errNoReply: 'El daemon no respondió.',
|
|
501
512
|
errNotApplied: 'El daemon no aplicó el cambio (revisa los logs del servicio).',
|
|
502
513
|
errNotDeleted: 'El daemon no borró la variable (revisa los logs del servicio).',
|
|
@@ -534,7 +545,7 @@ const en = {
|
|
|
534
545
|
updateRestartOffer: (version) => `${version} is installed. Restart the vault and this screen now? It locks the open vaults`,
|
|
535
546
|
updateRestarting: 'Restarting the vault…',
|
|
536
547
|
updateRestartLater: (version) => `${version} is installed: the vault restarts by itself in a couple of minutes`,
|
|
537
|
-
daemonStale: (running, installed) => `⚠ the service runs ${running} but the installed binary is ${installed}: restart it (systemctl --user restart dotrino-vault)`,
|
|
548
|
+
daemonStale: (running, installed) => `⚠ the service runs ${running} but the installed binary is ${installed}: restart it${LINUX ? ' (systemctl --user restart dotrino-vault)' : ''}`,
|
|
538
549
|
activeVault: 'Active vault: ',
|
|
539
550
|
tooSmall: 'Terminal too small',
|
|
540
551
|
tooSmallHint: (cols, rows) => `Resize to ≥ 24×9 (now ${cols}×${rows}).`,
|
|
@@ -667,6 +678,14 @@ const en = {
|
|
|
667
678
|
settingUpdateNowAsks: 'Done: it asks for approval before updating.',
|
|
668
679
|
settingUpdateNowAlone: 'Done: it updates on its own.',
|
|
669
680
|
settingSaveFailed: (m) => `The setting could not be saved: ${m}`,
|
|
681
|
+
settingAutostartOn: 'The vault starts on its own when you sign in to this computer.',
|
|
682
|
+
settingAutostartOff: 'The vault does not start on its own: you have to open it every time.',
|
|
683
|
+
settingAutostartUnknown: 'It is not known whether the vault starts on its own when you sign in.',
|
|
684
|
+
settingAutostartHint: 'While it is off, your devices cannot ask this vault for anything.',
|
|
685
|
+
settingAutostartNoUnit: 'To have it start on its own, install it with your system package (.deb or .rpm).',
|
|
686
|
+
settingAutostartSaving: 'Saving…',
|
|
687
|
+
settingAutostartNowOn: 'Done: it starts on its own when you sign in.',
|
|
688
|
+
settingAutostartNowOff: 'Done: it no longer starts on its own.',
|
|
670
689
|
doorsIntro: (name) => `What opens “${name}”. Any of these opens it.`,
|
|
671
690
|
doorsNone: 'No lock: it opens by itself on this machine.',
|
|
672
691
|
doorPassword: 'password',
|
|
@@ -926,7 +945,7 @@ const en = {
|
|
|
926
945
|
helpJoin: ['Esc vaults', 'l Español', 'q quit'],
|
|
927
946
|
|
|
928
947
|
downTitle: 'The vault daemon is not running.',
|
|
929
|
-
downBody1: '
|
|
948
|
+
downBody1: 'This screen gives orders to the vault service (the keeper of your key). Without it',
|
|
930
949
|
downBody2: 'it cannot create vaults, list devices or touch secrets.',
|
|
931
950
|
downStart: ' try to start it: ',
|
|
932
951
|
downRecheck: ' check again',
|
|
@@ -941,7 +960,7 @@ const en = {
|
|
|
941
960
|
startedNotReady: 'It started but does not answer yet; press R',
|
|
942
961
|
startFailed: (err) => `Could not start it: ${err}`,
|
|
943
962
|
|
|
944
|
-
errDaemonDown: 'The daemon is not running. Start it: systemctl --user start dotrino-vault (or restart the TUI).',
|
|
963
|
+
errDaemonDown: LINUX ? 'The daemon is not running. Start it: systemctl --user start dotrino-vault (or restart the TUI).' : 'The vault service is not running. Close this screen and open it again to start it.',
|
|
945
964
|
errNoReply: 'The daemon did not answer.',
|
|
946
965
|
errNotApplied: 'The daemon did not apply the change (check the service logs).',
|
|
947
966
|
errNotDeleted: 'The daemon did not delete the variable (check the service logs).',
|
package/src/tui/term.js
CHANGED
|
@@ -87,8 +87,13 @@ export function padEnd (s, width) {
|
|
|
87
87
|
|
|
88
88
|
// ------------------------------- tema/estilos -------------------------------
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
/**
|
|
91
|
+
* `color: true` fuerza los estilos aunque no haya terminal: lo usa el puente de la ventana
|
|
92
|
+
* (`gui.js`), que convierte esos mismos escapes en tramos con estilo. Así las filas que
|
|
93
|
+
* pinta la TUI son, carácter a carácter, las que pinta la ventana.
|
|
94
|
+
*/
|
|
95
|
+
export function makeTheme ({ color } = {}) {
|
|
96
|
+
const on = color ?? supportsColor()
|
|
92
97
|
const sgr = (...c) => (on ? CSI + c.join(';') + 'm' : '')
|
|
93
98
|
const R = on ? CSI + '0m' : ''
|
|
94
99
|
const wrap = (open) => (s) => on ? open + s + R : s
|
package/src/update.js
CHANGED
|
@@ -124,8 +124,12 @@ async function instalarDesde (work, pick, release, { binDir, log, fetchVerifiedI
|
|
|
124
124
|
const src = fs.readdirSync(work).map((d) => path.join(work, d)).find((d) => fs.existsSync(path.join(d, 'dotrino-vaultd')))
|
|
125
125
|
if (!src) return { ok: false, code: 'BAD_TARBALL', reason: 'the tarball has no dotrino-vaultd inside' }
|
|
126
126
|
fs.mkdirSync(binDir, { recursive: true })
|
|
127
|
+
// LA VENTANA se pone al día con la bóveda, pero solo donde ya estaba: una bóveda de
|
|
128
|
+
// servidor no tiene por qué recibir un programa de escritorio que nadie instaló.
|
|
129
|
+
const names = ['dotrino-vault', 'dotrino-vaultd']
|
|
130
|
+
if (fs.existsSync(path.join(binDir, 'dotrino-vault-desktop')) && fs.existsSync(path.join(src, 'dotrino-vault-desktop'))) names.unshift('dotrino-vault-desktop')
|
|
127
131
|
// El CLI primero y el daemon al final: cambiar el daemon es lo que dispara el reinicio.
|
|
128
|
-
for (const name of
|
|
132
|
+
for (const name of names) {
|
|
129
133
|
const tmp = path.join(binDir, `.${name}.new`)
|
|
130
134
|
fs.copyFileSync(path.join(src, name), tmp)
|
|
131
135
|
fs.chmodSync(tmp, 0o755)
|