miravo 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/README.md +1 -1
  2. package/console-dist/assets/activity-DsWeRq4h.js +1 -0
  3. package/console-dist/assets/api-4AqKvsm9.js +1 -0
  4. package/console-dist/assets/arrow-right-Ea_UhTNi.js +1 -0
  5. package/console-dist/assets/arrow-up-down-DFSjzzl2.js +1 -0
  6. package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +1 -0
  7. package/console-dist/assets/box-C1WaoOMF.js +1 -0
  8. package/console-dist/assets/card-BjeP32PN.js +1 -0
  9. package/console-dist/assets/catalog-BJzyzbLR.js +1 -0
  10. package/console-dist/assets/catalog-DKjEm-c4.js +1 -0
  11. package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +1 -0
  12. package/console-dist/assets/checkbox-DZlJXTE4.js +1 -0
  13. package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +3 -0
  14. package/console-dist/assets/combobox-BGnuCYxO.js +1 -0
  15. package/console-dist/assets/control-CPhvwx9o.js +1 -0
  16. package/console-dist/assets/control-Cg2up9NA.js +1 -0
  17. package/console-dist/assets/copy-button-WkbNkMcd.js +1 -0
  18. package/console-dist/assets/createLucideIcon-Bwxw859K.js +1 -0
  19. package/console-dist/assets/deploy-tab-JeWDwPoZ.js +1 -0
  20. package/console-dist/assets/dist-BhbDML0a.js +1 -0
  21. package/console-dist/assets/dist-Cy8XTNFT.js +2 -0
  22. package/console-dist/assets/download-DQ6NH16U.js +1 -0
  23. package/console-dist/assets/engine-store-CrLnz692.js +1 -0
  24. package/console-dist/assets/events-CXm29Ok4.js +1 -0
  25. package/console-dist/assets/events-D01IxhnP.js +1 -0
  26. package/console-dist/assets/fleet-CfxbKXt9.js +1 -0
  27. package/console-dist/assets/fleet-lotMGVqe.js +1 -0
  28. package/console-dist/assets/format-kXTonpDJ.js +1 -0
  29. package/console-dist/assets/gauge-Ci8zNZlk.js +1 -0
  30. package/console-dist/assets/get-error-message-3Q5Q-2G5.js +1 -0
  31. package/console-dist/assets/index-CVPKSDvJ.js +12 -0
  32. package/console-dist/assets/index-DJkgDeun.css +2 -0
  33. package/console-dist/assets/input-BWEQFptx.js +1 -0
  34. package/console-dist/assets/instance-detail-C5IVwuh3.js +1 -0
  35. package/console-dist/assets/label-uephX5ZG.js +1 -0
  36. package/console-dist/assets/layers-CUsLQ1fq.js +1 -0
  37. package/console-dist/assets/login-C3RJli0m.js +126 -0
  38. package/console-dist/assets/models-B_UioWQ5.js +1 -0
  39. package/console-dist/assets/models-_IPlPiZ7.js +1 -0
  40. package/console-dist/assets/not-found-B8gAm08S.js +1 -0
  41. package/console-dist/assets/platform-targets-R3T-4eJ7.js +1 -0
  42. package/console-dist/assets/platforms-BWh0BQ3Y.js +1 -0
  43. package/console-dist/assets/platforms-DStNbtVc.js +1 -0
  44. package/console-dist/assets/proxy-CsnYqnPp.js +9 -0
  45. package/console-dist/assets/query-keys-BvtE7BMs.js +1 -0
  46. package/console-dist/assets/radio-CoHegOv5.js +1 -0
  47. package/console-dist/assets/rolldown-runtime-COnpUsM8.js +1 -0
  48. package/console-dist/assets/select-CeO4vP0G.js +1 -0
  49. package/console-dist/assets/select-display-items-CIzY8953.js +1 -0
  50. package/console-dist/assets/server-D9Kfn8fQ.js +1 -0
  51. package/console-dist/assets/shallow-6lGjQhtJ.js +1 -0
  52. package/console-dist/assets/tags-CiXOIHtY.js +4 -0
  53. package/console-dist/assets/tags-FQCXIxrP.js +1 -0
  54. package/console-dist/assets/tooltip-HGLF366Q.js +1 -0
  55. package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +1 -0
  56. package/console-dist/assets/transition-types-CPlGtM8w.js +1 -0
  57. package/console-dist/assets/triangle-alert-adNk_sea.js +1 -0
  58. package/console-dist/assets/use-platform-stream-DGbK1k2L.js +2 -0
  59. package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +2 -0
  60. package/console-dist/assets/validation-Blvud9_O.js +1 -0
  61. package/console-dist/assets/vendor-charts-CAZdaPlq.js +60 -0
  62. package/console-dist/assets/vendor-table-wd8JuMmT.js +4 -0
  63. package/console-dist/assets/vendor-ui-ByahnaY0.js +1 -0
  64. package/console-dist/index.html +70 -2
  65. package/package.json +9 -8
  66. package/src/__tests__/integration/executable-methods-runner.ts +7 -6
  67. package/src/__tests__/integration/executable-methods.integration.ts +214 -122
  68. package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +314 -0
  69. package/src/commands/adapter.ts +180 -7
  70. package/src/commands/apply.test.ts +62 -0
  71. package/src/commands/apply.ts +216 -0
  72. package/src/commands/cleanup.test.ts +34 -0
  73. package/src/commands/cleanup.ts +201 -0
  74. package/src/commands/export.test.ts +51 -0
  75. package/src/commands/export.ts +404 -0
  76. package/src/commands/fork.ts +2 -2
  77. package/src/commands/inject.ts +2 -2
  78. package/src/commands/load.ts +1 -1
  79. package/src/commands/logs.ts +13 -15
  80. package/src/commands/models.test.ts +1 -1
  81. package/src/commands/models.ts +1 -1
  82. package/src/commands/pause.ts +3 -3
  83. package/src/commands/platforms.test.ts +186 -0
  84. package/src/commands/platforms.ts +371 -0
  85. package/src/commands/remove.ts +1 -1
  86. package/src/commands/reset.ts +2 -2
  87. package/src/commands/spawn.ts +2 -2
  88. package/src/commands/speed.ts +2 -2
  89. package/src/commands/start/adapter-config.ts +157 -0
  90. package/src/commands/start/catalog-le-dt.test.ts +50 -0
  91. package/src/commands/start/catalog-le-dt.ts +187 -0
  92. package/src/commands/start/catalog-model-schema.ts +89 -0
  93. package/src/commands/start/catalog-template-detail.ts +54 -0
  94. package/src/commands/start/daemon.ts +255 -0
  95. package/src/commands/start/display.ts +150 -0
  96. package/src/commands/start/engine-setup.ts +247 -0
  97. package/src/commands/start/events.ts +126 -0
  98. package/src/commands/start/index.ts +897 -0
  99. package/src/commands/start/protocol-plans.ts +239 -0
  100. package/src/commands/start/template-loader.ts +197 -0
  101. package/src/commands/start/template-resolver.ts +185 -0
  102. package/src/commands/start/types.ts +73 -0
  103. package/src/commands/start.test.ts +137 -16
  104. package/src/commands/status.ts +1 -1
  105. package/src/commands/stop.ts +19 -8
  106. package/src/commands/templates.test.ts +1 -1
  107. package/src/commands/templates.ts +1 -3
  108. package/src/index.ts +11 -3
  109. package/src/lib/admin-auth.ts +157 -0
  110. package/src/lib/admin-client.test.ts +31 -15
  111. package/src/lib/admin-client.ts +106 -6
  112. package/src/lib/admin-platform-routes.ts +1017 -0
  113. package/src/lib/admin-platform-stream.test.ts +434 -0
  114. package/src/lib/admin-platform-stream.ts +788 -0
  115. package/src/lib/admin-responses.ts +211 -0
  116. package/src/lib/admin-server.test.ts +274 -55
  117. package/src/lib/admin-server.ts +387 -248
  118. package/src/lib/auth-store.ts +87 -0
  119. package/src/lib/daemon-registry.ts +12 -14
  120. package/src/lib/le/le-apply.test.ts +591 -0
  121. package/src/lib/le/le-apply.ts +832 -0
  122. package/src/lib/le/le-auth.ts +89 -0
  123. package/src/lib/le/le-cleanup.test.ts +168 -0
  124. package/src/lib/le/le-cleanup.ts +399 -0
  125. package/src/lib/le/le-cli-helpers.ts +77 -0
  126. package/src/lib/le/le-client.ts +184 -0
  127. package/src/lib/le/le-concurrency.test.ts +157 -0
  128. package/src/lib/le/le-concurrency.ts +78 -0
  129. package/src/lib/le/le-connection-test.ts +42 -0
  130. package/src/lib/le/le-devicehub-properties.test.ts +53 -0
  131. package/src/lib/le/le-devicehub-properties.ts +33 -0
  132. package/src/lib/le/le-devicehub.ts +213 -0
  133. package/src/lib/le/le-digital-twins.ts +285 -0
  134. package/src/lib/le/le-drivers.ts +23 -0
  135. package/src/lib/le/le-endpoint.ts +40 -0
  136. package/src/lib/le/le-environments.test.ts +73 -0
  137. package/src/lib/le/le-environments.ts +156 -0
  138. package/src/lib/le/le-markers.ts +87 -0
  139. package/src/lib/le/le-plan.test.ts +26 -0
  140. package/src/lib/le/le-plan.ts +109 -0
  141. package/src/lib/le/le-platform-operations.test.ts +198 -0
  142. package/src/lib/le/le-platform-operations.ts +340 -0
  143. package/src/lib/le/le-static-model-schema.ts +78 -0
  144. package/src/lib/le/le-static-planner.test.ts +255 -0
  145. package/src/lib/le/le-static-planner.ts +442 -0
  146. package/src/lib/le/le-template-exporter.test.ts +527 -0
  147. package/src/lib/le/le-template-exporter.ts +459 -0
  148. package/src/lib/le/le-value-types.ts +23 -0
  149. package/src/lib/mqtt-client.ts +1 -2
  150. package/src/lib/resolve-simulation.ts +1 -6
  151. package/src/lib/session-store.ts +110 -0
  152. package/src/lib/shutdown.test.ts +184 -2
  153. package/src/lib/shutdown.ts +104 -34
  154. package/src/lib/template-category.ts +3 -3
  155. package/src/lib/template-locator.ts +8 -2
  156. package/src/lib/ws-hub.ts +72 -46
  157. package/src/logger.ts +39 -14
  158. package/src/output.ts +3 -0
  159. package/console-dist/assets/index-C3XMYq26.css +0 -2
  160. package/console-dist/assets/index-hUJSpKtB.js +0 -119
  161. package/src/commands/start.ts +0 -2023
