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.
- package/LICENSE +199 -0
- package/README.md +29 -59
- package/bin/miravo.js +84 -0
- package/package.json +21 -37
- package/console-dist/assets/activity-DsWeRq4h.js +0 -1
- package/console-dist/assets/api-4AqKvsm9.js +0 -1
- package/console-dist/assets/arrow-right-Ea_UhTNi.js +0 -1
- package/console-dist/assets/arrow-up-down-DFSjzzl2.js +0 -1
- package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +0 -1
- package/console-dist/assets/box-C1WaoOMF.js +0 -1
- package/console-dist/assets/card-BjeP32PN.js +0 -1
- package/console-dist/assets/catalog-BJzyzbLR.js +0 -1
- package/console-dist/assets/catalog-DKjEm-c4.js +0 -1
- package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +0 -1
- package/console-dist/assets/checkbox-DZlJXTE4.js +0 -1
- package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +0 -3
- package/console-dist/assets/combobox-BGnuCYxO.js +0 -1
- package/console-dist/assets/control-CPhvwx9o.js +0 -1
- package/console-dist/assets/control-Cg2up9NA.js +0 -1
- package/console-dist/assets/copy-button-WkbNkMcd.js +0 -1
- package/console-dist/assets/createLucideIcon-Bwxw859K.js +0 -1
- package/console-dist/assets/deploy-tab-JeWDwPoZ.js +0 -1
- package/console-dist/assets/dist-BhbDML0a.js +0 -1
- package/console-dist/assets/dist-Cy8XTNFT.js +0 -2
- package/console-dist/assets/download-DQ6NH16U.js +0 -1
- package/console-dist/assets/engine-store-CrLnz692.js +0 -1
- package/console-dist/assets/events-CXm29Ok4.js +0 -1
- package/console-dist/assets/events-D01IxhnP.js +0 -1
- package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
- package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
- package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
- package/console-dist/assets/fleet-CfxbKXt9.js +0 -1
- package/console-dist/assets/fleet-lotMGVqe.js +0 -1
- package/console-dist/assets/format-kXTonpDJ.js +0 -1
- package/console-dist/assets/gauge-Ci8zNZlk.js +0 -1
- package/console-dist/assets/get-error-message-3Q5Q-2G5.js +0 -1
- package/console-dist/assets/index-CVPKSDvJ.js +0 -12
- package/console-dist/assets/index-DJkgDeun.css +0 -2
- package/console-dist/assets/input-BWEQFptx.js +0 -1
- package/console-dist/assets/instance-detail-C5IVwuh3.js +0 -1
- package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
- package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
- package/console-dist/assets/label-uephX5ZG.js +0 -1
- package/console-dist/assets/layers-CUsLQ1fq.js +0 -1
- package/console-dist/assets/login-C3RJli0m.js +0 -126
- package/console-dist/assets/models-B_UioWQ5.js +0 -1
- package/console-dist/assets/models-_IPlPiZ7.js +0 -1
- package/console-dist/assets/not-found-B8gAm08S.js +0 -1
- package/console-dist/assets/platform-targets-R3T-4eJ7.js +0 -1
- package/console-dist/assets/platforms-BWh0BQ3Y.js +0 -1
- package/console-dist/assets/platforms-DStNbtVc.js +0 -1
- package/console-dist/assets/proxy-CsnYqnPp.js +0 -9
- package/console-dist/assets/query-keys-BvtE7BMs.js +0 -1
- package/console-dist/assets/radio-CoHegOv5.js +0 -1
- package/console-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
- package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
- package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
- package/console-dist/assets/select-CeO4vP0G.js +0 -1
- package/console-dist/assets/select-display-items-CIzY8953.js +0 -1
- package/console-dist/assets/server-D9Kfn8fQ.js +0 -1
- package/console-dist/assets/shallow-6lGjQhtJ.js +0 -1
- package/console-dist/assets/tags-CiXOIHtY.js +0 -4
- package/console-dist/assets/tags-FQCXIxrP.js +0 -1
- package/console-dist/assets/tooltip-HGLF366Q.js +0 -1
- package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +0 -1
- package/console-dist/assets/transition-types-CPlGtM8w.js +0 -1
- package/console-dist/assets/triangle-alert-adNk_sea.js +0 -1
- package/console-dist/assets/use-platform-stream-DGbK1k2L.js +0 -2
- package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +0 -2
- package/console-dist/assets/validation-Blvud9_O.js +0 -1
- package/console-dist/assets/vendor-charts-CAZdaPlq.js +0 -60
- package/console-dist/assets/vendor-table-wd8JuMmT.js +0 -4
- package/console-dist/assets/vendor-ui-ByahnaY0.js +0 -1
- package/console-dist/favicon.svg +0 -37
- package/console-dist/index.html +0 -82
- package/console-dist/miravo-logo.png +0 -0
- package/src/__tests__/integration/executable-methods-runner.ts +0 -262
- package/src/__tests__/integration/executable-methods.integration.ts +0 -2348
- package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +0 -314
- package/src/commands/adapter.ts +0 -249
- package/src/commands/apply.test.ts +0 -62
- package/src/commands/apply.ts +0 -216
- package/src/commands/cleanup.test.ts +0 -34
- package/src/commands/cleanup.ts +0 -201
- package/src/commands/cli-test-setup.ts +0 -91
- package/src/commands/export.test.ts +0 -51
- package/src/commands/export.ts +0 -404
- package/src/commands/fork.ts +0 -219
- package/src/commands/inject.test.ts +0 -57
- package/src/commands/inject.ts +0 -212
- package/src/commands/load.ts +0 -167
- package/src/commands/logs.ts +0 -173
- package/src/commands/models.test.ts +0 -162
- package/src/commands/models.ts +0 -338
- package/src/commands/pause.ts +0 -67
- package/src/commands/platforms.test.ts +0 -186
- package/src/commands/platforms.ts +0 -371
- package/src/commands/ps.ts +0 -116
- package/src/commands/remove.ts +0 -38
- package/src/commands/reset.ts +0 -44
- package/src/commands/rm.ts +0 -97
- package/src/commands/spawn.ts +0 -45
- package/src/commands/speed.ts +0 -40
- package/src/commands/start/adapter-config.ts +0 -157
- package/src/commands/start/catalog-le-dt.test.ts +0 -50
- package/src/commands/start/catalog-le-dt.ts +0 -187
- package/src/commands/start/catalog-model-schema.ts +0 -89
- package/src/commands/start/catalog-template-detail.ts +0 -54
- package/src/commands/start/daemon.ts +0 -255
- package/src/commands/start/display.ts +0 -150
- package/src/commands/start/engine-setup.ts +0 -247
- package/src/commands/start/events.ts +0 -126
- package/src/commands/start/index.ts +0 -897
- package/src/commands/start/protocol-plans.ts +0 -239
- package/src/commands/start/template-loader.ts +0 -197
- package/src/commands/start/template-resolver.ts +0 -185
- package/src/commands/start/types.ts +0 -73
- package/src/commands/start.test.ts +0 -480
- package/src/commands/status.test.ts +0 -20
- package/src/commands/status.ts +0 -470
- package/src/commands/stop.test.ts +0 -84
- package/src/commands/stop.ts +0 -169
- package/src/commands/templates.test.ts +0 -58
- package/src/commands/templates.ts +0 -277
- package/src/index.ts +0 -166
- package/src/lib/admin-auth.ts +0 -157
- package/src/lib/admin-client.test.ts +0 -51
- package/src/lib/admin-client.ts +0 -326
- package/src/lib/admin-platform-routes.ts +0 -1017
- package/src/lib/admin-platform-stream.test.ts +0 -434
- package/src/lib/admin-platform-stream.ts +0 -788
- package/src/lib/admin-responses.ts +0 -211
- package/src/lib/admin-server.test.ts +0 -781
- package/src/lib/admin-server.ts +0 -797
- package/src/lib/auth-store.ts +0 -87
- package/src/lib/daemon-registry.test.ts +0 -429
- package/src/lib/daemon-registry.ts +0 -343
- package/src/lib/le/le-apply.test.ts +0 -591
- package/src/lib/le/le-apply.ts +0 -832
- package/src/lib/le/le-auth.ts +0 -89
- package/src/lib/le/le-cleanup.test.ts +0 -168
- package/src/lib/le/le-cleanup.ts +0 -399
- package/src/lib/le/le-cli-helpers.ts +0 -77
- package/src/lib/le/le-client.ts +0 -184
- package/src/lib/le/le-concurrency.test.ts +0 -157
- package/src/lib/le/le-concurrency.ts +0 -78
- package/src/lib/le/le-connection-test.ts +0 -42
- package/src/lib/le/le-devicehub-properties.test.ts +0 -53
- package/src/lib/le/le-devicehub-properties.ts +0 -33
- package/src/lib/le/le-devicehub.ts +0 -213
- package/src/lib/le/le-digital-twins.ts +0 -285
- package/src/lib/le/le-drivers.ts +0 -23
- package/src/lib/le/le-endpoint.ts +0 -40
- package/src/lib/le/le-environments.test.ts +0 -73
- package/src/lib/le/le-environments.ts +0 -156
- package/src/lib/le/le-markers.ts +0 -87
- package/src/lib/le/le-plan.test.ts +0 -26
- package/src/lib/le/le-plan.ts +0 -109
- package/src/lib/le/le-platform-operations.test.ts +0 -198
- package/src/lib/le/le-platform-operations.ts +0 -340
- package/src/lib/le/le-static-model-schema.ts +0 -78
- package/src/lib/le/le-static-planner.test.ts +0 -255
- package/src/lib/le/le-static-planner.ts +0 -442
- package/src/lib/le/le-template-exporter.test.ts +0 -527
- package/src/lib/le/le-template-exporter.ts +0 -459
- package/src/lib/le/le-value-types.ts +0 -23
- package/src/lib/mqtt-client.ts +0 -115
- package/src/lib/parse.test.ts +0 -123
- package/src/lib/parse.ts +0 -87
- package/src/lib/resolve-simulation.test.ts +0 -118
- package/src/lib/resolve-simulation.ts +0 -181
- package/src/lib/session-store.ts +0 -110
- package/src/lib/shutdown.test.ts +0 -562
- package/src/lib/shutdown.ts +0 -621
- package/src/lib/template-category.test.ts +0 -71
- package/src/lib/template-category.ts +0 -72
- package/src/lib/template-locator.ts +0 -55
- package/src/lib/template-utils.ts +0 -45
- package/src/lib/ws-hub.ts +0 -281
- package/src/logger.test.ts +0 -172
- package/src/logger.ts +0 -402
- package/src/output.test.ts +0 -64
- package/src/output.ts +0 -160
package/src/commands/status.ts
DELETED
|
@@ -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
|
-
});
|