@oml/cli 0.7.0 → 0.9.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 +14 -7
- package/out/auth.d.ts +3 -17
- package/out/auth.js +64 -69
- package/out/auth.js.map +1 -1
- package/out/cli.js +69 -14
- package/out/cli.js.map +1 -1
- package/out/commands/compile.js +1 -1
- package/out/commands/compile.js.map +1 -1
- package/out/commands/lint.d.ts +1 -1
- package/out/commands/lint.js +2 -6
- package/out/commands/lint.js.map +1 -1
- package/out/commands/render.js +0 -17
- package/out/commands/render.js.map +1 -1
- package/out/platform-constants.d.ts +3 -0
- package/out/platform-constants.js +5 -0
- package/out/platform-constants.js.map +1 -0
- package/out/platform.d.ts +28 -0
- package/out/platform.js +81 -0
- package/out/platform.js.map +1 -0
- package/package.json +7 -5
- package/src/auth.ts +82 -80
- package/src/cli.ts +63 -14
- package/src/commands/compile.ts +1 -1
- package/src/commands/lint.ts +2 -6
- package/src/commands/render.ts +0 -18
- package/src/platform-constants.ts +5 -0
- package/src/platform.ts +102 -0
package/src/auth.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
2
|
|
|
3
|
+
import { exchangeGitHubToken, refreshSupabaseAccessToken } from '@oml/platform';
|
|
3
4
|
import chalk from 'chalk';
|
|
4
5
|
import * as fs from 'node:fs/promises';
|
|
5
6
|
import * as os from 'node:os';
|
|
6
7
|
import * as path from 'node:path';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_API_BASE_URL,
|
|
10
|
+
DEFAULT_SUPABASE_ANON_KEY,
|
|
11
|
+
DEFAULT_SUPABASE_URL
|
|
12
|
+
} from './platform-constants.js';
|
|
10
13
|
const GITHUB_DEVICE_CODE_URL = 'https://github.com/login/device/code';
|
|
11
14
|
const GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token';
|
|
12
15
|
const GITHUB_USER_URL = 'https://api.github.com/user';
|
|
13
16
|
const DEFAULT_GITHUB_CLIENT_ID = 'Ov23liQkHYczdOAvHp5P';
|
|
17
|
+
const API_BASE_URL_ENV = 'OML_PLATFORM_API_URL';
|
|
18
|
+
const SUPABASE_URL_ENV = 'OML_SUPABASE_URL';
|
|
19
|
+
const SUPABASE_ANON_KEY_ENV = 'OML_SUPABASE_ANON_KEY';
|
|
14
20
|
|
|
15
21
|
type Provider = 'github';
|
|
16
22
|
|
|
@@ -18,37 +24,29 @@ type StoredSession = {
|
|
|
18
24
|
provider: Provider;
|
|
19
25
|
userId: string;
|
|
20
26
|
userLabel?: string;
|
|
27
|
+
email: string | null;
|
|
28
|
+
tier?: string;
|
|
29
|
+
accessToken: string;
|
|
30
|
+
refreshToken: string;
|
|
31
|
+
tokenType: string;
|
|
32
|
+
expiresIn: number;
|
|
21
33
|
signedInAt: string;
|
|
22
34
|
};
|
|
23
35
|
|
|
24
36
|
type LoginOptions = {
|
|
25
37
|
};
|
|
26
38
|
|
|
27
|
-
type AuthStatus = {
|
|
28
|
-
session: StoredSession;
|
|
29
|
-
authorized: boolean;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
39
|
export class OmlCliAuthService {
|
|
33
|
-
private whitelistCache: Set<string> | null = null;
|
|
34
|
-
private whitelistCacheTime = 0;
|
|
35
40
|
|
|
36
41
|
async login(options: LoginOptions): Promise<void> {
|
|
37
42
|
const session = await this.authenticate();
|
|
38
|
-
|
|
39
|
-
const authorized = await this.isAuthorized(session);
|
|
40
|
-
if (!authorized) {
|
|
41
|
-
throw new Error('Sign in succeeded, but this account is not authorized.');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
43
|
await writeSession(session);
|
|
45
|
-
const summary = session.userLabel
|
|
44
|
+
const summary = session.userLabel ?? session.email ?? 'signed-in user';
|
|
46
45
|
console.error(chalk.green(`Signed in as ${summary} via ${session.provider}.`));
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
async logout(): Promise<void> {
|
|
50
49
|
await deleteSession();
|
|
51
|
-
this.clearWhitelistCache();
|
|
52
50
|
console.error(chalk.green('Signed out.'));
|
|
53
51
|
}
|
|
54
52
|
|
|
@@ -58,69 +56,57 @@ export class OmlCliAuthService {
|
|
|
58
56
|
console.error(chalk.yellow('Not signed in.'));
|
|
59
57
|
return;
|
|
60
58
|
}
|
|
61
|
-
const authorized = await this.isAuthorized(session);
|
|
62
59
|
console.error(`Provider: ${session.provider}`);
|
|
63
60
|
console.error(`User ID: ${session.userId}`);
|
|
64
61
|
console.error(`User label: ${session.userLabel ?? '(not set)'}`);
|
|
62
|
+
console.error(`Email: ${session.email ?? '(not set)'}`);
|
|
63
|
+
console.error(`Tier: ${session.tier ?? '(not set)'}`);
|
|
65
64
|
console.error(`Signed in at: ${session.signedInAt}`);
|
|
66
|
-
console.error(`Authorized: ${authorized ? 'yes' : 'no'}`);
|
|
67
65
|
}
|
|
68
66
|
|
|
69
|
-
async ensureAuthenticated(operationName: string): Promise<
|
|
67
|
+
async ensureAuthenticated(operationName: string): Promise<void> {
|
|
70
68
|
const session = await readSession();
|
|
71
69
|
if (!session) {
|
|
72
70
|
throw new Error(`${operationName} requires authentication. Run 'oml login' first.`);
|
|
73
71
|
}
|
|
74
|
-
|
|
75
|
-
const authorized = await this.isAuthorized(session);
|
|
76
|
-
if (!authorized) {
|
|
77
|
-
throw new Error(`${operationName} requires authorization. Your account is not authorized.`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return { session, authorized };
|
|
81
72
|
}
|
|
82
73
|
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
if (!
|
|
86
|
-
|
|
74
|
+
async getAccessToken(): Promise<string> {
|
|
75
|
+
const session = await readSession();
|
|
76
|
+
if (!session?.accessToken) {
|
|
77
|
+
throw new Error('OML CLI authentication is required. Run \'oml login\' first.');
|
|
87
78
|
}
|
|
88
|
-
|
|
89
|
-
return whitelist.has(userId);
|
|
79
|
+
return session.accessToken;
|
|
90
80
|
}
|
|
91
81
|
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
if (!
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const now = Date.now();
|
|
99
|
-
if (this.whitelistCache && (now - this.whitelistCacheTime) < CACHE_DURATION_MS) {
|
|
100
|
-
return this.whitelistCache;
|
|
82
|
+
async refreshAccessToken(): Promise<string> {
|
|
83
|
+
const session = await readSession();
|
|
84
|
+
if (!session?.refreshToken) {
|
|
85
|
+
throw new Error('OML CLI authentication is required. Run \'oml login\' first.');
|
|
101
86
|
}
|
|
102
87
|
|
|
88
|
+
let refreshed;
|
|
103
89
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
this.whitelistCacheTime = now;
|
|
112
|
-
return this.whitelistCache;
|
|
113
|
-
} catch (error) {
|
|
114
|
-
console.error(chalk.yellow(
|
|
115
|
-
`OML: Could not verify authorization. ${error instanceof Error ? error.message : String(error)}`
|
|
116
|
-
));
|
|
117
|
-
return null;
|
|
90
|
+
refreshed = await refreshSupabaseAccessToken(
|
|
91
|
+
resolveSupabaseUrl(),
|
|
92
|
+
resolveSupabaseAnonKey(),
|
|
93
|
+
session.refreshToken
|
|
94
|
+
);
|
|
95
|
+
} catch {
|
|
96
|
+
throw new Error('Authentication refresh failed. Check your network connection or sign in again with \'oml login\'.');
|
|
118
97
|
}
|
|
119
|
-
}
|
|
120
98
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
99
|
+
const updatedSession: StoredSession = {
|
|
100
|
+
...session,
|
|
101
|
+
accessToken: refreshed.access_token,
|
|
102
|
+
refreshToken: refreshed.refresh_token,
|
|
103
|
+
tokenType: refreshed.token_type,
|
|
104
|
+
expiresIn: refreshed.expires_in,
|
|
105
|
+
email: refreshed.email ?? session.email,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
await writeSession(updatedSession);
|
|
109
|
+
return updatedSession.accessToken;
|
|
124
110
|
}
|
|
125
111
|
|
|
126
112
|
private async authenticate(): Promise<StoredSession> {
|
|
@@ -128,22 +114,6 @@ export class OmlCliAuthService {
|
|
|
128
114
|
}
|
|
129
115
|
}
|
|
130
116
|
|
|
131
|
-
function extractWhitelistEntries(data: unknown): string[] {
|
|
132
|
-
if (Array.isArray(data)) {
|
|
133
|
-
return data.map((value) => String(value));
|
|
134
|
-
}
|
|
135
|
-
if (typeof data === 'object' && data !== null) {
|
|
136
|
-
const record = data as { allowedUsers?: unknown; users?: unknown };
|
|
137
|
-
if (Array.isArray(record.allowedUsers)) {
|
|
138
|
-
return record.allowedUsers.map((value) => String(value));
|
|
139
|
-
}
|
|
140
|
-
if (Array.isArray(record.users)) {
|
|
141
|
-
return record.users.map((value) => String(value));
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
throw new Error('Invalid whitelist format. Expected array or object with "allowedUsers" or "users" property.');
|
|
145
|
-
}
|
|
146
|
-
|
|
147
117
|
async function readSession(): Promise<StoredSession | undefined> {
|
|
148
118
|
try {
|
|
149
119
|
const content = await fs.readFile(getSessionPath(), 'utf-8');
|
|
@@ -151,13 +121,25 @@ async function readSession(): Promise<StoredSession | undefined> {
|
|
|
151
121
|
if (!data.userId || !data.provider || !data.signedInAt) {
|
|
152
122
|
return undefined;
|
|
153
123
|
}
|
|
154
|
-
if (data.
|
|
124
|
+
if (!data.accessToken) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
if (!data.refreshToken || !data.tokenType || typeof data.expiresIn !== 'number') {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
if (data.provider !== 'github') {
|
|
155
131
|
return undefined;
|
|
156
132
|
}
|
|
157
133
|
return {
|
|
158
134
|
provider: data.provider,
|
|
159
135
|
userId: data.userId,
|
|
160
136
|
userLabel: data.userLabel,
|
|
137
|
+
email: data.email ?? null,
|
|
138
|
+
tier: data.tier,
|
|
139
|
+
accessToken: data.accessToken,
|
|
140
|
+
refreshToken: data.refreshToken,
|
|
141
|
+
tokenType: data.tokenType,
|
|
142
|
+
expiresIn: data.expiresIn,
|
|
161
143
|
signedInAt: data.signedInAt
|
|
162
144
|
};
|
|
163
145
|
} catch {
|
|
@@ -189,7 +171,7 @@ async function authenticateWithGitHub(): Promise<StoredSession> {
|
|
|
189
171
|
const clientId = resolveClientId();
|
|
190
172
|
const params = new URLSearchParams({
|
|
191
173
|
client_id: clientId,
|
|
192
|
-
scope: 'read:user'
|
|
174
|
+
scope: 'read:user user:email'
|
|
193
175
|
});
|
|
194
176
|
const response = await fetch(GITHUB_DEVICE_CODE_URL, {
|
|
195
177
|
method: 'POST',
|
|
@@ -223,10 +205,18 @@ async function authenticateWithGitHub(): Promise<StoredSession> {
|
|
|
223
205
|
throw new Error('GitHub user lookup did not return a login name.');
|
|
224
206
|
}
|
|
225
207
|
|
|
208
|
+
const platformSession = await exchangeGitHubToken(resolveApiBaseUrl(), token);
|
|
209
|
+
|
|
226
210
|
return {
|
|
227
211
|
provider: 'github',
|
|
228
|
-
userId:
|
|
212
|
+
userId: platformSession.user_id,
|
|
229
213
|
userLabel: user.login,
|
|
214
|
+
email: platformSession.email,
|
|
215
|
+
tier: platformSession.tier,
|
|
216
|
+
accessToken: platformSession.access_token,
|
|
217
|
+
refreshToken: platformSession.refresh_token,
|
|
218
|
+
tokenType: platformSession.token_type,
|
|
219
|
+
expiresIn: platformSession.expires_in,
|
|
230
220
|
signedInAt: new Date().toISOString()
|
|
231
221
|
};
|
|
232
222
|
}
|
|
@@ -292,6 +282,18 @@ function resolveClientId(): string {
|
|
|
292
282
|
);
|
|
293
283
|
}
|
|
294
284
|
|
|
285
|
+
function resolveApiBaseUrl(): string {
|
|
286
|
+
return process.env[API_BASE_URL_ENV]?.trim() || DEFAULT_API_BASE_URL;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function resolveSupabaseUrl(): string {
|
|
290
|
+
return process.env[SUPABASE_URL_ENV]?.trim() || DEFAULT_SUPABASE_URL;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function resolveSupabaseAnonKey(): string {
|
|
294
|
+
return process.env[SUPABASE_ANON_KEY_ENV]?.trim() || DEFAULT_SUPABASE_ANON_KEY;
|
|
295
|
+
}
|
|
296
|
+
|
|
295
297
|
function delay(ms: number): Promise<void> {
|
|
296
298
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
297
299
|
}
|
package/src/cli.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
2
|
|
|
3
|
-
import { OmlLanguageMetaData } from '@oml/language';
|
|
4
3
|
import chalk from 'chalk';
|
|
5
4
|
import { Command } from 'commander';
|
|
6
5
|
import * as fs from 'node:fs/promises';
|
|
@@ -12,6 +11,7 @@ import { lintAction } from './commands/lint.js';
|
|
|
12
11
|
import { renderAction } from './commands/render.js';
|
|
13
12
|
import { notifyIfCliUpdateAvailable } from './update.js';
|
|
14
13
|
import { validateAction } from './commands/validate.js';
|
|
14
|
+
import { initializePlatform, disposePlatform, trackCommand } from './platform.js';
|
|
15
15
|
|
|
16
16
|
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
|
17
17
|
|
|
@@ -27,32 +27,39 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
|
|
|
27
27
|
|
|
28
28
|
program
|
|
29
29
|
.command('login')
|
|
30
|
-
.description('
|
|
30
|
+
.description('sign in with GitHub for CLI authorization')
|
|
31
31
|
.action(async () => {
|
|
32
32
|
await authService.login({});
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
program
|
|
36
36
|
.command('logout')
|
|
37
|
-
.description('remove the local
|
|
37
|
+
.description('remove the local sign-in session')
|
|
38
38
|
.action(async () => {
|
|
39
39
|
await authService.logout();
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
program
|
|
43
43
|
.command('whoami')
|
|
44
|
-
.description('print the current
|
|
44
|
+
.description('print the current sign-in session')
|
|
45
45
|
.action(async () => {
|
|
46
46
|
await authService.whoami();
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
const fileExtensions = OmlLanguageMetaData.fileExtensions.join(', ');
|
|
50
49
|
program
|
|
51
50
|
.command('lint')
|
|
52
|
-
.argument('[file]', `source file (possible file extensions: ${fileExtensions})`)
|
|
53
51
|
.option('-w, --workspace <dir>', 'workspace root used to resolve cross-file references', '.')
|
|
54
52
|
.description('lints OML files and prints any syntax or validation errors')
|
|
55
|
-
.action(
|
|
53
|
+
.action(async (...args: unknown[]) => {
|
|
54
|
+
const done = trackCommand('oml-lint');
|
|
55
|
+
try {
|
|
56
|
+
await lintAction(...args as Parameters<typeof lintAction>);
|
|
57
|
+
done();
|
|
58
|
+
} catch (err) {
|
|
59
|
+
done(err);
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
56
63
|
|
|
57
64
|
program
|
|
58
65
|
.command('render')
|
|
@@ -69,7 +76,16 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
|
|
|
69
76
|
.option('-e, --explanations [value]', 'enable or disable inconsistency explanations', parseBooleanOption, true)
|
|
70
77
|
.option('-p, --profile [value]', 'include phase timings in the reasoner result', parseBooleanOption, false)
|
|
71
78
|
.description('reason the workspace, then render markdown files under the selected markdown folder to static html and copy referenced non-markdown assets')
|
|
72
|
-
.action(
|
|
79
|
+
.action(async (...args: unknown[]) => {
|
|
80
|
+
const done = trackCommand('oml-render');
|
|
81
|
+
try {
|
|
82
|
+
await renderAction(...args as Parameters<typeof renderAction>);
|
|
83
|
+
done();
|
|
84
|
+
} catch (err) {
|
|
85
|
+
done(err);
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
73
89
|
|
|
74
90
|
program
|
|
75
91
|
.command('compile')
|
|
@@ -80,7 +96,16 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
|
|
|
80
96
|
.option('--only', 'skip lint and compile from the current workspace state')
|
|
81
97
|
.option('--pretty', 'pretty-print Turtle/TriG output with blank lines between top-level blocks')
|
|
82
98
|
.description('compile OML files to RDF and write them to an output folder')
|
|
83
|
-
.action(
|
|
99
|
+
.action(async (...args: unknown[]) => {
|
|
100
|
+
const done = trackCommand('oml-compile');
|
|
101
|
+
try {
|
|
102
|
+
await compileAction(...args as Parameters<typeof compileAction>);
|
|
103
|
+
done();
|
|
104
|
+
} catch (err) {
|
|
105
|
+
done(err);
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
84
109
|
|
|
85
110
|
program
|
|
86
111
|
.command('reason')
|
|
@@ -96,8 +121,15 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
|
|
|
96
121
|
.option('-p, --profile [value]', 'include phase timings in the reasoner result', parseBooleanOption, false)
|
|
97
122
|
.description('compile OML files, then run consistency checking for every ontology in dependency order')
|
|
98
123
|
.action(async (opts) => {
|
|
99
|
-
const
|
|
100
|
-
|
|
124
|
+
const done = trackCommand('oml-reason');
|
|
125
|
+
try {
|
|
126
|
+
const { reasonAction } = await import('./commands/reason.js');
|
|
127
|
+
await reasonAction(opts);
|
|
128
|
+
done();
|
|
129
|
+
} catch (err) {
|
|
130
|
+
done(err);
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
101
133
|
});
|
|
102
134
|
|
|
103
135
|
program
|
|
@@ -113,16 +145,33 @@ export async function runCli(argv: string[] = process.argv): Promise<void> {
|
|
|
113
145
|
.option('-e, --explanations [value]', 'enable or disable inconsistency explanations', parseBooleanOption, true)
|
|
114
146
|
.option('-p, --profile [value]', 'include phase timings in the reasoner result', parseBooleanOption, false)
|
|
115
147
|
.description('compile and reason the workspace, then validate nested markdown table-editor SHACL blocks against their context models')
|
|
116
|
-
.action(
|
|
148
|
+
.action(async (...args: unknown[]) => {
|
|
149
|
+
const done = trackCommand('oml-validate');
|
|
150
|
+
try {
|
|
151
|
+
await validateAction(...args as Parameters<typeof validateAction>);
|
|
152
|
+
done();
|
|
153
|
+
} catch (err) {
|
|
154
|
+
done(err);
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
117
158
|
|
|
118
159
|
program.hook('preAction', async (_thisCommand, actionCommand) => {
|
|
119
160
|
if (actionCommand.name() === 'login' || actionCommand.name() === 'logout' || actionCommand.name() === 'whoami') {
|
|
120
161
|
return;
|
|
121
162
|
}
|
|
122
|
-
|
|
163
|
+
// Require either GitHub auth or API key, then connect to platform
|
|
164
|
+
if (!process.env.OML_PLATFORM_API_KEY) {
|
|
165
|
+
await authService.ensureAuthenticated('OML CLI');
|
|
166
|
+
}
|
|
167
|
+
await initializePlatform(authService);
|
|
123
168
|
});
|
|
124
169
|
|
|
125
|
-
|
|
170
|
+
try {
|
|
171
|
+
await program.parseAsync(argv);
|
|
172
|
+
} finally {
|
|
173
|
+
await disposePlatform();
|
|
174
|
+
}
|
|
126
175
|
await updateCheck;
|
|
127
176
|
}
|
|
128
177
|
|
package/src/commands/compile.ts
CHANGED
|
@@ -25,7 +25,7 @@ export const compileAction = async (opts: CompileOptions): Promise<void> => {
|
|
|
25
25
|
const format = normalizeFormatExtension(opts.format);
|
|
26
26
|
|
|
27
27
|
if (!opts.only) {
|
|
28
|
-
await lintAction(
|
|
28
|
+
await lintAction({ workspace: workspaceRoot });
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const workspaceStat = await fs.stat(workspaceRoot).catch(() => undefined);
|
package/src/commands/lint.ts
CHANGED
|
@@ -9,20 +9,16 @@ export type LintOptions = {
|
|
|
9
9
|
workspaceRoot?: string
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
export const lintAction = async (
|
|
12
|
+
export const lintAction = async (opts: LintOptions): Promise<void> => {
|
|
13
13
|
const startedAt = Date.now();
|
|
14
14
|
const workspaceRoot = opts.workspace ?? opts.workspaceRoot ?? '.';
|
|
15
15
|
const backend = createBackend();
|
|
16
16
|
try {
|
|
17
|
-
const result = await backend.validate(
|
|
17
|
+
const result = await backend.validate(undefined, workspaceRoot);
|
|
18
18
|
if (result.filesChecked === 0) {
|
|
19
19
|
console.log(chalk.yellow(`No .oml files found under ${workspaceRoot}.`));
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
|
-
if (fileName && result.warnings === 0) {
|
|
23
|
-
console.log(chalk.green('lint: no syntax or validation errors found.'));
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
22
|
if (result.warnings > 0) {
|
|
27
23
|
console.log(chalk.yellow(`lint: ${result.filesChecked} OML file(s) checked with ${result.warnings} warning(s). [${formatDuration(Date.now() - startedAt)}]`));
|
|
28
24
|
process.exit(1);
|
package/src/commands/render.ts
CHANGED
|
@@ -37,7 +37,6 @@ const SUPPORTED_MD_BLOCK_KINDS = new Set<MdBlockKind>([
|
|
|
37
37
|
'table-editor'
|
|
38
38
|
]);
|
|
39
39
|
const LINK_ATTRIBUTE_KEYS = new Set(['href', 'src', 'xlinkHref', 'xlink:href']);
|
|
40
|
-
const RENDER_PROFILE = process.env.OML_RENDER_PROFILE === '1';
|
|
41
40
|
|
|
42
41
|
export type RenderOptions = {
|
|
43
42
|
workspace?: string,
|
|
@@ -163,7 +162,6 @@ export const renderAction = async (
|
|
|
163
162
|
console.log(chalk.yellow(`No markdown files found under ${markdownRoot}.`));
|
|
164
163
|
}
|
|
165
164
|
console.log(chalk.green(`render: ${renderableMarkdownFiles.length} markdown file(s) rendered in ${path.relative(process.cwd(), output) || output} [${formatDuration(Date.now() - renderStartedAt)}]`));
|
|
166
|
-
logRenderTiming('render.total', renderStartedAt, `${renderableMarkdownFiles.length} markdown file(s)`);
|
|
167
165
|
} finally {
|
|
168
166
|
await backend.dispose();
|
|
169
167
|
}
|
|
@@ -303,7 +301,6 @@ async function renderMarkdownFile(
|
|
|
303
301
|
explicitMarkdown?: string,
|
|
304
302
|
forcedModelUri?: string
|
|
305
303
|
): Promise<{ workspaceAssets: Set<string>; blockArtifactFiles: number }> {
|
|
306
|
-
const pageStartedAt = Date.now();
|
|
307
304
|
const markdown = explicitMarkdown ?? await fs.readFile(inputFile, 'utf-8');
|
|
308
305
|
const prepared = markdownRuntime.prepare(markdown);
|
|
309
306
|
const rewriteResult = rewriteRenderedLinks(prepared.renderedHtml, {
|
|
@@ -323,7 +320,6 @@ async function renderMarkdownFile(
|
|
|
323
320
|
modelUri,
|
|
324
321
|
blocks: executableBlocks
|
|
325
322
|
}, workspaceRoot)).results;
|
|
326
|
-
logRenderTiming('render.page.blocks', pageStartedAt, path.relative(workspaceRoot, inputFile));
|
|
327
323
|
const renderedBlockResults: RenderedBlockResult[] = blockResults.map((result) => ({
|
|
328
324
|
...result,
|
|
329
325
|
options: optionsByBlockId.get(result.blockId)
|
|
@@ -358,7 +354,6 @@ async function renderMarkdownFile(
|
|
|
358
354
|
currentPageStack
|
|
359
355
|
)
|
|
360
356
|
: {};
|
|
361
|
-
logRenderTiming('render.page.templates', pageStartedAt, path.relative(workspaceRoot, inputFile));
|
|
362
357
|
const runtimeScriptRelative = toRelativeWebPath(path.dirname(outputFile), runtimeScriptFile);
|
|
363
358
|
const stylesheetRelative = toRelativeWebPath(path.dirname(outputFile), stylesheetFile);
|
|
364
359
|
const wikiLinkHrefByKey = buildWikiLinkHrefMapForPage(wikiPageIndex, outputFile);
|
|
@@ -381,7 +376,6 @@ async function renderMarkdownFile(
|
|
|
381
376
|
for (const asset of blockRewriteResult.workspaceAssets) {
|
|
382
377
|
workspaceAssets.add(asset);
|
|
383
378
|
}
|
|
384
|
-
logRenderTiming('render.page.total', pageStartedAt, path.relative(workspaceRoot, inputFile));
|
|
385
379
|
return { workspaceAssets, blockArtifactFiles: blockArtifacts.count };
|
|
386
380
|
}
|
|
387
381
|
|
|
@@ -824,7 +818,6 @@ async function queryRdfTypesForIris(
|
|
|
824
818
|
modelUri: string,
|
|
825
819
|
iris: ReadonlyArray<string>
|
|
826
820
|
): Promise<Map<string, Set<string>>> {
|
|
827
|
-
const startedAt = Date.now();
|
|
828
821
|
const byIri = new Map<string, Set<string>>();
|
|
829
822
|
if (iris.length === 0) {
|
|
830
823
|
return byIri;
|
|
@@ -865,7 +858,6 @@ WHERE {
|
|
|
865
858
|
types.add(typeIri);
|
|
866
859
|
byIri.set(subject, types);
|
|
867
860
|
}
|
|
868
|
-
logRenderTiming('render.types', startedAt, `${iris.length} iri(s)`);
|
|
869
861
|
return byIri;
|
|
870
862
|
}
|
|
871
863
|
|
|
@@ -897,7 +889,6 @@ async function generateInstanceTemplatePages(
|
|
|
897
889
|
attemptedInstanceIris: Set<string>,
|
|
898
890
|
currentPageStack: Set<string>
|
|
899
891
|
): Promise<Record<string, string>> {
|
|
900
|
-
const startedAt = Date.now();
|
|
901
892
|
const iriToHref: Record<string, string> = {};
|
|
902
893
|
if (!templateGenerationEnabled) {
|
|
903
894
|
return iriToHref;
|
|
@@ -958,18 +949,9 @@ async function generateInstanceTemplatePages(
|
|
|
958
949
|
}
|
|
959
950
|
iriToHref[iri] = toRelativeWebPath(path.dirname(pageOutputFile), absolute);
|
|
960
951
|
}
|
|
961
|
-
logRenderTiming('render.wikilinks', startedAt, `${unresolved.length} unresolved iri(s)`);
|
|
962
952
|
return iriToHref;
|
|
963
953
|
}
|
|
964
954
|
|
|
965
|
-
function logRenderTiming(label: string, startedAt: number, detail: string): void {
|
|
966
|
-
if (!RENDER_PROFILE) {
|
|
967
|
-
return;
|
|
968
|
-
}
|
|
969
|
-
const elapsed = Date.now() - startedAt;
|
|
970
|
-
console.log(chalk.gray(`[render-profile] ${label} ${elapsed}ms ${detail}`));
|
|
971
|
-
}
|
|
972
|
-
|
|
973
955
|
function wrapHtml(
|
|
974
956
|
content: string,
|
|
975
957
|
runtimeScriptPath: string,
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_API_BASE_URL = 'https://oml-platform-worker.melaasar.workers.dev';
|
|
4
|
+
export const DEFAULT_SUPABASE_URL = 'https://lrbsnujufmasiyvslhmw.supabase.co';
|
|
5
|
+
export const DEFAULT_SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxyYnNudWp1Zm1hc2l5dnNsaG13Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzM3MjM3ODgsImV4cCI6MjA4OTI5OTc4OH0._gip9hlWPMbwxbS_zDIUXWLstWO7KWVev6HPU-5HVsw';
|
package/src/platform.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OML Platform integration for the CLI.
|
|
5
|
+
*
|
|
6
|
+
* The platform is the sole authorization mechanism. Users must have
|
|
7
|
+
* a valid API key configured and the platform must be reachable for
|
|
8
|
+
* commands to execute. No whitelist or other fallback is used.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { OmlClient, FileStorageAdapter } from '@oml/platform';
|
|
12
|
+
import type { OmlClientConfig } from '@oml/platform';
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { DEFAULT_API_BASE_URL } from './platform-constants.js';
|
|
15
|
+
import { OmlCliAuthService } from './auth.js';
|
|
16
|
+
const API_BASE_URL_ENV = 'OML_PLATFORM_API_URL';
|
|
17
|
+
|
|
18
|
+
let client: OmlClient | null = null;
|
|
19
|
+
|
|
20
|
+
export type CommandInvocationTracker = ReturnType<OmlClient['trackInvocation']>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Initialize the platform client. Call once during CLI startup.
|
|
24
|
+
* Uses OML_PLATFORM_API_KEY when present, otherwise the stored
|
|
25
|
+
* OAuth platform session from OML CLI login.
|
|
26
|
+
* Throws if the platform is unreachable or the chosen auth mode
|
|
27
|
+
* is not configured.
|
|
28
|
+
*/
|
|
29
|
+
export async function initializePlatform(
|
|
30
|
+
authService: OmlCliAuthService,
|
|
31
|
+
apiBaseUrl = DEFAULT_API_BASE_URL
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
const key = process.env.OML_PLATFORM_API_KEY;
|
|
34
|
+
const resolvedApiBaseUrl = process.env[API_BASE_URL_ENV]?.trim() || apiBaseUrl;
|
|
35
|
+
|
|
36
|
+
const config: OmlClientConfig = {
|
|
37
|
+
apiBaseUrl: resolvedApiBaseUrl,
|
|
38
|
+
tool: 'oml-cli',
|
|
39
|
+
storage: new FileStorageAdapter(),
|
|
40
|
+
auth: key
|
|
41
|
+
? { method: 'api_key', key }
|
|
42
|
+
: {
|
|
43
|
+
method: 'oauth',
|
|
44
|
+
getToken: () => authService.getAccessToken(),
|
|
45
|
+
refreshToken: () => authService.refreshAccessToken(),
|
|
46
|
+
},
|
|
47
|
+
onConcurrencyLimit: (info) => {
|
|
48
|
+
console.error(chalk.yellow(
|
|
49
|
+
`OML Platform: concurrent session limit reached `
|
|
50
|
+
+ `(${info.active_sessions}/${info.max_sessions}). `
|
|
51
|
+
+ `Close another instance or upgrade your plan.`
|
|
52
|
+
));
|
|
53
|
+
},
|
|
54
|
+
onAuthError: (error) => {
|
|
55
|
+
console.error(chalk.red(
|
|
56
|
+
`OML Platform: authentication error — ${toGenericPlatformErrorMessage(error)}`
|
|
57
|
+
));
|
|
58
|
+
},
|
|
59
|
+
debug: process.env.OML_PLATFORM_DEBUG === '1',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const platformClient = new OmlClient(config);
|
|
63
|
+
try {
|
|
64
|
+
await platformClient.initialize();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new Error(`OML CLI could not connect to the authorization service. ${toGenericPlatformErrorMessage(error)}`);
|
|
67
|
+
}
|
|
68
|
+
client = platformClient;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Dispose the platform client. Flushes any buffered telemetry
|
|
73
|
+
* and ends the session. Call at the end of CLI execution.
|
|
74
|
+
*/
|
|
75
|
+
export async function disposePlatform(): Promise<void> {
|
|
76
|
+
if (client) {
|
|
77
|
+
await client.dispose();
|
|
78
|
+
client = null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Track a CLI command invocation. Returns a done() callback
|
|
84
|
+
* that should be called on completion or error.
|
|
85
|
+
*/
|
|
86
|
+
export function trackCommand(
|
|
87
|
+
featureId: string,
|
|
88
|
+
metadata?: Record<string, unknown>
|
|
89
|
+
): CommandInvocationTracker {
|
|
90
|
+
if (!client) {
|
|
91
|
+
return (() => {}) as CommandInvocationTracker;
|
|
92
|
+
}
|
|
93
|
+
return client.trackInvocation(featureId, metadata);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function toGenericPlatformErrorMessage(error: unknown): string {
|
|
97
|
+
const message = error instanceof Error ? error.message.trim() : String(error).trim();
|
|
98
|
+
if (!message || message === 'fetch failed') {
|
|
99
|
+
return 'Check your network connection or sign in again with \'oml login\'.';
|
|
100
|
+
}
|
|
101
|
+
return message;
|
|
102
|
+
}
|