miravo 0.6.0 → 0.6.2

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.
Files changed (187) hide show
  1. package/LICENSE +199 -0
  2. package/README.md +29 -59
  3. package/bin/miravo.js +84 -0
  4. package/package.json +21 -37
  5. package/console-dist/assets/activity-DsWeRq4h.js +0 -1
  6. package/console-dist/assets/api-4AqKvsm9.js +0 -1
  7. package/console-dist/assets/arrow-right-Ea_UhTNi.js +0 -1
  8. package/console-dist/assets/arrow-up-down-DFSjzzl2.js +0 -1
  9. package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +0 -1
  10. package/console-dist/assets/box-C1WaoOMF.js +0 -1
  11. package/console-dist/assets/card-BjeP32PN.js +0 -1
  12. package/console-dist/assets/catalog-BJzyzbLR.js +0 -1
  13. package/console-dist/assets/catalog-DKjEm-c4.js +0 -1
  14. package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +0 -1
  15. package/console-dist/assets/checkbox-DZlJXTE4.js +0 -1
  16. package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +0 -3
  17. package/console-dist/assets/combobox-BGnuCYxO.js +0 -1
  18. package/console-dist/assets/control-CPhvwx9o.js +0 -1
  19. package/console-dist/assets/control-Cg2up9NA.js +0 -1
  20. package/console-dist/assets/copy-button-WkbNkMcd.js +0 -1
  21. package/console-dist/assets/createLucideIcon-Bwxw859K.js +0 -1
  22. package/console-dist/assets/deploy-tab-JeWDwPoZ.js +0 -1
  23. package/console-dist/assets/dist-BhbDML0a.js +0 -1
  24. package/console-dist/assets/dist-Cy8XTNFT.js +0 -2
  25. package/console-dist/assets/download-DQ6NH16U.js +0 -1
  26. package/console-dist/assets/engine-store-CrLnz692.js +0 -1
  27. package/console-dist/assets/events-CXm29Ok4.js +0 -1
  28. package/console-dist/assets/events-D01IxhnP.js +0 -1
  29. package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
  30. package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
  31. package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
  32. package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
  33. package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
  34. package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
  35. package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
  36. package/console-dist/assets/fleet-CfxbKXt9.js +0 -1
  37. package/console-dist/assets/fleet-lotMGVqe.js +0 -1
  38. package/console-dist/assets/format-kXTonpDJ.js +0 -1
  39. package/console-dist/assets/gauge-Ci8zNZlk.js +0 -1
  40. package/console-dist/assets/get-error-message-3Q5Q-2G5.js +0 -1
  41. package/console-dist/assets/index-CVPKSDvJ.js +0 -12
  42. package/console-dist/assets/index-DJkgDeun.css +0 -2
  43. package/console-dist/assets/input-BWEQFptx.js +0 -1
  44. package/console-dist/assets/instance-detail-C5IVwuh3.js +0 -1
  45. package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
  46. package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
  47. package/console-dist/assets/label-uephX5ZG.js +0 -1
  48. package/console-dist/assets/layers-CUsLQ1fq.js +0 -1
  49. package/console-dist/assets/login-C3RJli0m.js +0 -126
  50. package/console-dist/assets/models-B_UioWQ5.js +0 -1
  51. package/console-dist/assets/models-_IPlPiZ7.js +0 -1
  52. package/console-dist/assets/not-found-B8gAm08S.js +0 -1
  53. package/console-dist/assets/platform-targets-R3T-4eJ7.js +0 -1
  54. package/console-dist/assets/platforms-BWh0BQ3Y.js +0 -1
  55. package/console-dist/assets/platforms-DStNbtVc.js +0 -1
  56. package/console-dist/assets/proxy-CsnYqnPp.js +0 -9
  57. package/console-dist/assets/query-keys-BvtE7BMs.js +0 -1
  58. package/console-dist/assets/radio-CoHegOv5.js +0 -1
  59. package/console-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
  60. package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
  61. package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
  62. package/console-dist/assets/select-CeO4vP0G.js +0 -1
  63. package/console-dist/assets/select-display-items-CIzY8953.js +0 -1
  64. package/console-dist/assets/server-D9Kfn8fQ.js +0 -1
  65. package/console-dist/assets/shallow-6lGjQhtJ.js +0 -1
  66. package/console-dist/assets/tags-CiXOIHtY.js +0 -4
  67. package/console-dist/assets/tags-FQCXIxrP.js +0 -1
  68. package/console-dist/assets/tooltip-HGLF366Q.js +0 -1
  69. package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +0 -1
  70. package/console-dist/assets/transition-types-CPlGtM8w.js +0 -1
  71. package/console-dist/assets/triangle-alert-adNk_sea.js +0 -1
  72. package/console-dist/assets/use-platform-stream-DGbK1k2L.js +0 -2
  73. package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +0 -2
  74. package/console-dist/assets/validation-Blvud9_O.js +0 -1
  75. package/console-dist/assets/vendor-charts-CAZdaPlq.js +0 -60
  76. package/console-dist/assets/vendor-table-wd8JuMmT.js +0 -4
  77. package/console-dist/assets/vendor-ui-ByahnaY0.js +0 -1
  78. package/console-dist/favicon.svg +0 -37
  79. package/console-dist/index.html +0 -82
  80. package/console-dist/miravo-logo.png +0 -0
  81. package/src/__tests__/integration/executable-methods-runner.ts +0 -262
  82. package/src/__tests__/integration/executable-methods.integration.ts +0 -2348
  83. package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +0 -314
  84. package/src/commands/adapter.ts +0 -249
  85. package/src/commands/apply.test.ts +0 -62
  86. package/src/commands/apply.ts +0 -216
  87. package/src/commands/cleanup.test.ts +0 -34
  88. package/src/commands/cleanup.ts +0 -201
  89. package/src/commands/cli-test-setup.ts +0 -91
  90. package/src/commands/export.test.ts +0 -51
  91. package/src/commands/export.ts +0 -404
  92. package/src/commands/fork.ts +0 -219
  93. package/src/commands/inject.test.ts +0 -57
  94. package/src/commands/inject.ts +0 -212
  95. package/src/commands/load.ts +0 -167
  96. package/src/commands/logs.ts +0 -173
  97. package/src/commands/models.test.ts +0 -162
  98. package/src/commands/models.ts +0 -338
  99. package/src/commands/pause.ts +0 -67
  100. package/src/commands/platforms.test.ts +0 -186
  101. package/src/commands/platforms.ts +0 -371
  102. package/src/commands/ps.ts +0 -116
  103. package/src/commands/remove.ts +0 -38
  104. package/src/commands/reset.ts +0 -44
  105. package/src/commands/rm.ts +0 -97
  106. package/src/commands/spawn.ts +0 -45
  107. package/src/commands/speed.ts +0 -40
  108. package/src/commands/start/adapter-config.ts +0 -157
  109. package/src/commands/start/catalog-le-dt.test.ts +0 -50
  110. package/src/commands/start/catalog-le-dt.ts +0 -187
  111. package/src/commands/start/catalog-model-schema.ts +0 -89
  112. package/src/commands/start/catalog-template-detail.ts +0 -54
  113. package/src/commands/start/daemon.ts +0 -255
  114. package/src/commands/start/display.ts +0 -150
  115. package/src/commands/start/engine-setup.ts +0 -247
  116. package/src/commands/start/events.ts +0 -126
  117. package/src/commands/start/index.ts +0 -897
  118. package/src/commands/start/protocol-plans.ts +0 -239
  119. package/src/commands/start/template-loader.ts +0 -197
  120. package/src/commands/start/template-resolver.ts +0 -185
  121. package/src/commands/start/types.ts +0 -73
  122. package/src/commands/start.test.ts +0 -480
  123. package/src/commands/status.test.ts +0 -20
  124. package/src/commands/status.ts +0 -470
  125. package/src/commands/stop.test.ts +0 -84
  126. package/src/commands/stop.ts +0 -169
  127. package/src/commands/templates.test.ts +0 -58
  128. package/src/commands/templates.ts +0 -277
  129. package/src/index.ts +0 -166
  130. package/src/lib/admin-auth.ts +0 -157
  131. package/src/lib/admin-client.test.ts +0 -51
  132. package/src/lib/admin-client.ts +0 -326
  133. package/src/lib/admin-platform-routes.ts +0 -1017
  134. package/src/lib/admin-platform-stream.test.ts +0 -434
  135. package/src/lib/admin-platform-stream.ts +0 -788
  136. package/src/lib/admin-responses.ts +0 -211
  137. package/src/lib/admin-server.test.ts +0 -781
  138. package/src/lib/admin-server.ts +0 -797
  139. package/src/lib/auth-store.ts +0 -87
  140. package/src/lib/daemon-registry.test.ts +0 -429
  141. package/src/lib/daemon-registry.ts +0 -343
  142. package/src/lib/le/le-apply.test.ts +0 -591
  143. package/src/lib/le/le-apply.ts +0 -832
  144. package/src/lib/le/le-auth.ts +0 -89
  145. package/src/lib/le/le-cleanup.test.ts +0 -168
  146. package/src/lib/le/le-cleanup.ts +0 -399
  147. package/src/lib/le/le-cli-helpers.ts +0 -77
  148. package/src/lib/le/le-client.ts +0 -184
  149. package/src/lib/le/le-concurrency.test.ts +0 -157
  150. package/src/lib/le/le-concurrency.ts +0 -78
  151. package/src/lib/le/le-connection-test.ts +0 -42
  152. package/src/lib/le/le-devicehub-properties.test.ts +0 -53
  153. package/src/lib/le/le-devicehub-properties.ts +0 -33
  154. package/src/lib/le/le-devicehub.ts +0 -213
  155. package/src/lib/le/le-digital-twins.ts +0 -285
  156. package/src/lib/le/le-drivers.ts +0 -23
  157. package/src/lib/le/le-endpoint.ts +0 -40
  158. package/src/lib/le/le-environments.test.ts +0 -73
  159. package/src/lib/le/le-environments.ts +0 -156
  160. package/src/lib/le/le-markers.ts +0 -87
  161. package/src/lib/le/le-plan.test.ts +0 -26
  162. package/src/lib/le/le-plan.ts +0 -109
  163. package/src/lib/le/le-platform-operations.test.ts +0 -198
  164. package/src/lib/le/le-platform-operations.ts +0 -340
  165. package/src/lib/le/le-static-model-schema.ts +0 -78
  166. package/src/lib/le/le-static-planner.test.ts +0 -255
  167. package/src/lib/le/le-static-planner.ts +0 -442
  168. package/src/lib/le/le-template-exporter.test.ts +0 -527
  169. package/src/lib/le/le-template-exporter.ts +0 -459
  170. package/src/lib/le/le-value-types.ts +0 -23
  171. package/src/lib/mqtt-client.ts +0 -115
  172. package/src/lib/parse.test.ts +0 -123
  173. package/src/lib/parse.ts +0 -87
  174. package/src/lib/resolve-simulation.test.ts +0 -118
  175. package/src/lib/resolve-simulation.ts +0 -181
  176. package/src/lib/session-store.ts +0 -110
  177. package/src/lib/shutdown.test.ts +0 -562
  178. package/src/lib/shutdown.ts +0 -621
  179. package/src/lib/template-category.test.ts +0 -71
  180. package/src/lib/template-category.ts +0 -72
  181. package/src/lib/template-locator.ts +0 -55
  182. package/src/lib/template-utils.ts +0 -45
  183. package/src/lib/ws-hub.ts +0 -281
  184. package/src/logger.test.ts +0 -172
  185. package/src/logger.ts +0 -402
  186. package/src/output.test.ts +0 -64
  187. package/src/output.ts +0 -160
