komplian 0.6.0 → 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,26 +6,22 @@
6
6
  2. Browser login: `gh auth login -h github.com -s repo -s read:org -w`
7
7
  3. `npx komplian onboard --yes` (sin `@versión`: usa `latest` del registry público).
8
8
 
9
- **Orden típico del equipo:** onboard `postman login` / `postman --yes` → `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
- **`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.
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
 
13
13
  ### Postman (colección + entornos)
14
14
 
15
15
  1. En [Postman](https://postman.com) usa una cuenta con email **`@komplian.com`** (o añade ese email a tu perfil).
16
16
  2. Crea una **API key**: Settings → **API keys** → Generate.
17
- 3. **Una vez por máquina** (guarda la clave en `~/.komplian/postman-api-key`; no hace falta `export` después):
18
-
19
- ```bash
20
- npx komplian postman login
21
- ```
22
-
23
- 4. Cuando quieras sincronizar la colección:
17
+ 3. **Sincronizar** (la primera vez en terminal interactiva **te pide** la API key y la guarda en `~/.komplian/postman-api-key`; no hace falta `export` después):
24
18
 
25
19
  ```bash
26
20
  npx komplian postman --yes
27
21
  ```
28
22
 
23
+ Opcional: **`npx komplian postman login`** solo para guardar o rotar la clave sin ejecutar el sync.
24
+
29
25
  Opcional: `export POSTMAN_API_KEY=…` solo para la sesión actual (tiene prioridad sobre el archivo).
30
26
 
31
27
  El comando llama a `GET https://api.getpostman.com/me` y **solo continúa** si el email de la cuenta es `@komplian.com`. **Si ya existen** la colección **Komplian API** y los entornos con el mismo nombre en ese workspace, se **actualizan**; si no, se crean.
@@ -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));
@@ -2,9 +2,10 @@
2
2
  /**
3
3
  * Komplian Postman — API key + email @komplian.com (Postman /me), luego colección + entornos.
4
4
  *
5
- * Requisitos: Node 18+. Primera vez: npx komplian postman login
5
+ * Requisitos: Node 18+. Primera vez: ejecuta `npx komplian postman --yes` (o `postman` sin clave):
6
+ * en terminal interactiva te pide la API key una vez y la guarda; o `npx komplian postman login`.
6
7
  *
7
- * Clave: POSTMAN_API_KEY o ~/.komplian/postman-api-key (npx komplian postman login)
8
+ * Clave: POSTMAN_API_KEY, ~/.komplian/postman-api-key, o prompt la primera vez (TTY).
8
9
  * Seguridad: no registrar secretos; errores API redactados; login sin eco (TTY); ~/.komplian 700 + key 600.
9
10
  * Opcional: POSTMAN_WORKSPACE_ID, KOMPLIAN_EMAIL_DOMAIN, KOMPLIAN_POSTMAN_KEY_FILE, KOMPLIAN_DEBUG
10
11
  */
