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.
Files changed (161) hide show
  1. package/README.md +1 -1
  2. package/console-dist/assets/activity-DsWeRq4h.js +1 -0
  3. package/console-dist/assets/api-4AqKvsm9.js +1 -0
  4. package/console-dist/assets/arrow-right-Ea_UhTNi.js +1 -0
  5. package/console-dist/assets/arrow-up-down-DFSjzzl2.js +1 -0
  6. package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +1 -0
  7. package/console-dist/assets/box-C1WaoOMF.js +1 -0
  8. package/console-dist/assets/card-BjeP32PN.js +1 -0
  9. package/console-dist/assets/catalog-BJzyzbLR.js +1 -0
  10. package/console-dist/assets/catalog-DKjEm-c4.js +1 -0
  11. package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +1 -0
  12. package/console-dist/assets/checkbox-DZlJXTE4.js +1 -0
  13. package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +3 -0
  14. package/console-dist/assets/combobox-BGnuCYxO.js +1 -0
  15. package/console-dist/assets/control-CPhvwx9o.js +1 -0
  16. package/console-dist/assets/control-Cg2up9NA.js +1 -0
  17. package/console-dist/assets/copy-button-WkbNkMcd.js +1 -0
  18. package/console-dist/assets/createLucideIcon-Bwxw859K.js +1 -0
  19. package/console-dist/assets/deploy-tab-JeWDwPoZ.js +1 -0
  20. package/console-dist/assets/dist-BhbDML0a.js +1 -0
  21. package/console-dist/assets/dist-Cy8XTNFT.js +2 -0
  22. package/console-dist/assets/download-DQ6NH16U.js +1 -0
  23. package/console-dist/assets/engine-store-CrLnz692.js +1 -0
  24. package/console-dist/assets/events-CXm29Ok4.js +1 -0
  25. package/console-dist/assets/events-D01IxhnP.js +1 -0
  26. package/console-dist/assets/fleet-CfxbKXt9.js +1 -0
  27. package/console-dist/assets/fleet-lotMGVqe.js +1 -0
  28. package/console-dist/assets/format-kXTonpDJ.js +1 -0
  29. package/console-dist/assets/gauge-Ci8zNZlk.js +1 -0
  30. package/console-dist/assets/get-error-message-3Q5Q-2G5.js +1 -0
  31. package/console-dist/assets/index-CVPKSDvJ.js +12 -0
  32. package/console-dist/assets/index-DJkgDeun.css +2 -0
  33. package/console-dist/assets/input-BWEQFptx.js +1 -0
  34. package/console-dist/assets/instance-detail-C5IVwuh3.js +1 -0
  35. package/console-dist/assets/label-uephX5ZG.js +1 -0
  36. package/console-dist/assets/layers-CUsLQ1fq.js +1 -0
  37. package/console-dist/assets/login-C3RJli0m.js +126 -0
  38. package/console-dist/assets/models-B_UioWQ5.js +1 -0
  39. package/console-dist/assets/models-_IPlPiZ7.js +1 -0
  40. package/console-dist/assets/not-found-B8gAm08S.js +1 -0
  41. package/console-dist/assets/platform-targets-R3T-4eJ7.js +1 -0
  42. package/console-dist/assets/platforms-BWh0BQ3Y.js +1 -0
  43. package/console-dist/assets/platforms-DStNbtVc.js +1 -0
  44. package/console-dist/assets/proxy-CsnYqnPp.js +9 -0
  45. package/console-dist/assets/query-keys-BvtE7BMs.js +1 -0
  46. package/console-dist/assets/radio-CoHegOv5.js +1 -0
  47. package/console-dist/assets/rolldown-runtime-COnpUsM8.js +1 -0
  48. package/console-dist/assets/select-CeO4vP0G.js +1 -0
  49. package/console-dist/assets/select-display-items-CIzY8953.js +1 -0
  50. package/console-dist/assets/server-D9Kfn8fQ.js +1 -0
  51. package/console-dist/assets/shallow-6lGjQhtJ.js +1 -0
  52. package/console-dist/assets/tags-CiXOIHtY.js +4 -0
  53. package/console-dist/assets/tags-FQCXIxrP.js +1 -0
  54. package/console-dist/assets/tooltip-HGLF366Q.js +1 -0
  55. package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +1 -0
  56. package/console-dist/assets/transition-types-CPlGtM8w.js +1 -0
  57. package/console-dist/assets/triangle-alert-adNk_sea.js +1 -0
  58. package/console-dist/assets/use-platform-stream-DGbK1k2L.js +2 -0
  59. package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +2 -0
  60. package/console-dist/assets/validation-Blvud9_O.js +1 -0
  61. package/console-dist/assets/vendor-charts-CAZdaPlq.js +60 -0
  62. package/console-dist/assets/vendor-table-wd8JuMmT.js +4 -0
  63. package/console-dist/assets/vendor-ui-ByahnaY0.js +1 -0
  64. package/console-dist/index.html +70 -2
  65. package/package.json +9 -8
  66. package/src/__tests__/integration/executable-methods-runner.ts +7 -6
  67. package/src/__tests__/integration/executable-methods.integration.ts +214 -122
  68. package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +314 -0
  69. package/src/commands/adapter.ts +180 -7
  70. package/src/commands/apply.test.ts +62 -0
  71. package/src/commands/apply.ts +216 -0
  72. package/src/commands/cleanup.test.ts +34 -0
  73. package/src/commands/cleanup.ts +201 -0
  74. package/src/commands/export.test.ts +51 -0
  75. package/src/commands/export.ts +404 -0
  76. package/src/commands/fork.ts +2 -2
  77. package/src/commands/inject.ts +2 -2
  78. package/src/commands/load.ts +1 -1
  79. package/src/commands/logs.ts +13 -15
  80. package/src/commands/models.test.ts +1 -1
  81. package/src/commands/models.ts +1 -1
  82. package/src/commands/pause.ts +3 -3
  83. package/src/commands/platforms.test.ts +186 -0
  84. package/src/commands/platforms.ts +371 -0
  85. package/src/commands/remove.ts +1 -1
  86. package/src/commands/reset.ts +2 -2
  87. package/src/commands/spawn.ts +2 -2
  88. package/src/commands/speed.ts +2 -2
  89. package/src/commands/start/adapter-config.ts +157 -0
  90. package/src/commands/start/catalog-le-dt.test.ts +50 -0
  91. package/src/commands/start/catalog-le-dt.ts +187 -0
  92. package/src/commands/start/catalog-model-schema.ts +89 -0
  93. package/src/commands/start/catalog-template-detail.ts +54 -0
  94. package/src/commands/start/daemon.ts +255 -0
  95. package/src/commands/start/display.ts +150 -0
  96. package/src/commands/start/engine-setup.ts +247 -0
  97. package/src/commands/start/events.ts +126 -0
  98. package/src/commands/start/index.ts +897 -0
  99. package/src/commands/start/protocol-plans.ts +239 -0
  100. package/src/commands/start/template-loader.ts +197 -0
  101. package/src/commands/start/template-resolver.ts +185 -0
  102. package/src/commands/start/types.ts +73 -0
  103. package/src/commands/start.test.ts +137 -16
  104. package/src/commands/status.ts +1 -1
  105. package/src/commands/stop.ts +19 -8
  106. package/src/commands/templates.test.ts +1 -1
  107. package/src/commands/templates.ts +1 -3
  108. package/src/index.ts +11 -3
  109. package/src/lib/admin-auth.ts +157 -0
  110. package/src/lib/admin-client.test.ts +31 -15
  111. package/src/lib/admin-client.ts +106 -6
  112. package/src/lib/admin-platform-routes.ts +1017 -0
  113. package/src/lib/admin-platform-stream.test.ts +434 -0
  114. package/src/lib/admin-platform-stream.ts +788 -0
  115. package/src/lib/admin-responses.ts +211 -0
  116. package/src/lib/admin-server.test.ts +274 -55
  117. package/src/lib/admin-server.ts +387 -248
  118. package/src/lib/auth-store.ts +87 -0
  119. package/src/lib/daemon-registry.ts +12 -14
  120. package/src/lib/le/le-apply.test.ts +591 -0
  121. package/src/lib/le/le-apply.ts +832 -0
  122. package/src/lib/le/le-auth.ts +89 -0
  123. package/src/lib/le/le-cleanup.test.ts +168 -0
  124. package/src/lib/le/le-cleanup.ts +399 -0
  125. package/src/lib/le/le-cli-helpers.ts +77 -0
  126. package/src/lib/le/le-client.ts +184 -0
  127. package/src/lib/le/le-concurrency.test.ts +157 -0
  128. package/src/lib/le/le-concurrency.ts +78 -0
  129. package/src/lib/le/le-connection-test.ts +42 -0
  130. package/src/lib/le/le-devicehub-properties.test.ts +53 -0
  131. package/src/lib/le/le-devicehub-properties.ts +33 -0
  132. package/src/lib/le/le-devicehub.ts +213 -0
  133. package/src/lib/le/le-digital-twins.ts +285 -0
  134. package/src/lib/le/le-drivers.ts +23 -0
  135. package/src/lib/le/le-endpoint.ts +40 -0
  136. package/src/lib/le/le-environments.test.ts +73 -0
  137. package/src/lib/le/le-environments.ts +156 -0
  138. package/src/lib/le/le-markers.ts +87 -0
  139. package/src/lib/le/le-plan.test.ts +26 -0
  140. package/src/lib/le/le-plan.ts +109 -0
  141. package/src/lib/le/le-platform-operations.test.ts +198 -0
  142. package/src/lib/le/le-platform-operations.ts +340 -0
  143. package/src/lib/le/le-static-model-schema.ts +78 -0
  144. package/src/lib/le/le-static-planner.test.ts +255 -0
  145. package/src/lib/le/le-static-planner.ts +442 -0
  146. package/src/lib/le/le-template-exporter.test.ts +527 -0
  147. package/src/lib/le/le-template-exporter.ts +459 -0
  148. package/src/lib/le/le-value-types.ts +23 -0
  149. package/src/lib/mqtt-client.ts +1 -2
  150. package/src/lib/resolve-simulation.ts +1 -6
  151. package/src/lib/session-store.ts +110 -0
  152. package/src/lib/shutdown.test.ts +184 -2
  153. package/src/lib/shutdown.ts +104 -34
  154. package/src/lib/template-category.ts +3 -3
  155. package/src/lib/template-locator.ts +8 -2
  156. package/src/lib/ws-hub.ts +72 -46
  157. package/src/logger.ts +39 -14
  158. package/src/output.ts +3 -0
  159. package/console-dist/assets/index-C3XMYq26.css +0 -2
  160. package/console-dist/assets/index-hUJSpKtB.js +0 -119
  161. 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"),
