@oml/cli 0.14.16 → 0.15.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.
@@ -17,12 +17,19 @@ const SHUTDOWN_TIMEOUT_MS = 3000;
17
17
  const POLL_INTERVAL_MS = 100;
18
18
  const execFileAsync = promisify(execFile);
19
19
 
20
+ type EntitlementCache = {
21
+ expiry: number;
22
+ featureIds: string[];
23
+ token?: string;
24
+ };
25
+
20
26
  type StartServerOptions = {
21
27
  port?: number | string;
22
28
  workspace?: string;
23
29
  auth?: {
24
30
  accessToken: string;
25
31
  };
32
+ entitlementCache?: EntitlementCache;
26
33
  };
27
34
 
28
35
  type ServerStatePaths = {
@@ -34,7 +41,7 @@ type ServerStatePaths = {
34
41
  type ServerLockState = {
35
42
  pid: number;
36
43
  port: number;
37
- owner?: 'daemon' | 'extension';
44
+ owner?: 'cli' | 'extension';
38
45
  workspaceRoot?: string;
39
46
  };
40
47
 
@@ -63,6 +70,7 @@ function getServerStatePaths(workspace?: string): ServerStatePaths {
63
70
 
64
71
  async function cleanupStateFile(paths: ServerStatePaths): Promise<void> {
65
72
  await fs.rm(paths.lockFile, { force: true });
73
+ await fs.rm(paths.dir, { recursive: true, force: true });
66
74
  }
67
75
 
68
76
  function parseServerLock(raw: string): ServerLockState | undefined {
@@ -78,7 +86,7 @@ function parseServerLock(raw: string): ServerLockState | undefined {
78
86
  if (pidInt <= 0 || portInt <= 0 || portInt > 65535) {
79
87
  return undefined;
80
88
  }
81
- const owner = parsed.owner === 'daemon' || parsed.owner === 'extension'
89
+ const owner = parsed.owner === 'cli' || parsed.owner === 'extension'
82
90
  ? parsed.owner
83
91
  : undefined;
84
92
  const workspaceRoot = typeof parsed.workspaceRoot === 'string' && parsed.workspaceRoot.trim().length > 0
@@ -105,7 +113,7 @@ async function readServerLock(lockFile: string): Promise<ServerLockState | undef
105
113
  type RunningServer = {
106
114
  pid: number;
107
115
  port: number;
108
- owner: 'daemon' | 'extension' | 'unknown';
116
+ owner: 'cli' | 'extension' | 'unknown';
109
117
  workspaceRoot?: string;
110
118
  lockFile: string;
111
119
  };
@@ -128,6 +136,7 @@ async function listRunningServers(): Promise<RunningServer[]> {
128
136
  }
129
137
  if (!isProcessAlive(state.pid)) {
130
138
  await fs.rm(lockFile, { force: true });
139
+ await fs.rm(path.dirname(lockFile), { recursive: true, force: true });
131
140
  continue;
132
141
  }
133
142
  servers.push({
@@ -257,7 +266,7 @@ function normalizeStartPort(value: number | string | undefined): number {
257
266
  return intPort;
258
267
  }
259
268
 
260
- async function inspectListeningProcess(port: number): Promise<string> {
269
+ async function inspectListeningProcess(port: number): Promise<string | undefined> {
261
270
  const args = ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN'];
262
271
  try {
263
272
  const { stdout } = await execFileAsync('lsof', args);
@@ -265,7 +274,7 @@ async function inspectListeningProcess(port: number): Promise<string> {
265
274
  } catch (error) {
266
275
  const failed = error as NodeJS.ErrnoException & { stdout?: string };
267
276
  if (failed?.code === 'ENOENT') {
268
- throw new Error("Cannot inspect ports because 'lsof' is not installed or not on PATH.");
277
+ return undefined;
269
278
  }
270
279
  return typeof failed?.stdout === 'string' ? failed.stdout.trim() : '';
271
280
  }
@@ -279,6 +288,7 @@ function spawnServerProcess(
279
288
  options: {
280
289
  workspace?: string;
281
290
  auth?: { accessToken: string };
291
+ entitlementCache?: EntitlementCache;
282
292
  },
283
293
  ): ChildProcess {
284
294
  const args = [serverMainScript, `--port=${port}`];
@@ -289,6 +299,13 @@ function spawnServerProcess(
289
299
  if (options.auth?.accessToken) {
290
300
  args.push(`--token=${options.auth.accessToken}`);
291
301
  }
302
+ if (options.entitlementCache) {
303
+ args.push(`--entitlement-expiry=${options.entitlementCache.expiry}`);
304
+ args.push(`--entitlement-features=${JSON.stringify(options.entitlementCache.featureIds)}`);
305
+ if (options.entitlementCache.token) {
306
+ args.push(`--entitlement-token=${options.entitlementCache.token}`);
307
+ }
308
+ }
292
309
 
293
310
  return spawn(process.execPath, args, {
294
311
  detached,
@@ -352,6 +369,7 @@ async function serverStartDetached(
352
369
  const child = spawnServerProcess(serverMainScript, port, true, 'ignore', {
353
370
  workspace: options.workspace,
354
371
  auth: options.auth,
372
+ entitlementCache: options.entitlementCache,
355
373
  });
356
374
  if (!child.pid) {
357
375
  throw new Error('Failed to launch server process.');
@@ -392,7 +410,7 @@ export async function serverStopAction(): Promise<void> {
392
410
  return;
393
411
  }
394
412
  const owner = state.owner ?? 'unknown';
395
- if (owner !== 'daemon') {
413
+ if (owner !== 'cli') {
396
414
  process.stdout.write(`Server is running (pid ${state.pid}, port ${state.port}) and managed by ${owner}; not stopping.\n`);
397
415
  return;
398
416
  }
@@ -417,6 +435,11 @@ export async function serverStatusAction(): Promise<void> {
417
435
  return;
418
436
  }
419
437
  const output = await inspectListeningProcess(state.port);
438
+ if (output === undefined) {
439
+ const owner = state.owner ?? 'unknown';
440
+ process.stdout.write(`Server is running on http://${DEFAULT_HOST}:${state.port} (pid ${state.pid}, owner ${owner}).\n`);
441
+ return;
442
+ }
420
443
  if (!output) {
421
444
  process.stdout.write(`No listening process found on port ${state.port}.\n`);
422
445
  return;
@@ -444,13 +467,10 @@ export type RunServerOptions = {
444
467
  auth: {
445
468
  accessToken: string;
446
469
  };
470
+ entitlementCache?: EntitlementCache;
471
+ deviceId?: string;
447
472
  };
448
473
 
449
- function sendLspNotification(childStdin: NodeJS.WritableStream, method: string, params: unknown): void {
450
- const message = JSON.stringify({ jsonrpc: '2.0', method, params });
451
- const header = `Content-Length: ${Buffer.byteLength(message, 'utf-8')}\r\n\r\n`;
452
- childStdin.write(header + message, 'utf-8');
453
- }
454
474
 
455
475
  export async function serverRunAction(portArg: number | string | undefined, options: RunServerOptions): Promise<void> {
456
476
  if (process.env.OML_PLATFORM_API_KEY?.trim()) {
@@ -481,13 +501,23 @@ export async function serverRunAction(portArg: number | string | undefined, opti
481
501
  args.push(`--workspace=${path.resolve(options.workspace)}`);
482
502
  }
483
503
  args.push(`--token=${options.auth.accessToken}`);
504
+ if (options.entitlementCache) {
505
+ args.push(`--entitlement-expiry=${options.entitlementCache.expiry}`);
506
+ args.push(`--entitlement-features=${JSON.stringify(options.entitlementCache.featureIds)}`);
507
+ if (options.entitlementCache.token) {
508
+ args.push(`--entitlement-token=${options.entitlementCache.token}`);
509
+ }
510
+ }
511
+ if (options.deviceId) {
512
+ args.push(`--device-id=${options.deviceId}`);
513
+ }
484
514
 
485
515
  const child = spawn(process.execPath, args, {
486
516
  detached: false,
487
- stdio: ['pipe', 'inherit', 'inherit'],
517
+ stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
488
518
  });
489
519
 
490
- if (!child.pid || !child.stdin) {
520
+ if (!child.pid) {
491
521
  throw new Error('Failed to launch server process.');
492
522
  }
493
523
 
@@ -505,7 +535,17 @@ export async function serverRunAction(portArg: number | string | undefined, opti
505
535
  process.stdout.write(`OML server running on http://${DEFAULT_HOST}:${state.port} (pid ${state.pid})\n`);
506
536
  process.stdout.write('Press Ctrl-C to stop.\n');
507
537
 
508
- const stdin = child.stdin;
538
+ child.on('message', (msg: unknown) => {
539
+ if (msg && typeof msg === 'object' && (msg as Record<string, unknown>).type === 'entitlementsCached') {
540
+ const { expiry, featureIds, entitlementsToken } = msg as { expiry?: unknown; featureIds?: unknown; entitlementsToken?: unknown };
541
+ const expiryNum = Number(expiry);
542
+ const token = typeof entitlementsToken === 'string' ? entitlementsToken : undefined;
543
+ if (Number.isFinite(expiryNum) && Array.isArray(featureIds) && featureIds.every((x) => typeof x === 'string')) {
544
+ void options.authService.saveEntitlementCache(expiryNum, featureIds as string[], token);
545
+ }
546
+ }
547
+ });
548
+
509
549
  const REFRESH_INTERVAL_MS = 60 * 60 * 1000;
510
550
  const REFRESH_RETRY_BASE_MS = 15_000;
511
551
  const REFRESH_RETRY_MAX_MS = 5 * 60 * 1000;
@@ -533,7 +573,7 @@ export async function serverRunAction(portArg: number | string | undefined, opti
533
573
  const snapshot = await options.authService.getServerAuthSnapshot();
534
574
  if (snapshot.accessToken !== currentAccessToken) {
535
575
  currentAccessToken = snapshot.accessToken;
536
- sendLspNotification(stdin, '$/tokenRefreshed', { accessToken: snapshot.accessToken });
576
+ child.send({ type: 'tokenRefreshed', accessToken: snapshot.accessToken });
537
577
  }
538
578
  refreshFailureCount = 0;
539
579
  scheduleRefresh(REFRESH_INTERVAL_MS);
@@ -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
  };