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,25 +1,15 @@
1
- import { mkdir } from "node:fs/promises";
2
- import { basename, join, relative, resolve } from "node:path";
3
- import {
4
- type DiscoveredTemplateWithSource,
5
- discoverTemplates,
6
- loadTemplate,
7
- resolveModelPath,
8
- } from "@miravo/core";
9
- import {
10
- type DiscoveredTemplate,
11
- MIRAVO_CONTENT_DIR_ENV,
12
- MiravoError,
13
- type MiravoTemplate,
14
- TEMPLATE_FILE_EXTENSION,
15
- } from "@miravo/shared";
1
+ import { basename, relative, resolve } from "node:path";
2
+ import { type DiscoveredTemplateWithSource, discoverTemplates, loadTemplate } from "@miravo/core";
3
+ import { type DiscoveredTemplate, MiravoError, TEMPLATE_FILE_EXTENSION } from "@miravo/shared";
16
4
  import type { Command } from "commander";
17
5
  import { colors } from "consola/utils";
18
6
  import {
19
7
  groupTemplatesByCategory,
8
+ isStressBenchmarkTemplate,
20
9
  sortTemplatesByCategoryAndName,
21
10
  } from "../lib/template-category.ts";
22
11
  import { createCatalog, resolveTemplateIdentifier } from "../lib/template-locator.ts";
12
+ import { collectReferencedModels } from "../lib/template-utils.ts";
23
13
  import { configLog, handleCommandError, writeOutput } from "../logger.ts";
24
14
  import { formatKeyValue, formatTable } from "../output.ts";
25
15
 
