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