komplian 0.5.2 → 0.6.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 CHANGED
@@ -6,6 +6,8 @@
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
+ **Orden típico del equipo:** onboard → `postman login` / `postman --yes` → `mcp-tools --yes` → **`db:all:dev`** → `localhost --yes`. Ver **`ONBOARDING.md`**.
10
+
9
11
  **`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.
10
12
 
11
13
  ### Postman (colección + entornos)
@@ -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,23 +54,26 @@ 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 assertNotPlaceholderDbUrl(url, platform, environment) {
60
- if (!url.includes(LOCALHOST_DB_PLACEHOLDER)) return;
61
- const key = platformToDbKey(platform);
57
+ export function isPlaceholderDbUrl(url) {
58
+ return !!(url && String(url).includes(LOCALHOST_DB_PLACEHOLDER));
59
+ }
60
+
61
+ /** Primera URL no vacía que no sea el placeholder de localhost. */
62
+ export function pickFirstNonPlaceholder(...candidates) {
63
+ for (const c of candidates) {
64
+ const u = (c ?? "").trim();
65
+ if (u && !isPlaceholderDbUrl(u)) return u;
66
+ }
67
+ return "";
68
+ }
69
+
70
+ function assertNotPlaceholderDbUrl(url) {
71
+ if (!isPlaceholderDbUrl(url)) return;
62
72
  log(
63
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).`
64
74
  );
65
75
  log(``);
