miravo 0.5.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 -9
- 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 +20 -21
- 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 +96 -36
- 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 +18 -10
- package/src/output.ts +3 -0
- package/console-dist/assets/index-C1oOHU1J.js +0 -119
- package/console-dist/assets/index-C3XMYq26.css +0 -2
- package/src/commands/start.ts +0 -2253
|
@@ -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({
|
|
@@ -226,7 +225,7 @@ describe("resolveProtocolBootPlans", () => {
|
|
|
226
225
|
});
|
|
227
226
|
|
|
228
227
|
test("applies boot defaults when no persisted snapshot was restored", async () => {
|
|
229
|
-
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
228
|
+
const { resolveProtocolBootPlans } = await import("./start/index.ts");
|
|
230
229
|
|
|
231
230
|
expect(
|
|
232
231
|
resolveProtocolBootPlans({
|
|
@@ -242,7 +241,7 @@ describe("resolveProtocolBootPlans", () => {
|
|
|
242
241
|
});
|
|
243
242
|
|
|
244
243
|
test("treats explicit MQTT disable as authoritative over persisted state", async () => {
|
|
245
|
-
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
244
|
+
const { resolveProtocolBootPlans } = await import("./start/index.ts");
|
|
246
245
|
|
|
247
246
|
expect(
|
|
248
247
|
resolveProtocolBootPlans({
|
|
@@ -254,7 +253,7 @@ describe("resolveProtocolBootPlans", () => {
|
|
|
254
253
|
});
|
|
255
254
|
|
|
256
255
|
test("treats explicit CLI MQTT config overrides as authoritative over persisted config", async () => {
|
|
257
|
-
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
256
|
+
const { resolveProtocolBootPlans } = await import("./start/index.ts");
|
|
258
257
|
|
|
259
258
|
expect(
|
|
260
259
|
resolveProtocolBootPlans({
|
|
@@ -266,7 +265,7 @@ describe("resolveProtocolBootPlans", () => {
|
|
|
266
265
|
});
|
|
267
266
|
|
|
268
267
|
test("does not treat env fallback config as authoritative over persisted MQTT state", async () => {
|
|
269
|
-
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
268
|
+
const { resolveProtocolBootPlans } = await import("./start/index.ts");
|
|
270
269
|
|
|
271
270
|
expect(
|
|
272
271
|
resolveProtocolBootPlans({
|
|
@@ -278,7 +277,7 @@ describe("resolveProtocolBootPlans", () => {
|
|
|
278
277
|
});
|
|
279
278
|
|
|
280
279
|
test("treats explicit OPC UA enable as authoritative over persisted state", async () => {
|
|
281
|
-
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
280
|
+
const { resolveProtocolBootPlans } = await import("./start/index.ts");
|
|
282
281
|
|
|
283
282
|
expect(
|
|
284
283
|
resolveProtocolBootPlans({
|
|
@@ -290,7 +289,7 @@ describe("resolveProtocolBootPlans", () => {
|
|
|
290
289
|
});
|
|
291
290
|
|
|
292
291
|
test("does not treat ignored OPC UA config flags as persisted-state overrides", async () => {
|
|
293
|
-
const { resolveProtocolBootPlans } = await import("./start.ts");
|
|
292
|
+
const { resolveProtocolBootPlans } = await import("./start/index.ts");
|
|
294
293
|
|
|
295
294
|
expect(
|
|
296
295
|
resolveProtocolBootPlans({
|
|
@@ -304,7 +303,7 @@ describe("resolveProtocolBootPlans", () => {
|
|
|
304
303
|
|
|
305
304
|
describe("resolveDetachAdapterConfigs", () => {
|
|
306
305
|
test("uses template adapter configs when no CLI/env overrides are provided", async () => {
|
|
307
|
-
const { resolveDetachAdapterConfigs } = await import("./start.ts");
|
|
306
|
+
const { resolveDetachAdapterConfigs } = await import("./start/index.ts");
|
|
308
307
|
|
|
309
308
|
const template = createTemplateWithNamespace({ prefix: "factory" });
|
|
310
309
|
const withProtocols: MiravoTemplate = {
|
|
@@ -330,14 +329,14 @@ describe("resolveDetachAdapterConfigs", () => {
|
|
|
330
329
|
modbus: { enabled: true, portOverride: undefined, hostOverride: undefined },
|
|
331
330
|
}),
|
|
332
331
|
).toEqual({
|
|
333
|
-
mqtt: { port: 1889, host: "
|
|
332
|
+
mqtt: { port: 1889, host: "0.0.0.0", embedded: true },
|
|
334
333
|
opcua: { port: 4851, host: "127.0.0.1" },
|
|
335
334
|
modbus: { port: 1602, host: "127.0.0.1" },
|
|
336
335
|
});
|
|
337
336
|
});
|
|
338
337
|
|
|
339
338
|
test("CLI/env overrides take precedence over template values", async () => {
|
|
340
|
-
const { resolveDetachAdapterConfigs } = await import("./start.ts");
|
|
339
|
+
const { resolveDetachAdapterConfigs } = await import("./start/index.ts");
|
|
341
340
|
|
|
342
341
|
const template = createTemplateWithNamespace({ prefix: "factory" });
|
|
343
342
|
const withProtocols: MiravoTemplate = {
|
|
@@ -374,7 +373,7 @@ describe("resolveDetachAdapterConfigs", () => {
|
|
|
374
373
|
});
|
|
375
374
|
|
|
376
375
|
test("returns disabled placeholders when adapters are disabled", async () => {
|
|
377
|
-
const { resolveDetachAdapterConfigs } = await import("./start.ts");
|
|
376
|
+
const { resolveDetachAdapterConfigs } = await import("./start/index.ts");
|
|
378
377
|
const template = createTemplateWithNamespace({ prefix: "factory" });
|
|
379
378
|
|
|
380
379
|
expect(
|
|
@@ -398,7 +397,7 @@ describe("resolveDetachAdapterConfigs", () => {
|
|
|
398
397
|
});
|
|
399
398
|
|
|
400
399
|
test("marks external MQTT mode as non-embedded for detach conflict checks", async () => {
|
|
401
|
-
const { resolveDetachAdapterConfigs } = await import("./start.ts");
|
|
400
|
+
const { resolveDetachAdapterConfigs } = await import("./start/index.ts");
|
|
402
401
|
const template = createTemplateWithNamespace({ prefix: "factory" });
|
|
403
402
|
|
|
404
403
|
expect(
|
|
@@ -424,7 +423,7 @@ describe("resolveDetachAdapterConfigs", () => {
|
|
|
424
423
|
|
|
425
424
|
describe("error context aggregation", () => {
|
|
426
425
|
test("normalizeErrorContext redacts high-cardinality fragments", async () => {
|
|
427
|
-
const { normalizeErrorContext } = await import("./start.ts");
|
|
426
|
+
const { normalizeErrorContext } = await import("./start/index.ts");
|
|
428
427
|
|
|
429
428
|
expect(normalizeErrorContext('generator="rpm" instance="pump-001" tick=42')).toBe(
|
|
430
429
|
'generator="rpm" instance="*" tick=*',
|
|
@@ -435,7 +434,7 @@ describe("error context aggregation", () => {
|
|
|
435
434
|
});
|
|
436
435
|
|
|
437
436
|
test("incrementErrorContextCount caps unique keys and accumulates in overflow bucket", async () => {
|
|
438
|
-
const { incrementErrorContextCount } = await import("./start.ts");
|
|
437
|
+
const { incrementErrorContextCount } = await import("./start/index.ts");
|
|
439
438
|
const counts = new Map<string, number>();
|
|
440
439
|
|
|
441
440
|
for (let i = 0; i < 256; i++) {
|
|
@@ -453,7 +452,7 @@ describe("error context aggregation", () => {
|
|
|
453
452
|
|
|
454
453
|
describe("template picker choices", () => {
|
|
455
454
|
test("buildBuiltinTemplateChoices renders tree labels directly in select options", async () => {
|
|
456
|
-
const { buildBuiltinTemplateChoices } = await import("./start.ts");
|
|
455
|
+
const { buildBuiltinTemplateChoices } = await import("./start/index.ts");
|
|
457
456
|
const choices = buildBuiltinTemplateChoices([
|
|
458
457
|
discoveredTemplate("stress-50k", "Stress Benchmark 50K"),
|
|
459
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
|
});
|
package/src/lib/admin-client.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
EngineStateSnapshotSchema,
|
|
11
11
|
MiravoError,
|
|
12
12
|
} from "@miravo/shared";
|
|
13
|
+
import { loadCredential } from "./auth-store.ts";
|
|
13
14
|
|
|
14
15
|
const DEFAULT_ADMIN_HOST = "127.0.0.1";
|
|
15
16
|
const DEFAULT_ADMIN_PORT = 8080;
|
|
@@ -87,19 +88,67 @@ function parseAdminUrl(input: string): string {
|
|
|
87
88
|
return url.toString().replace(/\/$/, "");
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Session-based auth for CLI client
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
let sessionCookie: string | null = null;
|
|
96
|
+
|
|
97
|
+
async function loginToAdmin(adminUrl: string): Promise<void> {
|
|
98
|
+
// Resolve password: env > file > default
|
|
99
|
+
const envPassword = Bun.env["MIRAVO_ADMIN_PASSWORD"];
|
|
100
|
+
let password = "admin";
|
|
101
|
+
if (envPassword && envPassword.trim().length > 0) {
|
|
102
|
+
password = envPassword;
|
|
103
|
+
} else {
|
|
104
|
+
const credential = await loadCredential();
|
|
105
|
+
// If credential file exists, we can't reverse the hash — use "admin" as default.
|
|
106
|
+
// The server will verify against the stored hash. If the user changed their password,
|
|
107
|
+
// they need to set MIRAVO_ADMIN_PASSWORD env var for CLI access.
|
|
108
|
+
if (!credential) {
|
|
109
|
+
password = "admin";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const response = await fetch(`${adminUrl}/auth/login`, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: { "content-type": "application/json" },
|
|
116
|
+
body: JSON.stringify({ username: "admin", password }),
|
|
117
|
+
signal: AbortSignal.timeout(10_000),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
throw new MiravoError(
|
|
122
|
+
"CLI authentication failed.",
|
|
123
|
+
"Set MIRAVO_ADMIN_PASSWORD or log in with default credentials (admin/admin).",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const setCookie = response.headers.get("set-cookie");
|
|
128
|
+
if (setCookie) {
|
|
129
|
+
// Extract just the cookie value (before first ;)
|
|
130
|
+
sessionCookie = setCookie.split(";")[0] ?? null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function ensureAuthenticated(adminUrl: string): Promise<void> {
|
|
135
|
+
if (sessionCookie) return;
|
|
136
|
+
await loginToAdmin(adminUrl);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildRequestHeaders(body?: string): Record<string, string> {
|
|
91
140
|
const headers: Record<string, string> = {};
|
|
92
141
|
if (body) {
|
|
93
142
|
headers["content-type"] = "application/json";
|
|
94
143
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
headers["authorization"] = `Bearer ${adminToken}`;
|
|
144
|
+
if (sessionCookie) {
|
|
145
|
+
headers["cookie"] = sessionCookie;
|
|
98
146
|
}
|
|
99
|
-
return
|
|
147
|
+
return headers;
|
|
100
148
|
}
|
|
101
149
|
|
|
102
150
|
async function requestJson<T>(options: RequestJsonOptions<T>): Promise<T> {
|
|
151
|
+
await ensureAuthenticated(options.baseUrl);
|
|
103
152
|
const requestUrl = new URL(options.path, `${options.baseUrl}/`).toString();
|
|
104
153
|
const controller = new AbortController();
|
|
105
154
|
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
@@ -128,6 +177,18 @@ async function requestJson<T>(options: RequestJsonOptions<T>): Promise<T> {
|
|
|
128
177
|
);
|
|
129
178
|
}
|
|
130
179
|
|
|
180
|
+
// Re-authenticate on session expiry and retry once
|
|
181
|
+
if (response.status === 401) {
|
|
182
|
+
sessionCookie = null;
|
|
183
|
+
await loginToAdmin(options.baseUrl);
|
|
184
|
+
response = await fetch(requestUrl, {
|
|
185
|
+
method: options.method ?? "GET",
|
|
186
|
+
headers: buildRequestHeaders(options.body),
|
|
187
|
+
body: options.body,
|
|
188
|
+
signal: controller.signal,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
131
192
|
const text = await response.text();
|
|
132
193
|
let parsedJson: unknown;
|
|
133
194
|
if (text.length > 0) {
|
|
@@ -223,4 +284,43 @@ async function executeAdminCommand(
|
|
|
223
284
|
});
|
|
224
285
|
}
|
|
225
286
|
|
|
226
|
-
|
|
287
|
+
/** Schema that accepts any JSON object — used for adapter introspection (protocol-specific shape). */
|
|
288
|
+
const AnyObjectSchema = {
|
|
289
|
+
safeParse(value: unknown): SafeParseResult<Record<string, unknown>> {
|
|
290
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
291
|
+
return { success: true, data: value as Record<string, unknown> };
|
|
292
|
+
}
|
|
293
|
+
return { success: false };
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
async function getManifest(adminUrl: string, timeoutMs: number): Promise<Record<string, unknown>> {
|
|
298
|
+
return requestJson({
|
|
299
|
+
baseUrl: adminUrl,
|
|
300
|
+
path: "/manifest",
|
|
301
|
+
timeoutMs,
|
|
302
|
+
schema: AnyObjectSchema,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function getLeDtJson(
|
|
307
|
+
adminUrl: string,
|
|
308
|
+
modelName: string,
|
|
309
|
+
timeoutMs: number,
|
|
310
|
+
): Promise<Record<string, unknown>> {
|
|
311
|
+
return requestJson({
|
|
312
|
+
baseUrl: adminUrl,
|
|
313
|
+
path: `/catalog/models/${encodeURIComponent(modelName)}/le-dt.json`,
|
|
314
|
+
timeoutMs,
|
|
315
|
+
schema: AnyObjectSchema,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export {
|
|
320
|
+
executeAdminCommand,
|
|
321
|
+
getAdminSnapshot,
|
|
322
|
+
getAdminState,
|
|
323
|
+
getLeDtJson,
|
|
324
|
+
getManifest,
|
|
325
|
+
resolveAdminUrl,
|
|
326
|
+
};
|