@oml/cli 0.14.1 → 0.14.3

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.
@@ -1,9 +1,11 @@
1
1
  // Copyright (c) 2026 Modelware. All rights reserved.
2
2
 
3
+ import * as http from 'node:http';
3
4
  import * as fs from 'node:fs/promises';
4
5
  import * as os from 'node:os';
5
6
  import * as path from 'node:path';
6
7
  import { createHash } from 'node:crypto';
8
+ import { URL } from 'node:url';
7
9
 
8
10
  const DEFAULT_HOST = '127.0.0.1';
9
11
 
@@ -18,6 +20,21 @@ type Envelope<T> = {
18
20
  error?: string;
19
21
  };
20
22
 
23
+ type ErrorEnvelope = {
24
+ error?: string | {
25
+ code?: string;
26
+ message?: string;
27
+ status?: number;
28
+ retryable?: boolean;
29
+ };
30
+ };
31
+
32
+ type HttpJsonResponse = {
33
+ status: number;
34
+ statusText: string;
35
+ body: string;
36
+ };
37
+
21
38
  function workspaceHash(workspaceRoot: string): string {
22
39
  return createHash('sha256').update(path.resolve(workspaceRoot)).digest('hex');
23
40
  }
@@ -71,65 +88,135 @@ async function readRunningState(workspaceRoot = process.cwd()): Promise<ServerSt
71
88
  }
72
89
  }
73
90
 
74
- function createHeaders(token: string | undefined): Headers {
75
- const headers = new Headers();
76
- headers.set('content-type', 'application/json');
77
- if (token && token.trim().length > 0) {
78
- headers.set('authorization', `Bearer ${token.trim()}`);
79
- }
80
- return headers;
81
- }
82
-
83
91
  function ensureServerBaseUrl(state: ServerState | undefined): string {
84
92
  if (!state) {
85
- throw new Error('start server first');
93
+ throw new Error("OML server is not running. Start it with 'oml start'.");
86
94
  }
87
95
  return `http://${DEFAULT_HOST}:${state.port}`;
88
96
  }
89
97
 
