@documentation-robotics/cli 0.1.5 → 0.1.8
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/analyzers/stdio-client.js +4 -2
- package/dist/analyzers/stdio-client.js.map +2 -2
- package/dist/cli.js +1 -1
- package/dist/commands/version.js +1 -1
- package/dist/commands/visualize.js +6 -5
- package/dist/commands/visualize.js.map +2 -2
- package/dist/server/server.js +144 -86
- package/dist/server/server.js.map +2 -2
- package/dist/server-entry.js.map +2 -2
- package/dist/telemetry/index.js +1 -1
- package/package.json +20 -15
package/dist/server/server.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
|
|
2
2
|
import { cors } from "hono/cors";
|
|
3
3
|
import { swaggerUI } from "@hono/swagger-ui";
|
|
4
|
+
import { serve } from "@hono/node-server";
|
|
5
|
+
import { createNodeWebSocket } from "@hono/node-ws";
|
|
6
|
+
import { spawn, spawnSync } from "child_process";
|
|
7
|
+
import chokidar from "chokidar";
|
|
4
8
|
import { randomBytes } from "crypto";
|
|
5
9
|
import { createRequire } from "module";
|
|
6
10
|
import { fileURLToPath } from "url";
|
|
7
11
|
import path from "path";
|
|
8
|
-
let upgradeWebSocket, websocket;
|
|
9
12
|
const require2 = createRequire(import.meta.url);
|
|
10
13
|
import { Model } from "../core/model.js";
|
|
11
14
|
import { StagedChangesetStorage } from "../core/staged-changeset-storage.js";
|
|
@@ -31,7 +34,9 @@ class VisualizationServer {
|
|
|
31
34
|
app;
|
|
32
35
|
model;
|
|
33
36
|
_server;
|
|
34
|
-
//
|
|
37
|
+
// Node HTTP server returned by @hono/node-server
|
|
38
|
+
upgradeWebSocket;
|
|
39
|
+
injectWebSocket;
|
|
35
40
|
clients = /* @__PURE__ */ new Set();
|
|
36
41
|
watcher;
|
|
37
42
|
annotations = /* @__PURE__ */ new Map();
|
|
@@ -50,7 +55,7 @@ class VisualizationServer {
|
|
|
50
55
|
viewerPath;
|
|
51
56
|
// Optional custom viewer path
|
|
52
57
|
activeChatProcesses = /* @__PURE__ */ new Map();
|
|
53
|
-
// conversationId ->
|
|
58
|
+
// conversationId -> spawned chat process
|
|
54
59
|
chatConversationCounter = 0;
|
|
55
60
|
selectedChatClient;
|
|
56
61
|
// Selected chat client for server
|
|
@@ -119,7 +124,7 @@ class VisualizationServer {
|
|
|
119
124
|
}), ids;
|
|
120
125
|
})();
|
|
121
126
|
constructor(model, options) {
|
|
122
|
-
this.app = new OpenAPIHono(), this.model = model, this.
|
|
127
|
+
this.app = new OpenAPIHono(), this.model = model, this.loadWebSocketAdapter(), this.authEnabled = options?.authEnabled ?? process.env.DR_AUTH_ENABLED !== "false", this.authToken = options?.authToken || process.env.DR_AUTH_TOKEN || this.generateAuthToken(), this.withDanger = options?.withDanger || !1, this.viewerPath = options?.viewerPath || new URL("../viewer", import.meta.url).pathname, !this.authEnabled && options?.authToken && console.warn("[VisualizationServer] Warning: authToken provided but authEnabled is false. The token will be generated but not used for authentication. Set authEnabled: true to enable authentication."), this.app.use("/*", cors()), this.app.use("/*", telemetryMiddleware), this.app.use("/api/*", async (c, next) => {
|
|
123
128
|
if (!this.authEnabled)
|
|
124
129
|
return next();
|
|
125
130
|
const authHeader = c.req.header("Authorization"), queryToken = c.req.query("token");
|
|
@@ -130,19 +135,21 @@ class VisualizationServer {
|
|
|
130
135
|
});
|
|
131
136
|
}
|
|
132
137
|
/**
|
|
133
|
-
*
|
|
134
|
-
* This is called in the constructor to support WebSocket functionality
|
|
135
|
-
*
|
|
138
|
+
* Set up the WebSocket adapter for this server instance
|
|
139
|
+
* This is called in the constructor to support WebSocket functionality via @hono/node-ws,
|
|
140
|
+
* which works identically under Node.js and Bun (both implement Node's http module).
|
|
141
|
+
* Wrapped in try/catch as a defensive guard so a WS setup failure would degrade the
|
|
142
|
+
* server gracefully (no /ws route) rather than crash construction entirely, in case
|
|
143
|
+
* this ever becomes fallible again in the future.
|
|
136
144
|
*/
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
145
|
+
loadWebSocketAdapter() {
|
|
146
|
+
try {
|
|
147
|
+
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app: this.app });
|
|
148
|
+
this.injectWebSocket = injectWebSocket, this.upgradeWebSocket = upgradeWebSocket;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
151
|
+
console.warn(`[Server] WARNING: Could not set up WebSocket adapter: ${message}. WebSocket endpoints (/ws) will not be available.`), process.env.VERBOSE && error instanceof Error && error.stack && console.debug("[Server] Stack trace:", error.stack);
|
|
152
|
+
}
|
|
146
153
|
}
|
|
147
154
|
/**
|
|
148
155
|
* Detect and initialize available chat clients
|
|
@@ -858,7 +865,7 @@ class VisualizationServer {
|
|
|
858
865
|
{ name: "Annotations", description: "User annotations on model elements" },
|
|
859
866
|
{ name: "WebSocket", description: "Real-time updates via WebSocket" }
|
|
860
867
|
]
|
|
861
|
-
}), this.app.get("/api-docs", swaggerUI({ url: "/api-spec.yaml" })), upgradeWebSocket && this.app.get("/ws", async (c, next) => {
|
|
868
|
+
}), this.app.get("/api-docs", swaggerUI({ url: "/api-spec.yaml" })), this.upgradeWebSocket && this.app.get("/ws", async (c, next) => {
|
|
862
869
|
if (this.authEnabled) {
|
|
863
870
|
const queryToken = c.req.query("token"), wsProtocol = c.req.header("Sec-WebSocket-Protocol");
|
|
864
871
|
let protocolToken = null;
|
|
@@ -876,7 +883,7 @@ class VisualizationServer {
|
|
|
876
883
|
protocolToken && c.header("Sec-WebSocket-Protocol", "token");
|
|
877
884
|
}
|
|
878
885
|
return next();
|
|
879
|
-
}, upgradeWebSocket(() => ({
|
|
886
|
+
}, this.upgradeWebSocket(() => ({
|
|
880
887
|
onOpen: async (_evt, ws) => {
|
|
881
888
|
await this.recordWebSocketEvent("ws.connection.open", {
|
|
882
889
|
"ws.client_count": this.clients.size + 1
|
|
@@ -1274,23 +1281,33 @@ class VisualizationServer {
|
|
|
1274
1281
|
try {
|
|
1275
1282
|
const cmd = ["claude", "--agent", "dr-architect", "--print"];
|
|
1276
1283
|
this.withDanger && cmd.push("--dangerously-skip-permissions"), cmd.push("--verbose", "--output-format", "stream-json");
|
|
1277
|
-
const proc =
|
|
1278
|
-
cmd,
|
|
1284
|
+
const proc = spawn(cmd[0], cmd.slice(1), {
|
|
1279
1285
|
cwd: this.model.rootPath,
|
|
1280
|
-
|
|
1281
|
-
stdout: "pipe",
|
|
1282
|
-
stderr: "pipe"
|
|
1286
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1283
1287
|
});
|
|
1284
|
-
|
|
1285
|
-
|
|
1288
|
+
proc.on("error", (err) => {
|
|
1289
|
+
const errorMsg = getErrorMessage(err);
|
|
1290
|
+
console.error(`[Claude Code] Failed to spawn subprocess for conversation ${conversationId}: ${errorMsg}`), this.activeChatProcesses.delete(conversationId);
|
|
1291
|
+
try {
|
|
1292
|
+
ws.send(JSON.stringify({
|
|
1293
|
+
jsonrpc: "2.0",
|
|
1294
|
+
error: {
|
|
1295
|
+
code: JSONRPC_ERRORS.INTERNAL_ERROR,
|
|
1296
|
+
message: `Failed to launch Claude Code: ${errorMsg}`
|
|
1297
|
+
},
|
|
1298
|
+
id: requestId
|
|
1299
|
+
}));
|
|
1300
|
+
} catch (sendError) {
|
|
1301
|
+
console.error(`[Claude Code] Failed to send spawn-error notification: ${getErrorMessage(sendError)}`);
|
|
1302
|
+
}
|
|
1303
|
+
}), proc.stdin?.on("error", () => {
|
|
1304
|
+
}), this.activeChatProcesses.set(conversationId, proc), proc.stdin?.write(message), proc.stdin?.end();
|
|
1305
|
+
const decoder = new TextDecoder();
|
|
1286
1306
|
(async () => {
|
|
1287
1307
|
let accumulatedText = "", buffer = "";
|
|
1288
1308
|
try {
|
|
1289
|
-
for (
|
|
1290
|
-
const
|
|
1291
|
-
if (done)
|
|
1292
|
-
break;
|
|
1293
|
-
const chunk = decoder.decode(value, { stream: !0 });
|
|
1309
|
+
for await (const chunkBuf of proc.stdout) {
|
|
1310
|
+
const chunk = decoder.decode(chunkBuf, { stream: !0 });
|
|
1294
1311
|
buffer += chunk;
|
|
1295
1312
|
const lines = buffer.split(`
|
|
1296
1313
|
`);
|
|
@@ -1354,7 +1371,7 @@ class VisualizationServer {
|
|
|
1354
1371
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1355
1372
|
}
|
|
1356
1373
|
}));
|
|
1357
|
-
const exitCode = await proc
|
|
1374
|
+
const exitCode = await this.waitForExit(proc);
|
|
1358
1375
|
this.activeChatProcesses.delete(conversationId), ws.send(JSON.stringify({
|
|
1359
1376
|
jsonrpc: "2.0",
|
|
1360
1377
|
result: {
|
|
@@ -1367,6 +1384,8 @@ class VisualizationServer {
|
|
|
1367
1384
|
id: requestId
|
|
1368
1385
|
}));
|
|
1369
1386
|
} catch (error) {
|
|
1387
|
+
if (!proc.pid)
|
|
1388
|
+
return;
|
|
1370
1389
|
const errorMsg = getErrorMessage(error);
|
|
1371
1390
|
ws.send(JSON.stringify({
|
|
1372
1391
|
jsonrpc: "2.0",
|
|
@@ -1416,28 +1435,37 @@ class VisualizationServer {
|
|
|
1416
1435
|
async launchCopilotChat(ws, conversationId, message, requestId) {
|
|
1417
1436
|
try {
|
|
1418
1437
|
let cmd;
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
const proc = Bun.spawn({
|
|
1425
|
-
cmd,
|
|
1438
|
+
const ghResult = spawnSync("gh", ["copilot", "--version"], {
|
|
1439
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1440
|
+
});
|
|
1441
|
+
ghResult.error ? console.warn(`[Copilot] 'gh copilot --version' probe failed to spawn: ${getErrorMessage(ghResult.error)}`) : ghResult.status !== 0 && process.env.VERBOSE && console.log(`[Copilot] 'gh copilot' unavailable (exit ${ghResult.status}), falling back to standalone 'copilot'`), ghResult.status === 0 ? (cmd = ["gh", "copilot", "explain"], this.withDanger && cmd.push("--allow-all-tools"), cmd.push(message)) : (cmd = ["copilot", "explain"], this.withDanger && cmd.push("--allow-all-tools"), cmd.push(message));
|
|
1442
|
+
const proc = spawn(cmd[0], cmd.slice(1), {
|
|
1426
1443
|
cwd: this.model.rootPath,
|
|
1427
|
-
|
|
1428
|
-
stdout: "pipe",
|
|
1429
|
-
stderr: "pipe"
|
|
1444
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1430
1445
|
});
|
|
1431
|
-
|
|
1432
|
-
|
|
1446
|
+
proc.on("error", (err) => {
|
|
1447
|
+
const errorMsg = getErrorMessage(err);
|
|
1448
|
+
console.error(`[Copilot] Failed to spawn subprocess for conversation ${conversationId}: ${errorMsg}`), this.activeChatProcesses.delete(conversationId);
|
|
1449
|
+
try {
|
|
1450
|
+
ws.send(JSON.stringify({
|
|
1451
|
+
jsonrpc: "2.0",
|
|
1452
|
+
error: {
|
|
1453
|
+
code: JSONRPC_ERRORS.INTERNAL_ERROR,
|
|
1454
|
+
message: `Failed to launch GitHub Copilot: ${errorMsg}`
|
|
1455
|
+
},
|
|
1456
|
+
id: requestId
|
|
1457
|
+
}));
|
|
1458
|
+
} catch (sendError) {
|
|
1459
|
+
console.error(`[Copilot] Failed to send spawn-error notification: ${getErrorMessage(sendError)}`);
|
|
1460
|
+
}
|
|
1461
|
+
}), proc.stdin?.on("error", () => {
|
|
1462
|
+
}), this.activeChatProcesses.set(conversationId, proc);
|
|
1463
|
+
const decoder = new TextDecoder();
|
|
1433
1464
|
(async () => {
|
|
1434
1465
|
let accumulatedText = "";
|
|
1435
1466
|
try {
|
|
1436
|
-
for (
|
|
1437
|
-
const
|
|
1438
|
-
if (done)
|
|
1439
|
-
break;
|
|
1440
|
-
const chunk = decoder.decode(value, { stream: !0 });
|
|
1467
|
+
for await (const chunkBuf of proc.stdout) {
|
|
1468
|
+
const chunk = decoder.decode(chunkBuf, { stream: !0 });
|
|
1441
1469
|
accumulatedText += chunk, ws.send(JSON.stringify({
|
|
1442
1470
|
jsonrpc: "2.0",
|
|
1443
1471
|
method: "chat.response.chunk",
|
|
@@ -1449,7 +1477,7 @@ class VisualizationServer {
|
|
|
1449
1477
|
}
|
|
1450
1478
|
}));
|
|
1451
1479
|
}
|
|
1452
|
-
const exitCode = await proc
|
|
1480
|
+
const exitCode = await this.waitForExit(proc);
|
|
1453
1481
|
this.activeChatProcesses.delete(conversationId), ws.send(JSON.stringify({
|
|
1454
1482
|
jsonrpc: "2.0",
|
|
1455
1483
|
result: {
|
|
@@ -1462,6 +1490,8 @@ class VisualizationServer {
|
|
|
1462
1490
|
id: requestId
|
|
1463
1491
|
}));
|
|
1464
1492
|
} catch (error) {
|
|
1493
|
+
if (!proc.pid)
|
|
1494
|
+
return;
|
|
1465
1495
|
const errorMsg = getErrorMessage(error);
|
|
1466
1496
|
ws.send(JSON.stringify({
|
|
1467
1497
|
jsonrpc: "2.0",
|
|
@@ -1496,35 +1526,33 @@ class VisualizationServer {
|
|
|
1496
1526
|
* Setup file watcher for model changes
|
|
1497
1527
|
*/
|
|
1498
1528
|
setupFileWatcher() {
|
|
1499
|
-
const modelPath = `${this.model.rootPath}/documentation-robotics/model
|
|
1500
|
-
if (!bunWatch) {
|
|
1501
|
-
console.warn("[Watcher] Bun.watch not available, file watching disabled");
|
|
1502
|
-
return;
|
|
1503
|
-
}
|
|
1529
|
+
const modelPath = `${this.model.rootPath}/documentation-robotics/model`;
|
|
1504
1530
|
try {
|
|
1505
|
-
this.watcher =
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
}
|
|
1531
|
+
this.watcher = chokidar.watch(modelPath, {
|
|
1532
|
+
persistent: !0,
|
|
1533
|
+
ignoreInitial: !0
|
|
1534
|
+
}), this.watcher.on("all", async (_event, path2) => {
|
|
1535
|
+
process.env.VERBOSE && console.log(`[Watcher] File changed: ${path2}`);
|
|
1536
|
+
try {
|
|
1537
|
+
this.model = await Model.load(this.model.rootPath, { lazyLoad: !1 }), await this.loadChangesetsFromDisk();
|
|
1538
|
+
const modelData = await this.serializeModel(), message = JSON.stringify({
|
|
1539
|
+
type: "model.updated",
|
|
1540
|
+
data: modelData,
|
|
1541
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1542
|
+
});
|
|
1543
|
+
for (const client of this.clients)
|
|
1544
|
+
try {
|
|
1545
|
+
client.send(message);
|
|
1546
|
+
} catch (error) {
|
|
1547
|
+
const msg = getErrorMessage(error);
|
|
1548
|
+
console.warn(`[Watcher] Failed to send update: ${msg}`);
|
|
1549
|
+
}
|
|
1550
|
+
} catch (error) {
|
|
1551
|
+
const message = getErrorMessage(error);
|
|
1552
|
+
console.error(`[Watcher] Failed to reload model: ${message}`);
|
|
1527
1553
|
}
|
|
1554
|
+
}), this.watcher.on("error", (err) => {
|
|
1555
|
+
console.warn(`[Watcher] Could not watch ${modelPath}: ${getErrorMessage(err)}`);
|
|
1528
1556
|
});
|
|
1529
1557
|
} catch (err) {
|
|
1530
1558
|
console.warn(`[Watcher] Could not watch ${modelPath}: ${err}`);
|
|
@@ -2226,14 +2254,14 @@ class VisualizationServer {
|
|
|
2226
2254
|
* Start the server
|
|
2227
2255
|
*/
|
|
2228
2256
|
async start(port = 8080) {
|
|
2229
|
-
this.changesetStorage = new StagedChangesetStorage(this.model.rootPath), await this.loadChangesetsFromDisk(), this.setupFileWatcher()
|
|
2230
|
-
const { serve } = await import("bun");
|
|
2231
|
-
this._server = serve({
|
|
2257
|
+
this.changesetStorage = new StagedChangesetStorage(this.model.rootPath), await this.loadChangesetsFromDisk(), this.setupFileWatcher(), this._server = serve({
|
|
2232
2258
|
port,
|
|
2233
|
-
fetch: this.app.fetch
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2259
|
+
fetch: this.app.fetch
|
|
2260
|
+
}), await new Promise((resolve, reject) => {
|
|
2261
|
+
this._server.once("listening", resolve), this._server.once("error", (err) => {
|
|
2262
|
+
err.code === "EADDRINUSE" ? reject(new Error(`Port ${port} is already in use. Try a different port with --port <port>.`)) : reject(err);
|
|
2263
|
+
});
|
|
2264
|
+
}), this.injectWebSocket?.(this._server), console.log(`\u2713 Visualization server running at http://localhost:${port}`);
|
|
2237
2265
|
}
|
|
2238
2266
|
/**
|
|
2239
2267
|
* Get the current auth token
|
|
@@ -2247,6 +2275,34 @@ class VisualizationServer {
|
|
|
2247
2275
|
isAuthEnabled() {
|
|
2248
2276
|
return this.authEnabled;
|
|
2249
2277
|
}
|
|
2278
|
+
/**
|
|
2279
|
+
* Wait for a spawned child process to exit and resolve with its exit code
|
|
2280
|
+
* Node's ChildProcess doesn't expose a Bun-style `.exited` promise, so this
|
|
2281
|
+
* wraps the "exit" event (or the already-set exitCode, if it fired before
|
|
2282
|
+
* we started listening).
|
|
2283
|
+
*/
|
|
2284
|
+
waitForExit(proc) {
|
|
2285
|
+
return new Promise((resolve) => {
|
|
2286
|
+
if (proc.exitCode !== null) {
|
|
2287
|
+
resolve(proc.exitCode);
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
proc.once("exit", (code) => resolve(code ?? 0));
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
/**
|
|
2294
|
+
* Whether a child process has actually terminated.
|
|
2295
|
+
*
|
|
2296
|
+
* NOTE: Node's `ChildProcess.killed` means "kill() was successfully used to send a
|
|
2297
|
+
* signal" — it does NOT mean the process has exited (a process can ignore SIGTERM
|
|
2298
|
+
* and keep running with `.killed === true`). This differs from Bun's
|
|
2299
|
+
* `Subprocess.killed`, which does mean "has the process exited". Use exitCode/
|
|
2300
|
+
* signalCode (set only once the process has actually terminated) instead of
|
|
2301
|
+
* `.killed` to decide whether retry/escalation is still needed.
|
|
2302
|
+
*/
|
|
2303
|
+
hasExited(proc) {
|
|
2304
|
+
return proc.exitCode !== null || proc.signalCode !== null;
|
|
2305
|
+
}
|
|
2250
2306
|
/**
|
|
2251
2307
|
* Kill a process with retry logic and exponential backoff
|
|
2252
2308
|
* Attempts graceful SIGTERM first, then force SIGKILL if needed
|
|
@@ -2256,7 +2312,7 @@ class VisualizationServer {
|
|
|
2256
2312
|
const baseDelay = 100;
|
|
2257
2313
|
for (; attempt < 3; ) {
|
|
2258
2314
|
try {
|
|
2259
|
-
if (proc
|
|
2315
|
+
if (this.hasExited(proc) || (proc.kill("SIGTERM"), await new Promise((resolve) => setTimeout(resolve, 500)), this.hasExited(proc)))
|
|
2260
2316
|
return;
|
|
2261
2317
|
} catch {
|
|
2262
2318
|
}
|
|
@@ -2266,7 +2322,7 @@ class VisualizationServer {
|
|
|
2266
2322
|
}
|
|
2267
2323
|
}
|
|
2268
2324
|
try {
|
|
2269
|
-
proc
|
|
2325
|
+
this.hasExited(proc) || (proc.kill("SIGKILL"), console.warn(`[${label}] Process for conversation ${conversationId} did not respond to SIGTERM, force killed with SIGKILL`));
|
|
2270
2326
|
} catch (error) {
|
|
2271
2327
|
throw new Error(`Failed to kill process: ${error instanceof Error ? error.message : String(error)}`);
|
|
2272
2328
|
}
|
|
@@ -2282,7 +2338,9 @@ class VisualizationServer {
|
|
|
2282
2338
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2283
2339
|
errorMessage.includes("already closed") || console.warn(`[SERVER] Unexpected error closing WebSocket client: ${errorMessage}`);
|
|
2284
2340
|
}
|
|
2285
|
-
this.clients.clear(), this.watcher && this.watcher.close?.(), this._server && this._server.
|
|
2341
|
+
this.clients.clear(), this.watcher && this.watcher.close?.(), this._server && this._server.close((error) => {
|
|
2342
|
+
error && process.env.VERBOSE && console.warn(`[SERVER] Error while closing HTTP server: ${getErrorMessage(error)}`);
|
|
2343
|
+
});
|
|
2286
2344
|
}
|
|
2287
2345
|
}
|
|
2288
2346
|
export {
|