@@ -0,0 +1,404 @@
1
+ import type { ProtocolName } from "@miravo/shared";
2
+ import { CONTROL_CMD_TIMEOUT_MS } from "@miravo/shared";
3
+ import type { Command } from "commander";
4
+ import { getLeDtJson, getManifest, resolveAdminUrl } from "../lib/admin-client.ts";
5
+ import { LE_DRIVER_NAMES } from "../lib/le/le-drivers.ts";
6
+ import { buildDesiredStateFromTemplate } from "../lib/le/le-static-planner.ts";
7
+ import { buildLeExport } from "../lib/le/le-template-exporter.ts";
8
+ import { MODBUS_VALUE_TYPE, OPCUA_VALUE_TYPE } from "../lib/le/le-value-types.ts";
9
+ import { parseTimeoutSeconds } from "../lib/parse.ts";
10
+ import { createCatalog, resolveTemplateIdentifier } from "../lib/template-locator.ts";
11
+ import { handleCommandError, writeOutput } from "../logger.ts";
12
+
13
+ const VALID_PROTOCOLS = ["opcua", "modbus", "mqtt"] as const;
14
+
15
+ /** LE DeviceHub CSV header — matches real LE export format. */
16
+ const LE_CSV_HEADER =
17
+ "deviceId,deviceName,driverName,name,tagName,description,valueType,registerId,publishCoV,tagFormula,metaData,Retain,Qos,address,count,pollingInterval,endianness";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Command
21
+ // ---------------------------------------------------------------------------
22
+
23
+ interface ExportLeTagsOptions {
24
+ readonly device: string;
25
+ readonly protocol: string;
26
+ readonly instance?: string;
27
+ readonly output?: string;
28
+ readonly adminUrl?: string;
29
+ readonly timeout?: number;
30
+ }
31
+
32
+ export function registerExportCommand(program: Command): void {
33
+ const exportCmd = program
34
+ .command("export")
35
+ .description("Export simulation data in platform-specific formats");
36
+
37
+ // -------------------------------------------------------------------------
38
+ // miravo export le-template — offline full LE template (JSON + tags CSV)
39
+ // -------------------------------------------------------------------------
40
+
41
+ interface ExportLeOptions {
42
+ readonly template: string;
43
+ readonly host: string;
44
+ readonly protocol?: string;
45
+ readonly output?: string;
46
+ readonly modelsDir?: string;
47
+ }
48
+
49
+ exportCmd
50
+ .command("le-template")
51
+ .description(
52
+ "Export a full Litmus Edge template (devices + DT) and tags CSV — no running instance needed",
53
+ )
54
+ .requiredOption("-t, --template <id>", "Template ID, name, or path (required)")
55
+ .option("--host <hostname>", "Source hostname embedded in device endpoints", "localhost")
56
+ .option("--protocol <name>", "Protocol override: opcua, modbus, or mqtt")
57
+ .option("-o, --output <prefix>", "Output file prefix (default: <template-id>)")
58
+ .option("--models-dir <path>", "Extra directory to search for .twin.yaml model files")
59
+ .addHelpText(
60
+ "after",
61
+ `
62
+ Examples:
63
+ $ miravo export le-template -t smart-factory
64
+ $ miravo export le-template -t smart-factory --host 192.168.1.100 --protocol opcua
65
+ $ miravo export le-template -t ./my.miravo.yaml --models-dir ./models
66
+ $ miravo export le-template -t smart-factory -o ./out/sf`,
67
+ )
68
+ .action(async (options: ExportLeOptions) => {
69
+ try {
70
+ if (
71
+ options.protocol &&
72
+ !VALID_PROTOCOLS.includes(options.protocol as (typeof VALID_PROTOCOLS)[number])
73
+ ) {
74
+ writeOutput(`Unknown protocol "${options.protocol}". Valid options: opcua, modbus, mqtt`);
75
+ process.exitCode = 1;
76
+ return;
77
+ }
78
+
79
+ const catalog = options.modelsDir
80
+ ? createCatalog({ extraModelsDir: options.modelsDir })
81
+ : createCatalog();
82
+ const templatePath = await resolveTemplateIdentifier(options.template, catalog);
83
+ const { templateId, desired } = await buildDesiredStateFromTemplate({
84
+ templatePath,
85
+ catalog,
86
+ sourceHostname: options.host,
87
+ protocol: options.protocol as ProtocolName | undefined,
88
+ });
89
+
90
+ const { templateJson, tagsCSV } = buildLeExport(desired, templateId);
91
+
92
+ const prefix = options.output ?? templateId;
93
+ const jsonPath = `${prefix}.le-template.json`;
94
+ const csvPath = `${prefix}-tags.csv`;
95
+
96
+ await Bun.write(jsonPath, templateJson);
97
+ await Bun.write(csvPath, tagsCSV);
98
+
99
+ writeOutput(`LE template → ${jsonPath}`);
100
+ writeOutput(`Tags CSV → ${csvPath}`);
101
+ } catch (error) {
102
+ handleCommandError(error);
103
+ }
104
+ });
105
+
106
+ exportCmd
107
+ .command("le-tags")
108
+ .description("Export tags as Litmus Edge DeviceHub CSV for bulk import")
109
+ .requiredOption("--device <name>", "LE device name (required)")
110
+ .requiredOption("--protocol <name>", "Protocol to export: opcua, modbus, or mqtt (required)")
111
+ .option("--instance <id>", "Export only tags for a specific instance")
112
+ .option("-o, --output <file>", "Write CSV to file instead of stdout")
113
+ .option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
114
+ .option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
115
+ .addHelpText(
116
+ "after",
117
+ `
118
+ Examples:
119
+ $ miravo export le-tags --device DTW-FAB-PLC --protocol opcua
120
+ $ miravo export le-tags --device DTW-FAB-PLC --protocol modbus --instance pump-001
121
+ $ miravo export le-tags --device DTW-FAB-PLC --protocol mqtt -o tags.csv`,
122
+ )
123
+ .action(async (options: ExportLeTagsOptions) => {
124
+ try {
125
+ if (!VALID_PROTOCOLS.includes(options.protocol as (typeof VALID_PROTOCOLS)[number])) {
126
+ writeOutput(
127
+ `Unknown protocol "${options.protocol}". Valid options: ${VALID_PROTOCOLS.join(", ")}`,
128
+ );
129
+ process.exitCode = 1;
130
+ return;
131
+ }
132
+
133
+ const adminUrl = resolveAdminUrl(options.adminUrl);
134
+ const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
135
+ const manifest = await getManifest(adminUrl, timeoutMs);
136
+
137
+ const allTags = manifest["tags"] as Array<Record<string, unknown>> | undefined;
138
+
139
+ if (!allTags || allTags.length === 0) {
140
+ writeOutput("No tags found in manifest.");
141
+ return;
142
+ }
143
+
144
+ // Filter by instance if specified
145
+ const filteredTags = options.instance
146
+ ? allTags.filter((s) => s["instance"] === options.instance)
147
+ : allTags;
148
+
149
+ if (filteredTags.length === 0) {
150
+ writeOutput(
151
+ options.instance
152
+ ? `No tags found for instance "${options.instance}".`
153
+ : "No tags found in manifest.",
154
+ );
155
+ return;
156
+ }
157
+
158
+ const protocol = options.protocol;
159
+ const driverName = LE_DRIVER_NAMES[protocol as keyof typeof LE_DRIVER_NAMES] ?? protocol;
160
+ const deviceName = options.device;
161
+
162
+ const csvRows = [LE_CSV_HEADER];
163
+ let totalRows = 0;
164
+
165
+ for (const tag of filteredTags) {
166
+ const protocols = tag["protocols"] as Record<string, Record<string, unknown>> | undefined;
167
+ const memberName = String(tag["member"] ?? "");
168
+ const browsePath = String(tag["browsePath"] ?? "");
169
+ const dataType = String(tag["dataType"] ?? "");
170
+ const unit = typeof tag["unit"] === "string" ? tag["unit"] : undefined;
171
+ const euRangeRaw = tag["euRange"];
172
+ const euRange =
173
+ Array.isArray(euRangeRaw) &&
174
+ euRangeRaw.length === 2 &&
175
+ typeof euRangeRaw[0] === "number" &&
176
+ typeof euRangeRaw[1] === "number"
177
+ ? ([euRangeRaw[0], euRangeRaw[1]] as [number, number])
178
+ : undefined;
179
+ const model = String(tag["model"] ?? "");
180
+ const instance = String(tag["instance"] ?? "");
181
+ const access = String(tag["access"] ?? "");
182
+ const protocolData = protocols?.[protocol];
183
+ if (!protocolData) continue; // tag not exposed on this protocol (e.g. String on Modbus, property on MQTT)
184
+
185
+ csvRows.push(
186
+ buildRow(protocol, {
187
+ deviceName,
188
+ driverName,
189
+ memberName,
190
+ browsePath,
191
+ dataType,
192
+ protocolData,
193
+ model,
194
+ instance,
195
+ access,
196
+ ...(unit !== undefined && { unit }),
197
+ ...(euRange !== undefined && { euRange }),
198
+ }),
199
+ );
200
+ totalRows++;
201
+ }
202
+
203
+ const csv = `${csvRows.join("\n")}\n`;
204
+
205
+ if (options.output) {
206
+ await Bun.write(options.output, csv);
207
+ writeOutput(`Wrote ${totalRows} tags to ${options.output}`);
208
+ } else {
209
+ writeOutput(csv);
210
+ }
211
+ } catch (error) {
212
+ handleCommandError(error);
213
+ }
214
+ });
215
+
216
+ // -------------------------------------------------------------------------
217
+ // miravo export le-dt — export a model as Litmus Edge Digital Twin JSON
218
+ // -------------------------------------------------------------------------
219
+
220
+ exportCmd
221
+ .command("le-dt")
222
+ .description("Export a model as Litmus Edge Digital Twin JSON for import into LE")
223
+ .requiredOption("--model <name>", "Model name to export (required)")
224
+ .option("-o, --output <file>", "Write JSON to file instead of stdout")
225
+ .option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
226
+ .option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
227
+ .addHelpText(
228
+ "after",
229
+ `
230
+ Examples:
231
+ $ miravo export le-dt --model centrifugal-pump
232
+ $ miravo export le-dt --model centrifugal-pump -o pump.le-dt.json`,
233
+ )
234
+ .action(
235
+ async (options: {
236
+ readonly model: string;
237
+ readonly output?: string;
238
+ readonly adminUrl?: string;
239
+ readonly timeout?: number;
240
+ }) => {
241
+ try {
242
+ const adminUrl = resolveAdminUrl(options.adminUrl);
243
+ const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
244
+ const leDt = await getLeDtJson(adminUrl, options.model, timeoutMs);
245
+ const json = JSON.stringify(leDt, null, 2);
246
+
247
+ if (options.output) {
248
+ await Bun.write(options.output, json);
249
+ writeOutput(`Wrote LE Digital Twin JSON to ${options.output}`);
250
+ } else {
251
+ writeOutput(json);
252
+ }
253
+ } catch (error) {
254
+ handleCommandError(error);
255
+ }
256
+ },
257
+ );
258
+ }
259
+
260
+ // ---------------------------------------------------------------------------
261
+ // Row builder
262
+ // ---------------------------------------------------------------------------
263
+
264
+ interface RowInput {
265
+ readonly deviceName: string;
266
+ readonly driverName: string;
267
+ readonly memberName: string;
268
+ readonly browsePath: string;
269
+ readonly dataType: string;
270
+ readonly protocolData: Record<string, unknown> | undefined;
271
+ readonly unit?: string;
272
+ readonly euRange?: readonly [number, number];
273
+ readonly model: string;
274
+ readonly instance: string;
275
+ readonly access: string;
276
+ }
277
+
278
+ function buildRow(protocol: string, input: RowInput): string {
279
+ switch (protocol) {
280
+ case "mqtt":
281
+ return buildMqttRow(input);
282
+ case "opcua":
283
+ return buildOpcuaRow(input);
284
+ case "modbus":
285
+ return buildModbusRow(input);
286
+ default: {
287
+ const _exhaustive: never = protocol as never;
288
+ throw new Error(`Unsupported LE export protocol: "${_exhaustive}"`);
289
+ }
290
+ }
291
+ }
292
+
293
+ function buildMqttRow(input: RowInput): string {
294
+ const topic =
295
+ typeof input.protocolData?.["topic"] === "string"
296
+ ? input.protocolData["topic"]
297
+ : `${input.browsePath}/${input.memberName}`;
298
+
299
+ return [
300
+ "", // deviceId
301
+ esc(input.deviceName),
302
+ esc(input.driverName),
303
+ esc(topic), // name = MQTT topic
304
+ esc(input.memberName), // tagName
305
+ "", // description
306
+ "string", // valueType — MQTT is always string
307
+ "", // registerId
308
+ "false", // publishCoV
309
+ "", // tagFormula
310
+ buildMetaData(input), // metaData
311
+ "Disable", // Retain
312
+ "0", // Qos
313
+ "", // address
314
+ "", // count
315
+ "", // pollingInterval
316
+ "", // endianness
317
+ ].join(",");
318
+ }
319
+
320
+ function buildOpcuaRow(input: RowInput): string {
321
+ const nodeId =
322
+ typeof input.protocolData?.["nodeId"] === "string"
323
+ ? input.protocolData["nodeId"]
324
+ : `ns=1;s=${input.browsePath}/${input.memberName}`;
325
+ const valueType = OPCUA_VALUE_TYPE[input.dataType] ?? "string";
326
+
327
+ return [
328
+ "", // deviceId
329
+ esc(input.deviceName),
330
+ esc(input.driverName),
331
+ esc(nodeId), // name = OPC UA NodeId
332
+ esc(input.memberName), // tagName
333
+ "", // description
334
+ valueType,
335
+ "", // registerId
336
+ "false", // publishCoV
337
+ "", // tagFormula
338
+ buildMetaData(input), // metaData
339
+ "", // Retain
340
+ "", // Qos
341
+ "", // address
342
+ "", // count
343
+ "", // pollingInterval
344
+ "", // endianness
345
+ ].join(",");
346
+ }
347
+
348
+ function buildModbusRow(input: RowInput): string {
349
+ const registerType = String(input.protocolData?.["registerType"] ?? "");
350
+ const valueType = MODBUS_VALUE_TYPE[registerType] ?? "float32";
351
+ const address = input.protocolData?.["address"];
352
+
353
+ return [
354
+ "", // deviceId
355
+ esc(input.deviceName),
356
+ esc(input.driverName),
357
+ "H", // name = Holding register
358
+ esc(input.memberName), // tagName
359
+ "", // description
360
+ valueType,
361
+ "", // registerId
362
+ "false", // publishCoV
363
+ "", // tagFormula
364
+ buildMetaData(input), // metaData
365
+ "", // Retain
366
+ "", // Qos
367
+ typeof address === "number" ? String(address) : "0", // address
368
+ "1", // count — always 1 scalar value; LE derives register width from valueType
369
+ "1.0000000000", // pollingInterval
370
+ "AB CD", // endianness (big-endian — matches Miravo Modbus)
371
+ ].join(",");
372
+ }
373
+
374
+ // ---------------------------------------------------------------------------
375
+ // Helpers
376
+ // ---------------------------------------------------------------------------
377
+
378
+ /** Escape a comma-delimited CSV field: quote if it contains delimiters, quotes, or newlines. */
379
+ function esc(value: string): string {
380
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
381
+ return `"${value.replace(/"/g, '""')}"`;
382
+ }
383
+ return value;
384
+ }
385
+
386
+ /** Build the LE metaData JSON cell from Miravo tag metadata. */
387
+ function buildMetaData(input: RowInput): string {
388
+ const unsPath = input.browsePath ? `${input.browsePath}/${input.memberName}` : input.memberName;
389
+
390
+ const meta: Record<string, unknown> = {
391
+ uns_path: unsPath,
392
+ model: input.model,
393
+ instance: input.instance,
394
+ access: input.access,
395
+ };
396
+
397
+ if (input.unit != null) meta["unit"] = input.unit;
398
+ if (input.euRange != null) {
399
+ meta["eu_range_min"] = String(input.euRange[0]);
400
+ meta["eu_range_max"] = String(input.euRange[1]);
401
+ }
402
+
403
+ return esc(JSON.stringify(meta));
404
+ }
@@ -15,7 +15,7 @@ export function registerForkCommand(program: Command): void {
15
15
 
16
16
  fork
17
17
  .command("model <name>")
18
- .description("Fork a model to ~/.miravo/registry/local/models/")
18
+ .description("Copy a built-in model to local registry for customization")
19
19
  .option("--force", "Overwrite if already exists in local")
20
20
  .addHelpText(
21
21
  "after",
@@ -30,7 +30,7 @@ Examples:
30
30
 
31
31
  fork
32
32
  .command("template <idNameOrPath>")
33
- .description("Fork a template and its referenced models to ~/.miravo/registry/local/")
33
+ .description("Copy a template and its referenced models to local registry")
34
34
  .option("--force", "Overwrite if already exists in local")
35
35
  .option("--models-only", "Only fork referenced models, skip the template itself")
36
36
  .addHelpText(
@@ -21,11 +21,11 @@ interface InjectOptions {
21
21
  export function registerInjectCommand(program: Command): void {
22
22
  program
23
23
  .command("inject [instanceId] [fault]")
24
- .description("Trigger or clear a fault on a running instance")
24
+ .description("Trigger or clear a fault on a running instance (interactive if no args)")
25
25
  .option("--clear", "Clear the fault instead of triggering it")
26
26
  .option("--json", "Machine-readable JSON output")
27
27
  .option("--name <name>", "Target simulation name")
28
- .option("--admin-url <url>", "Miravo admin server URL (env: MIRAVO_ADMIN_URL)")
28
+ .option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
29
29
  .option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
30
30
  .addHelpText(
31
31
  "after",
@@ -37,7 +37,7 @@ export function registerLoadCommand(program: Command): void {
37
37
  parsePositiveInt,
38
38
  )
39
39
  .option("--json", "Machine-readable JSON output")
40
- .option("--admin-url <url>", "Miravo admin server URL (env: MIRAVO_ADMIN_URL)")
40
+ .option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
41
41
  .option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
42
42
  .addHelpText(
43
43
  "after",
@@ -126,29 +126,27 @@ async function logsAction(name: string, options: LogsOptions): Promise<void> {
126
126
  }
127
127
  }
128
128
 
129
- /** Format a WebSocket message for terminal output. Returns null for tick events. */
130
- export function formatWSEvent(msg: WSMessage): string | null {
131
- switch (msg.type) {
129
+ /** Format a WebSocket message for terminal output. Returns null for tick/snapshot events. */
130
+ function formatWSEvent(msg: WSMessage): string | null {
131
+ if (msg.type !== "runtime:event") return null;
132
+ const evt = msg.event;
133
+ switch (evt.kind) {
132
134
  case "fault:triggered":
133
- return `${colors.red("[fault:triggered]")} ${msg.instanceId} · ${msg.fault}`;
135
+ return `${colors.red("[fault:triggered]")} ${evt.instanceId} · ${evt.fault}`;
134
136
  case "fault:cleared":
135
- return `${colors.green("[fault:cleared]")} ${msg.instanceId} · ${msg.fault}`;
137
+ return `${colors.green("[fault:cleared]")} ${evt.instanceId} · ${evt.fault}`;
136
138
  case "lifecycle:changed":
137
- return `${colors.blue("[lifecycle:changed]")} ${msg.instanceId} · ${msg.from} → ${msg.to}`;
139
+ return `${colors.blue("[lifecycle:changed]")} ${evt.instanceId} · ${evt.from} → ${evt.to}`;
138
140
  case "instance:created":
139
- return `${colors.cyan("[instance:created]")} ${msg.id} · ${msg.assetType}`;
141
+ return `${colors.cyan("[instance:created]")} ${evt.id} · ${evt.assetType}`;
140
142
  case "instance:removed":
141
- return `${colors.yellow("[instance:removed]")} ${msg.id}`;
143
+ return `${colors.yellow("[instance:removed]")} ${evt.id}`;
142
144
  case "engine:state":
143
- return `${colors.magenta("[engine:state]")} ${msg.from} → ${msg.to}`;
145
+ return `${colors.magenta("[engine:state]")} ${evt.from} → ${evt.to}`;
144
146
  case "adapter:enabled":
145
- return `${colors.cyan("[adapter:enabled]")} ${msg.name}`;
147
+ return `${colors.cyan("[adapter:enabled]")} ${evt.name}`;
146
148
  case "adapter:disabled":
147
- return `${colors.yellow("[adapter:disabled]")} ${msg.name}`;
148
- case "tick":
149
- case "snapshot":
150
- case "resync-required":
151
- return null;
149
+ return `${colors.yellow("[adapter:disabled]")} ${evt.name}`;
152
150
  default:
153
151
  return null;
154
152
  }
@@ -88,7 +88,7 @@ describe("formatModelInspect", () => {
88
88
  expect(output).toContain("centrifugal-pump");
89
89
  expect(output).toContain("Parameters:");
90
90
  expect(output).toContain("rated_rpm");
91
- expect(output).toContain("Members (30):");
91
+ expect(output).toContain("Members (24):");
92
92
  expect(output).toContain("Methods (5):");
93
93
  expect(output).toContain("availability_state");
94
94
  expect(output).toContain("Lifecycle: healthy");
@@ -19,7 +19,7 @@ export function registerModelsCommand(program: Command): void {
19
19
  models
20
20
  .command("list")
21
21
  .description("Discover and list available models")
22
- .option("--dir <path>", "Additional directory to search for models")
22
+ .option("--dir <path>", "Additional directory to search for .twin.yaml models")
23
23
  .addHelpText(
24
24
  "after",
25
25
  `
@@ -14,9 +14,9 @@ interface PauseResumeOptions {
14
14
  export function registerPauseCommand(program: Command): void {
15
15
  program
16
16
  .command("pause")
17
- .description("Pause the running simulation")
17
+ .description("Pause the running simulation (state is preserved)")
18
18
  .option("--json", "Machine-readable JSON output")
19
- .option("--admin-url <url>", "Miravo admin server URL (env: MIRAVO_ADMIN_URL)")
19
+ .option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
20
20
  .option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
21
21
  .addHelpText(
22
22
  "after",
@@ -38,7 +38,7 @@ export function registerResumeCommand(program: Command): void {
38
38
  .command("resume")
39
39
  .description("Resume a paused simulation")
40
40
  .option("--json", "Machine-readable JSON output")
41
- .option("--admin-url <url>", "Miravo admin server URL (env: MIRAVO_ADMIN_URL)")
41
+ .option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
42
42
  .option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
43
43
  .addHelpText(
44
44
  "after",