90
- async function parseJsonResponse<T>(response: Response): Promise<T> {
91
- const text = await response.text();
98
+ function parseJsonResponse<T>(status: number, statusText: string, text: string): T {
92
99
  if (!text.trim()) {
93
- throw new Error(`Server returned HTTP ${response.status} ${response.statusText} with empty response body.`);
100
+ throw new Error(`Server returned HTTP ${status} ${statusText} with empty response body.`);
94
101
  }
95
102
  try {
96
103
  return JSON.parse(text) as T;
97
104
  } catch {
98
- throw new Error(`Server returned HTTP ${response.status} ${response.statusText} with invalid JSON response.`);
105
+ throw new Error(`Server returned HTTP ${status} ${statusText} with invalid JSON response.`);
106
+ }
107
+ }
108
+
109
+ function requestJson(method: 'GET' | 'POST', url: string, body?: string): Promise<HttpJsonResponse> {
110
+ const target = new URL(url);
111
+ return new Promise<HttpJsonResponse>((resolve, reject) => {
112
+ const req = http.request({
113
+ protocol: target.protocol,
114
+ hostname: target.hostname,
115
+ port: target.port,
116
+ path: `${target.pathname}${target.search}`,
117
+ method,
118
+ headers: {
119
+ 'content-type': 'application/json',
120
+ accept: 'application/json',
121
+ },
122
+ }, (res) => {
123
+ let payload = '';
124
+ res.setEncoding('utf-8');
125
+ res.on('data', (chunk: string) => {
126
+ payload += chunk;
127
+ });
128
+ res.on('end', () => {
129
+ resolve({
130
+ status: res.statusCode ?? 0,
131
+ statusText: res.statusMessage ?? '',
132
+ body: payload,
133
+ });
134
+ });
135
+ });
136
+ req.once('error', reject);
137
+ if (body !== undefined) {
138
+ req.write(body, 'utf-8');
139
+ }
140
+ req.end();
141
+ });
142
+ }
143
+
144
+ function buildResponseLike(response: HttpJsonResponse): { ok: boolean; status: number } {
145
+ return {
146
+ ok: response.status >= 200 && response.status < 300,
147
+ status: response.status,
99
148
  }
100
149
  }
101
150
 
102
151
  export async function restGet<T>(route: string, authToken?: string): Promise<T> {
152
+ void authToken;
103
153
  const baseUrl = ensureServerBaseUrl(await readRunningState());
104
- const response = await fetch(`${baseUrl}${route}`, {
105
- method: 'GET',
106
- headers: createHeaders(authToken),
107
- });
108
- const payload = await parseJsonResponse<T & { error?: string }>(response);
109
- if (!response.ok) {
110
- const message = (payload as { error?: string }).error;
111
- throw new Error(message && message.trim().length > 0 ? message : `Server request failed: GET ${route} (${response.status}).`);
154
+ let response: HttpJsonResponse;
155
+ try {
156
+ response = await requestJson('GET', `${baseUrl}${route}`);
157
+ } catch {
158
+ throw new Error(`OML server is unreachable at ${baseUrl}. Start it with 'oml start' or check connectivity.`);
159
+ }
160
+ const responseLike = buildResponseLike(response);
161
+ const payload = parseJsonResponse<T & ErrorEnvelope>(response.status, response.statusText, response.body);
162
+ if (!responseLike.ok) {
163
+ const message = normalizeServerError(payload.error);
164
+ throw new Error(message ?? `Server request failed: GET ${route} (${responseLike.status}).`);
112
165
  }
113
166
  return payload;
114
167
  }
115
168
 
116
169
  export async function restPost<T>(route: string, body: Record<string, unknown>, authToken?: string): Promise<T> {
170
+ void authToken;
117
171
  const baseUrl = ensureServerBaseUrl(await readRunningState());
118
- const response = await fetch(`${baseUrl}${route}`, {
119
- method: 'POST',
120
- headers: createHeaders(authToken),
121
- body: JSON.stringify(body),
122
- });
123
- const payload = await parseJsonResponse<Envelope<T> & { error?: string }>(response);
124
- if (!response.ok) {
125
- const message = payload.error;
126
- throw new Error(message && message.trim().length > 0 ? message : `Server request failed: POST ${route} (${response.status}).`);
172
+ let response: HttpJsonResponse;
173
+ try {
174
+ response = await requestJson('POST', `${baseUrl}${route}`, JSON.stringify(body));
175
+ } catch {
176
+ throw new Error(`OML server is unreachable at ${baseUrl}. Start it with 'oml start' or check connectivity.`);
177
+ }
178
+ const responseLike = buildResponseLike(response);
179
+ const payload = parseJsonResponse<Envelope<T> & ErrorEnvelope>(response.status, response.statusText, response.body);
180
+ if (!responseLike.ok) {
181
+ const message = normalizeServerError(payload.error);
182
+ throw new Error(message ?? `Server request failed: POST ${route} (${responseLike.status}).`);
127
183
  }
128
184
  if (payload.ok === false) {
129
- throw new Error(payload.error?.trim() || `Server request failed: POST ${route}.`);
185
+ const message = normalizeServerError(payload.error);
186
+ throw new Error(message ?? `Server request failed: POST ${route}.`);
130
187
  }
131
188
  if (payload.result === undefined) {
132
189
  throw new Error(`Server request failed: POST ${route} did not return a result.`);
133
190
  }
134
191
  return payload.result;
135
192
  }
193
+
194
+ function normalizeServerError(error: ErrorEnvelope['error']): string | undefined {
195
+ if (typeof error === 'string' && error.trim().length > 0) {
196
+ return error.trim();
197
+ }
198
+ if (error && typeof error === 'object') {
199
+ const code = typeof error.code === 'string' ? error.code.trim().toLowerCase() : '';
200
+ if (code === 'auth_required') {
201
+ return "OML server authentication is required. Sign in and restart the server with 'oml start'.";
202
+ }
203
+ if (code === 'entitlements_pending') {
204
+ return 'OML entitlements are still loading. Retry in a moment.';
205
+ }
206
+ if (code === 'entitlements_unavailable') {
207
+ return "OML entitlements are unavailable. Sign in again with 'oml login' and restart the server.";
208
+ }
209
+ if (code === 'not_entitled') {
210
+ const message = typeof error.message === 'string' ? error.message.trim() : '';
211
+ return message.length > 0 ? message : 'This command is not enabled for your account.';
212
+ }
213
+ const message = typeof error.message === 'string' ? error.message.trim() : '';
214
+ if (message.length > 0) {
215
+ return message;
216
+ }
217
+ if (code.length > 0) {
218
+ return code;
219
+ }
220
+ }
221
+ return undefined;
222
+ }