komplian 0.7.2 → 0.7.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/komplian-localhost.mjs +15 -8
- package/komplian-mcp-tools.mjs +12 -1
- package/komplian-onboard.mjs +271 -133
- package/komplian-setup.mjs +387 -73
- package/package.json +1 -1
package/komplian-localhost.mjs
CHANGED
|
@@ -519,12 +519,15 @@ export async function runLocalhost(argv) {
|
|
|
519
519
|
if (!cliQuiet()) {
|
|
520
520
|
log(`${c.cyan}━━ .env.local ━━${c.reset}`);
|
|
521
521
|
}
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
522
|
+
if (cliQuiet()) {
|
|
523
|
+
const labels = written.map((w) => w.label || "?").join(`${c.dim}·${c.reset} `);
|
|
524
|
+
logAlways(
|
|
525
|
+
` ${c.green}✓${c.reset} ${c.dim}.env.local${c.reset} ${labels}`
|
|
526
|
+
);
|
|
527
|
+
} else {
|
|
528
|
+
for (const w of written) {
|
|
529
|
+
const st = w.skipped ? `${c.dim}skip${c.reset}` : `${c.green}ok${c.reset}`;
|
|
530
|
+
const name = w.label || "?";
|
|
528
531
|
log(` ${st} ${name}`);
|
|
529
532
|
}
|
|
530
533
|
}
|
|
@@ -565,7 +568,7 @@ export async function runLocalhost(argv) {
|
|
|
565
568
|
});
|
|
566
569
|
|
|
567
570
|
if (cliQuiet()) {
|
|
568
|
-
logAlways(`${c.
|
|
571
|
+
logAlways(`${c.dim}dev servers · Ctrl+C stop${c.reset}`);
|
|
569
572
|
} else {
|
|
570
573
|
log(`${c.cyan}━━ dev servers (${services.length}) ━━${c.reset} ${c.dim}Ctrl+C stop${c.reset}`);
|
|
571
574
|
log("");
|
|
@@ -573,9 +576,13 @@ export async function runLocalhost(argv) {
|
|
|
573
576
|
|
|
574
577
|
const npx = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
575
578
|
const useShell = process.platform === "win32";
|
|
579
|
+
/** --raw avoids concurrently prefix logger bugs (e.g. prev.replace on Windows / npx). */
|
|
580
|
+
const concArgs = cliQuiet()
|
|
581
|
+
? ["--yes", "concurrently@9", "--raw", ...scripts]
|
|
582
|
+
: ["--yes", "concurrently@9", "-c", colors, "-n", names, ...scripts];
|
|
576
583
|
const child = spawn(
|
|
577
584
|
npx,
|
|
578
|
-
|
|
585
|
+
concArgs,
|
|
579
586
|
{
|
|
580
587
|
cwd: workspaceRoot,
|
|
581
588
|
stdio: "inherit",
|
package/komplian-mcp-tools.mjs
CHANGED
|
@@ -65,6 +65,15 @@ const KOMPLIAN_MCP_PRESET = {
|
|
|
65
65
|
args: ["-y", "chrome-devtools-mcp@latest"],
|
|
66
66
|
env: {},
|
|
67
67
|
},
|
|
68
|
+
/** Remote MCP — OAuth in Cursor (“Needs login”). */
|
|
69
|
+
"KOMPLIAN-vercel": {
|
|
70
|
+
url: "https://mcp.vercel.com",
|
|
71
|
+
},
|
|
72
|
+
/** Remote MCP — OAuth via mcp-remote (Neon docs). */
|
|
73
|
+
"KOMPLIAN-neon": {
|
|
74
|
+
command: "npx",
|
|
75
|
+
args: ["-y", "mcp-remote", "https://mcp.neon.tech/mcp"],
|
|
76
|
+
},
|
|
68
77
|
},
|
|
69
78
|
};
|
|
70
79
|
|
|
@@ -83,10 +92,12 @@ Enable each row in **Cursor → Settings → MCP**. Fill \`env\` per the table.
|
|
|
83
92
|
| **KOMPLIAN-sentry** | \`@sentry/mcp-server\` | Sentry | First time: browser login. **Komplian:** org \`komplian\`, \`regionUrl\` \`https://de.sentry.io\`, projects \`komplian-api\` / \`komplian-app\`. |
|
|
84
93
|
| **KOMPLIAN-stripe** | \`@stripe/mcp\` | Stripe API | \`STRIPE_SECRET_KEY\` (restricted / test in dev). [Stripe MCP docs](https://docs.stripe.com/mcp). |
|
|
85
94
|
| **KOMPLIAN-chrome-devtools** | \`chrome-devtools-mcp\` | Chrome (network, console, screenshots) | Needs **Node 20.19+** and stable Chrome. [Chrome DevTools MCP](https://developer.chrome.com/blog/chrome-devtools-mcp). |
|
|
95
|
+
| **KOMPLIAN-vercel** | \`url\` → \`https://mcp.vercel.com\` | Vercel projects & deploys | **OAuth** in Cursor (see [Vercel MCP](https://mcp.vercel.com/)). No token in JSON. |
|
|
96
|
+
| **KOMPLIAN-neon** | \`npx -y mcp-remote https://mcp.neon.tech/mcp\` | Neon Postgres / API | **OAuth** when the client starts (see [Neon MCP](https://neon.tech/docs/ai/neon-mcp-server)). Alt: local \`@neondatabase/mcp-server-neon\` + API key. |
|
|
86
97
|
|
|
87
98
|
## 2. Optional: Cursor native connectors
|
|
88
99
|
|
|
89
|
-
For Cursor OAuth/UI connectors: **Settings → MCP** → **Atlassian**, **Sentry**, **Stripe**, **Chrome DevTools**. They can coexist with **KOMPLIAN-*** entries; avoid duplicating the same capability twice.
|
|
100
|
+
For Cursor OAuth/UI connectors: **Settings → MCP** → **Atlassian**, **Sentry**, **Stripe**, **Chrome DevTools**, **Vercel**, **Neon**. They can coexist with **KOMPLIAN-*** entries; avoid duplicating the same capability twice.
|
|
90
101
|
|
|
91
102
|
## 3. Restart
|
|
92
103
|
|
package/komplian-onboard.mjs
CHANGED
|
@@ -46,31 +46,38 @@ function ux(s = "") {
|
|
|
46
46
|
console.log(s);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** Block-letter logo + box frame (OpenClaw-style table). */
|
|
49
50
|
function banner() {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
);
|
|
51
|
+
const art = [
|
|
52
|
+
"██╗ ██╗ ██████╗ ███╗ ███╗ ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗",
|
|
53
|
+
"██║ ██╔╝ ██╔═══██╗ ████╗ ████║ ██╔══██╗ ██║ ██║ ██╔══██╗ ████╗ ██║",
|
|
54
|
+
"█████╔╝ ██║ ██║ ██╔████╔██║ ██████╔╝ ██║ ██║ ███████║ ██╔██╗ ██║",
|
|
55
|
+
"██╔═██╗ ██║ ██║ ██║╚██╔╝██║ ██╔═══╝ ██║ ██║ ██╔══██║ ██║╚██╗██║",
|
|
56
|
+
"██║ ██╗ ╚██████╔╝ ██║ ╚═╝ ██║ ██║ ███████╗ ██║ ██║ ██║ ██║ ╚████║",
|
|
57
|
+
"╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═══╝",
|
|
58
|
+
];
|
|
59
|
+
const w = Math.max(...art.map((line) => [...line].length));
|
|
60
|
+
const pad = (line) => line + " ".repeat(w - [...line].length);
|
|
61
|
+
const horiz = "─".repeat(w + 2);
|
|
62
|
+
const b = `${c.cyan}${c.bold}`;
|
|
63
|
+
const x = c.reset;
|
|
64
|
+
ux(`${b}┌${horiz}┐${x}`);
|
|
65
|
+
for (const line of art) {
|
|
66
|
+
ux(`${b}│ ${pad(line)} │${x}`);
|
|
67
|
+
}
|
|
68
|
+
ux(`${b}└${horiz}┘${x}`);
|
|
69
|
+
ux(`${c.dim} Secure setup · GitHub CLI · Monorepo${c.reset}`);
|
|
70
|
+
ux("");
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
function
|
|
73
|
+
function startBatchSpinner(message) {
|
|
67
74
|
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
68
75
|
const cloud = `${c.blue}☁${c.reset}`;
|
|
69
76
|
let i = 0;
|
|
70
77
|
const id = setInterval(() => {
|
|
71
78
|
const fr = frames[i++ % frames.length];
|
|
72
79
|
process.stdout.write(
|
|
73
|
-
`\r ${cloud}
|
|
80
|
+
`\r ${cloud} ${c.cyan}${fr}${c.reset} ${c.dim}${message}${c.reset} `
|
|
74
81
|
);
|
|
75
82
|
}, 90);
|
|
76
83
|
return {
|
|
@@ -280,55 +287,27 @@ function isSafeTargetDir(abs) {
|
|
|
280
287
|
return !bad.includes(n);
|
|
281
288
|
}
|
|
282
289
|
|
|
283
|
-
function
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
return
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
if (status === "fail") {
|
|
292
|
-
return ` ${cloud} ${c.red}✗${c.reset} ${org}/${name} ${c.dim}failed${c.reset}`;
|
|
293
|
-
}
|
|
294
|
-
return ` ${cloud} ${c.cyan}…${c.reset} ${org}/${name} ${c.dim}cloning…${c.reset}`;
|
|
290
|
+
function summarizeCloneLine(repos, outcomes) {
|
|
291
|
+
const parts = repos.map((name) => {
|
|
292
|
+
const st = outcomes.get(name) || "fail";
|
|
293
|
+
if (st === "fail") return `${c.red}✗${c.reset} ${name}`;
|
|
294
|
+
if (st === "skip") return `${c.green}✓${c.reset} ${c.dim}${name}${c.reset}`;
|
|
295
|
+
return `${c.green}✓${c.reset} ${name}`;
|
|
296
|
+
});
|
|
297
|
+
return ` ${c.blue}☁${c.reset} ${parts.join(` `)}`;
|
|
295
298
|
}
|
|
296
299
|
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
ux(cloudLine(org, name, "skip"));
|
|
302
|
-
return true;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const args = useSsh
|
|
306
|
-
? ["clone", `git@github.com:${org}/${name}.git`, name]
|
|
307
|
-
: ["repo", "clone", `${org}/${name}`, name];
|
|
308
|
-
const cmd = useSsh ? "git" : "gh";
|
|
309
|
-
|
|
310
|
-
const tty = process.stdout.isTTY;
|
|
311
|
-
const spin = tty ? startRepoSpinner(name) : null;
|
|
312
|
-
if (!tty) {
|
|
313
|
-
ux(
|
|
314
|
-
`${c.blue}☁${c.reset} ${c.cyan}…${c.reset} ${c.bold}${name}${c.reset} ${c.dim}cloning…${c.reset}`
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const r = await runSpawn(cmd, args, workspace);
|
|
319
|
-
if (spin) spin.stop();
|
|
320
|
-
|
|
321
|
-
if (r.status === 0) {
|
|
322
|
-
ux(cloudLine(org, name, "ok"));
|
|
323
|
-
return true;
|
|
324
|
-
}
|
|
325
|
-
ux(cloudLine(org, name, "fail"));
|
|
326
|
-
const err = (r.stderr || "").trim();
|
|
327
|
-
if (err) {
|
|
328
|
-
if (q) console.error(`${c.dim}${err.slice(0, 500)}${c.reset}`);
|
|
329
|
-
else log(`${c.dim}${err}${c.reset}`);
|
|
300
|
+
function summarizeDepsLine(results) {
|
|
301
|
+
const ran = results.filter((r) => !r.skipped);
|
|
302
|
+
if (ran.length === 0) {
|
|
303
|
+
return ` ${c.blue}☁${c.reset} ${c.dim}dependencies — nothing to install${c.reset}`;
|
|
330
304
|
}
|
|
331
|
-
|
|
305
|
+
const parts = ran.map((r) =>
|
|
306
|
+
r.ok
|
|
307
|
+
? `${c.green}✓${c.reset} ${r.name}`
|
|
308
|
+
: `${c.yellow}○${c.reset} ${r.name}`
|
|
309
|
+
);
|
|
310
|
+
return ` ${c.blue}☁${c.reset} ${parts.join(` `)}`;
|
|
332
311
|
}
|
|
333
312
|
|
|
334
313
|
function copyCursorPack(workspace, cursorRepoUrl) {
|
|
@@ -371,12 +350,13 @@ function npmQuietFlags() {
|
|
|
371
350
|
return audit ? [] : ["--no-audit", "--no-fund"];
|
|
372
351
|
}
|
|
373
352
|
|
|
374
|
-
function npmInstallOneRepo(dir, name) {
|
|
353
|
+
function npmInstallOneRepo(dir, name, opts = {}) {
|
|
354
|
+
const silent = opts.silent === true;
|
|
375
355
|
const pkg = join(dir, "package.json");
|
|
376
356
|
if (!existsSync(pkg)) return { ok: true, skipped: true };
|
|
377
357
|
|
|
378
358
|
const q = process.env.KOMPLIAN_CLI_QUIET === "1";
|
|
379
|
-
const stdio = q ? "ignore" : "inherit";
|
|
359
|
+
const stdio = silent || q ? "ignore" : "inherit";
|
|
380
360
|
|
|
381
361
|
const yarnLock = join(dir, "yarn.lock");
|
|
382
362
|
const pnpmLock = join(dir, "pnpm-lock.yaml");
|
|
@@ -384,17 +364,21 @@ function npmInstallOneRepo(dir, name) {
|
|
|
384
364
|
|
|
385
365
|
if (existsSync(yarnLock)) {
|
|
386
366
|
if (!canRun("yarn", ["--version"])) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
367
|
+
if (!silent) {
|
|
368
|
+
log(
|
|
369
|
+
`${c.yellow}○${c.reset} ${name} ${c.dim}(yarn.lock; install yarn or run yarn install manually)${c.reset}`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
390
372
|
return { ok: true, skipped: true };
|
|
391
373
|
}
|
|
392
|
-
if (
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
374
|
+
if (!silent) {
|
|
375
|
+
if (q) {
|
|
376
|
+
ux(
|
|
377
|
+
` ${c.blue}☁${c.reset} ${c.cyan}…${c.reset} ${c.bold}${name}${c.reset} ${c.dim}yarn…${c.reset}`
|
|
378
|
+
);
|
|
379
|
+
} else {
|
|
380
|
+
log(`${c.dim}→${c.reset} ${name} ${c.dim}(yarn)${c.reset}`);
|
|
381
|
+
}
|
|
398
382
|
}
|
|
399
383
|
const r = spawnSync(
|
|
400
384
|
"yarn",
|
|
@@ -406,17 +390,21 @@ function npmInstallOneRepo(dir, name) {
|
|
|
406
390
|
|
|
407
391
|
if (existsSync(pnpmLock)) {
|
|
408
392
|
if (!canRun("pnpm", ["--version"])) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
393
|
+
if (!silent) {
|
|
394
|
+
log(
|
|
395
|
+
`${c.yellow}○${c.reset} ${name} ${c.dim}(pnpm-lock; install pnpm or run pnpm install manually)${c.reset}`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
412
398
|
return { ok: true, skipped: true };
|
|
413
399
|
}
|
|
414
|
-
if (
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
400
|
+
if (!silent) {
|
|
401
|
+
if (q) {
|
|
402
|
+
ux(
|
|
403
|
+
` ${c.blue}☁${c.reset} ${c.cyan}…${c.reset} ${c.bold}${name}${c.reset} ${c.dim}pnpm…${c.reset}`
|
|
404
|
+
);
|
|
405
|
+
} else {
|
|
406
|
+
log(`${c.dim}→${c.reset} ${name} ${c.dim}(pnpm)${c.reset}`);
|
|
407
|
+
}
|
|
420
408
|
}
|
|
421
409
|
const r = spawnSync(
|
|
422
410
|
"pnpm",
|
|
@@ -427,34 +415,42 @@ function npmInstallOneRepo(dir, name) {
|
|
|
427
415
|
}
|
|
428
416
|
|
|
429
417
|
if (!canRun("npm", ["--version"])) {
|
|
430
|
-
|
|
418
|
+
if (!silent) {
|
|
419
|
+
log(`${c.yellow}○${c.reset} npm not in PATH — skipping ${name}`);
|
|
420
|
+
}
|
|
431
421
|
return { ok: true, skipped: true };
|
|
432
422
|
}
|
|
433
423
|
|
|
434
424
|
const quiet = npmQuietFlags();
|
|
435
425
|
|
|
436
426
|
if (existsSync(npmLock)) {
|
|
437
|
-
if (
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
427
|
+
if (!silent) {
|
|
428
|
+
if (q) {
|
|
429
|
+
ux(
|
|
430
|
+
` ${c.blue}☁${c.reset} ${c.cyan}…${c.reset} ${c.bold}${name}${c.reset} ${c.dim}npm ci…${c.reset}`
|
|
431
|
+
);
|
|
432
|
+
} else {
|
|
433
|
+
log(`${c.dim}→${c.reset} ${name} ${c.dim}(npm ci)${c.reset}`);
|
|
434
|
+
}
|
|
443
435
|
}
|
|
444
436
|
const r = spawnSync("npm", ["ci", ...quiet], spawnWin({ cwd: dir, stdio }));
|
|
445
437
|
if (r.status === 0) return { ok: true, skipped: false };
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
438
|
+
if (!silent) {
|
|
439
|
+
log(
|
|
440
|
+
`${c.yellow}○${c.reset} ${name}: npm ci failed (lock out of sync?). ${c.dim}Try npm install in that repo.${c.reset}`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
449
443
|
return { ok: false, skipped: false };
|
|
450
444
|
}
|
|
451
445
|
|
|
452
|
-
if (
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
446
|
+
if (!silent) {
|
|
447
|
+
if (q) {
|
|
448
|
+
ux(
|
|
449
|
+
` ${c.blue}☁${c.reset} ${c.cyan}…${c.reset} ${c.bold}${name}${c.reset} ${c.dim}npm install…${c.reset}`
|
|
450
|
+
);
|
|
451
|
+
} else {
|
|
452
|
+
log(`${c.dim}→${c.reset} ${name} ${c.dim}(npm install — no new lockfile)${c.reset}`);
|
|
453
|
+
}
|
|
458
454
|
}
|
|
459
455
|
const r = spawnSync(
|
|
460
456
|
"npm",
|
|
@@ -464,33 +460,143 @@ function npmInstallOneRepo(dir, name) {
|
|
|
464
460
|
return { ok: r.status === 0, skipped: false };
|
|
465
461
|
}
|
|
466
462
|
|
|
467
|
-
function
|
|
463
|
+
function npmInstallSpawnAsync(cmd, args, cwd, stdio) {
|
|
464
|
+
return new Promise((resolve) => {
|
|
465
|
+
const child = spawn(cmd, args, spawnWin({ cwd, stdio }));
|
|
466
|
+
child.on("close", (code) => resolve(code === 0));
|
|
467
|
+
child.on("error", () => resolve(false));
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Same behavior as npmInstallOneRepo, non-blocking (parallel batch). */
|
|
472
|
+
async function npmInstallOneRepoAsync(dir, name, opts = {}) {
|
|
473
|
+
const silent = opts.silent === true;
|
|
474
|
+
const pkg = join(dir, "package.json");
|
|
475
|
+
if (!existsSync(pkg)) return { ok: true, skipped: true };
|
|
476
|
+
|
|
468
477
|
const q = process.env.KOMPLIAN_CLI_QUIET === "1";
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
478
|
+
const stdio = silent || q ? "ignore" : "inherit";
|
|
479
|
+
|
|
480
|
+
const yarnLock = join(dir, "yarn.lock");
|
|
481
|
+
const pnpmLock = join(dir, "pnpm-lock.yaml");
|
|
482
|
+
const npmLock = join(dir, "package-lock.json");
|
|
483
|
+
|
|
484
|
+
if (existsSync(yarnLock)) {
|
|
485
|
+
if (!canRun("yarn", ["--version"])) {
|
|
486
|
+
if (!silent) {
|
|
487
|
+
log(
|
|
488
|
+
`${c.yellow}○${c.reset} ${name} ${c.dim}(yarn.lock; install yarn or run yarn install manually)${c.reset}`
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
return { ok: true, skipped: true };
|
|
492
|
+
}
|
|
493
|
+
if (!silent) {
|
|
494
|
+
if (q) {
|
|
495
|
+
ux(
|
|
496
|
+
` ${c.blue}☁${c.reset} ${c.cyan}…${c.reset} ${c.bold}${name}${c.reset} ${c.dim}yarn…${c.reset}`
|
|
497
|
+
);
|
|
498
|
+
} else {
|
|
499
|
+
log(`${c.dim}→${c.reset} ${name} ${c.dim}(yarn)${c.reset}`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const ok = await npmInstallSpawnAsync(
|
|
503
|
+
"yarn",
|
|
504
|
+
["install", "--frozen-lockfile"],
|
|
505
|
+
dir,
|
|
506
|
+
stdio
|
|
507
|
+
);
|
|
508
|
+
return { ok, skipped: false };
|
|
475
509
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (!
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
510
|
+
|
|
511
|
+
if (existsSync(pnpmLock)) {
|
|
512
|
+
if (!canRun("pnpm", ["--version"])) {
|
|
513
|
+
if (!silent) {
|
|
514
|
+
log(
|
|
515
|
+
`${c.yellow}○${c.reset} ${name} ${c.dim}(pnpm-lock; install pnpm or run pnpm install manually)${c.reset}`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
return { ok: true, skipped: true };
|
|
519
|
+
}
|
|
520
|
+
if (!silent) {
|
|
521
|
+
if (q) {
|
|
522
|
+
ux(
|
|
523
|
+
` ${c.blue}☁${c.reset} ${c.cyan}…${c.reset} ${c.bold}${name}${c.reset} ${c.dim}pnpm…${c.reset}`
|
|
524
|
+
);
|
|
525
|
+
} else {
|
|
526
|
+
log(`${c.dim}→${c.reset} ${name} ${c.dim}(pnpm)${c.reset}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const ok = await npmInstallSpawnAsync(
|
|
530
|
+
"pnpm",
|
|
531
|
+
["install", "--frozen-lockfile"],
|
|
532
|
+
dir,
|
|
533
|
+
stdio
|
|
534
|
+
);
|
|
535
|
+
return { ok, skipped: false };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (!canRun("npm", ["--version"])) {
|
|
539
|
+
if (!silent) {
|
|
540
|
+
log(`${c.yellow}○${c.reset} npm not in PATH — skipping ${name}`);
|
|
541
|
+
}
|
|
542
|
+
return { ok: true, skipped: true };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const quiet = npmQuietFlags();
|
|
546
|
+
|
|
547
|
+
if (existsSync(npmLock)) {
|
|
548
|
+
if (!silent) {
|
|
549
|
+
if (q) {
|
|
550
|
+
ux(
|
|
551
|
+
` ${c.blue}☁${c.reset} ${c.cyan}…${c.reset} ${c.bold}${name}${c.reset} ${c.dim}npm ci…${c.reset}`
|
|
552
|
+
);
|
|
485
553
|
} else {
|
|
486
|
-
|
|
554
|
+
log(`${c.dim}→${c.reset} ${name} ${c.dim}(npm ci)${c.reset}`);
|
|
487
555
|
}
|
|
488
|
-
}
|
|
489
|
-
|
|
556
|
+
}
|
|
557
|
+
const ok = await npmInstallSpawnAsync("npm", ["ci", ...quiet], dir, stdio);
|
|
558
|
+
if (ok) return { ok: true, skipped: false };
|
|
559
|
+
if (!silent) {
|
|
560
|
+
log(
|
|
561
|
+
`${c.yellow}○${c.reset} ${name}: npm ci failed (lock out of sync?). ${c.dim}Try npm install in that repo.${c.reset}`
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
return { ok: false, skipped: false };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (!silent) {
|
|
568
|
+
if (q) {
|
|
569
|
+
ux(
|
|
570
|
+
` ${c.blue}☁${c.reset} ${c.cyan}…${c.reset} ${c.bold}${name}${c.reset} ${c.dim}npm install…${c.reset}`
|
|
571
|
+
);
|
|
490
572
|
} else {
|
|
491
|
-
log(`${c.
|
|
573
|
+
log(`${c.dim}→${c.reset} ${name} ${c.dim}(npm install — no new lockfile)${c.reset}`);
|
|
492
574
|
}
|
|
493
575
|
}
|
|
576
|
+
const ok = await npmInstallSpawnAsync(
|
|
577
|
+
"npm",
|
|
578
|
+
["install", ...quiet, "--no-package-lock"],
|
|
579
|
+
dir,
|
|
580
|
+
stdio
|
|
581
|
+
);
|
|
582
|
+
return { ok, skipped: false };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** Silent installs in parallel (faster than sequential). */
|
|
586
|
+
async function npmInstallBatchAsync(workspace) {
|
|
587
|
+
const jobs = [];
|
|
588
|
+
for (const ent of readdirSync(workspace)) {
|
|
589
|
+
const d = join(workspace, ent);
|
|
590
|
+
if (!statSync(d).isDirectory()) continue;
|
|
591
|
+
jobs.push(
|
|
592
|
+
npmInstallOneRepoAsync(d, ent, { silent: true }).then(({ ok, skipped }) => ({
|
|
593
|
+
name: ent,
|
|
594
|
+
ok,
|
|
595
|
+
skipped,
|
|
596
|
+
}))
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
return Promise.all(jobs);
|
|
494
600
|
}
|
|
495
601
|
|
|
496
602
|
function usage() {
|
|
@@ -721,36 +827,68 @@ async function main() {
|
|
|
721
827
|
mkdirSync(abs, { recursive: true });
|
|
722
828
|
const q = process.env.KOMPLIAN_CLI_QUIET === "1";
|
|
723
829
|
ux("");
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
if (team) log(`${c.cyan}━━ Team ━━${c.reset} ${c.bold}${team}${c.reset}`);
|
|
728
|
-
log(`${c.cyan}━━ Repos (${repos.length}) ━━${c.reset}`);
|
|
729
|
-
} else {
|
|
730
|
-
ux(`${c.dim}Workspace:${c.reset} ${abs}`);
|
|
731
|
-
if (team) ux(`${c.dim}Team:${c.reset} ${team}`);
|
|
732
|
-
ux(`${c.cyan}Repositories${c.reset} ${c.dim}(${repos.length})${c.reset}`);
|
|
733
|
-
}
|
|
830
|
+
ux(`${c.dim}${abs}${c.reset}`);
|
|
831
|
+
if (team) ux(`${c.dim}team · ${team}${c.reset}`);
|
|
832
|
+
ux(`${c.dim}${repos.join(" · ")}${c.reset}`);
|
|
734
833
|
ux("");
|
|
735
834
|
|
|
835
|
+
const outcomes = new Map();
|
|
836
|
+
let spin = process.stdout.isTTY ? startBatchSpinner("Cloning repositories…") : null;
|
|
837
|
+
if (!spin) {
|
|
838
|
+
ux(` ${c.blue}☁${c.reset} ${c.dim}Cloning repositories…${c.reset}`);
|
|
839
|
+
}
|
|
840
|
+
|
|
736
841
|
let failed = 0;
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
842
|
+
try {
|
|
843
|
+
for (const name of repos) {
|
|
844
|
+
const gitDir = join(abs, name, ".git");
|
|
845
|
+
if (existsSync(gitDir)) {
|
|
846
|
+
outcomes.set(name, "skip");
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
const cloneArgs = args.ssh
|
|
850
|
+
? ["clone", `git@github.com:${org}/${name}.git`, name]
|
|
851
|
+
: ["repo", "clone", `${org}/${name}`, name];
|
|
852
|
+
const cmd = args.ssh ? "git" : "gh";
|
|
853
|
+
const r = await runSpawn(cmd, cloneArgs, abs);
|
|
854
|
+
if (r.status === 0) {
|
|
855
|
+
outcomes.set(name, "ok");
|
|
856
|
+
} else {
|
|
857
|
+
outcomes.set(name, "fail");
|
|
858
|
+
failed += 1;
|
|
859
|
+
const err = (r.stderr || "").trim();
|
|
860
|
+
if (err) console.error(`${c.dim}${err.slice(0, 500)}${c.reset}`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
} finally {
|
|
864
|
+
if (spin) spin.stop();
|
|
740
865
|
}
|
|
741
866
|
|
|
867
|
+
ux(summarizeCloneLine(repos, outcomes));
|
|
868
|
+
|
|
742
869
|
copyCursorPack(abs, process.env.KOMPLIAN_CURSOR_REPO);
|
|
743
870
|
|
|
744
871
|
if (args.install) {
|
|
745
|
-
|
|
872
|
+
spin = process.stdout.isTTY ? startBatchSpinner("Installing dependencies…") : null;
|
|
873
|
+
if (!spin) {
|
|
874
|
+
ux(` ${c.blue}☁${c.reset} ${c.dim}Installing dependencies…${c.reset}`);
|
|
875
|
+
}
|
|
876
|
+
let depResults = [];
|
|
877
|
+
try {
|
|
878
|
+
depResults = await npmInstallBatchAsync(abs);
|
|
879
|
+
} finally {
|
|
880
|
+
if (spin) spin.stop();
|
|
881
|
+
}
|
|
882
|
+
ux(summarizeDepsLine(depResults));
|
|
746
883
|
}
|
|
747
884
|
|
|
748
885
|
ux("");
|
|
749
|
-
ux(`${c.cyan}━━ Done ━━${c.reset}`);
|
|
750
886
|
if (failed > 0) {
|
|
751
887
|
ux(
|
|
752
888
|
`${c.yellow}○${c.reset} ${failed} repo(s) failed — check access and retry.`
|
|
753
889
|
);
|
|
890
|
+
} else {
|
|
891
|
+
ux(`${c.green}✓${c.reset} ${c.dim}Repositories ready${c.reset}`);
|
|
754
892
|
}
|
|
755
893
|
ux(`${c.green}✓${c.reset} Open in Cursor: ${c.bold}File → Open Folder → ${abs}${c.reset}`);
|
|
756
894
|
if (!q) {
|
package/komplian-setup.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
9
9
|
import { createServer } from "node:http";
|
|
10
10
|
import { createHash, randomBytes } from "node:crypto";
|
|
11
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
12
12
|
import { dirname, join, resolve } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { homedir } from "node:os";
|
|
@@ -75,6 +75,41 @@ function neonOAuthEnvReady() {
|
|
|
75
75
|
return !!(id && sec && redir && a && b && w);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/** Project IDs for Neon Management API (API key or OAuth token). */
|
|
79
|
+
function neonProjectIdsTriplet() {
|
|
80
|
+
const app =
|
|
81
|
+
process.env.KOMPLIAN_NEON_PROJECT_ID_APP?.trim() ||
|
|
82
|
+
process.env.KOMPLIAN_NEON_OAUTH_PROJECT_ID_APP?.trim();
|
|
83
|
+
const admin =
|
|
84
|
+
process.env.KOMPLIAN_NEON_PROJECT_ID_ADMIN?.trim() ||
|
|
85
|
+
process.env.KOMPLIAN_NEON_OAUTH_PROJECT_ID_ADMIN?.trim();
|
|
86
|
+
const web =
|
|
87
|
+
process.env.KOMPLIAN_NEON_PROJECT_ID_WEB?.trim() ||
|
|
88
|
+
process.env.KOMPLIAN_NEON_OAUTH_PROJECT_ID_WEB?.trim();
|
|
89
|
+
return { app, admin, web };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function neonApiKeyResolveReady() {
|
|
93
|
+
const { app, admin, web } = neonProjectIdsTriplet();
|
|
94
|
+
return !!(app && admin && web);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function neonBuildTripletFromApiKey(apiKey) {
|
|
98
|
+
const { app, admin, web } = neonProjectIdsTriplet();
|
|
99
|
+
if (!app || !admin || !web) {
|
|
100
|
+
return { ok: false, error: "Neon project IDs are not configured in the environment." };
|
|
101
|
+
}
|
|
102
|
+
const [ra, rb, rw] = await Promise.all([
|
|
103
|
+
neonFetchConnectionUri(apiKey, app),
|
|
104
|
+
neonFetchConnectionUri(apiKey, admin),
|
|
105
|
+
neonFetchConnectionUri(apiKey, web),
|
|
106
|
+
]);
|
|
107
|
+
if (!ra.ok) return { ok: false, error: ra.error };
|
|
108
|
+
if (!rb.ok) return { ok: false, error: rb.error };
|
|
109
|
+
if (!rw.ok) return { ok: false, error: rw.error };
|
|
110
|
+
return { ok: true, db: { app: ra.uri, admin: rb.uri, web: rw.uri } };
|
|
111
|
+
}
|
|
112
|
+
|
|
78
113
|
function parseListenFromRedirectUri() {
|
|
79
114
|
const u = process.env.KOMPLIAN_NEON_OAUTH_REDIRECT_URI?.trim();
|
|
80
115
|
if (!u) return null;
|
|
@@ -200,74 +235,225 @@ function openBrowserSync(url) {
|
|
|
200
235
|
}
|
|
201
236
|
}
|
|
202
237
|
|
|
203
|
-
const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="132" height="28" viewBox="0 0 132 28" fill="none" aria-hidden="true"><text x="0" y="20" fill="#fafafa" font-family="system-ui,-apple-system,sans-serif" font-size="18" font-weight="600">Komplian</text><circle cx="124" cy="14" r="4" fill="#22c55e"/></svg>`;
|
|
204
|
-
|
|
205
238
|
function buildSetupPageHtml(token, opts) {
|
|
206
|
-
const {
|
|
207
|
-
|
|
239
|
+
const {
|
|
240
|
+
needsPostmanKey,
|
|
241
|
+
needsDbUrls,
|
|
242
|
+
emailDomain,
|
|
243
|
+
neonOAuth,
|
|
244
|
+
neonApiResolve,
|
|
245
|
+
showPostman,
|
|
246
|
+
logoHtml = "",
|
|
247
|
+
} = opts;
|
|
248
|
+
const safeDomain = String(emailDomain || "komplian.com").replace(/[<>&"]/g, "");
|
|
249
|
+
|
|
250
|
+
const postmanSection = showPostman
|
|
208
251
|
? `<section class="card">
|
|
209
|
-
<
|
|
210
|
-
<
|
|
211
|
-
<
|
|
212
|
-
<
|
|
252
|
+
<h2 class="card-title">Postman</h2>
|
|
253
|
+
<p class="fine">${needsPostmanKey ? `Add your API key once (account must match <strong>@${safeDomain}</strong>). Postman has no OAuth for the API — open their site and paste the key.` : `A key is already saved on this machine. Paste below only if you want to replace it.`}</p>
|
|
254
|
+
<a class="btn secondary" href="https://go.postman.co/settings/me/api-keys" target="_blank" rel="noopener">Open Postman · API keys</a>
|
|
255
|
+
<label for="postman_api_key">API key${needsPostmanKey ? " · required" : " · optional"}</label>
|
|
256
|
+
<input type="password" id="postman_api_key" autocomplete="off" placeholder="PMAK-…" ${needsPostmanKey ? "required" : ""} />
|
|
213
257
|
</section>`
|
|
214
258
|
: "";
|
|
215
259
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
260
|
+
let neonSection = "";
|
|
261
|
+
if (needsDbUrls) {
|
|
262
|
+
const oauthBlock =
|
|
263
|
+
neonOAuth &&
|
|
264
|
+
`<div class="stack">
|
|
265
|
+
<p class="fine">Sign in with Neon (OAuth). A browser tab will open; return here when it says linked.</p>
|
|
266
|
+
<button type="button" class="btn secondary" id="neon_oauth">Connect Neon</button>
|
|
267
|
+
<p id="neon_status" class="ok hidden">Neon linked — URLs loaded.</p>
|
|
268
|
+
</div>`;
|
|
269
|
+
const apiKeyBlock =
|
|
270
|
+
!neonOAuth &&
|
|
271
|
+
neonApiResolve &&
|
|
272
|
+
`<div class="stack">
|
|
273
|
+
<p class="fine">Paste your <a href="https://console.neon.tech/app/settings/api-keys" target="_blank" rel="noopener">Neon API key</a>. Project IDs are read from the CLI environment (KOMPLIAN_NEON_PROJECT_ID_* or OAUTH_* IDs).</p>
|
|
274
|
+
<label for="neon_api_key">Neon API key</label>
|
|
275
|
+
<input type="password" id="neon_api_key" autocomplete="off" placeholder="napi_…" />
|
|
276
|
+
<button type="button" class="btn secondary" id="neon_load">Load connection strings</button>
|
|
277
|
+
<p id="neon_key_status" class="ok hidden">URLs loaded from Neon.</p>
|
|
278
|
+
</div>`;
|
|
279
|
+
const fallbackBlock =
|
|
280
|
+
!neonOAuth &&
|
|
281
|
+
!neonApiResolve &&
|
|
282
|
+
`<p class="fine">Set <code>KOMPLIAN_NEON_PROJECT_ID_APP</code> (and ADMIN, WEB) in the environment to enable one-click fetch, or paste Postgres URLs below. <a href="https://console.neon.tech" target="_blank" rel="noopener">Neon Console</a></p>`;
|
|
283
|
+
|
|
284
|
+
const dbFields = `<label for="db_app">APP + API</label><textarea id="db_app" rows="2" spellcheck="false" autocomplete="off" placeholder="postgresql://…"></textarea>
|
|
285
|
+
<label for="db_admin">ADMIN</label><textarea id="db_admin" rows="2" spellcheck="false" autocomplete="off" placeholder="postgresql://…"></textarea>
|
|
286
|
+
<label for="db_web">WEB</label><textarea id="db_web" rows="2" spellcheck="false" autocomplete="off" placeholder="postgresql://…"></textarea>`;
|
|
287
|
+
|
|
288
|
+
neonSection = `<section class="card">
|
|
289
|
+
<h2 class="card-title">Neon · databases</h2>
|
|
290
|
+
${oauthBlock || ""}
|
|
291
|
+
${apiKeyBlock || ""}
|
|
292
|
+
${fallbackBlock || ""}
|
|
293
|
+
<details class="manual" open>
|
|
294
|
+
<summary>Or paste connection strings</summary>
|
|
295
|
+
<div class="details-body">${dbFields}</div>
|
|
296
|
+
</details>
|
|
297
|
+
</section>`;
|
|
298
|
+
}
|
|
231
299
|
|
|
232
300
|
return `<!DOCTYPE html>
|
|
233
301
|
<html lang="en">
|
|
234
302
|
<head>
|
|
235
303
|
<meta charset="utf-8" />
|
|
236
304
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
237
|
-
<title>Komplian</title>
|
|
305
|
+
<title>Komplian — local setup</title>
|
|
306
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
307
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
308
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
|
238
309
|
<style>
|
|
239
|
-
:root {
|
|
310
|
+
:root {
|
|
311
|
+
--bg: #0a0a0a;
|
|
312
|
+
--fg: #fafafa;
|
|
313
|
+
--muted: #a3a3a3;
|
|
314
|
+
--line: #262626;
|
|
315
|
+
--card: #111111;
|
|
316
|
+
--accent: #22c55e;
|
|
317
|
+
--accent-dim: rgba(34, 197, 94, 0.15);
|
|
318
|
+
}
|
|
240
319
|
* { box-sizing: border-box; }
|
|
241
|
-
body {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
.
|
|
250
|
-
.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
.
|
|
320
|
+
body {
|
|
321
|
+
margin: 0; min-height: 100vh;
|
|
322
|
+
font-family: Inter, system-ui, -apple-system, sans-serif;
|
|
323
|
+
background: var(--bg); color: var(--fg);
|
|
324
|
+
display: flex; align-items: center; justify-content: center;
|
|
325
|
+
padding: 1.5rem;
|
|
326
|
+
-webkit-font-smoothing: antialiased;
|
|
327
|
+
}
|
|
328
|
+
.wrap { width: 100%; max-width: 420px; position: relative; }
|
|
329
|
+
.brand {
|
|
330
|
+
display: flex; align-items: center; gap: 10px;
|
|
331
|
+
margin-bottom: 1.75rem; min-height: 32px;
|
|
332
|
+
}
|
|
333
|
+
.brand-logo { display: flex; align-items: center; max-width: 152px; }
|
|
334
|
+
.brand-logo :is(svg) { width: 100%; height: auto; display: block; }
|
|
335
|
+
.brand-word {
|
|
336
|
+
font-size: 1.5rem; font-weight: 600; letter-spacing: -0.04em; color: var(--fg);
|
|
337
|
+
}
|
|
338
|
+
.brand-dot {
|
|
339
|
+
width: 9px; height: 9px; border-radius: 50%;
|
|
340
|
+
background: var(--accent);
|
|
341
|
+
box-shadow: 0 0 14px rgba(34, 197, 94, 0.5);
|
|
342
|
+
}
|
|
343
|
+
.card {
|
|
344
|
+
background: var(--card);
|
|
345
|
+
border: 1px solid var(--line);
|
|
346
|
+
border-radius: 12px;
|
|
347
|
+
padding: 1.25rem 1.35rem;
|
|
348
|
+
margin-bottom: 1rem;
|
|
349
|
+
}
|
|
350
|
+
.card-title {
|
|
351
|
+
margin: 0 0 0.75rem;
|
|
352
|
+
font-size: 0.8rem; font-weight: 600;
|
|
353
|
+
letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted);
|
|
354
|
+
}
|
|
355
|
+
.stack { margin-bottom: 1rem; }
|
|
356
|
+
.stack:last-child { margin-bottom: 0; }
|
|
357
|
+
label {
|
|
358
|
+
display: block;
|
|
359
|
+
font-size: 0.7rem; font-weight: 500;
|
|
360
|
+
letter-spacing: 0.06em; text-transform: uppercase;
|
|
361
|
+
color: var(--muted); margin: 1rem 0 0.45rem;
|
|
362
|
+
}
|
|
363
|
+
input, textarea {
|
|
364
|
+
width: 100%;
|
|
365
|
+
padding: 0.65rem 0.8rem;
|
|
366
|
+
border-radius: 8px;
|
|
367
|
+
border: 1px solid var(--line);
|
|
368
|
+
background: var(--bg);
|
|
369
|
+
color: var(--fg);
|
|
370
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
371
|
+
font-size: 0.8125rem;
|
|
372
|
+
}
|
|
373
|
+
input:focus, textarea:focus {
|
|
374
|
+
outline: none;
|
|
375
|
+
border-color: #404040;
|
|
376
|
+
box-shadow: 0 0 0 3px var(--accent-dim);
|
|
377
|
+
}
|
|
378
|
+
.btn {
|
|
379
|
+
display: block; width: 100%;
|
|
380
|
+
margin-top: 0.65rem;
|
|
381
|
+
padding: 0.78rem 1rem;
|
|
382
|
+
border: none; border-radius: 8px;
|
|
383
|
+
font-family: inherit; font-weight: 600; font-size: 0.9rem;
|
|
384
|
+
cursor: pointer; text-align: center; text-decoration: none;
|
|
385
|
+
transition: opacity 0.15s, transform 0.1s;
|
|
386
|
+
}
|
|
387
|
+
.btn:active { transform: scale(0.99); }
|
|
388
|
+
.btn.primary { background: var(--fg); color: var(--bg); }
|
|
389
|
+
.btn.primary:hover { opacity: 0.92; }
|
|
390
|
+
.btn.secondary {
|
|
391
|
+
background: transparent; color: var(--fg);
|
|
392
|
+
border: 1px solid var(--line);
|
|
393
|
+
}
|
|
394
|
+
.btn.secondary:hover { border-color: #404040; background: rgba(255,255,255,0.03); }
|
|
395
|
+
.btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
|
|
396
|
+
.fine { font-size: 0.8125rem; color: var(--muted); line-height: 1.5; margin: 0 0 0.75rem; }
|
|
397
|
+
.fine a { color: var(--fg); text-decoration: underline; text-underline-offset: 3px; }
|
|
398
|
+
.fine code { font-size: 0.75rem; background: var(--bg); padding: 0.1rem 0.35rem; border-radius: 4px; }
|
|
399
|
+
.ok { color: var(--accent); font-size: 0.8125rem; margin: 0.5rem 0 0; font-weight: 500; }
|
|
400
|
+
.manual { margin-top: 1rem; border-top: 1px solid var(--line); padding-top: 1rem; }
|
|
401
|
+
.manual summary {
|
|
402
|
+
cursor: pointer; font-size: 0.8rem; font-weight: 500; color: var(--muted);
|
|
403
|
+
list-style: none;
|
|
404
|
+
}
|
|
405
|
+
.manual summary::-webkit-details-marker { display: none; }
|
|
406
|
+
.details-body { margin-top: 0.75rem; }
|
|
407
|
+
.err { color: #f87171; font-size: 0.8125rem; margin: 0.75rem 0 0; display: none; line-height: 1.4; }
|
|
408
|
+
.hidden { display: none !important; }
|
|
409
|
+
#overlay {
|
|
410
|
+
position: fixed; inset: 0; background: rgba(0,0,0,0.65);
|
|
411
|
+
display: none; align-items: center; justify-content: center;
|
|
412
|
+
z-index: 50; backdrop-filter: blur(4px);
|
|
413
|
+
}
|
|
414
|
+
#overlay.show { display: flex; }
|
|
415
|
+
.loader {
|
|
416
|
+
display: flex; flex-direction: column; align-items: center; gap: 1rem;
|
|
417
|
+
padding: 2rem; border-radius: 12px; background: var(--card); border: 1px solid var(--line);
|
|
418
|
+
}
|
|
419
|
+
.spinner {
|
|
420
|
+
width: 36px; height: 36px;
|
|
421
|
+
border: 3px solid var(--line);
|
|
422
|
+
border-top-color: var(--accent);
|
|
423
|
+
border-radius: 50%;
|
|
424
|
+
animation: spin 0.7s linear infinite;
|
|
425
|
+
}
|
|
426
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
427
|
+
.loader p { margin: 0; font-size: 0.875rem; color: var(--muted); }
|
|
255
428
|
</style>
|
|
256
429
|
</head>
|
|
257
430
|
<body>
|
|
431
|
+
<div id="overlay" aria-live="polite"><div class="loader"><div class="spinner"></div><p>Securing your session…</p></div></div>
|
|
258
432
|
<div class="wrap">
|
|
259
|
-
<
|
|
433
|
+
<header class="brand" aria-label="Komplian">
|
|
434
|
+
${
|
|
435
|
+
logoHtml
|
|
436
|
+
? `<div class="brand-logo">${logoHtml}</div>`
|
|
437
|
+
: `<span class="brand-word">Komplian</span><span class="brand-dot" aria-hidden="true"></span>`
|
|
438
|
+
}
|
|
439
|
+
</header>
|
|
260
440
|
${postmanSection}
|
|
261
|
-
${
|
|
262
|
-
|
|
263
|
-
<div id="err" class="err"></div>
|
|
441
|
+
${neonSection}
|
|
442
|
+
<div id="err" class="err" role="alert"></div>
|
|
264
443
|
<button type="button" class="btn primary" id="go">Continue</button>
|
|
265
444
|
</div>
|
|
266
445
|
<script>
|
|
267
446
|
const TOKEN = ${JSON.stringify(token)};
|
|
268
447
|
const needsPostman = ${JSON.stringify(needsPostmanKey)};
|
|
448
|
+
const showPostman = ${JSON.stringify(!!showPostman)};
|
|
269
449
|
const needsDb = ${JSON.stringify(needsDbUrls)};
|
|
270
450
|
const neonOAuth = ${JSON.stringify(!!neonOAuth)};
|
|
451
|
+
function setLoading(on, msg) {
|
|
452
|
+
const o = document.getElementById("overlay");
|
|
453
|
+
o.classList.toggle("show", on);
|
|
454
|
+
const p = o.querySelector(".loader p");
|
|
455
|
+
if (p && msg) p.textContent = msg;
|
|
456
|
+
}
|
|
271
457
|
function showErr(t) {
|
|
272
458
|
const e = document.getElementById("err");
|
|
273
459
|
e.textContent = t;
|
|
@@ -293,20 +479,57 @@ function buildSetupPageHtml(token, opts) {
|
|
|
293
479
|
}, 900);
|
|
294
480
|
document.getElementById("neon_oauth")?.addEventListener("click", () => { location.href = "/neon/start"; });
|
|
295
481
|
}
|
|
482
|
+
document.getElementById("neon_load")?.addEventListener("click", async () => {
|
|
483
|
+
document.getElementById("err").style.display = "none";
|
|
484
|
+
const k = (document.getElementById("neon_api_key")||{}).value?.trim() || "";
|
|
485
|
+
if (!k) { showErr("Paste your Neon API key."); return; }
|
|
486
|
+
setLoading(true, "Fetching Neon connection strings…");
|
|
487
|
+
try {
|
|
488
|
+
const r = await fetch("/neon/resolve-key", { method:"POST", headers:{ "Content-Type":"application/json" }, body: JSON.stringify({ token: TOKEN, neon_api_key: k }) });
|
|
489
|
+
const j = await r.json().catch(() => ({}));
|
|
490
|
+
if (!r.ok || !j.ok) { showErr(j.error || "Neon API error."); return; }
|
|
491
|
+
document.getElementById("neon_key_status")?.classList.remove("hidden");
|
|
492
|
+
if (j.db) {
|
|
493
|
+
const a = document.getElementById("db_app");
|
|
494
|
+
const b = document.getElementById("db_admin");
|
|
495
|
+
const w = document.getElementById("db_web");
|
|
496
|
+
if (a) a.value = j.db.app || "";
|
|
497
|
+
if (b) b.value = j.db.admin || "";
|
|
498
|
+
if (w) w.value = j.db.web || "";
|
|
499
|
+
}
|
|
500
|
+
} finally { setLoading(false, "Securing your session…"); }
|
|
501
|
+
});
|
|
296
502
|
document.getElementById("go").onclick = async () => {
|
|
297
503
|
document.getElementById("err").style.display = "none";
|
|
298
504
|
const body = { token: TOKEN };
|
|
299
|
-
|
|
505
|
+
const pmEl = document.getElementById("postman_api_key");
|
|
506
|
+
if (showPostman) body.postman_api_key = (pmEl && pmEl.value) ? pmEl.value.trim() : "";
|
|
300
507
|
if (needsDb) {
|
|
301
508
|
body.db_app = (document.getElementById("db_app")||{}).value || "";
|
|
302
509
|
body.db_admin = (document.getElementById("db_admin")||{}).value || "";
|
|
303
510
|
body.db_web = (document.getElementById("db_web")||{}).value || "";
|
|
511
|
+
const nk = (document.getElementById("neon_api_key")||{}).value?.trim() || "";
|
|
512
|
+
if (nk) body.neon_api_key = nk;
|
|
304
513
|
}
|
|
305
514
|
const btn = document.getElementById("go");
|
|
515
|
+
if (needsPostman && !body.postman_api_key) {
|
|
516
|
+
showErr("Postman API key is required.");
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
306
519
|
btn.disabled = true;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
520
|
+
setLoading(true, "Validating and saving…");
|
|
521
|
+
try {
|
|
522
|
+
if (await submitBody(body)) {
|
|
523
|
+
setLoading(true, "Done — closing…");
|
|
524
|
+
setTimeout(() => { try { window.close(); } catch(_){} window.location.href = "about:blank"; }, 500);
|
|
525
|
+
} else {
|
|
526
|
+
btn.disabled = false;
|
|
527
|
+
setLoading(false);
|
|
528
|
+
}
|
|
529
|
+
} catch (_) {
|
|
530
|
+
btn.disabled = false;
|
|
531
|
+
setLoading(false);
|
|
532
|
+
}
|
|
310
533
|
};
|
|
311
534
|
</script>
|
|
312
535
|
</body>
|
|
@@ -319,6 +542,9 @@ function runSetupBrowserForm(opts) {
|
|
|
319
542
|
needsDbUrls,
|
|
320
543
|
emailDomain,
|
|
321
544
|
neonOAuth,
|
|
545
|
+
neonApiResolve,
|
|
546
|
+
showPostman,
|
|
547
|
+
logoHtml = "",
|
|
322
548
|
listenHost,
|
|
323
549
|
listenPort,
|
|
324
550
|
timeoutMs = 600_000,
|
|
@@ -344,6 +570,50 @@ function runSetupBrowserForm(opts) {
|
|
|
344
570
|
return;
|
|
345
571
|
}
|
|
346
572
|
|
|
573
|
+
if (req.method === "POST" && url.pathname === "/neon/resolve-key") {
|
|
574
|
+
let raw = "";
|
|
575
|
+
for await (const ch of req) raw += ch;
|
|
576
|
+
let data;
|
|
577
|
+
try {
|
|
578
|
+
data = JSON.parse(raw || "{}");
|
|
579
|
+
} catch {
|
|
580
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
581
|
+
res.end(JSON.stringify({ ok: false, error: "Bad JSON." }));
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (data.token !== token) {
|
|
585
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
586
|
+
res.end(JSON.stringify({ ok: false, error: "Bad token." }));
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (!neonApiKeyResolveReady()) {
|
|
590
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
591
|
+
res.end(
|
|
592
|
+
JSON.stringify({
|
|
593
|
+
ok: false,
|
|
594
|
+
error: "Set KOMPLIAN_NEON_PROJECT_ID_APP, _ADMIN, and _WEB (or OAuth project ID env vars).",
|
|
595
|
+
})
|
|
596
|
+
);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const apiKey = String(data.neon_api_key || "").trim();
|
|
600
|
+
if (!apiKey) {
|
|
601
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
602
|
+
res.end(JSON.stringify({ ok: false, error: "Neon API key required." }));
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const trip = await neonBuildTripletFromApiKey(apiKey);
|
|
606
|
+
if (!trip.ok) {
|
|
607
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
608
|
+
res.end(JSON.stringify({ ok: false, error: trip.error || "Neon API error." }));
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
ctx.neonTriplet = trip.db;
|
|
612
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
613
|
+
res.end(JSON.stringify({ ok: true, db: trip.db }));
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
|
|
347
617
|
if (req.method === "GET" && url.pathname === "/neon/start" && neonOAuth) {
|
|
348
618
|
const { verifier, challenge } = newPkce();
|
|
349
619
|
const state = b64url(randomBytes(16));
|
|
@@ -410,6 +680,9 @@ function runSetupBrowserForm(opts) {
|
|
|
410
680
|
needsDbUrls,
|
|
411
681
|
emailDomain,
|
|
412
682
|
neonOAuth,
|
|
683
|
+
neonApiResolve: !!neonApiResolve,
|
|
684
|
+
showPostman: !!showPostman,
|
|
685
|
+
logoHtml,
|
|
413
686
|
})
|
|
414
687
|
);
|
|
415
688
|
return;
|
|
@@ -436,40 +709,72 @@ function runSetupBrowserForm(opts) {
|
|
|
436
709
|
|
|
437
710
|
if (needsPostmanKey) {
|
|
438
711
|
const pk = String(data.postman_api_key || "").trim();
|
|
439
|
-
if (pk) {
|
|
440
|
-
|
|
712
|
+
if (!pk) {
|
|
713
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
714
|
+
res.end(JSON.stringify({ ok: false, error: "Postman API key required." }));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const v = await validatePostmanApiKeyForKomplian(pk, emailDomain);
|
|
718
|
+
if (!v.ok) {
|
|
719
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
720
|
+
res.end(JSON.stringify({ ok: false, error: v.error || "Invalid Postman key." }));
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
out.postmanKey = pk;
|
|
724
|
+
} else {
|
|
725
|
+
const pkOpt = String(data.postman_api_key || "").trim();
|
|
726
|
+
if (pkOpt) {
|
|
727
|
+
const v = await validatePostmanApiKeyForKomplian(pkOpt, emailDomain);
|
|
441
728
|
if (!v.ok) {
|
|
442
729
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
443
730
|
res.end(JSON.stringify({ ok: false, error: v.error || "Invalid Postman key." }));
|
|
444
731
|
return;
|
|
445
732
|
}
|
|
446
|
-
out.postmanKey =
|
|
733
|
+
out.postmanKey = pkOpt;
|
|
447
734
|
}
|
|
448
735
|
}
|
|
449
736
|
|
|
450
737
|
if (needsDbUrls) {
|
|
738
|
+
let dbOut = null;
|
|
451
739
|
if (ctx.neonTriplet && isValidTriplet(ctx.neonTriplet)) {
|
|
452
|
-
|
|
740
|
+
dbOut = ctx.neonTriplet;
|
|
453
741
|
} else {
|
|
454
742
|
const triplet = {
|
|
455
743
|
app: String(data.db_app || "").trim(),
|
|
456
744
|
admin: String(data.db_admin || "").trim(),
|
|
457
745
|
web: String(data.db_web || "").trim(),
|
|
458
746
|
};
|
|
459
|
-
if (
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
747
|
+
if (isValidTriplet(triplet)) {
|
|
748
|
+
dbOut = triplet;
|
|
749
|
+
} else {
|
|
750
|
+
const nk = String(data.neon_api_key || "").trim();
|
|
751
|
+
if (nk && neonApiKeyResolveReady()) {
|
|
752
|
+
const trip = await neonBuildTripletFromApiKey(nk);
|
|
753
|
+
if (!trip.ok) {
|
|
754
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
755
|
+
res.end(JSON.stringify({ ok: false, error: trip.error || "Neon API error." }));
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
dbOut = trip.db;
|
|
759
|
+
ctx.neonTriplet = trip.db;
|
|
760
|
+
}
|
|
470
761
|
}
|
|
471
|
-
out.db = triplet;
|
|
472
762
|
}
|
|
763
|
+
if (!dbOut || !isValidTriplet(dbOut)) {
|
|
764
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
765
|
+
res.end(
|
|
766
|
+
JSON.stringify({
|
|
767
|
+
ok: false,
|
|
768
|
+
error: neonOAuth
|
|
769
|
+
? "Connect Neon, load URLs with API key, or paste three postgres URLs."
|
|
770
|
+
: neonApiKeyResolveReady()
|
|
771
|
+
? "Paste three postgres URLs, or use “Load connection strings” / Neon API key on submit."
|
|
772
|
+
: "Three valid postgres:// URLs required (or configure Neon project IDs + API key).",
|
|
773
|
+
})
|
|
774
|
+
);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
out.db = dbOut;
|
|
473
778
|
}
|
|
474
779
|
|
|
475
780
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -538,9 +843,9 @@ function usage() {
|
|
|
538
843
|
out(``);
|
|
539
844
|
out(` Runs: onboard → postman → mcp-tools → db:all:dev → localhost`);
|
|
540
845
|
out(` Secrets: local browser on 127.0.0.1 (no URLs or DB strings printed).`);
|
|
541
|
-
out(` Neon OAuth (optional):
|
|
542
|
-
out(` _REDIRECT_URI (http://127.0.0.1:<port>/neon/callback),
|
|
543
|
-
out(`
|
|
846
|
+
out(` Neon OAuth (optional): KOMPLIAN_NEON_OAUTH_CLIENT_ID, _CLIENT_SECRET,`);
|
|
847
|
+
out(` _REDIRECT_URI (http://127.0.0.1:<port>/neon/callback), + KOMPLIAN_NEON_OAUTH_PROJECT_ID_*`);
|
|
848
|
+
out(` Neon API key (optional): set KOMPLIAN_NEON_PROJECT_ID_APP, _ADMIN, _WEB then paste key in setup UI.`);
|
|
544
849
|
out(``);
|
|
545
850
|
out(` --terminal-only No browser`);
|
|
546
851
|
out(` -w, --workspace Clone / monorepo root`);
|
|
@@ -583,7 +888,6 @@ export async function runSetup(argv) {
|
|
|
583
888
|
|
|
584
889
|
mkdirSync(workspaceAbs, { recursive: true });
|
|
585
890
|
|
|
586
|
-
out(`${c.cyan}1/5${c.reset} repos`);
|
|
587
891
|
const onboardArgs = ["--yes"];
|
|
588
892
|
if (opts.team) onboardArgs.push("-t", opts.team);
|
|
589
893
|
if (opts.allRepos) onboardArgs.push("--all-repos");
|
|
@@ -619,6 +923,18 @@ export async function runSetup(argv) {
|
|
|
619
923
|
const needsPostmanKey = !existingPmKey;
|
|
620
924
|
const needsDbUrls = !envDbOk && !savedDbOk;
|
|
621
925
|
const neonOAuth = neonOAuthEnvReady();
|
|
926
|
+
const neonApiResolve = needsDbUrls && neonApiKeyResolveReady();
|
|
927
|
+
const showPostman = needsPostmanKey || needsDbUrls;
|
|
928
|
+
|
|
929
|
+
let logoHtml = "";
|
|
930
|
+
const logoPath = join(monorepoRoot, "web/public/images/logo-white.svg");
|
|
931
|
+
if (existsSync(logoPath)) {
|
|
932
|
+
try {
|
|
933
|
+
logoHtml = readFileSync(logoPath, "utf8").replace(/fill="#000000"/g, 'fill="#fafafa"');
|
|
934
|
+
} catch {
|
|
935
|
+
/* ignore */
|
|
936
|
+
}
|
|
937
|
+
}
|
|
622
938
|
|
|
623
939
|
if (neonOAuth && needsDbUrls) {
|
|
624
940
|
const lp = parseListenFromRedirectUri();
|
|
@@ -629,7 +945,6 @@ export async function runSetup(argv) {
|
|
|
629
945
|
}
|
|
630
946
|
|
|
631
947
|
if (useBrowser && (needsPostmanKey || needsDbUrls)) {
|
|
632
|
-
out(`${c.cyan}browser${c.reset} local form`);
|
|
633
948
|
try {
|
|
634
949
|
const lp = neonOAuth && needsDbUrls ? parseListenFromRedirectUri() : null;
|
|
635
950
|
const form = await runSetupBrowserForm({
|
|
@@ -637,6 +952,9 @@ export async function runSetup(argv) {
|
|
|
637
952
|
needsDbUrls,
|
|
638
953
|
emailDomain,
|
|
639
954
|
neonOAuth: !!(neonOAuth && needsDbUrls),
|
|
955
|
+
neonApiResolve,
|
|
956
|
+
showPostman,
|
|
957
|
+
logoHtml,
|
|
640
958
|
listenHost: lp?.host || "127.0.0.1",
|
|
641
959
|
listenPort: lp?.port ?? 0,
|
|
642
960
|
});
|
|
@@ -652,16 +970,12 @@ export async function runSetup(argv) {
|
|
|
652
970
|
}
|
|
653
971
|
}
|
|
654
972
|
|
|
655
|
-
out(`${c.cyan}2/5${c.reset} postman`);
|
|
656
973
|
await runPostman(["--yes"]);
|
|
657
974
|
|
|
658
|
-
out(`${c.cyan}3/5${c.reset} mcp`);
|
|
659
975
|
await runMcpTools(["--yes"]);
|
|
660
976
|
|
|
661
|
-
out(`${c.cyan}4/5${c.reset} databases`);
|
|
662
977
|
await runDbAllDev(["--yes", "-w", monorepoRoot]);
|
|
663
978
|
|
|
664
|
-
out(`${c.cyan}5/5${c.reset} dev`);
|
|
665
979
|
await runLocalhost(["--yes"]);
|
|
666
980
|
|
|
667
981
|
out(`${c.green}✓${c.reset} ready`);
|