komplian 0.3.7 → 0.4.0

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
@@ -6,8 +6,24 @@
6
6
  2. Browser login: `gh auth login -h github.com -s repo -s read:org -w`
7
7
  3. `npx komplian onboard --yes`
8
8
 
9
+ ### Postman (colección + entornos)
10
+
11
+ 1. En [Postman](https://postman.com) usa una cuenta con email **`@komplian.com`** (o añade ese email a tu perfil).
12
+ 2. Crea una **API key**: Settings → **API keys** → Generate.
13
+ 3. En la terminal:
14
+
15
+ ```bash
16
+ export POSTMAN_API_KEY=pmak-…
17
+ npx komplian postman --yes
18
+ ```
19
+
20
+ 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** (carpetas como en el workspace interno), los entornos **Komplian — Local Dev** y **Komplian — Production** (variables `baseUrl`, `apiKey`, `adminApiKey`, IDs, etc.), y deja copia en `./komplian-postman/*.json` por si hace falta importar a mano.
21
+
22
+ - Solo exportar archivos (sin subir por API): `npx komplian postman --yes --export-only`
23
+ - Otro workspace: `POSTMAN_WORKSPACE_ID=<id>` (si no, se usa un workspace por defecto con permiso de escritura)
24
+
9
25
  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.
10
- **npm install** runs with `--no-audit --no-fund` unless `KOMPLIAN_NPM_AUDIT=1`. Run `npm audit` in each repo when you work on it.
26
+ **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`.
11
27
 
12
28
  **Maintainers:** publish from **`scripts/`** (folder with `package.json`), not the monorepo root:
13
29
 
@@ -304,36 +304,99 @@ function copyCursorPack(workspace, cursorRepoUrl) {
304
304
  );
305
305
  }
306
306
 
307
- function npmInstallEach(workspace) {
308
- log("");
309
- log(`${c.cyan}━━ npm install por repo ━━${c.reset}`);
310
- if (!canRun("npm", ["--version"])) {
311
- log(`${c.yellow}○${c.reset} npm no está en PATH — omito installs`);
312
- return;
313
- }
307
+ /** Sin esto, `npm install` crea o retoca package-lock.json y git muestra cambios sin querer. */
308
+ function npmQuietFlags() {
314
309
  const audit =
315
310
  process.env.KOMPLIAN_NPM_AUDIT === "1" || process.env.KOMPLIAN_NPM_AUDIT === "true";
316
- const installArgs = audit
317
- ? ["install"]
318
- : ["install", "--no-audit", "--no-fund"];
311
+ return audit ? [] : ["--no-audit", "--no-fund"];
312
+ }
313
+
314
+ function npmInstallOneRepo(dir, name) {
315
+ const pkg = join(dir, "package.json");
316
+ if (!existsSync(pkg)) return { ok: true, skipped: true };
317
+
318
+ const yarnLock = join(dir, "yarn.lock");
319
+ const pnpmLock = join(dir, "pnpm-lock.yaml");
320
+ const npmLock = join(dir, "package-lock.json");
321
+
322
+ if (existsSync(yarnLock)) {
323
+ if (!canRun("yarn", ["--version"])) {
324
+ log(
325
+ `${c.yellow}○${c.reset} ${name} ${c.dim}(yarn.lock; instala yarn o ejecuta yarn install a mano)${c.reset}`
326
+ );
327
+ return { ok: true, skipped: true };
328
+ }
329
+ log(`${c.dim}→${c.reset} ${name} ${c.dim}(yarn)${c.reset}`);
330
+ const r = spawnSync(
331
+ "yarn",
332
+ ["install", "--frozen-lockfile"],
333
+ spawnWin({ cwd: dir, stdio: "inherit" })
334
+ );
335
+ return { ok: r.status === 0, skipped: false };
336
+ }
337
+
338
+ if (existsSync(pnpmLock)) {
339
+ if (!canRun("pnpm", ["--version"])) {
340
+ log(
341
+ `${c.yellow}○${c.reset} ${name} ${c.dim}(pnpm-lock; instala pnpm o pnpm install a mano)${c.reset}`
342
+ );
343
+ return { ok: true, skipped: true };
344
+ }
345
+ log(`${c.dim}→${c.reset} ${name} ${c.dim}(pnpm)${c.reset}`);
346
+ const r = spawnSync(
347
+ "pnpm",
348
+ ["install", "--frozen-lockfile"],
349
+ spawnWin({ cwd: dir, stdio: "inherit" })
350
+ );
351
+ return { ok: r.status === 0, skipped: false };
352
+ }
353
+
354
+ if (!canRun("npm", ["--version"])) {
355
+ log(`${c.yellow}○${c.reset} npm no está en PATH — omito ${name}`);
356
+ return { ok: true, skipped: true };
357
+ }
358
+
359
+ const quiet = npmQuietFlags();
360
+
361
+ if (existsSync(npmLock)) {
362
+ log(`${c.dim}→${c.reset} ${name} ${c.dim}(npm ci — lock sin cambios)${c.reset}`);
363
+ const r = spawnSync("npm", ["ci", ...quiet], spawnWin({ cwd: dir, stdio: "inherit" }));
364
+ if (r.status === 0) return { ok: true, skipped: false };
365
+ log(
366
+ `${c.yellow}○${c.reset} ${name}: npm ci falló (¿lock desincronizado?). ${c.dim}Revisa con npm install en ese repo.${c.reset}`
367
+ );
368
+ return { ok: false, skipped: false };
369
+ }
370
+
371
+ log(`${c.dim}→${c.reset} ${name} ${c.dim}(npm install — sin crear package-lock)${c.reset}`);
372
+ const r = spawnSync(
373
+ "npm",
374
+ ["install", ...quiet, "--no-package-lock"],
375
+ spawnWin({ cwd: dir, stdio: "inherit" })
376
+ );
377
+ return { ok: r.status === 0, skipped: false };
378
+ }
379
+
380
+ function npmInstallEach(workspace) {
381
+ log("");
382
+ log(`${c.cyan}━━ Dependencias por repo ━━${c.reset}`);
319
383
  for (const ent of readdirSync(workspace)) {
320
384
  const d = join(workspace, ent);
321
385
  if (!statSync(d).isDirectory()) continue;
322
- const pkg = join(d, "package.json");
323
- if (!existsSync(pkg)) continue;
324
- log(`${c.dim}→${c.reset} ${ent}`);
325
- const ir = spawnSync("npm", installArgs, spawnWin({ cwd: d, stdio: "inherit" }));
326
- if (ir.status !== 0) {
327
- log(`${c.yellow}○${c.reset} npm install con avisos en ${ent}`);
328
- } else {
386
+ const { ok, skipped } = npmInstallOneRepo(d, ent);
387
+ if (skipped) continue;
388
+ if (ok) {
329
389
  log(`${c.green}✓${c.reset} ${ent}`);
390
+ } else {
391
+ log(`${c.yellow}○${c.reset} ${ent}`);
330
392
  }
331
393
  }
332
394
  }
333
395
 
334
396
  function usage() {
335
- log(`Uso: komplian onboard [opciones] [carpeta]`);
397
+ log(`Uso: komplian onboard [opciones] [carpeta] | komplian postman [opciones]`);
336
398
  log(` npx komplian onboard --yes`);
399
+ log(` npx komplian postman --yes ${c.dim}(POSTMAN_API_KEY + email @komplian.com)${c.reset}`);
337
400
  log(``);
338
401
  log(` Antes (una vez): gh auth login -h github.com -s repo -s read:org -w`);
339
402
  log(` Requisitos: Node 18+, git, GitHub CLI (gh)`);
@@ -416,8 +479,15 @@ function normalizeArgv(argv) {
416
479
  }
417
480
 
418
481
  async function main() {
482
+ const rawArgv = process.argv.slice(2);
483
+ if (rawArgv[0] === "postman") {
484
+ const { runPostman } = await import("./komplian-postman.mjs");
485
+ await runPostman(rawArgv.slice(1));
486
+ return;
487
+ }
488
+
419
489
  const configPath = join(__dirname, "komplian-team-repos.json");
420
- const { argv, fromOnboardSubcommand } = normalizeArgv(process.argv.slice(2));
490
+ const { argv, fromOnboardSubcommand } = normalizeArgv(rawArgv);
421
491
  const args = parseArgs(argv);
422
492
  if (fromOnboardSubcommand && !args.noInstall && !args.listTeams && !args.help) {
423
493
  args.install = true;
@@ -521,7 +591,7 @@ async function main() {
521
591
  }
522
592
  log(`${c.green}✓${c.reset} Cursor: ${c.bold}File → Open Folder → ${abs}${c.reset}`);
523
593
  log(
524
- `${c.dim} npm install usa --no-audit --no-fund (menos ruido). Auditoría: cd cada repo y npm audit. KOMPLIAN_NPM_AUDIT=1 para resumen completo.${c.reset}`
594
+ `${c.dim} Con package-lock.json: npm ci (no retoca el lock). Sin lock: npm install --no-package-lock. yarn/pnpm: lock congelado. KOMPLIAN_NPM_AUDIT=1 activa auditoría en npm.${c.reset}`
525
595
  );
526
596
  log(`${c.dim} .env.example → .env por proyecto; secretos en 1Password — nunca commit.${c.reset}`);
527
597
  }
@@ -0,0 +1,466 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Komplian Postman — API key + email @komplian.com (Postman /me), luego colección + entornos.
4
+ *
5
+ * Requisitos: Node 18+, variable POSTMAN_API_KEY (Postman → Settings → API keys)
6
+ *
7
+ * export POSTMAN_API_KEY=pmak-...
8
+ * npx komplian postman --yes
9
+ *
10
+ * Opcional: POSTMAN_WORKSPACE_ID (si no, usa el primer workspace con permiso de escritura)
11
+ * Opcional: KOMPLIAN_EMAIL_DOMAIN=komplian.com (solo para pruebas internas del script)
12
+ */
13
+
14
+ import { writeFileSync, mkdirSync } from "node:fs";
15
+ import { join, resolve } from "node:path";
16
+
17
+ const POSTMAN_API = "https://api.getpostman.com";
18
+
19
+ const c = {
20
+ reset: "\x1b[0m",
21
+ dim: "\x1b[2m",
22
+ bold: "\x1b[1m",
23
+ cyan: "\x1b[36m",
24
+ green: "\x1b[32m",
25
+ red: "\x1b[31m",
26
+ yellow: "\x1b[33m",
27
+ };
28
+
29
+ function log(s = "") {
30
+ console.log(s);
31
+ }
32
+
33
+ function headers(apiKey) {
34
+ return {
35
+ "X-API-Key": apiKey,
36
+ Accept: "application/json",
37
+ "Content-Type": "application/json",
38
+ };
39
+ }
40
+
41
+ async function pmFetch(apiKey, path, opts = {}) {
42
+ const url = path.startsWith("http") ? path : `${POSTMAN_API}${path}`;
43
+ const r = await fetch(url, {
44
+ ...opts,
45
+ headers: { ...headers(apiKey), ...opts.headers },
46
+ });
47
+ const text = await r.text();
48
+ let body;
49
+ try {
50
+ body = text ? JSON.parse(text) : {};
51
+ } catch {
52
+ body = { _raw: text };
53
+ }
54
+ return { ok: r.ok, status: r.status, body };
55
+ }
56
+
57
+ function extractEmail(meBody) {
58
+ const b = meBody ?? {};
59
+ const u = b.user ?? b.data?.user ?? b;
60
+ const e = u?.email ?? b.email ?? b.data?.email;
61
+ return typeof e === "string" ? e.trim().toLowerCase() : "";
62
+ }
63
+
64
+ function emailAllowed(email, domain) {
65
+ const d = domain.toLowerCase().replace(/^@/, "");
66
+ return email.endsWith(`@${d}`);
67
+ }
68
+
69
+ function req(name, method, path, auth = "apiKey") {
70
+ const h = [];
71
+ if (auth === "apiKey") {
72
+ h.push({ key: "X-API-Key", value: "{{apiKey}}", type: "text" });
73
+ } else if (auth === "admin") {
74
+ h.push({ key: "X-API-Key", value: "{{adminApiKey}}", type: "text" });
75
+ }
76
+ const raw = path.startsWith("http") ? path : `{{baseUrl}}${path}`;
77
+ return {
78
+ name,
79
+ request: {
80
+ method,
81
+ header: h,
82
+ url: raw,
83
+ description:
84
+ auth === "none"
85
+ ? "Sin cabecera X-API-Key (público)."
86
+ : auth === "admin"
87
+ ? "Cabecera X-API-Key con valor de adminApiKey."
88
+ : "Cabecera X-API-Key con valor de apiKey (workspace).",
89
+ },
90
+ };
91
+ }
92
+
93
+ function folder(name, items) {
94
+ return { name, item: items };
95
+ }
96
+
97
+ function buildCollection() {
98
+ return {
99
+ info: {
100
+ name: "Komplian API",
101
+ description:
102
+ "Colección generada por `npx komplian postman`. Variables: baseUrl, apiKey, adminApiKey, workspaceId, widgetId, sessionId, leadId, flowId, sourceId.",
103
+ schema:
104
+ "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
105
+ },
106
+ item: [
107
+ folder("Health", [
108
+ req("Root", "GET", "/", "none"),
109
+ req("Health check", "GET", "/health", "none"),
110
+ ]),
111
+ folder("Public", [
112
+ req("Lead capture (web)", "POST", "/api/web/leads", "none"),
113
+ ]),
114
+ folder("Widget (Public)", [
115
+ req("Get widget config", "GET", "/api/widget/{{widgetId}}/config", "none"),
116
+ req("Create session", "POST", "/api/widget/{{widgetId}}/session", "none"),
117
+ req("Send message", "POST", "/api/widget/{{widgetId}}/message", "none"),
118
+ req("Get messages", "GET", "/api/widget/{{widgetId}}/messages/{{sessionId}}", "none"),
119
+ req("List sessions", "GET", "/api/widget/{{widgetId}}/sessions", "none"),
120
+ ]),
121
+ folder("Workspaces", [
122
+ req("List workspaces", "GET", "/api/workspaces", "apiKey"),
123
+ req("Get workspace", "GET", "/api/workspaces/{{workspaceId}}", "apiKey"),
124
+ req("Update workspace", "PUT", "/api/workspaces/{{workspaceId}}", "apiKey"),
125
+ req("Delete workspace", "DELETE", "/api/workspaces/{{workspaceId}}", "apiKey"),
126
+ req("Restore workspace", "POST", "/api/workspaces/{{workspaceId}}/restore", "apiKey"),
127
+ req("Regenerate API key", "POST", "/api/workspaces/{{workspaceId}}/api-key", "apiKey"),
128
+ req("Update logo", "PUT", "/api/workspaces/{{workspaceId}}/logo", "apiKey"),
129
+ req("Diagnostic", "GET", "/api/workspaces/{{workspaceId}}/diagnostic", "apiKey"),
130
+ ]),
131
+ folder("AI & Chat", [
132
+ req("AI chat", "POST", "/api/ai/chat", "apiKey"),
133
+ req("AI chat (stream)", "POST", "/api/ai/chat/stream", "apiKey"),
134
+ req("Test knowledge", "POST", "/api/ai/test-knowledge", "apiKey"),
135
+ req("Generate flow (AI)", "POST", "/api/ai/flows/generate", "apiKey"),
136
+ ]),
137
+ folder("Knowledge Base", [
138
+ req("Create source", "POST", "/api/knowledge/sources", "apiKey"),
139
+ req("List sources", "GET", "/api/knowledge/sources", "apiKey"),
140
+ req("Get source", "GET", "/api/knowledge/sources/{{sourceId}}", "apiKey"),
141
+ req("Update source", "PUT", "/api/knowledge/sources/{{sourceId}}", "apiKey"),
142
+ req("Delete source", "DELETE", "/api/knowledge/sources/{{sourceId}}", "apiKey"),
143
+ req("Scrape source", "POST", "/api/knowledge/sources/{{sourceId}}/scrape", "apiKey"),
144
+ req("Source jobs", "GET", "/api/knowledge/sources/{{sourceId}}/jobs", "apiKey"),
145
+ req("Search knowledge", "POST", "/api/knowledge/search", "apiKey"),
146
+ req("Knowledge stats", "GET", "/api/knowledge/stats", "apiKey"),
147
+ req("Upload document", "POST", "/api/knowledge/documents", "apiKey"),
148
+ req("Process document", "POST", "/api/knowledge/documents/{{documentId}}/process", "apiKey"),
149
+ req("Process document URL", "POST", "/api/knowledge/documents/{{documentId}}/process-url", "apiKey"),
150
+ req("Detect doc type", "GET", "/api/knowledge/documents/detect-type", "apiKey"),
151
+ req("Get manual entries", "GET", "/api/knowledge/manual/{{workspaceId}}", "apiKey"),
152
+ req("Create manual entry", "POST", "/api/knowledge/manual", "apiKey"),
153
+ req("Update manual entry", "PUT", "/api/knowledge/manual/{{entryId}}", "apiKey"),
154
+ req("Delete manual entry", "DELETE", "/api/knowledge/manual/{{entryId}}", "apiKey"),
155
+ ]),
156
+ folder("Widget Config", [
157
+ req("Get config", "GET", "/api/widget/config/{{workspaceId}}", "apiKey"),
158
+ req("Update config", "PUT", "/api/widget/config/{{workspaceId}}", "apiKey"),
159
+ req("Publish", "POST", "/api/widget/config/{{workspaceId}}/publish", "apiKey"),
160
+ req("Unpublish", "POST", "/api/widget/config/{{workspaceId}}/unpublish", "apiKey"),
161
+ ]),
162
+ folder("Inbox", [
163
+ req("List sessions", "GET", "/api/inbox/{{workspaceId}}/sessions", "apiKey"),
164
+ req("Get session", "GET", "/api/inbox/session/{{sessionId}}", "apiKey"),
165
+ req("Takeover", "POST", "/api/inbox/session/{{sessionId}}/takeover", "apiKey"),
166
+ req("Agent message", "POST", "/api/inbox/session/{{sessionId}}/message", "apiKey"),
167
+ req("Typing", "POST", "/api/inbox/session/{{sessionId}}/typing", "apiKey"),
168
+ req("Mark read", "POST", "/api/inbox/session/{{sessionId}}/read", "apiKey"),
169
+ req("Archive", "POST", "/api/inbox/session/{{sessionId}}/archive", "apiKey"),
170
+ req("Translate", "POST", "/api/inbox/session/{{sessionId}}/translate", "apiKey"),
171
+ req("Products for inbox", "GET", "/api/inbox/{{workspaceId}}/products", "apiKey"),
172
+ ]),
173
+ folder("Analytics", [
174
+ req("Knowledge analytics", "GET", "/api/analytics/knowledge/{{workspaceId}}", "apiKey"),
175
+ req("Query log", "GET", "/api/analytics/knowledge/{{workspaceId}}/queries", "apiKey"),
176
+ req("Knowledge gaps", "GET", "/api/analytics/knowledge/{{workspaceId}}/gaps", "apiKey"),
177
+ req("Query chunks", "GET", "/api/analytics/knowledge/query/{{queryId}}/chunks", "apiKey"),
178
+ req("AI analytics", "GET", "/api/analytics/ai/{{workspaceId}}", "apiKey"),
179
+ ]),
180
+ folder("Leads", [
181
+ req("List leads", "GET", "/api/leads", "apiKey"),
182
+ req("Create lead", "POST", "/api/leads", "apiKey"),
183
+ req("Get lead", "GET", "/api/leads/{{leadId}}", "apiKey"),
184
+ req("Update lead", "PATCH", "/api/leads/{{leadId}}", "apiKey"),
185
+ req("Delete lead", "DELETE", "/api/leads/{{leadId}}", "apiKey"),
186
+ req("Timeline", "GET", "/api/leads/{{leadId}}/timeline", "apiKey"),
187
+ req("Get notes", "GET", "/api/leads/{{leadId}}/notes", "apiKey"),
188
+ req("Add note", "POST", "/api/leads/{{leadId}}/notes", "apiKey"),
189
+ req("Lead stats", "GET", "/api/leads/stats/{{workspaceId}}", "apiKey"),
190
+ ]),
191
+ folder("Flows", [
192
+ req("List flows", "GET", "/api/flows", "apiKey"),
193
+ req("Create flow", "POST", "/api/flows", "apiKey"),
194
+ req("Flow templates", "GET", "/api/flows/templates", "apiKey"),
195
+ req("Get flow", "GET", "/api/flows/{{flowId}}", "apiKey"),
196
+ req("Update flow", "PATCH", "/api/flows/{{flowId}}", "apiKey"),
197
+ req("Delete flow", "DELETE", "/api/flows/{{flowId}}", "apiKey"),
198
+ req("Publish", "POST", "/api/flows/{{flowId}}/publish", "apiKey"),
199
+ req("Pause", "POST", "/api/flows/{{flowId}}/pause", "apiKey"),
200
+ req("Duplicate", "POST", "/api/flows/{{flowId}}/duplicate", "apiKey"),
201
+ req("Flow analytics", "GET", "/api/flows/{{flowId}}/analytics", "apiKey"),
202
+ req("Executions", "GET", "/api/flows/{{flowId}}/executions", "apiKey"),
203
+ req("Test flow", "POST", "/api/flows/{{flowId}}/test", "apiKey"),
204
+ req("Trigger event", "POST", "/api/flows/events", "apiKey"),
205
+ req("Cron (flows)", "POST", "/api/flows/cron", "apiKey"),
206
+ ]),
207
+ folder("Email", [req("Send email", "POST", "/api/email/send", "apiKey")]),
208
+ folder("Users & Plans", [
209
+ req("List users", "GET", "/api/users", "apiKey"),
210
+ req("Get user", "GET", "/api/users/{{userId}}", "apiKey"),
211
+ req("Plans", "GET", "/api/plans", "apiKey"),
212
+ ]),
213
+ folder("Webhooks", [
214
+ req("WhatsApp verify", "GET", "/webhooks/whatsapp", "none"),
215
+ req("WhatsApp", "POST", "/webhooks/whatsapp", "none"),
216
+ req("Instagram verify", "GET", "/webhooks/instagram", "none"),
217
+ req("Instagram", "POST", "/webhooks/instagram", "none"),
218
+ req("Shopify GDPR data_request", "POST", "/webhooks/shopify/customers/data_request", "none"),
219
+ req("Shopify GDPR customers/redact", "POST", "/webhooks/shopify/customers/redact", "none"),
220
+ req("Shopify GDPR shop/redact", "POST", "/webhooks/shopify/shop/redact", "none"),
221
+ ]),
222
+ folder("N8N", [
223
+ req("n8n healthz", "GET", "https://n8n.komplian.com/healthz", "none"),
224
+ {
225
+ name: "Lead pipeline webhook (info)",
226
+ request: {
227
+ method: "GET",
228
+ header: [],
229
+ url: "https://n8n.komplian.com/webhook/komplian-lead-pipeline",
230
+ description:
231
+ "POST real desde automatización; GET solo como referencia de URL en Postman.",
232
+ },
233
+ },
234
+ ]),
235
+ ],
236
+ variable: [
237
+ { key: "baseUrl", value: "https://api.komplian.com", type: "string" },
238
+ ],
239
+ };
240
+ }
241
+
242
+ function buildEnvironments() {
243
+ const secret = (key) => ({
244
+ key,
245
+ value: "",
246
+ type: "secret",
247
+ enabled: true,
248
+ });
249
+ const plain = (key, value) => ({
250
+ key,
251
+ value,
252
+ type: "default",
253
+ enabled: true,
254
+ });
255
+
256
+ const common = [
257
+ plain("baseUrl", ""),
258
+ secret("apiKey"),
259
+ secret("adminApiKey"),
260
+ plain("workspaceId", ""),
261
+ plain("widgetId", ""),
262
+ plain("sessionId", ""),
263
+ plain("leadId", ""),
264
+ plain("flowId", ""),
265
+ plain("sourceId", ""),
266
+ plain("documentId", ""),
267
+ plain("queryId", ""),
268
+ plain("userId", ""),
269
+ plain("entryId", ""),
270
+ ];
271
+
272
+ return [
273
+ {
274
+ name: "Komplian — Local Dev",
275
+ values: common.map((v) =>
276
+ v.key === "baseUrl"
277
+ ? { ...v, value: "http://localhost:4000", type: "default" }
278
+ : { ...v }
279
+ ),
280
+ },
281
+ {
282
+ name: "Komplian — Production",
283
+ values: common.map((v) =>
284
+ v.key === "baseUrl"
285
+ ? { ...v, value: "https://api.komplian.com", type: "default" }
286
+ : { ...v }
287
+ ),
288
+ },
289
+ ];
290
+ }
291
+
292
+ async function pickWorkspaceId(apiKey, explicit) {
293
+ if (explicit && explicit.trim()) return explicit.trim();
294
+ const { ok, body } = await pmFetch(apiKey, "/workspaces");
295
+ if (!ok || !body.workspaces?.length) {
296
+ log(`${c.red}✗${c.reset} No se pudieron listar workspaces. ¿API key válida?`);
297
+ process.exit(1);
298
+ }
299
+ const w =
300
+ body.workspaces.find((x) => x.type === "personal") || body.workspaces[0];
301
+ return w.id;
302
+ }
303
+
304
+ async function verifyKomplianEmail(apiKey, domain) {
305
+ const { ok, status, body } = await pmFetch(apiKey, "/me");
306
+ if (!ok) {
307
+ log(
308
+ `${c.red}✗${c.reset} POSTMAN_API_KEY inválida o sin acceso (${status}).`
309
+ );
310
+ process.exit(1);
311
+ }
312
+ const email = extractEmail(body);
313
+ if (!email) {
314
+ log(
315
+ `${c.red}✗${c.reset} La API de Postman no devolvió email en /me. Usa una cuenta Postman con email visible.`
316
+ );
317
+ process.exit(1);
318
+ }
319
+ if (!emailAllowed(email, domain)) {
320
+ log(
321
+ `${c.red}✗${c.reset} Este comando solo permite cuentas **@${domain}**. Sesión Postman: ${c.bold}${email}${c.reset}`
322
+ );
323
+ log(
324
+ `${c.dim} Crea la API key desde una cuenta @${domain} o añade ese email a tu usuario en Postman.${c.reset}`
325
+ );
326
+ process.exit(1);
327
+ }
328
+ log(
329
+ `${c.green}✓${c.reset} Postman: ${c.bold}${email}${c.reset} (${c.dim}dominio permitido${c.reset})`
330
+ );
331
+ return email;
332
+ }
333
+
334
+ async function createCollection(apiKey, workspaceId, collection) {
335
+ const q = new URLSearchParams({ workspace: workspaceId });
336
+ return pmFetch(apiKey, `/collections?${q}`, {
337
+ method: "POST",
338
+ body: JSON.stringify({ collection }),
339
+ });
340
+ }
341
+
342
+ async function createEnvironment(apiKey, workspaceId, env) {
343
+ const q = new URLSearchParams({ workspace: workspaceId });
344
+ return pmFetch(apiKey, `/environments?${q}`, {
345
+ method: "POST",
346
+ body: JSON.stringify({ environment: env }),
347
+ });
348
+ }
349
+
350
+ function parseArgs(argv) {
351
+ const out = { yes: false, exportOnly: false, outDir: "", help: false };
352
+ for (let i = 0; i < argv.length; i++) {
353
+ const a = argv[i];
354
+ if (a === "--yes" || a === "-y") out.yes = true;
355
+ else if (a === "--export-only") out.exportOnly = true;
356
+ else if (a === "--out") out.outDir = argv[++i] || "";
357
+ else if (a === "-h" || a === "--help") out.help = true;
358
+ else if (a.startsWith("-")) {
359
+ log(`${c.red}✗${c.reset} Opción desconocida: ${a}`);
360
+ process.exit(1);
361
+ }
362
+ }
363
+ return out;
364
+ }
365
+
366
+ function usage() {
367
+ log(`Uso: komplian postman [opciones]`);
368
+ log(` ${c.dim}POSTMAN_API_KEY${c.reset} Obligatoria (Postman → Settings → API keys)`);
369
+ log(` ${c.dim}Dominio email${c.reset} Solo cuentas @komplian.com (verificado vía GET /me)`);
370
+ log(``);
371
+ log(` -y, --yes Sin confirmaciones interactivas`);
372
+ log(` --export-only Solo escribe JSON en disco (no llama a la API de Postman)`);
373
+ log(` --out <dir> Carpeta para export (por defecto: ./komplian-postman)`);
374
+ log(` -h, --help`);
375
+ log(``);
376
+ log(` Opcional: POSTMAN_WORKSPACE_ID, KOMPLIAN_EMAIL_DOMAIN (defecto: komplian.com)`);
377
+ }
378
+
379
+ export async function runPostman(argv) {
380
+ const args = parseArgs(argv);
381
+ if (args.help) {
382
+ usage();
383
+ return;
384
+ }
385
+
386
+ const apiKey = process.env.POSTMAN_API_KEY?.trim();
387
+ if (!apiKey) {
388
+ log(`${c.red}✗${c.reset} Falta ${c.bold}POSTMAN_API_KEY${c.reset} en el entorno.`);
389
+ log(
390
+ `${c.dim} Postman → Settings (avatar) → API keys → Generate.${c.reset}`
391
+ );
392
+ process.exit(1);
393
+ }
394
+
395
+ const domain = (process.env.KOMPLIAN_EMAIL_DOMAIN || "komplian.com").trim();
396
+ await verifyKomplianEmail(apiKey, domain);
397
+
398
+ const collection = buildCollection();
399
+ const envs = buildEnvironments();
400
+ const workspaceId = await pickWorkspaceId(
401
+ apiKey,
402
+ process.env.POSTMAN_WORKSPACE_ID
403
+ );
404
+ log(
405
+ `${c.green}✓${c.reset} Workspace Postman: ${c.bold}${workspaceId}${c.reset}`
406
+ );
407
+
408
+ const outBase = args.outDir
409
+ ? resolve(args.outDir)
410
+ : resolve(process.cwd(), "komplian-postman");
411
+ mkdirSync(outBase, { recursive: true });
412
+
413
+ const collPath = join(outBase, "Komplian-API.postman_collection.json");
414
+ writeFileSync(collPath, JSON.stringify(collection, null, 2), "utf8");
415
+ log(`${c.green}✓${c.reset} Export: ${c.dim}${collPath}${c.reset}`);
416
+
417
+ for (const env of envs) {
418
+ const safe = env.name.replace(/[\\/]/g, "-") + ".postman_environment.json";
419
+ writeFileSync(join(outBase, safe), JSON.stringify({ ...env }, null, 2), "utf8");
420
+ log(`${c.green}✓${c.reset} Export: ${c.dim}${join(outBase, safe)}${c.reset}`);
421
+ }
422
+
423
+ if (args.exportOnly) {
424
+ log("");
425
+ log(
426
+ `${c.cyan}━━ Listo (solo archivos) ━━${c.reset} Importa en Postman: ${c.bold}Import${c.reset} → arrastra la carpeta o los JSON.`
427
+ );
428
+ return;
429
+ }
430
+
431
+ log("");
432
+ log(`${c.cyan}━━ Subiendo a Postman (API) ━━${c.reset}`);
433
+
434
+ const cr = await createCollection(apiKey, workspaceId, collection);
435
+ if (!cr.ok) {
436
+ log(
437
+ `${c.yellow}○${c.reset} Colección API: ${cr.status} — ${JSON.stringify(cr.body).slice(0, 400)}`
438
+ );
439
+ log(
440
+ `${c.dim} Si el nombre ya existe, borra la colección antigua en Postman o importa el JSON exportado.${c.reset}`
441
+ );
442
+ } else {
443
+ const uid = cr.body?.collection?.uid || cr.body?.collection?.id || "?";
444
+ log(`${c.green}✓${c.reset} Colección creada en Postman (${uid})`);
445
+ }
446
+
447
+ for (const env of envs) {
448
+ const er = await createEnvironment(apiKey, workspaceId, {
449
+ name: env.name,
450
+ values: env.values,
451
+ });
452
+ if (!er.ok) {
453
+ log(
454
+ `${c.yellow}○${c.reset} Entorno "${env.name}": ${er.status} — ${JSON.stringify(er.body).slice(0, 200)}`
455
+ );
456
+ } else {
457
+ log(`${c.green}✓${c.reset} Entorno: ${c.bold}${env.name}${c.reset}`);
458
+ }
459
+ }
460
+
461
+ log("");
462
+ log(`${c.green}✓${c.reset} Abre Postman → workspace → revisa ${c.bold}Komplian API${c.reset} y los entornos.`);
463
+ log(
464
+ `${c.dim} Rellena apiKey / adminApiKey / IDs en el entorno (secret).${c.reset}`
465
+ );
466
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "komplian",
3
- "version": "0.3.7",
4
- "description": "Komplian developer workspace setup: GitHub CLI (browser login) + git clone by team. Node 18+, git, gh — no OAuth App to register.",
3
+ "version": "0.4.0",
4
+ "description": "Komplian developer workspace: GitHub clone (onboard) + Postman collection/environments (postman). Node 18+.",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=18"
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "files": [
13
13
  "komplian-onboard.mjs",
14
+ "komplian-postman.mjs",
14
15
  "komplian-team-repos.json",
15
16
  "README.md"
16
17
  ],
@@ -18,7 +19,7 @@
18
19
  "access": "public"
19
20
  },
20
21
  "scripts": {
21
- "prepublishOnly": "node --check komplian-onboard.mjs"
22
+ "prepublishOnly": "node --check komplian-onboard.mjs && node --check komplian-postman.mjs"
22
23
  },
23
24
  "keywords": [
24
25
  "komplian",