@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.
@@ -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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/index.js').catch((error) => {
3
+ console.error(error);
4
+ process.exit(1);
5
+ });
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
+ });
@@ -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
+ }