komplian 0.4.7 → 0.5.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/KOMPLIAN_DATABASE_URLS.env.example +26 -0
- package/komplian-db.mjs +451 -0
- package/komplian-mcp-tools.mjs +28 -15
- package/komplian-onboard.mjs +21 -1
- package/package.json +5 -3
|
@@ -0,0 +1,26 @@
|
|
|
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=
|
|
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
|
+
# Solo si NO usas Neon (p. ej. Postgres local en Docker):
|
|
26
|
+
# KOMPLIAN_DATABASE_ALLOW_NON_NEON=1
|
package/komplian-db.mjs
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
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: host debe ser *.neon.tech salvo KOMPLIAN_DATABASE_ALLOW_NON_NEON=1
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
23
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
24
|
+
import { dirname, join, resolve } from "node:path";
|
|
25
|
+
import { createInterface } from "node:readline/promises";
|
|
26
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
27
|
+
|
|
28
|
+
const c = {
|
|
29
|
+
reset: "\x1b[0m",
|
|
30
|
+
dim: "\x1b[2m",
|
|
31
|
+
bold: "\x1b[1m",
|
|
32
|
+
cyan: "\x1b[36m",
|
|
33
|
+
red: "\x1b[31m",
|
|
34
|
+
yellow: "\x1b[33m",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function log(s = "") {
|
|
38
|
+
console.log(s);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const PLATFORMS = new Set([
|
|
42
|
+
"app",
|
|
43
|
+
"admin",
|
|
44
|
+
"web",
|
|
45
|
+
"api-app",
|
|
46
|
+
"api-admin",
|
|
47
|
+
"api-web",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const ENVIRONMENTS = new Set(["development", "staging", "production"]);
|
|
51
|
+
|
|
52
|
+
/** Lista por defecto si no configuras KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST */
|
|
53
|
+
const DEFAULT_PRODUCTION_ALLOWLIST = ["josue.santana@komplian.com"];
|
|
54
|
+
|
|
55
|
+
function parseEnvFile(content) {
|
|
56
|
+
const out = {};
|
|
57
|
+
for (const line of content.split(/\r?\n/)) {
|
|
58
|
+
const t = line.trim();
|
|
59
|
+
if (!t || t.startsWith("#")) continue;
|
|
60
|
+
const eq = t.indexOf("=");
|
|
61
|
+
if (eq === -1) continue;
|
|
62
|
+
const key = t.slice(0, eq).trim();
|
|
63
|
+
let val = t.slice(eq + 1).trim();
|
|
64
|
+
if (
|
|
65
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
66
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
67
|
+
) {
|
|
68
|
+
val = val.slice(1, -1);
|
|
69
|
+
}
|
|
70
|
+
out[key] = val;
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function loadMergedEnvFiles(workspaceRoot) {
|
|
76
|
+
const out = {};
|
|
77
|
+
const paths = [
|
|
78
|
+
join(workspaceRoot, "KOMPLIAN_DATABASE_URLS.env"),
|
|
79
|
+
join(workspaceRoot, ".komplian", "KOMPLIAN_DATABASE_URLS.env"),
|
|
80
|
+
join(workspaceRoot, "KOMPLIAN_LOCALHOST_SECRETS.env"),
|
|
81
|
+
join(workspaceRoot, ".komplian", "KOMPLIAN_LOCALHOST_SECRETS.env"),
|
|
82
|
+
join(workspaceRoot, "komplian-localhost.secrets.env"),
|
|
83
|
+
join(workspaceRoot, ".komplian", "localhost-secrets.env"),
|
|
84
|
+
];
|
|
85
|
+
for (const p of paths) {
|
|
86
|
+
if (!existsSync(p)) continue;
|
|
87
|
+
try {
|
|
88
|
+
Object.assign(out, parseEnvFile(readFileSync(p, "utf8")));
|
|
89
|
+
} catch {
|
|
90
|
+
/* ignore */
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readEnvLocalKey(workspaceRoot, project, keys) {
|
|
97
|
+
const p = join(workspaceRoot, project, ".env.local");
|
|
98
|
+
if (!existsSync(p)) return "";
|
|
99
|
+
let map;
|
|
100
|
+
try {
|
|
101
|
+
map = parseEnvFile(readFileSync(p, "utf8"));
|
|
102
|
+
} catch {
|
|
103
|
+
return "";
|
|
104
|
+
}
|
|
105
|
+
for (const k of keys) {
|
|
106
|
+
const v = map[k];
|
|
107
|
+
if (v && String(v).trim()) return String(v).trim();
|
|
108
|
+
}
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function findWorkspaceRoot(start) {
|
|
113
|
+
let dir = resolve(start);
|
|
114
|
+
for (let i = 0; i < 8; i++) {
|
|
115
|
+
if (
|
|
116
|
+
existsSync(join(dir, "api", "package.json")) &&
|
|
117
|
+
existsSync(join(dir, "app", "package.json"))
|
|
118
|
+
) {
|
|
119
|
+
return dir;
|
|
120
|
+
}
|
|
121
|
+
const parent = dirname(dir);
|
|
122
|
+
if (parent === dir) break;
|
|
123
|
+
dir = parent;
|
|
124
|
+
}
|
|
125
|
+
return resolve(start);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function maskDatabaseUrl(url) {
|
|
129
|
+
try {
|
|
130
|
+
const u = new URL(url);
|
|
131
|
+
if (u.password) u.password = "***";
|
|
132
|
+
return u.toString();
|
|
133
|
+
} catch {
|
|
134
|
+
return "[URL inválida]";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** api-app → APP (misma base que app). */
|
|
139
|
+
function platformToDbKey(platform) {
|
|
140
|
+
const p = platform.toLowerCase();
|
|
141
|
+
if (p === "app" || p === "api-app") return "APP";
|
|
142
|
+
if (p === "admin" || p === "api-admin") return "ADMIN";
|
|
143
|
+
if (p === "web" || p === "api-web") return "WEB";
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function loadProductionAllowlist(workspaceRoot) {
|
|
148
|
+
const raw = process.env.KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST?.trim();
|
|
149
|
+
if (raw) {
|
|
150
|
+
return raw
|
|
151
|
+
.split(",")
|
|
152
|
+
.map((s) => s.trim().toLowerCase())
|
|
153
|
+
.filter(Boolean);
|
|
154
|
+
}
|
|
155
|
+
const p = join(
|
|
156
|
+
workspaceRoot,
|
|
157
|
+
".komplian",
|
|
158
|
+
"KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST"
|
|
159
|
+
);
|
|
160
|
+
if (existsSync(p)) {
|
|
161
|
+
try {
|
|
162
|
+
return readFileSync(p, "utf8")
|
|
163
|
+
.split(/\r?\n/)
|
|
164
|
+
.map((l) => l.trim().toLowerCase())
|
|
165
|
+
.filter((l) => l && !l.startsWith("#"));
|
|
166
|
+
} catch {
|
|
167
|
+
/* ignore */
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return DEFAULT_PRODUCTION_ALLOWLIST.map((e) => e.toLowerCase());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function assertNeonHost(url) {
|
|
174
|
+
if (process.env.KOMPLIAN_DATABASE_ALLOW_NON_NEON === "1") return;
|
|
175
|
+
try {
|
|
176
|
+
const u = new URL(url);
|
|
177
|
+
const h = u.hostname.toLowerCase();
|
|
178
|
+
if (!h.includes("neon.tech")) {
|
|
179
|
+
log(
|
|
180
|
+
`${c.red}✗${c.reset} Solo hosts Neon (postgresql://*.neon.tech). Para excepciones: ${c.dim}KOMPLIAN_DATABASE_ALLOW_NON_NEON=1${c.reset}`
|
|
181
|
+
);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
log(`${c.red}✗${c.reset} URL de base de datos inválida.`);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function resolveOperatorEmail() {
|
|
191
|
+
const fromEnv = process.env.KOMPLIAN_DATABASE_OPERATOR_EMAIL?.trim();
|
|
192
|
+
if (fromEnv) return fromEnv;
|
|
193
|
+
try {
|
|
194
|
+
const r = spawnSync("git", ["config", "user.email"], {
|
|
195
|
+
encoding: "utf8",
|
|
196
|
+
shell: false,
|
|
197
|
+
});
|
|
198
|
+
if (r.status === 0 && r.stdout?.trim()) return r.stdout.trim();
|
|
199
|
+
} catch {
|
|
200
|
+
/* ignore */
|
|
201
|
+
}
|
|
202
|
+
const rl = createInterface({ input, output });
|
|
203
|
+
const ans = await rl.question(
|
|
204
|
+
`${c.bold}Email operador (@komplian.com) para acceso producción:${c.reset} `
|
|
205
|
+
);
|
|
206
|
+
rl.close();
|
|
207
|
+
return (ans || "").trim();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function assertProductionAccess(email, allowlist) {
|
|
211
|
+
const e = email.toLowerCase();
|
|
212
|
+
if (!e || !e.endsWith("@komplian.com")) {
|
|
213
|
+
log(
|
|
214
|
+
`${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}.`
|
|
215
|
+
);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
if (!allowlist.includes(e)) {
|
|
219
|
+
log(
|
|
220
|
+
`${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}.`
|
|
221
|
+
);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function resolveDatabaseUrl(workspaceRoot, platform, environment) {
|
|
227
|
+
const dbKey = platformToDbKey(platform);
|
|
228
|
+
if (!dbKey) return "";
|
|
229
|
+
const file = loadMergedEnvFiles(workspaceRoot);
|
|
230
|
+
const envU = environment.toUpperCase();
|
|
231
|
+
const primaryKey = `KOMPLIAN_DATABASE_URL_${dbKey}_${envU}`;
|
|
232
|
+
|
|
233
|
+
let url =
|
|
234
|
+
process.env[primaryKey]?.trim() || file[primaryKey]?.trim() || "";
|
|
235
|
+
|
|
236
|
+
if (environment === "development" && !url) {
|
|
237
|
+
const single =
|
|
238
|
+
process.env.KOMPLIAN_LOCALHOST_DATABASE_URL?.trim() ||
|
|
239
|
+
file.KOMPLIAN_LOCALHOST_DATABASE_URL?.trim();
|
|
240
|
+
if (dbKey === "APP") {
|
|
241
|
+
url =
|
|
242
|
+
process.env.KOMPLIAN_LOCALHOST_APP_DATABASE_URL?.trim() ||
|
|
243
|
+
file.KOMPLIAN_LOCALHOST_APP_DATABASE_URL?.trim() ||
|
|
244
|
+
single ||
|
|
245
|
+
readEnvLocalKey(workspaceRoot, "app", ["DATABASE_URL"]) ||
|
|
246
|
+
readEnvLocalKey(workspaceRoot, "api", ["APP_DATABASE_URL"]);
|
|
247
|
+
} else if (dbKey === "ADMIN") {
|
|
248
|
+
url =
|
|
249
|
+
process.env.KOMPLIAN_LOCALHOST_ADMIN_DATABASE_URL?.trim() ||
|
|
250
|
+
file.KOMPLIAN_LOCALHOST_ADMIN_DATABASE_URL?.trim() ||
|
|
251
|
+
single ||
|
|
252
|
+
readEnvLocalKey(workspaceRoot, "admin", ["DATABASE_URL"]) ||
|
|
253
|
+
readEnvLocalKey(workspaceRoot, "api", ["ADMIN_DATABASE_URL"]);
|
|
254
|
+
} else if (dbKey === "WEB") {
|
|
255
|
+
url =
|
|
256
|
+
process.env.KOMPLIAN_LOCALHOST_WEB_DATABASE_URL?.trim() ||
|
|
257
|
+
file.KOMPLIAN_LOCALHOST_WEB_DATABASE_URL?.trim() ||
|
|
258
|
+
single ||
|
|
259
|
+
readEnvLocalKey(workspaceRoot, "web", ["DATABASE_URL"]) ||
|
|
260
|
+
readEnvLocalKey(workspaceRoot, "api", ["WEB_DATABASE_URL"]);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return url;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function findPsqlBinary() {
|
|
268
|
+
const candidates = [
|
|
269
|
+
"psql",
|
|
270
|
+
"/opt/homebrew/opt/libpq/bin/psql",
|
|
271
|
+
"/usr/local/opt/libpq/bin/psql",
|
|
272
|
+
];
|
|
273
|
+
for (const p of candidates) {
|
|
274
|
+
if (p !== "psql" && !existsSync(p)) continue;
|
|
275
|
+
const r = spawnSync(p, ["--version"], {
|
|
276
|
+
encoding: "utf8",
|
|
277
|
+
shell: false,
|
|
278
|
+
windowsHide: true,
|
|
279
|
+
});
|
|
280
|
+
if (!r.error && r.status === 0) return p;
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function parseDbArgs(argv) {
|
|
286
|
+
const opts = {
|
|
287
|
+
platform: "app",
|
|
288
|
+
environment: "development",
|
|
289
|
+
workspace: "",
|
|
290
|
+
command: "",
|
|
291
|
+
url: "",
|
|
292
|
+
quiet: false,
|
|
293
|
+
help: false,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
let rest = argv;
|
|
297
|
+
if (
|
|
298
|
+
argv.length >= 2 &&
|
|
299
|
+
PLATFORMS.has(argv[0].toLowerCase()) &&
|
|
300
|
+
ENVIRONMENTS.has(argv[1].toLowerCase())
|
|
301
|
+
) {
|
|
302
|
+
opts.platform = argv[0].toLowerCase();
|
|
303
|
+
opts.environment = argv[1].toLowerCase();
|
|
304
|
+
rest = argv.slice(2);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
for (let i = 0; i < rest.length; i++) {
|
|
308
|
+
const a = rest[i];
|
|
309
|
+
if (a === "-h" || a === "--help") opts.help = true;
|
|
310
|
+
else if (a === "--quiet" || a === "-q") opts.quiet = true;
|
|
311
|
+
else if (a === "--target" || a === "--platform" || a === "-t") {
|
|
312
|
+
opts.platform = (rest[++i] || "app").toLowerCase();
|
|
313
|
+
} else if (a === "--env" || a === "-e") {
|
|
314
|
+
opts.environment = (rest[++i] || "development").toLowerCase();
|
|
315
|
+
} else if (a === "--workspace" || a === "-w") {
|
|
316
|
+
opts.workspace = rest[++i] || "";
|
|
317
|
+
} else if (a === "-c" || a === "--command") {
|
|
318
|
+
opts.command = rest[++i] || "";
|
|
319
|
+
} else if (a === "--url" || a === "-u") {
|
|
320
|
+
opts.url = rest[++i] || "";
|
|
321
|
+
} else if (a.startsWith("-")) {
|
|
322
|
+
log(`${c.red}✗${c.reset} Opción desconocida: ${a}`);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
} else if (!opts.workspace) opts.workspace = a;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return opts;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function usageDb() {
|
|
331
|
+
log(`Uso:`);
|
|
332
|
+
log(` ${c.bold}npx komplian db:app:development${c.reset}`);
|
|
333
|
+
log(` ${c.bold}npx komplian db:admin:staging${c.reset}`);
|
|
334
|
+
log(` ${c.bold}npx komplian db:web:production${c.reset} ${c.dim}(solo @komplian.com + allowlist)${c.reset}`);
|
|
335
|
+
log(` npx komplian db --platform app --env development`);
|
|
336
|
+
log(` npx komplian db -c "SELECT 1" --platform web --env development`);
|
|
337
|
+
log(``);
|
|
338
|
+
log(` Plataformas: ${c.dim}app | admin | web | api-app | api-admin | api-web${c.reset}`);
|
|
339
|
+
log(` Entornos: ${c.dim}development | staging | production${c.reset}`);
|
|
340
|
+
log(``);
|
|
341
|
+
log(` Variables KOMPLIAN (recomendado):`);
|
|
342
|
+
log(` ${c.dim}KOMPLIAN_DATABASE_URL_APP_DEVELOPMENT${c.reset}`);
|
|
343
|
+
log(` ${c.dim}KOMPLIAN_DATABASE_URL_APP_STAGING / _PRODUCTION${c.reset} (idem ADMIN, WEB)`);
|
|
344
|
+
log(` Archivo opcional: ${c.dim}KOMPLIAN_DATABASE_URLS.env${c.reset} (gitignored)`);
|
|
345
|
+
log(``);
|
|
346
|
+
log(` Producción:`);
|
|
347
|
+
log(` ${c.dim}KOMPLIAN_DATABASE_OPERATOR_EMAIL${c.reset} o git user.email`);
|
|
348
|
+
log(` ${c.dim}KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST${c.reset} (coma) o .komplian/KOMPLIAN_DATABASE_PRODUCTION_ALLOWLIST`);
|
|
349
|
+
log(``);
|
|
350
|
+
log(` -c, --command SQL no interactivo`);
|
|
351
|
+
log(` -u, --url URL explícita (sigue reglas Neon / producción)`);
|
|
352
|
+
log(` -w, --workspace Raíz monorepo`);
|
|
353
|
+
log(` -q, --quiet`);
|
|
354
|
+
log(` -h, --help`);
|
|
355
|
+
log(``);
|
|
356
|
+
log(` Requisito: ${c.bold}psql${c.reset} (${c.dim}brew install libpq${c.reset} en macOS).`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export async function runDb(argv) {
|
|
360
|
+
const opts = parseDbArgs(argv);
|
|
361
|
+
if (opts.help) {
|
|
362
|
+
usageDb();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (!PLATFORMS.has(opts.platform)) {
|
|
367
|
+
log(`${c.red}✗${c.reset} Plataforma inválida: ${opts.platform}`);
|
|
368
|
+
usageDb();
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
if (!ENVIRONMENTS.has(opts.environment)) {
|
|
372
|
+
log(`${c.red}✗${c.reset} Entorno inválido: ${opts.environment}`);
|
|
373
|
+
usageDb();
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const workspaceRoot = opts.workspace.trim()
|
|
378
|
+
? resolve(opts.workspace.replace(/^~(?=$|[/\\])/, process.env.HOME || ""))
|
|
379
|
+
: findWorkspaceRoot(process.cwd());
|
|
380
|
+
|
|
381
|
+
const allowlist = loadProductionAllowlist(workspaceRoot);
|
|
382
|
+
|
|
383
|
+
if (opts.environment === "production") {
|
|
384
|
+
const email = await resolveOperatorEmail();
|
|
385
|
+
assertProductionAccess(email, allowlist);
|
|
386
|
+
if (!opts.quiet) {
|
|
387
|
+
log(
|
|
388
|
+
`${c.yellow}⚠${c.reset} Producción · operador ${c.bold}${email.toLowerCase()}${c.reset} (allowlist Komplian)`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (
|
|
394
|
+
!(opts.url || "").trim() &&
|
|
395
|
+
!existsSync(join(workspaceRoot, "api", "package.json"))
|
|
396
|
+
) {
|
|
397
|
+
log(`${c.red}✗${c.reset} No parece un monorepo Komplian: ${workspaceRoot}`);
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
let url =
|
|
402
|
+
(opts.url || "").trim() ||
|
|
403
|
+
resolveDatabaseUrl(workspaceRoot, opts.platform, opts.environment);
|
|
404
|
+
|
|
405
|
+
if (!url) {
|
|
406
|
+
log(`${c.red}✗${c.reset} Sin URL para ${opts.platform} / ${opts.environment}.`);
|
|
407
|
+
log(
|
|
408
|
+
` Define ${c.bold}KOMPLIAN_DATABASE_URL_${platformToDbKey(opts.platform)}_${opts.environment.toUpperCase()}${c.reset} o ${c.dim}KOMPLIAN_DATABASE_URLS.env${c.reset}.`
|
|
409
|
+
);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
assertNeonHost(url);
|
|
414
|
+
|
|
415
|
+
const psql = findPsqlBinary();
|
|
416
|
+
if (!psql) {
|
|
417
|
+
log(`${c.red}✗${c.reset} No se encontró ${c.bold}psql${c.reset}.`);
|
|
418
|
+
log(` macOS: ${c.dim}brew install libpq${c.reset}`);
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (!opts.quiet) {
|
|
423
|
+
log(
|
|
424
|
+
`${c.cyan}→${c.reset} ${c.bold}${opts.platform}${c.reset} / ${c.bold}${opts.environment}${c.reset} ${maskDatabaseUrl(url)}`
|
|
425
|
+
);
|
|
426
|
+
log(`${c.dim} ${psql}${c.reset}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const args = [url];
|
|
430
|
+
if (opts.command) {
|
|
431
|
+
args.push("-c", opts.command, "-X");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const child = spawn(psql, args, {
|
|
435
|
+
stdio: "inherit",
|
|
436
|
+
shell: false,
|
|
437
|
+
env: { ...process.env, PAGER: process.env.PAGER || "less" },
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
child.on("error", (err) => {
|
|
441
|
+
if (err && err.code === "ENOENT") {
|
|
442
|
+
log(`${c.red}✗${c.reset} No se pudo ejecutar psql.`);
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
throw err;
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
child.on("exit", (code) => {
|
|
449
|
+
process.exit(code ?? 0);
|
|
450
|
+
});
|
|
451
|
+
}
|
package/komplian-mcp-tools.mjs
CHANGED
|
@@ -38,6 +38,15 @@ const KOMPLIAN_MCP_PRESET = {
|
|
|
38
38
|
GITHUB_PERSONAL_ACCESS_TOKEN: "",
|
|
39
39
|
},
|
|
40
40
|
},
|
|
41
|
+
"KOMPLIAN-atlassian": {
|
|
42
|
+
command: "npx",
|
|
43
|
+
args: ["-y", "mcp-atlassian"],
|
|
44
|
+
env: {
|
|
45
|
+
ATLASSIAN_BASE_URL: "https://komplian.atlassian.net",
|
|
46
|
+
ATLASSIAN_EMAIL: "",
|
|
47
|
+
ATLASSIAN_API_TOKEN: "",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
41
50
|
"KOMPLIAN-sentry": {
|
|
42
51
|
command: "npx",
|
|
43
52
|
args: ["-y", "@sentry/mcp-server@latest"],
|
|
@@ -50,6 +59,11 @@ const KOMPLIAN_MCP_PRESET = {
|
|
|
50
59
|
STRIPE_SECRET_KEY: "",
|
|
51
60
|
},
|
|
52
61
|
},
|
|
62
|
+
"KOMPLIAN-chrome-devtools": {
|
|
63
|
+
command: "npx",
|
|
64
|
+
args: ["-y", "chrome-devtools-mcp@latest"],
|
|
65
|
+
env: {},
|
|
66
|
+
},
|
|
53
67
|
},
|
|
54
68
|
};
|
|
55
69
|
|
|
@@ -59,28 +73,27 @@ Este archivo lo genera \`npx komplian mcp-tools\`. **No commitees** tokens; \`.c
|
|
|
59
73
|
|
|
60
74
|
## 1. Servidores en \`.cursor/mcp.json\` (stdio / npx)
|
|
61
75
|
|
|
62
|
-
|
|
63
|
-
|----|--------|---------|
|
|
64
|
-
| **KOMPLIAN-github** | GitHub API (issues, PRs) | \`GITHUB_PERSONAL_ACCESS_TOKEN\` en el bloque \`env\` del servidor (PAT con scopes repo/workflow según necesidad). El paquete npm está deprecado a favor del servidor oficial; si falla, sustituye por la config que indique [github/github-mcp-server](https://github.com/github/github-mcp-server). |
|
|
65
|
-
| **KOMPLIAN-sentry** | Sentry (issues, trazas) | Primera vez: login por navegador (device code). **Komplian:** \`organizationSlug\` = \`komplian\`, \`regionUrl\` = \`https://de.sentry.io\`. Proyectos: \`komplian-api\`, \`komplian-app\`. |
|
|
66
|
-
| **KOMPLIAN-stripe** | Stripe API | \`STRIPE_SECRET_KEY\` en \`env\` (mejor **Restricted key** / test en dev). Modo test vs live según [docs Stripe MCP](https://docs.stripe.com/mcp). |
|
|
76
|
+
Activa cada fila en **Cursor → Settings → MCP** (interruptor). Rellena \`env\` según la tabla.
|
|
67
77
|
|
|
68
|
-
|
|
78
|
+
| ID | Paquete | Qué es | Rellena / notas |
|
|
79
|
+
|----|---------|--------|-----------------|
|
|
80
|
+
| **KOMPLIAN-github** | \`@modelcontextprotocol/server-github\` | GitHub API | \`GITHUB_PERSONAL_ACCESS_TOKEN\` (PAT). El paquete npm está deprecado; alternativa: [github/github-mcp-server](https://github.com/github/github-mcp-server). |
|
|
81
|
+
| **KOMPLIAN-atlassian** | \`mcp-atlassian\` | Jira + Confluence (Cloud) | \`ATLASSIAN_EMAIL\` + \`ATLASSIAN_API_TOKEN\` ([crear token](https://id.atlassian.com/manage-profile/security/api-tokens)). \`ATLASSIAN_BASE_URL\` ya apunta a \`https://komplian.atlassian.net\`. Jira proyecto **KAPP**. |
|
|
82
|
+
| **KOMPLIAN-sentry** | \`@sentry/mcp-server\` | Sentry | Primera vez: login en navegador. **Komplian:** org \`komplian\`, \`regionUrl\` \`https://de.sentry.io\`, proyectos \`komplian-api\` / \`komplian-app\`. |
|
|
83
|
+
| **KOMPLIAN-stripe** | \`@stripe/mcp\` | Stripe API | \`STRIPE_SECRET_KEY\` (restricted key / test en dev). [Docs Stripe MCP](https://docs.stripe.com/mcp). |
|
|
84
|
+
| **KOMPLIAN-chrome-devtools** | \`chrome-devtools-mcp\` | Chrome (red, consola, screenshots) | Requiere **Node 20.19+** y Chrome estable. Ver [Chrome DevTools MCP](https://developer.chrome.com/blog/chrome-devtools-mcp). |
|
|
69
85
|
|
|
70
|
-
|
|
86
|
+
## 2. Opcional: integraciones nativas de Cursor
|
|
71
87
|
|
|
72
|
-
|
|
73
|
-
|-------------|-----------------|
|
|
74
|
-
| **Atlassian** (Jira + Confluence) | Site \`komplian.atlassian.net\`, proyecto Jira **KAPP**. Plugin \`plugin-atlassian-atlassian\` / \`user-mcp-atlassian\`. |
|
|
75
|
-
| **Chrome DevTools** | Depuración del navegador (\`user-chrome-devtools\`). |
|
|
88
|
+
Si además quieres los conectores de Cursor (OAuth / UI): **Settings → MCP** y busca **Atlassian**, **Sentry**, **Stripe**, **Chrome DevTools** — pueden coexistir con las entradas **KOMPLIAN-***; evita duplicar la misma acción en dos servidores.
|
|
76
89
|
|
|
77
90
|
## 3. Reinicio
|
|
78
91
|
|
|
79
|
-
Tras editar \`mcp.json\`, **reinicia Cursor**
|
|
92
|
+
Tras editar \`mcp.json\`, **reinicia Cursor** para cargar MCP.
|
|
80
93
|
|
|
81
94
|
## 4. Referencia interna
|
|
82
95
|
|
|
83
|
-
|
|
96
|
+
\`.cursor/rules/mcp-integrations.mdc\` (org Sentry, Stripe, KAPP).
|
|
84
97
|
`;
|
|
85
98
|
|
|
86
99
|
function findWorkspaceRoot(start) {
|
|
@@ -143,8 +156,8 @@ function parseMcpArgs(argv) {
|
|
|
143
156
|
function usageMcpTools() {
|
|
144
157
|
log(`Uso: npx komplian mcp-tools [opciones] [carpeta-monorepo]`);
|
|
145
158
|
log(``);
|
|
146
|
-
log(` Añade servidores MCP KOMPLIAN-* en .cursor/mcp.json (GitHub, Sentry, Stripe)
|
|
147
|
-
log(` y genera .cursor/KOMPLIAN_MCP_SETUP.md
|
|
159
|
+
log(` Añade servidores MCP KOMPLIAN-* en .cursor/mcp.json (GitHub, Atlassian, Sentry, Stripe, Chrome).`);
|
|
160
|
+
log(` y genera .cursor/KOMPLIAN_MCP_SETUP.md.`);
|
|
148
161
|
log(``);
|
|
149
162
|
log(` ${c.dim}Secretos: vacíos en JSON; no se publican en el paquete npm komplian.${c.reset}`);
|
|
150
163
|
log(``);
|
package/komplian-onboard.mjs
CHANGED
|
@@ -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
|
-
"description": "Komplian CLI: onboard, Postman, localhost, mcp-tools (
|
|
3
|
+
"version": "0.5.0",
|
|
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",
|