@@ -1,621 +0,0 @@
1
- import type { Engine, EngineMetricsSnapshot } from "@miravo/core";
2
- import type { AdapterHealthSnapshot } from "@miravo/shared";
3
- import type { TreeItem } from "consola/utils";
4
- import { box, colors, formatTree, stripAnsi } from "consola/utils";
5
- import {
6
- adapterLogger,
7
- engineLog,
8
- isJsonLogFormat,
9
- isPlain,
10
- isQuiet,
11
- writeOutput,
12
- } from "../logger.ts";
13
- import { computeAverageThroughput, formatCount, formatDuration, sep } from "../output.ts";
14
-
15
- const SHUTDOWN_TASK_TIMEOUT_MS = 6_000;
16
- const FINAL_OUTPUT_GRACE_MS = 50;
17
- const FORWARDED_SIGINT_COALESCE_WINDOW_MS = 100;
18
-
19
- type ShutdownTaskStage = "cleanup" | "finalize";
20
- type ShutdownPhase = "booting" | "running" | "shutting_down" | "forced_exit" | "exited";
21
- type ShutdownRequestSource = "api" | "sigint" | "sigterm";
22
-
23
- interface ShutdownTask {
24
- readonly name: string;
25
- readonly run: () => void | Promise<void>;
26
- }
27
-
28
- /** Parameters for the shutdown controller. Supports dependency injection for tests. */
29
- interface ShutdownControllerOptions {
30
- /** The RNG seed used for this run (printed in summary for reproducibility). */
31
- readonly seed: number;
32
- /** Whether the seed was explicitly configured (vs. randomly generated). */
33
- readonly seedExplicit: boolean;
34
- /** Optional aggregated engine-error counts by context. */
35
- readonly errorContextCounts?: ReadonlyMap<string, number>;
36
- /** Process exit function. Defaults to process.exit. Inject for testing. */
37
- readonly exit?: (code: number) => void;
38
- /** Optional writer override for the final shutdown summary render. */
39
- readonly writeFinalOutput?: (text: string) => Promise<void>;
40
- /** Per-task timeout in ms. Defaults to SHUTDOWN_TASK_TIMEOUT_MS. Inject a small value in tests. */
41
- readonly taskTimeoutMs?: number;
42
- /** Duplicate SIGINT coalescing window in ms. Inject a small value in tests. */
43
- readonly sigintCoalesceWindowMs?: number;
44
- }
45
-
46
- interface ShutdownController {
47
- readonly completion: Promise<void>;
48
- addTask(stage: ShutdownTaskStage, name: string, run: () => void | Promise<void>): void;
49
- isShuttingDown(): boolean;
50
- markRunning(): void;
51
- requestShutdown(reason: string): void;
52
- runShutdown(): Promise<void>;
53
- setEngine(engine: Engine): void;
54
- setErrorContextCounts(counts: ReadonlyMap<string, number>): void;
55
- teardown(): void;
56
- waitForShutdownRequest(): Promise<string>;
57
- }
58
-
59
- /**
60
- * Create a single-owner shutdown controller for `miravo start`.
61
- *
62
- * Signals are installed immediately. The controller owns the full shutdown
63
- * lifecycle: pre-stop cleanup, engine stop, post-stop cleanup, final summary,
64
- * and process exit. Callers should await `completion` to keep the command alive
65
- * until shutdown fully finishes.
66
- */
67
- export function createShutdownController(options: ShutdownControllerOptions): ShutdownController {
68
- const exit = options.exit ?? ((code: number) => process.exit(code));
69
- const writeFinalOutput = options.writeFinalOutput ?? writeShutdownOutput;
70
- const taskTimeoutMs = options.taskTimeoutMs ?? SHUTDOWN_TASK_TIMEOUT_MS;
71
- const sigintCoalesceWindowMs =
72
- options.sigintCoalesceWindowMs ?? FORWARDED_SIGINT_COALESCE_WINDOW_MS;
73
-
74
- let phase: ShutdownPhase = "booting";
75
- let forcedExitRequested = false;
76
- let engine: Engine | null = null;
77
- let errorContextCounts = options.errorContextCounts;
78
- let requestedReason: string | null = null;
79
- let requestSource: ShutdownRequestSource | null = null;
80
- let shutdownPromise: Promise<void> | null = null;
81
- let requestedAtMs = 0;
82
- let forwardedSigintIgnored = false;
83
-
84
- const tasks: Record<ShutdownTaskStage, ShutdownTask[]> = {
85
- cleanup: [],
86
- finalize: [],
87
- };
88
-
89
- let resolveCompletion = () => {};
90
- const completion = new Promise<void>((resolve) => {
91
- resolveCompletion = resolve;
92
- });
93
- let resolveRequest = (_reason: string) => {};
94
- const request = new Promise<string>((resolve) => {
95
- resolveRequest = resolve;
96
- });
97
-
98
- const keepAlive = setInterval(() => {}, 60_000);
99
- const shouldIgnoreForwardedSigint = (): boolean => {
100
- if (requestSource !== "sigint" || forwardedSigintIgnored) {
101
- return false;
102
- }
103
- const elapsedMs = performance.now() - requestedAtMs;
104
- if (elapsedMs > sigintCoalesceWindowMs) {
105
- return false;
106
- }
107
- forwardedSigintIgnored = true;
108
- engineLog.debug(`Ignoring duplicate SIGINT ${elapsedMs.toFixed(1)}ms after shutdown request.`);
109
- return true;
110
- };
111
-
112
- const forceSignalHandler = (): void => {
113
- if (shouldIgnoreForwardedSigint()) {
114
- return;
115
- }
116
- forceExit();
117
- };
118
- // Absorbs SIGTERM during graceful shutdown without terminating.
119
- // bun run / bunx forwards SIGTERM to the child process when the parent
120
- // runner receives SIGINT and exits — leaving SIGTERM unhandled would restore
121
- // the OS default (abrupt termination). This no-op keeps the process alive
122
- // long enough for cleanup tasks and engine.stop() to complete.
123
- const absorbSigterm = (): void => {};
124
-
125
- const forceExit = (): void => {
126
- if (phase === "forced_exit" || phase === "exited") return;
127
- forcedExitRequested = true;
128
- phase = "forced_exit";
129
- clearInterval(keepAlive);
130
- engineLog.warn("Force stopping. Goodbye.");
131
- resolveCompletion();
132
- exit(1);
133
- };
134
-
135
- const runWithTimeout = async (name: string, run: () => void | Promise<void>): Promise<void> => {
136
- let timer: ReturnType<typeof setTimeout> | null = null;
137
- // Declared outside try so finally can suppress late rejections on timeout.
138
- // Remains null if run() throws synchronously — no dangling promise in that case.
139
- let runPromise: Promise<void> | null = null;
140
- try {
141
- runPromise = Promise.resolve(run()); // sync throws are caught by this try block
142
- await Promise.race([
143
- runPromise,
144
- new Promise<void>((resolve) => {
145
- timer = setTimeout(() => {
146
- engineLog.warn(`${name} timed out after ${taskTimeoutMs / 1000}s.`);
147
- resolve();
148
- }, taskTimeoutMs);
149
- }),
150
- ]);
151
- } catch (error) {
152
- engineLog.error(`Error during ${name}:`, error);
153
- } finally {
154
- if (timer !== null) {
155
- clearTimeout(timer);
156
- }
157
- // If the timeout fired first, runPromise is still pending. Attach a no-op
158
- // catch so a future rejection from the dangling task does not become an
159
- // unhandled-rejection crash.
160
- runPromise?.catch(() => {});
161
- }
162
- };
163
-
164
- const runStage = async (stage: ShutdownTaskStage): Promise<void> => {
165
- const stageTasks = tasks[stage];
166
- let index = 0;
167
- while (index < stageTasks.length) {
168
- const task = stageTasks[index];
169
- if (task !== undefined) {
170
- await runWithTimeout(task.name, task.run);
171
- }
172
- index++;
173
- }
174
- };
175
-
176
- const shutdown = async (reason: string): Promise<void> => {
177
- if (phase === "forced_exit" || phase === "exited") return;
178
- phase = "shutting_down";
179
- engineLog.info("Stopping simulation...");
180
-
181
- const finalMetrics = engine?.getMetrics() ?? null;
182
-
183
- await runStage("cleanup");
184
-
185
- const engineToStop = engine;
186
- if (engineToStop) {
187
- await runWithTimeout("engine stop", () => engineToStop.stop(reason));
188
- }
189
-
190
- if (!forcedExitRequested && finalMetrics !== null) {
191
- await printShutdownSummary(
192
- finalMetrics,
193
- options.seed,
194
- options.seedExplicit,
195
- errorContextCounts,
196
- writeFinalOutput,
197
- ).catch((error) => {
198
- engineLog.error("Error rendering shutdown summary:", error);
199
- });
200
- }
201
-
202
- await runStage("finalize");
203
-
204
- const forced = forcedExitRequested;
205
- clearInterval(keepAlive);
206
- phase = "exited";
207
- resolveCompletion();
208
-
209
- if (!forced) {
210
- exit(0);
211
- }
212
- };
213
-
214
- const armForceExitHandlers = (): void => {
215
- process.removeListener("SIGINT", sigintHandler);
216
- process.removeListener("SIGTERM", sigtermHandler);
217
- // Use process.on (not once): under `bun run <script>` a single Ctrl+C sends
218
- // SIGINT to the entire foreground process group, then Bun forwards the same
219
- // SIGINT to its child process as well. The first duplicate is coalesced
220
- // inside forceSignalHandler(); later SIGINTs still force-exit.
221
- process.on("SIGINT", forceSignalHandler);
222
- // SIGTERM is absorbed (no-op) rather than re-armed as a force handler.
223
- // bun run / bunx forwards SIGTERM to the child process when the parent
224
- // runner exits from its own SIGINT — leaving SIGTERM unhandled would
225
- // restore the OS default (abrupt kill), so we absorb it instead and let
226
- // cleanup tasks finish. A second Ctrl+C (SIGINT) still force-stops.
227
- // Use process.on (not once): after the first SIGTERM is absorbed the listener
228
- // must remain registered. If it were once, a second OS-level SIGTERM would
229
- // find no handler and revert to the OS default (abrupt termination).
230
- // teardown() removes it when shutdown completes.
231
- process.on("SIGTERM", absorbSigterm);
232
- };
233
-
234
- const requestShutdownInternal = (reason: string, source: ShutdownRequestSource): void => {
235
- if (phase === "forced_exit" || phase === "exited") return;
236
- if (requestedReason !== null) {
237
- // Only force-exit if shutdown is actively running. A duplicate request
238
- // that arrives before runShutdown() starts (e.g. admin /shutdown racing
239
- // with SIGINT) is silently deduplicated — first caller wins.
240
- if (phase === "shutting_down") {
241
- forceExit();
242
- }
243
- return;
244
- }
245
- requestedReason = reason;
246
- requestSource = source;
247
- requestedAtMs = performance.now();
248
- forwardedSigintIgnored = false;
249
- resolveRequest(reason);
250
- armForceExitHandlers();
251
- };
252
-
253
- const requestShutdown = (reason: string): void => {
254
- requestShutdownInternal(reason, "api");
255
- };
256
-
257
- const sigintHandler = (): void => {
258
- requestShutdownInternal("user interrupt", "sigint");
259
- };
260
-
261
- const sigtermHandler = (): void => {
262
- requestShutdownInternal("SIGTERM", "sigterm");
263
- };
264
-
265
- process.once("SIGINT", sigintHandler);
266
- process.once("SIGTERM", sigtermHandler);
267
-
268
- return {
269
- completion,
270
- addTask(stage, name, run) {
271
- tasks[stage].push({ name, run });
272
- },
273
- isShuttingDown() {
274
- return requestedReason !== null || phase === "shutting_down" || phase === "forced_exit";
275
- },
276
- markRunning() {
277
- if (phase === "booting") {
278
- phase = "running";
279
- }
280
- },
281
- requestShutdown,
282
- runShutdown() {
283
- if (shutdownPromise) {
284
- return shutdownPromise;
285
- }
286
- if (requestedReason === null) {
287
- throw new Error("Shutdown was not requested before runShutdown().");
288
- }
289
- shutdownPromise = shutdown(requestedReason);
290
- return shutdownPromise;
291
- },
292
- setEngine(nextEngine) {
293
- engine = nextEngine;
294
- },
295
- setErrorContextCounts(counts) {
296
- errorContextCounts = counts;
297
- },
298
- teardown() {
299
- process.removeListener("SIGINT", sigintHandler);
300
- process.removeListener("SIGTERM", sigtermHandler);
301
- process.removeListener("SIGINT", forceSignalHandler);
302
- process.removeListener("SIGTERM", absorbSigterm);
303
- clearInterval(keepAlive);
304
- },
305
- waitForShutdownRequest() {
306
- return request;
307
- },
308
- };
309
- }
310
-
311
- // ---------------------------------------------------------------------------
312
- // Shared helpers
313
- // ---------------------------------------------------------------------------
314
-
315
- interface SummaryInput {
316
- readonly metrics: EngineMetricsSnapshot;
317
- readonly seed?: number | undefined;
318
- readonly seedExplicit?: boolean | undefined;
319
- readonly errorContextCounts?: ReadonlyMap<string, number> | undefined;
320
- }
321
-
322
- function healthStateLabel(state: AdapterHealthSnapshot["state"]): string {
323
- switch (state) {
324
- case "healthy":
325
- return colors.green(state);
326
- case "busy":
327
- return colors.yellow(state);
328
- case "saturated":
329
- return colors.red(state);
330
- default: {
331
- const _exhaustive: never = state;
332
- return _exhaustive;
333
- }
334
- }
335
- }
336
-
337
- function adapterLabel(name: string): string {
338
- if (name === "opcua") return "OPC UA";
339
- if (name === "modbus") return "Modbus";
340
- return name.toUpperCase();
341
- }
342
-
343
- function formatAdapterLine(
344
- name: string,
345
- m: EngineMetricsSnapshot["adapters"][number]["metrics"],
346
- uptimeMs: number,
347
- ): string {
348
- const connStr = m.connected ? colors.green("✓") : colors.red("✗");
349
- const verb = name === "opcua" || name === "modbus" ? "updates" : "published";
350
- const avgThroughput = computeAverageThroughput(m.messagesPublished, uptimeMs);
351
- const throughputStr = avgThroughput !== null ? `${sep}${formatCount(avgThroughput)} msg/s` : "";
352
- const errStr =
353
- m.errorsCount > 0 ? `${sep}${colors.yellow(formatCount(m.errorsCount))} errors` : "";
354
- const health = m.health ? `${sep}${healthStateLabel(m.health.state)}` : "";
355
- return `${connStr} ${formatCount(m.messagesPublished)} ${verb}${throughputStr}${errStr}${health}`;
356
- }
357
-
358
- // ---------------------------------------------------------------------------
359
- // Periodic summary (single-line per component)
360
- // ---------------------------------------------------------------------------
361
-
362
- /**
363
- * Print a compact periodic summary: one line for core stats, one per adapter.
364
- */
365
- export function printSummaryLine(metrics: EngineMetricsSnapshot): void {
366
- if (isQuiet()) return;
367
-
368
- if (isJsonLogFormat()) {
369
- engineLog.info("Periodic summary", buildJsonSummary({ metrics }));
370
- return;
371
- }
372
-
373
- // Blank line before each summary block to visually separate it from event lines
374
- writeOutput();
375
-
376
- const { core, fleet, adapters } = metrics;
377
-
378
- // Core stats — omit tick count (redundant with rate) and instance count (static, already in banner)
379
- const parts = [
380
- formatDuration(core.uptimeMs),
381
- `${core.actualTicksPerSecond.toFixed(1)} ticks/s`,
382
- `${formatCount(core.memberValueCount)} data points`,
383
- ];
384
- if (fleet.activeFaultCount > 0) {
385
- parts.push(`${colors.yellow(String(fleet.activeFaultCount))} faults`);
386
- }
387
- engineLog.info(parts.join(sep));
388
-
389
- // Per-adapter line
390
- for (const adapter of adapters) {
391
- adapterLogger(adapter.name).info(
392
- formatAdapterLine(adapter.name, adapter.metrics, core.uptimeMs),
393
- );
394
- }
395
- }
396
-
397
- // ---------------------------------------------------------------------------
398
- // Shutdown summary (box)
399
- // ---------------------------------------------------------------------------
400
-
401
- function buildSummaryLines(input: SummaryInput): string[] {
402
- const { core, fleet, adapters } = input.metrics;
403
- const lines: string[] = [];
404
-
405
- // Core stats
406
- lines.push(
407
- `${colors.bold(formatDuration(core.uptimeMs))}${sep}${colors.bold(formatCount(core.tickCount))} ticks${sep}${colors.bold(String(core.actualTicksPerSecond.toFixed(1)))} ticks/s`,
408
- );
409
- lines.push(
410
- `${colors.bold(formatCount(core.instanceCount))} instances${sep}${colors.bold(formatCount(core.memberValueCount))} data points${sep}sim: ${colors.bold(core.simulationTimeSeconds.toFixed(1))}s`,
411
- );
412
-
413
- // Adapters
414
- for (const adapter of adapters) {
415
- const m = adapter.metrics;
416
- const label = adapterLabel(adapter.name);
417
- const connStr = m.connected ? colors.green("connected") : colors.red("disconnected");
418
- const errStr =
419
- m.errorsCount === 0
420
- ? colors.green(formatCount(m.errorsCount))
421
- : colors.yellow(formatCount(m.errorsCount));
422
- const avgThroughput = computeAverageThroughput(m.messagesPublished, core.uptimeMs);
423
- const throughputStr = avgThroughput !== null ? `${sep}${formatCount(avgThroughput)} msg/s` : "";
424
- const verb = adapter.name === "opcua" || adapter.name === "modbus" ? "updates" : "published";
425
- lines.push(
426
- `${label}: ${connStr}${sep}${colors.bold(formatCount(m.messagesPublished))} ${verb}${sep}${errStr} errors${throughputStr}`,
427
- );
428
- if (m.health) {
429
- const pendingPrefix =
430
- m.health.pendingWorkCount > 0
431
- ? `${formatCount(m.health.pendingWorkCount)} pending${sep}`
432
- : "";
433
- lines.push(` ${pendingPrefix}${healthStateLabel(m.health.state)}`);
434
- }
435
- }
436
-
437
- // Fleet health warnings
438
- const healthParts: string[] = [];
439
- if (fleet.activeFaultCount > 0) {
440
- healthParts.push(`${colors.yellow(String(fleet.activeFaultCount))} active faults`);
441
- }
442
- if (core.quarantinedInstanceCount > 0) {
443
- healthParts.push(`${colors.yellow(String(core.quarantinedInstanceCount))} quarantined`);
444
- }
445
- if (core.engineErrorsCount > 0) {
446
- healthParts.push(`${colors.yellow(formatCount(core.engineErrorsCount))} engine errors`);
447
- }
448
- if (core.tickOverrunsCount > 0) {
449
- healthParts.push(`${colors.yellow(formatCount(core.tickOverrunsCount))} overruns`);
450
- }
451
- if (core.circuitBreakerTripsCount > 0) {
452
- healthParts.push(`${colors.yellow(formatCount(core.circuitBreakerTripsCount))} breaker trips`);
453
- }
454
- if (healthParts.length > 0) {
455
- lines.push(healthParts.join(sep));
456
- }
457
-
458
- // Fault breakdown
459
- if (fleet.faultBreakdown.length > 0) {
460
- const faultItems: TreeItem[] = fleet.faultBreakdown.map(
461
- (f) => `${colors.yellow(String(f.count))} ${f.fault}`,
462
- );
463
- lines.push(formatTree(faultItems, { color: "yellow", prefix: " " }).trimEnd());
464
- }
465
-
466
- // Models & lifecycle
467
- const treeItems: TreeItem[] = [];
468
- if (fleet.modelBreakdown.length > 0) {
469
- treeItems.push({
470
- text: `${colors.cyan("Models")} ${colors.dim(`(${fleet.modelBreakdown.length})`)}`,
471
- children: fleet.modelBreakdown.map((m) => `${colors.bold(String(m.count))} ${m.model}`),
472
- });
473
- }
474
- const lifecycleEntries = Object.entries(fleet.lifecycleBreakdown).filter(
475
- ([, count]) => count > 0,
476
- );
477
- if (lifecycleEntries.length > 0) {
478
- treeItems.push({
479
- text: colors.cyan("Lifecycle"),
480
- children: lifecycleEntries.map(([stage, count]) => `${colors.bold(String(count))} ${stage}`),
481
- });
482
- }
483
- if (treeItems.length > 0) {
484
- lines.push(formatTree(treeItems, { color: "gray", prefix: " " }).trimEnd());
485
- }
486
-
487
- // Seed
488
- if (input.seed !== undefined) {
489
- const seedLine = input.seedExplicit
490
- ? `Seed: ${colors.bold(String(input.seed))}`
491
- : `Seed: ${colors.bold(String(input.seed))} ${colors.gray("(add seed to config to reproduce)")}`;
492
- lines.push(seedLine);
493
- }
494
-
495
- // Top error contexts
496
- if (input.errorContextCounts && input.errorContextCounts.size > 0) {
497
- const topContexts = [...input.errorContextCounts.entries()]
498
- .sort((a, b) => b[1] - a[1])
499
- .slice(0, 5);
500
- const errorItems: TreeItem[] = topContexts.map(
501
- ([context, count]) => `${context} ${colors.dim("\u00B7")} ${colors.bold(formatCount(count))}`,
502
- );
503
- lines.push(colors.cyan("Top errors"));
504
- lines.push(formatTree(errorItems, { color: "red", prefix: " " }).trimEnd());
505
- }
506
-
507
- return lines;
508
- }
509
-
510
- function buildJsonSummary(input: SummaryInput): Record<string, unknown> {
511
- const { core, fleet, adapters } = input.metrics;
512
-
513
- const adapterSummaries: Record<string, unknown>[] = [];
514
- for (const adapter of adapters) {
515
- const m = adapter.metrics;
516
- const avgThroughput = computeAverageThroughput(m.messagesPublished, core.uptimeMs);
517
- adapterSummaries.push({
518
- name: adapter.name,
519
- messagesPublished: m.messagesPublished,
520
- errorsCount: m.errorsCount,
521
- connected: m.connected,
522
- ...(avgThroughput !== null ? { avgMsgPerSecond: avgThroughput } : {}),
523
- ...(m.health
524
- ? { health: { state: m.health.state, pendingWorkCount: m.health.pendingWorkCount } }
525
- : {}),
526
- });
527
- }
528
-
529
- const summary: Record<string, unknown> = {
530
- uptimeMs: core.uptimeMs,
531
- tickCount: core.tickCount,
532
- ticksPerSecond: Number(core.actualTicksPerSecond.toFixed(1)),
533
- simulationTimeSeconds: Number(core.simulationTimeSeconds.toFixed(1)),
534
- instanceCount: core.instanceCount,
535
- memberValueCount: core.memberValueCount,
536
- engineErrorsCount: core.engineErrorsCount,
537
- activeFaultCount: fleet.activeFaultCount,
538
- quarantinedInstanceCount: core.quarantinedInstanceCount,
539
- tickOverrunsCount: core.tickOverrunsCount,
540
- circuitBreakerTripsCount: core.circuitBreakerTripsCount,
541
- lifecycleBreakdown: fleet.lifecycleBreakdown,
542
- adapters: adapterSummaries,
543
- };
544
-
545
- if (input.seed !== undefined) {
546
- summary["seed"] = input.seed;
547
- summary["seedExplicit"] = input.seedExplicit ?? false;
548
- }
549
-
550
- if (input.errorContextCounts && input.errorContextCounts.size > 0) {
551
- const topContexts = [...input.errorContextCounts.entries()]
552
- .sort((a, b) => b[1] - a[1])
553
- .slice(0, 5);
554
- summary["topErrorContexts"] = Object.fromEntries(topContexts);
555
- }
556
-
557
- return summary;
558
- }
559
-
560
- function renderSummaryText(title: string, input: SummaryInput): string {
561
- const lines = buildSummaryLines(input);
562
-
563
- if (isPlain()) {
564
- return [title, ...lines.map((line) => ` ${stripAnsi(line)}`), ""].join("\n");
565
- }
566
-
567
- return box(lines.join("\n"), {
568
- title: ` ${title} `,
569
- style: { borderColor: "gray", borderStyle: "rounded" },
570
- });
571
- }
572
-
573
- async function printShutdownSummary(
574
- metrics: EngineMetricsSnapshot,
575
- seed: number,
576
- seedExplicit: boolean,
577
- errorContextCounts?: ReadonlyMap<string, number>,
578
- writeFinal: (text: string) => Promise<void> = writeShutdownOutput,
579
- ): Promise<void> {
580
- if (isQuiet()) return;
581
-
582
- if (isJsonLogFormat()) {
583
- engineLog.info(
584
- "Shutdown summary",
585
- buildJsonSummary({ metrics, seed, seedExplicit, errorContextCounts }),
586
- );
587
- await flushOutputStreams();
588
- return;
589
- }
590
-
591
- await writeFinal(
592
- renderSummaryText("Shutdown Summary", {
593
- metrics,
594
- seed,
595
- seedExplicit,
596
- errorContextCounts,
597
- }),
598
- );
599
- }
600
-
601
- async function writeShutdownOutput(text: string): Promise<void> {
602
- await writeToStream(process.stdout, text.endsWith("\n") ? text : `${text}\n`);
603
- await flushOutputStreams();
604
- }
605
-
606
- async function writeToStream(stream: NodeJS.WriteStream, text: string): Promise<void> {
607
- await new Promise<void>((resolve, reject) => {
608
- stream.write(text, (error?: Error | null) => {
609
- if (error) {
610
- reject(error);
611
- return;
612
- }
613
- resolve();
614
- });
615
- });
616
- }
617
-
618
- async function flushOutputStreams(): Promise<void> {
619
- await Promise.all([writeToStream(process.stdout, ""), writeToStream(process.stderr, "")]);
620
- await Bun.sleep(FINAL_OUTPUT_GRACE_MS);
621
- }
@@ -1,71 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { DiscoveredTemplate } from "@miravo/shared";
3
- import {
4
- categorizeTemplate,
5
- groupTemplatesByCategory,
6
- sortTemplatesByCategoryAndName,
7
- } from "./template-category.ts";
8
-
9
- function discoveredTemplate(id: string, name: string): DiscoveredTemplate {
10
- return {
11
- path: `/tmp/${id}.miravo.yaml`,
12
- id,
13
- name,
14
- areaCount: 1,
15
- twinAssignmentCount: 1,
16
- inlineModelCount: 0,
17
- uniqueModelCount: 1,
18
- modelNames: ["model-a"],
19
- totalTwinCount: 1,
20
- };
21
- }
22
-
23
- describe("template category helpers", () => {
24
- test("categorizeTemplate maps stress templates to Stress & Benchmark", () => {
25
- expect(categorizeTemplate(discoveredTemplate("stress-5k", "Stress 5K"))).toBe(
26
- "Stress & Benchmark",
27
- );
28
- expect(categorizeTemplate(discoveredTemplate("stress-10k", "Stress 10K"))).toBe(
29
- "Stress & Benchmark",
30
- );
31
- expect(categorizeTemplate(discoveredTemplate("smart-factory", "Smart Factory"))).toBe(
32
- "Industrial Scenarios",
33
- );
34
- });
35
-
36
- test("sortTemplatesByCategoryAndName sorts by category then name", () => {
37
- const templates = [
38
- discoveredTemplate("stress-100k", "Stress 100K"),
39
- discoveredTemplate("water-treatment", "Water Treatment"),
40
- discoveredTemplate("smart-factory", "Smart Factory"),
41
- discoveredTemplate("stress-10k", "Stress 10K"),
42
- discoveredTemplate("stress-5k", "Stress 5K"),
43
- discoveredTemplate("stress-50k", "Stress 50K"),
44
- ];
45
-
46
- const sortedIds = sortTemplatesByCategoryAndName(templates).map((template) => template.id);
47
- expect(sortedIds).toEqual([
48
- "smart-factory",
49
- "water-treatment",
50
- "stress-100k",
51
- "stress-10k",
52
- "stress-50k",
53
- "stress-5k",
54
- ]);
55
- });
56
-
57
- test("groupTemplatesByCategory returns non-empty groups in category order", () => {
58
- const templates = sortTemplatesByCategoryAndName([
59
- discoveredTemplate("stress-50k", "Stress 50K"),
60
- discoveredTemplate("water-treatment", "Water Treatment"),
61
- ]);
62
-
63
- const grouped = groupTemplatesByCategory(templates);
64
- expect(grouped.map((group) => group.category)).toEqual([
65
- "Industrial Scenarios",
66
- "Stress & Benchmark",
67
- ]);
68
- expect(grouped[0]?.templates.map((template) => template.id)).toEqual(["water-treatment"]);
69
- expect(grouped[1]?.templates.map((template) => template.id)).toEqual(["stress-50k"]);
70
- });
71
- });