komplian 0.4.3 → 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
 
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "komplian",
3
- "version": "0.4.3",
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",