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/adapter.ts
DELETED
|
@@ -1,76 +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 { parsePositiveInt, parseTimeoutSeconds } from "../lib/parse.ts";
|
|
6
|
-
import { handleCommandError, handleCommandResult } from "../logger.ts";
|
|
7
|
-
|
|
8
|
-
interface AdapterOptions {
|
|
9
|
-
readonly port?: number;
|
|
10
|
-
readonly host?: string;
|
|
11
|
-
readonly json?: boolean;
|
|
12
|
-
readonly adminUrl?: string;
|
|
13
|
-
readonly timeout?: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function registerAdapterCommand(program: Command): void {
|
|
17
|
-
const adapter = program.command("adapter").description("Manage protocol adapters (MQTT, OPC UA)");
|
|
18
|
-
|
|
19
|
-
adapter
|
|
20
|
-
.command("enable <name>")
|
|
21
|
-
.description("Enable a protocol adapter (mqtt, opcua)")
|
|
22
|
-
.option("-p, --port <number>", "Override adapter port", parsePositiveInt)
|
|
23
|
-
.option("--host <host>", "Override adapter bind host")
|
|
24
|
-
.option("--json", "Machine-readable JSON output")
|
|
25
|
-
.option("--admin-url <url>", "Miravo admin server URL (env: MIRAVO_ADMIN_URL)")
|
|
26
|
-
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
27
|
-
.addHelpText(
|
|
28
|
-
"after",
|
|
29
|
-
`
|
|
30
|
-
Examples:
|
|
31
|
-
$ miravo adapter enable mqtt Enable MQTT adapter
|
|
32
|
-
$ miravo adapter enable opcua -p 4841 Enable OPC UA on custom port`,
|
|
33
|
-
)
|
|
34
|
-
.action(async (name: string, options: AdapterOptions) => {
|
|
35
|
-
try {
|
|
36
|
-
const adminUrl = resolveAdminUrl(options.adminUrl);
|
|
37
|
-
const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
|
|
38
|
-
const config: Record<string, unknown> = {};
|
|
39
|
-
if (options.port !== undefined) config["port"] = options.port;
|
|
40
|
-
if (options.host !== undefined) config["host"] = options.host;
|
|
41
|
-
const command: EngineCommand = {
|
|
42
|
-
type: "enableAdapter",
|
|
43
|
-
adapter: name,
|
|
44
|
-
...(Object.keys(config).length > 0 ? { config } : {}),
|
|
45
|
-
};
|
|
46
|
-
const result = await executeAdminCommand(adminUrl, command, timeoutMs);
|
|
47
|
-
handleCommandResult(result, options.json);
|
|
48
|
-
} catch (error) {
|
|
49
|
-
handleCommandError(error);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
adapter
|
|
54
|
-
.command("disable <name>")
|
|
55
|
-
.description("Disable a running protocol adapter (mqtt, opcua)")
|
|
56
|
-
.option("--json", "Machine-readable JSON output")
|
|
57
|
-
.option("--admin-url <url>", "Miravo admin server URL (env: MIRAVO_ADMIN_URL)")
|
|
58
|
-
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
59
|
-
.addHelpText(
|
|
60
|
-
"after",
|
|
61
|
-
`
|
|
62
|
-
Examples:
|
|
63
|
-
$ miravo adapter disable mqtt Disable MQTT adapter`,
|
|
64
|
-
)
|
|
65
|
-
.action(async (name: string, options: AdapterOptions) => {
|
|
66
|
-
try {
|
|
67
|
-
const adminUrl = resolveAdminUrl(options.adminUrl);
|
|
68
|
-
const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
|
|
69
|
-
const command: EngineCommand = { type: "disableAdapter", adapter: name };
|
|
70
|
-
const result = await executeAdminCommand(adminUrl, command, timeoutMs);
|
|
71
|
-
handleCommandResult(result, options.json);
|
|
72
|
-
} catch (error) {
|
|
73
|
-
handleCommandError(error);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared CLI test utilities: suppresses logs and provides command lookup.
|
|
3
|
-
*
|
|
4
|
-
* Every CLI test file should import this module. It:
|
|
5
|
-
* 1. Mutes all consola loggers at import time (level -999 + no-op reporter)
|
|
6
|
-
* 2. Re-mutes before every test (guards against tests that unlock/reconfigure)
|
|
7
|
-
*
|
|
8
|
-
* Note: process.exitCode must be reset in each test file's own beforeEach/afterEach.
|
|
9
|
-
*/
|
|
10
|
-
import { beforeEach } from "bun:test";
|
|
11
|
-
import {
|
|
12
|
-
configLog,
|
|
13
|
-
configureLogger,
|
|
14
|
-
controlLog,
|
|
15
|
-
engineLog,
|
|
16
|
-
faultLog,
|
|
17
|
-
lifecycleLog,
|
|
18
|
-
lockLogger,
|
|
19
|
-
logger,
|
|
20
|
-
mqttLog,
|
|
21
|
-
unlockLogger,
|
|
22
|
-
} from "../logger.ts";
|
|
23
|
-
|
|
24
|
-
const noopReporter = { log() {} };
|
|
25
|
-
const allLoggers = [logger, engineLog, mqttLog, configLog, lifecycleLog, faultLog, controlLog];
|
|
26
|
-
|
|
27
|
-
export function muteAllLoggers(): void {
|
|
28
|
-
unlockLogger();
|
|
29
|
-
configureLogger({ quiet: true });
|
|
30
|
-
for (const l of allLoggers) {
|
|
31
|
-
l.level = -999;
|
|
32
|
-
l.setReporters([noopReporter]);
|
|
33
|
-
}
|
|
34
|
-
lockLogger();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
muteAllLoggers();
|
|
38
|
-
|
|
39
|
-
beforeEach(() => {
|
|
40
|
-
muteAllLoggers();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
/** Resets process.exitCode — pass to beforeEach/afterEach in test files. */
|
|
44
|
-
export const resetExitCode = () => {
|
|
45
|
-
process.exitCode = 0;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/** Creates a program and finds a command by name. */
|
|
49
|
-
export async function findCliCommand(name: string) {
|
|
50
|
-
const { createProgram } = await import("../index.ts");
|
|
51
|
-
const program = createProgram();
|
|
52
|
-
const cmd = program.commands.find((c) => c.name() === name);
|
|
53
|
-
return { program, cmd };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Creates a CLI program with exitOverride, runs parseAsync with the given args,
|
|
58
|
-
* and swallows any Commander exitOverride errors.
|
|
59
|
-
*/
|
|
60
|
-
export async function runCliCommand(args: string[]): Promise<void> {
|
|
61
|
-
const { createProgram } = await import("../index.ts");
|
|
62
|
-
const program = createProgram();
|
|
63
|
-
program.exitOverride();
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
await program.parseAsync(["node", "miravo", ...args]);
|
|
67
|
-
} catch {
|
|
68
|
-
// Commander may throw on exitOverride
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Saves an env var, sets it to a new value, runs the callback, then restores it.
|
|
74
|
-
*/
|
|
75
|
-
export async function withEnvVar<T>(
|
|
76
|
-
key: string,
|
|
77
|
-
value: string,
|
|
78
|
-
fn: () => T | Promise<T>,
|
|
79
|
-
): Promise<T> {
|
|
80
|
-
const prev = Bun.env[key];
|
|
81
|
-
Bun.env[key] = value;
|
|
82
|
-
try {
|
|
83
|
-
return await fn();
|
|
84
|
-
} finally {
|
|
85
|
-
if (prev === undefined) {
|
|
86
|
-
delete Bun.env[key];
|
|
87
|
-
} else {
|
|
88
|
-
Bun.env[key] = prev;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
package/src/commands/fork.ts
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
import { mkdir, rename, unlink } from "node:fs/promises";
|
|
2
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
-
import { loadTemplate, resolveModelPath } from "@miravo/core";
|
|
4
|
-
import { MiravoError } from "@miravo/shared";
|
|
5
|
-
import type { Command } from "commander";
|
|
6
|
-
import { colors } from "consola/utils";
|
|
7
|
-
import { createCatalog, resolveTemplateIdentifier } from "../lib/template-locator.ts";
|
|
8
|
-
import { collectReferencedModels } from "../lib/template-utils.ts";
|
|
9
|
-
import { configLog, handleCommandError, writeOutput } from "../logger.ts";
|
|
10
|
-
|
|
11
|
-
export function registerForkCommand(program: Command): void {
|
|
12
|
-
const fork = program
|
|
13
|
-
.command("fork")
|
|
14
|
-
.description("Copy a model or template to your local registry for customization");
|
|
15
|
-
|
|
16
|
-
fork
|
|
17
|
-
.command("model <name>")
|
|
18
|
-
.description("Fork a model to ~/.miravo/registry/local/models/")
|
|
19
|
-
.option("--force", "Overwrite if already exists in local")
|
|
20
|
-
.addHelpText(
|
|
21
|
-
"after",
|
|
22
|
-
`
|
|
23
|
-
Examples:
|
|
24
|
-
$ miravo fork model centrifugal-pump Fork a model for customization
|
|
25
|
-
$ miravo fork model centrifugal-pump --force Overwrite existing fork`,
|
|
26
|
-
)
|
|
27
|
-
.action(async (name: string, options: { readonly force?: boolean }) => {
|
|
28
|
-
await forkModelAction(name, options);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
fork
|
|
32
|
-
.command("template <idNameOrPath>")
|
|
33
|
-
.description("Fork a template and its referenced models to ~/.miravo/registry/local/")
|
|
34
|
-
.option("--force", "Overwrite if already exists in local")
|
|
35
|
-
.option("--models-only", "Only fork referenced models, skip the template itself")
|
|
36
|
-
.addHelpText(
|
|
37
|
-
"after",
|
|
38
|
-
`
|
|
39
|
-
Examples:
|
|
40
|
-
$ miravo fork template water-treatment Fork template + referenced models
|
|
41
|
-
$ miravo fork template water-treatment --models-only Fork only models`,
|
|
42
|
-
)
|
|
43
|
-
.action(
|
|
44
|
-
async (
|
|
45
|
-
idNameOrPath: string,
|
|
46
|
-
options: { readonly force?: boolean; readonly modelsOnly?: boolean },
|
|
47
|
-
) => {
|
|
48
|
-
await forkTemplateAction(idNameOrPath, options);
|
|
49
|
-
},
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// Fork model
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
async function forkModelAction(name: string, options: { readonly force?: boolean }): Promise<void> {
|
|
58
|
-
try {
|
|
59
|
-
const catalog = createCatalog();
|
|
60
|
-
const resolvedPath = await catalog.resolveModel(name);
|
|
61
|
-
const localModelsDir = join(catalog.getLocalRegistryDir(), "models");
|
|
62
|
-
await mkdir(localModelsDir, { recursive: true });
|
|
63
|
-
|
|
64
|
-
const targetPath = join(localModelsDir, basename(resolvedPath));
|
|
65
|
-
const copied = await atomicCopyFile({
|
|
66
|
-
sourcePath: resolvedPath,
|
|
67
|
-
targetPath,
|
|
68
|
-
label: `model "${name}"`,
|
|
69
|
-
force: options.force ?? false,
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
writeOutput();
|
|
73
|
-
if (copied) {
|
|
74
|
-
writeOutput(` ${colors.bold("Forked to local:")}`);
|
|
75
|
-
writeOutput(` models/${basename(resolvedPath)}`);
|
|
76
|
-
}
|
|
77
|
-
writeOutput();
|
|
78
|
-
} catch (error) {
|
|
79
|
-
handleCommandError(error);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// Fork template
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
async function forkTemplateAction(
|
|
88
|
-
idNameOrPath: string,
|
|
89
|
-
options: { readonly force?: boolean; readonly modelsOnly?: boolean },
|
|
90
|
-
): Promise<void> {
|
|
91
|
-
try {
|
|
92
|
-
const catalog = createCatalog();
|
|
93
|
-
const resolvedPath = await resolveTemplateIdentifier(idNameOrPath, catalog);
|
|
94
|
-
|
|
95
|
-
const localDir = catalog.getLocalRegistryDir();
|
|
96
|
-
const localModelsDir = join(localDir, "models");
|
|
97
|
-
const localTemplatesDir = join(localDir, "templates");
|
|
98
|
-
await mkdir(localModelsDir, { recursive: true });
|
|
99
|
-
await mkdir(localTemplatesDir, { recursive: true });
|
|
100
|
-
|
|
101
|
-
const forked: string[] = [];
|
|
102
|
-
const skipped: string[] = [];
|
|
103
|
-
|
|
104
|
-
// Fork the template itself (unless --models-only)
|
|
105
|
-
if (!options.modelsOnly) {
|
|
106
|
-
const targetPath = join(localTemplatesDir, basename(resolvedPath));
|
|
107
|
-
const copied = await atomicCopyFile({
|
|
108
|
-
sourcePath: resolvedPath,
|
|
109
|
-
targetPath,
|
|
110
|
-
label: `template "${idNameOrPath}"`,
|
|
111
|
-
force: options.force ?? false,
|
|
112
|
-
});
|
|
113
|
-
if (copied) {
|
|
114
|
-
forked.push(`templates/${basename(resolvedPath)}`);
|
|
115
|
-
} else {
|
|
116
|
-
skipped.push(`templates/${basename(resolvedPath)}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Fork referenced models
|
|
121
|
-
const template = await loadTemplate(resolvedPath);
|
|
122
|
-
const inlineModels = new Set((template.models ?? []).map((m) => m.model.name));
|
|
123
|
-
const referencedModels = collectReferencedModels(template);
|
|
124
|
-
const searchPaths = catalog.getTemplateValidationSearchPaths(resolvedPath);
|
|
125
|
-
|
|
126
|
-
for (const modelName of referencedModels) {
|
|
127
|
-
if (inlineModels.has(modelName)) continue;
|
|
128
|
-
|
|
129
|
-
let sourceModelPath: string;
|
|
130
|
-
try {
|
|
131
|
-
sourceModelPath = await resolveModelPath(modelName, searchPaths);
|
|
132
|
-
} catch {
|
|
133
|
-
configLog.warn(`Skipped model "${modelName}": not found.`);
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const targetModelPath = join(localModelsDir, basename(sourceModelPath));
|
|
138
|
-
const copied = await atomicCopyFile({
|
|
139
|
-
sourcePath: sourceModelPath,
|
|
140
|
-
targetPath: targetModelPath,
|
|
141
|
-
label: `model "${modelName}"`,
|
|
142
|
-
force: options.force ?? false,
|
|
143
|
-
});
|
|
144
|
-
if (copied) {
|
|
145
|
-
forked.push(`models/${basename(sourceModelPath)}`);
|
|
146
|
-
} else {
|
|
147
|
-
skipped.push(`models/${basename(sourceModelPath)}`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
writeOutput();
|
|
152
|
-
if (forked.length > 0) {
|
|
153
|
-
writeOutput(` ${colors.bold("Forked to local:")}`);
|
|
154
|
-
for (const f of forked) {
|
|
155
|
-
writeOutput(` ${f}`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
if (skipped.length > 0) {
|
|
159
|
-
for (const s of skipped) {
|
|
160
|
-
writeOutput(` ${s} ${colors.dim("(skipped — already in local)")}`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
if (forked.length === 0 && skipped.length === 0) {
|
|
164
|
-
writeOutput(" Nothing to fork.");
|
|
165
|
-
}
|
|
166
|
-
writeOutput();
|
|
167
|
-
} catch (error) {
|
|
168
|
-
handleCommandError(error);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
|
-
// Helpers
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Copy a file using stage-to-temp-and-rename for atomic writes.
|
|
178
|
-
* Returns true if the file was copied, false if skipped (already exists and no --force).
|
|
179
|
-
*/
|
|
180
|
-
async function atomicCopyFile(options: {
|
|
181
|
-
readonly sourcePath: string;
|
|
182
|
-
readonly targetPath: string;
|
|
183
|
-
readonly label: string;
|
|
184
|
-
readonly force: boolean;
|
|
185
|
-
}): Promise<boolean> {
|
|
186
|
-
const { sourcePath, targetPath, label, force } = options;
|
|
187
|
-
|
|
188
|
-
if (resolve(sourcePath) === resolve(targetPath)) {
|
|
189
|
-
configLog.warn(`Skipped ${label}: source and destination are the same.`);
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const targetExists = await Bun.file(targetPath).exists();
|
|
194
|
-
if (targetExists && !force) {
|
|
195
|
-
configLog.info(`Skipped ${label}: already exists in local. Use --force to overwrite.`);
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const contents = await Bun.file(sourcePath).text();
|
|
200
|
-
const tmpPath = join(dirname(targetPath), `.${basename(targetPath)}.tmp`);
|
|
201
|
-
|
|
202
|
-
try {
|
|
203
|
-
await Bun.write(tmpPath, contents);
|
|
204
|
-
await rename(tmpPath, targetPath);
|
|
205
|
-
} catch {
|
|
206
|
-
// Clean up temp file on failure
|
|
207
|
-
try {
|
|
208
|
-
await unlink(tmpPath);
|
|
209
|
-
} catch {
|
|
210
|
-
// Ignore cleanup errors
|
|
211
|
-
}
|
|
212
|
-
throw new MiravoError(
|
|
213
|
-
`Failed to write ${label} to "${targetPath}".`,
|
|
214
|
-
`Ensure "${dirname(targetPath)}" is writable by the current user.`,
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return true;
|
|
219
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { findCliCommand, resetExitCode } from "./cli-test-setup.ts";
|
|
3
|
-
|
|
4
|
-
beforeEach(resetExitCode);
|
|
5
|
-
afterEach(resetExitCode);
|
|
6
|
-
|
|
7
|
-
describe("miravo inject", () => {
|
|
8
|
-
test("sets exit code 1 with invalid admin URL", async () => {
|
|
9
|
-
const { program } = await findCliCommand("inject");
|
|
10
|
-
program.exitOverride();
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
await program.parseAsync([
|
|
14
|
-
"node",
|
|
15
|
-
"miravo",
|
|
16
|
-
"inject",
|
|
17
|
-
"pump-001",
|
|
18
|
-
"cavitation",
|
|
19
|
-
"--admin-url",
|
|
20
|
-
"not-a-url",
|
|
21
|
-
]);
|
|
22
|
-
} catch {
|
|
23
|
-
// Expected
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
expect(process.exitCode).toBe(1);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("requires terminal for interactive mode", async () => {
|
|
30
|
-
// Save originals
|
|
31
|
-
const origStdoutTTY = process.stdout.isTTY;
|
|
32
|
-
const origStdinTTY = process.stdin.isTTY;
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
// Simulate non-TTY
|
|
36
|
-
Object.defineProperty(process.stdout, "isTTY", { value: false, configurable: true });
|
|
37
|
-
Object.defineProperty(process.stdin, "isTTY", { value: false, configurable: true });
|
|
38
|
-
|
|
39
|
-
// Import inject action indirectly via program
|
|
40
|
-
const { program } = await findCliCommand("inject");
|
|
41
|
-
program.exitOverride();
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
// No positional args → interactive mode needed → should fail in non-TTY
|
|
45
|
-
await program.parseAsync(["node", "miravo", "inject"]);
|
|
46
|
-
} catch {
|
|
47
|
-
// Expected
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
expect(process.exitCode).toBe(1);
|
|
51
|
-
} finally {
|
|
52
|
-
// Restore
|
|
53
|
-
Object.defineProperty(process.stdout, "isTTY", { value: origStdoutTTY, configurable: true });
|
|
54
|
-
Object.defineProperty(process.stdin, "isTTY", { value: origStdinTTY, configurable: true });
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
});
|
package/src/commands/inject.ts
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { confirm, intro, isCancel, outro, select } from "@clack/prompts";
|
|
2
|
-
import type { EngineCommand } from "@miravo/shared";
|
|
3
|
-
import { CONTROL_CMD_TIMEOUT_MS, MiravoError } from "@miravo/shared";
|
|
4
|
-
import type { Command } from "commander";
|
|
5
|
-
import { executeAdminCommand, getAdminState, resolveAdminUrl } from "../lib/admin-client.ts";
|
|
6
|
-
import { parseTimeoutSeconds } from "../lib/parse.ts";
|
|
7
|
-
import { faultLog, handleCommandError } from "../logger.ts";
|
|
8
|
-
|
|
9
|
-
interface InjectOptions {
|
|
10
|
-
readonly clear?: boolean;
|
|
11
|
-
readonly json?: boolean;
|
|
12
|
-
readonly name?: string;
|
|
13
|
-
readonly adminUrl?: string;
|
|
14
|
-
readonly timeout?: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Register the `miravo inject` command with the root Commander program.
|
|
19
|
-
* Triggers or clears a fault on a running twin instance via the admin HTTP server.
|
|
20
|
-
*/
|
|
21
|
-
export function registerInjectCommand(program: Command): void {
|
|
22
|
-
program
|
|
23
|
-
.command("inject [instanceId] [fault]")
|
|
24
|
-
.description("Trigger or clear a fault on a running instance")
|
|
25
|
-
.option("--clear", "Clear the fault instead of triggering it")
|
|
26
|
-
.option("--json", "Machine-readable JSON output")
|
|
27
|
-
.option("--name <name>", "Target simulation name")
|
|
28
|
-
.option("--admin-url <url>", "Miravo admin server URL (env: MIRAVO_ADMIN_URL)")
|
|
29
|
-
.option("--timeout <seconds>", "Timeout in seconds (default: 5)", parseTimeoutSeconds)
|
|
30
|
-
.addHelpText(
|
|
31
|
-
"after",
|
|
32
|
-
`
|
|
33
|
-
Examples:
|
|
34
|
-
$ miravo inject Interactive mode (pick instance + fault)
|
|
35
|
-
$ miravo inject pump-001 cavitation Trigger fault directly
|
|
36
|
-
$ miravo inject pump-001 cavitation --clear Clear an active fault`,
|
|
37
|
-
)
|
|
38
|
-
.action(
|
|
39
|
-
async (instanceId: string | undefined, fault: string | undefined, options: InjectOptions) => {
|
|
40
|
-
await injectAction(instanceId, fault, options);
|
|
41
|
-
},
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
// Action
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
|
|
49
|
-
async function injectAction(
|
|
50
|
-
instanceId: string | undefined,
|
|
51
|
-
fault: string | undefined,
|
|
52
|
-
options: InjectOptions,
|
|
53
|
-
): Promise<void> {
|
|
54
|
-
// Fail fast before broker connect when interactive prompts would be required.
|
|
55
|
-
if ((!instanceId || !fault) && (!process.stdout.isTTY || !process.stdin.isTTY)) {
|
|
56
|
-
faultLog.error("Interactive mode requires a terminal. Usage: miravo inject <instance> <fault>");
|
|
57
|
-
process.exitCode = 1;
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const timeoutMs = options.timeout ?? CONTROL_CMD_TIMEOUT_MS;
|
|
63
|
-
const adminUrl = resolveAdminUrl(options.adminUrl);
|
|
64
|
-
const stateResult = await getTargetState(adminUrl, timeoutMs, options.name);
|
|
65
|
-
|
|
66
|
-
let targetInstance = instanceId;
|
|
67
|
-
let targetFault = fault;
|
|
68
|
-
|
|
69
|
-
// Interactive mode: pick instance and fault via prompts
|
|
70
|
-
if (!targetInstance || !targetFault) {
|
|
71
|
-
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
72
|
-
faultLog.error(
|
|
73
|
-
"Interactive mode requires a terminal. Usage: miravo inject <instance> <fault>",
|
|
74
|
-
);
|
|
75
|
-
process.exitCode = 1;
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const picked = await pickInstanceAndFault(stateResult, options.clear ?? false);
|
|
80
|
-
if (!picked) return;
|
|
81
|
-
|
|
82
|
-
targetInstance = picked.instanceId;
|
|
83
|
-
targetFault = picked.fault;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const command: EngineCommand = options.clear
|
|
87
|
-
? { type: "clearFault", instanceId: targetInstance, fault: targetFault }
|
|
88
|
-
: { type: "triggerFault", instanceId: targetInstance, fault: targetFault };
|
|
89
|
-
|
|
90
|
-
const result = await executeAdminCommand(adminUrl, command, timeoutMs);
|
|
91
|
-
|
|
92
|
-
if (options.json) {
|
|
93
|
-
console.log(JSON.stringify(result, null, 2));
|
|
94
|
-
} else if (result.success) {
|
|
95
|
-
faultLog.success(result.message);
|
|
96
|
-
} else {
|
|
97
|
-
faultLog.error(result.message);
|
|
98
|
-
if (result.error) {
|
|
99
|
-
faultLog.error(`Fix: ${result.error}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (!result.success) process.exitCode = 1;
|
|
103
|
-
} catch (error) {
|
|
104
|
-
handleCommandError(error, faultLog);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// ---------------------------------------------------------------------------
|
|
109
|
-
// Read state snapshot from admin server
|
|
110
|
-
// ---------------------------------------------------------------------------
|
|
111
|
-
|
|
112
|
-
interface StateSnapshot {
|
|
113
|
-
readonly models: ReadonlyArray<{
|
|
114
|
-
readonly name: string;
|
|
115
|
-
readonly faultNames: readonly string[];
|
|
116
|
-
}>;
|
|
117
|
-
readonly instances: ReadonlyArray<{
|
|
118
|
-
readonly id: string;
|
|
119
|
-
readonly model: string;
|
|
120
|
-
readonly lifecycleStage: string;
|
|
121
|
-
readonly activeFaults: readonly string[];
|
|
122
|
-
}>;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async function getTargetState(
|
|
126
|
-
adminUrl: string,
|
|
127
|
-
timeoutMs: number,
|
|
128
|
-
expectedName?: string,
|
|
129
|
-
): Promise<StateSnapshot> {
|
|
130
|
-
const state = await getAdminState(adminUrl, timeoutMs);
|
|
131
|
-
if (expectedName && state.simulationName !== expectedName) {
|
|
132
|
-
throw new MiravoError(
|
|
133
|
-
`Connected to simulation "${state.simulationName}" instead of "${expectedName}".`,
|
|
134
|
-
"Pass --admin-url for the correct Miravo process, or omit --name to target the connected simulation.",
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
return state as StateSnapshot;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
// Interactive: pick instance and fault
|
|
142
|
-
// ---------------------------------------------------------------------------
|
|
143
|
-
|
|
144
|
-
async function pickInstanceAndFault(
|
|
145
|
-
state: StateSnapshot,
|
|
146
|
-
clearing: boolean,
|
|
147
|
-
): Promise<{ instanceId: string; fault: string } | null> {
|
|
148
|
-
if (state.instances.length === 0) {
|
|
149
|
-
faultLog.error("No instances running. Spawn instances first with 'miravo start'.");
|
|
150
|
-
process.exitCode = 1;
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
intro(clearing ? "Clear a fault" : "Inject a fault");
|
|
155
|
-
|
|
156
|
-
// Pick instance
|
|
157
|
-
const instanceChoice = await select({
|
|
158
|
-
message: "Which instance?",
|
|
159
|
-
options: state.instances.map((inst) => ({
|
|
160
|
-
value: inst.id,
|
|
161
|
-
label: inst.id,
|
|
162
|
-
hint: `${inst.model} · ${inst.lifecycleStage}${inst.activeFaults.length > 0 ? ` · faults: ${inst.activeFaults.join(", ")}` : ""}`,
|
|
163
|
-
})),
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
if (isCancel(instanceChoice)) {
|
|
167
|
-
outro("Cancelled.");
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const selectedInstance = state.instances.find((i) => i.id === instanceChoice);
|
|
172
|
-
if (!selectedInstance) return null;
|
|
173
|
-
|
|
174
|
-
// Get available faults for this instance's model
|
|
175
|
-
const model = state.models.find((m) => m.name === selectedInstance.model);
|
|
176
|
-
const availableFaults = clearing ? selectedInstance.activeFaults : (model?.faultNames ?? []);
|
|
177
|
-
|
|
178
|
-
if (availableFaults.length === 0) {
|
|
179
|
-
const reason = clearing
|
|
180
|
-
? `No active faults on ${selectedInstance.id}.`
|
|
181
|
-
: `Model "${selectedInstance.model}" has no defined faults.`;
|
|
182
|
-
outro(reason);
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Pick fault
|
|
187
|
-
const faultChoice = await select({
|
|
188
|
-
message: clearing ? "Which fault to clear?" : "Which fault to trigger?",
|
|
189
|
-
options: availableFaults.map((f) => ({
|
|
190
|
-
value: f,
|
|
191
|
-
label: f,
|
|
192
|
-
})),
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
if (isCancel(faultChoice)) {
|
|
196
|
-
outro("Cancelled.");
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Confirm
|
|
201
|
-
const action = clearing ? "Clear" : "Trigger";
|
|
202
|
-
const confirmed = await confirm({
|
|
203
|
-
message: `${action} ${faultChoice as string} on ${instanceChoice as string}?`,
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
if (isCancel(confirmed) || !confirmed) {
|
|
207
|
-
outro("Cancelled.");
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return { instanceId: instanceChoice as string, fault: faultChoice as string };
|
|
212
|
-
}
|