komplian 0.5.3 → 0.6.1

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 CHANGED
@@ -6,24 +6,22 @@
6
6
  2. Browser login: `gh auth login -h github.com -s repo -s read:org -w`
7
7
  3. `npx komplian onboard --yes` (sin `@versión`: usa `latest` del registry público).
8
8
 
9
- **`npm ERR! ETARGET` / “No matching version”** (también si falló **`postman`**, no solo `onboard`): es el mismo paquete npm. Usa **`npx komplian postman login`** / **`npx komplian postman --yes`** **sin** `@0.4.x`; comprueba `npm config get registry` → `https://registry.npmjs.org/`; `npm cache clean --force`. Detalle: `ONBOARDING.md` en el monorepo.
9
+ **Orden típico del equipo:** onboard **`postman --yes`** (pide la clave la primera vez en TTY) `mcp-tools --yes` **`db:all:dev`** → `localhost --yes`. Ver **`ONBOARDING.md`**.
10
+
11
+ **`npm ERR! ETARGET` / “No matching version”** (también si falló **`postman`**, no solo `onboard`): es el mismo paquete npm. Usa **`npx komplian postman --yes`** **sin** `@0.4.x`; comprueba `npm config get registry` → `https://registry.npmjs.org/`; `npm cache clean --force`. Detalle: `ONBOARDING.md` en el monorepo.
10
12
 
11
13
  ### Postman (colección + entornos)
12
14
 
