@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/README.md +36 -36
- package/out/auth/auth.d.ts +10 -0
- package/out/auth/auth.js +250 -137
- package/out/auth/auth.js.map +1 -1
- package/out/cli.js +48 -26
- package/out/cli.js.map +1 -1
- package/out/commands/export.d.ts +0 -1
- package/out/commands/export.js +0 -1
- package/out/commands/export.js.map +1 -1
- package/out/commands/lint.js +2 -1
- package/out/commands/lint.js.map +1 -1
- package/out/commands/reason.d.ts +0 -1
- package/out/commands/reason.js +20 -7
- package/out/commands/reason.js.map +1 -1
- package/out/commands/render.d.ts +0 -1
- package/out/commands/render.js.map +1 -1
- package/out/commands/server/actions.d.ts +8 -0
- package/out/commands/server/actions.js +38 -10
- package/out/commands/server/actions.js.map +1 -1
- package/out/commands/server/rest.js +15 -8
- package/out/commands/server/rest.js.map +1 -1
- package/out/commands/validate.js +9 -6
- package/out/commands/validate.js.map +1 -1
- package/package.json +6 -4
- package/src/auth/auth.ts +265 -153
- package/src/cli.ts +55 -35
- package/src/commands/export.ts +0 -2
- package/src/commands/lint.ts +2 -1
- package/src/commands/reason.ts +21 -10
- package/src/commands/render.ts +0 -1
- package/src/commands/server/actions.ts +49 -10
- package/src/commands/server/rest.ts +17 -9
- package/src/commands/validate.ts +8 -6
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> [
|
|
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>] [--
|
|
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>] [--
|
|
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
|
-
.
|
|
150
|
-
.
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
|
326
|
-
'
|
|
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
|
-
|
|
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);
|
package/src/commands/export.ts
CHANGED
|
@@ -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,
|
package/src/commands/lint.ts
CHANGED
|
@@ -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
|
-
|
|
74
|
+
console.warn(chalk.yellow(formatLintSummary(result, elapsedMs)));
|
|
75
|
+
return;
|
|
75
76
|
}
|
|
76
77
|
console.log(chalk.green(formatLintSummary(result, elapsedMs)));
|
|
77
78
|
};
|
package/src/commands/reason.ts
CHANGED
|
@@ -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
|
|
154
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|
package/src/commands/render.ts
CHANGED
|
@@ -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
|
|
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: ['
|
|
521
|
+
stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
|
|
493
522
|
});
|
|
494
523
|
|
|
495
|
-
if (!child.pid
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.';
|
package/src/commands/validate.ts
CHANGED
|
@@ -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
|
|
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}
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
91
|
+
return;
|
|
90
92
|
}
|
|
91
|
-
console.log(chalk.green(`validate: ${result.filesChecked}
|
|
93
|
+
console.log(chalk.green(`validate: ${result.filesChecked} SHACL target(s) scanned [${formatDuration(validateElapsedMs)}]`));
|
|
92
94
|
};
|