komplian 0.4.1 → 0.4.4

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
@@ -4,7 +4,9 @@
4
4
 
5
5
  1. Install [GitHub CLI](https://cli.github.com) and **git** (one-time per machine).
6
6
  2. Browser login: `gh auth login -h github.com -s repo -s read:org -w`
7
- 3. `npx komplian onboard --yes`
7
+ 3. `npx komplian onboard --yes` (sin `@versión`: usa `latest` del registry público).
8
+
9
+ **`npm ERR! ETARGET` / “No matching version”** (también si falló **`postman`**, no solo `onboard`): es el mismo paquete npm. Usa **`npx komplian postman login`** / **`npx komplian postman --yes`** **sin** `@0.4.x`; comprueba `npm config get registry` → `https://registry.npmjs.org/`; `npm cache clean --force`. Detalle: `ONBOARDING.md` en el monorepo.
8
10
 
9
11
  ### Postman (colección + entornos)
10
12
 
@@ -24,11 +26,15 @@ npx komplian postman --yes
24
26
 
25
27
  Opcional: `export POSTMAN_API_KEY=…` solo para la sesión actual (tiene prioridad sobre el archivo).
26
28
 
27
- El comando llama a `GET https://api.getpostman.com/me` y **solo continúa** si el email de la cuenta es `@komplian.com`. Crea la colección **Komplian API**, los entornos **Komplian Local Dev** y **Komplian Production**, y deja copia en `./komplian-postman/*.json`.
29
+ 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.
30
+
31
+ **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).
28
32
 
29
33
  - Solo exportar archivos (sin subir por API): `npx komplian postman --yes --export-only`
30
34
  - Otro workspace: `POSTMAN_WORKSPACE_ID=<id>`
31
- - Ruta alternativa del archivo de clave: `KOMPLIAN_POSTMAN_KEY_FILE`
35
+ - Ruta alternativa del archivo de clave Postman: `KOMPLIAN_POSTMAN_KEY_FILE`
36
+
37
+ **Seguridad (CLI Postman):** clave en `~/.komplian/` (permisos reforzados por el CLI), prompt sin eco, errores redactados; detalle técnico arriba. **Normativa del equipo:** archivo **`SECURITY.md`** en la raíz del monorepo Komplian (no va en el paquete npm).
32
38
 
33
39
  No OAuth App registration — `gh` uses GitHub’s built-in flow. **Default workspace:** current working directory (`process.cwd()`), not `~/komplian`. Pass a path as last argument to clone elsewhere.
34
40
  **Dependencies:** repos with `package-lock.json` use **`npm ci`** (does not modify the lockfile, so no spurious git changes). Repos without a lockfile use **`npm install --no-package-lock`** so onboarding does not create a new `package-lock.json`. Yarn / pnpm repos use frozen lock installs when `yarn` / `pnpm` is on PATH. Unless `KOMPLIAN_NPM_AUDIT=1`, npm runs with `--no-audit --no-fund`.
