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.
- package/komplian-localhost.mjs +224 -111
- package/komplian-mcp-tools.mjs +238 -0
- package/komplian-onboard.mjs +7 -1
- package/package.json +4 -3
package/komplian-localhost.mjs
CHANGED
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Komplian localhost — genera `.env.local`
|
|
4
|
-
* api, app, web, admin, docs
|
|
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:
|
|
7
|
-
* No imprime
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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 `#
|
|
157
|
-
#
|
|
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
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
"",
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
writeAtomic(p,
|
|
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
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
|
311
|
-
log(` ${c.dim}Neon:
|
|
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
|
|
314
|
-
log(` --force
|
|
315
|
-
log(` --minimal Solo app + web + docs
|
|
316
|
-
log(` --env-only Solo escribir env
|
|
317
|
-
log(` -w, --workspace Ruta al monorepo
|
|
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}(
|
|
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
|
+
}
|
package/komplian-onboard.mjs
CHANGED
|
@@ -394,11 +394,12 @@ function npmInstallEach(workspace) {
|
|
|
394
394
|
}
|
|
395
395
|
|
|
396
396
|
function usage() {
|
|
397
|
-
log(`Uso: komplian onboard
|
|
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.
|
|
4
|
-
"description": "Komplian
|
|
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",
|