komplian 0.4.8 → 0.5.2

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.
@@ -0,0 +1,28 @@
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
@@ -0,0 +1,492 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Komplian db — psql contra Neon (PostgreSQL). Convención KOMPLIAN_* para URLs y acceso.
4
+ *
5
+ * Sintaxis recomendada:
6
+ * npx komplian db:app:development
7
+ * npx komplian db:admin:staging
8
+ * npx komplian db:web:production
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
15
+ *
16
+ * Producción: solo emails @komplian.com en KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST
17
+ * (por defecto josue.santana@komplian.com). Operador: KOMPLIAN_DATABASE_OPERATOR_EMAIL o git config user.email.
18
+ *
19
+ * Neon: staging/producción exigen *.neon.tech (salvo KOMPLIAN_DATABASE_ALLOW_NON_NEON=1).
20
+ * Development: permite host no Neon (Postgres local); aviso en consola. Forzar Neon en dev: KOMPLIAN_DATABASE_REQUIRE_NEON_DEVELOPMENT=1
21
+ */
22
+
23
+ import { spawn, spawnSync } from "node:child_process";
24
+ import { existsSync, readFileSync } from "node:fs";
25
+ import { dirname, join, resolve } from "node:path";
26
+ import { createInterface } from "node:readline/promises";
27
+ import { stdin as input, stdout as output } from "node:process";
28
+
29
+ const c = {
30
+ reset: "\x1b[0m",
31
+ dim: "\x1b[2m",
32
+ bold: "\x1b[1m",
33
+ cyan: "\x1b[36m",
34
+ red: "\x1b[31m",
35
+ yellow: "\x1b[33m",
36
+ };
37
+
38
+ function log(s = "") {
39
+ console.log(s);
40
+ }
41
+
42
+ const PLATFORMS = new Set([
43
+ "app",
44
+ "admin",
45
+ "web",
46
+ "api-app",
47
+ "api-admin",
48
+ "api-web",
49
+ ]);
50
+
51
+ const ENVIRONMENTS = new Set(["development", "staging", "production"]);
52
+
53
+ /** Lista por defecto si no configuras KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST */
54
+ const DEFAULT_PRODUCTION_ALLOWLIST = ["josue.santana@komplian.com"];
55
+
56
+ /** Misma cadena que `komplian-localhost.mjs` cuando no hay URL real. */
57
+ const LOCALHOST_DB_PLACEHOLDER = "komplian_localhost_placeholder";
58
+
59
+ function assertNotPlaceholderDbUrl(url, platform, environment) {
60
+ if (!url.includes(LOCALHOST_DB_PLACEHOLDER)) return;
61
+ const key = platformToDbKey(platform);
62
+ log(
63
+ `${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
+ );
65
+ 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
+ process.exit(1);
77
+ }
78
+
79
+ function parseEnvFile(content) {
80
+ const out = {};
81
+ for (const line of content.split(/\r?\n/)) {
82
+ const t = line.trim();
83
+ if (!t || t.startsWith("#")) continue;
84
+ const eq = t.indexOf("=");
85
+ if (eq === -1) continue;
86
+ const key = t.slice(0, eq).trim();
87
+ let val = t.slice(eq + 1).trim();
88
+ if (
89
+ (val.startsWith('"') && val.endsWith('"')) ||
90
+ (val.startsWith("'") && val.endsWith("'"))
91
+ ) {
92
+ val = val.slice(1, -1);
93
+ }
94
+ out[key] = val;
95
+ }
96
+ return out;
97
+ }
98
+
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
+ }
116
+ }
117
+ return out;
118
+ }
119
+
120
+ function readEnvLocalKey(workspaceRoot, project, keys) {
121
+ const p = join(workspaceRoot, project, ".env.local");
122
+ if (!existsSync(p)) return "";
123
+ let map;
124
+ try {
125
+ map = parseEnvFile(readFileSync(p, "utf8"));
126
+ } catch {
127
+ return "";
128
+ }
129
+ for (const k of keys) {
130
+ const v = map[k];
131
+ if (v && String(v).trim()) return String(v).trim();
132
+ }
133
+ return "";
134
+ }
135
+
136
+ function findWorkspaceRoot(start) {
137
+ let dir = resolve(start);
138
+ for (let i = 0; i < 8; i++) {
139
+ if (
140
+ existsSync(join(dir, "api", "package.json")) &&
141
+ existsSync(join(dir, "app", "package.json"))
142
+ ) {
143
+ return dir;
144
+ }
145
+ const parent = dirname(dir);
146
+ if (parent === dir) break;
147
+ dir = parent;
148
+ }
149
+ return resolve(start);
150
+ }
151
+
152
+ function maskDatabaseUrl(url) {
153
+ try {
154
+ const u = new URL(url);
155
+ if (u.password) u.password = "***";
156
+ return u.toString();
157
+ } catch {
158
+ return "[URL inválida]";
159
+ }
160
+ }
161
+
162
+ /** api-app → APP (misma base que app). */
163
+ function platformToDbKey(platform) {
164
+ const p = platform.toLowerCase();
165
+ if (p === "app" || p === "api-app") return "APP";
166
+ if (p === "admin" || p === "api-admin") return "ADMIN";
167
+ if (p === "web" || p === "api-web") return "WEB";
168
+ return null;
169
+ }
170
+
171
+ function loadProductionAllowlist(workspaceRoot) {
172
+ const raw = process.env.KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST?.trim();
173
+ if (raw) {
174
+ return raw
175
+ .split(",")
176
+ .map((s) => s.trim().toLowerCase())
177
+ .filter(Boolean);
178
+ }
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
+ return DEFAULT_PRODUCTION_ALLOWLIST.map((e) => e.toLowerCase());
195
+ }
196
+
197
+ function assertNeonHost(url, environment) {
198
+ if (process.env.KOMPLIAN_DATABASE_ALLOW_NON_NEON === "1") return;
199
+ try {
200
+ const u = new URL(url);
201
+ const h = u.hostname.toLowerCase();
202
+ const isNeon = h.includes("neon.tech");
203
+ if (isNeon) return;
204
+
205
+ if (environment === "development") {
206
+ if (process.env.KOMPLIAN_DATABASE_REQUIRE_NEON_DEVELOPMENT === "1") {
207
+ log(
208
+ `${c.red}✗${c.reset} Development: host no Neon (${u.hostname}). Quita ${c.dim}KOMPLIAN_DATABASE_REQUIRE_NEON_DEVELOPMENT${c.reset} o usa URL *.neon.tech.`
209
+ );
210
+ process.exit(1);
211
+ }
212
+ log(
213
+ `${c.dim}○${c.reset} Development: conexión no Neon (${u.hostname}). Staging/producción siguen exigiendo *.neon.tech.`
214
+ );
215
+ return;
216
+ }
217
+
218
+ log(
219
+ `${c.red}✗${c.reset} Solo hosts Neon (postgresql://*.neon.tech). Para excepciones: ${c.dim}KOMPLIAN_DATABASE_ALLOW_NON_NEON=1${c.reset}`
220
+ );
221
+ process.exit(1);
222
+ } catch {
223
+ log(`${c.red}✗${c.reset} URL de base de datos inválida.`);
224
+ process.exit(1);
225
+ }
226
+ }
227
+
228
+ async function resolveOperatorEmail() {
229
+ const fromEnv = process.env.KOMPLIAN_DATABASE_OPERATOR_EMAIL?.trim();
230
+ if (fromEnv) return fromEnv;
231
+ try {
232
+ const r = spawnSync("git", ["config", "user.email"], {
233
+ encoding: "utf8",
234
+ shell: false,
235
+ });
236
+ if (r.status === 0 && r.stdout?.trim()) return r.stdout.trim();
237
+ } catch {
238
+ /* ignore */
239
+ }
240
+ const rl = createInterface({ input, output });
241
+ const ans = await rl.question(
242
+ `${c.bold}Email operador (@komplian.com) para acceso producción:${c.reset} `
243
+ );
244
+ rl.close();
245
+ return (ans || "").trim();
246
+ }
247
+
248
+ function assertProductionAccess(email, allowlist) {
249
+ const e = email.toLowerCase();
250
+ if (!e || !e.endsWith("@komplian.com")) {
251
+ log(
252
+ `${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}.`
253
+ );
254
+ process.exit(1);
255
+ }
256
+ if (!allowlist.includes(e)) {
257
+ 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}.`
259
+ );
260
+ process.exit(1);
261
+ }
262
+ }
263
+
264
+ function resolveDatabaseUrl(workspaceRoot, platform, environment) {
265
+ const dbKey = platformToDbKey(platform);
266
+ if (!dbKey) return "";
267
+ const file = loadMergedEnvFiles(workspaceRoot);
268
+ const envU = environment.toUpperCase();
269
+ const primaryKey = `KOMPLIAN_DATABASE_URL_${dbKey}_${envU}`;
270
+
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
+ }
300
+ }
301
+
302
+ return url;
303
+ }
304
+
305
+ function findPsqlBinary() {
306
+ const candidates = [
307
+ "psql",
308
+ "/opt/homebrew/opt/libpq/bin/psql",
309
+ "/usr/local/opt/libpq/bin/psql",
310
+ ];
311
+ for (const p of candidates) {
312
+ if (p !== "psql" && !existsSync(p)) continue;
313
+ const r = spawnSync(p, ["--version"], {
314
+ encoding: "utf8",
315
+ shell: false,
316
+ windowsHide: true,
317
+ });
318
+ if (!r.error && r.status === 0) return p;
319
+ }
320
+ return null;
321
+ }
322
+
323
+ function parseDbArgs(argv) {
324
+ const opts = {
325
+ platform: "app",
326
+ environment: "development",
327
+ workspace: "",
328
+ command: "",
329
+ url: "",
330
+ quiet: false,
331
+ help: false,
332
+ };
333
+
334
+ let rest = argv;
335
+ if (
336
+ argv.length >= 2 &&
337
+ PLATFORMS.has(argv[0].toLowerCase()) &&
338
+ ENVIRONMENTS.has(argv[1].toLowerCase())
339
+ ) {
340
+ opts.platform = argv[0].toLowerCase();
341
+ opts.environment = argv[1].toLowerCase();
342
+ rest = argv.slice(2);
343
+ }
344
+
345
+ for (let i = 0; i < rest.length; i++) {
346
+ const a = rest[i];
347
+ if (a === "-h" || a === "--help") opts.help = true;
348
+ else if (a === "--quiet" || a === "-q") opts.quiet = true;
349
+ else if (a === "--target" || a === "--platform" || a === "-t") {
350
+ opts.platform = (rest[++i] || "app").toLowerCase();
351
+ } else if (a === "--env" || a === "-e") {
352
+ opts.environment = (rest[++i] || "development").toLowerCase();
353
+ } else if (a === "--workspace" || a === "-w") {
354
+ opts.workspace = rest[++i] || "";
355
+ } else if (a === "-c" || a === "--command") {
356
+ opts.command = rest[++i] || "";
357
+ } else if (a === "--url" || a === "-u") {
358
+ opts.url = rest[++i] || "";
359
+ } else if (a.startsWith("-")) {
360
+ log(`${c.red}✗${c.reset} Opción desconocida: ${a}`);
361
+ process.exit(1);
362
+ } else if (!opts.workspace) opts.workspace = a;
363
+ }
364
+
365
+ return opts;
366
+ }
367
+
368
+ function usageDb() {
369
+ log(`Uso:`);
370
+ log(` ${c.bold}npx komplian db:app:development${c.reset}`);
371
+ log(` ${c.bold}npx komplian db:admin:staging${c.reset}`);
372
+ log(` ${c.bold}npx komplian db:web:production${c.reset} ${c.dim}(solo @komplian.com + allowlist)${c.reset}`);
373
+ log(` npx komplian db --platform app --env development`);
374
+ log(` npx komplian db -c "SELECT 1" --platform web --env development`);
375
+ log(``);
376
+ log(` Plataformas: ${c.dim}app | admin | web | api-app | api-admin | api-web${c.reset}`);
377
+ log(` Entornos: ${c.dim}development | staging | production${c.reset}`);
378
+ 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)`);
383
+ log(` Neon: ${c.dim}staging/producción${c.reset} exigen host *.neon.tech; ${c.dim}development${c.reset} permite Postgres local (aviso).`);
384
+ log(``);
385
+ 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`);
388
+ log(``);
389
+ log(` -c, --command SQL no interactivo`);
390
+ log(` -u, --url URL explícita (sigue reglas Neon / producción)`);
391
+ log(` -w, --workspace Raíz monorepo`);
392
+ log(` -q, --quiet`);
393
+ log(` -h, --help`);
394
+ log(``);
395
+ log(` Requisito: ${c.bold}psql${c.reset} (${c.dim}brew install libpq${c.reset} en macOS).`);
396
+ }
397
+
398
+ export async function runDb(argv) {
399
+ const opts = parseDbArgs(argv);
400
+ if (opts.help) {
401
+ usageDb();
402
+ return;
403
+ }
404
+
405
+ if (!PLATFORMS.has(opts.platform)) {
406
+ log(`${c.red}✗${c.reset} Plataforma inválida: ${opts.platform}`);
407
+ usageDb();
408
+ process.exit(1);
409
+ }
410
+ if (!ENVIRONMENTS.has(opts.environment)) {
411
+ log(`${c.red}✗${c.reset} Entorno inválido: ${opts.environment}`);
412
+ usageDb();
413
+ process.exit(1);
414
+ }
415
+
416
+ const workspaceRoot = opts.workspace.trim()
417
+ ? resolve(opts.workspace.replace(/^~(?=$|[/\\])/, process.env.HOME || ""))
418
+ : findWorkspaceRoot(process.cwd());
419
+
420
+ const allowlist = loadProductionAllowlist(workspaceRoot);
421
+
422
+ if (opts.environment === "production") {
423
+ const email = await resolveOperatorEmail();
424
+ assertProductionAccess(email, allowlist);
425
+ if (!opts.quiet) {
426
+ log(
427
+ `${c.yellow}⚠${c.reset} Producción · operador ${c.bold}${email.toLowerCase()}${c.reset} (allowlist Komplian)`
428
+ );
429
+ }
430
+ }
431
+
432
+ if (
433
+ !(opts.url || "").trim() &&
434
+ !existsSync(join(workspaceRoot, "api", "package.json"))
435
+ ) {
436
+ log(`${c.red}✗${c.reset} No parece un monorepo Komplian: ${workspaceRoot}`);
437
+ process.exit(1);
438
+ }
439
+
440
+ let url =
441
+ (opts.url || "").trim() ||
442
+ resolveDatabaseUrl(workspaceRoot, opts.platform, opts.environment);
443
+
444
+ if (!url) {
445
+ 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
+ );
449
+ process.exit(1);
450
+ }
451
+
452
+ assertNotPlaceholderDbUrl(url, opts.platform, opts.environment);
453
+
454
+ assertNeonHost(url, opts.environment);
455
+
456
+ const psql = findPsqlBinary();
457
+ if (!psql) {
458
+ log(`${c.red}✗${c.reset} No se encontró ${c.bold}psql${c.reset}.`);
459
+ log(` macOS: ${c.dim}brew install libpq${c.reset}`);
460
+ process.exit(1);
461
+ }
462
+
463
+ if (!opts.quiet) {
464
+ log(
465
+ `${c.cyan}→${c.reset} ${c.bold}${opts.platform}${c.reset} / ${c.bold}${opts.environment}${c.reset} ${maskDatabaseUrl(url)}`
466
+ );
467
+ log(`${c.dim} ${psql}${c.reset}`);
468
+ }
469
+
470
+ const args = [url];
471
+ if (opts.command) {
472
+ args.push("-c", opts.command, "-X");
473
+ }
474
+
475
+ const child = spawn(psql, args, {
476
+ stdio: "inherit",
477
+ shell: false,
478
+ env: { ...process.env, PAGER: process.env.PAGER || "less" },
479
+ });
480
+
481
+ child.on("error", (err) => {
482
+ if (err && err.code === "ENOENT") {
483
+ log(`${c.red}✗${c.reset} No se pudo ejecutar psql.`);
484
+ process.exit(1);
485
+ }
486
+ throw err;
487
+ });
488
+
489
+ child.on("exit", (code) => {
490
+ process.exit(code ?? 0);
491
+ });
492
+ }
@@ -394,12 +394,13 @@ function npmInstallEach(workspace) {
394
394
  }
395
395
 
396
396
  function usage() {
397
- log(`Uso: komplian onboard | postman | localhost | mcp-tools [opciones]`);
397
+ log(`Uso: komplian onboard | postman | localhost | mcp-tools | db [opciones]`);
398
398
  log(` npx komplian onboard --yes`);
399
399
  log(` npx komplian postman login ${c.dim}(una vez · guarda API key)${c.reset}`);
400
400
  log(` npx komplian postman --yes ${c.dim}(email @komplian.com)${c.reset}`);
401
401
  log(` npx komplian localhost --yes ${c.dim}(env local + api app web admin docs)${c.reset}`);
402
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}`);
403
404
  log(``);
