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,89 +0,0 @@
1
- /**
2
- * LE auth and hostname resolution.
3
- *
4
- * Resolution order: CLI flag → environment variable → ~/.miravo/platforms/environments.json → fail.
5
- */
6
-
7
- import type { LEClientConfig } from "./le-client.ts";
8
- import { getDefaultEnvironment, getEnvironmentByLabel } from "./le-environments.ts";
9
-
10
- /**
11
- * Resolve a named environment from ~/.miravo/platforms/environments.json.
12
- * Returns undefined when no `--environment` flag was provided,
13
- * or throws when the label doesn't match any stored environment.
14
- */
15
- async function resolveNamedEnvironment(
16
- environmentLabel: string | undefined,
17
- ): Promise<
18
- | { readonly hostname: string; readonly apiToken: string; readonly sourceHostname: string }
19
- | undefined
20
- > {
21
- if (!environmentLabel) return undefined;
22
- const env = await getEnvironmentByLabel(environmentLabel);
23
- if (!env) {
24
- throw new Error(
25
- `Environment "${environmentLabel}" not found. Check the label in ~/.miravo/platforms/environments.json or the console.`,
26
- );
27
- }
28
- return env;
29
- }
30
-
31
- export async function resolveLeAuth(opts: {
32
- environment?: string | undefined;
33
- leHostname?: string | undefined;
34
- leApiToken?: string | undefined;
35
- }): Promise<LEClientConfig> {
36
- // 1. Named environment (--environment flag)
37
- const named = await resolveNamedEnvironment(opts.environment);
38
- if (named) {
39
- return {
40
- hostname: opts.leHostname || named.hostname,
41
- apiToken: opts.leApiToken || named.apiToken,
42
- };
43
- }
44
-
45
- // 2. CLI flags / env vars
46
- const hostname = opts.leHostname || Bun.env["LITMUS_EDGE_HOSTNAME"];
47
- const apiToken = opts.leApiToken || Bun.env["LITMUS_EDGE_API_TOKEN"];
48
-
49
- if (hostname && apiToken) {
50
- return { hostname, apiToken };
51
- }
52
-
53
- // 3. Default environment from ~/.miravo/platforms/environments.json
54
- const defaultEnv = await getDefaultEnvironment();
55
- if (defaultEnv) {
56
- return {
57
- hostname: hostname || defaultEnv.hostname,
58
- apiToken: apiToken || defaultEnv.apiToken,
59
- };
60
- }
61
-
62
- if (!hostname) {
63
- throw new Error(
64
- "Missing LE hostname. Provide --environment, --le-hostname, set LITMUS_EDGE_HOSTNAME, or configure an environment via the console.",
65
- );
66
- }
67
- throw new Error(
68
- "Missing LE API token. Provide --environment, --le-api-token, set LITMUS_EDGE_API_TOKEN, or configure an environment via the console.",
69
- );
70
- }
71
-
72
- export async function resolveSourceHostname(opts: {
73
- environment?: string | undefined;
74
- sourceHostname?: string | undefined;
75
- }): Promise<string> {
76
- const hostname = opts.sourceHostname || Bun.env["MIRAVO_SOURCE_HOSTNAME"];
77
- if (hostname) return hostname;
78
-
79
- // Named environment takes priority over default
80
- const named = await resolveNamedEnvironment(opts.environment);
81
- if (named) return named.sourceHostname;
82
-
83
- const defaultEnv = await getDefaultEnvironment();
84
- if (defaultEnv) return defaultEnv.sourceHostname;
85
-
86
- throw new Error(
87
- "Missing source hostname. Provide --environment, --source-hostname, set MIRAVO_SOURCE_HOSTNAME, or configure an environment via the console.",
88
- );
89
- }
@@ -1,168 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { buildCleanupPlan, executeCleanup } from "./le-cleanup.ts";
3
- import type { LEDevice, LEDeviceHub } from "./le-devicehub.ts";
4
- import type { LEDigitalTwins, LEModel } from "./le-digital-twins.ts";
5
-
6
- function createModel(id: string, name: string): LEModel {
7
- return { ID: id, Name: name, Description: "", Version: 1 };
8
- }
9
-
10
- function createManagedDevice(name: string, modelName: string): LEDevice {
11
- return {
12
- ID: `device-${name}`,
13
- Name: name,
14
- DriverID: "driver-1",
15
- Properties: [],
16
- MetaData: [
17
- { Name: "miravo_managed", Value: "true" },
18
- { Name: "miravo_template", Value: "smart-factory" },
19
- { Name: "miravo_instance", Value: "pump-001" },
20
- { Name: "miravo_model", Value: modelName },
21
- { Name: "miravo_protocol", Value: "opcua" },
22
- ],
23
- Description: "",
24
- };
25
- }
26
-
27
- function createCleanupOptions(
28
- overrides?: Partial<{
29
- templateId: string;
30
- scope: readonly ("devices" | "models" | "instances")[];
31
- allManaged: boolean;
32
- }>,
33
- ): {
34
- templateId: string;
35
- targetHost: string;
36
- scope: readonly ("devices" | "models" | "instances")[];
37
- filters: { area?: string; line?: string; instance?: string };
38
- allManaged: boolean;
39
- dryRun: boolean;
40
- } {
41
- return {
42
- templateId: overrides?.templateId ?? "smart-factory",
43
- targetHost: "le.example.local",
44
- scope: overrides?.scope ?? ["models"],
45
- filters: {},
46
- allManaged: overrides?.allManaged ?? true,
47
- dryRun: true,
48
- };
49
- }
50
-
51
- function managedModelStaticAttrs(): { Key: string; Value: string }[] {
52
- return [
53
- { Key: "miravo_managed", Value: "true" },
54
- { Key: "miravo_template", Value: "smart-factory" },
55
- { Key: "miravo_model", Value: "ac-induction-motor" },
56
- ];
57
- }
58
-
59
- describe("buildCleanupPlan model discovery", () => {
60
- test("all-managed includes models with DT markers when devices are gone", async () => {
61
- const models = [
62
- createModel("model-1", "AC-INDUCTION-MOTOR"),
63
- createModel("model-2", "AC-INDUCTION-MOTOR"),
64
- createModel("model-3", "UNRELATED-MODEL"),
65
- ];
66
-
67
- const hub: LEDeviceHub = {
68
- async listDevices() {
69
- return [];
70
- },
71
- } as unknown as LEDeviceHub;
72
-
73
- const dt: LEDigitalTwins = {
74
- async listModels() {
75
- return models;
76
- },
77
- async listStaticAttributes(opts: { modelId?: string; instanceId?: string }) {
78
- const mid = opts.modelId;
79
- if (mid === "model-1" || mid === "model-2") return managedModelStaticAttrs();
80
- return [];
81
- },
82
- } as unknown as LEDigitalTwins;
83
-
84
- const result = await buildCleanupPlan(hub, dt, createCleanupOptions({ allManaged: true }));
85
-
86
- expect(result.plan.entries).toHaveLength(2);
87
- expect(result.plan.entries.map((entry) => entry.resourceId).sort()).toEqual([
88
- "model-1",
89
- "model-2",
90
- ]);
91
- });
92
-
93
- test("device-marker fallback includes all duplicate DT model IDs for scoped cleanup", async () => {
94
- const models = [
95
- createModel("model-1", "AC-INDUCTION-MOTOR"),
96
- createModel("model-2", "AC-INDUCTION-MOTOR"),
97
- ];
98
-
99
- const hub: LEDeviceHub = {
100
- async listDevices() {
101
- return [createManagedDevice("pump-001", "ac-induction-motor")];
102
- },
103
- } as unknown as LEDeviceHub;
104
-
105
- const dt: LEDigitalTwins = {
106
- async listModels() {
107
- return models;
108
- },
109
- async listStaticAttributes() {
110
- return [];
111
- },
112
- } as unknown as LEDigitalTwins;
113
-
114
- const result = await buildCleanupPlan(
115
- hub,
116
- dt,
117
- createCleanupOptions({ allManaged: false, templateId: "smart-factory", scope: ["models"] }),
118
- );
119
-
120
- expect(result.plan.entries).toHaveLength(2);
121
- expect(result.plan.entries.map((entry) => entry.resourceId).sort()).toEqual([
122
- "model-1",
123
- "model-2",
124
- ]);
125
- });
126
- });
127
-
128
- describe("executeCleanup duplicate-safe deletion", () => {
129
- test("deletes duplicate model names by resource ID", async () => {
130
- const models = [
131
- createModel("model-1", "AC-INDUCTION-MOTOR"),
132
- createModel("model-2", "AC-INDUCTION-MOTOR"),
133
- ];
134
-
135
- const hub: LEDeviceHub = {
136
- async listDevices() {
137
- return [];
138
- },
139
- } as unknown as LEDeviceHub;
140
-
141
- const deletedModelIds: string[] = [];
142
- const dt: LEDigitalTwins = {
143
- async listModels() {
144
- return models;
145
- },
146
- async listStaticAttributes(opts: { modelId?: string; instanceId?: string }) {
147
- const mid = opts.modelId;
148
- if (mid === "model-1" || mid === "model-2") return managedModelStaticAttrs();
149
- return [];
150
- },
151
- async deleteModel(id: string) {
152
- deletedModelIds.push(id);
153
- },
154
- } as unknown as LEDigitalTwins;
155
-
156
- const planResult = await buildCleanupPlan(hub, dt, createCleanupOptions({ allManaged: true }));
157
- const result = await executeCleanup(
158
- hub,
159
- dt,
160
- planResult,
161
- createCleanupOptions({ allManaged: true }),
162
- );
163
-
164
- expect(result.failed).toBe(0);
165
- expect(result.deleted).toBe(2);
166
- expect(deletedModelIds.sort()).toEqual(["model-1", "model-2"]);
167
- });
168
- });
@@ -1,399 +0,0 @@
1
- /**
2
- * LE Cleanup reconciler.
3
- *
4
- * Discovers Miravo-managed resources and deletes them in
5
- * reverse dependency order: instances → models → devices.
6
- */
7
-
8
- import type { ResourceProgress } from "./le-apply.ts";
9
- import { formatThrownError, pMap } from "./le-concurrency.ts";
10
- import type { LEDevice, LEDeviceHub } from "./le-devicehub.ts";
11
- import type { LEDigitalTwins, LEInstance, LEModel } from "./le-digital-twins.ts";
12
- import { type DeviceMarkerData, parseDeviceMarkers, parseDTMarkers } from "./le-markers.ts";
13
- import type { ApplyPlan, ApplyScope, PlanEntry } from "./le-plan.ts";
14
-
15
- /** Max concurrent LE API calls during cleanup discovery. Tuned for edge devices. */
16
- const LE_CLEANUP_CONCURRENCY = 5;
17
-
18
- interface CleanupOptions {
19
- readonly templateId: string;
20
- readonly targetHost: string;
21
- readonly scope: readonly ApplyScope[];
22
- readonly filters: {
23
- readonly area?: string | undefined;
24
- readonly line?: string | undefined;
25
- readonly instance?: string | undefined;
26
- };
27
- readonly allManaged: boolean;
28
- readonly dryRun: boolean;
29
- readonly onProgress?: ((msg: string) => void) | undefined;
30
- readonly onResourceComplete?: ((progress: ResourceProgress) => void) | undefined;
31
- readonly signal?: AbortSignal | undefined;
32
- }
33
-
34
- interface CleanupResult {
35
- readonly plan: ApplyPlan;
36
- readonly deleted: number;
37
- readonly failed: number;
38
- readonly failedEntries: readonly { entry: PlanEntry; error: string }[];
39
- readonly error?: string | undefined;
40
- }
41
-
42
- interface CleanupSnapshot {
43
- readonly deviceById: Map<string, LEDevice>;
44
- readonly modelById: Map<string, LEModel>;
45
- readonly instanceById: Map<string, LEInstance>;
46
- readonly deviceByName: Map<string, LEDevice[]>;
47
- readonly modelByName: Map<string, LEModel[]>;
48
- readonly instanceByName: Map<string, LEInstance[]>;
49
- }
50
-
51
- interface CleanupPlanResult {
52
- readonly plan: ApplyPlan;
53
- readonly snapshot: CleanupSnapshot;
54
- }
55
-
56
- function indexByName<T extends { Name: string }>(items: readonly T[]): Map<string, T[]> {
57
- const byName = new Map<string, T[]>();
58
- for (const item of items) {
59
- const existing = byName.get(item.Name);
60
- if (existing) {
61
- existing.push(item);
62
- } else {
63
- byName.set(item.Name, [item]);
64
- }
65
- }
66
- return byName;
67
- }
68
-
69
- /** Build a cleanup plan by discovering managed resources. */
70
- export async function buildCleanupPlan(
71
- hub: LEDeviceHub,
72
- dt: LEDigitalTwins,
73
- opts: CleanupOptions,
74
- ): Promise<CleanupPlanResult> {
75
- const scope = opts.scope;
76
-
77
- const needModels = scope.includes("models");
78
- const needDevices = scope.includes("devices");
79
- const needInstances = scope.includes("instances");
80
- // Models are fetched when in scope or when instances are in scope (for the ownership cascade)
81
- const fetchModels = needModels || needInstances;
82
- // Devices are fetched when in scope or when models are in scope (device markers are the fallback for model discovery)
83
- const fetchDevices = needDevices || fetchModels;
84
-
85
- // Fetch all lists in parallel
86
- const [models, devices, instances] = await Promise.all([
87
- fetchModels ? dt.listModels() : ([] as readonly LEModel[]),
88
- fetchDevices ? hub.listDevices() : ([] as readonly LEDevice[]),
89
- needInstances
90
- ? dt.listInstances().catch((): readonly LEInstance[] => {
91
- opts.onProgress?.(
92
- "⚠ Warning: LE ListInstances failed — skipping instance cleanup scope.",
93
- );
94
- return [];
95
- })
96
- : ([] as readonly LEInstance[]),
97
- ]);
98
-
99
- // --- Discover managed devices early (needed as model fallback) ---
100
- // Devices store markers in DeviceHub metadata which is reliable — unlike
101
- // DT static attributes which may fail during apply.
102
- const managedDeviceMarkers: { device: LEDevice; markers: DeviceMarkerData }[] = [];
103
- if (fetchDevices) {
104
- for (const device of devices) {
105
- const markers = parseDeviceMarkers(device.MetaData);
106
- if (!markers) continue;
107
- if (!opts.allManaged && markers.templateId !== opts.templateId) continue;
108
- managedDeviceMarkers.push({ device, markers });
109
- }
110
- }
111
-
112
- // --- Models ---
113
- // managedModelIds is always populated (even when models not in scope) because
114
- // the instance ownership cascade needs it.
115
- const managedModelIds = new Set<string>();
116
- const modelEntries: PlanEntry[] = [];
117
- const modelByName = indexByName(models);
118
-
119
- function includeManagedModel(model: LEModel): void {
120
- if (managedModelIds.has(model.ID)) return;
121
- managedModelIds.add(model.ID);
122
- if (!needModels) return;
123
- modelEntries.push({
124
- action: "delete",
125
- resourceType: "model",
126
- name: model.Name,
127
- resourceId: model.ID,
128
- detail: `(id: ${model.ID})`,
129
- });
130
- }
131
-
132
- // Discover model + instance markers in a single concurrent pass (shared pool).
133
- // Both hit the same DT ListStaticAttributes endpoint, so a shared concurrency
134
- // limit avoids overloading the edge device.
135
- const modelMarkersById = new Map<string, ReturnType<typeof parseDTMarkers>>();
136
- const instanceMarkersById = new Map<string, ReturnType<typeof parseDTMarkers>>();
137
-
138
- type MarkerQuery =
139
- | { readonly kind: "model"; readonly entity: LEModel }
140
- | { readonly kind: "instance"; readonly entity: LEInstance };
141
-
142
- const markerQueries: MarkerQuery[] = [
143
- ...models.map((m): MarkerQuery => ({ kind: "model", entity: m })),
144
- ...(needInstances ? instances.map((i): MarkerQuery => ({ kind: "instance", entity: i })) : []),
145
- ];
146
-
147
- await pMap(
148
- markerQueries,
149
- async (q) => {
150
- const id = q.entity.ID;
151
- const target = q.kind === "model" ? modelMarkersById : instanceMarkersById;
152
- try {
153
- const attrs = await dt.listStaticAttributes(
154
- q.kind === "model" ? { modelId: id } : { instanceId: id },
155
- );
156
- target.set(id, parseDTMarkers(attrs));
157
- } catch {
158
- target.set(id, undefined);
159
- }
160
- },
161
- { concurrency: LE_CLEANUP_CONCURRENCY },
162
- );
163
-
164
- // --- Process model markers ---
165
-
166
- for (const model of models) {
167
- const markers = modelMarkersById.get(model.ID);
168
- if (!markers) continue;
169
- if (!opts.allManaged && markers.templateId !== opts.templateId) continue;
170
- includeManagedModel(model);
171
- }
172
-
173
- // Device-marker fallback: device metadata stores miravo_model (raw catalog name);
174
- // DT model Name is the uppercased version. Include all matching model IDs to handle
175
- // duplicates created by re-import.
176
- for (const { markers } of managedDeviceMarkers) {
177
- for (const model of modelByName.get(markers.modelName.toUpperCase()) ?? []) {
178
- includeManagedModel(model);
179
- }
180
- }
181
-
182
- // --- Process instance markers ---
183
- // Instances with miravo_managed="true" are included directly.
184
- // Instances without markers (or with miravo_managed="false") are included
185
- // if their parent model is managed — model ownership cascades to all instances.
186
- const instanceEntries: PlanEntry[] = [];
187
- if (needInstances) {
188
- for (const inst of instances) {
189
- const markers = instanceMarkersById.get(inst.ID);
190
- let include: boolean;
191
- if (markers) {
192
- include = opts.allManaged || markers.templateId === opts.templateId;
193
- } else {
194
- // No instance markers — include if the parent model is managed
195
- include = managedModelIds.has(inst.ModelID);
196
- }
197
- if (!include) continue;
198
- if (opts.filters.instance) {
199
- if (!markers) {
200
- opts.onProgress?.(
201
- `⚠ Warning: instance ${inst.Name} has no markers — cannot match --instance filter, skipping.`,
202
- );
203
- continue;
204
- }
205
- if (markers.instanceId !== opts.filters.instance) continue;
206
- }
207
- instanceEntries.push({
208
- action: "delete",
209
- resourceType: "instance",
210
- name: inst.Name,
211
- resourceId: inst.ID,
212
- detail: `(id: ${inst.ID})`,
213
- });
214
- }
215
- }
216
-
217
- // Deletion order: instances → models → devices
218
- const entries: PlanEntry[] = [...instanceEntries, ...modelEntries];
219
-
220
- if (needDevices) {
221
- for (const { device, markers } of managedDeviceMarkers) {
222
- if (opts.filters.instance && markers.instanceId !== opts.filters.instance) continue;
223
- entries.push({
224
- action: "delete",
225
- resourceType: "device",
226
- name: device.Name,
227
- resourceId: device.ID,
228
- detail: `(id: ${device.ID})`,
229
- });
230
- }
231
- }
232
-
233
- const plan: ApplyPlan = {
234
- sourceMode: opts.allManaged ? "managed" : "static",
235
- targetHost: opts.targetHost,
236
- sourceHostname: undefined,
237
- scope: [...opts.scope],
238
- filters: opts.filters,
239
- entries,
240
- };
241
-
242
- return {
243
- plan,
244
- snapshot: {
245
- deviceById: new Map(devices.map((d) => [d.ID, d])),
246
- modelById: new Map(models.map((m) => [m.ID, m])),
247
- instanceById: new Map(instances.map((i) => [i.ID, i])),
248
- deviceByName: indexByName(devices),
249
- modelByName: indexByName(models),
250
- instanceByName: indexByName(instances),
251
- },
252
- };
253
- }
254
-
255
- /** Execute cleanup using the snapshot from plan building (no re-fetch). */
256
- export async function executeCleanup(
257
- hub: LEDeviceHub,
258
- dt: LEDigitalTwins,
259
- { plan, snapshot }: CleanupPlanResult,
260
- opts: CleanupOptions,
261
- ): Promise<CleanupResult> {
262
- const log = opts.onProgress ?? (() => {});
263
- const onResource = opts.onResourceComplete;
264
- let deleted = 0;
265
-
266
- // Pre-compute per-phase totals for progress reporting
267
- const phaseTotals = new Map<string, number>();
268
- const phaseCounters = new Map<string, number>();
269
- for (const entry of plan.entries) {
270
- phaseTotals.set(entry.resourceType, (phaseTotals.get(entry.resourceType) ?? 0) + 1);
271
- }
272
-
273
- function emitProgress(
274
- phase: string,
275
- name: string,
276
- status: "success" | "failed",
277
- error?: string,
278
- count?: number,
279
- ): void {
280
- const idx = (phaseCounters.get(phase) ?? 0) + 1;
281
- phaseCounters.set(phase, idx);
282
- onResource?.({
283
- phase,
284
- resourceType: phase,
285
- name,
286
- action: "delete",
287
- status,
288
- error,
289
- index: idx,
290
- of: phaseTotals.get(phase) ?? 0,
291
- count,
292
- });
293
- }
294
-
295
- const { deviceById, modelById, instanceById, deviceByName, modelByName, instanceByName } =
296
- snapshot;
297
-
298
- function resolveInstance(entry: PlanEntry): LEInstance | undefined {
299
- if (entry.resourceId) return instanceById.get(entry.resourceId);
300
- const matches = instanceByName.get(entry.name) ?? [];
301
- return matches[0];
302
- }
303
-
304
- function resolveModel(entry: PlanEntry): LEModel | undefined {
305
- if (entry.resourceId) return modelById.get(entry.resourceId);
306
- const matches = modelByName.get(entry.name) ?? [];
307
- return matches[0];
308
- }
309
-
310
- function resolveDevice(entry: PlanEntry): LEDevice | undefined {
311
- if (entry.resourceId) return deviceById.get(entry.resourceId);
312
- const matches = deviceByName.get(entry.name) ?? [];
313
- return matches[0];
314
- }
315
-
316
- // Group entries by resource type, then process in dependency order
317
- const CLEANUP_ORDER = ["instance", "model", "device"] as const;
318
- type CleanupPhase = (typeof CLEANUP_ORDER)[number];
319
- const groups = new Map<CleanupPhase, PlanEntry[]>();
320
- for (const entry of plan.entries) {
321
- if (entry.resourceType === "param-sync") continue;
322
- const phase = entry.resourceType as CleanupPhase;
323
- const list = groups.get(phase) ?? [];
324
- list.push(entry);
325
- groups.set(phase, list);
326
- }
327
-
328
- const failedEntries: { entry: PlanEntry; error: string }[] = [];
329
-
330
- async function deleteEntry(entry: PlanEntry): Promise<void> {
331
- switch (entry.resourceType) {
332
- case "instance": {
333
- const inst = resolveInstance(entry);
334
- if (!inst) {
335
- log(` SKIP instance ${entry.name} (not found)`);
336
- return;
337
- }
338
- await dt.deleteInstance(inst.ID);
339
- log(`✓ DELETE DT instance ${entry.name}`);
340
- deleted++;
341
- emitProgress("instance", entry.name, "success");
342
- break;
343
- }
344
- case "model": {
345
- const model = resolveModel(entry);
346
- if (!model) {
347
- log(` SKIP model ${entry.name} (not found)`);
348
- return;
349
- }
350
- await dt.deleteModel(model.ID);
351
- log(`✓ DELETE DT model ${entry.name}`);
352
- deleted++;
353
- emitProgress("model", entry.name, "success");
354
- break;
355
- }
356
- case "device": {
357
- const device = resolveDevice(entry);
358
- if (!device) {
359
- log(` SKIP device ${entry.name} (not found)`);
360
- return;
361
- }
362
- await hub.deleteDevices([device.ID]);
363
- log(`✓ DELETE device ${entry.name}`);
364
- deleted++;
365
- emitProgress("device", entry.name, "success");
366
- break;
367
- }
368
- }
369
- }
370
-
371
- for (const phase of CLEANUP_ORDER) {
372
- const entries = groups.get(phase);
373
- if (!entries?.length) continue;
374
- if (opts.signal?.aborted) break;
375
-
376
- await pMap(
377
- entries,
378
- async (entry) => {
379
- try {
380
- await deleteEntry(entry);
381
- } catch (err) {
382
- const msg = formatThrownError(err);
383
- log(`✗ DELETE ${entry.resourceType} ${entry.name} — ${msg}`);
384
- emitProgress(entry.resourceType, entry.name, "failed", msg);
385
- failedEntries.push({ entry, error: msg });
386
- }
387
- },
388
- { concurrency: LE_CLEANUP_CONCURRENCY, ...(opts.signal ? { signal: opts.signal } : {}) },
389
- );
390
- }
391
-
392
- return {
393
- plan,
394
- deleted,
395
- failed: failedEntries.length,
396
- failedEntries,
397
- error: failedEntries[0]?.error,
398
- };
399
- }