@betterdb/monitor 0.4.2 → 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/dist/index.js ADDED
@@ -0,0 +1,749 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJS = (cb, mod) => function __require() {
9
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+
28
+ // package.json
29
+ var require_package = __commonJS({
30
+ "package.json"(exports2, module2) {
31
+ module2.exports = {
32
+ name: "@betterdb/monitor",
33
+ version: "0.4.4",
34
+ description: "Monitor and observe your Valkey/Redis databases",
35
+ bin: {
36
+ betterdb: "./bin/betterdb.js"
37
+ },
38
+ publishConfig: {
39
+ access: "public"
40
+ },
41
+ files: [
42
+ "bin/",
43
+ "dist/",
44
+ "assets/"
45
+ ],
46
+ engines: {
47
+ node: ">=20.0.0"
48
+ },
49
+ scripts: {
50
+ build: "node scripts/build.mjs",
51
+ prepublishOnly: "pnpm run build"
52
+ },
53
+ dependencies: {
54
+ "@inquirer/prompts": "^7.0.0",
55
+ commander: "^12.0.0",
56
+ picocolors: "^1.0.0"
57
+ },
58
+ devDependencies: {
59
+ "@betterdb/shared": "workspace:*",
60
+ "@vercel/ncc": "^0.38.0",
61
+ typescript: "^5.7.0",
62
+ esbuild: "^0.25.0",
63
+ "@types/node": "^22.0.0"
64
+ },
65
+ peerDependencies: {
66
+ "better-sqlite3": ">=11.0.0"
67
+ },
68
+ peerDependenciesMeta: {
69
+ "better-sqlite3": {
70
+ optional: true
71
+ }
72
+ },
73
+ keywords: [
74
+ "valkey",
75
+ "redis",
76
+ "monitor",
77
+ "database",
78
+ "cli",
79
+ "observability"
80
+ ],
81
+ author: "BetterDB",
82
+ license: "SEE LICENSE IN LICENSE",
83
+ repository: {
84
+ type: "git",
85
+ url: "https://github.com/BetterDB-inc/monitor.git"
86
+ },
87
+ homepage: "https://betterdb.com"
88
+ };
89
+ }
90
+ });
91
+
92
+ // src/index.ts
93
+ var import_commander = require("commander");
94
+ var import_fs2 = require("fs");
95
+
96
+ // src/config.ts
97
+ var import_fs = require("fs");
98
+ var import_os = require("os");
99
+ var import_path = require("path");
100
+
101
+ // src/types.ts
102
+ var DEFAULT_CONFIG = {
103
+ database: {
104
+ host: "localhost",
105
+ port: 6379,
106
+ username: "default",
107
+ password: "",
108
+ type: "auto"
109
+ },
110
+ storage: {
111
+ type: "sqlite",
112
+ sqlitePath: "~/.betterdb/data/audit.db"
113
+ },
114
+ security: {},
115
+ app: {
116
+ port: 3001,
117
+ anomalyDetection: true
118
+ }
119
+ };
120
+
121
+ // ../shared/src/encryption.ts
122
+ var import_crypto = require("crypto");
123
+ var ALGORITHM = "aes-256-gcm";
124
+ var IV_LENGTH = 12;
125
+ var AUTH_TAG_LENGTH = 16;
126
+ var DEFAULT_KEK_SALT = "betterdb-kek-salt-v1";
127
+ function getKekSalt() {
128
+ return process.env.ENCRYPTION_KEK_SALT || DEFAULT_KEK_SALT;
129
+ }
130
+ var EnvelopeEncryptionService = class {
131
+ kek;
132
+ constructor(masterKey) {
133
+ if (!masterKey || masterKey.length < 16) {
134
+ throw new Error("ENCRYPTION_KEY must be at least 16 characters");
135
+ }
136
+ this.kek = (0, import_crypto.scryptSync)(masterKey, getKekSalt(), 32);
137
+ }
138
+ encrypt(plaintext) {
139
+ const dek = (0, import_crypto.randomBytes)(32);
140
+ const dataIv = (0, import_crypto.randomBytes)(IV_LENGTH);
141
+ const dataCipher = (0, import_crypto.createCipheriv)(ALGORITHM, dek, dataIv);
142
+ const encryptedData = Buffer.concat([
143
+ dataIv,
144
+ dataCipher.update(plaintext, "utf8"),
145
+ dataCipher.final(),
146
+ dataCipher.getAuthTag()
147
+ ]);
148
+ const dekIv = (0, import_crypto.randomBytes)(IV_LENGTH);
149
+ const dekCipher = (0, import_crypto.createCipheriv)(ALGORITHM, this.kek, dekIv);
150
+ const encryptedDek = Buffer.concat([
151
+ dekIv,
152
+ dekCipher.update(dek),
153
+ dekCipher.final(),
154
+ dekCipher.getAuthTag()
155
+ ]);
156
+ const envelope = {
157
+ v: 1,
158
+ dek: encryptedDek.toString("base64"),
159
+ data: encryptedData.toString("base64")
160
+ };
161
+ return JSON.stringify(envelope);
162
+ }
163
+ decrypt(ciphertext) {
164
+ const envelope = JSON.parse(ciphertext);
165
+ if (envelope.v !== 1) {
166
+ throw new Error(`Unsupported encryption version: ${envelope.v}`);
167
+ }
168
+ const dekBuffer = Buffer.from(envelope.dek, "base64");
169
+ const dekIv = dekBuffer.subarray(0, IV_LENGTH);
170
+ const dekEncrypted = dekBuffer.subarray(IV_LENGTH, -AUTH_TAG_LENGTH);
171
+ const dekAuthTag = dekBuffer.subarray(-AUTH_TAG_LENGTH);
172
+ const dekDecipher = (0, import_crypto.createDecipheriv)(ALGORITHM, this.kek, dekIv, {
173
+ authTagLength: AUTH_TAG_LENGTH
174
+ });
175
+ dekDecipher.setAuthTag(dekAuthTag);
176
+ const dek = Buffer.concat([
177
+ dekDecipher.update(dekEncrypted),
178
+ dekDecipher.final()
179
+ ]);
180
+ const dataBuffer = Buffer.from(envelope.data, "base64");
181
+ const dataIv = dataBuffer.subarray(0, IV_LENGTH);
182
+ const dataEncrypted = dataBuffer.subarray(IV_LENGTH, -AUTH_TAG_LENGTH);
183
+ const dataAuthTag = dataBuffer.subarray(-AUTH_TAG_LENGTH);
184
+ const dataDecipher = (0, import_crypto.createDecipheriv)(ALGORITHM, dek, dataIv, {
185
+ authTagLength: AUTH_TAG_LENGTH
186
+ });
187
+ dataDecipher.setAuthTag(dataAuthTag);
188
+ return dataDecipher.update(dataEncrypted, void 0, "utf8") + dataDecipher.final("utf8");
189
+ }
190
+ static isEncrypted(value) {
191
+ if (!value.startsWith("{")) return false;
192
+ try {
193
+ const parsed = JSON.parse(value);
194
+ return parsed.v === 1 && typeof parsed.dek === "string" && typeof parsed.data === "string";
195
+ } catch {
196
+ return false;
197
+ }
198
+ }
199
+ };
200
+
201
+ // src/config.ts
202
+ function getConfigDir() {
203
+ return (0, import_path.join)((0, import_os.homedir)(), ".betterdb");
204
+ }
205
+ function getConfigPath() {
206
+ return (0, import_path.join)(getConfigDir(), "config.json");
207
+ }
208
+ function getDataDir() {
209
+ return (0, import_path.join)(getConfigDir(), "data");
210
+ }
211
+ function configExists() {
212
+ return (0, import_fs.existsSync)(getConfigPath());
213
+ }
214
+ function ensureDirectories() {
215
+ const configDir = getConfigDir();
216
+ const dataDir = getDataDir();
217
+ if (!(0, import_fs.existsSync)(configDir)) {
218
+ (0, import_fs.mkdirSync)(configDir, { recursive: true });
219
+ }
220
+ if (!(0, import_fs.existsSync)(dataDir)) {
221
+ (0, import_fs.mkdirSync)(dataDir, { recursive: true });
222
+ }
223
+ }
224
+ function loadConfig() {
225
+ const configPath = getConfigPath();
226
+ if (!(0, import_fs.existsSync)(configPath)) {
227
+ return DEFAULT_CONFIG;
228
+ }
229
+ let parsed;
230
+ try {
231
+ const content = (0, import_fs.readFileSync)(configPath, "utf-8");
232
+ parsed = JSON.parse(content);
233
+ } catch (error) {
234
+ console.error("Warning: Failed to parse config file, using defaults");
235
+ return DEFAULT_CONFIG;
236
+ }
237
+ const merged = {
238
+ database: { ...DEFAULT_CONFIG.database, ...parsed.database },
239
+ storage: { ...DEFAULT_CONFIG.storage, ...parsed.storage },
240
+ security: { ...DEFAULT_CONFIG.security, ...parsed.security, encryptionKey: void 0 },
241
+ app: { ...DEFAULT_CONFIG.app, ...parsed.app }
242
+ };
243
+ return decryptConfig(merged);
244
+ }
245
+ function saveConfig(config) {
246
+ ensureDirectories();
247
+ const configPath = getConfigPath();
248
+ const configToSave = encryptConfig({
249
+ ...config,
250
+ security: { ...config.security, encryptionKey: void 0 }
251
+ });
252
+ (0, import_fs.writeFileSync)(configPath, JSON.stringify(configToSave, null, 2));
253
+ }
254
+ function expandPath(path) {
255
+ if (path === "~") {
256
+ return (0, import_os.homedir)();
257
+ }
258
+ if (path.startsWith("~/") || path.startsWith("~\\")) {
259
+ return (0, import_path.join)((0, import_os.homedir)(), path.slice(2));
260
+ }
261
+ if (path.startsWith("~")) {
262
+ return (0, import_path.join)((0, import_os.homedir)(), path.slice(1));
263
+ }
264
+ return path;
265
+ }
266
+ function getEncryptionKey() {
267
+ return process.env.ENCRYPTION_KEY;
268
+ }
269
+ function encryptConfig(config) {
270
+ const key = getEncryptionKey();
271
+ const password = config.database.password;
272
+ if (!key || !password || EnvelopeEncryptionService.isEncrypted(password)) {
273
+ return config;
274
+ }
275
+ try {
276
+ const encryption = new EnvelopeEncryptionService(key);
277
+ return {
278
+ ...config,
279
+ database: {
280
+ ...config.database,
281
+ password: encryption.encrypt(password)
282
+ }
283
+ };
284
+ } catch (error) {
285
+ console.warn("Warning: Failed to encrypt database password, storing plaintext");
286
+ return config;
287
+ }
288
+ }
289
+ function decryptConfig(config) {
290
+ const password = config.database.password;
291
+ if (!password || !EnvelopeEncryptionService.isEncrypted(password)) {
292
+ return config;
293
+ }
294
+ const key = getEncryptionKey();
295
+ if (!key) {
296
+ throw new Error(
297
+ "ENCRYPTION_KEY not set; database password is encrypted. Set ENCRYPTION_KEY and retry, or re-run setup to update credentials."
298
+ );
299
+ }
300
+ try {
301
+ const encryption = new EnvelopeEncryptionService(key);
302
+ return {
303
+ ...config,
304
+ database: {
305
+ ...config.database,
306
+ password: encryption.decrypt(password)
307
+ }
308
+ };
309
+ } catch (error) {
310
+ throw new Error(
311
+ "Failed to decrypt stored database password. Check ENCRYPTION_KEY or re-run setup to update credentials."
312
+ );
313
+ }
314
+ }
315
+
316
+ // src/banner.ts
317
+ var import_picocolors = __toESM(require("picocolors"));
318
+ function printBanner(version) {
319
+ console.log();
320
+ console.log(import_picocolors.default.cyan(import_picocolors.default.bold(" BetterDB Monitor")));
321
+ console.log(import_picocolors.default.dim(` v${version}`));
322
+ console.log();
323
+ }
324
+ function printStartupInfo(config) {
325
+ const { database, storage, app } = config;
326
+ console.log(import_picocolors.default.dim(" Database:"));
327
+ console.log(` ${import_picocolors.default.cyan("Host:")} ${database.host}:${database.port}`);
328
+ console.log(` ${import_picocolors.default.cyan("Type:")} ${database.type}`);
329
+ if (database.username && database.username !== "default") {
330
+ console.log(` ${import_picocolors.default.cyan("User:")} ${database.username}`);
331
+ }
332
+ console.log();
333
+ console.log(import_picocolors.default.dim(" Storage:"));
334
+ console.log(` ${import_picocolors.default.cyan("Type:")} ${storage.type}`);
335
+ if (storage.type === "sqlite" && storage.sqlitePath) {
336
+ console.log(` ${import_picocolors.default.cyan("Path:")} ${storage.sqlitePath}`);
337
+ } else if (storage.type === "postgres" && storage.postgresUrl) {
338
+ const maskedUrl = storage.postgresUrl.replace(
339
+ /^([^:]+:\/\/[^:@]+):([^@]+)@/,
340
+ "$1:****@"
341
+ );
342
+ console.log(` ${import_picocolors.default.cyan("URL:")} ${maskedUrl}`);
343
+ }
344
+ console.log();
345
+ console.log(import_picocolors.default.dim(" Server:"));
346
+ console.log(` ${import_picocolors.default.cyan("URL:")} ${import_picocolors.default.underline(`http://localhost:${app.port}`)}`);
347
+ console.log(` ${import_picocolors.default.cyan("API:")} ${import_picocolors.default.underline(`http://localhost:${app.port}/api`)}`);
348
+ console.log(` ${import_picocolors.default.cyan("Docs:")} ${import_picocolors.default.underline(`http://localhost:${app.port}/api/docs`)}`);
349
+ console.log();
350
+ }
351
+ function printSuccess(message) {
352
+ console.log(import_picocolors.default.green(` \u2713 ${message}`));
353
+ }
354
+ function printError(message) {
355
+ console.log(import_picocolors.default.red(` \u2717 ${message}`));
356
+ }
357
+ function printWarning(message) {
358
+ console.log(import_picocolors.default.yellow(` ! ${message}`));
359
+ }
360
+ function printInfo(message) {
361
+ console.log(import_picocolors.default.dim(` ${message}`));
362
+ }
363
+
364
+ // src/setup.ts
365
+ var import_prompts = require("@inquirer/prompts");
366
+ var import_picocolors2 = __toESM(require("picocolors"));
367
+ var import_path2 = require("path");
368
+ function isSqliteAvailable() {
369
+ try {
370
+ require.resolve("better-sqlite3");
371
+ return true;
372
+ } catch {
373
+ return false;
374
+ }
375
+ }
376
+ async function runSetupWizard() {
377
+ console.log();
378
+ console.log(import_picocolors2.default.cyan(import_picocolors2.default.bold(" BetterDB Setup Wizard")));
379
+ console.log(import_picocolors2.default.dim(" Configure your monitoring instance"));
380
+ console.log();
381
+ const cleanup = () => {
382
+ console.log();
383
+ printInfo("Setup cancelled");
384
+ process.exit(0);
385
+ };
386
+ process.on("SIGINT", cleanup);
387
+ try {
388
+ const config = await promptConfiguration();
389
+ saveConfig(config);
390
+ console.log();
391
+ printSuccess(`Configuration saved to ~/.betterdb/config.json`);
392
+ console.log();
393
+ return config;
394
+ } catch (error) {
395
+ if (error.message?.includes("User force closed")) {
396
+ cleanup();
397
+ }
398
+ throw error;
399
+ } finally {
400
+ process.off("SIGINT", cleanup);
401
+ }
402
+ }
403
+ async function promptConfiguration() {
404
+ console.log(import_picocolors2.default.dim(" Database Connection"));
405
+ console.log();
406
+ const dbHost = await (0, import_prompts.input)({
407
+ message: "Database host:",
408
+ default: DEFAULT_CONFIG.database.host
409
+ });
410
+ const dbPort = await (0, import_prompts.input)({
411
+ message: "Database port:",
412
+ default: String(DEFAULT_CONFIG.database.port),
413
+ validate: (value) => {
414
+ const port = parseInt(value, 10);
415
+ if (isNaN(port) || port < 1 || port > 65535) {
416
+ return "Please enter a valid port number (1-65535)";
417
+ }
418
+ return true;
419
+ }
420
+ });
421
+ const dbType = await (0, import_prompts.select)({
422
+ message: "Database type:",
423
+ choices: [
424
+ { name: "Auto-detect", value: "auto" },
425
+ { name: "Valkey", value: "valkey" },
426
+ { name: "Redis", value: "redis" }
427
+ ],
428
+ default: DEFAULT_CONFIG.database.type
429
+ });
430
+ const dbUsername = await (0, import_prompts.input)({
431
+ message: "Database username:",
432
+ default: DEFAULT_CONFIG.database.username
433
+ });
434
+ const dbPassword = await (0, import_prompts.password)({
435
+ message: "Database password (leave empty for none):",
436
+ mask: "*"
437
+ });
438
+ console.log();
439
+ console.log(import_picocolors2.default.dim(" Storage Configuration"));
440
+ console.log();
441
+ const sqliteAvailable = isSqliteAvailable();
442
+ const storageChoices = [];
443
+ if (sqliteAvailable) {
444
+ storageChoices.push({ name: "SQLite (recommended)", value: "sqlite" });
445
+ } else {
446
+ storageChoices.push({
447
+ name: "SQLite (requires: npm install better-sqlite3)",
448
+ value: "sqlite"
449
+ });
450
+ }
451
+ storageChoices.push({ name: "PostgreSQL", value: "postgres" });
452
+ storageChoices.push({ name: "In-memory (no persistence)", value: "memory" });
453
+ const storageType = await (0, import_prompts.select)({
454
+ message: "Storage type:",
455
+ choices: storageChoices,
456
+ default: sqliteAvailable ? "sqlite" : "postgres"
457
+ });
458
+ let sqlitePath;
459
+ let postgresUrl;
460
+ if (storageType === "sqlite") {
461
+ if (!sqliteAvailable) {
462
+ console.log();
463
+ printWarning("better-sqlite3 is not installed.");
464
+ printInfo("Run: npm install better-sqlite3");
465
+ printInfo("Or choose a different storage type.");
466
+ console.log();
467
+ }
468
+ ensureDirectories();
469
+ const defaultSqlitePath = (0, import_path2.join)(getDataDir(), "audit.db");
470
+ sqlitePath = await (0, import_prompts.input)({
471
+ message: "SQLite database path:",
472
+ default: defaultSqlitePath
473
+ });
474
+ } else if (storageType === "postgres") {
475
+ postgresUrl = await (0, import_prompts.input)({
476
+ message: "PostgreSQL connection URL:",
477
+ default: "postgresql://user:password@localhost:5432/betterdb",
478
+ validate: (value) => {
479
+ if (!value.startsWith("postgres://") && !value.startsWith("postgresql://")) {
480
+ return "URL must start with postgres:// or postgresql://";
481
+ }
482
+ return true;
483
+ }
484
+ });
485
+ }
486
+ console.log();
487
+ console.log(import_picocolors2.default.dim(" Application Settings"));
488
+ console.log();
489
+ const appPort = await (0, import_prompts.input)({
490
+ message: "Server port:",
491
+ default: String(DEFAULT_CONFIG.app.port),
492
+ validate: (value) => {
493
+ const port = parseInt(value, 10);
494
+ if (isNaN(port) || port < 1 || port > 65535) {
495
+ return "Please enter a valid port number (1-65535)";
496
+ }
497
+ return true;
498
+ }
499
+ });
500
+ const anomalyDetection = await (0, import_prompts.confirm)({
501
+ message: "Enable anomaly detection?",
502
+ default: DEFAULT_CONFIG.app.anomalyDetection
503
+ });
504
+ console.log();
505
+ const configureAdvanced = await (0, import_prompts.confirm)({
506
+ message: "Configure advanced settings (encryption, license)?",
507
+ default: false
508
+ });
509
+ let encryptionKey;
510
+ let licenseKey;
511
+ if (configureAdvanced) {
512
+ console.log();
513
+ console.log(import_picocolors2.default.dim(" Advanced Settings"));
514
+ console.log();
515
+ const encryptionKeyInput = await (0, import_prompts.password)({
516
+ message: "Encryption key (min 16 chars, leave empty to skip):",
517
+ mask: "*",
518
+ validate: (value) => {
519
+ if (value && value.length < 16) {
520
+ return "Encryption key must be at least 16 characters";
521
+ }
522
+ return true;
523
+ }
524
+ });
525
+ if (encryptionKeyInput) {
526
+ encryptionKey = encryptionKeyInput;
527
+ process.env.ENCRYPTION_KEY = encryptionKeyInput;
528
+ }
529
+ licenseKey = await (0, import_prompts.input)({
530
+ message: "License key (leave empty for community edition):"
531
+ });
532
+ if (!licenseKey) {
533
+ licenseKey = void 0;
534
+ }
535
+ }
536
+ if (encryptionKey) {
537
+ console.log();
538
+ printInfo("ENCRYPTION_KEY is not stored in config.");
539
+ printInfo("Set ENCRYPTION_KEY in your environment before running BetterDB.");
540
+ }
541
+ return {
542
+ database: {
543
+ host: dbHost,
544
+ port: parseInt(dbPort, 10),
545
+ username: dbUsername,
546
+ password: dbPassword,
547
+ type: dbType
548
+ },
549
+ storage: {
550
+ type: storageType,
551
+ sqlitePath,
552
+ postgresUrl: postgresUrl || void 0
553
+ },
554
+ security: {},
555
+ app: {
556
+ port: parseInt(appPort, 10),
557
+ anomalyDetection,
558
+ licenseKey
559
+ }
560
+ };
561
+ }
562
+
563
+ // src/runner.ts
564
+ var import_child_process = require("child_process");
565
+ var import_path3 = require("path");
566
+ var serverProcess = null;
567
+ var isShuttingDown = false;
568
+ function mapConfigToEnv(config, staticDir) {
569
+ const env = {
570
+ // Always set these for CLI mode
571
+ NODE_ENV: "production",
572
+ AI_ENABLED: "false",
573
+ // Static directory for bundled web assets
574
+ BETTERDB_STATIC_DIR: staticDir,
575
+ // Database connection
576
+ DB_HOST: config.database.host,
577
+ DB_PORT: String(config.database.port),
578
+ DB_USERNAME: config.database.username,
579
+ DB_PASSWORD: config.database.password,
580
+ DB_TYPE: config.database.type,
581
+ // Storage configuration
582
+ STORAGE_TYPE: config.storage.type,
583
+ // Application settings
584
+ PORT: String(config.app.port),
585
+ ANOMALY_DETECTION_ENABLED: config.app.anomalyDetection ? "true" : "false"
586
+ };
587
+ if (config.storage.type === "sqlite" && config.storage.sqlitePath) {
588
+ env.STORAGE_SQLITE_FILEPATH = expandPath(config.storage.sqlitePath);
589
+ }
590
+ if (config.storage.type === "postgres" && config.storage.postgresUrl) {
591
+ env.STORAGE_URL = config.storage.postgresUrl;
592
+ }
593
+ if (config.app.licenseKey) {
594
+ env.BETTERDB_LICENSE_KEY = config.app.licenseKey;
595
+ }
596
+ return env;
597
+ }
598
+ function getServerPath() {
599
+ return (0, import_path3.join)(__dirname, "..", "assets", "server", "index.js");
600
+ }
601
+ function getStaticDir() {
602
+ return (0, import_path3.join)(__dirname, "..", "assets", "web");
603
+ }
604
+ function startServer(config) {
605
+ return new Promise((resolve, reject) => {
606
+ const serverPath = getServerPath();
607
+ const staticDir = getStaticDir();
608
+ const env = mapConfigToEnv(config, staticDir);
609
+ const fullEnv = { ...process.env, ...env };
610
+ serverProcess = (0, import_child_process.fork)(serverPath, [], {
611
+ env: fullEnv,
612
+ stdio: "inherit"
613
+ });
614
+ let started = false;
615
+ serverProcess.once("spawn", () => {
616
+ started = true;
617
+ resolve();
618
+ });
619
+ serverProcess.on("error", (error) => {
620
+ printError(`Failed to start server: ${error.message}`);
621
+ reject(error);
622
+ });
623
+ serverProcess.on("exit", (code, signal) => {
624
+ serverProcess = null;
625
+ if (code !== null) {
626
+ if (code !== 0) {
627
+ if (isShuttingDown) {
628
+ return;
629
+ }
630
+ printError(`Server exited with code ${code}`);
631
+ if (!started) {
632
+ reject(new Error(`Server exited with code ${code}`));
633
+ return;
634
+ }
635
+ process.exit(code);
636
+ }
637
+ return;
638
+ }
639
+ if (signal) {
640
+ printInfo(`Server killed by signal ${signal}`);
641
+ if (!started) {
642
+ reject(new Error(`Server killed by signal ${signal}`));
643
+ return;
644
+ }
645
+ if (isShuttingDown) {
646
+ process.exit(0);
647
+ return;
648
+ }
649
+ process.exit(1);
650
+ }
651
+ });
652
+ });
653
+ }
654
+ function setupSignalHandlers() {
655
+ const shutdown = (signal) => {
656
+ console.log();
657
+ printInfo(`Received ${signal}, shutting down...`);
658
+ isShuttingDown = true;
659
+ if (serverProcess) {
660
+ serverProcess.kill("SIGTERM");
661
+ setTimeout(() => {
662
+ if (serverProcess) {
663
+ serverProcess.kill("SIGKILL");
664
+ }
665
+ process.exit(0);
666
+ }, 5e3);
667
+ } else {
668
+ process.exit(0);
669
+ }
670
+ };
671
+ process.on("SIGINT", () => shutdown("SIGINT"));
672
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
673
+ }
674
+
675
+ // src/index.ts
676
+ var packageJson = require_package();
677
+ var VERSION = packageJson.version;
678
+ var program = new import_commander.Command();
679
+ program.name("betterdb").description("Monitor and observe your Valkey/Redis databases").version(VERSION, "-v, --version", "Display version number").option("-s, --setup", "Run the setup wizard").option("--no-setup", "Skip setup wizard even if no config exists").option("-p, --port <port>", "Server port", parseInt).option("--db-host <host>", "Database host").option("--db-port <port>", "Database port", parseInt).option("--storage-type <type>", "Storage type (sqlite, postgres, memory)").action(runCli);
680
+ program.command("setup").description("Run the interactive setup wizard (without starting server)").action(async () => {
681
+ printBanner(VERSION);
682
+ await runSetupWizard();
683
+ });
684
+ async function runCli(options) {
685
+ printBanner(VERSION);
686
+ const serverPath = getServerPath();
687
+ if (!(0, import_fs2.existsSync)(serverPath)) {
688
+ printError("Bundled server not found.");
689
+ printInfo("This usually means the package was not built correctly.");
690
+ printInfo("If running from source, run: pnpm cli:build");
691
+ process.exit(1);
692
+ }
693
+ let config;
694
+ const skipSetup = options.setup === false;
695
+ if (options.setup || !configExists() && !skipSetup) {
696
+ if (!options.setup) {
697
+ printInfo("No configuration found. Starting setup wizard...");
698
+ console.log();
699
+ }
700
+ try {
701
+ config = await runSetupWizard();
702
+ } catch (error) {
703
+ printError(error.message);
704
+ process.exit(1);
705
+ }
706
+ } else if (!configExists()) {
707
+ printError("No configuration found.");
708
+ printInfo(`Run 'betterdb --setup' or create config at ${getConfigPath()}`);
709
+ process.exit(1);
710
+ } else {
711
+ try {
712
+ config = loadConfig();
713
+ } catch (error) {
714
+ printError(error.message);
715
+ process.exit(1);
716
+ }
717
+ }
718
+ config = applyCliOverrides(config, options);
719
+ printStartupInfo(config);
720
+ setupSignalHandlers();
721
+ printInfo("Starting server...");
722
+ console.log();
723
+ try {
724
+ await startServer(config);
725
+ } catch (error) {
726
+ printError(`Failed to start: ${error.message}`);
727
+ process.exit(1);
728
+ }
729
+ }
730
+ function applyCliOverrides(config, options) {
731
+ const result = { ...config };
732
+ if (options.port) {
733
+ result.app = { ...result.app, port: options.port };
734
+ }
735
+ if (options.dbHost) {
736
+ result.database = { ...result.database, host: options.dbHost };
737
+ }
738
+ if (options.dbPort) {
739
+ result.database = { ...result.database, port: options.dbPort };
740
+ }
741
+ if (options.storageType) {
742
+ const validTypes = ["sqlite", "postgres", "memory"];
743
+ if (validTypes.includes(options.storageType)) {
744
+ result.storage = { ...result.storage, type: options.storageType };
745
+ }
746
+ }
747
+ return result;
748
+ }
749
+ program.parseAsync();