@@ -0,0 +1,458 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Komplian localhost — genera `.env.local` con secretos aleatorios (crypto) y arranca
4
+ * api, app, web, admin, docs en paralelo (concurrently).
5
+ *
6
+ * Seguridad: nada se commitea; secretos solo en disco local (.env*.local / .komplian/).
7
+ * No imprime valores de claves.
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
20
+ */
21
+
22
+ import { randomBytes } from "node:crypto";
23
+ import { spawn } from "node:child_process";
24
+ import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
25
+ import { dirname, join, resolve } from "node:path";
26
+ import { fileURLToPath } from "node:url";
27
+ import { createInterface } from "node:readline/promises";
28
+ import { stdin as input, stdout as output } from "node:process";
29
+
30
+ const __dirname = dirname(fileURLToPath(import.meta.url));
31
+
32
+ const c = {
33
+ reset: "\x1b[0m",
34
+ dim: "\x1b[2m",
35
+ bold: "\x1b[1m",
36
+ cyan: "\x1b[36m",
37
+ green: "\x1b[32m",
38
+ red: "\x1b[31m",
39
+ yellow: "\x1b[33m",
40
+ };
41
+
42
+ function log(s = "") {
43
+ console.log(s);
44
+ }
45
+
46
+ const PLACEHOLDER_DB =
47
+ "postgresql://127.0.0.1:5432/komplian_localhost_placeholder?sslmode=disable";
48
+
49
+ function rndHex(n = 32) {
50
+ return randomBytes(n).toString("hex");
51
+ }
52
+
53
+ function parseEnvFile(content) {
54
+ const out = {};
55
+ for (const line of content.split(/\r?\n/)) {
56
+ const t = line.trim();
57
+ if (!t || t.startsWith("#")) continue;
58
+ const eq = t.indexOf("=");
59
+ if (eq === -1) continue;
60
+ const key = t.slice(0, eq).trim();
61
+ let val = t.slice(eq + 1).trim();
62
+ if (
63
+ (val.startsWith('"') && val.endsWith('"')) ||
64
+ (val.startsWith("'") && val.endsWith("'"))
65
+ ) {
66
+ val = val.slice(1, -1);
67
+ }
68
+ out[key] = val;
69
+ }
70
+ return out;
71
+ }
72
+
73
+ function loadSecretsFromDisk(workspaceRoot) {
74
+ const paths = [
75
+ join(workspaceRoot, "komplian-localhost.secrets.env"),
76
+ join(workspaceRoot, ".komplian", "localhost-secrets.env"),
77
+ ];
78
+ for (const p of paths) {
79
+ if (!existsSync(p)) continue;
80
+ try {
81
+ return parseEnvFile(readFileSync(p, "utf8"));
82
+ } catch {
83
+ /* ignore */
84
+ }
85
+ }
86
+ return {};
87
+ }
88
+
89
+ function resolveDatabaseUrls(workspaceRoot) {
90
+ const file = loadSecretsFromDisk(workspaceRoot);
91
+ const single =
92
+ process.env.KOMPLIAN_LOCALHOST_DATABASE_URL?.trim() ||
93
+ file.KOMPLIAN_LOCALHOST_DATABASE_URL?.trim();
94
+ const app =
95
+ process.env.KOMPLIAN_LOCALHOST_APP_DATABASE_URL?.trim() ||
96
+ file.KOMPLIAN_LOCALHOST_APP_DATABASE_URL?.trim() ||
97
+ single;
98
+ const admin =
99
+ process.env.KOMPLIAN_LOCALHOST_ADMIN_DATABASE_URL?.trim() ||
100
+ file.KOMPLIAN_LOCALHOST_ADMIN_DATABASE_URL?.trim() ||
101
+ single;
102
+ const web =
103
+ process.env.KOMPLIAN_LOCALHOST_WEB_DATABASE_URL?.trim() ||
104
+ file.KOMPLIAN_LOCALHOST_WEB_DATABASE_URL?.trim() ||
105
+ single;
106
+ const hasReal =
107
+ !!(app && admin && web) &&
108
+ !String(app).includes("komplian_localhost_placeholder");
109
+ return { app, admin, web, apiApp: app, apiAdmin: admin, apiWeb: web, hasReal };
110
+ }
111
+
112
+ function writeAtomic(path, content) {
113
+ writeFileSync(path, content, { encoding: "utf8", mode: 0o600 });
114
+ try {
115
+ if (process.platform !== "win32") chmodSync(path, 0o600);
116
+ } catch {
117
+ /* ignore */
118
+ }
119
+ }
120
+
121
+ function findWorkspaceRoot(start) {
122
+ let dir = resolve(start);
123
+ for (let i = 0; i < 8; i++) {
124
+ if (
125
+ existsSync(join(dir, "api", "package.json")) &&
126
+ existsSync(join(dir, "app", "package.json"))
127
+ ) {
128
+ return dir;
129
+ }
130
+ const parent = dirname(dir);
131
+ if (parent === dir) break;
132
+ dir = parent;
133
+ }
134
+ return resolve(start);
135
+ }
136
+
137
+ function buildShared() {
138
+ const apiKey = rndHex(32);
139
+ const adminApiKey = rndHex(32);
140
+ return {
141
+ apiKey,
142
+ adminApiKey,
143
+ jwtSecret: rndHex(32),
144
+ encryptionKey: rndHex(32),
145
+ nextAuthSecret: rndHex(32),
146
+ csrfSecret: rndHex(32),
147
+ cronSecret: rndHex(32),
148
+ internalWebhook: rndHex(32),
149
+ twoFactorEnc: rndHex(32),
150
+ hashSecret: rndHex(32),
151
+ nextAuthAdmin: rndHex(32),
152
+ };
153
+ }
154
+
155
+ function header(project) {
156
+ return `# Generated by komplian localhost — do not commit (${project})
157
+ # Regenerate: npx komplian localhost --yes --force
158
+
159
+ `;
160
+ }
161
+
162
+ function writeAppEnv(root, s, db, opts) {
163
+ const p = join(root, "app", ".env.local");
164
+ 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"));
190
+ return { path: p, skipped: false };
191
+ }
192
+
193
+ function writeApiEnv(root, s, db, opts) {
194
+ const p = join(root, "api", ".env.local");
195
+ 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"));
225
+ return { path: p, skipped: false };
226
+ }
227
+
228
+ function writeWebEnv(root, s, db, opts) {
229
+ const p = join(root, "web", ".env.local");
230
+ 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"));
240
+ return { path: p, skipped: false };
241
+ }
242
+
243
+ function writeAdminEnv(root, s, db, opts) {
244
+ const p = join(root, "admin", ".env.local");
245
+ 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"));
256
+ return { path: p, skipped: false };
257
+ }
258
+
259
+ function writeDocsEnv(root, opts) {
260
+ const p = join(root, "docs", ".env.local");
261
+ 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"));
267
+ return { path: p, skipped: false };
268
+ }
269
+
270
+ async function confirmOverwrite(yes) {
271
+ if (yes) return true;
272
+ const rl = createInterface({ input, output });
273
+ const ans = await rl.question(
274
+ `\n${c.bold}Sobrescribir .env.local existentes? [y/N]${c.reset} `
275
+ );
276
+ rl.close();
277
+ return /^y(es)?$/i.test((ans || "").trim());
278
+ }
279
+
280
+ function parseLocalhostArgs(argv) {
281
+ const opts = {
282
+ yes: false,
283
+ force: false,
284
+ envOnly: false,
285
+ workspace: "",
286
+ minimal: false,
287
+ help: false,
288
+ };
289
+ const rest = [];
290
+ for (let i = 0; i < argv.length; i++) {
291
+ const a = argv[i];
292
+ if (a === "--yes" || a === "-y") opts.yes = true;
293
+ else if (a === "--force") opts.force = true;
294
+ else if (a === "--env-only") opts.envOnly = true;
295
+ else if (a === "--minimal") opts.minimal = true;
296
+ else if (a === "-h" || a === "--help") opts.help = true;
297
+ else if (a === "--workspace" || a === "-w") opts.workspace = argv[++i] || "";
298
+ else if (a.startsWith("-")) {
299
+ log(`${c.red}✗${c.reset} Opción desconocida: ${a}`);
300
+ process.exit(1);
301
+ } else rest.push(a);
302
+ }
303
+ if (rest[0]) opts.workspace = rest[0];
304
+ return opts;
305
+ }
306
+
307
+ function usageLocalhost() {
308
+ log(`Uso: npx komplian localhost [opciones] [carpeta-monorepo]`);
309
+ 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}`);
312
+ 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)`);
318
+ log(` -h, --help`);
319
+ }
320
+
321
+ function checkNodeModules(root, names) {
322
+ const missing = [];
323
+ for (const n of names) {
324
+ if (!existsSync(join(root, n, "node_modules"))) missing.push(n);
325
+ }
326
+ return missing;
327
+ }
328
+
329
+ export async function runLocalhost(argv) {
330
+ const opts = parseLocalhostArgs(argv);
331
+ if (opts.help) {
332
+ usageLocalhost();
333
+ return;
334
+ }
335
+
336
+ let workspaceRoot = opts.workspace.trim()
337
+ ? resolve(opts.workspace.replace(/^~(?=$|[/\\])/, process.env.HOME || ""))
338
+ : findWorkspaceRoot(process.cwd());
339
+
340
+ if (!existsSync(join(workspaceRoot, "api", "package.json"))) {
341
+ log(`${c.red}✗${c.reset} No parece un monorepo Komplian (falta api/package.json): ${workspaceRoot}`);
342
+ process.exit(1);
343
+ }
344
+
345
+ const db = resolveDatabaseUrls(workspaceRoot);
346
+ const dbForWrites = {
347
+ app: db.app || PLACEHOLDER_DB,
348
+ admin: db.admin || PLACEHOLDER_DB,
349
+ web: db.web || PLACEHOLDER_DB,
350
+ apiApp: db.apiApp || PLACEHOLDER_DB,
351
+ apiAdmin: db.apiAdmin || PLACEHOLDER_DB,
352
+ apiWeb: db.apiWeb || PLACEHOLDER_DB,
353
+ };
354
+
355
+ const hasAnyEnv = [
356
+ "app",
357
+ "api",
358
+ "web",
359
+ "admin",
360
+ "docs",
361
+ ].some((name) => existsSync(join(workspaceRoot, name, ".env.local")));
362
+
363
+ if (hasAnyEnv && !opts.force) {
364
+ const ok = await confirmOverwrite(opts.yes);
365
+ if (!ok) {
366
+ log(`${c.yellow}○${c.reset} Cancelado. Usa ${c.bold}--force${c.reset} para regenerar.`);
367
+ return;
368
+ }
369
+ opts.force = true;
370
+ }
371
+
372
+ const s = buildShared();
373
+
374
+ const written = [];
375
+ written.push(writeAppEnv(workspaceRoot, s, dbForWrites, opts));
376
+ if (!opts.minimal) {
377
+ written.push(writeApiEnv(workspaceRoot, s, dbForWrites, opts));
378
+ written.push(writeWebEnv(workspaceRoot, s, dbForWrites, opts));
379
+ written.push(writeAdminEnv(workspaceRoot, s, dbForWrites, opts));
380
+ } else {
381
+ written.push(writeWebEnv(workspaceRoot, s, dbForWrites, opts));
382
+ }
383
+ written.push(writeDocsEnv(workspaceRoot, opts));
384
+
385
+ log("");
386
+ log(`${c.cyan}━━ .env.local ━━${c.reset} ${c.dim}(permisos 600)${c.reset}`);
387
+ for (const w of written) {
388
+ const st = w.skipped ? `${c.dim}sin cambios${c.reset}` : `${c.green}ok${c.reset}`;
389
+ log(` ${st} ${w.path}`);
390
+ }
391
+
392
+ if (!db.hasReal && !opts.minimal) {
393
+ 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
+ }
398
+
399
+ if (opts.envOnly) {
400
+ log("");
401
+ log(`${c.green}✓${c.reset} Solo entorno. Arranca con: ${c.bold}npx komplian localhost --yes${c.reset}`);
402
+ return;
403
+ }
404
+
405
+ const services = opts.minimal
406
+ ? ["app", "web", "docs"]
407
+ : ["api", "app", "web", "admin", "docs"];
408
+ const missing = checkNodeModules(workspaceRoot, services);
409
+ if (missing.length) {
410
+ log("");
411
+ log(`${c.red}✗${c.reset} Falta node_modules en: ${missing.join(", ")}`);
412
+ log(` ${c.dim}Ejecuta npm install en cada carpeta (o npx komplian onboard --yes --install).${c.reset}`);
413
+ process.exit(1);
414
+ }
415
+
416
+ const colors = "blue,green,cyan,magenta,yellow";
417
+ const names = services.join(",");
418
+ const scripts = services.map((name) => {
419
+ const dir = join(workspaceRoot, name);
420
+ if (/[\s"]/.test(dir)) {
421
+ return `npm run dev --prefix "${dir.replace(/"/g, '\\"')}"`;
422
+ }
423
+ return `npm run dev --prefix ${dir}`;
424
+ });
425
+
426
+ log("");
427
+ log(`${c.cyan}━━ Servicios (${services.length}) ━━${c.reset} ${workspaceRoot}`);
428
+ log(`${c.dim}Ctrl+C detiene todos.${c.reset}`);
429
+ log("");
430
+
431
+ const child = spawn(
432
+ process.platform === "win32" ? "npx.cmd" : "npx",
433
+ ["--yes", "concurrently@9", "-c", colors, "-n", names, ...scripts],
434
+ {
435
+ cwd: workspaceRoot,
436
+ stdio: "inherit",
437
+ shell: true,
438
+ env: { ...process.env },
439
+ }
440
+ );
441
+
442
+ const killAll = () => {
443
+ try {
444
+ child.kill("SIGINT");
445
+ } catch {
446
+ /* ignore */
447
+ }
448
+ };
449
+ process.on("SIGINT", () => {
450
+ killAll();
451
+ process.exit(0);
452
+ });
453
+ process.on("SIGTERM", killAll);
454
+
455
+ child.on("exit", (code) => {
456
+ process.exit(code ?? 0);
457
+ });
458
+ }
@@ -394,10 +394,11 @@ function npmInstallEach(workspace) {
394
394
  }
395
395
 
396
396
  function usage() {
397
- log(`Uso: komplian onboard [opciones] [carpeta] | komplian postman [opciones]`);
397
+ log(`Uso: komplian onboard [opciones] [carpeta] | komplian postman [opciones] | komplian localhost [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
+ log(` npx komplian localhost --yes ${c.dim}(env local + api app web admin docs)${c.reset}`);
401
402
  log(``);
402
403
  log(` Antes (una vez): gh auth login -h github.com -s repo -s read:org -w`);
403
404
  log(` Requisitos: Node 18+, git, GitHub CLI (gh)`);
@@ -486,6 +487,11 @@ async function main() {
486
487
  await runPostman(rawArgv.slice(1));
487
488
  return;
488
489
  }
490
+ if (rawArgv[0] === "localhost") {
491
+ const { runLocalhost } = await import("./komplian-localhost.mjs");
492
+ await runLocalhost(rawArgv.slice(1));
493
+ return;
494
+ }
489
495
 
490
496
  const configPath = join(__dirname, "komplian-team-repos.json");
491
497
  const { argv, fromOnboardSubcommand } = normalizeArgv(rawArgv);
@@ -5,7 +5,8 @@
5
5
  * Requisitos: Node 18+. Primera vez: npx komplian postman login
6
6
  *
7
7
  * Clave: POSTMAN_API_KEY o ~/.komplian/postman-api-key (npx komplian postman login)
8
- * Opcional: POSTMAN_WORKSPACE_ID, KOMPLIAN_EMAIL_DOMAIN, KOMPLIAN_POSTMAN_KEY_FILE
8
+ * Seguridad: no registrar secretos; errores API redactados; login sin eco (TTY); ~/.komplian 700 + key 600.
9
+ * Opcional: POSTMAN_WORKSPACE_ID, KOMPLIAN_EMAIL_DOMAIN, KOMPLIAN_POSTMAN_KEY_FILE, KOMPLIAN_DEBUG
9
10
  */
10
11
 
11
12
  import {
@@ -36,6 +37,175 @@ function log(s = "") {
36
37
  console.log(s);
37
38
  }
38
39
 
40
+ /** Ruta para logs sin exponer home completo (solo prefijo ~). */
41
+ function formatHomePath(absPath) {
42
+ const h = homedir();
43
+ if (absPath.startsWith(h)) return `~${absPath.slice(h.length)}`;
44
+ return absPath;
45
+ }
46
+
47
+ function maskEmail(email) {
48
+ if (!email || typeof email !== "string" || !email.includes("@")) {
49
+ return "[sin email]";
50
+ }
51
+ const [user, domain] = email.split("@");
52
+ if (!domain) return "[sin email]";
53
+ const u =
54
+ user.length <= 1 ? "*" : `${user[0]}${"*".repeat(Math.min(3, user.length - 1))}`;
55
+ return `${u}@${domain}`;
56
+ }
57
+
58
+ function looksLikeSecretString(s) {
59
+ return (
60
+ typeof s === "string" &&
61
+ s.length >= 24 &&
62
+ /^[A-Za-z0-9_\-.+/=]+$/.test(s)
63
+ );
64
+ }
65
+
66
+ /** Solo mensajes genéricos; nunca volcar cuerpos crudos de la API (pueden incluir tokens). */
67
+ function safeApiErrorHint(body) {
68
+ if (body == null || typeof body !== "object") return "";
69
+ const parts = [];
70
+ const code = body.error?.name || body.error?.code || body.error?.type;
71
+ if (typeof code === "string" && code.length < 64 && !looksLikeSecretString(code)) {
72
+ parts.push(code);
73
+ }
74
+ const msg = body.error?.message || body.message;
75
+ if (
76
+ typeof msg === "string" &&
77
+ msg.length > 0 &&
78
+ msg.length < 160 &&
79
+ !looksLikeSecretString(msg)
80
+ ) {
81
+ parts.push(msg);
82
+ }
83
+ return parts.join(" · ").slice(0, 200);
84
+ }
85
+
86
+ function sanitizeForLog(value, depth = 0) {
87
+ if (depth > 8) return "[…]";
88
+ if (value == null) return value;
89
+ if (typeof value === "string") {
90
+ if (looksLikeSecretString(value)) return "[redacted]";
91
+ return value.length > 80 ? `${value.slice(0, 40)}…` : value;
92
+ }
93
+ if (typeof value !== "object") return value;
94
+ if (Array.isArray(value)) {
95
+ return value.slice(0, 15).map((v) => sanitizeForLog(v, depth + 1));
96
+ }
97
+ const out = {};
98
+ for (const [k, v] of Object.entries(value)) {
99
+ const lower = k.toLowerCase();
100
+ if (
101
+ /(password|secret|token|apikey|api_key|authorization|credential|bearer|pmak|pm_)/i.test(
102
+ k
103
+ ) ||
104
+ lower === "value" ||
105
+ (lower === "key" && depth > 0)
106
+ ) {
107
+ out[k] = "[redacted]";
108
+ } else {
109
+ out[k] = sanitizeForLog(v, depth + 1);
110
+ }
111
+ }
112
+ return out;
113
+ }
114
+
115
+ function logApiFailure(context, status, body) {
116
+ const hint = safeApiErrorHint(body);
117
+ let extra = "";
118
+ if (
119
+ process.env.KOMPLIAN_DEBUG === "1" ||
120
+ process.env.KOMPLIAN_DEBUG === "true"
121
+ ) {
122
+ try {
123
+ extra = ` ${JSON.stringify(sanitizeForLog(body)).slice(0, 480)}`;
124
+ } catch {
125
+ extra = " [debug: no serializable]";
126
+ }
127
+ }
128
+ log(
129
+ `${c.yellow}○${c.reset} ${context}: HTTP ${status}${hint ? ` — ${hint}` : ""}${extra}`
130
+ );
131
+ }
132
+
133
+ function ensureSecureKomplianDir() {
134
+ const dir = join(homedir(), ".komplian");
135
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
136
+ try {
137
+ chmodSync(dir, 0o700);
138
+ } catch {
139
+ /* Windows u otros */
140
+ }
141
+ return dir;
142
+ }
143
+
144
+ /**
145
+ * Lectura de API key sin eco (TTY). Fallback con aviso si no hay raw mode (p. ej. algunos Windows).
146
+ */
147
+ async function readPasswordLine(promptText) {
148
+ const prompt = `${c.cyan}${promptText}${c.reset}`;
149
+ if (!input.isTTY) {
150
+ return new Promise((resolve, reject) => {
151
+ const chunks = [];
152
+ input.on("data", (d) => chunks.push(d));
153
+ input.on("end", () => {
154
+ resolve(Buffer.concat(chunks).toString("utf8").trim());
155
+ });
156
+ input.on("error", reject);
157
+ });
158
+ }
159
+ if (typeof input.setRawMode !== "function") {
160
+ log(
161
+ `${c.dim}○ Entrada visible (terminal sin modo oculto). No compartas pantalla con la clave.${c.reset}`
162
+ );
163
+ const rl = createInterface({ input, output });
164
+ const line = await rl.question(
165
+ `${c.cyan}Postman API key${c.reset} (Settings → API keys): `
166
+ );
167
+ rl.close();
168
+ return line.trim();
169
+ }
170
+ return new Promise((resolve) => {
171
+ process.stderr.write(prompt);
172
+ let buf = "";
173
+ const cleanup = () => {
174
+ try {
175
+ input.setRawMode(false);
176
+ } catch {
177
+ /* ignore */
178
+ }
179
+ input.removeListener("data", onData);
180
+ };
181
+ const onData = (chunk) => {
182
+ const s = chunk.toString("utf8");
183
+ for (let i = 0; i < s.length; i++) {
184
+ const code = s.charCodeAt(i);
185
+ if (code === 13 || code === 10) {
186
+ cleanup();
187
+ process.stderr.write("\n");
188
+ resolve(buf.trim());
189
+ return;
190
+ }
191
+ if (code === 3) {
192
+ cleanup();
193
+ process.exit(130);
194
+ }
195
+ if (code === 127 || code === 8) {
196
+ buf = buf.slice(0, -1);
197
+ continue;
198
+ }
199
+ buf += s[i];
200
+ }
201
+ };
202
+ input.setRawMode(true);
203
+ input.resume();
204
+ input.setEncoding("utf8");
205
+ input.on("data", onData);
206
+ });
207
+ }
208
+
39
209
  function defaultKeyPath() {
40
210
  const override = process.env.KOMPLIAN_POSTMAN_KEY_FILE?.trim();
41
211
  if (override) return resolve(override);
@@ -65,11 +235,102 @@ function printMissingKeyHelp() {
65
235
  log(``);
66
236
  log(` ${c.dim}O en la sesión actual: export POSTMAN_API_KEY=…${c.reset}`);
67
237
  log(
68
- `${c.dim} Archivo manual: ${defaultKeyPath()}${c.reset}`
238
+ `${c.dim} Archivo manual: ${formatHomePath(defaultKeyPath())}${c.reset}`
69
239
  );
70
240
  process.exit(1);
71
241
  }
72
242
 
243
+ /** Parseo mínimo .env (sin dependencia dotenv). */
244
+ function parseEnvFile(text) {
245
+ const out = {};
246
+ for (const line of text.split("\n")) {
247
+ const t = line.trim();
248
+ if (!t || t.startsWith("#")) continue;
249
+ const eq = t.indexOf("=");
250
+ if (eq < 1) continue;
251
+ const key = t.slice(0, eq).trim();
252
+ let val = t.slice(eq + 1).trim();
253
+ if (
254
+ (val.startsWith('"') && val.endsWith('"')) ||
255
+ (val.startsWith("'") && val.endsWith("'"))
256
+ ) {
257
+ val = val.slice(1, -1);
258
+ }
259
+ out[key] = val;
260
+ }
261
+ return out;
262
+ }
263
+
264
+ function discoverDotenvPaths(cwd, extraPath) {
265
+ const paths = [];
266
+ if (extraPath?.trim()) paths.push(resolve(extraPath.trim()));
267
+ if (process.env.KOMPLIAN_DOTENV?.trim()) {
268
+ paths.push(resolve(process.env.KOMPLIAN_DOTENV.trim()));
269
+ }
270
+ for (const rel of [
271
+ "api/.env",
272
+ "../api/.env",
273
+ ".env",
274
+ "app/.env",
275
+ "../app/.env",
276
+ ]) {
277
+ paths.push(resolve(cwd, rel));
278
+ }
279
+ return [...new Set(paths)];
280
+ }
281
+
282
+ function loadMergedEnvFiles(cwd, extraDotenv) {
283
+ const merged = {};
284
+ for (const p of discoverDotenvPaths(cwd, extraDotenv)) {
285
+ if (!existsSync(p)) continue;
286
+ try {
287
+ Object.assign(merged, parseEnvFile(readFileSync(p, "utf8")));
288
+ } catch {
289
+ /* ignore */
290
+ }
291
+ }
292
+ return merged;
293
+ }
294
+
295
+ /**
296
+ * Valores para variables de Postman (API Komplian): API_KEY, ADMIN_API_KEY, IDs, etc.
297
+ * Orden: process.env por cada alias, luego claves en .env fusionados.
298
+ */
299
+ function loadKomplianSecrets(cwd, extraDotenv) {
300
+ const file = loadMergedEnvFiles(cwd, extraDotenv);
301
+ const get = (...keys) => {
302
+ for (const k of keys) {
303
+ const e = process.env[k];
304
+ if (e != null && String(e).trim()) return String(e).trim();
305
+ }
306
+ for (const k of keys) {
307
+ const f = file[k];
308
+ if (f != null && String(f).trim()) return String(f).trim();
309
+ }
310
+ return "";
311
+ };
312
+ return {
313
+ apiKey: get("KOMPLIAN_API_KEY", "API_KEY"),
314
+ adminApiKey: get("KOMPLIAN_ADMIN_API_KEY", "ADMIN_API_KEY"),
315
+ workspaceId: get("KOMPLIAN_WORKSPACE_ID", "WORKSPACE_ID"),
316
+ widgetId: get("KOMPLIAN_WIDGET_ID", "WIDGET_ID"),
317
+ sessionId: get("KOMPLIAN_SESSION_ID", "SESSION_ID"),
318
+ leadId: get("KOMPLIAN_LEAD_ID", "LEAD_ID"),
319
+ flowId: get("KOMPLIAN_FLOW_ID", "FLOW_ID"),
320
+ sourceId: get("KOMPLIAN_SOURCE_ID", "SOURCE_ID"),
321
+ documentId: get("KOMPLIAN_DOCUMENT_ID", "DOCUMENT_ID"),
322
+ queryId: get("KOMPLIAN_QUERY_ID", "QUERY_ID"),
323
+ userId: get("KOMPLIAN_USER_ID", "USER_ID"),
324
+ entryId: get("KOMPLIAN_ENTRY_ID", "ENTRY_ID"),
325
+ };
326
+ }
327
+
328
+ function summarizeSecretsLoaded(s) {
329
+ return Object.keys(s).filter(
330
+ (k) => s[k] != null && String(s[k]).trim() !== ""
331
+ );
332
+ }
333
+
73
334
  function headers(apiKey) {
74
335
  return {
75
336
  "X-API-Key": apiKey,
@@ -279,52 +540,50 @@ function buildCollection() {
279
540
  };
280
541
  }
281
542
 
282
- function buildEnvironments() {
283
- const secret = (key) => ({
284
- key,
285
- value: "",
286
- type: "secret",
287
- enabled: true,
288
- });
289
- const plain = (key, value) => ({
290
- key,
291
- value,
292
- type: "default",
293
- enabled: true,
294
- });
543
+ /**
544
+ * @param {Record<string, string>} secrets - desde API_KEY / api/.env (no subir a git los JSON locales).
545
+ */
546
+ function buildEnvironments(secrets = {}) {
547
+ const pick = (key, fallback = "") => {
548
+ const v = secrets[key];
549
+ if (v != null && String(v).trim() !== "") return String(v).trim();
550
+ return fallback;
551
+ };
552
+ const row = (key, def = "") => {
553
+ const val = pick(key, def);
554
+ const isSecret = key === "apiKey" || key === "adminApiKey";
555
+ return {
556
+ key,
557
+ value: val,
558
+ type: isSecret ? "secret" : "default",
559
+ enabled: true,
560
+ };
561
+ };
295
562
 
296
- const common = [
297
- plain("baseUrl", ""),
298
- secret("apiKey"),
299
- secret("adminApiKey"),
300
- plain("workspaceId", ""),
301
- plain("widgetId", ""),
302
- plain("sessionId", ""),
303
- plain("leadId", ""),
304
- plain("flowId", ""),
305
- plain("sourceId", ""),
306
- plain("documentId", ""),
307
- plain("queryId", ""),
308
- plain("userId", ""),
309
- plain("entryId", ""),
563
+ const rowsForBase = (baseUrl) => [
564
+ row("baseUrl", baseUrl),
565
+ row("apiKey"),
566
+ row("adminApiKey"),
567
+ row("workspaceId"),
568
+ row("widgetId"),
569
+ row("sessionId"),
570
+ row("leadId"),
571
+ row("flowId"),
572
+ row("sourceId"),
573
+ row("documentId"),
574
+ row("queryId"),
575
+ row("userId"),
576
+ row("entryId"),
310
577
  ];
311
578
 
312
579
  return [
313
580
  {
314
581
  name: "Komplian — Local Dev",
315
- values: common.map((v) =>
316
- v.key === "baseUrl"
317
- ? { ...v, value: "http://localhost:4000", type: "default" }
318
- : { ...v }
319
- ),
582
+ values: rowsForBase("http://localhost:4000"),
320
583
  },
321
584
  {
322
585
  name: "Komplian — Production",
323
- values: common.map((v) =>
324
- v.key === "baseUrl"
325
- ? { ...v, value: "https://api.komplian.com", type: "default" }
326
- : { ...v }
327
- ),
586
+ values: rowsForBase("https://api.komplian.com"),
328
587
  },
329
588
  ];
330
589
  }
@@ -358,7 +617,7 @@ async function verifyKomplianEmail(apiKey, domain) {
358
617
  }
359
618
  if (!emailAllowed(email, domain)) {
360
619
  log(
361
- `${c.red}✗${c.reset} Este comando solo permite cuentas **@${domain}**. Sesión Postman: ${c.bold}${email}${c.reset}`
620
+ `${c.red}✗${c.reset} Este comando solo permite cuentas **@${domain}**. Sesión: ${c.bold}${maskEmail(email)}${c.reset}`
362
621
  );
363
622
  log(
364
623
  `${c.dim} Crea la API key desde una cuenta @${domain} o añade ese email a tu usuario en Postman.${c.reset}`
@@ -366,7 +625,7 @@ async function verifyKomplianEmail(apiKey, domain) {
366
625
  process.exit(1);
367
626
  }
368
627
  log(
369
- `${c.green}✓${c.reset} Postman: ${c.bold}${email}${c.reset} (${c.dim}dominio permitido${c.reset})`
628
+ `${c.green}✓${c.reset} Postman: ${c.bold}${maskEmail(email)}${c.reset} (${c.dim}dominio permitido${c.reset})`
370
629
  );
371
630
  return email;
372
631
  }
@@ -387,13 +646,93 @@ async function createEnvironment(apiKey, workspaceId, env) {
387
646
  });
388
647
  }
389
648
 
649
+ async function listCollections(apiKey, workspaceId) {
650
+ const q = new URLSearchParams({ workspace: workspaceId });
651
+ return pmFetch(apiKey, `/collections?${q}`);
652
+ }
653
+
654
+ async function listEnvironments(apiKey, workspaceId) {
655
+ const q = new URLSearchParams({ workspace: workspaceId });
656
+ return pmFetch(apiKey, `/environments?${q}`);
657
+ }
658
+
659
+ function firstArray(...candidates) {
660
+ for (const c of candidates) {
661
+ if (Array.isArray(c)) return c;
662
+ }
663
+ return [];
664
+ }
665
+
666
+ function findCollectionByName(body, name) {
667
+ const arr = firstArray(
668
+ body.collections,
669
+ body.data?.collections,
670
+ body.data,
671
+ Array.isArray(body) ? body : null
672
+ );
673
+ return arr.find(
674
+ (c) =>
675
+ c.name === name ||
676
+ c.collection?.name === name ||
677
+ c.info?.name === name
678
+ );
679
+ }
680
+
681
+ function findEnvironmentByName(body, name) {
682
+ const arr = firstArray(
683
+ body.environments,
684
+ body.data?.environments,
685
+ body.data,
686
+ Array.isArray(body) ? body : null
687
+ );
688
+ return arr.find((e) => e.name === name);
689
+ }
690
+
691
+ async function upsertCollection(apiKey, workspaceId, collection) {
692
+ const name = collection.info?.name;
693
+ const list = await listCollections(apiKey, workspaceId);
694
+ if (!list.ok) {
695
+ return { ok: false, status: list.status, body: list.body };
696
+ }
697
+ const existing = findCollectionByName(list.body, name);
698
+ if (!existing) {
699
+ const r = await createCollection(apiKey, workspaceId, collection);
700
+ return { ...r, op: "create" };
701
+ }
702
+ const uid = existing.uid ?? existing.id;
703
+ const r = await pmFetch(apiKey, `/collections/${uid}`, {
704
+ method: "PUT",
705
+ body: JSON.stringify({ collection }),
706
+ });
707
+ return { ...r, op: "update" };
708
+ }
709
+
710
+ async function upsertEnvironment(apiKey, workspaceId, env) {
711
+ const list = await listEnvironments(apiKey, workspaceId);
712
+ if (!list.ok) {
713
+ return { ok: false, status: list.status, body: list.body };
714
+ }
715
+ const existing = findEnvironmentByName(list.body, env.name);
716
+ if (!existing) {
717
+ const r = await createEnvironment(apiKey, workspaceId, env);
718
+ return { ...r, op: "create" };
719
+ }
720
+ const uid = existing.uid ?? existing.id;
721
+ const r = await pmFetch(apiKey, `/environments/${uid}`, {
722
+ method: "PUT",
723
+ body: JSON.stringify({ environment: env }),
724
+ });
725
+ return { ...r, op: "update" };
726
+ }
727
+
390
728
  function parseArgs(argv) {
391
- const out = { yes: false, exportOnly: false, outDir: "", help: false };
729
+ const out = { yes: false, exportOnly: false, outDir: "", help: false, dotenv: "" };
392
730
  for (let i = 0; i < argv.length; i++) {
393
731
  const a = argv[i];
394
732
  if (a === "--yes" || a === "-y") out.yes = true;
395
733
  else if (a === "--export-only") out.exportOnly = true;
396
734
  else if (a === "--out") out.outDir = argv[++i] || "";
735
+ else if (a === "--dotenv") out.dotenv = argv[++i] || "";
397
736
  else if (a === "-h" || a === "--help") out.help = true;
398
737
  else if (a.startsWith("-")) {
399
738
  log(`${c.red}✗${c.reset} Opción desconocida: ${a}`);
@@ -423,8 +762,10 @@ function usage() {
423
762
  log(` -y, --yes Sin prompts extra`);
424
763
  log(` --export-only Solo escribe JSON en disco (no llama a la API de Postman)`);
425
764
  log(` --out <dir> Carpeta para export (por defecto: ./komplian-postman)`);
765
+ log(` --dotenv <ruta> .env extra (además de api/.env, .env, KOMPLIAN_DOTENV)`);
426
766
  log(` -h, --help`);
427
767
  log(``);
768
+ log(` Variables Komplian en Postman: API_KEY, ADMIN_API_KEY, WORKSPACE_ID, … (env o .env)`);
428
769
  log(` Opcional: POSTMAN_WORKSPACE_ID, KOMPLIAN_EMAIL_DOMAIN, KOMPLIAN_POSTMAN_KEY_FILE`);
429
770
  }
430
771
 
@@ -436,25 +777,6 @@ function usageLogin() {
436
777
  log(` Postman → Settings (avatar) → API keys → Generate`);
437
778
  }
438
779
 
439
- async function promptApiKey() {
440
- if (input.isTTY) {
441
- const rl = createInterface({ input, output });
442
- const line = await rl.question(
443
- `${c.cyan}Pega tu Postman API key${c.reset} (Settings → API keys): `
444
- );
445
- rl.close();
446
- return line.trim();
447
- }
448
- return new Promise((resolve, reject) => {
449
- const chunks = [];
450
- input.on("data", (d) => chunks.push(d));
451
- input.on("end", () => {
452
- resolve(Buffer.concat(chunks).toString("utf8").trim());
453
- });
454
- input.on("error", reject);
455
- });
456
- }
457
-
458
780
  async function runPostmanLogin(argv) {
459
781
  const la = parseLoginArgs(argv);
460
782
  if (la.help) {
@@ -471,7 +793,9 @@ async function runPostmanLogin(argv) {
471
793
  const domain = (process.env.KOMPLIAN_EMAIL_DOMAIN || "komplian.com").trim();
472
794
  const keyPath = defaultKeyPath();
473
795
 
474
- let apiKey = await promptApiKey();
796
+ let apiKey = await readPasswordLine(
797
+ "Postman API key (no se muestra al escribir; Settings → API keys): "
798
+ );
475
799
  if (!apiKey) {
476
800
  log(`${c.red}✗${c.reset} Clave vacía.`);
477
801
  process.exit(1);
@@ -479,7 +803,7 @@ async function runPostmanLogin(argv) {
479
803
 
480
804
  await verifyKomplianEmail(apiKey, domain);
481
805
 
482
- mkdirSync(join(homedir(), ".komplian"), { recursive: true });
806
+ ensureSecureKomplianDir();
483
807
  writeFileSync(keyPath, `${apiKey}\n`, { encoding: "utf8", mode: 0o600 });
484
808
  try {
485
809
  chmodSync(keyPath, 0o600);
@@ -489,7 +813,7 @@ async function runPostmanLogin(argv) {
489
813
 
490
814
  log("");
491
815
  log(
492
- `${c.green}✓${c.reset} Guardada en ${c.bold}${keyPath}${c.reset} (solo tu usuario).`
816
+ `${c.green}✓${c.reset} Guardada en ${c.bold}${formatHomePath(keyPath)}${c.reset} (permiso 600; no subir a git).`
493
817
  );
494
818
  log(
495
819
  `${c.dim} Siguiente: ${c.reset}${c.cyan}npx komplian postman --yes${c.reset}`
@@ -522,14 +846,29 @@ export async function runPostman(argv) {
522
846
  printMissingKeyHelp();
523
847
  }
524
848
  if (source && source !== "POSTMAN_API_KEY") {
525
- log(`${c.dim}→ API key: ${source}${c.reset}`);
849
+ log(`${c.dim}→ API key Postman desde: ${formatHomePath(source)}${c.reset}`);
526
850
  }
527
851
 
528
852
  const domain = (process.env.KOMPLIAN_EMAIL_DOMAIN || "komplian.com").trim();
529
853
  await verifyKomplianEmail(apiKey, domain);
530
854
 
531
855
  const collection = buildCollection();
532
- const envs = buildEnvironments();
856
+ const cwd = process.cwd();
857
+ const secrets = loadKomplianSecrets(cwd, args.dotenv);
858
+ const envsForUpload = buildEnvironments(secrets);
859
+ const envsForExport = buildEnvironments({});
860
+
861
+ const filled = summarizeSecretsLoaded(secrets);
862
+ if (filled.length) {
863
+ log(
864
+ `${c.green}✓${c.reset} Komplian → Postman: ${c.dim}${filled.join(", ")}${c.reset} (process.env / .env)`
865
+ );
866
+ } else {
867
+ log(
868
+ `${c.dim}○ Sin API_KEY / ADMIN_API_KEY / IDs en env o .env — los entornos en Postman quedarán vacíos en esos campos.${c.reset}`
869
+ );
870
+ }
871
+
533
872
  const workspaceId = await pickWorkspaceId(
534
873
  apiKey,
535
874
  process.env.POSTMAN_WORKSPACE_ID
@@ -547,10 +886,10 @@ export async function runPostman(argv) {
547
886
  writeFileSync(collPath, JSON.stringify(collection, null, 2), "utf8");
548
887
  log(`${c.green}✓${c.reset} Export: ${c.dim}${collPath}${c.reset}`);
549
888
 
550
- for (const env of envs) {
889
+ for (const env of envsForExport) {
551
890
  const safe = env.name.replace(/[\\/]/g, "-") + ".postman_environment.json";
552
891
  writeFileSync(join(outBase, safe), JSON.stringify({ ...env }, null, 2), "utf8");
553
- log(`${c.green}✓${c.reset} Export: ${c.dim}${join(outBase, safe)}${c.reset}`);
892
+ log(`${c.green}✓${c.reset} Export: ${c.dim}${join(outBase, safe)}${c.reset} ${c.dim}(sin secretos; no commitear)${c.reset}`);
554
893
  }
555
894
 
556
895
  if (args.exportOnly) {
@@ -562,38 +901,40 @@ export async function runPostman(argv) {
562
901
  }
563
902
 
564
903
  log("");
565
- log(`${c.cyan}━━ Subiendo a Postman (API) ━━${c.reset}`);
904
+ log(`${c.cyan}━━ Sincronizar Postman (crear o actualizar) ━━${c.reset}`);
566
905
 
567
- const cr = await createCollection(apiKey, workspaceId, collection);
906
+ const cr = await upsertCollection(apiKey, workspaceId, collection);
568
907
  if (!cr.ok) {
908
+ logApiFailure("Colección API", cr.status, cr.body);
569
909
  log(
570
- `${c.yellow}○${c.reset} Colección API: ${cr.status} ${JSON.stringify(cr.body).slice(0, 400)}`
571
- );
572
- log(
573
- `${c.dim} Si el nombre ya existe, borra la colección antigua en Postman o importa el JSON exportado.${c.reset}`
910
+ `${c.dim} Revisa permisos del workspace o importa el JSON exportado. Depuración: KOMPLIAN_DEBUG=1 (salida redactada).${c.reset}`
574
911
  );
575
912
  } else {
913
+ const verb = cr.op === "update" ? "actualizada" : "creada";
576
914
  const uid = cr.body?.collection?.uid || cr.body?.collection?.id || "?";
577
- log(`${c.green}✓${c.reset} Colección creada en Postman (${uid})`);
915
+ log(`${c.green}✓${c.reset} Colección ${verb}: ${c.bold}Komplian API${c.reset} (${uid})`);
578
916
  }
579
917
 
580
- for (const env of envs) {
581
- const er = await createEnvironment(apiKey, workspaceId, {
918
+ for (const env of envsForUpload) {
919
+ const er = await upsertEnvironment(apiKey, workspaceId, {
582
920
  name: env.name,
583
921
  values: env.values,
584
922
  });
585
923
  if (!er.ok) {
924
+ logApiFailure(`Entorno "${env.name}"`, er.status, er.body);
925
+ } else {
926
+ const verb = er.op === "update" ? "actualizado" : "creado";
586
927
  log(
587
- `${c.yellow}○${c.reset} Entorno "${env.name}": ${er.status}${JSON.stringify(er.body).slice(0, 200)}`
928
+ `${c.green}✓${c.reset} Entorno ${verb}: ${c.bold}${env.name}${c.reset}`
588
929
  );
589
- } else {
590
- log(`${c.green}✓${c.reset} Entorno: ${c.bold}${env.name}${c.reset}`);
591
930
  }
592
931
  }
593
932
 
594
933
  log("");
595
- log(`${c.green}✓${c.reset} Abre Postman workspace → revisa ${c.bold}Komplian API${c.reset} y los entornos.`);
596
- log(
597
- `${c.dim} Rellena apiKey / adminApiKey / IDs en el entorno (secret).${c.reset}`
598
- );
934
+ log(`${c.green}✓${c.reset} En Postman: elige entorno ${c.dim}(Local Dev / Production)${c.reset} y prueba las peticiones.`);
935
+ if (!filled.length) {
936
+ log(
937
+ `${c.dim} Para rellenar apiKey: exporta API_KEY o pon api/.env y vuelve a ejecutar este comando.${c.reset}`
938
+ );
939
+ }
599
940
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "komplian",
3
- "version": "0.4.1",
3
+ "version": "0.4.4",
4
4
  "description": "Komplian developer workspace: GitHub clone (onboard) + Postman collection/environments (postman). Node 18+.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -12,6 +12,7 @@
12
12
  "files": [
13
13
  "komplian-onboard.mjs",
14
14
  "komplian-postman.mjs",
15
+ "komplian-localhost.mjs",
15
16
  "komplian-team-repos.json",
16
17
  "README.md"
17
18
  ],
@@ -19,7 +20,7 @@
19
20
  "access": "public"
20
21
  },
21
22
  "scripts": {
22
- "prepublishOnly": "node --check komplian-onboard.mjs && node --check komplian-postman.mjs"
23
+ "prepublishOnly": "node --check komplian-onboard.mjs && node --check komplian-postman.mjs && node --check komplian-localhost.mjs"
23
24
  },
24
25
  "keywords": [
25
26
  "komplian",