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,404 +0,0 @@
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
- }
@@ -1,219 +0,0 @@
1
- import { mkdir, rename, unlink } from "node:fs/promises";
2
- import { basename, dirname, join, resolve } from "node:path";
3
- import { loadTemplate, resolveModelPath } from "@miravo/core";
4
- import { MiravoError } from "@miravo/shared";
5
- import type { Command } from "commander";
6
- import { colors } from "consola/utils";
7
- import { createCatalog, resolveTemplateIdentifier } from "../lib/template-locator.ts";
8
- import { collectReferencedModels } from "../lib/template-utils.ts";
9
- import { configLog, handleCommandError, writeOutput } from "../logger.ts";
10
-
11
- export function registerForkCommand(program: Command): void {
12
- const fork = program
13
- .command("fork")
14
- .description("Copy a model or template to your local registry for customization");
15
-
16
- fork
17
- .command("model <name>")
18
- .description("Copy a built-in model to local registry for customization")
19
- .option("--force", "Overwrite if already exists in local")
20
- .addHelpText(
21
- "after",
22
- `
23
- Examples:
24
- $ miravo fork model centrifugal-pump Fork a model for customization
25
- $ miravo fork model centrifugal-pump --force Overwrite existing fork`,
26
- )
27
- .action(async (name: string, options: { readonly force?: boolean }) => {
28
- await forkModelAction(name, options);
29
- });
30
-
31
- fork
32
- .command("template <idNameOrPath>")
33
- .description("Copy a template and its referenced models to local registry")
34
- .option("--force", "Overwrite if already exists in local")
35
- .option("--models-only", "Only fork referenced models, skip the template itself")
36
- .addHelpText(
37
- "after",
38
- `
39
- Examples:
40
- $ miravo fork template water-treatment Fork template + referenced models
41
- $ miravo fork template water-treatment --models-only Fork only models`,
42
- )
43
- .action(
44
- async (
45
- idNameOrPath: string,
46
- options: { readonly force?: boolean; readonly modelsOnly?: boolean },
47
- ) => {
48
- await forkTemplateAction(idNameOrPath, options);
49
- },
50
- );
51
- }
52
-
53
- // ---------------------------------------------------------------------------
54
- // Fork model
55
- // ---------------------------------------------------------------------------
56
-
57
- async function forkModelAction(name: string, options: { readonly force?: boolean }): Promise<void> {
58
- try {
59
- const catalog = createCatalog();
60
- const resolvedPath = await catalog.resolveModel(name);
61
- const localModelsDir = join(catalog.getLocalRegistryDir(), "models");
62
- await mkdir(localModelsDir, { recursive: true });
63
-
64
- const targetPath = join(localModelsDir, basename(resolvedPath));
65
- const copied = await atomicCopyFile({
66
- sourcePath: resolvedPath,
67
- targetPath,
68
- label: `model "${name}"`,
69
- force: options.force ?? false,
70
- });
71
-
72
- writeOutput();
73
- if (copied) {
74
- writeOutput(` ${colors.bold("Forked to local:")}`);
75
- writeOutput(` models/${basename(resolvedPath)}`);
76
- }
77
- writeOutput();
78
- } catch (error) {
79
- handleCommandError(error);
80
- }
81
- }
82
-
83
- // ---------------------------------------------------------------------------
84
- // Fork template
85
- // ---------------------------------------------------------------------------
86
-
87
- async function forkTemplateAction(
88
- idNameOrPath: string,
89
- options: { readonly force?: boolean; readonly modelsOnly?: boolean },
90
- ): Promise<void> {
91
- try {
92
- const catalog = createCatalog();
93
- const resolvedPath = await resolveTemplateIdentifier(idNameOrPath, catalog);
94
-
95
- const localDir = catalog.getLocalRegistryDir();
96
- const localModelsDir = join(localDir, "models");
97
- const localTemplatesDir = join(localDir, "templates");
98
- await mkdir(localModelsDir, { recursive: true });
99
- await mkdir(localTemplatesDir, { recursive: true });
100
-
101
- const forked: string[] = [];
102
- const skipped: string[] = [];
103
-
104
- // Fork the template itself (unless --models-only)
105
- if (!options.modelsOnly) {
106
- const targetPath = join(localTemplatesDir, basename(resolvedPath));
107
- const copied = await atomicCopyFile({
108
- sourcePath: resolvedPath,
109
- targetPath,
110
- label: `template "${idNameOrPath}"`,
111
- force: options.force ?? false,
112
- });
113
- if (copied) {
114
- forked.push(`templates/${basename(resolvedPath)}`);
115
- } else {
116
- skipped.push(`templates/${basename(resolvedPath)}`);
117
- }
118
- }
119
-
120
- // Fork referenced models
121
- const template = await loadTemplate(resolvedPath);
122
- const inlineModels = new Set((template.models ?? []).map((m) => m.model.name));
123
- const referencedModels = collectReferencedModels(template);
124
- const searchPaths = catalog.getTemplateValidationSearchPaths(resolvedPath);
125
-
126
- for (const modelName of referencedModels) {
127
- if (inlineModels.has(modelName)) continue;
128
-
129
- let sourceModelPath: string;
130
- try {
131
- sourceModelPath = await resolveModelPath(modelName, searchPaths);
132
- } catch {
133
- configLog.warn(`Skipped model "${modelName}": not found.`);
134
- continue;
135
- }
136
-
137
- const targetModelPath = join(localModelsDir, basename(sourceModelPath));
138
- const copied = await atomicCopyFile({
139
- sourcePath: sourceModelPath,
140
- targetPath: targetModelPath,
141
- label: `model "${modelName}"`,
142
- force: options.force ?? false,
143
- });
144
- if (copied) {
145
- forked.push(`models/${basename(sourceModelPath)}`);
146
- } else {
147
- skipped.push(`models/${basename(sourceModelPath)}`);
148
- }
149
- }
150
-
151
- writeOutput();
152
- if (forked.length > 0) {
153
- writeOutput(` ${colors.bold("Forked to local:")}`);
154
- for (const f of forked) {
155
- writeOutput(` ${f}`);
156
- }
157
- }
158
- if (skipped.length > 0) {
159
- for (const s of skipped) {
160
- writeOutput(` ${s} ${colors.dim("(skipped — already in local)")}`);
161
- }
162
- }
163
- if (forked.length === 0 && skipped.length === 0) {
164
- writeOutput(" Nothing to fork.");
165
- }
166
- writeOutput();
167
- } catch (error) {
168
- handleCommandError(error);
169
- }
170
- }
171
-
172
- // ---------------------------------------------------------------------------
173
- // Helpers
174
- // ---------------------------------------------------------------------------
175
-
176
- /**
177
- * Copy a file using stage-to-temp-and-rename for atomic writes.
178
- * Returns true if the file was copied, false if skipped (already exists and no --force).
179
- */
180
- async function atomicCopyFile(options: {
181
- readonly sourcePath: string;
182
- readonly targetPath: string;
183
- readonly label: string;
184
- readonly force: boolean;
185
- }): Promise<boolean> {
186
- const { sourcePath, targetPath, label, force } = options;
187
-
188
- if (resolve(sourcePath) === resolve(targetPath)) {
189
- configLog.warn(`Skipped ${label}: source and destination are the same.`);
190
- return false;
191
- }
192
-
193
- const targetExists = await Bun.file(targetPath).exists();
194
- if (targetExists && !force) {
195
- configLog.info(`Skipped ${label}: already exists in local. Use --force to overwrite.`);
196
- return false;
197
- }
198
-
199
- const contents = await Bun.file(sourcePath).text();
200
- const tmpPath = join(dirname(targetPath), `.${basename(targetPath)}.tmp`);
201
-
202
- try {
203
- await Bun.write(tmpPath, contents);
204
- await rename(tmpPath, targetPath);
205
- } catch {
206
- // Clean up temp file on failure
207
- try {
208
- await unlink(tmpPath);
209
- } catch {
210
- // Ignore cleanup errors
211
- }
212
- throw new MiravoError(
213
- `Failed to write ${label} to "${targetPath}".`,
214
- `Ensure "${dirname(targetPath)}" is writable by the current user.`,
215
- );
216
- }
217
-
218
- return true;
219
- }
@@ -1,57 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { findCliCommand, resetExitCode } from "./cli-test-setup.ts";
3
-
4
- beforeEach(resetExitCode);
5
- afterEach(resetExitCode);
6
-
7
- describe("miravo inject", () => {
8
- test("sets exit code 1 with invalid admin URL", async () => {
9
- const { program } = await findCliCommand("inject");
10
- program.exitOverride();
11
-
12
- try {
13
- await program.parseAsync([
14
- "node",
15
- "miravo",
16
- "inject",
17
- "pump-001",
18
- "cavitation",
19
- "--admin-url",
20
- "not-a-url",
21
- ]);
22
- } catch {
23
- // Expected
24
- }
25
-
26
- expect(process.exitCode).toBe(1);
27
- });
28
-
29
- test("requires terminal for interactive mode", async () => {
30
- // Save originals
31
- const origStdoutTTY = process.stdout.isTTY;
32
- const origStdinTTY = process.stdin.isTTY;
33
-
34
- try {
35
- // Simulate non-TTY
36
- Object.defineProperty(process.stdout, "isTTY", { value: false, configurable: true });
37
- Object.defineProperty(process.stdin, "isTTY", { value: false, configurable: true });
38
-
39
- // Import inject action indirectly via program
40
- const { program } = await findCliCommand("inject");
41
- program.exitOverride();
42
-
43
- try {
44
- // No positional args → interactive mode needed → should fail in non-TTY
45
- await program.parseAsync(["node", "miravo", "inject"]);
46
- } catch {
47
- // Expected
48
- }
49
-
50
- expect(process.exitCode).toBe(1);
51
- } finally {
52
- // Restore
53
- Object.defineProperty(process.stdout, "isTTY", { value: origStdoutTTY, configurable: true });
54
- Object.defineProperty(process.stdin, "isTTY", { value: origStdinTTY, configurable: true });
55
- }
56
- });
57
- });