miravo 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/LICENSE +199 -0
  2. package/README.md +29 -59
  3. package/bin/miravo.js +84 -0
  4. package/package.json +21 -37
  5. package/console-dist/assets/activity-DsWeRq4h.js +0 -1
  6. package/console-dist/assets/api-4AqKvsm9.js +0 -1
  7. package/console-dist/assets/arrow-right-Ea_UhTNi.js +0 -1
  8. package/console-dist/assets/arrow-up-down-DFSjzzl2.js +0 -1
  9. package/console-dist/assets/blueprint-card-classes-DBFCDy0M.js +0 -1
  10. package/console-dist/assets/box-C1WaoOMF.js +0 -1
  11. package/console-dist/assets/card-BjeP32PN.js +0 -1
  12. package/console-dist/assets/catalog-BJzyzbLR.js +0 -1
  13. package/console-dist/assets/catalog-DKjEm-c4.js +0 -1
  14. package/console-dist/assets/catalog-template-icons-BrUeHaCY.js +0 -1
  15. package/console-dist/assets/checkbox-DZlJXTE4.js +0 -1
  16. package/console-dist/assets/chunk-OE4NN4TA-CEjpfhS6.js +0 -3
  17. package/console-dist/assets/combobox-BGnuCYxO.js +0 -1
  18. package/console-dist/assets/control-CPhvwx9o.js +0 -1
  19. package/console-dist/assets/control-Cg2up9NA.js +0 -1
  20. package/console-dist/assets/copy-button-WkbNkMcd.js +0 -1
  21. package/console-dist/assets/createLucideIcon-Bwxw859K.js +0 -1
  22. package/console-dist/assets/deploy-tab-JeWDwPoZ.js +0 -1
  23. package/console-dist/assets/dist-BhbDML0a.js +0 -1
  24. package/console-dist/assets/dist-Cy8XTNFT.js +0 -2
  25. package/console-dist/assets/download-DQ6NH16U.js +0 -1
  26. package/console-dist/assets/engine-store-CrLnz692.js +0 -1
  27. package/console-dist/assets/events-CXm29Ok4.js +0 -1
  28. package/console-dist/assets/events-D01IxhnP.js +0 -1
  29. package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
  30. package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
  31. package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
  32. package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
  33. package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
  34. package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
  35. package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
  36. package/console-dist/assets/fleet-CfxbKXt9.js +0 -1
  37. package/console-dist/assets/fleet-lotMGVqe.js +0 -1
  38. package/console-dist/assets/format-kXTonpDJ.js +0 -1
  39. package/console-dist/assets/gauge-Ci8zNZlk.js +0 -1
  40. package/console-dist/assets/get-error-message-3Q5Q-2G5.js +0 -1
  41. package/console-dist/assets/index-CVPKSDvJ.js +0 -12
  42. package/console-dist/assets/index-DJkgDeun.css +0 -2
  43. package/console-dist/assets/input-BWEQFptx.js +0 -1
  44. package/console-dist/assets/instance-detail-C5IVwuh3.js +0 -1
  45. package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
  46. package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
  47. package/console-dist/assets/label-uephX5ZG.js +0 -1
  48. package/console-dist/assets/layers-CUsLQ1fq.js +0 -1
  49. package/console-dist/assets/login-C3RJli0m.js +0 -126
  50. package/console-dist/assets/models-B_UioWQ5.js +0 -1
  51. package/console-dist/assets/models-_IPlPiZ7.js +0 -1
  52. package/console-dist/assets/not-found-B8gAm08S.js +0 -1
  53. package/console-dist/assets/platform-targets-R3T-4eJ7.js +0 -1
  54. package/console-dist/assets/platforms-BWh0BQ3Y.js +0 -1
  55. package/console-dist/assets/platforms-DStNbtVc.js +0 -1
  56. package/console-dist/assets/proxy-CsnYqnPp.js +0 -9
  57. package/console-dist/assets/query-keys-BvtE7BMs.js +0 -1
  58. package/console-dist/assets/radio-CoHegOv5.js +0 -1
  59. package/console-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
  60. package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
  61. package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
  62. package/console-dist/assets/select-CeO4vP0G.js +0 -1
  63. package/console-dist/assets/select-display-items-CIzY8953.js +0 -1
  64. package/console-dist/assets/server-D9Kfn8fQ.js +0 -1
  65. package/console-dist/assets/shallow-6lGjQhtJ.js +0 -1
  66. package/console-dist/assets/tags-CiXOIHtY.js +0 -4
  67. package/console-dist/assets/tags-FQCXIxrP.js +0 -1
  68. package/console-dist/assets/tooltip-HGLF366Q.js +0 -1
  69. package/console-dist/assets/transition-nav-link-Df4Rk6vV.js +0 -1
  70. package/console-dist/assets/transition-types-CPlGtM8w.js +0 -1
  71. package/console-dist/assets/triangle-alert-adNk_sea.js +0 -1
  72. package/console-dist/assets/use-platform-stream-DGbK1k2L.js +0 -2
  73. package/console-dist/assets/use-transition-navigate-CCpcWXB0.js +0 -2
  74. package/console-dist/assets/validation-Blvud9_O.js +0 -1
  75. package/console-dist/assets/vendor-charts-CAZdaPlq.js +0 -60
  76. package/console-dist/assets/vendor-table-wd8JuMmT.js +0 -4
  77. package/console-dist/assets/vendor-ui-ByahnaY0.js +0 -1
  78. package/console-dist/favicon.svg +0 -37
  79. package/console-dist/index.html +0 -82
  80. package/console-dist/miravo-logo.png +0 -0
  81. package/src/__tests__/integration/executable-methods-runner.ts +0 -262
  82. package/src/__tests__/integration/executable-methods.integration.ts +0 -2348
  83. package/src/__tests__/integration/opcua-concurrent-clients.integration.ts +0 -314
  84. package/src/commands/adapter.ts +0 -249
  85. package/src/commands/apply.test.ts +0 -62
  86. package/src/commands/apply.ts +0 -216
  87. package/src/commands/cleanup.test.ts +0 -34
  88. package/src/commands/cleanup.ts +0 -201
  89. package/src/commands/cli-test-setup.ts +0 -91
  90. package/src/commands/export.test.ts +0 -51
  91. package/src/commands/export.ts +0 -404
  92. package/src/commands/fork.ts +0 -219
  93. package/src/commands/inject.test.ts +0 -57
  94. package/src/commands/inject.ts +0 -212
  95. package/src/commands/load.ts +0 -167
  96. package/src/commands/logs.ts +0 -173
  97. package/src/commands/models.test.ts +0 -162
  98. package/src/commands/models.ts +0 -338
  99. package/src/commands/pause.ts +0 -67
  100. package/src/commands/platforms.test.ts +0 -186
  101. package/src/commands/platforms.ts +0 -371
  102. package/src/commands/ps.ts +0 -116
  103. package/src/commands/remove.ts +0 -38
  104. package/src/commands/reset.ts +0 -44
  105. package/src/commands/rm.ts +0 -97
  106. package/src/commands/spawn.ts +0 -45
  107. package/src/commands/speed.ts +0 -40
  108. package/src/commands/start/adapter-config.ts +0 -157
  109. package/src/commands/start/catalog-le-dt.test.ts +0 -50
  110. package/src/commands/start/catalog-le-dt.ts +0 -187
  111. package/src/commands/start/catalog-model-schema.ts +0 -89
  112. package/src/commands/start/catalog-template-detail.ts +0 -54
  113. package/src/commands/start/daemon.ts +0 -255
  114. package/src/commands/start/display.ts +0 -150
  115. package/src/commands/start/engine-setup.ts +0 -247
  116. package/src/commands/start/events.ts +0 -126
  117. package/src/commands/start/index.ts +0 -897
  118. package/src/commands/start/protocol-plans.ts +0 -239
  119. package/src/commands/start/template-loader.ts +0 -197
  120. package/src/commands/start/template-resolver.ts +0 -185
  121. package/src/commands/start/types.ts +0 -73
  122. package/src/commands/start.test.ts +0 -480
  123. package/src/commands/status.test.ts +0 -20
  124. package/src/commands/status.ts +0 -470
  125. package/src/commands/stop.test.ts +0 -84
  126. package/src/commands/stop.ts +0 -169
  127. package/src/commands/templates.test.ts +0 -58
  128. package/src/commands/templates.ts +0 -277
  129. package/src/index.ts +0 -166
  130. package/src/lib/admin-auth.ts +0 -157
  131. package/src/lib/admin-client.test.ts +0 -51
  132. package/src/lib/admin-client.ts +0 -326
  133. package/src/lib/admin-platform-routes.ts +0 -1017
  134. package/src/lib/admin-platform-stream.test.ts +0 -434
  135. package/src/lib/admin-platform-stream.ts +0 -788
  136. package/src/lib/admin-responses.ts +0 -211
  137. package/src/lib/admin-server.test.ts +0 -781
  138. package/src/lib/admin-server.ts +0 -797
  139. package/src/lib/auth-store.ts +0 -87
  140. package/src/lib/daemon-registry.test.ts +0 -429
  141. package/src/lib/daemon-registry.ts +0 -343
  142. package/src/lib/le/le-apply.test.ts +0 -591
  143. package/src/lib/le/le-apply.ts +0 -832
  144. package/src/lib/le/le-auth.ts +0 -89
  145. package/src/lib/le/le-cleanup.test.ts +0 -168
  146. package/src/lib/le/le-cleanup.ts +0 -399
  147. package/src/lib/le/le-cli-helpers.ts +0 -77
  148. package/src/lib/le/le-client.ts +0 -184
  149. package/src/lib/le/le-concurrency.test.ts +0 -157
  150. package/src/lib/le/le-concurrency.ts +0 -78
  151. package/src/lib/le/le-connection-test.ts +0 -42
  152. package/src/lib/le/le-devicehub-properties.test.ts +0 -53
  153. package/src/lib/le/le-devicehub-properties.ts +0 -33
  154. package/src/lib/le/le-devicehub.ts +0 -213
  155. package/src/lib/le/le-digital-twins.ts +0 -285
  156. package/src/lib/le/le-drivers.ts +0 -23
  157. package/src/lib/le/le-endpoint.ts +0 -40
  158. package/src/lib/le/le-environments.test.ts +0 -73
  159. package/src/lib/le/le-environments.ts +0 -156
  160. package/src/lib/le/le-markers.ts +0 -87
  161. package/src/lib/le/le-plan.test.ts +0 -26
  162. package/src/lib/le/le-plan.ts +0 -109
  163. package/src/lib/le/le-platform-operations.test.ts +0 -198
  164. package/src/lib/le/le-platform-operations.ts +0 -340
  165. package/src/lib/le/le-static-model-schema.ts +0 -78
  166. package/src/lib/le/le-static-planner.test.ts +0 -255
  167. package/src/lib/le/le-static-planner.ts +0 -442
  168. package/src/lib/le/le-template-exporter.test.ts +0 -527
  169. package/src/lib/le/le-template-exporter.ts +0 -459
  170. package/src/lib/le/le-value-types.ts +0 -23
  171. package/src/lib/mqtt-client.ts +0 -115
  172. package/src/lib/parse.test.ts +0 -123
  173. package/src/lib/parse.ts +0 -87
  174. package/src/lib/resolve-simulation.test.ts +0 -118
  175. package/src/lib/resolve-simulation.ts +0 -181
  176. package/src/lib/session-store.ts +0 -110
  177. package/src/lib/shutdown.test.ts +0 -562
  178. package/src/lib/shutdown.ts +0 -621
  179. package/src/lib/template-category.test.ts +0 -71
  180. package/src/lib/template-category.ts +0 -72
  181. package/src/lib/template-locator.ts +0 -55
  182. package/src/lib/template-utils.ts +0 -45
  183. package/src/lib/ws-hub.ts +0 -281
  184. package/src/logger.test.ts +0 -172
  185. package/src/logger.ts +0 -402
  186. package/src/output.test.ts +0 -64
  187. package/src/output.ts +0 -160
@@ -1,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
- }