@oml/cli 0.9.0 → 0.11.0

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.
@@ -15,15 +15,16 @@ import {
15
15
  type MdBlockKind,
16
16
  type MdExecutableBlock
17
17
  } from '@oml/markdown';
18
+ import { runWithIriLabelSnapshot } from '@oml/markdown/renderers';
18
19
  import { STATIC_MARKDOWN_RUNTIME_BUNDLE_FILE, STATIC_MARKDOWN_RUNTIME_CSS } from '@oml/markdown/static';
19
20
  import { normalizeFormatExtension } from '../backend/reasoned-output.js';
20
21
  import { createBackend } from '../backend/create-backend.js';
21
22
  import type { CliBackend } from '../backend/backend-types.js';
23
+ import { failCli } from '../cli-error.js';
22
24
  import { formatDuration } from '../util.js';
23
25
  import type { ReasonOptions } from './reason.js';
24
26
  import { reasonAction } from './reason.js';
25
27
  import { resolveCompileOutputRoot, resolveCompileWorkspaceRoot } from './compile.js';
26
- const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
27
28
  const markdownRuntime = new MarkdownPreviewRuntime(new MarkdownHandlerRegistry());
28
29
  const SUPPORTED_MD_BLOCK_KINDS = new Set<MdBlockKind>([
29
30
  'table',
@@ -84,18 +85,15 @@ export const renderAction = async (
84
85
  const output = path.resolve(opts.web);
85
86
  const workspaceStat = await fs.stat(workspaceRoot).catch(() => undefined);
86
87
  if (!workspaceStat || !workspaceStat.isDirectory()) {
87
- console.error(chalk.red(`Workspace folder does not exist: ${workspaceRoot}`));
88
- process.exit(1);
88
+ failCli(chalk.red(`Workspace folder does not exist: ${workspaceRoot}`));
89
89
  }
90
90
  const markdownStat = await fs.stat(markdownRoot).catch(() => undefined);
91
91
  if (!markdownStat || !markdownStat.isDirectory()) {
92
- console.error(chalk.red(`Markdown folder does not exist: ${markdownRoot}`));
93
- process.exit(1);
92
+ failCli(chalk.red(`Markdown folder does not exist: ${markdownRoot}`));
94
93
  }
95
94
 
96
95
  if (isSameOrDescendant(markdownRoot, output)) {
97
- console.error(chalk.red('Web output folder cannot be inside the markdown folder.'));
98
- process.exit(1);
96
+ failCli(chalk.red('Web output folder cannot be inside the markdown folder.'));
99
97
  }
100
98
 
101
99
  if (opts.clean) {
@@ -108,6 +106,7 @@ export const renderAction = async (
108
106
  const templateCatalog = buildTemplateCatalog(templateFiles);
109
107
  const explicitContextModelUri = resolveModelUriOption(opts.context, workspaceRoot);
110
108
  const templateGenerationEnabled = explicitContextModelUri !== undefined;
109
+ const memberLabelSnapshot = await backend.getWorkspaceMemberLabelSnapshot(workspaceRoot);
111
110
  const wikiPageIndex = buildWikiPageIndex(renderableMarkdownFiles, markdownRoot, output);
112
111
  const generatedInstancePages = new Set<string>();
113
112
  const instancePageByIri = new Map<string, string>();
@@ -129,11 +128,11 @@ export const renderAction = async (
129
128
  staticAssets.stylesheetFile,
130
129
  templateGenerationEnabled,
131
130
  explicitContextModelUri,
131
+ memberLabelSnapshot,
132
132
  wikiPageIndex,
133
133
  templateCatalog,
134
134
  generatedInstancePages,
135
- instancePageByIri
136
- ,
135
+ instancePageByIri,
137
136
  attemptedInstanceIris
138
137
  );
139
138
  const referencedWorkspaceAssets = rendered.workspaceAssets;
@@ -292,6 +291,7 @@ async function renderMarkdownFile(
292
291
  stylesheetFile: string,
293
292
  templateGenerationEnabled: boolean,
294
293
  defaultContextModelUri: string | undefined,
294
+ memberLabelSnapshot: Record<string, string>,
295
295
  wikiPageIndex: ReadonlyMap<string, string>,
296
296
  templateCatalog: TemplateCatalog,
297
297
  generatedInstancePages: Set<string>,
@@ -302,7 +302,9 @@ async function renderMarkdownFile(
302
302
  forcedModelUri?: string
303
303
  ): Promise<{ workspaceAssets: Set<string>; blockArtifactFiles: number }> {
304
304
  const markdown = explicitMarkdown ?? await fs.readFile(inputFile, 'utf-8');
305
- const prepared = markdownRuntime.prepare(markdown);
305
+ const inlineContextUri = extractLeadingFrontMatter(markdown)?.contextUri;
306
+ const modelUri = forcedModelUri ?? resolveContextModelUri(inputFile, inlineContextUri, workspaceRoot) ?? defaultContextModelUri;
307
+ const prepared = runWithIriLabelSnapshot(memberLabelSnapshot, () => markdownRuntime.prepare(markdown));
306
308
  const rewriteResult = rewriteRenderedLinks(prepared.renderedHtml, {
307
309
  workspaceRoot,
308
310
  inputRoot,
@@ -312,7 +314,6 @@ async function renderMarkdownFile(
312
314
  });
313
315
  const executableBlocks = toExecutableBlocks(prepared.codeBlocks);
314
316
  const optionsByBlockId = new Map(prepared.codeBlocks.map((block) => [block.id, block.options] as const));
315
- const modelUri = forcedModelUri ?? resolveContextModelUri(inputFile, prepared.contextUri, workspaceRoot) ?? defaultContextModelUri;
316
317
  const blockResults = executableBlocks.length === 0
317
318
  ? []
318
319
  : (await backend.executeMarkdownBlocks({
@@ -344,6 +345,7 @@ async function renderMarkdownFile(
344
345
  stylesheetFile,
345
346
  templateGenerationEnabled,
346
347
  modelUri,
348
+ memberLabelSnapshot,
347
349
  pageWikilinkIris,
348
350
  renderedBlockResults,
349
351
  wikiPageIndex,
@@ -368,7 +370,8 @@ async function renderMarkdownFile(
368
370
  blockRewriteResult.results,
369
371
  wikiLinkHrefByKey,
370
372
  { ...resolvedInstanceLinks, ...iriAliasHrefByIri },
371
- templateGenerationEnabled && Boolean(modelUri)
373
+ templateGenerationEnabled && Boolean(modelUri),
374
+ memberLabelSnapshot
372
375
  );
373
376
  await fs.mkdir(path.dirname(outputFile), { recursive: true });
374
377
  await fs.writeFile(outputFile, html, 'utf-8');
@@ -445,8 +448,8 @@ async function writeStaticAssets(outputRoot: string): Promise<{
445
448
  const mergedStylesheet = `${sourceStylesheet}\n\n${STATIC_MARKDOWN_RUNTIME_CSS}\n`;
446
449
  const runtimeVersion = createHash('sha1').update(runtimeBundle).digest('hex').slice(0, 12);
447
450
  const stylesheetVersion = createHash('sha1').update(mergedStylesheet).digest('hex').slice(0, 12);
448
- const runtimeFile = path.join(outputRoot, '_oml', `markdown-static-${runtimeVersion}.js`);
449
- const stylesheetFile = path.join(outputRoot, '_oml', `markdown-webview-${stylesheetVersion}.css`);
451
+ const runtimeFile = path.join(outputRoot, 'assets', `markdown-static-${runtimeVersion}.js`);
452
+ const stylesheetFile = path.join(outputRoot, 'assets', `markdown-webview-${stylesheetVersion}.css`);
450
453
  await fs.mkdir(path.dirname(runtimeFile), { recursive: true });
451
454
  await fs.writeFile(runtimeFile, runtimeBundle, 'utf-8');
452
455
  await fs.writeFile(stylesheetFile, mergedStylesheet, 'utf-8');
@@ -468,18 +471,17 @@ async function loadStaticRuntimeBundle(): Promise<string> {
468
471
  }
469
472
 
470
473
  async function loadCodeBlockStylesheet(): Promise<string> {
471
- const candidates = [
472
- path.resolve(__dirname, '..', '..', '..', 'extension', 'src', 'webview', 'markdown-webview.css'),
473
- path.resolve(__dirname, '..', '..', '..', 'extension', 'out', 'webview', 'markdown-webview.css'),
474
- ];
475
- for (const candidate of candidates) {
476
- try {
477
- return await fs.readFile(candidate, 'utf-8');
478
- } catch {
479
- // Try next candidate.
480
- }
474
+ const require = createRequire(import.meta.url);
475
+ const staticEntry = require.resolve('@oml/markdown/static');
476
+ const stylesheetPath = path.resolve(path.dirname(staticEntry), '..', '..', 'src', 'static', 'markdown-webview.css');
477
+ try {
478
+ return await fs.readFile(stylesheetPath, 'utf-8');
479
+ } catch {
480
+ throw new Error(
481
+ `Unable to load markdown webview stylesheet at '${stylesheetPath}'. `
482
+ + 'The installed @oml/markdown package is missing a required static asset.'
483
+ );
481
484
  }
482
- return '';
483
485
  }
484
486
 
485
487
  async function writeBlockArtifacts(outputFile: string, results: ReadonlyArray<RenderedBlockResult>): Promise<{
@@ -880,6 +882,7 @@ async function generateInstanceTemplatePages(
880
882
  stylesheetFile: string,
881
883
  templateGenerationEnabled: boolean,
882
884
  queryContextModelUri: string,
885
+ memberLabelSnapshot: Record<string, string>,
883
886
  pageWikilinkIris: ReadonlySet<string>,
884
887
  results: ReadonlyArray<RenderedBlockResult>,
885
888
  wikiPageIndex: ReadonlyMap<string, string>,
@@ -932,6 +935,7 @@ async function generateInstanceTemplatePages(
932
935
  stylesheetFile,
933
936
  templateGenerationEnabled,
934
937
  queryContextModelUri,
938
+ memberLabelSnapshot,
935
939
  wikiPageIndex,
936
940
  templateCatalog,
937
941
  generatedInstancePages,
@@ -960,7 +964,8 @@ function wrapHtml(
960
964
  blockResults: ReadonlyArray<RenderedBlockResult>,
961
965
  wikiLinkHrefByKey: Record<string, string>,
962
966
  iriAliasHrefByIri: Record<string, string>,
963
- linkingEnabled: boolean
967
+ linkingEnabled: boolean,
968
+ memberLabelSnapshot?: Record<string, string>
964
969
  ): string {
965
970
  const escapedManifest = escapeJsonForScript(JSON.stringify(blockManifest));
966
971
  const inlineResults = Object.fromEntries(blockResults.map((result) => [result.blockId, result]));
@@ -968,6 +973,7 @@ function wrapHtml(
968
973
  const escapedWikiIndex = escapeJsonForScript(JSON.stringify(wikiLinkHrefByKey));
969
974
  const escapedIriAliasIndex = escapeJsonForScript(JSON.stringify(iriAliasHrefByIri));
970
975
  const escapedLinkingConfig = escapeJsonForScript(JSON.stringify({ linkingEnabled }));
976
+ const escapedMemberLabels = escapeJsonForScript(JSON.stringify(memberLabelSnapshot ?? {}));
971
977
  return `<!doctype html>
972
978
  <html lang="en">
973
979
  <head>
@@ -983,6 +989,7 @@ ${content}
983
989
  <script id="oml-md-wikilink-index" type="application/json">${escapedWikiIndex}</script>
984
990
  <script id="oml-md-wikilink-iri-aliases" type="application/json">${escapedIriAliasIndex}</script>
985
991
  <script id="oml-md-wikilink-config" type="application/json">${escapedLinkingConfig}</script>
992
+ <script id="oml-md-member-labels" type="application/json">${escapedMemberLabels}</script>
986
993
  <script src="${escapeAttribute(runtimeScriptPath)}"></script>
987
994
  </body>
988
995
  </html>
@@ -10,6 +10,7 @@ import * as fs from 'node:fs/promises';
10
10
  import * as path from 'node:path';
11
11
  import * as url from 'node:url';
12
12
  import { loadPreparedDatasetFromOutput, normalizeFormatExtension } from '../backend/reasoned-output.js';
13
+ import { failCli } from '../cli-error.js';
13
14
  import { formatDuration } from '../util.js';
14
15
  import type { ReasonOptions } from './reason.js';
15
16
  import { reasonAction } from './reason.js';
@@ -40,8 +41,7 @@ export const validateAction = async (opts: ValidateOptions): Promise<void> => {
40
41
 
41
42
  const markdownStat = await fs.stat(markdownRoot).catch(() => undefined);
42
43
  if (!markdownStat?.isDirectory()) {
43
- console.error(chalk.red(`Markdown folder does not exist: ${markdownRoot}`));
44
- process.exit(1);
44
+ failCli(chalk.red(`Markdown folder does not exist: ${markdownRoot}`));
45
45
  }
46
46
 
47
47
  if (!opts.only) {
@@ -98,7 +98,7 @@ export const validateAction = async (opts: ValidateOptions): Promise<void> => {
98
98
 
99
99
  reportValidationResults(results, workspaceRoot, markdownRoot, validationStartedAt);
100
100
  if (results.some((result) => result.error || result.issues.length > 0 || !result.conforms)) {
101
- process.exit(1);
101
+ failCli('');
102
102
  }
103
103
  };
104
104
 
package/src/main.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  // Copyright (c) 2026 Modelware. All rights reserved.
2
2
 
3
- import { runCli } from './cli.js';
3
+ import { reportCliError, runCli } from './cli.js';
4
4
 
5
- void runCli();
5
+ void runCli().catch((error) => {
6
+ process.exitCode = reportCliError(error);
7
+ });
package/src/platform.ts CHANGED
@@ -8,14 +8,16 @@
8
8
  * commands to execute. No whitelist or other fallback is used.
9
9
  */
10
10
 
11
- import { OmlClient, FileStorageAdapter } from '@oml/platform';
11
+ import { OmlClient, FileStorageAdapter, installNodeShutdownHandlers } from '@oml/platform';
12
12
  import type { OmlClientConfig } from '@oml/platform';
13
+ import type { NodeShutdownHandle } from '@oml/platform';
13
14
  import chalk from 'chalk';
14
15
  import { DEFAULT_API_BASE_URL } from './platform-constants.js';
15
16
  import { OmlCliAuthService } from './auth.js';
16
17
  const API_BASE_URL_ENV = 'OML_PLATFORM_API_URL';
17
18
 
18
19
  let client: OmlClient | null = null;
20
+ let shutdownHandle: NodeShutdownHandle | null = null;
19
21
 
20
22
  export type CommandInvocationTracker = ReturnType<OmlClient['trackInvocation']>;
21
23
 
@@ -30,6 +32,8 @@ export async function initializePlatform(
30
32
  authService: OmlCliAuthService,
31
33
  apiBaseUrl = DEFAULT_API_BASE_URL
32
34
  ): Promise<void> {
35
+ await disposePlatform();
36
+
33
37
  const key = process.env.OML_PLATFORM_API_KEY;
34
38
  const resolvedApiBaseUrl = process.env[API_BASE_URL_ENV]?.trim() || apiBaseUrl;
35
39
 
@@ -66,6 +70,7 @@ export async function initializePlatform(
66
70
  throw new Error(`OML CLI could not connect to the authorization service. ${toGenericPlatformErrorMessage(error)}`);
67
71
  }
68
72
  client = platformClient;
73
+ shutdownHandle = installNodeShutdownHandlers(platformClient);
69
74
  }
70
75
 
71
76
  /**
@@ -73,6 +78,10 @@ export async function initializePlatform(
73
78
  * and ends the session. Call at the end of CLI execution.
74
79
  */
75
80
  export async function disposePlatform(): Promise<void> {
81
+ if (shutdownHandle) {
82
+ shutdownHandle.dispose();
83
+ shutdownHandle = null;
84
+ }
76
85
  if (client) {
77
86
  await client.dispose();
78
87
  client = null;
package/src/util.ts CHANGED
@@ -5,17 +5,16 @@ import chalk from 'chalk';
5
5
  import * as path from 'node:path';
6
6
  import * as fs from 'node:fs';
7
7
  import { URI } from 'langium';
8
+ import { failCli } from './cli-error.js';
8
9
 
9
10
  export async function extractDocument(fileName: string, services: LangiumCoreServices, workspaceRoot?: string): Promise<LangiumDocument> {
10
11
  const extensions = services.LanguageMetaData.fileExtensions;
11
12
  if (!extensions.includes(path.extname(fileName))) {
12
- console.error(chalk.yellow(`Please choose a file with one of these extensions: ${extensions}.`));
13
- process.exit(1);
13
+ failCli(chalk.yellow(`Please choose a file with one of these extensions: ${extensions}.`));
14
14
  }
15
15
 
16
16
  if (!fs.existsSync(fileName)) {
17
- console.error(chalk.red(`File ${fileName} does not exist.`));
18
- process.exit(1);
17
+ failCli(chalk.red(`File ${fileName} does not exist.`));
19
18
  }
20
19
 
21
20
  if (workspaceRoot) {
@@ -31,13 +30,13 @@ export async function extractDocument(fileName: string, services: LangiumCoreSer
31
30
  const diagnostics = document.diagnostics ?? [];
32
31
  const validationErrors = diagnostics.filter(e => e.severity === 1);
33
32
  if (validationErrors.length > 0) {
34
- console.error(chalk.red(`There are validation errors in ${path.resolve(fileName)}:`));
33
+ let errorMessage = chalk.red(`There are validation errors in ${path.resolve(fileName)}:`);
35
34
  for (const validationError of validationErrors) {
36
- console.error(chalk.red(
35
+ errorMessage += `\n${chalk.red(
37
36
  `line ${validationError.range.start.line + 1}: ${validationError.message} [${document.textDocument.getText(validationError.range)}]`
38
- ));
37
+ )}`;
39
38
  }
40
- process.exit(1);
39
+ failCli(errorMessage);
41
40
  }
42
41
 
43
42
  const validationWarnings = diagnostics.filter(e => e.severity === 2);