13
15
  1. En [Postman](https://postman.com) usa una cuenta con email **`@komplian.com`** (o añade ese email a tu perfil).
14
16
  2. Crea una **API key**: Settings → **API keys** → Generate.
15
- 3. **Una vez por máquina** (guarda la clave en `~/.komplian/postman-api-key`; no hace falta `export` después):
16
-
17
- ```bash
18
- npx komplian postman login
19
- ```
20
-
21
- 4. Cuando quieras sincronizar la colección:
17
+ 3. **Sincronizar** (la primera vez en terminal interactiva **te pide** la API key y la guarda en `~/.komplian/postman-api-key`; no hace falta `export` después):
22
18
 
23
19
  ```bash
24
20
  npx komplian postman --yes
25
21
  ```
26
22
 
23
+ Opcional: **`npx komplian postman login`** solo para guardar o rotar la clave sin ejecutar el sync.
24
+
27
25
  Opcional: `export POSTMAN_API_KEY=…` solo para la sesión actual (tiene prioridad sobre el archivo).
28
26
 
29
27
  El comando llama a `GET https://api.getpostman.com/me` y **solo continúa** si el email de la cuenta es `@komplian.com`. **Si ya existen** la colección **Komplian API** y los entornos con el mismo nombre en ese workspace, se **actualizan**; si no, se crean.
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * db:all:dev — Verificación empleado @komplian.com (Postman GET /me, misma clave que postman login),
4
+ * guarda las 3 URLs de desarrollo en ~/.komplian/dev-databases.json (600) y las propaga a
5
+ * app|api|web|admin/.env.local. No usa archivos secretos en la raíz del monorepo.
6
+ *
7
+ * No interactivo: export KOMPLIAN_DEV_APP_DATABASE_URL, _ADMIN_, _WEB_ y npx komplian db:all:dev --yes
8
+ */
9
+
10
+ import {
11
+ chmodSync,
12
+ existsSync,
13
+ mkdirSync,
14
+ readFileSync,
15
+ writeFileSync,
16
+ } from "node:fs";
17
+ import { join, resolve } from "node:path";
18
+ import { homedir } from "node:os";
19
+ import { createInterface } from "node:readline/promises";
20
+ import { stdin as input, stdout as output } from "node:process";
21
+
22
+ import { assertKomplianEmployeePostman } from "./komplian-postman.mjs";
23
+ import {
24
+ applyOverridesToEnvContent,
25
+ formatEnvValue,
26
+ findWorkspaceRoot,
27
+ writeAtomic,
28
+ } from "./komplian-localhost.mjs";
29
+ import {
30
+ loadHomeDevDatabases,
31
+ maskDatabaseUrl,
32
+ } from "./komplian-db.mjs";
33
+
34
+ const c = {
35
+ reset: "\x1b[0m",
36
+ dim: "\x1b[2m",
37
+ bold: "\x1b[1m",
38
+ cyan: "\x1b[36m",
39
+ green: "\x1b[32m",
40
+ red: "\x1b[31m",
41
+ yellow: "\x1b[33m",
42
+ };
43
+
44
+ function log(s = "") {
45
+ console.log(s);
46
+ }
47
+
48
+ const PLACEHOLDER = "komplian_localhost_placeholder";
49
+ const DEV_DB_NAME = "dev-databases.json";
50
+
51
+ function ensureKomplianHome() {
52
+ const dir = join(homedir(), ".komplian");
53
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
54
+ try {
55
+ chmodSync(dir, 0o700);
56
+ } catch {
57
+ /* ignore */
58
+ }
59
+ return dir;
60
+ }
61
+
62
+ function devDbPath() {
63
+ return join(ensureKomplianHome(), DEV_DB_NAME);
64
+ }
65
+
66
+ function writeSavedDevDatabases(data) {
67
+ const p = devDbPath();
68
+ const body = JSON.stringify(
69
+ {
70
+ schemaVersion: 1,
71
+ app: data.app,
72
+ admin: data.admin,
73
+ web: data.web,
74
+ savedAt: new Date().toISOString(),
75
+ },
76
+ null,
77
+ 2
78
+ );
79
+ writeFileSync(p, body, { encoding: "utf8", mode: 0o600 });
80
+ try {
81
+ chmodSync(p, 0o600);
82
+ } catch {
83
+ /* ignore */
84
+ }
85
+ }
86
+
87
+ function isValidPostgresUrl(s) {
88
+ const t = (s || "").trim();
89
+ if (!t.startsWith("postgresql://") && !t.startsWith("postgres://")) return false;
90
+ if (t.includes(PLACEHOLDER)) return false;
91
+ try {
92
+ const u = new URL(t);
93
+ return !!u.hostname;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ function isValidTriplet(d) {
100
+ return (
101
+ isValidPostgresUrl(d.app) &&
102
+ isValidPostgresUrl(d.admin) &&
103
+ isValidPostgresUrl(d.web)
104
+ );
105
+ }
106
+
107
+ function headerDbAllDev() {
108
+ return `# KOMPLIAN — development databases (komplian db:all:dev · no commitear)\n`;
109
+ }
110
+
111
+ function patchEnvLocal(workspaceRoot, project, overrides) {
112
+ const envPath = join(workspaceRoot, project, ".env.local");
113
+ const examplePath = join(workspaceRoot, project, ".env.example");
114
+ let body;
115
+ let prefix = "";
116
+ if (existsSync(envPath)) {
117
+ body = applyOverridesToEnvContent(readFileSync(envPath, "utf8"), overrides);
118
+ } else if (existsSync(examplePath)) {
119
+ prefix = headerDbAllDev();
120
+ body = applyOverridesToEnvContent(readFileSync(examplePath, "utf8"), overrides);
121
+ } else {
122
+ prefix = headerDbAllDev();
123
+ body = Object.entries(overrides)
124
+ .map(([k, v]) => `${k}=${formatEnvValue(v)}`)
125
+ .join("\n");
126
+ }
127
+ writeAtomic(envPath, prefix + body);
128
+ }
129
+
130
+ function propagateToMonorepo(workspaceRoot, data) {
131
+ patchEnvLocal(workspaceRoot, "app", { DATABASE_URL: data.app });
132
+ patchEnvLocal(workspaceRoot, "admin", { DATABASE_URL: data.admin });
133
+ patchEnvLocal(workspaceRoot, "web", { DATABASE_URL: data.web });
134
+ patchEnvLocal(workspaceRoot, "api", {
135
+ APP_DATABASE_URL: data.app,
136
+ ADMIN_DATABASE_URL: data.admin,
137
+ WEB_DATABASE_URL: data.web,
138
+ });
139
+ }
140
+
141
+ function collectUrlsFromEnv() {
142
+ return {
143
+ app: process.env.KOMPLIAN_DEV_APP_DATABASE_URL?.trim() || "",
144
+ admin: process.env.KOMPLIAN_DEV_ADMIN_DATABASE_URL?.trim() || "",
145
+ web: process.env.KOMPLIAN_DEV_WEB_DATABASE_URL?.trim() || "",
146
+ };
147
+ }
148
+
149
+ async function collectUrlsInteractive(rl) {
150
+ log(` ${c.dim}postgresql://… desde Neon (rama development) u homólogo.${c.reset}`);
151
+ const app = (await rl.question(`${c.bold}URL APP${c.reset} (app + API): `)).trim();
152
+ const admin = (await rl.question(`${c.bold}URL ADMIN${c.reset}: `)).trim();
153
+ const web = (await rl.question(`${c.bold}URL WEB${c.reset} (pilot): `)).trim();
154
+ return { app, admin, web };
155
+ }
156
+
157
+ function parseArgs(argv) {
158
+ const opts = {
159
+ yes: false,
160
+ force: false,
161
+ workspace: "",
162
+ help: false,
163
+ };
164
+ const rest = [];
165
+ for (let i = 0; i < argv.length; i++) {
166
+ const a = argv[i];
167
+ if (a === "--yes" || a === "-y") opts.yes = true;
168
+ else if (a === "--force") opts.force = true;
169
+ else if (a === "-w" || a === "--workspace") opts.workspace = argv[++i] || "";
170
+ else if (a === "-h" || a === "--help") opts.help = true;
171
+ else if (a.startsWith("-")) {
172
+ log(`${c.red}✗${c.reset} Opción desconocida: ${a}`);
173
+ process.exit(1);
174
+ } else rest.push(a);
175
+ }
176
+ if (rest[0]) opts.workspace = rest[0];
177
+ return opts;
178
+ }
179
+
180
+ function usage() {
181
+ log(`Uso: npx komplian db:all:dev [opciones] [monorepo]`);
182
+ log(``);
183
+ log(` Tras ${c.bold}npx komplian postman login${c.reset}: verifica @komplian.com y configura las 3 bases de desarrollo.`);
184
+ log(` Guarda en ${c.dim}~/.komplian/dev-databases.json${c.reset} y actualiza ${c.dim}app|api|web|admin/.env.local${c.reset}.`);
185
+ log(``);
186
+ log(` Sin prompts: ${c.dim}KOMPLIAN_DEV_APP_DATABASE_URL${c.reset}, ${c.dim}_ADMIN_${c.reset}, ${c.dim}_WEB_${c.reset} + ${c.bold}--yes${c.reset}`);
187
+ log(` ${c.bold}--force${c.reset} Volver a pedir URLs (o usar env) aunque exista el JSON`);
188
+ log(` -w, --workspace Raíz del monorepo`);
189
+ log(` -h, --help`);
190
+ }
191
+
192
+ export async function runDbAllDev(argv) {
193
+ const opts = parseArgs(argv);
194
+ if (opts.help) {
195
+ usage();
196
+ return;
197
+ }
198
+
199
+ const workspaceRoot = opts.workspace.trim()
200
+ ? resolve(opts.workspace.replace(/^~(?=$|[/\\])/, process.env.HOME || ""))
201
+ : findWorkspaceRoot(process.cwd());
202
+
203
+ if (!existsSync(join(workspaceRoot, "api", "package.json"))) {
204
+ log(`${c.red}✗${c.reset} No parece un monorepo Komplian: ${workspaceRoot}`);
205
+ process.exit(1);
206
+ }
207
+
208
+ log(`${c.cyan}━━ Komplian — bases de datos (development) ━━${c.reset}`);
209
+ await assertKomplianEmployeePostman();
210
+
211
+ const fromEnv = collectUrlsFromEnv();
212
+ const envOk = isValidTriplet(fromEnv);
213
+ const saved = loadHomeDevDatabases();
214
+ const savedOk = saved && isValidTriplet(saved);
215
+
216
+ /** @type {{ app: string; admin: string; web: string }} */
217
+ let data;
218
+
219
+ if (envOk) {
220
+ data = fromEnv;
221
+ log(`${c.green}✓${c.reset} URLs desde variables de entorno (KOMPLIAN_DEV_*_DATABASE_URL).`);
222
+ } else if (savedOk && !opts.force && !envOk) {
223
+ data = saved;
224
+ log(
225
+ `${c.dim}○${c.reset} Usando ${c.bold}~/.komplian/dev-databases.json${c.reset} (${c.dim}--force${c.reset} para cambiar).`
226
+ );
227
+ } else if (opts.yes && !envOk) {
228
+ log(
229
+ `${c.red}✗${c.reset} Con ${c.bold}--yes${c.reset} define las tres variables ${c.dim}KOMPLIAN_DEV_APP_DATABASE_URL${c.reset}, ${c.dim}_ADMIN_${c.reset}, ${c.dim}_WEB_${c.reset}, o ejecuta sin ${c.bold}--yes${c.reset} para modo interactivo.`
230
+ );
231
+ process.exit(1);
232
+ } else {
233
+ const rl = createInterface({ input, output });
234
+ log(``);
235
+ log(`${c.bold}Introduce las connection strings de desarrollo (3).${c.reset}`);
236
+ data = await collectUrlsInteractive(rl);
237
+ rl.close();
238
+ }
239
+
240
+ if (!isValidTriplet(data)) {
241
+ log(`${c.red}✗${c.reset} Las tres URLs deben ser postgres:// o postgresql:// válidas (sin placeholder).`);
242
+ process.exit(1);
243
+ }
244
+
245
+ writeSavedDevDatabases(data);
246
+ propagateToMonorepo(workspaceRoot, data);
247
+
248
+ log(``);
249
+ log(`${c.green}✓${c.reset} Guardado ${c.dim}~/.komplian/${DEV_DB_NAME}${c.reset} y .env.local en app, api, web, admin.`);
250
+ log(` ${c.dim}APP${c.reset} ${maskDatabaseUrl(data.app)}`);
251
+ log(` ${c.dim}ADMIN${c.reset} ${maskDatabaseUrl(data.admin)}`);
252
+ log(` ${c.dim}WEB${c.reset} ${maskDatabaseUrl(data.web)}`);
253
+ log(``);
254
+ log(`${c.dim}Siguiente: ${c.reset}${c.cyan}npx komplian localhost --yes${c.reset}`);
255
+ log(`${c.dim}psql: ${c.reset}${c.cyan}npx komplian db:app:development${c.reset} …`);
256
+ }
package/komplian-db.mjs CHANGED
@@ -7,11 +7,8 @@
7
7
  * npx komplian db:admin:staging
8
8
  * npx komplian db:web:production
9
9
  *
10
- * URLs (prioridad: process.env KOMPLIAN_DATABASE_URLS.env KOMPLIAN_LOCALHOST_SECRETS.env .env.local solo development):
11
- * KOMPLIAN_DATABASE_URL_APP_DEVELOPMENT | _STAGING | _PRODUCTION
12
- * KOMPLIAN_DATABASE_URL_ADMIN_*
13
- * KOMPLIAN_DATABASE_URL_WEB_*
14
- * Legacy development: KOMPLIAN_LOCALHOST_*_DATABASE_URL
10
+ * Development: 1) .env.local por repo; 2) ~/.komplian/dev-databases.json (db:all:dev); 3) variables de sesión KOMPLIAN_*.
11
+ * Staging/producción: solo KOMPLIAN_DATABASE_URL_*_STAGING|_PRODUCTION (env o archivos en raíz), nunca .env.local.
15
12
  *
