komplian 0.4.5 → 0.4.7

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.
@@ -1,22 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Komplian localhost — genera `.env.local` con secretos aleatorios (crypto) y arranca
4
- * api, app, web, admin, docs en paralelo (concurrently).
3
+ * Komplian localhost — genera `.env.local` completos a partir de cada proyecto `/.env.example`
4
+ * (misma estructura y comentarios) + secretos aleatorios (crypto). Arranca api, app, web, admin, docs.
5
5
  *
6
- * Seguridad: nada se commitea; secretos solo en disco local (.env*.local / .komplian/).
7
- * No imprime valores de claves.
6
+ * Seguridad: los `.env*` no van en el paquete npm `komplian` (solo *.mjs + JSON + README).
7
+ * No imprime secretos. Tras generar, muestra tabla de URLs locales (puertos fijos).
8
8
  *
9
- * Uso:
10
- * npx komplian localhost --yes
11
- * npx komplian localhost --yes --minimal # solo app + web + docs (sin DB real)
12
- * npx komplian localhost --env-only --force # solo escribir env
13
- *
14
- * URLs de Neon (opcional, una o tres):
15
- * KOMPLIAN_LOCALHOST_DATABASE_URL # misma para app/admin/web/api
16
- * KOMPLIAN_LOCALHOST_APP_DATABASE_URL
17
- * KOMPLIAN_LOCALHOST_ADMIN_DATABASE_URL
18
- * KOMPLIAN_LOCALHOST_WEB_DATABASE_URL
19
- * O archivo (gitignored): komplian-localhost.secrets.env o .komplian/localhost-secrets.env
9
+ * Neon: KOMPLIAN_LOCALHOST_APP_DATABASE_URL, …_ADMIN_…, …_WEB_… o KOMPLIAN_LOCALHOST_SECRETS.env
10
+ * (plantilla en la raíz del monorepo: KOMPLIAN_LOCALHOST_SECRETS.env.example).
20
11
  */
21
12
 
22
13
  import { randomBytes } from "node:crypto";
