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.
@@ -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 { DigitalTwinModel, GeneratorConfig } from "@miravo/shared";
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 digital twin models");
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
- .option(
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
- .option(
36
- "--content-dir <path>",
37
- "User content root with /templates and /models (env: MIRAVO_CONTENT_DIR)",
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(options.contentDir);
42
+ const catalog = createCatalog();
56
43
  const allModels = await catalog.listModels();
57
44
 
58
45
  const builtinModels: DiscoveredModelWithSource[] = [];
59
- const contentDirModels: DiscoveredModelWithSource[] = [];
60
- const localModels: DiscoveredModelWithSource[] = [];
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 "content-dir":
68
- contentDirModels.push(model);
54
+ case "local":
55
+ localRegModels.push(model);
69
56
  break;
70
57
  case "cwd":
71
- localModels.push(model);
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(localModels.map((m) => m.path));
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
- localModels.push({ ...extra, source: { kind: "cwd", path: resolvedDir } });
79
+ cwdModels.push({ ...extra, source: { kind: "cwd", path: resolvedDir } });
93
80
  }
94
81
  }
95
82
  }
96
83
 
97
- if (builtinModels.length === 0 && localModels.length === 0 && contentDirModels.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
- sensors: colors.dim("Sensors"),
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
- sensors: String(m.sensorCount),
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("Content Directory", contentDirModels);
131
- renderSection("Local", localModels);
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; readonly contentDir?: string },
133
+ options: { readonly raw?: boolean },
145
134
  ): Promise<void> {
146
135
  try {
147
- const catalog = createCatalog(options.contentDir);
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: DigitalTwinModel,
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
- // Sensors
254
- lines.push(` ${colors.bold(`Sensors (${model.sensors.length}):`)}`);
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
- model.sensors.map((s) => ({
263
- name: s.name,
264
- unit: s.unit,
265
- generator: formatGeneratorSummary(s.generator),
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, "..", "..", "..", "..", "templates");
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(builtinTemplatesDir, "stress-10k-uns.miravo.yaml"),
18
- resolve(builtinTemplatesDir, "stress-50k-uns.miravo.yaml"),
19
- resolve(builtinTemplatesDir, "stress-100k-uns.miravo.yaml"),
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 = `name: test-sensor
29
- sensors:
30
- - name: temperature
31
- unit: celsius
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 MIRAVO_HEALTH_PORT env var sets exit code 1", async () => {
154
- await withEnvVar("MIRAVO_HEALTH_PORT", "NaN", async () => {
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-uns", "Stress Benchmark 50K UNS"),
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-uns", "Stress Benchmark 10K UNS"),
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 UNS",
290
- " \u2514\u2500 Stress Benchmark 50K UNS",
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-uns");
264
+ expect(choices[4]?.hint).toContain("stress-10k");
296
265
  });
297
266
  });