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,157 @@
1
+ import type { MiravoTemplate, ProtocolConfig } from "@miravo/shared";
2
+ import {
3
+ ConfigValidationError,
4
+ DEFAULT_MODBUS_PORT,
5
+ DEFAULT_MQTT_PORT,
6
+ DEFAULT_OPCUA_PORT,
7
+ } from "@miravo/shared";
8
+ import { displayHost } from "../../lib/daemon-registry.ts";
9
+ import type { MqttMode, MqttRunOverrides } from "./types.ts";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Step 5: Adapter default config builders
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export function buildMQTTDefaultConfig(
16
+ template: MiravoTemplate,
17
+ portOverride: number | undefined,
18
+ mqttOverrides: MqttRunOverrides,
19
+ simulationName: string,
20
+ ): Record<string, unknown> {
21
+ const mqttConfig = template.protocols?.mqtt;
22
+ const port = portOverride ?? mqttConfig?.port ?? DEFAULT_MQTT_PORT;
23
+ let embedded = mqttConfig?.embedded ?? true;
24
+ let external = mqttConfig?.external;
25
+ let host = "0.0.0.0";
26
+ let username = mqttConfig?.username;
27
+ let password = mqttConfig?.password;
28
+ const modeOverride = mqttOverrides.mode;
29
+ if (modeOverride !== undefined) {
30
+ embedded = modeOverride === "embedded";
31
+ if (embedded) {
32
+ external = undefined;
33
+ username = undefined;
34
+ password = undefined;
35
+ }
36
+ }
37
+ if (mqttOverrides.url !== undefined) external = mqttOverrides.url;
38
+ if (mqttOverrides.host !== undefined) host = mqttOverrides.host;
39
+ if (mqttOverrides.username !== undefined) username = mqttOverrides.username;
40
+ if (mqttOverrides.password !== undefined) password = mqttOverrides.password;
41
+
42
+ if (embedded && (external || username || password)) {
43
+ throw new ConfigValidationError({
44
+ message: "Embedded MQTT mode cannot be combined with external MQTT URL or credentials.",
45
+ fix: "Use --mqtt-mode external for external broker settings, or remove external MQTT overrides.",
46
+ });
47
+ }
48
+ if (!embedded && !external) {
49
+ throw new ConfigValidationError({
50
+ message: "MQTT mode is external but no broker URL was provided.",
51
+ fix: "Set protocols.mqtt.external in template or pass --mqtt-url / MIRAVO_MQTT_URL.",
52
+ });
53
+ }
54
+ if (password && !username) {
55
+ throw new ConfigValidationError({
56
+ message: "MQTT password was provided without username.",
57
+ fix: "Provide --mqtt-username / MIRAVO_MQTT_USERNAME when setting a password.",
58
+ });
59
+ }
60
+
61
+ const drainBudget = mqttOverrides.drainBudget ?? mqttConfig?.drain_budget;
62
+ const batchSize = mqttOverrides.batchSize ?? mqttConfig?.batch_size;
63
+ const brokerConcurrency = mqttOverrides.brokerConcurrency ?? mqttConfig?.broker_concurrency;
64
+
65
+ return {
66
+ name: "mqtt",
67
+ enabled: true,
68
+ simulationName,
69
+ embedded,
70
+ port,
71
+ host,
72
+ retain: mqttConfig?.retain ?? false,
73
+ ...(external !== undefined ? { external } : {}),
74
+ ...(username !== undefined ? { username } : {}),
75
+ ...(password !== undefined ? { password } : {}),
76
+ ...(mqttConfig?.reconnect !== undefined ? { reconnect: mqttConfig.reconnect } : {}),
77
+ ...(mqttConfig?.rbe !== undefined ? { rbe: mqttConfig.rbe } : {}),
78
+ ...(mqttConfig?.metadata !== undefined ? { metadata: mqttConfig.metadata } : {}),
79
+ ...(drainBudget !== undefined ? { drain_budget: drainBudget } : {}),
80
+ ...(batchSize !== undefined ? { batch_size: batchSize } : {}),
81
+ ...(brokerConcurrency !== undefined ? { broker_concurrency: brokerConcurrency } : {}),
82
+ };
83
+ }
84
+
85
+ export function buildOPCUADefaultConfig(
86
+ template: MiravoTemplate,
87
+ portOverride: number | undefined,
88
+ hostOverride: string | undefined,
89
+ simulationName: string,
90
+ writeBudgetOverride?: number,
91
+ ): Record<string, unknown> {
92
+ const opcuaConfig = template.protocols?.opcua;
93
+ const port = portOverride ?? opcuaConfig?.port ?? DEFAULT_OPCUA_PORT;
94
+ const host = hostOverride ?? opcuaConfig?.host ?? "0.0.0.0";
95
+ const writeBudget = writeBudgetOverride ?? opcuaConfig?.write_budget;
96
+
97
+ return {
98
+ name: "opcua",
99
+ enabled: true,
100
+ port,
101
+ host,
102
+ security: opcuaConfig?.security ?? "none",
103
+ simulationName,
104
+ ...(opcuaConfig?.buildInfo ? { buildInfo: opcuaConfig.buildInfo } : {}),
105
+ ...(writeBudget !== undefined ? { write_budget: writeBudget } : {}),
106
+ };
107
+ }
108
+
109
+ export function buildModbusDefaultConfig(
110
+ template: MiravoTemplate,
111
+ portOverride: number | undefined,
112
+ hostOverride: string | undefined,
113
+ ): Record<string, unknown> {
114
+ const modbusConfig = template.protocols?.modbus;
115
+ const port = portOverride ?? modbusConfig?.port ?? DEFAULT_MODBUS_PORT;
116
+ const host = hostOverride ?? modbusConfig?.host ?? "0.0.0.0";
117
+ return { name: "modbus", enabled: true, port, host };
118
+ }
119
+
120
+ /** Derive the MQTT endpoint string from a resolved adapter config. */
121
+ export function buildMQTTEndpoint(config: ProtocolConfig): string {
122
+ const cfg = config as Record<string, unknown>;
123
+ const port = cfg["port"] ?? DEFAULT_MQTT_PORT;
124
+ const embedded = cfg["embedded"] ?? true;
125
+ const external = cfg["external"] as string | undefined;
126
+ return embedded ? `mqtt://localhost:${port}` : (external as string);
127
+ }
128
+
129
+ /** Derive the OPC UA endpoint string from a resolved adapter config. */
130
+ export function buildOPCUAEndpoint(config: ProtocolConfig): string {
131
+ const cfg = config as Record<string, unknown>;
132
+ const port = cfg["port"] ?? DEFAULT_OPCUA_PORT;
133
+ const host = cfg["host"] ?? "0.0.0.0";
134
+ const resolvedDisplayHost = host === "0.0.0.0" ? "localhost" : host;
135
+ return `opc.tcp://${resolvedDisplayHost}:${port}`;
136
+ }
137
+
138
+ export function buildModbusEndpoint(config: Record<string, unknown>): string {
139
+ const port = (config as { port?: number }).port ?? DEFAULT_MODBUS_PORT;
140
+ const host = (config as { host?: string }).host ?? "0.0.0.0";
141
+ return `modbus://${displayHost(String(host))}:${port}`;
142
+ }
143
+
144
+ export function parseMqttModeOption(raw: string | undefined): MqttMode | undefined {
145
+ if (raw === undefined) {
146
+ return undefined;
147
+ }
148
+
149
+ if (raw === "embedded" || raw === "external") {
150
+ return raw;
151
+ }
152
+
153
+ throw new ConfigValidationError({
154
+ message: `Invalid MQTT mode "${raw}".`,
155
+ fix: "Use --mqtt-mode embedded|external or MIRAVO_MQTT_MODE=embedded|external.",
156
+ });
157
+ }
@@ -0,0 +1,50 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { ModelSchema } from "@miravo/shared";
3
+ import { buildLeDtJsonFromSchema } from "./catalog-le-dt.ts";
4
+
5
+ function parseExport(json: string): {
6
+ staticAttributes: { key: string; value: string }[];
7
+ } {
8
+ const payload = JSON.parse(json) as [{ staticAttributes: { key: string; value: string }[] }];
9
+ return payload[0];
10
+ }
11
+
12
+ const modelSchema: ModelSchema = {
13
+ name: "pump",
14
+ description: "Pump model",
15
+ parameters: {},
16
+ members: {
17
+ serial: {
18
+ kind: "property",
19
+ dataType: "String",
20
+ access: "read",
21
+ unit: null,
22
+ euRange: null,
23
+ default: "P-001",
24
+ },
25
+ rpm: {
26
+ kind: "variable",
27
+ dataType: "Float",
28
+ access: "read",
29
+ unit: null,
30
+ euRange: null,
31
+ default: null,
32
+ },
33
+ },
34
+ methods: {},
35
+ faults: [],
36
+ lifecycle: null,
37
+ };
38
+
39
+ describe("catalog LE DT export", () => {
40
+ test("includes Miravo marker placeholders in single-model export", () => {
41
+ const exported = parseExport(buildLeDtJsonFromSchema(modelSchema));
42
+ const markerValues = new Map(exported.staticAttributes.map((attr) => [attr.key, attr.value]));
43
+
44
+ expect(markerValues.get("miravo_managed")).toBe("false");
45
+ expect(markerValues.get("miravo_template")).toBe("__miravo_placeholder__");
46
+ expect(markerValues.get("miravo_model")).toBe("__miravo_placeholder__");
47
+ expect(markerValues.get("miravo_instance")).toBe("__miravo_placeholder__");
48
+ expect(markerValues.get("serial")).toBe("P-001");
49
+ });
50
+ });
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Generates Litmus Edge Digital Twin import JSON from Miravo models.
3
+ *
4
+ * The output matches the format produced by LE's `ExportModel` GraphQL mutation
5
+ * and can be imported directly via `ImportModel` or the LE UI.
6
+ *
7
+ * Model export mapping (`buildLeDtJsonFromSchema`):
8
+ * - kind: property → LE static attributes (placeholder values)
9
+ * - kind: variable (r/rw)→ LE dynamic attributes via topicExpression devicehub.alias.{device}.<member>
10
+ * - LE parameters → wiring placeholders only ({device})
11
+ * - Methods/faults/lifecycle are not exported
12
+ */
13
+
14
+ import type { ModelSchema } from "@miravo/shared";
15
+
16
+ /** Maps Miravo dataType → LE DynamicAttributeType enum values. */
17
+ const LE_DATA_TYPE: Readonly<Record<string, string>> = {
18
+ Double: "number",
19
+ Float: "number",
20
+ Int32: "number",
21
+ UInt16: "number",
22
+ UInt32: "number",
23
+ Boolean: "boolean",
24
+ String: "string",
25
+ };
26
+
27
+ const ZERO = "00000000-0000-0000-0000-000000000000";
28
+ const MIRAVO_MARKER_PLACEHOLDER = "__miravo_placeholder__";
29
+
30
+ interface StaticAttr {
31
+ readonly id: string;
32
+ readonly parentId: string;
33
+ readonly modelId: string;
34
+ readonly instanceId: string;
35
+ readonly key: string;
36
+ readonly value: string;
37
+ readonly type: number;
38
+ }
39
+
40
+ function createStaticAttr(modelId: string, key: string, value: string): StaticAttr {
41
+ return {
42
+ id: crypto.randomUUID(),
43
+ parentId: ZERO,
44
+ modelId,
45
+ instanceId: ZERO,
46
+ key,
47
+ value,
48
+ type: 0,
49
+ };
50
+ }
51
+
52
+ function buildMiravoMarkerPlaceholders(modelId: string): StaticAttr[] {
53
+ return [
54
+ createStaticAttr(modelId, "miravo_managed", "false"),
55
+ createStaticAttr(modelId, "miravo_template", MIRAVO_MARKER_PLACEHOLDER),
56
+ createStaticAttr(modelId, "miravo_model", MIRAVO_MARKER_PLACEHOLDER),
57
+ createStaticAttr(modelId, "miravo_instance", MIRAVO_MARKER_PLACEHOLDER),
58
+ createStaticAttr(modelId, "uns_path", MIRAVO_MARKER_PLACEHOLDER),
59
+ ];
60
+ }
61
+
62
+ export function buildLeDtJsonFromSchema(schema: ModelSchema): string {
63
+ const modelId = crypto.randomUUID();
64
+
65
+ // Static attributes: Miravo marker placeholders + property members.
66
+ // LE materializes instance-owned static attrs from model attrs present at
67
+ // instance creation time, so these placeholders must exist on the model.
68
+ const staticAttributes = [
69
+ ...buildMiravoMarkerPlaceholders(modelId),
70
+ ...Object.entries(schema.members)
71
+ .filter(([, m]) => m.kind === "property")
72
+ .map(([key, m]) =>
73
+ createStaticAttr(modelId, key, m.default != null ? String(m.default) : "<>"),
74
+ ),
75
+ ];
76
+
77
+ // Dynamic attributes: readable variable members
78
+ const dynamicAttributes = Object.entries(schema.members)
79
+ .filter(([, m]) => m.kind === "variable")
80
+ .map(([name, m]) => ({
81
+ id: crypto.randomUUID(),
82
+ modelId,
83
+ parentId: ZERO,
84
+ instanceId: ZERO,
85
+ topic: "",
86
+ name,
87
+ unit: m.unit ?? "",
88
+ dataType: LE_DATA_TYPE[m.dataType] ?? "number",
89
+ schemaId: null,
90
+ skipParentKey: false,
91
+ topicExpression: `devicehub.alias.{device}.${name}`,
92
+ }));
93
+
94
+ // Hierarchy: "info" folder → static attrs, "measurements" folder → dynamic attrs
95
+ const hierarchyNodes = buildHierarchy(modelId, staticAttributes, dynamicAttributes);
96
+
97
+ return JSON.stringify(
98
+ [
99
+ {
100
+ id: modelId,
101
+ assets: "ASSET",
102
+ name: schema.name.toUpperCase(),
103
+ description: schema.description ?? "",
104
+ image: null,
105
+ dynamicAttributes,
106
+ staticAttributes,
107
+ hierarchyNodes,
108
+ transformationBackup: null,
109
+ parameters: [{ name: "device", value: "miravo" }],
110
+ isUNS: false,
111
+ connectorName: "",
112
+ version: 0,
113
+ },
114
+ ],
115
+ null,
116
+ 2,
117
+ );
118
+ }
119
+
120
+ function buildHierarchy(
121
+ modelId: string,
122
+ staticAttrs: ReadonlyArray<{ id: string; key: string }>,
123
+ dynamicAttrs: ReadonlyArray<{ id: string; name: string }>,
124
+ ): Array<Record<string, unknown>> {
125
+ const nodes: Array<Record<string, unknown>> = [];
126
+ let rootPos = 0;
127
+
128
+ if (staticAttrs.length > 0) {
129
+ const folderId = crypto.randomUUID();
130
+ nodes.push(hFolder(folderId, modelId, rootPos++, "info"));
131
+ let pos = 0;
132
+ for (const a of staticAttrs) {
133
+ nodes.push(hLeaf(modelId, pos++, folderId, a.key, a.id, "static"));
134
+ }
135
+ }
136
+
137
+ if (dynamicAttrs.length > 0) {
138
+ const folderId = crypto.randomUUID();
139
+ nodes.push(hFolder(folderId, modelId, rootPos, "measurements"));
140
+ let pos = 0;
141
+ for (const a of dynamicAttrs) {
142
+ nodes.push(hLeaf(modelId, pos++, folderId, a.name, a.id, "dynamic"));
143
+ }
144
+ }
145
+
146
+ return nodes;
147
+ }
148
+
149
+ function hFolder(
150
+ id: string,
151
+ modelId: string,
152
+ position: number,
153
+ name: string,
154
+ ): Record<string, unknown> {
155
+ return {
156
+ id,
157
+ modelId,
158
+ position,
159
+ parentId: null,
160
+ name,
161
+ isFolder: true,
162
+ attributeId: null,
163
+ attributeType: null,
164
+ nodeType: "folder",
165
+ };
166
+ }
167
+
168
+ function hLeaf(
169
+ modelId: string,
170
+ position: number,
171
+ parentId: string,
172
+ name: string,
173
+ attributeId: string,
174
+ attributeType: string,
175
+ ): Record<string, unknown> {
176
+ return {
177
+ id: crypto.randomUUID(),
178
+ modelId,
179
+ position,
180
+ parentId,
181
+ name,
182
+ isFolder: false,
183
+ attributeId,
184
+ attributeType,
185
+ nodeType: "attribute",
186
+ };
187
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Builds a ModelSchema from raw YAML without the engine runtime.
3
+ * Used by the catalog endpoints to serve model details for models
4
+ * that may not be loaded into the current simulation.
5
+ */
6
+
7
+ import type { ModelSchema } from "@miravo/shared";
8
+ import { TwinModelSchema } from "@miravo/shared";
9
+ import { parse as parseYAML } from "yaml";
10
+
11
+ /**
12
+ * Parse a .twin.yaml file and return a ModelSchema.
13
+ * Mirrors the logic in engine-introspection.ts getModelSchema()
14
+ * but operates on the raw parsed YAML instead of runtime objects.
15
+ */
16
+ export async function buildModelSchemaFromYaml(modelPath: string): Promise<ModelSchema> {
17
+ const text = await Bun.file(modelPath).text();
18
+ const raw = TwinModelSchema.parse(parseYAML(text));
19
+
20
+ const parameters: Record<
21
+ string,
22
+ { type: string; default: number | string | boolean; unit?: string }
23
+ > = {};
24
+ if (raw.parameters) {
25
+ for (const [name, def] of Object.entries(raw.parameters)) {
26
+ parameters[name] = {
27
+ type: def.type,
28
+ default: def.default,
29
+ ...(def.unit ? { unit: def.unit } : {}),
30
+ };
31
+ }
32
+ }
33
+
34
+ const members: Record<string, ModelSchema["members"][string]> = {};
35
+ for (const [name, def] of Object.entries(raw.members)) {
36
+ const rawDefault = def.default;
37
+ members[name] = {
38
+ kind: def.kind,
39
+ dataType: def.data_type,
40
+ access: def.access,
41
+ unit: def.unit ?? null,
42
+ euRange: "eu_range" in def ? (def.eu_range ?? null) : null,
43
+ default:
44
+ typeof rawDefault === "number" ||
45
+ typeof rawDefault === "string" ||
46
+ typeof rawDefault === "boolean"
47
+ ? rawDefault
48
+ : null,
49
+ };
50
+ }
51
+
52
+ const methods: Record<string, ModelSchema["methods"][string]> = {};
53
+ if (raw.methods) {
54
+ for (const [name, def] of Object.entries(raw.methods)) {
55
+ methods[name] = {
56
+ description: def.description ?? null,
57
+ inputArguments: def.input_arguments
58
+ ? Object.entries(def.input_arguments).map(([argName, arg]) => ({
59
+ name: argName,
60
+ dataType: arg.data_type,
61
+ description: arg.description ?? null,
62
+ }))
63
+ : [],
64
+ outputArguments: def.output_arguments
65
+ ? Object.entries(def.output_arguments).map(([argName, arg]) => ({
66
+ name: argName,
67
+ dataType: arg.data_type,
68
+ description: arg.description ?? null,
69
+ }))
70
+ : [],
71
+ };
72
+ }
73
+ }
74
+
75
+ return {
76
+ name: raw.model.name,
77
+ description: raw.model.description ?? null,
78
+ parameters,
79
+ members,
80
+ methods,
81
+ faults: raw.faults?.map((f) => f.name) ?? [],
82
+ lifecycle: raw.lifecycle
83
+ ? {
84
+ stages: raw.lifecycle.stages.map((s) => s.name),
85
+ totalHours: raw.lifecycle.stages.reduce((sum, s) => sum + s.duration_hours[1], 0),
86
+ }
87
+ : null,
88
+ };
89
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Builds a CatalogTemplateDetail from raw YAML.
3
+ * Used by the catalog endpoint to serve template topology
4
+ * for the inspect side sheet.
5
+ */
6
+
7
+ import type { CatalogTemplate, CatalogTemplateDetail } from "@miravo/shared";
8
+ import { MiravoTemplateSchema } from "@miravo/shared";
9
+ import { parse as parseYAML } from "yaml";
10
+
11
+ export async function buildTemplateDetail(
12
+ templatePath: string,
13
+ base: CatalogTemplate,
14
+ ): Promise<CatalogTemplateDetail> {
15
+ const text = await Bun.file(templatePath).text();
16
+ const raw = MiravoTemplateSchema.parse(parseYAML(text));
17
+
18
+ const topology = raw.areas.map((area) => ({
19
+ name: area.name,
20
+ ...(area.protocol_scope ? { protocol_scope: area.protocol_scope } : {}),
21
+ lines: area.lines.map((line) => ({
22
+ name: line.name,
23
+ ...(line.protocol_scope ? { protocol_scope: line.protocol_scope } : {}),
24
+ twins: line.twins.map((twin) => ({
25
+ model: twin.model,
26
+ count: twin.count,
27
+ variation: twin.variation,
28
+ ...(twin.parameters ? { parameters: twin.parameters } : {}),
29
+ ...(twin.protocol_scope ? { protocol_scope: twin.protocol_scope } : {}),
30
+ })),
31
+ })),
32
+ }));
33
+
34
+ // Extract namespace (ISA-95 or flat)
35
+ const namespace = raw.namespace
36
+ ? "enterprise" in raw.namespace
37
+ ? { enterprise: raw.namespace.enterprise, site: raw.namespace.site }
38
+ : { prefix: raw.namespace.prefix }
39
+ : undefined;
40
+
41
+ return {
42
+ ...base,
43
+ topology,
44
+ ...(namespace ? { namespace } : {}),
45
+ ...(raw.settings
46
+ ? {
47
+ settings: {
48
+ tickIntervalMs: raw.settings.tick_interval_ms,
49
+ timeMultiplier: raw.settings.time_multiplier,
50
+ },
51
+ }
52
+ : {}),
53
+ };
54
+ }