@@ -72,6 +63,8 @@ function parseEnvFile(content) {
72
63
 
73
64
  function loadSecretsFromDisk(workspaceRoot) {
74
65
  const paths = [
66
+ join(workspaceRoot, "KOMPLIAN_LOCALHOST_SECRETS.env"),
67
+ join(workspaceRoot, ".komplian", "KOMPLIAN_LOCALHOST_SECRETS.env"),
75
68
  join(workspaceRoot, "komplian-localhost.secrets.env"),
76
69
  join(workspaceRoot, ".komplian", "localhost-secrets.env"),
77
70
  ];
@@ -153,120 +146,240 @@ function buildShared() {
153
146
  }
154
147
 
155
148
  function header(project) {
156
- return `# Generated by komplian localhost do not commit (${project})
157
- # Regenerate: npx komplian localhost --yes --force
149
+ return `# KOMPLIAN generado por komplian localhost (no commitear) ${project}
150
+ # Regenerar: npx komplian localhost --yes --force
151
+ # Los .env / .env.local no se publican en el paquete npm \`komplian\` (solo scripts/*.mjs).
158
152
 
159
153
  `;
160
154
  }
161
155
 
156
+ /** Serializa valor para KEY= en .env (comillas si hace falta). */
157
+ function formatEnvValue(val) {
158
+ if (val === undefined || val === null) return '""';
159
+ const s = String(val);
160
+ if (s === "") return '""';
161
+ if (/[\s#"']/.test(s) || s.includes("\n")) {
162
+ return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
163
+ }
164
+ return s;
165
+ }
166
+
167
+ function collectKeysFromEnvContent(content) {
168
+ const keys = new Set();
169
+ for (const line of content.split(/\r?\n/)) {
170
+ const t = line.trim();
171
+ if (!t || t.startsWith("#")) continue;
172
+ const eq = line.indexOf("=");
173
+ if (eq === -1) continue;
174
+ keys.add(line.slice(0, eq).trim());
175
+ }
176
+ return keys;
177
+ }
178
+
179
+ /**
180
+ * Sustituye claves presentes en el contenido y añade al final las que faltan.
181
+ * Mantiene comentarios al final de línea (# ...).
182
+ */
183
+ function applyOverridesToEnvContent(content, overrides) {
184
+ const present = collectKeysFromEnvContent(content);
185
+ const lines = content.split(/\r?\n/);
186
+ const out = lines.map((line) => {
187
+ const trimmed = line.trim();
188
+ if (!trimmed || trimmed.startsWith("#")) return line;
189
+ const eq = line.indexOf("=");
190
+ if (eq === -1) return line;
191
+ const key = line.slice(0, eq).trim();
192
+ if (!Object.prototype.hasOwnProperty.call(overrides, key)) return line;
193
+ const rest = line.slice(eq + 1);
194
+ const hashIdx = rest.search(/\s+#/);
195
+ const suffix = hashIdx >= 0 ? rest.slice(hashIdx) : "";
196
+ return `${key}=${formatEnvValue(overrides[key])}${suffix}`;
197
+ });
198
+ const append = [];
199
+ for (const [key, val] of Object.entries(overrides)) {
200
+ if (!present.has(key)) append.push(`${key}=${formatEnvValue(val)}`);
201
+ }
202
+ const tail =
203
+ append.length > 0
204
+ ? `\n\n# --- KOMPLIAN localhost (claves que no estaban en .env.example) ---\n${append.join("\n")}`
205
+ : "";
206
+ return out.join("\n") + tail;
207
+ }
208
+
209
+ function buildAppOverrides(s, db, opts) {
210
+ const appUrl = db.app || PLACEHOLDER_DB;
211
+ const useNoDb = opts.minimal || String(appUrl).includes("placeholder");
212
+ /** @type {Record<string, string>} */
213
+ const o = {
214
+ DATABASE_URL: appUrl,
215
+ NEXTAUTH_SECRET: s.nextAuthSecret,
216
+ NEXTAUTH_URL: "http://localhost:3001",
217
+ CSRF_SECRET: s.csrfSecret,
218
+ NEXT_PUBLIC_APP_URL: "http://localhost:3001",
219
+ KOMPLIAN_API_URL: "http://localhost:4000",
220
+ KOMPLIAN_API_KEY: s.apiKey,
221
+ KOMPLIAN_INTERNAL_WEBHOOK_SECRET: s.internalWebhook,
222
+ ENCRYPTION_KEY: s.encryptionKey,
223
+ OPENAI_API_KEY: "",
224
+ MAILBOX_OAUTH_SECRET: s.nextAuthSecret,
225
+ NEXT_PUBLIC_API_URL: "http://localhost:4000",
226
+ AUTH_SECRET: s.nextAuthSecret,
227
+ TWO_FACTOR_ENCRYPTION_KEY: s.twoFactorEnc,
228
+ HASH_SECRET: s.hashSecret,
229
+ };
230
+ if (useNoDb) o.DEV_MODE_NO_DB = "true";
231
+ return o;
232
+ }
233
+
234
+ function buildApiOverrides(s, db) {
235
+ return {
236
+ APP_DATABASE_URL: db.apiApp || PLACEHOLDER_DB,
237
+ ADMIN_DATABASE_URL: db.apiAdmin || PLACEHOLDER_DB,
238
+ WEB_DATABASE_URL: db.apiWeb || PLACEHOLDER_DB,
239
+ API_KEY: s.apiKey,
240
+ ADMIN_API_KEY: s.adminApiKey,
241
+ KOMPLIAN_INTERNAL_WEBHOOK_SECRET: s.internalWebhook,
242
+ JWT_SECRET: s.jwtSecret,
243
+ ENCRYPTION_KEY: s.encryptionKey,
244
+ OPENAI_API_KEY: "",
245
+ RESEND_API_KEY: "",
246
+ CRON_SECRET: s.cronSecret,
247
+ APP_URL: "http://localhost:3001",
248
+ WEB_URL: "http://localhost:3003",
249
+ ADMIN_URL: "http://localhost:3002",
250
+ API_BASE_URL: "http://localhost:4000",
251
+ PORT: "4000",
252
+ NODE_ENV: "development",
253
+ LOG_LEVEL: "debug",
254
+ };
255
+ }
256
+
257
+ function buildWebOverrides(s, db) {
258
+ return {
259
+ DATABASE_URL: db.web || PLACEHOLDER_DB,
260
+ KOMPLIAN_API_URL: "http://localhost:4000",
261
+ KOMPLIAN_API_KEY: s.apiKey,
262
+ };
263
+ }
264
+
265
+ function buildAdminOverrides(s, db) {
266
+ return {
267
+ DATABASE_URL: db.admin || PLACEHOLDER_DB,
268
+ NEXTAUTH_URL: "http://localhost:3002",
269
+ NEXTAUTH_SECRET: s.nextAuthAdmin,
270
+ AUTH_SECRET: s.nextAuthAdmin,
271
+ API_URL: "http://localhost:4000",
272
+ ADMIN_API_KEY: s.adminApiKey,
273
+ };
274
+ }
275
+
276
+ function buildDocsOverrides() {
277
+ return {
278
+ NODE_ENV: "development",
279
+ };
280
+ }
281
+
282
+ function envFromOverridesOnly(overrides) {
283
+ return Object.entries(overrides)
284
+ .map(([k, v]) => `${k}=${formatEnvValue(v)}`)
285
+ .join("\n");
286
+ }
287
+
162
288
  function writeAppEnv(root, s, db, opts) {
163
289
  const p = join(root, "app", ".env.local");
164
290
  if (existsSync(p) && !opts.force) return { path: p, skipped: true };
165
- const useNoDb = opts.minimal || !db.app || db.app.includes("placeholder");
166
- const lines = [
167
- header("app"),
168
- useNoDb ? "DEV_MODE_NO_DB=true" : "DEV_MODE_NO_DB=",
169
- "",
170
- `DATABASE_URL=${db.app || PLACEHOLDER_DB}`,
171
- `NEXTAUTH_URL=http://localhost:3001`,
172
- `NEXTAUTH_SECRET=${s.nextAuthSecret}`,
173
- `AUTH_SECRET=${s.nextAuthSecret}`,
174
- `CSRF_SECRET=${s.csrfSecret}`,
175
- "",
176
- `NEXT_PUBLIC_APP_URL=http://localhost:3001`,
177
- `NEXT_PUBLIC_API_URL=http://localhost:4000`,
178
- "",
179
- `KOMPLIAN_API_URL=http://localhost:4000`,
180
- `KOMPLIAN_API_KEY=${s.apiKey}`,
181
- `KOMPLIAN_INTERNAL_WEBHOOK_SECRET=${s.internalWebhook}`,
182
- "",
183
- `ENCRYPTION_KEY=${s.encryptionKey}`,
184
- `TWO_FACTOR_ENCRYPTION_KEY=${s.twoFactorEnc}`,
185
- `HASH_SECRET=${s.hashSecret}`,
186
- "",
187
- "# Optional: OPENAI_API_KEY= Stripe, Shopify, Google OAuth — ver app/.env.example",
188
- ];
189
- writeAtomic(p, lines.join("\n"));
291
+ const examplePath = join(root, "app", ".env.example");
292
+ const overrides = buildAppOverrides(s, db, opts);
293
+ let body;
294
+ if (existsSync(examplePath)) {
295
+ body = applyOverridesToEnvContent(readFileSync(examplePath, "utf8"), overrides);
296
+ } else {
297
+ body = envFromOverridesOnly(overrides);
298
+ }
299
+ writeAtomic(p, header("app") + body);
190
300
  return { path: p, skipped: false };
191
301
  }
192
302
 
193
303
  function writeApiEnv(root, s, db, opts) {
194
304
  const p = join(root, "api", ".env.local");
195
305
  if (existsSync(p) && !opts.force) return { path: p, skipped: true };
196
- const appU = db.apiApp || PLACEHOLDER_DB;
197
- const adminU = db.apiAdmin || PLACEHOLDER_DB;
198
- const webU = db.apiWeb || PLACEHOLDER_DB;
199
- const lines = [
200
- header("api"),
201
- `APP_DATABASE_URL=${appU}`,
202
- `ADMIN_DATABASE_URL=${adminU}`,
203
- `WEB_DATABASE_URL=${webU}`,
204
- "",
205
- `API_KEY=${s.apiKey}`,
206
- `ADMIN_API_KEY=${s.adminApiKey}`,
207
- `KOMPLIAN_INTERNAL_WEBHOOK_SECRET=${s.internalWebhook}`,
208
- `JWT_SECRET=${s.jwtSecret}`,
209
- `ENCRYPTION_KEY=${s.encryptionKey}`,
210
- "",
211
- "# OPENAI_API_KEY= RESEND_API_KEY= — ver api/.env.example",
212
- "",
213
- `CRON_SECRET=${s.cronSecret}`,
214
- "",
215
- `APP_URL=http://localhost:3001`,
216
- `WEB_URL=http://localhost:3003`,
217
- `ADMIN_URL=http://localhost:3002`,
218
- `API_BASE_URL=http://localhost:4000`,
219
- "",
220
- `PORT=4000`,
221
- `NODE_ENV=development`,
222
- `LOG_LEVEL=debug`,
223
- ];
224
- writeAtomic(p, lines.join("\n"));
306
+ const examplePath = join(root, "api", ".env.example");
307
+ const overrides = buildApiOverrides(s, db);
308
+ let body;
309
+ if (existsSync(examplePath)) {
310
+ body = applyOverridesToEnvContent(readFileSync(examplePath, "utf8"), overrides);
311
+ } else {
312
+ body = envFromOverridesOnly(overrides);
313
+ }
314
+ writeAtomic(p, header("api") + body);
225
315
  return { path: p, skipped: false };
226
316
  }
227
317
 
228
318
  function writeWebEnv(root, s, db, opts) {
229
319
  const p = join(root, "web", ".env.local");
230
320
  if (existsSync(p) && !opts.force) return { path: p, skipped: true };
231
- const lines = [
232
- header("web"),
233
- `DATABASE_URL=${db.web || PLACEHOLDER_DB}`,
234
- `KOMPLIAN_API_URL=http://localhost:4000`,
235
- `KOMPLIAN_API_KEY=${s.apiKey}`,
236
- "",
237
- "# GMAIL_USER= GMAIL_PASS= — opcional",
238
- ];
239
- writeAtomic(p, lines.join("\n"));
321
+ const examplePath = join(root, "web", ".env.example");
322
+ const overrides = buildWebOverrides(s, db);
323
+ let body;
324
+ if (existsSync(examplePath)) {
325
+ body = applyOverridesToEnvContent(readFileSync(examplePath, "utf8"), overrides);
326
+ } else {
327
+ body = envFromOverridesOnly(overrides);
328
+ }
329
+ writeAtomic(p, header("web") + body);
240
330
  return { path: p, skipped: false };
241
331
  }
242
332
 
243
333
  function writeAdminEnv(root, s, db, opts) {
244
334
  const p = join(root, "admin", ".env.local");
245
335
  if (existsSync(p) && !opts.force) return { path: p, skipped: true };
246
- const lines = [
247
- header("admin"),
248
- `DATABASE_URL=${db.admin || PLACEHOLDER_DB}`,
249
- `NEXTAUTH_URL=http://localhost:3002`,
250
- `NEXTAUTH_SECRET=${s.nextAuthAdmin}`,
251
- `AUTH_SECRET=${s.nextAuthAdmin}`,
252
- `API_URL=http://localhost:4000`,
253
- `ADMIN_API_KEY=${s.adminApiKey}`,
254
- ];
255
- writeAtomic(p, lines.join("\n"));
336
+ const examplePath = join(root, "admin", ".env.example");
337
+ const overrides = buildAdminOverrides(s, db);
338
+ let body;
339
+ if (existsSync(examplePath)) {
340
+ body = applyOverridesToEnvContent(readFileSync(examplePath, "utf8"), overrides);
341
+ } else {
342
+ body = envFromOverridesOnly(overrides);
343
+ }
344
+ writeAtomic(p, header("admin") + body);
256
345
  return { path: p, skipped: false };
257
346
  }
258
347
 
259
348
  function writeDocsEnv(root, opts) {
260
349
  const p = join(root, "docs", ".env.local");
261
350
  if (existsSync(p) && !opts.force) return { path: p, skipped: true };
262
- const lines = [
263
- header("docs"),
264
- "# Vacío: docs no requiere secretos para next dev local",
265
- ];
266
- writeAtomic(p, lines.join("\n"));
351
+ const examplePath = join(root, "docs", ".env.example");
352
+ if (!existsSync(examplePath)) {
353
+ writeAtomic(
354
+ p,
355
+ `${header("docs")}# Sin variables obligatorias para next dev.\n`
356
+ );
357
+ return { path: p, skipped: false };
358
+ }
359
+ const overrides = buildDocsOverrides();
360
+ let content = readFileSync(examplePath, "utf8");
361
+ content = applyOverridesToEnvContent(content, overrides);
362
+ writeAtomic(p, header("docs") + content);
267
363
  return { path: p, skipped: false };
268
364
  }
269
365
 
366
+ function printLocalUrls(minimal) {
367
+ log("");
368
+ log(`${c.bold}Local — abre en el navegador${c.reset}`);
369
+ if (!minimal) {
370
+ log(` ${c.cyan}API${c.reset} http://localhost:4000 ${c.dim}(/health)${c.reset}`);
371
+ }
372
+ log(` ${c.cyan}App${c.reset} http://localhost:3001`);
373
+ if (!minimal) {
374
+ log(` ${c.cyan}Admin${c.reset} http://localhost:3002`);
375
+ log(` ${c.cyan}Web${c.reset} http://localhost:3003`);
376
+ } else {
377
+ log(` ${c.cyan}Web${c.reset} http://localhost:3003`);
378
+ }
379
+ log(` ${c.cyan}Docs${c.reset} http://localhost:3004`);
380
+ log("");
381
+ }
382
+
270
383
  async function confirmOverwrite(yes) {
271
384
  if (yes) return true;
272
385
  const rl = createInterface({ input, output });
@@ -307,14 +420,14 @@ function parseLocalhostArgs(argv) {
307
420
  function usageLocalhost() {
308
421
  log(`Uso: npx komplian localhost [opciones] [carpeta-monorepo]`);
309
422
  log(``);
310
- log(` Genera .env.local por proyecto (secretos aleatorios) y ejecuta npm run dev en paralelo.`);
311
- log(` ${c.dim}Neon: export KOMPLIAN_LOCALHOST_DATABASE_URL=… o komplian-localhost.secrets.env${c.reset}`);
423
+ log(` Genera .env.local completos (desde cada .env.example + secretos KOMPLIAN) y arranca todos los dev servers.`);
424
+ log(` ${c.dim}Neon: KOMPLIAN_LOCALHOST_APP_DATABASE_URL, …_ADMIN_…, …_WEB_… o archivo KOMPLIAN_LOCALHOST_SECRETS.env${c.reset}`);
312
425
  log(``);
313
- log(` -y, --yes Sin confirmación interactiva`);
314
- log(` --force Sobrescribir .env.local aunque existan`);
315
- log(` --minimal Solo app + web + docs (omitir api y admin)`);
316
- log(` --env-only Solo escribir env, no arrancar servidores`);
317
- log(` -w, --workspace Ruta al monorepo (por defecto: cwd)`);
426
+ log(` -y, --yes Sin confirmación`);
427
+ log(` --force Regenerar .env.local`);
428
+ log(` --minimal Solo app + web + docs`);
429
+ log(` --env-only Solo escribir .env.local`);
430
+ log(` -w, --workspace Ruta al monorepo`);
318
431
  log(` -h, --help`);
319
432
  }
320
433
 
@@ -383,22 +496,24 @@ export async function runLocalhost(argv) {
383
496
  written.push(writeDocsEnv(workspaceRoot, opts));
384
497
 
385
498
  log("");
386
- log(`${c.cyan}━━ .env.local ━━${c.reset} ${c.dim}(permisos 600)${c.reset}`);
499
+ log(`${c.cyan}━━ .env.local ━━${c.reset} ${c.dim}(600, no se publican en npm)${c.reset}`);
387
500
  for (const w of written) {
388
501
  const st = w.skipped ? `${c.dim}sin cambios${c.reset}` : `${c.green}ok${c.reset}`;
389
502
  log(` ${st} ${w.path}`);
390
503
  }
391
504
 
505
+ printLocalUrls(opts.minimal);
506
+
392
507
  if (!db.hasReal && !opts.minimal) {
508
+ log(
509
+ `${c.yellow}⚠${c.reset} Sin Neon: exporta ${c.bold}KOMPLIAN_LOCALHOST_APP_DATABASE_URL${c.reset} / ${c.bold}ADMIN${c.reset} / ${c.bold}WEB${c.reset} o usa ${c.bold}KOMPLIAN_LOCALHOST_SECRETS.env${c.reset} (plantilla: KOMPLIAN_LOCALHOST_SECRETS.env.example).`
510
+ );
511
+ log(`${c.dim} O ${c.bold}--minimal${c.reset} (solo app + web + docs).${c.reset}`);
393
512
  log("");
394
- log(`${c.yellow}⚠${c.reset} Sin URLs Neon reales: api/admin/web usarán un placeholder local.`);
395
- log(` ${c.dim}Define KOMPLIAN_LOCALHOST_DATABASE_URL o komplian-localhost.secrets.env${c.reset}`);
396
- log(` ${c.dim}O usa ${c.bold}--minimal${c.reset} para solo app + web + docs.${c.reset}`);
397
513
  }
398
514
 
399
515
  if (opts.envOnly) {
400
- log("");
401
- log(`${c.green}✓${c.reset} Solo entorno. Arranca con: ${c.bold}npx komplian localhost --yes${c.reset}`);
516
+ log(`${c.green}✓${c.reset} Entorno listo. Arranca: ${c.bold}npx komplian localhost --yes${c.reset}`);
402
517
  return;
403
518
  }
404
519
 
@@ -423,9 +538,7 @@ export async function runLocalhost(argv) {
423
538
  return `npm run dev --prefix ${dir}`;
424
539
  });
425
540
 
426
- log("");
427
- log(`${c.cyan}━━ Servicios (${services.length}) ━━${c.reset} ${workspaceRoot}`);
428
- log(`${c.dim}Ctrl+C detiene todos.${c.reset}`);
541
+ log(`${c.cyan}━━ Arranque (${services.length}) ━━${c.reset} ${c.dim}Ctrl+C detiene todo${c.reset}`);
429
542
  log("");
430
543
 
431
544
  // shell: false — con shell:true Node concatena args para /bin/sh -c y concurrently
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Komplian MCP — escribe `.cursor/mcp.json` con servidores MCP por stdio (prefijo KOMPLIAN-*)
4
+ * y genera `.cursor/KOMPLIAN_MCP_SETUP.md` (plugins de Cursor que no van en JSON).
5
+ *
6
+ * No incluye secretos: tokens vacíos; rellena en Cursor o vía KOMPLIAN_MCP_SECRETS.env (local, gitignored).
7
+ *
8
+ * Uso: npx komplian mcp-tools --yes
9
+ */
10
+
11
+ import { mkdirSync, readFileSync, existsSync, writeFileSync } from "node:fs";
12
+ import { dirname, join, resolve } from "node:path";
13
+ import { homedir } from "node:os";
14
+ import { createInterface } from "node:readline/promises";
15
+ import { stdin as input, stdout as output } from "node:process";
16
+
17
+ const c = {
18
+ reset: "\x1b[0m",
19
+ dim: "\x1b[2m",
20
+ bold: "\x1b[1m",
21
+ cyan: "\x1b[36m",
22
+ green: "\x1b[32m",
23
+ red: "\x1b[31m",
24
+ yellow: "\x1b[33m",
25
+ };
26
+
27
+ function log(s = "") {
28
+ console.log(s);
29
+ }
30
+
31
+ /** Preset Komplian: solo claves KOMPLIAN-* para no pisar servidores del dev. */
32
+ const KOMPLIAN_MCP_PRESET = {
33
+ mcpServers: {
34
+ "KOMPLIAN-github": {
35
+ command: "npx",
36
+ args: ["-y", "@modelcontextprotocol/server-github"],
37
+ env: {
38
+ GITHUB_PERSONAL_ACCESS_TOKEN: "",
39
+ },
40
+ },
41
+ "KOMPLIAN-sentry": {
42
+ command: "npx",
43
+ args: ["-y", "@sentry/mcp-server@latest"],
44
+ env: {},
45
+ },
46
+ "KOMPLIAN-stripe": {
47
+ command: "npx",
48
+ args: ["-y", "@stripe/mcp"],
49
+ env: {
50
+ STRIPE_SECRET_KEY: "",
51
+ },
52
+ },
53
+ },
54
+ };
55
+
56
+ const SETUP_MD = `# KOMPLIAN — MCP en Cursor
57
+
58
+ Este archivo lo genera \`npx komplian mcp-tools\`. **No commitees** tokens; \`.cursor/mcp.json\` puede llevar env vacíos que rellenas solo en local.
59
+
60
+ ## 1. Servidores en \`.cursor/mcp.json\` (stdio / npx)
61
+
62
+ | ID | Qué es | Rellena |
63
+ |----|--------|---------|
64
+ | **KOMPLIAN-github** | GitHub API (issues, PRs) | \`GITHUB_PERSONAL_ACCESS_TOKEN\` en el bloque \`env\` del servidor (PAT con scopes repo/workflow según necesidad). El paquete npm está deprecado a favor del servidor oficial; si falla, sustituye por la config que indique [github/github-mcp-server](https://github.com/github/github-mcp-server). |
65
+ | **KOMPLIAN-sentry** | Sentry (issues, trazas) | Primera vez: login por navegador (device code). **Komplian:** \`organizationSlug\` = \`komplian\`, \`regionUrl\` = \`https://de.sentry.io\`. Proyectos: \`komplian-api\`, \`komplian-app\`. |
66
+ | **KOMPLIAN-stripe** | Stripe API | \`STRIPE_SECRET_KEY\` en \`env\` (mejor **Restricted key** / test en dev). Modo test vs live según [docs Stripe MCP](https://docs.stripe.com/mcp). |
67
+
68
+ ## 2. Solo en Cursor (plugins / integraciones)
69
+
70
+ Estos **no** se instalan con npx en este JSON; actívalos en **Cursor → Settings → MCP** (o integraciones) e inicia sesión:
71
+
72
+ | Integración | Uso en Komplian |
73
+ |-------------|-----------------|
74
+ | **Atlassian** (Jira + Confluence) | Site \`komplian.atlassian.net\`, proyecto Jira **KAPP**. Plugin \`plugin-atlassian-atlassian\` / \`user-mcp-atlassian\`. |
75
+ | **Chrome DevTools** | Depuración del navegador (\`user-chrome-devtools\`). |
76
+
77
+ ## 3. Reinicio
78
+
79
+ Tras editar \`mcp.json\`, **reinicia Cursor** (o recarga ventana) para cargar MCP.
80
+
81
+ ## 4. Referencia interna
82
+
83
+ Ver \`.cursor/rules/mcp-integrations.mdc\` en el monorepo (org Sentry, Stripe, reglas KAPP).
84
+ `;
85
+
86
+ function findWorkspaceRoot(start) {
87
+ let dir = resolve(start);
88
+ for (let i = 0; i < 8; i++) {
89
+ if (
90
+ existsSync(join(dir, "api", "package.json")) &&
91
+ existsSync(join(dir, "app", "package.json"))
92
+ ) {
93
+ return dir;
94
+ }
95
+ const parent = dirname(dir);
96
+ if (parent === dir) break;
97
+ dir = parent;
98
+ }
99
+ return resolve(start);
100
+ }
101
+
102
+ function mergeKomplianPreset(existing, preset, force) {
103
+ const base =
104
+ existing && typeof existing === "object" ? structuredClone(existing) : {};
105
+ if (!base.mcpServers || typeof base.mcpServers !== "object") {
106
+ base.mcpServers = {};
107
+ }
108
+ for (const [name, cfg] of Object.entries(preset.mcpServers)) {
109
+ if (!name.startsWith("KOMPLIAN-")) continue;
110
+ if (!force && base.mcpServers[name]) continue;
111
+ base.mcpServers[name] = structuredClone(cfg);
112
+ }
113
+ return base;
114
+ }
115
+
116
+ function parseMcpArgs(argv) {
117
+ const opts = {
118
+ yes: false,
119
+ force: false,
120
+ dryRun: false,
121
+ global: false,
122
+ help: false,
123
+ workspace: "",
124
+ };
125
+ const rest = [];
126
+ for (let i = 0; i < argv.length; i++) {
127
+ const a = argv[i];
128
+ if (a === "--yes" || a === "-y") opts.yes = true;
129
+ else if (a === "--force") opts.force = true;
130
+ else if (a === "--dry-run") opts.dryRun = true;
131
+ else if (a === "--global") opts.global = true;
132
+ else if (a === "-h" || a === "--help") opts.help = true;
133
+ else if (a === "--workspace" || a === "-w") opts.workspace = argv[++i] || "";
134
+ else if (a.startsWith("-")) {
135
+ log(`${c.red}✗${c.reset} Opción desconocida: ${a}`);
136
+ process.exit(1);
137
+ } else rest.push(a);
138
+ }
139
+ if (rest[0]) opts.workspace = rest[0];
140
+ return opts;
141
+ }
142
+
143
+ function usageMcpTools() {
144
+ log(`Uso: npx komplian mcp-tools [opciones] [carpeta-monorepo]`);
145
+ log(``);
146
+ log(` Añade servidores MCP KOMPLIAN-* en .cursor/mcp.json (GitHub, Sentry, Stripe)`);
147
+ log(` y genera .cursor/KOMPLIAN_MCP_SETUP.md (Atlassian + Chrome DevTools = solo Cursor).`);
148
+ log(``);
149
+ log(` ${c.dim}Secretos: vacíos en JSON; no se publican en el paquete npm komplian.${c.reset}`);
150
+ log(``);
151
+ log(` -y, --yes Sin confirmación`);
152
+ log(` --force Sobreescribe entradas KOMPLIAN-* existentes`);
153
+ log(` --dry-run Imprime JSON sin escribir`);
154
+ log(` --global Fusiona en ~/.cursor/mcp.json (toda la máquina)`);
155
+ log(` -w, --workspace Raíz del monorepo (por defecto: cwd)`);
156
+ log(` -h, --help`);
157
+ }
158
+
159
+ async function confirmForceOverwrite(yes) {
160
+ if (yes) return true;
161
+ const rl = createInterface({ input, output });
162
+ const ans = await rl.question(
163
+ `\n${c.bold}¿Sobrescribir entradas KOMPLIAN-* existentes? [y/N]${c.reset} `
164
+ );
165
+ rl.close();
166
+ return /^y(es)?$/i.test((ans || "").trim());
167
+ }
168
+
169
+ export async function runMcpTools(argv) {
170
+ const opts = parseMcpArgs(argv);
171
+ if (opts.help) {
172
+ usageMcpTools();
173
+ return;
174
+ }
175
+
176
+ const workspaceRoot = opts.workspace.trim()
177
+ ? resolve(opts.workspace.replace(/^~(?=$|[/\\])/, homedir()))
178
+ : findWorkspaceRoot(process.cwd());
179
+
180
+ const cursorDir = opts.global
181
+ ? join(homedir(), ".cursor")
182
+ : join(workspaceRoot, ".cursor");
183
+ const mcpPath = join(cursorDir, "mcp.json");
184
+ const setupPath = join(cursorDir, "KOMPLIAN_MCP_SETUP.md");
185
+
186
+ let existing = {};
187
+ if (existsSync(mcpPath)) {
188
+ try {
189
+ existing = JSON.parse(readFileSync(mcpPath, "utf8"));
190
+ } catch {
191
+ log(`${c.red}✗${c.reset} JSON inválido: ${mcpPath}`);
192
+ process.exit(1);
193
+ }
194
+ }
195
+
196
+ const hadKomplian = Object.keys(existing.mcpServers || {}).some((k) =>
197
+ k.startsWith("KOMPLIAN-")
198
+ );
199
+ if (opts.force && hadKomplian && !opts.yes) {
200
+ const ok = await confirmForceOverwrite(opts.yes);
201
+ if (!ok) {
202
+ log(`${c.yellow}○${c.reset} Cancelado.`);
203
+ return;
204
+ }
205
+ }
206
+
207
+ const merged = mergeKomplianPreset(existing, KOMPLIAN_MCP_PRESET, opts.force);
208
+ const json = JSON.stringify(merged, null, 2);
209
+
210
+ if (opts.dryRun) {
211
+ log(`${c.cyan}━━ dry-run ━━${c.reset} ${mcpPath}`);
212
+ log(json);
213
+ log("");
214
+ log(SETUP_MD);
215
+ return;
216
+ }
217
+
218
+ mkdirSync(cursorDir, { recursive: true });
219
+ const prevStr = existsSync(mcpPath) ? readFileSync(mcpPath, "utf8") : "";
220
+ const changed = prevStr.trim() !== json.trim();
221
+ if (changed) {
222
+ writeFileSync(mcpPath, json + "\n", "utf8");
223
+ }
224
+ writeFileSync(setupPath, SETUP_MD, "utf8");
225
+
226
+ log("");
227
+ const mcpMark = changed ? `${c.green}✓${c.reset}` : `${c.dim}○${c.reset}`;
228
+ const mcpNote = changed ? "" : ` ${c.dim}(sin cambios)${c.reset}`;
229
+ log(`${mcpMark} ${c.bold}mcp.json${c.reset} ${c.dim}${mcpPath}${c.reset}${mcpNote}`);
230
+ log(`${c.green}✓${c.reset} ${c.bold}KOMPLIAN_MCP_SETUP.md${c.reset} ${c.dim}${setupPath}${c.reset}`);
231
+ log("");
232
+ log(`${c.bold}Siguiente${c.reset}`);
233
+ log(` 1. Rellena tokens vacíos en Cursor (Settings → MCP) o edita ${c.bold}mcp.json${c.reset} en local.`);
234
+ log(` 2. Activa en Cursor: ${c.dim}Atlassian + Chrome DevTools${c.reset} (ver guía arriba).`);
235
+ log(` 3. Reinicia Cursor.`);
236
+ log("");
237
+ log(`${c.dim}Paquete npm komplian solo incluye *.mjs — nunca sube tu mcp.json.${c.reset}`);
238
+ }
@@ -394,11 +394,12 @@ function npmInstallEach(workspace) {
394
394
  }
395
395
 
396
396
  function usage() {
397
- log(`Uso: komplian onboard [opciones] [carpeta] | komplian postman [opciones] | komplian localhost [opciones]`);
397
+ log(`Uso: komplian onboard | postman | localhost | mcp-tools [opciones]`);
398
398
  log(` npx komplian onboard --yes`);
399
399
  log(` npx komplian postman login ${c.dim}(una vez · guarda API key)${c.reset}`);
400
400
  log(` npx komplian postman --yes ${c.dim}(email @komplian.com)${c.reset}`);
401
401
  log(` npx komplian localhost --yes ${c.dim}(env local + api app web admin docs)${c.reset}`);
402
+ log(` npx komplian mcp-tools --yes ${c.dim}(.cursor/mcp.json + guía MCP Komplian)${c.reset}`);
402
403
  log(``);
403
404
  log(` Antes (una vez): gh auth login -h github.com -s repo -s read:org -w`);
404
405
  log(` Requisitos: Node 18+, git, GitHub CLI (gh)`);
@@ -492,6 +493,11 @@ async function main() {
492
493
  await runLocalhost(rawArgv.slice(1));
493
494
  return;
494
495
  }
496
+ if (rawArgv[0] === "mcp-tools") {
497
+ const { runMcpTools } = await import("./komplian-mcp-tools.mjs");
498
+ await runMcpTools(rawArgv.slice(1));
499
+ return;
500
+ }
495
501
 
496
502
  const configPath = join(__dirname, "komplian-team-repos.json");
497
503
  const { argv, fromOnboardSubcommand } = normalizeArgv(rawArgv);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "komplian",
3
- "version": "0.4.5",
4
- "description": "Komplian developer workspace: GitHub clone (onboard) + Postman collection/environments (postman). Node 18+.",
3
+ "version": "0.4.7",
4
+ "description": "Komplian CLI: onboard, Postman, localhost, mcp-tools (Cursor MCP). Node 18+. Published tarball has no .env / secrets.",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=18"
@@ -13,6 +13,7 @@
13
13
  "komplian-onboard.mjs",
14
14
  "komplian-postman.mjs",
15
15
  "komplian-localhost.mjs",
16
+ "komplian-mcp-tools.mjs",
16
17
  "komplian-team-repos.json",
17
18
  "README.md"
18
19
  ],
@@ -20,7 +21,7 @@
20
21
  "access": "public"
21
22
  },
22
23
  "scripts": {
23
- "prepublishOnly": "node --check komplian-onboard.mjs && node --check komplian-postman.mjs && node --check komplian-localhost.mjs"
24
+ "prepublishOnly": "node --check komplian-onboard.mjs && node --check komplian-postman.mjs && node --check komplian-localhost.mjs && node --check komplian-mcp-tools.mjs"
24
25
  },
25
26
  "keywords": [
26
27
  "komplian",