komplian 0.7.1 → 0.7.3

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
@@ -26,7 +26,7 @@ Opcional: `export POSTMAN_API_KEY=…` solo para la sesión actual (tiene priori
26
26
 
27
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.
28
28
 
29
- **Variables de la API Komplian** (`apiKey`, `adminApiKey`, `workspaceId`, …) se rellenan en Postman automáticamente si están en `process.env` o en archivos `.env` (busca `api/.env`, `.env`, etc.; o `KOMPLIAN_DOTENV` / `--dotenv ruta`). Nombres típicos: `API_KEY`, `ADMIN_API_KEY`, `KOMPLIAN_WORKSPACE_ID`. Los JSON exportados en `./komplian-postman/` **no** incluyen secretos (para no commitearlos).
29
+ **Variables de la API Komplian** (`apiKey`, `adminApiKey`, `workspaceId`, …) se rellenan en Postman automáticamente si están en `process.env` o en archivos `.env` (busca `api/.env`, `.env`, etc.; o `KOMPLIAN_DOTENV` / `--dotenv ruta`). Nombres típicos: `API_KEY`, `ADMIN_API_KEY`, `KOMPLIAN_WORKSPACE_ID`. Los JSON exportados en `~/.komplian/postman-export/` (o `--out`) **no** incluyen secretos (para no commitearlos).
30
30
 
31
31
  - Solo exportar archivos (sin subir por API): `npx komplian postman --yes --export-only`
32
32
  - Otro workspace: `POSTMAN_WORKSPACE_ID=<id>`
@@ -519,12 +519,15 @@ export async function runLocalhost(argv) {
519
519
  if (!cliQuiet()) {
520
520
  log(`${c.cyan}━━ .env.local ━━${c.reset}`);
521
521
  }
522
- for (const w of written) {
523
- const st = w.skipped ? `${c.dim}skip${c.reset}` : `${c.green}ok${c.reset}`;
524
- const name = w.label || "?";
525
- if (cliQuiet()) {
526
- logAlways(` ${st} ${name}`);
527
- } else {
522
+ if (cliQuiet()) {
523
+ const labels = written.map((w) => w.label || "?").join(`${c.dim}·${c.reset} `);
524
+ logAlways(
525
+ ` ${c.green}✓${c.reset} ${c.dim}.env.local${c.reset} ${labels}`
526
+ );
527
+ } else {
528
+ for (const w of written) {
529
+ const st = w.skipped ? `${c.dim}skip${c.reset}` : `${c.green}ok${c.reset}`;
530
+ const name = w.label || "?";
528
531
  log(` ${st} ${name}`);
529
532
  }
530
533
  }
@@ -565,7 +568,7 @@ export async function runLocalhost(argv) {
565
568
  });
566
569
 
567
570
  if (cliQuiet()) {
568
- logAlways(`${c.cyan}━━ dev servers ━━${c.reset}`);
571
+ logAlways(`${c.dim}dev servers · Ctrl+C stop${c.reset}`);
569
572
  } else {
570
573
  log(`${c.cyan}━━ dev servers (${services.length}) ━━${c.reset} ${c.dim}Ctrl+C stop${c.reset}`);
571
574
  log("");
@@ -573,9 +576,13 @@ export async function runLocalhost(argv) {
573
576
 
574
577
  const npx = process.platform === "win32" ? "npx.cmd" : "npx";
575
578
  const useShell = process.platform === "win32";
579
+ /** --raw avoids concurrently prefix logger bugs (e.g. prev.replace on Windows / npx). */
580
+ const concArgs = cliQuiet()
581
+ ? ["--yes", "concurrently@9", "--raw", ...scripts]
582
+ : ["--yes", "concurrently@9", "-c", colors, "-n", names, ...scripts];
576
583
  const child = spawn(
577
584
  npx,
578
- ["--yes", "concurrently@9", "-c", colors, "-n", names, ...scripts],
585
+ concArgs,
579
586
  {
580
587
  cwd: workspaceRoot,
581
588
  stdio: "inherit",
@@ -65,6 +65,15 @@ const KOMPLIAN_MCP_PRESET = {
65
65
  args: ["-y", "chrome-devtools-mcp@latest"],
66
66
  env: {},
67
67
  },
68
+ /** Remote MCP — OAuth in Cursor (“Needs login”). */
69
+ "KOMPLIAN-vercel": {
70
+ url: "https://mcp.vercel.com",
71
+ },
72
+ /** Remote MCP — OAuth via mcp-remote (Neon docs). */
73
+ "KOMPLIAN-neon": {
74
+ command: "npx",
75
+ args: ["-y", "mcp-remote", "https://mcp.neon.tech/mcp"],
76
+ },
68
77
  },
69
78
  };
70
79
 
@@ -83,10 +92,12 @@ Enable each row in **Cursor → Settings → MCP**. Fill \`env\` per the table.
83
92
  | **KOMPLIAN-sentry** | \`@sentry/mcp-server\` | Sentry | First time: browser login. **Komplian:** org \`komplian\`, \`regionUrl\` \`https://de.sentry.io\`, projects \`komplian-api\` / \`komplian-app\`. |
84
93
  | **KOMPLIAN-stripe** | \`@stripe/mcp\` | Stripe API | \`STRIPE_SECRET_KEY\` (restricted / test in dev). [Stripe MCP docs](https://docs.stripe.com/mcp). |
85
94
  | **KOMPLIAN-chrome-devtools** | \`chrome-devtools-mcp\` | Chrome (network, console, screenshots) | Needs **Node 20.19+** and stable Chrome. [Chrome DevTools MCP](https://developer.chrome.com/blog/chrome-devtools-mcp). |
95
+ | **KOMPLIAN-vercel** | \`url\` → \`https://mcp.vercel.com\` | Vercel projects & deploys | **OAuth** in Cursor (see [Vercel MCP](https://mcp.vercel.com/)). No token in JSON. |
96
+ | **KOMPLIAN-neon** | \`npx -y mcp-remote https://mcp.neon.tech/mcp\` | Neon Postgres / API | **OAuth** when the client starts (see [Neon MCP](https://neon.tech/docs/ai/neon-mcp-server)). Alt: local \`@neondatabase/mcp-server-neon\` + API key. |
86
97
 
87
98
  ## 2. Optional: Cursor native connectors
88
99
 
89
- For Cursor OAuth/UI connectors: **Settings → MCP** → **Atlassian**, **Sentry**, **Stripe**, **Chrome DevTools**. They can coexist with **KOMPLIAN-*** entries; avoid duplicating the same capability twice.
100
+ For Cursor OAuth/UI connectors: **Settings → MCP** → **Atlassian**, **Sentry**, **Stripe**, **Chrome DevTools**, **Vercel**, **Neon**. They can coexist with **KOMPLIAN-*** entries; avoid duplicating the same capability twice.
90
101
 
91
102
  ## 3. Restart
92
103
 
@@ -8,7 +8,7 @@
8
8
  * npx komplian onboard --yes
9
9
  */
10
10
 
11
- import { spawnSync } from "node:child_process";
11
+ import { spawnSync, spawn } from "node:child_process";
12
12
  import { existsSync, readFileSync, mkdirSync, cpSync, rmSync, readdirSync, statSync } from "node:fs";
13
13
  import { dirname, join, resolve, normalize } from "node:path";
14
14
  import { fileURLToPath } from "node:url";
@@ -41,21 +41,73 @@ function log(s = "") {
41
41
  console.log(s);
42
42
  }
43
43
 
44
+ /** Branding and repo progress: always visible (even when KOMPLIAN_CLI_QUIET=1). */
45
+ function ux(s = "") {
46
+ console.log(s);
47
+ }
48
+
49
+ /** Block-letter logo + box frame (OpenClaw-style table). */
44
50
  function banner() {
45
- log(
46
- [
47
- `${c.cyan}${c.bold}`,
48
- "██╗ ██╗ ██████╗ ███╗ ███╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗",
49
- "██║ ██╔╝ ██╔═══██╗ ████╗ ████║ ██╔══██╗ ██║ ██║ ██╔══██╗ ████╗ ██║",
50
- "█████╔╝ ██║ ██║ ██╔████╔██║ ██████╔╝ ██║ ██║ ███████║ ██╔██╗ ██║",
51
- "██╔═██╗ ██║ ██║ ██║╚██╔╝██║ ██╔═══╝ ██║ ██║ ██╔══██║ ██║╚██╗██║",
52
- "██║ ██╗ ╚██████╔╝ ██║ ╚═╝ ██║ ██║ ███████╗ ██║ ██║ ██║ ██║ ╚████║",
53
- "╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═══╝",
54
- `${c.reset}`,
55
- `${c.dim} Secure setup · GitHub CLI · git clone · Zero org OAuth setup${c.reset}`,
56
- "",
57
- ].join("\n")
58
- );
51
+ const art = [
52
+ "██╗ ██╗ ██████╗ ███╗ ███╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗",
53
+ "██║ ██╔╝ ██╔═══██╗ ████╗ ████║ ██╔══██╗ ██║ ██║ ██╔══██╗ ████╗ ██║",
54
+ "█████╔╝ ██║ ██║ ██╔████╔██║ ██████╔╝ ██║ ██║ ███████║ ██╔██╗ ██║",
55
+ "██╔═██╗ ██║ ██║ ██║╚██╔╝██║ ██╔═══╝ ██║ ██║ ██╔══██║ ██║╚██╗██║",
56
+ "██║ ██╗ ╚██████╔╝ ██║ ╚═╝ ██║ ██║ ███████╗ ██║ ██║ ██║ ██║ ╚████║",
57
+ "╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═══╝",
58
+ ];
59
+ const w = Math.max(...art.map((line) => [...line].length));
60
+ const pad = (line) => line + " ".repeat(w - [...line].length);
61
+ const horiz = "─".repeat(w + 2);
62
+ const b = `${c.cyan}${c.bold}`;
63
+ const x = c.reset;
64
+ ux(`${b}┌${horiz}┐${x}`);
65
+ for (const line of art) {
66
+ ux(`${b}│ ${pad(line)} │${x}`);
67
+ }
68
+ ux(`${b}└${horiz}┘${x}`);
69
+ ux(`${c.dim} Secure setup · GitHub CLI · Monorepo${c.reset}`);
70
+ ux("");
71
+ }
72
+
73
+ function startBatchSpinner(message) {
74
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
75
+ const cloud = `${c.blue}☁${c.reset}`;
76
+ let i = 0;
77
+ const id = setInterval(() => {
78
+ const fr = frames[i++ % frames.length];
79
+ process.stdout.write(
80
+ `\r ${cloud} ${c.cyan}${fr}${c.reset} ${c.dim}${message}${c.reset} `
81
+ );
82
+ }, 90);
83
+ return {
84
+ stop() {
85
+ clearInterval(id);
86
+ process.stdout.write("\r\x1b[K");
87
+ },
88
+ };
89
+ }
90
+
91
+ function runSpawn(cmd, args, cwd) {
92
+ return new Promise((resolve) => {
93
+ const child = spawn(cmd, args, {
94
+ ...spawnWin({
95
+ cwd,
96
+ stdio: ["ignore", "pipe", "pipe"],
97
+ }),
98
+ });
99
+ let stderr = "";
100
+ child.stderr?.setEncoding?.("utf8");
101
+ child.stderr?.on("data", (d) => {
102
+ stderr += String(d);
103
+ });
104
+ child.on("close", (code) => {
105
+ resolve({ status: code === 0 ? 0 : code ?? 1, stderr });
106
+ });
107
+ child.on("error", (err) => {
108
+ resolve({ status: 1, stderr: String(err?.message || err) });
109
+ });
110
+ });
59
111
  }
60
112
 
61
113
  function ghOk() {
@@ -71,8 +123,8 @@ function ghOk() {
71
123
  }
72
124
 
73
125
  function runGhAuth() {
74
- log(
75
- `${c.yellow}→${c.reset} Abre ${c.bold}GitHub${c.reset} en el navegador (OAuth). No vemos tu contraseña.`
126
+ ux(
127
+ `${c.yellow}→${c.reset} Opening ${c.bold}GitHub${c.reset} in your browser (OAuth). Your password is not shown here.`
76
128
  );
77
129
  const r = spawnSync(
78
130
  "gh",
@@ -102,32 +154,32 @@ function verifyOrgMembership(org) {
102
154
  try {
103
155
  const j = JSON.parse(mem.stdout);
104
156
  if (j.state === "active") return;
105
- log(
106
- `${c.red}✗${c.reset} Membresía en ${c.bold}${org}${c.reset} no activa (state: ${j.state || "?"}).`
157
+ console.error(
158
+ `${c.red}✗${c.reset} Org ${c.bold}${org}${c.reset} membership not active (state: ${j.state || "?"}).`
107
159
  );
108
160
  process.exit(1);
109
161
  } catch {
110
- log(`${c.red}✗${c.reset} Respuesta inválida al verificar la org.`);
162
+ console.error(`${c.red}✗${c.reset} Invalid response while verifying org membership.`);
111
163
  process.exit(1);
112
164
  }
113
165
  }
114
166
  const hint = (mem.stderr + mem.stdout).toLowerCase();
115
167
  if (hint.includes("404") || hint.includes("not found")) {
116
- log(
117
- `${c.red}✗${c.reset} Esta cuenta ${c.bold}no es miembro${c.reset} de la org ${c.bold}${org}${c.reset}.`
168
+ console.error(
169
+ `${c.red}✗${c.reset} This account is ${c.bold}not a member${c.reset} of org ${c.bold}${org}${c.reset}.`
118
170
  );
119
- log(
120
- `${c.dim} ¿Otra cuenta? gh auth logout && gh auth login -h github.com -s repo -s read:org -w${c.reset}`
171
+ console.error(
172
+ `${c.dim} Wrong account? gh auth logout && gh auth login -h github.com -s repo -s read:org -w${c.reset}`
121
173
  );
122
174
  process.exit(1);
123
175
  }
124
176
  if (hint.includes("403") || hint.includes("read:org") || hint.includes("scope")) {
125
- log(`${c.red}✗${c.reset} Falta scope ${c.bold}read:org${c.reset} en gh.`);
126
- log(`${c.dim} gh auth refresh -h github.com -s repo -s read:org${c.reset}`);
177
+ console.error(`${c.red}✗${c.reset} GitHub CLI needs ${c.bold}read:org${c.reset} scope.`);
178
+ console.error(`${c.dim} gh auth refresh -h github.com -s repo -s read:org${c.reset}`);
127
179
  process.exit(1);
128
180
  }
129
- log(
130
- `${c.red}✗${c.reset} No se pudo verificar la org (código ${mem.status}):\n${c.dim}${(mem.stderr || mem.stdout).trim()}${c.reset}`
181
+ console.error(
182
+ `${c.red}✗${c.reset} Could not verify org (exit ${mem.status}):\n${c.dim}${(mem.stderr || mem.stdout).trim()}${c.reset}`
131
183
  );
132
184
  process.exit(1);
133
185
  }
@@ -159,10 +211,12 @@ function needCmd(name, hint = "") {
159
211
  ? canRun("git", ["--version"])
160
212
  : canRun(name, ["--version"]);
161
213
  if (!ok) {
162
- log(`${c.red}✗${c.reset} No se puede ejecutar ${c.bold}${name}${c.reset} (¿PATH o instalación?).${hint ? ` ${hint}` : ""}`);
214
+ console.error(
215
+ `${c.red}✗${c.reset} Cannot run ${c.bold}${name}${c.reset} (PATH or install?).${hint ? ` ${hint}` : ""}`
216
+ );
163
217
  if (IS_WIN) {
164
- log(
165
- `${c.dim} Windows: cierra y abre la terminal tras instalar gh/git, o reinicia para refrescar PATH.${c.reset}`
218
+ console.error(
219
+ `${c.dim} Windows: restart the terminal after installing gh/git, or reboot to refresh PATH.${c.reset}`
166
220
  );
167
221
  }
168
222
  process.exit(1);
@@ -180,7 +234,7 @@ function ghRepoList(org) {
180
234
  spawnWin({ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
181
235
  );
182
236
  if (r.status !== 0) {
183
- log(`${c.red}✗${c.reset} gh repo list falló:\n${r.stderr || r.stdout}`);
237
+ console.error(`${c.red}✗${c.reset} gh repo list failed:\n${r.stderr || r.stdout}`);
184
238
  process.exit(1);
185
239
  }
186
240
  const rows = JSON.parse(r.stdout || "[]");
@@ -209,8 +263,8 @@ function resolveRepos(cfg, org, teamArg, allRepos) {
209
263
 
210
264
  const teams = cfg.teams || {};
211
265
  if (!teams[team]) {
212
- log(
213
- `${c.red}✗${c.reset} Equipo desconocido ${c.bold}${team}${c.reset}. Válidos: ${Object.keys(teams).sort().join(", ")}`
266
+ console.error(
267
+ `${c.red}✗${c.reset} Unknown team ${c.bold}${team}${c.reset}. Valid: ${Object.keys(teams).sort().join(", ")}`
214
268
  );
215
269
  process.exit(2);
216
270
  }
@@ -233,52 +287,27 @@ function isSafeTargetDir(abs) {
233
287
  return !bad.includes(n);
234
288
  }
235
289
 
236
- function cloudLine(org, name, status) {
237
- const cloud = `${c.blue}☁${c.reset}`;
238
- if (status === "ok") {
239
- return ` ${cloud} ${c.green}✓${c.reset} ${c.bold}${org}/${name}${c.reset} ${c.dim}clonado${c.reset}`;
240
- }
241
- if (status === "skip") {
242
- return ` ${cloud} ${c.yellow}○${c.reset} ${org}/${name} ${c.dim}(ya existe)${c.reset}`;
243
- }
244
- if (status === "fail") {
245
- return ` ${cloud} ${c.red}✗${c.reset} ${org}/${name} ${c.dim}falló${c.reset}`;
246
- }
247
- return ` ${cloud} ${c.cyan}…${c.reset} ${org}/${name} ${c.dim}clonando…${c.reset}`;
290
+ function summarizeCloneLine(repos, outcomes) {
291
+ const parts = repos.map((name) => {
292
+ const st = outcomes.get(name) || "fail";
293
+ if (st === "fail") return `${c.red}✗${c.reset} ${name}`;
294
+ if (st === "skip") return `${c.green}✓${c.reset} ${c.dim}${name}${c.reset}`;
295
+ return `${c.green}✓${c.reset} ${name}`;
296
+ });
297
+ return ` ${c.blue}☁${c.reset} ${parts.join(` `)}`;
248
298
  }
249
299
 
250
- function cloneOne(org, name, workspace, useSsh) {
251
- const q = process.env.KOMPLIAN_CLI_QUIET === "1";
252
- const gitDir = join(workspace, name, ".git");
253
- if (existsSync(gitDir)) {
254
- if (q) console.log(`${c.yellow}○${c.reset} ${name}`);
255
- else log(cloudLine(org, name, "skip"));
256
- return true;
257
- }
258
- if (!q) process.stdout.write(`\r${cloudLine(org, name, "pending")}`);
259
- const args = useSsh
260
- ? ["clone", `git@github.com:${org}/${name}.git`, name]
261
- : ["repo", "clone", `${org}/${name}`, name];
262
- const cmd = useSsh ? "git" : "gh";
263
- const r = spawnSync(cmd, args, {
264
- ...spawnWin({
265
- cwd: workspace,
266
- encoding: "utf8",
267
- stdio: ["ignore", "pipe", "pipe"],
268
- }),
269
- });
270
- if (!q) process.stdout.write("\r\x1b[K");
271
- if (r.status === 0) {
272
- if (q) console.log(`${c.green}✓${c.reset} ${name}`);
273
- else log(cloudLine(org, name, "ok"));
274
- return true;
275
- }
276
- if (q) console.error(`${c.red}✗${c.reset} ${name}`);
277
- else {
278
- log(cloudLine(org, name, "fail"));
279
- if (r.stderr) log(`${c.dim}${r.stderr.trim()}${c.reset}`);
280
- }
281
- return false;
300
+ function summarizeDepsLine(results) {
301
+ const ran = results.filter((r) => !r.skipped);
302
+ if (ran.length === 0) {
303
+ return ` ${c.blue}☁${c.reset} ${c.dim}dependencies — nothing to install${c.reset}`;
304
+ }
305
+ const parts = ran.map((r) =>
306
+ r.ok
307
+ ? `${c.green}✓${c.reset} ${r.name}`
308
+ : `${c.yellow}○${c.reset} ${r.name}`
309
+ );
310
+ return ` ${c.blue}☁${c.reset} ${parts.join(` `)}`;
282
311
  }
283
312
 
284
313
  function copyCursorPack(workspace, cursorRepoUrl) {
@@ -286,7 +315,7 @@ function copyCursorPack(workspace, cursorRepoUrl) {
286
315
  if (cursorRepoUrl && String(cursorRepoUrl).trim()) {
287
316
  const tmp = join(workspace, ".cursor-bootstrap-tmp");
288
317
  rmSync(tmp, { recursive: true, force: true });
289
- log(`${c.dim}→${c.reset} Cursor pack (clone superficial)…`);
318
+ ux(`${c.dim}→${c.reset} Cursor rules pack…`);
290
319
  const r = spawnSync(
291
320
  "git",
292
321
  ["clone", "--depth", "1", String(cursorRepoUrl).trim(), tmp],
@@ -300,15 +329,18 @@ function copyCursorPack(workspace, cursorRepoUrl) {
300
329
  rmSync(rootCursor, { recursive: true, force: true });
301
330
  cpSync(join(tmp, ".cursor"), rootCursor, { recursive: true });
302
331
  rmSync(tmp, { recursive: true, force: true });
303
- log(`${c.green}✓${c.reset} ${c.bold}.cursor${c.reset} ${c.dim}KOMPLIAN_CURSOR_REPO${c.reset}`);
332
+ ux(`${c.green}✓${c.reset} ${c.bold}.cursor${c.reset} ${c.dim}(KOMPLIAN_CURSOR_REPO)${c.reset}`);
304
333
  return;
305
334
  }
306
335
  rmSync(tmp, { recursive: true, force: true });
307
- log(`${c.yellow}○${c.reset} KOMPLIAN_CURSOR_REPO: falló el clone o no hay .cursor/ en la raíz`);
336
+ ux(
337
+ `${c.yellow}○${c.reset} KOMPLIAN_CURSOR_REPO: clone failed or no .cursor/ at repo root`
338
+ );
339
+ } else if (!process.env.KOMPLIAN_CLI_QUIET) {
340
+ log(
341
+ `${c.dim}○ No .cursor/ (optional: KOMPLIAN_CURSOR_REPO=<git url>).${c.reset}`
342
+ );
308
343
  }
309
- log(
310
- `${c.dim}○ Sin .cursor/ en el workspace (opcional: KOMPLIAN_CURSOR_REPO=<url git>).${c.reset}`
311
- );
312
344
  }
313
345
 
314
346
  /** Sin esto, `npm install` crea o retoca package-lock.json y git muestra cambios sin querer. */
@@ -318,12 +350,13 @@ function npmQuietFlags() {
318
350
  return audit ? [] : ["--no-audit", "--no-fund"];
319
351
  }
320
352
 
321
- function npmInstallOneRepo(dir, name) {
353
+ function npmInstallOneRepo(dir, name, opts = {}) {
354
+ const silent = opts.silent === true;
322
355
  const pkg = join(dir, "package.json");
323
356
  if (!existsSync(pkg)) return { ok: true, skipped: true };
324
357
 
325
- const stdio =
326
- process.env.KOMPLIAN_CLI_QUIET === "1" ? "ignore" : "inherit";
358
+ const q = process.env.KOMPLIAN_CLI_QUIET === "1";
359
+ const stdio = silent || q ? "ignore" : "inherit";
327
360
 
328
361
  const yarnLock = join(dir, "yarn.lock");
329
362
  const pnpmLock = join(dir, "pnpm-lock.yaml");
@@ -331,12 +364,22 @@ function npmInstallOneRepo(dir, name) {
331
364
 
332
365
  if (existsSync(yarnLock)) {
333
366
  if (!canRun("yarn", ["--version"])) {
334
- log(
335
- `${c.yellow}○${c.reset} ${name} ${c.dim}(yarn.lock; instala yarn o ejecuta yarn install a mano)${c.reset}`
336
- );
367
+ if (!silent) {
368
+ log(
369
+ `${c.yellow}○${c.reset} ${name} ${c.dim}(yarn.lock; install yarn or run yarn install manually)${c.reset}`
370
+ );
371
+ }
337
372
  return { ok: true, skipped: true };
338
373
  }
339
- log(`${c.dim}→${c.reset} ${name} ${c.dim}(yarn)${c.reset}`);
374
+ if (!silent) {
375
+ if (q) {
376
+ ux(
377
+ ` ${c.blue}☁${c.reset} ${c.cyan}…${c.reset} ${c.bold}${name}${c.reset} ${c.dim}yarn…${c.reset}`
378
+ );
379
+ } else {
380
+ log(`${c.dim}→${c.reset} ${name} ${c.dim}(yarn)${c.reset}`);
381
+ }
382
+ }
340
383
  const r = spawnSync(
341
384
  "yarn",
342
385
  ["install", "--frozen-lockfile"],
@@ -347,12 +390,22 @@ function npmInstallOneRepo(dir, name) {
347
390
 
348
391
  if (existsSync(pnpmLock)) {
349
392
  if (!canRun("pnpm", ["--version"])) {
350
- log(
351
- `${c.yellow}○${c.reset} ${name} ${c.dim}(pnpm-lock; instala pnpm o pnpm install a mano)${c.reset}`
352
- );
393
+ if (!silent) {
394
+ log(
395
+ `${c.yellow}○${c.reset} ${name} ${c.dim}(pnpm-lock; install pnpm or run pnpm install manually)${c.reset}`
396
+ );
397
+ }
353
398
  return { ok: true, skipped: true };
354
399
  }
355
- log(`${c.dim}→${c.reset} ${name} ${c.dim}(pnpm)${c.reset}`);
400
+ if (!silent) {
401
+ if (q) {
402
+ ux(
403
+ ` ${c.blue}☁${c.reset} ${c.cyan}…${c.reset} ${c.bold}${name}${c.reset} ${c.dim}pnpm…${c.reset}`
404
+ );
405
+ } else {
406
+ log(`${c.dim}→${c.reset} ${name} ${c.dim}(pnpm)${c.reset}`);
407
+ }
408
+ }
356
409
  const r = spawnSync(
357
410
  "pnpm",
358
411
  ["install", "--frozen-lockfile"],
@@ -362,23 +415,43 @@ function npmInstallOneRepo(dir, name) {
362
415
  }
363
416
 
364
417
  if (!canRun("npm", ["--version"])) {
365
- log(`${c.yellow}○${c.reset} npm no está en PATH — omito ${name}`);
418
+ if (!silent) {
419
+ log(`${c.yellow}○${c.reset} npm not in PATH — skipping ${name}`);
420
+ }
366
421
  return { ok: true, skipped: true };
367
422
  }
368
423
 
369
424
  const quiet = npmQuietFlags();
370
425
 
371
426
  if (existsSync(npmLock)) {
372
- log(`${c.dim}→${c.reset} ${name} ${c.dim}(npm ci — lock sin cambios)${c.reset}`);
427
+ if (!silent) {
428
+ if (q) {
429
+ ux(
430
+ ` ${c.blue}☁${c.reset} ${c.cyan}…${c.reset} ${c.bold}${name}${c.reset} ${c.dim}npm ci…${c.reset}`
431
+ );
432
+ } else {
433
+ log(`${c.dim}→${c.reset} ${name} ${c.dim}(npm ci)${c.reset}`);
434
+ }
435
+ }
373
436
  const r = spawnSync("npm", ["ci", ...quiet], spawnWin({ cwd: dir, stdio }));
374
437
  if (r.status === 0) return { ok: true, skipped: false };
375
- log(
376
- `${c.yellow}○${c.reset} ${name}: npm ci falló (¿lock desincronizado?). ${c.dim}Revisa con npm install en ese repo.${c.reset}`
377
- );
438
+ if (!silent) {
439
+ log(
440
+ `${c.yellow}○${c.reset} ${name}: npm ci failed (lock out of sync?). ${c.dim}Try npm install in that repo.${c.reset}`
441
+ );
442
+ }
378
443
  return { ok: false, skipped: false };
379
444
  }
380
445
 
381
- log(`${c.dim}→${c.reset} ${name} ${c.dim}(npm install — sin crear package-lock)${c.reset}`);
446
+ if (!silent) {
447
+ if (q) {
448
+ ux(
449
+ ` ${c.blue}☁${c.reset} ${c.cyan}…${c.reset} ${c.bold}${name}${c.reset} ${c.dim}npm install…${c.reset}`
450
+ );
451
+ } else {
452
+ log(`${c.dim}→${c.reset} ${name} ${c.dim}(npm install — no new lockfile)${c.reset}`);
453
+ }
454
+ }
382
455
  const r = spawnSync(
383
456
  "npm",
384
457
  ["install", ...quiet, "--no-package-lock"],
@@ -387,52 +460,47 @@ function npmInstallOneRepo(dir, name) {
387
460
  return { ok: r.status === 0, skipped: false };
388
461
  }
389
462
 
390
- function npmInstallEach(workspace) {
391
- const q = process.env.KOMPLIAN_CLI_QUIET === "1";
392
- if (!q) {
393
- log("");
394
- log(`${c.cyan}━━ dependencies ━━${c.reset}`);
395
- }
463
+ /** Silent installs for batch UX (single spinner + one summary line). */
464
+ function npmInstallBatch(workspace) {
465
+ const results = [];
396
466
  for (const ent of readdirSync(workspace)) {
397
467
  const d = join(workspace, ent);
398
468
  if (!statSync(d).isDirectory()) continue;
399
- const { ok, skipped } = npmInstallOneRepo(d, ent);
400
- if (skipped) continue;
401
- if (q) {
402
- console.log(`${ok ? c.green + "✓" + c.reset : c.yellow + "○" + c.reset} ${ent}`);
403
- } else if (ok) {
404
- log(`${c.green}✓${c.reset} ${ent}`);
405
- } else {
406
- log(`${c.yellow}○${c.reset} ${ent}`);
407
- }
469
+ const { ok, skipped } = npmInstallOneRepo(d, ent, { silent: true });
470
+ results.push({ name: ent, ok, skipped });
408
471
  }
472
+ return results;
409
473
  }
410
474
 
411
475
  function usage() {
412
- log(`Uso: komplian setup | onboard | postman | mcp-tools | db:all:dev | localhost | db …`);
413
- 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}`);
414
- log(` ${c.bold}Setup por pasos:${c.reset}`);
415
- log(` 1. npx komplian onboard --yes`);
416
- log(` 2. npx komplian postman --yes ${c.dim}(@komplian.com)${c.reset}`);
417
- log(` 3. npx komplian mcp-tools --yes`);
418
- log(` 4. npx komplian db:all:dev ${c.dim}(Postman + 3 URLs dev → ~/.komplian + .env.local)${c.reset}`);
419
- log(` 5. npx komplian localhost --yes`);
420
- log(``);
421
- log(` npx komplian db:app:development ${c.dim}(psql una DB)${c.reset}`);
422
- log(``);
423
- log(` Antes (una vez): gh auth login -h github.com -s repo -s read:org -w`);
424
- log(` Requisitos: Node 18+, git, GitHub CLI (gh)`);
425
- log(``);
426
- log(` onboard implica --install salvo --no-install`);
427
- log(` [carpeta] Destino (por defecto: directorio actual, no ~/komplian)`);
428
- log(` -y, --yes Sin menú interactivo (equipo por defecto del JSON)`);
429
- log(` -t, --team <slug> Equipo en komplian-team-repos.json`);
430
- log(` -i, --install npm install en cada repo con package.json`);
431
- log(` --no-install No ejecutar npm install`);
432
- log(` --all-repos Todos los repos visibles (menos exclude_from_all)`);
433
- log(` --ssh Clonar con git@github.com:…`);
434
- log(` --list-teams Lista slugs de equipo (solo JSON) y sale`);
435
- log(` -h, --help`);
476
+ console.log(`Usage: komplian setup | onboard | postman | mcp-tools | db:all:dev | localhost | db …`);
477
+ console.log(
478
+ ` ${c.bold}All-in-one:${c.reset} ${c.cyan}npx komplian setup${c.reset} ${c.dim}(onboard+postman+mcp+db+localhost; browser form if needed)${c.reset}`
479
+ );
480
+ console.log(` ${c.bold}Step by step:${c.reset}`);
481
+ console.log(` 1. npx komplian onboard --yes`);
482
+ console.log(` 2. npx komplian postman --yes ${c.dim}(@komplian.com)${c.reset}`);
483
+ console.log(` 3. npx komplian mcp-tools --yes`);
484
+ console.log(
485
+ ` 4. npx komplian db:all:dev ${c.dim}(Postman + 3 dev URLs → ~/.komplian + .env.local)${c.reset}`
486
+ );
487
+ console.log(` 5. npx komplian localhost --yes`);
488
+ console.log(``);
489
+ console.log(` npx komplian db:app:development ${c.dim}(psql one DB)${c.reset}`);
490
+ console.log(``);
491
+ console.log(` Once: gh auth login -h github.com -s repo -s read:org -w`);
492
+ console.log(` Requires: Node 18+, git, GitHub CLI (gh)`);
493
+ console.log(``);
494
+ console.log(` onboard implies --install unless --no-install`);
495
+ console.log(` [folder] Target (default: cwd, not ~/komplian)`);
496
+ console.log(` -y, --yes Non-interactive (default team from JSON)`);
497
+ console.log(` -t, --team <slug> Team in komplian-team-repos.json`);
498
+ console.log(` -i, --install npm install in each repo with package.json`);
499
+ console.log(` --no-install Skip npm install`);
500
+ console.log(` --all-repos All visible repos (minus exclude_from_all)`);
501
+ console.log(` --ssh Clone with git@github.com:…`);
502
+ console.log(` --list-teams Print team slugs (JSON only) and exit`);
503
+ console.log(` -h, --help`);
436
504
  }
437
505
 
438
506
  function parseArgs(argv) {
@@ -459,8 +527,8 @@ function parseArgs(argv) {
459
527
  else if (a === "-h" || a === "--help") out.help = true;
460
528
  else if (a === "-t" || a === "--team") {
461
529
  out.team = argv[++i] || "";
462
- } else if (a.startsWith("-")) {
463
- log(`${c.red}✗${c.reset} Opción desconocida: ${a}`);
530
+ } else if (a.startsWith("-")) {
531
+ console.error(`${c.red}✗${c.reset} Unknown option: ${a}`);
464
532
  usage();
465
533
  process.exit(1);
466
534
  } else rest.push(a);
@@ -560,20 +628,22 @@ async function main() {
560
628
  }
561
629
 
562
630
  if (!existsSync(configPath)) {
563
- log(`${c.red}✗${c.reset} Falta la config: ${configPath}`);
631
+ console.error(`${c.red}✗${c.reset} Missing config: ${configPath}`);
564
632
  process.exit(1);
565
633
  }
566
634
 
567
635
  const cfg = loadConfig(configPath);
568
636
 
569
637
  if (args.listTeams) {
570
- log(Object.keys(cfg.teams || {}).sort().join("\n"));
638
+ console.log(Object.keys(cfg.teams || {}).sort().join("\n"));
571
639
  return;
572
640
  }
573
641
 
574
642
  const nodeMajor = Number(process.versions.node.split(".")[0], 10);
575
643
  if (nodeMajor < 18) {
576
- log(`${c.red}✗${c.reset} Hace falta Node 18+. Tienes ${process.versions.node}.`);
644
+ console.error(
645
+ `${c.red}✗${c.reset} Node 18+ required. You have ${process.versions.node}.`
646
+ );
577
647
  process.exit(1);
578
648
  }
579
649
 
@@ -593,18 +663,16 @@ async function main() {
593
663
  process.exit(1);
594
664
  }
595
665
  }
596
- if (!process.env.KOMPLIAN_CLI_QUIET) {
597
- log(`${c.green}✓${c.reset} GitHub CLI OK`);
598
- }
666
+
667
+ banner();
668
+ ux(`${c.green}✓${c.reset} GitHub CLI`);
599
669
 
600
670
  const org = process.env.KOMPLIAN_ORG || cfg.org || "Komplian";
601
671
  verifyOrgMembership(org);
602
- logGhIdentity();
603
- log(
604
- `${c.green}✓${c.reset} Org ${c.bold}${org}${c.reset}: ${c.dim}membresía activa${c.reset}`
605
- );
606
-
607
- banner();
672
+ if (!process.env.KOMPLIAN_CLI_QUIET) {
673
+ logGhIdentity();
674
+ }
675
+ ux(`${c.green}✓${c.reset} Org ${c.bold}${org}${c.reset}`);
608
676
 
609
677
  let team = args.team;
610
678
  if (!team && !args.yes && !args.allRepos) {
@@ -613,7 +681,9 @@ async function main() {
613
681
 
614
682
  const repos = resolveRepos(cfg, org, team, args.allRepos);
615
683
  if (repos.length === 0) {
616
- log(`${c.red}✗${c.reset} No hay repos que clonar (permisos / equipo / org).`);
684
+ console.error(
685
+ `${c.red}✗${c.reset} No repositories to clone (permissions / team / org).`
686
+ );
617
687
  process.exit(1);
618
688
  }
619
689
 
@@ -623,40 +693,85 @@ async function main() {
623
693
  }
624
694
  const abs = resolve(workspace.replace(/^~(?=$|[/\\])/, homedir()));
625
695
  if (!isSafeTargetDir(abs)) {
626
- log(`${c.red}✗${c.reset} Carpeta destino no segura: ${abs}`);
696
+ console.error(`${c.red}✗${c.reset} Unsafe destination folder: ${abs}`);
627
697
  process.exit(1);
628
698
  }
629
699
 
630
700
  mkdirSync(abs, { recursive: true });
631
- log("");
632
- log(`${c.cyan}━━ Workspace ━━${c.reset} ${c.bold}${abs}${c.reset}`);
633
- log(`${c.cyan}━━ Org ━━${c.reset} ${c.bold}${org}${c.reset}`);
634
- if (team) log(`${c.cyan}━━ Equipo ━━${c.reset} ${c.bold}${team}${c.reset}`);
635
- log(`${c.cyan}━━ Repos (${repos.length}) ━━${c.reset}`);
636
- log("");
701
+ const q = process.env.KOMPLIAN_CLI_QUIET === "1";
702
+ ux("");
703
+ ux(`${c.dim}${abs}${c.reset}`);
704
+ if (team) ux(`${c.dim}team · ${team}${c.reset}`);
705
+ ux(`${c.dim}${repos.join(" · ")}${c.reset}`);
706
+ ux("");
707
+
708
+ const outcomes = new Map();
709
+ let spin = process.stdout.isTTY ? startBatchSpinner("Cloning repositories…") : null;
710
+ if (!spin) {
711
+ ux(` ${c.blue}☁${c.reset} ${c.dim}Cloning repositories…${c.reset}`);
712
+ }
637
713
 
638
714
  let failed = 0;
639
- for (const name of repos) {
640
- const ok = cloneOne(org, name, abs, args.ssh);
641
- if (!ok) failed += 1;
715
+ try {
716
+ for (const name of repos) {
717
+ const gitDir = join(abs, name, ".git");
718
+ if (existsSync(gitDir)) {
719
+ outcomes.set(name, "skip");
720
+ continue;
721
+ }
722
+ const cloneArgs = args.ssh
723
+ ? ["clone", `git@github.com:${org}/${name}.git`, name]
724
+ : ["repo", "clone", `${org}/${name}`, name];
725
+ const cmd = args.ssh ? "git" : "gh";
726
+ const r = await runSpawn(cmd, cloneArgs, abs);
727
+ if (r.status === 0) {
728
+ outcomes.set(name, "ok");
729
+ } else {
730
+ outcomes.set(name, "fail");
731
+ failed += 1;
732
+ const err = (r.stderr || "").trim();
733
+ if (err) console.error(`${c.dim}${err.slice(0, 500)}${c.reset}`);
734
+ }
735
+ }
736
+ } finally {
737
+ if (spin) spin.stop();
642
738
  }
643
739
 
740
+ ux(summarizeCloneLine(repos, outcomes));
741
+
644
742
  copyCursorPack(abs, process.env.KOMPLIAN_CURSOR_REPO);
645
743
 
646
744
  if (args.install) {
647
- npmInstallEach(abs);
745
+ spin = process.stdout.isTTY ? startBatchSpinner("Installing dependencies…") : null;
746
+ if (!spin) {
747
+ ux(` ${c.blue}☁${c.reset} ${c.dim}Installing dependencies…${c.reset}`);
748
+ }
749
+ let depResults = [];
750
+ try {
751
+ depResults = npmInstallBatch(abs);
752
+ } finally {
753
+ if (spin) spin.stop();
754
+ }
755
+ ux(summarizeDepsLine(depResults));
648
756
  }
649
757
 
650
- log("");
651
- log(`${c.cyan}━━ Listo ━━${c.reset}`);
758
+ ux("");
652
759
  if (failed > 0) {
653
- log(`${c.yellow}○${c.reset} ${failed} repo(s) fallaron — revisa acceso y reintenta.`);
760
+ ux(
761
+ `${c.yellow}○${c.reset} ${failed} repo(s) failed — check access and retry.`
762
+ );
763
+ } else {
764
+ ux(`${c.green}✓${c.reset} ${c.dim}Repositories ready${c.reset}`);
765
+ }
766
+ ux(`${c.green}✓${c.reset} Open in Cursor: ${c.bold}File → Open Folder → ${abs}${c.reset}`);
767
+ if (!q) {
768
+ log(
769
+ `${c.dim} package-lock: npm ci. No lock: npm install --no-package-lock. yarn/pnpm: frozen lockfile. KOMPLIAN_NPM_AUDIT=1 enables npm audit.${c.reset}`
770
+ );
771
+ log(
772
+ `${c.dim} Copy .env.example → .env per project; secrets in 1Password — never commit.${c.reset}`
773
+ );
654
774
  }
655
- log(`${c.green}✓${c.reset} Cursor: ${c.bold}File → Open Folder → ${abs}${c.reset}`);
656
- log(
657
- `${c.dim} Con package-lock.json: npm ci (no retoca el lock). Sin lock: npm install --no-package-lock. yarn/pnpm: lock congelado. KOMPLIAN_NPM_AUDIT=1 activa auditoría en npm.${c.reset}`
658
- );
659
- log(`${c.dim} .env.example → .env por proyecto; secretos en 1Password — nunca commit.${c.reset}`);
660
775
  }
661
776
 
662
777
  main().catch((e) => {
@@ -46,6 +46,11 @@ function formatHomePath(absPath) {
46
46
  return absPath;
47
47
  }
48
48
 
49
+ /** JSON exports never go into the monorepo root (avoids accidental commits). */
50
+ function defaultPostmanExportDir() {
51
+ return join(homedir(), ".komplian", "postman-export");
52
+ }
53
+
49
54
  function maskEmail(email) {
50
55
  if (!email || typeof email !== "string" || !email.includes("@")) {
51
56
  return "[sin email]";
@@ -855,7 +860,7 @@ function usage() {
855
860
  log(``);
856
861
  log(` -y, --yes Sin prompts extra (si falta API key y hay TTY, pide la clave una vez y guarda)`);
857
862
  log(` --export-only Solo escribe JSON en disco (no llama a la API de Postman)`);
858
- log(` --out <dir> Carpeta para export (por defecto: ./komplian-postman)`);
863
+ log(` --out <dir> Export folder (default: ~/.komplian/postman-export)`);
859
864
  log(` --dotenv <ruta> .env extra (además de api/.env, .env, KOMPLIAN_DOTENV)`);
860
865
  log(` -h, --help`);
861
866
  log(``);
@@ -965,23 +970,29 @@ export async function runPostman(argv) {
965
970
  apiKey,
966
971
  process.env.POSTMAN_WORKSPACE_ID
967
972
  );
968
- log(
969
- `${c.green}✓${c.reset} Workspace Postman: ${c.bold}${workspaceId}${c.reset}`
970
- );
973
+ if (!process.env.KOMPLIAN_CLI_QUIET) {
974
+ log(`${c.green}✓${c.reset} Postman workspace: ${c.bold}${workspaceId}${c.reset}`);
975
+ }
971
976
 
972
- const outBase = args.outDir
973
- ? resolve(args.outDir)
974
- : resolve(process.cwd(), "komplian-postman");
977
+ ensureSecureKomplianDir();
978
+ const outBase = args.outDir ? resolve(args.outDir) : defaultPostmanExportDir();
975
979
  mkdirSync(outBase, { recursive: true });
976
980
 
977
981
  const collPath = join(outBase, "Komplian-API.postman_collection.json");
978
982
  writeFileSync(collPath, JSON.stringify(collection, null, 2), "utf8");
979
- log(`${c.green}✓${c.reset} Export: ${c.dim}${collPath}${c.reset}`);
983
+ const quiet = process.env.KOMPLIAN_CLI_QUIET === "1";
984
+ if (!quiet) {
985
+ log(`${c.green}✓${c.reset} Export: ${c.dim}${collPath}${c.reset}`);
986
+ }
980
987
 
981
988
  for (const env of envsForExport) {
982
989
  const safe = env.name.replace(/[\\/]/g, "-") + ".postman_environment.json";
983
990
  writeFileSync(join(outBase, safe), JSON.stringify({ ...env }, null, 2), "utf8");
984
- log(`${c.green}✓${c.reset} Export: ${c.dim}${join(outBase, safe)}${c.reset} ${c.dim}(sin secretos; no commitear)${c.reset}`);
991
+ if (!quiet) {
992
+ log(
993
+ `${c.green}✓${c.reset} Export: ${c.dim}${join(outBase, safe)}${c.reset} ${c.dim}(no secrets)${c.reset}`
994
+ );
995
+ }
985
996
  }
986
997
 
987
998
  if (args.exportOnly) {
@@ -583,7 +583,6 @@ export async function runSetup(argv) {
583
583
 
584
584
  mkdirSync(workspaceAbs, { recursive: true });
585
585
 
586
- out(`${c.cyan}1/5${c.reset} repos`);
587
586
  const onboardArgs = ["--yes"];
588
587
  if (opts.team) onboardArgs.push("-t", opts.team);
589
588
  if (opts.allRepos) onboardArgs.push("--all-repos");
@@ -629,7 +628,6 @@ export async function runSetup(argv) {
629
628
  }
630
629
 
631
630
  if (useBrowser && (needsPostmanKey || needsDbUrls)) {
632
- out(`${c.cyan}browser${c.reset} local form`);
633
631
  try {
634
632
  const lp = neonOAuth && needsDbUrls ? parseListenFromRedirectUri() : null;
635
633
  const form = await runSetupBrowserForm({
@@ -652,16 +650,12 @@ export async function runSetup(argv) {
652
650
  }
653
651
  }
654
652
 
655
- out(`${c.cyan}2/5${c.reset} postman`);
656
653
  await runPostman(["--yes"]);
657
654
 
658
- out(`${c.cyan}3/5${c.reset} mcp`);
659
655
  await runMcpTools(["--yes"]);
660
656
 
661
- out(`${c.cyan}4/5${c.reset} databases`);
662
657
  await runDbAllDev(["--yes", "-w", monorepoRoot]);
663
658
 
664
- out(`${c.cyan}5/5${c.reset} dev`);
665
659
  await runLocalhost(["--yes"]);
666
660
 
667
661
  out(`${c.green}✓${c.reset} ready`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "komplian",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Komplian CLI: setup (all-in-one), onboard, Postman, localhost, mcp-tools, db (psql). Node 18+.",
5
5
  "type": "module",
6
6
  "engines": {