datapeek 0.1.1

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 ADDED
@@ -0,0 +1,82 @@
1
+ # Datapeek
2
+
3
+ A local SQL database browser CLI tool, similar to Prisma Studio, for browsing MS SQL Server databases.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g datapeek
9
+ ```
10
+
11
+ Or use directly with npx:
12
+
13
+ ```bash
14
+ npx datapeek
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ### With Connection String
20
+
21
+ ```bash
22
+ npx datapeek "Server=localhost;Database=MyDB;User Id=sa;Password=password;"
23
+ ```
24
+
25
+ ### Without Connection String
26
+
27
+ ```bash
28
+ npx datapeek
29
+ ```
30
+
31
+ This will open a connection dialog in your browser where you can enter your connection details.
32
+
33
+ ## Development
34
+
35
+ ### Running in Development Mode
36
+
37
+ ```bash
38
+ # Install dependencies
39
+ npm install
40
+
41
+ # Run development server (opens browser automatically)
42
+ npm run dev
43
+
44
+ # Run with connection string
45
+ CONNECTION_STRING="Server=localhost;Database=MyDB;User Id=sa;Password=password;" npm run dev
46
+ ```
47
+
48
+ ### Building for Production
49
+
50
+ ```bash
51
+ npm run build
52
+ ```
53
+
54
+ ## Features
55
+
56
+ - 🗄️ Browse database tables and schemas
57
+ - 📊 View table data with pagination
58
+ - 🔍 Search and filter tables
59
+ - 📝 SQL query editor with syntax highlighting
60
+ - 💾 Recent connections history
61
+ - 🎨 Modern, Apple-style UI
62
+ - 🌓 Dark mode support
63
+
64
+ ## Example Connection Strings
65
+
66
+ ```bash
67
+ # Local SQL Server with SQL Auth
68
+ Server=localhost;Database=MyDB;User Id=sa;Password=MyPassword;
69
+
70
+ # Local SQL Server with Windows Auth
71
+ Server=localhost;Database=MyDB;Integrated Security=true;
72
+
73
+ # SQL Server Express
74
+ Server=localhost\\SQLEXPRESS;Database=MyDB;User Id=sa;Password=MyPassword;
75
+
76
+ # Remote SQL Server
77
+ Server=myserver.database.windows.net;Database=MyDB;User Id=myuser;Password=MyPassword;Encrypt=true;
78
+ ```
79
+
80
+ ## License
81
+
82
+ MIT
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server/db/mssql.ts
4
+ import sql from "mssql";
5
+ var pool = null;
6
+ function parseConnectionString(connectionString) {
7
+ const config = {
8
+ server: "",
9
+ database: "",
10
+ options: {
11
+ encrypt: true,
12
+ trustServerCertificate: false,
13
+ enableArithAbort: true
14
+ }
15
+ };
16
+ const parts = connectionString.split(";");
17
+ for (const part of parts) {
18
+ const [key, ...valueParts] = part.split("=");
19
+ const value = valueParts.join("=").trim();
20
+ const keyLower = key.trim().toLowerCase();
21
+ switch (keyLower) {
22
+ case "server":
23
+ case "data source":
24
+ let serverValue = value;
25
+ if (serverValue.startsWith("tcp:")) {
26
+ serverValue = serverValue.substring(4);
27
+ }
28
+ const [serverHost, serverPort] = serverValue.split(",");
29
+ config.server = serverHost.trim();
30
+ if (serverPort) {
31
+ config.port = parseInt(serverPort.trim(), 10);
32
+ }
33
+ break;
34
+ case "database":
35
+ case "initial catalog":
36
+ config.database = value;
37
+ break;
38
+ case "user id":
39
+ case "userid":
40
+ case "uid":
41
+ config.user = value;
42
+ break;
43
+ case "password":
44
+ case "pwd":
45
+ config.password = value;
46
+ break;
47
+ case "port":
48
+ config.port = parseInt(value, 10);
49
+ break;
50
+ case "encrypt":
51
+ config.options.encrypt = value.toLowerCase() === "true";
52
+ break;
53
+ case "trustservercertificate":
54
+ case "trust server certificate":
55
+ config.options.trustServerCertificate = value.toLowerCase() === "true";
56
+ break;
57
+ }
58
+ }
59
+ return config;
60
+ }
61
+ async function testConnection(config) {
62
+ const testPool = typeof config === "string" ? new sql.ConnectionPool(parseConnectionString(config)) : new sql.ConnectionPool(config);
63
+ try {
64
+ await testPool.connect();
65
+ await testPool.close();
66
+ } catch (error) {
67
+ throw error;
68
+ }
69
+ }
70
+ async function connect(config) {
71
+ await disconnect();
72
+ const connectionConfig = typeof config === "string" ? parseConnectionString(config) : config;
73
+ pool = new sql.ConnectionPool(connectionConfig);
74
+ try {
75
+ await pool.connect();
76
+ } catch (error) {
77
+ pool = null;
78
+ throw error;
79
+ }
80
+ }
81
+ async function disconnect() {
82
+ if (pool) {
83
+ try {
84
+ await pool.close();
85
+ } catch (error) {
86
+ }
87
+ pool = null;
88
+ }
89
+ }
90
+ function getConnection() {
91
+ return pool;
92
+ }
93
+ async function executeQuery(query, parameters) {
94
+ if (!pool || !pool.connected) {
95
+ throw new Error("Not connected to database");
96
+ }
97
+ const request = pool.request();
98
+ if (parameters) {
99
+ for (const param of parameters) {
100
+ if (param.type) {
101
+ request.input(param.name, param.type, param.value);
102
+ } else {
103
+ request.input(param.name, param.value);
104
+ }
105
+ }
106
+ }
107
+ const result = await request.query(query);
108
+ return result.recordset || [];
109
+ }
110
+
111
+ export {
112
+ parseConnectionString,
113
+ testConnection,
114
+ connect,
115
+ disconnect,
116
+ getConnection,
117
+ executeQuery
118
+ };
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server/db/mssql.ts
4
+ import sql from "mssql";
5
+ var pool = null;
6
+ function parseConnectionString(connectionString) {
7
+ const config = {
8
+ server: "",
9
+ database: "",
10
+ options: {
11
+ encrypt: true,
12
+ trustServerCertificate: false,
13
+ enableArithAbort: true
14
+ }
15
+ };
16
+ const parts = connectionString.split(";");
17
+ for (const part of parts) {
18
+ const [key, ...valueParts] = part.split("=");
19
+ const value = valueParts.join("=").trim();
20
+ switch (key.trim().toLowerCase()) {
21
+ case "server":
22
+ case "data source":
23
+ config.server = value;
24
+ break;
25
+ case "database":
26
+ case "initial catalog":
27
+ config.database = value;
28
+ break;
29
+ case "user id":
30
+ case "uid":
31
+ config.user = value;
32
+ break;
33
+ case "password":
34
+ case "pwd":
35
+ config.password = value;
36
+ break;
37
+ case "port":
38
+ config.port = parseInt(value, 10);
39
+ break;
40
+ case "encrypt":
41
+ config.options.encrypt = value.toLowerCase() === "true";
42
+ break;
43
+ case "trustservercertificate":
44
+ case "trust server certificate":
45
+ config.options.trustServerCertificate = value.toLowerCase() === "true";
46
+ break;
47
+ }
48
+ }
49
+ return config;
50
+ }
51
+ async function testConnection(config) {
52
+ const testPool = typeof config === "string" ? new sql.ConnectionPool(parseConnectionString(config)) : new sql.ConnectionPool(config);
53
+ try {
54
+ await testPool.connect();
55
+ await testPool.close();
56
+ } catch (error) {
57
+ throw error;
58
+ }
59
+ }
60
+ async function connect(config) {
61
+ await disconnect();
62
+ const connectionConfig = typeof config === "string" ? parseConnectionString(config) : config;
63
+ pool = new sql.ConnectionPool(connectionConfig);
64
+ try {
65
+ await pool.connect();
66
+ } catch (error) {
67
+ pool = null;
68
+ throw error;
69
+ }
70
+ }
71
+ async function disconnect() {
72
+ if (pool) {
73
+ try {
74
+ await pool.close();
75
+ } catch (error) {
76
+ }
77
+ pool = null;
78
+ }
79
+ }
80
+ function getConnection() {
81
+ return pool;
82
+ }
83
+ async function executeQuery(query, parameters) {
84
+ if (!pool || !pool.connected) {
85
+ throw new Error("Not connected to database");
86
+ }
87
+ const request = pool.request();
88
+ if (parameters) {
89
+ for (const param of parameters) {
90
+ if (param.type) {
91
+ request.input(param.name, param.type, param.value);
92
+ } else {
93
+ request.input(param.name, param.value);
94
+ }
95
+ }
96
+ }
97
+ const result = await request.query(query);
98
+ return result.recordset || [];
99
+ }
100
+
101
+ export {
102
+ parseConnectionString,
103
+ testConnection,
104
+ connect,
105
+ disconnect,
106
+ getConnection,
107
+ executeQuery
108
+ };
@@ -0,0 +1,379 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ connect,
4
+ disconnect,
5
+ executeQuery,
6
+ getConnection,
7
+ testConnection
8
+ } from "./chunk-4IGBRCNN.js";
9
+
10
+ // src/server/index.ts
11
+ import express from "express";
12
+ import cors from "cors";
13
+ import path from "path";
14
+ import { fileURLToPath } from "url";
15
+
16
+ // src/server/routes/connection.ts
17
+ import { Router } from "express";
18
+ var connectionRoutes = Router();
19
+ connectionRoutes.post("/test", async (req, res) => {
20
+ const timeout = setTimeout(() => {
21
+ if (!res.headersSent) {
22
+ res.status(408).json({
23
+ success: false,
24
+ message: "Connection test timeout"
25
+ });
26
+ }
27
+ }, 1e4);
28
+ try {
29
+ const config = req.body;
30
+ await testConnection(config);
31
+ clearTimeout(timeout);
32
+ if (!res.headersSent) {
33
+ res.json({ success: true, message: "Connection successful" });
34
+ }
35
+ } catch (error) {
36
+ clearTimeout(timeout);
37
+ if (!res.headersSent) {
38
+ res.status(400).json({
39
+ success: false,
40
+ message: error.message || "Connection failed"
41
+ });
42
+ }
43
+ }
44
+ });
45
+ connectionRoutes.get("/provided", (req, res) => {
46
+ const connString = getProvidedConnectionString();
47
+ res.json({ connectionString: connString || null });
48
+ });
49
+ connectionRoutes.post("/", async (req, res) => {
50
+ const timeout = setTimeout(() => {
51
+ if (!res.headersSent) {
52
+ res.status(408).json({
53
+ success: false,
54
+ message: "Connection timeout"
55
+ });
56
+ }
57
+ }, 15e3);
58
+ try {
59
+ const config = req.body;
60
+ await connect(config);
61
+ clearTimeout(timeout);
62
+ if (!res.headersSent) {
63
+ res.json({ success: true, message: "Connected" });
64
+ }
65
+ } catch (error) {
66
+ clearTimeout(timeout);
67
+ if (!res.headersSent) {
68
+ res.status(400).json({
69
+ success: false,
70
+ message: error.message || "Connection failed"
71
+ });
72
+ }
73
+ }
74
+ });
75
+ connectionRoutes.delete("/", async (req, res) => {
76
+ try {
77
+ await disconnect();
78
+ res.json({ success: true, message: "Disconnected" });
79
+ } catch (error) {
80
+ res.status(500).json({
81
+ success: false,
82
+ message: error.message || "Disconnect failed"
83
+ });
84
+ }
85
+ });
86
+ connectionRoutes.get("/status", async (req, res) => {
87
+ try {
88
+ const { getConnection: getConnection2, executeQuery: executeQuery2 } = await import("./mssql-4KECNFPA.js");
89
+ const pool = getConnection2();
90
+ if (pool && pool.connected) {
91
+ try {
92
+ const result = await executeQuery2("SELECT DB_NAME() as databaseName");
93
+ const databaseName = result[0]?.databaseName || null;
94
+ res.json({ connected: true, databaseName });
95
+ } catch {
96
+ res.json({ connected: true, databaseName: null });
97
+ }
98
+ } else {
99
+ res.json({ connected: false });
100
+ }
101
+ } catch {
102
+ res.json({ connected: false });
103
+ }
104
+ });
105
+
106
+ // src/server/routes/tables.ts
107
+ import { Router as Router2 } from "express";
108
+ import sql from "mssql";
109
+ var tableRoutes = Router2();
110
+ tableRoutes.get("/", async (req, res) => {
111
+ try {
112
+ const pool = getConnection();
113
+ if (!pool || !pool.connected) {
114
+ return res.status(400).json({ error: "Not connected to database" });
115
+ }
116
+ const query = `
117
+ SELECT
118
+ TABLE_SCHEMA as schemaName,
119
+ TABLE_NAME as tableName
120
+ FROM INFORMATION_SCHEMA.TABLES
121
+ WHERE TABLE_TYPE = 'BASE TABLE'
122
+ ORDER BY TABLE_SCHEMA, TABLE_NAME
123
+ `;
124
+ const result = await executeQuery(query);
125
+ res.json(result);
126
+ } catch (error) {
127
+ res.status(500).json({ error: error.message || "Failed to fetch tables" });
128
+ }
129
+ });
130
+ tableRoutes.get("/:schema/:table", async (req, res) => {
131
+ try {
132
+ const { schema, table } = req.params;
133
+ const pool = getConnection();
134
+ if (!pool || !pool.connected) {
135
+ return res.status(400).json({ error: "Not connected to database" });
136
+ }
137
+ const query = `
138
+ SELECT
139
+ c.COLUMN_NAME as columnName,
140
+ c.DATA_TYPE as dataType,
141
+ c.CHARACTER_MAXIMUM_LENGTH as maxLength,
142
+ c.IS_NULLABLE as isNullable,
143
+ c.COLUMN_DEFAULT as defaultValue,
144
+ CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END as isPrimaryKey
145
+ FROM INFORMATION_SCHEMA.COLUMNS c
146
+ LEFT JOIN (
147
+ SELECT ku.TABLE_SCHEMA, ku.TABLE_NAME, ku.COLUMN_NAME
148
+ FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
149
+ INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE ku
150
+ ON tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
151
+ AND tc.CONSTRAINT_NAME = ku.CONSTRAINT_NAME
152
+ ) pk ON c.TABLE_SCHEMA = pk.TABLE_SCHEMA
153
+ AND c.TABLE_NAME = pk.TABLE_NAME
154
+ AND c.COLUMN_NAME = pk.COLUMN_NAME
155
+ WHERE c.TABLE_SCHEMA = @schema
156
+ AND c.TABLE_NAME = @table
157
+ ORDER BY c.ORDINAL_POSITION
158
+ `;
159
+ const result = await executeQuery(query, [
160
+ { name: "schema", value: schema, type: sql.NVarChar },
161
+ { name: "table", value: table, type: sql.NVarChar }
162
+ ]);
163
+ res.json(result);
164
+ } catch (error) {
165
+ res.status(500).json({ error: error.message || "Failed to fetch table structure" });
166
+ }
167
+ });
168
+ tableRoutes.get("/:schema/:table/data", async (req, res) => {
169
+ try {
170
+ const { schema, table } = req.params;
171
+ const page = parseInt(req.query.page) || 1;
172
+ const pageSize = Math.min(parseInt(req.query.pageSize) || 100, 1e3);
173
+ const sortColumn = req.query.sortColumn;
174
+ const sortDirection = req.query.sortDirection || "asc";
175
+ const offset = (page - 1) * pageSize;
176
+ const pool = getConnection();
177
+ if (!pool || !pool.connected) {
178
+ return res.status(400).json({ error: "Not connected to database" });
179
+ }
180
+ const countQuery = `SELECT COUNT(*) as total FROM [${schema}].[${table}]`;
181
+ const countResult = await executeQuery(countQuery);
182
+ const total = countResult[0]?.total || 0;
183
+ let orderByColumn = sortColumn || "";
184
+ let orderByDirection = sortDirection?.toUpperCase() === "DESC" ? "DESC" : "ASC";
185
+ if (orderByColumn) {
186
+ try {
187
+ const validateQuery = `
188
+ SELECT COLUMN_NAME
189
+ FROM INFORMATION_SCHEMA.COLUMNS
190
+ WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = @table AND COLUMN_NAME = @column
191
+ `;
192
+ const validateResult = await executeQuery(validateQuery, [
193
+ { name: "schema", value: schema, type: sql.NVarChar },
194
+ { name: "table", value: table, type: sql.NVarChar },
195
+ { name: "column", value: orderByColumn, type: sql.NVarChar }
196
+ ]);
197
+ if (validateResult.length === 0) {
198
+ orderByColumn = "";
199
+ }
200
+ } catch (e) {
201
+ orderByColumn = "";
202
+ }
203
+ }
204
+ if (!orderByColumn) {
205
+ try {
206
+ const structureQuery = `
207
+ SELECT TOP 1 COLUMN_NAME
208
+ FROM INFORMATION_SCHEMA.COLUMNS
209
+ WHERE TABLE_SCHEMA = @schema AND TABLE_NAME = @table
210
+ ORDER BY ORDINAL_POSITION
211
+ `;
212
+ const structureResult = await executeQuery(structureQuery, [
213
+ { name: "schema", value: schema, type: sql.NVarChar },
214
+ { name: "table", value: table, type: sql.NVarChar }
215
+ ]);
216
+ if (structureResult.length > 0) {
217
+ orderByColumn = structureResult[0].COLUMN_NAME;
218
+ }
219
+ } catch (e) {
220
+ }
221
+ }
222
+ let data;
223
+ let generatedQuery = "";
224
+ if (orderByColumn) {
225
+ generatedQuery = `SELECT * FROM [${schema}].[${table}]
226
+ ORDER BY [${orderByColumn}] ${orderByDirection}
227
+ OFFSET ${offset} ROWS
228
+ FETCH NEXT ${pageSize} ROWS ONLY`;
229
+ const dataQuery = `
230
+ SELECT * FROM [${schema}].[${table}]
231
+ ORDER BY [${orderByColumn}] ${orderByDirection}
232
+ OFFSET @offset ROWS
233
+ FETCH NEXT @pageSize ROWS ONLY
234
+ `;
235
+ data = await executeQuery(dataQuery, [
236
+ { name: "offset", value: offset, type: sql.Int },
237
+ { name: "pageSize", value: pageSize, type: sql.Int }
238
+ ]);
239
+ } else {
240
+ generatedQuery = `SELECT * FROM (
241
+ SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) as rn
242
+ FROM [${schema}].[${table}]
243
+ ) t
244
+ WHERE rn > ${offset} AND rn <= ${offset + pageSize}
245
+ ORDER BY rn`;
246
+ const dataQuery = `
247
+ SELECT * FROM (
248
+ SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) as rn
249
+ FROM [${schema}].[${table}]
250
+ ) t
251
+ WHERE rn > @offset AND rn <= @offset + @pageSize
252
+ ORDER BY rn
253
+ `;
254
+ data = await executeQuery(dataQuery, [
255
+ { name: "offset", value: offset, type: sql.Int },
256
+ { name: "pageSize", value: pageSize, type: sql.Int }
257
+ ]);
258
+ data = data.map((row) => {
259
+ const { rn, ...rest } = row;
260
+ return rest;
261
+ });
262
+ }
263
+ res.json({
264
+ data,
265
+ query: generatedQuery,
266
+ pagination: {
267
+ page,
268
+ pageSize,
269
+ total,
270
+ totalPages: Math.ceil(total / pageSize)
271
+ }
272
+ });
273
+ } catch (error) {
274
+ console.error("Error fetching table data:", error);
275
+ const errorMessage = error.message || "Failed to fetch table data";
276
+ const errorDetails = error.originalError?.message || error.originalError?.info?.message || "";
277
+ res.status(500).json({
278
+ error: errorMessage,
279
+ details: errorDetails
280
+ });
281
+ }
282
+ });
283
+
284
+ // src/server/routes/query.ts
285
+ import { Router as Router3 } from "express";
286
+ var queryRoutes = Router3();
287
+ queryRoutes.post("/", async (req, res) => {
288
+ try {
289
+ const { query: sqlQuery } = req.body;
290
+ if (!sqlQuery || typeof sqlQuery !== "string") {
291
+ return res.status(400).json({ error: "Query is required" });
292
+ }
293
+ const trimmedQuery = sqlQuery.trim().toUpperCase();
294
+ if (!trimmedQuery.startsWith("SELECT")) {
295
+ return res.status(400).json({
296
+ error: "Only SELECT queries are allowed (read-only mode)"
297
+ });
298
+ }
299
+ const dangerousKeywords = ["DROP", "DELETE", "INSERT", "UPDATE", "ALTER", "CREATE", "TRUNCATE", "EXEC", "EXECUTE"];
300
+ const hasDangerousKeyword = dangerousKeywords.some(
301
+ (keyword) => trimmedQuery.includes(keyword)
302
+ );
303
+ if (hasDangerousKeyword) {
304
+ return res.status(400).json({
305
+ error: "Query contains prohibited keywords. Only SELECT queries are allowed."
306
+ });
307
+ }
308
+ const result = await executeQuery(sqlQuery);
309
+ res.json({ data: result });
310
+ } catch (error) {
311
+ res.status(500).json({
312
+ error: error.message || "Query execution failed",
313
+ details: error.originalError?.message
314
+ });
315
+ }
316
+ });
317
+
318
+ // src/server/index.ts
319
+ var __filename = fileURLToPath(import.meta.url);
320
+ var __dirname = path.dirname(__filename);
321
+ var providedConnectionString;
322
+ async function startServer(port2, connectionString2) {
323
+ providedConnectionString = connectionString2;
324
+ const app = express();
325
+ app.use(cors());
326
+ app.use(express.json());
327
+ app.use("/api/connect", connectionRoutes);
328
+ app.use("/api/tables", tableRoutes);
329
+ app.use("/api/query", queryRoutes);
330
+ if (process.env.NODE_ENV === "production") {
331
+ const clientDist = path.join(__dirname, "../client");
332
+ app.use(express.static(clientDist));
333
+ app.get("*", (req, res) => {
334
+ if (req.path.startsWith("/api")) {
335
+ return res.status(404).json({ error: "Not found" });
336
+ }
337
+ res.sendFile(path.join(clientDist, "index.html"));
338
+ });
339
+ }
340
+ app.get("/api/health", (req, res) => {
341
+ res.json({ status: "ok", hasConnectionString: !!providedConnectionString });
342
+ });
343
+ return new Promise((resolve, reject) => {
344
+ const server = app.listen(port2, () => {
345
+ resolve(app);
346
+ });
347
+ server.on("error", (error) => {
348
+ if (error.code === "EADDRINUSE") {
349
+ reject(new Error(`Port ${port2} is already in use`));
350
+ } else {
351
+ reject(error);
352
+ }
353
+ });
354
+ });
355
+ }
356
+ function getProvidedConnectionString() {
357
+ return providedConnectionString;
358
+ }
359
+
360
+ // src/server/dev.ts
361
+ var port = parseInt(process.env.PORT || "4983", 10);
362
+ var connectionString = process.env.CONNECTION_STRING;
363
+ if (connectionString) {
364
+ console.log(`\u{1F4CA} Connection string detected: ${connectionString.substring(0, 50)}...`);
365
+ }
366
+ startServer(port, connectionString).then(() => {
367
+ console.log(`
368
+ \u{1F680} Datapeek dev server running at http://localhost:${port}`);
369
+ console.log(`\u{1F4E1} API endpoints available at http://localhost:${port}/api`);
370
+ if (connectionString) {
371
+ console.log(`\u{1F4CA} Connection string provided: ${connectionString.substring(0, 50)}...`);
372
+ }
373
+ console.log(`
374
+ Press Ctrl+C to stop
375
+ `);
376
+ }).catch((error) => {
377
+ console.error("Failed to start server:", error);
378
+ process.exit(1);
379
+ });