@oml/cli 0.14.17 → 0.16.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.
package/src/cli.ts CHANGED
@@ -15,6 +15,7 @@ import { notifyIfCliUpdateAvailable } from './update.js';
15
15
  import { validateAction } from './commands/validate.js';
16
16
  import { CliExitError } from './cli-error.js';
17
17
  import { trackCommand } from './auth/platform.js';
18
+ import { readWorkspaceSettings } from '@oml/server/workspace-settings';
18
19
 
19
20
  const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
20
21
  let debugEnabled = false;
@@ -28,20 +29,20 @@ export interface CliCommandInfo {
28
29
  export function getWorkspaceCommands(): CliCommandInfo[] {
29
30
  return [
30
31
  { name: 'lint', description: 'lints OML files and prints any syntax or validation errors' },
31
- {
32
- name: 'render [options]',
32
+ {
33
+ name: 'render [options]',
33
34
  description: 'lint the workspace, then render markdown files to static html',
34
- usage: 'render -m <input-folder> -b <output-folder> [--clean] [-c <ontology-iri>]'
35
+ usage: 'render -m <input-folder> -b <output-folder> [-c <ontology-iri>]'
35
36
  },
36
- {
37
- name: 'export [options]',
37
+ {
38
+ name: 'export [options]',
38
39
  description: 'export asserted OWL files (no reasoning)',
39
- usage: 'export [-o <dir>] [-f <ext>] [--clean] [--pretty]'
40
+ usage: 'export [-o <dir>] [-f <ext>] [--pretty]'
40
41
  },
41
- {
42
- name: 'reason [options]',
42
+ {
43
+ name: 'reason [options]',
43
44
  description: 'run workspace consistency checks (or persist assertions/entailments with --owl)',
44
- usage: 'reason [-o <dir>] [-f <ext>] [--clean] [--pretty] [-e <true|false>]'
45
+ usage: 'reason [-o <dir>] [-f <ext>] [--pretty] [-e <true|false>]'
45
46
  },
46
47
  {
47
48
  name: 'validate [options]',
@@ -94,14 +95,20 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
94
95
  .option('-w, --workspace <workspace>', 'workspace root used by REST facade initialize (default: cwd)')
95
96
  .description('start an OML server')
96
97
  .action(async (port: string | undefined, options: { port?: string; workspace?: string }) => {
98
+ const [entitlementCache, deviceId] = await Promise.all([
99
+ authService.getEntitlementCache().then((c) => c ?? undefined),
100
+ authService.getDeviceId().catch(() => undefined),
101
+ ]);
97
102
  if (process.env.OML_PLATFORM_API_KEY?.trim()) {
98
- await serverStartAction(port, { ...options, auth: await resolveServerStartAuth() });
103
+ await serverStartAction(port, { ...options, auth: await resolveServerStartAuth(), entitlementCache });
99
104
  return;
100
105
  }
101
106
  await serverRunAction(port, {
102
107
  ...options,
103
108
  authService,
104
109
  auth: await resolveServerRunAuth(authService),
110
+ entitlementCache,
111
+ deviceId,
105
112
  });
106
113
  });
107
114
 
@@ -146,19 +153,26 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
146
153
 
147
154
  program
148
155
  .command('render')
149
- .requiredOption('-m, --md <input-folder>', 'folder containing markdown files to render')
150
- .requiredOption('-b, --web <output-folder>', 'folder where rendered static site files are written')
151
- .option('--clean', 'remove output folder before render')
156
+ .option('-m, --md <input-folder>', 'folder containing markdown files to render')
157
+ .option('-b, --web <output-folder>', 'folder where rendered static site files are written')
152
158
  .option('-c, --context <model-path>', 'workspace-relative .oml model path used as default navigation context for wikilinks')
153
159
  .description('lint the workspace, then render markdown files to static html')
154
160
  .action(async (...args: unknown[]) => {
155
161
  const done = trackCommand('oml-render');
156
162
  try {
157
163
  const authToken = resolveServerRequestToken();
158
- await renderAction({
159
- ...(args[0] as Record<string, unknown> | undefined ?? {}),
160
- authToken,
161
- } as Parameters<typeof renderAction>[0]);
164
+ const cliOpts = args[0] as Record<string, unknown> | undefined ?? {};
165
+ const wsSettings = await readWorkspaceSettings(process.cwd());
166
+ const md = cliOpts['md'] ?? wsSettings.defaults?.render?.md;
167
+ const web = cliOpts['web'] ?? wsSettings.defaults?.render?.web;
168
+ if (!md) {
169
+ throw new CliExitError('Missing required option: -m, --md <input-folder>. Provide it on the command line or set defaults.render.md in .oml/settings.yml.');
170
+ }
171
+ if (!web) {
172
+ throw new CliExitError('Missing required option: -b, --web <output-folder>. Provide it on the command line or set defaults.render.web in .oml/settings.yml.');
173
+ }
174
+ const context = cliOpts['context'] ?? wsSettings.defaults?.render?.context;
175
+ await renderAction({ md, web, context, authToken } as Parameters<typeof renderAction>[0]);
162
176
  done();
163
177
  } catch (err) {
164
178
  done(err);
@@ -170,15 +184,21 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
170
184
  .command('export')
171
185
  .option('-o, --owl <dir>', 'folder where RDF output files are written')
172
186
  .option('-f, --format <ext>', 'RDF format extension: ttl, trig, nt, nq, or n3', 'ttl')
173
- .option('--clean', 'remove output folder before export')
174
187
  .option('--pretty', 'pretty-print Turtle/TriG output with blank lines between top-level blocks')
175
188
  .description('export asserted OWL files (no reasoning)')
176
189
  .action(async (...args: unknown[]) => {
177
190
  const done = trackCommand('oml-export');
178
191
  try {
179
192
  const authToken = resolveServerRequestToken();
193
+ const cliOpts = args[0] as Record<string, unknown> | undefined ?? {};
194
+ const wsSettings = await readWorkspaceSettings(process.cwd());
195
+ const owl = cliOpts['owl'] ?? wsSettings.defaults?.build?.owl;
196
+ if (!owl) {
197
+ throw new CliExitError('Missing required option: -o, --owl <dir>. Provide it on the command line or set defaults.build.owl in .oml/settings.yml.');
198
+ }
180
199
  await exportAction({
181
- ...(args[0] as Record<string, unknown> | undefined ?? {}),
200
+ ...cliOpts,
201
+ owl,
182
202
  authToken,
183
203
  } as Parameters<typeof exportAction>[0]);
184
204
  done();
@@ -192,7 +212,6 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
192
212
  .command('reason')
193
213
  .option('-o, --owl <dir>', 'persist assertions/entailments to folder (default: check-only with no persistence)')
194
214
  .option('-f, --format <ext>', 'RDF format extension: ttl, trig, nt, nq, or n3', 'ttl')
195
- .option('--clean', 'remove output folder before reason (only when --owl is provided)')
196
215
  .option('--pretty', 'pretty-print Turtle/TriG output with blank lines between top-level blocks (only when --owl is provided)')
197
216
  .option('-u, --unique-names-assumption [value]', 'enable or disable the unique names assumption', parseBooleanOption, true)
198
217
  .option('-e, --explanation [value]', 'enable or disable inconsistency explanations', parseBooleanOption, false)
@@ -202,7 +221,13 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
202
221
  try {
203
222
  const { reasonAction } = await import('./commands/reason.js');
204
223
  const authToken = resolveServerRequestToken();
205
- await reasonAction({ ...(opts as Record<string, unknown>), authToken } as Parameters<typeof reasonAction>[0]);
224
+ const cliOpts = opts as Record<string, unknown>;
225
+ const wsSettings = await readWorkspaceSettings(process.cwd());
226
+ const owl = cliOpts['owl'] ?? wsSettings.defaults?.build?.owl;
227
+ if (!owl) {
228
+ throw new CliExitError('Missing required option: -o, --owl <dir>. Provide it on the command line or set defaults.build.owl in .oml/settings.yml.');
229
+ }
230
+ await reasonAction({ ...cliOpts, owl, authToken } as Parameters<typeof reasonAction>[0]);
206
231
  done();
207
232
  } catch (err) {
208
233
  done(err);
@@ -240,6 +265,9 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
240
265
  ) {
241
266
  return;
242
267
  }
268
+ if (!process.env.OML_PLATFORM_API_KEY?.trim() && !(await authService.hasStoredCredential())) {
269
+ throw new CliExitError('OML CLI authentication is required. Run \'oml login\' and retry.');
270
+ }
243
271
  await assertServerRunning();
244
272
  });
245
273
 
@@ -322,29 +350,21 @@ async function resolveServerStartAuth(): Promise<{ accessToken: string }> {
322
350
  const apiKey = process.env.OML_PLATFORM_API_KEY?.trim();
323
351
  if (!apiKey) {
324
352
  throw new CliExitError(
325
- 'OML_PLATFORM_API_KEY is not set. For interactive use, run \'oml start\'. ' +
326
- 'For non-interactive use, set OML_PLATFORM_API_KEY and retry.'
353
+ 'OML_PLATFORM_API_KEY is required for non-interactive server start. ' +
354
+ 'Unset it and run \'oml start\' again to use interactive OAuth login instead.'
327
355
  );
328
356
  }
329
357
  return { accessToken: apiKey };
330
358
  }
331
359
 
332
- async function resolveServerRunAuth(authService: OmlCliAuthService): Promise<{
333
- accessToken: string;
334
- }> {
335
- let snapshot;
336
- try {
337
- snapshot = await authService.getServerAuthSnapshot();
338
- } catch {
360
+ async function resolveServerRunAuth(authService: OmlCliAuthService): Promise<{ accessToken: string }> {
361
+ if (!(await authService.hasStoredCredential())) {
362
+ console.error(chalk.cyan('Authentication required to start the server. Signing in...'));
339
363
  await authService.login({});
340
- snapshot = await authService.getServerAuthSnapshot();
341
364
  }
342
- return {
343
- accessToken: snapshot.accessToken,
344
- };
365
+ return { accessToken: (await authService.getServerAuthSnapshot()).accessToken };
345
366
  }
346
367
 
347
-
348
368
  function formatDetailedError(error: unknown): string {
349
369
  if (!(error instanceof Error)) {
350
370
  return String(error);
@@ -10,7 +10,6 @@ import { restPost } from './server/rest.js';
10
10
  export type ExportOptions = {
11
11
  owl?: string;
12
12
  format?: 'ttl' | 'trig' | 'nt' | 'nq' | 'n3' | string;
13
- clean?: boolean;
14
13
  pretty?: boolean;
15
14
  authToken?: string;
16
15
  };
@@ -44,7 +43,6 @@ export const exportAction = async (opts: ExportOptions): Promise<void> => {
44
43
  {
45
44
  owl: opts.owl,
46
45
  format,
47
- clean: opts.clean,
48
46
  pretty: opts.pretty === true,
49
47
  } as Record<string, unknown>,
50
48
  opts.authToken,
@@ -71,7 +71,8 @@ export const lintAction = async (opts: LintOptions): Promise<void> => {
71
71
  failCli(chalk.red(formatLintSummary(result, elapsedMs)));
72
72
  }
73
73
  if (result.warnings > 0) {
74
- failCli(chalk.yellow(formatLintSummary(result, elapsedMs)));
74
+ console.warn(chalk.yellow(formatLintSummary(result, elapsedMs)));
75
+ return;
75
76
  }
76
77
  console.log(chalk.green(formatLintSummary(result, elapsedMs)));
77
78
  };
@@ -16,7 +16,6 @@ type RdfFormat = 'ttl' | 'trig' | 'nt' | 'nq' | 'n3';
16
16
  export type ReasonOptions = {
17
17
  owl?: string;
18
18
  format?: RdfFormat | string;
19
- clean?: boolean;
20
19
  pretty?: boolean;
21
20
  explanation?: boolean;
22
21
  uniqueNamesAssumption?: boolean;
@@ -36,7 +35,6 @@ type AssertionsPayload = {
36
35
  files: Array<{
37
36
  modelUri: string;
38
37
  ontologyIri: string;
39
- path: string;
40
38
  content: string;
41
39
  }>;
42
40
  };
@@ -150,8 +148,21 @@ function resolveEntailmentsPath(uri: string): string {
150
148
  return trimmed;
151
149
  }
152
150
 
153
- function modelUriToRelativePath(modelUri: string): string {
154
- const trimmed = modelUri.trim();
151
+ function ontologyIriToTempPath(ontologyIri: string, format: RdfFormat): string {
152
+ try {
153
+ const iri = new URL(ontologyIri);
154
+ const rawPath = iri.pathname.replace(/\/+$/, '');
155
+ const segments = rawPath.split('/').filter(Boolean);
156
+ const stem = segments.at(-1) || 'index';
157
+ const dirSegs = iri.host ? [iri.host, ...segments.slice(0, -1)] : segments.slice(0, -1);
158
+ return dirSegs.length > 0 ? path.join(...dirSegs, `${stem}.${format}`) : `${stem}.${format}`;
159
+ } catch {
160
+ return ontologyIri.replace(/[^a-zA-Z0-9_/-]/g, '_') + '.' + format;
161
+ }
162
+ }
163
+
164
+ function modelUriToRelativePath(modelUri: string | undefined): string {
165
+ const trimmed = (modelUri ?? '').trim();
155
166
  if (trimmed.startsWith('file://')) {
156
167
  const absolute = fileURLToPath(trimmed);
157
168
  const relative = path.relative(process.cwd(), absolute);
@@ -190,9 +201,7 @@ export const reasonAction = async (opts: ReasonOptions): Promise<void> => {
190
201
  : undefined;
191
202
  const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'oml-reason-cli-'));
192
203
  if (outputDir) {
193
- if (opts.clean === true) {
194
- await fs.rm(outputDir, { recursive: true, force: true });
195
- }
204
+ await fs.rm(outputDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
196
205
  }
197
206
  const orderedFiles = sortFilesLeafToRoot(assertions.files);
198
207
  let firstInconsistent: { file: AssertionsPayload['files'][number]; result: Record<string, unknown> } | undefined;
@@ -200,12 +209,12 @@ export const reasonAction = async (opts: ReasonOptions): Promise<void> => {
200
209
  let entailmentsProduced = 0;
201
210
  try {
202
211
  for (const file of orderedFiles) {
203
- const target = path.join(tempRoot, file.path);
212
+ const target = path.join(tempRoot, ontologyIriToTempPath(file.ontologyIri, outputFormat));
204
213
  await fs.mkdir(path.dirname(target), { recursive: true });
205
214
  await fs.writeFile(target, prettyPrintRdf(file.content, outputFormat), 'utf-8');
206
215
  }
207
216
  for (const file of orderedFiles) {
208
- const target = path.join(tempRoot, file.path);
217
+ const target = path.join(tempRoot, ontologyIriToTempPath(file.ontologyIri, outputFormat));
209
218
  try {
210
219
  const result = await checkConsistency({
211
220
  input: target,
@@ -257,7 +266,9 @@ export const reasonAction = async (opts: ReasonOptions): Promise<void> => {
257
266
  failCli(chalk.red(`reason failed: ${modelUri}: ${firstFailure.error}`));
258
267
  }
259
268
  if (firstInconsistent) {
260
- const where = modelUriToRelativePath(firstInconsistent.file.modelUri);
269
+ const where = modelUriToRelativePath(firstInconsistent.file.modelUri)
270
+ || firstInconsistent.file.ontologyIri.trim()
271
+ || '<unknown>';
261
272
  if (opts.explanation === true) {
262
273
  failCli(chalk.red(`Inconsistency found in: ${where}\n${JSON.stringify(firstInconsistent.result, null, 2)}`));
263
274
  }
@@ -10,7 +10,6 @@ import { restPost } from './server/rest.js';
10
10
  export type RenderOptions = {
11
11
  md: string;
12
12
  web: string;
13
- clean?: boolean;
14
13
  context?: string;
15
14
  authToken?: string;
16
15
  };
@@ -10,6 +10,7 @@ import { createRequire } from 'node:module';
10
10
  import { spawn, execFile, type ChildProcess } from 'node:child_process';
11
11
  import { promisify } from 'node:util';
12
12
  import type { OmlCliAuthService } from '../../auth/auth.js';
13
+ import { readWorkspaceSettings } from '@oml/server/workspace-settings';
13
14
 
14
15
  const DEFAULT_HOST = '127.0.0.1';
15
16
  const STARTUP_TIMEOUT_MS = 15_000;
@@ -17,12 +18,19 @@ const SHUTDOWN_TIMEOUT_MS = 3000;
17
18
  const POLL_INTERVAL_MS = 100;
18
19
  const execFileAsync = promisify(execFile);
19
20
 
21
+ type EntitlementCache = {
22
+ expiry: number;
23
+ featureIds: string[];
24
+ token?: string;
25
+ };
26
+
20
27
  type StartServerOptions = {
21
28
  port?: number | string;
22
29
  workspace?: string;
23
30
  auth?: {
24
31
  accessToken: string;
25
32
  };
33
+ entitlementCache?: EntitlementCache;
26
34
  };
27
35
 
28
36
  type ServerStatePaths = {
@@ -63,6 +71,7 @@ function getServerStatePaths(workspace?: string): ServerStatePaths {
63
71
 
64
72
  async function cleanupStateFile(paths: ServerStatePaths): Promise<void> {
65
73
  await fs.rm(paths.lockFile, { force: true });
74
+ await fs.rm(paths.dir, { recursive: true, force: true });
66
75
  }
67
76
 
68
77
  function parseServerLock(raw: string): ServerLockState | undefined {
@@ -128,6 +137,7 @@ async function listRunningServers(): Promise<RunningServer[]> {
128
137
  }
129
138
  if (!isProcessAlive(state.pid)) {
130
139
  await fs.rm(lockFile, { force: true });
140
+ await fs.rm(path.dirname(lockFile), { recursive: true, force: true });
131
141
  continue;
132
142
  }
133
143
  servers.push({
@@ -279,6 +289,7 @@ function spawnServerProcess(
279
289
  options: {
280
290
  workspace?: string;
281
291
  auth?: { accessToken: string };
292
+ entitlementCache?: EntitlementCache;
282
293
  },
283
294
  ): ChildProcess {
284
295
  const args = [serverMainScript, `--port=${port}`];
@@ -289,6 +300,13 @@ function spawnServerProcess(
289
300
  if (options.auth?.accessToken) {
290
301
  args.push(`--token=${options.auth.accessToken}`);
291
302
  }
303
+ if (options.entitlementCache) {
304
+ args.push(`--entitlement-expiry=${options.entitlementCache.expiry}`);
305
+ args.push(`--entitlement-features=${JSON.stringify(options.entitlementCache.featureIds)}`);
306
+ if (options.entitlementCache.token) {
307
+ args.push(`--entitlement-token=${options.entitlementCache.token}`);
308
+ }
309
+ }
292
310
 
293
311
  return spawn(process.execPath, args, {
294
312
  detached,
@@ -327,7 +345,10 @@ export async function serverStartAction(portArg: number | string | undefined, op
327
345
  if (portArg !== undefined && options.port !== undefined && String(portArg) !== String(options.port)) {
328
346
  throw new Error(`Conflicting port values '${portArg}' and '${options.port}'. Use either the positional port or --port, not both.`);
329
347
  }
330
- const port = normalizeStartPort(options.port ?? portArg);
348
+ const workspaceRoot = options.workspace ?? process.cwd();
349
+ const wsSettings = await readWorkspaceSettings(workspaceRoot);
350
+ const settingsPort = wsSettings.server?.port;
351
+ const port = normalizeStartPort(options.port ?? portArg ?? settingsPort);
331
352
  const serverMainScript = resolveServerMainScript();
332
353
  const scriptExists = await canReadFile(serverMainScript);
333
354
  if (!scriptExists) {
@@ -352,6 +373,7 @@ async function serverStartDetached(
352
373
  const child = spawnServerProcess(serverMainScript, port, true, 'ignore', {
353
374
  workspace: options.workspace,
354
375
  auth: options.auth,
376
+ entitlementCache: options.entitlementCache,
355
377
  });
356
378
  if (!child.pid) {
357
379
  throw new Error('Failed to launch server process.');
@@ -449,13 +471,10 @@ export type RunServerOptions = {
449
471
  auth: {
450
472
  accessToken: string;
451
473
  };
474
+ entitlementCache?: EntitlementCache;
475
+ deviceId?: string;
452
476
  };
453
477
 
454
- function sendLspNotification(childStdin: NodeJS.WritableStream, method: string, params: unknown): void {
455
- const message = JSON.stringify({ jsonrpc: '2.0', method, params });
456
- const header = `Content-Length: ${Buffer.byteLength(message, 'utf-8')}\r\n\r\n`;
457
- childStdin.write(header + message, 'utf-8');
458
- }
459
478
 
460
479
  export async function serverRunAction(portArg: number | string | undefined, options: RunServerOptions): Promise<void> {
461
480
  if (process.env.OML_PLATFORM_API_KEY?.trim()) {
@@ -486,13 +505,23 @@ export async function serverRunAction(portArg: number | string | undefined, opti
486
505
  args.push(`--workspace=${path.resolve(options.workspace)}`);
487
506
  }
488
507
  args.push(`--token=${options.auth.accessToken}`);
508
+ if (options.entitlementCache) {
509
+ args.push(`--entitlement-expiry=${options.entitlementCache.expiry}`);
510
+ args.push(`--entitlement-features=${JSON.stringify(options.entitlementCache.featureIds)}`);
511
+ if (options.entitlementCache.token) {
512
+ args.push(`--entitlement-token=${options.entitlementCache.token}`);
513
+ }
514
+ }
515
+ if (options.deviceId) {
516
+ args.push(`--device-id=${options.deviceId}`);
517
+ }
489
518
 
490
519
  const child = spawn(process.execPath, args, {
491
520
  detached: false,
492
- stdio: ['pipe', 'inherit', 'inherit'],
521
+ stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
493
522
  });
494
523
 
495
- if (!child.pid || !child.stdin) {
524
+ if (!child.pid) {
496
525
  throw new Error('Failed to launch server process.');
497
526
  }
498
527
 
@@ -510,7 +539,17 @@ export async function serverRunAction(portArg: number | string | undefined, opti
510
539
  process.stdout.write(`OML server running on http://${DEFAULT_HOST}:${state.port} (pid ${state.pid})\n`);
511
540
  process.stdout.write('Press Ctrl-C to stop.\n');
512
541
 
513
- const stdin = child.stdin;
542
+ child.on('message', (msg: unknown) => {
543
+ if (msg && typeof msg === 'object' && (msg as Record<string, unknown>).type === 'entitlementsCached') {
544
+ const { expiry, featureIds, entitlementsToken } = msg as { expiry?: unknown; featureIds?: unknown; entitlementsToken?: unknown };
545
+ const expiryNum = Number(expiry);
546
+ const token = typeof entitlementsToken === 'string' ? entitlementsToken : undefined;
547
+ if (Number.isFinite(expiryNum) && Array.isArray(featureIds) && featureIds.every((x) => typeof x === 'string')) {
548
+ void options.authService.saveEntitlementCache(expiryNum, featureIds as string[], token);
549
+ }
550
+ }
551
+ });
552
+
514
553
  const REFRESH_INTERVAL_MS = 60 * 60 * 1000;
515
554
  const REFRESH_RETRY_BASE_MS = 15_000;
516
555
  const REFRESH_RETRY_MAX_MS = 5 * 60 * 1000;
@@ -538,7 +577,7 @@ export async function serverRunAction(portArg: number | string | undefined, opti
538
577
  const snapshot = await options.authService.getServerAuthSnapshot();
539
578
  if (snapshot.accessToken !== currentAccessToken) {
540
579
  currentAccessToken = snapshot.accessToken;
541
- sendLspNotification(stdin, '$/tokenRefreshed', { accessToken: snapshot.accessToken });
580
+ child.send({ type: 'tokenRefreshed', accessToken: snapshot.accessToken });
542
581
  }
543
582
  refreshFailureCount = 0;
544
583
  scheduleRefresh(REFRESH_INTERVAL_MS);
@@ -14,6 +14,7 @@ const CLI_DEBUG_ENABLED = false;
14
14
  type ServerState = {
15
15
  pid: number;
16
16
  port: number;
17
+ owner?: 'cli' | 'extension' | 'unknown';
17
18
  };
18
19
 
19
20
  type Envelope<T> = {
@@ -76,7 +77,7 @@ function isProcessAlive(pid: number): boolean {
76
77
 
77
78
  function parseServerState(raw: string): ServerState | undefined {
78
79
  try {
79
- const parsed = JSON.parse(raw) as { pid?: unknown; port?: unknown };
80
+ const parsed = JSON.parse(raw) as { pid?: unknown; port?: unknown; owner?: unknown };
80
81
  const pid = Number(parsed.pid);
81
82
  const port = Number(parsed.port);
82
83
  if (!Number.isFinite(pid) || !Number.isFinite(port)) {
@@ -87,7 +88,9 @@ function parseServerState(raw: string): ServerState | undefined {
87
88
  if (pidInt <= 0 || portInt <= 0 || portInt > 65535) {
88
89
  return undefined;
89
90
  }
90
- return { pid: pidInt, port: portInt };
91
+ const owner = typeof parsed.owner === 'string' ? parsed.owner.trim().toLowerCase() : '';
92
+ const normalizedOwner = owner === 'cli' || owner === 'extension' ? owner : 'unknown';
93
+ return { pid: pidInt, port: portInt, owner: normalizedOwner };
91
94
  } catch {
92
95
  return undefined;
93
96
  }
@@ -178,7 +181,8 @@ function buildResponseLike(response: HttpJsonResponse): { ok: boolean; status: n
178
181
 
179
182
  export async function restGet<T>(route: string, authToken?: string): Promise<T> {
180
183
  void authToken;
181
- const baseUrl = ensureServerBaseUrl(await readRunningState());
184
+ const state = await readRunningState();
185
+ const baseUrl = ensureServerBaseUrl(state);
182
186
  debugCli('rest.get.begin', { route, baseUrl });
183
187
  let response: HttpJsonResponse;
184
188
  try {
@@ -190,7 +194,7 @@ export async function restGet<T>(route: string, authToken?: string): Promise<T>
190
194
  const responseLike = buildResponseLike(response);
191
195
  const payload = parseJsonResponse<T & ErrorEnvelope>(response.status, response.statusText, response.body);
192
196
  if (!responseLike.ok) {
193
- const message = normalizeServerError(payload.error);
197
+ const message = normalizeServerError(payload.error, state?.owner);
194
198
  debugCli('rest.get.failed', { route, status: responseLike.status, message: message ?? 'n/a' });
195
199
  throw new Error(message ?? `Server request failed: GET ${route} (${responseLike.status}).`);
196
200
  }
@@ -200,7 +204,8 @@ export async function restGet<T>(route: string, authToken?: string): Promise<T>
200
204
 
201
205
  export async function restPost<T>(route: string, body: Record<string, unknown>, authToken?: string): Promise<T> {
202
206
  void authToken;
203
- const baseUrl = ensureServerBaseUrl(await readRunningState());
207
+ const state = await readRunningState();
208
+ const baseUrl = ensureServerBaseUrl(state);
204
209
  debugCli('rest.post.begin', { route, baseUrl });
205
210
  let response: HttpJsonResponse;
206
211
  try {
@@ -212,12 +217,12 @@ export async function restPost<T>(route: string, body: Record<string, unknown>,
212
217
  const responseLike = buildResponseLike(response);
213
218
  const payload = parseJsonResponse<Envelope<T> & ErrorEnvelope>(response.status, response.statusText, response.body);
214
219
  if (!responseLike.ok) {
215
- const message = normalizeServerError(payload.error);
220
+ const message = normalizeServerError(payload.error, state?.owner);
216
221
  debugCli('rest.post.failed', { route, status: responseLike.status, message: message ?? 'n/a' });
217
222
  throw new Error(message ?? `Server request failed: POST ${route} (${responseLike.status}).`);
218
223
  }
219
224
  if (payload.ok === false) {
220
- const message = normalizeServerError(payload.error);
225
+ const message = normalizeServerError(payload.error, state?.owner);
221
226
  debugCli('rest.post.notok', { route, message: message ?? 'n/a' });
222
227
  throw new Error(message ?? `Server request failed: POST ${route}.`);
223
228
  }
@@ -229,14 +234,17 @@ export async function restPost<T>(route: string, body: Record<string, unknown>,
229
234
  return payload.result;
230
235
  }
231
236
 
232
- function normalizeServerError(error: ErrorEnvelope['error']): string | undefined {
237
+ function normalizeServerError(error: ErrorEnvelope['error'], serverOwner?: ServerState['owner']): string | undefined {
233
238
  if (typeof error === 'string' && error.trim().length > 0) {
234
239
  return error.trim();
235
240
  }
236
241
  if (error && typeof error === 'object') {
237
242
  const code = typeof error.code === 'string' ? error.code.trim().toLowerCase() : '';
238
243
  if (code === 'auth_required') {
239
- return "OML server authentication is required. Sign in and restart the server with 'oml start'.";
244
+ if (serverOwner === 'extension') {
245
+ return 'OML server authentication is required. Sign in using the OML Code extension and try again.';
246
+ }
247
+ return "OML server authentication is required. Sign in using the CLI ('oml login') and try again.";
240
248
  }
241
249
  if (code === 'entitlements_pending') {
242
250
  return 'OML entitlements are still loading. Retry in a moment.';
@@ -31,7 +31,7 @@ export const validateAction = async (opts: ValidateOptions): Promise<void> => {
31
31
  }>('/v0/validate', {}, opts.authToken);
32
32
 
33
33
  if (result.filesChecked === 0) {
34
- console.log(chalk.yellow('No markdown/table-editor targets found in server workspace.'));
34
+ console.log(chalk.yellow('No SHACL validation targets found in server workspace.'));
35
35
  return;
36
36
  }
37
37
  printLintDiagnostics({
@@ -77,16 +77,18 @@ export const validateAction = async (opts: ValidateOptions): Promise<void> => {
77
77
  if (result.errors > 0) {
78
78
  const failingItems = Number(result.failingItems ?? 0);
79
79
  if (failingItems > 0) {
80
- failCli(chalk.red(`validate: ${result.filesChecked} table-editor block(s) scanned [${formatDuration(validateElapsedMs)}]; ${failingItems} failing block(s), ${result.errors} error(s), ${result.warnings} warning(s)`));
80
+ failCli(chalk.red(`validate: ${result.filesChecked} SHACL target(s) scanned [${formatDuration(validateElapsedMs)}]; ${failingItems} failing block(s), ${result.errors} error(s), ${result.warnings} warning(s)`));
81
81
  }
82
- failCli(chalk.red(`validate: ${result.filesChecked} item(s) checked with ${result.errors} error(s) and ${result.warnings} warning(s). [${formatDuration(validateElapsedMs)}]`));
82
+ failCli(chalk.red(`validate: ${result.filesChecked} SHACL target(s) checked with ${result.errors} error(s) and ${result.warnings} warning(s). [${formatDuration(validateElapsedMs)}]`));
83
83
  }
84
84
  if (result.warnings > 0) {
85
85
  const failingItems = Number(result.failingItems ?? 0);
86
86
  if (failingItems > 0) {
87
- failCli(chalk.yellow(`validate: ${result.filesChecked} table-editor block(s) scanned [${formatDuration(validateElapsedMs)}]; ${failingItems} failing block(s), ${result.errors} error(s), ${result.warnings} warning(s)`));
87
+ console.warn(chalk.yellow(`validate: ${result.filesChecked} SHACL target(s) scanned [${formatDuration(validateElapsedMs)}]; ${failingItems} failing block(s), ${result.errors} error(s), ${result.warnings} warning(s)`));
88
+ } else {
89
+ console.warn(chalk.yellow(`validate: ${result.filesChecked} SHACL target(s) checked with ${result.warnings} warning(s). [${formatDuration(validateElapsedMs)}]`));
88
90
  }
89
- failCli(chalk.yellow(`validate: ${result.filesChecked} item(s) checked with ${result.warnings} warning(s). [${formatDuration(validateElapsedMs)}]`));
91
+ return;
90
92
  }
91
- console.log(chalk.green(`validate: ${result.filesChecked} table-editor block(s) scanned [${formatDuration(validateElapsedMs)}]`));
93
+ console.log(chalk.green(`validate: ${result.filesChecked} SHACL target(s) scanned [${formatDuration(validateElapsedMs)}]`));
92
94
  };