@@ -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>", "Miravo admin server URL (env: MIRAVO_ADMIN_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)",
@@ -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 adminUrl = `http://${entry.adminHost}:${entry.adminPort}/shutdown`;
85
- const headers: Record<string, string> = { "content-type": "application/json" };
86
- const adminToken = Bun.env["MIRAVO_ADMIN_TOKEN"];
87
- if (adminToken) {
88
- headers["authorization"] = `Bearer ${adminToken}`;
89
- }
90
- const response = await fetch(adminUrl, {
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("production");
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://github.com/amine-amaach/miravo")
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://github.com/amine-amaach/miravo`);
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["MIRAVO_ADMIN_TOKEN"];
9
+ delete Bun.env["MIRAVO_ADMIN_PASSWORD"];
10
10
  });
11
11
 
12
12
  describe("admin-client", () => {
13
- test("forwards MIRAVO_ADMIN_TOKEN on command requests", async () => {
14
- Bun.env["MIRAVO_ADMIN_TOKEN"] = "secret-token";
15
-
16
- globalThis.fetch = (async (_input, init) => {
17
- expect(init?.headers).toEqual({
18
- "content-type": "application/json",
19
- authorization: "Bearer secret-token",
20
- });
21
- return Response.json({
22
- success: true,
23
- command: "pause",
24
- correlationId: "req-1",
25
- message: "paused",
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
  });