miravo 0.4.0 → 0.6.0
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/README.md +1 -1
- package/console-dist/assets/activity-DsWeRq4h.js +1 -0
- package/console-dist/assets/api-4AqKvsm9.js +1 -0
- package/console-dist/assets/arrow-right-Ea_UhTNi.js +1 -0
- package/console-dist/assets/arrow-up-down-DFSjzzl2.js +1 -0
- package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +1 -0
- package/console-dist/assets/box-C1WaoOMF.js +1 -0
- package/console-dist/assets/card-BjeP32PN.js +1 -0
- package/console-dist/assets/catalog-BJzyzbLR.js +1 -0
- package/console-dist/assets/catalog-DKjEm-c4.js +1 -0
- package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +1 -0
- package/console-dist/assets/checkbox-DZlJXTE4.js +1 -0
- package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +3 -0
- package/console-dist/assets/combobox-BGnuCYxO.js +1 -0
- package/console-dist/assets/control-CPhvwx9o.js +1 -0
- package/console-dist/assets/control-Cg2up9NA.js +1 -0
- package/console-dist/assets/copy-button-WkbNkMcd.js +1 -0
- package/console-dist/assets/createLucideIcon-Bwxw859K.js +1 -0
- package/console-dist/assets/deploy-tab-JeWDwPoZ.js +1 -0
- package/console-dist/assets/dist-BhbDML0a.js +1 -0
- package/console-dist/assets/dist-Cy8XTNFT.js +2 -0
- package/console-dist/assets/download-DQ6NH16U.js +1 -0
- package/console-dist/assets/engine-store-CrLnz692.js +1 -0
- package/console-dist/assets/events-CXm29Ok4.js +1 -0
- package/console-dist/assets/events-D01IxhnP.js +1 -0
- package/console-dist/assets/fleet-CfxbKXt9.js +1 -0
- package/console-dist/assets/fleet-lotMGVqe.js +1 -0
- package/console-dist/assets/format-kXTonpDJ.js +1 -0
- package/console-dist/assets/gauge-Ci8zNZlk.js +1 -0
- package/console-dist/assets/get-error-message-3Q5Q-2G5.js +1 -0
- package/console-dist/assets/index-CVPKSDvJ.js +12 -0
- package/console-dist/assets/index-DJkgDeun.css +2 -0
- package/console-dist/assets/input-BWEQFptx.js +1 -0
- package/console-dist/assets/instance-detail-C5IVwuh3.js +1 -0
- package/console-dist/assets/label-uephX5ZG.js +1 -0
- package/console-dist/assets/layers-CUsLQ1fq.js +1 -0
- package/console-dist/assets/login-C3RJli0m.js +126 -0
- package/console-dist/assets/models-B_UioWQ5.js +1 -0
- package/console-dist/assets/models-_IPlPiZ7.js +1 -0
- package/console-dist/assets/not-found-B8gAm08S.js +1 -0
- package/console-dist/assets/platform-targets-R3T-4eJ7.js +1 -0
- package/console-dist/assets/platforms-BWh0BQ3Y.js +1 -0
- package/console-dist/assets/platforms-DStNbtVc.js +1 -0
- package/console-dist/assets/proxy-CsnYqnPp.js +9 -0
- package/console-dist/assets/query-keys-BvtE7BMs.js +1 -0
- package/console-dist/assets/radio-CoHegOv5.js +1 -0
- package/console-dist/assets/rolldown-runtime-COnpUsM8.js +1 -0
- package/console-dist/assets/select-CeO4vP0G.js +1 -0
- package/console-dist/assets/select-display-items-CIzY8953.js +1 -0
- package/console-dist/assets/server-D9Kfn8fQ.js +1 -0
- package/console-dist/assets/shallow-6lGjQhtJ.js +1 -0
- package/console-dist/assets/tags-CiXOIHtY.js +4 -0
- package/console-dist/assets/tags-FQCXIxrP.js +1 -0
- package/console-dist/assets/tooltip-HGLF366Q.js +1 -0
- package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +1 -0
- package/console-dist/assets/transition-types-CPlGtM8w.js +1 -0
- package/console-dist/assets/triangle-alert-adNk_sea.js +1 -0
- package/console-dist/assets/use-platform-stream-DGbK1k2L.js +2 -0
- package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +2 -0
- package/console-dist/assets/validation-Blvud9_O.js +1 -0
- package/console-dist/assets/vendor-charts-CAZdaPlq.js +60 -0
- package/console-dist/assets/vendor-table-wd8JuMmT.js +4 -0
- package/console-dist/assets/vendor-ui-ByahnaY0.js +1 -0
- package/console-dist/index.html +70 -2
- package/package.json +9 -8
- package/src/__tests__/integration/executable-methods-runner.ts +7 -6
- package/src/__tests__/integration/executable-methods.integration.ts +214 -122
- package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +314 -0
- package/src/commands/adapter.ts +180 -7
- package/src/commands/apply.test.ts +62 -0
- package/src/commands/apply.ts +216 -0
- package/src/commands/cleanup.test.ts +34 -0
- package/src/commands/cleanup.ts +201 -0
- package/src/commands/export.test.ts +51 -0
- package/src/commands/export.ts +404 -0
- package/src/commands/fork.ts +2 -2
- package/src/commands/inject.ts +2 -2
- package/src/commands/load.ts +1 -1
- package/src/commands/logs.ts +13 -15
- package/src/commands/models.test.ts +1 -1
- package/src/commands/models.ts +1 -1
- package/src/commands/pause.ts +3 -3
- package/src/commands/platforms.test.ts +186 -0
- package/src/commands/platforms.ts +371 -0
- package/src/commands/remove.ts +1 -1
- package/src/commands/reset.ts +2 -2
- package/src/commands/spawn.ts +2 -2
- package/src/commands/speed.ts +2 -2
- package/src/commands/start/adapter-config.ts +157 -0
- package/src/commands/start/catalog-le-dt.test.ts +50 -0
- package/src/commands/start/catalog-le-dt.ts +187 -0
- package/src/commands/start/catalog-model-schema.ts +89 -0
- package/src/commands/start/catalog-template-detail.ts +54 -0
- package/src/commands/start/daemon.ts +255 -0
- package/src/commands/start/display.ts +150 -0
- package/src/commands/start/engine-setup.ts +247 -0
- package/src/commands/start/events.ts +126 -0
- package/src/commands/start/index.ts +897 -0
- package/src/commands/start/protocol-plans.ts +239 -0
- package/src/commands/start/template-loader.ts +197 -0
- package/src/commands/start/template-resolver.ts +185 -0
- package/src/commands/start/types.ts +73 -0
- package/src/commands/start.test.ts +137 -16
- package/src/commands/status.ts +1 -1
- package/src/commands/stop.ts +19 -8
- package/src/commands/templates.test.ts +1 -1
- package/src/commands/templates.ts +1 -3
- package/src/index.ts +11 -3
- package/src/lib/admin-auth.ts +157 -0
- package/src/lib/admin-client.test.ts +31 -15
- package/src/lib/admin-client.ts +106 -6
- package/src/lib/admin-platform-routes.ts +1017 -0
- package/src/lib/admin-platform-stream.test.ts +434 -0
- package/src/lib/admin-platform-stream.ts +788 -0
- package/src/lib/admin-responses.ts +211 -0
- package/src/lib/admin-server.test.ts +274 -55
- package/src/lib/admin-server.ts +387 -248
- package/src/lib/auth-store.ts +87 -0
- package/src/lib/daemon-registry.ts +12 -14
- package/src/lib/le/le-apply.test.ts +591 -0
- package/src/lib/le/le-apply.ts +832 -0
- package/src/lib/le/le-auth.ts +89 -0
- package/src/lib/le/le-cleanup.test.ts +168 -0
- package/src/lib/le/le-cleanup.ts +399 -0
- package/src/lib/le/le-cli-helpers.ts +77 -0
- package/src/lib/le/le-client.ts +184 -0
- package/src/lib/le/le-concurrency.test.ts +157 -0
- package/src/lib/le/le-concurrency.ts +78 -0
- package/src/lib/le/le-connection-test.ts +42 -0
- package/src/lib/le/le-devicehub-properties.test.ts +53 -0
- package/src/lib/le/le-devicehub-properties.ts +33 -0
- package/src/lib/le/le-devicehub.ts +213 -0
- package/src/lib/le/le-digital-twins.ts +285 -0
- package/src/lib/le/le-drivers.ts +23 -0
- package/src/lib/le/le-endpoint.ts +40 -0
- package/src/lib/le/le-environments.test.ts +73 -0
- package/src/lib/le/le-environments.ts +156 -0
- package/src/lib/le/le-markers.ts +87 -0
- package/src/lib/le/le-plan.test.ts +26 -0
- package/src/lib/le/le-plan.ts +109 -0
- package/src/lib/le/le-platform-operations.test.ts +198 -0
- package/src/lib/le/le-platform-operations.ts +340 -0
- package/src/lib/le/le-static-model-schema.ts +78 -0
- package/src/lib/le/le-static-planner.test.ts +255 -0
- package/src/lib/le/le-static-planner.ts +442 -0
- package/src/lib/le/le-template-exporter.test.ts +527 -0
- package/src/lib/le/le-template-exporter.ts +459 -0
- package/src/lib/le/le-value-types.ts +23 -0
- package/src/lib/mqtt-client.ts +1 -2
- package/src/lib/resolve-simulation.ts +1 -6
- package/src/lib/session-store.ts +110 -0
- package/src/lib/shutdown.test.ts +184 -2
- package/src/lib/shutdown.ts +104 -34
- package/src/lib/template-category.ts +3 -3
- package/src/lib/template-locator.ts +8 -2
- package/src/lib/ws-hub.ts +72 -46
- package/src/logger.ts +39 -14
- package/src/output.ts +3 -0
- package/console-dist/assets/index-C3XMYq26.css +0 -2
- package/console-dist/assets/index-hUJSpKtB.js +0 -119
- package/src/commands/start.ts +0 -2023
|
@@ -27,7 +27,6 @@ const BUILTIN_TEMPLATE_PATHS = [
|
|
|
27
27
|
resolve(stressTemplatesDir, "stress-10k.miravo.yaml"),
|
|
28
28
|
resolve(stressTemplatesDir, "stress-50k.miravo.yaml"),
|
|
29
29
|
resolve(stressTemplatesDir, "stress-100k.miravo.yaml"),
|
|
30
|
-
resolve(stressTemplatesDir, "stress-200k.miravo.yaml"),
|
|
31
30
|
];
|
|
32
31
|
|
|
33
32
|
beforeEach(resetExitCode);
|
|
@@ -157,7 +156,7 @@ function createTemplateWithNamespace(namespace?: MiravoTemplate["namespace"]): M
|
|
|
157
156
|
|
|
158
157
|
describe("resolveSimulationName", () => {
|
|
159
158
|
test("priority: --name > MIRAVO_NAME > namespace", async () => {
|
|
160
|
-
const { resolveSimulationName } = await import("./start.ts");
|
|
159
|
+
const { resolveSimulationName } = await import("./start/index.ts");
|
|
161
160
|
const template = createTemplateWithNamespace({ enterprise: "acme", site: "site-a" });
|
|
162
161
|
|
|
163
162
|
expect(
|
|
@@ -172,20 +171,20 @@ describe("resolveSimulationName", () => {
|
|
|
172
171
|
});
|
|
173
172
|
|
|
174
173
|
test("uses flat namespace prefix when ISA-95 site is not present", async () => {
|
|
175
|
-
const { resolveSimulationName } = await import("./start.ts");
|
|
174
|
+
const { resolveSimulationName } = await import("./start/index.ts");
|
|
176
175
|
const template = createTemplateWithNamespace({ prefix: "flat-prefix" });
|
|
177
176
|
expect(resolveSimulationName({ template })).toBe("flat-prefix");
|
|
178
177
|
});
|
|
179
178
|
|
|
180
179
|
test("throws when template has no namespace and no explicit name", async () => {
|
|
181
|
-
const { resolveSimulationName } = await import("./start.ts");
|
|
180
|
+
const { resolveSimulationName } = await import("./start/index.ts");
|
|
182
181
|
const template = createTemplateWithNamespace();
|
|
183
182
|
|
|
184
183
|
expect(() => resolveSimulationName({ template })).toThrow("Template has no namespace.");
|
|
185
184
|
});
|
|
186
185
|
|
|
187
186
|
test("rejects invalid names with special characters", async () => {
|
|
188
|
-
const { resolveSimulationName } = await import("./start.ts");
|
|
187
|
+
const { resolveSimulationName } = await import("./start/index.ts");
|
|
189
188
|
const template = createTemplateWithNamespace({ prefix: "valid" });
|
|
190
189
|
|
|
191
190
|
expect(() => resolveSimulationName({ template, cliName: "UPPER-CASE" })).toThrow(
|
|
@@ -202,7 +201,7 @@ describe("resolveSimulationName", () => {
|
|
|
202
201
|
});
|
|
203
202
|
|
|
204
203
|
test("accepts single-character name", async () => {
|
|
205
|
-
const { resolveSimulationName } = await import("./start.ts");
|
|
204
|
+
const { resolveSimulationName } = await import("./start/index.ts");
|
|
206
205
|
const template = createTemplateWithNamespace({ prefix: "x" });
|
|
207
206
|
expect(resolveSimulationName({ template })).toBe("x");
|
|
208
207
|
});
|
|
@@ -210,7 +209,7 @@ describe("resolveSimulationName", () => {
|
|
|
210
209
|
|
|
211
210
|
describe("resolveProtocolBootPlans", () => {
|
|
212
211
|
test("preserves restored adapter state when no explicit protocol overrides are provided", async () => {
|
|
213
|
-
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
212
|
+
const { resolveProtocolBootPlans } = await import("./start/index.ts");
|
|
214
213
|
|
|
215
214
|
expect(
|
|
216
215
|
resolveProtocolBootPlans({
|
|
@@ -221,11 +220,12 @@ describe("resolveProtocolBootPlans", () => {
|
|
|
221
220
|
).toEqual([
|
|
222
221
|
{ name: "mqtt", shouldEnable: true, overrideRestore: false },
|
|
223
222
|
{ name: "opcua", shouldEnable: false, overrideRestore: false },
|
|
223
|
+
{ name: "modbus", shouldEnable: false, overrideRestore: false },
|
|
224
224
|
]);
|
|
225
225
|
});
|
|
226
226
|
|
|
227
227
|
test("applies boot defaults when no persisted snapshot was restored", async () => {
|
|
228
|
-
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
228
|
+
const { resolveProtocolBootPlans } = await import("./start/index.ts");
|
|
229
229
|
|
|
230
230
|
expect(
|
|
231
231
|
resolveProtocolBootPlans({
|
|
@@ -236,11 +236,12 @@ describe("resolveProtocolBootPlans", () => {
|
|
|
236
236
|
).toEqual([
|
|
237
237
|
{ name: "mqtt", shouldEnable: true, overrideRestore: true },
|
|
238
238
|
{ name: "opcua", shouldEnable: false, overrideRestore: true },
|
|
239
|
+
{ name: "modbus", shouldEnable: false, overrideRestore: true },
|
|
239
240
|
]);
|
|
240
241
|
});
|
|
241
242
|
|
|
242
243
|
test("treats explicit MQTT disable as authoritative over persisted state", async () => {
|
|
243
|
-
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
244
|
+
const { resolveProtocolBootPlans } = await import("./start/index.ts");
|
|
244
245
|
|
|
245
246
|
expect(
|
|
246
247
|
resolveProtocolBootPlans({
|
|
@@ -252,7 +253,7 @@ describe("resolveProtocolBootPlans", () => {
|
|
|
252
253
|
});
|
|
253
254
|
|
|
254
255
|
test("treats explicit CLI MQTT config overrides as authoritative over persisted config", async () => {
|
|
255
|
-
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
256
|
+
const { resolveProtocolBootPlans } = await import("./start/index.ts");
|
|
256
257
|
|
|
257
258
|
expect(
|
|
258
259
|
resolveProtocolBootPlans({
|
|
@@ -264,7 +265,7 @@ describe("resolveProtocolBootPlans", () => {
|
|
|
264
265
|
});
|
|
265
266
|
|
|
266
267
|
test("does not treat env fallback config as authoritative over persisted MQTT state", async () => {
|
|
267
|
-
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
268
|
+
const { resolveProtocolBootPlans } = await import("./start/index.ts");
|
|
268
269
|
|
|
269
270
|
expect(
|
|
270
271
|
resolveProtocolBootPlans({
|
|
@@ -276,7 +277,7 @@ describe("resolveProtocolBootPlans", () => {
|
|
|
276
277
|
});
|
|
277
278
|
|
|
278
279
|
test("treats explicit OPC UA enable as authoritative over persisted state", async () => {
|
|
279
|
-
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
280
|
+
const { resolveProtocolBootPlans } = await import("./start/index.ts");
|
|
280
281
|
|
|
281
282
|
expect(
|
|
282
283
|
resolveProtocolBootPlans({
|
|
@@ -288,7 +289,7 @@ describe("resolveProtocolBootPlans", () => {
|
|
|
288
289
|
});
|
|
289
290
|
|
|
290
291
|
test("does not treat ignored OPC UA config flags as persisted-state overrides", async () => {
|
|
291
|
-
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
292
|
+
const { resolveProtocolBootPlans } = await import("./start/index.ts");
|
|
292
293
|
|
|
293
294
|
expect(
|
|
294
295
|
resolveProtocolBootPlans({
|
|
@@ -300,9 +301,129 @@ describe("resolveProtocolBootPlans", () => {
|
|
|
300
301
|
});
|
|
301
302
|
});
|
|
302
303
|
|
|
304
|
+
describe("resolveDetachAdapterConfigs", () => {
|
|
305
|
+
test("uses template adapter configs when no CLI/env overrides are provided", async () => {
|
|
306
|
+
const { resolveDetachAdapterConfigs } = await import("./start/index.ts");
|
|
307
|
+
|
|
308
|
+
const template = createTemplateWithNamespace({ prefix: "factory" });
|
|
309
|
+
const withProtocols: MiravoTemplate = {
|
|
310
|
+
...template,
|
|
311
|
+
protocols: {
|
|
312
|
+
mqtt: { port: 1889, embedded: true, retain: false, metadata: "base" },
|
|
313
|
+
opcua: { port: 4851, host: "127.0.0.1", security: "none" },
|
|
314
|
+
modbus: { port: 1602, host: "127.0.0.1" },
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
expect(
|
|
319
|
+
resolveDetachAdapterConfigs({
|
|
320
|
+
template: withProtocols,
|
|
321
|
+
simulationName: "factory-sim",
|
|
322
|
+
mqtt: { enabled: true, portOverride: undefined, overrides: {} },
|
|
323
|
+
opcua: {
|
|
324
|
+
enabled: true,
|
|
325
|
+
portOverride: undefined,
|
|
326
|
+
hostOverride: undefined,
|
|
327
|
+
writeBudgetOverride: undefined,
|
|
328
|
+
},
|
|
329
|
+
modbus: { enabled: true, portOverride: undefined, hostOverride: undefined },
|
|
330
|
+
}),
|
|
331
|
+
).toEqual({
|
|
332
|
+
mqtt: { port: 1889, host: "0.0.0.0", embedded: true },
|
|
333
|
+
opcua: { port: 4851, host: "127.0.0.1" },
|
|
334
|
+
modbus: { port: 1602, host: "127.0.0.1" },
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("CLI/env overrides take precedence over template values", async () => {
|
|
339
|
+
const { resolveDetachAdapterConfigs } = await import("./start/index.ts");
|
|
340
|
+
|
|
341
|
+
const template = createTemplateWithNamespace({ prefix: "factory" });
|
|
342
|
+
const withProtocols: MiravoTemplate = {
|
|
343
|
+
...template,
|
|
344
|
+
protocols: {
|
|
345
|
+
mqtt: { port: 1889, embedded: true, retain: false, metadata: "base" },
|
|
346
|
+
opcua: { port: 4851, host: "127.0.0.1", security: "none" },
|
|
347
|
+
modbus: { port: 1602, host: "127.0.0.1" },
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
expect(
|
|
352
|
+
resolveDetachAdapterConfigs({
|
|
353
|
+
template: withProtocols,
|
|
354
|
+
simulationName: "factory-sim",
|
|
355
|
+
mqtt: {
|
|
356
|
+
enabled: true,
|
|
357
|
+
portOverride: 1999,
|
|
358
|
+
overrides: { host: "0.0.0.0" },
|
|
359
|
+
},
|
|
360
|
+
opcua: {
|
|
361
|
+
enabled: true,
|
|
362
|
+
portOverride: 4852,
|
|
363
|
+
hostOverride: "0.0.0.0",
|
|
364
|
+
writeBudgetOverride: undefined,
|
|
365
|
+
},
|
|
366
|
+
modbus: { enabled: true, portOverride: 1702, hostOverride: "0.0.0.0" },
|
|
367
|
+
}),
|
|
368
|
+
).toEqual({
|
|
369
|
+
mqtt: { port: 1999, host: "0.0.0.0", embedded: true },
|
|
370
|
+
opcua: { port: 4852, host: "0.0.0.0" },
|
|
371
|
+
modbus: { port: 1702, host: "0.0.0.0" },
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("returns disabled placeholders when adapters are disabled", async () => {
|
|
376
|
+
const { resolveDetachAdapterConfigs } = await import("./start/index.ts");
|
|
377
|
+
const template = createTemplateWithNamespace({ prefix: "factory" });
|
|
378
|
+
|
|
379
|
+
expect(
|
|
380
|
+
resolveDetachAdapterConfigs({
|
|
381
|
+
template,
|
|
382
|
+
simulationName: "factory-sim",
|
|
383
|
+
mqtt: { enabled: false, portOverride: 1999, overrides: { host: "0.0.0.0" } },
|
|
384
|
+
opcua: {
|
|
385
|
+
enabled: false,
|
|
386
|
+
portOverride: 4852,
|
|
387
|
+
hostOverride: "0.0.0.0",
|
|
388
|
+
writeBudgetOverride: 4096,
|
|
389
|
+
},
|
|
390
|
+
modbus: { enabled: false, portOverride: 1702, hostOverride: "0.0.0.0" },
|
|
391
|
+
}),
|
|
392
|
+
).toEqual({
|
|
393
|
+
mqtt: { port: undefined, host: undefined, embedded: false },
|
|
394
|
+
opcua: { port: undefined, host: undefined },
|
|
395
|
+
modbus: { port: undefined, host: undefined },
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("marks external MQTT mode as non-embedded for detach conflict checks", async () => {
|
|
400
|
+
const { resolveDetachAdapterConfigs } = await import("./start/index.ts");
|
|
401
|
+
const template = createTemplateWithNamespace({ prefix: "factory" });
|
|
402
|
+
|
|
403
|
+
expect(
|
|
404
|
+
resolveDetachAdapterConfigs({
|
|
405
|
+
template,
|
|
406
|
+
simulationName: "factory-sim",
|
|
407
|
+
mqtt: {
|
|
408
|
+
enabled: true,
|
|
409
|
+
portOverride: undefined,
|
|
410
|
+
overrides: { mode: "external", url: "mqtt://broker.example.com:1883" },
|
|
411
|
+
},
|
|
412
|
+
opcua: {
|
|
413
|
+
enabled: false,
|
|
414
|
+
portOverride: undefined,
|
|
415
|
+
hostOverride: undefined,
|
|
416
|
+
writeBudgetOverride: undefined,
|
|
417
|
+
},
|
|
418
|
+
modbus: { enabled: false, portOverride: undefined, hostOverride: undefined },
|
|
419
|
+
}).mqtt.embedded,
|
|
420
|
+
).toBe(false);
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
303
424
|
describe("error context aggregation", () => {
|
|
304
425
|
test("normalizeErrorContext redacts high-cardinality fragments", async () => {
|
|
305
|
-
const { normalizeErrorContext } = await import("./start.ts");
|
|
426
|
+
const { normalizeErrorContext } = await import("./start/index.ts");
|
|
306
427
|
|
|
307
428
|
expect(normalizeErrorContext('generator="rpm" instance="pump-001" tick=42')).toBe(
|
|
308
429
|
'generator="rpm" instance="*" tick=*',
|
|
@@ -313,7 +434,7 @@ describe("error context aggregation", () => {
|
|
|
313
434
|
});
|
|
314
435
|
|
|
315
436
|
test("incrementErrorContextCount caps unique keys and accumulates in overflow bucket", async () => {
|
|
316
|
-
const { incrementErrorContextCount } = await import("./start.ts");
|
|
437
|
+
const { incrementErrorContextCount } = await import("./start/index.ts");
|
|
317
438
|
const counts = new Map<string, number>();
|
|
318
439
|
|
|
319
440
|
for (let i = 0; i < 256; i++) {
|
|
@@ -331,7 +452,7 @@ describe("error context aggregation", () => {
|
|
|
331
452
|
|
|
332
453
|
describe("template picker choices", () => {
|
|
333
454
|
test("buildBuiltinTemplateChoices renders tree labels directly in select options", async () => {
|
|
334
|
-
const { buildBuiltinTemplateChoices } = await import("./start.ts");
|
|
455
|
+
const { buildBuiltinTemplateChoices } = await import("./start/index.ts");
|
|
335
456
|
const choices = buildBuiltinTemplateChoices([
|
|
336
457
|
discoveredTemplate("stress-50k", "Stress Benchmark 50K"),
|
|
337
458
|
discoveredTemplate("water-treatment", "Water Treatment Plant"),
|
package/src/commands/status.ts
CHANGED
|
@@ -45,7 +45,7 @@ export function registerStatusCommand(program: Command): void {
|
|
|
45
45
|
.option("-w, --watch", "Poll the admin server and re-render on updates")
|
|
46
46
|
.option("--instance <id>", "Show detailed status for one instance")
|
|
47
47
|
.option("--json", "Machine-readable JSON output")
|
|
48
|
-
.option("--admin-url <url>", "
|
|
48
|
+
.option("--admin-url <url>", "Admin API URL (env: MIRAVO_ADMIN_URL)")
|
|
49
49
|
.option(
|
|
50
50
|
"--template <path>",
|
|
51
51
|
"Path to .miravo.yaml for MQTT discovery connection info (env: MIRAVO_TEMPLATE)",
|
package/src/commands/stop.ts
CHANGED
|
@@ -81,15 +81,26 @@ async function stopAction(name: string, options: StopOptions): Promise<void> {
|
|
|
81
81
|
let shutdownSent = false;
|
|
82
82
|
let canForceKill = true;
|
|
83
83
|
try {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
84
|
+
const adminBase = `http://${entry.adminHost}:${entry.adminPort}`;
|
|
85
|
+
// Authenticate with session-based login
|
|
86
|
+
// Resolve password: env > default.
|
|
87
|
+
// If credential file exists, we can't reverse the hash — use "admin" as default.
|
|
88
|
+
// The server will verify against the stored hash. If the user changed their password,
|
|
89
|
+
// they need to set MIRAVO_ADMIN_PASSWORD env var for CLI access.
|
|
90
|
+
const envPassword = Bun.env["MIRAVO_ADMIN_PASSWORD"];
|
|
91
|
+
const password = envPassword && envPassword.trim().length > 0 ? envPassword : "admin";
|
|
92
|
+
const loginResponse = await fetch(`${adminBase}/auth/login`, {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: { "content-type": "application/json" },
|
|
95
|
+
body: JSON.stringify({ username: "admin", password }),
|
|
96
|
+
signal: AbortSignal.timeout(2_000),
|
|
97
|
+
});
|
|
98
|
+
const setCookie = loginResponse.ok ? loginResponse.headers.get("set-cookie") : null;
|
|
99
|
+
const cookie = setCookie ? (setCookie.split(";")[0] ?? "") : "";
|
|
100
|
+
|
|
101
|
+
const response = await fetch(`${adminBase}/shutdown`, {
|
|
91
102
|
method: "POST",
|
|
92
|
-
headers,
|
|
103
|
+
headers: { "content-type": "application/json", ...(cookie ? { cookie } : {}) },
|
|
93
104
|
body: JSON.stringify({ reason: "miravo stop" }),
|
|
94
105
|
signal: AbortSignal.timeout(2_000),
|
|
95
106
|
});
|
|
@@ -36,7 +36,7 @@ describe("formatTemplateInspect", () => {
|
|
|
36
36
|
expect(output).toContain("Namespace:");
|
|
37
37
|
expect(output).toContain("Twin assignments:");
|
|
38
38
|
expect(output).toContain("Areas:");
|
|
39
|
-
expect(output).toContain("
|
|
39
|
+
expect(output).toContain("machining");
|
|
40
40
|
});
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -19,7 +19,7 @@ export function registerTemplatesCommand(program: Command): void {
|
|
|
19
19
|
templates
|
|
20
20
|
.command("list")
|
|
21
21
|
.description("Discover and list available templates")
|
|
22
|
-
.option("--dir <path>", "Additional directory to search for templates")
|
|
22
|
+
.option("--dir <path>", "Additional directory to search for .miravo.yaml templates")
|
|
23
23
|
.addHelpText(
|
|
24
24
|
"after",
|
|
25
25
|
`
|
|
@@ -275,5 +275,3 @@ export function formatTemplateInspect(
|
|
|
275
275
|
|
|
276
276
|
return lines.join("\n");
|
|
277
277
|
}
|
|
278
|
-
|
|
279
|
-
export type { DiscoveredTemplate };
|
package/src/index.ts
CHANGED
|
@@ -2,19 +2,23 @@
|
|
|
2
2
|
import { MIRAVO_VERSION } from "@miravo/shared";
|
|
3
3
|
import { Command, InvalidOptionArgumentError } from "commander";
|
|
4
4
|
import { registerAdapterCommand } from "./commands/adapter.ts";
|
|
5
|
+
import { registerApplyCommand } from "./commands/apply.ts";
|
|
6
|
+
import { registerCleanupCommand } from "./commands/cleanup.ts";
|
|
7
|
+
import { registerExportCommand } from "./commands/export.ts";
|
|
5
8
|
import { registerForkCommand } from "./commands/fork.ts";
|
|
6
9
|
import { registerInjectCommand } from "./commands/inject.ts";
|
|
7
10
|
import { registerLoadCommand } from "./commands/load.ts";
|
|
8
11
|
import { registerLogsCommand } from "./commands/logs.ts";
|
|
9
12
|
import { registerModelsCommand } from "./commands/models.ts";
|
|
10
13
|
import { registerPauseCommand, registerResumeCommand } from "./commands/pause.ts";
|
|
14
|
+
import { registerEnvironmentsCommand } from "./commands/platforms.ts";
|
|
11
15
|
import { registerPsCommand } from "./commands/ps.ts";
|
|
12
16
|
import { registerRemoveCommand } from "./commands/remove.ts";
|
|
13
17
|
import { registerResetCommand } from "./commands/reset.ts";
|
|
14
18
|
import { registerRmCommand } from "./commands/rm.ts";
|
|
15
19
|
import { registerSpawnCommand } from "./commands/spawn.ts";
|
|
16
20
|
import { registerSpeedCommand } from "./commands/speed.ts";
|
|
17
|
-
import { registerStartCommand } from "./commands/start.ts";
|
|
21
|
+
import { registerStartCommand } from "./commands/start/index.ts";
|
|
18
22
|
import { registerStatusCommand } from "./commands/status.ts";
|
|
19
23
|
import { registerStopCommand } from "./commands/stop.ts";
|
|
20
24
|
import { registerTemplatesCommand } from "./commands/templates.ts";
|
|
@@ -57,7 +61,7 @@ export function createProgram(): Command {
|
|
|
57
61
|
},
|
|
58
62
|
"pretty",
|
|
59
63
|
)
|
|
60
|
-
.addHelpText("afterAll", "\nDocs: https://
|
|
64
|
+
.addHelpText("afterAll", "\nDocs: https://miravo.dev/docs")
|
|
61
65
|
.action(() => {
|
|
62
66
|
writeOutput(`miravo v${MIRAVO_VERSION} — Industrial environment platform
|
|
63
67
|
|
|
@@ -67,7 +71,7 @@ Quick start:
|
|
|
67
71
|
$ miravo ps List background simulations
|
|
68
72
|
|
|
69
73
|
Run miravo --help for all commands.
|
|
70
|
-
Docs: https://
|
|
74
|
+
Docs: https://miravo.dev/docs`);
|
|
71
75
|
})
|
|
72
76
|
.hook("preAction", (_thisCommand, actionCommand) => {
|
|
73
77
|
const rootOpts = program.opts<{
|
|
@@ -111,6 +115,10 @@ Docs: https://github.com/amine-amaach/miravo`);
|
|
|
111
115
|
registerResetCommand(program);
|
|
112
116
|
registerInjectCommand(program);
|
|
113
117
|
registerAdapterCommand(program);
|
|
118
|
+
registerExportCommand(program);
|
|
119
|
+
registerApplyCommand(program);
|
|
120
|
+
registerCleanupCommand(program);
|
|
121
|
+
registerEnvironmentsCommand(program);
|
|
114
122
|
|
|
115
123
|
// Content discovery
|
|
116
124
|
registerModelsCommand(program);
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth route handlers for the Miravo admin server.
|
|
3
|
+
*
|
|
4
|
+
* Each exported function handles a single auth endpoint and returns a Response.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ChangePasswordRequestSchema, LoginRequestSchema } from "@miravo/shared";
|
|
8
|
+
import { checkContentType, jsonResponse } from "./admin-responses.ts";
|
|
9
|
+
import type { SessionStore } from "./session-store.ts";
|
|
10
|
+
import { buildClearCookie, buildSessionCookie, extractSessionId } from "./session-store.ts";
|
|
11
|
+
|
|
12
|
+
/** Minimal auth configuration needed by auth handlers. */
|
|
13
|
+
interface AuthConfig {
|
|
14
|
+
readonly verifyPassword: (password: string) => Promise<boolean>;
|
|
15
|
+
readonly isDefaultPassword: boolean;
|
|
16
|
+
readonly sessionStore: SessionStore;
|
|
17
|
+
readonly changePassword: (oldPassword: string, newPassword: string) => Promise<void>;
|
|
18
|
+
readonly anonymousMode?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* POST /auth/login
|
|
23
|
+
* Validates credentials and sets a session cookie on success.
|
|
24
|
+
*/
|
|
25
|
+
export async function handleLogin(request: Request, auth: AuthConfig): Promise<Response> {
|
|
26
|
+
if (auth.anonymousMode) {
|
|
27
|
+
const session = auth.sessionStore.create("anonymous");
|
|
28
|
+
return new Response(JSON.stringify({ ok: true, changePassword: false }), {
|
|
29
|
+
status: 200,
|
|
30
|
+
headers: {
|
|
31
|
+
"content-type": "application/json; charset=utf-8",
|
|
32
|
+
"set-cookie": buildSessionCookie(session.id),
|
|
33
|
+
"cache-control": "no-store",
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ctBlock = checkContentType(request);
|
|
39
|
+
if (ctBlock) return ctBlock;
|
|
40
|
+
|
|
41
|
+
let payload: unknown;
|
|
42
|
+
try {
|
|
43
|
+
payload = await request.json();
|
|
44
|
+
} catch {
|
|
45
|
+
return jsonResponse({ message: "Invalid JSON.", error: "Send valid JSON." }, 400);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const validation = LoginRequestSchema.safeParse(payload);
|
|
49
|
+
if (!validation.success) {
|
|
50
|
+
return jsonResponse(
|
|
51
|
+
{ message: "Invalid login request.", error: "Provide username and password." },
|
|
52
|
+
400,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (validation.data.username !== "admin") {
|
|
56
|
+
return jsonResponse(
|
|
57
|
+
{ message: "Invalid credentials.", error: "Check username and password." },
|
|
58
|
+
401,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const passwordValid = await auth.verifyPassword(validation.data.password);
|
|
62
|
+
if (!passwordValid) {
|
|
63
|
+
return jsonResponse(
|
|
64
|
+
{ message: "Invalid credentials.", error: "Check username and password." },
|
|
65
|
+
401,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const session = auth.sessionStore.create("admin");
|
|
70
|
+
return new Response(JSON.stringify({ ok: true, changePassword: auth.isDefaultPassword }), {
|
|
71
|
+
status: 200,
|
|
72
|
+
headers: {
|
|
73
|
+
"content-type": "application/json; charset=utf-8",
|
|
74
|
+
"set-cookie": buildSessionCookie(session.id),
|
|
75
|
+
"cache-control": "no-store",
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* POST /auth/logout
|
|
82
|
+
* Destroys the current session and clears the session cookie.
|
|
83
|
+
*/
|
|
84
|
+
export function handleLogout(request: Request, auth: AuthConfig): Response {
|
|
85
|
+
const sessionId = extractSessionId(request);
|
|
86
|
+
if (sessionId) auth.sessionStore.destroy(sessionId);
|
|
87
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
88
|
+
status: 200,
|
|
89
|
+
headers: {
|
|
90
|
+
"content-type": "application/json; charset=utf-8",
|
|
91
|
+
"set-cookie": buildClearCookie(),
|
|
92
|
+
"cache-control": "no-store",
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* PUT /auth/password
|
|
99
|
+
* Changes the admin password. Requires an active session.
|
|
100
|
+
* The caller is responsible for verifying the session before calling this handler.
|
|
101
|
+
*/
|
|
102
|
+
export async function handlePasswordChange(request: Request, auth: AuthConfig): Promise<Response> {
|
|
103
|
+
const ctBlock = checkContentType(request);
|
|
104
|
+
if (ctBlock) return ctBlock;
|
|
105
|
+
|
|
106
|
+
let payload: unknown;
|
|
107
|
+
try {
|
|
108
|
+
payload = await request.json();
|
|
109
|
+
} catch {
|
|
110
|
+
return jsonResponse({ message: "Invalid JSON.", error: "Send valid JSON." }, 400);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const validation = ChangePasswordRequestSchema.safeParse(payload);
|
|
114
|
+
if (!validation.success) {
|
|
115
|
+
return jsonResponse(
|
|
116
|
+
{
|
|
117
|
+
message: "Invalid request.",
|
|
118
|
+
error: "Provide oldPassword and newPassword (min 6 chars).",
|
|
119
|
+
},
|
|
120
|
+
400,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
await auth.changePassword(validation.data.oldPassword, validation.data.newPassword);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
const msg = err instanceof Error ? err.message : "Password change failed";
|
|
128
|
+
return jsonResponse({ message: msg, error: "Check old password." }, 400);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Invalidate all existing sessions, then issue a fresh one for the caller
|
|
132
|
+
auth.sessionStore.clear();
|
|
133
|
+
const session = auth.sessionStore.create("admin");
|
|
134
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
135
|
+
status: 200,
|
|
136
|
+
headers: {
|
|
137
|
+
"content-type": "application/json",
|
|
138
|
+
"set-cookie": buildSessionCookie(session.id),
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* GET /auth/status
|
|
145
|
+
* Returns authentication state for the current session cookie.
|
|
146
|
+
*/
|
|
147
|
+
export function handleAuthStatus(request: Request, auth: AuthConfig): Response {
|
|
148
|
+
if (auth.anonymousMode) {
|
|
149
|
+
return jsonResponse({ authenticated: true, username: "anonymous" }, 200);
|
|
150
|
+
}
|
|
151
|
+
const sessionId = extractSessionId(request);
|
|
152
|
+
const session = sessionId ? auth.sessionStore.validate(sessionId) : null;
|
|
153
|
+
if (session) {
|
|
154
|
+
return jsonResponse({ authenticated: true, username: session.username }, 200);
|
|
155
|
+
}
|
|
156
|
+
return jsonResponse({ authenticated: false }, 200);
|
|
157
|
+
}
|
|
@@ -6,29 +6,45 @@ const originalFetch = globalThis.fetch;
|
|
|
6
6
|
|
|
7
7
|
afterEach(() => {
|
|
8
8
|
globalThis.fetch = originalFetch;
|
|
9
|
-
delete Bun.env["
|
|
9
|
+
delete Bun.env["MIRAVO_ADMIN_PASSWORD"];
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
describe("admin-client", () => {
|
|
13
|
-
test("
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
13
|
+
test("authenticates via session login and sends cookie on command requests", async () => {
|
|
14
|
+
let loginCalled = false;
|
|
15
|
+
let commandCookie = "";
|
|
16
|
+
|
|
17
|
+
globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
18
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
19
|
+
|
|
20
|
+
// Login request
|
|
21
|
+
if (url.endsWith("/auth/login")) {
|
|
22
|
+
loginCalled = true;
|
|
23
|
+
return new Response(JSON.stringify({ ok: true, changePassword: false }), {
|
|
24
|
+
status: 200,
|
|
25
|
+
headers: { "set-cookie": "miravo_session=test-session-id; HttpOnly; Path=/" },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Command request — verify session cookie is sent
|
|
30
|
+
if (url.endsWith("/commands")) {
|
|
31
|
+
commandCookie = (init?.headers as Record<string, string>)?.["cookie"] ?? "";
|
|
32
|
+
return Response.json({
|
|
33
|
+
success: true,
|
|
34
|
+
command: "pause",
|
|
35
|
+
correlationId: "req-1",
|
|
36
|
+
message: "paused",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return Response.json({ message: "Not found" }, { status: 404 });
|
|
27
41
|
}) as typeof fetch;
|
|
28
42
|
|
|
29
43
|
const command: EngineCommand = { type: "pause", correlationId: "req-1" };
|
|
30
44
|
const result = await executeAdminCommand("http://127.0.0.1:8080", command, 1_000);
|
|
31
45
|
|
|
46
|
+
expect(loginCalled).toBe(true);
|
|
47
|
+
expect(commandCookie).toBe("miravo_session=test-session-id");
|
|
32
48
|
expect(result.success).toBe(true);
|
|
33
49
|
expect(result.command).toBe("pause");
|
|
34
50
|
});
|