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.
Files changed (69) hide show
  1. package/LICENSE +199 -0
  2. package/README.md +7 -70
  3. package/bin/miravo.js +84 -0
  4. package/package.json +20 -36
  5. package/console-dist/assets/fira-code-cyrillic-ext-wght-normal-DhYMMuQd.woff2 +0 -0
  6. package/console-dist/assets/fira-code-cyrillic-wght-normal-Y3u8pIsh.woff2 +0 -0
  7. package/console-dist/assets/fira-code-greek-ext-wght-normal-wWus70Ix.woff2 +0 -0
  8. package/console-dist/assets/fira-code-greek-wght-normal-B2SviObF.woff2 +0 -0
  9. package/console-dist/assets/fira-code-latin-ext-wght-normal-Dvfvaomy.woff2 +0 -0
  10. package/console-dist/assets/fira-code-latin-wght-normal-CHoedHDv.woff2 +0 -0
  11. package/console-dist/assets/fira-code-symbols2-wght-normal-CE6EOz_n.woff2 +0 -0
  12. package/console-dist/assets/index-C1oOHU1J.js +0 -119
  13. package/console-dist/assets/index-C3XMYq26.css +0 -2
  14. package/console-dist/assets/karla-latin-ext-wght-normal-Dq0rrjgs.woff2 +0 -0
  15. package/console-dist/assets/karla-latin-wght-normal-C3-ma4ov.woff2 +0 -0
  16. package/console-dist/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
  17. package/console-dist/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
  18. package/console-dist/favicon.svg +0 -37
  19. package/console-dist/index.html +0 -14
  20. package/console-dist/miravo-logo.png +0 -0
  21. package/src/__tests__/integration/executable-methods-runner.ts +0 -261
  22. package/src/__tests__/integration/executable-methods.integration.ts +0 -2256
  23. package/src/commands/adapter.ts +0 -76
  24. package/src/commands/cli-test-setup.ts +0 -91
  25. package/src/commands/fork.ts +0 -219
  26. package/src/commands/inject.test.ts +0 -57
  27. package/src/commands/inject.ts +0 -212
  28. package/src/commands/load.ts +0 -167
  29. package/src/commands/logs.ts +0 -175
  30. package/src/commands/models.test.ts +0 -162
  31. package/src/commands/models.ts +0 -338
  32. package/src/commands/pause.ts +0 -67
  33. package/src/commands/ps.ts +0 -116
  34. package/src/commands/remove.ts +0 -38
  35. package/src/commands/reset.ts +0 -44
  36. package/src/commands/rm.ts +0 -97
  37. package/src/commands/spawn.ts +0 -45
  38. package/src/commands/speed.ts +0 -40
  39. package/src/commands/start.test.ts +0 -481
  40. package/src/commands/start.ts +0 -2253
  41. package/src/commands/status.test.ts +0 -20
  42. package/src/commands/status.ts +0 -470
  43. package/src/commands/stop.test.ts +0 -84
  44. package/src/commands/stop.ts +0 -158
  45. package/src/commands/templates.test.ts +0 -58
  46. package/src/commands/templates.ts +0 -279
  47. package/src/index.ts +0 -158
  48. package/src/lib/admin-client.test.ts +0 -35
  49. package/src/lib/admin-client.ts +0 -226
  50. package/src/lib/admin-server.test.ts +0 -562
  51. package/src/lib/admin-server.ts +0 -658
  52. package/src/lib/daemon-registry.test.ts +0 -429
  53. package/src/lib/daemon-registry.ts +0 -345
  54. package/src/lib/mqtt-client.ts +0 -116
  55. package/src/lib/parse.test.ts +0 -123
  56. package/src/lib/parse.ts +0 -87
  57. package/src/lib/resolve-simulation.test.ts +0 -118
  58. package/src/lib/resolve-simulation.ts +0 -186
  59. package/src/lib/shutdown.test.ts +0 -380
  60. package/src/lib/shutdown.ts +0 -561
  61. package/src/lib/template-category.test.ts +0 -71
  62. package/src/lib/template-category.ts +0 -72
  63. package/src/lib/template-locator.ts +0 -49
  64. package/src/lib/template-utils.ts +0 -45
  65. package/src/lib/ws-hub.ts +0 -255
  66. package/src/logger.test.ts +0 -172
  67. package/src/logger.ts +0 -394
  68. package/src/output.test.ts +0 -64
  69. package/src/output.ts +0 -157
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }