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/lib/le/le-auth.ts
DELETED
|
@@ -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
|
-
});
|
package/src/lib/le/le-cleanup.ts
DELETED
|
@@ -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
|
-
}
|