66
- log(` Pon una URL real en la raíz del monorepo, por ejemplo:`);
67
- log(
68
- ` ${c.dim}KOMPLIAN_LOCALHOST_${key}_DATABASE_URL=postgresql://…${c.reset}`
69
- );
70
- log(
71
- ` o ${c.dim}KOMPLIAN_DATABASE_URL_${key}_${environment.toUpperCase()}=…${c.reset} en ${c.dim}KOMPLIAN_DATABASE_URLS.env${c.reset}`
72
- );
73
- log(
74
- ` (${c.dim}KOMPLIAN_LOCALHOST_SECRETS.env${c.reset} / ${c.dim}.komplian/${c.reset}…). Luego vuelve a ejecutar el comando.`
75
- );
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).`);
76
77
  process.exit(1);
77
78
  }
78
79
 
@@ -96,25 +97,21 @@ function parseEnvFile(content) {
96
97
  return out;
97
98
  }
98
99
 
99
- function loadMergedEnvFiles(workspaceRoot) {
100
- const out = {};
101
- const paths = [
102
- join(workspaceRoot, "KOMPLIAN_DATABASE_URLS.env"),
103
- join(workspaceRoot, ".komplian", "KOMPLIAN_DATABASE_URLS.env"),
104
- join(workspaceRoot, "KOMPLIAN_LOCALHOST_SECRETS.env"),
105
- join(workspaceRoot, ".komplian", "KOMPLIAN_LOCALHOST_SECRETS.env"),
106
- join(workspaceRoot, "komplian-localhost.secrets.env"),
107
- join(workspaceRoot, ".komplian", "localhost-secrets.env"),
108
- ];
109
- for (const p of paths) {
110
- if (!existsSync(p)) continue;
111
- try {
112
- Object.assign(out, parseEnvFile(readFileSync(p, "utf8")));
113
- } catch {
114
- /* ignore */
115
- }
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;
116
114
  }
117
- return out;
118
115
  }
119
116
 
120
117
  function readEnvLocalKey(workspaceRoot, project, keys) {
@@ -149,7 +146,7 @@ function findWorkspaceRoot(start) {
149
146
  return resolve(start);
150
147
  }
151
148
 
152
- function maskDatabaseUrl(url) {
149
+ export function maskDatabaseUrl(url) {
153
150
  try {
154
151
  const u = new URL(url);
155
152
  if (u.password) u.password = "***";
@@ -168,7 +165,7 @@ function platformToDbKey(platform) {
168
165
  return null;
169
166
  }
170
167
 
171
- function loadProductionAllowlist(workspaceRoot) {
168
+ function loadProductionAllowlist() {
172
169
  const raw = process.env.KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST?.trim();
173
170
  if (raw) {
174
171
  return raw
@@ -176,21 +173,6 @@ function loadProductionAllowlist(workspaceRoot) {
176
173
  .map((s) => s.trim().toLowerCase())
177
174
  .filter(Boolean);
178
175
  }
179
- const p = join(
180
- workspaceRoot,
181
- ".komplian",
182
- "KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST"
183
- );
184
- if (existsSync(p)) {
185
- try {
186
- return readFileSync(p, "utf8")
187
- .split(/\r?\n/)
188
- .map((l) => l.trim().toLowerCase())
189
- .filter((l) => l && !l.startsWith("#"));
190
- } catch {
191
- /* ignore */
192
- }
193
- }
194
176
  return DEFAULT_PRODUCTION_ALLOWLIST.map((e) => e.toLowerCase());
195
177
  }
196
178
 
@@ -245,7 +227,7 @@ async function resolveOperatorEmail() {
245
227
  return (ans || "").trim();
246
228
  }
247
229
 
248
- function assertProductionAccess(email, allowlist) {
230
+ function assertProductionAccessLocalAllowlist(email, allowlist) {
249
231
  const e = email.toLowerCase();
250
232
  if (!e || !e.endsWith("@komplian.com")) {
251
233
  log(
@@ -255,51 +237,123 @@ function assertProductionAccess(email, allowlist) {
255
237
  }
256
238
  if (!allowlist.includes(e)) {
257
239
  log(
258
- `${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}.`
259
241
  );
260
242
  process.exit(1);
261
243
  }
262
244
  }
263
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
+
264
298
  function resolveDatabaseUrl(workspaceRoot, platform, environment) {
265
299
  const dbKey = platformToDbKey(platform);
266
300
  if (!dbKey) return "";
267
- const file = loadMergedEnvFiles(workspaceRoot);
268
301
  const envU = environment.toUpperCase();
269
302
  const primaryKey = `KOMPLIAN_DATABASE_URL_${dbKey}_${envU}`;
303
+ const home = loadHomeDevDatabases();
270
304
 
271
- let url =
272
- process.env[primaryKey]?.trim() || file[primaryKey]?.trim() || "";
273
-
274
- if (environment === "development" && !url) {
275
- const single =
276
- process.env.KOMPLIAN_LOCALHOST_DATABASE_URL?.trim() ||
277
- file.KOMPLIAN_LOCALHOST_DATABASE_URL?.trim();
278
- if (dbKey === "APP") {
279
- url =
280
- process.env.KOMPLIAN_LOCALHOST_APP_DATABASE_URL?.trim() ||
281
- file.KOMPLIAN_LOCALHOST_APP_DATABASE_URL?.trim() ||
282
- single ||
283
- readEnvLocalKey(workspaceRoot, "app", ["DATABASE_URL"]) ||
284
- readEnvLocalKey(workspaceRoot, "api", ["APP_DATABASE_URL"]);
285
- } else if (dbKey === "ADMIN") {
286
- url =
287
- process.env.KOMPLIAN_LOCALHOST_ADMIN_DATABASE_URL?.trim() ||
288
- file.KOMPLIAN_LOCALHOST_ADMIN_DATABASE_URL?.trim() ||
289
- single ||
290
- readEnvLocalKey(workspaceRoot, "admin", ["DATABASE_URL"]) ||
291
- readEnvLocalKey(workspaceRoot, "api", ["ADMIN_DATABASE_URL"]);
292
- } else if (dbKey === "WEB") {
293
- url =
294
- process.env.KOMPLIAN_LOCALHOST_WEB_DATABASE_URL?.trim() ||
295
- file.KOMPLIAN_LOCALHOST_WEB_DATABASE_URL?.trim() ||
296
- single ||
297
- readEnvLocalKey(workspaceRoot, "web", ["DATABASE_URL"]) ||
298
- readEnvLocalKey(workspaceRoot, "api", ["WEB_DATABASE_URL"]);
299
- }
305
+ if (environment !== "development") {
306
+ return pickFirstNonPlaceholder(process.env[primaryKey]);
307
+ }
308
+
309
+ const single = (process.env.KOMPLIAN_LOCALHOST_DATABASE_URL || "").trim();
310
+
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"])
326
+ );
300
327
  }
301
328
 
302
- 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 "";
303
357
  }
304
358
 
305
359
  function findPsqlBinary() {
@@ -376,15 +430,15 @@ function usageDb() {
376
430
  log(` Plataformas: ${c.dim}app | admin | web | api-app | api-admin | api-web${c.reset}`);
377
431
  log(` Entornos: ${c.dim}development | staging | production${c.reset}`);
378
432
  log(``);
379
- log(` Variables KOMPLIAN (recomendado):`);
380
- log(` ${c.dim}KOMPLIAN_DATABASE_URL_APP_DEVELOPMENT${c.reset}`);
381
- log(` ${c.dim}KOMPLIAN_DATABASE_URL_APP_STAGING / _PRODUCTION${c.reset} (idem ADMIN, WEB)`);
382
- 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).`);
383
436
  log(` Neon: ${c.dim}staging/producción${c.reset} exigen host *.neon.tech; ${c.dim}development${c.reset} permite Postgres local (aviso).`);
