miravo 0.5.0 → 0.6.1
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 +7 -70
- package/bin/miravo.js +84 -0
- package/package.json +20 -36
- 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/index-C1oOHU1J.js +0 -119
- package/console-dist/assets/index-C3XMYq26.css +0 -2
- 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/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/favicon.svg +0 -37
- package/console-dist/index.html +0 -14
- package/console-dist/miravo-logo.png +0 -0
- package/src/__tests__/integration/executable-methods-runner.ts +0 -261
- package/src/__tests__/integration/executable-methods.integration.ts +0 -2256
- package/src/commands/adapter.ts +0 -76
- package/src/commands/cli-test-setup.ts +0 -91
- 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 -175
- 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/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.test.ts +0 -481
- package/src/commands/start.ts +0 -2253
- 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 -158
- package/src/commands/templates.test.ts +0 -58
- package/src/commands/templates.ts +0 -279
- package/src/index.ts +0 -158
- package/src/lib/admin-client.test.ts +0 -35
- package/src/lib/admin-client.ts +0 -226
- package/src/lib/admin-server.test.ts +0 -562
- package/src/lib/admin-server.ts +0 -658
- package/src/lib/daemon-registry.test.ts +0 -429
- package/src/lib/daemon-registry.ts +0 -345
- package/src/lib/mqtt-client.ts +0 -116
- 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 -186
- package/src/lib/shutdown.test.ts +0 -380
- package/src/lib/shutdown.ts +0 -561
- package/src/lib/template-category.test.ts +0 -71
- package/src/lib/template-category.ts +0 -72
- package/src/lib/template-locator.ts +0 -49
- package/src/lib/template-utils.ts +0 -45
- package/src/lib/ws-hub.ts +0 -255
- package/src/logger.test.ts +0 -172
- package/src/logger.ts +0 -394
- package/src/output.test.ts +0 -64
- package/src/output.ts +0 -157
package/src/commands/models.ts
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { relative, resolve } from "node:path";
|
|
3
|
-
import { type DiscoveredModelWithSource, loadTwinModel } from "@miravo/core";
|
|
4
|
-
import type { GeneratorConfig, TwinModel } from "@miravo/shared";
|
|
5
|
-
import { TWIN_FILE_EXTENSION } from "@miravo/shared";
|
|
6
|
-
import type { Command } from "commander";
|
|
7
|
-
import { colors } from "consola/utils";
|
|
8
|
-
import { createCatalog } from "../lib/template-locator.ts";
|
|
9
|
-
import { configLog, handleCommandError, writeOutput } from "../logger.ts";
|
|
10
|
-
import { formatTable } from "../output.ts";
|
|
11
|
-
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
// Command registration
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
|
|
16
|
-
export function registerModelsCommand(program: Command): void {
|
|
17
|
-
const models = program.command("models").description("List and inspect asset models");
|
|
18
|
-
|
|
19
|
-
models
|
|
20
|
-
.command("list")
|
|
21
|
-
.description("Discover and list available models")
|
|
22
|
-
.option("--dir <path>", "Additional directory to search for models")
|
|
23
|
-
.addHelpText(
|
|
24
|
-
"after",
|
|
25
|
-
`
|
|
26
|
-
Examples:
|
|
27
|
-
$ miravo models list List all discoverable models`,
|
|
28
|
-
)
|
|
29
|
-
.action(async (options: { readonly dir?: string }) => {
|
|
30
|
-
await modelsListAction(options);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
models
|
|
34
|
-
.command("inspect <name>")
|
|
35
|
-
.description("Show detailed information about a model")
|
|
36
|
-
.option("--raw", "Dump the raw YAML file to stdout")
|
|
37
|
-
.addHelpText(
|
|
38
|
-
"after",
|
|
39
|
-
`
|
|
40
|
-
Examples:
|
|
41
|
-
$ miravo models inspect centrifugal-pump Show model details
|
|
42
|
-
$ miravo models inspect centrifugal-pump --raw Dump raw YAML`,
|
|
43
|
-
)
|
|
44
|
-
.action(async (name: string, options: { readonly raw?: boolean }) => {
|
|
45
|
-
await modelsInspectAction(name, options);
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
// models list
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
|
|
53
|
-
async function modelsListAction(options: { readonly dir?: string }): Promise<void> {
|
|
54
|
-
try {
|
|
55
|
-
const catalog = createCatalog();
|
|
56
|
-
const allModels = await catalog.listModels();
|
|
57
|
-
|
|
58
|
-
const builtinModels: DiscoveredModelWithSource[] = [];
|
|
59
|
-
const localRegModels: DiscoveredModelWithSource[] = [];
|
|
60
|
-
const cwdModels: DiscoveredModelWithSource[] = [];
|
|
61
|
-
|
|
62
|
-
for (const model of allModels) {
|
|
63
|
-
switch (model.source.kind) {
|
|
64
|
-
case "builtin":
|
|
65
|
-
builtinModels.push(model);
|
|
66
|
-
break;
|
|
67
|
-
case "local":
|
|
68
|
-
localRegModels.push(model);
|
|
69
|
-
break;
|
|
70
|
-
case "cwd":
|
|
71
|
-
cwdModels.push(model);
|
|
72
|
-
break;
|
|
73
|
-
default: {
|
|
74
|
-
const _exhaustive: never = model.source.kind;
|
|
75
|
-
throw new Error(`Unexpected content source kind: ${String(_exhaustive)}`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (options.dir) {
|
|
81
|
-
const resolvedDir = resolve(options.dir);
|
|
82
|
-
if (!existsSync(resolvedDir)) {
|
|
83
|
-
configLog.error(`Directory not found: ${resolvedDir}`);
|
|
84
|
-
process.exitCode = 1;
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const { discoverModels } = await import("@miravo/core");
|
|
88
|
-
const extraModels = await discoverModels(resolvedDir);
|
|
89
|
-
const existingPaths = new Set(cwdModels.map((m) => m.path));
|
|
90
|
-
for (const extra of extraModels) {
|
|
91
|
-
if (!existingPaths.has(extra.path)) {
|
|
92
|
-
existingPaths.add(extra.path);
|
|
93
|
-
cwdModels.push({ ...extra, source: { kind: "cwd", path: resolvedDir } });
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (builtinModels.length === 0 && cwdModels.length === 0 && localRegModels.length === 0) {
|
|
99
|
-
configLog.info("No models found.");
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const headers = {
|
|
104
|
-
name: colors.dim("Name"),
|
|
105
|
-
members: colors.dim("Members"),
|
|
106
|
-
methods: colors.dim("Methods"),
|
|
107
|
-
faults: colors.dim("Faults"),
|
|
108
|
-
lifecycle: colors.dim("Lifecycle"),
|
|
109
|
-
source: colors.dim("Source"),
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const renderSection = (label: string, models: readonly DiscoveredModelWithSource[]): void => {
|
|
113
|
-
if (models.length === 0) return;
|
|
114
|
-
writeOutput();
|
|
115
|
-
writeOutput(` ${colors.bold(`${label}:`)}`);
|
|
116
|
-
writeOutput(
|
|
117
|
-
formatTable(
|
|
118
|
-
headers,
|
|
119
|
-
models.map((m) => ({
|
|
120
|
-
name: colors.cyan(m.name),
|
|
121
|
-
members: String(m.memberCount),
|
|
122
|
-
methods: String(m.methodCount),
|
|
123
|
-
faults: String(m.faultCount),
|
|
124
|
-
lifecycle:
|
|
125
|
-
m.lifecycleStageCount > 0 ? `${m.lifecycleStageCount} stages` : colors.dim("none"),
|
|
126
|
-
source: colors.dim(m.source.kind),
|
|
127
|
-
})),
|
|
128
|
-
),
|
|
129
|
-
);
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
renderSection("Built-in", builtinModels);
|
|
133
|
-
renderSection("Local", localRegModels);
|
|
134
|
-
renderSection("Project", cwdModels);
|
|
135
|
-
writeOutput();
|
|
136
|
-
} catch (error) {
|
|
137
|
-
handleCommandError(error);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// ---------------------------------------------------------------------------
|
|
142
|
-
// models inspect
|
|
143
|
-
// ---------------------------------------------------------------------------
|
|
144
|
-
|
|
145
|
-
async function modelsInspectAction(
|
|
146
|
-
name: string,
|
|
147
|
-
options: { readonly raw?: boolean },
|
|
148
|
-
): Promise<void> {
|
|
149
|
-
try {
|
|
150
|
-
const catalog = createCatalog();
|
|
151
|
-
const isPath = name.includes("/") || name.endsWith(TWIN_FILE_EXTENSION);
|
|
152
|
-
const resolvedPath = isPath ? resolve(name) : await catalog.resolveModel(name);
|
|
153
|
-
|
|
154
|
-
const sourceLabel = catalog.getSourceLabel(resolvedPath);
|
|
155
|
-
|
|
156
|
-
if (options.raw) {
|
|
157
|
-
const rawText = await Bun.file(resolvedPath).text();
|
|
158
|
-
process.stdout.write(rawText);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const model = await loadTwinModel(resolvedPath);
|
|
163
|
-
const output = formatModelInspect(model, resolvedPath, sourceLabel);
|
|
164
|
-
writeOutput(output);
|
|
165
|
-
} catch (error) {
|
|
166
|
-
handleCommandError(error);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ---------------------------------------------------------------------------
|
|
171
|
-
// Formatting helpers (exported for testing)
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
|
-
|
|
174
|
-
export function formatGeneratorSummary(generator: GeneratorConfig): string {
|
|
175
|
-
switch (generator.type) {
|
|
176
|
-
case "sine":
|
|
177
|
-
return `sine (center: ${generator.center})`;
|
|
178
|
-
case "noise":
|
|
179
|
-
return `noise (center: ${generator.center})`;
|
|
180
|
-
case "correlated":
|
|
181
|
-
return `correlated (source: ${generator.source})`;
|
|
182
|
-
case "random-walk":
|
|
183
|
-
return `random-walk (${generator.min}..${generator.max})`;
|
|
184
|
-
case "step":
|
|
185
|
-
return `step (${generator.values.length} values)`;
|
|
186
|
-
case "state-machine":
|
|
187
|
-
return `state-machine (${generator.states.length} states)`;
|
|
188
|
-
case "health-dependent":
|
|
189
|
-
return `health-dependent (${Object.keys(generator.stages).length} stages)`;
|
|
190
|
-
case "ramp":
|
|
191
|
-
return `ramp (rate: ${generator.rate}, duration: ${generator.duration}s)`;
|
|
192
|
-
case "mean-reverting-noise":
|
|
193
|
-
return `mean-reverting-noise (mean: ${generator.mean}, tau: ${generator.tau}s)`;
|
|
194
|
-
case "timetable":
|
|
195
|
-
return `timetable (${generator.points.length} points)`;
|
|
196
|
-
case "first-order-lag":
|
|
197
|
-
return `first-order-lag (tau: ${generator.tau}s)`;
|
|
198
|
-
case "integrator":
|
|
199
|
-
return `integrator (gain: ${generator.gain})`;
|
|
200
|
-
case "transport-delay":
|
|
201
|
-
return `transport-delay (delay: ${generator.delay}s)`;
|
|
202
|
-
case "slew-rate-limiter":
|
|
203
|
-
return `slew-rate-limiter (rate_up: ${generator.rate_up})`;
|
|
204
|
-
case "second-order":
|
|
205
|
-
return `second-order (freq: ${generator.natural_freq_hz}Hz, zeta: ${generator.damping_ratio})`;
|
|
206
|
-
case "hysteresis-relay":
|
|
207
|
-
return `hysteresis-relay (on: ${generator.on_threshold}, off: ${generator.off_threshold})`;
|
|
208
|
-
case "weighted-sum":
|
|
209
|
-
return `weighted-sum (${generator.terms.length} terms)`;
|
|
210
|
-
case "gate":
|
|
211
|
-
return `gate (enabled: ${generator.enabled_member})`;
|
|
212
|
-
case "waveform":
|
|
213
|
-
return `waveform (shape: ${generator.shape}, period: ${generator.period}s)`;
|
|
214
|
-
case "chirp":
|
|
215
|
-
return `chirp (${generator.start_freq_hz}–${generator.end_freq_hz}Hz, ${generator.sweep_duration}s)`;
|
|
216
|
-
case "impulse":
|
|
217
|
-
return `impulse (amplitude: ${generator.amplitude}${generator.probability !== undefined ? `, p=${generator.probability}` : ``})`;
|
|
218
|
-
default: {
|
|
219
|
-
const _exhaustive: never = generator;
|
|
220
|
-
return String(_exhaustive);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export function formatModelInspect(
|
|
226
|
-
model: TwinModel,
|
|
227
|
-
sourcePath: string,
|
|
228
|
-
sourceLabel: string,
|
|
229
|
-
): string {
|
|
230
|
-
const lines: string[] = [];
|
|
231
|
-
|
|
232
|
-
// Header
|
|
233
|
-
if (model.model.description) {
|
|
234
|
-
lines.push(
|
|
235
|
-
` ${colors.cyan(colors.bold(model.model.name))} ${colors.dim("\u2014")} ${model.model.description}`,
|
|
236
|
-
);
|
|
237
|
-
} else {
|
|
238
|
-
lines.push(` ${colors.cyan(colors.bold(model.model.name))}`);
|
|
239
|
-
}
|
|
240
|
-
lines.push("");
|
|
241
|
-
|
|
242
|
-
// Parameters
|
|
243
|
-
if (model.parameters && Object.keys(model.parameters).length > 0) {
|
|
244
|
-
lines.push(` ${colors.bold("Parameters:")}`);
|
|
245
|
-
lines.push(
|
|
246
|
-
formatTable(
|
|
247
|
-
{
|
|
248
|
-
name: colors.dim("Name"),
|
|
249
|
-
type: colors.dim("Type"),
|
|
250
|
-
defaultVal: colors.dim("Default"),
|
|
251
|
-
unit: colors.dim("Unit"),
|
|
252
|
-
},
|
|
253
|
-
Object.entries(model.parameters).map(([name, param]) => ({
|
|
254
|
-
name,
|
|
255
|
-
type: param.type,
|
|
256
|
-
defaultVal: String(param.default),
|
|
257
|
-
unit: param.unit ?? "",
|
|
258
|
-
})),
|
|
259
|
-
),
|
|
260
|
-
);
|
|
261
|
-
lines.push("");
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Members
|
|
265
|
-
const memberEntries = Object.entries(model.members) as Array<
|
|
266
|
-
[string, TwinModel["members"][string]]
|
|
267
|
-
>;
|
|
268
|
-
lines.push(` ${colors.bold(`Members (${memberEntries.length}):`)}`);
|
|
269
|
-
lines.push(
|
|
270
|
-
formatTable(
|
|
271
|
-
{
|
|
272
|
-
name: colors.dim("Name"),
|
|
273
|
-
kind: colors.dim("Kind"),
|
|
274
|
-
dataType: colors.dim("Data Type"),
|
|
275
|
-
unit: colors.dim("Unit"),
|
|
276
|
-
generator: colors.dim("Generator"),
|
|
277
|
-
},
|
|
278
|
-
memberEntries.map(([name, member]) => ({
|
|
279
|
-
name,
|
|
280
|
-
kind: member.kind,
|
|
281
|
-
dataType: member.data_type,
|
|
282
|
-
unit: member.unit ?? "",
|
|
283
|
-
generator:
|
|
284
|
-
member.kind === "variable" && member.generator
|
|
285
|
-
? formatGeneratorSummary(member.generator)
|
|
286
|
-
: colors.dim("none"),
|
|
287
|
-
})),
|
|
288
|
-
),
|
|
289
|
-
);
|
|
290
|
-
lines.push("");
|
|
291
|
-
|
|
292
|
-
const methodEntries = Object.entries(model.methods ?? {});
|
|
293
|
-
if (methodEntries.length > 0) {
|
|
294
|
-
lines.push(` ${colors.bold(`Methods (${methodEntries.length}):`)}`);
|
|
295
|
-
lines.push(
|
|
296
|
-
formatTable(
|
|
297
|
-
{
|
|
298
|
-
name: colors.dim("Name"),
|
|
299
|
-
inputs: colors.dim("Inputs"),
|
|
300
|
-
outputs: colors.dim("Outputs"),
|
|
301
|
-
description: colors.dim("Description"),
|
|
302
|
-
},
|
|
303
|
-
methodEntries.map(([name, method]) => ({
|
|
304
|
-
name,
|
|
305
|
-
inputs: String(Object.keys(method.input_arguments ?? {}).length),
|
|
306
|
-
outputs: String(Object.keys(method.output_arguments ?? {}).length),
|
|
307
|
-
description: method.description ?? "",
|
|
308
|
-
})),
|
|
309
|
-
),
|
|
310
|
-
);
|
|
311
|
-
lines.push("");
|
|
312
|
-
} else {
|
|
313
|
-
lines.push(` ${colors.bold("Methods:")} ${colors.dim("none")}`);
|
|
314
|
-
lines.push("");
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Lifecycle
|
|
318
|
-
if (model.lifecycle && model.lifecycle.stages.length > 0) {
|
|
319
|
-
const stageNames = model.lifecycle.stages.map((s) => s.name).join(` ${colors.dim("\u2192")} `);
|
|
320
|
-
lines.push(` ${colors.bold("Lifecycle:")} ${stageNames}`);
|
|
321
|
-
} else {
|
|
322
|
-
lines.push(` ${colors.bold("Lifecycle:")} ${colors.dim("none")}`);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Faults
|
|
326
|
-
if (model.faults && model.faults.length > 0) {
|
|
327
|
-
const faultNames = model.faults.map((f) => f.name).join(", ");
|
|
328
|
-
lines.push(` ${colors.bold("Faults:")} ${faultNames}`);
|
|
329
|
-
} else {
|
|
330
|
-
lines.push(` ${colors.bold("Faults:")} ${colors.dim("none")}`);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Source
|
|
334
|
-
const relPath = relative(process.cwd(), sourcePath);
|
|
335
|
-
lines.push(` ${colors.bold("Source:")} ${sourceLabel} ${colors.gray(`(${relPath})`)}`);
|
|
336
|
-
|
|
337
|
-
return lines.join("\n");
|
|
338
|
-
}
|
package/src/commands/pause.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import type { EngineCommand } from "@miravo/shared";
|
|
2
|
-
import { CONTROL_CMD_TIMEOUT_MS } from "@miravo/shared";
|
|
3
|
-
import type { Command } from "commander";
|
|
4
|
-
import { executeAdminCommand, resolveAdminUrl } from "../lib/admin-client.ts";
|
|
5
|
-
import { parseTimeoutSeconds } from "../lib/parse.ts";
|
|
6
|
-
import { handleCommandError, handleCommandResult } from "../logger.ts";
|
|
7
|
-
|
|
8
|
-
interface PauseResumeOptions {
|
|
9
|
-
readonly json?: boolean;
|
|
10
|
-
readonly adminUrl?: string;
|
|
11
|
-
readonly timeout?: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function registerPauseCommand(program: Command): void {
|
|
15
|
-
program
|
|
16
|
-
.command("pause")
|
|
17
|
-
.description("Pause the running simulation")
|
|
18
|
-
.option("--json", "Machine-readable JSON output")
|
|
19
|
-
.option("--admin-url <url>", "Miravo admin server URL (env: MIRAVO_ADMIN_URL)")
|
|
20
|
-
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
21
|
-
.addHelpText(
|
|
22
|
-
"after",
|
|
23
|
-
`
|
|
24
|
-
Examples:
|
|
25
|
-
$ miravo pause Pause the simulation`,
|
|
26
|
-
)
|
|
27
|
-
.action(async (options: PauseResumeOptions) => {
|
|
28
|
-
try {
|
|
29
|
-
await sendSimpleCommand("pause", options);
|
|
30
|
-
} catch (error) {
|
|
31
|
-
handleCommandError(error);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function registerResumeCommand(program: Command): void {
|
|
37
|
-
program
|
|
38
|
-
.command("resume")
|
|
39
|
-
.description("Resume a paused simulation")
|
|
40
|
-
.option("--json", "Machine-readable JSON output")
|
|
41
|
-
.option("--admin-url <url>", "Miravo admin server URL (env: MIRAVO_ADMIN_URL)")
|
|
42
|
-
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
43
|
-
.addHelpText(
|
|
44
|
-
"after",
|
|
45
|
-
`
|
|
46
|
-
Examples:
|
|
47
|
-
$ miravo resume Resume a paused simulation`,
|
|
48
|
-
)
|
|
49
|
-
.action(async (options: PauseResumeOptions) => {
|
|
50
|
-
try {
|
|
51
|
-
await sendSimpleCommand("resume", options);
|
|
52
|
-
} catch (error) {
|
|
53
|
-
handleCommandError(error);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function sendSimpleCommand(
|
|
59
|
-
type: "pause" | "resume",
|
|
60
|
-
options: PauseResumeOptions,
|
|
61
|
-
): Promise<void> {
|
|
62
|
-
const adminUrl = resolveAdminUrl(options.adminUrl);
|
|
63
|
-
const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
|
|
64
|
-
const command: EngineCommand = { type };
|
|
65
|
-
const result = await executeAdminCommand(adminUrl, command, timeoutMs);
|
|
66
|
-
handleCommandResult(result, options.json);
|
|
67
|
-
}
|
package/src/commands/ps.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import type { EngineMetricsSnapshot } from "@miravo/shared";
|
|
2
|
-
import type { Command } from "commander";
|
|
3
|
-
import { isPidAlive, listEntries, sweepStaleEntries } from "../lib/daemon-registry.ts";
|
|
4
|
-
import { handleCommandError, writeOutput } from "../logger.ts";
|
|
5
|
-
import { formatTable } from "../output.ts";
|
|
6
|
-
|
|
7
|
-
interface PsOptions {
|
|
8
|
-
readonly json?: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function registerPsCommand(program: Command): void {
|
|
12
|
-
program
|
|
13
|
-
.command("ps")
|
|
14
|
-
.description("List background simulations with live metrics")
|
|
15
|
-
.option("--json", "Machine-readable JSON output")
|
|
16
|
-
.addHelpText(
|
|
17
|
-
"after",
|
|
18
|
-
`
|
|
19
|
-
Examples:
|
|
20
|
-
$ miravo ps List background simulations
|
|
21
|
-
$ miravo ps --json Machine-readable JSON output`,
|
|
22
|
-
)
|
|
23
|
-
.action(async (options: PsOptions) => {
|
|
24
|
-
try {
|
|
25
|
-
await psAction(options);
|
|
26
|
-
} catch (error) {
|
|
27
|
-
handleCommandError(error);
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
type PsRow = Record<string, string> & {
|
|
33
|
-
readonly name: string;
|
|
34
|
-
readonly status: string;
|
|
35
|
-
readonly uptime: string;
|
|
36
|
-
readonly ticks: string;
|
|
37
|
-
readonly instances: string;
|
|
38
|
-
readonly admin: string;
|
|
39
|
-
readonly mqtt: string;
|
|
40
|
-
readonly opcua: string;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
async function psAction(options: PsOptions): Promise<void> {
|
|
44
|
-
await sweepStaleEntries();
|
|
45
|
-
const entries = await listEntries();
|
|
46
|
-
|
|
47
|
-
if (entries.length === 0) {
|
|
48
|
-
writeOutput("No running simulations.");
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const rows: PsRow[] = [];
|
|
53
|
-
|
|
54
|
-
for (const entry of entries) {
|
|
55
|
-
const alive = isPidAlive(entry.pid);
|
|
56
|
-
let status = alive ? "running" : "dead";
|
|
57
|
-
let uptime = "-";
|
|
58
|
-
let ticks = "-";
|
|
59
|
-
let instances = "-";
|
|
60
|
-
|
|
61
|
-
if (alive) {
|
|
62
|
-
try {
|
|
63
|
-
const url = `http://${entry.adminHost}:${entry.adminPort}/metrics`;
|
|
64
|
-
const response = await fetch(url, { signal: AbortSignal.timeout(2_000) });
|
|
65
|
-
if (response.ok) {
|
|
66
|
-
const metrics = (await response.json()) as EngineMetricsSnapshot;
|
|
67
|
-
status = metrics.core.state === "running" ? "running" : metrics.core.state;
|
|
68
|
-
uptime = formatUptime(metrics.core.uptimeMs);
|
|
69
|
-
ticks = String(metrics.core.tickCount);
|
|
70
|
-
instances = String(metrics.core.instanceCount);
|
|
71
|
-
}
|
|
72
|
-
} catch {
|
|
73
|
-
status = "starting";
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const mqttAdapter = entry.adapters["mqtt"];
|
|
78
|
-
const opcuaAdapter = entry.adapters["opcua"];
|
|
79
|
-
|
|
80
|
-
rows.push({
|
|
81
|
-
name: entry.name,
|
|
82
|
-
status,
|
|
83
|
-
uptime,
|
|
84
|
-
ticks,
|
|
85
|
-
instances,
|
|
86
|
-
admin: `:${entry.adminPort}`,
|
|
87
|
-
mqtt: mqttAdapter ? `:${mqttAdapter.port}` : "-",
|
|
88
|
-
opcua: opcuaAdapter ? `:${opcuaAdapter.port}` : "-",
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (options.json) {
|
|
93
|
-
writeOutput(JSON.stringify(rows, null, 2));
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const headers: Record<string, string> = {
|
|
98
|
-
name: "NAME",
|
|
99
|
-
status: "STATUS",
|
|
100
|
-
uptime: "UPTIME",
|
|
101
|
-
ticks: "TICKS",
|
|
102
|
-
instances: "INSTANCES",
|
|
103
|
-
admin: "ADMIN",
|
|
104
|
-
mqtt: "MQTT",
|
|
105
|
-
opcua: "OPC UA",
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
writeOutput(formatTable(headers, rows));
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function formatUptime(ms: number): string {
|
|
112
|
-
const totalSeconds = Math.floor(ms / 1_000);
|
|
113
|
-
const hours = Math.floor(totalSeconds / 3_600);
|
|
114
|
-
const minutes = Math.floor((totalSeconds % 3_600) / 60);
|
|
115
|
-
return `${hours}h ${String(minutes).padStart(2, "0")}m`;
|
|
116
|
-
}
|
package/src/commands/remove.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { EngineCommand } from "@miravo/shared";
|
|
2
|
-
import { CONTROL_CMD_TIMEOUT_MS } from "@miravo/shared";
|
|
3
|
-
import type { Command } from "commander";
|
|
4
|
-
import { executeAdminCommand, resolveAdminUrl } from "../lib/admin-client.ts";
|
|
5
|
-
import { parseTimeoutSeconds } from "../lib/parse.ts";
|
|
6
|
-
import { handleCommandError, handleCommandResult } from "../logger.ts";
|
|
7
|
-
|
|
8
|
-
interface RemoveOptions {
|
|
9
|
-
readonly json?: boolean;
|
|
10
|
-
readonly adminUrl?: string;
|
|
11
|
-
readonly timeout?: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function registerRemoveCommand(program: Command): void {
|
|
15
|
-
program
|
|
16
|
-
.command("remove <instanceId>")
|
|
17
|
-
.description("Remove an instance from the running simulation")
|
|
18
|
-
.option("--json", "Machine-readable JSON output")
|
|
19
|
-
.option("--admin-url <url>", "Miravo admin server URL (env: MIRAVO_ADMIN_URL)")
|
|
20
|
-
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
21
|
-
.addHelpText(
|
|
22
|
-
"after",
|
|
23
|
-
`
|
|
24
|
-
Examples:
|
|
25
|
-
$ miravo remove pump-001 Remove instance by ID`,
|
|
26
|
-
)
|
|
27
|
-
.action(async (instanceId: string, options: RemoveOptions) => {
|
|
28
|
-
try {
|
|
29
|
-
const adminUrl = resolveAdminUrl(options.adminUrl);
|
|
30
|
-
const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
|
|
31
|
-
const command: EngineCommand = { type: "removeInstance", instanceId };
|
|
32
|
-
const result = await executeAdminCommand(adminUrl, command, timeoutMs);
|
|
33
|
-
handleCommandResult(result, options.json);
|
|
34
|
-
} catch (error) {
|
|
35
|
-
handleCommandError(error);
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
}
|
package/src/commands/reset.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { EngineCommand } from "@miravo/shared";
|
|
2
|
-
import { CONTROL_CMD_TIMEOUT_MS } from "@miravo/shared";
|
|
3
|
-
import type { Command } from "commander";
|
|
4
|
-
import { executeAdminCommand, resolveAdminUrl } from "../lib/admin-client.ts";
|
|
5
|
-
import { parseTimeoutSeconds } from "../lib/parse.ts";
|
|
6
|
-
import { handleCommandError, handleCommandResult } from "../logger.ts";
|
|
7
|
-
|
|
8
|
-
interface ResetOptions {
|
|
9
|
-
readonly clearPersistence?: boolean;
|
|
10
|
-
readonly json?: boolean;
|
|
11
|
-
readonly adminUrl?: string;
|
|
12
|
-
readonly timeout?: number;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function registerResetCommand(program: Command): void {
|
|
16
|
-
program
|
|
17
|
-
.command("reset")
|
|
18
|
-
.description("Reset the simulation: remove all instances and template runs")
|
|
19
|
-
.option("--clear-persistence", "Also clear persisted state (start fresh on next boot)")
|
|
20
|
-
.option("--json", "Machine-readable JSON output")
|
|
21
|
-
.option("--admin-url <url>", "Miravo admin server URL (env: MIRAVO_ADMIN_URL)")
|
|
22
|
-
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
23
|
-
.addHelpText(
|
|
24
|
-
"after",
|
|
25
|
-
`
|
|
26
|
-
Examples:
|
|
27
|
-
$ miravo reset Reset simulation state
|
|
28
|
-
$ miravo reset --clear-persistence Reset and clear saved state`,
|
|
29
|
-
)
|
|
30
|
-
.action(async (options: ResetOptions) => {
|
|
31
|
-
try {
|
|
32
|
-
const adminUrl = resolveAdminUrl(options.adminUrl);
|
|
33
|
-
const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
|
|
34
|
-
const command: EngineCommand = {
|
|
35
|
-
type: "resetSimulation",
|
|
36
|
-
clearPersistence: options.clearPersistence ?? false,
|
|
37
|
-
};
|
|
38
|
-
const result = await executeAdminCommand(adminUrl, command, timeoutMs);
|
|
39
|
-
handleCommandResult(result, options.json);
|
|
40
|
-
} catch (error) {
|
|
41
|
-
handleCommandError(error);
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
}
|
package/src/commands/rm.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import type { Command } from "commander";
|
|
2
|
-
import { probeEntryIdentity, readEntry, removeEntry } from "../lib/daemon-registry.ts";
|
|
3
|
-
import { handleCommandError, writeOutput } from "../logger.ts";
|
|
4
|
-
|
|
5
|
-
interface RmOptions {
|
|
6
|
-
readonly force?: boolean;
|
|
7
|
-
readonly json?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function registerRmCommand(program: Command): void {
|
|
11
|
-
program
|
|
12
|
-
.command("rm <name>")
|
|
13
|
-
.description("Remove a stale daemon registry entry")
|
|
14
|
-
.option("-f, --force", "Kill the process if still alive before removing")
|
|
15
|
-
.option("--json", "Machine-readable JSON output")
|
|
16
|
-
.addHelpText(
|
|
17
|
-
"after",
|
|
18
|
-
`
|
|
19
|
-
Examples:
|
|
20
|
-
$ miravo rm my-sim Remove stale registry entry
|
|
21
|
-
$ miravo rm my-sim --force Kill process and remove entry`,
|
|
22
|
-
)
|
|
23
|
-
.action(async (name: string, options: RmOptions) => {
|
|
24
|
-
try {
|
|
25
|
-
await rmAction(name, options);
|
|
26
|
-
} catch (error) {
|
|
27
|
-
handleCommandError(error);
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function jsonOut(obj: Record<string, unknown>): void {
|
|
33
|
-
console.log(JSON.stringify(obj, null, 2));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async function rmAction(name: string, options: RmOptions): Promise<void> {
|
|
37
|
-
const entry = await readEntry(name);
|
|
38
|
-
if (!entry) {
|
|
39
|
-
const msg = `Simulation "${name}" not found in daemon registry.`;
|
|
40
|
-
if (options.json) {
|
|
41
|
-
jsonOut({ success: false, name, message: msg });
|
|
42
|
-
} else {
|
|
43
|
-
writeOutput(msg);
|
|
44
|
-
}
|
|
45
|
-
process.exitCode = 1;
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const identity = await probeEntryIdentity(entry);
|
|
50
|
-
|
|
51
|
-
if (identity.status === "unreachable") {
|
|
52
|
-
const msg = `Simulation "${name}" could not be safely verified because its admin server is unreachable while PID ${entry.pid} is still alive.`;
|
|
53
|
-
if (options.json) {
|
|
54
|
-
jsonOut({
|
|
55
|
-
success: false,
|
|
56
|
-
name,
|
|
57
|
-
message: msg,
|
|
58
|
-
error: "Retry later, or inspect the process manually before removing the registry entry.",
|
|
59
|
-
});
|
|
60
|
-
} else {
|
|
61
|
-
writeOutput(
|
|
62
|
-
`${msg} Retry later, or inspect the process manually before removing the registry entry.`,
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
process.exitCode = 1;
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (identity.status === "verified") {
|
|
70
|
-
if (!options.force) {
|
|
71
|
-
const msg = `Simulation "${name}" is still running (PID ${entry.pid}). Use --force to kill it, or 'miravo stop ${name}' for graceful shutdown.`;
|
|
72
|
-
if (options.json) {
|
|
73
|
-
jsonOut({ success: false, name, message: msg });
|
|
74
|
-
} else {
|
|
75
|
-
writeOutput(msg);
|
|
76
|
-
}
|
|
77
|
-
process.exitCode = 1;
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Force kill
|
|
82
|
-
try {
|
|
83
|
-
process.kill(entry.pid, "SIGKILL");
|
|
84
|
-
} catch {
|
|
85
|
-
// Ignore — may have exited between check and kill
|
|
86
|
-
}
|
|
87
|
-
await Bun.sleep(200);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
await removeEntry(name);
|
|
91
|
-
const msg = `Removed daemon registry entry for "${name}".`;
|
|
92
|
-
if (options.json) {
|
|
93
|
-
jsonOut({ success: true, name, message: msg });
|
|
94
|
-
} else {
|
|
95
|
-
writeOutput(msg);
|
|
96
|
-
}
|
|
97
|
-
}
|