@livon/cli 0.27.0-rc.1
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/THIRD_PARTY_NOTICES.md +28 -0
- package/bin/livon.js +5 -0
- package/dist/index.cjs +603 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +574 -0
- package/package.json +43 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Third-Party Notices
|
|
2
|
+
|
|
3
|
+
This package includes third-party software. The following licenses apply:
|
|
4
|
+
|
|
5
|
+
------------------------------------------------------------------------------
|
|
6
|
+
msgpackr
|
|
7
|
+
------------------------------------------------------------------------------
|
|
8
|
+
License: MIT
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2020 Kris Zyp
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
package/bin/livon.js
ADDED
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __webpack_require__ = {};
|
|
3
|
+
(()=>{
|
|
4
|
+
__webpack_require__.n = (module)=>{
|
|
5
|
+
var getter = module && module.__esModule ? ()=>module['default'] : ()=>module;
|
|
6
|
+
__webpack_require__.d(getter, {
|
|
7
|
+
a: getter
|
|
8
|
+
});
|
|
9
|
+
return getter;
|
|
10
|
+
};
|
|
11
|
+
})();
|
|
12
|
+
(()=>{
|
|
13
|
+
__webpack_require__.d = (exports1, definition)=>{
|
|
14
|
+
for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
get: definition[key]
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
})();
|
|
20
|
+
(()=>{
|
|
21
|
+
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
22
|
+
})();
|
|
23
|
+
var __webpack_exports__ = {};
|
|
24
|
+
const generate_namespaceObject = require("@livon/client/generate");
|
|
25
|
+
const external_node_child_process_namespaceObject = require("node:child_process");
|
|
26
|
+
const external_node_crypto_namespaceObject = require("node:crypto");
|
|
27
|
+
const external_node_fs_namespaceObject = require("node:fs");
|
|
28
|
+
const external_node_path_namespaceObject = require("node:path");
|
|
29
|
+
var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject);
|
|
30
|
+
const external_ws_namespaceObject = require("ws");
|
|
31
|
+
var external_ws_default = /*#__PURE__*/ __webpack_require__.n(external_ws_namespaceObject);
|
|
32
|
+
const external_msgpackr_namespaceObject = require("msgpackr");
|
|
33
|
+
const RETRY_RESET_AFTER_CONNECTION = 'livon.retry.reset_after_connection';
|
|
34
|
+
const createDefaultOptions = ()=>({
|
|
35
|
+
endpoint: '',
|
|
36
|
+
port: void 0,
|
|
37
|
+
out: '',
|
|
38
|
+
poll: void 0,
|
|
39
|
+
timeout: void 0,
|
|
40
|
+
event: '$explain',
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: {},
|
|
43
|
+
payload: void 0
|
|
44
|
+
});
|
|
45
|
+
const readOptionValue = ({ argv, index, arg })=>{
|
|
46
|
+
if (arg.includes('=')) return {
|
|
47
|
+
nextIndex: index + 1,
|
|
48
|
+
value: arg.split('=').slice(1).join('=')
|
|
49
|
+
};
|
|
50
|
+
const value = argv[index + 1];
|
|
51
|
+
if (!value || value.startsWith('-')) return {
|
|
52
|
+
nextIndex: index + 1,
|
|
53
|
+
value: void 0
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
nextIndex: index + 2,
|
|
57
|
+
value
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
const readCliArgs = ({ argv, index, options })=>{
|
|
61
|
+
const arg = argv[index];
|
|
62
|
+
if (!arg) return {
|
|
63
|
+
options,
|
|
64
|
+
command: []
|
|
65
|
+
};
|
|
66
|
+
if ('--' === arg) return {
|
|
67
|
+
options,
|
|
68
|
+
command: argv.slice(index + 1)
|
|
69
|
+
};
|
|
70
|
+
if (!arg.startsWith('-')) return {
|
|
71
|
+
options,
|
|
72
|
+
command: argv.slice(index)
|
|
73
|
+
};
|
|
74
|
+
if ('--no-event' === arg) return readCliArgs({
|
|
75
|
+
argv,
|
|
76
|
+
index: index + 1,
|
|
77
|
+
options: {
|
|
78
|
+
...options,
|
|
79
|
+
event: void 0,
|
|
80
|
+
method: 'GET'
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
if (arg.startsWith('--endpoint')) {
|
|
84
|
+
const { value, nextIndex } = readOptionValue({
|
|
85
|
+
argv,
|
|
86
|
+
index,
|
|
87
|
+
arg
|
|
88
|
+
});
|
|
89
|
+
return readCliArgs({
|
|
90
|
+
argv,
|
|
91
|
+
index: nextIndex,
|
|
92
|
+
options: {
|
|
93
|
+
...options,
|
|
94
|
+
endpoint: value ?? ''
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (arg.startsWith('--out')) {
|
|
99
|
+
const { value, nextIndex } = readOptionValue({
|
|
100
|
+
argv,
|
|
101
|
+
index,
|
|
102
|
+
arg
|
|
103
|
+
});
|
|
104
|
+
return readCliArgs({
|
|
105
|
+
argv,
|
|
106
|
+
index: nextIndex,
|
|
107
|
+
options: {
|
|
108
|
+
...options,
|
|
109
|
+
out: value ?? ''
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (arg.startsWith('--poll')) {
|
|
114
|
+
const { value, nextIndex } = readOptionValue({
|
|
115
|
+
argv,
|
|
116
|
+
index,
|
|
117
|
+
arg
|
|
118
|
+
});
|
|
119
|
+
return readCliArgs({
|
|
120
|
+
argv,
|
|
121
|
+
index: nextIndex,
|
|
122
|
+
options: {
|
|
123
|
+
...options,
|
|
124
|
+
poll: value ? Number(value) : void 0
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (arg.startsWith('--timeout')) {
|
|
129
|
+
const { value, nextIndex } = readOptionValue({
|
|
130
|
+
argv,
|
|
131
|
+
index,
|
|
132
|
+
arg
|
|
133
|
+
});
|
|
134
|
+
if (value) {
|
|
135
|
+
const parsed = Number(value);
|
|
136
|
+
if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`Invalid --timeout value: ${value}`);
|
|
137
|
+
return readCliArgs({
|
|
138
|
+
argv,
|
|
139
|
+
index: nextIndex,
|
|
140
|
+
options: {
|
|
141
|
+
...options,
|
|
142
|
+
timeout: parsed
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return readCliArgs({
|
|
147
|
+
argv,
|
|
148
|
+
index: nextIndex,
|
|
149
|
+
options
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
if (arg.startsWith('--port')) {
|
|
153
|
+
const { value, nextIndex } = readOptionValue({
|
|
154
|
+
argv,
|
|
155
|
+
index,
|
|
156
|
+
arg
|
|
157
|
+
});
|
|
158
|
+
if (value) {
|
|
159
|
+
const parsed = Number(value);
|
|
160
|
+
if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`Invalid --port value: ${value}`);
|
|
161
|
+
return readCliArgs({
|
|
162
|
+
argv,
|
|
163
|
+
index: nextIndex,
|
|
164
|
+
options: {
|
|
165
|
+
...options,
|
|
166
|
+
port: parsed
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return readCliArgs({
|
|
171
|
+
argv,
|
|
172
|
+
index: nextIndex,
|
|
173
|
+
options
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (arg.startsWith('--event')) {
|
|
177
|
+
const { value, nextIndex } = readOptionValue({
|
|
178
|
+
argv,
|
|
179
|
+
index,
|
|
180
|
+
arg
|
|
181
|
+
});
|
|
182
|
+
return readCliArgs({
|
|
183
|
+
argv,
|
|
184
|
+
index: nextIndex,
|
|
185
|
+
options: {
|
|
186
|
+
...options,
|
|
187
|
+
event: value,
|
|
188
|
+
method: 'POST'
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (arg.startsWith('--method')) {
|
|
193
|
+
const { value, nextIndex } = readOptionValue({
|
|
194
|
+
argv,
|
|
195
|
+
index,
|
|
196
|
+
arg
|
|
197
|
+
});
|
|
198
|
+
return readCliArgs({
|
|
199
|
+
argv,
|
|
200
|
+
index: nextIndex,
|
|
201
|
+
options: {
|
|
202
|
+
...options,
|
|
203
|
+
method: value && 'GET' === value.toUpperCase() ? 'GET' : 'POST'
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (arg.startsWith('--header')) {
|
|
208
|
+
const { value, nextIndex } = readOptionValue({
|
|
209
|
+
argv,
|
|
210
|
+
index,
|
|
211
|
+
arg
|
|
212
|
+
});
|
|
213
|
+
if (value) {
|
|
214
|
+
const [key, ...rest] = value.split(':');
|
|
215
|
+
if (key && rest.length > 0) return readCliArgs({
|
|
216
|
+
argv,
|
|
217
|
+
index: nextIndex,
|
|
218
|
+
options: {
|
|
219
|
+
...options,
|
|
220
|
+
headers: {
|
|
221
|
+
...options.headers,
|
|
222
|
+
[key.trim()]: rest.join(':').trim()
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return readCliArgs({
|
|
228
|
+
argv,
|
|
229
|
+
index: nextIndex,
|
|
230
|
+
options
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (arg.startsWith('--payload')) {
|
|
234
|
+
const { value, nextIndex } = readOptionValue({
|
|
235
|
+
argv,
|
|
236
|
+
index,
|
|
237
|
+
arg
|
|
238
|
+
});
|
|
239
|
+
if (value) try {
|
|
240
|
+
return readCliArgs({
|
|
241
|
+
argv,
|
|
242
|
+
index: nextIndex,
|
|
243
|
+
options: {
|
|
244
|
+
...options,
|
|
245
|
+
payload: JSON.parse(value)
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
} catch (error) {
|
|
249
|
+
throw new Error(`Invalid JSON for --payload: ${error instanceof Error ? error.message : String(error)}`);
|
|
250
|
+
}
|
|
251
|
+
return readCliArgs({
|
|
252
|
+
argv,
|
|
253
|
+
index: nextIndex,
|
|
254
|
+
options
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
return readCliArgs({
|
|
258
|
+
argv,
|
|
259
|
+
index: index + 1,
|
|
260
|
+
options
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
const readCliInput = (argv)=>{
|
|
264
|
+
const parsed = readCliArgs({
|
|
265
|
+
argv,
|
|
266
|
+
index: 0,
|
|
267
|
+
options: createDefaultOptions()
|
|
268
|
+
});
|
|
269
|
+
const options = {
|
|
270
|
+
...parsed.options
|
|
271
|
+
};
|
|
272
|
+
if (!options.endpoint && options.port) options.endpoint = `ws://127.0.0.1:${options.port}/ws`;
|
|
273
|
+
if (!options.endpoint) throw new Error('Missing required --endpoint or --port');
|
|
274
|
+
if (options.port) options.endpoint = applyPortToEndpoint(options.endpoint, options.port);
|
|
275
|
+
if (!options.out) throw new Error('Missing required --out');
|
|
276
|
+
if (void 0 === options.event) throw new Error('Missing required --event for websocket mode.');
|
|
277
|
+
return {
|
|
278
|
+
options,
|
|
279
|
+
command: parsed.command
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
const applyPortToEndpoint = (endpoint, port)=>{
|
|
283
|
+
const url = new URL(endpoint);
|
|
284
|
+
if ('ws:' !== url.protocol && 'wss:' !== url.protocol) throw new Error('Endpoint must be ws:// or wss:// for websocket mode.');
|
|
285
|
+
url.port = String(port);
|
|
286
|
+
if (!url.pathname || '/' === url.pathname) url.pathname = '/ws';
|
|
287
|
+
return url.toString();
|
|
288
|
+
};
|
|
289
|
+
const hashAst = (ast)=>(0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify(ast)).digest('hex');
|
|
290
|
+
const compactMetadata = (metadata)=>{
|
|
291
|
+
if (!metadata) return;
|
|
292
|
+
return Object.keys(metadata).length > 0 ? metadata : void 0;
|
|
293
|
+
};
|
|
294
|
+
const compactContext = (context)=>{
|
|
295
|
+
if (!context || 0 === Object.keys(context).length) return;
|
|
296
|
+
return context;
|
|
297
|
+
};
|
|
298
|
+
const encodePayload = (value)=>(0, external_msgpackr_namespaceObject.pack)(value);
|
|
299
|
+
const decodePayload = (payload)=>payload ? (0, external_msgpackr_namespaceObject.unpack)(payload) : void 0;
|
|
300
|
+
const isRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
|
|
301
|
+
const binaryFromSocketData = (data)=>{
|
|
302
|
+
if (Array.isArray(data)) return new Uint8Array(Buffer.concat(data));
|
|
303
|
+
if ('string' == typeof data) throw new Error('Expected binary WebSocket payload.');
|
|
304
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
305
|
+
if (Buffer.isBuffer(data)) return new Uint8Array(data);
|
|
306
|
+
if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
307
|
+
return new Uint8Array(Buffer.from(data));
|
|
308
|
+
};
|
|
309
|
+
const ensureEvent = (value)=>{
|
|
310
|
+
if (!value) throw new Error('Missing required --event for websocket mode.');
|
|
311
|
+
return value;
|
|
312
|
+
};
|
|
313
|
+
const buildWireEnvelope = (input)=>{
|
|
314
|
+
const metadata = compactMetadata(input.metadata);
|
|
315
|
+
const context = compactContext(input.context);
|
|
316
|
+
const base = {
|
|
317
|
+
event: input.event,
|
|
318
|
+
metadata,
|
|
319
|
+
context: context ? encodePayload(context) : void 0
|
|
320
|
+
};
|
|
321
|
+
return {
|
|
322
|
+
...base,
|
|
323
|
+
payload: encodePayload(input.payload)
|
|
324
|
+
};
|
|
325
|
+
};
|
|
326
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
327
|
+
const CLIENT_GENERATOR_HASH = (0, generate_namespaceObject.getClientGeneratorFingerprint)();
|
|
328
|
+
const fetchAst = async (options, etag)=>{
|
|
329
|
+
const endpoint = options.endpoint.trim();
|
|
330
|
+
if (!endpoint.startsWith('ws://') && !endpoint.startsWith('wss://')) throw new Error('Endpoint must be ws:// or wss:// for $explain.');
|
|
331
|
+
return new Promise((resolve, reject)=>{
|
|
332
|
+
const ws = new (external_ws_default())(endpoint, {
|
|
333
|
+
headers: options.headers
|
|
334
|
+
});
|
|
335
|
+
let resolved = false;
|
|
336
|
+
let hadConnection = false;
|
|
337
|
+
const timeoutMs = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
338
|
+
const timeout = setTimeout(()=>{
|
|
339
|
+
if (!resolved) {
|
|
340
|
+
resolved = true;
|
|
341
|
+
ws.close();
|
|
342
|
+
reject(new Error('Timed out waiting for $explain response.'));
|
|
343
|
+
}
|
|
344
|
+
}, timeoutMs);
|
|
345
|
+
const finish = (result)=>{
|
|
346
|
+
if (resolved) return;
|
|
347
|
+
resolved = true;
|
|
348
|
+
clearTimeout(timeout);
|
|
349
|
+
ws.close();
|
|
350
|
+
resolve(result);
|
|
351
|
+
};
|
|
352
|
+
const fail = (error)=>{
|
|
353
|
+
if (resolved) return;
|
|
354
|
+
resolved = true;
|
|
355
|
+
clearTimeout(timeout);
|
|
356
|
+
ws.close();
|
|
357
|
+
const retryAware = error;
|
|
358
|
+
if (hadConnection) retryAware[RETRY_RESET_AFTER_CONNECTION] = true;
|
|
359
|
+
reject(error);
|
|
360
|
+
};
|
|
361
|
+
ws.on('error', (error)=>{
|
|
362
|
+
fail(error);
|
|
363
|
+
});
|
|
364
|
+
ws.on('open', ()=>{
|
|
365
|
+
hadConnection = true;
|
|
366
|
+
const eventName = ensureEvent(options.event);
|
|
367
|
+
const request = buildWireEnvelope({
|
|
368
|
+
event: eventName,
|
|
369
|
+
payload: options.payload ?? null,
|
|
370
|
+
metadata: etag ? {
|
|
371
|
+
ifNoneMatch: etag
|
|
372
|
+
} : void 0
|
|
373
|
+
});
|
|
374
|
+
ws.send((0, external_msgpackr_namespaceObject.pack)(request));
|
|
375
|
+
});
|
|
376
|
+
ws.on('message', (data)=>{
|
|
377
|
+
let parsed;
|
|
378
|
+
try {
|
|
379
|
+
parsed = (0, external_msgpackr_namespaceObject.unpack)(binaryFromSocketData(data));
|
|
380
|
+
} catch {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (!isRecord(parsed)) return;
|
|
384
|
+
const envelope = parsed;
|
|
385
|
+
if (envelope.event && envelope.event !== options.event) return;
|
|
386
|
+
if (envelope.error) {
|
|
387
|
+
const decoded = decodePayload(envelope.error);
|
|
388
|
+
const message = decoded && 'object' == typeof decoded && 'message' in decoded ? String(decoded.message ?? 'Explain error') : 'Explain error';
|
|
389
|
+
fail(new Error(message));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const payload = envelope.payload ? decodePayload(envelope.payload) : parsed;
|
|
393
|
+
if (!payload || 'object' != typeof payload) return;
|
|
394
|
+
const response = payload;
|
|
395
|
+
if (response.notModified) return void finish({
|
|
396
|
+
notModified: true,
|
|
397
|
+
etag: response.etag ?? etag
|
|
398
|
+
});
|
|
399
|
+
if ('ast' in response) finish({
|
|
400
|
+
ast: response.ast,
|
|
401
|
+
checksum: response.checksum,
|
|
402
|
+
schemaVersion: response.schemaVersion,
|
|
403
|
+
generatedAt: response.generatedAt,
|
|
404
|
+
etag: response.etag ?? response.checksum ?? etag
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
};
|
|
409
|
+
const resolveOutputPaths = (out)=>{
|
|
410
|
+
const isFile = external_node_path_default().extname(out).length > 0;
|
|
411
|
+
const outDir = isFile ? external_node_path_default().dirname(out) : out;
|
|
412
|
+
const astFile = external_node_path_default().join(outDir, 'ast.ts');
|
|
413
|
+
const clientFile = isFile ? out : external_node_path_default().join(outDir, 'client.ts');
|
|
414
|
+
const checksumFile = external_node_path_default().join(outDir, '.livon.client.checksum');
|
|
415
|
+
return {
|
|
416
|
+
outDir,
|
|
417
|
+
astFile,
|
|
418
|
+
clientFile,
|
|
419
|
+
checksumFile
|
|
420
|
+
};
|
|
421
|
+
};
|
|
422
|
+
const readCachedChecksum = async (checksumFile)=>{
|
|
423
|
+
const raw = (await external_node_fs_namespaceObject.promises.readFile(checksumFile, 'utf8').catch(()=>'')).trim();
|
|
424
|
+
if (!raw) return {};
|
|
425
|
+
if (raw.startsWith('{')) try {
|
|
426
|
+
const parsed = JSON.parse(raw);
|
|
427
|
+
const generatorHash = 'string' == typeof parsed.generatorHash ? parsed.generatorHash : void 0;
|
|
428
|
+
const etag = 'string' == typeof parsed.etag ? parsed.etag : void 0;
|
|
429
|
+
return {
|
|
430
|
+
generatorHash,
|
|
431
|
+
etag
|
|
432
|
+
};
|
|
433
|
+
} catch {}
|
|
434
|
+
const legacyVersionSeparator = raw.indexOf(':');
|
|
435
|
+
if (raw.startsWith('client-generator-') && legacyVersionSeparator > 0) {
|
|
436
|
+
const etag = raw.slice(legacyVersionSeparator + 1).trim();
|
|
437
|
+
return {
|
|
438
|
+
etag: etag || void 0
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
return {
|
|
442
|
+
etag: raw
|
|
443
|
+
};
|
|
444
|
+
};
|
|
445
|
+
const writeClientFiles = async (ast, options, meta)=>{
|
|
446
|
+
const { outDir, astFile, clientFile, checksumFile } = resolveOutputPaths(options.out);
|
|
447
|
+
await external_node_fs_namespaceObject.promises.mkdir(outDir, {
|
|
448
|
+
recursive: true
|
|
449
|
+
});
|
|
450
|
+
const previous = await readCachedChecksum(checksumFile);
|
|
451
|
+
const checksum = meta?.checksum ?? hashAst(ast);
|
|
452
|
+
const etagBase = (meta?.etag ?? checksum).trim();
|
|
453
|
+
const hasSameGenerator = previous.generatorHash === CLIENT_GENERATOR_HASH;
|
|
454
|
+
const hasSameEtag = previous.etag === etagBase;
|
|
455
|
+
if (hasSameGenerator && hasSameEtag) return {
|
|
456
|
+
updated: false,
|
|
457
|
+
checksum,
|
|
458
|
+
etag: etagBase,
|
|
459
|
+
schemaVersion: meta?.schemaVersion,
|
|
460
|
+
generatedAt: meta?.generatedAt
|
|
461
|
+
};
|
|
462
|
+
const generated = (0, generate_namespaceObject.generateClientFiles)({
|
|
463
|
+
ast: ast
|
|
464
|
+
});
|
|
465
|
+
const astSource = generated.files[generated.astFile];
|
|
466
|
+
const clientSource = generated.files[generated.clientFile];
|
|
467
|
+
if (!astSource || !clientSource) throw new Error('Generated client sources were empty.');
|
|
468
|
+
await external_node_fs_namespaceObject.promises.writeFile(astFile, astSource, 'utf8');
|
|
469
|
+
await external_node_fs_namespaceObject.promises.writeFile(clientFile, clientSource, 'utf8');
|
|
470
|
+
await external_node_fs_namespaceObject.promises.writeFile(checksumFile, JSON.stringify({
|
|
471
|
+
generatorHash: CLIENT_GENERATOR_HASH,
|
|
472
|
+
etag: etagBase
|
|
473
|
+
}), 'utf8');
|
|
474
|
+
return {
|
|
475
|
+
updated: true,
|
|
476
|
+
checksum,
|
|
477
|
+
etag: etagBase,
|
|
478
|
+
schemaVersion: meta?.schemaVersion,
|
|
479
|
+
generatedAt: meta?.generatedAt
|
|
480
|
+
};
|
|
481
|
+
};
|
|
482
|
+
const startCommandRuntime = ({ command })=>{
|
|
483
|
+
const [commandName, ...commandArgs] = command;
|
|
484
|
+
if (!commandName) throw new Error('Missing command to run after livon sync.');
|
|
485
|
+
const child = (0, external_node_child_process_namespaceObject.spawn)(commandName, commandArgs, {
|
|
486
|
+
stdio: 'inherit',
|
|
487
|
+
env: process.env
|
|
488
|
+
});
|
|
489
|
+
const stopChild = ()=>{
|
|
490
|
+
if (child.killed) return;
|
|
491
|
+
child.kill('SIGTERM');
|
|
492
|
+
};
|
|
493
|
+
process.on('exit', stopChild);
|
|
494
|
+
process.on('SIGINT', ()=>{
|
|
495
|
+
stopChild();
|
|
496
|
+
process.exit(130);
|
|
497
|
+
});
|
|
498
|
+
process.on('SIGTERM', ()=>{
|
|
499
|
+
stopChild();
|
|
500
|
+
process.exit(143);
|
|
501
|
+
});
|
|
502
|
+
const waitForExit = new Promise((resolve, reject)=>{
|
|
503
|
+
child.on('error', (error)=>{
|
|
504
|
+
reject(error);
|
|
505
|
+
});
|
|
506
|
+
child.on('exit', (code, signal)=>{
|
|
507
|
+
const exitCode = 'number' == typeof code ? code : signal ? 1 : 0;
|
|
508
|
+
if (0 !== exitCode) console.error(`livon: linked command exited with code ${exitCode}`);
|
|
509
|
+
resolve(exitCode);
|
|
510
|
+
process.exit(exitCode);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
return {
|
|
514
|
+
waitForExit
|
|
515
|
+
};
|
|
516
|
+
};
|
|
517
|
+
const run = async ()=>{
|
|
518
|
+
const cli = readCliInput(process.argv.slice(2));
|
|
519
|
+
const options = cli.options;
|
|
520
|
+
const commandRuntimeInput = cli.command.length > 0 ? {
|
|
521
|
+
command: cli.command
|
|
522
|
+
} : void 0;
|
|
523
|
+
let commandRuntime;
|
|
524
|
+
const ensureCommandRuntime = ()=>{
|
|
525
|
+
if (!commandRuntimeInput || commandRuntime) return;
|
|
526
|
+
commandRuntime = startCommandRuntime(commandRuntimeInput);
|
|
527
|
+
};
|
|
528
|
+
const execute = async ()=>{
|
|
529
|
+
const { checksumFile } = resolveOutputPaths(options.out);
|
|
530
|
+
const cached = await readCachedChecksum(checksumFile);
|
|
531
|
+
const useCachedEtag = cached.generatorHash === CLIENT_GENERATOR_HASH;
|
|
532
|
+
const cachedEtag = useCachedEtag ? cached.etag : void 0;
|
|
533
|
+
const result = await fetchAst(options, cachedEtag);
|
|
534
|
+
if (result.notModified) return;
|
|
535
|
+
if (void 0 === result.ast) throw new Error('Explain response missing AST.');
|
|
536
|
+
const writeResult = await writeClientFiles(result.ast, options, {
|
|
537
|
+
checksum: result.checksum,
|
|
538
|
+
etag: result.etag,
|
|
539
|
+
schemaVersion: result.schemaVersion,
|
|
540
|
+
generatedAt: result.generatedAt
|
|
541
|
+
});
|
|
542
|
+
if (writeResult.updated) {
|
|
543
|
+
const meta = [];
|
|
544
|
+
if (writeResult.schemaVersion) meta.push(`schema ${writeResult.schemaVersion}`);
|
|
545
|
+
if (writeResult.generatedAt) meta.push(`generated ${writeResult.generatedAt}`);
|
|
546
|
+
const metaInfo = meta.length > 0 ? `, ${meta.join(', ')}` : '';
|
|
547
|
+
console.log(`livon: client updated (checksum ${writeResult.checksum}${metaInfo})`);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
const withRetry = async (action)=>{
|
|
551
|
+
const maxAttempts = 20;
|
|
552
|
+
const baseDelay = 250;
|
|
553
|
+
const runAttempt = async (attempt, resetApplied)=>{
|
|
554
|
+
try {
|
|
555
|
+
await action();
|
|
556
|
+
return;
|
|
557
|
+
} catch (error) {
|
|
558
|
+
const retryAware = error;
|
|
559
|
+
const shouldReset = Boolean(retryAware?.[RETRY_RESET_AFTER_CONNECTION]) && !resetApplied;
|
|
560
|
+
const nextAttempt = shouldReset ? 1 : attempt + 1;
|
|
561
|
+
const nextResetApplied = shouldReset ? true : resetApplied;
|
|
562
|
+
if (nextAttempt >= maxAttempts) throw new Error('livon: giving up after repeated retries');
|
|
563
|
+
const wait = baseDelay * Math.min(nextAttempt, 10);
|
|
564
|
+
console.warn(`livon: attempt ${nextAttempt}/${maxAttempts} failed: ${error instanceof Error ? error.message : String(error)} – retrying in ${wait}ms`);
|
|
565
|
+
await new Promise((resolve)=>setTimeout(resolve, wait));
|
|
566
|
+
await runAttempt(nextAttempt, nextResetApplied);
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
await runAttempt(0, false);
|
|
570
|
+
};
|
|
571
|
+
if (options.poll && options.poll > 0) {
|
|
572
|
+
let inFlight = false;
|
|
573
|
+
const tick = async ()=>{
|
|
574
|
+
if (inFlight) return;
|
|
575
|
+
inFlight = true;
|
|
576
|
+
try {
|
|
577
|
+
await withRetry(execute);
|
|
578
|
+
ensureCommandRuntime();
|
|
579
|
+
} catch (error) {
|
|
580
|
+
console.error('livon: poll error', error);
|
|
581
|
+
} finally{
|
|
582
|
+
inFlight = false;
|
|
583
|
+
setTimeout(tick, options.poll);
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
await tick();
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
await withRetry(execute);
|
|
590
|
+
ensureCommandRuntime();
|
|
591
|
+
if (commandRuntime) {
|
|
592
|
+
const commandExitCode = await commandRuntime.waitForExit;
|
|
593
|
+
process.exit(commandExitCode);
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
run().catch((error)=>{
|
|
597
|
+
console.error(error);
|
|
598
|
+
process.exit(1);
|
|
599
|
+
});
|
|
600
|
+
for(var __rspack_i in __webpack_exports__)exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
601
|
+
Object.defineProperty(exports, '__esModule', {
|
|
602
|
+
value: true
|
|
603
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
import { generateClientFiles, getClientGeneratorFingerprint } from "@livon/client/generate";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { promises } from "node:fs";
|
|
5
|
+
import node_path from "node:path";
|
|
6
|
+
import ws_0 from "ws";
|
|
7
|
+
import { pack, unpack } from "msgpackr";
|
|
8
|
+
const RETRY_RESET_AFTER_CONNECTION = 'livon.retry.reset_after_connection';
|
|
9
|
+
const createDefaultOptions = ()=>({
|
|
10
|
+
endpoint: '',
|
|
11
|
+
port: void 0,
|
|
12
|
+
out: '',
|
|
13
|
+
poll: void 0,
|
|
14
|
+
timeout: void 0,
|
|
15
|
+
event: '$explain',
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: {},
|
|
18
|
+
payload: void 0
|
|
19
|
+
});
|
|
20
|
+
const readOptionValue = ({ argv, index, arg })=>{
|
|
21
|
+
if (arg.includes('=')) return {
|
|
22
|
+
nextIndex: index + 1,
|
|
23
|
+
value: arg.split('=').slice(1).join('=')
|
|
24
|
+
};
|
|
25
|
+
const value = argv[index + 1];
|
|
26
|
+
if (!value || value.startsWith('-')) return {
|
|
27
|
+
nextIndex: index + 1,
|
|
28
|
+
value: void 0
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
nextIndex: index + 2,
|
|
32
|
+
value
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
const readCliArgs = ({ argv, index, options })=>{
|
|
36
|
+
const arg = argv[index];
|
|
37
|
+
if (!arg) return {
|
|
38
|
+
options,
|
|
39
|
+
command: []
|
|
40
|
+
};
|
|
41
|
+
if ('--' === arg) return {
|
|
42
|
+
options,
|
|
43
|
+
command: argv.slice(index + 1)
|
|
44
|
+
};
|
|
45
|
+
if (!arg.startsWith('-')) return {
|
|
46
|
+
options,
|
|
47
|
+
command: argv.slice(index)
|
|
48
|
+
};
|
|
49
|
+
if ('--no-event' === arg) return readCliArgs({
|
|
50
|
+
argv,
|
|
51
|
+
index: index + 1,
|
|
52
|
+
options: {
|
|
53
|
+
...options,
|
|
54
|
+
event: void 0,
|
|
55
|
+
method: 'GET'
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
if (arg.startsWith('--endpoint')) {
|
|
59
|
+
const { value, nextIndex } = readOptionValue({
|
|
60
|
+
argv,
|
|
61
|
+
index,
|
|
62
|
+
arg
|
|
63
|
+
});
|
|
64
|
+
return readCliArgs({
|
|
65
|
+
argv,
|
|
66
|
+
index: nextIndex,
|
|
67
|
+
options: {
|
|
68
|
+
...options,
|
|
69
|
+
endpoint: value ?? ''
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
if (arg.startsWith('--out')) {
|
|
74
|
+
const { value, nextIndex } = readOptionValue({
|
|
75
|
+
argv,
|
|
76
|
+
index,
|
|
77
|
+
arg
|
|
78
|
+
});
|
|
79
|
+
return readCliArgs({
|
|
80
|
+
argv,
|
|
81
|
+
index: nextIndex,
|
|
82
|
+
options: {
|
|
83
|
+
...options,
|
|
84
|
+
out: value ?? ''
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (arg.startsWith('--poll')) {
|
|
89
|
+
const { value, nextIndex } = readOptionValue({
|
|
90
|
+
argv,
|
|
91
|
+
index,
|
|
92
|
+
arg
|
|
93
|
+
});
|
|
94
|
+
return readCliArgs({
|
|
95
|
+
argv,
|
|
96
|
+
index: nextIndex,
|
|
97
|
+
options: {
|
|
98
|
+
...options,
|
|
99
|
+
poll: value ? Number(value) : void 0
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (arg.startsWith('--timeout')) {
|
|
104
|
+
const { value, nextIndex } = readOptionValue({
|
|
105
|
+
argv,
|
|
106
|
+
index,
|
|
107
|
+
arg
|
|
108
|
+
});
|
|
109
|
+
if (value) {
|
|
110
|
+
const parsed = Number(value);
|
|
111
|
+
if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`Invalid --timeout value: ${value}`);
|
|
112
|
+
return readCliArgs({
|
|
113
|
+
argv,
|
|
114
|
+
index: nextIndex,
|
|
115
|
+
options: {
|
|
116
|
+
...options,
|
|
117
|
+
timeout: parsed
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return readCliArgs({
|
|
122
|
+
argv,
|
|
123
|
+
index: nextIndex,
|
|
124
|
+
options
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (arg.startsWith('--port')) {
|
|
128
|
+
const { value, nextIndex } = readOptionValue({
|
|
129
|
+
argv,
|
|
130
|
+
index,
|
|
131
|
+
arg
|
|
132
|
+
});
|
|
133
|
+
if (value) {
|
|
134
|
+
const parsed = Number(value);
|
|
135
|
+
if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`Invalid --port value: ${value}`);
|
|
136
|
+
return readCliArgs({
|
|
137
|
+
argv,
|
|
138
|
+
index: nextIndex,
|
|
139
|
+
options: {
|
|
140
|
+
...options,
|
|
141
|
+
port: parsed
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return readCliArgs({
|
|
146
|
+
argv,
|
|
147
|
+
index: nextIndex,
|
|
148
|
+
options
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (arg.startsWith('--event')) {
|
|
152
|
+
const { value, nextIndex } = readOptionValue({
|
|
153
|
+
argv,
|
|
154
|
+
index,
|
|
155
|
+
arg
|
|
156
|
+
});
|
|
157
|
+
return readCliArgs({
|
|
158
|
+
argv,
|
|
159
|
+
index: nextIndex,
|
|
160
|
+
options: {
|
|
161
|
+
...options,
|
|
162
|
+
event: value,
|
|
163
|
+
method: 'POST'
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (arg.startsWith('--method')) {
|
|
168
|
+
const { value, nextIndex } = readOptionValue({
|
|
169
|
+
argv,
|
|
170
|
+
index,
|
|
171
|
+
arg
|
|
172
|
+
});
|
|
173
|
+
return readCliArgs({
|
|
174
|
+
argv,
|
|
175
|
+
index: nextIndex,
|
|
176
|
+
options: {
|
|
177
|
+
...options,
|
|
178
|
+
method: value && 'GET' === value.toUpperCase() ? 'GET' : 'POST'
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (arg.startsWith('--header')) {
|
|
183
|
+
const { value, nextIndex } = readOptionValue({
|
|
184
|
+
argv,
|
|
185
|
+
index,
|
|
186
|
+
arg
|
|
187
|
+
});
|
|
188
|
+
if (value) {
|
|
189
|
+
const [key, ...rest] = value.split(':');
|
|
190
|
+
if (key && rest.length > 0) return readCliArgs({
|
|
191
|
+
argv,
|
|
192
|
+
index: nextIndex,
|
|
193
|
+
options: {
|
|
194
|
+
...options,
|
|
195
|
+
headers: {
|
|
196
|
+
...options.headers,
|
|
197
|
+
[key.trim()]: rest.join(':').trim()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return readCliArgs({
|
|
203
|
+
argv,
|
|
204
|
+
index: nextIndex,
|
|
205
|
+
options
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (arg.startsWith('--payload')) {
|
|
209
|
+
const { value, nextIndex } = readOptionValue({
|
|
210
|
+
argv,
|
|
211
|
+
index,
|
|
212
|
+
arg
|
|
213
|
+
});
|
|
214
|
+
if (value) try {
|
|
215
|
+
return readCliArgs({
|
|
216
|
+
argv,
|
|
217
|
+
index: nextIndex,
|
|
218
|
+
options: {
|
|
219
|
+
...options,
|
|
220
|
+
payload: JSON.parse(value)
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
} catch (error) {
|
|
224
|
+
throw new Error(`Invalid JSON for --payload: ${error instanceof Error ? error.message : String(error)}`);
|
|
225
|
+
}
|
|
226
|
+
return readCliArgs({
|
|
227
|
+
argv,
|
|
228
|
+
index: nextIndex,
|
|
229
|
+
options
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return readCliArgs({
|
|
233
|
+
argv,
|
|
234
|
+
index: index + 1,
|
|
235
|
+
options
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
const readCliInput = (argv)=>{
|
|
239
|
+
const parsed = readCliArgs({
|
|
240
|
+
argv,
|
|
241
|
+
index: 0,
|
|
242
|
+
options: createDefaultOptions()
|
|
243
|
+
});
|
|
244
|
+
const options = {
|
|
245
|
+
...parsed.options
|
|
246
|
+
};
|
|
247
|
+
if (!options.endpoint && options.port) options.endpoint = `ws://127.0.0.1:${options.port}/ws`;
|
|
248
|
+
if (!options.endpoint) throw new Error('Missing required --endpoint or --port');
|
|
249
|
+
if (options.port) options.endpoint = applyPortToEndpoint(options.endpoint, options.port);
|
|
250
|
+
if (!options.out) throw new Error('Missing required --out');
|
|
251
|
+
if (void 0 === options.event) throw new Error('Missing required --event for websocket mode.');
|
|
252
|
+
return {
|
|
253
|
+
options,
|
|
254
|
+
command: parsed.command
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
const applyPortToEndpoint = (endpoint, port)=>{
|
|
258
|
+
const url = new URL(endpoint);
|
|
259
|
+
if ('ws:' !== url.protocol && 'wss:' !== url.protocol) throw new Error('Endpoint must be ws:// or wss:// for websocket mode.');
|
|
260
|
+
url.port = String(port);
|
|
261
|
+
if (!url.pathname || '/' === url.pathname) url.pathname = '/ws';
|
|
262
|
+
return url.toString();
|
|
263
|
+
};
|
|
264
|
+
const hashAst = (ast)=>createHash('sha256').update(JSON.stringify(ast)).digest('hex');
|
|
265
|
+
const compactMetadata = (metadata)=>{
|
|
266
|
+
if (!metadata) return;
|
|
267
|
+
return Object.keys(metadata).length > 0 ? metadata : void 0;
|
|
268
|
+
};
|
|
269
|
+
const compactContext = (context)=>{
|
|
270
|
+
if (!context || 0 === Object.keys(context).length) return;
|
|
271
|
+
return context;
|
|
272
|
+
};
|
|
273
|
+
const encodePayload = (value)=>pack(value);
|
|
274
|
+
const decodePayload = (payload)=>payload ? unpack(payload) : void 0;
|
|
275
|
+
const isRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
|
|
276
|
+
const binaryFromSocketData = (data)=>{
|
|
277
|
+
if (Array.isArray(data)) return new Uint8Array(Buffer.concat(data));
|
|
278
|
+
if ('string' == typeof data) throw new Error('Expected binary WebSocket payload.');
|
|
279
|
+
if (data instanceof ArrayBuffer) return new Uint8Array(data);
|
|
280
|
+
if (Buffer.isBuffer(data)) return new Uint8Array(data);
|
|
281
|
+
if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
282
|
+
return new Uint8Array(Buffer.from(data));
|
|
283
|
+
};
|
|
284
|
+
const ensureEvent = (value)=>{
|
|
285
|
+
if (!value) throw new Error('Missing required --event for websocket mode.');
|
|
286
|
+
return value;
|
|
287
|
+
};
|
|
288
|
+
const buildWireEnvelope = (input)=>{
|
|
289
|
+
const metadata = compactMetadata(input.metadata);
|
|
290
|
+
const context = compactContext(input.context);
|
|
291
|
+
const base = {
|
|
292
|
+
event: input.event,
|
|
293
|
+
metadata,
|
|
294
|
+
context: context ? encodePayload(context) : void 0
|
|
295
|
+
};
|
|
296
|
+
return {
|
|
297
|
+
...base,
|
|
298
|
+
payload: encodePayload(input.payload)
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
302
|
+
const CLIENT_GENERATOR_HASH = getClientGeneratorFingerprint();
|
|
303
|
+
const fetchAst = async (options, etag)=>{
|
|
304
|
+
const endpoint = options.endpoint.trim();
|
|
305
|
+
if (!endpoint.startsWith('ws://') && !endpoint.startsWith('wss://')) throw new Error('Endpoint must be ws:// or wss:// for $explain.');
|
|
306
|
+
return new Promise((resolve, reject)=>{
|
|
307
|
+
const ws = new ws_0(endpoint, {
|
|
308
|
+
headers: options.headers
|
|
309
|
+
});
|
|
310
|
+
let resolved = false;
|
|
311
|
+
let hadConnection = false;
|
|
312
|
+
const timeoutMs = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
313
|
+
const timeout = setTimeout(()=>{
|
|
314
|
+
if (!resolved) {
|
|
315
|
+
resolved = true;
|
|
316
|
+
ws.close();
|
|
317
|
+
reject(new Error('Timed out waiting for $explain response.'));
|
|
318
|
+
}
|
|
319
|
+
}, timeoutMs);
|
|
320
|
+
const finish = (result)=>{
|
|
321
|
+
if (resolved) return;
|
|
322
|
+
resolved = true;
|
|
323
|
+
clearTimeout(timeout);
|
|
324
|
+
ws.close();
|
|
325
|
+
resolve(result);
|
|
326
|
+
};
|
|
327
|
+
const fail = (error)=>{
|
|
328
|
+
if (resolved) return;
|
|
329
|
+
resolved = true;
|
|
330
|
+
clearTimeout(timeout);
|
|
331
|
+
ws.close();
|
|
332
|
+
const retryAware = error;
|
|
333
|
+
if (hadConnection) retryAware[RETRY_RESET_AFTER_CONNECTION] = true;
|
|
334
|
+
reject(error);
|
|
335
|
+
};
|
|
336
|
+
ws.on('error', (error)=>{
|
|
337
|
+
fail(error);
|
|
338
|
+
});
|
|
339
|
+
ws.on('open', ()=>{
|
|
340
|
+
hadConnection = true;
|
|
341
|
+
const eventName = ensureEvent(options.event);
|
|
342
|
+
const request = buildWireEnvelope({
|
|
343
|
+
event: eventName,
|
|
344
|
+
payload: options.payload ?? null,
|
|
345
|
+
metadata: etag ? {
|
|
346
|
+
ifNoneMatch: etag
|
|
347
|
+
} : void 0
|
|
348
|
+
});
|
|
349
|
+
ws.send(pack(request));
|
|
350
|
+
});
|
|
351
|
+
ws.on('message', (data)=>{
|
|
352
|
+
let parsed;
|
|
353
|
+
try {
|
|
354
|
+
parsed = unpack(binaryFromSocketData(data));
|
|
355
|
+
} catch {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (!isRecord(parsed)) return;
|
|
359
|
+
const envelope = parsed;
|
|
360
|
+
if (envelope.event && envelope.event !== options.event) return;
|
|
361
|
+
if (envelope.error) {
|
|
362
|
+
const decoded = decodePayload(envelope.error);
|
|
363
|
+
const message = decoded && 'object' == typeof decoded && 'message' in decoded ? String(decoded.message ?? 'Explain error') : 'Explain error';
|
|
364
|
+
fail(new Error(message));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const payload = envelope.payload ? decodePayload(envelope.payload) : parsed;
|
|
368
|
+
if (!payload || 'object' != typeof payload) return;
|
|
369
|
+
const response = payload;
|
|
370
|
+
if (response.notModified) return void finish({
|
|
371
|
+
notModified: true,
|
|
372
|
+
etag: response.etag ?? etag
|
|
373
|
+
});
|
|
374
|
+
if ('ast' in response) finish({
|
|
375
|
+
ast: response.ast,
|
|
376
|
+
checksum: response.checksum,
|
|
377
|
+
schemaVersion: response.schemaVersion,
|
|
378
|
+
generatedAt: response.generatedAt,
|
|
379
|
+
etag: response.etag ?? response.checksum ?? etag
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
};
|
|
384
|
+
const resolveOutputPaths = (out)=>{
|
|
385
|
+
const isFile = node_path.extname(out).length > 0;
|
|
386
|
+
const outDir = isFile ? node_path.dirname(out) : out;
|
|
387
|
+
const astFile = node_path.join(outDir, 'ast.ts');
|
|
388
|
+
const clientFile = isFile ? out : node_path.join(outDir, 'client.ts');
|
|
389
|
+
const checksumFile = node_path.join(outDir, '.livon.client.checksum');
|
|
390
|
+
return {
|
|
391
|
+
outDir,
|
|
392
|
+
astFile,
|
|
393
|
+
clientFile,
|
|
394
|
+
checksumFile
|
|
395
|
+
};
|
|
396
|
+
};
|
|
397
|
+
const readCachedChecksum = async (checksumFile)=>{
|
|
398
|
+
const raw = (await promises.readFile(checksumFile, 'utf8').catch(()=>'')).trim();
|
|
399
|
+
if (!raw) return {};
|
|
400
|
+
if (raw.startsWith('{')) try {
|
|
401
|
+
const parsed = JSON.parse(raw);
|
|
402
|
+
const generatorHash = 'string' == typeof parsed.generatorHash ? parsed.generatorHash : void 0;
|
|
403
|
+
const etag = 'string' == typeof parsed.etag ? parsed.etag : void 0;
|
|
404
|
+
return {
|
|
405
|
+
generatorHash,
|
|
406
|
+
etag
|
|
407
|
+
};
|
|
408
|
+
} catch {}
|
|
409
|
+
const legacyVersionSeparator = raw.indexOf(':');
|
|
410
|
+
if (raw.startsWith('client-generator-') && legacyVersionSeparator > 0) {
|
|
411
|
+
const etag = raw.slice(legacyVersionSeparator + 1).trim();
|
|
412
|
+
return {
|
|
413
|
+
etag: etag || void 0
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
etag: raw
|
|
418
|
+
};
|
|
419
|
+
};
|
|
420
|
+
const writeClientFiles = async (ast, options, meta)=>{
|
|
421
|
+
const { outDir, astFile, clientFile, checksumFile } = resolveOutputPaths(options.out);
|
|
422
|
+
await promises.mkdir(outDir, {
|
|
423
|
+
recursive: true
|
|
424
|
+
});
|
|
425
|
+
const previous = await readCachedChecksum(checksumFile);
|
|
426
|
+
const checksum = meta?.checksum ?? hashAst(ast);
|
|
427
|
+
const etagBase = (meta?.etag ?? checksum).trim();
|
|
428
|
+
const hasSameGenerator = previous.generatorHash === CLIENT_GENERATOR_HASH;
|
|
429
|
+
const hasSameEtag = previous.etag === etagBase;
|
|
430
|
+
if (hasSameGenerator && hasSameEtag) return {
|
|
431
|
+
updated: false,
|
|
432
|
+
checksum,
|
|
433
|
+
etag: etagBase,
|
|
434
|
+
schemaVersion: meta?.schemaVersion,
|
|
435
|
+
generatedAt: meta?.generatedAt
|
|
436
|
+
};
|
|
437
|
+
const generated = generateClientFiles({
|
|
438
|
+
ast: ast
|
|
439
|
+
});
|
|
440
|
+
const astSource = generated.files[generated.astFile];
|
|
441
|
+
const clientSource = generated.files[generated.clientFile];
|
|
442
|
+
if (!astSource || !clientSource) throw new Error('Generated client sources were empty.');
|
|
443
|
+
await promises.writeFile(astFile, astSource, 'utf8');
|
|
444
|
+
await promises.writeFile(clientFile, clientSource, 'utf8');
|
|
445
|
+
await promises.writeFile(checksumFile, JSON.stringify({
|
|
446
|
+
generatorHash: CLIENT_GENERATOR_HASH,
|
|
447
|
+
etag: etagBase
|
|
448
|
+
}), 'utf8');
|
|
449
|
+
return {
|
|
450
|
+
updated: true,
|
|
451
|
+
checksum,
|
|
452
|
+
etag: etagBase,
|
|
453
|
+
schemaVersion: meta?.schemaVersion,
|
|
454
|
+
generatedAt: meta?.generatedAt
|
|
455
|
+
};
|
|
456
|
+
};
|
|
457
|
+
const startCommandRuntime = ({ command })=>{
|
|
458
|
+
const [commandName, ...commandArgs] = command;
|
|
459
|
+
if (!commandName) throw new Error('Missing command to run after livon sync.');
|
|
460
|
+
const child = spawn(commandName, commandArgs, {
|
|
461
|
+
stdio: 'inherit',
|
|
462
|
+
env: process.env
|
|
463
|
+
});
|
|
464
|
+
const stopChild = ()=>{
|
|
465
|
+
if (child.killed) return;
|
|
466
|
+
child.kill('SIGTERM');
|
|
467
|
+
};
|
|
468
|
+
process.on('exit', stopChild);
|
|
469
|
+
process.on('SIGINT', ()=>{
|
|
470
|
+
stopChild();
|
|
471
|
+
process.exit(130);
|
|
472
|
+
});
|
|
473
|
+
process.on('SIGTERM', ()=>{
|
|
474
|
+
stopChild();
|
|
475
|
+
process.exit(143);
|
|
476
|
+
});
|
|
477
|
+
const waitForExit = new Promise((resolve, reject)=>{
|
|
478
|
+
child.on('error', (error)=>{
|
|
479
|
+
reject(error);
|
|
480
|
+
});
|
|
481
|
+
child.on('exit', (code, signal)=>{
|
|
482
|
+
const exitCode = 'number' == typeof code ? code : signal ? 1 : 0;
|
|
483
|
+
if (0 !== exitCode) console.error(`livon: linked command exited with code ${exitCode}`);
|
|
484
|
+
resolve(exitCode);
|
|
485
|
+
process.exit(exitCode);
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
return {
|
|
489
|
+
waitForExit
|
|
490
|
+
};
|
|
491
|
+
};
|
|
492
|
+
const run = async ()=>{
|
|
493
|
+
const cli = readCliInput(process.argv.slice(2));
|
|
494
|
+
const options = cli.options;
|
|
495
|
+
const commandRuntimeInput = cli.command.length > 0 ? {
|
|
496
|
+
command: cli.command
|
|
497
|
+
} : void 0;
|
|
498
|
+
let commandRuntime;
|
|
499
|
+
const ensureCommandRuntime = ()=>{
|
|
500
|
+
if (!commandRuntimeInput || commandRuntime) return;
|
|
501
|
+
commandRuntime = startCommandRuntime(commandRuntimeInput);
|
|
502
|
+
};
|
|
503
|
+
const execute = async ()=>{
|
|
504
|
+
const { checksumFile } = resolveOutputPaths(options.out);
|
|
505
|
+
const cached = await readCachedChecksum(checksumFile);
|
|
506
|
+
const useCachedEtag = cached.generatorHash === CLIENT_GENERATOR_HASH;
|
|
507
|
+
const cachedEtag = useCachedEtag ? cached.etag : void 0;
|
|
508
|
+
const result = await fetchAst(options, cachedEtag);
|
|
509
|
+
if (result.notModified) return;
|
|
510
|
+
if (void 0 === result.ast) throw new Error('Explain response missing AST.');
|
|
511
|
+
const writeResult = await writeClientFiles(result.ast, options, {
|
|
512
|
+
checksum: result.checksum,
|
|
513
|
+
etag: result.etag,
|
|
514
|
+
schemaVersion: result.schemaVersion,
|
|
515
|
+
generatedAt: result.generatedAt
|
|
516
|
+
});
|
|
517
|
+
if (writeResult.updated) {
|
|
518
|
+
const meta = [];
|
|
519
|
+
if (writeResult.schemaVersion) meta.push(`schema ${writeResult.schemaVersion}`);
|
|
520
|
+
if (writeResult.generatedAt) meta.push(`generated ${writeResult.generatedAt}`);
|
|
521
|
+
const metaInfo = meta.length > 0 ? `, ${meta.join(', ')}` : '';
|
|
522
|
+
console.log(`livon: client updated (checksum ${writeResult.checksum}${metaInfo})`);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
const withRetry = async (action)=>{
|
|
526
|
+
const maxAttempts = 20;
|
|
527
|
+
const baseDelay = 250;
|
|
528
|
+
const runAttempt = async (attempt, resetApplied)=>{
|
|
529
|
+
try {
|
|
530
|
+
await action();
|
|
531
|
+
return;
|
|
532
|
+
} catch (error) {
|
|
533
|
+
const retryAware = error;
|
|
534
|
+
const shouldReset = Boolean(retryAware?.[RETRY_RESET_AFTER_CONNECTION]) && !resetApplied;
|
|
535
|
+
const nextAttempt = shouldReset ? 1 : attempt + 1;
|
|
536
|
+
const nextResetApplied = shouldReset ? true : resetApplied;
|
|
537
|
+
if (nextAttempt >= maxAttempts) throw new Error('livon: giving up after repeated retries');
|
|
538
|
+
const wait = baseDelay * Math.min(nextAttempt, 10);
|
|
539
|
+
console.warn(`livon: attempt ${nextAttempt}/${maxAttempts} failed: ${error instanceof Error ? error.message : String(error)} – retrying in ${wait}ms`);
|
|
540
|
+
await new Promise((resolve)=>setTimeout(resolve, wait));
|
|
541
|
+
await runAttempt(nextAttempt, nextResetApplied);
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
await runAttempt(0, false);
|
|
545
|
+
};
|
|
546
|
+
if (options.poll && options.poll > 0) {
|
|
547
|
+
let inFlight = false;
|
|
548
|
+
const tick = async ()=>{
|
|
549
|
+
if (inFlight) return;
|
|
550
|
+
inFlight = true;
|
|
551
|
+
try {
|
|
552
|
+
await withRetry(execute);
|
|
553
|
+
ensureCommandRuntime();
|
|
554
|
+
} catch (error) {
|
|
555
|
+
console.error('livon: poll error', error);
|
|
556
|
+
} finally{
|
|
557
|
+
inFlight = false;
|
|
558
|
+
setTimeout(tick, options.poll);
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
await tick();
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
await withRetry(execute);
|
|
565
|
+
ensureCommandRuntime();
|
|
566
|
+
if (commandRuntime) {
|
|
567
|
+
const commandExitCode = await commandRuntime.waitForExit;
|
|
568
|
+
process.exit(commandExitCode);
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
run().catch((error)=>{
|
|
572
|
+
console.error(error);
|
|
573
|
+
process.exit(1);
|
|
574
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@livon/cli",
|
|
3
|
+
"version": "0.27.0-rc.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"dist",
|
|
12
|
+
"THIRD_PARTY_NOTICES.md"
|
|
13
|
+
],
|
|
14
|
+
"bin": {
|
|
15
|
+
"livon": "bin/livon.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"msgpackr": "latest",
|
|
19
|
+
"ws": "latest",
|
|
20
|
+
"@livon/client": "0.27.0-rc.1",
|
|
21
|
+
"@livon/config": "0.27.0-rc.1"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@rslib/core": "latest",
|
|
25
|
+
"@types/ws": "latest",
|
|
26
|
+
"@typescript-eslint/eslint-plugin": "8.54.0",
|
|
27
|
+
"@typescript-eslint/parser": "8.54.0",
|
|
28
|
+
"@types/node": "latest",
|
|
29
|
+
"eslint": "9.0.0",
|
|
30
|
+
"typescript": "latest",
|
|
31
|
+
"@livon/config": "0.27.0-rc.1"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "rslib build",
|
|
35
|
+
"build:watch": "rslib dev",
|
|
36
|
+
"dev": "true",
|
|
37
|
+
"lint": "eslint .",
|
|
38
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
39
|
+
"test": "true",
|
|
40
|
+
"test:unit": "true",
|
|
41
|
+
"test:integration": "true"
|
|
42
|
+
}
|
|
43
|
+
}
|