16
13
  * Producción: solo emails @komplian.com en KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST
17
14
  * (por defecto josue.santana@komplian.com). Operador: KOMPLIAN_DATABASE_OPERATOR_EMAIL o git config user.email.
@@ -22,6 +19,7 @@
22
19
 
23
20
  import { spawn, spawnSync } from "node:child_process";
24
21
  import { existsSync, readFileSync } from "node:fs";
22
+ import { homedir } from "node:os";
25
23
  import { dirname, join, resolve } from "node:path";
26
24
  import { createInterface } from "node:readline/promises";
27
25
  import { stdin as input, stdout as output } from "node:process";
@@ -56,12 +54,12 @@ const DEFAULT_PRODUCTION_ALLOWLIST = ["josue.santana@komplian.com"];
56
54
  /** Misma cadena que `komplian-localhost.mjs` cuando no hay URL real. */
57
55
  const LOCALHOST_DB_PLACEHOLDER = "komplian_localhost_placeholder";
58
56
 
59
- function isPlaceholderDbUrl(url) {
57
+ export function isPlaceholderDbUrl(url) {
60
58
  return !!(url && String(url).includes(LOCALHOST_DB_PLACEHOLDER));
61
59
  }
62
60
 
63
61
  /** Primera URL no vacía que no sea el placeholder de localhost. */
64
- function pickFirstNonPlaceholder(...candidates) {
62
+ export function pickFirstNonPlaceholder(...candidates) {
65
63
  for (const c of candidates) {
66
64
  const u = (c ?? "").trim();
67
65
  if (u && !isPlaceholderDbUrl(u)) return u;
@@ -75,7 +73,7 @@ function assertNotPlaceholderDbUrl(url) {
75
73
  `${c.red}✗${c.reset} La URL es el ${c.bold}placeholder${c.reset} de \`komplian localhost\` (no apunta a una base de datos real).`
76
74
  );
77
75
  log(``);
78
- log(` Pon una URL real (raíz del monorepo: ${c.dim}KOMPLIAN_LOCALHOST_*_DATABASE_URL${c.reset}, ${c.dim}KOMPLIAN_DATABASE_URLS.env${c.reset}, o ${c.dim}--url${c.reset}).`);
76
+ log(` Pon una URL real (${c.bold}npx komplian db:all:dev${c.reset}, ${c.dim}--url${c.reset}, o variables en sesión).`);
79
77
  process.exit(1);
80
78
  }
81
79
 
@@ -99,25 +97,21 @@ function parseEnvFile(content) {
99
97
  return out;
100
98
  }
101
99
 
102
- function loadMergedEnvFiles(workspaceRoot) {
103
- const out = {};
104
- const paths = [
105
- join(workspaceRoot, "KOMPLIAN_DATABASE_URLS.env"),
106
- join(workspaceRoot, ".komplian", "KOMPLIAN_DATABASE_URLS.env"),
107
- join(workspaceRoot, "KOMPLIAN_LOCALHOST_SECRETS.env"),
108
- join(workspaceRoot, ".komplian", "KOMPLIAN_LOCALHOST_SECRETS.env"),
109
- join(workspaceRoot, "komplian-localhost.secrets.env"),
110
- join(workspaceRoot, ".komplian", "localhost-secrets.env"),
111
- ];
112
- for (const p of paths) {
113
- if (!existsSync(p)) continue;
114
- try {
115
- Object.assign(out, parseEnvFile(readFileSync(p, "utf8")));
116
- } catch {
117
- /* ignore */
118
- }
100
+ /** ~/.komplian/dev-databases.json (creado por db:all:dev). No se leen secretos en la raíz del monorepo. */
101
+ export function loadHomeDevDatabases() {
102
+ const p = join(homedir(), ".komplian", "dev-databases.json");
103
+ if (!existsSync(p)) return null;
104
+ try {
105
+ const j = JSON.parse(readFileSync(p, "utf8"));
106
+ if (!j || typeof j !== "object") return null;
107
+ return {
108
+ app: String(j.app || "").trim(),
109
+ admin: String(j.admin || "").trim(),
110
+ web: String(j.web || "").trim(),
111
+ };
112
+ } catch {
113
+ return null;
119
114
  }
120
- return out;
121
115
  }
122
116
 
123
117
  function readEnvLocalKey(workspaceRoot, project, keys) {
@@ -152,7 +146,7 @@ function findWorkspaceRoot(start) {
152
146
  return resolve(start);
153
147
  }
154
148
 
155
- function maskDatabaseUrl(url) {
149
+ export function maskDatabaseUrl(url) {
156
150
  try {
157
151
  const u = new URL(url);
158
152
  if (u.password) u.password = "***";
@@ -171,7 +165,7 @@ function platformToDbKey(platform) {
171
165
  return null;
172
166
  }
173
167
 
174
- function loadProductionAllowlist(workspaceRoot) {
168
+ function loadProductionAllowlist() {
175
169
  const raw = process.env.KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST?.trim();
176
170
  if (raw) {
177
171
  return raw
@@ -179,21 +173,6 @@ function loadProductionAllowlist(workspaceRoot) {
179
173
  .map((s) => s.trim().toLowerCase())
180
174
  .filter(Boolean);
181
175
  }
182
- const p = join(
183
- workspaceRoot,
184
- ".komplian",
185
- "KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST"
186
- );
187
- if (existsSync(p)) {
188
- try {
189
- return readFileSync(p, "utf8")
190
- .split(/\r?\n/)
191
- .map((l) => l.trim().toLowerCase())
192
- .filter((l) => l && !l.startsWith("#"));
193
- } catch {
194
- /* ignore */
195
- }
196
- }
197
176
  return DEFAULT_PRODUCTION_ALLOWLIST.map((e) => e.toLowerCase());
198
177
  }
199
178
 
@@ -248,7 +227,7 @@ async function resolveOperatorEmail() {
248
227
  return (ans || "").trim();
249
228
  }
250
229
 
251
- function assertProductionAccess(email, allowlist) {
230
+ function assertProductionAccessLocalAllowlist(email, allowlist) {
252
231
  const e = email.toLowerCase();
253
232
  if (!e || !e.endsWith("@komplian.com")) {
254
233
  log(
@@ -258,57 +237,123 @@ function assertProductionAccess(email, allowlist) {
258
237
  }
259
238
  if (!allowlist.includes(e)) {
260
239
  log(
261
- `${c.red}✗${c.reset} Producción: este email no está autorizado. Añádelo en ${c.bold}KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST${c.reset} o en ${c.dim}.komplian/KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST${c.reset}.`
240
+ `${c.red}✗${c.reset} Producción: email no autorizado (local). Usa Admin ${c.bold}Prod DB CLI${c.reset} o ${c.dim}KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST${c.reset} / ${c.dim}KOMPLIAN_API_URL${c.reset}+${c.dim}ADMIN_API_KEY${c.reset}.`
262
241
  );
263
242
  process.exit(1);
264
243
  }
265
244
  }
266
245
 
246
+ /** Si hay KOMPLIAN_API_URL + ADMIN_API_KEY, la API central decide (lista en Admin, super admin). */
247
+ async function verifyProductionAccessViaApi(email) {
248
+ const base = (process.env.KOMPLIAN_API_URL || "").trim().replace(/\/$/, "");
249
+ const apiKey = (process.env.ADMIN_API_KEY || "").trim();
250
+ if (!base || !apiKey) return null;
251
+ try {
252
+ const ac = new AbortController();
253
+ const t = setTimeout(() => ac.abort(), 15000);
254
+ const r = await fetch(`${base}/api/admin/production-db-cli/verify`, {
255
+ method: "POST",
256
+ headers: {
257
+ "Content-Type": "application/json",
258
+ "X-API-Key": apiKey,
259
+ },
260
+ body: JSON.stringify({ email: email.toLowerCase() }),
261
+ signal: ac.signal,
262
+ });
263
+ clearTimeout(t);
264
+ const j = await r.json().catch(() => ({}));
265
+ if (!r.ok || j.success !== true || typeof j.allowed !== "boolean") return null;
266
+ return j.allowed;
267
+ } catch {
268
+ return null;
269
+ }
270
+ }
271
+
272
+ async function assertProductionAccessResolved(email, quiet) {
273
+ const e = (email || "").toLowerCase().trim();
274
+ if (!e || !e.endsWith("@komplian.com")) {
275
+ log(
276
+ `${c.red}✗${c.reset} Producción: solo cuentas ${c.bold}@komplian.com${c.reset}. Define ${c.dim}KOMPLIAN_DATABASE_OPERATOR_EMAIL${c.reset} o ${c.dim}git config user.email${c.reset}.`
277
+ );
278
+ process.exit(1);
279
+ }
280
+ const verdict = await verifyProductionAccessViaApi(e);
281
+ if (verdict === true) {
282
+ if (!quiet) {
283
+ log(
284
+ `${c.dim}○${c.reset} Producción: allowlist desde ${c.bold}API central${c.reset} (Admin → Prod DB CLI).`
285
+ );
286
+ }
287
+ return;
288
+ }
289
+ if (verdict === false) {
290
+ log(
291
+ `${c.red}✗${c.reset} Producción: este email no está en la lista (Admin → Prod DB CLI, solo super admin).`
292
+ );
293
+ process.exit(1);
294
+ }
295
+ assertProductionAccessLocalAllowlist(e, loadProductionAllowlist());
296
+ }
297
+
267
298
  function resolveDatabaseUrl(workspaceRoot, platform, environment) {
268
299
  const dbKey = platformToDbKey(platform);
269
300
  if (!dbKey) return "";
270
- const file = loadMergedEnvFiles(workspaceRoot);
271
301
  const envU = environment.toUpperCase();
272
302
  const primaryKey = `KOMPLIAN_DATABASE_URL_${dbKey}_${envU}`;
303
+ const home = loadHomeDevDatabases();
273
304
 
274
- let url = pickFirstNonPlaceholder(
275
- process.env[primaryKey],
276
- file[primaryKey]
277
- );
305
+ if (environment !== "development") {
306
+ return pickFirstNonPlaceholder(process.env[primaryKey]);
307
+ }
308
+
309
+ const single = (process.env.KOMPLIAN_LOCALHOST_DATABASE_URL || "").trim();
278
310
 
279
- if (environment === "development" && !url) {
280
- const single = pickFirstNonPlaceholder(
281
- process.env.KOMPLIAN_LOCALHOST_DATABASE_URL,
282
- file.KOMPLIAN_LOCALHOST_DATABASE_URL
311
+ let fromRepos = "";
312
+ if (dbKey === "APP") {
313
+ fromRepos = pickFirstNonPlaceholder(
314
+ readEnvLocalKey(workspaceRoot, "app", ["DATABASE_URL"]),
315
+ readEnvLocalKey(workspaceRoot, "api", ["APP_DATABASE_URL"])
316
+ );
317
+ } else if (dbKey === "ADMIN") {
318
+ fromRepos = pickFirstNonPlaceholder(
319
+ readEnvLocalKey(workspaceRoot, "admin", ["DATABASE_URL"]),
320
+ readEnvLocalKey(workspaceRoot, "api", ["ADMIN_DATABASE_URL"])
321
+ );
322
+ } else if (dbKey === "WEB") {
323
+ fromRepos = pickFirstNonPlaceholder(
324
+ readEnvLocalKey(workspaceRoot, "web", ["DATABASE_URL"]),
325
+ readEnvLocalKey(workspaceRoot, "api", ["WEB_DATABASE_URL"])
283
326
  );
284
- if (dbKey === "APP") {
285
- url = pickFirstNonPlaceholder(
286
- process.env.KOMPLIAN_LOCALHOST_APP_DATABASE_URL,
287
- file.KOMPLIAN_LOCALHOST_APP_DATABASE_URL,
288
- single,
289
- readEnvLocalKey(workspaceRoot, "app", ["DATABASE_URL"]),
290
- readEnvLocalKey(workspaceRoot, "api", ["APP_DATABASE_URL"])
291
- );
292
- } else if (dbKey === "ADMIN") {
293
- url = pickFirstNonPlaceholder(
294
- process.env.KOMPLIAN_LOCALHOST_ADMIN_DATABASE_URL,
295
- file.KOMPLIAN_LOCALHOST_ADMIN_DATABASE_URL,
296
- single,
297
- readEnvLocalKey(workspaceRoot, "admin", ["DATABASE_URL"]),
298
- readEnvLocalKey(workspaceRoot, "api", ["ADMIN_DATABASE_URL"])
299
- );
300
- } else if (dbKey === "WEB") {
301
- url = pickFirstNonPlaceholder(
302
- process.env.KOMPLIAN_LOCALHOST_WEB_DATABASE_URL,
303
- file.KOMPLIAN_LOCALHOST_WEB_DATABASE_URL,
304
- single,
305
- readEnvLocalKey(workspaceRoot, "web", ["DATABASE_URL"]),
306
- readEnvLocalKey(workspaceRoot, "api", ["WEB_DATABASE_URL"])
307
- );
308
- }
309
327
  }
310
328
 
311
- return url;
329
+ if (dbKey === "APP") {
330
+ return pickFirstNonPlaceholder(
331
+ fromRepos,
332
+ process.env[primaryKey],
333
+ home?.app,
334
+ process.env.KOMPLIAN_LOCALHOST_APP_DATABASE_URL,
335
+ single
336
+ );
337
+ }
338
+ if (dbKey === "ADMIN") {
339
+ return pickFirstNonPlaceholder(
340
+ fromRepos,
341
+ process.env[primaryKey],
342
+ home?.admin,
343
+ process.env.KOMPLIAN_LOCALHOST_ADMIN_DATABASE_URL,
344
+ single
345
+ );
346
+ }
347
+ if (dbKey === "WEB") {
348
+ return pickFirstNonPlaceholder(
349
+ fromRepos,
350
+ process.env[primaryKey],
351
+ home?.web,
352
+ process.env.KOMPLIAN_LOCALHOST_WEB_DATABASE_URL,
353
+ single
354
+ );
355
+ }
356
+ return "";
312
357
  }
313
358
 
314
359
  function findPsqlBinary() {
@@ -385,15 +430,15 @@ function usageDb() {
385
430
  log(` Plataformas: ${c.dim}app | admin | web | api-app | api-admin | api-web${c.reset}`);
386
431
  log(` Entornos: ${c.dim}development | staging | production${c.reset}`);
387
432
  log(``);
388
- log(` Variables KOMPLIAN (recomendado):`);
389
- log(` ${c.dim}KOMPLIAN_DATABASE_URL_APP_DEVELOPMENT${c.reset}`);
390
- log(` ${c.dim}KOMPLIAN_DATABASE_URL_APP_STAGING / _PRODUCTION${c.reset} (idem ADMIN, WEB)`);
391
- log(` Archivo opcional: ${c.dim}KOMPLIAN_DATABASE_URLS.env${c.reset} (gitignored)`);
433
+ log(` ${c.bold}db:all:dev${c.reset} Configura las 3 DB de desarrollo (Postman @komplian.com) → ~/.komplian + cada repo /.env.local`);
434
+ log(` Development: ${c.dim}.env.local por repo${c.reset}, ${c.dim}~/.komplian/dev-databases.json${c.reset}, variables en sesión.`);
435
+ log(` Staging/producción: solo ${c.dim}KOMPLIAN_DATABASE_URL_*_STAGING|_PRODUCTION${c.reset} en el entorno (nunca .env.local).`);
392
436
  log(` Neon: ${c.dim}staging/producción${c.reset} exigen host *.neon.tech; ${c.dim}development${c.reset} permite Postgres local (aviso).`);
393
437
  log(``);
394
438
  log(` Producción:`);
395
- log(` ${c.dim}KOMPLIAN_DATABASE_OPERATOR_EMAIL${c.reset} o git user.email`);
396
- log(` ${c.dim}KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST${c.reset} (coma) o .komplian/KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST`);
439
+ log(` ${c.dim}KOMPLIAN_DATABASE_OPERATOR_EMAIL${c.reset} o git user.email (@komplian.com)`);
440
+ log(` Preferido: ${c.bold}Admin → Prod DB CLI${c.reset} (super admin) + ${c.dim}KOMPLIAN_API_URL${c.reset} + ${c.dim}ADMIN_API_KEY${c.reset} en la sesión del operador.`);
441
+ log(` Sin API: ${c.dim}KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST${c.reset} (coma) en el entorno local.`);
397
442
  log(``);
398
443
  log(` -c, --command SQL no interactivo`);
399
444
  log(` -u, --url URL explícita (sigue reglas Neon / producción)`);
@@ -426,14 +471,12 @@ export async function runDb(argv) {
426
471
  ? resolve(opts.workspace.replace(/^~(?=$|[/\\])/, process.env.HOME || ""))
427
472
  : findWorkspaceRoot(process.cwd());
428
473
 
429
- const allowlist = loadProductionAllowlist(workspaceRoot);
430
-
431
474
  if (opts.environment === "production") {
432
475
  const email = await resolveOperatorEmail();
433
- assertProductionAccess(email, allowlist);
476
+ await assertProductionAccessResolved(email, opts.quiet);
434
477
  if (!opts.quiet) {
435
478
  log(
436
- `${c.yellow}⚠${c.reset} Producción · operador ${c.bold}${email.toLowerCase()}${c.reset} (allowlist Komplian)`
479
+ `${c.yellow}⚠${c.reset} Producción · operador ${c.bold}${email.toLowerCase()}${c.reset}`
437
480
  );
438
481
  }
439
482
  }
@@ -452,12 +495,17 @@ export async function runDb(argv) {
452
495
 
453
496
  if (!url) {
454
497
  log(`${c.red}✗${c.reset} Sin URL para ${opts.platform} / ${opts.environment}.`);
455
- log(
456
- ` Define ${c.bold}KOMPLIAN_DATABASE_URL_${platformToDbKey(opts.platform)}_${opts.environment.toUpperCase()}${c.reset} o ${c.dim}KOMPLIAN_DATABASE_URLS.env${c.reset}.`
457
- );
458
498
  if (opts.environment === "development") {
499
+ const pk = platformToDbKey(opts.platform);
500
+ log(
501
+ ` Pon la URL de Neon (rama dev) en ${c.bold}${opts.platform === "web" || opts.platform === "api-web" ? "web" : opts.platform === "admin" || opts.platform === "api-admin" ? "admin" : "app"}/.env.local${c.reset} (${c.dim}DATABASE_URL${c.reset}) o en ${c.dim}api/.env.local${c.reset} (${c.dim}${pk === "WEB" ? "WEB_DATABASE_URL" : pk === "ADMIN" ? "ADMIN_DATABASE_URL" : "APP_DATABASE_URL"}${c.reset}).`
502
+ );
503
+ log(
504
+ ` ${c.dim}(Placeholders ignorados; ${c.bold}npx komplian db:all:dev${c.reset} o ${c.bold}KOMPLIAN_DATABASE_URL_${pk}_DEVELOPMENT${c.reset} en el entorno.)${c.reset}`
505
+ );
506
+ } else {
459
507
  log(
460
- ` ${c.dim}(Los valores con ${LOCALHOST_DB_PLACEHOLDER} en .env.local se ignoran; usa una URL real en la raíz del monorepo.)${c.reset}`
508
+ ` Exporta ${c.bold}KOMPLIAN_DATABASE_URL_${platformToDbKey(opts.platform)}_${opts.environment.toUpperCase()}${c.reset} en el entorno (CI / operador).`
461
509
  );
462
510
  }
463
511
  process.exit(1);
@@ -6,8 +6,8 @@
6
6
  * Seguridad: los `.env*` no van en el paquete npm `komplian` (solo *.mjs + JSON + README).
7
7
  * No imprime secretos. Tras generar, muestra tabla de URLs locales (puertos fijos).
8
8
  *
9
- * Neon: KOMPLIAN_LOCALHOST_APP_DATABASE_URL, …_ADMIN_…, …_WEB_… o KOMPLIAN_LOCALHOST_SECRETS.env
10
- * (plantilla en la raíz del monorepo: KOMPLIAN_LOCALHOST_SECRETS.env.example).
9
+ * Bases de datos: primero cada repo `/.env.local`; luego ~/.komplian/dev-databases.json (npx komplian db:all:dev);
10
+ * opcional en la sesión: KOMPLIAN_LOCALHOST_*_DATABASE_URL (sin archivos secretos en la raíz del monorepo).
11
11
  */
12
12
 
13
13
  import { randomBytes } from "node:crypto";
@@ -18,6 +18,11 @@ import { fileURLToPath } from "node:url";
18
18
  import { createInterface } from "node:readline/promises";
19
19
  import { stdin as input, stdout as output } from "node:process";
20
20
 
21
+ import {
22
+ loadHomeDevDatabases,
23
+ pickFirstNonPlaceholder,
24
+ } from "./komplian-db.mjs";
25
+
21
26
  const __dirname = dirname(fileURLToPath(import.meta.url));
22
27
 
23
28
  const c = {
@@ -61,41 +66,41 @@ function parseEnvFile(content) {
61
66
  return out;
62
67
  }
63
68
 
64
- function loadSecretsFromDisk(workspaceRoot) {
65
- const paths = [
66
- join(workspaceRoot, "KOMPLIAN_LOCALHOST_SECRETS.env"),
67
- join(workspaceRoot, ".komplian", "KOMPLIAN_LOCALHOST_SECRETS.env"),
68
- join(workspaceRoot, "komplian-localhost.secrets.env"),
69
- join(workspaceRoot, ".komplian", "localhost-secrets.env"),
70
- ];
71
- for (const p of paths) {
72
- if (!existsSync(p)) continue;
73
- try {
74
- return parseEnvFile(readFileSync(p, "utf8"));
75
- } catch {
76
- /* ignore */
77
- }
69
+ function readEnvLocalDbUrl(workspaceRoot, project, key) {
70
+ const p = join(workspaceRoot, project, ".env.local");
71
+ if (!existsSync(p)) return "";
72
+ try {
73
+ const m = parseEnvFile(readFileSync(p, "utf8"));
74
+ return String(m[key] || "").trim();
75
+ } catch {
76
+ return "";
78
77
  }
79
- return {};
80
78
  }
81
79
 
82
80
  function resolveDatabaseUrls(workspaceRoot) {
83
- const file = loadSecretsFromDisk(workspaceRoot);
84
- const single =
85
- process.env.KOMPLIAN_LOCALHOST_DATABASE_URL?.trim() ||
86
- file.KOMPLIAN_LOCALHOST_DATABASE_URL?.trim();
87
- const app =
88
- process.env.KOMPLIAN_LOCALHOST_APP_DATABASE_URL?.trim() ||
89
- file.KOMPLIAN_LOCALHOST_APP_DATABASE_URL?.trim() ||
90
- single;
91
- const admin =
92
- process.env.KOMPLIAN_LOCALHOST_ADMIN_DATABASE_URL?.trim() ||
93
- file.KOMPLIAN_LOCALHOST_ADMIN_DATABASE_URL?.trim() ||
94
- single;
95
- const web =
96
- process.env.KOMPLIAN_LOCALHOST_WEB_DATABASE_URL?.trim() ||
97
- file.KOMPLIAN_LOCALHOST_WEB_DATABASE_URL?.trim() ||
98
- single;
81
+ const home = loadHomeDevDatabases();
82
+ const single = (process.env.KOMPLIAN_LOCALHOST_DATABASE_URL || "").trim();
83
+ const app = pickFirstNonPlaceholder(
84
+ readEnvLocalDbUrl(workspaceRoot, "app", "DATABASE_URL"),
85
+ readEnvLocalDbUrl(workspaceRoot, "api", "APP_DATABASE_URL"),
86
+ home?.app,
87
+ process.env.KOMPLIAN_LOCALHOST_APP_DATABASE_URL,
88
+ single
89
+ );
90
+ const admin = pickFirstNonPlaceholder(
91
+ readEnvLocalDbUrl(workspaceRoot, "admin", "DATABASE_URL"),
92
+ readEnvLocalDbUrl(workspaceRoot, "api", "ADMIN_DATABASE_URL"),
93
+ home?.admin,
94
+ process.env.KOMPLIAN_LOCALHOST_ADMIN_DATABASE_URL,
95
+ single
96
+ );
97
+ const web = pickFirstNonPlaceholder(
98
+ readEnvLocalDbUrl(workspaceRoot, "web", "DATABASE_URL"),
99
+ readEnvLocalDbUrl(workspaceRoot, "api", "WEB_DATABASE_URL"),
100
+ home?.web,
101
+ process.env.KOMPLIAN_LOCALHOST_WEB_DATABASE_URL,
102
+ single
103
+ );
99
104
  const hasReal =
100
105
  !!(app && admin && web) &&
101
106
  !String(app).includes("komplian_localhost_placeholder");
@@ -421,7 +426,7 @@ function usageLocalhost() {
421
426
  log(`Uso: npx komplian localhost [opciones] [carpeta-monorepo]`);
422
427
  log(``);
423
428
  log(` Genera .env.local completos (desde cada .env.example + secretos KOMPLIAN) y arranca todos los dev servers.`);
424
- log(` ${c.dim}Neon: KOMPLIAN_LOCALHOST_APP_DATABASE_URL, …_ADMIN_…, …_WEB_… o archivo KOMPLIAN_LOCALHOST_SECRETS.env${c.reset}`);
429
+ log(` ${c.dim}DBs: npx komplian db:all:dev antes (o ~/.komplian/dev-databases.json / variables en sesión).${c.reset}`);
425
430
  log(``);
426
431
  log(` -y, --yes Sin confirmación`);
427
432
  log(` --force Regenerar .env.local`);
@@ -506,9 +511,8 @@ export async function runLocalhost(argv) {
506
511
 
507
512
  if (!db.hasReal && !opts.minimal) {
508
513
  log(
509
- `${c.yellow}⚠${c.reset} Sin Neon: exporta ${c.bold}KOMPLIAN_LOCALHOST_APP_DATABASE_URL${c.reset} / ${c.bold}ADMIN${c.reset} / ${c.bold}WEB${c.reset} o usa ${c.bold}KOMPLIAN_LOCALHOST_SECRETS.env${c.reset} (plantilla: KOMPLIAN_LOCALHOST_SECRETS.env.example).`
514
+ `${c.yellow}⚠${c.reset} Sin URLs de desarrollo reales. Ejecuta ${c.bold}npx komplian db:all:dev${c.reset} y luego vuelve a lanzar localhost (o ${c.bold}--minimal${c.reset}).`
510
515
  );
511
- log(`${c.dim} O ${c.bold}--minimal${c.reset} (solo app + web + docs).${c.reset}`);
512
516
  log("");
513
517
  }
514
518
 
@@ -573,3 +577,11 @@ export async function runLocalhost(argv) {
573
577
  process.exit(code ?? 0);
574
578
  });
575
579
  }
580
+
581
+ export {
582
+ applyOverridesToEnvContent,
583
+ formatEnvValue,
584
+ parseEnvFile,
585
+ writeAtomic,
586
+ findWorkspaceRoot,
587
+ };
@@ -394,13 +394,15 @@ function npmInstallEach(workspace) {
394
394
  }
395
395
 
396
396
  function usage() {
397
- log(`Uso: komplian onboard | postman | localhost | mcp-tools | db [opciones]`);
398
- log(` npx komplian onboard --yes`);
399
- log(` npx komplian postman login ${c.dim}(una vez · guarda API key)${c.reset}`);
400
- log(` npx komplian postman --yes ${c.dim}(email @komplian.com)${c.reset}`);
401
- log(` npx komplian localhost --yes ${c.dim}(env local + api app web admin docs)${c.reset}`);
402
- log(` npx komplian mcp-tools --yes ${c.dim}(.cursor/mcp.json + guía MCP Komplian)${c.reset}`);
403
- log(` npx komplian db:app:development ${c.dim}(psql Neon · KOMPLIAN_DATABASE_URL_*)${c.reset}`);
397
+ log(`Uso: komplian onboard | postman | mcp-tools | db:all:dev | localhost | db …`);
398
+ log(` ${c.bold}Setup estándar (orden):${c.reset}`);
399
+ log(` 1. npx komplian onboard --yes`);
400
+ log(` 2. npx komplian postman login → npx komplian postman --yes ${c.dim}(@komplian.com)${c.reset}`);
401
+ log(` 3. npx komplian mcp-tools --yes`);
402
+ log(` 4. npx komplian db:all:dev ${c.dim}(Postman + 3 URLs dev → ~/.komplian + .env.local)${c.reset}`);
403
+ log(` 5. npx komplian localhost --yes`);
404
+ log(``);
405
+ log(` npx komplian db:app:development ${c.dim}(psql una DB)${c.reset}`);
404
406
  log(``);
405
407
  log(` Antes (una vez): gh auth login -h github.com -s repo -s read:org -w`);
406
408
  log(` Requisitos: Node 18+, git, GitHub CLI (gh)`);
@@ -499,6 +501,11 @@ async function main() {
499
501
  await runMcpTools(rawArgv.slice(1));
500
502
  return;
501
503
  }
504
+ if (rawArgv[0] === "db:all:dev") {
505
+ const { runDbAllDev } = await import("./komplian-db-all-dev.mjs");
506
+ await runDbAllDev(rawArgv.slice(1));
507
+ return;
508
+ }
502
509
  if (rawArgv[0]?.startsWith("db:")) {
503
510
  const { runDb } = await import("./komplian-db.mjs");
504
511
  const seg = rawArgv[0].split(":").filter(Boolean);
@@ -2,9 +2,10 @@
2
2
  /**
3
3
  * Komplian Postman — API key + email @komplian.com (Postman /me), luego colección + entornos.
4
4
  *
5
- * Requisitos: Node 18+. Primera vez: npx komplian postman login
5
+ * Requisitos: Node 18+. Primera vez: ejecuta `npx komplian postman --yes` (o `postman` sin clave):
6
+ * en terminal interactiva te pide la API key una vez y la guarda; o `npx komplian postman login`.
6
7
  *
7
- * Clave: POSTMAN_API_KEY o ~/.komplian/postman-api-key (npx komplian postman login)
8
+ * Clave: POSTMAN_API_KEY, ~/.komplian/postman-api-key, o prompt la primera vez (TTY).
8
9
  * Seguridad: no registrar secretos; errores API redactados; login sin eco (TTY); ~/.komplian 700 + key 600.
9
10
  * Opcional: POSTMAN_WORKSPACE_ID, KOMPLIAN_EMAIL_DOMAIN, KOMPLIAN_POSTMAN_KEY_FILE, KOMPLIAN_DEBUG
10
11
  */
@@ -229,17 +230,72 @@ function resolveApiKey() {
229
230
  function printMissingKeyHelp() {
230
231
  log(`${c.red}✗${c.reset} No hay API key de Postman.`);
231
232
  log(``);
232
- log(` ${c.bold}Una vez por máquina:${c.reset}`);
233
- log(` ${c.cyan}npx komplian postman login${c.reset}`);
234
- log(` (pega la clave de Postman Settings → API keys → Generate)`);
233
+ log(` ${c.bold}Terminal interactiva (primera vez):${c.reset}`);
234
+ log(
235
+ ` ${c.cyan}npx komplian postman --yes${c.reset} (pide la clave y sigue con el sync)`
236
+ );
237
+ log(
238
+ ` ${c.cyan}npx komplian postman login${c.reset} (solo guardar clave; luego postman --yes)`
239
+ );
235
240
  log(``);
236
- log(` ${c.dim}O en la sesión actual: export POSTMAN_API_KEY=…${c.reset}`);
241
+ log(` ${c.bold}Sin TTY (CI, pipes):${c.reset}`);
242
+ log(` ${c.dim}export POSTMAN_API_KEY=…${c.reset}`);
237
243
  log(
238
- `${c.dim} Archivo manual: ${formatHomePath(defaultKeyPath())}${c.reset}`
244
+ `${c.dim} Archivo: ${formatHomePath(defaultKeyPath())} (permiso 600)${c.reset}`
239
245
  );
240
246
  process.exit(1);
241
247
  }
242
248
 
249
+ /**
250
+ * POSTMAN_API_KEY o ~/.komplian/postman-api-key. Si falta y hay TTY, pide la clave
251
+ * (misma validación que `postman login`), guarda y devuelve la resolución.
252
+ */
253
+ async function resolveApiKeyWithOptionalInteractiveSetup(domain) {
254
+ let r = resolveApiKey();
255
+ if (r.key) return r;
256
+ if (!input.isTTY) {
257
+ printMissingKeyHelp();
258
+ }
259
+
260
+ log(`${c.cyan}━━ Primera vez: Postman API key ━━${c.reset}`);
261
+ log(
262
+ `${c.dim}Cuenta @${domain}. Postman → Settings → API keys → Generate. Se guarda en ${formatHomePath(defaultKeyPath())}.${c.reset}`
263
+ );
264
+ log("");
265
+
266
+ const raw = await readPasswordLine(
267
+ "Postman API key (no se muestra al escribir; Settings → API keys): "
268
+ );
269
+ const k = (raw || "").trim();
270
+ if (!k) {
271
+ log(`${c.red}✗${c.reset} Clave vacía.`);
272
+ process.exit(1);
273
+ }
274
+
275
+ await verifyKomplianEmail(k, domain, { quietSuccess: true });
276
+
277
+ const keyPath = defaultKeyPath();
278
+ ensureSecureKomplianDir();
279
+ writeFileSync(keyPath, `${k}\n`, { encoding: "utf8", mode: 0o600 });
280
+ try {
281
+ chmodSync(keyPath, 0o600);
282
+ } catch {
283
+ /* Windows u otros */
284
+ }
285
+
286
+ log("");
287
+ log(
288
+ `${c.green}✓${c.reset} Guardada en ${c.bold}${formatHomePath(keyPath)}${c.reset} — continuando…`
289
+ );
290
+
291
+ r = resolveApiKey();
292
+ if (!r.key) {
293
+ log(`${c.red}✗${c.reset} No se pudo leer la clave guardada.`);
294
+ process.exit(1);
295
+ }
296
+ return r;
297
+ }
298
+
243
299
  /** Parseo mínimo .env (sin dependencia dotenv). */
244
300
  function parseEnvFile(text) {
245
301
  const out = {};
@@ -600,7 +656,8 @@ async function pickWorkspaceId(apiKey, explicit) {
600
656
  return w.id;
601
657
  }
602
658
 
603
- async function verifyKomplianEmail(apiKey, domain) {
659
+ async function verifyKomplianEmail(apiKey, domain, opts = {}) {
660
+ const quietOk = opts.quietSuccess === true;
604
661
  const { ok, status, body } = await pmFetch(apiKey, "/me");
605
662
  if (!ok) {
606
663
  log(
@@ -624,9 +681,11 @@ async function verifyKomplianEmail(apiKey, domain) {
624
681
  );
625
682
  process.exit(1);
626
683
  }
627
- log(
628
- `${c.green}✓${c.reset} Postman: ${c.bold}${maskEmail(email)}${c.reset} (${c.dim}dominio permitido${c.reset})`
629
- );
684
+ if (!quietOk) {
685
+ log(
686
+ `${c.green}✓${c.reset} Postman: ${c.bold}${maskEmail(email)}${c.reset} (${c.dim}dominio permitido${c.reset})`
687
+ );
688
+ }
630
689
  return email;
631
690
  }
632
691
 
@@ -759,7 +818,7 @@ function usage() {
759
818
  log(` Clave: ${c.dim}POSTMAN_API_KEY${c.reset} o archivo ${c.dim}~/.komplian/postman-api-key${c.reset} (véase ${c.cyan}postman login${c.reset})`);
760
819
  log(` Dominio email: solo @komplian.com (GET /me)`);
761
820
  log(``);
762
- log(` -y, --yes Sin prompts extra`);
821
+ log(` -y, --yes Sin prompts extra (si falta API key y hay TTY, pide la clave una vez y guarda)`);
763
822
  log(` --export-only Solo escribe JSON en disco (no llama a la API de Postman)`);
764
823
  log(` --out <dir> Carpeta para export (por defecto: ./komplian-postman)`);
765
824
  log(` --dotenv <ruta> .env extra (además de api/.env, .env, KOMPLIAN_DOTENV)`);
@@ -771,8 +830,8 @@ function usage() {
771
830
 
772
831
  function usageLogin() {
773
832
  log(`Uso: komplian postman login`);
774
- log(` Guarda la API key de Postman en ${c.dim}~/.komplian/postman-api-key${c.reset} (permiso 600).`);
775
- log(` Tras esto: ${c.cyan}npx komplian postman --yes${c.reset} sin exportar variables.`);
833
+ log(` Guarda o sustituye la API key en ${c.dim}~/.komplian/postman-api-key${c.reset} (permiso 600).`);
834
+ log(` Opcional: ${c.cyan}npx komplian postman --yes${c.reset} ya pide la clave la primera vez si falta.`);
776
835
  log(``);
777
836
  log(` Postman → Settings (avatar) → API keys → Generate`);
778
837
  }
@@ -841,15 +900,13 @@ export async function runPostman(argv) {
841
900
  return;
842
901
  }
843
902
 
844
- const { key: apiKey, source } = resolveApiKey();
845
- if (!apiKey) {
846
- printMissingKeyHelp();
847
- }
903
+ const domain = (process.env.KOMPLIAN_EMAIL_DOMAIN || "komplian.com").trim();
904
+ const { key: apiKey, source } =
905
+ await resolveApiKeyWithOptionalInteractiveSetup(domain);
848
906
  if (source && source !== "POSTMAN_API_KEY") {
849
907
  log(`${c.dim}→ API key Postman desde: ${formatHomePath(source)}${c.reset}`);
850
908
  }
851
909
 
852
- const domain = (process.env.KOMPLIAN_EMAIL_DOMAIN || "komplian.com").trim();
853
910
  await verifyKomplianEmail(apiKey, domain);
854
911
 
855
912
  const collection = buildCollection();
@@ -938,3 +995,16 @@ export async function runPostman(argv) {
938
995
  );
939
996
  }
940
997
  }
998
+
999
+ /**
1000
+ * GET /me y dominio @komplian.com. Si no hay clave y hay TTY, pide guardarla (como `postman --yes`).
1001
+ */
1002
+ export async function assertKomplianEmployeePostman() {
1003
+ const domain = (process.env.KOMPLIAN_EMAIL_DOMAIN || "komplian.com").trim();
1004
+ const { key, source } =
1005
+ await resolveApiKeyWithOptionalInteractiveSetup(domain);
1006
+ if (source && source !== "POSTMAN_API_KEY") {
1007
+ log(`${c.dim}→ Postman API key desde: ${formatHomePath(source)}${c.reset}`);
1008
+ }
1009
+ return verifyKomplianEmail(key, domain);
1010
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "komplian",
3
- "version": "0.5.3",
3
+ "version": "0.6.1",
4
4
  "description": "Komplian CLI: onboard, Postman, localhost, mcp-tools, db (psql). Node 18+. Published tarball has no .env / secrets.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -15,7 +15,7 @@
15
15
  "komplian-localhost.mjs",
16
16
  "komplian-mcp-tools.mjs",
17
17
  "komplian-db.mjs",
18
- "KOMPLIAN_DATABASE_URLS.env.example",
18
+ "komplian-db-all-dev.mjs",
19
19
  "komplian-team-repos.json",
20
20
  "README.md"
21
21
  ],
@@ -23,7 +23,7 @@
23
23
  "access": "public"
24
24
  },
25
25
  "scripts": {
26
- "prepublishOnly": "node --check komplian-onboard.mjs && node --check komplian-postman.mjs && node --check komplian-localhost.mjs && node --check komplian-mcp-tools.mjs && node --check komplian-db.mjs"
26
+ "prepublishOnly": "node --check komplian-onboard.mjs && node --check komplian-postman.mjs && node --check komplian-localhost.mjs && node --check komplian-mcp-tools.mjs && node --check komplian-db.mjs && node --check komplian-db-all-dev.mjs"
27
27
  },
28
28
  "keywords": [
29
29
  "komplian",
@@ -1,28 +0,0 @@
1
- # Copia a KOMPLIAN_DATABASE_URLS.env (raíz del monorepo) y rellena.
2
- # No commitees URLs con contraseña. Hosts: Neon (*.neon.tech).
3
-
4
- # --- App DB (Prisma en app/, raw SQL en api/) ---
5
- KOMPLIAN_DATABASE_URL_APP_DEVELOPMENT=
6
- KOMPLIAN_DATABASE_URL_APP_STAGING=
7
- KOMPLIAN_DATABASE_URL_APP_PRODUCTION=
8
-
9
- # --- Admin DB ---
10
- KOMPLIAN_DATABASE_URL_ADMIN_DEVELOPMENT=
11
- KOMPLIAN_DATABASE_URL_ADMIN_STAGING=
12
- KOMPLIAN_DATABASE_URL_ADMIN_PRODUCTION=
13
-
14
- # --- Web / pilot DB ---
15
- KOMPLIAN_DATABASE_URL_WEB_DEVELOPMENT=postgresql://neondb_owner:npg_1crYb3PaCjlf@ep-frosty-bird-agql4oq5-pooler.c-2.eu-central-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require
16
- KOMPLIAN_DATABASE_URL_WEB_STAGING=
17
- KOMPLIAN_DATABASE_URL_WEB_PRODUCTION=
18
-
19
- # Producción: solo @komplian.com; lista separada por comas (opcional; por defecto josue.santana@komplian.com).
20
- # KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST=josue.santana@komplian.com,otro@komplian.com
21
-
22
- # Operador para comprobar acceso a producción (alternativa: git config user.email).
23
- # KOMPLIAN_DATABASE_OPERATOR_EMAIL=josue.santana@komplian.com
24
-
25
- # Staging/producción: host debe ser Neon (*.neon.tech), salvo:
26
- # KOMPLIAN_DATABASE_ALLOW_NON_NEON=1
27
- # Development: Postgres local está permitido (mensaje informativo). Para exigir Neon también en dev:
28
- # KOMPLIAN_DATABASE_REQUIRE_NEON_DEVELOPMENT=1