404
405
  log(` Antes (una vez): gh auth login -h github.com -s repo -s read:org -w`);
405
406
  log(` Requisitos: Node 18+, git, GitHub CLI (gh)`);
@@ -498,6 +499,25 @@ async function main() {
498
499
  await runMcpTools(rawArgv.slice(1));
499
500
  return;
500
501
  }
502
+ if (rawArgv[0]?.startsWith("db:")) {
503
+ const { runDb } = await import("./komplian-db.mjs");
504
+ const seg = rawArgv[0].split(":").filter(Boolean);
505
+ if (seg.length === 3 && seg[0] === "db") {
506
+ await runDb([
507
+ seg[1].toLowerCase(),
508
+ seg[2].toLowerCase(),
509
+ ...rawArgv.slice(1),
510
+ ]);
511
+ return;
512
+ }
513
+ log(`${c.red}✗${c.reset} Uso: npx komplian db:app:development`);
514
+ process.exit(1);
515
+ }
516
+ if (rawArgv[0] === "db") {
517
+ const { runDb } = await import("./komplian-db.mjs");
518
+ await runDb(rawArgv.slice(1));
519
+ return;
520
+ }
501
521
 
502
522
  const configPath = join(__dirname, "komplian-team-repos.json");
503
523
  const { argv, fromOnboardSubcommand } = normalizeArgv(rawArgv);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "komplian",
3
- "version": "0.4.8",
4
- "description": "Komplian CLI: onboard, Postman, localhost, mcp-tools (Cursor MCP). Node 18+. Published tarball has no .env / secrets.",
3
+ "version": "0.5.2",
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": {
7
7
  "node": ">=18"
@@ -14,6 +14,8 @@
14
14
  "komplian-postman.mjs",
15
15
  "komplian-localhost.mjs",
16
16
  "komplian-mcp-tools.mjs",
17
+ "komplian-db.mjs",
18
+ "KOMPLIAN_DATABASE_URLS.env.example",
17
19
  "komplian-team-repos.json",
18
20
  "README.md"
19
21
  ],
@@ -21,7 +23,7 @@
21
23
  "access": "public"
22
24
  },
23
25
  "scripts": {
24
- "prepublishOnly": "node --check komplian-onboard.mjs && node --check komplian-postman.mjs && node --check komplian-localhost.mjs && node --check komplian-mcp-tools.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"
25
27
  },
26
28
  "keywords": [
27
29
  "komplian",