komplian 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,11 +24,15 @@ npx komplian postman --yes
24
24
 
25
25
  Opcional: `export POSTMAN_API_KEY=…` solo para la sesión actual (tiene prioridad sobre el archivo).
26
26
 
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`.
27
+ El comando llama a `GET https://api.getpostman.com/me` y **solo continúa** si el email de la cuenta es `@komplian.com`. **Si ya existen** la colección **Komplian API** y los entornos con el mismo nombre en ese workspace, se **actualizan**; si no, se crean.
28
+
29
+ **Variables de la API Komplian** (`apiKey`, `adminApiKey`, `workspaceId`, …) se rellenan en Postman automáticamente si están en `process.env` o en archivos `.env` (busca `api/.env`, `.env`, etc.; o `KOMPLIAN_DOTENV` / `--dotenv ruta`). Nombres típicos: `API_KEY`, `ADMIN_API_KEY`, `KOMPLIAN_WORKSPACE_ID`. Los JSON exportados en `./komplian-postman/` **no** incluyen secretos (para no commitearlos).
28
30
 
29
31
  - Solo exportar archivos (sin subir por API): `npx komplian postman --yes --export-only`
30
32
  - Otro workspace: `POSTMAN_WORKSPACE_ID=<id>`
31
- - Ruta alternativa del archivo de clave: `KOMPLIAN_POSTMAN_KEY_FILE`
33
+ - Ruta alternativa del archivo de clave Postman: `KOMPLIAN_POSTMAN_KEY_FILE`
34
+
35
+ **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
36
 
33
37
  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
38
  **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`.
@@ -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.3",
4
4
  "description": "Komplian developer workspace: GitHub clone (onboard) + Postman collection/environments (postman). Node 18+.",
5
5
  "type": "module",
6
6
  "engines": {