@@ -30,11 +20,7 @@ export function registerTemplatesCommand(program: Command): void {
30
20
  .command("list")
31
21
  .description("Discover and list available templates")
32
22
  .option("--dir <path>", "Additional directory to search for templates")
33
- .option(
34
- "--content-dir <path>",
35
- "User content root with /templates and /models (env: MIRAVO_CONTENT_DIR)",
36
- )
37
- .action(async (options: { readonly dir?: string; readonly contentDir?: string }) => {
23
+ .action(async (options: { readonly dir?: string }) => {
38
24
  await templatesListAction(options);
39
25
  });
40
26
 
@@ -42,18 +28,11 @@ export function registerTemplatesCommand(program: Command): void {
42
28
  .command("inspect <idNameOrPath>")
43
29
  .description("Show detailed information about a template by id, name, or path")
44
30
  .option("--raw", "Dump the raw YAML file to stdout")
45
- .option("--copy", "Copy template + referenced built-in models to user content directory")
46
- .option(
47
- "--content-dir <path>",
48
- "User content root with /templates and /models (env: MIRAVO_CONTENT_DIR)",
49
- )
50
31
  .action(
51
32
  async (
52
33
  idNameOrPath: string,
53
34
  options: {
54
35
  readonly raw?: boolean;
55
- readonly copy?: boolean;
56
- readonly contentDir?: string;
57
36
  },
58
37
  ) => {
59
38
  await templatesInspectAction(idNameOrPath, options);
@@ -63,20 +42,19 @@ export function registerTemplatesCommand(program: Command): void {
63
42
 
64
43
  interface TemplateListOptions {
65
44
  readonly dir?: string;
66
- readonly contentDir?: string;
67
45
  }
68
46
 
69
47
  interface TemplateInspectOptions {
70
48
  readonly raw?: boolean;
71
- readonly copy?: boolean;
72
- readonly contentDir?: string;
73
49
  }
74
50
 
75
51
  async function templatesListAction(options: TemplateListOptions): Promise<void> {
76
52
  try {
77
- const catalog = createCatalog(options.contentDir);
53
+ const catalog = createCatalog();
78
54
  const all = await catalog.listTemplates();
79
- const builtinTemplates = all.filter((t) => t.source.kind === "builtin");
55
+ const builtinTemplates = all.filter(
56
+ (t) => t.source.kind === "builtin" && !isStressBenchmarkTemplate(t.id),
57
+ );
80
58
  const localTemplates: DiscoveredTemplateWithSource[] = all.filter(
81
59
  (t) => t.source.kind !== "builtin",
82
60
  );
@@ -164,17 +142,10 @@ async function templatesInspectAction(
164
142
  options: TemplateInspectOptions,
165
143
  ): Promise<void> {
166
144
  try {
167
- const catalog = createCatalog(options.contentDir);
145
+ const catalog = createCatalog();
168
146
  const resolvedPath = await resolveTemplateIdentifier(idNameOrPath, catalog);
169
147
  const sourceLabel = catalog.getSourceLabel(resolvedPath);
170
148
 
171
- if (options.raw && options.copy) {
172
- throw new MiravoError(
173
- "Cannot combine --raw with --copy.",
174
- "Run either 'miravo templates inspect <template> --raw' or 'miravo templates inspect <template> --copy'.",
175
- );
176
- }
177
-
178
149
  if (options.raw) {
179
150
  const rawText = await Bun.file(resolvedPath).text();
180
151
  process.stdout.write(rawText);
@@ -183,163 +154,13 @@ async function templatesInspectAction(
183
154
 
184
155
  const template = await loadTemplate(resolvedPath);
185
156
  writeOutput(formatTemplateInspect(template, resolvedPath, sourceLabel));
186
- if (options.copy) {
187
- const contentDir = options.contentDir ?? Bun.env[MIRAVO_CONTENT_DIR_ENV];
188
- const targetRootDir = contentDir ? resolve(contentDir) : process.cwd();
189
- await copyTemplateBundle({
190
- sourceTemplatePath: resolvedPath,
191
- template,
192
- contentRootDir: targetRootDir,
193
- builtinModelsDir: catalog.getBuiltinModelsDir(),
194
- });
195
- }
196
157
  } catch (error) {
197
158
  handleCommandError(error);
198
159
  }
199
160
  }
200
161
 
201
- interface CopyTemplateBundleOptions {
202
- readonly sourceTemplatePath: string;
203
- readonly template: MiravoTemplate;
204
- readonly contentRootDir: string;
205
- readonly builtinModelsDir: string;
206
- }
207
-
208
- interface CopyStats {
209
- created: number;
210
- skipped: number;
211
- warnings: number;
212
- }
213
-
214
- async function copyTemplateBundle(options: CopyTemplateBundleOptions): Promise<void> {
215
- const { sourceTemplatePath, template, contentRootDir, builtinModelsDir } = options;
216
- const templatesDir = join(contentRootDir, "templates");
217
- const modelsDir = join(contentRootDir, "models");
218
- try {
219
- await mkdir(templatesDir, { recursive: true });
220
- await mkdir(modelsDir, { recursive: true });
221
- } catch (error) {
222
- throw toContentWriteError(error, contentRootDir);
223
- }
224
-
225
- const stats: CopyStats = { created: 0, skipped: 0, warnings: 0 };
226
-
227
- const targetTemplatePath = resolve(templatesDir, basename(sourceTemplatePath));
228
- await copyFileWithPolicy({
229
- sourcePath: sourceTemplatePath,
230
- targetPath: targetTemplatePath,
231
- kind: "template",
232
- stats,
233
- contentRootDir,
234
- });
235
-
236
- const inlineModels = new Set((template.models ?? []).map((model) => model.name));
237
- const referencedModels = collectReferencedModels(template);
238
-
239
- for (const modelName of referencedModels) {
240
- if (inlineModels.has(modelName)) {
241
- continue;
242
- }
243
-
244
- let sourceModelPath: string;
245
- try {
246
- sourceModelPath = await resolveModelPath(modelName, [builtinModelsDir]);
247
- } catch {
248
- configLog.warn(`Skipped model "${modelName}": built-in model not found.`);
249
- stats.warnings++;
250
- continue;
251
- }
252
- if (!sourceModelPath.startsWith(builtinModelsDir)) {
253
- configLog.warn(`Skipped model "${modelName}": only built-in referenced models are copied.`);
254
- stats.warnings++;
255
- continue;
256
- }
257
-
258
- const targetModelPath = resolve(modelsDir, basename(sourceModelPath));
259
- await copyFileWithPolicy({
260
- sourcePath: sourceModelPath,
261
- targetPath: targetModelPath,
262
- kind: `model "${modelName}"`,
263
- stats,
264
- contentRootDir,
265
- });
266
- }
267
-
268
- writeOutput();
269
- writeOutput(` ${colors.bold("Copy Summary:")}`);
270
- writeOutput(` root: ${colors.cyan(contentRootDir)}`);
271
- writeOutput(
272
- ` created: ${stats.created} · skipped: ${stats.skipped} · warnings: ${stats.warnings}`,
273
- );
274
- }
275
-
276
- async function copyFileWithPolicy(options: {
277
- readonly sourcePath: string;
278
- readonly targetPath: string;
279
- readonly kind: string;
280
- readonly stats: CopyStats;
281
- readonly contentRootDir: string;
282
- }): Promise<void> {
283
- const { sourcePath, targetPath, kind, stats, contentRootDir } = options;
284
- if (resolve(sourcePath) === resolve(targetPath)) {
285
- configLog.warn(`Skipped ${kind}: source and destination are the same (${targetPath}).`);
286
- stats.skipped++;
287
- stats.warnings++;
288
- return;
289
- }
290
-
291
- const targetExists = await Bun.file(targetPath).exists();
292
- if (targetExists) {
293
- configLog.warn(`Skipped ${kind}: already exists in content directory (${targetPath}).`);
294
- stats.skipped++;
295
- return;
296
- }
297
-
298
- const contents = await Bun.file(sourcePath).text();
299
- try {
300
- await Bun.write(targetPath, contents);
301
- } catch (error) {
302
- throw toContentWriteError(error, contentRootDir);
303
- }
304
- stats.created++;
305
- }
306
-
307
- function toContentWriteError(error: unknown, contentRootDir: string): MiravoError {
308
- if (isPermissionError(error)) {
309
- return new MiravoError(
310
- `Cannot write to content directory "${contentRootDir}".`,
311
- `Ensure "${contentRootDir}" is writable by the current user/container (for example: chmod/chown the mount, or run compose with UID/GID mapping).`,
312
- );
313
- }
314
-
315
- const message = error instanceof Error ? error.message : String(error);
316
- return new MiravoError(
317
- `Failed to copy content into "${contentRootDir}": ${message}`,
318
- "Ensure the destination path exists and is writable, then retry with --copy.",
319
- );
320
- }
321
-
322
- function isPermissionError(error: unknown): boolean {
323
- if (!error || typeof error !== "object") return false;
324
- if (!("code" in error)) return false;
325
- const code = (error as { code?: unknown }).code;
326
- return code === "EACCES" || code === "EPERM";
327
- }
328
-
329
- function collectReferencedModels(template: MiravoTemplate): Set<string> {
330
- const referencedModels = new Set<string>();
331
- for (const area of template.areas) {
332
- for (const line of area.lines) {
333
- for (const twin of line.twins) {
334
- referencedModels.add(twin.model);
335
- }
336
- }
337
- }
338
- return referencedModels;
339
- }
340
-
341
162
  export function formatTemplateInspect(
342
- template: MiravoTemplate,
163
+ template: import("@miravo/shared").MiravoTemplate,
343
164
  sourcePath: string,
344
165
  sourceLabel: string,
345
166
  ): string {
package/src/index.ts CHANGED
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env bun
2
2
  import { MIRAVO_VERSION } from "@miravo/shared";
3
3
  import { Command, InvalidOptionArgumentError } from "commander";
4
+ import { registerForkCommand } from "./commands/fork.ts";
4
5
  import { registerInjectCommand } from "./commands/inject.ts";
5
6
  import { registerModelsCommand } from "./commands/models.ts";
6
7
  import { registerStartCommand } from "./commands/start.ts";
7
8
  import { registerStatusCommand } from "./commands/status.ts";
8
9
  import { registerTemplatesCommand } from "./commands/templates.ts";
9
- import { configureLogger, parseLogFormat } from "./logger.ts";
10
+ import { configureLogger, parseLogFormat, parseLogLevel } from "./logger.ts";
10
11
 
11
12
  /**
12
13
  * Create and configure the root Commander.js program.
@@ -17,11 +18,19 @@ export function createProgram(): Command {
17
18
 
18
19
  program
19
20
  .name("miravo")
20
- .description("Digital Twin Engine for Industrial IoT")
21
+ .description("Industrial environment platform for realistic software-defined plants")
21
22
  .version(MIRAVO_VERSION, "-V, --version", "Print version")
22
- .option("-v, --verbose", "Enable debug-level logging")
23
+ .option("-v, --verbose", "Shorthand for --log-level debug")
23
24
  .option("--silent", "Suppress all output except errors")
24
25
  .option("--plain", "Use stable plain-text output (no ANSI styling)")
26
+ .option("--log-level <level>", "Log level: debug | info | warn | error", (value: string) => {
27
+ try {
28
+ return parseLogLevel(value);
29
+ } catch (error) {
30
+ const message = error instanceof Error ? error.message : String(error);
31
+ throw new InvalidOptionArgumentError(message);
32
+ }
33
+ })
25
34
  .option(
26
35
  "--log-format <format>",
27
36
  "Log format: pretty | json",
@@ -40,18 +49,21 @@ export function createProgram(): Command {
40
49
  verbose?: boolean;
41
50
  silent?: boolean;
42
51
  plain?: boolean;
52
+ logLevel?: "debug" | "info" | "warn" | "error";
43
53
  logFormat?: "pretty" | "json";
44
54
  }>();
45
55
  const cmdOpts = actionCommand.opts<{
46
56
  verbose?: boolean;
47
57
  silent?: boolean;
48
58
  plain?: boolean;
59
+ logLevel?: "debug" | "info" | "warn" | "error";
49
60
  logFormat?: "pretty" | "json";
50
61
  }>();
51
62
  configureLogger({
52
63
  verbose: rootOpts.verbose ?? cmdOpts.verbose,
53
64
  silent: rootOpts.silent ?? cmdOpts.silent,
54
65
  plain: rootOpts.plain ?? cmdOpts.plain,
66
+ logLevel: rootOpts.logLevel ?? cmdOpts.logLevel,
55
67
  logFormat: rootOpts.logFormat ?? cmdOpts.logFormat,
56
68
  });
57
69
  });
@@ -59,24 +71,46 @@ export function createProgram(): Command {
59
71
  registerStartCommand(program);
60
72
  registerModelsCommand(program);
61
73
  registerTemplatesCommand(program);
74
+ registerForkCommand(program);
62
75
  registerInjectCommand(program);
63
76
  registerStatusCommand(program);
64
- // TODO(cli): Add `miravo completion <shell>` to generate shell completion scripts.
65
77
 
66
78
  return program;
67
79
  }
68
80
 
69
81
  // Only run when executed directly, not when imported in tests
70
82
  if (import.meta.path === Bun.main) {
71
- // Import the generated manifest to trigger Bun's file embedding in compiled binaries.
83
+ // In compiled binaries, extract embedded content to a temp directory so
84
+ // ContentCatalog's filesystem-based discovery can find built-in models/templates.
72
85
  // The manifest only exists during `bun run build:standalone`; the catch is a no-op in dev.
73
86
  try {
74
87
  // @ts-expect-error — file is generated at build time and does not exist in source tree
75
- await import("./_embedded-manifest.ts");
88
+ const manifest = await import("./_embedded-manifest.ts");
89
+ const { modelPaths, templatePaths } = manifest as {
90
+ modelPaths: string[];
91
+ templatePaths: string[];
92
+ };
93
+ const { basename, join } = await import("node:path");
94
+ const { tmpdir } = await import("node:os");
95
+ const { mkdirSync } = await import("node:fs");
96
+ const { registerEmbeddedContentDir } = await import("@miravo/core");
97
+
98
+ const contentDir = join(tmpdir(), "miravo-embedded");
99
+ const modelsDir = join(contentDir, "models");
100
+ const templatesDir = join(contentDir, "templates");
101
+ mkdirSync(modelsDir, { recursive: true });
102
+ mkdirSync(templatesDir, { recursive: true });
103
+
104
+ for (const p of modelPaths) {
105
+ await Bun.write(join(modelsDir, basename(p)), Bun.file(p));
106
+ }
107
+ for (const p of templatePaths) {
108
+ await Bun.write(join(templatesDir, basename(p)), Bun.file(p));
109
+ }
110
+
111
+ registerEmbeddedContentDir(contentDir);
76
112
  } catch {
77
113
  // Not a compiled binary or manifest not present — expected in dev mode
78
114
  }
79
- const { prepareEmbeddedContent } = await import("@miravo/core");
80
- await prepareEmbeddedContent();
81
115
  createProgram().parse();
82
116
  }
@@ -0,0 +1,214 @@
1
+ import type {
2
+ CommandResult,
3
+ EngineCommand,
4
+ EngineMetricsSnapshot,
5
+ EngineStateSnapshot,
6
+ } from "@miravo/shared";
7
+ import {
8
+ CommandResultSchema,
9
+ EngineMetricsSnapshotSchema,
10
+ EngineStateSnapshotSchema,
11
+ MiravoError,
12
+ } from "@miravo/shared";
13
+
14
+ const DEFAULT_ADMIN_HOST = "127.0.0.1";
15
+ const DEFAULT_ADMIN_PORT = 8080;
16
+
17
+ interface SafeParseResult<T> {
18
+ readonly success: boolean;
19
+ readonly data?: T;
20
+ }
21
+
22
+ interface SafeParseSchema<T> {
23
+ safeParse(value: unknown): SafeParseResult<T>;
24
+ }
25
+
26
+ interface RequestJsonOptions<T> {
27
+ readonly baseUrl: string;
28
+ readonly path: string;
29
+ readonly timeoutMs: number;
30
+ readonly schema: SafeParseSchema<T>;
31
+ readonly method?: "GET" | "POST";
32
+ readonly body?: string;
33
+ }
34
+
35
+ function normalizeAdminHost(host: string): string {
36
+ return host === "0.0.0.0" || host === "::" ? DEFAULT_ADMIN_HOST : host;
37
+ }
38
+
39
+ function resolveAdminUrl(adminUrl?: string): string {
40
+ const explicitUrl = adminUrl ?? Bun.env["MIRAVO_ADMIN_URL"];
41
+ if (explicitUrl && explicitUrl.trim().length > 0) {
42
+ return parseAdminUrl(explicitUrl);
43
+ }
44
+
45
+ const hostValue = Bun.env["MIRAVO_ADMIN_HOST"] ?? DEFAULT_ADMIN_HOST;
46
+ if (hostValue.trim().length === 0) {
47
+ throw new MiravoError(
48
+ "Invalid MIRAVO_ADMIN_HOST environment variable: value is empty.",
49
+ "Set MIRAVO_ADMIN_HOST to a valid host such as 127.0.0.1 or 0.0.0.0.",
50
+ );
51
+ }
52
+ const host = normalizeAdminHost(hostValue);
53
+ const portValue = Bun.env["MIRAVO_ADMIN_PORT"];
54
+ if (!portValue) {
55
+ return `http://${host}:${DEFAULT_ADMIN_PORT}`;
56
+ }
57
+
58
+ const parsedPort = Number(portValue);
59
+ if (!Number.isInteger(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
60
+ throw new MiravoError(
61
+ `Invalid MIRAVO_ADMIN_PORT environment variable: "${portValue}".`,
62
+ "Set MIRAVO_ADMIN_PORT to a valid port number between 1 and 65535.",
63
+ );
64
+ }
65
+
66
+ return `http://${host}:${parsedPort}`;
67
+ }
68
+
69
+ function parseAdminUrl(input: string): string {
70
+ let url: URL;
71
+ try {
72
+ url = new URL(input);
73
+ } catch {
74
+ throw new MiravoError(
75
+ `Invalid admin URL: "${input}".`,
76
+ "Pass --admin-url with a full HTTP URL such as http://127.0.0.1:8080.",
77
+ );
78
+ }
79
+
80
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
81
+ throw new MiravoError(
82
+ `Invalid admin URL protocol: "${url.protocol}".`,
83
+ "Pass an HTTP or HTTPS URL such as http://127.0.0.1:8080.",
84
+ );
85
+ }
86
+
87
+ return url.toString().replace(/\/$/, "");
88
+ }
89
+
90
+ async function requestJson<T>(options: RequestJsonOptions<T>): Promise<T> {
91
+ const requestUrl = new URL(options.path, `${options.baseUrl}/`).toString();
92
+ const controller = new AbortController();
93
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs);
94
+
95
+ try {
96
+ let response: Response;
97
+ try {
98
+ response = await fetch(requestUrl, {
99
+ method: options.method ?? "GET",
100
+ headers: options.body ? { "content-type": "application/json" } : undefined,
101
+ body: options.body,
102
+ signal: controller.signal,
103
+ });
104
+ } catch (error) {
105
+ if (error instanceof DOMException && error.name === "AbortError") {
106
+ throw new MiravoError(
107
+ `Admin request to ${requestUrl} timed out after ${options.timeoutMs}ms.`,
108
+ "Check that Miravo is running and the admin URL is reachable.",
109
+ );
110
+ }
111
+
112
+ const message = error instanceof Error ? error.message : String(error);
113
+ throw new MiravoError(
114
+ `Admin request to ${requestUrl} failed: ${message}`,
115
+ "Check that Miravo is running and the admin URL is correct.",
116
+ );
117
+ }
118
+
119
+ const text = await response.text();
120
+ let parsedJson: unknown;
121
+ if (text.length > 0) {
122
+ try {
123
+ parsedJson = JSON.parse(text);
124
+ } catch {
125
+ throw new MiravoError(
126
+ `Admin server returned invalid JSON from ${requestUrl}.`,
127
+ "Check the Miravo admin server logs and retry.",
128
+ );
129
+ }
130
+ }
131
+
132
+ if (!response.ok) {
133
+ const details =
134
+ typeof parsedJson === "object" &&
135
+ parsedJson !== null &&
136
+ "message" in parsedJson &&
137
+ typeof (parsedJson as Record<string, unknown>)["message"] === "string"
138
+ ? String((parsedJson as Record<string, unknown>)["message"])
139
+ : `HTTP ${response.status}`;
140
+ const fix =
141
+ typeof parsedJson === "object" &&
142
+ parsedJson !== null &&
143
+ "error" in parsedJson &&
144
+ typeof (parsedJson as Record<string, unknown>)["error"] === "string"
145
+ ? String((parsedJson as Record<string, unknown>)["error"])
146
+ : "Check that Miravo is running and the admin request is valid.";
147
+ throw new MiravoError(`Admin request to ${requestUrl} failed: ${details}`, fix);
148
+ }
149
+
150
+ const validation = options.schema.safeParse(parsedJson);
151
+ if (!validation.success || validation.data === undefined) {
152
+ throw new MiravoError(
153
+ `Admin server returned an unexpected payload from ${requestUrl}.`,
154
+ "Check that the Miravo CLI and server are running the same version.",
155
+ );
156
+ }
157
+
158
+ return validation.data;
159
+ } finally {
160
+ clearTimeout(timer);
161
+ }
162
+ }
163
+
164
+ async function getAdminMetrics(
165
+ adminUrl: string,
166
+ timeoutMs: number,
167
+ ): Promise<EngineMetricsSnapshot> {
168
+ return requestJson({
169
+ baseUrl: adminUrl,
170
+ path: "/metrics",
171
+ timeoutMs,
172
+ schema: EngineMetricsSnapshotSchema,
173
+ });
174
+ }
175
+
176
+ async function getAdminState(adminUrl: string, timeoutMs: number): Promise<EngineStateSnapshot> {
177
+ return requestJson({
178
+ baseUrl: adminUrl,
179
+ path: "/state",
180
+ timeoutMs,
181
+ schema: EngineStateSnapshotSchema,
182
+ });
183
+ }
184
+
185
+ async function getAdminSnapshot(
186
+ adminUrl: string,
187
+ timeoutMs: number,
188
+ ): Promise<{
189
+ readonly metrics: EngineMetricsSnapshot;
190
+ readonly state: EngineStateSnapshot;
191
+ }> {
192
+ const [metrics, state] = await Promise.all([
193
+ getAdminMetrics(adminUrl, timeoutMs),
194
+ getAdminState(adminUrl, timeoutMs),
195
+ ]);
196
+ return { metrics, state };
197
+ }
198
+
199
+ async function executeAdminCommand(
200
+ adminUrl: string,
201
+ command: EngineCommand,
202
+ timeoutMs: number,
203
+ ): Promise<CommandResult> {
204
+ return requestJson({
205
+ baseUrl: adminUrl,
206
+ path: "/commands",
207
+ timeoutMs,
208
+ schema: CommandResultSchema,
209
+ method: "POST",
210
+ body: JSON.stringify(command),
211
+ });
212
+ }
213
+
214
+ export { executeAdminCommand, getAdminMetrics, getAdminSnapshot, getAdminState, resolveAdminUrl };