@dotrino/vault 0.45.0 → 0.46.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/bin/dotrino-env.js +58 -19
- package/package.json +1 -1
- package/src/admin.js +16 -3
- package/src/env.js +54 -11
package/bin/dotrino-env.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* dotrino-env — CLI del "dotenv contra el vault".
|
|
4
4
|
*
|
|
5
|
-
* dotrino-env enroll --ns <ns> [--qr <
|
|
5
|
+
* dotrino-env enroll --profile <p> --ns <ns> [--qr <inv>] enrola ESTA máquina (una vez)
|
|
6
6
|
* dotrino-env status qué hay enrolado aquí
|
|
7
7
|
* dotrino-env check [--ns <ns>] pide los secretos y lista sus NOMBRES (nunca valores)
|
|
8
8
|
* dotrino-env run [--ns <ns>] -- <cmd> [args…] corre un comando con los secretos en su entorno
|
|
9
9
|
*
|
|
10
10
|
* El enrolamiento es el registro del cliente contra el vault del dueño:
|
|
11
11
|
* 1. en el vault: dotrino-vault pair --service <ns> (invitación con scope SOLO vault:secrets:<ns>)
|
|
12
|
-
* 2. aquí: dotrino-env enroll --ns <ns>
|
|
12
|
+
* 2. aquí: dotrino-env enroll --profile <p> --ns <ns> (pegas la invitación; se MUESTRA un código)
|
|
13
13
|
* 3. en el vault: dotrino-vault approve <código> (lo tipeas leyéndolo de esta pantalla)
|
|
14
14
|
*/
|
|
15
15
|
import fs from 'node:fs'
|
|
@@ -17,7 +17,7 @@ import path from 'node:path'
|
|
|
17
17
|
import readline from 'node:readline/promises'
|
|
18
18
|
import { spawn } from 'node:child_process'
|
|
19
19
|
import { enrollService, readServiceIdentity, fetchSecrets } from '../src/service.js'
|
|
20
|
-
import { loadEnv, serviceDir, serviceRoot, listEnrolled, resolveNs } from '../src/env.js'
|
|
20
|
+
import { loadEnv, serviceDir, serviceRoot, listEnrolled, resolveNs, resolveEnvProfile, isValidEnvProfile } from '../src/env.js'
|
|
21
21
|
// Se usaba sin importarlo: `enroll` —el comando principal— moría con
|
|
22
22
|
// «sharedParseInvite is not defined» en cuanto tocaba una invitación. Nunca
|
|
23
23
|
// llegó a funcionar, y por eso el único servicio enrolado del ecosistema (el
|
|
@@ -30,14 +30,14 @@ const flag = (name) => { const i = argv.indexOf('--' + name); return i >= 0 ? ar
|
|
|
30
30
|
function help () {
|
|
31
31
|
console.log(`dotrino-env — credenciales del vault en vez del .env
|
|
32
32
|
|
|
33
|
-
enroll --ns <ns> [--code <invitación>] [--dir <dir>]
|
|
33
|
+
enroll --profile <perfil> --ns <ns> [--code <invitación>] [--dir <dir>]
|
|
34
34
|
Registra ESTE servicio contra el vault (una sola vez).
|
|
35
35
|
Antes, en el vault: dotrino-vault pair --service <ns>
|
|
36
36
|
Si no pasas --code (alias --qr), se pide por consola (también acepta stdin).
|
|
37
37
|
|
|
38
38
|
status servicios enrolados en esta máquina
|
|
39
39
|
check [--ns <ns>] pide los secretos al vault y lista sus NOMBRES (nunca los valores)
|
|
40
|
-
run [--ns <ns>] -- <cmd> [args…]
|
|
40
|
+
run --profile <perfil> [--ns <ns>] -- <cmd> [args…]
|
|
41
41
|
ssh-agent [--ns <ns>] [--socket <ruta>]
|
|
42
42
|
agente SSH con las llaves (SSH_KEY_* del cajón) solo en memoria;
|
|
43
43
|
pide el cajón al arrancar. Pon SSH_AUTH_SOCK en la ruta que imprime.
|
|
@@ -50,7 +50,7 @@ En tu código:
|
|
|
50
50
|
El vault MANDA: sus valores pisan los del .env y los del entorno. Para una
|
|
51
51
|
corrida suelta sin que pise nada: DOTRINO_ENV_OVERRIDE=0
|
|
52
52
|
|
|
53
|
-
Entorno: DOTRINO_NS · DOTRINO_ENV_DIR · DOTRINO_ENV_HOME · DOTRINO_ENV_QUIET
|
|
53
|
+
Entorno: DOTRINO_NS · DOTRINO_ENV_PROFILE · DOTRINO_ENV_DIR · DOTRINO_ENV_HOME · DOTRINO_ENV_QUIET
|
|
54
54
|
DOTRINO_ENV_OVERRIDE`)
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -73,10 +73,31 @@ async function readInvite () {
|
|
|
73
73
|
return answer
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* El PERFIL LOCAL, obligatorio. Es una etiqueta de ESTA máquina para agrupar lo que
|
|
78
|
+
* pertenece a una bóveda: `~/.dotrino/service/<perfil>/<ns>`. No viaja, no entra en el
|
|
79
|
+
* acta y no se compara con el perfil de la bóveda — solo evita que dos bóvedas con un
|
|
80
|
+
* cajón del mismo nombre se pisen el directorio, que antes dejaba al primero inservible.
|
|
81
|
+
*/
|
|
82
|
+
function perfilLocal () {
|
|
83
|
+
const p = resolveEnvProfile(flag('profile'))
|
|
84
|
+
if (!p) {
|
|
85
|
+
console.error('falta --profile <nombre> (o DOTRINO_ENV_PROFILE)')
|
|
86
|
+
console.error('')
|
|
87
|
+
console.error(' Es una ETIQUETA TUYA para agrupar lo de una bóveda, no el perfil del vault:')
|
|
88
|
+
console.error(' ~/.dotrino/service/<perfil>/<ns>')
|
|
89
|
+
console.error(' Sirve para que dos bóvedas puedan tener un cajón llamado igual.')
|
|
90
|
+
process.exit(2)
|
|
91
|
+
}
|
|
92
|
+
if (!isValidEnvProfile(p)) { console.error('perfil inválido: usa [a-z0-9-]{1,32}'); process.exit(2) }
|
|
93
|
+
return p
|
|
94
|
+
}
|
|
95
|
+
|
|
76
96
|
async function cmdEnroll () {
|
|
77
97
|
const ns = flag('ns')
|
|
78
98
|
if (!ns) { console.error('falta --ns <ns> (el mismo del `dotrino-vault pair --service <ns>`)'); process.exit(2) }
|
|
79
|
-
const
|
|
99
|
+
const perfil = perfilLocal()
|
|
100
|
+
const dir = flag('dir') || serviceDir(perfil, ns)
|
|
80
101
|
const qr = parseInvite((flag('qr') || flag('code')) || await readInvite())
|
|
81
102
|
|
|
82
103
|
console.log('\nEnrolando el servicio "%s" contra el vault…', ns)
|
|
@@ -104,22 +125,40 @@ async function cmdEnroll () {
|
|
|
104
125
|
}
|
|
105
126
|
})
|
|
106
127
|
console.log('\nListo. Identidad del servicio en: %s', path.join(dir, 'service-identity.json'))
|
|
107
|
-
|
|
128
|
+
// El papel ya no vence: vale mientras el acta lo diga. Decir «vence» era describir un
|
|
129
|
+
// reloj que ya no existe, y encima REVENTABA —`new Date(undefined).toISOString()` lanza
|
|
130
|
+
// RangeError—, o sea que el comando de diagnóstico se caía justo cuando lo necesitas.
|
|
131
|
+
console.log('Certificado con scope: %s (%s)', (cert.scope || []).join(', '),
|
|
132
|
+
typeof cert.seq === 'number' ? `acta #${cert.seq}` : `del modelo viejo, vence ${new Date(cert.exp).toISOString().slice(0, 10)}`)
|
|
108
133
|
console.log('\nEn tu app: import \'@dotrino/vault/config\' (con DOTRINO_NS=%s)', ns)
|
|
109
134
|
}
|
|
110
135
|
|
|
111
136
|
function cmdStatus () {
|
|
112
137
|
const found = listEnrolled()
|
|
113
138
|
if (!found.length) {
|
|
114
|
-
console.log('Ningún servicio enrolado en %s\n Enrola uno: dotrino-env enroll --ns <ns>', serviceRoot())
|
|
139
|
+
console.log('Ningún servicio enrolado en %s\n Enrola uno: dotrino-env enroll --profile <perfil> --ns <ns>', serviceRoot())
|
|
115
140
|
return
|
|
116
141
|
}
|
|
117
|
-
for (const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
142
|
+
for (const e of found) {
|
|
143
|
+
// LO QUE QUEDÓ EN EL FORMATO VIEJO se dice y no se usa. Repescarlo en silencio sería
|
|
144
|
+
// fingir que la migración no hace falta; callarlo dejaría un servicio «desaparecido».
|
|
145
|
+
if (e.legacy) {
|
|
146
|
+
const viejo = path.join(serviceRoot(), e.ns)
|
|
147
|
+
console.log('%s ⚠ del formato anterior, sin perfil — no se usa', e.ns)
|
|
148
|
+
console.log(' muévelo: mkdir -p %s/<perfil> && mv %s %s/<perfil>/', serviceRoot(), viejo, serviceRoot())
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
const dir = serviceDir(e.profile, e.ns)
|
|
152
|
+
const id = readServiceIdentity(dir)
|
|
153
|
+
const c = id?.cert
|
|
154
|
+
// El papel se describe por el ACTA a la que se ató. Uno del modelo viejo se dice tal
|
|
155
|
+
// cual, con su fecha, porque ESO sí importa: es lo único que todavía caduca, y sabrá
|
|
156
|
+
// el operador que le queda una migración por hacer.
|
|
157
|
+
const papel = typeof c?.seq === 'number'
|
|
158
|
+
? `acta #${c.seq}`
|
|
159
|
+
: (typeof c?.exp === 'number' ? `modelo viejo · vence ${new Date(c.exp).toISOString().slice(0, 10)}` : '?')
|
|
160
|
+
console.log('%s/%s\n dir: %s\n vault: %s…\n scope: %s\n cert: %s',
|
|
161
|
+
e.profile, e.ns, dir, String(id.iss).slice(0, 24), (c?.scope || []).join(', '), papel)
|
|
123
162
|
}
|
|
124
163
|
}
|
|
125
164
|
|
|
@@ -128,7 +167,7 @@ async function cmdCheck () {
|
|
|
128
167
|
// `fetchSecrets` y no `loadEnv`: listar NO debe tener efectos secundarios. Con
|
|
129
168
|
// `loadEnv` esto inyectaría el bundle en el entorno del propio `check`, que es
|
|
130
169
|
// justo lo que un comando de diagnóstico no tiene por qué hacer.
|
|
131
|
-
const secrets = await fetchSecrets({ dir: serviceDir(ns), ns })
|
|
170
|
+
const secrets = await fetchSecrets({ dir: serviceDir(perfilLocal(), ns), ns })
|
|
132
171
|
const keys = Object.keys(secrets)
|
|
133
172
|
console.log('ns "%s": %d secreto(s)%s', ns, keys.length, keys.length ? ':' : '')
|
|
134
173
|
for (const k of keys) console.log(' ' + k) // NUNCA los valores
|
|
@@ -142,9 +181,9 @@ async function cmdCheck () {
|
|
|
142
181
|
async function cmdRun () {
|
|
143
182
|
const sep = argv.indexOf('--')
|
|
144
183
|
const cmd = sep >= 0 ? argv.slice(sep + 1) : []
|
|
145
|
-
if (!cmd.length) { console.error('uso: dotrino-env run [--ns <ns>] -- <cmd> [args…]'); process.exit(2) }
|
|
184
|
+
if (!cmd.length) { console.error('uso: dotrino-env run --profile <perfil> [--ns <ns>] -- <cmd> [args…]'); process.exit(2) }
|
|
146
185
|
const onPending = () => console.error('[dotrino-env] waiting for approval on your phone…')
|
|
147
|
-
const { injected, overridden } = await loadEnv({ ns: flag('ns'), onPending })
|
|
186
|
+
const { injected, overridden } = await loadEnv({ ns: flag('ns'), profile: perfilLocal(), onPending })
|
|
148
187
|
console.error('[dotrino-env] %d valor(es) en el entorno de %s%s', injected.length, cmd[0],
|
|
149
188
|
overridden.length ? ` (pisados: ${overridden.join(', ')})` : '')
|
|
150
189
|
const child = spawn(cmd[0], cmd.slice(1), { stdio: 'inherit', env: process.env })
|
|
@@ -166,7 +205,7 @@ async function cmdSshAgent () {
|
|
|
166
205
|
const { serviceDir, resolveNs } = await import('../src/env.js')
|
|
167
206
|
const { waitForSecrets } = await import('../src/service.js')
|
|
168
207
|
const ns = resolveNs(flag('ns') || undefined)
|
|
169
|
-
const dir = flag('dir') || serviceDir(ns)
|
|
208
|
+
const dir = flag('dir') || serviceDir(perfilLocal(), ns)
|
|
170
209
|
const socketPath = flag('socket') || defaultSocketPath(dir)
|
|
171
210
|
console.error('[dotrino-env] asking the vault for the keys of «%s»…', ns)
|
|
172
211
|
const secrets = await waitForSecrets({ dir, ns, onPending: () => console.error('[dotrino-env] waiting for approval on your phone…') })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dotrino/vault",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.0",
|
|
4
4
|
"description": "Usa ESTE dispositivo (navegador) como bóveda/CA del ecosistema Dotrino: atiende enrolamientos por el proxy y firma certificados de delegación a tus máquinas. Incluye el cliente de SERVICIO (Node): un proyecto se enrola una vez y jala sus credenciales del vault en vez del .env (`import '@dotrino/vault/config'`).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/admin.js
CHANGED
|
@@ -60,7 +60,20 @@ export const ADMIN_OPS = Object.freeze([
|
|
|
60
60
|
])
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
*
|
|
63
|
+
* QUÉ SE ATIENDE CON EL CANDADO ECHADO.
|
|
64
|
+
*
|
|
65
|
+
* No es «solo mirar», y confundirlo fue un error: **guardar una variable NO necesita la
|
|
66
|
+
* maestra**. Sellar un valor usa las llaves PÚBLICAS de quien va a leerlo, así que la
|
|
67
|
+
* bóveda puede hacerlo sin ningún secreto suyo (`docs/secretos-sellados.md` §8.1) — es
|
|
68
|
+
* justo lo que hace barata la rotación. Meterlo en el mismo saco que revocar dejaba la
|
|
69
|
+
* consola sin poder configurar nada con el perfil cerrado, que es su trabajo diario.
|
|
70
|
+
*
|
|
71
|
+
* Lo que SÍ se queda fuera es `revoke`: reescribe el acta, y sellar el acta es de la
|
|
72
|
+
* maestra. Cerrada no hay con qué.
|
|
73
|
+
*
|
|
74
|
+
* (Si el cajón tiene su llave sellada con la frase, repartirla a los miembros puede quedar
|
|
75
|
+
* a deber hasta que alguien abra: eso ya lo dice la bóveda y se salda solo. Guardar el
|
|
76
|
+
* valor, que es lo que se pidió, se hace igual.)
|
|
64
77
|
*
|
|
65
78
|
* El candado ES de la consola —los aparatos ya emparejados siguen leyendo, guardando y
|
|
66
79
|
* firmando con SU llave— así que cerrar el perfil tiene que cerrar exactamente esto. No
|
|
@@ -72,7 +85,7 @@ export const ADMIN_OPS = Object.freeze([
|
|
|
72
85
|
* Se dejan las de LEER a propósito: si se cortaran todas, la consola no podría ni
|
|
73
86
|
* enterarse de por qué no funciona y parecería rota. Así puede decir «está cerrada».
|
|
74
87
|
*/
|
|
75
|
-
export const ADMIN_OPS_WHILE_LOCKED = Object.freeze(['audit', 'vars'])
|
|
88
|
+
export const ADMIN_OPS_WHILE_LOCKED = Object.freeze(['audit', 'vars', 'var.set', 'var.setMany'])
|
|
76
89
|
|
|
77
90
|
/** Cuánto se recuerda un nonce ya usado (el doble de la ventana de frescura). */
|
|
78
91
|
export const ADMIN_NONCE_TTL_MS = 10 * 60 * 1000
|
|
@@ -136,7 +149,7 @@ export function createAdminDesk ({
|
|
|
136
149
|
}
|
|
137
150
|
const by = await deviceIdOf(chk.device).catch(() => null)
|
|
138
151
|
|
|
139
|
-
// CERRADA NO SE
|
|
152
|
+
// CERRADA NO SE SELLA EL ACTA. Va después de autorizar (el candado es estado del perfil,
|
|
140
153
|
// y a un desconocido no se le cuenta) y ANTES de quemar el nonce: quien reintente
|
|
141
154
|
// tras abrir la bóveda no tiene por qué generar otro.
|
|
142
155
|
//
|
package/src/env.js
CHANGED
|
@@ -29,18 +29,59 @@ export function serviceRoot () {
|
|
|
29
29
|
return process.env.DOTRINO_ENV_HOME || path.join(os.homedir(), '.dotrino', 'service')
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
32
|
+
/**
|
|
33
|
+
* EL PERFIL LOCAL: `~/.dotrino/service/<perfil>/<ns>`.
|
|
34
|
+
*
|
|
35
|
+
* Es una ETIQUETA TUYA, de esta máquina, y NO tiene nada que ver con el perfil de la
|
|
36
|
+
* bóveda: nadie la manda por la red, no entra en el acta y no se compara con nada. Sirve
|
|
37
|
+
* solo para que dos bóvedas puedan tener un cajón que se llame igual.
|
|
38
|
+
*
|
|
39
|
+
* Antes la ruta era `<raíz>/<ns>` y el nombre del cajón era una CLAVE GLOBAL del usuario:
|
|
40
|
+
* si dos bóvedas usaban `aws`, el segundo enrolamiento pisaba al primero y lo dejaba
|
|
41
|
+
* inservible —avisaba, pero el papel del primero seguía existiendo en su bóveda sin
|
|
42
|
+
* identidad en disco que lo usara—. Obligar a nombrar el perfil quita esa colisión de
|
|
43
|
+
* raíz en vez de pedirle al usuario que se acuerde de no repetir nombres.
|
|
44
|
+
*
|
|
45
|
+
* Mismo alfabeto que el `ns` porque es lo mismo: un segmento de ruta.
|
|
46
|
+
*/
|
|
47
|
+
export const isValidEnvProfile = (p) => typeof p === 'string' && /^[a-z0-9-]{1,32}$/.test(p)
|
|
48
|
+
|
|
49
|
+
/** Directorio de la identidad del servicio `ns` en el perfil local `profile`. */
|
|
50
|
+
export function serviceDir (profile, ns) {
|
|
34
51
|
if (process.env.DOTRINO_ENV_DIR) return process.env.DOTRINO_ENV_DIR
|
|
52
|
+
if (!isValidEnvProfile(profile)) {
|
|
53
|
+
throw new Error('missing local profile: pass --profile <nombre> (use [a-z0-9-]{1,32}). It is a label of YOUR machine, not the vault profile')
|
|
54
|
+
}
|
|
35
55
|
if (!isValidSecretsNs(ns)) throw new Error('invalid ns (use [a-z0-9-]{1,32}, e.g. "myapp")')
|
|
36
|
-
return path.join(serviceRoot(), ns)
|
|
56
|
+
return path.join(serviceRoot(), profile, ns)
|
|
37
57
|
}
|
|
38
58
|
|
|
39
|
-
/**
|
|
59
|
+
/** El perfil local por defecto: `--profile`, o `DOTRINO_ENV_PROFILE`. */
|
|
60
|
+
export const resolveEnvProfile = (v) => v || process.env.DOTRINO_ENV_PROFILE || null
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Lo enrolado en esta máquina: `[{ profile, ns }]`. Recorre DOS niveles.
|
|
64
|
+
*
|
|
65
|
+
* Y avisa de lo que quedó en el nivel de antes (`<raíz>/<ns>`): no se lee ni se usa —
|
|
66
|
+
* repescarlo en silencio sería fingir que la migración no hace falta— pero callarlo
|
|
67
|
+
* dejaría al dueño con un servicio que «desapareció» sin explicación.
|
|
68
|
+
*/
|
|
40
69
|
export function listEnrolled () {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
70
|
+
const out = []
|
|
71
|
+
let dirs = []
|
|
72
|
+
try { dirs = fs.readdirSync(serviceRoot(), { withFileTypes: true }) } catch (_) { return out }
|
|
73
|
+
for (const d of dirs) {
|
|
74
|
+
if (!d.isDirectory() || !isValidEnvProfile(d.name)) continue
|
|
75
|
+
// Un enrolamiento del formato viejo es un directorio con `service-identity.json`
|
|
76
|
+
// DENTRO; uno del nuevo tiene subdirectorios. Así se distinguen sin adivinar.
|
|
77
|
+
if (readServiceIdentity(path.join(serviceRoot(), d.name))) { out.push({ legacy: true, ns: d.name }); continue }
|
|
78
|
+
let hijos = []
|
|
79
|
+
try { hijos = fs.readdirSync(path.join(serviceRoot(), d.name)) } catch (_) { continue }
|
|
80
|
+
for (const ns of hijos) {
|
|
81
|
+
if (isValidSecretsNs(ns) && readServiceIdentity(path.join(serviceRoot(), d.name, ns))) out.push({ profile: d.name, ns })
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out
|
|
44
85
|
}
|
|
45
86
|
|
|
46
87
|
/**
|
|
@@ -98,10 +139,12 @@ let lastApplied = null
|
|
|
98
139
|
* `overridden` = las que YA tenían otro valor en el entorno y el vault pisó. Es
|
|
99
140
|
* el dato que delata un `.env` rancio, así que se reporta en vez de callarse.
|
|
100
141
|
*/
|
|
101
|
-
export async function loadEnv ({ ns, dir, override, wait = true, required = [], onRetry, onPending } = {}) {
|
|
142
|
+
export async function loadEnv ({ ns, profile, dir, override, wait = true, required = [], onRetry, onPending } = {}) {
|
|
102
143
|
if (override === undefined) override = overrideByDefault()
|
|
103
144
|
ns = resolveNs(ns)
|
|
104
|
-
|
|
145
|
+
// El PERFIL LOCAL es obligatorio salvo que se diga el directorio a pelo: es lo que hace
|
|
146
|
+
// que dos bóvedas puedan tener un cajón con el mismo nombre sin pisarse.
|
|
147
|
+
dir = dir || serviceDir(resolveEnvProfile(profile), ns)
|
|
105
148
|
const load = wait ? waitForSecrets : fetchSecrets
|
|
106
149
|
const secrets = await load({ dir, ns, onRetry, onPending })
|
|
107
150
|
|
|
@@ -199,9 +242,9 @@ export function applyEnv (secrets, override = overrideByDefault()) {
|
|
|
199
242
|
* también. Se elige 0 porque salir a propósito no es un fallo.
|
|
200
243
|
* @returns {Promise<{stop:()=>void}>}
|
|
201
244
|
*/
|
|
202
|
-
export async function watchEnv ({ ns, dir, applied = lastApplied ?? undefined, onUpdate, exitCode = 0, quiet = false, ...rest } = {}) {
|
|
245
|
+
export async function watchEnv ({ ns, profile, dir, applied = lastApplied ?? undefined, onUpdate, exitCode = 0, quiet = false, ...rest } = {}) {
|
|
203
246
|
ns = resolveNs(ns)
|
|
204
|
-
dir = dir || serviceDir(ns)
|
|
247
|
+
dir = dir || serviceDir(resolveEnvProfile(profile), ns)
|
|
205
248
|
const say = (m) => { if (!quiet) console.error(m) }
|
|
206
249
|
|
|
207
250
|
const exitNow = (reason) => {
|