komplian 0.6.1 → 0.7.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,7 +6,7 @@
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 --yes`** (pide la clave la primera vez en TTY) → `mcp-tools --yes` → **`db:all:dev`** → `localhost --yes`. Ver **`ONBOARDING.md`**.
9
+ **Orden típico del equipo:** **`npx komplian setup`** (todo en uno; formulario local en navegador si hace falta) **o** paso a paso: onboard **`postman --yes`** → `mcp-tools --yes` → **`db:all:dev`** → `localhost --yes`. Ver **`ONBOARDING.md`**.
10
10
 
11
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.
12
12
 
@@ -394,10 +394,11 @@ function npmInstallEach(workspace) {
394
394
  }
395
395
 
396
396
  function usage() {
397
- log(`Uso: komplian onboard | postman | mcp-tools | db:all:dev | localhost | db …`);
398
- log(` ${c.bold}Setup estándar (orden):${c.reset}`);
397
+ log(`Uso: komplian setup | onboard | postman | mcp-tools | db:all:dev | localhost | db …`);
398
+ log(` ${c.bold}Todo en uno:${c.reset} ${c.cyan}npx komplian setup${c.reset} ${c.dim}(onboard+postman+mcp+db+localhost; formulario en navegador si hace falta)${c.reset}`);
399
+ log(` ${c.bold}Setup por pasos:${c.reset}`);
399
400
  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(` 2. npx komplian postman --yes ${c.dim}(@komplian.com)${c.reset}`);
401
402
  log(` 3. npx komplian mcp-tools --yes`);
402
403
  log(` 4. npx komplian db:all:dev ${c.dim}(Postman + 3 URLs dev → ~/.komplian + .env.local)${c.reset}`);
403
404
  log(` 5. npx komplian localhost --yes`);
@@ -486,6 +487,11 @@ function normalizeArgv(argv) {
486
487
 
487
488
  async function main() {
488
489
  const rawArgv = process.argv.slice(2);
490
+ if (rawArgv[0] === "setup") {
491
+ const { runSetup } = await import("./komplian-setup.mjs");
492
+ await runSetup(rawArgv.slice(1));
493
+ return;
494
+ }
489
495
  if (rawArgv[0] === "postman") {
490
496
  const { runPostman } = await import("./komplian-postman.mjs");
491
497
  await runPostman(rawArgv.slice(1));
@@ -216,7 +216,7 @@ function defaultKeyPath() {
216
216
  /**
217
217
  * Orden: POSTMAN_API_KEY → ~/.komplian/postman-api-key (o KOMPLIAN_POSTMAN_KEY_FILE).
218
218
  */
219
- function resolveApiKey() {
219
+ export function resolveApiKey() {
220
220
  const env = process.env.POSTMAN_API_KEY?.trim();
221
221
  if (env) return { key: env, source: "POSTMAN_API_KEY" };
222
222
  const path = defaultKeyPath();
@@ -227,6 +227,21 @@ function resolveApiKey() {
227
227
  return { key: "", source: null };
228
228
  }
229
229
 
230
+ /** Guarda la clave en ~/.komplian/postman-api-key (uso: `komplian setup` vía navegador). */
231
+ export function savePostmanApiKeyToKomplianHome(apiKey) {
232
+ const k = (apiKey || "").trim();
233
+ if (!k) return false;
234
+ const keyPath = defaultKeyPath();
235
+ ensureSecureKomplianDir();
236
+ writeFileSync(keyPath, `${k}\n`, { encoding: "utf8", mode: 0o600 });
237
+ try {
238
+ chmodSync(keyPath, 0o600);
239
+ } catch {
240
+ /* Windows u otros */
241
+ }
242
+ return true;
243
+ }
244
+
230
245
  function printMissingKeyHelp() {
231
246
  log(`${c.red}✗${c.reset} No hay API key de Postman.`);
232
247
  log(``);
@@ -656,6 +671,32 @@ async function pickWorkspaceId(apiKey, explicit) {
656
671
  return w.id;
657
672
  }
658
673
 
674
+ /** Validación sin `process.exit` (p. ej. formulario web local en `komplian setup`). */
675
+ export async function validatePostmanApiKeyForKomplian(apiKey, domain) {
676
+ const d = (domain || "komplian.com").trim().replace(/^@/, "");
677
+ const { ok, status, body } = await pmFetch(apiKey, "/me");
678
+ if (!ok) {
679
+ return {
680
+ ok: false,
681
+ error: `Postman rechazó la clave (HTTP ${status}).`,
682
+ };
683
+ }
684
+ const email = extractEmail(body);
685
+ if (!email) {
686
+ return {
687
+ ok: false,
688
+ error: "La API de Postman no devolvió email en /me.",
689
+ };
690
+ }
691
+ if (!emailAllowed(email, d)) {
692
+ return {
693
+ ok: false,
694
+ error: `Se requiere una cuenta con email @${d}.`,
695
+ };
696
+ }
697
+ return { ok: true };
698
+ }
699
+
659
700
  async function verifyKomplianEmail(apiKey, domain, opts = {}) {
660
701
  const quietOk = opts.quietSuccess === true;
661
702
  const { ok, status, body } = await pmFetch(apiKey, "/me");
@@ -0,0 +1,477 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Komplian setup — un solo comando: onboard → postman → mcp-tools → db:all:dev → localhost.
4
+ * Por defecto abre el navegador en localhost para datos sensibles (Postman API key, URLs Neon dev)
5
+ * con texto de ayuda; alternativa: --terminal-only (sin navegador).
6
+ */
7
+
8
+ import { spawnSync } from "node:child_process";
9
+ import { createServer } from "node:http";
10
+ import { randomBytes } from "node:crypto";
11
+ import { existsSync, mkdirSync } from "node:fs";
12
+ import { dirname, join, resolve } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import { homedir } from "node:os";
15
+
16
+ import {
17
+ resolveApiKey,
18
+ validatePostmanApiKeyForKomplian,
19
+ savePostmanApiKeyToKomplianHome,
20
+ runPostman,
21
+ } from "./komplian-postman.mjs";
22
+ import { runDbAllDev } from "./komplian-db-all-dev.mjs";
23
+ import { runMcpTools } from "./komplian-mcp-tools.mjs";
24
+ import { runLocalhost, findWorkspaceRoot } from "./komplian-localhost.mjs";
25
+ import { loadHomeDevDatabases } from "./komplian-db.mjs";
26
+
27
+ const __dirname = dirname(fileURLToPath(import.meta.url));
28
+ const IS_WIN = process.platform === "win32";
29
+
30
+ function spawnWin(extra = {}) {
31
+ return IS_WIN ? { ...extra, shell: true } : extra;
32
+ }
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
+
50
+ function isValidPostgresUrl(s) {
51
+ const t = (s || "").trim();
52
+ if (!t.startsWith("postgresql://") && !t.startsWith("postgres://")) return false;
53
+ if (t.includes(PLACEHOLDER)) return false;
54
+ try {
55
+ const u = new URL(t);
56
+ return !!u.hostname;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ function isValidTriplet(d) {
63
+ return (
64
+ isValidPostgresUrl(d.app) &&
65
+ isValidPostgresUrl(d.admin) &&
66
+ isValidPostgresUrl(d.web)
67
+ );
68
+ }
69
+
70
+ function collectUrlsFromEnv() {
71
+ return {
72
+ app: process.env.KOMPLIAN_DEV_APP_DATABASE_URL?.trim() || "",
73
+ admin: process.env.KOMPLIAN_DEV_ADMIN_DATABASE_URL?.trim() || "",
74
+ web: process.env.KOMPLIAN_DEV_WEB_DATABASE_URL?.trim() || "",
75
+ };
76
+ }
77
+
78
+ function openBrowserSync(url) {
79
+ try {
80
+ if (process.platform === "darwin") {
81
+ spawnSync("open", [url], { stdio: "ignore" });
82
+ } else if (process.platform === "win32") {
83
+ spawnSync("cmd", ["/c", "start", "", url], { ...spawnWin(), stdio: "ignore" });
84
+ } else {
85
+ spawnSync("xdg-open", [url], { stdio: "ignore" });
86
+ }
87
+ } catch {
88
+ log(`${c.yellow}○${c.reset} No se pudo abrir el navegador. Abre manualmente: ${c.bold}${url}${c.reset}`);
89
+ }
90
+ }
91
+
92
+ function buildSetupPageHtml(token, { needsPostmanKey, needsDbUrls, emailDomain }) {
93
+ const esc = (s) =>
94
+ String(s)
95
+ .replace(/&/g, "&")
96
+ .replace(/</g, "&lt;")
97
+ .replace(/"/g, "&quot;");
98
+ const postmanBlock = needsPostmanKey
99
+ ? `
100
+ <section class="card">
101
+ <h2>1. Postman API key</h2>
102
+ <p class="help">Crea una clave en Postman → <strong>Settings</strong> → <strong>API keys</strong> → Generate. Cuenta con email <strong>@${esc(emailDomain)}</strong>. Se guarda en <code>~/.komplian/postman-api-key</code>. Si la dejas vacía, la terminal te la pedirá en el paso Postman.</p>
103
+ <label for="postman_api_key">API key (opcional aquí)</label>
104
+ <input type="password" id="postman_api_key" name="postman_api_key" autocomplete="off" placeholder="PMAK-…" />
105
+ </section>`
106
+ : "";
107
+ const dbBlock = needsDbUrls
108
+ ? `
109
+ <section class="card">
110
+ <h2>${needsPostmanKey ? "2" : "1"}. Bases de datos (solo desarrollo)</h2>
111
+ <p class="help">Pega las connection strings <strong>postgresql://…</strong> de Neon (rama <em>development</em> u homólogo). Tres bases: <strong>app</strong> (también usa la API), <strong>admin</strong> y <strong>web</strong> (pilot). Se escriben en <code>~/.komplian/dev-databases.json</code> y en cada <code>.env.local</code> del monorepo.</p>
112
+ <label for="db_app">URL APP (app + API)</label>
113
+ <textarea id="db_app" name="db_app" rows="2" placeholder="postgresql://…"></textarea>
114
+ <label for="db_admin">URL ADMIN</label>
115
+ <textarea id="db_admin" name="db_admin" rows="2" placeholder="postgresql://…"></textarea>
116
+ <label for="db_web">URL WEB (pilot)</label>
117
+ <textarea id="db_web" name="db_web" rows="2" placeholder="postgresql://…"></textarea>
118
+ </section>`
119
+ : "";
120
+ return `<!DOCTYPE html>
121
+ <html lang="es">
122
+ <head>
123
+ <meta charset="utf-8" />
124
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
125
+ <title>Komplian setup</title>
126
+ <style>
127
+ :root { font-family: system-ui, sans-serif; background: #0a0a0a; color: #e5e5e5; }
128
+ body { max-width: 640px; margin: 2rem auto; padding: 0 1rem; }
129
+ h1 { font-size: 1.25rem; font-weight: 600; }
130
+ .card { background: #171717; border: 1px solid #262626; border-radius: 10px; padding: 1.25rem; margin-bottom: 1.25rem; }
131
+ .help { color: #a3a3a3; font-size: 0.9rem; line-height: 1.5; margin: 0 0 1rem; }
132
+ label { display: block; font-size: 0.8rem; color: #737373; margin: 0.75rem 0 0.35rem; }
133
+ input, textarea { width: 100%; box-sizing: border-box; padding: 0.6rem 0.75rem; border-radius: 8px; border: 1px solid #404040; background: #0a0a0a; color: #fafafa; font-family: ui-monospace, monospace; font-size: 0.85rem; }
134
+ button { margin-top: 1.25rem; width: 100%; padding: 0.85rem; border: none; border-radius: 8px; background: #fafafa; color: #0a0a0a; font-weight: 600; cursor: pointer; font-size: 1rem; }
135
+ button:disabled { opacity: 0.5; cursor: not-allowed; }
136
+ .err { color: #f87171; font-size: 0.9rem; margin-top: 0.75rem; white-space: pre-wrap; }
137
+ .ok { color: #4ade80; margin-top: 1rem; }
138
+ code { font-size: 0.8em; background: #262626; padding: 0.1em 0.35em; border-radius: 4px; }
139
+ </style>
140
+ </head>
141
+ <body>
142
+ <h1>Komplian — asistente de setup</h1>
143
+ <p class="help">Esta ventana es local (solo tu ordenador). Rellena lo que pida el asistente y pulsa <strong>Continuar</strong>. Puedes cerrar la pestaña después del mensaje de éxito.</p>
144
+ ${postmanBlock}
145
+ ${dbBlock}
146
+ <div id="err" class="err" style="display:none"></div>
147
+ <div id="ok" class="ok" style="display:none"></div>
148
+ <button type="button" id="go">Continuar</button>
149
+ <script>
150
+ const TOKEN = ${JSON.stringify(token)};
151
+ const needsPostman = ${JSON.stringify(needsPostmanKey)};
152
+ const needsDb = ${JSON.stringify(needsDbUrls)};
153
+ document.getElementById("go").onclick = async () => {
154
+ const err = document.getElementById("err");
155
+ const ok = document.getElementById("ok");
156
+ err.style.display = "none";
157
+ ok.style.display = "none";
158
+ const body = { token: TOKEN };
159
+ if (needsPostman) body.postman_api_key = (document.getElementById("postman_api_key") || {}).value || "";
160
+ if (needsDb) {
161
+ body.db_app = (document.getElementById("db_app") || {}).value || "";
162
+ body.db_admin = (document.getElementById("db_admin") || {}).value || "";
163
+ body.db_web = (document.getElementById("db_web") || {}).value || "";
164
+ }
165
+ const btn = document.getElementById("go");
166
+ btn.disabled = true;
167
+ try {
168
+ const r = await fetch("/submit", {
169
+ method: "POST",
170
+ headers: { "Content-Type": "application/json" },
171
+ body: JSON.stringify(body),
172
+ });
173
+ const j = await r.json().catch(() => ({}));
174
+ if (!r.ok || !j.ok) {
175
+ err.textContent = j.error || "Error al validar. Revisa los datos.";
176
+ err.style.display = "block";
177
+ btn.disabled = false;
178
+ return;
179
+ }
180
+ ok.textContent = "Listo. Vuelve a la terminal; puedes cerrar esta pestaña.";
181
+ ok.style.display = "block";
182
+ } catch (e) {
183
+ err.textContent = "No se pudo enviar. ¿Sigues en la misma red local?";
184
+ err.style.display = "block";
185
+ btn.disabled = false;
186
+ }
187
+ };
188
+ </script>
189
+ </body>
190
+ </html>`;
191
+ }
192
+
193
+ /**
194
+ * Servidor solo en 127.0.0.1. Devuelve { postmanKey?, db } según lo pedido.
195
+ */
196
+ function runSetupBrowserForm(opts) {
197
+ const {
198
+ needsPostmanKey,
199
+ needsDbUrls,
200
+ emailDomain,
201
+ timeoutMs = 600_000,
202
+ } = opts;
203
+
204
+ return new Promise((resolvePromise, rejectPromise) => {
205
+ const token = randomBytes(24).toString("hex");
206
+ const server = createServer(async (req, res) => {
207
+ const url = new URL(req.url || "/", `http://127.0.0.1`);
208
+
209
+ if (req.method === "GET" && url.pathname === "/") {
210
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
211
+ res.end(
212
+ buildSetupPageHtml(token, {
213
+ needsPostmanKey,
214
+ needsDbUrls,
215
+ emailDomain,
216
+ })
217
+ );
218
+ return;
219
+ }
220
+
221
+ if (req.method === "POST" && url.pathname === "/submit") {
222
+ let raw = "";
223
+ for await (const ch of req) raw += ch;
224
+ let data;
225
+ try {
226
+ data = JSON.parse(raw || "{}");
227
+ } catch {
228
+ res.writeHead(400, { "Content-Type": "application/json" });
229
+ res.end(JSON.stringify({ ok: false, error: "JSON inválido." }));
230
+ return;
231
+ }
232
+ if (data.token !== token) {
233
+ res.writeHead(403, { "Content-Type": "application/json" });
234
+ res.end(JSON.stringify({ ok: false, error: "Token inválido." }));
235
+ return;
236
+ }
237
+
238
+ const out = { postmanKey: "", db: null };
239
+
240
+ if (needsPostmanKey) {
241
+ const pk = String(data.postman_api_key || "").trim();
242
+ if (pk) {
243
+ const v = await validatePostmanApiKeyForKomplian(pk, emailDomain);
244
+ if (!v.ok) {
245
+ res.writeHead(400, { "Content-Type": "application/json" });
246
+ res.end(JSON.stringify({ ok: false, error: v.error || "Postman inválido." }));
247
+ return;
248
+ }
249
+ out.postmanKey = pk;
250
+ }
251
+ }
252
+
253
+ if (needsDbUrls) {
254
+ const triplet = {
255
+ app: String(data.db_app || "").trim(),
256
+ admin: String(data.db_admin || "").trim(),
257
+ web: String(data.db_web || "").trim(),
258
+ };
259
+ if (!isValidTriplet(triplet)) {
260
+ res.writeHead(400, { "Content-Type": "application/json" });
261
+ res.end(
262
+ JSON.stringify({
263
+ ok: false,
264
+ error:
265
+ "Las tres URLs deben ser postgresql:// o postgres:// válidas (sin placeholder).",
266
+ })
267
+ );
268
+ return;
269
+ }
270
+ out.db = triplet;
271
+ }
272
+
273
+ res.writeHead(200, { "Content-Type": "application/json" });
274
+ res.end(JSON.stringify({ ok: true }));
275
+
276
+ server.close(() => {
277
+ clearTimeout(timer);
278
+ resolvePromise(out);
279
+ });
280
+ return;
281
+ }
282
+
283
+ res.writeHead(404);
284
+ res.end();
285
+ });
286
+
287
+ const timer = setTimeout(() => {
288
+ server.close();
289
+ rejectPromise(new Error("Tiempo agotado esperando el formulario en el navegador."));
290
+ }, timeoutMs);
291
+
292
+ server.listen(0, "127.0.0.1", () => {
293
+ const addr = server.address();
294
+ const port = typeof addr === "object" && addr ? addr.port : 0;
295
+ const openUrl = `http://127.0.0.1:${port}/`;
296
+ log("");
297
+ log(
298
+ `${c.cyan}━━ Navegador (formulario local) ━━${c.reset} ${c.bold}${openUrl}${c.reset}`
299
+ );
300
+ log(
301
+ `${c.dim}Solo escucha en tu máquina (127.0.0.1). Cierra la pestaña tras el mensaje de éxito.${c.reset}`
302
+ );
303
+ openBrowserSync(openUrl);
304
+ });
305
+
306
+ server.on("error", (e) => {
307
+ clearTimeout(timer);
308
+ rejectPromise(e);
309
+ });
310
+ });
311
+ }
312
+
313
+ function parseArgs(argv) {
314
+ const out = {
315
+ terminalOnly: false,
316
+ workspace: "",
317
+ help: false,
318
+ team: "",
319
+ ssh: false,
320
+ allRepos: false,
321
+ noInstall: false,
322
+ };
323
+ for (let i = 0; i < argv.length; i++) {
324
+ const a = argv[i];
325
+ if (a === "--terminal-only" || a === "--no-browser")
326
+ out.terminalOnly = true;
327
+ else if (a === "-w" || a === "--workspace") out.workspace = argv[++i] || "";
328
+ else if (a === "-t" || a === "--team") out.team = argv[++i] || "";
329
+ else if (a === "--ssh") out.ssh = true;
330
+ else if (a === "--all-repos") out.allRepos = true;
331
+ else if (a === "--no-install") out.noInstall = true;
332
+ else if (a === "-h" || a === "--help") out.help = true;
333
+ else if (a.startsWith("-")) {
334
+ log(`${c.red}✗${c.reset} Opción desconocida: ${a}`);
335
+ process.exit(1);
336
+ } else if (!out.workspace) out.workspace = a;
337
+ }
338
+ return out;
339
+ }
340
+
341
+ function usage() {
342
+ log(`Uso: npx komplian setup [opciones] [carpeta-workspace]`);
343
+ log(``);
344
+ log(
345
+ ` Orden: ${c.bold}onboard${c.reset} → ${c.bold}postman${c.reset} → ${c.bold}mcp-tools${c.reset} → ${c.bold}db:all:dev${c.reset} → ${c.bold}localhost${c.reset}`
346
+ );
347
+ log(
348
+ ` Por defecto abre el ${c.bold}navegador${c.reset} para Postman API key (si falta) y las 3 URLs Neon dev (si faltan).`
349
+ );
350
+ log(``);
351
+ log(` --terminal-only Sin formulario web; postman/db usan terminal o env`);
352
+ log(` -w, --workspace Raíz del monorepo (destino del clone)`);
353
+ log(` -t, --team Equipo (komplian-team-repos.json)`);
354
+ log(` --all-repos Clonar todos los repos del JSON`);
355
+ log(` --ssh Clonar por SSH`);
356
+ log(` --no-install No npm install tras clone`);
357
+ log(` -h, --help`);
358
+ }
359
+
360
+ function runOnboardChild(args) {
361
+ const script = join(__dirname, "komplian-onboard.mjs");
362
+ const argv = ["onboard", ...args];
363
+ const r = spawnSync(process.execPath, [script, ...argv], {
364
+ stdio: "inherit",
365
+ windowsHide: true,
366
+ });
367
+ return r.status === 0;
368
+ }
369
+
370
+ export async function runSetup(argv) {
371
+ const opts = parseArgs(argv);
372
+ if (opts.help) {
373
+ usage();
374
+ return;
375
+ }
376
+
377
+ const nodeMajor = Number(process.versions.node.split(".")[0], 10);
378
+ if (nodeMajor < 18) {
379
+ log(`${c.red}✗${c.reset} Hace falta Node 18+.`);
380
+ process.exit(1);
381
+ }
382
+
383
+ let workspaceArg = (opts.workspace || "").trim();
384
+ if (!workspaceArg) workspaceArg = process.cwd();
385
+ const workspaceAbs = resolve(
386
+ workspaceArg.replace(/^~(?=$|[/\\])/, homedir())
387
+ );
388
+
389
+ mkdirSync(workspaceAbs, { recursive: true });
390
+
391
+ log(`${c.cyan}${c.bold}━━ Komplian setup ━━${c.reset}`);
392
+ log(`${c.dim}Workspace:${c.reset} ${workspaceAbs}`);
393
+ log("");
394
+
395
+ const onboardArgs = ["--yes"];
396
+ if (opts.team) onboardArgs.push("-t", opts.team);
397
+ if (opts.allRepos) onboardArgs.push("--all-repos");
398
+ if (opts.ssh) onboardArgs.push("--ssh");
399
+ if (opts.noInstall) onboardArgs.push("--no-install");
400
+ onboardArgs.push(workspaceAbs);
401
+
402
+ log(`${c.cyan}━━ 1/5 Onboard ━━${c.reset}`);
403
+ if (!runOnboardChild(onboardArgs)) {
404
+ log(`${c.red}✗${c.reset} Onboard falló. Revisa GitHub CLI (gh) y permisos.`);
405
+ process.exit(1);
406
+ }
407
+
408
+ let monorepoRoot = workspaceAbs;
409
+ if (!existsSync(join(monorepoRoot, "api", "package.json"))) {
410
+ monorepoRoot = findWorkspaceRoot(workspaceAbs);
411
+ }
412
+ if (!existsSync(join(monorepoRoot, "api", "package.json"))) {
413
+ log(
414
+ `${c.red}✗${c.reset} No se encontró monorepo con api/package.json bajo ${workspaceAbs}.`
415
+ );
416
+ process.exit(1);
417
+ }
418
+
419
+ process.chdir(monorepoRoot);
420
+
421
+ const emailDomain = (process.env.KOMPLIAN_EMAIL_DOMAIN || "komplian.com").trim();
422
+ const useBrowser =
423
+ !opts.terminalOnly && process.env.KOMPLIAN_SETUP_NO_BROWSER !== "1";
424
+
425
+ const envTriplet = collectUrlsFromEnv();
426
+ const envDbOk = isValidTriplet(envTriplet);
427
+ const saved = loadHomeDevDatabases();
428
+ const savedDbOk = saved && isValidTriplet(saved);
429
+ const { key: existingPmKey } = resolveApiKey();
430
+ const needsPostmanKey = !existingPmKey;
431
+ const needsDbUrls = !envDbOk && !savedDbOk;
432
+
433
+ if (useBrowser && (needsPostmanKey || needsDbUrls)) {
434
+ log(`${c.cyan}━━ Formulario en el navegador ━━${c.reset}`);
435
+ try {
436
+ const form = await runSetupBrowserForm({
437
+ needsPostmanKey,
438
+ needsDbUrls,
439
+ emailDomain,
440
+ });
441
+ if (form.postmanKey) {
442
+ savePostmanApiKeyToKomplianHome(form.postmanKey);
443
+ log(`${c.green}✓${c.reset} Postman API key guardada en ~/.komplian/`);
444
+ }
445
+ if (form.db) {
446
+ process.env.KOMPLIAN_DEV_APP_DATABASE_URL = form.db.app;
447
+ process.env.KOMPLIAN_DEV_ADMIN_DATABASE_URL = form.db.admin;
448
+ process.env.KOMPLIAN_DEV_WEB_DATABASE_URL = form.db.web;
449
+ log(`${c.green}✓${c.reset} URLs de desarrollo recibidas desde el formulario.`);
450
+ }
451
+ } catch (e) {
452
+ log(
453
+ `${c.red}✗${c.reset} Formulario web: ${e?.message || e}. Prueba ${c.bold}--terminal-only${c.reset} o define variables de entorno.`
454
+ );
455
+ process.exit(1);
456
+ }
457
+ }
458
+
459
+ log(`${c.cyan}━━ 2/5 Postman ━━${c.reset}`);
460
+ await runPostman(["--yes"]);
461
+
462
+ log(`${c.cyan}━━ 3/5 MCP tools ━━${c.reset}`);
463
+ await runMcpTools(["--yes"]);
464
+
465
+ log(`${c.cyan}━━ 4/5 Bases de datos (development) ━━${c.reset}`);
466
+ const dbArgs = ["--yes", "-w", monorepoRoot];
467
+ await runDbAllDev(dbArgs);
468
+
469
+ log(`${c.cyan}━━ 5/5 Localhost ━━${c.reset}`);
470
+ await runLocalhost(["--yes"]);
471
+
472
+ log("");
473
+ log(`${c.green}✓${c.reset} ${c.bold}Setup completado.${c.reset}`);
474
+ log(
475
+ `${c.dim}Monorepo:${c.reset} ${monorepoRoot} ${c.dim}· Cursor: File → Open Folder${c.reset}`
476
+ );
477
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "komplian",
3
- "version": "0.6.1",
4
- "description": "Komplian CLI: onboard, Postman, localhost, mcp-tools, db (psql). Node 18+. Published tarball has no .env / secrets.",
3
+ "version": "0.7.0",
4
+ "description": "Komplian CLI: setup (todo en uno), onboard, Postman, localhost, mcp-tools, db (psql). Node 18+.",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=18"
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "files": [
13
13
  "komplian-onboard.mjs",
14
+ "komplian-setup.mjs",
14
15
  "komplian-postman.mjs",
15
16
  "komplian-localhost.mjs",
16
17
  "komplian-mcp-tools.mjs",
@@ -23,7 +24,7 @@
23
24
  "access": "public"
24
25
  },
25
26
  "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 && node --check komplian-db-all-dev.mjs"
27
+ "prepublishOnly": "node --check komplian-onboard.mjs && node --check komplian-setup.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
28
  },
28
29
  "keywords": [
29
30
  "komplian",