@oml/cli 0.14.2 → 0.14.4

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,62 +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
- return headers;
78
- }
79
-
80
91
  function ensureServerBaseUrl(state: ServerState | undefined): string {
81
92
  if (!state) {
82
- throw new Error('start server first');
93
+ throw new Error("OML server is not running. Start it with 'oml start'.");
83
94
  }
84
95
  return `http://${DEFAULT_HOST}:${state.port}`;
85
96
  }
86
97
 
87
- async function parseJsonResponse<T>(response: Response): Promise<T> {
88
- const text = await response.text();
98
+ function parseJsonResponse<T>(status: number, statusText: string, text: string): T {
89
99
  if (!text.trim()) {
90
- 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.`);
91
101
  }
92
102
  try {
93
103
  return JSON.parse(text) as T;
94
104
  } catch {
95
- 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,
96
148
  }
97
149
  }
98
150
 
99
151
  export async function restGet<T>(route: string, authToken?: string): Promise<T> {
152
+ void authToken;
100
153
  const baseUrl = ensureServerBaseUrl(await readRunningState());
101
- const response = await fetch(`${baseUrl}${route}`, {
102
- method: 'GET',
103
- headers: createHeaders(authToken),
104
- });
105
- const payload = await parseJsonResponse<T & { error?: string }>(response);
106
- if (!response.ok) {
107
- const message = (payload as { error?: string }).error;
108
- 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}).`);
109
165
  }
110
166
  return payload;
111
167
  }
112
168
 
113
169
  export async function restPost<T>(route: string, body: Record<string, unknown>, authToken?: string): Promise<T> {
170
+ void authToken;
114
171
  const baseUrl = ensureServerBaseUrl(await readRunningState());
115
- const response = await fetch(`${baseUrl}${route}`, {
116
- method: 'POST',
117
- headers: createHeaders(authToken),
118
- body: JSON.stringify(body),
119
- });
120
- const payload = await parseJsonResponse<Envelope<T> & { error?: string }>(response);
121
- if (!response.ok) {
122
- const message = payload.error;
123
- 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}).`);
124
183
  }
125
184
  if (payload.ok === false) {
126
- 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}.`);
127
187
  }
128
188
  if (payload.result === undefined) {
129
189
  throw new Error(`Server request failed: POST ${route} did not return a result.`);
130
190
  }
131
191
  return payload.result;
132
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
+ }