miravo 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +199 -0
- package/README.md +29 -59
- package/bin/miravo.js +84 -0
- package/package.json +21 -37
- package/console-dist/assets/activity-DsWeRq4h.js +0 -1
- package/console-dist/assets/api-4AqKvsm9.js +0 -1
- package/console-dist/assets/arrow-right-Ea_UhTNi.js +0 -1
- package/console-dist/assets/arrow-up-down-DFSjzzl2.js +0 -1
- package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +0 -1
- package/console-dist/assets/box-C1WaoOMF.js +0 -1
- package/console-dist/assets/card-BjeP32PN.js +0 -1
- package/console-dist/assets/catalog-BJzyzbLR.js +0 -1
- package/console-dist/assets/catalog-DKjEm-c4.js +0 -1
- package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +0 -1
- package/console-dist/assets/checkbox-DZlJXTE4.js +0 -1
- package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +0 -3
- package/console-dist/assets/combobox-BGnuCYxO.js +0 -1
- package/console-dist/assets/control-CPhvwx9o.js +0 -1
- package/console-dist/assets/control-Cg2up9NA.js +0 -1
- package/console-dist/assets/copy-button-WkbNkMcd.js +0 -1
- package/console-dist/assets/createLucideIcon-Bwxw859K.js +0 -1
- package/console-dist/assets/deploy-tab-JeWDwPoZ.js +0 -1
- package/console-dist/assets/dist-BhbDML0a.js +0 -1
- package/console-dist/assets/dist-Cy8XTNFT.js +0 -2
- package/console-dist/assets/download-DQ6NH16U.js +0 -1
- package/console-dist/assets/engine-store-CrLnz692.js +0 -1
- package/console-dist/assets/events-CXm29Ok4.js +0 -1
- package/console-dist/assets/events-D01IxhnP.js +0 -1
- package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
- package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
- package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
- package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
- package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
- package/console-dist/assets/fleet-CfxbKXt9.js +0 -1
- package/console-dist/assets/fleet-lotMGVqe.js +0 -1
- package/console-dist/assets/format-kXTonpDJ.js +0 -1
- package/console-dist/assets/gauge-Ci8zNZlk.js +0 -1
- package/console-dist/assets/get-error-message-3Q5Q-2G5.js +0 -1
- package/console-dist/assets/index-CVPKSDvJ.js +0 -12
- package/console-dist/assets/index-DJkgDeun.css +0 -2
- package/console-dist/assets/input-BWEQFptx.js +0 -1
- package/console-dist/assets/instance-detail-C5IVwuh3.js +0 -1
- package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
- package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
- package/console-dist/assets/label-uephX5ZG.js +0 -1
- package/console-dist/assets/layers-CUsLQ1fq.js +0 -1
- package/console-dist/assets/login-C3RJli0m.js +0 -126
- package/console-dist/assets/models-B_UioWQ5.js +0 -1
- package/console-dist/assets/models-_IPlPiZ7.js +0 -1
- package/console-dist/assets/not-found-B8gAm08S.js +0 -1
- package/console-dist/assets/platform-targets-R3T-4eJ7.js +0 -1
- package/console-dist/assets/platforms-BWh0BQ3Y.js +0 -1
- package/console-dist/assets/platforms-DStNbtVc.js +0 -1
- package/console-dist/assets/proxy-CsnYqnPp.js +0 -9
- package/console-dist/assets/query-keys-BvtE7BMs.js +0 -1
- package/console-dist/assets/radio-CoHegOv5.js +0 -1
- package/console-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
- package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
- package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
- package/console-dist/assets/select-CeO4vP0G.js +0 -1
- package/console-dist/assets/select-display-items-CIzY8953.js +0 -1
- package/console-dist/assets/server-D9Kfn8fQ.js +0 -1
- package/console-dist/assets/shallow-6lGjQhtJ.js +0 -1
- package/console-dist/assets/tags-CiXOIHtY.js +0 -4
- package/console-dist/assets/tags-FQCXIxrP.js +0 -1
- package/console-dist/assets/tooltip-HGLF366Q.js +0 -1
- package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +0 -1
- package/console-dist/assets/transition-types-CPlGtM8w.js +0 -1
- package/console-dist/assets/triangle-alert-adNk_sea.js +0 -1
- package/console-dist/assets/use-platform-stream-DGbK1k2L.js +0 -2
- package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +0 -2
- package/console-dist/assets/validation-Blvud9_O.js +0 -1
- package/console-dist/assets/vendor-charts-CAZdaPlq.js +0 -60
- package/console-dist/assets/vendor-table-wd8JuMmT.js +0 -4
- package/console-dist/assets/vendor-ui-ByahnaY0.js +0 -1
- package/console-dist/favicon.svg +0 -37
- package/console-dist/index.html +0 -82
- package/console-dist/miravo-logo.png +0 -0
- package/src/__tests__/integration/executable-methods-runner.ts +0 -262
- package/src/__tests__/integration/executable-methods.integration.ts +0 -2348
- package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +0 -314
- package/src/commands/adapter.ts +0 -249
- package/src/commands/apply.test.ts +0 -62
- package/src/commands/apply.ts +0 -216
- package/src/commands/cleanup.test.ts +0 -34
- package/src/commands/cleanup.ts +0 -201
- package/src/commands/cli-test-setup.ts +0 -91
- package/src/commands/export.test.ts +0 -51
- package/src/commands/export.ts +0 -404
- package/src/commands/fork.ts +0 -219
- package/src/commands/inject.test.ts +0 -57
- package/src/commands/inject.ts +0 -212
- package/src/commands/load.ts +0 -167
- package/src/commands/logs.ts +0 -173
- package/src/commands/models.test.ts +0 -162
- package/src/commands/models.ts +0 -338
- package/src/commands/pause.ts +0 -67
- package/src/commands/platforms.test.ts +0 -186
- package/src/commands/platforms.ts +0 -371
- package/src/commands/ps.ts +0 -116
- package/src/commands/remove.ts +0 -38
- package/src/commands/reset.ts +0 -44
- package/src/commands/rm.ts +0 -97
- package/src/commands/spawn.ts +0 -45
- package/src/commands/speed.ts +0 -40
- package/src/commands/start/adapter-config.ts +0 -157
- package/src/commands/start/catalog-le-dt.test.ts +0 -50
- package/src/commands/start/catalog-le-dt.ts +0 -187
- package/src/commands/start/catalog-model-schema.ts +0 -89
- package/src/commands/start/catalog-template-detail.ts +0 -54
- package/src/commands/start/daemon.ts +0 -255
- package/src/commands/start/display.ts +0 -150
- package/src/commands/start/engine-setup.ts +0 -247
- package/src/commands/start/events.ts +0 -126
- package/src/commands/start/index.ts +0 -897
- package/src/commands/start/protocol-plans.ts +0 -239
- package/src/commands/start/template-loader.ts +0 -197
- package/src/commands/start/template-resolver.ts +0 -185
- package/src/commands/start/types.ts +0 -73
- package/src/commands/start.test.ts +0 -480
- package/src/commands/status.test.ts +0 -20
- package/src/commands/status.ts +0 -470
- package/src/commands/stop.test.ts +0 -84
- package/src/commands/stop.ts +0 -169
- package/src/commands/templates.test.ts +0 -58
- package/src/commands/templates.ts +0 -277
- package/src/index.ts +0 -166
- package/src/lib/admin-auth.ts +0 -157
- package/src/lib/admin-client.test.ts +0 -51
- package/src/lib/admin-client.ts +0 -326
- package/src/lib/admin-platform-routes.ts +0 -1017
- package/src/lib/admin-platform-stream.test.ts +0 -434
- package/src/lib/admin-platform-stream.ts +0 -788
- package/src/lib/admin-responses.ts +0 -211
- package/src/lib/admin-server.test.ts +0 -781
- package/src/lib/admin-server.ts +0 -797
- package/src/lib/auth-store.ts +0 -87
- package/src/lib/daemon-registry.test.ts +0 -429
- package/src/lib/daemon-registry.ts +0 -343
- package/src/lib/le/le-apply.test.ts +0 -591
- package/src/lib/le/le-apply.ts +0 -832
- package/src/lib/le/le-auth.ts +0 -89
- package/src/lib/le/le-cleanup.test.ts +0 -168
- package/src/lib/le/le-cleanup.ts +0 -399
- package/src/lib/le/le-cli-helpers.ts +0 -77
- package/src/lib/le/le-client.ts +0 -184
- package/src/lib/le/le-concurrency.test.ts +0 -157
- package/src/lib/le/le-concurrency.ts +0 -78
- package/src/lib/le/le-connection-test.ts +0 -42
- package/src/lib/le/le-devicehub-properties.test.ts +0 -53
- package/src/lib/le/le-devicehub-properties.ts +0 -33
- package/src/lib/le/le-devicehub.ts +0 -213
- package/src/lib/le/le-digital-twins.ts +0 -285
- package/src/lib/le/le-drivers.ts +0 -23
- package/src/lib/le/le-endpoint.ts +0 -40
- package/src/lib/le/le-environments.test.ts +0 -73
- package/src/lib/le/le-environments.ts +0 -156
- package/src/lib/le/le-markers.ts +0 -87
- package/src/lib/le/le-plan.test.ts +0 -26
- package/src/lib/le/le-plan.ts +0 -109
- package/src/lib/le/le-platform-operations.test.ts +0 -198
- package/src/lib/le/le-platform-operations.ts +0 -340
- package/src/lib/le/le-static-model-schema.ts +0 -78
- package/src/lib/le/le-static-planner.test.ts +0 -255
- package/src/lib/le/le-static-planner.ts +0 -442
- package/src/lib/le/le-template-exporter.test.ts +0 -527
- package/src/lib/le/le-template-exporter.ts +0 -459
- package/src/lib/le/le-value-types.ts +0 -23
- package/src/lib/mqtt-client.ts +0 -115
- package/src/lib/parse.test.ts +0 -123
- package/src/lib/parse.ts +0 -87
- package/src/lib/resolve-simulation.test.ts +0 -118
- package/src/lib/resolve-simulation.ts +0 -181
- package/src/lib/session-store.ts +0 -110
- package/src/lib/shutdown.test.ts +0 -562
- package/src/lib/shutdown.ts +0 -621
- package/src/lib/template-category.test.ts +0 -71
- package/src/lib/template-category.ts +0 -72
- package/src/lib/template-locator.ts +0 -55
- package/src/lib/template-utils.ts +0 -45
- package/src/lib/ws-hub.ts +0 -281
- package/src/logger.test.ts +0 -172
- package/src/logger.ts +0 -402
- package/src/output.test.ts +0 -64
- package/src/output.ts +0 -160
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Builds a ModelSchema from raw YAML without the engine runtime.
|
|
3
|
-
* Used by the catalog endpoints to serve model details for models
|
|
4
|
-
* that may not be loaded into the current simulation.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { ModelSchema } from "@miravo/shared";
|
|
8
|
-
import { TwinModelSchema } from "@miravo/shared";
|
|
9
|
-
import { parse as parseYAML } from "yaml";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Parse a .twin.yaml file and return a ModelSchema.
|
|
13
|
-
* Mirrors the logic in engine-introspection.ts getModelSchema()
|
|
14
|
-
* but operates on the raw parsed YAML instead of runtime objects.
|
|
15
|
-
*/
|
|
16
|
-
export async function buildModelSchemaFromYaml(modelPath: string): Promise<ModelSchema> {
|
|
17
|
-
const text = await Bun.file(modelPath).text();
|
|
18
|
-
const raw = TwinModelSchema.parse(parseYAML(text));
|
|
19
|
-
|
|
20
|
-
const parameters: Record<
|
|
21
|
-
string,
|
|
22
|
-
{ type: string; default: number | string | boolean; unit?: string }
|
|
23
|
-
> = {};
|
|
24
|
-
if (raw.parameters) {
|
|
25
|
-
for (const [name, def] of Object.entries(raw.parameters)) {
|
|
26
|
-
parameters[name] = {
|
|
27
|
-
type: def.type,
|
|
28
|
-
default: def.default,
|
|
29
|
-
...(def.unit ? { unit: def.unit } : {}),
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const members: Record<string, ModelSchema["members"][string]> = {};
|
|
35
|
-
for (const [name, def] of Object.entries(raw.members)) {
|
|
36
|
-
const rawDefault = def.default;
|
|
37
|
-
members[name] = {
|
|
38
|
-
kind: def.kind,
|
|
39
|
-
dataType: def.data_type,
|
|
40
|
-
access: def.access,
|
|
41
|
-
unit: def.unit ?? null,
|
|
42
|
-
euRange: "eu_range" in def ? (def.eu_range ?? null) : null,
|
|
43
|
-
default:
|
|
44
|
-
typeof rawDefault === "number" ||
|
|
45
|
-
typeof rawDefault === "string" ||
|
|
46
|
-
typeof rawDefault === "boolean"
|
|
47
|
-
? rawDefault
|
|
48
|
-
: null,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const methods: Record<string, ModelSchema["methods"][string]> = {};
|
|
53
|
-
if (raw.methods) {
|
|
54
|
-
for (const [name, def] of Object.entries(raw.methods)) {
|
|
55
|
-
methods[name] = {
|
|
56
|
-
description: def.description ?? null,
|
|
57
|
-
inputArguments: def.input_arguments
|
|
58
|
-
? Object.entries(def.input_arguments).map(([argName, arg]) => ({
|
|
59
|
-
name: argName,
|
|
60
|
-
dataType: arg.data_type,
|
|
61
|
-
description: arg.description ?? null,
|
|
62
|
-
}))
|
|
63
|
-
: [],
|
|
64
|
-
outputArguments: def.output_arguments
|
|
65
|
-
? Object.entries(def.output_arguments).map(([argName, arg]) => ({
|
|
66
|
-
name: argName,
|
|
67
|
-
dataType: arg.data_type,
|
|
68
|
-
description: arg.description ?? null,
|
|
69
|
-
}))
|
|
70
|
-
: [],
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
name: raw.model.name,
|
|
77
|
-
description: raw.model.description ?? null,
|
|
78
|
-
parameters,
|
|
79
|
-
members,
|
|
80
|
-
methods,
|
|
81
|
-
faults: raw.faults?.map((f) => f.name) ?? [],
|
|
82
|
-
lifecycle: raw.lifecycle
|
|
83
|
-
? {
|
|
84
|
-
stages: raw.lifecycle.stages.map((s) => s.name),
|
|
85
|
-
totalHours: raw.lifecycle.stages.reduce((sum, s) => sum + s.duration_hours[1], 0),
|
|
86
|
-
}
|
|
87
|
-
: null,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Builds a CatalogTemplateDetail from raw YAML.
|
|
3
|
-
* Used by the catalog endpoint to serve template topology
|
|
4
|
-
* for the inspect side sheet.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { CatalogTemplate, CatalogTemplateDetail } from "@miravo/shared";
|
|
8
|
-
import { MiravoTemplateSchema } from "@miravo/shared";
|
|
9
|
-
import { parse as parseYAML } from "yaml";
|
|
10
|
-
|
|
11
|
-
export async function buildTemplateDetail(
|
|
12
|
-
templatePath: string,
|
|
13
|
-
base: CatalogTemplate,
|
|
14
|
-
): Promise<CatalogTemplateDetail> {
|
|
15
|
-
const text = await Bun.file(templatePath).text();
|
|
16
|
-
const raw = MiravoTemplateSchema.parse(parseYAML(text));
|
|
17
|
-
|
|
18
|
-
const topology = raw.areas.map((area) => ({
|
|
19
|
-
name: area.name,
|
|
20
|
-
...(area.protocol_scope ? { protocol_scope: area.protocol_scope } : {}),
|
|
21
|
-
lines: area.lines.map((line) => ({
|
|
22
|
-
name: line.name,
|
|
23
|
-
...(line.protocol_scope ? { protocol_scope: line.protocol_scope } : {}),
|
|
24
|
-
twins: line.twins.map((twin) => ({
|
|
25
|
-
model: twin.model,
|
|
26
|
-
count: twin.count,
|
|
27
|
-
variation: twin.variation,
|
|
28
|
-
...(twin.parameters ? { parameters: twin.parameters } : {}),
|
|
29
|
-
...(twin.protocol_scope ? { protocol_scope: twin.protocol_scope } : {}),
|
|
30
|
-
})),
|
|
31
|
-
})),
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
|
-
// Extract namespace (ISA-95 or flat)
|
|
35
|
-
const namespace = raw.namespace
|
|
36
|
-
? "enterprise" in raw.namespace
|
|
37
|
-
? { enterprise: raw.namespace.enterprise, site: raw.namespace.site }
|
|
38
|
-
: { prefix: raw.namespace.prefix }
|
|
39
|
-
: undefined;
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
...base,
|
|
43
|
-
topology,
|
|
44
|
-
...(namespace ? { namespace } : {}),
|
|
45
|
-
...(raw.settings
|
|
46
|
-
? {
|
|
47
|
-
settings: {
|
|
48
|
-
tickIntervalMs: raw.settings.tick_interval_ms,
|
|
49
|
-
timeMultiplier: raw.settings.time_multiplier,
|
|
50
|
-
},
|
|
51
|
-
}
|
|
52
|
-
: {}),
|
|
53
|
-
};
|
|
54
|
-
}
|
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
DEFAULT_MODBUS_PORT,
|
|
4
|
-
DEFAULT_MQTT_PORT,
|
|
5
|
-
DEFAULT_OPCUA_PORT,
|
|
6
|
-
MiravoError,
|
|
7
|
-
} from "@miravo/shared";
|
|
8
|
-
import { colors } from "consola/utils";
|
|
9
|
-
import type { DaemonRegistryEntry } from "../../lib/daemon-registry.ts";
|
|
10
|
-
import {
|
|
11
|
-
acquireNameLock,
|
|
12
|
-
checkNameConflict,
|
|
13
|
-
checkPortConflicts,
|
|
14
|
-
displayHost,
|
|
15
|
-
isPidAlive,
|
|
16
|
-
writeEntry,
|
|
17
|
-
} from "../../lib/daemon-registry.ts";
|
|
18
|
-
import { controlLog, writeOutput } from "../../logger.ts";
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Detach mode — spawn background simulation
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
interface DetachOptions {
|
|
25
|
-
readonly simulationName: string;
|
|
26
|
-
readonly templatePath: string;
|
|
27
|
-
readonly seed: number;
|
|
28
|
-
readonly seedExplicit: boolean;
|
|
29
|
-
readonly adminHost: string;
|
|
30
|
-
readonly adminPort: number;
|
|
31
|
-
readonly mqttPort: number | undefined;
|
|
32
|
-
readonly mqttHost: string | undefined;
|
|
33
|
-
readonly mqttEmbedded: boolean;
|
|
34
|
-
readonly mqttEnabled: boolean;
|
|
35
|
-
readonly opcuaPort: number | undefined;
|
|
36
|
-
readonly opcuaHost: string | undefined;
|
|
37
|
-
readonly opcuaEnabled: boolean;
|
|
38
|
-
readonly modbusPort: number | undefined;
|
|
39
|
-
readonly modbusHost: string | undefined;
|
|
40
|
-
readonly modbusEnabled: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function detachAction(opts: DetachOptions): Promise<void> {
|
|
44
|
-
const { simulationName } = opts;
|
|
45
|
-
|
|
46
|
-
// 1. Acquire exclusive lock to prevent concurrent starts
|
|
47
|
-
const releaseLock = await acquireNameLock(simulationName);
|
|
48
|
-
try {
|
|
49
|
-
await detachActionLocked(opts, releaseLock);
|
|
50
|
-
} catch (error) {
|
|
51
|
-
await releaseLock();
|
|
52
|
-
throw error;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function detachActionLocked(
|
|
57
|
-
opts: DetachOptions,
|
|
58
|
-
releaseLock: () => Promise<void>,
|
|
59
|
-
): Promise<void> {
|
|
60
|
-
const { simulationName, adminHost, adminPort } = opts;
|
|
61
|
-
|
|
62
|
-
// 1. Check name conflict
|
|
63
|
-
const existing = await checkNameConflict(simulationName);
|
|
64
|
-
if (existing) {
|
|
65
|
-
throw new MiravoError(
|
|
66
|
-
`Simulation "${simulationName}" is already running (PID ${existing.pid}).`,
|
|
67
|
-
`Stop it first: miravo stop ${simulationName}`,
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// 2. Check port conflicts
|
|
72
|
-
const ports = new Map<string, number>();
|
|
73
|
-
ports.set("admin", adminPort);
|
|
74
|
-
if (opts.mqttEnabled && opts.mqttEmbedded && opts.mqttPort !== undefined) {
|
|
75
|
-
ports.set("mqtt", opts.mqttPort);
|
|
76
|
-
}
|
|
77
|
-
if (opts.opcuaEnabled && opts.opcuaPort !== undefined) ports.set("opcua", opts.opcuaPort);
|
|
78
|
-
if (opts.modbusEnabled && opts.modbusPort !== undefined) ports.set("modbus", opts.modbusPort);
|
|
79
|
-
|
|
80
|
-
const conflicts = await checkPortConflicts(ports);
|
|
81
|
-
if (conflicts.length > 0) {
|
|
82
|
-
throw new MiravoError(
|
|
83
|
-
conflicts[0] ?? "Port conflict detected.",
|
|
84
|
-
"Use different port options or stop the conflicting simulation.",
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// 3. Generate run ID and rebuild args
|
|
89
|
-
const runId = crypto.randomUUID();
|
|
90
|
-
const childArgs = rebuildArgsForDetach(opts);
|
|
91
|
-
|
|
92
|
-
// 4. Spawn child process
|
|
93
|
-
controlLog.info(`Starting simulation "${simulationName}" in background...`);
|
|
94
|
-
const proc = Bun.spawn({
|
|
95
|
-
cmd: [process.execPath, ...childArgs],
|
|
96
|
-
stdin: "ignore",
|
|
97
|
-
stdout: "ignore",
|
|
98
|
-
stderr: "ignore",
|
|
99
|
-
env: {
|
|
100
|
-
...process.env,
|
|
101
|
-
MIRAVO_DAEMON_RUN_ID: runId,
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
proc.unref();
|
|
105
|
-
|
|
106
|
-
// 5. Poll for readiness
|
|
107
|
-
const adminUrl = `http://${adminHost}:${adminPort}`;
|
|
108
|
-
const ready = await pollReadiness(proc.pid, adminUrl, runId, 10, 1_000);
|
|
109
|
-
|
|
110
|
-
if (!ready) {
|
|
111
|
-
// Kill child if still alive
|
|
112
|
-
if (isPidAlive(proc.pid)) {
|
|
113
|
-
try {
|
|
114
|
-
process.kill(proc.pid, "SIGKILL");
|
|
115
|
-
} catch {
|
|
116
|
-
// Ignore kill errors
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
throw new MiravoError(
|
|
121
|
-
`Simulation "${simulationName}" failed to start within the readiness timeout.`,
|
|
122
|
-
"Check template and port availability. Run without -d to see full output.",
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// 6. Write registry entry
|
|
127
|
-
const adapters: DaemonRegistryEntry["adapters"] = {};
|
|
128
|
-
if (opts.mqttEnabled) {
|
|
129
|
-
const mqttPort = opts.mqttPort ?? DEFAULT_MQTT_PORT;
|
|
130
|
-
adapters["mqtt"] = { port: mqttPort, host: opts.mqttHost ?? "0.0.0.0" };
|
|
131
|
-
}
|
|
132
|
-
if (opts.opcuaEnabled) {
|
|
133
|
-
const opcuaPort = opts.opcuaPort ?? DEFAULT_OPCUA_PORT;
|
|
134
|
-
adapters["opcua"] = { port: opcuaPort, host: opts.opcuaHost ?? "0.0.0.0" };
|
|
135
|
-
}
|
|
136
|
-
if (opts.modbusEnabled) {
|
|
137
|
-
const modbusPort = opts.modbusPort ?? DEFAULT_MODBUS_PORT;
|
|
138
|
-
adapters["modbus"] = { port: modbusPort, host: opts.modbusHost ?? "0.0.0.0" };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const entry: DaemonRegistryEntry = {
|
|
142
|
-
name: simulationName,
|
|
143
|
-
pid: proc.pid,
|
|
144
|
-
adminPort,
|
|
145
|
-
adminHost,
|
|
146
|
-
adapters,
|
|
147
|
-
templatePath: resolve(opts.templatePath),
|
|
148
|
-
startedAt: new Date().toISOString(),
|
|
149
|
-
seed: opts.seed,
|
|
150
|
-
runId,
|
|
151
|
-
};
|
|
152
|
-
try {
|
|
153
|
-
await writeEntry(entry);
|
|
154
|
-
} catch (err) {
|
|
155
|
-
// Registry write failed — kill the orphan child so it doesn't run invisible
|
|
156
|
-
// to miravo ps/stop/rm.
|
|
157
|
-
try {
|
|
158
|
-
process.kill(proc.pid, "SIGKILL");
|
|
159
|
-
} catch {
|
|
160
|
-
// Process may have already exited
|
|
161
|
-
}
|
|
162
|
-
throw err;
|
|
163
|
-
}
|
|
164
|
-
await releaseLock();
|
|
165
|
-
|
|
166
|
-
// 7. Print detach summary
|
|
167
|
-
printDetachSummary(entry);
|
|
168
|
-
process.exit(0);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function rebuildArgsForDetach(opts: DetachOptions): string[] {
|
|
172
|
-
// Start from the original argv, filtering out -d/--detach
|
|
173
|
-
const originalArgs = process.argv.slice(1); // skip bun/node executable
|
|
174
|
-
const filtered: string[] = [];
|
|
175
|
-
let hasName = false;
|
|
176
|
-
let hasSeed = false;
|
|
177
|
-
|
|
178
|
-
for (let i = 0; i < originalArgs.length; i++) {
|
|
179
|
-
const arg = originalArgs[i];
|
|
180
|
-
if (arg === undefined) continue;
|
|
181
|
-
if (arg === "-d" || arg === "--detach") continue;
|
|
182
|
-
if (arg === "-n" || arg === "--name") hasName = true;
|
|
183
|
-
if (arg === "--seed") hasSeed = true;
|
|
184
|
-
filtered.push(arg);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Inject --name if not explicitly passed (ensures child uses same resolved name)
|
|
188
|
-
if (!hasName) {
|
|
189
|
-
filtered.push("--name", opts.simulationName);
|
|
190
|
-
}
|
|
191
|
-
// Inject --seed to freeze the seed (so child reproduces the same simulation)
|
|
192
|
-
if (!hasSeed) {
|
|
193
|
-
filtered.push("--seed", String(opts.seed));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return filtered;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async function pollReadiness(
|
|
200
|
-
pid: number,
|
|
201
|
-
adminUrl: string,
|
|
202
|
-
runId: string,
|
|
203
|
-
maxRetries: number,
|
|
204
|
-
intervalMs: number,
|
|
205
|
-
): Promise<boolean> {
|
|
206
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
207
|
-
await Bun.sleep(intervalMs);
|
|
208
|
-
|
|
209
|
-
if (!isPidAlive(pid)) return false;
|
|
210
|
-
|
|
211
|
-
try {
|
|
212
|
-
// Use /readyz to verify identity via runId. Accept any status (not just "ok")
|
|
213
|
-
// so external-MQTT daemons with slow broker reconnect can still start.
|
|
214
|
-
const response = await fetch(`${adminUrl}/readyz`, {
|
|
215
|
-
signal: AbortSignal.timeout(2_000),
|
|
216
|
-
});
|
|
217
|
-
const body = (await response.json()) as Record<string, unknown>;
|
|
218
|
-
if (body["runId"] === runId) return true;
|
|
219
|
-
} catch {
|
|
220
|
-
// Not ready yet — retry
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
return false;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function printDetachSummary(entry: DaemonRegistryEntry): void {
|
|
227
|
-
const admin = `http://${displayHost(entry.adminHost)}:${entry.adminPort}`;
|
|
228
|
-
const lines = [
|
|
229
|
-
`Simulation "${entry.name}" started in background (PID ${entry.pid})`,
|
|
230
|
-
"",
|
|
231
|
-
` Console ${admin}`,
|
|
232
|
-
];
|
|
233
|
-
|
|
234
|
-
for (const [name, adapter] of Object.entries(entry.adapters) as [
|
|
235
|
-
string,
|
|
236
|
-
{ port: number; host: string },
|
|
237
|
-
][]) {
|
|
238
|
-
const host = displayHost(adapter.host);
|
|
239
|
-
if (name === "mqtt") {
|
|
240
|
-
lines.push(` MQTT mqtt://${host}:${adapter.port}`);
|
|
241
|
-
} else if (name === "opcua") {
|
|
242
|
-
lines.push(` OPC UA opc.tcp://${host}:${adapter.port}`);
|
|
243
|
-
} else if (name === "modbus") {
|
|
244
|
-
lines.push(` Modbus modbus://${host}:${adapter.port}`);
|
|
245
|
-
} else {
|
|
246
|
-
lines.push(` ${name} ${host}:${adapter.port}`);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
lines.push("");
|
|
251
|
-
lines.push(` Use ${colors.cyan(`miravo logs ${entry.name}`)} to view events`);
|
|
252
|
-
lines.push(` Use ${colors.cyan(`miravo stop ${entry.name}`)} to stop`);
|
|
253
|
-
|
|
254
|
-
writeOutput(lines.join("\n"));
|
|
255
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { basename } from "node:path";
|
|
2
|
-
import type { Engine } from "@miravo/core";
|
|
3
|
-
import type { MiravoTemplate, TwinModel } from "@miravo/shared";
|
|
4
|
-
import { MIRAVO_VERSION } from "@miravo/shared";
|
|
5
|
-
import { box, colors } from "consola/utils";
|
|
6
|
-
import { forEachTwin } from "../../lib/template-utils.ts";
|
|
7
|
-
import { configLog, engineLog, isPlain, writeOutput } from "../../logger.ts";
|
|
8
|
-
import { formatKeyValue, formatTable, sep } from "../../output.ts";
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Step 6: Startup banner
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
export function printStartupBanner(
|
|
15
|
-
templatePath: string,
|
|
16
|
-
engine: Engine,
|
|
17
|
-
seed: number,
|
|
18
|
-
seedExplicit: boolean,
|
|
19
|
-
mqttEndpoint: string | null,
|
|
20
|
-
opcuaEndpoint: string | null,
|
|
21
|
-
modbusEndpoint: string | null,
|
|
22
|
-
simulationName: string,
|
|
23
|
-
adminEndpoint: string,
|
|
24
|
-
): void {
|
|
25
|
-
const state = engine.getState();
|
|
26
|
-
// Tier 1: identity — name, instance count, model count
|
|
27
|
-
const identity =
|
|
28
|
-
` ${colors.bold(simulationName)}` +
|
|
29
|
-
`${sep}${colors.bold(String(state.instances.length))} instances` +
|
|
30
|
-
`${sep}${colors.bold(String(state.models.length))} models`;
|
|
31
|
-
|
|
32
|
-
// Tier 2: active endpoints only — disabled adapters are omitted
|
|
33
|
-
const endpointEntries: Array<readonly [string, string]> = [
|
|
34
|
-
[colors.dim("Admin"), colors.cyan(adminEndpoint)],
|
|
35
|
-
];
|
|
36
|
-
if (mqttEndpoint) endpointEntries.push([colors.dim("MQTT"), colors.cyan(mqttEndpoint)]);
|
|
37
|
-
if (opcuaEndpoint) endpointEntries.push([colors.dim("OPC UA"), colors.cyan(opcuaEndpoint)]);
|
|
38
|
-
if (modbusEndpoint) endpointEntries.push([colors.dim("Modbus"), colors.cyan(modbusEndpoint)]);
|
|
39
|
-
|
|
40
|
-
// Tier 3: config footer — compact, all dimmed, one line
|
|
41
|
-
const seedStr = seedExplicit ? `seed ${seed}` : `seed ${seed} (random)`;
|
|
42
|
-
const configFooter = colors.dim(
|
|
43
|
-
` ${basename(templatePath)} · ${state.config.tickIntervalMs}ms · ${state.config.timeMultiplier}x · ${seedStr}`,
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const content = [identity, "", formatKeyValue(endpointEntries), "", configFooter].join("\n");
|
|
47
|
-
const header = `${colors.cyan(colors.bold("Miravo"))} ${colors.dim(`v${MIRAVO_VERSION}`)}`;
|
|
48
|
-
|
|
49
|
-
if (isPlain()) {
|
|
50
|
-
writeOutput(header);
|
|
51
|
-
writeOutput(content);
|
|
52
|
-
writeOutput();
|
|
53
|
-
engineLog.start("Press Ctrl+C to stop.");
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
writeOutput(
|
|
58
|
-
box(content, {
|
|
59
|
-
title: header,
|
|
60
|
-
style: { borderColor: "cyan", borderStyle: "rounded" },
|
|
61
|
-
}),
|
|
62
|
-
);
|
|
63
|
-
engineLog.start("Press Ctrl+C to stop.");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
// Step 3: Dry-run summary
|
|
68
|
-
// ---------------------------------------------------------------------------
|
|
69
|
-
|
|
70
|
-
export function printDryRunSummary(
|
|
71
|
-
template: MiravoTemplate,
|
|
72
|
-
models: Map<string, TwinModel>,
|
|
73
|
-
seed: number,
|
|
74
|
-
seedExplicit: boolean,
|
|
75
|
-
templatePath: string,
|
|
76
|
-
): void {
|
|
77
|
-
configLog.success("Dry-run validation passed.");
|
|
78
|
-
|
|
79
|
-
const seedValue = seedExplicit
|
|
80
|
-
? colors.bold(String(seed))
|
|
81
|
-
: `${colors.bold(String(seed))} ${colors.gray("(random — add seed to config to reproduce)")}`;
|
|
82
|
-
|
|
83
|
-
const lines: string[] = [];
|
|
84
|
-
|
|
85
|
-
lines.push(
|
|
86
|
-
formatKeyValue([
|
|
87
|
-
[colors.dim("Template"), templatePath],
|
|
88
|
-
[colors.dim("Seed"), seedValue],
|
|
89
|
-
]),
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
// Model table
|
|
93
|
-
const modelRows = [...models.values()].map((m) => ({
|
|
94
|
-
name: colors.cyan(m.model.name),
|
|
95
|
-
members: String(Object.keys(m.members).length),
|
|
96
|
-
faults: String(m.faults?.length ?? 0),
|
|
97
|
-
lifecycle: m.lifecycle ? `${m.lifecycle.stages.length} stages` : colors.dim("none"),
|
|
98
|
-
}));
|
|
99
|
-
|
|
100
|
-
if (modelRows.length > 0) {
|
|
101
|
-
lines.push("");
|
|
102
|
-
lines.push(` ${colors.bold("Models:")}`);
|
|
103
|
-
lines.push(
|
|
104
|
-
formatTable(
|
|
105
|
-
{
|
|
106
|
-
name: colors.dim("Name"),
|
|
107
|
-
members: colors.dim("Members"),
|
|
108
|
-
faults: colors.dim("Faults"),
|
|
109
|
-
lifecycle: colors.dim("Lifecycle"),
|
|
110
|
-
},
|
|
111
|
-
modelRows,
|
|
112
|
-
),
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Instance summary
|
|
117
|
-
let totalInstances = 0;
|
|
118
|
-
let totalMembers = 0;
|
|
119
|
-
forEachTwin(template, (_area, _line, twin) => {
|
|
120
|
-
totalInstances += twin.count;
|
|
121
|
-
const model = models.get(twin.model);
|
|
122
|
-
if (model) {
|
|
123
|
-
totalMembers += twin.count * Object.keys(model.members).length;
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
lines.push("");
|
|
128
|
-
lines.push(
|
|
129
|
-
formatKeyValue([
|
|
130
|
-
[colors.dim("Total instances"), colors.bold(String(totalInstances))],
|
|
131
|
-
[colors.dim("Total members"), colors.bold(String(totalMembers))],
|
|
132
|
-
]),
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
if (isPlain()) {
|
|
136
|
-
writeOutput("Summary");
|
|
137
|
-
for (const line of lines) {
|
|
138
|
-
writeOutput(line);
|
|
139
|
-
}
|
|
140
|
-
writeOutput();
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
writeOutput(
|
|
145
|
-
box(lines.join("\n"), {
|
|
146
|
-
title: " Summary ",
|
|
147
|
-
style: { borderColor: "cyan", borderStyle: "rounded" },
|
|
148
|
-
}),
|
|
149
|
-
);
|
|
150
|
-
}
|