384
437
  log(``);
385
438
  log(` Producción:`);
386
- log(` ${c.dim}KOMPLIAN_DATABASE_OPERATOR_EMAIL${c.reset} o git user.email`);
387
- 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.`);
388
442
  log(``);
389
443
  log(` -c, --command SQL no interactivo`);
390
444
  log(` -u, --url URL explícita (sigue reglas Neon / producción)`);
@@ -417,14 +471,12 @@ export async function runDb(argv) {
417
471
  ? resolve(opts.workspace.replace(/^~(?=$|[/\\])/, process.env.HOME || ""))
418
472
  : findWorkspaceRoot(process.cwd());
419
473
 
420
- const allowlist = loadProductionAllowlist(workspaceRoot);
421
-
422
474
  if (opts.environment === "production") {
423
475
  const email = await resolveOperatorEmail();
424
- assertProductionAccess(email, allowlist);
476
+ await assertProductionAccessResolved(email, opts.quiet);
425
477
  if (!opts.quiet) {
426
478
  log(
427
- `${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}`
428
480
  );
429
481
  }
430
482
  }
@@ -443,13 +495,23 @@ export async function runDb(argv) {
443
495
 
444
496
  if (!url) {
445
497
  log(`${c.red}✗${c.reset} Sin URL para ${opts.platform} / ${opts.environment}.`);
446
- log(
447
- ` Define ${c.bold}KOMPLIAN_DATABASE_URL_${platformToDbKey(opts.platform)}_${opts.environment.toUpperCase()}${c.reset} o ${c.dim}KOMPLIAN_DATABASE_URLS.env${c.reset}.`
448
- );
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 {
507
+ log(
508
+ ` Exporta ${c.bold}KOMPLIAN_DATABASE_URL_${platformToDbKey(opts.platform)}_${opts.environment.toUpperCase()}${c.reset} en el entorno (CI / operador).`
509
+ );
510
+ }
449
511
  process.exit(1);
450
512
  }
451
513
 
452
- assertNotPlaceholderDbUrl(url, opts.platform, opts.environment);
514
+ assertNotPlaceholderDbUrl(url);
453
515
 
454
516
  assertNeonHost(url, opts.environment);
455
517
 
@@ -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);
@@ -938,3 +938,20 @@ export async function runPostman(argv) {
938
938
  );
939
939
  }
940
940
  }
941
+
942
+ /**
943
+ * Misma verificación que `postman --yes`: GET /me y dominio @komplian.com.
944
+ * Requiere `npx komplian postman login` (o POSTMAN_API_KEY en la sesión).
945
+ */
946
+ export async function assertKomplianEmployeePostman() {
947
+ const domain = (process.env.KOMPLIAN_EMAIL_DOMAIN || "komplian.com").trim();
948
+ const { key, source } = resolveApiKey();
949
+ if (!key) {
950
+ printMissingKeyHelp();
951
+ process.exit(1);
952
+ }
953
+ if (source && source !== "POSTMAN_API_KEY") {
954
+ log(`${c.dim}→ Postman API key desde: ${formatHomePath(source)}${c.reset}`);
955
+ }
956
+ return verifyKomplianEmail(key, domain);
957
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "komplian",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
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