@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/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
- const DEFAULT_WHITELIST_URL = 'https://www.modelware.io/oml-code/auth/permissions.json';
9
- const CACHE_DURATION_MS = 5 * 60 * 1000;
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 ? `${session.userLabel} (${session.userId})` : session.userId;
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<AuthStatus> {
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
- private async isAuthorized(session: StoredSession): Promise<boolean> {
84
- const whitelist = await this.fetchWhitelist();
85
- if (!whitelist) {
86
- return true;
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
- const userId = session.userId.toLowerCase();
89
- return whitelist.has(userId);
79
+ return session.accessToken;
90
80
  }
91
81
 
92
- private async fetchWhitelist(): Promise<Set<string> | null> {
93
- const whitelistUrl = process.env.OML_AUTH_WHITELIST_URL ?? DEFAULT_WHITELIST_URL;
94
- if (!whitelistUrl) {
95
- return null;
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
- const response = await fetch(whitelistUrl);
105
- if (!response.ok) {
106
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
107
- }
108
- const data = await response.json();
109
- const userList = extractWhitelistEntries(data);
110
- this.whitelistCache = new Set(userList.map((id) => String(id).toLowerCase()));
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
- private clearWhitelistCache(): void {
122
- this.whitelistCache = null;
123
- this.whitelistCacheTime = 0;
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.provider !== 'github' && data.provider !== 'microsoft') {
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: user.login,
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('run GitHub device-flow sign-in for CLI authorization checks')
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 beta sign-in session')
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 beta sign-in session and authorization state')
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(lintAction);
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(renderAction);
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(compileAction);
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 { reasonAction } = await import('./commands/reason.js');
100
- await reasonAction(opts);
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(validateAction);
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
- await authService.ensureAuthenticated('OML CLI');
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
- await program.parseAsync(argv);
170
+ try {
171
+ await program.parseAsync(argv);
172
+ } finally {
173
+ await disposePlatform();
174
+ }
126
175
  await updateCheck;
127
176
  }
128
177
 
@@ -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(undefined, { workspace: workspaceRoot });
28
+ await lintAction({ workspace: workspaceRoot });
29
29
  }
30
30
 
31
31
  const workspaceStat = await fs.stat(workspaceRoot).catch(() => undefined);
@@ -9,20 +9,16 @@ export type LintOptions = {
9
9
  workspaceRoot?: string
10
10
  };
11
11
 
12
- export const lintAction = async (fileName: string | undefined, opts: LintOptions): Promise<void> => {
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(fileName, workspaceRoot);
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);
@@ -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';
@@ -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
+ }