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,788 +0,0 @@
1
- /**
2
- * SSE streaming handlers for platform apply/cleanup operations.
3
- *
4
- * Emits server-sent events as resources are created/updated/deleted,
5
- * giving the console real-time progress feedback.
6
- */
7
-
8
- import type { EventLog } from "@miravo/core";
9
- import { colors } from "consola/utils";
10
- import { platformLog } from "../logger.ts";
11
- import { formatDuration } from "../output.ts";
12
- import {
13
- DEFAULT_APPLY_SCOPE,
14
- DEFAULT_CLEANUP_SCOPE,
15
- PlatformCleanupBodySchema,
16
- resolveApplyContext,
17
- } from "./admin-platform-routes.ts";
18
- import type { ResourceProgress } from "./le/le-apply.ts";
19
- import { buildApplyPlan, executePlan } from "./le/le-apply.ts";
20
- import { buildCleanupPlan, executeCleanup } from "./le/le-cleanup.ts";
21
- import { extractTemplateId } from "./le/le-cli-helpers.ts";
22
- import { LEClient } from "./le/le-client.ts";
23
- import { formatThrownError } from "./le/le-concurrency.ts";
24
- import { LEDeviceHub } from "./le/le-devicehub.ts";
25
- import { LEDigitalTwins } from "./le/le-digital-twins.ts";
26
- import { DEFAULT_PLATFORM_ID, getEnvironment, touchEnvironment } from "./le/le-environments.ts";
27
- import type { ApplyPlan } from "./le/le-plan.ts";
28
- import {
29
- appendPlatformOperation,
30
- createPlatformOperationTracker,
31
- } from "./le/le-platform-operations.ts";
32
- import { createCatalog, resolveTemplateIdentifier } from "./template-locator.ts";
33
-
34
- // --- Active operation registry ---
35
-
36
- const activeOperations = new Map<string, AbortController>();
37
-
38
- /** Cancel an in-progress streaming operation. Returns true if found and aborted. */
39
- export function cancelOperation(operationId: string): boolean {
40
- const ac = activeOperations.get(operationId);
41
- if (!ac) return false;
42
- ac.abort();
43
- return true;
44
- }
45
-
46
- // --- SSE helpers ---
47
-
48
- type SSESend = (event: string, data: unknown) => void;
49
-
50
- function createSSEResponse(handler: (send: SSESend) => Promise<void>): Response {
51
- return new Response(
52
- new ReadableStream({
53
- async start(controller) {
54
- const encoder = new TextEncoder();
55
- const send: SSESend = (event, data) => {
56
- try {
57
- controller.enqueue(
58
- encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`),
59
- );
60
- } catch {
61
- // Ignore errors if the stream controller is already closed (e.g., client disconnected)
62
- }
63
- };
64
- try {
65
- await handler(send);
66
- } catch (err) {
67
- const msg = err instanceof Error ? err.message : String(err);
68
- send("error", { message: msg });
69
- } finally {
70
- try {
71
- controller.close();
72
- } catch {
73
- // Ignore if already closed
74
- }
75
- }
76
- },
77
- }),
78
- {
79
- headers: {
80
- "Content-Type": "text/event-stream",
81
- "Cache-Control": "no-cache",
82
- Connection: "keep-alive",
83
- },
84
- },
85
- );
86
- }
87
-
88
- // --- Apply stream ---
89
-
90
- export function handleApplyStream(body: unknown, eventLog?: EventLog | undefined): Response {
91
- return createSSEResponse(async (send) => {
92
- const ctx = await resolveApplyContext(body);
93
- if (ctx instanceof Response) {
94
- const errorBody = (await ctx.json()) as { message?: string; error?: string };
95
- send("error", { message: errorBody.message ?? "Request validation failed." });
96
- return;
97
- }
98
-
99
- touchEnvironment(ctx.req.environmentId).catch(() => {});
100
-
101
- const operationId = crypto.randomUUID();
102
- const startTime = performance.now();
103
- const startedAt = Date.now();
104
-
105
- const scope = ctx.req.scope ?? DEFAULT_APPLY_SCOPE;
106
- const ac = new AbortController();
107
- activeOperations.set(operationId, ac);
108
-
109
- let totalAppliedCount = 0;
110
- let totalFailedCount = 0;
111
- let currentPlan: ApplyPlan | undefined;
112
-
113
- const maxAttempts = 3;
114
- let attempt = 0;
115
- const finalActivityLogs: Parameters<NonNullable<EventLog>["pushExternal"]>[0][] = [];
116
- const tracker = createPlatformOperationTracker({
117
- operationId,
118
- operationType: "apply",
119
- templateId: ctx.templateId,
120
- environmentId: ctx.req.environmentId,
121
- environmentLabel: ctx.env.label,
122
- hostname: ctx.env.hostname,
123
- scope,
124
- filters: { area: ctx.req.area, line: ctx.req.line, instance: ctx.req.instance },
125
- startedAt,
126
- });
127
-
128
- try {
129
- while (attempt < maxAttempts) {
130
- attempt++;
131
-
132
- try {
133
- const planOpts = {
134
- templateId: ctx.templateId,
135
- targetHost: ctx.env.hostname,
136
- sourceHostname: ctx.env.sourceHostname,
137
- scope,
138
- filters: { area: ctx.req.area, line: ctx.req.line, instance: ctx.req.instance },
139
- ifUnmanaged: ctx.req.ifUnmanaged ?? "fail",
140
- dryRun: false,
141
- };
142
-
143
- const { plan, snapshot } = await buildApplyPlan(ctx.hub, ctx.dt, ctx.desired, planOpts);
144
- currentPlan = plan;
145
-
146
- const totalPlanned = plan.entries.length;
147
- const phaseTotals = computePhaseTotals(plan.entries);
148
-
149
- send("started", {
150
- operationId,
151
- type: "apply",
152
- template: ctx.req.template,
153
- model: ctx.req.model,
154
- environment: ctx.env.label,
155
- totalPlanned,
156
- attempt,
157
- maxAttempts,
158
- });
159
-
160
- platformLog.start(
161
- `apply ${colors.bold(ctx.templateId)} ${colors.dim("\u2192")} ${colors.cyan(ctx.env.label)} · ${colors.bold(String(totalPlanned))} planned`,
162
- );
163
-
164
- let currentPhase = "";
165
- const applyOpts = {
166
- ...planOpts,
167
- onResourceComplete: (progress: ResourceProgress) => {
168
- if (progress.status === "success") {
169
- totalAppliedCount++;
170
- } else {
171
- totalFailedCount++;
172
- }
173
-
174
- tracker.onResourceComplete(progress);
175
- if (progress.phase !== currentPhase) {
176
- currentPhase = progress.phase;
177
- send("phase", {
178
- operationId,
179
- phase: currentPhase,
180
- total: phaseTotals.get(currentPhase) ?? 0,
181
- });
182
- }
183
- send("resource", {
184
- operationId,
185
- phase: progress.phase,
186
- name: progress.name,
187
- action: progress.action,
188
- status: progress.status,
189
- ...(progress.error ? { error: progress.error } : {}),
190
- index: progress.index,
191
- of: progress.of,
192
- });
193
-
194
- finalActivityLogs.push({
195
- type: "platform:resource",
196
- operationId,
197
- operationType: "apply",
198
- templateId: ctx.templateId,
199
- environmentLabel: ctx.env.label,
200
- hostname: ctx.env.hostname,
201
- phase: progress.phase,
202
- resourceType: progress.resourceType,
203
- resourceName: progress.name,
204
- action: progress.action,
205
- status: progress.status,
206
- ...(progress.error ? { error: progress.error } : {}),
207
- });
208
- },
209
- };
210
-
211
- const blockers = plan.entries.filter((e) => e.action === "blocked");
212
- if (blockers.length > 0) {
213
- const duration = Math.round(performance.now() - startTime);
214
- const completedAt = Date.now();
215
- platformLog.warn(
216
- `apply blocked · ${colors.yellow(String(blockers.length))} blocked · ${formatDuration(duration)}`,
217
- );
218
-
219
- const blockerIndices = new Map<string, number>();
220
- for (const b of blockers) {
221
- const idx = (blockerIndices.get(b.resourceType) ?? 0) + 1;
222
- blockerIndices.set(b.resourceType, idx);
223
-
224
- applyOpts.onResourceComplete({
225
- phase: b.resourceType,
226
- resourceType: b.resourceType,
227
- name: b.name,
228
- action: b.action,
229
- status: "failed",
230
- error:
231
- "blockReason" in b && typeof b.blockReason === "string"
232
- ? b.blockReason
233
- : "Resource is blocked",
234
- index: idx,
235
- of: phaseTotals.get(b.resourceType) ?? blockers.length,
236
- });
237
- }
238
-
239
- send("complete", {
240
- operationId,
241
- applied: totalAppliedCount,
242
- failed: totalFailedCount,
243
- duration,
244
- plan,
245
- });
246
- for (const log of finalActivityLogs) {
247
- eventLog?.pushExternal(log);
248
- }
249
- eventLog?.pushExternal({
250
- type: "platform:operation",
251
- operationId,
252
- operationType: "apply",
253
- templateId: ctx.templateId,
254
- environmentLabel: ctx.env.label,
255
- hostname: ctx.env.hostname,
256
- applied: totalAppliedCount,
257
- failed: totalFailedCount,
258
- duration,
259
- });
260
- await appendPlatformOperation(
261
- tracker.finalize({
262
- successCount: totalAppliedCount,
263
- failedCount: totalFailedCount,
264
- blockedCount: blockers.length,
265
- completedAt,
266
- }),
267
- ).catch(() => {});
268
- return;
269
- }
270
-
271
- const result = await executePlan(
272
- ctx.hub,
273
- ctx.dt,
274
- plan,
275
- ctx.desired,
276
- { ...applyOpts, signal: ac.signal },
277
- snapshot,
278
- );
279
- const duration = Math.round(performance.now() - startTime);
280
- const completedAt = Date.now();
281
-
282
- if (ac.signal.aborted) {
283
- platformLog.info(
284
- `apply cancelled · ${colors.green(String(totalAppliedCount))} applied · ${formatDuration(duration)}`,
285
- );
286
- send("cancelled", {
287
- operationId,
288
- applied: totalAppliedCount,
289
- failed: totalFailedCount,
290
- duration,
291
- plan: result.plan,
292
- });
293
- for (const log of finalActivityLogs) {
294
- eventLog?.pushExternal(log);
295
- }
296
- await appendPlatformOperation(
297
- tracker.finalize({
298
- successCount: totalAppliedCount,
299
- failedCount: totalFailedCount,
300
- completedAt,
301
- error: "Cancelled",
302
- }),
303
- ).catch(() => {});
304
- return;
305
- }
306
-
307
- if (result.failed > 0 && attempt < maxAttempts) {
308
- platformLog.warn(
309
- `apply attempt ${attempt} finished with ${colors.yellow(String(result.failed))} errors, retrying in 3s...`,
310
- );
311
- send("retrying", {
312
- operationId,
313
- attempt,
314
- maxAttempts,
315
- delayMs: 3000,
316
- error: `Attempt ${attempt} finished with ${result.failed} errors`,
317
- });
318
- await Bun.sleep(3000);
319
- continue;
320
- }
321
-
322
- if (result.failed > 0) {
323
- platformLog.warn(
324
- `apply done · ${colors.green(String(totalAppliedCount))} created · ${colors.yellow(String(totalFailedCount))} failed · ${formatDuration(duration)}`,
325
- );
326
- } else {
327
- platformLog.success(
328
- `apply done · ${colors.bold(String(totalAppliedCount))} created · ${formatDuration(duration)}`,
329
- );
330
- }
331
-
332
- send("complete", {
333
- operationId,
334
- applied: totalAppliedCount,
335
- failed: totalFailedCount,
336
- duration,
337
- plan: result.plan,
338
- });
339
-
340
- for (const log of finalActivityLogs) {
341
- eventLog?.pushExternal(log);
342
- }
343
- eventLog?.pushExternal({
344
- type: "platform:operation",
345
- operationId,
346
- operationType: "apply",
347
- templateId: ctx.templateId,
348
- environmentLabel: ctx.env.label,
349
- hostname: ctx.env.hostname,
350
- applied: totalAppliedCount,
351
- failed: totalFailedCount,
352
- duration,
353
- });
354
- await appendPlatformOperation(
355
- tracker.finalize({
356
- successCount: totalAppliedCount,
357
- failedCount: totalFailedCount,
358
- completedAt,
359
- ...(result.error ? { error: result.error } : {}),
360
- }),
361
- ).catch(() => {});
362
- return;
363
- } catch (err) {
364
- const duration = Math.round(performance.now() - startTime);
365
- const completedAt = Date.now();
366
-
367
- if (ac.signal.aborted || (err instanceof Error && err.name === "AbortError")) {
368
- platformLog.info(
369
- `apply cancelled · ${colors.green(String(totalAppliedCount))} applied · ${formatDuration(duration)}`,
370
- );
371
- send("cancelled", {
372
- operationId,
373
- applied: totalAppliedCount,
374
- failed: totalFailedCount,
375
- duration,
376
- plan: currentPlan ?? {
377
- sourceMode: "static",
378
- targetHost: ctx.env.hostname,
379
- scope: [],
380
- filters: {},
381
- entries: [],
382
- },
383
- });
384
- for (const log of finalActivityLogs) {
385
- eventLog?.pushExternal(log);
386
- }
387
- await appendPlatformOperation(
388
- tracker.finalize({
389
- successCount: totalAppliedCount,
390
- failedCount: totalFailedCount,
391
- completedAt,
392
- error: "Cancelled",
393
- }),
394
- ).catch(() => {});
395
- return;
396
- }
397
-
398
- const message = formatThrownError(err);
399
-
400
- if (attempt < maxAttempts) {
401
- platformLog.warn(`apply attempt ${attempt} failed: ${message}, retrying in 3s...`);
402
- send("retrying", {
403
- operationId,
404
- attempt,
405
- maxAttempts,
406
- delayMs: 3000,
407
- error: message,
408
- });
409
- await Bun.sleep(3000);
410
- continue;
411
- }
412
-
413
- platformLog.error(`apply failed · ${message}`);
414
- for (const log of finalActivityLogs) {
415
- eventLog?.pushExternal(log);
416
- }
417
- await appendPlatformOperation(
418
- tracker.finalize({
419
- successCount: totalAppliedCount,
420
- failedCount: totalFailedCount || 1,
421
- completedAt,
422
- error: message,
423
- }),
424
- ).catch(() => {});
425
- throw err;
426
- }
427
- }
428
- } finally {
429
- activeOperations.delete(operationId);
430
- }
431
- });
432
- }
433
-
434
- // --- Cleanup stream ---
435
-
436
- export function handleCleanupStream(body: unknown, eventLog?: EventLog | undefined): Response {
437
- return createSSEResponse(async (send) => {
438
- const parsed = PlatformCleanupBodySchema.safeParse(body);
439
- if (!parsed.success) {
440
- send("error", { message: parsed.error.issues.map((i) => i.message).join("; ") });
441
- return;
442
- }
443
- const req = parsed.data;
444
-
445
- const env = await getEnvironment(req.environmentId);
446
- if (!env) {
447
- send("error", { message: "Environment not found." });
448
- return;
449
- }
450
- if (env.platformId !== DEFAULT_PLATFORM_ID) {
451
- send("error", {
452
- message: `Environment "${env.label}" uses unsupported platform "${env.platformId}".`,
453
- });
454
- return;
455
- }
456
-
457
- touchEnvironment(req.environmentId).catch(() => {});
458
-
459
- const allManaged = req.allManaged ?? false;
460
- if (!allManaged && !req.template) {
461
- send("error", { message: "Provide 'template' or set 'allManaged: true'." });
462
- return;
463
- }
464
-
465
- const operationId = crypto.randomUUID();
466
- const startTime = performance.now();
467
- const startedAt = Date.now();
468
-
469
- const client = new LEClient({ hostname: env.hostname, apiToken: env.apiToken });
470
- const hub = new LEDeviceHub(client);
471
- const dt = new LEDigitalTwins(client);
472
- await client.checkAuth();
473
-
474
- let templateId = "all-managed";
475
- if (!allManaged && req.template) {
476
- const catalog = createCatalog();
477
- const templatePath = await resolveTemplateIdentifier(req.template, catalog);
478
- templateId = extractTemplateId(templatePath);
479
- }
480
-
481
- const scope = req.scope ?? DEFAULT_CLEANUP_SCOPE;
482
- const ac = new AbortController();
483
- activeOperations.set(operationId, ac);
484
-
485
- let totalDeletedCount = 0;
486
- let totalFailedCount = 0;
487
- let currentPlan: ApplyPlan | undefined;
488
-
489
- const maxAttempts = 3;
490
- let attempt = 0;
491
- const finalActivityLogs: Parameters<NonNullable<EventLog>["pushExternal"]>[0][] = [];
492
- const tracker = createPlatformOperationTracker({
493
- operationId,
494
- operationType: "cleanup",
495
- templateId,
496
- environmentId: req.environmentId,
497
- environmentLabel: env.label,
498
- hostname: env.hostname,
499
- scope,
500
- filters: { area: req.area, line: req.line, instance: req.instance },
501
- startedAt,
502
- });
503
-
504
- try {
505
- while (attempt < maxAttempts) {
506
- attempt++;
507
-
508
- try {
509
- const basePlanOpts = {
510
- templateId,
511
- targetHost: env.hostname,
512
- scope,
513
- filters: { area: req.area, line: req.line, instance: req.instance },
514
- allManaged,
515
- dryRun: false,
516
- };
517
-
518
- const planResult = await buildCleanupPlan(hub, dt, basePlanOpts);
519
- currentPlan = planResult.plan;
520
- const totalPlanned = planResult.plan.entries.length;
521
- const phaseTotals = computePhaseTotals(planResult.plan.entries);
522
-
523
- send("started", {
524
- operationId,
525
- type: "cleanup",
526
- template: req.template,
527
- environment: env.label,
528
- totalPlanned,
529
- attempt,
530
- maxAttempts,
531
- });
532
-
533
- platformLog.start(
534
- `cleanup ${colors.bold(templateId)} ${colors.dim("\u2192")} ${colors.cyan(env.label)} · ${colors.bold(String(totalPlanned))} planned`,
535
- );
536
-
537
- let currentPhase = "";
538
- const cleanupOpts = {
539
- ...basePlanOpts,
540
- onResourceComplete: (progress: ResourceProgress) => {
541
- if (progress.status === "success") {
542
- totalDeletedCount++;
543
- } else {
544
- totalFailedCount++;
545
- }
546
-
547
- tracker.onResourceComplete(progress);
548
- if (progress.phase !== currentPhase) {
549
- currentPhase = progress.phase;
550
- send("phase", {
551
- operationId,
552
- phase: currentPhase,
553
- total: phaseTotals.get(currentPhase) ?? 0,
554
- });
555
- }
556
- send("resource", {
557
- operationId,
558
- phase: progress.phase,
559
- name: progress.name,
560
- action: progress.action,
561
- status: progress.status,
562
- ...(progress.error ? { error: progress.error } : {}),
563
- index: progress.index,
564
- of: progress.of,
565
- });
566
-
567
- finalActivityLogs.push({
568
- type: "platform:resource",
569
- operationId,
570
- operationType: "cleanup",
571
- templateId,
572
- environmentLabel: env.label,
573
- hostname: env.hostname,
574
- phase: progress.phase,
575
- resourceType: progress.resourceType,
576
- resourceName: progress.name,
577
- action: progress.action,
578
- status: progress.status,
579
- ...(progress.error ? { error: progress.error } : {}),
580
- });
581
- },
582
- };
583
-
584
- if (planResult.plan.entries.length === 0) {
585
- const duration = Math.round(performance.now() - startTime);
586
- const completedAt = Date.now();
587
- platformLog.info(`cleanup done · nothing to remove · ${formatDuration(duration)}`);
588
- send("complete", {
589
- operationId,
590
- deleted: totalDeletedCount,
591
- failed: totalFailedCount,
592
- duration,
593
- plan: planResult.plan,
594
- });
595
- for (const log of finalActivityLogs) {
596
- eventLog?.pushExternal(log);
597
- }
598
- eventLog?.pushExternal({
599
- type: "platform:operation",
600
- operationId,
601
- operationType: "cleanup",
602
- templateId,
603
- environmentLabel: env.label,
604
- hostname: env.hostname,
605
- applied: totalDeletedCount,
606
- failed: totalFailedCount,
607
- duration,
608
- });
609
- await appendPlatformOperation(
610
- tracker.finalize({
611
- successCount: totalDeletedCount,
612
- failedCount: totalFailedCount,
613
- completedAt,
614
- }),
615
- ).catch(() => {});
616
- return;
617
- }
618
-
619
- const result = await executeCleanup(hub, dt, planResult, {
620
- ...cleanupOpts,
621
- signal: ac.signal,
622
- });
623
- const duration = Math.round(performance.now() - startTime);
624
- const completedAt = Date.now();
625
-
626
- if (ac.signal.aborted) {
627
- platformLog.info(
628
- `cleanup cancelled · ${colors.green(String(totalDeletedCount))} deleted · ${formatDuration(duration)}`,
629
- );
630
- send("cancelled", {
631
- operationId,
632
- deleted: totalDeletedCount,
633
- failed: totalFailedCount,
634
- duration,
635
- plan: result.plan,
636
- });
637
- for (const log of finalActivityLogs) {
638
- eventLog?.pushExternal(log);
639
- }
640
- await appendPlatformOperation(
641
- tracker.finalize({
642
- successCount: totalDeletedCount,
643
- failedCount: totalFailedCount,
644
- completedAt,
645
- error: "Cancelled",
646
- }),
647
- ).catch(() => {});
648
- return;
649
- }
650
-
651
- if (result.failed > 0 && attempt < maxAttempts) {
652
- platformLog.warn(
653
- `cleanup attempt ${attempt} finished with ${colors.yellow(String(result.failed))} errors, retrying in 3s...`,
654
- );
655
- send("retrying", {
656
- operationId,
657
- attempt,
658
- maxAttempts,
659
- delayMs: 3000,
660
- error: `Attempt ${attempt} finished with ${result.failed} errors`,
661
- });
662
- await Bun.sleep(3000);
663
- continue;
664
- }
665
-
666
- if (result.failed > 0) {
667
- platformLog.warn(
668
- `cleanup done · ${colors.green(String(totalDeletedCount))} deleted · ${colors.yellow(String(totalFailedCount))} failed · ${formatDuration(duration)}`,
669
- );
670
- } else {
671
- platformLog.success(
672
- `cleanup done · ${colors.bold(String(totalDeletedCount))} deleted · ${formatDuration(duration)}`,
673
- );
674
- }
675
-
676
- send("complete", {
677
- operationId,
678
- deleted: totalDeletedCount,
679
- failed: totalFailedCount,
680
- duration,
681
- plan: result.plan,
682
- });
683
-
684
- for (const log of finalActivityLogs) {
685
- eventLog?.pushExternal(log);
686
- }
687
- eventLog?.pushExternal({
688
- type: "platform:operation",
689
- operationId,
690
- operationType: "cleanup",
691
- templateId,
692
- environmentLabel: env.label,
693
- hostname: env.hostname,
694
- applied: totalDeletedCount,
695
- failed: totalFailedCount,
696
- duration,
697
- });
698
- await appendPlatformOperation(
699
- tracker.finalize({
700
- successCount: totalDeletedCount,
701
- failedCount: totalFailedCount,
702
- completedAt,
703
- ...(result.error ? { error: result.error } : {}),
704
- }),
705
- ).catch(() => {});
706
- return;
707
- } catch (err) {
708
- const duration = Math.round(performance.now() - startTime);
709
- const completedAt = Date.now();
710
-
711
- if (ac.signal.aborted || (err instanceof Error && err.name === "AbortError")) {
712
- platformLog.info(
713
- `cleanup cancelled · ${colors.green(String(totalDeletedCount))} deleted · ${formatDuration(duration)}`,
714
- );
715
- send("cancelled", {
716
- operationId,
717
- deleted: totalDeletedCount,
718
- failed: totalFailedCount,
719
- duration,
720
- plan: currentPlan ?? {
721
- sourceMode: "static",
722
- targetHost: env.hostname,
723
- scope: [],
724
- filters: {},
725
- entries: [],
726
- },
727
- });
728
- for (const log of finalActivityLogs) {
729
- eventLog?.pushExternal(log);
730
- }
731
- await appendPlatformOperation(
732
- tracker.finalize({
733
- successCount: totalDeletedCount,
734
- failedCount: totalFailedCount,
735
- completedAt,
736
- error: "Cancelled",
737
- }),
738
- ).catch(() => {});
739
- return;
740
- }
741
-
742
- const message = formatThrownError(err);
743
-
744
- if (attempt < maxAttempts) {
745
- platformLog.warn(`cleanup attempt ${attempt} failed: ${message}, retrying in 3s...`);
746
- send("retrying", {
747
- operationId,
748
- attempt,
749
- maxAttempts,
750
- delayMs: 3000,
751
- error: message,
752
- });
753
- await Bun.sleep(3000);
754
- continue;
755
- }
756
-
757
- platformLog.error(`cleanup failed · ${message}`);
758
- for (const log of finalActivityLogs) {
759
- eventLog?.pushExternal(log);
760
- }
761
- await appendPlatformOperation(
762
- tracker.finalize({
763
- successCount: totalDeletedCount,
764
- failedCount: totalFailedCount || 1,
765
- completedAt,
766
- error: message,
767
- }),
768
- ).catch(() => {});
769
- throw err;
770
- }
771
- }
772
- } finally {
773
- activeOperations.delete(operationId);
774
- }
775
- });
776
- }
777
-
778
- // --- Shared helpers ---
779
-
780
- function computePhaseTotals(
781
- entries: readonly { action: string; resourceType: string }[],
782
- ): Map<string, number> {
783
- const totals = new Map<string, number>();
784
- for (const entry of entries) {
785
- totals.set(entry.resourceType, (totals.get(entry.resourceType) ?? 0) + 1);
786
- }
787
- return totals;
788
- }