miravo 0.1.4 → 0.2.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/README.md +38 -32
- package/package.json +11 -5
- package/src/__tests__/integration/executable-methods-runner.ts +253 -0
- package/src/__tests__/integration/executable-methods.integration.ts +1688 -0
- package/src/commands/fork.ts +205 -0
- package/src/commands/inject.test.ts +6 -9
- package/src/commands/inject.ts +48 -158
- package/src/commands/models.test.ts +32 -8
- package/src/commands/models.ts +71 -45
- package/src/commands/start.test.ts +32 -63
- package/src/commands/start.ts +333 -193
- package/src/commands/status.test.ts +3 -22
- package/src/commands/status.ts +114 -279
- package/src/commands/templates.test.ts +4 -50
- package/src/commands/templates.ts +12 -191
- package/src/index.ts +42 -8
- package/src/lib/admin-client.ts +214 -0
- package/src/lib/admin-server.test.ts +276 -0
- package/src/lib/admin-server.ts +307 -0
- package/src/lib/shutdown.test.ts +221 -93
- package/src/lib/shutdown.ts +478 -98
- package/src/lib/template-category.test.ts +14 -7
- package/src/lib/template-category.ts +6 -1
- package/src/lib/template-locator.ts +5 -8
- package/src/lib/template-utils.ts +14 -0
- package/src/logger.test.ts +2 -2
- package/src/logger.ts +68 -15
- package/src/output.ts +41 -1
- package/src/lib/health-server.test.ts +0 -161
- package/src/lib/health-server.ts +0 -154
package/src/commands/models.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { relative, resolve } from "node:path";
|
|
3
3
|
import { type DiscoveredModelWithSource, loadTwinModel } from "@miravo/core";
|
|
4
|
-
import type {
|
|
4
|
+
import type { GeneratorConfig, TwinModel } from "@miravo/shared";
|
|
5
5
|
import { TWIN_FILE_EXTENSION } from "@miravo/shared";
|
|
6
6
|
import type { Command } from "commander";
|
|
7
7
|
import { colors } from "consola/utils";
|
|
@@ -14,17 +14,13 @@ import { formatTable } from "../output.ts";
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
|
|
16
16
|
export function registerModelsCommand(program: Command): void {
|
|
17
|
-
const models = program.command("models").description("List and inspect
|
|
17
|
+
const models = program.command("models").description("List and inspect asset models");
|
|
18
18
|
|
|
19
19
|
models
|
|
20
20
|
.command("list")
|
|
21
21
|
.description("Discover and list available models")
|
|
22
22
|
.option("--dir <path>", "Additional directory to search for models")
|
|
23
|
-
.
|
|
24
|
-
"--content-dir <path>",
|
|
25
|
-
"User content root with /templates and /models (env: MIRAVO_CONTENT_DIR)",
|
|
26
|
-
)
|
|
27
|
-
.action(async (options: { readonly dir?: string; readonly contentDir?: string }) => {
|
|
23
|
+
.action(async (options: { readonly dir?: string }) => {
|
|
28
24
|
await modelsListAction(options);
|
|
29
25
|
});
|
|
30
26
|
|
|
@@ -32,43 +28,34 @@ export function registerModelsCommand(program: Command): void {
|
|
|
32
28
|
.command("inspect <name>")
|
|
33
29
|
.description("Show detailed information about a model")
|
|
34
30
|
.option("--raw", "Dump the raw YAML file to stdout")
|
|
35
|
-
.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
)
|
|
39
|
-
.action(
|
|
40
|
-
async (name: string, options: { readonly raw?: boolean; readonly contentDir?: string }) => {
|
|
41
|
-
await modelsInspectAction(name, options);
|
|
42
|
-
},
|
|
43
|
-
);
|
|
31
|
+
.action(async (name: string, options: { readonly raw?: boolean }) => {
|
|
32
|
+
await modelsInspectAction(name, options);
|
|
33
|
+
});
|
|
44
34
|
}
|
|
45
35
|
|
|
46
36
|
// ---------------------------------------------------------------------------
|
|
47
37
|
// models list
|
|
48
38
|
// ---------------------------------------------------------------------------
|
|
49
39
|
|
|
50
|
-
async function modelsListAction(options: {
|
|
51
|
-
readonly dir?: string;
|
|
52
|
-
readonly contentDir?: string;
|
|
53
|
-
}): Promise<void> {
|
|
40
|
+
async function modelsListAction(options: { readonly dir?: string }): Promise<void> {
|
|
54
41
|
try {
|
|
55
|
-
const catalog = createCatalog(
|
|
42
|
+
const catalog = createCatalog();
|
|
56
43
|
const allModels = await catalog.listModels();
|
|
57
44
|
|
|
58
45
|
const builtinModels: DiscoveredModelWithSource[] = [];
|
|
59
|
-
const
|
|
60
|
-
const
|
|
46
|
+
const localRegModels: DiscoveredModelWithSource[] = [];
|
|
47
|
+
const cwdModels: DiscoveredModelWithSource[] = [];
|
|
61
48
|
|
|
62
49
|
for (const model of allModels) {
|
|
63
50
|
switch (model.source.kind) {
|
|
64
51
|
case "builtin":
|
|
65
52
|
builtinModels.push(model);
|
|
66
53
|
break;
|
|
67
|
-
case "
|
|
68
|
-
|
|
54
|
+
case "local":
|
|
55
|
+
localRegModels.push(model);
|
|
69
56
|
break;
|
|
70
57
|
case "cwd":
|
|
71
|
-
|
|
58
|
+
cwdModels.push(model);
|
|
72
59
|
break;
|
|
73
60
|
default: {
|
|
74
61
|
const _exhaustive: never = model.source.kind;
|
|
@@ -85,23 +72,24 @@ async function modelsListAction(options: {
|
|
|
85
72
|
}
|
|
86
73
|
const { discoverModels } = await import("@miravo/core");
|
|
87
74
|
const extraModels = await discoverModels(resolvedDir);
|
|
88
|
-
const existingPaths = new Set(
|
|
75
|
+
const existingPaths = new Set(cwdModels.map((m) => m.path));
|
|
89
76
|
for (const extra of extraModels) {
|
|
90
77
|
if (!existingPaths.has(extra.path)) {
|
|
91
78
|
existingPaths.add(extra.path);
|
|
92
|
-
|
|
79
|
+
cwdModels.push({ ...extra, source: { kind: "cwd", path: resolvedDir } });
|
|
93
80
|
}
|
|
94
81
|
}
|
|
95
82
|
}
|
|
96
83
|
|
|
97
|
-
if (builtinModels.length === 0 &&
|
|
84
|
+
if (builtinModels.length === 0 && cwdModels.length === 0 && localRegModels.length === 0) {
|
|
98
85
|
configLog.info("No models found.");
|
|
99
86
|
return;
|
|
100
87
|
}
|
|
101
88
|
|
|
102
89
|
const headers = {
|
|
103
90
|
name: colors.dim("Name"),
|
|
104
|
-
|
|
91
|
+
members: colors.dim("Members"),
|
|
92
|
+
methods: colors.dim("Methods"),
|
|
105
93
|
faults: colors.dim("Faults"),
|
|
106
94
|
lifecycle: colors.dim("Lifecycle"),
|
|
107
95
|
source: colors.dim("Source"),
|
|
@@ -116,7 +104,8 @@ async function modelsListAction(options: {
|
|
|
116
104
|
headers,
|
|
117
105
|
models.map((m) => ({
|
|
118
106
|
name: colors.cyan(m.name),
|
|
119
|
-
|
|
107
|
+
members: String(m.memberCount),
|
|
108
|
+
methods: String(m.methodCount),
|
|
120
109
|
faults: String(m.faultCount),
|
|
121
110
|
lifecycle:
|
|
122
111
|
m.lifecycleStageCount > 0 ? `${m.lifecycleStageCount} stages` : colors.dim("none"),
|
|
@@ -127,8 +116,8 @@ async function modelsListAction(options: {
|
|
|
127
116
|
};
|
|
128
117
|
|
|
129
118
|
renderSection("Built-in", builtinModels);
|
|
130
|
-
renderSection("
|
|
131
|
-
renderSection("
|
|
119
|
+
renderSection("Local", localRegModels);
|
|
120
|
+
renderSection("Project", cwdModels);
|
|
132
121
|
writeOutput();
|
|
133
122
|
} catch (error) {
|
|
134
123
|
handleCommandError(error);
|
|
@@ -141,10 +130,10 @@ async function modelsListAction(options: {
|
|
|
141
130
|
|
|
142
131
|
async function modelsInspectAction(
|
|
143
132
|
name: string,
|
|
144
|
-
options: { readonly raw?: boolean
|
|
133
|
+
options: { readonly raw?: boolean },
|
|
145
134
|
): Promise<void> {
|
|
146
135
|
try {
|
|
147
|
-
const catalog = createCatalog(
|
|
136
|
+
const catalog = createCatalog();
|
|
148
137
|
const isPath = name.includes("/") || name.endsWith(TWIN_FILE_EXTENSION);
|
|
149
138
|
const resolvedPath = isPath ? resolve(name) : await catalog.resolveModel(name);
|
|
150
139
|
|
|
@@ -204,6 +193,8 @@ export function formatGeneratorSummary(generator: GeneratorConfig): string {
|
|
|
204
193
|
return `hysteresis-relay (on: ${generator.on_threshold}, off: ${generator.off_threshold})`;
|
|
205
194
|
case "weighted-sum":
|
|
206
195
|
return `weighted-sum (${generator.terms.length} terms)`;
|
|
196
|
+
case "gate":
|
|
197
|
+
return `gate (enabled: ${generator.enabled_member})`;
|
|
207
198
|
default: {
|
|
208
199
|
const _exhaustive: never = generator;
|
|
209
200
|
return String(_exhaustive);
|
|
@@ -212,19 +203,19 @@ export function formatGeneratorSummary(generator: GeneratorConfig): string {
|
|
|
212
203
|
}
|
|
213
204
|
|
|
214
205
|
export function formatModelInspect(
|
|
215
|
-
model:
|
|
206
|
+
model: TwinModel,
|
|
216
207
|
sourcePath: string,
|
|
217
208
|
sourceLabel: string,
|
|
218
209
|
): string {
|
|
219
210
|
const lines: string[] = [];
|
|
220
211
|
|
|
221
212
|
// Header
|
|
222
|
-
if (model.description) {
|
|
213
|
+
if (model.model.description) {
|
|
223
214
|
lines.push(
|
|
224
|
-
` ${colors.cyan(colors.bold(model.name))} ${colors.dim("\u2014")} ${model.description}`,
|
|
215
|
+
` ${colors.cyan(colors.bold(model.model.name))} ${colors.dim("\u2014")} ${model.model.description}`,
|
|
225
216
|
);
|
|
226
217
|
} else {
|
|
227
|
-
lines.push(` ${colors.cyan(colors.bold(model.name))}`);
|
|
218
|
+
lines.push(` ${colors.cyan(colors.bold(model.model.name))}`);
|
|
228
219
|
}
|
|
229
220
|
lines.push("");
|
|
230
221
|
|
|
@@ -250,24 +241,59 @@ export function formatModelInspect(
|
|
|
250
241
|
lines.push("");
|
|
251
242
|
}
|
|
252
243
|
|
|
253
|
-
//
|
|
254
|
-
|
|
244
|
+
// Members
|
|
245
|
+
const memberEntries = Object.entries(model.members) as Array<
|
|
246
|
+
[string, TwinModel["members"][string]]
|
|
247
|
+
>;
|
|
248
|
+
lines.push(` ${colors.bold(`Members (${memberEntries.length}):`)}`);
|
|
255
249
|
lines.push(
|
|
256
250
|
formatTable(
|
|
257
251
|
{
|
|
258
252
|
name: colors.dim("Name"),
|
|
253
|
+
kind: colors.dim("Kind"),
|
|
254
|
+
dataType: colors.dim("Data Type"),
|
|
259
255
|
unit: colors.dim("Unit"),
|
|
260
256
|
generator: colors.dim("Generator"),
|
|
261
257
|
},
|
|
262
|
-
|
|
263
|
-
name
|
|
264
|
-
|
|
265
|
-
|
|
258
|
+
memberEntries.map(([name, member]) => ({
|
|
259
|
+
name,
|
|
260
|
+
kind: member.kind,
|
|
261
|
+
dataType: member.data_type,
|
|
262
|
+
unit: member.unit ?? "",
|
|
263
|
+
generator:
|
|
264
|
+
member.kind === "variable" && member.generator
|
|
265
|
+
? formatGeneratorSummary(member.generator)
|
|
266
|
+
: colors.dim("none"),
|
|
266
267
|
})),
|
|
267
268
|
),
|
|
268
269
|
);
|
|
269
270
|
lines.push("");
|
|
270
271
|
|
|
272
|
+
const methodEntries = Object.entries(model.methods ?? {});
|
|
273
|
+
if (methodEntries.length > 0) {
|
|
274
|
+
lines.push(` ${colors.bold(`Methods (${methodEntries.length}):`)}`);
|
|
275
|
+
lines.push(
|
|
276
|
+
formatTable(
|
|
277
|
+
{
|
|
278
|
+
name: colors.dim("Name"),
|
|
279
|
+
inputs: colors.dim("Inputs"),
|
|
280
|
+
outputs: colors.dim("Outputs"),
|
|
281
|
+
description: colors.dim("Description"),
|
|
282
|
+
},
|
|
283
|
+
methodEntries.map(([name, method]) => ({
|
|
284
|
+
name,
|
|
285
|
+
inputs: String(Object.keys(method.input_arguments ?? {}).length),
|
|
286
|
+
outputs: String(Object.keys(method.output_arguments ?? {}).length),
|
|
287
|
+
description: method.description ?? "",
|
|
288
|
+
})),
|
|
289
|
+
),
|
|
290
|
+
);
|
|
291
|
+
lines.push("");
|
|
292
|
+
} else {
|
|
293
|
+
lines.push(` ${colors.bold("Methods:")} ${colors.dim("none")}`);
|
|
294
|
+
lines.push("");
|
|
295
|
+
}
|
|
296
|
+
|
|
271
297
|
// Lifecycle
|
|
272
298
|
if (model.lifecycle && model.lifecycle.stages.length > 0) {
|
|
273
299
|
const stageNames = model.lifecycle.stages.map((s) => s.name).join(` ${colors.dim("\u2192")} `);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdir } from "node:fs/promises";
|
|
3
2
|
import { resolve } from "node:path";
|
|
4
3
|
import { createTmpFileUtils } from "@miravo/core/test-tmp-utils";
|
|
5
4
|
import type { DiscoveredTemplate, MiravoTemplate } from "@miravo/shared";
|
|
@@ -8,15 +7,27 @@ import { resetExitCode, runCliCommand, withEnvVar } from "./cli-test-setup.ts";
|
|
|
8
7
|
const { writeTmpFile, cleanupTmpDir } = createTmpFileUtils(
|
|
9
8
|
resolve(import.meta.dir, ".tmp-test-start"),
|
|
10
9
|
);
|
|
11
|
-
const builtinTemplatesDir = resolve(import.meta.dir, "..", "..", "..", "
|
|
10
|
+
const builtinTemplatesDir = resolve(import.meta.dir, "..", "..", "..", "content", "templates");
|
|
11
|
+
const stressTemplatesDir = resolve(
|
|
12
|
+
import.meta.dir,
|
|
13
|
+
"..",
|
|
14
|
+
"..",
|
|
15
|
+
"..",
|
|
16
|
+
"..",
|
|
17
|
+
"tests",
|
|
18
|
+
"fixtures",
|
|
19
|
+
"stress",
|
|
20
|
+
);
|
|
12
21
|
const BUILTIN_TEMPLATE_PATHS = [
|
|
13
22
|
resolve(builtinTemplatesDir, "smart-factory.miravo.yaml"),
|
|
14
23
|
resolve(builtinTemplatesDir, "water-treatment.miravo.yaml"),
|
|
15
24
|
resolve(builtinTemplatesDir, "commercial-building.miravo.yaml"),
|
|
16
25
|
resolve(builtinTemplatesDir, "backup-power.miravo.yaml"),
|
|
17
|
-
resolve(
|
|
18
|
-
resolve(
|
|
19
|
-
resolve(
|
|
26
|
+
resolve(stressTemplatesDir, "stress-5k.miravo.yaml"),
|
|
27
|
+
resolve(stressTemplatesDir, "stress-10k.miravo.yaml"),
|
|
28
|
+
resolve(stressTemplatesDir, "stress-50k.miravo.yaml"),
|
|
29
|
+
resolve(stressTemplatesDir, "stress-100k.miravo.yaml"),
|
|
30
|
+
resolve(stressTemplatesDir, "stress-200k.miravo.yaml"),
|
|
20
31
|
];
|
|
21
32
|
|
|
22
33
|
beforeEach(resetExitCode);
|
|
@@ -25,10 +36,15 @@ afterEach(async () => {
|
|
|
25
36
|
await cleanupTmpDir();
|
|
26
37
|
});
|
|
27
38
|
|
|
28
|
-
const MINIMAL_MODEL = `
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
const MINIMAL_MODEL = `schema_version: "2"
|
|
40
|
+
model:
|
|
41
|
+
name: test-sensor
|
|
42
|
+
members:
|
|
43
|
+
temperature:
|
|
44
|
+
kind: variable
|
|
45
|
+
data_type: Double
|
|
46
|
+
access: read
|
|
47
|
+
unit: degC
|
|
32
48
|
generator:
|
|
33
49
|
type: noise
|
|
34
50
|
center: 25
|
|
@@ -61,17 +77,6 @@ function discoveredTemplate(id: string, name: string, description?: string): Dis
|
|
|
61
77
|
};
|
|
62
78
|
}
|
|
63
79
|
|
|
64
|
-
async function setupContentDir(subdir: string, templateFileName: string) {
|
|
65
|
-
const contentRoot = resolve(import.meta.dir, ".tmp-test-start", subdir);
|
|
66
|
-
const contentModelsDir = resolve(contentRoot, "models");
|
|
67
|
-
const contentTemplatesDir = resolve(contentRoot, "templates");
|
|
68
|
-
await mkdir(contentModelsDir, { recursive: true });
|
|
69
|
-
await mkdir(contentTemplatesDir, { recursive: true });
|
|
70
|
-
await Bun.write(resolve(contentModelsDir, "test-sensor.twin.yaml"), MINIMAL_MODEL);
|
|
71
|
-
await Bun.write(resolve(contentTemplatesDir, templateFileName), MINIMAL_TEMPLATE("test-sensor"));
|
|
72
|
-
return contentRoot;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
80
|
describe("miravo start", () => {
|
|
76
81
|
// We test the internal logic by importing the createProgram and
|
|
77
82
|
// using Commander's parseAsync with exitOverride to prevent process.exit
|
|
@@ -101,42 +106,6 @@ describe("miravo start", () => {
|
|
|
101
106
|
expect(process.exitCode).toBe(0);
|
|
102
107
|
});
|
|
103
108
|
|
|
104
|
-
test("--template resolves by ID from --content-dir templates", async () => {
|
|
105
|
-
const contentRoot = await setupContentDir(
|
|
106
|
-
"content-id-resolution",
|
|
107
|
-
"factory-template.miravo.yaml",
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
await runCliCommand([
|
|
111
|
-
"start",
|
|
112
|
-
"--template",
|
|
113
|
-
"factory-template",
|
|
114
|
-
"--content-dir",
|
|
115
|
-
contentRoot,
|
|
116
|
-
"--name",
|
|
117
|
-
"test-sim",
|
|
118
|
-
"--dry-run",
|
|
119
|
-
]);
|
|
120
|
-
|
|
121
|
-
expect(process.exitCode).toBe(0);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("--template prefers user content over built-in template IDs", async () => {
|
|
125
|
-
const contentRoot = await setupContentDir("content-precedence", "smart-factory.miravo.yaml");
|
|
126
|
-
|
|
127
|
-
await runCliCommand([
|
|
128
|
-
"start",
|
|
129
|
-
"--template",
|
|
130
|
-
"smart-factory",
|
|
131
|
-
"--content-dir",
|
|
132
|
-
contentRoot,
|
|
133
|
-
"--dry-run",
|
|
134
|
-
]);
|
|
135
|
-
|
|
136
|
-
// User template lacks namespace; if built-in won precedence this would be 0.
|
|
137
|
-
expect(process.exitCode).toBe(1);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
109
|
test("invalid template YAML sets exit code 1", async () => {
|
|
141
110
|
const templatePath = await writeTmpFile("bad.miravo.yaml", "not: valid: yaml: [");
|
|
142
111
|
await runCliCommand(["start", "--template", templatePath]);
|
|
@@ -150,8 +119,8 @@ describe("miravo start", () => {
|
|
|
150
119
|
});
|
|
151
120
|
});
|
|
152
121
|
|
|
153
|
-
test("invalid
|
|
154
|
-
await withEnvVar("
|
|
122
|
+
test("invalid MIRAVO_ADMIN_PORT env var sets exit code 1", async () => {
|
|
123
|
+
await withEnvVar("MIRAVO_ADMIN_PORT", "NaN", async () => {
|
|
155
124
|
await runCliCommand(["start", "--dry-run"]);
|
|
156
125
|
expect(process.exitCode).toBe(1);
|
|
157
126
|
});
|
|
@@ -271,14 +240,14 @@ describe("template picker choices", () => {
|
|
|
271
240
|
test("buildBuiltinTemplateChoices renders tree labels directly in select options", async () => {
|
|
272
241
|
const { buildBuiltinTemplateChoices } = await import("./start.ts");
|
|
273
242
|
const choices = buildBuiltinTemplateChoices([
|
|
274
|
-
discoveredTemplate("stress-50k
|
|
243
|
+
discoveredTemplate("stress-50k", "Stress Benchmark 50K"),
|
|
275
244
|
discoveredTemplate("water-treatment", "Water Treatment Plant"),
|
|
276
245
|
discoveredTemplate(
|
|
277
246
|
"smart-factory",
|
|
278
247
|
"Smart Factory Production Line",
|
|
279
248
|
"Discrete manufacturing line",
|
|
280
249
|
),
|
|
281
|
-
discoveredTemplate("stress-10k
|
|
250
|
+
discoveredTemplate("stress-10k", "Stress Benchmark 10K"),
|
|
282
251
|
]);
|
|
283
252
|
|
|
284
253
|
expect(choices.map((choice) => choice.label)).toEqual([
|
|
@@ -286,12 +255,12 @@ describe("template picker choices", () => {
|
|
|
286
255
|
" \u251C\u2500 Smart Factory Production Line",
|
|
287
256
|
" \u2514\u2500 Water Treatment Plant",
|
|
288
257
|
"Stress & Benchmark",
|
|
289
|
-
" \u251C\u2500 Stress Benchmark 10K
|
|
290
|
-
" \u2514\u2500 Stress Benchmark 50K
|
|
258
|
+
" \u251C\u2500 Stress Benchmark 10K",
|
|
259
|
+
" \u2514\u2500 Stress Benchmark 50K",
|
|
291
260
|
]);
|
|
292
261
|
expect(choices[0]?.disabled).toBe(true);
|
|
293
262
|
expect(choices[1]?.disabled).toBeUndefined();
|
|
294
263
|
expect(choices[1]?.hint).toContain("Discrete manufacturing line");
|
|
295
|
-
expect(choices[4]?.hint).toContain("stress-10k
|
|
264
|
+
expect(choices[4]?.hint).toContain("stress-10k");
|
|
296
265
|
});
|
|
297
266
|
});
|