@@ -215,7 +216,7 @@ function defaultKeyPath() {
215
216
  /**
216
217
  * Orden: POSTMAN_API_KEY → ~/.komplian/postman-api-key (o KOMPLIAN_POSTMAN_KEY_FILE).
217
218
  */
218
- function resolveApiKey() {
219
+ export function resolveApiKey() {
219
220
  const env = process.env.POSTMAN_API_KEY?.trim();
220
221
  if (env) return { key: env, source: "POSTMAN_API_KEY" };
221
222
  const path = defaultKeyPath();
@@ -226,20 +227,90 @@ function resolveApiKey() {
226
227
  return { key: "", source: null };
227
228
  }
228
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
+
229
245
  function printMissingKeyHelp() {
230
246
  log(`${c.red}✗${c.reset} No hay API key de Postman.`);
231
247
  log(``);
232
- log(` ${c.bold}Una vez por máquina:${c.reset}`);
233
- log(` ${c.cyan}npx komplian postman login${c.reset}`);
234
- log(` (pega la clave de Postman Settings → API keys → Generate)`);
248
+ log(` ${c.bold}Terminal interactiva (primera vez):${c.reset}`);
249
+ log(
250
+ ` ${c.cyan}npx komplian postman --yes${c.reset} (pide la clave y sigue con el sync)`
251
+ );
252
+ log(
253
+ ` ${c.cyan}npx komplian postman login${c.reset} (solo guardar clave; luego postman --yes)`
254
+ );
235
255
  log(``);
236
- log(` ${c.dim}O en la sesión actual: export POSTMAN_API_KEY=…${c.reset}`);
256
+ log(` ${c.bold}Sin TTY (CI, pipes):${c.reset}`);
257
+ log(` ${c.dim}export POSTMAN_API_KEY=…${c.reset}`);
237
258
  log(
238
- `${c.dim} Archivo manual: ${formatHomePath(defaultKeyPath())}${c.reset}`
259
+ `${c.dim} Archivo: ${formatHomePath(defaultKeyPath())} (permiso 600)${c.reset}`
239
260
  );
240
261
  process.exit(1);
241
262
  }
242
263
 
264
+ /**
265
+ * POSTMAN_API_KEY o ~/.komplian/postman-api-key. Si falta y hay TTY, pide la clave
266
+ * (misma validación que `postman login`), guarda y devuelve la resolución.
267
+ */
268
+ async function resolveApiKeyWithOptionalInteractiveSetup(domain) {
269
+ let r = resolveApiKey();
270
+ if (r.key) return r;
271
+ if (!input.isTTY) {
272
+ printMissingKeyHelp();
273
+ }
274
+
275
+ log(`${c.cyan}━━ Primera vez: Postman API key ━━${c.reset}`);
276
+ log(
277
+ `${c.dim}Cuenta @${domain}. Postman → Settings → API keys → Generate. Se guarda en ${formatHomePath(defaultKeyPath())}.${c.reset}`
278
+ );
279
+ log("");
280
+
281
+ const raw = await readPasswordLine(
282
+ "Postman API key (no se muestra al escribir; Settings → API keys): "
283
+ );
284
+ const k = (raw || "").trim();
285
+ if (!k) {
286
+ log(`${c.red}✗${c.reset} Clave vacía.`);
287
+ process.exit(1);
288
+ }
289
+
290
+ await verifyKomplianEmail(k, domain, { quietSuccess: true });
291
+
292
+ const keyPath = defaultKeyPath();
293
+ ensureSecureKomplianDir();
294
+ writeFileSync(keyPath, `${k}\n`, { encoding: "utf8", mode: 0o600 });
295
+ try {
296
+ chmodSync(keyPath, 0o600);
297
+ } catch {
298
+ /* Windows u otros */
299
+ }
300
+
301
+ log("");
302
+ log(
303
+ `${c.green}✓${c.reset} Guardada en ${c.bold}${formatHomePath(keyPath)}${c.reset} — continuando…`
304
+ );
305
+
306
+ r = resolveApiKey();
307
+ if (!r.key) {
308
+ log(`${c.red}✗${c.reset} No se pudo leer la clave guardada.`);
309
+ process.exit(1);
310
+ }
311
+ return r;
312
+ }
313
+
243
314
  /** Parseo mínimo .env (sin dependencia dotenv). */
244
315
  function parseEnvFile(text) {
245
316
  const out = {};
@@ -600,7 +671,34 @@ async function pickWorkspaceId(apiKey, explicit) {
600
671
  return w.id;
601
672
  }
602
673
 
603
- async function verifyKomplianEmail(apiKey, domain) {
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
+
700
+ async function verifyKomplianEmail(apiKey, domain, opts = {}) {
701
+ const quietOk = opts.quietSuccess === true;
604
702
  const { ok, status, body } = await pmFetch(apiKey, "/me");
605
703
  if (!ok) {
606
704
  log(
@@ -624,9 +722,11 @@ async function verifyKomplianEmail(apiKey, domain) {
624
722
  );
625
723
  process.exit(1);
626
724
  }
627
- log(
628
- `${c.green}✓${c.reset} Postman: ${c.bold}${maskEmail(email)}${c.reset} (${c.dim}dominio permitido${c.reset})`
629
- );
725
+ if (!quietOk) {
726
+ log(
727
+ `${c.green}✓${c.reset} Postman: ${c.bold}${maskEmail(email)}${c.reset} (${c.dim}dominio permitido${c.reset})`
728
+ );
729
+ }
630
730
  return email;
631
731
  }
632
732
 
@@ -759,7 +859,7 @@ function usage() {
759
859
  log(` Clave: ${c.dim}POSTMAN_API_KEY${c.reset} o archivo ${c.dim}~/.komplian/postman-api-key${c.reset} (véase ${c.cyan}postman login${c.reset})`);
760
860
  log(` Dominio email: solo @komplian.com (GET /me)`);
761
861
  log(``);
762
- log(` -y, --yes Sin prompts extra`);
862
+ log(` -y, --yes Sin prompts extra (si falta API key y hay TTY, pide la clave una vez y guarda)`);
763
863
  log(` --export-only Solo escribe JSON en disco (no llama a la API de Postman)`);
764
864
  log(` --out <dir> Carpeta para export (por defecto: ./komplian-postman)`);
765
865
  log(` --dotenv <ruta> .env extra (además de api/.env, .env, KOMPLIAN_DOTENV)`);
@@ -771,8 +871,8 @@ function usage() {
771
871
 
772
872
  function usageLogin() {
773
873
  log(`Uso: komplian postman login`);
774
- log(` Guarda la API key de Postman en ${c.dim}~/.komplian/postman-api-key${c.reset} (permiso 600).`);
775
- log(` Tras esto: ${c.cyan}npx komplian postman --yes${c.reset} sin exportar variables.`);
874
+ log(` Guarda o sustituye la API key en ${c.dim}~/.komplian/postman-api-key${c.reset} (permiso 600).`);
875
+ log(` Opcional: ${c.cyan}npx komplian postman --yes${c.reset} ya pide la clave la primera vez si falta.`);
776
876
  log(``);
777
877
  log(` Postman → Settings (avatar) → API keys → Generate`);
778
878
  }
@@ -841,15 +941,13 @@ export async function runPostman(argv) {
841
941
  return;
842
942
  }
843
943
 
844
- const { key: apiKey, source } = resolveApiKey();
845
- if (!apiKey) {
846
- printMissingKeyHelp();
847
- }
944
+ const domain = (process.env.KOMPLIAN_EMAIL_DOMAIN || "komplian.com").trim();
945
+ const { key: apiKey, source } =
946
+ await resolveApiKeyWithOptionalInteractiveSetup(domain);
848
947
  if (source && source !== "POSTMAN_API_KEY") {
849
948
  log(`${c.dim}→ API key Postman desde: ${formatHomePath(source)}${c.reset}`);
850
949
  }
851
950
 
852
- const domain = (process.env.KOMPLIAN_EMAIL_DOMAIN || "komplian.com").trim();
853
951
  await verifyKomplianEmail(apiKey, domain);
854
952
 
855
953
  const collection = buildCollection();
@@ -940,16 +1038,12 @@ export async function runPostman(argv) {
940
1038
  }
941
1039
 
942
1040
  /**
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).
1041
+ * GET /me y dominio @komplian.com. Si no hay clave y hay TTY, pide guardarla (como `postman --yes`).
945
1042
  */
946
1043
  export async function assertKomplianEmployeePostman() {
947
1044
  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
- }
1045
+ const { key, source } =
1046
+ await resolveApiKeyWithOptionalInteractiveSetup(domain);
953
1047
  if (source && source !== "POSTMAN_API_KEY") {
954
1048
  log(`${c.dim}→ Postman API key desde: ${formatHomePath(source)}${c.reset}`);
955
1049
  }
@@ -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, "&amp;")
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.0",
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",