miravo 0.1.4 → 0.2.0
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 +38 -32
- package/package.json +11 -5
- package/src/__tests__/integration/executable-methods-runner.ts +253 -0
- package/src/__tests__/integration/executable-methods.integration.ts +1688 -0
- package/src/commands/fork.ts +205 -0
- package/src/commands/inject.test.ts +6 -9
- package/src/commands/inject.ts +48 -158
- package/src/commands/models.test.ts +32 -8
- package/src/commands/models.ts +71 -45
- package/src/commands/start.test.ts +32 -63
- package/src/commands/start.ts +281 -149
- package/src/commands/status.test.ts +3 -22
- package/src/commands/status.ts +114 -279
- package/src/commands/templates.test.ts +4 -50
- package/src/commands/templates.ts +12 -191
- package/src/index.ts +16 -6
- package/src/lib/admin-client.ts +214 -0
- package/src/lib/admin-server.test.ts +276 -0
- package/src/lib/admin-server.ts +307 -0
- package/src/lib/shutdown.test.ts +2 -2
- package/src/lib/shutdown.ts +218 -35
- package/src/lib/template-category.test.ts +14 -7
- package/src/lib/template-category.ts +6 -1
- package/src/lib/template-locator.ts +5 -8
- package/src/lib/template-utils.ts +14 -0
- package/src/logger.test.ts +2 -2
- package/src/logger.ts +68 -15
- package/src/output.ts +41 -1
- package/src/lib/health-server.test.ts +0 -161
- package/src/lib/health-server.ts +0 -154
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Miravo
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Industrial environment platform. Canonical asset models, multi-protocol output, structured plant topology.
|
|
4
4
|
|
|
5
|
-
Miravo
|
|
5
|
+
Miravo simulates industrial assets with realistic physics — correlated signals, lifecycle degradation, cascading faults — and projects them over MQTT and OPC UA from a single canonical asset model. One command gives you a fully structured namespace with typed members that behave like a real plant.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
MiravoWorks/smart-factory
|
|
@@ -56,7 +56,7 @@ Connect any MQTT client to `localhost:1883` and subscribe to `#` to see the data
|
|
|
56
56
|
|
|
57
57
|
## Built-In Models
|
|
58
58
|
|
|
59
|
-
Miravo ships with 8
|
|
59
|
+
Miravo ships with 8 canonical asset models, each with typed members, lifecycle stages, and fault modes:
|
|
60
60
|
|
|
61
61
|
| Model | Key Signals | Fault Examples |
|
|
62
62
|
| ------------------ | ----------------------------------------- | ---------------------------------- |
|
|
@@ -87,24 +87,29 @@ miravo start --content-dir ./my-content
|
|
|
87
87
|
|
|
88
88
|
Start the simulation engine with an embedded MQTT broker.
|
|
89
89
|
|
|
90
|
-
| Option
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
| `--
|
|
95
|
-
| `--
|
|
96
|
-
|
|
|
97
|
-
| `--
|
|
98
|
-
| `--
|
|
99
|
-
|
|
|
100
|
-
| `--mqtt-
|
|
101
|
-
| `--mqtt-
|
|
102
|
-
| `--mqtt-
|
|
103
|
-
| `--mqtt-
|
|
104
|
-
| `--
|
|
105
|
-
| `--
|
|
106
|
-
| `--
|
|
107
|
-
| `--
|
|
90
|
+
| Option | Description |
|
|
91
|
+
| --------------------------------- | ----------------------------------------------------------------------------- |
|
|
92
|
+
| `-t, --template <id\|name\|path>` | Template to run (env: `MIRAVO_TEMPLATE`) |
|
|
93
|
+
| `-n, --name <name>` | Simulation name for control topics (env: `MIRAVO_NAME`) |
|
|
94
|
+
| `--content-dir <path>` | User content root with `/templates` and `/models` (env: `MIRAVO_CONTENT_DIR`) |
|
|
95
|
+
| `--models-dir <path>` | Additional `.twin.yaml` model directory |
|
|
96
|
+
| `-s, --speed <multiplier>` | Time multiplier (default: `1`) |
|
|
97
|
+
| `--seed <number>` | Deterministic RNG seed |
|
|
98
|
+
| `--tick <ms>` | Tick interval in ms (default: `1000`) |
|
|
99
|
+
| `-p, --mqtt-port <number>` | MQTT port (default: `1883`, env: `MIRAVO_MQTT_PORT`) |
|
|
100
|
+
| `--mqtt-host <host>` | MQTT bind host (default: `127.0.0.1`, env: `MIRAVO_MQTT_HOST`) |
|
|
101
|
+
| `--mqtt-mode <mode>` | `embedded` or `external` (env: `MIRAVO_MQTT_MODE`) |
|
|
102
|
+
| `--mqtt-url <url>` | External broker URL (env: `MIRAVO_MQTT_URL`) |
|
|
103
|
+
| `--mqtt-username <user>` | External broker username (env: `MIRAVO_MQTT_USERNAME`) |
|
|
104
|
+
| `--mqtt-password <pass>` | External broker password (env: `MIRAVO_MQTT_PASSWORD`) |
|
|
105
|
+
| `--no-mqtt` | Disable MQTT broker |
|
|
106
|
+
| `--opcua` | Enable OPC UA server |
|
|
107
|
+
| `--opcua-port <number>` | OPC UA port (default: `4840`, env: `MIRAVO_OPCUA_PORT`) |
|
|
108
|
+
| `--opcua-host <host>` | OPC UA bind host (default: `0.0.0.0`, env: `MIRAVO_OPCUA_HOST`) |
|
|
109
|
+
| `--admin-host <host>` | Admin server host (default: `127.0.0.1`, env: `MIRAVO_ADMIN_HOST`) |
|
|
110
|
+
| `--admin-port <number>` | Admin server port (default: `8080`, env: `MIRAVO_ADMIN_PORT`) |
|
|
111
|
+
| `-d, --dry-run` | Validate and print summary without starting |
|
|
112
|
+
| `--summary-interval <s>` | Seconds between periodic summaries (default: `5`) |
|
|
108
113
|
|
|
109
114
|
### `miravo templates list`
|
|
110
115
|
|
|
@@ -126,7 +131,7 @@ Show template details or copy it locally.
|
|
|
126
131
|
|
|
127
132
|
### `miravo models list`
|
|
128
133
|
|
|
129
|
-
List all discovered
|
|
134
|
+
List all discovered canonical asset models.
|
|
130
135
|
|
|
131
136
|
| Option | Description |
|
|
132
137
|
| -------------- | ---------------------------- |
|
|
@@ -134,7 +139,7 @@ List all discovered digital twin models.
|
|
|
134
139
|
|
|
135
140
|
### `miravo models inspect <name>`
|
|
136
141
|
|
|
137
|
-
Show model details —
|
|
142
|
+
Show model details — typed members, lifecycle stages, fault definitions.
|
|
138
143
|
|
|
139
144
|
| Option | Description |
|
|
140
145
|
| ------- | -------------- |
|
|
@@ -142,15 +147,16 @@ Show model details — sensors, lifecycle stages, fault definitions.
|
|
|
142
147
|
|
|
143
148
|
### `miravo status`
|
|
144
149
|
|
|
145
|
-
Read live state from a running simulation via MQTT
|
|
150
|
+
Read live state from a running simulation via the admin HTTP server. `--discover` still uses MQTT when enabled.
|
|
146
151
|
|
|
147
152
|
| Option | Description |
|
|
148
153
|
| ------------------- | ----------------------------------------------------------- |
|
|
149
|
-
|
|
|
154
|
+
| `--admin-url <url>` | Admin server URL (env: `MIRAVO_ADMIN_URL`) |
|
|
155
|
+
| `-w, --watch` | Poll and re-render on updates |
|
|
150
156
|
| `--instance <id>` | Show one instance instead of fleet overview |
|
|
151
157
|
| `--json` | Machine-readable JSON output |
|
|
152
|
-
| `--template <path>` | Template for
|
|
153
|
-
| `--timeout <ms>` |
|
|
158
|
+
| `--template <path>` | Template for MQTT discovery only (env: `MIRAVO_TEMPLATE`) |
|
|
159
|
+
| `--timeout <ms>` | Request timeout (default: `5000`) |
|
|
154
160
|
|
|
155
161
|
### `miravo inject [instanceId] [fault]`
|
|
156
162
|
|
|
@@ -159,8 +165,8 @@ Trigger or clear faults on running instances.
|
|
|
159
165
|
| Option | Description |
|
|
160
166
|
| ------------------- | ----------------------------------------------------------- |
|
|
161
167
|
| `--clear` | Clear fault instead of triggering |
|
|
162
|
-
| `--
|
|
163
|
-
| `--timeout <ms>` |
|
|
168
|
+
| `--admin-url <url>` | Admin server URL (env: `MIRAVO_ADMIN_URL`) |
|
|
169
|
+
| `--timeout <ms>` | Request timeout (default: `5000`) |
|
|
164
170
|
|
|
165
171
|
If `instanceId` and `fault` are omitted in a TTY, an interactive picker is shown.
|
|
166
172
|
|
|
@@ -193,13 +199,13 @@ docker run --rm -p 1883:1883 -p 8080:8080 \
|
|
|
193
199
|
amineamaach/miravo:latest start --template my-template
|
|
194
200
|
```
|
|
195
201
|
|
|
196
|
-
|
|
202
|
+
The admin server exposes `/healthz`, `/readyz`, `/state`, `/metrics`, and `/commands` on port 8080.
|
|
197
203
|
|
|
198
204
|
## Links
|
|
199
205
|
|
|
200
206
|
- [GitHub](https://github.com/amine-amaach/miravo)
|
|
201
|
-
- [Built-in Models & Templates Reference](https://github.com/amine-amaach/miravo/blob/main/docs/builtin-twins-and-templates.md)
|
|
202
|
-
- [Writing Custom Twin Models](https://github.com/amine-amaach/miravo/blob/main/docs/builtin-twins-and-templates.md)
|
|
207
|
+
- [Built-in Models & Templates Reference](https://github.com/amine-amaach/miravo/blob/main/docs/ai/05-builtin-twins-and-templates.md)
|
|
208
|
+
- [Writing Custom Twin Models](https://github.com/amine-amaach/miravo/blob/main/docs/ai/05-builtin-twins-and-templates.md)
|
|
203
209
|
|
|
204
210
|
## License
|
|
205
211
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miravo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Industrial environment platform with canonical asset models and MQTT + OPC UA protocol adapters",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -31,11 +31,17 @@
|
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@clack/prompts": "^1.1.0",
|
|
34
|
-
"@miravo/core": "0.
|
|
35
|
-
"@miravo/mqtt": "0.
|
|
36
|
-
"@miravo/
|
|
34
|
+
"@miravo/core": "0.2.0",
|
|
35
|
+
"@miravo/mqtt": "0.2.0",
|
|
36
|
+
"@miravo/opcua": "0.2.0",
|
|
37
|
+
"@miravo/shared": "0.2.0",
|
|
37
38
|
"commander": "^13.0.0",
|
|
38
39
|
"consola": "^3.4.0",
|
|
39
40
|
"mqtt": "^5.15.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"node-opcua-client": "^2.163.1",
|
|
44
|
+
"node-opcua-variant": "^2.163.0",
|
|
45
|
+
"yaml": "^2.8.1"
|
|
40
46
|
}
|
|
41
47
|
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createControlPlane,
|
|
3
|
+
createEngine,
|
|
4
|
+
loadTemplate,
|
|
5
|
+
loadTwinModel,
|
|
6
|
+
resolveModelPath,
|
|
7
|
+
} from "@miravo/core";
|
|
8
|
+
import { MQTTAdapter } from "@miravo/mqtt";
|
|
9
|
+
import { OPCUAAdapter } from "@miravo/opcua";
|
|
10
|
+
import type { MiravoTemplate, TwinModel } from "@miravo/shared";
|
|
11
|
+
import { startAdminServer } from "../../lib/admin-server.ts";
|
|
12
|
+
|
|
13
|
+
interface RunnerArgs {
|
|
14
|
+
readonly template: string;
|
|
15
|
+
readonly modelsDir: string;
|
|
16
|
+
readonly readyFile: string;
|
|
17
|
+
readonly rbeEnabled: boolean;
|
|
18
|
+
readonly rbeDeadband: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseArgs(argv: readonly string[]): RunnerArgs {
|
|
22
|
+
const values = new Map<string, string>();
|
|
23
|
+
for (let index = 0; index < argv.length; index += 2) {
|
|
24
|
+
const key = argv[index];
|
|
25
|
+
const value = argv[index + 1];
|
|
26
|
+
if (!key?.startsWith("--") || value === undefined) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
"Invalid smoke runner arguments. Expected --template <path> --models-dir <path> --ready-file <path>.",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
values.set(key.slice(2), value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const template = values.get("template");
|
|
35
|
+
const modelsDir = values.get("models-dir");
|
|
36
|
+
const readyFile = values.get("ready-file");
|
|
37
|
+
if (!template || !modelsDir || !readyFile) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
"Missing smoke runner arguments. Provide --template, --models-dir, and --ready-file.",
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const rbeEnabled = values.get("rbe-enabled") === "true";
|
|
44
|
+
const rbeDeadband = Number.parseFloat(values.get("rbe-deadband") ?? "0");
|
|
45
|
+
|
|
46
|
+
return { template, modelsDir, readyFile, rbeEnabled, rbeDeadband };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveSimulationName(template: MiravoTemplate): string {
|
|
50
|
+
const namespace = template.namespace;
|
|
51
|
+
if (!namespace) {
|
|
52
|
+
throw new Error("Smoke template is missing namespace metadata.");
|
|
53
|
+
}
|
|
54
|
+
if ("enterprise" in namespace) {
|
|
55
|
+
return `${namespace.enterprise}-${namespace.site}`.toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
return namespace.prefix.toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildUNSBasePath(template: MiravoTemplate, areaName: string, lineName: string): string {
|
|
61
|
+
const segments: string[] = [];
|
|
62
|
+
const namespace = template.namespace;
|
|
63
|
+
if (namespace !== undefined) {
|
|
64
|
+
if ("enterprise" in namespace) {
|
|
65
|
+
segments.push(namespace.enterprise, namespace.site);
|
|
66
|
+
} else {
|
|
67
|
+
segments.push(namespace.prefix);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
segments.push(areaName, lineName);
|
|
71
|
+
return segments.join("/");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function loadModels(
|
|
75
|
+
template: MiravoTemplate,
|
|
76
|
+
modelsDir: string,
|
|
77
|
+
): Promise<Map<string, TwinModel>> {
|
|
78
|
+
const models = new Map<string, TwinModel>();
|
|
79
|
+
if (template.models) {
|
|
80
|
+
for (const inlineModel of template.models) {
|
|
81
|
+
models.set(inlineModel.model.name, inlineModel);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const referencedModelNames = new Set<string>();
|
|
86
|
+
for (const area of template.areas) {
|
|
87
|
+
for (const line of area.lines) {
|
|
88
|
+
for (const twin of line.twins) {
|
|
89
|
+
referencedModelNames.add(twin.model);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const modelName of referencedModelNames) {
|
|
95
|
+
if (models.has(modelName)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const modelPath = await resolveModelPath(modelName, [modelsDir]);
|
|
99
|
+
const model = await loadTwinModel(modelPath);
|
|
100
|
+
models.set(modelName, model);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return models;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function main(): Promise<void> {
|
|
107
|
+
const args = parseArgs(process.argv.slice(2));
|
|
108
|
+
const template = await loadTemplate(args.template);
|
|
109
|
+
const models = await loadModels(template, args.modelsDir);
|
|
110
|
+
const simulationName = resolveSimulationName(template);
|
|
111
|
+
const tickIntervalMs = template.settings?.tick_interval_ms ?? 100;
|
|
112
|
+
const timeMultiplier = template.settings?.time_multiplier ?? 1;
|
|
113
|
+
const seed = template.settings?.seed ?? 42;
|
|
114
|
+
|
|
115
|
+
const engine = createEngine({
|
|
116
|
+
simulationName,
|
|
117
|
+
seed,
|
|
118
|
+
tickIntervalMs,
|
|
119
|
+
timeMultiplier,
|
|
120
|
+
templateMetadata: {
|
|
121
|
+
name: template.name,
|
|
122
|
+
path: args.template,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
for (const [, model] of models) {
|
|
127
|
+
engine.runtime.registerModel(model);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (const area of template.areas) {
|
|
131
|
+
for (const line of area.lines) {
|
|
132
|
+
for (const twin of line.twins) {
|
|
133
|
+
engine.runtime.spawn(twin.model, twin.count, {
|
|
134
|
+
variation: twin.variation,
|
|
135
|
+
unsBasePath: buildUNSBasePath(template, area.name, line.name),
|
|
136
|
+
...(twin.parameters !== undefined ? { parameterOverrides: twin.parameters } : {}),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const mqttAdapter = new MQTTAdapter(engine.bus);
|
|
143
|
+
await mqttAdapter.start({
|
|
144
|
+
name: "mqtt",
|
|
145
|
+
enabled: true,
|
|
146
|
+
embedded: true,
|
|
147
|
+
host: "127.0.0.1",
|
|
148
|
+
port: 0,
|
|
149
|
+
retain: false,
|
|
150
|
+
simulationName,
|
|
151
|
+
...(args.rbeEnabled ? { rbe: { enabled: true, deadband: args.rbeDeadband } } : {}),
|
|
152
|
+
});
|
|
153
|
+
engine.registerAdapter(mqttAdapter);
|
|
154
|
+
|
|
155
|
+
const opcuaAdapter = new OPCUAAdapter(engine.bus, engine);
|
|
156
|
+
await opcuaAdapter.start({
|
|
157
|
+
name: "opcua",
|
|
158
|
+
enabled: true,
|
|
159
|
+
host: "127.0.0.1",
|
|
160
|
+
port: 0,
|
|
161
|
+
security: "none",
|
|
162
|
+
simulationName,
|
|
163
|
+
});
|
|
164
|
+
engine.registerAdapter(opcuaAdapter);
|
|
165
|
+
|
|
166
|
+
const transport = mqttAdapter.getControlTransport();
|
|
167
|
+
if (!transport) {
|
|
168
|
+
throw new Error("Smoke runtime failed to resolve MQTT control transport.");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const controlPlane = createControlPlane({
|
|
172
|
+
simulationName,
|
|
173
|
+
engine,
|
|
174
|
+
transport,
|
|
175
|
+
bus: engine.bus,
|
|
176
|
+
});
|
|
177
|
+
controlPlane.start();
|
|
178
|
+
|
|
179
|
+
let lastTickProgressAt = performance.now();
|
|
180
|
+
const onTickProgress = () => {
|
|
181
|
+
lastTickProgressAt = performance.now();
|
|
182
|
+
};
|
|
183
|
+
engine.bus.on("tick:complete", onTickProgress);
|
|
184
|
+
|
|
185
|
+
const adminServer = startAdminServer({
|
|
186
|
+
engine,
|
|
187
|
+
host: "127.0.0.1",
|
|
188
|
+
port: 0,
|
|
189
|
+
tickIntervalMs,
|
|
190
|
+
mqttEnabled: true,
|
|
191
|
+
isMqttConnected: () => mqttAdapter.getMetrics().connected,
|
|
192
|
+
opcuaEnabled: true,
|
|
193
|
+
isOpcuaConnected: () => opcuaAdapter.getMetrics().connected,
|
|
194
|
+
getLastTickProgressAt: () => lastTickProgressAt,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
engine.start();
|
|
198
|
+
|
|
199
|
+
await Bun.sleep(Math.max(250, tickIntervalMs * 3));
|
|
200
|
+
|
|
201
|
+
const [instanceId, instance] = [...engine.runtime.getInstances()][0] ?? [];
|
|
202
|
+
if (!instanceId || !instance) {
|
|
203
|
+
throw new Error("Smoke runtime did not spawn any instances from the template.");
|
|
204
|
+
}
|
|
205
|
+
if (!mqttAdapter.brokerUrl) {
|
|
206
|
+
throw new Error("Smoke runtime did not expose an MQTT broker URL.");
|
|
207
|
+
}
|
|
208
|
+
if (!opcuaAdapter.endpointUrl) {
|
|
209
|
+
throw new Error("Smoke runtime did not expose an OPC UA endpoint URL.");
|
|
210
|
+
}
|
|
211
|
+
if (opcuaAdapter.port <= 0) {
|
|
212
|
+
throw new Error("Smoke runtime did not expose an OPC UA port.");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await Bun.write(
|
|
216
|
+
args.readyFile,
|
|
217
|
+
JSON.stringify({
|
|
218
|
+
simulationName,
|
|
219
|
+
adminUrl: adminServer.endpoint,
|
|
220
|
+
mqttBrokerUrl: mqttAdapter.brokerUrl,
|
|
221
|
+
opcuaEndpointUrl: opcuaAdapter.endpointUrl,
|
|
222
|
+
instanceId,
|
|
223
|
+
instancePath: instance.browsePath.join("/"),
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
let shuttingDown = false;
|
|
228
|
+
const shutdown = async () => {
|
|
229
|
+
if (shuttingDown) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
shuttingDown = true;
|
|
233
|
+
adminServer.stop();
|
|
234
|
+
engine.bus.off("tick:complete", onTickProgress);
|
|
235
|
+
controlPlane.stop();
|
|
236
|
+
await engine.stop("smoke runner shutdown");
|
|
237
|
+
process.exit(0);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
process.on("SIGINT", () => {
|
|
241
|
+
void shutdown();
|
|
242
|
+
});
|
|
243
|
+
process.on("SIGTERM", () => {
|
|
244
|
+
void shutdown();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
await new Promise(() => {});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
void main().catch((error) => {
|
|
251
|
+
console.error(error instanceof Error ? (error.stack ?? error.message) : String(error));
|
|
252
|
+
process.exit(1);
|
|
253
|
+
});
|