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,216 +0,0 @@
1
- /**
2
- * CLI command: miravo apply le
3
- */
4
-
5
- import type { ProtocolName } from "@miravo/shared";
6
- import { ProtocolNameSchema } from "@miravo/shared";
7
- import type { Command } from "commander";
8
- import { buildApplyPlan, executePlan } from "../lib/le/le-apply.ts";
9
- import { resolveLeAuth, resolveSourceHostname } from "../lib/le/le-auth.ts";
10
- import {
11
- extractTemplateId,
12
- parseScopeOption,
13
- writePlanOutput,
14
- writeStatusMessage,
15
- } from "../lib/le/le-cli-helpers.ts";
16
- import { LEClient } from "../lib/le/le-client.ts";
17
- import { LEDeviceHub } from "../lib/le/le-devicehub.ts";
18
- import { LEDigitalTwins } from "../lib/le/le-digital-twins.ts";
19
- import type { ApplyScope } from "../lib/le/le-plan.ts";
20
- import { renderPlanHuman } from "../lib/le/le-plan.ts";
21
- import { buildDesiredStateFromTemplate } from "../lib/le/le-static-planner.ts";
22
- import { createCatalog, resolveTemplateIdentifier } from "../lib/template-locator.ts";
23
- import { handleCommandError, writeOutput } from "../logger.ts";
24
-
25
- const DEFAULT_SCOPE: ApplyScope[] = ["devices", "models", "instances"];
26
-
27
- function parseIfUnmanagedPolicy(raw: string | undefined): {
28
- readonly policy?: "fail" | "skip" | undefined;
29
- readonly error?: string | undefined;
30
- } {
31
- const normalized = raw?.trim();
32
- if (!normalized) return { policy: "fail" };
33
- if (normalized === "fail" || normalized === "skip") return { policy: normalized };
34
- return { error: `invalid --if-unmanaged value "${raw}". Valid values: fail, skip.` };
35
- }
36
-
37
- interface ApplyLeOptions {
38
- readonly environment?: string;
39
- readonly leHostname?: string;
40
- readonly leApiToken?: string;
41
- readonly template?: string;
42
- readonly protocol?: string;
43
- readonly scope?: string;
44
- readonly area?: string;
45
- readonly line?: string;
46
- readonly instance?: string;
47
- readonly sourceHostname?: string;
48
- readonly ifUnmanaged?: string;
49
- readonly dryRun?: boolean;
50
- readonly json?: boolean;
51
- }
52
-
53
- export function registerApplyCommand(program: Command): void {
54
- const applyCmd = program
55
- .command("apply")
56
- .description("Apply Miravo resources to external platforms");
57
-
58
- applyCmd
59
- .command("le")
60
- .description("Apply a Miravo template to Litmus Edge (create/update managed resources)")
61
- .option(
62
- "--environment <label>",
63
- "Named LE environment from ~/.miravo/platforms/environments.json",
64
- )
65
- .option("--le-hostname <host>", "Litmus Edge hostname or IP (env: LITMUS_EDGE_HOSTNAME)")
66
- .option("--le-api-token <token>", "Litmus Edge API token (env: LITMUS_EDGE_API_TOKEN)")
67
- .requiredOption("--template <path>", "Template name or file path")
68
- .option("--protocol <name>", "Protocol: mqtt, opcua, or modbus (default: opcua)")
69
- .option("--scope <scope>", `Scope: comma-separated (default: ${DEFAULT_SCOPE.join(",")})`)
70
- .option("--area <name>", "Filter by area name")
71
- .option("--line <name>", "Filter by line name")
72
- .option("--instance <id>", "Filter by instance ID")
73
- .option(
74
- "--source-hostname <host>",
75
- "Hostname LE uses to reach Miravo (env: MIRAVO_SOURCE_HOSTNAME)",
76
- )
77
- .option("--if-unmanaged <policy>", "Policy for unmanaged collisions: fail or skip", "fail")
78
- .option("--dry-run", "Print plan without applying", false)
79
- .option("--json", "Output plan as JSON", false)
80
- .action(async (options: ApplyLeOptions) => {
81
- try {
82
- const jsonOutput = options.json ?? false;
83
- const writeStatus = (msg: string): void => writeStatusMessage(jsonOutput, msg, writeOutput);
84
-
85
- const leConfig = await resolveLeAuth({
86
- environment: options.environment,
87
- leHostname: options.leHostname,
88
- leApiToken: options.leApiToken,
89
- });
90
-
91
- const scopeResult = parseScopeOption(options.scope, DEFAULT_SCOPE);
92
- if (!scopeResult.scope) {
93
- writeStatus(`Error: ${scopeResult.error}`);
94
- process.exitCode = 1;
95
- return;
96
- }
97
- const scope = scopeResult.scope;
98
-
99
- const unmanagedResult = parseIfUnmanagedPolicy(options.ifUnmanaged);
100
- if (!unmanagedResult.policy) {
101
- writeStatus(`Error: ${unmanagedResult.error}`);
102
- process.exitCode = 1;
103
- return;
104
- }
105
-
106
- const protocolParse =
107
- options.protocol !== undefined
108
- ? ProtocolNameSchema.safeParse(options.protocol)
109
- : undefined;
110
- if (protocolParse && !protocolParse.success) {
111
- writeStatus(
112
- `Error: invalid --protocol value "${options.protocol}". Valid values: mqtt, opcua, modbus.`,
113
- );
114
- process.exitCode = 1;
115
- return;
116
- }
117
- const protocol: ProtocolName | undefined = protocolParse?.data;
118
-
119
- const client = new LEClient(leConfig);
120
- const hub = new LEDeviceHub(client);
121
- const dt = new LEDigitalTwins(client);
122
-
123
- writeStatus("Checking LE connectivity...");
124
- await client.checkAuth();
125
-
126
- const sourceHostname = await resolveSourceHostname({
127
- environment: options.environment,
128
- sourceHostname: options.sourceHostname,
129
- });
130
-
131
- const catalog = createCatalog();
132
- const templatePath = await resolveTemplateIdentifier(options.template as string, catalog);
133
-
134
- let defaultProtocolNotified = false;
135
- const staticPlan = await buildDesiredStateFromTemplate({
136
- templatePath,
137
- catalog,
138
- sourceHostname,
139
- protocol,
140
- filters: { area: options.area, line: options.line, instance: options.instance },
141
- onDefaultProtocol: (p) => {
142
- if (!defaultProtocolNotified) {
143
- writeStatus(
144
- `No --protocol specified and template has no protocol config; defaulting to ${p}.`,
145
- );
146
- defaultProtocolNotified = true;
147
- }
148
- },
149
- });
150
-
151
- const desired = staticPlan.desired;
152
-
153
- if (
154
- desired.devices.length === 0 &&
155
- desired.models.length === 0 &&
156
- desired.instances.length === 0
157
- ) {
158
- writeStatus("Warning: empty desired state for the chosen protocol/filters.");
159
- }
160
-
161
- const applyOpts = {
162
- templateId: extractTemplateId(templatePath),
163
- targetHost: leConfig.hostname,
164
- sourceHostname,
165
- scope,
166
- filters: { area: options.area, line: options.line, instance: options.instance },
167
- ifUnmanaged: unmanagedResult.policy,
168
- dryRun: options.dryRun ?? false,
169
- };
170
-
171
- const { plan, snapshot } = await buildApplyPlan(hub, dt, desired, applyOpts);
172
-
173
- const blockers = plan.entries.filter((e) => e.action === "blocked");
174
- if (blockers.length > 0 && !options.dryRun) {
175
- writePlanOutput(plan, jsonOutput, writeOutput);
176
- writeStatus(`${blockers.length} blocker(s) found. Fix and retry.`);
177
- process.exitCode = 1;
178
- return;
179
- }
180
-
181
- if (options.dryRun) {
182
- writePlanOutput(plan, jsonOutput, writeOutput);
183
- return;
184
- }
185
-
186
- if (jsonOutput) {
187
- writePlanOutput(plan, true, writeOutput);
188
- } else {
189
- writeOutput(renderPlanHuman(plan));
190
- writeOutput("");
191
- }
192
-
193
- const result = await executePlan(
194
- hub,
195
- dt,
196
- plan,
197
- desired,
198
- {
199
- ...applyOpts,
200
- onProgress: (msg) => writeStatus(msg),
201
- },
202
- snapshot,
203
- );
204
-
205
- if (!jsonOutput) writeOutput("");
206
- if (result.failed > 0) {
207
- writeStatus(`Done with errors. ${result.applied} applied, ${result.failed} failed.`);
208
- process.exitCode = 1;
209
- } else {
210
- writeStatus(`Done. ${result.applied} resources applied.`);
211
- }
212
- } catch (error) {
213
- handleCommandError(error);
214
- }
215
- });
216
- }
@@ -1,34 +0,0 @@
1
- import { afterEach, beforeEach, expect, test } from "bun:test";
2
- import { resetExitCode, runCliCommand } from "./cli-test-setup.ts";
3
-
4
- beforeEach(resetExitCode);
5
- afterEach(resetExitCode);
6
-
7
- test("cleanup le rejects invalid --scope values", async () => {
8
- await runCliCommand([
9
- "cleanup",
10
- "le",
11
- "--all-managed",
12
- "--le-hostname",
13
- "le.example.local",
14
- "--le-api-token",
15
- "token",
16
- "--scope",
17
- "foo,bar",
18
- ]);
19
-
20
- expect(process.exitCode).toBe(1);
21
- });
22
-
23
- test("cleanup le requires --template or --all-managed", async () => {
24
- await runCliCommand([
25
- "cleanup",
26
- "le",
27
- "--le-hostname",
28
- "le.example.local",
29
- "--le-api-token",
30
- "token",
31
- ]);
32
-
33
- expect(process.exitCode).toBe(1);
34
- });
@@ -1,201 +0,0 @@
1
- /**
2
- * CLI command: miravo cleanup le
3
- */
4
-
5
- import type { Command } from "commander";
6
- import { resolveLeAuth } from "../lib/le/le-auth.ts";
7
- import { buildCleanupPlan, executeCleanup } from "../lib/le/le-cleanup.ts";
8
- import {
9
- extractTemplateId,
10
- parseScopeOption,
11
- writePlanOutput,
12
- writeStatusMessage,
13
- } from "../lib/le/le-cli-helpers.ts";
14
- import { LEClient } from "../lib/le/le-client.ts";
15
- import { LEDeviceHub } from "../lib/le/le-devicehub.ts";
16
- import { LEDigitalTwins } from "../lib/le/le-digital-twins.ts";
17
- import type { ApplyScope } from "../lib/le/le-plan.ts";
18
- import { createCatalog, resolveTemplateIdentifier } from "../lib/template-locator.ts";
19
- import { handleCommandError, writeOutput } from "../logger.ts";
20
-
21
- const DEFAULT_CLEANUP_SCOPE: ApplyScope[] = ["instances", "models", "devices"];
22
-
23
- interface CleanupLeOptions {
24
- readonly environment?: string;
25
- readonly leHostname?: string;
26
- readonly leApiToken?: string;
27
- readonly template?: string;
28
- readonly scope?: string;
29
- readonly area?: string;
30
- readonly line?: string;
31
- readonly instance?: string;
32
- readonly allManaged?: boolean;
33
- readonly dryRun?: boolean;
34
- readonly json?: boolean;
35
- readonly yes?: boolean;
36
- }
37
-
38
- export function registerCleanupCommand(program: Command): void {
39
- const cleanupCmd = program
40
- .command("cleanup")
41
- .description("Clean up Miravo-managed resources from external platforms");
42
-
43
- cleanupCmd
44
- .command("le")
45
- .description("Remove Miravo-managed resources from Litmus Edge")
46
- .option(
47
- "--environment <label>",
48
- "Named LE environment from ~/.miravo/platforms/environments.json",
49
- )
50
- .option("--le-hostname <host>", "Litmus Edge hostname or IP (env: LITMUS_EDGE_HOSTNAME)")
51
- .option("--le-api-token <token>", "Litmus Edge API token (env: LITMUS_EDGE_API_TOKEN)")
52
- .option("--template <path>", "Template name or file path (for scoped cleanup)")
53
- .option(
54
- "--scope <scope>",
55
- `Scope: comma-separated (default: ${DEFAULT_CLEANUP_SCOPE.join(",")})`,
56
- )
57
- .option("--area <name>", "Filter by area name")
58
- .option("--line <name>", "Filter by line name")
59
- .option("--instance <id>", "Filter by instance ID")
60
- .option(
61
- "--all-managed",
62
- "Remove ALL Miravo-managed resources (ignores template filtering)",
63
- false,
64
- )
65
- .option("--dry-run", "Print plan without deleting", false)
66
- .option("--json", "Output plan as JSON", false)
67
- .option("--yes", "Skip confirmation prompts", false)
68
- .action(async (options: CleanupLeOptions) => {
69
- try {
70
- const jsonOutput = options.json ?? false;
71
- const writeStatus = (msg: string): void => writeStatusMessage(jsonOutput, msg, writeOutput);
72
-
73
- const leConfig = await resolveLeAuth({
74
- environment: options.environment,
75
- leHostname: options.leHostname,
76
- leApiToken: options.leApiToken,
77
- });
78
-
79
- const scopeResult = parseScopeOption(options.scope, DEFAULT_CLEANUP_SCOPE);
80
- if (!scopeResult.scope) {
81
- writeStatus(`Error: ${scopeResult.error}`);
82
- process.exitCode = 1;
83
- return;
84
- }
85
- const scope = scopeResult.scope;
86
-
87
- let templateId = "all-managed";
88
- if (!options.allManaged) {
89
- if (!options.template) {
90
- writeStatus("Error: provide --template or use --all-managed to clean all.");
91
- process.exitCode = 1;
92
- return;
93
- }
94
- const catalog = createCatalog();
95
- const templatePath = await resolveTemplateIdentifier(options.template, catalog);
96
- templateId = extractTemplateId(templatePath);
97
- }
98
-
99
- const client = new LEClient(leConfig);
100
- const hub = new LEDeviceHub(client);
101
- const dt = new LEDigitalTwins(client);
102
-
103
- writeStatus("Checking LE connectivity...");
104
- await client.checkAuth();
105
-
106
- const cleanupOpts = {
107
- templateId,
108
- targetHost: leConfig.hostname,
109
- scope,
110
- filters: { area: options.area, line: options.line, instance: options.instance },
111
- allManaged: options.allManaged ?? false,
112
- dryRun: options.dryRun ?? false,
113
- };
114
-
115
- const maxAttempts = 3;
116
- let attempt = 0;
117
- let totalDeleted = 0;
118
-
119
- while (attempt < maxAttempts) {
120
- attempt++;
121
- try {
122
- const planResult = await buildCleanupPlan(hub, dt, cleanupOpts);
123
-
124
- if (planResult.plan.entries.length === 0) {
125
- if (attempt === 1) {
126
- if (jsonOutput) {
127
- writePlanOutput(planResult.plan, true, writeOutput);
128
- } else {
129
- writeOutput("No Miravo-managed resources found to clean up.");
130
- }
131
- } else {
132
- if (!jsonOutput) writeOutput("");
133
- writeStatus(`Done. ${totalDeleted} resources deleted.`);
134
- }
135
- return;
136
- }
137
-
138
- if (options.dryRun) {
139
- writePlanOutput(planResult.plan, jsonOutput, writeOutput);
140
- return;
141
- }
142
-
143
- if (attempt === 1) {
144
- if (jsonOutput) {
145
- writePlanOutput(planResult.plan, true, writeOutput);
146
- } else {
147
- writeOutput(
148
- `Found ${planResult.plan.entries.length} managed resources to delete:\n`,
149
- );
150
- for (const entry of planResult.plan.entries) {
151
- writeOutput(` DELETE ${entry.resourceType} ${entry.name}`);
152
- }
153
- writeOutput("");
154
- }
155
-
156
- if (!options.yes) {
157
- writeStatus("Use --yes to confirm deletion, or --dry-run to preview.");
158
- process.exitCode = 1;
159
- return;
160
- }
161
- }
162
-
163
- const result = await executeCleanup(hub, dt, planResult, {
164
- ...cleanupOpts,
165
- onProgress: (msg) => writeStatus(msg),
166
- });
167
-
168
- totalDeleted += result.deleted;
169
-
170
- if (result.failed > 0 && attempt < maxAttempts) {
171
- writeStatus(
172
- `Attempt ${attempt} finished with ${result.failed} errors, retrying in 3s...`,
173
- );
174
- await Bun.sleep(3000);
175
- continue;
176
- }
177
-
178
- if (!jsonOutput) writeOutput("");
179
- if (result.failed > 0) {
180
- writeStatus(`Done with errors. ${totalDeleted} deleted, ${result.failed} failed.`);
181
- process.exitCode = 1;
182
- } else {
183
- writeStatus(`Done. ${totalDeleted} resources deleted.`);
184
- }
185
- return;
186
- } catch (err) {
187
- if (attempt < maxAttempts && !options.dryRun && options.yes) {
188
- writeStatus(
189
- `Attempt ${attempt} failed: ${err instanceof Error ? err.message : String(err)}, retrying in 3s...`,
190
- );
191
- await Bun.sleep(3000);
192
- continue;
193
- }
194
- throw err;
195
- }
196
- }
197
- } catch (error) {
198
- handleCommandError(error);
199
- }
200
- });
201
- }
@@ -1,91 +0,0 @@
1
- /**
2
- * Shared CLI test utilities: suppresses logs and provides command lookup.
3
- *
4
- * Every CLI test file should import this module. It:
5
- * 1. Mutes all consola loggers at import time (level -999 + no-op reporter)
6
- * 2. Re-mutes before every test (guards against tests that unlock/reconfigure)
7
- *
8
- * Note: process.exitCode must be reset in each test file's own beforeEach/afterEach.
9
- */
10
- import { beforeEach } from "bun:test";
11
- import {
12
- configLog,
13
- configureLogger,
14
- controlLog,
15
- engineLog,
16
- faultLog,
17
- lifecycleLog,
18
- lockLogger,
19
- logger,
20
- mqttLog,
21
- unlockLogger,
22
- } from "../logger.ts";
23
-
24
- const noopReporter = { log() {} };
25
- const allLoggers = [logger, engineLog, mqttLog, configLog, lifecycleLog, faultLog, controlLog];
26
-
27
- export function muteAllLoggers(): void {
28
- unlockLogger();
29
- configureLogger({ quiet: true });
30
- for (const l of allLoggers) {
31
- l.level = -999;
32
- l.setReporters([noopReporter]);
33
- }
34
- lockLogger();
35
- }
36
-
37
- muteAllLoggers();
38
-
39
- beforeEach(() => {
40
- muteAllLoggers();
41
- });
42
-
43
- /** Resets process.exitCode — pass to beforeEach/afterEach in test files. */
44
- export const resetExitCode = () => {
45
- process.exitCode = 0;
46
- };
47
-
48
- /** Creates a program and finds a command by name. */
49
- export async function findCliCommand(name: string) {
50
- const { createProgram } = await import("../index.ts");
51
- const program = createProgram();
52
- const cmd = program.commands.find((c) => c.name() === name);
53
- return { program, cmd };
54
- }
55
-
56
- /**
57
- * Creates a CLI program with exitOverride, runs parseAsync with the given args,
58
- * and swallows any Commander exitOverride errors.
59
- */
60
- export async function runCliCommand(args: string[]): Promise<void> {
61
- const { createProgram } = await import("../index.ts");
62
- const program = createProgram();
63
- program.exitOverride();
64
-
65
- try {
66
- await program.parseAsync(["node", "miravo", ...args]);
67
- } catch {
68
- // Commander may throw on exitOverride
69
- }
70
- }
71
-
72
- /**
73
- * Saves an env var, sets it to a new value, runs the callback, then restores it.
74
- */
75
- export async function withEnvVar<T>(
76
- key: string,
77
- value: string,
78
- fn: () => T | Promise<T>,
79
- ): Promise<T> {
80
- const prev = Bun.env[key];
81
- Bun.env[key] = value;
82
- try {
83
- return await fn();
84
- } finally {
85
- if (prev === undefined) {
86
- delete Bun.env[key];
87
- } else {
88
- Bun.env[key] = prev;
89
- }
90
- }
91
- }
@@ -1,51 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { findCliCommand, resetExitCode } from "./cli-test-setup.ts";
3
-
4
- describe("miravo export le-tags", () => {
5
- test("export command is registered", async () => {
6
- resetExitCode();
7
- const { cmd } = await findCliCommand("export");
8
- expect(cmd).toBeDefined();
9
- });
10
-
11
- test("le-tags subcommand is registered", async () => {
12
- resetExitCode();
13
- const { cmd } = await findCliCommand("export");
14
- const leTags = cmd?.commands.find((c) => c.name() === "le-tags");
15
- expect(leTags).toBeDefined();
16
- expect(leTags?.description()).toContain("Litmus Edge");
17
- });
18
-
19
- test("le-tags has required --device option", async () => {
20
- resetExitCode();
21
- const { cmd } = await findCliCommand("export");
22
- const leTags = cmd?.commands.find((c) => c.name() === "le-tags");
23
- const deviceOpt = leTags?.options.find((o) => o.long === "--device");
24
- expect(deviceOpt).toBeDefined();
25
- expect(deviceOpt?.required).toBe(true);
26
- });
27
-
28
- test("le-tags has --instance option", async () => {
29
- resetExitCode();
30
- const { cmd } = await findCliCommand("export");
31
- const leTags = cmd?.commands.find((c) => c.name() === "le-tags");
32
- const instanceOpt = leTags?.options.find((o) => o.long === "--instance");
33
- expect(instanceOpt).toBeDefined();
34
- });
35
-
36
- test("le-tags has --output option", async () => {
37
- resetExitCode();
38
- const { cmd } = await findCliCommand("export");
39
- const leTags = cmd?.commands.find((c) => c.name() === "le-tags");
40
- const outputOpt = leTags?.options.find((o) => o.long === "--output");
41
- expect(outputOpt).toBeDefined();
42
- });
43
-
44
- test("le-tags has --protocol option", async () => {
45
- resetExitCode();
46
- const { cmd } = await findCliCommand("export");
47
- const leTags = cmd?.commands.find((c) => c.name() === "le-tags");
48
- const protocolOpt = leTags?.options.find((o) => o.long === "--protocol");
49
- expect(protocolOpt).toBeDefined();
50
- });
51
- });