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,470 +0,0 @@
1
- import {
2
- CONTROL_STATUS_TIMEOUT_MS,
3
- type EngineMetricsSnapshot,
4
- type EngineStateSnapshot,
5
- MiravoError,
6
- } from "@miravo/shared";
7
- import type { Command } from "commander";
8
- import { colors } from "consola/utils";
9
- import { getAdminSnapshot, resolveAdminUrl } from "../lib/admin-client.ts";
10
- import { connectToBroker, disconnectClient } from "../lib/mqtt-client.ts";
11
- import { parseTimeoutSeconds } from "../lib/parse.ts";
12
- import { discoverRunningSimulations } from "../lib/resolve-simulation.ts";
13
- import { engineLog, handleCommandError, isPlain, writeOutput } from "../logger.ts";
14
- import {
15
- formatCount,
16
- formatDuration,
17
- formatHealthSuffix,
18
- formatKeyValue,
19
- formatTable,
20
- } from "../output.ts";
21
-
22
- const STATUS_WATCH_INTERVAL_MS = 1000;
23
-
24
- interface StatusOptions {
25
- readonly name?: string;
26
- readonly discover?: boolean;
27
- readonly watch?: boolean;
28
- readonly instance?: string;
29
- readonly json?: boolean;
30
- readonly adminUrl?: string;
31
- readonly template?: string;
32
- readonly timeout?: number;
33
- }
34
-
35
- /**
36
- * Register the `miravo status` command with the root Commander program.
37
- * Shows status of a running simulation via the admin HTTP server.
38
- */
39
- export function registerStatusCommand(program: Command): void {
40
- program
41
- .command("status")
42
- .description("Show live status, metrics, and instances of a running simulation")
43
- .option("--name <name>", "Target simulation name")
44
- .option("--discover", "Discover running simulations and exit")
45
- .option("-w, --watch", "Poll the admin server and re-render on updates")
46
- .option("--instance <id>", "Show detailed status for one instance")
47
- .option("--json", "Machine-readable JSON output")
48
- .option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
49
- .option(
50
- "--template <path>",
51
- "Path to .miravo.yaml for MQTT discovery connection info (env: MIRAVO_TEMPLATE)",
52
- )
53
- .option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
54
- .addHelpText(
55
- "after",
56
- `
57
- Examples:
58
- $ miravo status Show simulation overview
59
- $ miravo status -w Watch mode (live updates)
60
- $ miravo status --instance pump-001 Detail view for one instance
61
- $ miravo status --json Machine-readable JSON output
62
- $ miravo status --discover Find running simulations via MQTT`,
63
- )
64
- .action(async (options: StatusOptions) => {
65
- await statusAction(options);
66
- });
67
- }
68
-
69
- type MetricsSnapshot = EngineMetricsSnapshot;
70
- type StateSnapshot = EngineStateSnapshot;
71
-
72
- // ---------------------------------------------------------------------------
73
- // Action
74
- // ---------------------------------------------------------------------------
75
-
76
- async function statusAction(options: StatusOptions): Promise<void> {
77
- try {
78
- const timeoutMs = options.timeout ?? CONTROL_STATUS_TIMEOUT_MS;
79
- if (options.discover) {
80
- await discoverMode(options, timeoutMs);
81
- return;
82
- }
83
-
84
- const adminUrl = resolveAdminUrl(options.adminUrl);
85
- if (options.watch) {
86
- await watchMode(adminUrl, options, timeoutMs);
87
- } else {
88
- await snapshotMode(adminUrl, options, timeoutMs);
89
- }
90
- } catch (error) {
91
- handleCommandError(error, engineLog);
92
- }
93
- }
94
-
95
- // ---------------------------------------------------------------------------
96
- // Discover mode (MQTT-only)
97
- // ---------------------------------------------------------------------------
98
-
99
- async function discoverMode(options: StatusOptions, timeoutMs: number): Promise<void> {
100
- const templateOption = options.template ?? Bun.env["MIRAVO_TEMPLATE"];
101
- const { client, brokerInfo } = await connectToBroker(templateOption, timeoutMs);
102
- try {
103
- const simulations = await discoverRunningSimulations(client, {
104
- timeoutMs,
105
- brokerUrl: brokerInfo.url,
106
- });
107
- if (simulations.length === 0) {
108
- engineLog.error(
109
- `No running simulations found on broker ${brokerInfo.url}. Start one with 'miravo start'.`,
110
- );
111
- process.exitCode = 1;
112
- return;
113
- }
114
- writeOutput(`Found ${simulations.length} simulation(s):`);
115
- for (const simulationName of simulations) {
116
- writeOutput(` ${simulationName}`);
117
- }
118
- } finally {
119
- disconnectClient(client);
120
- }
121
- }
122
-
123
- // ---------------------------------------------------------------------------
124
- // Snapshot mode (default): get one reading and exit
125
- // ---------------------------------------------------------------------------
126
-
127
- async function snapshotMode(
128
- adminUrl: string,
129
- options: StatusOptions,
130
- timeoutMs: number,
131
- ): Promise<void> {
132
- const data = await getStatusSnapshot(adminUrl, timeoutMs, options.name);
133
-
134
- if (options.json) {
135
- const filtered = filterForJson(data.metrics, data.state, options.instance);
136
- writeOutput(JSON.stringify(filtered.payload, null, 2));
137
- if (filtered.missingInstance) {
138
- process.exitCode = 1;
139
- }
140
- return;
141
- }
142
-
143
- if (options.instance) {
144
- renderInstanceDetail(data.metrics, data.state, options.instance);
145
- } else {
146
- renderOverview(data.metrics, data.state);
147
- }
148
- }
149
-
150
- // ---------------------------------------------------------------------------
151
- // Watch mode: continuous updates
152
- // ---------------------------------------------------------------------------
153
-
154
- async function watchMode(
155
- adminUrl: string,
156
- options: StatusOptions,
157
- timeoutMs: number,
158
- ): Promise<void> {
159
- let firstRender = true;
160
- let stopped = false;
161
- const stop = (): void => {
162
- stopped = true;
163
- };
164
-
165
- process.once("SIGINT", stop);
166
- process.once("SIGTERM", stop);
167
-
168
- try {
169
- while (!stopped) {
170
- const snapshot = await getStatusSnapshot(adminUrl, timeoutMs, options.name);
171
- renderFrame(snapshot.metrics, snapshot.state, options, firstRender);
172
- firstRender = false;
173
-
174
- if (!stopped) {
175
- await Bun.sleep(STATUS_WATCH_INTERVAL_MS);
176
- }
177
- }
178
- } finally {
179
- process.off("SIGINT", stop);
180
- process.off("SIGTERM", stop);
181
- }
182
- }
183
-
184
- function renderFrame(
185
- metrics: MetricsSnapshot,
186
- state: StateSnapshot,
187
- options: StatusOptions,
188
- _isFirst: boolean,
189
- ): void {
190
- if (options.json) {
191
- // NDJSON: one JSON object per line
192
- const filtered = filterForJson(metrics, state, options.instance);
193
- writeOutput(JSON.stringify(filtered.payload));
194
- if (filtered.missingInstance) {
195
- process.exitCode = 1;
196
- }
197
- return;
198
- }
199
-
200
- // Clear screen for re-render
201
- if (process.stdout.isTTY && !isPlain()) {
202
- process.stdout.write("\x1B[2J\x1B[H");
203
- }
204
-
205
- if (options.instance) {
206
- renderInstanceDetail(metrics, state, options.instance);
207
- } else {
208
- renderOverview(metrics, state);
209
- }
210
- }
211
-
212
- // ---------------------------------------------------------------------------
213
- // JSON output filtering
214
- // ---------------------------------------------------------------------------
215
-
216
- /**
217
- * When `--instance` is combined with `--json`, narrow the output to the
218
- * single requested instance instead of dumping the full fleet payload.
219
- */
220
- function filterForJson(
221
- metrics: MetricsSnapshot,
222
- state: StateSnapshot,
223
- instanceId?: string,
224
- ): { readonly payload: unknown; readonly missingInstance: boolean } {
225
- if (!instanceId) {
226
- return { payload: { metrics, state }, missingInstance: false };
227
- }
228
-
229
- const instance = state.instances.find((i) => i.id === instanceId);
230
- if (!instance) {
231
- return {
232
- payload: {
233
- error: `Instance "${instanceId}" not found`,
234
- available: state.instances.map((i) => i.id),
235
- },
236
- missingInstance: true,
237
- };
238
- }
239
-
240
- return {
241
- payload: {
242
- metrics: metrics.core,
243
- instance,
244
- ...(state.config.template ? { template: state.config.template } : {}),
245
- },
246
- missingInstance: false,
247
- };
248
- }
249
-
250
- async function getStatusSnapshot(
251
- adminUrl: string,
252
- timeoutMs: number,
253
- expectedName?: string,
254
- ): Promise<{ readonly metrics: MetricsSnapshot; readonly state: StateSnapshot }> {
255
- const snapshot = await getAdminSnapshot(adminUrl, timeoutMs);
256
- if (expectedName && snapshot.state.simulationName !== expectedName) {
257
- throw new MiravoError(
258
- `Connected to simulation "${snapshot.state.simulationName}" instead of "${expectedName}".`,
259
- "Pass --admin-url for the correct Miravo process, or omit --name to inspect the connected simulation.",
260
- );
261
- }
262
- return snapshot;
263
- }
264
-
265
- // ---------------------------------------------------------------------------
266
- // Render: overview
267
- // ---------------------------------------------------------------------------
268
-
269
- function renderOverview(metrics: MetricsSnapshot, state: StateSnapshot): void {
270
- const { core, fleet } = metrics;
271
- const mqttAdapter = metrics.adapters.find((a) => a.name === "mqtt");
272
-
273
- const sep = colors.dim(" | ");
274
-
275
- // Header
276
- writeOutput();
277
- writeOutput(` ${colors.bold(colors.cyan("Miravo Status"))}`);
278
- writeOutput();
279
-
280
- // Engine line
281
- const stateColor =
282
- core.state === "running" ? colors.green : core.state === "paused" ? colors.yellow : colors.red;
283
- const tps =
284
- core.actualTicksPerSecond > 0 ? `${core.actualTicksPerSecond.toFixed(1)} ticks/s` : "";
285
-
286
- writeOutput(
287
- formatKeyValue([
288
- [
289
- colors.dim("Engine"),
290
- `${stateColor(core.state)}${sep}tick #${formatCount(core.tickCount)}${sep}uptime: ${formatDuration(core.uptimeMs)}${tps ? sep + tps : ""}`,
291
- ],
292
- [
293
- colors.dim("Fleet"),
294
- `${colors.bold(String(core.instanceCount))} instances${sep}${colors.bold(String(core.memberValueCount))} data points`,
295
- ],
296
- ]),
297
- );
298
-
299
- if (state.config.template) {
300
- const templateLabel = `${colors.bold(state.config.template.name)}${sep}${colors.dim(state.config.template.path)}`;
301
- writeOutput(formatKeyValue([[colors.dim("Template"), templateLabel]]));
302
- }
303
-
304
- // Health
305
- const lifecycleEntries = Object.entries(fleet.lifecycleBreakdown);
306
- if (lifecycleEntries.length > 0) {
307
- const healthStr = lifecycleEntries.map(([stage, count]) => `${count} ${stage}`).join(", ");
308
- writeOutput(formatKeyValue([[colors.dim("Health"), healthStr]]));
309
- }
310
-
311
- // Faults
312
- if (fleet.activeFaultCount > 0) {
313
- const faultStr = fleet.faultBreakdown.map((f) => `${f.fault} (${f.count})`).join(", ");
314
- writeOutput(
315
- formatKeyValue([
316
- [
317
- colors.dim("Faults"),
318
- `${colors.yellow(String(fleet.activeFaultCount))} active: ${faultStr}`,
319
- ],
320
- ]),
321
- );
322
- } else {
323
- writeOutput(formatKeyValue([[colors.dim("Faults"), colors.green("none")]]));
324
- }
325
-
326
- // MQTT
327
- if (mqttAdapter) {
328
- const connStr = mqttAdapter.metrics.connected
329
- ? colors.green("connected")
330
- : colors.red("disconnected");
331
- const errStr =
332
- mqttAdapter.metrics.errorsCount > 0
333
- ? colors.yellow(String(mqttAdapter.metrics.errorsCount))
334
- : colors.green("0");
335
- const healthStr = formatHealthSuffix(mqttAdapter.metrics, sep);
336
- writeOutput(
337
- formatKeyValue([
338
- [
339
- colors.dim("MQTT"),
340
- `${connStr}${sep}${formatCount(mqttAdapter.metrics.messagesPublished)} published${sep}${errStr} errors${healthStr}`,
341
- ],
342
- ]),
343
- );
344
- }
345
-
346
- // OPC UA
347
- const opcuaAdapter = metrics.adapters.find((a) => a.name === "opcua");
348
- if (opcuaAdapter) {
349
- const connStr = opcuaAdapter.metrics.connected
350
- ? colors.green("connected")
351
- : colors.red("disconnected");
352
- const errStr =
353
- opcuaAdapter.metrics.errorsCount > 0
354
- ? colors.yellow(String(opcuaAdapter.metrics.errorsCount))
355
- : colors.green("0");
356
- const healthStr = formatHealthSuffix(opcuaAdapter.metrics, sep);
357
- writeOutput(
358
- formatKeyValue([
359
- [
360
- colors.dim("OPC UA"),
361
- `${connStr}${sep}${formatCount(opcuaAdapter.metrics.messagesPublished)} updates${sep}${errStr} errors${healthStr}`,
362
- ],
363
- ]),
364
- );
365
- }
366
-
367
- // Errors
368
- if (
369
- core.engineErrorsCount > 0 ||
370
- core.tickOverrunsCount > 0 ||
371
- core.circuitBreakerTripsCount > 0
372
- ) {
373
- writeOutput(
374
- formatKeyValue([
375
- [
376
- colors.dim("Errors"),
377
- `${core.engineErrorsCount} engine${sep}${core.tickOverrunsCount} overruns${sep}${core.circuitBreakerTripsCount} breaker trips`,
378
- ],
379
- ]),
380
- );
381
- }
382
-
383
- // Instances table
384
- if (state.instances.length > 0) {
385
- const hasQuarantined = state.instances.some((i) => i.isQuarantined);
386
-
387
- writeOutput();
388
- writeOutput(` ${colors.bold("Instances:")}`);
389
- const rows = state.instances.map((inst) => ({
390
- id: colors.cyan(inst.id),
391
- model: inst.model,
392
- stage: inst.lifecycleStage,
393
- faults:
394
- inst.activeFaults.length > 0
395
- ? colors.yellow(inst.activeFaults.join(", "))
396
- : colors.dim("none"),
397
- hours: `${inst.operatingHours.toFixed(0)}h`,
398
- ...(hasQuarantined ? { quarantined: inst.isQuarantined ? colors.red("yes") : "" } : {}),
399
- }));
400
-
401
- const headers: Record<string, string> = {
402
- id: colors.dim("ID"),
403
- model: colors.dim("Model"),
404
- stage: colors.dim("Stage"),
405
- faults: colors.dim("Faults"),
406
- hours: colors.dim("Hours"),
407
- };
408
- if (hasQuarantined) {
409
- headers["quarantined"] = colors.dim("Quarantined");
410
- }
411
-
412
- writeOutput(formatTable(headers, rows));
413
- }
414
-
415
- writeOutput();
416
- }
417
-
418
- // ---------------------------------------------------------------------------
419
- // Render: instance detail
420
- // ---------------------------------------------------------------------------
421
-
422
- function renderInstanceDetail(
423
- metrics: MetricsSnapshot,
424
- state: StateSnapshot,
425
- instanceId: string,
426
- ): void {
427
- const instance = state.instances.find((i) => i.id === instanceId);
428
- if (!instance) {
429
- const available = state.instances.map((i) => i.id).join(", ");
430
- engineLog.error(`Instance "${instanceId}" not found. Available: ${available || "(none)"}`);
431
- process.exitCode = 1;
432
- return;
433
- }
434
-
435
- const { core } = metrics;
436
-
437
- writeOutput();
438
- writeOutput(
439
- ` ${colors.bold(colors.cyan(`Instance: ${instance.id}`))} ${colors.dim(`(${instance.model})`)}`,
440
- );
441
- writeOutput();
442
-
443
- writeOutput(
444
- formatKeyValue([
445
- [colors.dim("UNS Path"), instance.browsePath.join("/")],
446
- [
447
- colors.dim("Lifecycle"),
448
- `${instance.lifecycleStage} (${formatCount(Math.floor(instance.operatingHours))} operating hours)`,
449
- ],
450
- [
451
- colors.dim("Faults"),
452
- instance.activeFaults.length > 0
453
- ? colors.yellow(instance.activeFaults.join(", "))
454
- : colors.green("none"),
455
- ],
456
- [colors.dim("Quarantined"), instance.isQuarantined ? colors.red("yes") : colors.green("no")],
457
- [colors.dim("Engine"), `${core.state} · tick #${formatCount(core.tickCount)}`],
458
- ...(state.config.template
459
- ? [
460
- [
461
- colors.dim("Template"),
462
- `${state.config.template.name} (${state.config.template.path})`,
463
- ] as const,
464
- ]
465
- : []),
466
- ]),
467
- );
468
-
469
- writeOutput();
470
- }
@@ -1,84 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdtemp, rm } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import type { DaemonRegistryEntry } from "../lib/daemon-registry.ts";
6
- import { readEntry, writeEntry } from "../lib/daemon-registry.ts";
7
- import { resetExitCode, runCliCommand } from "./cli-test-setup.ts";
8
-
9
- let testHome: string;
10
- const originalFetch = globalThis.fetch;
11
- const originalKill = process.kill;
12
- const forwardKill = originalKill as (pid: number, signal?: number | NodeJS.Signals) => void;
13
-
14
- function makeEntry(overrides: Partial<DaemonRegistryEntry> = {}): DaemonRegistryEntry {
15
- return {
16
- name: "test-sim",
17
- pid: 42_424,
18
- adminPort: 8080,
19
- adminHost: "127.0.0.1",
20
- adapters: {},
21
- templatePath: "/tmp/test.miravo.yaml",
22
- startedAt: "2026-03-18T10:00:00Z",
23
- seed: 42,
24
- runId: "run-123",
25
- ...overrides,
26
- };
27
- }
28
-
29
- beforeEach(resetExitCode);
30
-
31
- afterEach(async () => {
32
- globalThis.fetch = originalFetch;
33
- process.kill = originalKill;
34
- delete Bun.env["MIRAVO_HOME"];
35
- resetExitCode();
36
- if (testHome) {
37
- await rm(testHome, { recursive: true, force: true });
38
- }
39
- });
40
-
41
- describe("miravo stop", () => {
42
- test("force-kills a verified daemon after /shutdown makes /readyz disappear", async () => {
43
- testHome = await mkdtemp(join(tmpdir(), "miravo-stop-test-"));
44
- Bun.env["MIRAVO_HOME"] = testHome;
45
-
46
- let alive = true;
47
- process.kill = ((pid: number, signal?: number | NodeJS.Signals) => {
48
- if (pid !== 42_424) {
49
- return forwardKill(pid, signal);
50
- }
51
- if (signal === 0 || signal === undefined) {
52
- if (!alive) {
53
- const error = new Error("ESRCH") as NodeJS.ErrnoException;
54
- error.code = "ESRCH";
55
- throw error;
56
- }
57
- return;
58
- }
59
- if (signal === "SIGKILL") {
60
- alive = false;
61
- }
62
- }) as typeof process.kill;
63
-
64
- globalThis.fetch = (async (input, init) => {
65
- const url = String(input);
66
- if (url.endsWith("/readyz")) {
67
- return Response.json({ status: "paused", runId: "run-123" }, { status: 503 });
68
- }
69
- if (url.endsWith("/shutdown")) {
70
- expect(init?.method).toBe("POST");
71
- return Response.json({ status: "shutting_down" }, { status: 200 });
72
- }
73
- throw new Error(`Unexpected fetch: ${url}`);
74
- }) as typeof fetch;
75
-
76
- await writeEntry(makeEntry());
77
-
78
- await runCliCommand(["stop", "test-sim", "--timeout", "0.001"]);
79
-
80
- expect(alive).toBe(false);
81
- expect(await readEntry("test-sim")).toBeNull();
82
- expect(process.exitCode).toBe(0);
83
- });
84
- });